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
: AllowsState
instances to be cheaply cloned and shared across multiple threads and widgets without ownership issues.Mutex
: Ensures thread-safe access to the underlyingvalue
and 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
. TheDerefMut
implementation automatically marks the state as changed by settinginner.changed = inner.dependencies
.
- Dependency Tracking: Implements the
DependencyHandler
trait, allowingDynWidget
s 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 aDynWidget
first registers itself as a listener to this dependency. It increments an internal counter of listeners.check()
: Called byDynWidget
during itsauto_refresh
cycle. It decrements thechanged
counter and returnstrue
if 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 otherDependencyHandler
s) thisDynWidget
is listening to.
- A
- Key Methods:
dependency(d: D)
: Registers aDependencyHandler
with 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 registeredDependencyHandler
s. Ifhandler.check()
returnstrue
for 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>>>
wheredependencies
andchanged
are 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 aDynWidget
for theDiv
. - It clones
Arc<State<i32>>
(thecount
variable) and captures it in theDynWidget
's creation closure. - It calls
dyn_widget.dependency(count.clone())
. This callscount.add()
, incrementingcount.inner.dependencies
to1
.
- The
-
Modify State:
// In a separate thread or event handler:
**count.get() += 1;count.get()
locks theMutex
and 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 is1
in this case). - When the
MutexGuard
is dropped, theMutex
is released.
-
Render Loop (
Screen::render
):- During the next animation frame,
Screen::render
iterates through its top-level widgets. - It encounters our
DynWidget
for theDiv
. - It calls
dyn_widget.auto_refresh()
. auto_refresh()
iterates through its registered dependencies (onlycount
in this case).- It calls
count.check()
.count.inner.changed
is1
.count.check()
decrementscount.inner.changed
to0
and returnstrue
.
- Since
check()
returnedtrue
,dyn_widget.refresh()
is called. refresh()
re-executes theDynWidget
's original creation closure.- The closure captures the
count
state (which now has the incremented value). - A new
Div
Element
instance is created with the updated string:"Current count: 1"
. - This new
Element
and its initial components replace the old ones inside theDynWidget
'sMutex
es.
- The closure captures the
- The
Screen
then proceeds to render the updatedDiv
with 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
DynWidget
s to minimize re-renders to only the affected parts of the UI tree. - Frequent Updates: If a
State
is updated extremely frequently (e.g., every millisecond), it will trigger a refresh on every frame thatauto_refresh
is 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.