1use std::cell::RefCell;
2use std::collections::{BTreeMap, BTreeSet};
3use std::fmt;
4use std::rc::{Rc, Weak};
5
6use rhai::{
7 Array, CustomType, Dynamic, EvalAltResult, FLOAT, FnPtr, INT, ImmutableString, Map,
8 NativeCallContext, Position, TypeBuilder,
9};
10use unicode_segmentation::UnicodeSegmentation;
11
12use crate::{
13 AssetId, ComponentInstancePath, HostCallback, MotionSource, OpaqueHandle, OverlayId,
14 OverlayKind, OverlayPlacement, PrimitiveNode, ScriptCallback, ScriptGeneration, Style,
15 UiEventBinding, UiEventHandler, UiValue,
16};
17
18#[derive(Clone, Debug, PartialEq)]
19pub enum ImageSourceSpec {
20 Handle(OpaqueHandle),
21 Asset(AssetId),
22}
23
24#[derive(Clone, Debug, PartialEq)]
25pub struct OverlayNodeSpec {
26 pub id: OverlayId,
27 pub parent: Option<OverlayId>,
28 pub kind: OverlayKind,
29 pub placement: OverlayPlacement,
30 pub anchor: Option<crate::OverlayBounds>,
31 pub open: bool,
32 pub gap: f64,
33 pub modal: bool,
34 pub dismiss: OverlayDismissPolicy,
35 pub tooltip_delays: Option<TooltipDelays>,
36 pub initial_focus: OverlayInitialFocus,
37 pub activate_on_trigger: bool,
38 pub width_policy: OverlayWidthPolicy,
39}
40
41#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
42pub enum OverlayWidthPolicy {
43 #[default]
44 Content,
45 MatchTrigger,
46}
47
48#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
54pub enum OverlayInitialFocus {
55 #[default]
57 Panel,
58 First,
66}
67
68#[derive(Clone, Copy, Debug, Eq, PartialEq)]
69pub enum LayerPlacement {
70 TopLeft,
71 TopRight,
72 BottomLeft,
73 BottomRight,
74 Center,
75 Fill,
76}
77
78#[derive(Clone, Debug, PartialEq)]
79pub struct LayerNodeSpec {
80 pub id: OverlayId,
81 pub placement: LayerPlacement,
82 pub inset: f64,
83 pub priority: usize,
84}
85
86#[derive(Clone, Copy, Debug, Eq, PartialEq)]
87pub struct TooltipDelays {
88 pub show_ms: u64,
89 pub hide_ms: u64,
90}
91
92#[derive(Clone, Copy, Debug, PartialEq)]
93pub struct OverlayDismissPolicy {
94 pub escape: bool,
95 pub outside: bool,
96}
97
98#[derive(Clone, Debug, Eq, Hash, PartialEq)]
100pub struct NodeKey(ImmutableString);
101
102impl NodeKey {
103 #[must_use]
104 pub fn new(value: impl Into<ImmutableString>) -> Self {
105 Self(value.into())
106 }
107
108 #[must_use]
109 pub fn as_str(&self) -> &str {
110 self.0.as_str()
111 }
112}
113
114#[derive(Clone, Debug, Eq, PartialEq)]
115pub struct SourceLocation {
116 pub module: String,
117 pub line: u32,
118 pub column: u32,
119}
120
121#[derive(Clone, Debug, PartialEq)]
122pub struct Span {
123 text: ImmutableString,
124 key: Option<ImmutableString>,
125 color: Option<crate::ColorValue>,
126 bold: bool,
127 italic: bool,
128 motions: Vec<MotionSource>,
129}
130
131impl Span {
132 #[must_use]
133 pub fn new(text: impl Into<ImmutableString>) -> Self {
134 Self {
135 text: text.into(),
136 key: None,
137 color: None,
138 bold: false,
139 italic: false,
140 motions: Vec::new(),
141 }
142 }
143
144 #[must_use]
145 pub fn color(mut self, color: crate::ColorValue) -> Self {
146 self.color = Some(color);
147 self
148 }
149
150 #[must_use]
151 pub fn with_key(mut self, key: impl Into<ImmutableString>) -> Self {
152 self.key = Some(key.into());
153 self
154 }
155
156 #[must_use]
157 pub fn motion(mut self, source: MotionSource) -> Self {
158 self.motions
159 .retain(|existing| existing.property() != source.property());
160 self.motions.push(source);
161 self
162 }
163
164 #[must_use]
165 pub const fn bold(mut self) -> Self {
166 self.bold = true;
167 self
168 }
169
170 #[must_use]
171 pub const fn italic(mut self) -> Self {
172 self.italic = true;
173 self
174 }
175
176 #[must_use]
177 pub fn text(&self) -> &str {
178 self.text.as_str()
179 }
180
181 #[must_use]
182 pub fn key(&self) -> Option<&str> {
183 self.key.as_deref()
184 }
185
186 #[must_use]
187 pub fn motions(&self) -> &[MotionSource] {
188 &self.motions
189 }
190
191 #[must_use]
192 pub const fn color_value(&self) -> Option<&crate::ColorValue> {
193 self.color.as_ref()
194 }
195
196 #[must_use]
197 pub const fn is_bold(&self) -> bool {
198 self.bold
199 }
200
201 #[must_use]
202 pub const fn is_italic(&self) -> bool {
203 self.italic
204 }
205}
206
207impl CustomType for Span {
208 fn build(mut builder: TypeBuilder<Self>) {
209 builder
210 .with_name("Span")
211 .with_fn("with_key", |span: &mut Self, key: ImmutableString| {
212 span.clone().with_key(key)
213 })
214 .with_fn("color", |span: &mut Self, color: crate::ColorValue| {
215 span.clone().color(color)
216 })
217 .with_fn("motion", |span: &mut Self, source: MotionSource| {
218 span.clone().motion(source)
219 })
220 .with_fn("bold", |span: &mut Self| span.clone().bold())
221 .with_fn("italic", |span: &mut Self| span.clone().italic());
222 }
223}
224
225#[derive(Clone, Debug, PartialEq)]
226pub enum UiNodeKind {
227 Text {
228 text: ImmutableString,
229 },
230 RichText {
231 text: ImmutableString,
232 spans: Vec<Span>,
233 },
234 Canvas {
235 scene: crate::CanvasScene,
236 },
237 Svg {
238 source: crate::InlineSvg,
239 },
240 Box {
241 children: Vec<UiNode>,
242 },
243 Fragment {
244 children: Vec<UiNode>,
245 },
246 Custom {
247 primitive: PrimitiveNode,
248 },
249 Image {
250 source: ImageSourceSpec,
251 },
252 DirectionalImage {
253 left_to_right: ImageSourceSpec,
254 right_to_left: ImageSourceSpec,
255 },
256 Overlay {
257 trigger: Box<UiNode>,
258 content: Box<UiNode>,
259 spec: OverlayNodeSpec,
260 },
261 Layer {
262 content: Box<UiNode>,
263 spec: LayerNodeSpec,
264 },
265 VirtualCollection {
266 spec: crate::VirtualCollectionNodeSpec,
267 },
268 ErrorBoundary {
269 child: Box<UiNode>,
270 fallback: Box<UiNode>,
271 },
272}
273
274#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
277pub enum UiNodeKindTag {
278 Text,
279 Canvas,
280 Svg,
281 Box,
282 Fragment,
283 Custom,
284 Image,
285 DirectionalImage,
286 Overlay,
287 Layer,
288 VirtualCollection,
289 ErrorBoundary,
290}
291
292#[derive(Clone, Debug, PartialEq)]
295pub struct UiNode {
296 kind: UiNodeKind,
297 key: Option<NodeKey>,
298 style: Rc<Style>,
299 part_styles: BTreeMap<String, Style>,
300 source: Option<SourceLocation>,
301 component_root: Option<ComponentInstancePath>,
302 attributes: BTreeMap<String, UiValue>,
303 handlers: BTreeMap<String, Vec<UiEventBinding>>,
304 handler_payloads: BTreeMap<String, UiValue>,
305 motions: Vec<MotionSource>,
306 exit_motions: Vec<MotionSource>,
307 progress_motions: Vec<crate::MotionProgressBinding>,
308 timelines: Vec<crate::MotionTimeline>,
309 signal_bindings: BTreeMap<crate::SignalProperty, crate::NativeSignal>,
310 table_layout: Option<Rc<crate::table_layout::TableLayout>>,
311 table_column: Option<usize>,
312 element_ref: Option<crate::ElementRef>,
313 presentation: Vec<NodePresentationMutation>,
314 component_snapshot: Option<ComponentOwnedSnapshotRef>,
315}
316
317fn default_node_style() -> Rc<Style> {
318 thread_local! {
319 static STYLE: Rc<Style> = Rc::new(Style::new());
320 }
321 STYLE.with(Rc::clone)
322}
323
324#[derive(Clone, Default)]
325pub(crate) struct ComponentOwnedSnapshot(Rc<RefCell<Option<Rc<UiNode>>>>);
326
327#[derive(Clone, Default)]
328pub(crate) struct ComponentOwnedSnapshotRef(Weak<RefCell<Option<Rc<UiNode>>>>);
329
330impl ComponentOwnedSnapshot {
331 pub(crate) fn reference(&self) -> ComponentOwnedSnapshotRef {
332 ComponentOwnedSnapshotRef(Rc::downgrade(&self.0))
333 }
334
335 pub(crate) fn current(&self) -> Option<Rc<UiNode>> {
336 self.0.borrow().clone()
337 }
338
339 pub(crate) fn restore(&self, snapshot: Option<Rc<UiNode>>) {
340 *self.0.borrow_mut() = snapshot;
341 }
342
343 pub(crate) fn update_if_active(&self, node: &UiNode) {
344 let mut snapshot = self.0.borrow_mut();
345 if snapshot.is_some() {
346 *snapshot = Some(Rc::new(node.clone()));
347 }
348 }
349
350 pub(crate) fn replace_if_active(&self, node: Rc<UiNode>) {
351 let mut snapshot = self.0.borrow_mut();
352 if snapshot.is_some() {
353 *snapshot = Some(node);
354 }
355 }
356}
357
358impl fmt::Debug for ComponentOwnedSnapshot {
359 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
360 formatter
361 .debug_struct("ComponentOwnedSnapshot")
362 .field("active", &self.0.borrow().is_some())
363 .finish()
364 }
365}
366
367impl ComponentOwnedSnapshotRef {
368 fn current(&self) -> Option<Rc<UiNode>> {
369 self.0.upgrade()?.borrow().clone()
370 }
371
372 fn activate(&self, node: &UiNode) {
373 let Some(snapshot) = self.0.upgrade() else {
374 return;
375 };
376 let mut snapshot = snapshot.borrow_mut();
377 if snapshot.is_none() {
378 *snapshot = Some(Rc::new(node.clone()));
379 }
380 }
381}
382
383impl fmt::Debug for ComponentOwnedSnapshotRef {
384 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
385 formatter
386 .debug_struct("ComponentOwnedSnapshotRef")
387 .field("live", &(self.0.strong_count() > 0))
388 .finish()
389 }
390}
391
392impl PartialEq for ComponentOwnedSnapshotRef {
393 fn eq(&self, _: &Self) -> bool {
394 true
399 }
400}
401
402#[derive(Clone, Debug, PartialEq)]
404enum NodePresentationMutation {
405 Key(NodeKey),
406 Style(Rc<Style>),
407 PartStyles(Rc<BTreeMap<String, Style>>),
408 Signal(crate::SignalProperty, crate::NativeSignal),
409 TableTrack(Rc<crate::table_layout::TableLayout>),
410 TableColumn(usize),
411 ElementRef(crate::ElementRef),
412 Attribute(String, UiValue),
413 Handler(String, UiEventBinding),
414 HandlerPayload(String, UiValue),
415 Motion(MotionSource),
416 ExitMotion(MotionSource),
417 ProgressMotion(crate::MotionProgressBinding),
418 Timeline(Box<crate::MotionTimeline>),
419 MotionGroup(String),
420}
421
422impl NodePresentationMutation {
423 fn apply(&self, node: &mut UiNode) {
424 match self {
425 Self::Key(key) => node.key = Some(key.clone()),
426 Self::Style(style) => Rc::make_mut(&mut node.style).merge_in_place(style),
427 Self::PartStyles(styles) => node.part_styles.extend(styles.as_ref().clone()),
428 Self::Signal(property, signal) => {
429 node.signal_bindings.insert(*property, signal.clone());
430 }
431 Self::TableTrack(layout) => node.table_layout = Some(Rc::clone(layout)),
432 Self::TableColumn(index) => node.table_column = Some(*index),
433 Self::ElementRef(reference) => node.element_ref = Some(reference.clone()),
434 Self::Attribute(name, value) => {
435 node.attributes.insert(name.clone(), value.clone());
436 }
437 Self::Handler(event, binding) => {
438 node.handlers
439 .entry(event.clone())
440 .or_default()
441 .push(binding.clone());
442 }
443 Self::HandlerPayload(event, payload) => {
444 node.handler_payloads.insert(event.clone(), payload.clone());
445 }
446 Self::Motion(animation) => {
447 node.motions
448 .retain(|existing| existing.property() != animation.property());
449 node.motions.push(animation.clone());
450 }
451 Self::ExitMotion(motion) => {
452 node.exit_motions
453 .retain(|existing| existing.property() != motion.property());
454 node.exit_motions.push(motion.clone());
455 }
456 Self::ProgressMotion(binding) => {
457 node.progress_motions
458 .retain(|existing| existing.property() != binding.property());
459 node.progress_motions.push(binding.clone());
460 }
461 Self::Timeline(timeline) => {
462 node.timelines
463 .retain(|existing| existing.name != timeline.name);
464 node.timelines.push(timeline.as_ref().clone());
465 }
466 Self::MotionGroup(group) => apply_motion_group_contents(node, group),
467 }
468 }
469
470 fn bind_generation(&mut self, generation: ScriptGeneration) {
471 match self {
472 Self::Handler(_, binding) => {
473 if let Some(callback) = binding.handler_mut().as_script_mut() {
474 callback.bind_generation(generation);
475 }
476 }
477 Self::Timeline(timeline) => timeline.bind_generation(generation),
478 _ => {}
479 }
480 }
481
482 fn bind_component_scope(
483 &mut self,
484 component: &ComponentInstancePath,
485 incarnation: crate::ComponentIncarnation,
486 events: &BTreeMap<String, crate::EventSchema>,
487 native_context: Option<&crate::invocation::ScriptInvocationContext>,
488 ) {
489 match self {
490 Self::Handler(_, binding) => {
491 if let Some(callback) = binding.handler_mut().as_script_mut() {
492 callback.bind_component_scope_if_unset(component, incarnation, events.clone());
493 if let Some(context) = native_context {
494 callback.bind_native_context_if_unset(context.clone());
495 }
496 }
497 }
498 Self::Timeline(timeline) => {
499 timeline.bind_component_scope(component, incarnation, events, native_context);
500 }
501 _ => {}
502 }
503 }
504}
505
506#[derive(Clone, Debug)]
507pub(crate) struct ComponentSubtreeIndex {
508 addresses: BTreeMap<ComponentInstancePath, Vec<ComponentSubtreeStep>>,
509}
510
511#[derive(Clone, Debug)]
512enum ComponentSubtreeStep {
513 Child(usize),
514 CustomNode(String),
515 CustomNodes(String, usize),
516 OverlayTrigger,
517 OverlayContent,
518 LayerContent,
519 VirtualItem(usize),
520 ErrorChild,
521 ErrorFallback,
522}
523
524impl ComponentSubtreeIndex {
525 pub(crate) fn contains(&self, component: &ComponentInstancePath) -> bool {
526 self.addresses.contains_key(component)
527 }
528
529 pub(crate) fn new(root: &UiNode) -> Self {
530 let mut index = Self {
531 addresses: BTreeMap::new(),
532 };
533 index.visit(root, &mut Vec::new());
534 index
535 }
536
537 pub(crate) fn get<'a>(
538 &self,
539 root: &'a UiNode,
540 component: &ComponentInstancePath,
541 ) -> Option<&'a UiNode> {
542 let mut node = root;
543 for step in self.addresses.get(component)? {
544 node = match (step, &node.kind) {
545 (
546 ComponentSubtreeStep::Child(index),
547 UiNodeKind::Box { children } | UiNodeKind::Fragment { children },
548 ) => children.get(*index)?,
549 (ComponentSubtreeStep::CustomNode(name), UiNodeKind::Custom { primitive }) => {
550 match primitive.props.get(name.as_str())? {
551 crate::PrimitiveValue::Node(node) => node,
552 _ => return None,
553 }
554 }
555 (
556 ComponentSubtreeStep::CustomNodes(name, index),
557 UiNodeKind::Custom { primitive },
558 ) => match primitive.props.get(name.as_str())? {
559 crate::PrimitiveValue::Nodes(nodes) => nodes.get(*index)?,
560 _ => return None,
561 },
562 (ComponentSubtreeStep::OverlayTrigger, UiNodeKind::Overlay { trigger, .. }) => {
563 trigger
564 }
565 (ComponentSubtreeStep::OverlayContent, UiNodeKind::Overlay { content, .. }) => {
566 content
567 }
568 (ComponentSubtreeStep::LayerContent, UiNodeKind::Layer { content, .. }) => content,
569 (
570 ComponentSubtreeStep::VirtualItem(index),
571 UiNodeKind::VirtualCollection { spec },
572 ) => spec.realized.get(index)?,
573 (ComponentSubtreeStep::ErrorChild, UiNodeKind::ErrorBoundary { child, .. }) => {
574 child
575 }
576 (
577 ComponentSubtreeStep::ErrorFallback,
578 UiNodeKind::ErrorBoundary { fallback, .. },
579 ) => fallback,
580 _ => return None,
581 };
582 }
583 Some(node)
584 }
585
586 fn visit(&mut self, node: &UiNode, address: &mut Vec<ComponentSubtreeStep>) {
587 if let Some(component) = node.component_root() {
588 self.addresses.insert(component.clone(), address.clone());
589 }
590 match &node.kind {
591 UiNodeKind::Box { children } | UiNodeKind::Fragment { children } => {
592 for (index, child) in children.iter().enumerate() {
593 address.push(ComponentSubtreeStep::Child(index));
594 self.visit(child, address);
595 address.pop();
596 }
597 }
598 UiNodeKind::Custom { primitive } => {
599 for (name, value) in primitive.props.iter() {
600 match value {
601 crate::PrimitiveValue::Node(node) => {
602 address.push(ComponentSubtreeStep::CustomNode(name.to_string()));
603 self.visit(node.as_ref(), address);
604 address.pop();
605 }
606 crate::PrimitiveValue::Nodes(nodes) => {
607 for (index, node) in nodes.iter().enumerate() {
608 address.push(ComponentSubtreeStep::CustomNodes(
609 name.to_string(),
610 index,
611 ));
612 self.visit(node, address);
613 address.pop();
614 }
615 }
616 _ => {}
617 }
618 }
619 }
620 UiNodeKind::Overlay {
621 trigger, content, ..
622 } => {
623 address.push(ComponentSubtreeStep::OverlayTrigger);
624 self.visit(trigger, address);
625 address.pop();
626 address.push(ComponentSubtreeStep::OverlayContent);
627 self.visit(content, address);
628 address.pop();
629 }
630 UiNodeKind::Layer { content, .. } => {
631 address.push(ComponentSubtreeStep::LayerContent);
632 self.visit(content, address);
633 address.pop();
634 }
635 UiNodeKind::VirtualCollection { spec } => {
636 for (index, node) in &spec.realized {
637 address.push(ComponentSubtreeStep::VirtualItem(*index));
638 self.visit(node, address);
639 address.pop();
640 }
641 }
642 UiNodeKind::ErrorBoundary { child, fallback } => {
643 address.push(ComponentSubtreeStep::ErrorChild);
644 self.visit(child, address);
645 address.pop();
646 address.push(ComponentSubtreeStep::ErrorFallback);
647 self.visit(fallback, address);
648 address.pop();
649 }
650 UiNodeKind::Text { .. }
651 | UiNodeKind::RichText { .. }
652 | UiNodeKind::Canvas { .. }
653 | UiNodeKind::Svg { .. }
654 | UiNodeKind::Image { .. }
655 | UiNodeKind::DirectionalImage { .. } => {}
656 }
657 }
658}
659
660impl UiNode {
661 #[must_use]
662 pub fn text(text: impl Into<ImmutableString>) -> Self {
663 Self {
664 kind: UiNodeKind::Text { text: text.into() },
665 key: None,
666 style: default_node_style(),
667 part_styles: BTreeMap::new(),
668 source: None,
669 component_root: None,
670 attributes: BTreeMap::new(),
671 handlers: BTreeMap::new(),
672 handler_payloads: BTreeMap::new(),
673 motions: Vec::new(),
674 exit_motions: Vec::new(),
675 progress_motions: Vec::new(),
676 timelines: Vec::new(),
677 signal_bindings: BTreeMap::new(),
678 table_layout: None,
679 table_column: None,
680 element_ref: None,
681 presentation: Vec::new(),
682 component_snapshot: None,
683 }
684 }
685
686 #[must_use]
687 pub fn rich_text(spans: Vec<Span>) -> Self {
688 let text = spans.iter().map(Span::text).collect::<String>().into();
689 Self {
690 kind: UiNodeKind::RichText { text, spans },
691 key: None,
692 style: default_node_style(),
693 part_styles: BTreeMap::new(),
694 source: None,
695 component_root: None,
696 attributes: BTreeMap::new(),
697 handlers: BTreeMap::new(),
698 handler_payloads: BTreeMap::new(),
699 motions: Vec::new(),
700 exit_motions: Vec::new(),
701 progress_motions: Vec::new(),
702 timelines: Vec::new(),
703 signal_bindings: BTreeMap::new(),
704 table_layout: None,
705 table_column: None,
706 element_ref: None,
707 presentation: Vec::new(),
708 component_snapshot: None,
709 }
710 }
711
712 #[must_use]
713 pub fn canvas(scene: crate::CanvasScene) -> Self {
714 let mut node = Self::text("");
715 node.kind = UiNodeKind::Canvas { scene };
716 node
717 }
718
719 pub fn svg(source: impl Into<String>) -> Result<Self, crate::InlineSvgError> {
726 let mut node = Self::text("");
727 node.kind = UiNodeKind::Svg {
728 source: crate::InlineSvg::new(source)?,
729 };
730 Ok(node)
731 }
732
733 #[must_use]
734 pub fn box_node(children: Vec<Self>) -> Self {
735 Self {
736 kind: UiNodeKind::Box { children },
737 key: None,
738 style: default_node_style(),
739 part_styles: BTreeMap::new(),
740 source: None,
741 component_root: None,
742 attributes: BTreeMap::new(),
743 handlers: BTreeMap::new(),
744 handler_payloads: BTreeMap::new(),
745 motions: Vec::new(),
746 exit_motions: Vec::new(),
747 progress_motions: Vec::new(),
748 timelines: Vec::new(),
749 signal_bindings: BTreeMap::new(),
750 table_layout: None,
751 table_column: None,
752 element_ref: None,
753 presentation: Vec::new(),
754 component_snapshot: None,
755 }
756 }
757
758 #[must_use]
759 pub fn fragment(children: Vec<Self>) -> Self {
760 let mut node = Self::box_node(children);
761 node.kind = match node.kind {
762 UiNodeKind::Box { children } => UiNodeKind::Fragment { children },
763 _ => unreachable!(),
764 };
765 node
766 }
767
768 #[must_use]
769 pub fn column(children: Vec<Self>) -> Self {
770 Self::box_node(children).with_style(&Style::new().flex_col())
771 }
772
773 #[must_use]
774 pub fn row(children: Vec<Self>) -> Self {
775 Self::box_node(children).with_style(&Style::new().flex_row())
776 }
777
778 #[must_use]
779 pub fn custom(primitive: PrimitiveNode) -> Self {
780 let key = primitive.key.as_ref().map(NodeKey::new);
781 Self {
782 kind: UiNodeKind::Custom { primitive },
783 key,
784 style: default_node_style(),
785 part_styles: BTreeMap::new(),
786 source: None,
787 component_root: None,
788 attributes: BTreeMap::new(),
789 handlers: BTreeMap::new(),
790 handler_payloads: BTreeMap::new(),
791 motions: Vec::new(),
792 exit_motions: Vec::new(),
793 progress_motions: Vec::new(),
794 timelines: Vec::new(),
795 signal_bindings: BTreeMap::new(),
796 table_layout: None,
797 table_column: None,
798 element_ref: None,
799 presentation: Vec::new(),
800 component_snapshot: None,
801 }
802 }
803
804 #[must_use]
805 pub fn error_boundary(child: Self, fallback: Self) -> Self {
806 Self {
807 kind: UiNodeKind::ErrorBoundary {
808 child: Box::new(child),
809 fallback: Box::new(fallback),
810 },
811 key: None,
812 style: default_node_style(),
813 part_styles: BTreeMap::new(),
814 source: None,
815 component_root: None,
816 attributes: BTreeMap::new(),
817 handlers: BTreeMap::new(),
818 handler_payloads: BTreeMap::new(),
819 motions: Vec::new(),
820 exit_motions: Vec::new(),
821 progress_motions: Vec::new(),
822 timelines: Vec::new(),
823 signal_bindings: BTreeMap::new(),
824 table_layout: None,
825 table_column: None,
826 element_ref: None,
827 presentation: Vec::new(),
828 component_snapshot: None,
829 }
830 }
831
832 #[must_use]
833 pub fn image(handle: OpaqueHandle) -> Self {
834 Self::image_source(ImageSourceSpec::Handle(handle))
835 }
836
837 #[must_use]
838 pub fn asset_image(asset: AssetId) -> Self {
839 Self::image_source(ImageSourceSpec::Asset(asset))
840 }
841
842 fn image_source(source: ImageSourceSpec) -> Self {
843 Self {
844 kind: UiNodeKind::Image { source },
845 key: None,
846 style: default_node_style(),
847 part_styles: BTreeMap::new(),
848 source: None,
849 component_root: None,
850 attributes: BTreeMap::new(),
851 handlers: BTreeMap::new(),
852 handler_payloads: BTreeMap::new(),
853 motions: Vec::new(),
854 exit_motions: Vec::new(),
855 progress_motions: Vec::new(),
856 timelines: Vec::new(),
857 signal_bindings: BTreeMap::new(),
858 table_layout: None,
859 table_column: None,
860 element_ref: None,
861 presentation: Vec::new(),
862 component_snapshot: None,
863 }
864 }
865
866 #[must_use]
867 pub fn directional_image(left_to_right: OpaqueHandle, right_to_left: OpaqueHandle) -> Self {
868 Self::directional_image_sources(
869 ImageSourceSpec::Handle(left_to_right),
870 ImageSourceSpec::Handle(right_to_left),
871 )
872 }
873
874 #[must_use]
875 pub fn directional_asset_image(left_to_right: AssetId, right_to_left: AssetId) -> Self {
876 Self::directional_image_sources(
877 ImageSourceSpec::Asset(left_to_right),
878 ImageSourceSpec::Asset(right_to_left),
879 )
880 }
881
882 fn directional_image_sources(
883 left_to_right: ImageSourceSpec,
884 right_to_left: ImageSourceSpec,
885 ) -> Self {
886 Self {
887 kind: UiNodeKind::DirectionalImage {
888 left_to_right,
889 right_to_left,
890 },
891 key: None,
892 style: default_node_style(),
893 part_styles: BTreeMap::new(),
894 source: None,
895 component_root: None,
896 attributes: BTreeMap::new(),
897 handlers: BTreeMap::new(),
898 handler_payloads: BTreeMap::new(),
899 motions: Vec::new(),
900 exit_motions: Vec::new(),
901 progress_motions: Vec::new(),
902 timelines: Vec::new(),
903 signal_bindings: BTreeMap::new(),
904 table_layout: None,
905 table_column: None,
906 element_ref: None,
907 presentation: Vec::new(),
908 component_snapshot: None,
909 }
910 }
911
912 #[must_use]
913 pub fn overlay(trigger: Self, content: Self, spec: OverlayNodeSpec) -> Self {
914 Self {
915 kind: UiNodeKind::Overlay {
916 trigger: Box::new(trigger),
917 content: Box::new(content),
918 spec,
919 },
920 key: None,
921 style: default_node_style(),
922 part_styles: BTreeMap::new(),
923 source: None,
924 component_root: None,
925 attributes: BTreeMap::new(),
926 handlers: BTreeMap::new(),
927 handler_payloads: BTreeMap::new(),
928 motions: Vec::new(),
929 exit_motions: Vec::new(),
930 progress_motions: Vec::new(),
931 timelines: Vec::new(),
932 signal_bindings: BTreeMap::new(),
933 table_layout: None,
934 table_column: None,
935 element_ref: None,
936 presentation: Vec::new(),
937 component_snapshot: None,
938 }
939 }
940
941 #[must_use]
942 pub fn layer(content: Self, spec: LayerNodeSpec) -> Self {
943 let key = spec.id.as_str().to_owned();
944 Self {
945 kind: UiNodeKind::Layer {
946 content: Box::new(content),
947 spec,
948 },
949 key: Some(NodeKey::new(key)),
950 style: default_node_style(),
951 part_styles: BTreeMap::new(),
952 source: None,
953 component_root: None,
954 attributes: BTreeMap::new(),
955 handlers: BTreeMap::new(),
956 handler_payloads: BTreeMap::new(),
957 motions: Vec::new(),
958 exit_motions: Vec::new(),
959 progress_motions: Vec::new(),
960 timelines: Vec::new(),
961 signal_bindings: BTreeMap::new(),
962 table_layout: None,
963 table_column: None,
964 element_ref: None,
965 presentation: Vec::new(),
966 component_snapshot: None,
967 }
968 }
969
970 #[must_use]
971 pub fn virtual_collection(spec: crate::VirtualCollectionNodeSpec) -> Self {
972 let key = spec.id.key.clone();
973 Self {
974 kind: UiNodeKind::VirtualCollection { spec },
975 key: Some(NodeKey::new(key)),
976 style: default_node_style(),
977 part_styles: BTreeMap::new(),
978 source: None,
979 component_root: None,
980 attributes: BTreeMap::new(),
981 handlers: BTreeMap::new(),
982 handler_payloads: BTreeMap::new(),
983 motions: Vec::new(),
984 exit_motions: Vec::new(),
985 progress_motions: Vec::new(),
986 timelines: Vec::new(),
987 signal_bindings: BTreeMap::new(),
988 table_layout: None,
989 table_column: None,
990 element_ref: None,
991 presentation: Vec::new(),
992 component_snapshot: None,
993 }
994 }
995
996 fn apply_presentation_mutation(&mut self, mutation: NodePresentationMutation) {
997 if self.component_root.is_some()
998 && let Some(snapshot) = self.component_snapshot.clone()
999 {
1000 snapshot.activate(self);
1001 }
1002 mutation.apply(self);
1003 if self.component_root.is_some() {
1004 self.presentation.push(mutation);
1005 }
1006 }
1007
1008 fn restore_presentation(&mut self, presentation: Vec<NodePresentationMutation>) {
1009 for mutation in &presentation {
1010 mutation.apply(self);
1011 }
1012 self.presentation.extend(presentation);
1013 }
1014
1015 #[must_use]
1016 pub fn with_key(mut self, key: impl Into<ImmutableString>) -> Self {
1017 self.apply_presentation_mutation(NodePresentationMutation::Key(NodeKey::new(key)));
1018 self
1019 }
1020
1021 #[must_use]
1022 pub fn with_style(mut self, style: &Style) -> Self {
1023 self.apply_presentation_mutation(NodePresentationMutation::Style(Rc::new(style.clone())));
1024 self
1025 }
1026
1027 pub fn with_signal_binding(
1033 mut self,
1034 property: crate::SignalProperty,
1035 signal: crate::NativeSignal,
1036 ) -> Result<Self, crate::SignalError> {
1037 let expected = property.signal_kind();
1038 let actual = signal.id().kind();
1039 if expected != actual {
1040 return Err(crate::SignalError::InvalidBinding {
1041 property,
1042 expected,
1043 actual,
1044 });
1045 }
1046 self.apply_presentation_mutation(NodePresentationMutation::Signal(property, signal));
1047 Ok(self)
1048 }
1049
1050 #[must_use]
1051 pub fn with_element_ref(mut self, reference: crate::ElementRef) -> Self {
1052 self.apply_presentation_mutation(NodePresentationMutation::ElementRef(reference));
1053 self
1054 }
1055
1056 #[must_use]
1057 pub fn with_scrollbars(mut self, spec: crate::ScrollbarSpec) -> Self {
1058 self.apply_presentation_mutation(NodePresentationMutation::Attribute(
1059 "scrollbar_horizontal".to_owned(),
1060 UiValue::String(spec.horizontal().as_str().to_owned()),
1061 ));
1062 self.apply_presentation_mutation(NodePresentationMutation::Attribute(
1063 "scrollbar_vertical".to_owned(),
1064 UiValue::String(spec.vertical().as_str().to_owned()),
1065 ));
1066 self
1067 }
1068
1069 #[must_use]
1070 pub fn with_part_styles(mut self, styles: BTreeMap<String, Style>) -> Self {
1071 self.apply_presentation_mutation(NodePresentationMutation::PartStyles(Rc::new(styles)));
1072 self
1073 }
1074
1075 pub(crate) fn with_owned_part_styles(mut self, styles: BTreeMap<String, Style>) -> Self {
1076 self.part_styles.extend(styles);
1077 self
1078 }
1079
1080 #[must_use]
1081 pub fn with_part_style(mut self, part: impl Into<String>, style: Style) -> Self {
1082 self.apply_presentation_mutation(NodePresentationMutation::PartStyles(Rc::new(
1083 BTreeMap::from([(part.into(), style)]),
1084 )));
1085 self
1086 }
1087
1088 #[must_use]
1089 pub fn with_source(mut self, source: SourceLocation) -> Self {
1090 self.source = Some(source);
1091 self
1092 }
1093
1094 #[cfg(test)]
1095 pub(crate) fn with_component_root(mut self, component: ComponentInstancePath) -> Self {
1096 self.component_root = Some(component);
1097 self.presentation.clear();
1098 self.component_snapshot = None;
1099 self
1100 }
1101
1102 pub(crate) fn with_component_root_snapshot(
1103 mut self,
1104 component: ComponentInstancePath,
1105 snapshot: ComponentOwnedSnapshotRef,
1106 ) -> Self {
1107 self.component_root = Some(component);
1108 self.presentation.clear();
1109 self.component_snapshot = Some(snapshot);
1110 self
1111 }
1112
1113 #[must_use]
1114 pub fn component_root(&self) -> Option<&ComponentInstancePath> {
1115 self.component_root.as_ref()
1116 }
1117
1118 pub(crate) fn replace_component_subtree(
1119 &mut self,
1120 component: &ComponentInstancePath,
1121 replacement: Self,
1122 ) -> bool {
1123 if self.component_root.as_ref() == Some(component) {
1124 let presentation = std::mem::take(&mut self.presentation);
1125 *self = replacement;
1126 self.restore_presentation(presentation);
1127 return true;
1128 }
1129 match &mut self.kind {
1130 UiNodeKind::Box { children } | UiNodeKind::Fragment { children } => {
1131 replace_in_nodes(children.iter_mut(), component, &replacement)
1132 }
1133 UiNodeKind::Custom { primitive } => {
1134 for (_, value) in primitive.props.iter_mut() {
1135 let replaced = match value {
1136 crate::PrimitiveValue::Node(node) => {
1137 node.replace_component_subtree(component, replacement.clone())
1138 }
1139 crate::PrimitiveValue::Nodes(nodes) => {
1140 replace_in_nodes(nodes.iter_mut(), component, &replacement)
1141 }
1142 _ => false,
1143 };
1144 if replaced {
1145 return true;
1146 }
1147 }
1148 false
1149 }
1150 UiNodeKind::Overlay {
1151 trigger, content, ..
1152 }
1153 | UiNodeKind::ErrorBoundary {
1154 child: trigger,
1155 fallback: content,
1156 } => {
1157 trigger.replace_component_subtree(component, replacement.clone())
1158 || content.replace_component_subtree(component, replacement)
1159 }
1160 UiNodeKind::Layer { content, .. } => {
1161 content.replace_component_subtree(component, replacement)
1162 }
1163 UiNodeKind::VirtualCollection { spec } => {
1164 replace_in_nodes(spec.realized.values_mut(), component, &replacement)
1165 }
1166 UiNodeKind::Text { .. }
1167 | UiNodeKind::RichText { .. }
1168 | UiNodeKind::Canvas { .. }
1169 | UiNodeKind::Svg { .. }
1170 | UiNodeKind::Image { .. }
1171 | UiNodeKind::DirectionalImage { .. } => false,
1172 }
1173 }
1174
1175 pub(crate) fn activate_component_snapshots(&mut self) {
1176 if self.component_root.is_some()
1177 && let Some(snapshot) = self.component_snapshot.clone()
1178 {
1179 snapshot.activate(self);
1180 }
1181 self.visit_component_children_mut(Self::activate_component_snapshots);
1182 }
1183
1184 pub(crate) fn hydrate_component_subtrees(&mut self) {
1185 if let Some(current) = self
1186 .component_snapshot
1187 .as_ref()
1188 .and_then(ComponentOwnedSnapshotRef::current)
1189 {
1190 let presentation = std::mem::take(&mut self.presentation);
1191 *self = current.as_ref().clone();
1192 self.restore_presentation(presentation);
1193 }
1194 self.visit_component_children_mut(Self::hydrate_component_subtrees);
1195 }
1196
1197 fn visit_component_children_mut(&mut self, mut visit: impl FnMut(&mut Self)) {
1198 match &mut self.kind {
1199 UiNodeKind::Box { children } | UiNodeKind::Fragment { children } => {
1200 for child in children {
1201 visit(child);
1202 }
1203 }
1204 UiNodeKind::Custom { primitive } => {
1205 for (_, value) in primitive.props.iter_mut() {
1206 match value {
1207 crate::PrimitiveValue::Node(node) => {
1208 visit(node);
1209 }
1210 crate::PrimitiveValue::Nodes(nodes) => {
1211 for node in nodes {
1212 visit(node);
1213 }
1214 }
1215 crate::PrimitiveValue::Data(_)
1216 | crate::PrimitiveValue::Callback(_)
1217 | crate::PrimitiveValue::Style(_)
1218 | crate::PrimitiveValue::Length(_)
1219 | crate::PrimitiveValue::Asset(_)
1220 | crate::PrimitiveValue::Signal(_)
1221 | crate::PrimitiveValue::Ref(_)
1222 | crate::PrimitiveValue::Document(_) => {}
1223 #[cfg(feature = "charts")]
1224 crate::PrimitiveValue::ChartData(_) => {}
1225 }
1226 }
1227 }
1228 UiNodeKind::Overlay {
1229 trigger, content, ..
1230 } => {
1231 visit(trigger);
1232 visit(content);
1233 }
1234 UiNodeKind::Layer { content, .. } => {
1235 visit(content);
1236 }
1237 UiNodeKind::VirtualCollection { spec } => {
1238 for item in spec.realized.values_mut() {
1239 visit(item);
1240 }
1241 }
1242 UiNodeKind::ErrorBoundary { child, fallback } => {
1243 visit(child);
1244 visit(fallback);
1245 }
1246 UiNodeKind::Text { .. }
1247 | UiNodeKind::RichText { .. }
1248 | UiNodeKind::Canvas { .. }
1249 | UiNodeKind::Svg { .. }
1250 | UiNodeKind::Image { .. }
1251 | UiNodeKind::DirectionalImage { .. } => {}
1252 }
1253 }
1254
1255 pub(crate) fn virtual_collection_items(
1256 &self,
1257 id: &crate::VirtualCollectionId,
1258 ) -> Option<&BTreeMap<usize, UiNode>> {
1259 self.virtual_collection_spec(id).map(|spec| &spec.realized)
1260 }
1261
1262 pub(crate) fn virtual_read_contributions(
1263 &self,
1264 ) -> std::collections::BTreeSet<crate::read_dependency::ReadContribution> {
1265 let mut active = std::collections::BTreeSet::new();
1266 self.collect_virtual_read_contributions(&mut active);
1267 active
1268 }
1269
1270 fn collect_virtual_read_contributions(
1271 &self,
1272 active: &mut std::collections::BTreeSet<crate::read_dependency::ReadContribution>,
1273 ) {
1274 if let UiNodeKind::VirtualCollection { spec } = &self.kind {
1275 for index in spec.realized.keys() {
1276 if let Some(key) = spec.data.key(*index) {
1277 active.insert(crate::read_dependency::ReadContribution::VirtualItem {
1278 collection: spec.id.clone(),
1279 key: key.to_owned(),
1280 });
1281 }
1282 }
1283 }
1284 for (_, children) in self.retained_child_groups() {
1285 for child in children {
1286 child.collect_virtual_read_contributions(active);
1287 }
1288 }
1289 }
1290
1291 pub(crate) fn virtual_collection_spec(
1292 &self,
1293 id: &crate::VirtualCollectionId,
1294 ) -> Option<&crate::VirtualCollectionNodeSpec> {
1295 match &self.kind {
1296 UiNodeKind::VirtualCollection { spec } if &spec.id == id => Some(spec),
1297 UiNodeKind::VirtualCollection { spec } => spec
1298 .realized
1299 .values()
1300 .find_map(|child| child.virtual_collection_spec(id)),
1301 UiNodeKind::Custom { primitive } => {
1302 primitive.props.iter().find_map(|(_, value)| match value {
1303 crate::PrimitiveValue::Node(node) => node.virtual_collection_spec(id),
1304 crate::PrimitiveValue::Nodes(nodes) => nodes
1305 .iter()
1306 .find_map(|node| node.virtual_collection_spec(id)),
1307 _ => None,
1308 })
1309 }
1310 UiNodeKind::Box { children } | UiNodeKind::Fragment { children } => children
1311 .iter()
1312 .find_map(|child| child.virtual_collection_spec(id)),
1313 UiNodeKind::Overlay {
1314 trigger, content, ..
1315 }
1316 | UiNodeKind::ErrorBoundary {
1317 child: trigger,
1318 fallback: content,
1319 } => trigger
1320 .virtual_collection_spec(id)
1321 .or_else(|| content.virtual_collection_spec(id)),
1322 UiNodeKind::Layer { content, .. } => content.virtual_collection_spec(id),
1323 _ => None,
1324 }
1325 }
1326
1327 pub(crate) fn replace_virtual_collection_items(
1328 &mut self,
1329 id: &crate::VirtualCollectionId,
1330 items: BTreeMap<usize, UiNode>,
1331 ) -> bool {
1332 self.replace_virtual_collection_items_in(id, &mut Some(items))
1333 }
1334
1335 fn replace_virtual_collection_items_in(
1340 &mut self,
1341 id: &crate::VirtualCollectionId,
1342 items: &mut Option<BTreeMap<usize, UiNode>>,
1343 ) -> bool {
1344 match &mut self.kind {
1345 UiNodeKind::VirtualCollection { spec } if &spec.id == id => {
1346 if let Some(items) = items.take() {
1347 spec.realized = items;
1348 }
1349 true
1350 }
1351 UiNodeKind::VirtualCollection { spec } => spec
1352 .realized
1353 .values_mut()
1354 .any(|child| child.replace_virtual_collection_items_in(id, items)),
1355 UiNodeKind::Custom { primitive } => {
1356 primitive.props.iter_mut().any(|(_, value)| match value {
1357 crate::PrimitiveValue::Node(node) => {
1358 node.replace_virtual_collection_items_in(id, items)
1359 }
1360 crate::PrimitiveValue::Nodes(nodes) => nodes
1361 .iter_mut()
1362 .any(|node| node.replace_virtual_collection_items_in(id, items)),
1363 _ => false,
1364 })
1365 }
1366 UiNodeKind::Box { children } | UiNodeKind::Fragment { children } => children
1367 .iter_mut()
1368 .any(|child| child.replace_virtual_collection_items_in(id, items)),
1369 UiNodeKind::Overlay {
1370 trigger, content, ..
1371 }
1372 | UiNodeKind::ErrorBoundary {
1373 child: trigger,
1374 fallback: content,
1375 } => {
1376 trigger.replace_virtual_collection_items_in(id, items)
1377 || content.replace_virtual_collection_items_in(id, items)
1378 }
1379 UiNodeKind::Layer { content, .. } => {
1380 content.replace_virtual_collection_items_in(id, items)
1381 }
1382 _ => false,
1383 }
1384 }
1385
1386 #[must_use]
1387 pub fn with_attribute(mut self, name: impl Into<String>, value: UiValue) -> Self {
1388 self.apply_presentation_mutation(NodePresentationMutation::Attribute(name.into(), value));
1389 self
1390 }
1391
1392 #[must_use]
1393 pub fn with_handler(
1394 mut self,
1395 event: impl Into<String>,
1396 handler: impl Into<UiEventHandler>,
1397 ) -> Self {
1398 self.apply_presentation_mutation(NodePresentationMutation::Handler(
1399 event.into(),
1400 UiEventBinding::new(crate::EventPhase::Target, handler),
1401 ));
1402 self
1403 }
1404
1405 #[must_use]
1406 pub fn with_handler_phase(
1407 mut self,
1408 event: impl Into<String>,
1409 phase: crate::EventPhase,
1410 handler: impl Into<UiEventHandler>,
1411 ) -> Self {
1412 self.apply_presentation_mutation(NodePresentationMutation::Handler(
1413 event.into(),
1414 UiEventBinding::new(phase, handler),
1415 ));
1416 self
1417 }
1418
1419 #[must_use]
1420 pub fn with_host_handler(self, event: impl Into<String>, callback: HostCallback) -> Self {
1421 self.with_handler(event, UiEventHandler::Host(callback))
1422 }
1423
1424 #[must_use]
1425 pub fn with_handler_payload(mut self, event: impl Into<String>, payload: UiValue) -> Self {
1426 self.apply_presentation_mutation(NodePresentationMutation::HandlerPayload(
1427 event.into(),
1428 payload,
1429 ));
1430 self
1431 }
1432
1433 #[must_use]
1434 pub fn with_motion(mut self, animation: MotionSource) -> Self {
1435 self.apply_presentation_mutation(NodePresentationMutation::Motion(animation));
1436 self
1437 }
1438
1439 #[must_use]
1440 pub fn with_exit_motion(mut self, motion: MotionSource) -> Self {
1441 self.apply_presentation_mutation(NodePresentationMutation::ExitMotion(motion));
1442 self
1443 }
1444
1445 #[must_use]
1446 pub fn with_progress_motion(mut self, binding: crate::MotionProgressBinding) -> Self {
1447 self.apply_presentation_mutation(NodePresentationMutation::ProgressMotion(binding));
1448 self
1449 }
1450
1451 #[must_use]
1452 pub fn with_motion_replay_key(mut self, key: impl Into<String>) -> Self {
1453 self.apply_presentation_mutation(NodePresentationMutation::Attribute(
1454 "motion_replay_key".to_owned(),
1455 UiValue::String(key.into()),
1456 ));
1457 self
1458 }
1459
1460 #[must_use]
1461 pub fn with_motion_particle_count(mut self, count: usize) -> Self {
1462 self.apply_presentation_mutation(NodePresentationMutation::Attribute(
1463 "motion_particle_count".to_owned(),
1464 UiValue::Integer(INT::try_from(count).unwrap_or(INT::MAX)),
1465 ));
1466 self
1467 }
1468
1469 #[must_use]
1470 pub fn with_timeline(mut self, timeline: crate::MotionTimeline) -> Self {
1471 self.apply_presentation_mutation(NodePresentationMutation::Timeline(Box::new(timeline)));
1472 self
1473 }
1474
1475 #[must_use]
1477 pub fn with_layout_motion(mut self, duration_ms: u64, easing: crate::MotionEasing) -> Self {
1478 self.apply_presentation_mutation(NodePresentationMutation::Attribute(
1479 "layout_motion_duration_ms".to_owned(),
1480 UiValue::Integer(INT::try_from(duration_ms).unwrap_or(INT::MAX)),
1481 ));
1482 self.apply_presentation_mutation(NodePresentationMutation::Attribute(
1483 "layout_motion_easing".to_owned(),
1484 UiValue::String(easing.as_str().to_owned()),
1485 ));
1486 self
1487 }
1488
1489 #[must_use]
1491 pub fn with_shared_layout(mut self, group: impl Into<String>, id: impl Into<String>) -> Self {
1492 self.apply_presentation_mutation(NodePresentationMutation::Attribute(
1493 "shared_layout_group".to_owned(),
1494 UiValue::String(group.into()),
1495 ));
1496 self.apply_presentation_mutation(NodePresentationMutation::Attribute(
1497 "shared_layout_id".to_owned(),
1498 UiValue::String(id.into()),
1499 ));
1500 self
1501 }
1502
1503 #[must_use]
1504 pub fn with_shared_layout_id(mut self, id: impl Into<String>) -> Self {
1505 self.apply_presentation_mutation(NodePresentationMutation::Attribute(
1506 "shared_layout_id".to_owned(),
1507 UiValue::String(id.into()),
1508 ));
1509 self
1510 }
1511
1512 #[must_use]
1513 pub fn handlers(&self) -> &BTreeMap<String, Vec<UiEventBinding>> {
1514 &self.handlers
1515 }
1516
1517 #[must_use]
1518 pub fn handler(&self, event: &str) -> Option<&UiEventHandler> {
1519 self.handlers
1520 .get(event)?
1521 .iter()
1522 .find(|binding| binding.phase() == crate::EventPhase::Target)
1523 .map(UiEventBinding::handler)
1524 }
1525
1526 #[must_use]
1527 pub fn event_handlers(&self, event: &str) -> &[UiEventBinding] {
1528 self.handlers.get(event).map_or(&[], Vec::as_slice)
1529 }
1530
1531 #[must_use]
1532 pub fn handler_payload(&self, event: &str) -> Option<&UiValue> {
1533 self.handler_payloads.get(event)
1534 }
1535
1536 #[must_use]
1537 pub(crate) const fn handler_payloads(&self) -> &BTreeMap<String, UiValue> {
1538 &self.handler_payloads
1539 }
1540
1541 #[must_use]
1542 pub fn motions(&self) -> &[MotionSource] {
1543 &self.motions
1544 }
1545
1546 #[must_use]
1547 pub fn exit_motions(&self) -> &[MotionSource] {
1548 &self.exit_motions
1549 }
1550
1551 #[must_use]
1552 pub fn progress_motions(&self) -> &[crate::MotionProgressBinding] {
1553 &self.progress_motions
1554 }
1555
1556 pub(crate) fn motion_ghost(&self) -> Option<Self> {
1557 self.motion_ghost_node(true)
1558 }
1559
1560 fn motion_ghost_node(&self, root: bool) -> Option<Self> {
1561 let mut ghost = self.clone();
1562 ghost.handlers.clear();
1563 ghost.handler_payloads.clear();
1564 ghost.signal_bindings.clear();
1565 ghost.element_ref = None;
1566 ghost.component_root = None;
1567 ghost.component_snapshot = None;
1568 ghost.presentation.clear();
1569 ghost.motions.clear();
1570 ghost.exit_motions.clear();
1571 ghost.progress_motions.clear();
1572 ghost.timelines.clear();
1573 ghost.attributes.clear();
1574 ghost
1575 .attributes
1576 .insert("motion_ghost".to_owned(), UiValue::Bool(true));
1577 let style = &mut Rc::make_mut(&mut ghost.style).base;
1578 style.hit_test = None;
1579 style.cursor = None;
1580 if root {
1581 style.margin = crate::LayoutEdgeLengths::default();
1582 style.position = None;
1583 style.top = None;
1584 style.right = None;
1585 style.bottom = None;
1586 style.left = None;
1587 style.inset_start = None;
1588 style.inset_end = None;
1589 style.translate_x = None;
1590 style.translate_y = None;
1591 style.align_self = None;
1592 style.flex_grow = None;
1593 style.flex_grow_weight = None;
1594 style.flex_shrink = None;
1595 style.flex_basis = None;
1596 }
1597 match &mut ghost.kind {
1598 UiNodeKind::Box { children } | UiNodeKind::Fragment { children } => {
1599 *children = children
1600 .iter()
1601 .map(|child| child.motion_ghost_node(false))
1602 .collect::<Option<Vec<_>>>()?;
1603 }
1604 UiNodeKind::Text { .. }
1605 | UiNodeKind::RichText { .. }
1606 | UiNodeKind::Canvas { .. }
1607 | UiNodeKind::Svg { .. }
1608 | UiNodeKind::Image { .. }
1609 | UiNodeKind::DirectionalImage { .. } => {}
1610 UiNodeKind::Custom { .. }
1611 | UiNodeKind::Overlay { .. }
1612 | UiNodeKind::Layer { .. }
1613 | UiNodeKind::VirtualCollection { .. }
1614 | UiNodeKind::ErrorBoundary { .. } => return None,
1615 }
1616 Some(ghost)
1617 }
1618
1619 #[must_use]
1620 pub fn timelines(&self) -> &[crate::MotionTimeline] {
1621 &self.timelines
1622 }
1623
1624 pub(crate) fn bind_generation(&mut self, generation: ScriptGeneration) {
1625 for bindings in self.handlers.values_mut() {
1626 for binding in bindings {
1627 if let Some(callback) = binding.handler_mut().as_script_mut() {
1628 callback.bind_generation(generation);
1629 }
1630 }
1631 }
1632 for timeline in &mut self.timelines {
1633 timeline.bind_generation(generation);
1634 }
1635 for mutation in &mut self.presentation {
1636 mutation.bind_generation(generation);
1637 }
1638 match &mut self.kind {
1639 UiNodeKind::Box { children } | UiNodeKind::Fragment { children } => {
1640 for child in children {
1641 child.bind_generation(generation);
1642 }
1643 }
1644 UiNodeKind::ErrorBoundary { child, fallback } => {
1645 child.bind_generation(generation);
1646 fallback.bind_generation(generation);
1647 }
1648 UiNodeKind::Overlay {
1649 trigger, content, ..
1650 } => {
1651 trigger.bind_generation(generation);
1652 content.bind_generation(generation);
1653 }
1654 UiNodeKind::Layer { content, .. } => content.bind_generation(generation),
1655 UiNodeKind::Text { .. }
1656 | UiNodeKind::RichText { .. }
1657 | UiNodeKind::Canvas { .. }
1658 | UiNodeKind::Svg { .. }
1659 | UiNodeKind::Custom { .. }
1660 | UiNodeKind::Image { .. }
1661 | UiNodeKind::DirectionalImage { .. } => {}
1662 UiNodeKind::VirtualCollection { spec } => {
1663 for item in spec.realized.values_mut() {
1664 item.bind_generation(generation);
1665 }
1666 }
1667 }
1668 }
1669
1670 pub(crate) fn bind_component_scope(
1671 &mut self,
1672 component: &crate::ComponentInstancePath,
1673 incarnation: crate::ComponentIncarnation,
1674 events: &BTreeMap<String, crate::EventSchema>,
1675 native_context: Option<&crate::invocation::ScriptInvocationContext>,
1676 ) {
1677 for bindings in self.handlers.values_mut() {
1678 for binding in bindings {
1679 if let Some(callback) = binding.handler_mut().as_script_mut() {
1680 callback.bind_component_scope_if_unset(component, incarnation, events.clone());
1681 if let Some(context) = native_context {
1682 callback.bind_native_context_if_unset(context.clone());
1683 }
1684 }
1685 }
1686 }
1687 for timeline in &mut self.timelines {
1688 timeline.bind_component_scope(component, incarnation, events, native_context);
1689 }
1690 for mutation in &mut self.presentation {
1691 mutation.bind_component_scope(component, incarnation, events, native_context);
1692 }
1693 match &mut self.kind {
1694 UiNodeKind::Box { children } | UiNodeKind::Fragment { children } => {
1695 for child in children {
1696 child.bind_component_scope(component, incarnation, events, native_context);
1697 }
1698 }
1699 UiNodeKind::Custom { primitive } => {
1700 primitive.props.bind_component_scope(
1701 component,
1702 incarnation,
1703 events,
1704 native_context,
1705 );
1706 }
1707 UiNodeKind::ErrorBoundary { child, fallback } => {
1708 child.bind_component_scope(component, incarnation, events, native_context);
1709 fallback.bind_component_scope(component, incarnation, events, native_context);
1710 }
1711 UiNodeKind::Overlay {
1712 trigger, content, ..
1713 } => {
1714 trigger.bind_component_scope(component, incarnation, events, native_context);
1715 content.bind_component_scope(component, incarnation, events, native_context);
1716 }
1717 UiNodeKind::Layer { content, .. } => {
1718 content.bind_component_scope(component, incarnation, events, native_context);
1719 }
1720 UiNodeKind::VirtualCollection { spec } => {
1721 for item in spec.realized.values_mut() {
1722 item.bind_component_scope(component, incarnation, events, native_context);
1723 }
1724 }
1725 UiNodeKind::Text { .. }
1726 | UiNodeKind::RichText { .. }
1727 | UiNodeKind::Canvas { .. }
1728 | UiNodeKind::Svg { .. }
1729 | UiNodeKind::Image { .. }
1730 | UiNodeKind::DirectionalImage { .. } => {}
1731 }
1732 }
1733
1734 #[must_use]
1735 pub fn kind(&self) -> &UiNodeKind {
1736 &self.kind
1737 }
1738
1739 #[must_use]
1740 pub const fn kind_tag(&self) -> UiNodeKindTag {
1741 match self.kind {
1742 UiNodeKind::Text { .. } | UiNodeKind::RichText { .. } => UiNodeKindTag::Text,
1743 UiNodeKind::Canvas { .. } => UiNodeKindTag::Canvas,
1744 UiNodeKind::Svg { .. } => UiNodeKindTag::Svg,
1745 UiNodeKind::Box { .. } => UiNodeKindTag::Box,
1746 UiNodeKind::Fragment { .. } => UiNodeKindTag::Fragment,
1747 UiNodeKind::Custom { .. } => UiNodeKindTag::Custom,
1748 UiNodeKind::Image { .. } => UiNodeKindTag::Image,
1749 UiNodeKind::DirectionalImage { .. } => UiNodeKindTag::DirectionalImage,
1750 UiNodeKind::Overlay { .. } => UiNodeKindTag::Overlay,
1751 UiNodeKind::Layer { .. } => UiNodeKindTag::Layer,
1752 UiNodeKind::VirtualCollection { .. } => UiNodeKindTag::VirtualCollection,
1753 UiNodeKind::ErrorBoundary { .. } => UiNodeKindTag::ErrorBoundary,
1754 }
1755 }
1756
1757 pub(crate) fn retained_child_groups(&self) -> Vec<(String, Vec<&Self>)> {
1758 match &self.kind {
1759 UiNodeKind::Box { children } | UiNodeKind::Fragment { children } => {
1760 vec![("children".to_owned(), children.iter().collect())]
1761 }
1762 UiNodeKind::Custom { primitive } => primitive
1763 .props
1764 .iter()
1765 .filter_map(|(name, value)| match value {
1766 crate::PrimitiveValue::Node(node) => {
1767 Some((format!("prop:{name}"), vec![node.as_ref()]))
1768 }
1769 crate::PrimitiveValue::Nodes(nodes) => {
1770 Some((format!("prop:{name}"), nodes.iter().collect::<Vec<_>>()))
1771 }
1772 crate::PrimitiveValue::Data(_)
1773 | crate::PrimitiveValue::Callback(_)
1774 | crate::PrimitiveValue::Style(_)
1775 | crate::PrimitiveValue::Length(_)
1776 | crate::PrimitiveValue::Asset(_)
1777 | crate::PrimitiveValue::Signal(_)
1778 | crate::PrimitiveValue::Ref(_)
1779 | crate::PrimitiveValue::Document(_) => None,
1780 #[cfg(feature = "charts")]
1781 crate::PrimitiveValue::ChartData(_) => None,
1782 })
1783 .collect(),
1784 UiNodeKind::Overlay {
1785 trigger, content, ..
1786 } => vec![
1787 ("trigger".to_owned(), vec![trigger.as_ref()]),
1788 ("content".to_owned(), vec![content.as_ref()]),
1789 ],
1790 UiNodeKind::Layer { content, .. } => {
1791 vec![("content".to_owned(), vec![content.as_ref()])]
1792 }
1793 UiNodeKind::VirtualCollection { spec } => {
1794 vec![("items".to_owned(), spec.realized.values().collect())]
1795 }
1796 UiNodeKind::ErrorBoundary { child, fallback } => vec![
1797 ("child".to_owned(), vec![child.as_ref()]),
1798 ("fallback".to_owned(), vec![fallback.as_ref()]),
1799 ],
1800 UiNodeKind::Text { .. }
1801 | UiNodeKind::RichText { .. }
1802 | UiNodeKind::Canvas { .. }
1803 | UiNodeKind::Svg { .. }
1804 | UiNodeKind::Image { .. }
1805 | UiNodeKind::DirectionalImage { .. } => Vec::new(),
1806 }
1807 }
1808
1809 #[must_use]
1810 pub fn key(&self) -> Option<&NodeKey> {
1811 self.key.as_ref()
1812 }
1813
1814 #[must_use]
1815 pub fn style(&self) -> &Style {
1816 &self.style
1817 }
1818
1819 #[must_use]
1820 pub fn part_style(&self, part: &str) -> Option<&Style> {
1821 self.part_styles.get(part)
1822 }
1823
1824 pub fn part_styles(&self) -> impl Iterator<Item = (&str, &Style)> {
1825 self.part_styles
1826 .iter()
1827 .map(|(name, style)| (name.as_str(), style))
1828 }
1829
1830 #[must_use]
1831 pub fn source(&self) -> Option<&SourceLocation> {
1832 self.source.as_ref()
1833 }
1834
1835 #[must_use]
1836 pub fn attributes(&self) -> &BTreeMap<String, UiValue> {
1837 &self.attributes
1838 }
1839
1840 pub fn signal_bindings(
1841 &self,
1842 ) -> impl ExactSizeIterator<Item = (crate::SignalProperty, &crate::NativeSignal)> {
1843 self.signal_bindings
1844 .iter()
1845 .map(|(property, signal)| (*property, signal))
1846 }
1847
1848 #[must_use]
1849 pub const fn element_ref(&self) -> Option<&crate::ElementRef> {
1850 self.element_ref.as_ref()
1851 }
1852
1853 pub(crate) fn table_layout(&self) -> Option<&crate::table_layout::TableLayout> {
1854 self.table_layout.as_deref()
1855 }
1856
1857 pub(crate) const fn table_column(&self) -> Option<usize> {
1858 self.table_column
1859 }
1860
1861 pub(crate) fn resolve_table_layout(
1864 &mut self,
1865 plan: &crate::table_layout::ResolvedColumns,
1866 border: f64,
1867 ) {
1868 self.table_layout = Some(Rc::new(crate::table_layout::TableLayout::Resolved {
1869 extent: plan.extent + border,
1870 }));
1871 if let UiNodeKind::Box { children } = &mut self.kind {
1872 for child in children {
1873 child.resolve_table_columns(plan);
1874 let mut width = Style::new();
1875 width.base.width = Some(crate::Length::Pixels(plan.extent).into());
1876 width.base.flex_shrink = Some(false);
1877 Rc::make_mut(&mut child.style).merge_in_place(&width);
1878 }
1879 }
1880 }
1881
1882 fn resolve_table_columns(&mut self, plan: &crate::table_layout::ResolvedColumns) {
1883 if self.table_layout.is_some() {
1884 return;
1885 }
1886 if let Some(width) = self.table_column.and_then(|index| plan.widths.get(index)) {
1887 let style = Rc::make_mut(&mut self.style);
1888 style.base.width = Some(crate::Length::Pixels(*width).into());
1889 style.base.min_width = style.base.width;
1890 style.base.max_width = style.base.width;
1891 style.base.flex_basis = None;
1892 style.base.flex_grow = Some(false);
1893 style.base.flex_grow_weight = None;
1894 style.base.flex_shrink = Some(false);
1895 self.signal_bindings.remove(&crate::SignalProperty::Width);
1896 self.signal_bindings
1897 .remove(&crate::SignalProperty::WidthOverride);
1898 }
1899 match &mut self.kind {
1900 UiNodeKind::Box { children } | UiNodeKind::Fragment { children } => {
1901 for child in children {
1902 child.resolve_table_columns(plan);
1903 }
1904 }
1905 UiNodeKind::VirtualCollection { spec } => {
1906 for child in spec.realized.values_mut() {
1907 child.resolve_table_columns(plan);
1908 }
1909 }
1910 _ => {}
1911 }
1912 }
1913}
1914
1915fn replace_in_nodes<'a>(
1916 nodes: impl IntoIterator<Item = &'a mut UiNode>,
1917 component: &ComponentInstancePath,
1918 replacement: &UiNode,
1919) -> bool {
1920 for node in nodes {
1921 if node.replace_component_subtree(component, replacement.clone()) {
1922 return true;
1923 }
1924 }
1925 false
1926}
1927
1928impl CustomType for UiNode {
1929 fn build(mut builder: TypeBuilder<Self>) {
1930 builder
1931 .with_name("UiNode")
1932 .with_fn("with_key", |node: &mut Self, key: ImmutableString| {
1933 node.clone().with_key(key)
1934 })
1935 .with_fn("with_style", |node: &mut Self, style: Style| {
1936 node.clone().with_style(&style)
1937 })
1938 .with_fn(
1939 "with_table_track",
1940 |node: &mut Self, columns: Array, signals: Array| {
1941 if !matches!(node.kind(), UiNodeKind::Box { .. }) {
1942 return Err(crate::table_layout::runtime_error(
1943 "Table track requires a Box container",
1944 ));
1945 }
1946 crate::table_layout::decode_columns(&columns, &signals).map(|columns| {
1947 let mut result = node.clone();
1948 result.apply_presentation_mutation(NodePresentationMutation::TableTrack(
1949 Rc::new(crate::table_layout::TableLayout::Columns(columns)),
1950 ));
1951 result
1952 })
1953 },
1954 )
1955 .with_fn(
1956 "with_table_column",
1957 |node: &mut Self, index: INT| -> Result<Self, Box<EvalAltResult>> {
1958 let index = usize::try_from(index).map_err(|_| {
1959 crate::table_layout::runtime_error(
1960 "Table column index must be non-negative",
1961 )
1962 })?;
1963 if index >= 256 {
1964 return Err(crate::table_layout::runtime_error(
1965 "Table column index must be below 256",
1966 ));
1967 }
1968 let mut result = node.clone();
1969 result
1970 .apply_presentation_mutation(NodePresentationMutation::TableColumn(index));
1971 Ok(result)
1972 },
1973 )
1974 .with_fn(
1975 "bind_signal",
1976 |node: &mut Self,
1977 property: ImmutableString,
1978 signal: crate::NativeSignal|
1979 -> Result<Self, Box<EvalAltResult>> {
1980 let property = crate::SignalProperty::parse(property.as_str())
1981 .map_err(|error| Box::new(crate::signal::signal_runtime_error(&error)))?;
1982 node.clone()
1983 .with_signal_binding(property, signal)
1984 .map_err(|error| Box::new(crate::signal::signal_runtime_error(&error)))
1985 },
1986 )
1987 .with_fn(
1988 "bind_parent_signal",
1989 |node: &mut Self,
1990 context: crate::UiContext,
1991 property: ImmutableString,
1992 key: ImmutableString|
1993 -> Result<Self, Box<EvalAltResult>> {
1994 let property = crate::SignalProperty::parse(property.as_str())
1995 .map_err(|error| Box::new(crate::signal::signal_runtime_error(&error)))?;
1996 if property.signal_kind() != crate::SignalKind::OptionalFloat {
1997 return Err(Box::new(crate::signal::signal_runtime_error(
1998 &"parent signal binding currently supports optional-float properties",
1999 )));
2000 }
2001 let signal = context
2002 .parent_optional_float_signal_ref(key.as_str())
2003 .map_err(|error| Box::new(crate::signal::signal_runtime_error(&error)))?;
2004 node.clone()
2005 .with_signal_binding(property, signal)
2006 .map_err(|error| Box::new(crate::signal::signal_runtime_error(&error)))
2007 },
2008 )
2009 .with_fn(
2010 "with_ref",
2011 |node: &mut Self, reference: crate::ElementRef| {
2012 node.clone().with_element_ref(reference)
2013 },
2014 )
2015 .with_fn(
2016 "with_part_style",
2017 |node: &mut Self, part: ImmutableString, style: Style| {
2018 node.clone().with_part_style(part.to_string(), style)
2019 },
2020 );
2021 register_value_event_methods(&mut builder);
2022 register_raw_event_methods(&mut builder);
2023 register_semantic_event_methods(&mut builder);
2024 register_node_behavior_methods(&mut builder);
2025 register_accessibility_methods(&mut builder);
2026 }
2027}
2028
2029#[allow(clippy::too_many_lines)]
2030fn register_node_behavior_methods(builder: &mut TypeBuilder<UiNode>) {
2031 builder
2032 .with_fn("motion", |node: &mut UiNode, animation: MotionSource| {
2033 node.clone().with_motion(animation)
2034 })
2035 .with_fn("enter_motion", |node: &mut UiNode, motion: MotionSource| {
2036 node.clone().with_motion(motion)
2037 })
2038 .with_fn("exit_motion", |node: &mut UiNode, motion: MotionSource| {
2039 node.clone().with_exit_motion(motion)
2040 })
2041 .with_fn(
2042 "progress_motion",
2043 |node: &mut UiNode, binding: crate::MotionProgressBinding| {
2044 node.clone().with_progress_motion(binding)
2045 },
2046 )
2047 .with_fn(
2048 "motion_replay_key",
2049 |node: &mut UiNode, key: ImmutableString| {
2050 node.clone().with_motion_replay_key(key.to_string())
2051 },
2052 )
2053 .with_fn(
2054 "motion_particle_count",
2055 |node: &mut UiNode, count: INT| -> Result<UiNode, Box<EvalAltResult>> {
2056 let count = usize::try_from(count).map_err(|_| {
2057 Box::new(EvalAltResult::ErrorRuntime(
2058 "motion particle count must be non-negative".into(),
2059 Position::NONE,
2060 ))
2061 })?;
2062 Ok(node.clone().with_motion_particle_count(count))
2063 },
2064 )
2065 .with_fn(
2066 "timeline",
2067 |node: &mut UiNode, timeline: crate::MotionTimeline| {
2068 node.clone().with_timeline(timeline)
2069 },
2070 )
2071 .with_fn(
2072 "layout_motion",
2073 |node: &mut UiNode,
2074 duration_ms: INT,
2075 easing: ImmutableString|
2076 -> Result<UiNode, Box<EvalAltResult>> {
2077 let duration_ms = u64::try_from(duration_ms).map_err(|_| {
2078 Box::new(EvalAltResult::ErrorRuntime(
2079 "layout motion duration must be non-negative".into(),
2080 Position::NONE,
2081 ))
2082 })?;
2083 let easing = crate::MotionEasing::parse(easing.as_str()).map_err(|error| {
2084 Box::new(EvalAltResult::ErrorRuntime(
2085 error.to_string().into(),
2086 Position::NONE,
2087 ))
2088 })?;
2089 Ok(node.clone().with_layout_motion(duration_ms, easing))
2090 },
2091 )
2092 .with_fn(
2093 "shared_layout",
2094 |node: &mut UiNode, group: ImmutableString, id: ImmutableString| {
2095 node.clone()
2096 .with_shared_layout(group.to_string(), id.to_string())
2097 },
2098 )
2099 .with_fn("shared_layout", |node: &mut UiNode, id: ImmutableString| {
2100 node.clone().with_shared_layout_id(id.to_string())
2101 })
2102 .with_fn("disabled", |node: &mut UiNode, disabled: bool| {
2103 node.clone()
2104 .with_attribute("disabled", UiValue::Bool(disabled))
2105 })
2106 .with_fn("tab_stop", |node: &mut UiNode, tab_stop: bool| {
2107 node.clone()
2108 .with_attribute("tab_stop", UiValue::Bool(tab_stop))
2109 })
2110 .with_fn(
2111 "selectable",
2112 |node: &mut UiNode, selectable: bool| -> Result<UiNode, Box<EvalAltResult>> {
2113 if selectable && !matches!(node.kind(), UiNodeKind::Text { .. }) {
2114 return Err(Box::new(EvalAltResult::ErrorRuntime(
2115 "selectable(true) is supported only on text() nodes".into(),
2116 Position::NONE,
2117 )));
2118 }
2119 Ok(node
2120 .clone()
2121 .with_attribute("selectable", UiValue::Bool(selectable)))
2122 },
2123 )
2124 .with_fn(
2125 "tab_index",
2126 |node: &mut UiNode, index: INT| -> Result<UiNode, Box<EvalAltResult>> {
2127 if !(-32_768..=32_767).contains(&index) {
2128 return Err(Box::new(EvalAltResult::ErrorRuntime(
2129 "tab index must be between -32768 and 32767".into(),
2130 Position::NONE,
2131 )));
2132 }
2133 Ok(node
2134 .clone()
2135 .with_attribute("tab_index", UiValue::Integer(index)))
2136 },
2137 )
2138 .with_fn("tab_group", |node: &mut UiNode| {
2139 node.clone()
2140 .with_attribute("tab_group", UiValue::Bool(true))
2141 })
2142 .with_fn(
2143 "scrollbars",
2144 |node: &mut UiNode,
2145 horizontal: ImmutableString,
2146 vertical: ImmutableString|
2147 -> Result<UiNode, Box<EvalAltResult>> {
2148 let spec = crate::ScrollbarSpec::new(horizontal.as_str(), vertical.as_str())
2149 .map_err(|error| {
2150 Box::new(EvalAltResult::ErrorRuntime(error.into(), Position::NONE))
2151 })?;
2152 Ok(node.clone().with_scrollbars(spec))
2153 },
2154 )
2155 .with_fn(
2156 "on_key_value",
2157 |call: NativeCallContext<'_>,
2158 node: &mut UiNode,
2159 key: ImmutableString,
2160 callback: FnPtr,
2161 payload: Dynamic|
2162 -> Result<UiNode, Box<EvalAltResult>> {
2163 let key = normalize_key_handler_name(key.as_str())?;
2164 let payload = UiValue::from_dynamic(payload).map_err(|error| {
2165 Box::new(EvalAltResult::ErrorRuntime(
2166 error.to_string().into(),
2167 Position::NONE,
2168 ))
2169 })?;
2170 let event = format!("key:{key}");
2171 Ok(node
2172 .clone()
2173 .with_handler(event.clone(), retained_script_callback(&call, callback)?)
2174 .with_handler_payload(event, payload))
2175 },
2176 );
2177}
2178
2179fn register_value_event_methods(builder: &mut TypeBuilder<UiNode>) {
2180 builder
2181 .with_fn(
2182 "on_click_value",
2183 |call: NativeCallContext<'_>,
2184 node: &mut UiNode,
2185 callback: FnPtr,
2186 payload: Dynamic|
2187 -> Result<UiNode, Box<EvalAltResult>> {
2188 let payload = dynamic_ui_value(payload)?;
2189 Ok(node
2190 .clone()
2191 .with_handler("click", retained_script_callback(&call, callback)?)
2192 .with_handler_payload("click", payload))
2193 },
2194 )
2195 .with_fn(
2196 "on_hover_value",
2197 |call: NativeCallContext<'_>,
2198 node: &mut UiNode,
2199 callback: FnPtr,
2200 payload: Dynamic|
2201 -> Result<UiNode, Box<EvalAltResult>> {
2202 let payload = dynamic_ui_value(payload)?;
2203 Ok(node
2204 .clone()
2205 .with_handler("hover_change", retained_script_callback(&call, callback)?)
2206 .with_handler_payload("hover_change", payload))
2207 },
2208 );
2209}
2210
2211fn register_raw_event_methods(builder: &mut TypeBuilder<UiNode>) {
2212 builder
2213 .with_fn(
2214 "on_click",
2215 |call: NativeCallContext<'_>,
2216 node: &mut UiNode,
2217 callback: FnPtr|
2218 -> Result<UiNode, Box<EvalAltResult>> {
2219 Ok(node
2220 .clone()
2221 .with_handler("click", retained_script_callback(&call, callback)?))
2222 },
2223 )
2224 .with_fn(
2225 "on",
2226 |call: NativeCallContext<'_>,
2227 node: &mut UiNode,
2228 event: ImmutableString,
2229 callback: FnPtr|
2230 -> Result<UiNode, Box<EvalAltResult>> {
2231 let event = normalize_node_event_name(event.as_str())?;
2232 Ok(node
2233 .clone()
2234 .with_handler(event, retained_script_callback(&call, callback)?))
2235 },
2236 )
2237 .with_fn(
2238 "on_capture",
2239 |call: NativeCallContext<'_>,
2240 node: &mut UiNode,
2241 event: ImmutableString,
2242 callback: FnPtr|
2243 -> Result<UiNode, Box<EvalAltResult>> {
2244 let event = normalize_node_event_name(event.as_str())?;
2245 Ok(node.clone().with_handler_phase(
2246 event,
2247 crate::EventPhase::Capture,
2248 retained_script_callback(&call, callback)?,
2249 ))
2250 },
2251 )
2252 .with_fn(
2253 "on_bubble",
2254 |call: NativeCallContext<'_>,
2255 node: &mut UiNode,
2256 event: ImmutableString,
2257 callback: FnPtr|
2258 -> Result<UiNode, Box<EvalAltResult>> {
2259 let event = normalize_node_event_name(event.as_str())?;
2260 Ok(node.clone().with_handler_phase(
2261 event,
2262 crate::EventPhase::Bubble,
2263 retained_script_callback(&call, callback)?,
2264 ))
2265 },
2266 );
2267 register_native_event_methods(builder);
2268}
2269
2270fn register_native_event_methods(builder: &mut TypeBuilder<UiNode>) {
2271 builder
2272 .with_fn(
2273 "on_click",
2274 |node: &mut UiNode,
2275 handler: crate::NativeHandlerRef|
2276 -> Result<UiNode, Box<EvalAltResult>> {
2277 handler.validate_event("click").map_err(|error| {
2278 Box::new(EvalAltResult::ErrorRuntime(
2279 error.to_string().into(),
2280 Position::NONE,
2281 ))
2282 })?;
2283 Ok(node.clone().with_handler("click", handler))
2284 },
2285 )
2286 .with_fn(
2287 "on_click_value",
2288 |node: &mut UiNode,
2289 handler: crate::NativeHandlerRef,
2290 payload: Dynamic|
2291 -> Result<UiNode, Box<EvalAltResult>> {
2292 handler.validate_event("click").map_err(|error| {
2293 Box::new(EvalAltResult::ErrorRuntime(
2294 error.to_string().into(),
2295 Position::NONE,
2296 ))
2297 })?;
2298 Ok(node
2299 .clone()
2300 .with_handler("click", handler)
2301 .with_handler_payload("click", dynamic_ui_value(payload)?))
2302 },
2303 )
2304 .with_fn(
2305 "on",
2306 |node: &mut UiNode,
2307 event: ImmutableString,
2308 handler: crate::NativeHandlerRef|
2309 -> Result<UiNode, Box<EvalAltResult>> {
2310 let event = normalize_node_event_name(event.as_str())?;
2311 handler.validate_event(event.as_str()).map_err(|error| {
2312 Box::new(EvalAltResult::ErrorRuntime(
2313 error.to_string().into(),
2314 Position::NONE,
2315 ))
2316 })?;
2317 Ok(node.clone().with_handler(event, handler))
2318 },
2319 )
2320 .with_fn(
2321 "on_capture",
2322 |node: &mut UiNode,
2323 event: ImmutableString,
2324 handler: crate::NativeHandlerRef|
2325 -> Result<UiNode, Box<EvalAltResult>> {
2326 let event = normalize_node_event_name(event.as_str())?;
2327 handler.validate_event(event.as_str()).map_err(|error| {
2328 Box::new(EvalAltResult::ErrorRuntime(
2329 error.to_string().into(),
2330 Position::NONE,
2331 ))
2332 })?;
2333 Ok(node
2334 .clone()
2335 .with_handler_phase(event, crate::EventPhase::Capture, handler))
2336 },
2337 )
2338 .with_fn(
2339 "on_bubble",
2340 |node: &mut UiNode,
2341 event: ImmutableString,
2342 handler: crate::NativeHandlerRef|
2343 -> Result<UiNode, Box<EvalAltResult>> {
2344 let event = normalize_node_event_name(event.as_str())?;
2345 handler.validate_event(event.as_str()).map_err(|error| {
2346 Box::new(EvalAltResult::ErrorRuntime(
2347 error.to_string().into(),
2348 Position::NONE,
2349 ))
2350 })?;
2351 Ok(node
2352 .clone()
2353 .with_handler_phase(event, crate::EventPhase::Bubble, handler))
2354 },
2355 );
2356}
2357
2358fn normalize_node_event_name(event: &str) -> Result<String, Box<EvalAltResult>> {
2359 if let Some(key) = event.strip_prefix("key:") {
2360 return normalize_key_handler_name(key).map(|key| format!("key:{key}"));
2361 }
2362 if (1..=64).contains(&event.len())
2364 && event.chars().all(|character| {
2365 character.is_ascii_lowercase()
2366 || character.is_ascii_digit()
2367 || matches!(character, '_' | ':')
2368 })
2369 {
2370 Ok(event.to_owned())
2371 } else {
2372 Err(Box::new(EvalAltResult::ErrorRuntime(
2373 format!("event `{event}` must be a 1-64 character snake_case or namespaced name")
2374 .into(),
2375 Position::NONE,
2376 )))
2377 }
2378}
2379
2380fn normalize_key_handler_name(key: &str) -> Result<String, Box<EvalAltResult>> {
2383 let named = key
2384 .bytes()
2385 .all(|byte| byte.is_ascii_alphanumeric() || byte == b'_');
2386 let punctuation =
2387 key.len() == 1 && key.as_bytes()[0].is_ascii_punctuation() && key.as_bytes()[0] != b':';
2388 if (1..=60).contains(&key.len()) && (named || punctuation) {
2389 Ok(key.to_ascii_lowercase())
2390 } else {
2391 Err(Box::new(EvalAltResult::ErrorRuntime(
2392 "key handler name must be 1-60 ASCII letters, digits or `_`, or one ASCII punctuation character other than `:`; whitespace and chords are not accepted"
2393 .into(),
2394 Position::NONE,
2395 )))
2396 }
2397}
2398
2399fn register_semantic_event_methods(builder: &mut TypeBuilder<UiNode>) {
2400 for (method, event) in [
2401 ("on_hover_change", "hover_change"),
2402 ("on_open_change", "open_change"),
2403 ("on_change", "change"),
2404 ] {
2405 let event = event.to_owned();
2406 builder.with_fn(
2407 method,
2408 move |call: NativeCallContext<'_>,
2409 node: &mut UiNode,
2410 callback: FnPtr|
2411 -> Result<UiNode, Box<EvalAltResult>> {
2412 Ok(node
2413 .clone()
2414 .with_handler(event.clone(), retained_script_callback(&call, callback)?))
2415 },
2416 );
2417 }
2418}
2419
2420fn retained_script_callback(
2421 call: &NativeCallContext<'_>,
2422 callback: FnPtr,
2423) -> Result<ScriptCallback, Box<EvalAltResult>> {
2424 let mut callback = ScriptCallback::try_from_fn_ptr(callback, ScriptGeneration::default())
2425 .map_err(|error| {
2426 Box::new(EvalAltResult::ErrorRuntime(
2427 error.to_string().into(),
2428 Position::NONE,
2429 ))
2430 })?;
2431 callback.bind_native_context_if_unset(
2432 crate::invocation::ScriptInvocationContext::capture_retained(call),
2433 );
2434 Ok(callback)
2435}
2436
2437fn register_accessibility_methods(builder: &mut TypeBuilder<UiNode>) {
2438 register_accessibility_state_methods(builder);
2439 register_accessibility_reference_methods(builder);
2440}
2441
2442fn register_accessibility_state_methods(builder: &mut TypeBuilder<UiNode>) {
2443 builder
2444 .with_fn(
2445 "accessibility_role",
2446 |node: &mut UiNode, role: ImmutableString| {
2447 node.clone()
2448 .with_attribute("role", UiValue::String(role.to_string()))
2449 },
2450 )
2451 .with_fn(
2452 "accessibility_label",
2453 |node: &mut UiNode, label: ImmutableString| {
2454 node.clone()
2455 .with_attribute("label", UiValue::String(label.to_string()))
2456 },
2457 )
2458 .with_fn(
2459 "accessibility_checked",
2460 |node: &mut UiNode, value: Dynamic| -> Result<UiNode, Box<EvalAltResult>> {
2461 let value = dynamic_ui_value(value)?;
2462 Ok(node.clone().with_attribute("checked", value))
2463 },
2464 )
2465 .with_fn(
2466 "accessibility_pressed",
2467 |node: &mut UiNode, pressed: bool| {
2468 node.clone()
2469 .with_attribute("pressed", UiValue::Bool(pressed))
2470 },
2471 )
2472 .with_fn(
2473 "accessibility_selected",
2474 |node: &mut UiNode, selected: bool| {
2475 node.clone()
2476 .with_attribute("selected", UiValue::Bool(selected))
2477 },
2478 )
2479 .with_fn(
2480 "accessibility_expanded",
2481 |node: &mut UiNode, expanded: bool| {
2482 node.clone()
2483 .with_attribute("expanded", UiValue::Bool(expanded))
2484 },
2485 )
2486 .with_fn(
2487 "accessibility_orientation",
2488 |node: &mut UiNode,
2489 orientation: ImmutableString|
2490 -> Result<UiNode, Box<EvalAltResult>> {
2491 if !matches!(orientation.as_str(), "horizontal" | "vertical") {
2492 return Err(Box::new(EvalAltResult::ErrorRuntime(
2493 "accessibility_orientation must be horizontal or vertical".into(),
2494 Position::NONE,
2495 )));
2496 }
2497 Ok(node
2498 .clone()
2499 .with_attribute("orientation", UiValue::String(orientation.to_string())))
2500 },
2501 );
2502 register_accessibility_value_methods(builder);
2503 register_accessibility_collection_methods(builder);
2504}
2505
2506fn register_accessibility_value_methods(builder: &mut TypeBuilder<UiNode>) {
2507 register_accessibility_range_methods(builder);
2508 builder
2509 .with_fn(
2510 "accessibility_value",
2511 |node: &mut UiNode, value: Dynamic| -> Result<UiNode, Box<EvalAltResult>> {
2512 let value = dynamic_ui_value(value)?;
2513 Ok(node.clone().with_attribute("value", value))
2514 },
2515 )
2516 .with_fn(
2517 "accessibility_placeholder",
2518 |node: &mut UiNode, placeholder: ImmutableString| {
2519 node.clone()
2520 .with_attribute("placeholder", UiValue::String(placeholder.to_string()))
2521 },
2522 )
2523 .with_fn(
2524 "accessibility_key_shortcuts",
2525 |node: &mut UiNode, shortcuts: ImmutableString| {
2526 node.clone()
2527 .with_attribute("key_shortcuts", UiValue::String(shortcuts.to_string()))
2528 },
2529 )
2530 .with_fn(
2531 "accessibility_current",
2532 |node: &mut UiNode, value: ImmutableString| -> Result<UiNode, Box<EvalAltResult>> {
2533 if !matches!(
2534 value.as_str(),
2535 "page" | "step" | "location" | "date" | "time" | "true"
2536 ) {
2537 return Err(Box::new(EvalAltResult::ErrorRuntime(
2538 "accessibility_current must be page, step, location, date, time, or true"
2539 .into(),
2540 Position::NONE,
2541 )));
2542 }
2543 Ok(node
2544 .clone()
2545 .with_attribute("current", UiValue::String(value.to_string())))
2546 },
2547 )
2548 .with_fn(
2549 "accessibility_invalid",
2550 |node: &mut UiNode, invalid: bool| {
2551 node.clone()
2552 .with_attribute("invalid", UiValue::Bool(invalid))
2553 },
2554 )
2555 .with_fn(
2556 "accessibility_read_only",
2557 |node: &mut UiNode, read_only: bool| {
2558 node.clone()
2559 .with_attribute("read_only", UiValue::Bool(read_only))
2560 },
2561 );
2562}
2563
2564fn register_accessibility_collection_methods(builder: &mut TypeBuilder<UiNode>) {
2565 builder
2566 .with_fn(
2567 "accessibility_option",
2568 |node: &mut UiNode,
2569 label: ImmutableString,
2570 selected: bool,
2571 position: INT,
2572 size: INT|
2573 -> Result<UiNode, Box<EvalAltResult>> {
2574 let position = accessibility_positive_index(position, "option position")?;
2575 let size = accessibility_positive_index(size, "option set size")?;
2576 Ok(node
2577 .clone()
2578 .with_attribute("role", UiValue::String("option".to_owned()))
2579 .with_attribute("label", UiValue::String(label.to_string()))
2580 .with_attribute("selected", UiValue::Bool(selected))
2581 .with_attribute("position_in_set", UiValue::Integer(position))
2582 .with_attribute("size_of_set", UiValue::Integer(size)))
2583 },
2584 )
2585 .with_fn(
2586 "accessibility_table_cell",
2587 |node: &mut UiNode,
2588 label: ImmutableString,
2589 column: INT|
2590 -> Result<UiNode, Box<EvalAltResult>> {
2591 let column = accessibility_positive_index(column, "table column index")?;
2592 Ok(node
2593 .clone()
2594 .with_attribute("role", UiValue::String("gridcell".to_owned()))
2595 .with_attribute("label", UiValue::String(label.to_string()))
2596 .with_attribute("column_index", UiValue::Integer(column)))
2597 },
2598 )
2599 .with_fn(
2600 "accessibility_column_header",
2601 |node: &mut UiNode,
2602 label: ImmutableString,
2603 column: INT|
2604 -> Result<UiNode, Box<EvalAltResult>> {
2605 let column = accessibility_positive_index(column, "table column index")?;
2606 Ok(node
2607 .clone()
2608 .with_attribute("role", UiValue::String("columnheader".to_owned()))
2609 .with_attribute("label", UiValue::String(label.to_string()))
2610 .with_attribute("column_index", UiValue::Integer(column)))
2611 },
2612 )
2613 .with_fn(
2614 "accessibility_table_row",
2615 |node: &mut UiNode,
2616 label: ImmutableString,
2617 selected: bool,
2618 row: INT,
2619 position: INT,
2620 size: INT|
2621 -> Result<UiNode, Box<EvalAltResult>> {
2622 let row = accessibility_positive_index(row, "table row index")?;
2623 let position = accessibility_positive_index(position, "row position")?;
2624 let size = accessibility_positive_index(size, "row set size")?;
2625 Ok(node
2626 .clone()
2627 .with_attribute("role", UiValue::String("row".to_owned()))
2628 .with_attribute("label", UiValue::String(label.to_string()))
2629 .with_attribute("selected", UiValue::Bool(selected))
2630 .with_attribute("row_index", UiValue::Integer(row))
2631 .with_attribute("position_in_set", UiValue::Integer(position))
2632 .with_attribute("size_of_set", UiValue::Integer(size)))
2633 },
2634 );
2635 for (method, attribute) in [
2636 ("accessibility_level", "level"),
2637 ("accessibility_position_in_set", "position_in_set"),
2638 ("accessibility_size_of_set", "size_of_set"),
2639 ("accessibility_row_index", "row_index"),
2640 ("accessibility_column_index", "column_index"),
2641 ("accessibility_row_count", "row_count"),
2642 ("accessibility_column_count", "column_count"),
2643 ] {
2644 let attribute = attribute.to_owned();
2645 builder.with_fn(
2646 method,
2647 move |node: &mut UiNode, value: INT| -> Result<UiNode, Box<EvalAltResult>> {
2648 if value < 1 {
2649 return Err(Box::new(EvalAltResult::ErrorRuntime(
2650 format!("{attribute} must be at least 1").into(),
2651 Position::NONE,
2652 )));
2653 }
2654 Ok(node
2655 .clone()
2656 .with_attribute(attribute.clone(), UiValue::Integer(value)))
2657 },
2658 );
2659 }
2660}
2661
2662fn accessibility_positive_index(value: INT, name: &str) -> Result<INT, Box<EvalAltResult>> {
2663 if value < 1 {
2664 Err(Box::new(EvalAltResult::ErrorRuntime(
2665 format!("accessibility {name} must be at least 1").into(),
2666 Position::NONE,
2667 )))
2668 } else {
2669 Ok(value)
2670 }
2671}
2672
2673fn register_accessibility_range_methods(builder: &mut TypeBuilder<UiNode>) {
2674 builder
2675 .with_fn(
2676 "accessibility_value_min",
2677 |node: &mut UiNode, value: Dynamic| -> Result<UiNode, Box<EvalAltResult>> {
2678 let value = accessibility_number(value, "minimum")?;
2679 Ok(node
2680 .clone()
2681 .with_attribute("value_min", UiValue::Float(value)))
2682 },
2683 )
2684 .with_fn(
2685 "accessibility_value_max",
2686 |node: &mut UiNode, value: Dynamic| -> Result<UiNode, Box<EvalAltResult>> {
2687 let value = accessibility_number(value, "maximum")?;
2688 Ok(node
2689 .clone()
2690 .with_attribute("value_max", UiValue::Float(value)))
2691 },
2692 );
2693}
2694
2695fn accessibility_number(value: Dynamic, name: &str) -> Result<f64, Box<EvalAltResult>> {
2696 let value = if value.is::<FLOAT>() {
2697 value.cast::<FLOAT>()
2698 } else if value.is::<INT>() {
2699 value
2700 .cast::<INT>()
2701 .to_string()
2702 .parse::<f64>()
2703 .map_err(|_| {
2704 Box::new(EvalAltResult::ErrorRuntime(
2705 format!("accessibility {name} value cannot be represented").into(),
2706 Position::NONE,
2707 ))
2708 })?
2709 } else {
2710 return Err(Box::new(EvalAltResult::ErrorRuntime(
2711 format!("accessibility {name} value must be a number").into(),
2712 Position::NONE,
2713 )));
2714 };
2715 if !value.is_finite() {
2716 return Err(Box::new(EvalAltResult::ErrorRuntime(
2717 format!("accessibility {name} value must be finite").into(),
2718 Position::NONE,
2719 )));
2720 }
2721 Ok(value)
2722}
2723
2724fn register_accessibility_reference_methods(builder: &mut TypeBuilder<UiNode>) {
2725 builder
2726 .with_fn(
2727 "accessibility_id",
2728 |node: &mut UiNode, id: ImmutableString| {
2729 node.clone()
2730 .with_attribute("semantic_id", UiValue::String(id.to_string()))
2731 },
2732 )
2733 .with_fn(
2734 "test_id",
2735 |node: &mut UiNode, id: ImmutableString| -> Result<UiNode, Box<EvalAltResult>> {
2736 let id = id.to_string();
2737 if id.is_empty()
2738 || id.len() > 128
2739 || !id.chars().all(|character| {
2740 character.is_ascii_alphanumeric()
2741 || matches!(character, '_' | '-' | '.' | ':')
2742 })
2743 {
2744 return Err(Box::new(EvalAltResult::ErrorRuntime(
2745 "test_id must be 1-128 safe identifier characters".into(),
2746 Position::NONE,
2747 )));
2748 }
2749 Ok(node.clone().with_attribute("test_id", UiValue::String(id)))
2750 },
2751 )
2752 .with_fn(
2753 "accessibility_labelled_by",
2754 |node: &mut UiNode, id: ImmutableString| {
2755 node.clone()
2756 .with_attribute("labelled_by", UiValue::String(id.to_string()))
2757 },
2758 )
2759 .with_fn(
2760 "accessibility_described_by",
2761 |node: &mut UiNode, ids: ImmutableString| {
2762 node.clone()
2763 .with_attribute("described_by", UiValue::String(ids.to_string()))
2764 },
2765 )
2766 .with_fn(
2767 "accessibility_required",
2768 |node: &mut UiNode, required: bool| {
2769 node.clone()
2770 .with_attribute("required", UiValue::Bool(required))
2771 },
2772 );
2773}
2774
2775fn dynamic_ui_value(value: Dynamic) -> Result<UiValue, Box<EvalAltResult>> {
2776 UiValue::from_dynamic(value).map_err(|error| {
2777 Box::new(EvalAltResult::ErrorRuntime(
2778 error.to_string().into(),
2779 Position::NONE,
2780 ))
2781 })
2782}
2783
2784pub(crate) fn text_node(call: NativeCallContext<'_>, text: ImmutableString) -> UiNode {
2785 with_call_source(UiNode::text(text), call)
2786}
2787
2788pub(crate) fn span_value(text: ImmutableString) -> Span {
2789 Span::new(text)
2790}
2791
2792pub(crate) fn rich_text_node(
2793 call: NativeCallContext<'_>,
2794 spans: Array,
2795) -> Result<UiNode, Box<EvalAltResult>> {
2796 let spans = spans
2797 .into_iter()
2798 .enumerate()
2799 .map(|(index, value)| {
2800 value.try_cast::<Span>().ok_or_else(|| {
2801 Box::new(EvalAltResult::ErrorRuntime(
2802 format!("text span {index} must be Span").into(),
2803 Position::NONE,
2804 ))
2805 })
2806 })
2807 .collect::<Result<Vec<_>, _>>()?;
2808 let text = spans.iter().map(Span::text).collect::<String>();
2809 let mut boundaries = text
2810 .grapheme_indices(true)
2811 .map(|(index, _)| index)
2812 .collect::<BTreeSet<_>>();
2813 boundaries.insert(text.len());
2814 let mut offset = 0usize;
2815 for (index, span) in spans.iter().enumerate() {
2816 offset = offset.saturating_add(span.text().len());
2817 if !boundaries.contains(&offset) {
2818 return Err(Box::new(EvalAltResult::ErrorRuntime(
2819 format!("text span {index} splits an extended grapheme cluster").into(),
2820 Position::NONE,
2821 )));
2822 }
2823 }
2824 Ok(with_call_source(UiNode::rich_text(spans), call))
2825}
2826
2827pub(crate) fn canvas_node(call: NativeCallContext<'_>, scene: crate::CanvasScene) -> UiNode {
2828 with_call_source(UiNode::canvas(scene), call)
2829}
2830
2831pub(crate) fn svg_node(
2832 call: NativeCallContext<'_>,
2833 source: &str,
2834) -> Result<UiNode, Box<EvalAltResult>> {
2835 UiNode::svg(source)
2836 .map(|node| with_call_source(node, call))
2837 .map_err(|error| {
2838 Box::new(EvalAltResult::ErrorRuntime(
2839 error.to_string().into(),
2840 Position::NONE,
2841 ))
2842 })
2843}
2844
2845pub(crate) fn box_node(
2846 call: NativeCallContext<'_>,
2847 children: Array,
2848) -> Result<UiNode, Box<rhai::EvalAltResult>> {
2849 collect_children(children, "box")
2850 .map(|children| with_call_source(UiNode::box_node(children), call))
2851}
2852
2853pub(crate) fn fragment_node(
2854 call: NativeCallContext<'_>,
2855 children: Array,
2856) -> Result<UiNode, Box<rhai::EvalAltResult>> {
2857 collect_children(children, "fragment")
2858 .map(|children| with_call_source(UiNode::fragment(children), call))
2859}
2860
2861#[allow(clippy::needless_pass_by_value)]
2862pub(crate) fn motion_group_node(
2863 call: NativeCallContext<'_>,
2864 id: ImmutableString,
2865 children: Array,
2866) -> Result<UiNode, Box<EvalAltResult>> {
2867 let id = id.to_string();
2868 if id.is_empty()
2869 || id.len() > 128
2870 || !id
2871 .chars()
2872 .all(|character| character.is_ascii_alphanumeric() || matches!(character, '_' | '-'))
2873 {
2874 return Err(Box::new(EvalAltResult::ErrorRuntime(
2875 "motion group id must be 1-128 safe ASCII characters".into(),
2876 Position::NONE,
2877 )));
2878 }
2879 let mut node = fragment_node(call, children)?;
2880 apply_motion_group(&mut node, &id);
2881 Ok(node)
2882}
2883
2884pub(crate) fn apply_motion_group(node: &mut UiNode, group: &str) {
2885 if node.component_root.is_some() {
2886 node.apply_presentation_mutation(NodePresentationMutation::MotionGroup(group.to_owned()));
2887 } else {
2888 apply_motion_group_contents(node, group);
2889 }
2890}
2891
2892fn apply_motion_group_contents(node: &mut UiNode, group: &str) {
2893 if node.attributes.contains_key("shared_layout_id")
2894 && !node.attributes.contains_key("shared_layout_group")
2895 {
2896 node.attributes.insert(
2897 "shared_layout_group".to_owned(),
2898 UiValue::String(group.to_owned()),
2899 );
2900 }
2901 match &mut node.kind {
2902 UiNodeKind::Box { children } | UiNodeKind::Fragment { children } => {
2903 for child in children {
2904 apply_motion_group(child, group);
2905 }
2906 }
2907 UiNodeKind::Overlay {
2908 trigger, content, ..
2909 } => {
2910 apply_motion_group(trigger, group);
2911 apply_motion_group(content, group);
2912 }
2913 UiNodeKind::Layer { content, .. } => apply_motion_group(content, group),
2914 UiNodeKind::ErrorBoundary { child, fallback } => {
2915 apply_motion_group(child, group);
2916 apply_motion_group(fallback, group);
2917 }
2918 UiNodeKind::VirtualCollection { spec } => {
2919 if spec.inherited_motion_group.is_none() {
2920 spec.inherited_motion_group = Some(group.to_owned());
2921 }
2922 for child in spec.realized.values_mut() {
2923 apply_motion_group(child, group);
2924 }
2925 }
2926 UiNodeKind::Text { .. }
2927 | UiNodeKind::RichText { .. }
2928 | UiNodeKind::Canvas { .. }
2929 | UiNodeKind::Svg { .. }
2930 | UiNodeKind::Custom { .. }
2931 | UiNodeKind::Image { .. }
2932 | UiNodeKind::DirectionalImage { .. } => {}
2933 }
2934}
2935
2936pub(crate) fn stack_node(
2937 call: NativeCallContext<'_>,
2938 children: Array,
2939) -> Result<UiNode, Box<rhai::EvalAltResult>> {
2940 collect_children(children, "stack").map(|children| {
2941 with_call_source(
2942 UiNode::box_node(children).with_style(&Style::new().relative()),
2943 call,
2944 )
2945 })
2946}
2947
2948pub(crate) fn column_node(
2949 call: NativeCallContext<'_>,
2950 children: Array,
2951) -> Result<UiNode, Box<rhai::EvalAltResult>> {
2952 collect_children(children, "column")
2953 .map(|children| with_call_source(UiNode::column(children), call))
2954}
2955
2956pub(crate) fn row_node(
2957 call: NativeCallContext<'_>,
2958 children: Array,
2959) -> Result<UiNode, Box<rhai::EvalAltResult>> {
2960 collect_children(children, "row").map(|children| with_call_source(UiNode::row(children), call))
2961}
2962
2963pub(crate) fn error_boundary_node(
2964 call: NativeCallContext<'_>,
2965 child: UiNode,
2966 fallback: UiNode,
2967) -> UiNode {
2968 with_call_source(UiNode::error_boundary(child, fallback), call)
2969}
2970
2971pub(crate) fn lazy_error_boundary_node(
2972 call: NativeCallContext<'_>,
2973 child: FnPtr,
2974 fallback: FnPtr,
2975) -> Result<UiNode, Box<EvalAltResult>> {
2976 let fallback_node = fallback.call_within_context::<UiNode>(&call, ())?;
2977 let child_result = child.call_within_context::<UiNode>(&call, ());
2978 drop(child);
2979 drop(fallback);
2980 match child_result {
2981 Ok(child) => Ok(with_call_source(
2982 UiNode::error_boundary(child, fallback_node),
2983 call,
2984 )),
2985 Err(error) => Ok(with_call_source(
2986 fallback_node.with_attribute("boundary_error", UiValue::String(error.to_string())),
2987 call,
2988 )),
2989 }
2990}
2991
2992pub(crate) fn image_node(call: NativeCallContext<'_>, handle: OpaqueHandle) -> UiNode {
2993 with_call_source(UiNode::image(handle), call)
2994}
2995
2996pub(crate) fn asset_image_node(call: NativeCallContext<'_>, asset: AssetId) -> UiNode {
2997 with_call_source(UiNode::asset_image(asset), call)
2998}
2999
3000pub(crate) fn generic_image_node(
3001 call: NativeCallContext<'_>,
3002 source: Dynamic,
3003) -> Result<UiNode, Box<EvalAltResult>> {
3004 Ok(with_call_source(
3005 UiNode::image_source(parse_image_source(source)?),
3006 call,
3007 ))
3008}
3009
3010pub(crate) fn directional_image_node(
3011 call: NativeCallContext<'_>,
3012 left_to_right: OpaqueHandle,
3013 right_to_left: OpaqueHandle,
3014) -> UiNode {
3015 with_call_source(
3016 UiNode::directional_image(left_to_right, right_to_left),
3017 call,
3018 )
3019}
3020
3021pub(crate) fn directional_asset_image_node(
3022 call: NativeCallContext<'_>,
3023 left_to_right: AssetId,
3024 right_to_left: AssetId,
3025) -> UiNode {
3026 with_call_source(
3027 UiNode::directional_asset_image(left_to_right, right_to_left),
3028 call,
3029 )
3030}
3031
3032pub(crate) fn generic_directional_image_node(
3033 call: NativeCallContext<'_>,
3034 left_to_right: Dynamic,
3035 right_to_left: Dynamic,
3036) -> Result<UiNode, Box<EvalAltResult>> {
3037 Ok(with_call_source(
3038 UiNode::directional_image_sources(
3039 parse_image_source(left_to_right)?,
3040 parse_image_source(right_to_left)?,
3041 ),
3042 call,
3043 ))
3044}
3045
3046fn parse_image_source(source: Dynamic) -> Result<ImageSourceSpec, Box<EvalAltResult>> {
3047 if source.is::<AssetId>() {
3048 Ok(ImageSourceSpec::Asset(source.cast::<AssetId>()))
3049 } else if source.is::<OpaqueHandle>() {
3050 let handle = source.cast::<OpaqueHandle>();
3051 if handle.kind() == "image" {
3052 Ok(ImageSourceSpec::Handle(handle))
3053 } else {
3054 Err(Box::new(EvalAltResult::ErrorRuntime(
3055 format!(
3056 "image source handle must have kind `image`, got `{}`",
3057 handle.kind()
3058 )
3059 .into(),
3060 Position::NONE,
3061 )))
3062 }
3063 } else {
3064 Err(Box::new(EvalAltResult::ErrorRuntime(
3065 format!(
3066 "image source must be an AssetId or image handle, got {}",
3067 source.type_name()
3068 )
3069 .into(),
3070 Position::NONE,
3071 )))
3072 }
3073}
3074
3075pub(crate) fn overlay_node(
3076 call: NativeCallContext<'_>,
3077 trigger: UiNode,
3078 overlay_content: UiNode,
3079 mut config: Map,
3080) -> Result<UiNode, Box<EvalAltResult>> {
3081 let id = required_string(&mut config, "id")?;
3082 if id.trim().is_empty() {
3083 return overlay_config_error("overlay ID cannot be empty");
3084 }
3085 let parent = optional_string(&mut config, "parent")?;
3086 let kind = match optional_string(&mut config, "kind")?.as_deref() {
3087 None | Some("popover") => OverlayKind::Popover,
3088 Some("combobox") => OverlayKind::Combobox,
3089 Some("tooltip") => OverlayKind::Tooltip,
3090 Some("dialog") => OverlayKind::Dialog,
3091 Some("sheet") => OverlayKind::Sheet,
3092 Some("menu") => OverlayKind::Menu,
3093 Some(other) => return overlay_config_error(format!("unknown overlay kind `{other}`")),
3094 };
3095 let placement = match optional_string(&mut config, "placement")?.as_deref() {
3096 None | Some("bottom") => OverlayPlacement::Bottom,
3097 Some("top") => OverlayPlacement::Top,
3098 Some("left") => OverlayPlacement::Left,
3099 Some("right") => OverlayPlacement::Right,
3100 Some("start") => OverlayPlacement::Start,
3101 Some("end") => OverlayPlacement::End,
3102 Some("center") => OverlayPlacement::Center,
3103 Some(other) => {
3104 return overlay_config_error(format!("unknown overlay placement `{other}`"));
3105 }
3106 };
3107 let open = optional_bool(&mut config, "open")?.unwrap_or(false);
3108 let anchor = optional_overlay_bounds(&mut config, "anchor")?;
3109 let gap = optional_number(&mut config, "gap")?.unwrap_or(8.0);
3110 if !gap.is_finite() || gap < 0.0 {
3111 return overlay_config_error("overlay gap must be finite and non-negative");
3112 }
3113 let modal = optional_bool(&mut config, "modal")?
3114 .unwrap_or(matches!(kind, OverlayKind::Dialog | OverlayKind::Sheet));
3115 let activate_on_trigger = optional_bool(&mut config, "activate_on_trigger")?.unwrap_or(true);
3116 let width_policy = if optional_bool(&mut config, "match_trigger_width")?.unwrap_or(false) {
3117 OverlayWidthPolicy::MatchTrigger
3118 } else {
3119 OverlayWidthPolicy::Content
3120 };
3121 let initial_focus = match optional_string(&mut config, "initial_focus")?.as_deref() {
3122 None | Some("panel") => OverlayInitialFocus::Panel,
3123 Some("first") => OverlayInitialFocus::First,
3124 Some(other) => {
3125 return overlay_config_error(format!(
3126 "unknown overlay initial_focus `{other}` (expected `panel` or `first`)"
3127 ));
3128 }
3129 };
3130 let dismiss_on_escape = optional_bool(&mut config, "dismiss_on_escape")?.unwrap_or(true);
3131 let dismiss_on_outside = optional_bool(&mut config, "dismiss_on_outside")?.unwrap_or(true);
3132 let show_delay_ms = optional_usize(&mut config, "show_delay_ms")?;
3133 let hide_delay_ms = optional_usize(&mut config, "hide_delay_ms")?;
3134 let tooltip_delays = (kind == OverlayKind::Tooltip).then(|| TooltipDelays {
3135 show_ms: u64::try_from(show_delay_ms.unwrap_or(500)).unwrap_or(u64::MAX),
3136 hide_ms: u64::try_from(hide_delay_ms.unwrap_or(100)).unwrap_or(u64::MAX),
3137 });
3138 if let Some((unknown, _)) = config.into_iter().next() {
3139 return overlay_config_error(format!("unknown overlay config field `{unknown}`"));
3140 }
3141 Ok(with_call_source(
3142 UiNode::overlay(
3143 trigger,
3144 overlay_content,
3145 OverlayNodeSpec {
3146 id: OverlayId::new(id),
3147 parent: parent.map(OverlayId::new),
3148 kind,
3149 placement,
3150 anchor,
3151 open,
3152 gap,
3153 modal,
3154 dismiss: OverlayDismissPolicy {
3155 escape: dismiss_on_escape,
3156 outside: dismiss_on_outside,
3157 },
3158 tooltip_delays,
3159 initial_focus,
3160 activate_on_trigger,
3161 width_policy,
3162 },
3163 ),
3164 call,
3165 ))
3166}
3167
3168pub(crate) fn layer_node(
3169 call: NativeCallContext<'_>,
3170 content: UiNode,
3171 mut config: Map,
3172) -> Result<UiNode, Box<EvalAltResult>> {
3173 let id = required_string(&mut config, "id")?;
3174 if id.trim().is_empty() {
3175 return overlay_config_error("layer ID cannot be empty");
3176 }
3177 let placement = match optional_string(&mut config, "placement")?.as_deref() {
3178 None | Some("top_right") => LayerPlacement::TopRight,
3179 Some("top_left") => LayerPlacement::TopLeft,
3180 Some("bottom_left") => LayerPlacement::BottomLeft,
3181 Some("bottom_right") => LayerPlacement::BottomRight,
3182 Some("center") => LayerPlacement::Center,
3183 Some("fill") => LayerPlacement::Fill,
3184 Some(other) => return overlay_config_error(format!("unknown layer placement `{other}`")),
3185 };
3186 let inset = optional_number(&mut config, "inset")?.unwrap_or(12.0);
3187 if !inset.is_finite() || inset < 0.0 {
3188 return overlay_config_error("layer inset must be finite and non-negative");
3189 }
3190 let priority = optional_usize(&mut config, "priority")?.unwrap_or(9_000);
3191 if priority > 1_000_000 {
3192 return overlay_config_error("layer priority cannot exceed 1000000");
3193 }
3194 if let Some((unknown, _)) = config.into_iter().next() {
3195 return overlay_config_error(format!("unknown layer config field `{unknown}`"));
3196 }
3197 Ok(with_call_source(
3198 UiNode::layer(
3199 content,
3200 LayerNodeSpec {
3201 id: OverlayId::new(id),
3202 placement,
3203 inset,
3204 priority,
3205 },
3206 ),
3207 call,
3208 ))
3209}
3210
3211fn with_call_source(node: UiNode, call: NativeCallContext<'_>) -> UiNode {
3212 let position = call.call_position();
3213 let module = call
3214 .call_source()
3215 .or_else(|| call.fn_source())
3216 .map(ToOwned::to_owned);
3217 let _ = std::hint::black_box(call);
3218 let Some(module) = module else {
3219 return node;
3220 };
3221 let (Some(line), Some(column)) = (position.line(), position.position()) else {
3222 return node;
3223 };
3224 let (Ok(line), Ok(column)) = (u32::try_from(line), u32::try_from(column)) else {
3225 return node;
3226 };
3227 node.with_source(SourceLocation {
3228 module,
3229 line,
3230 column,
3231 })
3232}
3233
3234fn required_string(config: &mut Map, name: &str) -> Result<String, Box<EvalAltResult>> {
3235 optional_string(config, name)?.ok_or_else(|| {
3236 Box::new(EvalAltResult::ErrorRuntime(
3237 format!("overlay config field `{name}` is required").into(),
3238 Position::NONE,
3239 ))
3240 })
3241}
3242
3243fn optional_string(config: &mut Map, name: &str) -> Result<Option<String>, Box<EvalAltResult>> {
3244 let Some(value) = config.remove(name) else {
3245 return Ok(None);
3246 };
3247 value
3248 .try_cast::<ImmutableString>()
3249 .map(|value| Some(value.to_string()))
3250 .ok_or_else(|| Box::new(overlay_type_error(name, "string")))
3251}
3252
3253fn optional_bool(config: &mut Map, name: &str) -> Result<Option<bool>, Box<EvalAltResult>> {
3254 let Some(value) = config.remove(name) else {
3255 return Ok(None);
3256 };
3257 value
3258 .try_cast::<bool>()
3259 .map(Some)
3260 .ok_or_else(|| Box::new(overlay_type_error(name, "bool")))
3261}
3262
3263fn optional_number(config: &mut Map, name: &str) -> Result<Option<f64>, Box<EvalAltResult>> {
3264 let Some(value) = config.remove(name) else {
3265 return Ok(None);
3266 };
3267 if value.is::<INT>() {
3268 value
3269 .cast::<INT>()
3270 .to_string()
3271 .parse::<f64>()
3272 .map(Some)
3273 .map_err(|_| Box::new(overlay_type_error(name, "number")))
3274 } else if value.is::<FLOAT>() {
3275 Ok(Some(value.cast::<FLOAT>()))
3276 } else {
3277 Err(Box::new(overlay_type_error(name, "number")))
3278 }
3279}
3280
3281fn optional_usize(config: &mut Map, name: &str) -> Result<Option<usize>, Box<EvalAltResult>> {
3282 let Some(value) = config.remove(name) else {
3283 return Ok(None);
3284 };
3285 value
3286 .try_cast::<INT>()
3287 .and_then(|value| usize::try_from(value).ok())
3288 .map(Some)
3289 .ok_or_else(|| Box::new(overlay_type_error(name, "a non-negative integer")))
3290}
3291
3292fn optional_overlay_bounds(
3293 config: &mut Map,
3294 name: &str,
3295) -> Result<Option<crate::OverlayBounds>, Box<EvalAltResult>> {
3296 let Some(value) = config.remove(name) else {
3297 return Ok(None);
3298 };
3299 let Some(mut bounds) = value.try_cast::<Map>() else {
3300 return Err(Box::new(overlay_type_error(
3301 name,
3302 "a map with x, y, width, and height",
3303 )));
3304 };
3305 let x = optional_number(&mut bounds, "x")?
3306 .ok_or_else(|| Box::new(overlay_type_error("anchor.x", "number")))?;
3307 let y = optional_number(&mut bounds, "y")?
3308 .ok_or_else(|| Box::new(overlay_type_error("anchor.y", "number")))?;
3309 let width = optional_number(&mut bounds, "width")?.unwrap_or(0.0);
3310 let height = optional_number(&mut bounds, "height")?.unwrap_or(0.0);
3311 if let Some((unknown, _)) = bounds.into_iter().next() {
3312 return overlay_config_error(format!("unknown overlay anchor field `{unknown}`"));
3313 }
3314 if !x.is_finite()
3315 || !y.is_finite()
3316 || !width.is_finite()
3317 || width < 0.0
3318 || !height.is_finite()
3319 || height < 0.0
3320 {
3321 return overlay_config_error("overlay anchor geometry must be finite and non-negative");
3322 }
3323 Ok(Some(crate::OverlayBounds {
3324 x,
3325 y,
3326 width,
3327 height,
3328 }))
3329}
3330
3331fn overlay_type_error(name: &str, expected: &str) -> EvalAltResult {
3332 EvalAltResult::ErrorRuntime(
3333 format!("overlay config field `{name}` must be {expected}").into(),
3334 Position::NONE,
3335 )
3336}
3337
3338fn overlay_config_error<T>(message: impl Into<String>) -> Result<T, Box<EvalAltResult>> {
3339 Err(Box::new(EvalAltResult::ErrorRuntime(
3340 message.into().into(),
3341 Position::NONE,
3342 )))
3343}
3344
3345fn collect_children(
3346 children: Array,
3347 constructor: &str,
3348) -> Result<Vec<UiNode>, Box<rhai::EvalAltResult>> {
3349 let mut nodes = Vec::with_capacity(children.len());
3350 for (index, child) in children.into_iter().enumerate() {
3351 match child.try_cast::<UiNode>() {
3352 Some(node) => nodes.push(node),
3353 None => {
3354 return Err(format!("{constructor} child at index {index} is not a UiNode").into());
3355 }
3356 }
3357 }
3358 Ok(nodes)
3359}
3360
3361#[cfg(test)]
3362mod tests {
3363 use super::*;
3364 use crate::{
3365 ColorValue, EventPropagation, Length, MotionProperty, MotionTransition, Rgba8, SignalId,
3366 SignalKind, SignalProperty,
3367 };
3368
3369 #[test]
3370 fn node_styles_share_immutable_snapshots_and_mutate_without_aliasing() {
3371 let empty = UiNode::text("empty");
3372 assert!(Rc::ptr_eq(&empty.style, &UiNode::text("other").style));
3373 let original = empty.with_style(
3374 &Style::new()
3375 .opacity(0.25)
3376 .unwrap()
3377 .inset_start(crate::SignedLength::pixels(-4.0).unwrap()),
3378 );
3379 let shared = original.clone();
3380 assert!(Rc::ptr_eq(&original.style, &shared.style));
3381 let changed = shared.with_style(
3382 &Style::new()
3383 .opacity(0.75)
3384 .unwrap()
3385 .inset_end(Length::Pixels(2.0)),
3386 );
3387 assert_eq!(original.style().base.opacity, Some(0.25));
3388 assert!(original.style().base.inset_end.is_none());
3389 assert_eq!(changed.style().base.opacity, Some(0.75));
3390 let ghost = changed.motion_ghost().unwrap();
3391 assert!(ghost.style().base.inset_start.is_none());
3392 assert!(ghost.style().base.inset_end.is_none());
3393 assert!(changed.style().base.inset_start.is_some());
3394 assert!(changed.style().base.inset_end.is_some());
3395 println!(
3396 "UiNode={} bytes; Style={} bytes",
3397 std::mem::size_of::<UiNode>(),
3398 std::mem::size_of::<Style>()
3399 );
3400 assert!(
3401 std::mem::size_of::<UiNode>() <= 2048,
3402 "node stack values must not inline a multi-kilobyte Style"
3403 );
3404 }
3405
3406 #[test]
3407 fn node_identity_style_source_and_attributes_are_runtime_owned() {
3408 let node = UiNode::text("hello")
3409 .with_key("greeting")
3410 .with_style(
3411 &Style::new()
3412 .width(Length::pixels(100.0).unwrap())
3413 .text_color(ColorValue::Literal(Rgba8::from_rgb_hex(0x00ff_ffff))),
3414 )
3415 .with_source(SourceLocation {
3416 module: "ui/main.rhai".to_owned(),
3417 line: 4,
3418 column: 9,
3419 })
3420 .with_attribute("role", UiValue::String("label".to_owned()));
3421
3422 assert_eq!(node.key().map(NodeKey::as_str), Some("greeting"));
3423 assert_eq!(node.source().unwrap().line, 4);
3424 assert_eq!(
3425 node.attributes().get("role"),
3426 Some(&UiValue::String("label".to_owned()))
3427 );
3428 }
3429
3430 #[test]
3431 fn selectable_rejects_non_text_nodes_at_the_script_boundary() {
3432 let mut runtime = crate::RuntimeEngine::new();
3433 let compiled = runtime
3434 .compile_named("selectable.rhai", "fn view() { row([]).selectable(true) }")
3435 .unwrap();
3436 let error = runtime.render(&compiled).unwrap_err();
3437 assert!(
3438 error
3439 .to_string()
3440 .contains("selectable(true) is supported only on text() nodes"),
3441 "{error}"
3442 );
3443 }
3444
3445 #[test]
3446 fn component_replacement_preserves_every_external_presentation_mutation() {
3447 let component = ComponentInstancePath::root("Probe", "owned");
3448 let signal = crate::NativeSignal::new(
3449 SignalId::new(component.clone(), "opacity", SignalKind::Float).unwrap(),
3450 );
3451 let reference =
3452 crate::ElementRef::new(crate::ElementRefId::new(component.clone(), "target").unwrap());
3453 let style = Style::new().width(Length::pixels(120.0).unwrap());
3454 let part_style = Style::new().height(Length::pixels(24.0).unwrap());
3455 let animation = MotionSource::Transition(MotionTransition::new(
3456 MotionProperty::Opacity,
3457 0.0,
3458 1.0,
3459 100,
3460 ));
3461 let mut presented = UiNode::text("old")
3462 .with_key("owned")
3463 .with_component_root(component.clone())
3464 .with_key("presented")
3465 .with_style(&style)
3466 .with_part_style("label", part_style.clone())
3467 .with_signal_binding(SignalProperty::Opacity, signal.clone())
3468 .unwrap()
3469 .with_element_ref(reference.clone())
3470 .with_attribute("role", UiValue::String("status".to_owned()))
3471 .with_host_handler(
3472 "click",
3473 crate::HostCallback::new("outer.click", |_, _, _| EventPropagation::Handled),
3474 )
3475 .with_handler_payload("click", UiValue::Integer(7))
3476 .with_motion(animation.clone());
3477 assert_eq!(presented.presentation.len(), 9);
3478
3479 let replacement = UiNode::text("new")
3480 .with_key("replacement")
3481 .with_component_root(component.clone());
3482 assert!(presented.replace_component_subtree(&component, replacement));
3483 assert!(matches!(presented.kind(), UiNodeKind::Text { text } if text == "new"));
3484 assert_eq!(presented.key().map(NodeKey::as_str), Some("presented"));
3485 assert_eq!(presented.style(), &style);
3486 assert_eq!(presented.part_style("label"), Some(&part_style));
3487 assert_eq!(
3488 presented.signal_bindings().collect::<Vec<_>>(),
3489 vec![(SignalProperty::Opacity, &signal)]
3490 );
3491 assert_eq!(presented.element_ref(), Some(&reference));
3492 assert_eq!(
3493 presented.attributes().get("role"),
3494 Some(&UiValue::String("status".to_owned()))
3495 );
3496 assert_eq!(presented.event_handlers("click").len(), 1);
3497 assert_eq!(
3498 presented.handler_payload("click"),
3499 Some(&UiValue::Integer(7))
3500 );
3501 assert_eq!(presented.motions(), &[animation]);
3502 assert_eq!(presented.presentation.len(), 9);
3503 }
3504
3505 #[test]
3506 fn component_snapshot_activates_lazily_and_stays_free_of_outer_presentation() {
3507 let component = ComponentInstancePath::root("Probe", "lazy");
3508 let snapshot = ComponentOwnedSnapshot::default();
3509 let node = UiNode::text("initial")
3510 .with_component_root_snapshot(component.clone(), snapshot.reference());
3511 assert!(snapshot.current().is_none());
3512
3513 let mut presented = node.with_style(&Style::new().opacity(0.5).unwrap());
3514 let owned = snapshot
3515 .current()
3516 .expect("first outer mutation activates snapshot");
3517 assert_eq!(Rc::strong_count(&snapshot.0), 1, "node link must stay weak");
3518 assert!(matches!(owned.kind(), UiNodeKind::Text { text } if text == "initial"));
3519 assert_eq!(owned.style().base.opacity, None);
3520
3521 let replacement =
3522 UiNode::text("updated").with_component_root_snapshot(component, snapshot.reference());
3523 snapshot.update_if_active(&replacement);
3524 presented.hydrate_component_subtrees();
3525 assert!(matches!(presented.kind(), UiNodeKind::Text { text } if text == "updated"));
3526 assert_eq!(presented.style().base.opacity, Some(0.5));
3527 }
3528
3529 #[test]
3530 fn inherited_motion_group_replays_across_nested_component_replacement() {
3531 let outer = ComponentInstancePath::root("Outer", "root");
3532 let inner = outer.child("Inner", "child");
3533 let inner_node = UiNode::text("initial")
3534 .with_shared_layout_id("item")
3535 .with_component_root(inner.clone());
3536 let mut root = UiNode::column(vec![inner_node]).with_component_root(outer);
3537
3538 apply_motion_group(&mut root, "outer-group");
3539 let UiNodeKind::Box { children } = root.kind() else {
3540 panic!("expected component column");
3541 };
3542 assert_eq!(
3543 children[0].attributes().get("shared_layout_group"),
3544 Some(&UiValue::String("outer-group".to_owned()))
3545 );
3546
3547 let replacement = UiNode::text("updated")
3548 .with_shared_layout_id("item")
3549 .with_component_root(inner.clone());
3550 assert!(root.replace_component_subtree(&inner, replacement));
3551 let UiNodeKind::Box { children } = root.kind() else {
3552 panic!("expected component column");
3553 };
3554 assert_eq!(
3555 children[0].attributes().get("shared_layout_group"),
3556 Some(&UiValue::String("outer-group".to_owned()))
3557 );
3558 assert!(matches!(children[0].kind(), UiNodeKind::Text { text } if text == "updated"));
3559 }
3560
3561 fn find_realized<'a>(
3562 node: &'a UiNode,
3563 id: &crate::VirtualCollectionId,
3564 ) -> Option<&'a BTreeMap<usize, UiNode>> {
3565 node.virtual_collection_items(id)
3566 }
3567
3568 #[test]
3569 fn realized_items_move_into_nested_collections_without_copies() {
3570 let id = crate::VirtualCollectionId {
3575 component: crate::ComponentInstancePath::root("Table", "readers"),
3576 key: "rows".to_owned(),
3577 };
3578 let collection = || {
3579 UiNode::virtual_collection(crate::VirtualCollectionNodeSpec {
3580 id: id.clone(),
3581 label: "Rows".to_owned(),
3582 data: Vec::<UiValue>::new().into(),
3583 realized: BTreeMap::new(),
3584 estimated_height: 30.0,
3585 height: Some(600.0),
3586 overdraw_pixels: 120.0,
3587 bottom_align: false,
3588 follow_tail: false,
3589 reveal_key: None,
3590 sticky_headers: std::sync::Arc::new(std::collections::BTreeSet::new()),
3591 inherited_motion_group: None,
3592 })
3593 };
3594 let siblings = || {
3595 (0..8)
3596 .map(|index| UiNode::text(format!("sibling {index}")))
3597 .collect::<Vec<_>>()
3598 };
3599 let nested = |inner: UiNode| {
3600 let mut children = siblings();
3601 children.push(UiNode::box_node(vec![UiNode::fragment(vec![inner])]));
3602 UiNode::column(children)
3603 };
3604 let layouts = vec![
3605 nested(collection()),
3606 nested(UiNode::error_boundary(UiNode::text("child"), collection())),
3607 UiNode::row(vec![
3608 nested(UiNode::text("no collection here")),
3609 nested(collection()),
3610 ]),
3611 {
3612 let mut outer = collection();
3613 let UiNodeKind::VirtualCollection { spec } = &mut outer.kind else {
3614 unreachable!()
3615 };
3616 spec.id.key = "outer".to_owned();
3617 spec.realized.insert(0, collection());
3618 outer
3619 },
3620 UiNode::custom(PrimitiveNode {
3621 primitive: crate::PrimitiveId::parse("test.slot").unwrap(),
3622 key: None,
3623 props: crate::PrimitiveProps::new().with(
3624 "content",
3625 crate::PrimitiveValue::Node(Box::new(collection())),
3626 ),
3627 }),
3628 UiNode::custom(PrimitiveNode {
3629 primitive: crate::PrimitiveId::parse("test.slots").unwrap(),
3630 key: None,
3631 props: crate::PrimitiveProps::new().with(
3632 "content",
3633 crate::PrimitiveValue::Nodes(vec![UiNode::text("prefix"), collection()]),
3634 ),
3635 }),
3636 ];
3637 for (index, mut root) in layouts.into_iter().enumerate() {
3638 let item = UiNode::text("row")
3639 .with_attribute("probe", UiValue::String(format!("row payload {index}")));
3640 let Some(UiValue::String(payload)) = item.attributes().get("probe") else {
3642 panic!("layout {index}: probe attribute missing before replacement");
3643 };
3644 let payload_ptr = payload.as_ptr();
3645 assert!(root.replace_virtual_collection_items(&id, BTreeMap::from([(7, item)])));
3646 let realized = find_realized(&root, &id).expect("collection keeps the realized items");
3647 let Some(UiValue::String(landed)) = realized[&7].attributes().get("probe") else {
3648 panic!("layout {index}: realized item lost its probe attribute");
3649 };
3650 assert_eq!(
3651 landed.as_ptr(),
3652 payload_ptr,
3653 "layout {index}: realized items were copied"
3654 );
3655 }
3656 }
3657
3658 #[test]
3659 fn key_handler_names_accept_punctuation_keys() {
3660 for script in [
3664 "fn h(ctx, payload) { () } fn view() { text(\"x\").on_key_value(\"?\", Fn(\"h\"), ()) }",
3665 "fn h(ctx, payload) { () } fn view() { text(\"x\").on_key_value(\"[\", Fn(\"h\"), ()) }",
3666 "fn h(ctx, event) { () } fn view() { text(\"x\").on(\"key:/\", Fn(\"h\")) }",
3667 "fn h(ctx, event) { () } fn view() { text(\"x\").on(\"key:-\", Fn(\"h\")) }",
3668 "fn h(ctx, payload) { () } fn view() { text(\"x\").on_key_value(\"escape\", Fn(\"h\"), ()) }",
3669 ] {
3670 let mut runtime = crate::RuntimeEngine::new();
3671 let compiled = runtime.compile_named("keys.rhai", script).unwrap();
3672 runtime.render(&compiled).unwrap_or_else(|error| {
3673 panic!("script should register its key handler: {script}\n{error}")
3674 });
3675 }
3676 }
3677
3678 #[test]
3679 fn key_handler_names_reject_invalid_key_names() {
3680 for script in [
3681 "fn h(ctx, payload) { () } fn view() { text(\"x\").on_key_value(\"\", Fn(\"h\"), ()) }",
3682 "fn h(ctx, payload) { () } fn view() { text(\"x\").on_key_value(\"a b\", Fn(\"h\"), ()) }",
3683 "fn h(ctx, payload) { () } fn view() { text(\"x\").on_key_value(\"héllo\", Fn(\"h\"), ()) }",
3684 "fn h(ctx, payload) { () } fn view() { text(\"x\").on_key_value(\"a:b\", Fn(\"h\"), ()) }",
3685 "fn h(ctx, event) { () } fn view() { text(\"x\").on(\"key:\", Fn(\"h\")) }",
3686 "fn h(ctx, event) { () } fn view() { text(\"x\").on(\"key:a b\", Fn(\"h\")) }",
3687 ] {
3688 let mut runtime = crate::RuntimeEngine::new();
3689 let compiled = runtime.compile_named("keys.rhai", script).unwrap();
3690 assert!(
3691 runtime.render(&compiled).is_err(),
3692 "script should be rejected: {script}"
3693 );
3694 }
3695 }
3696
3697 fn key_registration_script(method: &str, key: &str) -> String {
3698 let name = if method == "on_key_value" {
3699 key.to_owned()
3700 } else {
3701 format!("key:{key}")
3702 };
3703 let name = serde_json::to_string(&name).unwrap();
3704 let payload = if method == "on_key_value" { ", 7" } else { "" };
3705 format!(
3706 "fn h(ctx,p){{()}} fn view(){{text(\"target\").{method}({name},Fn(\"h\"){payload})}}"
3707 )
3708 }
3709
3710 #[test]
3711 fn all_rhai_key_entries_share_normalization_and_exact_limits() {
3712 for (method, phase) in [
3713 ("on_key_value", crate::EventPhase::Target),
3714 ("on", crate::EventPhase::Target),
3715 ("on_capture", crate::EventPhase::Capture),
3716 ("on_bubble", crate::EventPhase::Bubble),
3717 ] {
3718 let long = "A".repeat(60);
3719 for key in [
3720 "Escape", "A", "1", "page_up", "?", "/", "[", "]", "-", "=", &long,
3721 ] {
3722 let mut runtime = crate::RuntimeEngine::new();
3723 let compiled = runtime
3724 .compile_named("key_contract.rhai", &key_registration_script(method, key))
3725 .unwrap();
3726 let node = runtime.render(&compiled).unwrap();
3727 let event = format!("key:{}", key.to_ascii_lowercase());
3728 assert_eq!(node.handlers().len(), 1, "{method}, {key:?}");
3729 assert_eq!(node.event_handlers(&event)[0].phase(), phase);
3730 if method == "on_key_value" {
3731 assert_eq!(node.handler_payload(&event), Some(&UiValue::Integer(7)));
3732 }
3733 }
3734 let long = "a".repeat(61);
3735 for key in [
3736 "", " ", " escape", "escape ", "\t", "\n", "\0", ":", "a:b", "中文", "é", "cmd-s",
3737 "shift-/", "??", "[]", &long,
3738 ] {
3739 let mut runtime = crate::RuntimeEngine::new();
3740 let compiled = runtime
3741 .compile_named("key_contract.rhai", &key_registration_script(method, key))
3742 .unwrap();
3743 assert!(
3744 runtime.render(&compiled).is_err(),
3745 "{method} accepted {key:?}"
3746 );
3747 }
3748 }
3749 }
3750
3751 #[test]
3752 fn key_normalization_does_not_change_ordinary_event_namespaces() {
3753 for event in ["click", "value_changed", "custom:event", ":", "::"] {
3754 assert_eq!(normalize_node_event_name(event).unwrap(), event);
3755 }
3756 for event in [
3757 "",
3758 "Click",
3759 "custom:Event",
3760 "Key:Escape",
3761 "custom-event",
3762 "custom:?",
3763 "a b",
3764 ] {
3765 assert!(
3766 normalize_node_event_name(event).is_err(),
3767 "accepted {event:?}"
3768 );
3769 }
3770 assert!(normalize_node_event_name(&"a".repeat(64)).is_ok());
3771 assert!(normalize_node_event_name(&"a".repeat(65)).is_err());
3772 }
3773}