feat: implement SegmentedButton widget

This commit is contained in:
Michael Aaron Murphy 2022-12-28 12:42:28 +01:00 committed by Ashley Wulber
parent 01701759c9
commit e97c258422
8 changed files with 740 additions and 244 deletions

View file

@ -0,0 +1,245 @@
/// Copyright 2022 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
mod state;
mod style;
pub use self::state::{ButtonContent, Key, SecondaryState, State, WidgetState};
pub use self::style::{Appearance, ButtonAppearance, StyleSheet};
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::{layout, renderer, widget::Tree, Clipboard, Layout, Shell, Widget};
/// A linear set of options for choosing between.
#[derive(Setters)]
pub struct SegmentedButton<'a, Message, Renderer>
where
Renderer: iced_native::Renderer,
Renderer::Theme: StyleSheet,
{
state: &'a WidgetState,
width: Length,
height: Length,
spacing: u16,
#[setters(into)]
style: <Renderer::Theme as StyleSheet>::Style,
#[setters(skip)]
on_activate: Option<Box<dyn Fn(Key) -> Message>>,
}
impl<'a, Message, Renderer> SegmentedButton<'a, Message, Renderer>
where
Renderer: iced_native::Renderer,
Renderer::Theme: StyleSheet,
{
#[must_use]
pub fn new(state: &'a WidgetState) -> Self {
Self {
state,
height: Length::Units(48),
width: Length::Fill,
spacing: 0,
style: <Renderer::Theme as StyleSheet>::Style::default(),
on_activate: None,
}
}
#[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
}
}
#[must_use]
pub fn segmented_button<Message, Renderer, Data>(
state: &State<Data>,
) -> SegmentedButton<Message, Renderer>
where
Renderer: iced_native::Renderer,
Renderer::Theme: StyleSheet,
{
SegmentedButton::new(&state.inner)
}
impl<'a, Message, Renderer> Widget<Message, Renderer> for SegmentedButton<'a, Message, Renderer>
where
Renderer: iced_native::Renderer + iced_native::text::Renderer,
Renderer::Theme: StyleSheet,
Message: 'static + Clone,
{
fn width(&self) -> Length {
self.width
}
fn height(&self) -> Length {
self.height
}
fn layout(&self, renderer: &Renderer, limits: &layout::Limits) -> layout::Node {
let limits = limits.width(self.width).height(self.height);
let bounds = limits.max();
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);
}
layout::Node::new(limits.resolve(Size::new(width, height)))
}
fn on_event(
&mut self,
_tree: &mut Tree,
event: Event,
layout: Layout<'_>,
cursor_position: Point,
_renderer: &Renderer,
_clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
) -> event::Status {
let bounds = layout.bounds();
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;
if bounds.contains(cursor_position) {
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;
}
}
}
}
}
event::Status::Ignored
}
fn mouse_interaction(
&self,
_tree: &Tree,
layout: Layout<'_>,
cursor_position: iced::Point,
_viewport: &iced::Rectangle,
_renderer: &Renderer,
) -> iced_native::mouse::Interaction {
if layout.bounds().contains(cursor_position) {
iced_native::mouse::Interaction::Pointer
} else {
iced_native::mouse::Interaction::Idle
}
}
fn draw(
&self,
_tree: &Tree,
renderer: &mut Renderer,
theme: &<Renderer as iced_native::Renderer>::Theme,
_style: &renderer::Style,
layout: Layout<'_>,
_cursor_position: iced::Point,
_viewport: &iced::Rectangle,
) {
let appearance = theme.appearance(&self.style);
let bounds = layout.bounds();
let button_width = bounds.width / self.state.buttons.len() as f32;
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 button_appearance = if self.state.active == key {
appearance.button_active
} else {
appearance.button_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: button_appearance.border_radius,
border_width: 0.0,
border_color: Color::TRANSPARENT,
},
button_appearance
.background
.unwrap_or(Background::Color(Color::TRANSPARENT)),
);
}
// Render the bottom border.
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,
);
}
// Render the text.
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: Default::default(),
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<SegmentedButton<'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: SegmentedButton<'a, Message, Renderer>) -> Self {
Self::new(widget)
}
}

View file

@ -0,0 +1,82 @@
/// Copyright 2022 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
use slotmap::{SecondaryMap, SlotMap};
slotmap::new_key_type! {
pub struct Key;
}
/// Contains all state for interacting with a [`SegmentedButton`].
pub struct State<Data> {
pub inner: WidgetState,
pub data: SecondaryState<Data>,
}
impl<Data> Default for State<Data> {
fn default() -> Self {
Self {
inner: WidgetState::default(),
data: SecondaryState::default(),
}
}
}
/// State which is most useful to the widget.
#[derive(Default)]
pub struct WidgetState {
pub buttons: SlotMap<Key, ButtonContent>,
pub active: Key,
}
/// State which is most useful to the application.
pub type SecondaryState<Data> = SecondaryMap<Key, Data>;
impl<Data> State<Data> {
/// The ID of the active button.
#[must_use]
pub fn active(&self) -> Key {
self.inner.active
}
/// Get the application data for the active button.
#[must_use]
pub fn active_data(&self) -> Option<&Data> {
self.data(self.active())
}
/// Get the application data for a button.
#[must_use]
pub fn data(&self, key: Key) -> Option<&Data> {
self.data.get(key)
}
/// Insert a new button.
pub fn insert(&mut self, content: impl Into<ButtonContent>, data: Data) -> Key {
let key = self.inner.buttons.insert(content.into());
self.data.insert(key, data);
key
}
/// Removes a button.
pub fn remove(&mut self, key: Key) -> Option<Data> {
self.inner.buttons.remove(key);
self.data.remove(key)
}
/// Activates this button.
pub fn activate(&mut self, key: Key) {
self.inner.active = key;
}
}
/// Data to be drawn in a [`SegmentedButton`] button.
pub struct ButtonContent {
pub text: String,
}
impl From<String> for ButtonContent {
fn from(text: String) -> Self {
ButtonContent { text }
}
}

View file

@ -0,0 +1,29 @@
/// Copyright 2022 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
use iced_core::{Background, BorderRadius, Color};
/// The appearance of a [`SegmentedButton`].
#[derive(Clone, Copy)]
pub struct Appearance {
pub button_active: ButtonAppearance,
pub button_inactive: ButtonAppearance,
}
/// The appearance of a button in the [`SegmentedButton`]
#[derive(Clone, Copy)]
pub struct ButtonAppearance {
pub background: Option<Background>,
pub border_radius: BorderRadius,
pub border_bottom: Option<(f32, Color)>,
pub text_color: Color,
}
/// Defines the [`Appearance`] of a [`SegmentedButton`].
pub trait StyleSheet {
/// The supported style of the [`StyleSheet`].
type Style: Default;
/// The [`Appearance`] of the [`SegmentedButton`].
fn appearance(&self, style: &Self::Style) -> Appearance;
}