1pub 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
17pub 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 pub protocol: ProtocolDescriptor,
28 pub types: TypeTable,
30 pub sessions: Vec<SessionDef>,
31 pub windows: WindowCatalog,
32 pub players: Vec<PlayerDef>,
34 pub components: Vec<ComponentDef>,
35 pub texts: Vec<LocalizedText>,
37 pub resources: Vec<ResourceDef>,
39 pub layouts: Option<LayoutConfig>,
42 pub contracts: Vec<FeatureContract>,
44 #[serde(default)]
46 pub style: style::StyleSheet,
47}
48
49impl Application {
50 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 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#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
98pub struct LayoutConfig {
99 pub compact_below: f64,
101 #[serde(default, skip_serializing_if = "Option::is_none")]
103 pub medium_below: Option<f64>,
104 #[serde(default, skip_serializing_if = "Option::is_none")]
107 pub expanded_below: Option<f64>,
108}
109
110impl LayoutConfig {
111 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 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#[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 pub const ALL: [LayoutSize; 4] = [
147 LayoutSize::Compact,
148 LayoutSize::Medium,
149 LayoutSize::Expanded,
150 LayoutSize::Large,
151 ];
152
153 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 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#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
185pub struct FeatureContract {
186 pub name: String,
188 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 pub name: String,
200 #[serde(default, skip_serializing_if = "String::is_empty")]
201 pub doc: String,
202 pub commands: Vec<String>,
205 pub layouts: Vec<LayoutSize>,
207 pub guaranteed: Option<String>,
209 pub exemptions: Vec<Exemption>,
210 pub coverage: Vec<SizeCoverage>,
212 pub source: SourceLocation,
213}
214
215#[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#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
230#[serde(tag = "kind", rename_all = "snake_case")]
231pub enum Coverage {
232 Covered {
234 entries: Vec<String>,
235 },
236 Conditional {
238 entries: Vec<String>,
239 },
240 Missing,
241}
242
243#[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 Guaranteed {
252 entries: Vec<String>,
253 reason: String,
254 },
255 Exempt {
257 reason: String,
258 },
259 Missing,
260 Unproven {
262 entries: Vec<String>,
263 },
264}
265
266impl FeatureResult {
267 pub fn is_error(&self) -> bool {
269 matches!(
270 self,
271 FeatureResult::Missing | FeatureResult::Unproven { .. }
272 )
273 }
274}
275
276impl FeatureDef {
277 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#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
315pub struct ResourceDef {
316 pub name: String,
319 pub declared: SourceLocation,
321 #[serde(default)]
324 pub picture: Option<SvgPicture>,
325}
326
327#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
329#[serde(rename_all = "snake_case")]
330pub enum SvgPicture {
331 Still,
333 Tinted,
335 Themed,
338}
339
340#[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 #[serde(default, skip_serializing_if = "Option::is_none")]
359 pub icon: Option<String>,
360 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
364 pub platform_icons: BTreeMap<String, String>,
365 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
370 pub alternate_icons: BTreeMap<String, BTreeMap<String, String>>,
371 #[serde(default, skip_serializing_if = "Option::is_none")]
374 pub publisher: Option<String>,
375 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
378 pub files_folder: bool,
379 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
383 pub background_audio: bool,
384 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
387 pub overlays: bool,
388 #[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 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#[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 Auto,
473 Virtual,
475 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 Settings,
487 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 pub sessions: Vec<String>,
506 pub close: Option<CloseCheck>,
508 #[serde(default)]
510 pub chrome: WindowChrome,
511 #[serde(default)]
514 pub immersive: Option<Expr>,
515 #[serde(default)]
517 pub idle_after: Option<f64>,
518 #[serde(default)]
520 pub idle_enabled: Option<Expr>,
521 #[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 #[default]
532 Standard,
533 Unified,
536}
537
538#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
542pub struct CloseCheck {
543 pub when: Expr,
544 pub message: Text,
545 pub save: Option<Action>,
547}
548
549#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
552pub struct WindowCatalog {
553 pub handle: String,
554 pub primary: String,
555 pub windows: Vec<WindowDef>,
556 #[serde(default)]
559 pub accent: Option<Expr>,
560 #[serde(default)]
563 pub palette: Option<Expr>,
564 #[serde(default)]
567 pub appearance: Option<Expr>,
568 #[serde(default, skip_serializing_if = "Option::is_none")]
572 pub icon: Option<Expr>,
573 #[serde(default, skip_serializing_if = "Option::is_none")]
577 pub menu_bar: Option<Element>,
578 #[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 Client { namespace: String },
599 Windows,
601 Players,
603 Value {
605 #[serde(rename = "type")]
606 ty: Type,
607 },
608 Session { session: String },
610}
611
612#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
613pub struct ParamDef {
614 pub name: String,
615 pub kind: ParamKind,
616 #[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#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
650#[serde(tag = "scope", rename_all = "snake_case")]
651pub enum Binding {
652 Engine { field: usize, path: Vec<usize> },
654 Session {
656 session: String,
657 field: usize,
658 path: Vec<usize>,
659 },
660 Local {
663 name: String,
664 path: Vec<usize>,
665 root: Type,
666 },
667 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 If {
697 cond: Box<Expr>,
698 then: Box<Expr>,
699 otherwise: Box<Expr>,
700 },
701 Corner {
705 name: String,
706 radius: f64,
707 },
708}
709
710#[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 Localized {
723 text: LocalizedText,
724 args: Vec<(String, Expr)>,
725 },
726 Dynamic { expr: Expr },
728}
729
730#[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 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 Player {
779 id: String,
780 },
781 Query {
783 query: QueryBinding,
784 },
785 Feature {
787 contract: String,
788 feature: String,
789 },
790}
791
792#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
796pub struct QueryBinding {
797 pub query: usize,
799 pub args: Vec<Expr>,
800 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#[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 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 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: usize, args: Vec<Expr> },
865 Window { op: WindowOp, window: String },
867 Media {
870 player: String,
871 op: MediaOp,
872 args: Vec<Expr>,
873 },
874 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 Set { value: Expr },
884 Toggle,
886 PushStr { value: Expr },
889 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 pub kind: String,
905 pub id: Option<String>,
907 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 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 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 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 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 pub fn walk<'a>(&'a self, visit: &mut impl FnMut(&'a Node)) {
1091 self.walk_in(None, &mut |node, _| visit(node));
1092 }
1093
1094 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 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 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 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 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 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 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
1253pub const LIBRARY_DESCRIPTOR_VERSION: u32 = 1;
1255
1256#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1261pub struct ComponentLibrary {
1262 pub descriptor_version: u32,
1263 pub ir_version: u32,
1264 pub package: String,
1266 pub version: String,
1268 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
1276pub 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
1289pub 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
1310pub fn lowered_list_align(grid: &Element) -> Option<&str> {
1313 (grid.kind == "Grid")
1314 .then(|| grid.keyword("align"))
1315 .flatten()
1316}
1317
1318pub 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
1335pub 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
1360fn lower_bodies(windows: &mut [&mut Node], components: &mut [(&str, &mut Node)]) {
1363 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
1398fn scrolls_inside(element: &Element, scrolling: bool) -> bool {
1401 match element.kind.as_str() {
1402 "ScrollView" => element.keyword("axis") != Some("horizontal"),
1403 "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 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
1490struct ListLook {
1492 gap: Option<Prop>,
1493 align: String,
1494 stagger: Option<Prop>,
1495 path: String,
1496}
1497
1498fn 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 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
1570fn 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
1601pub 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 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 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 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}