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<()>;