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#[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#[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#[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#[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 unsafe { f(&mut *ctx) }
490 }
491
492 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}