From 3127de3296ecd4ecf42415454607f3d4b18fc21d Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Thu, 12 Oct 2023 13:23:21 +0200 Subject: [PATCH] feat(widget): add `ContextDrawer` widget --- src/widget/context_drawer/mod.rs | 22 +++ src/widget/context_drawer/overlay.rs | 119 +++++++++++++ src/widget/context_drawer/widget.rs | 239 +++++++++++++++++++++++++++ src/widget/mod.rs | 3 + 4 files changed, 383 insertions(+) create mode 100644 src/widget/context_drawer/mod.rs create mode 100644 src/widget/context_drawer/overlay.rs create mode 100644 src/widget/context_drawer/widget.rs diff --git a/src/widget/context_drawer/mod.rs b/src/widget/context_drawer/mod.rs new file mode 100644 index 00000000..3a3982fd --- /dev/null +++ b/src/widget/context_drawer/mod.rs @@ -0,0 +1,22 @@ +// Copyright 2023 System76 +// SPDX-License-Identifier: MPL-2.0 + +mod overlay; + +mod widget; +pub use widget::ContextDrawer; + +use crate::Element; + +pub fn context_drawer<'a, Message: Clone + 'static, Content, Drawer>( + header: &'a str, + on_close: Message, + content: Content, + drawer: Drawer, +) -> ContextDrawer<'a, Message> +where + Content: Into>, + Drawer: Into>, +{ + ContextDrawer::new(header, content, drawer, on_close) +} diff --git a/src/widget/context_drawer/overlay.rs b/src/widget/context_drawer/overlay.rs new file mode 100644 index 00000000..3c84cde9 --- /dev/null +++ b/src/widget/context_drawer/overlay.rs @@ -0,0 +1,119 @@ +// Copyright 2023 System76 +// SPDX-License-Identifier: MPL-2.0 + +use crate::Element; + +use iced::advanced::layout::{self, Layout}; +use iced::advanced::widget::{self, Operation, OperationOutputWrapper}; +use iced::advanced::{overlay, renderer}; +use iced::advanced::{Clipboard, Shell}; +use iced::{event, mouse, Event, Point, Rectangle, Size}; +use iced_core::Widget; + +pub(super) struct Overlay<'a, 'b, Message> { + pub(super) content: &'b mut Element<'a, Message>, + pub(super) tree: &'b mut widget::Tree, + pub(super) width: f32, +} + +impl<'a, 'b, Message> overlay::Overlay for Overlay<'a, 'b, Message> +where + Message: Clone, +{ + fn layout(&self, renderer: &crate::Renderer, bounds: Size, position: Point) -> layout::Node { + let limits = layout::Limits::new(Size::ZERO, bounds) + .width(self.width) + .height(bounds.height - 8.0 - position.y); + + let mut node = self.content.as_widget().layout(renderer, &limits); + let node_size = node.size(); + + node.move_to(Point { + x: if bounds.width > node_size.width - 8.0 { + bounds.width - node_size.width - 8.0 + } else { + 0.0 + }, + y: if bounds.height > node_size.height - 8.0 { + bounds.height - node_size.height - 8.0 + } else { + 0.0 + }, + }); + + node + } + + fn on_event( + &mut self, + event: Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &crate::Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + self.content.as_widget_mut().on_event( + self.tree, + event, + layout, + cursor, + renderer, + clipboard, + shell, + &layout.bounds(), + ) + } + + fn draw( + &self, + renderer: &mut crate::Renderer, + theme: &crate::Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + ) { + self.content.draw( + self.tree, + renderer, + theme, + style, + layout, + cursor, + &layout.bounds(), + ); + } + + fn operate( + &mut self, + layout: Layout<'_>, + renderer: &crate::Renderer, + operation: &mut dyn Operation>, + ) { + self.content + .as_widget_mut() + .operate(self.tree, layout, renderer, operation); + } + + fn mouse_interaction( + &self, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + renderer: &crate::Renderer, + ) -> mouse::Interaction { + self.content + .as_widget() + .mouse_interaction(self.tree, layout, cursor, viewport, renderer) + } + + fn overlay<'c>( + &'c mut self, + layout: Layout<'_>, + renderer: &crate::Renderer, + ) -> Option> { + self.content + .as_widget_mut() + .overlay(self.tree, layout, renderer) + } +} diff --git a/src/widget/context_drawer/widget.rs b/src/widget/context_drawer/widget.rs new file mode 100644 index 00000000..3be9b977 --- /dev/null +++ b/src/widget/context_drawer/widget.rs @@ -0,0 +1,239 @@ +// Copyright 2023 System76 +// SPDX-License-Identifier: MPL-2.0 + +use crate::widget::{ + button, column, container, icon, row, scrollable, text, LayerContainer, Space, +}; +use crate::{Apply, Element, Renderer, Theme}; + +use super::overlay::Overlay; + +use iced_core::alignment; +use iced_core::event::{self, Event}; +use iced_core::widget::{Operation, Tree}; +use iced_core::{ + layout, mouse, overlay as iced_overlay, renderer, Clipboard, Color, Layout, Length, Padding, + Rectangle, Shell, Widget, +}; + +use iced_renderer::core::widget::OperationOutputWrapper; +pub use iced_style::container::{Appearance, StyleSheet}; + +#[must_use] +pub struct ContextDrawer<'a, Message> { + content: Element<'a, Message>, + drawer: Element<'a, Message>, + on_close: Option, +} + +impl<'a, Message: Clone + 'static> ContextDrawer<'a, Message> { + /// Creates an empty [`ContextDrawer`]. + pub fn new( + header: &'a str, + content: Content, + drawer: Drawer, + on_close: Message, + ) -> Self + where + Content: Into>, + Drawer: Into>, + { + let header = row::with_capacity(3) + .height(Length::Fixed(80.0)) + .width(Length::Fixed(480.0)) + .padding(Padding { + top: 0.0, + bottom: 0.0, + left: 32.0, + right: 32.0, + }) + .push(Space::new(Length::FillPortion(1), Length::Shrink)) + .push( + text::heading(header) + .width(Length::FillPortion(1)) + .height(Length::Fill) + .horizontal_alignment(alignment::Horizontal::Center) + .vertical_alignment(alignment::Vertical::Center), + ) + .push( + button::text("Close") + .trailing_icon(icon::from_name("go-next-symbolic")) + .on_press(on_close) + .apply(container) + .width(Length::FillPortion(1)) + .height(Length::Fill) + .align_x(alignment::Horizontal::Right) + .center_y(), + ); + + let pane = column::with_capacity(2).push(header).push(scrollable( + container(drawer.into()).padding(Padding { + top: 0.0, + left: 32.0, + right: 32.0, + bottom: 32.0, + }), + )); + + ContextDrawer { + content: content.into(), + drawer: LayerContainer::new(pane) + .style(crate::style::Container::custom(move |theme| { + let palette = theme.cosmic(); + + container::Appearance { + icon_color: Some(Color::from(palette.primary.on)), + text_color: Some(Color::from(palette.primary.on)), + background: Some(iced::Background::Color(palette.primary.base.into())), + border_radius: 8.0.into(), + border_width: 0.0, + border_color: Color::TRANSPARENT, + } + })) + .layer(cosmic_theme::Layer::Primary) + .width(Length::Fill) + .height(Length::Fill) + .max_width(480.0) + .into(), + on_close: None, + } + } + + // Optionally assigns message to `on_close` event. + pub fn on_close_maybe(mut self, message: Option) -> Self { + self.on_close = message; + self + } +} + +impl<'a, Message: Clone> Widget for ContextDrawer<'a, Message> { + fn children(&self) -> Vec { + vec![Tree::new(&self.content), Tree::new(&self.drawer)] + } + + fn diff(&mut self, tree: &mut Tree) { + tree.diff_children(&mut [&mut self.content, &mut self.drawer]); + } + + fn width(&self) -> Length { + self.content.as_widget().width() + } + + fn height(&self) -> Length { + self.content.as_widget().height() + } + + fn layout(&self, renderer: &Renderer, limits: &layout::Limits) -> layout::Node { + self.content.as_widget().layout(renderer, limits) + } + + fn operate( + &self, + tree: &mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn Operation>, + ) { + self.content + .as_widget() + .operate(&mut tree.children[0], layout, renderer, operation); + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + viewport: &Rectangle, + ) -> event::Status { + self.content.as_widget_mut().on_event( + &mut tree.children[0], + event, + layout, + cursor, + renderer, + clipboard, + shell, + viewport, + ) + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + self.content.as_widget().mouse_interaction( + &tree.children[0], + layout, + cursor, + viewport, + renderer, + ) + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Theme, + renderer_style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + ) { + self.content.as_widget().draw( + &tree.children[0], + renderer, + theme, + renderer_style, + layout, + cursor, + viewport, + ); + } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut Tree, + layout: Layout<'_>, + _renderer: &Renderer, + ) -> Option> { + let bounds = layout.bounds(); + + Some(iced_overlay::Element::new( + layout.position(), + Box::new(Overlay { + content: &mut self.drawer, + tree: &mut tree.children[1], + width: bounds.width, + }), + )) + } + + #[cfg(feature = "a11y")] + /// get the a11y nodes for the widget + fn a11y_nodes( + &self, + layout: Layout<'_>, + state: &Tree, + p: mouse::Cursor, + ) -> iced_accessibility::A11yTree { + let c_layout = layout.children().next().unwrap(); + let c_state = &state.children[0]; + self.content.as_widget().a11y_nodes(c_layout, c_state, p) + } +} + +impl<'a, Message: 'a + Clone> From> for Element<'a, Message> { + fn from(widget: ContextDrawer<'a, Message>) -> Element<'a, Message> { + Element::new(widget) + } +} diff --git a/src/widget/mod.rs b/src/widget/mod.rs index 668b27d7..2550f7ec 100644 --- a/src/widget/mod.rs +++ b/src/widget/mod.rs @@ -31,6 +31,9 @@ pub use card::*; pub mod color_picker; pub use color_picker::{ColorPicker, ColorPickerModel}; +pub mod context_drawer; +pub use context_drawer::{context_drawer, ContextDrawer}; + pub use column::{column, Column}; pub mod column { pub type Column<'a, Message> = iced::widget::Column<'a, Message, crate::Renderer>;