1use std::collections::HashMap;
4use std::fmt;
5use std::sync::{Arc, Mutex};
6
7use serde::Serialize;
8use zbus::zvariant::{OwnedValue, Str, Type, Value};
9
10#[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
34impl 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
56impl 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
70pub enum MenuItem<T> {
74 Standard(StandardItem<T>),
75 Separator,
77 Checkmark(CheckmarkItem<T>),
78 SubMenu(SubMenu<T>),
79 RadioGroup(RadioGroup<T>),
80}
81
82pub struct StandardItem<T> {
84 pub label: String,
92 pub enabled: bool,
94 pub visible: bool,
96 pub icon_name: String,
98 pub icon_data: Vec<u8>,
100 pub shortcut: Vec<Vec<String>>,
109 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
156pub struct SubMenu<T> {
158 pub label: String,
166 pub enabled: bool,
168 pub visible: bool,
170 pub icon_name: String,
172 pub icon_data: Vec<u8>,
174 pub shortcut: Vec<Vec<String>>,
183 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
227pub struct CheckmarkItem<T> {
229 pub label: String,
237 pub enabled: bool,
239 pub visible: bool,
241 pub checked: bool,
242 pub icon_name: String,
244 pub icon_data: Vec<u8>,
246 pub shortcut: Vec<Vec<String>>,
255 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
309pub 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
332pub struct RadioItem {
334 pub label: String,
342 pub enabled: bool,
344 pub visible: bool,
346 pub icon_name: String,
348 pub icon_data: Vec<u8>,
350 pub shortcut: Vec<Vec<String>>,
359 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 label: String,
388 enabled: bool,
390 visible: bool,
392 icon_name: String,
394 icon_data: Vec<u8>,
396 shortcut: Vec<Vec<String>>,
405 toggle_type: ToggleType,
406 toggle_state: ToggleState,
413 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 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 Standard,
640 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 Checkmark,
669 Radio,
671 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 Normal,
716 Informative,
718 Warning,
720 Alert,
722}
723
724impl 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)]; 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 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}