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 `effect` / `scoped_effect` for one‑off side‑effects with cleanups:
77//!
78//! ```ignore
79//! use repose_core::*;
80//!
81//! fn Example() -> View {
82//!     scoped_effect(|| {
83//!         log::info!("Mounted Example");
84//!         on_unmount(|| log::info!("Unmounted Example"))
85//!     });
86//!
87//!     // ...
88//!     repose_ui::Box(Modifier::new())
89//! }
90//! ```
91//!
92//! - `effect` runs once when the view is composed and returns a `Dispose`
93//!   guard that will be run when the scope is torn down.
94//! - `scoped_effect` is wired to the current `Scope` and is cleaned up on
95//!   scope disposal (e.g. when a navigation entry is popped).
96//!
97//! For long‑running tasks (network, timers), prefer building small helpers on
98//! top of `scoped_effect` so everything cleans up correctly when the UI that
99//! owns it disappears.
100
101pub mod animation;
102pub mod animation_driver;
103pub mod clipboard;
104pub mod color;
105pub mod cursor;
106pub mod dnd;
107pub mod effects;
108pub mod effects_ext;
109pub mod error;
110pub mod focus;
111pub mod frame_clock;
112pub mod geometry;
113pub mod gesture;
114pub mod indication;
115pub mod input;
116pub mod locals;
117pub mod modifier;
118pub mod nested_scroll;
119pub mod prelude;
120pub mod present_mode;
121pub mod reactive;
122pub mod render_api;
123pub mod render_context;
124pub mod runtime;
125pub mod debounce;
126pub mod scope;
127pub mod scope_cache;
128pub mod scroll;
129pub mod semantics;
130pub mod shortcuts;
131pub mod signal;
132pub mod state;
133pub mod tests;
134pub mod text;
135
136#[cfg(feature = "accesskit")]
137pub mod a11y;
138pub mod view;
139
140pub use color::*;
141pub use cursor::*;
142pub use dnd::*;
143pub use effects::*;
144pub use effects_ext::*;
145pub use focus::*;
146pub use frame_clock::{
147    peek_frame_request, request_frame, request_present, signal_fired, take_frame_request,
148    take_present_request, take_signal_fired,
149};
150pub use geometry::*;
151pub use gesture::*;
152pub use indication::*;
153pub use locals::*;
154pub use modifier::*;
155pub use prelude::*;
156pub use present_mode::*;
157pub use reactive::*;
158pub use render_api::*;
159pub use render_context::{ImageHandleGuard, RenderCommand, RenderContext};
160pub use runtime::*;
161pub use runtime::{FocusDirection, FocusManager, FocusRequester, take_focus_request};
162pub use semantics::*;
163pub use signal::*;
164pub use state::*;
165pub use text::*;
166pub use view::*;
167
168pub use repose_macros::View;
169
170/// Memoized composition scope with input + signal tracking.
171///
172/// Wraps a composable block, caching its output as long as:
173/// 1. The explicit inputs are unchanged (by `Hash` comparison).
174/// 2. No signal read during body execution has been written since last run.
175///
176/// When the cache is hit, the body is NOT executed -> the previously-composed
177/// View is returned instead, with proper ID and composer cursor advancement
178/// to keep sibling scopes consistent.
179///
180/// # Usage
181///
182/// ```ignore
183/// use repose_core::*;
184///
185/// fn MyView(s: &mut Scheduler, title: &str, count: i32) -> View {
186///     scope!("my_view", s, [title, count], {
187///         Column(Modifier::new()).child((
188///             Text(title),
189///             Text(format!("Count: {count}")),
190///         ))
191///     })
192/// }
193/// ```
194///
195/// # Signal auto-tracking
196///
197/// Any `Signal::get()` call inside the body automatically registers the scope
198/// as a dependency. When that signal is written, the scope is marked dirty and
199/// recomposed on the next frame. You don't need to put signal values in the
200/// input list -> the reactive system handles dependencies implicitly.
201///
202/// ```ignore
203/// let size = signal(100.0);
204/// scope!("animated", s, [], {
205///     let cur = size.get();  // auto-tracked; cache invalidated on write
206///     Box(Modifier::new().size(cur, cur))
207/// })
208/// ```
209///
210/// # `f32`/`f64` in explicit inputs
211///
212/// Float types don't implement `Hash`. For float inputs, use `.to_bits()`:
213///
214/// ```ignore
215/// scope!("s", s, [my_float.to_bits()], { ... })
216/// ```
217///
218/// Or -> better -> read floats from a `Signal<f32>` inside the body (auto-tracked).
219///
220/// # Compatibility with `remember`
221///
222/// `remember` slots consumed inside the body are tracked and properly advanced
223/// on cache hit, so sibling `remember` calls remain consistent.
224#[macro_export]
225macro_rules! scope {
226    // With explicit inputs
227    ($key:expr, $s:expr, [$($input:expr),+ $(,)?], $body:block) => {{
228        let _key: &str = $key;
229
230        let _input_hash = {
231            use std::hash::{Hash, Hasher};
232            let mut _hasher = std::collections::hash_map::DefaultHasher::new();
233            $(
234                Hash::hash(&$input, &mut _hasher);
235            )*
236            _hasher.finish()
237        };
238
239        if !$crate::scope_cache::should_run(_key, _input_hash) {
240            $crate::scope_cache::get_cached(_key, $s)
241        } else {
242            $crate::scope_cache::clear_scope_deps(_key);
243
244            let _prev_cursor = $crate::runtime::COMPOSER.with(|c| c.borrow().cursor);
245
246            $s.enter_scope(_key);
247            let mut _result = $crate::scope_cache::with_scope_key(_key, || $body);
248            $s.exit_scope();
249
250            _result.modifier.repaint_boundary = true;
251            _result.scope_key = Some(_key.to_string());
252
253            let _slot_delta = $crate::runtime::COMPOSER.with(|c| c.borrow().cursor) - _prev_cursor;
254
255            $crate::scope_cache::set_cache(_key, _input_hash, _result.clone(), _slot_delta);
256
257            _result
258        }
259    }};
260
261    // Without explicit inputs -> skip Hash import
262    ($key:expr, $s:expr, [], $body:block) => {{
263        let _key: &str = $key;
264        let _input_hash: u64 = 0;
265
266        if !$crate::scope_cache::should_run(_key, _input_hash) {
267            $crate::scope_cache::get_cached(_key, $s)
268        } else {
269            $crate::scope_cache::clear_scope_deps(_key);
270
271            let _prev_cursor = $crate::runtime::COMPOSER.with(|c| c.borrow().cursor);
272
273            $s.enter_scope(_key);
274            let mut _result = $crate::scope_cache::with_scope_key(_key, || $body);
275            $s.exit_scope();
276
277            _result.modifier.repaint_boundary = true;
278            _result.scope_key = Some(_key.to_string());
279
280            let _slot_delta = $crate::runtime::COMPOSER.with(|c| c.borrow().cursor) - _prev_cursor;
281
282            $crate::scope_cache::set_cache(_key, _input_hash, _result.clone(), _slot_delta);
283
284            _result
285        }
286    }};
287}