improv(segmented-button): Express vertical/horizontal variants as a state machine
It's difficult to make iterative developments when there's two nearly-identical types that need to be kept synchronized to any change. Rust gives us traits so we should use them instead of duplicating code. This made it easier to make styling and layout improvements to both instances of the segmented button.
This commit is contained in:
parent
b704eea531
commit
357de5e9be
9 changed files with 633 additions and 726 deletions
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright 2022 System76 <info@system76.com>
|
||||
// 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<Message, Data>(
|
||||
state: &State<Data>,
|
||||
) -> HorizontalSegmentedButton<Message, crate::Renderer> {
|
||||
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<Message, Data>(
|
|||
pub fn horizontal_segmented_selection<Message, Data>(
|
||||
state: &State<Data>,
|
||||
) -> HorizontalSegmentedButton<Message, crate::Renderer> {
|
||||
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<Message, Data>(
|
|||
pub fn vertical_segmented_selection<Message, Data>(
|
||||
state: &State<Data>,
|
||||
) -> VerticalSegmentedButton<Message, crate::Renderer> {
|
||||
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<Message, Data>(
|
|||
pub fn vertical_view_switcher<Message, Data>(
|
||||
state: &State<Data>,
|
||||
) -> VerticalSegmentedButton<Message, crate::Renderer> {
|
||||
VerticalSegmentedButton::new(&state.inner)
|
||||
SegmentedButton::new(&state.inner)
|
||||
.button_padding([16, 0, 16, 0])
|
||||
.button_height(48)
|
||||
.style(crate::theme::SegmentedButton::ViewSwitcher)
|
||||
|
|
|
|||
|
|
@ -1,172 +1,72 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// 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<Message, Renderer, Data>(
|
||||
state: &State<Data>,
|
||||
) -> HorizontalSegmentedButton<Message, Renderer>
|
||||
) -> SegmentedButton<Horizontal, Message, Renderer>
|
||||
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: <Renderer::Theme as StyleSheet>::Style,
|
||||
/// Emits the ID of the activated widget on selection.
|
||||
#[setters(skip)]
|
||||
on_activate: Option<Box<dyn Fn(Key) -> 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: <Renderer::Theme as StyleSheet>::Style::default(),
|
||||
on_activate: None,
|
||||
}
|
||||
fn variant_appearance(
|
||||
theme: &<Self::Renderer as iced_native::Renderer>::Theme,
|
||||
style: &<<Self::Renderer as iced_native::Renderer>::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<Message, Renderer>
|
||||
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::<UniqueWidgetState>()
|
||||
}
|
||||
|
||||
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::<UniqueWidgetState>();
|
||||
|
||||
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: &<Renderer as iced_native::Renderer>::Theme,
|
||||
_style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
_cursor_position: iced::Point,
|
||||
_viewport: &iced::Rectangle,
|
||||
) {
|
||||
let state = tree.state.downcast_ref::<UniqueWidgetState>();
|
||||
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<iced_native::overlay::Element<'b, Message, Renderer>> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> From<HorizontalSegmentedButton<'a, Message, Renderer>>
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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};
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ slotmap::new_key_type! {
|
|||
|
||||
/// Contains all state for interacting with a segmented button.
|
||||
pub struct State<Data> {
|
||||
/// State that is shareable across widget(s).
|
||||
/// State that is shared with widget drawing.
|
||||
pub inner: SharedWidgetState,
|
||||
|
||||
/// State unique to the application.
|
||||
|
|
|
|||
|
|
@ -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<Background>,
|
||||
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<Background>,
|
||||
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<Background>,
|
||||
pub first: ButtonAppearance,
|
||||
pub middle: ButtonAppearance,
|
||||
pub last: ButtonAppearance,
|
||||
pub text_color: Color,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<Message, Renderer, Data>(
|
||||
state: &State<Data>,
|
||||
) -> VerticalSegmentedButton<Message, Renderer>
|
||||
) -> SegmentedButton<Vertical, Message, Renderer>
|
||||
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: <Renderer::Theme as StyleSheet>::Style,
|
||||
/// Emits the ID of the activated widget on selection.
|
||||
#[setters(skip)]
|
||||
on_activate: Option<Box<dyn Fn(Key) -> 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: <Renderer::Theme as StyleSheet>::Style::default(),
|
||||
on_activate: None,
|
||||
}
|
||||
fn variant_appearance(
|
||||
theme: &<Self::Renderer as iced_native::Renderer>::Theme,
|
||||
style: &<<Self::Renderer as iced_native::Renderer>::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<Message, Renderer>
|
||||
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::<UniqueWidgetState>()
|
||||
}
|
||||
|
||||
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::<UniqueWidgetState>();
|
||||
|
||||
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: &<Renderer as iced_native::Renderer>::Theme,
|
||||
_style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
_cursor_position: iced::Point,
|
||||
_viewport: &iced::Rectangle,
|
||||
) {
|
||||
let state = tree.state.downcast_ref::<UniqueWidgetState>();
|
||||
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<iced_native::overlay::Element<'b, Message, Renderer>> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> From<VerticalSegmentedButton<'a, Message, Renderer>>
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
347
src/widget/segmented_button/widget.rs
Normal file
347
src/widget/segmented_button/widget.rs
Normal file
|
|
@ -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: &<Self::Renderer as iced_native::Renderer>::Theme,
|
||||
style: &<<Self::Renderer as iced_native::Renderer>::Theme as StyleSheet>::Style,
|
||||
) -> super::Appearance
|
||||
where
|
||||
<Self::Renderer as iced_native::Renderer>::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: <Renderer::Theme as StyleSheet>::Style,
|
||||
#[setters(skip)]
|
||||
/// Emits the ID of the activated widget on selection.
|
||||
pub(super) on_activate: Option<Box<dyn Fn(Key) -> Message>>,
|
||||
#[setters(skip)]
|
||||
/// Defines the implementation of this struct
|
||||
variant: PhantomData<Variant>,
|
||||
}
|
||||
|
||||
impl<'a, Variant, Message, Renderer> SegmentedButton<'a, Variant, Message, Renderer>
|
||||
where
|
||||
Renderer: iced_native::Renderer + iced_native::text::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
Self: SegmentedVariant<Renderer = Renderer>,
|
||||
{
|
||||
#[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: <Renderer::Theme as StyleSheet>::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<Message, Renderer>
|
||||
for SegmentedButton<'a, Variant, Message, Renderer>
|
||||
where
|
||||
Renderer: iced_native::Renderer + iced_native::text::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
Self: SegmentedVariant<Renderer = Renderer>,
|
||||
Message: 'static + Clone,
|
||||
{
|
||||
fn tag(&self) -> tree::Tag {
|
||||
tree::Tag::of::<UniqueWidgetState>()
|
||||
}
|
||||
|
||||
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::<UniqueWidgetState>();
|
||||
|
||||
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: &<Renderer as iced_native::Renderer>::Theme,
|
||||
_style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
_cursor_position: iced::Point,
|
||||
_viewport: &iced::Rectangle,
|
||||
) {
|
||||
let state = tree.state.downcast_ref::<UniqueWidgetState>();
|
||||
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<iced_native::overlay::Element<'b, Message, Renderer>> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Variant, Message, Renderer> From<SegmentedButton<'a, Variant, Message, Renderer>>
|
||||
for Element<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: iced_native::Renderer + iced_native::text::Renderer + 'a,
|
||||
Renderer::Theme: StyleSheet,
|
||||
SegmentedButton<'a, Variant, Message, Renderer>: SegmentedVariant<Renderer = Renderer>,
|
||||
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,
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue