Skip to main content

fission_core/
registry.rs

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
17/// The canonical 3-argument handler signature for modern reducers.
18///
19/// ```rust,ignore
20/// fn handle_increment(state: &mut Counter, _: Increment, _ctx: &mut ReducerContext<Counter>) {
21///     state.count += 1;
22/// }
23/// ```
24pub type Handler<S, A> = for<'a, 'b, 'c> fn(&mut S, A, &mut ReducerContext<'a, 'b, 'c, S>);
25
26/// Trait that allows both 2-argument (legacy) and 3-argument (modern) handler
27/// functions to be used with [`ActionRegistry::register`] and
28/// [`BuildCtx::bind`].
29pub trait IntoHandler<S: AppState, A> {
30    /// Invoke the handler with the given state, action, and context.
31    fn call<'a, 'b, 'c>(&self, state: &mut S, action: A, ctx: &mut ReducerContext<'a, 'b, 'c, S>);
32}
33
34// Impl for Legacy (2-arg)
35impl<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
41// Impl for Modern (3-arg)
42impl<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
48// Internal typed reducer storage
49type TypedReducer<S> = Box<dyn for<'a, 'b, 'c> Fn(&mut S, &ActionEnvelope, &mut Effects<'a, S>, &'b ActionInput) -> Result<()> + Send + Sync>;
50
51/// A per-frame collection of action handlers registered during widget building.
52///
53/// `ActionRegistry` is populated by [`BuildCtx::bind`] calls. After the widget
54/// tree is built, the registry is absorbed into the [`Runtime`](crate::Runtime)
55/// via [`Runtime::absorb_registry`](crate::Runtime::absorb_registry).
56pub 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/// Identifies which visual property an animation targets.
134///
135/// Built-in properties have well-known default values (e.g. opacity defaults
136/// to 1.0, translation defaults to 0.0). Custom properties use 0.0.
137///
138/// # Example
139///
140/// ```rust,ignore
141/// ctx.anim_for(widget_id).request(AnimationRequest {
142///     property: AnimationPropertyId::Opacity,
143///     from: AnimationStartValue::Current,
144///     to: 0.0,
145///     duration_ms: 300,
146///     repeat: false,
147///     delay_ms: 0,
148/// });
149/// ```
150#[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/// Where an animation starts from.
177#[derive(Clone, Debug)]
178pub enum AnimationStartValue {
179    /// Start from an explicit value.
180    Explicit(f32),
181    /// Start from whatever the current animated value is.
182    Current,
183}
184
185/// A request to animate a visual property on a widget.
186///
187/// Registered via [`BuildCtx::request_animation_for`] or
188/// [`AnimCtx::request`].
189#[derive(Clone, Debug)]
190pub struct AnimationRequest {
191    /// The property to animate.
192    pub property: AnimationPropertyId,
193    /// Starting value.
194    pub from: AnimationStartValue,
195    /// Target value.
196    pub to: f32,
197    /// Duration in milliseconds.
198    pub duration_ms: u64,
199    /// Whether to loop the animation.
200    pub repeat: bool,
201    /// Delay before the animation starts (in milliseconds).
202    pub delay_ms: u64,
203}
204
205/// Registration data for a [`Video`](crate::ui::Video) widget collected during
206/// widget building.
207#[derive(Clone, Debug)]
208pub struct VideoRegistration {
209    /// The stable widget identity of the video node.
210    pub node_id: WidgetNodeId,
211    /// URL or asset path to the video file.
212    pub source: String,
213    /// Whether to start playing automatically.
214    pub autoplay: bool,
215    /// Whether to loop playback.
216    pub loop_playback: bool,
217}
218
219/// Registration data for a platform web view collected during widget building.
220#[derive(Clone, Debug)]
221pub struct WebRegistration {
222    /// The stable widget identity of the web view node.
223    pub node_id: WidgetNodeId,
224    /// The URL to load.
225    pub url: String,
226    /// Optional custom user-agent string.
227    pub user_agent: Option<String>,
228}
229
230/// The mutable context passed to [`Widget::build`](crate::Widget::build).
231///
232/// `BuildCtx` is where widgets register side-effects that must survive beyond
233/// the build phase:
234///
235/// - **Action binding** -- [`bind`](BuildCtx::bind) registers a handler and
236///   returns an [`ActionEnvelope`] that can be stored in widget fields like
237///   `on_press`.
238/// - **Portals** -- [`register_portal`](BuildCtx::register_portal) places a
239///   node in the global overlay stack (modals, toasts, flyouts).
240/// - **Animations** -- [`request_animation_for`](BuildCtx::request_animation_for)
241///   or the [`anim_for`](BuildCtx::anim_for) helper.
242/// - **Video / WebView registration** -- [`register_video`](BuildCtx::register_video),
243///   [`register_web_view`](BuildCtx::register_web_view).
244///
245/// # Example
246///
247/// ```rust,ignore
248/// fn build(&self, ctx: &mut BuildCtx<S>, view: &View<S>) -> Node {
249///     let on_press = ctx.bind(MyAction { .. }, handler as fn(&mut S, MyAction));
250///     Button { on_press: Some(on_press), ..Default::default() }.into_node()
251/// }
252/// ```
253pub struct BuildCtx<S: AppState> {
254    /// The action registry that accumulates handlers during the build phase.
255    pub registry: ActionRegistry<S>,
256    /// Pending animation requests.
257    pub animation_requests: Vec<(WidgetNodeId, AnimationRequest)>,
258    /// Registered video nodes.
259    pub video_nodes: Vec<VideoRegistration>,
260    /// Registered web view nodes.
261    pub web_nodes: Vec<WebRegistration>,
262    /// Portal entries (overlays, modals, toasts).
263    pub portals: Vec<PortalEntry>,
264    portal_seq: u64,
265}
266
267/// Z-order layer for portal entries.
268///
269/// Portals are sorted by layer (then by registration order within a layer).
270/// Higher layers paint on top of lower layers.
271#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
272pub enum PortalLayer {
273    /// Default overlay layer.
274    Default = 0,
275    /// Modal dialog layer.
276    Modal = 100,
277    /// Flyout / dropdown layer.
278    Flyout = 200,
279    /// Toast notification layer (topmost).
280    Toast = 300,
281}
282
283/// An entry in the portal overlay stack.
284///
285/// Created by [`BuildCtx::register_portal`] and friends.
286#[derive(Clone, Debug)]
287pub struct PortalEntry {
288    /// Which overlay layer this portal belongs to.
289    pub layer: PortalLayer,
290    /// Insertion order (for stable ordering within a layer).
291    pub seq: u64,
292    /// Optional stable identity.
293    pub id: Option<WidgetNodeId>,
294    /// The portal's widget tree.
295    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}