State Management and Reactivity
OSUI provides a built-in, lightweight state management system that enables reactive updates to your UI. This system is centered around the State<T>
struct and the DependencyHandler
trait, allowing DynWidget
s to automatically re-render when their associated data changes.
State<T>
: Your Reactive Data Container
The State<T>
struct is a wrapper around your data T
that facilitates dependency tracking.
#[derive(Debug, Clone)]
pub struct State<T> {
inner: Arc<Mutex<Inner<T>>>,
}
#[derive(Debug)]
pub struct Inner<T> {
value: T,
dependencies: usize, // Number of widgets depending on this state
changed: usize, // Counter for changes waiting to be processed by dependents
}
Arc<Mutex<Inner<T>>>
: The core ofState<T>
is its use ofArc
andMutex
.Arc
allowsState<T>
instances to be shared across multiple widgets and threads without needing to clone the underlying dataT
itself, which is crucial forDynWidget
s that track multiple dependencies.Mutex
ensures safe concurrent access to thevalue
and metadata (dependencies
,changed
), preventing data races.
Creating State (use_state
)
You create a new State<T>
instance using the use_state
helper function:
pub fn use_state<T>(v: T) -> State<T> { /* ... */ }
Example:
use osui::prelude::*;
fn main() -> std::io::Result<()> {
let screen = Screen::new();
screen.extension(InputExtension);
screen.extension(RelativeFocusExtension::new());
// Create a new state variable for a counter
let count = use_state(0);
// Spawn a thread to increment the counter every second
std::thread::spawn({
let count = count.clone(); // Clone the Arc<State<T>> for the new thread
move || loop {
// Get a mutable lock on the Inner<T> to modify the value
// DerefMut implementation on Inner<T> automatically marks it as changed
*count.get() += 1;
std::thread::sleep(std::time::Duration::from_secs(1));
}
});
rsx! {
// Declare the widget as dependent on `count`
%count
Div {
// Access the value using Deref on Inner<T>
format!("This number increments: {}", count.get())
}
}.draw(&screen);
screen.run()?;
Ok(())
}
Accessing and Modifying State
State::get()
: Returns aMutexGuard<'_, Inner<T>>
. This provides mutable access to the underlyingvalue
withinInner<T>
.Inner<T>
implementsDeref
andDerefMut
forT
. This means you can treatcount.get()
like a direct reference toT
.- Crucially, when you use
DerefMut
(e.g.,*count.get() += 1
), thechanged
counter withinInner<T>
is automatically incremented. This is how OSUI knows the state has been modified and needs to trigger a re-render.
State::get_dl()
: (Short for "get, don't lock") Returns a cloned copy of the value. This is useful when you only need to read the value and want to avoid holding theMutexGuard
for longer than necessary, which can prevent deadlocks in complex scenarios. However, it doesn't mark the state as changed.State::set(v: T)
: Replaces the entire value and marks the state as changed.State::update()
: Explicitly marks the state as changed without modifying its value. Useful if internal parts ofT
are modified outside of directDerefMut
access.
DependencyHandler
Trait
The DependencyHandler
trait is the interface through which DynWidget
s observe changes in their dependencies. State<T>
implements this trait.
pub trait DependencyHandler: std::fmt::Debug + Send + Sync {
fn add(&self);
fn check(&self) -> bool;
}
add()
: Called when aDynWidget
registers itself as a dependent of thisState<T>
. It increments thedependencies
counter withinInner<T>
.check()
: Called byDynWidget
s (specifically byDynWidget::auto_refresh()
) to determine if the state has changed since the last check.- It decrements the
changed
counter if it's greater than zero, signifying that a change has been "consumed" by a dependent. - It returns
true
ifchanged
was greater than zero, indicating a fresh update.
- It decrements the
How Reactivity Works
- Widget Creation: When
rsx!
creates aDynWidget
with a%state_var
dependency,state_var.add()
is called, incrementingstate_var.inner.dependencies
. - State Modification: When
*state_var.get() = new_value
orstate_var.set(new_value)
is called, thestate_var.inner.changed
counter is set tostate_var.inner.dependencies
. This means all widgets currently depending on this state are marked for a refresh. - Automatic Refresh: In each rendering frame,
DynWidget::auto_refresh()
is called.- It iterates through its registered
DependencyHandler
s. - For each dependency, it calls
dependency.check()
. - If
check()
returnstrue
(meaning the state has changed and hasn't been consumed yet by this widget), theDynWidget
's internalload
closure is re-executed (self.refresh()
). This rebuilds the widget'sElement
andComponent
s, picking up the new state value. - The
check()
method decrements thechanged
counter, ensuring that a single modification to the state triggers exactly one rebuild for each dependent widget.
- It iterates through its registered
- Re-render: The rebuilt widget is then rendered on the next frame, reflecting the updated state.
This system provides a robust and efficient way to manage dynamic UI elements, abstracting away the complexities of manual DOM updates and allowing developers to focus on defining their UI's desired state.