From c69f04d5f31046b6d555f0f01e6c65eb1a28bdc7 Mon Sep 17 00:00:00 2001 From: Alexander van Saase Date: Fri, 20 Oct 2023 15:39:22 +0200 Subject: [PATCH] Implement grid flex layout --- Cargo.toml | 3 +- examples/grid/Cargo.toml | 2 - examples/grid/src/main.rs | 98 ++++----- src/lib.rs | 5 +- src/native/grid.rs | 418 -------------------------------------- src/native/grid/layout.rs | 215 ++++++++++++++++++++ src/native/grid/mod.rs | 7 + src/native/grid/types.rs | 245 ++++++++++++++++++++++ src/native/grid/widget.rs | 183 +++++++++++++++++ src/native/mod.rs | 2 - 10 files changed, 694 insertions(+), 484 deletions(-) delete mode 100644 src/native/grid.rs create mode 100644 src/native/grid/layout.rs create mode 100644 src/native/grid/mod.rs create mode 100644 src/native/grid/types.rs create mode 100644 src/native/grid/widget.rs diff --git a/Cargo.toml b/Cargo.toml index 408507e2..18468324 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,7 @@ date_picker = ["chrono", "once_cell", "icon_text"] color_picker = ["icon_text", "iced_widget/canvas"] cupertino = ["iced_widget/canvas", "time"] floating_element = [] -grid = [] +grid = ["itertools"] glow = [] # TODO icon_text = ["icons"] icons = [] @@ -66,6 +66,7 @@ num-traits = { version = "0.2.16", optional = true } time = { version = "0.3.23", features = ["local-offset"], optional = true } chrono = { version = "0.4.26", optional = true } once_cell = { version = "1.18.0", optional = true } +itertools = { version = "0.11.0", optional = true } [dependencies.iced_widget] diff --git a/examples/grid/Cargo.toml b/examples/grid/Cargo.toml index f8aed8f0..ea3fc0ca 100644 --- a/examples/grid/Cargo.toml +++ b/examples/grid/Cargo.toml @@ -4,8 +4,6 @@ version = "0.1.0" authors = ["Alexander van Saase "] edition = "2021" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - [dependencies] iced_aw = { workspace = true, features = ["grid"] } iced.workspace = true diff --git a/examples/grid/src/main.rs b/examples/grid/src/main.rs index 174ae2ce..596b0c65 100644 --- a/examples/grid/src/main.rs +++ b/examples/grid/src/main.rs @@ -1,17 +1,19 @@ -use iced::widget::{checkbox, column, container, pick_list, radio, row, slider}; +use iced::widget::{checkbox, container, pick_list, row, slider}; +use iced::Padding; use iced::{ alignment::{Horizontal, Vertical}, Color, Element, Length, Sandbox, Settings, }; -use iced_aw::{grid, grid_row, Strategy}; +use iced_aw::{grid, grid_row}; struct App { horizontal_alignment: Horizontal, - vertical_alignemnt: Vertical, + vertical_alignment: Vertical, column_spacing: f32, row_spacing: f32, - row_strategy: Strategy, - column_strategy: Strategy, + fill_width: bool, + fill_height: bool, + padding: f32, debug_layout: bool, } @@ -21,8 +23,9 @@ enum Message { VerticalAlignment(Vertical), ColumnSpacing(f32), RowSpacing(f32), - RowStrategy(Strategy), - ColumnStrategy(Strategy), + FillWidth(bool), + FillHeight(bool), + Padding(f32), DebugToggled(bool), } @@ -32,11 +35,12 @@ impl Sandbox for App { fn new() -> Self { Self { horizontal_alignment: Horizontal::Left, - vertical_alignemnt: Vertical::Center, + vertical_alignment: Vertical::Center, column_spacing: 5.0, row_spacing: 5.0, - row_strategy: Strategy::Minimum, - column_strategy: Strategy::Minimum, + fill_width: false, + fill_height: false, + padding: 0.0, debug_layout: false, } } @@ -48,11 +52,12 @@ impl Sandbox for App { fn update(&mut self, message: Self::Message) { match message { Message::HorizontalAlignment(align) => self.horizontal_alignment = align, - Message::VerticalAlignment(align) => self.vertical_alignemnt = align, + Message::VerticalAlignment(align) => self.vertical_alignment = align, Message::ColumnSpacing(spacing) => self.column_spacing = spacing, Message::RowSpacing(spacing) => self.row_spacing = spacing, - Message::RowStrategy(strategy) => self.row_strategy = strategy, - Message::ColumnStrategy(strategy) => self.column_strategy = strategy, + Message::FillWidth(fill) => self.fill_width = fill, + Message::FillHeight(fill) => self.fill_height = fill, + Message::Padding(value) => self.padding = value, Message::DebugToggled(enabled) => self.debug_layout = enabled, } } @@ -72,58 +77,47 @@ impl Sandbox for App { .iter() .map(vertical_alignment_to_string) .collect::>(), - Some(vertical_alignment_to_string(&self.vertical_alignemnt)), + Some(vertical_alignment_to_string(&self.vertical_alignment)), |selected| Message::VerticalAlignment(string_to_vertical_align(&selected)), ); let row_spacing_slider = - slider(0.0..=100.0, self.row_spacing, Message::RowSpacing).width(200.0); + slider(0.0..=100.0, self.row_spacing, Message::RowSpacing).width(Length::Fill); let col_spacing_slider = - slider(0.0..=100.0, self.column_spacing, Message::ColumnSpacing).width(200.0); + slider(0.0..=100.0, self.column_spacing, Message::ColumnSpacing).width(Length::Fill); let debug_mode_check = checkbox("", self.debug_layout, Message::DebugToggled); - let row_height_radio = column( - STRATEGIES - .iter() - .map(|strategy| { - let name = strategy_to_string(strategy); - radio(name, strategy, Some(&self.row_strategy), |click| { - Message::RowStrategy(click.clone()) - }) - }) - .map(Element::from) - .collect(), - ) - .spacing(5); - - let col_width_radio = row(STRATEGIES - .iter() - .map(|strategy| { - let name = strategy_to_string(strategy); - radio(name, strategy, Some(&self.column_strategy), |click| { - Message::ColumnStrategy(click.clone()) - }) - }) - .map(Element::from) - .collect()) + let fill_checkboxes = row![ + checkbox("Width", self.fill_width, Message::FillWidth), + checkbox("Height", self.fill_height, Message::FillHeight) + ] .spacing(10); - let grid = grid!( - grid_row!("Horizontal alignment", horizontal_align_pick), + let padding_slider = + slider(0.0..=100.0, self.padding, Message::Padding).width(Length::Fixed(400.0)); + + let mut grid = grid!( + grid_row!("Horizontal alignment", horizontal_align_pick,), grid_row!("Vertical alignment", vertical_align_pick), grid_row!("Row spacing", row_spacing_slider), grid_row!("Column spacing", col_spacing_slider), - grid_row!("Row height", row_height_radio), - grid_row!("Column width", col_width_radio), + grid_row!("Fill space", fill_checkboxes), + grid_row!("Padding", padding_slider), grid_row!("Debug mode", debug_mode_check) ) .horizontal_alignment(self.horizontal_alignment) - .vertical_alignment(self.vertical_alignemnt) + .vertical_alignment(self.vertical_alignment) .row_spacing(self.row_spacing) .column_spacing(self.column_spacing) - .row_height_strategy(self.row_strategy.clone()) - .column_width_strategy(self.column_strategy.clone()); + .padding(Padding::new(self.padding)); + + if self.fill_width { + grid = grid.width(Length::Fill); + } + if self.fill_height { + grid = grid.height(Length::Fill); + } let mut contents = Element::from(grid); if self.debug_layout { @@ -143,8 +137,6 @@ const HORIZONTAL_ALIGNMENTS: [Horizontal; 3] = const VERTICAL_ALIGNMENTS: [Vertical; 3] = [Vertical::Top, Vertical::Center, Vertical::Bottom]; -const STRATEGIES: [Strategy; 2] = [Strategy::Minimum, Strategy::Equal]; - fn horizontal_align_to_string(alignment: &Horizontal) -> String { match alignment { Horizontal::Left => "Left", @@ -181,14 +173,6 @@ fn string_to_vertical_align(input: &str) -> Vertical { } } -fn strategy_to_string(strategy: &Strategy) -> String { - match strategy { - Strategy::Minimum => "Minimum", - Strategy::Equal => "Equal", - } - .to_string() -} - fn main() -> iced::Result { App::run(Settings::default()) } diff --git a/src/lib.rs b/src/lib.rs index f0d6f096..7950b925 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -84,10 +84,7 @@ mod platform { #[doc(no_inline)] #[cfg(feature = "grid")] - pub use { - crate::native::grid, - grid::{Grid, GridRow, Strategy}, - }; + pub use crate::native::grid::{Grid, GridRow}; #[doc(no_inline)] #[cfg(feature = "modal")] diff --git a/src/native/grid.rs b/src/native/grid.rs deleted file mode 100644 index 29e965ef..00000000 --- a/src/native/grid.rs +++ /dev/null @@ -1,418 +0,0 @@ -//! A container to layout widgets in a grid. - -use iced_widget::core::{ - alignment::{Horizontal, Vertical}, - event, - layout::{Limits, Node}, - mouse, overlay, - overlay::Group, - renderer::Style, - widget::{Operation, Tree}, - Clipboard, Element, Event, Layout, Length, Pixels, Point, Rectangle, Shell, Size, Widget, -}; - -/// A container that distributes its contents in a grid of rows and columns. -/// -/// The number of columns is determined by the row with the most elements. -#[allow(missing_debug_implementations)] -pub struct Grid<'a, Message, Renderer = crate::Renderer> { - rows: Vec>, - horizontal_alignment: Horizontal, - vertical_alignment: Vertical, - row_height_strategy: Strategy, - columng_width_stratgey: Strategy, - row_spacing: Pixels, - column_spacing: Pixels, -} - -impl<'a, Message, Renderer> Default for Grid<'a, Message, Renderer> -where - Renderer: iced_widget::core::Renderer, -{ - fn default() -> Self { - Self { - rows: Vec::new(), - horizontal_alignment: Horizontal::Left, - vertical_alignment: Vertical::Center, - row_height_strategy: Strategy::Minimum, - columng_width_stratgey: Strategy::Minimum, - row_spacing: 1.0.into(), - column_spacing: 1.0.into(), - } - } -} - -impl<'a, Message, Renderer> Grid<'a, Message, Renderer> -where - Renderer: iced_widget::core::Renderer, -{ - /// Creates a new [`Grid`]. - #[must_use] - pub fn new() -> Self { - Self::default() - } - - /// Creates a [`Grid`] with the given [`GridRow`]s. - #[must_use] - pub fn with_rows(rows: Vec>) -> Self { - Self { - rows, - ..Default::default() - } - } - - /// Adds a [`GridRow`] to the [`Grid`]. - #[must_use] - pub fn push(mut self, row: GridRow<'a, Message, Renderer>) -> Self { - self.rows.push(row); - self - } - - /// Sets the horizontal alignment of the widgets within their cells. Default: - /// [`Horizontal::Left`] - #[must_use] - pub fn horizontal_alignment(mut self, align: Horizontal) -> Self { - self.horizontal_alignment = align; - self - } - - /// Sets the vertical alignment of the widgets within their cells. Default: - /// [`Vertical::Center`] - #[must_use] - pub fn vertical_alignment(mut self, align: Vertical) -> Self { - self.vertical_alignment = align; - self - } - - /// Sets the [`Strategy`] used to determine the height of the rows. - #[must_use] - pub fn row_height_strategy(mut self, strategy: Strategy) -> Self { - self.row_height_strategy = strategy; - self - } - - /// Sets the [`Strategy`] used to determine the width of the columns. - #[must_use] - pub fn column_width_strategy(mut self, strategy: Strategy) -> Self { - self.columng_width_stratgey = strategy; - self - } - - /// Sets the spacing between the rows and columns. - // pub fn spacing(mut self, spacing: impl Into) -> Self { - #[must_use] - pub fn spacing(mut self, spacing: f32) -> Self { - let spacing: Pixels = spacing.into(); - self.row_spacing = spacing; - self.column_spacing = spacing; - self - } - - /// Sets the spacing between the rows. - #[must_use] - pub fn row_spacing(mut self, spacing: impl Into) -> Self { - self.row_spacing = spacing.into(); - self - } - - /// Sets the spacing between the columns. - #[must_use] - pub fn column_spacing(mut self, spacing: impl Into) -> Self { - self.column_spacing = spacing.into(); - self - } - - fn elements_iter(&self) -> impl Iterator> { - self.rows.iter().flat_map(|row| row.elements.iter()) - } - - fn elements_iter_mut(&mut self) -> impl Iterator> { - self.rows.iter_mut().flat_map(|row| row.elements.iter_mut()) - } - - fn column_count(&self) -> usize { - self.rows - .iter() - .map(|row| row.elements.len()) - .max() - .unwrap_or(0) - } - - fn row_count(&self) -> usize { - self.rows.len() - } - - fn element_count(&self) -> usize { - self.rows.iter().map(|row| row.elements.len()).sum() - } -} - -/// A container that distributes its contents in a row of a [`crate::Grid`]. -#[allow(missing_debug_implementations)] -pub struct GridRow<'a, Message, Renderer = crate::Renderer> { - pub(crate) elements: Vec>, -} - -impl<'a, Message, Renderer> Default for GridRow<'a, Message, Renderer> -where - Renderer: iced_widget::core::Renderer, -{ - fn default() -> Self { - Self { - elements: Vec::new(), - } - } -} - -impl<'a, Message, Renderer> GridRow<'a, Message, Renderer> -where - Renderer: iced_widget::core::Renderer, -{ - /// Creates a new [`GridRow`]. - #[must_use] - pub fn new() -> Self { - Self::default() - } - - /// Creates a new [`GridRow`] with the given widgets. - #[must_use] - pub fn with_elements(children: Vec>>) -> Self { - Self { - elements: children.into_iter().map(std::convert::Into::into).collect(), - } - } - - /// Adds a widget to the [`GridRow`]. - #[must_use] - pub fn push(mut self, element: E) -> Self - where - E: Into>, - { - self.elements.push(element.into()); - self - } -} - -impl<'a, Message, Renderer> Widget for Grid<'a, Message, Renderer> -where - Renderer: iced_widget::core::Renderer, -{ - fn width(&self) -> Length { - Length::Shrink - } - - fn height(&self) -> Length { - Length::Shrink - } - - fn layout(&self, renderer: &Renderer, limits: &Limits) -> Node { - if self.element_count() == 0 { - return Node::new(Size::ZERO); - } - - let limits = limits.width(self.width()).height(self.height()); - - // Calculate the column widths and row heights to fit the contents - let mut min_columns_widths = Vec::::with_capacity(self.column_count()); - let mut min_row_heights = Vec::::with_capacity(self.row_count()); - let mut max_row_height = 0.0f32; - let mut max_column_width = 0.0f32; - for row in &self.rows { - let mut row_height = 0.0f32; - - for (col_idx, element) in row.elements.iter().enumerate() { - let layout = element.as_widget().layout(renderer, &limits); - let Size { width, height } = layout.size(); - - #[allow(clippy::option_if_let_else)] - if let Some(column_width) = min_columns_widths.get_mut(col_idx) { - *column_width = column_width.max(width); - } else { - min_columns_widths.insert(col_idx, width); - } - - row_height = row_height.max(height); - max_column_width = max_column_width.max(width); - } - min_row_heights.push(row_height); - max_row_height = max_row_height.max(row_height); - } - - // Create the grid layout - let mut x = 0.0; - let mut y = 0.0; - let mut nodes = Vec::with_capacity(self.element_count()); - for (row_idx, row) in self.rows.iter().enumerate() { - x = 0.0; - let row_height = match self.row_height_strategy { - Strategy::Minimum => min_row_heights[row_idx], - Strategy::Equal => max_row_height, - }; - for (col_idx, element) in row.elements.iter().enumerate() { - let col_width = match self.columng_width_stratgey { - Strategy::Minimum => min_columns_widths[col_idx], - Strategy::Equal => max_column_width, - }; - let cell_size = Size::new(col_width, row_height); - - let mut node = element.as_widget().layout(renderer, &limits); - node.move_to(Point::new(x, y)); - node.align( - self.horizontal_alignment.into(), - self.vertical_alignment.into(), - cell_size, - ); - nodes.push(node); - x += col_width; - if col_idx < row.elements.len() - 1 { - x += self.column_spacing.0; - } - } - y += row_height; - if row_idx < self.rows.len() - 1 { - y += self.row_spacing.0; - } - } - - let grid_size = Size::new(x, y); - - Node::with_children(grid_size, nodes) - } - - fn draw( - &self, - state: &Tree, - renderer: &mut Renderer, - theme: &::Theme, - style: &Style, - layout: Layout<'_>, - cursor: mouse::Cursor, - viewport: &Rectangle, - ) { - for ((element, state), layout) in self - .elements_iter() - .zip(&state.children) - .zip(layout.children()) - { - element - .as_widget() - .draw(state, renderer, theme, style, layout, cursor, viewport); - } - } - - fn children(&self) -> Vec { - self.elements_iter().map(Tree::new).collect() - } - - fn diff(&self, tree: &mut Tree) { - tree.diff_children(&self.elements_iter().collect::>()); - } - - fn operate( - &self, - state: &mut Tree, - layout: Layout<'_>, - renderer: &Renderer, - operation: &mut dyn Operation, - ) { - for ((element, state), layout) in self - .elements_iter() - .zip(&mut state.children) - .zip(layout.children()) - { - element - .as_widget() - .operate(state, layout, renderer, operation); - } - } - - fn on_event( - &mut self, - state: &mut Tree, - event: Event, - layout: Layout<'_>, - cursor: mouse::Cursor, - renderer: &Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - viewport: &Rectangle, - ) -> event::Status { - let children_status = self - .elements_iter_mut() - .zip(&mut state.children) - .zip(layout.children()) - .map(|((child, state), layout)| { - child.as_widget_mut().on_event( - state, - event.clone(), - layout, - cursor, - renderer, - clipboard, - shell, - viewport, - ) - }); - - children_status.fold(event::Status::Ignored, event::Status::merge) - } - - fn mouse_interaction( - &self, - state: &Tree, - layout: Layout<'_>, - cursor: mouse::Cursor, - viewport: &Rectangle, - renderer: &Renderer, - ) -> mouse::Interaction { - self.elements_iter() - .zip(&state.children) - .zip(layout.children()) - .map(|((e, state), layout)| { - e.as_widget() - .mouse_interaction(state, layout, cursor, viewport, renderer) - }) - .fold(mouse::Interaction::default(), |interaction, next| { - interaction.max(next) - }) - } - - fn overlay<'b>( - &'b mut self, - tree: &'b mut Tree, - layout: Layout<'_>, - renderer: &Renderer, - ) -> Option> { - let children = self - .elements_iter_mut() - .zip(&mut tree.children) - .zip(layout.children()) - .filter_map(|((child, state), layout)| { - child.as_widget_mut().overlay(state, layout, renderer) - }) - .collect::>(); - - (!children.is_empty()).then(|| Group::with_children(children).overlay()) - } -} - -impl<'a, Message, Renderer> From> for Element<'a, Message, Renderer> -where - Renderer: iced_widget::core::Renderer + 'a, - Message: 'static, -{ - fn from(grid: Grid<'a, Message, Renderer>) -> Self { - Element::new(grid) - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -/// Strategy used for determining the widths and height of columns and rows. -pub enum Strategy { - /// Each row (column) has the height (width) needed to fit its contents. - Minimum, - - /// All rows (columns) have the same height (width). The height (width) is determined by the - /// row (column) with the talest (widest) contents. - Equal, -} diff --git a/src/native/grid/layout.rs b/src/native/grid/layout.rs new file mode 100644 index 00000000..b9e902f7 --- /dev/null +++ b/src/native/grid/layout.rs @@ -0,0 +1,215 @@ +use std::cmp::Ordering; + +use iced_widget::core::{ + alignment::{Horizontal, Vertical}, + layout::{Limits, Node}, + Length, Padding, Pixels, Point, Size, +}; +use itertools::{Itertools, Position}; + +use super::types::GridRow; + +#[allow(clippy::too_many_arguments)] +pub(super) fn layout( + renderer: &Renderer, + limits: &Limits, + column_count: usize, + row_count: usize, + element_count: usize, + rows: &[GridRow<'_, Message, Renderer>], + column_spacing: Pixels, + row_spacing: Pixels, + padding: Padding, + horizontal_alignment: Horizontal, + vertical_alignment: Vertical, + width: Length, + height: Length, + column_lengths: &[Length], + row_lengths: &[Length], +) -> Node +where + Renderer: iced_widget::core::Renderer, +{ + let mut column_widths = Vec::::with_capacity(column_count); + let mut row_heights = Vec::::with_capacity(row_count); + + // Measure the minimum row and column size to fit the contents + minimum_row_column_sizes(renderer, &mut column_widths, &mut row_heights, rows); + + // Adjust for fixed row and column sizes + adjust_size_for_fixed_length(&mut column_widths, column_lengths); + adjust_size_for_fixed_length(&mut row_heights, row_lengths); + + // Calculate grid limits + let min_size = Size::new( + total_length(&column_widths, column_spacing), + total_length(&row_heights, row_spacing), + ); + + let grid_limits = limits + .pad(padding) + .min_width(min_size.width) + .min_height(min_size.height) + .width(width) + .height(height); + let grid_size = grid_limits.fill().max(min_size); + + // Allocate the available space + let available_width = grid_size.width - total_spacing(column_count, column_spacing); + let available_height = grid_size.height - total_spacing(row_count, row_spacing); + allocate_space(&mut column_widths, column_lengths, available_width); + allocate_space(&mut row_heights, row_lengths, available_height); + + // Lay out the widgets + create_grid_layout( + element_count, + rows, + &row_heights, + &column_widths, + renderer, + horizontal_alignment, + vertical_alignment, + column_spacing, + row_spacing, + grid_size, + ) +} + +fn minimum_row_column_sizes( + renderer: &Renderer, + column_widths: &mut Vec, + row_heights: &mut Vec, + rows: &[GridRow<'_, Message, Renderer>], +) where + Renderer: iced_widget::core::Renderer, +{ + for row in rows { + let mut row_height = 0.0f32; + + for (col_idx, element) in row.elements.iter().enumerate() { + let child_limits = Limits::NONE.width(Length::Shrink).height(Length::Shrink); + let Size { width, height } = element.as_widget().layout(renderer, &child_limits).size(); + + #[allow(clippy::option_if_let_else)] + match column_widths.get_mut(col_idx) { + Some(col_width) => *col_width = col_width.max(width), + None => column_widths.insert(col_idx, width), + } + + row_height = row_height.max(height); + } + row_heights.push(row_height); + } +} + +fn adjust_size_for_fixed_length(sizes: &mut [f32], length_settings: &[Length]) { + for (size, lenght) in sizes.iter_mut().zip(length_settings.iter().cycle()) { + if let Length::Fixed(value) = *lenght { + *size = size.max(value); + } + } +} + +fn total_length(element_sizes: &[f32], spacing: Pixels) -> f32 { + let n_elements = element_sizes.len(); + element_sizes.iter().sum::() + total_spacing(n_elements, spacing) +} + +fn total_spacing(element_count: usize, spacing: Pixels) -> f32 { + element_count.saturating_sub(1) as f32 * spacing.0 +} + +fn allocate_space(current_sizes: &mut [f32], length_settings: &[Length], available_space: f32) { + let mut fill_factor_sum = length_settings + .iter() + .cycle() + .take(current_sizes.len()) + .map(Length::fill_factor) + .sum::(); + + if fill_factor_sum == 0 { + return; + } + + let mut space_to_divide = available_space; + + let sorted_iter = current_sizes + .iter_mut() + .zip(length_settings.iter().cycle()) + .sorted_by(|(&mut a_size, &a_length), (&mut b_size, &b_length)| { + if a_length.fill_factor() == 0 { + return Ordering::Less; + } else if b_length.fill_factor() == 0 { + return Ordering::Greater; + } + + (b_size / f32::from(b_length.fill_factor())) + .total_cmp(&(a_size / f32::from(a_length.fill_factor()))) + }); + + for (size, length) in sorted_iter { + let fill_factor = length.fill_factor(); + let fill_size = f32::from(fill_factor) / f32::from(fill_factor_sum) * space_to_divide; + let new_size = size.max(fill_size); + fill_factor_sum -= fill_factor; + space_to_divide -= new_size; + *size = new_size; + } +} + +#[allow(clippy::too_many_arguments)] +fn create_grid_layout( + element_count: usize, + rows: &[GridRow<'_, Message, Renderer>], + row_heights: &[f32], + column_widths: &[f32], + renderer: &Renderer, + horizontal_alignment: Horizontal, + vertical_alignment: Vertical, + column_spacing: Pixels, + row_spacing: Pixels, + grid_size: Size, +) -> Node +where + Renderer: iced_widget::core::Renderer, +{ + let mut y = 0.0; + let mut nodes = Vec::with_capacity(element_count); + for (row_position, (row, &row_height)) in rows.iter().zip(row_heights).with_position() { + let mut x = 0.0; + for (col_position, (element, &column_width)) in + row.elements.iter().zip(column_widths).with_position() + { + let widget = element.as_widget(); + let widget_limits = Limits::NONE + .width(widget.width()) + .height(widget.height()) + .max_width(column_width) + .max_height(row_height); + + let mut node = widget.layout(renderer, &widget_limits); + node.move_to(Point::new(x, y)); + node.align( + horizontal_alignment.into(), + vertical_alignment.into(), + Size::new(column_width, row_height), + ); + nodes.push(node); + + x += column_width; + if not_last(col_position) { + x += column_spacing.0; + } + } + y += row_height; + if not_last(row_position) { + y += row_spacing.0; + } + } + + Node::with_children(grid_size, nodes) +} + +fn not_last(position: Position) -> bool { + position != Position::Last && position != Position::Only +} diff --git a/src/native/grid/mod.rs b/src/native/grid/mod.rs new file mode 100644 index 00000000..026fd79f --- /dev/null +++ b/src/native/grid/mod.rs @@ -0,0 +1,7 @@ +//! A container to layout widgets in a grid. + +mod layout; +mod types; +mod widget; + +pub use types::{Grid, GridRow}; diff --git a/src/native/grid/types.rs b/src/native/grid/types.rs new file mode 100644 index 00000000..04e0d17c --- /dev/null +++ b/src/native/grid/types.rs @@ -0,0 +1,245 @@ +use iced_widget::core::{ + alignment::{Horizontal, Vertical}, + Element, Length, Padding, Pixels, +}; + +/// A container that distributes its contents in a grid of rows and columns. +/// +/// The number of columns is determined by the row with the most elements. +#[allow(missing_debug_implementations)] +pub struct Grid<'a, Message, Renderer = crate::Renderer> { + pub(super) rows: Vec>, + pub(super) horizontal_alignment: Horizontal, + pub(super) vertical_alignment: Vertical, + pub(super) column_spacing: Pixels, + pub(super) row_spacing: Pixels, + pub(super) padding: Padding, + pub(super) width: Length, + pub(super) height: Length, + pub(super) column_widths: Vec, + pub(super) row_heights: Vec, +} + +impl<'a, Message, Renderer> Default for Grid<'a, Message, Renderer> +where + Renderer: iced_widget::core::Renderer, +{ + fn default() -> Self { + Self { + rows: Vec::new(), + horizontal_alignment: Horizontal::Left, + vertical_alignment: Vertical::Center, + column_spacing: 1.0.into(), + row_spacing: 1.0.into(), + padding: Padding::ZERO, + width: Length::Shrink, + height: Length::Shrink, + column_widths: vec![Length::Fill], + row_heights: vec![Length::Fill], + } + } +} + +impl<'a, Message, Renderer> Grid<'a, Message, Renderer> +where + Renderer: iced_widget::core::Renderer, +{ + /// Creates a new [`Grid`]. + #[must_use] + pub fn new() -> Self { + Self::default() + } + + /// Creates a [`Grid`] with the given [`GridRow`]s. + #[must_use] + pub fn with_rows(rows: Vec>) -> Self { + Self { + rows, + ..Default::default() + } + } + + /// Adds a [`GridRow`] to the [`Grid`]. + #[must_use] + pub fn push(mut self, row: GridRow<'a, Message, Renderer>) -> Self { + self.rows.push(row); + self + } + + /// Sets the horizontal alignment of the widgets within their cells. Default: + /// [`Horizontal::Left`] + #[must_use] + pub fn horizontal_alignment(mut self, align: Horizontal) -> Self { + self.horizontal_alignment = align; + self + } + + /// Sets the vertical alignment of the widgets within their cells. Default: + /// [`Vertical::Center`] + #[must_use] + pub fn vertical_alignment(mut self, align: Vertical) -> Self { + self.vertical_alignment = align; + self + } + + /// Sets the spacing between rows and columns. To set row and column spacing separately, use + /// [`Self::column_spacing()`] and [`Self::row_spacing()`]. + #[must_use] + pub fn spacing(mut self, spacing: f32) -> Self { + let spacing: Pixels = spacing.into(); + self.row_spacing = spacing; + self.column_spacing = spacing; + self + } + + /// Sets the spacing between columns. + #[must_use] + pub fn column_spacing(mut self, spacing: impl Into) -> Self { + self.column_spacing = spacing.into(); + self + } + + /// Sets the spacing between rows. + #[must_use] + pub fn row_spacing(mut self, spacing: impl Into) -> Self { + self.row_spacing = spacing.into(); + self + } + + /// Sets the padding around the grid. + #[must_use] + pub fn padding(mut self, padding: impl Into) -> Self { + self.padding = padding.into(); + self + } + + /// Sets the grid width. + #[must_use] + pub fn width(mut self, width: impl Into) -> Self { + self.width = width.into(); + self + } + + /// Sets the grid height. + #[must_use] + pub fn height(mut self, height: impl Into) -> Self { + self.height = height.into(); + self + } + + /// Sets the column width. + /// + /// The same setting will be used for all columns. To set separate values for each column, use + /// [`Self::column_widths()`]. Columns are never smaller than the space needed to fit their + /// contents. + #[must_use] + pub fn column_width(mut self, width: impl Into) -> Self { + self.column_widths = vec![width.into()]; + self + } + + /// Sets the row height. + /// + /// The same setting will be used for all rows. To set separate values for each row, use + /// [`Self::row_heights()`]. Rows are never smaller than the space needed to fit their + /// contents. + #[must_use] + pub fn row_height(mut self, height: impl Into) -> Self { + self.row_heights = vec![height.into()]; + self + } + + /// Sets a separate width for each column. + /// + /// Columns are never smaller than the space needed to fit their contents. When supplying fewer + /// values than the number of columns, values are are repeated using + /// [`std::iter::Iterator::cycle()`]. + #[must_use] + pub fn column_widths(mut self, widths: &[Length]) -> Self { + self.column_widths = widths.into(); + self + } + + /// Sets a separate height for each row. + /// + /// Rows are never smaller than the space needed to fit their contents. When supplying fewer + /// values than the number of rows, values are are repeated using + /// [`std::iter::Iterator::cycle()`]. + #[must_use] + pub fn row_heights(mut self, heights: &[Length]) -> Self { + self.row_heights = heights.into(); + self + } + + pub(super) fn elements_iter(&self) -> impl Iterator> { + self.rows.iter().flat_map(|row| row.elements.iter()) + } + + pub(super) fn elements_iter_mut( + &mut self, + ) -> impl Iterator> { + self.rows.iter_mut().flat_map(|row| row.elements.iter_mut()) + } + + pub(super) fn column_count(&self) -> usize { + self.rows + .iter() + .map(|row| row.elements.len()) + .max() + .unwrap_or(0) + } + + pub(super) fn row_count(&self) -> usize { + self.rows.len() + } + + pub(super) fn element_count(&self) -> usize { + self.rows.iter().map(|row| row.elements.len()).sum() + } +} + +/// A container that distributes its contents in a row of a [`crate::Grid`]. +#[allow(missing_debug_implementations)] +pub struct GridRow<'a, Message, Renderer = crate::Renderer> { + pub(crate) elements: Vec>, +} + +impl<'a, Message, Renderer> Default for GridRow<'a, Message, Renderer> +where + Renderer: iced_widget::core::Renderer, +{ + fn default() -> Self { + Self { + elements: Vec::new(), + } + } +} + +impl<'a, Message, Renderer> GridRow<'a, Message, Renderer> +where + Renderer: iced_widget::core::Renderer, +{ + /// Creates a new [`GridRow`]. + #[must_use] + pub fn new() -> Self { + Self::default() + } + + /// Creates a new [`GridRow`] with the given widgets. + #[must_use] + pub fn with_elements(children: Vec>>) -> Self { + Self { + elements: children.into_iter().map(std::convert::Into::into).collect(), + } + } + + /// Adds a widget to the [`GridRow`]. + #[must_use] + pub fn push(mut self, element: E) -> Self + where + E: Into>, + { + self.elements.push(element.into()); + self + } +} diff --git a/src/native/grid/widget.rs b/src/native/grid/widget.rs new file mode 100644 index 00000000..a6fe011d --- /dev/null +++ b/src/native/grid/widget.rs @@ -0,0 +1,183 @@ +use iced_widget::core::{ + event, + layout::{Limits, Node}, + mouse, overlay, + overlay::Group, + renderer::Style, + widget::{Operation, Tree}, + Clipboard, Element, Event, Layout, Length, Rectangle, Shell, Size, Widget, +}; + +use super::{layout::layout, types::Grid}; + +impl<'a, Message, Renderer> Widget for Grid<'a, Message, Renderer> +where + Renderer: iced_widget::core::Renderer, +{ + fn width(&self) -> Length { + self.width + } + + fn height(&self) -> Length { + self.height + } + + fn layout(&self, renderer: &Renderer, limits: &Limits) -> Node { + if self.element_count() == 0 { + return Node::new(Size::ZERO); + } + + assert!( + !self.column_widths.is_empty(), + "At least one column width is required" + ); + assert!( + !self.row_heights.is_empty(), + "At least one row height is required" + ); + + layout( + renderer, + limits, + self.column_count(), + self.row_count(), + self.element_count(), + &self.rows, + self.column_spacing, + self.row_spacing, + self.padding, + self.horizontal_alignment, + self.vertical_alignment, + self.width, + self.height, + &self.column_widths, + &self.row_heights, + ) + } + + fn draw( + &self, + state: &Tree, + renderer: &mut Renderer, + theme: &::Theme, + style: &Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + ) { + for ((element, state), layout) in self + .elements_iter() + .zip(&state.children) + .zip(layout.children()) + { + element + .as_widget() + .draw(state, renderer, theme, style, layout, cursor, viewport); + } + } + + fn children(&self) -> Vec { + self.elements_iter().map(Tree::new).collect() + } + + fn diff(&self, tree: &mut Tree) { + tree.diff_children(&self.elements_iter().collect::>()); + } + + fn operate( + &self, + state: &mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn Operation, + ) { + for ((element, state), layout) in self + .elements_iter() + .zip(&mut state.children) + .zip(layout.children()) + { + element + .as_widget() + .operate(state, layout, renderer, operation); + } + } + + fn on_event( + &mut self, + state: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + viewport: &Rectangle, + ) -> event::Status { + let children_status = self + .elements_iter_mut() + .zip(&mut state.children) + .zip(layout.children()) + .map(|((child, state), layout)| { + child.as_widget_mut().on_event( + state, + event.clone(), + layout, + cursor, + renderer, + clipboard, + shell, + viewport, + ) + }); + + children_status.fold(event::Status::Ignored, event::Status::merge) + } + + fn mouse_interaction( + &self, + state: &Tree, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + self.elements_iter() + .zip(&state.children) + .zip(layout.children()) + .map(|((e, state), layout)| { + e.as_widget() + .mouse_interaction(state, layout, cursor, viewport, renderer) + }) + .fold(mouse::Interaction::default(), |interaction, next| { + interaction.max(next) + }) + } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + ) -> Option> { + let children = self + .elements_iter_mut() + .zip(&mut tree.children) + .zip(layout.children()) + .filter_map(|((child, state), layout)| { + child.as_widget_mut().overlay(state, layout, renderer) + }) + .collect::>(); + + (!children.is_empty()).then(|| Group::with_children(children).overlay()) + } +} + +impl<'a, Message, Renderer> From> for Element<'a, Message, Renderer> +where + Renderer: iced_widget::core::Renderer + 'a, + Message: 'static, +{ + fn from(grid: Grid<'a, Message, Renderer>) -> Self { + Element::new(grid) + } +} diff --git a/src/native/mod.rs b/src/native/mod.rs index 92860114..4ba345bf 100644 --- a/src/native/mod.rs +++ b/src/native/mod.rs @@ -64,8 +64,6 @@ pub mod grid; pub use grid::Grid; #[cfg(feature = "grid")] pub use grid::GridRow; -#[cfg(feature = "grid")] -pub use grid::Strategy; #[cfg(feature = "modal")] pub mod modal;