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, +}