feat(segmented-button): Vertical orientation functionality

This commit is contained in:
Michael Aaron Murphy 2022-12-30 17:39:35 +01:00 committed by Ashley Wulber
parent de580ffefc
commit ad0443af3f
6 changed files with 166 additions and 49 deletions

View file

@ -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"

View file

@ -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)
}
}

View file

@ -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<Message, Data>(
state: &State<Data>,
) -> SegmentedButton<Message, crate::Renderer> {
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<Message, Data>(
state: &State<Data>,
) -> SegmentedButton<Message, crate::Renderer> {
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)
}

View file

@ -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<Message, Renderer, Data>(
state: &State<Data>,
) -> SegmentedButton<Message, Renderer>
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: <Renderer::Theme as StyleSheet>::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: <Renderer::Theme as StyleSheet>::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<Message, Renderer, Data>(
state: &State<Data>,
) -> SegmentedButton<Message, Renderer>
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<dyn FnMut() -> 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<Message, Renderer> 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::<PrivateWidgetState>();
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::<PrivateWidgetState>();
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()),

View file

@ -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<Key>,
}
/// State which is most useful to the application.

View file

@ -1,6 +1,7 @@
// Copyright 2022 System76 <info@system76.com>
// 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;
}