1use super::{
2 ItemAnchor, ItemSelector, ItemSelectorTyped, Menu, MenuBar, MenuBarError, MenuBarPatch,
3 MenuBarPatchOp, MenuItem, MenuTarget,
4};
5
6impl MenuBarPatch {
7 pub fn apply_to(&self, base: &mut MenuBar) -> Result<(), MenuBarError> {
8 for (idx, op) in self.ops.iter().cloned().enumerate() {
9 apply_patch_op(base, idx, op)?;
10 }
11 Ok(())
12 }
13}
14
15fn apply_patch_op(
16 menu_bar: &mut MenuBar,
17 index: usize,
18 op: MenuBarPatchOp,
19) -> Result<(), MenuBarError> {
20 fn menu_index(menu_bar: &MenuBar, title: &str) -> Option<usize> {
21 menu_bar
22 .menus
23 .iter()
24 .position(|m| m.title.as_ref() == title)
25 }
26
27 fn resolve_menu_items_mut<'a>(
28 menu_bar: &'a mut MenuBar,
29 target: &MenuTarget,
30 ) -> Option<&'a mut Vec<MenuItem>> {
31 let path: Vec<&str> = match target {
32 MenuTarget::Title(title) => vec![title.as_str()],
33 MenuTarget::Path(parts) => parts.iter().map(|s| s.as_str()).collect(),
34 };
35 let (first, rest) = path.split_first()?;
36 let menu = menu_bar
37 .menus
38 .iter_mut()
39 .find(|m| m.title.as_ref() == *first)?;
40
41 fn resolve_submenu_items_mut<'a>(
42 items: &'a mut [MenuItem],
43 path: &[&str],
44 ) -> Option<&'a mut Vec<MenuItem>> {
45 let (first, rest) = path.split_first()?;
46 for item in items.iter_mut() {
47 if let MenuItem::Submenu {
48 title,
49 items: submenu_items,
50 ..
51 } = item
52 && title.as_ref() == *first
53 {
54 if rest.is_empty() {
55 return Some(submenu_items);
56 }
57 return resolve_submenu_items_mut(submenu_items, rest);
58 }
59 }
60 None
61 }
62
63 if rest.is_empty() {
64 return Some(&mut menu.items);
65 }
66
67 resolve_submenu_items_mut(&mut menu.items, rest)
68 }
69
70 fn resolve_anchor_index(items: &[MenuItem], anchor: &ItemAnchor) -> Option<usize> {
71 match anchor {
72 ItemAnchor::Index(i) => Some(*i),
73 ItemAnchor::Command(cmd) => items.iter().position(|item| match item {
74 MenuItem::Command { command, .. } => command.as_str() == cmd.as_str(),
75 _ => false,
76 }),
77 }
78 }
79
80 fn resolve_selector_index(items: &[MenuItem], selector: &ItemSelector) -> Option<usize> {
81 match selector {
82 ItemSelector::Anchor(anchor) => resolve_anchor_index(items, anchor),
83 ItemSelector::Typed(ItemSelectorTyped::Submenu { title }) => items.iter().position(
84 |item| matches!(item, MenuItem::Submenu { title: t, .. } if t.as_ref() == title),
85 ),
86 ItemSelector::Typed(ItemSelectorTyped::Label { title }) => items.iter().position(
87 |item| matches!(item, MenuItem::Label { title: t } if t.as_ref() == title),
88 ),
89 }
90 }
91
92 let fail = |error: String| Err(MenuBarError::PatchFailed { index, error });
93
94 match op {
95 MenuBarPatchOp::AppendMenu {
96 title,
97 role,
98 mnemonic,
99 items,
100 } => {
101 menu_bar.menus.push(Menu {
102 title: title.into(),
103 role,
104 mnemonic,
105 items,
106 });
107 Ok(())
108 }
109 MenuBarPatchOp::InsertMenuBefore {
110 title,
111 role,
112 mnemonic,
113 before,
114 items,
115 } => {
116 let Some(before_idx) = menu_index(menu_bar, &before) else {
117 return fail(format!("target menu not found: {before}"));
118 };
119 menu_bar.menus.insert(
120 before_idx,
121 Menu {
122 title: title.into(),
123 role,
124 mnemonic,
125 items,
126 },
127 );
128 Ok(())
129 }
130 MenuBarPatchOp::InsertMenuAfter {
131 title,
132 role,
133 mnemonic,
134 after,
135 items,
136 } => {
137 let Some(after_idx) = menu_index(menu_bar, &after) else {
138 return fail(format!("target menu not found: {after}"));
139 };
140 let insert_at = (after_idx + 1).min(menu_bar.menus.len());
141 menu_bar.menus.insert(
142 insert_at,
143 Menu {
144 title: title.into(),
145 role,
146 mnemonic,
147 items,
148 },
149 );
150 Ok(())
151 }
152 MenuBarPatchOp::RemoveMenu { title } => {
153 let Some(idx) = menu_index(menu_bar, &title) else {
154 return fail(format!("menu not found: {title}"));
155 };
156 menu_bar.menus.remove(idx);
157 Ok(())
158 }
159 MenuBarPatchOp::RenameMenu { from, to } => {
160 let Some(idx) = menu_index(menu_bar, &from) else {
161 return fail(format!("menu not found: {from}"));
162 };
163 menu_bar.menus[idx].title = to.into();
164 Ok(())
165 }
166 MenuBarPatchOp::MoveMenuBefore { title, before } => {
167 let Some(from_idx) = menu_index(menu_bar, &title) else {
168 return fail(format!("menu not found: {title}"));
169 };
170 let Some(mut to_idx) = menu_index(menu_bar, &before) else {
171 return fail(format!("target menu not found: {before}"));
172 };
173 let menu = menu_bar.menus.remove(from_idx);
174 if from_idx < to_idx {
175 to_idx = to_idx.saturating_sub(1);
176 }
177 menu_bar.menus.insert(to_idx, menu);
178 Ok(())
179 }
180 MenuBarPatchOp::MoveMenuAfter { title, after } => {
181 let Some(from_idx) = menu_index(menu_bar, &title) else {
182 return fail(format!("menu not found: {title}"));
183 };
184 let Some(mut to_idx) = menu_index(menu_bar, &after) else {
185 return fail(format!("target menu not found: {after}"));
186 };
187 let menu = menu_bar.menus.remove(from_idx);
188 if from_idx <= to_idx {
189 to_idx = to_idx.saturating_sub(1);
190 }
191 let insert_at = (to_idx + 1).min(menu_bar.menus.len());
192 menu_bar.menus.insert(insert_at, menu);
193 Ok(())
194 }
195 MenuBarPatchOp::RemoveAt { menu, at } => {
196 let Some(items) = resolve_menu_items_mut(menu_bar, &menu) else {
197 return fail("menu path not found".to_string());
198 };
199 let Some(item_idx) = resolve_selector_index(items, &at) else {
200 return fail("item selector not found".to_string());
201 };
202 if item_idx >= items.len() {
203 return fail(format!("index out of bounds: {item_idx}"));
204 }
205 items.remove(item_idx);
206 Ok(())
207 }
208 MenuBarPatchOp::MoveAtBefore { menu, at, before } => {
209 let Some(items) = resolve_menu_items_mut(menu_bar, &menu) else {
210 return fail("menu path not found".to_string());
211 };
212 let Some(from_idx) = resolve_selector_index(items, &at) else {
213 return fail("item selector not found".to_string());
214 };
215 let Some(mut to_idx) = resolve_selector_index(items, &before) else {
216 return fail("before selector not found".to_string());
217 };
218 if from_idx >= items.len() {
219 return fail(format!("index out of bounds: {from_idx}"));
220 }
221 if to_idx >= items.len() {
222 return fail(format!("index out of bounds: {to_idx}"));
223 }
224
225 let item = items.remove(from_idx);
226 if from_idx < to_idx {
227 to_idx = to_idx.saturating_sub(1);
228 }
229 items.insert(to_idx.min(items.len()), item);
230 Ok(())
231 }
232 MenuBarPatchOp::MoveAtAfter { menu, at, after } => {
233 let Some(items) = resolve_menu_items_mut(menu_bar, &menu) else {
234 return fail("menu path not found".to_string());
235 };
236 let Some(from_idx) = resolve_selector_index(items, &at) else {
237 return fail("item selector not found".to_string());
238 };
239 let Some(mut to_idx) = resolve_selector_index(items, &after) else {
240 return fail("after selector not found".to_string());
241 };
242 if from_idx >= items.len() {
243 return fail(format!("index out of bounds: {from_idx}"));
244 }
245 if to_idx >= items.len() {
246 return fail(format!("index out of bounds: {to_idx}"));
247 }
248
249 let item = items.remove(from_idx);
250 if from_idx <= to_idx {
251 to_idx = to_idx.saturating_sub(1);
252 }
253 let insert_at = (to_idx + 1).min(items.len());
254 items.insert(insert_at, item);
255 Ok(())
256 }
257 MenuBarPatchOp::RemoveItem { menu, command } => apply_patch_op(
258 menu_bar,
259 index,
260 MenuBarPatchOp::RemoveAt {
261 menu,
262 at: ItemSelector::Anchor(ItemAnchor::Command(command)),
263 },
264 ),
265 MenuBarPatchOp::InsertItemBefore { menu, before, item } => {
266 let Some(items) = resolve_menu_items_mut(menu_bar, &menu) else {
267 return fail("menu path not found".to_string());
268 };
269 let before = ItemSelector::Anchor(before);
270 let Some(item_idx) = resolve_selector_index(items, &before) else {
271 return fail("before selector not found".to_string());
272 };
273 let insert_at = item_idx.min(items.len());
274 items.insert(insert_at, item);
275 Ok(())
276 }
277 MenuBarPatchOp::InsertItemAfter { menu, after, item } => {
278 let Some(items) = resolve_menu_items_mut(menu_bar, &menu) else {
279 return fail("menu path not found".to_string());
280 };
281 let after = ItemSelector::Anchor(after);
282 let Some(item_idx) = resolve_selector_index(items, &after) else {
283 return fail("after selector not found".to_string());
284 };
285 let insert_at = (item_idx + 1).min(items.len());
286 items.insert(insert_at, item);
287 Ok(())
288 }
289 MenuBarPatchOp::PrependItem { menu, item } => {
290 let Some(items) = resolve_menu_items_mut(menu_bar, &menu) else {
291 return fail("menu path not found".to_string());
292 };
293 items.insert(0, item);
294 Ok(())
295 }
296 MenuBarPatchOp::AppendItem { menu, item } => {
297 let Some(items) = resolve_menu_items_mut(menu_bar, &menu) else {
298 return fail("menu path not found".to_string());
299 };
300 items.push(item);
301 Ok(())
302 }
303 MenuBarPatchOp::MoveItemBefore {
304 menu,
305 command,
306 before,
307 } => apply_patch_op(
308 menu_bar,
309 index,
310 MenuBarPatchOp::MoveAtBefore {
311 menu,
312 at: ItemSelector::Anchor(ItemAnchor::Command(command)),
313 before: ItemSelector::Anchor(before),
314 },
315 ),
316 MenuBarPatchOp::MoveItemAfter {
317 menu,
318 command,
319 after,
320 } => apply_patch_op(
321 menu_bar,
322 index,
323 MenuBarPatchOp::MoveAtAfter {
324 menu,
325 at: ItemSelector::Anchor(ItemAnchor::Command(command)),
326 after: ItemSelector::Anchor(after),
327 },
328 ),
329 }
330}