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:
@@ -267,7 +267,10 @@ pub struct BrowseState {
|
||||
pub username: String,
|
||||
pub status: BrowseStatus,
|
||||
pub tree: Vec<BrowseNode>,
|
||||
pub expanded: HashSet<String>,
|
||||
expanded: HashSet<String>,
|
||||
/// The tree flattened for the current expansion, kept between frames:
|
||||
/// a heavy sharer's tree runs to tens of thousands of rows.
|
||||
rows: Vec<BrowseRow>,
|
||||
pub selected_row: usize,
|
||||
pub file_count: usize,
|
||||
pub folder_count: usize,
|
||||
@@ -282,6 +285,7 @@ impl BrowseState {
|
||||
status: BrowseStatus::Loading,
|
||||
tree: Vec::new(),
|
||||
expanded: HashSet::new(),
|
||||
rows: Vec::new(),
|
||||
selected_row: 0,
|
||||
file_count: 0,
|
||||
folder_count: 0,
|
||||
@@ -310,12 +314,30 @@ impl BrowseState {
|
||||
self.file_count = built.file_count;
|
||||
self.folder_count = built.folder_count;
|
||||
self.selected_row = 0;
|
||||
self.refresh();
|
||||
}
|
||||
|
||||
/// The flattened visible rows for the current expansion state.
|
||||
#[must_use]
|
||||
pub fn rows(&self) -> Vec<BrowseRow> {
|
||||
flatten_browse(&self.tree, &self.expanded)
|
||||
pub fn rows(&self) -> &[BrowseRow] {
|
||||
&self.rows
|
||||
}
|
||||
|
||||
/// Open or close the folder at `path`.
|
||||
pub fn set_expanded(&mut self, path: &str, expanded: bool) {
|
||||
if expanded {
|
||||
self.expanded.insert(path.to_string());
|
||||
} else {
|
||||
self.expanded.remove(path);
|
||||
}
|
||||
self.refresh();
|
||||
}
|
||||
|
||||
/// Re-flatten after the tree or the expansion changed under `rows`.
|
||||
pub fn refresh(&mut self) {
|
||||
self.rows = flatten_browse(&self.tree, &self.expanded);
|
||||
self.selected_row =
|
||||
self.selected_row.min(self.rows.len().saturating_sub(1));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -579,6 +601,19 @@ mod tests {
|
||||
assert_eq!(tabs.retry_active(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expanding_and_collapsing_refresh_the_rows_and_keep_the_selection() {
|
||||
let mut state = BrowseState::loading("bob".to_string());
|
||||
state.load(&[dir("music\\album", &[("a.mp3", 1), ("b.mp3", 2)])]);
|
||||
assert_eq!(state.rows().len(), 2, "music open, album closed");
|
||||
state.set_expanded("music\\album", true);
|
||||
assert_eq!(state.rows().len(), 4);
|
||||
state.selected_row = 3;
|
||||
state.set_expanded("music\\album", false);
|
||||
assert_eq!(state.rows().len(), 2);
|
||||
assert_eq!(state.selected_row, 1, "pulled back onto the last row");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn browse_state_load_auto_expands_top_level() {
|
||||
let mut state = BrowseState::loading("bob".to_string());
|
||||
|
||||
Reference in New Issue
Block a user