Skip to main content

fission_core/ui/
node.rs

1use super::custom_render::CustomRenderObject;
2use super::traits::{InternalLower, InternalLowerer};
3#[cfg(feature = "interactive-canvas")]
4use super::widgets::InteractiveViewer;
5use super::widgets::{
6    ActionScope, Align, Button, Checkbox, Clip, Column, Composite, Container, ContextMenuEntry,
7    ContextMenuRegion, FocusScope, GestureDetector, Grid, GridItem, Icon, Image, LazyColumn,
8    Overlay, Positioned, Pressable, Radio, Responsive, RichText, Row, SafeArea, Scroll,
9    SemanticsRegion, Slider, Spacer, Switch, Text, TextInput, Transform, Video, ZStack,
10};
11use crate::lowering::InternalLoweringCx;
12use fission_ir::{Op, StructuralOp, WidgetId};
13use serde::{Deserialize, Serialize};
14use std::ops::ControlFlow;
15use std::sync::Arc;
16
17#[derive(Clone, Debug, Serialize, Deserialize)]
18pub struct Widget {
19    kind: Box<WidgetKind>,
20}
21
22#[derive(Clone, Debug, Serialize, Deserialize)]
23pub enum WidgetKind {
24    Identified {
25        id: WidgetId,
26        child: Widget,
27    },
28    ActionScope(ActionScope),
29    Row(Row),
30    Column(Column),
31    Align(Align),
32    FocusScope(FocusScope),
33    Clip(Clip),
34    Text(Text),
35    RichText(RichText),
36    Transform(Transform),
37    #[cfg(feature = "interactive-canvas")]
38    InteractiveViewer(InteractiveViewer),
39    Button(Button),
40    Pressable(Pressable),
41    TextInput(TextInput),
42    Scroll(Scroll),
43    SemanticsRegion(SemanticsRegion),
44    Image(Image),
45    Video(Video),
46    ZStack(ZStack),
47    Overlay(Overlay),
48    Container(Container),
49    ContextMenuRegion(ContextMenuRegion),
50    GestureDetector(GestureDetector),
51    Grid(Grid),
52    GridItem(GridItem),
53    Responsive(Responsive),
54    Checkbox(Checkbox),
55    Switch(Switch),
56    Radio(Radio),
57    SafeArea(SafeArea),
58    Positioned(Positioned),
59    Spacer(Spacer),
60    Slider(Slider),
61    LazyColumn(LazyColumn),
62    Icon(Icon),
63    Composite(Composite),
64    Custom(InternalRenderNode),
65}
66
67impl Widget {
68    const CHILD_ROLE: u32 = 0xF155_2000;
69
70    /// Returns the concrete kind represented by this type-erased widget.
71    pub fn kind(&self) -> &WidgetKind {
72        self.kind.as_ref()
73    }
74
75    /// Visits this widget and every descendant in depth-first order.
76    ///
77    /// Returning [`ControlFlow::Break`] stops traversal immediately. The
78    /// visitor is read-only; callers that need to transform a tree should
79    /// construct a new widget tree through the normal authoring API.
80    pub fn visit(&self, visitor: &mut impl FnMut(&Widget) -> ControlFlow<()>) -> ControlFlow<()> {
81        visitor(self)?;
82        match self.kind.as_ref() {
83            WidgetKind::Identified { child, .. }
84            | WidgetKind::ActionScope(ActionScope { child, .. })
85            | WidgetKind::Align(Align { child, .. })
86            | WidgetKind::Clip(Clip { child, .. })
87            | WidgetKind::Transform(Transform { child, .. })
88            | WidgetKind::Pressable(Pressable { child, .. })
89            | WidgetKind::GestureDetector(GestureDetector { child, .. })
90            | WidgetKind::GridItem(GridItem { child, .. })
91            | WidgetKind::SafeArea(SafeArea { child, .. })
92            | WidgetKind::Composite(Composite { child, .. }) => child.visit(visitor),
93            WidgetKind::Row(Row { children, .. })
94            | WidgetKind::Column(Column { children, .. })
95            | WidgetKind::FocusScope(FocusScope { children, .. })
96            | WidgetKind::ZStack(ZStack { children, .. })
97            | WidgetKind::Grid(Grid { children, .. })
98            | WidgetKind::LazyColumn(LazyColumn { children, .. }) => {
99                for child in children {
100                    child.visit(visitor)?;
101                }
102                ControlFlow::Continue(())
103            }
104            WidgetKind::Button(Button { child, .. })
105            | WidgetKind::Scroll(Scroll { child, .. })
106            | WidgetKind::SemanticsRegion(SemanticsRegion { child, .. })
107            | WidgetKind::Container(Container { child, .. })
108            | WidgetKind::Positioned(Positioned { child, .. }) => {
109                if let Some(child) = child {
110                    child.visit(visitor)?;
111                }
112                ControlFlow::Continue(())
113            }
114            WidgetKind::Overlay(Overlay {
115                content, overlay, ..
116            }) => {
117                content.visit(visitor)?;
118                overlay.visit(visitor)
119            }
120            WidgetKind::ContextMenuRegion(ContextMenuRegion { child, menu, .. }) => {
121                child.visit(visitor)?;
122                for entry in &menu.items {
123                    if let ContextMenuEntry::Item(item) = entry {
124                        item.child.visit(visitor)?;
125                    }
126                }
127                ControlFlow::Continue(())
128            }
129            WidgetKind::Responsive(Responsive {
130                cases, fallback, ..
131            }) => {
132                for case in cases {
133                    case.child.visit(visitor)?;
134                }
135                fallback.visit(visitor)
136            }
137            WidgetKind::RichText(RichText { inline_widgets, .. }) => {
138                for inline in inline_widgets {
139                    inline.widget.visit(visitor)?;
140                }
141                ControlFlow::Continue(())
142            }
143            WidgetKind::TextInput(TextInput { prefix, suffix, .. }) => {
144                if let Some(prefix) = prefix {
145                    prefix.visit(visitor)?;
146                }
147                if let Some(suffix) = suffix {
148                    suffix.visit(visitor)?;
149                }
150                ControlFlow::Continue(())
151            }
152            #[cfg(feature = "interactive-canvas")]
153            WidgetKind::InteractiveViewer(InteractiveViewer { child, .. }) => child.visit(visitor),
154            WidgetKind::Custom(_)
155            | WidgetKind::Text(_)
156            | WidgetKind::Image(_)
157            | WidgetKind::Video(_)
158            | WidgetKind::Checkbox(_)
159            | WidgetKind::Switch(_)
160            | WidgetKind::Radio(_)
161            | WidgetKind::Spacer(_)
162            | WidgetKind::Slider(_)
163            | WidgetKind::Icon(_) => ControlFlow::Continue(()),
164        }
165    }
166
167    pub(crate) fn with_id(self, id: WidgetId) -> Self {
168        let kind = match *self.kind {
169            WidgetKind::Identified { child, .. } => WidgetKind::Identified { id, child },
170            WidgetKind::ActionScope(w) => WidgetKind::Identified {
171                id,
172                child: Widget {
173                    kind: Box::new(WidgetKind::ActionScope(w)),
174                },
175            },
176            WidgetKind::Custom(w) => WidgetKind::Identified {
177                id,
178                child: Widget {
179                    kind: Box::new(WidgetKind::Custom(w)),
180                },
181            },
182            WidgetKind::Row(mut w) => {
183                w.id = Some(id);
184                WidgetKind::Row(w)
185            }
186            WidgetKind::Column(mut w) => {
187                w.id = Some(id);
188                WidgetKind::Column(w)
189            }
190            WidgetKind::Align(mut w) => {
191                w.id = Some(id);
192                WidgetKind::Align(w)
193            }
194            WidgetKind::FocusScope(mut w) => {
195                w.id = Some(id);
196                WidgetKind::FocusScope(w)
197            }
198            WidgetKind::Clip(mut w) => {
199                w.id = Some(id);
200                WidgetKind::Clip(w)
201            }
202            WidgetKind::Text(mut w) => {
203                w.id = Some(id);
204                WidgetKind::Text(w)
205            }
206            WidgetKind::RichText(mut w) => {
207                w.id = Some(id);
208                WidgetKind::RichText(w)
209            }
210            WidgetKind::Transform(mut w) => {
211                w.id = Some(id);
212                WidgetKind::Transform(w)
213            }
214            #[cfg(feature = "interactive-canvas")]
215            WidgetKind::InteractiveViewer(mut w) => {
216                w.id = Some(id);
217                WidgetKind::InteractiveViewer(w)
218            }
219            WidgetKind::Button(mut w) => {
220                w.id = Some(id);
221                WidgetKind::Button(w)
222            }
223            WidgetKind::Pressable(mut w) => {
224                w.id = Some(id);
225                WidgetKind::Pressable(w)
226            }
227            WidgetKind::TextInput(mut w) => {
228                w.id = Some(id);
229                WidgetKind::TextInput(w)
230            }
231            WidgetKind::Scroll(mut w) => {
232                w.id = Some(id);
233                WidgetKind::Scroll(w)
234            }
235            WidgetKind::SemanticsRegion(mut w) => {
236                w.id = Some(id);
237                WidgetKind::SemanticsRegion(w)
238            }
239            WidgetKind::Image(mut w) => {
240                w.id = Some(id);
241                WidgetKind::Image(w)
242            }
243            WidgetKind::Video(mut w) => {
244                w.id = Some(id);
245                WidgetKind::Video(w)
246            }
247            WidgetKind::ZStack(mut w) => {
248                w.id = Some(id);
249                WidgetKind::ZStack(w)
250            }
251            WidgetKind::Overlay(mut w) => {
252                w.id = Some(id);
253                WidgetKind::Overlay(w)
254            }
255            WidgetKind::Container(mut w) => {
256                w.id = Some(id);
257                WidgetKind::Container(w)
258            }
259            WidgetKind::ContextMenuRegion(mut w) => {
260                w.id = Some(id);
261                WidgetKind::ContextMenuRegion(w)
262            }
263            WidgetKind::GestureDetector(mut w) => {
264                w.id = Some(id);
265                WidgetKind::GestureDetector(w)
266            }
267            WidgetKind::Grid(mut w) => {
268                w.id = Some(id);
269                WidgetKind::Grid(w)
270            }
271            WidgetKind::GridItem(mut w) => {
272                w.id = Some(id);
273                WidgetKind::GridItem(w)
274            }
275            WidgetKind::Responsive(mut w) => {
276                w.id = Some(id);
277                WidgetKind::Responsive(w)
278            }
279            WidgetKind::Checkbox(mut w) => {
280                w.id = Some(id);
281                WidgetKind::Checkbox(w)
282            }
283            WidgetKind::Switch(mut w) => {
284                w.id = Some(id);
285                WidgetKind::Switch(w)
286            }
287            WidgetKind::Radio(mut w) => {
288                w.id = Some(id);
289                WidgetKind::Radio(w)
290            }
291            WidgetKind::SafeArea(mut w) => {
292                w.id = Some(id);
293                WidgetKind::SafeArea(w)
294            }
295            WidgetKind::Positioned(mut w) => {
296                w.id = Some(id);
297                WidgetKind::Positioned(w)
298            }
299            WidgetKind::Spacer(mut w) => {
300                w.id = Some(id);
301                WidgetKind::Spacer(w)
302            }
303            WidgetKind::Slider(mut w) => {
304                w.id = Some(id);
305                WidgetKind::Slider(w)
306            }
307            WidgetKind::LazyColumn(mut w) => {
308                w.id = Some(id);
309                WidgetKind::LazyColumn(w)
310            }
311            WidgetKind::Icon(mut w) => {
312                w.id = Some(id);
313                WidgetKind::Icon(w)
314            }
315            WidgetKind::Composite(mut w) => {
316                w.id = Some(id);
317                WidgetKind::Composite(w)
318            }
319        };
320        Self {
321            kind: Box::new(kind),
322        }
323    }
324
325    pub fn id<I>(self, id: I) -> Self
326    where
327        I: Into<WidgetId>,
328    {
329        self.with_id(id.into())
330    }
331
332    pub(crate) fn custom(node: InternalRenderNode) -> Self {
333        Self {
334            kind: Box::new(WidgetKind::Custom(node)),
335        }
336    }
337
338    pub(crate) fn from_pressable_raw(pressable: Pressable) -> Self {
339        Self {
340            kind: Box::new(WidgetKind::Pressable(pressable)),
341        }
342    }
343
344    pub(crate) fn into_text(self) -> Result<Text, Self> {
345        match *self.kind {
346            WidgetKind::Text(text) => Ok(text),
347            kind => Err(Self {
348                kind: Box::new(kind),
349            }),
350        }
351    }
352
353    pub(crate) fn kind_name(&self) -> &'static str {
354        match &*self.kind {
355            WidgetKind::Identified { .. } => "Identified",
356            WidgetKind::ActionScope(_) => "ActionScope",
357            WidgetKind::Row(_) => "Row",
358            WidgetKind::Column(_) => "Column",
359            WidgetKind::Align(_) => "Align",
360            WidgetKind::FocusScope(_) => "FocusScope",
361            WidgetKind::Clip(_) => "Clip",
362            WidgetKind::Text(_) => "Text",
363            WidgetKind::RichText(_) => "RichText",
364            WidgetKind::Transform(_) => "Transform",
365            #[cfg(feature = "interactive-canvas")]
366            WidgetKind::InteractiveViewer(_) => "InteractiveViewer",
367            WidgetKind::Button(_) => "Button",
368            WidgetKind::Pressable(_) => "Pressable",
369            WidgetKind::TextInput(_) => "TextInput",
370            WidgetKind::Scroll(_) => "Scroll",
371            WidgetKind::SemanticsRegion(_) => "SemanticsRegion",
372            WidgetKind::Image(_) => "Image",
373            WidgetKind::Video(_) => "Video",
374            WidgetKind::ZStack(_) => "ZStack",
375            WidgetKind::Overlay(_) => "Overlay",
376            WidgetKind::Container(_) => "Container",
377            WidgetKind::ContextMenuRegion(_) => "ContextMenuRegion",
378            WidgetKind::GestureDetector(_) => "GestureDetector",
379            WidgetKind::Grid(_) => "Grid",
380            WidgetKind::GridItem(_) => "GridItem",
381            WidgetKind::Responsive(_) => "Responsive",
382            WidgetKind::Checkbox(_) => "Checkbox",
383            WidgetKind::Switch(_) => "Switch",
384            WidgetKind::Radio(_) => "Radio",
385            WidgetKind::SafeArea(_) => "SafeArea",
386            WidgetKind::Positioned(_) => "Positioned",
387            WidgetKind::Spacer(_) => "Spacer",
388            WidgetKind::Slider(_) => "Slider",
389            WidgetKind::LazyColumn(_) => "LazyColumn",
390            WidgetKind::Icon(_) => "Icon",
391            WidgetKind::Composite(_) => "Composite",
392            WidgetKind::Custom(_) => "Custom",
393        }
394    }
395
396    fn kind_discriminator(&self) -> u32 {
397        // These values are part of structural identity. Never renumber an
398        // existing kind; append a new value even when variants move.
399        match &*self.kind {
400            WidgetKind::Identified { .. } => 1,
401            WidgetKind::ActionScope(_) => 2,
402            WidgetKind::Row(_) => 3,
403            WidgetKind::Column(_) => 4,
404            WidgetKind::Align(_) => 5,
405            WidgetKind::FocusScope(_) => 6,
406            WidgetKind::Clip(_) => 7,
407            WidgetKind::Text(_) => 8,
408            WidgetKind::RichText(_) => 9,
409            WidgetKind::Transform(_) => 10,
410            #[cfg(feature = "interactive-canvas")]
411            WidgetKind::InteractiveViewer(_) => 11,
412            WidgetKind::Button(_) => 12,
413            WidgetKind::Pressable(_) => 13,
414            WidgetKind::TextInput(_) => 14,
415            WidgetKind::Scroll(_) => 15,
416            WidgetKind::SemanticsRegion(_) => 16,
417            WidgetKind::Image(_) => 17,
418            WidgetKind::Video(_) => 18,
419            WidgetKind::ZStack(_) => 19,
420            WidgetKind::Overlay(_) => 20,
421            WidgetKind::Container(_) => 21,
422            WidgetKind::ContextMenuRegion(_) => 22,
423            WidgetKind::GestureDetector(_) => 23,
424            WidgetKind::Grid(_) => 24,
425            WidgetKind::GridItem(_) => 25,
426            WidgetKind::Responsive(_) => 26,
427            WidgetKind::Checkbox(_) => 27,
428            WidgetKind::Switch(_) => 28,
429            WidgetKind::Radio(_) => 29,
430            WidgetKind::SafeArea(_) => 30,
431            WidgetKind::Positioned(_) => 31,
432            WidgetKind::Spacer(_) => 32,
433            WidgetKind::Slider(_) => 33,
434            WidgetKind::LazyColumn(_) => 34,
435            WidgetKind::Icon(_) => 35,
436            WidgetKind::Composite(_) => 36,
437            WidgetKind::Custom(_) => 37,
438        }
439    }
440
441    pub(crate) fn declared_id(&self) -> Option<WidgetId> {
442        match &*self.kind {
443            WidgetKind::Identified { id, .. } => Some(*id),
444            WidgetKind::ActionScope(_) => None,
445            WidgetKind::Custom(widget) => widget
446                .lowerer
447                .as_ref()
448                .and_then(|lowerer| lowerer.widget_id()),
449            WidgetKind::Row(widget) => widget.id,
450            WidgetKind::Column(widget) => widget.id,
451            WidgetKind::Align(widget) => widget.id,
452            WidgetKind::FocusScope(widget) => widget.id,
453            WidgetKind::Clip(widget) => widget.id,
454            WidgetKind::Text(widget) => widget.id,
455            WidgetKind::RichText(widget) => widget.id,
456            WidgetKind::Transform(widget) => widget.id,
457            #[cfg(feature = "interactive-canvas")]
458            WidgetKind::InteractiveViewer(widget) => widget.id,
459            WidgetKind::Button(widget) => widget.id,
460            WidgetKind::Pressable(widget) => widget.id,
461            WidgetKind::TextInput(widget) => widget.id,
462            WidgetKind::Scroll(widget) => widget.id,
463            WidgetKind::SemanticsRegion(widget) => widget.id,
464            WidgetKind::Image(widget) => widget.id,
465            WidgetKind::Video(widget) => widget.id,
466            WidgetKind::ZStack(widget) => widget.id,
467            WidgetKind::Overlay(widget) => widget.id,
468            WidgetKind::Container(widget) => widget.id,
469            WidgetKind::ContextMenuRegion(widget) => widget.id,
470            WidgetKind::GestureDetector(widget) => widget.id,
471            WidgetKind::Grid(widget) => widget.id,
472            WidgetKind::GridItem(widget) => widget.id,
473            WidgetKind::Responsive(widget) => widget.id,
474            WidgetKind::Checkbox(widget) => widget.id,
475            WidgetKind::Switch(widget) => widget.id,
476            WidgetKind::Radio(widget) => widget.id,
477            WidgetKind::SafeArea(widget) => widget.id,
478            WidgetKind::Positioned(widget) => widget.id,
479            WidgetKind::Spacer(widget) => widget.id,
480            WidgetKind::Slider(widget) => widget.id,
481            WidgetKind::LazyColumn(widget) => widget.id,
482            WidgetKind::Icon(widget) => widget.id,
483            WidgetKind::Composite(widget) => widget.id,
484        }
485    }
486
487    pub(crate) fn resolve_identities(self, root: WidgetId) -> Self {
488        self.resolve_identity(root)
489    }
490
491    fn resolve_identity(self, automatic_id: WidgetId) -> Self {
492        let resolved = if self.declared_id().is_some() {
493            self
494        } else {
495            self.with_id(automatic_id)
496        };
497        let parent = resolved.declared_id().unwrap_or(automatic_id);
498        resolved.resolve_descendants(parent)
499    }
500
501    fn resolve_descendants(self, parent: WidgetId) -> Self {
502        let child = |widget: Widget, slot: u32| {
503            let id = WidgetId::derived(
504                parent.as_u128(),
505                &[Self::CHILD_ROLE, slot, widget.kind_discriminator()],
506            );
507            widget.resolve_identity(id)
508        };
509        let children = |widgets: Vec<Widget>, first_slot: u32| {
510            widgets
511                .into_iter()
512                .enumerate()
513                .map(|(index, widget)| child(widget, first_slot + index as u32))
514                .collect()
515        };
516
517        let kind = match *self.kind {
518            WidgetKind::Identified {
519                id,
520                child: identified_child,
521            } => WidgetKind::Identified {
522                id,
523                // The structural wrapper is the logical identity for widget
524                // kinds that cannot store an id directly (ActionScope and
525                // Custom). Re-resolving that child would create wrappers
526                // recursively; only its descendants need identities here.
527                child: identified_child.resolve_descendants(id),
528            },
529            WidgetKind::ActionScope(mut widget) => {
530                widget.child = child(widget.child, 0);
531                WidgetKind::ActionScope(widget)
532            }
533            WidgetKind::Row(mut widget) => {
534                widget.children = children(widget.children, 0);
535                WidgetKind::Row(widget)
536            }
537            WidgetKind::Column(mut widget) => {
538                widget.children = children(widget.children, 0);
539                WidgetKind::Column(widget)
540            }
541            WidgetKind::Align(mut widget) => {
542                widget.child = child(widget.child, 0);
543                WidgetKind::Align(widget)
544            }
545            WidgetKind::FocusScope(mut widget) => {
546                widget.children = children(widget.children, 0);
547                WidgetKind::FocusScope(widget)
548            }
549            WidgetKind::Clip(mut widget) => {
550                widget.child = child(widget.child, 0);
551                WidgetKind::Clip(widget)
552            }
553            WidgetKind::Text(widget) => WidgetKind::Text(widget),
554            WidgetKind::RichText(mut widget) => {
555                for (index, inline) in widget.inline_widgets.iter_mut().enumerate() {
556                    inline.widget = child(inline.widget.clone(), index as u32);
557                }
558                WidgetKind::RichText(widget)
559            }
560            WidgetKind::Transform(mut widget) => {
561                widget.child = child(widget.child, 0);
562                WidgetKind::Transform(widget)
563            }
564            #[cfg(feature = "interactive-canvas")]
565            WidgetKind::InteractiveViewer(mut widget) => {
566                widget.child = child(widget.child, 0);
567                WidgetKind::InteractiveViewer(widget)
568            }
569            WidgetKind::Button(mut widget) => {
570                widget.child = widget.child.map(|value| child(value, 0));
571                WidgetKind::Button(widget)
572            }
573            WidgetKind::Pressable(mut widget) => {
574                widget.child = child(widget.child, 0);
575                WidgetKind::Pressable(widget)
576            }
577            WidgetKind::TextInput(mut widget) => {
578                widget.prefix = widget.prefix.map(|value| child(value, 0));
579                widget.suffix = widget.suffix.map(|value| child(value, 1));
580                WidgetKind::TextInput(widget)
581            }
582            WidgetKind::Scroll(mut widget) => {
583                widget.child = widget.child.map(|value| child(value, 0));
584                WidgetKind::Scroll(widget)
585            }
586            WidgetKind::SemanticsRegion(mut widget) => {
587                widget.child = widget.child.map(|value| child(value, 0));
588                WidgetKind::SemanticsRegion(widget)
589            }
590            WidgetKind::Image(widget) => WidgetKind::Image(widget),
591            WidgetKind::Video(widget) => WidgetKind::Video(widget),
592            WidgetKind::ZStack(mut widget) => {
593                widget.children = children(widget.children, 0);
594                WidgetKind::ZStack(widget)
595            }
596            WidgetKind::Overlay(mut widget) => {
597                widget.content = child(widget.content, 0);
598                widget.overlay = child(widget.overlay, 1);
599                WidgetKind::Overlay(widget)
600            }
601            WidgetKind::Container(mut widget) => {
602                widget.child = widget.child.map(|value| child(value, 0));
603                WidgetKind::Container(widget)
604            }
605            WidgetKind::ContextMenuRegion(mut widget) => {
606                widget.child = child(widget.child, 0);
607                for (index, entry) in widget.menu.items.iter_mut().enumerate() {
608                    if let ContextMenuEntry::Item(item) = entry {
609                        item.child = child(item.child.clone(), 1 + index as u32);
610                    }
611                }
612                WidgetKind::ContextMenuRegion(widget)
613            }
614            WidgetKind::GestureDetector(mut widget) => {
615                widget.child = child(widget.child, 0);
616                WidgetKind::GestureDetector(widget)
617            }
618            WidgetKind::Grid(mut widget) => {
619                widget.children = children(widget.children, 0);
620                WidgetKind::Grid(widget)
621            }
622            WidgetKind::GridItem(mut widget) => {
623                widget.child = child(widget.child, 0);
624                WidgetKind::GridItem(widget)
625            }
626            WidgetKind::Responsive(mut widget) => {
627                for (index, case) in widget.cases.iter_mut().enumerate() {
628                    case.child = child(case.child.clone(), index as u32);
629                }
630                widget.fallback = child(widget.fallback, u32::MAX);
631                WidgetKind::Responsive(widget)
632            }
633            WidgetKind::Checkbox(widget) => WidgetKind::Checkbox(widget),
634            WidgetKind::Switch(widget) => WidgetKind::Switch(widget),
635            WidgetKind::Radio(widget) => WidgetKind::Radio(widget),
636            WidgetKind::SafeArea(mut widget) => {
637                widget.child = child(widget.child, 0);
638                WidgetKind::SafeArea(widget)
639            }
640            WidgetKind::Positioned(mut widget) => {
641                widget.child = widget.child.map(|value| child(value, 0));
642                WidgetKind::Positioned(widget)
643            }
644            WidgetKind::Spacer(widget) => WidgetKind::Spacer(widget),
645            WidgetKind::Slider(widget) => WidgetKind::Slider(widget),
646            WidgetKind::LazyColumn(mut widget) => {
647                widget.children = children(widget.children, 0);
648                WidgetKind::LazyColumn(widget)
649            }
650            WidgetKind::Icon(widget) => WidgetKind::Icon(widget),
651            WidgetKind::Composite(mut widget) => {
652                widget.child = child(widget.child, 0);
653                WidgetKind::Composite(widget)
654            }
655            WidgetKind::Custom(widget) => WidgetKind::Custom(widget),
656        };
657
658        Self {
659            kind: Box::new(kind),
660        }
661    }
662
663    pub(crate) fn as_row(&self) -> Option<&Row> {
664        match &*self.kind {
665            WidgetKind::Identified { child, .. } => child.as_row(),
666            WidgetKind::Row(widget) => Some(widget),
667            _ => None,
668        }
669    }
670
671    pub(crate) fn as_column(&self) -> Option<&Column> {
672        match &*self.kind {
673            WidgetKind::Identified { child, .. } => child.as_column(),
674            WidgetKind::Column(widget) => Some(widget),
675            _ => None,
676        }
677    }
678
679    pub(crate) fn as_container(&self) -> Option<&Container> {
680        match &*self.kind {
681            WidgetKind::Identified { child, .. } => child.as_container(),
682            WidgetKind::Container(widget) => Some(widget),
683            _ => None,
684        }
685    }
686
687    pub(crate) fn as_scroll(&self) -> Option<&Scroll> {
688        match &*self.kind {
689            WidgetKind::Identified { child, .. } => child.as_scroll(),
690            WidgetKind::Scroll(widget) => Some(widget),
691            _ => None,
692        }
693    }
694
695    pub(crate) fn as_rich_text(&self) -> Option<&RichText> {
696        match &*self.kind {
697            WidgetKind::Identified { child, .. } => child.as_rich_text(),
698            WidgetKind::RichText(widget) => Some(widget),
699            _ => None,
700        }
701    }
702
703    pub(crate) fn as_text(&self) -> Option<&Text> {
704        match &*self.kind {
705            WidgetKind::Identified { child, .. } => child.as_text(),
706            WidgetKind::Text(widget) => Some(widget),
707            _ => None,
708        }
709    }
710
711    pub(crate) fn as_text_input(&self) -> Option<&TextInput> {
712        match &*self.kind {
713            WidgetKind::Identified { child, .. } => child.as_text_input(),
714            WidgetKind::TextInput(widget) => Some(widget),
715            _ => None,
716        }
717    }
718
719    pub(crate) fn as_button(&self) -> Option<&Button> {
720        match &*self.kind {
721            WidgetKind::Identified { child, .. } => child.as_button(),
722            WidgetKind::Button(widget) => Some(widget),
723            _ => None,
724        }
725    }
726
727    pub(crate) fn as_gesture_detector(&self) -> Option<&GestureDetector> {
728        match &*self.kind {
729            WidgetKind::Identified { child, .. } => child.as_gesture_detector(),
730            WidgetKind::GestureDetector(widget) => Some(widget),
731            _ => None,
732        }
733    }
734
735    pub(crate) fn as_zstack(&self) -> Option<&ZStack> {
736        match &*self.kind {
737            WidgetKind::Identified { child, .. } => child.as_zstack(),
738            WidgetKind::ZStack(widget) => Some(widget),
739            _ => None,
740        }
741    }
742
743    #[cfg(feature = "interactive-canvas")]
744    pub(crate) fn as_interactive_viewer(&self) -> Option<&InteractiveViewer> {
745        match &*self.kind {
746            WidgetKind::Identified { child, .. } => child.as_interactive_viewer(),
747            WidgetKind::InteractiveViewer(widget) => Some(widget),
748            _ => None,
749        }
750    }
751}
752
753/// Overrides Fission's automatic structural identity for a widget.
754///
755/// Explicit IDs are normally only needed for logical items in dynamic
756/// collections or for code that must address a particular widget. An explicit
757/// ID also scopes all automatically identified descendants, so a stateful
758/// subtree follows its logical item when reordered.
759pub trait WidgetIdExt: Into<Widget> + Sized {
760    fn id<I>(self, id: I) -> Widget
761    where
762        I: Into<WidgetId>,
763    {
764        let id = id.into();
765        crate::build::with_widget_id(id, || {
766            let widget: Widget = self.into();
767            widget.with_id(id)
768        })
769    }
770}
771
772impl<T> WidgetIdExt for T where T: Into<Widget> {}
773
774impl Widget {
775    pub(crate) fn lower(&self, cx: &mut InternalLoweringCx) -> WidgetId {
776        match &*self.kind {
777            WidgetKind::Identified { id, child } => {
778                cx.push_scope(*id);
779                let child_id = child.lower(cx);
780                cx.pop_scope();
781                let mut builder = crate::lowering::InternalIrBuilder::new(
782                    (*id).into(),
783                    Op::Structural(StructuralOp::Group {
784                        stable_hash: id.as_u128() as u64,
785                    }),
786                );
787                builder.add_child(child_id);
788                builder.build(cx)
789            }
790            WidgetKind::ActionScope(w) => w.lower(cx),
791            WidgetKind::Row(w) => w.lower(cx),
792            WidgetKind::Column(w) => w.lower(cx),
793            WidgetKind::Align(w) => w.lower(cx),
794            WidgetKind::FocusScope(w) => w.lower(cx),
795            WidgetKind::Clip(w) => w.lower(cx),
796            WidgetKind::Text(w) => w.lower(cx),
797            WidgetKind::RichText(w) => w.lower(cx),
798            WidgetKind::Transform(w) => w.lower(cx),
799            #[cfg(feature = "interactive-canvas")]
800            WidgetKind::InteractiveViewer(w) => w.lower(cx),
801            WidgetKind::Button(w) => w.lower(cx),
802            WidgetKind::Pressable(w) => w.lower(cx),
803            WidgetKind::TextInput(w) => w.lower(cx),
804            WidgetKind::Scroll(w) => w.lower(cx),
805            WidgetKind::SemanticsRegion(w) => w.lower(cx),
806            WidgetKind::Image(w) => w.lower(cx),
807            WidgetKind::Video(w) => w.lower(cx),
808            WidgetKind::ZStack(w) => w.lower(cx),
809            WidgetKind::Overlay(w) => w.lower(cx),
810            WidgetKind::Container(w) => w.lower(cx),
811            WidgetKind::ContextMenuRegion(w) => w.lower(cx),
812            WidgetKind::GestureDetector(w) => w.lower(cx),
813            WidgetKind::Grid(w) => w.lower(cx),
814            WidgetKind::GridItem(w) => w.lower(cx),
815            WidgetKind::Responsive(w) => w.lower(cx),
816            WidgetKind::Checkbox(w) => w.lower(cx),
817            WidgetKind::Switch(w) => w.lower(cx),
818            WidgetKind::Radio(w) => w.lower(cx),
819            WidgetKind::SafeArea(w) => w.lower(cx),
820            WidgetKind::Positioned(w) => w.lower(cx),
821            WidgetKind::Spacer(w) => w.lower(cx),
822            WidgetKind::Slider(w) => w.lower(cx),
823            WidgetKind::LazyColumn(w) => w.lower(cx),
824            WidgetKind::Icon(w) => w.lower(cx),
825            WidgetKind::Composite(w) => w.lower(cx),
826            WidgetKind::Custom(w) => {
827                let lowerer = w
828                    .lowerer
829                    .as_ref()
830                    .expect("CustomWidget lowerer must be set");
831                let wrapper = lowerer.widget_id().unwrap_or_else(|| cx.next_node_id());
832                cx.push_scope(wrapper);
833                let child_id = lowerer.lower_dyn(cx);
834                cx.pop_scope();
835                let mut builder = crate::lowering::InternalIrBuilder::new(
836                    wrapper,
837                    Op::Structural(StructuralOp::Group {
838                        stable_hash: lowerer.stable_key(),
839                    }),
840                );
841                builder.add_child(child_id);
842                let node_id = builder.build(cx);
843
844                // If the custom node carries a render object, store it in the
845                // IR so that hit-testing and event handling can find it later.
846                // We wrap the `Arc<dyn CustomRenderObject>` in a `RenderObjectHolder`
847                // so it can be stored as `Arc<dyn Any + Send + Sync>` in the
848                // dependency-free IR crate and downcast back later.
849                if let Some(render_obj) = &w.render_object {
850                    let holder = crate::ui::custom_render::RenderObjectHolder(render_obj.clone());
851                    let erased: fission_ir::AnyRenderObject = Arc::new(holder);
852                    cx.ir.custom_render_objects.insert(node_id, erased);
853                }
854
855                node_id
856            }
857        }
858    }
859}
860
861impl From<Row> for Widget {
862    fn from(w: Row) -> Self {
863        Self {
864            kind: Box::new(WidgetKind::Row(w)),
865        }
866    }
867}
868impl From<ActionScope> for Widget {
869    fn from(w: ActionScope) -> Self {
870        Self {
871            kind: Box::new(WidgetKind::ActionScope(w)),
872        }
873    }
874}
875impl From<Column> for Widget {
876    fn from(w: Column) -> Self {
877        Self {
878            kind: Box::new(WidgetKind::Column(w)),
879        }
880    }
881}
882impl From<Align> for Widget {
883    fn from(w: Align) -> Self {
884        Self {
885            kind: Box::new(WidgetKind::Align(w)),
886        }
887    }
888}
889impl From<FocusScope> for Widget {
890    fn from(w: FocusScope) -> Self {
891        Self {
892            kind: Box::new(WidgetKind::FocusScope(w)),
893        }
894    }
895}
896impl From<Clip> for Widget {
897    fn from(w: Clip) -> Self {
898        Self {
899            kind: Box::new(WidgetKind::Clip(w)),
900        }
901    }
902}
903impl From<Text> for Widget {
904    fn from(w: Text) -> Self {
905        Self {
906            kind: Box::new(WidgetKind::Text(w)),
907        }
908    }
909}
910impl From<RichText> for Widget {
911    fn from(w: RichText) -> Self {
912        Self {
913            kind: Box::new(WidgetKind::RichText(w)),
914        }
915    }
916}
917impl From<Transform> for Widget {
918    fn from(w: Transform) -> Self {
919        Self {
920            kind: Box::new(WidgetKind::Transform(w)),
921        }
922    }
923}
924#[cfg(feature = "interactive-canvas")]
925impl From<InteractiveViewer> for Widget {
926    fn from(w: InteractiveViewer) -> Self {
927        Self {
928            kind: Box::new(WidgetKind::InteractiveViewer(w)),
929        }
930    }
931}
932impl From<Button> for Widget {
933    fn from(mut w: Button) -> Self {
934        if let Some(motion) = w.motion.take() {
935            let button_id = crate::build::current_widget_id()
936                .or(w.id)
937                .unwrap_or_else(|| WidgetId::explicit("fission.core.button.motion"));
938            w.id = Some(button_id);
939            let motion_id = WidgetId::derived(button_id.as_u128(), &[0xB0770]);
940            let tracks = motion.interaction_tracks(button_id);
941            let ripple = motion.ripple();
942            let base = Self {
943                kind: Box::new(WidgetKind::Button(w)),
944            };
945            let with_motion: Widget = if tracks.is_empty() {
946                base
947            } else {
948                crate::motion::Motion {
949                    id: motion_id,
950                    tracks,
951                    child: base,
952                    ..Default::default()
953                }
954                .into()
955            };
956            return if let Some(effect) = ripple {
957                crate::motion::RippleLayer {
958                    id: WidgetId::derived(button_id.as_u128(), &[0xA11E]),
959                    effect,
960                    child: with_motion,
961                }
962                .into()
963            } else {
964                with_motion
965            };
966        }
967        Self {
968            kind: Box::new(WidgetKind::Button(w)),
969        }
970    }
971}
972impl From<TextInput> for Widget {
973    fn from(w: TextInput) -> Self {
974        Self {
975            kind: Box::new(WidgetKind::TextInput(w)),
976        }
977    }
978}
979impl From<Scroll> for Widget {
980    fn from(w: Scroll) -> Self {
981        Self {
982            kind: Box::new(WidgetKind::Scroll(w)),
983        }
984    }
985}
986impl From<SemanticsRegion> for Widget {
987    fn from(w: SemanticsRegion) -> Self {
988        Self {
989            kind: Box::new(WidgetKind::SemanticsRegion(w)),
990        }
991    }
992}
993impl From<Image> for Widget {
994    fn from(w: Image) -> Self {
995        Self {
996            kind: Box::new(WidgetKind::Image(w)),
997        }
998    }
999}
1000impl From<Video> for Widget {
1001    fn from(w: Video) -> Self {
1002        let node_id = crate::build::current_widget_id()
1003            .or(w.id)
1004            .unwrap_or_else(|| fission_ir::WidgetId::explicit(&w.source.key()));
1005        crate::build::try_register_video(crate::registry::VideoRegistration {
1006            node_id,
1007            source: w.source.as_str().to_string(),
1008            autoplay: w.autoplay,
1009            loop_playback: w.loop_playback,
1010            audio: w.audio.clone(),
1011        });
1012        Self {
1013            kind: Box::new(WidgetKind::Video(w)),
1014        }
1015    }
1016}
1017impl From<ZStack> for Widget {
1018    fn from(w: ZStack) -> Self {
1019        Self {
1020            kind: Box::new(WidgetKind::ZStack(w)),
1021        }
1022    }
1023}
1024impl From<Overlay> for Widget {
1025    fn from(w: Overlay) -> Self {
1026        Self {
1027            kind: Box::new(WidgetKind::Overlay(w)),
1028        }
1029    }
1030}
1031impl From<ContextMenuRegion> for Widget {
1032    fn from(w: ContextMenuRegion) -> Self {
1033        Self {
1034            kind: Box::new(WidgetKind::ContextMenuRegion(w)),
1035        }
1036    }
1037}
1038
1039impl From<Container> for Widget {
1040    fn from(w: Container) -> Self {
1041        Self {
1042            kind: Box::new(WidgetKind::Container(w)),
1043        }
1044    }
1045}
1046impl From<GestureDetector> for Widget {
1047    fn from(w: GestureDetector) -> Self {
1048        Self {
1049            kind: Box::new(WidgetKind::GestureDetector(w)),
1050        }
1051    }
1052}
1053impl From<Grid> for Widget {
1054    fn from(w: Grid) -> Self {
1055        Self {
1056            kind: Box::new(WidgetKind::Grid(w)),
1057        }
1058    }
1059}
1060impl From<GridItem> for Widget {
1061    fn from(w: GridItem) -> Self {
1062        Self {
1063            kind: Box::new(WidgetKind::GridItem(w)),
1064        }
1065    }
1066}
1067impl From<Responsive> for Widget {
1068    fn from(w: Responsive) -> Self {
1069        Self {
1070            kind: Box::new(WidgetKind::Responsive(w)),
1071        }
1072    }
1073}
1074impl From<Checkbox> for Widget {
1075    fn from(w: Checkbox) -> Self {
1076        Self {
1077            kind: Box::new(WidgetKind::Checkbox(w)),
1078        }
1079    }
1080}
1081impl From<Switch> for Widget {
1082    fn from(w: Switch) -> Self {
1083        Self {
1084            kind: Box::new(WidgetKind::Switch(w)),
1085        }
1086    }
1087}
1088impl From<Radio> for Widget {
1089    fn from(w: Radio) -> Self {
1090        Self {
1091            kind: Box::new(WidgetKind::Radio(w)),
1092        }
1093    }
1094}
1095impl From<SafeArea> for Widget {
1096    fn from(w: SafeArea) -> Self {
1097        Self {
1098            kind: Box::new(WidgetKind::SafeArea(w)),
1099        }
1100    }
1101}
1102impl From<Composite> for Widget {
1103    fn from(w: Composite) -> Self {
1104        Self {
1105            kind: Box::new(WidgetKind::Composite(w)),
1106        }
1107    }
1108}
1109impl From<Positioned> for Widget {
1110    fn from(w: Positioned) -> Self {
1111        Self {
1112            kind: Box::new(WidgetKind::Positioned(w)),
1113        }
1114    }
1115}
1116impl From<Spacer> for Widget {
1117    fn from(w: Spacer) -> Self {
1118        Self {
1119            kind: Box::new(WidgetKind::Spacer(w)),
1120        }
1121    }
1122}
1123impl From<Slider> for Widget {
1124    fn from(w: Slider) -> Self {
1125        Self {
1126            kind: Box::new(WidgetKind::Slider(w)),
1127        }
1128    }
1129}
1130impl From<LazyColumn> for Widget {
1131    fn from(w: LazyColumn) -> Self {
1132        Self {
1133            kind: Box::new(WidgetKind::LazyColumn(w)),
1134        }
1135    }
1136}
1137impl From<Icon> for Widget {
1138    fn from(w: Icon) -> Self {
1139        Self {
1140            kind: Box::new(WidgetKind::Icon(w)),
1141        }
1142    }
1143}
1144
1145#[derive(Clone, Debug, Serialize, Deserialize)]
1146pub struct InternalRenderNode {
1147    pub debug_tag: String,
1148    #[serde(skip)]
1149    pub lowerer: Option<Arc<dyn InternalLowerer>>,
1150    /// Optional render object that participates in hit-testing, event handling,
1151    /// and painting.  When `None`, the node behaves exactly as before (lowering
1152    /// only via `InternalLowerer`).
1153    #[serde(skip)]
1154    pub render_object: Option<Arc<dyn CustomRenderObject>>,
1155}
1156
1157pub type CustomWidget = InternalRenderNode;
1158
1159impl From<CustomWidget> for Widget {
1160    fn from(node: CustomWidget) -> Self {
1161        Widget::custom(node)
1162    }
1163}
1164
1165#[cfg(test)]
1166mod visitor_tests {
1167    use super::*;
1168
1169    #[test]
1170    fn visitor_walks_nested_widgets_and_can_stop() {
1171        let root: Widget = Column {
1172            children: vec![Text::new("first").into(), Text::new("second").into()],
1173            ..Default::default()
1174        }
1175        .into();
1176        let mut visited = 0;
1177        let result = root.visit(&mut |widget| {
1178            visited += 1;
1179            if matches!(widget.kind(), WidgetKind::Text(_)) {
1180                ControlFlow::Break(())
1181            } else {
1182                ControlFlow::Continue(())
1183            }
1184        });
1185        assert!(matches!(result, ControlFlow::Break(())));
1186        assert_eq!(visited, 2);
1187    }
1188}