From e5c9dd54b3f51e913f39b38e8907c321c8bfd040 Mon Sep 17 00:00:00 2001
From: Joao Freitas <51237625+jhff@users.noreply.github.com>
Date: Fri, 19 May 2023 11:24:52 +0100
Subject: Add ability to drag pane to the pane grid edges & optional style for
 dragged pane

---
 widget/src/pane_grid.rs       | 281 +++++++++++++++++++++++++++++-------------
 widget/src/pane_grid/node.rs  |  10 ++
 widget/src/pane_grid/state.rs |  89 +++++++++++--
 3 files changed, 281 insertions(+), 99 deletions(-)

(limited to 'widget/src')

diff --git a/widget/src/pane_grid.rs b/widget/src/pane_grid.rs
index 040d6bb3..0a8500dc 100644
--- a/widget/src/pane_grid.rs
+++ b/widget/src/pane_grid.rs
@@ -581,39 +581,49 @@ pub fn update<'a, Message, T: Draggable>(
         | Event::Touch(touch::Event::FingerLost { .. }) => {
             if let Some((pane, _)) = action.picked_pane() {
                 if let Some(on_drag) = on_drag {
-                    let dropped_region =
-                        cursor.position().and_then(|cursor_position| {
-                            contents
+                    if let Some(cursor_position) = cursor.position() {
+                        let event = if let Some(edge) =
+                            in_edge(layout, cursor_position)
+                        {
+                            DragEvent::Dropped {
+                                pane,
+                                target: Target::PaneGrid(edge),
+                            }
+                        } else {
+                            let dropped_region = contents
                                 .zip(layout.children())
                                 .filter_map(|(target, layout)| {
                                     layout_region(layout, cursor_position)
                                         .map(|region| (target, region))
                                 })
-                                .next()
-                        });
-
-                    let event = match dropped_region {
-                        Some(((target, _), region)) if pane != target => {
-                            DragEvent::Dropped {
-                                pane,
-                                target,
-                                region,
+                                .next();
+
+                            match dropped_region {
+                                Some(((target, _), region))
+                                    if pane != target =>
+                                {
+                                    DragEvent::Dropped {
+                                        pane,
+                                        target: Target::Pane {
+                                            pane: target,
+                                            region,
+                                        },
+                                    }
+                                }
+                                _ => DragEvent::Canceled { pane },
                             }
-                        }
-                        _ => DragEvent::Canceled { pane },
-                    };
+                        };
 
-                    shell.publish(on_drag(event));
+                        shell.publish(on_drag(event));
+                    }
                 }
 
-                *action = state::Action::Idle;
-
                 event_status = event::Status::Captured;
             } else if action.picked_split().is_some() {
-                *action = state::Action::Idle;
-
                 event_status = event::Status::Captured;
             }
+
+            *action = state::Action::Idle;
         }
         Event::Mouse(mouse::Event::CursorMoved { .. })
         | Event::Touch(touch::Event::FingerMoved { .. }) => {
@@ -671,13 +681,13 @@ fn layout_region(layout: Layout<'_>, cursor_position: Point) -> Option<Region> {
     }
 
     let region = if cursor_position.x < (bounds.x + bounds.width / 3.0) {
-        Region::Left
+        Region::Edge(Edge::Left)
     } else if cursor_position.x > (bounds.x + 2.0 * bounds.width / 3.0) {
-        Region::Right
+        Region::Edge(Edge::Right)
     } else if cursor_position.y < (bounds.y + bounds.height / 3.0) {
-        Region::Top
+        Region::Edge(Edge::Top)
     } else if cursor_position.y > (bounds.y + 2.0 * bounds.height / 3.0) {
-        Region::Bottom
+        Region::Edge(Edge::Bottom)
     } else {
         Region::Center
     };
@@ -833,28 +843,32 @@ pub fn draw<Renderer, T>(
 
     let mut render_picked_pane = None;
 
-    for ((id, pane), layout) in contents.zip(layout.children()) {
+    let cursor_in_edge = cursor
+        .position()
+        .and_then(|cursor_position| in_edge(layout, cursor_position));
+
+    for ((id, pane), pane_layout) in contents.zip(layout.children()) {
         match picked_pane {
             Some((dragging, origin)) if id == dragging => {
-                render_picked_pane = Some((pane, origin, layout));
+                render_picked_pane = Some((pane, origin, pane_layout));
             }
             Some((dragging, _)) if id != dragging => {
                 draw_pane(
                     pane,
                     renderer,
                     default_style,
-                    layout,
+                    pane_layout,
                     pane_cursor,
                     viewport,
                 );
 
-                if picked_pane.is_some() {
+                if picked_pane.is_some() && cursor_in_edge.is_none() {
                     if let Some(region) =
                         cursor.position().and_then(|cursor_position| {
-                            layout_region(layout, cursor_position)
+                            layout_region(pane_layout, cursor_position)
                         })
                     {
-                        let bounds = layout_region_bounds(layout, region);
+                        let bounds = layout_region_bounds(pane_layout, region);
                         let hovered_region_style = theme.hovered_region(style);
 
                         renderer.fill_quad(
@@ -875,7 +889,7 @@ pub fn draw<Renderer, T>(
                     pane,
                     renderer,
                     default_style,
-                    layout,
+                    pane_layout,
                     pane_cursor,
                     viewport,
                 );
@@ -883,6 +897,23 @@ pub fn draw<Renderer, T>(
         }
     }
 
+    if picked_pane.is_some() {
+        if let Some(edge) = cursor_in_edge {
+            let hovered_region_style = theme.hovered_region(style);
+            let bounds = edge_bounds(layout, edge);
+
+            renderer.fill_quad(
+                renderer::Quad {
+                    bounds,
+                    border_radius: hovered_region_style.border_radius.into(),
+                    border_width: hovered_region_style.border_width,
+                    border_color: hovered_region_style.border_color,
+                },
+                theme.hovered_region(style).background,
+            );
+        }
+    }
+
     // Render picked pane last
     if let Some((pane, origin, layout)) = render_picked_pane {
         if let Some(cursor_position) = cursor.position() {
@@ -907,71 +938,131 @@ pub fn draw<Renderer, T>(
         }
     }
 
-    if let Some((axis, split_region, is_picked)) = picked_split {
-        let highlight = if is_picked {
-            theme.picked_split(style)
-        } else {
-            theme.hovered_split(style)
-        };
-
-        if let Some(highlight) = highlight {
-            renderer.fill_quad(
-                renderer::Quad {
-                    bounds: match axis {
-                        Axis::Horizontal => Rectangle {
-                            x: split_region.x,
-                            y: (split_region.y
-                                + (split_region.height - highlight.width)
-                                    / 2.0)
-                                .round(),
-                            width: split_region.width,
-                            height: highlight.width,
-                        },
-                        Axis::Vertical => Rectangle {
-                            x: (split_region.x
-                                + (split_region.width - highlight.width) / 2.0)
-                                .round(),
-                            y: split_region.y,
-                            width: highlight.width,
-                            height: split_region.height,
+    if picked_pane.is_none() {
+        if let Some((axis, split_region, is_picked)) = picked_split {
+            let highlight = if is_picked {
+                theme.picked_split(style)
+            } else {
+                theme.hovered_split(style)
+            };
+
+            if let Some(highlight) = highlight {
+                renderer.fill_quad(
+                    renderer::Quad {
+                        bounds: match axis {
+                            Axis::Horizontal => Rectangle {
+                                x: split_region.x,
+                                y: (split_region.y
+                                    + (split_region.height - highlight.width)
+                                        / 2.0)
+                                    .round(),
+                                width: split_region.width,
+                                height: highlight.width,
+                            },
+                            Axis::Vertical => Rectangle {
+                                x: (split_region.x
+                                    + (split_region.width - highlight.width)
+                                        / 2.0)
+                                    .round(),
+                                y: split_region.y,
+                                width: highlight.width,
+                                height: split_region.height,
+                            },
                         },
+                        border_radius: 0.0.into(),
+                        border_width: 0.0,
+                        border_color: Color::TRANSPARENT,
                     },
-                    border_radius: 0.0.into(),
-                    border_width: 0.0,
-                    border_color: Color::TRANSPARENT,
-                },
-                highlight.color,
-            );
+                    highlight.color,
+                );
+            }
         }
     }
 }
 
-fn layout_region_bounds(layout: Layout<'_>, region: Region) -> Rectangle {
+const THICKNESS_RATIO: f32 = 25.0;
+
+fn in_edge(layout: Layout<'_>, cursor: Point) -> Option<Edge> {
     let bounds = layout.bounds();
 
-    match region {
-        Region::Center => bounds,
-        Region::Top => Rectangle {
-            height: bounds.height / 2.0,
+    let height_thickness = bounds.height / THICKNESS_RATIO;
+    let width_thickness = bounds.width / THICKNESS_RATIO;
+    let thickness = height_thickness.min(width_thickness);
+
+    if cursor.x > bounds.x && cursor.x < bounds.x + thickness {
+        Some(Edge::Left)
+    } else if cursor.x > bounds.x + bounds.width - thickness
+        && cursor.x < bounds.x + bounds.width
+    {
+        Some(Edge::Right)
+    } else if cursor.y > bounds.y && cursor.y < bounds.y + thickness {
+        Some(Edge::Top)
+    } else if cursor.y > bounds.y + bounds.height - thickness
+        && cursor.y < bounds.y + bounds.height
+    {
+        Some(Edge::Bottom)
+    } else {
+        None
+    }
+}
+
+fn edge_bounds(layout: Layout<'_>, edge: Edge) -> Rectangle {
+    let bounds = layout.bounds();
+
+    let height_thickness = bounds.height / THICKNESS_RATIO;
+    let width_thickness = bounds.width / THICKNESS_RATIO;
+    let thickness = height_thickness.min(width_thickness);
+
+    match edge {
+        Edge::Top => Rectangle {
+            height: thickness,
             ..bounds
         },
-        Region::Left => Rectangle {
-            width: bounds.width / 2.0,
+        Edge::Left => Rectangle {
+            width: thickness,
             ..bounds
         },
-        Region::Right => Rectangle {
-            x: bounds.x + bounds.width / 2.0,
-            width: bounds.width / 2.0,
+        Edge::Right => Rectangle {
+            x: bounds.x + bounds.width - thickness,
+            width: thickness,
             ..bounds
         },
-        Region::Bottom => Rectangle {
-            y: bounds.y + bounds.height / 2.0,
-            height: bounds.height / 2.0,
+        Edge::Bottom => Rectangle {
+            y: bounds.y + bounds.height - thickness,
+            height: thickness,
             ..bounds
         },
     }
 }
 
+fn layout_region_bounds(layout: Layout<'_>, region: Region) -> Rectangle {
+    let bounds = layout.bounds();
+
+    match region {
+        Region::Center => bounds,
+        Region::Edge(edge) => match edge {
+            Edge::Top => Rectangle {
+                height: bounds.height / 2.0,
+                ..bounds
+            },
+            Edge::Left => Rectangle {
+                width: bounds.width / 2.0,
+                ..bounds
+            },
+            Edge::Right => Rectangle {
+                x: bounds.x + bounds.width / 2.0,
+                width: bounds.width / 2.0,
+                ..bounds
+            },
+            Edge::Bottom => Rectangle {
+                y: bounds.y + bounds.height / 2.0,
+                height: bounds.height / 2.0,
+                ..bounds
+            },
+        },
+    }
+}
+
 /// An event produced during a drag and drop interaction of a [`PaneGrid`].
 #[derive(Debug, Clone, Copy)]
 pub enum DragEvent {
@@ -986,11 +1077,8 @@ pub enum DragEvent {
         /// The picked [`Pane`].
         pane: Pane,
 
-        /// The [`Pane`] where the picked one was dropped on.
-        target: Pane,
-
-        /// The [`Region`] of the target [`Pane`] where the picked one was dropped on.
-        region: Region,
+        /// The [`Target`] where the picked [`Pane`] was dropped on.
+        target: Target,
     },
 
     /// A [`Pane`] was picked and then dropped outside of other [`Pane`]
@@ -1001,19 +1089,40 @@ pub enum DragEvent {
     },
 }
 
+/// The [`Target`] area a pane can be dropped on.
+#[derive(Debug, Clone, Copy)]
+pub enum Target {
+    /// The [`Edge`} of the full [`PaneGrid`].
+    PaneGrid(Edge),
+    /// A single [`Pane`] of the [`PaneGrid`].
+    Pane {
+        /// The targetted [`Pane`].
+        pane: Pane,
+        /// The targetted area of the [`Pane`].
+        region: Region,
+    },
+}
+
 /// The region of a [`Pane`].
 #[derive(Debug, Clone, Copy, Default)]
 pub enum Region {
     /// Center region.
     #[default]
     Center,
-    /// Top region.
+    /// Edge region.
+    Edge(Edge),
+}
+
+/// The edges of an area.
+#[derive(Debug, Clone, Copy)]
+pub enum Edge {
+    /// Top edge.
     Top,
-    /// Left region.
+    /// Left edge.
     Left,
-    /// Right region.
+    /// Right edge.
     Right,
-    /// Bottom region.
+    /// Bottom edge.
     Bottom,
 }
 
diff --git a/widget/src/pane_grid/node.rs b/widget/src/pane_grid/node.rs
index 3976acd8..6de5920f 100644
--- a/widget/src/pane_grid/node.rs
+++ b/widget/src/pane_grid/node.rs
@@ -120,6 +120,16 @@ impl Node {
         };
     }
 
+    pub(crate) fn split_inverse(&mut self, id: Split, axis: Axis, pane: Pane) {
+        *self = Node::Split {
+            id,
+            axis,
+            ratio: 0.5,
+            a: Box::new(Node::Pane(pane)),
+            b: Box::new(self.clone()),
+        };
+    }
+
     pub(crate) fn update(&mut self, f: &impl Fn(&mut Node)) {
         if let Node::Split { a, b, .. } = self {
             a.update(f);
diff --git a/widget/src/pane_grid/state.rs b/widget/src/pane_grid/state.rs
index 1f034ca3..34781a90 100644
--- a/widget/src/pane_grid/state.rs
+++ b/widget/src/pane_grid/state.rs
@@ -3,7 +3,7 @@
 //! [`PaneGrid`]: crate::widget::PaneGrid
 use crate::core::{Point, Size};
 use crate::pane_grid::{
-    Axis, Configuration, Direction, Node, Pane, Region, Split,
+    Axis, Configuration, Direction, Edge, Node, Pane, Region, Split,
 };
 
 use std::collections::HashMap;
@@ -173,18 +173,20 @@ impl<T> State<T> {
     pub fn split_with(&mut self, target: &Pane, pane: &Pane, region: Region) {
         match region {
             Region::Center => self.swap(pane, target),
-            Region::Top => {
-                self.split_and_swap(Axis::Horizontal, target, pane, true)
-            }
-            Region::Bottom => {
-                self.split_and_swap(Axis::Horizontal, target, pane, false)
-            }
-            Region::Left => {
-                self.split_and_swap(Axis::Vertical, target, pane, true)
-            }
-            Region::Right => {
-                self.split_and_swap(Axis::Vertical, target, pane, false)
-            }
+            Region::Edge(edge) => match edge {
+                Edge::Top => {
+                    self.split_and_swap(Axis::Horizontal, target, pane, true)
+                }
+                Edge::Bottom => {
+                    self.split_and_swap(Axis::Horizontal, target, pane, false)
+                }
+                Edge::Left => {
+                    self.split_and_swap(Axis::Vertical, target, pane, true)
+                }
+                Edge::Right => {
+                    self.split_and_swap(Axis::Vertical, target, pane, false)
+                }
+            },
         }
     }
 
@@ -204,6 +206,67 @@ impl<T> State<T> {
         }
     }
 
+    /// Move [`Pane`] to an [`Edge`] of the [`PaneGrid`].
+    pub fn move_to_edge(&mut self, pane: &Pane, edge: Edge) {
+        match edge {
+            Edge::Top => {
+                self.split_major_node_and_swap(Axis::Horizontal, pane, true)
+            }
+            Edge::Bottom => {
+                self.split_major_node_and_swap(Axis::Horizontal, pane, false)
+            }
+            Edge::Left => {
+                self.split_major_node_and_swap(Axis::Vertical, pane, true)
+            }
+            Edge::Right => {
+                self.split_major_node_and_swap(Axis::Vertical, pane, false)
+            }
+        }
+    }
+
+    fn split_major_node_and_swap(
+        &mut self,
+        axis: Axis,
+        pane: &Pane,
+        swap: bool,
+    ) {
+        if let Some((state, _)) = self.close(pane) {
+            let _ = self.split_major_node(axis, state, swap);
+        }
+    }
+
+    fn split_major_node(
+        &mut self,
+        axis: Axis,
+        state: T,
+        swap: bool,
+    ) -> Option<(Pane, Split)> {
+        let major_node = &mut self.internal.layout;
+
+        let new_pane = {
+            self.internal.last_id = self.internal.last_id.checked_add(1)?;
+
+            Pane(self.internal.last_id)
+        };
+
+        let new_split = {
+            self.internal.last_id = self.internal.last_id.checked_add(1)?;
+
+            Split(self.internal.last_id)
+        };
+
+        if swap {
+            major_node.split_inverse(new_split, axis, new_pane)
+        } else {
+            major_node.split(new_split, axis, new_pane)
+        };
+
+        let _ = self.panes.insert(new_pane, state);
+        let _ = self.maximized.take();
+
+        Some((new_pane, new_split))
+    }
+
     /// Swaps the position of the provided panes in the [`State`].
     ///
     /// If you want to swap panes on drag and drop in your [`PaneGrid`], you
-- 
cgit