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 AmeView<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 AmeEntity:

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

new_amethystate panics with the error when the slice fails to open. try_new_amethystate takes the same closure and returns the error instead.

AmeEntity<T> is an alias for Entity<AmeView<T>>. AmeView 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 an external subscription and therefore do not notify the entity. Use .fork() when writing from a background thread if you want the UI to react.

The adapter builds on gpui-pre 0.3.6, a crates.io release of GPUI, and needs Rust 1.95. Depend on the same package under the name gpui, so the app and the adapter share one copy of the crate:

[dependencies]
gpui = { package = "gpui-pre", version = "0.3.6" }

Two copies, say the adapter’s and one from git, are two different crates to Cargo, and their types do not match.