feat: focusable segmented items in segmented button

This commit is contained in:
Michael Aaron Murphy 2023-01-09 16:18:02 +01:00 committed by Michael Murphy
parent a89ec01297
commit 29c7444a30
14 changed files with 794 additions and 611 deletions

View file

@ -3,18 +3,48 @@
use std::marker::PhantomData;
use super::state::{Key, Selectable, SharedWidgetState};
use super::model::{Key, Model, WidgetModel};
use super::selection_modes::Selectable;
use super::style::StyleSheet;
use derive_setters::Setters;
use iced::{
alignment, event, mouse, touch, Background, Color, Element, Event, Length, Point, Rectangle,
Size,
alignment, event, keyboard, mouse, touch, Background, Color, Command, Element, Event, Length,
Point, Rectangle, Size,
};
use iced_core::BorderRadius;
use iced_native::widget::tree;
use iced_native::widget::{self, operation, tree, Operation};
use iced_native::{layout, renderer, widget::Tree, Clipboard, Layout, Shell, Widget};
/// State that is maintained by each individual widget.
#[derive(Default)]
struct LocalState {
/// The first focusable key.
first: Key,
/// If the widget is focused or not.
focused: bool,
/// The key inside the widget that is currently focused.
focused_key: Key,
/// The ID of the button that is being hovered. Defaults to null.
hovered: Key,
}
impl operation::Focusable for LocalState {
fn is_focused(&self) -> bool {
self.focused
}
fn focus(&mut self) {
self.focused = true;
self.focused_key = self.first;
}
fn unfocus(&mut self) {
self.focused = false;
self.focused_key = Key::default();
}
}
/// Isolates variant-specific behaviors from [`SegmentedButton`].
pub trait SegmentedVariant {
type Renderer: iced_native::Renderer;
@ -44,9 +74,10 @@ where
Renderer::Theme: StyleSheet,
Selection: Selectable,
{
/// Contains application state also used for drawing.
/// The model borrowed from the application create this widget.
#[setters(skip)]
pub(super) state: &'a SharedWidgetState<Selection>,
pub(super) model: &'a WidgetModel<Selection>,
pub(super) id: Option<Id>,
/// Padding around a button.
pub(super) button_padding: [u16; 4],
/// Desired height of a button.
@ -65,7 +96,7 @@ where
pub(super) width: Length,
/// Desired height of the widget.
pub(super) height: Length,
/// Desired spacing between buttons.
/// Desired spacing between items.
pub(super) spacing: u16,
/// Style to draw the widget in.
#[setters(into)]
@ -90,9 +121,10 @@ where
Selection: Selectable,
{
#[must_use]
pub fn new(state: &'a SharedWidgetState<Selection>) -> Self {
pub fn new<Component>(model: &'a Model<Selection, Component>) -> Self {
Self {
state,
model: &model.widget,
id: None,
button_padding: [4, 4, 4, 4],
button_height: 32,
button_spacing: 4,
@ -109,6 +141,46 @@ where
}
}
fn focus_previous(&mut self, state: &mut LocalState) -> event::Status {
let mut previous_key: Option<Key> = None;
for key in self.model.items.keys() {
if key == state.focused_key {
return match previous_key {
Some(next_focus) => {
state.focused_key = next_focus;
event::Status::Captured
}
None => break,
};
}
previous_key = Some(key);
}
state.focused_key = Key::default();
event::Status::Ignored
}
fn focus_next(&mut self, state: &mut LocalState) -> event::Status {
let mut keys = self.model.items.keys();
while let Some(key) = keys.next() {
if key == state.focused_key {
return match keys.next() {
Some(next_focus) => {
state.focused_key = next_focus;
event::Status::Captured
}
None => break,
};
}
}
state.focused_key = Key::default();
event::Status::Ignored
}
/// 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 {
@ -125,7 +197,7 @@ where
let mut width = 0.0f32;
let mut height = 0.0f32;
for (_, content) in self.state.buttons.iter() {
for content in self.model.items.values() {
let mut button_width = 0.0f32;
let mut button_height = 0.0f32;
@ -169,11 +241,14 @@ where
Message: 'static + Clone,
{
fn tag(&self) -> tree::Tag {
tree::Tag::of::<UniqueWidgetState>()
tree::Tag::of::<LocalState>()
}
fn state(&self) -> tree::State {
tree::State::new(UniqueWidgetState::default())
tree::State::new(LocalState {
first: self.model.items.keys().next().unwrap_or_default(),
..LocalState::default()
})
}
fn width(&self) -> Length {
@ -199,32 +274,72 @@ where
shell: &mut Shell<'_, Message>,
) -> event::Status {
let bounds = layout.bounds();
let state = tree.state.downcast_mut::<UniqueWidgetState>();
let state = tree.state.downcast_mut::<LocalState>();
if bounds.contains(cursor_position) {
for (nth, (key, _)) in self.state.buttons.iter().enumerate() {
for (nth, (key, content)) in self.model.items.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 content.enabled {
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
{
// 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;
shell.publish(on_activate(key));
return event::Status::Captured;
}
}
}
break;
}
}
} else {
state.hovered = Key::default();
}
if state.focused {
if let Event::Keyboard(keyboard::Event::KeyPressed {
key_code: keyboard::KeyCode::Tab,
modifiers,
..
}) = event
{
return if modifiers.shift() {
self.focus_previous(state)
} else {
self.focus_next(state)
};
}
if let Some(on_activate) = self.on_activate.as_ref() {
if let Event::Keyboard(keyboard::Event::KeyReleased {
key_code: keyboard::KeyCode::Enter,
..
}) = event
{
shell.publish(on_activate(state.focused_key));
return event::Status::Captured;
}
}
}
event::Status::Ignored
}
fn operate(
&self,
tree: &mut Tree,
_layout: Layout<'_>,
operation: &mut dyn Operation<Message>,
) {
let state = tree.state.downcast_mut::<LocalState>();
operation.focusable(state, self.id.as_ref().map(|id| &id.0));
}
fn mouse_interaction(
&self,
_tree: &Tree,
@ -234,14 +349,23 @@ where
_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
if bounds.contains(cursor_position) {
for (nth, content) in self.model.items.values().enumerate() {
if self
.variant_button_bounds(bounds, nth)
.contains(cursor_position)
{
return if content.enabled {
iced_native::mouse::Interaction::Pointer
} else {
iced_native::mouse::Interaction::Idle
};
}
}
}
iced_native::mouse::Interaction::Idle
}
#[allow(clippy::too_many_lines)]
@ -255,10 +379,10 @@ where
_cursor_position: iced::Point,
_viewport: &iced::Rectangle,
) {
let state = tree.state.downcast_ref::<UniqueWidgetState>();
let state = tree.state.downcast_ref::<LocalState>();
let appearance = Self::variant_appearance(theme, &self.style);
let bounds = layout.bounds();
let button_amount = self.state.buttons.len();
let button_amount = self.model.items.len();
// Draw the background, if a background was defined.
if let Some(background) = appearance.background {
@ -273,11 +397,13 @@ where
);
}
// Draw each of the buttons in the widget.
for (nth, (key, content)) in self.state.buttons.iter().enumerate() {
// Draw each of the items in the widget.
for (nth, (key, content)) in self.model.items.iter().enumerate() {
let mut bounds = self.variant_button_bounds(bounds, nth);
let (status_appearance, font) = if self.state.selection.is_active(key) {
let (status_appearance, font) = if state.focused_key == key {
(appearance.focus, &self.font_active)
} else if self.model.selection.is_active(key) {
(appearance.active, &self.font_active)
} else if state.hovered == key {
(appearance.hover, &self.font_hovered)
@ -413,7 +539,7 @@ where
Message: 'static + Clone,
{
fn from(mut widget: SegmentedButton<'a, Variant, Selection, Message, Renderer>) -> Self {
if widget.state.buttons.is_empty() {
if widget.model.items.is_empty() {
widget.spacing = 0;
}
@ -421,9 +547,33 @@ where
}
}
/// 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,
/// A command that focuses a segmented item stored in a widget.
#[must_use]
pub fn focus<Message: 'static>(id: Id) -> Command<Message> {
Command::widget(operation::focusable::focus(id.0))
}
/// The identifier of a segmented item.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Id(widget::Id);
impl Id {
/// Creates a custom [`Id`].
pub fn new(id: impl Into<std::borrow::Cow<'static, str>>) -> Self {
Self(widget::Id::new(id))
}
/// Creates a unique [`Id`].
///
/// This function produces a different [`Id`] every time it is called.
#[must_use]
pub fn unique() -> Self {
Self(widget::Id::unique())
}
}
impl From<Id> for widget::Id {
fn from(id: Id) -> Self {
id.0
}
}