From 357de5e9be4110fa3762cd7ec057760353505620 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Wed, 4 Jan 2023 05:37:20 +0100 Subject: [PATCH] improv(segmented-button): Express vertical/horizontal variants as a state machine It's difficult to make iterative developments when there's two nearly-identical types that need to be kept synchronized to any change. Rust gives us traits so we should use them instead of duplicating code. This made it easier to make styling and layout improvements to both instances of the segmented button. --- src/theme/segmented_button.rs | 243 ++++++++++----- src/widget/mod.rs | 1 - src/widget/segmented_button/cosmic.rs | 10 +- src/widget/segmented_button/horizontal.rs | 356 +++------------------- src/widget/segmented_button/mod.rs | 15 +- src/widget/segmented_button/state.rs | 2 +- src/widget/segmented_button/style.rs | 32 +- src/widget/segmented_button/vertical.rs | 353 +++------------------ src/widget/segmented_button/widget.rs | 347 +++++++++++++++++++++ 9 files changed, 633 insertions(+), 726 deletions(-) create mode 100644 src/widget/segmented_button/widget.rs diff --git a/src/theme/segmented_button.rs b/src/theme/segmented_button.rs index 3be26701..df165259 100644 --- a/src/theme/segmented_button.rs +++ b/src/theme/segmented_button.rs @@ -3,7 +3,7 @@ use crate::theme::Theme; use crate::widget::segmented_button; -use iced_core::{Background, BorderRadius, Color}; +use iced_core::{Background, BorderRadius}; #[derive(Clone, Copy, Default)] pub enum SegmentedButton { @@ -24,69 +24,122 @@ impl segmented_button::StyleSheet for Theme { SegmentedButton::ViewSwitcher => { let cosmic = self.cosmic(); segmented_button::Appearance { - background: None, - border_color: Color::TRANSPARENT, border_radius: BorderRadius::from(0.0), - border_width: 0.0, - button_active: segmented_button::ButtonAppearance { + active: segmented_button::ButtonStatusAppearance { background: Some(Background::Color(cosmic.primary.component.base.into())), - border_bottom: Some((4.0, cosmic.accent.base.into())), - border_radius_first: BorderRadius::from([8.0, 8.0, 0.0, 0.0]), - border_radius_last: BorderRadius::from([8.0, 8.0, 0.0, 0.0]), - border_radius_middle: BorderRadius::from([8.0, 8.0, 0.0, 0.0]), + first: segmented_button::ButtonAppearance { + border_radius: BorderRadius::from([8.0, 8.0, 0.0, 0.0]), + border_bottom: Some((4.0, cosmic.accent.base.into())), + ..Default::default() + }, + middle: segmented_button::ButtonAppearance { + border_radius: BorderRadius::from([8.0, 8.0, 0.0, 0.0]), + border_bottom: Some((4.0, cosmic.accent.base.into())), + ..Default::default() + }, + last: segmented_button::ButtonAppearance { + border_radius: BorderRadius::from([8.0, 8.0, 0.0, 0.0]), + border_bottom: Some((4.0, cosmic.accent.base.into())), + ..Default::default() + }, text_color: cosmic.accent.base.into(), }, - button_inactive: segmented_button::ButtonAppearance { + inactive: segmented_button::ButtonStatusAppearance { background: None, - border_bottom: Some((1.0, cosmic.accent.base.into())), - border_radius_first: BorderRadius::from(0.0), - border_radius_last: BorderRadius::from(0.0), - border_radius_middle: BorderRadius::from(0.0), + first: segmented_button::ButtonAppearance { + border_radius: BorderRadius::from(0.0), + border_bottom: Some((1.0, cosmic.accent.base.into())), + ..Default::default() + }, + middle: segmented_button::ButtonAppearance { + border_radius: BorderRadius::from(0.0), + border_bottom: Some((1.0, cosmic.accent.base.into())), + ..Default::default() + }, + last: segmented_button::ButtonAppearance { + border_radius: BorderRadius::from(0.0), + border_bottom: Some((1.0, cosmic.accent.base.into())), + ..Default::default() + }, text_color: cosmic.primary.on.into(), }, - button_hover: segmented_button::ButtonAppearance { + hover: segmented_button::ButtonStatusAppearance { background: Some(Background::Color(cosmic.primary.component.hover.into())), - border_bottom: Some((1.0, cosmic.accent.base.into())), - border_radius_first: BorderRadius::from([8.0, 8.0, 0.0, 0.0]), - border_radius_last: BorderRadius::from([8.0, 8.0, 0.0, 0.0]), - border_radius_middle: BorderRadius::from([8.0, 8.0, 0.0, 0.0]), + first: segmented_button::ButtonAppearance { + border_radius: BorderRadius::from([8.0, 8.0, 0.0, 0.0]), + border_bottom: Some((1.0, cosmic.accent.base.into())), + ..Default::default() + }, + middle: segmented_button::ButtonAppearance { + border_radius: BorderRadius::from([8.0, 8.0, 0.0, 0.0]), + border_bottom: Some((1.0, cosmic.accent.base.into())), + ..Default::default() + }, + last: segmented_button::ButtonAppearance { + border_radius: BorderRadius::from([8.0, 8.0, 0.0, 0.0]), + border_bottom: Some((1.0, cosmic.accent.base.into())), + ..Default::default() + }, text_color: cosmic.accent.base.into(), }, + ..Default::default() } } SegmentedButton::Selection => { let cosmic = self.cosmic(); segmented_button::Appearance { - background: None, - border_color: Color::TRANSPARENT, border_radius: BorderRadius::from(0.0), - border_width: 0.0, - button_active: segmented_button::ButtonAppearance { + active: segmented_button::ButtonStatusAppearance { background: Some(Background::Color( cosmic.secondary.component.divider.into(), )), - border_bottom: None, - border_radius_first: BorderRadius::from([24.0, 0.0, 0.0, 24.0]), - border_radius_last: BorderRadius::from([0.0, 24.0, 24.0, 0.0]), - border_radius_middle: BorderRadius::from(0.0), + first: segmented_button::ButtonAppearance { + border_radius: BorderRadius::from([24.0, 0.0, 0.0, 24.0]), + ..Default::default() + }, + middle: segmented_button::ButtonAppearance { + border_radius: BorderRadius::from(0.0), + ..Default::default() + }, + last: segmented_button::ButtonAppearance { + border_radius: BorderRadius::from([0.0, 24.0, 24.0, 0.0]), + ..Default::default() + }, text_color: cosmic.accent.base.into(), }, - button_inactive: segmented_button::ButtonAppearance { + inactive: segmented_button::ButtonStatusAppearance { background: Some(Background::Color(cosmic.secondary.component.base.into())), - border_bottom: None, - border_radius_first: BorderRadius::from([24.0, 0.0, 0.0, 24.0]), - border_radius_last: BorderRadius::from([0.0, 24.0, 24.0, 0.0]), - border_radius_middle: BorderRadius::from(0.0), + first: segmented_button::ButtonAppearance { + border_radius: BorderRadius::from([24.0, 0.0, 0.0, 24.0]), + ..Default::default() + }, + middle: segmented_button::ButtonAppearance { + border_radius: BorderRadius::from(0.0), + ..Default::default() + }, + last: segmented_button::ButtonAppearance { + border_radius: BorderRadius::from([0.0, 24.0, 24.0, 0.0]), + ..Default::default() + }, text_color: cosmic.primary.on.into(), }, - button_hover: segmented_button::ButtonAppearance { + hover: segmented_button::ButtonStatusAppearance { background: Some(Background::Color(cosmic.primary.component.hover.into())), - border_bottom: None, - border_radius_first: BorderRadius::from([24.0, 0.0, 0.0, 24.0]), - border_radius_last: BorderRadius::from([0.0, 24.0, 24.0, 0.0]), - border_radius_middle: BorderRadius::from(0.0), + first: segmented_button::ButtonAppearance { + border_radius: BorderRadius::from([24.0, 0.0, 0.0, 24.0]), + ..Default::default() + }, + middle: segmented_button::ButtonAppearance { + border_radius: BorderRadius::from(0.0), + ..Default::default() + }, + last: segmented_button::ButtonAppearance { + border_radius: BorderRadius::from([0.0, 24.0, 24.0, 0.0]), + ..Default::default() + }, text_color: cosmic.accent.base.into(), }, + ..Default::default() } } SegmentedButton::Custom(func) => func(self), @@ -98,69 +151,113 @@ impl segmented_button::StyleSheet for Theme { SegmentedButton::ViewSwitcher => { let cosmic = self.cosmic(); segmented_button::Appearance { - background: None, - border_color: Color::TRANSPARENT, border_radius: BorderRadius::from(0.0), - border_width: 0.0, - button_active: segmented_button::ButtonAppearance { + active: segmented_button::ButtonStatusAppearance { background: Some(Background::Color(cosmic.primary.component.base.into())), - border_bottom: None, - border_radius_first: BorderRadius::from(24.0), - border_radius_last: BorderRadius::from(24.0), - border_radius_middle: BorderRadius::from(24.0), + first: segmented_button::ButtonAppearance { + border_radius: BorderRadius::from(24.0), + ..Default::default() + }, + middle: segmented_button::ButtonAppearance { + border_radius: BorderRadius::from(24.0), + ..Default::default() + }, + last: segmented_button::ButtonAppearance { + border_radius: BorderRadius::from(24.0), + ..Default::default() + }, text_color: cosmic.accent.base.into(), }, - button_inactive: segmented_button::ButtonAppearance { + inactive: segmented_button::ButtonStatusAppearance { background: None, - border_bottom: None, - border_radius_first: BorderRadius::from(24.0), - border_radius_last: BorderRadius::from(24.0), - border_radius_middle: BorderRadius::from(24.0), + first: segmented_button::ButtonAppearance { + border_radius: BorderRadius::from(24.0), + ..Default::default() + }, + middle: segmented_button::ButtonAppearance { + border_radius: BorderRadius::from(24.0), + ..Default::default() + }, + last: segmented_button::ButtonAppearance { + border_radius: BorderRadius::from(24.0), + ..Default::default() + }, text_color: cosmic.primary.on.into(), }, - button_hover: segmented_button::ButtonAppearance { + hover: segmented_button::ButtonStatusAppearance { background: Some(Background::Color(cosmic.primary.component.hover.into())), - border_bottom: None, - border_radius_first: BorderRadius::from(24.0), - border_radius_last: BorderRadius::from(24.0), - border_radius_middle: BorderRadius::from(24.0), + first: segmented_button::ButtonAppearance { + border_radius: BorderRadius::from(24.0), + ..Default::default() + }, + middle: segmented_button::ButtonAppearance { + border_radius: BorderRadius::from(24.0), + ..Default::default() + }, + last: segmented_button::ButtonAppearance { + border_radius: BorderRadius::from(24.0), + ..Default::default() + }, text_color: cosmic.accent.base.into(), }, + ..Default::default() } } SegmentedButton::Selection => { let cosmic = self.cosmic(); segmented_button::Appearance { - background: None, - border_color: Color::TRANSPARENT, border_radius: BorderRadius::from(0.0), - border_width: 0.0, - button_active: segmented_button::ButtonAppearance { + active: segmented_button::ButtonStatusAppearance { background: Some(Background::Color( cosmic.secondary.component.divider.into(), )), - border_bottom: None, - border_radius_first: BorderRadius::from([24.0, 24.0, 0.0, 0.0]), - border_radius_last: BorderRadius::from([0.0, 0.0, 24.0, 24.0]), - border_radius_middle: BorderRadius::from(0.0), + first: segmented_button::ButtonAppearance { + border_radius: BorderRadius::from([24.0, 24.0, 0.0, 0.0]), + ..Default::default() + }, + middle: segmented_button::ButtonAppearance { + border_radius: BorderRadius::from(0.0), + ..Default::default() + }, + last: segmented_button::ButtonAppearance { + border_radius: BorderRadius::from([0.0, 0.0, 24.0, 24.0]), + ..Default::default() + }, text_color: cosmic.accent.base.into(), }, - button_inactive: segmented_button::ButtonAppearance { + inactive: segmented_button::ButtonStatusAppearance { background: Some(Background::Color(cosmic.secondary.component.base.into())), - border_bottom: None, - border_radius_first: BorderRadius::from([24.0, 24.0, 0.0, 0.0]), - border_radius_last: BorderRadius::from([0.0, 0.0, 24.0, 24.0]), - border_radius_middle: BorderRadius::from(0.0), + first: segmented_button::ButtonAppearance { + border_radius: BorderRadius::from([24.0, 24.0, 0.0, 0.0]), + ..Default::default() + }, + middle: segmented_button::ButtonAppearance { + border_radius: BorderRadius::from(0.0), + ..Default::default() + }, + last: segmented_button::ButtonAppearance { + border_radius: BorderRadius::from([0.0, 0.0, 24.0, 24.0]), + ..Default::default() + }, text_color: cosmic.primary.on.into(), }, - button_hover: segmented_button::ButtonAppearance { + hover: segmented_button::ButtonStatusAppearance { background: Some(Background::Color(cosmic.primary.component.hover.into())), - border_bottom: None, - border_radius_first: BorderRadius::from([24.0, 24.0, 0.0, 0.0]), - border_radius_last: BorderRadius::from([0.0, 0.0, 24.0, 24.0]), - border_radius_middle: BorderRadius::from(0.0), + first: segmented_button::ButtonAppearance { + border_radius: BorderRadius::from([24.0, 24.0, 0.0, 0.0]), + ..Default::default() + }, + middle: segmented_button::ButtonAppearance { + border_radius: BorderRadius::from(0.0), + ..Default::default() + }, + last: segmented_button::ButtonAppearance { + border_radius: BorderRadius::from([0.0, 0.0, 24.0, 24.0]), + ..Default::default() + }, text_color: cosmic.accent.base.into(), }, + ..Default::default() } } SegmentedButton::Custom(func) => func(self), diff --git a/src/widget/mod.rs b/src/widget/mod.rs index c8814534..039d842e 100644 --- a/src/widget/mod.rs +++ b/src/widget/mod.rs @@ -25,7 +25,6 @@ pub use toggler::toggler; pub mod segmented_button; pub use segmented_button::{ horizontal_segmented_button, vertical_segmented_button, HorizontalSegmentedButton, - VerticalSegmentedButton, }; pub mod settings; diff --git a/src/widget/segmented_button/cosmic.rs b/src/widget/segmented_button/cosmic.rs index be48e2f0..1628774c 100644 --- a/src/widget/segmented_button/cosmic.rs +++ b/src/widget/segmented_button/cosmic.rs @@ -1,7 +1,7 @@ // Copyright 2022 System76 // SPDX-License-Identifier: MPL-2.0 -use super::{HorizontalSegmentedButton, State, VerticalSegmentedButton}; +use super::{HorizontalSegmentedButton, SegmentedButton, State, VerticalSegmentedButton}; /// Appears as a collection of tabs for developing a tabbed interface. /// @@ -10,7 +10,7 @@ use super::{HorizontalSegmentedButton, State, VerticalSegmentedButton}; pub fn horizontal_view_switcher( state: &State, ) -> HorizontalSegmentedButton { - HorizontalSegmentedButton::new(&state.inner) + SegmentedButton::new(&state.inner) .button_padding([16, 0, 16, 0]) .button_height(48) .style(crate::theme::SegmentedButton::ViewSwitcher) @@ -24,7 +24,7 @@ pub fn horizontal_view_switcher( pub fn horizontal_segmented_selection( state: &State, ) -> HorizontalSegmentedButton { - HorizontalSegmentedButton::new(&state.inner) + SegmentedButton::new(&state.inner) .button_padding([16, 0, 16, 0]) .button_height(32) .style(crate::theme::SegmentedButton::Selection) @@ -38,7 +38,7 @@ pub fn horizontal_segmented_selection( pub fn vertical_segmented_selection( state: &State, ) -> VerticalSegmentedButton { - VerticalSegmentedButton::new(&state.inner) + SegmentedButton::new(&state.inner) .button_padding([16, 0, 16, 0]) .button_height(32) .style(crate::theme::SegmentedButton::Selection) @@ -52,7 +52,7 @@ pub fn vertical_segmented_selection( pub fn vertical_view_switcher( state: &State, ) -> VerticalSegmentedButton { - VerticalSegmentedButton::new(&state.inner) + SegmentedButton::new(&state.inner) .button_padding([16, 0, 16, 0]) .button_height(48) .style(crate::theme::SegmentedButton::ViewSwitcher) diff --git a/src/widget/segmented_button/horizontal.rs b/src/widget/segmented_button/horizontal.rs index e51594b4..5dcaeddc 100644 --- a/src/widget/segmented_button/horizontal.rs +++ b/src/widget/segmented_button/horizontal.rs @@ -1,172 +1,72 @@ -// Copyright 2023 System76 -// SPDX-License-Identifier: MPL-2.0 - -use super::state::{Key, SharedWidgetState, State}; +use super::state::State; use super::style::StyleSheet; -use super::UniqueWidgetState; +use super::widget::{SegmentedButton, SegmentedVariant}; -use derive_setters::Setters; -use iced::{ - alignment::{Horizontal, Vertical}, - event, mouse, touch, Background, Color, Element, Event, Length, Point, Rectangle, Size, -}; -use iced_core::BorderRadius; -use iced_native::widget::tree; -use iced_native::{layout, renderer, widget::Tree, Clipboard, Layout, Shell, Widget}; +use iced::{Length, Rectangle, Size}; +use iced_native::layout; -/// Creates a [`HorizontalSegmentedButton`]. +/// A type marker defining the horizontal variant of a [`SegmentedButton`]. +pub struct Horizontal; + +/// Horizontal [`SegmentedButton`]. +pub type HorizontalSegmentedButton<'a, Message, Renderer> = + SegmentedButton<'a, Horizontal, Message, Renderer>; + +/// Horizontal implementation of the [`SegmentedButton`]. #[must_use] pub fn horizontal_segmented_button( state: &State, -) -> HorizontalSegmentedButton +) -> SegmentedButton where Renderer: iced_native::Renderer + iced_native::text::Renderer, Renderer::Theme: StyleSheet, { - HorizontalSegmentedButton::new(&state.inner) + SegmentedButton::new(&state.inner) } -/// A widget providing a conjoined set of horizontally-arranged buttons for choosing between. -/// -/// The data for the widget comes from a [`State`] that is maintained the application. -#[derive(Setters)] -pub struct HorizontalSegmentedButton<'a, Message, Renderer> +impl<'a, Message, Renderer> SegmentedVariant for SegmentedButton<'a, Horizontal, Message, Renderer> where Renderer: iced_native::Renderer + iced_native::text::Renderer, Renderer::Theme: StyleSheet, { - /// Contains application state also used for drawing. - #[setters(skip)] - state: &'a SharedWidgetState, - /// Desired font for active tabs. - font_active: Renderer::Font, - /// Desired font for hovered tabs. - font_hovered: Renderer::Font, - /// Desired font for inactive tabs. - font_inactive: Renderer::Font, - /// Desired width of the widget. - width: Length, - /// Desired height of the widget. - height: Length, - /// Padding around a button. - button_padding: [u16; 4], - /// Desired height of a button. - button_height: u16, - /// Desired spacing between buttons. - spacing: u16, - /// Style to draw the widget in. - #[setters(into)] - style: ::Style, - /// Emits the ID of the activated widget on selection. - #[setters(skip)] - on_activate: Option Message>>, -} + type Renderer = Renderer; -impl<'a, Message, Renderer> HorizontalSegmentedButton<'a, Message, Renderer> -where - Renderer: iced_native::Renderer + iced_native::text::Renderer, - Renderer::Theme: StyleSheet, -{ - #[must_use] - pub fn new(state: &'a SharedWidgetState) -> Self { - Self { - state, - font_active: Renderer::Font::default(), - font_hovered: Renderer::Font::default(), - font_inactive: Renderer::Font::default(), - height: Length::Shrink, - width: Length::Fill, - button_padding: [4, 4, 4, 4], - button_height: 32, - spacing: 0, - style: ::Style::default(), - on_activate: None, - } + fn variant_appearance( + theme: &::Theme, + style: &<::Theme as StyleSheet>::Style, + ) -> super::Appearance { + theme.horizontal(style) } - /// Emits the ID of the activated widget on selection. - #[must_use] - pub fn on_activate(mut self, on_activate: impl Fn(Key) -> Message + 'static) -> Self { - self.on_activate = Some(Box::from(on_activate)); - self - } + #[allow(clippy::cast_precision_loss)] + fn variant_button_bounds(&self, mut bounds: Rectangle, nth: usize) -> Rectangle { + let num = self.state.buttons.len(); + if num != 0 { + let spacing = f32::from(self.spacing); + bounds.width = (bounds.width - (num as f32 * spacing) + spacing) / num as f32; - /// Creates a closure for generating the layout bounds of the buttons. - fn button_bounds(&self, bounds: Rectangle) -> impl FnMut() -> Rectangle { - let button_amount = self.state.buttons.len(); - let width = bounds.width / button_amount as f32; - let spacing = self.spacing as f32; - let half = spacing / 2.0; - let mut bounds = bounds; - bounds.width = width; - let mut counter = 1; - - move || { - let mut clone = bounds; - if counter == 1 { - clone.width -= half; - } else if counter == button_amount { - clone.x += spacing; - clone.width -= spacing; - } else { - clone.x += half; - clone.width -= half; + if nth != 0 { + bounds.x += (nth as f32 * bounds.width) + (nth as f32 * spacing); } - - bounds.x += width; - counter += 1; - clone } + + bounds } - 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 += self.button_padding[0] as f32 + self.button_padding[2] as f32; - h += self.button_padding[1] as f32 + self.button_padding[3] as f32; - h = h.max(self.button_height as f32); - (w, h) - } -} - -impl<'a, Message, Renderer> Widget - for HorizontalSegmentedButton<'a, Message, Renderer> -where - Renderer: iced_native::Renderer + iced_native::text::Renderer, - Renderer::Theme: StyleSheet, - Message: 'static + Clone, -{ - fn tag(&self) -> tree::Tag { - tree::Tag::of::() - } - - fn state(&self) -> tree::State { - tree::State::new(UniqueWidgetState::default()) - } - - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - self.height - } - - fn layout(&self, renderer: &Renderer, limits: &layout::Limits) -> layout::Node { - let mut width = 0.0f32; - let mut height = 0.0f32; + #[allow(clippy::cast_precision_loss)] + #[allow(clippy::cast_possible_truncation)] + #[allow(clippy::cast_sign_loss)] + fn variant_layout(&self, renderer: &Renderer, limits: &layout::Limits) -> layout::Node { let limits = limits.width(self.width); let text_size = renderer.default_size(); - for (_, content) in self.state.buttons.iter() { - let (w, h) = self.measure_button(renderer, &content.text, text_size, limits.max()); - width += w + f32::from(self.spacing * 2); - height = height.max(h); + let (mut width, height) = self.max_button_dimensions(renderer, text_size, limits.max()); + + let num = self.state.buttons.len(); + let spacing = f32::from(self.spacing); + + if num != 0 { + width = (num as f32 * width) + (num as f32 * spacing) - spacing; } let size = limits @@ -175,176 +75,4 @@ where layout::Node::new(size) } - - fn on_event( - &mut self, - tree: &mut Tree, - event: Event, - layout: Layout<'_>, - cursor_position: Point, - _renderer: &Renderer, - _clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - ) -> event::Status { - let bounds = layout.bounds(); - let mut bounds_generator = self.button_bounds(bounds); - let state = tree.state.downcast_mut::(); - - if bounds.contains(cursor_position) { - for (key, _) in self.state.buttons.iter() { - let bounds = bounds_generator(); - if bounds.contains(cursor_position) { - // Record that the mouse is hovering over this button. - state.hovered = key; - - if let Some(on_activate) = self.on_activate.as_ref() { - if let Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerLifted { .. }) = event - { - shell.publish(on_activate(key)); - return event::Status::Captured; - } - } - } - } - } else { - state.hovered = Key::default(); - } - - event::Status::Ignored - } - - fn mouse_interaction( - &self, - _tree: &Tree, - layout: Layout<'_>, - cursor_position: iced::Point, - _viewport: &iced::Rectangle, - _renderer: &Renderer, - ) -> iced_native::mouse::Interaction { - let mut generator = self.button_bounds(layout.bounds()); - - if (0..self.state.buttons.len()).any(move |_| generator().contains(cursor_position)) { - iced_native::mouse::Interaction::Pointer - } else { - iced_native::mouse::Interaction::Idle - } - } - - fn draw( - &self, - tree: &Tree, - renderer: &mut Renderer, - theme: &::Theme, - _style: &renderer::Style, - layout: Layout<'_>, - _cursor_position: iced::Point, - _viewport: &iced::Rectangle, - ) { - let state = tree.state.downcast_ref::(); - let appearance = theme.horizontal(&self.style); - let bounds = layout.bounds(); - let button_amount = self.state.buttons.len(); - - let mut bounds_generator = self.button_bounds(bounds); - - // Draw the background, if a background was defined. - if let Some(background) = appearance.background { - renderer.fill_quad( - renderer::Quad { - bounds, - border_radius: appearance.border_radius, - border_width: appearance.border_width, - border_color: appearance.border_color, - }, - background, - ); - } - - // Draw each of the buttons in the widget. - for (num, (key, content)) in self.state.buttons.iter().enumerate() { - let bounds = bounds_generator(); - - let (button_appearance, font) = if self.state.active == key { - (appearance.button_active, &self.font_active) - } else if state.hovered == key { - (appearance.button_hover, &self.font_hovered) - } else { - (appearance.button_inactive, &self.font_inactive) - }; - - let x = bounds.center_x(); - let y = bounds.center_y(); - - // Render the background of the button. - if button_appearance.background.is_some() { - renderer.fill_quad( - renderer::Quad { - bounds, - border_radius: if num == 0 { - button_appearance.border_radius_first - } else if num + 1 == button_amount { - button_appearance.border_radius_last - } else { - button_appearance.border_radius_middle - }, - border_width: 0.0, - border_color: Color::TRANSPARENT, - }, - button_appearance - .background - .unwrap_or(Background::Color(Color::TRANSPARENT)), - ); - } - - // Draw the bottom border defined for this button. - if let Some((width, background)) = button_appearance.border_bottom { - let mut bounds = bounds; - bounds.y = bounds.y + bounds.height - width; - bounds.height = width; - - renderer.fill_quad( - renderer::Quad { - bounds, - border_radius: BorderRadius::from(0.0), - border_width: 0.0, - border_color: Color::TRANSPARENT, - }, - background, - ); - } - - // 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: button_appearance.text_color, - font: font.clone(), - horizontal_alignment: Horizontal::Center, - vertical_alignment: Vertical::Center, - }); - } - } - - fn overlay<'b>( - &'b self, - _tree: &'b mut Tree, - _layout: iced_native::Layout<'_>, - _renderer: &Renderer, - ) -> Option> { - None - } -} - -impl<'a, Message, Renderer> From> - for Element<'a, Message, Renderer> -where - Renderer: iced_native::Renderer + iced_native::text::Renderer + 'a, - Renderer::Theme: StyleSheet, - Message: 'static + Clone, -{ - fn from(widget: HorizontalSegmentedButton<'a, Message, Renderer>) -> Self { - Self::new(widget) - } } diff --git a/src/widget/segmented_button/mod.rs b/src/widget/segmented_button/mod.rs index 77211ef0..73e21e81 100644 --- a/src/widget/segmented_button/mod.rs +++ b/src/widget/segmented_button/mod.rs @@ -48,15 +48,10 @@ mod horizontal; mod state; mod style; mod vertical; +mod widget; -pub use self::horizontal::{horizontal_segmented_button, HorizontalSegmentedButton}; +pub use self::horizontal::{horizontal_segmented_button, Horizontal, HorizontalSegmentedButton}; pub use self::state::{ButtonContent, Key, SecondaryState, SharedWidgetState, State}; -pub use self::style::{Appearance, ButtonAppearance, StyleSheet}; -pub use self::vertical::{vertical_segmented_button, VerticalSegmentedButton}; - -/// State that is maintained by each individual widget. -#[derive(Default)] -struct UniqueWidgetState { - /// The ID of the button that is being hovered. Defaults to null. - hovered: Key, -} +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 e62293b0..b4fa8ad2 100644 --- a/src/widget/segmented_button/state.rs +++ b/src/widget/segmented_button/state.rs @@ -11,7 +11,7 @@ slotmap::new_key_type! { /// Contains all state for interacting with a segmented button. pub struct State { - /// State that is shareable across widget(s). + /// State that is shared with widget drawing. pub inner: SharedWidgetState, /// State unique to the application. diff --git a/src/widget/segmented_button/style.rs b/src/widget/segmented_button/style.rs index 0db84e4b..35316b5d 100644 --- a/src/widget/segmented_button/style.rs +++ b/src/widget/segmented_button/style.rs @@ -4,25 +4,35 @@ use iced_core::{Background, BorderRadius, Color}; /// The appearance of a segmented button. -#[derive(Clone, Copy)] +#[derive(Default, Clone, Copy)] pub struct Appearance { pub background: Option, - pub border_color: Color, pub border_radius: BorderRadius, - pub border_width: f32, - pub button_active: ButtonAppearance, - pub button_inactive: ButtonAppearance, - pub button_hover: ButtonAppearance, + pub border_bottom: Option<(f32, Color)>, + pub border_end: Option<(f32, Color)>, + pub border_start: Option<(f32, Color)>, + pub border_top: Option<(f32, Color)>, + pub active: ButtonStatusAppearance, + pub inactive: ButtonStatusAppearance, + pub hover: ButtonStatusAppearance, } /// The appearance of a button in the segmented button -#[derive(Clone, Copy)] +#[derive(Default, Clone, Copy)] pub struct ButtonAppearance { - pub background: Option, + pub border_radius: BorderRadius, pub border_bottom: Option<(f32, Color)>, - pub border_radius_first: BorderRadius, - pub border_radius_middle: BorderRadius, - pub border_radius_last: BorderRadius, + pub border_end: Option<(f32, Color)>, + pub border_start: Option<(f32, Color)>, + pub border_top: Option<(f32, Color)>, +} + +#[derive(Default, Clone, Copy)] +pub struct ButtonStatusAppearance { + pub background: Option, + pub first: ButtonAppearance, + pub middle: ButtonAppearance, + pub last: ButtonAppearance, pub text_color: Color, } diff --git a/src/widget/segmented_button/vertical.rs b/src/widget/segmented_button/vertical.rs index 66124467..5e9c4e69 100644 --- a/src/widget/segmented_button/vertical.rs +++ b/src/widget/segmented_button/vertical.rs @@ -1,169 +1,72 @@ -use super::state::{Key, SharedWidgetState, State}; +use super::state::State; use super::style::StyleSheet; -use super::UniqueWidgetState; +use super::widget::{SegmentedButton, SegmentedVariant}; -use derive_setters::Setters; -use iced::{ - alignment::{Horizontal, Vertical}, - event, mouse, touch, Background, Color, Element, Event, Length, Point, Rectangle, Size, -}; -use iced_core::BorderRadius; -use iced_native::widget::tree; -use iced_native::{layout, renderer, widget::Tree, Clipboard, Layout, Shell, Widget}; +use iced::{Length, Rectangle, Size}; +use iced_native::layout; -/// Creates a [`VerticalSegmentedButton`]. +/// A type marker defining the vertical variant of a [`SegmentedButton`]. +pub struct Vertical; + +/// Vertical [`SegmentedButton`]. +pub type VerticalSegmentedButton<'a, Message, Renderer> = + SegmentedButton<'a, Vertical, Message, Renderer>; + +/// Vertical implementation of the [`SegmentedButton`]. #[must_use] pub fn vertical_segmented_button( state: &State, -) -> VerticalSegmentedButton +) -> SegmentedButton where Renderer: iced_native::Renderer + iced_native::text::Renderer, Renderer::Theme: StyleSheet, { - VerticalSegmentedButton::new(&state.inner) + SegmentedButton::new(&state.inner) } -/// A widget providing a conjoined set of linear buttons for choosing between. -/// -/// The data for the widget comes from a [`State`] that is maintained the application. -#[derive(Setters)] -pub struct VerticalSegmentedButton<'a, Message, Renderer> +impl<'a, Message, Renderer> SegmentedVariant for SegmentedButton<'a, Vertical, Message, Renderer> where Renderer: iced_native::Renderer + iced_native::text::Renderer, Renderer::Theme: StyleSheet, { - /// Contains application state also used for drawing. - #[setters(skip)] - state: &'a SharedWidgetState, - /// Desired font for active tabs. - font_active: Renderer::Font, - /// Desired font for hovered tabs. - font_hovered: Renderer::Font, - /// Desired font for inactive tabs. - font_inactive: Renderer::Font, - /// Desired width of the widget. - width: Length, - /// Desired height of the widget. - height: Length, - /// Padding around a button. - button_padding: [u16; 4], - /// Desired height of a button. - button_height: u16, - /// Desired spacing between buttons. - spacing: u16, - /// Style to draw the widget in. - #[setters(into)] - style: ::Style, - /// Emits the ID of the activated widget on selection. - #[setters(skip)] - on_activate: Option Message>>, -} + type Renderer = Renderer; -impl<'a, Message, Renderer> VerticalSegmentedButton<'a, Message, Renderer> -where - Renderer: iced_native::Renderer + iced_native::text::Renderer, - Renderer::Theme: StyleSheet, -{ - #[must_use] - pub fn new(state: &'a SharedWidgetState) -> Self { - Self { - state, - font_active: Renderer::Font::default(), - font_hovered: Renderer::Font::default(), - font_inactive: Renderer::Font::default(), - height: Length::Shrink, - width: Length::Fill, - button_padding: [4, 4, 4, 4], - button_height: 32, - spacing: 0, - style: ::Style::default(), - on_activate: None, - } + fn variant_appearance( + theme: &::Theme, + style: &<::Theme as StyleSheet>::Style, + ) -> super::Appearance { + theme.vertical(style) } - /// Emits the ID of the activated widget on selection. - #[must_use] - pub fn on_activate(mut self, on_activate: impl Fn(Key) -> Message + 'static) -> Self { - self.on_activate = Some(Box::from(on_activate)); - self - } + #[allow(clippy::cast_precision_loss)] + fn variant_button_bounds(&self, mut bounds: Rectangle, nth: usize) -> Rectangle { + let num = self.state.buttons.len(); + if num != 0 { + let spacing = f32::from(self.spacing); + bounds.height = (bounds.height - (num as f32 * spacing) + spacing) / num as f32; - /// Creates a closure for generating the layout bounds of the buttons. - fn button_bounds(&self, bounds: Rectangle) -> impl FnMut() -> Rectangle { - let button_amount = self.state.buttons.len(); - let height = bounds.height / button_amount as f32; - let spacing = self.spacing as f32; - let half = spacing / 2.0; - let mut bounds = bounds; - bounds.height = height; - let mut counter = 1; - - move || { - let mut clone = bounds; - if counter == 1 { - clone.height -= half; - } else if counter == button_amount { - clone.y += spacing; - clone.height -= spacing; - } else { - clone.y += half; - clone.height -= half; + if nth != 0 { + bounds.y += (nth as f32 * bounds.height) + (nth as f32 * spacing); } - - bounds.y += height; - counter += 1; - clone } + + bounds } - 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 += self.button_padding[0] as f32 + self.button_padding[2] as f32; - h += self.button_padding[1] as f32 + self.button_padding[3] as f32; - h = h.max(self.button_height as f32); - (w, h) - } -} - -impl<'a, Message, Renderer> Widget - for VerticalSegmentedButton<'a, Message, Renderer> -where - Renderer: iced_native::Renderer + iced_native::text::Renderer, - Renderer::Theme: StyleSheet, - Message: 'static + Clone, -{ - fn tag(&self) -> tree::Tag { - tree::Tag::of::() - } - - fn state(&self) -> tree::State { - tree::State::new(UniqueWidgetState::default()) - } - - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - self.height - } - - fn layout(&self, renderer: &Renderer, limits: &layout::Limits) -> layout::Node { - let mut width = 0.0f32; - let mut height = 0.0f32; + #[allow(clippy::cast_precision_loss)] + #[allow(clippy::cast_possible_truncation)] + #[allow(clippy::cast_sign_loss)] + fn variant_layout(&self, renderer: &Renderer, limits: &layout::Limits) -> layout::Node { let limits = limits.width(self.width); let text_size = renderer.default_size(); - for (_, content) in self.state.buttons.iter() { - let (w, h) = self.measure_button(renderer, &content.text, text_size, limits.max()); - height += h + f32::from(self.spacing * 2); - width = width.max(w); + let (width, mut height) = self.max_button_dimensions(renderer, text_size, limits.max()); + + let num = self.state.buttons.len(); + let spacing = f32::from(self.spacing); + + if num != 0 { + height = (num as f32 * height) + (num as f32 * spacing) - spacing; } let size = limits @@ -172,176 +75,4 @@ where layout::Node::new(size) } - - fn on_event( - &mut self, - tree: &mut Tree, - event: Event, - layout: Layout<'_>, - cursor_position: Point, - _renderer: &Renderer, - _clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - ) -> event::Status { - let bounds = layout.bounds(); - let mut bounds_generator = self.button_bounds(bounds); - let state = tree.state.downcast_mut::(); - - if bounds.contains(cursor_position) { - for (key, _) in self.state.buttons.iter() { - let bounds = bounds_generator(); - if bounds.contains(cursor_position) { - // Record that the mouse is hovering over this button. - state.hovered = key; - - if let Some(on_activate) = self.on_activate.as_ref() { - if let Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerLifted { .. }) = event - { - shell.publish(on_activate(key)); - return event::Status::Captured; - } - } - } - } - } else { - state.hovered = Key::default(); - } - - event::Status::Ignored - } - - fn mouse_interaction( - &self, - _tree: &Tree, - layout: Layout<'_>, - cursor_position: iced::Point, - _viewport: &iced::Rectangle, - _renderer: &Renderer, - ) -> iced_native::mouse::Interaction { - let mut generator = self.button_bounds(layout.bounds()); - - if (0..self.state.buttons.len()).any(move |_| generator().contains(cursor_position)) { - iced_native::mouse::Interaction::Pointer - } else { - iced_native::mouse::Interaction::Idle - } - } - - fn draw( - &self, - tree: &Tree, - renderer: &mut Renderer, - theme: &::Theme, - _style: &renderer::Style, - layout: Layout<'_>, - _cursor_position: iced::Point, - _viewport: &iced::Rectangle, - ) { - let state = tree.state.downcast_ref::(); - let appearance = theme.vertical(&self.style); - let bounds = layout.bounds(); - let button_amount = self.state.buttons.len(); - - let mut bounds_generator = self.button_bounds(bounds); - - // Draw the background, if a background was defined. - if let Some(background) = appearance.background { - renderer.fill_quad( - renderer::Quad { - bounds, - border_radius: appearance.border_radius, - border_width: appearance.border_width, - border_color: appearance.border_color, - }, - background, - ); - } - - // Draw each of the buttons in the widget. - for (num, (key, content)) in self.state.buttons.iter().enumerate() { - let bounds = bounds_generator(); - - let (button_appearance, font) = if self.state.active == key { - (appearance.button_active, &self.font_active) - } else if state.hovered == key { - (appearance.button_hover, &self.font_hovered) - } else { - (appearance.button_inactive, &self.font_inactive) - }; - - let x = bounds.center_x(); - let y = bounds.center_y(); - - // Render the background of the button. - if button_appearance.background.is_some() { - renderer.fill_quad( - renderer::Quad { - bounds, - border_radius: if num == 0 { - button_appearance.border_radius_first - } else if num + 1 == button_amount { - button_appearance.border_radius_last - } else { - button_appearance.border_radius_middle - }, - border_width: 0.0, - border_color: Color::TRANSPARENT, - }, - button_appearance - .background - .unwrap_or(Background::Color(Color::TRANSPARENT)), - ); - } - - // Draw the bottom border defined for this button. - if let Some((width, background)) = button_appearance.border_bottom { - let mut bounds = bounds; - bounds.y = bounds.y + bounds.height - width; - bounds.height = width; - - renderer.fill_quad( - renderer::Quad { - bounds, - border_radius: BorderRadius::from(0.0), - border_width: 0.0, - border_color: Color::TRANSPARENT, - }, - background, - ); - } - - // 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: button_appearance.text_color, - font: font.clone(), - horizontal_alignment: Horizontal::Center, - vertical_alignment: Vertical::Center, - }); - } - } - - fn overlay<'b>( - &'b self, - _tree: &'b mut Tree, - _layout: iced_native::Layout<'_>, - _renderer: &Renderer, - ) -> Option> { - None - } -} - -impl<'a, Message, Renderer> From> - for Element<'a, Message, Renderer> -where - Renderer: iced_native::Renderer + iced_native::text::Renderer + 'a, - Renderer::Theme: StyleSheet, - Message: 'static + Clone, -{ - fn from(widget: VerticalSegmentedButton<'a, Message, Renderer>) -> Self { - Self::new(widget) - } } diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs new file mode 100644 index 00000000..e4c14fd6 --- /dev/null +++ b/src/widget/segmented_button/widget.rs @@ -0,0 +1,347 @@ +use std::marker::PhantomData; + +use super::state::{Key, SharedWidgetState}; +use super::style::StyleSheet; + +use derive_setters::Setters; +use iced::{ + alignment, event, mouse, touch, Background, Color, Element, Event, Length, Point, Rectangle, + Size, +}; +use iced_core::BorderRadius; +use iced_native::widget::tree; +use iced_native::{layout, renderer, widget::Tree, Clipboard, Layout, Shell, Widget}; + +/// Isolates variant-specific behaviors from [`SegmentedButton`]. +pub trait SegmentedVariant { + type Renderer: iced_native::Renderer; + + /// Get the appearance for this variant of the widget. + fn variant_appearance( + theme: &::Theme, + style: &<::Theme as StyleSheet>::Style, + ) -> super::Appearance + where + ::Theme: StyleSheet; + + /// Calculates the bounds for the given button by its position. + fn variant_button_bounds(&self, bounds: Rectangle, position: usize) -> Rectangle; + + /// Calculates the layout of this variant. + fn variant_layout(&self, renderer: &Self::Renderer, limits: &layout::Limits) -> layout::Node; +} + +#[derive(Setters)] +pub struct SegmentedButton<'a, Variant, Message, Renderer> +where + Renderer: iced_native::Renderer + iced_native::text::Renderer, + Renderer::Theme: StyleSheet, +{ + /// Contains application state also used for drawing. + #[setters(skip)] + pub(super) state: &'a SharedWidgetState, + /// 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, + /// 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. + #[setters(into)] + pub(super) style: ::Style, + #[setters(skip)] + /// Emits the ID of the activated widget on selection. + pub(super) on_activate: Option Message>>, + #[setters(skip)] + /// Defines the implementation of this struct + variant: PhantomData, +} + +impl<'a, Variant, Message, Renderer> SegmentedButton<'a, Variant, Message, Renderer> +where + Renderer: iced_native::Renderer + iced_native::text::Renderer, + Renderer::Theme: StyleSheet, + Self: SegmentedVariant, +{ + #[must_use] + pub fn new(state: &'a SharedWidgetState) -> Self { + Self { + state, + font_active: Renderer::Font::default(), + font_hovered: Renderer::Font::default(), + font_inactive: Renderer::Font::default(), + height: Length::Shrink, + width: Length::Fill, + button_padding: [4, 4, 4, 4], + button_height: 32, + spacing: 0, + style: ::Style::default(), + on_activate: None, + variant: PhantomData, + } + } + + /// Emits the ID of the activated widget on selection. + #[must_use] + pub fn on_activate(mut self, on_activate: impl Fn(Key) -> Message + 'static) -> Self { + self.on_activate = Some(Box::from(on_activate)); + 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, + text_size: u16, + bounds: Size, + ) -> (f32, f32) { + let mut width = 0.0f32; + 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); + } + + (width, height) + } +} + +impl<'a, Variant, Message, Renderer> Widget + for SegmentedButton<'a, Variant, Message, Renderer> +where + Renderer: iced_native::Renderer + iced_native::text::Renderer, + Renderer::Theme: StyleSheet, + Self: SegmentedVariant, + Message: 'static + Clone, +{ + fn tag(&self) -> tree::Tag { + tree::Tag::of::() + } + + fn state(&self) -> tree::State { + tree::State::new(UniqueWidgetState::default()) + } + + fn width(&self) -> Length { + self.width + } + + fn height(&self) -> Length { + self.height + } + + fn layout(&self, renderer: &Renderer, limits: &layout::Limits) -> layout::Node { + self.variant_layout(renderer, limits) + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + _renderer: &Renderer, + _clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + let bounds = layout.bounds(); + let state = tree.state.downcast_mut::(); + + if bounds.contains(cursor_position) { + for (nth, (key, _)) in self.state.buttons.iter().enumerate() { + let bounds = self.variant_button_bounds(bounds, nth); + if bounds.contains(cursor_position) { + // Record that the mouse is hovering over this button. + state.hovered = key; + + if let Some(on_activate) = self.on_activate.as_ref() { + if let Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerLifted { .. }) = event + { + shell.publish(on_activate(key)); + return event::Status::Captured; + } + } + } + } + } else { + state.hovered = Key::default(); + } + + event::Status::Ignored + } + + fn mouse_interaction( + &self, + _tree: &Tree, + layout: Layout<'_>, + cursor_position: iced::Point, + _viewport: &iced::Rectangle, + _renderer: &Renderer, + ) -> iced_native::mouse::Interaction { + let bounds = layout.bounds(); + if (0..self.state.buttons.len()).any(|nth| { + self.variant_button_bounds(bounds, nth) + .contains(cursor_position) + }) { + iced_native::mouse::Interaction::Pointer + } else { + iced_native::mouse::Interaction::Idle + } + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &::Theme, + _style: &renderer::Style, + layout: Layout<'_>, + _cursor_position: iced::Point, + _viewport: &iced::Rectangle, + ) { + let state = tree.state.downcast_ref::(); + let appearance = Self::variant_appearance(theme, &self.style); + let bounds = layout.bounds(); + let button_amount = self.state.buttons.len(); + + // Draw the background, if a background was defined. + if let Some(background) = appearance.background { + renderer.fill_quad( + renderer::Quad { + bounds, + border_radius: appearance.border_radius, + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + background, + ); + } + + // 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 (status_appearance, font) = if self.state.active == key { + (appearance.active, &self.font_active) + } else if state.hovered == key { + (appearance.hover, &self.font_hovered) + } else { + (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 { + status_appearance.last + } else { + status_appearance.middle + }; + + // Render the background of the button. + if status_appearance.background.is_some() { + renderer.fill_quad( + renderer::Quad { + bounds, + border_radius: button_appearance.border_radius, + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + status_appearance + .background + .unwrap_or(Background::Color(Color::TRANSPARENT)), + ); + } + + // Draw the bottom border defined for this button. + if let Some((width, background)) = button_appearance.border_bottom { + let mut bounds = bounds; + bounds.y = bounds.y + bounds.height - width; + bounds.height = width; + + renderer.fill_quad( + renderer::Quad { + bounds, + border_radius: BorderRadius::from(0.0), + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + background, + ); + } + + // 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, + }); + } + } + + fn overlay<'b>( + &'b self, + _tree: &'b mut Tree, + _layout: iced_native::Layout<'_>, + _renderer: &Renderer, + ) -> Option> { + None + } +} + +impl<'a, Variant, Message, Renderer> From> + for Element<'a, Message, Renderer> +where + Renderer: iced_native::Renderer + iced_native::text::Renderer + 'a, + Renderer::Theme: StyleSheet, + SegmentedButton<'a, Variant, Message, Renderer>: SegmentedVariant, + Variant: 'static, + Message: 'static + Clone, +{ + fn from(mut widget: SegmentedButton<'a, Variant, Message, Renderer>) -> Self { + if widget.state.buttons.is_empty() { + widget.spacing = 0; + } + + Self::new(widget) + } +} + +/// State that is maintained by each individual widget. +#[derive(Default)] +struct UniqueWidgetState { + /// The ID of the button that is being hovered. Defaults to null. + hovered: Key, +}