Skip to main content

repose_core/
lib.rs

1//! # State, Signals, and Effects
2//!
3//! Repose uses a small reactive core instead of an explicit widget tree with
4//! mutable fields. There are three main pieces:
5//!
6//! - `Signal<T>` - observable, reactive value.
7//! - `remember*` - lifecycle‑aware storage bound to composition.
8//! - `effect` / `scoped_effect` - side‑effects with cleanup.
9//!
10//! ## Signals
11//!
12//! `Signal<T>` is a cloneable handle to a piece of state:
13//!
14//! ```rust
15//! use repose_core::*;
16//!
17//! let count = signal(0);
18//! count.set(1);
19//! count.update(|v| *v += 1);
20//! assert_eq!(count.get(), 2);
21//! ```
22//!
23//! Reads participate in a dependency graph: when you call `get()` inside an
24//! observer or `produce_state`, future writes will automatically recompute that
25//! observer.
26//!
27//! ## Remembered state
28//!
29//! UI state is typically held in `remember_*` slots rather than globals:
30//!
31//! ```ignore
32//! use repose_core::*;
33//!
34//! fn CounterView() -> View {
35//!     let count = remember_mutable(|| 0); // auto-requests a frame on set/update
36//!
37//!     let on_click = {
38//!         let count = count.clone();
39//!         move || count.update(|c| *c += 1)
40//!     };
41//!
42//!     repose_ui::Button(
43//!         format!("Count = {}", *count.get()),
44//!         on_click,
45//!     )
46//! }
47//! ```
48//!
49//! - `remember` and `remember_mutable` are order‑based: the Nth call in a
50//!   composition slot always refers to the Nth stored value.
51//! - `remember_with_key` and `remember_state_with_key` are key‑based and more
52//!   stable across conditional branches.
53//!
54//! ## Derived state
55//!
56//! `produce_state` computes a `Signal<T>` from other signals and recomputes it
57//! automatically when dependencies change:
58//!
59//! ```rust
60//! use repose_core::*;
61//!
62//! let first = signal("Jane".to_string());
63//! let last  = signal("Doe".to_string());
64//!
65//! let full = produce_state("full_name", {
66//!     let first = first.clone();
67//!     let last  = last.clone();
68//!     move || format!("{} {}", first.get(), last.get())
69//! });
70//!
71//! assert_eq!(full.get(), "Jane Doe");
72//! ```
73//!
74//! ## Effects and cleanup
75//!
76//! Use `scoped_effect_once` / `disposable_effect` for mount-once side-effects
77//! with cleanups:
78//!
79//! ```ignore
80//! use repose_core::*;
81//!
82//! fn Example() -> View {
83//!     scoped_effect_once(|| {
84//!         log::info!("Mounted Example");
85//!         on_unmount(|| log::info!("Unmounted Example"))
86//!     });
87//!
88//!     // ...
89//!     repose_ui::Box(Modifier::new())
90//! }
91//! ```
92//!
93//! - `effect` / `scoped_effect` run on every call and register cleanup on the
94//!   current `Scope`. Called directly in a composable body they re-run every
95//!   frame - use `effect_once` / `scoped_effect_once` for mount-once setup.
96//! - `disposable_effect(key, ..)` re-runs on key change and cleans up on
97//!   unmount (callsite-keyed, branch-stable).
98//! - `launched_effect!(key, ..)` is the cancellable launched variant;
99//!   `launched_effect_uncancelled!` is explicit fire-and-forget.
100//!
101//! For long‑running tasks (network, timers), prefer building small helpers on
102//! top of `disposable_effect` so everything cleans up correctly when the UI that
103//! owns it disappears.
104
105pub mod animation;
106pub mod animation_driver;
107pub mod clipboard;
108pub mod color;
109pub mod cursor;
110pub mod debounce;
111pub mod dnd;
112pub mod effects;
113pub mod effects_ext;
114pub mod error;
115pub mod focus;
116pub mod frame_clock;
117pub mod geometry;
118pub mod gesture;
119pub mod image_intrinsic;
120pub mod indication;
121pub mod input;
122pub mod locals;
123pub mod modifier;
124pub mod nested_scroll;
125pub mod prelude;
126pub mod present_mode;
127pub mod reactive;
128pub mod render_api;
129pub mod render_context;
130pub mod rumble;
131pub mod runtime;
132pub mod scope;
133pub mod scope_cache;
134pub mod scroll;
135pub mod semantics;
136pub mod shortcuts;
137pub mod signal;
138pub mod state;
139pub mod tests;
140pub mod text;
141pub mod units;
142
143pub mod timer;
144
145#[cfg(feature = "accesskit")]
146pub mod a11y;
147pub mod view;
148pub mod visual;
149
150pub use color::*;
151pub use cursor::*;
152pub use effects::*;
153pub use effects_ext::*;
154pub use focus::*;
155pub use frame_clock::{
156    peek_frame_request, request_frame, request_present, signal_fired, take_frame_request,
157    take_present_request, take_signal_fired,
158};
159pub use geometry::*;
160pub use gesture::*;
161pub use image_intrinsic::{
162    clear_image_intrinsic_size, image_intrinsic_size, set_image_intrinsic_size,
163};
164pub use locals::*;
165pub use modifier::*;
166pub use prelude::*;
167pub use present_mode::*;
168pub use reactive::*;
169pub use render_api::*;
170pub use render_context::{ImageHandleGuard, RenderCommand, RenderContext};
171pub use runtime::*;
172pub use runtime::{FocusDirection, FocusManager, FocusRequester, take_focus_request};
173pub use semantics::*;
174pub use signal::*;
175pub use state::*;
176pub use text::*;
177pub use units::*;
178pub use view::*;
179pub use visual::*;
180
181pub use repose_macros::View;
182
183/// Memoized composition scope with input + signal tracking.
184///
185/// Wraps a composable block, caching its output as long as:
186/// 1. The explicit inputs are unchanged (by `Hash` comparison).
187/// 2. No signal read during body execution has been written since last run.
188///
189/// When the cache is hit, the body is NOT executed -> the previously-composed
190/// View is returned instead, with proper ID and composer cursor advancement
191/// to keep sibling scopes consistent.
192///
193/// # Usage
194///
195/// ```ignore
196/// use repose_core::*;
197///
198/// fn MyView(s: &mut Scheduler, title: &str, count: i32) -> View {
199///     scope!("my_view", s, [title, count], {
200///         Column(Modifier::new()).child((
201///             Text(title),
202///             Text(format!("Count: {count}")),
203///         ))
204///     })
205/// }
206/// ```
207///
208/// Keys are global bare strings: two call sites sharing one key share one
209/// cache entry. Use `scope_auto!` for static call sites or `scope_keyed!`
210/// with a stable item id inside lists.
211///
212/// # Signal auto-tracking
213///
214/// Any `Signal::get()` call inside the body automatically registers the scope
215/// as a dependency. When that signal is written, the scope is marked dirty and
216/// recomposed on the next frame. You don't need to put signal values in the
217/// input list -> the reactive system handles dependencies implicitly.
218///
219/// ```ignore
220/// let size = signal(100.0);
221/// scope!("animated", s, [], {
222///     let cur = size.get();  // auto-tracked; cache invalidated on write
223///     Box(Modifier::new().size(cur, cur))
224/// })
225/// ```
226///
227/// # `f32`/`f64` in explicit inputs
228///
229/// Float types don't implement `Hash`. For float inputs, use `.to_bits()`:
230///
231/// ```ignore
232/// scope!("s", s, [my_float.to_bits()], { ... })
233/// ```
234///
235/// Or -> better -> read floats from a `Signal<f32>` inside the body (auto-tracked).
236///
237/// # Compatibility with `remember`
238///
239/// `remember` slots consumed inside the body are tracked and properly advanced
240/// on cache hit, so sibling `remember` calls remain consistent.
241#[macro_export]
242macro_rules! scope {
243    // With explicit inputs
244    ($key:expr, $s:expr, [$($input:expr),+ $(,)?], $body:block) => {{
245        let _key = $key.to_string();
246        let _key: &str = &_key;
247        $crate::scope_cache::validate_scope_key(_key);
248
249        let _input_hash = {
250            use std::hash::{Hash, Hasher};
251            let mut _hasher = std::collections::hash_map::DefaultHasher::new();
252            $(
253                Hash::hash(&$input, &mut _hasher);
254            )*
255            _hasher.finish()
256        };
257
258        if !$crate::scope_cache::should_run(_key, _input_hash) {
259            $crate::scope_cache::get_cached(_key, $s)
260        } else {
261            let _dirty_before = $crate::scope_cache::dirty_generation(_key);
262            $crate::scope_cache::clear_scope_deps(_key);
263
264            let _prev_cursor = $crate::runtime::COMPOSER.with(|c| c.borrow().cursor);
265
266            let _sched_guard = $s.scope_guard_raw(_key);
267            let mut _result = $crate::scope_cache::with_scope_key(_key, || $body);
268            drop(_sched_guard);
269
270            _result.modifier.repaint_boundary = true;
271            _result.scope_key = Some(_key.to_string());
272
273            let _slot_delta = $crate::runtime::COMPOSER.with(|c| c.borrow().cursor) - _prev_cursor;
274
275            $crate::scope_cache::set_cache_preserving_dirty(
276                _key,
277                _input_hash,
278                _result.clone(),
279                _slot_delta,
280                _dirty_before,
281            );
282
283            _result
284        }
285    }};
286
287    // Without explicit inputs -> skip Hash import
288    ($key:expr, $s:expr, [], $body:block) => {{
289        let _key = $key.to_string();
290        let _key: &str = &_key;
291        $crate::scope_cache::validate_scope_key(_key);
292        let _input_hash: u64 = 0;
293
294        if !$crate::scope_cache::should_run(_key, _input_hash) {
295            $crate::scope_cache::get_cached(_key, $s)
296        } else {
297            let _dirty_before = $crate::scope_cache::dirty_generation(_key);
298            $crate::scope_cache::clear_scope_deps(_key);
299
300            let _prev_cursor = $crate::runtime::COMPOSER.with(|c| c.borrow().cursor);
301
302            let _sched_guard = $s.scope_guard_raw(_key);
303            let mut _result = $crate::scope_cache::with_scope_key(_key, || $body);
304            drop(_sched_guard);
305
306            _result.modifier.repaint_boundary = true;
307            _result.scope_key = Some(_key.to_string());
308
309            let _slot_delta = $crate::runtime::COMPOSER.with(|c| c.borrow().cursor) - _prev_cursor;
310
311            $crate::scope_cache::set_cache_preserving_dirty(
312                _key,
313                _input_hash,
314                _result.clone(),
315                _slot_delta,
316                _dirty_before,
317            );
318
319            _result
320        }
321    }};
322}
323
324/// `scope!` with an auto-namespaced key (`module:file:line:col`). Use for
325/// static call sites so two components cannot share one cache entry.
326/// For dynamic lists, pass a stable item id as part of `$key` instead.
327#[macro_export]
328macro_rules! scope_auto {
329    ($s:expr, [$($input:expr),* $(,)?], $body:block) => {{
330        $crate::scope!(
331            concat!(module_path!(), ":", file!(), ":", line!(), ":", column!()),
332            $s,
333            [$($input),*],
334            $body
335        )
336    }};
337}
338
339/// `scope!` keyed by a stable item id plus the call site. Use inside loops /
340/// lazy lists so each item owns its cache entry.
341#[macro_export]
342macro_rules! scope_keyed {
343    ($s:expr, $id:expr, [$($input:expr),* $(,)?], $body:block) => {{
344        $crate::scope!(
345            format!(
346                "{}:{}:{}:{}:{}",
347                module_path!(),
348                file!(),
349                line!(),
350                column!(),
351                $id
352            ),
353            $s,
354            [$($input),*],
355            $body
356        )
357    }};
358}