perf(tui): flatten a browsed share once and draw one page of it

Moving through a browsed share felt laggy for the same reason the
results did: the popup re-flattened the whole expanded tree on every
frame, cloning every row's name and path, then built a table row for
each, and every keypress flattened it twice more. A heavy sharer's
tree runs to tens of thousands of rows once a few folders are open.

The flattened rows now live on the browse state and are rebuilt only
when a folder opens or closes, and the popup draws the visible page
alone. In the harness a 30,000-row tree went from 11ms to 0.35ms a
frame and a keypress from 2.7ms to under a microsecond.

The downloads pane had the same shape (every transfer became a row
each frame, and a select-all can queue ten thousand), so it takes the
same page window, which now lives beside page_of for every pane to
share.
This commit is contained in:
Michel de Graaf
2026-09-08 11:24:36 +02:00
parent 1fcde0a78b
commit 0931bb01b4
6 changed files with 129 additions and 68 deletions
+25 -5
View File
@@ -4,8 +4,8 @@ use crate::ui::{
GLYPH_ACTIVE, GLYPH_DONE, GLYPH_FAILED, GLYPH_QUEUED, HIGHLIGHT_SYMBOL,
accent_style, body_style, dimmed_style, download_status_glyph, error_style,
format_bytes, format_speed, header_style, inactive_style, info_style,
pane_block, pane_title, primary_style, row_highlight_style, success_style,
warning_style,
page_of, pane_block, pane_title, primary_style, row_highlight_style,
success_style, visible_range, warning_style,
};
use ratatui::{
Frame,
@@ -81,7 +81,23 @@ pub fn render_downloads_pane(
.height(1);
let name_width = column_width(area, &WIDTHS, NAME_COLUMN);
let mut rows: Vec<Row> = downloads
// Downloads come first, uploads after; one page of the two together.
let total = downloads.len() + uploads.len();
if let Some(selected) = table_state.selected_mut() {
*selected = (*selected).min(total - 1);
}
let window = visible_range(
table_state.offset(),
table_state.selected(),
total,
page_of(Some(area), 1),
);
let start = window.start;
let split = downloads.len();
let shown_downloads = &downloads[start.min(split)..window.end.min(split)];
let shown_uploads =
&uploads[start.saturating_sub(split)..window.end.saturating_sub(split)];
let mut rows: Vec<Row> = shown_downloads
.iter()
.map(|download_entry| {
let download = &download_entry.download;
@@ -153,7 +169,7 @@ pub fn render_downloads_pane(
})
.collect();
rows.extend(uploads.iter().map(|upload| {
rows.extend(shown_uploads.iter().map(|upload| {
let (status_icon, status_style) = match &upload.status {
UploadStatus::Queued(_) => (GLYPH_QUEUED, warning_style()),
UploadStatus::InProgress => (GLYPH_ACTIVE, accent_style()),
@@ -213,5 +229,9 @@ pub fn render_downloads_pane(
focused,
)));
frame.render_stateful_widget(table, area, table_state);
// The table holds one page, so the state it gets is shifted onto it.
let mut page_state = TableState::default()
.with_selected(table_state.selected().map(|selected| selected - start));
frame.render_stateful_widget(table, area, &mut page_state);
*table_state.offset_mut() = start;
}