Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Counter Example

Live Demo

Overview

The counter is the minimal vgui application. It demonstrates:

  • create_signal for state management.
  • create_memo for a derived value (doubled).
  • <Show> for conditional rendering (positive/negative/zero/odd/even).
  • Component functions taking ReadSignal and WriteSignal as props.
  • twc! macro for dynamic Tailwind class composition.
  • Tailwind classes via class= with hover: variants.

Source Code

#![cfg_attr(target_family = "wasm", no_main)]

use gpui::{px, size, App, Bounds, WindowBounds, WindowOptions};
use vgui::prelude::*;

#[cfg(not(target_family = "wasm"))]
use gpui_platform::application;

#[cfg(target_family = "wasm")]
use gpui_platform::single_threaded_web;

fn dynamic_button(
    label: String,
    count: ReadSignal<i32>,
    set_count: WriteSignal<i32>,
    delta: i32,
) -> impl gpui::IntoElement {
    view! {
        <button
            class={twc!(
                "p-2 rounded text-white",
                (delta > 0).then_some("bg-[#0000ff] hover:bg-[#000088]"),
                (delta < 0).then_some("bg-[#FF0000] hover:bg-[#880000]"),
                (count.get() == 0).then_some("bg-[#666666] hover:bg-[#444444]")
            )}
            on:click={click(move |cx| set_count.update(cx, |n| *n += delta))}
        >
            {label}
        </button>
    }
}

fn app() -> impl gpui::IntoElement {
    let (count, set_count) = create_signal(0i32);
    let doubled = create_memo({
        let count = count.clone();
        move || count.get() * 2
    });
    view! {
        <div class="flex flex-col gap-3 p-4 bg-[#505050] w-[500px] h-[500px] justify-center items-center text-white">
            <span>{format!("Hello, {}!", count.get())}</span>
            <span>{format!("doubled {}", doubled.get())}</span>
            <Show when={count.get() > 0}>
                <span>{"positive"}</span>
            </Show>
            <Show when={count.get() < 0}>
                <span>{"negative"}</span>
            </Show>
            <Show when={count.get() == 0}>
                <span>{"zero"}</span>
            </Show>
            <Show when={count.get() & 1 == 1} fallback={view! { <span>{"even"}</span> }}>
                <span>{"odd"}</span>
            </Show>
            {dynamic_button(String::from("Increment"), count.clone(), set_count.clone(), 1)}
            {dynamic_button(String::from("Decrement"), count.clone(), set_count.clone(), -1)}
        </div>
    }
}

fn run() {
    #[cfg(not(target_family = "wasm"))]
    let gpui_app = application();

    #[cfg(target_family = "wasm")]
    let gpui_app = single_threaded_web();

    let launch = |cx: &mut App| {
        let bounds = Bounds::centered(None, size(px(500.), px(500.0)), cx);
        cx.open_window(
            WindowOptions {
                window_bounds: Some(WindowBounds::Windowed(bounds)),
                ..Default::default()
            },
            |window, cx| vgui::mount(window, cx, app),
        )
        .unwrap();
    };

    #[cfg(not(target_family = "wasm"))]
    gpui_app.run(launch);

    #[cfg(target_family = "wasm")]
    std::mem::forget(gpui_app.run_embedded(launch));
}

#[cfg(not(target_family = "wasm"))]
fn main() {
    run();
}

#[cfg(target_family = "wasm")]
#[wasm_bindgen::prelude::wasm_bindgen(start)]
pub fn start() {
    gpui_platform::web_init();
    vgui::intercept_keyboard_events();
    run();
}

Key Concepts

Signal + memo

create_signal(0i32) creates the count state. create_memo derives doubled from it — the memo recomputes only when count changes, not on every render.

<Show> with and without fallback

Four <Show> blocks demonstrate both forms. The first three have no fallback (render nothing when false). The fourth uses fallback to show “even” when the count is not odd.

dynamic_button with twc!

A single dynamic_button function takes (label, count: ReadSignal, set_count: WriteSignal, delta) and returns impl IntoElement. The twc! macro composes conditional Tailwind classes at runtime: a base string plus Option<&str> arguments that are included only when their condition is true. The increment button (delta > 0) gets blue classes, the decrement button (delta < 0) gets red classes, and when the count is zero both get a neutral gray. Both buttons are created via interpolation {dynamic_button(...)} rather than uppercase tags, since they take multiple non-attribute arguments.

click helper

The click(move |cx| ...) helper wraps a simple Fn(&mut App) closure into the gpui event handler signature Fn(&ClickEvent, &mut Window, &mut App).

Tailwind classes with hover variants

class={twc!("p-2 rounded text-white", ...)} uses arbitrary color values, a hover: variant for the background color, and standard spacing/typography utilities.

Running

Native:

cargo run -p vgui-counter

Web (WASM):

# Build the WASM binary
cargo build --target wasm32-unknown-unknown -p vgui-counter --release

# Generate JS bindings
wasm-bindgen --target web --out-dir examples/counter/dist \
    --no-typescript target/wasm32-unknown-unknown/release/counter.wasm

# Serve and open in a browser
python3 scripts/serve_plain.py 8080 examples/counter