From e1d1b0bad5c37cf380512364971e82110eda730b Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Mon, 2 Oct 2023 16:46:16 +0200 Subject: [PATCH] feat(widget): initial implementation of Grid widget --- Cargo.toml | 3 + src/widget/grid/layout.rs | 162 +++++++++++++++++++ src/widget/grid/mod.rs | 12 ++ src/widget/grid/widget.rs | 329 ++++++++++++++++++++++++++++++++++++++ src/widget/mod.rs | 3 + 5 files changed, 509 insertions(+) create mode 100644 src/widget/grid/layout.rs create mode 100644 src/widget/grid/mod.rs create mode 100644 src/widget/grid/widget.rs diff --git a/Cargo.toml b/Cargo.toml index 6694556bb5a..a04d4d5f7e7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -116,6 +116,9 @@ optional = true version = "0.8" optional = true +[dependencies.taffy] +git = "https://github.com/DioxusLabs/taffy" +features = ["grid"] [workspace] members = [ diff --git a/src/widget/grid/layout.rs b/src/widget/grid/layout.rs new file mode 100644 index 00000000000..5227d3f3359 --- /dev/null +++ b/src/widget/grid/layout.rs @@ -0,0 +1,162 @@ +// Copyright 2023 System76 +// SPDX-License-Identifier: MPL-2.0 + +use super::widget::Assignment; +use crate::{Element, Renderer}; +use iced_core::layout::{Limits, Node}; +use iced_core::{Alignment, Length, Padding, Point, Size}; + +use taffy::geometry::{Line, Rect}; +use taffy::style::{AlignContent, AlignItems, Display, GridPlacement, Style}; +use taffy::style_helpers::{auto, length}; +use taffy::Taffy; + +#[allow(clippy::too_many_lines)] +pub fn resolve( + renderer: &Renderer, + limits: &Limits, + items: &[Element<'_, Message>], + assignments: &[Assignment], + width: Length, + height: Length, + padding: Padding, + column_alignment: Alignment, + row_alignment: Alignment, + column_spacing: f32, + row_spacing: f32, +) -> Node { + let max_size = limits.max(); + + let mut leafs = Vec::with_capacity(items.len()); + let mut nodes = Vec::with_capacity(items.len()); + + let mut taffy = Taffy::with_capacity(items.len() + 1); + + // Attach widgets as child nodes. + for (child, assignment) in items.iter().zip(assignments.iter()) { + // Calculate the dimensdrawions of the item. + let child_node = child.as_widget().layout(renderer, &limits); + let size = child_node.size(); + + nodes.push(child_node); + + // Attach widget as leaf to be later assigned to grid. + let leaf = taffy.new_leaf(Style { + grid_column: Line { + start: GridPlacement::Line((assignment.column as i16).into()), + end: GridPlacement::Span(assignment.height.into()), + }, + grid_row: Line { + start: GridPlacement::Line((assignment.row as i16).into()), + end: GridPlacement::Span(assignment.width.into()), + }, + size: taffy::geometry::Size { + width: length(size.width), + height: length(size.height), + }, + ..Style::default() + }); + + match leaf { + Ok(leaf) => leafs.push(leaf), + Err(why) => { + tracing::error!(%why, "cannot add leaf node to grid"); + continue; + } + } + } + + let root = taffy.new_with_children( + Style { + align_items: Some(match width { + Length::Fill | Length::FillPortion(_) => AlignItems::Stretch, + _ => match row_alignment { + Alignment::Start => AlignItems::Start, + Alignment::Center => AlignItems::Center, + Alignment::End => AlignItems::End, + }, + }), + + display: Display::Grid, + + gap: taffy::geometry::Size { + width: length(row_spacing), + height: length(column_spacing), + }, + + justify_content: Some(match height { + Length::Fill | Length::FillPortion(_) => AlignContent::Stretch, + _ => match column_alignment { + Alignment::Start => AlignContent::Start, + Alignment::Center => AlignContent::Center, + Alignment::End => AlignContent::End, + }, + }), + + padding: Rect { + left: length(padding.left), + right: length(padding.right), + top: length(padding.top), + bottom: length(padding.bottom), + }, + + size: taffy::geometry::Size { + width: match width { + Length::Fixed(fixed) => length(fixed), + _ => auto(), + }, + height: match height { + Length::Fixed(fixed) => length(fixed), + _ => auto(), + }, + }, + + ..Style::default() + }, + &leafs, + ); + + let root = match root { + Ok(root) => root, + Err(why) => { + tracing::error!(%why, "grid root style invalid"); + return Node::new(Size::ZERO); + } + }; + + if let Err(why) = taffy.compute_layout( + root, + taffy::geometry::Size { + width: length(max_size.width), + height: length(max_size.height), + }, + ) { + tracing::error!(%why, "grid layout did not compute"); + return Node::new(Size::ZERO); + } + + let grid_layout = match taffy.layout(root) { + Ok(layout) => layout, + Err(why) => { + tracing::error!(%why, "cannot get layout of grid"); + return Node::new(Size::ZERO); + } + }; + + for (leaf, node) in leafs.into_iter().zip(nodes.iter_mut()) { + if let Ok(leaf_layout) = taffy.layout(leaf) { + let location = leaf_layout.location; + node.move_to(Point { + x: location.x, + y: location.y, + }); + } + } + + let grid_size = Size { + width: grid_layout.size.width, + height: grid_layout.size.height, + }; + + Node::with_children(grid_size.pad(padding), nodes) +} diff --git a/src/widget/grid/mod.rs b/src/widget/grid/mod.rs new file mode 100644 index 00000000000..4a2daf08e3c --- /dev/null +++ b/src/widget/grid/mod.rs @@ -0,0 +1,12 @@ +// Copyright 2023 System76 +// SPDX-License-Identifier: MPL-2.0 + +pub mod layout; +pub mod widget; + +pub use widget::{Grid, Item}; + +/// Responsively generates rows and columns of widgets based on its dimmensions. +pub const fn grid<'a, Message>() -> Grid<'a, Message> { + Grid::new() +} diff --git a/src/widget/grid/widget.rs b/src/widget/grid/widget.rs new file mode 100644 index 00000000000..971675faafb --- /dev/null +++ b/src/widget/grid/widget.rs @@ -0,0 +1,329 @@ +// Copyright 2023 System76 +// SPDX-License-Identifier: MPL-2.0 + +use crate::{Element, Renderer}; +use derive_setters::Setters; +use iced_core::event::{self, Event}; +use iced_core::widget::{Operation, Tree}; +use iced_core::{ + layout, mouse, overlay, renderer, Alignment, Clipboard, Layout, Length, Padding, Rectangle, + Shell, Widget, +}; +use iced_renderer::core::widget::OperationOutputWrapper; + +/// Responsively generates rows and columns of widgets based on its dimmensions. +#[must_use] +#[derive(Setters)] +pub struct Grid<'a, Message> { + #[setters(skip)] + children: Vec>, + /// Where children shall be assigned in the grid. + #[setters(skip)] + assignments: Vec, + /// Sets the padding around the widget. + padding: Padding, + /// Alignment across columns + column_alignment: Alignment, + /// Alignment across rows + row_alignment: Alignment, + /// Sets the space between each column of items. + column_spacing: u16, + /// Sets the space between each item in a row. + row_spacing: u16, + /// Sets the width of the grid. + width: Length, + /// Sets the height of the grid. + height: Length, + /// Sets the max width + max_width: f32, + #[setters(skip)] + column: u16, + #[setters(skip)] + row: u16, +} + +impl<'a, Message> Grid<'a, Message> { + pub const fn new() -> Self { + Self { + children: Vec::new(), + assignments: Vec::new(), + padding: Padding::ZERO, + column_alignment: Alignment::Start, + row_alignment: Alignment::Start, + column_spacing: 4, + row_spacing: 4, + width: Length::Shrink, + height: Length::Shrink, + max_width: f32::INFINITY, + column: 1, + row: 1, + } + } + + /// Attach a new element with a given grid assignment. + pub fn push(mut self, widget: impl Into>) -> Self { + self.children.push(widget.into()); + + self.assignments.push(Assignment { + column: self.column, + row: self.row, + width: 1, + height: 1, + }); + + self.column += 1; + + self + } + + /// Attach a new element with custom properties + pub fn push_with(mut self, widget: W, setup: S) -> Self + where + W: Into>, + S: Fn(Assignment) -> Assignment, + { + self.children.push(widget.into()); + + self.assignments.push(setup(Assignment { + column: self.column, + row: self.row, + width: 1, + height: 1, + })); + + self.column += 1; + + self + } + + pub fn insert_row(mut self) -> Self { + self.row += 1; + self.column = 1; + self + } +} + +impl<'a, Message: 'static + Clone> Widget for Grid<'a, Message> { + fn children(&self) -> Vec { + self.children.iter().map(Tree::new).collect() + } + + fn diff(&mut self, tree: &mut Tree) { + tree.diff_children(self.children.as_mut_slice()); + } + + 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 + .max_width(self.max_width) + .width(self.width()) + .height(self.height()); + + super::layout::resolve( + renderer, + &limits, + &self.children, + &self.assignments, + self.width, + self.height, + self.padding, + self.column_alignment, + self.row_alignment, + f32::from(self.column_spacing), + f32::from(self.row_spacing), + ) + } + + fn operate( + &self, + tree: &mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn Operation>, + ) { + operation.container(None, layout.bounds(), &mut |operation| { + self.children + .iter() + .zip(&mut tree.children) + .zip(layout.children()) + .for_each(|((child, state), layout)| { + child + .as_widget() + .operate(state, 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.children + .iter_mut() + .zip(&mut tree.children) + .zip(layout.children()) + .map(|((child, state), layout)| { + child.as_widget_mut().on_event( + state, + event.clone(), + layout, + cursor, + renderer, + clipboard, + shell, + viewport, + ) + }) + .fold(event::Status::Ignored, event::Status::merge) + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + self.children + .iter() + .zip(&tree.children) + .zip(layout.children()) + .map(|((child, state), layout)| { + child + .as_widget() + .mouse_interaction(state, layout, cursor, viewport, renderer) + }) + .max() + .unwrap_or_default() + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &crate::Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + ) { + for ((child, state), layout) in self + .children + .iter() + .zip(&tree.children) + .zip(layout.children()) + { + child + .as_widget() + .draw(state, renderer, theme, style, layout, cursor, viewport); + } + } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + ) -> Option> { + overlay::from_children(&mut self.children, tree, layout, renderer) + } + + #[cfg(feature = "a11y")] + /// get the a11y nodes for the widget + fn a11y_nodes( + &self, + layout: Layout<'_>, + state: &Tree, + p: mouse::Cursor, + ) -> iced_accessibility::A11yTree { + use iced_accessibility::A11yTree; + A11yTree::join( + self.children + .iter() + .zip(layout.children()) + .zip(state.children.iter()) + .map(|((c, c_layout), state)| c.as_widget().a11y_nodes(c_layout, state, p)), + ) + } +} + +impl<'a, Message: 'static + Clone> From> for Element<'a, Message> { + fn from(flex_row: Grid<'a, Message>) -> Self { + Self::new(flex_row) + } +} + +#[derive(Copy, Clone, Debug, Setters)] +#[must_use] +pub struct Assignment { + pub(super) column: u16, + pub(super) row: u16, + pub(super) width: u16, + pub(super) height: u16, +} + +impl Assignment { + pub const fn new() -> Self { + Self { + column: 0, + row: 0, + width: 1, + height: 1, + } + } +} + +impl From<(u16, u16)> for Assignment { + fn from((column, row): (u16, u16)) -> Self { + Self { + column, + row, + width: 1, + height: 1, + } + } +} + +impl From<(u16, u16, u16, u16)> for Assignment { + fn from((column, row, width, height): (u16, u16, u16, u16)) -> Self { + Self { + column, + row, + width, + height, + } + } +} + +#[must_use] +pub struct Item<'a, Message> { + widget: Element<'a, Message>, + assignment: Assignment, +} + +impl<'a, Message> Item<'a, Message> { + pub fn width(mut self, width: u16) -> Self { + self.assignment.width = width; + self + } + + pub fn height(mut self, height: u16) -> Self { + self.assignment.height = height; + self + } +} diff --git a/src/widget/mod.rs b/src/widget/mod.rs index fa95d9985fb..922f17687af 100644 --- a/src/widget/mod.rs +++ b/src/widget/mod.rs @@ -106,6 +106,9 @@ pub mod divider { pub mod flex_row; pub use flex_row::{flex_row, FlexRow}; +pub mod grid; +pub use grid::{grid, Grid}; + mod header_bar; pub use header_bar::{header_bar, HeaderBar};