Skip to content

GPUI

GPUI uses an entity model with deferred notification — mutations happen inside entity update closures, and the framework notifies dependents after the closure returns. Synchronous Field<T> subscriptions don’t compose with this model directly, so the integration bridges them through an async channel.

amethystate-gpui provides RpView<T> — a wrapper that holds a state slice and a ReactiveScope. On construction it subscribes to all external changes on the slice and sends a unit message over an unbounded channel. A background task inside the entity drains that channel and calls entity_cx.notify(), which triggers a GPUI re-render.

This means GPUI reads state synchronously during render via .get(), while change detection happens asynchronously in the background.

[dependencies]
amethystate-gpui = "*"

Initialize the store before opening any windows:

StoreBuilder::new("./app.redb").init_global();
#[amethystate(prefix = "counter")]
pub struct CounterState {
#[amestate(default = 0)]
pub count: i32,
}

Use cx.new_amethystate() instead of cx.new() to wrap a state slice in an RpEntity:

struct CounterView {
state: RpEntity<CounterState>,
}
impl CounterView {
fn new(cx: &mut Context<Self>) -> Self {
let state = cx.new_amethystate(CounterState::new).unwrap();
Self { state }
}
}

RpEntity<T> is an alias for Entity<RpView<T>>. RpView<T> derefs to T, so state fields are accessed directly through the entity.

impl Render for CounterView {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let current_count = self.state.read(cx).count().get();
div().child(format!("Count: {}", current_count))
}
}

Writes can happen from anywhere — the entity’s on_click handler, a background thread, another part of the app. Any external write triggers a notify() and a re-render:

// from a click handler inside render
let state = self.state.clone();
Button::new("Increment")
.on_click(move |_, _, cx| {
state.read(cx).count().update(|v| v + 1).ok();
})
// from a background thread via fork
let forked = state.read(cx).fork();
std::thread::spawn(move || {
loop {
std::thread::sleep(Duration::from_secs(2));
forked.count().update(|v| v + 1).ok();
}
});

Note that writes from the same instance (non-forked) do not trigger subscribe_external and therefore do not notify the entity. Use .fork() when writing from a background thread if you want the UI to react.

If your project depends on a git version of GPUI, add a [patch] to your workspace Cargo.toml to ensure a single copy of the crate is used:

[patch.crates-io]
gpui = { git = "https://github.com/zed-industries/zed", rev = "abc123" }

Without this, Cargo will treat the crates.io and git versions as separate crates and you’ll get type mismatch errors at compile time.