diff options
-rw-r--r-- | core/src/point.rs | 10 | ||||
-rw-r--r-- | native/src/mouse_cursor.rs | 6 | ||||
-rw-r--r-- | native/src/widget/pane_grid.rs | 116 | ||||
-rw-r--r-- | native/src/widget/pane_grid/node.rs | 67 | ||||
-rw-r--r-- | native/src/widget/pane_grid/state.rs | 65 | ||||
-rw-r--r-- | wgpu/src/renderer/widget/pane_grid.rs | 8 | ||||
-rw-r--r-- | winit/src/conversion.rs | 4 |
7 files changed, 265 insertions, 11 deletions
diff --git a/core/src/point.rs b/core/src/point.rs index b55f5099..b855cd91 100644 --- a/core/src/point.rs +++ b/core/src/point.rs @@ -22,6 +22,16 @@ impl Point { pub const fn new(x: f32, y: f32) -> Self { Self { x, y } } + + /// Computes the distance to another [`Point`]. + /// + /// [`Point`]: struct.Point.html + pub fn distance(&self, to: Point) -> f32 { + let a = self.x - to.x; + let b = self.y - to.y; + + f32::sqrt(a * a + b * b) + } } impl From<[f32; 2]> for Point { diff --git a/native/src/mouse_cursor.rs b/native/src/mouse_cursor.rs index c7297e0e..0dad3edc 100644 --- a/native/src/mouse_cursor.rs +++ b/native/src/mouse_cursor.rs @@ -21,6 +21,12 @@ pub enum MouseCursor { /// The cursor is over a text widget. Text, + + /// The cursor is resizing a widget horizontally. + ResizingHorizontally, + + /// The cursor is resizing a widget vertically. + ResizingVertically, } impl Default for MouseCursor { diff --git a/native/src/widget/pane_grid.rs b/native/src/widget/pane_grid.rs index 68f32bc0..5229962d 100644 --- a/native/src/widget/pane_grid.rs +++ b/native/src/widget/pane_grid.rs @@ -14,7 +14,7 @@ pub use state::{Focus, State}; use crate::{ input::{keyboard, mouse, ButtonState}, layout, Clipboard, Element, Event, Hasher, Layout, Length, Point, Size, - Widget, + Vector, Widget, }; #[allow(missing_debug_implementations)] @@ -26,6 +26,7 @@ pub struct PaneGrid<'a, Message, Renderer> { height: Length, spacing: u16, on_drag: Option<Box<dyn Fn(DragEvent) -> Message>>, + on_resize: Option<Box<dyn Fn(ResizeEvent) -> Message>>, } impl<'a, Message, Renderer> PaneGrid<'a, Message, Renderer> { @@ -67,6 +68,7 @@ impl<'a, Message, Renderer> PaneGrid<'a, Message, Renderer> { height: Length::Fill, spacing: 0, on_drag: None, + on_resize: None, } } @@ -101,6 +103,14 @@ impl<'a, Message, Renderer> PaneGrid<'a, Message, Renderer> { self.on_drag = Some(Box::new(f)); self } + + pub fn on_resize( + mut self, + f: impl Fn(ResizeEvent) -> Message + 'static, + ) -> Self { + self.on_resize = Some(Box::new(f)); + self + } } #[derive(Debug, Clone, Copy)] @@ -110,6 +120,12 @@ pub enum DragEvent { Canceled { pane: Pane }, } +#[derive(Debug, Clone, Copy)] +pub struct ResizeEvent { + pub split: Split, + pub ratio: f32, +} + impl<'a, Message, Renderer> Widget<Message, Renderer> for PaneGrid<'a, Message, Renderer> where @@ -178,7 +194,7 @@ where if let Some(((pane, _), _)) = clicked_region.next() { match &self.on_drag { Some(on_drag) if self.modifiers.alt => { - self.state.drag(pane); + self.state.pick_pane(pane); messages.push(on_drag(DragEvent::Picked { pane: *pane, @@ -193,7 +209,7 @@ where } } ButtonState::Released => { - if let Some(pane) = self.state.dragged() { + if let Some(pane) = self.state.picked_pane() { self.state.focus(&pane); if let Some(on_drag) = &self.on_drag { @@ -220,13 +236,101 @@ where } } }, + Event::Mouse(mouse::Event::Input { + button: mouse::Button::Right, + state, + }) if self.on_resize.is_some() + && self.state.picked_pane().is_none() + && self.modifiers.alt => + { + match state { + ButtonState::Pressed => { + let bounds = layout.bounds(); + + let splits = self.state.splits( + f32::from(self.spacing), + Size::new(bounds.width, bounds.height), + ); + + let mut sorted_splits: Vec<_> = splits.iter().collect(); + let offset = Vector::new(bounds.x, bounds.y); + + sorted_splits.sort_by_key( + |(_, (axis, rectangle, ratio))| { + let center = match axis { + Axis::Horizontal => Point::new( + rectangle.x + rectangle.width / 2.0, + rectangle.y + rectangle.height * ratio, + ), + + Axis::Vertical => Point::new( + rectangle.x + rectangle.width * ratio, + rectangle.y + rectangle.height / 2.0, + ), + }; + + cursor_position + .distance(center + offset) + .round() + as u32 + }, + ); + + if let Some((split, (axis, _, _))) = + sorted_splits.first() + { + self.state.pick_split(split, *axis); + } + } + ButtonState::Released => { + self.state.drop_split(); + } + } + } + Event::Mouse(mouse::Event::CursorMoved { .. }) => { + if let Some(on_resize) = &self.on_resize { + if let Some((split, _)) = self.state.picked_split() { + let bounds = layout.bounds(); + + let splits = self.state.splits( + f32::from(self.spacing), + Size::new(bounds.width, bounds.height), + ); + + if let Some((axis, rectangle, _)) = splits.get(&split) { + let ratio = match axis { + Axis::Horizontal => { + let position = cursor_position.x - bounds.x + + rectangle.x; + + (position / (rectangle.x + rectangle.width)) + .max(0.1) + .min(0.9) + } + Axis::Vertical => { + let position = cursor_position.y - bounds.y + + rectangle.y; + + (position + / (rectangle.y + rectangle.height)) + .max(0.1) + .min(0.9) + } + }; + + messages + .push(on_resize(ResizeEvent { split, ratio })); + } + } + } + } Event::Keyboard(keyboard::Event::Input { modifiers, .. }) => { *self.modifiers = modifiers; } _ => {} } - if self.state.dragged().is_none() { + if self.state.picked_pane().is_none() { { self.elements.iter_mut().zip(layout.children()).for_each( |((_, pane), layout)| { @@ -254,7 +358,8 @@ where renderer.draw( defaults, &self.elements, - self.state.dragged(), + self.state.picked_pane(), + self.state.picked_split().map(|(_, axis)| axis), layout, cursor_position, ) @@ -297,6 +402,7 @@ pub trait Renderer: crate::Renderer + Sized { defaults: &Self::Defaults, content: &[(Pane, Element<'_, Message, Self>)], dragging: Option<Pane>, + resizing: Option<Axis>, layout: Layout<'_>, cursor_position: Point, ) -> Self::Output; diff --git a/native/src/widget/pane_grid/node.rs b/native/src/widget/pane_grid/node.rs index 08046956..4d5970b8 100644 --- a/native/src/widget/pane_grid/node.rs +++ b/native/src/widget/pane_grid/node.rs @@ -55,6 +55,25 @@ impl Node { f(self); } + pub fn resize(&mut self, split: &Split, percentage: f32) -> bool { + match self { + Node::Split { + id, ratio, a, b, .. + } => { + if id == split { + *ratio = (percentage * 1_000_000.0).round() as u32; + + true + } else if a.resize(split, percentage) { + true + } else { + b.resize(split, percentage) + } + } + Node::Pane(_) => false, + } + } + pub fn remove(&mut self, pane: &Pane) -> Option<Pane> { match self { Node::Split { a, b, .. } => { @@ -93,6 +112,27 @@ impl Node { regions } + pub fn splits( + &self, + spacing: f32, + size: Size, + ) -> HashMap<Split, (Axis, Rectangle, f32)> { + let mut splits = HashMap::new(); + + self.compute_splits( + spacing / 2.0, + &Rectangle { + x: 0.0, + y: 0.0, + width: size.width, + height: size.height, + }, + &mut splits, + ); + + splits + } + pub fn pane(&self) -> Option<Pane> { match self { Node::Split { .. } => None, @@ -129,4 +169,31 @@ impl Node { } } } + + fn compute_splits( + &self, + halved_spacing: f32, + current: &Rectangle, + splits: &mut HashMap<Split, (Axis, Rectangle, f32)>, + ) { + match self { + Node::Split { + axis, + ratio, + a, + b, + id, + } => { + let ratio = *ratio as f32 / 1_000_000.0; + let (region_a, region_b) = + axis.split(current, ratio, halved_spacing); + + let _ = splits.insert(*id, (*axis, *current, ratio)); + + a.compute_splits(halved_spacing, ®ion_a, splits); + b.compute_splits(halved_spacing, ®ion_b, splits); + } + Node::Pane(_) => {} + } + } } diff --git a/native/src/widget/pane_grid/state.rs b/native/src/widget/pane_grid/state.rs index b0e571f0..456ad78a 100644 --- a/native/src/widget/pane_grid/state.rs +++ b/native/src/widget/pane_grid/state.rs @@ -134,6 +134,10 @@ impl<T> State<T> { }); } + pub fn resize(&mut self, split: &Split, percentage: f32) { + let _ = self.internal.layout.resize(split, percentage); + } + pub fn close(&mut self, pane: &Pane) -> Option<T> { if let Some(sibling) = self.internal.layout.remove(pane) { self.focus(&sibling); @@ -153,14 +157,25 @@ pub struct Internal { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Action { - Idle { focus: Option<Pane> }, - Dragging { pane: Pane }, + Idle { + focus: Option<Pane>, + }, + Dragging { + pane: Pane, + }, + Resizing { + split: Split, + axis: Axis, + focus: Option<Pane>, + }, } impl Action { pub fn focus(&self) -> Option<(Pane, Focus)> { match self { - Action::Idle { focus } => focus.map(|pane| (pane, Focus::Idle)), + Action::Idle { focus } | Action::Resizing { focus, .. } => { + focus.map(|pane| (pane, Focus::Idle)) + } Action::Dragging { pane } => Some((*pane, Focus::Dragging)), } } @@ -171,13 +186,20 @@ impl Internal { self.action } - pub fn dragged(&self) -> Option<Pane> { + pub fn picked_pane(&self) -> Option<Pane> { match self.action { Action::Dragging { pane } => Some(pane), _ => None, } } + pub fn picked_split(&self) -> Option<(Split, Axis)> { + match self.action { + Action::Resizing { split, axis, .. } => Some((split, axis)), + _ => None, + } + } + pub fn regions( &self, spacing: f32, @@ -186,14 +208,47 @@ impl Internal { self.layout.regions(spacing, size) } + pub fn splits( + &self, + spacing: f32, + size: Size, + ) -> HashMap<Split, (Axis, Rectangle, f32)> { + self.layout.splits(spacing, size) + } + pub fn focus(&mut self, pane: &Pane) { self.action = Action::Idle { focus: Some(*pane) }; } - pub fn drag(&mut self, pane: &Pane) { + pub fn pick_pane(&mut self, pane: &Pane) { self.action = Action::Dragging { pane: *pane }; } + pub fn pick_split(&mut self, split: &Split, axis: Axis) { + // TODO: Obtain `axis` from layout itself. Maybe we should implement + // `Node::find_split` + if self.picked_pane().is_some() { + return; + } + + let focus = self.action.focus().map(|(pane, _)| pane); + + self.action = Action::Resizing { + split: *split, + axis, + focus, + }; + } + + pub fn drop_split(&mut self) { + match self.action { + Action::Resizing { focus, .. } => { + self.action = Action::Idle { focus }; + } + _ => {} + } + } + pub fn unfocus(&mut self) { self.action = Action::Idle { focus: None }; } diff --git a/wgpu/src/renderer/widget/pane_grid.rs b/wgpu/src/renderer/widget/pane_grid.rs index 8fb4a1a9..a00b49ea 100644 --- a/wgpu/src/renderer/widget/pane_grid.rs +++ b/wgpu/src/renderer/widget/pane_grid.rs @@ -1,6 +1,6 @@ use crate::{Primitive, Renderer}; use iced_native::{ - pane_grid::{self, Pane}, + pane_grid::{self, Axis, Pane}, Element, Layout, MouseCursor, Point, Rectangle, Vector, }; @@ -10,6 +10,7 @@ impl pane_grid::Renderer for Renderer { defaults: &Self::Defaults, content: &[(Pane, Element<'_, Message, Self>)], dragging: Option<Pane>, + resizing: Option<Axis>, layout: Layout<'_>, cursor_position: Point, ) -> Self::Output { @@ -70,6 +71,11 @@ impl pane_grid::Renderer for Renderer { Primitive::Group { primitives }, if dragging.is_some() { MouseCursor::Grabbing + } else if let Some(axis) = resizing { + match axis { + Axis::Horizontal => MouseCursor::ResizingHorizontally, + Axis::Vertical => MouseCursor::ResizingVertically, + } } else { mouse_cursor }, diff --git a/winit/src/conversion.rs b/winit/src/conversion.rs index b6a0b64b..74852876 100644 --- a/winit/src/conversion.rs +++ b/winit/src/conversion.rs @@ -116,6 +116,10 @@ pub fn mouse_cursor(mouse_cursor: MouseCursor) -> winit::window::CursorIcon { MouseCursor::Grab => winit::window::CursorIcon::Grab, MouseCursor::Grabbing => winit::window::CursorIcon::Grabbing, MouseCursor::Text => winit::window::CursorIcon::Text, + MouseCursor::ResizingHorizontally => { + winit::window::CursorIcon::EwResize + } + MouseCursor::ResizingVertically => winit::window::CursorIcon::NsResize, } } |