use egui::{
emath, remap, vec2, Align, CursorIcon, Id, Label, Layout, Popup, PopupAnchor, PopupKind, Rect,
Shape, Stroke, Ui, UiBuilder, Vec2, WidgetText,
};
use crate::{NodeId, RowLayout, TreeViewSettings};
pub trait NodeConfig<NodeIdType> {
fn id(&self) -> &NodeIdType;
fn is_dir(&self) -> bool;
fn label(&mut self, ui: &mut Ui);
fn flatten(&self) -> bool {
false
}
fn default_open(&self) -> bool {
true
}
fn drop_allowed(&self) -> bool {
self.is_dir()
}
fn activatable(&self) -> bool {
!self.is_dir()
}
fn node_height(&self) -> Option<f32> {
None
}
fn has_custom_icon(&self) -> bool {
false
}
#[allow(unused)]
fn icon(&mut self, ui: &mut Ui) {}
fn has_custom_closer(&self) -> bool {
false
}
#[allow(unused)]
fn closer(&mut self, ui: &mut Ui, closer_state: CloserState) {}
fn has_context_menu(&self) -> bool {
false
}
#[allow(unused)]
fn context_menu(&mut self, ui: &mut Ui) {}
}
pub struct NodeBuilder<'add_ui, NodeIdType> {
id: NodeIdType,
is_dir: bool,
flatten: bool,
default_open: bool,
drop_allowed: bool,
activatable: bool,
node_height: Option<f32>,
#[allow(clippy::type_complexity)]
icon: Option<Box<dyn FnMut(&mut Ui) + 'add_ui>>,
#[allow(clippy::type_complexity)]
closer: Option<Box<dyn FnMut(&mut Ui, CloserState) + 'add_ui>>,
#[allow(clippy::type_complexity)]
label: Option<Box<dyn FnMut(&mut Ui) + 'add_ui>>,
#[allow(clippy::type_complexity)]
context_menu: Option<Box<dyn FnMut(&mut Ui) + 'add_ui>>,
}
impl<'add_ui, NodeIdType: NodeId> NodeBuilder<'add_ui, NodeIdType> {
pub fn leaf(id: NodeIdType) -> Self {
Self {
id,
is_dir: false,
flatten: false,
drop_allowed: false,
activatable: true,
node_height: None,
icon: None,
closer: None,
label: None,
context_menu: None,
default_open: true,
}
}
pub fn dir(id: NodeIdType) -> Self {
Self {
id,
is_dir: true,
flatten: false,
drop_allowed: true,
activatable: false,
node_height: None,
icon: None,
closer: None,
label: None,
context_menu: None,
default_open: true,
}
}
pub fn flatten(mut self, flatten: bool) -> Self {
self.flatten = flatten;
self
}
pub fn default_open(mut self, default_open: bool) -> Self {
self.default_open = default_open;
self
}
pub fn drop_allowed(mut self, drop_allowed: bool) -> Self {
self.drop_allowed = drop_allowed;
self
}
pub fn activatable(mut self, activatable: bool) -> Self {
self.activatable = activatable;
self
}
pub fn height(mut self, height: f32) -> Self {
self.node_height = Some(height);
self
}
pub fn icon(
mut self,
add_icon: impl FnMut(&mut Ui) + 'add_ui,
) -> NodeBuilder<'add_ui, NodeIdType> {
self.icon = Some(Box::new(add_icon));
self
}
pub fn closer(
mut self,
add_closer: impl FnMut(&mut Ui, CloserState) + 'add_ui,
) -> NodeBuilder<'add_ui, NodeIdType> {
self.closer = Some(Box::new(add_closer));
self
}
pub fn label(self, text: impl Into<WidgetText> + 'add_ui) -> Self {
let widget_text = text.into();
self.label_ui(move |ui| {
ui.add(Label::new(widget_text.clone()).selectable(false));
})
}
pub fn label_ui(
mut self,
add_label: impl FnMut(&mut Ui) + 'add_ui,
) -> NodeBuilder<'add_ui, NodeIdType> {
self.label = Some(Box::new(add_label));
self
}
pub fn context_menu(
mut self,
add_context_menu: impl FnMut(&mut Ui) + 'add_ui,
) -> NodeBuilder<'add_ui, NodeIdType> {
self.context_menu = Some(Box::new(add_context_menu));
self
}
}
impl<NodeIdType: NodeId> NodeConfig<NodeIdType> for NodeBuilder<'_, NodeIdType> {
fn id(&self) -> &NodeIdType {
&self.id
}
fn is_dir(&self) -> bool {
self.is_dir
}
fn flatten(&self) -> bool {
self.flatten
}
fn default_open(&self) -> bool {
self.default_open
}
fn drop_allowed(&self) -> bool {
self.drop_allowed
}
fn activatable(&self) -> bool {
self.activatable
}
fn node_height(&self) -> Option<f32> {
self.node_height
}
fn has_custom_icon(&self) -> bool {
self.icon.is_some()
}
fn icon(&mut self, ui: &mut Ui) {
if let Some(icon) = &mut self.icon {
(icon)(ui);
}
}
fn has_custom_closer(&self) -> bool {
self.closer.is_some()
}
fn closer(&mut self, ui: &mut Ui, closer_state: CloserState) {
if let Some(closer) = &mut self.closer {
(closer)(ui, closer_state);
}
}
fn label(&mut self, ui: &mut Ui) {
if let Some(label) = &mut self.label {
(label)(ui);
}
}
fn has_context_menu(&self) -> bool {
self.context_menu.is_some()
}
fn context_menu(&mut self, ui: &mut Ui) {
if let Some(context_menu) = &mut self.context_menu {
(context_menu)(ui);
}
}
}
pub(crate) struct Node<'config, NodeIdType> {
pub id: NodeIdType,
pub is_dir: bool,
pub is_open: bool,
pub drop_allowed: bool,
pub activatable: bool,
pub node_height: f32,
pub indent: usize,
pub config: &'config mut dyn NodeConfig<NodeIdType>,
}
impl<'config, NodeIdType: NodeId> Node<'config, NodeIdType> {
pub fn from_config(
is_open: bool,
default_node_height: f32,
indent: usize,
config: &'config mut dyn NodeConfig<NodeIdType>,
) -> Self {
Self {
id: config.id().clone(),
is_dir: config.is_dir(),
is_open,
drop_allowed: config.drop_allowed(),
activatable: config.activatable(),
node_height: config.node_height().unwrap_or(default_node_height),
indent,
config,
}
}
pub fn show_node(
&mut self,
ui: &mut Ui,
settings: &TreeViewSettings,
row_rect: Rect,
selected: bool,
has_focus: bool,
) -> (Option<Rect>, Option<Rect>, Rect) {
let mut ui = ui.new_child(
UiBuilder::new()
.max_rect(row_rect)
.layout(Layout::left_to_right(egui::Align::Center)),
);
let fg_stroke = if selected && has_focus {
ui.visuals().selection.stroke
} else if selected {
ui.visuals().widgets.inactive.fg_stroke
} else {
ui.visuals().widgets.noninteractive.fg_stroke
};
ui.visuals_mut().widgets.noninteractive.fg_stroke = fg_stroke;
ui.visuals_mut().widgets.inactive.fg_stroke = fg_stroke;
let original_item_spacing = ui.spacing().item_spacing;
ui.spacing_mut().item_spacing = Vec2::ZERO;
let (reserve_closer, draw_closer, reserve_icon, draw_icon) = match settings.row_layout {
RowLayout::Compact => (self.is_dir, self.is_dir, false, false),
RowLayout::CompactAlignedLabels => (
self.is_dir,
self.is_dir,
!self.is_dir,
!self.is_dir && self.config.has_custom_icon(),
),
RowLayout::AlignedIcons => (
true,
self.is_dir,
self.config.has_custom_icon(),
self.config.has_custom_icon(),
),
RowLayout::AlignedIconsAndLabels => {
(true, self.is_dir, true, self.config.has_custom_icon())
}
};
ui.set_height(self.node_height);
ui.add_space(original_item_spacing.x);
ui.add_space(ui.spacing().item_spacing.x);
ui.add_space(self.indent as f32 * settings.override_indent.unwrap_or(ui.spacing().indent));
let closer = draw_closer.then(|| {
let (small_rect, big_rect) = ui
.spacing()
.icon_rectangles(ui.available_rect_before_wrap());
let res = ui.scope_builder(UiBuilder::new().max_rect(big_rect), |ui| {
let is_hovered = ui
.input(|i| i.pointer.latest_pos())
.is_some_and(|pos| ui.max_rect().contains(pos));
if is_hovered {
ui.ctx().set_cursor_icon(CursorIcon::PointingHand);
}
if self.config.has_custom_closer() {
self.config.closer(
ui,
CloserState {
is_open: self.is_open,
is_hovered,
},
);
} else {
let icon_id = Id::new(&self.id).with("tree view closer icon");
let openness = ui.ctx().animate_bool(icon_id, self.is_open);
paint_default_icon(ui, openness, &small_rect, is_hovered);
}
ui.allocate_space(ui.available_size_before_wrap());
});
res.response.rect
});
if closer.is_none() && reserve_closer {
ui.add_space(ui.spacing().icon_width);
}
let icon = if draw_icon && self.config.has_custom_icon() {
let (_, big_rect) = ui
.spacing()
.icon_rectangles(ui.available_rect_before_wrap());
Some(
ui.scope_builder(UiBuilder::new().max_rect(big_rect), |ui| {
ui.set_min_size(big_rect.size());
self.config.icon(ui);
})
.response
.rect,
)
} else {
None
};
if icon.is_none() && reserve_icon {
ui.add_space(ui.spacing().icon_width);
}
ui.add_space(2.0);
let label = ui
.scope(|ui| {
ui.spacing_mut().item_spacing = original_item_spacing;
self.config.label(ui);
})
.response
.rect;
ui.add_space(original_item_spacing.x);
(closer, icon, label)
}
pub(crate) fn show_context_menu_popup(&mut self, ui: &mut Ui, should_open: bool) -> bool {
if self.config.has_context_menu() {
Popup::new(
ui.id().with(&self.id).with("egui_ltreeview_context_menu"),
ui.ctx().clone(),
PopupAnchor::PointerFixed,
ui.layer_id(),
)
.kind(PopupKind::Menu)
.layout(Layout::top_down_justified(Align::Min))
.style(egui::containers::menu::menu_style)
.gap(0.0)
.open_memory(should_open.then_some(egui::SetOpenCommand::Bool(true)))
.show(|ui| {
self.config.context_menu(ui);
})
.is_some()
} else {
false
}
}
}
pub(crate) fn paint_default_icon(ui: &mut Ui, openness: f32, rect: &Rect, is_hovered: bool) {
let visuals = if is_hovered {
ui.visuals().widgets.hovered
} else {
ui.visuals().widgets.inactive
};
let rect = Rect::from_center_size(rect.center(), vec2(rect.width(), rect.height()) * 0.75);
let rect = rect.expand(visuals.expansion);
let mut points = vec![rect.left_top(), rect.right_top(), rect.center_bottom()];
use std::f32::consts::TAU;
let rotation = emath::Rot2::from_angle(remap(openness, 0.0..=1.0, -TAU / 4.0..=0.0));
for p in &mut points {
*p = rect.center() + rotation * (*p - rect.center());
}
ui.painter().add(Shape::convex_polygon(
points,
visuals.fg_stroke.color,
Stroke::NONE,
));
}
#[derive(Debug)]
pub struct CloserState {
pub is_open: bool,
pub is_hovered: bool,
}