Components & slots
Tailor's Library is guise's component set, one entry per component, grouped the way the docs group them. There are 124 of them.
The catalog#
| Category | Count | Some of what is in it |
|---|---|---|
| Layout | 25 | Frame, Absolute frame, Stack, Group, Center, Grid, Container, Card, Paper, Panel, Scroll area, App shell, Split panel, Expanded, Settings screen, Settings section, Settings row, About, Window controls |
| Typography | 9 | Text, Title, Anchor, Code, Kbd, Mark, Blockquote, Markdown, Spoiler |
| Controls | 13 | Button, Action icon, Close button, Copy button, Badge, Chip, Icon, Theme icon, Theme picker, Indicator, Rating, WebView |
| Inputs | 27 | Text input, Text area, Number, Password, PIN, Select, Combobox, Autocomplete, Checkbox, Switch, Radio, Segmented, Slider, Range slider, Colour, Tags, Date, Time, Calendar, File, Dropzone, Transfer, Field, Editor, Markdown editor |
| Data | 11 | Avatar, Avatar group, List, Table, Tabs, Accordion, Tab bar, Timeline, Tree view, Carousel, Virtual list |
| Feedback | 11 | Alert, Notification, Loader, Progress, Ring progress, Skeleton, Modal, Drawer, Tooltip, Loading overlay, Confirm |
| Navigation | 8 | Breadcrumbs, Nav link, Stepper, Pagination, Status bar, Navigation menu, Menu, Context menu |
| Charts | 6 | Sparkline, Line, Area, Bar, Pie, Scatter |
| Media | 1 | Image |
| AI | 13 | Chat view, Message, Composer, Streaming text, Thinking, Reasoning, Tool call, Token meter, Cost, Citation, Sources, Model picker, AI settings |
What is deliberately not in it#
The catalog is checked against guise's own source by a test
(catalog::coverage in tailor-model), so a component added to the library is
either catalogued or listed as an exclusion with a reason. Adding one to guise
and forgetting Tailor now fails cargo test rather than going unnoticed for a
release.
The standing exclusions, and why:
| What | Why |
|---|---|
The flex/ primitives (Align, Padding, Spacer, SizedBox, Wrap, …) |
pixel-based twins of layout/, which the catalog offers instead |
Animated, Presence, Transition, Collapse |
motion is a property of a node, not a component you drop |
Popover, HoverCard |
trigger and content are 'static element closures; Tooltip covers what fits in a prop |
Spotlight, Tour |
steps point at element ids that only exist in the running app |
MenuBar |
app-level menus dispatch actions rather than lay anything out |
PaneGroup |
the host owns the items; the component owns only the layout over them |
Draggable, DropTarget, SortableList |
generic over a payload or a collection the host owns |
GpuView |
draws a GpuScene the host assembles in code |
OverlayHost, ToastStack, DevTools |
one per window, installed by the host |
UpdatePrompt, UpdateNotice |
driven by a live Updater; there is nothing to preview |
ResizeHandles |
window chrome, meaningful only against a real borderless window |
The Library searches across names and blurbs, and the category pills filter it. Drag an entry onto the canvas, or click it to drop it into the selection — click is faster once you know where a thing is going, because it lands inside whatever is selected rather than wherever you let go.
Right-clicking an entry offers Insert into the selection and Insert at the top level.
Two kinds of component#
The catalog marks which is which, and it decides the shape of the generated file:
- Stateless builders. A
Buttonis a value: you hand it a label, a variant and a click handler, and it draws. Most of the catalog is this. - Stateful entities. A
TextInputowns a buffer, a caret and an IME. It is a gpui entity, created withcx.new, and it emits events rather than taking handlers.
You do not pick between them; the component is one or the other. What it changes
for you is that a document holding any entity generates as a Render entity
with a constructor, rather than a RenderOnce builder. See
what gets generated.
The entities are every text-ish input, every picker, the overlays that own open-state, and the big surfaces — Editor, Markdown editor, Tabs, Accordion, Pagination, Carousel, Table view, Tree view, Tab bar, Split panel.
Slots#
Children live in slots, not in one list. Most components have only the default
children slot, but the ones you build a screen out of have named regions:
| Component | Slots |
|---|---|
AppShell |
children, header, navbar, aside, footer |
Panel |
children, icon, action, footer |
Button |
left section, right section |
SplitPanel |
first, second |
StatusBar |
left, center, right |
Field |
children |
Tabs / Accordion |
one per tab or section, from the labels you typed |
Drop into a slot from the canvas or from the Outline. An empty slot draws a dashed placeholder so there is something to aim at, and the Outline shows slots as their own rows — which is the easier target when a region is a 64px strip.
Some slots are single: a shell has one header, not a list of them. Dropping a second node into a single slot replaces what was there.
The containers Tailor draws itself#
Tabs, Accordion, SplitPanel, AppShell, Carousel, SettingsView and
VirtualList take their regions as 'static closures. A closure is opaque — a
designer cannot drop a node into one — so Tailor draws those from the theme
instead of instantiating them.
That is not a downgrade, it is the point: drawing them is what lets you click a
tab to reveal the slot behind it and drop into it. A real Tabs would show you
one panel and hide the rest behind a closure you cannot open.
Generated code uses the real component. The drawing is a canvas affordance
and never leaves the canvas. SettingsView is the one whose generated shape
differs most from its drawn one: the canvas shows a sidebar and one page's
slot, and the export declares .page(id, title) per page and fills them from a
single .content(|page, ..| match page { .. }) closure, because that is the
API the real component has.
One consequence, and the Problems panel will tell you about it: a stateful
component inside one of these is a problem. Their regions are 'static, so
a TextInput in an AppShell header cannot be a field of the screen — it would
have to be created inside the closure, on every frame. Extract that part into
its own component, which generates its own entity, and place that instead.
Event handlers inside those regions are fine — Tailor routes them through a weak handle rather than a borrowed context, which is what a hand-written host does.
Components you build#
A document is either a screen (generates a Render entity) or a
component (generates a RenderOnce builder). File → New Component, or
the Add a component item on a tab's right-click menu.
Your components appear at the top of the Library under This project. Place
one and you get a @Name node: it renders inline on the canvas, so the screen
looks like the screen, and it generates as Name::new().
A placed component is a reference, not a copy. Change the component and every placement changes.
Right-clicking one in the Library offers Insert, Edit the component (opens its document), Rename…, Duplicate and Delete.
Extract to a component#
Select some nodes on the canvas, right-click, Extract to a component…. Tailor lifts them into a new component document and leaves a reference behind in their place.
The selection has to share one parent, because that is where the reference goes back.
Cycles#
A component that would contain itself is refused when you drop it, and again
when the file is loaded — a hand-edited .tailor with a loop in it comes back
repaired rather than hanging the app that opened it.
That check is on names, because a @Name reference carries a name. It is also
why duplicating a component gives the copy a new name rather than a new id
alone: two documents called the same thing would generate the same Rust type,
which the Problems panel reports as an error.
Props#
The inspector's Attributes tab is generated from the catalog, one row per prop, with the control the prop's type asks for: a text field, a number, a size or variant picker, a colour swatch, an icon picker with search, a list editor.
Defaults are shown greyed. Tailor does not restate a default in the generated code — the file is what you would have written, not a dump of every prop — so a prop you never touched costs nothing in the output.
Any text, number or boolean prop can be bound to a state variable instead of holding a literal. See state, bindings and actions.
Adding a component to the catalog#
If you are working on Tailor itself: the catalog is the single source of truth, and adding a component is two edits that have to happen together.
- One
comp!entry incrates/guise/src/catalog/: the kind, the Rust type, the category, the blurb, the constructor shape, the props with their types and defaults, the slots, the events. - One arm in
crates/guiserender/src/nodes.rs, which turns a node of that kind into a live guise component for the canvas.
PropSpec::emit decides what the generator prints for each prop — a method
call, a bare flag, or something custom. Editing the catalog without the renderer
(or the other way round) is how a canvas and an export drift apart, which is the
one failure this design exists to prevent.
Two tests hold that together, and both fail loudly rather than quietly:
coverage(tailor-guise) readslibraries/guise.surface— the checked-in record of what the pinned crate ships — and asserts everyRenderOncebuilder andRenderentity is either in the catalog or inEXCLUDEDwith a reason. Adding a component to guise and forgetting Tailor is a failing test, not a missing library entry noticed a release later.every_catalog_kind_generates_something(tailor-guise) generates one document per catalog kind and asserts each names its own type. Acomp!entry with no generator support fails here.
The catalog is a provider's, not Tailor's — guise is one implementation of three traits rather than a built-in. See component libraries for what it takes to add another.
Constructor shapes live in Ctor: Unit, Id, IdAnd(prop), Arg(prop),
Args(&[prop, ..]) for the ones that take two facts (AIMessage::new(role, body)), the three Entity variants, and Special for the handful the emitter
writes by hand.