From 34f32a7fa45ff8b2975c2b6132168d3e375f6f98 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 26 Sep 2023 01:20:55 +0300 Subject: [PATCH 01/16] ContextMenu respects window borders and resizing --- src/native/overlay/context_menu.rs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/native/overlay/context_menu.rs b/src/native/overlay/context_menu.rs index 70565388..1f91f934 100644 --- a/src/native/overlay/context_menu.rs +++ b/src/native/overlay/context_menu.rs @@ -12,7 +12,7 @@ use iced_widget::core::{ mouse::{self, Cursor}, overlay, renderer, touch, widget::tree::Tree, - Clipboard, Color, Element, Event, Layout, Point, Rectangle, Shell, Size, + window, Clipboard, Color, Element, Event, Layout, Point, Rectangle, Shell, Size, }; /// The overlay of the [`ContextMenu`](crate::native::ContextMenu). @@ -75,6 +75,16 @@ where let max_size = limits.max(); let mut content = self.content.as_widget().layout(renderer, &limits); + + // Try to stay inside borders + let mut position = position; + if position.x + content.size().width > bounds.width { + position.x = f32::max(0.0, position.x - content.size().width); + } + if position.y + content.size().height > bounds.height { + position.y = f32::max(0.0, position.y - content.size().height); + } + content.move_to(position); Node::with_children(max_size, vec![content]) @@ -166,6 +176,11 @@ where } } + Event::Window(window::Event::Resized { .. }) => { + self.state.show = false; + Status::Captured + } + _ => Status::Ignored, }; From e19c67cf908679944633e5a42e972f699ee0ae51 Mon Sep 17 00:00:00 2001 From: Joel Aschmann Date: Sat, 30 Sep 2023 17:01:31 +0200 Subject: [PATCH 02/16] Update event handling for context-menu overlay --- src/native/overlay/context_menu.rs | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/src/native/overlay/context_menu.rs b/src/native/overlay/context_menu.rs index 1f91f934..34ee7911 100644 --- a/src/native/overlay/context_menu.rs +++ b/src/native/overlay/context_menu.rs @@ -144,10 +144,13 @@ where .next() .expect("Native: Layout should have a content layout."); + let mut forward_event_to_children = true; + let status = match event { Event::Keyboard(keyboard::Event::KeyPressed { key_code, .. }) => { if key_code == keyboard::KeyCode::Escape { self.state.show = false; + forward_event_to_children = false; Status::Captured } else { Status::Ignored @@ -158,34 +161,30 @@ where mouse::Button::Left | mouse::Button::Right, )) | Event::Touch(touch::Event::FingerPressed { .. }) => { - if cursor.is_over(layout_children.bounds()) { - Status::Ignored - } else { + if !cursor.is_over(layout_children.bounds()) { self.state.show = false; - Status::Captured + forward_event_to_children = false; } + Status::Captured } Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => { // close when released because because button send message on release self.state.show = false; - if cursor.is_over(layout_children.bounds()) { - Status::Ignored - } else { - Status::Captured - } + Status::Captured } Event::Window(window::Event::Resized { .. }) => { self.state.show = false; + forward_event_to_children = false; Status::Captured } _ => Status::Ignored, }; - match status { - Status::Ignored => self.content.as_widget_mut().on_event( + let child_status = if forward_event_to_children { + self.content.as_widget_mut().on_event( self.tree, event, layout_children, @@ -194,7 +193,13 @@ where clipboard, shell, &layout.bounds(), - ), + ) + } else { + Status::Ignored + }; + + match child_status { + Status::Ignored => status, Status::Captured => Status::Captured, } } From 5a58637724f9e3878ca4a0ea763328afd18b3f04 Mon Sep 17 00:00:00 2001 From: dave-wathen Date: Thu, 5 Oct 2023 22:51:48 +0100 Subject: [PATCH 03/16] Set tag for MenuBar --- src/native/menu/menu_bar.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/native/menu/menu_bar.rs b/src/native/menu/menu_bar.rs index 0b2d4762..0e4a19fb 100644 --- a/src/native/menu/menu_bar.rs +++ b/src/native/menu/menu_bar.rs @@ -242,6 +242,10 @@ where } } + fn tag(&self) -> tree::Tag { + tree::Tag::of::() + } + fn state(&self) -> tree::State { tree::State::new(MenuBarState::default()) } From 475544ac6d66368f6ea5cdcbc6b874727835b90f Mon Sep 17 00:00:00 2001 From: ojji Date: Sun, 8 Oct 2023 13:17:16 +0200 Subject: [PATCH 04/16] fix broken layout in the `wrap` example --- examples/wrap/src/main.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/wrap/src/main.rs b/examples/wrap/src/main.rs index 2d1ee4dc..b3b1650c 100644 --- a/examples/wrap/src/main.rs +++ b/examples/wrap/src/main.rs @@ -173,7 +173,8 @@ impl Sandbox for RandStrings { .push(line_spacing_input) .push(line_minimal_length_input) .height(iced::Length::Shrink) - .align_items(iced::Alignment::Center); + .align_items(iced::Alignment::Center) + .width(iced::Length::Fixed(130.0)); Row::new().push(ctrls).push(vertcal).push(horizontal).into() } From ecf8aa6f7d1cc4e7ca7f6ed1bee4b793a3288f5b Mon Sep 17 00:00:00 2001 From: ojji Date: Sun, 8 Oct 2023 13:17:48 +0200 Subject: [PATCH 05/16] refactor typo --- examples/wrap/src/main.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/wrap/src/main.rs b/examples/wrap/src/main.rs index b3b1650c..cf20908b 100644 --- a/examples/wrap/src/main.rs +++ b/examples/wrap/src/main.rs @@ -113,7 +113,7 @@ impl Sandbox for RandStrings { let RandStrings { vbuttons, hbuttons, .. } = self; - let vertcal = Container::new( + let vertical = Container::new( vbuttons .iter() .fold(Wrap::new_vertical(), |wrap, button| { @@ -176,6 +176,6 @@ impl Sandbox for RandStrings { .align_items(iced::Alignment::Center) .width(iced::Length::Fixed(130.0)); - Row::new().push(ctrls).push(vertcal).push(horizontal).into() + Row::new().push(ctrls).push(vertical).push(horizontal).into() } } From 8bc13f886f3deae029e9efa470b1f777d8f536c1 Mon Sep 17 00:00:00 2001 From: ojji Date: Mon, 9 Oct 2023 07:52:06 +0200 Subject: [PATCH 06/16] change NumberInput width from Fill to Shrink --- examples/wrap/src/main.rs | 3 +-- src/native/number_input.rs | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/examples/wrap/src/main.rs b/examples/wrap/src/main.rs index cf20908b..5611476b 100644 --- a/examples/wrap/src/main.rs +++ b/examples/wrap/src/main.rs @@ -173,8 +173,7 @@ impl Sandbox for RandStrings { .push(line_spacing_input) .push(line_minimal_length_input) .height(iced::Length::Shrink) - .align_items(iced::Alignment::Center) - .width(iced::Length::Fixed(130.0)); + .align_items(iced::Alignment::Center); Row::new().push(ctrls).push(vertical).push(horizontal).into() } diff --git a/src/native/number_input.rs b/src/native/number_input.rs index e97377c2..7590a2bc 100644 --- a/src/native/number_input.rs +++ b/src/native/number_input.rs @@ -125,7 +125,7 @@ where on_change: Box::new(on_changed), style: ::Style::default(), font: Renderer::Font::default(), - width: Length::Fill, + width: Length::Shrink, } } From 025380f2b9a4b53e9aae5fe51291d0c4d44f2e99 Mon Sep 17 00:00:00 2001 From: Alexander van Saase Date: Wed, 11 Oct 2023 20:23:34 +0200 Subject: [PATCH 07/16] 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 08/16] 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 09/16] 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 10/16] 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(), From 393a4bc6378eb2a2720f2f7e70b419220813be5d Mon Sep 17 00:00:00 2001 From: ojji Date: Thu, 12 Oct 2023 08:43:29 +0200 Subject: [PATCH 11/16] make formatting bot happy --- examples/wrap/src/main.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/examples/wrap/src/main.rs b/examples/wrap/src/main.rs index 5611476b..2f166d3b 100644 --- a/examples/wrap/src/main.rs +++ b/examples/wrap/src/main.rs @@ -175,6 +175,10 @@ impl Sandbox for RandStrings { .height(iced::Length::Shrink) .align_items(iced::Alignment::Center); - Row::new().push(ctrls).push(vertical).push(horizontal).into() + Row::new() + .push(ctrls) + .push(vertical) + .push(horizontal) + .into() } } From c69f04d5f31046b6d555f0f01e6c65eb1a28bdc7 Mon Sep 17 00:00:00 2001 From: Alexander van Saase Date: Fri, 20 Oct 2023 15:39:22 +0200 Subject: [PATCH 12/16] 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; From db6afd3a7f0ef4ed5f6dc1c3dbf9b9602327b1a9 Mon Sep 17 00:00:00 2001 From: Alexander van Saase Date: Sat, 21 Oct 2023 00:27:44 +0200 Subject: [PATCH 13/16] Fix padding --- src/native/grid/layout.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/native/grid/layout.rs b/src/native/grid/layout.rs index b9e902f7..1bde7e27 100644 --- a/src/native/grid/layout.rs +++ b/src/native/grid/layout.rs @@ -52,7 +52,7 @@ where .min_height(min_size.height) .width(width) .height(height); - let grid_size = grid_limits.fill().max(min_size); + let grid_size = grid_limits.fill(); // Allocate the available space let available_width = grid_size.width - total_spacing(column_count, column_spacing); @@ -71,6 +71,7 @@ where vertical_alignment, column_spacing, row_spacing, + padding, grid_size, ) } @@ -168,15 +169,16 @@ fn create_grid_layout( vertical_alignment: Vertical, column_spacing: Pixels, row_spacing: Pixels, + padding: Padding, grid_size: Size, ) -> Node where Renderer: iced_widget::core::Renderer, { - let mut y = 0.0; + let mut y = padding.top; 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; + let mut x = padding.left; for (col_position, (element, &column_width)) in row.elements.iter().zip(column_widths).with_position() { @@ -207,7 +209,7 @@ where } } - Node::with_children(grid_size, nodes) + Node::with_children(grid_size.pad(padding), nodes) } fn not_last(position: Position) -> bool { From 806eb7d72ca8f2242d47323e9457da83576c7f66 Mon Sep 17 00:00:00 2001 From: bq-wrongway Date: Mon, 23 Oct 2023 19:40:01 +0200 Subject: [PATCH 14/16] Added segmented button,segmented button style, and example --- Cargo.toml | 3 + examples/segmented_button/Cargo.toml | 14 + examples/segmented_button/resources/dl.svg | 1 + .../segmented_button/resources/file_add.png | Bin 0 -> 731 bytes examples/segmented_button/resources/heart.svg | 4 + examples/segmented_button/resources/ico.svg | 1 + examples/segmented_button/resources/light.svg | 4 + examples/segmented_button/src/main.rs | 104 +++++++ src/native/mod.rs | 7 + src/native/segmented_button.rs | 286 ++++++++++++++++++ src/style/mod.rs | 5 + src/style/segmented_button.rs | 98 ++++++ 12 files changed, 527 insertions(+) create mode 100644 examples/segmented_button/Cargo.toml create mode 100644 examples/segmented_button/resources/dl.svg create mode 100644 examples/segmented_button/resources/file_add.png create mode 100644 examples/segmented_button/resources/heart.svg create mode 100644 examples/segmented_button/resources/ico.svg create mode 100644 examples/segmented_button/resources/light.svg create mode 100644 examples/segmented_button/src/main.rs create mode 100644 src/native/segmented_button.rs create mode 100644 src/style/segmented_button.rs diff --git a/Cargo.toml b/Cargo.toml index 18468324..29a46688 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,7 @@ menu = [] quad = [] spinner = [] context_menu = [] +segmented_button = [] default = [ "badge", @@ -59,6 +60,7 @@ default = [ "context_menu", "spinner", "cupertino", + "segmented_button", ] [dependencies] @@ -105,6 +107,7 @@ members = [ "examples/spinner", "examples/context_menu", "examples/WidgetIDReturn", + "examples/segmented_button", ] [workspace.dependencies.iced] diff --git a/examples/segmented_button/Cargo.toml b/examples/segmented_button/Cargo.toml new file mode 100644 index 00000000..fcc534f0 --- /dev/null +++ b/examples/segmented_button/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "segmented_button" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + + +[dependencies] +iced_aw = { workspace = true, features = [ + "segmented_button", + "icons", +] } +iced.workspace = true \ No newline at end of file diff --git a/examples/segmented_button/resources/dl.svg b/examples/segmented_button/resources/dl.svg new file mode 100644 index 00000000..ea276a4e --- /dev/null +++ b/examples/segmented_button/resources/dl.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/segmented_button/resources/file_add.png b/examples/segmented_button/resources/file_add.png new file mode 100644 index 0000000000000000000000000000000000000000..31284f0fbba473fbbd21aa220a019eb252a5cbcc GIT binary patch literal 731 zcmV<10wn#3P)20@74d;s<8L5%qE2x5># zeSzFOh#tha;Ph#xi4$+< z4(n)1=Nv>dV+2=rT!KFhl4Re=fD{mh=_fGVQLu7oyqnHVVd%+7fTVINAmZ-Df!9TJ zseq}JnQf3Xv5TD0NbJ%F6Vp_YGo=NTvYhBACn)nF495)9s(#-Vyy+1H7xG_p zx~>m~@qRw%wKovGaMS&@I@R@|rPVBx=9WJYp>OE{x7a!I?=xY*yREZJFOTM*{qP0? zjN5w!Im6G6<$3nSBS==U_9CtE7|-YBd&4KltMnNW&D8b5Nbg}DkvVT5ZKfNU6Sz{h z{6c?HXhU(5jx;de~`c)}htOFg+-8B8`MX zly%C!%m_^yrq7k2f}53c@@<;55ch7UeV7&k#_hZVST{`bTAlhO_8;mA{K6w$fUN)k N002ovPDHLkV1fv%Nap|m literal 0 HcmV?d00001 diff --git a/examples/segmented_button/resources/heart.svg b/examples/segmented_button/resources/heart.svg new file mode 100644 index 00000000..2f2c8b89 --- /dev/null +++ b/examples/segmented_button/resources/heart.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/examples/segmented_button/resources/ico.svg b/examples/segmented_button/resources/ico.svg new file mode 100644 index 00000000..901ad80a --- /dev/null +++ b/examples/segmented_button/resources/ico.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/segmented_button/resources/light.svg b/examples/segmented_button/resources/light.svg new file mode 100644 index 00000000..6d3a91b3 --- /dev/null +++ b/examples/segmented_button/resources/light.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/examples/segmented_button/src/main.rs b/examples/segmented_button/src/main.rs new file mode 100644 index 00000000..fe7e63bb --- /dev/null +++ b/examples/segmented_button/src/main.rs @@ -0,0 +1,104 @@ +use iced::widget::container; +use iced::{Element, Length, Sandbox, Settings}; +use iced::widget::{column, row, text}; + +use iced_aw::native::segmented_button; +use segmented_button::SegmentedButton; + +pub fn main() -> iced::Result { + Example::run(Settings::default()) +} + +#[derive(Default)] +struct Example { + selected_radio: Option, +} + +#[derive(Debug, Clone, Copy)] +enum Message { + RadioSelected(Choice), +} + +impl Sandbox for Example { + type Message = Message; + + fn new() -> Self { + Self { + selected_radio: Some(Choice::A), + } + } + + fn title(&self) -> String { + String::from("Radio - Iced") + } + + fn update(&mut self, message: Message) { + match message { + Message::RadioSelected(value) => { + self.selected_radio = Some(value); + } + } + } + + fn view(&self) -> Element { + // let selected_radio = Some(Choice::A); + + // i added a row just to demonstrate that anything can be used as a child, + //in this case instead of A B C you might add icons + let a = SegmentedButton::new( + row!(text("HEAVY "), "A"), + Choice::A, + self.selected_radio, + Message::RadioSelected, + ); + + + let b = SegmentedButton::new( + row!(text("MEDIUM "), "B"), + Choice::B, + self.selected_radio, + Message::RadioSelected, + ); + + let c = SegmentedButton::new( + row!(text("LIGHT "), "C"), + Choice::C, + self.selected_radio, + Message::RadioSelected, + ); + let content = column![ + row![a, b, c], + text(self.selected_radio.unwrap().to_string()) + ] + .align_items(iced::Alignment::Center); + + container(content) + .width(Length::Fill) + .height(Length::Fill) + .center_x() + .center_y() + .into() + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum Choice { + #[default] + A, + B, + C, +} + +impl std::fmt::Display for Choice { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + Choice::A => "A", + Choice::B => "B", + Choice::C => "C", + } + ) + } +} diff --git a/src/native/mod.rs b/src/native/mod.rs index 4ba345bf..02e39316 100644 --- a/src/native/mod.rs +++ b/src/native/mod.rs @@ -125,3 +125,10 @@ pub mod context_menu; /// A context menu pub type ContextMenu<'a, Overlay, Message, Renderer> = context_menu::ContextMenu<'a, Overlay, Message, Renderer>; + + +#[cfg(feature = "segmented_button")] +pub mod segmented_button; +#[cfg(feature = "segmented_button")] +/// A badge for color highlighting small information. +pub type SegmentedButton<'a, Message, Renderer> = segmented_button::SegmentedButton<'a, Message, Renderer>; \ No newline at end of file diff --git a/src/native/segmented_button.rs b/src/native/segmented_button.rs new file mode 100644 index 00000000..6f9e6bd2 --- /dev/null +++ b/src/native/segmented_button.rs @@ -0,0 +1,286 @@ +//! Create choices using segnmented_button buttons. +use iced_widget::core::{ + event, + layout::{Limits, Node}, + mouse::{self, Cursor}, + renderer, touch, + widget::Tree, + Alignment, Background, Clipboard, Color, Element, Event, Layout, Length, Padding, Point, + Rectangle, Shell, Widget, +}; + +pub use crate::style::segmented_button::StyleSheet; + + + +/// A segmented_button for color highlighting small information. +/// +/// # Example +/// ```ignore +/// # use iced::widget::Text; +/// # use iced_aw::SegmentedButton; +/// # +/// #[derive(Debug, Clone)] +/// enum Message { +/// } +/// +/// let segmented_button = SegmentedButton::::new(Text::new("Text")); +/// ``` +#[allow(missing_debug_implementations)] +pub struct SegmentedButton<'a, Message, Renderer> +where + Renderer: iced_widget::core::Renderer, + Renderer::Theme: StyleSheet, +{ + is_selected: bool, + on_click: Message, + /// The padding of the [`SegmentedButton`]. + padding: Padding, + /// The width of the [`SegmentedButton`]. + width: Length, + /// The height of the [`SegmentedButton`]. + height: Length, + /// The horizontal alignment of the [`SegmentedButton`] + horizontal_alignment: Alignment, + /// The vertical alignment of the [`SegmentedButton`] + vertical_alignment: Alignment, + /// The style of the [`SegmentedButton`] + style: ::Style, + /// The content [`Element`] of the [`SegmentedButton`] + content: Element<'a, Message, Renderer>, +} + +impl<'a, Message, Renderer> SegmentedButton<'a, Message, Renderer> +where + Renderer: iced_widget::core::Renderer, + Renderer::Theme: StyleSheet, +{ + /// Creates a new [`SegmentedButton`](SegmentedButton) with the given content. + /// + /// It expects: + /// * the content [`Element`] to display in the [`SegmentedButton`](SegmentedButton). + pub fn new(content: T, value: V, selected: Option, f: F) -> Self + where + T: Into>, + V: Eq + Copy, + F: FnOnce(V) -> Message, + { + SegmentedButton { + is_selected: Some(value) == selected, + on_click: f(value), + padding: Padding::new(3.0), + width: Length::Shrink, + height: Length::Shrink, + horizontal_alignment: Alignment::Center, + vertical_alignment: Alignment::Center, + style: ::Style::default(), + content: content.into(), + } + } + + /// Sets the padding of the [`SegmentedButton`](SegmentedButton). + pub fn padding(mut self, units: Padding) -> Self { + self.padding = units; + self + } + + /// Sets the width of the [`SegmentedButton`](SegmentedButton). + pub fn width(mut self, width: Length) -> Self { + self.width = width; + self + } + + /// Sets the height of the [`SegmentedButton`](SegmentedButton). + pub fn height(mut self, height: Length) -> Self { + self.height = height; + self + } + + /// Sets the horizontal alignment of the content of the [`SegmentedButton`](SegmentedButton). + pub fn align_x(mut self, alignment: Alignment) -> Self { + self.horizontal_alignment = alignment; + self + } + + /// Sets the vertical alignment of the content of the [`SegmentedButton`](SegmentedButton). + pub fn align_y(mut self, alignment: Alignment) -> Self { + self.vertical_alignment = alignment; + self + } + + /// Sets the style of the [`SegmentedButton`](SegmentedButton). + pub fn style(mut self, style: ::Style) -> Self { + self.style = style; + self + } +} + +impl<'a, Message, Renderer> Widget for SegmentedButton<'a, Message, Renderer> +where + Message: 'a + Clone, + Renderer: 'a + iced_widget::core::Renderer, + Renderer::Theme: StyleSheet, +{ + fn children(&self) -> Vec { + vec![Tree::new(&self.content)] + } + + fn diff(&self, tree: &mut Tree) { + tree.diff_children(std::slice::from_ref(&self.content)); + } + + fn width(&self) -> Length { + self.width + } + + fn height(&self) -> Length { + self.height + } + + fn layout(&self, renderer: &Renderer, limits: &Limits) -> Node { + let padding = self.padding.into(); + let limits = limits + .loose() + .width(self.width) + .height(self.height) + .pad(padding); + + let mut content = self.content.as_widget().layout(renderer, &limits.loose()); + let size = limits.resolve(content.size()); + + content.move_to(Point::new(padding.left, padding.top)); + content.align(self.horizontal_alignment, self.vertical_alignment, size); + + Node::with_children(size.pad(padding), vec![content]) + } + + 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 { + match event { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) => { + if cursor.is_over(layout.bounds()) { + shell.publish(self.on_click.clone()); + + return event::Status::Captured; + } + } + _ => {} + } + + event::Status::Ignored + } + + fn mouse_interaction( + &self, + _state: &Tree, + layout: Layout<'_>, + cursor: Cursor, + _viewport: &Rectangle, + _renderer: &Renderer, + ) -> mouse::Interaction { + if cursor.is_over(layout.bounds()) { + mouse::Interaction::Pointer + } else { + mouse::Interaction::default() + } + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Renderer::Theme, + _style: &renderer::Style, + layout: Layout<'_>, + cursor: Cursor, + viewport: &Rectangle, + ) { + let bounds = layout.bounds(); + let mut children = layout.children(); + let is_mouse_over = bounds.contains(cursor.position().unwrap_or_default()); + let style_sheet = if is_mouse_over { + theme.hovered(&self.style) + } else { + theme.active(&self.style) + }; + + renderer.fill_quad( + renderer::Quad { + bounds, + border_radius: 2.0.into(), + border_width: style_sheet.border_width, + border_color: style_sheet.border_color.unwrap_or(Color::BLACK), + }, + style_sheet.background, + ); + if self.is_selected { + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: bounds.x, + y: bounds.y, + width: bounds.width, + height: bounds.height, + }, + border_radius: 2.0.into(), + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + style_sheet.selected_color, + ); + } + //just for the testing as of now needs to clearup and make styling based of basecolor + if is_mouse_over && !self.is_selected { + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: bounds.x, + y: bounds.y, + width: bounds.width, + height: bounds.height, + }, + border_radius: 2.0.into(), + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + Background::Color([0.0, 0.0, 0.0, 0.5].into()), + ); + } + + self.content.as_widget().draw( + &tree.children[0], + renderer, + theme, + &renderer::Style { + text_color: style_sheet.text_color, + }, + children + .next() + .expect("Graphics: Layout should have a children layout for SegmentedButton"), + cursor, + viewport, + ); + } +} + +impl<'a, Message, Renderer> From> + for Element<'a, Message, Renderer> +where + Message: 'a + Clone, + Renderer: 'a + iced_widget::core::Renderer, + Renderer::Theme: StyleSheet, +{ + fn from(segmented_button: SegmentedButton<'a, Message, Renderer>) -> Self { + Self::new(segmented_button) + } +} diff --git a/src/style/mod.rs b/src/style/mod.rs index 241d2f92..4e01c332 100644 --- a/src/style/mod.rs +++ b/src/style/mod.rs @@ -61,3 +61,8 @@ pub use spinner::SpinnerStyle; pub mod context_menu; #[cfg(feature = "context_menu")] pub use context_menu::ContextMenuStyle; + +#[cfg(feature = "segmented_button")] +pub mod segmented_button; +#[cfg(feature = "segmented_button")] +pub use segmented_button::SegmentedButton; diff --git a/src/style/segmented_button.rs b/src/style/segmented_button.rs new file mode 100644 index 00000000..cf3faeec --- /dev/null +++ b/src/style/segmented_button.rs @@ -0,0 +1,98 @@ +//! Use a segmented_button as an alternative to radio button. + +use iced_widget::{ + core::{Background, Color}, + style::Theme, +}; +/// The appearance of a [`SegmentedButton`] +#[derive(Clone, Copy, Debug)] +pub struct Appearance { + /// The background of the [`SegmentedButton`] + pub background: Background, + /// selection hightlight color + pub selected_color: Color, + + /// The border radius of the [`SegmentedButton`] + /// If no radius is specified the default one will be used. + pub border_radius: Option, + + /// The border with of the [`SegmentedButton`] + pub border_width: f32, + + /// The border color of the [`SegmentedButton`] + pub border_color: Option, + + /// The default text color of the [`SegmentedButton`] + pub text_color: Color, +} + +/// The appearance of a [`SegmentedButton`] +pub trait StyleSheet { + ///Style for the trait to use. + type Style: Default; + /// The normal appearance of a [`SegmentedButton`](crate::native::segmented_button::SegmentedButton). + fn active(&self, style: &Self::Style) -> Appearance; + + /// The appearance when the [`SegmentedButton`] + fn hovered(&self, style: &Self::Style) -> Appearance { + let active = self.active(style); + + Appearance { + background: Background::Color([0.33, 0.87, 0.33].into()), + selected_color: Color::from_rgb(0.208, 0.576, 0.961), + ..active + } + } +} + +impl std::default::Default for Appearance { + fn default() -> Self { + Self { + background: Background::Color([0.87, 0.87, 0.87].into()), + selected_color: Color::from_rgb( + 0x5E as f32 / 255.0, + 0x7C as f32 / 255.0, + 0xE2 as f32 / 255.0, + ), + border_radius: None, + border_width: 1.0, + border_color: Some([0.8, 0.8, 0.8].into()), + text_color: Color::BLACK, + } + } +} + +#[derive(Default)] +#[allow(missing_docs, clippy::missing_docs_in_private_items)] +/// Default ``SegmentedButton`` Styles +pub enum SegmentedButton { + #[default] + Default, + Custom(Box>), +} + +impl SegmentedButton { + /// Creates a custom [`SegmentedButtonStyles`] style variant. + pub fn custom(style_sheet: impl StyleSheet