widgets.md raw

Widgets

The builtin widget vocabulary. Every widget accepts the common layout properties (:width :height :padding :gap :direction :align-items :justify-content :align-self :grow :shrink :basis), :class, :tooltip, :enabled, :focused, and the universal events (:on-click :on-counted-click :on-key :on-focus-change :on-file-drop and the :on-pointer-… family); this page lists what each adds.

Properties marked bindable accept expressions and update reactively; the rest are static.

:font-family is a CSS font-family string wherever it appears — a comma-separated preference list in which sans-serif, monospace, and serif are generic names rather than faces somebody installed. So :font-family "monospace" asks for whatever this machine calls monospace, and :font-family "Iosevka, monospace" prefers a specific face and says what to do without it. It means the same thing on every widget that takes it.

Three ideas the whole set is built on

Reading any one widget is easier once these are clear, because they are why the vocabulary is smaller than you might expect.

Values are controlled. A checkbox does not own whether it is checked, a dialog does not own whether it is open, and a slider does not own its position. Your state is the truth: the widget reports what the user did, your handler decides, and the binding is what changes the widget. A programmatic set is always silent — it never re-raises the event — which is what stops a binding from looping.

Nothing holds your data. There is no menu model, no list model, no tab controller. Rows are a for over your own records, and the wire carries identity:

(menu :label "Recent"
  (for doc recents :key doc.id
    (menu-item :label doc.name :on-select (open-recent doc.id))))

The two combine into the thing that surprises people: there is no radio-group or tab-group widget. “One of these” is a comparison in a binding, so mutual exclusion is a fact about your data rather than a container’s job:

(for choice choices :key choice.id
  (radio :checked (= choice.id chosen) :on-select (choose choice.id)))

A widget exists to be announced. Several controls here could have been a themed checkbox or a styled container — a radio is a checkbox with a round box, and a tab strip is a row. They are separate widgets because a theme rule cannot tell assistive technology what a control is, and “announced as a checkbox” is wrong for something operated as a radio. Where you see a near-duplicate, that is usually why.

The set

ForWidgets
Structurecontainer, separator
Texttext (rich text, links), text-input, combo-box
Actingbutton
Choosingcheckbox, radio, switch, tab / tab-list, list / list-item
Valuesslider, stepper, progress
Picturesimage
Scrollingscroll-area
Popupsmenu-bar / menu / menu-item / menu-separator, context-menu, dropdown, tooltip, dialog
Disclosureexpander

Two more exist that no file writes: menu-panel (the popup a menu opens) and tooltip’s panel. They are nameable in a theme and rejected in widget position, because a parent builds them.

container

The structural workhorse: a flexbox box that may paint a background.

PropertyTypeNotes
:backgroundcolorbindable
:corner-radiusnumber (px)bindable

A container with an :on-click is a button; :hover/:active theme rules (or hovered/pressed state driven by the pointer events) give it interaction feedback.

text

A shaped, wrapped text label (parley).

PropertyTypeNotes
:textstring or (rich …)bindable; interpolation is the usual use
:font-sizenumber (px)bindable
:font-familystringbindable
:colorcolorbindable
:on-linkhandler (String payload)fires when a (link …) span is clicked

See rich text for (rich …) and its (link …) form.

A text is selectable: drag to highlight a range, double-click for a word, triple-click for a line, and Ctrl+C copies it (Ctrl+A selects the whole paragraph). On Wayland a selection also populates the primary selection, so middle-click pastes it elsewhere. A label takes focus when you click into it — so a rendered note can be copied from — but it is not a Tab stop, so it never interrupts a Tab walk between the controls around it, and losing focus drops its selection (only one paragraph highlights at a time). The highlight color is the theme’s selection-color. A plain click (with no drag) still follows a (link …) under it; a drag selects instead.

Rich text

:text takes a (rich …) form as well as a string:

(text :text (rich "Hello " (bold "World") (color "#ff00ff" "!")))

Nesting is the structure, so there is no markup inside a string, nothing to escape, and no tag to leave unclosed — a mismatched paren is a paren error like any other. Styles compose by nesting, and neither form needs to know about the other:

(rich "Styles " (bold "compose by " (italic "nesting")) ", and "
      (underline "underline") " or " (strike "strike") " work too.")
(rich "A " (size 22 (color "#2e8b57" "big green")) " run.")
FormEffect
(bold …)weight 700
(italic …)italic face
(underline …), (strike …)decorations
(color "#rrggbb" …)brush
(size 20 …)font size
(link "target" …)a link to target (see below)

The tree is flattened at compile time into a plain string plus the ranges that differ from it, so nothing walks a tree while painting. Bold and italic resolve through real faces: nothing is synthesized, so a font collection without a bold face renders bold as regular rather than smearing the glyphs.

The same thing is available in Rust as Text::span(range, TextSpan).

Links

(link "target" …) makes its content a link. Clicking it fires the paragraph’s :on-link with the target string:

(text :text (rich "See the " (link "home" "home page") " for more, or "
                  (link "faq" (bold "the FAQ")) ".")
      :on-link (navigate payload))

The target is opaque: guiduck never resolves it, so it can be a URL, a page name, a record id — whatever the handler means by it. (link …) nests like every other form, in either order, so (link "faq" (bold …)) and (bold (link "faq" …)) both work.

A link is a span of a paragraph, not a widget inside one, and that is the point. Wrapping across a link needs the link and the prose around it to be one parley layout: the paragraph above breaks between home and page if the column is narrow enough, exactly as it would between any two words. A link that were its own widget could only break the line at its own edges. So hit testing reaches inside the paragraph, down to the span — clicking either line’s half of a wrapped link names the same link, and the pointer is the hand over both.

What a link looks like is the theme’s (link-color, link-underline-width on text), not the widget’s — see theming. Whatever else the span sets wins over it, so (link "x" (color "#f00" …)) is a red link.

Accessibility: each link span contributes a Link node carrying its text, its target, and its bounds, and a screen reader clicking it fires :on-link through the same wire the pointer uses.

image

A graphic, scaled into the widget’s bounds.

PropertyTypeNotes
:sourcegraphicstatic; what to draw

A graphic is either vector geometry or a file’s pixels, interchangeably:

(image :source (asset "icons/logo.png"))
(image :source (path (move 0.5 0) (line 1 1) (line 0 1) (close))
       :width 24 :height 24)

Pixels carry their own color. A (path …) source is bare geometry, so it is filled with the theme’s :color token, and its coordinates are in its own space — conventionally the unit square — scaled into whatever box the widget gets.

Sizing. Whatever box it ends up with, the graphic scales to fill it: the target defines the size. What differs is only what the widget can tell layout when nothing else constrains it. A raster source reports its natural pixels (constrain one axis and the other follows the aspect ratio). A path reports nothing — scale-free geometry has no size, and inventing one would be a lie — so it must be given a box by :width / :height or by flex. An unsized path source draws nothing.

An asset reference is a path resolved against the crate’s asset search path — the assets/ directory under the crate root, or whatever Cargo.toml says:

[package.metadata.guiduck]
asset-path = ["assets", "vendor/icons"]

Directories are probed in order, first hit wins, and a path that cannot be found is a compile error listing everywhere that was looked. This mirrors the component search path exactly (see Components); it is the same mechanism under a different key.

The bytes are located and embedded at build time (include_bytes!), so editing an image rebuilds the crate that draws it, exactly as editing a .gdc does. That is why :source is static rather than bindable, and why an asset path may not interpolate, be absolute, or climb out with .. — a build must not depend on the machine it runs on. Dev mode reads the same file from disk instead, so an edited image appears on reload without a rebuild. Supported formats are PNG and JPEG.

Nothing about (asset …) is image-specific: it names a file, and the type of the property it lands in decides what the bytes mean.

Give it an :alt unless it is decoration: an image with no name is invisible to a screen reader, and leaving it off is how a decorative one says it may be skipped.

Otherwise an image is an ordinary flex item and takes the usual layout properties and pointer events.

text-input

A single editor line-or-block on parley’s editing core.

PropertyTypeNotes
:textstringbindable — see controlled inputs below
:font-sizenumber (px)bindable
:font-familystringbindable
:multilinetrue/falseaccept line breaks; default false
:autofocustrue/falseliteral; at most one per composed tree
:on-changehandler (String payload)fires once per user edit

Built-in behavior:

  • Editing: character input, Backspace/Delete (with Ctrl: by word), arrow keys with Shift to select and Ctrl to move by word, Home/End, Ctrl+A select-all.
  • Clipboard: Ctrl+C / Ctrl+X / Ctrl+V.
  • Primary selection (Wayland): any selection — drag, shift-moves, double/triple-click, Ctrl+A — populates the primary selection; middle-click pastes it at the click point.
  • Mouse: click places the caret, drag selects, double-click selects the word, triple-click the line.
  • IME: preedit is rendered inline and is not an edit; committing emits :on-change.

With :multiline true, Enter inserts a line break and pasted or committed newlines are kept; the field wraps and grows vertically, and reports itself to assistive technology as a multi-line editor. A single-line field (the default) declines Enter — so a default button still sees it — and flattens any newline that arrives by paste or IME. When a multi-line field is taller than a scroll-area it sits in, the caret is kept in view automatically as it moves (the same reveal that scrolls a Tab-focused control into view).

:on-change reports user edits only — one event per actual change, with the field’s new content. Programmatic writes through the :text binding never re-raise it, which makes the controlled input pattern safe and idiomatic:

(state current String :init "")
(handler edited String)

(text-input :text current :autofocus true :on-change edited)
fn edited(&mut self, cx: FormCx, value: &str) {
    cx.current.set(value.to_owned());
}

The field renders the state, the handler writes the state; on a hot reload the field is rebuilt with its content restored from the state.

Like every std control, a text input’s appearance is data: the default theme supplies its background, border, caret, and selection colors, and the focused field’s thicker accent border is a (text-input :focus) rule (see Theming).

Combo box

combo-box is a field you can type in that also opens a list. It is a text-input that hosts a popup — the editing, selection, clipboard and IME are all the text input’s, not a second copy — and Down opens the list.

It holds no candidates. The rows are ordinary deferred content, so they are a for over your own state:

(combo-box :text draft :on-change (filter payload)
  (for candidate matches :key candidate
    (menu-item :label candidate :on-select (choose candidate))))

Filtering is yours, and there is deliberately no completion query wire for it. A query (:get-image) exists for a value the widget discovers by itself and you never see. A combo box’s text arrives through :on-change before you would be asked for candidates, so a handler writing matches is the whole mechanism; pulling back what was just pushed would be a second path to the same value.

button

A push button with a centered label.

PropertyTypeNotes
:labelStringbindable; the button’s text
:font-sizenumberbindable
:font-familyStringbindable
:on-clickhandleractivation wire
:autofocustrue / falsestatic

Buttons are focusable. A pointer click fires :on-click; while focused, Enter and Space activate the button through the same wire (keyboard activation is converted into a synthesized click at the button’s center, the path assistive technology’s Click action also takes). A button’s appearance is data: the built-in default theme supplies its background, label color, corner radius, and focus ring across normal, hover, active, focus, and disabled states, so it looks right with no app theme, and an app theme overrides any of those tokens — including per-state with :hover / :active / :focus / :disabled rules (see Theming). The cursor becomes a pointer over a button.

checkbox

A box with a check mark and an optional label.

PropertyTypeNotes
:checkedboolbindable; controlled
:labelStringbindable
:font-sizenumberbindable
:font-familyStringbindable
:on-togglehandler (bool payload)fires on user toggle
:autofocustrue / falsestatic

:checked is a controlled value, exactly like text-input’s :text: the widget never flips it silently. A click (or Space while focused) toggles the displayed state and fires :on-toggle with the new state as a bool payload; the handler decides what to persist, and a :checked binding reflects the stored value back. Because a programmatic set is equality-gated and never re-emits, the binding cannot loop. This is the first non-String widget payload — wire it to a bool handler:

(handler set-flag bool)
(checkbox :label "wifi" :checked opt.on
          :on-toggle (set-flag opt.id payload))

Like the button, a checkbox’s appearance is data supplied by the default theme: box fill and border (with checked variants), the check mark, and colors, all overridable by an app theme. The mark is a graphic, so a theme can replace it with different geometry — (path …) / (svg-path …) — or with a file’s pixels, without touching widget code:

(rule checkbox :check-mark (svg-path "M0.24 0.5 L0.76 0.5"))
(rule checkbox :check-mark (asset "icons/check.png"))

A path mark obeys :check-color and :check-width; a raster mark carries its own color and ignores them, varying across states by swapping the asset (see Theming).

Radio buttons

radio is a checkbox’s twin with three differences, two of which a theme cannot reach: assistive technology is told it is a radio button, and choosing is one-way — clicking the already-chosen one is not a change, so it reports nothing and does not flip off. The look is the third, and the only one you could have got from a theme rule.

It holds no group. Mutual exclusion is a fact about your data, so :checked binds to a comparison and the wire carries which was chosen:

(for choice choices :key choice.id
  (radio :label choice.name
         :checked (= choice.id chosen)
         :on-select (choose choice.id)))

:on-select rather than :on-toggle, because a radio reports being chosen rather than a new boolean — what was chosen is the wire’s own argument, exactly as for a menu item.

Its tokens are the checkbox’s, so a theme dresses the two alike; the default theme differs only in :box-corner-radius and :check-mark.

switch

On or off, rather than ticked or not.

PropertyTypeNotes
:checkedboolbindable; controlled
:labelStringbindable
:font-size, :font-familybindable
:on-togglehandler (bool payload)fires on user toggle

Behaviourally it is a checkbox — a switch flips both ways — so it reports :on-toggle and the contract is the same. What it adds is the announcement, and a theme that draws a track and a knob instead of a box and a tick. Its tokens are the checkbox’s, so one theme dresses both.

Value and range

slider takes a :value from 0 to 1 and the application scales it. That is a decision, not an omission: a slider carrying its own minimum, maximum and step would repeat arithmetic the application is already doing, in units the compiler cannot check — and the fraction is what the accessibility layer wants anyway, as a percentage. Drag it, or use the arrows, Home and End.

stepper does carry a real range (:min, :max, :step), because the number it shows is the number you mean; showing “0.42” for a value of 42 would be a lie. Up/Down step, PageUp/PageDown step by ten.

Both report :on-value-change with an f64, and both are controlled: a programmatic set is silent, so a binding cannot loop.

progress is the non-interactive one — a :value from 0 to 1, clamped.

(slider :value volume :on-value-change (set-volume payload))
(stepper :value count :min 0 :max 10 :step 1 :on-value-change (set-count payload))
(progress :value done)
WidgetProperties
slider:value (f64 0–1, bindable, controlled), :on-value-change
stepper:value, :min, :max, :step (f64, bindable), :font-size, :font-family, :on-value-change
progress:value (f64 0–1, bindable)

:on-value-change carries an f64 — deliberately not :on-change, which carries a String. A slider and a text field both “change”, and one wire name meaning two payload types is how a contract stops being checkable.

Keys: a slider takes Left/Right/Up/Down (a twentieth each), Home and End; a stepper takes Up/Down (one step) and PageUp/PageDown (ten). In both, a key that would move nothing is not consumed, so an exhausted control never swallows a keystroke that could still reach Tab or a shortcut.

Grouping and navigation

separator is a rule. Its thickness is the theme’s and its length the layout’s, so the same widget is a horizontal line in a column and a vertical one in a row. (menu-separator is this idea confined to a menu.)

tab / tab-list and list-item / list are the same shape as a radio group, and for the same reason: none of them holds a selection. One of a set is expressed by binding each item’s own flag to a comparison, so the container exists to be announced — a screen reader is told these are tabs, or a list, which a container full of rows cannot say.

(tab-list :direction row :gap 4
  (for page pages :key page.id
    (tab :label page.name :selected (= page.id showing) :on-select (show page.id))))
WidgetProperties
separator
tab-list, list— (containers)
tab, list-item:selected (bindable, controlled), :label, :font-size, :font-family, :on-select
expander:open (bindable, controlled), :title, :on-toggle

A list-item belongs inside a list, checked at compile time.

expander is a titled row that shows or hides what is under it. Controlled like a dialog’s :open: it asks through :on-toggle and your binding answers. That is what makes an accordion expressible without an accordion widget — bind each :open to a comparison, exactly as for radios. Its content is not deferred, unlike a menu’s: it is part of the page and its height participates in the layout around it.

scroll-area

A clipping viewport over overflowing content, with overlay scrollbars.

PropertyTypeNotes
:axisvertical / horizontal / bothstatic; default vertical
:offset-x, :offset-ynumber (px)bindable, controlled
:on-scrollwire; event.offset-x / event.offset-yfires when the reader moves it

Where the reader is, is your state. The position is controlled like :checked and :open: you set it, the reader moving the view reports through :on-scroll, and a programmatic set is silent so the two cannot echo. That is what makes “start this page at the top” something a navigation handler can just do:

(scroll-area :grow 1 :offset-y scroll :on-scroll (scrolled event.offset-y)
  …)
fn navigate(&mut self, cx: WikiCx, to: &str) {
    // …load the page…
    cx.scroll.set(0.0);
}

Wire :on-scroll even if you only ever reset to the top: without it your copy of the position goes stale the moment the reader scrolls, and the next set(0.0) is equality-gated into doing nothing.

Behavior: mouse-wheel scrolling (the innermost scrollable under the pointer consumes it; at the end of its range the scroll bubbles to an enclosing scrollable); draggable overlay thumbs; offsets clamp to the content extent. Scrolling is paint-only — it never re-runs layout — and hit testing tracks the offset exactly, so pointer targets and pixels cannot disagree.

Children overflow along the scrolling axes rather than being fitted into the viewport — a scroll area will not stretch content across an axis it scrolls, so a document keeps its own height however small the window gets. That is the widget’s own guarantee, not something to remember: a box smaller than what it paints would still draw below the fold while ceasing to answer the pointer, since hit testing is bounded by a widget’s box and painting is not.

What is still yours: along the axis children are stacked on, flex shrinking applies as usual, so rows with a declared size want :shrink 0 to keep it — as ScrollList.gdc does.

The thumbs’ appearance is data — :thumb-color and :thumb-corner-radius in the theme.

Keyboard scrolling

A scroll-area is click-focusable but not a Tab stop — click into one and arrows, PageUp/PageDown, and Home/End scroll it, without a viewport interrupting a Tab walk between the controls inside it. A key that would move nothing is not consumed, so an exhausted scroll never swallows a keystroke.

menu-bar, menu, menu-item, menu-separator

Menus, as a shape:

(menu-bar
  (menu :label "File"
    (menu-item :label "New" :accel "Ctrl+N" :on-select new-file)
    (menu-separator)
    (menu :label "Recent"
      (for doc recents :key doc.id
        (menu-item :label doc.name :on-select (open-recent doc.id))))
    (menu-item :label "Quit" :accel "Ctrl+Q" :on-select quit)))
WidgetPropertyNotes
menu:labela bar title, or a submenu row
menu-item:labelthe row’s text
menu-item:accele.g. "Ctrl+Shift+P"; static
menu-item:on-selecthandler; no payload

The shape is checked at compile time: a menu-item or menu-separator belongs to a menu, and a menu to a menu-bar or another menu. That is not tidiness — it is how a menu knows whether it is a title (popup below) or a submenu row (popup beside), without inspecting anything at runtime.

There is no menu model. Rows are widgets, and data-driven menus are for over your own records, with the wire carrying identity — :on-select (open-recent doc.id). Records stay app-side.

Menu content is built when it opens

Everything under a menu is deferred: it is built when the popup opens and disposed when it closes. A closed menu’s rows do not exist — they occupy no memory, lay out nothing, shape no text, and subscribe to no signals, so a menu over a long list costs nothing until you open it, and does not make that list’s updates more expensive for anything else.

The laziness is recursive: only the opened path is built. Opening File above does not build Recent’s rows; opening Recent does.

Opening costs roughly 17µs per row (mostly text shaping), so a normal menu opens in well under a millisecond; a 200-row menu takes ~3.5ms, one frame’s work.

Context menus

(container :class "row"
  (text :text todo.label)
  (context-menu
    (menu-item :label "Rename" :on-select (rename todo.id))
    (menu-separator)
    (menu-item :label "Delete" :on-select (remove todo.id))))

A context-menu attaches to the widget that encloses it: right-pressing that widget — or anything inside it — opens its rows at the pointer. It draws nothing and takes no space.

There is no handler and no coordinate to deal with. The framework knows where the press was, and a context menu declared inside a for closes over its own loop variable, so the wire above already knows which row it belongs to. Nesting works the way you would want: the innermost context menu at or above the press wins, so a row’s own beats the list’s.

Its rows are deferred like any menu’s, so a context menu on every row of a thousand-row list costs nothing until one is opened. A right-press with no context menu above it is not the framework’s, and passes through.

Accelerators

:accel registers through the shortcut table at mount, and renders right-aligned in its row. The focused widget still sees a key first, so a text input’s Ctrl+C beats an accelerator.

An accelerator fires while its menu is closed — which is when its row does not exist — so it is wired to the same handler the row is, not to the row. That is also why :accel cannot live inside if or for: there would be nothing to register at mount, and the wire’s arguments would read a loop variable that has no value.

Keyboard and highlight

An open menu’s panel takes focus; Up/Down/Home/End move a highlight within it, Enter chooses the highlighted row, Right opens a submenu, Left closes one level, and Escape closes one level. Rows are never focused, so arrowing costs no focus traffic and the previous focus returns when the menu closes. Hovering a row makes it the current one — the pointer and the arrows move the same highlight, so they cannot disagree.

Appearance is data, as everywhere: menu-panel (background, border, radius), menu-bar, menu-separator, and the rows’ :background / :background-highlighted, :color / :color-highlighted, :accel-color, and :submenu-mark — a graphic, so the ▸ can be vector or raster (see Theming).

dropdown

A field that opens a list.

PropertyTypeNotes
:labelStringbindable; the chosen option’s text
:font-size, :font-familybindable
:autofocustrue / falsestatic
(dropdown :label chosen
  (for o options :key o.id
    (menu-item :label o.name :on-select (choose o.id))))

Structurally it is a menu — the same rows, the same popup, the same highlight, dismissal, and deferral. It differs in being a control: it is focusable, opens with Enter/Space/Down, draws like a field, and its list matches its width so it reads as the field opening.

It holds no list and no selection: :label is the chosen option’s text, which you compute from your own data. for is the model, and the wire carries identity — the same stance menus take.

Tooltips

:tooltip "text" on any widget. The pointer rests, the note appears below, and moving away takes it back. The pointer passes straight through it: a tooltip never takes hover from the widget it describes, and never swallows a click meant for what is underneath.

(button :label "Save" :tooltip "Write the file to disk" :on-click save)

Static and universal. Theme it through the tooltip rule (background, border, radius, and the text’s :color).

dialog

A modal sheet you open by binding :open.

PropertyTypeNotes
:openboolbindable; controlled
:titlestringbindable
:on-closehandlerthe user asked to dismiss
(dialog :open confirming :title "Delete draft?" :on-close cancel
  (text :text "This cannot be undone.")
  (container :direction row :gap 8 :justify-content flex-end
    (button :label "Cancel" :on-click cancel)
    (button :label "Delete" :on-click (delete-it picked))))

:open is controlled, exactly like a checkbox’s :checked: you own it, and the dialog never flips it. Escape fires :on-close and does not close — the sheet stays until you clear :open. That is deliberate: a sheet that closed itself would leave :open saying true while the screen said otherwise, and your state is the truth.

The body is deferred like a menu’s, so a closed dialog costs nothing. It centers, dims what is behind it, traps focus, puts focus on the first control inside, and gives focus back when it closes — none of which the dialog itself does; that is the overlay layer.

There is no header/body/footer: a title is a string, and the rest is content you write, button row included.

A known limitation: these are in-window sheets, not real dialog windows. A real one is managed by the compositor — movable outside its parent, decorated, floated, announced as a window. We cannot open one yet: on Wayland a toplevel becomes a dialog through xdg_toplevel.set_parent, and winit exposes neither that call nor the handle to make it with. This should change when winit supports it more broadly; the surface above is chosen so nothing in it says “overlay”.

For file dialogs, use guiduck::app::file_dialog::FileDialog — the desktop’s own chooser, which is a real window, and honours the user’s bookmarks, settings, and sandbox.

Focus

text-input, button, and checkbox participate in focus; Tab and Shift+Tab cycle through focusable widgets in tree order, and clicking a focusable widget (or a descendant of one) focuses it. Clicking elsewhere does not clear focus: the framework maintains the invariant that if any focusable widget exists, one has focus — on mount, after removals, and across hot reloads. :autofocus picks which widget starts focused; the invariant is the fallback. The focused widget shows a focus ring; themes can restyle it through the :focus state selector.

Widgets you write in Rust

This page is the builtin vocabulary, but it is not a closed one. A widget your app needs and the framework does not ship — a chart, a canvas, a document view — you write in Rust, in your own crate, and name from a .gdc exactly as you name a container. A small manifest declares its interface; see Widget manifests (.gdw).