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(mut w: Button) -> Self {
513        if let Some(motion) = w.motion.take() {
514            let button_id = crate::build::current_widget_id()
515                .or(w.id)
516                .unwrap_or_else(|| WidgetId::explicit("fission.core.button.motion"));
517            w.id = Some(button_id);
518            let motion_id = WidgetId::derived(button_id.as_u128(), &[0xB0770]);
519            let tracks = motion.interaction_tracks(button_id);
520            let ripple = motion.ripple();
521            let base = Self {
522                kind: Box::new(WidgetKind::Button(w)),
523            };
524            let with_motion: Widget = if tracks.is_empty() {
525                base
526            } else {
527                crate::motion::Motion {
528                    id: motion_id,
529                    tracks,
530                    child: base,
531                    ..Default::default()
532                }
533                .into()
534            };
535            return if let Some(effect) = ripple {
536                crate::motion::RippleLayer {
537                    id: WidgetId::derived(button_id.as_u128(), &[0xA11E]),
538                    effect,
539                    child: with_motion,
540                }
541                .into()
542            } else {
543                with_motion
544            };
545        }
546        Self {
547            kind: Box::new(WidgetKind::Button(w)),
548        }
549    }
550}
551impl From<TextInput> for Widget {
552    fn from(w: TextInput) -> Self {
553        Self {
554            kind: Box::new(WidgetKind::TextInput(w)),
555        }
556    }
557}
558impl From<Scroll> for Widget {
559    fn from(w: Scroll) -> Self {
560        Self {
561            kind: Box::new(WidgetKind::Scroll(w)),
562        }
563    }
564}
565impl From<SemanticsRegion> for Widget {
566    fn from(w: SemanticsRegion) -> Self {
567        Self {
568            kind: Box::new(WidgetKind::SemanticsRegion(w)),
569        }
570    }
571}
572impl From<Image> for Widget {
573    fn from(w: Image) -> Self {
574        Self {
575            kind: Box::new(WidgetKind::Image(w)),
576        }
577    }
578}
579impl From<Video> for Widget {
580    fn from(w: Video) -> Self {
581        let node_id = crate::build::current_widget_id()
582            .or(w.id)
583            .unwrap_or_else(|| fission_ir::WidgetId::explicit(&w.source));
584        crate::build::try_register_video(crate::registry::VideoRegistration {
585            node_id,
586            source: w.source.clone(),
587            autoplay: w.autoplay,
588            loop_playback: w.loop_playback,
589        });
590        Self {
591            kind: Box::new(WidgetKind::Video(w)),
592        }
593    }
594}
595impl From<ZStack> for Widget {
596    fn from(w: ZStack) -> Self {
597        Self {
598            kind: Box::new(WidgetKind::ZStack(w)),
599        }
600    }
601}
602impl From<Overlay> for Widget {
603    fn from(w: Overlay) -> Self {
604        Self {
605            kind: Box::new(WidgetKind::Overlay(w)),
606        }
607    }
608}
609impl From<Container> for Widget {
610    fn from(w: Container) -> Self {
611        Self {
612            kind: Box::new(WidgetKind::Container(w)),
613        }
614    }
615}
616impl From<GestureDetector> for Widget {
617    fn from(w: GestureDetector) -> Self {
618        Self {
619            kind: Box::new(WidgetKind::GestureDetector(w)),
620        }
621    }
622}
623impl From<Grid> for Widget {
624    fn from(w: Grid) -> Self {
625        Self {
626            kind: Box::new(WidgetKind::Grid(w)),
627        }
628    }
629}
630impl From<GridItem> for Widget {
631    fn from(w: GridItem) -> Self {
632        Self {
633            kind: Box::new(WidgetKind::GridItem(w)),
634        }
635    }
636}
637impl From<Checkbox> for Widget {
638    fn from(w: Checkbox) -> Self {
639        Self {
640            kind: Box::new(WidgetKind::Checkbox(w)),
641        }
642    }
643}
644impl From<Switch> for Widget {
645    fn from(w: Switch) -> Self {
646        Self {
647            kind: Box::new(WidgetKind::Switch(w)),
648        }
649    }
650}
651impl From<Radio> for Widget {
652    fn from(w: Radio) -> Self {
653        Self {
654            kind: Box::new(WidgetKind::Radio(w)),
655        }
656    }
657}
658impl From<SafeArea> for Widget {
659    fn from(w: SafeArea) -> Self {
660        Self {
661            kind: Box::new(WidgetKind::SafeArea(w)),
662        }
663    }
664}
665impl From<Composite> for Widget {
666    fn from(w: Composite) -> Self {
667        Self {
668            kind: Box::new(WidgetKind::Composite(w)),
669        }
670    }
671}
672impl From<Positioned> for Widget {
673    fn from(w: Positioned) -> Self {
674        Self {
675            kind: Box::new(WidgetKind::Positioned(w)),
676        }
677    }
678}
679impl From<Spacer> for Widget {
680    fn from(w: Spacer) -> Self {
681        Self {
682            kind: Box::new(WidgetKind::Spacer(w)),
683        }
684    }
685}
686impl From<Slider> for Widget {
687    fn from(w: Slider) -> Self {
688        Self {
689            kind: Box::new(WidgetKind::Slider(w)),
690        }
691    }
692}
693impl From<LazyColumn> for Widget {
694    fn from(w: LazyColumn) -> Self {
695        Self {
696            kind: Box::new(WidgetKind::LazyColumn(w)),
697        }
698    }
699}
700impl From<Icon> for Widget {
701    fn from(w: Icon) -> Self {
702        Self {
703            kind: Box::new(WidgetKind::Icon(w)),
704        }
705    }
706}
707
708#[derive(Clone, Debug, Serialize, Deserialize)]
709pub struct InternalRenderNode {
710    pub debug_tag: String,
711    #[serde(skip)]
712    pub lowerer: Option<Arc<dyn InternalLowerer>>,
713    /// Optional render object that participates in hit-testing, event handling,
714    /// and painting.  When `None`, the node behaves exactly as before (lowering
715    /// only via `InternalLowerer`).
716    #[serde(skip)]
717    pub render_object: Option<Arc<dyn CustomRenderObject>>,
718}
719
720pub type CustomWidget = InternalRenderNode;
721
722impl From<CustomWidget> for Widget {
723    fn from(node: CustomWidget) -> Self {
724        Widget::custom(node)
725    }
726}