1mod events;
23mod pass;
24mod reconcile;
25mod restyle;
26
27pub use pass::{LayoutStep, LeafData};
28
29use std::collections::{BTreeSet, HashMap, HashSet};
30use std::sync::Arc;
31
32use indexmap::IndexMap;
33use serde::{Deserialize, Serialize};
34use serde_json::{Map, Value};
35use taffy::style::Direction;
36
37use crate::catalog::{CatalogView, A2UI_BASIC_CATALOG_ID};
38use crate::data::{get_pointer, set_pointer};
39use crate::engine::Engine;
40use crate::layout_tree::{LNode, LayerClass, LayerSpec, ListSpec, LocalState, NodeKind, ToastSpec};
41use crate::measure::{ControlBox, MeasureMemo, TextStyle};
42use crate::overlay::OverlayPlacement;
43use crate::recipes::RecipeIndex;
44use crate::reducer::{reduce_surface, ReduceOptions, ReduceResult};
45use crate::strings::StringTable;
46use crate::style::Visual;
47use crate::theme::{apply_contrast, apply_density, resolve_mode, Density, Mode, ModeSetting, ResolvedTheme};
48use crate::types::{ExtensionDef, FlatComponent, NestedNode, ReduceIssue, UiNode};
49
50pub use crate::layout_tree::ToastSpec as Toast;
51
52pub const HOVER_CLOSE_MS: u32 = 150;
56
57#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize)]
60pub struct Frame {
61 pub x: f32,
62 pub y: f32,
63 pub w: f32,
64 pub h: f32,
65}
66
67#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
71pub struct PlacedNode {
72 pub index: u32,
73 pub id: String,
74 pub component: String,
75 #[serde(skip_serializing_if = "Option::is_none")]
76 pub part: Option<String>,
77 #[serde(skip_serializing_if = "Option::is_none")]
78 pub owner: Option<String>,
79 #[serde(skip_serializing_if = "Option::is_none")]
80 pub owner_component: Option<String>,
81 #[serde(skip_serializing_if = "Option::is_none")]
82 pub catalog_id: Option<String>,
83 #[serde(skip_serializing_if = "Option::is_none")]
84 pub extension_kind: Option<String>,
85 pub depth: u32,
86 #[serde(skip_serializing_if = "Option::is_none")]
87 pub parent: Option<u32>,
88 #[serde(default, skip_serializing_if = "Vec::is_empty")]
90 pub children: Vec<u32>,
91 pub layer: u32,
92 pub kind: NodeKind,
93 pub props: Map<String, Value>,
94 #[serde(skip_serializing_if = "Option::is_none")]
95 pub lines: Option<u32>,
96 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
97 pub pressable: bool,
98 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
99 pub hidden: bool,
100 #[serde(skip_serializing_if = "Option::is_none")]
101 pub accessibility: Option<Value>,
102 #[serde(skip_serializing_if = "Option::is_none")]
104 pub trigger_for: Option<String>,
105 #[serde(default, skip_serializing_if = "Vec::is_empty")]
108 pub states: Vec<String>,
109 #[serde(skip_serializing_if = "Option::is_none")]
111 pub form: Option<String>,
112 #[serde(skip_serializing_if = "Option::is_none")]
114 pub live: Option<String>,
115 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
117 pub removed: bool,
118}
119
120#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
121pub struct PlacedFrame {
122 pub index: u32,
123 pub x: f32,
124 pub y: f32,
125 pub w: f32,
126 pub h: f32,
127}
128
129#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
131pub struct Layer {
132 pub layer: u32,
133 pub kind: String,
134 pub owner: String,
135 pub root: u32,
136 #[serde(skip_serializing_if = "Option::is_none")]
137 pub anchor_frame: Option<Frame>,
138 #[serde(skip_serializing_if = "Option::is_none")]
139 pub placement: Option<OverlayPlacement>,
140 pub position: String,
143 pub class: LayerClass,
145 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
147 pub modal: bool,
148 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
150 pub dismissible: bool,
151 pub frames: Vec<PlacedFrame>,
152}
153
154#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
155pub struct ListOutput {
156 pub id: String,
157 pub node: u32,
158 pub content_height: f32,
159 pub start: u32,
160 pub end: u32,
161 pub count: u32,
162 pub windowed: bool,
163 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
165 pub horizontal: bool,
166}
167
168#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
173pub struct StickyOutput {
174 pub index: u32,
175 pub dx: f32,
176 pub dy: f32,
177}
178
179#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
184pub struct ScrollOutput {
185 pub index: u32,
186 pub offset_x: f32,
187 pub offset_y: f32,
188 pub content_width: f32,
189 pub content_height: f32,
190 pub scroll_x: bool,
191 pub scroll_y: bool,
192}
193
194#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
198pub struct NodeDelta {
199 pub added: Vec<u32>,
200 pub removed: Vec<u32>,
201 pub changed: Vec<u32>,
202 pub renumbered: bool,
203}
204
205impl NodeDelta {
206 pub fn is_empty(&self) -> bool {
207 self.added.is_empty() && self.removed.is_empty() && self.changed.is_empty() && !self.renumbered
208 }
209}
210
211#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
212pub struct LayoutOutput {
213 pub frames: Vec<PlacedFrame>,
215 pub layers: Vec<Layer>,
216 pub lists: Vec<ListOutput>,
217 pub scrolls: Vec<ScrollOutput>,
218 #[serde(default, skip_serializing_if = "Vec::is_empty")]
221 pub sticky: Vec<StickyOutput>,
222 pub toasts: Vec<ToastSpec>,
225 pub visual_changes: Vec<u32>,
227 pub delta: NodeDelta,
229 pub structure_version: u64,
231 pub surface_width: f32,
232 pub surface_height: f32,
233 pub overflow: bool,
235 pub direction: String,
237 pub breakpoint: Option<String>,
239 pub measure_rounds: u32,
240 pub upcalls: u32,
241 pub restyled: u32,
243 pub rebuilt: bool,
245 pub built_nodes: u32,
248 pub layout_ns: u64,
249}
250
251#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
256#[serde(tag = "kind", rename_all = "camelCase", rename_all_fields = "camelCase")]
257#[non_exhaustive]
258pub enum OutEvent {
259 Action { name: String, component_id: String, event: String, context: Value, #[serde(skip_serializing_if = "Option::is_none")] payload: Option<Value> },
262 OpenUrl { url: String },
264 FunctionCall { component_id: String, name: String, args: Value },
271 DataChanged { path: String, value: Value },
273 Input { component_id: String, name: String, #[serde(skip_serializing_if = "Option::is_none")] path: Option<String>, value: Value, commit: bool },
275 Focus { id: String, index: u32 },
277 Announce { text: String, live: String },
279 Copy { text: String },
281 PickFiles { component_id: String, #[serde(skip_serializing_if = "Option::is_none")] accept: Option<String>, multiple: bool },
284 Relayout,
286 HoverTimer { owner: String, delay_ms: u32 },
291 ScrollSurface { x: f32, y: f32 },
295}
296
297#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
299#[serde(rename_all = "camelCase")]
300pub enum SurfaceCommand {
301 Focus { id: String },
302 Announce { text: String, #[serde(default)] live: Option<String> },
303 ScrollIntoView { id: String },
304 ScrollToIndex {
307 id: String,
308 index: u32,
309 #[serde(default, skip_serializing_if = "Option::is_none")]
310 align: Option<String>,
311 },
312}
313
314#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize)]
316pub struct Insets {
317 pub top: f32,
318 pub right: f32,
319 pub bottom: f32,
320 pub left: f32,
321}
322
323#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
325#[serde(rename_all = "lowercase")]
326pub enum ContrastSetting {
327 Normal,
328 High,
329 #[default]
330 System,
331}
332
333#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
335#[serde(rename_all = "camelCase", default)]
336pub struct SurfaceSettings {
337 pub locale: String,
339 pub strings: IndexMap<String, String>,
341 pub mode: ModeSetting,
342 pub system_dark: bool,
344 pub density: Density,
345 pub contrast: ContrastSetting,
346 pub system_high_contrast: bool,
348 pub font_scale: f32,
350 pub hover: bool,
352 pub reduced_motion: bool,
354 pub insets: Insets,
355 pub today: Option<String>,
357 pub hover_close_ms: u32,
364 pub read_only: bool,
368 pub form_factor: Option<crate::conditions::FormFactor>,
372}
373
374impl Default for SurfaceSettings {
375 fn default() -> Self {
376 SurfaceSettings {
377 locale: crate::locale::DEFAULT_LOCALE.to_string(),
378 strings: IndexMap::new(),
379 mode: ModeSetting::Light,
380 system_dark: false,
381 density: Density::Default,
382 contrast: ContrastSetting::Normal,
383 system_high_contrast: false,
384 font_scale: 1.0,
385 hover: true,
386 reduced_motion: false,
387 insets: Insets::default(),
388 today: None,
389 hover_close_ms: 0,
390 read_only: false,
391 form_factor: None,
392 }
393 }
394}
395
396#[derive(Debug, Clone)]
397pub struct SurfaceOptions {
398 pub catalog_id: String,
399 pub theme: Option<Arc<ResolvedTheme>>,
401 pub mode: Mode,
402 pub extensions: Vec<ExtensionDef>,
403 pub rounding: bool,
404 pub expand_controls: Option<bool>,
407}
408
409impl Default for SurfaceOptions {
410 fn default() -> Self {
411 SurfaceOptions {
412 catalog_id: crate::catalog::CORE_CATALOG_ID.to_string(),
413 theme: Some(crate::themes::default_theme()),
414 mode: Mode::Light,
415 extensions: Vec::new(),
416 rounding: false,
417 expand_controls: None,
418 }
419 }
420}
421
422#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
423pub struct ApplyOutcome {
424 pub structure_changed: bool,
425 pub issues: Vec<ReduceIssue>,
433}
434
435#[derive(Debug, Clone, Copy, PartialEq, Default)]
437struct RadiusPercent {
438 all: Option<f32>,
439 corners: [Option<f32>; 4],
440}
441
442#[derive(Debug, Clone, Default)]
444struct NodeState {
445 flat: Map<String, Value>,
446 visual: Visual,
447 text: TextStyle,
448 control: ControlBox,
449 content_version: u32,
451 conditional: bool,
453 radius_pct: Option<RadiusPercent>,
454 styled: bool,
456 direction: Direction,
458 line_height_set: bool,
460}
461
462#[derive(Debug, Clone)]
463struct Carried {
464 intrinsics: crate::measure::Intrinsics,
465 heights: Vec<(i64, f32)>,
466 props: Map<String, Value>,
467 lines: Option<u32>,
468}
469
470pub struct Surface {
471 pub id: String,
472 pub catalog_id: String,
473 view: Arc<CatalogView>,
474 recipes: Arc<RecipeIndex>,
475 extensions: Vec<ExtensionDef>,
476 theme: Option<Arc<ResolvedTheme>>,
478 effective: Option<Arc<ResolvedTheme>>,
479 settings: SurfaceSettings,
480 strings: StringTable,
481 mode: Mode,
482 expand_controls: Option<bool>,
483 components: Vec<FlatComponent>,
484 nested: Option<NestedNode>,
485 data: Value,
486 root: Option<UiNode>,
487 pub(crate) issues: Vec<ReduceIssue>,
491 reduce_issues: Vec<ReduceIssue>,
492 pub(crate) build_issues: Vec<ReduceIssue>,
493 pub(crate) style_issues: Vec<ReduceIssue>,
494 local: LocalState,
495 states: HashMap<String, Vec<String>>,
496
497 nodes: Vec<LNode>,
499 live: Vec<bool>,
500 slot_of: HashMap<String, u32>,
501 free: Vec<u32>,
502 main_root: Option<u32>,
503 layers: Vec<LayerSpec>,
504 lists: Vec<ListSpec>,
505 toasts: Vec<ToastSpec>,
506 node_state: Vec<NodeState>,
507 engine: Engine,
508 memo: MeasureMemo,
509 memo_versions: HashMap<u32, u32>,
510 last_measurer: Option<u64>,
511 viewport: (f32, f32),
512 layer_viewport: Option<(f32, f32)>,
514 max_height: Option<f32>,
515 direction: Direction,
516 rounding: bool,
517 structure_version: u64,
518 needs_build: bool,
519 restyle_all: bool,
520 style_dirty: BTreeSet<u32>,
521 visuals_dirty: HashSet<u32>,
522 template_cache: HashMap<String, Option<UiNode>>,
525 reduce_pending: bool,
528 component_index: HashMap<String, usize>,
530 last_frames: Vec<Frame>,
532 archive: HashMap<String, Carried>,
535 responsive: bool,
536 breakpoint: Option<String>,
537 delta_added: BTreeSet<u32>,
538 delta_removed: BTreeSet<u32>,
539 delta_changed: BTreeSet<u32>,
540 renumbered: bool,
541 pending: Vec<OutEvent>,
542 scrolls: Vec<ScrollOutput>,
543 restyled: u32,
545 rebuilt: bool,
546 built_nodes: u32,
547 step_state: Option<pass::PassState>,
549 subtree_pending: BTreeSet<String>,
552 hover_closing: HashSet<String>,
554 seed: crate::layout_tree::BuildSeed,
557 formatter: Arc<dyn crate::format::Formatter>,
559 clock: Option<f64>,
561 data_version: u64,
563}
564
565unsafe impl Send for Surface {}
571
572impl Surface {
573 pub fn new(id: &str, options: SurfaceOptions) -> Surface {
574 let view = CatalogView::with(&options.extensions);
575 let recipes = if options.extensions.is_empty() { RecipeIndex::core() } else { Arc::new(RecipeIndex::new(&view)) };
576 let mut engine = Engine::new();
577 engine.set_rounding(options.rounding);
578 let settings = SurfaceSettings { mode: if options.mode == Mode::Dark { ModeSetting::Dark } else { ModeSetting::Light }, ..SurfaceSettings::default() };
579 let mut s = Surface {
580 id: id.to_string(),
581 catalog_id: options.catalog_id,
582 view,
583 recipes,
584 extensions: options.extensions,
585 theme: options.theme,
586 effective: None,
587 strings: crate::strings::DEFAULT_STRINGS.clone(),
588 settings,
589 mode: options.mode,
590 expand_controls: options.expand_controls,
591 components: Vec::new(),
592 nested: None,
593 data: Value::Object(Map::new()),
594 root: None,
595 issues: Vec::new(),
596 reduce_issues: Vec::new(),
597 build_issues: Vec::new(),
598 style_issues: Vec::new(),
599 local: LocalState::default(),
600 states: HashMap::new(),
601 nodes: Vec::new(),
602 live: Vec::new(),
603 slot_of: HashMap::new(),
604 free: Vec::new(),
605 main_root: None,
606 layers: Vec::new(),
607 lists: Vec::new(),
608 toasts: Vec::new(),
609 node_state: Vec::new(),
610 engine,
611 memo: MeasureMemo::default(),
612 memo_versions: HashMap::new(),
613 last_measurer: None,
614 viewport: (0.0, 0.0),
615 layer_viewport: None,
616 max_height: None,
617 direction: Direction::Ltr,
618 rounding: options.rounding,
619 structure_version: 0,
620 needs_build: true,
621 restyle_all: true,
622 style_dirty: BTreeSet::new(),
623 visuals_dirty: HashSet::new(),
624 template_cache: HashMap::new(),
625 reduce_pending: false,
626 component_index: HashMap::new(),
627 last_frames: Vec::new(),
628 archive: HashMap::new(),
629 responsive: false,
630 breakpoint: None,
631 delta_added: BTreeSet::new(),
632 delta_removed: BTreeSet::new(),
633 delta_changed: BTreeSet::new(),
634 renumbered: false,
635 pending: Vec::new(),
636 scrolls: Vec::new(),
637 restyled: 0,
638 rebuilt: false,
639 built_nodes: 0,
640 step_state: None,
641 subtree_pending: BTreeSet::new(),
642 hover_closing: HashSet::new(),
643 seed: Default::default(),
644 formatter: Arc::new(crate::format::EnglishFormatter),
645 clock: None,
646 data_version: 0,
647 };
648 s.refresh_theme();
649 s
650 }
651
652 pub fn set_formatter(&mut self, formatter: Arc<dyn crate::format::Formatter>) {
662 self.formatter = formatter;
663 self.needs_build = true;
664 }
665
666 pub fn formatter(&self) -> &Arc<dyn crate::format::Formatter> {
668 &self.formatter
669 }
670
671 pub fn set_clock(&mut self, now_ms: Option<f64>) {
675 self.clock = now_ms;
676 self.needs_build = true;
677 }
678
679 pub fn tick(&mut self) {
681 self.needs_build = true;
682 }
683
684 pub fn uses_clock(&self) -> bool {
690 fn relative_call(v: &Value) -> bool {
691 match v {
692 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),
693 Value::Array(a) => a.iter().any(relative_call),
694 _ => false,
695 }
696 }
697 if self.clock.is_some() {
698 return false;
699 }
700 let mut found = false;
701 for tree in self.root.iter().chain(self.template_cache.values().flatten()) {
702 tree.walk(&mut |n| found = found || n.props.values().any(relative_call));
703 }
704 found
705 || self.nodes.iter().zip(&self.live).any(|(n, live)| {
706 *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")))
707 })
708 }
709
710 pub fn register_extension(&mut self, ext: ExtensionDef) -> Result<(), String> {
712 let ext = crate::extension::define_extension(ext)?;
713 let mut all = self.extensions.clone();
714 all.retain(|e| e.id != ext.id);
715 all.push(ext);
716 self.view = CatalogView::with(&all);
717 self.recipes = Arc::new(RecipeIndex::new(&self.view));
718 self.extensions = all;
719 self.template_cache.clear();
720 self.reduce();
721 Ok(())
722 }
723
724 fn refresh_theme(&mut self) {
727 let contrast = match self.settings.contrast {
728 ContrastSetting::High => true,
729 ContrastSetting::Normal => false,
730 ContrastSetting::System => self.settings.system_high_contrast,
731 };
732 self.effective = self.theme.as_ref().map(|t| {
733 if self.settings.density == Density::Default && !contrast {
734 return t.clone();
735 }
736 let mut out = apply_density(t, self.settings.density);
737 if contrast {
738 out = apply_contrast(&out);
739 }
740 Arc::new(out)
741 });
742 self.restyle_all = true;
743 }
744
745 pub fn set_theme(&mut self, theme: Option<Arc<ResolvedTheme>>) {
746 self.theme = theme;
747 self.refresh_theme();
748 self.needs_build = true;
750 self.invalidate_measures();
751 }
752
753 pub fn theme(&self) -> Option<&Arc<ResolvedTheme>> {
754 self.theme.as_ref()
755 }
756
757 pub fn effective_theme(&self) -> Option<&Arc<ResolvedTheme>> {
760 self.effective.as_ref()
761 }
762
763 pub fn set_mode(&mut self, mode: Mode) {
764 self.settings.mode = if mode == Mode::Dark { ModeSetting::Dark } else { ModeSetting::Light };
765 if self.mode != mode {
766 self.mode = mode;
767 self.restyle_all = true;
768 }
769 }
770
771 pub fn mode(&self) -> Mode {
772 self.mode
773 }
774
775 pub fn settings(&self) -> &SurfaceSettings {
776 &self.settings
777 }
778
779 pub fn set_settings(&mut self, settings: SurfaceSettings) {
782 let old = std::mem::replace(&mut self.settings, settings);
783 let s = self.settings.clone();
784 if old.strings != s.strings {
785 self.strings = crate::strings::string_table(&s.strings);
786 self.needs_build = true;
787 }
788 if old.read_only != s.read_only {
789 self.needs_build = true;
790 self.restyle_all = true;
791 }
792 if old.locale != s.locale || old.today != s.today {
793 self.needs_build = true;
794 self.restyle_all = true;
795 }
796 if old.density != s.density || old.contrast != s.contrast || old.system_high_contrast != s.system_high_contrast {
797 self.refresh_theme();
798 }
799 let mode = resolve_mode(s.mode, s.system_dark);
800 if mode != self.mode {
801 self.mode = mode;
802 self.restyle_all = true;
803 }
804 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 {
805 self.restyle_all = true;
806 }
807 if old.insets != s.insets {
808 self.engine.mark_all_dirty();
809 }
810 }
811
812 pub fn set_read_only(&mut self, on: bool) {
814 let mut s = self.settings.clone();
815 s.read_only = on;
816 self.set_settings(s);
817 }
818
819 pub fn set_locale(&mut self, locale: &str) {
820 let mut s = self.settings.clone();
821 s.locale = locale.to_string();
822 self.set_settings(s);
823 }
824
825 pub fn set_strings(&mut self, overrides: IndexMap<String, String>) {
826 let mut s = self.settings.clone();
827 s.strings = overrides;
828 self.set_settings(s);
829 }
830
831 pub fn set_mode_setting(&mut self, mode: ModeSetting, system_dark: bool) {
833 let mut s = self.settings.clone();
834 s.mode = mode;
835 s.system_dark = system_dark;
836 self.set_settings(s);
837 }
838
839 pub fn set_density(&mut self, density: Density) {
840 let mut s = self.settings.clone();
841 s.density = density;
842 self.set_settings(s);
843 }
844
845 pub fn set_contrast(&mut self, contrast: ContrastSetting, system_high: bool) {
846 let mut s = self.settings.clone();
847 s.contrast = contrast;
848 s.system_high_contrast = system_high;
849 self.set_settings(s);
850 }
851
852 pub fn set_font_scale(&mut self, scale: f32) {
854 let mut s = self.settings.clone();
855 s.font_scale = scale.max(0.1);
856 self.set_settings(s);
857 }
858
859 pub fn set_insets(&mut self, insets: Insets) {
860 let mut s = self.settings.clone();
861 s.insets = insets;
862 self.set_settings(s);
863 }
864
865 pub fn set_form_factor(&mut self, form_factor: Option<crate::conditions::FormFactor>) {
867 let mut s = self.settings.clone();
868 s.form_factor = form_factor;
869 self.set_settings(s);
870 }
871
872 pub fn set_pointer(&mut self, hover: bool, reduced_motion: bool) {
874 let mut s = self.settings.clone();
875 s.hover = hover;
876 s.reduced_motion = reduced_motion;
877 self.set_settings(s);
878 }
879
880 pub fn set_rounding(&mut self, on: bool) {
883 self.rounding = on;
884 self.engine.set_rounding(on);
885 self.engine.mark_all_dirty();
886 }
887
888 pub fn set_viewport(&mut self, width: f32, height: f32, max_height: Option<f32>) -> bool {
892 if self.viewport == (width, height) && self.max_height == max_height {
893 return false;
894 }
895 let height_changed = self.viewport.1 != height || self.max_height != max_height;
896 self.viewport = (width, height);
897 self.max_height = max_height;
898 for (i, s) in self.node_state.iter().enumerate() {
899 if s.conditional && self.live.get(i).copied().unwrap_or(false) {
900 self.style_dirty.insert(i as u32);
901 }
902 }
903 if (height_changed && !self.lists.is_empty()) || (self.responsive && self.active_breakpoint() != self.breakpoint) {
906 self.needs_build = true;
907 }
908 true
909 }
910
911 pub fn viewport(&self) -> (f32, f32) {
912 self.viewport
913 }
914
915 pub fn set_layer_viewport(&mut self, width: f32, height: f32) -> bool {
923 let next = (width.is_finite() && width > 0.0).then_some((width, if height.is_finite() { height.max(0.0) } else { 0.0 }));
924 if self.layer_viewport == next {
925 return false;
926 }
927 self.layer_viewport = next;
928 true
929 }
930
931 pub fn layer_viewport(&self) -> Option<(f32, f32)> {
932 self.layer_viewport
933 }
934
935 fn active_breakpoint(&self) -> Option<String> {
937 crate::conditions::active_breakpoint(self.viewport.0, &self.breakpoints())
938 }
939
940 fn breakpoints(&self) -> IndexMap<String, f32> {
941 match &self.effective {
942 Some(t) if !t.tokens.breakpoint.is_empty() => t.tokens.breakpoint.iter().map(|(k, v)| (k.clone(), *v as f32)).collect(),
943 _ => crate::conditions::default_breakpoints(),
944 }
945 }
946
947 pub fn set_states(&mut self, id: &str, states: Vec<String>) -> bool {
958 let current = self.states.get(id);
959 if current.is_some_and(|c| *c == states) || (current.is_none() && states.is_empty()) {
960 return false;
961 }
962 if states.is_empty() {
963 self.states.remove(id);
964 } else {
965 self.states.insert(id.to_string(), states);
966 }
967 if let Some(&slot) = self.slot_of.get(id) {
968 self.style_dirty.insert(slot);
969 self.lend_states(slot);
970 for target in self.hover_targets(slot) {
971 let open = self.local.open.get(&target).copied().unwrap_or(false);
972 let active = self.hover_active(&target);
973 if active && !open {
974 self.hover_closing.remove(&target);
975 let events = self.set_open(&target, true);
976 self.pending.extend(events.into_iter().filter(|e| !matches!(e, OutEvent::Relayout)));
977 } else if active {
978 self.hover_closing.remove(&target);
979 } else if open && self.settings.hover_close_ms == 0 {
980 let events = self.set_open(&target, false);
981 self.pending.extend(events.into_iter().filter(|e| !matches!(e, OutEvent::Relayout)));
982 } else if open && self.hover_closing.insert(target.clone()) {
983 self.pending.push(OutEvent::HoverTimer { owner: target, delay_ms: self.settings.hover_close_ms });
984 }
985 }
986 }
987 true
988 }
989
990 pub fn hover_timeout(&mut self, owner: &str) -> Vec<OutEvent> {
994 if !self.hover_closing.remove(owner) || !self.opens_on_hover(owner) || self.hover_active(owner) {
995 return vec![];
996 }
997 if !self.local.open.get(owner).copied().unwrap_or(false) {
998 return vec![];
999 }
1000 self.set_open(owner, false)
1001 }
1002
1003 fn hover_targets(&self, slot: u32) -> Vec<String> {
1008 let mut out = Vec::new();
1009 let mut cur = Some(slot);
1010 let mut guard = 0;
1011 while let Some(c) = cur {
1012 guard += 1;
1013 if guard > 4096 || !self.live.get(c as usize).copied().unwrap_or(false) {
1014 break;
1015 }
1016 let n = &self.nodes[c as usize];
1017 if let Some(t) = n.trigger_for.as_ref().filter(|_| c == slot) {
1018 if self.opens_on_hover(t) && !out.contains(t) {
1019 out.push(t.clone());
1020 }
1021 }
1022 cur = match n.parent {
1023 Some(p) => Some(p),
1024 None => match self.layers.iter().find(|l| l.root == c) {
1025 Some(l) => {
1026 if self.opens_on_hover(&l.owner) && !out.contains(&l.owner) {
1027 out.push(l.owner.clone());
1028 }
1029 self.slot_of.get(&l.owner).copied()
1030 }
1031 None => None,
1032 },
1033 };
1034 }
1035 out
1036 }
1037
1038 fn hover_active(&self, target: &str) -> bool {
1040 self.states.iter().any(|(id, st)| {
1041 st.iter().any(|s| s == "hover" || s == "focus-visible" || s == "focus")
1042 && self.slot_of.get(id).is_some_and(|&slot| self.live[slot as usize] && self.hover_targets(slot).iter().any(|t| t == target))
1043 && (st.iter().any(|s| s == "hover" || s == "focus-visible") || self.in_layer_of(self.slot_of[id], target))
1046 })
1047 }
1048
1049 fn in_layer_of(&self, slot: u32, owner: &str) -> bool {
1051 let Some(layer) = self.layers.iter().find(|l| l.owner == owner) else { return false };
1052 let mut cur = Some(slot);
1053 while let Some(c) = cur {
1054 if c == layer.root {
1055 return true;
1056 }
1057 cur = self.nodes[c as usize].parent;
1058 }
1059 false
1060 }
1061
1062 fn opens_on_hover(&self, target: &str) -> bool {
1063 let Some(&slot) = self.slot_of.get(target) else { return false };
1064 let n = &self.nodes[slot as usize];
1065 n.component == "Tooltip" || (n.component == "Popover" && n.props.get("openOn").and_then(Value::as_str) == Some("hover"))
1066 }
1067
1068 pub fn set_pressed(&mut self, ids: &[String]) -> bool {
1070 self.set_flagged("pressed", ids)
1071 }
1072
1073 pub fn set_hovered(&mut self, ids: &[String]) -> bool {
1077 self.set_flagged("hover", ids)
1078 }
1079
1080 pub fn set_hover(&mut self, id: &str, hovered: bool) -> bool {
1083 let mut s = self.states.get(id).cloned().unwrap_or_default();
1084 let has = s.iter().any(|x| x == "hover");
1085 if has == hovered {
1086 return false;
1087 }
1088 if hovered {
1089 s.push("hover".into());
1090 } else {
1091 s.retain(|x| x != "hover");
1092 }
1093 self.set_states(id, s)
1094 }
1095
1096 pub fn host_states(&self, id: &str) -> &[String] {
1099 self.states.get(id).map(Vec::as_slice).unwrap_or(&[])
1100 }
1101
1102 fn set_flagged(&mut self, flag: &str, ids: &[String]) -> bool {
1104 let mut changed = false;
1105 let keep: HashSet<&String> = ids.iter().collect();
1106 let had: Vec<String> = self.states.iter().filter(|(_, s)| s.iter().any(|x| x == flag)).map(|(k, _)| k.clone()).collect();
1107 for id in had {
1108 if !keep.contains(&id) {
1109 let mut s = self.states.get(&id).cloned().unwrap_or_default();
1110 s.retain(|x| x != flag);
1111 changed |= self.set_states(&id, s);
1112 }
1113 }
1114 for id in ids {
1115 let mut s = self.states.get(id).cloned().unwrap_or_default();
1116 if !s.iter().any(|x| x == flag) {
1117 s.push(flag.into());
1118 changed |= self.set_states(id, s);
1119 }
1120 }
1121 changed
1122 }
1123
1124 pub fn take_events(&mut self) -> Vec<OutEvent> {
1127 std::mem::take(&mut self.pending)
1128 }
1129
1130 pub fn invalidate_measures(&mut self) {
1132 self.memo.clear();
1133 self.memo_versions.clear();
1134 self.archive.clear();
1135 self.engine.mark_all_dirty();
1136 }
1137
1138 pub fn mark_dirty(&mut self, index: u32) -> bool {
1140 if !self.live.get(index as usize).copied().unwrap_or(false) {
1141 return false;
1142 }
1143 self.memo.forget(index);
1144 self.memo_versions.remove(&index);
1145 self.engine.mark_dirty(index);
1146 true
1147 }
1148
1149 pub fn apply(&mut self, message: &Value) -> Result<ApplyOutcome, String> {
1156 let obj = message.as_object().ok_or("message must be an object")?;
1157 if let Some(create) = obj.get("createSurface") {
1158 if let Some(cid) = create.get("catalogId").and_then(Value::as_str) {
1159 self.catalog_id = cid.to_string();
1160 }
1161 if let Some(sid) = create.get("surfaceId").and_then(Value::as_str) {
1162 self.id = sid.to_string();
1163 }
1164 self.set_component_list(Vec::new());
1165 self.nested = None;
1166 self.data = Value::Object(Map::new());
1167 self.data_version += 1;
1168 self.local = LocalState::default();
1169 self.template_cache.clear();
1170 self.reduce();
1171 return Ok(ApplyOutcome { structure_changed: true, issues: self.issues.clone() });
1172 }
1173 if let Some(update) = obj.get("updateComponents") {
1174 let components = update.get("components").ok_or("updateComponents.components is required")?;
1175 let list: Vec<FlatComponent> = serde_json::from_value(components.clone()).map_err(|e| format!("components: {e}"))?;
1176 return Ok(self.update_components(list));
1177 }
1178 self.ensure_reduced();
1179 if let Some(update) = obj.get("updateDataModel") {
1180 let path = update.get("path").and_then(Value::as_str).unwrap_or("");
1181 let value = update.get("value").cloned();
1182 let issues = match self.set_data(path, value) {
1183 Ok(()) => Vec::new(),
1184 Err(message) => vec![ReduceIssue { id: path.to_string(), message }],
1185 };
1186 return Ok(ApplyOutcome { structure_changed: self.needs_build, issues });
1187 }
1188 if obj.get("deleteSurface").is_some() {
1189 self.set_component_list(Vec::new());
1190 self.nested = None;
1191 self.root = None;
1192 self.data = Value::Object(Map::new());
1193 self.data_version += 1;
1194 self.local = LocalState::default();
1195 self.needs_build = true;
1196 return Ok(ApplyOutcome { structure_changed: true, issues: Vec::new() });
1197 }
1198 Err("unknown message: expected createSurface | updateComponents | updateDataModel | deleteSurface".into())
1199 }
1200
1201 pub fn update_components(&mut self, list: Vec<FlatComponent>) -> ApplyOutcome {
1207 if self.nested.is_some() {
1208 self.set_component_list(Vec::new());
1209 }
1210 let mut replaced: HashMap<String, Option<FlatComponent>> = HashMap::new();
1213 let mut order: Vec<String> = Vec::new();
1214 for c in list {
1215 if !replaced.contains_key(&c.id) {
1216 order.push(c.id.clone());
1217 }
1218 match self.component_index.get(&c.id) {
1219 Some(&i) => {
1220 let old = std::mem::replace(&mut self.components[i], c);
1221 replaced.entry(old.id.clone()).or_insert(Some(old));
1222 }
1223 None => {
1224 replaced.entry(c.id.clone()).or_insert(None);
1225 self.component_index.insert(c.id.clone(), self.components.len());
1226 self.components.push(c);
1227 }
1228 }
1229 }
1230 self.nested = None;
1231 self.retain_updated_field_values(&replaced);
1232 self.reduce_pending = true;
1233 self.needs_build = true;
1234 let options = ReduceOptions::new(&self.catalog_id).with_view(self.view.clone());
1237 let (components, index) = (&self.components, &self.component_index);
1238 let lookup = |id: &str| index.get(id).map(|&i| components[i].clone());
1239 let mut issues = Vec::new();
1240 for id in &order {
1241 if let Some(&i) = index.get(id.as_str()) {
1242 issues.extend(crate::reducer::component_issues(&components[i], &options, &lookup));
1243 }
1244 }
1245 ApplyOutcome { structure_changed: true, issues }
1246 }
1247
1248 pub fn set_nested(&mut self, tree: NestedNode) -> ApplyOutcome {
1251 self.nested = Some(tree);
1252 self.set_component_list(Vec::new());
1253 self.template_cache.clear();
1254 self.local.field_values.clear();
1255 self.reduce();
1256 ApplyOutcome { structure_changed: true, issues: self.issues.clone() }
1257 }
1258
1259 pub fn set_components(&mut self, components: Vec<FlatComponent>) -> ApplyOutcome {
1261 let before = if self.nested.is_some() { Vec::new() } else { std::mem::take(&mut self.components) };
1262 self.set_component_list(components);
1263 self.nested = None;
1264 self.template_cache.clear();
1265 self.retain_field_values(&before);
1266 self.reduce();
1267 ApplyOutcome { structure_changed: true, issues: self.issues.clone() }
1268 }
1269
1270 fn set_component_list(&mut self, components: Vec<FlatComponent>) {
1272 self.component_index = components.iter().enumerate().map(|(i, c)| (c.id.clone(), i)).collect();
1273 self.components = components;
1274 }
1275
1276 fn retain_updated_field_values(&mut self, replaced: &HashMap<String, Option<FlatComponent>>) {
1280 if self.local.field_values.is_empty() {
1281 return;
1282 }
1283 let (components, index) = (&self.components, &self.component_index);
1284 self.local.field_values.retain(|key, _| {
1285 let mut candidate = key.as_str();
1286 loop {
1287 if let Some(old) = replaced.get(candidate) {
1288 let new = index.get(candidate).map(|&i| &components[i]);
1289 return matches!((old.as_ref(), new), (Some(a), Some(b)) if a == b);
1290 }
1291 if index.contains_key(candidate) {
1292 return true;
1293 }
1294 match candidate.rfind('.') {
1295 Some(i) => candidate = &candidate[..i],
1296 None => return false,
1297 }
1298 }
1299 });
1300 }
1301
1302 fn retain_field_values(&mut self, before: &[FlatComponent]) {
1309 if self.local.field_values.is_empty() {
1310 return;
1311 }
1312 let old: HashMap<&str, &FlatComponent> = before.iter().map(|c| (c.id.as_str(), c)).collect();
1313 let new: HashMap<&str, &FlatComponent> = self.components.iter().map(|c| (c.id.as_str(), c)).collect();
1314 self.local.field_values.retain(|key, _| {
1315 let mut candidate = key.as_str();
1316 loop {
1317 if old.contains_key(candidate) || new.contains_key(candidate) {
1318 return matches!((old.get(candidate), new.get(candidate)), (Some(a), Some(b)) if a == b);
1319 }
1320 match candidate.rfind('.') {
1321 Some(i) => candidate = &candidate[..i],
1322 None => return false,
1323 }
1324 }
1325 });
1326 }
1327
1328 pub fn set_data(&mut self, path: &str, value: Option<Value>) -> Result<(), String> {
1334 set_pointer(&mut self.data, path, value)?;
1335 self.data_version += 1;
1336 self.needs_build = true;
1337 Ok(())
1338 }
1339
1340 pub fn data(&self) -> &Value {
1341 &self.data
1342 }
1343
1344 pub fn components(&self) -> &[FlatComponent] {
1347 &self.components
1348 }
1349
1350 pub fn root(&mut self) -> Option<&UiNode> {
1351 self.ensure_reduced();
1352 self.root.as_ref()
1353 }
1354
1355 pub fn issues(&mut self) -> &[ReduceIssue] {
1358 self.ensure_reduced();
1359 &self.issues
1360 }
1361
1362 pub(crate) fn ensure_reduced(&mut self) {
1364 if self.reduce_pending {
1365 self.reduce();
1366 }
1367 }
1368
1369 fn reduce(&mut self) {
1370 crate::roomy(|| self.reduce_now())
1371 }
1372
1373 fn reduce_now(&mut self) {
1374 self.template_cache.clear();
1375 self.local.static_keys.clear();
1376 self.local.key_cache.clear();
1377 let options = ReduceOptions::new(&self.catalog_id).with_view(self.view.clone());
1378 let result: Option<ReduceResult> = if let Some(nested) = &self.nested {
1379 Some(crate::reducer::reduce_nested(nested, &options))
1380 } else if !self.components.is_empty() {
1381 Some(reduce_surface(&self.components, &options))
1382 } else {
1383 None
1384 };
1385 match result {
1386 Some(r) => {
1387 self.root = Some(r.root);
1388 self.reduce_issues = r.issues;
1389 self.template_cache = r.templates.unwrap_or_default().into_iter().map(|(id, n)| (id, Some(n))).collect();
1392 }
1393 None => {
1394 self.root = None;
1395 self.reduce_issues.clear();
1396 }
1397 }
1398 self.build_issues.clear();
1399 self.style_issues.clear();
1400 self.refresh_issues();
1401 self.reduce_pending = false;
1404 self.needs_build = true;
1405 }
1406
1407 pub(crate) fn refresh_issues(&mut self) {
1409 let mut all: Vec<ReduceIssue> = Vec::with_capacity(self.reduce_issues.len() + self.build_issues.len() + self.style_issues.len());
1410 for issue in self.reduce_issues.iter().chain(&self.build_issues).chain(&self.style_issues) {
1411 if !all.contains(issue) {
1412 all.push(issue.clone());
1413 }
1414 }
1415 self.issues = all;
1416 }
1417
1418 fn gap_px(&self, name: &str) -> f32 {
1419 match &self.effective {
1420 Some(t) => t.tokens.spacing.get(name).copied().unwrap_or(0.0) as f32,
1421 None => match name {
1422 "xxs" => 2.0,
1423 "xs" => 4.0,
1424 "sm" => 8.0,
1425 "md" => 12.0,
1426 "lg" => 16.0,
1427 "xl" => 24.0,
1428 _ => 0.0,
1429 },
1430 }
1431 }
1432
1433 pub fn structure_version(&self) -> u64 {
1438 self.structure_version
1439 }
1440
1441 fn placed(&self, i: usize) -> PlacedNode {
1442 let n = &self.nodes[i];
1443 let removed = !self.live[i];
1444 let mut states: Vec<String> = Vec::new();
1445 for q in [&n.part_query, &n.own_query].into_iter().flatten() {
1446 for s in &q.states {
1447 if !states.contains(s) {
1448 states.push(s.clone());
1449 }
1450 }
1451 }
1452 PlacedNode {
1453 index: i as u32,
1454 id: n.id.clone(),
1455 component: n.component.clone(),
1456 part: n.part.clone(),
1457 owner: n.owner.clone(),
1458 owner_component: n.owner_component.clone(),
1459 catalog_id: n.catalog_id.clone(),
1460 extension_kind: n.extension_kind.clone(),
1461 depth: n.depth,
1462 parent: if removed { None } else { n.parent },
1463 children: if removed { Vec::new() } else { n.children.clone() },
1464 layer: n.layer,
1465 kind: n.kind,
1466 props: if removed { Map::new() } else { n.props.clone() },
1467 lines: n.lines,
1468 pressable: n.pressable && !removed,
1469 hidden: n.hidden || removed,
1470 accessibility: n.accessibility.clone(),
1471 trigger_for: n.trigger_for.clone(),
1472 states,
1473 form: n.form.clone(),
1474 live: n.live.clone(),
1475 removed,
1476 }
1477 }
1478
1479 pub fn nodes(&mut self) -> Vec<PlacedNode> {
1481 self.settle();
1482 (0..self.nodes.len()).map(|i| self.placed(i)).collect()
1483 }
1484
1485 pub fn nodes_at(&mut self, indices: &[u32]) -> Vec<PlacedNode> {
1487 self.settle();
1488 indices.iter().filter(|i| (**i as usize) < self.nodes.len()).map(|i| self.placed(*i as usize)).collect()
1489 }
1490
1491 pub fn node_count(&mut self) -> usize {
1493 self.settle();
1494 self.live.iter().filter(|l| **l).count()
1495 }
1496
1497 pub fn visual(&self, index: u32) -> Option<&Visual> {
1499 self.node_state.get(index as usize).map(|s| &s.visual)
1500 }
1501
1502 pub fn visuals(&self) -> Vec<Visual> {
1503 self.node_state.iter().map(|s| s.visual.clone()).collect()
1504 }
1505
1506 pub fn text_style(&self, index: u32) -> Option<&TextStyle> {
1507 self.node_state.get(index as usize).map(|s| &s.text)
1508 }
1509
1510 pub fn layout_node(&self, index: u32) -> Option<&LNode> {
1511 self.nodes.get(index as usize).filter(|_| self.live.get(index as usize).copied().unwrap_or(false))
1512 }
1513
1514 pub fn index_of(&self, id: &str) -> Option<u32> {
1515 self.slot_of.get(id).copied()
1516 }
1517
1518 pub fn hover_styled(&self, index: u32) -> bool {
1526 let Some(n) = self.layout_node(index) else { return false };
1527 if n.base_style.contains_key(":hover") {
1528 return true;
1529 }
1530 let Some(theme) = &self.effective else { return false };
1531 [&n.own_query, &n.part_query].into_iter().flatten().any(|q| crate::theme::recipe_reacts_to(theme, q, "hover"))
1532 }
1533
1534 pub fn last_frame(&self, index: u32) -> Option<Frame> {
1535 self.last_frames.get(index as usize).copied()
1536 }
1537
1538 pub fn direction(&self) -> Direction {
1539 self.direction
1540 }
1541
1542 pub fn is_basic(&self) -> bool {
1544 self.catalog_id == A2UI_BASIC_CATALOG_ID
1545 }
1546
1547 pub fn get_data(&self, path: &str) -> Option<&Value> {
1548 get_pointer(&self.data, path)
1549 }
1550
1551 pub fn strings(&self) -> &StringTable {
1553 &self.strings
1554 }
1555
1556 fn settle(&mut self) {
1561 if self.step_state.is_some() {
1562 return;
1563 }
1564 self.ensure_reduced();
1565 if self.needs_build {
1566 self.subtree_pending.clear();
1567 self.rebuild();
1568 } else {
1569 for id in std::mem::take(&mut self.subtree_pending) {
1570 if !self.rebuild_subtree(&id) {
1571 self.rebuild();
1572 self.subtree_pending.clear();
1573 break;
1574 }
1575 }
1576 }
1577 }
1578
1579 fn state_lender(n: &LNode) -> Option<String> {
1582 let owner = n.owner.as_deref()?;
1583 match (n.owner_component.as_deref(), n.part.as_deref()) {
1584 (Some("Tabs"), Some("close")) => n.id.strip_prefix(&format!("{owner}.close")).map(|i| format!("{owner}.tab{i}")),
1585 _ => None,
1586 }
1587 }
1588
1589 fn lend_states(&mut self, slot: u32) {
1592 let n = &self.nodes[slot as usize];
1593 if let (Some(owner), Some("Tabs"), Some("tab")) = (n.owner.clone(), n.owner_component.as_deref(), n.part.as_deref()) {
1594 if let Some(&close) = n.id.strip_prefix(&format!("{owner}.tab")).and_then(|i| self.slot_of.get(&format!("{owner}.close{i}"))) {
1595 self.style_dirty.insert(close);
1596 }
1597 }
1598 let n = &self.nodes[slot as usize];
1599 let hovered = self.states.get(&n.id).is_some_and(|s| s.iter().any(|x| x == "hover"));
1600 if let (true, Some(owner), Some("Menu"), Some("item")) = (hovered, n.owner.clone(), n.owner_component.as_deref(), n.part.as_deref()) {
1604 let top = n.id.strip_prefix(&format!("{owner}.item.")).filter(|rest| !rest.contains('.')).and_then(|i| i.parse::<usize>().ok());
1605 if let Some(i) = top {
1606 let submenu = n.props.get("kind").and_then(Value::as_str) == Some("submenu");
1607 let open = self.local.submenu.get(&owner).copied();
1608 let next = if submenu { Some(i) } else { None };
1609 if open != next && (submenu || open.is_some()) {
1610 match next {
1611 Some(i) => self.local.submenu.insert(owner.clone(), i),
1612 None => self.local.submenu.remove(&owner),
1613 };
1614 self.needs_build = true;
1615 if !self.pending.iter().any(|e| matches!(e, OutEvent::Relayout)) {
1616 self.pending.push(OutEvent::Relayout);
1617 }
1618 }
1619 }
1620 }
1621 let n = &self.nodes[slot as usize];
1622 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()) {
1623 let key = match n.owner_component.as_deref() {
1624 Some("SearchList") => Some(owner.clone()),
1625 Some("Menu") => n.id.strip_prefix(&format!("{owner}.item")).and_then(|rest| rest.rsplit_once('.')).map(|(sub, _)| format!("{owner}.search{sub}")),
1627 _ => None,
1628 };
1629 if let Some(key) = key {
1630 if self.local.active.get(&key).copied().unwrap_or(0) != position as usize {
1631 self.local.active.insert(key, position as usize);
1632 self.needs_build = true;
1633 if !self.pending.iter().any(|e| matches!(e, OutEvent::Relayout)) {
1634 self.pending.push(OutEvent::Relayout);
1635 }
1636 }
1637 }
1638 }
1639 }
1640
1641 fn states_of(&self, n: &LNode) -> Vec<String> {
1642 let mut states = self.states.get(&n.id).cloned().unwrap_or_default();
1643 if let Some(lender) = Self::state_lender(n) {
1644 for s in self.states.get(&lender).into_iter().flatten() {
1645 if matches!(s.as_str(), "hover" | "focus" | "focus-visible") && !states.contains(s) {
1646 states.push(s.clone());
1647 }
1648 }
1649 }
1650 if n.props.get("disabled").and_then(Value::as_bool) == Some(true) && !states.iter().any(|s| s == "disabled") {
1651 states.push("disabled".into());
1652 }
1653 for q in [&n.part_query, &n.own_query].into_iter().flatten() {
1654 for s in &q.states {
1655 if !states.contains(s) {
1656 states.push(s.clone());
1657 }
1658 }
1659 }
1660 states
1661 }
1662}