diff --git a/Cargo.toml b/Cargo.toml index 380dda3a..b2ed54e6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,7 @@ palette = "0.6.1" cosmic-panel-config = {git = "https://github.com/pop-os/cosmic-panel", optional = true, branch = "master_jammy" } sctk = { package = "smithay-client-toolkit", git = "https://github.com/Smithay/client-toolkit", optional = true, rev = "73346019952f82ec7e4d4d15f5d66841b54e8b61" } slotmap = "1.0.6" +stack_dst = "0.7.2" [dependencies.cosmic-theme] git = "https://github.com/pop-os/cosmic-theme.git" diff --git a/src/theme/mod.rs b/src/theme/mod.rs index 5c4a6f17..62c252a8 100644 --- a/src/theme/mod.rs +++ b/src/theme/mod.rs @@ -27,6 +27,7 @@ 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}; @@ -880,7 +881,7 @@ pub enum SegmentedButton { impl segmented_button::StyleSheet for Theme { type Style = SegmentedButton; - fn appearance(&self, style: &Self::Style) -> segmented_button::Appearance { + fn appearance(&self, style: &Self::Style, orientation: Orientation) -> segmented_button::Appearance { match style { SegmentedButton::ViewSwitcher => { let cosmic = self.cosmic(); @@ -915,7 +916,7 @@ impl segmented_button::StyleSheet for Theme { } } } - SegmentedButton::Selection => { + SegmentedButton::Selection if orientation == Orientation::Horizontal => { let cosmic = self.cosmic(); segmented_button::Appearance { background: None, @@ -948,6 +949,39 @@ impl segmented_button::StyleSheet for Theme { } } } + 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/segmented_button/cosmic.rs b/src/widget/segmented_button/cosmic.rs index 14b1ea9e..f354bbc9 100644 --- a/src/widget/segmented_button/cosmic.rs +++ b/src/widget/segmented_button/cosmic.rs @@ -2,7 +2,6 @@ // SPDX-License-Identifier: MPL-2.0 use super::{SegmentedButton, State}; -use iced_core::Length; /// Appears as a collection of tabs for developing a tabbed interface. /// @@ -12,7 +11,8 @@ pub fn view_switcher( state: &State, ) -> SegmentedButton { SegmentedButton::new(&state.inner) - .height(Length::Units(48)) + .button_padding([16, 0, 16, 0]) + .button_height(48) .style(crate::theme::SegmentedButton::ViewSwitcher) .font_active(crate::font::FONT_SEMIBOLD) } @@ -25,7 +25,8 @@ pub fn segmented_selection( state: &State, ) -> SegmentedButton { SegmentedButton::new(&state.inner) - .height(Length::Units(32)) + .button_padding([16, 0, 16, 0]) + .button_height(32) .style(crate::theme::SegmentedButton::Selection) .font_active(crate::font::FONT_SEMIBOLD) } diff --git a/src/widget/segmented_button/mod.rs b/src/widget/segmented_button/mod.rs index 9708d712..d33d0c6c 100644 --- a/src/widget/segmented_button/mod.rs +++ b/src/widget/segmented_button/mod.rs @@ -50,6 +50,8 @@ mod style; pub use self::state::{ButtonContent, Key, SecondaryState, State, WidgetState}; pub use self::style::{Appearance, ButtonAppearance, StyleSheet}; +use crate::widget::Orientation; + use derive_setters::Setters; use iced::{ alignment::{Horizontal, Vertical}, @@ -59,6 +61,18 @@ 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. #[derive(Default)] struct PrivateWidgetState { @@ -78,19 +92,25 @@ where /// Contains application state also used for drawing. #[setters(skip)] state: &'a WidgetState, - /// The desired font for active tabs. + /// Desired font for active tabs. font_active: Renderer::Font, - /// The desired font for hovered tabs. + /// Desired font for hovered tabs. font_hovered: Renderer::Font, - /// The desired font for inactive tabs. + /// Desired font for inactive tabs. font_inactive: Renderer::Font, - /// The desired width of the widget. + /// Orientation of the buttons. + orientation: Orientation, + /// Desired width of the widget. width: Length, - /// The desired height of the widget. + /// Desired height of the widget. height: Length, - /// The desired spacing between widgets. + /// Padding around a button. + button_padding: [u16; 4], + /// Desired height of a button. + button_height: u16, + /// Desired spacing between buttons. spacing: u16, - /// The style to draw the widget in. + /// Style to draw the widget in. #[setters(into)] style: ::Style, /// Emits the ID of the activated widget on selection. @@ -110,8 +130,11 @@ where font_active: Renderer::Font::default(), font_hovered: Renderer::Font::default(), font_inactive: Renderer::Font::default(), - height: Length::Units(32), + 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, @@ -124,18 +147,61 @@ where self.on_activate = Some(Box::from(on_activate)); self } -} -/// 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) + /// 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> @@ -161,18 +227,31 @@ where } fn layout(&self, renderer: &Renderer, limits: &layout::Limits) -> layout::Node { - let limits = limits.width(self.width).height(self.height); + let mut width = 0.0f32; + let mut height = 0.0f32; + let mut limits = limits.width(self.width); + let text_size = renderer.default_size(); - let bounds = limits.max(); + 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); + } - let size = renderer.default_size(); - - let mut width = 0.0; - let height = bounds.height; - - for (_, content) in self.state.buttons.iter() { - let (w, _) = renderer.measure(&content.text, size, Default::default(), bounds); - width += w + f32::from(self.spacing * 2); + 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))) @@ -189,15 +268,12 @@ where 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) { - let button_width = bounds.width / self.state.buttons.len() as f32; - for (num, (key, _)) in self.state.buttons.iter().enumerate() { - let mut bounds = bounds; - bounds.width = button_width; - bounds.x += num as f32 * button_width; - + 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; @@ -245,11 +321,13 @@ where _viewport: &iced::Rectangle, ) { let state = tree.state.downcast_ref::(); - let appearance = theme.appearance(&self.style); + let appearance = theme.appearance(&self.style, self.orientation); let bounds = layout.bounds(); let button_amount = self.state.buttons.len(); - let button_width = bounds.width / button_amount as f32; + 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 { @@ -262,10 +340,9 @@ where ); } + // Draw each of the buttons in the widget. for (num, (key, content)) in self.state.buttons.iter().enumerate() { - let mut bounds = bounds; - bounds.width = button_width; - bounds.x += num as f32 * button_width; + let bounds = bounds_generator(); let (button_appearance, font) = if self.state.active == key { (appearance.button_active, &self.font_active) @@ -299,7 +376,7 @@ where ); } - // Render the bottom border. + // 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; @@ -316,7 +393,7 @@ where ); } - // Render the text. + // Draw the text in this button. renderer.fill_text(iced_native::text::Text { content: &content.text, size: f32::from(renderer.default_size()), diff --git a/src/widget/segmented_button/state.rs b/src/widget/segmented_button/state.rs index 58819f1e..31357f1c 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; +use std::{borrow::Cow, cell::Cell}; slotmap::new_key_type! { /// An ID for a segmented button @@ -35,6 +35,9 @@ pub struct WidgetState { /// 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 35f76a35..d04c2846 100644 --- a/src/widget/segmented_button/style.rs +++ b/src/widget/segmented_button/style.rs @@ -1,6 +1,7 @@ // 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,5 +33,5 @@ pub trait StyleSheet { type Style: Default; /// The [`Appearance`] of the segmented button. - fn appearance(&self, style: &Self::Style) -> Appearance; + fn appearance(&self, style: &Self::Style, orientation: Orientation) -> Appearance; }