Added item contexts and changed the context menu functions to take builder functions

This commit is contained in:
Adam Cosner 2025-01-10 22:44:42 -08:00
parent 0b0e143fe4
commit ea4e6c6cde
3 changed files with 139 additions and 97 deletions

View file

@ -3,6 +3,8 @@
//! Table API example //! Table API example
use std::collections::HashMap;
use chrono::Datelike; use chrono::Datelike;
use cosmic::app::{Core, Settings, Task}; use cosmic::app::{Core, Settings, Task};
use cosmic::iced_core::Size; use cosmic::iced_core::Size;
@ -100,6 +102,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
pub enum Message { pub enum Message {
ItemSelect(table::Entity), ItemSelect(table::Entity),
CategorySelect(Category, bool), CategorySelect(Category, bool),
NoOp,
} }
/// The [`App`] stores application-specific state. /// The [`App`] stores application-specific state.
@ -187,6 +190,7 @@ impl cosmic::Application for App {
Message::CategorySelect(category, descending) => { Message::CategorySelect(category, descending) => {
self.table_model.sort(category, descending) self.table_model.sort(category, descending)
} }
Message::NoOp => {}
} }
Task::none() Task::none()
} }
@ -197,15 +201,96 @@ impl cosmic::Application for App {
if size.width < 600.0 { if size.width < 600.0 {
widget::compact_table(&self.table_model) widget::compact_table(&self.table_model)
.on_item_select(Message::ItemSelect) .on_item_select(Message::ItemSelect)
.on_category_select(Message::CategorySelect) .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) .apply(Element::from)
} else { } else {
widget::table(&self.table_model) widget::table(&self.table_model)
.on_item_select(Message::ItemSelect) .on_item_select(Message::ItemSelect)
.on_category_select(Message::CategorySelect) .on_category_select(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(match category {
Category::Name => widget::menu::items(
&HashMap::new(),
vec![
widget::menu::Item::Button(
"Action on Name Category",
None,
Action::None,
),
widget::menu::Item::Button(
"Other action on Name",
None,
Action::None,
),
],
),
Category::Date => widget::menu::items(
&HashMap::new(),
vec![
widget::menu::Item::Button(
"Action on Date Category",
None,
Action::None,
),
widget::menu::Item::Button(
"Other action on Date",
None,
Action::None,
),
],
),
Category::Size => widget::menu::items(
&HashMap::new(),
vec![
widget::menu::Item::Button(
"Action on Size Category",
None,
Action::None,
),
widget::menu::Item::Button(
"Other action on Size",
None,
Action::None,
),
],
),
})
})
.apply(Element::from) .apply(Element::from)
} }
}) })
.into() .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
}
}

View file

@ -30,23 +30,18 @@ where
#[setters(into)] #[setters(into)]
pub(super) item_padding: Padding, pub(super) item_padding: Padding,
pub(super) item_spacing: u16, pub(super) item_spacing: u16,
pub(super) icon_size: u16,
#[setters(into)] #[setters(into)]
pub(super) divider_padding: Padding, pub(super) divider_padding: Padding,
#[setters(skip)] #[setters(skip)]
pub(super) item_context_tree: Option<Vec<menu::Tree<'a, Message>>>, pub(super) item_context_builder: Box<dyn Fn(&Item) -> Option<Vec<menu::Tree<'a, Message>>>>,
#[setters(skip)]
pub(super) category_context_tree: Option<Vec<menu::Tree<'a, Message>>>,
#[setters(skip)] #[setters(skip)]
pub(super) on_item_select: Option<Box<dyn Fn(Entity) -> Message + 'a>>, pub(super) on_item_select: Option<Box<dyn Fn(Entity) -> Message + 'a>>,
#[setters(skip)] #[setters(skip)]
pub(super) on_item_context: Option<Box<dyn Fn(Entity) -> Message + 'a>>, pub(super) on_item_context: Option<Box<dyn Fn(Entity) -> Message + 'a>>,
#[setters(skip)]
pub(super) on_category_select: Option<Box<dyn Fn(Category, bool) -> Message + 'a>>,
#[setters(skip)]
pub(super) on_category_context: Option<Box<dyn Fn(Category) -> Message + 'a>>,
} }
impl<'a, SelectionMode, Item, Category, Message> impl<'a, SelectionMode, Item, Category, Message>
@ -65,6 +60,8 @@ where
.map(|entity| { .map(|entity| {
let item = val.model.item(entity).unwrap(); let item = val.model.item(entity).unwrap();
let selected = val.model.is_active(entity); let selected = val.model.is_active(entity);
let context_menu = (val.item_context_builder)(&item);
widget::column() widget::column()
.spacing(val.item_spacing) .spacing(val.item_spacing)
.push( .push(
@ -140,7 +137,8 @@ where
} else { } else {
ma ma
} }
}), })
.apply(|ma| widget::context_menu(ma, context_menu)),
) )
.apply(Element::from) .apply(Element::from)
}) })
@ -176,14 +174,11 @@ where
item_padding: Padding::from(space_xxs).into(), item_padding: Padding::from(space_xxs).into(),
item_spacing: 0, item_spacing: 0,
icon_size: 48,
item_context_builder: Box::new(|_| None),
on_item_select: None, on_item_select: None,
on_item_context: None, on_item_context: None,
item_context_tree: None,
on_category_select: None,
on_category_context: None,
category_context_tree: None,
} }
} }
@ -203,47 +198,12 @@ where
self self
} }
pub fn item_context(mut self, context_menu: Option<Vec<menu::Tree<'a, Message>>>) -> Self pub fn item_context<F>(mut self, context_menu_builder: F) -> Self
where where
F: Fn(&Item) -> Option<Vec<menu::Tree<'a, Message>>> + 'static,
Message: 'static, Message: 'static,
{ {
self.item_context_tree = self.item_context_builder = Box::new(context_menu_builder);
context_menu.map(|menus| vec![menu::Tree::with_children(widget::row(), menus)]);
if let Some(ref mut context_menu) = self.item_context_tree {
context_menu.iter_mut().for_each(menu::Tree::set_index);
}
self
}
pub fn category_context(mut self, context_menu: Option<Vec<menu::Tree<'a, Message>>>) -> Self
where
Message: 'static,
{
self.category_context_tree =
context_menu.map(|menus| vec![menu::Tree::with_children(widget::row(), menus)]);
if let Some(ref mut context_menu) = self.category_context_tree {
context_menu.iter_mut().for_each(menu::Tree::set_index);
}
self
}
pub fn on_category_select<F>(mut self, on_select: F) -> Self
where
F: Fn(Category, bool) -> Message + 'a,
{
self.on_category_select = Some(Box::new(on_select));
self
}
pub fn on_category_context<F>(mut self, on_select: F) -> Self
where
F: Fn(Category) -> Message + 'a,
{
self.on_category_context = Some(Box::new(on_select));
self self
} }
} }

View file

@ -33,15 +33,15 @@ where
#[setters(into)] #[setters(into)]
pub(super) item_padding: Padding, pub(super) item_padding: Padding,
pub(super) item_spacing: u16, pub(super) item_spacing: u16,
pub(super) icon_size: u16,
#[setters(into)] #[setters(into)]
pub(super) divider_padding: Padding, pub(super) divider_padding: Padding,
#[setters(skip)] #[setters(skip)]
pub(super) item_context_tree: Option<Vec<menu::Tree<'a, Message>>>, pub(super) item_context_builder: Box<dyn Fn(&Item) -> Option<Vec<menu::Tree<'a, Message>>>>,
#[setters(skip)] #[setters(skip)]
pub(super) category_context_trees: pub(super) category_contexts: Box<dyn Fn(Category) -> Option<Vec<menu::Tree<'a, Message>>>>,
std::collections::HashMap<Category, Option<Vec<menu::Tree<'a, Message>>>>,
#[setters(skip)] #[setters(skip)]
pub(super) on_item_select: Option<Box<dyn Fn(Entity) -> Message + 'a>>, pub(super) on_item_select: Option<Box<dyn Fn(Entity) -> Message + 'a>>,
@ -65,29 +65,34 @@ where
fn from(val: TableView<'a, SelectionMode, Item, Category, Message>) -> Self { fn from(val: TableView<'a, SelectionMode, Item, Category, Message>) -> Self {
let cosmic_theme::Spacing { space_xxxs, .. } = theme::active().cosmic().spacing; let cosmic_theme::Spacing { space_xxxs, .. } = theme::active().cosmic().spacing;
let mut category_contexts = val.category_context_trees.into_values();
let header_row = val let header_row = val
.model .model
.categories .categories
.iter() .iter()
.cloned()
.map(|category| { .map(|category| {
let cat_context_tree = category_contexts.next().unwrap(); let cat_context_tree = (val.category_contexts)(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() widget::row()
.spacing(space_xxxs) .spacing(space_xxxs)
.push(widget::text::heading(category.to_string())) .push(widget::text::heading(category.to_string()))
.push_maybe(if let Some(sort) = val.model.sort { .push_maybe(match sort_state {
if sort.0 == *category { 1 => Some(widget::icon::from_name("pan-up-symbolic").icon()),
match sort.1 { 2 => Some(widget::icon::from_name("pan-down-symbolic").icon()),
true => Some(widget::icon::from_name("pan-up-symbolic").icon()), _ => None,
false => Some(widget::icon::from_name("pan-down-symbolic").icon()),
}
} else {
None
}
} else {
None
}) })
.apply(container) .apply(container)
.padding( .padding(
@ -100,9 +105,9 @@ where
.apply(|mouse_area| { .apply(|mouse_area| {
if let Some(ref on_category_select) = val.on_category_select { if let Some(ref on_category_select) = val.on_category_select {
mouse_area.on_press((on_category_select)( mouse_area.on_press((on_category_select)(
*category, category,
if let Some(sort) = val.model.sort { if let Some(sort) = val.model.sort {
if sort.0 == *category { if sort.0 == category {
!sort.1 !sort.1
} else { } else {
false false
@ -121,6 +126,7 @@ where
.collect::<Vec<Element<'a, Message>>>() .collect::<Vec<Element<'a, Message>>>()
.apply(widget::row::with_children) .apply(widget::row::with_children)
.apply(Element::from); .apply(Element::from);
// Build the items
let items_full = if val.model.items.is_empty() { let items_full = if val.model.items.is_empty() {
vec![divider::horizontal::default() vec![divider::horizontal::default()
.apply(container) .apply(container)
@ -129,10 +135,11 @@ where
} else { } else {
val.model val.model
.iter() .iter()
.map(|entity| { .map(move |entity| {
let item = val.model.item(entity).unwrap(); let item = val.model.item(entity).unwrap();
let categories = &val.model.categories; let categories = &val.model.categories;
let selected = val.model.is_active(entity); let selected = val.model.is_active(entity);
let item_context = (val.item_context_builder)(&item);
vec![ vec![
divider::horizontal::default() divider::horizontal::default()
@ -144,7 +151,10 @@ where
.map(|category| { .map(|category| {
widget::row() widget::row()
.spacing(space_xxxs) .spacing(space_xxxs)
.push_maybe(item.get_icon(*category).map(|icon| icon.size(24))) .push_maybe(
item.get_icon(*category)
.map(|icon| icon.size(val.icon_size)),
)
.push(widget::text::body(item.get_text(*category))) .push(widget::text::body(item.get_text(*category)))
.align_y(Alignment::Center) .align_y(Alignment::Center)
.apply(container) .apply(container)
@ -190,6 +200,7 @@ where
mouse_area mouse_area
} }
}) })
.apply(|mouse_area| widget::context_menu(mouse_area, item_context))
.apply(Element::from), .apply(Element::from),
] ]
}) })
@ -223,7 +234,7 @@ where
.. ..
} = theme::active().cosmic().spacing; } = theme::active().cosmic().spacing;
let mut result = Self { Self {
model, model,
element_padding: Padding::from(0), element_padding: Padding::from(0),
@ -231,21 +242,16 @@ where
item_padding: Padding::from(space_xxs).into(), item_padding: Padding::from(space_xxs).into(),
item_spacing: 0, item_spacing: 0,
icon_size: 24,
on_item_select: None, on_item_select: None,
on_item_context: None, on_item_context: None,
item_context_tree: None, item_context_builder: Box::new(|_| None),
on_category_select: None, on_category_select: None,
on_category_context: None, on_category_context: None,
category_context_trees: std::collections::HashMap::new(), category_contexts: Box::new(|_| None),
};
for category in model.categories.iter().cloned() {
result.category_context_trees.insert(category, None);
} }
result
} }
pub fn on_item_select<F>(mut self, on_select: F) -> Self pub fn on_item_select<F>(mut self, on_select: F) -> Self
@ -264,30 +270,21 @@ where
self self
} }
pub fn item_context(mut self, context_menu: Option<Vec<menu::Tree<'a, Message>>>) -> Self pub fn item_context<F>(mut self, context_menu_builder: F) -> Self
where where
F: Fn(&Item) -> Option<Vec<menu::Tree<'a, Message>>> + 'static,
Message: 'static, Message: 'static,
{ {
self.item_context_tree = self.item_context_builder = Box::new(context_menu_builder);
context_menu.map(|menus| vec![menu::Tree::with_children(widget::row(), menus)]);
if let Some(ref mut context_menu) = self.item_context_tree {
context_menu.iter_mut().for_each(menu::Tree::set_index);
}
self self
} }
pub fn category_context(mut self, category: Category, context_menu: Option<Vec<menu::Tree<'a, Message>>>) -> Self pub fn category_context<F>(mut self, context_menu_builder: F) -> Self
where where
F: Fn(Category) -> Option<Vec<menu::Tree<'a, Message>>> + 'static,
Message: 'static, Message: 'static,
{ {
*self.category_context_trees.get_mut(&category).unwrap() = self.category_contexts = Box::new(context_menu_builder);
context_menu.map(|menus| vec![menu::Tree::with_children(widget::row(), menus)]);
if let Some(ref mut context_menu) = self.category_context_trees.get_mut(&category).unwrap() {
context_menu.iter_mut().for_each(menu::Tree::set_index);
}
self self
} }