cosmic/widget/menu/
menu_tree.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
// From iced_aw, license MIT

//! A tree structure for constructing a hierarchical menu

use std::borrow::Cow;
use std::collections::HashMap;
use std::rc::Rc;

use iced_widget::core::{renderer, Element};

use crate::iced_core::{Alignment, Length};
use crate::widget::icon;
use crate::widget::menu::action::MenuAction;
use crate::widget::menu::key_bind::KeyBind;
use crate::{theme, widget};

/// Nested menu is essentially a tree of items, a menu is a collection of items
/// a menu itself can also be an item of another menu.
///
/// A `MenuTree` represents a node in the tree, it holds a widget as a menu item
/// for its parent, and a list of menu tree as child nodes.
/// Conceptually a node is either a menu(inner node) or an item(leaf node),
/// but there's no need to explicitly distinguish them here, if a menu tree
/// has children, it's a menu, otherwise it's an item
#[allow(missing_debug_implementations)]
pub struct MenuTree<'a, Message, Renderer = crate::Renderer> {
    /// The menu tree will be flatten into a vector to build a linear widget tree,
    /// the `index` field is the index of the item in that vector
    pub(crate) index: usize,

    /// The item of the menu tree
    pub(crate) item: Element<'a, Message, crate::Theme, Renderer>,
    /// The children of the menu tree
    pub(crate) children: Vec<MenuTree<'a, Message, Renderer>>,
    /// The width of the menu tree
    pub(crate) width: Option<u16>,
    /// The height of the menu tree
    pub(crate) height: Option<u16>,
}

impl<'a, Message, Renderer> MenuTree<'a, Message, Renderer>
where
    Renderer: renderer::Renderer,
{
    /// Create a new menu tree from a widget
    pub fn new(item: impl Into<Element<'a, Message, crate::Theme, Renderer>>) -> Self {
        Self {
            index: 0,
            item: item.into(),
            children: Vec::new(),
            width: None,
            height: None,
        }
    }

    /// Create a menu tree from a widget and a vector of sub trees
    pub fn with_children(
        item: impl Into<Element<'a, Message, crate::Theme, Renderer>>,
        children: Vec<impl Into<MenuTree<'a, Message, Renderer>>>,
    ) -> Self {
        Self {
            index: 0,
            item: item.into(),
            children: children.into_iter().map(Into::into).collect(),
            width: None,
            height: None,
        }
    }

    /// Sets the width of the menu tree.
    /// See [`ItemWidth`]
    ///
    /// [`ItemWidth`]:`super::ItemWidth`
    #[must_use]
    pub fn width(mut self, width: u16) -> Self {
        self.width = Some(width);
        self
    }

    /// Sets the height of the menu tree.
    /// See [`ItemHeight`]
    ///
    /// [`ItemHeight`]: `super::ItemHeight`
    #[must_use]
    pub fn height(mut self, height: u16) -> Self {
        self.height = Some(height);
        self
    }

    /* Keep `set_index()` and `flattern()` recurse in the same order */

    /// Set the index of each item
    pub(crate) fn set_index(&mut self) {
        /// inner counting function.
        fn rec<Message, Renderer>(mt: &mut MenuTree<'_, Message, Renderer>, count: &mut usize) {
            // keep items under the same menu line up
            mt.children.iter_mut().for_each(|c| {
                c.index = *count;
                *count += 1;
            });

            mt.children.iter_mut().for_each(|c| rec(c, count));
        }

        let mut count = 0;
        self.index = count;
        count += 1;
        rec(self, &mut count);
    }

    /// Flatten the menu tree
    pub(crate) fn flattern(&'a self) -> Vec<&Self> {
        /// Inner flattening function
        fn rec<'a, Message, Renderer>(
            mt: &'a MenuTree<'a, Message, Renderer>,
            flat: &mut Vec<&MenuTree<'a, Message, Renderer>>,
        ) {
            mt.children.iter().for_each(|c| {
                flat.push(c);
            });

            mt.children.iter().for_each(|c| {
                rec(c, flat);
            });
        }

        let mut flat = Vec::new();
        flat.push(self);
        rec(self, &mut flat);

        flat
    }
}

impl<'a, Message, Renderer> From<Element<'a, Message, crate::Theme, Renderer>>
    for MenuTree<'a, Message, Renderer>
where
    Renderer: renderer::Renderer,
{
    fn from(value: Element<'a, Message, crate::Theme, Renderer>) -> Self {
        Self::new(value)
    }
}

pub fn menu_button<'a, Message: 'a>(
    children: Vec<crate::Element<'a, Message>>,
) -> crate::widget::Button<'a, Message> {
    widget::button::custom(
        widget::Row::with_children(children)
            .align_y(Alignment::Center)
            .height(Length::Fill)
            .width(Length::Fill),
    )
    .height(Length::Fixed(36.0))
    .padding([4, 16])
    .width(Length::Fill)
    .class(theme::Button::MenuItem)
}

/// Represents a menu item that performs an action when selected or a separator between menu items.
///
/// - `Action` - Represents a menu item that performs an action when selected.
///     - `L` - The label of the menu item.
///     - `A` - The action to perform when the menu item is selected, the action must implement the `MenuAction` trait.
/// - `CheckBox` - Represents a checkbox menu item.
///     - `L` - The label of the menu item.
///     - `bool` - The state of the checkbox.
///     - `A` - The action to perform when the menu item is selected, the action must implement the `MenuAction` trait.
/// - `Folder` - Represents a folder menu item.
///     - `L` - The label of the menu item.
///     - `Vec<MenuItem<A, L>>` - A vector of menu items.
/// - `Divider` - Represents a divider between menu items.
pub enum MenuItem<A: MenuAction, L: Into<Cow<'static, str>>> {
    /// Represents a button menu item.
    Button(L, Option<icon::Handle>, A),
    /// Represents a button menu item that is disabled.
    ButtonDisabled(L, Option<icon::Handle>, A),
    /// Represents a checkbox menu item.
    CheckBox(L, Option<icon::Handle>, bool, A),
    /// Represents a folder menu item.
    Folder(L, Vec<MenuItem<A, L>>),
    /// Represents a divider between menu items.
    Divider,
}

/// Create a root menu item.
///
/// # Arguments
/// - `label` - The label of the menu item.
///
/// # Returns
/// - A button for the root menu item.
pub fn menu_root<'a, Message, Renderer: renderer::Renderer>(
    label: impl Into<Cow<'a, str>> + 'a,
) -> iced::Element<'a, Message, crate::Theme, Renderer>
where
    Element<'a, Message, crate::Theme, Renderer>: From<widget::Button<'a, Message>>,
{
    widget::button::custom(widget::text(label))
        .padding([4, 12])
        .class(theme::Button::MenuRoot)
        .into()
}

/// Create a list of menu items from a vector of `MenuItem`.
///
/// The `MenuItem` can be either an action or a separator.
///
/// # Arguments
/// - `key_binds` - A reference to a `HashMap` that maps `KeyBind` to `A`.
/// - `children` - A vector of `MenuItem`.
///
/// # Returns
/// - A vector of `MenuTree`.
pub fn menu_items<
    'a,
    A: MenuAction<Message = Message>,
    L: Into<Cow<'static, str>> + 'static,
    Message: 'a,
    Renderer: renderer::Renderer + 'a,
>(
    key_binds: &HashMap<KeyBind, A>,
    children: Vec<MenuItem<A, L>>,
) -> Vec<MenuTree<'a, Message, Renderer>>
where
    Element<'a, Message, crate::Theme, Renderer>: From<widget::button::Button<'a, Message>>,
{
    fn find_key<A: MenuAction>(action: &A, key_binds: &HashMap<KeyBind, A>) -> String {
        for (key_bind, key_action) in key_binds {
            if action == key_action {
                return key_bind.to_string();
            }
        }
        String::new()
    }

    let size = children.len();

    children
        .into_iter()
        .enumerate()
        .flat_map(|(i, item)| {
            let mut trees = vec![];
            let spacing = crate::theme::active().cosmic().spacing;

            match item {
                MenuItem::Button(label, icon, action) => {
                    let key = find_key(&action, key_binds);
                    let mut items = vec![
                        widget::text(label).into(),
                        widget::horizontal_space().into(),
                        widget::text(key).into(),
                    ];

                    if let Some(icon) = icon {
                        items.insert(0, widget::icon::icon(icon).size(14).into());
                        items.insert(1, widget::Space::with_width(spacing.space_xxs).into());
                    }

                    let menu_button = menu_button(items).on_press(action.message());

                    trees.push(MenuTree::<Message, Renderer>::new(menu_button));
                }
                MenuItem::ButtonDisabled(label, icon, action) => {
                    let key = find_key(&action, key_binds);

                    let mut items = vec![
                        widget::text(label).into(),
                        widget::horizontal_space().into(),
                        widget::text(key).into(),
                    ];

                    if let Some(icon) = icon {
                        items.insert(0, widget::icon::icon(icon).size(14).into());
                        items.insert(1, widget::Space::with_width(spacing.space_xxs).into());
                    }

                    let menu_button = menu_button(items);

                    trees.push(MenuTree::<Message, Renderer>::new(menu_button));
                }
                MenuItem::CheckBox(label, icon, value, action) => {
                    let key = find_key(&action, key_binds);
                    let mut items = vec![
                        if value {
                            widget::icon::from_name("object-select-symbolic")
                                .size(16)
                                .icon()
                                .class(theme::Svg::Custom(Rc::new(|theme| {
                                    iced_widget::svg::Style {
                                        color: Some(theme.cosmic().accent_color().into()),
                                    }
                                })))
                                .width(Length::Fixed(16.0))
                                .into()
                        } else {
                            widget::Space::with_width(Length::Fixed(16.0)).into()
                        },
                        widget::Space::with_width(spacing.space_xxs).into(),
                        widget::text(label).align_x(iced::Alignment::Start).into(),
                        widget::horizontal_space().into(),
                        widget::text(key).into(),
                    ];

                    if let Some(icon) = icon {
                        items.insert(1, widget::Space::with_width(spacing.space_xxs).into());
                        items.insert(2, widget::icon::icon(icon).size(14).into());
                    }

                    trees.push(MenuTree::new(menu_button(items).on_press(action.message())));
                }
                MenuItem::Folder(label, children) => {
                    trees.push(MenuTree::<Message, Renderer>::with_children(
                        menu_button(vec![
                            widget::text(label).into(),
                            widget::horizontal_space().into(),
                            widget::icon::from_name("pan-end-symbolic")
                                .size(16)
                                .icon()
                                .into(),
                        ])
                        .class(
                            // Menu folders have no on_press so they take on the disabled style by default
                            if children.is_empty() {
                                // This will make the folder use the disabled style if it has no children
                                theme::Button::MenuItem
                            } else {
                                // This will make the folder use the enabled style if it has children
                                theme::Button::MenuFolder
                            },
                        ),
                        menu_items(key_binds, children),
                    ));
                }
                MenuItem::Divider => {
                    if i != size - 1 {
                        trees.push(MenuTree::<Message, Renderer>::new(
                            widget::divider::horizontal::light(),
                        ));
                    }
                }
            }
            trees
        })
        .collect()
}