feat(widget): add table widget
This commit is contained in:
parent
c955c8400f
commit
2753941aad
12 changed files with 1599 additions and 1 deletions
15
examples/table-view/Cargo.toml
Normal file
15
examples/table-view/Cargo.toml
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
[package]
|
||||||
|
name = "table-view"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
tracing = "0.1.37"
|
||||||
|
tracing-subscriber = "0.3.17"
|
||||||
|
tracing-log = "0.2.0"
|
||||||
|
chrono = "*"
|
||||||
|
|
||||||
|
[dependencies.libcosmic]
|
||||||
|
features = ["debug", "multi-window", "wayland", "winit", "desktop", "tokio"]
|
||||||
|
path = "../.."
|
||||||
|
default-features = false
|
||||||
272
examples/table-view/src/main.rs
Normal file
272
examples/table-view/src/main.rs
Normal file
|
|
@ -0,0 +1,272 @@
|
||||||
|
// Copyright 2023 System76 <info@system76.com>
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
//! Table API example
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use chrono::Datelike;
|
||||||
|
use cosmic::app::{Core, Settings, Task};
|
||||||
|
use cosmic::iced_core::Size;
|
||||||
|
use cosmic::prelude::*;
|
||||||
|
use cosmic::widget::table;
|
||||||
|
use cosmic::widget::{self, nav_bar};
|
||||||
|
use cosmic::{executor, iced};
|
||||||
|
|
||||||
|
#[derive(Debug, Default, PartialEq, Eq, Clone, Copy, Hash)]
|
||||||
|
pub enum Category {
|
||||||
|
#[default]
|
||||||
|
Name,
|
||||||
|
Date,
|
||||||
|
Size,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for Category {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str(match self {
|
||||||
|
Self::Name => "Name",
|
||||||
|
Self::Date => "Date",
|
||||||
|
Self::Size => "Size",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl table::ItemCategory for Category {
|
||||||
|
fn width(&self) -> iced::Length {
|
||||||
|
match self {
|
||||||
|
Self::Name => iced::Length::Fill,
|
||||||
|
Self::Date => iced::Length::Fixed(200.0),
|
||||||
|
Self::Size => iced::Length::Fixed(150.0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Item {
|
||||||
|
name: String,
|
||||||
|
date: chrono::DateTime<chrono::Local>,
|
||||||
|
size: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Item {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
name: Default::default(),
|
||||||
|
date: Default::default(),
|
||||||
|
size: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl table::ItemInterface<Category> for Item {
|
||||||
|
fn get_icon(&self, category: Category) -> Option<cosmic::widget::Icon> {
|
||||||
|
if category == Category::Name {
|
||||||
|
Some(cosmic::widget::icon::from_name("application-x-executable-symbolic").icon())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_text(&self, category: Category) -> std::borrow::Cow<'static, str> {
|
||||||
|
match category {
|
||||||
|
Category::Name => self.name.clone().into(),
|
||||||
|
Category::Date => self.date.format("%Y/%m/%d").to_string().into(),
|
||||||
|
Category::Size => format!("{} items", self.size).into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn compare(&self, other: &Self, category: Category) -> std::cmp::Ordering {
|
||||||
|
match category {
|
||||||
|
Category::Name => self.name.to_lowercase().cmp(&other.name.to_lowercase()),
|
||||||
|
Category::Date => self.date.cmp(&other.date),
|
||||||
|
Category::Size => self.size.cmp(&other.size),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Runs application with these settings
|
||||||
|
#[rustfmt::skip]
|
||||||
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
tracing_subscriber::fmt::init();
|
||||||
|
let _ = tracing_log::LogTracer::init();
|
||||||
|
|
||||||
|
let settings = Settings::default()
|
||||||
|
.size(Size::new(1024., 768.));
|
||||||
|
|
||||||
|
cosmic::app::run::<App>(settings, ())?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Messages that are used specifically by our [`App`].
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub enum Message {
|
||||||
|
ItemSelect(table::Entity),
|
||||||
|
CategorySelect(Category),
|
||||||
|
PrintMsg(String),
|
||||||
|
NoOp,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The [`App`] stores application-specific state.
|
||||||
|
pub struct App {
|
||||||
|
core: Core,
|
||||||
|
table_model: table::SingleSelectModel<Item, Category>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Implement [`cosmic::Application`] to integrate with COSMIC.
|
||||||
|
impl cosmic::Application for App {
|
||||||
|
/// Default async executor to use with the app.
|
||||||
|
type Executor = executor::Default;
|
||||||
|
|
||||||
|
/// Argument received [`cosmic::Application::new`].
|
||||||
|
type Flags = ();
|
||||||
|
|
||||||
|
/// Message type specific to our [`App`].
|
||||||
|
type Message = Message;
|
||||||
|
|
||||||
|
/// The unique application ID to supply to the window manager.
|
||||||
|
const APP_ID: &'static str = "org.cosmic.AppDemoTable";
|
||||||
|
|
||||||
|
fn core(&self) -> &Core {
|
||||||
|
&self.core
|
||||||
|
}
|
||||||
|
|
||||||
|
fn core_mut(&mut self) -> &mut Core {
|
||||||
|
&mut self.core
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates the application, and optionally emits task on initialize.
|
||||||
|
fn init(core: Core, _: Self::Flags) -> (Self, Task<Self::Message>) {
|
||||||
|
let mut nav_model = nav_bar::Model::default();
|
||||||
|
|
||||||
|
nav_model.activate_position(0);
|
||||||
|
|
||||||
|
let mut table_model =
|
||||||
|
table::Model::new(vec![Category::Name, Category::Date, Category::Size]);
|
||||||
|
|
||||||
|
let _ = table_model.insert(Item {
|
||||||
|
name: "Foo".into(),
|
||||||
|
date: chrono::DateTime::default()
|
||||||
|
.with_day(1)
|
||||||
|
.unwrap()
|
||||||
|
.with_month(1)
|
||||||
|
.unwrap()
|
||||||
|
.with_year(1970)
|
||||||
|
.unwrap(),
|
||||||
|
size: 2,
|
||||||
|
});
|
||||||
|
let _ = table_model.insert(Item {
|
||||||
|
name: "Bar".into(),
|
||||||
|
date: chrono::DateTime::default()
|
||||||
|
.with_day(2)
|
||||||
|
.unwrap()
|
||||||
|
.with_month(1)
|
||||||
|
.unwrap()
|
||||||
|
.with_year(1970)
|
||||||
|
.unwrap(),
|
||||||
|
size: 4,
|
||||||
|
});
|
||||||
|
let _ = table_model.insert(Item {
|
||||||
|
name: "Baz".into(),
|
||||||
|
date: chrono::DateTime::default()
|
||||||
|
.with_day(3)
|
||||||
|
.unwrap()
|
||||||
|
.with_month(1)
|
||||||
|
.unwrap()
|
||||||
|
.with_year(1970)
|
||||||
|
.unwrap(),
|
||||||
|
size: 12,
|
||||||
|
});
|
||||||
|
|
||||||
|
let app = App { core, table_model };
|
||||||
|
|
||||||
|
let command = Task::none();
|
||||||
|
|
||||||
|
(app, command)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle application events here.
|
||||||
|
fn update(&mut self, message: Self::Message) -> Task<Self::Message> {
|
||||||
|
match message {
|
||||||
|
Message::ItemSelect(entity) => self.table_model.activate(entity),
|
||||||
|
Message::CategorySelect(category) => {
|
||||||
|
let mut ascending = true;
|
||||||
|
if let Some(old_sort) = self.table_model.get_sort() {
|
||||||
|
if old_sort.0 == category {
|
||||||
|
ascending = !old_sort.1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.table_model.sort(category, ascending)
|
||||||
|
}
|
||||||
|
Message::PrintMsg(string) => tracing_log::log::info!("{}", string),
|
||||||
|
Message::NoOp => {}
|
||||||
|
}
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a view after each update.
|
||||||
|
fn view(&self) -> Element<Self::Message> {
|
||||||
|
cosmic::widget::responsive(|size| {
|
||||||
|
if size.width < 600.0 {
|
||||||
|
widget::compact_table(&self.table_model)
|
||||||
|
.on_item_left_click(Message::ItemSelect)
|
||||||
|
.item_context(|item| {
|
||||||
|
Some(widget::menu::items(
|
||||||
|
&HashMap::new(),
|
||||||
|
vec![widget::menu::Item::Button(
|
||||||
|
format!("Action on {}", item.name),
|
||||||
|
None,
|
||||||
|
Action::None,
|
||||||
|
)],
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.apply(Element::from)
|
||||||
|
} else {
|
||||||
|
widget::table(&self.table_model)
|
||||||
|
.on_item_left_click(Message::ItemSelect)
|
||||||
|
.on_category_left_click(Message::CategorySelect)
|
||||||
|
.item_context(|item| {
|
||||||
|
Some(widget::menu::items(
|
||||||
|
&HashMap::new(),
|
||||||
|
vec![widget::menu::Item::Button(
|
||||||
|
format!("Action on {}", item.name),
|
||||||
|
None,
|
||||||
|
Action::None,
|
||||||
|
)],
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.category_context(|category| {
|
||||||
|
Some(widget::menu::items(
|
||||||
|
&HashMap::new(),
|
||||||
|
vec![
|
||||||
|
widget::menu::Item::Button(
|
||||||
|
format!("Action on {} category", category.to_string()),
|
||||||
|
None,
|
||||||
|
Action::None,
|
||||||
|
),
|
||||||
|
widget::menu::Item::Button(
|
||||||
|
format!("Other action on {} category", category.to_string()),
|
||||||
|
None,
|
||||||
|
Action::None,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.apply(Element::from)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||||
|
enum Action {
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl widget::menu::Action for Action {
|
||||||
|
type Message = Message;
|
||||||
|
|
||||||
|
fn message(&self) -> Self::Message {
|
||||||
|
Message::NoOp
|
||||||
|
}
|
||||||
|
}
|
||||||
2
justfile
2
justfile
|
|
@ -1,4 +1,4 @@
|
||||||
examples := 'applet application calendar config context-menu cosmic image-button menu multi-window nav-context open-dialog'
|
examples := 'applet application calendar config context-menu cosmic image-button menu multi-window nav-context open-dialog table-view'
|
||||||
clippy_args := '-W clippy::all -W clippy::pedantic'
|
clippy_args := '-W clippy::all -W clippy::pedantic'
|
||||||
|
|
||||||
# Check for errors and linter warnings
|
# Check for errors and linter warnings
|
||||||
|
|
|
||||||
|
|
@ -323,6 +323,10 @@ pub use spin_button::{SpinButton, spin_button, vertical as vertical_spin_button}
|
||||||
|
|
||||||
pub mod tab_bar;
|
pub mod tab_bar;
|
||||||
|
|
||||||
|
pub mod table;
|
||||||
|
#[doc(inline)]
|
||||||
|
pub use table::{compact_table, table};
|
||||||
|
|
||||||
pub mod text;
|
pub mod text;
|
||||||
#[doc(inline)]
|
#[doc(inline)]
|
||||||
pub use text::{Text, text};
|
pub use text::{Text, text};
|
||||||
|
|
|
||||||
47
src/widget/table/mod.rs
Normal file
47
src/widget/table/mod.rs
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
//! A widget allowing the user to display tables of information with optional sorting by category
|
||||||
|
//!
|
||||||
|
|
||||||
|
pub mod model;
|
||||||
|
pub use model::{
|
||||||
|
category::ItemCategory,
|
||||||
|
category::ItemInterface,
|
||||||
|
selection::{MultiSelect, SingleSelect},
|
||||||
|
Entity, Model,
|
||||||
|
};
|
||||||
|
pub mod widget;
|
||||||
|
pub use widget::compact::CompactTableView;
|
||||||
|
pub use widget::standard::TableView;
|
||||||
|
|
||||||
|
pub type SingleSelectTableView<'a, Item, Category, Message> =
|
||||||
|
TableView<'a, SingleSelect, Item, Category, Message>;
|
||||||
|
pub type SingleSelectModel<Item, Category> = Model<SingleSelect, Item, Category>;
|
||||||
|
|
||||||
|
pub type MultiSelectTableView<'a, Item, Category, Message> =
|
||||||
|
TableView<'a, MultiSelect, Item, Category, Message>;
|
||||||
|
pub type MultiSelectModel<Item, Category> = Model<MultiSelect, Item, Category>;
|
||||||
|
|
||||||
|
pub fn table<'a, SelectionMode, Item, Category, Message>(
|
||||||
|
model: &'a Model<SelectionMode, Item, Category>,
|
||||||
|
) -> TableView<'a, SelectionMode, Item, Category, Message>
|
||||||
|
where
|
||||||
|
Message: Clone,
|
||||||
|
SelectionMode: Default,
|
||||||
|
Category: ItemCategory,
|
||||||
|
Item: ItemInterface<Category>,
|
||||||
|
Model<SelectionMode, Item, Category>: model::selection::Selectable,
|
||||||
|
{
|
||||||
|
TableView::new(model)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn compact_table<'a, SelectionMode, Item, Category, Message>(
|
||||||
|
model: &'a Model<SelectionMode, Item, Category>,
|
||||||
|
) -> CompactTableView<'a, SelectionMode, Item, Category, Message>
|
||||||
|
where
|
||||||
|
Message: Clone,
|
||||||
|
SelectionMode: Default,
|
||||||
|
Category: ItemCategory,
|
||||||
|
Item: ItemInterface<Category>,
|
||||||
|
Model<SelectionMode, Item, Category>: model::selection::Selectable,
|
||||||
|
{
|
||||||
|
CompactTableView::new(model)
|
||||||
|
}
|
||||||
19
src/widget/table/model/category.rs
Normal file
19
src/widget/table/model/category.rs
Normal 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;
|
||||||
|
}
|
||||||
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
|
||||||
|
}
|
||||||
|
}
|
||||||
365
src/widget/table/model/mod.rs
Normal file
365
src/widget/table/model/mod.rs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
257
src/widget/table/widget/compact.rs
Normal file
257
src/widget/table/widget/compact.rs
Normal file
|
|
@ -0,0 +1,257 @@
|
||||||
|
use derive_setters::Setters;
|
||||||
|
|
||||||
|
use crate::widget::table::model::{
|
||||||
|
category::{ItemCategory, ItemInterface},
|
||||||
|
selection::Selectable,
|
||||||
|
Entity, Model,
|
||||||
|
};
|
||||||
|
use crate::{
|
||||||
|
theme,
|
||||||
|
widget::{self, container, menu},
|
||||||
|
Apply, Element,
|
||||||
|
};
|
||||||
|
use iced::{Alignment, Border, Padding};
|
||||||
|
|
||||||
|
#[derive(Setters)]
|
||||||
|
#[must_use]
|
||||||
|
pub struct CompactTableView<'a, SelectionMode, Item, Category, Message>
|
||||||
|
where
|
||||||
|
Category: ItemCategory,
|
||||||
|
Item: ItemInterface<Category>,
|
||||||
|
Model<SelectionMode, Item, Category>: Selectable,
|
||||||
|
SelectionMode: Default,
|
||||||
|
Message: Clone + 'static,
|
||||||
|
{
|
||||||
|
pub(super) model: &'a Model<SelectionMode, Item, Category>,
|
||||||
|
|
||||||
|
#[setters(into)]
|
||||||
|
pub(super) element_padding: Padding,
|
||||||
|
|
||||||
|
#[setters(into)]
|
||||||
|
pub(super) item_padding: Padding,
|
||||||
|
pub(super) item_spacing: u16,
|
||||||
|
pub(super) icon_size: u16,
|
||||||
|
|
||||||
|
#[setters(into)]
|
||||||
|
pub(super) divider_padding: Padding,
|
||||||
|
|
||||||
|
// === Item Interaction ===
|
||||||
|
#[setters(skip)]
|
||||||
|
pub(super) on_item_mb_left: Option<Box<dyn Fn(Entity) -> Message + 'static>>,
|
||||||
|
#[setters(skip)]
|
||||||
|
pub(super) on_item_mb_double: Option<Box<dyn Fn(Entity) -> Message + 'static>>,
|
||||||
|
#[setters(skip)]
|
||||||
|
pub(super) on_item_mb_mid: Option<Box<dyn Fn(Entity) -> Message + 'static>>,
|
||||||
|
#[setters(skip)]
|
||||||
|
pub(super) on_item_mb_right: Option<Box<dyn Fn(Entity) -> Message + 'static>>,
|
||||||
|
#[setters(skip)]
|
||||||
|
pub(super) item_context_builder: Box<dyn Fn(&Item) -> Option<Vec<menu::Tree<'a, Message>>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, SelectionMode, Item, Category, Message>
|
||||||
|
From<CompactTableView<'a, SelectionMode, Item, Category, Message>> for Element<'a, Message>
|
||||||
|
where
|
||||||
|
Category: ItemCategory,
|
||||||
|
Item: ItemInterface<Category>,
|
||||||
|
Model<SelectionMode, Item, Category>: Selectable,
|
||||||
|
SelectionMode: Default,
|
||||||
|
Message: Clone + 'static,
|
||||||
|
{
|
||||||
|
fn from(val: CompactTableView<'a, SelectionMode, Item, Category, Message>) -> Self {
|
||||||
|
let cosmic_theme::Spacing { space_xxxs, .. } = theme::active().cosmic().spacing;
|
||||||
|
val.model
|
||||||
|
.iter()
|
||||||
|
.map(|entity| {
|
||||||
|
let item = val.model.item(entity).unwrap();
|
||||||
|
let selected = val.model.is_active(entity);
|
||||||
|
let context_menu = (val.item_context_builder)(&item);
|
||||||
|
|
||||||
|
widget::column()
|
||||||
|
.spacing(val.item_spacing)
|
||||||
|
.push(
|
||||||
|
widget::divider::horizontal::default()
|
||||||
|
.apply(container)
|
||||||
|
.padding(val.divider_padding),
|
||||||
|
)
|
||||||
|
.push(
|
||||||
|
widget::row()
|
||||||
|
.spacing(space_xxxs)
|
||||||
|
.align_y(Alignment::Center)
|
||||||
|
.push_maybe(
|
||||||
|
item.get_icon(Category::default())
|
||||||
|
.map(|icon| icon.size(val.icon_size)),
|
||||||
|
)
|
||||||
|
.push(
|
||||||
|
widget::column()
|
||||||
|
.push(widget::text::body(item.get_text(Category::default())))
|
||||||
|
.push({
|
||||||
|
let mut elements = val
|
||||||
|
.model
|
||||||
|
.categories
|
||||||
|
.iter()
|
||||||
|
.skip_while(|cat| **cat != Category::default())
|
||||||
|
.map(|category| {
|
||||||
|
vec![
|
||||||
|
widget::text::caption(item.get_text(*category))
|
||||||
|
.apply(Element::from),
|
||||||
|
widget::text::caption("-").apply(Element::from),
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.flatten()
|
||||||
|
.collect::<Vec<Element<'a, Message>>>();
|
||||||
|
elements.pop();
|
||||||
|
elements
|
||||||
|
.apply(widget::row::with_children)
|
||||||
|
.spacing(space_xxxs)
|
||||||
|
.wrap()
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.apply(container)
|
||||||
|
.padding(val.item_padding)
|
||||||
|
.width(iced::Length::Fill)
|
||||||
|
.class(theme::Container::custom(move |theme| {
|
||||||
|
widget::container::Style {
|
||||||
|
icon_color: if selected {
|
||||||
|
Some(theme.cosmic().on_accent_color().into())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
text_color: if selected {
|
||||||
|
Some(theme.cosmic().on_accent_color().into())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
background: if selected {
|
||||||
|
Some(iced::Background::Color(
|
||||||
|
theme.cosmic().accent_color().into(),
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
border: Border {
|
||||||
|
radius: theme.cosmic().radius_xs().into(),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
shadow: Default::default(),
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.apply(widget::mouse_area)
|
||||||
|
// Left click
|
||||||
|
.apply(|mouse_area| {
|
||||||
|
if let Some(ref on_item_mb) = val.on_item_mb_left {
|
||||||
|
mouse_area.on_press((on_item_mb)(entity))
|
||||||
|
} else {
|
||||||
|
mouse_area
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// Double click
|
||||||
|
.apply(|mouse_area| {
|
||||||
|
if let Some(ref on_item_mb) = val.on_item_mb_left {
|
||||||
|
mouse_area.on_double_click((on_item_mb)(entity))
|
||||||
|
} else {
|
||||||
|
mouse_area
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// Middle click
|
||||||
|
.apply(|mouse_area| {
|
||||||
|
if let Some(ref on_item_mb) = val.on_item_mb_mid {
|
||||||
|
mouse_area.on_middle_press((on_item_mb)(entity))
|
||||||
|
} else {
|
||||||
|
mouse_area
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// Right click
|
||||||
|
.apply(|mouse_area| {
|
||||||
|
if let Some(ref on_item_mb) = val.on_item_mb_right {
|
||||||
|
mouse_area.on_right_press((on_item_mb)(entity))
|
||||||
|
} else {
|
||||||
|
mouse_area
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.apply(|ma| widget::context_menu(ma, context_menu)),
|
||||||
|
)
|
||||||
|
.apply(Element::from)
|
||||||
|
})
|
||||||
|
.collect::<Vec<Element<'a, Message>>>()
|
||||||
|
.apply(widget::column::with_children)
|
||||||
|
.spacing(val.item_spacing)
|
||||||
|
.padding(val.element_padding)
|
||||||
|
.apply(Element::from)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, SelectionMode, Item, Category, Message>
|
||||||
|
CompactTableView<'a, SelectionMode, Item, Category, Message>
|
||||||
|
where
|
||||||
|
SelectionMode: Default,
|
||||||
|
Model<SelectionMode, Item, Category>: Selectable,
|
||||||
|
Category: ItemCategory,
|
||||||
|
Item: ItemInterface<Category>,
|
||||||
|
Message: Clone + 'static,
|
||||||
|
{
|
||||||
|
pub fn new(model: &'a Model<SelectionMode, Item, Category>) -> Self {
|
||||||
|
let cosmic_theme::Spacing {
|
||||||
|
space_xxxs,
|
||||||
|
space_xxs,
|
||||||
|
..
|
||||||
|
} = theme::active().cosmic().spacing;
|
||||||
|
|
||||||
|
Self {
|
||||||
|
model,
|
||||||
|
element_padding: Padding::from(0),
|
||||||
|
|
||||||
|
divider_padding: Padding::from(0).left(space_xxxs).right(space_xxxs),
|
||||||
|
|
||||||
|
item_padding: Padding::from(space_xxs).into(),
|
||||||
|
item_spacing: 0,
|
||||||
|
icon_size: 48,
|
||||||
|
|
||||||
|
on_item_mb_left: None,
|
||||||
|
on_item_mb_double: None,
|
||||||
|
on_item_mb_mid: None,
|
||||||
|
on_item_mb_right: None,
|
||||||
|
item_context_builder: Box::new(|_| None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_item_left_click<F>(mut self, on_click: F) -> Self
|
||||||
|
where
|
||||||
|
F: Fn(Entity) -> Message + 'static,
|
||||||
|
{
|
||||||
|
self.on_item_mb_left = Some(Box::new(on_click));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_item_double_click<F>(mut self, on_click: F) -> Self
|
||||||
|
where
|
||||||
|
F: Fn(Entity) -> Message + 'static,
|
||||||
|
{
|
||||||
|
self.on_item_mb_double = Some(Box::new(on_click));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_item_middle_click<F>(mut self, on_click: F) -> Self
|
||||||
|
where
|
||||||
|
F: Fn(Entity) -> Message + 'static,
|
||||||
|
{
|
||||||
|
self.on_item_mb_mid = Some(Box::new(on_click));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_item_right_click<F>(mut self, on_click: F) -> Self
|
||||||
|
where
|
||||||
|
F: Fn(Entity) -> Message + 'static,
|
||||||
|
{
|
||||||
|
self.on_item_mb_right = Some(Box::new(on_click));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn item_context<F>(mut self, context_menu_builder: F) -> Self
|
||||||
|
where
|
||||||
|
F: Fn(&Item) -> Option<Vec<menu::Tree<'a, Message>>> + 'static,
|
||||||
|
Message: 'static,
|
||||||
|
{
|
||||||
|
self.item_context_builder = Box::new(context_menu_builder);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
2
src/widget/table/widget/mod.rs
Normal file
2
src/widget/table/widget/mod.rs
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
pub mod compact;
|
||||||
|
pub mod standard;
|
||||||
375
src/widget/table/widget/standard.rs
Normal file
375
src/widget/table/widget/standard.rs
Normal file
|
|
@ -0,0 +1,375 @@
|
||||||
|
use derive_setters::Setters;
|
||||||
|
|
||||||
|
use crate::widget::table::model::{
|
||||||
|
category::{ItemCategory, ItemInterface},
|
||||||
|
selection::Selectable,
|
||||||
|
Entity, Model,
|
||||||
|
};
|
||||||
|
use crate::{
|
||||||
|
theme,
|
||||||
|
widget::{self, container, divider, menu},
|
||||||
|
Apply, Element,
|
||||||
|
};
|
||||||
|
use iced::{Alignment, Border, Length, Padding};
|
||||||
|
|
||||||
|
// THIS IS A PLACEHOLDER UNTIL A MORE SOPHISTICATED WIDGET CAN BE DEVELOPED
|
||||||
|
|
||||||
|
#[derive(Setters)]
|
||||||
|
#[must_use]
|
||||||
|
pub struct TableView<'a, SelectionMode, Item, Category, Message>
|
||||||
|
where
|
||||||
|
Category: ItemCategory,
|
||||||
|
Item: ItemInterface<Category>,
|
||||||
|
Model<SelectionMode, Item, Category>: Selectable,
|
||||||
|
SelectionMode: Default,
|
||||||
|
Message: Clone + 'static,
|
||||||
|
{
|
||||||
|
pub(super) model: &'a Model<SelectionMode, Item, Category>,
|
||||||
|
|
||||||
|
#[setters(into)]
|
||||||
|
pub(super) element_padding: Padding,
|
||||||
|
#[setters(into)]
|
||||||
|
pub(super) width: Length,
|
||||||
|
#[setters(into)]
|
||||||
|
pub(super) height: Length,
|
||||||
|
|
||||||
|
#[setters(into)]
|
||||||
|
pub(super) item_padding: Padding,
|
||||||
|
pub(super) item_spacing: u16,
|
||||||
|
pub(super) icon_spacing: u16,
|
||||||
|
pub(super) icon_size: u16,
|
||||||
|
|
||||||
|
#[setters(into)]
|
||||||
|
pub(super) divider_padding: Padding,
|
||||||
|
|
||||||
|
// === Item Interaction ===
|
||||||
|
#[setters(skip)]
|
||||||
|
pub(super) on_item_mb_left: Option<Box<dyn Fn(Entity) -> Message + 'static>>,
|
||||||
|
#[setters(skip)]
|
||||||
|
pub(super) on_item_mb_double: Option<Box<dyn Fn(Entity) -> Message + 'static>>,
|
||||||
|
#[setters(skip)]
|
||||||
|
pub(super) on_item_mb_mid: Option<Box<dyn Fn(Entity) -> Message + 'static>>,
|
||||||
|
#[setters(skip)]
|
||||||
|
pub(super) on_item_mb_right: Option<Box<dyn Fn(Entity) -> Message + 'static>>,
|
||||||
|
#[setters(skip)]
|
||||||
|
pub(super) item_context_builder: Box<dyn Fn(&Item) -> Option<Vec<menu::Tree<'a, Message>>>>,
|
||||||
|
// Item DND
|
||||||
|
|
||||||
|
// === Category Interaction ===
|
||||||
|
#[setters(skip)]
|
||||||
|
pub(super) on_category_mb_left: Option<Box<dyn Fn(Category) -> Message + 'static>>,
|
||||||
|
#[setters(skip)]
|
||||||
|
pub(super) on_category_mb_double: Option<Box<dyn Fn(Category) -> Message + 'static>>,
|
||||||
|
#[setters(skip)]
|
||||||
|
pub(super) on_category_mb_mid: Option<Box<dyn Fn(Category) -> Message + 'static>>,
|
||||||
|
#[setters(skip)]
|
||||||
|
pub(super) on_category_mb_right: Option<Box<dyn Fn(Category) -> Message + 'static>>,
|
||||||
|
#[setters(skip)]
|
||||||
|
pub(super) category_context_builder:
|
||||||
|
Box<dyn Fn(Category) -> Option<Vec<menu::Tree<'a, Message>>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, SelectionMode, Item, Category, Message>
|
||||||
|
From<TableView<'a, SelectionMode, Item, Category, Message>> for Element<'a, Message>
|
||||||
|
where
|
||||||
|
Category: ItemCategory,
|
||||||
|
Item: ItemInterface<Category>,
|
||||||
|
Model<SelectionMode, Item, Category>: Selectable,
|
||||||
|
SelectionMode: Default,
|
||||||
|
Message: Clone + 'static,
|
||||||
|
{
|
||||||
|
fn from(val: TableView<'a, SelectionMode, Item, Category, Message>) -> Self {
|
||||||
|
// Header row
|
||||||
|
let header_row = val
|
||||||
|
.model
|
||||||
|
.categories
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.map(|category| {
|
||||||
|
let cat_context_tree = (val.category_context_builder)(category);
|
||||||
|
|
||||||
|
let mut sort_state = 0;
|
||||||
|
|
||||||
|
if let Some(sort) = val.model.sort {
|
||||||
|
if sort.0 == category {
|
||||||
|
if sort.1 {
|
||||||
|
sort_state = 1;
|
||||||
|
} else {
|
||||||
|
sort_state = 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Build the category header
|
||||||
|
widget::row()
|
||||||
|
.spacing(val.icon_spacing)
|
||||||
|
.push(widget::text::heading(category.to_string()))
|
||||||
|
.push_maybe(match sort_state {
|
||||||
|
1 => Some(widget::icon::from_name("pan-up-symbolic").icon()),
|
||||||
|
2 => Some(widget::icon::from_name("pan-down-symbolic").icon()),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.apply(container)
|
||||||
|
.padding(
|
||||||
|
Padding::default()
|
||||||
|
.left(val.item_padding.left)
|
||||||
|
.right(val.item_padding.right),
|
||||||
|
)
|
||||||
|
.width(category.width())
|
||||||
|
.apply(widget::mouse_area)
|
||||||
|
.apply(|mouse_area| {
|
||||||
|
if let Some(ref on_category_select) = val.on_category_mb_left {
|
||||||
|
mouse_area.on_press((on_category_select)(category))
|
||||||
|
} else {
|
||||||
|
mouse_area
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.apply(|mouse_area| widget::context_menu(mouse_area, cat_context_tree))
|
||||||
|
.apply(Element::from)
|
||||||
|
})
|
||||||
|
.collect::<Vec<Element<'a, Message>>>()
|
||||||
|
.apply(widget::row::with_children)
|
||||||
|
.apply(Element::from);
|
||||||
|
// Build the items
|
||||||
|
let items_full = if val.model.items.is_empty() {
|
||||||
|
vec![divider::horizontal::default()
|
||||||
|
.apply(container)
|
||||||
|
.padding(val.divider_padding)
|
||||||
|
.apply(Element::from)]
|
||||||
|
} else {
|
||||||
|
val.model
|
||||||
|
.iter()
|
||||||
|
.map(move |entity| {
|
||||||
|
let item = val.model.item(entity).unwrap();
|
||||||
|
let categories = &val.model.categories;
|
||||||
|
let selected = val.model.is_active(entity);
|
||||||
|
let item_context = (val.item_context_builder)(&item);
|
||||||
|
|
||||||
|
vec![
|
||||||
|
divider::horizontal::default()
|
||||||
|
.apply(container)
|
||||||
|
.padding(val.divider_padding)
|
||||||
|
.apply(Element::from),
|
||||||
|
categories
|
||||||
|
.iter()
|
||||||
|
.map(|category| {
|
||||||
|
widget::row()
|
||||||
|
.spacing(val.icon_spacing)
|
||||||
|
.push_maybe(
|
||||||
|
item.get_icon(*category)
|
||||||
|
.map(|icon| icon.size(val.icon_size)),
|
||||||
|
)
|
||||||
|
.push(widget::text::body(item.get_text(*category)))
|
||||||
|
.align_y(Alignment::Center)
|
||||||
|
.apply(container)
|
||||||
|
.width(category.width())
|
||||||
|
.align_y(Alignment::Center)
|
||||||
|
.apply(Element::from)
|
||||||
|
})
|
||||||
|
.collect::<Vec<Element<'a, Message>>>()
|
||||||
|
.apply(widget::row::with_children)
|
||||||
|
.apply(container)
|
||||||
|
.padding(val.item_padding)
|
||||||
|
.class(theme::Container::custom(move |theme| {
|
||||||
|
widget::container::Style {
|
||||||
|
icon_color: if selected {
|
||||||
|
Some(theme.cosmic().on_accent_color().into())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
text_color: if selected {
|
||||||
|
Some(theme.cosmic().on_accent_color().into())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
background: if selected {
|
||||||
|
Some(iced::Background::Color(
|
||||||
|
theme.cosmic().accent_color().into(),
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
border: Border {
|
||||||
|
radius: theme.cosmic().radius_xs().into(),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
shadow: Default::default(),
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.apply(widget::mouse_area)
|
||||||
|
// Left click
|
||||||
|
.apply(|mouse_area| {
|
||||||
|
if let Some(ref on_item_mb) = val.on_item_mb_left {
|
||||||
|
mouse_area.on_press((on_item_mb)(entity))
|
||||||
|
} else {
|
||||||
|
mouse_area
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// Double click
|
||||||
|
.apply(|mouse_area| {
|
||||||
|
if let Some(ref on_item_mb) = val.on_item_mb_left {
|
||||||
|
mouse_area.on_double_click((on_item_mb)(entity))
|
||||||
|
} else {
|
||||||
|
mouse_area
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// Middle click
|
||||||
|
.apply(|mouse_area| {
|
||||||
|
if let Some(ref on_item_mb) = val.on_item_mb_mid {
|
||||||
|
mouse_area.on_middle_press((on_item_mb)(entity))
|
||||||
|
} else {
|
||||||
|
mouse_area
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// Right click
|
||||||
|
.apply(|mouse_area| {
|
||||||
|
if let Some(ref on_item_mb) = val.on_item_mb_right {
|
||||||
|
mouse_area.on_right_press((on_item_mb)(entity))
|
||||||
|
} else {
|
||||||
|
mouse_area
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.apply(|mouse_area| widget::context_menu(mouse_area, item_context))
|
||||||
|
.apply(Element::from),
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.flatten()
|
||||||
|
.collect::<Vec<Element<'a, Message>>>()
|
||||||
|
};
|
||||||
|
vec![vec![header_row], items_full]
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.collect::<Vec<Element<'a, Message>>>()
|
||||||
|
.apply(widget::column::with_children)
|
||||||
|
.width(val.width)
|
||||||
|
.height(val.height)
|
||||||
|
.spacing(val.item_spacing)
|
||||||
|
.padding(val.element_padding)
|
||||||
|
.apply(Element::from)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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>,
|
||||||
|
Message: Clone + 'static,
|
||||||
|
{
|
||||||
|
pub fn new(model: &'a Model<SelectionMode, Item, Category>) -> Self {
|
||||||
|
let cosmic_theme::Spacing {
|
||||||
|
space_xxxs,
|
||||||
|
space_xxs,
|
||||||
|
..
|
||||||
|
} = theme::active().cosmic().spacing;
|
||||||
|
|
||||||
|
Self {
|
||||||
|
model,
|
||||||
|
|
||||||
|
element_padding: Padding::from(0),
|
||||||
|
width: Length::Fill,
|
||||||
|
height: Length::Shrink,
|
||||||
|
|
||||||
|
item_padding: Padding::from(space_xxs).into(),
|
||||||
|
item_spacing: 0,
|
||||||
|
icon_spacing: space_xxxs,
|
||||||
|
icon_size: 24,
|
||||||
|
|
||||||
|
divider_padding: Padding::from(0).left(space_xxxs).right(space_xxxs),
|
||||||
|
|
||||||
|
on_item_mb_left: None,
|
||||||
|
on_item_mb_double: None,
|
||||||
|
on_item_mb_mid: None,
|
||||||
|
on_item_mb_right: None,
|
||||||
|
item_context_builder: Box::new(|_| None),
|
||||||
|
|
||||||
|
on_category_mb_left: None,
|
||||||
|
on_category_mb_double: None,
|
||||||
|
on_category_mb_mid: None,
|
||||||
|
on_category_mb_right: None,
|
||||||
|
category_context_builder: Box::new(|_| None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_item_left_click<F>(mut self, on_click: F) -> Self
|
||||||
|
where
|
||||||
|
F: Fn(Entity) -> Message + 'static,
|
||||||
|
{
|
||||||
|
self.on_item_mb_left = Some(Box::new(on_click));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_item_double_click<F>(mut self, on_click: F) -> Self
|
||||||
|
where
|
||||||
|
F: Fn(Entity) -> Message + 'static,
|
||||||
|
{
|
||||||
|
self.on_item_mb_double = Some(Box::new(on_click));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_item_middle_click<F>(mut self, on_click: F) -> Self
|
||||||
|
where
|
||||||
|
F: Fn(Entity) -> Message + 'static,
|
||||||
|
{
|
||||||
|
self.on_item_mb_mid = Some(Box::new(on_click));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_item_right_click<F>(mut self, on_click: F) -> Self
|
||||||
|
where
|
||||||
|
F: Fn(Entity) -> Message + 'static,
|
||||||
|
{
|
||||||
|
self.on_item_mb_right = Some(Box::new(on_click));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn item_context<F>(mut self, context_menu_builder: F) -> Self
|
||||||
|
where
|
||||||
|
F: Fn(&Item) -> Option<Vec<menu::Tree<'a, Message>>> + 'static,
|
||||||
|
Message: 'static,
|
||||||
|
{
|
||||||
|
self.item_context_builder = Box::new(context_menu_builder);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_category_left_click<F>(mut self, on_select: F) -> Self
|
||||||
|
where
|
||||||
|
F: Fn(Category) -> Message + 'static,
|
||||||
|
{
|
||||||
|
self.on_category_mb_left = Some(Box::new(on_select));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
pub fn on_category_double_click<F>(mut self, on_select: F) -> Self
|
||||||
|
where
|
||||||
|
F: Fn(Category) -> Message + 'static,
|
||||||
|
{
|
||||||
|
self.on_category_mb_double = Some(Box::new(on_select));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
pub fn on_category_middle_click<F>(mut self, on_select: F) -> Self
|
||||||
|
where
|
||||||
|
F: Fn(Category) -> Message + 'static,
|
||||||
|
{
|
||||||
|
self.on_category_mb_mid = Some(Box::new(on_select));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_category_right_click<F>(mut self, on_select: F) -> Self
|
||||||
|
where
|
||||||
|
F: Fn(Category) -> Message + 'static,
|
||||||
|
{
|
||||||
|
self.on_category_mb_right = Some(Box::new(on_select));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn category_context<F>(mut self, context_menu_builder: F) -> Self
|
||||||
|
where
|
||||||
|
F: Fn(Category) -> Option<Vec<menu::Tree<'a, Message>>> + 'static,
|
||||||
|
Message: 'static,
|
||||||
|
{
|
||||||
|
self.category_context_builder = Box::new(context_menu_builder);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue