State and Reactivity in OSUI
OSUI's reactivity model enables your UI to automatically update when underlying data changes, eliminating the need for manual DOM manipulation. This is achieved through the State<T> struct and its integration with DynWidgets via the DependencyHandler trait.
The Problem: Dynamic UIs
Imagine you have a counter that increments over time, and you want to display its current value in your TUI. Without a reactive system, you would manually:
- Get the new count.
- Clear the old count from the screen.
- Draw the new count at the correct position.
- Manage redraws if other elements shift.
This becomes complex quickly, especially with multiple, interconnected pieces of dynamic data.
The OSUI Solution: State<T>
State<T> is a generic type that wraps your data T and provides mechanisms to signal when T has changed. This signal then triggers a re-render of any UI widget that depends on that State.
1. Creating Reactive State: use_state()
The use_state function is the primary way to create a new State<T> instance:
use osui::prelude::*;
fn main() -> std::io::Result<()> {
let screen = Screen::new();
screen.extension(InputExtension); // Needed for event loop
// Create a new State<i32> initialized with 0
let counter = use_state(0);
// ... UI definition and screen.run()
Ok(())
}
State<T> internally uses Arc<Mutex<Inner<T>>>, making it Send + Sync and safely shareable across threads and widgets.
2. Modifying State and Triggering Updates
The most common way to interact with State<T> is via its get() method, which returns a MutexGuard<'_, Inner<T>>. The Inner<T> struct implements Deref and DerefMut for T, allowing you to treat state.get() much like a direct mutable reference to your data.
Key Point: When you use DerefMut (e.g., *state.get() = ... or *state.get() += 1), the State automatically registers that it has changed. This is critical for reactivity.
use osui::prelude::*;
use std::{thread, time::Duration};
fn main() -> std::io::Result<()> {
let screen = Screen::new();
screen.extension(InputExtension); // Required for terminal setup
let counter = use_state(0);
// Spawn a new thread to increment the counter every second.
// We clone `counter` (which is cheap due to Arc) to move it into the thread.
thread::spawn({
let counter_clone = counter.clone(); // Clone the Arc for the new thread
move || {
loop {
// Get a mutable lock on the counter state
// The DerefMut implementation on `Inner<i32>` will mark the state as changed
// (setting `inner.changed = inner.dependencies`)
*counter_clone.get() += 1;
thread::sleep(Duration::from_secs(1));
}
}
});
rsx! {
// Declare that this Div widget depends on the `counter` state.
// The `%counter` syntax is a critical part of connecting state to UI.
%counter
Div {
// Access the value of the state using `.get()`.
// `format!` macro will automatically call `Display` for `State<T>`.
format!("Current Count: {}", counter.get())
}
}
.draw(&screen);
screen.run()?;
Ok(())
}
In this example:
*counter_clone.get() += 1;modifies thei32value and simultaneously tellsState<i32>that it has been updated.- Because the
Divelement was declared with%counterinrsx!, it becomes aDynWidgetand registerscounteras one of its dependencies. - In the main rendering loop (inside
screen.run()),DynWidgets repeatedly calldependency.check(). Whencounter.check()returnstrue(because it was marked as changed), theDynWidgetrebuilds its internalElementand components, refreshing its content with the newcountervalue.
3. Declaring Dependencies in rsx!
The %variable_name syntax in the rsx! macro is how you declare that a widget depends on a State<T> variable (or any type implementing DependencyHandler).
let my_data = use_state("Initial".to_string());
// ...
rsx! {
%my_data // This Div will re-render when `my_data` changes
Div {
format!("Data: {}", my_data.get())
}
}
You can declare multiple dependencies: %state1 %state2 Div { ... }.
When to use State::set() or State::update()
State::set(new_value): Use this when you want to entirely replace theTvalue within theStateand mark it as changed.// Instead of: *my_state.get() = "New".to_string();
my_state.set("New".to_string());State::update(): Use this if you modify the innerTvalue through a path that doesn't triggerDerefMut(e.g., ifTis a complex struct and you modify one of its fields after getting a&mut Tbut without re-assigning the wholeT). This explicitly tellsStatethat it has changed.// If MyComplexData has an internal field modified, and you only got `&mut MyComplexData`
// but didn't reassign the whole struct.
let mut data_lock = my_complex_data.get();
data_lock.some_internal_field.modify();
// After modifying, you might need to manually update if DerefMut didn't catch it
// (though in most simple cases, DerefMut handles it automatically).
my_complex_data.update();
Cloning State<T>
Since State<T> uses Arc, cloning a State<T> instance is very cheap. It only increments the reference count of the shared Arc. This is how you pass State into closures or other threads without moving ownership.
let original_state = use_state(0);
let cloned_state = original_state.clone(); // This is just an Arc clone
By leveraging State<T> and dependency tracking, OSUI enables you to build dynamic, responsive terminal UIs with a clear separation of concerns between data and presentation.