feat: implement SegmentedButton widget
This commit is contained in:
parent
01701759c9
commit
e97c258422
8 changed files with 740 additions and 244 deletions
245
src/widget/segmented_button/mod.rs
Normal file
245
src/widget/segmented_button/mod.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
82
src/widget/segmented_button/state.rs
Normal file
82
src/widget/segmented_button/state.rs
Normal 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 }
|
||||
}
|
||||
}
|
||||
29
src/widget/segmented_button/style.rs
Normal file
29
src/widget/segmented_button/style.rs
Normal 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;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue