guise-ui 1.2.1

A component library for gpui, Zed's GPU-accelerated UI framework: a themed palette, sizing tokens, 130+ composable components, a reactive state layer, and an in-app Safari-style inspector.
Documentation
//! The inspector, docked beside an app to inspect:
//! `cargo run -p guise-ui --example devtools`
//!
//! The left half is an ordinary `guise` UI. The right half is [`DevTools`],
//! reading that UI's live component tree and the records this example reports
//! as you click around.
//!
//! Pass a tab name to open on it: `--example devtools -- network`.

use std::time::Duration;

use gpui::prelude::*;
use gpui::{
    div, px, size, App, Bounds, Context, Entity, IntoElement, SharedString, Window, WindowBounds,
    WindowOptions,
};
use guise::prelude::*;
use guise::theme::{ColorName, Size, Theme};

struct Demo {
    devtools: Entity<DevTools>,
    checked: bool,
    plan: Signal<usize>,
    requests: u32,
    name: Entity<TextInput>,
}

impl Demo {
    fn new(cx: &mut Context<Self>) -> Self {
        let devtools = cx.new(|cx| DevTools::new(cx).tab(starting_tab()));
        let name = cx.new(|cx| {
            TextInput::new(cx)
                .label("Project")
                .placeholder("guise")
                .size(Size::Sm)
        });

        // The console prompt has nothing to evaluate against in a compiled
        // binary, so the host answers it — which is the point of the event.
        cx.subscribe(
            &devtools,
            |_this: &mut Demo, _devtools, event: &DevToolsEvent, cx| {
                // The one thing the inspector cannot do alone: open a file in
                // whatever the host calls an editor.
                if let DevToolsEvent::RevealSource(source) = event {
                    guise::devtools::log(
                        cx,
                        LogLevel::Info,
                        format!("Host asked to open {}", source.short()),
                    );
                }
            },
        )
        .detach();

        // Publish a storage domain up front so the Storage panel opens on
        // something rather than on an empty state.
        guise::devtools::storage_set(
            cx,
            StorageDomain::new("prefs", "example.preferences")
                .kind(StorageKind::Local)
                .entry(StorageEntry::new("theme", "dark"))
                .entry(StorageEntry::new("plan", "0"))
                .entry(StorageEntry::new("window", "1280×820")),
        );
        guise::devtools::log(
            cx,
            LogLevel::Info,
            "Inspector attached. Click around on the left; the tree updates every frame.",
        );
        guise::devtools::log_record(
            cx,
            LogRecord::new(LogLevel::Warning, "Theme has no explicit `info` override")
                .detail("scheme", "Dark")
                .detail("falling back to", "Cyan/8"),
        );

        // A little seeded history so every panel opens on something. A real app
        // would have reported these as the work actually happened.
        let seeded: [(&str, &str, ResourceKind, u16, u64, u64, u64); 5] = [
            (
                "GET",
                "https://api.example.com/v1/session",
                ResourceKind::Document,
                200,
                1_204,
                3_180,
                46,
            ),
            (
                "GET",
                "https://cdn.example.com/assets/app.css",
                ResourceKind::Stylesheet,
                200,
                8_940,
                41_220,
                88,
            ),
            (
                "GET",
                "https://cdn.example.com/assets/Inter.woff2",
                ResourceKind::Font,
                200,
                31_002,
                31_002,
                132,
            ),
            (
                "POST",
                "https://api.example.com/v1/events",
                ResourceKind::Fetch,
                202,
                612,
                612,
                24,
            ),
            (
                "GET",
                "https://api.example.com/v1/items?page=1",
                ResourceKind::Fetch,
                503,
                344,
                344,
                214,
            ),
        ];
        for (index, (method, url, kind, status, transfer, resource, ms)) in
            seeded.into_iter().enumerate()
        {
            let mut record = NetworkRecord::new(method, url)
                .kind(kind)
                .status(
                    status,
                    if status < 400 {
                        "OK"
                    } else {
                        "Service Unavailable"
                    },
                )
                .sizes(transfer, resource)
                .timings(Timings {
                    stalled: Duration::from_millis(1 + index as u64),
                    dns: Duration::from_millis(4),
                    connect: Duration::from_millis(9),
                    tls: Duration::from_millis(16),
                    request: Duration::from_millis(2),
                    response: Duration::from_millis(ms),
                })
                .request_header("Accept", "*/*")
                .request_header("Cookie", "session=8f2c1a; theme=dark")
                .response_header("Content-Type", "application/json")
                .response_header("Set-Cookie", "session=8f2c1a; Path=/; HttpOnly")
                .response_body("{\n  \"items\": [],\n  \"page\": 1\n}");
            record.protocol = SharedString::new_static("h2");
            record.priority = SharedString::new_static("High");
            record.remote_address = SharedString::new_static("93.184.216.34:443");
            record.start = Duration::from_millis(index as u64 * 60);
            record.state = if status >= 400 {
                RequestState::Failed
            } else {
                RequestState::Finished
            };
            guise::devtools::network_begin(cx, record);
        }

        for (label, kind, ms) in [
            ("Theme::init", TimelineKind::Script, 3u64),
            ("layout pass", TimelineKind::Layout, 12),
            ("paint", TimelineKind::Paint, 7),
            ("GET /v1/session", TimelineKind::Network, 46),
            ("reindex()", TimelineKind::Script, 28),
        ] {
            guise::devtools::timeline_event(
                cx,
                TimelineEvent::new(kind, label, Duration::from_millis(ms)),
            );
        }

        let plan = use_state(cx, 0usize);

        Demo {
            devtools,
            checked: true,
            plan,
            requests: 0,
            name,
        }
    }

    /// Report a request the way a host with a real HTTP client would: open the
    /// record when it starts, settle it by id when it finishes.
    fn fetch(&mut self, cx: &mut Context<Self>) {
        self.requests += 1;
        let n = self.requests;

        let id = guise::devtools::network_begin(
            cx,
            NetworkRecord::new("GET", format!("https://api.example.com/v1/items?page={n}"))
                .kind(ResourceKind::Fetch)
                .request_header("Accept", "application/json")
                .request_header("Cookie", "session=8f2c1a; theme=dark"),
        );

        let Some(id) = id else { return };
        let failed = n.is_multiple_of(4);
        guise::devtools::network_update(cx, id, move |record| {
            record.timings = Timings {
                stalled: Duration::from_millis(2),
                dns: Duration::from_millis(4),
                connect: Duration::from_millis(11),
                tls: Duration::from_millis(18),
                request: Duration::from_millis(3),
                response: Duration::from_millis(21 + u64::from(n % 6) * 14),
            };
            record.transfer_size = 4_200 + u64::from(n) * 137;
            record.resource_size = 11_800 + u64::from(n) * 402;
            record.protocol = SharedString::new_static("h2");
            record.remote_address = SharedString::new_static("93.184.216.34:443");
            record.priority = SharedString::new_static("High");
            record
                .response_headers
                .push(("Content-Type".into(), "application/json".into()));
            record.response_headers.push((
                "Set-Cookie".into(),
                "session=8f2c1a; Path=/; HttpOnly".into(),
            ));
            record.response_body = Some("{\n  \"items\": [],\n  \"page\": 1\n}".into());
            if failed {
                record.state = RequestState::Failed;
                record.status = Some(503);
                record.status_text = SharedString::new_static("Service Unavailable");
                record.error = Some(SharedString::new_static("upstream timed out"));
            } else {
                record.state = RequestState::Finished;
                record.status = Some(200);
                record.status_text = SharedString::new_static("OK");
            }
        });

        if failed {
            guise::devtools::log(
                cx,
                LogLevel::Error,
                format!("Request {n} failed: upstream timed out"),
            );
        }
    }
}

impl Render for Demo {
    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
        let t = cx.global::<Theme>();
        let body = t.body().hsla();
        let text = t.text().hsla();
        let border = t.border().hsla();
        let font = t.font_family.clone();

        let app = Stack::new()
            .gap(Size::Lg)
            .child(Title::new("A small app").order(2))
            .child(
                Text::new(
                    "Everything here reports itself to the inspector on the right. \
                     Open Elements and select a node to read its real style.",
                )
                .size(Size::Sm)
                .dimmed(),
            )
            .child(self.name.clone())
            .child(
                Group::new()
                    .gap(Size::Sm)
                    .child(
                        Button::new("fetch", "Fetch items")
                            .variant(Variant::Filled)
                            .on_click(cx.listener(|this: &mut Demo, _, _, cx| {
                                this.fetch(cx);
                                cx.notify();
                            })),
                    )
                    .child(
                        Button::new("work", "Slow work")
                            .variant(Variant::Default)
                            .on_click(cx.listener(|_this, _, _, cx| {
                                guise::devtools::measure(cx, "reindex()", || {
                                    std::thread::sleep(Duration::from_millis(28));
                                });
                                cx.notify();
                            })),
                    )
                    .child(
                        Button::new("warn", "Warn")
                            .variant(Variant::Light)
                            .color(ColorName::Yellow)
                            .on_click(cx.listener(|_this, _, _, cx| {
                                guise::devtools::log_record(
                                    cx,
                                    LogRecord::new(
                                        LogLevel::Warning,
                                        "Layout pass exceeded the frame budget",
                                    )
                                    .detail("budget", "16.7ms")
                                    .detail("actual", "22.4ms"),
                                );
                                cx.notify();
                            })),
                    ),
            )
            .child(
                Card::new().child(
                    Stack::new()
                        .gap(Size::Sm)
                        .child(Title::new("Settings").order(4))
                        .child(
                            Checkbox::new("notify")
                                .label("Email notifications")
                                .checked(self.checked)
                                .on_change(cx.listener(|this: &mut Demo, _, _, cx| {
                                    this.checked = !this.checked;
                                    guise::devtools::log(
                                        cx,
                                        LogLevel::Log,
                                        format!("notifications = {}", this.checked),
                                    );
                                    cx.notify();
                                })),
                        )
                        .child(
                            RadioGroup::new()
                                .label("Plan")
                                .options(["Free", "Pro", "Team"])
                                .value(self.plan.get(cx))
                                .bind(self.plan.binding()),
                        )
                        .child(
                            Group::new()
                                .gap(Size::Xs)
                                .child(Badge::new("stable").color(ColorName::Green))
                                .child(Badge::new("v0.13").variant(Variant::Light)),
                        ),
                ),
            )
            .child(
                Alert::new("Nothing here opens a socket — the host reports, guise displays.")
                    .variant(Variant::Light),
            );

        div()
            .size_full()
            .flex()
            .bg(body)
            .text_color(text)
            .font_family(font)
            .child(
                div()
                    .flex_1()
                    .min_w(px(0.0))
                    .h_full()
                    .p(px(28.0))
                    .overflow_hidden()
                    .child(app),
            )
            .child(div().w(px(1.0)).h_full().bg(border))
            .child(
                div()
                    .flex_none()
                    .w(px(620.0))
                    .h_full()
                    .child(self.devtools.clone()),
            )
    }
}

/// The tab named on the command line, so a screenshot or a demo can open
/// straight onto the panel it is about.
fn starting_tab() -> DevToolsTab {
    let arg = std::env::args().nth(1).unwrap_or_default().to_lowercase();
    DevToolsTab::ALL
        .into_iter()
        .find(|tab| tab.label().to_lowercase() == arg)
        .unwrap_or(DevToolsTab::Elements)
}

fn main() {
    gpui::Application::new().run(|cx: &mut App| {
        Theme::dark().init(cx);
        DevToolsState::new().init(cx);

        let bounds = Bounds::centered(None, size(px(1360.0), px(840.0)), cx);
        cx.open_window(
            WindowOptions {
                window_bounds: Some(WindowBounds::Windowed(bounds)),
                ..Default::default()
            },
            |_window, cx| cx.new(Demo::new),
        )
        .expect("open window");
        cx.activate(true);
    });
}