cosmic/widget/table/widget/
standard.rs

1use derive_setters::Setters;
2
3use crate::widget::table::model::{
4    Entity, Model,
5    category::{ItemCategory, ItemInterface},
6    selection::Selectable,
7};
8use crate::{
9    Apply, Element, theme,
10    widget::{self, container, divider, menu},
11};
12use iced::{Alignment, Border, Length, Padding};
13
14// THIS IS A PLACEHOLDER UNTIL A MORE SOPHISTICATED WIDGET CAN BE DEVELOPED
15
16#[derive(Setters)]
17#[must_use]
18pub struct TableView<'a, SelectionMode, Item, Category, Message>
19where
20    Category: ItemCategory,
21    Item: ItemInterface<Category>,
22    Model<SelectionMode, Item, Category>: Selectable,
23    SelectionMode: Default,
24    Message: Clone + 'static,
25{
26    pub(super) model: &'a Model<SelectionMode, Item, Category>,
27
28    #[setters(into)]
29    pub(super) element_padding: Padding,
30    #[setters(into)]
31    pub(super) width: Length,
32    #[setters(into)]
33    pub(super) height: Length,
34
35    #[setters(into)]
36    pub(super) item_padding: Padding,
37    pub(super) item_spacing: u16,
38    pub(super) icon_spacing: u16,
39    pub(super) icon_size: u16,
40
41    #[setters(into)]
42    pub(super) divider_padding: Padding,
43
44    // === Item Interaction ===
45    #[setters(skip)]
46    pub(super) on_item_mb_left: Option<Box<dyn Fn(Entity) -> Message + 'static>>,
47    #[setters(skip)]
48    pub(super) on_item_mb_double: Option<Box<dyn Fn(Entity) -> Message + 'static>>,
49    #[setters(skip)]
50    pub(super) on_item_mb_mid: Option<Box<dyn Fn(Entity) -> Message + 'static>>,
51    #[setters(skip)]
52    pub(super) on_item_mb_right: Option<Box<dyn Fn(Entity) -> Message + 'static>>,
53    #[setters(skip)]
54    pub(super) item_context_builder: Box<dyn Fn(&Item) -> Option<Vec<menu::Tree<Message>>>>,
55    // Item DND
56
57    // === Category Interaction ===
58    #[setters(skip)]
59    pub(super) on_category_mb_left: Option<Box<dyn Fn(Category) -> Message + 'static>>,
60    #[setters(skip)]
61    pub(super) on_category_mb_double: Option<Box<dyn Fn(Category) -> Message + 'static>>,
62    #[setters(skip)]
63    pub(super) on_category_mb_mid: Option<Box<dyn Fn(Category) -> Message + 'static>>,
64    #[setters(skip)]
65    pub(super) on_category_mb_right: Option<Box<dyn Fn(Category) -> Message + 'static>>,
66    #[setters(skip)]
67    pub(super) category_context_builder: Box<dyn Fn(Category) -> Option<Vec<menu::Tree<Message>>>>,
68}
69
70impl<'a, SelectionMode, Item, Category, Message>
71    From<TableView<'a, SelectionMode, Item, Category, Message>> for Element<'a, Message>
72where
73    Category: ItemCategory,
74    Item: ItemInterface<Category>,
75    Model<SelectionMode, Item, Category>: Selectable,
76    SelectionMode: Default,
77    Message: Clone + 'static,
78{
79    fn from(val: TableView<'a, SelectionMode, Item, Category, Message>) -> Self {
80        // Header row
81        let header_row = val
82            .model
83            .categories
84            .iter()
85            .copied()
86            .map(|category| {
87                let cat_context_tree = (val.category_context_builder)(category);
88
89                let mut sort_state = 0;
90
91                if let Some(sort) = val.model.sort {
92                    if sort.0 == category {
93                        if sort.1 {
94                            sort_state = 1;
95                        } else {
96                            sort_state = 2;
97                        }
98                    }
99                };
100
101                // Build the category header
102                widget::row::with_capacity(2)
103                    .spacing(val.icon_spacing)
104                    .push(widget::text::heading(category.to_string()))
105                    .push_maybe(match sort_state {
106                        1 => Some(widget::icon::from_name("pan-up-symbolic").icon()),
107                        2 => Some(widget::icon::from_name("pan-down-symbolic").icon()),
108                        _ => None,
109                    })
110                    .apply(container)
111                    .padding(
112                        Padding::default()
113                            .left(val.item_padding.left)
114                            .right(val.item_padding.right),
115                    )
116                    .width(category.width())
117                    .apply(widget::mouse_area)
118                    .apply(|mouse_area| {
119                        if let Some(ref on_category_select) = val.on_category_mb_left {
120                            mouse_area.on_press((on_category_select)(category))
121                        } else {
122                            mouse_area
123                        }
124                    })
125                    .apply(|mouse_area| widget::context_menu(mouse_area, cat_context_tree))
126                    .apply(Element::from)
127            })
128            .apply(widget::row::with_children)
129            .apply(Element::from);
130        // Build the items
131        let items_full = if val.model.items.is_empty() {
132            vec![
133                divider::horizontal::default()
134                    .apply(container)
135                    .padding(val.divider_padding)
136                    .apply(Element::from),
137            ]
138        } else {
139            val.model
140                .iter()
141                .flat_map(move |entity| {
142                    let item = val.model.item(entity).unwrap();
143                    let categories = &val.model.categories;
144                    let selected = val.model.is_active(entity);
145                    let item_context = (val.item_context_builder)(item);
146
147                    [
148                        divider::horizontal::default()
149                            .apply(container)
150                            .padding(val.divider_padding)
151                            .apply(Element::from),
152                        categories
153                            .iter()
154                            .map(|category| {
155                                widget::row::with_capacity(2)
156                                    .spacing(val.icon_spacing)
157                                    .push_maybe(
158                                        item.get_icon(*category)
159                                            .map(|icon| icon.size(val.icon_size)),
160                                    )
161                                    .push(widget::text::body(item.get_text(*category)))
162                                    .align_y(Alignment::Center)
163                                    .apply(container)
164                                    .width(category.width())
165                                    .align_y(Alignment::Center)
166                                    .apply(Element::from)
167                            })
168                            .apply(widget::row::with_children)
169                            .apply(container)
170                            .padding(val.item_padding)
171                            .class(theme::Container::custom(move |theme| {
172                                widget::container::Style {
173                                    icon_color: if selected {
174                                        Some(theme.cosmic().on_accent_color().into())
175                                    } else {
176                                        None
177                                    },
178                                    text_color: if selected {
179                                        Some(theme.cosmic().on_accent_color().into())
180                                    } else {
181                                        None
182                                    },
183                                    background: if selected {
184                                        Some(iced::Background::Color(
185                                            theme.cosmic().accent_color().into(),
186                                        ))
187                                    } else {
188                                        None
189                                    },
190                                    border: Border {
191                                        radius: theme.cosmic().radius_xs().into(),
192                                        ..Default::default()
193                                    },
194                                    shadow: Default::default(),
195                                    snap: true,
196                                }
197                            }))
198                            .apply(widget::mouse_area)
199                            // Left click
200                            .apply(|mouse_area| {
201                                if let Some(ref on_item_mb) = val.on_item_mb_left {
202                                    mouse_area.on_press((on_item_mb)(entity))
203                                } else {
204                                    mouse_area
205                                }
206                            })
207                            // Double click
208                            .apply(|mouse_area| {
209                                if let Some(ref on_item_mb) = val.on_item_mb_double {
210                                    mouse_area.on_double_click((on_item_mb)(entity))
211                                } else {
212                                    mouse_area
213                                }
214                            })
215                            // Middle click
216                            .apply(|mouse_area| {
217                                if let Some(ref on_item_mb) = val.on_item_mb_mid {
218                                    mouse_area.on_middle_press((on_item_mb)(entity))
219                                } else {
220                                    mouse_area
221                                }
222                            })
223                            // Right click
224                            .apply(|mouse_area| {
225                                if let Some(ref on_item_mb) = val.on_item_mb_right {
226                                    mouse_area.on_right_press((on_item_mb)(entity))
227                                } else {
228                                    mouse_area
229                                }
230                            })
231                            .apply(|mouse_area| widget::context_menu(mouse_area, item_context))
232                            .apply(Element::from),
233                    ]
234                })
235                .collect::<Vec<Element<'a, Message>>>()
236        };
237        let mut elements = items_full;
238        elements.insert(0, header_row);
239        elements
240            .apply(widget::column::with_children)
241            .width(val.width)
242            .height(val.height)
243            .spacing(val.item_spacing)
244            .padding(val.element_padding)
245            .apply(Element::from)
246    }
247}
248
249impl<'a, SelectionMode, Item, Category, Message>
250    TableView<'a, SelectionMode, Item, Category, Message>
251where
252    SelectionMode: Default,
253    Model<SelectionMode, Item, Category>: Selectable,
254    Category: ItemCategory,
255    Item: ItemInterface<Category>,
256    Message: Clone + 'static,
257{
258    pub fn new(model: &'a Model<SelectionMode, Item, Category>) -> Self {
259        let cosmic_theme::Spacing {
260            space_xxxs,
261            space_xxs,
262            ..
263        } = theme::spacing();
264
265        Self {
266            model,
267
268            element_padding: Padding::from(0),
269            width: Length::Fill,
270            height: Length::Shrink,
271
272            item_padding: Padding::from(space_xxs),
273            item_spacing: 0,
274            icon_spacing: space_xxxs,
275            icon_size: 24,
276
277            divider_padding: Padding::from(0).left(space_xxxs).right(space_xxxs),
278
279            on_item_mb_left: None,
280            on_item_mb_double: None,
281            on_item_mb_mid: None,
282            on_item_mb_right: None,
283            item_context_builder: Box::new(|_| None),
284
285            on_category_mb_left: None,
286            on_category_mb_double: None,
287            on_category_mb_mid: None,
288            on_category_mb_right: None,
289            category_context_builder: Box::new(|_| None),
290        }
291    }
292
293    pub fn on_item_left_click<F>(mut self, on_click: F) -> Self
294    where
295        F: Fn(Entity) -> Message + 'static,
296    {
297        self.on_item_mb_left = Some(Box::new(on_click));
298        self
299    }
300
301    pub fn on_item_double_click<F>(mut self, on_click: F) -> Self
302    where
303        F: Fn(Entity) -> Message + 'static,
304    {
305        self.on_item_mb_double = Some(Box::new(on_click));
306        self
307    }
308
309    pub fn on_item_middle_click<F>(mut self, on_click: F) -> Self
310    where
311        F: Fn(Entity) -> Message + 'static,
312    {
313        self.on_item_mb_mid = Some(Box::new(on_click));
314        self
315    }
316
317    pub fn on_item_right_click<F>(mut self, on_click: F) -> Self
318    where
319        F: Fn(Entity) -> Message + 'static,
320    {
321        self.on_item_mb_right = Some(Box::new(on_click));
322        self
323    }
324
325    pub fn item_context<F>(mut self, context_menu_builder: F) -> Self
326    where
327        F: Fn(&Item) -> Option<Vec<menu::Tree<Message>>> + 'static,
328        Message: 'static,
329    {
330        self.item_context_builder = Box::new(context_menu_builder);
331        self
332    }
333
334    pub fn on_category_left_click<F>(mut self, on_select: F) -> Self
335    where
336        F: Fn(Category) -> Message + 'static,
337    {
338        self.on_category_mb_left = Some(Box::new(on_select));
339        self
340    }
341    pub fn on_category_double_click<F>(mut self, on_select: F) -> Self
342    where
343        F: Fn(Category) -> Message + 'static,
344    {
345        self.on_category_mb_double = Some(Box::new(on_select));
346        self
347    }
348    pub fn on_category_middle_click<F>(mut self, on_select: F) -> Self
349    where
350        F: Fn(Category) -> Message + 'static,
351    {
352        self.on_category_mb_mid = Some(Box::new(on_select));
353        self
354    }
355
356    pub fn on_category_right_click<F>(mut self, on_select: F) -> Self
357    where
358        F: Fn(Category) -> Message + 'static,
359    {
360        self.on_category_mb_right = Some(Box::new(on_select));
361        self
362    }
363
364    pub fn category_context<F>(mut self, context_menu_builder: F) -> Self
365    where
366        F: Fn(Category) -> Option<Vec<menu::Tree<Message>>> + 'static,
367        Message: 'static,
368    {
369        self.category_context_builder = Box::new(context_menu_builder);
370        self
371    }
372}