-
-
Notifications
You must be signed in to change notification settings - Fork 481
feat(dev_otui): visual .otui screen editor inside the client #1782
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
569fd4e
feat(dev_otui): visual .otui screen editor inside the client
renanale 56264c3
fix(dev_otui): report a file read failure instead of swallowing it
renanale a0d9122
fix(dev_otui): verify the file reference still points at the selected…
renanale cfbb70d
fix(dev_otui): clear nodeError when the preview closes
renanale a326dd4
fix(dev_otui): do not start a drag without a selected widget
renanale b7016bf
fix(dev_otui): keep the new-screen id in English and never let it be …
renanale 657bc94
docs(dev_otui): list the hidden state toggle and hyphenate self-test
renanale f71d252
Merge branch 'main' into feat/otui-editor
renanale File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | | ||
| | `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()`. | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.