Skip to main content

fission_core/ui/
node.rs

1use super::custom_render::CustomRenderObject;
2use super::traits::{InternalLower, InternalLowerer};
3use super::widgets::{
4    ActionScope, Align, Button, Checkbox, Clip, Column, Composite, Container, ContextMenuRegion,
5    FocusScope, GestureDetector, Grid, GridItem, Icon, Image, LazyColumn, Overlay, Positioned,
6    Radio, RichText, Row, SafeArea, Scroll, SemanticsRegion, Slider, Spacer, Switch, Text,
7    TextInput, Transform, Video, ZStack,
8};
9use crate::lowering::InternalLoweringCx;
10use fission_ir::{Op, StructuralOp, WidgetId};
11use serde::{Deserialize, Serialize};
12use std::sync::Arc;
13
14#[derive(Clone, Debug, Serialize, Deserialize)]
15pub struct Widget {
16    kind: Box<WidgetKind>,
17}
18
19#[derive(Clone, Debug, Serialize, Deserialize)]
20enum WidgetKind {
21    Identified { id: WidgetId, child: Widget },
22    ActionScope(ActionScope),
23    Row(Row),
24    Column(Column),
25    Align(Align),
26    FocusScope(FocusScope),
27    Clip(Clip),
28    Text(Text),
29    RichText(RichText),
30    Transform(Transform),
31    Button(Button),
32    TextInput(TextInput),
33    Scroll(Scroll),
34    SemanticsRegion(SemanticsRegion),
35    Image(Image),
36    Video(Video),
37    ZStack(ZStack),
38    Overlay(Overlay),
39    Container(Container),
40    ContextMenuRegion(ContextMenuRegion),
41    GestureDetector(GestureDetector),
42    Grid(Grid),
43    GridItem(GridItem),
44    Checkbox(Checkbox),
45    Switch(Switch),
46    Radio(Radio),
47    SafeArea(SafeArea),
48    Positioned(Positioned),
49    Spacer(Spacer),
50    Slider(Slider),
51    LazyColumn(LazyColumn),
52    Icon(Icon),
53    Composite(Composite),
54    Custom(InternalRenderNode),
55}
56
57impl Widget {
58    pub(crate) fn with_id(self, id: WidgetId) -> Self {
59        let kind = match *self.kind {
60            WidgetKind::Identified { child, .. } => WidgetKind::Identified { id, child },
61            WidgetKind::ActionScope(w) => WidgetKind::Identified {
62                id,
63                child: Widget {
64                    kind: Box::new(WidgetKind::ActionScope(w)),
65                },
66            },
67            WidgetKind::Custom(w) => WidgetKind::Identified {
68                id,
69                child: Widget {
70                    kind: Box::new(WidgetKind::Custom(w)),
71                },
72            },
73            WidgetKind::Row(mut w) => {
74                w.id = Some(id);
75                WidgetKind::Row(w)
76            }
77            WidgetKind::Column(mut w) => {
78                w.id = Some(id);
79                WidgetKind::Column(w)
80            }
81            WidgetKind::Align(mut w) => {
82                w.id = Some(id);
83                WidgetKind::Align(w)
84            }
85            WidgetKind::FocusScope(mut w) => {
86                w.id = Some(id);
87                WidgetKind::FocusScope(w)
88            }
89            WidgetKind::Clip(mut w) => {
90                w.id = Some(id);
91                WidgetKind::Clip(w)
92            }
93            WidgetKind::Text(mut w) => {
94                w.id = Some(id);
95                WidgetKind::Text(w)
96            }
97            WidgetKind::RichText(mut w) => {
98                w.id = Some(id);
99                WidgetKind::RichText(w)
100            }
101            WidgetKind::Transform(mut w) => {
102                w.id = Some(id);
103                WidgetKind::Transform(w)
104            }
105            WidgetKind::Button(mut w) => {
106                w.id = Some(id);
107                WidgetKind::Button(w)
108            }
109            WidgetKind::TextInput(mut w) => {
110                w.id = Some(id);
111                WidgetKind::TextInput(w)
112            }
113            WidgetKind::Scroll(mut w) => {
114                w.id = Some(id);
115                WidgetKind::Scroll(w)
116            }
117            WidgetKind::SemanticsRegion(mut w) => {
118                w.id = Some(id);
119                WidgetKind::SemanticsRegion(w)
120            }
121            WidgetKind::Image(mut w) => {
122                w.id = Some(id);
123                WidgetKind::Image(w)
124            }
125            WidgetKind::Video(mut w) => {
126                w.id = Some(id);
127                WidgetKind::Video(w)
128            }
129            WidgetKind::ZStack(mut w) => {
130                w.id = Some(id);
131                WidgetKind::ZStack(w)
132            }
133            WidgetKind::Overlay(mut w) => {
134                w.id = Some(id);
135                WidgetKind::Overlay(w)
136            }
137            WidgetKind::Container(mut w) => {
138                w.id = Some(id);
139                WidgetKind::Container(w)
140            }
141            WidgetKind::ContextMenuRegion(mut w) => {
142                w.id = Some(id);
143                WidgetKind::ContextMenuRegion(w)
144            }
145            WidgetKind::GestureDetector(mut w) => {
146                w.id = Some(id);
147                WidgetKind::GestureDetector(w)
148            }
149            WidgetKind::Grid(mut w) => {
150                w.id = Some(id);
151                WidgetKind::Grid(w)
152            }
153            WidgetKind::GridItem(mut w) => {
154                w.id = Some(id);
155                WidgetKind::GridItem(w)
156            }
157            WidgetKind::Checkbox(mut w) => {
158                w.id = Some(id);
159                WidgetKind::Checkbox(w)
160            }
161            WidgetKind::Switch(mut w) => {
162                w.id = Some(id);
163                WidgetKind::Switch(w)
164            }
165            WidgetKind::Radio(mut w) => {
166                w.id = Some(id);
167                WidgetKind::Radio(w)
168            }
169            WidgetKind::SafeArea(mut w) => {
170                w.id = Some(id);
171                WidgetKind::SafeArea(w)
172            }
173            WidgetKind::Positioned(mut w) => {
174                w.id = Some(id);
175                WidgetKind::Positioned(w)
176            }
177            WidgetKind::Spacer(mut w) => {
178                w.id = Some(id);
179                WidgetKind::Spacer(w)
180            }
181            WidgetKind::Slider(mut w) => {
182                w.id = Some(id);
183                WidgetKind::Slider(w)
184            }
185            WidgetKind::LazyColumn(mut w) => {
186                w.id = Some(id);
187                WidgetKind::LazyColumn(w)
188            }
189            WidgetKind::Icon(mut w) => {
190                w.id = Some(id);
191                WidgetKind::Icon(w)
192            }
193            WidgetKind::Composite(mut w) => {
194                w.id = Some(id);
195                WidgetKind::Composite(w)
196            }
197        };
198        Self {
199            kind: Box::new(kind),
200        }
201    }
202
203    pub fn id<I>(self, id: I) -> Self
204    where
205        I: Into<WidgetId>,
206    {
207        self.with_id(id.into())
208    }
209
210    pub(crate) fn custom(node: InternalRenderNode) -> Self {
211        Self {
212            kind: Box::new(WidgetKind::Custom(node)),
213        }
214    }
215
216    pub(crate) fn into_text(self) -> Result<Text, Self> {
217        match *self.kind {
218            WidgetKind::Text(text) => Ok(text),
219            kind => Err(Self {
220                kind: Box::new(kind),
221            }),
222        }
223    }
224
225    pub(crate) fn kind_name(&self) -> &'static str {
226        match &*self.kind {
227            WidgetKind::Identified { .. } => "Identified",
228            WidgetKind::ActionScope(_) => "ActionScope",
229            WidgetKind::Row(_) => "Row",
230            WidgetKind::Column(_) => "Column",
231            WidgetKind::Align(_) => "Align",
232            WidgetKind::FocusScope(_) => "FocusScope",
233            WidgetKind::Clip(_) => "Clip",
234            WidgetKind::Text(_) => "Text",
235            WidgetKind::RichText(_) => "RichText",
236            WidgetKind::Transform(_) => "Transform",
237            WidgetKind::Button(_) => "Button",
238            WidgetKind::TextInput(_) => "TextInput",
239            WidgetKind::Scroll(_) => "Scroll",
240            WidgetKind::SemanticsRegion(_) => "SemanticsRegion",
241            WidgetKind::Image(_) => "Image",
242            WidgetKind::Video(_) => "Video",
243            WidgetKind::ZStack(_) => "ZStack",
244            WidgetKind::Overlay(_) => "Overlay",
245            WidgetKind::Container(_) => "Container",
246            WidgetKind::ContextMenuRegion(_) => "ContextMenuRegion",
247            WidgetKind::GestureDetector(_) => "GestureDetector",
248            WidgetKind::Grid(_) => "Grid",
249            WidgetKind::GridItem(_) => "GridItem",
250            WidgetKind::Checkbox(_) => "Checkbox",
251            WidgetKind::Switch(_) => "Switch",
252            WidgetKind::Radio(_) => "Radio",
253            WidgetKind::SafeArea(_) => "SafeArea",
254            WidgetKind::Positioned(_) => "Positioned",
255            WidgetKind::Spacer(_) => "Spacer",
256            WidgetKind::Slider(_) => "Slider",
257            WidgetKind::LazyColumn(_) => "LazyColumn",
258            WidgetKind::Icon(_) => "Icon",
259            WidgetKind::Composite(_) => "Composite",
260            WidgetKind::Custom(_) => "Custom",
261        }
262    }
263
264    pub(crate) fn as_row(&self) -> Option<&Row> {
265        match &*self.kind {
266            WidgetKind::Identified { child, .. } => child.as_row(),
267            WidgetKind::Row(widget) => Some(widget),
268            _ => None,
269        }
270    }
271
272    pub(crate) fn as_column(&self) -> Option<&Column> {
273        match &*self.kind {
274            WidgetKind::Identified { child, .. } => child.as_column(),
275            WidgetKind::Column(widget) => Some(widget),
276            _ => None,
277        }
278    }
279
280    pub(crate) fn as_container(&self) -> Option<&Container> {
281        match &*self.kind {
282            WidgetKind::Identified { child, .. } => child.as_container(),
283            WidgetKind::Container(widget) => Some(widget),
284            _ => None,
285        }
286    }
287
288    pub(crate) fn as_scroll(&self) -> Option<&Scroll> {
289        match &*self.kind {
290            WidgetKind::Identified { child, .. } => child.as_scroll(),
291            WidgetKind::Scroll(widget) => Some(widget),
292            _ => None,
293        }
294    }
295
296    pub(crate) fn as_rich_text(&self) -> Option<&RichText> {
297        match &*self.kind {
298            WidgetKind::Identified { child, .. } => child.as_rich_text(),
299            WidgetKind::RichText(widget) => Some(widget),
300            _ => None,
301        }
302    }
303
304    pub(crate) fn as_text(&self) -> Option<&Text> {
305        match &*self.kind {
306            WidgetKind::Identified { child, .. } => child.as_text(),
307            WidgetKind::Text(widget) => Some(widget),
308            _ => None,
309        }
310    }
311
312    pub(crate) fn as_text_input(&self) -> Option<&TextInput> {
313        match &*self.kind {
314            WidgetKind::Identified { child, .. } => child.as_text_input(),
315            WidgetKind::TextInput(widget) => Some(widget),
316            _ => None,
317        }
318    }
319
320    pub(crate) fn as_button(&self) -> Option<&Button> {
321        match &*self.kind {
322            WidgetKind::Identified { child, .. } => child.as_button(),
323            WidgetKind::Button(widget) => Some(widget),
324            _ => None,
325        }
326    }
327
328    pub(crate) fn as_gesture_detector(&self) -> Option<&GestureDetector> {
329        match &*self.kind {
330            WidgetKind::Identified { child, .. } => child.as_gesture_detector(),
331            WidgetKind::GestureDetector(widget) => Some(widget),
332            _ => None,
333        }
334    }
335
336    pub(crate) fn as_zstack(&self) -> Option<&ZStack> {
337        match &*self.kind {
338            WidgetKind::Identified { child, .. } => child.as_zstack(),
339            WidgetKind::ZStack(widget) => Some(widget),
340            _ => None,
341        }
342    }
343}
344
345pub trait WidgetIdExt: Into<Widget> + Sized {
346    fn id<I>(self, id: I) -> Widget
347    where
348        I: Into<WidgetId>,
349    {
350        let id = id.into();
351        crate::build::with_widget_id(id, || {
352            let widget: Widget = self.into();
353            widget.with_id(id)
354        })
355    }
356}
357
358impl<T> WidgetIdExt for T where T: Into<Widget> {}
359
360impl Widget {
361    pub(crate) fn lower(&self, cx: &mut InternalLoweringCx) -> WidgetId {
362        match &*self.kind {
363            WidgetKind::Identified { id, child } => {
364                let child_id = child.lower(cx);
365                let mut builder = crate::lowering::InternalIrBuilder::new(
366                    (*id).into(),
367                    Op::Structural(StructuralOp::Group {
368                        stable_hash: id.as_u128() as u64,
369                    }),
370                );
371                builder.add_child(child_id);
372                builder.build(cx)
373            }
374            WidgetKind::ActionScope(w) => w.lower(cx),
375            WidgetKind::Row(w) => w.lower(cx),
376            WidgetKind::Column(w) => w.lower(cx),
377            WidgetKind::Align(w) => w.lower(cx),
378            WidgetKind::FocusScope(w) => w.lower(cx),
379            WidgetKind::Clip(w) => w.lower(cx),
380            WidgetKind::Text(w) => w.lower(cx),
381            WidgetKind::RichText(w) => w.lower(cx),
382            WidgetKind::Transform(w) => w.lower(cx),
383            WidgetKind::Button(w) => w.lower(cx),
384            WidgetKind::TextInput(w) => w.lower(cx),
385            WidgetKind::Scroll(w) => w.lower(cx),
386            WidgetKind::SemanticsRegion(w) => w.lower(cx),
387            WidgetKind::Image(w) => w.lower(cx),
388            WidgetKind::Video(w) => w.lower(cx),
389            WidgetKind::ZStack(w) => w.lower(cx),
390            WidgetKind::Overlay(w) => w.lower(cx),
391            WidgetKind::Container(w) => w.lower(cx),
392            WidgetKind::ContextMenuRegion(w) => w.lower(cx),
393            WidgetKind::GestureDetector(w) => w.lower(cx),
394            WidgetKind::Grid(w) => w.lower(cx),
395            WidgetKind::GridItem(w) => w.lower(cx),
396            WidgetKind::Checkbox(w) => w.lower(cx),
397            WidgetKind::Switch(w) => w.lower(cx),
398            WidgetKind::Radio(w) => w.lower(cx),
399            WidgetKind::SafeArea(w) => w.lower(cx),
400            WidgetKind::Positioned(w) => w.lower(cx),
401            WidgetKind::Spacer(w) => w.lower(cx),
402            WidgetKind::Slider(w) => w.lower(cx),
403            WidgetKind::LazyColumn(w) => w.lower(cx),
404            WidgetKind::Icon(w) => w.lower(cx),
405            WidgetKind::Composite(w) => w.lower(cx),
406            WidgetKind::Custom(w) => {
407                let lowerer = w
408                    .lowerer
409                    .as_ref()
410                    .expect("CustomWidget lowerer must be set");
411                let child_id = lowerer.lower_dyn(cx);
412                let wrapper = cx.next_node_id();
413                let mut builder = crate::lowering::InternalIrBuilder::new(
414                    wrapper,
415                    Op::Structural(StructuralOp::Group {
416                        stable_hash: lowerer.stable_key(),
417                    }),
418                );
419                builder.add_child(child_id);
420                let node_id = builder.build(cx);
421
422                // If the custom node carries a render object, store it in the
423                // IR so that hit-testing and event handling can find it later.
424                // We wrap the `Arc<dyn CustomRenderObject>` in a `RenderObjectHolder`
425                // so it can be stored as `Arc<dyn Any + Send + Sync>` in the
426                // dependency-free IR crate and downcast back later.
427                if let Some(render_obj) = &w.render_object {
428                    let holder = crate::ui::custom_render::RenderObjectHolder(render_obj.clone());
429                    let erased: fission_ir::AnyRenderObject = Arc::new(holder);
430                    // Register the render object at the wrapper AND every node in
431                    // the lowered subtree so the parent-walk from any hit descendant
432                    // finds it regardless of tree depth.
433                    cx.ir.custom_render_objects.insert(node_id, erased.clone());
434                    fn register_subtree(
435                        ir: &mut fission_ir::CoreIR,
436                        node_id: fission_ir::WidgetId,
437                        erased: &fission_ir::AnyRenderObject,
438                    ) {
439                        ir.custom_render_objects.insert(node_id, erased.clone());
440                        if let Some(children) = ir.nodes.get(&node_id).map(|n| n.children.clone()) {
441                            for child_id in children {
442                                register_subtree(ir, child_id, erased);
443                            }
444                        }
445                    }
446                    register_subtree(&mut cx.ir, child_id, &erased);
447                }
448
449                node_id
450            }
451        }
452    }
453}
454
455impl From<Row> for Widget {
456    fn from(w: Row) -> Self {
457        Self {
458            kind: Box::new(WidgetKind::Row(w)),
459        }
460    }
461}
462impl From<ActionScope> for Widget {
463    fn from(w: ActionScope) -> Self {
464        Self {
465            kind: Box::new(WidgetKind::ActionScope(w)),
466        }
467    }
468}
469impl From<Column> for Widget {
470    fn from(w: Column) -> Self {
471        Self {
472            kind: Box::new(WidgetKind::Column(w)),
473        }
474    }
475}
476impl From<Align> for Widget {
477    fn from(w: Align) -> Self {
478        Self {
479            kind: Box::new(WidgetKind::Align(w)),
480        }
481    }
482}
483impl From<FocusScope> for Widget {
484    fn from(w: FocusScope) -> Self {
485        Self {
486            kind: Box::new(WidgetKind::FocusScope(w)),
487        }
488    }
489}
490impl From<Clip> for Widget {
491    fn from(w: Clip) -> Self {
492        Self {
493            kind: Box::new(WidgetKind::Clip(w)),
494        }
495    }
496}
497impl From<Text> for Widget {
498    fn from(w: Text) -> Self {
499        Self {
500            kind: Box::new(WidgetKind::Text(w)),
501        }
502    }
503}
504impl From<RichText> for Widget {
505    fn from(w: RichText) -> Self {
506        Self {
507            kind: Box::new(WidgetKind::RichText(w)),
508        }
509    }
510}
511impl From<Transform> for Widget {
512    fn from(w: Transform) -> Self {
513        Self {
514            kind: Box::new(WidgetKind::Transform(w)),
515        }
516    }
517}
518impl From<Button> for Widget {
519    fn from(mut w: Button) -> Self {
520        if let Some(motion) = w.motion.take() {
521            let button_id = crate::build::current_widget_id()
522                .or(w.id)
523                .unwrap_or_else(|| WidgetId::explicit("fission.core.button.motion"));
524            w.id = Some(button_id);
525            let motion_id = WidgetId::derived(button_id.as_u128(), &[0xB0770]);
526            let tracks = motion.interaction_tracks(button_id);
527            let ripple = motion.ripple();
528            let base = Self {
529                kind: Box::new(WidgetKind::Button(w)),
530            };
531            let with_motion: Widget = if tracks.is_empty() {
532                base
533            } else {
534                crate::motion::Motion {
535                    id: motion_id,
536                    tracks,
537                    child: base,
538                    ..Default::default()
539                }
540                .into()
541            };
542            return if let Some(effect) = ripple {
543                crate::motion::RippleLayer {
544                    id: WidgetId::derived(button_id.as_u128(), &[0xA11E]),
545                    effect,
546                    child: with_motion,
547                }
548                .into()
549            } else {
550                with_motion
551            };
552        }
553        Self {
554            kind: Box::new(WidgetKind::Button(w)),
555        }
556    }
557}
558impl From<TextInput> for Widget {
559    fn from(w: TextInput) -> Self {
560        Self {
561            kind: Box::new(WidgetKind::TextInput(w)),
562        }
563    }
564}
565impl From<Scroll> for Widget {
566    fn from(w: Scroll) -> Self {
567        Self {
568            kind: Box::new(WidgetKind::Scroll(w)),
569        }
570    }
571}
572impl From<SemanticsRegion> for Widget {
573    fn from(w: SemanticsRegion) -> Self {
574        Self {
575            kind: Box::new(WidgetKind::SemanticsRegion(w)),
576        }
577    }
578}
579impl From<Image> for Widget {
580    fn from(w: Image) -> Self {
581        Self {
582            kind: Box::new(WidgetKind::Image(w)),
583        }
584    }
585}
586impl From<Video> for Widget {
587    fn from(w: Video) -> Self {
588        let node_id = crate::build::current_widget_id()
589            .or(w.id)
590            .unwrap_or_else(|| fission_ir::WidgetId::explicit(&w.source.key()));
591        crate::build::try_register_video(crate::registry::VideoRegistration {
592            node_id,
593            source: w.source.as_str().to_string(),
594            autoplay: w.autoplay,
595            loop_playback: w.loop_playback,
596            audio: w.audio.clone(),
597        });
598        Self {
599            kind: Box::new(WidgetKind::Video(w)),
600        }
601    }
602}
603impl From<ZStack> for Widget {
604    fn from(w: ZStack) -> Self {
605        Self {
606            kind: Box::new(WidgetKind::ZStack(w)),
607        }
608    }
609}
610impl From<Overlay> for Widget {
611    fn from(w: Overlay) -> Self {
612        Self {
613            kind: Box::new(WidgetKind::Overlay(w)),
614        }
615    }
616}
617impl From<ContextMenuRegion> for Widget {
618    fn from(w: ContextMenuRegion) -> Self {
619        Self {
620            kind: Box::new(WidgetKind::ContextMenuRegion(w)),
621        }
622    }
623}
624
625impl From<Container> for Widget {
626    fn from(w: Container) -> Self {
627        Self {
628            kind: Box::new(WidgetKind::Container(w)),
629        }
630    }
631}
632impl From<GestureDetector> for Widget {
633    fn from(w: GestureDetector) -> Self {
634        Self {
635            kind: Box::new(WidgetKind::GestureDetector(w)),
636        }
637    }
638}
639impl From<Grid> for Widget {
640    fn from(w: Grid) -> Self {
641        Self {
642            kind: Box::new(WidgetKind::Grid(w)),
643        }
644    }
645}
646impl From<GridItem> for Widget {
647    fn from(w: GridItem) -> Self {
648        Self {
649            kind: Box::new(WidgetKind::GridItem(w)),
650        }
651    }
652}
653impl From<Checkbox> for Widget {
654    fn from(w: Checkbox) -> Self {
655        Self {
656            kind: Box::new(WidgetKind::Checkbox(w)),
657        }
658    }
659}
660impl From<Switch> for Widget {
661    fn from(w: Switch) -> Self {
662        Self {
663            kind: Box::new(WidgetKind::Switch(w)),
664        }
665    }
666}
667impl From<Radio> for Widget {
668    fn from(w: Radio) -> Self {
669        Self {
670            kind: Box::new(WidgetKind::Radio(w)),
671        }
672    }
673}
674impl From<SafeArea> for Widget {
675    fn from(w: SafeArea) -> Self {
676        Self {
677            kind: Box::new(WidgetKind::SafeArea(w)),
678        }
679    }
680}
681impl From<Composite> for Widget {
682    fn from(w: Composite) -> Self {
683        Self {
684            kind: Box::new(WidgetKind::Composite(w)),
685        }
686    }
687}
688impl From<Positioned> for Widget {
689    fn from(w: Positioned) -> Self {
690        Self {
691            kind: Box::new(WidgetKind::Positioned(w)),
692        }
693    }
694}
695impl From<Spacer> for Widget {
696    fn from(w: Spacer) -> Self {
697        Self {
698            kind: Box::new(WidgetKind::Spacer(w)),
699        }
700    }
701}
702impl From<Slider> for Widget {
703    fn from(w: Slider) -> Self {
704        Self {
705            kind: Box::new(WidgetKind::Slider(w)),
706        }
707    }
708}
709impl From<LazyColumn> for Widget {
710    fn from(w: LazyColumn) -> Self {
711        Self {
712            kind: Box::new(WidgetKind::LazyColumn(w)),
713        }
714    }
715}
716impl From<Icon> for Widget {
717    fn from(w: Icon) -> Self {
718        Self {
719            kind: Box::new(WidgetKind::Icon(w)),
720        }
721    }
722}
723
724#[derive(Clone, Debug, Serialize, Deserialize)]
725pub struct InternalRenderNode {
726    pub debug_tag: String,
727    #[serde(skip)]
728    pub lowerer: Option<Arc<dyn InternalLowerer>>,
729    /// Optional render object that participates in hit-testing, event handling,
730    /// and painting.  When `None`, the node behaves exactly as before (lowering
731    /// only via `InternalLowerer`).
732    #[serde(skip)]
733    pub render_object: Option<Arc<dyn CustomRenderObject>>,
734}
735
736pub type CustomWidget = InternalRenderNode;
737
738impl From<CustomWidget> for Widget {
739    fn from(node: CustomWidget) -> Self {
740        Widget::custom(node)
741    }
742}