diff --git a/src/widget/flex_row.rs b/src/widget/flex_row.rs deleted file mode 100644 index 33bd3bb1..00000000 --- a/src/widget/flex_row.rs +++ /dev/null @@ -1,137 +0,0 @@ -// Copyright 2023 System76 -// SPDX-License-Identifier: MPL-2.0 - -use crate::ext::CollectionWidget; -use crate::widget::{column, row}; -use crate::Element; -use apply::Apply; -use derive_setters::Setters; -use iced_core::{alignment, Length, Size}; -use std::cell::RefCell; - -/// Responsively generates rows and columns of widgets based on its dimmensions. -#[derive(Setters)] -pub struct FlexRow<'a, Message> { - #[allow(clippy::type_complexity)] - #[setters(skip)] - generator: Box>, Size) -> u16 + 'a>, - /// Sets the space between each column of items. - column_spacing: u16, - /// Sets the space between each item in a row. - row_spacing: u16, - /// Sets the max number of items per row. - max_items: Option, - /// Sets the horizontal alignment of the [`FlexRow`]. - align_x: alignment::Horizontal, - /// Sets the vertical alignment of the [`FlexRow`]. - align_y: alignment::Vertical, - /// Sets the width of the [`FlexRow`]. - width: Length, - /// Sets the height of the [`FlexRow`]. - height: Length, -} - -/// Responsively generates rows and columns of widgets based on its dimmensions. -/// -/// The `generator` input is a closure which must return the max width of all -/// elements created, while storing elements in the provided `Vec`. -/// -/// ## Example -/// -/// Suppose that there is a `COLOR_VALUE` variable which contains an array of -/// color values, and a `color_button` function which creates an `Element` from -/// a color. -/// -/// We already know beforehand that our color buttons will have a fixed width -/// of `70`, so we store elements in the provided `Vec` and return `70`. -/// -/// ```ignore -/// use iced_core::{alignment, Length}; -/// -/// let flex_row = cosmic::widget::flex_row(|vec, _size| { -/// let elements = DEFAULT_COLORS -/// .iter() -/// .cloned() -/// .map(color_button); -/// -/// vec.extend(elements); -/// -/// 70 -/// }); -/// -/// flex_row -/// .column_spacing(12) -/// .row_spacing(16) -/// .width(Length::Fill) -/// .align_x(alignment::Horizontal::Center) -/// .into() -/// ``` -pub fn flex_row<'a, Message: 'static>( - generator: impl Fn(&mut Vec>, Size) -> u16 + 'a, -) -> FlexRow<'a, Message> { - FlexRow { - generator: Box::new(generator), - column_spacing: 4, - row_spacing: 4, - max_items: None, - align_x: alignment::Horizontal::Left, - align_y: alignment::Vertical::Top, - width: Length::Shrink, - height: Length::Shrink, - } -} - -impl<'a, Message: 'static> From> for Element<'a, Message> { - fn from(container: FlexRow<'a, Message>) -> Self { - let elements = RefCell::new(Vec::new()); - - iced::widget::responsive(move |size| { - let mut elements = elements.borrow_mut(); - let item_width = (container.generator)(&mut elements, size); - - let mut items_per_row = flex_row_items( - size.width, - f32::from(item_width), - f32::from(container.row_spacing), - ) as usize; - - if let Some(max_items) = container.max_items { - items_per_row = items_per_row.max(max_items as usize); - } - - let mut elements_column = Vec::with_capacity(elements.len() / items_per_row); - - let mut iterator = elements.drain(..); - - while let Some(element) = iterator.next() { - let elements_row = row::with_capacity(items_per_row) - .spacing(container.row_spacing) - .push(element) - .extend(iterator.by_ref().take(items_per_row - 1)); - - elements_column.push(elements_row.into()); - } - - column::with_children(elements_column) - .spacing(container.column_spacing) - .apply(iced::widget::container) - .align_x(container.align_x) - .align_y(container.align_y) - .width(container.width) - .height(container.height) - .into() - }) - .into() - } -} - -#[allow(clippy::cast_precision_loss)] -fn flex_row_items(available: f32, item_width: f32, spacing: f32) -> u32 { - let mut items = 2; - - while available >= (item_width + spacing) * items as f32 - spacing { - items += 1; - } - - items - 1 -} diff --git a/src/widget/flex_row/layout.rs b/src/widget/flex_row/layout.rs new file mode 100644 index 00000000..5227cb84 --- /dev/null +++ b/src/widget/flex_row/layout.rs @@ -0,0 +1,70 @@ +// Copyright 2023 System76 +// SPDX-License-Identifier: MPL-2.0 + +use crate::{Element, Renderer}; +use iced_core::layout::{Limits, Node}; +use iced_core::{Padding, Point, Size}; + +pub fn resolve( + renderer: &Renderer, + limits: &Limits, + items: &[Element<'_, Message>], + padding: Padding, + column_spacing: f32, + row_spacing: f32, +) -> Node { + let limits = limits.pad(padding); + + let mut nodes = Vec::with_capacity(items.len()); + + let max_flex_width = limits.max().width; + let mut flex_width = 0.0f32; + let mut flex_height = 0.0f32; + + let mut current_row_width = 0.0f32; + let mut current_row_height = 0.0f32; + + let mut row_buffer = Vec::with_capacity(8); + + for child in items { + // Calculate the dimensions of the item. + let child_node = child.as_widget().layout(renderer, &limits); + let size = child_node.size(); + + // Calculate the required additional width to fit the item into the current row. + let required_width = size.width + + if row_buffer.is_empty() { + 0.0 + } else { + row_spacing + }; + + // If it fits, add it to the current row, or create a new one. + if current_row_width + required_width <= max_flex_width { + current_row_width += required_width; + current_row_height = current_row_height.max(size.height); + + row_buffer.push(child_node); + } else { + if flex_height != 0.0f32 { + flex_height += column_spacing; + } + + let mut pos_x = 0.0f32; + let pos_y = flex_height; + + for mut child_node in row_buffer.drain(..) { + child_node.move_to(Point::new(pos_x, pos_y)); + pos_x += row_spacing + child_node.size().width; + nodes.push(child_node); + } + + flex_height += current_row_height; + flex_width = flex_width.max(current_row_width); + current_row_width = 0.0f32; + } + } + + let flex_size = limits.resolve(Size::new(flex_width, flex_height)); + Node::with_children(flex_size.pad(padding), nodes) +} diff --git a/src/widget/flex_row/mod.rs b/src/widget/flex_row/mod.rs new file mode 100644 index 00000000..95161090 --- /dev/null +++ b/src/widget/flex_row/mod.rs @@ -0,0 +1,14 @@ +// Copyright 2023 System76 +// SPDX-License-Identifier: MPL-2.0 + +pub mod layout; +pub mod widget; + +pub use widget::FlexRow; + +use crate::Element; + +/// Responsively generates rows and columns of widgets based on its dimmensions. +pub const fn flex_row(children: Vec>) -> FlexRow { + FlexRow::new(children) +} diff --git a/src/widget/flex_row/widget.rs b/src/widget/flex_row/widget.rs new file mode 100644 index 00000000..667b44b0 --- /dev/null +++ b/src/widget/flex_row/widget.rs @@ -0,0 +1,202 @@ +// Copyright 2023 System76 +// SPDX-License-Identifier: MPL-2.0 + +use crate::{Element, Renderer}; +use derive_setters::Setters; +use iced_core::event::{self, Event}; +use iced_core::widget::{Operation, Tree}; +use iced_core::{ + layout, mouse, overlay, renderer, Clipboard, Layout, Length, Padding, Rectangle, Shell, Widget, +}; +use iced_renderer::core::widget::OperationOutputWrapper; + +/// Responsively generates rows and columns of widgets based on its dimmensions. +#[derive(Setters)] +#[must_use] +pub struct FlexRow<'a, Message> { + #[setters(skip)] + children: Vec>, + /// Sets the padding around the widget. + padding: Padding, + /// Sets the space between each column of items. + column_spacing: u16, + /// Sets the space between each item in a row. + row_spacing: u16, + /// Sets the width. + width: Length, + /// Sets the max width + max_width: f32, +} + +impl<'a, Message> FlexRow<'a, Message> { + pub const fn new(children: Vec>) -> Self { + Self { + children, + padding: Padding::ZERO, + column_spacing: 4, + row_spacing: 4, + width: Length::Shrink, + max_width: f32::INFINITY, + } + } +} + +impl<'a, Message: 'static + Clone> Widget for FlexRow<'a, Message> { + fn children(&self) -> Vec { + self.children.iter().map(Tree::new).collect() + } + + fn diff(&mut self, tree: &mut Tree) { + tree.diff_children(self.children.as_mut_slice()); + } + + fn width(&self) -> Length { + self.width + } + + fn height(&self) -> Length { + Length::Shrink + } + + fn layout(&self, renderer: &Renderer, limits: &layout::Limits) -> layout::Node { + let limits = limits + .max_width(self.max_width) + .width(self.width()) + .height(self.height()); + + super::layout::resolve( + renderer, + &limits, + &self.children, + self.padding, + f32::from(self.column_spacing), + f32::from(self.row_spacing), + ) + } + + fn operate( + &self, + tree: &mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn Operation>, + ) { + operation.container(None, layout.bounds(), &mut |operation| { + self.children + .iter() + .zip(&mut tree.children) + .zip(layout.children()) + .for_each(|((child, state), layout)| { + child + .as_widget() + .operate(state, layout, renderer, operation); + }); + }); + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + viewport: &Rectangle, + ) -> event::Status { + self.children + .iter_mut() + .zip(&mut tree.children) + .zip(layout.children()) + .map(|((child, state), layout)| { + child.as_widget_mut().on_event( + state, + event.clone(), + layout, + cursor, + renderer, + clipboard, + shell, + viewport, + ) + }) + .fold(event::Status::Ignored, event::Status::merge) + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + self.children + .iter() + .zip(&tree.children) + .zip(layout.children()) + .map(|((child, state), layout)| { + child + .as_widget() + .mouse_interaction(state, layout, cursor, viewport, renderer) + }) + .max() + .unwrap_or_default() + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &crate::Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + ) { + for ((child, state), layout) in self + .children + .iter() + .zip(&tree.children) + .zip(layout.children()) + { + child + .as_widget() + .draw(state, renderer, theme, style, layout, cursor, viewport); + } + } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + ) -> Option> { + overlay::from_children(&mut self.children, tree, layout, renderer) + } + + #[cfg(feature = "a11y")] + /// get the a11y nodes for the widget + fn a11y_nodes( + &self, + layout: Layout<'_>, + state: &Tree, + p: mouse::Cursor, + ) -> iced_accessibility::A11yTree { + use iced_accessibility::A11yTree; + A11yTree::join( + self.children + .iter() + .zip(layout.children()) + .zip(state.children.iter()) + .map(|((c, c_layout), state)| c.as_widget().a11y_nodes(c_layout, state, p)), + ) + } +} + +impl<'a, Message: 'static + Clone> From> for Element<'a, Message> { + fn from(flex_row: FlexRow<'a, Message>) -> Self { + Self::new(flex_row) + } +}