diff --git a/src/widget/table/model/mod.rs b/src/widget/table/model/mod.rs index 43fe47c3..b6935435 100644 --- a/src/widget/table/model/mod.rs +++ b/src/widget/table/model/mod.rs @@ -43,7 +43,7 @@ where pub(super) selection: SelectionMode, /// What category to sort by and whether it's ascending or not - pub(super) sort: (Category, bool), + pub(super) sort: Option<(Category, bool)>, /// Application-managed data associated with each item pub(super) storage: Storage, @@ -62,7 +62,7 @@ where indents: SecondaryMap::default(), order: VecDeque::new(), selection: SelectionMode::default(), - sort: (Category::default(), false), + sort: None, storage: Storage::default(), } } @@ -342,12 +342,18 @@ where /// 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); + self.sort = Some((category, ascending)); let mut order: Vec = self.order.iter().cloned().collect(); order.sort_by(|entity_a, entity_b| { - self.item(*entity_a) - .unwrap() - .compare(self.item(*entity_b).unwrap(), category) + 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(); } diff --git a/src/widget/table/widget.rs b/src/widget/table/widget.rs deleted file mode 100644 index ef7ad410..00000000 --- a/src/widget/table/widget.rs +++ /dev/null @@ -1,174 +0,0 @@ -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, - Model: Selectable, - SelectionMode: Default, -{ - pub(super) model: &'a Model, - - 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 Message + 'a>, - pub(super) on_category_select: Box 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: Selectable, - Category: ItemCategory, - Item: ItemInterface, -{ - pub fn new( - model: &'a Model, - 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: ::Class<'a>) -> Self { - self.style = style; - self - } - - pub fn padding(mut self, padding: impl Into) -> 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) -> 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() - } -} diff --git a/src/widget/table/widget/mod.rs b/src/widget/table/widget/mod.rs new file mode 100644 index 00000000..02f62e3d --- /dev/null +++ b/src/widget/table/widget/mod.rs @@ -0,0 +1,331 @@ +mod state; +use state::State; + +use super::model::{ + category::{ItemCategory, ItemInterface}, + selection::Selectable, + Entity, Model, +}; +use crate::{ + ext::CollectionWidget, + 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 + +#[must_use] +pub struct TableView<'a, SelectionMode, Item, Category, Message> +where + Category: ItemCategory, + Item: ItemInterface, + Model: Selectable, + SelectionMode: Default, + Message: Clone + 'static, +{ + pub(super) model: &'a Model, + + pub(super) item_spacing: u16, + pub(super) element_padding: Padding, + pub(super) item_padding: Padding, + pub(super) divider_padding: Padding, + pub(super) item_context_tree: Option>>, + pub(super) category_context_tree: Option>>, + + pub(super) on_item_select: Option Message + 'a>>, + pub(super) on_item_context: Option Message + 'a>>, + pub(super) on_category_select: Option Message + 'a>>, + pub(super) on_category_context: Option Message + 'a>>, +} + +impl<'a, SelectionMode, Item, Category, Message> + TableView<'a, SelectionMode, Item, Category, Message> +where + SelectionMode: Default, + Model: Selectable, + Category: ItemCategory, + Item: ItemInterface, + Message: Clone + 'static, +{ + pub fn new(model: &'a Model) -> Self { + let cosmic_theme::Spacing { + space_xxxs, + space_xxs, + .. + } = theme::active().cosmic().spacing; + + Self { + model, + item_spacing: 0, + element_padding: Padding::from(0), + divider_padding: Padding::from(0).left(space_xxxs).right(space_xxxs), + item_padding: Padding::from(space_xxs).into(), + on_item_select: None, + on_item_context: None, + item_context_tree: None, + on_category_select: None, + on_category_context: None, + category_context_tree: None, + } + } + + pub fn item_spacing(mut self, spacing: u16) -> Self { + self.item_spacing = spacing; + self + } + + pub fn element_padding(mut self, padding: impl Into) -> Self { + self.element_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 item_padding(mut self, padding: impl Into) -> Self { + self.item_padding = padding.into(); + self + } + + pub fn on_item_select(mut self, on_select: F) -> Self + where + F: Fn(Entity) -> Message + 'a, + { + self.on_item_select = Some(Box::new(on_select)); + self + } + + pub fn on_item_context(mut self, on_select: F) -> Self + where + F: Fn(Entity) -> Message + 'a, + { + self.on_item_context = Some(Box::new(on_select)); + self + } + + pub fn item_context(mut self, context_menu: Option>>) -> Self + where + Message: 'static, + { + self.item_context_tree = + 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 on_category_select(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(mut self, on_select: F) -> Self + where + F: Fn(Category) -> Message + 'a, + { + self.on_category_context = Some(Box::new(on_select)); + self + } + + #[must_use] + pub fn element_standard(&self) -> Element<'a, Message> { + let cosmic_theme::Spacing { space_xxxs, .. } = theme::active().cosmic().spacing; + + let header_row = self + .model + .categories + .iter() + .map(|category| { + widget::row() + .spacing(space_xxxs) + .push(widget::text::heading(category.to_string())) + .push_maybe(if let Some(sort) = self.model.sort { + if sort.0 == *category { + match sort.1 { + true => Some(widget::icon::from_name("pan-up-symbolic").icon()), + false => Some(widget::icon::from_name("pan-down-symbolic").icon()), + } + } else { + None + } + } else { + None + }) + .apply(container) + .padding( + Padding::default() + .left(self.item_padding.left) + .right(self.item_padding.right), + ) + .width(category.width()) + .apply(widget::mouse_area) + .apply(|mouse_area| { + if let Some(ref on_category_select) = self.on_category_select { + mouse_area.on_press((on_category_select)( + *category, + if let Some(sort) = self.model.sort { + if sort.0 == *category { + !sort.1 + } else { + false + } + } else { + false + }, + )) + } else { + mouse_area + } + }) + .apply(Element::from) + }) + .collect::>>() + .apply(widget::row::with_children) + .apply(Element::from); + let items_full = if self.model.items.is_empty() { + vec![divider::horizontal::default() + .apply(container) + .padding(self.divider_padding) + .apply(Element::from)] + } else { + self.model + .order + .iter() + .map(|entity| { + let item = self.model.item(*entity).unwrap(); + let categories = &self.model.categories; + let selected = self.model.is_active(*entity); + + vec![ + divider::horizontal::default() + .apply(container) + .padding(self.divider_padding) + .apply(Element::from), + categories + .iter() + .map(|category| { + widget::row() + .push_maybe(item.get_icon(*category).map(|icon| icon.size(24))) + .push(widget::text::body(item.get_text(*category))) + .align_y(Alignment::Center) + .apply(container) + .width(category.width()) + .align_y(Alignment::Center) + .padding(self.item_padding) + .apply(Element::from) + }) + .collect::>>() + .apply(widget::row::with_children) + .apply(widget::mouse_area) + .apply(|mouse_area| { + if let Some(ref on_item_select) = self.on_item_select { + mouse_area.on_press((on_item_select)(*entity)) + } else { + mouse_area + } + }) + .apply(container) + .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(Element::from), + ] + }) + .flatten() + .collect::>>() + }; + vec![vec![header_row], items_full] + .into_iter() + .flatten() + .collect::>>() + .apply(widget::column::with_children) + .apply(Element::from) + } + + #[must_use] + pub fn element_compact(&self) -> Element<'a, Message> { + let cosmic_theme::Spacing { space_xxxs, .. } = theme::active().cosmic().spacing; + self.model + .iter() + .map(|entity| { + let item = self.model.item(entity).unwrap(); + widget::column() + .push( + widget::divider::horizontal::default() + .apply(container) + .padding(self.divider_padding), + ) + .push( + widget::row() + .align_y(Alignment::Center) + .push_maybe( + item.get_icon(Category::default()).map(|icon| icon.size(48)), + ) + .push( + widget::column() + .push(widget::text::body(item.get_text(Category::default()))) + .push({ + let mut elements = self + .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::>>(); + elements.pop(); + elements + .apply(widget::row::with_children) + .spacing(space_xxxs) + .wrap() + }), + ) + .apply(container) + .padding(self.item_padding), + ) + .apply(Element::from) + }) + .collect::>>() + .apply(widget::column::with_children) + .apply(Element::from) + } +} diff --git a/src/widget/table/widget/state.rs b/src/widget/table/widget/state.rs new file mode 100644 index 00000000..4d529db4 --- /dev/null +++ b/src/widget/table/widget/state.rs @@ -0,0 +1,8 @@ +use slotmap::SecondaryMap; + +use crate::widget::table::Entity; + +pub struct State { + pub(super) num_items: usize, + pub(super) selected: SecondaryMap, +}