1use crate::event::{Key, Modifiers};
7use crate::platform;
8
9#[derive(Clone, Debug)]
10pub enum MenuEntry {
11 Item(MenuItem),
12 Separator,
13}
14
15#[derive(Clone, Debug)]
16pub struct MenuItem {
17 pub label: String,
18 pub icon: Option<char>,
19 pub shortcut: Option<String>,
20 pub accelerator: Option<MenuShortcut>,
21 pub enabled: bool,
22 pub selection: MenuSelection,
23 pub action: Option<String>,
24 pub submenu: Vec<MenuEntry>,
25 pub close_on_activate: bool,
26}
27
28#[derive(Clone, Copy, Debug, PartialEq, Eq)]
29pub enum MenuSelection {
30 None,
31 Check { selected: bool },
32 Radio { selected: bool },
33}
34
35impl MenuItem {
36 pub fn action(label: impl Into<String>, action: impl Into<String>) -> Self {
37 Self {
38 label: label.into(),
39 icon: None,
40 shortcut: None,
41 accelerator: None,
42 enabled: true,
43 selection: MenuSelection::None,
44 action: Some(action.into()),
45 submenu: Vec::new(),
46 close_on_activate: true,
47 }
48 }
49
50 pub fn submenu(label: impl Into<String>, submenu: Vec<MenuEntry>) -> Self {
51 Self {
52 label: label.into(),
53 icon: None,
54 shortcut: None,
55 accelerator: None,
56 enabled: true,
57 selection: MenuSelection::None,
58 action: None,
59 submenu,
60 close_on_activate: false,
61 }
62 }
63
64 pub fn disabled(mut self) -> Self {
65 self.enabled = false;
66 self
67 }
68
69 pub fn checked(mut self, checked: bool) -> Self {
70 self.selection = MenuSelection::Check { selected: checked };
71 self
72 }
73
74 pub fn radio(mut self, selected: bool) -> Self {
75 self.selection = MenuSelection::Radio { selected };
76 self
77 }
78
79 pub fn keep_open(mut self) -> Self {
80 self.close_on_activate = false;
81 self
82 }
83
84 pub fn close_on_activate(mut self, close: bool) -> Self {
85 self.close_on_activate = close;
86 self
87 }
88
89 pub fn icon(mut self, icon: char) -> Self {
90 self.icon = Some(icon);
91 self
92 }
93
94 pub fn shortcut(mut self, shortcut: impl Into<String>) -> Self {
95 let shortcut = shortcut.into();
96 self.accelerator = MenuShortcut::parse(&shortcut);
97 self.shortcut = Some(shortcut);
98 self
99 }
100
101 pub fn accelerator(mut self, accelerator: MenuShortcut) -> Self {
102 self.shortcut = Some(accelerator.display_text());
103 self.accelerator = Some(accelerator);
104 self
105 }
106
107 pub fn has_submenu(&self) -> bool {
108 !self.submenu.is_empty()
109 }
110}
111
112impl From<MenuItem> for MenuEntry {
113 fn from(item: MenuItem) -> Self {
114 Self::Item(item)
115 }
116}
117
118#[derive(Clone, Copy, Debug, PartialEq, Eq)]
119pub struct MenuShortcut {
120 pub key: ShortcutKey,
121 pub command: bool,
123 pub shift: bool,
124 pub alt: bool,
125}
126
127#[derive(Clone, Copy, Debug, PartialEq, Eq)]
128pub enum ShortcutKey {
129 Char(char),
130 Insert,
131 Delete,
132 Backspace,
133 Enter,
134 Escape,
135}
136
137impl MenuShortcut {
138 pub fn command_char(ch: char) -> Self {
139 Self {
140 key: ShortcutKey::Char(ch.to_ascii_uppercase()),
141 command: true,
142 shift: false,
143 alt: false,
144 }
145 }
146
147 pub fn parse(text: &str) -> Option<Self> {
148 let mut command = false;
149 let mut shift = false;
150 let mut alt = false;
151 let mut key = None;
152 for part in text.split('+') {
153 let token = part.trim();
154 match token.to_ascii_lowercase().as_str() {
155 "ctrl" | "control" | "cmd" | "command" | "meta" => command = true,
156 "shift" => shift = true,
157 "alt" | "option" => alt = true,
158 "insert" => key = Some(ShortcutKey::Insert),
159 "delete" | "del" => key = Some(ShortcutKey::Delete),
160 "backspace" => key = Some(ShortcutKey::Backspace),
161 "enter" | "return" => key = Some(ShortcutKey::Enter),
162 "esc" | "escape" => key = Some(ShortcutKey::Escape),
163 _ => {
164 let mut chars = token.chars();
165 let ch = chars.next()?;
166 if chars.next().is_none() {
167 key = Some(ShortcutKey::Char(ch.to_ascii_uppercase()));
168 } else {
169 return None;
170 }
171 }
172 }
173 }
174 Some(Self {
175 key: key?,
176 command,
177 shift,
178 alt,
179 })
180 }
181
182 pub fn matches(self, key: &Key, modifiers: Modifiers) -> bool {
183 let command_matches = if self.command {
184 platform::command_modifier_pressed(modifiers)
185 } else {
186 platform::command_modifier_released(modifiers)
187 };
188 command_matches
189 && modifiers.shift == self.shift
190 && modifiers.alt == self.alt
191 && self.key.matches(key)
192 }
193
194 pub fn display_text(self) -> String {
195 let mut parts = Vec::new();
196 if self.command {
197 parts.push(platform::primary_modifier_label().to_string());
198 }
199 if self.shift {
200 parts.push("Shift".to_string());
201 }
202 if self.alt {
203 parts.push("Alt".to_string());
204 }
205 parts.push(self.key.display_text());
206 parts.join("+")
207 }
208}
209
210impl ShortcutKey {
211 fn matches(self, key: &Key) -> bool {
212 match (self, key) {
213 (Self::Char(expected), Key::Char(actual)) => {
214 expected.eq_ignore_ascii_case(&actual.to_ascii_uppercase())
215 }
216 (Self::Insert, Key::Insert)
217 | (Self::Delete, Key::Delete)
218 | (Self::Backspace, Key::Backspace)
219 | (Self::Enter, Key::Enter)
220 | (Self::Escape, Key::Escape) => true,
221 _ => false,
222 }
223 }
224
225 fn display_text(self) -> String {
226 match self {
227 Self::Char(ch) => ch.to_string(),
228 Self::Insert => "Insert".to_string(),
229 Self::Delete => "Delete".to_string(),
230 Self::Backspace => "Backspace".to_string(),
231 Self::Enter => "Enter".to_string(),
232 Self::Escape => "Esc".to_string(),
233 }
234 }
235}