From 9b154c6d1de12e8ada5613644bcca6a04a50b1c2 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sun, 23 Apr 2023 20:28:23 +0200 Subject: [PATCH] Preview drag destinations --- crates/egui_extras/src/dock/mod.rs | 226 +++++++++++++++++++++++------ crates/emath/src/rect.rs | 24 +++ examples/dock/src/main.rs | 4 +- 3 files changed, 211 insertions(+), 43 deletions(-) diff --git a/crates/egui_extras/src/dock/mod.rs b/crates/egui_extras/src/dock/mod.rs index 0d11d190b..0d37793fd 100644 --- a/crates/egui_extras/src/dock/mod.rs +++ b/crates/egui_extras/src/dock/mod.rs @@ -1,8 +1,8 @@ use std::collections::{HashMap, HashSet}; use egui::{ - pos2, vec2, CursorIcon, Id, Key, NumExt, Rect, Response, Sense, Style, TextStyle, Ui, - WidgetText, + pos2, vec2, Color32, CursorIcon, Id, Key, NumExt, Pos2, Rect, Response, Sense, Style, + TextStyle, Ui, WidgetText, }; // ---------------------------------------------------------------------------- @@ -277,40 +277,80 @@ impl Dock { self.nodes.node_ui(behavior, ui, self.root); - if let Some(mouse_pos) = ui.input(|i| { - if i.pointer.could_any_button_be_click() { - // Wait until the mouse has move a bit or been down long enough - // before registerint a drag - None - } else { - i.pointer.hover_pos() - } - }) { - // Check if anything is being dragged: - for (node_id, node) in &self.nodes.nodes { - let id = node_id.id(); - if ui.memory(|mem| mem.is_being_dragged(id)) { - // Abort on escape: - if ui.input(|i| i.key_pressed(Key::Escape)) { - ui.memory_mut(|mem| mem.stop_dragging()); - continue; - } + // Check if anything is being dragged: + let mouse_pos = ui.input(|i| i.pointer.hover_pos()); + let dragged_id = self.dragged_id(ui.ctx()); + if let (Some(mouse_pos), Some(dragged_node_id)) = (mouse_pos, dragged_id) { + // 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| { + egui::Frame::popup(ui.style()).show(ui, |ui| { + // TODO: preview contents + let text = behavior.tab_text_for_node(&self.nodes, dragged_node_id); + ui.label(text); + }); + }); - // This node is being dragged! - egui::Area::new(id.with("preview")) - .pivot(egui::Align2::CENTER_CENTER) - .current_pos(mouse_pos) - .interactable(false) - .show(ui.ctx(), |ui| { - egui::Frame::popup(ui.style()).show(ui, |ui| { - // TODO: preview contents - let text = behavior.tab_text_for_node(&self.nodes, *node_id); - ui.label(text); - }); - }); + let mut drop_context = DropContext { + dragged_node_id, + mouse_pos, + best_dist_sq: f32::INFINITY, + target_node_id: None, + preview_rect: None, + }; + self.nodes.find_drop_target(&mut drop_context, self.root); + + if let Some(preview_rect) = drop_context.preview_rect { + ui.painter().rect_filled( + preview_rect, + 1.0, + Color32::LIGHT_BLUE.gamma_multiply(0.5), + ); + } + + // if ui.input(|i| i.pointer.any_released()) { + // ui.memory_mut(|mem| mem.stop_dragging()); + // if let Some(target_node_id) = drop_context.target_node_id { + // self.remove_node_id_from_parent(dragged_node_id); + // // self.drop_node(behavior, target_node_id, dragged_node_id); // TODO + // } + // } + } + } + + /// Find the currently dragged node, if any. + fn dragged_id(&self, ctx: &egui::Context) -> Option { + if ctx.input(|i| i.pointer.could_any_button_be_click()) { + // 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 + } + + fn remove_node_id_from_parent(&mut self, dragged_node_id: NodeId) { + self.nodes + .remove_node_id_from_parent(self.root, dragged_node_id); } } @@ -349,14 +389,9 @@ impl Nodes { return GcAction::Remove; } } - NodeLayout::Tabs(layout) => { - layout - .children - .retain(|&child| self.gc_node_id(behavior, visited, child) == GcAction::Keep); - } - NodeLayout::Horizontal(layout) => { - layout - .children + NodeLayout::Tabs(Tabs { children, .. }) + | NodeLayout::Horizontal(Horizontal { children, .. }) => { + children .retain(|&child| self.gc_node_id(behavior, visited, child) == GcAction::Keep); } } @@ -480,9 +515,16 @@ impl Nodes { for &child_id in &tabs.children { let selected = child_id == tabs.active; let id = child_id.id(); + + let is_node_being_dragged = ui.memory(|mem| mem.is_being_dragged(id)) + && !ui.input(|i| i.pointer.could_any_button_be_click()); + if is_node_being_dragged { + continue; // leave a gap! + } + let response = behavior.tab_ui(self, ui, id, child_id, selected); let response = response.on_hover_cursor(CursorIcon::Grab); - if response.clicked() { + if response.clicked() || response.drag_started() { tabs.active = child_id; } } @@ -503,3 +545,105 @@ impl Nodes { } } } + +// ---------------------------------------------------------------------------- +// Dropping + +struct DropContext { + dragged_node_id: NodeId, + mouse_pos: Pos2, + + target_node_id: Option, + best_dist_sq: f32, + preview_rect: Option, +} + +impl DropContext { + fn suggest_point(&mut self, node_id: NodeId, target_point: Pos2, preview_rect: Rect) { + let dist_sq = self.mouse_pos.distance_sq(target_point); + if dist_sq < self.best_dist_sq { + self.best_dist_sq = dist_sq; + self.target_node_id = Some(node_id); + self.preview_rect = Some(preview_rect); + } + } +} + +impl Nodes { + fn find_drop_target(&self, drop_context: &mut DropContext, node_id: NodeId) { + if drop_context.dragged_node_id == node_id { + // Can't drag a node onto self or any children + return; + } + let Some(node) = self.nodes.get(&node_id) else { return; }; + + drop_context.suggest_point( + node_id, + node.rect.left_center(), + node.rect.split_left_right_at_fraction(0.5).0, + ); + drop_context.suggest_point( + node_id, + node.rect.right_center(), + node.rect.split_left_right_at_fraction(0.5).1, + ); + drop_context.suggest_point( + node_id, + node.rect.center_top(), + node.rect.split_top_bottom_at_fraction(0.5).0, + ); + drop_context.suggest_point( + node_id, + node.rect.center_bottom(), + node.rect.split_top_bottom_at_fraction(0.5).1, + ); + drop_context.suggest_point(node_id, node.rect.center(), node.rect); + + match &node.layout { + NodeLayout::Leaf(_) => {} + NodeLayout::Tabs(Tabs { active, .. }) => { + if let Some(active_node) = self.nodes.get(active) { + // Suggest dropping into tab bar + let tabs_rect = active_node + .rect + .split_top_bottom_at_y(active_node.rect.top()) + .0; + if tabs_rect.contains(drop_context.mouse_pos) { + drop_context.suggest_point(node_id, drop_context.mouse_pos, tabs_rect) + } + } + + self.find_drop_target(drop_context, *active); + } + NodeLayout::Horizontal(Horizontal { children, .. }) => { + for &child in children { + self.find_drop_target(drop_context, child); + } + } + } + } + + // fn drop_node( + // behavior: &mut dyn Behavior, + // dropped_node_id: NodeId, + // target_node_id: NodeId, + // mouse_pos: Pos2, + // ) { + // // TODO + // } + + fn remove_node_id_from_parent(&mut self, it: NodeId, remove: NodeId) { + let Some(mut node) = self.nodes.remove(&it) else { return; }; + match &mut node.layout { + NodeLayout::Leaf(_) => {} + NodeLayout::Tabs(Tabs { children, .. }) + | NodeLayout::Horizontal(Horizontal { children, .. }) => { + children.retain(|&child| { + self.remove_node_id_from_parent(child, remove); + child != remove + }); + } + } + self.nodes.insert(it, node); + } +} diff --git a/crates/emath/src/rect.rs b/crates/emath/src/rect.rs index 6300bb114..c93e681df 100644 --- a/crates/emath/src/rect.rs +++ b/crates/emath/src/rect.rs @@ -521,6 +521,30 @@ impl Rect { pub fn right_bottom(&self) -> Pos2 { pos2(self.right(), self.bottom()) } + + /// Split rectangle in left and right halfs. `t` is expected to be in the (0,1) range. + pub fn split_left_right_at_fraction(&self, t: f32) -> (Rect, Rect) { + self.split_left_right_at_x(lerp(self.min.x..=self.max.x, t)) + } + + /// Split rectangle in left and right halfs at the given `x` coordinate. + pub fn split_left_right_at_x(&self, split_x: f32) -> (Rect, Rect) { + let left = Rect::from_min_max(self.min, Pos2::new(split_x, self.max.y)); + let right = Rect::from_min_max(Pos2::new(split_x, self.min.y), self.max); + (left, right) + } + + /// Split rectangle in top and bottom halfs. `t` is expected to be in the (0,1) range. + pub fn split_top_bottom_at_fraction(&self, t: f32) -> (Rect, Rect) { + self.split_top_bottom_at_y(lerp(self.min.y..=self.max.y, t)) + } + + /// Split rectangle in top and bottom halfs at the given `y` coordinate. + pub fn split_top_bottom_at_y(&self, split_y: f32) -> (Rect, Rect) { + let top = Rect::from_min_max(self.min, Pos2::new(self.max.x, split_y)); + let bottom = Rect::from_min_max(Pos2::new(self.min.x, split_y), self.max); + (top, bottom) + } } impl std::fmt::Debug for Rect { diff --git a/examples/dock/src/main.rs b/examples/dock/src/main.rs index cdc7ad6f9..057304672 100644 --- a/examples/dock/src/main.rs +++ b/examples/dock/src/main.rs @@ -8,7 +8,7 @@ use egui_extras::dock; fn main() -> Result<(), eframe::Error> { env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`). let options = eframe::NativeOptions { - initial_window_size: Some(egui::vec2(320.0, 240.0)), + initial_window_size: Some(egui::vec2(800.0, 600.0)), ..Default::default() }; eframe::run_native("Dock", options, Box::new(|_cc| Box::::default())) @@ -22,7 +22,7 @@ pub struct View { impl View { pub fn with_nr(i: usize) -> Self { Self { - title: format!("Node {}", i), + title: format!("View {i}"), color: egui::epaint::Hsva::new(0.1 * i as f32, 0.5, 0.5, 1.0).into(), } }