diff options
Diffstat (limited to 'widget/src/pane_grid')
| -rw-r--r-- | widget/src/pane_grid/axis.rs | 241 | ||||
| -rw-r--r-- | widget/src/pane_grid/configuration.rs | 26 | ||||
| -rw-r--r-- | widget/src/pane_grid/content.rs | 373 | ||||
| -rw-r--r-- | widget/src/pane_grid/direction.rs | 12 | ||||
| -rw-r--r-- | widget/src/pane_grid/draggable.rs | 12 | ||||
| -rw-r--r-- | widget/src/pane_grid/node.rs | 250 | ||||
| -rw-r--r-- | widget/src/pane_grid/pane.rs | 5 | ||||
| -rw-r--r-- | widget/src/pane_grid/split.rs | 5 | ||||
| -rw-r--r-- | widget/src/pane_grid/state.rs | 348 | ||||
| -rw-r--r-- | widget/src/pane_grid/title_bar.rs | 432 | 
10 files changed, 1704 insertions, 0 deletions
diff --git a/widget/src/pane_grid/axis.rs b/widget/src/pane_grid/axis.rs new file mode 100644 index 00000000..a3049230 --- /dev/null +++ b/widget/src/pane_grid/axis.rs @@ -0,0 +1,241 @@ +use crate::core::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 { +    /// Splits the provided [`Rectangle`] on the current [`Axis`] with the +    /// given `ratio` and `spacing`. +    pub fn split( +        &self, +        rectangle: &Rectangle, +        ratio: f32, +        spacing: f32, +    ) -> (Rectangle, Rectangle) { +        match self { +            Axis::Horizontal => { +                let height_top = +                    (rectangle.height * ratio - spacing / 2.0).round(); +                let height_bottom = rectangle.height - height_top - spacing; + +                ( +                    Rectangle { +                        height: height_top, +                        ..*rectangle +                    }, +                    Rectangle { +                        y: rectangle.y + height_top + spacing, +                        height: height_bottom, +                        ..*rectangle +                    }, +                ) +            } +            Axis::Vertical => { +                let width_left = +                    (rectangle.width * ratio - spacing / 2.0).round(); +                let width_right = rectangle.width - width_left - spacing; + +                ( +                    Rectangle { +                        width: width_left, +                        ..*rectangle +                    }, +                    Rectangle { +                        x: rectangle.x + width_left + spacing, +                        width: width_right, +                        ..*rectangle +                    }, +                ) +            } +        } +    } + +    /// Calculates the bounds of the split line in a [`Rectangle`] region. +    pub fn split_line_bounds( +        &self, +        rectangle: Rectangle, +        ratio: f32, +        spacing: f32, +    ) -> Rectangle { +        match self { +            Axis::Horizontal => Rectangle { +                x: rectangle.x, +                y: (rectangle.y + rectangle.height * ratio - spacing / 2.0) +                    .round(), +                width: rectangle.width, +                height: spacing, +            }, +            Axis::Vertical => Rectangle { +                x: (rectangle.x + rectangle.width * ratio - spacing / 2.0) +                    .round(), +                y: rectangle.y, +                width: spacing, +                height: rectangle.height, +            }, +        } +    } +} + +#[cfg(test)] +mod tests { +    use super::*; + +    enum Case { +        Horizontal { +            overall_height: f32, +            spacing: f32, +            top_height: f32, +            bottom_y: f32, +            bottom_height: f32, +        }, +        Vertical { +            overall_width: f32, +            spacing: f32, +            left_width: f32, +            right_x: f32, +            right_width: f32, +        }, +    } + +    #[test] +    fn split() { +        let cases = vec![ +            // Even height, even spacing +            Case::Horizontal { +                overall_height: 10.0, +                spacing: 2.0, +                top_height: 4.0, +                bottom_y: 6.0, +                bottom_height: 4.0, +            }, +            // Odd height, even spacing +            Case::Horizontal { +                overall_height: 9.0, +                spacing: 2.0, +                top_height: 4.0, +                bottom_y: 6.0, +                bottom_height: 3.0, +            }, +            // Even height, odd spacing +            Case::Horizontal { +                overall_height: 10.0, +                spacing: 1.0, +                top_height: 5.0, +                bottom_y: 6.0, +                bottom_height: 4.0, +            }, +            // Odd height, odd spacing +            Case::Horizontal { +                overall_height: 9.0, +                spacing: 1.0, +                top_height: 4.0, +                bottom_y: 5.0, +                bottom_height: 4.0, +            }, +            // Even width, even spacing +            Case::Vertical { +                overall_width: 10.0, +                spacing: 2.0, +                left_width: 4.0, +                right_x: 6.0, +                right_width: 4.0, +            }, +            // Odd width, even spacing +            Case::Vertical { +                overall_width: 9.0, +                spacing: 2.0, +                left_width: 4.0, +                right_x: 6.0, +                right_width: 3.0, +            }, +            // Even width, odd spacing +            Case::Vertical { +                overall_width: 10.0, +                spacing: 1.0, +                left_width: 5.0, +                right_x: 6.0, +                right_width: 4.0, +            }, +            // Odd width, odd spacing +            Case::Vertical { +                overall_width: 9.0, +                spacing: 1.0, +                left_width: 4.0, +                right_x: 5.0, +                right_width: 4.0, +            }, +        ]; +        for case in cases { +            match case { +                Case::Horizontal { +                    overall_height, +                    spacing, +                    top_height, +                    bottom_y, +                    bottom_height, +                } => { +                    let a = Axis::Horizontal; +                    let r = Rectangle { +                        x: 0.0, +                        y: 0.0, +                        width: 10.0, +                        height: overall_height, +                    }; +                    let (top, bottom) = a.split(&r, 0.5, spacing); +                    assert_eq!( +                        top, +                        Rectangle { +                            height: top_height, +                            ..r +                        } +                    ); +                    assert_eq!( +                        bottom, +                        Rectangle { +                            y: bottom_y, +                            height: bottom_height, +                            ..r +                        } +                    ); +                } +                Case::Vertical { +                    overall_width, +                    spacing, +                    left_width, +                    right_x, +                    right_width, +                } => { +                    let a = Axis::Vertical; +                    let r = Rectangle { +                        x: 0.0, +                        y: 0.0, +                        width: overall_width, +                        height: 10.0, +                    }; +                    let (left, right) = a.split(&r, 0.5, spacing); +                    assert_eq!( +                        left, +                        Rectangle { +                            width: left_width, +                            ..r +                        } +                    ); +                    assert_eq!( +                        right, +                        Rectangle { +                            x: right_x, +                            width: right_width, +                            ..r +                        } +                    ); +                } +            } +        } +    } +} diff --git a/widget/src/pane_grid/configuration.rs b/widget/src/pane_grid/configuration.rs new file mode 100644 index 00000000..ddbc3bc2 --- /dev/null +++ b/widget/src/pane_grid/configuration.rs @@ -0,0 +1,26 @@ +use crate::pane_grid::Axis; + +/// The arrangement of a [`PaneGrid`]. +/// +/// [`PaneGrid`]: crate::widget::PaneGrid +#[derive(Debug, Clone)] +pub enum Configuration<T> { +    /// A split of the available space. +    Split { +        /// The direction of the split. +        axis: Axis, + +        /// The ratio of the split in [0.0, 1.0]. +        ratio: f32, + +        /// The left/top [`Configuration`] of the split. +        a: Box<Configuration<T>>, + +        /// The right/bottom [`Configuration`] of the split. +        b: Box<Configuration<T>>, +    }, +    /// A [`Pane`]. +    /// +    /// [`Pane`]: crate::widget::pane_grid::Pane +    Pane(T), +} diff --git a/widget/src/pane_grid/content.rs b/widget/src/pane_grid/content.rs new file mode 100644 index 00000000..035ef05b --- /dev/null +++ b/widget/src/pane_grid/content.rs @@ -0,0 +1,373 @@ +use crate::container; +use crate::core::event::{self, Event}; +use crate::core::layout; +use crate::core::mouse; +use crate::core::overlay; +use crate::core::renderer; +use crate::core::widget::{self, Tree}; +use crate::core::{Clipboard, Element, Layout, Point, Rectangle, Shell, Size}; +use crate::pane_grid::{Draggable, TitleBar}; + +/// The content of a [`Pane`]. +/// +/// [`Pane`]: crate::widget::pane_grid::Pane +#[allow(missing_debug_implementations)] +pub struct Content<'a, Message, Renderer = crate::Renderer> +where +    Renderer: crate::core::Renderer, +    Renderer::Theme: container::StyleSheet, +{ +    title_bar: Option<TitleBar<'a, Message, Renderer>>, +    body: Element<'a, Message, Renderer>, +    style: <Renderer::Theme as container::StyleSheet>::Style, +} + +impl<'a, Message, Renderer> Content<'a, Message, Renderer> +where +    Renderer: crate::core::Renderer, +    Renderer::Theme: container::StyleSheet, +{ +    /// Creates a new [`Content`] with the provided body. +    pub fn new(body: impl Into<Element<'a, Message, Renderer>>) -> Self { +        Self { +            title_bar: None, +            body: body.into(), +            style: Default::default(), +        } +    } + +    /// Sets the [`TitleBar`] of this [`Content`]. +    pub fn title_bar( +        mut self, +        title_bar: TitleBar<'a, Message, Renderer>, +    ) -> Self { +        self.title_bar = Some(title_bar); +        self +    } + +    /// Sets the style of the [`Content`]. +    pub fn style( +        mut self, +        style: impl Into<<Renderer::Theme as container::StyleSheet>::Style>, +    ) -> Self { +        self.style = style.into(); +        self +    } +} + +impl<'a, Message, Renderer> Content<'a, Message, Renderer> +where +    Renderer: crate::core::Renderer, +    Renderer::Theme: container::StyleSheet, +{ +    pub(super) fn state(&self) -> Tree { +        let children = if let Some(title_bar) = self.title_bar.as_ref() { +            vec![Tree::new(&self.body), title_bar.state()] +        } else { +            vec![Tree::new(&self.body), Tree::empty()] +        }; + +        Tree { +            children, +            ..Tree::empty() +        } +    } + +    pub(super) fn diff(&self, tree: &mut Tree) { +        if tree.children.len() == 2 { +            if let Some(title_bar) = self.title_bar.as_ref() { +                title_bar.diff(&mut tree.children[1]); +            } + +            tree.children[0].diff(&self.body); +        } else { +            *tree = self.state(); +        } +    } + +    /// Draws the [`Content`] with the provided [`Renderer`] and [`Layout`]. +    /// +    /// [`Renderer`]: crate::Renderer +    pub fn draw( +        &self, +        tree: &Tree, +        renderer: &mut Renderer, +        theme: &Renderer::Theme, +        style: &renderer::Style, +        layout: Layout<'_>, +        cursor_position: Point, +        viewport: &Rectangle, +    ) { +        use container::StyleSheet; + +        let bounds = layout.bounds(); + +        { +            let style = theme.appearance(&self.style); + +            container::draw_background(renderer, &style, bounds); +        } + +        if let Some(title_bar) = &self.title_bar { +            let mut children = layout.children(); +            let title_bar_layout = children.next().unwrap(); +            let body_layout = children.next().unwrap(); + +            let show_controls = bounds.contains(cursor_position); + +            self.body.as_widget().draw( +                &tree.children[0], +                renderer, +                theme, +                style, +                body_layout, +                cursor_position, +                viewport, +            ); + +            title_bar.draw( +                &tree.children[1], +                renderer, +                theme, +                style, +                title_bar_layout, +                cursor_position, +                viewport, +                show_controls, +            ); +        } else { +            self.body.as_widget().draw( +                &tree.children[0], +                renderer, +                theme, +                style, +                layout, +                cursor_position, +                viewport, +            ); +        } +    } + +    pub(crate) fn layout( +        &self, +        renderer: &Renderer, +        limits: &layout::Limits, +    ) -> layout::Node { +        if let Some(title_bar) = &self.title_bar { +            let max_size = limits.max(); + +            let title_bar_layout = title_bar +                .layout(renderer, &layout::Limits::new(Size::ZERO, max_size)); + +            let title_bar_size = title_bar_layout.size(); + +            let mut body_layout = self.body.as_widget().layout( +                renderer, +                &layout::Limits::new( +                    Size::ZERO, +                    Size::new( +                        max_size.width, +                        max_size.height - title_bar_size.height, +                    ), +                ), +            ); + +            body_layout.move_to(Point::new(0.0, title_bar_size.height)); + +            layout::Node::with_children( +                max_size, +                vec![title_bar_layout, body_layout], +            ) +        } else { +            self.body.as_widget().layout(renderer, limits) +        } +    } + +    pub(crate) fn operate( +        &self, +        tree: &mut Tree, +        layout: Layout<'_>, +        renderer: &Renderer, +        operation: &mut dyn widget::Operation<Message>, +    ) { +        let body_layout = if let Some(title_bar) = &self.title_bar { +            let mut children = layout.children(); + +            title_bar.operate( +                &mut tree.children[1], +                children.next().unwrap(), +                renderer, +                operation, +            ); + +            children.next().unwrap() +        } else { +            layout +        }; + +        self.body.as_widget().operate( +            &mut tree.children[0], +            body_layout, +            renderer, +            operation, +        ); +    } + +    pub(crate) fn on_event( +        &mut self, +        tree: &mut Tree, +        event: Event, +        layout: Layout<'_>, +        cursor_position: Point, +        renderer: &Renderer, +        clipboard: &mut dyn Clipboard, +        shell: &mut Shell<'_, Message>, +        is_picked: bool, +    ) -> event::Status { +        let mut event_status = event::Status::Ignored; + +        let body_layout = if let Some(title_bar) = &mut self.title_bar { +            let mut children = layout.children(); + +            event_status = title_bar.on_event( +                &mut tree.children[1], +                event.clone(), +                children.next().unwrap(), +                cursor_position, +                renderer, +                clipboard, +                shell, +            ); + +            children.next().unwrap() +        } else { +            layout +        }; + +        let body_status = if is_picked { +            event::Status::Ignored +        } else { +            self.body.as_widget_mut().on_event( +                &mut tree.children[0], +                event, +                body_layout, +                cursor_position, +                renderer, +                clipboard, +                shell, +            ) +        }; + +        event_status.merge(body_status) +    } + +    pub(crate) fn mouse_interaction( +        &self, +        tree: &Tree, +        layout: Layout<'_>, +        cursor_position: Point, +        viewport: &Rectangle, +        renderer: &Renderer, +        drag_enabled: bool, +    ) -> mouse::Interaction { +        let (body_layout, title_bar_interaction) = +            if let Some(title_bar) = &self.title_bar { +                let mut children = layout.children(); +                let title_bar_layout = children.next().unwrap(); + +                let is_over_pick_area = title_bar +                    .is_over_pick_area(title_bar_layout, cursor_position); + +                if is_over_pick_area && drag_enabled { +                    return mouse::Interaction::Grab; +                } + +                let mouse_interaction = title_bar.mouse_interaction( +                    &tree.children[1], +                    title_bar_layout, +                    cursor_position, +                    viewport, +                    renderer, +                ); + +                (children.next().unwrap(), mouse_interaction) +            } else { +                (layout, mouse::Interaction::default()) +            }; + +        self.body +            .as_widget() +            .mouse_interaction( +                &tree.children[0], +                body_layout, +                cursor_position, +                viewport, +                renderer, +            ) +            .max(title_bar_interaction) +    } + +    pub(crate) fn overlay<'b>( +        &'b mut self, +        tree: &'b mut Tree, +        layout: Layout<'_>, +        renderer: &Renderer, +    ) -> Option<overlay::Element<'b, Message, Renderer>> { +        if let Some(title_bar) = self.title_bar.as_mut() { +            let mut children = layout.children(); +            let title_bar_layout = children.next()?; + +            let mut states = tree.children.iter_mut(); +            let body_state = states.next().unwrap(); +            let title_bar_state = states.next().unwrap(); + +            match title_bar.overlay(title_bar_state, title_bar_layout, renderer) +            { +                Some(overlay) => Some(overlay), +                None => self.body.as_widget_mut().overlay( +                    body_state, +                    children.next()?, +                    renderer, +                ), +            } +        } else { +            self.body.as_widget_mut().overlay( +                &mut tree.children[0], +                layout, +                renderer, +            ) +        } +    } +} + +impl<'a, Message, Renderer> Draggable for &Content<'a, Message, Renderer> +where +    Renderer: crate::core::Renderer, +    Renderer::Theme: container::StyleSheet, +{ +    fn can_be_dragged_at( +        &self, +        layout: Layout<'_>, +        cursor_position: Point, +    ) -> bool { +        if let Some(title_bar) = &self.title_bar { +            let mut children = layout.children(); +            let title_bar_layout = children.next().unwrap(); + +            title_bar.is_over_pick_area(title_bar_layout, cursor_position) +        } else { +            false +        } +    } +} + +impl<'a, T, Message, Renderer> From<T> for Content<'a, Message, Renderer> +where +    T: Into<Element<'a, Message, Renderer>>, +    Renderer: crate::core::Renderer, +    Renderer::Theme: container::StyleSheet, +{ +    fn from(element: T) -> Self { +        Self::new(element) +    } +} diff --git a/widget/src/pane_grid/direction.rs b/widget/src/pane_grid/direction.rs new file mode 100644 index 00000000..b31a8737 --- /dev/null +++ b/widget/src/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/widget/src/pane_grid/draggable.rs b/widget/src/pane_grid/draggable.rs new file mode 100644 index 00000000..a9274dad --- /dev/null +++ b/widget/src/pane_grid/draggable.rs @@ -0,0 +1,12 @@ +use crate::core::{Layout, Point}; + +/// A pane that can be dragged. +pub trait Draggable { +    /// Returns whether the [`Draggable`] with the given [`Layout`] can be picked +    /// at the provided cursor position. +    fn can_be_dragged_at( +        &self, +        layout: Layout<'_>, +        cursor_position: Point, +    ) -> bool; +} diff --git a/widget/src/pane_grid/node.rs b/widget/src/pane_grid/node.rs new file mode 100644 index 00000000..3976acd8 --- /dev/null +++ b/widget/src/pane_grid/node.rs @@ -0,0 +1,250 @@ +use crate::core::{Rectangle, Size}; +use crate::pane_grid::{Axis, Pane, Split}; + +use std::collections::BTreeMap; + +/// A layout node of a [`PaneGrid`]. +/// +/// [`PaneGrid`]: crate::widget::PaneGrid +#[derive(Debug, Clone)] +pub enum Node { +    /// The region of this [`Node`] is split into two. +    Split { +        /// The [`Split`] of this [`Node`]. +        id: Split, + +        /// The direction of the split. +        axis: Axis, + +        /// The ratio of the split in [0.0, 1.0]. +        ratio: f32, + +        /// The left/top [`Node`] of the split. +        a: Box<Node>, + +        /// The right/bottom [`Node`] of the split. +        b: Box<Node>, +    }, +    /// The region of this [`Node`] is taken by a [`Pane`]. +    Pane(Pane), +} + +impl Node { +    /// Returns an iterator over each [`Split`] in this [`Node`]. +    pub fn splits(&self) -> impl Iterator<Item = &Split> { +        let mut unvisited_nodes = vec![self]; + +        std::iter::from_fn(move || { +            while let Some(node) = unvisited_nodes.pop() { +                if let Node::Split { id, a, b, .. } = node { +                    unvisited_nodes.push(a); +                    unvisited_nodes.push(b); + +                    return Some(id); +                } +            } + +            None +        }) +    } + +    /// Returns the rectangular region for each [`Pane`] in the [`Node`] given +    /// the spacing between panes and the total available space. +    pub fn pane_regions( +        &self, +        spacing: f32, +        size: Size, +    ) -> BTreeMap<Pane, Rectangle> { +        let mut regions = BTreeMap::new(); + +        self.compute_regions( +            spacing, +            &Rectangle { +                x: 0.0, +                y: 0.0, +                width: size.width, +                height: size.height, +            }, +            &mut regions, +        ); + +        regions +    } + +    /// Returns the axis, rectangular region, and ratio for each [`Split`] in +    /// the [`Node`] given the spacing between panes and the total available +    /// space. +    pub fn split_regions( +        &self, +        spacing: f32, +        size: Size, +    ) -> BTreeMap<Split, (Axis, Rectangle, f32)> { +        let mut splits = BTreeMap::new(); + +        self.compute_splits( +            spacing, +            &Rectangle { +                x: 0.0, +                y: 0.0, +                width: size.width, +                height: size.height, +            }, +            &mut splits, +        ); + +        splits +    } + +    pub(crate) 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(crate) fn split(&mut self, id: Split, axis: Axis, new_pane: Pane) { +        *self = Node::Split { +            id, +            axis, +            ratio: 0.5, +            a: Box::new(self.clone()), +            b: Box::new(Node::Pane(new_pane)), +        }; +    } + +    pub(crate) fn update(&mut self, f: &impl Fn(&mut Node)) { +        if let Node::Split { a, b, .. } = self { +            a.update(f); +            b.update(f); +        } + +        f(self); +    } + +    pub(crate) fn resize(&mut self, split: &Split, percentage: f32) -> bool { +        match self { +            Node::Split { +                id, ratio, a, b, .. +            } => { +                if id == split { +                    *ratio = percentage; + +                    true +                } else if a.resize(split, percentage) { +                    true +                } else { +                    b.resize(split, percentage) +                } +            } +            Node::Pane(_) => false, +        } +    } + +    pub(crate) 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, +        } +    } + +    fn pane(&self) -> Option<Pane> { +        match self { +            Node::Split { .. } => None, +            Node::Pane(pane) => Some(*pane), +        } +    } + +    fn first_pane(&self) -> Pane { +        match self { +            Node::Split { a, .. } => a.first_pane(), +            Node::Pane(pane) => *pane, +        } +    } + +    fn compute_regions( +        &self, +        spacing: f32, +        current: &Rectangle, +        regions: &mut BTreeMap<Pane, Rectangle>, +    ) { +        match self { +            Node::Split { +                axis, ratio, a, b, .. +            } => { +                let (region_a, region_b) = axis.split(current, *ratio, spacing); + +                a.compute_regions(spacing, ®ion_a, regions); +                b.compute_regions(spacing, ®ion_b, regions); +            } +            Node::Pane(pane) => { +                let _ = regions.insert(*pane, *current); +            } +        } +    } + +    fn compute_splits( +        &self, +        spacing: f32, +        current: &Rectangle, +        splits: &mut BTreeMap<Split, (Axis, Rectangle, f32)>, +    ) { +        match self { +            Node::Split { +                axis, +                ratio, +                a, +                b, +                id, +            } => { +                let (region_a, region_b) = axis.split(current, *ratio, spacing); + +                let _ = splits.insert(*id, (*axis, *current, *ratio)); + +                a.compute_splits(spacing, ®ion_a, splits); +                b.compute_splits(spacing, ®ion_b, splits); +            } +            Node::Pane(_) => {} +        } +    } +} + +impl std::hash::Hash for Node { +    fn hash<H: std::hash::Hasher>(&self, state: &mut H) { +        match self { +            Node::Split { +                id, +                axis, +                ratio, +                a, +                b, +            } => { +                id.hash(state); +                axis.hash(state); +                ((ratio * 100_000.0) as u32).hash(state); +                a.hash(state); +                b.hash(state); +            } +            Node::Pane(pane) => { +                pane.hash(state); +            } +        } +    } +} diff --git a/widget/src/pane_grid/pane.rs b/widget/src/pane_grid/pane.rs new file mode 100644 index 00000000..d6fbab83 --- /dev/null +++ b/widget/src/pane_grid/pane.rs @@ -0,0 +1,5 @@ +/// A rectangular region in a [`PaneGrid`] used to display widgets. +/// +/// [`PaneGrid`]: crate::widget::PaneGrid +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Pane(pub(super) usize); diff --git a/widget/src/pane_grid/split.rs b/widget/src/pane_grid/split.rs new file mode 100644 index 00000000..8132272a --- /dev/null +++ b/widget/src/pane_grid/split.rs @@ -0,0 +1,5 @@ +/// A divider that splits a region in a [`PaneGrid`] into two different panes. +/// +/// [`PaneGrid`]: crate::widget::PaneGrid +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Split(pub(super) usize); diff --git a/widget/src/pane_grid/state.rs b/widget/src/pane_grid/state.rs new file mode 100644 index 00000000..a6e2ec7f --- /dev/null +++ b/widget/src/pane_grid/state.rs @@ -0,0 +1,348 @@ +//! The state of a [`PaneGrid`]. +//! +//! [`PaneGrid`]: crate::widget::PaneGrid +use crate::core::{Point, Size}; +use crate::pane_grid::{Axis, Configuration, Direction, Node, Pane, Split}; + +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`]: crate::widget::PaneGrid +/// [`PaneGrid::new`]: crate::widget::PaneGrid::new +#[derive(Debug, Clone)] +pub struct State<T> { +    /// The panes of the [`PaneGrid`]. +    /// +    /// [`PaneGrid`]: crate::widget::PaneGrid +    pub panes: HashMap<Pane, T>, + +    /// The internal state of the [`PaneGrid`]. +    /// +    /// [`PaneGrid`]: crate::widget::PaneGrid +    pub internal: Internal, + +    /// The maximized [`Pane`] of the [`PaneGrid`]. +    /// +    /// [`PaneGrid`]: crate::widget::PaneGrid +    pub(super) maximized: Option<Pane>, +} + +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. +    pub fn new(first_pane_state: T) -> (Self, Pane) { +        ( +            Self::with_configuration(Configuration::Pane(first_pane_state)), +            Pane(0), +        ) +    } + +    /// Creates a new [`State`] with the given [`Configuration`]. +    pub fn with_configuration(config: impl Into<Configuration<T>>) -> Self { +        let mut panes = HashMap::new(); + +        let internal = +            Internal::from_configuration(&mut panes, config.into(), 0); + +        State { +            panes, +            internal, +            maximized: None, +        } +    } + +    /// Returns the total amount of panes in the [`State`]. +    pub fn len(&self) -> usize { +        self.panes.len() +    } + +    /// Returns `true` if the amount of panes in the [`State`] is 0. +    pub fn is_empty(&self) -> bool { +        self.len() == 0 +    } + +    /// Returns the internal state of the given [`Pane`], if it exists. +    pub fn get(&self, pane: &Pane) -> Option<&T> { +        self.panes.get(pane) +    } + +    /// Returns the internal state of the given [`Pane`] with mutability, if it +    /// exists. +    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. +    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. +    pub fn iter_mut(&mut self) -> impl Iterator<Item = (&Pane, &mut T)> { +        self.panes.iter_mut() +    } + +    /// Returns the layout of the [`State`]. +    pub fn layout(&self) -> &Node { +        &self.internal.layout +    } + +    /// Returns the adjacent [`Pane`] of another [`Pane`] in the given +    /// direction, if there is one. +    pub fn adjacent(&self, pane: &Pane, direction: Direction) -> Option<Pane> { +        let regions = self +            .internal +            .layout +            .pane_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) +    } + +    /// Splits the given [`Pane`] into two in the given [`Axis`] and +    /// initializing the new [`Pane`] with the provided internal state. +    pub fn split( +        &mut self, +        axis: Axis, +        pane: &Pane, +        state: T, +    ) -> Option<(Pane, Split)> { +        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); +        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 +    /// will need to call this method when handling a [`DragEvent`]. +    /// +    /// [`PaneGrid`]: crate::widget::PaneGrid +    /// [`DragEvent`]: crate::widget::pane_grid::DragEvent +    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`]. +    /// +    /// [`PaneGrid`]: crate::widget::PaneGrid +    /// [`ResizeEvent`]: crate::widget::pane_grid::ResizeEvent +    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 and its closest +    /// sibling, if it exists. +    pub fn close(&mut self, pane: &Pane) -> Option<(T, Pane)> { +        if self.maximized == Some(*pane) { +            let _ = self.maximized.take(); +        } + +        if let Some(sibling) = self.internal.layout.remove(pane) { +            self.panes.remove(pane).map(|state| (state, sibling)) +        } else { +            None +        } +    } + +    /// Maximize the given [`Pane`]. Only this pane will be rendered by the +    /// [`PaneGrid`] until [`Self::restore()`] is called. +    /// +    /// [`PaneGrid`]: crate::widget::PaneGrid +    pub fn maximize(&mut self, pane: &Pane) { +        self.maximized = Some(*pane); +    } + +    /// Restore the currently maximized [`Pane`] to it's normal size. All panes +    /// will be rendered by the [`PaneGrid`]. +    /// +    /// [`PaneGrid`]: crate::widget::PaneGrid +    pub fn restore(&mut self) { +        let _ = self.maximized.take(); +    } + +    /// Returns the maximized [`Pane`] of the [`PaneGrid`]. +    /// +    /// [`PaneGrid`]: crate::widget::PaneGrid +    pub fn maximized(&self) -> Option<Pane> { +        self.maximized +    } +} + +/// The internal state of a [`PaneGrid`]. +/// +/// [`PaneGrid`]: crate::widget::PaneGrid +#[derive(Debug, Clone)] +pub struct Internal { +    layout: Node, +    last_id: usize, +} + +impl Internal { +    /// Initializes the [`Internal`] state of a [`PaneGrid`] from a +    /// [`Configuration`]. +    /// +    /// [`PaneGrid`]: crate::widget::PaneGrid +    pub fn from_configuration<T>( +        panes: &mut HashMap<Pane, T>, +        content: Configuration<T>, +        next_id: usize, +    ) -> Self { +        let (layout, last_id) = match content { +            Configuration::Split { axis, ratio, a, b } => { +                let Internal { +                    layout: a, +                    last_id: next_id, +                    .. +                } = Self::from_configuration(panes, *a, next_id); + +                let Internal { +                    layout: b, +                    last_id: next_id, +                    .. +                } = Self::from_configuration(panes, *b, next_id); + +                ( +                    Node::Split { +                        id: Split(next_id), +                        axis, +                        ratio, +                        a: Box::new(a), +                        b: Box::new(b), +                    }, +                    next_id + 1, +                ) +            } +            Configuration::Pane(state) => { +                let id = Pane(next_id); +                let _ = panes.insert(id, state); + +                (Node::Pane(id), next_id + 1) +            } +        }; + +        Self { layout, last_id } +    } +} + +/// The current action of a [`PaneGrid`]. +/// +/// [`PaneGrid`]: crate::widget::PaneGrid +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Action { +    /// The [`PaneGrid`] is idle. +    /// +    /// [`PaneGrid`]: crate::widget::PaneGrid +    Idle, +    /// A [`Pane`] in the [`PaneGrid`] is being dragged. +    /// +    /// [`PaneGrid`]: crate::widget::PaneGrid +    Dragging { +        /// The [`Pane`] being dragged. +        pane: Pane, +        /// The starting [`Point`] of the drag interaction. +        origin: Point, +    }, +    /// A [`Split`] in the [`PaneGrid`] is being dragged. +    /// +    /// [`PaneGrid`]: crate::widget::PaneGrid +    Resizing { +        /// The [`Split`] being dragged. +        split: Split, +        /// The [`Axis`] of the [`Split`]. +        axis: Axis, +    }, +} + +impl Action { +    /// Returns the current [`Pane`] that is being dragged, if any. +    pub fn picked_pane(&self) -> Option<(Pane, Point)> { +        match *self { +            Action::Dragging { pane, origin, .. } => Some((pane, origin)), +            _ => None, +        } +    } + +    /// Returns the current [`Split`] that is being dragged, if any. +    pub fn picked_split(&self) -> Option<(Split, Axis)> { +        match *self { +            Action::Resizing { split, axis, .. } => Some((split, axis)), +            _ => None, +        } +    } +} + +impl Internal { +    /// The layout [`Node`] of the [`Internal`] state +    pub fn layout(&self) -> &Node { +        &self.layout +    } +} diff --git a/widget/src/pane_grid/title_bar.rs b/widget/src/pane_grid/title_bar.rs new file mode 100644 index 00000000..2129937b --- /dev/null +++ b/widget/src/pane_grid/title_bar.rs @@ -0,0 +1,432 @@ +use crate::container; +use crate::core::event::{self, Event}; +use crate::core::layout; +use crate::core::mouse; +use crate::core::overlay; +use crate::core::renderer; +use crate::core::widget::{self, Tree}; +use crate::core::{ +    Clipboard, Element, Layout, Padding, Point, Rectangle, Shell, Size, +}; + +/// The title bar of a [`Pane`]. +/// +/// [`Pane`]: crate::widget::pane_grid::Pane +#[allow(missing_debug_implementations)] +pub struct TitleBar<'a, Message, Renderer = crate::Renderer> +where +    Renderer: crate::core::Renderer, +    Renderer::Theme: container::StyleSheet, +{ +    content: Element<'a, Message, Renderer>, +    controls: Option<Element<'a, Message, Renderer>>, +    padding: Padding, +    always_show_controls: bool, +    style: <Renderer::Theme as container::StyleSheet>::Style, +} + +impl<'a, Message, Renderer> TitleBar<'a, Message, Renderer> +where +    Renderer: crate::core::Renderer, +    Renderer::Theme: container::StyleSheet, +{ +    /// Creates a new [`TitleBar`] with the given content. +    pub fn new<E>(content: E) -> Self +    where +        E: Into<Element<'a, Message, Renderer>>, +    { +        Self { +            content: content.into(), +            controls: None, +            padding: Padding::ZERO, +            always_show_controls: false, +            style: Default::default(), +        } +    } + +    /// Sets the controls of the [`TitleBar`]. +    pub fn controls( +        mut self, +        controls: impl Into<Element<'a, Message, Renderer>>, +    ) -> Self { +        self.controls = Some(controls.into()); +        self +    } + +    /// Sets the [`Padding`] of the [`TitleBar`]. +    pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self { +        self.padding = padding.into(); +        self +    } + +    /// Sets the style of the [`TitleBar`]. +    pub fn style( +        mut self, +        style: impl Into<<Renderer::Theme as container::StyleSheet>::Style>, +    ) -> Self { +        self.style = style.into(); +        self +    } + +    /// Sets whether or not the [`controls`] attached to this [`TitleBar`] are +    /// always visible. +    /// +    /// By default, the controls are only visible when the [`Pane`] of this +    /// [`TitleBar`] is hovered. +    /// +    /// [`controls`]: Self::controls +    /// [`Pane`]: crate::widget::pane_grid::Pane +    pub fn always_show_controls(mut self) -> Self { +        self.always_show_controls = true; +        self +    } +} + +impl<'a, Message, Renderer> TitleBar<'a, Message, Renderer> +where +    Renderer: crate::core::Renderer, +    Renderer::Theme: container::StyleSheet, +{ +    pub(super) fn state(&self) -> Tree { +        let children = if let Some(controls) = self.controls.as_ref() { +            vec![Tree::new(&self.content), Tree::new(controls)] +        } else { +            vec![Tree::new(&self.content), Tree::empty()] +        }; + +        Tree { +            children, +            ..Tree::empty() +        } +    } + +    pub(super) fn diff(&self, tree: &mut Tree) { +        if tree.children.len() == 2 { +            if let Some(controls) = self.controls.as_ref() { +                tree.children[1].diff(controls); +            } + +            tree.children[0].diff(&self.content); +        } else { +            *tree = self.state(); +        } +    } + +    /// Draws the [`TitleBar`] with the provided [`Renderer`] and [`Layout`]. +    /// +    /// [`Renderer`]: crate::Renderer +    pub fn draw( +        &self, +        tree: &Tree, +        renderer: &mut Renderer, +        theme: &Renderer::Theme, +        inherited_style: &renderer::Style, +        layout: Layout<'_>, +        cursor_position: Point, +        viewport: &Rectangle, +        show_controls: bool, +    ) { +        use container::StyleSheet; + +        let bounds = layout.bounds(); +        let style = theme.appearance(&self.style); +        let inherited_style = renderer::Style { +            text_color: style.text_color.unwrap_or(inherited_style.text_color), +        }; + +        container::draw_background(renderer, &style, bounds); + +        let mut children = layout.children(); +        let padded = children.next().unwrap(); + +        let mut children = padded.children(); +        let title_layout = children.next().unwrap(); +        let mut show_title = true; + +        if let Some(controls) = &self.controls { +            if show_controls || self.always_show_controls { +                let controls_layout = children.next().unwrap(); +                if title_layout.bounds().width + controls_layout.bounds().width +                    > padded.bounds().width +                { +                    show_title = false; +                } + +                controls.as_widget().draw( +                    &tree.children[1], +                    renderer, +                    theme, +                    &inherited_style, +                    controls_layout, +                    cursor_position, +                    viewport, +                ); +            } +        } + +        if show_title { +            self.content.as_widget().draw( +                &tree.children[0], +                renderer, +                theme, +                &inherited_style, +                title_layout, +                cursor_position, +                viewport, +            ); +        } +    } + +    /// Returns whether the mouse cursor is over the pick area of the +    /// [`TitleBar`] or not. +    /// +    /// The whole [`TitleBar`] is a pick area, except its controls. +    pub fn is_over_pick_area( +        &self, +        layout: Layout<'_>, +        cursor_position: Point, +    ) -> bool { +        if layout.bounds().contains(cursor_position) { +            let mut children = layout.children(); +            let padded = children.next().unwrap(); +            let mut children = padded.children(); +            let title_layout = children.next().unwrap(); + +            if self.controls.is_some() { +                let controls_layout = children.next().unwrap(); + +                if title_layout.bounds().width + controls_layout.bounds().width +                    > padded.bounds().width +                { +                    !controls_layout.bounds().contains(cursor_position) +                } else { +                    !controls_layout.bounds().contains(cursor_position) +                        && !title_layout.bounds().contains(cursor_position) +                } +            } else { +                !title_layout.bounds().contains(cursor_position) +            } +        } else { +            false +        } +    } + +    pub(crate) fn layout( +        &self, +        renderer: &Renderer, +        limits: &layout::Limits, +    ) -> layout::Node { +        let limits = limits.pad(self.padding); +        let max_size = limits.max(); + +        let title_layout = self +            .content +            .as_widget() +            .layout(renderer, &layout::Limits::new(Size::ZERO, max_size)); + +        let title_size = title_layout.size(); + +        let mut node = if let Some(controls) = &self.controls { +            let mut controls_layout = controls +                .as_widget() +                .layout(renderer, &layout::Limits::new(Size::ZERO, max_size)); + +            let controls_size = controls_layout.size(); +            let space_before_controls = max_size.width - controls_size.width; + +            let height = title_size.height.max(controls_size.height); + +            controls_layout.move_to(Point::new(space_before_controls, 0.0)); + +            layout::Node::with_children( +                Size::new(max_size.width, height), +                vec![title_layout, controls_layout], +            ) +        } else { +            layout::Node::with_children( +                Size::new(max_size.width, title_size.height), +                vec![title_layout], +            ) +        }; + +        node.move_to(Point::new(self.padding.left, self.padding.top)); + +        layout::Node::with_children(node.size().pad(self.padding), vec![node]) +    } + +    pub(crate) fn operate( +        &self, +        tree: &mut Tree, +        layout: Layout<'_>, +        renderer: &Renderer, +        operation: &mut dyn widget::Operation<Message>, +    ) { +        let mut children = layout.children(); +        let padded = children.next().unwrap(); + +        let mut children = padded.children(); +        let title_layout = children.next().unwrap(); +        let mut show_title = true; + +        if let Some(controls) = &self.controls { +            let controls_layout = children.next().unwrap(); + +            if title_layout.bounds().width + controls_layout.bounds().width +                > padded.bounds().width +            { +                show_title = false; +            } + +            controls.as_widget().operate( +                &mut tree.children[1], +                controls_layout, +                renderer, +                operation, +            ) +        }; + +        if show_title { +            self.content.as_widget().operate( +                &mut tree.children[0], +                title_layout, +                renderer, +                operation, +            ) +        } +    } + +    pub(crate) fn on_event( +        &mut self, +        tree: &mut Tree, +        event: Event, +        layout: Layout<'_>, +        cursor_position: Point, +        renderer: &Renderer, +        clipboard: &mut dyn Clipboard, +        shell: &mut Shell<'_, Message>, +    ) -> event::Status { +        let mut children = layout.children(); +        let padded = children.next().unwrap(); + +        let mut children = padded.children(); +        let title_layout = children.next().unwrap(); +        let mut show_title = true; + +        let control_status = if let Some(controls) = &mut self.controls { +            let controls_layout = children.next().unwrap(); +            if title_layout.bounds().width + controls_layout.bounds().width +                > padded.bounds().width +            { +                show_title = false; +            } + +            controls.as_widget_mut().on_event( +                &mut tree.children[1], +                event.clone(), +                controls_layout, +                cursor_position, +                renderer, +                clipboard, +                shell, +            ) +        } else { +            event::Status::Ignored +        }; + +        let title_status = if show_title { +            self.content.as_widget_mut().on_event( +                &mut tree.children[0], +                event, +                title_layout, +                cursor_position, +                renderer, +                clipboard, +                shell, +            ) +        } else { +            event::Status::Ignored +        }; + +        control_status.merge(title_status) +    } + +    pub(crate) fn mouse_interaction( +        &self, +        tree: &Tree, +        layout: Layout<'_>, +        cursor_position: Point, +        viewport: &Rectangle, +        renderer: &Renderer, +    ) -> mouse::Interaction { +        let mut children = layout.children(); +        let padded = children.next().unwrap(); + +        let mut children = padded.children(); +        let title_layout = children.next().unwrap(); + +        let title_interaction = self.content.as_widget().mouse_interaction( +            &tree.children[0], +            title_layout, +            cursor_position, +            viewport, +            renderer, +        ); + +        if let Some(controls) = &self.controls { +            let controls_layout = children.next().unwrap(); +            let controls_interaction = controls.as_widget().mouse_interaction( +                &tree.children[1], +                controls_layout, +                cursor_position, +                viewport, +                renderer, +            ); + +            if title_layout.bounds().width + controls_layout.bounds().width +                > padded.bounds().width +            { +                controls_interaction +            } else { +                controls_interaction.max(title_interaction) +            } +        } else { +            title_interaction +        } +    } + +    pub(crate) fn overlay<'b>( +        &'b mut self, +        tree: &'b mut Tree, +        layout: Layout<'_>, +        renderer: &Renderer, +    ) -> Option<overlay::Element<'b, Message, Renderer>> { +        let mut children = layout.children(); +        let padded = children.next()?; + +        let mut children = padded.children(); +        let title_layout = children.next()?; + +        let Self { +            content, controls, .. +        } = self; + +        let mut states = tree.children.iter_mut(); +        let title_state = states.next().unwrap(); +        let controls_state = states.next().unwrap(); + +        content +            .as_widget_mut() +            .overlay(title_state, title_layout, renderer) +            .or_else(move || { +                controls.as_mut().and_then(|controls| { +                    let controls_layout = children.next()?; + +                    controls.as_widget_mut().overlay( +                        controls_state, +                        controls_layout, +                        renderer, +                    ) +                }) +            }) +    } +}  | 
