diff options
Diffstat (limited to 'native/src/widget')
| -rw-r--r-- | native/src/widget/column.rs | 2 | ||||
| -rw-r--r-- | native/src/widget/pane_grid.rs | 647 | ||||
| -rw-r--r-- | native/src/widget/pane_grid/axis.rs | 54 | ||||
| -rw-r--r-- | native/src/widget/pane_grid/direction.rs | 12 | ||||
| -rw-r--r-- | native/src/widget/pane_grid/node.rs | 199 | ||||
| -rw-r--r-- | native/src/widget/pane_grid/pane.rs | 5 | ||||
| -rw-r--r-- | native/src/widget/pane_grid/split.rs | 5 | ||||
| -rw-r--r-- | native/src/widget/pane_grid/state.rs | 368 | 
8 files changed, 1291 insertions, 1 deletions
| diff --git a/native/src/widget/column.rs b/native/src/widget/column.rs index a7a6f242..b1adc6e3 100644 --- a/native/src/widget/column.rs +++ b/native/src/widget/column.rs @@ -219,7 +219,7 @@ pub trait Renderer: crate::Renderer + Sized {      /// - the [`Layout`] of the [`Column`] and its children      /// - the cursor position      /// -    /// [`Column`]: struct.Row.html +    /// [`Column`]: struct.Column.html      /// [`Layout`]: ../layout/struct.Layout.html      fn draw<Message>(          &mut self, diff --git a/native/src/widget/pane_grid.rs b/native/src/widget/pane_grid.rs new file mode 100644 index 00000000..a88f591a --- /dev/null +++ b/native/src/widget/pane_grid.rs @@ -0,0 +1,647 @@ +//! Let your users split regions of your application and organize layout dynamically. +//! +//! [](https://gfycat.com/mixedflatjellyfish) +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, +}; + +/// A collection of panes distributed using either vertical or horizontal splits +/// to completely fill the space available. +/// +/// [](https://gfycat.com/mixedflatjellyfish) +/// +/// This distribution of space is common in tiling window managers (like +/// [`awesome`](https://awesomewm.org/), [`i3`](https://i3wm.org/), or even +/// [`tmux`](https://github.com/tmux/tmux)). +/// +/// A [`PaneGrid`] supports: +/// +/// * Vertical and horizontal splits +/// * Tracking of the last active pane +/// * Mouse-based resizing +/// * Drag and drop to reorganize panes +/// * Hotkey support +/// * Configurable modifier keys +/// * [`State`] API to perform actions programmatically (`split`, `swap`, `resize`, etc.) +/// +/// ## Example +/// +/// ``` +/// # use iced_native::{pane_grid, Text}; +/// # +/// # type PaneGrid<'a, Message> = +/// #     iced_native::PaneGrid<'a, Message, iced_native::renderer::Null>; +/// # +/// enum PaneState { +///     SomePane, +///     AnotherKindOfPane, +/// } +/// +/// enum Message { +///     PaneDragged(pane_grid::DragEvent), +///     PaneResized(pane_grid::ResizeEvent), +/// } +/// +/// let (mut state, _) = pane_grid::State::new(PaneState::SomePane); +/// +/// let pane_grid = PaneGrid::new(&mut state, |pane, state, focus| { +///     match state { +///         PaneState::SomePane => Text::new("This is some pane"), +///         PaneState::AnotherKindOfPane => Text::new("This is another kind of pane"), +///     }.into() +/// }) +///     .on_drag(Message::PaneDragged) +///     .on_resize(Message::PaneResized); +/// ``` +/// +/// [`PaneGrid`]: struct.PaneGrid.html +/// [`State`]: struct.State.html +#[allow(missing_debug_implementations)] +pub struct PaneGrid<'a, Message, Renderer> { +    state: &'a mut state::Internal, +    pressed_modifiers: &'a mut keyboard::ModifiersState, +    elements: Vec<(Pane, Element<'a, Message, Renderer>)>, +    width: Length, +    height: Length, +    spacing: u16, +    modifier_keys: keyboard::ModifiersState, +    on_drag: Option<Box<dyn Fn(DragEvent) -> Message>>, +    on_resize: Option<Box<dyn Fn(ResizeEvent) -> Message>>, +    on_key_press: Option<Box<dyn Fn(KeyPressEvent) -> Option<Message>>>, +} + +impl<'a, Message, Renderer> PaneGrid<'a, Message, Renderer> { +    /// Creates a [`PaneGrid`] with the given [`State`] and view function. +    /// +    /// The view function will be called to display each [`Pane`] present in the +    /// [`State`]. +    /// +    /// [`PaneGrid`]: struct.PaneGrid.html +    /// [`State`]: struct.State.html +    /// [`Pane`]: struct.Pane.html +    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, +            pressed_modifiers: &mut state.modifiers, +            elements, +            width: Length::Fill, +            height: Length::Fill, +            spacing: 0, +            modifier_keys: keyboard::ModifiersState { +                control: true, +                ..Default::default() +            }, +            on_drag: None, +            on_resize: None, +            on_key_press: None, +        } +    } + +    /// Sets the width of the [`PaneGrid`]. +    /// +    /// [`PaneGrid`]: struct.PaneGrid.html +    pub fn width(mut self, width: Length) -> Self { +        self.width = width; +        self +    } + +    /// Sets the height of the [`PaneGrid`]. +    /// +    /// [`PaneGrid`]: struct.PaneGrid.html +    pub fn height(mut self, height: Length) -> Self { +        self.height = height; +        self +    } + +    /// Sets the spacing _between_ the panes of the [`PaneGrid`]. +    /// +    /// [`PaneGrid`]: struct.PaneGrid.html +    pub fn spacing(mut self, units: u16) -> Self { +        self.spacing = units; +        self +    } + +    /// Sets the modifier keys of the [`PaneGrid`]. +    /// +    /// The modifier keys will need to be pressed to trigger dragging, resizing, +    /// and key events. +    /// +    /// The default modifier key is `Ctrl`. +    /// +    /// [`PaneGrid`]: struct.PaneGrid.html +    pub fn modifier_keys( +        mut self, +        modifier_keys: keyboard::ModifiersState, +    ) -> Self { +        self.modifier_keys = modifier_keys; +        self +    } + +    /// Enables the drag and drop interactions of the [`PaneGrid`], which will +    /// use the provided function to produce messages. +    /// +    /// Panes can be dragged using `Modifier keys + Left click`. +    /// +    /// [`PaneGrid`]: struct.PaneGrid.html +    pub fn on_drag( +        mut self, +        f: impl Fn(DragEvent) -> Message + 'static, +    ) -> Self { +        self.on_drag = Some(Box::new(f)); +        self +    } + +    /// Enables the resize interactions of the [`PaneGrid`], which will +    /// use the provided function to produce messages. +    /// +    /// Panes can be resized using `Modifier keys + Right click`. +    /// +    /// [`PaneGrid`]: struct.PaneGrid.html +    pub fn on_resize( +        mut self, +        f: impl Fn(ResizeEvent) -> Message + 'static, +    ) -> Self { +        self.on_resize = Some(Box::new(f)); +        self +    } + +    /// Captures hotkey interactions with the [`PaneGrid`], using the provided +    /// function to produce messages. +    /// +    /// The function will be called when: +    ///   - a [`Pane`] is focused +    ///   - a key is pressed +    ///   - all the modifier keys are pressed +    /// +    /// If the function returns `None`, the key press event will be discarded +    /// without producing any message. +    /// +    /// This method is particularly useful to implement hotkey interactions. +    /// For instance, you can use it to enable splitting, swapping, or resizing +    /// panes by pressing combinations of keys. +    /// +    /// [`PaneGrid`]: struct.PaneGrid.html +    /// [`Pane`]: struct.Pane.html +    pub fn on_key_press( +        mut self, +        f: impl Fn(KeyPressEvent) -> Option<Message> + 'static, +    ) -> Self { +        self.on_key_press = 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 })); +                } +            } +        } +    } +} + +/// An event produced during a drag and drop interaction of a [`PaneGrid`]. +/// +/// [`PaneGrid`]: struct.PaneGrid.html +#[derive(Debug, Clone, Copy)] +pub enum DragEvent { +    /// A [`Pane`] was picked for dragging. +    /// +    /// [`Pane`]: struct.Pane.html +    Picked { +        /// The picked [`Pane`]. +        /// +        /// [`Pane`]: struct.Pane.html +        pane: Pane, +    }, + +    /// A [`Pane`] was dropped on top of another [`Pane`]. +    /// +    /// [`Pane`]: struct.Pane.html +    Dropped { +        /// The picked [`Pane`]. +        /// +        /// [`Pane`]: struct.Pane.html +        pane: Pane, + +        /// The [`Pane`] where the picked one was dropped on. +        /// +        /// [`Pane`]: struct.Pane.html +        target: Pane, +    }, + +    /// A [`Pane`] was picked and then dropped outside of other [`Pane`] +    /// boundaries. +    /// +    /// [`Pane`]: struct.Pane.html +    Canceled { +        /// The picked [`Pane`]. +        /// +        /// [`Pane`]: struct.Pane.html +        pane: Pane, +    }, +} + +/// An event produced during a resize interaction of a [`PaneGrid`]. +/// +/// [`PaneGrid`]: struct.PaneGrid.html +#[derive(Debug, Clone, Copy)] +pub struct ResizeEvent { +    /// The [`Split`] that is being dragged for resizing. +    /// +    /// [`Split`]: struct.Split.html +    pub split: Split, + +    /// The new ratio of the [`Split`]. +    /// +    /// The ratio is a value in [0, 1], representing the exact position of a +    /// [`Split`] between two panes. +    /// +    /// [`Split`]: struct.Split.html +    pub ratio: f32, +} + +/// An event produced during a key press interaction of a [`PaneGrid`]. +/// +/// [`PaneGrid`]: struct.PaneGrid.html +#[derive(Debug, Clone, Copy)] +pub struct KeyPressEvent { +    /// The key that was pressed. +    pub key_code: keyboard::KeyCode, + +    /// The state of the modifier keys when the key was pressed. +    pub modifiers: keyboard::ModifiersState, +} + +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 +                                    .pressed_modifiers +                                    .matches(self.modifier_keys) => +                            { +                                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.pressed_modifiers.matches(self.modifier_keys) => +            { +                let bounds = layout.bounds(); + +                if bounds.contains(cursor_position) { +                    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, +                key_code, +                state, +            }) => { +                if let Some(on_key_press) = &self.on_key_press { +                    // TODO: Discard when event is captured +                    if state == ButtonState::Pressed { +                        if let Some(_) = self.state.active_pane() { +                            if modifiers.matches(self.modifier_keys) { +                                if let Some(message) = +                                    on_key_press(KeyPressEvent { +                                        key_code, +                                        modifiers, +                                    }) +                                { +                                    messages.push(message); +                                } +                            } +                        } +                    } +                } + +                *self.pressed_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 [`Axis`] that is currently being resized +    /// - the [`Layout`] of the [`PaneGrid`] and its elements +    /// - the cursor position +    /// +    /// [`PaneGrid`]: struct.PaneGrid.html +    /// [`Pane`]: struct.Pane.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..f0e3f362 --- /dev/null +++ b/native/src/widget/pane_grid/axis.rs @@ -0,0 +1,54 @@ +use crate::Rectangle; + +/// A fixed reference line for the measurement of coordinates. +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] +pub enum Axis { +    /// The horizontal axis: — +    Horizontal, +    /// The vertical axis: | +    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(); +                let height_bottom = rectangle.height - height_top; + +                ( +                    Rectangle { +                        height: height_top - halved_spacing, +                        ..*rectangle +                    }, +                    Rectangle { +                        y: rectangle.y + height_top + halved_spacing, +                        height: height_bottom - halved_spacing, +                        ..*rectangle +                    }, +                ) +            } +            Axis::Vertical => { +                let width_left = (rectangle.width * ratio).round(); +                let width_right = rectangle.width - width_left; + +                ( +                    Rectangle { +                        width: width_left - halved_spacing, +                        ..*rectangle +                    }, +                    Rectangle { +                        x: rectangle.x + width_left + halved_spacing, +                        width: width_right - halved_spacing, +                        ..*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..b31a8737 --- /dev/null +++ b/native/src/widget/pane_grid/direction.rs @@ -0,0 +1,12 @@ +/// A four cardinal direction. +#[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..f9866407 --- /dev/null +++ b/native/src/widget/pane_grid/pane.rs @@ -0,0 +1,5 @@ +/// A rectangular region in a [`PaneGrid`] used to display widgets. +/// +/// [`PaneGrid`]: struct.PaneGrid.html +#[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..d020c510 --- /dev/null +++ b/native/src/widget/pane_grid/split.rs @@ -0,0 +1,5 @@ +/// A divider that splits a region in a [`PaneGrid`] into two different panes. +/// +/// [`PaneGrid`]: struct.PaneGrid.html +#[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..0a8b8419 --- /dev/null +++ b/native/src/widget/pane_grid/state.rs @@ -0,0 +1,368 @@ +use crate::{ +    input::keyboard, +    pane_grid::{node::Node, Axis, Direction, Pane, Split}, +    Hasher, Point, Rectangle, Size, +}; + +use std::collections::HashMap; + +/// The state of a [`PaneGrid`]. +/// +/// It keeps track of the state of each [`Pane`] and the position of each +/// [`Split`]. +/// +/// The [`State`] needs to own any mutable contents a [`Pane`] may need. This is +/// why this struct is generic over the type `T`. Values of this type are +/// provided to the view function of [`PaneGrid::new`] for displaying each +/// [`Pane`]. +/// +/// [`PaneGrid`]: struct.PaneGrid.html +/// [`PaneGrid::new`]: struct.PaneGrid.html#method.new +/// [`Pane`]: struct.Pane.html +/// [`Split`]: struct.Split.html +/// [`State`]: struct.State.html +#[derive(Debug)] +pub struct State<T> { +    pub(super) panes: HashMap<Pane, T>, +    pub(super) internal: Internal, +    pub(super) modifiers: keyboard::ModifiersState, +} + +/// The current focus of a [`Pane`]. +/// +/// [`Pane`]: struct.Pane.html +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Focus { +    /// The [`Pane`] is just focused. +    /// +    /// [`Pane`]: struct.Pane.html +    Idle, + +    /// The [`Pane`] is being dragged. +    /// +    /// [`Pane`]: struct.Pane.html +    Dragging, +} + +impl<T> State<T> { +    /// Creates a new [`State`], initializing the first pane with the provided +    /// state. +    /// +    /// Alongside the [`State`], it returns the first [`Pane`] identifier. +    /// +    /// [`State`]: struct.State.html +    /// [`Pane`]: struct.Pane.html +    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, +        ) +    } + +    /// Returns the total amount of panes in the [`State`]. +    /// +    /// [`State`]: struct.State.html +    pub fn len(&self) -> usize { +        self.panes.len() +    } + +    /// Returns the internal state of the given [`Pane`], if it exists. +    /// +    /// [`Pane`]: struct.Pane.html +    pub fn get_mut(&mut self, pane: &Pane) -> Option<&mut T> { +        self.panes.get_mut(pane) +    } + +    /// Returns an iterator over all the panes of the [`State`], alongside its +    /// internal state. +    /// +    /// [`State`]: struct.State.html +    pub fn iter(&self) -> impl Iterator<Item = (&Pane, &T)> { +        self.panes.iter() +    } + +    /// Returns a mutable iterator over all the panes of the [`State`], +    /// alongside its internal state. +    /// +    /// [`State`]: struct.State.html +    pub fn iter_mut(&mut self) -> impl Iterator<Item = (&Pane, &mut T)> { +        self.panes.iter_mut() +    } + +    /// Returns the active [`Pane`] of the [`State`], if there is one. +    /// +    /// A [`Pane`] is active if it is focused and is __not__ being dragged. +    /// +    /// [`Pane`]: struct.Pane.html +    /// [`State`]: struct.State.html +    pub fn active(&self) -> Option<Pane> { +        self.internal.active_pane() +    } + +    /// Returns the adjacent [`Pane`] of another [`Pane`] in the given +    /// direction, if there is one. +    /// +    /// ## Example +    /// You can combine this with [`State::active`] to find the pane that is +    /// adjacent to the current active one, and then swap them. For instance: +    /// +    /// ``` +    /// # use iced_native::pane_grid; +    /// # +    /// # let (mut state, _) = pane_grid::State::new(()); +    /// # +    /// if let Some(active) = state.active() { +    ///     if let Some(adjacent) = state.adjacent(&active, pane_grid::Direction::Right) { +    ///         state.swap(&active, &adjacent); +    ///     } +    /// } +    /// ``` +    /// +    /// [`Pane`]: struct.Pane.html +    /// [`State::active`]: struct.State.html#method.active +    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) +    } + +    /// Focuses the given [`Pane`]. +    /// +    /// [`Pane`]: struct.Pane.html +    pub fn focus(&mut self, pane: &Pane) { +        self.internal.focus(pane); +    } + +    /// Splits the given [`Pane`] into two in the given [`Axis`] and +    /// initializing the new [`Pane`] with the provided internal state. +    /// +    /// [`Pane`]: struct.Pane.html +    /// [`Axis`]: enum.Axis.html +    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) +    } + +    /// Swaps the position of the provided panes in the [`State`]. +    /// +    /// If you want to swap panes on drag and drop in your [`PaneGrid`], you +    /// will need to call this method when handling a [`DragEvent`]. +    /// +    /// [`State`]: struct.State.html +    /// [`PaneGrid`]: struct.PaneGrid.html +    /// [`DragEvent`]: struct.DragEvent.html +    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); +                } +            } +        }); +    } + +    /// Resizes two panes by setting the position of the provided [`Split`]. +    /// +    /// The ratio is a value in [0, 1], representing the exact position of a +    /// [`Split`] between two panes. +    /// +    /// If you want to enable resize interactions in your [`PaneGrid`], you will +    /// need to call this method when handling a [`ResizeEvent`]. +    /// +    /// [`Split`]: struct.Split.html +    /// [`PaneGrid`]: struct.PaneGrid.html +    /// [`ResizeEvent`]: struct.ResizeEvent.html +    pub fn resize(&mut self, split: &Split, ratio: f32) { +        let _ = self.internal.layout.resize(split, ratio); +    } + +    /// Closes the given [`Pane`] and returns its internal state, if it exists. +    /// +    /// [`Pane`]: struct.Pane.html +    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 active_pane(&self) -> Option<Pane> { +        match self.action { +            Action::Idle { focus } => focus, +            _ => None, +        } +    } + +    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); +    } +} | 
