Button

One action, clearly expressed. Create a button, respond to activation, and give it a look.

xgpui_luma::controls::buttonNative GPUI control

Try it

Run the Radix code sample below. Click Save, or focus it and press Enter or Space, to update the save count.

Live GPUI rendering in your browser. Open example separately ↗

01Create a button

Create the SDK button through the Radix builder. The SDK owns its state and events; Radix supplies presentation.

Rust · SDK button via Radix
use gpui::{Context, Entity};
use xgpui_luma::controls::button::Button;
use xgpui_luma::infra::presenter::HasPresenter;
use xgpui_luma_look_radix as radix;

struct Example {
    control: Entity<Button>,
    save_requests: usize,
    focused: bool,
}

impl Example {
    fn new(cx: &mut Context<Self>) -> Self {
        let control = radix::Button::new("save")
            .solid()
            .label("Save")
            .on_click(cx, |app, cx| {
                app.save_requests += 1;
                cx.notify();
            })
            .on_focus_changed(cx, |app, focused, cx| {
                app.focused = focused;
                cx.notify();
            })
            .spawn(cx);
        Self { control, save_requests: 0, focused: false }
    }
}

The live preview compiles this exact code block. Its host initializes Luma, fonts, the Radix look, and keyboard bindings, then renders self.control.clone() with the save count and focus state.

Behavior lives in the SDK. A look supplies styling and templates. Its builder still creates the same SDK button entity.

Buttons from shared factories

Import ButtonEventExt to attach the same callbacks to an existing SDK Button entity. The methods return that entity and retain the callbacks automatically.

Rust · Existing Button entity
use xgpui_luma::controls::button::ButtonEventExt;

// A shared factory has already created this SDK Button entity.
fn bind_factory_button(control: Entity<Button>, cx: &mut Context<Example>) -> Entity<Button> {
    control.on_click(cx, |app, cx| {
        app.save_requests += 1;
        cx.notify();
    })
}

When a captured entity can own this Button, capture it weakly to avoid an ownership cycle.

Events

Builder callbacks update the owning app through its Context. The Button retains them automatically; no application subscription field is needed. cx.notify() requests an owner redraw. Use .on_event(cx, ...) for the complete event enum; ordinary cx.subscribe observers still work.

Event / payloadCallbackApplication use
Click.on_click(cx, ...)Execute the application command.
FocusChanged { focused: bool }.on_focus_changed(cx, ...)Focus state changed.
EnabledChanged { enabled: bool }.on_enabled_changed(cx, ...)Enabled state changed.
HoverChanged { hovered: bool }.on_hover_changed(cx, ...)Pointer hover state changed.

Builder callbacks append and run in registration order. They hold the owner weakly: after it is released they do nothing. Captures are released when the Button or unspawned builder is dropped. Separate entity bindings and subscribers follow GPUI ordering. Callback-triggered state changes use the same event queue; set_enabled emits only when state changes.

ButtonEvent · Event definitions. Match the events you need; allow for other variants.

03Disable an unavailable action

Update the entity when application state changes. Disabling the control prevents activation; changing its appearance alone does not.

Rust · After creation
self.control.update(cx, |button, cx| {
    button.set_enabled(false, cx);
});

Pass true to enable it again. To start disabled, add .enabled(false) to the builder before .spawn(cx).

Interaction notes

  • The button participates in focus navigation by default.
  • Keyboard activation uses the SDK’s command key profile. Keep Luma’s initialization and key bindings in place.
  • Use a unique, stable control ID within the containing view.
  • These are focused integration snippets, not a complete runnable application.

Explore the implementation: SDK button · Shadcn builder · Radix builder