Gallery and hot reload
The gallery is an application that shows every component in both themes. Use it to browse what exists, to develop a component with live reload, and to capture screenshots.
It lives in crates/limn-gallery. Run everything from nix-shell.
Opening the gallery
cargo run -p limn-gallery
The sidebar lists the pages. “Tema claro” at the bottom switches theme, and the line beneath it shows the last action received, which is a quick way to check that a control reports what you expect.
The page Forma y movimiento changes the tokens live: square, default or round corners, three paces, and a switch for reduced motion.
Shared owner lifecycle illustration
cargo run -p limn-gallery -- --page wayland-data-owner
This is a static, synthetic limn illustration: no native owner is attached. It shows why endpoint A can be Detached while endpoint B and their owner remain Ready. It contains no reception controls, native-handle calls or clipboard calls, and does not establish protocol or compositor behavior. See WaylandDataOwner and the canonical file-drop guide.
The shared-owner API deliberately has no live mode in the existing native demo below. Stock eframe owns a clipboard data device on its display; attaching an exclusive owner behind it would violate that owner’s contract. A functional SDK demo depends on real shared clipboard integration. A fake clipboard backend, changed bind order or disabled clipboard would not prove that integration. The existing binary and its historical receiver results remain separate from this synthetic page.
Native file-drop demo
The receive-only Wayland example runs in its own binary:
cargo run -p limn-gallery --bin file_drop
Use Linux/Wayland and drop fixture files into the outlined target. After the complete list reaches EOF, choose Accept Copy or Reject within 30 s. The demo limits a batch to eight files and 32 KiB of decoded paths; the library’s default decision timeout remains 2 s. It shows basename metadata without opening files. Accept authorizes one local Copy finish attempt; it does not prepare or send file content.
This binary attaches only FileDropReceiver, independently of the normal
gallery and its hot-reload/screenshot mode. Its close handshake keeps the
host event loop running until the worker reports Closed. A non-Wayland
window reports unavailable without attaching the legacy path. See
File drops before using the unsafe API. A real fixture test on
Hyprland 0.55.0 with Dolphin 26.04.3 reached Ready but received no drop metadata:
drag events went to the eframe host clipboard’s first data device instead of
the receiver’s separate device. Ready proves initialization only. This setup
needs shared data-device ownership and a host/SDK integration hook that keeps
clipboard text working; changing bind order or disabling clipboard is not a
fix.
A later native run on private nested Weston 15.0.1 received two fixture basenames, accepted the first offer with an explicit Accept Copy click (Ended Ok), and rejected the second with Reject (Ended Cancelled). The Close safely control then led to normal demo exit 0 before cleanup of the source and compositor. This proves only metadata reception, explicit local decisions and normal closure in that setup. It does not fix the Hyprland conflict, verify clipboard functionality, open file contents, or exercise an application upload/send. Other combinations remain unverified.
The target and Close control stay outside the scroll area. Metadata and event logs scroll independently so that their growth cannot shift the target and invalidate its frozen geometry. An earlier nested run was cancelled by that layout change and then timed out; it is retained as a partial attempt, not a normal-close success. Three real UiTree demo tests cover hit testing and stable target geometry at 700×760 and 980×800. Private-server tests alone do not establish native interoperability.
Hot reload
dx serve --hot-patch -p limn-gallery --features hot
Save a file in limn or in the gallery and the running window updates in
under a second. The page you were on, the scroll position, focus and every
value you typed are kept.
The gallery gets this from the library, like any application can: see
Hot reload for how it works, what needs a restart and what
it costs. A restart is not a fresh start: the gallery keeps its state with
limn::hot::remember, so a change that cannot be patched still lands on the
same page with the same values. Passing --page starts clean.
The gallery keeps demo state in a map keyed by control ID rather than in one field per demo, precisely so that adding a demo never changes a struct.
Adding a page
The gallery has a page for each component. A module draws sections, and
PAGES gives each component the sections that are its own, so components
that are shown together can share a module and still have their own pages.
-
Write the sections in
crates/limn-gallery/src/pages/<name>.rs:use super::{row, section, Cx, Section}; use limn::{Component, Switch}; pub fn view(cx: &Cx) -> Vec<Section> { vec![section(cx, "Switch", vec![row(vec![ Switch::new("demo-switch", cx.state.flag("demo-switch"), cx.palette).render(), ])])] } -
Declare the module and list the component in
PAGESinpages/mod.rs:Page { id: "switch", group: "Entradas", title: "Switch", source: "inputs", view: inputs::view, sections: &["Switch"] },titleis the name of the type andidthat name in kebab-case: the address of the component’s page on the site and the name of its guide,docs/components/<id>.md.sectionslists the titles of its sections; leave it empty when the whole module is one component.
A test fails when a section is on no page or a page names a section that does
not exist. Cx gives a module the palette, the state and the width available
to it. section, row and caption are layout helpers shared by the
modules; an element without a title becomes a section with .into().
Demo state without update code
Pages do not write code to handle actions. The gallery interprets the ID of whatever was clicked:
| ID | Effect |
|---|---|
name | Flips a flag. Read it with cx.state.flag("name"). |
group=value | Chooses within a group. Read it with cx.state.choice("group", default). |
a=1&b=2 | Makes several choices at once. |
Text typed into an input is stored under the input’s ID and read with
cx.state.text("id").
So a switch is one line: Switch::new("demo-switch", cx.state.flag("demo-switch"), cx.palette).
A demo whose state is neither a flag nor a choice, such as the sort and the
column widths of the table page, answers its actions in pages::update.
Capturing a page
cargo run -p limn-gallery -- --page dropdown-menu --set overlay-menu=open --screenshot menu.png
| Option | Effect |
|---|---|
--page <id> | The page to open. IDs are in PAGES. |
--pages | Print the components as JSON, one page each, and exit. |
--light | Light theme. |
--set <id=value> | Start as if that control had been clicked. Repeatable. Use it to capture an open menu or dialog. |
--hover <id> | Hold the pointer over a control, to capture its hover state or tooltip. |
--end | Scroll the page to its end. |
--screenshot <file.png> | Save the window and exit. Runs with reduced motion so nothing is caught mid-transition. |
The screenshot is the size the window manager gives the window, so a tiling window manager will produce different sizes at different times.
See also
- Testing for checks that do not need a window.
- Contributing a component for the full workflow.