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    /// `[app].photos`: the application saves pictures to the user's photos
389    /// (`bern::platform::save_image`), which iOS asks the user to allow.
390    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
391    pub photos: bool,
392}
393
394#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
395pub struct Languages {
396    pub supported: Vec<String>,
397    pub default: String,
398    pub strict: bool,
399}
400
401#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
402#[serde(rename_all = "snake_case")]
403pub enum ThemeMode {
404    System,
405    Light,
406    Dark,
407}
408
409#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
410pub struct Srgb {
411    pub red: u8,
412    pub green: u8,
413    pub blue: u8,
414}
415
416impl Srgb {
417    pub fn parse(hex: &str) -> Option<Self> {
418        let hex = hex.strip_prefix('#')?;
419        if hex.len() != 6 || !hex.chars().all(|c| c.is_ascii_hexdigit()) {
420            return None;
421        }
422        let byte = |index: usize| u8::from_str_radix(&hex[index..index + 2], 16).ok();
423        Some(Self {
424            red: byte(0)?,
425            green: byte(2)?,
426            blue: byte(4)?,
427        })
428    }
429}
430
431#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
432pub struct Theme {
433    pub mode: ThemeMode,
434    /// Optional brand color. Native targets map it to their accent color
435    /// interface; algorithm targets seed the Bern palette with it.
436    pub brand_seed: Option<Srgb>,
437}
438
439#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
440pub struct SessionFieldDef {
441    pub name: String,
442    #[serde(rename = "type")]
443    pub ty: Type,
444    pub initial: Value,
445}
446
447/// Typed drafts owned by a UI session, one instance per hosting window.
448#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
449pub struct SessionDef {
450    pub name: String,
451    pub fields: Vec<SessionFieldDef>,
452}
453
454impl SessionDef {
455    pub fn field_index(&self, name: &str) -> Option<usize> {
456        self.fields.iter().position(|field| field.name == name)
457    }
458}
459
460#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
461#[serde(rename_all = "snake_case")]
462pub enum WindowInitial {
463    Open,
464    Closed,
465}
466
467#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
468#[serde(rename_all = "snake_case")]
469pub enum Presentation {
470    /// Native where the target has system windows, otherwise the host's
471    /// virtual presentation (on the web: the page, or a dialog).
472    Auto,
473    /// The host's in-app presentation, even where native windows exist.
474    Virtual,
475    /// A draggable, resizable window: a native window, or a floating panel
476    /// inside the page.
477    Floating,
478    NativeRequired,
479}
480
481#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
482#[serde(rename_all = "snake_case")]
483pub enum WindowRole {
484    Standard,
485    /// Application preferences window.
486    Settings,
487    /// A modal window over the primary window, such as a first-run flow:
488    /// a sheet where the platform has them.
489    Dialog,
490}
491
492#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
493pub struct WindowDef {
494    pub id: String,
495    pub title: LocalizedText,
496    pub initial: WindowInitial,
497    pub presentation: Presentation,
498    pub role: WindowRole,
499    pub width: Option<f64>,
500    pub height: Option<f64>,
501    pub min_width: Option<f64>,
502    pub min_height: Option<f64>,
503    pub content: Node,
504    /// Session state types instantiated for each instance of this window.
505    pub sessions: Vec<String>,
506    /// Declared close check, run whenever closing is requested.
507    pub close: Option<CloseCheck>,
508    /// Whether content extends under a transparent title bar.
509    #[serde(default)]
510    pub chrome: WindowChrome,
511    /// While true, the window hides its title bar and toolbar (and on
512    /// touch platforms the status bar), and the pointer once it rests.
513    #[serde(default)]
514    pub immersive: Option<Expr>,
515    /// Seconds without input after which `window.idle` turns true.
516    #[serde(default)]
517    pub idle_after: Option<f64>,
518    /// Pause the idle timer while interacting with persistent controls.
519    #[serde(default)]
520    pub idle_enabled: Option<Expr>,
521    /// On a phone, the window stands in landscape while this is true and
522    /// in portrait otherwise; elsewhere it follows the device.
523    #[serde(default)]
524    pub landscape: Option<Expr>,
525}
526
527#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
528#[serde(rename_all = "snake_case")]
529pub enum WindowChrome {
530    /// The platform's title bar.
531    #[default]
532    Standard,
533    /// Content under a transparent title bar, the system window controls
534    /// over it.
535    Unified,
536}
537
538/// The close check of a window. When `when` holds, closing waits for a
539/// decision: Save (running `save`), Discard or Cancel. The expressions are
540/// evaluated against the closing window's own session drafts.
541#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
542pub struct CloseCheck {
543    pub when: Expr,
544    pub message: Text,
545    /// Engine command a Save decision runs; closing waits for its result.
546    pub save: Option<Action>,
547}
548
549/// Static window kinds declared by the root `WindowSet`. The first version
550/// supports single-instance windows only.
551#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
552pub struct WindowCatalog {
553    pub handle: String,
554    pub primary: String,
555    pub windows: Vec<WindowDef>,
556    /// The accent every window shows: an enum whose variants name accents
557    /// of `style!`.
558    #[serde(default)]
559    pub accent: Option<Expr>,
560    /// Optional JSON map of semantic roles to light/dark #RRGGBB pairs.
561    /// The application supplies a complete, contrast-checked palette.
562    #[serde(default)]
563    pub palette: Option<Expr>,
564    /// The appearance every window shows: an enum of `System`, `Light`
565    /// and `Dark`.
566    #[serde(default)]
567    pub appearance: Option<Expr>,
568    /// The application icon the system shows: an enum whose `Default`
569    /// variant is `[app.icon]` and whose other variants name icons of
570    /// `[app.alternate_icons]` (`DarkSkin` names `dark_skin`).
571    #[serde(default, skip_serializing_if = "Option::is_none")]
572    pub icon: Option<Expr>,
573    /// The `MenuBar`: its `CommandMenu`s, their `CommandItem`s, dividers
574    /// and submenus, whose drafts are the primary window's. Only a target
575    /// with a system menu bar shows it.
576    #[serde(default, skip_serializing_if = "Option::is_none")]
577    pub menu_bar: Option<Element>,
578    /// The `StatusItem`: the application's item in the system's status
579    /// area and the `CommandItem`s, dividers and submenus of its menu, which
580    /// run without a window. Only a target with a status area shows it; with
581    /// it, closing the last window leaves the application running.
582    #[serde(default, skip_serializing_if = "Option::is_none")]
583    pub status_item: Option<Element>,
584}
585
586#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
587pub struct SourceLocation {
588    pub file: String,
589    pub line: u32,
590    pub column: u32,
591}
592
593#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
594#[serde(tag = "kind", rename_all = "snake_case")]
595pub enum ParamKind {
596    /// The typed engine client of the application protocol (its namespace)
597    /// or of one of its modules.
598    Client { namespace: String },
599    /// The generated window handle.
600    Windows,
601    /// The application's player catalog.
602    Players,
603    /// A value of a schema type, read-only inside the component.
604    Value {
605        #[serde(rename = "type")]
606        ty: Type,
607    },
608    /// Session state owned by the hosting window.
609    Session { session: String },
610}
611
612#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
613pub struct ParamDef {
614    pub name: String,
615    pub kind: ParamKind,
616    /// `#[content]`: text the component shows as data, so callers pass
617    /// data and never a literal.
618    #[serde(default, skip_serializing_if = "is_false")]
619    pub content: bool,
620}
621
622fn is_false(value: &bool) -> bool {
623    !value
624}
625
626#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
627pub struct ComponentDef {
628    pub name: String,
629    pub module: Vec<String>,
630    pub params: Vec<ParamDef>,
631    pub body: Node,
632    pub source: SourceLocation,
633}
634
635#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
636#[serde(rename_all = "snake_case")]
637pub enum BinaryOp {
638    Eq,
639    Ne,
640    Lt,
641    Le,
642    Gt,
643    Ge,
644    And,
645    Or,
646}
647
648/// Where a binding reads from.
649#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
650#[serde(tag = "scope", rename_all = "snake_case")]
651pub enum Binding {
652    /// Engine state field, then record field indices.
653    Engine { field: usize, path: Vec<usize> },
654    /// Session state of the hosting window.
655    Session {
656        session: String,
657        field: usize,
658        path: Vec<usize>,
659    },
660    /// Template variable, pattern binding or component parameter, with the
661    /// type of the variable itself so record paths can be resolved.
662    Local {
663        name: String,
664        path: Vec<usize>,
665        root: Type,
666    },
667    /// UI state of the hosting window, such as `window.idle`.
668    Window { field: String },
669}
670
671#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
672#[serde(tag = "kind", rename_all = "snake_case")]
673pub enum ExprKind {
674    Literal {
675        value: Value,
676    },
677    Binding {
678        binding: Binding,
679    },
680    Variant {
681        variant: u32,
682        fields: Vec<Expr>,
683    },
684    Not {
685        expr: Box<Expr>,
686    },
687    Neg {
688        expr: Box<Expr>,
689    },
690    Binary {
691        op: BinaryOp,
692        lhs: Box<Expr>,
693        rhs: Box<Expr>,
694    },
695    /// One of two values of the expression's type.
696    If {
697        cond: Box<Expr>,
698        then: Box<Expr>,
699        otherwise: Box<Expr>,
700    },
701    /// A named corner radius of `style!` (`corner { card: 22 }`), a float.
702    /// `radius` is its value on every platform; [`Application::on_platform`]
703    /// puts the platform's value in its place before a target generates.
704    Corner {
705        name: String,
706        radius: f64,
707    },
708}
709
710/// A type-checked restricted expression.
711#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
712pub struct Expr {
713    pub kind: ExprKind,
714    #[serde(rename = "type")]
715    pub ty: Type,
716}
717
718#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
719#[serde(tag = "kind", rename_all = "snake_case")]
720pub enum Text {
721    /// Declared translations, with typed interpolation arguments.
722    Localized {
723        text: LocalizedText,
724        args: Vec<(String, Expr)>,
725    },
726    /// Runtime data such as user input or media titles.
727    Dynamic { expr: Expr },
728}
729
730/// A writable session field bound to a control.
731#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
732pub struct FieldRef {
733    pub session: String,
734    pub field: usize,
735    #[serde(rename = "type")]
736    pub ty: Type,
737}
738
739#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
740#[serde(tag = "kind", rename_all = "snake_case")]
741pub enum Prop {
742    Keyword {
743        value: String,
744    },
745    Number {
746        value: f64,
747    },
748    Icon {
749        name: String,
750    },
751    /// A declared resource, by its name in `[resources]`.
752    Resource {
753        name: String,
754    },
755    Text {
756        text: Text,
757    },
758    Expr {
759        expr: Expr,
760    },
761    Field {
762        field: FieldRef,
763    },
764    Window {
765        id: String,
766    },
767    TypeName {
768        name: String,
769    },
770    Key {
771        field: usize,
772        name: String,
773    },
774    Id {
775        value: String,
776    },
777    /// An application player.
778    Player {
779        id: String,
780    },
781    /// A typed query call whose result feeds the element's template.
782    Query {
783        query: QueryBinding,
784    },
785    /// The feature of the enclosing contract an entry reaches.
786    Feature {
787        contract: String,
788        feature: String,
789    },
790}
791
792/// A query of the engine catalog bound with its arguments. The arguments
793/// are restricted expressions; when their values change, the target asks
794/// again under a new parameter version and shows only the current answer.
795#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
796pub struct QueryBinding {
797    /// Catalog index of the query in the protocol.
798    pub query: usize,
799    pub args: Vec<Expr>,
800    /// The query's declared result.
801    pub result: Type,
802}
803
804#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
805#[serde(rename_all = "snake_case")]
806pub enum WindowOp {
807    ToggleFullscreen,
808    Open,
809    Close,
810    Activate,
811}
812
813/// A standard media command on an application player.
814#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
815#[serde(rename_all = "snake_case")]
816pub enum MediaOp {
817    Open,
818    Play,
819    Pause,
820    Seek,
821    SetVolume,
822    SetMuted,
823}
824
825impl MediaOp {
826    pub const ALL: [MediaOp; 6] = [
827        MediaOp::Open,
828        MediaOp::Play,
829        MediaOp::Pause,
830        MediaOp::Seek,
831        MediaOp::SetVolume,
832        MediaOp::SetMuted,
833    ];
834
835    /// Method name in declarations, for example `open`.
836    pub fn method(self) -> &'static str {
837        match self {
838            MediaOp::Open => "open",
839            MediaOp::Play => "play",
840            MediaOp::Pause => "pause",
841            MediaOp::Seek => "seek",
842            MediaOp::SetVolume => "set_volume",
843            MediaOp::SetMuted => "set_muted",
844        }
845    }
846
847    /// Command id in the media catalog, for example `media.open`.
848    pub fn command(self) -> &'static str {
849        match self {
850            MediaOp::Open => "media.open",
851            MediaOp::Play => "media.play",
852            MediaOp::Pause => "media.pause",
853            MediaOp::Seek => "media.seek",
854            MediaOp::SetVolume => "media.set_volume",
855            MediaOp::SetMuted => "media.set_muted",
856        }
857    }
858}
859
860#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
861#[serde(tag = "kind", rename_all = "snake_case")]
862pub enum Action {
863    /// Engine command by catalog index with positional arguments.
864    Engine { command: usize, args: Vec<Expr> },
865    /// Generated window session command.
866    Window { op: WindowOp, window: String },
867    /// Standard media command on an application player; the player id is
868    /// the first parameter, `args` the rest.
869    Media {
870        player: String,
871        op: MediaOp,
872        args: Vec<Expr>,
873    },
874    /// A change of a session draft: interface state such as an open panel
875    /// or a step of a flow, not engine state.
876    Session { field: FieldRef, op: SessionOp },
877}
878
879#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
880#[serde(tag = "kind", rename_all = "snake_case")]
881pub enum SessionOp {
882    /// `draft.field.set(value)`.
883    Set { value: Expr },
884    /// `draft.field.toggle()` on a `bool`.
885    Toggle,
886    /// `draft.field.push_str(text)` on a `String`: the text after it, as
887    /// a key of an on-screen keyboard types.
888    PushStr { value: Expr },
889    /// `draft.field.pop()` on a `String`: without its last character, as
890    /// a keyboard's delete.
891    Pop,
892}
893
894#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
895pub struct Template {
896    pub var: String,
897    #[serde(rename = "type")]
898    pub ty: Type,
899}
900
901#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
902pub struct Element {
903    /// Built-in component name from [`spec::COMPONENTS`].
904    pub kind: String,
905    /// Control identity for interactive components.
906    pub id: Option<String>,
907    /// Structural path inside the declaring component, stable for a build.
908    pub path: String,
909    pub props: BTreeMap<String, Prop>,
910    pub template: Option<Template>,
911    pub children: Vec<Node>,
912    pub action: Option<Action>,
913    pub source: SourceLocation,
914}
915
916impl Element {
917    pub fn prop(&self, name: &str) -> Option<&Prop> {
918        self.props.get(name)
919    }
920
921    /// The items of its `ContextMenu`, dividers among them, if it has one.
922    pub fn context_menu(&self) -> Option<Vec<&Element>> {
923        self.children.iter().find_map(|node| match node {
924            Node::Element(menu) if menu.kind == "ContextMenu" => Some(
925                menu.children
926                    .iter()
927                    .filter_map(|node| match node {
928                        Node::Element(item) => Some(item),
929                        _ => None,
930                    })
931                    .collect(),
932            ),
933            _ => None,
934        })
935    }
936
937    /// Whether it has children to show besides a `ContextMenu`.
938    pub fn has_content(&self) -> bool {
939        self.children
940            .iter()
941            .any(|node| !matches!(node, Node::Element(menu) if menu.kind == "ContextMenu"))
942    }
943
944    pub fn keyword(&self, name: &str) -> Option<&str> {
945        match self.props.get(name) {
946            Some(Prop::Keyword { value }) => Some(value),
947            _ => None,
948        }
949    }
950
951    pub fn number(&self, name: &str) -> Option<f64> {
952        match self.props.get(name) {
953            Some(Prop::Number { value }) => Some(*value),
954            _ => None,
955        }
956    }
957
958    pub fn text(&self, name: &str) -> Option<&Text> {
959        match self.props.get(name) {
960            Some(Prop::Text { text }) => Some(text),
961            _ => None,
962        }
963    }
964
965    pub fn icon(&self, name: &str) -> Option<&str> {
966        match self.props.get(name) {
967            Some(Prop::Icon { name }) => Some(name),
968            _ => None,
969        }
970    }
971
972    pub fn resource(&self, name: &str) -> Option<&str> {
973        match self.props.get(name) {
974            Some(Prop::Resource { name }) => Some(name),
975            _ => None,
976        }
977    }
978
979    /// What follows the element's action: with `submit: clear` on a
980    /// `TextInput`, or `clears: draft.x` on a `Button`, the draft becomes
981    /// empty, and the keyboard goes back to the window.
982    pub fn clearing(&self) -> Option<Action> {
983        let field = if self.keyword("submit") == Some("clear") {
984            self.field("value")?
985        } else {
986            self.field("clears")?
987        };
988        Some(Action::Session {
989            field: field.clone(),
990            op: SessionOp::Set {
991                value: Expr {
992                    kind: ExprKind::Literal {
993                        value: Value::Text(String::new()),
994                    },
995                    ty: Type::Text,
996                },
997            },
998        })
999    }
1000
1001    pub fn field(&self, name: &str) -> Option<&FieldRef> {
1002        match self.props.get(name) {
1003            Some(Prop::Field { field }) => Some(field),
1004            _ => None,
1005        }
1006    }
1007
1008    pub fn expr(&self, name: &str) -> Option<&Expr> {
1009        match self.props.get(name) {
1010            Some(Prop::Expr { expr }) => Some(expr),
1011            _ => None,
1012        }
1013    }
1014
1015    pub fn query(&self, name: &str) -> Option<&QueryBinding> {
1016        match self.props.get(name) {
1017            Some(Prop::Query { query }) => Some(query),
1018            _ => None,
1019        }
1020    }
1021}
1022
1023#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1024#[serde(tag = "kind", rename_all = "snake_case")]
1025pub enum ComponentArg {
1026    Client,
1027    Windows,
1028    Players,
1029    Value { expr: Expr },
1030}
1031
1032#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1033pub struct ComponentRef {
1034    pub component: String,
1035    pub args: Vec<(String, ComponentArg)>,
1036    pub source: SourceLocation,
1037}
1038
1039#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1040pub struct IfNode {
1041    pub branches: Vec<(Expr, Vec<Node>)>,
1042    pub otherwise: Vec<Node>,
1043}
1044
1045#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1046pub struct IfLetNode {
1047    pub var: String,
1048    pub value: Expr,
1049    #[serde(rename = "inner_type")]
1050    pub inner: Type,
1051    pub then: Vec<Node>,
1052    pub otherwise: Vec<Node>,
1053}
1054
1055#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1056pub struct MatchBinding {
1057    pub name: String,
1058    pub field: usize,
1059    #[serde(rename = "type")]
1060    pub ty: Type,
1061}
1062
1063#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1064pub struct MatchArm {
1065    /// `None` for the `_` arm.
1066    pub variant: Option<u32>,
1067    pub bindings: Vec<MatchBinding>,
1068    pub body: Vec<Node>,
1069}
1070
1071#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1072pub struct MatchNode {
1073    pub value: Expr,
1074    pub enum_name: String,
1075    pub arms: Vec<MatchArm>,
1076}
1077
1078#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1079#[serde(tag = "node", rename_all = "snake_case")]
1080pub enum Node {
1081    Element(Element),
1082    Component(ComponentRef),
1083    If(IfNode),
1084    IfLet(IfLetNode),
1085    Match(MatchNode),
1086}
1087
1088impl Node {
1089    /// Visit this node and all descendants, depth first.
1090    pub fn walk<'a>(&'a self, visit: &mut impl FnMut(&'a Node)) {
1091        self.walk_in(None, &mut |node, _| visit(node));
1092    }
1093
1094    /// Visit every element at or under this node, depth first, to change
1095    /// it.
1096    pub fn elements_mut(&mut self, visit: &mut impl FnMut(&mut Element)) {
1097        let children: Vec<&mut Node> = match self {
1098            Node::Element(element) => {
1099                visit(element);
1100                element.children.iter_mut().collect()
1101            }
1102            Node::Component(_) => Vec::new(),
1103            Node::If(node) => node
1104                .branches
1105                .iter_mut()
1106                .flat_map(|(_, body)| body.iter_mut())
1107                .chain(node.otherwise.iter_mut())
1108                .collect(),
1109            Node::IfLet(node) => node
1110                .then
1111                .iter_mut()
1112                .chain(node.otherwise.iter_mut())
1113                .collect(),
1114            Node::Match(node) => node
1115                .arms
1116                .iter_mut()
1117                .flat_map(|arm| arm.body.iter_mut())
1118                .collect(),
1119        };
1120        for child in children {
1121            child.elements_mut(visit);
1122        }
1123    }
1124
1125    /// Visit this node and all descendants, depth first, each with the kind
1126    /// of the nearest element around it; `parent` is the one around this
1127    /// node.
1128    pub fn walk_in<'a>(
1129        &'a self,
1130        parent: Option<&'a str>,
1131        visit: &mut impl FnMut(&'a Node, Option<&'a str>),
1132    ) {
1133        visit(self, parent);
1134        let parent = match self {
1135            Node::Element(element) => Some(element.kind.as_str()),
1136            _ => parent,
1137        };
1138        match self {
1139            Node::Element(element) => {
1140                for child in &element.children {
1141                    child.walk_in(parent, visit);
1142                }
1143            }
1144            Node::Component(_) => {}
1145            Node::If(node) => {
1146                for (_, body) in &node.branches {
1147                    for child in body {
1148                        child.walk_in(parent, visit);
1149                    }
1150                }
1151                for child in &node.otherwise {
1152                    child.walk_in(parent, visit);
1153                }
1154            }
1155            Node::IfLet(node) => {
1156                for child in node.then.iter().chain(&node.otherwise) {
1157                    child.walk_in(parent, visit);
1158                }
1159            }
1160            Node::Match(node) => {
1161                for arm in &node.arms {
1162                    for child in &arm.body {
1163                        child.walk_in(parent, visit);
1164                    }
1165                }
1166            }
1167        }
1168    }
1169}
1170
1171impl Expr {
1172    /// Visit this expression and every sub-expression.
1173    pub fn walk<'a>(&'a self, visit: &mut impl FnMut(&'a Expr)) {
1174        visit(self);
1175        match &self.kind {
1176            ExprKind::Literal { .. } | ExprKind::Binding { .. } | ExprKind::Corner { .. } => {}
1177            ExprKind::Variant { fields, .. } => {
1178                for field in fields {
1179                    field.walk(visit);
1180                }
1181            }
1182            ExprKind::Not { expr } | ExprKind::Neg { expr } => expr.walk(visit),
1183            ExprKind::Binary { lhs, rhs, .. } => {
1184                lhs.walk(visit);
1185                rhs.walk(visit);
1186            }
1187            ExprKind::If {
1188                cond,
1189                then,
1190                otherwise,
1191            } => {
1192                cond.walk(visit);
1193                then.walk(visit);
1194                otherwise.walk(visit);
1195            }
1196        }
1197    }
1198
1199    /// Session state types this expression reads.
1200    pub fn sessions(&self) -> Vec<String> {
1201        let mut sessions = Vec::new();
1202        self.walk(&mut |expr| {
1203            if let ExprKind::Binding {
1204                binding: Binding::Session { session, .. },
1205            } = &expr.kind
1206                && !sessions.contains(session)
1207            {
1208                sessions.push(session.clone());
1209            }
1210        });
1211        sessions
1212    }
1213
1214    /// Whether this expression reads engine state.
1215    pub fn reads_engine(&self) -> bool {
1216        let mut reads = false;
1217        self.walk(&mut |expr| {
1218            if let ExprKind::Binding {
1219                binding: Binding::Engine { .. },
1220            } = &expr.kind
1221            {
1222                reads = true;
1223            }
1224        });
1225        reads
1226    }
1227}
1228
1229impl Text {
1230    /// Expressions interpolated into this text.
1231    pub fn exprs(&self) -> Vec<&Expr> {
1232        match self {
1233            Text::Localized { args, .. } => args.iter().map(|(_, expr)| expr).collect(),
1234            Text::Dynamic { expr } => vec![expr],
1235        }
1236    }
1237}
1238
1239impl CloseCheck {
1240    /// Every expression the check evaluates.
1241    pub fn exprs(&self) -> Vec<&Expr> {
1242        let mut exprs = vec![&self.when];
1243        exprs.extend(self.message.exprs());
1244        if let Some(Action::Engine { args, .. }) = &self.save {
1245            exprs.extend(args);
1246        }
1247        exprs
1248    }
1249}
1250
1251pub use bern_protocol::label_key;
1252
1253/// Version of the [`ComponentLibrary`] layout.
1254pub const LIBRARY_DESCRIPTOR_VERSION: u32 = 1;
1255
1256/// Versioned description of a component library crate, generated from its
1257/// sources. Applications check it against their own configuration before
1258/// using its components: descriptor and IR versions, the crate version it
1259/// was generated from, and translations for every required language.
1260#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1261pub struct ComponentLibrary {
1262    pub descriptor_version: u32,
1263    pub ir_version: u32,
1264    /// Cargo package name of the library.
1265    pub package: String,
1266    /// Cargo package version the descriptor was generated from.
1267    pub version: String,
1268    /// Fingerprint of the library sources and configuration.
1269    pub source_fingerprint: String,
1270    pub languages: Vec<String>,
1271    pub types: TypeTable,
1272    pub components: Vec<ComponentDef>,
1273    pub texts: Vec<LocalizedText>,
1274}
1275
1276/// Whether `node`, followed in a row by `next`, is the words that lead the
1277/// row: a text, or a column standing at its leading edge, before a spacer.
1278/// It takes the spacer's room itself, so the controls after it keep their
1279/// width and the words wrap in what is left, as a list row's label does.
1280pub fn leads_row(node: &Node, next: Option<&Node>) -> bool {
1281    let words = matches!(node, Node::Element(element)
1282        if element.prop("width").is_none()
1283            && (element.kind == "Text"
1284                || (element.kind == "Column"
1285                    && matches!(element.keyword("align"), None | Some("leading")))));
1286    words && matches!(next, Some(Node::Element(spacer)) if spacer.kind == "Spacer")
1287}
1288
1289/// The `ForEach` of a grid that, standing in a vertical scroll view, builds
1290/// only the rows near what is in sight: a grid whose one child is a keyed
1291/// `ForEach`, not laid out as masonry. Every target builds such a grid
1292/// lazily; anywhere else it builds all its rows.
1293pub fn lazy_grid_items(grid: &Element) -> Option<&Element> {
1294    if grid.kind != "Grid" || grid.keyword("masonry") == Some("true") {
1295        return None;
1296    }
1297    match grid.children.as_slice() {
1298        [Node::Element(each)]
1299            if each.kind == "ForEach"
1300                && each.template.is_some()
1301                && each.expr("items").is_some()
1302                && matches!(each.prop("key"), Some(Prop::Key { .. })) =>
1303        {
1304            Some(each)
1305        }
1306        _ => None,
1307    }
1308}
1309
1310/// The `align` a list lowered by [`lower_lists`] keeps: how its rows
1311/// stand across it, as in the column it came from.
1312pub fn lowered_list_align(grid: &Element) -> Option<&str> {
1313    (grid.kind == "Grid")
1314        .then(|| grid.keyword("align"))
1315        .flatten()
1316}
1317
1318/// Whether `nodes`, the children of a column, hold a list lowered by
1319/// [`lower_lists`], directly or in their conditions.
1320pub fn holds_lowered_list(nodes: &[Node]) -> bool {
1321    nodes.iter().any(|node| match node {
1322        Node::Element(element) => lowered_list_align(element).is_some(),
1323        Node::If(node) => {
1324            node.branches
1325                .iter()
1326                .any(|(_, body)| holds_lowered_list(body))
1327                || holds_lowered_list(&node.otherwise)
1328        }
1329        Node::IfLet(node) => holds_lowered_list(&node.then) || holds_lowered_list(&node.otherwise),
1330        Node::Match(node) => node.arms.iter().any(|arm| holds_lowered_list(&arm.body)),
1331        Node::Component(_) => false,
1332    })
1333}
1334
1335/// A list in a vertical scroll view builds only the rows near what is in
1336/// sight: a keyed `ForEach` among the children of a `Column` there (through
1337/// conditions) becomes a one-column grid of its rows, which every target
1338/// builds lazily (see [`lazy_grid_items`]). The grid keeps the column's
1339/// look: `align` says how the rows stand across it, `row_gap` is the
1340/// column's `gap` (absent when the column has none, for the target's own),
1341/// it staggers as the column does, and with no rows it takes no place. A
1342/// component counts as in a scroll view when every place that shows it is.
1343/// Columns that `reflow` keep building all their rows, since their rows
1344/// slide among each other.
1345pub fn lower_lists(app: &mut Application) {
1346    let mut windows: Vec<&mut Node> = app
1347        .windows
1348        .windows
1349        .iter_mut()
1350        .map(|window| &mut window.content)
1351        .collect();
1352    let mut components: Vec<(&str, &mut Node)> = app
1353        .components
1354        .iter_mut()
1355        .map(|component| (component.name.as_str(), &mut component.body))
1356        .collect();
1357    lower_bodies(&mut windows, &mut components);
1358}
1359
1360/// [`lower_lists`] over the contents of the windows and the bodies of the
1361/// components, by name.
1362fn lower_bodies(windows: &mut [&mut Node], components: &mut [(&str, &mut Node)]) {
1363    // Which components are only ever shown inside a vertical scroll view,
1364    // found by going over the tree until nothing changes.
1365    let mut scrolled: BTreeMap<String, bool> = components
1366        .iter()
1367        .map(|(name, _)| (name.to_string(), true))
1368        .collect();
1369    loop {
1370        let mut calls: BTreeMap<String, bool> = BTreeMap::new();
1371        let mut note = |name: &str, inside: bool| {
1372            let entry = calls.entry(name.to_string()).or_insert(true);
1373            *entry = *entry && inside;
1374        };
1375        for window in windows.iter() {
1376            calls_in(window, false, &mut note);
1377        }
1378        for (name, body) in components.iter() {
1379            calls_in(body, scrolled[*name], &mut note);
1380        }
1381        let next: BTreeMap<String, bool> = scrolled
1382            .keys()
1383            .map(|name| (name.clone(), calls.get(name).copied().unwrap_or(false)))
1384            .collect();
1385        if next == scrolled {
1386            break;
1387        }
1388        scrolled = next;
1389    }
1390    for window in windows.iter_mut() {
1391        lower_in(window, false);
1392    }
1393    for (name, body) in components.iter_mut() {
1394        lower_in(body, scrolled[*name]);
1395    }
1396}
1397
1398/// Whether what an element contains scrolls with a vertical scroll view
1399/// around it, given whether the element itself does.
1400fn scrolls_inside(element: &Element, scrolling: bool) -> bool {
1401    match element.kind.as_str() {
1402        "ScrollView" => element.keyword("axis") != Some("horizontal"),
1403        // A layer or page of its own, shown apart from the scroll view.
1404        "Modal" | "PopoverButton" | "Menu" | "ContextMenu" | "Toolbar" | "SplitView"
1405        | "Sidebar" | "Detail" | "Tabs" | "Tab" | "TabAccessory" | "Navigation" | "Destination"
1406        | "Window" | "WindowSet" | "Form" | "Table" => false,
1407        _ => scrolling,
1408    }
1409}
1410
1411fn calls_in(node: &Node, scrolling: bool, note: &mut impl FnMut(&str, bool)) {
1412    match node {
1413        Node::Element(element) => {
1414            let inside = scrolls_inside(element, scrolling);
1415            for child in &element.children {
1416                calls_in(child, inside, note);
1417            }
1418        }
1419        Node::Component(call) => note(&call.component, scrolling),
1420        Node::If(node) => {
1421            for (_, body) in &node.branches {
1422                body.iter()
1423                    .for_each(|child| calls_in(child, scrolling, note));
1424            }
1425            node.otherwise
1426                .iter()
1427                .for_each(|child| calls_in(child, scrolling, note));
1428        }
1429        Node::IfLet(node) => {
1430            node.then
1431                .iter()
1432                .chain(&node.otherwise)
1433                .for_each(|child| calls_in(child, scrolling, note));
1434        }
1435        Node::Match(node) => {
1436            for arm in &node.arms {
1437                arm.body
1438                    .iter()
1439                    .for_each(|child| calls_in(child, scrolling, note));
1440            }
1441        }
1442    }
1443}
1444
1445fn lower_in(node: &mut Node, scrolling: bool) {
1446    match node {
1447        Node::Element(element) => {
1448            let inside = scrolls_inside(element, scrolling);
1449            // What the rows hold first, so that the column a row's parts
1450            // are put in below is not taken for one of the app's.
1451            for child in &mut element.children {
1452                lower_in(child, inside);
1453            }
1454            if inside && element.kind == "Column" && element.prop("reflow").is_none() {
1455                let look = ListLook {
1456                    gap: element.prop("gap").cloned(),
1457                    align: element.keyword("align").unwrap_or("leading").to_string(),
1458                    stagger: element.prop("stagger").cloned(),
1459                    path: element.path.clone(),
1460                };
1461                let mut count = 0;
1462                lower_rows(&mut element.children, &look, &mut count);
1463            }
1464        }
1465        Node::Component(_) => {}
1466        Node::If(node) => {
1467            for (_, body) in &mut node.branches {
1468                body.iter_mut().for_each(|child| lower_in(child, scrolling));
1469            }
1470            node.otherwise
1471                .iter_mut()
1472                .for_each(|child| lower_in(child, scrolling));
1473        }
1474        Node::IfLet(node) => {
1475            node.then
1476                .iter_mut()
1477                .chain(&mut node.otherwise)
1478                .for_each(|child| lower_in(child, scrolling));
1479        }
1480        Node::Match(node) => {
1481            for arm in &mut node.arms {
1482                arm.body
1483                    .iter_mut()
1484                    .for_each(|child| lower_in(child, scrolling));
1485            }
1486        }
1487    }
1488}
1489
1490/// How the column a list stands in lays it out.
1491struct ListLook {
1492    gap: Option<Prop>,
1493    align: String,
1494    stagger: Option<Prop>,
1495    path: String,
1496}
1497
1498/// Each keyed `ForEach` among `children` (and in the branches of the
1499/// conditions among them), put in a one-column grid.
1500fn lower_rows(children: &mut [Node], look: &ListLook, count: &mut usize) {
1501    for child in children.iter_mut() {
1502        match child {
1503            Node::Element(each)
1504                if each.kind == "ForEach"
1505                    && each.template.is_some()
1506                    && each.expr("items").is_some()
1507                    && matches!(each.prop("key"), Some(Prop::Key { .. })) =>
1508            {
1509                let mut props = BTreeMap::new();
1510                // As many columns as fit this wide: one.
1511                props.insert("min_width".to_string(), Prop::Number { value: 1.0e6 });
1512                props.insert("gap".to_string(), Prop::Number { value: 0.0 });
1513                if let Some(gap) = &look.gap {
1514                    props.insert("row_gap".to_string(), gap.clone());
1515                }
1516                props.insert(
1517                    "align".to_string(),
1518                    Prop::Keyword {
1519                        value: look.align.clone(),
1520                    },
1521                );
1522                if let Some(stagger) = &look.stagger {
1523                    props.insert("stagger".to_string(), stagger.clone());
1524                }
1525                let source = each.source.clone();
1526                let path = format!("{}/list{}", look.path, count);
1527                *count += 1;
1528                let mut each = std::mem::replace(
1529                    child,
1530                    Node::Component(ComponentRef {
1531                        component: String::new(),
1532                        args: Vec::new(),
1533                        source: source.clone(),
1534                    }),
1535                );
1536                if let Node::Element(each) = &mut each {
1537                    stack_parts(each, look, &path);
1538                }
1539                *child = Node::Element(Element {
1540                    kind: "Grid".into(),
1541                    id: None,
1542                    path,
1543                    props,
1544                    template: None,
1545                    children: vec![each],
1546                    action: None,
1547                    source,
1548                });
1549            }
1550            Node::If(node) => {
1551                for (_, body) in &mut node.branches {
1552                    lower_rows(body, look, count);
1553                }
1554                lower_rows(&mut node.otherwise, look, count);
1555            }
1556            Node::IfLet(node) => {
1557                lower_rows(&mut node.then, look, count);
1558                lower_rows(&mut node.otherwise, look, count);
1559            }
1560            Node::Match(node) => {
1561                for arm in &mut node.arms {
1562                    lower_rows(&mut arm.body, look, count);
1563                }
1564            }
1565            _ => {}
1566        }
1567    }
1568}
1569
1570/// A row of more than one part, such as a line and the divider under it,
1571/// keeps its parts one under another as they stood in the column: in a
1572/// column of their own, as far apart as the column's children and standing
1573/// across it as they did. A grid's cell holds one row on every target,
1574/// which otherwise lays several parts over one another or apart.
1575fn stack_parts(each: &mut Element, look: &ListLook, path: &str) {
1576    if each.children.len() < 2 {
1577        return;
1578    }
1579    let mut props = BTreeMap::new();
1580    if let Some(gap) = &look.gap {
1581        props.insert("gap".to_string(), gap.clone());
1582    }
1583    props.insert(
1584        "align".to_string(),
1585        Prop::Keyword {
1586            value: look.align.clone(),
1587        },
1588    );
1589    each.children = vec![Node::Element(Element {
1590        kind: "Column".into(),
1591        id: None,
1592        path: format!("{path}/row"),
1593        props,
1594        template: None,
1595        children: std::mem::take(&mut each.children),
1596        action: None,
1597        source: each.source.clone(),
1598    })];
1599}
1600
1601/// Resource key of a text declared on an element property.
1602pub fn text_key(component: &str, node_path: &str, prop: &str) -> String {
1603    format!("{component}.{node_path}.{prop}")
1604}
1605
1606#[cfg(test)]
1607mod tests {
1608    use super::*;
1609
1610    fn element(kind: &str, children: Vec<Node>) -> Node {
1611        Node::Element(Element {
1612            kind: kind.into(),
1613            id: None,
1614            path: kind.into(),
1615            props: BTreeMap::new(),
1616            template: None,
1617            children,
1618            action: None,
1619            source: SourceLocation::default(),
1620        })
1621    }
1622
1623    fn with_props(node: Node, props: &[(&str, Prop)]) -> Node {
1624        let Node::Element(mut element) = node else {
1625            unreachable!()
1626        };
1627        for (name, prop) in props {
1628            element.props.insert(name.to_string(), prop.clone());
1629        }
1630        Node::Element(element)
1631    }
1632
1633    fn keyed_rows() -> Node {
1634        let Node::Element(mut each) = element("ForEach", vec![element("Text", vec![])]) else {
1635            unreachable!()
1636        };
1637        each.template = Some(Template {
1638            var: "row".into(),
1639            ty: Type::Int,
1640        });
1641        each.props.insert(
1642            "items".into(),
1643            Prop::Expr {
1644                expr: Expr {
1645                    kind: ExprKind::Literal {
1646                        value: Value::Int(0),
1647                    },
1648                    ty: Type::Int,
1649                },
1650            },
1651        );
1652        each.props.insert(
1653            "key".into(),
1654            Prop::Key {
1655                field: 0,
1656                name: "id".into(),
1657            },
1658        );
1659        Node::Element(each)
1660    }
1661
1662    fn kinds(node: &Node) -> Vec<String> {
1663        let Node::Element(element) = node else {
1664            return vec![];
1665        };
1666        element
1667            .children
1668            .iter()
1669            .map(|child| match child {
1670                Node::Element(child) => child.kind.clone(),
1671                Node::If(_) => "if".into(),
1672                _ => "other".into(),
1673            })
1674            .collect()
1675    }
1676
1677    #[test]
1678    fn a_list_in_a_scroll_view_becomes_a_lazy_column_of_its_rows() {
1679        let column = with_props(
1680            element(
1681                "Column",
1682                vec![
1683                    element("Text", vec![]),
1684                    keyed_rows(),
1685                    element("Spacer", vec![]),
1686                ],
1687            ),
1688            &[
1689                ("gap", Prop::Number { value: 4.0 }),
1690                (
1691                    "align",
1692                    Prop::Keyword {
1693                        value: "center".into(),
1694                    },
1695                ),
1696            ],
1697        );
1698        let mut tree = element("ScrollView", vec![column]);
1699        lower_bodies(&mut [&mut tree], &mut []);
1700        let Node::Element(scroll) = &tree else {
1701            unreachable!()
1702        };
1703        assert_eq!(kinds(&scroll.children[0]), ["Text", "Grid", "Spacer"]);
1704        let Node::Element(column) = &scroll.children[0] else {
1705            unreachable!()
1706        };
1707        let Node::Element(grid) = &column.children[1] else {
1708            unreachable!()
1709        };
1710        assert_eq!(lowered_list_align(grid), Some("center"));
1711        assert_eq!(grid.number("row_gap"), Some(4.0));
1712        assert!(
1713            lazy_grid_items(grid).is_some(),
1714            "it is a grid every target builds lazily"
1715        );
1716    }
1717
1718    #[test]
1719    fn a_row_of_several_parts_keeps_them_one_under_another() {
1720        // A line and the divider under it, and inside the row a list of
1721        // its own, which stays as the row's own.
1722        let Node::Element(mut each) = keyed_rows() else {
1723            unreachable!()
1724        };
1725        each.children = vec![
1726            element("Row", vec![]),
1727            element("Column", vec![element("Divider", vec![])]),
1728            keyed_rows(),
1729        ];
1730        let parts = each.children.clone();
1731        let column = with_props(
1732            element("Column", vec![Node::Element(each), keyed_rows()]),
1733            &[
1734                ("gap", Prop::Number { value: 4.0 }),
1735                (
1736                    "align",
1737                    Prop::Keyword {
1738                        value: "center".into(),
1739                    },
1740                ),
1741            ],
1742        );
1743        let mut tree = element("ScrollView", vec![column]);
1744        lower_bodies(&mut [&mut tree], &mut []);
1745        let rows_of = |at: usize| {
1746            let Node::Element(scroll) = &tree else {
1747                unreachable!()
1748            };
1749            let Node::Element(column) = &scroll.children[0] else {
1750                unreachable!()
1751            };
1752            let Node::Element(grid) = &column.children[at] else {
1753                unreachable!()
1754            };
1755            lazy_grid_items(grid).unwrap().clone()
1756        };
1757        let several = rows_of(0);
1758        assert_eq!(kinds(&Node::Element(several.clone())), ["Column"]);
1759        let Node::Element(stack) = &several.children[0] else {
1760            unreachable!()
1761        };
1762        assert_eq!(stack.children, parts, "the parts stay as they were");
1763        assert_eq!(stack.number("gap"), Some(4.0));
1764        assert_eq!(stack.keyword("align"), Some("center"));
1765        assert_eq!(stack.path, "Column/list0/row");
1766        // A row of one part is that part.
1767        assert_eq!(kinds(&Node::Element(rows_of(1))), ["Text"]);
1768    }
1769
1770    #[test]
1771    fn lists_outside_scroll_views_and_reflowing_lists_stay_as_they_are() {
1772        let mut outside = element("Column", vec![keyed_rows()]);
1773        let mut sideways = with_props(
1774            element("ScrollView", vec![element("Column", vec![keyed_rows()])]),
1775            &[(
1776                "axis",
1777                Prop::Keyword {
1778                    value: "horizontal".into(),
1779                },
1780            )],
1781        );
1782        let mut reflowing = element(
1783            "ScrollView",
1784            vec![with_props(
1785                element("Column", vec![keyed_rows()]),
1786                &[(
1787                    "reflow",
1788                    Prop::Keyword {
1789                        value: "smooth".into(),
1790                    },
1791                )],
1792            )],
1793        );
1794        let before = (outside.clone(), sideways.clone(), reflowing.clone());
1795        lower_bodies(&mut [&mut outside, &mut sideways, &mut reflowing], &mut []);
1796        assert_eq!((outside, sideways, reflowing), before);
1797    }
1798
1799    #[test]
1800    fn a_list_in_a_condition_and_in_a_component_shown_in_a_scroll_view_is_lowered() {
1801        let condition = Expr {
1802            kind: ExprKind::Literal {
1803                value: Value::Bool(true),
1804            },
1805            ty: Type::Bool,
1806        };
1807        let call = |name: &str| {
1808            Node::Component(ComponentRef {
1809                component: name.into(),
1810                args: vec![],
1811                source: SourceLocation::default(),
1812            })
1813        };
1814        let mut window = element("ScrollView", vec![call("songs")]);
1815        let mut songs = element(
1816            "Column",
1817            vec![Node::If(IfNode {
1818                branches: vec![(condition, vec![keyed_rows()])],
1819                otherwise: vec![],
1820            })],
1821        );
1822        // Shown both in a scroll view and outside one: left as it is.
1823        let mut window_two = element(
1824            "Column",
1825            vec![call("tags"), element("ScrollView", vec![call("tags")])],
1826        );
1827        let mut tags = element("Column", vec![keyed_rows()]);
1828        let tags_before = tags.clone();
1829        lower_bodies(
1830            &mut [&mut window, &mut window_two],
1831            &mut [("songs", &mut songs), ("tags", &mut tags)],
1832        );
1833        let Node::Element(column) = &songs else {
1834            unreachable!()
1835        };
1836        let Node::If(branch) = &column.children[0] else {
1837            unreachable!()
1838        };
1839        assert!(matches!(&branch.branches[0].1[0], Node::Element(grid) if grid.kind == "Grid"));
1840        assert_eq!(tags, tags_before);
1841    }
1842
1843    #[test]
1844    fn walks_report_the_nearest_element_around_each_node() {
1845        let condition = Expr {
1846            kind: ExprKind::Literal {
1847                value: Value::Bool(true),
1848            },
1849            ty: Type::Bool,
1850        };
1851        let tree = element(
1852            "Sidebar",
1853            vec![Node::If(IfNode {
1854                branches: vec![(condition, vec![element("Search", vec![])])],
1855                otherwise: vec![element("Column", vec![element("Text", vec![])])],
1856            })],
1857        );
1858        let mut seen = Vec::new();
1859        tree.walk_in(None, &mut |node, parent| {
1860            let name = match node {
1861                Node::Element(element) => element.kind.as_str(),
1862                Node::If(_) => "if",
1863                _ => "other",
1864            };
1865            seen.push((name, parent));
1866        });
1867        assert_eq!(
1868            seen,
1869            [
1870                ("Sidebar", None),
1871                ("if", Some("Sidebar")),
1872                ("Search", Some("Sidebar")),
1873                ("Column", Some("Sidebar")),
1874                ("Text", Some("Column")),
1875            ]
1876        );
1877        let mut count = 0;
1878        tree.walk(&mut |_| count += 1);
1879        assert_eq!(count, seen.len());
1880    }
1881}