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