Reactive Updates
OSUI incorporates a reactive programming model to automatically update parts of the UI in response to changes in application state. This system is built around State<T>, the DependencyHandler trait, and the DynWidget type, all orchestrated by the Screen's rendering loop.
The Problem: Manual UI Updates
In traditional UI frameworks without reactivity, when data changes, you would manually:
- Identify which UI elements depend on that data.
- Retrieve those elements.
- Update their properties or re-render them explicitly.
This can quickly become complex and error-prone in dynamic applications.
The Solution: OSUI's Reactivity
OSUI automates this process. When a State<T> value is modified, any DynWidget that has declared that State<T> as a dependency is automatically flagged for a rebuild. During the next render cycle, these flagged widgets are re-evaluated, reflecting the new data.
Key Components
1. State<T>: The Reactive Data Holder
State<T> is a generic struct that wraps your application data (T) and provides mechanisms for thread-safe access and change tracking.
- Internal Structure:
Arc<Mutex<Inner<T>>>Arc: AllowsStateinstances to be cheaply cloned and shared across multiple threads and widgets without ownership issues.Mutex: Ensures thread-safe access to the underlyingvalueand internal counters.Inner<T>: Containsvalue: T,dependencies: usize(how many widgets listen), andchanged: usize(how many dependents need a refresh).
- Modification:
my_state.set(new_value): Replaces the value and marks it as changed.**my_state.get() = new_value(ormy_state.get().deref_mut().field = new_value): Mutates the value directly through aMutexGuard. TheDerefMutimplementation automatically marks the state as changed by settinginner.changed = inner.dependencies.
- Dependency Tracking: Implements the
DependencyHandlertrait, allowingDynWidgets to register themselves.
(See Reference: State API for more details)
2. DependencyHandler Trait
A trait that State<T> (and potentially other future reactive types) implements. It defines two crucial methods:
add(): Called when aDynWidgetfirst registers itself as a listener to this dependency. It increments an internal counter of listeners.check(): Called byDynWidgetduring itsauto_refreshcycle. It decrements thechangedcounter and returnstrueif there are still pending changes to be processed by a listener. This ensures each listener processes a change only once per update cycle.
(See Reference: State API - DependencyHandler Trait for more details)
3. DynWidget: The Reactive Widget Wrapper
DynWidget is one of the two variants of the Widget enum (the other being StaticWidget). It is designed to be rebuilt when its dependencies change.
- Internal Structure: Holds:
- A
Mutex<Box<dyn FnMut() -> WidgetLoad>>: This is the original closure that built the widget. When a refresh is needed, this closure is re-executed to generate a newWidgetLoad. - A
Mutex<Vec<Box<dyn DependencyHandler>>>: A list of allState<T>instances (or otherDependencyHandlers) thisDynWidgetis listening to.
- A
- Key Methods:
dependency(d: D): Registers aDependencyHandlerwith this widget. This also callsd.add().refresh(): Forces the widget to rebuild immediately by re-executing its creation closure.auto_refresh(): The core of reactivity. It iterates through all registeredDependencyHandlers. Ifhandler.check()returnstruefor any of them, it callsrefresh()to rebuild the widget.
(See Reference: Widget API - DynWidget Struct for more details)
How Reactive Updates Work in Practice
Let's trace the flow with a simple counter example:
-
Define State:
let count = use_state(0);This creates an
Arc<Mutex<Inner<i32>>>wheredependenciesandchangedare initially0. -
Define Reactive UI (
rsx!):rsx! {
%count // Declare dependency on `count` state
Div {
"Current count: {count}" // `State<T>` implements `Display`
}
}.draw(&screen);- The
rsx!macro sees%count. This tells it to create aDynWidgetfor theDiv. - It clones
Arc<State<i32>>(thecountvariable) and captures it in theDynWidget's creation closure. - It calls
dyn_widget.dependency(count.clone()). This callscount.add(), incrementingcount.inner.dependenciesto1.
- The
-
Modify State:
// In a separate thread or event handler:
**count.get() += 1;count.get()locks theMutexand returns aMutexGuard<Inner<i32>>.**count.get()performs a mutable dereference tovalue: i32.- Crucially,
Inner<T>::deref_mut()is called, which then setscount.inner.changed = count.inner.dependencies(which is1in this case). - When the
MutexGuardis dropped, theMutexis released.
-
Render Loop (
Screen::render):- During the next animation frame,
Screen::renderiterates through its top-level widgets. - It encounters our
DynWidgetfor theDiv. - It calls
dyn_widget.auto_refresh(). auto_refresh()iterates through its registered dependencies (onlycountin this case).- It calls
count.check().count.inner.changedis1.count.check()decrementscount.inner.changedto0and returnstrue.
- Since
check()returnedtrue,dyn_widget.refresh()is called. refresh()re-executes theDynWidget's original creation closure.- The closure captures the
countstate (which now has the incremented value). - A new
DivElementinstance is created with the updated string:"Current count: 1". - This new
Elementand its initial components replace the old ones inside theDynWidget'sMutexes.
- The closure captures the
- The
Screenthen proceeds to render the updatedDivwith the correct text.
- During the next animation frame,
This cycle of state modification, dependency tracking, and automatic widget rebuilding forms the core of OSUI's reactivity, allowing you to focus on defining your UI's structure and behavior without constantly managing manual updates.
Performance Considerations
- Granularity: OSUI re-renders the entire widget (and its children) when any of its dependencies change. For large widgets with many children, consider breaking them into smaller, more granular
DynWidgets to minimize re-renders to only the affected parts of the UI tree. - Frequent Updates: If a
Stateis updated extremely frequently (e.g., every millisecond), it will trigger a refresh on every frame thatauto_refreshis called, which might be acceptable depending on complexity. get_dl()vs.get(): Useget_dl()when you only need to read a cloned value and do not intend to modify the state or hold the lock for an extended period. Useget()(andderef_mut()) when you need to modify the state.