1use std::collections::HashSet;
2
3use crate::{IconSet, WindowKey};
4
5#[derive(Clone, Debug, Eq, Hash, PartialEq)]
6pub struct TrayItemId(String);
7
8impl TrayItemId {
9 #[must_use]
10 pub fn new(value: impl Into<String>) -> Self {
11 Self(value.into())
12 }
13
14 #[must_use]
15 pub fn as_str(&self) -> &str {
16 &self.0
17 }
18}
19
20#[derive(Clone, Debug, Eq, PartialEq)]
21pub enum TrayAction {
22 Custom(String),
23 ShowWindow(WindowKey),
24 HideWindow(WindowKey),
25 ToggleWindow(WindowKey),
26 FocusWindow(WindowKey),
27 CloseWindow(WindowKey),
28 Quit,
29}
30
31#[derive(Clone, Debug, PartialEq)]
32pub enum TrayMenuItem {
33 Action {
34 id: TrayItemId,
35 label: String,
36 enabled: bool,
37 action: TrayAction,
38 },
39 Check {
40 id: TrayItemId,
41 label: String,
42 enabled: bool,
43 checked: bool,
44 action: TrayAction,
45 },
46 Separator,
47 Submenu {
48 id: TrayItemId,
49 label: String,
50 enabled: bool,
51 items: Vec<TrayMenuItem>,
52 },
53}
54
55impl TrayMenuItem {
56 #[must_use]
57 pub fn id(&self) -> Option<&TrayItemId> {
58 match self {
59 Self::Action { id, .. } | Self::Check { id, .. } | Self::Submenu { id, .. } => Some(id),
60 Self::Separator => None,
61 }
62 }
63}
64
65#[derive(Clone, Debug, PartialEq)]
66pub struct TrayConfig {
67 pub tooltip: Option<String>,
68 pub title: Option<String>,
69 pub icon: Option<IconSet>,
70 pub menu: Vec<TrayMenuItem>,
71 pub visible: bool,
72 pub icon_is_template: bool,
73 pub show_menu_on_left_click: bool,
74}
75
76impl Default for TrayConfig {
77 fn default() -> Self {
78 Self {
79 tooltip: None,
80 title: None,
81 icon: None,
82 menu: Vec::new(),
83 visible: true,
84 icon_is_template: false,
85 show_menu_on_left_click: true,
86 }
87 }
88}
89
90impl TrayConfig {
91 pub fn validate(&self) -> Result<(), TrayConfigError> {
92 let mut ids = HashSet::new();
93 validate_items(&self.menu, &mut ids)
94 }
95}
96
97fn validate_items(
98 items: &[TrayMenuItem],
99 ids: &mut HashSet<TrayItemId>,
100) -> Result<(), TrayConfigError> {
101 for item in items {
102 let Some(id) = item.id() else {
103 continue;
104 };
105 if id.as_str().is_empty() {
106 return Err(TrayConfigError::EmptyItemId);
107 }
108 if !ids.insert(id.clone()) {
109 return Err(TrayConfigError::DuplicateItemId(id.clone()));
110 }
111 if let TrayMenuItem::Submenu { items, .. } = item {
112 validate_items(items, ids)?;
113 }
114 }
115 Ok(())
116}
117
118#[derive(Clone, Debug, Eq, PartialEq)]
119pub enum TrayConfigError {
120 EmptyItemId,
121 DuplicateItemId(TrayItemId),
122}
123
124impl std::fmt::Display for TrayConfigError {
125 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
126 match self {
127 Self::EmptyItemId => formatter.write_str("tray item ids cannot be empty"),
128 Self::DuplicateItemId(id) => {
129 write!(formatter, "duplicate tray item id: {}", id.as_str())
130 }
131 }
132 }
133}
134
135impl std::error::Error for TrayConfigError {}
136
137#[derive(Clone, Copy, Debug, Eq, PartialEq)]
138pub enum TrayPointerButton {
139 Primary,
140 Secondary,
141 Middle,
142 Unknown,
143}
144
145#[derive(Clone, Debug, PartialEq)]
146pub enum TrayEvent {
147 Action {
148 id: TrayItemId,
149 action: TrayAction,
150 },
151 Click {
152 button: TrayPointerButton,
153 double: bool,
154 },
155}