From 025380f2b9a4b53e9aae5fe51291d0c4d44f2e99 Mon Sep 17 00:00:00 2001 From: Alexander van Saase Date: Wed, 11 Oct 2023 20:23:34 +0200 Subject: [PATCH 1/4] Implement new grid widget --- examples/grid/Cargo.toml | 4 +- examples/grid/src/main.rs | 207 +++++++++++++++++----- src/lib.rs | 2 +- src/native/grid.rs | 351 ------------------------------------ src/native/grid/grid.rs | 362 ++++++++++++++++++++++++++++++++++++++ src/native/grid/mod.rs | 9 + src/native/grid/row.rs | 44 +++++ src/native/helpers.rs | 42 ++++- src/native/mod.rs | 5 +- 9 files changed, 619 insertions(+), 407 deletions(-) delete mode 100644 src/native/grid.rs create mode 100644 src/native/grid/grid.rs create mode 100644 src/native/grid/mod.rs create mode 100644 src/native/grid/row.rs diff --git a/examples/grid/Cargo.toml b/examples/grid/Cargo.toml index 23ad6eec..f8aed8f0 100644 --- a/examples/grid/Cargo.toml +++ b/examples/grid/Cargo.toml @@ -1,11 +1,11 @@ [package] name = "grid" version = "0.1.0" -authors = ["daxpedda ", "Luni-4 "] +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 \ No newline at end of file +iced.workspace = true diff --git a/examples/grid/src/main.rs b/examples/grid/src/main.rs index e0afda9e..4104398a 100644 --- a/examples/grid/src/main.rs +++ b/examples/grid/src/main.rs @@ -1,79 +1,194 @@ +use iced::widget::{checkbox, column, container, pick_list, radio, row, slider}; use iced::{ - theme, - widget::{Button, Column, Container, Scrollable, Text}, - Alignment, Color, Element, Length, Sandbox, Settings, + alignment::{Horizontal, Vertical}, + Color, Element, Length, Sandbox, Settings, }; +use iced_aw::{grid, grid_row, Strategy}; -use iced_aw::grid; - -// Number of columns for the grid -const COLUMNS: usize = 2; - -fn main() -> iced::Result { - GridExample::run(Settings::default()) +struct App { + horizontal_alignment: Horizontal, + vertical_alignemnt: Vertical, + column_spacing: f32, + row_spacing: f32, + row_strategy: Strategy, + column_strategy: Strategy, + debug_layout: bool, } #[derive(Debug, Clone)] enum Message { - AddElement, -} - -struct GridExample { - element_index: usize, + HorizontalAlignment(Horizontal), + VerticalAlignment(Vertical), + ColumnSpacing(f32), + RowSpacing(f32), + RowStrategy(Strategy), + ColumnStrategy(Strategy), + DebugToggled(bool), } -impl Sandbox for GridExample { +impl Sandbox for App { type Message = Message; fn new() -> Self { - GridExample { element_index: 0 } + Self { + horizontal_alignment: Horizontal::Left, + vertical_alignemnt: Vertical::Center, + column_spacing: 5.0, + row_spacing: 5.0, + row_strategy: Strategy::Minimum, + column_strategy: Strategy::Minimum, + debug_layout: false, + } } fn title(&self) -> String { - String::from("Grid example") + "Iced Grid widget example".into() } - fn update(&mut self, message: self::Message) { + fn update(&mut self, message: Self::Message) { match message { - Message::AddElement => { - self.element_index += 1; - } + Message::HorizontalAlignment(align) => self.horizontal_alignment = align, + Message::VerticalAlignment(align) => self.vertical_alignemnt = 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::DebugToggled(enabled) => self.debug_layout = enabled, } } - fn view(&self) -> Element<'_, self::Message> { - // Creates a grid with two columns - let mut grid = grid!( - Text::new("Column 1").style(theme::Text::Color(Color::from_rgb8(255, 0, 0))), - Text::new("Column 2").style(theme::Text::Color(Color::from_rgb8(255, 0, 0))), - ) - .strategy(iced_aw::Strategy::Columns(2)); + fn view(&self) -> iced::Element<'_, Self::Message> { + let horizontal_align_pick = pick_list( + HORIZONTAL_ALIGNMENTS + .iter() + .map(horizontal_align_to_string) + .collect::>(), + Some(horizontal_align_to_string(&self.horizontal_alignment)), + |selected| Message::HorizontalAlignment(string_to_horizontal_align(&selected)), + ); - // Add elements to the grid - for i in 0..self.element_index { - grid.insert(Text::new(format!("Row {} Element {}", (i / COLUMNS), i))); - } + let vertical_align_pick = pick_list( + VERTICAL_ALIGNMENTS + .iter() + .map(vertical_alignment_to_string) + .collect::>(), + Some(vertical_alignment_to_string(&self.vertical_alignemnt)), + |selected| Message::VerticalAlignment(string_to_vertical_align(&selected)), + ); - let add_button: Element<'_, Message> = Button::new(Text::new("Add element")) - .on_press(Message::AddElement) - .into(); + let row_spacing_slider = + slider(0.0..=100.0, self.row_spacing, Message::RowSpacing).width(200.0); + let col_spacing_slider = + slider(0.0..=100.0, self.column_spacing, Message::ColumnSpacing).width(200.0); - let column: Element<'_, Message> = Column::new() - .spacing(15) - .max_width(600) - .align_items(Alignment::Center) - .push(grid) - .push(add_button) - .into(); + let debug_mode_check = checkbox("", self.debug_layout, Message::DebugToggled); - let content = Scrollable::new(column); + let row_height_radio = column( + STRATEGIES + .iter() + .map(|strat| { + let name = strategy_to_string(&strat); + radio(name, strat, Some(&self.row_strategy), |click| { + Message::RowStrategy(click.clone()) + }) + }) + .map(Element::from) + .collect(), + ) + .spacing(5); - Container::new(content) + let col_width_radio = row(STRATEGIES + .iter() + .map(|strat| { + let name = strategy_to_string(&strat); + radio(name, strat, Some(&self.column_strategy), |click| { + Message::ColumnStrategy(click.clone()) + }) + }) + .map(Element::from) + .collect()) + .spacing(10); + + let 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!("Debug mode", debug_mode_check) + ) + .horizontal_alignment(self.horizontal_alignment) + .vertical_alignment(self.vertical_alignemnt) + .row_spacing(self.row_spacing) + .column_spacing(self.column_spacing) + .row_height_strategy(self.row_strategy.clone()) + .column_width_strategy(self.column_strategy.clone()); + + let mut contents = Element::from(grid); + if self.debug_layout { + contents = contents.explain(Color::BLACK); + } + container(contents) .width(Length::Fill) .height(Length::Fill) - .padding(10) .center_x() .center_y() .into() } } + +const HORIZONTAL_ALIGNMENTS: [Horizontal; 3] = + [Horizontal::Left, Horizontal::Center, Horizontal::Right]; + +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", + Horizontal::Center => "Center", + Horizontal::Right => "Right", + } + .to_string() +} + +fn string_to_horizontal_align(input: &str) -> Horizontal { + match input { + "Left" => Horizontal::Left, + "Center" => Horizontal::Center, + "Right" => Horizontal::Right, + _ => panic!(), + } +} + +fn vertical_alignment_to_string(alignment: &Vertical) -> String { + match alignment { + Vertical::Top => "Top", + Vertical::Center => "Center", + Vertical::Bottom => "Bottom", + } + .to_string() +} + +fn string_to_vertical_align(input: &str) -> Vertical { + match input { + "Top" => Vertical::Top, + "Center" => Vertical::Center, + "Bottom" => Vertical::Bottom, + _ => panic!(), + } +} + +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 746153d3..f0d6f096 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -86,7 +86,7 @@ mod platform { #[cfg(feature = "grid")] pub use { crate::native::grid, - grid::{Grid, Strategy}, + grid::{Grid, GridRow, Strategy}, }; #[doc(no_inline)] diff --git a/src/native/grid.rs b/src/native/grid.rs deleted file mode 100644 index e473ff7b..00000000 --- a/src/native/grid.rs +++ /dev/null @@ -1,351 +0,0 @@ -//! Use a grid as an input element for creating grids. -//! -//! *This API requires the following crate features to be activated: `grid`* -use iced_widget::core::{ - self, event, - layout::{Limits, Node}, - mouse::{self, Cursor}, - overlay, renderer, - widget::{Operation, Tree}, - Clipboard, Element, Event, Layout, Length, Point, Rectangle, Shell, Size, Widget, -}; - -/// A container that distributes its contents in a grid. -/// -/// # Example -/// -/// ```ignore -/// # use iced::widget::Text; -/// # use iced_aw::Grid; -/// # -/// #[derive(Debug, Clone)] -/// enum Message { -/// } -/// -/// let grid = Grid::::with_columns(2) -/// .push(Text::new("First row, first column")) -/// .push(Text::new("First row, second column")) -/// .push(Text::new("Second row, first column")) -/// .push(Text::new("Second row, second column")); -/// -/// ``` -#[allow(missing_debug_implementations)] -pub struct Grid<'a, Message, Renderer = crate::Renderer> { - /// The distribution [`Strategy`] of the [`Grid`]. - strategy: Strategy, - /// The elements in the [`Grid`]. - elements: Vec>, -} - -/// The [`Strategy`] of how to distribute the columns of the [`Grid`]. -#[derive(Debug)] -pub enum Strategy { - /// Use `n` columns. - Columns(usize), - /// Try to fit as much columns that have a fixed width. - ColumnWidth(f32), -} - -impl Default for Strategy { - fn default() -> Self { - Self::Columns(1) - } -} - -impl<'a, Message, Renderer> Grid<'a, Message, Renderer> -where - Renderer: core::Renderer, -{ - /// Creates a [`Grid`] with ``Strategy::Columns(1)`` - /// Use ``strategy()`` to update the Strategy. - #[must_use] - pub fn new() -> Self { - Self::default() - } - - /// Inserts an [`Element`] into the [`Grid`]. - pub fn insert(&mut self, element: E) - where - E: Into>, - { - self.elements.push(element.into()); - } - - /// Adds an [`Element`] to the [`Grid`]. - #[must_use] - pub fn push(mut self, element: E) -> Self - where - E: Into>, - { - self.elements.push(element.into()); - self - } - - /// Sets the [`Grid`] Strategy. - /// Default is ``Strategy::Columns(1)``. - #[must_use] - pub fn strategy(mut self, strategy: Strategy) -> Self { - self.strategy = strategy; - self - } - - /// Creates a [`Grid`] with given elements and ``Strategy::Columns(1)`` - /// Use ``strategy()`` to update the Strategy. - #[must_use] - pub fn with_children(children: Vec>) -> Self { - Self { - strategy: Strategy::default(), - elements: children, - } - } - - /// Creates a new empty [`Grid`]. - /// Elements will be laid out in a specific amount of columns. - #[must_use] - pub fn with_columns(columns: usize) -> Self { - Self { - strategy: Strategy::Columns(columns), - elements: Vec::new(), - } - } - - /// Creates a new empty [`Grid`]. - /// Columns will be generated to fill the given space. - #[must_use] - pub fn with_column_width(column_width: f32) -> Self { - Self { - strategy: Strategy::ColumnWidth(column_width), - elements: Vec::new(), - } - } -} - -impl<'a, Message, Renderer> Default for Grid<'a, Message, Renderer> -where - Renderer: core::Renderer, -{ - fn default() -> Self { - Self { - strategy: Strategy::default(), - elements: Vec::new(), - } - } -} - -impl<'a, Message, Renderer> Widget for Grid<'a, Message, Renderer> -where - Renderer: core::Renderer, -{ - fn children(&self) -> Vec { - self.elements.iter().map(Tree::new).collect() - } - - fn diff(&self, tree: &mut Tree) { - tree.diff_children(&self.elements); - } - - fn width(&self) -> Length { - Length::Shrink - } - - fn height(&self) -> Length { - Length::Shrink - } - - fn layout(&self, renderer: &Renderer, limits: &Limits) -> Node { - if self.elements.is_empty() { - return Node::new(Size::ZERO); - } - - match self.strategy { - // find out how wide a column is by finding the widest cell in it - Strategy::Columns(columns) => { - if columns == 0 { - return Node::new(Size::ZERO); - } - - let mut layouts = Vec::with_capacity(self.elements.len()); - let mut column_widths = Vec::::with_capacity(columns); - - for (column, element) in (0..columns).cycle().zip(&self.elements) { - let layout = element.as_widget().layout(renderer, limits); - #[allow(clippy::option_if_let_else)] - match column_widths.get_mut(column) { - Some(column_width) => *column_width = column_width.max(layout.size().width), - None => column_widths.insert(column, layout.size().width), - } - - layouts.push(layout); - } - - let column_aligns = - std::iter::once(&0.) - .chain(column_widths.iter()) - .scan(0., |state, width| { - *state += width; - Some(*state) - }); - let grid_width = column_widths.iter().sum(); - - build_grid(columns, column_aligns, layouts.into_iter(), grid_width) - } - // find number of columns by checking how many can fit - Strategy::ColumnWidth(column_width) => { - let column_limits = limits.width(Length::Fixed(column_width)); - let max_width = limits.max().width; - let columns = (max_width / column_width).floor() as usize; - - let layouts = self - .elements - .iter() - .map(|element| element.as_widget().layout(renderer, &column_limits)); - let column_aligns = - std::iter::successors(Some(0.), |width| Some(width + column_width)); - #[allow(clippy::cast_precision_loss)] // TODO: possible precision loss - let grid_width = (columns as f32) * column_width; - - build_grid(columns, column_aligns, layouts, grid_width) - } - } - } - - fn on_event( - &mut self, - state: &mut Tree, - event: Event, - layout: Layout<'_>, - cursor: 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: 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 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 draw( - &self, - state: &Tree, - renderer: &mut Renderer, - theme: &Renderer::Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor: 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 overlay<'b>( - &'b mut self, - tree: &'b mut Tree, - layout: Layout<'_>, - renderer: &Renderer, - ) -> Option> { - overlay::from_children(&mut self.elements, tree, layout, renderer) - } -} - -/// Builds the layout of the [`Grid`]. -fn build_grid( - columns: usize, - column_aligns: impl Iterator + Clone, - layouts: impl Iterator + ExactSizeIterator, - grid_width: f32, -) -> Node { - let mut nodes = Vec::with_capacity(layouts.len()); - let mut grid_height = 0.; - let mut row_height = 0.; - - for ((column, column_align), mut node) in (0..columns).zip(column_aligns).cycle().zip(layouts) { - if column == 0 { - grid_height += row_height; - row_height = 0.; - } - - node.move_to(Point::new(column_align, grid_height)); - row_height = row_height.max(node.size().height); - nodes.push(node); - } - - grid_height += row_height; - - Node::with_children(Size::new(grid_width, grid_height), nodes) -} - -impl<'a, Message, Renderer> From> for Element<'a, Message, Renderer> -where - Renderer: core::Renderer + 'a, - Message: 'static, -{ - fn from(grid: Grid<'a, Message, Renderer>) -> Element<'a, Message, Renderer> { - Element::new(grid) - } -} diff --git a/src/native/grid/grid.rs b/src/native/grid/grid.rs new file mode 100644 index 00000000..5a23e2ae --- /dev/null +++ b/src/native/grid/grid.rs @@ -0,0 +1,362 @@ +//! 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, +}; + +use crate::grid::row::GridRow; + +/// 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`]. + pub fn new() -> Self { + Self::default() + } + + /// Creates a [`Grid`] with the given [`GridRow`]s. + pub fn with_rows(rows: Vec>) -> Self { + Self { + rows, + ..Default::default() + } + } + + /// Adds a [`GridRow`] to the [`Grid`]. + 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`] + 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`] + 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. + 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. + 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 { + 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. + pub fn row_spacing(mut self, spacing: impl Into) -> Self { + self.row_spacing = spacing.into(); + self + } + + /// Sets the spacing between the columns. + 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() + } +} + +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); + } + + // 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.iter() { + 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(); + + 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); + + let grid = Node::with_children(grid_size, nodes); + grid + } + + 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/mod.rs b/src/native/grid/mod.rs new file mode 100644 index 00000000..39a645c5 --- /dev/null +++ b/src/native/grid/mod.rs @@ -0,0 +1,9 @@ +//! Displays a [`Grid`]. +//! +//! *This API requires the following crate features to be activated: grid* + +mod grid; +mod row; + +pub use grid::{Grid, Strategy}; +pub use row::GridRow; diff --git a/src/native/grid/row.rs b/src/native/grid/row.rs new file mode 100644 index 00000000..35780a01 --- /dev/null +++ b/src/native/grid/row.rs @@ -0,0 +1,44 @@ +use iced_widget::core::Element; + +/// 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`]. + pub fn new() -> Self { + Self::default() + } + + /// Creates a new [`GridRow`] with the given widgets. + pub fn with_elements(children: Vec>>) -> Self { + Self { + elements: children.into_iter().map(|child| child.into()).collect(), + } + } + + /// Adds a widget to the [`GridRow`]. + pub fn push(mut self, element: E) -> Self + where + E: Into>, + { + self.elements.push(element.into()); + self + } +} diff --git a/src/native/helpers.rs b/src/native/helpers.rs index a9598e15..376bbf07 100644 --- a/src/native/helpers.rs +++ b/src/native/helpers.rs @@ -7,9 +7,10 @@ use iced_widget::core::{self, Color, Element}; #[allow(unused_imports)] use std::{borrow::Cow, fmt::Display, hash::Hash}; -/// Creates a [`Grid`] with the given children. +/// Creates a [`Grid`] with the given [`GridRow`]s. /// /// [`Grid`]: crate::Grid +/// [`GridRow`]: crate::GridRow #[cfg(feature = "grid")] #[macro_export] macro_rules! grid { @@ -17,7 +18,21 @@ macro_rules! grid { $crate::Grid::new() ); ($($x:expr),+ $(,)?) => ( - $crate::Grid::with_children(vec![$($crate::Element::from($x)),+]) + $crate::Grid::with_rows(vec![$($x),+]) + ); +} + +/// Creates a [`GridRow`] with the given widgets. +/// +/// [`GridRow`]: crate::GridRow +#[cfg(feature = "grid")] +#[macro_export] +macro_rules! grid_row { + () => ( + $crate::GridRow::new() + ); + ($($x:expr),+ $(,)?) => ( + $crate::GridRow::with_elements(vec![$(iced::Element::from($x)),+]) ); } @@ -199,15 +214,30 @@ where #[cfg(feature = "grid")] /// Shortcut helper to create a [`Grid`] Widget. /// +/// [`Grid`]: crate::grid::Grid +#[must_use] +pub fn grid<'a, Message, Renderer>( + rows: Vec>, +) -> crate::Grid<'a, Message, Renderer> +where + Renderer: core::Renderer, +{ + crate::Grid::with_rows(rows) +} + +#[cfg(feature = "grid")] +/// Shortcut helper to create a [`GridRow`] for the [`Grid`] Widget. +/// +/// [`GridRow`]: crate::GridRow /// [`Grid`]: crate::Grid #[must_use] -pub fn grid( - children: Vec>, -) -> crate::Grid +pub fn grid_row<'a, Message, Renderer>( + elements: Vec>>, +) -> crate::GridRow<'a, Message, Renderer> where Renderer: core::Renderer, { - crate::Grid::with_children(children) + crate::GridRow::with_elements(elements) } #[cfg(feature = "wrap")] diff --git a/src/native/mod.rs b/src/native/mod.rs index b680ae1b..92860114 100644 --- a/src/native/mod.rs +++ b/src/native/mod.rs @@ -60,7 +60,10 @@ pub type FloatingElement<'a, Message, Renderer> = pub mod grid; #[cfg(feature = "grid")] /// A container that distributes its contents in a grid. -pub type Grid<'a, Message, Renderer> = grid::Grid<'a, Message, Renderer>; +// pub type Grid<'a, Message, Renderer> = grid::Grid<'a, Message, Renderer>; +pub use grid::Grid; +#[cfg(feature = "grid")] +pub use grid::GridRow; #[cfg(feature = "grid")] pub use grid::Strategy; From b0285df107f3a5ca7f70ce61045be46e3cf7619a Mon Sep 17 00:00:00 2001 From: Alexander van Saase Date: Wed, 11 Oct 2023 20:58:44 +0200 Subject: [PATCH 2/4] Fix clippy lints --- src/native/{grid => }/grid.rs | 70 +++++++++++++++++++++++++++++++---- src/native/grid/mod.rs | 9 ----- src/native/grid/row.rs | 44 ---------------------- src/native/helpers.rs | 6 +-- 4 files changed, 65 insertions(+), 64 deletions(-) rename src/native/{grid => }/grid.rs (87%) delete mode 100644 src/native/grid/mod.rs delete mode 100644 src/native/grid/row.rs diff --git a/src/native/grid/grid.rs b/src/native/grid.rs similarity index 87% rename from src/native/grid/grid.rs rename to src/native/grid.rs index 5a23e2ae..bae2740b 100644 --- a/src/native/grid/grid.rs +++ b/src/native/grid.rs @@ -11,8 +11,6 @@ use iced_widget::core::{ Clipboard, Element, Event, Layout, Length, Pixels, Point, Rectangle, Shell, Size, Widget, }; -use crate::grid::row::GridRow; - /// 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. @@ -49,11 +47,13 @@ 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, @@ -62,6 +62,7 @@ where } /// Adds a [`GridRow`] to the [`Grid`]. + #[must_use] pub fn push(mut self, row: GridRow<'a, Message, Renderer>) -> Self { self.rows.push(row); self @@ -69,6 +70,7 @@ where /// 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 @@ -76,18 +78,21 @@ where /// 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 @@ -95,6 +100,7 @@ where /// 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; @@ -103,12 +109,14 @@ where } /// 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 @@ -139,6 +147,52 @@ where } } +/// 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, @@ -161,13 +215,14 @@ where 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.iter() { + 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 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 { @@ -198,7 +253,7 @@ where }; let cell_size = Size::new(col_width, row_height); - let mut node = element.as_widget().layout(renderer, &limits); + let mut node = element.as_widget().layout(renderer, limits); node.move_to(Point::new(x, y)); node.align( self.horizontal_alignment.into(), @@ -219,8 +274,7 @@ where let grid_size = Size::new(x, y); - let grid = Node::with_children(grid_size, nodes); - grid + Node::with_children(grid_size, nodes) } fn draw( @@ -249,7 +303,7 @@ where } fn diff(&self, tree: &mut Tree) { - tree.diff_children(&self.elements_iter().collect::>()) + tree.diff_children(&self.elements_iter().collect::>()); } fn operate( diff --git a/src/native/grid/mod.rs b/src/native/grid/mod.rs deleted file mode 100644 index 39a645c5..00000000 --- a/src/native/grid/mod.rs +++ /dev/null @@ -1,9 +0,0 @@ -//! Displays a [`Grid`]. -//! -//! *This API requires the following crate features to be activated: grid* - -mod grid; -mod row; - -pub use grid::{Grid, Strategy}; -pub use row::GridRow; diff --git a/src/native/grid/row.rs b/src/native/grid/row.rs deleted file mode 100644 index 35780a01..00000000 --- a/src/native/grid/row.rs +++ /dev/null @@ -1,44 +0,0 @@ -use iced_widget::core::Element; - -/// 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`]. - pub fn new() -> Self { - Self::default() - } - - /// Creates a new [`GridRow`] with the given widgets. - pub fn with_elements(children: Vec>>) -> Self { - Self { - elements: children.into_iter().map(|child| child.into()).collect(), - } - } - - /// Adds a widget to the [`GridRow`]. - pub fn push(mut self, element: E) -> Self - where - E: Into>, - { - self.elements.push(element.into()); - self - } -} diff --git a/src/native/helpers.rs b/src/native/helpers.rs index 376bbf07..5afc9d91 100644 --- a/src/native/helpers.rs +++ b/src/native/helpers.rs @@ -216,9 +216,9 @@ where /// /// [`Grid`]: crate::grid::Grid #[must_use] -pub fn grid<'a, Message, Renderer>( - rows: Vec>, -) -> crate::Grid<'a, Message, Renderer> +pub fn grid( + rows: Vec>, +) -> crate::Grid<'_, Message, Renderer> where Renderer: core::Renderer, { From a150fb07920f329afbda9f14ed8dbc1573148ad8 Mon Sep 17 00:00:00 2001 From: Alexander van Saase Date: Wed, 11 Oct 2023 22:29:54 +0200 Subject: [PATCH 3/4] Fix clippy lints in example --- examples/grid/src/main.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/grid/src/main.rs b/examples/grid/src/main.rs index 4104398a..174ae2ce 100644 --- a/examples/grid/src/main.rs +++ b/examples/grid/src/main.rs @@ -86,9 +86,9 @@ impl Sandbox for App { let row_height_radio = column( STRATEGIES .iter() - .map(|strat| { - let name = strategy_to_string(&strat); - radio(name, strat, Some(&self.row_strategy), |click| { + .map(|strategy| { + let name = strategy_to_string(strategy); + radio(name, strategy, Some(&self.row_strategy), |click| { Message::RowStrategy(click.clone()) }) }) @@ -99,9 +99,9 @@ impl Sandbox for App { let col_width_radio = row(STRATEGIES .iter() - .map(|strat| { - let name = strategy_to_string(&strat); - radio(name, strat, Some(&self.column_strategy), |click| { + .map(|strategy| { + let name = strategy_to_string(strategy); + radio(name, strategy, Some(&self.column_strategy), |click| { Message::ColumnStrategy(click.clone()) }) }) From 76c014440b5b380dad96392fa1c640fd491c41c3 Mon Sep 17 00:00:00 2001 From: Alexander van Saase Date: Wed, 11 Oct 2023 22:45:55 +0200 Subject: [PATCH 4/4] Fix child with Fill width pushing grid from frame --- src/native/grid.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/native/grid.rs b/src/native/grid.rs index bae2740b..29e965ef 100644 --- a/src/native/grid.rs +++ b/src/native/grid.rs @@ -210,6 +210,8 @@ where 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()); @@ -219,7 +221,7 @@ where let mut row_height = 0.0f32; for (col_idx, element) in row.elements.iter().enumerate() { - let layout = element.as_widget().layout(renderer, limits); + let layout = element.as_widget().layout(renderer, &limits); let Size { width, height } = layout.size(); #[allow(clippy::option_if_let_else)] @@ -253,7 +255,7 @@ where }; let cell_size = Size::new(col_width, row_height); - let mut node = element.as_widget().layout(renderer, limits); + let mut node = element.as_widget().layout(renderer, &limits); node.move_to(Point::new(x, y)); node.align( self.horizontal_alignment.into(),