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