# Preparing a `.fig` file for LazyUI Studio

LazyUI Studio reads the saved Figma archive itself. It does not use the Figma API. Deterministic detection needs nothing else; on paid plans, AI review asks an OpenAI model to refine widget semantics for the selected frame. This guide explains how a design is interpreted and how to get the closest Dear ImGui result.

## The short version

1. Put each screen you may want to convert in its own top-level **Frame**, **Component**, or **Instance**.
2. Design at the exact pixel size you want the ImGui window to use.
3. Keep the final resolved positions in Figma; LazyUI reads the resolved geometry and does not recreate auto layout at runtime.
4. Use real text layers, vector icons, image fills, and Figma effects rather than flattened screenshots where possible.
5. Export the untouched file with **File → Save local copy** and upload that `.fig` archive.

Controls, tabs, and navigation are recognized from how they look, so most designs need no naming at all. Only a few things need a hint: extra **pages** (one frame per navigation item), **popups** and the buttons that open them, **notifications** your code shows, and layers you want kept **static**. The hints are plain layer names (see [Name tags](#name-tags)); the optional [Figma plugin](#the-figma-plugin) applies them with one click.

## Pages

When the selected frame has a sidebar or a top bar of items, selecting an item switches the page. The selected frame is the page of the item highlighted in it. Every other item shows a blank page until a frame is assigned to it.

To give each item a page:

1. Duplicate the main frame for each item, highlight that item in the navigation, and change the content. Keep the frames the same size.
2. Name each frame after its item, for example `Visuals` or `Menu | Page: Visuals`. Lazy UIs also recognizes which item a frame highlights.
3. In **Configure → Screens and pages**, check the role Lazy UIs suggests for each frame and change any of them. An item without a frame shows a blank page.

Page content is everything outside the navigation column (or bar) that carries text, icons, images, or panels. The navigation, logo, window background, and decorative glows are shared by every page, and only the content of the assigned frames is used, so their copies of the sidebar never double up. A tab bar at the top of a page switches that page's remaining content the same way. Pages cross-fade when the selection changes, and controls on hidden pages ignore input.

A page doesn't have to be in the navigation. A frame with the same sidebar and the same item highlighted (a Settings page behind a gear icon, a product's details behind **View**) can be a **page a button opens**: the button whose label names it (or one tagged `go: Settings`) opens it over the same sidebar, and Back, Close, or Cancel on it, or choosing any navigation item, closes it. Buttons elsewhere whose labels name a navigation item's page select that item.

Pages and screens go together in one app: a login can lead to a main screen whose sidebar has its own pages. Each page belongs to the screen whose navigation shows it.

On the command line, `--page <item>=<frame>` assigns a frame to a navigation item (0-based, or `x` for a page a button opens), `--page <item>=<frame>@<screen>` gives the page to another screen of the flow, and `--pages` adds every page the analysis suggests.

## Popups

Design the popup where it should appear, visible, so it can be seen while editing. In the generated app it starts hidden and fades in, rising slightly, when its button is clicked.

1. Name the popup's frame or group `popup: Settings`, and keep it at the top of its parent's layer list so nothing covers it.
2. Name the layer that opens it `open: Settings`. The name after the colon must match the popup's.
3. Optionally, name a close icon or Cancel button inside the popup `close`.

While a popup is open, only the controls inside it respond. Clicking outside it, pressing Escape, or clicking a `close` layer closes it; clicking its button again toggles it. For a dimmed backdrop, put a full-size rectangle inside the popup group and it fades in with the popup. Popups float above every page.

## Notifications

A notification is a toast your own code shows, such as "Config saved" or "Injection failed". Design it where it should appear, usually a corner of the window, inside the screen you convert, and leave it visible. In the generated app it starts hidden.

1. Name its frame or group `notify: Saved` (`notification:` and `toast:` work too), and keep it at the top of its parent's layer list so nothing covers it.
2. Optionally, name a close icon inside it `close`, and a thin bar inside it `progress` (or `timer`): the bar empties as the toast's time runs out.
3. Optionally, name a button `show: Saved` to show the notification when it's clicked.

Show it from your code with the function the conversion adds to `generated_ui.hpp`:

```cpp
figma_generated::notify( "Saved" );                                           // the design's own texts
figma_generated::notify( "Saved", "Config loaded", "legit.cfg is active" );   // your title and message
figma_generated::notify( "Error", "Injection failed", nullptr, 0.f );         // stays until closed
```

The first two text layers, read top to bottom, take the title and the message, in the design's font, size, and color, starting where the design's text starts; `nullptr` keeps the design's text. The last argument is how many seconds it stays (4 by default); `0` keeps it until its close layer is clicked.

Toasts stack: the newest appears where it was designed and older ones move up, or down for a notification in the top half of the window. Notifications designed at the same spot share one stack of up to five toasts. The pointer over a toast pauses its timer, and a toast takes the clicks that land on it, so nothing beneath it reacts. Nothing inside a notification becomes a control except its `close` layer. Each stack is a `notification_state` in `figma_generated::controls` (`notify_saved.count` toasts are showing).

## Name tags

Tags are layer names, case-insensitive; a dash works instead of the colon.

| Layer name | Effect |
| --- | --- |
| `popup: Settings` | A popup that starts hidden. `modal:` and `dialog:` work too. |
| `open: Settings` | Clicking it opens or closes the Settings popup. `toggle:` and `show:` work too. |
| `close` or `close: …` | Inside a popup or notification: clicking it closes it. |
| `notify: Saved` | A notification that starts hidden; `figma_generated::notify( "Saved" )` shows it. `notification:` and `toast:` work too. |
| `show: Saved` | Clicking it shows the Saved notification with its own texts. |
| `progress` | Inside a notification: a bar that empties as its time runs out. `timer` works too. |
| `static: Preview` | Stays a plain visual; nothing inside it becomes a control. |
| `Menu \| Page: Visuals` | On a top-level frame: suggested as the page of the Visuals item, or, when no item has that name, as a page a `go: Visuals` button opens. |
| `go: Main` / `back` | A button that leads to the Main screen or page, or back to where the app was. It is a button whatever it looks like. |
| `nav: main` / `nav: sub` | On the group holding the items: the main navigation, or a page's own sub tabs. |
| `Visuals (selected)` | The item a navigation or tab bar shows as selected, when its look alone doesn't say. |

## The Figma plugin

The `figma-plugin` folder (also downloadable from the Studio's Figma plugin page) contains a small development plugin that applies these tags and checks them: popups without a button, buttons that open a missing popup or notification, close layers outside a popup or notification, notifications drawn as their own top-level frame, and popups or notifications covered by other layers.

1. In the Figma desktop app, open a design and choose **Plugins → Development → Import plugin from manifest…**, then pick `figma-plugin/manifest.json`.
2. Run **Plugins → Development → LazyUI Studio**, select a layer, and choose what it should do.

The plugin only renames layers; it has no network access and stores nothing else in the file.

## Frames, pages, and paint order

The Select Design step lists pages and their top-level frames. The selected frame is converted, plus the content of any frames assigned to its navigation items (see [Pages](#pages)). Its top-left becomes `(0, 0)` and every descendant keeps its exact floating-point position relative to that root.

Figma’s saved sibling position is used as the authoritative paint order. Layers lower in the Figma layer list are painted first; later siblings appear above them. Nested frame clipping and child order are preserved.

Use nested frames when content should be clipped. Groups are useful for organization, but a group does not create a clip by itself.

## Responsive Select Design preview

The Select Design preview always fits the selected frame into the available panel without changing its aspect ratio. The browser panel and preview stack automatically on narrower windows. Use **Transparency** to switch the checkerboard on when reviewing translucent layers.

This is a normalized browser layout preview, not a screenshot of compiled Dear ImGui. The downloaded project uses the same normalized geometry and assets.

## Geometry and corner radii

LazyUI preserves:

- Exact `x`, `y`, width, and height values
- Rotation transforms in the browser preview
- Four independent corner radii: top-left, top-right, bottom-right, and bottom-left
- Ellipses, lines, fills, strokes, stroke widths, and clipping

If opposing corner radii are too large to fit, they are scaled together using the same proportional rule used by vector renderers. Generated solid shapes, gradient assets, image clipping, effects, and background blur all use the four fitted values instead of replacing them with one maximum radius. A corner Figma leaves at 0 in independent-corner mode stays square, so a header with rounded top corners keeps square bottom corners.

A top-level frame with a corner radius and **Clip content** becomes a window with truly rounded corners: after everything inside it is drawn, the area outside its rounded corners is cleared to transparent, including images, glows, and blur.

The generated window can be dragged by any empty part of the design; controls keep their clicks. The Menu moves its Dear ImGui window, and the Loader moves its own OS window.

## Text and fonts

Keep copy as text layers. LazyUI reads the layout Figma itself computed for every text layer: each glyph's position and baseline, including kerning, letter spacing, alignment, and line breaks. Generated C++ draws every character at exactly that position, so text lines up with the design to the pixel.

Font sizes are converted from Figma's em size to Dear ImGui's pixel-height convention using the font file's own metrics. A 14 px Figma label renders as a 14 px em in ImGui, not the smaller size ImGui would pick for "14".

The converter embeds the real font file for each text style. It looks in the project font folders first (`assets/fonts` ships Geist Regular, Medium, SemiBold, and Bold), then in the fonts installed on this machine; fonts are never downloaded. When a family is missing, its glyphs are rebuilt into a font from the exact outlines Figma saved in the file, so the design's text still matches, ligatures and alternates included. Characters the design never drew, such as a value typed at runtime, are drawn with Geist, which is merged into that font.

With installed fonts, ligatures such as "fi" are drawn as their separate characters at Figma's caret positions, because Dear ImGui cannot reach a font's ligature glyphs. Text sits on Figma's fractional baselines rather than being snapped to whole pixels.

## Widgets and navigation

The converter detects semantics from geometry, component/instance structure, nearby text, repeated rows, and visual patterns. Names are only one signal. Supported mappings include:

- Checkbox and toggle switch
- Slider
- Combo and multi-combo
- Input and search bar
- Color picker
- Begin/End Child and Begin/End Group regions
- Spacing
- Tabs, sidebar/navigation items, and navigation containers

In the default **Exact visuals** mode, recognized controls keep their exact drawing and become interactive: checkboxes and toggles flip, sliders drag, navigation and tab highlights slide or fade to the selection, dropdowns open, search, text, and password fields accept input, keybinds listen for a key, and buttons react to hover and press. Everything animates with delta time and rests exactly on the design's values. The **Elements** tab after conversion lists every control that became interactive.

Recognition works on anatomy: a label and a small box on one row, a track with a thumb, a fill plus the rest of a track, an item whose background or accent differs from its siblings. Flat layers, nested component frames, and split shapes all work. When the design shows states side by side (an "off" toggle, a "hovered" tab, a "disabled" row), those colors are used for the transitions and each element keeps its own resting look. An unchecked checkbox has no border unless the design draws one on an unchecked box.

In tab bars and sidebars, the highlight slides to the selected item and accent bars and dots travel with it, whether they are loose layers or drawn inside the selected item. When the selected tab's pill takes up room in the row (items after it sit further away by exactly its padding, as with an icon-and-label tab next to icon-only tabs), the clicked tab grows and its neighbours slide aside; a label losing the selection takes the idle icon color.

To keep something that looks like a control as a plain visual, name it `static: …` or choose **Visual only** in **What is this?**.

In **Native widgets** mode, confident detections use the Loader or Menu framework widget instead. The **What is this?** area lets you answer the converter’s question before generation; you never need to rename the Figma layer just to force a widget.

### AI review (paid plans)

When the server has an OpenAI API key, the mapping workspace automatically sends a compact description of the selected frame’s layers (text, hierarchy, geometry, the shapes LazyUI measured such as pill tracks and square boxes, and the deterministic hints) and a picture of the frame to the configured model (GPT-6 Luna by default), with rules for what each control needs and what never counts as one. The answer comes back in a strict JSON schema. Each frame is reviewed once; **Run again** asks the model a second time.

Every suggestion is then checked before it is used. It is discarded when the layer does not exist, when it is the frame itself, when the layers lack the visual evidence the type needs (a square box for a checkbox, a pill track for a toggle, a long track for a slider, a surface for a button), when it covers most of the frame, when it contradicts a layer named by the designer, or when it sits inside, around, or on top of a control that was already found. Studio reports how many guesses were discarded. Only the reviewed frame's description and picture are sent; the original `.fig` and framework source stay on the server.

The AI result is an assistance layer, not executable output. Manual **What is this?** choices have final authority, and the deterministic detector works on its own whenever AI review is off, over its limits, or unavailable. Set `OPENAI_API_KEY` (and optionally `LAZYUI_AI_MODEL` and `LAZYUI_AI_REASONING`) in the server's `.env` file.

For a strong detection signal, keep a control’s label, track/background, value, and icon in the same component, instance, frame, or compact visual row.

## Icons

Use vector geometry for icons. LazyUI automatically discovers standalone vectors and compact vector groups, including unnamed icons.

Every detected icon is:

1. Exported as an individual SVG.
2. Given a stable, deterministic name and private-use Unicode codepoint.
3. Compiled into `lazyui-generated-icons.ttf` on a pixel grid, so the glyph lands exactly where the icon sits in Figma at its exact size, including rotated and flipped icons.
4. Rendered from that font in generated C++.

Icons with several colors become one glyph layer per color, drawn in paint order. Identical icons (the same check mark in every row) share a single glyph.

A group only counts as an icon when everything painted inside it is a solid-color vector. A checkbox made of a filled rectangle plus a check mark stays a composition: the rectangle is drawn natively and only the check becomes a glyph.

Provider-style names such as `icon-asset-name:crosshair` or `uil:crosshair` use the suffix (`crosshair`) as the readable asset name. Names are optional; surrounding text and layer context provide an automatic fallback. Icons are never replaced with random Unicode symbols.

Avoid outlining an icon together with a large decorative background in one vector. Put the icon vectors in a compact group so the automatic bounds describe only the icon.

## Fills, images, and gradients

Solid fills are generated as draw-list geometry. Linear and radial gradients retain their stops, opacity, and saved transform. Gradients that Dear ImGui cannot represent exactly are rendered to a transparent PNG at the layer’s exact size and corner shape.

Embedded images are extracted from the archive, content-hashed, deduplicated, and mapped with Figma’s Fill, Fit, or crop behavior. Image layers keep transparency, opacity, dimensions, and four-corner clipping. An image shown much smaller than its source (a 736 px photo in a 34 px avatar) is resampled once at conversion time to exactly its displayed size. That keeps the build small and avoids the aliasing Dear ImGui would otherwise produce without mipmaps.

Strokes honor Figma's alignment: an inside 1 px border stays inside the shape and pixel-crisp. Independent side weights, such as a bottom-only divider, draw only those sides; on rounded corners they taper exactly as Figma draws them.

Frames with **Clip content** and rounded corners clip their children with those rounded corners, the way Figma does.

Do not replace linked images after saving the local copy: the image bytes must be embedded in the `.fig` archive.

## Effects

The parser reads:

- Drop shadow
- Inner shadow
- Glow-like zero-offset shadows
- Layer blur
- Background blur

Drop shadows, inner shadows, and layer blurs are rendered at conversion time as true Gaussian textures (Figma's radius is twice the standard deviation). They honor spread and offset, and drop shadows are knocked out under translucent fills unless **Show behind transparent areas** is enabled. Shadow textures are alpha masks tinted with the exact effect color, and each texture is trimmed to the pixels it actually covers. A blur on a group with children is composited as the whole group before blurring.

Background blur runs live on the GPU: everything the ImGui scene has drawn behind the layer is blurred with a Gaussian of Figma's exact radius, sampled only inside the layer as Figma does, and composited inside the layer's rounded shape. Layers set to **Linear dodge** are drawn with an additive GPU blend state; other blend modes fall back to normal blending with a warning.

Keep blur and glow layers inside the frame they visually belong to. If the effect intentionally extends outside a clipped parent, disable **Clip content** on that parent in Figma.

## Components, instances, and auto layout

Components and instances are accepted as selectable top-level designs and as nested content. Auto-layout frames use the final positions and sizes stored in the document. Runtime C++ does not reproduce Figma’s constraint or auto-layout engine.

Before saving a local copy, make sure the design is in the final variant and size you want to generate.

## Masks

Figma's **Use as mask** works as in Figma: the mask layer is not drawn, and every sibling above it in the same group is shown only through it.

- An opaque, unrotated rectangle, rounded rectangle, or circle mask clips the layers above it natively, so they stay crisp and any controls inside keep working.
- Every other mask (an image used as an alpha mask, a vector shape, a gradient or translucent mask, an outline mask over text or effects, or a luminance mask) is composited through the mask at conversion time into one exact texture drawn in the mask's place.

## Complex artwork

Complex vectors, blend compositions, and effects that cannot be represented reliably are never silently dropped. The converter either:

- Exports an exact vector icon to SVG and the icon font,
- Rasterizes static artwork to a transparent PNG, or
- Keeps the closest supported representation and adds a warning naming the affected layer.

Review the **Warnings** tab after conversion. Informational rasterization messages are expected for decorative artwork and do not necessarily indicate a visual problem.

## Fidelity checklist

Before conversion:

- Select the intended top-level frame and verify its dimensions.
- Check that the design's fonts are installed on this machine (or in a project font folder) when an exact font match matters.
- Confirm active navigation backgrounds sit below their icon and label in the Figma layer order.
- Keep icons as vectors and images embedded.
- Confirm each clipped panel is a frame with **Clip content** enabled.
- Review large blurs at 100% zoom in Figma.

After conversion:

- Compare the Select frame preview with Figma before choosing an output. Click any layer to inspect its exact values.
- Review uncertain elements in **What is this?**.
- Use **Compare → Build & render** to compile the project and see the real Dear ImGui output next to the design. The Swipe and Difference views show any mismatch at a glance.
- Check the layer-specific warnings.

## Troubleshooting

**A popup never shows**  
Check that its button is named `open: ` followed by the popup's name, and that the popup is a frame or group named `popup: ` with the same name. The Figma plugin's checks find both mistakes.

**A notification never shows**
Check that the name passed to `notify( )` matches the one after `notify:` on the layer, and that the notification is a frame or group inside the converted screen, at the top of its layer list. The Elements tab lists every notification the conversion found.

**Another page is blank**  
Items without an assigned frame show a blank page on purpose. Assign a frame under **Configure → Pages**; only frames of about the same size are offered.

**Something reacts that should not**  
Name it `static: …` or choose **Visual only** in **What is this?**.

**A foreground looks washed out or covered**  
Check sibling order in Figma. Background patterns, gradients, and blur decorations must be below cards, labels, and controls.

**A control looks right but does not react**  
Check that **Animated controls** is on and look at the **Elements** tab: it lists every control that became interactive. A box on a different row from its label, or a slider whose fill and track share one color, may not be recognized; open **What is this?** and choose the intended widget. This records an explicit mapping without changing the `.fig` file.

**An icon is missing**  
Confirm it is vector geometry rather than a text glyph from an unavailable font. A failed icon conversion is surfaced as an error because detected icons are required to complete SVG-to-font generation.

**A font looks different**  
Missing fonts are rebuilt from Figma's outlines automatically. If a layer still uses a different face, the file carried no outline data for it; install the exact font or add its `.ttf`/`.otf` files to a folder listed in `designFontDirectories` in `config/<framework>-framework.json`, then convert again. The Warnings tab names every affected layer.

**The archive is rejected**  
Upload the untouched local Figma Design copy. JSON renamed to `.fig`, truncated downloads, archives without `canvas.fig`, and unsupported canvas versions are rejected deliberately.
