ksni/
menu.rs

1//! Types used to construct a menu
2
3use std::collections::HashMap;
4use std::fmt;
5use std::sync::{Arc, Mutex};
6
7use serde::Serialize;
8use zbus::zvariant::{OwnedValue, Str, Type, Value};
9
10// pub struct Properties {
11//     /// Tells if the menus are in a normal state or they believe that they
12//     /// could use some attention.  Cases for showing them would be if help
13//     /// were referring to them or they accessors were being highlighted.
14//     /// This property can have two values: "normal" in almost all cases and
15//     /// "notice" when they should have a higher priority to be shown.
16//     pub status: Status,
17//     /// A list of directories that should be used for finding icons using
18//     /// the icon naming spec.  Idealy there should only be one for the icon
19//     /// theme, but additional ones are often added by applications for
20//     /// app specific icons.
21//     pub icon_theme_path: Vec<String>,
22// }
23
24/// Direction of texts
25#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Type, Serialize)]
26#[zvariant(signature = "s")]
27pub enum TextDirection {
28    #[serde(rename = "ltr")]
29    LeftToRight,
30    #[serde(rename = "rtl")]
31    RightToLeft,
32}
33
34// The Value dervie macro can only handle `dict` or `a{sv}` values
35// so we impl it manually
36impl From<TextDirection> for Value<'_> {
37    fn from(value: TextDirection) -> Self {
38        value.to_string().into()
39    }
40}
41
42impl fmt::Display for TextDirection {
43    fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
44        self.serialize(f)
45    }
46}
47
48#[derive(Type, Serialize)]
49#[zvariant(signature = "s")]
50#[serde(rename_all = "lowercase")]
51pub(crate) enum Status {
52    Normal,
53    Notice,
54}
55
56// The Value dervie macro can only handle `dict` or `a{sv}` values
57// so we impl it manually
58impl From<Status> for Value<'_> {
59    fn from(value: Status) -> Self {
60        value.to_string().into()
61    }
62}
63
64impl fmt::Display for Status {
65    fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
66        self.serialize(f)
67    }
68}
69
70/// All types of menu item
71///
72/// Do not use directly (except [`MenuItem::Separator`]), see examples in top level documents
73pub enum MenuItem<T> {
74    Standard(StandardItem<T>),
75    /// A separator
76    Separator,
77    Checkmark(CheckmarkItem<T>),
78    SubMenu(SubMenu<T>),
79    RadioGroup(RadioGroup<T>),
80}
81
82/// Menu item, the standard one
83pub struct StandardItem<T> {
84    /// Text of the item, except that:
85    /// -# two consecutive underscore characters "__" are displayed as a
86    /// single underscore,
87    /// -# any remaining underscore characters are not displayed at all,
88    /// -# the first of those remaining underscore characters (unless it is
89    /// the last character in the string) indicates that the following
90    /// character is the access key.
91    pub label: String,
92    /// Whether the item can be activated or not.
93    pub enabled: bool,
94    /// True if the item is visible in the menu.
95    pub visible: bool,
96    /// Icon name of the item, following the freedesktop.org icon spec.
97    pub icon_name: String,
98    /// PNG data of the icon.
99    pub icon_data: Vec<u8>,
100    /// The shortcut of the item. Each array represents the key press
101    /// in the list of keypresses. Each list of strings contains a list of
102    /// modifiers and then the key that is used. The modifier strings
103    /// allowed are: "Control", "Alt", "Shift" and "Super".
104    /// - A simple shortcut like Ctrl+S is represented as:
105    ///   [["Control", "S"]]
106    /// - A complex shortcut like Ctrl+Q, Alt+X is represented as:
107    ///   [["Control", "Q"], ["Alt", "X"]]
108    pub shortcut: Vec<Vec<String>>,
109    /// How the menuitem feels the information it's displaying to the
110    /// user should be presented.
111    pub disposition: Disposition,
112    pub activate: Box<dyn Fn(&mut T) + Send>,
113}
114
115impl<T> Default for StandardItem<T> {
116    fn default() -> Self {
117        StandardItem {
118            label: String::default(),
119            enabled: true,
120            visible: true,
121            icon_name: String::default(),
122            icon_data: Vec::default(),
123            shortcut: Vec::default(),
124            disposition: Disposition::Normal,
125            activate: Box::new(|_this| {}),
126        }
127    }
128}
129
130impl<T> From<StandardItem<T>> for MenuItem<T> {
131    fn from(item: StandardItem<T>) -> Self {
132        MenuItem::Standard(item)
133    }
134}
135
136impl<T: 'static> From<StandardItem<T>> for RawMenuItem<T> {
137    fn from(item: StandardItem<T>) -> Self {
138        let activate = item.activate;
139        Self {
140            r#type: ItemType::Standard,
141            label: item.label,
142            enabled: item.enabled,
143            visible: item.visible,
144            icon_name: item.icon_name,
145            icon_data: item.icon_data,
146            shortcut: item.shortcut,
147            disposition: item.disposition,
148            on_clicked: Box::new(move |this: &mut T, _id| {
149                (activate)(this);
150            }),
151            ..Default::default()
152        }
153    }
154}
155
156/// Menu item, a container of another menu tree
157pub struct SubMenu<T> {
158    /// Text of the item, except that:
159    /// -# two consecutive underscore characters "__" are displayed as a
160    /// single underscore,
161    /// -# any remaining underscore characters are not displayed at all,
162    /// -# the first of those remaining underscore characters (unless it is
163    /// the last character in the string) indicates that the following
164    /// character is the access key.
165    pub label: String,
166    /// Whether the item can be activated or not.
167    pub enabled: bool,
168    /// True if the item is visible in the menu.
169    pub visible: bool,
170    /// Icon name of the item, following the freedesktop.org icon spec.
171    pub icon_name: String,
172    /// PNG data of the icon.
173    pub icon_data: Vec<u8>,
174    /// The shortcut of the item. Each array represents the key press
175    /// in the list of keypresses. Each list of strings contains a list of
176    /// modifiers and then the key that is used. The modifier strings
177    /// allowed are: "Control", "Alt", "Shift" and "Super".
178    /// - A simple shortcut like Ctrl+S is represented as:
179    ///   [["Control", "S"]]
180    /// - A complex shortcut like Ctrl+Q, Alt+X is represented as:
181    ///   [["Control", "Q"], ["Alt", "X"]]
182    pub shortcut: Vec<Vec<String>>,
183    /// How the menuitem feels the information it's displaying to the
184    /// user should be presented.
185    pub disposition: Disposition,
186    pub submenu: Vec<MenuItem<T>>,
187}
188
189impl<T> Default for SubMenu<T> {
190    fn default() -> Self {
191        Self {
192            label: String::default(),
193            enabled: true,
194            visible: true,
195            icon_name: String::default(),
196            icon_data: Vec::default(),
197            shortcut: Vec::default(),
198            disposition: Disposition::Normal,
199            submenu: Vec::default(),
200        }
201    }
202}
203
204impl<T> From<SubMenu<T>> for MenuItem<T> {
205    fn from(item: SubMenu<T>) -> Self {
206        MenuItem::SubMenu(item)
207    }
208}
209
210impl<T> From<SubMenu<T>> for RawMenuItem<T> {
211    fn from(item: SubMenu<T>) -> Self {
212        Self {
213            r#type: ItemType::Standard,
214            label: item.label,
215            enabled: item.enabled,
216            visible: item.visible,
217            icon_name: item.icon_name,
218            icon_data: item.icon_data,
219            shortcut: item.shortcut,
220            disposition: item.disposition,
221            on_clicked: Box::new(move |_this: &mut T, _id| Default::default()),
222            ..Default::default()
223        }
224    }
225}
226
227/// Menu item, checkable
228pub struct CheckmarkItem<T> {
229    /// Text of the item, except that:
230    /// -# two consecutive underscore characters "__" are displayed as a
231    /// single underscore,
232    /// -# any remaining underscore characters are not displayed at all,
233    /// -# the first of those remaining underscore characters (unless it is
234    /// the last character in the string) indicates that the following
235    /// character is the access key.
236    pub label: String,
237    /// Whether the item can be activated or not.
238    pub enabled: bool,
239    /// True if the item is visible in the menu.
240    pub visible: bool,
241    pub checked: bool,
242    /// PNG data of the icon.
243    pub icon_name: String,
244    /// PNG data of the icon.
245    pub icon_data: Vec<u8>,
246    /// The shortcut of the item. Each array represents the key press
247    /// in the list of keypresses. Each list of strings contains a list of
248    /// modifiers and then the key that is used. The modifier strings
249    /// allowed are: "Control", "Alt", "Shift" and "Super".
250    /// - A simple shortcut like Ctrl+S is represented as:
251    ///   [["Control", "S"]]
252    /// - A complex shortcut like Ctrl+Q, Alt+X is represented as:
253    ///   [["Control", "Q"], ["Alt", "X"]]
254    pub shortcut: Vec<Vec<String>>,
255    /// How the menuitem feels the information it's displaying to the
256    /// user should be presented.
257    pub disposition: Disposition,
258    pub activate: Box<dyn Fn(&mut T) + Send>,
259}
260
261impl<T> Default for CheckmarkItem<T> {
262    fn default() -> Self {
263        CheckmarkItem {
264            label: String::default(),
265            enabled: true,
266            visible: true,
267            checked: false,
268            icon_name: String::default(),
269            icon_data: Vec::default(),
270            shortcut: Vec::default(),
271            disposition: Disposition::Normal,
272            activate: Box::new(|_this| {}),
273        }
274    }
275}
276
277impl<T> From<CheckmarkItem<T>> for MenuItem<T> {
278    fn from(item: CheckmarkItem<T>) -> Self {
279        MenuItem::Checkmark(item)
280    }
281}
282
283impl<T: 'static> From<CheckmarkItem<T>> for RawMenuItem<T> {
284    fn from(item: CheckmarkItem<T>) -> Self {
285        let activate = item.activate;
286        Self {
287            r#type: ItemType::Standard,
288            label: item.label,
289            enabled: item.enabled,
290            visible: item.visible,
291            icon_name: item.icon_name,
292            icon_data: item.icon_data,
293            shortcut: item.shortcut,
294            toggle_type: ToggleType::Checkmark,
295            toggle_state: if item.checked {
296                ToggleState::On
297            } else {
298                ToggleState::Off
299            },
300            disposition: item.disposition,
301            on_clicked: Box::new(move |this: &mut T, _id| {
302                (activate)(this);
303            }),
304            ..Default::default()
305        }
306    }
307}
308
309/// Menu item, contains [`RadioItem`]
310pub struct RadioGroup<T> {
311    pub selected: usize,
312    pub select: Box<dyn Fn(&mut T, usize) + Send>,
313    pub options: Vec<RadioItem>,
314}
315
316impl<T> Default for RadioGroup<T> {
317    fn default() -> Self {
318        Self {
319            selected: 0,
320            select: Box::new(|_, _| {}),
321            options: Default::default(),
322        }
323    }
324}
325
326impl<T> From<RadioGroup<T>> for MenuItem<T> {
327    fn from(item: RadioGroup<T>) -> Self {
328        MenuItem::RadioGroup(item)
329    }
330}
331
332/// Items of [`RadioGroup`]
333pub struct RadioItem {
334    /// Text of the item, except that:
335    /// -# two consecutive underscore characters "__" are displayed as a
336    /// single underscore,
337    /// -# any remaining underscore characters are not displayed at all,
338    /// -# the first of those remaining underscore characters (unless it is
339    /// the last character in the string) indicates that the following
340    /// character is the access key.
341    pub label: String,
342    /// Whether the item can be activated or not.
343    pub enabled: bool,
344    /// True if the item is visible in the menu.
345    pub visible: bool,
346    /// Icon name of the item, following the freedesktop.org icon spec.
347    pub icon_name: String,
348    /// PNG data of the icon.
349    pub icon_data: Vec<u8>,
350    /// The shortcut of the item. Each array represents the key press
351    /// in the list of keypresses. Each list of strings contains a list of
352    /// modifiers and then the key that is used. The modifier strings
353    /// allowed are: "Control", "Alt", "Shift" and "Super".
354    /// - A simple shortcut like Ctrl+S is represented as:
355    ///   [["Control", "S"]]
356    /// - A complex shortcut like Ctrl+Q, Alt+X is represented as:
357    ///   [["Control", "Q"], ["Alt", "X"]]
358    pub shortcut: Vec<Vec<String>>,
359    /// How the menuitem feels the information it's displaying to the
360    /// user should be presented.
361    pub disposition: Disposition,
362}
363
364impl Default for RadioItem {
365    fn default() -> Self {
366        Self {
367            label: String::default(),
368            enabled: true,
369            visible: true,
370            icon_name: String::default(),
371            icon_data: Vec::default(),
372            shortcut: Vec::default(),
373            disposition: Disposition::Normal,
374        }
375    }
376}
377
378pub(crate) struct RawMenuItem<T> {
379    r#type: ItemType,
380    /// Text of the item, except that:
381    /// -# two consecutive underscore characters "__" are displayed as a
382    /// single underscore,
383    /// -# any remaining underscore characters are not displayed at all,
384    /// -# the first of those remaining underscore characters (unless it is
385    /// the last character in the string) indicates that the following
386    /// character is the access key.
387    label: String,
388    /// Whether the item can be activated or not.
389    enabled: bool,
390    /// True if the item is visible in the menu.
391    visible: bool,
392    /// Icon name of the item, following the freedesktop.org icon spec.
393    icon_name: String,
394    /// PNG data of the icon.
395    icon_data: Vec<u8>,
396    /// The shortcut of the item. Each array represents the key press
397    /// in the list of keypresses. Each list of strings contains a list of
398    /// modifiers and then the key that is used. The modifier strings
399    /// allowed are: "Control", "Alt", "Shift" and "Super".
400    /// - A simple shortcut like Ctrl+S is represented as:
401    ///   [["Control", "S"]]
402    /// - A complex shortcut like Ctrl+Q, Alt+X is represented as:
403    ///   [["Control", "Q"], ["Alt", "X"]]
404    shortcut: Vec<Vec<String>>,
405    toggle_type: ToggleType,
406    /// Describe the current state of a "togglable" item.
407    /// Note:
408    /// The implementation does not itself handle ensuring that only one
409    /// item in a radio group is set to "on", or that a group does not have
410    /// "on" and "indeterminate" items simultaneously; maintaining this
411    /// policy is up to the toolkit wrappers.
412    toggle_state: ToggleState,
413    /// How the menuitem feels the information it's displaying to the
414    /// user should be presented.
415    disposition: Disposition,
416    pub on_clicked: Box<dyn Fn(&mut T, usize) + Send>,
417}
418
419macro_rules! if_not_default_then_insert {
420    ($map: ident, $item: ident, $default: ident, $filter: ident, $property: ident) => {
421        if_not_default_then_insert!($map, $item, $default, $filter, $property, (|r| r));
422    };
423    ($map: ident, $item: ident, $default: ident, $filter: ident, $property: ident, $to_refarg: tt) => {{
424        let name = stringify!($property).replace('_', "-");
425        if_not_default_then_insert!($map, $item, $default, $filter, $property, name, $to_refarg);
426    }};
427    ($map: ident, $item: ident, $default: ident, $filter: ident, $property: ident, $property_name: tt, $to_refarg: tt) => {
428        if ($filter.is_empty() || $filter.contains(&$property_name.to_string()))
429            && $item.$property != $default.$property
430        {
431            $map.insert(
432                $property_name.to_string(),
433                OwnedValue::from($to_refarg($item.$property.clone())),
434            );
435        }
436    };
437}
438
439impl<T> fmt::Debug for RawMenuItem<T> {
440    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
441        write!(f, "Item {}", self.label)
442    }
443}
444
445impl<T> RawMenuItem<T> {
446    pub(crate) fn to_dbus_map(&self, property_filter: &[String]) -> HashMap<String, OwnedValue> {
447        let mut properties: HashMap<String, OwnedValue> = HashMap::with_capacity(11);
448
449        let default: RawMenuItem<T> = RawMenuItem::default();
450        if_not_default_then_insert!(
451            properties,
452            self,
453            default,
454            property_filter,
455            r#type,
456            "type",
457            (|r: ItemType| r)
458        );
459        if_not_default_then_insert!(
460            properties,
461            self,
462            default,
463            property_filter,
464            label,
465            (|r: String| -> Str { r.into() })
466        );
467        if_not_default_then_insert!(properties, self, default, property_filter, enabled);
468        if_not_default_then_insert!(properties, self, default, property_filter, visible);
469        if_not_default_then_insert!(
470            properties,
471            self,
472            default,
473            property_filter,
474            icon_name,
475            (|r: String| -> Str { r.into() })
476        );
477        if_not_default_then_insert!(
478            properties,
479            self,
480            default,
481            property_filter,
482            icon_data,
483            (|r: Vec<u8>| -> OwnedValue {
484                Value::from(r)
485                    .try_into()
486                    .expect("unreachable: Vec<u8> to OwnedValue")
487            })
488        );
489        if_not_default_then_insert!(
490            properties,
491            self,
492            default,
493            property_filter,
494            shortcut,
495            (|r: Vec<Vec<String>>| -> OwnedValue {
496                Value::from(r)
497                    .try_into()
498                    .expect("unreachable: Vec<Vec<String>> to OwnedValue")
499            })
500        );
501        if_not_default_then_insert!(properties, self, default, property_filter, toggle_type);
502        if_not_default_then_insert!(properties, self, default, property_filter, toggle_state);
503        if_not_default_then_insert!(properties, self, default, property_filter, disposition);
504
505        properties
506    }
507
508    pub(crate) fn diff(&self, other: &Self) -> Option<(HashMap<String, OwnedValue>, Vec<String>)> {
509        let default = Self::default();
510        let mut updated_props: HashMap<String, OwnedValue> = HashMap::new();
511        let mut removed_props = Vec::new();
512        if self.r#type != other.r#type {
513            if other.r#type == default.r#type {
514                removed_props.push("type".into());
515            } else {
516                updated_props.insert(
517                    "type".into(),
518                    <OwnedValue as From<Str>>::from(other.r#type.to_string().into()),
519                );
520            }
521        }
522        if self.label != other.label {
523            if other.label == default.label {
524                removed_props.push("label".into());
525            } else {
526                updated_props.insert(
527                    "label".into(),
528                    <OwnedValue as From<Str>>::from(other.label.clone().into()),
529                );
530            }
531        }
532        if self.enabled != other.enabled {
533            if other.enabled == default.enabled {
534                removed_props.push("enabled".into());
535            } else {
536                updated_props.insert("enabled".into(), OwnedValue::from(other.enabled));
537            }
538        }
539        if self.visible != other.visible {
540            if other.visible == default.visible {
541                removed_props.push("visible".into());
542            } else {
543                updated_props.insert("visible".into(), OwnedValue::from(other.visible));
544            }
545        }
546        if self.icon_name != other.icon_name {
547            if other.icon_name == default.icon_name {
548                removed_props.push("icon-name".into());
549            } else {
550                updated_props.insert(
551                    "icon-name".into(),
552                    <OwnedValue as From<Str>>::from(other.icon_name.clone().into()),
553                );
554            }
555        }
556        if self.icon_data != other.icon_data {
557            if other.icon_data == default.icon_data {
558                removed_props.push("icon-data".into());
559            } else {
560                updated_props.insert(
561                    "icon-data".into(),
562                    <OwnedValue as TryFrom<Value>>::try_from(other.icon_data.clone().into())
563                        .expect("unreachable: Vec<u8> to OwnedValue"),
564                );
565            }
566        }
567        if self.shortcut != other.shortcut {
568            if other.shortcut == default.shortcut {
569                removed_props.push("shortcut".into());
570            } else {
571                updated_props.insert(
572                    "shortcut".into(),
573                    <OwnedValue as TryFrom<Value>>::try_from(other.shortcut.clone().into())
574                        .expect("unreachable: Vec<Vec<u8>> to OwnedValue"),
575                );
576            }
577        }
578        if self.toggle_type != other.toggle_type {
579            if other.toggle_type == default.toggle_type {
580                removed_props.push("toggle-type".into());
581            } else {
582                updated_props.insert(
583                    "toggle-type".into(),
584                    <OwnedValue as From<Str>>::from(other.toggle_type.to_string().into()),
585                );
586            }
587        }
588        if self.toggle_state != other.toggle_state {
589            if other.toggle_state == default.toggle_state {
590                removed_props.push("toggle-state".into());
591            } else {
592                updated_props.insert(
593                    "toggle-state".into(),
594                    OwnedValue::from(other.toggle_state as i32),
595                );
596            }
597        }
598        if self.disposition != other.disposition {
599            if other.disposition == default.disposition {
600                removed_props.push("disposition".into());
601            } else {
602                updated_props.insert(
603                    "disposition".into(),
604                    <OwnedValue as From<Str>>::from(other.disposition.to_string().into()),
605                );
606            }
607        }
608        if updated_props.is_empty() && removed_props.is_empty() {
609            None
610        } else {
611            Some((updated_props, removed_props))
612        }
613    }
614}
615
616impl<T> Default for RawMenuItem<T> {
617    fn default() -> Self {
618        RawMenuItem {
619            r#type: ItemType::Standard,
620            label: String::default(),
621            enabled: true,
622            visible: true,
623            icon_name: String::default(),
624            icon_data: Vec::default(),
625            shortcut: Vec::default(),
626            toggle_type: ToggleType::Null,
627            toggle_state: ToggleState::Indeterminate,
628            disposition: Disposition::Normal,
629            //submenu: Vec::default(),
630            on_clicked: Box::new(|_this: &mut T, _id| Default::default()),
631        }
632    }
633}
634
635#[allow(dead_code)]
636#[derive(Debug, Eq, PartialEq, Clone)]
637enum ItemType {
638    /// An item which can be clicked to trigger an action or show another menu
639    Standard,
640    /// A separator
641    Separator,
642}
643
644impl fmt::Display for ItemType {
645    fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
646        use ItemType::*;
647        match self {
648            Standard => f.write_str("standard"),
649            Separator => f.write_str("separator"),
650        }
651    }
652}
653
654impl From<ItemType> for OwnedValue {
655    fn from(value: ItemType) -> Self {
656        use ItemType::*;
657        let s = match value {
658            Standard => "standard",
659            Separator => "separator",
660        };
661        Str::from_static(s).into()
662    }
663}
664
665#[derive(Debug, Copy, Clone, Eq, PartialEq)]
666enum ToggleType {
667    /// Item is an independent togglable item
668    Checkmark,
669    /// Item is part of a group where only one item can be toggled at a time
670    Radio,
671    /// Item cannot be toggled
672    Null,
673}
674
675impl fmt::Display for ToggleType {
676    fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
677        use ToggleType::*;
678        let r = match self {
679            Checkmark => "checkmark",
680            Radio => "radio",
681            Null => "",
682        };
683        f.write_str(r)
684    }
685}
686
687impl From<ToggleType> for OwnedValue {
688    fn from(value: ToggleType) -> Self {
689        use ToggleType::*;
690        let s = match value {
691            Checkmark => "checkmark",
692            Radio => "radio",
693            Null => "",
694        };
695        Str::from_static(s).into()
696    }
697}
698
699#[derive(Debug, Copy, Clone, Eq, PartialEq)]
700enum ToggleState {
701    Off = 0,
702    On = 1,
703    Indeterminate = -1,
704}
705
706impl From<ToggleState> for OwnedValue {
707    fn from(value: ToggleState) -> Self {
708        (value as i32).into()
709    }
710}
711
712#[derive(Debug, Copy, Clone, Eq, PartialEq)]
713pub enum Disposition {
714    /// A standard menu item
715    Normal,
716    /// Providing additional information to the user
717    Informative,
718    /// Looking at potentially harmful results
719    Warning,
720    /// Something bad could potentially happen
721    Alert,
722}
723
724/// Item disposition
725impl fmt::Display for Disposition {
726    fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
727        use Disposition::*;
728        let r = match self {
729            Normal => "normal",
730            Informative => "informative",
731            Warning => "warning",
732            Alert => "alert",
733        };
734        f.write_str(r)
735    }
736}
737
738impl From<Disposition> for OwnedValue {
739    fn from(value: Disposition) -> Self {
740        use Disposition::*;
741        let s = match value {
742            Normal => "normal",
743            Informative => "informative",
744            Warning => "warning",
745            Alert => "alert",
746        };
747        Str::from_static(s).into()
748    }
749}
750
751pub(crate) fn menu_flatten<T: 'static>(
752    items: Vec<MenuItem<T>>,
753) -> Vec<(RawMenuItem<T>, Vec<usize>)> {
754    let mut list: Vec<(RawMenuItem<T>, Vec<usize>)> =
755        vec![(RawMenuItem::default(), Vec::with_capacity(items.len()))];
756
757    let mut stack = vec![(items, 0)]; // (menu, menu's parent)
758
759    while let Some((mut current_menu, parent_index)) = stack.pop() {
760        while !current_menu.is_empty() {
761            match current_menu.remove(0) {
762                MenuItem::Standard(item) => {
763                    let index = list.len();
764                    list.push((item.into(), Vec::new()));
765                    // Add self to parent's submenu
766                    list[parent_index].1.push(index);
767                }
768                MenuItem::Separator => {
769                    let item = RawMenuItem {
770                        r#type: ItemType::Separator,
771                        ..Default::default()
772                    };
773                    let index = list.len();
774                    list.push((item, Vec::new()));
775                    list[parent_index].1.push(index);
776                }
777                MenuItem::Checkmark(item) => {
778                    let index = list.len();
779                    list.push((item.into(), Vec::new()));
780                    list[parent_index].1.push(index);
781                }
782                MenuItem::SubMenu(mut item) => {
783                    let submenu = std::mem::replace(&mut item.submenu, Default::default());
784                    let index = list.len();
785                    list.push((item.into(), Vec::with_capacity(submenu.len())));
786                    list[parent_index].1.push(index);
787                    if !submenu.is_empty() {
788                        stack.push((current_menu, parent_index));
789                        stack.push((submenu, index));
790                        break;
791                    }
792                }
793                MenuItem::RadioGroup(group) => {
794                    let offset = list.len();
795                    let on_selected = Arc::new(Mutex::new(group.select));
796                    for (idx, option) in group.options.into_iter().enumerate() {
797                        let on_selected = on_selected.clone();
798                        let item = RawMenuItem {
799                            r#type: ItemType::Standard,
800                            label: option.label,
801                            enabled: option.enabled,
802                            visible: option.visible,
803                            icon_name: option.icon_name,
804                            icon_data: option.icon_data,
805                            shortcut: option.shortcut,
806                            toggle_type: ToggleType::Radio,
807                            toggle_state: if idx == group.selected {
808                                ToggleState::On
809                            } else {
810                                ToggleState::Off
811                            },
812                            disposition: option.disposition,
813                            on_clicked: Box::new(move |this: &mut T, id| {
814                                (on_selected.lock().unwrap())(this, id - offset);
815                            }),
816                            ..Default::default()
817                        };
818                        let index = list.len();
819                        list.push((item, Vec::new()));
820                        list[parent_index].1.push(index);
821                    }
822                }
823            }
824        }
825    }
826
827    list
828}
829
830#[cfg(test)]
831mod test {
832    use super::*;
833
834    #[test]
835    fn test_enums() {
836        assert_eq!(TextDirection::LeftToRight.to_string(), "ltr");
837        assert_eq!(TextDirection::RightToLeft.to_string(), "rtl");
838    }
839
840    #[test]
841    fn test_menu_flatten() {
842        let x: Vec<MenuItem<()>> = vec![
843            SubMenu {
844                label: "a".into(),
845                submenu: vec![
846                    SubMenu {
847                        label: "a1".into(),
848                        submenu: vec![StandardItem {
849                            label: "a1.1".into(),
850                            ..Default::default()
851                        }
852                        .into()],
853                        ..Default::default()
854                    }
855                    .into(),
856                    StandardItem {
857                        label: "a2".into(),
858                        ..Default::default()
859                    }
860                    .into(),
861                ],
862                ..Default::default()
863            }
864            .into(),
865            StandardItem {
866                label: "b".into(),
867                ..Default::default()
868            }
869            .into(),
870            SubMenu {
871                label: "c".into(),
872                submenu: vec![
873                    StandardItem {
874                        label: "c1".into(),
875                        ..Default::default()
876                    }
877                    .into(),
878                    SubMenu {
879                        label: "c2".into(),
880                        submenu: vec![StandardItem {
881                            label: "c2.1".into(),
882                            ..Default::default()
883                        }
884                        .into()],
885                        ..Default::default()
886                    }
887                    .into(),
888                ],
889                ..Default::default()
890            }
891            .into(),
892        ];
893
894        let r = menu_flatten(x);
895        let expect: Vec<(RawMenuItem<()>, Vec<usize>)> = vec![
896            (
897                RawMenuItem {
898                    label: "".into(),
899                    ..Default::default()
900                },
901                vec![1, 5, 6],
902            ),
903            (
904                RawMenuItem {
905                    label: "a".into(),
906                    ..Default::default()
907                },
908                vec![2, 4],
909            ),
910            (
911                RawMenuItem {
912                    label: "a1".into(),
913                    ..Default::default()
914                },
915                vec![3],
916            ),
917            (
918                RawMenuItem {
919                    label: "a1.1".into(),
920                    ..Default::default()
921                },
922                vec![],
923            ),
924            (
925                RawMenuItem {
926                    label: "a2".into(),
927                    ..Default::default()
928                },
929                vec![],
930            ),
931            (
932                RawMenuItem {
933                    label: "b".into(),
934                    ..Default::default()
935                },
936                vec![],
937            ),
938            (
939                RawMenuItem {
940                    label: "c".into(),
941                    ..Default::default()
942                },
943                vec![7, 8],
944            ),
945            (
946                RawMenuItem {
947                    label: "c1".into(),
948                    ..Default::default()
949                },
950                vec![],
951            ),
952            (
953                RawMenuItem {
954                    label: "c2".into(),
955                    ..Default::default()
956                },
957                vec![9],
958            ),
959            (
960                RawMenuItem {
961                    label: "c2.1".into(),
962                    ..Default::default()
963                },
964                vec![],
965            ),
966        ];
967        assert_eq!(r.len(), 10);
968        assert_eq!(r[0].1, expect[0].1);
969        assert_eq!(r[1].1, expect[1].1);
970        assert_eq!(r[2].1, expect[2].1);
971        assert_eq!(r[3].1, expect[3].1);
972        assert_eq!(r[4].1, expect[4].1);
973        assert_eq!(r[5].1, expect[5].1);
974        assert_eq!(r[6].1, expect[6].1);
975        assert_eq!(r[7].1, expect[7].1);
976        assert_eq!(r[8].1, expect[8].1);
977        assert_eq!(r[9].1, expect[9].1);
978        assert_eq!(r[0].0.label, expect[0].0.label);
979        assert_eq!(r[1].0.label, expect[1].0.label);
980        assert_eq!(r[2].0.label, expect[2].0.label);
981        assert_eq!(r[3].0.label, expect[3].0.label);
982        assert_eq!(r[4].0.label, expect[4].0.label);
983        assert_eq!(r[5].0.label, expect[5].0.label);
984        assert_eq!(r[6].0.label, expect[6].0.label);
985        assert_eq!(r[7].0.label, expect[7].0.label);
986        assert_eq!(r[8].0.label, expect[8].0.label);
987        assert_eq!(r[9].0.label, expect[9].0.label);
988    }
989}