Skip to main content

egui_components/
tree.rs

1//! Tree widget powered by [`egui_ltreeview`].
2//!
3//! Our prior hand-rolled tree didn't deliver working keyboard navigation, so
4//! we delegate to `egui_ltreeview` (MIT-licensed) which provides keyboard
5//! arrows, multi-select, drag-and-drop, and other features for free. Theming
6//! is applied automatically — `egui_components_theme::Theme::install` writes
7//! into `egui::Visuals`, which is what `egui_ltreeview` reads.
8//!
9//! Public types are re-exported below so callers can use them as
10//! `egui_components::Tree` etc. without depending on `egui_ltreeview`
11//! directly.
12//!
13//! Selected rows in `egui_ltreeview` use `visuals.selection.bg_fill`, which
14//! our theme sets to the bright text-selection color. That's correct for text
15//! inputs but visually loud for a tree. [`Tree::show_themed`] applies a small
16//! local visual tweak so the selected row uses the same soft secondary
17//! background that [`crate::ListItem`] uses, keeping the look consistent.
18//!
19//! ```ignore
20//! use egui_components::{Tree, TreeViewBuilder};
21//!
22//! let id = ui.make_persistent_id("file-tree");
23//! let (response, actions) = Tree::show_themed(ui, id, |builder| {
24//!     builder.dir("src", "src");
25//!     builder.leaf("src/lib.rs", "lib.rs");
26//!     builder.close_dir();
27//!     builder.leaf("Cargo.toml", "Cargo.toml");
28//! });
29//! for action in actions {
30//!     if let egui_components::TreeAction::SetSelected(ids) = action {
31//!         // update your own state from ids
32//!     }
33//! }
34//! ```
35
36use egui::{Id, Response, Ui};
37use egui_components_theme::Theme;
38
39pub use egui_ltreeview::{
40    Action as TreeAction, Activate, DirPosition, IndentHintStyle, NodeId, RowLayout, TreeView,
41    TreeViewBuilder, TreeViewSettings, TreeViewState,
42};
43
44/// Convenience alias — `egui_components::Tree::new(id)` returns the upstream
45/// [`TreeView`].
46pub type Tree<'cm, NodeIdType> = TreeView<'cm, NodeIdType>;
47
48/// Build + show a [`TreeView`] inside a scope that overrides
49/// `visuals.selection.bg_fill` to the theme's `secondary_background`, so the
50/// selected row matches the look of [`crate::ListItem`]. Returns the upstream
51/// `(Response, Vec<TreeAction>)`.
52pub fn show_themed<NodeIdType, F>(
53    ui: &mut Ui,
54    id: Id,
55    body: F,
56) -> (Response, Vec<TreeAction<NodeIdType>>)
57where
58    NodeIdType: NodeId + Send + Sync + 'static,
59    F: FnOnce(&mut TreeViewBuilder<'_, NodeIdType>),
60{
61    // `egui_ltreeview`'s `draw_indent_hint` calls `Rect::clamp` on the current
62    // clip rect, which panics if that rect is "negative" (`min > max`). When
63    // the tree lives inside a scroll area and is scrolled fully out of view,
64    // the clip rect collapses to a non-overlapping (negative) rect. Skip
65    // rendering in that case rather than letting the dependency panic — the
66    // tree simply isn't visible then anyway.
67    if ui.clip_rect().is_negative() {
68        let response = ui.allocate_response(egui::Vec2::ZERO, egui::Sense::hover());
69        return (response, Vec::new());
70    }
71
72    let theme = Theme::get(ui.ctx());
73    let result = ui.scope(|ui| {
74        ui.visuals_mut().selection.bg_fill = theme.colors.secondary_background;
75        TreeView::new(id).show(ui, body)
76    });
77    result.inner
78}