1use crate::{
2 action::video::{
3 VideoPause, VideoPlay, VideoSeek, VideoSetMuted, VideoSetRate, VideoSetVolume, VideoStop,
4 },
5 Action, ActionEnvelope, ActionId, AppState, BoxedReducer,
6 ui::Node,
7 context::{Effects, ReducerContext},
8 effect::{EffectEnvelope, ActionInput},
9};
10use anyhow::{anyhow, Result};
11use fission_ir::{NodeId, WidgetNodeId};
12use serde::{Deserialize, Serialize};
13use std::any::TypeId;
14use std::collections::{BTreeMap, HashMap};
15use std::sync::Arc;
16
17pub type Handler<S, A> = for<'a, 'b, 'c> fn(&mut S, A, &mut ReducerContext<'a, 'b, 'c, S>);
25
26pub trait IntoHandler<S: AppState, A> {
30 fn call<'a, 'b, 'c>(&self, state: &mut S, action: A, ctx: &mut ReducerContext<'a, 'b, 'c, S>);
32}
33
34impl<S: AppState, A> IntoHandler<S, A> for fn(&mut S, A) {
36 fn call<'a, 'b, 'c>(&self, state: &mut S, action: A, _ctx: &mut ReducerContext<'a, 'b, 'c, S>) {
37 (self)(state, action);
38 }
39}
40
41impl<S: AppState, A> IntoHandler<S, A> for for<'a, 'b, 'c> fn(&mut S, A, &mut ReducerContext<'a, 'b, 'c, S>) {
43 fn call<'a, 'b, 'c>(&self, state: &mut S, action: A, ctx: &mut ReducerContext<'a, 'b, 'c, S>) {
44 (self)(state, action, ctx);
45 }
46}
47
48type TypedReducer<S> = Box<dyn for<'a, 'b, 'c> Fn(&mut S, &ActionEnvelope, &mut Effects<'a, S>, &'b ActionInput) -> Result<()> + Send + Sync>;
50
51pub struct ActionRegistry<S: AppState> {
57 handlers: BTreeMap<ActionId, TypedReducer<S>>,
58}
59
60impl<S: AppState> Default for ActionRegistry<S> {
61 fn default() -> Self {
62 Self {
63 handlers: BTreeMap::new(),
64 }
65 }
66}
67
68impl<S: AppState> ActionRegistry<S> {
69 pub fn new() -> Self {
70 Self::default()
71 }
72
73 pub fn register<A: Action, H: IntoHandler<S, A> + Send + Sync + 'static>(&mut self, handler: H) {
74 let action_id = A::static_id();
75
76 let typed_reducer: TypedReducer<S> = Box::new(
77 move |state: &mut S, envelope: &ActionEnvelope, effects, input| -> Result<()> {
78 let action: A = serde_json::from_slice(&envelope.payload)
79 .map_err(|e| anyhow!("Failed to deserialize action: {}", e))?;
80
81 let mut ctx = ReducerContext {
82 effects,
83 input,
84 };
85
86 handler.call(state, action, &mut ctx);
87 Ok(())
88 },
89 );
90
91 self.handlers.insert(action_id, typed_reducer);
92 }
93
94 pub fn into_runtime_reducers(self) -> HashMap<ActionId, Vec<BoxedReducer>> {
95 let mut runtime_reducers: HashMap<ActionId, Vec<BoxedReducer>> = HashMap::new();
96 let state_type_id = TypeId::of::<S>();
97
98 for (action_id, typed_reducer) in self.handlers {
99 let boxed_reducer: BoxedReducer = Box::new(
100 move |app_states: &mut HashMap<TypeId, Box<dyn AppState>>,
101 action: &ActionEnvelope,
102 _target: NodeId,
103 out_effects: &mut Vec<EffectEnvelope>,
104 input: &ActionInput|
105 -> Result<()> {
106 if let Some(state_box) = app_states.get_mut(&state_type_id) {
107 let concrete_state = state_box.downcast_mut::<S>().ok_or_else(|| {
108 anyhow!("Failed to downcast AppState to concrete type")
109 })?;
110
111 let mut effects_builder = Effects::new_headless(0);
112
113 typed_reducer(concrete_state, action, &mut effects_builder, input)?;
114
115 out_effects.extend(effects_builder.out);
116
117 Ok(())
118 } else {
119 anyhow::bail!("Target AppState for reducer not found in runtime.");
120 }
121 },
122 );
123
124 runtime_reducers
125 .entry(action_id)
126 .or_default()
127 .push(boxed_reducer);
128 }
129 runtime_reducers
130 }
131}
132
133#[derive(Clone, Debug, PartialEq, Eq, Hash)]
151pub enum AnimationPropertyId {
152 Opacity,
153 TranslateX,
154 TranslateY,
155 Scale,
156 Rotation,
157 Custom(Arc<str>),
158}
159
160impl AnimationPropertyId {
161 pub fn opacity() -> Self { Self::Opacity }
162 pub fn translate_x() -> Self { Self::TranslateX }
163 pub fn translate_y() -> Self { Self::TranslateY }
164 pub fn scale() -> Self { Self::Scale }
165 pub fn rotation() -> Self { Self::Rotation }
166 pub fn custom(name: impl Into<String>) -> Self { Self::Custom(Arc::from(name.into())) }
167 pub fn default_value(&self) -> f32 {
168 match self {
169 Self::Opacity => 1.0,
170 Self::Scale => 1.0,
171 Self::TranslateX | Self::TranslateY | Self::Rotation | Self::Custom(_) => 0.0,
172 }
173 }
174}
175
176#[derive(Clone, Debug)]
178pub enum AnimationStartValue {
179 Explicit(f32),
181 Current,
183}
184
185#[derive(Clone, Debug)]
190pub struct AnimationRequest {
191 pub property: AnimationPropertyId,
193 pub from: AnimationStartValue,
195 pub to: f32,
197 pub duration_ms: u64,
199 pub repeat: bool,
201 pub delay_ms: u64,
203}
204
205#[derive(Clone, Debug)]
208pub struct VideoRegistration {
209 pub node_id: WidgetNodeId,
211 pub source: String,
213 pub autoplay: bool,
215 pub loop_playback: bool,
217}
218
219#[derive(Clone, Debug)]
221pub struct WebRegistration {
222 pub node_id: WidgetNodeId,
224 pub url: String,
226 pub user_agent: Option<String>,
228}
229
230pub struct BuildCtx<S: AppState> {
254 pub registry: ActionRegistry<S>,
256 pub animation_requests: Vec<(WidgetNodeId, AnimationRequest)>,
258 pub video_nodes: Vec<VideoRegistration>,
260 pub web_nodes: Vec<WebRegistration>,
262 pub portals: Vec<PortalEntry>,
264 portal_seq: u64,
265}
266
267#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
272pub enum PortalLayer {
273 Default = 0,
275 Modal = 100,
277 Flyout = 200,
279 Toast = 300,
281}
282
283#[derive(Clone, Debug)]
287pub struct PortalEntry {
288 pub layer: PortalLayer,
290 pub seq: u64,
292 pub id: Option<WidgetNodeId>,
294 pub node: Node,
296}
297
298impl<S: AppState> BuildCtx<S> {
299 pub fn new() -> Self {
300 Self {
301 registry: ActionRegistry::new(),
302 animation_requests: Vec::new(),
303 video_nodes: Vec::new(),
304 web_nodes: Vec::new(),
305 portals: Vec::new(),
306 portal_seq: 0,
307 }
308 }
309
310 pub fn bind<A: Action, H>(&mut self, action: A, handler: H) -> ActionEnvelope
311 where H: IntoHandler<S, A> + Send + Sync + 'static
312 {
313 self.registry.register(handler);
314
315 ActionEnvelope {
316 id: A::static_id(),
317 payload: action.encode(),
318 }
319 }
320
321 pub fn request_animation_for(&mut self, target: WidgetNodeId, request: AnimationRequest) {
322 self.animation_requests.push((target, request));
323 }
324
325 pub fn register_video(&mut self, registration: VideoRegistration) {
326 self.video_nodes.push(registration);
327 }
328
329 pub fn register_web_view(&mut self, registration: WebRegistration) {
330 self.web_nodes.push(registration);
331 }
332
333 pub fn take_animation_requests(&mut self) -> Vec<(WidgetNodeId, AnimationRequest)> {
334 std::mem::take(&mut self.animation_requests)
335 }
336
337 pub fn take_video_registrations(&mut self) -> Vec<VideoRegistration> {
338 std::mem::take(&mut self.video_nodes)
339 }
340
341 pub fn take_web_registrations(&mut self) -> Vec<WebRegistration> {
342 std::mem::take(&mut self.web_nodes)
343 }
344
345 pub fn register_portal(&mut self, node: Node) {
346 self.register_portal_with_layer(PortalLayer::Default, None, node);
347 }
348
349 pub fn register_portal_with_id(&mut self, id: WidgetNodeId, node: Node) {
350 self.register_portal_with_layer(PortalLayer::Default, Some(id), node);
351 }
352
353 pub fn register_portal_with_layer(&mut self, layer: PortalLayer, id: Option<WidgetNodeId>, node: Node) {
354 let seq = self.portal_seq;
355 self.portal_seq = self.portal_seq.wrapping_add(1);
356 self.portals.push(PortalEntry { layer, seq, id, node });
357 }
358
359 pub fn take_portals(&mut self) -> Vec<(Option<WidgetNodeId>, Node)> {
360 let mut entries = std::mem::take(&mut self.portals);
361 entries.sort_by(|a, b| (a.layer, a.seq).cmp(&(b.layer, b.seq)));
362 entries.into_iter().map(|e| (e.id, e.node)).collect()
363 }
364
365 pub fn anim_for(&mut self, target: WidgetNodeId) -> AnimCtx<'_, S> {
366 AnimCtx { target, ctx: self }
367 }
368
369 pub fn video_controls(&self, target: WidgetNodeId) -> VideoControlCtx {
370 VideoControlCtx { target }
371 }
372}
373
374pub struct AnimCtx<'a, S: AppState> {
375 target: WidgetNodeId,
376 ctx: &'a mut BuildCtx<S>,
377}
378
379impl<'a, S: AppState> AnimCtx<'a, S> {
380 pub fn request(&mut self, request: AnimationRequest) {
381 self.ctx.request_animation_for(self.target, request);
382 }
383
384 pub fn request_for(&mut self, target: WidgetNodeId, request: AnimationRequest) {
385 self.ctx.request_animation_for(target, request);
386 }
387}
388
389#[derive(Clone, Copy)]
390pub struct VideoControlCtx {
391 target: WidgetNodeId,
392}
393
394impl VideoControlCtx {
395 pub fn play(&self) -> ActionEnvelope {
396 let action = VideoPlay {
397 target: self.target,
398 };
399 ActionEnvelope {
400 id: VideoPlay::static_id(),
401 payload: action.encode(),
402 }
403 }
404
405 pub fn pause(&self) -> ActionEnvelope {
406 let action = VideoPause {
407 target: self.target,
408 };
409 ActionEnvelope {
410 id: VideoPause::static_id(),
411 payload: action.encode(),
412 }
413 }
414
415 pub fn stop(&self) -> ActionEnvelope {
416 let action = VideoStop {
417 target: self.target,
418 };
419 ActionEnvelope {
420 id: VideoStop::static_id(),
421 payload: action.encode(),
422 }
423 }
424
425 pub fn seek_to(&self, position_ms: u64) -> ActionEnvelope {
426 let action = VideoSeek {
427 target: self.target,
428 position_ms,
429 };
430 ActionEnvelope {
431 id: VideoSeek::static_id(),
432 payload: action.encode(),
433 }
434 }
435
436 pub fn set_rate(&self, rate: f32) -> ActionEnvelope {
437 let action = VideoSetRate {
438 target: self.target,
439 rate,
440 };
441 ActionEnvelope {
442 id: VideoSetRate::static_id(),
443 payload: action.encode(),
444 }
445 }
446
447 pub fn set_volume(&self, volume: f32) -> ActionEnvelope {
448 let action = VideoSetVolume {
449 target: self.target,
450 volume,
451 };
452 ActionEnvelope {
453 id: VideoSetVolume::static_id(),
454 payload: action.encode(),
455 }
456 }
457
458 pub fn set_muted(&self, muted: bool) -> ActionEnvelope {
459 let action = VideoSetMuted {
460 target: self.target,
461 muted,
462 };
463 ActionEnvelope {
464 id: VideoSetMuted::static_id(),
465 payload: action.encode(),
466 }
467 }
468}