Skip to content

Commit 3912417

Browse files
committed
fix: tree is a mode toggle, not a separate tab; preserve expanded state
Rework tree view UX per feedback: - Tree is no longer a separate ViewKind/tab. Instead, 't' toggles tree_mode which applies to both Top and Processes views. Tab cycle stays Flamegraph -> Top -> Processes (-> Output). - When tree_mode is on, Top and Processes render as an expandable call tree. When off, they render as flat lists (original behavior). - Expanded node state now persists across live flamegraph refreshes. Node IDs are remapped from old to new flamegraph by name matching in handle_flamegraph_replacement(), same pattern as zoom/selection. Updated help tags show 't tree mode' in list views and 't list mode' when tree is active.
1 parent 5e38ddc commit 3912417

3 files changed

Lines changed: 51 additions & 41 deletions

File tree

profile-bee-tui/src/handler.rs

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,21 +26,23 @@ pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> {
2626
pub fn handle_command(key_event: KeyEvent, app: &mut App) -> AppResult<()> {
2727
let mut key_handled = handle_command_generic(key_event, app)?;
2828
if !key_handled {
29-
match app.flamegraph_state().view_kind {
29+
let view = app.flamegraph_state().view_kind;
30+
let tree_mode = app.flamegraph_view.state.tree_mode;
31+
match view {
3032
ViewKind::FlameGraph => {
3133
key_handled = handle_command_flamegraph(key_event, app)?;
3234
}
35+
ViewKind::Table | ViewKind::ProcessList if tree_mode => {
36+
key_handled = handle_command_tree_view(key_event, app)?;
37+
}
3338
ViewKind::Table => {
3439
key_handled = handle_command_table(key_event, app)?;
3540
}
36-
ViewKind::Output => {
37-
key_handled = handle_command_output(key_event, app)?;
38-
}
3941
ViewKind::ProcessList => {
4042
key_handled = handle_command_process_list(key_event, app)?;
4143
}
42-
ViewKind::TreeView => {
43-
key_handled = handle_command_tree_view(key_event, app)?;
44+
ViewKind::Output => {
45+
key_handled = handle_command_output(key_event, app)?;
4446
}
4547
}
4648
}
@@ -97,11 +99,13 @@ pub fn handle_command_generic(key_event: KeyEvent, app: &mut App) -> AppResult<b
9799
app.toggle_debug();
98100
}
99101
KeyCode::Char('t') => {
100-
if app.flamegraph_state().view_kind == ViewKind::TreeView {
101-
app.flamegraph_view.state.view_kind = ViewKind::FlameGraph;
102+
app.flamegraph_view.state.tree_mode = !app.flamegraph_view.state.tree_mode;
103+
let mode_str = if app.flamegraph_view.state.tree_mode {
104+
"on"
102105
} else {
103-
app.flamegraph_view.state.view_kind = ViewKind::TreeView;
104-
}
106+
"off"
107+
};
108+
app.set_transient_message(&format!("Tree mode: {}", mode_str));
105109
}
106110
_ => {
107111
key_handled = false;

profile-bee-tui/src/state.rs

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@ pub enum ViewKind {
4646
Table,
4747
Output,
4848
ProcessList,
49-
TreeView,
5049
}
5150

5251
#[derive(Default, Debug, Clone)]
@@ -74,6 +73,9 @@ pub struct FlameGraphState {
7473
/// When true, stacks are prefixed with "process_name (pid)" root frames
7574
/// to split the flamegraph by process. Toggled with 'p'.
7675
pub pid_mode: bool,
76+
/// When true, the Top/Processes views show an expandable tree instead
77+
/// of a flat list. Toggled with 't'.
78+
pub tree_mode: bool,
7779
pub view_kind: ViewKind,
7880
pub table_state: TableState,
7981
pub process_list_state: TableState,
@@ -117,6 +119,7 @@ impl Default for FlameGraphState {
117119
search_pattern: None,
118120
freeze: false,
119121
pid_mode: false,
122+
tree_mode: false,
120123
view_kind: ViewKind::FlameGraph,
121124
table_state: TableState::default(),
122125
process_list_state: TableState::default(),
@@ -161,7 +164,6 @@ impl FlameGraphState {
161164
ViewKind::Table => ViewKind::ProcessList,
162165
ViewKind::ProcessList => ViewKind::FlameGraph,
163166
ViewKind::Output => ViewKind::FlameGraph,
164-
ViewKind::TreeView => ViewKind::FlameGraph,
165167
};
166168
}
167169

@@ -173,7 +175,6 @@ impl FlameGraphState {
173175
ViewKind::Table => ViewKind::ProcessList,
174176
ViewKind::ProcessList => ViewKind::Output,
175177
ViewKind::Output => ViewKind::FlameGraph,
176-
ViewKind::TreeView => ViewKind::FlameGraph,
177178
};
178179
}
179180

@@ -202,6 +203,15 @@ impl FlameGraphState {
202203
if let Some(p) = &self.search_pattern {
203204
new.set_hits(p);
204205
}
206+
207+
// Remap expanded tree node IDs from old to new flamegraph.
208+
// Nodes are matched by full name so expanded state survives data refreshes.
209+
let old_expanded = std::mem::take(&mut self.tree_view_state.expanded);
210+
for old_id in old_expanded {
211+
if let Some(new_id) = Self::get_new_stack_id(&old_id, old, new) {
212+
self.tree_view_state.expanded.insert(new_id);
213+
}
214+
}
205215
}
206216

207217
fn get_new_stack_id(

profile-bee-tui/src/ui.rs

Lines changed: 24 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -159,12 +159,14 @@ impl<'a> FlamelensWidget<'a> {
159159

160160
if self.is_flamegraph_view() {
161161
self.render_flamegraph(split[0], buf, state);
162-
} else if self.is_table_view() {
163-
self.render_table(split[0], buf);
164-
} else if self.is_process_list_view() {
165-
self.render_process_list(split[0], buf);
166-
} else if self.is_tree_view() {
167-
self.render_tree_view(split[0], buf);
162+
} else if self.is_table_view() || self.is_process_list_view() {
163+
if self.app.flamegraph_state().tree_mode {
164+
self.render_tree_view(split[0], buf);
165+
} else if self.is_table_view() {
166+
self.render_table(split[0], buf);
167+
} else {
168+
self.render_process_list(split[0], buf);
169+
}
168170
}
169171

170172
self.render_output_panel(split[1], buf);
@@ -178,12 +180,14 @@ impl<'a> FlamelensWidget<'a> {
178180
state.output_view_height = main_area.height;
179181
} else if self.is_flamegraph_view() {
180182
self.render_flamegraph(main_area, buf, state);
181-
} else if self.is_process_list_view() {
182-
self.render_process_list(main_area, buf);
183-
} else if self.is_tree_view() {
184-
self.render_tree_view(main_area, buf);
185-
} else {
186-
self.render_table(main_area, buf);
183+
} else if self.is_table_view() || self.is_process_list_view() {
184+
if self.app.flamegraph_state().tree_mode {
185+
self.render_tree_view(main_area, buf);
186+
} else if self.is_table_view() {
187+
self.render_table(main_area, buf);
188+
} else {
189+
self.render_process_list(main_area, buf);
190+
}
187191
}
188192
let flamegraph_render_time = tic.elapsed();
189193

@@ -235,23 +239,25 @@ impl<'a> FlamelensWidget<'a> {
235239
help_tags.add("j/k", "scroll");
236240
help_tags.add("f/b", "page up/down");
237241
help_tags.add("G/g", "bottom/top");
238-
} else if self.is_process_list_view() {
239-
help_tags.add("j/k", "move cursor");
240-
help_tags.add("enter", "zoom into process");
241-
help_tags.add("esc/p", "back to flamegraph");
242-
} else if self.is_tree_view() {
242+
} else if self.app.flamegraph_state().tree_mode {
243+
// Tree mode (applies to both Top and Processes views)
243244
help_tags.add("j/k", "move cursor");
244245
help_tags.add("enter/l", "expand");
245246
help_tags.add("h", "collapse/parent");
246247
help_tags.add("esc", "collapse all");
247-
help_tags.add("t", "back to flamegraph");
248+
help_tags.add("t", "list mode");
249+
} else if self.is_process_list_view() {
250+
help_tags.add("j/k", "move cursor");
251+
help_tags.add("enter", "zoom into process");
252+
help_tags.add("t", "tree mode");
248253
} else {
249254
// Table view
250255
help_tags.add("j/k", "move cursor");
251256
help_tags.add("f/b", "scroll");
252257
help_tags.add("1", "sort by total");
253258
help_tags.add("2", "sort by own");
254259
help_tags.add("/", "filter");
260+
help_tags.add("t", "tree mode");
255261
if self.app.has_output() {
256262
help_tags.add("o", "output panel");
257263
}
@@ -846,12 +852,6 @@ impl<'a> FlamelensWidget<'a> {
846852
ViewKind::ProcessList,
847853
self.app.flamegraph_state().view_kind,
848854
));
849-
header_bottom_title_spans.push(Span::from(" | "));
850-
header_bottom_title_spans.push(_get_view_kind_span(
851-
"Tree",
852-
ViewKind::TreeView,
853-
self.app.flamegraph_state().view_kind,
854-
));
855855
if self.app.has_output() {
856856
header_bottom_title_spans.push(Span::from(" | "));
857857
header_bottom_title_spans.push(_get_view_kind_span(
@@ -1030,10 +1030,6 @@ impl<'a> FlamelensWidget<'a> {
10301030
fn is_process_list_view(&self) -> bool {
10311031
self.view_kind() == ViewKind::ProcessList
10321032
}
1033-
1034-
fn is_tree_view(&self) -> bool {
1035-
self.view_kind() == ViewKind::TreeView
1036-
}
10371033
}
10381034

10391035
struct HelpTags {

0 commit comments

Comments
 (0)