Skip to content

Complete example

A full counter — real gpui view, projected window, and both drivers in sync. This is examples/gpui/counter, trimmed of comments.

rust
use gpui::*;
use gpui_component::{Root, button::{Button, ButtonVariants as _}, h_flex, v_flex};
use purview_gpui::{ActionError, Bridge, Outcome, PvFields, PvWindow, on_entity};

struct Counter {
    count: i64,
    field: PvFields,   // the projection handle lives in the view
}

impl Counter {
    /// The single mutation path: real state and projection move together.
    fn bump(&mut self, delta: i64, cx: &mut Context<Self>) {
        self.count += delta;
        self.field.set("Count", self.count);
        cx.notify();
    }
}

impl Render for Counter {
    fn render(&mut self, _w: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
        v_flex().gap_4().size_full().items_center().justify_center()
            .child(format!("Count: {}", self.count))
            .child(h_flex().gap_2()
                .child(Button::new("dec").label("-1")
                    .on_click(cx.listener(|this, _, _, cx| this.bump(-1, cx))))
                .child(Button::new("inc").primary().label("+1")
                    .on_click(cx.listener(|this, _, _, cx| this.bump(1, cx)))))
    }
}

fn register_actions(win: &PvWindow, counter: &Entity<Counter>) {
    win.actions().add("increment", "Increment").on(on_entity(counter, |c, cx| {
        c.bump(1, cx);
        Ok::<_, ActionError>(Outcome::message(format!("count = {}", c.count)))
    }));
    win.actions().add("decrement", "Decrement").on(on_entity(counter, |c, cx| {
        c.bump(-1, cx);
        Ok::<_, ActionError>(Outcome::message(format!("count = {}", c.count)))
    }));
}

fn main() {
    Application::new()
        .with_assets(gpui_component_assets::Assets)
        .run(|cx: &mut App| {
            gpui_component::init(cx);

            let papp = Bridge::new()
                .instructions_append("A counter. Use increment/decrement.")
                .http()
                .bind("127.0.0.1:8931")
                .expect("bind the MCP port")
                .install(cx);

            let win = papp.open("Counter");
            win.set_summary("A counter you can drive from the UI or an agent");
            let field = win.blocks().fields([("Count", 0)]);

            let options = WindowOptions {
                window_bounds: Some(WindowBounds::centered(size(px(300.), px(180.)), cx)),
                ..Default::default()
            };
            cx.open_window(options, move |window, cx| {
                window.set_window_title("Counter");
                let counter = cx.new(|_| Counter { count: 0, field });
                register_actions(&win, &counter);
                cx.new(|cx| Root::new(counter, window, cx))
            })
            .expect("failed to open window");
        });
}

Note what is not there: no channel, no drain loop, no server handle to keep alive. And bump() is the only writer — clicking a button and calling {winId}__increment take the same path.