diff --git a/Cargo.toml b/Cargo.toml index 0a78e67f..bcbb6ce3 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 = [] @@ -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] @@ -67,6 +69,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] @@ -105,6 +108,7 @@ members = [ "examples/spinner", "examples/context_menu", "examples/WidgetIDReturn", + "examples/segmented_button", ] [workspace.dependencies.iced] diff --git a/examples/grid/Cargo.toml b/examples/grid/Cargo.toml index 23ad6eec..ea3fc0ca 100644 --- a/examples/grid/Cargo.toml +++ b/examples/grid/Cargo.toml @@ -1,11 +1,9 @@ [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..596b0c65 100644 --- a/examples/grid/src/main.rs +++ b/examples/grid/src/main.rs @@ -1,79 +1,178 @@ +use iced::widget::{checkbox, container, pick_list, row, slider}; +use iced::Padding; 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}; -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_alignment: Vertical, + column_spacing: f32, + row_spacing: f32, + fill_width: bool, + fill_height: bool, + padding: f32, + debug_layout: bool, } #[derive(Debug, Clone)] enum Message { - AddElement, -} - -struct GridExample { - element_index: usize, + HorizontalAlignment(Horizontal), + VerticalAlignment(Vertical), + ColumnSpacing(f32), + RowSpacing(f32), + FillWidth(bool), + FillHeight(bool), + Padding(f32), + 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_alignment: Vertical::Center, + column_spacing: 5.0, + row_spacing: 5.0, + fill_width: false, + fill_height: false, + padding: 0.0, + 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_alignment = align, + Message::ColumnSpacing(spacing) => self.column_spacing = spacing, + Message::RowSpacing(spacing) => self.row_spacing = spacing, + 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, } } - 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_alignment)), + |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(Length::Fill); + let col_spacing_slider = + slider(0.0..=100.0, self.column_spacing, Message::ColumnSpacing).width(Length::Fill); - 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 fill_checkboxes = row![ + checkbox("Width", self.fill_width, Message::FillWidth), + checkbox("Height", self.fill_height, Message::FillHeight) + ] + .spacing(10); - Container::new(content) + 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!("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_alignment) + .row_spacing(self.row_spacing) + .column_spacing(self.column_spacing) + .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 { + 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]; + +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 main() -> iced::Result { + App::run(Settings::default()) +} 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/src/main.rs b/examples/segmented_button/src/main.rs new file mode 100644 index 00000000..9d4d2149 --- /dev/null +++ b/examples/segmented_button/src/main.rs @@ -0,0 +1,103 @@ +use iced::widget::container; +use iced::widget::{column, row, text}; +use iced::{Element, Length, Sandbox, Settings}; + +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/examples/wrap/src/main.rs b/examples/wrap/src/main.rs index 2d1ee4dc..2f166d3b 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| { @@ -175,6 +175,10 @@ impl Sandbox for RandStrings { .height(iced::Length::Shrink) .align_items(iced::Alignment::Center); - Row::new().push(ctrls).push(vertcal).push(horizontal).into() + Row::new() + .push(ctrls) + .push(vertical) + .push(horizontal) + .into() } } diff --git a/src/lib.rs b/src/lib.rs index 416061c5..1c70d7e9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -87,10 +87,7 @@ mod platform { #[doc(no_inline)] #[cfg(feature = "grid")] - pub use { - crate::native::grid, - grid::{Grid, 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 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/layout.rs b/src/native/grid/layout.rs new file mode 100644 index 00000000..1bde7e27 --- /dev/null +++ b/src/native/grid/layout.rs @@ -0,0 +1,217 @@ +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(); + + // 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, + padding, + 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, + padding: Padding, + grid_size: Size, +) -> Node +where + Renderer: iced_widget::core::Renderer, +{ + 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 = padding.left; + 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.pad(padding), 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/helpers.rs b/src/native/helpers.rs index a9598e15..5afc9d91 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`]: crate::grid::Grid #[must_use] pub fn grid( - children: Vec>, -) -> crate::Grid + rows: Vec>, +) -> crate::Grid<'_, 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_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/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()) } diff --git a/src/native/mod.rs b/src/native/mod.rs index b680ae1b..306f4b18 100644 --- a/src/native/mod.rs +++ b/src/native/mod.rs @@ -60,9 +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::Strategy; +pub use grid::GridRow; #[cfg(feature = "modal")] pub mod modal; @@ -124,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>; diff --git a/src/native/number_input.rs b/src/native/number_input.rs index d54f0c69..a793f798 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, } } diff --git a/src/native/overlay/context_menu.rs b/src/native/overlay/context_menu.rs index 70565388..34ee7911 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]) @@ -134,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 @@ -148,29 +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, @@ -179,7 +193,13 @@ where clipboard, shell, &layout.bounds(), - ), + ) + } else { + Status::Ignored + }; + + match child_status { + Status::Ignored => status, Status::Captured => Status::Captured, } } diff --git a/src/native/segmented_button.rs b/src/native/segmented_button.rs new file mode 100644 index 00000000..ef02d26e --- /dev/null +++ b/src/native/segmented_button.rs @@ -0,0 +1,290 @@ +//! 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 `segnmented_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). + #[must_use] + pub fn padding(mut self, units: Padding) -> Self { + self.padding = units; + self + } + + /// Sets the width of the [`SegmentedButton`](SegmentedButton). + #[must_use] + pub fn width(mut self, width: Length) -> Self { + self.width = width; + self + } + + /// Sets the height of the [`SegmentedButton`](SegmentedButton). + #[must_use] + pub fn height(mut self, height: Length) -> Self { + self.height = height; + self + } + + /// Sets the horizontal alignment of the content of the [`SegmentedButton`](SegmentedButton). + #[must_use] + 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). + #[must_use] + pub fn align_y(mut self, alignment: Alignment) -> Self { + self.vertical_alignment = alignment; + self + } + + /// Sets the style of the [`SegmentedButton`](SegmentedButton). + #[must_use] + 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; + 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..093144a3 --- /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