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:
Michael Aaron Murphy 2023-01-04 05:37:20 +01:00
parent b704eea531
commit 357de5e9be
No known key found for this signature in database
GPG key ID: B2732D4240C9212C
9 changed files with 633 additions and 726 deletions

View file

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