Skip to main content

bern_ir/
lib.rs

1//! Bern's intermediate representation.
2//!
3//! The IR states cross-platform semantics only: no DOM, SwiftUI, GTK or
4//! WinUI types appear here. Every target module consumes the same
5//! [`Application`] and decides privately how to realize it.
6
7pub mod spec;
8pub mod style;
9
10use bern_protocol::{LocalizedText, ProtocolDescriptor, Type, TypeTable, Value};
11
12mod corners;
13mod television;
14use serde::{Deserialize, Serialize};
15use std::collections::BTreeMap;
16
17/// Version of the IR layout. Targets declare the range they consume.
18pub const IR_VERSION: u32 = 8;
19
20#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
21pub struct Application {
22    pub ir_version: u32,
23    pub app: AppInfo,
24    pub languages: Languages,
25    pub theme: Theme,
26    /// The engine protocol the UI binds to.
27    pub protocol: ProtocolDescriptor,
28    /// Protocol types merged with UI-only value types.
29    pub types: TypeTable,
30    pub sessions: Vec<SessionDef>,
31    pub windows: WindowCatalog,
32    /// Media players owned by the application, shared by every window.
33    pub players: Vec<PlayerDef>,
34    pub components: Vec<ComponentDef>,
35    /// Every localized text with its stable resource key.
36    pub texts: Vec<LocalizedText>,
37    /// Files shipped with the application, sorted by name.
38    pub resources: Vec<ResourceDef>,
39    /// Layout breakpoints from `[layouts]`, when the application declares
40    /// them.
41    pub layouts: Option<LayoutConfig>,
42    /// Feature contracts with the coverage the compiler proved.
43    pub contracts: Vec<FeatureContract>,
44    /// Style overrides and motion tokens from `style!`.
45    #[serde(default)]
46    pub style: style::StyleSheet,
47}
48
49impl Application {
50    /// Visit the nodes of every component and then of every window's
51    /// content, depth first, each with the kind of the nearest element
52    /// around it.
53    pub fn walk<'a>(&'a self, visit: &mut impl FnMut(&'a Node, Option<&'a str>)) {
54        for component in &self.components {
55            component.body.walk_in(None, visit);
56        }
57        for window in &self.windows.windows {
58            window.content.walk_in(None, visit);
59        }
60    }
61
62    pub fn component(&self, name: &str) -> Option<&ComponentDef> {
63        self.components
64            .iter()
65            .find(|component| component.name == name)
66    }
67
68    pub fn session(&self, name: &str) -> Option<&SessionDef> {
69        self.sessions.iter().find(|session| session.name == name)
70    }
71
72    pub fn window(&self, id: &str) -> Option<&WindowDef> {
73        self.windows.windows.iter().find(|window| window.id == id)
74    }
75
76    /// Whether any declaration plays media.
77    pub fn uses_media(&self) -> bool {
78        let mut uses = !self.players.is_empty();
79        let mut visit = |node: &Node| {
80            if let Node::Element(element) = node
81                && element.kind == "Video"
82            {
83                uses = true;
84            }
85        };
86        for component in &self.components {
87            component.body.walk(&mut visit);
88        }
89        for window in &self.windows.windows {
90            window.content.walk(&mut visit);
91        }
92        uses
93    }
94}
95
96/// Breakpoints choosing between layout sizes, from `[layouts]`.
97#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
98pub struct LayoutConfig {
99    /// Available width, in logical units, below which `compact` applies.
100    pub compact_below: f64,
101    /// Below which `medium` applies; without it there is no `medium`.
102    #[serde(default, skip_serializing_if = "Option::is_none")]
103    pub medium_below: Option<f64>,
104    /// Below which `expanded` applies, and `large` from it; without it
105    /// there is no `large`.
106    #[serde(default, skip_serializing_if = "Option::is_none")]
107    pub expanded_below: Option<f64>,
108}
109
110impl LayoutConfig {
111    /// The size for an available width: every width maps to exactly one.
112    pub fn size(&self, width: f64) -> LayoutSize {
113        if width < self.compact_below {
114            LayoutSize::Compact
115        } else if self.medium_below.is_some_and(|below| width < below) {
116            LayoutSize::Medium
117        } else if self.expanded_below.is_none_or(|below| width < below) {
118            LayoutSize::Expanded
119        } else {
120            LayoutSize::Large
121        }
122    }
123
124    /// Whether the breakpoints give `size` a range of widths.
125    pub fn has(&self, size: LayoutSize) -> bool {
126        match size {
127            LayoutSize::Compact | LayoutSize::Expanded => true,
128            LayoutSize::Medium => self.medium_below.is_some(),
129            LayoutSize::Large => self.expanded_below.is_some(),
130        }
131    }
132}
133
134/// A layout size of `Adaptive`.
135#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
136#[serde(rename_all = "snake_case")]
137pub enum LayoutSize {
138    Compact,
139    Medium,
140    Expanded,
141    Large,
142}
143
144impl LayoutSize {
145    /// Every size, narrowest first.
146    pub const ALL: [LayoutSize; 4] = [
147        LayoutSize::Compact,
148        LayoutSize::Medium,
149        LayoutSize::Expanded,
150        LayoutSize::Large,
151    ];
152
153    /// Sizes every `Adaptive` gives a layout of its own, and feature
154    /// contracts are checked in; the others borrow one.
155    pub const REQUIRED: [LayoutSize; 2] = [LayoutSize::Compact, LayoutSize::Expanded];
156
157    pub fn name(self) -> &'static str {
158        match self {
159            LayoutSize::Compact => "compact",
160            LayoutSize::Medium => "medium",
161            LayoutSize::Expanded => "expanded",
162            LayoutSize::Large => "large",
163        }
164    }
165
166    /// The size whose layout this one shows when it has none of its own:
167    /// the next larger declared size, else the next smaller.
168    pub fn shown(self, declared: &[LayoutSize]) -> Option<LayoutSize> {
169        if declared.contains(&self) {
170            return Some(self);
171        }
172        let larger = Self::ALL.iter().copied().filter(|size| *size > self);
173        let smaller = Self::ALL.iter().rev().copied().filter(|size| *size < self);
174        larger.chain(smaller).find(|size| declared.contains(size))
175    }
176
177    pub fn parse(name: &str) -> Option<Self> {
178        Self::ALL.into_iter().find(|size| size.name() == name)
179    }
180}
181
182/// A `#[bern::features]` contract: what users can do within the scope of
183/// one component, and how each feature is reached in each layout size.
184#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
185pub struct FeatureContract {
186    /// The features enum, for example `MediaFeature`.
187    pub name: String,
188    /// The component whose subtree is the contract's scope.
189    pub component: String,
190    #[serde(default, skip_serializing_if = "String::is_empty")]
191    pub doc: String,
192    pub features: Vec<FeatureDef>,
193    pub source: SourceLocation,
194}
195
196#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
197pub struct FeatureDef {
198    /// The variant, for example `Clip`.
199    pub name: String,
200    #[serde(default, skip_serializing_if = "String::is_empty")]
201    pub doc: String,
202    /// Command ids implementing the feature: protocol commands such as
203    /// `showcase.feature`, or standard media commands such as `media.play`.
204    pub commands: Vec<String>,
205    /// Sizes the feature applies to.
206    pub layouts: Vec<LayoutSize>,
207    /// A manual guarantee for coverage that cannot be proven statically.
208    pub guaranteed: Option<String>,
209    pub exemptions: Vec<Exemption>,
210    /// What the compiler proved, per applicable size.
211    pub coverage: Vec<SizeCoverage>,
212    pub source: SourceLocation,
213}
214
215/// A platform on which a feature need not be reachable, with the reason.
216#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
217pub struct Exemption {
218    pub platform: String,
219    pub reason: String,
220}
221
222#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
223pub struct SizeCoverage {
224    pub size: LayoutSize,
225    pub coverage: Coverage,
226}
227
228/// Entries found for a feature in one size, by control target.
229#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
230#[serde(tag = "kind", rename_all = "snake_case")]
231pub enum Coverage {
232    /// At least one entry is present whatever the state.
233    Covered {
234        entries: Vec<String>,
235    },
236    /// Entries exist only under conditions the framework cannot prove.
237    Conditional {
238        entries: Vec<String>,
239    },
240    Missing,
241}
242
243/// The outcome for one feature in one size on one platform.
244#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
245#[serde(tag = "result", rename_all = "snake_case")]
246pub enum FeatureResult {
247    Covered {
248        entries: Vec<String>,
249    },
250    /// Only conditional entries, accepted by a manual guarantee; not proven.
251    Guaranteed {
252        entries: Vec<String>,
253        reason: String,
254    },
255    /// Not reachable, and exempt on this platform.
256    Exempt {
257        reason: String,
258    },
259    Missing,
260    /// Only conditional entries and no guarantee.
261    Unproven {
262        entries: Vec<String>,
263    },
264}
265
266impl FeatureResult {
267    /// Whether the result fails a strict build.
268    pub fn is_error(&self) -> bool {
269        matches!(
270            self,
271            FeatureResult::Missing | FeatureResult::Unproven { .. }
272        )
273    }
274}
275
276impl FeatureDef {
277    /// The results on `platform`, per applicable size.
278    pub fn results(&self, platform: &str) -> Vec<(LayoutSize, FeatureResult)> {
279        let exemption = self
280            .exemptions
281            .iter()
282            .find(|exemption| exemption.platform == platform);
283        self.coverage
284            .iter()
285            .map(|SizeCoverage { size, coverage }| {
286                let result = match (coverage, exemption, &self.guaranteed) {
287                    (Coverage::Covered { entries }, _, _) => FeatureResult::Covered {
288                        entries: entries.clone(),
289                    },
290                    (_, Some(exemption), _) => FeatureResult::Exempt {
291                        reason: exemption.reason.clone(),
292                    },
293                    (Coverage::Conditional { entries }, None, Some(reason)) => {
294                        FeatureResult::Guaranteed {
295                            entries: entries.clone(),
296                            reason: reason.clone(),
297                        }
298                    }
299                    (Coverage::Conditional { entries }, None, None) => FeatureResult::Unproven {
300                        entries: entries.clone(),
301                    },
302                    (Coverage::Missing, None, _) => FeatureResult::Missing,
303                };
304                (*size, result)
305            })
306            .collect()
307    }
308}
309
310/// A file the application ships with its build, declared in the
311/// `[resources]` table of `Bern.toml`. Engines resolve it by name at run
312/// time; each target decides where it lives in the product. Neither the
313/// contents nor a build machine path are part of the IR.
314#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
315pub struct ResourceDef {
316    /// The file's `/`-separated path relative to `Bern.toml`, for example
317    /// `media/aurora.mp4`.
318    pub name: String,
319    /// The `[resources]` entry that declared the file.
320    pub declared: SourceLocation,
321    /// How an SVG document shows as a picture of it, with no document
322    /// engine; `None` for one that moves or runs, and for other files.
323    #[serde(default)]
324    pub picture: Option<SvgPicture>,
325}
326
327/// How an SVG document shows as a picture of it (`bern_svg::picture`).
328#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
329#[serde(rename_all = "snake_case")]
330pub enum SvgPicture {
331    /// In its own colors.
332    Still,
333    /// In the color of the text around it, as a template.
334    Tinted,
335    /// In colors of its own for the light and for the dark appearance: a
336    /// picture of each.
337    Themed,
338}
339
340/// An application player from the `#[bern::players]` catalog. Its id is
341/// the stable `player_id` every entry uses.
342#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
343pub struct PlayerDef {
344    pub id: String,
345    #[serde(default, skip_serializing_if = "String::is_empty")]
346    pub doc: String,
347}
348
349#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
350pub struct AppInfo {
351    pub id: String,
352    pub bundle_id: String,
353    pub name: LocalizedText,
354    pub version: String,
355    /// The application icon from `[app].icon`: a square PNG's
356    /// `/`-separated path relative to `Bern.toml`. Without one, targets use
357    /// the Bern logo.
358    #[serde(default, skip_serializing_if = "Option::is_none")]
359    pub icon: Option<String>,
360    /// Platform name to the icon that platform uses instead, from
361    /// `[app.icon]`: a square PNG, or a file or folder in the platform's own
362    /// form, as a `/`-separated path relative to `Bern.toml`.
363    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
364    pub platform_icons: BTreeMap<String, String>,
365    /// Icons the application can switch to at run time, from
366    /// `[app.alternate_icons.<name>]`: name to its entries, each as
367    /// `[app.icon]` has them (`default` and platform names, `/`-separated
368    /// paths relative to `Bern.toml`). `WindowSet(icon:)` chooses one.
369    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
370    pub alternate_icons: BTreeMap<String, BTreeMap<String, String>>,
371    /// Who publishes the application, from `[app].publisher`: shown by
372    /// installers and package managers.
373    #[serde(default, skip_serializing_if = "Option::is_none")]
374    pub publisher: Option<String>,
375    /// `[app].files_folder`: on iOS the application's Documents folder shows
376    /// in the Files app, for files the user puts there for it.
377    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
378    pub files_folder: bool,
379    /// `[app].background_audio`: the application's own audio plays on
380    /// behind the lock screen and in the background (iOS's audio background
381    /// mode).
382    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
383    pub background_audio: bool,
384    /// `[app].overlays`: the application draws text over other apps
385    /// (`bern::overlay`), which Android asks the user to allow.
386    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
387    pub overlays: bool,
388}
389
390#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
391pub struct Languages {
392    pub supported: Vec<String>,
393    pub default: String,
394    pub strict: bool,
395}
396
397#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
398#[serde(rename_all = "snake_case")]
399pub enum ThemeMode {
400    System,
401    Light,
402    Dark,
403}
404
405#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
406pub struct Srgb {
407    pub red: u8,
408    pub green: u8,
409    pub blue: u8,
410}
411
412impl Srgb {
413    pub fn parse(hex: &str) -> Option<Self> {
414        let hex = hex.strip_prefix('#')?;
415        if hex.len() != 6 || !hex.chars().all(|c| c.is_ascii_hexdigit()) {
416            return None;
417        }
418        let byte = |index: usize| u8::from_str_radix(&hex[index..index + 2], 16).ok();
419        Some(Self {
420            red: byte(0)?,
421            green: byte(2)?,
422            blue: byte(4)?,
423        })
424    }
425}
426
427#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
428pub struct Theme {
429    pub mode: ThemeMode,
430    /// Optional brand color. Native targets map it to their accent color
431    /// interface; algorithm targets seed the Bern palette with it.
432    pub brand_seed: Option<Srgb>,
433}
434
435#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
436pub struct SessionFieldDef {
437    pub name: String,
438    #[serde(rename = "type")]
439    pub ty: Type,
440    pub initial: Value,
441}
442
443/// Typed drafts owned by a UI session, one instance per hosting window.
444#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
445pub struct SessionDef {
446    pub name: String,
447    pub fields: Vec<SessionFieldDef>,
448}
449
450impl SessionDef {
451    pub fn field_index(&self, name: &str) -> Option<usize> {
452        self.fields.iter().position(|field| field.name == name)
453    }
454}
455
456#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
457#[serde(rename_all = "snake_case")]
458pub enum WindowInitial {
459    Open,
460    Closed,
461}
462
463#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
464#[serde(rename_all = "snake_case")]
465pub enum Presentation {
466    /// Native where the target has system windows, otherwise the host's
467    /// virtual presentation (on the web: the page, or a dialog).
468    Auto,
469    /// The host's in-app presentation, even where native windows exist.
470    Virtual,
471    /// A draggable, resizable window: a native window, or a floating panel
472    /// inside the page.
473    Floating,
474    NativeRequired,
475}
476
477#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
478#[serde(rename_all = "snake_case")]
479pub enum WindowRole {
480    Standard,
481    /// Application preferences window.
482    Settings,
483    /// A modal window over the primary window, such as a first-run flow:
484    /// a sheet where the platform has them.
485    Dialog,
486}
487
488#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
489pub struct WindowDef {
490    pub id: String,
491    pub title: LocalizedText,
492    pub initial: WindowInitial,
493    pub presentation: Presentation,
494    pub role: WindowRole,
495    pub width: Option<f64>,
496    pub height: Option<f64>,
497    pub min_width: Option<f64>,
498    pub min_height: Option<f64>,
499    pub content: Node,
500    /// Session state types instantiated for each instance of this window.
501    pub sessions: Vec<String>,
502    /// Declared close check, run whenever closing is requested.
503    pub close: Option<CloseCheck>,
504    /// Whether content extends under a transparent title bar.
505    #[serde(default)]
506    pub chrome: WindowChrome,
507    /// While true, the window hides its title bar and toolbar (and on
508    /// touch platforms the status bar), and the pointer once it rests.
509    #[serde(default)]
510    pub immersive: Option<Expr>,
511    /// Seconds without input after which `window.idle` turns true.
512    #[serde(default)]
513    pub idle_after: Option<f64>,
514    /// Pause the idle timer while interacting with persistent controls.
515    #[serde(default)]
516    pub idle_enabled: Option<Expr>,
517    /// On a phone, the window stands in landscape while this is true and
518    /// in portrait otherwise; elsewhere it follows the device.
519    #[serde(default)]
520    pub landscape: Option<Expr>,
521}
522
523#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
524#[serde(rename_all = "snake_case")]
525pub enum WindowChrome {
526    /// The platform's title bar.
527    #[default]
528    Standard,
529    /// Content under a transparent title bar, the system window controls
530    /// over it.
531    Unified,
532}
533
534/// The close check of a window. When `when` holds, closing waits for a
535/// decision: Save (running `save`), Discard or Cancel. The expressions are
536/// evaluated against the closing window's own session drafts.
537#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
538pub struct CloseCheck {
539    pub when: Expr,
540    pub message: Text,
541    /// Engine command a Save decision runs; closing waits for its result.
542    pub save: Option<Action>,
543}
544
545/// Static window kinds declared by the root `WindowSet`. The first version
546/// supports single-instance windows only.
547#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
548pub struct WindowCatalog {
549    pub handle: String,
550    pub primary: String,
551    pub windows: Vec<WindowDef>,
552    /// The accent every window shows: an enum whose variants name accents
553    /// of `style!`.
554    #[serde(default)]
555    pub accent: Option<Expr>,
556    /// Optional JSON map of semantic roles to light/dark #RRGGBB pairs.
557    /// The application supplies a complete, contrast-checked palette.
558    #[serde(default)]
559    pub palette: Option<Expr>,
560    /// The appearance every window shows: an enum of `System`, `Light`
561    /// and `Dark`.
562    #[serde(default)]
563    pub appearance: Option<Expr>,
564    /// The application icon the system shows: an enum whose `Default`
565    /// variant is `[app.icon]` and whose other variants name icons of
566    /// `[app.alternate_icons]` (`DarkSkin` names `dark_skin`).
567    #[serde(default, skip_serializing_if = "Option::is_none")]
568    pub icon: Option<Expr>,
569    /// The `MenuBar`: its `CommandMenu`s, their `CommandItem`s, dividers
570    /// and submenus, whose drafts are the primary window's. Only a target
571    /// with a system menu bar shows it.
572    #[serde(default, skip_serializing_if = "Option::is_none")]
573    pub menu_bar: Option<Element>,
574    /// The `StatusItem`: the application's item in the system's status
575    /// area and the `CommandItem`s, dividers and submenus of its menu, which
576    /// run without a window. Only a target with a status area shows it; with
577    /// it, closing the last window leaves the application running.
578    #[serde(default, skip_serializing_if = "Option::is_none")]
579    pub status_item: Option<Element>,
580}
581
582#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
583pub struct SourceLocation {
584    pub file: String,
585    pub line: u32,
586    pub column: u32,
587}
588
589#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
590#[serde(tag = "kind", rename_all = "snake_case")]
591pub enum ParamKind {
592    /// The typed engine client of the application protocol (its namespace)
593    /// or of one of its modules.
594    Client { namespace: String },
595    /// The generated window handle.
596    Windows,
597    /// The application's player catalog.
598    Players,
599    /// A value of a schema type, read-only inside the component.
600    Value {
601        #[serde(rename = "type")]
602        ty: Type,
603    },
604    /// Session state owned by the hosting window.
605    Session { session: String },
606}
607
608#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
609pub struct ParamDef {
610    pub name: String,
611    pub kind: ParamKind,
612    /// `#[content]`: text the component shows as data, so callers pass
613    /// data and never a literal.
614    #[serde(default, skip_serializing_if = "is_false")]
615    pub content: bool,
616}
617
618fn is_false(value: &bool) -> bool {
619    !value
620}
621
622#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
623pub struct ComponentDef {
624    pub name: String,
625    pub module: Vec<String>,
626    pub params: Vec<ParamDef>,
627    pub body: Node,
628    pub source: SourceLocation,
629}
630
631#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
632#[serde(rename_all = "snake_case")]
633pub enum BinaryOp {
634    Eq,
635    Ne,
636    Lt,
637    Le,
638    Gt,
639    Ge,
640    And,
641    Or,
642}
643
644/// Where a binding reads from.
645#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
646#[serde(tag = "scope", rename_all = "snake_case")]
647pub enum Binding {
648    /// Engine state field, then record field indices.
649    Engine { field: usize, path: Vec<usize> },
650    /// Session state of the hosting window.
651    Session {
652        session: String,
653        field: usize,
654        path: Vec<usize>,
655    },
656    /// Template variable, pattern binding or component parameter, with the
657    /// type of the variable itself so record paths can be resolved.
658    Local {
659        name: String,
660        path: Vec<usize>,
661        root: Type,
662    },
663    /// UI state of the hosting window, such as `window.idle`.
664    Window { field: String },
665}
666
667#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
668#[serde(tag = "kind", rename_all = "snake_case")]
669pub enum ExprKind {
670    Literal {
671        value: Value,
672    },
673    Binding {
674        binding: Binding,
675    },
676    Variant {
677        variant: u32,
678        fields: Vec<Expr>,
679    },
680    Not {
681        expr: Box<Expr>,
682    },
683    Neg {
684        expr: Box<Expr>,
685    },
686    Binary {
687        op: BinaryOp,
688        lhs: Box<Expr>,
689        rhs: Box<Expr>,
690    },
691    /// One of two values of the expression's type.
692    If {
693        cond: Box<Expr>,
694        then: Box<Expr>,
695        otherwise: Box<Expr>,
696    },
697    /// A named corner radius of `style!` (`corner { card: 22 }`), a float.
698    /// `radius` is its value on every platform; [`Application::on_platform`]
699    /// puts the platform's value in its place before a target generates.
700    Corner {
701        name: String,
702        radius: f64,
703    },
704}
705
706/// A type-checked restricted expression.
707#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
708pub struct Expr {
709    pub kind: ExprKind,
710    #[serde(rename = "type")]
711    pub ty: Type,
712}
713
714#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
715#[serde(tag = "kind", rename_all = "snake_case")]
716pub enum Text {
717    /// Declared translations, with typed interpolation arguments.
718    Localized {
719        text: LocalizedText,
720        args: Vec<(String, Expr)>,
721    },
722    /// Runtime data such as user input or media titles.
723    Dynamic { expr: Expr },
724}
725
726/// A writable session field bound to a control.
727#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
728pub struct FieldRef {
729    pub session: String,
730    pub field: usize,
731    #[serde(rename = "type")]
732    pub ty: Type,
733}
734
735#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
736#[serde(tag = "kind", rename_all = "snake_case")]
737pub enum Prop {
738    Keyword {
739        value: String,
740    },
741    Number {
742        value: f64,
743    },
744    Icon {
745        name: String,
746    },
747    /// A declared resource, by its name in `[resources]`.
748    Resource {
749        name: String,
750    },
751    Text {
752        text: Text,
753    },
754    Expr {
755        expr: Expr,
756    },
757    Field {
758        field: FieldRef,
759    },
760    Window {
761        id: String,
762    },
763    TypeName {
764        name: String,
765    },
766    Key {
767        field: usize,
768        name: String,
769    },
770    Id {
771        value: String,
772    },
773    /// An application player.
774    Player {
775        id: String,
776    },
777    /// A typed query call whose result feeds the element's template.
778    Query {
779        query: QueryBinding,
780    },
781    /// The feature of the enclosing contract an entry reaches.
782    Feature {
783        contract: String,
784        feature: String,
785    },
786}
787
788/// A query of the engine catalog bound with its arguments. The arguments
789/// are restricted expressions; when their values change, the target asks
790/// again under a new parameter version and shows only the current answer.
791#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
792pub struct QueryBinding {
793    /// Catalog index of the query in the protocol.
794    pub query: usize,
795    pub args: Vec<Expr>,
796    /// The query's declared result.
797    pub result: Type,
798}
799
800#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
801#[serde(rename_all = "snake_case")]
802pub enum WindowOp {
803    ToggleFullscreen,
804    Open,
805    Close,
806    Activate,
807}
808
809/// A standard media command on an application player.
810#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
811#[serde(rename_all = "snake_case")]
812pub enum MediaOp {
813    Open,
814    Play,
815    Pause,
816    Seek,
817    SetVolume,
818    SetMuted,
819}
820
821impl MediaOp {
822    pub const ALL: [MediaOp; 6] = [
823        MediaOp::Open,
824        MediaOp::Play,
825        MediaOp::Pause,
826        MediaOp::Seek,
827        MediaOp::SetVolume,
828        MediaOp::SetMuted,
829    ];
830
831    /// Method name in declarations, for example `open`.
832    pub fn method(self) -> &'static str {
833        match self {
834            MediaOp::Open => "open",
835            MediaOp::Play => "play",
836            MediaOp::Pause => "pause",
837            MediaOp::Seek => "seek",
838            MediaOp::SetVolume => "set_volume",
839            MediaOp::SetMuted => "set_muted",
840        }
841    }
842
843    /// Command id in the media catalog, for example `media.open`.
844    pub fn command(self) -> &'static str {
845        match self {
846            MediaOp::Open => "media.open",
847            MediaOp::Play => "media.play",
848            MediaOp::Pause => "media.pause",
849            MediaOp::Seek => "media.seek",
850            MediaOp::SetVolume => "media.set_volume",
851            MediaOp::SetMuted => "media.set_muted",
852        }
853    }
854}
855
856#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
857#[serde(tag = "kind", rename_all = "snake_case")]
858pub enum Action {
859    /// Engine command by catalog index with positional arguments.
860    Engine { command: usize, args: Vec<Expr> },
861    /// Generated window session command.
862    Window { op: WindowOp, window: String },
863    /// Standard media command on an application player; the player id is
864    /// the first parameter, `args` the rest.
865    Media {
866        player: String,
867        op: MediaOp,
868        args: Vec<Expr>,
869    },
870    /// A change of a session draft: interface state such as an open panel
871    /// or a step of a flow, not engine state.
872    Session { field: FieldRef, op: SessionOp },
873}
874
875#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
876#[serde(tag = "kind", rename_all = "snake_case")]
877pub enum SessionOp {
878    /// `draft.field.set(value)`.
879    Set { value: Expr },
880    /// `draft.field.toggle()` on a `bool`.
881    Toggle,
882    /// `draft.field.push_str(text)` on a `String`: the text after it, as
883    /// a key of an on-screen keyboard types.
884    PushStr { value: Expr },
885    /// `draft.field.pop()` on a `String`: without its last character, as
886    /// a keyboard's delete.
887    Pop,
888}
889
890#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
891pub struct Template {
892    pub var: String,
893    #[serde(rename = "type")]
894    pub ty: Type,
895}
896
897#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
898pub struct Element {
899    /// Built-in component name from [`spec::COMPONENTS`].
900    pub kind: String,
901    /// Control identity for interactive components.
902    pub id: Option<String>,
903    /// Structural path inside the declaring component, stable for a build.
904    pub path: String,
905    pub props: BTreeMap<String, Prop>,
906    pub template: Option<Template>,
907    pub children: Vec<Node>,
908    pub action: Option<Action>,
909    pub source: SourceLocation,
910}
911
912impl Element {
913    pub fn prop(&self, name: &str) -> Option<&Prop> {
914        self.props.get(name)
915    }
916
917    /// The items of its `ContextMenu`, dividers among them, if it has one.
918    pub fn context_menu(&self) -> Option<Vec<&Element>> {
919        self.children.iter().find_map(|node| match node {
920            Node::Element(menu) if menu.kind == "ContextMenu" => Some(
921                menu.children
922                    .iter()
923                    .filter_map(|node| match node {
924                        Node::Element(item) => Some(item),
925                        _ => None,
926                    })
927                    .collect(),
928            ),
929            _ => None,
930        })
931    }
932
933    /// Whether it has children to show besides a `ContextMenu`.
934    pub fn has_content(&self) -> bool {
935        self.children
936            .iter()
937            .any(|node| !matches!(node, Node::Element(menu) if menu.kind == "ContextMenu"))
938    }
939
940    pub fn keyword(&self, name: &str) -> Option<&str> {
941        match self.props.get(name) {
942            Some(Prop::Keyword { value }) => Some(value),
943            _ => None,
944        }
945    }
946
947    pub fn number(&self, name: &str) -> Option<f64> {
948        match self.props.get(name) {
949            Some(Prop::Number { value }) => Some(*value),
950            _ => None,
951        }
952    }
953
954    pub fn text(&self, name: &str) -> Option<&Text> {
955        match self.props.get(name) {
956            Some(Prop::Text { text }) => Some(text),
957            _ => None,
958        }
959    }
960
961    pub fn icon(&self, name: &str) -> Option<&str> {
962        match self.props.get(name) {
963            Some(Prop::Icon { name }) => Some(name),
964            _ => None,
965        }
966    }
967
968    pub fn resource(&self, name: &str) -> Option<&str> {
969        match self.props.get(name) {
970            Some(Prop::Resource { name }) => Some(name),
971            _ => None,
972        }
973    }
974
975    /// What follows the element's action: with `submit: clear` on a
976    /// `TextInput`, or `clears: draft.x` on a `Button`, the draft becomes
977    /// empty, and the keyboard goes back to the window.
978    pub fn clearing(&self) -> Option<Action> {
979        let field = if self.keyword("submit") == Some("clear") {
980            self.field("value")?
981        } else {
982            self.field("clears")?
983        };
984        Some(Action::Session {
985            field: field.clone(),
986            op: SessionOp::Set {
987                value: Expr {
988                    kind: ExprKind::Literal {
989                        value: Value::Text(String::new()),
990                    },
991                    ty: Type::Text,
992                },
993            },
994        })
995    }
996
997    pub fn field(&self, name: &str) -> Option<&FieldRef> {
998        match self.props.get(name) {
999            Some(Prop::Field { field }) => Some(field),
1000            _ => None,
1001        }
1002    }
1003
1004    pub fn expr(&self, name: &str) -> Option<&Expr> {
1005        match self.props.get(name) {
1006            Some(Prop::Expr { expr }) => Some(expr),
1007            _ => None,
1008        }
1009    }
1010
1011    pub fn query(&self, name: &str) -> Option<&QueryBinding> {
1012        match self.props.get(name) {
1013            Some(Prop::Query { query }) => Some(query),
1014            _ => None,
1015        }
1016    }
1017}
1018
1019#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1020#[serde(tag = "kind", rename_all = "snake_case")]
1021pub enum ComponentArg {
1022    Client,
1023    Windows,
1024    Players,
1025    Value { expr: Expr },
1026}
1027
1028#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1029pub struct ComponentRef {
1030    pub component: String,
1031    pub args: Vec<(String, ComponentArg)>,
1032    pub source: SourceLocation,
1033}
1034
1035#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1036pub struct IfNode {
1037    pub branches: Vec<(Expr, Vec<Node>)>,
1038    pub otherwise: Vec<Node>,
1039}
1040
1041#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1042pub struct IfLetNode {
1043    pub var: String,
1044    pub value: Expr,
1045    #[serde(rename = "inner_type")]
1046    pub inner: Type,
1047    pub then: Vec<Node>,
1048    pub otherwise: Vec<Node>,
1049}
1050
1051#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1052pub struct MatchBinding {
1053    pub name: String,
1054    pub field: usize,
1055    #[serde(rename = "type")]
1056    pub ty: Type,
1057}
1058
1059#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1060pub struct MatchArm {
1061    /// `None` for the `_` arm.
1062    pub variant: Option<u32>,
1063    pub bindings: Vec<MatchBinding>,
1064    pub body: Vec<Node>,
1065}
1066
1067#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1068pub struct MatchNode {
1069    pub value: Expr,
1070    pub enum_name: String,
1071    pub arms: Vec<MatchArm>,
1072}
1073
1074#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1075#[serde(tag = "node", rename_all = "snake_case")]
1076pub enum Node {
1077    Element(Element),
1078    Component(ComponentRef),
1079    If(IfNode),
1080    IfLet(IfLetNode),
1081    Match(MatchNode),
1082}
1083
1084impl Node {
1085    /// Visit this node and all descendants, depth first.
1086    pub fn walk<'a>(&'a self, visit: &mut impl FnMut(&'a Node)) {
1087        self.walk_in(None, &mut |node, _| visit(node));
1088    }
1089
1090    /// Visit every element at or under this node, depth first, to change
1091    /// it.
1092    pub fn elements_mut(&mut self, visit: &mut impl FnMut(&mut Element)) {
1093        let children: Vec<&mut Node> = match self {
1094            Node::Element(element) => {
1095                visit(element);
1096                element.children.iter_mut().collect()
1097            }
1098            Node::Component(_) => Vec::new(),
1099            Node::If(node) => node
1100                .branches
1101                .iter_mut()
1102                .flat_map(|(_, body)| body.iter_mut())
1103                .chain(node.otherwise.iter_mut())
1104                .collect(),
1105            Node::IfLet(node) => node
1106                .then
1107                .iter_mut()
1108                .chain(node.otherwise.iter_mut())
1109                .collect(),
1110            Node::Match(node) => node
1111                .arms
1112                .iter_mut()
1113                .flat_map(|arm| arm.body.iter_mut())
1114                .collect(),
1115        };
1116        for child in children {
1117            child.elements_mut(visit);
1118        }
1119    }
1120
1121    /// Visit this node and all descendants, depth first, each with the kind
1122    /// of the nearest element around it; `parent` is the one around this
1123    /// node.
1124    pub fn walk_in<'a>(
1125        &'a self,
1126        parent: Option<&'a str>,
1127        visit: &mut impl FnMut(&'a Node, Option<&'a str>),
1128    ) {
1129        visit(self, parent);
1130        let parent = match self {
1131            Node::Element(element) => Some(element.kind.as_str()),
1132            _ => parent,
1133        };
1134        match self {
1135            Node::Element(element) => {
1136                for child in &element.children {
1137                    child.walk_in(parent, visit);
1138                }
1139            }
1140            Node::Component(_) => {}
1141            Node::If(node) => {
1142                for (_, body) in &node.branches {
1143                    for child in body {
1144                        child.walk_in(parent, visit);
1145                    }
1146                }
1147                for child in &node.otherwise {
1148                    child.walk_in(parent, visit);
1149                }
1150            }
1151            Node::IfLet(node) => {
1152                for child in node.then.iter().chain(&node.otherwise) {
1153                    child.walk_in(parent, visit);
1154                }
1155            }
1156            Node::Match(node) => {
1157                for arm in &node.arms {
1158                    for child in &arm.body {
1159                        child.walk_in(parent, visit);
1160                    }
1161                }
1162            }
1163        }
1164    }
1165}
1166
1167impl Expr {
1168    /// Visit this expression and every sub-expression.
1169    pub fn walk<'a>(&'a self, visit: &mut impl FnMut(&'a Expr)) {
1170        visit(self);
1171        match &self.kind {
1172            ExprKind::Literal { .. } | ExprKind::Binding { .. } | ExprKind::Corner { .. } => {}
1173            ExprKind::Variant { fields, .. } => {
1174                for field in fields {
1175                    field.walk(visit);
1176                }
1177            }
1178            ExprKind::Not { expr } | ExprKind::Neg { expr } => expr.walk(visit),
1179            ExprKind::Binary { lhs, rhs, .. } => {
1180                lhs.walk(visit);
1181                rhs.walk(visit);
1182            }
1183            ExprKind::If {
1184                cond,
1185                then,
1186                otherwise,
1187            } => {
1188                cond.walk(visit);
1189                then.walk(visit);
1190                otherwise.walk(visit);
1191            }
1192        }
1193    }
1194
1195    /// Session state types this expression reads.
1196    pub fn sessions(&self) -> Vec<String> {
1197        let mut sessions = Vec::new();
1198        self.walk(&mut |expr| {
1199            if let ExprKind::Binding {
1200                binding: Binding::Session { session, .. },
1201            } = &expr.kind
1202                && !sessions.contains(session)
1203            {
1204                sessions.push(session.clone());
1205            }
1206        });
1207        sessions
1208    }
1209
1210    /// Whether this expression reads engine state.
1211    pub fn reads_engine(&self) -> bool {
1212        let mut reads = false;
1213        self.walk(&mut |expr| {
1214            if let ExprKind::Binding {
1215                binding: Binding::Engine { .. },
1216            } = &expr.kind
1217            {
1218                reads = true;
1219            }
1220        });
1221        reads
1222    }
1223}
1224
1225impl Text {
1226    /// Expressions interpolated into this text.
1227    pub fn exprs(&self) -> Vec<&Expr> {
1228        match self {
1229            Text::Localized { args, .. } => args.iter().map(|(_, expr)| expr).collect(),
1230            Text::Dynamic { expr } => vec![expr],
1231        }
1232    }
1233}
1234
1235impl CloseCheck {
1236    /// Every expression the check evaluates.
1237    pub fn exprs(&self) -> Vec<&Expr> {
1238        let mut exprs = vec![&self.when];
1239        exprs.extend(self.message.exprs());
1240        if let Some(Action::Engine { args, .. }) = &self.save {
1241            exprs.extend(args);
1242        }
1243        exprs
1244    }
1245}
1246
1247pub use bern_protocol::label_key;
1248
1249/// Version of the [`ComponentLibrary`] layout.
1250pub const LIBRARY_DESCRIPTOR_VERSION: u32 = 1;
1251
1252/// Versioned description of a component library crate, generated from its
1253/// sources. Applications check it against their own configuration before
1254/// using its components: descriptor and IR versions, the crate version it
1255/// was generated from, and translations for every required language.
1256#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1257pub struct ComponentLibrary {
1258    pub descriptor_version: u32,
1259    pub ir_version: u32,
1260    /// Cargo package name of the library.
1261    pub package: String,
1262    /// Cargo package version the descriptor was generated from.
1263    pub version: String,
1264    /// Fingerprint of the library sources and configuration.
1265    pub source_fingerprint: String,
1266    pub languages: Vec<String>,
1267    pub types: TypeTable,
1268    pub components: Vec<ComponentDef>,
1269    pub texts: Vec<LocalizedText>,
1270}
1271
1272/// Whether `node`, followed in a row by `next`, is the words that lead the
1273/// row: a text, or a column standing at its leading edge, before a spacer.
1274/// It takes the spacer's room itself, so the controls after it keep their
1275/// width and the words wrap in what is left, as a list row's label does.
1276pub fn leads_row(node: &Node, next: Option<&Node>) -> bool {
1277    let words = matches!(node, Node::Element(element)
1278        if element.prop("width").is_none()
1279            && (element.kind == "Text"
1280                || (element.kind == "Column"
1281                    && matches!(element.keyword("align"), None | Some("leading")))));
1282    words && matches!(next, Some(Node::Element(spacer)) if spacer.kind == "Spacer")
1283}
1284
1285/// The `ForEach` of a grid that, standing in a vertical scroll view, builds
1286/// only the rows near what is in sight: a grid whose one child is a keyed
1287/// `ForEach`, not laid out as masonry. Every target builds such a grid
1288/// lazily; anywhere else it builds all its rows.
1289pub fn lazy_grid_items(grid: &Element) -> Option<&Element> {
1290    if grid.kind != "Grid" || grid.keyword("masonry") == Some("true") {
1291        return None;
1292    }
1293    match grid.children.as_slice() {
1294        [Node::Element(each)]
1295            if each.kind == "ForEach"
1296                && each.template.is_some()
1297                && each.expr("items").is_some()
1298                && matches!(each.prop("key"), Some(Prop::Key { .. })) =>
1299        {
1300            Some(each)
1301        }
1302        _ => None,
1303    }
1304}
1305
1306/// The `align` a list lowered by [`lower_lists`] keeps: how its rows
1307/// stand across it, as in the column it came from.
1308pub fn lowered_list_align(grid: &Element) -> Option<&str> {
1309    (grid.kind == "Grid")
1310        .then(|| grid.keyword("align"))
1311        .flatten()
1312}
1313
1314/// Whether `nodes`, the children of a column, hold a list lowered by
1315/// [`lower_lists`], directly or in their conditions.
1316pub fn holds_lowered_list(nodes: &[Node]) -> bool {
1317    nodes.iter().any(|node| match node {
1318        Node::Element(element) => lowered_list_align(element).is_some(),
1319        Node::If(node) => {
1320            node.branches
1321                .iter()
1322                .any(|(_, body)| holds_lowered_list(body))
1323                || holds_lowered_list(&node.otherwise)
1324        }
1325        Node::IfLet(node) => holds_lowered_list(&node.then) || holds_lowered_list(&node.otherwise),
1326        Node::Match(node) => node.arms.iter().any(|arm| holds_lowered_list(&arm.body)),
1327        Node::Component(_) => false,
1328    })
1329}
1330
1331/// A list in a vertical scroll view builds only the rows near what is in
1332/// sight: a keyed `ForEach` among the children of a `Column` there (through
1333/// conditions) becomes a one-column grid of its rows, which every target
1334/// builds lazily (see [`lazy_grid_items`]). The grid keeps the column's
1335/// look: `align` says how the rows stand across it, `row_gap` is the
1336/// column's `gap` (absent when the column has none, for the target's own),
1337/// it staggers as the column does, and with no rows it takes no place. A
1338/// component counts as in a scroll view when every place that shows it is.
1339/// Columns that `reflow` keep building all their rows, since their rows
1340/// slide among each other.
1341pub fn lower_lists(app: &mut Application) {
1342    let mut windows: Vec<&mut Node> = app
1343        .windows
1344        .windows
1345        .iter_mut()
1346        .map(|window| &mut window.content)
1347        .collect();
1348    let mut components: Vec<(&str, &mut Node)> = app
1349        .components
1350        .iter_mut()
1351        .map(|component| (component.name.as_str(), &mut component.body))
1352        .collect();
1353    lower_bodies(&mut windows, &mut components);
1354}
1355
1356/// [`lower_lists`] over the contents of the windows and the bodies of the
1357/// components, by name.
1358fn lower_bodies(windows: &mut [&mut Node], components: &mut [(&str, &mut Node)]) {
1359    // Which components are only ever shown inside a vertical scroll view,
1360    // found by going over the tree until nothing changes.
1361    let mut scrolled: BTreeMap<String, bool> = components
1362        .iter()
1363        .map(|(name, _)| (name.to_string(), true))
1364        .collect();
1365    loop {
1366        let mut calls: BTreeMap<String, bool> = BTreeMap::new();
1367        let mut note = |name: &str, inside: bool| {
1368            let entry = calls.entry(name.to_string()).or_insert(true);
1369            *entry = *entry && inside;
1370        };
1371        for window in windows.iter() {
1372            calls_in(window, false, &mut note);
1373        }
1374        for (name, body) in components.iter() {
1375            calls_in(body, scrolled[*name], &mut note);
1376        }
1377        let next: BTreeMap<String, bool> = scrolled
1378            .keys()
1379            .map(|name| (name.clone(), calls.get(name).copied().unwrap_or(false)))
1380            .collect();
1381        if next == scrolled {
1382            break;
1383        }
1384        scrolled = next;
1385    }
1386    for window in windows.iter_mut() {
1387        lower_in(window, false);
1388    }
1389    for (name, body) in components.iter_mut() {
1390        lower_in(body, scrolled[*name]);
1391    }
1392}
1393
1394/// Whether what an element contains scrolls with a vertical scroll view
1395/// around it, given whether the element itself does.
1396fn scrolls_inside(element: &Element, scrolling: bool) -> bool {
1397    match element.kind.as_str() {
1398        "ScrollView" => element.keyword("axis") != Some("horizontal"),
1399        // A layer or page of its own, shown apart from the scroll view.
1400        "Modal" | "PopoverButton" | "Menu" | "ContextMenu" | "Toolbar" | "SplitView"
1401        | "Sidebar" | "Detail" | "Tabs" | "Tab" | "TabAccessory" | "Navigation" | "Destination"
1402        | "Window" | "WindowSet" | "Form" | "Table" => false,
1403        _ => scrolling,
1404    }
1405}
1406
1407fn calls_in(node: &Node, scrolling: bool, note: &mut impl FnMut(&str, bool)) {
1408    match node {
1409        Node::Element(element) => {
1410            let inside = scrolls_inside(element, scrolling);
1411            for child in &element.children {
1412                calls_in(child, inside, note);
1413            }
1414        }
1415        Node::Component(call) => note(&call.component, scrolling),
1416        Node::If(node) => {
1417            for (_, body) in &node.branches {
1418                body.iter()
1419                    .for_each(|child| calls_in(child, scrolling, note));
1420            }
1421            node.otherwise
1422                .iter()
1423                .for_each(|child| calls_in(child, scrolling, note));
1424        }
1425        Node::IfLet(node) => {
1426            node.then
1427                .iter()
1428                .chain(&node.otherwise)
1429                .for_each(|child| calls_in(child, scrolling, note));
1430        }
1431        Node::Match(node) => {
1432            for arm in &node.arms {
1433                arm.body
1434                    .iter()
1435                    .for_each(|child| calls_in(child, scrolling, note));
1436            }
1437        }
1438    }
1439}
1440
1441fn lower_in(node: &mut Node, scrolling: bool) {
1442    match node {
1443        Node::Element(element) => {
1444            let inside = scrolls_inside(element, scrolling);
1445            // What the rows hold first, so that the column a row's parts
1446            // are put in below is not taken for one of the app's.
1447            for child in &mut element.children {
1448                lower_in(child, inside);
1449            }
1450            if inside && element.kind == "Column" && element.prop("reflow").is_none() {
1451                let look = ListLook {
1452                    gap: element.prop("gap").cloned(),
1453                    align: element.keyword("align").unwrap_or("leading").to_string(),
1454                    stagger: element.prop("stagger").cloned(),
1455                    path: element.path.clone(),
1456                };
1457                let mut count = 0;
1458                lower_rows(&mut element.children, &look, &mut count);
1459            }
1460        }
1461        Node::Component(_) => {}
1462        Node::If(node) => {
1463            for (_, body) in &mut node.branches {
1464                body.iter_mut().for_each(|child| lower_in(child, scrolling));
1465            }
1466            node.otherwise
1467                .iter_mut()
1468                .for_each(|child| lower_in(child, scrolling));
1469        }
1470        Node::IfLet(node) => {
1471            node.then
1472                .iter_mut()
1473                .chain(&mut node.otherwise)
1474                .for_each(|child| lower_in(child, scrolling));
1475        }
1476        Node::Match(node) => {
1477            for arm in &mut node.arms {
1478                arm.body
1479                    .iter_mut()
1480                    .for_each(|child| lower_in(child, scrolling));
1481            }
1482        }
1483    }
1484}
1485
1486/// How the column a list stands in lays it out.
1487struct ListLook {
1488    gap: Option<Prop>,
1489    align: String,
1490    stagger: Option<Prop>,
1491    path: String,
1492}
1493
1494/// Each keyed `ForEach` among `children` (and in the branches of the
1495/// conditions among them), put in a one-column grid.
1496fn lower_rows(children: &mut [Node], look: &ListLook, count: &mut usize) {
1497    for child in children.iter_mut() {
1498        match child {
1499            Node::Element(each)
1500                if each.kind == "ForEach"
1501                    && each.template.is_some()
1502                    && each.expr("items").is_some()
1503                    && matches!(each.prop("key"), Some(Prop::Key { .. })) =>
1504            {
1505                let mut props = BTreeMap::new();
1506                // As many columns as fit this wide: one.
1507                props.insert("min_width".to_string(), Prop::Number { value: 1.0e6 });
1508                props.insert("gap".to_string(), Prop::Number { value: 0.0 });
1509                if let Some(gap) = &look.gap {
1510                    props.insert("row_gap".to_string(), gap.clone());
1511                }
1512                props.insert(
1513                    "align".to_string(),
1514                    Prop::Keyword {
1515                        value: look.align.clone(),
1516                    },
1517                );
1518                if let Some(stagger) = &look.stagger {
1519                    props.insert("stagger".to_string(), stagger.clone());
1520                }
1521                let source = each.source.clone();
1522                let path = format!("{}/list{}", look.path, count);
1523                *count += 1;
1524                let mut each = std::mem::replace(
1525                    child,
1526                    Node::Component(ComponentRef {
1527                        component: String::new(),
1528                        args: Vec::new(),
1529                        source: source.clone(),
1530                    }),
1531                );
1532                if let Node::Element(each) = &mut each {
1533                    stack_parts(each, look, &path);
1534                }
1535                *child = Node::Element(Element {
1536                    kind: "Grid".into(),
1537                    id: None,
1538                    path,
1539                    props,
1540                    template: None,
1541                    children: vec![each],
1542                    action: None,
1543                    source,
1544                });
1545            }
1546            Node::If(node) => {
1547                for (_, body) in &mut node.branches {
1548                    lower_rows(body, look, count);
1549                }
1550                lower_rows(&mut node.otherwise, look, count);
1551            }
1552            Node::IfLet(node) => {
1553                lower_rows(&mut node.then, look, count);
1554                lower_rows(&mut node.otherwise, look, count);
1555            }
1556            Node::Match(node) => {
1557                for arm in &mut node.arms {
1558                    lower_rows(&mut arm.body, look, count);
1559                }
1560            }
1561            _ => {}
1562        }
1563    }
1564}
1565
1566/// A row of more than one part, such as a line and the divider under it,
1567/// keeps its parts one under another as they stood in the column: in a
1568/// column of their own, as far apart as the column's children and standing
1569/// across it as they did. A grid's cell holds one row on every target,
1570/// which otherwise lays several parts over one another or apart.
1571fn stack_parts(each: &mut Element, look: &ListLook, path: &str) {
1572    if each.children.len() < 2 {
1573        return;
1574    }
1575    let mut props = BTreeMap::new();
1576    if let Some(gap) = &look.gap {
1577        props.insert("gap".to_string(), gap.clone());
1578    }
1579    props.insert(
1580        "align".to_string(),
1581        Prop::Keyword {
1582            value: look.align.clone(),
1583        },
1584    );
1585    each.children = vec![Node::Element(Element {
1586        kind: "Column".into(),
1587        id: None,
1588        path: format!("{path}/row"),
1589        props,
1590        template: None,
1591        children: std::mem::take(&mut each.children),
1592        action: None,
1593        source: each.source.clone(),
1594    })];
1595}
1596
1597/// Resource key of a text declared on an element property.
1598pub fn text_key(component: &str, node_path: &str, prop: &str) -> String {
1599    format!("{component}.{node_path}.{prop}")
1600}
1601
1602#[cfg(test)]
1603mod tests {
1604    use super::*;
1605
1606    fn element(kind: &str, children: Vec<Node>) -> Node {
1607        Node::Element(Element {
1608            kind: kind.into(),
1609            id: None,
1610            path: kind.into(),
1611            props: BTreeMap::new(),
1612            template: None,
1613            children,
1614            action: None,
1615            source: SourceLocation::default(),
1616        })
1617    }
1618
1619    fn with_props(node: Node, props: &[(&str, Prop)]) -> Node {
1620        let Node::Element(mut element) = node else {
1621            unreachable!()
1622        };
1623        for (name, prop) in props {
1624            element.props.insert(name.to_string(), prop.clone());
1625        }
1626        Node::Element(element)
1627    }
1628
1629    fn keyed_rows() -> Node {
1630        let Node::Element(mut each) = element("ForEach", vec![element("Text", vec![])]) else {
1631            unreachable!()
1632        };
1633        each.template = Some(Template {
1634            var: "row".into(),
1635            ty: Type::Int,
1636        });
1637        each.props.insert(
1638            "items".into(),
1639            Prop::Expr {
1640                expr: Expr {
1641                    kind: ExprKind::Literal {
1642                        value: Value::Int(0),
1643                    },
1644                    ty: Type::Int,
1645                },
1646            },
1647        );
1648        each.props.insert(
1649            "key".into(),
1650            Prop::Key {
1651                field: 0,
1652                name: "id".into(),
1653            },
1654        );
1655        Node::Element(each)
1656    }
1657
1658    fn kinds(node: &Node) -> Vec<String> {
1659        let Node::Element(element) = node else {
1660            return vec![];
1661        };
1662        element
1663            .children
1664            .iter()
1665            .map(|child| match child {
1666                Node::Element(child) => child.kind.clone(),
1667                Node::If(_) => "if".into(),
1668                _ => "other".into(),
1669            })
1670            .collect()
1671    }
1672
1673    #[test]
1674    fn a_list_in_a_scroll_view_becomes_a_lazy_column_of_its_rows() {
1675        let column = with_props(
1676            element(
1677                "Column",
1678                vec![
1679                    element("Text", vec![]),
1680                    keyed_rows(),
1681                    element("Spacer", vec![]),
1682                ],
1683            ),
1684            &[
1685                ("gap", Prop::Number { value: 4.0 }),
1686                (
1687                    "align",
1688                    Prop::Keyword {
1689                        value: "center".into(),
1690                    },
1691                ),
1692            ],
1693        );
1694        let mut tree = element("ScrollView", vec![column]);
1695        lower_bodies(&mut [&mut tree], &mut []);
1696        let Node::Element(scroll) = &tree else {
1697            unreachable!()
1698        };
1699        assert_eq!(kinds(&scroll.children[0]), ["Text", "Grid", "Spacer"]);
1700        let Node::Element(column) = &scroll.children[0] else {
1701            unreachable!()
1702        };
1703        let Node::Element(grid) = &column.children[1] else {
1704            unreachable!()
1705        };
1706        assert_eq!(lowered_list_align(grid), Some("center"));
1707        assert_eq!(grid.number("row_gap"), Some(4.0));
1708        assert!(
1709            lazy_grid_items(grid).is_some(),
1710            "it is a grid every target builds lazily"
1711        );
1712    }
1713
1714    #[test]
1715    fn a_row_of_several_parts_keeps_them_one_under_another() {
1716        // A line and the divider under it, and inside the row a list of
1717        // its own, which stays as the row's own.
1718        let Node::Element(mut each) = keyed_rows() else {
1719            unreachable!()
1720        };
1721        each.children = vec![
1722            element("Row", vec![]),
1723            element("Column", vec![element("Divider", vec![])]),
1724            keyed_rows(),
1725        ];
1726        let parts = each.children.clone();
1727        let column = with_props(
1728            element("Column", vec![Node::Element(each), keyed_rows()]),
1729            &[
1730                ("gap", Prop::Number { value: 4.0 }),
1731                (
1732                    "align",
1733                    Prop::Keyword {
1734                        value: "center".into(),
1735                    },
1736                ),
1737            ],
1738        );
1739        let mut tree = element("ScrollView", vec![column]);
1740        lower_bodies(&mut [&mut tree], &mut []);
1741        let rows_of = |at: usize| {
1742            let Node::Element(scroll) = &tree else {
1743                unreachable!()
1744            };
1745            let Node::Element(column) = &scroll.children[0] else {
1746                unreachable!()
1747            };
1748            let Node::Element(grid) = &column.children[at] else {
1749                unreachable!()
1750            };
1751            lazy_grid_items(grid).unwrap().clone()
1752        };
1753        let several = rows_of(0);
1754        assert_eq!(kinds(&Node::Element(several.clone())), ["Column"]);
1755        let Node::Element(stack) = &several.children[0] else {
1756            unreachable!()
1757        };
1758        assert_eq!(stack.children, parts, "the parts stay as they were");
1759        assert_eq!(stack.number("gap"), Some(4.0));
1760        assert_eq!(stack.keyword("align"), Some("center"));
1761        assert_eq!(stack.path, "Column/list0/row");
1762        // A row of one part is that part.
1763        assert_eq!(kinds(&Node::Element(rows_of(1))), ["Text"]);
1764    }
1765
1766    #[test]
1767    fn lists_outside_scroll_views_and_reflowing_lists_stay_as_they_are() {
1768        let mut outside = element("Column", vec![keyed_rows()]);
1769        let mut sideways = with_props(
1770            element("ScrollView", vec![element("Column", vec![keyed_rows()])]),
1771            &[(
1772                "axis",
1773                Prop::Keyword {
1774                    value: "horizontal".into(),
1775                },
1776            )],
1777        );
1778        let mut reflowing = element(
1779            "ScrollView",
1780            vec![with_props(
1781                element("Column", vec![keyed_rows()]),
1782                &[(
1783                    "reflow",
1784                    Prop::Keyword {
1785                        value: "smooth".into(),
1786                    },
1787                )],
1788            )],
1789        );
1790        let before = (outside.clone(), sideways.clone(), reflowing.clone());
1791        lower_bodies(&mut [&mut outside, &mut sideways, &mut reflowing], &mut []);
1792        assert_eq!((outside, sideways, reflowing), before);
1793    }
1794
1795    #[test]
1796    fn a_list_in_a_condition_and_in_a_component_shown_in_a_scroll_view_is_lowered() {
1797        let condition = Expr {
1798            kind: ExprKind::Literal {
1799                value: Value::Bool(true),
1800            },
1801            ty: Type::Bool,
1802        };
1803        let call = |name: &str| {
1804            Node::Component(ComponentRef {
1805                component: name.into(),
1806                args: vec![],
1807                source: SourceLocation::default(),
1808            })
1809        };
1810        let mut window = element("ScrollView", vec![call("songs")]);
1811        let mut songs = element(
1812            "Column",
1813            vec![Node::If(IfNode {
1814                branches: vec![(condition, vec![keyed_rows()])],
1815                otherwise: vec![],
1816            })],
1817        );
1818        // Shown both in a scroll view and outside one: left as it is.
1819        let mut window_two = element(
1820            "Column",
1821            vec![call("tags"), element("ScrollView", vec![call("tags")])],
1822        );
1823        let mut tags = element("Column", vec![keyed_rows()]);
1824        let tags_before = tags.clone();
1825        lower_bodies(
1826            &mut [&mut window, &mut window_two],
1827            &mut [("songs", &mut songs), ("tags", &mut tags)],
1828        );
1829        let Node::Element(column) = &songs else {
1830            unreachable!()
1831        };
1832        let Node::If(branch) = &column.children[0] else {
1833            unreachable!()
1834        };
1835        assert!(matches!(&branch.branches[0].1[0], Node::Element(grid) if grid.kind == "Grid"));
1836        assert_eq!(tags, tags_before);
1837    }
1838
1839    #[test]
1840    fn walks_report_the_nearest_element_around_each_node() {
1841        let condition = Expr {
1842            kind: ExprKind::Literal {
1843                value: Value::Bool(true),
1844            },
1845            ty: Type::Bool,
1846        };
1847        let tree = element(
1848            "Sidebar",
1849            vec![Node::If(IfNode {
1850                branches: vec![(condition, vec![element("Search", vec![])])],
1851                otherwise: vec![element("Column", vec![element("Text", vec![])])],
1852            })],
1853        );
1854        let mut seen = Vec::new();
1855        tree.walk_in(None, &mut |node, parent| {
1856            let name = match node {
1857                Node::Element(element) => element.kind.as_str(),
1858                Node::If(_) => "if",
1859                _ => "other",
1860            };
1861            seen.push((name, parent));
1862        });
1863        assert_eq!(
1864            seen,
1865            [
1866                ("Sidebar", None),
1867                ("if", Some("Sidebar")),
1868                ("Search", Some("Sidebar")),
1869                ("Column", Some("Sidebar")),
1870                ("Text", Some("Column")),
1871            ]
1872        );
1873        let mut count = 0;
1874        tree.walk(&mut |_| count += 1);
1875        assert_eq!(count, seen.len());
1876    }
1877}