feat(segmented-button): configurable close icons

This commit is contained in:
Michael Aaron Murphy 2023-02-13 15:57:30 +01:00 committed by Jeremy Soller
parent 843919e44f
commit 4fa61eeafd
10 changed files with 288 additions and 64 deletions

View file

@ -48,6 +48,13 @@ where
self
}
/// Defines that the close button should appear
#[allow(clippy::must_use_candidate, clippy::return_self_not_must_use)]
pub fn closable(mut self) -> Self {
self.model.0.closable_set(self.id, true);
self
}
/// Associates extra data with an external secondary map.
///
/// The secondary map internally uses a `Vec`, so should only be used for data that

View file

@ -63,6 +63,13 @@ where
self
}
/// Shows a close button for this item.
#[allow(clippy::must_use_candidate, clippy::return_self_not_must_use)]
pub fn closable(self) -> Self {
self.model.closable_set(self.id, true);
self
}
/// Associates data with the item.
///
/// There may only be one data component per Rust type.

View file

@ -24,11 +24,15 @@ slotmap::new_key_type! {
#[derive(Clone, Debug)]
pub struct Settings {
pub enabled: bool,
pub closable: bool,
}
impl Default for Settings {
fn default() -> Self {
Self { enabled: true }
Self {
enabled: true,
closable: false,
}
}
}
@ -83,6 +87,16 @@ where
Selectable::activate(self, id);
}
/// Activates the item at the given position, returning true if it was activated.
pub fn activate_position(&mut self, position: u16) -> bool {
if let Some(entity) = self.entity_at(position) {
self.activate(entity);
return true;
}
false
}
/// Creates a builder for initializing a model.
///
/// ```ignore
@ -112,6 +126,13 @@ where
}
}
/// Shows or hides the item's close button.
pub fn closable_set(&mut self, id: Entity, closable: bool) {
if let Some(settings) = self.items.get_mut(id) {
settings.closable = closable;
}
}
/// Check if an item exists in the map.
///
/// ```ignore
@ -187,6 +208,12 @@ where
}
}
/// Get the item that is located at a given position.
#[must_use]
pub fn entity_at(&mut self, position: u16) -> Option<Entity> {
self.order.get(position as usize).copied()
}
/// Immutable reference to the icon associated with the item.
///
/// ```ignore
@ -239,10 +266,21 @@ where
EntityMut { model: self, id }
}
/// Check if the given ID is the active ID.
#[must_use]
pub fn is_active(&self, id: Entity) -> bool {
<Self as Selectable>::is_active(self, id)
}
/// Whether the item should contain a close button.
#[must_use]
pub fn is_closable(&self, id: Entity) -> bool {
self.items.get(id).map_or(false, |e| e.closable)
}
/// Check if the item is enabled.
///
/// ```ignore
///
/// if model.is_enabled(id) {
/// if let Some(text) = model.text(id) {
/// println!("{text} is enabled");
@ -254,14 +292,21 @@ where
self.items.get(id).map_or(false, |e| e.enabled)
}
/// Iterates across items in the model in the order that they are displayed.
pub fn iter(&self) -> impl Iterator<Item = Entity> + '_ {
self.order.iter().copied()
}
/// The position of the item in the model.
///
/// ```ignore
/// if let Some(position) = model.position(id) {
/// println!("found item at {}", position);
/// }
pub fn position(&self, id: Entity) -> Option<usize> {
self.order.iter().position(|k| *k == id)
#[must_use]
pub fn position(&self, id: Entity) -> Option<u16> {
#[allow(clippy::cast_possible_truncation)]
self.order.iter().position(|k| *k == id).map(|v| v as u16)
}
/// Change the position of an item in the model.
@ -278,7 +323,7 @@ where
let position = self.order.len().min(position as usize);
self.order.remove(index);
self.order.remove(index as usize);
self.order.insert(position, id);
Some(position)
}
@ -301,7 +346,7 @@ where
return false
};
self.order.swap(first_index, second_index);
self.order.swap(first_index as usize, second_index as usize);
true
}
@ -319,7 +364,7 @@ where
}
if let Some(index) = self.position(id) {
self.order.remove(index);
self.order.remove(index as usize);
}
}

View file

@ -33,8 +33,10 @@ impl Selectable for Model<SingleSelect> {
self.selection.active = id;
}
fn deactivate(&mut self, _id: Entity) {
self.selection.active = Entity::default();
fn deactivate(&mut self, id: Entity) {
if id == self.selection.active {
self.selection.active = Entity::default();
}
}
fn is_active(&self, id: Entity) -> bool {

View file

@ -1,11 +1,9 @@
// Copyright 2022 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
use std::marker::PhantomData;
use super::model::{Entity, Model, Selectable};
use super::style::StyleSheet;
use crate::widget::{icon, IconSource};
use derive_setters::Setters;
use iced::{
alignment, event, keyboard, mouse, touch, Background, Color, Command, Element, Event, Length,
@ -14,6 +12,7 @@ use iced::{
use iced_core::BorderRadius;
use iced_native::widget::{self, operation, tree, Operation};
use iced_native::{layout, renderer, widget::Tree, Clipboard, Layout, Shell, Widget};
use std::marker::PhantomData;
/// State that is maintained by each individual widget.
#[derive(Default)]
@ -80,6 +79,10 @@ where
pub(super) model: &'a Model<SelectionMode>,
/// iced widget ID
pub(super) id: Option<Id>,
/// The icon used for the close button.
pub(super) close_icon: IconSource<'a>,
/// Show the close icon only when item is hovered.
pub(super) show_close_icon_on_hover: bool,
/// Padding around a button.
pub(super) button_padding: [u16; 4],
/// Desired height of a button.
@ -105,9 +108,11 @@ where
/// 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(Entity) -> Message>>,
/// Emits the ID of the item that was activated.
#[setters(strip_option)]
pub(super) on_activate: Option<fn(Entity) -> Message>,
#[setters(strip_option)]
pub(super) on_close: Option<fn(Entity) -> Message>,
#[setters(skip)]
/// Defines the implementation of this struct
variant: PhantomData<Variant>,
@ -130,6 +135,8 @@ where
Self {
model,
id: None,
close_icon: IconSource::from("window-close-symbolic"),
show_close_icon_on_hover: false,
button_padding: [4, 4, 4, 4],
button_height: 32,
button_spacing: 4,
@ -143,6 +150,7 @@ where
spacing: 0,
style: <Renderer::Theme as StyleSheet>::Style::default(),
on_activate: None,
on_close: None,
variant: PhantomData,
}
}
@ -200,13 +208,6 @@ where
event::Status::Ignored
}
/// Emits the ID of the activated widget on selection.
#[must_use]
pub fn on_activate(mut self, on_activate: impl Fn(Entity) -> Message + 'static) -> Self {
self.on_activate = Some(Box::from(on_activate));
self
}
pub(super) fn max_button_dimensions(&self, renderer: &Renderer, bounds: Size) -> (f32, f32) {
let mut width = 0.0f32;
let mut height = 0.0f32;
@ -225,8 +226,14 @@ where
// Add icon to measurement if icon was given.
if self.model.icon(key).is_some() {
button_height = button_height.max(f32::from(self.icon_size));
button_width += f32::from(self.icon_size) + f32::from(self.button_spacing);
}
// Add close button to measurement if found.
if self.model.is_closable(key) {
button_height = button_height.max(f32::from(self.icon_size));
button_width += f32::from(self.icon_size) + f32::from(self.button_spacing);
button_height = f32::from(self.icon_size);
}
height = height.max(button_height);
@ -299,6 +306,28 @@ where
// Record that the mouse is hovering over this button.
state.hovered = key;
// If marked as closable, show a close icon.
if self.model.items[key].closable {
if let Some(on_close) = self.on_close.as_ref() {
if close_bounds(
bounds,
f32::from(self.icon_size),
self.button_padding,
)
.contains(cursor_position)
{
if let Event::Mouse(mouse::Event::ButtonReleased(
mouse::Button::Left,
))
| Event::Touch(touch::Event::FingerLifted { .. }) = event
{
shell.publish(on_close(key));
return event::Status::Captured;
}
}
}
}
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
@ -416,11 +445,14 @@ where
for (nth, key) in self.model.order.iter().copied().enumerate() {
let mut bounds = self.variant_button_bounds(bounds, nth);
let key_is_active = self.model.is_active(key);
let key_is_hovered = state.hovered == key;
let (status_appearance, font) = if state.focused_key == key {
(appearance.focus, &self.font_active)
} else if self.model.is_active(key) {
} else if key_is_active {
(appearance.active, &self.font_active)
} else if state.hovered == key {
} else if key_is_hovered {
(appearance.hover, &self.font_hovered)
} else {
(appearance.inactive, &self.font_inactive)
@ -466,6 +498,8 @@ where
);
}
let original_bounds = bounds;
let y = bounds.center_y();
// Draw the image beside the text.
@ -489,11 +523,12 @@ where
bounds.x += offset;
bounds.width -= offset;
match icon.load(self.icon_size, None, false) {
crate::widget::icon::Handle::Image(_handle) => {
icon::Handle::Image(_handle) => {
unimplemented!()
}
crate::widget::icon::Handle::Svg(handle) => {
icon::Handle::Svg(handle) => {
iced_native::svg::Renderer::draw(
renderer,
handle,
@ -523,6 +558,30 @@ where
vertical_alignment: alignment::Vertical::Center,
});
}
let show_close_button =
(key_is_active || !self.show_close_icon_on_hover || key_is_hovered)
&& self.model.is_closable(key);
// Draw a close button if this is set.
if show_close_button {
let width = f32::from(self.icon_size);
let icon_bounds = close_bounds(original_bounds, width, self.button_padding);
match self.close_icon.load(self.icon_size, None, false) {
icon::Handle::Image(_handle) => {
unimplemented!()
}
icon::Handle::Svg(handle) => {
iced_native::svg::Renderer::draw(
renderer,
handle,
Some(status_appearance.text_color),
icon_bounds,
);
}
}
}
}
}
@ -592,3 +651,17 @@ impl From<Id> for widget::Id {
id.0
}
}
/// Calculates the bounds of the close button within the area of an item.
fn close_bounds(area: Rectangle<f32>, icon_size: f32, button_padding: [u16; 4]) -> Rectangle<f32> {
let top = f32::from(button_padding[1]);
let end = f32::from(button_padding[2]);
let unpadded_height = area.height - top - end;
Rectangle {
x: area.x + area.width - icon_size - f32::from(button_padding[2]),
y: area.y + f32::from(button_padding[1]) + (unpadded_height / 2.0),
width: icon_size,
height: icon_size,
}
}