1use std::cell::RefCell;
5use std::collections::HashMap;
6use std::rc::{Rc, Weak};
7
8use tilo_resource::{
9 ActionDecl, Binds, Collection, Edit, Effect, Id, Model, Params, Path, Resource, Segment,
10 Settable,
11};
12use ui_core::LocalState;
13use ui_core::i18n::LabelKey;
14use ui_core::record::RecordSaveStatus;
15use ui_core::widget::{Button, Checkbox, Element, Text, TextInput};
16
17use super::controls::{ActionControl, Actions};
18use super::dispatch::{Dispatcher, Submit};
19use super::each::Each;
20use super::effect::{EffectAnswer, EffectCx, EffectStatus, Effects};
21use super::env::RecordEnv;
22use super::form::{Attached, Form, FormField, Forms, RecordField};
23use super::live::Live;
24use super::reactive::{Reads, Updaters};
25use super::scenario::Run;
26use super::settings::ViewStorage;
27use super::stage::Stages;
28use super::surface::{ResourceSurface, SurfaceCx, SurfaceHost};
29use super::switch::Switch;
30
31fn disabled(mut button: Button) -> Button {
32 button.set_enabled(false);
33 button
34}
35
36type Bind<R> = Box<dyn Fn(&<R as Resource>::State) -> Vec<ActionDecl<R>>>;
37type BindList<R> = Vec<(Weak<()>, Bind<R>)>;
38
39pub(crate) struct Bindings<R: Resource>(Rc<RefCell<BindList<R>>>);
43
44impl<R: Resource> Clone for Bindings<R> {
45 fn clone(&self) -> Self {
46 Self(Rc::clone(&self.0))
47 }
48}
49
50impl<R: Resource> Bindings<R> {
51 pub(crate) fn new() -> Self {
52 Self(Rc::default())
53 }
54
55 pub(crate) fn declarations(&self, state: &R::State) -> Vec<ActionDecl<R>> {
57 let mut bindings = self.0.borrow_mut();
58 bindings.retain(|(alive, _)| alive.strong_count() > 0);
59 bindings.iter().flat_map(|(_, bind)| bind(state)).collect()
60 }
61}
62
63pub(crate) type Focus<R, T> = Rc<dyn for<'a> Fn(&'a <R as Resource>::State) -> Option<&'a T>>;
66
67pub struct ViewCx<R: Resource, T: 'static = <R as Resource>::State> {
73 pub(crate) env: RecordEnv<R>,
74 pub(crate) focus: Focus<R, T>,
75 pub(crate) updaters: Updaters<R>,
76 pub(crate) gate: Option<super::reactive::Gate>,
79 pub(crate) action_bindings: Actions<R>,
80 pub(crate) item: Option<Id>,
81 pub(crate) path: Option<Vec<Segment>>,
85 pub(crate) controls: Rc<RefCell<HashMap<String, u32>>>,
86 pub(crate) forms: Forms,
88 pub(crate) stages: Stages,
89 pub(crate) feeds: super::history::HistoryFeeds,
90 pub(crate) bindings: Bindings<R>,
91}
92
93impl<R: Resource, T: 'static> std::ops::Deref for ViewCx<R, T> {
94 type Target = RecordEnv<R>;
95 fn deref(&self) -> &RecordEnv<R> {
96 &self.env
97 }
98}
99
100impl<R: Resource> ViewCx<R> {
101 pub(crate) fn root(
104 live: Live<R>,
105 alive: &Rc<()>,
106 dispatch: Dispatcher<R>,
107 published: tilo_actions::Resource,
108 run: Run,
109 storage: Rc<ViewStorage>,
110 ) -> Self {
111 let mut published = published;
113 for spec in super::history::specs() {
114 if !published.actions.iter().any(|action| action.id == spec.id) {
115 published = published.with_action(spec);
116 }
117 }
118 let app = dispatch.app_id();
119 Self {
120 env: RecordEnv {
121 live,
122 alive: Rc::downgrade(alive),
123 dispatch,
124 saved: LocalState::new(RecordSaveStatus::Saved),
125 run,
126 storage,
127 effects: Effects::inert(),
128 },
129 focus: Rc::new(|state| Some(state)),
130 updaters: Updaters::new(),
131 gate: None,
132 action_bindings: Actions::new(app, Rc::new(published)),
133 item: None,
134 path: Some(Vec::new()),
135 controls: Rc::default(),
136 forms: Forms::default(),
137 stages: Stages::new(),
138 feeds: super::history::HistoryFeeds::default(),
139 bindings: Bindings::new(),
140 }
141 }
142}
143
144impl<R: Resource, T: 'static> ViewCx<R, T> {
145 pub fn live(&self) -> &Live<R> {
147 &self.live
148 }
149
150 pub fn permission_state(&self, need: tilo_actions::ActionNeed) -> LocalState<bool> {
156 self.dispatch.permission_state(need)
157 }
158
159 pub fn govern_state(&self) -> LocalState<bool> {
162 self.dispatch.govern_state()
163 }
164
165 pub fn read_only_state(&self) -> LocalState<bool> {
168 self.dispatch.read_only_state()
169 }
170
171 pub fn item(&self) -> Option<&Id> {
173 self.item.as_ref()
174 }
175
176 pub fn history(&self, last: usize) -> LocalState<Vec<super::history::HistoryVersion>> {
181 self.feeds.follow(self.alive.clone(), last)
182 }
183
184 pub fn read<V>(&self, read: impl FnOnce(Option<&T>) -> V) -> V {
187 self.live.with(|state| read((self.focus)(state)))
188 }
189
190 pub fn local<V: Send + Sync + 'static>(&self, initial: V) -> LocalState<V> {
192 LocalState::new(initial)
193 }
194
195 pub fn on_open<O: tilo_resource::Outcome>(&self, mut open: impl FnMut(&str) -> O + 'static) {
204 self.stages.push_opener(
205 self.alive.clone(),
206 Box::new(move |anchor| open(anchor).into_result().map_err(|refusal| refusal.0)),
207 );
208 }
209
210 pub fn on_open_item(
215 &self,
216 mut open: impl FnMut(&Id) -> Result<(), tilo_resource::Refusal> + 'static,
217 ) {
218 self.stages.push_opener(
219 self.alive.clone(),
220 Box::new(move |anchor| {
221 let (_, item) = tilo_resource::items::item_of_resource::<R>(anchor)
222 .ok_or_else(|| format!("`{anchor}` is not an item of {}", R::LABEL))?;
223 open(&item).map_err(|refusal| refusal.0)
224 }),
225 );
226 }
227
228 pub fn open_search(&self, request: &ui_core::search::SearchRequest) {
232 self.dispatch.open_search(request);
233 }
234
235 pub fn search_button(&self, request: ui_core::search::SearchRequest) -> Button {
238 ui_core::search::search_button(request)
239 }
240
241 pub fn on_stage(&self, stage: impl FnMut(Option<&str>) + 'static) {
248 self.stages.push(self.alive.clone(), Box::new(stage));
249 }
250
251 fn reader<V: Default + 'static>(
252 &self,
253 read: impl Fn(&T) -> V + 'static,
254 ) -> impl Fn(&R::State) -> V + 'static {
255 let focus = Rc::clone(&self.focus);
256 move |state| (focus)(state).map(&read).unwrap_or_default()
257 }
258
259 fn control_id(&self, action: &str) -> String {
262 let base = match &self.item {
263 Some(item) => format!("{action}-{item}"),
264 None => action.to_owned(),
265 };
266 let mut seen = self.controls.borrow_mut();
267 let count = seen.entry(base.clone()).or_insert(0);
268 *count = count.saturating_add(1);
269 if *count == 1 {
270 base
271 } else {
272 format!("{base}-{count}")
273 }
274 }
275
276 fn follow<V: PartialEq + Default + Clone + 'static>(
279 &self,
280 read: impl Fn(&T) -> V + 'static,
281 sink: impl FnMut(V) + 'static,
282 ) -> V {
283 self.follow_state(Reads::Always, self.reader(read), sink)
284 }
285
286 fn follow_state<V: PartialEq + Clone + 'static>(
289 &self,
290 reads: Reads,
291 read: impl Fn(&R::State) -> V + 'static,
292 mut sink: impl FnMut(V) + 'static,
293 ) -> V {
294 let initial = self.live.with(&read);
295 let mut last = initial.clone();
296 self.updaters.push(
297 self.alive.clone(),
298 self.gate.clone(),
299 reads,
300 Box::new(move |state| {
301 let next = read(state);
302 if next != last {
303 last.clone_from(&next);
304 sink(next);
305 }
306 }),
307 );
308 initial
309 }
310
311 pub fn text(&self, read: impl Fn(&T) -> String + 'static) -> Text {
313 let live = self.live.clone();
314 let read = self.reader(read);
315 Text::dynamic(move || live.with(&read))
316 }
317
318 pub fn button(&self, label: impl Into<LabelKey>, submit: impl Submit<R>) -> Button {
322 let label: LabelKey = label.into();
323 let id = self.control_id(submit.action_id());
324 let button = Button::new(&id, label.as_str());
325 match submit.control_binding(&self.actions()) {
326 Some(binding) => button.on_action(binding),
327 None => disabled(button),
328 }
329 }
330
331 pub fn form<P: Params<Resource = R>>(&self) -> Form<P> {
338 self.forms.get::<P>()
339 }
340
341 pub fn input<M>(&self, field: impl Bindable<R, String, M>) -> TextInput {
345 match field.attach(self, Path::replace) {
346 Attached::Record { id, state, binding } => {
347 let input = TextInput::from_state(&state).with_id(&id);
348 match binding {
349 Some(binding) => input.on_action(*binding),
350 None => input.disabled(true),
351 }
352 }
353 Attached::Local { id, state } => TextInput::from_state(&state).with_id(&id),
354 }
355 }
356
357 pub fn checkbox<M>(
360 &self,
361 label: impl Into<LabelKey>,
362 field: impl Bindable<R, bool, M>,
363 ) -> Checkbox {
364 let label: LabelKey = label.into();
365 match field.attach(self, Path::set) {
366 Attached::Record { id, state, binding } => {
367 let checkbox = Checkbox::from_state(&state).with_id(&id).with_label(label);
368 match binding {
369 Some(binding) => checkbox.on_action(*binding),
370 None => checkbox.with_enabled(false),
371 }
372 }
373 Attached::Local { id, state } => {
374 Checkbox::from_state(&state).with_id(&id).with_label(label)
375 }
376 }
377 }
378
379 pub fn control<V, C, M>(
383 &self,
384 make: impl FnOnce(&LocalState<V>) -> C,
385 field: impl Bindable<R, V, M>,
386 ) -> C
387 where
388 V: Settable + 'static,
389 C: ActionControl<V>,
390 {
391 match field.attach(self, Path::set) {
392 Attached::Record { state, binding, .. } => {
393 let control = make(&state);
394 match binding {
395 Some(binding) => control.bind_action(*binding),
396 None => control.unavailable(),
397 }
398 }
399 Attached::Local { state, .. } => make(&state),
400 }
401 }
402
403 pub fn bind_declarations(&self, bind: impl Fn(&R::State) -> Vec<ActionDecl<R>> + 'static) {
413 if let Some(gate) = &self.gate {
415 gate.read_while_unmounted();
416 }
417 self.bindings
418 .0
419 .borrow_mut()
420 .push((self.alive.clone(), Box::new(bind)));
421 }
422
423 pub fn actions(&self) -> Actions<R> {
426 self.action_bindings.clone()
427 }
428
429 pub fn value<V>(&self, read: impl Fn(&T) -> V + 'static) -> LocalState<V>
432 where
433 V: PartialEq + Default + Clone + 'static,
434 {
435 let state = LocalState::new(V::default());
436 let sink = state.clone();
437 let initial = self.follow(read, move |value| sink.set(value));
438 state.set(initial);
439 state
440 }
441
442 pub fn control_with<V, C>(
450 &self,
451 make: impl FnOnce(&LocalState<V>) -> C,
452 read: impl Fn(&T) -> V + 'static,
453 action: impl Binds<R, V>,
454 ) -> C
455 where
456 V: PartialEq + Default + Clone + 'static,
457 C: ActionControl<V>,
458 {
459 let control = make(&self.value(read));
460 self.actions().bind(control, action)
461 }
462
463 pub fn switch<K>(
467 &self,
468 key: &LocalState<K>,
469 part: impl Fn(&mut ViewCx<R, T>, &K) -> Element + 'static,
470 ) -> Element
471 where
472 K: Clone + PartialEq + 'static,
473 {
474 let env = self.env.clone();
475 let focus = Rc::clone(&self.focus);
476 let updaters = self.updaters.clone();
477 let gate = self.gate.clone();
478 let action_bindings = self.action_bindings.clone();
479 let item = self.item.clone();
480 let path = self.path.clone();
481 let stages = self.stages.clone();
482 let feeds = self.feeds.clone();
483 let bindings = self.bindings.clone();
484 Switch::keyed(
485 key.clone(),
486 Box::new(move |value: &K, alive: &Rc<()>| {
487 let mut cx = ViewCx {
488 env: env.living(alive),
489 focus: Rc::clone(&focus),
490 updaters: updaters.clone(),
491 gate: gate.clone(),
492 action_bindings: action_bindings.clone(),
493 item: item.clone(),
494 path: path.clone(),
495 controls: Rc::default(),
497 forms: Forms::default(),
498 stages: stages.clone(),
499 feeds: feeds.clone(),
500 bindings: bindings.clone(),
501 };
502 part(&mut cx, value)
503 }),
504 )
505 }
506
507 pub fn surface<S, E>(&self, mut surface: S) -> Element
511 where
512 S: ResourceSurface<R, E> + 'static,
513 E: Effect,
514 {
515 let jobs = self.effects.surface_jobs();
516 surface.attach(SurfaceCx {
517 env: self.env.clone(),
518 jobs: jobs.clone(),
519 actions: self.actions(),
520 });
521 surface.present(&self.live.get());
522 let host = SurfaceHost::new(
523 self.live.clone(),
524 self.dispatch.clone(),
525 &self.effects,
526 jobs,
527 self.alive.clone(),
528 surface,
529 );
530 let pending = host.staging();
531 self.stages.push(
532 self.alive.clone(),
533 Box::new(move |scenario| {
534 *pending.borrow_mut() = Some(scenario.map(str::to_owned));
535 }),
536 );
537 super::surface::HostedSurface::element(Box::new(host))
538 }
539
540 pub fn effects<E: Effect>(
552 &self,
553 handle: impl FnMut(E, &mut EffectCx<R>) -> EffectAnswer + 'static,
554 ) -> EffectsHandle {
555 let status: StatusSource = Rc::default();
556 if let Some(gate) = &self.gate {
559 gate.read_while_unmounted();
560 }
561 let source = Rc::clone(&status);
564 self.effects.publish::<E>(self.alive.clone(), move || {
565 let now = source
566 .borrow()
567 .as_ref()
568 .map(|status| status())
569 .unwrap_or_default();
570 super::effect::published_specs::<E>(&now)
571 });
572 if self.effects.env().is_some() {
573 self.effects.handle(self.alive.clone(), handle);
574 }
575 EffectsHandle { status }
576 }
577
578 pub fn jobs(&self) -> super::Jobs<R> {
581 self.effects.view_jobs()
582 }
583
584 pub fn effect_button<E: Effect>(&self, label: impl Into<LabelKey>, effect: &E) -> Button {
587 let id = self.control_id(effect.id());
588 self.actions()
589 .bind_effect_invocation(Button::new(&id, label.into().as_str()), effect)
590 }
591}
592
593impl<R: Resource, T: Model + 'static> ViewCx<R, T> {
594 #[allow(
603 clippy::panic,
604 reason = "a row whose collection has no path is a mistake the first run of the view shows"
605 )]
606 pub fn paths(&self) -> T::Paths<R> {
607 let Some(path) = &self.path else {
608 panic!(
609 "this row's collection is not a field of the value `each` read it from; read it as `|value| &value.field`"
610 );
611 };
612 T::paths_at(Path::from_segments(path.clone()))
613 }
614
615 pub fn each<I: Model + 'static>(
624 &self,
625 items: impl Fn(&T) -> &Collection<I> + 'static,
626 row: impl Fn(&mut ViewCx<R, I>) -> Element + 'static,
627 ) -> Each<R> {
628 let items: ItemsOf<T, I> = Rc::new(items);
629 let located = self.path.as_ref().and_then(|base| {
630 self.read(|value| {
631 let value = value?;
632 let relative = locate(value, items(value))?;
633 let mut path = base.clone();
634 path.extend(relative);
635 Some(path)
636 })
637 });
638 let focus = Rc::clone(&self.focus);
639 let read_items = Rc::clone(&items);
640 let ids = move |state: &R::State| -> Vec<Id> {
641 (focus)(state).map_or_else(Vec::new, |value| read_items(value).ids().cloned().collect())
642 };
643 let stale = Rc::new(std::cell::Cell::new(false));
644 let mark = Rc::clone(&stale);
645 self.updaters.push(
646 self.alive.clone(),
647 self.gate.clone(),
648 located.clone().map_or(Reads::Always, Reads::Members),
649 Box::new(move |_| mark.set(true)),
650 );
651 let parent_focus = Rc::clone(&self.focus);
652 let env = self.env.clone();
653 let updaters = self.updaters.clone();
654 let action_bindings = self.action_bindings.clone();
655 let controls = Rc::clone(&self.controls);
656 let stages = self.stages.clone();
657 let feeds = self.feeds.clone();
658 let bindings = self.bindings.clone();
659 Each::new(
660 self.live.clone(),
661 self.dispatch.clone(),
662 Box::new(ids),
663 stale,
664 self.updaters.clone(),
665 Box::new(
666 move |id: &Id, alive: &Rc<()>, gate: &super::reactive::Gate| {
667 let parent = Rc::clone(&parent_focus);
668 let items = Rc::clone(&items);
669 let key = id.clone();
670 let focus: Focus<R, I> = Rc::new(move |state| {
671 (parent)(state).and_then(|value| items(value).get(&key))
672 });
673 let path = located.as_ref().map(|located| {
674 let mut path = located.clone();
675 path.push(Segment::Item(id.clone()));
676 path
677 });
678 let mut cx = ViewCx {
679 env: env.living(alive),
680 focus,
681 updaters: updaters.clone(),
682 gate: Some(gate.clone()),
683 action_bindings: action_bindings.clone(),
684 item: Some(id.clone()),
685 path,
686 controls: Rc::clone(&controls),
687 forms: Forms::default(),
688 stages: stages.clone(),
689 feeds: feeds.clone(),
690 bindings: bindings.clone(),
691 };
692 row(&mut cx)
693 },
694 ),
695 )
696 }
697}
698
699type ItemsOf<T, I> = Rc<dyn Fn(&T) -> &Collection<I>>;
701
702fn locate<T: Model + 'static, I: 'static>(
705 value: &T,
706 target: &Collection<I>,
707) -> Option<Vec<Segment>> {
708 fn walk<T: Model + 'static>(
709 value: &T,
710 shape: tilo_resource::Shape,
711 prefix: &mut Vec<Segment>,
712 target: *const u8,
713 ) -> bool {
714 for field in shape.fields() {
715 prefix.push(Segment::Field(field.key.to_owned()));
716 let found = match field.shape {
717 tilo_resource::Shape::Collection(_) => value
718 .get_at(prefix)
719 .is_some_and(|at| std::ptr::eq(std::ptr::from_ref(at).cast::<u8>(), target)),
720 tilo_resource::Shape::Record(_) => walk(value, field.shape, prefix, target),
721 tilo_resource::Shape::Value
722 | tilo_resource::Shape::Keyed(_)
723 | tilo_resource::Shape::Map(_) => false,
724 };
725 if found {
726 return true;
727 }
728 prefix.pop();
729 }
730 false
731 }
732 let mut path = Vec::new();
733 walk(
734 value,
735 T::SHAPE,
736 &mut path,
737 std::ptr::from_ref(target).cast::<u8>(),
738 )
739 .then_some(path)
740}
741
742impl<R, T: 'static> ViewCx<R, T>
744where
745 R: Resource<State = R> + Model,
746{
747 fn follow_path<V: PartialEq + Default + Clone + 'static>(
750 &self,
751 path: &Path<R, V>,
752 sink: impl FnMut(V) + 'static,
753 ) -> V {
754 let read = path.clone();
755 self.follow_state(
756 Reads::At(path.segments().to_vec()),
757 move |state| read.get(state).cloned().unwrap_or_default(),
758 sink,
759 )
760 }
761
762 fn path_control_id<V>(&self, path: &Path<R, V>) -> String {
765 let base = path
766 .segments()
767 .iter()
768 .map(ToString::to_string)
769 .collect::<Vec<_>>()
770 .join("-");
771 let mut seen = self.controls.borrow_mut();
772 let count = seen.entry(base.clone()).or_insert(0);
773 *count = count.saturating_add(1);
774 if *count == 1 {
775 base
776 } else {
777 format!("{base}-{count}")
778 }
779 }
780
781 pub fn value_at<V>(&self, path: &Path<R, V>) -> LocalState<V>
785 where
786 V: PartialEq + Default + Clone + 'static,
787 {
788 let state = LocalState::new(V::default());
789 let sink = state.clone();
790 let initial = self.follow_path(path, move |value| sink.set(value));
791 state.set(initial);
792 state
793 }
794
795 pub fn route_graph_text(
799 &self,
800 values: &ui_core::UiValues,
801 ui_path: &str,
802 path: Path<R, String>,
803 ) {
804 let shown = self.value_at(&path);
805 if let Some(binding) = self.actions().edit_binding(path, Path::replace) {
806 values.route(ui_path, shown, binding);
807 }
808 }
809
810 pub fn route_graph_bool(&self, values: &ui_core::UiValues, ui_path: &str, path: Path<R, bool>) {
813 let shown = self.value_at(&path);
814 if let Some(binding) = self.actions().edit_binding(path, Path::set) {
815 values.route_bool(ui_path, shown, binding);
816 }
817 }
818
819 pub fn control_map<V, I, C>(
825 &self,
826 make: impl FnOnce(&LocalState<I>) -> C,
827 path: Path<R, V>,
828 show: impl Fn(&V) -> I + 'static,
829 store: impl Fn(I) -> V + Send + Sync + 'static,
830 ) -> C
831 where
832 V: Settable + PartialEq + Default + Clone + 'static,
833 I: PartialEq + Clone + Send + Sync + 'static,
834 C: ActionControl<I>,
835 {
836 let read = path.clone();
837 let initial = show(&V::default());
838 let state = LocalState::new(initial);
839 let sink = state.clone();
840 let shown = self.follow_state(
841 Reads::At(path.segments().to_vec()),
842 move |record| show(&read.get(record).cloned().unwrap_or_default()),
843 move |value| sink.set(value),
844 );
845 state.set(shown);
846 let control = make(&state);
847 let binding = self
848 .actions()
849 .edit_binding(path.cast::<I>(), move |path, input| {
850 path.clone().cast::<V>().set(store(input))
851 });
852 match binding {
853 Some(binding) => control.bind_action(binding),
854 None => control.unavailable(),
855 }
856 }
857}
858
859pub trait Bindable<R: Resource, V, M>: Sized {
863 #[doc(hidden)]
864 fn attach<T: 'static>(
865 self,
866 cx: &ViewCx<R, T>,
867 edit: fn(&Path<R, V>, V) -> Edit<R>,
868 ) -> Attached<V>;
869}
870
871impl<R, V> Bindable<R, V, RecordField> for Path<R, V>
872where
873 R: Resource<State = R> + Model,
874 V: PartialEq + Default + Clone + Send + Sync + 'static,
875{
876 fn attach<T: 'static>(
877 self,
878 cx: &ViewCx<R, T>,
879 edit: fn(&Path<R, V>, V) -> Edit<R>,
880 ) -> Attached<V> {
881 let id = cx.path_control_id(&self);
882 let state = cx.value_at(&self);
883 let binding = cx.actions().edit_binding(self, edit).map(Box::new);
884 Attached::Record { id, state, binding }
885 }
886}
887
888impl<R, P, V> Bindable<R, V, FormField> for Path<P, V>
889where
890 R: Resource,
891 P: Params<Resource = R>,
892 V: serde::Serialize + Default + Send + Sync + 'static,
893{
894 fn attach<T: 'static>(
895 self,
896 cx: &ViewCx<R, T>,
897 _edit: fn(&Path<R, V>, V) -> Edit<R>,
898 ) -> Attached<V> {
899 let state = cx.form::<P>().field(&self);
900 let id = std::iter::once(P::ACTION.id.to_owned())
901 .chain(self.segments().iter().map(ToString::to_string))
902 .collect::<Vec<_>>()
903 .join("-");
904 Attached::Local { id, state }
905 }
906}
907
908type StatusSource = Rc<RefCell<Option<Box<dyn Fn() -> Vec<EffectStatus>>>>>;
909
910pub struct EffectsHandle {
912 status: StatusSource,
913}
914
915impl EffectsHandle {
916 pub fn status(self, status: impl Fn() -> Vec<EffectStatus> + 'static) {
920 *self.status.borrow_mut() = Some(Box::new(status));
921 }
922}