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