From 444e389496bb92a928b7731175f00f51ec4f0caa Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Tue, 3 Jan 2023 19:35:34 +0100 Subject: [PATCH] refactor!: separate horizontal and vertical segmented button widgets - Removes the orientation enum in favor of two separate widgets - Implements the spacing attribute for both widgets - Demo is updated to display spaced variants of the widgets --- Cargo.toml | 3 +- examples/cosmic/src/window.rs | 5 +- examples/cosmic/src/window/demo.rs | 58 +++- src/theme/mod.rs | 124 +------ src/theme/segmented_button.rs | 169 ++++++++++ src/widget/mod.rs | 16 +- src/widget/segmented_button/cosmic.rs | 42 ++- src/widget/segmented_button/horizontal.rs | 350 ++++++++++++++++++++ src/widget/segmented_button/mod.rs | 381 +--------------------- src/widget/segmented_button/state.rs | 13 +- src/widget/segmented_button/style.rs | 8 +- src/widget/segmented_button/vertical.rs | 347 ++++++++++++++++++++ 12 files changed, 969 insertions(+), 547 deletions(-) create mode 100644 src/theme/segmented_button.rs create mode 100644 src/widget/segmented_button/horizontal.rs create mode 100644 src/widget/segmented_button/vertical.rs diff --git a/Cargo.toml b/Cargo.toml index 8ff566a0..08bf09d2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" name = "cosmic" [features] -default = ["wayland"] +default = ["swbuf", "winit"] debug = ["iced/debug"] swbuf = ["iced/swbuf", "iced_swbuf"] wayland = ["iced/wayland", "iced_glow"] @@ -25,7 +25,6 @@ palette = "0.6.1" cosmic-panel-config = {git = "https://github.com/pop-os/cosmic-panel", optional = true } sctk = { package = "smithay-client-toolkit", git = "https://github.com/Smithay/client-toolkit", optional = true, rev = "3776d4a" } slotmap = "1.0.6" -stack_dst = "0.7.2" [dependencies.cosmic-theme] git = "https://github.com/pop-os/cosmic-theme.git" diff --git a/examples/cosmic/src/window.rs b/examples/cosmic/src/window.rs index 946c3bd9..60e12d1f 100644 --- a/examples/cosmic/src/window.rs +++ b/examples/cosmic/src/window.rs @@ -7,10 +7,7 @@ use cosmic::{ iced_native::window, iced_winit::window::{close, drag, minimize, toggle_maximize}, theme::{self, Theme}, - widget::{ - header_bar, icon, list, nav_bar, nav_button, scrollable, segmented_button, settings, - spin_button::{SpinButtonModel, SpinMessage}, - }, + widget::{header_bar, icon, list, nav_bar, nav_button, scrollable, settings}, Element, ElementExt, }; use std::{ diff --git a/examples/cosmic/src/window/demo.rs b/examples/cosmic/src/window/demo.rs index 512046d5..71c25006 100644 --- a/examples/cosmic/src/window/demo.rs +++ b/examples/cosmic/src/window/demo.rs @@ -2,10 +2,11 @@ use cosmic::{ iced::widget::{checkbox, pick_list, progress_bar, radio, row, slider}, iced::{Alignment, Length}, theme::{Button as ButtonTheme, Theme}, - widget::{button, segmented_button::{self, cosmic::{view_switcher, segmented_selection}}, settings, toggler, Orientation, spin_button::{SpinButtonModel, SpinMessage}}, + widget::{button, settings, toggler, spin_button::{SpinButtonModel, SpinMessage}}, Element, }; +use cosmic::widget::segmented_button::{self, cosmic::{horizontal_segmented_selection, horizontal_view_switcher, vertical_segmented_selection, vertical_view_switcher}}; use super::{Page, Window}; pub enum DemoView { @@ -79,7 +80,7 @@ impl State { settings::view_column(vec![ window.page_title(Page::Demo), - view_switcher(&self.view_switcher) + horizontal_view_switcher(&self.view_switcher) .on_activate(Message::ViewSwitcher) .into(), match self.view_switcher.active_data() { @@ -170,50 +171,71 @@ impl State { cosmic::iced::widget::text("Selection") .font(cosmic::font::FONT_SEMIBOLD) .into(), - segmented_selection(&self.selection) + cosmic::iced::widget::text("Horizontal").into(), + horizontal_segmented_selection(&self.selection) .on_activate(Message::Selection) .into(), - segmented_selection(&self.selection) + cosmic::iced::widget::text("Horizontal With Spacing").into(), + horizontal_segmented_selection(&self.selection) + .spacing(8) .on_activate(Message::Selection) - .orientation(Orientation::Vertical) .into(), + cosmic::iced::widget::text("Vertical").into(), + vertical_segmented_selection(&self.selection) + .on_activate(Message::Selection) + .into(), + cosmic::iced::widget::text("Vertical With Spacing").into(), cosmic::iced::widget::row(vec![ - segmented_selection(&self.selection) + vertical_segmented_selection(&self.selection) + .spacing(8) .on_activate(Message::Selection) - .orientation(Orientation::Vertical) .width(Length::FillPortion(1)) .into(), - segmented_selection(&self.selection) + vertical_segmented_selection(&self.selection) + .spacing(8) .on_activate(Message::Selection) - .orientation(Orientation::Vertical) .width(Length::FillPortion(1)) .into(), - segmented_selection(&self.selection) + vertical_segmented_selection(&self.selection) + .spacing(8) .on_activate(Message::Selection) - .orientation(Orientation::Vertical) .width(Length::FillPortion(1)) .into(), ]) .spacing(12) .width(Length::Fill) .into(), - cosmic::iced::widget::text("ViewSwitcher") + cosmic::iced::widget::text("View Switcher") .font(cosmic::font::FONT_SEMIBOLD) .into(), + cosmic::iced::widget::text("Horizontal").into(), + horizontal_view_switcher(&self.selection) + .on_activate(Message::Selection) + .into(), + cosmic::iced::widget::text("Horizontal With Spacing").into(), + horizontal_view_switcher(&self.selection) + .spacing(8) + .on_activate(Message::Selection) + .into(), + cosmic::iced::widget::text("Vertical").into(), + vertical_view_switcher(&self.selection) + .on_activate(Message::Selection) + .into(), + cosmic::iced::widget::text("Vertical With Spacing").into(), cosmic::iced::widget::row(vec![ - view_switcher(&self.selection) + vertical_view_switcher(&self.selection) + .spacing(8) .on_activate(Message::Selection) - .orientation(Orientation::Vertical) .width(Length::FillPortion(1)) .into(), - view_switcher(&self.selection) + vertical_view_switcher(&self.selection) + .spacing(8) .on_activate(Message::Selection) - .orientation(Orientation::Vertical) .width(Length::FillPortion(1)) .into(), - view_switcher(&self.selection) + vertical_view_switcher(&self.selection) + .spacing(8) .on_activate(Message::Selection) - .orientation(Orientation::Vertical) .width(Length::FillPortion(1)) .into(), ]) diff --git a/src/theme/mod.rs b/src/theme/mod.rs index 62c252a8..df110931 100644 --- a/src/theme/mod.rs +++ b/src/theme/mod.rs @@ -3,11 +3,13 @@ pub mod expander; pub mod palette; +mod segmented_button; use std::hash::Hash; use std::hash::Hasher; pub use self::palette::Palette; +pub use self::segmented_button::SegmentedButton; use cosmic_theme::Component; use iced_core::BorderRadius; @@ -27,8 +29,6 @@ use iced_style::svg; use iced_style::text; use iced_style::text_input; use iced_style::toggler; -use crate::widget::Orientation; -use crate::widget::segmented_button; use iced_core::{Background, Color}; @@ -866,123 +866,3 @@ impl text_input::StyleSheet for Theme { palette.primary.weak.color } } - -#[derive(Clone, Copy, Default)] -pub enum SegmentedButton { - /// A tabbed widget for switching between views in an interface. - #[default] - ViewSwitcher, - /// A widget for multiple choice selection. - Selection, - /// Or implement any custom theme of your liking. - Custom(fn(&Theme) -> segmented_button::Appearance) -} - -impl segmented_button::StyleSheet for Theme { - type Style = SegmentedButton; - - fn appearance(&self, style: &Self::Style, orientation: Orientation) -> segmented_button::Appearance { - 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::Selection if orientation == Orientation::Horizontal => { - 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.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), - text_color: cosmic.accent.base.into(), - }, - button_inactive: segmented_button::ButtonAppearance { - 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), - text_color: cosmic.primary.on.into(), - }, - button_hover: segmented_button::ButtonAppearance { - 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), - text_color: cosmic.accent.base.into(), - } - } - } - 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 { - 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), - text_color: cosmic.accent.base.into(), - }, - button_inactive: segmented_button::ButtonAppearance { - 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), - text_color: cosmic.primary.on.into(), - }, - button_hover: segmented_button::ButtonAppearance { - 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), - text_color: cosmic.accent.base.into(), - } - } - } - SegmentedButton::Custom(func) => func(self) - } - } -} \ No newline at end of file diff --git a/src/theme/segmented_button.rs b/src/theme/segmented_button.rs new file mode 100644 index 00000000..3be26701 --- /dev/null +++ b/src/theme/segmented_button.rs @@ -0,0 +1,169 @@ +// Copyright 2023 System76 +// SPDX-License-Identifier: MPL-2.0 + +use crate::theme::Theme; +use crate::widget::segmented_button; +use iced_core::{Background, BorderRadius, Color}; + +#[derive(Clone, Copy, Default)] +pub enum SegmentedButton { + /// A tabbed widget for switching between views in an interface. + #[default] + ViewSwitcher, + /// A widget for multiple choice selection. + Selection, + /// Or implement any custom theme of your liking. + Custom(fn(&Theme) -> segmented_button::Appearance), +} + +impl segmented_button::StyleSheet for Theme { + type Style = SegmentedButton; + + fn horizontal(&self, style: &Self::Style) -> segmented_button::Appearance { + 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::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 { + 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), + text_color: cosmic.accent.base.into(), + }, + button_inactive: segmented_button::ButtonAppearance { + 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), + text_color: cosmic.primary.on.into(), + }, + button_hover: segmented_button::ButtonAppearance { + 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), + text_color: cosmic.accent.base.into(), + }, + } + } + SegmentedButton::Custom(func) => func(self), + } + } + + fn vertical(&self, style: &Self::Style) -> segmented_button::Appearance { + 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: None, + border_radius_first: BorderRadius::from(24.0), + border_radius_last: BorderRadius::from(24.0), + border_radius_middle: BorderRadius::from(24.0), + text_color: cosmic.accent.base.into(), + }, + button_inactive: segmented_button::ButtonAppearance { + 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), + text_color: cosmic.primary.on.into(), + }, + button_hover: segmented_button::ButtonAppearance { + 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), + text_color: cosmic.accent.base.into(), + }, + } + } + 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 { + 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), + text_color: cosmic.accent.base.into(), + }, + button_inactive: segmented_button::ButtonAppearance { + 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), + text_color: cosmic.primary.on.into(), + }, + button_hover: segmented_button::ButtonAppearance { + 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), + text_color: cosmic.accent.base.into(), + }, + } + } + SegmentedButton::Custom(func) => func(self), + } + } +} diff --git a/src/widget/mod.rs b/src/widget/mod.rs index cc078a98..c840e3f4 100644 --- a/src/widget/mod.rs +++ b/src/widget/mod.rs @@ -23,7 +23,12 @@ mod toggler; pub use toggler::toggler; pub mod segmented_button; -pub use segmented_button::{SegmentedButton, segmented_button}; +pub use segmented_button::{ + HorizontalSegmentedButton, + VerticalSegmentedButton, + horizontal_segmented_button, + vertical_segmented_button +}; pub mod settings; @@ -38,11 +43,4 @@ pub use spin_button::{SpinButton, spin_button}; pub mod rectangle_tracker; -pub mod aspect_ratio; - -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] -pub enum Orientation { - #[default] - Horizontal, - Vertical -} \ No newline at end of file +pub mod aspect_ratio; \ No newline at end of file diff --git a/src/widget/segmented_button/cosmic.rs b/src/widget/segmented_button/cosmic.rs index f354bbc9..be48e2f0 100644 --- a/src/widget/segmented_button/cosmic.rs +++ b/src/widget/segmented_button/cosmic.rs @@ -1,16 +1,16 @@ // Copyright 2022 System76 // SPDX-License-Identifier: MPL-2.0 -use super::{SegmentedButton, State}; +use super::{HorizontalSegmentedButton, State, VerticalSegmentedButton}; /// Appears as a collection of tabs for developing a tabbed interface. /// /// The data for the widget comes from a [`State`] that is maintained the application. #[must_use] -pub fn view_switcher( +pub fn horizontal_view_switcher( state: &State, -) -> SegmentedButton { - SegmentedButton::new(&state.inner) +) -> HorizontalSegmentedButton { + HorizontalSegmentedButton::new(&state.inner) .button_padding([16, 0, 16, 0]) .button_height(48) .style(crate::theme::SegmentedButton::ViewSwitcher) @@ -21,12 +21,40 @@ pub fn view_switcher( /// /// The data for the widget comes from a [`State`] that is maintained the application. #[must_use] -pub fn segmented_selection( +pub fn horizontal_segmented_selection( state: &State, -) -> SegmentedButton { - SegmentedButton::new(&state.inner) +) -> HorizontalSegmentedButton { + HorizontalSegmentedButton::new(&state.inner) .button_padding([16, 0, 16, 0]) .button_height(32) .style(crate::theme::SegmentedButton::Selection) .font_active(crate::font::FONT_SEMIBOLD) } + +/// Appears as a selection of choices for choosing between. +/// +/// The data for the widget comes from a [`State`] that is maintained the application. +#[must_use] +pub fn vertical_segmented_selection( + state: &State, +) -> VerticalSegmentedButton { + VerticalSegmentedButton::new(&state.inner) + .button_padding([16, 0, 16, 0]) + .button_height(32) + .style(crate::theme::SegmentedButton::Selection) + .font_active(crate::font::FONT_SEMIBOLD) +} + +/// Appears as a collection of tabs for developing a tabbed interface. +/// +/// The data for the widget comes from a [`State`] that is maintained the application. +#[must_use] +pub fn vertical_view_switcher( + state: &State, +) -> VerticalSegmentedButton { + VerticalSegmentedButton::new(&state.inner) + .button_padding([16, 0, 16, 0]) + .button_height(48) + .style(crate::theme::SegmentedButton::ViewSwitcher) + .font_active(crate::font::FONT_SEMIBOLD) +} diff --git a/src/widget/segmented_button/horizontal.rs b/src/widget/segmented_button/horizontal.rs new file mode 100644 index 00000000..e51594b4 --- /dev/null +++ b/src/widget/segmented_button/horizontal.rs @@ -0,0 +1,350 @@ +// Copyright 2023 System76 +// SPDX-License-Identifier: MPL-2.0 + +use super::state::{Key, SharedWidgetState, State}; +use super::style::StyleSheet; +use super::UniqueWidgetState; + +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}; + +/// Creates a [`HorizontalSegmentedButton`]. +#[must_use] +pub fn horizontal_segmented_button( + state: &State, +) -> HorizontalSegmentedButton +where + Renderer: iced_native::Renderer + iced_native::text::Renderer, + Renderer::Theme: StyleSheet, +{ + HorizontalSegmentedButton::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> +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>>, +} + +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, + } + } + + /// 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 + } + + /// 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; + } + + bounds.x += width; + counter += 1; + clone + } + } + + 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; + 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 size = limits + .height(Length::Units(height as u16)) + .resolve(Size::new(width, height)); + + 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 d33d0c6c..94d52e52 100644 --- a/src/widget/segmented_button/mod.rs +++ b/src/widget/segmented_button/mod.rs @@ -44,386 +44,19 @@ /// COSMIC configurations of [`SegmentedButton`]. pub mod cosmic; +mod horizontal; mod state; mod style; +mod vertical; -pub use self::state::{ButtonContent, Key, SecondaryState, State, WidgetState}; +pub use self::horizontal::{horizontal_segmented_button, 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}; -use crate::widget::Orientation; - -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}; - -/// Creates a widget that presents multiple conjoined buttons. -#[must_use] -pub fn segmented_button( - state: &State, -) -> SegmentedButton -where - Renderer: iced_native::Renderer + iced_native::text::Renderer, - Renderer::Theme: StyleSheet, -{ - SegmentedButton::new(&state.inner) -} - -/// State that is maintained by the widget internally. +/// State that is maintained by each individual widget. #[derive(Default)] -struct PrivateWidgetState { +struct UniqueWidgetState { /// The ID of the button that is being hovered. Defaults to null. hovered: Key, } - -/// 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 SegmentedButton<'a, 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 WidgetState, - /// 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, - /// Orientation of the buttons. - orientation: Orientation, - /// 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>>, -} - -impl<'a, Message, Renderer> SegmentedButton<'a, Message, Renderer> -where - Renderer: iced_native::Renderer + iced_native::text::Renderer, - Renderer::Theme: StyleSheet, -{ - #[must_use] - pub fn new(state: &'a WidgetState) -> Self { - Self { - state, - font_active: Renderer::Font::default(), - font_hovered: Renderer::Font::default(), - font_inactive: Renderer::Font::default(), - orientation: Orientation::Horizontal, - height: Length::Shrink, - width: Length::Fill, - button_padding: [4, 4, 4, 4], - button_height: 32, - spacing: 0, - style: ::Style::default(), - on_activate: None, - } - } - - /// 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 - } - - /// Creates a closure for generating the layout bounds of the buttons. - fn button_bounds( - &self, - bounds: Rectangle, - ) -> stack_dst::ValueA Rectangle, [usize; 4]> { - let button_amount = self.state.buttons.len() as f32; - match self.orientation { - Orientation::Horizontal => { - let width = bounds.width / button_amount; - let mut bounds = bounds; - bounds.width = width; - - let closure = move || { - let clone = bounds; - bounds.x += width; - clone - }; - - stack_dst::ValueA::new_stable(closure, |p| p as _) - .ok() - .unwrap() - } - - Orientation::Vertical => { - let height = bounds.height / button_amount; - let mut bounds = bounds; - bounds.height = height; - - let closure = move || { - let clone = bounds; - bounds.y += height; - clone - }; - - stack_dst::ValueA::new_stable(closure, |p| p as _) - .ok() - .unwrap() - } - } - } - - 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 SegmentedButton<'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(PrivateWidgetState::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; - let mut limits = limits.width(self.width); - let text_size = renderer.default_size(); - - match self.orientation { - Orientation::Horizontal => { - 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); - } - - limits = limits.height(Length::Units(height as u16)); - } - Orientation::Vertical => { - 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); - } - limits = limits.height(Length::Units(height as u16)); - } - } - - layout::Node::new(limits.resolve(Size::new(width, height))) - } - - 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 { - if layout.bounds().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.appearance(&self.style, self.orientation); - 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: SegmentedButton<'a, Message, Renderer>) -> Self { - Self::new(widget) - } -} diff --git a/src/widget/segmented_button/state.rs b/src/widget/segmented_button/state.rs index 31357f1c..e62293b0 100644 --- a/src/widget/segmented_button/state.rs +++ b/src/widget/segmented_button/state.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: MPL-2.0 use slotmap::{SecondaryMap, SlotMap}; -use std::{borrow::Cow, cell::Cell}; +use std::borrow::Cow; slotmap::new_key_type! { /// An ID for a segmented button @@ -11,8 +11,8 @@ slotmap::new_key_type! { /// Contains all state for interacting with a segmented button. pub struct State { - /// State that is shared with widget drawing. - pub inner: WidgetState, + /// State that is shareable across widget(s). + pub inner: SharedWidgetState, /// State unique to the application. pub data: SecondaryState, @@ -21,7 +21,7 @@ pub struct State { impl Default for State { fn default() -> Self { Self { - inner: WidgetState::default(), + inner: SharedWidgetState::default(), data: SecondaryState::default(), } } @@ -29,15 +29,12 @@ impl Default for State { /// State which is most useful to the widget. #[derive(Default)] -pub struct WidgetState { +pub struct SharedWidgetState { /// The content used for drawing segmented buttons. pub buttons: SlotMap, /// The actively-selected segmented button. pub active: Key, - - /// The button currently hovered. - pub hovered: Cell, } /// State which is most useful to the application. diff --git a/src/widget/segmented_button/style.rs b/src/widget/segmented_button/style.rs index d04c2846..0db84e4b 100644 --- a/src/widget/segmented_button/style.rs +++ b/src/widget/segmented_button/style.rs @@ -1,7 +1,6 @@ // Copyright 2022 System76 // SPDX-License-Identifier: MPL-2.0 -use crate::widget::Orientation; use iced_core::{Background, BorderRadius, Color}; /// The appearance of a segmented button. @@ -32,6 +31,9 @@ pub trait StyleSheet { /// The supported style of the [`StyleSheet`]. type Style: Default; - /// The [`Appearance`] of the segmented button. - fn appearance(&self, style: &Self::Style, orientation: Orientation) -> Appearance; + /// The horizontal [`Appearance`] of the segmented button. + fn horizontal(&self, style: &Self::Style) -> Appearance; + + /// The vertical [`Appearance`] of the segmented button. + fn vertical(&self, style: &Self::Style) -> Appearance; } diff --git a/src/widget/segmented_button/vertical.rs b/src/widget/segmented_button/vertical.rs new file mode 100644 index 00000000..66124467 --- /dev/null +++ b/src/widget/segmented_button/vertical.rs @@ -0,0 +1,347 @@ +use super::state::{Key, SharedWidgetState, State}; +use super::style::StyleSheet; +use super::UniqueWidgetState; + +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}; + +/// Creates a [`VerticalSegmentedButton`]. +#[must_use] +pub fn vertical_segmented_button( + state: &State, +) -> VerticalSegmentedButton +where + Renderer: iced_native::Renderer + iced_native::text::Renderer, + Renderer::Theme: StyleSheet, +{ + VerticalSegmentedButton::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> +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>>, +} + +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, + } + } + + /// 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 + } + + /// 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; + } + + bounds.y += height; + counter += 1; + clone + } + } + + 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; + 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 size = limits + .height(Length::Units(height as u16)) + .resolve(Size::new(width, height)); + + 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) + } +}