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 unsafe { f(&mut *ctx) }
407 }
408
409 pub fn bind<A, H>(&self, action: A, handler: H) -> crate::ActionEnvelope
410 where
411 A: crate::Action,
412 H: crate::registry::IntoHandler<S, A> + Send + Sync + 'static,
413 {
414 self.with_exact_ctx(|ctx| ctx.bind(action, handler))
415 }
416
417 pub fn register<A, H>(&self, handler: H)
418 where
419 A: crate::Action,
420 H: crate::registry::IntoHandler<S, A> + Send + Sync + 'static,
421 {
422 self.with_exact_ctx(|ctx| ctx.register::<A, H>(handler));
423 }
424
425 pub fn bind_local<T, A, H>(
426 &self,
427 action: A,
428 field: crate::StateField<T>,
429 handler: H,
430 ) -> crate::ActionEnvelope
431 where
432 T: crate::GlobalState + Clone + 'static,
433 A: crate::Action,
434 H: crate::registry::IntoHandler<T, A> + Send + Sync + 'static,
435 {
436 let action_id = field.action_id::<A>();
437 let field_key = field.key().clone();
438 let reducer: crate::BoxedReducer = Box::new(
439 move |app_states,
440 envelope: &crate::ActionEnvelope,
441 _target,
442 _effects,
443 _input,
444 _callback_registry|
445 -> anyhow::Result<()> {
446 let action: A = serde_json::from_slice(&envelope.payload).map_err(|error| {
447 anyhow::anyhow!("Failed to deserialize local action: {error}")
448 })?;
449 let Some(store) = app_states
450 .get_mut(&TypeId::of::<crate::state::LocalStateStore>())
451 .and_then(|state| state.downcast_mut::<crate::state::LocalStateStore>())
452 else {
453 anyhow::bail!("Fission local widget state store is not registered in Runtime");
454 };
455 let mut effects_builder = crate::Effects::<T>::new_headless(0);
456 let mut reducer_ctx = crate::ReducerContext {
457 effects: &mut effects_builder,
458 input: _input,
459 };
460 store.update::<T>(&field_key, |value| {
461 handler.call(value, action, &mut reducer_ctx)
462 })?;
463 _effects.extend(effects_builder.out);
464 Ok(())
465 },
466 );
467
468 let (ctx, register_runtime_reducer) = BUILD_SCOPES.with(|scopes| {
469 let scopes = scopes.borrow();
470 let Some(scope) = scopes.last() else {
471 panic!(
472 "Fission build context for `{}` requested outside an active build pass",
473 type_name::<S>()
474 );
475 };
476 (scope.ctx, scope.register_runtime_reducer)
477 });
478 unsafe {
479 register_runtime_reducer(ctx, action_id, reducer);
480 }
481
482 crate::ActionEnvelope {
483 id: action_id,
484 payload: action.encode(),
485 }
486 }
487
488 pub fn register_motion(&self, declaration: crate::motion::MotionDeclaration) {
489 let motion_declarations = BUILD_SCOPES.with(|scopes| {
490 let scopes = scopes.borrow();
491 let Some(scope) = scopes.last() else {
492 panic!(
493 "Fission build context for `{}` requested outside an active build pass",
494 type_name::<S>()
495 );
496 };
497 scope.motion_declarations
498 });
499 unsafe {
500 (*motion_declarations).push(declaration);
501 }
502 }
503
504 pub fn register_video(&self, registration: crate::registry::VideoRegistration) {
505 let video_nodes = BUILD_SCOPES.with(|scopes| {
506 let scopes = scopes.borrow();
507 let Some(scope) = scopes.last() else {
508 panic!(
509 "Fission build context for `{}` requested outside an active build pass",
510 type_name::<S>()
511 );
512 };
513 scope.video_nodes
514 });
515 unsafe {
516 (*video_nodes).push(registration);
517 }
518 }
519
520 pub fn register_web_view(&self, registration: crate::registry::WebRegistration) {
521 let web_nodes = BUILD_SCOPES.with(|scopes| {
522 let scopes = scopes.borrow();
523 let Some(scope) = scopes.last() else {
524 panic!(
525 "Fission build context for `{}` requested outside an active build pass",
526 type_name::<S>()
527 );
528 };
529 scope.web_nodes
530 });
531 unsafe {
532 (*web_nodes).push(registration);
533 }
534 }
535
536 pub fn with_resources<R>(
537 &self,
538 f: impl FnOnce(&mut crate::registry::ResourceRegistry) -> R,
539 ) -> R {
540 let resources = BUILD_SCOPES.with(|scopes| {
541 let scopes = scopes.borrow();
542 let Some(scope) = scopes.last() else {
543 panic!(
544 "Fission build context for `{}` requested outside an active build pass",
545 type_name::<S>()
546 );
547 };
548 scope.resources
549 });
550 unsafe { f(&mut *resources) }
551 }
552
553 pub fn register_portal(&self, node: crate::Widget) {
554 self.register_portal_with_layer(crate::PortalLayer::Default, None, node);
555 }
556
557 pub fn register_portal_with_id(&self, id: crate::WidgetId, node: crate::Widget) {
558 self.register_portal_with_layer(crate::PortalLayer::Default, Some(id), node);
559 }
560
561 pub fn register_portal_with_layer(
562 &self,
563 layer: crate::PortalLayer,
564 id: Option<crate::WidgetId>,
565 node: crate::Widget,
566 ) {
567 let (ctx, portals, next_portal_seq) = BUILD_SCOPES.with(|scopes| {
568 let scopes = scopes.borrow();
569 let Some(scope) = scopes.last() else {
570 panic!(
571 "Fission build context for `{}` requested outside an active build pass",
572 type_name::<S>()
573 );
574 };
575 (scope.ctx, scope.portals, scope.next_portal_seq)
576 });
577 unsafe {
578 let seq = next_portal_seq(ctx);
579 (*portals).push(crate::registry::PortalEntry {
580 layer,
581 seq,
582 id,
583 node,
584 });
585 }
586 }
587
588 pub fn video_controls(&self, target: crate::WidgetId) -> crate::registry::VideoControlCtx {
589 self.with_exact_ctx(|ctx| ctx.video_controls(target))
590 }
591}
592
593impl<S: GlobalState> ViewHandle<S> {
594 fn with_common_scope<R>(&self, f: impl FnOnce(&BuildScope) -> R) -> R {
595 BUILD_SCOPES.with(|scopes| {
596 let scopes = scopes.borrow();
597 let Some(scope) = scopes.last() else {
598 panic!(
599 "Fission view for `{}` requested outside an active build pass",
600 type_name::<S>()
601 );
602 };
603 f(scope)
604 })
605 }
606
607 pub fn state(&self) -> &S {
608 BUILD_SCOPES.with(|scopes| {
609 let scopes = scopes.borrow();
610 let Some(index) = exact_scope_index::<S>(&scopes) else {
611 panic!(
612 "Fission view state for `{}` requested outside an active build pass",
613 type_name::<S>()
614 );
615 };
616 unsafe { (*scopes[index].view.cast::<View<'_, S>>()).state }
617 })
618 }
619
620 pub fn runtime(&self) -> &crate::RuntimeState {
621 self.with_common_scope(|scope| unsafe { &*scope.runtime })
622 }
623
624 pub fn env(&self) -> &crate::Env {
625 self.with_common_scope(|scope| unsafe { &*scope.env })
626 }
627
628 pub fn layout(&self) -> Option<&crate::LayoutSnapshot> {
629 self.with_common_scope(|scope| unsafe { scope.layout.map(|layout| &*layout) })
630 }
631
632 pub fn theme(&self) -> &fission_theme::Theme {
633 &self.env().theme
634 }
635
636 pub fn i18n(&self) -> &fission_i18n::I18nRegistry {
637 &self.env().i18n
638 }
639
640 pub fn get_rect(&self, id: crate::WidgetId) -> Option<crate::LayoutRect> {
641 let node_id: fission_ir::WidgetId = id.into();
642 self.layout()
643 .and_then(|layout| layout.get_node_rect(node_id))
644 }
645
646 pub fn get_constraints(&self, id: crate::WidgetId) -> Option<crate::BoxConstraints> {
647 let node_id: fission_ir::WidgetId = id.into();
648 self.layout()
649 .and_then(|layout| layout.get_node_constraints(node_id))
650 }
651
652 pub fn viewport_size(&self) -> crate::LayoutSize {
653 self.env().viewport_size
654 }
655
656 pub fn select<R>(&self, selector: impl FnOnce(&S) -> R) -> R {
657 selector(self.state())
658 }
659
660 pub fn select_with<T: crate::view::Selector<S>>(&self) -> T::Output {
661 T::select(*self)
662 }
663
664 pub fn global(&self) -> <S as crate::view::FissionViewField>::View<'_>
665 where
666 S: crate::view::FissionViewField,
667 {
668 <S as crate::view::FissionViewField>::view_field(self.state())
669 }
670
671 pub fn motion_value(
672 &self,
673 widget_id: crate::WidgetId,
674 property: crate::MotionPropertyId,
675 ) -> crate::MotionValue {
676 self.runtime()
677 .motion
678 .values
679 .get(&(widget_id, property.clone()))
680 .cloned()
681 .unwrap_or_else(|| property.default_value())
682 }
683
684 pub fn motion_scalar(
685 &self,
686 widget_id: crate::WidgetId,
687 property: crate::MotionPropertyId,
688 ) -> f32 {
689 self.runtime().motion.scalar_value(widget_id, property)
690 }
691
692 pub fn video_state(&self, widget_id: crate::WidgetId) -> Option<&crate::env::VideoState> {
693 self.runtime().video.states.get(&widget_id)
694 }
695}