diff options
Diffstat (limited to 'native/src')
| -rw-r--r-- | native/src/element.rs | 2 | ||||
| -rw-r--r-- | native/src/input/keyboard/modifiers_state.rs | 2 | ||||
| -rw-r--r-- | native/src/lib.rs | 2 | ||||
| -rw-r--r-- | native/src/mouse_cursor.rs | 6 | ||||
| -rw-r--r-- | native/src/widget.rs | 3 | ||||
| -rw-r--r-- | native/src/widget/pane_grid.rs | 432 | ||||
| -rw-r--r-- | native/src/widget/pane_grid/axis.rs | 54 | ||||
| -rw-r--r-- | native/src/widget/pane_grid/direction.rs | 7 | ||||
| -rw-r--r-- | native/src/widget/pane_grid/node.rs | 199 | ||||
| -rw-r--r-- | native/src/widget/pane_grid/pane.rs | 2 | ||||
| -rw-r--r-- | native/src/widget/pane_grid/split.rs | 2 | ||||
| -rw-r--r-- | native/src/widget/pane_grid/state.rs | 261 | 
12 files changed, 969 insertions, 3 deletions
diff --git a/native/src/element.rs b/native/src/element.rs index 276f7614..4e7c7fc6 100644 --- a/native/src/element.rs +++ b/native/src/element.rs @@ -243,7 +243,7 @@ where      }      /// Computes the _layout_ hash of the [`Element`]. -    ///  +    ///      /// [`Element`]: struct.Element.html      pub fn hash_layout(&self, state: &mut Hasher) {          self.widget.hash_layout(state); diff --git a/native/src/input/keyboard/modifiers_state.rs b/native/src/input/keyboard/modifiers_state.rs index 4e3794b3..3058c065 100644 --- a/native/src/input/keyboard/modifiers_state.rs +++ b/native/src/input/keyboard/modifiers_state.rs @@ -1,5 +1,5 @@  /// The current state of the keyboard modifiers. -#[derive(Debug, Clone, Copy, PartialEq)] +#[derive(Debug, Clone, Copy, PartialEq, Default)]  pub struct ModifiersState {      /// Whether a shift key is pressed      pub shift: bool, diff --git a/native/src/lib.rs b/native/src/lib.rs index e4e7baee..4551a982 100644 --- a/native/src/lib.rs +++ b/native/src/lib.rs @@ -34,7 +34,7 @@  //! [`window::Renderer`]: window/trait.Renderer.html  //! [`UserInterface`]: struct.UserInterface.html  //! [renderer]: renderer/index.html -#![deny(missing_docs)] +//#![deny(missing_docs)]  #![deny(missing_debug_implementations)]  #![deny(unused_results)]  #![forbid(unsafe_code)] 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.rs b/native/src/widget.rs index f9424b02..88f819c9 100644 --- a/native/src/widget.rs +++ b/native/src/widget.rs @@ -25,6 +25,7 @@ pub mod checkbox;  pub mod column;  pub mod container;  pub mod image; +pub mod pane_grid;  pub mod progress_bar;  pub mod radio;  pub mod row; @@ -46,6 +47,8 @@ pub use container::Container;  #[doc(no_inline)]  pub use image::Image;  #[doc(no_inline)] +pub use pane_grid::PaneGrid; +#[doc(no_inline)]  pub use progress_bar::ProgressBar;  #[doc(no_inline)]  pub use radio::Radio; diff --git a/native/src/widget/pane_grid.rs b/native/src/widget/pane_grid.rs new file mode 100644 index 00000000..7135efe4 --- /dev/null +++ b/native/src/widget/pane_grid.rs @@ -0,0 +1,432 @@ +mod axis; +mod direction; +mod node; +mod pane; +mod split; +mod state; + +pub use axis::Axis; +pub use direction::Direction; +pub use pane::Pane; +pub use split::Split; +pub use state::{Focus, State}; + +use crate::{ +    input::{keyboard, mouse, ButtonState}, +    layout, Clipboard, Element, Event, Hasher, Layout, Length, Point, Size, +    Widget, +}; + +#[allow(missing_debug_implementations)] +pub struct PaneGrid<'a, Message, Renderer> { +    state: &'a mut state::Internal, +    modifiers: &'a mut keyboard::ModifiersState, +    elements: Vec<(Pane, Element<'a, Message, Renderer>)>, +    width: Length, +    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> { +    pub fn new<T>( +        state: &'a mut State<T>, +        view: impl Fn( +            Pane, +            &'a mut T, +            Option<Focus>, +        ) -> Element<'a, Message, Renderer>, +    ) -> Self { +        let elements = { +            let action = state.internal.action(); +            let current_focus = action.focus(); + +            state +                .panes +                .iter_mut() +                .map(move |(pane, pane_state)| { +                    let focus = match current_focus { +                        Some((focused_pane, focus)) +                            if *pane == focused_pane => +                        { +                            Some(focus) +                        } +                        _ => None, +                    }; + +                    (*pane, view(*pane, pane_state, focus)) +                }) +                .collect() +        }; + +        Self { +            state: &mut state.internal, +            modifiers: &mut state.modifiers, +            elements, +            width: Length::Fill, +            height: Length::Fill, +            spacing: 0, +            on_drag: None, +            on_resize: None, +        } +    } + +    /// Sets the width of the [`PaneGrid`]. +    /// +    /// [`PaneGrid`]: struct.Column.html +    pub fn width(mut self, width: Length) -> Self { +        self.width = width; +        self +    } + +    /// Sets the height of the [`PaneGrid`]. +    /// +    /// [`PaneGrid`]: struct.Column.html +    pub fn height(mut self, height: Length) -> Self { +        self.height = height; +        self +    } + +    /// Sets the spacing _between_ the panes of the [`PaneGrid`]. +    /// +    /// [`PaneGrid`]: struct.Column.html +    pub fn spacing(mut self, units: u16) -> Self { +        self.spacing = units; +        self +    } + +    pub fn on_drag( +        mut self, +        f: impl Fn(DragEvent) -> Message + 'static, +    ) -> Self { +        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 +    } + +    fn trigger_resize( +        &mut self, +        layout: Layout<'_>, +        cursor_position: Point, +        messages: &mut Vec<Message>, +    ) { +        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.y - bounds.y - rectangle.y; + +                            (position / rectangle.height).max(0.1).min(0.9) +                        } +                        Axis::Vertical => { +                            let position = +                                cursor_position.x - bounds.x - rectangle.x; + +                            (position / rectangle.width).max(0.1).min(0.9) +                        } +                    }; + +                    messages.push(on_resize(ResizeEvent { split, ratio })); +                } +            } +        } +    } +} + +#[derive(Debug, Clone, Copy)] +pub enum DragEvent { +    Picked { pane: Pane }, +    Dropped { pane: Pane, target: Pane }, +    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 +    Renderer: self::Renderer + 'static, +    Message: 'static, +{ +    fn width(&self) -> Length { +        self.width +    } + +    fn height(&self) -> Length { +        self.height +    } + +    fn layout( +        &self, +        renderer: &Renderer, +        limits: &layout::Limits, +    ) -> layout::Node { +        let limits = limits.width(self.width).height(self.height); +        let size = limits.resolve(Size::ZERO); + +        let regions = self.state.regions(f32::from(self.spacing), size); + +        let children = self +            .elements +            .iter() +            .filter_map(|(pane, element)| { +                let region = regions.get(pane)?; +                let size = Size::new(region.width, region.height); + +                let mut node = +                    element.layout(renderer, &layout::Limits::new(size, size)); + +                node.move_to(Point::new(region.x, region.y)); + +                Some(node) +            }) +            .collect(); + +        layout::Node::with_children(size, children) +    } + +    fn on_event( +        &mut self, +        event: Event, +        layout: Layout<'_>, +        cursor_position: Point, +        messages: &mut Vec<Message>, +        renderer: &Renderer, +        clipboard: Option<&dyn Clipboard>, +    ) { +        match event { +            Event::Mouse(mouse::Event::Input { +                button: mouse::Button::Left, +                state, +            }) => match state { +                ButtonState::Pressed => { +                    let mut clicked_region = +                        self.elements.iter().zip(layout.children()).filter( +                            |(_, layout)| { +                                layout.bounds().contains(cursor_position) +                            }, +                        ); + +                    if let Some(((pane, _), _)) = clicked_region.next() { +                        match &self.on_drag { +                            Some(on_drag) if self.modifiers.alt => { +                                self.state.pick_pane(pane); + +                                messages.push(on_drag(DragEvent::Picked { +                                    pane: *pane, +                                })); +                            } +                            _ => { +                                self.state.focus(pane); +                            } +                        } +                    } else { +                        self.state.unfocus(); +                    } +                } +                ButtonState::Released => { +                    if let Some(pane) = self.state.picked_pane() { +                        self.state.focus(&pane); + +                        if let Some(on_drag) = &self.on_drag { +                            let mut dropped_region = self +                                .elements +                                .iter() +                                .zip(layout.children()) +                                .filter(|(_, layout)| { +                                    layout.bounds().contains(cursor_position) +                                }); + +                            let event = match dropped_region.next() { +                                Some(((target, _), _)) if pane != *target => { +                                    DragEvent::Dropped { +                                        pane, +                                        target: *target, +                                    } +                                } +                                _ => DragEvent::Canceled { pane }, +                            }; + +                            messages.push(on_drag(event)); +                        } +                    } +                } +            }, +            Event::Mouse(mouse::Event::Input { +                button: mouse::Button::Right, +                state: ButtonState::Pressed, +            }) if self.on_resize.is_some() +                && self.state.picked_pane().is_none() +                && self.modifiers.alt => +            { +                let bounds = layout.bounds(); +                let relative_cursor = Point::new( +                    cursor_position.x - bounds.x, +                    cursor_position.y - bounds.y, +                ); + +                let splits = self.state.splits( +                    f32::from(self.spacing), +                    Size::new(bounds.width, bounds.height), +                ); + +                let mut sorted_splits: Vec<_> = splits +                    .iter() +                    .filter(|(_, (axis, rectangle, _))| match axis { +                        Axis::Horizontal => { +                            relative_cursor.x > rectangle.x +                                && relative_cursor.x +                                    < rectangle.x + rectangle.width +                        } +                        Axis::Vertical => { +                            relative_cursor.y > rectangle.y +                                && relative_cursor.y +                                    < rectangle.y + rectangle.height +                        } +                    }) +                    .collect(); + +                sorted_splits.sort_by_key(|(_, (axis, rectangle, ratio))| { +                    let distance = match axis { +                        Axis::Horizontal => (relative_cursor.y +                            - (rectangle.y + rectangle.height * ratio)) +                            .abs(), +                        Axis::Vertical => (relative_cursor.x +                            - (rectangle.x + rectangle.width * ratio)) +                            .abs(), +                    }; + +                    distance.round() as u32 +                }); + +                if let Some((split, (axis, _, _))) = sorted_splits.first() { +                    self.state.pick_split(split, *axis); +                    self.trigger_resize(layout, cursor_position, messages); +                } +            } +            Event::Mouse(mouse::Event::Input { +                button: mouse::Button::Right, +                state: ButtonState::Released, +            }) if self.state.picked_split().is_some() => { +                self.state.drop_split(); +            } +            Event::Mouse(mouse::Event::CursorMoved { .. }) => { +                self.trigger_resize(layout, cursor_position, messages); +            } +            Event::Keyboard(keyboard::Event::Input { modifiers, .. }) => { +                *self.modifiers = modifiers; +            } +            _ => {} +        } + +        if self.state.picked_pane().is_none() { +            { +                self.elements.iter_mut().zip(layout.children()).for_each( +                    |((_, pane), layout)| { +                        pane.widget.on_event( +                            event.clone(), +                            layout, +                            cursor_position, +                            messages, +                            renderer, +                            clipboard, +                        ) +                    }, +                ); +            } +        } +    } + +    fn draw( +        &self, +        renderer: &mut Renderer, +        defaults: &Renderer::Defaults, +        layout: Layout<'_>, +        cursor_position: Point, +    ) -> Renderer::Output { +        renderer.draw( +            defaults, +            &self.elements, +            self.state.picked_pane(), +            self.state.picked_split().map(|(_, axis)| axis), +            layout, +            cursor_position, +        ) +    } + +    fn hash_layout(&self, state: &mut Hasher) { +        use std::hash::Hash; + +        std::any::TypeId::of::<PaneGrid<'_, Message, Renderer>>().hash(state); +        self.width.hash(state); +        self.height.hash(state); +        self.state.hash_layout(state); + +        for (_, element) in &self.elements { +            element.hash_layout(state); +        } +    } +} + +/// The renderer of a [`PaneGrid`]. +/// +/// Your [renderer] will need to implement this trait before being +/// able to use a [`PaneGrid`] in your user interface. +/// +/// [`PaneGrid`]: struct.PaneGrid.html +/// [renderer]: ../../renderer/index.html +pub trait Renderer: crate::Renderer + Sized { +    /// Draws a [`PaneGrid`]. +    /// +    /// It receives: +    /// - the elements of the [`PaneGrid`] +    /// - the [`Pane`] that is currently being dragged +    /// - the [`Layout`] of the [`PaneGrid`] and its elements +    /// - the cursor position +    /// +    /// [`Column`]: struct.Row.html +    /// [`Layout`]: ../layout/struct.Layout.html +    fn draw<Message>( +        &mut self, +        defaults: &Self::Defaults, +        content: &[(Pane, Element<'_, Message, Self>)], +        dragging: Option<Pane>, +        resizing: Option<Axis>, +        layout: Layout<'_>, +        cursor_position: Point, +    ) -> Self::Output; +} + +impl<'a, Message, Renderer> From<PaneGrid<'a, Message, Renderer>> +    for Element<'a, Message, Renderer> +where +    Renderer: self::Renderer + 'static, +    Message: 'static, +{ +    fn from( +        pane_grid: PaneGrid<'a, Message, Renderer>, +    ) -> Element<'a, Message, Renderer> { +        Element::new(pane_grid) +    } +} diff --git a/native/src/widget/pane_grid/axis.rs b/native/src/widget/pane_grid/axis.rs new file mode 100644 index 00000000..f8d53e09 --- /dev/null +++ b/native/src/widget/pane_grid/axis.rs @@ -0,0 +1,54 @@ +use crate::Rectangle; + +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] +pub enum Axis { +    Horizontal, +    Vertical, +} + +impl Axis { +    pub(super) fn split( +        &self, +        rectangle: &Rectangle, +        ratio: f32, +        halved_spacing: f32, +    ) -> (Rectangle, Rectangle) { +        match self { +            Axis::Horizontal => { +                let height_top = +                    (rectangle.height * ratio).round() - halved_spacing; +                let height_bottom = +                    rectangle.height - height_top - halved_spacing; + +                ( +                    Rectangle { +                        height: height_top, +                        ..*rectangle +                    }, +                    Rectangle { +                        y: rectangle.y + height_top + halved_spacing, +                        height: height_bottom, +                        ..*rectangle +                    }, +                ) +            } +            Axis::Vertical => { +                let width_left = +                    (rectangle.width * ratio).round() - halved_spacing; +                let width_right = rectangle.width - width_left - halved_spacing; + +                ( +                    Rectangle { +                        width: width_left, +                        ..*rectangle +                    }, +                    Rectangle { +                        x: rectangle.x + width_left + halved_spacing, +                        width: width_right, +                        ..*rectangle +                    }, +                ) +            } +        } +    } +} diff --git a/native/src/widget/pane_grid/direction.rs b/native/src/widget/pane_grid/direction.rs new file mode 100644 index 00000000..0ee90557 --- /dev/null +++ b/native/src/widget/pane_grid/direction.rs @@ -0,0 +1,7 @@ +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Direction { +    Up, +    Down, +    Left, +    Right, +} diff --git a/native/src/widget/pane_grid/node.rs b/native/src/widget/pane_grid/node.rs new file mode 100644 index 00000000..4d5970b8 --- /dev/null +++ b/native/src/widget/pane_grid/node.rs @@ -0,0 +1,199 @@ +use crate::{ +    pane_grid::{Axis, Pane, Split}, +    Rectangle, Size, +}; + +use std::collections::HashMap; + +#[derive(Debug, Clone, Hash)] +pub enum Node { +    Split { +        id: Split, +        axis: Axis, +        ratio: u32, +        a: Box<Node>, +        b: Box<Node>, +    }, +    Pane(Pane), +} + +impl Node { +    pub fn find(&mut self, pane: &Pane) -> Option<&mut Node> { +        match self { +            Node::Split { a, b, .. } => { +                a.find(pane).or_else(move || b.find(pane)) +            } +            Node::Pane(p) => { +                if p == pane { +                    Some(self) +                } else { +                    None +                } +            } +        } +    } + +    pub fn split(&mut self, id: Split, axis: Axis, new_pane: Pane) { +        *self = Node::Split { +            id, +            axis, +            ratio: 500_000, +            a: Box::new(self.clone()), +            b: Box::new(Node::Pane(new_pane)), +        }; +    } + +    pub fn update(&mut self, f: &impl Fn(&mut Node)) { +        match self { +            Node::Split { a, b, .. } => { +                a.update(f); +                b.update(f); +            } +            _ => {} +        } + +        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, .. } => { +                if a.pane() == Some(*pane) { +                    *self = *b.clone(); +                    Some(self.first_pane()) +                } else if b.pane() == Some(*pane) { +                    *self = *a.clone(); +                    Some(self.first_pane()) +                } else { +                    a.remove(pane).or_else(|| b.remove(pane)) +                } +            } +            Node::Pane(_) => None, +        } +    } + +    pub fn regions( +        &self, +        spacing: f32, +        size: Size, +    ) -> HashMap<Pane, Rectangle> { +        let mut regions = HashMap::new(); + +        self.compute_regions( +            spacing / 2.0, +            &Rectangle { +                x: 0.0, +                y: 0.0, +                width: size.width, +                height: size.height, +            }, +            &mut regions, +        ); + +        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, +            Node::Pane(pane) => Some(*pane), +        } +    } + +    pub fn first_pane(&self) -> Pane { +        match self { +            Node::Split { a, .. } => a.first_pane(), +            Node::Pane(pane) => *pane, +        } +    } + +    fn compute_regions( +        &self, +        halved_spacing: f32, +        current: &Rectangle, +        regions: &mut HashMap<Pane, Rectangle>, +    ) { +        match self { +            Node::Split { +                axis, ratio, a, b, .. +            } => { +                let ratio = *ratio as f32 / 1_000_000.0; +                let (region_a, region_b) = +                    axis.split(current, ratio, halved_spacing); + +                a.compute_regions(halved_spacing, ®ion_a, regions); +                b.compute_regions(halved_spacing, ®ion_b, regions); +            } +            Node::Pane(pane) => { +                let _ = regions.insert(*pane, *current); +            } +        } +    } + +    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/pane.rs b/native/src/widget/pane_grid/pane.rs new file mode 100644 index 00000000..cfca3b03 --- /dev/null +++ b/native/src/widget/pane_grid/pane.rs @@ -0,0 +1,2 @@ +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct Pane(pub(super) usize); diff --git a/native/src/widget/pane_grid/split.rs b/native/src/widget/pane_grid/split.rs new file mode 100644 index 00000000..c2dad980 --- /dev/null +++ b/native/src/widget/pane_grid/split.rs @@ -0,0 +1,2 @@ +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct Split(pub(super) usize); diff --git a/native/src/widget/pane_grid/state.rs b/native/src/widget/pane_grid/state.rs new file mode 100644 index 00000000..456ad78a --- /dev/null +++ b/native/src/widget/pane_grid/state.rs @@ -0,0 +1,261 @@ +use crate::{ +    input::keyboard, +    pane_grid::{node::Node, Axis, Direction, Pane, Split}, +    Hasher, Point, Rectangle, Size, +}; + +use std::collections::HashMap; + +#[derive(Debug)] +pub struct State<T> { +    pub(super) panes: HashMap<Pane, T>, +    pub(super) internal: Internal, +    pub(super) modifiers: keyboard::ModifiersState, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Focus { +    Idle, +    Dragging, +} + +impl<T> State<T> { +    pub fn new(first_pane_state: T) -> (Self, Pane) { +        let first_pane = Pane(0); + +        let mut panes = HashMap::new(); +        let _ = panes.insert(first_pane, first_pane_state); + +        ( +            State { +                panes, +                internal: Internal { +                    layout: Node::Pane(first_pane), +                    last_id: 0, +                    action: Action::Idle { focus: None }, +                }, +                modifiers: keyboard::ModifiersState::default(), +            }, +            first_pane, +        ) +    } + +    pub fn len(&self) -> usize { +        self.panes.len() +    } + +    pub fn get_mut(&mut self, pane: &Pane) -> Option<&mut T> { +        self.panes.get_mut(pane) +    } + +    pub fn iter(&self) -> impl Iterator<Item = (&Pane, &T)> { +        self.panes.iter() +    } + +    pub fn iter_mut(&mut self) -> impl Iterator<Item = (&Pane, &mut T)> { +        self.panes.iter_mut() +    } + +    pub fn active(&self) -> Option<Pane> { +        match self.internal.action { +            Action::Idle { focus } => focus, +            _ => None, +        } +    } + +    pub fn adjacent(&self, pane: &Pane, direction: Direction) -> Option<Pane> { +        let regions = +            self.internal.layout.regions(0.0, Size::new(4096.0, 4096.0)); + +        let current_region = regions.get(pane)?; + +        let target = match direction { +            Direction::Left => { +                Point::new(current_region.x - 1.0, current_region.y + 1.0) +            } +            Direction::Right => Point::new( +                current_region.x + current_region.width + 1.0, +                current_region.y + 1.0, +            ), +            Direction::Up => { +                Point::new(current_region.x + 1.0, current_region.y - 1.0) +            } +            Direction::Down => Point::new( +                current_region.x + 1.0, +                current_region.y + current_region.height + 1.0, +            ), +        }; + +        let mut colliding_regions = +            regions.iter().filter(|(_, region)| region.contains(target)); + +        let (pane, _) = colliding_regions.next()?; + +        Some(*pane) +    } + +    pub fn focus(&mut self, pane: &Pane) { +        self.internal.focus(pane); +    } + +    pub fn split(&mut self, axis: Axis, pane: &Pane, state: T) -> Option<Pane> { +        let node = self.internal.layout.find(pane)?; + +        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) +        }; + +        node.split(new_split, axis, new_pane); + +        let _ = self.panes.insert(new_pane, state); +        self.focus(&new_pane); + +        Some(new_pane) +    } + +    pub fn swap(&mut self, a: &Pane, b: &Pane) { +        self.internal.layout.update(&|node| match node { +            Node::Split { .. } => {} +            Node::Pane(pane) => { +                if pane == a { +                    *node = Node::Pane(*b); +                } else if pane == b { +                    *node = Node::Pane(*a); +                } +            } +        }); +    } + +    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); +            self.panes.remove(pane) +        } else { +            None +        } +    } +} + +#[derive(Debug)] +pub struct Internal { +    layout: Node, +    last_id: usize, +    action: Action, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Action { +    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 } | Action::Resizing { focus, .. } => { +                focus.map(|pane| (pane, Focus::Idle)) +            } +            Action::Dragging { pane } => Some((*pane, Focus::Dragging)), +        } +    } +} + +impl Internal { +    pub fn action(&self) -> Action { +        self.action +    } + +    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, +        size: Size, +    ) -> HashMap<Pane, Rectangle> { +        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 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 }; +    } + +    pub fn hash_layout(&self, hasher: &mut Hasher) { +        use std::hash::Hash; + +        self.layout.hash(hasher); +    } +}  | 
