From 53ce1904a29b846462e5f45ebc9a41cc35c267a6 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Wed, 26 Apr 2023 19:42:46 +0200 Subject: [PATCH] Code cleanup: move branch-gnostic code into branch.rs --- crates/egui_extras/src/dock/branch.rs | 496 ++++++++++++++++++++++ crates/egui_extras/src/dock/mod.rs | 583 ++------------------------ 2 files changed, 539 insertions(+), 540 deletions(-) create mode 100644 crates/egui_extras/src/dock/branch.rs diff --git a/crates/egui_extras/src/dock/branch.rs b/crates/egui_extras/src/dock/branch.rs new file mode 100644 index 000000000..10d41304a --- /dev/null +++ b/crates/egui_extras/src/dock/branch.rs @@ -0,0 +1,496 @@ +use std::collections::{BTreeMap, HashMap, HashSet}; + +use egui::{pos2, vec2, NumExt as _, Rect}; + +use super::{ + is_being_dragged, is_possible_drag, sizes_from_shares, Behavior, DropContext, GridLoc, + InsertionPoint, LayoutInsertion, NodeId, Nodes, +}; + +// ---------------------------------------------------------------------------- + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub enum Layout { + #[default] + Tabs, + Horizontal, + Vertical, + Grid, +} + +impl Layout { + pub const ALL: [Self; 4] = [Self::Tabs, Self::Horizontal, Self::Vertical, Self::Grid]; +} + +// ---------------------------------------------------------------------------- + +/// How large of a share of space each child has, on a 1D axis. +#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)] +pub struct Shares { + /// How large of a share each child has. + /// + /// For instance, the shares `[1, 2, 3]` means that the first child gets 1/6 of the space, + /// the second gets 2/6 and the third gets 3/6. + pub shares: HashMap, +} + +impl Shares { + pub fn replace_with(&mut self, a: NodeId, b: NodeId) { + if let Some(share) = self.shares.remove(&a) { + self.shares.insert(b, share); + } + } + + pub fn split(&self, children: &[NodeId], available_width: f32) -> Vec { + let mut num_shares = 0.0; + for child in children { + num_shares += self.shares.get(child).copied().unwrap_or(1.0); + } + if num_shares == 0.0 { + num_shares = 1.0; + } + children + .iter() + .map(|child| { + available_width * self.shares.get(child).copied().unwrap_or(1.0) / num_shares + }) + .collect() + } +} + +// ---------------------------------------------------------------------------- + +#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)] +pub struct Branch { + pub layout: Layout, + pub children: Vec, + + /// Only if [`Self.typ`] == [`Layout::Tab`] + pub active_tab: NodeId, + + /// Only for linear layouts (horizontal or vertical). + pub linear_shares: Shares, + + /// Only if [`Self.typ`] == [`Layout::Grid`]. + /// col,row + pub grid_locations: HashMap, + + /// Share of the avilable width assigned to each column. + pub grid_col_shares: Vec, + /// Share of the avilable height assigned to each row. + pub grid_row_shares: Vec, +} + +impl Branch { + pub fn new(layout: Layout, children: Vec) -> Self { + let active_tab = children.first().copied().unwrap_or_default(); + Self { + layout, + children, + active_tab, + ..Default::default() + } + } + + pub fn new_tabs(children: Vec) -> Self { + Self::new(Layout::Tabs, children) + } +} + +// ---------------------------------------------------------------------------- +// Layout + +impl Branch { + pub(super) fn layout( + &mut self, + nodes: &mut Nodes, + style: &egui::Style, + behavior: &mut dyn Behavior, + drop_context: &mut DropContext, + rect: Rect, + node_id: NodeId, + ) { + if self.children.is_empty() { + return; + } + + match self.layout { + Layout::Tabs => self.layout_tabs(nodes, style, behavior, drop_context, rect), + Layout::Horizontal => { + self.layout_horizontal(nodes, style, behavior, drop_context, rect); + } + Layout::Vertical => self.layout_vertical(nodes, style, behavior, drop_context, rect), + Layout::Grid => self.layout_grid(nodes, style, behavior, drop_context, rect, node_id), + } + } + + fn layout_tabs( + &self, + nodes: &mut Nodes, + style: &egui::Style, + behavior: &mut dyn Behavior, + drop_context: &mut DropContext, + rect: Rect, + ) { + let mut active_rect = rect; + active_rect.min.y += behavior.tab_bar_height(style); + + if false { + nodes.layout_node(style, behavior, drop_context, active_rect, self.active_tab); + } else { + // Layout all nodes in case the user switches active tab + // TODO: only layout active tab, or don't register drop-zones during layout. + for &child_id in &self.children { + nodes.layout_node(style, behavior, drop_context, active_rect, child_id); + } + } + } + + fn layout_horizontal( + &mut self, + nodes: &mut Nodes, + style: &egui::Style, + behavior: &mut dyn Behavior, + drop_context: &mut DropContext, + rect: Rect, + ) { + let num_gaps = self.children.len().saturating_sub(1); + let gap_width = behavior.gap_width(style); + let total_gap_width = gap_width * num_gaps as f32; + let available_width = (rect.width() - total_gap_width).at_least(0.0); + + let widths = self.linear_shares.split(&self.children, available_width); + + let mut x = rect.min.x; + for (child, width) in self.children.iter().zip(widths) { + let child_rect = Rect::from_min_size(pos2(x, rect.min.y), vec2(width, rect.height())); + nodes.layout_node(style, behavior, drop_context, child_rect, *child); + x += width + gap_width; + } + } + + fn layout_vertical( + &mut self, + nodes: &mut Nodes, + style: &egui::Style, + behavior: &mut dyn Behavior, + drop_context: &mut DropContext, + rect: Rect, + ) { + let num_gaps = self.children.len().saturating_sub(1); + let gap_height = behavior.gap_width(style); + let total_gap_height = gap_height * num_gaps as f32; + let available_height = (rect.height() - total_gap_height).at_least(0.0); + + let heights = self.linear_shares.split(&self.children, available_height); + + let mut y = rect.min.y; + for (child, height) in self.children.iter().zip(heights) { + let child_rect = Rect::from_min_size(pos2(rect.min.x, y), vec2(rect.width(), height)); + nodes.layout_node(style, behavior, drop_context, child_rect, *child); + y += height + gap_height; + } + } + + fn layout_grid( + &mut self, + nodes: &mut Nodes, + style: &egui::Style, + behavior: &mut dyn Behavior, + drop_context: &mut DropContext, + rect: Rect, + node_id: NodeId, + ) { + let child_ids: HashSet = self.children.iter().copied().collect(); + + let mut num_cols = 2.min(self.children.len()); // les than 2 and it is not a grid + + // Where to place each node? + let mut node_id_from_location: BTreeMap = Default::default(); + self.grid_locations.retain(|&child_id, &mut loc| { + if child_ids.contains(&child_id) { + match node_id_from_location.entry(loc) { + std::collections::btree_map::Entry::Occupied(_) => { + false // two nodes assigned to the same position - forget this one for now + } + std::collections::btree_map::Entry::Vacant(entry) => { + entry.insert(child_id); + num_cols = num_cols.max(loc.col + 1); + true + } + } + } else { + false // child no longer exists + } + }); + + // Find location for nodes that don't have one yet + let mut next_pos = 0; + for &child_id in &self.children { + if let std::collections::hash_map::Entry::Vacant(entry) = + self.grid_locations.entry(child_id) + { + // find a position: + loop { + let loc = GridLoc::from_col_row(next_pos % num_cols, next_pos / num_cols); + if node_id_from_location.contains_key(&loc) { + next_pos += 1; + continue; + } + entry.insert(loc); + node_id_from_location.insert(loc, child_id); + break; + } + } + } + + // Everything has a location - now we know how many rows we have: + let num_rows = node_id_from_location.keys().last().unwrap().row + 1; + + // Figure out where each column and row goes: + self.grid_col_shares.resize(num_cols, 1.0); + self.grid_row_shares.resize(num_rows, 1.0); + + let gap = behavior.gap_width(style); + let col_widths = sizes_from_shares(&self.grid_col_shares, rect.width(), gap); + let row_heights = sizes_from_shares(&self.grid_row_shares, rect.height(), gap); + + let mut col_x = vec![rect.left()]; + for &width in &col_widths { + col_x.push(col_x.last().unwrap() + width + gap); + } + + let mut row_y = vec![rect.top()]; + for &height in &row_heights { + row_y.push(row_y.last().unwrap() + height + gap); + } + + // Each child now has a location. Use this to order them, in case we will ater do auto-layouts: + self.children + .sort_by_key(|&child| self.grid_locations[&child]); + + // Place each child: + for &child in &self.children { + let loc = self.grid_locations[&child]; + let child_rect = Rect::from_min_size( + pos2(col_x[loc.col], row_y[loc.row]), + vec2(col_widths[loc.col], row_heights[loc.row]), + ); + nodes.layout_node(style, behavior, drop_context, child_rect, child); + } + + // Register drop-zones: + for col in 0..num_cols { + for row in 0..num_rows { + let cell_rect = Rect::from_min_size( + pos2(col_x[col], row_y[row]), + vec2(col_widths[col], row_heights[row]), + ); + drop_context.suggest_rect( + InsertionPoint::new( + node_id, + LayoutInsertion::Grid(GridLoc::from_col_row(col, row)), + ), + cell_rect, + ); + } + } + } +} + +// ---------------------------------------------------------------------------- +// UI +impl Branch { + pub(super) fn ui( + &mut self, + nodes: &mut Nodes, + behavior: &mut dyn Behavior, + drop_context: &mut DropContext, + ui: &mut egui::Ui, + rect: Rect, + node_id: NodeId, + ) { + match self.layout { + Layout::Tabs => { + self.tabs_ui(nodes, behavior, drop_context, ui, rect, node_id); + } + Layout::Horizontal => { + self.horizontal_ui(nodes, behavior, drop_context, ui, node_id); + } + Layout::Vertical => { + self.vertical_ui(nodes, behavior, drop_context, ui, node_id); + } + Layout::Grid => { + self.grid_ui(nodes, behavior, drop_context, ui); + } + } + } + + fn tabs_ui( + &mut self, + nodes: &mut Nodes, + behavior: &mut dyn Behavior, + drop_context: &mut DropContext, + ui: &mut egui::Ui, + rect: Rect, + node_id: NodeId, + ) { + if !self.children.iter().any(|&child| child == self.active_tab) { + // Make sure something is active: + self.active_tab = self.children.first().copied().unwrap_or_default(); + } + + let tab_bar_height = behavior.tab_bar_height(ui.style()); + let tab_bar_rect = rect.split_top_bottom_at_y(rect.top() + tab_bar_height).0; + let mut tab_bar_ui = ui.child_ui(tab_bar_rect, *ui.layout()); + + // Show tab bar: + tab_bar_ui.horizontal(|ui| { + let mut prev_tab_rect: Option = None; + let mut insertion_index = 0; // skips over drag-source, if any, beacuse it will be removed then re-inserted + + for (i, &child_id) in self.children.iter().enumerate() { + if is_being_dragged(ui.ctx(), child_id) { + continue; // leave a gap! + } + + let selected = child_id == self.active_tab; + let id = child_id.id(); + + let response = behavior.tab_ui(nodes, ui, id, child_id, selected); + let response = response.on_hover_cursor(egui::CursorIcon::Grab); + if response.clicked() { + self.active_tab = child_id; + } + + if let Some(mouse_pos) = drop_context.mouse_pos { + if drop_context.dragged_node_id.is_some() && response.rect.contains(mouse_pos) { + // Expand this tab - maybe the user wants to drop something into it! + self.active_tab = child_id; + } + } + + let rect = response.rect; + + { + // suggest dropping before this tab: + let before_point = if let Some(prev_tab_rect) = prev_tab_rect { + // between + prev_tab_rect.right_center().lerp(rect.left_center(), 0.5) + } else { + // before first + rect.left_center() + }; + + drop_context.suggest_rect( + InsertionPoint::new(node_id, LayoutInsertion::Tabs(insertion_index)), + Rect::from_center_size(before_point, vec2(4.0, rect.height())), + ); + } + + if i + 1 == self.children.len() { + // suggest dropping after last tab: + drop_context.suggest_rect( + InsertionPoint::new(node_id, LayoutInsertion::Tabs(insertion_index + 1)), + Rect::from_center_size(rect.right_center(), vec2(4.0, rect.height())), + ); + } + + prev_tab_rect = Some(rect); + insertion_index += 1; + } + }); + + // When dragged, don't show it (it is "being held") + let is_active_being_dragged = ui.memory(|mem| mem.is_being_dragged(self.active_tab.id())) + && is_possible_drag(ui.ctx()); + if !is_active_being_dragged { + nodes.node_ui(behavior, drop_context, ui, self.active_tab); + } + } + + fn horizontal_ui( + &mut self, + nodes: &mut Nodes, + behavior: &mut dyn Behavior, + drop_context: &mut DropContext, + ui: &mut egui::Ui, + parent_id: NodeId, + ) { + let mut prev_rect: Option = None; + let mut insertion_index = 0; // skips over drag-source, if any, beacuse it will be removed then re-inserted + + for (i, &child) in self.children.iter().enumerate() { + let Some(rect) = nodes.rect(child) else { continue; }; + + if is_being_dragged(ui.ctx(), child) { + // Leave a hole, and suggest that hole as drop-target: + drop_context.suggest_rect( + InsertionPoint::new(parent_id, LayoutInsertion::Horizontal(i)), + rect, + ); + } else { + nodes.node_ui(behavior, drop_context, ui, child); + + if let Some(prev_rect) = prev_rect { + // Suggest dropping between the rects: + drop_context.suggest_rect( + InsertionPoint::new( + parent_id, + LayoutInsertion::Horizontal(insertion_index), + ), + Rect::from_min_max(prev_rect.center_top(), rect.center_bottom()), + ); + } else { + // Suggest dropping before the first child: + drop_context.suggest_rect( + InsertionPoint::new(parent_id, LayoutInsertion::Horizontal(0)), + rect.split_left_right_at_fraction(0.66).0, + ); + } + + if i + 1 == self.children.len() { + // Suggest dropping after the last child: + drop_context.suggest_rect( + InsertionPoint::new( + parent_id, + LayoutInsertion::Horizontal(insertion_index + 1), + ), + rect.split_left_right_at_fraction(0.33).1, + ); + } + insertion_index += 1; + } + + prev_rect = Some(rect); + } + } + + fn vertical_ui( + &mut self, + nodes: &mut Nodes, + behavior: &mut dyn Behavior, + drop_context: &mut DropContext, + ui: &mut egui::Ui, + parent_id: NodeId, + ) { + // TODO: drag-and-drop + for child in &self.children { + nodes.node_ui(behavior, drop_context, ui, *child); + } + } + + fn grid_ui( + &mut self, + nodes: &mut Nodes, + behavior: &mut dyn Behavior, + drop_context: &mut DropContext, + ui: &mut egui::Ui, + ) { + // Grid drops are handled during layout. TODO: handle here instead. + + for &child in &self.children { + nodes.node_ui(behavior, drop_context, ui, child); + } + } +} diff --git a/crates/egui_extras/src/dock/mod.rs b/crates/egui_extras/src/dock/mod.rs index 2debdb75d..a7917d7a2 100644 --- a/crates/egui_extras/src/dock/mod.rs +++ b/crates/egui_extras/src/dock/mod.rs @@ -1,9 +1,10 @@ -use std::collections::{BTreeMap, HashMap, HashSet}; +use std::collections::{HashMap, HashSet}; -use egui::{ - pos2, vec2, CursorIcon, Id, Key, NumExt, Pos2, Rect, Response, Sense, Style, TextStyle, Ui, - WidgetText, -}; +use egui::{Id, Key, NumExt, Pos2, Rect, Response, Sense, TextStyle, Ui, WidgetText}; + +mod branch; + +pub use branch::{Branch, Layout}; // ---------------------------------------------------------------------------- // Types required for state @@ -113,90 +114,6 @@ impl GridLoc { } } -#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)] -pub struct Branch { - pub layout: Layout, - pub children: Vec, - - /// Only if [`Self.typ`] == [`Layout::Tab`] - pub active_tab: NodeId, - - /// Only for linear layouts (horizontal or vertical). - pub linear_shares: Shares, - - /// Only if [`Self.typ`] == [`Layout::Grid`]. - /// col,row - pub grid_locations: HashMap, - - /// Share of the avilable width assigned to each column. - pub grid_col_shares: Vec, - /// Share of the avilable height assigned to each row. - pub grid_row_shares: Vec, -} - -impl Branch { - pub fn new(layout: Layout, children: Vec) -> Self { - let active_tab = children.first().copied().unwrap_or_default(); - Self { - layout, - children, - active_tab, - ..Default::default() - } - } - - pub fn new_tabs(children: Vec) -> Self { - Self::new(Layout::Tabs, children) - } -} - -/// How large of a share of space each child has, on a 1D axis. -#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)] -pub struct Shares { - /// How large of a share each child has. - /// - /// For instance, the shares `[1, 2, 3]` means that the first child gets 1/6 of the space, - /// the second gets 2/6 and the third gets 3/6. - pub shares: HashMap, -} - -impl Shares { - fn replace_with(&mut self, a: NodeId, b: NodeId) { - if let Some(share) = self.shares.remove(&a) { - self.shares.insert(b, share); - } - } - - fn split(&self, children: &[NodeId], available_width: f32) -> Vec { - let mut num_shares = 0.0; - for child in children { - num_shares += self.shares.get(child).copied().unwrap_or(1.0); - } - if num_shares == 0.0 { - num_shares = 1.0; - } - children - .iter() - .map(|child| { - available_width * self.shares.get(child).copied().unwrap_or(1.0) / num_shares - }) - .collect() - } -} - -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -pub enum Layout { - #[default] - Tabs, - Horizontal, - Vertical, - Grid, -} - -impl Layout { - pub const ALL: [Self; 4] = [Self::Tabs, Self::Horizontal, Self::Vertical, Self::Grid]; -} - #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum LayoutInsertion { Tabs(usize), @@ -298,12 +215,12 @@ pub trait Behavior { // Settings: /// The height of the bar holding tab names. - fn tab_bar_height(&self, _style: &Style) -> f32 { + fn tab_bar_height(&self, _style: &egui::Style) -> f32 { 20.0 } /// Width of the gap between nodes in a horizontal or vertical layout - fn gap_width(&self, _style: &Style) -> f32 { + fn gap_width(&self, _style: &egui::Style) -> f32 { 1.0 } @@ -378,8 +295,8 @@ impl Nodes { fn parent(&self, it: NodeId, needle_child: NodeId) -> Option { match &self.nodes.get(&it)? { Node::Leaf(_) => None, - Node::Branch(Branch { children, .. }) => { - for &child in children { + Node::Branch(branch) => { + for &child in &branch.children { if child == needle_child { return Some(it); } @@ -412,10 +329,12 @@ impl Nodes { 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 { @@ -533,7 +452,7 @@ impl Dock { 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 = CursorIcon::Grabbing); + ui.output_mut(|o| o.cursor_icon = egui::CursorIcon::Grabbing); // Preview what is being dragged: egui::Area::new(Id::new((dragged_node_id, "preview"))) @@ -725,8 +644,9 @@ impl Nodes { return GcAction::Remove; } } - Node::Branch(Branch { children, .. }) => { - children + Node::Branch(branch) => { + branch + .children .retain(|&child| self.gc_node_id(behavior, visited, child) == GcAction::Keep); } } @@ -741,7 +661,7 @@ impl Nodes { impl Nodes { fn layout_node( &mut self, - style: &Style, + style: &egui::Style, behavior: &mut dyn Behavior, drop_context: &mut DropContext, rect: Rect, @@ -751,224 +671,11 @@ impl Nodes { self.rects.insert(node_id, rect); if let Node::Branch(branch) = &mut node { - self.layout_branch(style, behavior, drop_context, rect, node_id, branch); + branch.layout(self, style, behavior, drop_context, rect, node_id); } self.nodes.insert(node_id, node); } - - fn layout_branch( - &mut self, - style: &Style, - behavior: &mut dyn Behavior, - drop_context: &mut DropContext, - rect: Rect, - node_id: NodeId, - branch: &mut Branch, - ) { - match branch.layout { - Layout::Tabs => self.layout_tabs(style, behavior, drop_context, rect, branch), - Layout::Horizontal => { - self.layout_horizontal(style, behavior, drop_context, rect, branch); - } - Layout::Vertical => self.layout_vertical(style, behavior, drop_context, rect, branch), - Layout::Grid => self.layout_grid(style, behavior, drop_context, rect, node_id, branch), - } - } - - fn layout_tabs( - &mut self, - style: &Style, - behavior: &mut dyn Behavior, - drop_context: &mut DropContext, - rect: Rect, - branch: &Branch, - ) { - let mut active_rect = rect; - active_rect.min.y += behavior.tab_bar_height(style); - - if false { - self.layout_node( - style, - behavior, - drop_context, - active_rect, - branch.active_tab, - ); - } else { - // Layout all nodes in case the user switches active tab - // TODO: only layout active tab, or don't register drop-zones during layout. - for &child_id in &branch.children { - self.layout_node(style, behavior, drop_context, active_rect, child_id); - } - } - } - - fn layout_horizontal( - &mut self, - style: &Style, - behavior: &mut dyn Behavior, - drop_context: &mut DropContext, - rect: Rect, - branch: &Branch, - ) { - if branch.children.is_empty() { - return; - } - let num_gaps = branch.children.len() - 1; - let gap_width = behavior.gap_width(style); - let total_gap_width = gap_width * num_gaps as f32; - let available_width = (rect.width() - total_gap_width).at_least(0.0); - - let widths = branch - .linear_shares - .split(&branch.children, available_width); - - let mut x = rect.min.x; - for (child, width) in branch.children.iter().zip(widths) { - let child_rect = Rect::from_min_size(pos2(x, rect.min.y), vec2(width, rect.height())); - self.layout_node(style, behavior, drop_context, child_rect, *child); - x += width + gap_width; - } - } - - fn layout_vertical( - &mut self, - style: &Style, - behavior: &mut dyn Behavior, - drop_context: &mut DropContext, - rect: Rect, - branch: &Branch, - ) { - if branch.children.is_empty() { - return; - } - let num_gaps = branch.children.len() - 1; - let gap_height = behavior.gap_width(style); - let total_gap_height = gap_height * num_gaps as f32; - let available_height = (rect.height() - total_gap_height).at_least(0.0); - - let heights = branch - .linear_shares - .split(&branch.children, available_height); - - let mut y = rect.min.y; - for (child, height) in branch.children.iter().zip(heights) { - let child_rect = Rect::from_min_size(pos2(rect.min.x, y), vec2(rect.width(), height)); - self.layout_node(style, behavior, drop_context, child_rect, *child); - y += height + gap_height; - } - } - - fn layout_grid( - &mut self, - style: &Style, - behavior: &mut dyn Behavior, - drop_context: &mut DropContext, - rect: Rect, - node_id: NodeId, - branch: &mut Branch, - ) { - if branch.children.is_empty() { - return; - } - - let child_ids: HashSet = branch.children.iter().copied().collect(); - - let mut num_cols = 2.min(branch.children.len()); // les than 2 and it is not a grid - - // Where to place each node? - let mut node_id_from_location: BTreeMap = Default::default(); - branch.grid_locations.retain(|&child_id, &mut loc| { - if child_ids.contains(&child_id) { - match node_id_from_location.entry(loc) { - std::collections::btree_map::Entry::Occupied(_) => { - false // two nodes assigned to the same position - forget this one for now - } - std::collections::btree_map::Entry::Vacant(entry) => { - entry.insert(child_id); - num_cols = num_cols.max(loc.col + 1); - true - } - } - } else { - false // child no longer exists - } - }); - - // Find location for nodes that don't have one yet - let mut next_pos = 0; - for &child_id in &branch.children { - if let std::collections::hash_map::Entry::Vacant(entry) = - branch.grid_locations.entry(child_id) - { - // find a position: - loop { - let loc = GridLoc::from_col_row(next_pos % num_cols, next_pos / num_cols); - if node_id_from_location.contains_key(&loc) { - next_pos += 1; - continue; - } - entry.insert(loc); - node_id_from_location.insert(loc, child_id); - break; - } - } - } - - // Everything has a location - now we know how many rows we have: - let num_rows = node_id_from_location.keys().last().unwrap().row + 1; - - // Figure out where each column and row goes: - branch.grid_col_shares.resize(num_cols, 1.0); - branch.grid_row_shares.resize(num_rows, 1.0); - - let gap = behavior.gap_width(style); - let col_widths = sizes_from_shares(&branch.grid_col_shares, rect.width(), gap); - let row_heights = sizes_from_shares(&branch.grid_row_shares, rect.height(), gap); - - let mut col_x = vec![rect.left()]; - for &width in &col_widths { - col_x.push(col_x.last().unwrap() + width + gap); - } - - let mut row_y = vec![rect.top()]; - for &height in &row_heights { - row_y.push(row_y.last().unwrap() + height + gap); - } - - // Each child now has a location. Use this to order them, in case we will ater do auto-layouts: - branch - .children - .sort_by_key(|&child| branch.grid_locations[&child]); - - // Place each child: - for &child in &branch.children { - let loc = branch.grid_locations[&child]; - let child_rect = Rect::from_min_size( - pos2(col_x[loc.col], row_y[loc.row]), - vec2(col_widths[loc.col], row_heights[loc.row]), - ); - self.layout_node(style, behavior, drop_context, child_rect, child); - } - - // Register drop-zones: - for col in 0..num_cols { - for row in 0..num_rows { - let cell_rect = Rect::from_min_size( - pos2(col_x[col], row_y[row]), - vec2(col_widths[col], row_heights[row]), - ); - drop_context.suggest_rect( - InsertionPoint::new( - node_id, - LayoutInsertion::Grid(GridLoc::from_col_row(col, row)), - ), - cell_rect, - ); - } - } - } } fn sizes_from_shares(shares: &[f32], available_size: f32, gap_width: f32) -> Vec { @@ -1087,210 +794,13 @@ impl Nodes { } } Node::Branch(branch) => { - self.branch_ui(behavior, drop_context, &mut ui, rect, node_id, 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; } - - fn branch_ui( - &mut self, - behavior: &mut dyn Behavior, - drop_context: &mut DropContext, - ui: &mut Ui, - rect: Rect, - node_id: NodeId, - branch: &mut Branch, - ) { - match branch.layout { - Layout::Tabs => { - self.tabs_ui(behavior, drop_context, ui, rect, node_id, branch); - } - Layout::Horizontal => { - self.horizontal_ui(behavior, drop_context, ui, node_id, branch); - } - Layout::Vertical => { - self.vertical_ui(behavior, drop_context, ui, node_id, branch); - } - Layout::Grid => { - self.grid_ui(behavior, drop_context, ui, branch); - } - } - } - - fn tabs_ui( - &mut self, - behavior: &mut dyn Behavior, - drop_context: &mut DropContext, - ui: &mut Ui, - rect: Rect, - node_id: NodeId, - branch: &mut Branch, - ) { - if !branch - .children - .iter() - .any(|&child| child == branch.active_tab) - { - // Make sure something is active: - branch.active_tab = branch.children.first().copied().unwrap_or_default(); - } - - let tab_bar_height = behavior.tab_bar_height(ui.style()); - let tab_bar_rect = rect.split_top_bottom_at_y(rect.top() + tab_bar_height).0; - let mut tab_bar_ui = ui.child_ui(tab_bar_rect, *ui.layout()); - - // Show tab bar: - tab_bar_ui.horizontal(|ui| { - let mut prev_tab_rect: Option = None; - let mut insertion_index = 0; // skips over drag-source, if any, beacuse it will be removed then re-inserted - - for (i, &child_id) in branch.children.iter().enumerate() { - if is_being_dragged(ui.ctx(), child_id) { - continue; // leave a gap! - } - - let selected = child_id == branch.active_tab; - let id = child_id.id(); - - let response = behavior.tab_ui(self, ui, id, child_id, selected); - let response = response.on_hover_cursor(CursorIcon::Grab); - if response.clicked() { - branch.active_tab = child_id; - } - - if let Some(mouse_pos) = drop_context.mouse_pos { - if drop_context.dragged_node_id.is_some() && response.rect.contains(mouse_pos) { - // Expand this tab - maybe the user wants to drop something into it! - branch.active_tab = child_id; - } - } - - let rect = response.rect; - - { - // suggest dropping before this tab: - let before_point = if let Some(prev_tab_rect) = prev_tab_rect { - // between - prev_tab_rect.right_center().lerp(rect.left_center(), 0.5) - } else { - // before first - rect.left_center() - }; - - drop_context.suggest_rect( - InsertionPoint::new(node_id, LayoutInsertion::Tabs(insertion_index)), - Rect::from_center_size(before_point, vec2(4.0, rect.height())), - ); - } - - if i + 1 == branch.children.len() { - // suggest dropping after last tab: - drop_context.suggest_rect( - InsertionPoint::new(node_id, LayoutInsertion::Tabs(insertion_index + 1)), - Rect::from_center_size(rect.right_center(), vec2(4.0, rect.height())), - ); - } - - prev_tab_rect = Some(rect); - insertion_index += 1; - } - }); - - // When dragged, don't show it (it is "being held") - let is_active_being_dragged = ui.memory(|mem| mem.is_being_dragged(branch.active_tab.id())) - && is_possible_drag(ui.ctx()); - if !is_active_being_dragged { - self.node_ui(behavior, drop_context, ui, branch.active_tab); - } - } - - fn horizontal_ui( - &mut self, - behavior: &mut dyn Behavior, - drop_context: &mut DropContext, - ui: &mut Ui, - parent_id: NodeId, - branch: &mut Branch, - ) { - let mut prev_rect: Option = None; - let mut insertion_index = 0; // skips over drag-source, if any, beacuse it will be removed then re-inserted - - for (i, &child) in branch.children.iter().enumerate() { - let Some(rect) = self.rect(child) else { continue; }; - - if is_being_dragged(ui.ctx(), child) { - // Leave a hole, and suggest that hole as drop-target: - drop_context.suggest_rect( - InsertionPoint::new(parent_id, LayoutInsertion::Horizontal(i)), - rect, - ); - } else { - self.node_ui(behavior, drop_context, ui, child); - - if let Some(prev_rect) = prev_rect { - // Suggest dropping between the rects: - drop_context.suggest_rect( - InsertionPoint::new( - parent_id, - LayoutInsertion::Horizontal(insertion_index), - ), - Rect::from_min_max(prev_rect.center_top(), rect.center_bottom()), - ); - } else { - // Suggest dropping before the first child: - drop_context.suggest_rect( - InsertionPoint::new(parent_id, LayoutInsertion::Horizontal(0)), - rect.split_left_right_at_fraction(0.66).0, - ); - } - - if i + 1 == branch.children.len() { - // Suggest dropping after the last child: - drop_context.suggest_rect( - InsertionPoint::new( - parent_id, - LayoutInsertion::Horizontal(insertion_index + 1), - ), - rect.split_left_right_at_fraction(0.33).1, - ); - } - insertion_index += 1; - } - - prev_rect = Some(rect); - } - } - - fn vertical_ui( - &mut self, - behavior: &mut dyn Behavior, - drop_context: &mut DropContext, - ui: &mut Ui, - parent_id: NodeId, - branch: &mut Branch, - ) { - // TODO: drag-and-drop - for child in &branch.children { - self.node_ui(behavior, drop_context, ui, *child); - } - } - - fn grid_ui( - &mut self, - behavior: &mut dyn Behavior, - drop_context: &mut DropContext, - ui: &mut Ui, - branch: &mut Branch, - ) { - // Grid drops are handled during layout - - for &child in &branch.children { - self.node_ui(behavior, drop_context, ui, child); - } - } } // ---------------------------------------------------------------------------- @@ -1329,33 +839,30 @@ impl Nodes { } }); - match branch.layout { - Layout::Tabs => { - if options.prune_empty_tabs && branch.children.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]); - } - } + if branch.layout == Layout::Tabs { + if options.prune_empty_tabs && branch.children.is_empty() { + log::debug!("Simplify: removing empty tabs node"); + return SimplifyAction::Remove; } - Layout::Horizontal | Layout::Vertical | Layout::Grid => { - if options.prune_empty_layouts && branch.children.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"); + 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.children.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]); + } } } @@ -1380,14 +887,10 @@ impl Nodes { return; } } - Node::Branch(Branch { - layout: typ, - children, - .. - }) => { - let is_tabs = *typ == Layout::Tabs; - for child in children { - self.make_all_leaves_children_of_tabs(is_tabs, *child); + Node::Branch(branch) => { + let is_tabs = branch.layout == Layout::Tabs; + for &child in &branch.children { + self.make_all_leaves_children_of_tabs(is_tabs, child); } } }