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}
389
390#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
391pub struct Languages {
392 pub supported: Vec<String>,
393 pub default: String,
394 pub strict: bool,
395}
396
397#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
398#[serde(rename_all = "snake_case")]
399pub enum ThemeMode {
400 System,
401 Light,
402 Dark,
403}
404
405#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
406pub struct Srgb {
407 pub red: u8,
408 pub green: u8,
409 pub blue: u8,
410}
411
412impl Srgb {
413 pub fn parse(hex: &str) -> Option<Self> {
414 let hex = hex.strip_prefix('#')?;
415 if hex.len() != 6 || !hex.chars().all(|c| c.is_ascii_hexdigit()) {
416 return None;
417 }
418 let byte = |index: usize| u8::from_str_radix(&hex[index..index + 2], 16).ok();
419 Some(Self {
420 red: byte(0)?,
421 green: byte(2)?,
422 blue: byte(4)?,
423 })
424 }
425}
426
427#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
428pub struct Theme {
429 pub mode: ThemeMode,
430 pub brand_seed: Option<Srgb>,
433}
434
435#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
436pub struct SessionFieldDef {
437 pub name: String,
438 #[serde(rename = "type")]
439 pub ty: Type,
440 pub initial: Value,
441}
442
443#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
445pub struct SessionDef {
446 pub name: String,
447 pub fields: Vec<SessionFieldDef>,
448}
449
450impl SessionDef {
451 pub fn field_index(&self, name: &str) -> Option<usize> {
452 self.fields.iter().position(|field| field.name == name)
453 }
454}
455
456#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
457#[serde(rename_all = "snake_case")]
458pub enum WindowInitial {
459 Open,
460 Closed,
461}
462
463#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
464#[serde(rename_all = "snake_case")]
465pub enum Presentation {
466 Auto,
469 Virtual,
471 Floating,
474 NativeRequired,
475}
476
477#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
478#[serde(rename_all = "snake_case")]
479pub enum WindowRole {
480 Standard,
481 Settings,
483 Dialog,
486}
487
488#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
489pub struct WindowDef {
490 pub id: String,
491 pub title: LocalizedText,
492 pub initial: WindowInitial,
493 pub presentation: Presentation,
494 pub role: WindowRole,
495 pub width: Option<f64>,
496 pub height: Option<f64>,
497 pub min_width: Option<f64>,
498 pub min_height: Option<f64>,
499 pub content: Node,
500 pub sessions: Vec<String>,
502 pub close: Option<CloseCheck>,
504 #[serde(default)]
506 pub chrome: WindowChrome,
507 #[serde(default)]
510 pub immersive: Option<Expr>,
511 #[serde(default)]
513 pub idle_after: Option<f64>,
514 #[serde(default)]
516 pub idle_enabled: Option<Expr>,
517 #[serde(default)]
520 pub landscape: Option<Expr>,
521}
522
523#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
524#[serde(rename_all = "snake_case")]
525pub enum WindowChrome {
526 #[default]
528 Standard,
529 Unified,
532}
533
534#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
538pub struct CloseCheck {
539 pub when: Expr,
540 pub message: Text,
541 pub save: Option<Action>,
543}
544
545#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
548pub struct WindowCatalog {
549 pub handle: String,
550 pub primary: String,
551 pub windows: Vec<WindowDef>,
552 #[serde(default)]
555 pub accent: Option<Expr>,
556 #[serde(default)]
559 pub palette: Option<Expr>,
560 #[serde(default)]
563 pub appearance: Option<Expr>,
564 #[serde(default, skip_serializing_if = "Option::is_none")]
568 pub icon: Option<Expr>,
569 #[serde(default, skip_serializing_if = "Option::is_none")]
573 pub menu_bar: Option<Element>,
574 #[serde(default, skip_serializing_if = "Option::is_none")]
579 pub status_item: Option<Element>,
580}
581
582#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
583pub struct SourceLocation {
584 pub file: String,
585 pub line: u32,
586 pub column: u32,
587}
588
589#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
590#[serde(tag = "kind", rename_all = "snake_case")]
591pub enum ParamKind {
592 Client { namespace: String },
595 Windows,
597 Players,
599 Value {
601 #[serde(rename = "type")]
602 ty: Type,
603 },
604 Session { session: String },
606}
607
608#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
609pub struct ParamDef {
610 pub name: String,
611 pub kind: ParamKind,
612 #[serde(default, skip_serializing_if = "is_false")]
615 pub content: bool,
616}
617
618fn is_false(value: &bool) -> bool {
619 !value
620}
621
622#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
623pub struct ComponentDef {
624 pub name: String,
625 pub module: Vec<String>,
626 pub params: Vec<ParamDef>,
627 pub body: Node,
628 pub source: SourceLocation,
629}
630
631#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
632#[serde(rename_all = "snake_case")]
633pub enum BinaryOp {
634 Eq,
635 Ne,
636 Lt,
637 Le,
638 Gt,
639 Ge,
640 And,
641 Or,
642}
643
644#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
646#[serde(tag = "scope", rename_all = "snake_case")]
647pub enum Binding {
648 Engine { field: usize, path: Vec<usize> },
650 Session {
652 session: String,
653 field: usize,
654 path: Vec<usize>,
655 },
656 Local {
659 name: String,
660 path: Vec<usize>,
661 root: Type,
662 },
663 Window { field: String },
665}
666
667#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
668#[serde(tag = "kind", rename_all = "snake_case")]
669pub enum ExprKind {
670 Literal {
671 value: Value,
672 },
673 Binding {
674 binding: Binding,
675 },
676 Variant {
677 variant: u32,
678 fields: Vec<Expr>,
679 },
680 Not {
681 expr: Box<Expr>,
682 },
683 Neg {
684 expr: Box<Expr>,
685 },
686 Binary {
687 op: BinaryOp,
688 lhs: Box<Expr>,
689 rhs: Box<Expr>,
690 },
691 If {
693 cond: Box<Expr>,
694 then: Box<Expr>,
695 otherwise: Box<Expr>,
696 },
697 Corner {
701 name: String,
702 radius: f64,
703 },
704}
705
706#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
708pub struct Expr {
709 pub kind: ExprKind,
710 #[serde(rename = "type")]
711 pub ty: Type,
712}
713
714#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
715#[serde(tag = "kind", rename_all = "snake_case")]
716pub enum Text {
717 Localized {
719 text: LocalizedText,
720 args: Vec<(String, Expr)>,
721 },
722 Dynamic { expr: Expr },
724}
725
726#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
728pub struct FieldRef {
729 pub session: String,
730 pub field: usize,
731 #[serde(rename = "type")]
732 pub ty: Type,
733}
734
735#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
736#[serde(tag = "kind", rename_all = "snake_case")]
737pub enum Prop {
738 Keyword {
739 value: String,
740 },
741 Number {
742 value: f64,
743 },
744 Icon {
745 name: String,
746 },
747 Resource {
749 name: String,
750 },
751 Text {
752 text: Text,
753 },
754 Expr {
755 expr: Expr,
756 },
757 Field {
758 field: FieldRef,
759 },
760 Window {
761 id: String,
762 },
763 TypeName {
764 name: String,
765 },
766 Key {
767 field: usize,
768 name: String,
769 },
770 Id {
771 value: String,
772 },
773 Player {
775 id: String,
776 },
777 Query {
779 query: QueryBinding,
780 },
781 Feature {
783 contract: String,
784 feature: String,
785 },
786}
787
788#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
792pub struct QueryBinding {
793 pub query: usize,
795 pub args: Vec<Expr>,
796 pub result: Type,
798}
799
800#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
801#[serde(rename_all = "snake_case")]
802pub enum WindowOp {
803 ToggleFullscreen,
804 Open,
805 Close,
806 Activate,
807}
808
809#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
811#[serde(rename_all = "snake_case")]
812pub enum MediaOp {
813 Open,
814 Play,
815 Pause,
816 Seek,
817 SetVolume,
818 SetMuted,
819}
820
821impl MediaOp {
822 pub const ALL: [MediaOp; 6] = [
823 MediaOp::Open,
824 MediaOp::Play,
825 MediaOp::Pause,
826 MediaOp::Seek,
827 MediaOp::SetVolume,
828 MediaOp::SetMuted,
829 ];
830
831 pub fn method(self) -> &'static str {
833 match self {
834 MediaOp::Open => "open",
835 MediaOp::Play => "play",
836 MediaOp::Pause => "pause",
837 MediaOp::Seek => "seek",
838 MediaOp::SetVolume => "set_volume",
839 MediaOp::SetMuted => "set_muted",
840 }
841 }
842
843 pub fn command(self) -> &'static str {
845 match self {
846 MediaOp::Open => "media.open",
847 MediaOp::Play => "media.play",
848 MediaOp::Pause => "media.pause",
849 MediaOp::Seek => "media.seek",
850 MediaOp::SetVolume => "media.set_volume",
851 MediaOp::SetMuted => "media.set_muted",
852 }
853 }
854}
855
856#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
857#[serde(tag = "kind", rename_all = "snake_case")]
858pub enum Action {
859 Engine { command: usize, args: Vec<Expr> },
861 Window { op: WindowOp, window: String },
863 Media {
866 player: String,
867 op: MediaOp,
868 args: Vec<Expr>,
869 },
870 Session { field: FieldRef, op: SessionOp },
873}
874
875#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
876#[serde(tag = "kind", rename_all = "snake_case")]
877pub enum SessionOp {
878 Set { value: Expr },
880 Toggle,
882 PushStr { value: Expr },
885 Pop,
888}
889
890#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
891pub struct Template {
892 pub var: String,
893 #[serde(rename = "type")]
894 pub ty: Type,
895}
896
897#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
898pub struct Element {
899 pub kind: String,
901 pub id: Option<String>,
903 pub path: String,
905 pub props: BTreeMap<String, Prop>,
906 pub template: Option<Template>,
907 pub children: Vec<Node>,
908 pub action: Option<Action>,
909 pub source: SourceLocation,
910}
911
912impl Element {
913 pub fn prop(&self, name: &str) -> Option<&Prop> {
914 self.props.get(name)
915 }
916
917 pub fn context_menu(&self) -> Option<Vec<&Element>> {
919 self.children.iter().find_map(|node| match node {
920 Node::Element(menu) if menu.kind == "ContextMenu" => Some(
921 menu.children
922 .iter()
923 .filter_map(|node| match node {
924 Node::Element(item) => Some(item),
925 _ => None,
926 })
927 .collect(),
928 ),
929 _ => None,
930 })
931 }
932
933 pub fn has_content(&self) -> bool {
935 self.children
936 .iter()
937 .any(|node| !matches!(node, Node::Element(menu) if menu.kind == "ContextMenu"))
938 }
939
940 pub fn keyword(&self, name: &str) -> Option<&str> {
941 match self.props.get(name) {
942 Some(Prop::Keyword { value }) => Some(value),
943 _ => None,
944 }
945 }
946
947 pub fn number(&self, name: &str) -> Option<f64> {
948 match self.props.get(name) {
949 Some(Prop::Number { value }) => Some(*value),
950 _ => None,
951 }
952 }
953
954 pub fn text(&self, name: &str) -> Option<&Text> {
955 match self.props.get(name) {
956 Some(Prop::Text { text }) => Some(text),
957 _ => None,
958 }
959 }
960
961 pub fn icon(&self, name: &str) -> Option<&str> {
962 match self.props.get(name) {
963 Some(Prop::Icon { name }) => Some(name),
964 _ => None,
965 }
966 }
967
968 pub fn resource(&self, name: &str) -> Option<&str> {
969 match self.props.get(name) {
970 Some(Prop::Resource { name }) => Some(name),
971 _ => None,
972 }
973 }
974
975 pub fn clearing(&self) -> Option<Action> {
979 let field = if self.keyword("submit") == Some("clear") {
980 self.field("value")?
981 } else {
982 self.field("clears")?
983 };
984 Some(Action::Session {
985 field: field.clone(),
986 op: SessionOp::Set {
987 value: Expr {
988 kind: ExprKind::Literal {
989 value: Value::Text(String::new()),
990 },
991 ty: Type::Text,
992 },
993 },
994 })
995 }
996
997 pub fn field(&self, name: &str) -> Option<&FieldRef> {
998 match self.props.get(name) {
999 Some(Prop::Field { field }) => Some(field),
1000 _ => None,
1001 }
1002 }
1003
1004 pub fn expr(&self, name: &str) -> Option<&Expr> {
1005 match self.props.get(name) {
1006 Some(Prop::Expr { expr }) => Some(expr),
1007 _ => None,
1008 }
1009 }
1010
1011 pub fn query(&self, name: &str) -> Option<&QueryBinding> {
1012 match self.props.get(name) {
1013 Some(Prop::Query { query }) => Some(query),
1014 _ => None,
1015 }
1016 }
1017}
1018
1019#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1020#[serde(tag = "kind", rename_all = "snake_case")]
1021pub enum ComponentArg {
1022 Client,
1023 Windows,
1024 Players,
1025 Value { expr: Expr },
1026}
1027
1028#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1029pub struct ComponentRef {
1030 pub component: String,
1031 pub args: Vec<(String, ComponentArg)>,
1032 pub source: SourceLocation,
1033}
1034
1035#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1036pub struct IfNode {
1037 pub branches: Vec<(Expr, Vec<Node>)>,
1038 pub otherwise: Vec<Node>,
1039}
1040
1041#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1042pub struct IfLetNode {
1043 pub var: String,
1044 pub value: Expr,
1045 #[serde(rename = "inner_type")]
1046 pub inner: Type,
1047 pub then: Vec<Node>,
1048 pub otherwise: Vec<Node>,
1049}
1050
1051#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1052pub struct MatchBinding {
1053 pub name: String,
1054 pub field: usize,
1055 #[serde(rename = "type")]
1056 pub ty: Type,
1057}
1058
1059#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1060pub struct MatchArm {
1061 pub variant: Option<u32>,
1063 pub bindings: Vec<MatchBinding>,
1064 pub body: Vec<Node>,
1065}
1066
1067#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1068pub struct MatchNode {
1069 pub value: Expr,
1070 pub enum_name: String,
1071 pub arms: Vec<MatchArm>,
1072}
1073
1074#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1075#[serde(tag = "node", rename_all = "snake_case")]
1076pub enum Node {
1077 Element(Element),
1078 Component(ComponentRef),
1079 If(IfNode),
1080 IfLet(IfLetNode),
1081 Match(MatchNode),
1082}
1083
1084impl Node {
1085 pub fn walk<'a>(&'a self, visit: &mut impl FnMut(&'a Node)) {
1087 self.walk_in(None, &mut |node, _| visit(node));
1088 }
1089
1090 pub fn elements_mut(&mut self, visit: &mut impl FnMut(&mut Element)) {
1093 let children: Vec<&mut Node> = match self {
1094 Node::Element(element) => {
1095 visit(element);
1096 element.children.iter_mut().collect()
1097 }
1098 Node::Component(_) => Vec::new(),
1099 Node::If(node) => node
1100 .branches
1101 .iter_mut()
1102 .flat_map(|(_, body)| body.iter_mut())
1103 .chain(node.otherwise.iter_mut())
1104 .collect(),
1105 Node::IfLet(node) => node
1106 .then
1107 .iter_mut()
1108 .chain(node.otherwise.iter_mut())
1109 .collect(),
1110 Node::Match(node) => node
1111 .arms
1112 .iter_mut()
1113 .flat_map(|arm| arm.body.iter_mut())
1114 .collect(),
1115 };
1116 for child in children {
1117 child.elements_mut(visit);
1118 }
1119 }
1120
1121 pub fn walk_in<'a>(
1125 &'a self,
1126 parent: Option<&'a str>,
1127 visit: &mut impl FnMut(&'a Node, Option<&'a str>),
1128 ) {
1129 visit(self, parent);
1130 let parent = match self {
1131 Node::Element(element) => Some(element.kind.as_str()),
1132 _ => parent,
1133 };
1134 match self {
1135 Node::Element(element) => {
1136 for child in &element.children {
1137 child.walk_in(parent, visit);
1138 }
1139 }
1140 Node::Component(_) => {}
1141 Node::If(node) => {
1142 for (_, body) in &node.branches {
1143 for child in body {
1144 child.walk_in(parent, visit);
1145 }
1146 }
1147 for child in &node.otherwise {
1148 child.walk_in(parent, visit);
1149 }
1150 }
1151 Node::IfLet(node) => {
1152 for child in node.then.iter().chain(&node.otherwise) {
1153 child.walk_in(parent, visit);
1154 }
1155 }
1156 Node::Match(node) => {
1157 for arm in &node.arms {
1158 for child in &arm.body {
1159 child.walk_in(parent, visit);
1160 }
1161 }
1162 }
1163 }
1164 }
1165}
1166
1167impl Expr {
1168 pub fn walk<'a>(&'a self, visit: &mut impl FnMut(&'a Expr)) {
1170 visit(self);
1171 match &self.kind {
1172 ExprKind::Literal { .. } | ExprKind::Binding { .. } | ExprKind::Corner { .. } => {}
1173 ExprKind::Variant { fields, .. } => {
1174 for field in fields {
1175 field.walk(visit);
1176 }
1177 }
1178 ExprKind::Not { expr } | ExprKind::Neg { expr } => expr.walk(visit),
1179 ExprKind::Binary { lhs, rhs, .. } => {
1180 lhs.walk(visit);
1181 rhs.walk(visit);
1182 }
1183 ExprKind::If {
1184 cond,
1185 then,
1186 otherwise,
1187 } => {
1188 cond.walk(visit);
1189 then.walk(visit);
1190 otherwise.walk(visit);
1191 }
1192 }
1193 }
1194
1195 pub fn sessions(&self) -> Vec<String> {
1197 let mut sessions = Vec::new();
1198 self.walk(&mut |expr| {
1199 if let ExprKind::Binding {
1200 binding: Binding::Session { session, .. },
1201 } = &expr.kind
1202 && !sessions.contains(session)
1203 {
1204 sessions.push(session.clone());
1205 }
1206 });
1207 sessions
1208 }
1209
1210 pub fn reads_engine(&self) -> bool {
1212 let mut reads = false;
1213 self.walk(&mut |expr| {
1214 if let ExprKind::Binding {
1215 binding: Binding::Engine { .. },
1216 } = &expr.kind
1217 {
1218 reads = true;
1219 }
1220 });
1221 reads
1222 }
1223}
1224
1225impl Text {
1226 pub fn exprs(&self) -> Vec<&Expr> {
1228 match self {
1229 Text::Localized { args, .. } => args.iter().map(|(_, expr)| expr).collect(),
1230 Text::Dynamic { expr } => vec![expr],
1231 }
1232 }
1233}
1234
1235impl CloseCheck {
1236 pub fn exprs(&self) -> Vec<&Expr> {
1238 let mut exprs = vec![&self.when];
1239 exprs.extend(self.message.exprs());
1240 if let Some(Action::Engine { args, .. }) = &self.save {
1241 exprs.extend(args);
1242 }
1243 exprs
1244 }
1245}
1246
1247pub use bern_protocol::label_key;
1248
1249pub const LIBRARY_DESCRIPTOR_VERSION: u32 = 1;
1251
1252#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1257pub struct ComponentLibrary {
1258 pub descriptor_version: u32,
1259 pub ir_version: u32,
1260 pub package: String,
1262 pub version: String,
1264 pub source_fingerprint: String,
1266 pub languages: Vec<String>,
1267 pub types: TypeTable,
1268 pub components: Vec<ComponentDef>,
1269 pub texts: Vec<LocalizedText>,
1270}
1271
1272pub fn leads_row(node: &Node, next: Option<&Node>) -> bool {
1277 let words = matches!(node, Node::Element(element)
1278 if element.prop("width").is_none()
1279 && (element.kind == "Text"
1280 || (element.kind == "Column"
1281 && matches!(element.keyword("align"), None | Some("leading")))));
1282 words && matches!(next, Some(Node::Element(spacer)) if spacer.kind == "Spacer")
1283}
1284
1285pub fn lazy_grid_items(grid: &Element) -> Option<&Element> {
1290 if grid.kind != "Grid" || grid.keyword("masonry") == Some("true") {
1291 return None;
1292 }
1293 match grid.children.as_slice() {
1294 [Node::Element(each)]
1295 if each.kind == "ForEach"
1296 && each.template.is_some()
1297 && each.expr("items").is_some()
1298 && matches!(each.prop("key"), Some(Prop::Key { .. })) =>
1299 {
1300 Some(each)
1301 }
1302 _ => None,
1303 }
1304}
1305
1306pub fn lowered_list_align(grid: &Element) -> Option<&str> {
1309 (grid.kind == "Grid")
1310 .then(|| grid.keyword("align"))
1311 .flatten()
1312}
1313
1314pub fn holds_lowered_list(nodes: &[Node]) -> bool {
1317 nodes.iter().any(|node| match node {
1318 Node::Element(element) => lowered_list_align(element).is_some(),
1319 Node::If(node) => {
1320 node.branches
1321 .iter()
1322 .any(|(_, body)| holds_lowered_list(body))
1323 || holds_lowered_list(&node.otherwise)
1324 }
1325 Node::IfLet(node) => holds_lowered_list(&node.then) || holds_lowered_list(&node.otherwise),
1326 Node::Match(node) => node.arms.iter().any(|arm| holds_lowered_list(&arm.body)),
1327 Node::Component(_) => false,
1328 })
1329}
1330
1331pub fn lower_lists(app: &mut Application) {
1342 let mut windows: Vec<&mut Node> = app
1343 .windows
1344 .windows
1345 .iter_mut()
1346 .map(|window| &mut window.content)
1347 .collect();
1348 let mut components: Vec<(&str, &mut Node)> = app
1349 .components
1350 .iter_mut()
1351 .map(|component| (component.name.as_str(), &mut component.body))
1352 .collect();
1353 lower_bodies(&mut windows, &mut components);
1354}
1355
1356fn lower_bodies(windows: &mut [&mut Node], components: &mut [(&str, &mut Node)]) {
1359 let mut scrolled: BTreeMap<String, bool> = components
1362 .iter()
1363 .map(|(name, _)| (name.to_string(), true))
1364 .collect();
1365 loop {
1366 let mut calls: BTreeMap<String, bool> = BTreeMap::new();
1367 let mut note = |name: &str, inside: bool| {
1368 let entry = calls.entry(name.to_string()).or_insert(true);
1369 *entry = *entry && inside;
1370 };
1371 for window in windows.iter() {
1372 calls_in(window, false, &mut note);
1373 }
1374 for (name, body) in components.iter() {
1375 calls_in(body, scrolled[*name], &mut note);
1376 }
1377 let next: BTreeMap<String, bool> = scrolled
1378 .keys()
1379 .map(|name| (name.clone(), calls.get(name).copied().unwrap_or(false)))
1380 .collect();
1381 if next == scrolled {
1382 break;
1383 }
1384 scrolled = next;
1385 }
1386 for window in windows.iter_mut() {
1387 lower_in(window, false);
1388 }
1389 for (name, body) in components.iter_mut() {
1390 lower_in(body, scrolled[*name]);
1391 }
1392}
1393
1394fn scrolls_inside(element: &Element, scrolling: bool) -> bool {
1397 match element.kind.as_str() {
1398 "ScrollView" => element.keyword("axis") != Some("horizontal"),
1399 "Modal" | "PopoverButton" | "Menu" | "ContextMenu" | "Toolbar" | "SplitView"
1401 | "Sidebar" | "Detail" | "Tabs" | "Tab" | "TabAccessory" | "Navigation" | "Destination"
1402 | "Window" | "WindowSet" | "Form" | "Table" => false,
1403 _ => scrolling,
1404 }
1405}
1406
1407fn calls_in(node: &Node, scrolling: bool, note: &mut impl FnMut(&str, bool)) {
1408 match node {
1409 Node::Element(element) => {
1410 let inside = scrolls_inside(element, scrolling);
1411 for child in &element.children {
1412 calls_in(child, inside, note);
1413 }
1414 }
1415 Node::Component(call) => note(&call.component, scrolling),
1416 Node::If(node) => {
1417 for (_, body) in &node.branches {
1418 body.iter()
1419 .for_each(|child| calls_in(child, scrolling, note));
1420 }
1421 node.otherwise
1422 .iter()
1423 .for_each(|child| calls_in(child, scrolling, note));
1424 }
1425 Node::IfLet(node) => {
1426 node.then
1427 .iter()
1428 .chain(&node.otherwise)
1429 .for_each(|child| calls_in(child, scrolling, note));
1430 }
1431 Node::Match(node) => {
1432 for arm in &node.arms {
1433 arm.body
1434 .iter()
1435 .for_each(|child| calls_in(child, scrolling, note));
1436 }
1437 }
1438 }
1439}
1440
1441fn lower_in(node: &mut Node, scrolling: bool) {
1442 match node {
1443 Node::Element(element) => {
1444 let inside = scrolls_inside(element, scrolling);
1445 for child in &mut element.children {
1448 lower_in(child, inside);
1449 }
1450 if inside && element.kind == "Column" && element.prop("reflow").is_none() {
1451 let look = ListLook {
1452 gap: element.prop("gap").cloned(),
1453 align: element.keyword("align").unwrap_or("leading").to_string(),
1454 stagger: element.prop("stagger").cloned(),
1455 path: element.path.clone(),
1456 };
1457 let mut count = 0;
1458 lower_rows(&mut element.children, &look, &mut count);
1459 }
1460 }
1461 Node::Component(_) => {}
1462 Node::If(node) => {
1463 for (_, body) in &mut node.branches {
1464 body.iter_mut().for_each(|child| lower_in(child, scrolling));
1465 }
1466 node.otherwise
1467 .iter_mut()
1468 .for_each(|child| lower_in(child, scrolling));
1469 }
1470 Node::IfLet(node) => {
1471 node.then
1472 .iter_mut()
1473 .chain(&mut node.otherwise)
1474 .for_each(|child| lower_in(child, scrolling));
1475 }
1476 Node::Match(node) => {
1477 for arm in &mut node.arms {
1478 arm.body
1479 .iter_mut()
1480 .for_each(|child| lower_in(child, scrolling));
1481 }
1482 }
1483 }
1484}
1485
1486struct ListLook {
1488 gap: Option<Prop>,
1489 align: String,
1490 stagger: Option<Prop>,
1491 path: String,
1492}
1493
1494fn lower_rows(children: &mut [Node], look: &ListLook, count: &mut usize) {
1497 for child in children.iter_mut() {
1498 match child {
1499 Node::Element(each)
1500 if each.kind == "ForEach"
1501 && each.template.is_some()
1502 && each.expr("items").is_some()
1503 && matches!(each.prop("key"), Some(Prop::Key { .. })) =>
1504 {
1505 let mut props = BTreeMap::new();
1506 props.insert("min_width".to_string(), Prop::Number { value: 1.0e6 });
1508 props.insert("gap".to_string(), Prop::Number { value: 0.0 });
1509 if let Some(gap) = &look.gap {
1510 props.insert("row_gap".to_string(), gap.clone());
1511 }
1512 props.insert(
1513 "align".to_string(),
1514 Prop::Keyword {
1515 value: look.align.clone(),
1516 },
1517 );
1518 if let Some(stagger) = &look.stagger {
1519 props.insert("stagger".to_string(), stagger.clone());
1520 }
1521 let source = each.source.clone();
1522 let path = format!("{}/list{}", look.path, count);
1523 *count += 1;
1524 let mut each = std::mem::replace(
1525 child,
1526 Node::Component(ComponentRef {
1527 component: String::new(),
1528 args: Vec::new(),
1529 source: source.clone(),
1530 }),
1531 );
1532 if let Node::Element(each) = &mut each {
1533 stack_parts(each, look, &path);
1534 }
1535 *child = Node::Element(Element {
1536 kind: "Grid".into(),
1537 id: None,
1538 path,
1539 props,
1540 template: None,
1541 children: vec![each],
1542 action: None,
1543 source,
1544 });
1545 }
1546 Node::If(node) => {
1547 for (_, body) in &mut node.branches {
1548 lower_rows(body, look, count);
1549 }
1550 lower_rows(&mut node.otherwise, look, count);
1551 }
1552 Node::IfLet(node) => {
1553 lower_rows(&mut node.then, look, count);
1554 lower_rows(&mut node.otherwise, look, count);
1555 }
1556 Node::Match(node) => {
1557 for arm in &mut node.arms {
1558 lower_rows(&mut arm.body, look, count);
1559 }
1560 }
1561 _ => {}
1562 }
1563 }
1564}
1565
1566fn stack_parts(each: &mut Element, look: &ListLook, path: &str) {
1572 if each.children.len() < 2 {
1573 return;
1574 }
1575 let mut props = BTreeMap::new();
1576 if let Some(gap) = &look.gap {
1577 props.insert("gap".to_string(), gap.clone());
1578 }
1579 props.insert(
1580 "align".to_string(),
1581 Prop::Keyword {
1582 value: look.align.clone(),
1583 },
1584 );
1585 each.children = vec![Node::Element(Element {
1586 kind: "Column".into(),
1587 id: None,
1588 path: format!("{path}/row"),
1589 props,
1590 template: None,
1591 children: std::mem::take(&mut each.children),
1592 action: None,
1593 source: each.source.clone(),
1594 })];
1595}
1596
1597pub fn text_key(component: &str, node_path: &str, prop: &str) -> String {
1599 format!("{component}.{node_path}.{prop}")
1600}
1601
1602#[cfg(test)]
1603mod tests {
1604 use super::*;
1605
1606 fn element(kind: &str, children: Vec<Node>) -> Node {
1607 Node::Element(Element {
1608 kind: kind.into(),
1609 id: None,
1610 path: kind.into(),
1611 props: BTreeMap::new(),
1612 template: None,
1613 children,
1614 action: None,
1615 source: SourceLocation::default(),
1616 })
1617 }
1618
1619 fn with_props(node: Node, props: &[(&str, Prop)]) -> Node {
1620 let Node::Element(mut element) = node else {
1621 unreachable!()
1622 };
1623 for (name, prop) in props {
1624 element.props.insert(name.to_string(), prop.clone());
1625 }
1626 Node::Element(element)
1627 }
1628
1629 fn keyed_rows() -> Node {
1630 let Node::Element(mut each) = element("ForEach", vec![element("Text", vec![])]) else {
1631 unreachable!()
1632 };
1633 each.template = Some(Template {
1634 var: "row".into(),
1635 ty: Type::Int,
1636 });
1637 each.props.insert(
1638 "items".into(),
1639 Prop::Expr {
1640 expr: Expr {
1641 kind: ExprKind::Literal {
1642 value: Value::Int(0),
1643 },
1644 ty: Type::Int,
1645 },
1646 },
1647 );
1648 each.props.insert(
1649 "key".into(),
1650 Prop::Key {
1651 field: 0,
1652 name: "id".into(),
1653 },
1654 );
1655 Node::Element(each)
1656 }
1657
1658 fn kinds(node: &Node) -> Vec<String> {
1659 let Node::Element(element) = node else {
1660 return vec![];
1661 };
1662 element
1663 .children
1664 .iter()
1665 .map(|child| match child {
1666 Node::Element(child) => child.kind.clone(),
1667 Node::If(_) => "if".into(),
1668 _ => "other".into(),
1669 })
1670 .collect()
1671 }
1672
1673 #[test]
1674 fn a_list_in_a_scroll_view_becomes_a_lazy_column_of_its_rows() {
1675 let column = with_props(
1676 element(
1677 "Column",
1678 vec![
1679 element("Text", vec![]),
1680 keyed_rows(),
1681 element("Spacer", vec![]),
1682 ],
1683 ),
1684 &[
1685 ("gap", Prop::Number { value: 4.0 }),
1686 (
1687 "align",
1688 Prop::Keyword {
1689 value: "center".into(),
1690 },
1691 ),
1692 ],
1693 );
1694 let mut tree = element("ScrollView", vec![column]);
1695 lower_bodies(&mut [&mut tree], &mut []);
1696 let Node::Element(scroll) = &tree else {
1697 unreachable!()
1698 };
1699 assert_eq!(kinds(&scroll.children[0]), ["Text", "Grid", "Spacer"]);
1700 let Node::Element(column) = &scroll.children[0] else {
1701 unreachable!()
1702 };
1703 let Node::Element(grid) = &column.children[1] else {
1704 unreachable!()
1705 };
1706 assert_eq!(lowered_list_align(grid), Some("center"));
1707 assert_eq!(grid.number("row_gap"), Some(4.0));
1708 assert!(
1709 lazy_grid_items(grid).is_some(),
1710 "it is a grid every target builds lazily"
1711 );
1712 }
1713
1714 #[test]
1715 fn a_row_of_several_parts_keeps_them_one_under_another() {
1716 let Node::Element(mut each) = keyed_rows() else {
1719 unreachable!()
1720 };
1721 each.children = vec![
1722 element("Row", vec![]),
1723 element("Column", vec![element("Divider", vec![])]),
1724 keyed_rows(),
1725 ];
1726 let parts = each.children.clone();
1727 let column = with_props(
1728 element("Column", vec![Node::Element(each), keyed_rows()]),
1729 &[
1730 ("gap", Prop::Number { value: 4.0 }),
1731 (
1732 "align",
1733 Prop::Keyword {
1734 value: "center".into(),
1735 },
1736 ),
1737 ],
1738 );
1739 let mut tree = element("ScrollView", vec![column]);
1740 lower_bodies(&mut [&mut tree], &mut []);
1741 let rows_of = |at: usize| {
1742 let Node::Element(scroll) = &tree else {
1743 unreachable!()
1744 };
1745 let Node::Element(column) = &scroll.children[0] else {
1746 unreachable!()
1747 };
1748 let Node::Element(grid) = &column.children[at] else {
1749 unreachable!()
1750 };
1751 lazy_grid_items(grid).unwrap().clone()
1752 };
1753 let several = rows_of(0);
1754 assert_eq!(kinds(&Node::Element(several.clone())), ["Column"]);
1755 let Node::Element(stack) = &several.children[0] else {
1756 unreachable!()
1757 };
1758 assert_eq!(stack.children, parts, "the parts stay as they were");
1759 assert_eq!(stack.number("gap"), Some(4.0));
1760 assert_eq!(stack.keyword("align"), Some("center"));
1761 assert_eq!(stack.path, "Column/list0/row");
1762 assert_eq!(kinds(&Node::Element(rows_of(1))), ["Text"]);
1764 }
1765
1766 #[test]
1767 fn lists_outside_scroll_views_and_reflowing_lists_stay_as_they_are() {
1768 let mut outside = element("Column", vec![keyed_rows()]);
1769 let mut sideways = with_props(
1770 element("ScrollView", vec![element("Column", vec![keyed_rows()])]),
1771 &[(
1772 "axis",
1773 Prop::Keyword {
1774 value: "horizontal".into(),
1775 },
1776 )],
1777 );
1778 let mut reflowing = element(
1779 "ScrollView",
1780 vec![with_props(
1781 element("Column", vec![keyed_rows()]),
1782 &[(
1783 "reflow",
1784 Prop::Keyword {
1785 value: "smooth".into(),
1786 },
1787 )],
1788 )],
1789 );
1790 let before = (outside.clone(), sideways.clone(), reflowing.clone());
1791 lower_bodies(&mut [&mut outside, &mut sideways, &mut reflowing], &mut []);
1792 assert_eq!((outside, sideways, reflowing), before);
1793 }
1794
1795 #[test]
1796 fn a_list_in_a_condition_and_in_a_component_shown_in_a_scroll_view_is_lowered() {
1797 let condition = Expr {
1798 kind: ExprKind::Literal {
1799 value: Value::Bool(true),
1800 },
1801 ty: Type::Bool,
1802 };
1803 let call = |name: &str| {
1804 Node::Component(ComponentRef {
1805 component: name.into(),
1806 args: vec![],
1807 source: SourceLocation::default(),
1808 })
1809 };
1810 let mut window = element("ScrollView", vec![call("songs")]);
1811 let mut songs = element(
1812 "Column",
1813 vec![Node::If(IfNode {
1814 branches: vec![(condition, vec![keyed_rows()])],
1815 otherwise: vec![],
1816 })],
1817 );
1818 let mut window_two = element(
1820 "Column",
1821 vec![call("tags"), element("ScrollView", vec![call("tags")])],
1822 );
1823 let mut tags = element("Column", vec![keyed_rows()]);
1824 let tags_before = tags.clone();
1825 lower_bodies(
1826 &mut [&mut window, &mut window_two],
1827 &mut [("songs", &mut songs), ("tags", &mut tags)],
1828 );
1829 let Node::Element(column) = &songs else {
1830 unreachable!()
1831 };
1832 let Node::If(branch) = &column.children[0] else {
1833 unreachable!()
1834 };
1835 assert!(matches!(&branch.branches[0].1[0], Node::Element(grid) if grid.kind == "Grid"));
1836 assert_eq!(tags, tags_before);
1837 }
1838
1839 #[test]
1840 fn walks_report_the_nearest_element_around_each_node() {
1841 let condition = Expr {
1842 kind: ExprKind::Literal {
1843 value: Value::Bool(true),
1844 },
1845 ty: Type::Bool,
1846 };
1847 let tree = element(
1848 "Sidebar",
1849 vec![Node::If(IfNode {
1850 branches: vec![(condition, vec![element("Search", vec![])])],
1851 otherwise: vec![element("Column", vec![element("Text", vec![])])],
1852 })],
1853 );
1854 let mut seen = Vec::new();
1855 tree.walk_in(None, &mut |node, parent| {
1856 let name = match node {
1857 Node::Element(element) => element.kind.as_str(),
1858 Node::If(_) => "if",
1859 _ => "other",
1860 };
1861 seen.push((name, parent));
1862 });
1863 assert_eq!(
1864 seen,
1865 [
1866 ("Sidebar", None),
1867 ("if", Some("Sidebar")),
1868 ("Search", Some("Sidebar")),
1869 ("Column", Some("Sidebar")),
1870 ("Text", Some("Column")),
1871 ]
1872 );
1873 let mut count = 0;
1874 tree.walk(&mut |_| count += 1);
1875 assert_eq!(count, seen.len());
1876 }
1877}