From dd3ff2e62205240d0798fdb6a60c5444a0a28148 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Fri, 6 Jan 2023 01:39:09 +0100 Subject: [PATCH] feat(segmented-button): icon support with state ergonomics --- src/widget/segmented_button/horizontal.rs | 13 +- src/widget/segmented_button/mod.rs | 4 +- src/widget/segmented_button/state.rs | 123 ++++++++++++++--- src/widget/segmented_button/vertical.rs | 13 +- src/widget/segmented_button/widget.rs | 157 ++++++++++++++++------ 5 files changed, 244 insertions(+), 66 deletions(-) diff --git a/src/widget/segmented_button/horizontal.rs b/src/widget/segmented_button/horizontal.rs index 5dcaeddc..af8beb54 100644 --- a/src/widget/segmented_button/horizontal.rs +++ b/src/widget/segmented_button/horizontal.rs @@ -1,3 +1,6 @@ +// Copyright 2022 System76 +// SPDX-License-Identifier: MPL-2.0 + use super::state::State; use super::style::StyleSheet; use super::widget::{SegmentedButton, SegmentedVariant}; @@ -18,7 +21,10 @@ pub fn horizontal_segmented_button( state: &State, ) -> SegmentedButton where - Renderer: iced_native::Renderer + iced_native::text::Renderer, + Renderer: iced_native::Renderer + + iced_native::text::Renderer + + iced_native::image::Renderer + + iced_native::svg::Renderer, Renderer::Theme: StyleSheet, { SegmentedButton::new(&state.inner) @@ -26,7 +32,10 @@ where impl<'a, Message, Renderer> SegmentedVariant for SegmentedButton<'a, Horizontal, Message, Renderer> where - Renderer: iced_native::Renderer + iced_native::text::Renderer, + Renderer: iced_native::Renderer + + iced_native::text::Renderer + + iced_native::image::Renderer + + iced_native::svg::Renderer, Renderer::Theme: StyleSheet, { type Renderer = Renderer; diff --git a/src/widget/segmented_button/mod.rs b/src/widget/segmented_button/mod.rs index 73e21e81..ae89b307 100644 --- a/src/widget/segmented_button/mod.rs +++ b/src/widget/segmented_button/mod.rs @@ -35,7 +35,7 @@ //! Then use it in the view method to create segmented button widgets. //! //! ```ignore -//! let widget = segmentend_button(&application.state) +//! let widget = horizontal_segmentend_button(&application.state) //! .style(theme::SegmentedButton::Selection) //! .height(Length::Units(32)) //! .on_activate(AppMessage::Selected); @@ -51,7 +51,7 @@ mod vertical; mod widget; pub use self::horizontal::{horizontal_segmented_button, Horizontal, HorizontalSegmentedButton}; -pub use self::state::{ButtonContent, Key, SecondaryState, SharedWidgetState, State}; +pub use self::state::{Content, Key, SecondaryState, SharedWidgetState, State}; pub use self::style::{Appearance, ButtonAppearance, ButtonStatusAppearance, StyleSheet}; pub use self::vertical::{vertical_segmented_button, Vertical, VerticalSegmentedButton}; pub use self::widget::{SegmentedButton, SegmentedVariant}; diff --git a/src/widget/segmented_button/state.rs b/src/widget/segmented_button/state.rs index b4fa8ad2..ad0e8e76 100644 --- a/src/widget/segmented_button/state.rs +++ b/src/widget/segmented_button/state.rs @@ -1,9 +1,12 @@ // Copyright 2022 System76 // SPDX-License-Identifier: MPL-2.0 +use derive_setters::Setters; use slotmap::{SecondaryMap, SlotMap}; use std::borrow::Cow; +use crate::widget::IconSource; + slotmap::new_key_type! { /// An ID for a segmented button pub struct Key; @@ -31,7 +34,7 @@ impl Default for State { #[derive(Default)] pub struct SharedWidgetState { /// The content used for drawing segmented buttons. - pub buttons: SlotMap, + pub buttons: SlotMap, /// The actively-selected segmented button. pub active: Key, @@ -41,6 +44,16 @@ pub struct SharedWidgetState { pub type SecondaryState = SecondaryMap; impl State { + #[must_use] + pub fn builder() -> Builder { + Builder(Self::default()) + } + + /// Activates this button. + pub fn activate(&mut self, key: Key) { + self.inner.active = key; + } + /// The ID of the active button. #[must_use] pub fn active(&self) -> Key { @@ -53,54 +66,126 @@ impl State { self.data(self.active()) } + /// Convenience method for batching multiple operations + #[must_use] + pub fn batch(&mut self) -> Batch { + Batch(self) + } + + /// Enables or disables a button + #[must_use] + pub fn content(&self, key: Key) -> Option<&Content> { + self.inner.buttons.get(key) + } + + /// Enables or disables a button + #[must_use] + pub fn content_mut(&mut self, key: Key) -> Option<&mut Content> { + self.inner.buttons.get_mut(key) + } + /// Get the application data for a button. #[must_use] pub fn data(&self, key: Key) -> Option<&Data> { self.data.get(key) } + /// Get mutable application data for a button. + #[must_use] + pub fn data_mut(&mut self, key: Key) -> Option<&mut Data> { + self.data.get_mut(key) + } + /// Insert a new button. - pub fn insert(&mut self, content: impl Into, data: Data) -> Key { + pub fn insert(&mut self, content: impl Into, data: Data) -> Key { let key = self.inner.buttons.insert(content.into()); self.data.insert(key, data); key } + /// Inserts and activates a button. + pub fn insert_active(&mut self, content: impl Into, data: Data) -> Key { + let key = self.insert(content, data); + self.activate(key); + key + } + /// Removes a button. pub fn remove(&mut self, key: Key) -> Option { self.inner.buttons.remove(key); self.data.remove(key) } - - /// Activates this button. - pub fn activate(&mut self, key: Key) { - self.inner.active = key; - } } /// Data to be drawn in a segmented button. -pub struct ButtonContent { - pub text: Cow<'static, str>, +#[derive(Default, Setters)] +pub struct Content { + #[setters(strip_option, into)] + /// The label to display in this button. + pub text: Option>, + + #[setters(strip_option, into)] + /// An optionally-displayed icon beside the label. + pub icon: Option>, + + /// Whether the button is clickable or not. + pub enabled: bool, } -impl From for ButtonContent { +impl From for Content { fn from(text: String) -> Self { - ButtonContent { - text: Cow::Owned(text), - } + Self::from(Cow::Owned(text)) } } -impl From<&'static str> for ButtonContent { +impl From<&'static str> for Content { fn from(text: &'static str) -> Self { - ButtonContent { - text: Cow::Borrowed(text), - } + Self::from(Cow::Borrowed(text)) } } -impl From> for ButtonContent { +impl From> for Content { fn from(text: Cow<'static, str>) -> Self { - ButtonContent { text } + Content::default().text(text) + } +} + +pub struct Builder(State); + +impl Builder { + pub fn insert(mut self, content: impl Into, data: Data) -> Self { + self.0.insert(content, data); + self + } + + pub fn insert_active(mut self, content: impl Into, data: Data) -> Self { + self.0.insert_active(content, data); + self + } + + pub fn build(self) -> State { + self.0 + } +} + +/// Convenience type for batching multiple operations +pub struct Batch<'a, Data>(&'a mut State); + +impl<'a, Data> Batch<'a, Data> { + /// Insert a new button. + pub fn insert(self, content: impl Into, data: Data) -> Self { + self.0.insert(content, data); + self + } + + /// Inserts and activates a button. + pub fn insert_active(self, content: impl Into, data: Data) -> Self { + self.0.insert_active(content, data); + self + } + + /// Removes a button. + pub fn remove(&mut self, key: Key) { + self.0.remove(key); } } diff --git a/src/widget/segmented_button/vertical.rs b/src/widget/segmented_button/vertical.rs index 5e9c4e69..58f170e3 100644 --- a/src/widget/segmented_button/vertical.rs +++ b/src/widget/segmented_button/vertical.rs @@ -1,3 +1,6 @@ +// Copyright 2022 System76 +// SPDX-License-Identifier: MPL-2.0 + use super::state::State; use super::style::StyleSheet; use super::widget::{SegmentedButton, SegmentedVariant}; @@ -18,7 +21,10 @@ pub fn vertical_segmented_button( state: &State, ) -> SegmentedButton where - Renderer: iced_native::Renderer + iced_native::text::Renderer, + Renderer: iced_native::Renderer + + iced_native::text::Renderer + + iced_native::image::Renderer + + iced_native::svg::Renderer, Renderer::Theme: StyleSheet, { SegmentedButton::new(&state.inner) @@ -26,7 +32,10 @@ where impl<'a, Message, Renderer> SegmentedVariant for SegmentedButton<'a, Vertical, Message, Renderer> where - Renderer: iced_native::Renderer + iced_native::text::Renderer, + Renderer: iced_native::Renderer + + iced_native::text::Renderer + + iced_native::image::Renderer + + iced_native::svg::Renderer, Renderer::Theme: StyleSheet, { type Renderer = Renderer; diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index e4c14fd6..fd3f5431 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -1,3 +1,6 @@ +// Copyright 2022 System76 +// SPDX-License-Identifier: MPL-2.0 + use std::marker::PhantomData; use super::state::{Key, SharedWidgetState}; @@ -34,26 +37,33 @@ pub trait SegmentedVariant { #[derive(Setters)] pub struct SegmentedButton<'a, Variant, Message, Renderer> where - Renderer: iced_native::Renderer + iced_native::text::Renderer, + Renderer: iced_native::Renderer + + iced_native::text::Renderer + + iced_native::image::Renderer + + iced_native::svg::Renderer, Renderer::Theme: StyleSheet, { /// Contains application state also used for drawing. #[setters(skip)] pub(super) state: &'a SharedWidgetState, + /// Padding around a button. + pub(super) button_padding: [u16; 4], + /// Desired height of a button. + pub(super) button_height: u16, + /// Spacing between icon and text in button. + pub(super) button_spacing: u16, /// Desired font for active tabs. pub(super) font_active: Renderer::Font, /// Desired font for hovered tabs. pub(super) font_hovered: Renderer::Font, /// Desired font for inactive tabs. pub(super) font_inactive: Renderer::Font, + /// Size of icon + pub(super) icon_size: u16, /// Desired width of the widget. pub(super) width: Length, /// Desired height of the widget. pub(super) height: Length, - /// Padding around a button. - pub(super) button_padding: [u16; 4], - /// Desired height of a button. - pub(super) button_height: u16, /// Desired spacing between buttons. pub(super) spacing: u16, /// Style to draw the widget in. @@ -69,7 +79,10 @@ where impl<'a, Variant, Message, Renderer> SegmentedButton<'a, Variant, Message, Renderer> where - Renderer: iced_native::Renderer + iced_native::text::Renderer, + Renderer: iced_native::Renderer + + iced_native::text::Renderer + + iced_native::image::Renderer + + iced_native::svg::Renderer, Renderer::Theme: StyleSheet, Self: SegmentedVariant, { @@ -77,13 +90,15 @@ where pub fn new(state: &'a SharedWidgetState) -> Self { Self { state, + button_padding: [4, 4, 4, 4], + button_height: 32, + button_spacing: 4, font_active: Renderer::Font::default(), font_hovered: Renderer::Font::default(), font_inactive: Renderer::Font::default(), + icon_size: 24, height: Length::Shrink, width: Length::Fill, - button_padding: [4, 4, 4, 4], - button_height: 32, spacing: 0, style: ::Style::default(), on_activate: None, @@ -98,20 +113,6 @@ where self } - pub(super) fn measure_button( - &self, - renderer: &Renderer, - text: &str, - text_size: u16, - bounds: Size, - ) -> (f32, f32) { - let (mut w, mut h) = renderer.measure(text, text_size, Default::default(), bounds); - w += f32::from(self.button_padding[0]) + f32::from(self.button_padding[2]); - h += f32::from(self.button_padding[1]) + f32::from(self.button_padding[3]); - h = h.max(f32::from(self.button_height)); - (w, h) - } - pub(super) fn max_button_dimensions( &self, renderer: &Renderer, @@ -122,11 +123,32 @@ where let mut height = 0.0f32; for (_, content) in self.state.buttons.iter() { - let (w, h) = self.measure_button(renderer, &content.text, text_size, bounds); - height = height.max(h); - width = width.max(w); + let mut button_width = 0.0f32; + let mut button_height = 0.0f32; + + // Add text to measurement if text was given. + if let Some(text) = content.text.as_deref() { + let (w, h) = renderer.measure(text, text_size, Default::default(), bounds); + + button_width = w; + button_height = h; + } + + // Add icon to measurement if icon was given. + if content.icon.is_some() { + button_width += f32::from(self.icon_size) + f32::from(self.button_spacing); + button_height = f32::from(self.icon_size); + } + + height = height.max(button_height); + width = width.max(button_width); } + // Add button padding to the max size found + width += f32::from(self.button_padding[0]) + f32::from(self.button_padding[2]); + height += f32::from(self.button_padding[1]) + f32::from(self.button_padding[3]); + height = height.max(f32::from(self.button_height)); + (width, height) } } @@ -134,7 +156,10 @@ where impl<'a, Variant, Message, Renderer> Widget for SegmentedButton<'a, Variant, Message, Renderer> where - Renderer: iced_native::Renderer + iced_native::text::Renderer, + Renderer: iced_native::Renderer + + iced_native::text::Renderer + + iced_native::image::Renderer + + iced_native::svg::Renderer, Renderer::Theme: StyleSheet, Self: SegmentedVariant, Message: 'static + Clone, @@ -215,6 +240,7 @@ where } } + #[allow(clippy::too_many_lines)] fn draw( &self, tree: &Tree, @@ -245,7 +271,7 @@ where // Draw each of the buttons in the widget. for (nth, (key, content)) in self.state.buttons.iter().enumerate() { - let bounds = self.variant_button_bounds(bounds, nth); + let mut bounds = self.variant_button_bounds(bounds, nth); let (status_appearance, font) = if self.state.active == key { (appearance.active, &self.font_active) @@ -255,9 +281,6 @@ where (appearance.inactive, &self.font_inactive) }; - let x = bounds.center_x(); - let y = bounds.center_y(); - let button_appearance = if nth == 0 { status_appearance.first } else if nth + 1 == button_amount { @@ -298,16 +321,64 @@ where ); } - // Draw the text in this button. - renderer.fill_text(iced_native::text::Text { - content: &content.text, - size: f32::from(renderer.default_size()), - bounds: Rectangle { x, y, ..bounds }, - color: status_appearance.text_color, - font: font.clone(), - horizontal_alignment: alignment::Horizontal::Center, - vertical_alignment: alignment::Vertical::Center, - }); + let y = bounds.center_y(); + let text_size = renderer.default_size(); + + // Draw the image beside the text. + let horizontal_alignment = if let Some(icon) = &content.icon { + bounds.x += f32::from(self.button_padding[0]); + bounds.y += f32::from(self.button_padding[1]); + bounds.width -= + f32::from(self.button_padding[0]) - f32::from(self.button_padding[2]); + bounds.height -= + f32::from(self.button_padding[1]) - f32::from(self.button_padding[3]); + + let width = f32::from(self.icon_size); + let offset = width + f32::from(self.button_spacing); + bounds.y = y - width / 2.0; + + let icon_bounds = Rectangle { + width, + height: width, + ..bounds + }; + + bounds.x += offset; + bounds.width -= offset; + match icon.load(self.icon_size, None, false) { + crate::widget::icon::Handle::Image(_handle) => { + unimplemented!() + } + crate::widget::icon::Handle::Svg(handle) => { + iced_native::svg::Renderer::draw( + renderer, + handle, + Some(status_appearance.text_color), + icon_bounds, + ); + } + } + + alignment::Horizontal::Left + } else { + bounds.x = bounds.center_x(); + alignment::Horizontal::Center + }; + + if let Some(text) = content.text.as_deref() { + bounds.y = y; + + // Draw the text in this button. + renderer.fill_text(iced_native::text::Text { + content: text, + size: f32::from(text_size), + bounds, + color: status_appearance.text_color, + font: font.clone(), + horizontal_alignment, + vertical_alignment: alignment::Vertical::Center, + }); + } } } @@ -324,7 +395,11 @@ where impl<'a, Variant, Message, Renderer> From> for Element<'a, Message, Renderer> where - Renderer: iced_native::Renderer + iced_native::text::Renderer + 'a, + Renderer: iced_native::Renderer + + iced_native::text::Renderer + + iced_native::image::Renderer + + iced_native::svg::Renderer + + 'a, Renderer::Theme: StyleSheet, SegmentedButton<'a, Variant, Message, Renderer>: SegmentedVariant, Variant: 'static,