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