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