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
21pub type Handler<S, A> = for<'a, 'b, 'c> fn(&mut S, A, &mut ReducerContext<'a, 'b, 'c, S>);
29
30pub trait IntoHandler<S: GlobalState, A> {
34 fn call<'a, 'b, 'c>(&self, state: &mut S, action: A, ctx: &mut ReducerContext<'a, 'b, 'c, S>);
36}
37
38impl<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
45impl<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
54type 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
67pub 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#[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#[derive(Clone, Debug)]
266pub enum AnimationStartValue {
267 Explicit(f32),
269 Current,
271}
272
273#[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 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 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 pub property: AnimationPropertyId,
320 pub from: AnimationStartValue,
322 pub to: f32,
324 pub duration_ms: u64,
326 pub repeat: bool,
328 pub delay_ms: u64,
330 pub frame_interval_ms: Option<u64>,
335 pub easing: EasingFunction,
336}
337
338#[derive(Clone, Debug)]
341pub struct VideoRegistration {
342 pub node_id: WidgetId,
344 pub source: String,
346 pub autoplay: bool,
348 pub loop_playback: bool,
350}
351
352#[derive(Clone, Debug)]
354pub struct WebRegistration {
355 pub node_id: WidgetId,
357 pub url: String,
359 pub user_agent: Option<String>,
361}
362
363#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
368pub enum PortalLayer {
369 Default = 0,
371 Modal = 100,
373 Flyout = 200,
375 Toast = 300,
377}
378
379#[derive(Clone, Debug)]
383pub struct PortalEntry {
384 pub layer: PortalLayer,
386 pub seq: u64,
388 pub id: Option<WidgetId>,
390 pub node: Widget,
392}
393
394#[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}