guido 0.12.0

A reactive Rust GUI library using wgpu for Wayland layer shell widgets
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
//! Thread-local storage for signal values.
//!
//! Signal values are stored in a thread-local `RefCell`-protected vector,
//! using `Rc<dyn Any>` to erase what each slot holds — a `RefCell<T>` for a
//! mutable signal, the value for a stored one, the closure for a derived one.
//! This eliminates all locking overhead since signals are only accessed from
//! the main thread.
//!
//! ## Thread Safety
//!
//! - **Reading**: Direct `RefCell` borrow — zero locks.
//! - **Writing**: Direct `RefCell` borrow_mut — zero locks.
//! - **Background writes**: Queued via `WriteSignal` and flushed each frame.
//! - **Disposal**: Disposed signals are marked as `None` and will panic if accessed.
//!
//! ## Type Safety
//!
//! Type information is erased at storage but recovered at access time via `downcast_ref`.
//! Accessing a signal with the wrong type will panic with a clear error message.

use std::any::Any;
use std::cell::RefCell;
use std::rc::Rc;

use super::owner::{OwnerId, under_scope};
use super::runtime::SignalId;
use super::state::with_reactive;

type SignalValue = Rc<dyn Any>;

/// Storage slot for one signal. The generation survives disposal so recycled
/// indices can be told apart from their previous occupants — a stale
/// `SignalId` fails the generation check instead of silently aliasing.
struct Slot {
    value: Option<SignalValue>,
    generation: u32,
}

#[derive(Default)]
pub(crate) struct SignalStorage {
    slots: Vec<Slot>,
    /// Vacant slot indices available for reuse.
    free_indices: Vec<u32>,
}

impl SignalStorage {
    /// The storage the next `App` on this thread starts from: every slot
    /// vacant, every generation kept. Ids restarting from zero would let a
    /// handle the last `App` left behind pass the generation check against
    /// the first signal the next one creates.
    pub(crate) fn successor(&self) -> Self {
        Self {
            slots: self
                .slots
                .iter()
                .map(|slot| Slot {
                    value: None,
                    generation: slot.generation,
                })
                .collect(),
            free_indices: (0..self.slots.len() as u32).rev().collect(),
        }
    }
}

/// Briefly borrow storage to Rc::clone a signal's value handle.
///
/// Leptos-style: clone the Rc (O(1)), release the storage borrow, then let
/// the caller work with the value. Prevents re-entrant borrow panics when
/// user callbacks create new signals.
fn clone_slot_rc(id: SignalId, operation: &str) -> Rc<dyn Any> {
    with_reactive(|reactive| {
        let storage = reactive.storage.borrow();
        let slot = storage.slots.get(id.index()).unwrap_or_else(|| {
            panic!(
                "Invalid signal ID {}: out of bounds (storage has {} slots)",
                id,
                storage.slots.len()
            )
        });
        // A generation mismatch means the slot was recycled: the signal this
        // handle referred to is gone, same as the vacant-slot case below.
        if slot.generation != id.generation() {
            panic!(
                "Signal {} was disposed - cannot {} after owner cleanup. \
                 This usually means the signal's owner was disposed while you still hold a reference to the signal.",
                id, operation
            );
        }
        let value = slot.value.as_ref().unwrap_or_else(|| {
            panic!(
                "Signal {} was disposed - cannot {} after owner cleanup. \
                 This usually means the signal's owner was disposed while you still hold a reference to the signal.",
                id, operation
            )
        });
        Rc::clone(value)
    })
}

/// Like [`clone_slot_rc`] but returns None for vacant/recycled slots
/// instead of panicking. Used by write paths: writes race disposal by
/// design (queued `WriteSignal` flushes, delayed timers) and must degrade
/// to a no-op, not a crash.
fn try_clone_slot_rc(id: SignalId) -> Option<Rc<dyn Any>> {
    with_reactive(|reactive| {
        let storage = reactive.storage.borrow();
        let slot = storage.slots.get(id.index())?;
        if slot.generation != id.generation() {
            return None;
        }
        slot.value.as_ref().map(Rc::clone)
    })
}

/// Get a reference to the signal's RefCell, handling errors consistently.
fn with_signal_cell<T: 'static, R>(
    id: SignalId,
    operation: &str,
    f: impl FnOnce(&RefCell<T>) -> R,
) -> R {
    let rc = clone_slot_rc(id, operation);
    let cell = rc.downcast_ref::<RefCell<T>>().unwrap_or_else(|| {
        panic!(
            "Signal {} type mismatch: stored type does not match requested type {}",
            id,
            std::any::type_name::<T>()
        )
    });
    f(cell)
}

/// Allocate a slot and store the given value. Reuses slot indices from
/// disposed signals, bumping the generation so stale handles stay invalid.
fn alloc_slot(value: Rc<dyn Any>) -> SignalId {
    with_reactive(|reactive| {
        let mut storage = reactive.storage.borrow_mut();
        if let Some(index) = storage.free_indices.pop() {
            let slot = &mut storage.slots[index as usize];
            slot.generation = slot.generation.wrapping_add(1);
            slot.value = Some(value);
            return SignalId::new(index, slot.generation);
        }
        let index = storage.slots.len() as u32;
        storage.slots.push(Slot {
            value: Some(value),
            generation: 0,
        });
        SignalId::new(index, 0)
    })
}

/// Create a new mutable signal value (`Rc<RefCell<T>>`) and return its ID.
pub fn create_signal_value<T: 'static>(value: T) -> SignalId {
    alloc_slot(Rc::new(RefCell::new(value)))
}

/// Create a new immutable stored value (`Rc<T>`, no RefCell) and return its ID.
///
/// This is the cheap path for `create_stored()`: no RefCell wrapping, and the
/// caller skips runtime registration and dependency tracking. Saves per-signal:
/// - 8 bytes (no RefCell borrow flag)
/// - One `Runtime::register_signal` call
/// - `record_effect_read()` + `record_signal_read()` on every `.get()` call
pub fn create_stored_value<T: 'static>(value: T) -> SignalId {
    alloc_slot(Rc::new(value))
}

/// A derived signal's closure, and the scope it was written in.
///
/// The scope travels with the closure because the two are separated in time: a
/// property is declared once, in a widget factory, and its body runs on every
/// read from whatever phase reads it. Resolving a context against *that* would
/// answer the row's declaration when layout asked and nothing when paint did
/// (#335), so the closure carries where it was written and is run there.
///
/// It is the slot's value, so a read finds the closure by index, as it would a
/// stored value. The scope is held inside the `Rc`'s allocation rather than
/// beside it: the slot stays one 16-byte handle, and the eight bytes go where
/// an allocation already was.
struct Derived<T> {
    scope: Option<OwnerId>,
    call: Box<dyn Fn() -> T>,
}

/// Store a derived closure, with the scope it is written in, in a slot of its
/// own and return its ID.
pub fn create_derived_value<T: 'static>(closure: impl Fn() -> T + 'static) -> SignalId {
    let scope = with_reactive(|reactive| reactive.current_owner.get());
    alloc_slot(Rc::new(Derived {
        scope,
        call: Box::new(closure),
    }))
}

/// Call a derived signal's closure, under the scope it was written in.
///
/// Leptos-style: Rc::clone the closure handle and release the storage borrow
/// before calling the closure (which will read other signals from storage).
pub fn call_derived<T: 'static>(id: SignalId) -> T {
    with_stored_ref(id, |derived: &Derived<T>| {
        under_scope(derived.scope, || (derived.call)())
    })
}

/// Dispose a signal, marking it as unavailable and adding its ID to the free list.
///
/// After disposal, any attempt to read or write the signal will panic
/// with a clear error message. The ID will be reused by the next `create_signal_value`.
pub fn dispose_signal(id: SignalId) {
    with_reactive(|reactive| {
        let mut storage = reactive.storage.borrow_mut();
        let Some(slot) = storage
            .slots
            .get_mut(id.index())
            .filter(|slot| slot.generation == id.generation())
        else {
            return; // Stale id: slot already recycled, nothing to dispose
        };
        if slot.value.take().is_some() {
            storage.free_indices.push(id.index() as u32);
        }
    });
}

/// Get a stored (immutable) value by cloning it.
///
/// Reads `Rc<T>` directly — no RefCell borrow needed since the value is immutable.
/// Used by `Signal::get()` for `SignalKind::Stored` signals.
pub fn get_stored_value<T: Clone + 'static>(id: SignalId) -> T {
    with_stored_ref(id, |v: &T| v.clone())
}

/// Borrow a stored (immutable) value for reading.
///
/// Reads `Rc<T>` directly — no RefCell borrow needed.
pub fn with_stored_value<T: 'static, R>(id: SignalId, f: impl FnOnce(&T) -> R) -> R {
    with_stored_ref(id, f)
}

/// Borrow the Rc<T> for a stored (immutable) signal and apply `f`.
fn with_stored_ref<T: 'static, R>(id: SignalId, f: impl FnOnce(&T) -> R) -> R {
    let rc = clone_slot_rc(id, "read");
    let val = rc.downcast_ref::<T>().unwrap_or_else(|| {
        panic!(
            "Signal {} type mismatch: stored type does not match requested type {}",
            id,
            std::any::type_name::<T>()
        )
    });
    f(val)
}

/// Get a mutable signal's value (clones it)
pub fn get_signal_value<T: Clone + 'static>(id: SignalId) -> T {
    with_signal_cell(id, "read", |cell: &RefCell<T>| cell.borrow().clone())
}

/// Read a mutable signal's value, or `None` if its slot is gone.
///
/// The read-side mirror of the `try_*` writes below, for the one reader that
/// races disposal by design: the widget-ref registry outlives the scopes that
/// created the handles it holds, and looking is how it finds out.
pub fn try_get_signal_value<T: Clone + 'static>(id: SignalId) -> Option<T> {
    let rc = try_clone_slot_rc(id)?;
    Some(downcast_cell::<T>(&rc, id).borrow().clone())
}

/// Set a signal's value (in-place replace, no Box allocation)
pub fn set_signal_value<T: 'static>(id: SignalId, value: T) {
    with_signal_cell(id, "write", |cell: &RefCell<T>| {
        *cell.borrow_mut() = value;
    });
}

/// Update a signal's value with a closure. Returns the closure's result.
pub fn update_signal_value<T: 'static, R>(id: SignalId, f: impl FnOnce(&mut T) -> R) -> R {
    with_signal_cell(id, "update", |cell: &RefCell<T>| f(&mut cell.borrow_mut()))
}

/// Borrow a signal's value for reading
pub fn with_signal_value<T: 'static, R>(id: SignalId, f: impl FnOnce(&T) -> R) -> R {
    with_signal_cell(id, "borrow", |cell: &RefCell<T>| f(&cell.borrow()))
}

/// Compare and set: if the new value differs from the current value, replace it.
/// Returns `true` if the value was changed.
///
/// This performs the comparison and write in a single `with_signal_cell` call,
/// avoiding the overhead of two separate storage accesses.
pub fn compare_and_set_signal_value<T: PartialEq + 'static>(id: SignalId, value: T) -> bool {
    let Some(rc) = try_clone_slot_rc(id) else {
        // Writes race disposal by design (queued flushes, delayed timers):
        // dropping the write is the correct outcome, not a panic
        log::warn!("ignoring write to disposed signal {id}");
        return false;
    };
    let cell = downcast_cell::<T>(&rc, id);
    let mut current = cell.borrow_mut();
    if *current != value {
        *current = value;
        true
    } else {
        false
    }
}

/// Compare and update: clone the old value, apply the closure, compare, and return
/// whether the value changed. All in a single `with_signal_cell` call.
pub fn compare_and_update_signal_value<T: Clone + PartialEq + 'static>(
    id: SignalId,
    f: impl FnOnce(&mut T),
) -> bool {
    let Some(rc) = try_clone_slot_rc(id) else {
        log::warn!("ignoring update of disposed signal {id}");
        return false;
    };
    let cell = downcast_cell::<T>(&rc, id);
    let mut current = cell.borrow_mut();
    let old = current.clone();
    f(&mut current);
    old != *current
}

/// Set unconditionally: no comparison, no `PartialEq` bound. Returns
/// `true` if the write landed (`false` only when the slot is disposed).
///
/// Backs [`RwSignal::set_always`](super::RwSignal::set_always): trigger-style writes where every
/// emission must notify (or where the type has no meaningful equality).
pub fn set_signal_value_always<T: 'static>(id: SignalId, value: T) -> bool {
    let Some(rc) = try_clone_slot_rc(id) else {
        log::warn!("ignoring write to disposed signal {id}");
        return false;
    };
    let cell = downcast_cell::<T>(&rc, id);
    *cell.borrow_mut() = value;
    true
}

/// Update unconditionally: no comparison, no `PartialEq` bound. Returns
/// `true` if the update ran (`false` only when the slot is disposed).
pub fn update_signal_value_always<T: 'static>(id: SignalId, f: impl FnOnce(&mut T)) -> bool {
    let Some(rc) = try_clone_slot_rc(id) else {
        log::warn!("ignoring update of disposed signal {id}");
        return false;
    };
    let cell = downcast_cell::<T>(&rc, id);
    f(&mut cell.borrow_mut());
    true
}

fn downcast_cell<T: 'static>(rc: &Rc<dyn Any>, id: SignalId) -> &RefCell<T> {
    rc.downcast_ref::<RefCell<T>>().unwrap_or_else(|| {
        panic!(
            "Signal {} type mismatch: stored type does not match requested type {}",
            id,
            std::any::type_name::<T>()
        )
    })
}

/// How many signal slots storage currently holds — every slot ever allocated,
/// occupied or vacated, which is what makes it the right measure for "did this
/// primitive claim one". [`live_signal_count`] counts only the occupied ones.
///
/// Test-only: the cost of a primitive is only assertable against a number.
#[cfg(test)]
pub(crate) fn slot_count() -> usize {
    with_reactive(|reactive| reactive.storage.borrow().slots.len())
}

/// Throw the signal arena away without the rest of a teardown.
///
/// What `App::drop` does to it, on its own: for the tests that ask what a
/// handle answers once the values it names are gone. The whole teardown is
/// [`crate::reactive::state::reset`].
#[cfg(test)]
pub(crate) fn discard_all_signals() {
    with_reactive(|reactive| reactive.storage.take());
}

/// How many signal slots currently hold a value.
///
/// For leak tests: build something, tear it down, and the count has to come
/// back to where it started.
pub fn live_signal_count() -> usize {
    with_reactive(|reactive| {
        reactive
            .storage
            .borrow()
            .slots
            .iter()
            .filter(|slot| slot.value.is_some())
            .count()
    })
}

/// Check if a signal exists in the current thread's storage.
/// Used by `WriteSignal` to determine if we can write directly (same thread)
/// or must queue the write for the main thread.
pub fn has_signal(id: SignalId) -> bool {
    with_reactive(|reactive| {
        let storage = reactive.storage.borrow();
        storage
            .slots
            .get(id.index())
            .filter(|slot| slot.generation == id.generation())
            .and_then(|slot| slot.value.as_ref())
            .is_some()
    })
}

#[cfg(test)]
mod dispose_write_tests {
    use crate::reactive::create_signal;
    use crate::reactive::owner::{dispose_owner_now, with_owner};

    /// Writes race disposal by design (queued WriteSignal flushes, delayed
    /// timers): they must be dropped, not panic.
    #[test]
    fn write_to_disposed_signal_is_a_noop() {
        let (sig, owner) = with_owner(|| create_signal(1u32));
        dispose_owner_now(owner);
        sig.set(2); // must not panic
        sig.update(|v| *v += 1); // must not panic
    }
}

#[cfg(test)]
mod derived_scope_tests {
    use super::live_signal_count;
    use crate::reactive::owner::{dispose_owner_now, with_owner};
    use crate::reactive::{create_derived, create_signal};

    /// The other half of #335's rule: a derived closure's body runs under the
    /// scope the closure was written in, so what the body *makes* is filed
    /// there too — not under whoever happened to read it.
    ///
    /// Read from outside that scope, and with the reads' signals filed under
    /// nothing, they would outlive the closure that made them for the life of
    /// the application. Disposing the scope has to take them.
    #[test]
    fn what_a_derived_body_makes_belongs_to_the_scope_the_closure_was_written_in() {
        let baseline = live_signal_count();

        let (derived, scope) = with_owner(|| create_derived(|| create_signal(7u32).get()));
        assert_eq!(derived.get(), 7);
        assert_eq!(derived.get(), 7);
        assert!(
            live_signal_count() > baseline,
            "the reads made signals, which is what this is about"
        );

        dispose_owner_now(scope);

        assert_eq!(
            live_signal_count(),
            baseline,
            "everything the closure made went with the scope it was written in"
        );
    }
}