feat(widget): add table widget

This commit is contained in:
Adam Cosner 2025-03-24 16:48:20 +00:00 committed by GitHub
parent c955c8400f
commit 2753941aad
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 1599 additions and 1 deletions

View file

@ -0,0 +1,19 @@
use std::borrow::Cow;
use crate::widget::Icon;
/// Implementation of std::fmt::Display allows user to customize the header
/// Ideally, this is implemented on an enum.
pub trait ItemCategory:
Default + std::fmt::Display + Clone + Copy + PartialEq + Eq + std::hash::Hash
{
/// Function that gets the width of the data
fn width(&self) -> iced::Length;
}
pub trait ItemInterface<Category: ItemCategory> {
fn get_icon(&self, category: Category) -> Option<Icon>;
fn get_text(&self, category: Category) -> Cow<'static, str>;
fn compare(&self, other: &Self, category: Category) -> std::cmp::Ordering;
}

View file

@ -0,0 +1,127 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
use slotmap::{SecondaryMap, SparseSecondaryMap};
use super::{
category::{ItemCategory, ItemInterface},
Entity, Model, Selectable,
};
/// A newly-inserted item which may have additional actions applied to it.
pub struct EntityMut<
'a,
SelectionMode: Default,
Item: ItemInterface<Category>,
Category: ItemCategory,
> {
pub(super) id: Entity,
pub(super) model: &'a mut Model<SelectionMode, Item, Category>,
}
impl<'a, SelectionMode: Default, Item: ItemInterface<Category>, Category: ItemCategory>
EntityMut<'a, SelectionMode, Item, Category>
where
Model<SelectionMode, Item, Category>: Selectable,
{
/// Activates the newly-inserted item.
///
/// ```ignore
/// model.insert().text("Item A").activate();
/// ```
#[allow(clippy::must_use_candidate, clippy::return_self_not_must_use)]
pub fn activate(self) -> Self {
self.model.activate(self.id);
self
}
/// Associates extra data with an external secondary map.
///
/// The secondary map internally uses a `Vec`, so should only be used for data that
/// is commonly associated.
///
/// ```ignore
/// let mut secondary_data = segmented_button::SecondaryMap::default();
/// model.insert().text("Item A").secondary(&mut secondary_data, String::new("custom data"));
/// ```
#[allow(clippy::must_use_candidate, clippy::return_self_not_must_use)]
pub fn secondary<Data>(self, map: &mut SecondaryMap<Entity, Data>, data: Data) -> Self {
map.insert(self.id, data);
self
}
/// Associates extra data with an external sparse secondary map.
///
/// Sparse maps internally use a `HashMap`, for data that is sparsely associated.
///
/// ```ignore
/// let mut secondary_data = segmented_button::SparseSecondaryMap::default();
/// model.insert().text("Item A").secondary(&mut secondary_data, String::new("custom data"));
/// ```
#[allow(clippy::must_use_candidate, clippy::return_self_not_must_use)]
pub fn secondary_sparse<Data>(
self,
map: &mut SparseSecondaryMap<Entity, Data>,
data: Data,
) -> Self {
map.insert(self.id, data);
self
}
/// Associates data with the item.
///
/// There may only be one data component per Rust type.
///
/// ```ignore
/// model.insert().text("Item A").data(String::from("custom string"));
/// ```
#[allow(clippy::must_use_candidate, clippy::return_self_not_must_use)]
pub fn data<Data: 'static>(self, data: Data) -> Self {
self.model.data_set(self.id, data);
self
}
/// Returns the ID of the item that was inserted.
///
/// ```ignore
/// let id = model.insert("Item A").id();
/// ```
#[must_use]
pub fn id(self) -> Entity {
self.id
}
#[allow(clippy::must_use_candidate, clippy::return_self_not_must_use)]
pub fn indent(self, indent: u16) -> Self {
self.model.indent_set(self.id, indent);
self
}
/// Define the position of the item.
#[allow(clippy::must_use_candidate, clippy::return_self_not_must_use)]
pub fn position(self, position: u16) -> Self {
self.model.position_set(self.id, position);
self
}
/// Swap the position with another item in the model.
#[allow(clippy::must_use_candidate, clippy::return_self_not_must_use)]
pub fn position_swap(self, other: Entity) -> Self {
self.model.position_swap(self.id, other);
self
}
/// Defines the text for the item.
#[allow(clippy::must_use_candidate, clippy::return_self_not_must_use)]
pub fn item(self, item: Item) -> Self {
self.model.item_set(self.id, item);
self
}
/// Calls a function with the ID without consuming the wrapper.
#[allow(clippy::must_use_candidate, clippy::return_self_not_must_use)]
pub fn with_id(self, func: impl FnOnce(Entity)) -> Self {
func(self.id);
self
}
}

View file

@ -0,0 +1,365 @@
pub mod category;
pub mod entity;
pub mod selection;
use std::{
any::{Any, TypeId},
collections::{HashMap, VecDeque},
};
use category::{ItemCategory, ItemInterface};
use entity::EntityMut;
use selection::Selectable;
use slotmap::{SecondaryMap, SlotMap};
slotmap::new_key_type! {
/// Unique key type for items in the table
pub struct Entity;
}
/// The portion of the model used only by the application.
#[derive(Debug, Default)]
pub(super) struct Storage(HashMap<TypeId, SecondaryMap<Entity, Box<dyn Any>>>);
pub struct Model<SelectionMode: Default, Item: ItemInterface<Category>, Category: ItemCategory>
where
Category: ItemCategory,
{
pub(super) categories: Vec<Category>,
/// Stores the items
pub(super) items: SlotMap<Entity, Item>,
/// Whether the item is selected or not
pub(super) active: SecondaryMap<Entity, bool>,
/// Optional indents for the table items
pub(super) indents: SecondaryMap<Entity, u16>,
/// Order which the items will be displayed.
pub(super) order: VecDeque<Entity>,
/// Stores the current selection(s)
pub(super) selection: SelectionMode,
/// What category to sort by and whether it's ascending or not
pub(super) sort: Option<(Category, bool)>,
/// Application-managed data associated with each item
pub(super) storage: Storage,
}
impl<SelectionMode: Default, Item: ItemInterface<Category>, Category: ItemCategory>
Model<SelectionMode, Item, Category>
where
Self: Selectable,
{
pub fn new(categories: Vec<Category>) -> Self {
Self {
categories,
items: SlotMap::default(),
active: SecondaryMap::default(),
indents: SecondaryMap::default(),
order: VecDeque::new(),
selection: SelectionMode::default(),
sort: None,
storage: Storage::default(),
}
}
pub fn categories(&mut self, cats: Vec<Category>) {
self.categories = cats;
}
/// Activates the item in the model.
///
/// ```ignore
/// model.activate(id);
/// ```
pub fn activate(&mut self, id: Entity) {
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
}
/// Removes all items from the model.
///
/// Any IDs held elsewhere by the application will no longer be usable with the map.
/// The generation is incremented on removal, so the stale IDs will return `None` for
/// any attempt to get values from the map.
///
/// ```ignore
/// model.clear();
/// ```
pub fn clear(&mut self) {
for entity in self.order.clone() {
self.remove(entity);
}
}
/// Check if an item exists in the map.
///
/// ```ignore
/// if model.contains_item(id) {
/// println!("ID is still valid");
/// }
/// ```
pub fn contains_item(&self, id: Entity) -> bool {
self.items.contains_key(id)
}
/// Get an immutable reference to data associated with an item.
///
/// ```ignore
/// if let Some(data) = model.data::<String>(id) {
/// println!("found string on {:?}: {}", id, data);
/// }
/// ```
pub fn item(&self, id: Entity) -> Option<&Item> {
self.items.get(id)
}
/// Get a mutable reference to data associated with an item.
pub fn item_mut(&mut self, id: Entity) -> Option<&mut Item> {
self.items.get_mut(id)
}
/// Associates data with the item.
///
/// There may only be one data component per Rust type.
///
/// ```ignore
/// model.data_set::<String>(id, String::from("custom string"));
/// ```
pub fn item_set(&mut self, id: Entity, data: Item) {
if let Some(item) = self.items.get_mut(id) {
*item = data;
}
}
/// Get an immutable reference to data associated with an item.
///
/// ```ignore
/// if let Some(data) = model.data::<String>(id) {
/// println!("found string on {:?}: {}", id, data);
/// }
/// ```
pub fn data<Data: 'static>(&self, id: Entity) -> Option<&Data> {
self.storage
.0
.get(&TypeId::of::<Data>())
.and_then(|storage| storage.get(id))
.and_then(|data| data.downcast_ref())
}
/// Get a mutable reference to data associated with an item.
pub fn data_mut<Data: 'static>(&mut self, id: Entity) -> Option<&mut Data> {
self.storage
.0
.get_mut(&TypeId::of::<Data>())
.and_then(|storage| storage.get_mut(id))
.and_then(|data| data.downcast_mut())
}
/// Associates data with the item.
///
/// There may only be one data component per Rust type.
///
/// ```ignore
/// model.data_set::<String>(id, String::from("custom string"));
/// ```
pub fn data_set<Data: 'static>(&mut self, id: Entity, data: Data) {
if self.contains_item(id) {
self.storage
.0
.entry(TypeId::of::<Data>())
.or_default()
.insert(id, Box::new(data));
}
}
/// Removes a specific data type from the item.
///
/// ```ignore
/// model.data.remove::<String>(id);
/// ```
pub fn data_remove<Data: 'static>(&mut self, id: Entity) {
self.storage
.0
.get_mut(&TypeId::of::<Data>())
.and_then(|storage| storage.remove(id));
}
/// Enable or disable an item.
///
/// ```ignore
/// model.enable(id, true);
/// ```
pub fn enable(&mut self, id: Entity, enable: bool) {
if let Some(e) = self.active.get_mut(id) {
*e = enable;
}
}
/// 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()
}
/// Inserts a new item in the model.
///
/// ```ignore
/// let id = model.insert().text("Item A").icon("custom-icon").id();
/// ```
#[must_use]
pub fn insert(&mut self, item: Item) -> EntityMut<SelectionMode, Item, Category> {
let id = self.items.insert(item);
self.order.push_back(id);
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)
}
/// Check if the item is enabled.
///
/// ```ignore
/// if model.is_enabled(id) {
/// if let Some(text) = model.text(id) {
/// println!("{text} is enabled");
/// }
/// }
/// ```
#[must_use]
pub fn is_enabled(&self, id: Entity) -> bool {
self.active.get(id).map_or(false, |e| *e)
}
/// 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()
}
pub fn indent(&self, id: Entity) -> Option<u16> {
self.indents.get(id).copied()
}
pub fn indent_set(&mut self, id: Entity, indent: u16) -> Option<u16> {
if !self.contains_item(id) {
return None;
}
self.indents.insert(id, indent)
}
pub fn indent_remove(&mut self, id: Entity) -> Option<u16> {
self.indents.remove(id)
}
/// The position of the item in the model.
///
/// ```ignore
/// if let Some(position) = model.position(id) {
/// println!("found item at {}", position);
/// }
#[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.
///
/// ```ignore
/// if let Some(new_position) = model.position_set(id, 0) {
/// println!("placed item at {}", new_position);
/// }
/// ```
pub fn position_set(&mut self, id: Entity, position: u16) -> Option<usize> {
let Some(index) = self.position(id) else {
return None;
};
self.order.remove(index as usize);
let position = self.order.len().min(position as usize);
self.order.insert(position, id);
Some(position)
}
/// Swap the position of two items in the model.
///
/// Returns false if the swap cannot be performed.
///
/// ```ignore
/// if model.position_swap(first_id, second_id) {
/// println!("positions swapped");
/// }
/// ```
pub fn position_swap(&mut self, first: Entity, second: Entity) -> bool {
let Some(first_index) = self.position(first) else {
return false;
};
let Some(second_index) = self.position(second) else {
return false;
};
self.order.swap(first_index as usize, second_index as usize);
true
}
/// Removes an item from the model.
///
/// The generation of the slot for the ID will be incremented, so this ID will no
/// longer be usable with the map. Subsequent attempts to get values from the map
/// with this ID will return `None` and failed to assign values.
pub fn remove(&mut self, id: Entity) {
self.items.remove(id);
self.deactivate(id);
for storage in self.storage.0.values_mut() {
storage.remove(id);
}
if let Some(index) = self.position(id) {
self.order.remove(index as usize);
}
}
/// Get the sort data
pub fn get_sort(&self) -> Option<(Category, bool)> {
self.sort
}
/// Sorts items in the model, this should be called before it is drawn after all items have been added for the view
pub fn sort(&mut self, category: Category, ascending: bool) {
self.sort = Some((category, ascending));
let mut order: Vec<Entity> = self.order.iter().cloned().collect();
order.sort_by(|entity_a, entity_b| {
if ascending {
self.item(*entity_a)
.unwrap()
.compare(self.item(*entity_b).unwrap(), category)
} else {
self.item(*entity_b)
.unwrap()
.compare(self.item(*entity_a).unwrap(), category)
}
});
self.order = order.into();
}
}

View file

@ -0,0 +1,115 @@
// Copyright 2022 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
//! Describes logic specific to the single-select and multi-select modes of a model.
use super::{
category::{ItemCategory, ItemInterface},
Entity, Model,
};
use std::collections::HashSet;
/// Describes a type that has selectable items.
pub trait Selectable {
/// Activate an item.
fn activate(&mut self, id: Entity);
/// Deactivate an item.
fn deactivate(&mut self, id: Entity);
/// Checks if the item is active.
fn is_active(&self, id: Entity) -> bool;
}
/// [`Model<SingleSelect>`] Ensures that only one key may be selected.
#[derive(Debug, Default)]
pub struct SingleSelect {
pub active: Entity,
}
impl<Item: ItemInterface<Category>, Category: ItemCategory> Selectable
for Model<SingleSelect, Item, Category>
{
fn activate(&mut self, id: Entity) {
if !self.items.contains_key(id) {
return;
}
self.selection.active = id;
}
fn deactivate(&mut self, id: Entity) {
if id == self.selection.active {
self.selection.active = Entity::default();
}
}
fn is_active(&self, id: Entity) -> bool {
self.selection.active == id
}
}
impl<Item: ItemInterface<Category>, Category: ItemCategory> Model<SingleSelect, Item, Category> {
/// Get an immutable reference to the data associated with the active item.
#[must_use]
pub fn active_data<Data: 'static>(&self) -> Option<&Data> {
self.data(self.active())
}
/// Get a mutable reference to the data associated with the active item.
#[must_use]
pub fn active_data_mut<Data: 'static>(&mut self) -> Option<&mut Data> {
self.data_mut(self.active())
}
/// Deactivates the active item.
pub fn deactivate(&mut self) {
Selectable::deactivate(self, Entity::default());
}
/// The ID of the active item.
#[must_use]
pub fn active(&self) -> Entity {
self.selection.active
}
}
/// [`Model<MultiSelect>`] permits multiple keys to be active at a time.
#[derive(Debug, Default)]
pub struct MultiSelect {
pub active: HashSet<Entity>,
}
impl<Item: ItemInterface<Category>, Category: ItemCategory> Selectable
for Model<MultiSelect, Item, Category>
{
fn activate(&mut self, id: Entity) {
if !self.items.contains_key(id) {
return;
}
if !self.selection.active.insert(id) {
self.selection.active.remove(&id);
}
}
fn deactivate(&mut self, id: Entity) {
self.selection.active.remove(&id);
}
fn is_active(&self, id: Entity) -> bool {
self.selection.active.contains(&id)
}
}
impl<Item: ItemInterface<Category>, Category: ItemCategory> Model<MultiSelect, Item, Category> {
/// Deactivates the item in the model.
pub fn deactivate(&mut self, id: Entity) {
Selectable::deactivate(self, id);
}
/// The IDs of the active items.
pub fn active(&self) -> impl Iterator<Item = Entity> + '_ {
self.selection.active.iter().copied()
}
}