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