#![warn(missing_docs)]
#![cfg_attr(feature = "doc",
doc = "Visit the [`doc`] module documentation for further information about these topics:",
doc = "",
doc = make_table_of_contents::make_table_of_contents!("src/doc/doc.md", "doc/index.html")
)]
#![cfg_attr(
not(feature = "doc"),
doc = "Enable the `doc` feature for further information"
)]
#[cfg(feature = "doc")]
pub mod doc;
mod builder;
mod node;
mod state;
use egui::{
self, layers::ShapeIdx, vec2, Align, EventFilter, Id, Key, LayerId, Layout, Modifiers, NumExt,
Order, PointerButton, Popup, PopupAnchor, PopupKind, Pos2, Rangef, Rect, Response, Sense,
Shape, Ui, UiBuilder, Vec2,
};
use std::{collections::HashSet, hash::Hash};
pub use builder::*;
pub use node::*;
pub use state::*;
#[cfg(not(feature = "persistence"))]
pub trait NodeId: Clone + PartialEq + Eq + Hash {}
#[cfg(not(feature = "persistence"))]
impl<T> NodeId for T where T: Clone + PartialEq + Eq + Hash {}
#[cfg(feature = "persistence")]
pub trait NodeId:
Clone + PartialEq + Eq + Hash + serde::de::DeserializeOwned + serde::Serialize
{
}
#[cfg(feature = "persistence")]
impl<T> NodeId for T where
T: Clone + PartialEq + Eq + Hash + serde::de::DeserializeOwned + serde::Serialize
{
}
pub struct TreeView<'context_menu, NodeIdType> {
id: Id,
settings: TreeViewSettings,
#[allow(clippy::type_complexity)]
fallback_context_menu: Option<Box<dyn FnOnce(&mut Ui, &Vec<NodeIdType>) + 'context_menu>>,
}
impl<'context_menu, NodeIdType: NodeId> TreeView<'context_menu, NodeIdType> {
pub fn new(id: Id) -> Self {
Self {
id,
settings: TreeViewSettings::default(),
fallback_context_menu: None,
}
}
pub fn show(
self,
ui: &mut Ui,
build_tree_view: impl FnOnce(&mut TreeViewBuilder<'_, NodeIdType>),
) -> (Response, Vec<Action<NodeIdType>>)
where
NodeIdType: NodeId + Send + Sync + 'static,
{
let id = self.id;
let mut state = TreeViewState::load(ui, id).unwrap_or_default();
let res = self.show_state(ui, &mut state, build_tree_view);
state.store(ui, id);
res
}
pub fn show_state(
self,
ui: &mut Ui,
state: &mut TreeViewState<NodeIdType>,
build_tree_view: impl FnOnce(&mut TreeViewBuilder<'_, NodeIdType>),
) -> (Response, Vec<Action<NodeIdType>>)
where
NodeIdType: NodeId,
{
let TreeView {
id,
settings,
mut fallback_context_menu,
} = self;
ui.memory_mut(|m| {
m.set_focus_lock_filter(
id,
EventFilter {
tab: false,
escape: false,
horizontal_arrows: true,
vertical_arrows: true,
},
)
});
let (builder_response, tree_view_rect) = draw_foreground(
ui,
id,
&settings,
state,
build_tree_view,
&mut fallback_context_menu,
);
if !settings.allow_multi_select {
state.prune_selection_to_single_id();
}
if ui.memory(|m| m.has_focus(id)) {
if state.selected().is_empty() {
let fallback_selection = state.get_dragged().and_then(|v| v.first());
if let Some(fallback_selection) = fallback_selection {
state.set_one_selected(fallback_selection.clone());
}
}
}
if builder_response.interaction.clicked() || builder_response.interaction.drag_started() {
ui.memory_mut(|m| m.request_focus(id));
}
let mut actions = Vec::new();
if builder_response.interaction.dragged() {
if let Some((drop_id, position)) = &builder_response.drop_target {
actions.push(Action::Drag(DragAndDrop {
source: state.get_simplified_dragged().cloned().unwrap_or_default(),
target: drop_id.clone(),
position: position.clone(),
drop_marker_idx: builder_response.drop_marker_idx,
}))
} else if !builder_response.drop_on_self {
if let Some(position) = ui.ctx().pointer_latest_pos() {
actions.push(Action::DragExternal(DragAndDropExternal {
position,
source: state.get_simplified_dragged().cloned().unwrap_or_default(),
}));
}
}
}
if builder_response.interaction.drag_stopped() {
if let Some((drop_id, position)) = builder_response.drop_target {
actions.push(Action::Move(DragAndDrop {
source: state.get_simplified_dragged().cloned().unwrap_or_default(),
target: drop_id,
position,
drop_marker_idx: builder_response.drop_marker_idx,
}))
} else if !builder_response.drop_on_self {
if let Some(position) = ui.ctx().pointer_latest_pos() {
actions.push(Action::MoveExternal(DragAndDropExternal {
position,
source: state.get_simplified_dragged().cloned().unwrap_or_default(),
}));
}
}
}
if builder_response.selected {
actions.push(Action::SetSelected(state.selected().clone()));
}
if let Some(nodes_to_activate) = builder_response.activate {
actions.push(Action::Activate(Activate {
selected: nodes_to_activate.clone(),
modifiers: ui.ctx().input(|i| i.modifiers),
}));
}
if builder_response.interaction.drag_stopped() {
state.reset_dragged();
}
(
builder_response.interaction.with_new_rect(tree_view_rect),
actions,
)
}
}
impl<'context_menu, NodeIdType: NodeId> TreeView<'context_menu, NodeIdType> {
pub fn with_settings(mut self, settings: TreeViewSettings) -> Self {
self.settings = settings;
self
}
pub fn override_indent(mut self, indent: Option<f32>) -> Self {
self.settings.override_indent = indent;
self
}
pub fn override_striped(mut self, striped: Option<bool>) -> Self {
self.settings.override_striped = striped;
self
}
pub fn indent_hint_style(mut self, style: IndentHintStyle) -> Self {
self.settings.indent_hint_style = style;
self
}
pub fn row_layout(mut self, layout: RowLayout) -> Self {
self.settings.row_layout = layout;
self
}
pub fn allow_multi_selection(mut self, allow_multi_select: bool) -> Self {
self.settings.allow_multi_select = allow_multi_select;
self
}
pub fn range_selection_modifier(mut self, modifiers: Modifiers) -> Self {
self.settings.range_selection_modifier = modifiers;
self
}
pub fn set_selection_modifier(mut self, modifiers: Modifiers) -> Self {
self.settings.set_selection_modifier = modifiers;
self
}
pub fn allow_drag_and_drop(mut self, allow_drag_and_drop: bool) -> Self {
self.settings.allow_drag_and_drop = allow_drag_and_drop;
self
}
pub fn default_node_height(mut self, default_node_height: Option<f32>) -> Self {
self.settings.default_node_height = default_node_height;
self
}
pub fn fallback_context_menu(
mut self,
context_menu: impl FnOnce(&mut Ui, &Vec<NodeIdType>) + 'context_menu,
) -> Self {
self.fallback_context_menu = Some(Box::new(context_menu));
self
}
pub fn min_width(mut self, width: f32) -> Self {
self.settings.min_width = width;
self
}
pub fn min_height(mut self, height: f32) -> Self {
self.settings.min_height = height;
self
}
}
#[allow(clippy::type_complexity)]
fn draw_foreground<'context_menu, NodeIdType: NodeId>(
ui: &mut Ui,
id: Id,
settings: &TreeViewSettings,
state: &mut TreeViewState<NodeIdType>,
build_tree_view: impl FnOnce(&mut TreeViewBuilder<'_, NodeIdType>),
fall_back_context_menu: &mut Option<Box<dyn FnOnce(&mut Ui, &Vec<NodeIdType>) + 'context_menu>>,
) -> (TreeViewBuilderResponse<NodeIdType>, Rect) {
let interaction_rect = Rect::from_min_size(
ui.cursor().min,
ui.available_size()
.at_least(vec2(settings.min_width, settings.min_height))
.at_least(vec2(state.min_width, state.last_height)),
);
let interaction = interact_no_expansion(ui, interaction_rect, id, Sense::click_and_drag());
let input = get_input::<NodeIdType>(ui, &interaction, id, settings);
let ui_data = UiData {
interaction,
drag_layer: LayerId::new(Order::Tooltip, ui.make_persistent_id("ltreeviw drag layer")),
drag_layer_offset: ui
.input(|i| i.pointer.press_origin())
.zip(ui.input(|i| i.pointer.latest_pos()))
.zip(state.get_drag_overlay_offset())
.map(|((origin, latest), drag_overlay_offset)| (latest - origin) + drag_overlay_offset)
.unwrap_or_default(),
has_focus: ui.memory(|m| m.has_focus(id)),
drop_marker_idx: ui.painter().add(Shape::Noop),
};
let mut builder_ui = ui.new_child(
UiBuilder::new()
.layout(Layout::top_down(egui::Align::Min))
.max_rect(interaction_rect),
);
let mut builder_response = TreeViewBuilder::run(
&mut builder_ui,
state,
settings,
ui_data,
input,
build_tree_view,
);
let tree_view_rect = builder_response.space_used.union(interaction_rect);
ui.allocate_rect(tree_view_rect, Sense::hover());
state.min_width = state
.min_width
.at_least(builder_response.space_used.width());
state.last_height = builder_response.space_used.height();
let mut open_fallback_context_menu = false;
match builder_response.output {
BuilderActions::OpenFallbackContextmenu { for_selection } => {
open_fallback_context_menu = true;
state.show_fallback_context_menu_for_selection = for_selection;
builder_response.output = BuilderActions::None;
}
BuilderActions::SetDragged(dragged) => {
state.set_dragged(dragged);
builder_response.output = BuilderActions::None;
}
BuilderActions::SetSecondaryClicked(id) => {
state.secondary_selection = Some(id);
builder_response.output = BuilderActions::None;
}
BuilderActions::ActivateSelection(selection) => {
builder_response.activate = Some(selection);
builder_response.output = BuilderActions::None;
}
BuilderActions::ActivateThis(id) => {
builder_response.activate = Some(vec![id]);
builder_response.output = BuilderActions::None;
}
BuilderActions::SelectOneNode(id, scroll_to_rect) => {
builder_response.selected = true;
state.set_one_selected(id.clone());
state.set_cursor(None);
if let Some(scroll_to_rect) = scroll_to_rect {
ui.scroll_to_rect(scroll_to_rect, None);
}
builder_response.output = BuilderActions::None;
}
BuilderActions::ToggleSelection(id, scroll_to_rect) => {
builder_response.selected = true;
state.toggle_selected(&id);
state.set_pivot(Some(id));
if let Some(scroll_to_rect) = scroll_to_rect {
ui.scroll_to_rect(scroll_to_rect, None);
}
builder_response.output = BuilderActions::None;
}
BuilderActions::ShiftSelect(ids) => {
builder_response.selected = true;
state.set_selected_dont_change_pivot(ids);
builder_response.output = BuilderActions::None;
}
BuilderActions::Select {
selection,
pivot,
cursor,
scroll_to_rect,
} => {
builder_response.selected = true;
state.set_selected(selection);
state.set_pivot(Some(pivot));
state.set_cursor(Some(cursor));
ui.scroll_to_rect(scroll_to_rect, None);
builder_response.output = BuilderActions::None;
}
BuilderActions::SetCursor(id, scroll_to_rect) => {
state.set_cursor(Some(id));
ui.scroll_to_rect(scroll_to_rect, None);
builder_response.output = BuilderActions::None;
}
BuilderActions::SetOpenness(id, is_open) => {
state.set_openness(id, is_open);
builder_response.output = BuilderActions::None;
}
BuilderActions::SetLastclicked(id) => {
state.set_last_clicked(&id);
builder_response.output = BuilderActions::None;
}
BuilderActions::ClearSelection => {
state.set_selected(vec![]);
builder_response.selected = true;
}
BuilderActions::None => (),
}
if let Some(fallback_context_menu) = fall_back_context_menu.take() {
Popup::new(
Id::new(&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(open_fallback_context_menu.then_some(egui::SetOpenCommand::Bool(true)))
.show(|ui| {
if state.show_fallback_context_menu_for_selection {
fallback_context_menu(ui, state.selected());
} else {
fallback_context_menu(ui, &Vec::new());
}
});
}
(builder_response, tree_view_rect)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DirPosition<NodeIdType> {
First,
Last,
After(NodeIdType),
Before(NodeIdType),
}
#[derive(Clone, Debug)]
pub struct TreeViewSettings {
pub override_indent: Option<f32>,
pub override_striped: Option<bool>,
pub indent_hint_style: IndentHintStyle,
pub row_layout: RowLayout,
pub min_width: f32,
pub min_height: f32,
pub allow_multi_select: bool,
pub range_selection_modifier: Modifiers,
pub set_selection_modifier: Modifiers,
pub allow_drag_and_drop: bool,
pub default_node_height: Option<f32>,
}
impl Default for TreeViewSettings {
fn default() -> Self {
Self {
override_indent: None,
override_striped: None,
indent_hint_style: Default::default(),
row_layout: Default::default(),
min_width: 0.0,
min_height: 0.0,
allow_multi_select: true,
range_selection_modifier: Modifiers::SHIFT,
set_selection_modifier: Modifiers::COMMAND,
allow_drag_and_drop: true,
default_node_height: None,
}
}
}
#[derive(Default, Clone, Copy, PartialEq, Eq, Debug)]
pub enum IndentHintStyle {
None,
Line,
#[default]
Hook,
}
#[derive(Default, Clone, Copy, PartialEq, Eq, Debug)]
pub enum RowLayout {
Compact,
CompactAlignedLabels,
#[default]
AlignedIcons,
AlignedIconsAndLabels,
}
#[derive(Clone, Debug)]
pub enum Action<NodeIdType> {
SetSelected(Vec<NodeIdType>),
Move(DragAndDrop<NodeIdType>),
Drag(DragAndDrop<NodeIdType>),
Activate(Activate<NodeIdType>),
DragExternal(DragAndDropExternal<NodeIdType>),
MoveExternal(DragAndDropExternal<NodeIdType>),
}
#[derive(Clone, Debug)]
pub struct DragAndDropExternal<NodeIdType> {
pub source: Vec<NodeIdType>,
pub position: egui::Pos2,
}
#[derive(Clone, Debug)]
pub struct DragAndDrop<NodeIdType> {
pub source: Vec<NodeIdType>,
pub target: NodeIdType,
pub position: DirPosition<NodeIdType>,
drop_marker_idx: ShapeIdx,
}
impl<NodeIdType> DragAndDrop<NodeIdType> {
pub fn remove_drop_marker(&self, ui: &mut Ui) {
ui.painter().set(self.drop_marker_idx, Shape::Noop);
}
}
#[derive(Clone, Debug)]
pub struct Activate<NodeIdType> {
pub selected: Vec<NodeIdType>,
pub modifiers: Modifiers,
}
fn interact_no_expansion(ui: &mut Ui, rect: Rect, id: Id, sense: Sense) -> Response {
let spacing_before = ui.spacing().clone();
ui.spacing_mut().item_spacing = Vec2::ZERO;
let res = ui.interact(rect, id, sense);
*ui.spacing_mut() = spacing_before;
res
}
enum DropQuarter {
Top,
MiddleTop,
MiddleBottom,
Bottom,
}
impl DropQuarter {
fn new(range: Rangef, cursor_pos: f32) -> Option<DropQuarter> {
pub const DROP_LINE_HOVER_HEIGHT: f32 = 5.0;
let h0 = range.min;
let h1 = range.min + DROP_LINE_HOVER_HEIGHT;
let h2 = range.center();
let h3 = range.max - DROP_LINE_HOVER_HEIGHT;
let h4 = range.max;
match cursor_pos {
y if y >= h0 && y < h1 => Some(Self::Top),
y if y >= h1 && y < h2 => Some(Self::MiddleTop),
y if y >= h2 && y < h3 => Some(Self::MiddleBottom),
y if y >= h3 && y < h4 => Some(Self::Bottom),
_ => None,
}
}
}
struct UiData {
interaction: Response,
drag_layer: LayerId,
drag_layer_offset: Vec2,
has_focus: bool,
drop_marker_idx: ShapeIdx,
}
fn rect_contains_visually(rect: &Rect, pos: &Pos2) -> bool {
rect.min.x <= pos.x && pos.x < rect.max.x && rect.min.y <= pos.y && pos.y < rect.max.y
}
enum Input<NodeIdType> {
DragStarted {
pos: Pos2,
selected_node_dragged: bool,
visited_selected_nodes: HashSet<NodeIdType>,
simplified_dragged: Vec<NodeIdType>,
},
Dragged(Pos2),
SecondaryClick(Pos2),
Click {
pos: Pos2,
double: bool,
modifiers: Modifiers,
activatable_nodes: Vec<NodeIdType>,
shift_click_nodes: Option<Vec<NodeIdType>>,
},
KeyLeft,
KeyRight {
select_next: bool,
},
KeyUp {
previous_node: Option<(NodeIdType, Rect)>,
},
KeyUpAndCommand {
previous_node: Option<(NodeIdType, Rect)>,
},
KeyUpAndShift {
previous_node: Option<(NodeIdType, Rect)>,
nodes_to_select: Option<Vec<NodeIdType>>,
next_cursor: Option<(NodeIdType, Rect)>,
},
KeyDown(bool),
KeyDownAndCommand {
is_next: bool,
},
KeyDownAndShift {
nodes_to_select: Option<Vec<NodeIdType>>,
next_cursor: Option<(NodeIdType, Rect)>,
is_next: bool,
},
KeySpace,
CollectActivatableNodes {
activatable_nodes: Vec<NodeIdType>,
},
None,
}
fn get_input<NodeIdType>(
ui: &Ui,
interaction: &Response,
id: Id,
settings: &TreeViewSettings,
) -> Input<NodeIdType> {
let press_origin = ui.input(|i| i.pointer.press_origin());
let pointer_pos = ui.input(|i| i.pointer.interact_pos());
let modifiers = ui.input(|i| i.modifiers);
if interaction.context_menu_opened() {
if interaction.secondary_clicked() {
return Input::SecondaryClick(
pointer_pos.expect("If the tree view was clicked it must have a pointer position"),
);
}
return Input::None;
}
if interaction.drag_started_by(PointerButton::Primary) && settings.allow_drag_and_drop {
return Input::DragStarted {
pos: press_origin
.expect("If a drag has started it must have a position where the press started"),
selected_node_dragged: false,
visited_selected_nodes: HashSet::new(),
simplified_dragged: Vec::new(),
};
}
if (interaction.dragged_by(PointerButton::Primary)
|| interaction.drag_stopped_by(PointerButton::Primary))
&& settings.allow_drag_and_drop
{
return Input::Dragged(
pointer_pos.expect("If the tree view is dragged it must have a pointer position"),
);
}
if interaction.secondary_clicked() {
return Input::SecondaryClick(
pointer_pos.expect("If the tree view was clicked it must have a pointer position"),
);
}
if interaction.clicked_by(PointerButton::Primary)
|| interaction.drag_started_by(PointerButton::Primary) && !settings.allow_drag_and_drop
{
return Input::Click {
pos: pointer_pos.expect("If the tree view was clicked it must have a pointer position"),
double: interaction.double_clicked(),
modifiers,
activatable_nodes: Vec::new(),
shift_click_nodes: None,
};
}
if !ui.memory(|m| m.has_focus(id)) {
return Input::None;
}
if ui.input(|i| i.key_pressed(Key::ArrowLeft)) {
return Input::KeyLeft;
}
if ui.input(|i| i.key_pressed(Key::ArrowRight)) {
return Input::KeyRight { select_next: false };
}
if ui.input(|i| i.key_pressed(Key::ArrowUp)) {
if modifiers.matches_exact(settings.range_selection_modifier) {
return Input::KeyUpAndShift {
previous_node: None,
nodes_to_select: None,
next_cursor: None,
};
}
if modifiers.matches_exact(settings.set_selection_modifier) {
return Input::KeyUpAndCommand {
previous_node: None,
};
}
return Input::KeyUp {
previous_node: None,
};
}
if ui.input(|i| i.key_pressed(Key::ArrowDown)) {
if modifiers.matches_exact(settings.range_selection_modifier) {
return Input::KeyDownAndShift {
nodes_to_select: None,
next_cursor: None,
is_next: false,
};
}
if modifiers.matches_exact(settings.set_selection_modifier) {
return Input::KeyDownAndCommand { is_next: false };
}
return Input::KeyDown(false);
}
if ui.input(|i| i.key_pressed(Key::Space)) {
return Input::KeySpace;
}
if ui.input(|i| i.key_pressed(Key::Enter)) {
return Input::CollectActivatableNodes {
activatable_nodes: Vec::new(),
};
}
Input::None
}