1use 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
17type Shortcuts = HashMap<Shortcut, Span>;
19
20impl Checker {
21 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 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 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 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 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 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}