From e84eef7815ebceae961059d7168b3d0335834db3 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Thu, 4 May 2023 21:59:45 +0200 Subject: [PATCH] Code cleanup --- crates/egui_extras/src/dock/behavior.rs | 76 +++++++++++++++++----- crates/egui_extras/src/dock/dock_struct.rs | 73 ++++++++++----------- crates/egui_extras/src/dock/mod.rs | 56 +++++++++++----- crates/egui_extras/src/dock/nodes.rs | 36 ++-------- examples/dock/src/main.rs | 4 +- 5 files changed, 146 insertions(+), 99 deletions(-) diff --git a/crates/egui_extras/src/dock/behavior.rs b/crates/egui_extras/src/dock/behavior.rs index cbc628fe7..47c661d81 100644 --- a/crates/egui_extras/src/dock/behavior.rs +++ b/crates/egui_extras/src/dock/behavior.rs @@ -1,4 +1,6 @@ -use egui::{vec2, Color32, Id, Response, Rgba, Sense, Stroke, TextStyle, Ui, Visuals, WidgetText}; +use egui::{ + vec2, Color32, Id, Rect, Response, Rgba, Sense, Stroke, TextStyle, Ui, Visuals, WidgetText, +}; use super::{Node, NodeId, Nodes, ResizeState, SimplificationOptions, UiResponse}; @@ -7,14 +9,22 @@ pub trait Behavior { /// Show this leaf node in the given [`egui::Ui`]. /// /// If this is an unknown node, return [`NodeAction::Remove`] and the node will be removed. + /// + /// You can make the leaf draggable by returning [`UiResponse::DragStarted`] + /// when the user drags some handle. fn leaf_ui(&mut self, _ui: &mut Ui, _node_id: NodeId, _leaf: &mut Leaf) -> UiResponse; - fn tab_text_for_leaf(&mut self, leaf: &Leaf) -> WidgetText; + /// The title of a leaf tab. + fn tab_title_for_leaf(&mut self, leaf: &Leaf) -> WidgetText; - fn tab_text_for_node(&mut self, nodes: &Nodes, node_id: NodeId) -> WidgetText { + /// The title of a general tab. + /// + /// The default implementation uses the name of the layout for branches, and + /// calls [`Self::tab_text_for_leaf`] for leaves. + fn tab_title_for_node(&mut self, nodes: &Nodes, node_id: NodeId) -> WidgetText { if let Some(node) = nodes.nodes.get(&node_id) { match node { - Node::Leaf(leaf) => self.tab_text_for_leaf(leaf), + Node::Leaf(leaf) => self.tab_title_for_leaf(leaf), Node::Branch(branch) => format!("{:?}", branch.get_layout()).into(), } } else { @@ -23,6 +33,8 @@ pub trait Behavior { } /// Show the title of a tab as a button. + /// + /// You can override the default implementation to add e.g. a close button. fn tab_ui( &mut self, nodes: &Nodes, @@ -32,7 +44,7 @@ pub trait Behavior { active: bool, is_being_dragged: bool, ) -> Response { - let text = self.tab_text_for_node(nodes, node_id); + let text = self.tab_title_for_node(nodes, node_id); let font_id = TextStyle::Button.resolve(ui.style()); let galley = text.into_galley(ui, Some(false), f32::INFINITY, font_id); @@ -45,8 +57,8 @@ pub trait Behavior { // Show a gap when dragged if ui.is_rect_visible(rect) && !is_being_dragged { - let bg_color = self.tab_bg_color(ui.visuals(), active); - let stroke = self.tab_outline_stroke(ui.visuals(), active); + let bg_color = self.tab_bg_color(ui.visuals(), node_id, active); + let stroke = self.tab_outline_stroke(ui.visuals(), node_id, active); ui.painter().rect(rect.shrink(0.5), 0.0, bg_color, stroke); if active { @@ -58,7 +70,7 @@ pub trait Behavior { ); } - let text_color = self.tab_text_color(ui.visuals(), active); + let text_color = self.tab_text_color(ui.visuals(), node_id, active); ui.painter().galley_with_color( egui::Align2::CENTER_CENTER .align_size_within_rect(galley.size(), rect) @@ -76,7 +88,7 @@ pub trait Behavior { true } - /// Adds some UI to the top right of the tab bar. + /// Adds some UI to the top right of each tab bar. /// /// You can use this to, for instance, add a button for adding new tabs. /// @@ -89,7 +101,7 @@ pub trait Behavior { // -------- // Settings: - /// The height of the bar holding tab names. + /// The height of the bar holding tab titles. fn tab_bar_height(&self, _style: &egui::Style) -> f32 { 24.0 } @@ -100,15 +112,17 @@ pub trait Behavior { 1.0 } - // No child should shrink below this size + /// No child should shrink below this width nor height. fn min_size(&self) -> f32 { 32.0 } + /// What are the rules for simplifying the tree? fn simplification_options(&self) -> SimplificationOptions { SimplificationOptions::default() } + /// The stroke used for the lines in horizontal, vertical, and grid layouts. fn resize_stroke(&self, style: &egui::Style, resize_state: ResizeState) -> egui::Stroke { match resize_state { ResizeState::Idle => egui::Stroke::NONE, // Let the gap speak for itself @@ -122,7 +136,7 @@ pub trait Behavior { 8.0 } - /// The background color of the tab bar + /// The background color of the tab bar. fn tab_bar_color(&self, visuals: &Visuals) -> Color32 { if visuals.dark_mode { Color32::BLACK @@ -131,7 +145,8 @@ pub trait Behavior { } } - fn tab_bg_color(&self, visuals: &Visuals, active: bool) -> Color32 { + /// The background color of a tab. + fn tab_bg_color(&self, visuals: &Visuals, _node_id: NodeId, active: bool) -> Color32 { if active { visuals.window_fill() // same as the tab contents } else { @@ -140,7 +155,7 @@ pub trait Behavior { } /// Stroke of the outline around a tab title. - fn tab_outline_stroke(&self, visuals: &Visuals, active: bool) -> Stroke { + fn tab_outline_stroke(&self, visuals: &Visuals, _node_id: NodeId, active: bool) -> Stroke { if active { Stroke::new(1.0, visuals.widgets.active.bg_fill) } else { @@ -153,11 +168,42 @@ pub trait Behavior { Stroke::new(1.0, visuals.widgets.noninteractive.bg_stroke.color) } - fn tab_text_color(&self, visuals: &Visuals, active: bool) -> Color32 { + /// The color of the title text of the tab. + fn tab_text_color(&self, visuals: &Visuals, _node_id: NodeId, active: bool) -> Color32 { if active { visuals.widgets.active.text_color() } else { visuals.widgets.noninteractive.text_color() } } + + /// When drag-and-dropping a node, how do we preview what is about to happen? + fn paint_drag_preview( + &self, + visuals: &Visuals, + painter: &egui::Painter, + parent_rect: Option, + preview_rect: Rect, + ) { + let preview_stroke = visuals.selection.stroke; + let preview_color = preview_stroke.color; + + if let Some(parent_rect) = parent_rect { + // Show which parent we will be dropped into + painter.rect_stroke(parent_rect, 1.0, preview_stroke); + } + + painter.rect( + preview_rect, + 1.0, + preview_color.gamma_multiply(0.5), + preview_stroke, + ); + } + + /// Show we preview leaves that are being dragged, + /// i.e. show their ui in the region where they will end up? + fn preview_dragged_leaves(&self) -> bool { + false + } } diff --git a/crates/egui_extras/src/dock/dock_struct.rs b/crates/egui_extras/src/dock/dock_struct.rs index 2497bee6d..fc015d39c 100644 --- a/crates/egui_extras/src/dock/dock_struct.rs +++ b/crates/egui_extras/src/dock/dock_struct.rs @@ -68,8 +68,6 @@ impl std::fmt::Debug for Dock { // ---------------------------------------------------------------------------- -// Construction - impl Dock { pub fn new(root: NodeId, nodes: Nodes) -> Self { Self { @@ -79,6 +77,10 @@ impl Dock { } } + pub fn root(&self) -> NodeId { + self.root + } + pub fn parent_of(&self, node_id: NodeId) -> Option { self.nodes .nodes @@ -92,15 +94,10 @@ impl Dock { }) .map(|(id, _)| *id) } -} -// Usage -impl Dock { - pub fn root(&self) -> NodeId { - self.root - } - - /// Show all the leaves in the dock. + /// Show the dock in the given [`Ui`]. + /// + /// The dock will use upp all the avilable space - nothing more, nothing less. pub fn ui(&mut self, behavior: &mut dyn Behavior, ui: &mut Ui) { let options = behavior.simplification_options(); self.simplify(&options); @@ -133,6 +130,15 @@ impl Dock { self.nodes .node_ui(behavior, &mut drop_context, ui, self.root); + self.preview_dragged_node(behavior, &drop_context, ui); + } + + fn preview_dragged_node( + &mut self, + behavior: &mut dyn Behavior, + drop_context: &DropContext, + ui: &mut Ui, + ) { if let (Some(mouse_pos), Some(dragged_node_id)) = (drop_context.mouse_pos, drop_context.dragged_node_id) { @@ -147,8 +153,8 @@ impl Dock { let mut frame = egui::Frame::popup(ui.style()); frame.fill = frame.fill.gamma_multiply(0.5); // Make see-through frame.show(ui, |ui| { - // TODO: preview contents - let text = behavior.tab_text_for_node(&self.nodes, dragged_node_id); + // TODO(emilk): preview contents? + let text = behavior.tab_title_for_node(&self.nodes, dragged_node_id); ui.label(text); }); }); @@ -156,26 +162,14 @@ impl Dock { if let Some(preview_rect) = drop_context.preview_rect { let preview_rect = self.smooth_preview_rect(ui.ctx(), preview_rect); - let preview_stroke = ui.visuals().selection.stroke; - let preview_color = preview_stroke.color; + let parent_rect = drop_context + .best_insertion + .and_then(|insertion_point| self.nodes.try_rect(insertion_point.parent_id)); - if let Some(insertion_point) = &drop_context.best_insertion { - if let Some(parent_rect) = self.nodes.try_rect(insertion_point.parent_id) { - // Show which parent we will be dropped into - ui.painter().rect_stroke(parent_rect, 1.0, preview_stroke); - } - } + behavior.paint_drag_preview(ui.visuals(), ui.painter(), parent_rect, preview_rect); - ui.painter().rect( - preview_rect, - 1.0, - preview_color.gamma_multiply(0.5), - preview_stroke, - ); - - let preview_child = false; - if preview_child { - // Preview actual child? + if behavior.preview_dragged_leaves() { + // TODO(emilk): add support for previewing branches too. if preview_rect.width() > 32.0 && preview_rect.height() > 32.0 { if let Some(Node::Leaf(leaf)) = self.nodes.get_mut(dragged_node_id) { let _ = behavior.leaf_ui( @@ -200,6 +194,7 @@ impl Dock { } } + /// Take the preview rectangle and smooth it over time. fn smooth_preview_rect(&mut self, ctx: &egui::Context, new_rect: Rect) -> Rect { let dt = ctx.input(|input| input.stable_dt).at_most(0.1); let t = egui::emath::exponential_smooth_factor(0.9, 0.05, dt); @@ -228,7 +223,8 @@ impl Dock { } } - fn move_node(&mut self, moved_node_id: NodeId, insertion_point: InsertionPoint) { + /// Move the given node to the given insertion point. + pub fn move_node(&mut self, moved_node_id: NodeId, insertion_point: InsertionPoint) { log::debug!( "Moving {moved_node_id:?} into {:?}", insertion_point.insertion @@ -238,7 +234,7 @@ impl Dock { } /// Find the currently dragged node, if any. - fn dragged_id(&self, ctx: &egui::Context) -> Option { + pub fn dragged_id(&self, ctx: &egui::Context) -> Option { if !is_possible_drag(ctx) { // We're not sure we're dragging _at all_ yet. return None; @@ -246,7 +242,7 @@ impl Dock { for &node_id in self.nodes.nodes.keys() { if node_id == self.root { - continue; // now allowed to drag root + continue; // not allowed to drag root } let id = node_id.id(); @@ -264,9 +260,12 @@ impl Dock { None } - /// Performs no simplifcations! - fn remove_node_id_from_parent(&mut self, dragged_node_id: NodeId) { - self.nodes - .remove_node_id_from_parent(self.root, dragged_node_id); + /// Performs no simplifcations, nor does it remove the actual [`Node`]. + pub fn remove_node_id_from_parent(&mut self, remove_me: NodeId) { + for parent in self.nodes.nodes.values_mut() { + if let Node::Branch(branch) = parent { + branch.retain(|child| child != remove_me); + } + } } } diff --git a/crates/egui_extras/src/dock/mod.rs b/crates/egui_extras/src/dock/mod.rs index 98f635b15..ae748ab41 100644 --- a/crates/egui_extras/src/dock/mod.rs +++ b/crates/egui_extras/src/dock/mod.rs @@ -16,21 +16,36 @@ //! The user needs to implement this in order to specify the `ui` of each `Leaf` and //! the tab name of leaves (if there are tab nodes). //! -//! ## Implementation notes -//! In many places we want to recursively visit all noted, while also mutating them. -//! In order to not get into trouble with the borrow checker a trick is used: -//! each [`Node`] is removed, mutated, recursed, and then re-added. -//! You'll see this pattern many times reading the following code. -//! //! ## Shortcomings //! We use real recursion, so if your trees get too deep you will get a stack overflow. //! +//! //! ## Future improvements -//! * A new ui for each node, nested -//! * Per-tab close-buttons +//! * Easy per-tab close-buttons //! * Scrolling of tab-bar //! * Vertical tab bar -//! * Auto-grid layouts (re-arange as parent is resized) +//! * Auto-join nested horizontal/vertical layouts in the simplify step + +// ## Implementation notes +// In many places we want to recursively visit all noted, while also mutating them. +// In order to not get into trouble with the borrow checker a trick is used: +// each [`Node`] is removed, mutated, recursed, and then re-added. +// You'll see this pattern many times reading the following code. +// +// Each frame consists of two passes: layout, and ui. +// The layout pass figures out where each node should be placed. +// The ui pass does all the painting. +// These two passes could be combined into one pass if we wanted to, +// but having them split up makes the code slightly simpler, and +// leaves the door open for more complex layout (e.g. min/max sizes per node). +// +// Everything is quite dynamic, so we have a bunch of defensive coding that call `warn!` on failure. +// These situations should not happen in normal use, but could happen if the user messes with +// the internals of the tree, putting it in an invalid state. +// +// ## TODO before release: +// * Auto-grid layouts (re-arange as parent is resized) +// * Clip tab titles to not cover "add new tab" button use egui::{Id, Pos2, Rect}; @@ -96,6 +111,7 @@ pub enum UiResponse { DragStarted, } +/// What are the rules for simplifying the tree? #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub struct SimplificationOptions { pub prune_empty_tabs: bool, @@ -143,7 +159,7 @@ pub struct InsertionPoint { } impl InsertionPoint { - fn new(parent_id: NodeId, insertion: LayoutInsertion) -> Self { + pub fn new(parent_id: NodeId, insertion: LayoutInsertion) -> Self { Self { parent_id, insertion, @@ -185,7 +201,14 @@ struct DropContext { } impl DropContext { - fn on_node(&mut self, parent_id: NodeId, rect: Rect, node: &Node) { + fn on_node( + &mut self, + behavior: &mut dyn Behavior, + style: &egui::Style, + parent_id: NodeId, + rect: Rect, + node: &Node, + ) { if !self.enabled { return; } @@ -212,17 +235,18 @@ impl DropContext { ); } - // self.suggest_rect(InsertionPoint::new(parent_id, LayoutType::Tabs, 1), rect); + self.suggest_rect( + InsertionPoint::new(parent_id, LayoutInsertion::Tabs(usize::MAX)), + rect.split_top_bottom_at_y(rect.top() + behavior.tab_bar_height(style)) + .1, + ); } fn suggest_rect(&mut self, insertion: InsertionPoint, preview_rect: Rect) { - self.suggest_point(insertion, preview_rect.center(), preview_rect); - } - - fn suggest_point(&mut self, insertion: InsertionPoint, target_point: Pos2, preview_rect: Rect) { if !self.enabled { return; } + let target_point = preview_rect.center(); if let Some(mouse_pos) = self.mouse_pos { let dist_sq = mouse_pos.distance_sq(target_point); if dist_sq < self.best_dist_sq { diff --git a/crates/egui_extras/src/dock/nodes.rs b/crates/egui_extras/src/dock/nodes.rs index 47fdfb32c..ca16f9d74 100644 --- a/crates/egui_extras/src/dock/nodes.rs +++ b/crates/egui_extras/src/dock/nodes.rs @@ -14,7 +14,7 @@ pub struct Nodes { /// Filled in by the layout step at the start of each frame. #[serde(default, skip)] - pub rects: HashMap, + pub(super) rects: HashMap, } impl Default for Nodes { @@ -29,11 +29,11 @@ impl Default for Nodes { // ---------------------------------------------------------------------------- impl Nodes { - pub fn try_rect(&self, node_id: NodeId) -> Option { + pub(super) fn try_rect(&self, node_id: NodeId) -> Option { self.rects.get(&node_id).copied() } - pub fn rect(&self, node_id: NodeId) -> Rect { + pub(super) fn rect(&self, node_id: NodeId) -> Rect { let rect = self.try_rect(node_id); debug_assert!(rect.is_some(), "Failed to find rect for {node_id:?}"); rect.unwrap_or(egui::Rect::from_min_max(Pos2::ZERO, Pos2::ZERO)) @@ -90,23 +90,6 @@ impl Nodes { self.insert_node(Node::Branch(Branch::new_grid(children))) } - /// Performs no simplifcations! - /// // TODO: remove ? - pub(super) fn remove_node_id_from_parent(&mut self, it: NodeId, remove: NodeId) -> GcAction { - if it == remove { - return GcAction::Remove; - } - let Some(mut node) = self.nodes.remove(&it) else { - log::warn!("Unexpected missing node during removal"); - return GcAction::Remove; - }; - if let Node::Branch(branch) = &mut node { - branch.retain(|child| self.remove_node_id_from_parent(child, remove) == GcAction::Keep); - } - self.nodes.insert(it, node); - GcAction::Keep - } - pub fn insert(&mut self, insertion_point: InsertionPoint, child_id: NodeId) { let InsertionPoint { parent_id, @@ -257,7 +240,7 @@ impl Nodes { ui: &mut Ui, node_id: NodeId, ) { - // NOTE: important that we get thr rect and node in two steps, + // NOTE: important that we get the rect and node in two steps, // otherwise we could loose the node when there is no rect. let Some(rect) = self.try_rect(node_id) else { log::warn!("Failed to find rect for node {node_id:?} during ui"); @@ -273,14 +256,9 @@ impl Nodes { // Can't drag a node onto self or any children drop_context.enabled = false; } - drop_context.on_node(node_id, rect, &node); - - drop_context.suggest_rect( - InsertionPoint::new(node_id, LayoutInsertion::Tabs(usize::MAX)), - rect.split_top_bottom_at_y(rect.top() + behavior.tab_bar_height(ui.style())) - .1, - ); + drop_context.on_node(behavior, ui.style(), node_id, rect, &node); + // Each node gets its own `Ui`, nested inside each other, with proper clip rectangles. let mut ui = egui::Ui::new( ui.ctx().clone(), ui.layer_id(), @@ -314,7 +292,7 @@ impl Nodes { }; if let Node::Branch(branch) = &mut node { - // TODO: join nested versions of the same horizontal/vertical layouts + // TODO(emilk): join nested versions of the same horizontal/vertical layouts branch.simplify_children(|child| self.simplify(options, child)); diff --git a/examples/dock/src/main.rs b/examples/dock/src/main.rs index 33ae2e9e3..a12dca22c 100644 --- a/examples/dock/src/main.rs +++ b/examples/dock/src/main.rs @@ -119,7 +119,7 @@ impl dock::Behavior for DockBehavior { view.ui(ui) } - fn tab_text_for_leaf(&mut self, view: &View) -> egui::WidgetText { + fn tab_title_for_leaf(&mut self, view: &View) -> egui::WidgetText { format!("View {}", view.nr).into() } @@ -249,7 +249,7 @@ fn tree_ui( // Get the name BEFORE we remove the node below! let text = format!( "{} - {node_id:?}", - behavior.tab_text_for_node(nodes, node_id).text() + behavior.tab_title_for_node(nodes, node_id).text() ); let Some(mut node) = nodes.nodes.remove(&node_id) else {