Skip to main content

fission_core/
registry.rs

1use crate::{
2    action::video::{
3        VideoPause, VideoPlay, VideoSeek, VideoSetMuted, VideoSetRate, VideoSetVolume, VideoStop,
4    },
5    async_runtime::{
6        JobRef, JobRequestPayload, JobSpec, ServiceBindings, ServiceSlot, ServiceSpec,
7        ServiceStartPayload,
8    },
9    context::{Effects, ReducerContext},
10    effect::{ActionInput, Effect, EffectEnvelope},
11    ui::Widget,
12    Action, ActionEnvelope, ActionId, BoxedReducer, GlobalState,
13};
14use anyhow::{anyhow, Result};
15use fission_ir::WidgetId;
16use serde::Serialize;
17use std::any::TypeId;
18use std::collections::{BTreeMap, HashMap};
19use std::sync::Arc;
20
21/// The canonical 3-argument handler signature for modern reducers.
22///
23/// ```rust,ignore
24/// fn handle_increment(state: &mut Counter, _: Increment, _ctx: &mut ReducerContext<Counter>) {
25///     state.count += 1;
26/// }
27/// ```
28pub type Handler<S, A> = for<'a, 'b, 'c> fn(&mut S, A, &mut ReducerContext<'a, 'b, 'c, S>);
29
30/// Trait that allows both 2-argument (legacy) and 3-argument (modern) handler
31/// functions to be used with [`ActionRegistry::register`] and
32/// [`BuildCtxHandle::bind`](crate::build::BuildCtxHandle::bind).
33pub trait IntoHandler<S: GlobalState, A> {
34    /// Invoke the handler with the given state, action, and context.
35    fn call<'a, 'b, 'c>(&self, state: &mut S, action: A, ctx: &mut ReducerContext<'a, 'b, 'c, S>);
36}
37
38// Impl for Legacy (2-arg)
39impl<S: GlobalState, A> IntoHandler<S, A> for fn(&mut S, A) {
40    fn call<'a, 'b, 'c>(&self, state: &mut S, action: A, _ctx: &mut ReducerContext<'a, 'b, 'c, S>) {
41        (self)(state, action);
42    }
43}
44
45// Impl for Modern (3-arg)
46impl<S: GlobalState, A> IntoHandler<S, A>
47    for for<'a, 'b, 'c> fn(&mut S, A, &mut ReducerContext<'a, 'b, 'c, S>)
48{
49    fn call<'a, 'b, 'c>(&self, state: &mut S, action: A, ctx: &mut ReducerContext<'a, 'b, 'c, S>) {
50        (self)(state, action, ctx);
51    }
52}
53
54// Internal typed reducer storage
55type TypedReducer<S> = Box<
56    dyn for<'a, 'b, 'c> Fn(
57            &mut S,
58            &ActionEnvelope,
59            WidgetId,
60            &mut Effects<'a, S>,
61            &'b ActionInput,
62        ) -> Result<()>
63        + Send
64        + Sync,
65>;
66
67/// A per-frame collection of action handlers registered during widget building.
68///
69/// `ActionRegistry` is populated by [`BuildCtxHandle::bind`](crate::build::BuildCtxHandle::bind)
70/// calls. After the widget
71/// tree is built, the registry is absorbed into the [`Runtime`](crate::Runtime)
72/// via [`Runtime::absorb_registry`](crate::Runtime::absorb_registry).
73pub struct ActionRegistry<S: GlobalState> {
74    handlers: BTreeMap<ActionId, Vec<TypedReducer<S>>>,
75    runtime_handlers: BTreeMap<ActionId, Vec<BoxedReducer>>,
76}
77
78impl<S: GlobalState> Default for ActionRegistry<S> {
79    fn default() -> Self {
80        Self {
81            handlers: BTreeMap::new(),
82            runtime_handlers: BTreeMap::new(),
83        }
84    }
85}
86
87impl<S: GlobalState> ActionRegistry<S> {
88    pub fn new() -> Self {
89        Self::default()
90    }
91
92    pub fn register<A: Action, H: IntoHandler<S, A> + Send + Sync + 'static>(
93        &mut self,
94        handler: H,
95    ) {
96        let action_id = A::static_id();
97
98        let typed_reducer: TypedReducer<S> = Box::new(
99            move |state: &mut S,
100                  envelope: &ActionEnvelope,
101                  _target,
102                  effects,
103                  input|
104                  -> Result<()> {
105                let action: A = serde_json::from_slice(&envelope.payload)
106                    .map_err(|e| anyhow!("Failed to deserialize action: {}", e))?;
107
108                let mut ctx = ReducerContext { effects, input };
109
110                handler.call(state, action, &mut ctx);
111                Ok(())
112            },
113        );
114
115        self.handlers
116            .entry(action_id)
117            .or_default()
118            .push(typed_reducer);
119    }
120
121    pub(crate) fn register_runtime_reducer(&mut self, action_id: ActionId, reducer: BoxedReducer) {
122        self.runtime_handlers
123            .entry(action_id)
124            .or_default()
125            .push(reducer);
126    }
127
128    pub fn dispatch_with_input(
129        &mut self,
130        state: &mut S,
131        action: &ActionEnvelope,
132        target: WidgetId,
133        input: &ActionInput,
134    ) -> Result<Vec<EffectEnvelope>> {
135        let mut effects_builder = Effects::new_headless(0);
136        let target: WidgetId = target.into();
137        if let Some(reducers) = self.handlers.get_mut(&action.id) {
138            for reducer in reducers {
139                reducer(state, action, target, &mut effects_builder, input)?;
140            }
141        }
142        Ok(effects_builder.out)
143    }
144
145    pub fn dispatch(
146        &mut self,
147        state: &mut S,
148        action: &ActionEnvelope,
149        target: WidgetId,
150    ) -> Result<Vec<EffectEnvelope>> {
151        self.dispatch_with_input(state, action, target, &ActionInput::None)
152    }
153
154    pub(crate) fn into_runtime_reducers(self) -> HashMap<ActionId, Vec<BoxedReducer>> {
155        let mut runtime_reducers: HashMap<ActionId, Vec<BoxedReducer>> = HashMap::new();
156        let state_type_id = TypeId::of::<S>();
157
158        for (action_id, mut reducers) in self.runtime_handlers {
159            runtime_reducers
160                .entry(action_id)
161                .or_default()
162                .append(&mut reducers);
163        }
164
165        for (action_id, typed_reducers) in self.handlers {
166            for typed_reducer in typed_reducers {
167                let boxed_reducer: BoxedReducer = Box::new(
168                    move |app_states: &mut HashMap<TypeId, Box<dyn GlobalState>>,
169                          action: &ActionEnvelope,
170                          target: WidgetId,
171                          out_effects: &mut Vec<EffectEnvelope>,
172                          input: &ActionInput|
173                          -> Result<()> {
174                        if let Some(state_box) = app_states.get_mut(&state_type_id) {
175                            let concrete_state =
176                                state_box.downcast_mut::<S>().ok_or_else(|| {
177                                    anyhow!("Failed to downcast GlobalState to concrete type")
178                                })?;
179
180                            let mut effects_builder = Effects::new_headless(0);
181
182                            typed_reducer(
183                                concrete_state,
184                                action,
185                                target,
186                                &mut effects_builder,
187                                input,
188                            )?;
189
190                            out_effects.extend(effects_builder.out);
191
192                            Ok(())
193                        } else {
194                            anyhow::bail!("Target GlobalState for reducer not found in runtime.");
195                        }
196                    },
197                );
198
199                runtime_reducers
200                    .entry(action_id)
201                    .or_default()
202                    .push(boxed_reducer);
203            }
204        }
205        runtime_reducers
206    }
207}
208
209/// Identifies which visual property an animation targets.
210///
211/// Built-in properties have well-known default values (e.g. opacity defaults
212/// to 1.0, translation defaults to 0.0). Custom properties use 0.0.
213///
214/// # Example
215///
216/// ```rust,ignore
217/// ctx.anim_for(widget_id).request(AnimationRequest {
218///     property: AnimationPropertyId::Opacity,
219///     from: AnimationStartValue::Current,
220///     to: 0.0,
221///     duration_ms: 300,
222///     repeat: false,
223///     delay_ms: 0,
224/// });
225/// ```
226#[derive(Clone, Debug, PartialEq, Eq, Hash)]
227pub enum AnimationPropertyId {
228    Opacity,
229    TranslateX,
230    TranslateY,
231    Scale,
232    Rotation,
233    Custom(Arc<str>),
234}
235
236impl AnimationPropertyId {
237    pub fn opacity() -> Self {
238        Self::Opacity
239    }
240    pub fn translate_x() -> Self {
241        Self::TranslateX
242    }
243    pub fn translate_y() -> Self {
244        Self::TranslateY
245    }
246    pub fn scale() -> Self {
247        Self::Scale
248    }
249    pub fn rotation() -> Self {
250        Self::Rotation
251    }
252    pub fn custom(name: impl Into<String>) -> Self {
253        Self::Custom(Arc::from(name.into()))
254    }
255    pub fn default_value(&self) -> f32 {
256        match self {
257            Self::Opacity => 1.0,
258            Self::Scale => 1.0,
259            Self::TranslateX | Self::TranslateY | Self::Rotation | Self::Custom(_) => 0.0,
260        }
261    }
262}
263
264/// Where an animation starts from.
265#[derive(Clone, Debug)]
266pub enum AnimationStartValue {
267    /// Start from an explicit value.
268    Explicit(f32),
269    /// Start from whatever the current animated value is.
270    Current,
271}
272
273/// A request to animate a visual property on a widget.
274///
275/// Registered via [`BuildCtx::request_animation_for`] or
276/// [`AnimCtx::request`].
277/// Easing function applied to animation progress before interpolation.
278#[derive(Clone, Debug, PartialEq)]
279pub enum EasingFunction {
280    Linear,
281    EaseIn,
282    EaseOut,
283    EaseInOut,
284    CubicBezier(f32, f32, f32, f32),
285}
286
287impl Default for EasingFunction {
288    fn default() -> Self {
289        Self::EaseInOut
290    }
291}
292
293impl EasingFunction {
294    /// Map a linear progress value `t` in [0, 1] to an eased value.
295    pub fn apply(&self, t: f32) -> f32 {
296        match self {
297            Self::Linear => t,
298            Self::EaseIn => t * t,
299            Self::EaseOut => 1.0 - (1.0 - t) * (1.0 - t),
300            Self::EaseInOut => {
301                if t < 0.5 {
302                    2.0 * t * t
303                } else {
304                    1.0 - (-2.0 * t + 2.0).powi(2) / 2.0
305                }
306            }
307            Self::CubicBezier(_x1, y1, _x2, y2) => {
308                // Simplified cubic bezier approximation (y-axis only)
309                let t2 = t * t;
310                let t3 = t2 * t;
311                3.0 * (1.0 - t) * (1.0 - t) * t * y1 + 3.0 * (1.0 - t) * t2 * y2 + t3
312            }
313        }
314    }
315}
316#[derive(Clone, Debug)]
317pub struct AnimationRequest {
318    /// The property to animate.
319    pub property: AnimationPropertyId,
320    /// Starting value.
321    pub from: AnimationStartValue,
322    /// Target value.
323    pub to: f32,
324    /// Duration in milliseconds.
325    pub duration_ms: u64,
326    /// Whether to loop the animation.
327    pub repeat: bool,
328    /// Delay before the animation starts (in milliseconds).
329    pub delay_ms: u64,
330    /// Optional preferred redraw cadence for this animation (in milliseconds).
331    ///
332    /// This is primarily useful for low-priority repeating effects such as
333    /// loading indicators that do not need full frame-rate updates.
334    pub frame_interval_ms: Option<u64>,
335    pub easing: EasingFunction,
336}
337
338/// Registration data for a [`Video`](crate::ui::Video) widget collected during
339/// widget building.
340#[derive(Clone, Debug)]
341pub struct VideoRegistration {
342    /// The stable widget identity of the video node.
343    pub node_id: WidgetId,
344    /// URL or asset path to the video file.
345    pub source: String,
346    /// Whether to start playing automatically.
347    pub autoplay: bool,
348    /// Whether to loop playback.
349    pub loop_playback: bool,
350}
351
352/// Registration data for a platform web view collected during widget building.
353#[derive(Clone, Debug)]
354pub struct WebRegistration {
355    /// The stable widget identity of the web view node.
356    pub node_id: WidgetId,
357    /// The URL to load.
358    pub url: String,
359    /// Optional custom user-agent string.
360    pub user_agent: Option<String>,
361}
362
363/// Z-order layer for portal entries.
364///
365/// Portals are sorted by layer (then by registration order within a layer).
366/// Higher layers paint on top of lower layers.
367#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
368pub enum PortalLayer {
369    /// Default overlay layer.
370    Default = 0,
371    /// Modal dialog layer.
372    Modal = 100,
373    /// Flyout / dropdown layer.
374    Flyout = 200,
375    /// Toast notification layer (topmost).
376    Toast = 300,
377}
378
379/// An entry in the portal overlay stack.
380///
381/// Created by [`BuildCtx::register_portal`] and friends.
382#[derive(Clone, Debug)]
383pub struct PortalEntry {
384    /// Which overlay layer this portal belongs to.
385    pub layer: PortalLayer,
386    /// Insertion order (for stable ordering within a layer).
387    pub seq: u64,
388    /// Optional stable identity.
389    pub id: Option<WidgetId>,
390    /// The portal's widget tree.
391    pub node: Widget,
392}
393
394/// The mutable context available during `impl From<Component> for Widget`.
395///
396#[derive(Clone, Copy)]
397pub struct VideoControlCtx {
398    pub(crate) target: WidgetId,
399}
400
401impl VideoControlCtx {
402    pub fn play(&self) -> ActionEnvelope {
403        let action = VideoPlay {
404            target: self.target,
405        };
406        ActionEnvelope {
407            id: VideoPlay::static_id(),
408            payload: action.encode(),
409        }
410    }
411
412    pub fn pause(&self) -> ActionEnvelope {
413        let action = VideoPause {
414            target: self.target,
415        };
416        ActionEnvelope {
417            id: VideoPause::static_id(),
418            payload: action.encode(),
419        }
420    }
421
422    pub fn stop(&self) -> ActionEnvelope {
423        let action = VideoStop {
424            target: self.target,
425        };
426        ActionEnvelope {
427            id: VideoStop::static_id(),
428            payload: action.encode(),
429        }
430    }
431
432    pub fn seek_to(&self, position_ms: u64) -> ActionEnvelope {
433        let action = VideoSeek {
434            target: self.target,
435            position_ms,
436        };
437        ActionEnvelope {
438            id: VideoSeek::static_id(),
439            payload: action.encode(),
440        }
441    }
442
443    pub fn set_rate(&self, rate: f32) -> ActionEnvelope {
444        let action = VideoSetRate {
445            target: self.target,
446            rate,
447        };
448        ActionEnvelope {
449            id: VideoSetRate::static_id(),
450            payload: action.encode(),
451        }
452    }
453
454    pub fn set_volume(&self, volume: f32) -> ActionEnvelope {
455        let action = VideoSetVolume {
456            target: self.target,
457            volume,
458        };
459        ActionEnvelope {
460            id: VideoSetVolume::static_id(),
461            payload: action.encode(),
462        }
463    }
464
465    pub fn set_muted(&self, muted: bool) -> ActionEnvelope {
466        let action = VideoSetMuted {
467            target: self.target,
468            muted,
469        };
470        ActionEnvelope {
471            id: VideoSetMuted::static_id(),
472            payload: action.encode(),
473        }
474    }
475}
476
477#[derive(Clone, Debug, PartialEq, Eq, Hash)]
478pub struct ResourceKey(String);
479
480impl ResourceKey {
481    pub fn new(name: impl Into<String>) -> Self {
482        Self(name.into())
483    }
484
485    pub fn widget(name: impl AsRef<str>, id: WidgetId) -> Self {
486        Self(format!("widget:{}:{}", id.as_u128(), name.as_ref()))
487    }
488
489    pub fn as_str(&self) -> &str {
490        &self.0
491    }
492}
493
494#[derive(Clone, Copy, Debug, PartialEq, Eq)]
495pub enum ResourcePolicy {
496    PreserveOnChange,
497    RestartOnChange,
498}
499
500impl Default for ResourcePolicy {
501    fn default() -> Self {
502        Self::RestartOnChange
503    }
504}
505
506#[derive(Clone, Debug, PartialEq)]
507pub struct RuntimeResourceDeclaration {
508    pub key: String,
509    pub deps: Option<Vec<u8>>,
510    pub policy: ResourcePolicy,
511    pub kind: RuntimeResourceKind,
512}
513
514#[derive(Clone, Debug, PartialEq)]
515pub enum RuntimeResourceKind {
516    Job(JobResource),
517    Service(ServiceResource),
518    Timer(TimerResource),
519}
520
521#[derive(Clone, Debug, PartialEq)]
522pub struct JobResource {
523    pub key: ResourceKey,
524    pub effect: EffectEnvelope,
525    pub deps: Option<Vec<u8>>,
526    pub policy: ResourcePolicy,
527}
528
529impl JobResource {
530    pub fn new<J: JobSpec>(key: ResourceKey, job: JobRef<J>, request: J::Request) -> Self {
531        let payload =
532            serde_json::to_vec(&request).expect("job resource request serialization must succeed");
533        Self {
534            key,
535            effect: EffectEnvelope {
536                req_id: 0,
537                effect: Effect::Job(JobRequestPayload {
538                    job_name: job.name.to_string(),
539                    payload,
540                }),
541                on_ok: None,
542                on_err: None,
543                service_bindings: None,
544                resource: None,
545            },
546            deps: None,
547            policy: ResourcePolicy::RestartOnChange,
548        }
549    }
550
551    pub fn deps<T: Serialize>(mut self, deps: T) -> Self {
552        self.deps =
553            Some(serde_json::to_vec(&deps).expect("resource deps serialization must succeed"));
554        self
555    }
556
557    pub fn preserve_on_change(mut self) -> Self {
558        self.policy = ResourcePolicy::PreserveOnChange;
559        self
560    }
561
562    pub fn restart_on_change(mut self) -> Self {
563        self.policy = ResourcePolicy::RestartOnChange;
564        self
565    }
566
567    pub fn on_ok(mut self, action: ActionEnvelope) -> Self {
568        self.effect.on_ok = Some(action);
569        self
570    }
571
572    pub fn on_err(mut self, action: ActionEnvelope) -> Self {
573        self.effect.on_err = Some(action);
574        self
575    }
576}
577
578#[derive(Clone, Debug, PartialEq)]
579pub struct ServiceResource {
580    pub key: ResourceKey,
581    pub effect: EffectEnvelope,
582    pub deps: Option<Vec<u8>>,
583    pub policy: ResourcePolicy,
584}
585
586impl ServiceResource {
587    pub fn new<Svc: ServiceSpec>(
588        key: ResourceKey,
589        slot: ServiceSlot<Svc>,
590        config: Svc::Config,
591    ) -> Self {
592        let config = serde_json::to_vec(&config)
593            .expect("service resource config serialization must succeed");
594        Self {
595            key,
596            effect: EffectEnvelope {
597                req_id: 0,
598                effect: Effect::StartService(ServiceStartPayload {
599                    service_name: slot.ty.name.to_string(),
600                    slot_key: slot.slot_key().to_string(),
601                    config,
602                }),
603                on_ok: None,
604                on_err: None,
605                service_bindings: Some(ServiceBindings::default()),
606                resource: None,
607            },
608            deps: None,
609            policy: ResourcePolicy::RestartOnChange,
610        }
611    }
612
613    pub fn deps<T: Serialize>(mut self, deps: T) -> Self {
614        self.deps =
615            Some(serde_json::to_vec(&deps).expect("resource deps serialization must succeed"));
616        self
617    }
618
619    pub fn preserve_on_change(mut self) -> Self {
620        self.policy = ResourcePolicy::PreserveOnChange;
621        self
622    }
623
624    pub fn restart_on_change(mut self) -> Self {
625        self.policy = ResourcePolicy::RestartOnChange;
626        self
627    }
628
629    pub fn on_started(mut self, action: ActionEnvelope) -> Self {
630        if let Some(bindings) = self.effect.service_bindings.as_mut() {
631            bindings.on_started = Some(action);
632        }
633        self
634    }
635
636    pub fn on_start_failed(mut self, action: ActionEnvelope) -> Self {
637        if let Some(bindings) = self.effect.service_bindings.as_mut() {
638            bindings.on_start_failed = Some(action);
639        }
640        self
641    }
642
643    pub fn on_event(mut self, action: ActionEnvelope) -> Self {
644        if let Some(bindings) = self.effect.service_bindings.as_mut() {
645            bindings.on_event = Some(action);
646        }
647        self
648    }
649
650    pub fn on_stopped(mut self, action: ActionEnvelope) -> Self {
651        if let Some(bindings) = self.effect.service_bindings.as_mut() {
652            bindings.on_stopped = Some(action);
653        }
654        self
655    }
656
657    pub fn on_command_ok(mut self, action: ActionEnvelope) -> Self {
658        if let Some(bindings) = self.effect.service_bindings.as_mut() {
659            bindings.on_command_ok = Some(action);
660        }
661        self
662    }
663
664    pub fn on_command_err(mut self, action: ActionEnvelope) -> Self {
665        if let Some(bindings) = self.effect.service_bindings.as_mut() {
666            bindings.on_command_err = Some(action);
667        }
668        self
669    }
670}
671
672#[derive(Clone, Debug, PartialEq, Eq)]
673pub struct TimerResource {
674    pub key: ResourceKey,
675    pub interval_ms: u64,
676    pub payload: Vec<u8>,
677    pub on_tick: Option<ActionEnvelope>,
678    pub deps: Option<Vec<u8>>,
679    pub immediate: bool,
680    pub policy: ResourcePolicy,
681}
682
683impl TimerResource {
684    pub fn new<T: Serialize>(key: ResourceKey, interval: std::time::Duration, payload: T) -> Self {
685        Self {
686            key,
687            interval_ms: interval.as_millis() as u64,
688            payload: serde_json::to_vec(&payload)
689                .expect("timer resource payload serialization must succeed"),
690            on_tick: None,
691            deps: None,
692            immediate: false,
693            policy: ResourcePolicy::RestartOnChange,
694        }
695    }
696
697    pub fn deps<T: Serialize>(mut self, deps: T) -> Self {
698        self.deps =
699            Some(serde_json::to_vec(&deps).expect("resource deps serialization must succeed"));
700        self
701    }
702
703    pub fn preserve_on_change(mut self) -> Self {
704        self.policy = ResourcePolicy::PreserveOnChange;
705        self
706    }
707
708    pub fn restart_on_change(mut self) -> Self {
709        self.policy = ResourcePolicy::RestartOnChange;
710        self
711    }
712
713    pub fn immediate(mut self) -> Self {
714        self.immediate = true;
715        self
716    }
717
718    pub fn on_tick(mut self, action: ActionEnvelope) -> Self {
719        self.on_tick = Some(action);
720        self
721    }
722}
723
724#[derive(Default)]
725pub struct ResourceRegistry {
726    declarations: Vec<RuntimeResourceDeclaration>,
727    seen_keys: HashMap<String, usize>,
728}
729
730impl ResourceRegistry {
731    pub fn new() -> Self {
732        Self::default()
733    }
734
735    pub fn job(&mut self, resource: JobResource) {
736        self.push(RuntimeResourceDeclaration {
737            key: resource.key.as_str().to_string(),
738            deps: resource.deps.clone(),
739            policy: resource.policy,
740            kind: RuntimeResourceKind::Job(resource),
741        });
742    }
743
744    pub fn service(&mut self, resource: ServiceResource) {
745        self.push(RuntimeResourceDeclaration {
746            key: resource.key.as_str().to_string(),
747            deps: resource.deps.clone(),
748            policy: resource.policy,
749            kind: RuntimeResourceKind::Service(resource),
750        });
751    }
752
753    pub fn timer(&mut self, resource: TimerResource) {
754        self.push(RuntimeResourceDeclaration {
755            key: resource.key.as_str().to_string(),
756            deps: resource.deps.clone(),
757            policy: resource.policy,
758            kind: RuntimeResourceKind::Timer(resource),
759        });
760    }
761
762    pub fn take(&mut self) -> Vec<RuntimeResourceDeclaration> {
763        self.seen_keys.clear();
764        std::mem::take(&mut self.declarations)
765    }
766
767    fn push(&mut self, declaration: RuntimeResourceDeclaration) {
768        if let Some(index) = self.seen_keys.get(&declaration.key) {
769            panic!(
770                "duplicate runtime resource declaration for key '{}' at index {}",
771                declaration.key, index
772            );
773        }
774        let index = self.declarations.len();
775        self.seen_keys.insert(declaration.key.clone(), index);
776        self.declarations.push(declaration);
777    }
778}