diff --git a/Cargo.lock b/Cargo.lock index 75656cb5d..216ee4d44 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1331,6 +1331,7 @@ dependencies = [ "egui", "getrandom", "image", + "itertools", "log", "rand", "resvg", diff --git a/crates/egui_extras/Cargo.toml b/crates/egui_extras/Cargo.toml index 152c722be..82c3e3d99 100644 --- a/crates/egui_extras/Cargo.toml +++ b/crates/egui_extras/Cargo.toml @@ -39,6 +39,7 @@ egui = { version = "0.21.0", path = "../egui", default-features = false } # For dock: # required to make rand work on wasm, see https://github.com/rust-random/rand#wasm-support getrandom = { version = "0.2", features = ["js"] } +itertools = "0.10" log = { version = "0.4", features = ["std"] } rand = { version = "0.8.5", features = ["getrandom", "small_rng"] } diff --git a/crates/egui_extras/src/dock/branch/linear.rs b/crates/egui_extras/src/dock/branch/linear.rs index 65881b79b..3d990e126 100644 --- a/crates/egui_extras/src/dock/branch/linear.rs +++ b/crates/egui_extras/src/dock/branch/linear.rs @@ -1,7 +1,9 @@ use egui::{pos2, vec2, NumExt, Rect}; +use itertools::Itertools as _; use crate::dock::{ is_being_dragged, Behavior, DropContext, InsertionPoint, LayoutInsertion, NodeId, Nodes, + ResizeState, }; use super::Shares; @@ -161,6 +163,74 @@ impl Linear { prev_rect = Some(rect); } + + // ------------------------ + // resizing: + let parent_rect = nodes.rect(parent_id).unwrap(); + for (i, (left, right)) in self.children.iter().tuple_windows().enumerate() { + let resize_id = egui::Id::new((parent_id, "resize", i)); + + let left_rect = nodes.rect(*left).unwrap(); + let right_rect = nodes.rect(*right).unwrap(); + let x = egui::lerp(left_rect.right()..=right_rect.left(), 0.5); + + let mut resize_state = ResizeState::Idle; + if let Some(pointer) = ui.ctx().pointer_latest_pos() { + let we_are_on_top = ui + .ctx() + .layer_id_at(pointer) + .map_or(true, |top_layer_id| top_layer_id == ui.layer_id()); + + let mouse_over_resize_line = we_are_on_top + && parent_rect.y_range().contains(&pointer.y) + && (x - pointer.x).abs() <= ui.style().interaction.resize_grab_radius_side; + + if ui.input(|i| i.pointer.any_pressed() && i.pointer.any_down()) + && mouse_over_resize_line + { + ui.memory_mut(|mem| mem.set_dragged_id(resize_id)); + } + if ui.memory(|mem| mem.is_being_dragged(resize_id)) { + resize_state = ResizeState::Dragging; + } else { + let dragging_something_else = + ui.input(|i| i.pointer.any_down() || i.pointer.any_pressed()); + if mouse_over_resize_line && !dragging_something_else { + resize_state = ResizeState::Hovering; + } + } + + if resize_state == ResizeState::Dragging { + let dx = pointer.x - x; + if pointer.x < x { + // Expand right, shrink stuff to the left: + *self.shares.shares.entry(*right).or_insert(1.0) += shrink_shares( + behavior, + nodes, + &mut self.shares, + &self.children[0..=i].iter().copied().rev().collect_vec(), + dx.abs(), + ); + } else if x < pointer.x { + // Expand the left, shrink stuff to the right: + *self.shares.shares.entry(*left).or_insert(1.0) += shrink_shares( + behavior, + nodes, + &mut self.shares, + &self.children[i + 1..], + dx.abs(), + ); + } + } + + if resize_state != ResizeState::Idle { + ui.ctx().set_cursor_icon(egui::CursorIcon::ResizeHorizontal); + } + } + + let stroke = behavior.resize_stroke(ui.style(), resize_state); + ui.painter().vline(x, parent_rect.y_range(), stroke); + } } fn vertical_ui( @@ -175,5 +245,45 @@ impl Linear { for child in &self.children { nodes.node_ui(behavior, drop_context, ui, *child); } + + // TODO: resizing } } + +/// Try shrink the children by a total of `target_in_points`, +/// making sure no child gets smaller than its minimum size. +fn shrink_shares( + behavior: &dyn Behavior, + nodes: &Nodes, + shares: &mut Shares, + children: &[NodeId], + target_in_points: f32, +) -> f32 { + assert!(!children.is_empty()); + + let mut total_shares = 0.0; + let mut total_points = 0.0; + for &child in children { + total_shares += shares.shares.get(&child).copied().unwrap_or(1.0); + total_points += nodes.rect(child).unwrap().width(); + } + + let shares_per_point = total_shares / total_points; + + let min_size_in_points = shares_per_point * behavior.min_size(); + + let target_in_shares = shares_per_point * target_in_points; + let mut total_shares_lost = 0.0; + + for &child in children { + let share = shares.shares.entry(child).or_insert(1.0); + let shrink_by = (target_in_shares - total_shares_lost) + .min(*share - min_size_in_points) + .max(0.0); + + *share -= shrink_by; + total_shares_lost += shrink_by; + } + + total_shares_lost +} diff --git a/crates/egui_extras/src/dock/mod.rs b/crates/egui_extras/src/dock/mod.rs index 319309085..6c260ae22 100644 --- a/crates/egui_extras/src/dock/mod.rs +++ b/crates/egui_extras/src/dock/mod.rs @@ -1,3 +1,10 @@ +// # TODO +// * A new ui for each node, nested +// * Better drag-and-drop around the "empty" drag source +// * Resizing of vertical layouts and grids +// * Styling +// * Handle rects without a lot of unwraps + use std::collections::{HashMap, HashSet}; use egui::{Id, Key, NumExt, Pos2, Rect, Response, Sense, TextStyle, Ui, WidgetText}; @@ -207,9 +214,29 @@ pub trait Behavior { 1.0 } + // No child should shrink below this size + fn min_size(&self) -> f32 { + 32.0 + } + fn simplification_options(&self) -> SimplificationOptions { SimplificationOptions::default() } + + 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 + ResizeState::Hovering => style.visuals.widgets.hovered.fg_stroke, + ResizeState::Dragging => style.visuals.widgets.active.fg_stroke, + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum ResizeState { + Idle, + Hovering, + Dragging, } // ----------------------------------------------------------------------------