Skip to main content

fission_core/action/
mod.rs

1//! Actions, envelopes, and application state traits.
2//!
3//! This module defines the core data-flow primitives:
4//!
5//! - [`Action`] -- a strongly-typed, serialisable event payload.
6//! - [`ActionEnvelope`] -- the type-erased transport format dispatched through
7//!   the [`Runtime`](crate::Runtime).
8//! - [`ActionId`] -- a stable, content-addressed identifier derived from the
9//!   action's type name.
10//! - [`AppState`] -- trait for application state managed by the runtime.
11
12use blake3;
13use downcast_rs::{impl_downcast, Downcast};
14use fission_ir::NodeId;
15// use fission_macros::Action;
16use lazy_static::lazy_static;
17use serde::{de::DeserializeOwned, Deserialize, Serialize};
18use serde_json;
19use std::any::Any;
20
21use crate as fission_core;
22
23pub mod video;
24
25pub use video::{
26    VideoPause, VideoPlay, VideoSeek, VideoSetMuted, VideoSetRate, VideoSetVolume, VideoStop,
27};
28
29/// Built-in action to trigger an undo operation.
30///
31/// Applications that support undo/redo should register a reducer for this
32/// action on their state type.
33#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
34pub struct Undo;
35
36impl Action for Undo {
37    fn static_id() -> ActionId {
38        lazy_static! {
39            pub static ref UNDO_ACTION_ID: ActionId = ActionId::from_name("fission_core::Undo");
40        }
41        *UNDO_ACTION_ID
42    }
43}
44
45/// Built-in action to trigger a redo operation.
46#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
47pub struct Redo;
48
49impl Action for Redo {
50    fn static_id() -> ActionId {
51        lazy_static! {
52            pub static ref REDO_ACTION_ID: ActionId = ActionId::from_name("fission_core::Redo");
53        }
54        *REDO_ACTION_ID
55    }
56}
57
58/// A stable, globally unique identifier for an [`Action`] type.
59///
60/// `ActionId` is computed as the first 128 bits of a BLAKE3 hash of the
61/// action's fully-qualified type name, making it deterministic across
62/// compilations and platforms.
63///
64/// # Example
65///
66/// ```rust,ignore
67/// let id = ActionId::from_name("my_app::IncrementCounter");
68/// assert_eq!(id, ActionId::from_name("my_app::IncrementCounter")); // stable
69/// ```
70#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Serialize, Deserialize, PartialOrd, Ord)]
71pub struct ActionId(u128);
72
73impl ActionId {
74    /// Creates an `ActionId` from a raw `u128` value.
75    pub const fn from_u128(val: u128) -> Self {
76        Self(val)
77    }
78
79    /// Returns the underlying `u128` value.
80    pub fn as_u128(&self) -> u128 {
81        self.0
82    }
83
84    /// Derives a deterministic `ActionId` from a human-readable name string.
85    ///
86    /// The name is hashed with BLAKE3; the first 16 bytes become the id.
87    pub fn from_name(name: &str) -> Self {
88        let mut hasher = blake3::Hasher::new();
89        hasher.update(name.as_bytes());
90        let hash = hasher.finalize();
91        ActionId(u128::from_le_bytes(
92            hash.as_bytes()[0..16].try_into().unwrap(),
93        ))
94    }
95}
96
97
98/// Action dispatched by the text-editing controller when the user modifies a
99/// [`TextInput`](crate::ui::TextInput) field.
100///
101/// Contains the full new text and updated caret/selection positions.
102#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
103pub struct UpdateTextInput {
104    /// The IR node id of the text input that changed.
105    pub node_id: NodeId,
106    /// The complete new text value.
107    pub new_text: String,
108    /// Byte offset of the caret (insertion point).
109    pub new_caret: usize,
110    /// Byte offset of the selection anchor (equals `new_caret` when no
111    /// selection is active).
112    pub new_anchor: usize,
113}
114
115impl Action for UpdateTextInput {
116    fn static_id() -> ActionId {
117        lazy_static! {
118            pub static ref UPDATE_TEXT_INPUT_ACTION_ID: ActionId = ActionId::from_name("fission_core::UpdateTextInput");
119        }
120        *UPDATE_TEXT_INPUT_ACTION_ID
121    }
122}
123
124/// Payload dispatched when the caret/anchor position changes in a TextInput.
125#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
126pub struct CursorChanged {
127    pub caret: usize,
128    pub anchor: usize,
129}
130
131impl Action for CursorChanged {
132    fn static_id() -> ActionId {
133        lazy_static! {
134            pub static ref CURSOR_CHANGED_ACTION_ID: ActionId = ActionId::from_name("fission_core::CursorChanged");
135        }
136        *CURSOR_CHANGED_ACTION_ID
137    }
138}
139
140/// A strongly-typed, serialisable event payload.
141///
142/// Every action type must be `Serialize + DeserializeOwned + Send + Sync + Debug`
143/// and provide a stable [`ActionId`] via [`Action::static_id`]. The runtime
144/// uses JSON serialisation internally, so actions travel across the
145/// widget/reducer boundary without generics.
146///
147/// # Implementing `Action`
148///
149/// ```rust,ignore
150/// use fission_core::{Action, ActionId};
151/// use serde::{Deserialize, Serialize};
152///
153/// #[derive(Debug, Clone, Serialize, Deserialize)]
154/// struct SetName { name: String }
155///
156/// impl Action for SetName {
157///     fn static_id() -> ActionId {
158///         ActionId::from_name("my_app::SetName")
159///     }
160/// }
161/// ```
162pub trait Action: Serialize + DeserializeOwned + Any + Send + Sync + std::fmt::Debug {
163    /// Returns the globally unique, deterministic identifier for this action type.
164    fn static_id() -> ActionId
165    where
166        Self: Sized;
167
168    /// Serialises the action to JSON bytes for transport inside an
169    /// [`ActionEnvelope`].
170    fn encode(&self) -> Vec<u8> {
171        serde_json::to_vec(self).expect("Action serialization failed")
172    }
173}
174
175/// A type-erased action envelope that can be stored in widget trees and
176/// dispatched through the [`Runtime`](crate::Runtime).
177///
178/// `ActionEnvelope` pairs an [`ActionId`] with opaque JSON bytes so that the
179/// reducer pipeline can route and deserialise actions without compile-time
180/// knowledge of the concrete type.
181///
182/// # Creating an envelope
183///
184/// ```rust,ignore
185/// let envelope: ActionEnvelope = my_action.into();
186/// runtime.dispatch(envelope, target_node)?;
187/// ```
188#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
189pub struct ActionEnvelope {
190    /// The identifier that routes this envelope to the correct reducer(s).
191    pub id: ActionId,
192    /// Opaque JSON-serialised payload bytes.
193    pub payload: Vec<u8>,
194}
195
196/// A typed wrapper around an [`Action`] value that converts into an
197/// [`ActionEnvelope`] via `From`.
198#[derive(Debug, Clone, PartialEq, Eq)]
199pub struct ActionRef<T: Action>(pub T);
200
201impl<T: Action> From<ActionRef<T>> for ActionEnvelope {
202    fn from(action_ref: ActionRef<T>) -> Self {
203        ActionEnvelope {
204            id: T::static_id(),
205            payload: action_ref.0.encode(),
206        }
207    }
208}
209
210// Also allow direct conversion for convenience if desired?
211impl<T: Action> From<T> for ActionEnvelope {
212    fn from(action: T) -> Self {
213        ActionEnvelope {
214            id: T::static_id(),
215            payload: action.encode(),
216        }
217    }
218}
219
220/// Trait for application state managed by the [`Runtime`](crate::Runtime).
221///
222/// Any type that is `Send + Sync + Debug + 'static` can serve as application
223/// state. The runtime stores at most one instance of each concrete type.
224///
225/// # Example
226///
227/// ```rust,ignore
228/// #[derive(Debug, Default)]
229/// struct TodoList {
230///     items: Vec<String>,
231/// }
232/// impl AppState for TodoList {}
233///
234/// // Register with the runtime:
235/// runtime.add_app_state(Box::new(TodoList::default()))?;
236/// ```
237pub trait AppState: Any + Send + Sync + std::fmt::Debug + Downcast {}
238
239impl_downcast!(AppState);
240
241/// Type alias for the legacy 3-argument reducer signature used by
242/// [`Runtime::register_reducer`](crate::Runtime::register_reducer).
243///
244/// Prefer the modern handler signature via [`BuildCtx::bind`](crate::BuildCtx::bind) which
245/// provides access to effects and input context.
246pub type Reducer<S> = fn(&mut S, &ActionEnvelope, NodeId) -> anyhow::Result<()>;