Skip to main content

nifty_check/
elements.rs

1//! The built-in UI elements and the arguments each one takes. The checker dispatches on
2//! [`Element::kind`], and tools such as the language server read the same table.
3
4use std::borrow::Cow;
5use std::rc::Rc;
6
7use crate::hir::Type;
8
9/// A built-in element such as `label` or `row`.
10#[derive(Debug)]
11pub struct Element {
12    pub name: &'static str,
13    pub kind: ElementKind,
14    pub params: &'static [Param],
15    pub block: TrailingBlock,
16    /// Where it may be written.
17    pub places: &'static [Place],
18    pub summary: &'static str,
19}
20
21/// Where in a program an element may be written.
22#[derive(Clone, Copy, Debug, PartialEq, Eq)]
23pub enum Place {
24    /// Among the top-level declarations.
25    TopLevel,
26    /// In a window or view, among other elements.
27    Ui,
28    /// In a menu, among its items.
29    Menu,
30}
31
32const IN_UI: &[Place] = &[Place::Ui];
33
34/// Which element a table entry is, for the checker to dispatch on.
35#[derive(Clone, Copy, Debug, PartialEq, Eq)]
36pub enum ElementKind {
37    Window,
38    Column,
39    Row,
40    Scroll,
41    Label,
42    Button,
43    TextField,
44    Checkbox,
45    Switch,
46    Slider,
47    Picker,
48    NavItem,
49    Image,
50    Progress,
51    Spacer,
52    Divider,
53    ContextMenu,
54    DropTarget,
55    Menu,
56    MenuItem,
57}
58
59#[derive(Debug)]
60pub struct Param {
61    pub name: &'static str,
62    /// Positional parameters come first and have no label; the rest are always named.
63    pub positional: bool,
64    pub required: bool,
65    pub kind: ParamKind,
66}
67
68#[derive(Clone, Copy, Debug, PartialEq, Eq)]
69pub enum ParamKind {
70    /// Text that may change, such as `"{count} left"`.
71    Text,
72    /// Text written literally, without interpolation.
73    FixedText,
74    /// An `int` that may change, such as `item.percent`.
75    Number,
76    /// A `bool` that may change, such as `filter == "all"`.
77    Condition,
78    /// A list of text, such as `["png", "jpg"]`.
79    TextList,
80    /// A size in points, written as a number.
81    Points,
82    /// A whole number written literally, such as `-10`.
83    FixedNumber,
84    /// A keyboard shortcut written literally, such as `"shift+n"`.
85    Shortcut,
86    /// A window's name, such as `settings`.
87    Window,
88    /// `true` or `false`, written literally.
89    Flag,
90    /// A two-way binding such as `$draft`.
91    Binding(BoundType),
92    /// One of a fixed set of `.name` values.
93    Option(Options),
94}
95
96#[derive(Clone, Copy, Debug, PartialEq, Eq)]
97pub enum BoundType {
98    String,
99    Bool,
100    Int,
101    /// A list of strings, `[string]`.
102    Strings,
103}
104
105impl BoundType {
106    #[must_use]
107    pub fn ty(self) -> Type {
108        match self {
109            Self::String => Type::String,
110            Self::Bool => Type::Bool,
111            Self::Int => Type::Int,
112            Self::Strings => Type::List(Rc::new(Type::String)),
113        }
114    }
115
116    /// How the bound type is written, such as `int` or `[string]`.
117    #[must_use]
118    pub fn name(self) -> Cow<'static, str> {
119        match self.ty() {
120            Type::List(item) => Cow::Owned(format!("[{}]", primitive_name(&item))),
121            primitive => Cow::Borrowed(primitive_name(&primitive)),
122        }
123    }
124}
125
126fn primitive_name(ty: &Type) -> &'static str {
127    match ty.primitive_name() {
128        Some(name) => name,
129        None => unreachable!("bindings are to primitive types or lists of them"),
130    }
131}
132
133/// A set of values written as `.name`; [`Options::names`] lists them.
134#[derive(Clone, Copy, Debug, PartialEq, Eq)]
135pub enum Options {
136    Alignment,
137    Justification,
138    TextStyle,
139    TextColor,
140    TextAlignment,
141    ButtonStyle,
142    Surface,
143    Icon,
144    Backdrop,
145}
146
147/// What the `{ … }` after an element holds.
148#[derive(Clone, Copy, Debug, PartialEq, Eq)]
149pub enum TrailingBlock {
150    None,
151    /// Child elements.
152    Children,
153    /// Statements to run when the element is used.
154    Action,
155    /// Statements to run when the element is used, which it may go without.
156    OptionalAction,
157    /// A menu's items.
158    Menu,
159}
160
161impl ParamKind {
162    /// How the expected value reads in signatures, such as `number` or `$bool`.
163    #[must_use]
164    pub fn describe(self) -> Cow<'static, str> {
165        Cow::Borrowed(match self {
166            Self::Text => "string",
167            Self::FixedText => "fixed string",
168            Self::Number => "int",
169            Self::TextList => "[string]",
170            Self::Points => "number",
171            Self::FixedNumber => "fixed int",
172            Self::Shortcut => "shortcut",
173            Self::Window => "window name",
174            Self::Flag | Self::Condition => "bool",
175            Self::Option(Options::Alignment) => "alignment",
176            Self::Option(Options::Justification) => "justification",
177            Self::Option(Options::TextStyle) => "text style",
178            Self::Option(Options::TextColor) => "text color",
179            Self::Option(Options::TextAlignment) => "text alignment",
180            Self::Option(Options::ButtonStyle) => "button style",
181            Self::Option(Options::Surface) => "surface",
182            Self::Option(Options::Icon) => "icon",
183            Self::Option(Options::Backdrop) => "backdrop",
184            Self::Binding(bound) => return Cow::Owned(format!("${}", bound.name())),
185        })
186    }
187}
188
189const STACK_PARAMS: &[Param] = &[
190    named("spacing", ParamKind::Points),
191    named("padding", ParamKind::Points),
192    named("align", ParamKind::Option(Options::Alignment)),
193    named("justify", ParamKind::Option(Options::Justification)),
194    named("surface", ParamKind::Option(Options::Surface)),
195    named("width", ParamKind::Points),
196    named("max_width", ParamKind::Points),
197    named("grow", ParamKind::Flag),
198];
199
200/// A scroll area always takes the space its parent has left, so it takes every stack
201/// setting but `grow`, which comes last.
202const SCROLL_PARAMS: &[Param] = match STACK_PARAMS.split_last() {
203    Some((_grow, rest)) => rest,
204    None => STACK_PARAMS,
205};
206
207pub const ELEMENTS: &[Element] = &[
208    Element {
209        name: WINDOW,
210        kind: ElementKind::Window,
211        params: &[
212            positional("title", ParamKind::FixedText),
213            named("width", ParamKind::Points),
214            named("height", ParamKind::Points),
215            named("backdrop", ParamKind::Option(Options::Backdrop)),
216            named(OPEN, ParamKind::Flag),
217        ],
218        block: TrailingBlock::Children,
219        places: &[Place::TopLevel],
220        summary: "A window; only at the top level. Name it to open and close it from code.",
221    },
222    Element {
223        name: "column",
224        kind: ElementKind::Column,
225        params: STACK_PARAMS,
226        block: TrailingBlock::Children,
227        places: IN_UI,
228        summary: "Stacks its children top to bottom.",
229    },
230    Element {
231        name: "row",
232        kind: ElementKind::Row,
233        params: STACK_PARAMS,
234        block: TrailingBlock::Children,
235        places: IN_UI,
236        summary: "Stacks its children left to right.",
237    },
238    Element {
239        name: "scroll",
240        kind: ElementKind::Scroll,
241        params: SCROLL_PARAMS,
242        block: TrailingBlock::Children,
243        places: IN_UI,
244        summary: "Stacks its children top to bottom in the space left, scrolling them when they're too tall.",
245    },
246    Element {
247        name: "label",
248        kind: ElementKind::Label,
249        params: &[
250            positional("text", ParamKind::Text),
251            named("style", ParamKind::Option(Options::TextStyle)),
252            named("color", ParamKind::Option(Options::TextColor)),
253            named("align", ParamKind::Option(Options::TextAlignment)),
254            named("selectable", ParamKind::Flag),
255        ],
256        block: TrailingBlock::None,
257        places: IN_UI,
258        summary: "Shows text.",
259    },
260    Element {
261        name: "button",
262        kind: ElementKind::Button,
263        params: &[
264            positional("title", ParamKind::Text),
265            named("style", ParamKind::Option(Options::ButtonStyle)),
266            named("icon", ParamKind::Option(Options::Icon)),
267            named("selected", ParamKind::Condition),
268        ],
269        block: TrailingBlock::Action,
270        places: IN_UI,
271        summary: "Runs its block when clicked.",
272    },
273    Element {
274        name: "text_field",
275        kind: ElementKind::TextField,
276        params: &[
277            positional("value", ParamKind::Binding(BoundType::String)),
278            named("placeholder", ParamKind::FixedText),
279            named("lines", ParamKind::FixedNumber),
280        ],
281        block: TrailingBlock::OptionalAction,
282        places: IN_UI,
283        summary: "Edits bound text, on one line or several; its block runs on Return.",
284    },
285    Element {
286        name: "checkbox",
287        kind: ElementKind::Checkbox,
288        params: &[
289            required("checked", ParamKind::Binding(BoundType::Bool)),
290            named("title", ParamKind::Text),
291        ],
292        block: TrailingBlock::None,
293        places: IN_UI,
294        summary: "Toggles a bound `bool`.",
295    },
296    Element {
297        name: "switch",
298        kind: ElementKind::Switch,
299        params: &[
300            required("on", ParamKind::Binding(BoundType::Bool)),
301            named("title", ParamKind::Text),
302        ],
303        block: TrailingBlock::None,
304        places: IN_UI,
305        summary: "Turns a bound `bool` on and off, with a switch at the end of its title.",
306    },
307    Element {
308        name: "slider",
309        kind: ElementKind::Slider,
310        params: &[
311            positional("value", ParamKind::Binding(BoundType::Int)),
312            named("min", ParamKind::FixedNumber),
313            named("max", ParamKind::FixedNumber),
314        ],
315        block: TrailingBlock::None,
316        places: IN_UI,
317        summary: "Picks a bound `int` between `min` and `max` by dragging.",
318    },
319    Element {
320        name: "picker",
321        kind: ElementKind::Picker,
322        params: &[
323            positional("selection", ParamKind::Binding(BoundType::String)),
324            required("options", ParamKind::TextList),
325        ],
326        block: TrailingBlock::None,
327        places: IN_UI,
328        summary: "Picks one of a list of strings from a pop-up menu, into a bound `string`.",
329    },
330    Element {
331        name: "nav_item",
332        kind: ElementKind::NavItem,
333        params: &[
334            positional("title", ParamKind::Text),
335            required("value", ParamKind::Text),
336            required("selection", ParamKind::Binding(BoundType::String)),
337            named("icon", ParamKind::Option(Options::Icon)),
338            named("badge", ParamKind::Text),
339        ],
340        block: TrailingBlock::None,
341        places: IN_UI,
342        summary: "A sidebar row, selected while its binding holds its value.",
343    },
344    Element {
345        name: "image",
346        kind: ElementKind::Image,
347        params: &[
348            positional("path", ParamKind::Text),
349            named("width", ParamKind::Points),
350            named("height", ParamKind::Points),
351        ],
352        block: TrailingBlock::None,
353        places: IN_UI,
354        summary: "Shows the image in a file, keeping its shape.",
355    },
356    Element {
357        name: "progress",
358        kind: ElementKind::Progress,
359        params: &[named("value", ParamKind::Number)],
360        block: TrailingBlock::None,
361        places: IN_UI,
362        summary: "Shows how far along work is, as a percentage; without `value`, that it's busy.",
363    },
364    Element {
365        name: "spacer",
366        kind: ElementKind::Spacer,
367        params: &[],
368        block: TrailingBlock::None,
369        places: IN_UI,
370        summary: "Takes the remaining space.",
371    },
372    Element {
373        name: "divider",
374        kind: ElementKind::Divider,
375        params: &[],
376        block: TrailingBlock::None,
377        places: &[Place::Ui, Place::Menu],
378        summary: "A line between elements, or between a menu's items.",
379    },
380    Element {
381        name: "context_menu",
382        kind: ElementKind::ContextMenu,
383        params: &[],
384        block: TrailingBlock::Menu,
385        places: IN_UI,
386        summary: "A menu that shows when the stack it's in is right-clicked.",
387    },
388    Element {
389        name: "drop_target",
390        kind: ElementKind::DropTarget,
391        params: &[positional(
392            "dropped",
393            ParamKind::Binding(BoundType::Strings),
394        )],
395        block: TrailingBlock::OptionalAction,
396        places: IN_UI,
397        summary: "Takes files and links dropped on the stack it's in, writing their paths and addresses into the bound list, then runs its block.",
398    },
399    Element {
400        name: MENU,
401        kind: ElementKind::Menu,
402        params: &[positional("title", ParamKind::FixedText)],
403        block: TrailingBlock::Menu,
404        places: &[Place::TopLevel, Place::Menu],
405        summary: "A menu of the app's menu bar, or a submenu in one.",
406    },
407    Element {
408        name: "menu_item",
409        kind: ElementKind::MenuItem,
410        params: &[
411            positional("title", ParamKind::FixedText),
412            named("shortcut", ParamKind::Shortcut),
413        ],
414        block: TrailingBlock::Action,
415        places: &[Place::Menu],
416        summary: "A menu's item, which runs its block when chosen or its shortcut is pressed.",
417    },
418];
419
420/// The element called `name`, if it is built in.
421#[must_use]
422pub fn element(name: &str) -> Option<&'static Element> {
423    ELEMENTS.iter().find(|element| element.name == name)
424}
425
426/// The element that declares a window, which only appears at the top level.
427pub const WINDOW: &str = "window";
428/// The window setting that says whether it opens when the app starts.
429pub const OPEN: &str = "open";
430/// The element that declares a menu, at the top level or in another menu.
431pub const MENU: &str = "menu";
432
433pub(crate) const fn positional(name: &'static str, kind: ParamKind) -> Param {
434    Param {
435        name,
436        positional: true,
437        required: true,
438        kind,
439    }
440}
441
442pub(crate) const fn named(name: &'static str, kind: ParamKind) -> Param {
443    Param {
444        name,
445        positional: false,
446        required: false,
447        kind,
448    }
449}
450
451pub(crate) const fn required(name: &'static str, kind: ParamKind) -> Param {
452    Param {
453        required: true,
454        ..named(name, kind)
455    }
456}