Skip to main content

fission_core/
build.rs

1use crate::{build_context::BuildCtx, GlobalState, View};
2use std::any::{type_name, Any, TypeId};
3use std::cell::RefCell;
4use std::collections::{HashMap, HashSet};
5use std::marker::PhantomData;
6
7type NextPortalSeq = unsafe fn(*mut ()) -> u64;
8type RegisterRuntimeReducer = unsafe fn(*mut (), crate::ActionId, crate::BoxedReducer);
9
10struct BuildScope {
11    state_type: TypeId,
12    state_name: &'static str,
13    ctx: *mut (),
14    view: *const (),
15    resources: *mut crate::registry::ResourceRegistry,
16    motion_declarations: *mut Vec<crate::motion::MotionDeclaration>,
17    video_nodes: *mut Vec<crate::registry::VideoRegistration>,
18    web_nodes: *mut Vec<crate::registry::WebRegistration>,
19    portals: *mut Vec<crate::registry::PortalEntry>,
20    next_portal_seq: NextPortalSeq,
21    register_runtime_reducer: RegisterRuntimeReducer,
22    runtime: *const crate::RuntimeState,
23    env: *const crate::Env,
24    layout: Option<*const crate::LayoutSnapshot>,
25    local_state_ordinals: HashMap<(&'static str, &'static str), usize>,
26    local_state_seen: HashSet<crate::state::LocalStateKey>,
27    widget_id_stack: Vec<crate::WidgetId>,
28    providers: HashMap<TypeId, Vec<Box<dyn Any + Send + Sync>>>,
29}
30
31thread_local! {
32    static BUILD_SCOPES: RefCell<Vec<BuildScope>> = const { RefCell::new(Vec::new()) };
33}
34
35#[derive(Debug)]
36pub struct BuildCtxHandle<S: GlobalState> {
37    _state: PhantomData<fn() -> S>,
38}
39
40impl<S: GlobalState> Clone for BuildCtxHandle<S> {
41    fn clone(&self) -> Self {
42        *self
43    }
44}
45
46impl<S: GlobalState> Copy for BuildCtxHandle<S> {}
47
48#[derive(Debug)]
49pub struct ViewHandle<S: GlobalState> {
50    _state: PhantomData<fn() -> S>,
51}
52
53impl<S: GlobalState> Clone for ViewHandle<S> {
54    fn clone(&self) -> Self {
55        *self
56    }
57}
58
59impl<S: GlobalState> Copy for ViewHandle<S> {}
60
61#[doc(hidden)]
62pub fn enter<S, R>(ctx: &mut BuildCtx<S>, view: &View<'_, S>, f: impl FnOnce() -> R) -> R
63where
64    S: GlobalState,
65{
66    BUILD_SCOPES.with(|scopes| {
67        scopes.borrow_mut().push(BuildScope {
68            state_type: TypeId::of::<S>(),
69            state_name: type_name::<S>(),
70            ctx: (ctx as *mut BuildCtx<S>).cast::<()>(),
71            view: (view as *const View<'_, S>).cast::<()>(),
72            resources: &mut ctx.resources,
73            motion_declarations: &mut ctx.motion_declarations,
74            video_nodes: &mut ctx.video_nodes,
75            web_nodes: &mut ctx.web_nodes,
76            portals: &mut ctx.portals,
77            next_portal_seq: next_portal_seq::<S>,
78            register_runtime_reducer: register_runtime_reducer::<S>,
79            runtime: view.runtime(),
80            env: view.env(),
81            layout: view
82                .layout()
83                .map(|layout| layout as *const crate::LayoutSnapshot),
84            local_state_ordinals: HashMap::new(),
85            local_state_seen: HashSet::new(),
86            widget_id_stack: Vec::new(),
87            providers: HashMap::new(),
88        });
89    });
90
91    struct PopGuard;
92    impl Drop for PopGuard {
93        fn drop(&mut self) {
94            BUILD_SCOPES.with(|scopes| {
95                let mut scopes = scopes.borrow_mut();
96                let Some(scope) = scopes.pop() else {
97                    return;
98                };
99                if scopes.is_empty() {
100                    unsafe {
101                        (*scope.runtime)
102                            .local_widget_state
103                            .retain_active(&scope.local_state_seen);
104                    }
105                } else if let Some(parent) = scopes.last_mut() {
106                    parent.local_state_seen.extend(scope.local_state_seen);
107                }
108            });
109        }
110    }
111
112    let _guard = PopGuard;
113    f()
114}
115
116unsafe fn next_portal_seq<S: GlobalState>(ctx: *mut ()) -> u64 {
117    let ctx = unsafe { &mut *ctx.cast::<BuildCtx<S>>() };
118    ctx.portal_seq_for_scoped_build()
119}
120
121unsafe fn register_runtime_reducer<S: GlobalState>(
122    ctx: *mut (),
123    action_id: crate::ActionId,
124    reducer: crate::BoxedReducer,
125) {
126    let ctx = unsafe { &mut *ctx.cast::<BuildCtx<S>>() };
127    ctx.register_runtime_reducer(action_id, reducer);
128}
129
130pub(crate) fn resolve_local_state<T>(
131    component: &'static str,
132    field: &'static str,
133    make_default: impl FnOnce() -> T,
134) -> crate::StateField<T>
135where
136    T: Clone + Send + Sync + 'static,
137{
138    let (runtime, key) = BUILD_SCOPES.with(|scopes| {
139        let mut scopes = scopes.borrow_mut();
140        let Some(scope) = scopes.last_mut() else {
141            panic!(
142                "Fission local widget state field `{}` on `{}` was accessed outside an active build pass",
143                field, component
144            );
145        };
146
147        let key_path = scope
148            .widget_id_stack
149            .iter()
150            .map(|id| id.as_u128().to_string())
151            .collect::<Vec<_>>();
152        let ordinal = if key_path.is_empty() {
153            let next = scope
154                .local_state_ordinals
155                .entry((component, field))
156                .and_modify(|next| *next += 1)
157                .or_insert(0);
158            *next
159        } else {
160            0
161        };
162        let key = crate::state::LocalStateKey::new_scoped(component, field, key_path, ordinal);
163        if !scope.local_state_seen.insert(key.clone()) {
164            panic!(
165                "Duplicate Fission local widget state identity for `{}` on `{}`.",
166                field, component
167            );
168        }
169        (scope.runtime, key)
170    });
171    let value = unsafe { &*runtime }
172        .local_widget_state
173        .get_or_insert_with(key.clone(), make_default);
174    crate::StateField::resolved(key, value)
175}
176
177pub fn with_widget_id<R>(id: crate::WidgetId, f: impl FnOnce() -> R) -> R {
178    let pushed = BUILD_SCOPES.with(|scopes| {
179        let mut scopes = scopes.borrow_mut();
180        if let Some(scope) = scopes.last_mut() {
181            scope.widget_id_stack.push(id);
182            true
183        } else {
184            false
185        }
186    });
187
188    struct PopGuard(bool);
189    impl Drop for PopGuard {
190        fn drop(&mut self) {
191            if self.0 {
192                BUILD_SCOPES.with(|scopes| {
193                    if let Some(scope) = scopes.borrow_mut().last_mut() {
194                        scope.widget_id_stack.pop();
195                    }
196                });
197            }
198        }
199    }
200
201    let _guard = PopGuard(pushed);
202    f()
203}
204
205pub fn current_widget_id() -> Option<crate::WidgetId> {
206    BUILD_SCOPES.with(|scopes| {
207        scopes
208            .borrow()
209            .last()
210            .and_then(|scope| scope.widget_id_stack.last().copied())
211    })
212}
213
214pub fn provide<T, R>(value: T, f: impl FnOnce() -> R) -> R
215where
216    T: Clone + Send + Sync + 'static,
217{
218    BUILD_SCOPES.with(|scopes| {
219        let mut scopes = scopes.borrow_mut();
220        let Some(scope) = scopes.last_mut() else {
221            panic!(
222                "Fission build provider `{}` was installed outside an active build pass",
223                type_name::<T>()
224            );
225        };
226        scope
227            .providers
228            .entry(TypeId::of::<T>())
229            .or_default()
230            .push(Box::new(value));
231    });
232
233    struct PopGuard<T: 'static> {
234        _provider: PhantomData<T>,
235    }
236    impl<T: 'static> Drop for PopGuard<T> {
237        fn drop(&mut self) {
238            BUILD_SCOPES.with(|scopes| {
239                if let Some(scope) = scopes.borrow_mut().last_mut() {
240                    let provider_type = TypeId::of::<T>();
241                    if let Some(values) = scope.providers.get_mut(&provider_type) {
242                        values.pop();
243                        if values.is_empty() {
244                            scope.providers.remove(&provider_type);
245                        }
246                    }
247                }
248            });
249        }
250    }
251
252    let _guard = PopGuard::<T> {
253        _provider: PhantomData,
254    };
255    f()
256}
257
258pub fn try_read<T>() -> Option<T>
259where
260    T: Clone + Send + Sync + 'static,
261{
262    BUILD_SCOPES.with(|scopes| {
263        let scopes = scopes.borrow();
264        scopes.iter().rev().find_map(|scope| {
265            scope
266                .providers
267                .get(&TypeId::of::<T>())
268                .and_then(|values| values.last())
269                .and_then(|value| value.downcast_ref::<T>())
270                .cloned()
271        })
272    })
273}
274
275pub fn read<T>() -> T
276where
277    T: Clone + Send + Sync + 'static,
278{
279    try_read::<T>().unwrap_or_else(|| {
280        panic!(
281            "Fission build provider `{}` was not found in the active build scope",
282            type_name::<T>()
283        )
284    })
285}
286
287pub fn current<S>() -> (BuildCtxHandle<S>, ViewHandle<S>)
288where
289    S: GlobalState,
290{
291    assert_current_scope::<S>();
292    (
293        BuildCtxHandle {
294            _state: PhantomData,
295        },
296        ViewHandle {
297            _state: PhantomData,
298        },
299    )
300}
301
302pub fn try_register_video(registration: crate::registry::VideoRegistration) {
303    let video_nodes =
304        BUILD_SCOPES.with(|scopes| scopes.borrow().last().map(|scope| scope.video_nodes));
305    if let Some(video_nodes) = video_nodes {
306        unsafe {
307            (*video_nodes).push(registration);
308        }
309    }
310}
311
312pub fn try_register_motion(declaration: crate::motion::MotionDeclaration) {
313    let motion_declarations = BUILD_SCOPES.with(|scopes| {
314        scopes
315            .borrow()
316            .last()
317            .map(|scope| scope.motion_declarations)
318    });
319    if let Some(motion_declarations) = motion_declarations {
320        unsafe {
321            (*motion_declarations).push(declaration);
322        }
323    }
324}
325
326pub fn try_current_runtime_state() -> Option<&'static crate::RuntimeState> {
327    BUILD_SCOPES.with(|scopes| {
328        scopes
329            .borrow()
330            .last()
331            .map(|scope| unsafe { &*scope.runtime })
332    })
333}
334
335fn requested_common_scope<S: GlobalState>() -> bool {
336    TypeId::of::<S>() == TypeId::of::<()>()
337}
338
339fn assert_current_scope<S: GlobalState>() {
340    BUILD_SCOPES.with(|scopes| {
341        let scopes = scopes.borrow();
342        if requested_common_scope::<S>() {
343            if scopes.is_empty() {
344                panic!(
345                    "Fission build context for `{}` requested outside an active build pass",
346                    type_name::<S>()
347                );
348            }
349            return;
350        }
351
352        let Some(scope) = scopes
353            .iter()
354            .rev()
355            .find(|scope| scope.state_type == TypeId::of::<S>())
356        else {
357            panic!(
358                "Fission build context for `{}` requested outside an active build pass",
359                type_name::<S>()
360            );
361        };
362        let _ = scope.state_name;
363    });
364}
365
366fn exact_scope_index<S: GlobalState>(scopes: &[BuildScope]) -> Option<usize> {
367    scopes
368        .iter()
369        .enumerate()
370        .rev()
371        .find_map(|(index, scope)| (scope.state_type == TypeId::of::<S>()).then_some(index))
372}
373
374impl<S: GlobalState> BuildCtxHandle<S> {
375    fn with_exact_ctx<R>(&self, f: impl FnOnce(&mut BuildCtx<S>) -> R) -> R {
376        let ctx = BUILD_SCOPES.with(|scopes| {
377            let scopes = scopes.borrow();
378            let Some(index) = exact_scope_index::<S>(&scopes) else {
379                panic!(
380                    "Fission build context for `{}` requested outside an active build pass",
381                    type_name::<S>()
382                );
383            };
384            scopes[index].ctx.cast::<BuildCtx<S>>()
385        });
386        // Build scopes are entered and exited synchronously by the shell.
387        // Handles only resolve while that scope is active, so this raw
388        // pointer never outlives the build pass that created it.
389        unsafe { f(&mut *ctx) }
390    }
391
392    pub fn bind<A, H>(&self, action: A, handler: H) -> crate::ActionEnvelope
393    where
394        A: crate::Action,
395        H: crate::registry::IntoHandler<S, A> + Send + Sync + 'static,
396    {
397        self.with_exact_ctx(|ctx| ctx.bind(action, handler))
398    }
399
400    pub fn register<A, H>(&self, handler: H)
401    where
402        A: crate::Action,
403        H: crate::registry::IntoHandler<S, A> + Send + Sync + 'static,
404    {
405        self.with_exact_ctx(|ctx| ctx.register::<A, H>(handler));
406    }
407
408    pub fn bind_local<T, A, H>(
409        &self,
410        action: A,
411        field: crate::StateField<T>,
412        handler: H,
413    ) -> crate::ActionEnvelope
414    where
415        T: crate::GlobalState + Clone + 'static,
416        A: crate::Action,
417        H: crate::registry::IntoHandler<T, A> + Send + Sync + 'static,
418    {
419        let action_id = field.action_id::<A>();
420        let field_key = field.key().clone();
421        let reducer: crate::BoxedReducer = Box::new(
422            move |app_states,
423                  envelope: &crate::ActionEnvelope,
424                  _target,
425                  _effects,
426                  _input,
427                  _callback_registry|
428                  -> anyhow::Result<()> {
429                let action: A = serde_json::from_slice(&envelope.payload).map_err(|error| {
430                    anyhow::anyhow!("Failed to deserialize local action: {error}")
431                })?;
432                let Some(store) = app_states
433                    .get_mut(&TypeId::of::<crate::state::LocalStateStore>())
434                    .and_then(|state| state.downcast_mut::<crate::state::LocalStateStore>())
435                else {
436                    anyhow::bail!("Fission local widget state store is not registered in Runtime");
437                };
438                let mut effects_builder = crate::Effects::<T>::new_headless(0);
439                let mut reducer_ctx = crate::ReducerContext {
440                    effects: &mut effects_builder,
441                    input: _input,
442                };
443                store.update::<T>(&field_key, |value| {
444                    handler.call(value, action, &mut reducer_ctx)
445                })?;
446                _effects.extend(effects_builder.out);
447                Ok(())
448            },
449        );
450
451        let (ctx, register_runtime_reducer) = BUILD_SCOPES.with(|scopes| {
452            let scopes = scopes.borrow();
453            let Some(scope) = scopes.last() else {
454                panic!(
455                    "Fission build context for `{}` requested outside an active build pass",
456                    type_name::<S>()
457                );
458            };
459            (scope.ctx, scope.register_runtime_reducer)
460        });
461        unsafe {
462            register_runtime_reducer(ctx, action_id, reducer);
463        }
464
465        crate::ActionEnvelope {
466            id: action_id,
467            payload: action.encode(),
468        }
469    }
470
471    pub fn register_motion(&self, declaration: crate::motion::MotionDeclaration) {
472        let motion_declarations = BUILD_SCOPES.with(|scopes| {
473            let scopes = scopes.borrow();
474            let Some(scope) = scopes.last() else {
475                panic!(
476                    "Fission build context for `{}` requested outside an active build pass",
477                    type_name::<S>()
478                );
479            };
480            scope.motion_declarations
481        });
482        unsafe {
483            (*motion_declarations).push(declaration);
484        }
485    }
486
487    pub fn register_video(&self, registration: crate::registry::VideoRegistration) {
488        let video_nodes = BUILD_SCOPES.with(|scopes| {
489            let scopes = scopes.borrow();
490            let Some(scope) = scopes.last() else {
491                panic!(
492                    "Fission build context for `{}` requested outside an active build pass",
493                    type_name::<S>()
494                );
495            };
496            scope.video_nodes
497        });
498        unsafe {
499            (*video_nodes).push(registration);
500        }
501    }
502
503    pub fn register_web_view(&self, registration: crate::registry::WebRegistration) {
504        let web_nodes = BUILD_SCOPES.with(|scopes| {
505            let scopes = scopes.borrow();
506            let Some(scope) = scopes.last() else {
507                panic!(
508                    "Fission build context for `{}` requested outside an active build pass",
509                    type_name::<S>()
510                );
511            };
512            scope.web_nodes
513        });
514        unsafe {
515            (*web_nodes).push(registration);
516        }
517    }
518
519    pub fn with_resources<R>(
520        &self,
521        f: impl FnOnce(&mut crate::registry::ResourceRegistry) -> R,
522    ) -> R {
523        let resources = BUILD_SCOPES.with(|scopes| {
524            let scopes = scopes.borrow();
525            let Some(scope) = scopes.last() else {
526                panic!(
527                    "Fission build context for `{}` requested outside an active build pass",
528                    type_name::<S>()
529                );
530            };
531            scope.resources
532        });
533        unsafe { f(&mut *resources) }
534    }
535
536    pub fn register_portal(&self, node: crate::Widget) {
537        self.register_portal_with_layer(crate::PortalLayer::Default, None, node);
538    }
539
540    pub fn register_portal_with_id(&self, id: crate::WidgetId, node: crate::Widget) {
541        self.register_portal_with_layer(crate::PortalLayer::Default, Some(id), node);
542    }
543
544    pub fn register_portal_with_layer(
545        &self,
546        layer: crate::PortalLayer,
547        id: Option<crate::WidgetId>,
548        node: crate::Widget,
549    ) {
550        let (ctx, portals, next_portal_seq) = BUILD_SCOPES.with(|scopes| {
551            let scopes = scopes.borrow();
552            let Some(scope) = scopes.last() else {
553                panic!(
554                    "Fission build context for `{}` requested outside an active build pass",
555                    type_name::<S>()
556                );
557            };
558            (scope.ctx, scope.portals, scope.next_portal_seq)
559        });
560        unsafe {
561            let seq = next_portal_seq(ctx);
562            (*portals).push(crate::registry::PortalEntry {
563                layer,
564                seq,
565                id,
566                node,
567            });
568        }
569    }
570
571    pub fn video_controls(&self, target: crate::WidgetId) -> crate::registry::VideoControlCtx {
572        self.with_exact_ctx(|ctx| ctx.video_controls(target))
573    }
574}
575
576impl<S: GlobalState> ViewHandle<S> {
577    fn with_common_scope<R>(&self, f: impl FnOnce(&BuildScope) -> R) -> R {
578        BUILD_SCOPES.with(|scopes| {
579            let scopes = scopes.borrow();
580            let Some(scope) = scopes.last() else {
581                panic!(
582                    "Fission view for `{}` requested outside an active build pass",
583                    type_name::<S>()
584                );
585            };
586            f(scope)
587        })
588    }
589
590    pub fn state(&self) -> &S {
591        BUILD_SCOPES.with(|scopes| {
592            let scopes = scopes.borrow();
593            let Some(index) = exact_scope_index::<S>(&scopes) else {
594                panic!(
595                    "Fission view state for `{}` requested outside an active build pass",
596                    type_name::<S>()
597                );
598            };
599            unsafe { (*scopes[index].view.cast::<View<'_, S>>()).state }
600        })
601    }
602
603    pub fn runtime(&self) -> &crate::RuntimeState {
604        self.with_common_scope(|scope| unsafe { &*scope.runtime })
605    }
606
607    pub fn env(&self) -> &crate::Env {
608        self.with_common_scope(|scope| unsafe { &*scope.env })
609    }
610
611    pub fn layout(&self) -> Option<&crate::LayoutSnapshot> {
612        self.with_common_scope(|scope| unsafe { scope.layout.map(|layout| &*layout) })
613    }
614
615    pub fn theme(&self) -> &fission_theme::Theme {
616        &self.env().theme
617    }
618
619    pub fn i18n(&self) -> &fission_i18n::I18nRegistry {
620        &self.env().i18n
621    }
622
623    pub fn get_rect(&self, id: crate::WidgetId) -> Option<crate::LayoutRect> {
624        let node_id: fission_ir::WidgetId = id.into();
625        self.layout()
626            .and_then(|layout| layout.get_node_rect(node_id))
627    }
628
629    pub fn get_constraints(&self, id: crate::WidgetId) -> Option<crate::BoxConstraints> {
630        let node_id: fission_ir::WidgetId = id.into();
631        self.layout()
632            .and_then(|layout| layout.get_node_constraints(node_id))
633    }
634
635    pub fn viewport_size(&self) -> crate::LayoutSize {
636        self.env().viewport_size
637    }
638
639    pub fn select<R>(&self, selector: impl FnOnce(&S) -> R) -> R {
640        selector(self.state())
641    }
642
643    pub fn select_with<T: crate::view::Selector<S>>(&self) -> T::Output {
644        T::select(*self)
645    }
646
647    pub fn global(&self) -> <S as crate::view::FissionViewField>::View<'_>
648    where
649        S: crate::view::FissionViewField,
650    {
651        <S as crate::view::FissionViewField>::view_field(self.state())
652    }
653
654    pub fn motion_value(
655        &self,
656        widget_id: crate::WidgetId,
657        property: crate::MotionPropertyId,
658    ) -> crate::MotionValue {
659        self.runtime()
660            .motion
661            .values
662            .get(&(widget_id, property.clone()))
663            .cloned()
664            .unwrap_or_else(|| property.default_value())
665    }
666
667    pub fn motion_scalar(
668        &self,
669        widget_id: crate::WidgetId,
670        property: crate::MotionPropertyId,
671    ) -> f32 {
672        self.runtime().motion.scalar_value(widget_id, property)
673    }
674
675    pub fn video_state(&self, widget_id: crate::WidgetId) -> Option<&crate::env::VideoState> {
676        self.runtime().video.states.get(&widget_id)
677    }
678}