Skip to main content

fret_runtime/menu/
apply.rs

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}