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:

Threaded Progress Bar Update

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:

TraitTarget TypePurpose
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()
}

Hello Ferris Example

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.