Skip to main content

nifty_check/checker/
menu.rs

1//! Menus: the menu bar's menus, their items and submenus, and the items' shortcuts.
2
3use std::collections::HashMap;
4use std::mem;
5
6use nifty_runtime::{Menu, MenuEntry, Shortcut};
7use nifty_syntax::ast;
8use nifty_syntax::{Diagnostic, Span};
9
10use super::Checker;
11use super::args::Args;
12use super::local_types;
13use super::ui::{ElementCall, element_call};
14use crate::elements::{ElementKind, MENU, Place};
15use crate::hir::{Block, MenuBar, Node};
16
17/// The shortcuts given so far, and where, so each one runs one item.
18type Shortcuts = HashMap<Shortcut, Span>;
19
20impl Checker {
21    /// The menu bar's menus, written as top-level `menu(…)` calls, in order.
22    pub(super) fn menu_bar(&mut self, menus: &[ElementCall]) -> MenuBar {
23        let mut shortcuts = Shortcuts::new();
24        let menus = menus
25            .iter()
26            .filter_map(|call| self.menu(call, Some(&mut shortcuts)))
27            .collect();
28        MenuBar {
29            menus,
30            locals: local_types(mem::take(&mut self.body.locals)),
31        }
32    }
33
34    /// A `context_menu`, whose items take no shortcuts: they act on what was clicked, and
35    /// the stack a menu is in can show many times.
36    pub(super) fn context_menu(
37        &mut self,
38        name: &str,
39        block: Option<&ast::Block>,
40        span: Span,
41    ) -> Option<Node> {
42        let block = self.menu_block(name, block, span)?;
43        Some(Node::ContextMenu {
44            entries: self.menu_entries(&block.stmts, None),
45        })
46    }
47
48    /// The entries of a menu's block: items, separators and submenus. Items may take
49    /// shortcuts when there are `shortcuts` to add them to.
50    fn menu_entries(
51        &mut self,
52        stmts: &[ast::Stmt],
53        mut shortcuts: Option<&mut Shortcuts>,
54    ) -> Vec<MenuEntry<Block>> {
55        stmts
56            .iter()
57            .filter_map(|stmt| self.menu_entry(stmt, shortcuts.as_deref_mut()))
58            .collect()
59    }
60
61    fn menu_entry(
62        &mut self,
63        stmt: &ast::Stmt,
64        shortcuts: Option<&mut Shortcuts>,
65    ) -> Option<MenuEntry<Block>> {
66        let call = element_call(stmt).filter(|call| call.element.places.contains(&Place::Menu));
67        let Some(call) = call else {
68            self.error(
69                format!("only `menu_item`, `{MENU}` and `divider` can go in a menu"),
70                stmt.span,
71            );
72            return None;
73        };
74        match call.element.kind {
75            ElementKind::Menu => self.menu(&call, shortcuts).map(MenuEntry::Submenu),
76            ElementKind::MenuItem => self.menu_item(&call, shortcuts),
77            _ => {
78                let args = Args::built_in(self, call.element.name, call.element.params, call.args);
79                args.finish(self);
80                self.no_block(call.element.name, call.block);
81                Some(MenuEntry::Separator)
82            }
83        }
84    }
85
86    fn menu(
87        &mut self,
88        call: &ElementCall,
89        shortcuts: Option<&mut Shortcuts>,
90    ) -> Option<Menu<Block>> {
91        let name = call.element.name;
92        let mut args = Args::built_in(self, name, call.element.params, call.args);
93        let title = self.fixed_title(name, args.positional(), call.span);
94        args.finish(self);
95        let block = self.menu_block(name, call.block, call.span)?;
96        let entries = self.menu_entries(&block.stmts, shortcuts);
97        Some(Menu {
98            title: title?,
99            entries,
100        })
101    }
102
103    /// The block holding a menu's items, which every menu needs.
104    fn menu_block<'a>(
105        &mut self,
106        name: &str,
107        block: Option<&'a ast::Block>,
108        span: Span,
109    ) -> Option<&'a ast::Block> {
110        if block.is_none() {
111            self.error(
112                format!("`{name}` needs a `{{ … }}` block with its items"),
113                span,
114            );
115        }
116        block
117    }
118
119    fn menu_item(
120        &mut self,
121        call: &ElementCall,
122        shortcuts: Option<&mut Shortcuts>,
123    ) -> Option<MenuEntry<Block>> {
124        let name = call.element.name;
125        let mut args = Args::built_in(self, name, call.element.params, call.args);
126        let title = self.fixed_title(name, args.positional(), call.span);
127        let shortcut = args
128            .named("shortcut")
129            .and_then(|shortcut| self.shortcut(shortcut, shortcuts));
130        args.finish(self);
131        let Some(block) = call.block else {
132            self.error(
133                format!("`{name}` needs a `{{ … }}` block with what it does"),
134                call.span,
135            );
136            return None;
137        };
138        let action = self.action(block);
139        Some(MenuEntry::Item {
140            title: title?,
141            shortcut,
142            action,
143        })
144    }
145
146    /// A menu's or item's title, which is written literally.
147    fn fixed_title(&mut self, name: &str, title: Option<&ast::Expr>, span: Span) -> Option<String> {
148        if let Some(title) = title {
149            return self.const_string(title);
150        }
151        self.error(
152            format!("`{name}` needs a title, such as `{name}(\"File\")`"),
153            span,
154        );
155        None
156    }
157
158    /// A shortcut written literally, which no other item and no system command uses, in
159    /// a menu whose items take `shortcuts`.
160    fn shortcut(
161        &mut self,
162        expr: &ast::Expr,
163        shortcuts: Option<&mut Shortcuts>,
164    ) -> Option<Shortcut> {
165        let Some(shortcuts) = shortcuts else {
166            self.error(
167                format!(
168                    "a context menu's items take no shortcut; give it to an item of a top-level `{MENU}(…)`"
169                ),
170                expr.span,
171            );
172            return None;
173        };
174        let text = self.const_string(expr)?;
175        let shortcut = match Shortcut::parse(&text) {
176            Ok(shortcut) => shortcut,
177            Err(error) => {
178                self.error(error.to_string(), expr.span);
179                return None;
180            }
181        };
182        if let Some(command) = shortcut.taken_by() {
183            self.error(
184                format!(
185                    "`{shortcut}` is the system's shortcut for {}; choose another",
186                    command.title()
187                ),
188                expr.span,
189            );
190            return None;
191        }
192        if let Some(&first) = shortcuts.get(&shortcut) {
193            self.diagnostics.push(
194                Diagnostic::new(
195                    format!("another menu item already uses `{shortcut}`"),
196                    expr.span,
197                )
198                .with_note("it's used here", first),
199            );
200            return None;
201        }
202        shortcuts.insert(shortcut, expr.span);
203        Some(shortcut)
204    }
205}