From e606bf3ce46e82cae51131378190e1eabd5234b5 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Wed, 24 Jan 2024 17:32:50 +0100 Subject: [PATCH] feat(segmented_button): variable-width horizontal button when width is `Shrink` --- src/widget/segmented_button/horizontal.rs | 148 +++++++++++++----- src/widget/segmented_button/vertical.rs | 31 ++-- src/widget/segmented_button/widget.rs | 179 ++++++++++------------ 3 files changed, 211 insertions(+), 147 deletions(-) diff --git a/src/widget/segmented_button/horizontal.rs b/src/widget/segmented_button/horizontal.rs index 7b8b374a36d..b15e6383c5e 100644 --- a/src/widget/segmented_button/horizontal.rs +++ b/src/widget/segmented_button/horizontal.rs @@ -3,12 +3,13 @@ //! Implementation details for the horizontal layout of a segmented button. -use super::model::{Model, Selectable}; +use super::model::{Entity, Model, Selectable}; use super::style::StyleSheet; use super::widget::{LocalState, SegmentedButton, SegmentedVariant}; use iced::{Length, Rectangle, Size}; use iced_core::layout; +use iced_core::text::Renderer; /// Horizontal [`SegmentedButton`]. pub type HorizontalSegmentedButton<'a, SelectionMode, Message> = @@ -48,34 +49,40 @@ where &self, state: &LocalState, mut bounds: Rectangle, - nth: usize, - ) -> Option { + ) -> impl Iterator { let num = state.buttons_visible; + let spacing = f32::from(self.spacing); + let mut homogenous_width = 0.0; - // Do not display tabs that are currently hidden due to width constraints. - if state.collapsed && nth < state.buttons_offset { - return None; - } - - if num != 0 { - let offset_width; - (bounds.x, offset_width) = if state.collapsed { - (bounds.x + 16.0, 32.0) - } else { - (bounds.x, 0.0) - }; - - let spacing = f32::from(self.spacing); - bounds.width = ((num as f32).mul_add(-spacing, bounds.width - offset_width) + spacing) - / num as f32; - - if nth != state.buttons_offset { - let pos = (nth - state.buttons_offset) as f32; - bounds.x += pos.mul_add(bounds.width, pos * spacing); + if Length::Shrink != self.width || state.collapsed { + if state.collapsed { + bounds.x += 16.0; + bounds.width -= 32.0; } + + homogenous_width = + ((num as f32).mul_add(-spacing, bounds.width) + spacing) / num as f32; } - Some(bounds) + self.model + .order + .iter() + .copied() + .enumerate() + .skip(state.buttons_offset) + .take(state.buttons_visible) + .map(move |(nth, key)| { + let mut this_bounds = bounds; + + if !state.collapsed && Length::Shrink == self.width { + this_bounds.width = state.internal_layout[nth].width; + } else { + this_bounds.width = homogenous_width; + } + + bounds.x += this_bounds.width + spacing; + (key, this_bounds) + }) } #[allow(clippy::cast_precision_loss)] @@ -87,27 +94,92 @@ where renderer: &crate::Renderer, limits: &layout::Limits, ) -> layout::Node { + let num = self.model.order.len(); + let mut total_width = 0.0; + let spacing = f32::from(self.spacing); let limits = limits.width(self.width); - let (mut width, height) = self.max_button_dimensions(state, renderer, limits.max()); + let size; - let num = self.model.items.len(); - let spacing = f32::from(self.spacing); + if state.known_length != num { + if state.known_length > num { + state.buttons_offset -= state.buttons_offset.min(state.known_length - num); + } else { + state.buttons_offset += num - state.known_length; + } - if num != 0 { - width = (num as f32).mul_add(width, num as f32 * spacing) - spacing; + state.known_length = num; } - let size = limits - .height(Length::Fixed(height)) - .resolve(Size::new(width, height)); + if let Length::Shrink = self.width { + // Buttons will be rendered at their smallest widths possible. + state.internal_layout.clear(); + + let font = renderer.default_font(); + let mut total_height = 0.0f32; + + for &button in &self.model.order { + let (mut width, height) = self.button_dimensions(state, font, button); + width = f32::from(self.minimum_button_width).max(width); + total_width += width + spacing; + total_height = total_height.max(height); - let actual_width = size.width as usize; - let minimum_width = self.minimum_button_width as usize * self.model.items.len(); + state.internal_layout.push(Size::new(width, height)); + } + + // Get the max available width for placing buttons into. + let max_size = limits + .height(Length::Fixed(total_height)) + .resolve(Size::new(f32::MAX, total_height)); + + let mut visible_width = 32.0; + state.buttons_visible = 0; + + for button_size in &state.internal_layout { + visible_width += button_size.width; + + if max_size.width >= visible_width { + state.buttons_visible += 1; + } else { + break; + } + + visible_width += spacing; + } + + state.collapsed = num > 1 && state.buttons_visible != num; + + // If collapsed, use the maximum width available. + visible_width = if state.collapsed { + max_size.width - 32.0 + } else { + total_width + }; + + size = limits + .height(Length::Fixed(total_height)) + .resolve(Size::new(visible_width, total_height)); + } else { + // Buttons will be rendered with equal widths. + state.buttons_visible = self.model.items.len(); + let (width, height) = self.max_button_dimensions(state, renderer, limits.max()); + let total_width = (state.buttons_visible as f32) * (width + spacing); + + size = limits + .height(Length::Fixed(height)) + .resolve(Size::new(total_width, height)); + + let actual_width = size.width as usize; + let minimum_width = state.buttons_visible * self.minimum_button_width as usize; + state.collapsed = actual_width < minimum_width; + + if state.collapsed { + state.buttons_visible = + (actual_width / self.minimum_button_width as usize).min(state.buttons_visible); + } + } - state.buttons_visible = num; - state.collapsed = actual_width < minimum_width; - if state.collapsed { - state.buttons_visible = (actual_width / self.minimum_button_width as usize).min(num); + if !state.collapsed { + state.buttons_offset = 0; } layout::Node::new(size) diff --git a/src/widget/segmented_button/vertical.rs b/src/widget/segmented_button/vertical.rs index c6c6b0773a2..5c541a85049 100644 --- a/src/widget/segmented_button/vertical.rs +++ b/src/widget/segmented_button/vertical.rs @@ -3,7 +3,7 @@ //! Implementation details for the vertical layout of a segmented button. -use super::model::{Model, Selectable}; +use super::model::{Entity, Model, Selectable}; use super::style::StyleSheet; use super::widget::{LocalState, SegmentedButton, SegmentedVariant}; @@ -47,21 +47,22 @@ where #[allow(clippy::cast_precision_loss)] fn variant_button_bounds( &self, - _state: &LocalState, + state: &LocalState, mut bounds: Rectangle, - nth: usize, - ) -> Option { - let num = self.model.items.len(); - if num != 0 { - let spacing = f32::from(self.spacing); - bounds.height = (bounds.height - (num as f32 * spacing) + spacing) / num as f32; - - if nth != 0 { - bounds.y += (nth as f32 * bounds.height) + (nth as f32 * spacing); - } - } + ) -> impl Iterator { + let spacing = f32::from(self.spacing); - Some(bounds) + self.model + .order + .iter() + .copied() + .enumerate() + .map(move |(_nth, key)| { + let mut this_bounds = bounds; + this_bounds.height = state.internal_layout[0].height; + bounds.y += this_bounds.height + spacing; + (key, this_bounds) + }) } #[allow(clippy::cast_precision_loss)] @@ -73,8 +74,10 @@ where renderer: &crate::Renderer, limits: &layout::Limits, ) -> layout::Node { + state.internal_layout.clear(); let limits = limits.width(self.width); let (width, mut height) = self.max_button_dimensions(state, renderer, limits.max()); + state.internal_layout.push(Size::new(width, height)); let num = self.model.items.len(); let spacing = f32::from(self.spacing); diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index b56534d02ca..63214ddff59 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -32,13 +32,12 @@ pub trait SegmentedVariant { style: &crate::theme::SegmentedButton, ) -> super::Appearance; - /// Calculates the bounds for the given button by its position. + /// Calculates the bounds for visible buttons. fn variant_button_bounds( &self, state: &LocalState, bounds: Rectangle, - position: usize, - ) -> Option; + ) -> impl Iterator; /// Calculates the layout of this variant. fn variant_layout( @@ -137,6 +136,7 @@ where } } + /// Emitted when a tab is pressed. pub fn on_activate(mut self, on_activate: T) -> Self where T: Fn(Entity) -> Message + 'static, @@ -145,6 +145,7 @@ where self } + /// Emitted when a tab close button is pressed. pub fn on_close(mut self, on_close: T) -> Self where T: Fn(Entity) -> Message + 'static, @@ -293,62 +294,55 @@ where state.buttons_offset < self.model.order.len() - state.buttons_visible } - pub(super) fn max_button_dimensions( + pub(super) fn button_dimensions( &self, state: &mut LocalState, - renderer: &Renderer, - _bounds: Size, + font: crate::font::Font, + button: Entity, ) -> (f32, f32) { let mut width = 0.0f32; let mut height = 0.0f32; - let font = renderer.default_font(); - - for key in self.model.order.iter().copied() { - let mut button_width = 0.0f32; - let mut button_height = 0.0f32; - - // Add text to measurement if text was given. - if let Some((text, entry)) = self.model.text.get(key).zip(state.paragraphs.entry(key)) { - let paragraph = entry.or_insert_with(|| { - crate::Paragraph::with_text(Text { - content: text, - size: iced::Pixels(self.font_size), - bounds: Size::INFINITY, - font, - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Center, - shaping: Shaping::Advanced, - line_height: self.line_height, - }) - }); - - let Size { width, height } = paragraph.min_bounds(); - button_width = width; - button_height = height; - } + // Add text to measurement if text was given. + if let Some((text, entry)) = self + .model + .text + .get(button) + .zip(state.paragraphs.entry(button)) + { + let paragraph = entry.or_insert_with(|| { + crate::Paragraph::with_text(Text { + content: text, + size: iced::Pixels(self.font_size), + bounds: Size::INFINITY, + font, + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Center, + shaping: Shaping::Advanced, + line_height: self.line_height, + }) + }); - // Add indent to measurement if found. - if let Some(indent) = self.model.indent(key) { - button_width = - f32::from(indent).mul_add(f32::from(self.indent_spacing), button_width); - } + let size = paragraph.min_bounds(); + width += size.width; + height += size.height; + } - // Add icon to measurement if icon was given. - if let Some(icon) = self.model.icon(key) { - button_height = button_height.max(f32::from(icon.size)); - button_width += f32::from(icon.size) + f32::from(self.button_spacing); - } + // Add indent to measurement if found. + if let Some(indent) = self.model.indent(button) { + width = f32::from(indent).mul_add(f32::from(self.indent_spacing), width); + } - // Add close button to measurement if found. - if self.model.is_closable(key) { - button_height = button_height.max(f32::from(self.close_icon.size)); - button_width += - f32::from(self.close_icon.size) + f32::from(self.button_spacing) + 8.0; - } + // Add icon to measurement if icon was given. + if let Some(icon) = self.model.icon(button) { + height = height.max(f32::from(icon.size)); + width += f32::from(icon.size) + f32::from(self.button_spacing); + } - height = height.max(button_height); - width = width.max(button_width); + // Add close button to measurement if found. + if self.model.is_closable(button) { + height = height.max(f32::from(self.close_icon.size)); + width += f32::from(self.close_icon.size) + f32::from(self.button_spacing) + 8.0; } // Add button padding to the max size found @@ -358,6 +352,26 @@ where (width, height) } + + pub(super) fn max_button_dimensions( + &self, + state: &mut LocalState, + renderer: &Renderer, + _bounds: Size, + ) -> (f32, f32) { + let mut width = 0.0f32; + let mut height = 0.0f32; + let font = renderer.default_font(); + + for key in self.model.order.iter().copied() { + let (button_width, button_height) = self.button_dimensions(state, font, key); + + height = height.max(button_height); + width = width.max(button_width); + } + + (width, height) + } } impl<'a, Variant, SelectionMode, Message> Widget @@ -373,9 +387,7 @@ where } fn state(&self) -> tree::State { - // update the paragraphs for the model tree::State::new(LocalState { - first: self.model.order.iter().copied().next().unwrap_or_default(), paragraphs: SecondaryMap::new(), ..LocalState::default() }) @@ -478,19 +490,10 @@ where } } - for (nth, key) in self - .model - .order - .iter() - .copied() - .enumerate() - .skip(state.buttons_offset) - .take(state.buttons_visible) + for (key, bounds) in self + .variant_button_bounds(state, bounds) + .collect::>() { - let Some(bounds) = self.variant_button_bounds(state, bounds, nth) else { - continue; - }; - if cursor_position.is_over(bounds) { if self.model.items[key].enabled { // Record that the mouse is hovering over this button. @@ -689,19 +692,7 @@ where let bounds = layout.bounds(); if cursor_position.is_over(bounds) { - for (nth, key) in self - .model - .order - .iter() - .copied() - .enumerate() - .skip(state.buttons_offset) - .take(state.buttons_visible) - { - let Some(bounds) = self.variant_button_bounds(state, bounds, nth) else { - continue; - }; - + for (key, bounds) in self.variant_button_bounds(state, bounds) { if cursor_position.is_over(bounds) { return if self.model.items[key].enabled { iced_core::mouse::Interaction::Pointer @@ -827,19 +818,7 @@ where } // Draw each of the items in the widget. - for (nth, key) in self - .model - .order - .iter() - .copied() - .enumerate() - .skip(state.buttons_offset) - .take(state.buttons_visible) - { - let Some(mut bounds) = self.variant_button_bounds(state, bounds, nth) else { - continue; - }; - + for (nth, (key, mut bounds)) in self.variant_button_bounds(state, bounds).enumerate() { let key_is_active = self.model.is_active(key); let key_is_hovered = state.hovered == key; @@ -970,7 +949,15 @@ where bounds.position(), status_appearance.text_color, Rectangle { - width: bounds.width - close_icon_width, + width: { + let width = bounds.width - close_icon_width; + // TODO: determine cause of differences here. + if self.model.icon(key).is_some() { + width - f32::from(self.button_spacing) + } else { + width - 12.0 + } + }, ..original_bounds }, ); @@ -1026,20 +1013,22 @@ where /// State that is maintained by each individual widget. #[derive(Default)] pub struct LocalState { - /// Whether buttons need to be collapsed to preserve minimum width - pub(super) collapsed: bool, /// Defines how many buttons to show at a time. pub(super) buttons_visible: usize, /// Button visibility offset, when collapsed. pub(super) buttons_offset: usize, - /// The first focusable key. - first: Entity, + /// Whether buttons need to be collapsed to preserve minimum width + pub(super) collapsed: bool, /// If the widget is focused or not. focused: bool, /// The key inside the widget that is currently focused. focused_item: Focus, /// The ID of the button that is being hovered. Defaults to null. hovered: Entity, + /// Last known length of the model. + pub(super) known_length: usize, + /// Dimensions of internal buttons when shrinking + pub(super) internal_layout: Vec, /// The paragraphs for each text. paragraphs: SecondaryMap, /// Time since last tab activation from wheel movements. @@ -1131,7 +1120,7 @@ fn draw_icon( }); Widget::::draw( - Element::::from(icon.clone()).as_widget(), + Element::::from(icon).as_widget(), &Tree::empty(), renderer, theme,