From c79447ddd08340eb42dabceac5214dfff65b94f4 Mon Sep 17 00:00:00 2001 From: Ultradesu Date: Wed, 10 Jun 2026 23:46:19 +0100 Subject: [PATCH] Minor UI fixes. --- Cargo.lock | 3 +- Cargo.toml | 1 + src/ui/global.rs | 77 +++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 79 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c7de43d..824523f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1180,7 +1180,7 @@ checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" [[package]] name = "furumi_tui" -version = "0.1.0" +version = "0.1.1" dependencies = [ "anyhow", "arboard", @@ -1204,6 +1204,7 @@ dependencies = [ "toml", "tracing", "tracing-subscriber", + "unicode-width", "windows-sys 0.61.2", ] diff --git a/Cargo.toml b/Cargo.toml index 96aa214..62c4356 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,7 @@ tokio = { version = "1.52.3", features = ["rt-multi-thread", "macros", "sync", " toml = "1.1.2" tracing = "0.1.44" tracing-subscriber = { version = "0.3.23", features = ["env-filter"] } +unicode-width = "0.2.2" [target.'cfg(target_os="macos")'.dependencies] core-foundation = "0.10.1" diff --git a/src/ui/global.rs b/src/ui/global.rs index 4b0dd40..8648691 100644 --- a/src/ui/global.rs +++ b/src/ui/global.rs @@ -3,6 +3,7 @@ use ratatui::layout::{Alignment, Constraint, Layout, Rect}; use ratatui::style::{Color, Style}; use ratatui::text::{Line, Span}; use ratatui::widgets::{Block, Paragraph, Row, Table}; +use unicode_width::{UnicodeWidthChar, UnicodeWidthStr}; use super::{art, theme}; use crate::api::models::{ArtistCard, ReleaseCard}; @@ -12,6 +13,10 @@ use crate::app::state::{ }; use crate::art::cache_key; +const TILE_MARQUEE_STEP_MS: u128 = 250; +const TILE_MARQUEE_PAUSE_STEPS: u128 = 4; +const TILE_MARQUEE_GAP: &str = " "; + pub fn draw(frame: &mut Frame, area: Rect, state: &AppState) { match state.global.stack.last() { None => draw_grid(frame, area, state), @@ -102,7 +107,10 @@ fn draw_tile( height: 1, ..inner }; - frame.render_widget(Paragraph::new(Line::raw(title.to_string())), name_area); + frame.render_widget( + Paragraph::new(Line::raw(tile_title(title, name_area.width, selected))), + name_area, + ); if selected { frame.buffer_mut().set_style(name_area, theme::tab_active()); } @@ -123,6 +131,73 @@ fn draw_tile( } } +fn tile_title(title: &str, width: u16, selected: bool) -> String { + let width = usize::from(width); + if width == 0 { + return String::new(); + } + if !selected || UnicodeWidthStr::width(title) <= width { + return title.to_string(); + } + marquee_window(title, width) +} + +fn marquee_window(title: &str, width: usize) -> String { + let stream = format!("{title}{TILE_MARQUEE_GAP}"); + let cells: Vec<(char, usize)> = stream + .chars() + .map(|ch| (ch, UnicodeWidthChar::width(ch).unwrap_or(0))) + .collect(); + let total_width: usize = cells.iter().map(|(_, width)| *width).sum(); + if total_width == 0 { + return title.to_string(); + } + + let phase = marquee_phase(total_width); + let mut skipped = 0usize; + let mut index = 0usize; + while index < cells.len() { + let next = skipped + cells[index].1; + if next > phase { + break; + } + skipped = next; + index += 1; + } + + let mut out = String::new(); + let mut used = 0usize; + let mut steps = 0usize; + while used < width && steps < cells.len() + width { + let (ch, ch_width) = cells[(index + steps) % cells.len()]; + steps += 1; + if ch_width == 0 { + out.push(ch); + continue; + } + if used + ch_width > width { + continue; + } + out.push(ch); + used += ch_width; + } + out +} + +fn marquee_phase(total_width: usize) -> usize { + let tick = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|duration| duration.as_millis() / TILE_MARQUEE_STEP_MS) + .unwrap_or(0); + let cycle = total_width as u128 + TILE_MARQUEE_PAUSE_STEPS; + let phase = tick % cycle; + if phase < TILE_MARQUEE_PAUSE_STEPS { + 0 + } else { + (phase - TILE_MARQUEE_PAUSE_STEPS) as usize + } +} + /// One selectable row: left content, optional right-aligned suffix, full-row /// highlight when selected. fn draw_row(frame: &mut Frame, area: Rect, line: Line, right: Option, selected: bool) {