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 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}