From e757fdc15212a4936c3f6f38acbb091082c5de76 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Thu, 4 May 2023 20:56:58 +0200 Subject: [PATCH] Move code around a bit --- crates/egui_extras/src/dock/branch/grid.rs | 23 +- crates/egui_extras/src/dock/branch/linear.rs | 2 +- crates/egui_extras/src/dock/branch/tabs.rs | 2 +- crates/egui_extras/src/dock/dock_struct.rs | 432 +++++++++++ crates/egui_extras/src/dock/mod.rs | 772 ++----------------- crates/egui_extras/src/dock/nodes.rs | 222 ++++++ 6 files changed, 743 insertions(+), 710 deletions(-) create mode 100644 crates/egui_extras/src/dock/dock_struct.rs create mode 100644 crates/egui_extras/src/dock/nodes.rs diff --git a/crates/egui_extras/src/dock/branch/grid.rs b/crates/egui_extras/src/dock/branch/grid.rs index a51676e74..a101c1fbf 100644 --- a/crates/egui_extras/src/dock/branch/grid.rs +++ b/crates/egui_extras/src/dock/branch/grid.rs @@ -1,11 +1,10 @@ use std::collections::{BTreeMap, HashMap, HashSet}; -use egui::{emath::Rangef, pos2, vec2, Rect}; +use egui::{emath::Rangef, pos2, vec2, NumExt as _, Rect}; use itertools::Itertools as _; use crate::dock::{ - sizes_from_shares, Behavior, DropContext, InsertionPoint, LayoutInsertion, NodeId, Nodes, - ResizeState, + Behavior, DropContext, InsertionPoint, LayoutInsertion, NodeId, Nodes, ResizeState, }; /// Where in a grid? @@ -155,7 +154,7 @@ impl Grid { } } - pub fn ui( + pub(super) fn ui( &mut self, nodes: &mut Nodes, behavior: &mut dyn Behavior, @@ -360,3 +359,19 @@ fn shrink_shares( total_shares_lost } + +fn sizes_from_shares(shares: &[f32], available_size: f32, gap_width: f32) -> Vec { + assert!(!shares.is_empty()); + let available_size = available_size - gap_width * (shares.len() - 1) as f32; + let available_size = available_size.at_least(0.0); + + let total_share: f32 = shares.iter().sum(); + if total_share <= 0.0 { + vec![available_size / shares.len() as f32; shares.len()] + } else { + shares + .iter() + .map(|&share| share / total_share * available_size) + .collect() + } +} diff --git a/crates/egui_extras/src/dock/branch/linear.rs b/crates/egui_extras/src/dock/branch/linear.rs index 88753b560..42645dc02 100644 --- a/crates/egui_extras/src/dock/branch/linear.rs +++ b/crates/egui_extras/src/dock/branch/linear.rs @@ -95,7 +95,7 @@ impl Linear { } } - pub fn ui( + pub(super) fn ui( &mut self, nodes: &mut Nodes, behavior: &mut dyn Behavior, diff --git a/crates/egui_extras/src/dock/branch/tabs.rs b/crates/egui_extras/src/dock/branch/tabs.rs index 9726b5b49..5b8c1302f 100644 --- a/crates/egui_extras/src/dock/branch/tabs.rs +++ b/crates/egui_extras/src/dock/branch/tabs.rs @@ -46,7 +46,7 @@ impl Tabs { nodes.layout_node(style, behavior, active_rect, self.active); } - pub fn ui( + pub(super) fn ui( &mut self, nodes: &mut Nodes, behavior: &mut dyn Behavior, diff --git a/crates/egui_extras/src/dock/dock_struct.rs b/crates/egui_extras/src/dock/dock_struct.rs new file mode 100644 index 000000000..5b1b44db2 --- /dev/null +++ b/crates/egui_extras/src/dock/dock_struct.rs @@ -0,0 +1,432 @@ +use egui::{Id, NumExt as _, Pos2, Rect, Ui}; + +use super::{ + is_possible_drag, Behavior, Branch, DropContext, GcAction, Grid, InsertionPoint, + LayoutInsertion, Linear, LinearDir, Node, NodeId, Nodes, SimplificationOptions, SimplifyAction, + Tabs, +}; + +/// The top level type. Contains all persistent state, including layouts and sizes. +#[derive(Clone, serde::Serialize, serde::Deserialize)] +pub struct Dock { + pub root: NodeId, + pub nodes: Nodes, + + /// Smoothed avaerage of preview + #[serde(skip)] + pub smoothed_preview_rect: Option, +} + +impl Default for Dock { + fn default() -> Self { + Self { + root: Default::default(), + nodes: Default::default(), + smoothed_preview_rect: None, + } + } +} + +impl std::fmt::Debug for Dock { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + // Print a hiearchical view of the tree: + fn format_node( + f: &mut std::fmt::Formatter<'_>, + nodes: &Nodes, + indent: usize, + node_id: NodeId, + ) -> std::fmt::Result { + write!(f, "{} {node_id:?} ", " ".repeat(indent))?; + if let Some(node) = nodes.get(node_id) { + match node { + Node::Leaf(leaf) => writeln!(f, "Leaf {leaf:?}"), + Node::Branch(branch) => { + writeln!( + f, + "{}", + match branch { + Branch::Tabs(_) => "Tabs", + Branch::Linear(_) => "Linear", + Branch::Grid(_) => "Grid", + } + )?; + for &child in branch.children() { + format_node(f, nodes, indent + 1, child)?; + } + Ok(()) + } + } + } else { + write!(f, "DANGLING {node_id:?}") + } + } + + writeln!(f, "Dock {{")?; + format_node(f, &self.nodes, 1, self.root)?; + write!(f, "\n}}") + } +} + +// ---------------------------------------------------------------------------- + +// Construction + +impl Dock { + pub fn new(root: NodeId, nodes: Nodes) -> Self { + Self { + root, + nodes, + smoothed_preview_rect: None, + } + } + + pub fn parent_of(&self, node_id: NodeId) -> Option { + self.nodes + .nodes + .iter() + .find(|(_, node)| { + if let Node::Branch(branch) = node { + branch.children().contains(&node_id) + } else { + false + } + }) + .map(|(id, _)| *id) + } +} + +impl Nodes { + pub fn try_rect(&self, node_id: NodeId) -> Option { + self.rects.get(&node_id).copied() + } + + pub 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)) + } + + pub fn get(&self, node_id: NodeId) -> Option<&Node> { + self.nodes.get(&node_id) + } + + pub fn get_mut(&mut self, node_id: NodeId) -> Option<&mut Node> { + self.nodes.get_mut(&node_id) + } + + #[must_use] + pub fn insert_node(&mut self, node: Node) -> NodeId { + let id = NodeId::random(); + self.nodes.insert(id, node); + id + } + + #[must_use] + pub fn insert_leaf(&mut self, leaf: Leaf) -> NodeId { + self.insert_node(Node::Leaf(leaf)) + } + + #[must_use] + pub fn insert_branch(&mut self, branch: Branch) -> NodeId { + self.insert_node(Node::Branch(branch)) + } + + #[must_use] + pub fn insert_tab_node(&mut self, children: Vec) -> NodeId { + self.insert_node(Node::Branch(Branch::new_tabs(children))) + } + + #[must_use] + pub fn insert_horizontal_node(&mut self, children: Vec) -> NodeId { + self.insert_node(Node::Branch(Branch::new_linear( + LinearDir::Horizontal, + children, + ))) + } + + #[must_use] + pub fn insert_vertical_node(&mut self, children: Vec) -> NodeId { + self.insert_node(Node::Branch(Branch::new_linear( + LinearDir::Vertical, + children, + ))) + } + + #[must_use] + pub fn insert_grid_node(&mut self, children: Vec) -> NodeId { + self.insert_node(Node::Branch(Branch::new_grid(children))) + } + + /// Performs no simplifcations! + 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 + } + + fn insert(&mut self, insertion_point: InsertionPoint, child_id: NodeId) { + let InsertionPoint { + parent_id, + insertion, + } = insertion_point; + + let Some(mut node) = self.nodes.remove(&parent_id) else { + log::warn!("Failed to insert: could not find parent {parent_id:?}"); + return; + }; + + match insertion { + LayoutInsertion::Tabs(index) => { + if let Node::Branch(Branch::Tabs(tabs)) = &mut node { + let index = index.min(tabs.children.len()); + tabs.children.insert(index, child_id); + tabs.active = child_id; + self.nodes.insert(parent_id, node); + } else { + let new_node_id = self.insert_node(node); + let mut tabs = Tabs::new(vec![new_node_id]); + tabs.children.insert(index.min(1), child_id); + tabs.active = child_id; + self.nodes + .insert(parent_id, Node::Branch(Branch::Tabs(tabs))); + } + } + LayoutInsertion::Horizontal(index) => { + if let Node::Branch(Branch::Linear(Linear { + dir: LinearDir::Horizontal, + children, + .. + })) = &mut node + { + let index = index.min(children.len()); + children.insert(index, child_id); + self.nodes.insert(parent_id, node); + } else { + let new_node_id = self.insert_node(node); + let mut linear = Linear::new(LinearDir::Horizontal, vec![new_node_id]); + linear.children.insert(index.min(1), child_id); + self.nodes + .insert(parent_id, Node::Branch(Branch::Linear(linear))); + } + } + LayoutInsertion::Vertical(index) => { + if let Node::Branch(Branch::Linear(Linear { + dir: LinearDir::Vertical, + children, + .. + })) = &mut node + { + let index = index.min(children.len()); + children.insert(index, child_id); + self.nodes.insert(parent_id, node); + } else { + let new_node_id = self.insert_node(node); + let mut linear = Linear::new(LinearDir::Vertical, vec![new_node_id]); + linear.children.insert(index.min(1), child_id); + self.nodes + .insert(parent_id, Node::Branch(Branch::Linear(linear))); + } + } + LayoutInsertion::Grid(insert_location) => { + if let Node::Branch(Branch::Grid(grid)) = &mut node { + grid.locations.retain(|_, pos| *pos != insert_location); + grid.locations.insert(child_id, insert_location); + grid.children.push(child_id); + self.nodes.insert(parent_id, node); + } else { + let new_node_id = self.insert_node(node); + let mut grid = Grid::new(vec![new_node_id, child_id]); + grid.locations.insert(child_id, insert_location); + self.nodes + .insert(parent_id, Node::Branch(Branch::Grid(grid))); + } + } + } + } +} + +// Usage +impl Dock { + pub fn root(&self) -> NodeId { + self.root + } + + /// Show all the leaves in the dock. + pub fn ui(&mut self, behavior: &mut dyn Behavior, ui: &mut Ui) { + let options = behavior.simplification_options(); + self.simplify(&options); + if options.all_leaves_must_have_tabs { + self.nodes + .make_all_leaves_children_of_tabs(false, self.root); + } + + self.nodes.gc_root(behavior, self.root); + + self.nodes.rects.clear(); + + // Check if anything is being dragged: + let mut drop_context = DropContext { + enabled: true, + dragged_node_id: self.dragged_id(ui.ctx()), + mouse_pos: ui.input(|i| i.pointer.hover_pos()), + best_dist_sq: f32::INFINITY, + best_insertion: None, + preview_rect: None, + }; + + self.nodes.layout_node( + ui.style(), + behavior, + ui.available_rect_before_wrap(), + self.root, + ); + + self.nodes + .node_ui(behavior, &mut drop_context, ui, self.root); + + if let (Some(mouse_pos), Some(dragged_node_id)) = + (drop_context.mouse_pos, drop_context.dragged_node_id) + { + ui.output_mut(|o| o.cursor_icon = egui::CursorIcon::Grabbing); + + // Preview what is being dragged: + egui::Area::new(Id::new((dragged_node_id, "preview"))) + .pivot(egui::Align2::CENTER_CENTER) + .current_pos(mouse_pos) + .interactable(false) + .show(ui.ctx(), |ui| { + 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); + ui.label(text); + }); + }); + + 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; + + 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); + } + } + + 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 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( + &mut ui.child_ui(preview_rect, *ui.layout()), + dragged_node_id, + leaf, + ); + } + } + } + } + + if ui.input(|i| i.pointer.any_released()) { + ui.memory_mut(|mem| mem.stop_dragging()); + if let Some(insertion_point) = drop_context.best_insertion { + self.move_node(dragged_node_id, insertion_point); + } + self.smoothed_preview_rect = None; + } + } else { + self.smoothed_preview_rect = None; + } + } + + 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); + + let smoothed = self.smoothed_preview_rect.get_or_insert(new_rect); + *smoothed = smoothed.lerp_towards(&new_rect, t); + + let diff = smoothed.min.distance(new_rect.min) + smoothed.max.distance(new_rect.max); + if diff < 0.5 { + *smoothed = new_rect; + } else { + ctx.request_repaint(); + } + *smoothed + } + + fn simplify(&mut self, options: &SimplificationOptions) { + match self.nodes.simplify(options, self.root) { + SimplifyAction::Remove => { + log::warn!("Tried to simplify root node!"); // TODO: handle this + } + SimplifyAction::Keep => {} + SimplifyAction::Replace(new_root) => { + self.root = new_root; + } + } + } + + fn move_node(&mut self, moved_node_id: NodeId, insertion_point: InsertionPoint) { + log::debug!( + "Moving {moved_node_id:?} into {:?}", + insertion_point.insertion + ); + self.remove_node_id_from_parent(moved_node_id); + self.nodes.insert(insertion_point, moved_node_id); + } + + /// Find the currently dragged node, if any. + 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; + } + + for &node_id in self.nodes.nodes.keys() { + if node_id == self.root { + continue; // now allowed to drag root + } + + let id = node_id.id(); + let is_node_being_dragged = ctx.memory(|mem| mem.is_being_dragged(id)); + if is_node_being_dragged { + // Abort drags on escape: + if ctx.input(|i| i.key_pressed(egui::Key::Escape)) { + ctx.memory_mut(|mem| mem.stop_dragging()); + return None; + } + + return Some(node_id); + } + } + 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); + } +} diff --git a/crates/egui_extras/src/dock/mod.rs b/crates/egui_extras/src/dock/mod.rs index 8005fe6d5..5422139ed 100644 --- a/crates/egui_extras/src/dock/mod.rs +++ b/crates/egui_extras/src/dock/mod.rs @@ -1,31 +1,57 @@ -// # TODO -// * A new ui for each node, nested -// * Styling -// * Per-tab close-buttons -// * Scrolling of tab-bar -// * Adding extra stuff at the end of the tab-bar (e.g. an "Add new tab" button) -// * Vertical tab bar +//! # Dock +//! Tabs that can be dragged around and split up in horizontal, vertical, and grid-layouts. +//! +//! ## Overview +//! The user add leaves to a [`Dock`], arranged using [`Branch`]es. +//! This forms a layout tree. +//! Everything is generic over the type of leaves, leaving up to the user what to store in the tree. +//! +//! Each [`Node]` is either a `Leaf` or a [`Branch`]. +//! Each [`Node`] is identified by a (random) [`NodeId`]. +//! The nodes are stored in [`Nodes`]. +//! +//! The entire state is stored in a single [`Dock`] struct which consists of a [`Nodes`] and a root [`NodeId`]. +//! +//! The behavior and the look of the dock is controlled by the [`Behavior`] `trait`. +//! 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 +//! * Scrolling of tab-bar +//! * Vertical tab bar +//! * Auto-grid layouts (re-arange as parent is resized) -use std::collections::{HashMap, HashSet}; - -use egui::{Id, Key, NumExt, Pos2, Rect, Ui}; +use egui::{Id, Pos2, Rect}; mod behavior; mod branch; +mod dock_struct; +mod nodes; pub use behavior::Behavior; pub use branch::{Branch, Grid, GridLoc, Layout, Linear, LinearDir, Tabs}; +pub use dock_struct::Dock; +pub use nodes::Nodes; // ---------------------------------------------------------------------------- -// Types required for state -/// An identifier for a node in the dock tree, be it a branch or a leaf. +/// An identifier for a [`Node`] in the dock tree, be it a branch or a leaf. #[derive(Clone, Copy, Default, Hash, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub struct NodeId(u128); impl NodeId { - pub const ZERO: Self = Self(0); - + /// Generate a new random [`NodeId`]. pub fn random() -> Self { use rand::Rng as _; Self(rand::thread_rng().gen()) @@ -43,85 +69,9 @@ impl std::fmt::Debug for NodeId { } } -/// The top level type. Contains all persistent state, including layouts and sizes. -#[derive(Clone, serde::Serialize, serde::Deserialize)] -pub struct Dock { - pub root: NodeId, - pub nodes: Nodes, - - /// Smoothed avaerage of preview - #[serde(skip)] - pub smoothed_preview_rect: Option, -} - -impl Default for Dock { - fn default() -> Self { - Self { - root: Default::default(), - nodes: Default::default(), - smoothed_preview_rect: None, - } - } -} - -impl std::fmt::Debug for Dock { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - fn format_node( - f: &mut std::fmt::Formatter<'_>, - nodes: &Nodes, - indent: usize, - node_id: NodeId, - ) -> std::fmt::Result { - write!(f, "{} {node_id:?} ", " ".repeat(indent))?; - if let Some(node) = nodes.get(node_id) { - match node { - Node::Leaf(leaf) => writeln!(f, "Leaf {leaf:?}"), - Node::Branch(branch) => { - writeln!( - f, - "{}", - match branch { - Branch::Tabs(_) => "Tabs", - Branch::Linear(_) => "Linear", - Branch::Grid(_) => "Grid", - } - )?; - for &child in branch.children() { - format_node(f, nodes, indent + 1, child)?; - } - Ok(()) - } - } - } else { - write!(f, "DANGLING {node_id:?}") - } - } - - writeln!(f, "Dock {{")?; - format_node(f, &self.nodes, 1, self.root)?; - write!(f, "\n}}") - } -} - -/// Contains all node state, but no root. -#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] -pub struct Nodes { - pub nodes: HashMap>, - - /// Filled in by the layout step at the start of each frame. - #[serde(default, skip)] - pub rects: HashMap, -} - -impl Default for Nodes { - fn default() -> Self { - Self { - nodes: Default::default(), - rects: Default::default(), - } - } -} +// ---------------------------------------------------------------------------- +/// A node in the tree. Either a leaf or a [`Branch`]. #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] pub enum Node { Leaf(Leaf), @@ -137,33 +87,6 @@ impl Node { } } -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum LayoutInsertion { - Tabs(usize), - Horizontal(usize), - Vertical(usize), - Grid(GridLoc), -} - -#[derive(Clone, Copy, Debug)] -struct InsertionPoint { - parent_id: NodeId, - - /// Where in the parent? - insertion: LayoutInsertion, -} - -impl InsertionPoint { - fn new(parent_id: NodeId, insertion: LayoutInsertion) -> Self { - Self { - parent_id, - insertion, - } - } -} - -// ---------------------------------------------------------------------------- - #[must_use] #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum UiResponse { @@ -202,366 +125,43 @@ pub enum ResizeState { } // ---------------------------------------------------------------------------- -// Construction -impl Dock { - pub fn new(root: NodeId, nodes: Nodes) -> Self { - Self { - root, - nodes, - smoothed_preview_rect: None, - } - } - - pub fn parent_of(&self, node_id: NodeId) -> Option { - self.nodes - .nodes - .iter() - .find(|(_, node)| { - if let Node::Branch(branch) = node { - branch.children().contains(&node_id) - } else { - false - } - }) - .map(|(id, _)| *id) - } +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum LayoutInsertion { + Tabs(usize), + Horizontal(usize), + Vertical(usize), + Grid(GridLoc), } -impl Nodes { - pub fn try_rect(&self, node_id: NodeId) -> Option { - self.rects.get(&node_id).copied() - } +#[derive(Clone, Copy, Debug)] +struct InsertionPoint { + parent_id: NodeId, - pub 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)) - } + /// Where in the parent? + insertion: LayoutInsertion, +} - pub fn get(&self, node_id: NodeId) -> Option<&Node> { - self.nodes.get(&node_id) - } - - pub fn get_mut(&mut self, node_id: NodeId) -> Option<&mut Node> { - self.nodes.get_mut(&node_id) - } - - #[must_use] - pub fn insert_node(&mut self, node: Node) -> NodeId { - let id = NodeId::random(); - self.nodes.insert(id, node); - id - } - - #[must_use] - pub fn insert_leaf(&mut self, leaf: Leaf) -> NodeId { - self.insert_node(Node::Leaf(leaf)) - } - - #[must_use] - pub fn insert_branch(&mut self, branch: Branch) -> NodeId { - self.insert_node(Node::Branch(branch)) - } - - #[must_use] - pub fn insert_tab_node(&mut self, children: Vec) -> NodeId { - self.insert_node(Node::Branch(Branch::new_tabs(children))) - } - - #[must_use] - pub fn insert_horizontal_node(&mut self, children: Vec) -> NodeId { - self.insert_node(Node::Branch(Branch::new_linear( - LinearDir::Horizontal, - children, - ))) - } - - #[must_use] - pub fn insert_vertical_node(&mut self, children: Vec) -> NodeId { - self.insert_node(Node::Branch(Branch::new_linear( - LinearDir::Vertical, - children, - ))) - } - - #[must_use] - pub fn insert_grid_node(&mut self, children: Vec) -> NodeId { - self.insert_node(Node::Branch(Branch::new_grid(children))) - } - - /// Performs no simplifcations! - 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!("Unexepcted 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 - } - - fn insert(&mut self, insertion_point: InsertionPoint, child_id: NodeId) { - let InsertionPoint { +impl InsertionPoint { + fn new(parent_id: NodeId, insertion: LayoutInsertion) -> Self { + Self { parent_id, insertion, - } = insertion_point; - - let Some(mut node) = self.nodes.remove(&parent_id) else { - log::warn!("Failed to insert: could not find parent {parent_id:?}"); - return; - }; - - match insertion { - LayoutInsertion::Tabs(index) => { - if let Node::Branch(Branch::Tabs(tabs)) = &mut node { - let index = index.min(tabs.children.len()); - tabs.children.insert(index, child_id); - tabs.active = child_id; - self.nodes.insert(parent_id, node); - } else { - let new_node_id = self.insert_node(node); - let mut tabs = Tabs::new(vec![new_node_id]); - tabs.children.insert(index.min(1), child_id); - tabs.active = child_id; - self.nodes - .insert(parent_id, Node::Branch(Branch::Tabs(tabs))); - } - } - LayoutInsertion::Horizontal(index) => { - if let Node::Branch(Branch::Linear(Linear { - dir: LinearDir::Horizontal, - children, - .. - })) = &mut node - { - let index = index.min(children.len()); - children.insert(index, child_id); - self.nodes.insert(parent_id, node); - } else { - let new_node_id = self.insert_node(node); - let mut linear = Linear::new(LinearDir::Horizontal, vec![new_node_id]); - linear.children.insert(index.min(1), child_id); - self.nodes - .insert(parent_id, Node::Branch(Branch::Linear(linear))); - } - } - LayoutInsertion::Vertical(index) => { - if let Node::Branch(Branch::Linear(Linear { - dir: LinearDir::Vertical, - children, - .. - })) = &mut node - { - let index = index.min(children.len()); - children.insert(index, child_id); - self.nodes.insert(parent_id, node); - } else { - let new_node_id = self.insert_node(node); - let mut linear = Linear::new(LinearDir::Vertical, vec![new_node_id]); - linear.children.insert(index.min(1), child_id); - self.nodes - .insert(parent_id, Node::Branch(Branch::Linear(linear))); - } - } - LayoutInsertion::Grid(insert_location) => { - if let Node::Branch(Branch::Grid(grid)) = &mut node { - grid.locations.retain(|_, pos| *pos != insert_location); - grid.locations.insert(child_id, insert_location); - grid.children.push(child_id); - self.nodes.insert(parent_id, node); - } else { - let new_node_id = self.insert_node(node); - let mut grid = Grid::new(vec![new_node_id, child_id]); - grid.locations.insert(child_id, insert_location); - self.nodes - .insert(parent_id, Node::Branch(Branch::Grid(grid))); - } - } } } } -// Usage -impl Dock { - pub fn root(&self) -> NodeId { - self.root - } +#[derive(PartialEq, Eq)] +enum GcAction { + Keep, + Remove, +} - /// Show all the leaves in the dock. - pub fn ui(&mut self, behavior: &mut dyn Behavior, ui: &mut Ui) { - let options = behavior.simplification_options(); - self.simplify(&options); - if options.all_leaves_must_have_tabs { - self.nodes - .make_all_leaves_children_of_tabs(false, self.root); - } - - self.nodes.gc_root(behavior, self.root); - - self.nodes.rects.clear(); - - // Check if anything is being dragged: - let mut drop_context = DropContext { - enabled: true, - dragged_node_id: self.dragged_id(ui.ctx()), - mouse_pos: ui.input(|i| i.pointer.hover_pos()), - best_dist_sq: f32::INFINITY, - best_insertion: None, - preview_rect: None, - }; - - self.nodes.layout_node( - ui.style(), - behavior, - ui.available_rect_before_wrap(), - self.root, - ); - - self.nodes - .node_ui(behavior, &mut drop_context, ui, self.root); - - if let (Some(mouse_pos), Some(dragged_node_id)) = - (drop_context.mouse_pos, drop_context.dragged_node_id) - { - ui.output_mut(|o| o.cursor_icon = egui::CursorIcon::Grabbing); - - // Preview what is being dragged: - egui::Area::new(Id::new((dragged_node_id, "preview"))) - .pivot(egui::Align2::CENTER_CENTER) - .current_pos(mouse_pos) - .interactable(false) - .show(ui.ctx(), |ui| { - 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); - ui.label(text); - }); - }); - - 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; - - 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); - } - } - - 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 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( - &mut ui.child_ui(preview_rect, *ui.layout()), - dragged_node_id, - leaf, - ); - } - } - } - } - - if ui.input(|i| i.pointer.any_released()) { - ui.memory_mut(|mem| mem.stop_dragging()); - if let Some(insertion_point) = drop_context.best_insertion { - self.move_node(dragged_node_id, insertion_point); - } - self.smoothed_preview_rect = None; - } - } else { - self.smoothed_preview_rect = None; - } - } - - 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); - - let smoothed = self.smoothed_preview_rect.get_or_insert(new_rect); - *smoothed = smoothed.lerp_towards(&new_rect, t); - - let diff = smoothed.min.distance(new_rect.min) + smoothed.max.distance(new_rect.max); - if diff < 0.5 { - *smoothed = new_rect; - } else { - ctx.request_repaint(); - } - *smoothed - } - - fn simplify(&mut self, options: &SimplificationOptions) { - match self.nodes.simplify(options, self.root) { - SimplifyAction::Remove => { - log::warn!("Tried to simplify root node!"); // TODO: handle this - } - SimplifyAction::Keep => {} - SimplifyAction::Replace(new_root) => { - self.root = new_root; - } - } - } - - fn move_node(&mut self, moved_node_id: NodeId, insertion_point: InsertionPoint) { - log::debug!( - "Moving {moved_node_id:?} into {:?}", - insertion_point.insertion - ); - self.remove_node_id_from_parent(moved_node_id); - self.nodes.insert(insertion_point, moved_node_id); - } - - /// Find the currently dragged node, if any. - 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; - } - - for &node_id in self.nodes.nodes.keys() { - if node_id == self.root { - continue; // now allowed to drag root - } - - let id = node_id.id(); - let is_node_being_dragged = ctx.memory(|mem| mem.is_being_dragged(id)); - if is_node_being_dragged { - // Abort drags on escape: - if ctx.input(|i| i.key_pressed(Key::Escape)) { - ctx.memory_mut(|mem| mem.stop_dragging()); - return None; - } - - return Some(node_id); - } - } - 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); - } +#[must_use] +enum SimplifyAction { + Remove, + Keep, + Replace(NodeId), } fn is_possible_drag(ctx: &egui::Context) -> bool { @@ -577,104 +177,8 @@ fn is_being_dragged(ctx: &egui::Context, node_id: NodeId) -> bool { } // ---------------------------------------------------------------------------- -// gc -#[derive(PartialEq, Eq)] -enum GcAction { - Keep, - Remove, -} - -impl Nodes { - fn gc_root(&mut self, behavior: &mut dyn Behavior, root_id: NodeId) { - let mut visited = HashSet::default(); - self.gc_node_id(behavior, &mut visited, root_id); - - if visited.len() < self.nodes.len() { - log::warn!( - "GC collecting nodes: {:?}", - self.nodes - .keys() - .filter(|id| !visited.contains(id)) - .collect::>() - ); - } - - self.nodes.retain(|node_id, _| visited.contains(node_id)); - } - - fn gc_node_id( - &mut self, - behavior: &mut dyn Behavior, - visited: &mut HashSet, - node_id: NodeId, - ) -> GcAction { - let Some(mut node) = self.nodes.remove(&node_id) else { return GcAction::Remove; }; - if !visited.insert(node_id) { - log::warn!("Cycle or duplication detected"); - return GcAction::Remove; - } - - match &mut node { - Node::Leaf(leaf) => { - if !behavior.retain_leaf(leaf) { - return GcAction::Remove; - } - } - Node::Branch(branch) => { - branch.retain(|child| self.gc_node_id(behavior, visited, child) == GcAction::Keep); - } - } - self.nodes.insert(node_id, node); - GcAction::Keep - } -} - -// ---------------------------------------------------------------------------- -// layout - -impl Nodes { - fn layout_node( - &mut self, - style: &egui::Style, - behavior: &mut dyn Behavior, - rect: Rect, - node_id: NodeId, - ) { - let Some(mut node) = self.nodes.remove(&node_id) else { - log::warn!("Failed to find node {node_id:?} during layout"); - return; - }; - self.rects.insert(node_id, rect); - - if let Node::Branch(branch) = &mut node { - branch.layout(self, style, behavior, rect); - } - - self.nodes.insert(node_id, node); - } -} - -fn sizes_from_shares(shares: &[f32], available_size: f32, gap_width: f32) -> Vec { - assert!(!shares.is_empty()); - let available_size = available_size - gap_width * (shares.len() - 1) as f32; - let available_size = available_size.at_least(0.0); - - let total_share: f32 = shares.iter().sum(); - if total_share <= 0.0 { - vec![available_size / shares.len() as f32; shares.len()] - } else { - shares - .iter() - .map(|&share| share / total_share * available_size) - .collect() - } -} - -// ---------------------------------------------------------------------------- -// ui - -pub struct DropContext { +struct DropContext { enabled: bool, dragged_node_id: Option, mouse_pos: Option, @@ -733,143 +237,3 @@ impl DropContext { } } } - -impl Nodes { - fn node_ui( - &mut self, - behavior: &mut dyn Behavior, - drop_context: &mut DropContext, - ui: &mut Ui, - node_id: NodeId, - ) { - // NOTE: important that we get thr 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"); - return - }; - let Some(mut node) = self.nodes.remove(&node_id) else { - log::warn!("Failed to find node {node_id:?} during ui"); - return - }; - - let drop_context_was_enabled = drop_context.enabled; - if Some(node_id) == drop_context.dragged_node_id { - // 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, - ); - - let mut ui = egui::Ui::new( - ui.ctx().clone(), - ui.layer_id(), - ui.id().with(node_id), - rect, - rect, - ); - match &mut node { - Node::Leaf(leaf) => { - if behavior.leaf_ui(&mut ui, node_id, leaf) == UiResponse::DragStarted { - ui.memory_mut(|mem| mem.set_dragged_id(node_id.id())); - } - } - Node::Branch(branch) => { - branch.ui(self, behavior, drop_context, &mut ui, rect, node_id); - } - }; - - self.nodes.insert(node_id, node); - drop_context.enabled = drop_context_was_enabled; - } -} - -// ---------------------------------------------------------------------------- -// Simplification - -#[must_use] -enum SimplifyAction { - Remove, - Keep, - Replace(NodeId), -} - -impl Nodes { - fn simplify(&mut self, options: &SimplificationOptions, it: NodeId) -> SimplifyAction { - let Some(mut node) = self.nodes.remove(&it) else { - log::warn!("Failed to find node {it:?} during simplify"); - return SimplifyAction::Remove; - }; - - if let Node::Branch(branch) = &mut node { - // TODO: join nested versions of the same horizontal/vertical layouts - - branch.simplify_children(|child| self.simplify(options, child)); - - if branch.get_layout() == Layout::Tabs { - if options.prune_empty_tabs && branch.is_empty() { - log::debug!("Simplify: removing empty tabs node"); - return SimplifyAction::Remove; - } - if options.prune_single_child_tabs && branch.children().len() == 1 { - if options.all_leaves_must_have_tabs - && matches!(self.get(branch.children()[0]), Some(Node::Leaf(_))) - { - // Keep it - } else { - log::debug!("Simplify: collapsing single-child tabs node"); - return SimplifyAction::Replace(branch.children()[0]); - } - } - } else { - if options.prune_empty_layouts && branch.is_empty() { - log::debug!("Simplify: removing empty layout node"); - return SimplifyAction::Remove; - } - if options.prune_single_child_layouts && branch.children().len() == 1 { - log::debug!("Simplify: collapsing single-child layout node"); - return SimplifyAction::Replace(branch.children()[0]); - } - } - } - - self.nodes.insert(it, node); - SimplifyAction::Keep - } -} - -impl Nodes { - fn make_all_leaves_children_of_tabs(&mut self, parent_is_tabs: bool, it: NodeId) { - let Some(mut node) = self.nodes.remove(&it) else { - log::warn!("Failed to find node {it:?} during make_all_leaves_children_of_tabs"); - return; - }; - - match &mut node { - Node::Leaf(_) => { - if !parent_is_tabs { - // Add tabs to this leaf: - log::debug!("Auto-adding Tabs-parent to leaf {it:?}"); - let new_id = NodeId::random(); - self.nodes.insert(new_id, node); - self.nodes - .insert(it, Node::Branch(Branch::new_tabs(vec![new_id]))); - return; - } - } - Node::Branch(branch) => { - let is_tabs = branch.get_layout() == Layout::Tabs; - for &child in branch.children() { - self.make_all_leaves_children_of_tabs(is_tabs, child); - } - } - } - - self.nodes.insert(it, node); - } -} diff --git a/crates/egui_extras/src/dock/nodes.rs b/crates/egui_extras/src/dock/nodes.rs new file mode 100644 index 000000000..7ee67546e --- /dev/null +++ b/crates/egui_extras/src/dock/nodes.rs @@ -0,0 +1,222 @@ +use std::collections::{HashMap, HashSet}; + +use egui::{Rect, Ui}; + +use super::{ + Behavior, Branch, DropContext, GcAction, InsertionPoint, Layout, LayoutInsertion, Node, NodeId, + SimplificationOptions, SimplifyAction, UiResponse, +}; + +/// Contains all node state, but no root. +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +pub struct Nodes { + pub nodes: HashMap>, + + /// Filled in by the layout step at the start of each frame. + #[serde(default, skip)] + pub rects: HashMap, +} + +impl Default for Nodes { + fn default() -> Self { + Self { + nodes: Default::default(), + rects: Default::default(), + } + } +} + +// ---------------------------------------------------------------------------- + +impl Nodes { + pub(super) fn gc_root(&mut self, behavior: &mut dyn Behavior, root_id: NodeId) { + let mut visited = HashSet::default(); + self.gc_node_id(behavior, &mut visited, root_id); + + if visited.len() < self.nodes.len() { + log::warn!( + "GC collecting nodes: {:?}", + self.nodes + .keys() + .filter(|id| !visited.contains(id)) + .collect::>() + ); + } + + self.nodes.retain(|node_id, _| visited.contains(node_id)); + } + + fn gc_node_id( + &mut self, + behavior: &mut dyn Behavior, + visited: &mut HashSet, + node_id: NodeId, + ) -> GcAction { + let Some(mut node) = self.nodes.remove(&node_id) else { return GcAction::Remove; }; + if !visited.insert(node_id) { + log::warn!("Cycle or duplication detected"); + return GcAction::Remove; + } + + match &mut node { + Node::Leaf(leaf) => { + if !behavior.retain_leaf(leaf) { + return GcAction::Remove; + } + } + Node::Branch(branch) => { + branch.retain(|child| self.gc_node_id(behavior, visited, child) == GcAction::Keep); + } + } + self.nodes.insert(node_id, node); + GcAction::Keep + } + + pub(super) fn layout_node( + &mut self, + style: &egui::Style, + behavior: &mut dyn Behavior, + rect: Rect, + node_id: NodeId, + ) { + let Some(mut node) = self.nodes.remove(&node_id) else { + log::warn!("Failed to find node {node_id:?} during layout"); + return; + }; + self.rects.insert(node_id, rect); + + if let Node::Branch(branch) = &mut node { + branch.layout(self, style, behavior, rect); + } + + self.nodes.insert(node_id, node); + } + + pub(super) fn node_ui( + &mut self, + behavior: &mut dyn Behavior, + drop_context: &mut DropContext, + ui: &mut Ui, + node_id: NodeId, + ) { + // NOTE: important that we get thr 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"); + return + }; + let Some(mut node) = self.nodes.remove(&node_id) else { + log::warn!("Failed to find node {node_id:?} during ui"); + return + }; + + let drop_context_was_enabled = drop_context.enabled; + if Some(node_id) == drop_context.dragged_node_id { + // 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, + ); + + let mut ui = egui::Ui::new( + ui.ctx().clone(), + ui.layer_id(), + ui.id().with(node_id), + rect, + rect, + ); + match &mut node { + Node::Leaf(leaf) => { + if behavior.leaf_ui(&mut ui, node_id, leaf) == UiResponse::DragStarted { + ui.memory_mut(|mem| mem.set_dragged_id(node_id.id())); + } + } + Node::Branch(branch) => { + branch.ui(self, behavior, drop_context, &mut ui, rect, node_id); + } + }; + + self.nodes.insert(node_id, node); + drop_context.enabled = drop_context_was_enabled; + } + + pub(super) fn simplify( + &mut self, + options: &SimplificationOptions, + it: NodeId, + ) -> SimplifyAction { + let Some(mut node) = self.nodes.remove(&it) else { + log::warn!("Failed to find node {it:?} during simplify"); + return SimplifyAction::Remove; + }; + + if let Node::Branch(branch) = &mut node { + // TODO: join nested versions of the same horizontal/vertical layouts + + branch.simplify_children(|child| self.simplify(options, child)); + + if branch.get_layout() == Layout::Tabs { + if options.prune_empty_tabs && branch.is_empty() { + log::debug!("Simplify: removing empty tabs node"); + return SimplifyAction::Remove; + } + if options.prune_single_child_tabs && branch.children().len() == 1 { + if options.all_leaves_must_have_tabs + && matches!(self.get(branch.children()[0]), Some(Node::Leaf(_))) + { + // Keep it + } else { + log::debug!("Simplify: collapsing single-child tabs node"); + return SimplifyAction::Replace(branch.children()[0]); + } + } + } else { + if options.prune_empty_layouts && branch.is_empty() { + log::debug!("Simplify: removing empty layout node"); + return SimplifyAction::Remove; + } + if options.prune_single_child_layouts && branch.children().len() == 1 { + log::debug!("Simplify: collapsing single-child layout node"); + return SimplifyAction::Replace(branch.children()[0]); + } + } + } + + self.nodes.insert(it, node); + SimplifyAction::Keep + } + + pub(super) fn make_all_leaves_children_of_tabs(&mut self, parent_is_tabs: bool, it: NodeId) { + let Some(mut node) = self.nodes.remove(&it) else { + log::warn!("Failed to find node {it:?} during make_all_leaves_children_of_tabs"); + return; + }; + + match &mut node { + Node::Leaf(_) => { + if !parent_is_tabs { + // Add tabs to this leaf: + log::debug!("Auto-adding Tabs-parent to leaf {it:?}"); + let new_id = NodeId::random(); + self.nodes.insert(new_id, node); + self.nodes + .insert(it, Node::Branch(Branch::new_tabs(vec![new_id]))); + return; + } + } + Node::Branch(branch) => { + let is_tabs = branch.get_layout() == Layout::Tabs; + for &child in branch.children() { + self.make_all_leaves_children_of_tabs(is_tabs, child); + } + } + } + + self.nodes.insert(it, node); + } +}