Implemented rudimentary table widget, currently not interactible
This commit is contained in:
parent
75a11b3c84
commit
949a31855d
7 changed files with 796 additions and 1 deletions
|
|
@ -315,6 +315,8 @@ pub use spin_button::{spin_button, vertical as vertical_spin_button, SpinButton}
|
|||
|
||||
pub mod tab_bar;
|
||||
|
||||
pub mod table;
|
||||
|
||||
pub mod text;
|
||||
#[doc(inline)]
|
||||
pub use text::{text, Text};
|
||||
|
|
@ -337,7 +339,6 @@ pub use toggler::toggler;
|
|||
pub use tooltip::{tooltip, Tooltip};
|
||||
pub mod tooltip {
|
||||
use crate::Element;
|
||||
use std::borrow::Cow;
|
||||
|
||||
pub use iced::widget::tooltip::Position;
|
||||
|
||||
|
|
|
|||
7
src/widget/table/mod.rs
Normal file
7
src/widget/table/mod.rs
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
//! A widget allowing the user to display tables of information with optional sorting by category
|
||||
//!
|
||||
|
||||
pub mod model;
|
||||
pub use model::{Entity, Model};
|
||||
pub mod widget;
|
||||
pub use widget::TableView;
|
||||
17
src/widget/table/model/category.rs
Normal file
17
src/widget/table/model/category.rs
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
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 {
|
||||
/// Function that gets the width of the data
|
||||
fn width(&self) -> iced::Length;
|
||||
}
|
||||
|
||||
pub trait ItemInterface<Category: ItemCategory>: Default {
|
||||
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;
|
||||
}
|
||||
127
src/widget/table/model/entity.rs
Normal file
127
src/widget/table/model/entity.rs
Normal 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
|
||||
}
|
||||
}
|
||||
354
src/widget/table/model/mod.rs
Normal file
354
src/widget/table/model/mod.rs
Normal file
|
|
@ -0,0 +1,354 @@
|
|||
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: (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: (Category::default(), false),
|
||||
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) -> EntityMut<SelectionMode, Item, Category> {
|
||||
let id = self.items.insert(Item::default());
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 = (category, ascending);
|
||||
let mut order: Vec<Entity> = self.order.iter().cloned().collect();
|
||||
order.sort_by(|entity_a, entity_b| {
|
||||
self.item(*entity_a)
|
||||
.unwrap()
|
||||
.compare(self.item(*entity_b).unwrap(), category)
|
||||
});
|
||||
self.order = order.into();
|
||||
}
|
||||
}
|
||||
115
src/widget/table/model/selection.rs
Normal file
115
src/widget/table/model/selection.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
174
src/widget/table/widget.rs
Normal file
174
src/widget/table/widget.rs
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
use super::model::{
|
||||
category::{ItemCategory, ItemInterface},
|
||||
selection::Selectable,
|
||||
Entity, Model,
|
||||
};
|
||||
use crate::{
|
||||
ext::CollectionWidget,
|
||||
theme,
|
||||
widget::{self, container, divider},
|
||||
Apply, Element,
|
||||
};
|
||||
use iced::{Alignment, Padding};
|
||||
use iced_widget::container::Catalog;
|
||||
|
||||
// THIS IS A PLACEHOLDER UNTIL A MORE SOPHISTICATED WIDGET CAN BE DEVELOPED
|
||||
|
||||
#[must_use]
|
||||
pub struct TableView<'a, SelectionMode, Item, Category, Message>
|
||||
where
|
||||
Category: ItemCategory,
|
||||
Item: ItemInterface<Category>,
|
||||
Model<SelectionMode, Item, Category>: Selectable,
|
||||
SelectionMode: Default,
|
||||
{
|
||||
pub(super) model: &'a Model<SelectionMode, Item, Category>,
|
||||
|
||||
pub(super) spacing: u16,
|
||||
pub(super) padding: Padding,
|
||||
pub(super) list_item_padding: Padding,
|
||||
pub(super) divider_padding: Padding,
|
||||
pub(super) style: theme::Container<'a>,
|
||||
|
||||
pub(super) on_selected: Box<dyn Fn(Entity) -> Message + 'a>,
|
||||
pub(super) on_category_select: Box<dyn Fn(Category, bool) -> Message + 'a>,
|
||||
pub(super) on_option_hovered: Option<&'a dyn Fn(usize) -> Message>,
|
||||
}
|
||||
|
||||
impl<'a, SelectionMode, Item, Category, Message>
|
||||
TableView<'a, SelectionMode, Item, Category, Message>
|
||||
where
|
||||
SelectionMode: Default,
|
||||
Model<SelectionMode, Item, Category>: Selectable,
|
||||
Category: ItemCategory,
|
||||
Item: ItemInterface<Category>,
|
||||
{
|
||||
pub fn new(
|
||||
model: &'a Model<SelectionMode, Item, Category>,
|
||||
on_selected: impl Fn(Entity) -> Message + 'a,
|
||||
on_category_select: impl Fn(Category, bool) -> Message + 'a,
|
||||
on_option_hovered: Option<&'a dyn Fn(usize) -> Message>,
|
||||
) -> Self {
|
||||
let cosmic_theme::Spacing {
|
||||
space_xxxs,
|
||||
space_xxs,
|
||||
..
|
||||
} = theme::active().cosmic().spacing;
|
||||
Self {
|
||||
model,
|
||||
spacing: 0,
|
||||
padding: Padding::from(0),
|
||||
divider_padding: Padding::from(0).left(space_xxxs).right(space_xxxs),
|
||||
list_item_padding: Padding::from(space_xxs).into(),
|
||||
style: theme::Container::Background,
|
||||
|
||||
on_selected: Box::new(on_selected),
|
||||
on_category_select: Box::new(on_category_select),
|
||||
on_option_hovered,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn spacing(mut self, spacing: u16) -> Self {
|
||||
self.spacing = spacing;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style variant of this [`Circular`].
|
||||
pub fn style(mut self, style: <crate::Theme as Catalog>::Class<'a>) -> Self {
|
||||
self.style = style;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn padding(mut self, padding: impl Into<Padding>) -> Self {
|
||||
self.padding = padding.into();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn divider_padding(mut self, padding: u16) -> Self {
|
||||
self.divider_padding = Padding::from(0).left(padding).right(padding);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn list_item_padding(mut self, padding: impl Into<Padding>) -> Self {
|
||||
self.list_item_padding = padding.into();
|
||||
self
|
||||
}
|
||||
#[must_use]
|
||||
pub fn into_element(self) -> Element<'a, Message> {
|
||||
let cosmic_theme::Spacing { space_xxxs, .. } = theme::active().cosmic().spacing;
|
||||
|
||||
crate::widget::column()
|
||||
.push(widget::row::with_children(
|
||||
self.model
|
||||
.categories
|
||||
.iter()
|
||||
.map(|category| {
|
||||
container(
|
||||
widget::row()
|
||||
.spacing(space_xxxs)
|
||||
.push(widget::text::heading(category.to_string()))
|
||||
.push_maybe(if self.model.sort.0 == *category {
|
||||
match self.model.sort.1 {
|
||||
true => {
|
||||
Some(widget::icon::from_name("pan-up-symbolic").icon())
|
||||
}
|
||||
false => Some(
|
||||
widget::icon::from_name("pan-down-symbolic").icon(),
|
||||
),
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}),
|
||||
)
|
||||
.padding(
|
||||
Padding::from(0)
|
||||
.left(self.list_item_padding.left)
|
||||
.right(self.list_item_padding.right),
|
||||
)
|
||||
.width(category.width())
|
||||
.into()
|
||||
})
|
||||
.collect(),
|
||||
))
|
||||
.append(&mut if self.model.items.is_empty() {
|
||||
vec![container(divider::horizontal::default()).padding(self.divider_padding)]
|
||||
} else {
|
||||
self.model
|
||||
.order
|
||||
.iter()
|
||||
.map(|entity| {
|
||||
let item = self.model.item(*entity).unwrap();
|
||||
let categories = &self.model.categories;
|
||||
|
||||
vec![
|
||||
container(divider::horizontal::default()).padding(self.divider_padding),
|
||||
container(widget::row::with_children(
|
||||
categories
|
||||
.iter()
|
||||
.map(|category| {
|
||||
container(
|
||||
widget::row()
|
||||
.push_maybe(item.get_icon(*category))
|
||||
.push(widget::text::body(item.get_text(*category))),
|
||||
)
|
||||
.width(category.width())
|
||||
.align_y(Alignment::Center)
|
||||
.padding(self.list_item_padding)
|
||||
.apply(Element::from)
|
||||
})
|
||||
.collect(),
|
||||
)),
|
||||
]
|
||||
})
|
||||
.flatten()
|
||||
.collect()
|
||||
})
|
||||
.spacing(self.spacing)
|
||||
.padding(self.padding)
|
||||
.apply(container)
|
||||
.padding([self.spacing, 0])
|
||||
.class(self.style)
|
||||
.width(iced::Length::Fill)
|
||||
.into()
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue