Skip to main content

nifty_check/checker/
ui.rs

1//! Window and view bodies: elements, conditional and repeated UI, and what each dynamic part
2//! of the UI reads.
3
4use std::rc::Rc;
5
6use nifty_runtime::{
7    Axis, ButtonStyle, Icon, ImageSize, LabelStyle, Size, StackStyle, ToggleStyle,
8};
9use nifty_syntax::ast::{self, Arg, ElseBranch, ExprKind as Ast, IfStmt, StmtKind, StringPart};
10use nifty_syntax::{Diagnostic, Span};
11
12use super::args::Args;
13use super::consts::{
14    ALIGNMENTS, BACKDROPS, BUTTON_STYLES, ICONS, JUSTIFICATIONS, SURFACES, TEXT_ALIGNMENTS,
15    TEXT_COLORS, TEXT_STYLES,
16};
17use super::env::Context;
18use super::{Checker, Global};
19use crate::elements::{self, BoundType, Element, ElementKind, MENU, OPEN, WINDOW};
20use crate::hir::{
21    Binding, Block, Dynamic, NavItem, Node, Nodes, PlaceRoot, StateSet, Type, WindowDef,
22};
23
24const DEFAULT_WINDOW_SIZE: Size = Size::new(480.0, 320.0);
25
26/// Whether `path` names a file wherever the app runs: from the root on macOS or Windows,
27/// or the home folder. An empty path names no file, which shows nothing.
28fn is_absolute(path: &str) -> bool {
29    let mut chars = path.chars();
30    let drive_letter = matches!(
31        (chars.next(), chars.next(), chars.next()),
32        (Some(letter), Some(':'), Some('/' | '\\')) if letter.is_ascii_alphabetic()
33    );
34    let home = path == "~" || path.starts_with("~/") || path.starts_with("~\\");
35    path.is_empty() || drive_letter || home || path.starts_with(['/', '\\'])
36}
37/// The most lines a text field can be tall, which is more than fit a screen.
38const MAX_LINES: u32 = 100;
39/// The range a slider covers when its code doesn't say: a percentage.
40const DEFAULT_SLIDER_RANGE: (i64, i64) = (0, 100);
41
42/// A call to a built-in element, as written.
43pub(super) struct ElementCall<'a> {
44    pub element: &'static Element,
45    pub args: &'a [Arg],
46    pub block: Option<&'a ast::Block>,
47    pub span: Span,
48}
49
50/// The built-in element `stmt` calls, if it is a call to one by name.
51pub(super) fn element_call(stmt: &ast::Stmt) -> Option<ElementCall<'_>> {
52    let StmtKind::Expr(ast::Expr {
53        kind:
54            Ast::Call {
55                callee,
56                args,
57                trailing_block,
58            },
59        span,
60    }) = &stmt.kind
61    else {
62        return None;
63    };
64    let Ast::Name(name) = &callee.kind else {
65        return None;
66    };
67    Some(ElementCall {
68        element: elements::element(name)?,
69        args,
70        block: trailing_block.as_ref(),
71        span: *span,
72    })
73}
74
75/// The calls among `stmts` to elements that cover their whole stack, of `kind`.
76fn covering(stmts: &[ast::Stmt], kind: ElementKind) -> impl Iterator<Item = ElementCall<'_>> {
77    stmts
78        .iter()
79        .filter_map(element_call)
80        .filter(move |call| call.element.kind == kind)
81}
82
83/// The elements that cover their whole stack, rather than taking a place in it.
84const COVERING: [ElementKind; 2] = [ElementKind::ContextMenu, ElementKind::DropTarget];
85
86impl Checker {
87    /// A window's definition; one without a `named` window must start open, since no code
88    /// can open it.
89    pub(super) fn window(&mut self, call: &ElementCall, named: bool) -> Option<WindowDef> {
90        let window = call.element;
91        let (trailing_block, span) = (call.block, &call.span);
92        let mut args = Args::built_in(self, window.name, window.params, call.args);
93        let title = if let Some(title) = args.positional() {
94            self.const_string(title)
95        } else {
96            self.error(
97                "`window` needs a title, such as `window(\"My App\")`",
98                *span,
99            );
100            None
101        };
102        let width = args
103            .named("width")
104            .and_then(|width| self.const_points(width));
105        let height = args
106            .named("height")
107            .and_then(|height| self.const_points(height));
108        let backdrop = args
109            .named("backdrop")
110            .and_then(|backdrop| self.const_member(backdrop, BACKDROPS))
111            .unwrap_or_default();
112        let starts_open = args
113            .named(OPEN)
114            .and_then(|open| self.const_bool(open))
115            .unwrap_or(true);
116        if !starts_open && !named {
117            self.error(
118                format!(
119                    "a window that starts closed needs a name to open it by, such as `{WINDOW} settings(…)`"
120                ),
121                *span,
122            );
123        }
124        args.finish(self);
125        let Some(block) = trailing_block else {
126            self.error("`window` needs a `{ … }` block with its content", *span);
127            return None;
128        };
129
130        let nodes = self.ui_nodes(&block.stmts);
131        let (style, children) = match nodes.as_slice() {
132            [
133                Node::Stack {
134                    style,
135                    children,
136                    scrolls: false,
137                },
138            ] => (*style, Rc::clone(children)),
139            _ => (StackStyle::new(Axis::Vertical), nodes.into()),
140        };
141        Some(WindowDef {
142            title: title?,
143            starts_open,
144            size: Size::new(
145                width.unwrap_or(DEFAULT_WINDOW_SIZE.width),
146                height.unwrap_or(DEFAULT_WINDOW_SIZE.height),
147            ),
148            backdrop,
149            style,
150            children,
151            locals: self
152                .body
153                .locals
154                .iter()
155                .map(|local| local.ty.clone())
156                .collect(),
157        })
158    }
159
160    pub(super) fn ui_block(&mut self, stmts: &[ast::Stmt]) -> Nodes {
161        self.ui_nodes(stmts).into()
162    }
163
164    fn ui_nodes(&mut self, stmts: &[ast::Stmt]) -> Vec<Node> {
165        self.body.push_scope();
166        let nodes = stmts.iter().filter_map(|stmt| self.ui_stmt(stmt)).collect();
167        self.body.pop_scope();
168        self.one_covering_each(stmts);
169        nodes
170    }
171
172    fn ui_stmt(&mut self, stmt: &ast::Stmt) -> Option<Node> {
173        match &stmt.kind {
174            StmtKind::Expr(ast::Expr {
175                kind:
176                    Ast::Call {
177                        callee,
178                        args,
179                        trailing_block,
180                    },
181                span,
182            }) if matches!(callee.kind, Ast::Name(_)) => {
183                let Ast::Name(name) = &callee.kind else {
184                    return None;
185                };
186                self.element(name, args, trailing_block.as_ref(), *span)
187            }
188            StmtKind::If(if_stmt) => Some(self.ui_if(if_stmt, stmt.span)),
189            StmtKind::For {
190                index,
191                item,
192                iterable,
193                body,
194            } => {
195                let (head, deps) = self.dynamic(stmt.span, |checker| {
196                    checker.loop_head(iterable, index.is_some())
197                });
198                self.none_covering(&body.stmts);
199                let body = self.in_loop(index.as_ref(), item, &head, |checker| {
200                    checker.ui_block(&body.stmts)
201                });
202                Some(Node::For {
203                    head: Rc::new(head),
204                    body,
205                    deps,
206                })
207            }
208            _ => {
209                self.error(
210                    "only elements, `if` and `for` can appear here; put logic in a function or a button's action",
211                    stmt.span,
212                );
213                None
214            }
215        }
216    }
217
218    fn ui_if(&mut self, if_stmt: &IfStmt, span: Span) -> Node {
219        let mut conditions = Vec::new();
220        let mut bodies = Vec::new();
221        let mut otherwise = None;
222        let mut current = if_stmt;
223        loop {
224            conditions.push(&current.condition);
225            bodies.push(&current.then_block);
226            match &current.else_branch {
227                None => break,
228                Some(ElseBranch::Block(block)) => {
229                    otherwise = Some(block);
230                    break;
231                }
232                Some(ElseBranch::If(next)) => current = next,
233            }
234        }
235        let (conditions, deps) = self.dynamic(span, |checker| {
236            conditions
237                .into_iter()
238                .map(|condition| checker.expect(condition, &Type::Bool))
239                .collect::<Vec<_>>()
240        });
241        let branches = conditions
242            .into_iter()
243            .zip(bodies)
244            .map(|(condition, body)| (Rc::new(condition), self.ui_block(&body.stmts)))
245            .collect();
246        let otherwise =
247            otherwise.map_or_else(|| Vec::new().into(), |block| self.ui_block(&block.stmts));
248        Node::If {
249            branches,
250            otherwise,
251            deps,
252        }
253    }
254
255    fn element(
256        &mut self,
257        name: &str,
258        args: &[Arg],
259        block: Option<&ast::Block>,
260        span: Span,
261    ) -> Option<Node> {
262        let Some(element) = elements::element(name) else {
263            let args = Args::new(self, format!("`{name}`"), args);
264            return self.view_call(name, args, block, span);
265        };
266        self.built_in_element(element, args, block, span)
267    }
268
269    fn built_in_element(
270        &mut self,
271        element: &'static Element,
272        args: &[Arg],
273        block: Option<&ast::Block>,
274        span: Span,
275    ) -> Option<Node> {
276        let name = element.name;
277        let mut args = Args::built_in(self, name, element.params, args);
278        let node = match element.kind {
279            ElementKind::Column | ElementKind::Row | ElementKind::Scroll => {
280                let axis = if element.kind == ElementKind::Row {
281                    Axis::Horizontal
282                } else {
283                    Axis::Vertical
284                };
285                let scrolls = element.kind == ElementKind::Scroll;
286                let style = self.stack_style(axis, scrolls, &mut args);
287                let children =
288                    block.map_or_else(|| Vec::new().into(), |block| self.ui_block(&block.stmts));
289                args.finish(self);
290                return Some(Node::Stack {
291                    style,
292                    children,
293                    scrolls,
294                });
295            }
296            ElementKind::Label => {
297                let text = self.required_text(name, args.positional(), span)?;
298                let style = self.label_style(&mut args);
299                Node::Label {
300                    text: Rc::new(text),
301                    style,
302                }
303            }
304            ElementKind::Button => {
305                let node = self.button(name, &mut args, block, span);
306                args.finish(self);
307                return node;
308            }
309            ElementKind::TextField => {
310                let node = self.text_field(name, &mut args, block, span);
311                args.finish(self);
312                return node;
313            }
314            ElementKind::Checkbox => {
315                self.toggle(name, "checked", ToggleStyle::Checkbox, &mut args, span)?
316            }
317            ElementKind::Switch => self.toggle(name, "on", ToggleStyle::Switch, &mut args, span)?,
318            ElementKind::Slider => self.slider(name, &mut args, span)?,
319            ElementKind::Picker => self.picker(name, &mut args, span)?,
320            ElementKind::NavItem => self.nav_item(name, &mut args, span)?,
321            ElementKind::Image => self.image(name, &mut args, span)?,
322            ElementKind::Progress => Node::Progress {
323                value: args.named("value").map(|value| Rc::new(self.number(value))),
324            },
325            ElementKind::Spacer => Node::Spacer,
326            ElementKind::Divider => Node::Divider,
327            ElementKind::ContextMenu => {
328                args.finish(self);
329                return self.context_menu(name, block, span);
330            }
331            ElementKind::DropTarget => {
332                let strings = BoundType::Strings.ty();
333                let dropped = self.binding(name, None, args.positional(), &strings, span);
334                args.finish(self);
335                return Some(Node::DropTarget {
336                    dropped: Rc::new(dropped?),
337                    action: block.map(|block| self.action(block)),
338                });
339            }
340            ElementKind::Window => {
341                self.error("a window can only be declared at the top level", span);
342                return None;
343            }
344            ElementKind::Menu | ElementKind::MenuItem => {
345                self.error(
346                    format!("`{name}` goes in the app's menu bar: in a top-level `{MENU}(…)`"),
347                    span,
348                );
349                return None;
350            }
351        };
352        args.finish(self);
353        self.no_block(name, block);
354        Some(node)
355    }
356
357    /// A stack's settings; a scroll area always grows, so it isn't asked.
358    fn stack_style(&mut self, axis: Axis, scrolls: bool, args: &mut Args) -> StackStyle {
359        let defaults = StackStyle::new(axis);
360        StackStyle {
361            axis,
362            spacing: args
363                .named("spacing")
364                .and_then(|spacing| self.const_points(spacing))
365                .unwrap_or(defaults.spacing),
366            padding: args
367                .named("padding")
368                .and_then(|padding| self.const_points(padding))
369                .unwrap_or(defaults.padding),
370            align: args
371                .named("align")
372                .and_then(|align| self.const_member(align, ALIGNMENTS))
373                .unwrap_or(defaults.align),
374            justify: args
375                .named("justify")
376                .and_then(|justify| self.const_member(justify, JUSTIFICATIONS))
377                .unwrap_or(defaults.justify),
378            surface: args
379                .named("surface")
380                .and_then(|surface| self.const_member(surface, SURFACES))
381                .unwrap_or(defaults.surface),
382            grow: scrolls
383                || args
384                    .named("grow")
385                    .and_then(|grow| self.const_bool(grow))
386                    .unwrap_or(defaults.grow),
387            width: args
388                .named("width")
389                .and_then(|width| self.const_points(width))
390                .unwrap_or(defaults.width),
391            max_width: args
392                .named("max_width")
393                .and_then(|max_width| self.const_points(max_width))
394                .unwrap_or(defaults.max_width),
395        }
396    }
397
398    fn image(&mut self, name: &str, args: &mut Args, span: Span) -> Option<Node> {
399        let path = args.positional();
400        if let Some(path) = path
401            && let Ast::String(parts) = &path.kind
402            && let [StringPart::Text(text)] = &parts[..]
403            && !is_absolute(text)
404        {
405            self.error(
406                format!("`{name}` needs a full path, or one starting with `~`; images that come with an app aren't supported yet"),
407                path.span,
408            );
409        }
410        let path = self.required_text(name, path, span)?;
411        let size = ImageSize {
412            width: args
413                .named("width")
414                .and_then(|width| self.const_points(width))
415                .unwrap_or(ImageSize::FIT),
416            height: args
417                .named("height")
418                .and_then(|height| self.const_points(height))
419                .unwrap_or(ImageSize::FIT),
420        };
421        Some(Node::Image {
422            path: Rc::new(path),
423            size,
424        })
425    }
426
427    fn text_field(
428        &mut self,
429        name: &str,
430        args: &mut Args,
431        block: Option<&ast::Block>,
432        span: Span,
433    ) -> Option<Node> {
434        let value = self.binding(name, None, args.positional(), &Type::String, span);
435        let placeholder = args.named("placeholder");
436        let placeholder_text = placeholder.and_then(|placeholder| self.const_string(placeholder));
437        let lines = args
438            .named("lines")
439            .and_then(|lines| self.line_count(lines))
440            .unwrap_or(1);
441        if let Some(placeholder) = placeholder
442            && lines > 1
443        {
444            self.error(
445                format!("`{name}` shows a placeholder only on one line, with no `lines:`"),
446                placeholder.span,
447            );
448        }
449        if let Some(block) = block
450            && lines > 1
451        {
452            self.error(
453                format!("`{name}` takes Return as a new line when it has `lines:`, so it can't take a `{{ … }}` block to run on Return"),
454                block.span,
455            );
456        }
457        let action = block.map(|block| self.action(block));
458        Some(Node::TextField {
459            value: Rc::new(value?),
460            placeholder: placeholder_text.unwrap_or_default(),
461            lines,
462            action,
463        })
464    }
465
466    /// How many lines a text field is tall: at least one, and no more than fit a screen.
467    fn line_count(&mut self, expr: &ast::Expr) -> Option<u32> {
468        let count = self.const_number(expr)?;
469        let lines = u32::try_from(count)
470            .ok()
471            .filter(|lines| (1..=MAX_LINES).contains(lines));
472        if lines.is_none() {
473            self.error(format!("this must be between 1 and {MAX_LINES}"), expr.span);
474        }
475        lines
476    }
477
478    fn button(
479        &mut self,
480        name: &str,
481        args: &mut Args,
482        block: Option<&ast::Block>,
483        span: Span,
484    ) -> Option<Node> {
485        let title = self.required_text(name, args.positional(), span)?;
486        let style = args
487            .named("style")
488            .and_then(|style| self.const_member(style, BUTTON_STYLES))
489            .unwrap_or(ButtonStyle::Normal);
490        let icon = self.icon(args);
491        let selected = args.named("selected").map(|selected| {
492            if style == ButtonStyle::Primary {
493                self.error(
494                    "a `.primary` button is the window's default action, so it can't be `selected`; pick another style",
495                    selected.span,
496                );
497            }
498            Rc::new(self.condition(selected))
499        });
500        let Some(block) = block else {
501            self.error("`button` needs a `{ … }` block with what it does", span);
502            return None;
503        };
504        let action = self.action(block);
505        Some(Node::Button {
506            title: Rc::new(title),
507            style,
508            icon,
509            selected,
510            action,
511        })
512    }
513
514    /// A `checkbox` or `switch`, whose binding is labelled `label`.
515    fn toggle(
516        &mut self,
517        name: &str,
518        label: &str,
519        style: ToggleStyle,
520        args: &mut Args,
521        span: Span,
522    ) -> Option<Node> {
523        let checked = self.binding(name, Some(label), args.named(label), &Type::Bool, span);
524        let title = args
525            .named("title")
526            .map(|title| Rc::new(self.text(name, title)));
527        Some(Node::Checkbox {
528            checked: Rc::new(checked?),
529            title,
530            style,
531        })
532    }
533
534    fn slider(&mut self, name: &str, args: &mut Args, span: Span) -> Option<Node> {
535        let value = self.binding(name, None, args.positional(), &Type::Int, span);
536        let (default_min, default_max) = DEFAULT_SLIDER_RANGE;
537        let min = args
538            .named("min")
539            .map(|min| (min.span, self.const_number(min)));
540        let max = args
541            .named("max")
542            .map(|max| (max.span, self.const_number(max)));
543        let (min_span, min) = min.unwrap_or((span, Some(default_min)));
544        let (max_span, max) = max.unwrap_or((span, Some(default_max)));
545        let (min, max) = (min?, max?);
546        if min > max {
547            self.error(
548                format!("`{name}` needs `min` no higher than `max`, but {min} is above {max}"),
549                min_span.to(max_span),
550            );
551            return None;
552        }
553        Some(Node::Slider {
554            value: Rc::new(value?),
555            min,
556            max,
557        })
558    }
559
560    fn picker(&mut self, name: &str, args: &mut Args, span: Span) -> Option<Node> {
561        let selection = self.binding(name, None, args.positional(), &Type::String, span);
562        let options = args
563            .named("options")
564            .map(|options| self.shown(options, &Type::List(Rc::new(Type::String))));
565        if options.is_none() {
566            self.error(
567                format!("`{name}` needs `options:`, the strings to pick from, such as `options: [\"Low\", \"High\"]`"),
568                span,
569            );
570        }
571        let (selection, options) = (selection?, options?);
572        let mut deps = selection.deps.clone();
573        deps.extend(&options.deps);
574        Some(Node::Picker {
575            selection: Rc::new(selection),
576            options: Rc::new(options),
577            deps,
578        })
579    }
580
581    fn nav_item(&mut self, name: &str, args: &mut Args, span: Span) -> Option<Node> {
582        let title = self.required_text(name, args.positional(), span);
583        let value = args.named("value").map(|value| self.text(name, value));
584        if value.is_none() {
585            self.error(
586                format!("`{name}` needs `value:`, what `selection` holds while it's picked"),
587                span,
588            );
589        }
590        let selection = self.binding(
591            name,
592            Some("selection"),
593            args.named("selection"),
594            &Type::String,
595            span,
596        );
597        let icon = self.icon(args);
598        let badge = args.named("badge").map(|badge| self.text(name, badge));
599        let (title, value, selection) = (title?, value?, selection?);
600        let mut deps = title.deps.clone();
601        deps.extend(&value.deps);
602        deps.extend(&selection.deps);
603        if let Some(badge) = &badge {
604            deps.extend(&badge.deps);
605        }
606        Some(Node::NavItem(NavItem {
607            title: Rc::new(title),
608            value: Rc::new(value),
609            selection: Rc::new(selection),
610            icon,
611            badge: badge.map(Rc::new),
612            deps,
613        }))
614    }
615
616    fn icon(&mut self, args: &mut Args) -> Icon {
617        args.named("icon")
618            .and_then(|icon| self.const_member(icon, ICONS))
619            .unwrap_or_default()
620    }
621
622    fn label_style(&mut self, args: &mut Args) -> LabelStyle {
623        let font = args
624            .named("style")
625            .and_then(|style| self.const_member(style, TEXT_STYLES))
626            .unwrap_or_default();
627        let defaults = LabelStyle::new(font);
628        LabelStyle {
629            font,
630            color: args
631                .named("color")
632                .and_then(|color| self.const_member(color, TEXT_COLORS))
633                .unwrap_or(defaults.color),
634            align: args
635                .named("align")
636                .and_then(|align| self.const_member(align, TEXT_ALIGNMENTS))
637                .unwrap_or(defaults.align),
638            selectable: args
639                .named("selectable")
640                .and_then(|selectable| self.const_bool(selectable))
641                .unwrap_or(defaults.selectable),
642        }
643    }
644
645    fn view_call(
646        &mut self,
647        name: &str,
648        mut args: Args,
649        block: Option<&ast::Block>,
650        span: Span,
651    ) -> Option<Node> {
652        let Some(Global::View(view)) = self.global(name) else {
653            args.finish(self);
654            let message = match self.global(name) {
655                Some(Global::Fn(_)) => format!(
656                    "`{name}` is a function; call it from a button's action, or show its result with `label`"
657                ),
658                _ => format!("unknown element or view `{name}`"),
659            };
660            self.error(message, span);
661            return None;
662        };
663        let params = self.view_params[view.0].clone();
664        let (checked, deps) = self.dynamic(span, |checker| {
665            let mut checked = Vec::new();
666            for (param, ty) in &params {
667                let arg = args.param(param);
668                checked.push(checker.required_arg(arg, ty, span, || {
669                    format!("`{name}` needs an argument for `{param}`")
670                }));
671            }
672            checked
673        });
674        args.finish(self);
675        self.no_block(name, block);
676        Some(Node::View {
677            view,
678            args: checked.into(),
679            deps,
680        })
681    }
682
683    /// Reports each element in `stmts` that covers its stack after the first of its kind:
684    /// a stack has one context menu and one drop target.
685    fn one_covering_each(&mut self, stmts: &[ast::Stmt]) {
686        for kind in COVERING {
687            let mut calls = covering(stmts, kind);
688            let Some(first) = calls.next() else {
689                continue;
690            };
691            for extra in calls {
692                let name = extra.element.name;
693                self.diagnostics.push(
694                    Diagnostic::new(format!("a stack has one `{name}`"), extra.span)
695                        .with_note("its first is this one", first.span),
696                );
697            }
698        }
699    }
700
701    /// Reports an element that covers its stack straight in a `for`'s body, which would
702    /// cover the stack around the loop once for each item.
703    fn none_covering(&mut self, body: &[ast::Stmt]) {
704        for kind in COVERING {
705            for call in covering(body, kind) {
706                self.error(
707                    format!(
708                        "a `{}` here would cover the stack around the `for` once per item; put it in a `row` or `column` inside the loop",
709                        call.element.name
710                    ),
711                    call.span,
712                );
713            }
714        }
715    }
716
717    /// Reports a block given to `name`, which takes none.
718    pub(super) fn no_block(&mut self, name: &str, block: Option<&ast::Block>) {
719        if let Some(block) = block {
720            self.error(
721                format!("`{name}` doesn't take a `{{ … }}` block"),
722                block.span,
723            );
724        }
725    }
726
727    fn required_text(
728        &mut self,
729        element: &str,
730        expr: Option<&ast::Expr>,
731        span: Span,
732    ) -> Option<Dynamic> {
733        if let Some(expr) = expr {
734            return Some(self.text(element, expr));
735        }
736        self.error(
737            format!("`{element}` needs its text, such as `{element}(\"Hello\")`"),
738            span,
739        );
740        None
741    }
742
743    /// Text the UI shows; it updates whenever the state it reads changes.
744    fn text(&mut self, element: &str, expr: &ast::Expr) -> Dynamic {
745        let (expr, deps) =
746            self.dynamic(expr.span, |checker| checker.expr(expr, Some(&Type::String)));
747        match &expr.ty {
748            Type::String | Type::Error => {}
749            Type::Int | Type::Float | Type::Bool => self.error(
750                format!(
751                    "`{element}` shows text; turn this `{}` into text with \"{{…}}\"",
752                    self.type_name(&expr.ty)
753                ),
754                expr.span,
755            ),
756            other => self.require(&other.clone(), &Type::String, expr.span),
757        }
758        Dynamic { expr, deps }
759    }
760
761    /// An `int` the UI shows; it updates whenever the state it reads changes.
762    fn number(&mut self, expr: &ast::Expr) -> Dynamic {
763        self.shown(expr, &Type::Int)
764    }
765
766    /// A `bool` the UI shows; it updates whenever the state it reads changes.
767    fn condition(&mut self, expr: &ast::Expr) -> Dynamic {
768        self.shown(expr, &Type::Bool)
769    }
770
771    /// A value of type `ty` the UI shows; it updates whenever the state it reads changes.
772    fn shown(&mut self, expr: &ast::Expr, ty: &Type) -> Dynamic {
773        let (expr, deps) = self.dynamic(expr.span, |checker| checker.expect(expr, ty));
774        Dynamic { expr, deps }
775    }
776
777    fn binding(
778        &mut self,
779        element: &str,
780        label: Option<&str>,
781        expr: Option<&ast::Expr>,
782        ty: &Type,
783        span: Span,
784    ) -> Option<Binding> {
785        let (place, deps) = self.dynamic(expr.map_or(span, |expr| expr.span), |checker| {
786            let place = checker.bound_place(element, label, expr, ty, span)?;
787            if let PlaceRoot::State(state) = place.root {
788                checker.effects.reads.insert(state);
789            }
790            Some(place)
791        });
792        Some(Binding {
793            place: place?,
794            deps,
795        })
796    }
797
798    /// A button's action: statements that may change state.
799    pub(super) fn action(&mut self, block: &ast::Block) -> Block {
800        let outer = std::mem::replace(&mut self.body.context, Context::Action);
801        let action = self.block(block);
802        self.body.context = outer;
803        action
804    }
805
806    /// Checks UI code and returns the state it reads. Building the UI must not change state,
807    /// since it runs again whenever that state changes.
808    fn dynamic<T>(&mut self, span: Span, check: impl FnOnce(&mut Self) -> T) -> (T, StateSet) {
809        let (result, effects) = self.measure(check);
810        let (reads, writes) = self.fn_effects.total(&effects);
811        if writes {
812            self.error(
813                "showing the UI can't change state; do that in a button's action",
814                span,
815            );
816        }
817        (result, reads)
818    }
819}