perf(tui): bound the name scroll walk by the column width

Scrolled rows cost 31.7ms per frame at 30k results against 13.3ms on
develop, because every row segmented its whole filename into grapheme
clusters. Walk back from the end for at most one column of cells and
forward for at most the offset, so a row costs the column width rather
than the filename, and drop unicode-truncate for unicode-width. A tail
never starts on a combining mark.
This commit is contained in:
Michel de Graaf
2026-09-06 21:42:36 +02:00
parent 3193c73f5b
commit 06bc48fcd9
3 changed files with 41 additions and 8 deletions
Generated
+1 -1
View File
@@ -2564,7 +2564,7 @@ dependencies = [
"soulseek-rs-lib",
"tempfile",
"toml",
"unicode-truncate",
"unicode-width",
]
[[package]]
+1 -1
View File
@@ -30,7 +30,7 @@ soulseek-rs-lib = { version = "14.1.2", path = "../soulseek-rs-lib" }
clap = { version = "4.6.2", features = ["derive", "color", "wrap_help", "env"] }
clap_complete = "4.6.7"
ratatui = "0.30.2"
unicode-truncate = "2"
unicode-width = "0.2"
color-eyre = "0.6.5"
dotenv = "0.15.0"
igd-next = "0.17"
+39 -6
View File
@@ -11,7 +11,7 @@ use ratatui::{
widgets::{Cell, HighlightSpacing, Paragraph, Row, Table, TableState},
};
use std::collections::HashSet;
use unicode_truncate::UnicodeTruncateStr;
use unicode_width::UnicodeWidthChar;
const VERSION: &str = env!("CARGO_PKG_VERSION");
@@ -61,13 +61,37 @@ fn scroll_name(name: &str, offset: usize, width: usize) -> Line<'_> {
if offset == 0 {
return Line::from(name);
}
let cells = Span::raw(name).width();
let skip = offset.min(overflow(cells, width));
if skip == 0 {
let mut tail_cells = 0;
let mut furthest = name.len();
for (index, ch) in name.char_indices().rev() {
let cells = ch.width().unwrap_or(0);
if tail_cells + cells >= width {
if index == 0 && tail_cells + cells == width {
return Line::from(name);
}
break;
}
tail_cells += cells;
furthest = index;
}
if furthest == 0 {
return Line::from(name);
}
let (tail, _) = name.unicode_truncate_start(cells - skip);
Line::from(vec![Span::raw("…"), Span::raw(tail)])
let mut skipped = 0;
let mut start = furthest;
for (index, ch) in name.char_indices() {
if index >= furthest || skipped >= offset {
start = index;
break;
}
skipped += ch.width().unwrap_or(0);
}
while let Some(ch) = name[start..].chars().next()
&& ch.width() == Some(0)
{
start += ch.len_utf8();
}
Line::from(vec![Span::raw("…"), Span::raw(&name[start..])])
}
/// Whether the rendered row `display_idx` is selected. `selected_indices` holds
@@ -281,6 +305,15 @@ mod tests {
assert!(!screen.contains("亿東"), "{screen}");
}
#[test]
fn a_scrolled_name_never_starts_with_a_combining_mark() {
let name = format!("{}e\u{301}{}", "x".repeat(5), "y".repeat(20));
let items = [file(&name)];
let screen = render_rows(&items, 6);
assert!(screen.contains("…yyyyyyyyyy"), "{screen}");
assert!(!screen.contains("…\u{301}"), "{screen}");
}
#[test]
fn an_unscrolled_name_starts_at_its_beginning() {
let items = [file("abcdefghijklmnopqrstuvwxyz0123456789ABCD")];