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