From 3454483345a553bb71009df1ef08cfc53977ebee Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Wed, 28 Dec 2022 19:27:05 +0100 Subject: [PATCH] feat(segmented-button): Configurable background and hover styling --- src/theme/mod.rs | 56 ++++++++++++++++++---------- src/widget/segmented_button/mod.rs | 52 ++++++++++++++++++++++++-- src/widget/segmented_button/style.rs | 9 ++++- 3 files changed, 93 insertions(+), 24 deletions(-) diff --git a/src/theme/mod.rs b/src/theme/mod.rs index 587674f2..e9a151c5 100644 --- a/src/theme/mod.rs +++ b/src/theme/mod.rs @@ -868,8 +868,10 @@ impl text_input::StyleSheet for Theme { #[derive(Clone, Copy, Default)] pub enum SegmentedButton { + /// A tabbed widget for switching between views in an interface. #[default] - Default, + ViewSwitcher, + /// Or implement any custom theme of your liking. Custom(fn(&Theme) -> segmented_button::Appearance) } @@ -877,25 +879,41 @@ impl segmented_button::StyleSheet for Theme { type Style = SegmentedButton; fn appearance(&self, style: &Self::Style) -> segmented_button::Appearance { - if let SegmentedButton::Custom(func) = style { - return func(self); - } - - let cosmic = self.cosmic(); - - segmented_button::Appearance { - button_active: segmented_button::ButtonAppearance { - background: Some(Background::Color(cosmic.primary.component.base.into())), - border_bottom: Some((4.0, cosmic.accent.base.into())), - border_radius: BorderRadius::from([8.0, 8.0, 0.0, 0.0]), - text_color: cosmic.accent.base.into(), - }, - button_inactive: segmented_button::ButtonAppearance { - background: None, - border_bottom: Some((2.0, cosmic.accent.base.into())), - text_color: cosmic.primary.on.into(), - border_radius: BorderRadius::from(0.0), + match style { + 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 { + 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]), + text_color: cosmic.accent.base.into(), + }, + button_inactive: segmented_button::ButtonAppearance { + 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), + text_color: cosmic.primary.on.into(), + }, + button_hover: segmented_button::ButtonAppearance { + 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]), + text_color: cosmic.accent.base.into(), + } + } } + SegmentedButton::Custom(func) => func(self) } } } \ No newline at end of file diff --git a/src/widget/segmented_button/mod.rs b/src/widget/segmented_button/mod.rs index dc7531d0..f95ba0c5 100644 --- a/src/widget/segmented_button/mod.rs +++ b/src/widget/segmented_button/mod.rs @@ -13,8 +13,16 @@ use iced::{ 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}; +/// State that is maintained by the widget internally. +#[derive(Default)] +struct PrivateWidgetState { + /// The ID of the button that is being hovered. Defaults to null. + hovered: Key, +} + /// A linear set of options for choosing between. #[derive(Setters)] pub struct SegmentedButton<'a, Message, Renderer> @@ -73,6 +81,14 @@ where Renderer::Theme: StyleSheet, Message: 'static + Clone, { + fn tag(&self) -> tree::Tag { + tree::Tag::of::() + } + + fn state(&self) -> tree::State { + tree::State::new(PrivateWidgetState::default()) + } + fn width(&self) -> Length { self.width } @@ -101,7 +117,7 @@ where fn on_event( &mut self, - _tree: &mut Tree, + tree: &mut Tree, event: Event, layout: Layout<'_>, cursor_position: Point, @@ -110,6 +126,7 @@ where shell: &mut Shell<'_, Message>, ) -> event::Status { let bounds = layout.bounds(); + let state = tree.state.downcast_mut::(); if bounds.contains(cursor_position) { let button_width = bounds.width / self.state.buttons.len() as f32; @@ -119,6 +136,9 @@ where bounds.x += num as f32 * button_width; 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 @@ -129,6 +149,8 @@ where } } } + } else { + state.hovered = Key::default(); } event::Status::Ignored @@ -151,7 +173,7 @@ where fn draw( &self, - _tree: &Tree, + tree: &Tree, renderer: &mut Renderer, theme: &::Theme, _style: &renderer::Style, @@ -159,9 +181,23 @@ where _cursor_position: iced::Point, _viewport: &iced::Rectangle, ) { + let state = tree.state.downcast_ref::(); let appearance = theme.appearance(&self.style); let bounds = layout.bounds(); - let button_width = bounds.width / self.state.buttons.len() as f32; + let button_amount = self.state.buttons.len(); + let button_width = bounds.width / button_amount as f32; + + 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, + ); + } for (num, (key, content)) in self.state.buttons.iter().enumerate() { let mut bounds = bounds; @@ -170,6 +206,8 @@ where let button_appearance = if self.state.active == key { appearance.button_active + } else if state.hovered == key { + appearance.button_hover } else { appearance.button_inactive }; @@ -182,7 +220,13 @@ where renderer.fill_quad( renderer::Quad { bounds, - border_radius: button_appearance.border_radius, + 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, }, diff --git a/src/widget/segmented_button/style.rs b/src/widget/segmented_button/style.rs index 36396bb9..5e346bd3 100644 --- a/src/widget/segmented_button/style.rs +++ b/src/widget/segmented_button/style.rs @@ -6,16 +6,23 @@ use iced_core::{Background, BorderRadius, Color}; /// The appearance of a [`SegmentedButton`]. #[derive(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, } /// The appearance of a button in the [`SegmentedButton`] #[derive(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 text_color: Color, }