WARNING: Cushy is in early alpha. This guide doubly so.
Reactive Data Model
Cushy is designed around a reactive data model. In many UI frameworks, setting
the text of a Label
is done by telling the label what it's new text
is. The Cushy way is to create a Dynamic<T>
containing the value you want to
display, and give a copy to the label and keep a copy for your application to
update as needed.
When a widget uses a Dynamic<T>
, it informs Cushy that it needs to be either
redrawn or invalidated when its contents change. In turn, when Cushy detects a
change, it invalidates what is necessary.
This means that updating a progress bar from a different thread is as simple as
passing a clone of a Dynamic<T>
that has been provided to a progress bar:
#![allow(unused)] fn main() { let progress = Dynamic::new(0_u8); std::thread::spawn({ let progress = progress.clone(); move || { while progress.get() < 10 { std::thread::sleep(Duration::from_millis(100)); progress.set(progress.get() + 1); } } }); progress.progress_bar_to(10) }
The above snippet produces this user interface:
This example just shows one simple way that Cushy's reactive model simplifies development. To learn more, let's dive into the data types and traits that power everything.
How widgets interact with data
In Cushy, it's conventional for widgets to accept data using one of these traits:
Trait | Target Type | Purpose |
---|---|---|
IntoValue<T> | Value<T> | For possibly constant or dynamic values. Can be either a T or a Dynamic<T> . |
IntoDynamic<T> | Dynamic<T> | For values that are read from and written to. |
IntoReadOnly<T> | ReadOnly<T> | For values that are read-only. Can be either a T or a DynamicReader<T> . |
IntoDynamicReader<T> | DynamicReader<T> | For values that are read-only, but are unexpected to be constant. In general, IntoValue<T> should be preferred if a single value makes sense to accept. |
Let's look at an example of how these traits are utilizes.
Label::new()
accepts the value it is to display as a
ReadOnly<T>
where T: Display + ...
.
This showcases Cushy's philosophy of embracing the Rust type system. Rather than
forcing Label
to receive a String
, it accepts any type that implements
Display
, This allows it to accept a wide variety of types.
Beyond basic values, it can also be given a special type that the Label
can
react to when updated: a Dynamic<T>
or a DynamicReader<T>
.
What is a Dynamic<T>
?
A Dynamic<T>
is a reference-counted, threadsafe, async-friendly
location in memory that can invoke a series of callbacks when its contents
change. Let's revisit the example from the intro:
use cushy::value::{Dynamic, Source}; use cushy::widget::MakeWidget; use cushy::widgets::input::{Input, InputValue}; use cushy::Run; fn main() -> cushy::Result { // Create storage for user to enter a name. let name: Dynamic<String> = Dynamic::default(); // Create our label by using `map_each` to format the name, first checking // if it is empty. let greeting = name.map_each(|name| { let name = if name.is_empty() { "World" } else { name }; format!("Hello, {name}!") }); // Create the input widget with a placeholder. let name_input: Input = name.into_input().placeholder("Name"); // Stack our widgets as rows, and run the app. name_input.and(greeting).into_rows().run() }
Both the Input
and the Label
widgets have been given
instances of Dynamic<String>
s, but they are two different dynamics. The text
input field was given the dynamic we want to be edited. We react to the changes
through the name.map_each(...)
callback.
What is a DynamicReader<T>
?
A DynamicReader<T>
provides read-only access to a
Dynamic<T>
, and also can:
- block the current thread until the underlying
Dynamic<T>
is changed. - wait for a change in an async task.
- Detect when the underlying
Dynamic<T>
has had all of its instances dropped.
DynamicReader<T>
s can be created using Dynamic::into_reader
/Dynamic::create_reader
.