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//! - [`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/// Structured details for a text-input edit.
161///
162/// Every [`TextInput`](crate::ui::TextInput) binding receives this value through
163/// [`ActionInput::text_change`](crate::ActionInput::text_change), while the
164/// bound action payload continues to carry the application's stable context.
165#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
166pub struct UpdateTextInput {
167    /// The widget identity of the text input that changed.
168    pub node_id: WidgetId,
169    /// The complete new text value.
170    pub new_text: String,
171    /// Byte offset of the caret (insertion point).
172    pub new_caret: usize,
173    /// Byte offset of the selection anchor (equals `new_caret` when no
174    /// selection is active).
175    pub new_anchor: usize,
176    /// Complete value before the transaction. Older shell boundaries may not
177    /// know it and use an empty value until they adopt the complete-value API.
178    #[serde(default)]
179    pub old_value: crate::text_editing::TextEditingValue,
180    /// Complete value after the transaction.
181    #[serde(default)]
182    pub new_value: crate::text_editing::TextEditingValue,
183    /// Origin of this edit.
184    #[serde(default)]
185    pub source: crate::text_editing::TextEditSource,
186    /// Lifecycle phase represented by this action.
187    #[serde(default)]
188    pub phase: crate::text_editing::TextEditPhase,
189    /// Configured software-keyboard action for submit/completion events.
190    #[serde(default)]
191    pub editing_action: Option<fission_ir::semantics::TextInputAction>,
192    /// Validation state when `phase` is [`TextEditPhase::Validated`](crate::TextEditPhase::Validated).
193    #[serde(default)]
194    pub validation_state: Option<fission_ir::semantics::TextFieldValidationState>,
195    /// Accessible validation detail associated with `validation_state`.
196    #[serde(default)]
197    pub validation_message: Option<String>,
198}
199
200impl Default for UpdateTextInput {
201    fn default() -> Self {
202        Self {
203            node_id: WidgetId::from_u128(0),
204            new_text: String::new(),
205            new_caret: 0,
206            new_anchor: 0,
207            old_value: crate::TextEditingValue::default(),
208            new_value: crate::TextEditingValue::default(),
209            source: crate::TextEditSource::Programmatic,
210            phase: crate::TextEditPhase::Committed,
211            editing_action: None,
212            validation_state: None,
213            validation_message: None,
214        }
215    }
216}
217
218impl UpdateTextInput {
219    pub fn from_values(
220        node_id: WidgetId,
221        old_value: crate::TextEditingValue,
222        new_value: crate::TextEditingValue,
223        source: crate::TextEditSource,
224        phase: crate::TextEditPhase,
225    ) -> Self {
226        Self {
227            node_id,
228            new_text: new_value.text.clone(),
229            new_caret: new_value.selection.extent.utf8_offset(),
230            new_anchor: new_value.selection.base.utf8_offset(),
231            old_value,
232            new_value,
233            source,
234            phase,
235            editing_action: None,
236            validation_state: None,
237            validation_message: None,
238        }
239    }
240}
241
242/// Typed runtime input for a selection-only change.
243#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
244pub struct UpdateTextSelection {
245    pub node_id: WidgetId,
246    pub value: crate::text_editing::TextEditingValue,
247    pub source: crate::text_editing::TextEditSource,
248}
249
250/// Payload dispatched when the caret/anchor position changes in a TextInput.
251#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
252pub struct CursorChanged {
253    pub caret: usize,
254    pub anchor: usize,
255}
256
257impl Action for CursorChanged {
258    fn static_id() -> ActionId {
259        lazy_static! {
260            pub static ref CURSOR_CHANGED_ACTION_ID: ActionId =
261                ActionId::from_name("fission_core::CursorChanged");
262        }
263        *CURSOR_CHANGED_ACTION_ID
264    }
265}
266
267/// A strongly-typed, serialisable event payload.
268///
269/// Every action type must be `Serialize + DeserializeOwned + Send + Sync + Debug`
270/// and provide a stable [`ActionId`] via [`Action::static_id`]. The runtime
271/// uses JSON serialisation internally, so actions travel across the
272/// widget/reducer boundary without generics.
273///
274/// # Implementing `Action`
275///
276/// ```rust,ignore
277/// use fission_core::{Action, ActionId};
278/// use serde::{Deserialize, Serialize};
279///
280/// #[derive(Debug, Clone, Serialize, Deserialize)]
281/// struct SetName { name: String }
282///
283/// impl Action for SetName {
284///     fn static_id() -> ActionId {
285///         ActionId::from_name("my_app::SetName")
286///     }
287/// }
288/// ```
289pub trait Action: Serialize + DeserializeOwned + Any + Send + Sync + std::fmt::Debug {
290    /// Returns the globally unique, deterministic identifier for this action type.
291    fn static_id() -> ActionId
292    where
293        Self: Sized;
294
295    /// Serialises the action to JSON bytes for transport inside an
296    /// [`ActionEnvelope`].
297    fn encode(&self) -> Vec<u8> {
298        serde_json::to_vec(self).expect("Action serialization failed")
299    }
300}
301
302/// A type-erased action envelope that can be stored in widget trees and
303/// dispatched through the [`Runtime`](crate::Runtime).
304///
305/// `ActionEnvelope` pairs an [`ActionId`] with opaque JSON bytes so that the
306/// reducer pipeline can route and deserialise actions without compile-time
307/// knowledge of the concrete type.
308///
309/// # Creating an envelope
310///
311/// ```rust,ignore
312/// let envelope: ActionEnvelope = my_action.into();
313/// runtime.dispatch(envelope, widget_id)?;
314/// ```
315#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
316pub struct ActionEnvelope {
317    /// The identifier that routes this envelope to the correct reducer(s).
318    pub id: ActionId,
319    /// Opaque JSON-serialised payload bytes.
320    pub payload: Vec<u8>,
321}
322
323/// A typed wrapper around an [`Action`] value that converts into an
324/// [`ActionEnvelope`] via `From`.
325#[derive(Debug, Clone, PartialEq, Eq)]
326pub struct ActionRef<T: Action>(pub T);
327
328impl<T: Action> From<ActionRef<T>> for ActionEnvelope {
329    fn from(action_ref: ActionRef<T>) -> Self {
330        ActionEnvelope {
331            id: T::static_id(),
332            payload: action_ref.0.encode(),
333        }
334    }
335}
336
337// Also allow direct conversion for convenience if desired?
338impl<T: Action> From<T> for ActionEnvelope {
339    fn from(action: T) -> Self {
340        ActionEnvelope {
341            id: T::static_id(),
342            payload: action.encode(),
343        }
344    }
345}
346
347/// Trait for app-wide state managed by the [`Runtime`](crate::Runtime).
348///
349/// `GlobalState` is for domain state that belongs to the whole app or session:
350/// documents, logged-in user data, navigation state, caches, settings, and
351/// other data that should outlive an individual widget. Transient UI details
352/// that should disappear with one widget instance belong in local widget state
353/// instead.
354///
355/// # Example
356///
357/// ```rust,ignore
358/// #[derive(Debug, Default)]
359/// struct TodoList {
360///     items: Vec<String>,
361/// }
362/// impl GlobalState for TodoList {}
363///
364/// // Register with the runtime:
365/// runtime.add_global_state(Box::new(TodoList::default()))?;
366/// ```
367pub trait GlobalState: Any + Send + Sync + std::fmt::Debug + Downcast {}
368
369impl GlobalState for () {}
370impl GlobalState for bool {}
371impl GlobalState for String {}
372impl GlobalState for i8 {}
373impl GlobalState for i16 {}
374impl GlobalState for i32 {}
375impl GlobalState for i64 {}
376impl GlobalState for isize {}
377impl GlobalState for u8 {}
378impl GlobalState for u16 {}
379impl GlobalState for u32 {}
380impl GlobalState for u64 {}
381impl GlobalState for usize {}
382impl GlobalState for f32 {}
383impl GlobalState for f64 {}
384
385impl_downcast!(GlobalState);
386
387/// Type alias for the legacy 3-argument reducer signature used by
388/// [`Runtime::register_reducer`](crate::Runtime::register_reducer).
389///
390/// Prefer the modern handler signature via `ctx.bind(...)` from
391/// [`build::current`](crate::build::current), which provides access to effects
392/// and input context without exposing internal IR node identities.
393pub type Reducer<S> = fn(&mut S, &ActionEnvelope, WidgetId) -> anyhow::Result<()>;