1mod events;
23mod pass;
24mod reconcile;
25mod restyle;
26mod tabs;
27
28pub use pass::{LayoutStep, LeafData};
29
30use std::collections::{BTreeSet, HashMap, HashSet};
31use std::sync::Arc;
32
33use indexmap::IndexMap;
34use serde::{Deserialize, Serialize};
35use serde_json::{Map, Value};
36use taffy::style::Direction;
37
38use crate::catalog::{CatalogView, A2UI_BASIC_CATALOG_ID};
39use crate::data::{get_pointer, set_pointer};
40use crate::engine::Engine;
41use crate::layout_tree::{LNode, LayerClass, LayerSpec, ListSpec, LocalState, NodeKind, ToastSpec};
42use crate::measure::{ControlBox, MeasureMemo, TextStyle};
43use crate::overlay::OverlayPlacement;
44use crate::recipes::RecipeIndex;
45use crate::reducer::{reduce_surface, ReduceOptions, ReduceResult};
46use crate::strings::StringTable;
47use crate::style::Visual;
48use crate::theme::{apply_contrast, apply_density, resolve_mode, Density, Mode, ModeSetting, ResolvedTheme};
49use crate::types::{ExtensionDef, FlatComponent, NestedNode, ReduceIssue, UiNode};
50
51pub use crate::layout_tree::ToastSpec as Toast;
52
53pub const HOVER_CLOSE_MS: u32 = 150;
57
58#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize)]
61pub struct Frame {
62 pub x: f32,
63 pub y: f32,
64 pub w: f32,
65 pub h: f32,
66}
67
68#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
72pub struct PlacedNode {
73 pub index: u32,
74 pub id: String,
75 pub component: String,
76 #[serde(skip_serializing_if = "Option::is_none")]
77 pub part: Option<String>,
78 #[serde(skip_serializing_if = "Option::is_none")]
79 pub owner: Option<String>,
80 #[serde(skip_serializing_if = "Option::is_none")]
81 pub owner_component: Option<String>,
82 #[serde(skip_serializing_if = "Option::is_none")]
83 pub catalog_id: Option<String>,
84 #[serde(skip_serializing_if = "Option::is_none")]
85 pub extension_kind: Option<String>,
86 pub depth: u32,
87 #[serde(skip_serializing_if = "Option::is_none")]
88 pub parent: Option<u32>,
89 #[serde(default, skip_serializing_if = "Vec::is_empty")]
91 pub children: Vec<u32>,
92 pub layer: u32,
93 pub kind: NodeKind,
94 pub props: Map<String, Value>,
95 #[serde(skip_serializing_if = "Option::is_none")]
96 pub lines: Option<u32>,
97 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
98 pub pressable: bool,
99 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
100 pub hidden: bool,
101 #[serde(skip_serializing_if = "Option::is_none")]
102 pub accessibility: Option<Value>,
103 #[serde(skip_serializing_if = "Option::is_none")]
105 pub trigger_for: Option<String>,
106 #[serde(default, skip_serializing_if = "Vec::is_empty")]
109 pub states: Vec<String>,
110 #[serde(skip_serializing_if = "Option::is_none")]
112 pub form: Option<String>,
113 #[serde(skip_serializing_if = "Option::is_none")]
115 pub live: Option<String>,
116 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
118 pub removed: bool,
119}
120
121#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
122pub struct PlacedFrame {
123 pub index: u32,
124 pub x: f32,
125 pub y: f32,
126 pub w: f32,
127 pub h: f32,
128}
129
130#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
132pub struct Layer {
133 pub layer: u32,
134 pub kind: String,
135 pub owner: String,
136 pub root: u32,
137 #[serde(skip_serializing_if = "Option::is_none")]
138 pub anchor_frame: Option<Frame>,
139 #[serde(skip_serializing_if = "Option::is_none")]
140 pub placement: Option<OverlayPlacement>,
141 pub position: String,
144 pub class: LayerClass,
146 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
148 pub modal: bool,
149 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
151 pub dismissible: bool,
152 pub frames: Vec<PlacedFrame>,
153}
154
155#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
156pub struct ListOutput {
157 pub id: String,
158 pub node: u32,
159 pub content_height: f32,
160 pub start: u32,
161 pub end: u32,
162 pub count: u32,
163 pub windowed: bool,
164 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
166 pub horizontal: bool,
167}
168
169#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
174pub struct StickyOutput {
175 pub index: u32,
176 pub dx: f32,
177 pub dy: f32,
178}
179
180#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
185pub struct ScrollOutput {
186 pub index: u32,
187 pub offset_x: f32,
188 pub offset_y: f32,
189 pub content_width: f32,
190 pub content_height: f32,
191 pub scroll_x: bool,
192 pub scroll_y: bool,
193}
194
195#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
199pub struct NodeDelta {
200 pub added: Vec<u32>,
201 pub removed: Vec<u32>,
202 pub changed: Vec<u32>,
203 pub renumbered: bool,
204}
205
206impl NodeDelta {
207 pub fn is_empty(&self) -> bool {
208 self.added.is_empty() && self.removed.is_empty() && self.changed.is_empty() && !self.renumbered
209 }
210}
211
212#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
213pub struct LayoutOutput {
214 pub frames: Vec<PlacedFrame>,
216 pub layers: Vec<Layer>,
217 pub lists: Vec<ListOutput>,
218 pub scrolls: Vec<ScrollOutput>,
219 #[serde(default, skip_serializing_if = "Vec::is_empty")]
222 pub sticky: Vec<StickyOutput>,
223 pub toasts: Vec<ToastSpec>,
226 pub visual_changes: Vec<u32>,
228 pub delta: NodeDelta,
230 pub structure_version: u64,
232 pub surface_width: f32,
233 pub surface_height: f32,
234 pub overflow: bool,
236 pub direction: String,
238 pub breakpoint: Option<String>,
240 pub measure_rounds: u32,
241 pub upcalls: u32,
242 pub restyled: u32,
244 pub rebuilt: bool,
246 pub built_nodes: u32,
249 pub layout_ns: u64,
250}
251
252#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
257#[serde(tag = "kind", rename_all = "camelCase", rename_all_fields = "camelCase")]
258#[non_exhaustive]
259pub enum OutEvent {
260 Action { name: String, component_id: String, event: String, context: Value, #[serde(skip_serializing_if = "Option::is_none")] payload: Option<Value> },
263 OpenUrl { url: String },
265 FunctionCall { component_id: String, name: String, args: Value },
272 DataChanged { path: String, value: Value },
274 Input { component_id: String, name: String, #[serde(skip_serializing_if = "Option::is_none")] path: Option<String>, value: Value, commit: bool },
276 Focus { id: String, index: u32 },
278 Announce { text: String, live: String },
280 Copy { text: String },
282 PickFiles { component_id: String, #[serde(skip_serializing_if = "Option::is_none")] accept: Option<String>, multiple: bool },
285 Relayout,
287 HoverTimer { owner: String, delay_ms: u32 },
292 ScrollSurface { x: f32, y: f32 },
296}
297
298#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
300#[serde(rename_all = "camelCase")]
301pub enum SurfaceCommand {
302 Focus { id: String },
303 Announce { text: String, #[serde(default)] live: Option<String> },
304 ScrollIntoView { id: String },
305 ScrollToIndex {
308 id: String,
309 index: u32,
310 #[serde(default, skip_serializing_if = "Option::is_none")]
311 align: Option<String>,
312 },
313}
314
315#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize)]
317pub struct Insets {
318 pub top: f32,
319 pub right: f32,
320 pub bottom: f32,
321 pub left: f32,
322}
323
324#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
326#[serde(rename_all = "lowercase")]
327pub enum ContrastSetting {
328 Normal,
329 High,
330 #[default]
331 System,
332}
333
334#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
336#[serde(rename_all = "camelCase", default)]
337pub struct SurfaceSettings {
338 pub locale: String,
340 pub strings: IndexMap<String, String>,
342 pub mode: ModeSetting,
343 pub system_dark: bool,
345 pub density: Density,
346 pub contrast: ContrastSetting,
347 pub system_high_contrast: bool,
349 pub font_scale: f32,
351 pub hover: bool,
353 pub reduced_motion: bool,
355 pub insets: Insets,
356 pub today: Option<String>,
358 pub hover_close_ms: u32,
365 pub read_only: bool,
369 pub form_factor: Option<crate::conditions::FormFactor>,
373}
374
375impl Default for SurfaceSettings {
376 fn default() -> Self {
377 SurfaceSettings {
378 locale: crate::locale::DEFAULT_LOCALE.to_string(),
379 strings: IndexMap::new(),
380 mode: ModeSetting::Light,
381 system_dark: false,
382 density: Density::Default,
383 contrast: ContrastSetting::Normal,
384 system_high_contrast: false,
385 font_scale: 1.0,
386 hover: true,
387 reduced_motion: false,
388 insets: Insets::default(),
389 today: None,
390 hover_close_ms: 0,
391 read_only: false,
392 form_factor: None,
393 }
394 }
395}
396
397#[derive(Debug, Clone)]
398pub struct SurfaceOptions {
399 pub catalog_id: String,
400 pub theme: Option<Arc<ResolvedTheme>>,
402 pub mode: Mode,
403 pub extensions: Vec<ExtensionDef>,
404 pub rounding: bool,
405 pub expand_controls: Option<bool>,
408}
409
410impl Default for SurfaceOptions {
411 fn default() -> Self {
412 SurfaceOptions {
413 catalog_id: crate::catalog::CORE_CATALOG_ID.to_string(),
414 theme: Some(crate::themes::default_theme()),
415 mode: Mode::Light,
416 extensions: Vec::new(),
417 rounding: false,
418 expand_controls: None,
419 }
420 }
421}
422
423#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
424pub struct ApplyOutcome {
425 pub structure_changed: bool,
426 pub issues: Vec<ReduceIssue>,
434}
435
436#[derive(Debug, Clone, Copy, PartialEq, Default)]
438struct RadiusPercent {
439 all: Option<f32>,
440 corners: [Option<f32>; 4],
441}
442
443#[derive(Debug, Clone, Default)]
445struct NodeState {
446 flat: Map<String, Value>,
447 visual: Visual,
448 text: TextStyle,
449 control: ControlBox,
450 content_version: u32,
452 conditional: bool,
454 radius_pct: Option<RadiusPercent>,
455 styled: bool,
457 direction: Direction,
459 line_height_set: bool,
461}
462
463#[derive(Debug, Clone)]
464struct Carried {
465 intrinsics: crate::measure::Intrinsics,
466 heights: Vec<(i64, f32)>,
467 props: Map<String, Value>,
468 lines: Option<u32>,
469}
470
471pub struct Surface {
472 pub id: String,
473 pub catalog_id: String,
474 view: Arc<CatalogView>,
475 recipes: Arc<RecipeIndex>,
476 extensions: Vec<ExtensionDef>,
477 theme: Option<Arc<ResolvedTheme>>,
479 effective: Option<Arc<ResolvedTheme>>,
480 settings: SurfaceSettings,
481 strings: StringTable,
482 mode: Mode,
483 expand_controls: Option<bool>,
484 components: Vec<FlatComponent>,
485 nested: Option<NestedNode>,
486 data: Value,
487 root: Option<UiNode>,
488 pub(crate) issues: Vec<ReduceIssue>,
492 reduce_issues: Vec<ReduceIssue>,
493 pub(crate) build_issues: Vec<ReduceIssue>,
494 pub(crate) style_issues: Vec<ReduceIssue>,
495 local: LocalState,
496 states: HashMap<String, Vec<String>>,
497
498 nodes: Vec<LNode>,
500 live: Vec<bool>,
501 slot_of: HashMap<String, u32>,
502 free: Vec<u32>,
503 main_root: Option<u32>,
504 layers: Vec<LayerSpec>,
505 lists: Vec<ListSpec>,
506 toasts: Vec<ToastSpec>,
507 node_state: Vec<NodeState>,
508 engine: Engine,
509 memo: MeasureMemo,
510 memo_versions: HashMap<u32, u32>,
511 last_measurer: Option<u64>,
512 viewport: (f32, f32),
513 layer_viewport: Option<(f32, f32)>,
515 pub(crate) dialog_holds: HashMap<String, f32>,
521 pub(crate) dialog_holds_next: HashMap<String, f32>,
522 max_height: Option<f32>,
523 direction: Direction,
524 rounding: bool,
525 structure_version: u64,
526 needs_build: bool,
527 restyle_all: bool,
528 style_dirty: BTreeSet<u32>,
529 visuals_dirty: HashSet<u32>,
530 template_cache: HashMap<String, Option<UiNode>>,
533 reduce_pending: bool,
536 component_index: HashMap<String, usize>,
538 last_frames: Vec<Frame>,
540 archive: HashMap<String, Carried>,
543 responsive: bool,
544 breakpoint: Option<String>,
545 delta_added: BTreeSet<u32>,
546 delta_removed: BTreeSet<u32>,
547 delta_changed: BTreeSet<u32>,
548 renumbered: bool,
549 pending: Vec<OutEvent>,
550 scrolls: Vec<ScrollOutput>,
551 restyled: u32,
553 rebuilt: bool,
554 built_nodes: u32,
555 step_state: Option<pass::PassState>,
557 subtree_pending: BTreeSet<String>,
560 hover_closing: HashSet<String>,
562 seed: crate::layout_tree::BuildSeed,
565 formatter: Arc<dyn crate::format::Formatter>,
567 clock: Option<f64>,
569 data_version: u64,
571}
572
573unsafe impl Send for Surface {}
579
580impl Surface {
581 pub fn new(id: &str, options: SurfaceOptions) -> Surface {
582 let view = CatalogView::with(&options.extensions);
583 let recipes = if options.extensions.is_empty() { RecipeIndex::core() } else { Arc::new(RecipeIndex::new(&view)) };
584 let mut engine = Engine::new();
585 engine.set_rounding(options.rounding);
586 let settings = SurfaceSettings { mode: if options.mode == Mode::Dark { ModeSetting::Dark } else { ModeSetting::Light }, ..SurfaceSettings::default() };
587 let mut s = Surface {
588 id: id.to_string(),
589 catalog_id: options.catalog_id,
590 view,
591 recipes,
592 extensions: options.extensions,
593 theme: options.theme,
594 effective: None,
595 strings: crate::strings::DEFAULT_STRINGS.clone(),
596 settings,
597 mode: options.mode,
598 expand_controls: options.expand_controls,
599 components: Vec::new(),
600 nested: None,
601 data: Value::Object(Map::new()),
602 root: None,
603 issues: Vec::new(),
604 reduce_issues: Vec::new(),
605 build_issues: Vec::new(),
606 style_issues: Vec::new(),
607 local: LocalState::default(),
608 states: HashMap::new(),
609 nodes: Vec::new(),
610 live: Vec::new(),
611 slot_of: HashMap::new(),
612 free: Vec::new(),
613 main_root: None,
614 layers: Vec::new(),
615 lists: Vec::new(),
616 toasts: Vec::new(),
617 node_state: Vec::new(),
618 engine,
619 memo: MeasureMemo::default(),
620 memo_versions: HashMap::new(),
621 last_measurer: None,
622 viewport: (0.0, 0.0),
623 layer_viewport: None,
624 dialog_holds: HashMap::new(),
625 dialog_holds_next: HashMap::new(),
626 max_height: None,
627 direction: Direction::Ltr,
628 rounding: options.rounding,
629 structure_version: 0,
630 needs_build: true,
631 restyle_all: true,
632 style_dirty: BTreeSet::new(),
633 visuals_dirty: HashSet::new(),
634 template_cache: HashMap::new(),
635 reduce_pending: false,
636 component_index: HashMap::new(),
637 last_frames: Vec::new(),
638 archive: HashMap::new(),
639 responsive: false,
640 breakpoint: None,
641 delta_added: BTreeSet::new(),
642 delta_removed: BTreeSet::new(),
643 delta_changed: BTreeSet::new(),
644 renumbered: false,
645 pending: Vec::new(),
646 scrolls: Vec::new(),
647 restyled: 0,
648 rebuilt: false,
649 built_nodes: 0,
650 step_state: None,
651 subtree_pending: BTreeSet::new(),
652 hover_closing: HashSet::new(),
653 seed: Default::default(),
654 formatter: Arc::new(crate::format::EnglishFormatter),
655 clock: None,
656 data_version: 0,
657 };
658 s.refresh_theme();
659 s
660 }
661
662 pub fn set_formatter(&mut self, formatter: Arc<dyn crate::format::Formatter>) {
672 self.formatter = formatter;
673 self.needs_build = true;
674 }
675
676 pub fn formatter(&self) -> &Arc<dyn crate::format::Formatter> {
678 &self.formatter
679 }
680
681 pub fn set_clock(&mut self, now_ms: Option<f64>) {
685 self.clock = now_ms;
686 self.needs_build = true;
687 }
688
689 pub fn tick(&mut self) {
691 self.needs_build = true;
692 }
693
694 pub fn uses_clock(&self) -> bool {
700 fn relative_call(v: &Value) -> bool {
701 match v {
702 Value::Object(o) => (o.get("call").and_then(Value::as_str) == Some("formatRelativeTime") && o.get("args").and_then(|a| a.get("now")).is_none()) || o.values().any(relative_call),
703 Value::Array(a) => a.iter().any(relative_call),
704 _ => false,
705 }
706 }
707 if self.clock.is_some() {
708 return false;
709 }
710 let mut found = false;
711 for tree in self.root.iter().chain(self.template_cache.values().flatten()) {
712 tree.walk(&mut |n| found = found || n.props.values().any(relative_call));
713 }
714 found
715 || self.nodes.iter().zip(&self.live).any(|(n, live)| {
716 *live && n.component == "Table" && n.props.get("columns").and_then(Value::as_array).is_some_and(|cols| cols.iter().any(|c| c.get("type").and_then(Value::as_str) == Some("relativeTime")))
717 })
718 }
719
720 pub fn register_extension(&mut self, ext: ExtensionDef) -> Result<(), String> {
722 let ext = crate::extension::define_extension(ext)?;
723 let mut all = self.extensions.clone();
724 all.retain(|e| e.id != ext.id);
725 all.push(ext);
726 self.view = CatalogView::with(&all);
727 self.recipes = Arc::new(RecipeIndex::new(&self.view));
728 self.extensions = all;
729 self.template_cache.clear();
730 self.reduce();
731 Ok(())
732 }
733
734 fn refresh_theme(&mut self) {
737 let contrast = match self.settings.contrast {
738 ContrastSetting::High => true,
739 ContrastSetting::Normal => false,
740 ContrastSetting::System => self.settings.system_high_contrast,
741 };
742 self.effective = self.theme.as_ref().map(|t| {
743 if self.settings.density == Density::Default && !contrast {
744 return t.clone();
745 }
746 let mut out = apply_density(t, self.settings.density);
747 if contrast {
748 out = apply_contrast(&out);
749 }
750 Arc::new(out)
751 });
752 self.restyle_all = true;
753 }
754
755 pub fn set_theme(&mut self, theme: Option<Arc<ResolvedTheme>>) {
756 self.theme = theme;
757 self.refresh_theme();
758 self.needs_build = true;
760 self.invalidate_measures();
761 }
762
763 pub fn theme(&self) -> Option<&Arc<ResolvedTheme>> {
764 self.theme.as_ref()
765 }
766
767 pub fn effective_theme(&self) -> Option<&Arc<ResolvedTheme>> {
770 self.effective.as_ref()
771 }
772
773 pub fn set_mode(&mut self, mode: Mode) {
774 self.settings.mode = if mode == Mode::Dark { ModeSetting::Dark } else { ModeSetting::Light };
775 if self.mode != mode {
776 self.mode = mode;
777 self.restyle_all = true;
778 }
779 }
780
781 pub fn mode(&self) -> Mode {
782 self.mode
783 }
784
785 pub fn settings(&self) -> &SurfaceSettings {
786 &self.settings
787 }
788
789 pub fn set_settings(&mut self, settings: SurfaceSettings) {
792 let old = std::mem::replace(&mut self.settings, settings);
793 let s = self.settings.clone();
794 if old.strings != s.strings {
795 self.strings = crate::strings::string_table(&s.strings);
796 self.needs_build = true;
797 }
798 if old.read_only != s.read_only {
799 self.needs_build = true;
800 self.restyle_all = true;
801 }
802 if old.locale != s.locale || old.today != s.today {
803 self.needs_build = true;
804 self.restyle_all = true;
805 }
806 if old.density != s.density || old.contrast != s.contrast || old.system_high_contrast != s.system_high_contrast {
807 self.refresh_theme();
808 }
809 let mode = resolve_mode(s.mode, s.system_dark);
810 if mode != self.mode {
811 self.mode = mode;
812 self.restyle_all = true;
813 }
814 if (old.font_scale - s.font_scale).abs() > f32::EPSILON || old.hover != s.hover || old.reduced_motion != s.reduced_motion || old.form_factor != s.form_factor {
815 self.restyle_all = true;
816 }
817 if old.insets != s.insets {
818 self.engine.mark_all_dirty();
819 }
820 }
821
822 pub fn set_read_only(&mut self, on: bool) {
824 let mut s = self.settings.clone();
825 s.read_only = on;
826 self.set_settings(s);
827 }
828
829 pub fn set_locale(&mut self, locale: &str) {
830 let mut s = self.settings.clone();
831 s.locale = locale.to_string();
832 self.set_settings(s);
833 }
834
835 pub fn set_strings(&mut self, overrides: IndexMap<String, String>) {
836 let mut s = self.settings.clone();
837 s.strings = overrides;
838 self.set_settings(s);
839 }
840
841 pub fn set_mode_setting(&mut self, mode: ModeSetting, system_dark: bool) {
843 let mut s = self.settings.clone();
844 s.mode = mode;
845 s.system_dark = system_dark;
846 self.set_settings(s);
847 }
848
849 pub fn set_density(&mut self, density: Density) {
850 let mut s = self.settings.clone();
851 s.density = density;
852 self.set_settings(s);
853 }
854
855 pub fn set_contrast(&mut self, contrast: ContrastSetting, system_high: bool) {
856 let mut s = self.settings.clone();
857 s.contrast = contrast;
858 s.system_high_contrast = system_high;
859 self.set_settings(s);
860 }
861
862 pub fn set_font_scale(&mut self, scale: f32) {
864 let mut s = self.settings.clone();
865 s.font_scale = scale.max(0.1);
866 self.set_settings(s);
867 }
868
869 pub fn set_insets(&mut self, insets: Insets) {
870 let mut s = self.settings.clone();
871 s.insets = insets;
872 self.set_settings(s);
873 }
874
875 pub fn set_form_factor(&mut self, form_factor: Option<crate::conditions::FormFactor>) {
877 let mut s = self.settings.clone();
878 s.form_factor = form_factor;
879 self.set_settings(s);
880 }
881
882 pub fn set_pointer(&mut self, hover: bool, reduced_motion: bool) {
884 let mut s = self.settings.clone();
885 s.hover = hover;
886 s.reduced_motion = reduced_motion;
887 self.set_settings(s);
888 }
889
890 pub fn set_rounding(&mut self, on: bool) {
893 self.rounding = on;
894 self.engine.set_rounding(on);
895 self.engine.mark_all_dirty();
896 }
897
898 pub fn set_viewport(&mut self, width: f32, height: f32, max_height: Option<f32>) -> bool {
902 if self.viewport == (width, height) && self.max_height == max_height {
903 return false;
904 }
905 let height_changed = self.viewport.1 != height || self.max_height != max_height;
906 self.viewport = (width, height);
907 self.max_height = max_height;
908 for (i, s) in self.node_state.iter().enumerate() {
909 if s.conditional && self.live.get(i).copied().unwrap_or(false) {
910 self.style_dirty.insert(i as u32);
911 }
912 }
913 if (height_changed && !self.lists.is_empty()) || (self.responsive && self.active_breakpoint() != self.breakpoint) {
916 self.needs_build = true;
917 }
918 true
919 }
920
921 pub fn viewport(&self) -> (f32, f32) {
922 self.viewport
923 }
924
925 pub fn set_layer_viewport(&mut self, width: f32, height: f32) -> bool {
933 let next = (width.is_finite() && width > 0.0).then_some((width, if height.is_finite() { height.max(0.0) } else { 0.0 }));
934 if self.layer_viewport == next {
935 return false;
936 }
937 self.layer_viewport = next;
938 true
939 }
940
941 pub fn layer_viewport(&self) -> Option<(f32, f32)> {
942 self.layer_viewport
943 }
944
945 fn active_breakpoint(&self) -> Option<String> {
947 crate::conditions::active_breakpoint(self.viewport.0, &self.breakpoints())
948 }
949
950 fn breakpoints(&self) -> IndexMap<String, f32> {
951 match &self.effective {
952 Some(t) if !t.tokens.breakpoint.is_empty() => t.tokens.breakpoint.iter().map(|(k, v)| (k.clone(), *v as f32)).collect(),
953 _ => crate::conditions::default_breakpoints(),
954 }
955 }
956
957 pub fn set_states(&mut self, id: &str, states: Vec<String>) -> bool {
968 let current = self.states.get(id);
969 if current.is_some_and(|c| *c == states) || (current.is_none() && states.is_empty()) {
970 return false;
971 }
972 if states.is_empty() {
973 self.states.remove(id);
974 } else {
975 self.states.insert(id.to_string(), states);
976 }
977 if let Some(&slot) = self.slot_of.get(id) {
978 self.style_dirty.insert(slot);
979 self.lend_states(slot);
980 for target in self.hover_targets(slot) {
981 let open = self.local.open.get(&target).copied().unwrap_or(false);
982 let active = self.hover_active(&target);
983 if active && !open {
984 self.hover_closing.remove(&target);
985 let events = self.set_open(&target, true);
986 self.pending.extend(events.into_iter().filter(|e| !matches!(e, OutEvent::Relayout)));
987 } else if active {
988 self.hover_closing.remove(&target);
989 } else if open && self.settings.hover_close_ms == 0 {
990 let events = self.set_open(&target, false);
991 self.pending.extend(events.into_iter().filter(|e| !matches!(e, OutEvent::Relayout)));
992 } else if open && self.hover_closing.insert(target.clone()) {
993 self.pending.push(OutEvent::HoverTimer { owner: target, delay_ms: self.settings.hover_close_ms });
994 }
995 }
996 }
997 true
998 }
999
1000 pub fn hover_timeout(&mut self, owner: &str) -> Vec<OutEvent> {
1004 if !self.hover_closing.remove(owner) || !self.opens_on_hover(owner) || self.hover_active(owner) {
1005 return vec![];
1006 }
1007 if !self.local.open.get(owner).copied().unwrap_or(false) {
1008 return vec![];
1009 }
1010 self.set_open(owner, false)
1011 }
1012
1013 fn hover_targets(&self, slot: u32) -> Vec<String> {
1018 let mut out = Vec::new();
1019 let mut cur = Some(slot);
1020 let mut guard = 0;
1021 while let Some(c) = cur {
1022 guard += 1;
1023 if guard > 4096 || !self.live.get(c as usize).copied().unwrap_or(false) {
1024 break;
1025 }
1026 let n = &self.nodes[c as usize];
1027 if let Some(t) = n.trigger_for.as_ref().filter(|_| c == slot) {
1028 if self.opens_on_hover(t) && !out.contains(t) {
1029 out.push(t.clone());
1030 }
1031 }
1032 cur = match n.parent {
1033 Some(p) => Some(p),
1034 None => match self.layers.iter().find(|l| l.root == c) {
1035 Some(l) => {
1036 if self.opens_on_hover(&l.owner) && !out.contains(&l.owner) {
1037 out.push(l.owner.clone());
1038 }
1039 self.slot_of.get(&l.owner).copied()
1040 }
1041 None => None,
1042 },
1043 };
1044 }
1045 out
1046 }
1047
1048 fn hover_active(&self, target: &str) -> bool {
1050 self.states.iter().any(|(id, st)| {
1051 st.iter().any(|s| s == "hover" || s == "focus-visible" || s == "focus")
1052 && self.slot_of.get(id).is_some_and(|&slot| self.live[slot as usize] && self.hover_targets(slot).iter().any(|t| t == target))
1053 && (st.iter().any(|s| s == "hover" || s == "focus-visible") || self.in_layer_of(self.slot_of[id], target))
1056 })
1057 }
1058
1059 fn in_layer_of(&self, slot: u32, owner: &str) -> bool {
1061 let Some(layer) = self.layers.iter().find(|l| l.owner == owner) else { return false };
1062 let mut cur = Some(slot);
1063 while let Some(c) = cur {
1064 if c == layer.root {
1065 return true;
1066 }
1067 cur = self.nodes[c as usize].parent;
1068 }
1069 false
1070 }
1071
1072 fn opens_on_hover(&self, target: &str) -> bool {
1073 let Some(&slot) = self.slot_of.get(target) else { return false };
1074 let n = &self.nodes[slot as usize];
1075 n.component == "Tooltip" || (n.component == "Popover" && n.props.get("openOn").and_then(Value::as_str) == Some("hover"))
1076 }
1077
1078 pub fn set_pressed(&mut self, ids: &[String]) -> bool {
1080 self.set_flagged("pressed", ids)
1081 }
1082
1083 pub fn set_hovered(&mut self, ids: &[String]) -> bool {
1087 self.set_flagged("hover", ids)
1088 }
1089
1090 pub fn set_hover(&mut self, id: &str, hovered: bool) -> bool {
1093 let mut s = self.states.get(id).cloned().unwrap_or_default();
1094 let has = s.iter().any(|x| x == "hover");
1095 if has == hovered {
1096 return false;
1097 }
1098 if hovered {
1099 s.push("hover".into());
1100 } else {
1101 s.retain(|x| x != "hover");
1102 }
1103 self.set_states(id, s)
1104 }
1105
1106 pub fn host_states(&self, id: &str) -> &[String] {
1109 self.states.get(id).map(Vec::as_slice).unwrap_or(&[])
1110 }
1111
1112 fn set_flagged(&mut self, flag: &str, ids: &[String]) -> bool {
1114 let mut changed = false;
1115 let keep: HashSet<&String> = ids.iter().collect();
1116 let had: Vec<String> = self.states.iter().filter(|(_, s)| s.iter().any(|x| x == flag)).map(|(k, _)| k.clone()).collect();
1117 for id in had {
1118 if !keep.contains(&id) {
1119 let mut s = self.states.get(&id).cloned().unwrap_or_default();
1120 s.retain(|x| x != flag);
1121 changed |= self.set_states(&id, s);
1122 }
1123 }
1124 for id in ids {
1125 let mut s = self.states.get(id).cloned().unwrap_or_default();
1126 if !s.iter().any(|x| x == flag) {
1127 s.push(flag.into());
1128 changed |= self.set_states(id, s);
1129 }
1130 }
1131 changed
1132 }
1133
1134 pub fn take_events(&mut self) -> Vec<OutEvent> {
1137 std::mem::take(&mut self.pending)
1138 }
1139
1140 pub fn invalidate_measures(&mut self) {
1142 self.memo.clear();
1143 self.memo_versions.clear();
1144 self.archive.clear();
1145 self.engine.mark_all_dirty();
1146 }
1147
1148 pub fn mark_dirty(&mut self, index: u32) -> bool {
1150 if !self.live.get(index as usize).copied().unwrap_or(false) {
1151 return false;
1152 }
1153 self.memo.forget(index);
1154 self.memo_versions.remove(&index);
1155 self.engine.mark_dirty(index);
1156 true
1157 }
1158
1159 pub fn apply(&mut self, message: &Value) -> Result<ApplyOutcome, String> {
1166 let obj = message.as_object().ok_or("message must be an object")?;
1167 if let Some(create) = obj.get("createSurface") {
1168 if let Some(cid) = create.get("catalogId").and_then(Value::as_str) {
1169 self.catalog_id = cid.to_string();
1170 }
1171 if let Some(sid) = create.get("surfaceId").and_then(Value::as_str) {
1172 self.id = sid.to_string();
1173 }
1174 self.set_component_list(Vec::new());
1175 self.nested = None;
1176 self.data = Value::Object(Map::new());
1177 self.data_version += 1;
1178 self.local = LocalState::default();
1179 self.template_cache.clear();
1180 self.reduce();
1181 return Ok(ApplyOutcome { structure_changed: true, issues: self.issues.clone() });
1182 }
1183 if let Some(update) = obj.get("updateComponents") {
1184 let components = update.get("components").ok_or("updateComponents.components is required")?;
1185 let list: Vec<FlatComponent> = serde_json::from_value(components.clone()).map_err(|e| format!("components: {e}"))?;
1186 return Ok(self.update_components(list));
1187 }
1188 self.ensure_reduced();
1189 if let Some(update) = obj.get("updateDataModel") {
1190 let path = update.get("path").and_then(Value::as_str).unwrap_or("");
1191 let value = update.get("value").cloned();
1192 let issues = match self.set_data(path, value) {
1193 Ok(()) => Vec::new(),
1194 Err(message) => vec![ReduceIssue { id: path.to_string(), message }],
1195 };
1196 return Ok(ApplyOutcome { structure_changed: self.needs_build, issues });
1197 }
1198 if obj.get("deleteSurface").is_some() {
1199 self.set_component_list(Vec::new());
1200 self.nested = None;
1201 self.root = None;
1202 self.data = Value::Object(Map::new());
1203 self.data_version += 1;
1204 self.local = LocalState::default();
1205 self.needs_build = true;
1206 return Ok(ApplyOutcome { structure_changed: true, issues: Vec::new() });
1207 }
1208 Err("unknown message: expected createSurface | updateComponents | updateDataModel | deleteSurface".into())
1209 }
1210
1211 pub fn update_components(&mut self, list: Vec<FlatComponent>) -> ApplyOutcome {
1217 if self.nested.is_some() {
1218 self.set_component_list(Vec::new());
1219 }
1220 let mut replaced: HashMap<String, Option<FlatComponent>> = HashMap::new();
1223 let mut order: Vec<String> = Vec::new();
1224 for c in list {
1225 if !replaced.contains_key(&c.id) {
1226 order.push(c.id.clone());
1227 }
1228 match self.component_index.get(&c.id) {
1229 Some(&i) => {
1230 let old = std::mem::replace(&mut self.components[i], c);
1231 replaced.entry(old.id.clone()).or_insert(Some(old));
1232 }
1233 None => {
1234 replaced.entry(c.id.clone()).or_insert(None);
1235 self.component_index.insert(c.id.clone(), self.components.len());
1236 self.components.push(c);
1237 }
1238 }
1239 }
1240 self.nested = None;
1241 self.retain_updated_field_values(&replaced);
1242 self.reduce_pending = true;
1243 self.needs_build = true;
1244 let options = ReduceOptions::new(&self.catalog_id).with_view(self.view.clone());
1247 let (components, index) = (&self.components, &self.component_index);
1248 let lookup = |id: &str| index.get(id).map(|&i| components[i].clone());
1249 let mut issues = Vec::new();
1250 for id in &order {
1251 if let Some(&i) = index.get(id.as_str()) {
1252 issues.extend(crate::reducer::component_issues(&components[i], &options, &lookup));
1253 }
1254 }
1255 ApplyOutcome { structure_changed: true, issues }
1256 }
1257
1258 pub fn set_nested(&mut self, tree: NestedNode) -> ApplyOutcome {
1261 self.nested = Some(tree);
1262 self.set_component_list(Vec::new());
1263 self.template_cache.clear();
1264 self.local.field_values.clear();
1265 self.reduce();
1266 ApplyOutcome { structure_changed: true, issues: self.issues.clone() }
1267 }
1268
1269 pub fn set_components(&mut self, components: Vec<FlatComponent>) -> ApplyOutcome {
1271 let before = if self.nested.is_some() { Vec::new() } else { std::mem::take(&mut self.components) };
1272 self.set_component_list(components);
1273 self.nested = None;
1274 self.template_cache.clear();
1275 self.retain_field_values(&before);
1276 self.reduce();
1277 ApplyOutcome { structure_changed: true, issues: self.issues.clone() }
1278 }
1279
1280 fn set_component_list(&mut self, components: Vec<FlatComponent>) {
1282 self.component_index = components.iter().enumerate().map(|(i, c)| (c.id.clone(), i)).collect();
1283 self.components = components;
1284 }
1285
1286 fn retain_updated_field_values(&mut self, replaced: &HashMap<String, Option<FlatComponent>>) {
1290 if self.local.field_values.is_empty() {
1291 return;
1292 }
1293 let (components, index) = (&self.components, &self.component_index);
1294 self.local.field_values.retain(|key, _| {
1295 let mut candidate = key.as_str();
1296 loop {
1297 if let Some(old) = replaced.get(candidate) {
1298 let new = index.get(candidate).map(|&i| &components[i]);
1299 return matches!((old.as_ref(), new), (Some(a), Some(b)) if a == b);
1300 }
1301 if index.contains_key(candidate) {
1302 return true;
1303 }
1304 match candidate.rfind('.') {
1305 Some(i) => candidate = &candidate[..i],
1306 None => return false,
1307 }
1308 }
1309 });
1310 }
1311
1312 fn retain_field_values(&mut self, before: &[FlatComponent]) {
1319 if self.local.field_values.is_empty() {
1320 return;
1321 }
1322 let old: HashMap<&str, &FlatComponent> = before.iter().map(|c| (c.id.as_str(), c)).collect();
1323 let new: HashMap<&str, &FlatComponent> = self.components.iter().map(|c| (c.id.as_str(), c)).collect();
1324 self.local.field_values.retain(|key, _| {
1325 let mut candidate = key.as_str();
1326 loop {
1327 if old.contains_key(candidate) || new.contains_key(candidate) {
1328 return matches!((old.get(candidate), new.get(candidate)), (Some(a), Some(b)) if a == b);
1329 }
1330 match candidate.rfind('.') {
1331 Some(i) => candidate = &candidate[..i],
1332 None => return false,
1333 }
1334 }
1335 });
1336 }
1337
1338 pub fn set_data(&mut self, path: &str, value: Option<Value>) -> Result<(), String> {
1344 set_pointer(&mut self.data, path, value)?;
1345 self.data_version += 1;
1346 self.needs_build = true;
1347 Ok(())
1348 }
1349
1350 pub fn data(&self) -> &Value {
1351 &self.data
1352 }
1353
1354 pub fn components(&self) -> &[FlatComponent] {
1357 &self.components
1358 }
1359
1360 pub fn root(&mut self) -> Option<&UiNode> {
1361 self.ensure_reduced();
1362 self.root.as_ref()
1363 }
1364
1365 pub fn issues(&mut self) -> &[ReduceIssue] {
1368 self.ensure_reduced();
1369 &self.issues
1370 }
1371
1372 pub(crate) fn ensure_reduced(&mut self) {
1374 if self.reduce_pending {
1375 self.reduce();
1376 }
1377 }
1378
1379 fn reduce(&mut self) {
1380 crate::roomy(|| self.reduce_now())
1381 }
1382
1383 fn reduce_now(&mut self) {
1384 self.template_cache.clear();
1385 self.local.static_keys.clear();
1386 self.local.key_cache.clear();
1387 let options = ReduceOptions::new(&self.catalog_id).with_view(self.view.clone());
1388 let result: Option<ReduceResult> = if let Some(nested) = &self.nested {
1389 Some(crate::reducer::reduce_nested(nested, &options))
1390 } else if !self.components.is_empty() {
1391 Some(reduce_surface(&self.components, &options))
1392 } else {
1393 None
1394 };
1395 match result {
1396 Some(r) => {
1397 self.root = Some(r.root);
1398 self.reduce_issues = r.issues;
1399 self.template_cache = r.templates.unwrap_or_default().into_iter().map(|(id, n)| (id, Some(n))).collect();
1402 }
1403 None => {
1404 self.root = None;
1405 self.reduce_issues.clear();
1406 }
1407 }
1408 self.build_issues.clear();
1409 self.style_issues.clear();
1410 self.refresh_issues();
1411 self.reduce_pending = false;
1414 self.needs_build = true;
1415 }
1416
1417 pub(crate) fn refresh_issues(&mut self) {
1419 let mut all: Vec<ReduceIssue> = Vec::with_capacity(self.reduce_issues.len() + self.build_issues.len() + self.style_issues.len());
1420 for issue in self.reduce_issues.iter().chain(&self.build_issues).chain(&self.style_issues) {
1421 if !all.contains(issue) {
1422 all.push(issue.clone());
1423 }
1424 }
1425 self.issues = all;
1426 }
1427
1428 fn gap_px(&self, name: &str) -> f32 {
1429 match &self.effective {
1430 Some(t) => t.tokens.spacing.get(name).copied().unwrap_or(0.0) as f32,
1431 None => match name {
1432 "xxs" => 2.0,
1433 "xs" => 4.0,
1434 "sm" => 8.0,
1435 "md" => 12.0,
1436 "lg" => 16.0,
1437 "xl" => 24.0,
1438 _ => 0.0,
1439 },
1440 }
1441 }
1442
1443 pub fn structure_version(&self) -> u64 {
1448 self.structure_version
1449 }
1450
1451 fn placed(&self, i: usize) -> PlacedNode {
1452 let n = &self.nodes[i];
1453 let removed = !self.live[i];
1454 let mut states: Vec<String> = Vec::new();
1455 for q in [&n.part_query, &n.own_query].into_iter().flatten() {
1456 for s in &q.states {
1457 if !states.contains(s) {
1458 states.push(s.clone());
1459 }
1460 }
1461 }
1462 PlacedNode {
1463 index: i as u32,
1464 id: n.id.clone(),
1465 component: n.component.clone(),
1466 part: n.part.clone(),
1467 owner: n.owner.clone(),
1468 owner_component: n.owner_component.clone(),
1469 catalog_id: n.catalog_id.clone(),
1470 extension_kind: n.extension_kind.clone(),
1471 depth: n.depth,
1472 parent: if removed { None } else { n.parent },
1473 children: if removed { Vec::new() } else { n.children.clone() },
1474 layer: n.layer,
1475 kind: n.kind,
1476 props: if removed { Map::new() } else { n.props.clone() },
1477 lines: n.lines,
1478 pressable: n.pressable && !removed,
1479 hidden: n.hidden || removed,
1480 accessibility: n.accessibility.clone(),
1481 trigger_for: n.trigger_for.clone(),
1482 states,
1483 form: n.form.clone(),
1484 live: n.live.clone(),
1485 removed,
1486 }
1487 }
1488
1489 pub fn nodes(&mut self) -> Vec<PlacedNode> {
1491 self.settle();
1492 (0..self.nodes.len()).map(|i| self.placed(i)).collect()
1493 }
1494
1495 pub fn nodes_at(&mut self, indices: &[u32]) -> Vec<PlacedNode> {
1497 self.settle();
1498 indices.iter().filter(|i| (**i as usize) < self.nodes.len()).map(|i| self.placed(*i as usize)).collect()
1499 }
1500
1501 pub fn node_count(&mut self) -> usize {
1503 self.settle();
1504 self.live.iter().filter(|l| **l).count()
1505 }
1506
1507 pub fn visual(&self, index: u32) -> Option<&Visual> {
1509 self.node_state.get(index as usize).map(|s| &s.visual)
1510 }
1511
1512 pub fn visuals(&self) -> Vec<Visual> {
1513 self.node_state.iter().map(|s| s.visual.clone()).collect()
1514 }
1515
1516 pub fn text_style(&self, index: u32) -> Option<&TextStyle> {
1517 self.node_state.get(index as usize).map(|s| &s.text)
1518 }
1519
1520 pub fn layout_node(&self, index: u32) -> Option<&LNode> {
1521 self.nodes.get(index as usize).filter(|_| self.live.get(index as usize).copied().unwrap_or(false))
1522 }
1523
1524 pub fn index_of(&self, id: &str) -> Option<u32> {
1525 self.slot_of.get(id).copied()
1526 }
1527
1528 pub fn hover_styled(&self, index: u32) -> bool {
1536 let Some(n) = self.layout_node(index) else { return false };
1537 if n.base_style.contains_key(":hover") {
1538 return true;
1539 }
1540 let Some(theme) = &self.effective else { return false };
1541 [&n.own_query, &n.part_query].into_iter().flatten().any(|q| crate::theme::recipe_reacts_to(theme, q, "hover"))
1542 }
1543
1544 pub fn last_frame(&self, index: u32) -> Option<Frame> {
1545 self.last_frames.get(index as usize).copied()
1546 }
1547
1548 pub fn direction(&self) -> Direction {
1549 self.direction
1550 }
1551
1552 pub fn is_basic(&self) -> bool {
1554 self.catalog_id == A2UI_BASIC_CATALOG_ID
1555 }
1556
1557 pub fn get_data(&self, path: &str) -> Option<&Value> {
1558 get_pointer(&self.data, path)
1559 }
1560
1561 pub fn strings(&self) -> &StringTable {
1563 &self.strings
1564 }
1565
1566 fn settle(&mut self) {
1571 if self.step_state.is_some() {
1572 return;
1573 }
1574 self.ensure_reduced();
1575 if self.needs_build {
1576 self.subtree_pending.clear();
1577 self.rebuild();
1578 } else {
1579 for id in std::mem::take(&mut self.subtree_pending) {
1580 if !self.rebuild_subtree(&id) {
1581 self.rebuild();
1582 self.subtree_pending.clear();
1583 break;
1584 }
1585 }
1586 }
1587 }
1588
1589 fn state_lender(n: &LNode) -> Option<String> {
1592 let owner = n.owner.as_deref()?;
1593 match (n.owner_component.as_deref(), n.part.as_deref()) {
1594 (Some("Tabs"), Some("close")) => n.id.strip_prefix(&format!("{owner}.close")).map(|i| format!("{owner}.tab{i}")),
1595 _ => None,
1596 }
1597 }
1598
1599 fn lend_states(&mut self, slot: u32) {
1602 let n = &self.nodes[slot as usize];
1603 if let (Some(owner), Some("Tabs"), Some("tab")) = (n.owner.clone(), n.owner_component.as_deref(), n.part.as_deref()) {
1604 if let Some(&close) = n.id.strip_prefix(&format!("{owner}.tab")).and_then(|i| self.slot_of.get(&format!("{owner}.close{i}"))) {
1605 self.style_dirty.insert(close);
1606 }
1607 }
1608 let n = &self.nodes[slot as usize];
1609 let hovered = self.states.get(&n.id).is_some_and(|s| s.iter().any(|x| x == "hover"));
1610 if let (true, Some(owner), Some("Menu"), Some("item")) = (hovered, n.owner.clone(), n.owner_component.as_deref(), n.part.as_deref()) {
1614 let top = n.id.strip_prefix(&format!("{owner}.item.")).filter(|rest| !rest.contains('.')).and_then(|i| i.parse::<usize>().ok());
1615 if let Some(i) = top {
1616 let submenu = n.props.get("kind").and_then(Value::as_str) == Some("submenu");
1617 let open = self.local.submenu.get(&owner).copied();
1618 let next = if submenu { Some(i) } else { None };
1619 if open != next && (submenu || open.is_some()) {
1620 match next {
1621 Some(i) => self.local.submenu.insert(owner.clone(), i),
1622 None => self.local.submenu.remove(&owner),
1623 };
1624 self.needs_build = true;
1625 if !self.pending.iter().any(|e| matches!(e, OutEvent::Relayout)) {
1626 self.pending.push(OutEvent::Relayout);
1627 }
1628 }
1629 }
1630 }
1631 let n = &self.nodes[slot as usize];
1632 if let (true, Some(owner), Some(position), Some("item")) = (hovered, n.owner.clone(), n.props.get("position").and_then(Value::as_u64), n.part.as_deref()) {
1633 let key = match n.owner_component.as_deref() {
1634 Some("SearchList") => Some(owner.clone()),
1635 Some("Menu") => n.id.strip_prefix(&format!("{owner}.item")).and_then(|rest| rest.rsplit_once('.')).map(|(sub, _)| format!("{owner}.search{sub}")),
1637 _ => None,
1638 };
1639 if let Some(key) = key {
1640 if self.local.active.get(&key).copied().unwrap_or(0) != position as usize {
1641 self.local.active.insert(key, position as usize);
1642 self.needs_build = true;
1643 if !self.pending.iter().any(|e| matches!(e, OutEvent::Relayout)) {
1644 self.pending.push(OutEvent::Relayout);
1645 }
1646 }
1647 }
1648 }
1649 }
1650
1651 fn states_of(&self, n: &LNode) -> Vec<String> {
1652 let mut states = self.states.get(&n.id).cloned().unwrap_or_default();
1653 if let Some(lender) = Self::state_lender(n) {
1654 for s in self.states.get(&lender).into_iter().flatten() {
1655 if matches!(s.as_str(), "hover" | "focus" | "focus-visible") && !states.contains(s) {
1656 states.push(s.clone());
1657 }
1658 }
1659 }
1660 if n.props.get("disabled").and_then(Value::as_bool) == Some(true) && !states.iter().any(|s| s == "disabled") {
1661 states.push("disabled".into());
1662 }
1663 for q in [&n.part_query, &n.own_query].into_iter().flatten() {
1664 for s in &q.states {
1665 if !states.contains(s) {
1666 states.push(s.clone());
1667 }
1668 }
1669 }
1670 states
1671 }
1672}