Architecture
Workspace
guise/
├── Cargo.toml # workspace manifest (plain crates.io gpui)
├── docs/ # human docs (this directory)
├── site/ # docs-website generator (Bun; one page per docs/*.md, via render/nav.ts)
├── scripts/ # the icon-font generator
└── crates/
├── guise/ # the library — published as `guise-ui`, lib name `guise`
└── gallery/ # a live showcase (cargo run -p gallery)
Only crates/guise is published; the gallery is publish = false and is in the
workspace so CI builds it against every change.
Tailor, the visual interface builder that draws with these
components, used to live here too. It is its own project now, and depends on
guise-ui from crates.io like any other consumer — which is what keeps the
library honest about what it actually exposes.
The version lives once, in [workspace.package]. Cargo.lock is committed and
CI builds --locked, so bumping the version means regenerating the lockfile in
the same commit.
The gpui dependency
Everything builds against crates.io gpui = "0.2.2" — no git pins and
no [patch.crates-io] section, so guise-ui installs as a plain registry
dependency and publishes cleanly. The few style/scroll APIs the crates.io
snapshot lacks are shimmed in style.rs (FlexExt) via gpui's raw
StyleRefinement. (thirdparty/block/ is a leftover vendored crate; no
manifest references it.)
The library package is guise-ui — the guise name was taken on
crates.io — with [lib] name = "guise". Cargo commands address the package as
-p guise-ui, while code imports stay use guise::....
Library module map (crates/guise/src)
| Module | Contents |
|---|---|
theme/ |
Theme, Color, Palette, Scale, Size, ColorScheme, JSON theme files (Theme::from_json), prebuilt presets (Theme::preset), and the ThemeManager registry + ThemeChoice |
style.rs |
the Variant system and surface() resolver |
layout/ |
themed Stack, Group, Center, SimpleGrid, AppShell, Container, Space, plus Breakpoint/Responsive |
flex/ |
Flutter-style Row, Column, Container, Expanded, … |
input/ |
TextInput, TextArea, NumberInput, PasswordInput, PinInput, Select, Combobox, Checkbox, Switch, Radio, RadioGroup, CheckboxGroup, SegmentedControl, Slider, RangeSlider, Rating, ColorInput, TagsInput, Field, Autocomplete, Calendar, DatePicker, TimePicker, FileInput, Dropzone, Transfer, the Date/Time models, the TextEdit model, the shared single-line key map (keys.rs), line.rs — the shared field element (glyph-accurate caret, hit-testing, scrolling, IME) every single-line input is built on, and area.rs — its wrapped multi-line counterpart that draws TextArea |
editor/ |
Editor entity, the EditorModel buffer, Language highlighters (Rust, SQL, JSON, TOML, Python, JS/TS, Go, C, Markdown), Diagnostic/Severity |
markdown/ |
MarkdownEditor entity (live-preview markdown) and the read-only Markdown renderer, over pure block / inline / layout passes |
ai/ |
AIChatView, AIMessage, AIComposer, AIStreamingText, AIThinking, AIReasoning, AIToolCall, AICitation, AISources, AIModelPicker, AITokenMeter, AICost, AISettings — transport-agnostic; the host owns the request |
data/ |
Avatar, AvatarGroup, List, VirtualList, Table, TableView, DataView, TreeView, TabBar, Timeline, Tabs, Accordion |
chart/ |
Sparkline, LineChart, AreaChart, BarChart, ScatterChart, PieChart — canvas-painted builders with optional axes/legends/hover |
feedback/ |
Alert, Loader, Progress, RingProgress, Notification, ToastStack |
overlay/ |
Modal, ConfirmModal, Drawer, Menu, MenuBar, ContextMenu, Popover, HoverCard, LoadingOverlay, Spotlight, Tooltip, Tour, OverlayHost (window-level modal stack + toasts) |
nav/ |
Breadcrumbs, NavLink, NavigationMenu, Stepper, Pagination, StatusBar |
panegroup/ |
The Zed-style workspace: a pure PaneTree of splits whose leaves are tabbed Panes, plus compute_layout, nav and snapshot encode/decode — with the gpui entity layered on top. The host owns the items; the component owns layout, tab bars, dividers and drag/drop |
icon/ |
Icon, IconName, Glyph — all 1,991 Lucide glyphs, drawn from an icon font embedded in the crate. lucide.rs is generated by bun scripts/icons.ts; never hand-edit it |
reactive/ |
Signal, Binding, Context/Provider, hooks (use_state/watch/use_memo/use_effect), Form (per-field signals) + FormState |
settings/ |
SettingsView, SettingsSection, SettingsRow — settings-screen chrome only; the schema and the write path stay in the app |
devtools/ |
DevTools — a Safari-shaped inspector for the app itself: an Elements tree recorded by Probed::probe (with real StyleRefinement snapshots), plus Logs / Network / Storage / Timelines fed by the host, Sources read off disk, and an Audit computed from the tree |
update/ |
self-update: Updater/UpdateConfig (release check + in-place install, gpui-free), SHA-256 verification of the download (checksum.rs), and the UpdatePrompt/UpdateNotice entities that drive it |
macros.rs |
the row!/col!/… container macros, plus style! and color! |
anim/ |
The animation system: Easing/Curve, Spring, keyframed Motion, Sequence, Stagger, the Animated/.animate(..) one-shots, the Animator playhead, Presence (exit animations), and the motion! / sequence! macros |
dnd/ |
Draggable, DropTarget, SortableList — typed drag payloads |
transition.rs |
Transition / Collapse (true height) animations |
video/ |
VideoView, VideoFrame (RGBA/BGRA/I420), VideoFeed (one-slot mailbox for decoder threads), VideoFit — transport-agnostic; the host decodes |
webview.rs |
WebView — native embedded web view via wry (default-on webview feature) |
| root files | Button, Badge, Card, Paper, Panel, SplitPanel, Image, Mark, Blockquote, Spoiler, Text, Title, Anchor, Code, Kbd, Icon, ActionIcon, ThemeIcon, CloseButton, CopyButton, Chip, Indicator, Skeleton, Divider, ScrollArea, Scrollbar, Carousel, ThemePicker |
Conventions
- One component per file, lowercase names, no
-/_/spaces; group with directories (input/select.rs), not concatenated names. - Read everything from the theme via
guise::theme::theme(cx)— never hardcode a color or size. This is what makes light/dark switching free. - Builder methods take
mut selfand returnSelf(chainable). - Container components implement
ParentElement(justextend);.child/.childrencome for free. - Resolve all theme values into locals before any
cx.listener(...)or content-builder call —theme(cx)borrowscximmutably and those need it mutably, so a latetheme(cx)read overlaps the borrow and won't compile. - Closures stored on elements (
.hover,.on_click) must be'static— capture resolvedHsla/f32values, not the&Themeborrow.
Adding a component
- Create a file under the right module (or the crate root for a loose one).
- Define a
#[derive(IntoElement)]builder +impl RenderOnce, or aRender+EventEmitterentity if it owns state. Resolve visuals fromtheme(cx). - End
renderwith.probe("Name")— or.probe_anywhen the root is already a composed component rather than a styled element — plus an.attr(..)per meaningfulCopyprop. Without it the component is invisible in the DevTools Elements tree. - Re-export it from the module's
mod.rs, then fromlib.rs, then add it to theprelude. - Add a showcase to
crates/gallery/. - Write the component's docs section on the right
docs/page — and if that page is new, register it insite/render/nav.tsso the website picks it up. - For pure logic (parsing, range math, an editing model), add
#[cfg(test)]tests next to the code. For wiring that needs a live app — signals, bindings, entity events, the theme global — use the gpui test harness insrc/apptests.rs. - If Tailor should be able to place it, catalogue it there. Its coverage test reads a record of what this library ships and fails on a component that is neither catalogued nor excluded with a reason, so the gap shows up as a failing build on its side rather than as silent drift.
See the component model for the two patterns in detail.
Commands
cargo run -p gallery # launch the showcase
cargo check -p guise-ui # fast type-check (package is guise-ui; lib name is guise)
cargo test -p guise-ui # the library's tests: inline #[cfg(test)] + src/apptests.rs
cargo build --workspace --locked # what CI builds
cd site && bun run build.ts # docs/ -> site/dist