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