Skip to content
136 changes: 136 additions & 0 deletions modules/dev_otui/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
# dev_otui — visual `.otui` editor

A development tool for building and editing `.otui` screens while seeing the
real result. The preview is drawn by OTClient's own renderer, so bitmap fonts,
`image-border` 9-slice and the whole style cascade are exact rather than an
approximation.

Open it with the button on the top-right menu, or `Ctrl+Alt+U`.

## Quick start

1. **Open...** and pick any `.otui` in the project — it renders on screen and
the widget tree is filled in.
2. Click a widget in the tree, or tick **Edit** and click it directly on screen.
3. The property panel shows that widget's properties **as written in the file**.
Change a value and press Enter (or **Apply (preview)**) to see it live.
4. **Save to file** writes the change back, keeping the rest of the file intact.

Tick **Auto-reload** to have the preview follow the file while you edit it in
your text editor.

## What each control does

| Control | Purpose |
| --- | --- |
| `Load` / `Open...` | load the `.otui` typed in the field, or pick one from a searchable list |
| `New screen` | create a new file with a minimal `MainWindow`; refuses to overwrite |
| `Reload` | re-read the file, keeping the current selection |
| `+ Element` | insert a child widget, picked from a palette that shows a live preview of each style |
| `- Element` | delete the selected widget from the file, children included (asks first) |
| `Anchor...` | pick an anchor for the selected widget from the valid combinations |
| `Guides` | draw the parent's area and centre lines, and box the widgets this one is anchored to |
| `Style gallery` | render every style of every file under `/styles` in one scrollable list, grouped by file |
| `Auto-reload` | watch the file and reload it on change (300 ms) |
| `Debug boxes` | toggle `g_ui.setDebugBoxesDrawing`, outlining every widget |
| `Edit` | click to select on screen, drag to move, green corner to resize |
| `$on` / `$checked` / `$disabled` | force widget states to inspect their styling |
Comment thread
coderabbitai[bot] marked this conversation as resolved.
| `hidden` | hide the selected widget, to see what is underneath it |
| `Apply (preview)` | apply the panel to the live widget; the file is untouched |
| `Save to file` | write the changes to the `.otui` |

The gallery doubles as an audit of `data/styles`: a style that cannot be sampled
is listed with the reason instead of being hidden, and the same list is printed
to the terminal. The reason is worked out *before* instantiating — an unregistered
base class, a style nested inside another style, an `image-source` that is not in
the data folder — so the audit does not itself fill the log with engine errors.

Three classes are never instantiated at all (`UIStatsBar`, `UIPopupMenu`,
`UIPopupScrollMenu`). They build their own children in `create()` and expect a
host that a sample row cannot provide; standing alone, their layout never
settles and keeps rescheduling itself. Since deferred events are processed
inside the same dispatcher poll, that freezes the client with no error logged —
so they are refused up front rather than tried. The list is also built one
sample per frame: the window is drawn and usable while it fills, instead of the
client going dark through a single 250-sample layout pass.

## How it works

**The property panel mirrors the file's lines, not the widget's getters.**
`otml.lua` parses the `.otui` keeping the original lines, and edits rewrite only
the lines that change. Two consequences:

- Properties with no getter on `UIWidget` — `anchors.*`, `@onClick`, `!text` —
are visible and editable, and values appear exactly as written.
- Saving preserves comments, blank lines, ordering and alignment.

**Dragging rewrites margins, not coordinates.** Which margin depends on the
anchors the widget declares in the file: with `anchors.left` a drag becomes
`margin-left`; with `anchors.right` it becomes `margin-right` inverted; with
both, the widget shifts while keeping its width.

**The element palette previews what you are adding.** Highlighting an entry in
`+ Element` instantiates that style live in the side panel, with its name, size
and the file and line where it is defined. Styles that cannot be built
standalone say so instead of showing an empty box.

**New widgets come in anchored.** A widget with no anchor cannot be positioned
or dragged at all, so inserting one writes anchors with it: hooked below the
previous sibling when there is one (`anchors.top: prev.bottom`), or to the
parent's top-left when it is the first child. If the parent has a `layout:`, the
parent places its children and anchors would be ignored, so none are written.

**`Anchor...` only offers legal combinations.** An edge can only hook to an edge
of the same axis — `anchors.top` accepts `top`, `bottom` and `verticalCenter`,
never `left`. The list is built from that rule, over the parent, `prev`, `next`
and every sibling that has an id, plus the `fill` and `centerIn` shortcuts.

**Guides show what the widget is anchored to.** Blue is the parent's *padding*
rect — the area children are actually laid inside — with its centre lines.
Orange boxes the sibling an anchor points at, so `prev.bottom` stops being
guesswork. They redraw on selection, not on every drag frame: the parent does
not move while you drag.

**Style-only files get a showcase.** A third of the project's `.otui` files
(every one under `/styles`) declares only styles and has no main widget, so
`loadUI` returns `nil` for them. For those the editor instantiates a live sample
of each style; selecting a sample edits the style definition in the file.

Styles that need a specific parent — `MiniWindow` expects a
`MiniWindowContainer` — cannot be instantiated standalone and are listed in red
instead of breaking the showcase.

## Safety

Writing is the only destructive operation, so it is guarded:

- a `.bak` copy is written before each save;
- after writing, the file is read back and compared before reporting success;
- selection is validated by walking the on-screen tree and the file tree in
parallel, comparing the style name at every level. If they disagree — a widget
that came from an inherited style, for instance — saving is refused rather
than writing to the wrong node.

## Tests

The parser is the part capable of corrupting files, so it has a self-test.
In the client terminal (`Ctrl+T`):

```lua
modules.dev_otui.selfTest()
```

It asserts, among others: byte-for-byte round-trip, composite blocks such as
`layout:` not being mistaken for child widgets, edits not touching neighbouring
lines, insertion indentation, and removal of a block taking its children along.

## Notes

- Editing a file under `/styles` changes a style used by the whole client, not
by a single screen.
- `.otui` files are read with a CP1252 bitmap font; multi-byte UTF-8 characters
render as garbage.
- Changing `dev_otui.otmod` requires restarting the client:
`Module::reload()` reuses the script list captured at discovery time.
Changes to `.lua` and `.otui` only need
`g_modules.getModule('dev_otui'):reload()`.
Loading
Loading