1use 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
26fn 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}
37const MAX_LINES: u32 = 100;
39const DEFAULT_SLIDER_RANGE: (i64, i64) = (0, 100);
41
42pub(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
50pub(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
75fn 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
83const COVERING: [ElementKind; 2] = [ElementKind::ContextMenu, ElementKind::DropTarget];
85
86impl Checker {
87 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(¤t.condition);
225 bodies.push(¤t.then_block);
226 match ¤t.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 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 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 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 ¶ms {
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 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 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 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 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 fn number(&mut self, expr: &ast::Expr) -> Dynamic {
763 self.shown(expr, &Type::Int)
764 }
765
766 fn condition(&mut self, expr: &ast::Expr) -> Dynamic {
768 self.shown(expr, &Type::Bool)
769 }
770
771 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 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 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}