State

bind

Turn any expression over signals into a property that updates itself, and accept literals or reactive values through IntoProp.

FNbind
A reactive color
let on = cx.signal(false);
 
hstack((
    toggle(on),
    text(bind(move |rt| if on.get(rt) { "On".to_owned() } else { "Off".to_owned() }))
        .text_color(bind(move |rt| if on.get(rt) { rgb(0x16a34a) } else { rgb(0x64748b) })),
))
Signature
pub fn bind<T, F>(compute: F) -> Bind<F>
where
    F: FnMut(&mut Runtime) -> T + 'static;
 
pub trait IntoProp<T> {
    fn into_prop(self) -> Prop<T>;
}
 
pub enum Prop<T> {
    Static(T),
    Dynamic(Box<dyn FnMut(&mut Runtime) -> T>),
}

bind wraps a closure so it can stand in for a property value. When the element mounts, the closure runs once and its result becomes the initial value. Every signal and memo it read becomes a dependency. When one changes, the closure runs again and the view applies the new result to that one property. Nothing else on the element or in the tree is rebuilt.

Every property in CrownUI takes impl IntoProp<T>. That one trait is why .p(16.0), .p(padding_signal) and .p(bind(..)) all compile.

Parameters

computeimpl FnMut(&mut Runtime) -> T + 'staticrequired

Produces the property value. It receives &mut Runtime, not &mut Cx, so it reads signals, memos and stores but cannot build views. It runs once at mount and again whenever a value it read changes.

Returns

Bind<F>IntoProp<T>

An opaque wrapper. Its only use is to be passed where an impl IntoProp<T> is expected.

What converts into a property

IntoProp<T> has one method, into_prop(self) -> Prop<T>. These types implement it:

SourceTarget TResult
f32, f64, i32, u32f32Prop::Static, converted with as f32
String, &str, &StringStringProp::Static, an owned copy
ColorColorProp::Static
boolboolProp::Static
VectorVectorProp::Static
MaterialMaterialProp::Static
Signal<T> where T: CloneTProp::Dynamic, calls get
Memo<T> where T: CloneTProp::Dynamic, calls get
Bind<F>what F returnsProp::Dynamic, runs F
Prop<T>Titself

A signal converts only to a property of its own type. Signal<i32> does not convert into IntoProp<f32>. Use bind(move |rt| count.get(rt) as f32) for that. The same rule applies to bind: a closure that returns &str does not fit a String property, so return an owned String.

Prop methods

mapfn<U: 'static>(self, impl FnMut(T) -> U + 'static) -> Prop<U> where T: 'static

Transforms the value. A static prop is mapped once, now. A dynamic prop applies the transform after every recompute.

Prop is in crownui::ext. You only need to name it when you match on it.

Examples

Pass a signal directly

When a property shows a signal as it is, skip bind.

let name = cx.signal(String::from("Untitled"));
let width = cx.signal(240.0_f32);
 
text(name).w(width)

Derive a string

bind is the usual way to format a value for display.

text(bind(move |rt| format!("{} of {}", page.get(rt), total.get(rt))))

Accept any value in your own component

Take impl IntoProp<T> in your function, and callers can pass a literal, a signal, a memo or a bind. Call into_prop() when you need to change the value.

fn caption(label: impl IntoProp<String>) -> impl View {
    let shouted = label.into_prop().map(|label| label.to_uppercase());
    text(shouted).text_size(12.0)
}
 
caption("Settings");
caption(title_signal);
caption(bind(move |rt| format!("{} items", count.get(rt))));

Share one computation between properties

Each bind runs on its own. If several properties need the same derived value, compute it once in a memo and pass the memo.

let overdue = cx.memo(move |rt| due.get(rt) < today.get(rt));
 
text(title)
    .text_color(bind(move |rt| if overdue.get(rt) { rgb(0xdc2626) } else { rgb(0x0f172a) }))
    .label(bind(move |rt| String::from(if overdue.get(rt) { "Overdue task" } else { "Task" })))

Read a style in a variant

Variant styles take the same props, so a hover color follows the theme too. use_theme comes from crownui::kit.

use crownui::kit::use_theme;
 
let theme = use_theme(cx);
 
button("Save")
    .bg(bind(move |rt| theme.with(rt, |theme| theme.surface.raised)))
    .hover(|style| style.bg(bind(move |rt| theme.with(rt, |theme| theme.surface.hover))))

Why it works this way

CrownUI builds the view once and never diffs it. The only way for something on screen to change is for a property to be bound to signals. bind makes that binding explicit and local: you see which property depends on what, and only that property is touched when the inputs change.

Each dynamic prop becomes a small effect. On mount it computes the first value, which the element uses directly. On later runs it pushes a command for that node into a queue, and the view applies the queue after the flush. A text update re-measures that one text node. A color update marks that one node for repaint.

The costs:

  • Each bind is one observer with its own closure allocation. A list of 1000 rows with 3 bound properties each holds 3000 observers. For large lists, prefer lazy_list so only visible rows exist.
  • Closures must be 'static, so they capture Copy handles by value with move. Capture data you need as owned values, or put it in a signal.
  • There is no equality check before a style command is applied. If a bound value often recomputes to the same result, put a memo in front of it.

See signal, memo, styled and State.