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:
@@ -1,8 +1,8 @@
|
||||
use crate::models::{BrowseState, BrowseStatus, BrowseTabs};
|
||||
use crate::ui::{
|
||||
HIGHLIGHT_SYMBOL, body_style, dimmed_style, error_style, format_bytes,
|
||||
get_spinner_char, highlight_style, pane_block, primary_style,
|
||||
row_highlight_style, warning_style,
|
||||
get_spinner_char, highlight_style, page_of, pane_block, primary_style,
|
||||
row_highlight_style, visible_range, warning_style,
|
||||
};
|
||||
use ratatui::{
|
||||
Frame,
|
||||
@@ -106,8 +106,15 @@ fn render_browse_one(
|
||||
frame.render_widget(Paragraph::new(text).block(block), area);
|
||||
}
|
||||
BrowseStatus::Loaded => {
|
||||
let rows: Vec<Row> = browse
|
||||
.rows()
|
||||
let all = browse.rows();
|
||||
let window = visible_range(
|
||||
table_state.offset(),
|
||||
Some(browse.selected_row),
|
||||
all.len(),
|
||||
page_of(Some(area), 0),
|
||||
);
|
||||
let start = window.start;
|
||||
let rows: Vec<Row> = all[window]
|
||||
.iter()
|
||||
.map(|row| {
|
||||
let indent = " ".repeat(row.depth);
|
||||
@@ -142,8 +149,13 @@ fn render_browse_one(
|
||||
.highlight_spacing(HighlightSpacing::Always)
|
||||
.block(block);
|
||||
|
||||
table_state.select(Some(browse.selected_row));
|
||||
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(Some(
|
||||
browse.selected_row.min(all.len().saturating_sub(1)) - start,
|
||||
));
|
||||
frame.render_stateful_widget(table, area, &mut page_state);
|
||||
*table_state.offset_mut() = start;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ use crate::models::FileDisplayData;
|
||||
use crate::ui::{
|
||||
BYTES_PER_MB, HIGHLIGHT_SYMBOL, body_style, dimmed_style, format_bytes,
|
||||
header_style, info_style, page_of, pane_block, pane_title,
|
||||
row_highlight_style, success_style, warning_style,
|
||||
row_highlight_style, success_style, visible_range, warning_style,
|
||||
};
|
||||
use ratatui::{
|
||||
Frame,
|
||||
@@ -11,7 +11,7 @@ use ratatui::{
|
||||
text::{Line, Span},
|
||||
widgets::{Cell, HighlightSpacing, Paragraph, Row, Table, TableState},
|
||||
};
|
||||
use std::{collections::HashSet, ops::Range};
|
||||
use std::collections::HashSet;
|
||||
|
||||
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
@@ -85,26 +85,6 @@ fn row_is_selected(
|
||||
selected_indices.contains(&original)
|
||||
}
|
||||
|
||||
/// The page a `height`-tall table shows, by ratatui's rule (its
|
||||
/// `visible_rows` is private): the offset, moved only as far as keeps the
|
||||
/// selection in view. A selection past the end counts as the last row.
|
||||
fn visible_range(
|
||||
offset: usize,
|
||||
selected: Option<usize>,
|
||||
len: usize,
|
||||
height: usize,
|
||||
) -> Range<usize> {
|
||||
let height = height.max(1);
|
||||
let last = len.saturating_sub(1);
|
||||
let start = match selected.map(|selected| selected.min(last)) {
|
||||
Some(selected) => {
|
||||
offset.clamp((selected + 1).saturating_sub(height), selected)
|
||||
}
|
||||
None => offset.min(last),
|
||||
};
|
||||
start..(start + height).min(len)
|
||||
}
|
||||
|
||||
pub fn render_results_pane(
|
||||
frame: &mut Frame,
|
||||
area: Rect,
|
||||
@@ -250,9 +230,7 @@ pub fn render_results_pane(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
ResultsPaneParams, render_results_pane, row_is_selected, visible_range,
|
||||
};
|
||||
use super::{ResultsPaneParams, render_results_pane, row_is_selected};
|
||||
use crate::models::FileDisplayData;
|
||||
use ratatui::{Terminal, backend::TestBackend, widgets::TableState};
|
||||
use std::collections::HashSet;
|
||||
@@ -371,15 +349,6 @@ mod tests {
|
||||
assert!(!screen.contains("…"), "{screen}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_page_follows_the_selection_and_is_never_longer_than_the_pane() {
|
||||
assert_eq!(visible_range(500, Some(505), 10_000, 10), 500..510);
|
||||
assert_eq!(visible_range(0, Some(9_999), 10_000, 10), 9_990..10_000);
|
||||
assert_eq!(visible_range(500, Some(499), 10_000, 10), 499..509);
|
||||
assert_eq!(visible_range(9_995, None, 10_000, 10), 9_995..10_000);
|
||||
assert_eq!(visible_range(0, Some(9), 3, 2), 1..3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_far_selection_is_drawn_from_its_own_page() {
|
||||
let items: Vec<FileDisplayData> =
|
||||
|
||||
Reference in New Issue
Block a user