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