21 Commits
Author SHA1 Message Date
4DBug ff86ea5aa2 0.2.0
Build & Release / build (push) Waiting to run
2026-10-15 01:06:11 -05:00
4DBug 4ac57cb571 Roblox backend: redraw budget is opt-in (SetRedrawBudget default 0 = every changed tile drawn the same frame) 2026-10-15 00:55:43 -05:00
4DBug 15dc766d82 bundle.py: build-time Luau optimizations (draw-data identical; IMGUI_BUNDLE_NOOPT=1 disables): hot library globals as bundle locals, enum/constant folding (values dumped by the luau CLI; IMGUI_BUNDLE_STRICT=1 in CI), --@hoist pure helpers to inlinable bundle-level local functions, statement IM_ASSERT -> assert; builtin type instead of ImGui._GetTypeFunc() alias (GETGLOBAL 15903 -> 540) 2026-10-15 00:23:20 -05:00
4DBug ed53d0683e Library perf (draw-data identical): layout core (no ImVec2 __index in ItemSize, inlined ID/style/clip stacks, scalar ItemAdd/IsClippedEx/CalcItemSize/GetColorU32), text + widgets (scalar TextEx/ButtonEx/Checkbox/Slider/Drag/Selectable/TreeNode, word-wrap memo), tables (in-place clip rects, no per-row/cell temporaries, @native cell functions, scalar ClipWith), draw list (PrimRectXY, scalar AddRect/AddRectFilled/AddLine/PathRect/arcs) 2026-10-14 23:27:23 -05:00
4DBug b71e29ee35 Roblox backend perf (pixel-identical): redraw budget (SetRedrawBudget, default 1.5 ms; RenderRate default 0), exact in-place draw list compare + whole-pixel move fast path, partial tile redraws, flat rects hashed by in-tile rect, quads hashed by TL/BR, no full redraw on texture updates, @native CopyTexturePixels, reused partial upload buffers; imgui_draw: @native word wrap, ImStrByte/classifier inlined, text size cache covers wrapped text; t_pixels pixel oracle; run.py RUN_FLAGS 2026-10-14 22:26:38 -05:00
4DBug bcf383a079 tools/compare_upstream.py: port coverage + code similarity vs upstream, writes docs/PORT_STATUS.txt 2026-10-14 22:26:38 -05:00
4DBug ce82582e37 0.1.4
Build & Release / build (push) Waiting to run
2026-10-14 10:32:10 -05:00
4DBug 090525076f InputText: fix clipboard paste (filter was inverted, then off-by-one in the filtered buffer); ImParseFormatTrimDecorations: return [start, end) like upstream (Ctrl+click temp input showed decorations / crashed on "%d%%"); ShrinkWidths: tie order by Index descending like upstream; t_fixes test 2026-10-14 10:32:10 -05:00
4DBug 95fdf94912 Comments: mirror upstream imgui.cpp/.h, imgui_internal.h, widgets/draw/tables/demo and imstb_* comments (comment-only, code tokens unchanged)
Build & Release / build (push) Waiting to run
2026-10-14 10:23:55 -05:00
4DBug e502866075 CI: minified builds (ImGui.min.luau, ImGui-nodemo.min.luau, imgui_love.min.lua, imgui_love-nodemo.min.lua) via tools/minify.py; checks + release assets; run.py MINIFY=1 2026-10-14 10:23:55 -05:00
4DBug 1431d2e19d 0.1.3
Build & Release / build (push) Waiting to run
2026-10-14 06:26:28 -05:00
4DBug 7eb7d5fcbb Docking: set node.LastBgColor (closing dock hosts drew white bodies), host skips own bg, fix SetNextWindowBgAlpha; Roblox backend: frames skipped by the render rate cap are drawn later (UI could freeze when the app stopped rendering); t_dock_close test 2026-10-14 06:26:28 -05:00
4DBug 2558c30d32 0.1.2
Build & Release / build (push) Canceled after 0s
2026-10-09 02:24:21 -05:00
4DBug e228ed8638 Roblox backend: text proxy is opt-in (SetTextProxy), remove SetTextProxyDebug; new example main.client.luau 2026-10-09 02:22:30 -05:00
4DBug 28dc08e043 README: move 'How it works' below Getting Started
Build & Release / build (push) Canceled after 0s
2026-10-08 20:57:31 -05:00
4DBug f63a37f07d README: Wally package link; 0.1.1
Build & Release / build (push) Canceled after 0s
2026-10-08 20:54:31 -05:00
4DBug 08356b4f1f Wally: scope 4dbug (must match the GitHub account)
Build & Release / build (push) Canceled after 0s
2026-10-08 20:52:52 -05:00
4DBug 14e6da3b29 SetNextWindowCached(key): opt-in cached windows on top of the refresh policy (skip contents while idle), t_cached_window test, README snippet
Build & Release / build (push) Canceled after 0s
2026-10-08 20:43:56 -05:00
4DBug df4cf4cd80 Luau types: generated typed entry module build/ImGuiTyped.luau (requires the bundle as child Impl); Rojo/Wally use it 2026-10-08 20:36:27 -05:00
4DBug 2bef2ad43a Wally package (bug/imgui-luau), published by CI on v* tags 2026-10-08 20:30:22 -05:00
4DBug df9f194d87 README: AI disclosure wording; .gitignore 2026-10-08 20:29:29 -05:00
37 changed files with 47325 additions and 17657 deletions
+42 -25
View File
@@ -10,7 +10,7 @@ Dear ImGui for Roblox (and more)
A Lua/Luau port of [Dear ImGui](https://github.com/ocornut/imgui) (docking branch, 1.93 WIP). **Roblox is the primary target** (rendered with EditableImages); the same sources also run on [LÖVE](https://love2d.org) (LuaJIT, GPU renderer), and the backend layout leaves room for more. It follows the C++ source function by function, so upstream documentation, examples and the demo apply almost directly. A Lua/Luau port of [Dear ImGui](https://github.com/ocornut/imgui) (docking branch, 1.93 WIP). **Roblox is the primary target** (rendered with EditableImages); the same sources also run on [LÖVE](https://love2d.org) (LuaJIT, GPU renderer), and the backend layout leaves room for more. It follows the C++ source function by function, so upstream documentation, examples and the demo apply almost directly.
| [The Pitch](#the-pitch) - [Usage](#usage) - [How it works](#how-it-works) - [Demo](#demo) - [Getting Started & Integration](#getting-started--integration) - [Backends](#backends) | | [The Pitch](#the-pitch) - [Usage](#usage) - [Demo](#demo) - [Getting Started & Integration](#getting-started--integration-roblox) - [How it works](#how-it-works-roblox-backend) - [Backends](#backends) |
:----------------------------------------------------------: | :----------------------------------------------------------: |
| [Differences from C++](#differences-from-c-dear-imgui) - [Performance](#performance) - [Testing](#testing) - [Not ported yet](#not-ported-yet) | | [Differences from C++](#differences-from-c-dear-imgui) - [Performance](#performance) - [Testing](#testing) - [Not ported yet](#not-ported-yet) |
| [FAQ](#faq) - [Credits](#credits) - [License](#license) | | [FAQ](#faq) - [Credits](#credits) - [License](#license) |
@@ -79,27 +79,6 @@ Dear ImGui allows you to **create elaborate tools** as well as very short-lived
Every snippet in this README is run by [`tools/test/scripts/t_readme.luau`](/tools/test/scripts/t_readme.luau), and every image was rendered by this port with [`tools/screenshots/make.py`](/tools/screenshots/make.py). Every snippet in this README is run by [`tools/test/scripts/t_readme.luau`](/tools/test/scripts/t_readme.luau), and every image was rendered by this port with [`tools/screenshots/make.py`](/tools/screenshots/make.py).
### How it works (Roblox backend)
Dear ImGui itself never draws anything: every frame it produces `ImDrawData`, a list of textured, coloured triangles plus clip rectangles, and expects the backend to put them on screen. On desktop that is a few GPU draw calls. Roblox has no API to draw arbitrary triangles in 2D, so the Roblox backend ([backends/imgui_impl_roblox.lua](/backends/imgui_impl_roblox.lua)) does what a GPU would do, in Luau, and pushes the result into **EditableImages**.
**Why software rasterization?** The alternatives don't fit Dear ImGui:
- *One GUI Instance per widget/shape* (what most Roblox UI libraries do) means re-creating, re-parenting or diffing thousands of Instances every frame, can't express anti-aliased curves, per-vertex gradients or font-atlas glyphs, and would no longer be Dear ImGui's renderer output.
- *Rasterizing the draw data* keeps the output identical to Dear ImGui's: the same fonts, anti-aliasing, rounding and gradients, and every widget (including custom `ImDrawList` drawing) works without backend support.
The cost is CPU time in Luau, so the backend is built around **not drawing**:
1. **Layers per window.** Every draw list (one per top-level window, popup, tooltip) gets its own `Frame` with `ZIndex` following Dear ImGui's draw order. Its content is rasterized relative to the window, so **moving a window only changes the Frame's `Position`**; no pixel is redrawn or uploaded.
2. **Tiles.** Each layer is covered by 128×128 EditableImage tiles (`ImageLabel`s). Every primitive is hashed (positions relative to the layer, UVs, colours, texture, clip) into the tiles it touches. **Only tiles whose hash changed are re-rasterized and uploaded**, and only the dirty region of those (`WritePixelsBuffer` with a sub-rectangle). A static UI uploads nothing.
3. **Scrolling reuses pixels.** Window content is a separate layer inside a `ClipsDescendants` frame sized to the window's visible area, positioned at the scroll offset. Scrolling moves it like dragging moves a window; only rows entering view are drawn. Decorations (title bar, borders, scrollbar) stay in their own layers.
4. **Skipping unchanged windows.** Vertices are stored in a flat Luau `buffer` (36 bytes each) instead of tables. Before any per-tile work, a window's raw vertex/index bytes are compared with last frame's; if identical, its layers are just restacked.
5. **Fast rasterizer.** `@native` functions; axis-aligned quads (most rects and every glyph) take a span-fill path, flat rows are copied instead of re-blended, gradient rects (color pickers) are filled per row, and only real triangles (anti-aliased edges, curves) use the generic edge-function rasterizer.
6. **Render rate.** Tiles update at most `SetRenderRate(hz)` times per second (default 60), optionally lower while scrolling/resizing (`SetBusyRenderRate`). Your UI code and Dear ImGui's logic still run every frame, so input is never dropped.
**Input.** Mouse and keys come from `UserInputService`. While the mouse is over Dear ImGui (`io.WantCaptureMouse`), the tiles are `Active`, so clicks and the wheel don't reach the game (camera zoom). Games can't read the clipboard, so while a text field is active a hidden `TextBox` holds focus: the OS delivers typing (any layout), paste and copy/cut through it, and its text mirrors Dear ImGui's current selection. Fonts are TTFs rasterized by the ported stb_truetype into the font atlas, which becomes a backend texture like any image (`CreateTexture` works for your own pixels too).
**Instances inside windows.** The backend can place your own Instances (e.g. a `ViewportFrame`) inside a window's layer, so they stack, clip and move with it; see the `ImGui.Embed` widget in [examples/example_roblox/main.client.luau](/examples/example_roblox/main.client.luau).
### Demo ### Demo
Calling `ImGui.ShowDemoWindow()` creates a demo window that showcases a variety of features and examples. The code is in [imgui_demo.lua](/imgui_demo.lua), like upstream's `imgui_demo.cpp`. **It's strongly recommended to keep it at hand as a reference**: you can find the code for any widget you see by searching for its label. Calling `ImGui.ShowDemoWindow()` creates a demo window that showcases a variety of features and examples. The code is in [imgui_demo.lua](/imgui_demo.lua), like upstream's `imgui_demo.cpp`. **It's strongly recommended to keep it at hand as a reference**: you can find the code for any widget you see by searching for its label.
@@ -137,7 +116,9 @@ The Style Editor (`ImGui.ShowStyleEditor()`) and the Metrics/Debugger (`ImGui.Sh
### Getting Started & Integration (Roblox) ### Getting Started & Integration (Roblox)
1. Build the module: `python3 tools/bundle.py` writes `build/ImGui.luau` (`--no-demo` leaves the demo out). With [Wally](https://wally.run) ([4dbug/imgui-luau](https://wally.run/package/4dbug/imgui-luau)): add `ImGui = "4dbug/[email protected]"` under `[dependencies]` in your `wally.toml`, run `wally install`, then `require(ReplicatedStorage.Packages.ImGui)`. Or from source:
1. Build the module: `python3 tools/bundle.py` writes `build/ImGui.luau` (`--no-demo` leaves the demo out) and `build/ImGuiTyped.luau`, a small typed entry module (Luau types for the public `ImGui.*` API, `ImVec2`/`ImVec4` and the flag enums) that requires `build/ImGui.luau` as its child `Impl`. The Wally package and the Rojo projects use that layout, so you get autocomplete and type errors in `--!strict` scripts. The single-file `ImGui.luau` also works on its own, just untyped.
2. Sync with [Rojo](https://rojo.space): [`default.project.json`](/default.project.json) puts the module in `ReplicatedStorage.ImGui` and the example LocalScript in `StarterPlayerScripts`. 2. Sync with [Rojo](https://rojo.space): [`default.project.json`](/default.project.json) puts the module in `ReplicatedStorage.ImGui` and the example LocalScript in `StarterPlayerScripts`.
3. A minimal LocalScript (full version: [examples/example_roblox/main.client.luau](/examples/example_roblox/main.client.luau)): 3. A minimal LocalScript (full version: [examples/example_roblox/main.client.luau](/examples/example_roblox/main.client.luau)):
@@ -170,7 +151,8 @@ Backend options:
| --- | --- | | --- | --- |
| `ImGui_ImplRoblox.Init(parent, render_scale)` | `render_scale` = your OS display scale renders at physical-pixel resolution (crisper, same size as desktop imgui). | | `ImGui_ImplRoblox.Init(parent, render_scale)` | `render_scale` = your OS display scale renders at physical-pixel resolution (crisper, same size as desktop imgui). |
| `ImGui_ImplRoblox.SetRenderScale(scale)` | Change it at runtime. | | `ImGui_ImplRoblox.SetRenderScale(scale)` | Change it at runtime. |
| `ImGui_ImplRoblox.SetRenderRate(hz)` | Max tile redraws per second (default 60, `0` = every frame). Your code still runs every frame. | | `ImGui_ImplRoblox.SetRenderRate(hz)` | Max tile redraws per second (default `0` = every frame). Your code still runs every frame. |
| `ImGui_ImplRoblox.SetRedrawBudget(ms)` | Max milliseconds per frame spent redrawing changed tiles (opt-in; default `0` = no limit, every change is drawn the same frame). With a budget, tiles over it keep their old pixels for a few frames, so big changes are spread over frames instead of one long frame (parts of the UI may update a frame or two apart). |
| `ImGui_ImplRoblox.SetBusyRenderRate(hz)` | Optional lower redraw rate while scrolling, resizing or moving windows (default off). | | `ImGui_ImplRoblox.SetBusyRenderRate(hz)` | Optional lower redraw rate while scrolling, resizing or moving windows (default off). |
| `ImGui_ImplRoblox.SetProfiling(true)` | MicroProfiler (Ctrl+F6) labels for every backend stage. | | `ImGui_ImplRoblox.SetProfiling(true)` | MicroProfiler (Ctrl+F6) labels for every backend stage. |
| `ImGui_ImplRoblox.AddFile(path, bytes)` | Register a file (e.g. a `.ttf`) so `io.Fonts:AddFontFromFileTTF(path, size)` can load it. | | `ImGui_ImplRoblox.AddFile(path, bytes)` | Register a file (e.g. a `.ttf`) so `io.Fonts:AddFontFromFileTTF(path, size)` can load it. |
@@ -182,6 +164,27 @@ io.Fonts:AddFontDefault()
io.Fonts:AddFontFromFileTTF("fonts/Roboto-Medium.ttf", 16.0) io.Fonts:AddFontFromFileTTF("fonts/Roboto-Medium.ttf", 16.0)
``` ```
### How it works (Roblox backend)
Dear ImGui itself never draws anything: every frame it produces `ImDrawData`, a list of textured, coloured triangles plus clip rectangles, and expects the backend to put them on screen. On desktop that is a few GPU draw calls. Roblox has no API to draw arbitrary triangles in 2D, so the Roblox backend ([backends/imgui_impl_roblox.lua](/backends/imgui_impl_roblox.lua)) does what a GPU would do, in Luau, and pushes the result into **EditableImages**.
**Why software rasterization?** The alternatives don't fit Dear ImGui:
- *One GUI Instance per widget/shape* (what most Roblox UI libraries do) means re-creating, re-parenting or diffing thousands of Instances every frame, can't express anti-aliased curves, per-vertex gradients or font-atlas glyphs, and would no longer be Dear ImGui's renderer output.
- *Rasterizing the draw data* keeps the output identical to Dear ImGui's: the same fonts, anti-aliasing, rounding and gradients, and every widget (including custom `ImDrawList` drawing) works without backend support.
The cost is CPU time in Luau, so the backend is built around **not drawing**:
1. **Layers per window.** Every draw list (one per top-level window, popup, tooltip) gets its own `Frame` with `ZIndex` following Dear ImGui's draw order. Its content is rasterized relative to the window, so **moving a window only changes the Frame's `Position`**; no pixel is redrawn or uploaded.
2. **Tiles.** Each layer is covered by 128×128 EditableImage tiles (`ImageLabel`s). Every primitive is hashed (positions relative to the layer, UVs, colours, texture, clip) into the tiles it touches. **Only tiles whose hash changed are re-rasterized and uploaded**, and only the dirty region of those (`WritePixelsBuffer` with a sub-rectangle). A static UI uploads nothing.
3. **Scrolling reuses pixels.** Window content is a separate layer inside a `ClipsDescendants` frame sized to the window's visible area, positioned at the scroll offset. Scrolling moves it like dragging moves a window; only rows entering view are drawn. Decorations (title bar, borders, scrollbar) stay in their own layers.
4. **Skipping unchanged windows.** Vertices are stored in a flat Luau `buffer` (36 bytes each) instead of tables. Before any per-tile work, a window's raw vertex/index bytes are compared with last frame's; if identical, its layers are just restacked.
5. **Fast rasterizer.** `@native` functions; axis-aligned quads (most rects and every glyph) take a span-fill path, flat rows are copied instead of re-blended, gradient rects (color pickers) are filled per row, and only real triangles (anti-aliased edges, curves) use the generic edge-function rasterizer.
6. **Redraw budget.** Changed tiles are redrawn in the frame they change. Optionally, `SetRedrawBudget(ms)` caps the redraw time per frame; tiles over it keep their old pixels and follow on the next frames. Optionally capped with `SetRenderRate(hz)` / `SetBusyRenderRate(hz)`. Your UI code and Dear ImGui's logic still run every frame, so input is never dropped.
**Input.** Mouse and keys come from `UserInputService`. While the mouse is over Dear ImGui (`io.WantCaptureMouse`), the tiles are `Active`, so clicks and the wheel don't reach the game (camera zoom). Typing comes from key codes by default (printable ASCII). Call `ImGui_ImplRoblox.SetTextProxy(true)` to opt in to a hidden `TextBox` that holds focus while a text field is active: the OS then delivers typing (any layout), paste and copy/cut through it, and its text mirrors Dear ImGui's current selection. Fonts are TTFs rasterized by the ported stb_truetype into the font atlas, which becomes a backend texture like any image (`CreateTexture` works for your own pixels too).
**Instances inside windows.** The backend can place your own Instances (e.g. a `ViewportFrame`) inside a window's layer, so they stack, clip and move with it; see the `ImGui.Embed` widget in [examples/example_roblox/main.client.luau](/examples/example_roblox/main.client.luau).
### Backends ### Backends
| Backend | File | Build | Renderer | Example | | Backend | File | Build | Renderer | Example |
@@ -242,6 +245,20 @@ Measured headless (Luau CLI, Studio-like native code generation), demo window op
Most of the remaining cost is widget logic (your UI code and Dear ImGui's per-widget work), which runs every frame by design. Use `ImGuiListClipper` for long lists. Most of the remaining cost is widget logic (your UI code and Dear ImGui's per-widget work), which runs every frame by design. Use `ImGuiListClipper` for long lists.
Windows that rarely change can opt in to caching: `ImGui.SetNextWindowCached(key)` keeps last frame's drawing and makes `Begin` return `visible = false`, so your code for that window doesn't run. It refreshes when `key` changes, while the window is hovered, focused or active (plus one frame after), and when it appears, moves, resizes, scrolls, collapses or is docked. An idle 200-widget window goes from ~2.2 ms to ~0.15 ms per frame.
```lua
local stats = { version = 0, kills = 0 }
local function StatsWindow()
ImGui.SetNextWindowCached(stats.version) -- bump stats.version whenever stats change
local _, visible = ImGui.Begin("Stats")
if visible then -- false while the cached contents are reused
ImGui.Text("Kills: %d", stats.kills)
end
ImGui.End()
end
```
### Testing ### Testing
`tools/test/run.py` runs any LocalScript headless against a mocked Roblox (`tools/test/roblox_mock.luau`): `tools/test/run.py` runs any LocalScript headless against a mocked Roblox (`tools/test/roblox_mock.luau`):
@@ -283,7 +300,7 @@ Multi-select / box-select, typing-select, the Assets Browser example, the Item P
### AI disclosure ### AI disclosure
AI tools were used to assist in making this project (porting code, the Roblox and LÖVE backends, optimizations, tests and documentation). All changes were reviewed and tested by a human, and correctness is checked by the test suite (draw-data checksums against the reference behaviour, pixel comparisons, behaviour tests). Bug reports are welcome. AI tools were used to assist in making this project (porting code, optimizations, tests and documentation). All changes were reviewed and tested by a human, and correctness is checked by the test suite (draw-data checksums against the reference behaviour, pixel comparisons, behaviour tests). Bug reports are welcome.
### Credits ### Credits
+42 -3
View File
@@ -30,24 +30,47 @@ jobs:
unzip -q luau.zip -d luau && chmod +x luau/* && echo "$PWD/luau" >> "$GITHUB_PATH" unzip -q luau.zip -d luau && chmod +x luau/* && echo "$PWD/luau" >> "$GITHUB_PATH"
- name: Build - name: Build
env:
IMGUI_BUNDLE_STRICT: "1" # fail if enum folding (needs the luau CLI above) can't run
run: | run: |
mkdir -p dist mkdir -p dist
python3 tools/bundle.py dist/roblox # ImGui.luau (library + demo) python3 tools/bundle.py dist/roblox # ImGui.luau (library + demo)
python3 tools/bundle.py --no-demo dist/roblox-nodemo # ImGui.luau (library only) python3 tools/bundle.py --no-demo dist/roblox-nodemo # ImGui.luau (library only)
python3 tools/bundle.py --target love dist/love # imgui_love.lua (library + demo) python3 tools/bundle.py --target love dist/love # imgui_love.lua (library + demo)
python3 tools/bundle.py --target love --no-demo dist/love-nodemo python3 tools/bundle.py --target love --no-demo dist/love-nodemo
# minified variants (comments/whitespace stripped, same code): tools/minify.py
python3 tools/minify.py dist/roblox/ImGui.luau dist/ImGui.min.luau
python3 tools/minify.py dist/roblox-nodemo/ImGui.luau dist/ImGui-nodemo.min.luau
python3 tools/minify.py dist/love/imgui_love.lua dist/imgui_love.min.lua
python3 tools/minify.py dist/love-nodemo/imgui_love.lua dist/imgui_love-nodemo.min.lua
- name: Check - name: Check
run: | run: |
python3 tools/sync_github_readme.py --check python3 tools/sync_github_readme.py --check
luau-compile --null dist/roblox/ImGui.luau luau-compile --null dist/roblox/ImGui.luau
luau-compile --null dist/roblox-nodemo/ImGui.luau luau-compile --null dist/roblox-nodemo/ImGui.luau
sed 's|require(script:WaitForChild("Impl"))|nil|' dist/roblox/ImGuiTyped.luau > /tmp/ImGuiTyped.luau && luau-analyze /tmp/ImGuiTyped.luau
luajit -bl dist/love/imgui_love.lua > /dev/null luajit -bl dist/love/imgui_love.lua > /dev/null
luajit -bl dist/love-nodemo/imgui_love.lua > /dev/null luajit -bl dist/love-nodemo/imgui_love.lua > /dev/null
python3 tools/minify.py --test
luau-compile --null dist/ImGui.min.luau
luau-compile --null dist/ImGui-nodemo.min.luau
luajit -bl dist/imgui_love.min.lua > /dev/null
luajit -bl dist/imgui_love-nodemo.min.lua > /dev/null
python3 tools/test/run.py --main tools/test/scripts/t_readme.luau --frames 30 --quiet --roblox --timeout 300 | tee /tmp/r.txt python3 tools/test/run.py --main tools/test/scripts/t_readme.luau --frames 30 --quiet --roblox --timeout 300 | tee /tmp/r.txt
grep -q "README OK" /tmp/r.txt grep -q "README OK" /tmp/r.txt
python3 tools/test/run.py --main tools/test/scripts/t_cached_window.luau --frames 130 --quiet --roblox --timeout 300 | tee /tmp/c.txt
grep -q "CACHED OK" /tmp/c.txt
MINIFY=1 python3 tools/test/run.py --main tools/test/scripts/t_readme.luau --frames 30 --quiet --roblox --timeout 300 | tee /tmp/rm.txt
grep -q "README OK" /tmp/rm.txt
python3 tools/test/run.py --main tools/test/scripts/t_fixes.luau --frames 25 --quiet --roblox --timeout 300 | tee /tmp/f.txt
grep -q "FIXES OK" /tmp/f.txt
python3 tools/bundle.py --target love >/dev/null python3 tools/bundle.py --target love >/dev/null
luajit tools/test/love/t_drawhash.lua 60 Widgets,Basic luajit tools/test/love/t_drawhash.lua 60 Widgets,Basic | tee /tmp/h.txt
cp build/imgui_love.lua /tmp/imgui_love.lua && cp dist/imgui_love.min.lua build/imgui_love.lua
luajit tools/test/love/t_drawhash.lua 60 Widgets,Basic | tee /tmp/hm.txt
cp /tmp/imgui_love.lua build/imgui_love.lua
diff /tmp/h.txt /tmp/hm.txt # minified LÖVE bundle draws exactly the same
luajit tools/test/love/t_examples.lua luajit tools/test/love/t_examples.lua
- name: Package - name: Package
@@ -59,7 +82,7 @@ jobs:
cp love/imgui_love.lua imgui_love.lua cp love/imgui_love.lua imgui_love.lua
cp love-nodemo/imgui_love.lua imgui_love-nodemo.lua cp love-nodemo/imgui_love.lua imgui_love-nodemo.lua
# Roblox: module + example LocalScript + Rojo project # Roblox: module + example LocalScript + Rojo project
mkdir -p pkg-roblox/build && cp ImGui.luau pkg-roblox/build/ && cp ../default.project.json pkg-roblox/ mkdir -p pkg-roblox/build && cp ImGui.luau roblox/ImGuiTyped.luau pkg-roblox/build/ && cp ../default.project.json pkg-roblox/
mkdir -p pkg-roblox/examples && cp -r ../examples/example_roblox ../examples/example_shared pkg-roblox/examples/ mkdir -p pkg-roblox/examples && cp -r ../examples/example_roblox ../examples/example_shared pkg-roblox/examples/
(cd pkg-roblox && zip -qr ../imgui-luau-roblox-$V.zip .) (cd pkg-roblox && zip -qr ../imgui-luau-roblox-$V.zip .)
# LÖVE: bundle + fonts + examples (run: love examples/example_love) # LÖVE: bundle + fonts + examples (run: love examples/example_love)
@@ -77,9 +100,25 @@ jobs:
dist/ImGui-nodemo.luau dist/ImGui-nodemo.luau
dist/imgui_love.lua dist/imgui_love.lua
dist/imgui_love-nodemo.lua dist/imgui_love-nodemo.lua
dist/*.min.luau
dist/*.min.lua
dist/*.zip dist/*.zip
generate_release_notes: true generate_release_notes: true
- name: Publish to Wally (tag)
if: startsWith(github.ref, 'refs/tags/v')
env:
WALLY_TOKEN: ${{ secrets.WALLY_TOKEN }}
run: |
[ -n "$WALLY_TOKEN" ] || { echo "WALLY_TOKEN not set, skipping"; exit 0; }
grep -q "^version = \"${GITHUB_REF_NAME#v}\"" wally.toml || { echo "wally.toml version != tag"; exit 1; }
curl -fsSL -o wally.zip https://github.com/UpliftGames/wally/releases/download/v0.3.2/wally-v0.3.2-linux.zip
unzip -q wally.zip -d wallybin && chmod +x wallybin/wally
mkdir -p dist/wally/src && cp wally.toml dist/wally/ && cp dist/roblox/ImGuiTyped.luau dist/wally/src/init.luau && cp dist/roblox/ImGui.luau dist/wally/src/Impl.luau
echo '{"name":"imgui-luau","tree":{"$path":"src"}}' > dist/wally/default.project.json
wallybin/wally login --token "$WALLY_TOKEN"
wallybin/wally publish --project-path dist/wally
- name: Release (latest) - name: Release (latest)
if: "!startsWith(github.ref, 'refs/tags/v')" if: "!startsWith(github.ref, 'refs/tags/v')"
env: env:
@@ -88,4 +127,4 @@ jobs:
gh release delete latest --yes --cleanup-tag || true gh release delete latest --yes --cleanup-tag || true
gh release create latest --latest --title "Latest build (${GITHUB_SHA::7})" \ gh release create latest --latest --title "Latest build (${GITHUB_SHA::7})" \
--notes "Automatic build of ${GITHUB_SHA} on ${GITHUB_REF_NAME}. Tag a commit v* for a versioned release." \ --notes "Automatic build of ${GITHUB_SHA} on ${GITHUB_REF_NAME}. Tag a commit v* for a versioned release." \
dist/ImGui.luau dist/ImGui-nodemo.luau dist/imgui_love.lua dist/imgui_love-nodemo.lua dist/*.zip dist/ImGui.luau dist/ImGui-nodemo.luau dist/imgui_love.lua dist/imgui_love-nodemo.lua dist/*.min.luau dist/*.min.lua dist/*.zip
+4
View File
@@ -0,0 +1,4 @@
.opencode/
Packages/
wally.lock
__pycache__/
+42 -25
View File
@@ -7,7 +7,7 @@ Dear ImGui for Roblox (and more)
A Lua/Luau port of [Dear ImGui](https://github.com/ocornut/imgui) (docking branch, 1.93 WIP). **Roblox is the primary target** (rendered with EditableImages); the same sources also run on [LÖVE](https://love2d.org) (LuaJIT, GPU renderer), and the backend layout leaves room for more. It follows the C++ source function by function, so upstream documentation, examples and the demo apply almost directly. A Lua/Luau port of [Dear ImGui](https://github.com/ocornut/imgui) (docking branch, 1.93 WIP). **Roblox is the primary target** (rendered with EditableImages); the same sources also run on [LÖVE](https://love2d.org) (LuaJIT, GPU renderer), and the backend layout leaves room for more. It follows the C++ source function by function, so upstream documentation, examples and the demo apply almost directly.
| [The Pitch](#the-pitch) - [Usage](#usage) - [How it works](#how-it-works) - [Demo](#demo) - [Getting Started & Integration](#getting-started--integration) - [Backends](#backends) | | [The Pitch](#the-pitch) - [Usage](#usage) - [Demo](#demo) - [Getting Started & Integration](#getting-started--integration-roblox) - [How it works](#how-it-works-roblox-backend) - [Backends](#backends) |
:----------------------------------------------------------: | :----------------------------------------------------------: |
| [Differences from C++](#differences-from-c-dear-imgui) - [Performance](#performance) - [Testing](#testing) - [Not ported yet](#not-ported-yet) | | [Differences from C++](#differences-from-c-dear-imgui) - [Performance](#performance) - [Testing](#testing) - [Not ported yet](#not-ported-yet) |
| [FAQ](#faq) - [Credits](#credits) - [License](#license) | | [FAQ](#faq) - [Credits](#credits) - [License](#license) |
@@ -76,27 +76,6 @@ Dear ImGui allows you to **create elaborate tools** as well as very short-lived
Every snippet in this README is run by [`tools/test/scripts/t_readme.luau`](tools/test/scripts/t_readme.luau), and every image was rendered by this port with [`tools/screenshots/make.py`](tools/screenshots/make.py). Every snippet in this README is run by [`tools/test/scripts/t_readme.luau`](tools/test/scripts/t_readme.luau), and every image was rendered by this port with [`tools/screenshots/make.py`](tools/screenshots/make.py).
### How it works (Roblox backend)
Dear ImGui itself never draws anything: every frame it produces `ImDrawData`, a list of textured, coloured triangles plus clip rectangles, and expects the backend to put them on screen. On desktop that is a few GPU draw calls. Roblox has no API to draw arbitrary triangles in 2D, so the Roblox backend ([backends/imgui_impl_roblox.lua](backends/imgui_impl_roblox.lua)) does what a GPU would do, in Luau, and pushes the result into **EditableImages**.
**Why software rasterization?** The alternatives don't fit Dear ImGui:
- *One GUI Instance per widget/shape* (what most Roblox UI libraries do) means re-creating, re-parenting or diffing thousands of Instances every frame, can't express anti-aliased curves, per-vertex gradients or font-atlas glyphs, and would no longer be Dear ImGui's renderer output.
- *Rasterizing the draw data* keeps the output identical to Dear ImGui's: the same fonts, anti-aliasing, rounding and gradients, and every widget (including custom `ImDrawList` drawing) works without backend support.
The cost is CPU time in Luau, so the backend is built around **not drawing**:
1. **Layers per window.** Every draw list (one per top-level window, popup, tooltip) gets its own `Frame` with `ZIndex` following Dear ImGui's draw order. Its content is rasterized relative to the window, so **moving a window only changes the Frame's `Position`**; no pixel is redrawn or uploaded.
2. **Tiles.** Each layer is covered by 128×128 EditableImage tiles (`ImageLabel`s). Every primitive is hashed (positions relative to the layer, UVs, colours, texture, clip) into the tiles it touches. **Only tiles whose hash changed are re-rasterized and uploaded**, and only the dirty region of those (`WritePixelsBuffer` with a sub-rectangle). A static UI uploads nothing.
3. **Scrolling reuses pixels.** Window content is a separate layer inside a `ClipsDescendants` frame sized to the window's visible area, positioned at the scroll offset. Scrolling moves it like dragging moves a window; only rows entering view are drawn. Decorations (title bar, borders, scrollbar) stay in their own layers.
4. **Skipping unchanged windows.** Vertices are stored in a flat Luau `buffer` (36 bytes each) instead of tables. Before any per-tile work, a window's raw vertex/index bytes are compared with last frame's; if identical, its layers are just restacked.
5. **Fast rasterizer.** `@native` functions; axis-aligned quads (most rects and every glyph) take a span-fill path, flat rows are copied instead of re-blended, gradient rects (color pickers) are filled per row, and only real triangles (anti-aliased edges, curves) use the generic edge-function rasterizer.
6. **Render rate.** Tiles update at most `SetRenderRate(hz)` times per second (default 60), optionally lower while scrolling/resizing (`SetBusyRenderRate`). Your UI code and Dear ImGui's logic still run every frame, so input is never dropped.
**Input.** Mouse and keys come from `UserInputService`. While the mouse is over Dear ImGui (`io.WantCaptureMouse`), the tiles are `Active`, so clicks and the wheel don't reach the game (camera zoom). Games can't read the clipboard, so while a text field is active a hidden `TextBox` holds focus: the OS delivers typing (any layout), paste and copy/cut through it, and its text mirrors Dear ImGui's current selection. Fonts are TTFs rasterized by the ported stb_truetype into the font atlas, which becomes a backend texture like any image (`CreateTexture` works for your own pixels too).
**Instances inside windows.** The backend can place your own Instances (e.g. a `ViewportFrame`) inside a window's layer, so they stack, clip and move with it; see the `ImGui.Embed` widget in [examples/example_roblox/main.client.luau](examples/example_roblox/main.client.luau).
### Demo ### Demo
Calling `ImGui.ShowDemoWindow()` creates a demo window that showcases a variety of features and examples. The code is in [imgui_demo.lua](imgui_demo.lua), like upstream's `imgui_demo.cpp`. **It's strongly recommended to keep it at hand as a reference**: you can find the code for any widget you see by searching for its label. Calling `ImGui.ShowDemoWindow()` creates a demo window that showcases a variety of features and examples. The code is in [imgui_demo.lua](imgui_demo.lua), like upstream's `imgui_demo.cpp`. **It's strongly recommended to keep it at hand as a reference**: you can find the code for any widget you see by searching for its label.
@@ -134,7 +113,9 @@ The Style Editor (`ImGui.ShowStyleEditor()`) and the Metrics/Debugger (`ImGui.Sh
### Getting Started & Integration (Roblox) ### Getting Started & Integration (Roblox)
1. Build the module: `python3 tools/bundle.py` writes `build/ImGui.luau` (`--no-demo` leaves the demo out). With [Wally](https://wally.run) ([4dbug/imgui-luau](https://wally.run/package/4dbug/imgui-luau)): add `ImGui = "4dbug/[email protected]"` under `[dependencies]` in your `wally.toml`, run `wally install`, then `require(ReplicatedStorage.Packages.ImGui)`. Or from source:
1. Build the module: `python3 tools/bundle.py` writes `build/ImGui.luau` (`--no-demo` leaves the demo out) and `build/ImGuiTyped.luau`, a small typed entry module (Luau types for the public `ImGui.*` API, `ImVec2`/`ImVec4` and the flag enums) that requires `build/ImGui.luau` as its child `Impl`. The Wally package and the Rojo projects use that layout, so you get autocomplete and type errors in `--!strict` scripts. The single-file `ImGui.luau` also works on its own, just untyped.
2. Sync with [Rojo](https://rojo.space): [`default.project.json`](default.project.json) puts the module in `ReplicatedStorage.ImGui` and the example LocalScript in `StarterPlayerScripts`. 2. Sync with [Rojo](https://rojo.space): [`default.project.json`](default.project.json) puts the module in `ReplicatedStorage.ImGui` and the example LocalScript in `StarterPlayerScripts`.
3. A minimal LocalScript (full version: [examples/example_roblox/main.client.luau](examples/example_roblox/main.client.luau)): 3. A minimal LocalScript (full version: [examples/example_roblox/main.client.luau](examples/example_roblox/main.client.luau)):
@@ -167,7 +148,8 @@ Backend options:
| --- | --- | | --- | --- |
| `ImGui_ImplRoblox.Init(parent, render_scale)` | `render_scale` = your OS display scale renders at physical-pixel resolution (crisper, same size as desktop imgui). | | `ImGui_ImplRoblox.Init(parent, render_scale)` | `render_scale` = your OS display scale renders at physical-pixel resolution (crisper, same size as desktop imgui). |
| `ImGui_ImplRoblox.SetRenderScale(scale)` | Change it at runtime. | | `ImGui_ImplRoblox.SetRenderScale(scale)` | Change it at runtime. |
| `ImGui_ImplRoblox.SetRenderRate(hz)` | Max tile redraws per second (default 60, `0` = every frame). Your code still runs every frame. | | `ImGui_ImplRoblox.SetRenderRate(hz)` | Max tile redraws per second (default `0` = every frame). Your code still runs every frame. |
| `ImGui_ImplRoblox.SetRedrawBudget(ms)` | Max milliseconds per frame spent redrawing changed tiles (opt-in; default `0` = no limit, every change is drawn the same frame). With a budget, tiles over it keep their old pixels for a few frames, so big changes are spread over frames instead of one long frame (parts of the UI may update a frame or two apart). |
| `ImGui_ImplRoblox.SetBusyRenderRate(hz)` | Optional lower redraw rate while scrolling, resizing or moving windows (default off). | | `ImGui_ImplRoblox.SetBusyRenderRate(hz)` | Optional lower redraw rate while scrolling, resizing or moving windows (default off). |
| `ImGui_ImplRoblox.SetProfiling(true)` | MicroProfiler (Ctrl+F6) labels for every backend stage. | | `ImGui_ImplRoblox.SetProfiling(true)` | MicroProfiler (Ctrl+F6) labels for every backend stage. |
| `ImGui_ImplRoblox.AddFile(path, bytes)` | Register a file (e.g. a `.ttf`) so `io.Fonts:AddFontFromFileTTF(path, size)` can load it. | | `ImGui_ImplRoblox.AddFile(path, bytes)` | Register a file (e.g. a `.ttf`) so `io.Fonts:AddFontFromFileTTF(path, size)` can load it. |
@@ -179,6 +161,27 @@ io.Fonts:AddFontDefault()
io.Fonts:AddFontFromFileTTF("fonts/Roboto-Medium.ttf", 16.0) io.Fonts:AddFontFromFileTTF("fonts/Roboto-Medium.ttf", 16.0)
``` ```
### How it works (Roblox backend)
Dear ImGui itself never draws anything: every frame it produces `ImDrawData`, a list of textured, coloured triangles plus clip rectangles, and expects the backend to put them on screen. On desktop that is a few GPU draw calls. Roblox has no API to draw arbitrary triangles in 2D, so the Roblox backend ([backends/imgui_impl_roblox.lua](backends/imgui_impl_roblox.lua)) does what a GPU would do, in Luau, and pushes the result into **EditableImages**.
**Why software rasterization?** The alternatives don't fit Dear ImGui:
- *One GUI Instance per widget/shape* (what most Roblox UI libraries do) means re-creating, re-parenting or diffing thousands of Instances every frame, can't express anti-aliased curves, per-vertex gradients or font-atlas glyphs, and would no longer be Dear ImGui's renderer output.
- *Rasterizing the draw data* keeps the output identical to Dear ImGui's: the same fonts, anti-aliasing, rounding and gradients, and every widget (including custom `ImDrawList` drawing) works without backend support.
The cost is CPU time in Luau, so the backend is built around **not drawing**:
1. **Layers per window.** Every draw list (one per top-level window, popup, tooltip) gets its own `Frame` with `ZIndex` following Dear ImGui's draw order. Its content is rasterized relative to the window, so **moving a window only changes the Frame's `Position`**; no pixel is redrawn or uploaded.
2. **Tiles.** Each layer is covered by 128×128 EditableImage tiles (`ImageLabel`s). Every primitive is hashed (positions relative to the layer, UVs, colours, texture, clip) into the tiles it touches. **Only tiles whose hash changed are re-rasterized and uploaded**, and only the dirty region of those (`WritePixelsBuffer` with a sub-rectangle). A static UI uploads nothing.
3. **Scrolling reuses pixels.** Window content is a separate layer inside a `ClipsDescendants` frame sized to the window's visible area, positioned at the scroll offset. Scrolling moves it like dragging moves a window; only rows entering view are drawn. Decorations (title bar, borders, scrollbar) stay in their own layers.
4. **Skipping unchanged windows.** Vertices are stored in a flat Luau `buffer` (36 bytes each) instead of tables. Before any per-tile work, a window's raw vertex/index bytes are compared with last frame's; if identical, its layers are just restacked.
5. **Fast rasterizer.** `@native` functions; axis-aligned quads (most rects and every glyph) take a span-fill path, flat rows are copied instead of re-blended, gradient rects (color pickers) are filled per row, and only real triangles (anti-aliased edges, curves) use the generic edge-function rasterizer.
6. **Redraw budget.** Changed tiles are redrawn in the frame they change. Optionally, `SetRedrawBudget(ms)` caps the redraw time per frame; tiles over it keep their old pixels and follow on the next frames. Optionally capped with `SetRenderRate(hz)` / `SetBusyRenderRate(hz)`. Your UI code and Dear ImGui's logic still run every frame, so input is never dropped.
**Input.** Mouse and keys come from `UserInputService`. While the mouse is over Dear ImGui (`io.WantCaptureMouse`), the tiles are `Active`, so clicks and the wheel don't reach the game (camera zoom). Typing comes from key codes by default (printable ASCII). Call `ImGui_ImplRoblox.SetTextProxy(true)` to opt in to a hidden `TextBox` that holds focus while a text field is active: the OS then delivers typing (any layout), paste and copy/cut through it, and its text mirrors Dear ImGui's current selection. Fonts are TTFs rasterized by the ported stb_truetype into the font atlas, which becomes a backend texture like any image (`CreateTexture` works for your own pixels too).
**Instances inside windows.** The backend can place your own Instances (e.g. a `ViewportFrame`) inside a window's layer, so they stack, clip and move with it; see the `ImGui.Embed` widget in [examples/example_roblox/main.client.luau](examples/example_roblox/main.client.luau).
### Backends ### Backends
| Backend | File | Build | Renderer | Example | | Backend | File | Build | Renderer | Example |
@@ -239,6 +242,20 @@ Measured headless (Luau CLI, Studio-like native code generation), demo window op
Most of the remaining cost is widget logic (your UI code and Dear ImGui's per-widget work), which runs every frame by design. Use `ImGuiListClipper` for long lists. Most of the remaining cost is widget logic (your UI code and Dear ImGui's per-widget work), which runs every frame by design. Use `ImGuiListClipper` for long lists.
Windows that rarely change can opt in to caching: `ImGui.SetNextWindowCached(key)` keeps last frame's drawing and makes `Begin` return `visible = false`, so your code for that window doesn't run. It refreshes when `key` changes, while the window is hovered, focused or active (plus one frame after), and when it appears, moves, resizes, scrolls, collapses or is docked. An idle 200-widget window goes from ~2.2 ms to ~0.15 ms per frame.
```lua
local stats = { version = 0, kills = 0 }
local function StatsWindow()
ImGui.SetNextWindowCached(stats.version) -- bump stats.version whenever stats change
local _, visible = ImGui.Begin("Stats")
if visible then -- false while the cached contents are reused
ImGui.Text("Kills: %d", stats.kills)
end
ImGui.End()
end
```
### Testing ### Testing
`tools/test/run.py` runs any LocalScript headless against a mocked Roblox (`tools/test/roblox_mock.luau`): `tools/test/run.py` runs any LocalScript headless against a mocked Roblox (`tools/test/roblox_mock.luau`):
@@ -280,7 +297,7 @@ Multi-select / box-select, typing-select, the Assets Browser example, the Item P
### AI disclosure ### AI disclosure
AI tools were used to assist in making this project (porting code, the Roblox and LÖVE backends, optimizations, tests and documentation). All changes were reviewed and tested by a human, and correctness is checked by the test suite (draw-data checksums against the reference behaviour, pixel comparisons, behaviour tests). Bug reports are welcome. AI tools were used to assist in making this project (porting code, optimizations, tests and documentation). All changes were reviewed and tested by a human, and correctness is checked by the test suite (draw-data checksums against the reference behaviour, pixel comparisons, behaviour tests). Bug reports are welcome.
### Credits ### Credits
+394 -78
View File
@@ -142,6 +142,7 @@ end
--------------------------------------------------------- ---------------------------------------------------------
-- Copies a region of ImTextureData pixels (1-based byte table) into our RGBA8 buffer -- Copies a region of ImTextureData pixels (1-based byte table) into our RGBA8 buffer
@native
local function CopyTexturePixels(tex, dst, x, y, w, h) local function CopyTexturePixels(tex, dst, x, y, w, h)
local bpp, tex_w = tex.BytesPerPixel, tex.Width local bpp, tex_w = tex.BytesPerPixel, tex.Width
for row = 0, h - 1 do for row = 0, h - 1 do
@@ -208,6 +209,8 @@ end
-- Blending is non-premultiplied "over" with a transparent (alpha 0) background, so the game stays visible behind ImGui. -- Blending is non-premultiplied "over" with a transparent (alpha 0) background, so the game stays visible behind ImGui.
local buf, buf_w -- buffer/width of the tile being drawn local buf, buf_w -- buffer/width of the tile being drawn
local rect_cols = table.create(256, 0) -- scratch: texel column offsets for DrawRect
local sub_bufs, n_sub_bufs = {}, 0 -- scratch: partial tile upload buffers by size
local TILE_SIZE -- Vector2, set below local TILE_SIZE -- Vector2, set below
-- Blends one pixel (r, g, b, a are 0..255). One u32 read/write, one division in the general case. -- Blends one pixel (r, g, b, a are 0..255). One u32 read/write, one division in the general case.
@@ -337,18 +340,30 @@ local function DrawRect(x0, y0, x1, y1, u0, v0, u1, v1, col, t, cx0, cy0, cx1, c
local du, dv = (u1 - u0) / (x1 - x0), (v1 - v0) / (y1 - y0) local du, dv = (u1 - u0) / (x1 - x0), (v1 - v0) / (y1 - y0)
local tw, th, tbuf = t.Width, t.Height, t.Buffer local tw, th, tbuf = t.Width, t.Height, t.Buffer
local r255, g255, b255, a255 = r / 255, g / 255, b / 255, a / 255 local r255, g255, b255, a255 = r / 255, g / 255, b / 255, a / 255
-- The texel column only depends on x: compute it once per column instead of once per pixel
local ncol = px1 - px0
local cols = rect_cols
for c = 1, ncol do
local tx = floor((u0 + (px0 + c - 1 + 0.5 - x0) * du) * tw)
if tx < 0 then tx = 0 elseif tx >= tw then tx = tw - 1 end
cols[c] = tx * 4
end
for y = py0, py1 - 1 do for y = py0, py1 - 1 do
local ty = floor((v0 + (y + 0.5 - y0) * dv) * th) local ty = floor((v0 + (y + 0.5 - y0) * dv) * th)
if ty < 0 then ty = 0 elseif ty >= th then ty = th - 1 end if ty < 0 then ty = 0 elseif ty >= th then ty = th - 1 end
local trow = ty * tw local trow4 = ty * tw * 4
local o = (y * buf_w + px0) * 4 local o = (y * buf_w + px0) * 4
for x = px0, px1 - 1 do for c = 1, ncol do
local tx = floor((u0 + (x + 0.5 - x0) * du) * tw) local to = trow4 + cols[c]
if tx < 0 then tx = 0 elseif tx >= tw then tx = tw - 1 end
local to = (trow + tx) * 4
local ta = readu8(tbuf, to + 3) local ta = readu8(tbuf, to + 3)
if ta > 0 then if ta > 0 then
BlendPixel(o, readu8(tbuf, to) * r255, readu8(tbuf, to + 1) * g255, readu8(tbuf, to + 2) * b255, ta * a255) local sa = ta * a255
local sr, sg, sb = readu8(tbuf, to) * r255, readu8(tbuf, to + 1) * g255, readu8(tbuf, to + 2) * b255
if sa >= 255 then -- (BlendPixel's opaque case, inlined: most glyph texels)
writeu32(buf, o, floor(sr + 0.5) + floor(sg + 0.5) * 256 + floor(sb + 0.5) * 65536 + 4278190080)
else
BlendPixel(o, sr, sg, sb, sa)
end
end end
o = o + 4 o = o + 4
end end
@@ -442,6 +457,7 @@ local P1, P2 = 2147483647, 2147483629 -- two independent 31 bit hashes per tile
local KEY_W = 64 -- tile key = ty * KEY_W + tx (layers up to 16384 px wide) local KEY_W = 64 -- tile key = ty * KEY_W + tx (layers up to 16384 px wide)
local CLIP_PAD = 2048 -- content up to this far off screen is kept, so dragging a window past the edge doesn't redraw it local CLIP_PAD = 2048 -- content up to this far off screen is kept, so dragging a window past the edge doesn't redraw it
local LAYER_KEEP_FRAMES = 120 -- unused layers are destroyed after this many rendered frames local LAYER_KEEP_FRAMES = 120 -- unused layers are destroyed after this many rendered frames
local MAX_DEFER_FRAMES = 8 -- a tile is redrawn at the latest this many frames after it changed, even over the redraw budget
-- Primitive arrays (reused between frames, no per-frame allocation) -- Primitive arrays (reused between frames, no per-frame allocation)
local pr_a, pr_b, pr_c, pr_d, pr_t, pr_g = {}, {}, {}, {}, {}, {} -- vertices (pr_d == false -> triangle), texture, gradient kind local pr_a, pr_b, pr_c, pr_d, pr_t, pr_g = {}, {}, {}, {}, {}, {} -- vertices (pr_d == false -> triangle), texture, gradient kind
@@ -451,6 +467,7 @@ local pr_tex = {} -- texture id
local pr_vb = {} -- vertex buffer (draw list VtxBuffer.Buf) local pr_vb = {} -- vertex buffer (draw list VtxBuffer.Buf)
local pr_k = {} -- layer kind (1..3) local pr_k = {} -- layer kind (1..3)
local plist1, plist2, plist3 = {}, {}, {} -- primitive indices per layer kind local plist1, plist2, plist3 = {}, {}, {} -- primitive indices per layer kind
local cm_free, cm_kind, cm_x0, cm_y0, cm_x1, cm_y1 = {}, {}, {}, {}, {}, {} -- per draw command (collect -> Snapshot)
@native @native
local function HashVertex(h1, h2, vbuf, v, ox, oy) local function HashVertex(h1, h2, vbuf, v, ox, oy)
@@ -540,7 +557,7 @@ local function GetLayer(bd, draw_list, kind)
else else
frame = NewFrame(bd.Gui, false); root = frame frame = NewFrame(bd.Gui, false); root = frame
end end
layer = { Root = root, Frame = frame, Tiles = {}, TH1 = {}, TH2 = {}, TN = {}, TL = {}, TX = {}, TY = {}, Touched = {}, NTouched = 0, layer = { Root = root, Frame = frame, Tiles = {}, TH1 = {}, TH2 = {}, TN = {}, TL = {}, TL1 = {}, TL2 = {}, TLF = {}, TX = {}, TY = {}, Touched = {}, NTouched = 0,
Visible = false, X = false, Y = false, Z = -1, CX0 = false, CY0 = false, CX1 = false, CY1 = false } Visible = false, X = false, Y = false, Z = -1, CX0 = false, CY0 = false, CX1 = false, CY1 = false }
set[kind] = layer set[kind] = layer
return layer return layer
@@ -567,7 +584,8 @@ local function GetTile(bd, layer, key, tx, ty)
label.Visible = false label.Visible = false
label.Active = bd.TilesActive label.Active = bd.TilesActive
label.Parent = layer.Frame label.Parent = layer.Frame
tile = { Image = img, Label = label, Buffer = buffer.create(TILE * TILE * 4), H1 = -1, H2 = -1, Visible = false, BX0 = TILE, BY0 = TILE, BX1 = 0, BY1 = 0 } tile = { Image = img, Label = label, Buffer = buffer.create(TILE * TILE * 4), H1 = -1, H2 = -1, Visible = false, BX0 = TILE, BY0 = TILE, BX1 = 0, BY1 = 0,
ON = -1, OH1 = {}, OH2 = {}, OF = {}, OB0 = {}, OB1 = {}, OB2 = {}, OB3 = {} } -- last drawn primitive list (hashes, bounds in tile)
layer.Tiles[key] = tile layer.Tiles[key] = tile
return tile return tile
end end
@@ -587,31 +605,129 @@ local function DestroyTiles(bd)
bd.Layers = {} bd.Layers = {}
end end
-- Exact signature of everything in a draw list that affects its layers (nil = can't skip: user callbacks) -- Snapshot of everything in a draw list that affects its layers, compared exactly with the next frame's list:
local sig_buf = buffer.create(4096) -- 1 = identical (only restack), 2 = everything moved by the same whole pixels (dx, dy) (only move the layers: a window
-- being dragged), 0 = changed (full path). Compared in place (no strings built): early exit at the first difference.
local writef64 = buffer.writef64
local CMD_STRIDE = 70 -- clip (4 f64), ElemCount, IdxOffset, VtxOffset (u32), tex id (f64), kind (u8), free (u8), bounds of its primitives (4 x i32)
@native @native
local function DrawListSignature(draw_list, ox, oy) local function Snapshot(set, draw_list, ox, oy, sw, sh)
local nv = draw_list.VtxBuffer.Size * VTX_STRIDE
local idx, ni = draw_list.IdxBuffer.Data, draw_list.IdxBuffer.Size local idx, ni = draw_list.IdxBuffer.Data, draw_list.IdxBuffer.Size
local ncmd = draw_list.CmdBuffer.Size local ncmd = draw_list.CmdBuffer.Size
local need = ni * 4 + ncmd * 64 + 64 local sv, si, sc = set.SV, set.SI, set.SC
if buffer.len(sig_buf) < need then sig_buf = buffer.create(need * 2) end if not sv or buffer.len(sv) < nv then sv = buffer.create(max(nv, 64) * 2); set.SV = sv end
local b, o = sig_buf, 0 if not si or buffer.len(si) < ni * 4 then si = buffer.create(max(ni, 16) * 8); set.SI = si end
for i = 1, ni do writeu32(b, o, idx[i]); o += 4 end if not sc or buffer.len(sc) < ncmd * CMD_STRIDE then sc = buffer.create(max(ncmd, 4) * CMD_STRIDE * 2); set.SC = sc end
buffer.copy(sv, 0, draw_list.VtxBuffer.Buf, 0, nv)
for i = 1, ni do writeu32(si, (i - 1) * 4, idx[i]) end
local o, ci = 0, 0
for _, pcmd in draw_list.CmdBuffer:iter() do for _, pcmd in draw_list.CmdBuffer:iter() do
if pcmd.UserCallback ~= nil then return nil end if pcmd.UserCallback ~= nil then set.SnapOK = false; return end
local c = pcmd.ClipRect local c = pcmd.ClipRect
buffer.writef64(b, o, c.x); buffer.writef64(b, o + 8, c.y); buffer.writef64(b, o + 16, c.z); buffer.writef64(b, o + 24, c.w) writef64(sc, o, c.x); writef64(sc, o + 8, c.y); writef64(sc, o + 16, c.z); writef64(sc, o + 24, c.w)
writeu32(b, o + 32, pcmd.ElemCount); writeu32(b, o + 36, pcmd.IdxOffset); writeu32(b, o + 40, pcmd.VtxOffset) writeu32(sc, o + 32, pcmd.ElemCount); writeu32(sc, o + 36, pcmd.IdxOffset); writeu32(sc, o + 40, pcmd.VtxOffset)
buffer.writef64(b, o + 44, pcmd:GetTexID()) writef64(sc, o + 44, pcmd:GetTexID())
o += 52 ci += 1
buffer.writeu8(sc, o + 52, cm_kind[ci] or 0); buffer.writeu8(sc, o + 53, cm_free[ci] and 1 or 0)
if cm_free[ci] then
buffer.writei32(sc, o + 54, cm_x0[ci]); buffer.writei32(sc, o + 58, cm_y0[ci]); buffer.writei32(sc, o + 62, cm_x1[ci]); buffer.writei32(sc, o + 66, cm_y1[ci])
end
o += CMD_STRIDE
end
set.SVN, set.SIN, set.SCN = nv, ni, ncmd
set.SOX, set.SOY, set.SSW, set.SSH = ox, oy, sw, sh
set.SCC = draw_list._ContentClipX0 ~= nil
set.SCX0, set.SCY0, set.SCX1, set.SCY1 = draw_list._ContentClipX0 or 0, draw_list._ContentClipY0 or 0, draw_list._ContentClipX1 or 0, draw_list._ContentClipY1 or 0
set.SSX, set.SSY = draw_list._ScrollOriginX or 0, draw_list._ScrollOriginY or 0
set.SnapOK = true
end
-- a + d == b exactly, with no rounding in a + d (so every later "x - origin" gives the same result as before the move)
@native
local function ShiftedExact(a, d, b)
local s = a + d
if s ~= b then return false end
local bb = s - a
return (a - (s - bb)) + (d - bb) == 0
end
@native
local function CompareDrawList(set, draw_list, ox, oy, sw, sh)
if not set or not set.SnapOK then return 0 end
local nv = draw_list.VtxBuffer.Size * VTX_STRIDE
local ni, ncmd = draw_list.IdxBuffer.Size, draw_list.CmdBuffer.Size
if nv ~= set.SVN or ni ~= set.SIN or ncmd ~= set.SCN or ox ~= set.SOX or oy ~= set.SOY or sw ~= set.SSW or sh ~= set.SSH then return 0 end
if (draw_list._ContentClipX0 ~= nil) ~= set.SCC then return 0 end
local vb, sv = draw_list.VtxBuffer.Buf, set.SV
if nv == 0 then return 1 end
-- translation from the first vertex: whole pixels only, and only with DisplayPos at 0 (so it stays exact)
local dx, dy = readf64(vb, 0) - readf64(sv, 0), readf64(vb, 8) - readf64(sv, 8)
if dx ~= dx or dy ~= dy then return 0 end
if dx ~= 0 or dy ~= 0 then
if dx ~= floor(dx) or dy ~= floor(dy) or ox ~= 0 or oy ~= 0 or not set.SLimFree then return 0 end
-- everything must stay as far from the clip limits as before (nothing newly clamped)
if set.SEX0 + dx < -CLIP_PAD or set.SEY0 + dy < -CLIP_PAD or set.SEX1 + dx > sw + CLIP_PAD or set.SEY1 + dy > sh + CLIP_PAD then return 0 end
end
local idx, si = draw_list.IdxBuffer.Data, set.SI
for i = 1, ni do if idx[i] ~= readu32(si, (i - 1) * 4) then return 0 end end
local sc, o = set.SC, 0
local ccx0, ccy0, ccx1, ccy1 = draw_list._ContentClipX0, draw_list._ContentClipY0, draw_list._ContentClipX1, draw_list._ContentClipY1
local seen_content = false
for _, pcmd in draw_list.CmdBuffer:iter() do
if pcmd.UserCallback ~= nil then return 0 end
if pcmd.ElemCount ~= readu32(sc, o + 32) or pcmd.IdxOffset ~= readu32(sc, o + 36) or pcmd.VtxOffset ~= readu32(sc, o + 40) or pcmd:GetTexID() ~= readf64(sc, o + 44) then return 0 end
local c = pcmd.ClipRect
if dx == 0 and dy == 0 then
if c.x ~= readf64(sc, o) or c.y ~= readf64(sc, o + 8) or c.z ~= readf64(sc, o + 16) or c.w ~= readf64(sc, o + 24) then return 0 end
elseif not (ShiftedExact(readf64(sc, o), dx, c.x) and ShiftedExact(readf64(sc, o + 8), dy, c.y)
and ShiftedExact(readf64(sc, o + 16), dx, c.z) and ShiftedExact(readf64(sc, o + 24), dy, c.w)) then
-- An unchanged clip rect (e.g. the viewport) is fine if it clipped none of this command's primitives before
-- the move and still clips none after it
if c.x ~= readf64(sc, o) or c.y ~= readf64(sc, o + 8) or c.z ~= readf64(sc, o + 16) or c.w ~= readf64(sc, o + 24)
or buffer.readu8(sc, o + 53) == 0 then return 0 end
if buffer.readi32(sc, o + 54) + dx < floor(c.x) or buffer.readi32(sc, o + 58) + dy < floor(c.y)
or buffer.readi32(sc, o + 62) + dx > floor(c.z) or buffer.readi32(sc, o + 66) + dy > floor(c.w) then return 0 end
end
-- same layer kind as before (same rule as the collect pass)
if pcmd.ElemCount > 0 then
local kind = 1
if ccx0 and c.x >= ccx0 and c.y >= ccy0 and c.z <= ccx1 and c.w <= ccy1 then kind = 2; seen_content = true
elseif seen_content then kind = 3 end
if kind ~= buffer.readu8(sc, o + 52) then return 0 end
end
o += CMD_STRIDE
end
if dx == 0 and dy == 0 then
if set.SCC and (draw_list._ContentClipX0 ~= set.SCX0 or draw_list._ContentClipY0 ~= set.SCY0 or draw_list._ContentClipX1 ~= set.SCX1
or draw_list._ContentClipY1 ~= set.SCY1 or draw_list._ScrollOriginX ~= set.SSX or draw_list._ScrollOriginY ~= set.SSY) then return 0 end
for v = 0, nv - VTX_STRIDE, VTX_STRIDE do
if readf64(vb, v) ~= readf64(sv, v) or readf64(vb, v + 8) ~= readf64(sv, v + 8) or readf64(vb, v + 16) ~= readf64(sv, v + 16)
or readf64(vb, v + 24) ~= readf64(sv, v + 24) or readu32(vb, v + 32) ~= readu32(sv, v + 32) then return 0 end
end
return 1
end
if set.SCC and not (ShiftedExact(set.SCX0, dx, draw_list._ContentClipX0) and ShiftedExact(set.SCY0, dy, draw_list._ContentClipY0)
and ShiftedExact(set.SCX1, dx, draw_list._ContentClipX1) and ShiftedExact(set.SCY1, dy, draw_list._ContentClipY1)
and ShiftedExact(set.SSX, dx, draw_list._ScrollOriginX) and ShiftedExact(set.SSY, dy, draw_list._ScrollOriginY)) then return 0 end
for v = 0, nv - VTX_STRIDE, VTX_STRIDE do
if not ShiftedExact(readf64(sv, v), dx, readf64(vb, v)) or not ShiftedExact(readf64(sv, v + 8), dy, readf64(vb, v + 8))
or readf64(vb, v + 16) ~= readf64(sv, v + 16) or readf64(vb, v + 24) ~= readf64(sv, v + 24) or readu32(vb, v + 32) ~= readu32(sv, v + 32) then return 0 end
end
return 2, dx, dy
end
-- A window moved by whole pixels: its tiles are unchanged, only the layer frames move
local function ShiftLayer(layer, dx, dy, scale)
layer.X += dx; layer.Y += dy
local root = layer.Root
if root ~= layer.Frame then
layer.CX0 += dx; layer.CY0 += dy; layer.CX1 += dx; layer.CY1 += dy
root.Position = UDim2.fromOffset(layer.CX0 / scale, layer.CY0 / scale)
else
root.Position = UDim2.fromOffset(layer.X / scale, layer.Y / scale)
end end
buffer.writef64(b, o, ox); buffer.writef64(b, o + 8, oy)
buffer.writef64(b, o + 16, draw_list._ContentClipX0 or -1); buffer.writef64(b, o + 24, draw_list._ContentClipY0 or -1)
buffer.writef64(b, o + 32, draw_list._ContentClipX1 or -1); buffer.writef64(b, o + 40, draw_list._ContentClipY1 or -1)
buffer.writef64(b, o + 48, draw_list._ContentClipX0 and draw_list._ScrollOriginX or -1)
buffer.writef64(b, o + 56, draw_list._ContentClipX0 and draw_list._ScrollOriginY or -1)
o += 64
return readstring(draw_list.VtxBuffer.Buf, 0, draw_list.VtxBuffer.Size * VTX_STRIDE) .. readstring(b, 0, o)
end end
-- Overlays: user Instances (e.g. a Frame holding a ViewportFrame) placed inside a window, registered every frame with -- Overlays: user Instances (e.g. a Frame holding a ViewportFrame) placed inside a window, registered every frame with
@@ -690,9 +806,11 @@ local function PE() if profiling then profileend() end end
@native @native
local function DrawLayer(bd, layer, plist, np, lx0, ly0, ox, oy, force, z, clip_x0, clip_y0, clip_x1, clip_y1) local function DrawLayer(bd, layer, plist, np, lx0, ly0, ox, oy, force, z, clip_x0, clip_y0, clip_x1, clip_y1)
local scale = bd.Scale local scale = bd.Scale
local deferred = false
-- Pass 2: hash primitives into layer-relative tiles -- Pass 2: hash primitives into layer-relative tiles
PB("ImGui tile hash") PB("ImGui tile hash")
local th1, th2, tn, tl, touched = layer.TH1, layer.TH2, layer.TN, layer.TL, layer.Touched local th1, th2, tn, tl, touched = layer.TH1, layer.TH2, layer.TN, layer.TL, layer.Touched
local tl1, tl2, tlf = layer.TL1, layer.TL2, layer.TLF -- per-tile list of the primitives' hashes (for partial redraws)
for k = 1, layer.NTouched do tn[touched[k]] = 0 end for k = 1, layer.NTouched do tn[touched[k]] = 0 end
local nt = 0 local nt = 0
for pi = 1, np do for pi = 1, np do
@@ -701,23 +819,55 @@ local function DrawLayer(bd, layer, plist, np, lx0, ly0, ox, oy, force, z, clip_
-- The clip rect only affects pixels inside the primitive's bounds, so hash the clipped bounds (relative) -- The clip rect only affects pixels inside the primitive's bounds, so hash the clipped bounds (relative)
-- instead of the clip itself: a window clipped by the (fixed) viewport rect stays hash-stable when moved. -- instead of the clip itself: a window clipped by the (fixed) viewport rect stays hash-stable when moved.
local cx0, cy0, cx1, cy1 = pr_bx0[p] - lx0, pr_by0[p] - ly0, pr_bx1[p] - lx0, pr_by1[p] - ly0 local cx0, cy0, cx1, cy1 = pr_bx0[p] - lx0, pr_by0[p] - ly0, pr_bx1[p] - lx0, pr_by1[p] - ly0
local h1 = ((cx0 * 8191 + cy0) * 8191 + cx1) % P1
local h2 = ((cy1 * 8191 + cx1) * 8191 + cy0 + tex_id) % P2
local vax, vay = lx0 + ox, ly0 + oy
local vbuf = pr_vb[p] local vbuf = pr_vb[p]
h1, h2 = HashVertex(h1, h2, vbuf, pr_a[p], vax, vay) local va, vd = pr_a[p], pr_d[p]
-- Flat rect (single colour, no gradient, constant uv): its pixels in a tile are exactly its clipped pixel rect
-- inside that tile filled with one colour, so hash that per tile instead of the vertices. Resizing a window
-- then only changes the tiles its edges cross, not every tile its background covers.
local flat_rect, rcol, ruv = false, 0, 0
if vd and not pr_g[p] then
local u0, v0 = readf64(vbuf, va + 16), readf64(vbuf, va + 24)
local vc = pr_c[p]
if not pr_t[p] or (u0 == readf64(vbuf, vc + 16) and v0 == readf64(vbuf, vc + 24)) then
flat_rect, rcol, ruv = true, readu32(vbuf, va + 32), u0 * 8192 + v0
end
end
local h1, h2 = 0, 0
local fsig = -1 -- flat rect: colour/uv/texture without the geometry (-1: not a flat rect)
if flat_rect then fsig = ((rcol * 8191 + ruv * 4096) % P1 * 64 + tex_id) % P1 end
if not flat_rect then
h1 = ((cx0 * 8191 + cy0) * 8191 + cx1) % P1
h2 = ((cy1 * 8191 + cx1) * 8191 + cy0 + tex_id) % P2
local vax, vay = lx0 + ox, ly0 + oy
if vd then
-- Axis aligned quad: the collect pass checked that TR/BL (positions, uvs, colours) follow from TL/BR
-- (and the gradient kind), so TL + BR + kind determine its pixels
h1, h2 = HashVertex(h1, h2, vbuf, va, vax, vay)
h1, h2 = HashVertex(h1, h2, vbuf, pr_c[p], vax, vay)
local gk = pr_g[p]
if gk then h1 = (h1 * 48271 + gk) % P1; h2 = (h2 * 16807 + gk * 3) % P2 end
else
h1, h2 = HashVertex(h1, h2, vbuf, va, vax, vay)
h1, h2 = HashVertex(h1, h2, vbuf, pr_b[p], vax, vay) h1, h2 = HashVertex(h1, h2, vbuf, pr_b[p], vax, vay)
h1, h2 = HashVertex(h1, h2, vbuf, pr_c[p], vax, vay) h1, h2 = HashVertex(h1, h2, vbuf, pr_c[p], vax, vay)
local vd = pr_d[p] end
if vd then h1, h2 = HashVertex(h1, h2, vbuf, vd, vax, vay) end end
for ty = floor((pr_by0[p] - ly0) / TILE), floor((pr_by1[p] - 1 - ly0) / TILE) do for ty = floor((pr_by0[p] - ly0) / TILE), floor((pr_by1[p] - 1 - ly0) / TILE) do
for tx = floor((pr_bx0[p] - lx0) / TILE), floor((pr_bx1[p] - 1 - lx0) / TILE) do for tx = floor((pr_bx0[p] - lx0) / TILE), floor((pr_bx1[p] - 1 - lx0) / TILE) do
if flat_rect then
local tx0, ty0 = tx * TILE, ty * TILE
local ix0, iy0 = max(cx0 - tx0, 0), max(cy0 - ty0, 0)
local ix1, iy1 = min(cx1 - tx0, TILE), min(cy1 - ty0, TILE)
local r = ((ix0 * 131 + iy0) * 131 + ix1) * 131 + iy1
h1 = ((r * 48271 + rcol) % P1 * 48271 + ruv * 4096 + 7) % P1
h2 = ((r * 16807 + rcol + tex_id) % P2 * 16807 + ruv * 65521 + 11) % P2
end
local key = ty * KEY_W + tx local key = ty * KEY_W + tx
local k = tn[key] local k = tn[key]
if not k or k == 0 then if not k or k == 0 then
k = 0 k = 0
th1[key], th2[key] = 0, 0 th1[key], th2[key] = 0, 0
if not tl[key] then tl[key] = {} end if not tl[key] then tl[key] = {}; tl1[key] = {}; tl2[key] = {}; tlf[key] = {} end
nt = nt + 1 nt = nt + 1
touched[nt] = key touched[nt] = key
layer.TX[key], layer.TY[key] = tx, ty layer.TX[key], layer.TY[key] = tx, ty
@@ -725,6 +875,7 @@ local function DrawLayer(bd, layer, plist, np, lx0, ly0, ox, oy, force, z, clip_
k = k + 1 k = k + 1
tn[key] = k tn[key] = k
tl[key][k] = p tl[key][k] = p
tl1[key][k], tl2[key][k], tlf[key][k] = h1, h2, fsig
th1[key] = (th1[key] * 48271 + h1) % P1 th1[key] = (th1[key] * 48271 + h1) % P1
th2[key] = (th2[key] * 16807 + h2 + k) % P2 th2[key] = (th2[key] * 16807 + h2 + k) % P2
end end
@@ -758,7 +909,7 @@ local function DrawLayer(bd, layer, plist, np, lx0, ly0, ox, oy, force, z, clip_
PB("ImGui raster+upload") PB("ImGui raster+upload")
for key, tile in pairs(layer.Tiles) do for key, tile in pairs(layer.Tiles) do
if not tn[key] or tn[key] == 0 then if not tn[key] or tn[key] == 0 then
if tile.Visible then tile.Visible = false; tile.Label.Visible = false; tile.H1 = -1 end if tile.Visible then tile.Visible = false; tile.Label.Visible = false; tile.H1 = -1; tile.ON = -1 end
end end
end end
for kk = 1, nt do for kk = 1, nt do
@@ -766,14 +917,27 @@ local function DrawLayer(bd, layer, plist, np, lx0, ly0, ox, oy, force, z, clip_
local count = tn[key] local count = tn[key]
local tx, ty = layer.TX[key], layer.TY[key] local tx, ty = layer.TX[key], layer.TY[key]
local tile = GetTile(bd, layer, key, tx, ty) local tile = GetTile(bd, layer, key, tx, ty)
if force or tile.H1 ~= th1[key] or tile.H2 ~= th2[key] then local dirty = force or tile.H1 ~= th1[key] or tile.H2 ~= th2[key]
if dirty and bd.RedrawStarted and (tile.Deferred or 0) < MAX_DEFER_FRAMES
and ((bd.RedrawBudget > 0 and os.clock() - bd.FrameT0 >= bd.RedrawBudget) or (bd.TileLimit and bd.FrameTiles >= bd.TileLimit)) then -- (TileLimit: test hook, tools/test/scripts/t_pixels.luau)
-- Over this frame's redraw budget: keep showing the old pixels, redraw on a following frame. The tile's
-- stored hashes/list still describe what it shows, so the next comparison is against the real content.
tile.Deferred = (tile.Deferred or 0) + 1
if force then tile.H1 = -1; tile.ON = -1 end -- full redraw later (force doesn't persist)
deferred = true
dirty = false
end
if dirty then
bd.RedrawStarted = true
bd.FrameTiles += 1
tile.Deferred = 0
tile.H1, tile.H2 = th1[key], th2[key] tile.H1, tile.H2 = th1[key], th2[key]
local x0, y0 = lx0 + tx * TILE, ly0 + ty * TILE local x0, y0 = lx0 + tx * TILE, ly0 + ty * TILE
local x1, y1 = x0 + TILE, y0 + TILE local x1, y1 = x0 + TILE, y0 + TILE
buf, buf_w = tile.Buffer, TILE buf, buf_w = tile.Buffer, TILE
local list = tl[key] local list, lh1, lh2 = tl[key], tl1[key], tl2[key]
-- Dirty region = old content bounds + new content bounds. Outside it the tile is already local oh1, oh2, ob0, ob1, ob2, ob3, on = tile.OH1, tile.OH2, tile.OB0, tile.OB1, tile.OB2, tile.OB3, tile.ON
-- transparent, so only this region is cleared and uploaded (edge tiles are often mostly empty). -- New content bounds (union of the primitives' bounds in this tile)
local nx0, ny0, nx1, ny1 = TILE, TILE, 0, 0 local nx0, ny0, nx1, ny1 = TILE, TILE, 0, 0
for k = 1, count do for k = 1, count do
local p = list[k] local p = list[k]
@@ -785,8 +949,63 @@ local function DrawLayer(bd, layer, plist, np, lx0, ly0, ox, oy, force, z, clip_
if d > ny1 then ny1 = d end if d > ny1 then ny1 = d end
end end
nx0, ny0, nx1, ny1 = max(nx0, 0), max(ny0, 0), min(nx1, TILE), min(ny1, TILE) nx0, ny0, nx1, ny1 = max(nx0, 0), max(ny0, 0), min(nx1, TILE), min(ny1, TILE)
local rx0, ry0, rx1, ry1 = min(nx0, tile.BX0), min(ny0, tile.BY0), max(nx1, tile.BX1), max(ny1, tile.BY1) local rx0, ry0, rx1, ry1
if force or on < 0 then
-- Dirty region = old content bounds + new content bounds. Outside it the tile is already
-- transparent, so only this region is cleared and uploaded (edge tiles are often mostly empty).
rx0, ry0, rx1, ry1 = min(nx0, tile.BX0), min(ny0, tile.BY0), max(nx1, tile.BX1), max(ny1, tile.BY1)
else
-- Partial redraw: the primitives before the first and after the last changed one are the same as last
-- time. A pixel that no changed primitive (old or new) touches is covered by the same primitives in the
-- same order, so it is already correct: only the bounds of the changed ones are cleared and redrawn.
local i, lim = 1, min(on, count)
while i <= lim and oh1[i] == lh1[i] and oh2[i] == lh2[i] do i += 1 end
local j = 0
while j <= lim - i and oh1[on - j] == lh1[count - j] and oh2[on - j] == lh2[count - j] do j += 1 end
rx0, ry0, rx1, ry1 = TILE, TILE, 0, 0
local of, lf = tile.OF, tlf[key]
local paired = on - j - i == count - j - i
for k = i, count - j do
local p = list[k]
local a, b, c, d = max(pr_bx0[p] - x0, 0), max(pr_by0[p] - y0, 0), min(pr_bx1[p] - x0, TILE), min(pr_by1[p] - y0, TILE)
local oa, ob, oc, od = ob0[k], ob1[k], ob2[k], ob3[k]
if paired and lf[k] >= 0 and lf[k] == of[k] then
-- Same flat rect (colour/uv/texture) with new bounds: pixels inside both are unchanged, only the
-- strips between the old and new edges can differ (e.g. a window background growing on resize)
local uy0, uy1 = min(b, ob), max(d, od)
local ux0, ux1 = min(a, oa), max(c, oc)
if a ~= oa then ux0 = min(a, oa); local e = max(a, oa); rx0 = min(rx0, ux0); rx1 = max(rx1, e); ry0 = min(ry0, uy0); ry1 = max(ry1, uy1) end
if c ~= oc then local e = min(c, oc); rx0 = min(rx0, e); rx1 = max(rx1, max(c, oc)); ry0 = min(ry0, uy0); ry1 = max(ry1, uy1) end
if b ~= ob then ry0 = min(ry0, min(b, ob)); ry1 = max(ry1, max(b, ob)); rx0 = min(rx0, ux0); rx1 = max(rx1, ux1) end
if d ~= od then ry0 = min(ry0, min(d, od)); ry1 = max(ry1, max(d, od)); rx0 = min(rx0, ux0); rx1 = max(rx1, ux1) end
else
if a < rx0 then rx0 = a end
if b < ry0 then ry0 = b end
if c > rx1 then rx1 = c end
if d > ry1 then ry1 = d end
if k <= on - j then
if oa < rx0 then rx0 = oa end
if ob < ry0 then ry0 = ob end
if oc > rx1 then rx1 = oc end
if od > ry1 then ry1 = od end
end
end
end
for k = count - j + 1, on - j do -- old middle longer than the new one
if ob0[k] < rx0 then rx0 = ob0[k] end
if ob1[k] < ry0 then ry0 = ob1[k] end
if ob2[k] > rx1 then rx1 = ob2[k] end
if ob3[k] > ry1 then ry1 = ob3[k] end
end
end
tile.BX0, tile.BY0, tile.BX1, tile.BY1 = nx0, ny0, nx1, ny1 tile.BX0, tile.BY0, tile.BX1, tile.BY1 = nx0, ny0, nx1, ny1
-- Remember this list for the next partial redraw
for k = 1, count do
local p = list[k]
oh1[k], oh2[k], tile.OF[k] = lh1[k], lh2[k], tlf[key][k]
ob0[k], ob1[k], ob2[k], ob3[k] = max(pr_bx0[p] - x0, 0), max(pr_by0[p] - y0, 0), min(pr_bx1[p] - x0, TILE), min(pr_by1[p] - y0, TILE)
end
tile.ON = count
local rw = rx1 - rx0 local rw = rx1 - rx0
if rw == TILE then if rw == TILE then
buffer.fill(buf, ry0 * TILE * 4, 0, (ry1 - ry0) * TILE * 4) buffer.fill(buf, ry0 * TILE * 4, 0, (ry1 - ry0) * TILE * 4)
@@ -796,8 +1015,10 @@ local function DrawLayer(bd, layer, plist, np, lx0, ly0, ox, oy, force, z, clip_
local tox, toy = ox + x0, oy + y0 local tox, toy = ox + x0, oy + y0
for k = 1, count do for k = 1, count do
local p = list[k] local p = list[k]
local cx0, cy0 = max(pr_cx0[p], x0) - x0, max(pr_cy0[p], y0) - y0 -- clip to the dirty region (only those pixels are redrawn)
local cx1, cy1 = min(pr_cx1[p], x1) - x0, min(pr_cy1[p], y1) - y0 local cx0, cy0 = max(pr_cx0[p] - x0, rx0), max(pr_cy0[p] - y0, ry0)
local cx1, cy1 = min(pr_cx1[p] - x0, rx1), min(pr_cy1[p] - y0, ry1)
if cx0 >= cx1 or cy0 >= cy1 or pr_bx0[p] - x0 >= rx1 or pr_by0[p] - y0 >= ry1 or pr_bx1[p] - x0 <= rx0 or pr_by1[p] - y0 <= ry0 then continue end
local va, vc, vd, vbuf = pr_a[p], pr_c[p], pr_d[p], pr_vb[p] local va, vc, vd, vbuf = pr_a[p], pr_c[p], pr_d[p], pr_vb[p]
if vd then if vd then
local ax, ay, cx, cy = readf64(vbuf, va) - tox, readf64(vbuf, va + 8) - toy, readf64(vbuf, vc) - tox, readf64(vbuf, vc + 8) - toy local ax, ay, cx, cy = readf64(vbuf, va) - tox, readf64(vbuf, va + 8) - toy, readf64(vbuf, vc) - tox, readf64(vbuf, vc + 8) - toy
@@ -816,31 +1037,42 @@ local function DrawLayer(bd, layer, plist, np, lx0, ly0, ox, oy, force, z, clip_
tile.Image:WritePixelsBuffer(Vector2.zero, TILE_SIZE, buf) tile.Image:WritePixelsBuffer(Vector2.zero, TILE_SIZE, buf)
elseif rw > 0 and ry1 > ry0 then elseif rw > 0 and ry1 > ry0 then
local rh = ry1 - ry0 local rh = ry1 - ry0
local sub = buffer.create(rw * rh * 4) local sk = rw * 1024 + rh -- partial upload buffers are reused (WritePixelsBuffer needs the exact size)
local sub = sub_bufs[sk]
if not sub then
if n_sub_bufs >= 256 then table.clear(sub_bufs); n_sub_bufs = 0 end
sub = buffer.create(rw * rh * 4); sub_bufs[sk] = sub; n_sub_bufs += 1
end
for y = 0, rh - 1 do buffer.copy(sub, y * rw * 4, buf, ((ry0 + y) * TILE + rx0) * 4, rw * 4) end for y = 0, rh - 1 do buffer.copy(sub, y * rw * 4, buf, ((ry0 + y) * TILE + rx0) * 4, rw * 4) end
tile.Image:WritePixelsBuffer(Vector2.new(rx0, ry0), Vector2.new(rw, rh), sub) tile.Image:WritePixelsBuffer(Vector2.new(rx0, ry0), Vector2.new(rw, rh), sub)
end end
PE() PE()
end end
if not tile.Visible then if not tile.Visible and (tile.Deferred or 0) == 0 then -- (a deferred tile that was hidden holds old pixels: stays hidden until drawn)
tile.Visible = true tile.Visible = true
tile.Label.Visible = true tile.Label.Visible = true
end end
end end
PE() PE()
return deferred
end end
@native @native
function ImGui_ImplRoblox_RenderDrawData(draw_data) function ImGui_ImplRoblox_RenderDrawData(draw_data)
local bd = ImGui_ImplRoblox_GetBackendData() local bd = ImGui_ImplRoblox_GetBackendData()
PB("ImGui RenderDrawData") PB("ImGui RenderDrawData")
bd.FrameT0 = os.clock()
bd.FrameTiles = 0
bd.RedrawStarted = false -- the first dirty tile of a frame is always redrawn (progress), the rest within the budget
bd.Stale = nil
PB("ImGui textures") PB("ImGui textures")
if draw_data.Textures ~= nil then if draw_data.Textures ~= nil then
for _, tex in draw_data.Textures:iter() do for _, tex in draw_data.Textures:iter() do
if tex.Status ~= ImTextureStatus.OK then if tex.Status ~= ImTextureStatus.OK then
-- No redraw needed for WantUpdates: it only writes texture portions which have never been used before
-- (new glyphs/rects), so no existing tile samples them. Created textures get a new (never reused) id.
ImGui_ImplRoblox_UpdateTexture(tex) ImGui_ImplRoblox_UpdateTexture(tex)
bd.ForceRedraw = true -- texture contents changed: tiles sampling it must be redrawn
end end
end end
end end
@@ -862,7 +1094,8 @@ function ImGui_ImplRoblox_RenderDrawData(draw_data)
end end
if rate > 0 then if rate > 0 then
local now = os.clock() local now = os.clock()
if now + 0.002 < bd.NextRender then table.clear(bd.Overlays); PE(); return end -- Skipped: kept pending, so if no new frame comes (the app stopped rendering) Heartbeat still draws it
if now + 0.002 < bd.NextRender then bd.Pending = draw_data; PE(); return end
bd.NextRender = math.max(bd.NextRender + 1 / rate, now) bd.NextRender = math.max(bd.NextRender + 1 / rate, now)
end end
@@ -877,20 +1110,27 @@ function ImGui_ImplRoblox_RenderDrawData(draw_data)
local n = 0 local n = 0
local z = 0 local z = 0
local any_deferred = false
for _, draw_list in draw_data.CmdLists:iter() do for _, draw_list in draw_data.CmdLists:iter() do
-- Unchanged draw list (same vertex bytes, indices, commands, origin and scroll): its layers are already -- Unchanged draw list (same vertices, indices, commands, origin and scroll): its layers are already correct,
-- correct, only restack them. Static windows then cost one byte compare instead of collect + hash. -- only restack them. Moved by whole pixels (a dragged window): only move its layers. Either way no collect + hash.
PB("ImGui unchanged check") PB("ImGui unchanged check")
local sig = DrawListSignature(draw_list, ox, oy)
local set = bd.Layers[draw_list] local set = bd.Layers[draw_list]
if sig and set and not force and set.Sig == sig then local same, sdx, sdy = 0, 0, 0
if not force then same, sdx, sdy = CompareDrawList(set, draw_list, ox, oy, sw, sh) end
if same ~= 0 then
for kind = 1, 3 do for kind = 1, 3 do
local layer = set[kind] local layer = set[kind]
if layer and layer.Visible then if layer and layer.Visible then
if same == 2 then ShiftLayer(layer, sdx, sdy, bd.Scale) end
z += 1 z += 1
if layer.Z ~= z then layer.Z = z; layer.Root.ZIndex = z end if layer.Z ~= z then layer.Z = z; layer.Root.ZIndex = z end
end end
end end
if same == 2 then
Snapshot(set, draw_list, ox, oy, sw, sh)
set.SEX0 += sdx; set.SEY0 += sdy; set.SEX1 += sdx; set.SEY1 += sdy
end
set.LastUsed = frame_no set.LastUsed = frame_no
z = PlaceOverlays(bd, draw_list, z, ox, oy) z = PlaceOverlays(bd, draw_list, z, ox, oy)
PE() PE()
@@ -908,13 +1148,30 @@ function ImGui_ImplRoblox_RenderDrawData(draw_data)
content_x1, content_y1 = min(floor(ccx1 - ox), lim_x1), min(floor(ccy1 - oy), lim_y1) content_x1, content_y1 = min(floor(ccx1 - ox), lim_x1), min(floor(ccy1 - oy), lim_y1)
end end
local seen_content = false local seen_content = false
-- extents of everything clamped by the clip limits below (lim_free: nothing was), for the move fast path
local ex0, ey0, ex1, ey1 = math.huge, math.huge, -math.huge, -math.huge
local lim_free = true
if ccx0 then
local a, b, c, d = floor(ccx0 - ox), floor(ccy0 - oy), floor(ccx1 - ox), floor(ccy1 - oy)
if a < lim_x0 or b < lim_y0 or c > lim_x1 or d > lim_y1 then lim_free = false end
ex0, ey0, ex1, ey1 = min(ex0, a), min(ey0, b), max(ex1, c), max(ey1, d)
end
local ci = 0
for _, pcmd in draw_list.CmdBuffer:iter() do for _, pcmd in draw_list.CmdBuffer:iter() do
ci += 1
cm_free[ci], cm_kind[ci] = false, 0
if pcmd.UserCallback ~= nil then if pcmd.UserCallback ~= nil then
pcmd.UserCallback(draw_list, pcmd) pcmd.UserCallback(draw_list, pcmd)
elseif pcmd.ElemCount > 0 then elseif pcmd.ElemCount > 0 then
local clip = pcmd.ClipRect local clip = pcmd.ClipRect
local cx0, cy0 = max(floor(clip.x - ox), lim_x0), max(floor(clip.y - oy), lim_y0) local rcx0, rcy0, rcx1, rcy1 = floor(clip.x - ox), floor(clip.y - oy), floor(clip.z - ox), floor(clip.w - oy)
local cx1, cy1 = min(floor(clip.z - ox), lim_x1), min(floor(clip.w - oy), lim_y1) if rcx0 < lim_x0 or rcy0 < lim_y0 or rcx1 > lim_x1 or rcy1 > lim_y1 then lim_free = false end
if rcx0 < ex0 then ex0 = rcx0 end
if rcy0 < ey0 then ey0 = rcy0 end
if rcx1 > ex1 then ex1 = rcx1 end
if rcy1 > ey1 then ey1 = rcy1 end
local cx0, cy0 = max(rcx0, lim_x0), max(rcy0, lim_y0)
local cx1, cy1 = min(rcx1, lim_x1), min(rcy1, lim_y1)
local kind = 1 local kind = 1
if ccx0 and clip.x >= ccx0 and clip.y >= ccy0 and clip.z <= ccx1 and clip.w <= ccy1 then if ccx0 and clip.x >= ccx0 and clip.y >= ccy0 and clip.z <= ccx1 and clip.w <= ccy1 then
kind = 2; seen_content = true kind = 2; seen_content = true
@@ -925,6 +1182,8 @@ function ImGui_ImplRoblox_RenderDrawData(draw_data)
elseif seen_content then elseif seen_content then
kind = 3 kind = 3
end end
cm_kind[ci] = kind
local cfree, cex0, cey0, cex1, cey1 = cx1 > cx0 and cy1 > cy0, math.huge, math.huge, -math.huge, -math.huge
if cx1 > cx0 and cy1 > cy0 then if cx1 > cx0 and cy1 > cy0 then
local tex_id = pcmd:GetTexID() local tex_id = pcmd:GetTexID()
local t = bd.Textures[tex_id] local t = bd.Textures[tex_id]
@@ -970,6 +1229,16 @@ function ImGui_ImplRoblox_RenderDrawData(draw_data)
bx1, by1 = ceil(max(pax, pbx, pcx) - ox) + 1, ceil(max(pay, pby, pcy) - oy) + 1 bx1, by1 = ceil(max(pax, pbx, pcx) - ox) + 1, ceil(max(pay, pby, pcy) - oy) + 1
i = i + 3 i = i + 3
end end
if bx0 < cx0 or by0 < cy0 or bx1 > cx1 or by1 > cy1 then cfree = false end
if bx0 < cex0 then cex0 = bx0 end
if by0 < cey0 then cey0 = by0 end
if bx1 > cex1 then cex1 = bx1 end
if by1 > cey1 then cey1 = by1 end
if bx0 < ex0 then ex0 = bx0 end
if by0 < ey0 then ey0 = by0 end
if bx1 > ex1 then ex1 = bx1 end
if by1 > ey1 then ey1 = by1 end
if bx0 < lim_x0 or by0 < lim_y0 or bx1 > lim_x1 or by1 > lim_y1 then lim_free = false end
if bx0 < cx0 then bx0 = cx0 end if bx0 < cx0 then bx0 = cx0 end
if by0 < cy0 then by0 = cy0 end if by0 < cy0 then by0 = cy0 end
if bx1 > cx1 then bx1 = cx1 end if bx1 > cx1 then bx1 = cx1 end
@@ -983,12 +1252,16 @@ function ImGui_ImplRoblox_RenderDrawData(draw_data)
end end
end end
end end
if cex0 > cex1 then cex0, cey0, cex1, cey1 = 0, 0, 0, 0 end -- (no primitives)
cm_free[ci] = cfree
cm_x0[ci], cm_y0[ci], cm_x1[ci], cm_y1[ci] = cex0, cey0, cex1, cey1
end end
end end
PE() PE()
-- Split into layer kinds and place each one -- Split into layer kinds and place each one
set = bd.Layers[draw_list] set = bd.Layers[draw_list]
local list_deferred = false
local nk1, nk2, nk3 = 0, 0, 0 local nk1, nk2, nk3 = 0, 0, 0
local o1x, o1y, o3x, o3y = math.huge, math.huge, math.huge, math.huge local o1x, o1y, o3x, o3y = math.huge, math.huge, math.huge, math.huge
for p = first, n do for p = first, n do
@@ -1007,19 +1280,28 @@ function ImGui_ImplRoblox_RenderDrawData(draw_data)
end end
if nk1 > 0 then if nk1 > 0 then
z += 1 z += 1
DrawLayer(bd, GetLayer(bd, draw_list, 1), plist1, nk1, o1x, o1y, ox, oy, force, z) if DrawLayer(bd, GetLayer(bd, draw_list, 1), plist1, nk1, o1x, o1y, ox, oy, force, z) then list_deferred = true end
elseif set and set[1] then HideLayer(set[1]) end elseif set and set[1] then HideLayer(set[1]) end
if nk2 > 0 then if nk2 > 0 then
z += 1 z += 1
DrawLayer(bd, GetLayer(bd, draw_list, 2), plist2, nk2, floor(draw_list._ScrollOriginX - ox), floor(draw_list._ScrollOriginY - oy), ox, oy, force, z, if DrawLayer(bd, GetLayer(bd, draw_list, 2), plist2, nk2, floor(draw_list._ScrollOriginX - ox), floor(draw_list._ScrollOriginY - oy), ox, oy, force, z,
content_x0, content_y0, content_x1, content_y1) content_x0, content_y0, content_x1, content_y1) then list_deferred = true end
elseif set and set[2] then HideLayer(set[2]) end elseif set and set[2] then HideLayer(set[2]) end
if nk3 > 0 then if nk3 > 0 then
z += 1 z += 1
DrawLayer(bd, GetLayer(bd, draw_list, 3), plist3, nk3, o3x, o3y, ox, oy, force, z) if DrawLayer(bd, GetLayer(bd, draw_list, 3), plist3, nk3, o3x, o3y, ox, oy, force, z) then list_deferred = true end
elseif set and set[3] then HideLayer(set[3]) end elseif set and set[3] then HideLayer(set[3]) end
set = bd.Layers[draw_list] set = bd.Layers[draw_list]
if set then set.LastUsed = frame_no; set.Sig = sig end -- (deferred tiles: not "unchanged" next frame even if the draw list is, so they get redrawn)
if set then
set.LastUsed = frame_no
if list_deferred then set.SnapOK = false else
Snapshot(set, draw_list, ox, oy, sw, sh)
set.SLimFree = lim_free
set.SEX0, set.SEY0, set.SEX1, set.SEY1 = ex0, ey0, ex1, ey1
end
end
if list_deferred then any_deferred = true end
z = PlaceOverlays(bd, draw_list, z, ox, oy) z = PlaceOverlays(bd, draw_list, z, ox, oy)
end end
@@ -1032,7 +1314,7 @@ function ImGui_ImplRoblox_RenderDrawData(draw_data)
-- Hide layers not drawn this frame; free long unused ones (closed windows, old tooltips) -- Hide layers not drawn this frame; free long unused ones (closed windows, old tooltips)
for draw_list, set in pairs(bd.Layers) do for draw_list, set in pairs(bd.Layers) do
if set.LastUsed ~= frame_no then if set.LastUsed ~= frame_no then
set.Sig = nil -- hidden now: must go through the full path when shown again set.SnapOK = false -- hidden now: must go through the full path when shown again
for kind = 1, 3 do if set[kind] then HideLayer(set[kind]) end end for kind = 1, 3 do if set[kind] then HideLayer(set[kind]) end end
if frame_no - set.LastUsed > LAYER_KEEP_FRAMES then if frame_no - set.LastUsed > LAYER_KEEP_FRAMES then
DestroyLayerSet(set) DestroyLayerSet(set)
@@ -1044,6 +1326,8 @@ function ImGui_ImplRoblox_RenderDrawData(draw_data)
-- drop references so old vertex tables can be collected -- drop references so old vertex tables can be collected
for k = n + 1, bd.LastPrimCount do pr_t[k], pr_vb[k] = nil, nil end for k = n + 1, bd.LastPrimCount do pr_t[k], pr_vb[k] = nil, nil end
bd.LastPrimCount = n bd.LastPrimCount = n
-- Tiles left for later: if the app doesn't submit a new frame, Heartbeat finishes them from this draw data
if any_deferred then bd.Stale = draw_data; bd.StaleHB = bd.HB end
PE() PE()
end end
@@ -1069,7 +1353,7 @@ function ImGui_ImplRoblox_Init(parent, render_scale)
local bd = { local bd = {
Time = os.clock(), Gui = gui, Connections = {}, Time = os.clock(), Gui = gui, Connections = {},
Textures = {}, TextureCount = 0, MouseX = -1, MouseY = -1, Textures = {}, TextureCount = 0, MouseX = -1, MouseY = -1,
Layers = {}, Overlays = {}, OverlayHolders = {}, OverlayShown = {}, FrameNo = 0, Scale = render_scale or 1, TilesActive = false, RenderRate = 60, BusyRenderRate = 0, NextRender = 0, LastPrimCount = 0, ForceRedraw = true, Layers = {}, Overlays = {}, OverlayHolders = {}, OverlayShown = {}, FrameNo = 0, Scale = render_scale or 1, TilesActive = false, RenderRate = 0, BusyRenderRate = 0, RedrawBudget = 0, HB = 0, NextRender = 0, LastPrimCount = 0, ForceRedraw = true,
} }
io.BackendPlatformUserData = bd io.BackendPlatformUserData = bd
@@ -1127,8 +1411,36 @@ function ImGui_ImplRoblox_Init(parent, render_scale)
end end
end) end)
-- Text proxy: a hidden TextBox holds focus while an imgui text field is active, so the OS gives us typing (any bd.PendingChars = {}
-- layout/unicode), paste (Ctrl+V) and copy/cut (Ctrl+C/X of the proxy's text = imgui's current selection).
-- Render rate cap trailing edge: the last frame submitted is always drawn, even if it was skipped and the app
-- then stopped calling NewFrame (e.g. it rendered one empty frame to hide its windows)
connect(game:GetService("RunService").Heartbeat, function()
local d = bd.Pending
if d and os.clock() + 0.002 >= bd.NextRender then
bd.Pending = nil
ImGui_ImplRoblox_RenderDrawData(d)
elseif bd.Stale and bd.HB > bd.StaleHB then -- deferred tiles of a frame not followed by a new one
ImGui_ImplRoblox_RenderDrawData(bd.Stale)
end
bd.HB += 1
end)
connect(UserInputService.WindowFocused, function() ImGui.GetIO():AddFocusEvent(true) end)
connect(UserInputService.WindowFocusReleased, function() ImGui.GetIO():AddFocusEvent(false) end)
end
-- Text proxy (opt-in, ImGui_ImplRoblox.SetTextProxy(true)): a hidden TextBox holds focus while an imgui text field is
-- active, so the OS gives us typing (any layout/unicode), paste (Ctrl+V) and copy/cut (Ctrl+C/X of imgui's selection).
-- Off: typing comes from KeyCodes (printable ASCII), no OS clipboard.
function ImGui_ImplRoblox_SetTextProxy(enabled)
local bd = ImGui_ImplRoblox_GetBackendData()
if not enabled then
if bd.Proxy then bd.Proxy:Destroy(); bd.Proxy = nil end
proxy_focused = false
return
end
if bd.Proxy then return end
local proxy = Instance.new("TextBox") local proxy = Instance.new("TextBox")
proxy.Name = "TextProxy" proxy.Name = "TextProxy"
proxy.BackgroundTransparency = 1 proxy.BackgroundTransparency = 1
@@ -1140,12 +1452,11 @@ function ImGui_ImplRoblox_Init(parent, render_scale)
proxy.ClearTextOnFocus = false proxy.ClearTextOnFocus = false
proxy.TextEditable = true proxy.TextEditable = true
proxy.Text = "" proxy.Text = ""
proxy.Parent = gui proxy.Parent = bd.Gui
bd.Proxy, bd.ProxyText, bd.PendingChars = proxy, "", {} bd.Proxy, bd.ProxyText = proxy, ""
-- Clicking elsewhere (or Roblox taking focus) ends the edit, like clicking outside the field in imgui. -- Clicking elsewhere (or Roblox taking focus) ends the edit, like clicking outside the field in imgui.
-- (Text is polled in NewFrame instead of using Text changed signals: those are deferred in Roblox.) -- (Text is polled in NewFrame instead of using Text changed signals: those are deferred in Roblox.)
connect(proxy.FocusLost, function() proxy.FocusLost:Connect(function()
if bd.ProxyDebug then print("[ImGui text proxy] FocusLost, mouse over imgui:", ImGui.GetIO().WantCaptureMouse) end
if bd.ProxyReleasing then return end if bd.ProxyReleasing then return end
-- Roblox drops TextBox focus on any click, including clicks inside imgui (e.g. moving the cursor within the -- Roblox drops TextBox focus on any click, including clicks inside imgui (e.g. moving the cursor within the
-- same field). Those must not end the edit: just recapture next frame. Only a click outside imgui ends it. -- same field). Those must not end the edit: just recapture next frame. Only a click outside imgui ends it.
@@ -1154,9 +1465,6 @@ function ImGui_ImplRoblox_Init(parent, render_scale)
local g = ImGui.GetCurrentContext() local g = ImGui.GetCurrentContext()
if g and g.ActiveId ~= 0 and g.ActiveId == bd.ProxyActiveId then ImGui.ClearActiveID() end if g and g.ActiveId ~= 0 and g.ActiveId == bd.ProxyActiveId then ImGui.ClearActiveID() end
end) end)
connect(UserInputService.WindowFocused, function() ImGui.GetIO():AddFocusEvent(true) end)
connect(UserInputService.WindowFocusReleased, function() ImGui.GetIO():AddFocusEvent(false) end)
end end
function ImGui_ImplRoblox_Shutdown() function ImGui_ImplRoblox_Shutdown()
@@ -1177,6 +1485,8 @@ function ImGui_ImplRoblox_NewFrame()
PB("ImGui NewFrame (backend)") PB("ImGui NewFrame (backend)")
local io = ImGui.GetIO() local io = ImGui.GetIO()
local bd = ImGui_ImplRoblox_GetBackendData() local bd = ImGui_ImplRoblox_GetBackendData()
if bd.Pending then bd.Pending = nil; table.clear(bd.Overlays) end -- skipped frame superseded (its draw lists get reset)
bd.Stale = nil
local screen = bd.Gui.AbsoluteSize local screen = bd.Gui.AbsoluteSize
local w, h = floor(screen.X * bd.Scale), floor(screen.Y * bd.Scale) local w, h = floor(screen.X * bd.Scale), floor(screen.Y * bd.Scale)
@@ -1201,15 +1511,16 @@ function ImGui_ImplRoblox_NewFrame()
-- otherwise the KeyCode fallback keeps typing working. -- otherwise the KeyCode fallback keeps typing working.
local proxy = bd.Proxy local proxy = bd.Proxy
local g = ImGui.GetCurrentContext() local g = ImGui.GetCurrentContext()
local focused_box = UserInputService:GetFocusedTextBox() local focused_box = proxy and UserInputService:GetFocusedTextBox()
if io.WantTextInput and g.ActiveId ~= 0 then if not proxy then
-- text proxy disabled: KeyCode typing only
elseif io.WantTextInput and g.ActiveId ~= 0 then
if g.ActiveId ~= bd.ProxyActiveId or g.ActiveIdIsJustActivated or g.IO.MouseClicked[0] then bd.ProxyActiveId = g.ActiveId; bd.ProxyLostId = nil end -- (re)clicking a field recaptures if g.ActiveId ~= bd.ProxyActiveId or g.ActiveIdIsJustActivated or g.IO.MouseClicked[0] then bd.ProxyActiveId = g.ActiveId; bd.ProxyLostId = nil end -- (re)clicking a field recaptures
local mouse_up = not (io.MouseDown[0] or io.MouseDown[1] or io.MouseDown[2]) local mouse_up = not (io.MouseDown[0] or io.MouseDown[1] or io.MouseDown[2])
if focused_box ~= proxy and mouse_up and bd.ProxyLostId ~= g.ActiveId and (focused_box == nil or not focused_box:IsDescendantOf(game)) then if focused_box ~= proxy and mouse_up and bd.ProxyLostId ~= g.ActiveId and (focused_box == nil or not focused_box:IsDescendantOf(game)) then
bd.ProxyText = proxy.Text -- what it already holds is not new input (else re-typing it would collapse imgui's selection) bd.ProxyText = proxy.Text -- what it already holds is not new input (else re-typing it would collapse imgui's selection)
proxy:CaptureFocus() proxy:CaptureFocus()
focused_box = UserInputService:GetFocusedTextBox() focused_box = UserInputService:GetFocusedTextBox()
if bd.ProxyDebug then print("[ImGui text proxy] CaptureFocus ->", focused_box == proxy and "focused" or ("FAILED, focused = " .. tostring(focused_box))) end
end end
elseif focused_box == proxy then elseif focused_box == proxy then
bd.ProxyReleasing = true bd.ProxyReleasing = true
@@ -1217,13 +1528,11 @@ function ImGui_ImplRoblox_NewFrame()
bd.ProxyReleasing = false bd.ProxyReleasing = false
focused_box = nil focused_box = nil
end end
local was_focused = proxy_focused proxy_focused = proxy ~= nil and focused_box == proxy
proxy_focused = focused_box == proxy
local used_proxy_text = false local used_proxy_text = false
if proxy_focused then if proxy_focused then
-- 1) whatever the OS put in the proxy since last frame (typing, paste, cut) replaces imgui's selection -- 1) whatever the OS put in the proxy since last frame (typing, paste, cut) replaces imgui's selection
local text = proxy.Text local text = proxy.Text
if text ~= bd.ProxyText and bd.ProxyDebug then print("[ImGui text proxy] text", string.format("%q", text), "was", string.format("%q", tostring(bd.ProxyText))) end
if text ~= bd.ProxyText and text ~= bd.ProxyLastRaw then -- (LastRaw: don't re-send if our reset hasn't applied yet) if text ~= bd.ProxyText and text ~= bd.ProxyLastRaw then -- (LastRaw: don't re-send if our reset hasn't applied yet)
bd.ProxyLastRaw = text bd.ProxyLastRaw = text
-- InputText ignores characters while Ctrl is held (upstream, so shortcuts don't type); a paste arrives with -- InputText ignores characters while Ctrl is held (upstream, so shortcuts don't type); a paste arrives with
@@ -1259,7 +1568,6 @@ function ImGui_ImplRoblox_NewFrame()
proxy.Text = sel proxy.Text = sel
bd.ProxyLastRaw = nil bd.ProxyLastRaw = nil
bd.ProxyText = sel bd.ProxyText = sel
if bd.ProxyDebug then print("[ImGui text proxy] mirror selection", string.format("%q", sel)) end
end end
-- keep imgui's selection fully selected in the proxy, so Ctrl+C copies it and typing/Ctrl+V replaces it -- keep imgui's selection fully selected in the proxy, so Ctrl+C copies it and typing/Ctrl+V replaces it
local want_sel, want_cur = (#sel > 0) and 1 or -1, #sel + 1 local want_sel, want_cur = (#sel > 0) and 1 or -1, #sel + 1
@@ -1275,7 +1583,6 @@ function ImGui_ImplRoblox_NewFrame()
if not used_proxy_text then for _, c in ipairs(pending) do io:AddInputCharacter(c) end end if not used_proxy_text then for _, c in ipairs(pending) do io:AddInputCharacter(c) end end
table.clear(pending) table.clear(pending)
end end
if bd.ProxyDebug and was_focused ~= proxy_focused then print("[ImGui text proxy] proxy_focused =", proxy_focused) end
-- While the mouse is over imgui, make the tiles Active so Roblox stops passing clicks/wheel to the game (camera zoom etc.) -- While the mouse is over imgui, make the tiles Active so Roblox stops passing clicks/wheel to the game (camera zoom etc.)
local want = io.WantCaptureMouse local want = io.WantCaptureMouse
@@ -1286,7 +1593,15 @@ function ImGui_ImplRoblox_NewFrame()
PE() PE()
end end
--- Max tile redraws per second (default 60); 0 = every frame. imgui logic still runs every frame. --- Opt-in: max milliseconds per frame spent redrawing changed tiles (default 0 = no limit, every change is drawn the
--- same frame). With a budget (e.g. 1.5), tiles over it keep
--- their previous pixels and are redrawn on the following frames (each one at most a few frames late), so a big change
--- (resizing a large window, opening one, scrolling a long list) is spread over frames instead of one long frame.
function ImGui_ImplRoblox_SetRedrawBudget(ms)
ImGui_ImplRoblox_GetBackendData().RedrawBudget = (ms or 0) / 1000
end
--- Max tile redraws per second (default 0 = every frame, limited by the redraw budget instead). imgui logic still runs every frame.
function ImGui_ImplRoblox_SetRenderRate(hz) function ImGui_ImplRoblox_SetRenderRate(hz)
ImGui_ImplRoblox_GetBackendData().RenderRate = hz or 0 ImGui_ImplRoblox_GetBackendData().RenderRate = hz or 0
end end
@@ -1316,8 +1631,9 @@ ImGui_ImplRoblox = {
AddFile = ImGui_ImplRoblox_AddFile, AddFile = ImGui_ImplRoblox_AddFile,
SetRenderScale = ImGui_ImplRoblox_SetRenderScale, SetRenderScale = ImGui_ImplRoblox_SetRenderScale,
SetRenderRate = ImGui_ImplRoblox_SetRenderRate, SetRenderRate = ImGui_ImplRoblox_SetRenderRate,
SetRedrawBudget = ImGui_ImplRoblox_SetRedrawBudget,
SetOverlay = ImGui_ImplRoblox_SetOverlay, SetOverlay = ImGui_ImplRoblox_SetOverlay,
SetTextProxyDebug = function(on) ImGui_ImplRoblox_GetBackendData().ProxyDebug = on end, -- prints focus/text events SetTextProxy = ImGui_ImplRoblox_SetTextProxy,
CreateTexture = ImGui_ImplRoblox_CreateTexture, CreateTexture = ImGui_ImplRoblox_CreateTexture,
DestroyTexture = ImGui_ImplRoblox_DestroyTexture, DestroyTexture = ImGui_ImplRoblox_DestroyTexture,
GetRenderScale = function() return ImGui_ImplRoblox_GetBackendData().Scale end, GetRenderScale = function() return ImGui_ImplRoblox_GetBackendData().Scale end,
+1 -1
View File
@@ -1 +1 @@
[[12, "imgui.lua"], [18472, "imgui_demo.lua"], [25757, "imgui_draw.lua"], [30995, "imgui_h.lua"], [33193, "imgui_impl_roblox.lua"], [34525, "imgui_impl_roblox_fonts.lua"], [40928, "imgui_internal.lua"], [44100, "imgui_tables.lua"], [48447, "imgui_widgets.lua"], [58202, "imstb_rectpack.lua"], [58648, "imstb_textedit.lua"], [59889, "imstb_truetype.lua"], [62357, "imstd_minstdio.lua"]] [[46, "imgui.lua"], [21389, "imgui_demo.lua"], [29807, "imgui_draw.lua"], [35745, "imgui_h.lua"], [38412, "imgui_impl_roblox.lua"], [40060, "imgui_impl_roblox_fonts.lua"], [46463, "imgui_internal.lua"], [49932, "imgui_tables.lua"], [54991, "imgui_widgets.lua"], [65631, "imstb_rectpack.lua"], [66295, "imstb_textedit.lua"], [67837, "imstb_truetype.lua"], [70625, "imstd_minstdio.lua"]]
+15369 -7097
View File
File diff suppressed because it is too large Load Diff
+958
View File
@@ -0,0 +1,958 @@
--!strict
-- GENERATED by tools/gen_types.py - do not edit. The library is the child ModuleScript 'Impl'.
export type ImVec2 = { x: number, y: number, [any]: any }
export type ImVec4 = { x: number, y: number, z: number, w: number, [any]: any }
export type ImGuiAPI = {
AcceptDragDropPayload: (type: string, flags: number?) -> any,
ActivateItemByID: (...any) -> ...any,
AddDrawListToDrawDataEx: (...any) -> ...any,
AddSettingsHandler: (...any) -> ...any,
AddUpdateViewport: (...any) -> ...any,
AlignTextToFramePadding: () -> (),
ArrowButton: (str_id: string, dir: number) -> boolean,
ArrowButtonEx: (...any) -> ...any,
Begin: (name: string, open: boolean?, flags: number?) -> (boolean, boolean?),
BeginChild: (...any) -> ...any,
BeginChildEx: (...any) -> ...any,
BeginColumns: (...any) -> ...any,
BeginCombo: (label: string, preview_value: string, flags: number?) -> boolean,
BeginComboPopup: (...any) -> ...any,
BeginComboPreview: (...any) -> ...any,
BeginDisabled: (disabled: boolean?) -> (),
BeginDisabledOverrideReenable: (...any) -> ...any,
BeginDockableDragDropSource: (...any) -> ...any,
BeginDockableDragDropTarget: (...any) -> ...any,
BeginDocked: (...any) -> ...any,
BeginDragDropSource: (flags: number?) -> boolean,
BeginDragDropTarget: () -> boolean,
BeginDragDropTargetCustom: (...any) -> ...any,
BeginDragDropTargetViewport: (...any) -> ...any,
BeginErrorTooltip: (...any) -> ...any,
BeginGroup: () -> (),
BeginItemTooltip: () -> boolean,
BeginListBox: (label: string, size_arg: ImVec2?) -> boolean,
BeginMainMenuBar: () -> boolean,
BeginMenu: (label: string, enabled: boolean?) -> boolean,
BeginMenuBar: () -> boolean,
BeginMenuEx: (...any) -> ...any,
BeginPopup: (str_id: string, flags: number?) -> boolean,
BeginPopupContextItem: (str_id: string?, popup_flags: number?) -> boolean,
BeginPopupContextVoid: (str_id: string?, popup_flags: number?) -> boolean,
BeginPopupContextWindow: (str_id: string?, popup_flags: number?) -> boolean,
BeginPopupEx: (...any) -> ...any,
BeginPopupMenuEx: (...any) -> ...any,
BeginPopupModal: (name: string, p_open: boolean?, flags: number?) -> (boolean, boolean?),
BeginTabBar: (str_id: string, flags: number?) -> boolean,
BeginTabBarEx: (...any) -> ...any,
BeginTabItem: (label: string, p_open: boolean?, flags: number?) -> (boolean, boolean?),
BeginTable: (str_id: string, columns_count: number, flags: number?, outer_size: ImVec2?, inner_width: number?) -> boolean,
BeginTableEx: (...any) -> ...any,
BeginTooltip: () -> boolean,
BeginTooltipEx: (...any) -> ...any,
BeginTooltipHidden: (...any) -> ...any,
BeginViewportSideBar: (...any) -> ...any,
BringWindowToDisplayBack: (...any) -> ...any,
BringWindowToDisplayBehind: (...any) -> ...any,
BringWindowToDisplayFront: (...any) -> ...any,
BringWindowToFocusFront: (...any) -> ...any,
Bullet: () -> (),
BulletText: (fmt: string, ...any) -> (),
Button: (label: string, size_arg: ImVec2?) -> boolean,
ButtonBehavior: (...any) -> ...any,
ButtonEx: (...any) -> ...any,
CalcClipRectVisibleItemsY: (...any) -> ...any,
CalcItemSize: (...any) -> ...any,
CalcItemWidth: () -> number,
CalcRoundingFlagsForRectInRect: (...any) -> ...any,
CalcTextSize: (text: string, text_end: string?, hide_text_after_double_hash: boolean?, wrap_width: number?) -> ImVec2,
CalcTextSizeEx: (...any) -> ...any,
CalcTypematicRepeatAmount: (...any) -> ...any,
CalcWindowNextAutoFitSize: (...any) -> ...any,
CalcWrapWidthForPos: (...any) -> ...any,
Checkbox: (label: string, v: boolean?) -> (boolean, boolean?),
CheckboxFlags: (...any) -> ...any,
CleanupIniSettings: (...any) -> ...any,
ClearActiveID: (...any) -> ...any,
ClearDragDrop: (...any) -> ...any,
ClearIniSettings: (...any) -> ...any,
ClearWindowSettings: (...any) -> ...any,
CloseButton: (...any) -> ...any,
CloseCurrentPopup: () -> (),
ClosePopupToLevel: (...any) -> ...any,
ClosePopupsExceptModals: (...any) -> ...any,
ClosePopupsOverWindow: (...any) -> ...any,
CollapseButton: (...any) -> ...any,
CollapsingHeader: (...any) -> ...any,
ColorButton: (desc_id: string, col: ImVec4, flags: number?, size_arg: ImVec2?) -> boolean,
ColorConvertFloat4ToU32: (in_col: ImVec4) -> number,
ColorConvertHSVtoRGB: (...any) -> ...any,
ColorConvertRGBtoHSV: (...any) -> ...any,
ColorConvertU32ToFloat4: (...any) -> ...any,
ColorEdit3: (label: string, col: any, flags: number?) -> boolean,
ColorEdit4: (label: string, col: any, flags: number?) -> boolean,
ColorEditOptionsPopup: (...any) -> ...any,
ColorPicker3: (label: string, col: any, flags: number?) -> boolean,
ColorPicker4: (label: string, col: any, flags: number?, ref_col: any) -> boolean,
ColorPickerOptionsPopup: (...any) -> ...any,
ColorTooltip: (...any) -> ...any,
Columns: (columns_count: number?, id: string?, borders: boolean?) -> (),
Combo: (...any) -> ...any,
ConvertSingleModFlagToKey: (...any) -> ...any,
CreateContext: (shared_font_atlas: any) -> ...any,
CreateNewWindowSettings: (...any) -> ...any,
DataTypeApplyFromText: (...any) -> ...any,
DataTypeApplyOp: (...any) -> ...any,
DataTypeClamp: (...any) -> ...any,
DataTypeCompare: (...any) -> ...any,
DataTypeFormatString: (...any) -> ...any,
DataTypeGetInfo: (...any) -> ...any,
DataTypeIsZero: (...any) -> ...any,
DebugAllocHook: (...any) -> ...any,
DebugHookIdInfo: (...any) -> ...any,
DebugLog: (fmt: string, ...any) -> (),
DebugNodeColumns: (...any) -> ...any,
DebugNodeDockNode: (...any) -> ...any,
DebugNodeDrawList: (...any) -> ...any,
DebugNodeFont: (...any) -> ...any,
DebugNodeFontGlyph: (...any) -> ...any,
DebugNodeFontGlyphsForSrcMask: (...any) -> ...any,
DebugNodeStorage: (...any) -> ...any,
DebugNodeTabBar: (...any) -> ...any,
DebugNodeTable: (...any) -> ...any,
DebugNodeTableSettings: (...any) -> ...any,
DebugNodeTexture: (...any) -> ...any,
DebugNodeViewport: (...any) -> ...any,
DebugNodeWindow: (...any) -> ...any,
DebugNodeWindowSettings: (...any) -> ...any,
DebugNodeWindowsList: (...any) -> ...any,
DebugNodeWindowsListByBeginStackParent: (...any) -> ...any,
DebugRenderViewportThumbnail: (...any) -> ...any,
DebugTextEncoding: (str: string) -> (),
DestroyContext: (ctx: any) -> ...any,
DestroyPlatformWindow: (...any) -> ...any,
DestroyPlatformWindows: () -> (),
DestroyViewport: (...any) -> ...any,
DockBuilderAddNode: (...any) -> ...any,
DockBuilderCopyDockSpace: (...any) -> ...any,
DockBuilderCopyNode: (...any) -> ...any,
DockBuilderCopyWindowSettings: (...any) -> ...any,
DockBuilderDockWindow: (...any) -> ...any,
DockBuilderFinish: (...any) -> ...any,
DockBuilderGetCentralNode: (...any) -> ...any,
DockBuilderGetNode: (...any) -> ...any,
DockBuilderRemoveNode: (...any) -> ...any,
DockBuilderRemoveNodeChildNodes: (...any) -> ...any,
DockBuilderRemoveNodeDockedWindows: (...any) -> ...any,
DockBuilderSetNodePos: (...any) -> ...any,
DockBuilderSetNodeSize: (...any) -> ...any,
DockBuilderSplitNode: (...any) -> ...any,
DockContextAddNode: (...any) -> ...any,
DockContextBindNodeToWindow: (...any) -> ...any,
DockContextBuildAddWindowsToNodes: (...any) -> ...any,
DockContextBuildNodesFromSettings: (...any) -> ...any,
DockContextCalcDropPosForDocking: (...any) -> ...any,
DockContextClearNodes: (...any) -> ...any,
DockContextDeleteNode: (...any) -> ...any,
DockContextEndFrame: (...any) -> ...any,
DockContextFindNodeByID: (...any) -> ...any,
DockContextGenNodeID: (...any) -> ...any,
DockContextInitialize: (...any) -> ...any,
DockContextNewFrameUpdateDocking: (...any) -> ...any,
DockContextNewFrameUpdateUndocking: (...any) -> ...any,
DockContextProcessDock: (...any) -> ...any,
DockContextProcessUndockNode: (...any) -> ...any,
DockContextProcessUndockWindow: (...any) -> ...any,
DockContextPruneUnusedSettingsNodes: (...any) -> ...any,
DockContextQueueDock: (...any) -> ...any,
DockContextQueueNotifyRemovedNode: (...any) -> ...any,
DockContextQueueUndockNode: (...any) -> ...any,
DockContextQueueUndockWindow: (...any) -> ...any,
DockContextRebuildNodes: (...any) -> ...any,
DockContextRemoveNode: (...any) -> ...any,
DockContextShutdown: (...any) -> ...any,
DockNodeAddTabBar: (...any) -> ...any,
DockNodeAddWindow: (...any) -> ...any,
DockNodeApplyPosSizeToWindows: (...any) -> ...any,
DockNodeBeginAmendTabBar: (...any) -> ...any,
DockNodeCalcDropRectsAndTestMousePos: (...any) -> ...any,
DockNodeCalcSplitRects: (...any) -> ...any,
DockNodeCalcTabBarLayout: (...any) -> ...any,
DockNodeEndAmendTabBar: (...any) -> ...any,
DockNodeFindWindowByID: (...any) -> ...any,
DockNodeGetDepth: (...any) -> ...any,
DockNodeGetRootNode: (...any) -> ...any,
DockNodeGetTabOrder: (...any) -> ...any,
DockNodeGetWindowMenuButtonId: (...any) -> ...any,
DockNodeHideHostWindow: (...any) -> ...any,
DockNodeIsDropAllowed: (...any) -> ...any,
DockNodeIsInHierarchyOf: (...any) -> ...any,
DockNodeMoveChildNodes: (...any) -> ...any,
DockNodeMoveWindows: (...any) -> ...any,
DockNodePreviewDockRender: (...any) -> ...any,
DockNodePreviewDockSetup: (...any) -> ...any,
DockNodeRemoveTabBar: (...any) -> ...any,
DockNodeRemoveWindow: (...any) -> ...any,
DockNodeStartMouseMovingWindow: (...any) -> ...any,
DockNodeTreeFindFallbackLeafNode: (...any) -> ...any,
DockNodeTreeFindVisibleNodeByPos: (...any) -> ...any,
DockNodeTreeMerge: (...any) -> ...any,
DockNodeTreeSplit: (...any) -> ...any,
DockNodeTreeUpdatePosSize: (...any) -> ...any,
DockNodeTreeUpdateSplitter: (...any) -> ...any,
DockNodeUpdate: (...any) -> ...any,
DockNodeUpdateFlagsAndCollapse: (...any) -> ...any,
DockNodeUpdateForRootNode: (...any) -> ...any,
DockNodeUpdateHasCentralNodeChild: (...any) -> ...any,
DockNodeUpdateTabBar: (...any) -> ...any,
DockNodeUpdateVisibleFlag: (...any) -> ...any,
DockNodeWindowMenuHandler_Default: (...any) -> ...any,
DockNodeWindowMenuUpdate: (...any) -> ...any,
DockSpace: (dockspace_id: number, size_arg: ImVec2?, flags: number?, window_class: any) -> number,
DockSpaceOverViewport: (dockspace_id: number?, viewport: any, dockspace_flags: number?, window_class: any) -> number,
DragBehavior: (...any) -> ...any,
DragBehaviorT: (...any) -> ...any,
DragFloat: (label: string, v: number, v_speed: number?, v_min: number?, v_max: number?, format: string?, flags: number?) -> (number, boolean),
DragFloatRange2: (label: string, v_current_min: number, v_current_max: number, v_speed: number?, v_min: number?, v_max: number?, format: string?, format_max: string?, flags: number?) -> ...any,
DragInt: (label: string, v: number, v_speed: number?, v_min: number?, v_max: number?, format: string?, flags: number?) -> (number, boolean),
DragIntRange2: (label: string, v_current_min: number, v_current_max: number, v_speed: number?, v_min: number?, v_max: number?, format: string?, format_max: string?, flags: number?) -> ...any,
DragScalar: (label: string, data_type: number, data: any, v_speed: number?, min: any, max: any, format: string?, flags: number?) -> ...any,
DragScalarN: (label: string, data_type: number, v: any, components: number, v_speed: number?, v_min: any, v_max: any, format: string?, flags: number?) -> ...any,
Dummy: (size: ImVec2) -> (),
End: () -> (),
EndChild: () -> (),
EndColumns: (...any) -> ...any,
EndCombo: () -> (),
EndComboPreview: (...any) -> ...any,
EndDisabled: () -> (),
EndDisabledOverrideReenable: (...any) -> ...any,
EndDragDropSource: () -> (),
EndDragDropTarget: () -> (),
EndErrorTooltip: (...any) -> ...any,
EndFrame: () -> (),
EndGroup: () -> (),
EndListBox: () -> (),
EndMainMenuBar: () -> (),
EndMenu: () -> (),
EndMenuBar: () -> (),
EndPopup: () -> (),
EndTabBar: () -> (),
EndTabItem: () -> (),
EndTable: () -> (),
EndTooltip: () -> (),
ErrorCheckEndFrameFinalizeErrorTooltip: (...any) -> ...any,
ErrorCheckUsingSetCursorPosToExtendParentBoundaries: (...any) -> ...any,
ErrorLog: (...any) -> ...any,
ErrorRecoveryStoreState: (...any) -> ...any,
ErrorRecoveryTryToRecoverState: (...any) -> ...any,
ErrorRecoveryTryToRecoverWindowState: (...any) -> ...any,
ExtendHitBoxWhenNearViewportEdge: (...any) -> ...any,
FindBestWindowPosForPopup: (...any) -> ...any,
FindBestWindowPosForPopupEx: (...any) -> ...any,
FindBlockingModal: (...any) -> ...any,
FindBottomMostVisibleWindowWithinBeginStack: (...any) -> ...any,
FindFrontMostVisibleChildWindow: (...any) -> ...any,
FindHoveredViewportFromPlatformWindowStack: (...any) -> ...any,
FindOrCreateColumns: (...any) -> ...any,
FindPlatformMonitorForPos: (...any) -> ...any,
FindPlatformMonitorForRect: (...any) -> ...any,
FindRenderedTextEnd: (...any) -> ...any,
FindSettingsHandler: (...any) -> ...any,
FindViewportByID: (viewport_id: number) -> any,
FindWindowByID: (...any) -> ...any,
FindWindowByName: (...any) -> ...any,
FindWindowDisplayIndex: (...any) -> ...any,
FindWindowFocusIndex: (...any) -> ...any,
FindWindowNavFocusable: (...any) -> ...any,
FindWindowSettingsByID: (...any) -> ...any,
FindWindowSettingsByWindow: (...any) -> ...any,
FixupKeyChord: (...any) -> ...any,
FocusItem: (...any) -> ...any,
FocusTopMostWindowUnderOne: (...any) -> ...any,
FocusWindow: (...any) -> ...any,
GcAwakeTransientWindowBuffers: (...any) -> ...any,
GcCompactTransientMiscBuffers: (...any) -> ...any,
GcCompactTransientWindowBuffers: (...any) -> ...any,
GetBackgroundDrawList: (viewport: any) -> ...any,
GetClipboardText: () -> any,
GetColorU32: (...any) -> ...any,
GetColumnIndex: () -> number,
GetColumnNormFromOffset: (...any) -> ...any,
GetColumnOffset: (column_index: number?) -> number,
GetColumnOffsetFromNorm: (...any) -> ...any,
GetColumnWidth: (column_index: number?) -> number,
GetColumnsCount: () -> number,
GetColumnsID: (...any) -> ...any,
GetContentRegionAvail: () -> ImVec2,
GetContentRegionMax: (...any) -> ...any,
GetCurrentContext: () -> any,
GetCurrentWindow: (...any) -> ...any,
GetCurrentWindowRead: (...any) -> ...any,
GetCursorPos: () -> ImVec2,
GetCursorPosX: () -> number,
GetCursorPosY: () -> number,
GetCursorScreenPos: () -> ImVec2,
GetCursorStartPos: () -> ImVec2,
GetDefaultFont: (...any) -> ...any,
GetDragDropPayload: () -> any,
GetDrawData: () -> any,
GetDrawListSharedData: () -> any,
GetFont: () -> any,
GetFontSize: () -> number,
GetFontTexUvWhitePixel: () -> ImVec2,
GetForegroundDrawList: (viewport: any) -> ...any,
GetFrameCount: () -> number,
GetFrameHeight: () -> number,
GetFrameHeightWithSpacing: () -> number,
GetHoveredID: (...any) -> ...any,
GetID: (...any) -> ...any,
GetIDWithSeed: (...any) -> ...any,
GetIO: () -> any,
GetInputTextState: (...any) -> ...any,
GetItemClickedCountWithSingleClickDelay: (mouse_button: number?, delay: number?) -> number,
GetItemFlags: () -> number,
GetItemID: () -> number,
GetItemRectMax: () -> ImVec2,
GetItemRectMin: () -> ImVec2,
GetItemRectSize: () -> ImVec2,
GetKeyChordName: (...any) -> ...any,
GetKeyData: (...any) -> ...any,
GetKeyMagnitude2d: (...any) -> ...any,
GetKeyName: (key: number) -> any,
GetKeyOwner: (...any) -> ...any,
GetKeyOwnerData: (...any) -> ...any,
GetKeyPressedAmount: (key: number, repeat_delay: number, repeat_rate: number) -> number,
GetMainViewport: () -> any,
GetMergedModsFromKeys: (...any) -> ...any,
GetMetatables: (...any) -> ...any,
GetMouseButtonFromPopupFlags: (...any) -> ...any,
GetMouseClickedCount: (button: number) -> number,
GetMouseCursor: () -> number,
GetMouseDragDelta: (button: number?, lock_threshold: number?) -> ImVec2,
GetMousePos: () -> ImVec2,
GetMousePosOnOpeningCurrentPopup: () -> ImVec2,
GetNavTweakPressedAmount: (...any) -> ...any,
GetPlatformIO: () -> any,
GetPopupAllowedExtentRect: (...any) -> ...any,
GetRoundedFontSize: (...any) -> ...any,
GetScale: (...any) -> ...any,
GetScrollMaxX: () -> number,
GetScrollMaxY: () -> number,
GetScrollX: () -> number,
GetScrollY: () -> number,
GetShortcutRoutingData: (...any) -> ...any,
GetStateStorage: () -> any,
GetStyle: () -> any,
GetStyleColorName: (idx: number) -> any,
GetStyleColorVec4: (idx: number) -> ImVec4,
GetStyleVarInfo: (...any) -> ...any,
GetTextLineHeight: () -> number,
GetTextLineHeightWithSpacing: () -> number,
GetTime: () -> number,
GetTopMostAndVisiblePopupModal: (...any) -> ...any,
GetTopMostPopupModal: (...any) -> ...any,
GetTreeNodeToLabelSpacing: () -> number,
GetTypematicRepeatRate: (...any) -> ...any,
GetVersion: () -> any,
GetViewportPlatformMonitor: (...any) -> ...any,
GetWindowAlwaysWantOwnTabBar: (...any) -> ...any,
GetWindowAlwaysWantOwnViewport: (...any) -> ...any,
GetWindowContentRegionMax: (...any) -> ...any,
GetWindowContentRegionMin: (...any) -> ...any,
GetWindowDockID: () -> number,
GetWindowDpiScale: () -> number,
GetWindowDrawList: () -> any,
GetWindowHeight: () -> number,
GetWindowPos: () -> ImVec2,
GetWindowScrollbarID: (...any) -> ...any,
GetWindowScrollbarRect: (...any) -> ...any,
GetWindowSize: () -> ImVec2,
GetWindowViewport: () -> any,
GetWindowWidth: () -> number,
Image: (tex_ref: any, image_size: ImVec2, uv0: ImVec2?, uv1: ImVec2?) -> (),
ImageButton: (str_id: string, tex_ref: any, image_size: ImVec2, uv0: ImVec2?, uv1: ImVec2?, bg_col: ImVec4?, tint_col: ImVec4?) -> boolean,
ImageButtonEx: (...any) -> ...any,
ImageWithBg: (tex_ref: any, image_size: ImVec2, uv0: ImVec2?, uv1: ImVec2?, bg_col: ImVec4?, tint_col: ImVec4?) -> (),
Indent: (indent_w: number?) -> (),
Initialize: (...any) -> ...any,
InputDouble: (label: string, v: number, step: number?, step_fast: number?, format: string?, flags: number?) -> (number, boolean),
InputFloat: (label: string, v: number, step: number?, step_fast: number?, format: string?, flags: number?) -> (number, boolean),
InputFloat2: (label: string, v: any, format: string?, flags: number?) -> boolean,
InputFloat3: (label: string, v: any, format: string?, flags: number?) -> boolean,
InputFloat4: (label: string, v: any, format: string?, flags: number?) -> boolean,
InputInt: (label: string, v: number, step: number?, step_fast: number?, flags: number?) -> (number, boolean),
InputInt2: (label: string, v: any, flags: number?) -> boolean,
InputInt3: (label: string, v: any, flags: number?) -> boolean,
InputInt4: (label: string, v: any, flags: number?) -> boolean,
InputScalar: (label: string, data_type: number, data: any, step: any, step_fast: any, format: string?, flags: number?) -> ...any,
InputScalarN: (label: string, data_type: number, data: any, components: number, step: any, step_fast: any, format: string?, flags: number?) -> ...any,
InputText: (label: string, buf: any, buf_size: number, flags: number?, callback: any, user_data: any) -> ...any,
InputTextDeactivateHook: (...any) -> ...any,
InputTextEx: (...any) -> ...any,
InputTextMultiline: (label: string, buf: any, buf_size: number, size: ImVec2?, flags: number?, callback: any, user_data: any) -> ...any,
InputTextWithHint: (label: string, hint: string, buf: any, buf_size: number, flags: number?, callback: any, user_data: any) -> ...any,
InvisibleButton: (str_id: string, size_arg: ImVec2, flags: number?) -> boolean,
IsActiveIdUsingNavDir: (...any) -> ...any,
IsAliasKey: (...any) -> ...any,
IsAnyItemActive: () -> boolean,
IsAnyItemFocused: () -> boolean,
IsAnyItemHovered: () -> boolean,
IsAnyMouseDown: () -> boolean,
IsClippedEx: (...any) -> ...any,
IsDragDropActive: (...any) -> ...any,
IsDragDropPayloadBeingAccepted: (...any) -> ...any,
IsGamepadKey: (...any) -> ...any,
IsInNavFocusRoute: (...any) -> ...any,
IsItemActivated: () -> boolean,
IsItemActive: () -> boolean,
IsItemClicked: (mouse_button: number?) -> boolean,
IsItemDeactivated: () -> boolean,
IsItemDeactivatedAfterEdit: () -> boolean,
IsItemEdited: () -> boolean,
IsItemFocused: () -> boolean,
IsItemHovered: (flags: number?) -> boolean,
IsItemToggledOpen: () -> boolean,
IsItemToggledSelection: () -> boolean,
IsItemVisible: () -> boolean,
IsKeyChordPressed: (...any) -> ...any,
IsKeyDown: (...any) -> ...any,
IsKeyPressed: (key: number, is_repeat: boolean?) -> boolean,
IsKeyPressedEx: (...any) -> ...any,
IsKeyReleased: (...any) -> ...any,
IsKeyboardKey: (...any) -> ...any,
IsLRModKey: (...any) -> ...any,
IsMouseClicked: (button: number, is_repeat: boolean?) -> boolean,
IsMouseClickedEx: (...any) -> ...any,
IsMouseDoubleClicked: (...any) -> ...any,
IsMouseDown: (...any) -> ...any,
IsMouseDragPastThreshold: (...any) -> ...any,
IsMouseDragging: (button: number, lock_threshold: number?) -> boolean,
IsMouseHoveringRect: (r_min: ImVec2, r_max: ImVec2, clip: boolean?) -> boolean,
IsMouseKey: (...any) -> ...any,
IsMousePosValid: (mouse_pos: any) -> boolean,
IsMouseReleased: (...any) -> ...any,
IsMouseReleasedWithDelay: (button: number, delay: number?) -> boolean,
IsNamedKey: (...any) -> ...any,
IsNamedKeyOrMod: (...any) -> ...any,
IsPopupOpen: (id: string, popup_flags: number?) -> boolean,
IsPopupOpenRequestForItem: (...any) -> ...any,
IsPopupOpenRequestForWindow: (...any) -> ...any,
IsRectVisible: (...any) -> ...any,
IsViewportAbove: (...any) -> ...any,
IsWindowAbove: (...any) -> ...any,
IsWindowActiveAndVisible: (...any) -> ...any,
IsWindowAppearing: () -> boolean,
IsWindowChildOf: (...any) -> ...any,
IsWindowCollapsed: () -> boolean,
IsWindowContentHoverable: (...any) -> ...any,
IsWindowDocked: () -> boolean,
IsWindowFocused: (flags: number?) -> boolean,
IsWindowHovered: (flags: number?) -> boolean,
IsWindowInBeginStack: (...any) -> ...any,
IsWindowNavFocusable: (...any) -> ...any,
IsWindowWithinBeginStackOf: (...any) -> ...any,
ItemAdd: (...any) -> ...any,
ItemHandleShortcut: (...any) -> ...any,
ItemHoverable: (...any) -> ...any,
ItemSize: (...any) -> ...any,
KeepAliveID: (...any) -> ...any,
LabelText: (label: string, fmt: string, ...any) -> (),
ListBox: (...any) -> ...any,
LoadIniSettingsFromDisk: (ini_filename: string) -> (),
LoadIniSettingsFromMemory: (ini_data: string, ini_size: number?) -> (),
LocalizeGetMsg: (...any) -> ...any,
LocalizeRegisterEntries: (...any) -> ...any,
LogBegin: (...any) -> ...any,
LogButtons: () -> (),
LogFinish: () -> (),
LogRenderedText: (...any) -> ...any,
LogSetNextTextDecoration: (...any) -> ...any,
LogText: (fmt: string, ...any) -> (),
LogToBuffer: (...any) -> ...any,
LogToClipboard: (auto_open_depth: number?) -> (),
LogToFile: (auto_open_depth: number?, filename: string?) -> (),
LogToTTY: (auto_open_depth: number?) -> (),
MarkIniSettingsDirty: (...any) -> ...any,
MarkItemEdited: (...any) -> ...any,
MemAlloc: (...any) -> ...any,
MemFree: (...any) -> ...any,
MenuItem: (...any) -> ...any,
MenuItemEx: (...any) -> ...any,
MouseButtonToKey: (...any) -> ...any,
NavApplyItemToResult: (...any) -> ...any,
NavBiasScoringRect: (...any) -> ...any,
NavCalcPreferredRefPos: (...any) -> ...any,
NavCalcPreferredRefPosSource: (...any) -> ...any,
NavClearPreferredPosForAxis: (...any) -> ...any,
NavEndFrame: (...any) -> ...any,
NavHighlightActivated: (...any) -> ...any,
NavInitRequestApplyResult: (...any) -> ...any,
NavInitWindow: (...any) -> ...any,
NavMoveRequestApplyResult: (...any) -> ...any,
NavMoveRequestButNoResultYet: (...any) -> ...any,
NavMoveRequestCancel: (...any) -> ...any,
NavMoveRequestForward: (...any) -> ...any,
NavMoveRequestResolveWithLastItem: (...any) -> ...any,
NavMoveRequestResolveWithPastTreeNode: (...any) -> ...any,
NavMoveRequestSubmit: (...any) -> ...any,
NavMoveRequestTryWrapping: (...any) -> ...any,
NavProcessItem: (...any) -> ...any,
NavProcessItemForTabbingRequest: (...any) -> ...any,
NavRestoreLastChildNavWindow: (...any) -> ...any,
NavRestoreLayer: (...any) -> ...any,
NavSaveLastChildNavWindowIntoParent: (...any) -> ...any,
NavScoreItem: (...any) -> ...any,
NavUpdate: (...any) -> ...any,
NavUpdateAnyRequestFlag: (...any) -> ...any,
NavUpdateCancelRequest: (...any) -> ...any,
NavUpdateContextMenuRequest: (...any) -> ...any,
NavUpdateCreateMoveRequest: (...any) -> ...any,
NavUpdateCreateTabbingRequest: (...any) -> ...any,
NavUpdateCurrentWindowIsScrollPushableX: (...any) -> ...any,
NavUpdatePageUpPageDown: (...any) -> ...any,
NavUpdateWindowing: (...any) -> ...any,
NavUpdateWindowingApplyFocus: (...any) -> ...any,
NavUpdateWindowingOverlay: (...any) -> ...any,
NavUpdateWindowingTarget: (...any) -> ...any,
NewFrame: () -> (),
NewLine: () -> (),
NewTextBuffer: (str: string, size: number?) -> {number},
NextColumn: () -> (),
OpenPopup: (...any) -> ...any,
OpenPopupEx: (...any) -> ...any,
OpenPopupOnItemClick: (str_id: string?, popup_flags: number?) -> boolean,
PayloadIsDataType: (...any) -> ...any,
PlotEx: (...any) -> ...any,
PlotHistogram: (...any) -> ...any,
PlotLines: (...any) -> ...any,
PopClipRect: () -> (),
PopColumnsBackground: (...any) -> ...any,
PopFocusScope: (...any) -> ...any,
PopFont: () -> (),
PopID: () -> (),
PopItemFlag: () -> (),
PopItemWidth: () -> (),
PopPasswordFont: (...any) -> ...any,
PopStyleColor: (count: number?) -> (),
PopStyleVar: (count: number?) -> (),
PopTextWrapPos: () -> (),
ProgressBar: (fraction: number, size_arg: ImVec2?, overlay: string?) -> (),
PushClipRect: (clip_rect_min: ImVec2, clip_rect_max: ImVec2, intersect_with_current_clip_rect: boolean) -> (),
PushColumnClipRect: (...any) -> ...any,
PushColumnsBackground: (...any) -> ...any,
PushFocusScope: (...any) -> ...any,
PushFont: (font: any, font_size_base: number) -> ...any,
PushID: (...any) -> ...any,
PushItemFlag: (option: number, enabled: boolean) -> (),
PushItemWidth: (item_width: number) -> (),
PushMultiItemsWidths: (...any) -> ...any,
PushOverrideID: (...any) -> ...any,
PushPasswordFont: (...any) -> ...any,
PushStyleColor: (...any) -> ...any,
PushStyleVar: (...any) -> ...any,
PushStyleVarX: (idx: number, val_x: number) -> (),
PushStyleVarY: (idx: number, val_y: number) -> (),
PushTextWrapPos: (wrap_local_pos_x: number?) -> (),
RadioButton: (...any) -> ...any,
RadioButtonEx: (...any) -> ...any,
RegisterFontAtlas: (...any) -> ...any,
RemoveSettingsHandler: (...any) -> ...any,
Render: () -> (),
RenderArrow: (...any) -> ...any,
RenderArrowDockMenu: (...any) -> ...any,
RenderArrowPointingAt: (...any) -> ...any,
RenderBullet: (...any) -> ...any,
RenderCheckMark: (...any) -> ...any,
RenderColorComponentMarker: (...any) -> ...any,
RenderColorRectWithAlphaCheckerboard: (...any) -> ...any,
RenderDimmedBackgroundBehindWindow: (...any) -> ...any,
RenderDimmedBackgrounds: (...any) -> ...any,
RenderDragDropTargetRectEx: (...any) -> ...any,
RenderDragDropTargetRectForItem: (...any) -> ...any,
RenderDragDropTargetRectForViewport: (...any) -> ...any,
RenderFrame: (...any) -> ...any,
RenderFrameBorder: (...any) -> ...any,
RenderMouseCursor: (...any) -> ...any,
RenderNavCursor: (...any) -> ...any,
RenderPlatformWindowsDefault: (platform_render_arg: any, renderer_render_arg: any) -> ...any,
RenderRectFilledInRangeH: (...any) -> ...any,
RenderRectFilledWithHole: (...any) -> ...any,
RenderText: (...any) -> ...any,
RenderTextClipped: (...any) -> ...any,
RenderTextClippedEx: (...any) -> ...any,
RenderTextEllipsis: (...any) -> ...any,
RenderTextWrapped: (...any) -> ...any,
RenderViewportsThumbnails: (...any) -> ...any,
ResetMouseDragDelta: (button: number?) -> (),
RoundScalarWithFormatT: (...any) -> ...any,
SameLine: (offset_from_start_x: number?, spacing_w: number?) -> (),
SaveIniSettingsToDisk: (ini_filename: string) -> (),
SaveIniSettingsToMemory: (...any) -> ...any,
ScaleRatioFromValueT: (...any) -> ...any,
ScaleValueFromRatioT: (...any) -> ...any,
ScaleWindowsInViewport: (...any) -> ...any,
ScrollToItem: (...any) -> ...any,
ScrollToRect: (...any) -> ...any,
ScrollToRectEx: (...any) -> ...any,
Scrollbar: (...any) -> ...any,
ScrollbarEx: (...any) -> ...any,
Selectable: (...any) -> ...any,
Separator: () -> (),
SeparatorEx: (...any) -> ...any,
SeparatorText: (label: string) -> (),
SeparatorTextEx: (...any) -> ...any,
SetActiveID: (...any) -> ...any,
SetActiveIdUsingAllKeyboardKeys: (...any) -> ...any,
SetAllocatorFunctions: (alloc_func: any, free_func: any, user_data: any) -> ...any,
SetClipboardText: (text: string) -> (),
SetColorEditOptions: (...any) -> ...any,
SetColumnOffset: (column_index: number, offset: number) -> (),
SetColumnWidth: (column_index: number, width: number) -> (),
SetCurrentContext: (ctx: any) -> ...any,
SetCurrentFont: (...any) -> ...any,
SetCurrentViewport: (...any) -> ...any,
SetCursorPos: (local_pos: ImVec2) -> (),
SetCursorPosX: (x: number) -> (),
SetCursorPosY: (y: number) -> (),
SetCursorScreenPos: (pos: ImVec2) -> (),
SetDragDropPayload: (type: string, data: any, data_size: number, cond: number?) -> boolean,
SetFocusID: (...any) -> ...any,
SetHoveredID: (...any) -> ...any,
SetItemDefaultFocus: () -> (),
SetItemKeyOwner: (...any) -> ...any,
SetItemTooltip: (fmt: string, ...any) -> (),
SetKeyOwner: (...any) -> ...any,
SetKeyOwnersForKeyChord: (...any) -> ...any,
SetKeyboardFocusHere: (offset: number?) -> (),
SetLastItemData: (...any) -> ...any,
SetLastItemDataForChildWindowItem: (...any) -> ...any,
SetLastItemDataForWindow: (...any) -> ...any,
SetMouseCursor: (cursor_type: number) -> (),
SetNavCursorVisible: (visible: boolean) -> (),
SetNavCursorVisibleAfterMove: (...any) -> ...any,
SetNavFocusScope: (...any) -> ...any,
SetNavID: (...any) -> ...any,
SetNavWindow: (...any) -> ...any,
SetNextFrameWantCaptureKeyboard: (want: boolean) -> (),
SetNextFrameWantCaptureMouse: (want: boolean) -> (),
SetNextItemAllowOverlap: () -> (),
SetNextItemColorMarker: (...any) -> ...any,
SetNextItemOpen: (is_open: boolean, cond: number?) -> (),
SetNextItemRefVal: (...any) -> ...any,
SetNextItemShortcut: (key_chord: any, flags: number?) -> (),
SetNextItemStorageID: (storage_id: number) -> (),
SetNextItemWidth: (item_width: number) -> (),
SetNextWindowBgAlpha: (alpha: number) -> (),
SetNextWindowCached: (key: any) -> (),
SetNextWindowChildFlags: (...any) -> ...any,
SetNextWindowClass: (window_class: any) -> (),
SetNextWindowCollapsed: (collapsed: boolean, cond: number?) -> (),
SetNextWindowContentSize: (size: ImVec2) -> (),
SetNextWindowDockID: (id: number, cond: number?) -> (),
SetNextWindowFlags: (...any) -> ...any,
SetNextWindowFocus: () -> (),
SetNextWindowPos: (pos: ImVec2, cond: number?, pivot: ImVec2?) -> (),
SetNextWindowRefreshPolicy: (...any) -> ...any,
SetNextWindowScroll: (scroll: ImVec2) -> (),
SetNextWindowSize: (size: ImVec2, cond: number?) -> (),
SetNextWindowSizeConstraints: (size_min: ImVec2, size_max: ImVec2, custom_callback: any, custom_callback_user_data: any) -> ...any,
SetNextWindowViewport: (id: number) -> (),
SetScrollFromPosX: (...any) -> ...any,
SetScrollFromPosY: (...any) -> ...any,
SetScrollHereX: (center_x_ratio: number?) -> (),
SetScrollHereY: (center_y_ratio: number?) -> (),
SetScrollX: (...any) -> ...any,
SetScrollY: (...any) -> ...any,
SetShortcutRouting: (...any) -> ...any,
SetStateStorage: (tree: any) -> ...any,
SetTabItemClosed: (label: string) -> (),
SetTooltip: (fmt: string, ...any) -> (),
SetWindowClipRectBeforeSetChannel: (...any) -> ...any,
SetWindowCollapsed: (...any) -> ...any,
SetWindowDock: (...any) -> ...any,
SetWindowFocus: (...any) -> ...any,
SetWindowFontScale: (...any) -> ...any,
SetWindowHiddenAndSkipItemsForCurrentFrame: (...any) -> ...any,
SetWindowHitTestHole: (...any) -> ...any,
SetWindowPos: (...any) -> ...any,
SetWindowSize: (...any) -> ...any,
SetWindowViewport: (...any) -> ...any,
ShadeVertsLinearColorGradientKeepAlpha: (...any) -> ...any,
ShadeVertsLinearUV: (...any) -> ...any,
ShadeVertsTransformPos: (...any) -> ...any,
Shortcut: (...any) -> ...any,
ShowAboutWindow: (p_open: boolean?) -> ...any,
ShowDebugLogWindow: (p_open: boolean?) -> ...any,
ShowDemoWindow: (p_open: boolean?) -> ...any,
ShowFontAtlas: (...any) -> ...any,
ShowFontSelector: (label: string) -> (),
ShowIDStackToolWindow: (p_open: boolean?) -> ...any,
ShowMetricsWindow: (p_open: boolean?) -> ...any,
ShowStyleEditor: (ref: any) -> ...any,
ShowStyleSelector: (label: string) -> boolean,
ShowUserGuide: () -> (),
ShrinkWidths: (...any) -> ...any,
Shutdown: (...any) -> ...any,
SliderAngle: (label: string, v_rad: number, v_degrees_min: number?, v_degrees_max: number?, format: string?, flags: number?) -> (number, boolean),
SliderBehavior: (...any) -> ...any,
SliderBehaviorT: (...any) -> ...any,
SliderFloat: (label: string, v: number, v_min: number, v_max: number, format: string?, flags: number?) -> (number, boolean),
SliderFloat2: (label: string, v: any, v_min: number, v_max: number, format: string?, flags: number?) -> boolean,
SliderFloat3: (label: string, v: any, v_min: number, v_max: number, format: string?, flags: number?) -> boolean,
SliderFloat4: (label: string, v: any, v_min: number, v_max: number, format: string?, flags: number?) -> boolean,
SliderInt: (label: string, v: number, v_min: number, v_max: number, format: string?, flags: number?) -> (number, boolean),
SliderInt2: (label: string, v: any, v_min: number, v_max: number, format: string?, flags: number?) -> boolean,
SliderInt3: (label: string, v: any, v_min: number, v_max: number, format: string?, flags: number?) -> boolean,
SliderInt4: (label: string, v: any, v_min: number, v_max: number, format: string?, flags: number?) -> boolean,
SliderScalar: (label: string, data_type: number, data: any, min: any, max: any, format: string?, flags: number?) -> ...any,
SliderScalarN: (label: string, data_type: number, v: any, components: number, v_min: any, v_max: any, format: string?, flags: number?) -> ...any,
SmallButton: (label: string) -> boolean,
Spacing: () -> (),
SplitterBehavior: (...any) -> ...any,
StartMouseMovingWindow: (...any) -> ...any,
StartMouseMovingWindowOrNode: (...any) -> ...any,
StopMouseMovingWindow: (...any) -> ...any,
StyleColorsClassic: (dst: any) -> ...any,
StyleColorsDark: (dst: any) -> ...any,
StyleColorsLight: (dst: any) -> ...any,
TabBarAddTab: (...any) -> ...any,
TabBarCalcMaxTabWidth: (...any) -> ...any,
TabBarCalcTabID: (...any) -> ...any,
TabBarCloseTab: (...any) -> ...any,
TabBarFindByID: (...any) -> ...any,
TabBarFindMostRecentlySelectedTabForActiveWindow: (...any) -> ...any,
TabBarFindTabByID: (...any) -> ...any,
TabBarFindTabByOrder: (...any) -> ...any,
TabBarGetCurrentTab: (...any) -> ...any,
TabBarGetTabName: (...any) -> ...any,
TabBarGetTabOrder: (...any) -> ...any,
TabBarGetTabPos: (...any) -> ...any,
TabBarLayout: (...any) -> ...any,
TabBarProcessReorder: (...any) -> ...any,
TabBarQueueFocus: (...any) -> ...any,
TabBarQueueReorder: (...any) -> ...any,
TabBarQueueReorderFromMousePos: (...any) -> ...any,
TabBarRemove: (...any) -> ...any,
TabBarRemoveTab: (...any) -> ...any,
TabBarScrollClamp: (...any) -> ...any,
TabBarScrollToTab: (...any) -> ...any,
TabBarScrollingButtons: (...any) -> ...any,
TabBarTabListPopupButton: (...any) -> ...any,
TabItemBackground: (...any) -> ...any,
TabItemButton: (label: string, flags: number?) -> boolean,
TabItemCalcSize: (...any) -> ...any,
TabItemEx: (...any) -> ...any,
TabItemLabelAndCloseButton: (...any) -> ...any,
TabItemSpacing: (...any) -> ...any,
TableAngledHeadersRow: () -> (),
TableAngledHeadersRowEx: (...any) -> ...any,
TableApplyExternalUnclipRect: (...any) -> ...any,
TableApplyQueuedRequests: (...any) -> ...any,
TableBeginCell: (...any) -> ...any,
TableBeginContextMenuPopup: (...any) -> ...any,
TableBeginInitMemory: (...any) -> ...any,
TableBeginRow: (...any) -> ...any,
TableCalcMaxColumnWidth: (...any) -> ...any,
TableDrawBorders: (...any) -> ...any,
TableDrawDefaultContextMenu: (...any) -> ...any,
TableEndCell: (...any) -> ...any,
TableEndRow: (...any) -> ...any,
TableFindByID: (...any) -> ...any,
TableFixColumnSortDirection: (...any) -> ...any,
TableFixDisplayOrder: (...any) -> ...any,
TableGcCompactSettings: (...any) -> ...any,
TableGcCompactTransientBuffers: (...any) -> ...any,
TableGetBoundSettings: (...any) -> ...any,
TableGetCellBgRect: (...any) -> ...any,
TableGetColumnCount: () -> number,
TableGetColumnFlags: (column_n: number?) -> number,
TableGetColumnIndex: () -> number,
TableGetColumnName: (...any) -> ...any,
TableGetColumnNextSortDirection: (...any) -> ...any,
TableGetColumnResizeID: (...any) -> ...any,
TableGetColumnWidthAuto: (...any) -> ...any,
TableGetHeaderAngledMaxLabelWidth: (...any) -> ...any,
TableGetHeaderRowHeight: (...any) -> ...any,
TableGetHoveredColumn: () -> number,
TableGetHoveredRow: (...any) -> ...any,
TableGetInstanceData: (...any) -> ...any,
TableGetInstanceID: (...any) -> ...any,
TableGetRowIndex: () -> number,
TableGetSortSpecs: () -> any,
TableHeader: (label: string) -> (),
TableHeadersRow: () -> (),
TableInitColumnDefaults: (...any) -> ...any,
TableLoadSettings: (...any) -> ...any,
TableLoadSettingsForColumn: (...any) -> ...any,
TableLoadSettingsForColumns: (...any) -> ...any,
TableMergeDrawChannels: (...any) -> ...any,
TableNextColumn: () -> boolean,
TableNextRow: (row_flags: number?, row_min_height: number?) -> (),
TableOpenContextMenu: (...any) -> ...any,
TablePopBackgroundChannel: (...any) -> ...any,
TablePopColumnChannel: (...any) -> ...any,
TablePushBackgroundChannel: (...any) -> ...any,
TablePushColumnChannel: (...any) -> ...any,
TableQueueSetColumnDisplayOrder: (...any) -> ...any,
TableReconcileColumns: (...any) -> ...any,
TableRemove: (...any) -> ...any,
TableResetSettings: (...any) -> ...any,
TableSaveSettings: (...any) -> ...any,
TableSetBgColor: (target: number, color: number, column_n: number?) -> (),
TableSetColumnDisplayOrder: (...any) -> ...any,
TableSetColumnEnabled: (column_n: number, enabled: boolean) -> (),
TableSetColumnIndex: (column_n: number) -> boolean,
TableSetColumnSortDirection: (...any) -> ...any,
TableSetColumnWidth: (...any) -> ...any,
TableSetColumnWidthAutoAll: (...any) -> ...any,
TableSetColumnWidthAutoSingle: (...any) -> ...any,
TableSettingsAddSettingsHandler: (...any) -> ...any,
TableSettingsCreate: (...any) -> ...any,
TableSettingsFindByID: (...any) -> ...any,
TableSetupColumn: (label: string, flags: number?, init_width_or_weight: number?, user_data: number?) -> (),
TableSetupDrawChannels: (...any) -> ...any,
TableSetupScrollFreeze: (columns: number, rows: number) -> (),
TableSortSpecsBuild: (...any) -> ...any,
TableSortSpecsSanitize: (...any) -> ...any,
TableUpdateBorders: (...any) -> ...any,
TableUpdateColumnsWeightFromWidth: (...any) -> ...any,
TableUpdateLayout: (...any) -> ...any,
TeleportMousePos: (...any) -> ...any,
TempInputIsActive: (...any) -> ...any,
TempInputScalar: (...any) -> ...any,
TempInputText: (...any) -> ...any,
TestKeyOwner: (...any) -> ...any,
Text: (fmt: string, ...any) -> (),
TextAligned: (...any) -> ...any,
TextBufferToString: (buf: {number}) -> string,
TextColored: (col: ImVec4, fmt: string, ...any) -> (),
TextDisabled: (fmt: string, ...any) -> (),
TextEx: (...any) -> ...any,
TextLink: (label: string) -> boolean,
TextLinkOpenURL: (label: string, url: string?) -> boolean,
TextUnformatted: (text: string, text_end: string?) -> (),
TextV: (fmt: string, ...any) -> (),
TextWrapped: (fmt: string, ...any) -> (),
TranslateWindow: (...any) -> ...any,
TranslateWindowsInViewport: (...any) -> ...any,
TreeNode: (...any) -> ...any,
TreeNodeBehavior: (...any) -> ...any,
TreeNodeDrawLineToChildNode: (...any) -> ...any,
TreeNodeDrawLineToTreePop: (...any) -> ...any,
TreeNodeEx: (...any) -> ...any,
TreeNodeExV: (...any) -> ...any,
TreeNodeGetOpen: (storage_id: number) -> boolean,
TreeNodeSetOpen: (...any) -> ...any,
TreeNodeUpdateNextOpen: (...any) -> ...any,
TreeNodeV: (...any) -> ...any,
TreePop: () -> (),
TreePush: (...any) -> ...any,
TreePushOverrideID: (...any) -> ...any,
Unindent: (indent_w: number?) -> (),
UnregisterFontAtlas: (...any) -> ...any,
UpdateAliasKey: (...any) -> ...any,
UpdateCurrentFontSize: (...any) -> ...any,
UpdateDebugToolItemPathQuery: (...any) -> ...any,
UpdateFontsEndFrame: (...any) -> ...any,
UpdateFontsNewFrame: (...any) -> ...any,
UpdateHoveredWindowAndCaptureFlags: (...any) -> ...any,
UpdateInputEvents: (...any) -> ...any,
UpdateKeyRoutingTable: (...any) -> ...any,
UpdateKeyboardInputs: (...any) -> ...any,
UpdateMouseInputs: (...any) -> ...any,
UpdateMouseMovingWindowEndFrame: (...any) -> ...any,
UpdateMouseMovingWindowNewFrame: (...any) -> ...any,
UpdateMouseWheel: (...any) -> ...any,
UpdatePlatformWindows: () -> (),
UpdateSettings: (...any) -> ...any,
UpdateTexturesEndFrame: (...any) -> ...any,
UpdateTexturesNewFrame: (...any) -> ...any,
UpdateTryMergeWindowIntoHostViewport: (...any) -> ...any,
UpdateTryMergeWindowIntoHostViewports: (...any) -> ...any,
UpdateViewportPlatformMonitor: (...any) -> ...any,
UpdateViewportsEndFrame: (...any) -> ...any,
UpdateViewportsNewFrame: (...any) -> ...any,
UpdateWindowInFocusOrderList: (...any) -> ...any,
UpdateWindowParentAndRootLinks: (...any) -> ...any,
UpdateWindowSkipRefresh: (...any) -> ...any,
VSliderFloat: (label: string, size: ImVec2, v: number, v_min: number, v_max: number, format: string?, flags: number?) -> (number, boolean),
VSliderInt: (label: string, size: ImVec2, v: number, v_min: number, v_max: number, format: string?, flags: number?) -> (number, boolean),
VSliderScalar: (label: string, size: ImVec2, data_type: number, data: any, min: any, max: any, format: string?, flags: number?) -> ...any,
Value: (...any) -> ...any,
WindowPosAbsToRel: (...any) -> ...any,
WindowRectAbsToRel: (...any) -> ...any,
WindowRectRelToAbs: (...any) -> ...any,
WindowSelectViewport: (...any) -> ...any,
WindowSettingsAddSettingsHandler: (...any) -> ...any,
WindowSyncOwnedViewport: (...any) -> ...any,
_ByteArrayToString: (...any) -> ...any,
_GetTypeFunc: (...any) -> ...any,
_SetCurrentContext_Internal: (...any) -> ...any,
_SetCurrentContext_Tables: (...any) -> ...any,
_SetCurrentContext_Widgets: (...any) -> ...any,
[string]: any,
}
export type ImDrawFlags = { None: number, RoundCornersTopLeft: number, RoundCornersTopRight: number, RoundCornersBottomLeft: number, RoundCornersBottomRight: number, RoundCornersNone: number, Closed: number, RoundCornersTop: number, RoundCornersBottom: number, RoundCornersLeft: number, RoundCornersRight: number, RoundCornersAll: number, RoundCornersDefault_: number, RoundCornersMask_: number, InvalidMask_: number, [string]: number }
export type ImDrawListFlags = { None: number, AntiAliasedLines: number, AntiAliasedLinesUseTex: number, AntiAliasedFill: number, AllowVtxOffset: number, TextNoPixelSnap: number, [string]: number }
export type ImFontFlags = { None: number, NoLoadError: number, NoLoadGlyphs: number, LockBakedSizes: number, ImplicitRefSize: number, [string]: number }
export type ImGuiBackendFlags = { None: number, HasGamepad: number, HasMouseCursors: number, HasSetMousePos: number, RendererHasVtxOffset: number, RendererHasTextures: number, RendererHasViewports: number, PlatformHasViewports: number, HasMouseHoveredViewport: number, HasParentViewport: number, [string]: number }
export type ImGuiButtonFlags = { None: number, MouseButtonLeft: number, MouseButtonRight: number, MouseButtonMiddle: number, EnableNav: number, PressedOnClick: number, PressedOnClickRelease: number, PressedOnClickReleaseAnywhere: number, PressedOnRelease: number, PressedOnDoubleClick: number, PressedOnDragDropHold: number, FlattenChildren: number, AllowOverlap: number, AlignTextBaseLine: number, NoKeyModsAllowed: number, NoHoldingActiveId: number, NoNavFocus: number, NoHoveredOnFocus: number, NoSetKeyOwner: number, NoTestKeyOwner: number, NoFocus: number, MouseButtonMask_: number, PressedOnMask_: number, PressedOnDefault_: number, NoKeyModifiers: number, [string]: number }
export type ImGuiChildFlags = { None: number, Borders: number, AlwaysUseWindowPadding: number, ResizeX: number, ResizeY: number, AutoResizeX: number, AutoResizeY: number, AlwaysAutoResize: number, FrameStyle: number, NavFlattened: number, ResizeBoth: number, ResizeXAndY: number, [string]: number }
export type ImGuiCol = { Text: number, TextDisabled: number, WindowBg: number, ChildBg: number, PopupBg: number, Border: number, BorderShadow: number, FrameBg: number, FrameBgHovered: number, FrameBgActive: number, TitleBg: number, TitleBgActive: number, TitleBgCollapsed: number, MenuBarBg: number, ScrollbarBg: number, ScrollbarGrab: number, ScrollbarGrabHovered: number, ScrollbarGrabActive: number, CheckMark: number, CheckboxSelectedBg: number, SliderGrab: number, SliderGrabActive: number, Button: number, ButtonHovered: number, ButtonActive: number, Header: number, HeaderHovered: number, HeaderActive: number, Separator: number, SeparatorHovered: number, SeparatorActive: number, ResizeGrip: number, ResizeGripHovered: number, ResizeGripActive: number, InputTextCursor: number, TabHovered: number, Tab: number, TabSelected: number, TabSelectedOverline: number, TabDimmed: number, TabDimmedSelected: number, TabDimmedSelectedOverline: number, DockingPreview: number, DockingEmptyBg: number, PlotLines: number, PlotLinesHovered: number, PlotHistogram: number, PlotHistogramHovered: number, TableHeaderBg: number, TableBorderStrong: number, TableBorderLight: number, TableRowBg: number, TableRowBgAlt: number, TextLink: number, TextSelectedBg: number, TreeLines: number, DragDropTarget: number, DragDropTargetBg: number, UnsavedMarker: number, NavCursor: number, NavWindowingHighlight: number, NavWindowingDimBg: number, ModalWindowDimBg: number, COUNT: number, [string]: number }
export type ImGuiColorEditFlags = { None: number, NoAlpha: number, NoPicker: number, NoOptions: number, NoSmallPreview: number, NoInputs: number, NoTooltip: number, NoLabel: number, NoSidePreview: number, NoDragDrop: number, NoBorder: number, NoColorMarkers: number, AlphaOpaque: number, AlphaNoBg: number, AlphaPreviewHalf: number, AlphaBar: number, HDR: number, DisplayRGB: number, DisplayHSV: number, DisplayHex: number, Uint8: number, Float: number, PickerHueBar: number, PickerHueWheel: number, PickerNoRotate: number, InputRGB: number, InputHSV: number, DefaultOptions_: number, AlphaMask_: number, DisplayMask_: number, DataTypeMask_: number, PickerMask_: number, InputMask_: number, [string]: number }
export type ImGuiComboFlags = { None: number, PopupAlignLeft: number, HeightSmall: number, HeightRegular: number, HeightLarge: number, HeightLargest: number, NoArrowButton: number, NoPreview: number, WidthFitPreview: number, HeightMask_: number, CustomPreview: number, [string]: number }
export type ImGuiCond = { None: number, Always: number, Once: number, FirstUseEver: number, Appearing: number, [string]: number }
export type ImGuiConfigFlags = { None: number, NavEnableKeyboard: number, NavEnableGamepad: number, NoMouse: number, NoMouseCursorChange: number, NoKeyboard: number, DockingEnable: number, ViewportsEnable: number, IsSRGB: number, IsTouchScreen: number, [string]: number }
export type ImGuiDragDropFlags = { None: number, SourceNoPreviewTooltip: number, SourceNoDisableHover: number, SourceNoHoldToOpenOthers: number, SourceAllowNullID: number, SourceExtern: number, PayloadAutoExpire: number, PayloadNoCrossContext: number, PayloadNoCrossProcess: number, AcceptBeforeDelivery: number, AcceptNoDrawDefaultRect: number, AcceptNoPreviewTooltip: number, AcceptDrawAsHovered: number, AcceptPeekOnly: number, [string]: number }
export type ImGuiFocusedFlags = { None: number, ChildWindows: number, RootWindow: number, AnyWindow: number, NoPopupHierarchy: number, DockHierarchy: number, RootAndChildWindows: number, [string]: number }
export type ImGuiHoveredFlags = { None: number, ChildWindows: number, RootWindow: number, AnyWindow: number, NoPopupHierarchy: number, DockHierarchy: number, AllowWhenBlockedByPopup: number, AllowWhenBlockedByActiveItem: number, AllowWhenOverlappedByItem: number, AllowWhenOverlappedByWindow: number, AllowWhenDisabled: number, NoNavOverride: number, ForTooltip: number, Stationary: number, DelayNone: number, DelayShort: number, DelayNormal: number, NoSharedDelay: number, AllowWhenOverlapped: number, RectOnly: number, RootAndChildWindows: number, DelayMask_: number, AllowedMaskForIsWindowHovered: number, AllowedMaskForIsItemHovered: number, [string]: number }
export type ImGuiInputFlags = { None: number, Repeat: number, RouteActive: number, RouteFocused: number, RouteGlobal: number, RouteAlways: number, RouteOverFocused: number, RouteOverActive: number, RouteUnlessBgFocused: number, RouteFromRootWindow: number, Tooltip: number, RepeatRateDefault: number, RepeatRateNavMove: number, RepeatRateNavTweak: number, RepeatUntilRelease: number, RepeatUntilKeyModsChange: number, RepeatUntilKeyModsChangeFromNone: number, RepeatUntilOtherKeyPress: number, LockThisFrame: number, LockUntilRelease: number, CondHovered: number, CondActive: number, CondDefault_: number, RepeatRateMask_: number, RepeatUntilMask_: number, RepeatMask_: number, CondMask_: number, RouteTypeMask_: number, RouteOptionsMask_: number, SupportedByIsKeyPressed: number, SupportedByIsMouseClicked: number, SupportedByShortcut: number, SupportedBySetNextItemShortcut: number, SupportedBySetKeyOwner: number, SupportedBySetItemKeyOwner: number, [string]: number }
export type ImGuiInputTextFlags = { None: number, CharsDecimal: number, CharsHexadecimal: number, CharsScientific: number, CharsUppercase: number, CharsNoBlank: number, AllowTabInput: number, EnterReturnsTrue: number, EscapeClearsAll: number, CtrlEnterForNewLine: number, ReadOnly: number, Password: number, AlwaysOverwrite: number, AutoSelectAll: number, ParseEmptyRefVal: number, DisplayEmptyRefVal: number, NoHorizontalScroll: number, NoUndoRedo: number, ElideLeft: number, CallbackCompletion: number, CallbackHistory: number, CallbackAlways: number, CallbackCharFilter: number, CallbackResize: number, CallbackEdit: number, WordWrap: number, Multiline: number, TempInput: number, LocalizeDecimalPoint: number, [string]: number }
export type ImGuiItemFlags = { None: number, NoTabStop: number, NoNav: number, NoNavDefaultFocus: number, ButtonRepeat: number, AutoClosePopups: number, AllowDuplicateId: number, Disabled: number, LiveEditOnInputText: number, LiveEditOnInputScalar: number, LiveEditOnInput: number, ReadOnly: number, MixedValue: number, NoWindowHoverableCheck: number, AllowOverlap: number, NoNavDisableMouseHover: number, NoMarkEdited: number, NoFocus: number, Inputable: number, HasSelectionUserData: number, IsMultiSelect: number, Default_: number, [string]: number }
export type ImGuiItemStatusFlags = { None: number, HoveredRect: number, HasDisplayRect: number, Edited: number, ToggledSelection: number, ToggledOpen: number, HasDeactivated: number, Deactivated: number, HoveredWindow: number, Visible: number, HasClipRect: number, HasShortcut: number, EditedInternal: number, [string]: number }
export type ImGuiKey = { None: number, NamedKey_BEGIN: number, Tab: number, LeftArrow: number, RightArrow: number, UpArrow: number, DownArrow: number, PageUp: number, PageDown: number, Home: number, End: number, Insert: number, Delete: number, Backspace: number, Space: number, Enter: number, Escape: number, LeftCtrl: number, RightCtrl: number, Menu: number, K0: number, A: number, K: number, U: number, F1: number, F7: number, F13: number, F19: number, Apostrophe: number, CapsLock: number, Keypad0: number, KeypadDecimal: number, AppBack: number, AppForward: number, Oem102: number, GamepadStart: number, GamepadFaceLeft: number, GamepadDpadLeft: number, GamepadL1: number, GamepadL2: number, GamepadL3: number, GamepadLStickLeft: number, GamepadRStickLeft: number, MouseLeft: number, ReservedForModCtrl: number, NamedKey_END: number, NamedKey_COUNT: number, NavKeyboardTweakSlow: number, NavKeyboardTweakFast: number, NavGamepadTweakSlow: number, NavGamepadTweakFast: number, NavGamepadMenu: number, NavGamepadContextMenu: number, [string]: number }
export type ImGuiMouseSource = { Mouse: number, TouchScreen: number, Pen: number, COUNT: number, [string]: number }
export type ImGuiNextItemDataFlags = { None: number, HasWidth: number, HasOpen: number, HasShortcut: number, HasRefVal: number, HasStorageID: number, HasColorMarker: number, [string]: number }
export type ImGuiPopupFlags = { None: number, MouseButtonLeft: number, MouseButtonRight: number, MouseButtonMiddle: number, NoReopen: number, NoOpenOverExistingPopup: number, NoOpenOverItems: number, AnyPopupId: number, AnyPopupLevel: number, AnyPopup: number, MouseButtonShift_: number, MouseButtonMask_: number, InvalidMask_: number, [string]: number }
export type ImGuiSelectableFlags = { None: number, NoAutoClosePopups: number, SpanAllColumns: number, AllowDoubleClick: number, Disabled: number, AllowOverlap: number, Highlight: number, SelectOnNav: number, NoHoldingActiveID: number, SelectOnClick: number, SelectOnRelease: number, SpanAvailWidth: number, SetNavIdOnHover: number, NoPadWithHalfSpacing: number, NoSetKeyOwner: number, [string]: number }
export type ImGuiSliderFlags = { None: number, Logarithmic: number, NoRoundToFormat: number, NoInput: number, WrapAround: number, ClampOnInput: number, ClampZeroRange: number, NoSpeedTweaks: number, ColorMarkers: number, InvalidMask_: number, AlwaysClamp: number, Vertical: number, ReadOnly: number, [string]: number }
export type ImGuiStyleVar = { Alpha: number, DisabledAlpha: number, WindowPadding: number, WindowRounding: number, WindowBorderSize: number, WindowMinSize: number, WindowTitleAlign: number, ChildRounding: number, ChildBorderSize: number, PopupRounding: number, PopupBorderSize: number, FramePadding: number, FrameRounding: number, FrameBorderSize: number, ItemSpacing: number, ItemInnerSpacing: number, IndentSpacing: number, CellPadding: number, ScrollbarSize: number, ScrollbarRounding: number, ScrollbarPadding: number, GrabMinSize: number, GrabRounding: number, ImageRounding: number, ImageBorderSize: number, TabRounding: number, TabBorderSize: number, TabMinWidthBase: number, TabMinWidthShrink: number, TabBarBorderSize: number, TabBarOverlineSize: number, TableAngledHeadersAngle: number, TableAngledHeadersTextAlign: number, TreeLinesSize: number, TreeLinesRounding: number, MenuItemRounding: number, SelectableRounding: number, DragDropTargetRounding: number, ButtonTextAlign: number, SelectableTextAlign: number, SeparatorTextBorderSize: number, SeparatorTextAlign: number, SeparatorTextPadding: number, COUNT: number, [string]: number }
export type ImGuiTreeNodeFlags = { None: number, Selected: number, Framed: number, AllowOverlap: number, NoTreePushOnOpen: number, NoAutoOpenOnLog: number, DefaultOpen: number, OpenOnDoubleClick: number, OpenOnArrow: number, Leaf: number, Bullet: number, FramePadding: number, SpanAvailWidth: number, SpanFullWidth: number, SpanLabelWidth: number, SpanAllColumns: number, LabelSpanAllColumns: number, NavLeftJumpsToParent: number, DrawLinesNone: number, DrawLinesFull: number, DrawLinesToNodes: number, NoNavFocus: number, ClipLabelForTrailingButton: number, UpsideDownArrow: number, CollapsingHeader: number, OpenOnMask_: number, DrawLinesMask_: number, [string]: number }
export type ImGuiViewportFlags = { None: number, IsPlatformWindow: number, IsPlatformMonitor: number, OwnedByApp: number, NoDecoration: number, NoTaskBarIcon: number, NoFocusOnAppearing: number, NoFocusOnClick: number, NoInputs: number, NoRendererClear: number, NoAutoMerge: number, TopMost: number, CanHostOtherWindows: number, IsMinimized: number, IsFocused: number, [string]: number }
export type ImGuiWindowFlags = { None: number, NoTitleBar: number, NoResize: number, NoMove: number, NoScrollbar: number, NoScrollWithMouse: number, NoCollapse: number, AlwaysAutoResize: number, NoBackground: number, NoSavedSettings: number, NoMouseInputs: number, MenuBar: number, HorizontalScrollbar: number, NoFocusOnAppearing: number, NoBringToFrontOnFocus: number, AlwaysVerticalScrollbar: number, AlwaysHorizontalScrollbar: number, NoNavInputs: number, NoNavFocus: number, UnsavedDocument: number, NoDocking: number, DockNodeHost: number, ChildWindow: number, Tooltip: number, Popup: number, Modal: number, ChildMenu: number, NoNav: number, NoDecoration: number, NoInputs: number, [string]: number }
export type ImGuiModule = {
ImGui: ImGuiAPI,
ImVec2: (x: number?, y: number?, z: number?, w: number?) -> ImVec2,
ImVec4: (x: number?, y: number?, z: number?, w: number?) -> ImVec4,
ImDrawFlags: ImDrawFlags,
ImDrawListFlags: ImDrawListFlags,
ImFontFlags: ImFontFlags,
ImGuiBackendFlags: ImGuiBackendFlags,
ImGuiButtonFlags: ImGuiButtonFlags,
ImGuiChildFlags: ImGuiChildFlags,
ImGuiCol: ImGuiCol,
ImGuiColorEditFlags: ImGuiColorEditFlags,
ImGuiComboFlags: ImGuiComboFlags,
ImGuiCond: ImGuiCond,
ImGuiConfigFlags: ImGuiConfigFlags,
ImGuiDragDropFlags: ImGuiDragDropFlags,
ImGuiFocusedFlags: ImGuiFocusedFlags,
ImGuiHoveredFlags: ImGuiHoveredFlags,
ImGuiInputFlags: ImGuiInputFlags,
ImGuiInputTextFlags: ImGuiInputTextFlags,
ImGuiItemFlags: ImGuiItemFlags,
ImGuiItemStatusFlags: ImGuiItemStatusFlags,
ImGuiKey: ImGuiKey,
ImGuiMouseSource: ImGuiMouseSource,
ImGuiNextItemDataFlags: ImGuiNextItemDataFlags,
ImGuiPopupFlags: ImGuiPopupFlags,
ImGuiSelectableFlags: ImGuiSelectableFlags,
ImGuiSliderFlags: ImGuiSliderFlags,
ImGuiStyleVar: ImGuiStyleVar,
ImGuiTreeNodeFlags: ImGuiTreeNodeFlags,
ImGuiViewportFlags: ImGuiViewportFlags,
ImGuiWindowFlags: ImGuiWindowFlags,
[string]: any,
}
return (require(script:WaitForChild("Impl")) :: any) :: ImGuiModule
+1 -1
View File
@@ -1 +1 @@
[[149, "imgui.lua"], [18609, "imgui_demo.lua"], [25894, "imgui_draw.lua"], [31132, "imgui_h.lua"], [33330, "imgui_impl_love.lua"], [33870, "imgui_internal.lua"], [37042, "imgui_tables.lua"], [41389, "imgui_widgets.lua"], [51144, "imstb_rectpack.lua"], [51590, "imstb_textedit.lua"], [52831, "imstb_truetype.lua"], [55299, "imstd_minstdio.lua"]] [[183, "imgui.lua"], [21526, "imgui_demo.lua"], [29944, "imgui_draw.lua"], [35882, "imgui_h.lua"], [38549, "imgui_impl_love.lua"], [39089, "imgui_internal.lua"], [42558, "imgui_tables.lua"], [47617, "imgui_widgets.lua"], [58257, "imstb_rectpack.lua"], [58921, "imstb_textedit.lua"], [60463, "imstb_truetype.lua"], [63251, "imstd_minstdio.lua"]]
+14961 -7007
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -3,7 +3,7 @@
"tree": { "tree": {
"$className": "DataModel", "$className": "DataModel",
"ReplicatedStorage": { "ReplicatedStorage": {
"ImGui": { "$path": "build/ImGui.luau" } "ImGui": { "$path": "build/ImGuiTyped.luau", "Impl": { "$path": "build/ImGui.luau" } }
}, },
"StarterPlayer": { "StarterPlayer": {
"StarterPlayerScripts": { "StarterPlayerScripts": {
+2661
View File
File diff suppressed because it is too large Load Diff
-1
View File
@@ -45,7 +45,6 @@ KNOWN ISSUES
PERFORMANCE IDEAS (not done) PERFORMANCE IDEAS (not done)
---------------------------- ----------------------------
- Opt-in cached windows (skip user code for idle windows).
- Cheaper visible ColorEdit4 rows; cheaper Begin() for child windows. - Cheaper visible ColorEdit4 rows; cheaper Begin() for child windows.
PLANNED EXTENSIONS PLANNED EXTENSIONS
+421 -262
View File
@@ -1,336 +1,495 @@
-- LocalScript. Build the library first: `python3 tools/bundle.py` -> build/ImGui.luau (see default.project.json)
local ReplicatedStorage = game:GetService("ReplicatedStorage") local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService") local RunService = game:GetService("RunService")
local AssetService = game:GetService("AssetService")
local G = require(ReplicatedStorage:WaitForChild("ImGui")) local G = require(ReplicatedStorage:WaitForChild("ImGui"))
local ExampleImageViewerData, ExampleImageViewer_DrawOptions, ExampleImageViewer_DrawCanvas = G.ExampleImageViewerData, G.ExampleImageViewer_DrawOptions, G.ExampleImageViewer_DrawCanvas
local ImGui, ImVec4, ImGuiConfigFlags, ImGui_ImplRoblox = G.ImGui, G.ImVec4, G.ImGuiConfigFlags, G.ImGui_ImplRoblox local BaseEnvironment = getfenv()
setfenv(1, setmetatable({}, {
__index = function(_, Key)
local Value = G[Key]
if Value ~= nil then
return Value
end
return BaseEnvironment[Key]
end,
}))
local main_scale = 1.0 local MainScale = 1.0
-- Roblox UI points are multiplied by the OS display scale (e.g. 150%), which makes imgui look large. local RenderScale = 1.0
-- Set this to your display scale to render imgui at physical-pixel resolution (same size as a desktop imgui app). local RenderRate = 0 -- 0 = every frame (tile redraws are limited by the redraw budget instead)
local render_scale = 1.0 local ThumbnailSize = 420
local clear_color = ImVec4(0.45, 0.55, 0.60, 1.00) local FontSize = 13.0
local show_demo_window = true local FontNames = { "Cousine-Regular", "DroidSans", "Karla-Regular", "ProggyTiny", "Roboto-Medium" }
local show_another_window = false
local f = 0.0 local State = {
local counter = 0 ClearColor = ImVec4(0.45, 0.55, 0.60, 1.00),
ShowDemoWindow = true,
ShowAnotherWindow = false,
Float = 0.0,
Counter = 0,
}
local Spinner = {
Show = true,
Speed = 1.0,
Angle = 0.0,
Color = ImVec4(0.80, 0.65, 0.97, 1.0),
}
ImGui.CreateContext() ImGui.CreateContext()
local io = ImGui.GetIO() local IO = ImGui.GetIO()
io.ConfigFlags = bit32.bor(io.ConfigFlags, ImGuiConfigFlags.NavEnableKeyboard, ImGuiConfigFlags.DockingEnable) IO.ConfigFlags = bit32.bor(IO.ConfigFlags, ImGuiConfigFlags.NavEnableKeyboard, ImGuiConfigFlags.DockingEnable)
-- Catppuccin Mocha, Mauve accent (https://catppuccin.com/palette) -- catppuccin mocha mauve https://catppuccin.com/palette
local function StyleColorsCatppuccinMocha(style) local function StyleColorsCatppuccinMocha(Style)
local function hex(h, a) local function Hex(Value, Alpha)
return ImVec4(bit32.extract(h, 16, 8) / 255, bit32.extract(h, 8, 8) / 255, bit32.extract(h, 0, 8) / 255, a or 1.0) return ImVec4(
bit32.extract(Value, 16, 8) / 255,
bit32.extract(Value, 8, 8) / 255,
bit32.extract(Value, 0, 8) / 255,
Alpha or 1.0
)
end
local Crust, Mantle, Base = 0x11111b, 0x181825, 0x1e1e2e
local Surface0, Surface1, Surface2 = 0x313244, 0x45475a, 0x585b70
local Overlay0, Overlay2, Text = 0x6c7086, 0x9399b2, 0xcdd6f4
local Mauve, Lavender = 0xcba6f7, 0xb4befe
local Entries = {
{ "Text", Text },
{ "TextDisabled", Overlay0 },
{ "WindowBg", Base },
{ "ChildBg", Base, 0.0 },
{ "PopupBg", Mantle, 0.98 },
{ "Border", Surface1 },
{ "BorderShadow", Crust, 0.0 },
{ "FrameBg", Surface0 },
{ "FrameBgHovered", Surface1 },
{ "FrameBgActive", Surface2 },
{ "TitleBg", Mantle },
{ "TitleBgActive", Crust },
{ "TitleBgCollapsed", Mantle, 0.75 },
{ "MenuBarBg", Mantle },
{ "ScrollbarBg", Mantle },
{ "ScrollbarGrab", Surface1 },
{ "ScrollbarGrabHovered", Surface2 },
{ "ScrollbarGrabActive", Overlay0 },
{ "CheckMark", Mauve },
{ "SliderGrab", Mauve, 0.8 },
{ "SliderGrabActive", Mauve },
{ "Button", Surface0 },
{ "ButtonHovered", Surface1 },
{ "ButtonActive", Mauve, 0.6 },
{ "Header", Surface0 },
{ "HeaderHovered", Surface1 },
{ "HeaderActive", Mauve, 0.5 },
{ "Separator", Surface1 },
{ "SeparatorHovered", Mauve, 0.7 },
{ "SeparatorActive", Mauve },
{ "ResizeGrip", Mauve, 0.2 },
{ "ResizeGripHovered", Mauve, 0.6 },
{ "ResizeGripActive", Mauve, 0.9 },
{ "InputTextCursor", Text },
{ "Tab", Surface0 },
{ "TabHovered", Surface2 },
{ "TabSelected", Surface1 },
{ "TabSelectedOverline", Mauve },
{ "TabDimmed", Mantle },
{ "TabDimmedSelected", Surface0 },
{ "TabDimmedSelectedOverline", Overlay0 },
{ "DockingPreview", Mauve, 0.7 },
{ "DockingEmptyBg", Crust },
{ "PlotLines", Overlay2 },
{ "PlotLinesHovered", Mauve },
{ "PlotHistogram", Mauve, 0.8 },
{ "PlotHistogramHovered", Lavender },
{ "TableHeaderBg", Surface0 },
{ "TableBorderStrong", Surface1 },
{ "TableBorderLight", Surface0 },
{ "TableRowBg", Base, 0.0 },
{ "TableRowBgAlt", Surface0, 0.3 },
{ "TextLink", Mauve },
{ "TextSelectedBg", Mauve, 0.35 },
{ "TreeLines", Surface2 },
{ "DragDropTarget", Mauve, 0.9 },
{ "NavCursor", Lavender },
{ "NavWindowingHighlight", Lavender, 0.7 },
{ "NavWindowingDimBg", Crust, 0.5 },
{ "ModalWindowDimBg", Crust, 0.6 },
}
for _, Entry in ipairs(Entries) do
local Name, Color, Alpha = Entry[1], Entry[2], Entry[3]
Style.Colors[ImGuiCol[Name]] = Hex(Color, Alpha)
end end
local crust, mantle, base = 0x11111b, 0x181825, 0x1e1e2e
local surface0, surface1, surface2 = 0x313244, 0x45475a, 0x585b70
local overlay0, overlay2, text = 0x6c7086, 0x9399b2, 0xcdd6f4
local mauve, lavender = 0xcba6f7, 0xb4befe
local C, c = G.ImGuiCol, style.Colors
local function set(idx, h, a) c[idx] = hex(h, a) end
set(C.Text, text); set(C.TextDisabled, overlay0)
set(C.WindowBg, base); set(C.ChildBg, base, 0.0)
set(C.PopupBg, mantle, 0.98); set(C.Border, surface1); set(C.BorderShadow, crust, 0.0)
set(C.FrameBg, surface0); set(C.FrameBgHovered, surface1); set(C.FrameBgActive, surface2)
set(C.TitleBg, mantle); set(C.TitleBgActive, crust); set(C.TitleBgCollapsed, mantle, 0.75)
set(C.MenuBarBg, mantle)
set(C.ScrollbarBg, mantle); set(C.ScrollbarGrab, surface1)
set(C.ScrollbarGrabHovered, surface2); set(C.ScrollbarGrabActive, overlay0)
set(C.CheckMark, mauve); set(C.SliderGrab, mauve, 0.8); set(C.SliderGrabActive, mauve)
set(C.Button, surface0); set(C.ButtonHovered, surface1); set(C.ButtonActive, mauve, 0.6)
set(C.Header, surface0); set(C.HeaderHovered, surface1); set(C.HeaderActive, mauve, 0.5)
set(C.Separator, surface1); set(C.SeparatorHovered, mauve, 0.7); set(C.SeparatorActive, mauve)
set(C.ResizeGrip, mauve, 0.2); set(C.ResizeGripHovered, mauve, 0.6); set(C.ResizeGripActive, mauve, 0.9)
set(C.InputTextCursor, text)
set(C.TabHovered, surface2); set(C.Tab, surface0)
set(C.TabSelected, surface1); set(C.TabSelectedOverline, mauve)
set(C.TabDimmed, mantle); set(C.TabDimmedSelected, surface0); set(C.TabDimmedSelectedOverline, overlay0)
set(C.DockingPreview, mauve, 0.7); set(C.DockingEmptyBg, crust)
set(C.PlotLines, overlay2); set(C.PlotLinesHovered, mauve)
set(C.PlotHistogram, mauve, 0.8); set(C.PlotHistogramHovered, lavender)
set(C.TableHeaderBg, surface0); set(C.TableBorderStrong, surface1); set(C.TableBorderLight, surface0)
set(C.TableRowBg, base, 0.0); set(C.TableRowBgAlt, surface0, 0.3)
set(C.TextLink, mauve); set(C.TextSelectedBg, mauve, 0.35)
set(C.TreeLines, surface2)
set(C.DragDropTarget, mauve, 0.9)
set(C.NavCursor, lavender); set(C.NavWindowingHighlight, lavender, 0.7)
set(C.NavWindowingDimBg, crust, 0.5); set(C.ModalWindowDimBg, crust, 0.6)
end end
local style = ImGui.GetStyle() local Style = ImGui.GetStyle()
StyleColorsCatppuccinMocha(style) StyleColorsCatppuccinMocha(Style)
style:ScaleAllSizes(main_scale) Style:ScaleAllSizes(MainScale)
style.FontScaleDpi = main_scale Style.FontScaleDpi = MainScale
-- Fonts: ProggyClean stays the default (added first); the rest show up in Style Editor > Fonts IO.Fonts:AddFontDefault()
io.Fonts:AddFontDefault() for _, Name in ipairs(FontNames) do
for _, name in ipairs({ "Cousine-Regular", "DroidSans", "Karla-Regular", "ProggyTiny", "Roboto-Medium" }) do IO.Fonts:AddFontFromFileTTF("fonts/" .. Name .. ".ttf", FontSize)
io.Fonts:AddFontFromFileTTF("fonts/" .. name .. ".ttf", 13.0)
end end
ImGui_ImplRoblox.Init(nil, render_scale) ImGui_ImplRoblox.Init(nil, RenderScale)
ImGui_ImplRoblox.SetRenderRate(60) -- tile redraws per second; imgui logic + your code still run every frame ImGui_ImplRoblox.SetRenderRate(RenderRate)
-- ImGui_ImplRoblox.SetBusyRenderRate(30) -- optional: lower redraw rate while scrolling/resizing/moving windows -- ImGui_ImplRoblox.SetBusyRenderRate(30) -- Optional: lower redraw rate while scrolling/resizing/moving windows
ImGui_ImplRoblox.SetProfiling(true) -- MicroProfiler (Ctrl+F6) labels; turn off when not profiling -- ImGui_ImplRoblox.SetProfiling(true) -- MicroProfiler labels; turn off when not profiling
-- ImGui_ImplRoblox.SetTextProxy(true) -- Optional: hidden TextBox for OS typing (any layout), paste and copy/cut
local function ProfileBegin(Name)
if ImGui_ImplRoblox.IsProfiling() then
debug.profilebegin(Name)
end
end
local function ProfileEnd()
if ImGui_ImplRoblox.IsProfiling() then
debug.profileend()
end
end
local function P(name) if ImGui_ImplRoblox.IsProfiling() then debug.profilebegin(name) end end
local function PE() if ImGui_ImplRoblox.IsProfiling() then debug.profileend() end end
--------------------------------------------------------------------------------------------------------------------
-- Custom widget: ImGui.Embed(id, size, flags, init) -> Frame
-- Reserves a widget-sized area in the current window and shows a real Frame there, so any Instances (UI, a
-- ViewportFrame, ...) can live inside an imgui window. Immediate-mode friendly:
-- * one Frame per id (per window, through the ID stack, like any widget); `init(frame)` runs only when it is created
-- * call it every frame the Frame should be visible; when it isn't called (window closed/collapsed, tab or tree node
-- closed, scrolled fully out of view) the Frame is hidden, never destroyed, so your Instances keep their state
-- * size uses imgui sizing (ImVec2(-1, h) = fill width, 0 = default), style gives padding/rounding/background/border
-- * returns the Frame: edit properties, add or remove children any time
-- ImGui.DestroyEmbed(id) removes it for good (call while the same window is current, like the widget itself).
--------------------------------------------------------------------------------------------------------------------
local EmbedFlags = { local EmbedFlags = {
None = 0, None = 0,
NoClip = 1, -- don't clip to the window's content area (still moves/hides/stacks with the window) NoClip = 1, -- Don't clip to the window's content area (still moves/hides/stacks with the window)
NoBackground = 2, -- no FrameBg behind the Frame NoBackground = 2, -- No FrameBg behind the Frame
NoBorder = 4, -- no style border NoBorder = 4, -- No style border
NoPadding = 8, -- no FramePadding inside the Frame (UIPadding) NoPadding = 8, -- No FramePadding inside the Frame (UIPadding)
NoRounding = 16, -- no FrameRounding corners (UICorner) NoRounding = 16, -- No FrameRounding corners (UICorner)
} }
G.ImGuiEmbedFlags = EmbedFlags G.ImGuiEmbedFlags = EmbedFlags
local embeds = {} -- [ImGuiID] = { Frame, Padding, Corner } local Embeds = {}
function ImGui.Embed(id, size, flags, init) local function HasFlag(Flags, Flag)
flags = flags or EmbedFlags.None return bit32.band(Flags, Flag) ~= 0
local window = ImGui.GetCurrentWindow()
local gid = window:GetID(id)
local e = embeds[gid]
if not e then
local frame = Instance.new("Frame")
frame.Name = tostring(id)
frame.BackgroundTransparency = 1
frame.BorderSizePixel = 0
frame.Visible = false
e = { Frame = frame, Padding = Instance.new("UIPadding"), Corner = Instance.new("UICorner") }
e.Padding.Parent = frame
e.Corner.Parent = frame
embeds[gid] = e
if init then init(frame) end
end
if window.SkipItems then return e.Frame end
local style = ImGui.GetStyle()
local sz = ImGui.CalcItemSize(size or G.ImVec2(0, 0), ImGui.CalcItemWidth(), ImGui.GetFrameHeight() * 4)
local p = window.DC.CursorPos
local bb = G.ImRect(p.x, p.y, p.x + sz.x, p.y + sz.y)
ImGui.ItemSize(sz)
if not ImGui.ItemAdd(bb, gid) then return e.Frame end -- fully clipped: not submitted -> hidden this frame
local rounding = bit32.band(flags, EmbedFlags.NoRounding) == 0 and style.FrameRounding or 0
if bit32.band(flags, EmbedFlags.NoBackground) == 0 then
ImGui.RenderFrame(bb.Min, bb.Max, ImGui.GetColorU32(G.ImGuiCol.FrameBg), bit32.band(flags, EmbedFlags.NoBorder) == 0, rounding)
elseif bit32.band(flags, EmbedFlags.NoBorder) == 0 then
ImGui.RenderFrameBorder(bb.Min, bb.Max, rounding)
end
-- style -> Instances (only written when changed)
local scale = ImGui_ImplRoblox.GetRenderScale()
local px, py = 0, 0
if bit32.band(flags, EmbedFlags.NoPadding) == 0 then px, py = style.FramePadding.x / scale, style.FramePadding.y / scale end
if e.PX ~= px or e.PY ~= py then
e.PX, e.PY = px, py
e.Padding.PaddingLeft, e.Padding.PaddingRight = UDim.new(0, px), UDim.new(0, px)
e.Padding.PaddingTop, e.Padding.PaddingBottom = UDim.new(0, py), UDim.new(0, py)
end
if e.R ~= rounding then e.R = rounding; e.Corner.CornerRadius = UDim.new(0, rounding / scale) end
ImGui_ImplRoblox.SetOverlay(e.Frame, bb.Min.x, bb.Min.y, sz.x, sz.y, bit32.band(flags, EmbedFlags.NoClip) == 0)
return e.Frame
end end
function ImGui.DestroyEmbed(id) local function CreateEmbed(Id)
local gid = ImGui.GetCurrentWindow():GetID(id) local Frame = Instance.new("Frame")
local e = embeds[gid] Frame.Name = tostring(Id)
if e then e.Frame:Destroy(); embeds[gid] = nil end Frame.BackgroundTransparency = 1
Frame.BorderSizePixel = 0
Frame.Visible = false
local Padding = Instance.new("UIPadding")
Padding.Parent = Frame
local Corner = Instance.new("UICorner")
Corner.Parent = Frame
return { Frame = Frame, Padding = Padding, Corner = Corner }
end end
-- Example: Decal Viewer. Loads a decal/image asset's pixels into an imgui texture and shows it in the demo's function ImGui.Embed(Id, Size, Flags, Init)
-- image viewer (zoom with the wheel, drag to pan, toggleable pixel grid). Flags = Flags or EmbedFlags.None
local AssetService = game:GetService("AssetService")
local MarketplaceService = game:GetService("MarketplaceService")
local decal = { input = ImGui.NewTextBuffer("", 32), status = "Enter a decal or image id", tex = nil, w = 0, h = 0, loading = false,
viewer = ExampleImageViewerData(), show = true }
-- A Decal asset wraps an Image asset with a different id; the image is almost always uploaded just before it local Window = ImGui.GetCurrentWindow()
-- (same creator, id slightly lower). ponytail: heuristic scan of 10 ids, InsertService:LoadAsset when owned is exact. local GuiId = Window:GetID(Id)
local function ResolveImageId(id)
local ok, info = pcall(MarketplaceService.GetProductInfo, MarketplaceService, id) local Embed = Embeds[GuiId]
if not ok then return nil, "GetProductInfo failed: " .. tostring(info) end if not Embed then
if info.AssetTypeId == 1 then return id end -- already an Image Embed = CreateEmbed(Id)
if info.AssetTypeId ~= 13 then return nil, "asset is not a decal or image" end Embeds[GuiId] = Embed
for cand = id - 1, id - 10, -1 do
local ok2, ci = pcall(MarketplaceService.GetProductInfo, MarketplaceService, cand) if Init then
if ok2 and ci.AssetTypeId == 1 and ci.Creator and info.Creator and ci.Creator.Id == info.Creator.Id then return cand end Init(Embed.Frame)
end end
return nil, "couldn't find the decal's image id (try the image id directly)" end
if Window.SkipItems then
return Embed.Frame
end
local EmbedStyle = ImGui.GetStyle()
local ItemSize = ImGui.CalcItemSize(
Size or ImVec2(0, 0),
ImGui.CalcItemWidth(),
ImGui.GetFrameHeight() * 4
)
local CursorPos = Window.DC.CursorPos
local BoundingBox = ImRect(CursorPos.x, CursorPos.y, CursorPos.x + ItemSize.x, CursorPos.y + ItemSize.y)
ImGui.ItemSize(ItemSize)
if not ImGui.ItemAdd(BoundingBox, GuiId) then
return Embed.Frame
end
local Rounding = HasFlag(Flags, EmbedFlags.NoRounding) and 0 or EmbedStyle.FrameRounding
local ShowBorder = not HasFlag(Flags, EmbedFlags.NoBorder)
if not HasFlag(Flags, EmbedFlags.NoBackground) then
ImGui.RenderFrame(
BoundingBox.Min,
BoundingBox.Max,
ImGui.GetColorU32(ImGuiCol.FrameBg),
ShowBorder,
Rounding
)
elseif ShowBorder then
ImGui.RenderFrameBorder(BoundingBox.Min, BoundingBox.Max, Rounding)
end
local Scale = ImGui_ImplRoblox.GetRenderScale()
local PaddingX, PaddingY = 0, 0
if not HasFlag(Flags, EmbedFlags.NoPadding) then
PaddingX = EmbedStyle.FramePadding.x / Scale
PaddingY = EmbedStyle.FramePadding.y / Scale
end
if Embed.PaddingX ~= PaddingX or Embed.PaddingY ~= PaddingY then
Embed.PaddingX, Embed.PaddingY = PaddingX, PaddingY
local Padding = Embed.Padding
Padding.PaddingLeft = UDim.new(0, PaddingX)
Padding.PaddingRight = UDim.new(0, PaddingX)
Padding.PaddingTop = UDim.new(0, PaddingY)
Padding.PaddingBottom = UDim.new(0, PaddingY)
end
if Embed.Rounding ~= Rounding then
Embed.Rounding = Rounding
Embed.Corner.CornerRadius = UDim.new(0, Rounding / Scale)
end
ImGui_ImplRoblox.SetOverlay(
Embed.Frame,
BoundingBox.Min.x,
BoundingBox.Min.y,
ItemSize.x,
ItemSize.y,
not HasFlag(Flags, EmbedFlags.NoClip)
)
return Embed.Frame
end end
local function LoadDecal(id) function ImGui.DestroyEmbed(Id)
decal.loading = true local GuiId = ImGui.GetCurrentWindow():GetID(Id)
decal.status = "Loading " .. id .. "..." local Embed = Embeds[GuiId]
if Embed then
Embed.Frame:Destroy()
Embeds[GuiId] = nil
end
end
local Decal = {
Show = true,
Input = ImGui.NewTextBuffer("", 32),
Status = "Enter a decal or image id",
Loading = false,
Texture = nil,
Width = 0,
Height = 0,
Viewer = ExampleImageViewerData(),
}
local function LoadDecal(Id)
Decal.Loading = true
Decal.Status = "Loading " .. Id .. "..."
task.spawn(function() task.spawn(function()
local image_id, err = ResolveImageId(id) local Uri = string.format("rbxthumb://type=Asset&id=%d&w=%d&h=%d", Id, ThumbnailSize, ThumbnailSize)
decal.thumb = nil local Success, Result = pcall(AssetService.CreateEditableImageAsync, AssetService, Content.fromUri(Uri))
if image_id then
-- 1) the image itself (needs permission), 2) its 420x420 thumbnail (may be allowed when 1 isn't) if Success then
local ok, img = pcall(AssetService.CreateEditableImageAsync, AssetService, Content.fromUri("rbxassetid://" .. image_id)) local Size = Result.Size
if not ok then local Pixels = Result:ReadPixelsBuffer(Vector2.zero, Size)
local ok2, img2 = pcall(AssetService.CreateEditableImageAsync, AssetService, Content.fromUri("rbxthumb://type=Asset&id=" .. id .. "&w=420&h=420")) Result:Destroy()
if ok2 then ok, img = true, img2 end
if Decal.Texture then
ImGui_ImplRoblox.DestroyTexture(Decal.Texture)
end end
if ok then
local size = img.Size Decal.Texture = ImGui_ImplRoblox.CreateTexture(Pixels, Size.X, Size.Y)
local pixels = img:ReadPixelsBuffer(Vector2.zero, size) Decal.Width, Decal.Height = Size.X, Size.Y
img:Destroy() Decal.Viewer.ViewReset = true
if decal.tex then ImGui_ImplRoblox.DestroyTexture(decal.tex) end Decal.Status = string.format("Thumbnail %d: %dx%d", Id, Size.X, Size.Y)
decal.thumb = nil
decal.tex, decal.w, decal.h = ImGui_ImplRoblox.CreateTexture(pixels, size.X, size.Y), size.X, size.Y
decal.viewer.ViewReset = true
decal.status = string.format("Image %d: %dx%d", image_id, size.X, size.Y)
else else
-- 3) no pixel access: Roblox still displays any asset, so show its thumbnail in an ImageLabel (no grid) Decal.Status = "Failed to load thumbnail: " .. tostring(Result)
decal.thumb = "rbxthumb://type=Asset&id=" .. id .. "&w=420&h=420"
decal.status = "No pixel access (asset not owned by this experience's creator): display only, no zoom/grid"
end end
else
decal.thumb = "rbxthumb://type=Asset&id=" .. id .. "&w=420&h=420" Decal.Loading = false
decal.status = (err or "") .. " - showing thumbnail"
end
decal.loading = false
end) end)
end end
local function ShowDecalViewer() local function ShowDecalViewer()
if not decal.show then return end if not Decal.Show then
decal.show = ImGui.Begin("Decal Viewer", decal.show) return
end
Decal.Show = ImGui.Begin("Decal Viewer", Decal.Show)
ImGui.SetNextItemWidth(ImGui.GetFontSize() * 12) ImGui.SetNextItemWidth(ImGui.GetFontSize() * 12)
local enter = ImGui.InputTextWithHint("##id", "decal id", decal.input, 32, G.ImGuiInputTextFlags.CharsDecimal + G.ImGuiInputTextFlags.EnterReturnsTrue) local EnterPressed = ImGui.InputTextWithHint(
"##id",
"decal id",
Decal.Input,
32,
ImGuiInputTextFlags.CharsDecimal + ImGuiInputTextFlags.EnterReturnsTrue
)
ImGui.SameLine() ImGui.SameLine()
ImGui.BeginDisabled(decal.loading) ImGui.BeginDisabled(Decal.Loading)
if (ImGui.Button("Load") or enter) and not decal.loading then if (ImGui.Button("Load") or EnterPressed) and not Decal.Loading then
local id = tonumber(ImGui.TextBufferToString(decal.input)) local Id = tonumber(ImGui.TextBufferToString(Decal.Input))
if id then LoadDecal(id) else decal.status = "Not a number" end if Id and Id == math.floor(Id) then
LoadDecal(Id)
else
Decal.Status = "Not a valid id"
end
end end
ImGui.EndDisabled() ImGui.EndDisabled()
ImGui.SameLine(); ImGui.TextDisabled(decal.status)
if decal.thumb then ImGui.SameLine()
local avail = ImGui.GetContentRegionAvail() ImGui.TextDisabled(Decal.Status)
local frame = ImGui.Embed("decal_thumb", G.ImVec2(-1, math.max(avail.y, 50)), G.ImGuiEmbedFlags.None, function(f)
local img = Instance.new("ImageLabel") if Decal.Texture then
img.Name = "Image" ExampleImageViewer_DrawOptions(Decal.Viewer)
img.Size = UDim2.fromScale(1, 1)
img.BackgroundTransparency = 1 local Available = ImGui.GetContentRegionAvail()
img.ScaleType = Enum.ScaleType.Fit ExampleImageViewer_DrawCanvas(
img.Parent = f Decal.Viewer,
end) ImVec2(math.max(Available.x, 50), math.max(Available.y, 50)),
if frame.Image.Image ~= decal.thumb then frame.Image.Image = decal.thumb end Decal.Texture,
elseif decal.tex then Decal.Width,
ExampleImageViewer_DrawOptions(decal.viewer) Decal.Height
local avail = ImGui.GetContentRegionAvail() )
ExampleImageViewer_DrawCanvas(decal.viewer, G.ImVec2(math.max(avail.x, 50), math.max(avail.y, 50)), decal.tex, decal.w, decal.h)
end end
ImGui.End() ImGui.End()
end end
-- Example: a ViewportFrame with a spinning part, edited live from imgui widgets local function ShowHelloWindow()
local spin_speed = 1.0
local part_color = ImVec4(0.80, 0.65, 0.97, 1.0)
local spin_angle = 0.0
local show_viewport_window = true
RunService.Stepped:Connect(function()
P("ImGui frame")
ImGui_ImplRoblox.NewFrame()
P("ImGui.NewFrame"); ImGui.NewFrame(); PE()
P("ImGui UI")
-- Dockspace covering the whole screen; the centre stays transparent so the game shows through
ImGui.DockSpaceOverViewport(0, nil, G.ImGuiDockNodeFlags.PassthruCentralNode)
if show_demo_window then
ImGui.ShowDemoWindow()
end
do
ImGui.Begin("Hello, world!") ImGui.Begin("Hello, world!")
ImGui.Text("This is some useful text.") ImGui.Text("This is some useful text.")
local _
_, show_demo_window = ImGui.Checkbox("Demo Window", show_demo_window)
_, show_another_window = ImGui.Checkbox("Another Window", show_another_window)
f = ImGui.SliderFloat("float", f, 0.0, 1.0) local _
ImGui.ColorEdit3("clear color", clear_color) _, State.ShowDemoWindow = ImGui.Checkbox("Demo Window", State.ShowDemoWindow)
_, State.ShowAnotherWindow = ImGui.Checkbox("Another Window", State.ShowAnotherWindow)
State.Float = ImGui.SliderFloat("float", State.Float, 0.0, 1.0)
ImGui.ColorEdit3("clear color", State.ClearColor)
if ImGui.Button("Button") then if ImGui.Button("Button") then
counter = counter + 1 State.Counter += 1
end end
ImGui.SameLine() ImGui.SameLine()
ImGui.Text("counter = %d", counter) ImGui.Text("counter = %d", State.Counter)
ImGui.Text("Application average %.3f ms/frame (%.1f FPS)", 1000.0 / io.Framerate, io.Framerate) ImGui.Text("Application average %.3f ms/frame (%.1f FPS)", 1000.0 / IO.Framerate, IO.Framerate)
ImGui.End() ImGui.End()
end
local function ShowAnotherWindow()
if not State.ShowAnotherWindow then
return
end end
if show_another_window then State.ShowAnotherWindow = ImGui.Begin("Another Window", State.ShowAnotherWindow)
show_another_window = ImGui.Begin("Another Window", show_another_window)
ImGui.Text("Hello from another window!") ImGui.Text("Hello from another window!")
if ImGui.Button("Close Me") then if ImGui.Button("Close Me") then
show_another_window = false State.ShowAnotherWindow = false
end end
ImGui.End() ImGui.End()
end
local function CreateSpinnerViewport(Container)
local Viewport = Instance.new("ViewportFrame")
Viewport.Size = UDim2.fromScale(1, 1)
Viewport.BackgroundTransparency = 1
Viewport.Parent = Container
local Camera = Instance.new("Camera")
Camera.CFrame = CFrame.lookAt(Vector3.new(0, 2, 6), Vector3.zero)
Camera.Parent = Viewport
Viewport.CurrentCamera = Camera
local Part = Instance.new("Part")
Part.Name = "Spinner"
Part.Size = Vector3.new(2, 2, 2)
Part.Parent = Viewport
end
local function ShowViewportWindow()
if not Spinner.Show then
return
end end
PE() Spinner.Show = ImGui.Begin("Viewport", Spinner.Show)
Spinner.Speed = ImGui.SliderFloat("speed", Spinner.Speed, 0.0, 5.0)
ImGui.ColorEdit3("color", Spinner.Color)
local Frame = ImGui.Embed("viewport", ImVec2(-1, 220), EmbedFlags.None, CreateSpinnerViewport)
Spinner.Angle += Spinner.Speed * IO.DeltaTime
local Part = Frame.ViewportFrame.Spinner
Part.CFrame = CFrame.Angles(0.4, Spinner.Angle, 0)
Part.Color = Color3.new(Spinner.Color.x, Spinner.Color.y, Spinner.Color.z)
ImGui.End()
end
RunService.Stepped:Connect(function()
ProfileBegin("ImGui frame")
ImGui_ImplRoblox.NewFrame()
ProfileBegin("ImGui.NewFrame")
ImGui.NewFrame()
ProfileEnd()
ProfileBegin("ImGui UI")
ImGui.DockSpaceOverViewport(0, nil, ImGuiDockNodeFlags.PassthruCentralNode)
if State.ShowDemoWindow then
-- For reference: https://github.com/4DBug/imgui-luau/blob/master/imgui_demo.lua
ImGui.ShowDemoWindow()
end
ShowHelloWindow()
ShowAnotherWindow()
ShowDecalViewer() ShowDecalViewer()
ShowViewportWindow()
ProfileEnd()
if show_viewport_window then ProfileBegin("ImGui.Render")
show_viewport_window = ImGui.Begin("Viewport", show_viewport_window) ImGui.EndFrame()
spin_speed = ImGui.SliderFloat("speed", spin_speed, 0.0, 5.0) ImGui.Render()
ImGui.ColorEdit3("color", part_color) ProfileEnd()
local frame = ImGui.Embed("viewport", G.ImVec2(-1, 220), G.ImGuiEmbedFlags.None, function(f)
-- runs once
local vpf = Instance.new("ViewportFrame")
vpf.Size = UDim2.fromScale(1, 1)
vpf.BackgroundTransparency = 1
vpf.Parent = f
local cam = Instance.new("Camera")
cam.CFrame = CFrame.lookAt(Vector3.new(0, 2, 6), Vector3.new(0, 0, 0))
cam.Parent = vpf
vpf.CurrentCamera = cam
local part = Instance.new("Part")
part.Name = "Spinner"
part.Size = Vector3.new(2, 2, 2)
part.Parent = vpf
end)
-- every frame: edit what was created once
spin_angle += spin_speed * io.DeltaTime
local part = frame.ViewportFrame.Spinner
part.CFrame = CFrame.Angles(0.4, spin_angle, 0)
part.Color = Color3.new(part_color.x, part_color.y, part_color.z)
ImGui.Text("Frames created: 1 (same Frame every frame)")
ImGui.End()
end
P("ImGui.Render"); ImGui.EndFrame(); ImGui.Render(); PE()
ImGui_ImplRoblox.RenderDrawData(ImGui.GetDrawData()) ImGui_ImplRoblox.RenderDrawData(ImGui.GetDrawData())
PE() ProfileEnd()
end) end)
+1 -1
View File
@@ -3,7 +3,7 @@
"tree": { "tree": {
"$className": "DataModel", "$className": "DataModel",
"ReplicatedStorage": { "ReplicatedStorage": {
"ImGui": { "$path": "../../build/ImGui.luau" }, "ImGui": { "$path": "../../build/ImGuiTyped.luau", "Impl": { "$path": "../../build/ImGui.luau" } },
"SharedApp": { "$path": "app.lua" } "SharedApp": { "$path": "app.lua" }
}, },
"StarterPlayer": { "StarterPlayer": {
+3493 -610
View File
File diff suppressed because it is too large Load Diff
+1314 -181
View File
File diff suppressed because it is too large Load Diff
+938 -238
View File
File diff suppressed because it is too large Load Diff
+1172 -703
View File
File diff suppressed because it is too large Load Diff
+916 -619
View File
File diff suppressed because it is too large Load Diff
+915 -203
View File
File diff suppressed because it is too large Load Diff
+1251 -366
View File
File diff suppressed because it is too large Load Diff
+229 -11
View File
@@ -4,6 +4,63 @@
-- ALL TABLES IN THIS FILE ARE 1-BASED! -- ALL TABLES IN THIS FILE ARE 1-BASED!
-- Avoids table creation when possible -- Avoids table creation when possible
-- [DEAR IMGUI]
-- This is a slightly modified version of stb_rect_pack.h 1.01.
-- Grep for [DEAR IMGUI] to find the changes.
--
-- stb_rect_pack.h - v1.01 - public domain - rectangle packing
-- Sean Barrett 2014
--
-- Useful for e.g. packing rectangular textures into an atlas.
-- Does not do rotation.
--
-- Not necessarily the awesomest packing method, but better than
-- the totally naive one in stb_truetype (which is primarily what
-- this is meant to replace).
--
-- Has only had a few tests run, may have issues.
--
-- More docs to come.
--
-- Uses table.sort and assert (STBRP_SORT and STBRP_ASSERT below).
--
-- This library currently uses the Skyline Bottom-Left algorithm.
--
-- Please note: better rectangle packers are welcome! Please
-- implement them to the same API, but with a different init
-- function.
--
-- Credits
--
-- Library
-- Sean Barrett
-- Minor features
-- Martins Mozeiko
-- github:IntellectualKitty
--
-- Bugfixes / warning fixes
-- Jeremy Jaussaud
-- Fabian Giesen
--
-- Version history:
--
-- 1.01 (2021-07-11) always use large rect mode, expose STBRP__MAXVAL in public section
-- 1.00 (2019-02-25) avoid small space waste; gracefully fail too-wide rectangles
-- 0.99 (2019-02-07) warning fixes
-- 0.11 (2017-03-03) return packing success/fail result
-- 0.10 (2016-10-25) remove cast-away-const to avoid warnings
-- 0.09 (2016-08-27) fix compiler warnings
-- 0.08 (2015-09-13) really fix bug with empty rects (w=0 or h=0)
-- 0.07 (2015-09-13) fix bug with empty rects (w=0 or h=0)
-- 0.06 (2015-04-15) added STBRP_SORT to allow replacing qsort
-- 0.05: added STBRP_ASSERT to allow replacing assert
-- 0.04: fixed minor bug in STBRP_LARGE_RECTS support
-- 0.01: initial release
--
-- LICENSE
--
-- See end of file for license information.
--- The `prev_link` can be stbrp_context or stbrp_node --- The `prev_link` can be stbrp_context or stbrp_node
--- @diagnostic disable --- @diagnostic disable
@@ -13,7 +70,13 @@ local STBRP_SORT = table.sort
local STBRP_ASSERT = assert local STBRP_ASSERT = assert
local STBRP__NOTUSED = function(_) end local STBRP__NOTUSED = function(_) end
-------------------------------------------------------------------------------
--
-- INCLUDE SECTION
--
local STBRP__MAXVAL = 0x7fffffff local STBRP__MAXVAL = 0x7fffffff
-- Mostly for internal use, but this is the maximum supported coordinate value.
--- @enum STBRP_HEURISTIC_Skyline --- @enum STBRP_HEURISTIC_Skyline
local STBRP_HEURISTIC_Skyline = { local STBRP_HEURISTIC_Skyline = {
@@ -44,12 +107,17 @@ local STBRP__PREV_LINK_TYPE = {
--- @nodiscard --- @nodiscard
local function stbrp_rect() local function stbrp_rect()
return { return {
-- reserved for your use:
id = 0, id = 0,
-- input:
w = 0, w = 0,
h = 0, h = 0,
-- output:
x = 0, x = 0,
y = 0, y = 0,
was_packed = 0 was_packed = 0 -- non-zero if valid packing
} }
end end
@@ -92,7 +160,7 @@ local function stbrp_context()
active_head = nil, active_head = nil,
free_head = nil, free_head = nil,
extra = {stbrp_node(), stbrp_node()} extra = {stbrp_node(), stbrp_node()} -- we allocate two extra nodes so optimal user-node-count is 'width' not 'width+2'
} }
end end
@@ -113,13 +181,14 @@ local function stbrp__findresult()
} }
end end
-------------------------- -------------------------------------------------------------------------------
-------------------------- --
--- -- IMPLEMENTATION SECTION
--- IMPLEMENTATION SECTION --
---
---
-- Optionally select which packing heuristic the library should use. Different
-- heuristics will produce better/worse results for different data sets.
-- If you call init again, this will be reset to the default.
--- @param context stbrp_context --- @param context stbrp_context
--- @param heuristic int --- @param heuristic int
local function stbrp_setup_heuristic(context, heuristic) local function stbrp_setup_heuristic(context, heuristic)
@@ -131,16 +200,49 @@ local function stbrp_setup_heuristic(context, heuristic)
end end
end end
-- Optionally call this function after init but before doing any packing to
-- change the handling of the out-of-temp-memory scenario, described above.
-- If you call init again, this will be reset to the default (false).
--- @param context stbrp_context --- @param context stbrp_context
--- @param allow_out_of_mem boolean --- @param allow_out_of_mem boolean
local function stbrp_setup_allow_out_of_mem(context, allow_out_of_mem) local function stbrp_setup_allow_out_of_mem(context, allow_out_of_mem)
if allow_out_of_mem then if allow_out_of_mem then
-- if it's ok to run out of memory, then don't bother aligning them;
-- this gives better packing, but may fail due to OOM (even though
-- the rectangles easily fit). @TODO a smarter approach would be to only
-- quantize once we've hit OOM, then we could get rid of this parameter.
context.align = 1 context.align = 1
else else
-- if it's not ok to run out of memory, then quantize the widths
-- so that num_nodes is always enough nodes.
--
-- I.e. num_nodes * align >= width
-- align >= width / num_nodes
-- align = ceil(width/num_nodes)
context.align = (context.width + context.num_nodes - 1) / context.num_nodes context.align = (context.width + context.num_nodes - 1) / context.num_nodes
end end
end end
-- Initialize a rectangle packer to:
-- pack a rectangle that is 'width' by 'height' in dimensions
-- using temporary storage provided by the array 'nodes', which is 'num_nodes' long
--
-- You must call this function every time you start packing into a new target.
--
-- There is no "shutdown" function. The 'nodes' memory must stay valid for
-- the following stbrp_pack_rects() call (or calls), but can be freed after
-- the call (or calls) finish.
--
-- Note: to guarantee best results, either:
-- 1. make sure 'num_nodes' >= 'width'
-- or 2. call stbrp_setup_allow_out_of_mem() defined above with 'allow_out_of_mem = true'
--
-- If you don't do either of the above things, widths will be quantized to multiples
-- of small integers to guarantee the algorithm doesn't run out of temporary storage.
--
-- If you do #2, then the non-quantized algorithm will be used, but the algorithm
-- may run out of temporary storage and be unable to pack some rectangles.
--- @param context stbrp_context --- @param context stbrp_context
--- @param width int --- @param width int
--- @param height int --- @param height int
@@ -160,6 +262,7 @@ function stbrp_init_target(context, width, height, nodes, num_nodes)
context.num_nodes = num_nodes context.num_nodes = num_nodes
stbrp_setup_allow_out_of_mem(context, false) stbrp_setup_allow_out_of_mem(context, false)
-- node 0 is the full width, node 1 is the sentinel (lets us not store width explicitly)
context.extra[1].x = 0 context.extra[1].x = 0
context.extra[1].y = 0 context.extra[1].y = 0
context.extra[1].next = context.extra[2] context.extra[1].next = context.extra[2]
@@ -168,6 +271,8 @@ function stbrp_init_target(context, width, height, nodes, num_nodes)
context.extra[2].next = nil context.extra[2].next = nil
end end
-- find minimum y position if it starts at x1
-- Returns (min_y, waste_area)
--- @param c stbrp_context --- @param c stbrp_context
--- @param first stbrp_node --- @param first stbrp_node
--- @param x0 int --- @param x0 int
@@ -181,7 +286,7 @@ local function stbrp__skyline_find_min_y(c, first, x0, width)
STBRP_ASSERT(first.x <= x0) STBRP_ASSERT(first.x <= x0)
STBRP_ASSERT(node.next.x > x0) STBRP_ASSERT(node.next.x > x0) -- we ended up handling this in the caller for efficiency
STBRP_ASSERT(node.x <= x0) STBRP_ASSERT(node.x <= x0)
@@ -190,14 +295,19 @@ local function stbrp__skyline_find_min_y(c, first, x0, width)
local visited_width = 0 local visited_width = 0
while node.x < x1 do while node.x < x1 do
if node.y > min_y then if node.y > min_y then
-- raise min_y higher.
-- we've accounted for all waste up to min_y,
-- but we'll now add more waste for everything we've visited
waste_area = waste_area + visited_width * (node.y - min_y) waste_area = waste_area + visited_width * (node.y - min_y)
min_y = node.y min_y = node.y
-- the first time through, visited_width might be reduced
if node.x < x0 then if node.x < x0 then
visited_width = visited_width + node.next.x - x0 visited_width = visited_width + node.next.x - x0
else else
visited_width = visited_width + node.next.x - node.x visited_width = visited_width + node.next.x - node.x
end end
else else
-- add waste area
local under_width = node.next.x - node.x local under_width = node.next.x - node.x
if under_width + visited_width > width then if under_width + visited_width > width then
under_width = width - visited_width under_width = width - visited_width
@@ -223,10 +333,12 @@ local function stbrp__skyline_find_best_pos(c, width, height)
local best_prev_link = nil local best_prev_link = nil
local best_prev_link_type = nil local best_prev_link_type = nil
-- align to multiple of c.align
width = width + c.align - 1 width = width + c.align - 1
width = width - width % c.align width = width - width % c.align
STBRP_ASSERT(width % c.align == 0) STBRP_ASSERT(width % c.align == 0)
-- if it can't possibly fit, bail immediately
if width > c.width or height > c.height then if width > c.width or height > c.height then
fr.prev_link = nil fr.prev_link = nil
fr.prev_link_type = nil fr.prev_link_type = nil
@@ -242,7 +354,8 @@ local function stbrp__skyline_find_best_pos(c, width, height)
while (node.x + width <= c.width) do while (node.x + width <= c.width) do
local y, waste = stbrp__skyline_find_min_y(c, node, node.x, width) local y, waste = stbrp__skyline_find_min_y(c, node, node.x, width)
if c.heuristic == STBRP_HEURISTIC_Skyline.BL_sortHeight then if c.heuristic == STBRP_HEURISTIC_Skyline.BL_sortHeight then -- actually just want to test BL
-- bottom left
if y < best_y then if y < best_y then
best_y = y best_y = y
best_prev_link = prev_link best_prev_link = prev_link
@@ -250,7 +363,9 @@ local function stbrp__skyline_find_best_pos(c, width, height)
best_x = node.x best_x = node.x
end end
else else
-- best-fit
if y + height <= c.height then if y + height <= c.height then
-- can only use it if it first vertically
if y < best_y or (y == best_y and waste < best_waste) then if y < best_y or (y == best_y and waste < best_waste) then
best_y = y best_y = y
best_waste = waste best_waste = waste
@@ -266,12 +381,30 @@ local function stbrp__skyline_find_best_pos(c, width, height)
node = node.next node = node.next
end end
-- if doing best-fit (BF), we also have to try aligning right edge to each node position
--
-- e.g, if fitting
--
-- ____________________
-- |____________________|
--
-- into
--
-- | |
-- | ____________|
-- |____________|
--
-- then right-aligned reduces waste, but bottom-left BL is always chooses left-aligned
--
-- This makes BF take about 2x the time
if c.heuristic == STBRP_HEURISTIC_Skyline.BF_sortHeight then if c.heuristic == STBRP_HEURISTIC_Skyline.BF_sortHeight then
local tail = c.active_head local tail = c.active_head
local node_scan = c.active_head local node_scan = c.active_head
local prev_link_scan = c local prev_link_scan = c
local prev_link_scan_type = STBRP__PREV_LINK_TYPE.ACTIVE_HEAD local prev_link_scan_type = STBRP__PREV_LINK_TYPE.ACTIVE_HEAD
-- find first node that's admissible
while tail and tail.x < width do while tail and tail.x < width do
tail = tail.next tail = tail.next
end end
@@ -280,6 +413,7 @@ local function stbrp__skyline_find_best_pos(c, width, height)
local xpos = tail.x - width local xpos = tail.x - width
STBRP_ASSERT(xpos >= 0) STBRP_ASSERT(xpos >= 0)
-- find the left position that matches this
while node_scan.next and node_scan.next.x <= xpos do while node_scan.next and node_scan.next.x <= xpos do
prev_link_scan = node_scan prev_link_scan = node_scan
prev_link_scan_type = STBRP__PREV_LINK_TYPE.NEXT prev_link_scan_type = STBRP__PREV_LINK_TYPE.NEXT
@@ -295,6 +429,7 @@ local function stbrp__skyline_find_best_pos(c, width, height)
if y <= best_y then if y <= best_y then
if y < best_y or waste < best_waste or (waste == best_waste and xpos < best_x) then if y < best_y or waste < best_waste or (waste == best_waste and xpos < best_x) then
best_x = xpos best_x = xpos
-- STBRP_ASSERT(y <= best_y); [DEAR IMGUI]
best_y = y best_y = y
best_waste = waste best_waste = waste
best_prev_link = prev_link_scan best_prev_link = prev_link_scan
@@ -320,19 +455,29 @@ end
--- @param height int --- @param height int
--- @return stbrp__findresult --- @return stbrp__findresult
local function stbrp__skyline_pack_rectangle(context, width, height) local function stbrp__skyline_pack_rectangle(context, width, height)
-- find best position according to heuristic
local res = stbrp__skyline_find_best_pos(context, width, height) local res = stbrp__skyline_find_best_pos(context, width, height)
-- bail if:
-- 1. it failed
-- 2. the best node doesn't fit (we don't always check this)
-- 3. we're out of memory
if res.prev_link == nil or res.y + height > context.height or context.free_head == nil then if res.prev_link == nil or res.y + height > context.height or context.free_head == nil then
res.prev_link = nil res.prev_link = nil
res.prev_link_type = nil res.prev_link_type = nil
return res return res
end end
-- on success, create new node
local node = context.free_head local node = context.free_head
node.x = res.x node.x = res.x
node.y = res.y + height node.y = res.y + height
context.free_head = node.next context.free_head = node.next
-- insert the new node into the right starting point, and
-- let 'cur' point to the remaining nodes needing to be
-- stitched back in
local cur local cur
if res.prev_link_type == STBRP__PREV_LINK_TYPE.ACTIVE_HEAD then if res.prev_link_type == STBRP__PREV_LINK_TYPE.ACTIVE_HEAD then
cur = res.prev_link.active_head cur = res.prev_link.active_head
@@ -341,6 +486,7 @@ local function stbrp__skyline_pack_rectangle(context, width, height)
end end
if cur.x < res.x then if cur.x < res.x then
-- preserve the existing one, so start testing with the next one
local next = cur.next local next = cur.next
cur.next = node cur.next = node
cur = next cur = next
@@ -352,13 +498,17 @@ local function stbrp__skyline_pack_rectangle(context, width, height)
end end
end end
-- from here, traverse cur and free the nodes, until we get to one
-- that shouldn't be freed
while cur.next and cur.next.x <= res.x + width do while cur.next and cur.next.x <= res.x + width do
local next = cur.next local next = cur.next
-- move the current node to the free list
cur.next = context.free_head cur.next = context.free_head
context.free_head = cur context.free_head = cur
cur = next cur = next
end end
-- stitch the list back in
node.next = cur node.next = cur
if cur.x < res.x + width then if cur.x < res.x + width then
@@ -390,6 +540,29 @@ local function rect_original_order(a, b)
return a.was_packed < b.was_packed return a.was_packed < b.was_packed
end end
-- Assign packed locations to rectangles. The rectangles are of type
-- 'stbrp_rect' defined above, stored in the array 'rects', and there
-- are 'num_rects' many of them.
--
-- Rectangles which are successfully packed have the 'was_packed' flag
-- set to a non-zero value and 'x' and 'y' store the minimum location
-- on each axis (i.e. bottom-left in cartesian coordinates, top-left
-- if you imagine y increasing downwards). Rectangles which do not fit
-- have the 'was_packed' flag set to 0.
--
-- You should not try to access the 'rects' array from another thread
-- while this function is running, as the function temporarily reorders
-- the array while it executes.
--
-- To pack into another rectangle, you need to call stbrp_init_target
-- again. To continue packing into the same rectangle, you can call
-- this function again. Calling this multiple times with multiple rect
-- arrays will probably produce worse packing results than calling it
-- a single time with the full rectangle array, but the option is
-- available.
--
-- The function returns 1 if all of the rectangles were successfully
-- packed and 0 otherwise.
--- @param context stbrp_context --- @param context stbrp_context
--- @param rects stbrp_rect[] --- @param rects stbrp_rect[]
--- @param num_rects int --- @param num_rects int
@@ -397,15 +570,17 @@ end
local function stbrp_pack_rects(context, rects, num_rects) local function stbrp_pack_rects(context, rects, num_rects)
local all_rects_packed = 1 local all_rects_packed = 1
-- we use the 'was_packed' field internally to allow sorting/unsorting
for i = 1, num_rects do for i = 1, num_rects do
rects[i].was_packed = (i - 1) rects[i].was_packed = (i - 1)
end end
-- sort according to heuristic
STBRP_SORT(rects, rect_height_compare) STBRP_SORT(rects, rect_height_compare)
for i = 1, num_rects do for i = 1, num_rects do
if rects[i].w == 0 or rects[i].h == 0 then if rects[i].w == 0 or rects[i].h == 0 then
rects[i].x = 0 rects[i].x = 0 -- empty rect needs no space
rects[i].y = 0 rects[i].y = 0
else else
local fr = stbrp__skyline_pack_rectangle(context, rects[i].w, rects[i].h) local fr = stbrp__skyline_pack_rectangle(context, rects[i].w, rects[i].h)
@@ -419,8 +594,10 @@ local function stbrp_pack_rects(context, rects, num_rects)
end end
end end
-- unsort
STBRP_SORT(rects, rect_original_order) STBRP_SORT(rects, rect_original_order)
-- set was_packed flags and all_rects_packed status
for i = 1, num_rects do for i = 1, num_rects do
rects[i].was_packed = (rects[i].x == STBRP__MAXVAL and rects[i].y == STBRP__MAXVAL) and 0 or 1 rects[i].was_packed = (rects[i].x == STBRP__MAXVAL and rects[i].y == STBRP__MAXVAL) and 0 or 1
if rects[i].was_packed == 0 then if rects[i].was_packed == 0 then
@@ -428,6 +605,7 @@ local function stbrp_pack_rects(context, rects, num_rects)
end end
end end
-- return the all_rects_packed status
return all_rects_packed return all_rects_packed
end end
@@ -441,3 +619,43 @@ return {
setup_allow_out_of_mem = stbrp_setup_allow_out_of_mem, setup_allow_out_of_mem = stbrp_setup_allow_out_of_mem,
setup_heuristic = stbrp_setup_heuristic setup_heuristic = stbrp_setup_heuristic
} }
-- ------------------------------------------------------------------------------
-- This software is available under 2 licenses -- choose whichever you prefer.
-- ------------------------------------------------------------------------------
-- ALTERNATIVE A - MIT License
-- Copyright (c) 2017 Sean Barrett
-- Permission is hereby granted, free of charge, to any person obtaining a copy of
-- this software and associated documentation files (the "Software"), to deal in
-- the Software without restriction, including without limitation the rights to
-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
-- of the Software, and to permit persons to whom the Software is furnished to do
-- so, subject to the following conditions:
-- The above copyright notice and this permission notice shall be included in all
-- copies or substantial portions of the Software.
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-- SOFTWARE.
-- ------------------------------------------------------------------------------
-- ALTERNATIVE B - Public Domain (www.unlicense.org)
-- This is free and unencumbered software released into the public domain.
-- Anyone is free to copy, modify, publish, use, compile, sell, or distribute this
-- software, either in source code form or as a compiled binary, for any purpose,
-- commercial or non-commercial, and by any means.
-- In jurisdictions that recognize copyright laws, the author or authors of this
-- software dedicate any and all copyright interest in the software to the public
-- domain. We make this dedication for the benefit of the public at large and to
-- the detriment of our heirs and successors. We intend this dedication to be an
-- overt act of relinquishment in perpetuity of all present and future rights to
-- this software under copyright law.
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-- AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
-- ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
-- WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-- ------------------------------------------------------------------------------
+336 -35
View File
@@ -1,6 +1,221 @@
--- Dear ImGui --- Dear ImGui
-- This is a Lua port of original `imstb_textedit.h` -- This is a Lua port of original `imstb_textedit.h`
-- [DEAR IMGUI]
-- This is a slightly modified version of stb_textedit.h 1.14.
-- Those changes would need to be pushed into nothings/stb:
-- - Fix in stb_textedit_discard_redo (see https://github.com/nothings/stb/issues/321)
-- - Fix in stb_textedit_find_charpos to handle last line (see https://github.com/ocornut/imgui/issues/6000 + #6783)
-- - Added name to struct or it may be forward declared in our code.
-- - Added UTF-8 support (see https://github.com/nothings/stb/issues/188 + https://github.com/ocornut/imgui/pull/7925)
-- - Changed STB_TEXTEDIT_INSERTCHARS() to return inserted count (instead of 0/1 bool), allowing partial insertion.
-- Grep for [DEAR IMGUI] to find some changes.
-- - Also renamed macros used or defined outside of IMSTB_TEXTEDIT_IMPLEMENTATION block from STB_TEXTEDIT_* to IMSTB_TEXTEDIT_*
-- stb_textedit.h - v1.14 - public domain - Sean Barrett
-- Development of this library was sponsored by RAD Game Tools
--
-- This C header file implements the guts of a multi-line text-editing
-- widget; you implement display, word-wrapping, and low-level string
-- insertion/deletion, and stb_textedit will map user inputs into
-- insertions & deletions, plus updates to the cursor position,
-- selection state, and undo state.
--
-- It is intended for use in games and other systems that need to build
-- their own custom widgets and which do not have heavy text-editing
-- requirements (this library is not recommended for use for editing large
-- texts, as its performance does not scale and it has limited undo).
--
-- Non-trivial behaviors are modelled after Windows text controls.
--
--
-- LICENSE
--
-- See end of file for license information.
--
--
-- DEPENDENCIES
--
-- Uses the C runtime function 'memmove', which you can override
-- by defining IMSTB_TEXTEDIT_memmove before the implementation.
-- (In this port: ImStb.TEXTEDIT_memmove.)
--
--
-- VERSION HISTORY
--
-- !!!! (2025-10-23) changed STB_TEXTEDIT_INSERTCHARS() to return inserted count (instead of 0/1 bool), allowing partial insertion.
-- 1.14 (2021-07-11) page up/down, various fixes
-- 1.13 (2019-02-07) fix bug in undo size management
-- 1.12 (2018-01-29) user can change STB_TEXTEDIT_KEYTYPE, fix redo to avoid crash
-- 1.11 (2017-03-03) fix HOME on last line, dragging off single-line textfield
-- 1.10 (2016-10-25) suppress warnings about casting away const with -Wcast-qual
-- 1.9 (2016-08-27) customizable move-by-word
-- 1.8 (2016-04-02) better keyboard handling when mouse button is down
-- 1.7 (2015-09-13) change y range handling in case baseline is non-0
-- 1.6 (2015-04-15) allow STB_TEXTEDIT_memmove
-- 1.5 (2014-09-10) add support for secondary keys for OS X
-- 1.4 (2014-08-17) fix signed/unsigned warnings
-- 1.3 (2014-06-19) fix mouse clicking to round to nearest char boundary
-- 1.2 (2014-05-27) fix some RAD types that had crept into the new code
-- 1.1 (2013-12-15) move-by-word (requires STB_TEXTEDIT_IS_SPACE )
-- 1.0 (2012-07-26) improve documentation, initial public release
-- 0.3 (2012-02-24) bugfixes, single-line mode; insert mode
-- 0.2 (2011-11-28) fixes to undo/redo
-- 0.1 (2010-07-08) initial version
--
-- ADDITIONAL CONTRIBUTORS
--
-- Ulf Winklemann: move-by-word in 1.1
-- Fabian Giesen: secondary key inputs in 1.5
-- Martins Mozeiko: STB_TEXTEDIT_memmove in 1.6
-- Louis Schnellbach: page up/down in 1.14
--
-- Bugfixes:
-- Scott Graham
-- Daniel Keller
-- Omar Cornut
-- Dan Thompson
--
-- USAGE
--
-- The basic concept is that you provide a "string" object which
-- behaves like an array of characters. stb_textedit uses indices to
-- refer to positions in the string, implicitly representing positions
-- in the displayed textedit. This is true for both plain text and
-- rich text; even with rich text stb_truetype interacts with your
-- code as if there was an array of all the displayed characters.
--
-- Symbols you must define for implementation mode (in this port: fields of ImStb, see below):
--
-- STB_TEXTEDIT_STRING the type of object representing a string being edited,
-- typically this is a wrapper object with other data you need
--
-- STB_TEXTEDIT_STRINGLEN(obj) the length of the string (ideally O(1))
-- STB_TEXTEDIT_LAYOUTROW(r,obj,n) returns the results of laying out a line of characters
-- starting from character #n (see discussion below)
-- STB_TEXTEDIT_GETWIDTH(obj,n,i) returns the pixel delta from the xpos of the i'th character
-- to the xpos of the i+1'th char for a line of characters
-- starting at character #n (i.e. accounts for kerning
-- with previous char)
-- STB_TEXTEDIT_GETCHAR(obj,i) returns the i'th character of obj, 1-based in this port
-- STB_TEXTEDIT_NEWLINE the character returned by _GETCHAR() we recognize
-- as manually wordwrapping for end-of-line positioning
--
-- STB_TEXTEDIT_DELETECHARS(obj,i,n) delete n characters starting at i
-- STB_TEXTEDIT_INSERTCHARS(obj,i,c,c_offset,n) try to insert n characters at i (taken from array c starting at c_offset)
-- returns number of characters actually inserted. [DEAR IMGUI]
--
-- STB_TEXTEDIT_K_SHIFT a power of two that is or'd in to a keyboard input to represent the shift key
--
-- STB_TEXTEDIT_K_LEFT keyboard input to move cursor left
-- STB_TEXTEDIT_K_RIGHT keyboard input to move cursor right
-- STB_TEXTEDIT_K_UP keyboard input to move cursor up
-- STB_TEXTEDIT_K_DOWN keyboard input to move cursor down
-- STB_TEXTEDIT_K_PGUP keyboard input to move cursor up a page
-- STB_TEXTEDIT_K_PGDOWN keyboard input to move cursor down a page
-- STB_TEXTEDIT_K_LINESTART keyboard input to move cursor to start of line -- e.g. HOME
-- STB_TEXTEDIT_K_LINEEND keyboard input to move cursor to end of line -- e.g. END
-- STB_TEXTEDIT_K_TEXTSTART keyboard input to move cursor to start of text -- e.g. ctrl-HOME
-- STB_TEXTEDIT_K_TEXTEND keyboard input to move cursor to end of text -- e.g. ctrl-END
-- STB_TEXTEDIT_K_DELETE keyboard input to delete selection or character under cursor
-- STB_TEXTEDIT_K_BACKSPACE keyboard input to delete selection or character left of cursor
-- STB_TEXTEDIT_K_UNDO keyboard input to perform undo
-- STB_TEXTEDIT_K_REDO keyboard input to perform redo
--
-- STB_TEXTEDIT_MOVEWORDLEFT(obj,i) custom handler for WORDLEFT, returns index to move cursor to
-- STB_TEXTEDIT_MOVEWORDRIGHT(obj,i) custom handler for WORDRIGHT, returns index to move cursor to
-- STB_TEXTEDIT_K_WORDLEFT keyboard input to move cursor left one word -- e.g. ctrl-LEFT
-- STB_TEXTEDIT_K_WORDRIGHT keyboard input to move cursor right one word -- e.g. ctrl-RIGHT
--
-- To support UTF-8:
--
-- STB_TEXTEDIT_GETPREVCHARINDEX returns index of previous character
-- STB_TEXTEDIT_GETNEXTCHARINDEX returns index of next character
-- Do NOT define STB_TEXTEDIT_KEYTOTEXT.
-- Instead, call stb_textedit_text() directly for text contents.
--
-- Keyboard input must be encoded as a single integer value; e.g. a character code
-- and some bitflags that represent shift states. to simplify the interface, SHIFT must
-- be a bitflag, so we can test the shifted state of cursor movements to allow selection,
-- i.e. (STB_TEXTEDIT_K_RIGHT|STB_TEXTEDIT_K_SHIFT) should be shifted right-arrow.
--
-- STB_TEXTEDIT_LAYOUTROW returns information about the shape of one displayed
-- row of characters assuming they start on the i'th character--the width and
-- the height and the number of characters consumed. This allows this library
-- to traverse the entire layout incrementally. You need to compute word-wrapping
-- here.
--
-- Each textfield keeps its own insert mode state, which is not how normal
-- applications work. To keep an app-wide insert mode, update/copy the
-- "insert_mode" field of STB_TexteditState before/after calling API functions.
--
-- API (in this port: fields of the table returned by this file)
--
-- initialize_state(state, is_single_line)
--
-- click(str, state, x, y)
-- drag(str, state, x, y)
-- cut(str, state) -> bool
-- paste(str, state, text, len) -> bool
-- key(str, state, key)
-- text(str, state, text, text_len)
--
-- Each of these functions potentially updates the string and updates the
-- state.
--
-- initialize_state:
-- set the textedit state to a known good default state when initially
-- constructing the textedit.
--
-- click:
-- call this with the mouse x,y on a mouse down; it will update the cursor
-- and reset the selection start/end to the cursor point. the x,y must
-- be relative to the text widget, with (0,0) being the top left.
--
-- drag:
-- call this with the mouse x,y on a mouse drag/up; it will update the
-- cursor and the selection end point
--
-- cut:
-- call this to delete the current selection; returns true if there was
-- one. you should FIRST copy the current selection to the system paste buffer.
-- (To copy, just copy the current selection out of the string yourself.)
--
-- paste:
-- call this to paste text at the current cursor point or over the current
-- selection if there is one.
--
-- key:
-- call this for keyboard inputs sent to the textfield. you can use it
-- for "key down" events or for "translated" key events.
--
-- text: (added 2025)
-- call this to directly send text input the textfield, which is required
-- for UTF-8 support, because stb_textedit_key() + STB_TEXTEDIT_KEYTOTEXT()
-- cannot infer text length.
--
--
-- When rendering, you can read the cursor position and selection state from
-- the STB_TexteditState.
--
--
-- Notes:
--
-- This is designed to be usable in IMGUI, so it allows for the possibility of
-- running in an IMGUI that has NOT cached the multi-line layout. For this
-- reason, it provides an interface that is compatible with computing the
-- layout incrementally--we try to make sure we make as few passes through
-- as possible. (For example, to locate the mouse pointer in the text, we
-- could define functions that return the X and Y positions of characters
-- and binary search Y and then X, but if we're doing dynamic layout this
-- will run the layout algorithm many times, so instead we manually search
-- forward in one pass. Similar logic applies to e.g. up-arrow and
-- down-arrow movement.)
--
-- If it's run in a widget that *has* cached the layout, then this is less
-- efficient, but it's not horrible on modern computers. But you wouldn't
-- want to edit million-line files with it.
-- ALL TABLES IN THIS FILE ARE 1-BASED! -- ALL TABLES IN THIS FILE ARE 1-BASED!
-- Character Indexing: -- Character Indexing:
-- idx=n, the n'th char (1-based) -- idx=n, the n'th char (1-based)
@@ -13,6 +228,10 @@ local STB_TEXTEDIT_STRINGLEN = ImStb.TEXTEDIT_STRINGLEN
local STB_TEXTEDIT_GETCHAR = ImStb.TEXTEDIT_GETCHAR local STB_TEXTEDIT_GETCHAR = ImStb.TEXTEDIT_GETCHAR
local STB_TEXTEDIT_GETWIDTH = ImStb.TEXTEDIT_GETWIDTH local STB_TEXTEDIT_GETWIDTH = ImStb.TEXTEDIT_GETWIDTH
local STB_TEXTEDIT_LAYOUTROW = ImStb.TEXTEDIT_LAYOUTROW local STB_TEXTEDIT_LAYOUTROW = ImStb.TEXTEDIT_LAYOUTROW
-- [DEAR IMGUI]
-- Functions must be implemented for UTF8 support
-- Code in this file that uses those functions is modified for [DEAR IMGUI] and deviates from the original stb_textedit.
-- There is not necessarily a '[DEAR IMGUI]' at the usage sites.
local IMSTB_TEXTEDIT_GETNEXTCHARINDEX = ImStb.TEXTEDIT_GETNEXTCHARINDEX local IMSTB_TEXTEDIT_GETNEXTCHARINDEX = ImStb.TEXTEDIT_GETNEXTCHARINDEX
local IMSTB_TEXTEDIT_GETPREVCHARINDEX = ImStb.TEXTEDIT_GETPREVCHARINDEX local IMSTB_TEXTEDIT_GETPREVCHARINDEX = ImStb.TEXTEDIT_GETPREVCHARINDEX
local STB_TEXTEDIT_MOVEWORDLEFT = ImStb.TEXTEDIT_MOVEWORDLEFT local STB_TEXTEDIT_MOVEWORDLEFT = ImStb.TEXTEDIT_MOVEWORDLEFT
@@ -22,34 +241,35 @@ local STB_TEXTEDIT_MOVELINEEND = ImStb.TEXTEDIT_MOVELINEEND
local STB_TEXTEDIT_DELETECHARS = ImStb.TEXTEDIT_DELETECHARS local STB_TEXTEDIT_DELETECHARS = ImStb.TEXTEDIT_DELETECHARS
local STB_TEXTEDIT_INSERTCHARS = ImStb.TEXTEDIT_INSERTCHARS local STB_TEXTEDIT_INSERTCHARS = ImStb.TEXTEDIT_INSERTCHARS
STB_TEXTEDIT_NEWLINE = 10 STB_TEXTEDIT_NEWLINE = 10 -- the character returned by _GETCHAR() we recognize as manually wordwrapping for end-of-line positioning
STB_TEXTEDIT_K_LEFT = 0x200000 -- keyboard input to move cursor left STB_TEXTEDIT_K_LEFT = 0x200000 -- keyboard input to move cursor left
STB_TEXTEDIT_K_RIGHT = 0x200001 -- keyboard input to move cursor right STB_TEXTEDIT_K_RIGHT = 0x200001 -- keyboard input to move cursor right
STB_TEXTEDIT_K_UP = 0x200002 -- keyboard input to move cursor up STB_TEXTEDIT_K_UP = 0x200002 -- keyboard input to move cursor up
STB_TEXTEDIT_K_DOWN = 0x200003 -- keyboard input to move cursor down STB_TEXTEDIT_K_DOWN = 0x200003 -- keyboard input to move cursor down
STB_TEXTEDIT_K_LINESTART = 0x200004 -- keyboard input to move cursor to start of line STB_TEXTEDIT_K_LINESTART = 0x200004 -- keyboard input to move cursor to start of line -- e.g. HOME
STB_TEXTEDIT_K_LINEEND = 0x200005 -- keyboard input to move cursor to end of line STB_TEXTEDIT_K_LINEEND = 0x200005 -- keyboard input to move cursor to end of line -- e.g. END
STB_TEXTEDIT_K_TEXTSTART = 0x200006 -- keyboard input to move cursor to start of text STB_TEXTEDIT_K_TEXTSTART = 0x200006 -- keyboard input to move cursor to start of text -- e.g. ctrl-HOME
STB_TEXTEDIT_K_TEXTEND = 0x200007 -- keyboard input to move cursor to end of text STB_TEXTEDIT_K_TEXTEND = 0x200007 -- keyboard input to move cursor to end of text -- e.g. ctrl-END
STB_TEXTEDIT_K_DELETE = 0x200008 -- keyboard input to delete selection or character under cursor STB_TEXTEDIT_K_DELETE = 0x200008 -- keyboard input to delete selection or character under cursor
STB_TEXTEDIT_K_BACKSPACE = 0x200009 -- keyboard input to delete selection or character left of cursor STB_TEXTEDIT_K_BACKSPACE = 0x200009 -- keyboard input to delete selection or character left of cursor
STB_TEXTEDIT_K_UNDO = 0x20000A -- keyboard input to perform undo STB_TEXTEDIT_K_UNDO = 0x20000A -- keyboard input to perform undo
STB_TEXTEDIT_K_REDO = 0x20000B -- keyboard input to perform redo STB_TEXTEDIT_K_REDO = 0x20000B -- keyboard input to perform redo
STB_TEXTEDIT_K_WORDLEFT = 0x20000C -- keyboard input to move cursor left one word STB_TEXTEDIT_K_WORDLEFT = 0x20000C -- keyboard input to move cursor left one word -- e.g. ctrl-LEFT
STB_TEXTEDIT_K_WORDRIGHT = 0x20000D -- keyboard input to move cursor right one word STB_TEXTEDIT_K_WORDRIGHT = 0x20000D -- keyboard input to move cursor right one word -- e.g. ctrl-RIGHT
STB_TEXTEDIT_K_PGUP = 0x20000E -- keyboard input to move cursor up a page STB_TEXTEDIT_K_PGUP = 0x20000E -- keyboard input to move cursor up a page
STB_TEXTEDIT_K_PGDOWN = 0x20000F -- keyboard input to move cursor down a page STB_TEXTEDIT_K_PGDOWN = 0x20000F -- keyboard input to move cursor down a page
STB_TEXTEDIT_K_SHIFT = 0x400000 STB_TEXTEDIT_K_SHIFT = 0x400000 -- a power of two that is or'd in to a keyboard input to represent the shift key
IMSTB_TEXTEDIT_UNDOSTATECOUNT = 99 IMSTB_TEXTEDIT_UNDOSTATECOUNT = 99 -- the number of undo states to allow
local IMSTB_TEXTEDIT_UNDOCHARCOUNT = 999 local IMSTB_TEXTEDIT_UNDOCHARCOUNT = 999 -- the number of characters to store in the undo buffer
local IMSTB_TEXTEDIT_memmove = ImStb.TEXTEDIT_memmove local IMSTB_TEXTEDIT_memmove = ImStb.TEXTEDIT_memmove
-- forward declarations
local stb_text_undo local stb_text_undo
local stb_text_redo local stb_text_redo
---------------------------------------------------------------- -------------------------------------------------------------------------------
---------------------------------------------------------------- -------------------------------------------------------------------------------
--- ---
--- STB_TexteditState --- STB_TexteditState
--- ---
@@ -72,6 +292,7 @@ local stb_text_redo
--- @nodiscard --- @nodiscard
local function StbUndoRecord() local function StbUndoRecord()
return { return {
-- private data
where = 1, where = 1,
insert_length = 0, insert_length = 0,
delete_length = 0, delete_length = 0,
@@ -101,6 +322,7 @@ local function StbUndoState()
end end
return { return {
-- private data
undo_rec = undo_rec, undo_rec = undo_rec,
undo_char = undo_char, undo_char = undo_char,
@@ -131,16 +353,34 @@ end
--- @nodiscard --- @nodiscard
function STB_TexteditState() function STB_TexteditState()
return { return {
cursor = 0, ---------------------
--
-- public data
--
select_start = 0, cursor = 0,
-- position of the text cursor within the string
select_start = 0, -- selection start point
select_end = 0, select_end = 0,
-- selection start and end point in characters; if equal, no selection.
-- note that start may be less than or greater than end (e.g. when
-- dragging the mouse, start is where the initial click was, and you
-- can drag in either direction)
insert_mode = false, insert_mode = false,
-- each textfield keeps its own insert mode state. to keep an app-wide
-- insert mode, copy this value in/out of the app state
row_count_per_page = 0, row_count_per_page = 0,
-- page size in number of row.
-- this value MUST be set to >0 for pageup or pagedown in multilines documents.
cursor_at_end_of_line = false, ---------------------
--
-- private data
--
cursor_at_end_of_line = false, -- not implemented yet
initialized = false, initialized = false,
has_preferred_x = false, has_preferred_x = false,
single_line = false, single_line = false,
@@ -148,7 +388,7 @@ function STB_TexteditState()
padding2 = 0, padding2 = 0,
padding3 = 0, padding3 = 0,
preferred_x = 0.0, preferred_x = 0.0, -- this determines where the cursor up/down tries to seek to along x
undostate = StbUndoState() undostate = StbUndoState()
} }
@@ -159,12 +399,20 @@ local function STB_TEXT_HAS_SELECTION(s)
return s.select_start ~= s.select_end return s.select_start ~= s.select_end
end end
-------------------------------------------------------------------------------
---
--- StbTexteditRow
---
--- Result of layout query, used by stb_textedit to determine where
--- the text in each row is.
-- result of layout query
--- @class StbTexteditRow --- @class StbTexteditRow
--- @field x0 float # starting x location --- @field x0 float # starting x location, end x location (allows for align=right, etc)
--- @field x1 float # end x location --- @field x1 float # end x location
--- @field baseline_y_delta float # position of baseline relative to previous row's baseline --- @field baseline_y_delta float # position of baseline relative to previous row's baseline
--- @field ymin float # height of row above baseline --- @field ymin float # height of row above and below baseline
--- @field ymax float # height of row below baseline --- @field ymax float # height of row above and below baseline
--- @field num_chars int --- @field num_chars int
--- @return StbTexteditRow --- @return StbTexteditRow
@@ -196,10 +444,15 @@ end
---------------------------------------- ----------------------------------------
---------------------------------------- ----------------------------------------
--- ---
--- Implementation --- Implementation mode
--- ---
--- ---
-------------------------------------------------------------------------------
---
--- Mouse input handling
---
local stb_text_locate_coord do local stb_text_locate_coord do
-- only create once, reuse later -- only create once, reuse later
@@ -351,6 +604,7 @@ end
--- ---
--- ---
-- forward declarations
local stb_text_makeundo_insert local stb_text_makeundo_insert
local stb_text_makeundo_delete local stb_text_makeundo_delete
local stb_text_makeundo_replace local stb_text_makeundo_replace
@@ -399,8 +653,8 @@ function stb_textedit_find_charpos(find, str, n, single_line)
local z = STB_TEXTEDIT_STRINGLEN(str) local z = STB_TEXTEDIT_STRINGLEN(str)
local first local first
-- special case if it's at the end (may not be needed?)
if n == z + 1 and single_line then if n == z + 1 and single_line then
-- special case if it's at the end (may not be needed?)
STB_TEXTEDIT_LAYOUTROW(r, str, 1) STB_TEXTEDIT_LAYOUTROW(r, str, 1)
find.y = 0 find.y = 0
find.first_char = 1 find.first_char = 1
@@ -420,18 +674,18 @@ function stb_textedit_find_charpos(find, str, n, single_line)
if n < i + r.num_chars - 1 then if n < i + r.num_chars - 1 then
break break
end end
if str.LastMoveDirectionLR == ImGuiDir.Right and str.Stb.cursor > 1 and str.Stb.cursor == i + r.num_chars and STB_TEXTEDIT_GETCHAR(str, i + r.num_chars - 1) ~= STB_TEXTEDIT_NEWLINE then -- [IMGUI] Wrapping point handling if str.LastMoveDirectionLR == ImGuiDir.Right and str.Stb.cursor > 1 and str.Stb.cursor == i + r.num_chars and STB_TEXTEDIT_GETCHAR(str, i + r.num_chars - 1) ~= STB_TEXTEDIT_NEWLINE then -- [DEAR IMGUI] Wrapping point handling
break break
end end
if (i - 1) + r.num_chars == z and z > 0 and STB_TEXTEDIT_GETCHAR(str, z) ~= STB_TEXTEDIT_NEWLINE then -- [IMGUI] special handling for last line if (i - 1) + r.num_chars == z and z > 0 and STB_TEXTEDIT_GETCHAR(str, z) ~= STB_TEXTEDIT_NEWLINE then -- [DEAR IMGUI] special handling for last line
break break -- [DEAR IMGUI]
end end
prev_start = i prev_start = i
i = i + r.num_chars i = i + r.num_chars
find.y = find.y + r.baseline_y_delta find.y = find.y + r.baseline_y_delta
if i == z + 1 then -- [IMGUI] if i == z + 1 then -- [DEAR IMGUI]
r.num_chars = 0 r.num_chars = 0 -- [DEAR IMGUI]
break break -- [DEAR IMGUI]
end end
end end
@@ -479,7 +733,7 @@ local function stb_textedit_delete(str, state, where, len)
state.has_preferred_x = false state.has_preferred_x = false
end end
-- delete the selection -- delete the section
--- @param str IMSTB_TEXTEDIT_STRING --- @param str IMSTB_TEXTEDIT_STRING
--- @param state STB_TexteditState --- @param state STB_TexteditState
local function stb_textedit_delete_selection(str, state) local function stb_textedit_delete_selection(str, state)
@@ -498,7 +752,7 @@ local function stb_textedit_delete_selection(str, state)
end end
end end
-- canonicalize the selection so start <= end -- canoncialize the selection so start <= end
--- @param state STB_TexteditState --- @param state STB_TexteditState
local function stb_textedit_sortselection(state) local function stb_textedit_sortselection(state)
if state.select_end < state.select_start then if state.select_end < state.select_start then
@@ -579,7 +833,7 @@ local function stb_textedit_paste_internal(str, state, text, len)
end end
-- API key: process text input -- API key: process text input
-- [IMGUI] Added stb_textedit_text(), extracted out and called by stb_textedit_key() for backward compatibility. -- [DEAR IMGUI] Added stb_textedit_text(), extracted out and called by stb_textedit_key() for backward compatibility.
--- @param str IMSTB_TEXTEDIT_STRING --- @param str IMSTB_TEXTEDIT_STRING
--- @param state STB_TexteditState --- @param state STB_TexteditState
--- @param text IMSTB_TEXTEDIT_CHARTYPE[] --- @param text IMSTB_TEXTEDIT_CHARTYPE[]
@@ -732,7 +986,7 @@ function stb_textedit_key(str, state, key)
break break
end end
-- [IMGUI] -- [DEAR IMGUI]
-- going down while being on the last line shouldn't bring us to that line end -- going down while being on the last line shouldn't bring us to that line end
--if STB_TEXTEDIT_GETCHAR(str, find.first_char + find.length - 1) ~= STB_TEXTEDIT_NEWLINE then --if STB_TEXTEDIT_GETCHAR(str, find.first_char + find.length - 1) ~= STB_TEXTEDIT_NEWLINE then
-- break -- break
@@ -929,8 +1183,8 @@ end
----------------------------------------------------- -----------------------------------------------------
--- ---
--- Undo processing --- Undo processing
--- OPTIMIZE: the undo/redo buffer should be circular
--- ---
--- @OPTIMIZE: the undo/redo buffer should be circular
--- @param state StbUndoState --- @param state StbUndoState
local function stb_textedit_flush_redo(state) local function stb_textedit_flush_redo(state)
@@ -950,7 +1204,7 @@ local function stb_textedit_discard_undo(state)
IMSTB_TEXTEDIT_memmove(state.undo_char, 1, state.undo_char, n + 1, state.undo_char_point - 1) IMSTB_TEXTEDIT_memmove(state.undo_char, 1, state.undo_char, n + 1, state.undo_char_point - 1)
for i = 1, state.undo_point do for i = 1, state.undo_point do
if state.undo_rec[i].char_storage >= 0 then if state.undo_rec[i].char_storage >= 0 then
state.undo_rec[i].char_storage = state.undo_rec[i].char_storage - n -- OPTIMIZE: get rid of char_storage and infer it state.undo_rec[i].char_storage = state.undo_rec[i].char_storage - n -- @OPTIMIZE: get rid of char_storage and infer it
end end
end end
end end
@@ -983,7 +1237,7 @@ local function stb_textedit_discard_redo(state)
end end
-- now move all the redo records towards the end of the buffer; the first one is at 'redo_point' -- now move all the redo records towards the end of the buffer; the first one is at 'redo_point'
-- [IMGUI] -- [DEAR IMGUI]
local move_size = IMSTB_TEXTEDIT_UNDOSTATECOUNT - state.redo_point - 1 local move_size = IMSTB_TEXTEDIT_UNDOSTATECOUNT - state.redo_point - 1
IMSTB_TEXTEDIT_memmove(state.undo_rec, state.redo_point + 1, state.undo_rec, state.redo_point, move_size) IMSTB_TEXTEDIT_memmove(state.undo_rec, state.redo_point + 1, state.undo_rec, state.redo_point, move_size)
@@ -1129,6 +1383,9 @@ function stb_text_redo(str, state)
local u = s.undo_rec[s.undo_point] local u = s.undo_rec[s.undo_point]
local r = s.undo_rec[s.redo_point] local r = s.undo_rec[s.redo_point]
-- we KNOW there must be room for the undo record, because the redo record
-- was derived from an undo record
u.delete_length = r.insert_length u.delete_length = r.insert_length
u.insert_length = r.delete_length u.insert_length = r.delete_length
u.where = r.where u.where = r.where
@@ -1137,6 +1394,7 @@ function stb_text_redo(str, state)
if r.delete_length > 0 then if r.delete_length > 0 then
-- the redo record requires us to delete characters, so the undo record -- the redo record requires us to delete characters, so the undo record
-- needs to store the characters -- needs to store the characters
if s.undo_char_point + u.insert_length > s.redo_char_point then if s.undo_char_point + u.insert_length > s.redo_char_point then
u.insert_length = 0 u.insert_length = 0
u.delete_length = 0 u.delete_length = 0
@@ -1144,6 +1402,7 @@ function stb_text_redo(str, state)
u.char_storage = s.undo_char_point u.char_storage = s.undo_char_point
s.undo_char_point = s.undo_char_point + u.insert_length s.undo_char_point = s.undo_char_point + u.insert_length
-- now save the characters
for i = 0, u.insert_length - 1 do for i = 0, u.insert_length - 1 do
s.undo_char[u.char_storage + i] = STB_TEXTEDIT_GETCHAR(str, u.where + i) s.undo_char[u.char_storage + i] = STB_TEXTEDIT_GETCHAR(str, u.where + i)
end end
@@ -1153,6 +1412,7 @@ function stb_text_redo(str, state)
end end
if r.insert_length ~= 0 then if r.insert_length ~= 0 then
-- easy case: need to insert n characters
r.insert_length = STB_TEXTEDIT_INSERTCHARS(str, r.where, s.undo_char, r.char_storage, r.insert_length) r.insert_length = STB_TEXTEDIT_INSERTCHARS(str, r.where, s.undo_char, r.char_storage, r.insert_length)
s.redo_char_point = s.redo_char_point + r.insert_length s.redo_char_point = s.redo_char_point + r.insert_length
end end
@@ -1199,6 +1459,7 @@ function stb_text_makeundo_replace(str, state, where, old_length, new_length)
end end
end end
-- reset the state to default
--- @param state STB_TexteditState --- @param state STB_TexteditState
--- @param is_single_line bool --- @param is_single_line bool
local function stb_textedit_clear_state(state, is_single_line) local function stb_textedit_clear_state(state, is_single_line)
@@ -1222,7 +1483,7 @@ return {
click = stb_textedit_click, click = stb_textedit_click,
drag = stb_textedit_drag, drag = stb_textedit_drag,
createundo = stb_text_createundo, createundo = stb_text_createundo,
initialize_state = stb_textedit_clear_state, initialize_state = stb_textedit_clear_state, -- API initialize
text = stb_textedit_text, text = stb_textedit_text,
key = stb_textedit_key, key = stb_textedit_key,
@@ -1236,3 +1497,43 @@ return {
HAS_SELECTION = STB_TEXT_HAS_SELECTION HAS_SELECTION = STB_TEXT_HAS_SELECTION
} }
-------------------------------------------------------------------------------
-- This software is available under 2 licenses -- choose whichever you prefer.
-------------------------------------------------------------------------------
-- ALTERNATIVE A - MIT License
-- Copyright (c) 2017 Sean Barrett
-- Permission is hereby granted, free of charge, to any person obtaining a copy of
-- this software and associated documentation files (the "Software"), to deal in
-- the Software without restriction, including without limitation the rights to
-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
-- of the Software, and to permit persons to whom the Software is furnished to do
-- so, subject to the following conditions:
-- The above copyright notice and this permission notice shall be included in all
-- copies or substantial portions of the Software.
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-- SOFTWARE.
-------------------------------------------------------------------------------
-- ALTERNATIVE B - Public Domain (www.unlicense.org)
-- This is free and unencumbered software released into the public domain.
-- Anyone is free to copy, modify, publish, use, compile, sell, or distribute this
-- software, either in source code form or as a compiled binary, for any purpose,
-- commercial or non-commercial, and by any means.
-- In jurisdictions that recognize copyright laws, the author or authors of this
-- software dedicate any and all copyright interest in the software to the public
-- domain. We make this dedication for the benefit of the public at large and to
-- the detriment of our heirs and successors. We intend this dedication to be an
-- overt act of relinquishment in perpetuity of all present and future rights to
-- this software under copyright law.
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-- AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
-- ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
-- WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-------------------------------------------------------------------------------
+374 -54
View File
@@ -1,6 +1,164 @@
--- Dear ImGui --- Dear ImGui
-- This is a Lua port of original `imstb_truetype.h` -- This is a Lua port of original `imstb_truetype.h`
-- [DEAR IMGUI]
-- This is a slightly modified version of stb_truetype.h 1.26.
-- Mostly fixing for compiler and static analyzer warnings.
-- Grep for [DEAR IMGUI] to find the changes.
-- stb_truetype.h - v1.26 - public domain
-- authored from 2009-2021 by Sean Barrett / RAD Game Tools
--
-- =======================================================================
--
-- NO SECURITY GUARANTEE -- DO NOT USE THIS ON UNTRUSTED FONT FILES
--
-- This library does no range checking of the offsets found in the file,
-- meaning an attacker can use it to read arbitrary memory.
--
-- =======================================================================
--
-- This library processes TrueType files:
-- parse files
-- extract glyph metrics
-- extract glyph shapes
-- render glyphs to one-channel bitmaps with antialiasing (box filter)
--
-- Todo:
-- non-MS cmaps
-- crashproof on bad data
-- hinting? (no longer patented)
-- cleartype-style AA?
-- optimize: build edge-list directly from curves
-- optimize: rasterize directly from curves?
--
-- ADDITIONAL CONTRIBUTORS
--
-- Mikko Mononen: compound shape support, more cmap formats
-- Tor Andersson: kerning, subpixel rendering
-- Dougall Johnson: OpenType / Type 2 font handling
-- Daniel Ribeiro Maciel: basic GPOS-based kerning
--
-- Misc other:
-- Ryan Gordon
-- Simon Glass
-- github:IntellectualKitty
-- Imanol Celaya
-- Daniel Ribeiro Maciel
--
-- Bug/warning reports/fixes:
-- "Zer" on mollyrocket Fabian "ryg" Giesen github:NiLuJe
-- Cass Everitt Martins Mozeiko github:aloucks
-- stoiko (Haemimont Games) Cap Petschulat github:oyvindjam
-- Brian Hook Omar Cornut github:vassvik
-- Walter van Niftrik Ryan Griege
-- David Gow Peter LaValle
-- David Given Sergey Popov
-- Ivan-Assen Ivanov Giumo X. Clanjor
-- Anthony Pesch Higor Euripedes
-- Johan Duparc Thomas Fields
-- Hou Qiming Derek Vinyard
-- Rob Loach Cort Stratton
-- Kenney Phillis Jr. Brian Costabile
-- Ken Voskuil (kaesve)
--
-- USAGE
--
-- "Load" a font file from a memory buffer (you have to keep the buffer loaded)
-- stbtt_InitFont()
-- stbtt_GetFontOffsetForIndex() -- indexing for TTC font collections
--
-- Character advance/positioning
-- stbtt_GetFontVMetrics()
--
-- Starting with version 1.06, the rasterizer was replaced with a new,
-- faster and generally-more-precise rasterizer. The new rasterizer more
-- accurately measures pixel coverage for anti-aliasing, except in the case
-- where multiple shapes overlap, in which case it overestimates the AA pixel
-- coverage. Thus, anti-aliasing of intersecting shapes may look wrong.
-- (This port only implements the new rasterizer.)
--
-- ADDITIONAL DOCUMENTATION
--
-- Some important concepts to understand to use this library:
--
-- Codepoint
-- Characters are defined by unicode codepoints, e.g. 65 is
-- uppercase A, 231 is lowercase c with a cedilla, 0x7e30 is
-- the hiragana for "ma".
--
-- Glyph
-- A visual character shape (every codepoint is rendered as
-- some glyph)
--
-- Glyph index
-- A font-specific integer ID representing a glyph
--
-- Baseline
-- Glyph shapes are defined relative to a baseline, which is the
-- bottom of uppercase characters. Characters extend both above
-- and below the baseline.
--
-- Current Point
-- As you draw text to the screen, you keep track of a "current point"
-- which is the origin of each character. The current point's vertical
-- position is the baseline. Even "baked fonts" use this model.
--
-- Vertical Font Metrics
-- The vertical qualities of the font, used to vertically position
-- and space the characters. See docs for stbtt_GetFontVMetrics.
--
-- Font Size in Pixels or Points
-- The preferred interface for specifying font sizes in stb_truetype
-- is to specify how tall the font's vertical extent should be in pixels.
--
-- DETAILED USAGE:
--
-- Scale:
-- Select how high you want the font to be, in pixels.
-- Call ScaleForPixelHeight to compute a scale factor SF that will be
-- used by all other functions.
--
-- Current point:
-- Set the current point where the first character will appear. The
-- first character could extend left of the current point; this is font
-- dependent. You can either choose a current point that is the leftmost
-- point and hope, or add some padding, or check the bounding box or
-- left-side-bearing of the first character to be displayed and set
-- the current point based on that.
--
-- Displaying a character:
-- Compute the bounding box of the character. It will contain signed values
-- relative to <current_point, baseline>. I.e. if it returns x0,y0,x1,y1,
-- then the character should be displayed in the rectangle from
-- <current_point+SF*x0, baseline+SF*y0> to <current_point+SF*x1,baseline+SF*y1).
--
-- Advancing for the next character:
-- Call GlyphHMetrics, and compute 'current_point += SF * advance'.
--
--
-- ADVANCED USAGE
--
-- Quality:
--
-- - Use the functions with Subpixel at the end to allow your characters
-- to have subpixel positioning. Since the font is anti-aliased, not
-- hinted, this is very import for quality. (This is not possible with
-- baked fonts.)
--
-- Performance:
--
-- - Convert Unicode codepoints to glyph indexes and operate on the glyphs;
-- if you don't do this, stb_truetype is forced to do the conversion on
-- every call.
--
-- NOTES
--
-- The system uses the raw data found in the .ttf file without changing it
-- and without building auxiliary data structures. This is a bit inefficient
-- on little-endian systems (the data is big-endian), but assuming you're
-- caching the bitmaps or glyph shapes this shouldn't be a big deal.
--- STBTT_RASTERIZER_VERSION == 2 --- STBTT_RASTERIZER_VERSION == 2
local stbtt_fontinfo local stbtt_fontinfo
@@ -21,7 +179,7 @@ local stbtt_FindGlyphIndex
local STBTT_MAX_OVERSAMPLE = 8 local STBTT_MAX_OVERSAMPLE = 8
--- @enum STBTT_PLATFORM_ID --- @enum STBTT_PLATFORM_ID
local STBTT_PLATFORM_ID = { local STBTT_PLATFORM_ID = { -- platformID
UNICODE = 0, UNICODE = 0,
MAC = 1, MAC = 1,
ISO = 2, ISO = 2,
@@ -29,7 +187,7 @@ local STBTT_PLATFORM_ID = {
} }
--- @enum STBTT_MS_EID --- @enum STBTT_MS_EID
local STBTT_MS_EID = { local STBTT_MS_EID = { -- encodingID for STBTT_PLATFORM_ID_MICROSOFT
SYMBOL = 0, SYMBOL = 0,
UNICODE_BMP = 1, UNICODE_BMP = 1,
SHIFTJIS = 2, SHIFTJIS = 2,
@@ -75,6 +233,7 @@ local stbtt_int8 = ImS8
local stbtt_uint8 = ImU8 local stbtt_uint8 = ImU8
local unsigned_char = stbtt_uint8 local unsigned_char = stbtt_uint8
-- private structure
--- @class stbtt__buf --- @class stbtt__buf
--- @field data? table # 1-based byte table --- @field data? table # 1-based byte table
--- @field offset int # Every access to `data` will be offset by this --- @field offset int # Every access to `data` will be offset by this
@@ -93,6 +252,8 @@ local function stbtt__buf(initial_offset)
} }
end end
-- The following structure is defined publicly so you can declare one on
-- the stack or as a global or etc, but you should treat it as opaque.
--- @class stbtt_fontinfo --- @class stbtt_fontinfo
--- @field userdata any --- @field userdata any
--- @field data table # pointer to .ttf file --- @field data table # pointer to .ttf file
@@ -283,6 +444,7 @@ local function stbtt__new_active(e, off_x, start_point)
return z return z
end end
-- @TODO: don't expose this structure
--- @class stbtt__bitmap --- @class stbtt__bitmap
--- @field w int --- @field w int
--- @field h int --- @field h int
@@ -496,7 +658,7 @@ end
------------------------------------- -------------------------------------
------------------------------------- -------------------------------------
--- ---
--- Accessors to parse data from file --- accessors to parse data from file
--- ---
--- ---
@@ -535,14 +697,16 @@ local function stbtt_tag4(p, c0, c1, c2, c3, offset) offset = offset or 0; retur
local function stbtt_tag(p, str, offset) local c0, c1, c2, c3 = string.byte(str, 1, 4); return stbtt_tag4(p, c0, c1, c2, c3, offset) end local function stbtt_tag(p, str, offset) local c0, c1, c2, c3 = string.byte(str, 1, 4); return stbtt_tag4(p, c0, c1, c2, c3, offset) end
local function stbtt__isfont(font) local function stbtt__isfont(font)
if stbtt_tag4(font, 49, 0, 0, 0) then return true end -- check the version number
if stbtt_tag(font, "typ1") then return true end if stbtt_tag4(font, 49, 0, 0, 0) then return true end -- TrueType 1
if stbtt_tag(font, "OTTO") then return true end if stbtt_tag(font, "typ1") then return true end -- TrueType with type 1 font -- we don't support this!
if stbtt_tag4(font, 0, 1, 0, 0) then return true end if stbtt_tag(font, "OTTO") then return true end -- OpenType with CFF
if stbtt_tag(font, "true") then return true end if stbtt_tag4(font, 0, 1, 0, 0) then return true end -- OpenType 1.0
if stbtt_tag(font, "true") then return true end -- Apple specification for TrueType fonts
return false return false
end end
-- @OPTIMIZE: binary search
local function stbtt__find_table(data, font_start, tag) local function stbtt__find_table(data, font_start, tag)
local num_tables = ttUSHORT(data, font_start + 4) local num_tables = ttUSHORT(data, font_start + 4)
local tabledir = font_start + 12 local tabledir = font_start + 12
@@ -556,11 +720,14 @@ local function stbtt__find_table(data, font_start, tag)
end end
local function stbtt_GetFontOffsetForIndex_internal(font_collection, index) local function stbtt_GetFontOffsetForIndex_internal(font_collection, index)
-- if it's just a font, there's only one valid index
if stbtt__isfont(font_collection) then if stbtt__isfont(font_collection) then
if index == 0 then return 0 else return -1 end if index == 0 then return 0 else return -1 end
end end
-- check if it's a TTC
if stbtt_tag(font_collection, "ttcf") then if stbtt_tag(font_collection, "ttcf") then
-- version 1?
if ttULONG(font_collection, 4) == 0x00010000 or ttULONG(font_collection, 4) == 0x00020000 then if ttULONG(font_collection, 4) == 0x00010000 or ttULONG(font_collection, 4) == 0x00020000 then
local n = ttLONG(font_collection, 8) local n = ttLONG(font_collection, 8)
if index >= n then if index >= n then
@@ -598,23 +765,25 @@ local function stbtt_InitFont_internal(info, data, fontstart)
info.fontstart = fontstart info.fontstart = fontstart
info.cff = stbtt__new_buf(nil, 0) info.cff = stbtt__new_buf(nil, 0)
cmap = stbtt__find_table(data, fontstart, "cmap") cmap = stbtt__find_table(data, fontstart, "cmap") -- required
info.loca = stbtt__find_table(data, fontstart, "loca") info.loca = stbtt__find_table(data, fontstart, "loca") -- required
info.head = stbtt__find_table(data, fontstart, "head") info.head = stbtt__find_table(data, fontstart, "head") -- required
info.glyf = stbtt__find_table(data, fontstart, "glyf") info.glyf = stbtt__find_table(data, fontstart, "glyf") -- required
info.hhea = stbtt__find_table(data, fontstart, "hhea") info.hhea = stbtt__find_table(data, fontstart, "hhea") -- required
info.hmtx = stbtt__find_table(data, fontstart, "hmtx") info.hmtx = stbtt__find_table(data, fontstart, "hmtx") -- required
info.kern = stbtt__find_table(data, fontstart, "kern") info.kern = stbtt__find_table(data, fontstart, "kern") -- not required
info.gpos = stbtt__find_table(data, fontstart, "GPOS") info.gpos = stbtt__find_table(data, fontstart, "GPOS") -- not required
if (cmap == 0 or info.head == 0 or info.hhea == 0 or info.hmtx == 0) then if (cmap == 0 or info.head == 0 or info.hhea == 0 or info.hmtx == 0) then
return false return false
end end
if info.glyf ~= 0 then if info.glyf ~= 0 then
-- required for truetype
if info.loca == 0 then if info.loca == 0 then
return false return false
end end
else else
-- initialization for CFF / Type2 fonts (OTF)
local cff = stbtt__find_table(data, fontstart, "CFF ") local cff = stbtt__find_table(data, fontstart, "CFF ")
if cff == 0 then if cff == 0 then
return false return false
@@ -623,14 +792,16 @@ local function stbtt_InitFont_internal(info, data, fontstart)
info.fontdicts = stbtt__new_buf(nil, 0) info.fontdicts = stbtt__new_buf(nil, 0)
info.fdselect = stbtt__new_buf(nil, 0) info.fdselect = stbtt__new_buf(nil, 0)
info.cff = stbtt__new_buf(data, 64 * 1024 * 1024, cff) -- TODO: this should use size from table -- @TODO this should use size from table (not 512MB)
info.cff = stbtt__new_buf(data, 64 * 1024 * 1024, cff) -- PORT: 64MB here instead of 512MB
local b = info.cff local b = info.cff
-- read the header -- read the header
stbtt__buf_skip(b, 2) stbtt__buf_skip(b, 2)
stbtt__buf_seek(b, stbtt__buf_get8(b)) stbtt__buf_seek(b, stbtt__buf_get8(b)) -- hdrsize
-- TODO: the name INDEX could list multiple fonts, but we just use the first one. -- @TODO the name INDEX could list multiple fonts,
-- but we just use the first one.
stbtt__cff_get_index(b) -- name INDEX stbtt__cff_get_index(b) -- name INDEX
local topdictidx = stbtt__cff_get_index(b) local topdictidx = stbtt__cff_get_index(b)
local topdict = stbtt__cff_index_get(topdictidx, 0) local topdict = stbtt__cff_index_get(topdictidx, 0)
@@ -643,6 +814,7 @@ local function stbtt_InitFont_internal(info, data, fontstart)
local fdselectoff = stbtt__dict_get_ints(topdict, bit32.bor(0x100, 37), 1) local fdselectoff = stbtt__dict_get_ints(topdict, bit32.bor(0x100, 37), 1)
info.subrs = stbtt__get_subrs(b, topdict) info.subrs = stbtt__get_subrs(b, topdict)
-- we only support Type 2 charstrings
if cstype[1] ~= 2 then if cstype[1] ~= 2 then
return false return false
end end
@@ -651,6 +823,7 @@ local function stbtt_InitFont_internal(info, data, fontstart)
end end
if fdarrayoff[1] ~= 0 then if fdarrayoff[1] ~= 0 then
-- looks like a CID font
if fdselectoff[1] == 0 then if fdselectoff[1] == 0 then
return false return false
end end
@@ -673,19 +846,26 @@ local function stbtt_InitFont_internal(info, data, fontstart)
info.svg = -1 info.svg = -1
-- find a cmap encoding table we understand *now* to avoid searching
-- later. (todo: could make this installable)
-- the same regardless of glyph.
numTables = ttUSHORT(data, cmap + 2) numTables = ttUSHORT(data, cmap + 2)
info.index_map = 0 info.index_map = 0
for i = 0, numTables - 1 do for i = 0, numTables - 1 do
local encoding_record = cmap + 4 + 8 * i local encoding_record = cmap + 4 + 8 * i
-- find an encoding we understand:
local platform_id = ttUSHORT(data, encoding_record) local platform_id = ttUSHORT(data, encoding_record)
if platform_id == STBTT_PLATFORM_ID.MICROSOFT then if platform_id == STBTT_PLATFORM_ID.MICROSOFT then
local ms_eid = ttUSHORT(data, encoding_record + 2) local ms_eid = ttUSHORT(data, encoding_record + 2)
if ms_eid == STBTT_MS_EID.UNICODE_BMP or ms_eid == STBTT_MS_EID.UNICODE_FULL then if ms_eid == STBTT_MS_EID.UNICODE_BMP or ms_eid == STBTT_MS_EID.UNICODE_FULL then
-- MS/Unicode
info.index_map = cmap + ttULONG(data, encoding_record + 4) info.index_map = cmap + ttULONG(data, encoding_record + 4)
end end
elseif platform_id == STBTT_PLATFORM_ID.UNICODE then elseif platform_id == STBTT_PLATFORM_ID.UNICODE then
-- Mac/iOS has these
-- all the encodingIDs are unicode, so we don't bother to check it
info.index_map = cmap + ttULONG(data, encoding_record + 4) info.index_map = cmap + ttULONG(data, encoding_record + 4)
end end
end end
@@ -697,6 +877,11 @@ local function stbtt_InitFont_internal(info, data, fontstart)
return true return true
end end
-- If you're going to perform multiple operations on the same character
-- and you want a speed-up, call this function with the character you're
-- going to process, then use glyph-based functions instead of the
-- codepoint-based functions.
-- Returns 0 if the character codepoint is not defined in the font.
--- @param info stbtt_fontinfo --- @param info stbtt_fontinfo
--- @param unicode_codepoint int --- @param unicode_codepoint int
function stbtt_FindGlyphIndex(info, unicode_codepoint) function stbtt_FindGlyphIndex(info, unicode_codepoint)
@@ -704,7 +889,7 @@ function stbtt_FindGlyphIndex(info, unicode_codepoint)
local index_map = info.index_map local index_map = info.index_map
local format = ttUSHORT(data, index_map + 0) local format = ttUSHORT(data, index_map + 0)
if format == 0 then if format == 0 then -- apple byte encoding
local bytes = ttUSHORT(data, index_map + 2) local bytes = ttUSHORT(data, index_map + 2)
if unicode_codepoint < bytes - 6 then if unicode_codepoint < bytes - 6 then
return ttBYTE(data, index_map + 6 + unicode_codepoint) return ttBYTE(data, index_map + 6 + unicode_codepoint)
@@ -718,14 +903,15 @@ function stbtt_FindGlyphIndex(info, unicode_codepoint)
end end
return 0 return 0
elseif format == 2 then elseif format == 2 then
STBTT_assert(false) -- TODO: high-byte mapping for japanese/chinese/korean STBTT_assert(false) -- @TODO: high-byte mapping for japanese/chinese/korean
return 0 return 0
elseif format == 4 then elseif format == 4 then -- standard mapping for windows fonts: binary search collection of ranges
local segcount = bit32.rshift(ttUSHORT(data, index_map + 6), 1) local segcount = bit32.rshift(ttUSHORT(data, index_map + 6), 1)
local searchRange = bit32.rshift(ttUSHORT(data, index_map + 8), 1) local searchRange = bit32.rshift(ttUSHORT(data, index_map + 8), 1)
local entrySelector = ttUSHORT(data, index_map + 10) local entrySelector = ttUSHORT(data, index_map + 10)
local rangeShift = bit32.rshift(ttUSHORT(data, index_map + 12), 1) local rangeShift = bit32.rshift(ttUSHORT(data, index_map + 12), 1)
-- do a binary search of the segments
local endCount = index_map + 14 local endCount = index_map + 14
local search = endCount local search = endCount
@@ -733,10 +919,13 @@ function stbtt_FindGlyphIndex(info, unicode_codepoint)
return 0 return 0
end end
-- they lie from endCount .. endCount + segCount
-- but searchRange is the nearest power of two, so...
if unicode_codepoint >= ttUSHORT(data, search + rangeShift * 2) then if unicode_codepoint >= ttUSHORT(data, search + rangeShift * 2) then
search = search + rangeShift * 2 search = search + rangeShift * 2
end end
-- now decrement to bias correctly to find smallest
search = search - 2 search = search - 2
while entrySelector ~= 0 do while entrySelector ~= 0 do
searchRange = bit32.rshift(searchRange, 1) searchRange = bit32.rshift(searchRange, 1)
@@ -768,8 +957,9 @@ function stbtt_FindGlyphIndex(info, unicode_codepoint)
local ngroups = ttULONG(data, index_map + 12) local ngroups = ttULONG(data, index_map + 12)
local low = 0 local low = 0
local high = ngroups local high = ngroups
-- Binary search the right group.
while low < high do while low < high do
local mid = low + bit32.rshift(high - low, 1) local mid = low + bit32.rshift(high - low, 1) -- rounds down, so low <= mid < high
local start_char = ttULONG(data, index_map + 16 + mid * 12) local start_char = ttULONG(data, index_map + 16 + mid * 12)
local end_char = ttULONG(data, index_map + 16 + mid * 12 + 4) local end_char = ttULONG(data, index_map + 16 + mid * 12 + 4)
if unicode_codepoint < start_char then if unicode_codepoint < start_char then
@@ -785,10 +975,10 @@ function stbtt_FindGlyphIndex(info, unicode_codepoint)
end end
end end
end end
return 0 return 0 -- not found
end end
-- TODO: -- @TODO
STBTT_assert(false) STBTT_assert(false)
return 0 return 0
end end
@@ -810,8 +1000,8 @@ end
local function stbtt__GetGlyfOffset(info, glyph_index) local function stbtt__GetGlyfOffset(info, glyph_index)
STBTT_assert(info.cff.size == 0) STBTT_assert(info.cff.size == 0)
if glyph_index >= info.numGlyphs then return -1 end if glyph_index >= info.numGlyphs then return -1 end -- glyph index out of range
if info.indexToLocFormat >= 2 then return -1 end if info.indexToLocFormat >= 2 then return -1 end -- unknown index->glyph map format
local g1, g2 local g1, g2
if info.indexToLocFormat == 0 then if info.indexToLocFormat == 0 then
@@ -822,9 +1012,10 @@ local function stbtt__GetGlyfOffset(info, glyph_index)
g2 = info.glyf + ttULONG(info.data, info.loca + glyph_index * 4 + 4) g2 = info.glyf + ttULONG(info.data, info.loca + glyph_index * 4 + 4)
end end
if g1 == g2 then return -1 else return g1 end if g1 == g2 then return -1 else return g1 end -- if length is 0, return -1
end end
-- Gets the bounding box of the visible part of the glyph, in unscaled coordinates
--- @return integer, integer?, integer?, integer?, integer? --- @return integer, integer?, integer?, integer?, integer?
local function stbtt_GetGlyphBox(info, glyph_index) local function stbtt_GetGlyphBox(info, glyph_index)
local n, x0, y0, x1, y1 local n, x0, y0, x1, y1
@@ -896,7 +1087,7 @@ local function stbtt__GetGlyphShapeTT(info, glyph_index)
local points = (endPtsOfContours + numberOfContours * 2 + 2 + ins) + 1 -- 1-based idx local points = (endPtsOfContours + numberOfContours * 2 + 2 + ins) + 1 -- 1-based idx
local n = 1 + ttUSHORT(data, endPtsOfContours + numberOfContours * 2 - 2) local n = 1 + ttUSHORT(data, endPtsOfContours + numberOfContours * 2 - 2)
local m = n + 2 * numberOfContours local m = n + 2 * numberOfContours -- a loose bound on how many vertices we might need
vertices = {} vertices = {}
for i = 1, m do vertices[i] = stbtt_vertex() end for i = 1, m do vertices[i] = stbtt_vertex() end
@@ -907,7 +1098,11 @@ local function stbtt__GetGlyphShapeTT(info, glyph_index)
local next_move = 0 local next_move = 0
local flagcount = 0 local flagcount = 0
local off = m - n -- in first pass, we load uninterpreted data into the allocated array
-- above, shifted to the end of the array so we won't overwrite it when
-- we create our final data starting from the front
local off = m - n -- starting offset for uninterpreted data, regardless of how m ends up being calculated
-- first load flags -- first load flags
local flags = 0 local flags = 0
@@ -934,7 +1129,7 @@ local function stbtt__GetGlyphShapeTT(info, glyph_index)
if bit32.band(flags, 2) ~= 0 then if bit32.band(flags, 2) ~= 0 then
local dx = data[points] local dx = data[points]
points = points + 1 points = points + 1
if bit32.band(flags, 16) ~= 0 then if bit32.band(flags, 16) ~= 0 then -- ???
x = x + dx x = x + dx
else else
x = x - dx x = x - dx
@@ -956,7 +1151,7 @@ local function stbtt__GetGlyphShapeTT(info, glyph_index)
if bit32.band(flags, 4) ~= 0 then if bit32.band(flags, 4) ~= 0 then
local dy = data[points] local dy = data[points]
points = points + 1 points = points + 1
if bit32.band(flags, 32) ~= 0 then if bit32.band(flags, 32) ~= 0 then -- ???
y = y + dy y = y + dy
else else
y = y - dy y = y - dy
@@ -987,15 +1182,19 @@ local function stbtt__GetGlyphShapeTT(info, glyph_index)
-- now start the new one -- now start the new one
start_off = (bit32.band(flags, 1) == 0) start_off = (bit32.band(flags, 1) == 0)
if start_off then if start_off then
-- if we start off with an off-curve point, then when we need to find a point on the curve
-- where we can start, and we need to save some state for when we wraparound.
scx = x scx = x
scy = y scy = y
if bit32.band(vertices[off + i + 1].type, 1) == 0 then if bit32.band(vertices[off + i + 1].type, 1) == 0 then
-- next point is also a curve point, so interpolate an on-point curve
sx = bit32.rshift(x + stbtt_int32(vertices[off + i + 1].x), 1) sx = bit32.rshift(x + stbtt_int32(vertices[off + i + 1].x), 1)
sy = bit32.rshift(y + stbtt_int32(vertices[off + i + 1].y), 1) sy = bit32.rshift(y + stbtt_int32(vertices[off + i + 1].y), 1)
else else
-- otherwise just use the next point as our start point
sx = stbtt_int32(vertices[off + i + 1].x) sx = stbtt_int32(vertices[off + i + 1].x)
sy = stbtt_int32(vertices[off + i + 1].y) sy = stbtt_int32(vertices[off + i + 1].y)
i = i + 1 i = i + 1 -- we're using point i+1 as the starting point, so skip it
end end
else else
sx = x sx = x
@@ -1007,8 +1206,8 @@ local function stbtt__GetGlyphShapeTT(info, glyph_index)
next_move = 1 + ttUSHORT(data, endPtsOfContours + j * 2) next_move = 1 + ttUSHORT(data, endPtsOfContours + j * 2)
j = j + 1 j = j + 1
else else
if bit32.band(flags, 1) == 0 then if bit32.band(flags, 1) == 0 then -- if it's a curve
if was_off then if was_off then -- two off-curve control points in a row means interpolate an on-curve midpoint
stbtt_setvertex(vertices[num_vertices + 1], STBTT_VTX.CURVE, bit32.rshift(cx + x, 1), bit32.rshift(cy + y, 1), cx, cy) stbtt_setvertex(vertices[num_vertices + 1], STBTT_VTX.CURVE, bit32.rshift(cx + x, 1), bit32.rshift(cy + y, 1), cx, cy)
num_vertices = num_vertices + 1 num_vertices = num_vertices + 1
end end
@@ -1032,6 +1231,7 @@ local function stbtt__GetGlyphShapeTT(info, glyph_index)
num_vertices = stbtt__close_shape(vertices, num_vertices, was_off, start_off, sx, sy, scx, scy, cx, cy) num_vertices = stbtt__close_shape(vertices, num_vertices, was_off, start_off, sx, sy, scx, scy, cx, cy)
elseif numberOfContours < 0 then elseif numberOfContours < 0 then
-- Compound shapes.
local more = 1 local more = 1
local comp = g + 10 local comp = g + 10
num_vertices = 0 num_vertices = 0
@@ -1047,8 +1247,8 @@ local function stbtt__GetGlyphShapeTT(info, glyph_index)
flags = ttSHORT(data, comp) comp = comp + 2 flags = ttSHORT(data, comp) comp = comp + 2
gidx = ttSHORT(data, comp) comp = comp + 2 gidx = ttSHORT(data, comp) comp = comp + 2
if bit32.band(flags, 2) ~= 0 then if bit32.band(flags, 2) ~= 0 then -- XY values
if bit32.band(flags, 1) ~= 0 then if bit32.band(flags, 1) ~= 0 then -- shorts
mtx[5] = ttSHORT(data, comp) comp = comp + 2 mtx[5] = ttSHORT(data, comp) comp = comp + 2
mtx[6] = ttSHORT(data, comp) comp = comp + 2 mtx[6] = ttSHORT(data, comp) comp = comp + 2
else else
@@ -1056,7 +1256,7 @@ local function stbtt__GetGlyphShapeTT(info, glyph_index)
mtx[6] = ttCHAR(data, comp) comp = comp + 1 mtx[6] = ttCHAR(data, comp) comp = comp + 1
end end
else else
-- TODO: handle matching point -- @TODO handle matching point
STBTT_assert(false) STBTT_assert(false)
end end
@@ -1083,11 +1283,14 @@ local function stbtt__GetGlyphShapeTT(info, glyph_index)
mtx[4] = ttSHORT(data, comp) / 16384.0 comp = comp + 2 mtx[4] = ttSHORT(data, comp) / 16384.0 comp = comp + 2
end end
-- Find transformation scales.
m = STBTT_sqrt(mtx[1] * mtx[1] + mtx[2] * mtx[2]) m = STBTT_sqrt(mtx[1] * mtx[1] + mtx[2] * mtx[2])
n = STBTT_sqrt(mtx[3] * mtx[3] + mtx[4] * mtx[4]) n = STBTT_sqrt(mtx[3] * mtx[3] + mtx[4] * mtx[4])
-- Get indexed glyph.
comp_num_vertices, comp_verts = stbtt_GetGlyphShape(info, gidx) comp_num_vertices, comp_verts = stbtt_GetGlyphShape(info, gidx)
if comp_num_vertices > 0 then if comp_num_vertices > 0 then
-- Transform vertices.
for i = 1, comp_num_vertices do for i = 1, comp_num_vertices do
local v = comp_verts[i] local v = comp_verts[i]
local x = v.x local x = v.x
@@ -1100,6 +1303,7 @@ local function stbtt__GetGlyphShapeTT(info, glyph_index)
v.cy = stbtt_int16(n * (mtx[2] * x + mtx[4] * y + mtx[6])) v.cy = stbtt_int16(n * (mtx[2] * x + mtx[4] * y + mtx[6]))
end end
-- Append vertices.
if num_vertices == 0 then -- LUA: No malloc if num_vertices == 0 then -- LUA: No malloc
vertices = comp_verts vertices = comp_verts
else else
@@ -1111,6 +1315,7 @@ local function stbtt__GetGlyphShapeTT(info, glyph_index)
num_vertices = num_vertices + comp_num_vertices num_vertices = num_vertices + comp_num_vertices
end end
-- More components ?
more = bit32.band(flags, bit32.lshift(1, 5)) more = bit32.band(flags, bit32.lshift(1, 5))
end end
end end
@@ -1266,7 +1471,7 @@ local function stbtt__run_charstring(info, glyph_index, c)
clear_stack = 1 clear_stack = 1
b0 = stbtt__buf_get8(b) b0 = stbtt__buf_get8(b)
-- TODO: implement hinting -- @TODO implement hinting
if b0 == 0x13 or b0 == 0x14 then -- hintmask or cntrmask if b0 == 0x13 or b0 == 0x14 then -- hintmask or cntrmask
if in_header then if in_header then
maskbits = maskbits + trunc(sp / 2) -- implicit "vstem" maskbits = maskbits + trunc(sp / 2) -- implicit "vstem"
@@ -1415,6 +1620,8 @@ local function stbtt__run_charstring(info, glyph_index, c)
elseif b0 == 0x0C then -- two-byte escape elseif b0 == 0x0C then -- two-byte escape
local b1 = stbtt__buf_get8(b) local b1 = stbtt__buf_get8(b)
-- @TODO These "flex" implementations ignore the flex-depth and resolution,
-- and always draw beziers.
if b1 == 0x22 then -- hflex if b1 == 0x22 then -- hflex
if sp < 7 then return STBTT__CSERR("hflex stack") end if sp < 7 then return STBTT__CSERR("hflex stack") end
local dx1 = s[0] local dx1 = s[0]
@@ -1547,6 +1754,15 @@ function stbtt__GetGlyphInfoT2(info, glyph_index)
return ((r ~= 0) and c.num_vertices or 0), x0, y0, x1, y1 return ((r ~= 0) and c.num_vertices or 0), x0, y0, x1, y1
end end
-- returns # of vertices and fills *vertices with the pointer to them
-- these are expressed in "unscaled" coordinates
--
-- The shape is a series of contours. Each one starts with
-- a STBTT_moveto, then consists of a series of mixed
-- STBTT_lineto and STBTT_curveto segments. A lineto
-- draws a line from previous endpoint to its x,y; a curveto
-- draws a quadratic bezier from previous endpoint to
-- its x,y, using cx,cy as the bezier control point.
--- @param info stbtt_fontinfo --- @param info stbtt_fontinfo
--- @param glyph_index int --- @param glyph_index int
function stbtt_GetGlyphShape(info, glyph_index) function stbtt_GetGlyphShape(info, glyph_index)
@@ -1557,6 +1773,9 @@ function stbtt_GetGlyphShape(info, glyph_index)
end end
end end
-- leftSideBearing is the offset from the current horizontal position to the left edge of the character
-- advanceWidth is the offset from the current horizontal position to the next horizontal position
-- these are expressed in unscaled coordinates
--- @param info stbtt_fontinfo --- @param info stbtt_fontinfo
--- @param glyph_index int --- @param glyph_index int
function stbtt_GetGlyphHMetrics(info, glyph_index) function stbtt_GetGlyphHMetrics(info, glyph_index)
@@ -1574,6 +1793,12 @@ function stbtt_GetGlyphHMetrics(info, glyph_index)
return advanceWidth, leftSideBearing return advanceWidth, leftSideBearing
end end
-- ascent is the coordinate above the baseline the font extends; descent
-- is the coordinate below the baseline the font extends (i.e. it is typically negative)
-- lineGap is the spacing between one row's descent and the next row's ascent...
-- so you should advance the vertical position by "ascent - descent + lineGap"
-- these are expressed in unscaled coordinates, so you must multiply by
-- the scale factor for a given size
--- @param info stbtt_fontinfo --- @param info stbtt_fontinfo
function stbtt_GetFontVMetrics(info) function stbtt_GetFontVMetrics(info)
local ascent = ttSHORT(info.data, info.hhea + 4) local ascent = ttSHORT(info.data, info.hhea + 4)
@@ -1583,6 +1808,12 @@ function stbtt_GetFontVMetrics(info)
return ascent, descent, lineGap return ascent, descent, lineGap
end end
-- computes a scale factor to produce a font whose "height" is 'pixels' tall.
-- Height is measured as the distance from the highest ascender to the lowest
-- descender; in other words, it's equivalent to calling stbtt_GetFontVMetrics
-- and computing:
-- scale = pixels / (ascent - descent)
-- so if you prefer to measure height by the ascent only, use a similar calculation.
--- @param info stbtt_fontinfo --- @param info stbtt_fontinfo
--- @param height float --- @param height float
function stbtt_ScaleForPixelHeight(info, height) function stbtt_ScaleForPixelHeight(info, height)
@@ -1593,10 +1824,12 @@ end
------------------------------------ ------------------------------------
------------------------------------ ------------------------------------
--- ---
--- Antialiasing software rasterizer --- antialiasing software rasterizer
--- ---
--- ---
-- same as stbtt_GetGlyphBitmapBox, but you can specify a subpixel
-- shift for the character
--- @param font stbtt_fontinfo --- @param font stbtt_fontinfo
--- @param glyph int --- @param glyph int
--- @param scale_x float --- @param scale_x float
@@ -1624,6 +1857,11 @@ function stbtt_GetGlyphBitmapBoxSubpixel(font, glyph, scale_x, scale_y, shift_x,
return ix0, iy0, ix1, iy1 return ix0, iy0, ix1, iy1
end end
-- get the bbox of the bitmap centered around the glyph origin; so the
-- bitmap width is ix1-ix0, height is iy1-iy0, and location to place
-- the bitmap top left is (leftSideBearing*scale,iy0).
-- (Note that the bitmap uses y-increases-down, but the shape uses
-- y-increases-up, so GlyphBitmapBox and GlyphBox are inverted.)
--- @param font stbtt_fontinfo --- @param font stbtt_fontinfo
--- @param glyph int --- @param glyph int
--- @param scale_x float --- @param scale_x float
@@ -1646,6 +1884,8 @@ end
--- stbtt_MakeGlyphBitmap --- stbtt_MakeGlyphBitmap
--- stbtt_GetGlyphBitmapSubpixel --- stbtt_GetGlyphBitmapSubpixel
-- the edge passed in here does not cross the vertical line at x or the vertical line at x+1
-- (i.e. it has already been clipped to those)
--- @param scanline float[] --- @param scanline float[]
--- @param x int --- @param x int
--- @param e stbtt__active_edge --- @param e stbtt__active_edge
@@ -1687,7 +1927,7 @@ local function stbtt__handle_clipped_edge(scanline, x, e, x0, y0, x1, y1)
-- do nothing -- do nothing
else else
STBTT_assert(x0 >= x and x0 <= x + 1 and x1 >= x and x1 <= x + 1) STBTT_assert(x0 >= x and x0 <= x + 1 and x1 >= x and x1 <= x + 1)
scanline[x] = scanline[x] + e.direction * (y1 - y0) * (1 - ((x0 - x) + (x1 - x)) / 2) scanline[x] = scanline[x] + e.direction * (y1 - y0) * (1 - ((x0 - x) + (x1 - x)) / 2) -- coverage = 1 - average x position
end end
end end
@@ -1725,6 +1965,7 @@ local function stbtt__fill_active_edges_new(scanline, scanline_fill, len, e, y_t
while e do while e do
-- brute force every pixel -- brute force every pixel
-- compute intersection points with top & bottom -- compute intersection points with top & bottom
STBTT_assert(e.ey >= y_top) STBTT_assert(e.ey >= y_top)
@@ -1768,8 +2009,8 @@ local function stbtt__fill_active_edges_new(scanline, scanline_fill, len, e, y_t
if x_top >= 0 and x_bottom >= 0 and x_top < len and x_bottom < len then if x_top >= 0 and x_bottom >= 0 and x_top < len and x_bottom < len then
-- from here on, we don't have to range check x values -- from here on, we don't have to range check x values
if trunc(x_top) == trunc(x_bottom) then if trunc(x_top) == trunc(x_bottom) then
-- simple case, only spans one pixel
local height local height
-- simple case, only spans one pixel
local x = trunc(x_top) local x = trunc(x_top)
height = (sy1 - sy0) * e.direction height = (sy1 - sy0) * e.direction
STBTT_assert(x >= 0 and x < len) STBTT_assert(x >= 0 and x < len)
@@ -1801,8 +2042,25 @@ local function stbtt__fill_active_edges_new(scanline, scanline_fill, len, e, y_t
-- compute intersection with y axis at x2 -- compute intersection with y axis at x2
y_final = y_top + dy * (x2 - x0) y_final = y_top + dy * (x2 - x0)
-- x1 x_top x2 x_bottom
-- y_top +------|-----+------------+------------+--------|---+------------+
-- | | | | | |
-- | | | | | |
-- sy0 | Txxxxx|............|............|............|............|
-- y_crossing | *xxxxx.......|............|............|............|
-- | | xxxxx..|............|............|............|
-- | | /- xx*xxxx........|............|............|
-- | | dy < | xxxxxx..|............|............|
-- y_final | | \- | xx*xxx.........|............|
-- sy1 | | | | xxxxxB...|............|
-- | | | | | |
-- | | | | | |
-- y_bottom +------------+------------+------------+------------+------------+
--
-- goal is to measure the area covered by '.' in each pixel
-- if x2 is right at the right edge of x1, y_crossing can blow up, github #1057 -- if x2 is right at the right edge of x1, y_crossing can blow up, github #1057
-- TODO: maybe test against sy1 rather than y_bottom? -- @TODO: maybe test against sy1 rather than y_bottom?
if y_crossing > y_bottom then if y_crossing > y_bottom then
y_crossing = y_bottom y_crossing = y_bottom
end end
@@ -1824,6 +2082,16 @@ local function stbtt__fill_active_edges_new(scanline, scanline_fill, len, e, y_t
end end
end end
-- in second pixel, area covered by line segment found in first pixel
-- is always a rectangle 1 wide * the height of that line segment; this
-- is exactly what the variable 'area' stores. it also gets a contribution
-- from the line segment within it. the THIRD pixel will get the first
-- pixel's rectangle contribution, the second pixel's rectangle contribution,
-- and its own contribution. the 'own contribution' is the same in every pixel except
-- the leftmost and rightmost, a trapezoid that slides down in each pixel.
-- the second pixel's contribution to the third pixel will be the
-- rectangle 1 wide times the height change in the second pixel, which is dy.
step = sign * dy * 1 -- dy is dy/dx, change in y for every 1 change in x, step = sign * dy * 1 -- dy is dy/dx, change in y for every 1 change in x,
-- which multiplied by 1-pixel-width is how much pixel area changes for each step in x -- which multiplied by 1-pixel-width is how much pixel area changes for each step in x
-- so the area advances by 'step' every time -- so the area advances by 'step' every time
@@ -1843,7 +2111,27 @@ local function stbtt__fill_active_edges_new(scanline, scanline_fill, len, e, y_t
scanline_fill[x2 + 1] = scanline_fill[x2 + 1] + sign * (sy1 - sy0) scanline_fill[x2 + 1] = scanline_fill[x2 + 1] + sign * (sy1 - sy0)
end end
else else
-- if edge goes outside of box we're drawing, we require
-- clipping logic. since this does not match the intended use
-- of this library, we use a different, very slow brute
-- force implementation
-- note though that this does happen some of the time because
-- x_top and x_bottom can be extrapolated at the top & bottom of
-- the shape and actually lie outside the bounding box
for x = 0, len - 1 do for x = 0, len - 1 do
-- cases:
--
-- there can be up to two intersections with the pixel. any intersection
-- with left or right edges can be handled by splitting into two (or three)
-- regions. intersections with top & bottom do not necessitate case-wise logic.
--
-- the old way of doing this found the intersections with the left & right edges,
-- then used some simple logic to produce up to three segments in sorted order
-- from top-to-bottom. however, this had a problem: if an x edge was epsilon
-- across the x border, then the corresponding y position might not be distinct
-- from the other y segment, and it might ignored as an empty segment. to avoid
-- that, we need to explicitly produce segments based on x positions.
-- rename variables to clearly-defined pairs -- rename variables to clearly-defined pairs
local y0 = y_top local y0 = y_top
local x1 = x local x1 = x
@@ -1887,6 +2175,7 @@ local function stbtt__fill_active_edges_new(scanline, scanline_fill, len, e, y_t
end end
end end
-- directly AA rasterize edges w/o supersampling
--- @param result stbtt__bitmap --- @param result stbtt__bitmap
--- @param e stbtt__edge --- @param e stbtt__edge
--- @param n int --- @param n int
@@ -1914,28 +2203,33 @@ local function stbtt__rasterize_sorted_edges(result, e, n, vsubsample, off_x, of
for i = 0, result.w - 1 do scanline[i] = 0 end for i = 0, result.w - 1 do scanline[i] = 0 end
for i = 0, result.w do scanline2[i] = 0 end for i = 0, result.w do scanline2[i] = 0 end
-- update all active edges;
-- remove all active edges that terminate before the top of this scanline
local prev = nil local prev = nil
local curr = active local curr = active
while curr do while curr do
local next = curr.next local next = curr.next
if curr.ey <= scan_y_top then if curr.ey <= scan_y_top then
if prev then prev.next = next if prev then prev.next = next -- delete from list
else active = next end else active = next end
curr.direction = 0 curr.direction = 0
else else
prev = curr prev = curr -- advance through list
end end
curr = next curr = next
end end
-- insert all edges that start before the bottom of this scanline
local edge = e[p] local edge = e[p]
while edge.y0 <= scan_y_bottom do while edge.y0 <= scan_y_bottom do
if edge.y0 ~= edge.y1 then if edge.y0 ~= edge.y1 then
local z = stbtt__new_active(edge, off_x, scan_y_top) local z = stbtt__new_active(edge, off_x, scan_y_top)
if j == 0 and off_y ~= 0 and z.ey < scan_y_top then if j == 0 and off_y ~= 0 and z.ey < scan_y_top then
-- this can happen due to subpixel positioning and some kind of fp rounding error i think
z.ey = scan_y_top z.ey = scan_y_top
end end
STBTT_assert(z.ey >= scan_y_top) STBTT_assert(z.ey >= scan_y_top) -- if we get really unlucky a tiny bit of an edge can be out of bounds
-- insert at front
z.next = active z.next = active
active = z active = z
end end
@@ -1943,6 +2237,7 @@ local function stbtt__rasterize_sorted_edges(result, e, n, vsubsample, off_x, of
edge = e[p] edge = e[p]
end end
-- now process all active edges
if active then if active then
stbtt__fill_active_edges_new(scanline, scanline2, result.w, active, scan_y_top) stbtt__fill_active_edges_new(scanline, scanline2, result.w, active, scan_y_top)
end end
@@ -1961,10 +2256,11 @@ local function stbtt__rasterize_sorted_edges(result, e, n, vsubsample, off_x, of
end end
end end
-- advance all the edges
step = active step = active
while step do while step do
step.fx = step.fx + step.fdx step.fx = step.fx + step.fdx -- advance to position for current scanline
step = step.next step = step.next -- advance through list
end end
y = y + 1 y = y + 1
@@ -1999,6 +2295,7 @@ end
local function stbtt__rasterize(result, pts, wcount, windings, scale_x, scale_y, shift_x, shift_y, off_x, off_y, invert) local function stbtt__rasterize(result, pts, wcount, windings, scale_x, scale_y, shift_x, shift_y, off_x, off_y, invert)
local y_scale_inv = (invert) and -scale_y or scale_y local y_scale_inv = (invert) and -scale_y or scale_y
local vsubsample = 1 -- STBTT_RASTERIZER_VERSION == 2 local vsubsample = 1 -- STBTT_RASTERIZER_VERSION == 2
-- vsubsample should divide 255 evenly; otherwise we won't reach full opacity
-- now we have to blow out the windings into explicit edge lists -- now we have to blow out the windings into explicit edge lists
local n = 0 local n = 0
@@ -2066,8 +2363,7 @@ local function stbtt__add_point(points, n, x, y)
points[n + 1].y = y points[n + 1].y = y
end end
-- tessellate until threshold p is happy... -- tessellate until threshold p is happy... @TODO warped to compensate for non-linear stretching
-- TODO: warped to compensate for non-linear stretching
--- @param points? stbtt__point[] --- @param points? stbtt__point[]
--- @param num_points int --- @param num_points int
--- @param x0 float --- @param x0 float
@@ -2116,7 +2412,7 @@ end
--- @param n int --- @param n int
--- @return int num_points # updated num_points passed in --- @return int num_points # updated num_points passed in
local function stbtt__tesselate_cubic(points, num_points, x0, y0, x1, y1, x2, y2, x3, y3, objspace_flatness_squared, n) local function stbtt__tesselate_cubic(points, num_points, x0, y0, x1, y1, x2, y2, x3, y3, objspace_flatness_squared, n)
-- TODO: this "flatness" calculation is just made-up nonsense that seems to work well enough -- @TODO this "flatness" calculation is just made-up nonsense that seems to work well enough
local dx0 = x1 - x0 local dx0 = x1 - x0
local dy0 = y1 - y0 local dy0 = y1 - y0
local dx1 = x2 - x1 local dx1 = x2 - x1
@@ -2159,6 +2455,7 @@ local function stbtt__tesselate_cubic(points, num_points, x0, y0, x1, y1, x2, y2
return num_points return num_points
end end
-- returns number of contours
--- @param vertices stbtt_vertex[] --- @param vertices stbtt_vertex[]
--- @param num_verts int --- @param num_verts int
--- @param objspace_flatness float --- @param objspace_flatness float
@@ -2235,6 +2532,7 @@ local function stbtt_FlattenCurves(vertices, num_verts, objspace_flatness)
return points, num_contours, contour_lengths return points, num_contours, contour_lengths
end end
-- rasterize a shape with quadratic beziers into a bitmap
--- @param result stbtt__bitmap --- @param result stbtt__bitmap
--- @param flatness_in_pixels float --- @param flatness_in_pixels float
--- @param vertices stbtt_vertex[] --- @param vertices stbtt_vertex[]
@@ -2258,6 +2556,11 @@ local function stbtt_Rasterize(result, flatness_in_pixels, vertices, num_verts,
end end
end end
-- renders the glyph into storage you pass in for the bitmap,
-- in the form of 'output', with row spacing of 'out_stride' bytes. the bitmap
-- is clipped to out_w/out_h bytes. Call stbtt_GetGlyphBitmapBox to get the
-- width and height and positioning info for it first. You can also specify
-- a subpixel shift for the character.
--- @param info stbtt_fontinfo --- @param info stbtt_fontinfo
--- @param output unsigned_char[] --- @param output unsigned_char[]
--- @param out_w int --- @param out_w int
@@ -2286,7 +2589,7 @@ end
----------------- -----------------
----------------- -----------------
--- ---
--- Bitmap baking --- bitmap baking
--- ---
--- ---
@@ -2298,7 +2601,7 @@ end
--------------------------- ---------------------------
--------------------------- ---------------------------
--- ---
--- Bitmap baking / packing --- bitmap baking / packing
--- ---
--- ---
@@ -2380,9 +2683,16 @@ local function stbtt__oversample_shift(oversample)
if oversample == 0 then if oversample == 0 then
return 0.0 return 0.0
end end
-- The prefilter is a box filter of width "oversample",
-- which shifts phase by (oversample - 1)/2 pixels in
-- oversampled space. We want to shift in the opposite
-- direction to counter this.
return -(oversample - 1) / (2.0 * oversample) return -(oversample - 1) / (2.0 * oversample)
end end
-- same as stbtt_MakeGlyphBitmapSubpixel, but prefiltering
-- is performed. Returns (sub_x, sub_y).
--- @param info stbtt_fontinfo --- @param info stbtt_fontinfo
--- @param output unsigned_char[] --- @param output unsigned_char[]
--- @param out_w int --- @param out_w int
@@ -2415,7 +2725,7 @@ end
------------------- -------------------
------------------- -------------------
--- ---
--- SDF computation --- sdf computation
--- ---
--- ---
@@ -2441,10 +2751,20 @@ end
--- stbtt__matchpair --- stbtt__matchpair
--- stbtt__matches --- stbtt__matches
-- Each .ttf/.ttc file may have more than one font. Each font has a sequential
-- index number starting from 0. Call this function to get the font offset for
-- a given index; it returns -1 if the index is out of range. A regular .ttf
-- file will only define one font and it always be at offset 0, so it will
-- return '0' for index 0, and -1 for all other indices.
--- @param data table --- @param data table
--- @param index int --- @param index int
function stbtt_GetFontOffsetForIndex(data, index) return stbtt_GetFontOffsetForIndex_internal(data, index) end function stbtt_GetFontOffsetForIndex(data, index) return stbtt_GetFontOffsetForIndex_internal(data, index) end
-- Given an offset into the file that defines a font, this function builds
-- the necessary cached info for the rest of the system. You must allocate
-- the stbtt_fontinfo yourself (see stbtt_fontinfo()), and stbtt_InitFont will fill it out. You don't
-- need to do anything special to free it, because the contents are pure
-- value data with no additional data structures. Returns false on failure.
--- @param info stbtt_fontinfo --- @param info stbtt_fontinfo
--- @param data table --- @param data table
--- @param offset int --- @param offset int
+223 -10
View File
@@ -6,15 +6,35 @@
# Why: ModuleScripts don't share globals, and the setfenv() alternative turns off Luau's fast paths / native codegen. # Why: ModuleScripts don't share globals, and the setfenv() alternative turns off Luau's fast paths / native codegen.
# Each source file becomes a function in __FILES; IM_INCLUDE(name) runs it once and caches the result. # Each source file becomes a function in __FILES; IM_INCLUDE(name) runs it once and caches the result.
# The module returns a table with every global the library defines (ImGui, ImVec2, ImGuiConfigFlags, ImGui_ImplRoblox, ...). # The module returns a table with every global the library defines (ImGui, ImVec2, ImGuiConfigFlags, ImGui_ImplRoblox, ...).
import os, re # Roblox target also writes ImGuiTyped.luau (types + require(script.Impl), see tools/gen_types.py).
#
# Build-time optimizations (token rewrites with minify.py's lexer: strings/comments are never touched, every source line
# stays on its line so linemap.json still maps errors). IMGUI_BUNDLE_NOOPT=1 turns all off, IMGUI_BUNDLE_NO_<X>=1 one:
# A hot library globals (most referenced names assigned at a file's top level) become bundle-level locals declared
# before __FILES: every file's assignment/reference then hits an upvalue instead of GETGLOBAL. Fewer names for
# LuaJIT (60 upvalues per function). Library globals can't be patched from outside (use the returned table).
# B enum folding: the bundle is loaded once headless (Luau CLI) to dump enum-like tables (all values numbers, never
# written inside a function) and numeric constants assigned once at top level; reads `ImGuiCol.Text` / `FLT_MAX`
# inside function bodies become `0 --[[ImGuiCol.Text]]`. Load-time (top-level) code is left alone.
# C `--@hoist` functions in the sources (pure numeric helpers: ImMin, ImMax, ImClamp, ...) are moved to the bundle top
# as `local function`s (their source lines become blank) so Luau -O2 can inline them everywhere.
# E statement-form `IM_ASSERT(...)` becomes `assert(...)` (fast builtin); expression uses are left alone.
import os, re, sys, json, shutil, subprocess, tempfile
HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.dirname(HERE)
sys.path.insert(0, HERE)
from minify import TOKEN
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
import sys
NO_DEMO = "--no-demo" in sys.argv NO_DEMO = "--no-demo" in sys.argv
LOVE = False LOVE = False
if "--target" in sys.argv: if "--target" in sys.argv:
i = sys.argv.index("--target"); LOVE = sys.argv[i + 1] == "love"; del sys.argv[i:i + 2] i = sys.argv.index("--target"); LOVE = sys.argv[i + 1] == "love"; del sys.argv[i:i + 2]
sys.argv = [a for a in sys.argv if a != "--no-demo"] sys.argv = [a for a in sys.argv if a != "--no-demo"]
OPT = {x: os.environ.get("IMGUI_BUNDLE_NOOPT") != "1" and os.environ.get("IMGUI_BUNDLE_NO_" + x) != "1" for x in "ABCE"}
# A limits: Luau 200 locals in the main chunk (+ helpers, __FILES, ...), 200 upvalues per function; LuaJIT 60 upvalues
# per function (margin left for nested functions that also capture file locals)
MAX_LOCALS, MAX_UPVALUES = 160, (45 if LOVE else 150)
BACKEND = "imgui_impl_love.lua" if LOVE else "imgui_impl_roblox.lua" BACKEND = "imgui_impl_love.lua" if LOVE else "imgui_impl_roblox.lua"
SKIP = {"imgui_impl_roblox.lua", "imgui_impl_roblox_fonts.lua"} if LOVE else {"imgui_impl_love.lua"} SKIP = {"imgui_impl_roblox.lua", "imgui_impl_roblox_fonts.lua"} if LOVE else {"imgui_impl_love.lua"}
FILES = {} FILES = {}
@@ -25,6 +45,7 @@ for d in (ROOT, os.path.join(ROOT, "backends"), os.path.join(ROOT, "fonts")):
if LOVE: src = re.sub(r"^@native$", "", src, flags=re.M) # keep line numbers if LOVE: src = re.sub(r"^@native$", "", src, flags=re.M) # keep line numbers
FILES[f] = src FILES[f] = src
ORIG = dict(FILES) # for gen_types
ENTRY = ["imgui.lua", BACKEND] + ([] if NO_DEMO else ["imgui_demo.lua"]) # run order ENTRY = ["imgui.lua", BACKEND] + ([] if NO_DEMO else ["imgui_demo.lua"]) # run order
names = set() names = set()
@@ -32,16 +53,207 @@ for src in FILES.values():
names.update(re.findall(r"^function ([A-Za-z_]\w*)\s*\(", src, re.M)) names.update(re.findall(r"^function ([A-Za-z_]\w*)\s*\(", src, re.M))
names.update(re.findall(r"^([A-Za-z_]\w*)\s*=[^=]", src, re.M)) names.update(re.findall(r"^([A-Za-z_]\w*)\s*=[^=]", src, re.M))
out = ["--!optimize 2", "-- GENERATED by tools/bundle.py from *.lua, backends/, fonts/ - do not edit", ""] # ---- lexer helpers ----
if LOVE: KW = set("and break do else elseif end false for function goto if in local nil not or repeat return then true until while".split())
out = ["-- GENERATED by tools/bundle.py --target love - do not edit", re.search(r"--@compat-begin\n(.*?)--@compat-end", open(os.path.join(ROOT, "backends", "imgui_impl_love.lua")).read(), re.S).group(1), ""]
out += [ def lex(src):
"local __FILES, __LOADED = {}, {}", """[(kind, text, start)] without whitespace/comments; kinds: name num str op."""
toks, pos = [], 0
while pos < len(src):
m = TOKEN.match(src, pos)
k = m.lastgroup
if k in ("ceq", "seq"): k = "lcom" if m.group(0).startswith("--") else "lstr"
if k not in ("ws", "lcom", "com"): toks.append(("str" if k == "lstr" else k, m.group(0), pos))
pos = m.end()
return toks
def fdepths(toks):
"""function nesting depth of every token (0 = runs at load time)"""
st, fd, out = [], 0, []
for k, t, _ in toks:
out.append(fd)
if k == "name":
if t == "function": st.append(1); fd += 1
elif t in ("if", "do", "repeat"): st.append(0)
elif t in ("end", "until"): fd -= st.pop()
assert not st, "unbalanced blocks"
return out
def declared(toks):
"""names declared as locals / parameters / loop variables anywhere"""
out, n = set(), len(toks)
for i, (k, t, _) in enumerate(toks):
j = None
if t == "local": j = i + 2 if toks[i + 1][1] == "function" else i + 1
elif t == "for": j = i + 1
elif t == "function":
j = i + 1
while toks[j][1] != "(": j += 1
j += 1
while j is not None and j < n and toks[j][0] == "name" and toks[j][1] not in KW:
out.add(toks[j][1])
j += 1
if j < n and toks[j][1] == ",": j += 1
else: break
return out
def apply(src, edits):
for s, e, r in sorted(edits, reverse=True): src = src[:s] + r + src[e:]
return src
def isref(toks, i):
"""toks[i] is a variable read/write (not a field name)"""
return toks[i][0] == "name" and (i == 0 or toks[i - 1][1] not in (".", ":"))
def nexttext(toks, i): return toks[i + 1][1] if i + 1 < len(toks) else ""
# ---- C: --@hoist helpers ----
hoisted, hoisted_names = [], []
if OPT["C"]:
for f, src in FILES.items():
while "--@hoist\n" in src:
s = src.index("--@hoist\n") + len("--@hoist\n")
toks = lex(src[s:])
depth, i = 0, 0
while True: # first `function` ... matching end
t = toks[i][1]
if t == "function" or t in ("if", "do", "repeat"): depth += 1
elif t in ("end", "until"):
depth -= 1
if depth == 0: break
i += 1
e = s + toks[i][2] + 3
text = src[s:e]
m = re.search(r"^(local )?function ([A-Za-z_]\w*)", text, re.M)
hoisted_names.append(m.group(2))
hoisted.append(text[:m.start()] + "local function " + text[m.start() + len(m.group(0)) - len(m.group(2)):])
src = src[:s - len("--@hoist\n")] + "\n" + re.sub(r"[^\n]", "", text) + src[e:]
FILES[f] = src
HOIST_DEPS = "local type, mathMin, mathMax, mathFloor, mathCeil, bitOr, bitLShift = type, math.min, math.max, math.floor, math.ceil, bit32.bor, bit32.lshift"
# ---- E: statement IM_ASSERT -> assert ----
STMT_PREV_KW = {"end", "true", "false", "nil", "break", "then", "do", "else", "repeat"}
def rewrite_assert(src):
toks, edits = lex(src), []
for i, (k, t, p) in enumerate(toks):
if t == "IM_ASSERT" and isref(toks, i) and nexttext(toks, i) == "(":
pk, pt = (toks[i - 1][0], toks[i - 1][1]) if i else ("op", ";")
if (pk == "name" and (pt not in KW or pt in STMT_PREV_KW)) or pk in ("num", "str") or pt in (")", "]", "}", ";", "..."):
edits.append((p, p + len(t), "assert"))
return apply(src, edits)
if OPT["E"]:
FILES = {f: rewrite_assert(s) for f, s in FILES.items()}
hoisted = [rewrite_assert(s) for s in hoisted]
# ---- B: enum / constant folding ----
LUAU = shutil.which("luau") or "/nix/store/2m2ja613nndihc8qm10f2rhq6znp1nh4-luau-0.734/bin/luau"
def header():
return ["local __FILES, __LOADED = {}, {}",
"function IM_INCLUDE(name)", "function IM_INCLUDE(name)",
" local r = __LOADED[name]", " local r = __LOADED[name]",
" if r == nil then r = __FILES[name](); if r == nil then r = true end; __LOADED[name] = r end", " if r == nil then r = __FILES[name](); if r == nil then r = true end; __LOADED[name] = r end",
" return r", " return r",
"end", ""] "end", ""]
def numtext(s):
v = float(s)
if v != v or v in (float("inf"), float("-inf")) or (v == 0 and s.startswith("-")): return None
r = str(int(v)) if v.is_integer() and abs(v) < 2 ** 53 else repr(v)
return "(%s)" % r if v < 0 else r
def dump_values():
"""{name: number} and {name: {key: number}} of the loaded library (imgui.lua and what it includes)"""
src = ["--!optimize 1"] + ([HOIST_DEPS] + hoisted if hoisted else []) + header()
for name, s in sorted(FILES.items()): src += ["__FILES[%r] = function()" % name, s, "end"]
src += ['IM_INCLUDE("imgui.lua")',
'local function D(n, v) if type(v) == "number" then print("C", n, string.format("%.17g", v)) elseif type(v) == "table" then',
' for k, x in pairs(v) do print("T", n, type(k) == "string" and k or "?", type(x) == "number" and string.format("%.17g", x) or "?") end end end']
src += ["D(%r, %s)" % (n, n) for n in sorted(names)]
with tempfile.NamedTemporaryFile("w", suffix=".luau", delete=False) as fp: fp.write("\n".join(src))
try:
p = subprocess.run([LUAU, fp.name], capture_output=True, text=True, timeout=60)
finally:
os.unlink(fp.name)
if p.returncode: raise RuntimeError("enum dump failed:\n" + p.stdout[-2000:] + p.stderr[-2000:])
consts, tables = {}, {}
for line in p.stdout.splitlines():
a = line.split("\t")
if a[0] == "C": consts[a[1]] = a[2]
elif a[0] == "T": tables.setdefault(a[1], {})[a[2]] = a[3]
tables = {n: t for n, t in tables.items() if "?" not in t and "?" not in t.values()}
return consts, tables
def fold(files):
consts, tables = dump_values()
lexed = {f: lex(s) for f, s in files.items()}
depth = {f: fdepths(t) for f, t in lexed.items()}
# tables written inside a function, constants assigned more than once or not at top level: not foldable
assigns = {}
for f, toks in lexed.items():
for i, (k, t, _) in enumerate(toks):
if not isref(toks, i): continue
if t in tables and depth[f][i] > 0:
j = i + 1
if nexttext(toks, i) == ".": j = i + 3
elif nexttext(toks, i) == "[":
b = 0
while True:
b += {"[": 1, "]": -1}.get(toks[j][1], 0); j += 1
if b == 0: break
if j < len(toks) and toks[j][1] == "=": tables.pop(t, None)
if t in consts and nexttext(toks, i) == "=":
assigns[t] = assigns.get(t, 0) + (1 if depth[f][i] == 0 else 2)
consts = {n: v for n, v in consts.items() if assigns.get(n) == 1 and numtext(v)}
out = {}
for f, toks in lexed.items():
shadow = declared(toks)
edits = []
for i, (k, t, p) in enumerate(toks):
if depth[f][i] == 0 or not isref(toks, i) or t in shadow: continue
if t in tables and nexttext(toks, i) == "." and i + 2 < len(toks):
key = toks[i + 2][1]
if key in tables[t] and numtext(tables[t][key]) and nexttext(toks, i + 2) not in ("=", ".", ":", "[", "(", "{") and toks[i + 3][0] != "str":
edits.append((p, toks[i + 2][2] + len(key), "%s --[[%s.%s]]" % (numtext(tables[t][key]), t, key)))
elif t in consts and nexttext(toks, i) not in ("=", ".", ":", "[", "(", "{") and toks[i + 1][0] != "str":
edits.append((p, p + len(t), "%s --[[%s]]" % (numtext(consts[t]), t)))
out[f] = apply(files[f], edits)
return out
if OPT["B"]:
try:
allf = dict(FILES); allf.update({"@hoist%d" % i: "local function __h()\n" + s + "\nend" for i, s in enumerate(hoisted)})
allf = fold(allf)
hoisted = [allf.pop("@hoist%d" % i)[len("local function __h()\n"):-len("\nend")] for i in range(len(hoisted))]
FILES = allf
except (OSError, RuntimeError) as e:
if os.environ.get("IMGUI_BUNDLE_STRICT"): raise # CI: a release must not silently lose the folding
print("bundle.py: enum folding skipped (%s)" % str(e).strip()[:300], file=sys.stderr)
# ---- A: hot globals -> bundle locals ----
hot = []
if OPT["A"]:
count, used = {}, []
for src in FILES.values():
toks = lex(src)
used.append({t for i, (k, t, _) in enumerate(toks) if isref(toks, i)})
for i, (k, t, _) in enumerate(toks):
if t in names and isref(toks, i): count[t] = count.get(t, 0) + 1
# greedy by reference count; each file function's upvalues (hoisted names it uses) stay <= MAX_UPVALUES
taken = [len(u & set(hoisted_names + re.findall(r"\w+", HOIST_DEPS.split("=")[0]))) for u in used]
for n in sorted((n for n in count if n not in hoisted_names), key=lambda n: (-count[n], n)):
if len(hot) == MAX_LOCALS: break
if all(taken[i] < MAX_UPVALUES for i, u in enumerate(used) if n in u):
hot.append(n)
taken = [c + (n in u) for c, u in zip(taken, used)]
out = ["--!optimize 2", "-- GENERATED by tools/bundle.py from *.lua, backends/, fonts/ - do not edit", ""]
if LOVE:
out = ["-- GENERATED by tools/bundle.py --target love - do not edit", re.search(r"--@compat-begin\n(.*?)--@compat-end", open(os.path.join(ROOT, "backends", "imgui_impl_love.lua")).read(), re.S).group(1), ""]
if hot: out.append("local " + ", ".join(hot))
if hoisted: out += [HOIST_DEPS] + hoisted
out += header()
linemap = [] # (first bundle line, file) for error -> source mapping (tools/test/run.py) linemap = [] # (first bundle line, file) for error -> source mapping (tools/test/run.py)
for name, src in sorted(FILES.items()): for name, src in sorted(FILES.items()):
out.append("__FILES[%r] = function()" % name) out.append("__FILES[%r] = function()" % name)
@@ -57,11 +269,12 @@ for n in sorted(names):
out.append(" %s = %s," % (n, n)) out.append(" %s = %s," % (n, n))
out.append("}") out.append("}")
import sys
OUT = sys.argv[1] if len(sys.argv) > 1 else os.path.join(ROOT, "build") OUT = sys.argv[1] if len(sys.argv) > 1 else os.path.join(ROOT, "build")
os.makedirs(OUT, exist_ok=True) os.makedirs(OUT, exist_ok=True)
NAME = "imgui_love.lua" if LOVE else "ImGui.luau" NAME = "imgui_love.lua" if LOVE else "ImGui.luau"
open(os.path.join(OUT, NAME), "w").write("\n".join(out) + "\n") open(os.path.join(OUT, NAME), "w").write("\n".join(out) + "\n")
import json
json.dump(linemap, open(os.path.join(OUT, NAME.rsplit(".", 1)[0] + ".linemap.json"), "w")) json.dump(linemap, open(os.path.join(OUT, NAME.rsplit(".", 1)[0] + ".linemap.json"), "w"))
if not LOVE:
from gen_types import wrapper
open(os.path.join(OUT, "ImGuiTyped.luau"), "w").write(wrapper(ORIG))
print("wrote build/%s (%d files, %d exports)" % (NAME, len(FILES), len(names))) print("wrote build/%s (%d files, %d exports)" % (NAME, len(FILES), len(names)))
+559
View File
@@ -0,0 +1,559 @@
#!/usr/bin/env python3
# Compares the port against upstream Dear ImGui (tools/test/upstream/*) and writes docs/PORT_STATUS.txt.
# python3 tools/compare_upstream.py # print summary, write docs/PORT_STATUS.txt
# Ported %: upstream functions (by qualified name, overloads merged) that have a port definition; enum values; struct fields;
# public/internal API declarations in imgui.h / imgui_internal.h (namespace ImGui).
# Similarity %: for each ported function, both bodies are normalized to a common token stream (comments/types/punctuation
# dropped, C++ operators and names mapped to Lua: -> :: . ! && || _Flag) and compared with difflib ratio.
# File figures are weighted by upstream body size. Matching is by name, so it's an estimate, not a proof.
# Excluded from the %: C++-only (operators, destructors), #if 0, IMGUI_DISABLE_OBSOLETE_FUNCTIONS, OS-specific (#ifdef _WIN32...),
# test engine. They're listed separately in the report.
import os, re, sys
from difflib import SequenceMatcher
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from minify import TOKEN as LUA_TOKEN
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
UP = os.path.join(ROOT, "tools", "test", "upstream")
CPP_FILES = ["imgui.cpp", "imgui_widgets.cpp", "imgui_draw.cpp", "imgui_tables.cpp", "imgui_demo.cpp", "imgui.h", "imgui_internal.h"]
PORT_FILES = sorted(f for f in os.listdir(ROOT) if f.endswith(".lua"))
LUA_CLASS_ALIAS = {"IM_RECT": "ImRect", "IM_VECTOR": "ImVector", "IM_POOL": "ImPool", "IMGUI_VIEWPORT_P": "ImGuiViewportP",
"ImGuiTextBufferMT": "ImGuiTextBuffer", "IMGUI_TEXT_INDEX": "ImGuiTextIndex", "IM_PAYLOAD": "ImGuiPayload",
"DN": "ImGuiDockNode", "IM_VTXBUF": "ImVector"}
# ---------------------------------------------------------------------------------------------------------------------
# C++ side
# ---------------------------------------------------------------------------------------------------------------------
CPP_TOKEN = re.compile(r"""
(?P<nl>\n)
| (?P<ws>[ \t\r\f\v]+|\\\n)
| (?P<com>//[^\n]*|/\*.*?\*/)
| (?P<pp>\#(?:[^\n\\]|\\.|\\\n)*)
| (?P<str>(?:u8|u|U|L)?"(?:\\.|[^"\\\n])*"|'(?:\\.|[^'\\\n])*')
| (?P<num>0[xX][0-9a-fA-F']+[uUlL]*|(?:\d[\d']*\.?\d*|\.\d+)(?:[eE][+-]?\d+)?[fFuUlL]*)
| (?P<name>[A-Za-z_]\w*)
| (?P<op>->\*?|::|<<=|>>=|<<|>>|&&|\|\||==|!=|<=|>=|\+\+|--|[-+*/%&|^]=|\.\.\.|.)
""", re.S | re.X)
EXCLUDE_PP = [("if0", re.compile(r"^\s*#\s*if\s+0\b")),
("obsolete", re.compile(r"#\s*ifndef\s+IMGUI_DISABLE_OBSOLETE_FUNCTIONS|#\s*if\s+!defined\(IMGUI_DISABLE_OBSOLETE")),
("os-specific", re.compile(r"_WIN32|__APPLE__|__linux__|_MSC_VER|__EMSCRIPTEN__|IMGUI_DISABLE_WIN32|__ANDROID__|_XBOX|__MINGW")),
("test-engine", re.compile(r"IMGUI_ENABLE_TEST_ENGINE"))]
def cpp_tokens(src):
"""[(kind, text, line, excl)] without whitespace/comments; excl = exclusion tag from enclosing #if, or None."""
out, line, stack, sections = [], 1, [], []
for m in CPP_TOKEN.finditer(src):
kind, text = m.lastgroup, m.group(0)
if kind == "nl": line += 1; continue
if kind == "com":
s = re.match(r"//\s*\[SECTION\]\s*(.+)", text)
if s: sections.append((line, s.group(1).strip()))
line += text.count("\n"); continue
if kind == "ws": line += text.count("\n"); continue
if kind == "pp":
d = re.match(r"#\s*(\w+)", text); d = d.group(1) if d else ""
if d in ("if", "ifdef", "ifndef"):
stack.append(next((t for t, r in EXCLUDE_PP if r.search(text)), None))
elif d in ("else", "elif") and stack:
stack[-1] = None if stack[-1] in ("if0", "obsolete") else stack[-1] # #else of `#if 0` / obsolete is the live path
elif d == "endif" and stack:
stack.pop()
line += text.count("\n"); continue
out.append((kind, text, line, next((x for x in reversed(stack) if x), None)))
return out, sections
CTRL = {"if", "for", "while", "switch", "catch", "sizeof", "return", "alignof", "decltype", "IM_ASSERT", "defined"}
def match_back(toks, i, close, open_):
depth = 0
while i >= 0:
t = toks[i][1]
if t == close: depth += 1
elif t == open_:
depth -= 1
if depth == 0: return i
i -= 1
return -1
def match_fwd(toks, i):
depth = 0
pairs = {"{": "}", "(": ")", "[": "]"}
open_ = toks[i][1]; close = pairs[open_]
while i < len(toks):
t = toks[i][1]
if t == open_: depth += 1
elif t == close:
depth -= 1
if depth == 0: return i
i += 1
return len(toks) - 1
def func_head(toks, i):
"""toks[i] is '{'. If it opens a function body, return (class, name, name_tok_index, cpp_only) else None."""
j = i - 1
while j >= 0 and toks[j][1] in ("const", "noexcept", "override", "final", "IM_FMTARGS", "IM_FMTLIST"):
j -= 1
if j >= 0 and toks[j][1] == ")" and toks[match_back(toks, j, ")", "(") - 1][1] in ("IM_FMTARGS", "IM_FMTLIST"):
j = match_back(toks, j, ")", "(") - 2
# constructor initializer list: Foo::Foo(...) : a(1), b{2} {
k = j
while k >= 0 and toks[k][1] in (")", "}"):
o = match_back(toks, k, toks[k][1], "(" if toks[k][1] == ")" else "{")
if o <= 1: break
if toks[o - 2][1] in (",",) or (toks[o - 2][1] == ":" ):
if toks[o - 2][1] == ":": j = o - 3; break
k = o - 3; continue
break
if j < 0 or toks[j][1] != ")": return None
o = match_back(toks, j, ")", "(")
if o <= 0: return None
n = o - 1
name = toks[n][1]
cpp_only = False
if toks[n - 1][1] == "operator" or (n >= 1 and toks[n][0] == "op" and toks[n - 1][1] == "operator"):
return (None, "operator" + name, n, True)
if toks[n][0] == "op" and n >= 2 and toks[n - 1][1] == "operator": return (None, "operator", n, True)
if toks[n][0] != "name" or name in CTRL: return None
if toks[n - 1][1] == "~": cpp_only = True; name = "~" + name
cls = None
if toks[n - 1][1] == "::" or (cpp_only and toks[n - 2][1] == "::"):
q = n - 2 if not cpp_only else n - 3
cls = toks[q][1]
if toks[n - 1][1] == "operator": return (cls, "operator", n, True)
return (cls, name, n, cpp_only)
class UpFunc:
def __init__(s, file, cls, name, line, body, section, excl):
s.file, s.cls, s.name, s.line, s.body, s.section, s.excl = file, cls, name, line, body, section, excl
@property
def key(s): return "%s::%s" % (s.cls, s.name) if s.cls else s.name
def parse_cpp(fname):
src = open(os.path.join(UP, fname), encoding="utf-8", errors="replace").read()
toks, sections = cpp_tokens(src)
funcs, enums, structs, decls = [], {}, {}, {} # decls: namespace ImGui declarations (name -> line)
def section_at(line):
cur = ""
for l, s in sections:
if l <= line and l > 120: cur = s # skip the index at the top of each file
return cur
def scan(a, b, cls, ns):
i = a
stmt_start = a
while i < b:
k, t, line, excl = toks[i]
if t == "{":
head = func_head(toks, i)
e = match_fwd(toks, i)
pre = [x[1] for x in toks[stmt_start:i]]
if head:
hc, hn, ni, cpp_only = head
c = hc or cls
if cls and hn == cls: c = cls # inline ctor in struct
ex = "c++-only" if cpp_only or hn.startswith("~") else toks[ni][3]
if c and hn == c and fname.endswith(".h") and e - i < 4: ex = ex # trivial ctor kept
funcs.append(UpFunc(fname, c, hn, toks[ni][2], toks[i + 1:e], section_at(toks[ni][2]), ex))
elif "namespace" in pre:
q = pre.index("namespace")
scan(i + 1, e, cls, (pre[q + 1] if len(pre) > q + 1 else ""))
elif "enum" in pre:
nm = next((x for x in pre[pre.index("enum") + 1:] if re.match(r"[A-Za-z_]\w*$", x) and x not in ("class", "int")), None)
vals, j, depth = [], i + 1, 0
expect = True
while j < e:
tt = toks[j][1]
if tt in "([{": depth += 1
elif tt in ")]}": depth -= 1
elif depth == 0 and tt == ",": expect = True
elif expect and depth == 0 and toks[j][0] == "name":
vals.append((tt, toks[j][2], toks[j][3])); expect = False
j += 1
if nm: enums[nm] = (vals, line, section_at(line))
elif any(x in ("struct", "class", "union") for x in pre) and "=" not in pre:
names = [x for x in pre if re.match(r"[A-Za-z_]\w*$", x) and x not in ("struct", "class", "union", "IMGUI_API", "typedef", "final", "public", "private", "alignas")]
nm = names[0] if names else cls
if nm and "union" not in pre: structs.setdefault(nm, ([], line, section_at(line)))
scan(i + 1, e, nm, ns)
if nm and pre[:1] != ["union"] and toks[e + 1][1] != ";" and toks[e + 1][0] == "name" and cls:
pass
i = e + 1
if i < b and toks[i][1] == ";": i += 1
stmt_start = i
continue
if t == ";":
st = toks[stmt_start:i]
txt = [x[1] for x in st]
if txt and txt[0] in ("public", "private", "protected") and len(txt) > 1 and txt[1] == ":": st, txt = st[2:], txt[2:]
if txt and ns == "ImGui" and cls is None and "(" in txt and txt[0] not in ("typedef", "struct", "enum", "using"):
p = txt.index("(")
if p > 0 and st[p - 1][0] == "name" and txt[p - 1] not in CTRL:
decls.setdefault(txt[p - 1], (st[p - 1][2], st[p - 1][3], section_at(st[p - 1][2])))
elif txt and cls and txt[0] not in ("typedef", "friend", "static", "using", "enum", "struct", "class", "template", "IM_MSVC_RUNTIME_CHECKS_OFF", "IM_MSVC_RUNTIME_CHECKS_RESTORE"):
eq = txt.index("=") if "=" in txt else len(txt)
pa = txt.index("(") if "(" in txt else len(txt)
if pa >= eq: # not a method declaration
depth = 0
for j, x in enumerate(txt[:eq] + [";"]):
if x in ("<", "(", "["): depth += 1
elif x in (">", ")", "]"): depth -= 1
elif depth == 0 and x in (",", ";", ":") or (depth == 0 and x == "[" ):
if j > 0 and re.match(r"[A-Za-z_]\w*$", txt[j - 1]) and txt[j - 1] not in ("const", "unsigned", "int", "float"):
structs.setdefault(cls, ([], 0, ""))[0].append((txt[j - 1], st[j - 1][2], st[j - 1][3]))
if x == "[" and depth == 1 and j > 0 and re.match(r"[A-Za-z_]\w*$", txt[j - 1]):
structs.setdefault(cls, ([], 0, ""))[0].append((txt[j - 1], st[j - 1][2], st[j - 1][3]))
if eq < len(txt) and re.match(r"[A-Za-z_]\w*$", txt[eq - 1]):
structs.setdefault(cls, ([], 0, ""))[0].append((txt[eq - 1], st[eq - 1][2], st[eq - 1][3]))
stmt_start = i + 1
i += 1
scan(0, len(toks), None, "")
for k in structs: # dedupe fields
seen, out = set(), []
for f in structs[k][0]:
if f[0] not in seen: seen.add(f[0]); out.append(f)
structs[k] = (out,) + structs[k][1:]
return funcs, enums, structs, decls
# ---------------------------------------------------------------------------------------------------------------------
# Lua side
# ---------------------------------------------------------------------------------------------------------------------
def lua_tokens(src):
out, line, pos = [], 1, 0
while pos < len(src):
m = LUA_TOKEN.match(src, pos)
kind, text = m.lastgroup, m.group(0)
if kind in ("ceq", "seq"): kind = "lcom" if text.startswith("--") else "lstr"
if kind not in ("ws", "com", "lcom"):
out.append(("str" if kind in ("str", "lstr") else kind, text, line))
line += text.count("\n"); pos = m.end()
return out
OPENERS = {"function", "if", "do", "repeat"}
CLOSERS = {"end", "until"}
class PortFunc:
alias = None
def __init__(s, file, cls, name, line, body):
s.file, s.cls, s.name, s.line, s.body = file, cls, name, line, body
def parse_lua():
defs, enum_keys, globals_, fields = [], set(), set(), set()
for f in PORT_FILES:
toks = lua_tokens(open(os.path.join(ROOT, f), encoding="utf-8").read())
n = len(toks)
for i, (k, t, line) in enumerate(toks):
chain, start = None, None
if t == "function" and i + 1 < n and toks[i + 1][0] == "name":
j = i + 1; chain = [toks[j][1]]
while j + 2 < n and toks[j + 1][1] in (".", ":") and toks[j + 2][0] == "name":
chain.append(toks[j + 2][1]); j += 2
if j + 1 < n and toks[j + 1][1] == "(": start = j + 1
else: chain = None
elif k == "name" and i + 3 < n and toks[i + 1][1] == "=" and toks[i + 2][1] == "function" and toks[i + 3][1] == "(":
j = i; chain = [t]
while j >= 2 and toks[j - 1][1] == "." and toks[j - 2][0] == "name":
chain.insert(0, toks[j - 2][1]); j -= 2
start = i + 3; i0 = i + 2
if chain is None: continue
# body: from start's ')' to matching end
j = start
while toks[j][1] != ")": j += 1
body_start, depth, j = j + 1, 1, j + 1
while j < n and depth > 0:
tt = toks[j][1]
if tt in OPENERS and toks[j][0] == "name": depth += 1
elif tt in CLOSERS and toks[j][0] == "name": depth -= 1
j += 1
name = chain[-1]
cls = LUA_CLASS_ALIAS.get(chain[-2], chain[-2]) if len(chain) > 1 else None
pf = PortFunc(f, cls, name, line, toks[body_start:j - 1])
bt = [x[1] for x in pf.body]
if len(bt) >= 5 and bt[0] == "return" and bt[-3:] == ["(", "...", ")"] and all(x[0] == "name" or x[1] in (".", ":") for x in pf.body[1:-3]):
pf.alias = " ".join(bt[1:-3]) + "(...)"; pf.body = pf.body[1:-3] # forwarder: function(...) return ImGui.X(...) end
defs.append(pf)
# aliases / factories: ImGui.DragFloat2 = DragFloatN(2) ImFormatString = string.format ImGui.LogTextV = ImGui.LogText
for i, (k, t, line) in enumerate(toks):
if k != "name" or (i and toks[i - 1][2] == line) or t == "local": continue
j, chain = i, [t]
while j + 2 < n and toks[j + 1][1] == "." and toks[j + 2][0] == "name":
chain.append(toks[j + 2][1]); j += 2
if j + 2 >= n or toks[j + 1][1] != "=" or toks[j + 2][0] != "name" or toks[j + 2][1] in ("function", "nil", "true", "false", "not"): continue
if len(chain) == 1 and not re.match(r"(Im|STB|IMSTB|stb)", t): continue
if len(chain) > 1 and chain[0] not in ("ImGui", "ImStd", "ImStb", "MT") and chain[-2] not in LUA_CLASS_ALIAS: continue
rhs = [x for x in toks[j + 2:] if x[2] == toks[j + 2][2]]
cls = LUA_CLASS_ALIAS.get(chain[-2], chain[-2]) if len(chain) > 1 else None
pf = PortFunc(f, cls, chain[-1], line, rhs); pf.alias = " ".join(x[1] for x in rhs[:6])
defs.append(pf)
# enum tables / globals / fields
depth_stack = []
for i, (k, t, line) in enumerate(toks):
if t == "{":
owner = None
if i >= 2 and toks[i - 1][1] == "=" and toks[i - 2][0] == "name": owner = toks[i - 2][1]
depth_stack.append(owner)
elif t == "}":
if depth_stack: depth_stack.pop()
elif k == "name" and i + 1 < n and toks[i + 1][1] == "=":
prev = toks[i - 1][1] if i else ""
if prev in ("{", ",", ";") or (prev == "." ) or (prev not in ("local", "for") and toks[i - 1][0] != "name" and prev not in ("]", ")")):
fields.add(t)
if prev in ("{", ",") and depth_stack and depth_stack[-1]: enum_keys.add((depth_stack[-1], t))
if prev == "." and i >= 2: enum_keys.add((toks[i - 2][1], t))
if prev not in (".", "local", ",") and not depth_stack: globals_.add(t)
elif k == "str" and i >= 1 and toks[i - 1][1] == "[" and i + 2 < n and toks[i + 1][1] == "]" and toks[i + 2][1] == "=" and depth_stack and depth_stack[-1]:
enum_keys.add((depth_stack[-1], toks[i][1].strip("\"'")))
return defs, enum_keys, globals_, fields
# ---------------------------------------------------------------------------------------------------------------------
# Normalization + similarity
# ---------------------------------------------------------------------------------------------------------------------
CPP_DROP = {"const", "static", "float", "int", "unsigned", "signed", "bool", "char", "void", "auto", "double", "size_t", "short", "long",
"ImU8", "ImU16", "ImU32", "ImU64", "ImS8", "ImS16", "ImS32", "ImS64", "ImWchar", "ImGuiID", "struct", "inline", "IMGUI_API",
"constexpr", "template", "typename", "IM_UNUSED", "this", ";", "{", "}", "(", ")", "&", "*", "ImGuiContext", "ImGuiWindow", "ImGuiStyle",
"ImGuiIO", "ImDrawList", "ImFont", "ImRect", "ImVec2", "ImVec4", "case", "break", "default", ":", "[", "]"}
LUA_DROP = {"local", "then", "do", "end", "self", "(", ")", ";", "{", "}", "&", "*", "[", "]", ":", "bit32", "math", "string", "table"}
LUA_MAP = {"~=": "!=", "not": "!", "and": "&&", "or": "||", "nil": "NULL", "elseif": "else if", "band": "&", "bor": "|", "bxor": "^",
"bnot": "~", "lshift": "<<", "rshift": ">>", "btest": "&", "ImMax": "ImMax", "ImMin": "ImMin"}
CPP_MAP = {"->": ".", "::": ".", "nullptr": "NULL", "++": "+=", "--": "-=", "IM_COUNTOF": "#", "IM_ARRAYSIZE": "#", "true": "true", "false": "false"}
def norm_num(t):
try:
if t.lower().startswith("0x"): return str(int(t.rstrip("uUlL").replace("'", ""), 16))
return repr(float(t.rstrip("fFuUlL").replace("'", "")))
except ValueError: return t
def split_enum(t):
m = re.match(r"(Im[A-Za-z]+)_(\w+)$", t)
return [m.group(1), ".", m.group(2)] if m and not t.endswith("_") else [t]
def norm_cpp(body):
out = []
for k, t, _, _ in body:
if k == "num": out.append(norm_num(t)); continue
if k == "str": out.append("S"); continue
t = CPP_MAP.get(t, t)
if t in CPP_DROP: continue
out.extend(split_enum(t) if k == "name" else [t])
o2 = []
for i, t in enumerate(out): # drop the implicit ImGui:: qualifier (C++ calls inside the namespace omit it)
if t == "ImGui" and i + 1 < len(out) and out[i + 1] == ".": continue
if t == "." and o2 and o2[-1] in (".",): continue
o2.append(t)
return [t for i, t in enumerate(o2) if not (t == "." and (i == 0 or o2[i - 1] in ("(", ",", "=", "return")))]
def norm_lua(body):
out = []
for k, t, _ in body:
if k == "num": out.append(norm_num(t)); continue
if k == "str": out.append("S"); continue
if t in LUA_DROP: continue
t = LUA_MAP.get(t, t)
out.extend(t.split(" ") if " " in t else [t])
o2 = []
for i, t in enumerate(out):
if t == "ImGui" and i + 1 < len(out) and out[i + 1] == ".": continue
if t == "." and (not o2 or o2[-1] in ("&", "|", "^", "~", "<<", ">>", "=", ",", "return", "if", "while", "!")): continue
o2.append(t)
return o2
def similarity(a, b):
if not a and not b: return 1.0
return SequenceMatcher(None, a, b, autojunk=False).ratio()
def is_stub(port_norm, up_norm):
real = [t for t in port_norm if t not in ("return", "NULL", "false", "true", "0.0", "S", "IM_ASSERT", "!", ",", "=", "0")]
return len(up_norm) > 12 and (len(real) < 3 or not any(re.match(r"[A-Za-z_]\w*$", t) for t in real))
# ---------------------------------------------------------------------------------------------------------------------
def main():
defs, enum_keys, lua_globals, lua_fields = parse_lua()
by_name = {}
for d in sorted(defs, key=lambda d: d.alias is not None): by_name.setdefault(d.name, []).append(d)
port_classes = {d.cls for d in defs if d.cls}
def find(u):
m = re.match(r"I?M?STB_TEXTEDIT_(\w+?)(_IMPL)?$", u.name)
if m: # STB_TEXTEDIT_INSERTCHARS / IMSTB_TEXTEDIT_GETNEXTCHARINDEX_IMPL -> ImStb.TEXTEDIT_*
c = [d for d in by_name.get("TEXTEDIT_" + m.group(1), []) if d.cls == "ImStb"]
return c[0] if c else None
cands = by_name.get(u.name, [])
if u.cls and u.cls not in ("ImGui",):
exact = [d for d in cands if d.cls == u.cls]
if exact: return exact[0]
if u.name == u.cls: # constructor: function ImGuiStyle() / MT.X.new
c = [d for d in cands if d.cls is None] or [d for d in by_name.get("new", []) if d.cls == u.cls]
return c[0] if c else None
if u.cls in port_classes: return None
loose = [d for d in cands if d.cls in (None, "ImGui")] if len(cands) == 1 else []
return loose[0] if loose else None
pref = [d for d in cands if d.cls == "ImGui"] + [d for d in cands if d.cls is None] + [d for d in cands if d.cls in ("ImStd", "ImStb")]
return pref[0] if pref else None
up_names = set()
for f in CPP_FILES:
for u in parse_cpp(f)[0]: up_names.add(u.name)
def expand(p, depth=0, seen=()):
"""Port body with calls to port-only helpers in the same file inlined (the port splits some big functions)."""
out, body = [], p.body
for i, x in enumerate(body):
out.append(x)
if depth < 3 and x[0] == "name" and i + 1 < len(body) and body[i + 1][1] == "(" and x[1] not in up_names and (i == 0 or body[i - 1][1] not in (".", ":")):
h = [d for d in by_name.get(x[1], []) if d.file == p.file and not d.alias and d is not p and d.name not in seen]
if h: out.extend(expand(h[0], depth + 1, seen + (p.name,)))
return out
report, summary = [], []
all_up = {}
enums_all, structs_all, decls_by_file = {}, {}, {}
for f in CPP_FILES:
funcs, enums, structs, decls = parse_cpp(f)
merged = {}
for u in funcs: # merge overloads (keep the largest body for similarity)
if u.key not in merged or len(u.body) > len(merged[u.key].body):
if u.key in merged and merged[u.key].excl is None: u.excl = None
merged[u.key] = u
elif u.excl is None: merged[u.key].excl = None
all_up[f] = list(merged.values())
for k, v in enums.items(): enums_all.setdefault(k, v)
for k, v in structs.items():
if k in structs_all: structs_all[k][0].extend(x for x in v[0] if x[0] not in {y[0] for y in structs_all[k][0]})
else: structs_all[k] = v
decls_by_file[f] = decls
tot = {"n": 0, "have": 0, "stub": 0, "size": 0, "size_have": 0, "simw": 0.0, "size_sim": 0}
for f in CPP_FILES:
rows = []
st = {"n": 0, "have": 0, "stub": 0, "size": 0, "size_have": 0, "simw": 0.0, "size_sim": 0}
for u in sorted(all_up[f], key=lambda u: u.line):
un = norm_cpp(u.body)
if u.excl:
rows.append((u.section, "[-]", "", u, None, u.excl)); continue
st["n"] += 1; st["size"] += len(un)
p = find(u)
if p is None:
rows.append((u.section, "[ ]", "", u, None, "")); continue
body = p.body
if p.alias: # ImGui.X = ImGui.Y / Factory(n): compare against the target's body when it is a plain reference
tgt = [x[1] for x in p.body if x[0] == "name"]
t = by_name.get(tgt[-1]) if tgt and all(x[1] == "." or x[0] == "name" for x in p.body) else None
t = [d for d in (t or []) if not d.alias]
body = t[0].body if t else None
if body is None: # alias of a Lua builtin / factory: ported, but no body to compare
st["have"] += 1; st["size_have"] += len(un)
rows.append((u.section, "[x]", " n/a", u, p, "= " + p.alias)); continue
pn = norm_lua(expand(p) if not p.alias else body)
sim = similarity(un, pn)
stub = is_stub(pn, un) and not p.alias
st["have"] += 1; st["size_have"] += len(un); st["simw"] += sim * len(un); st["size_sim"] += len(un)
if stub: st["stub"] += 1
rows.append((u.section, "[~]" if stub else "[x]", "%3.0f%%" % (100 * sim), u, p, "stub" if stub else ("= " + p.alias if p.alias else "")))
for k in tot: tot[k] += st[k]
summary.append((f, st))
report.append((f, rows))
def pct(a, b): return "%5.1f%%" % (100.0 * a / b) if b else " - "
# enums / fields / API declarations
enum_rows, enum_n, enum_have = [], 0, 0
for nm, (vals, line, sec) in sorted(enums_all.items(), key=lambda x: x[0]):
base = re.sub(r"(Private)?_$", "", nm)
missing, have, counted = [], 0, 0
for v, vline, excl in vals:
if excl or v.endswith("_COUNT") and False: continue
pre, _, key = v.partition("_")
ok = v in lua_globals or (pre, key) in enum_keys or (base, key) in enum_keys or (nm, v) in enum_keys or (base, v) in enum_keys
if not ok and not key: ok = v in lua_globals
enum_n += 1; counted += 1
if ok: have += 1; enum_have += 1
else: missing.append(v)
enum_rows.append((nm, have, counted, missing))
field_rows, field_n, field_have = [], 0, 0
for nm, (fl, line, sec) in sorted(structs_all.items()):
fl = [x for x in fl if not x[2]]
if not fl: continue
missing = [x[0] for x in fl if x[0] not in lua_fields]
field_n += len(fl); field_have += len(fl) - len(missing)
field_rows.append((nm, len(fl) - len(missing), len(fl), missing))
api_rows = []
port_names = set(by_name)
for f, label in (("imgui.h", "Public API (imgui.h, namespace ImGui)"), ("imgui_internal.h", "Internal API (imgui_internal.h, namespace ImGui)")):
d = {k: v for k, v in decls_by_file[f].items() if not v[1]}
have = [k for k in d if k in port_names]
api_rows.append((label, len(have), len(d), sorted((k for k in d if k not in port_names), key=lambda k: d[k][0]), d))
# ------------------------------------------------------------------ summary (stdout + file)
head = []
head.append("%-22s %9s %9s %9s %11s %9s" % ("upstream file", "funcs", "ported", "by size", "similarity", "stubs"))
for f, s in summary:
head.append("%-22s %9d %9s %9s %11s %9d" % (f, s["n"], pct(s["have"], s["n"]), pct(s["size_have"], s["size"]),
pct(s["simw"], s["size_sim"]), s["stub"]))
head.append("%-22s %9d %9s %9s %11s %9d" % ("TOTAL", tot["n"], pct(tot["have"], tot["n"]), pct(tot["size_have"], tot["size"]),
pct(tot["simw"], tot["size_sim"]), tot["stub"]))
head.append("")
head.append("upstream code reproduced (similarity x ported size / all upstream size): %s" % pct(tot["simw"] / max(1, tot["size_sim"]) * tot["size_have"], tot["size"]))
for label, h, n, _, _ in api_rows: head.append("%-52s %4d / %4d %s" % (label, h, n, pct(h, n)))
head.append("%-52s %4d / %4d %s" % ("Enum values (imgui.h + imgui_internal.h)", enum_have, enum_n, pct(enum_have, enum_n)))
head.append("%-52s %4d / %4d %s" % ("Struct fields (imgui.h + imgui_internal.h)", field_have, field_n, pct(field_have, field_n)))
print("\n".join(head))
L = []
L.append("PORT STATUS: imgui-luau vs upstream Dear ImGui (docking, %s)" % re.search(r'IMGUI_VERSION\s+"([^"]+)"', open(os.path.join(UP, "imgui.h")).read()).group(1))
L.append("Generated by tools/compare_upstream.py - do not edit. Upstream sources: tools/test/upstream/.")
L.append("")
L.append("Columns: funcs = upstream function definitions (overloads merged), excluding [-] rows")
L.append(" ported = share that has a port definition with the same name")
L.append(" by size = same, weighted by upstream body size (tokens)")
L.append(" similarity = normalized token similarity of the ported bodies (difflib ratio), weighted by size")
L.append(" stubs = ported but (nearly) empty in the port")
L.append("Name-based matching: a renamed port function shows as [ ], a same-named one counts even if it differs. Treat as estimates.")
L.append("Port-only helpers called from a ported function (same file) are inlined before comparing, since the port splits some big functions.")
L.append("Known low scores that are not missing code: data-driven port code (e.g. DemoWindowTables' section table), the older 1.92")
L.append("polyline stroker (AddPolyline), Lua-native replacements (ImGuiStorage, ImStr*, ImFormatString -> string.format).")
L.append("")
L += head
L.append("")
L.append("Legend: [x] ported (sim %) [~] stub [ ] not ported [-] excluded (c++-only / obsolete / os-specific / #if 0 / test-engine)")
for f, rows in report:
L.append("")
L.append("=" * 100)
L.append("%s" % f)
L.append("=" * 100)
cur = None
for sec, mark, sim, u, p, note in rows:
if sec != cur:
cur = sec; L.append(""); L.append(" -- %s" % (sec or "(top)"))
where = "%s:%d" % (p.file, p.line) if p else ""
L.append(" %s %5s %-58s %-32s %s" % (mark, sim, u.key, where, note))
L.append("")
L.append("=" * 100)
L.append("API declarations not ported (by upstream line)")
L.append("=" * 100)
for label, h, n, missing, d in api_rows:
L.append(""); L.append(" %s: %d / %d" % (label, h, n))
cur = None
for k in missing:
if d[k][2] != cur: cur = d[k][2]; L.append(" -- %s" % (cur or "(top)"))
L.append(" [ ] ImGui::%s" % k)
L.append("")
L.append("=" * 100)
L.append("Enums (values ported / total; missing listed)")
L.append("=" * 100)
for nm, h, n, missing in enum_rows:
if not n: continue
L.append(" %-40s %4d / %4d%s" % (nm, h, n, (" missing: " + ", ".join(missing)) if missing else ""))
L.append("")
L.append("=" * 100)
L.append("Struct fields (name found in the port / total; missing listed). Field names are matched globally, so common names may over-count.")
L.append("=" * 100)
for nm, h, n, missing in field_rows:
L.append(" %-40s %4d / %4d%s" % (nm, h, n, (" missing: " + ", ".join(missing)) if missing else ""))
out = os.path.join(ROOT, "docs", "PORT_STATUS.txt")
open(out, "w").write("\n".join(L) + "\n")
print("\nwrote docs/PORT_STATUS.txt")
if __name__ == "__main__":
main()
+135
View File
@@ -0,0 +1,135 @@
#!/usr/bin/env python3
# Typed entry module for the Roblox build: bundle.py writes build/ImGuiTyped.luau = Luau types + `require(script.Impl)`,
# where Impl is the untouched build/ImGui.luau (the bundle itself is too large for Luau's type inference to finish,
# so the types live in this small wrapper instead). Library sources and the LÖVE build are not affected.
# Public ImGui.* signatures come from upstream imgui.h (namespace ImGui), mapped onto the port's own parameter lists.
# Anything ambiguous (overloads, char* buffers, arrays, multiple out-params, count mismatch) falls back to `any`,
# so the types never reject code that runs. Enum tables (ImGuiWindowFlags, ImGuiCol, ...) get their exact keys.
# Self-check: python3 tools/gen_types.py (prints a few signatures)
import os, re
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
NUM = re.compile(r"^(int|float|double|unsigned int|unsigned short|short|size_t|ImU32|ImS32|ImU64|ImS64|ImWchar|ImGuiID|Im\w*Flags|ImGui(Col|Cond|StyleVar|Key|MouseButton|MouseCursor|Dir|DataType|SortDirection|TableBgTarget|Axis))$")
VEC = {"ImVec2": "ImVec2", "ImVec4": "ImVec4"}
def lua_type(ctype):
t = re.sub(r"\bconst\b|&", "", ctype).strip()
t = re.sub(r"\s+", " ", t)
if t == "char*": return "string" if "const" in ctype else None
if t == "bool": return "boolean"
if NUM.match(t): return "number"
return VEC.get(t)
def split_params(s):
out, depth, cur = [], 0, ""
for ch in s:
if ch in "([": depth += 1
if ch in ")]": depth -= 1
if ch == "," and depth == 0: out.append(cur); cur = ""
else: cur += ch
if cur.strip(): out.append(cur)
return [p.strip() for p in out]
def upstream_decls():
src = open(os.path.join(ROOT, "tools", "test", "upstream", "imgui.h")).read()
body = src[src.index("namespace ImGui\n"):src.index("} // namespace ImGui")]
decls = {}
for m in re.finditer(r"IMGUI_API\s+([\w\s\*&]+?)\s*\b(\w+)\((.*?)\)\s*(?:IM_FMT\w+\(\d+\))?\s*;", body):
decls.setdefault(m.group(2), []).append((m.group(1).strip(), split_params(m.group(3)) if m.group(3).strip() not in ("", "void") else []))
return decls
LOOSE = "(...any) -> ...any"
PORT_ONLY = { # port additions that aren't in imgui.h
"SetNextWindowCached": "(key: any) -> ()",
"NewTextBuffer": "(str: string, size: number?) -> {number}",
"TextBufferToString": "(buf: {number}) -> string",
}
def signature(port_params, ret, cparams):
if any(p == "..." for p in cparams):
if port_params and port_params[-1] == "...": port_params = port_params[:-1]
cparams = [p for p in cparams if p != "..."]; vararg = True
else: vararg = False
if len(port_params) != len(cparams): return LOOSE
args, outs = [], []
for name, cp in zip(port_params, cparams):
decl, _, default = cp.partition("=")
m = re.match(r"(.*?)\b(\w+)\s*(\[\w*\])?\s*$", decl.strip())
if not m: return LOOSE
ctype, array = m.group(1).strip(), m.group(3)
if "(*" in cp: return LOOSE
ptr = ctype.endswith("*") and not re.search(r"\bchar\s*\*$", ctype)
if array: t = None; outs.append("array")
elif ptr and re.search(r"\bconst\b", ctype): t = None # input array (e.g. ColorPicker4 ref_col)
elif ptr:
t = lua_type(ctype[:-1])
if t in ("boolean", "number"): outs.append(t)
else: t = None; outs.append("other")
else: t = lua_type(ctype)
if name == "...": args.append("...any"); continue
opt = "?" if (default.strip() or (ptr and t == "boolean")) else ""
args.append("%s: %s%s" % (name, t or "any", opt if t else ""))
if vararg: args.append("...any")
r = re.sub(r"\bconst\b|&|IMGUI_API", "", ret).strip()
if r == "void": rt = "...any" if outs else "()"
elif r == "bool":
scal = [o for o in outs if o != "array"]
if not scal: rt = "boolean"
elif len(scal) == 1 and scal[0] == "boolean": rt = "(boolean, boolean?)"
elif len(scal) == 1 and scal[0] == "number": rt = "(number, boolean)"
else: rt = "...any"
else:
rt = lua_type(r) or "any"
if outs: rt = "...any"
return "(%s) -> %s" % (", ".join(args), rt)
def port_functions(files):
fns = {}
for src in files.values():
for m in re.finditer(r"^function ImGui\.(\w+)\((.*?)\)", src, re.M):
fns[m.group(1)] = [p.strip() for p in m.group(2).split(",") if p.strip()]
return fns
def enums(files):
src = files.get("imgui_h.lua", "")
out = {}
for m in re.finditer(r"^(Im\w+) = \{\n(.*?)^\}", src, re.M | re.S):
keys = re.findall(r"^\s+(\w+)\s*=", m.group(2), re.M)
if keys: out[m.group(1)] = keys
for f in files.values(): # keys added later (ImGuiKey letters in loops are covered by the indexer below)
for name, key in re.findall(r"^\s*(Im\w+)\.(\w+)\s*=", f, re.M):
if name in out and key not in out[name]: out[name].append(key)
return out
def types_header(files):
decls, fns = upstream_decls(), port_functions(files)
lines = ["export type ImVec2 = { x: number, y: number, [any]: any }",
"export type ImVec4 = { x: number, y: number, z: number, w: number, [any]: any }",
"export type ImGuiAPI = {"]
for name in sorted(fns):
d = decls.get(name)
sig = PORT_ONLY.get(name) or (signature(fns[name], *d[0]) if d and len(d) == 1 else LOOSE)
lines.append(" %s: %s," % (name, sig))
lines += [" [string]: any,", "}"]
for name, keys in sorted(enums(files).items()):
lines.append("export type %s = { %s, [string]: number }" % (name, ", ".join("%s: number" % k for k in keys)))
return lines + module_type(set(enums(files)))
def wrapper(files):
return "\n".join(["--!strict", "-- GENERATED by tools/gen_types.py - do not edit. The library is the child ModuleScript 'Impl'.", ""]
+ types_header(files) + ["", "return (require(script:WaitForChild(\"Impl\")) :: any) :: ImGuiModule", ""])
def module_type(enum_names):
# the bundle is too large for Luau to infer, so its return value is cast to this explicit type
lines = ["export type ImGuiModule = {", " ImGui: ImGuiAPI,"]
lines += [" %s: (x: number?, y: number?, z: number?, w: number?) -> %s," % (n, n) for n in ("ImVec2", "ImVec4")]
lines += [" %s: %s," % (n, n) for n in sorted(enum_names)]
return lines + [" [string]: any,", "}"]
if __name__ == "__main__":
import glob
files = {os.path.basename(f): open(f).read() for f in glob.glob(os.path.join(ROOT, "*.lua"))}
lines = types_header(files)
for l in lines:
if re.match(r" (Begin|Checkbox|SliderFloat|SliderFloat3|Text|Button|Combo|GetCursorPos|InputText|DragFloatRange2):", l): print(l)
loose = sum(LOOSE in l for l in lines); print("%d functions, %d loose" % (sum(l.startswith(" ") for l in lines), loose))
+107
View File
@@ -0,0 +1,107 @@
#!/usr/bin/env python3
# Minifies a Lua/Luau file: drops comments, indentation, blank lines and spaces that don't separate tokens.
# Kept: strings (verbatim), "--!" directive lines (--!optimize, --!native, ...), @native attributes, and one newline
# wherever the source had one (so no ambiguous-call issues, and error line numbers stay roughly meaningful).
# python3 tools/minify.py IN OUT # write minified IN to OUT
# python3 tools/minify.py --same-code F.. # check F.. have the same code tokens as `git show HEAD:F` (comment-only edits)
# python3 tools/minify.py --same-code-as OLD NEW # same check between two files
# python3 tools/minify.py --test # self-check
import re, subprocess, sys
from functools import lru_cache
TOKEN = re.compile(r"""
(?P<ws>[ \t\r\n\f\v]+)
| (?P<lcom>--\[(?P<ceq>=*)\[.*?\](?P=ceq)\])
| (?P<com>--[^\n]*)
| (?P<lstr>\[(?P<seq>=*)\[.*?\](?P=seq)\])
| (?P<str>"(?:\\.|\\\n|[^"\\\n])*"|'(?:\\.|\\\n|[^'\\\n])*'|`(?:\\.|\\\n|[^`\\])*`)
| (?P<num>0[xX][0-9a-fA-F_]*(?:\.[0-9a-fA-F_]*)?(?:[pP][+-]?\d+)?[uUlLi]*|0[bB][01_]+|(?:\d[\d_]*(?:\.[\d_]*)?|\.\d[\d_]*)(?:[eE][+-]?\d+)?[uUlLi]*)
| (?P<name>@?[A-Za-z_]\w*)
| (?P<op>\.\.\.|\.\.=|//=|\.\.|==|~=|<=|>=|//|::|->|[-+*/%^]=|.)
""", re.S | re.X)
def tokenize(src):
"""[(kind, text, newline_before)]; comments/whitespace folded into newline_before ("--!" lines kept as 'dir')."""
out, nl, pos = [], False, 0
while pos < len(src):
m = TOKEN.match(src, pos)
kind, text = m.lastgroup, m.group(0)
if kind in ("ceq", "seq"): kind = "lcom" if text.startswith("--") else "lstr"
pos = m.end()
if kind == "ws" or kind == "lcom" or (kind == "com" and not text.startswith("--!")):
nl = nl or "\n" in text
continue
if kind == "com": kind = "dir"
if kind == "lstr" or kind == "str": kind = "str"
out.append((kind, text, nl))
nl = False
return out
@lru_cache(maxsize=None)
def glue_ok(a, b):
"""True if `a..b` with no space still tokenizes as exactly a, b."""
if re.match(r"\w", a[-1]) and re.match(r"\w", b[0]): return False # real lexers read `0then`/`x1` as one token
return [t for _, t, _ in tokenize(a + b)] == [a, b]
def minify(src):
toks, out = tokenize(src), []
prev = None
for kind, text, nl in toks:
if prev is not None:
if nl or prev[0] == "dir" or kind == "dir": out.append("\n")
elif kind == "str" or prev[0] == "str": # strings are self-delimiting; only `[` + `[[` would fuse
if kind == "str" and text[0] == "[" and prev[1].endswith("["): out.append(" ")
elif not glue_ok(prev[1], text):
out.append(" ")
out.append(text)
prev = (kind, text)
return "".join(out) + "\n"
def code(src):
return [(k, t) for k, t, _ in tokenize(src)]
def test():
cases = {
"local a = 1 -- c\nlocal b = a - -1\n": "local a=1\nlocal b=a- -1\n",
"x = 1 .. 2": "x=1 ..2\n",
"x = t[ [[s]] ]": "x=t[ [[s]]]\n",
"x = t[ [==[s]==] ]": "x=t[ [==[s]==]]\n",
"--!optimize 2\n--[[ long\ncomment ]] local s = 'a -- b' .. \"c\\\"d\"": "--!optimize 2\nlocal s='a -- b'..\"c\\\"d\"\n",
"@native\nfunction f(...) return ... end": "@native\nfunction f(...)return...end\n",
"a = b == c and d ~= e or f <= g": "a=b==c and d~=e or f<=g\n",
"x = 0x10 + 1e-5 + .5": "x=0x10+1e-5+.5\n",
"f(a)\n\n\n(g)(b)": "f(a)\n(g)(b)\n",
"s = [[a\n-- not a comment\n]]": "s=[[a\n-- not a comment\n]]\n",
"x = a / / b": "x=a/ /b\n",
"x = a . . b": "x=a. .b\n",
"if a == 0 then return 1 end": "if a==0 then return 1 end\n",
"x = 1 .. y": "x=1 ..y\n",
}
for src, want in cases.items():
got = minify(src)
assert got == want, (src, got, want)
assert code(minify(src)) == code(src), src
print("minify OK")
if __name__ == "__main__":
if sys.argv[1] == "--test":
test()
elif sys.argv[1] in ("--same-code", "--same-code-as"):
bad = 0
pairs = [(sys.argv[2], sys.argv[3])] if sys.argv[1] == "--same-code-as" else [(None, f) for f in sys.argv[2:]]
for ref, f in pairs:
old = open(ref).read() if ref else subprocess.run(["git", "show", "HEAD:" + f], capture_output=True, text=True, check=True).stdout
a, b = code(old), code(open(f).read())
if a != b:
bad += 1
i = next((i for i, (x, y) in enumerate(zip(a, b)) if x != y), min(len(a), len(b)))
print("%s: CODE CHANGED at token %d: %r -> %r" % (f, i, a[i:i + 6], b[i:i + 6]))
else:
print("%s: same code" % f)
sys.exit(1 if bad else 0)
else:
src = open(sys.argv[1], encoding="utf-8").read()
dst = minify(src)
assert code(dst) == code(src), "minify changed code tokens"
open(sys.argv[2], "w", encoding="utf-8").write(dst)
print("wrote %s (%d -> %d bytes, %.0f%%)" % (sys.argv[2], len(src.encode()), len(dst.encode()), 100.0 * len(dst) / len(src)))
+3 -1
View File
@@ -16,6 +16,8 @@ G = "game, workspace, Instance, Enum, Vector2, Vector3, UDim2, Color3, Content,
def build_run(args, flags): def build_run(args, flags):
subprocess.run([sys.executable, os.path.join(ROOT, "tools", "bundle.py"), TMP] + (["--no-demo"] if os.environ.get("NO_DEMO") else []), check=True, stdout=subprocess.DEVNULL) subprocess.run([sys.executable, os.path.join(ROOT, "tools", "bundle.py"), TMP] + (["--no-demo"] if os.environ.get("NO_DEMO") else []), check=True, stdout=subprocess.DEVNULL)
if os.environ.get("MINIFY"): # test the minified bundle instead (error line numbers won't map back to sources)
subprocess.run([sys.executable, os.path.join(ROOT, "tools", "minify.py"), os.path.join(TMP, "ImGui.luau"), os.path.join(TMP, "ImGui.luau")], check=True, stdout=subprocess.DEVNULL)
out = ["--" if args.roblox else "--!native", "--!optimize 2", flags, "local SOURCE_FNS = {}"] out = ["--" if args.roblox else "--!native", "--!optimize 2", flags, "local SOURCE_FNS = {}"]
def add(key, path): def add(key, path):
s = "\n".join(l for l in open(path).read().split("\n") if not l.startswith("--!")) s = "\n".join(l for l in open(path).read().split("\n") if not l.startswith("--!"))
@@ -66,7 +68,7 @@ def main():
ap.add_argument("--quiet", action="store_true", help="no random input (scripted tests drive io themselves)") ap.add_argument("--quiet", action="store_true", help="no random input (scripted tests drive io themselves)")
args = ap.parse_args() args = ap.parse_args()
code, out = run(args, "") code, out = run(args, os.environ.get("RUN_FLAGS", ""))
print(out if os.environ.get("FULL_OUTPUT") else out[-4000:]) print(out if os.environ.get("FULL_OUTPUT") else out[-4000:])
sys.exit(code) sys.exit(code)
+58
View File
@@ -0,0 +1,58 @@
-- SetNextWindowCached: the same scripted scenario runs twice, window "Win##A" uncached then "Win##B" cached.
-- Every frame the window's draw list must hash identically in both phases, while B's body runs far less often.
-- run: python3 tools/test/run.py --main tools/test/scripts/t_cached_window.luau --frames 130 --quiet --roblox
local G = require(game:GetService("ReplicatedStorage"):WaitForChild("ImGui"))
local ImGui, ImVec2 = G.ImGui, G.ImVec2
ImGui.CreateContext()
G.ImGui_ImplRoblox.Init()
G.ImGui_ImplRoblox.SetRenderRate(0)
local io = ImGui.GetIO()
local P = 60 -- frames per phase
local frame, hashes, runs = 0, {}, { A = 0, B = 0 }
local function hash(dl)
local h = 0
local function mix(v) h = (h * 31 + math.floor(v * 64 + 0.5)) % 2147483647 end
local vb, id = dl.VtxBuffer.Buf, dl.IdxBuffer.Data
for i = 1, dl.VtxBuffer.Size do local o = (i - 1) * 36; mix(buffer.readf64(vb, o)); mix(buffer.readf64(vb, o + 8)); mix(buffer.readf64(vb, o + 16) * 4096); mix(buffer.readf64(vb, o + 24) * 4096); mix(buffer.readu32(vb, o + 32) / 64) end
for i = 1, dl.IdxBuffer.Size do mix(id[i]) end
for _, c in dl.CmdBuffer:iter() do mix(c.ClipRect.x); mix(c.ClipRect.y); mix(c.ClipRect.z); mix(c.ClipRect.w); mix(c.ElemCount) end
return h
end
game:GetService("RunService").Stepped:Connect(function()
frame += 1
if frame > 2 * P then return end
local phase = frame <= P and "A" or "B"
local t = (frame - 1) % P + 1 -- frame within the phase
G.ImGui_ImplRoblox.NewFrame()
io.DeltaTime = 1 / 60
-- mouse: away, over the button (hover highlight), away again
if t >= 20 and t < 26 then io:AddMousePosEvent(60, 75) else io:AddMousePosEvent(700, 500) end
local key = math.floor(t / 15) -- data version: changes at t = 15, 30, 45
ImGui.NewFrame()
if t == 3 then ImGui.SetWindowFocus(nil) end -- focused -> unfocused title bar must not stay stale
if t == 1 then ImGui.SetNextWindowFocus() end
ImGui.SetNextWindowPos(ImVec2(20, 20), G.ImGuiCond.Always)
ImGui.SetNextWindowSize(ImVec2(220, 140), G.ImGuiCond.Always)
if phase == "B" then ImGui.SetNextWindowCached(key) end
local _, visible = ImGui.Begin("Win##" .. phase)
if visible then
runs[phase] += 1
ImGui.Button("Button")
ImGui.Text("value %d", key)
ImGui.SliderFloat("slider", key * 0.1, 0, 1)
end
ImGui.End()
ImGui.Render()
local h = hash(ImGui.FindWindowByName("Win##" .. phase).DrawList)
if phase == "A" then hashes[t] = h
elseif h ~= hashes[t] then error(("frame %d: cached draw list differs from uncached"):format(t)) end
G.ImGui_ImplRoblox.RenderDrawData(ImGui.GetDrawData())
if frame == 2 * P then
print(("runs: uncached %d, cached %d"):format(runs.A, runs.B))
assert(runs.A == P, "uncached window must run every frame")
assert(runs.B < P / 3, "cached window ran too often")
print("CACHED OK")
end
end)
+68
View File
@@ -0,0 +1,68 @@
-- Docked windows (floating split node) stop being submitted: the host lingers one frame, its bodies must be filled
-- with the windows' bg colour (node.LastBgColor), not white, and everything must be gone within a few frames.
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local G = require(ReplicatedStorage:WaitForChild("ImGui"))
local ImGui, ImVec2 = G.ImGui, G.ImVec2
ImGui.CreateContext()
local io = ImGui.GetIO()
io.ConfigFlags = bit32.bor(io.ConfigFlags, G.ImGuiConfigFlags.DockingEnable)
G.ImGui_ImplRoblox.Init()
local frame, closed_at, done = 0, 31, false
RunService.Stepped:Connect(function()
if done then return end
frame += 1
G.ImGui_ImplRoblox.NewFrame()
ImGui.NewFrame()
if frame == 2 then
local id = ImGui.GetID("floating")
ImGui.DockBuilderRemoveNode(id)
ImGui.DockBuilderAddNode(id, 0)
ImGui.DockBuilderSetNodePos(id, ImVec2(100, 100))
ImGui.DockBuilderSetNodeSize(id, ImVec2(600, 400))
local left, right = ImGui.DockBuilderSplitNode(id, G.ImGuiDir.Left, 0.5)
ImGui.DockBuilderDockWindow("A", left)
ImGui.DockBuilderDockWindow("B", right)
ImGui.DockBuilderFinish(id)
end
if frame < closed_at then
for _, n in { "A", "B" } do
if ImGui.Begin(n) then ImGui.Text("window " .. n) end
ImGui.End()
end
end
if frame == closed_at - 1 then
local a, b = ImGui.FindWindowByName("A"), ImGui.FindWindowByName("B")
assert(a.DockNode and b.DockNode and a.DockNode ~= b.DockNode and a.DockNode.HostWindow, "A, B docked side by side")
end
ImGui.Render()
local dd = ImGui.GetDrawData()
if frame >= closed_at then
for _, dl in dd.CmdLists:iter() do
-- white vertices spanning a big area = a white body fill (white text glyphs are small)
local vb, x0, y0, x1, y1 = dl.VtxBuffer.Buf, math.huge, math.huge, -math.huge, -math.huge
for _, pcmd in dl.CmdBuffer:iter() do
for i = pcmd.IdxOffset + 1, pcmd.IdxOffset + pcmd.ElemCount do
local o = (pcmd.VtxOffset + dl.IdxBuffer.Data[i] - 1) * G.VTX_STRIDE
if buffer.readu32(vb, o + 32) == 0xFFFFFFFF then
local x, y = buffer.readf64(vb, o), buffer.readf64(vb, o + 8)
x0, y0, x1, y1 = math.min(x0, x), math.min(y0, y), math.max(x1, x), math.max(y1, y)
end
end
end
assert(not (x1 - x0 > 100 and y1 - y0 > 100), "white fill on frame " .. frame)
end
if dd.TotalVtxCount == 0 then
G.ImGui_ImplRoblox.RenderDrawData(dd)
for _, set in io.BackendPlatformUserData.Layers do
for kind = 1, 3 do assert(not (set[kind] and set[kind].Visible), "backend layer still visible after close") end
end
print("DOCK CLOSE OK after", frame - closed_at + 1, "frames")
done = true
return
else
assert(frame < closed_at + 3, "docked windows still drawn " .. (frame - closed_at + 1) .. " frames after close")
end
end
G.ImGui_ImplRoblox.RenderDrawData(dd)
end)
+57
View File
@@ -0,0 +1,57 @@
-- Regressions vs upstream: clipboard paste filter (InputTextEx), ImParseFormatTrimDecorations (Ctrl+click temp input), ShrinkWidths tie order
local G = require(game:GetService("ReplicatedStorage"):WaitForChild("ImGui"))
local ImGui, ImVec2 = G.ImGui, G.ImVec2
ImGui.CreateContext(); G.ImGui_ImplRoblox.Init()
local io = ImGui.GetIO()
local failed = {}
local function check(ok, msg) if not ok then table.insert(failed, msg); print("FAIL " .. msg) end end
-- ShrinkWidths: equal widths sort by Index descending (upstream ShrinkWidthItemComparer), so the rounding remainder goes to the highest Index first
local items = {}
for i = 1, 4 do local it = G.ImGuiShrinkWidthItem(); it.Index = i - 1; it.Width = 100; it.InitialWidth = 100; items[i] = it end
ImGui.ShrinkWidths(items, 1, 4, 2.0, 10.0)
local w = {}
for _, it in items do w[it.Index] = it.Width end
check(w[3] == 100 and w[2] == 100 and w[1] == 99 and w[0] == 99, ("ShrinkWidths widths %s %s %s %s"):format(w[0], w[1], w[2], w[3]))
local function edit_text()
local st = ImGui.GetCurrentContext().InputTextState
return G.ImGui._ByteArrayToString(st.TextA.Data, 1, st.TextLen + 1)
end
local buf = ImGui.NewTextBuffer("", 64)
local f, sf, di = 0, 45.0, 50
local rects = {}
local function rect() local a, b = ImGui.GetItemRectMin(), ImGui.GetItemRectMax(); return { ImVec2(a.x, a.y), ImVec2(b.x, b.y) } end -- copies: GetItemRectMin() returns the live LastItemData.Rect.Min
local function click(name, ctrl)
local r = rects[name]
io:AddMousePosEvent((r[1].x + r[2].x) / 2, (r[1].y + r[2].y) / 2)
if ctrl then io:AddKeyEvent(G.ImGuiMod_Ctrl, true) end
io:AddMouseButtonEvent(0, true)
end
local function release() io:AddMouseButtonEvent(0, false); io:AddKeyEvent(G.ImGuiMod_Ctrl, false) end
local function key(k) io:AddKeyEvent(k, true); io:AddKeyEvent(k, false) end
game:GetService("RunService").Stepped:Connect(function()
f += 1
G.ImGui_ImplRoblox.NewFrame()
if f == 3 then click("t") elseif f == 4 then release() end
-- clipboard: tab is filtered out (single line, no AllowTabInput), é is 2 bytes
if f == 6 then ImGui.SetClipboardText("ab\tc\u{e9}"); io:AddKeyEvent(G.ImGuiMod_Ctrl, true); io:AddKeyEvent(G.ImGuiKey.V, true) end
if f == 7 then io:AddKeyEvent(G.ImGuiKey.V, false); io:AddKeyEvent(G.ImGuiMod_Ctrl, false) end
if f == 9 then key(G.ImGuiKey.Escape) end
if f == 11 then click("s", true) elseif f == 12 then release() end
if f == 14 then key(G.ImGuiKey.Escape) end
if f == 16 then click("d", true) elseif f == 17 then release() end
ImGui.NewFrame()
ImGui.SetNextWindowPos(ImVec2(10, 10)); ImGui.SetNextWindowSize(ImVec2(300, 200))
ImGui.Begin("T")
ImGui.InputText("##t", buf, 64); rects.t = rect()
sf = ImGui.SliderFloat("##s", sf, 0.0, 100.0, "%.0f deg"); rects.s = rect()
di = ImGui.DragInt("##d", di, 1, 0, 100, "%d%%"); rects.d = rect()
ImGui.End()
ImGui.Render(); G.ImGui_ImplRoblox.RenderDrawData(ImGui.GetDrawData())
if f == 8 then check(edit_text() == "abc\u{e9}", "paste: '" .. edit_text() .. "'") end
if f == 13 then check(edit_text() == "45", "SliderFloat temp input: '" .. edit_text() .. "'") end
if f == 18 then check(edit_text() == "50", "DragInt temp input: '" .. edit_text() .. "'") end
if f == 20 and #failed == 0 then print("FIXES OK") end
end)
+133
View File
@@ -0,0 +1,133 @@
-- Backend pixel oracle: composites the ScreenGui (like t_composite) at checkpoints and prints "PIX <frame> <hash>".
-- Phases: resize (bottom-right, then top-left), move, scroll, hover, typing, new font sizes (glyph baking), colour drag.
-- Each phase ends with a few still frames, then a checkpoint. Run with RUN_FLAGS="ARG_FORCE=true" for a full redraw
-- every frame: incremental output must match it exactly (and match the previous backend version).
local G = require(game:GetService("ReplicatedStorage"):WaitForChild("ImGui"))
local ImGui, ImVec2, ImVec4 = G.ImGui, G.ImVec2, G.ImVec4
ImGui.CreateContext()
G.ImGui_ImplRoblox.Init()
G.ImGui_ImplRoblox.SetRenderRate(0)
if ARG_TILELIMIT then ImGui.GetIO().BackendPlatformUserData.TileLimit = ARG_TILELIMIT end
local io = ImGui.GetIO()
local W, H = 1280, 760
local col = ImVec4(0.4, 0.7, 0.2, 0.5)
local buf = ImGui.NewTextBuffer("type here", 128)
local slider = 0.3
local function composite(gui)
local screen = buffer.create(W * H * 3)
local function walk(inst, ox, oy, kx0, ky0, kx1, ky1)
local kids = inst:GetChildren()
table.sort(kids, function(a, b) return (a.ZIndex or 1) < (b.ZIndex or 1) end)
for _, c in ipairs(kids) do
if c.Visible ~= false then
local x, y = ox + (c.Position and c.Position.X or 0), oy + (c.Position and c.Position.Y or 0)
local cx0, cy0, cx1, cy1 = kx0, ky0, kx1, ky1
if c.ClipsDescendants then
cx0, cy0 = math.max(cx0, x), math.max(cy0, y)
cx1, cy1 = math.min(cx1, x + c.Size.X), math.min(cy1, y + c.Size.Y)
end
if c.ClassName == "ImageLabel" then
local img = c.ImageContent.Object
local px, sz = img.Pixels, img.Size.X
for ty = 0, sz - 1 do
local sy = y + ty
if sy >= cy0 and sy < cy1 then
for tx = 0, sz - 1 do
local sx = x + tx
if sx >= cx0 and sx < cx1 then
local o = (ty * sz + tx) * 4
local a = buffer.readu8(px, o + 3)
if a > 0 then
local d = (sy * W + sx) * 3
local fa = a / 255
for ch = 0, 2 do
local v = buffer.readu8(px, o + ch) * fa + buffer.readu8(screen, d + ch) * (1 - fa)
buffer.writeu8(screen, d + ch, math.floor(v + 0.5))
end
end
end
end
end
end
end
walk(c, x, y, cx0, cy0, cx1, cy1)
end
end
end
walk(gui, 0, 0, 0, 0, W, H)
local h = 0
for i = 0, W * H * 3 - 4, 4 do h = (h * 31 + buffer.readu32(screen, i)) % 2147483647 end
return h
end
-- phase list: name, frames of action (then 6 still frames + checkpoint)
local PHASES = { { "start", 4 }, { "resize_br", 24 }, { "resize_tl", 24 }, { "move", 24 }, { "scroll", 30 }, { "hover", 30 },
{ "type", 24 }, { "fonts", 24 }, { "color", 20 } }
local STILL = ARG_STILL or (ARG_TILELIMIT and 40 or 6) -- tile limit: deferred tiles need more frames to settle
local PT = {}
local pi, pf = 1, 0
local pos, size, scroll, fsize = ImVec2(40, 40), ImVec2(460, 520), 0, 13
local frame = 0
local typing = "Hello, glyphs! 0123 xyz"
game:GetService("RunService").Stepped:Connect(function()
local ph = PHASES[pi]
if not ph then return end
frame += 1; pf += 1
local name, n = ph[1], ph[2]
local act = pf <= n
G.ImGui_ImplRoblox.NewFrame()
io.DeltaTime = 1 / 60
io:AddMousePosEvent(1200, 740)
if act then
if name == "resize_br" then size = ImVec2(460 + pf * 7, 520 + pf * 5)
elseif name == "resize_tl" then pos = ImVec2(40 + pf * 3, 40 + pf * 2); size = ImVec2(size.x - 3, size.y - 2)
elseif name == "move" then pos = ImVec2(pos.x + 5, pos.y + 3)
elseif name == "scroll" then scroll += 9
elseif name == "hover" then io:AddMousePosEvent(pos.x + 30 + (pf * 17) % 300, pos.y + 40 + (pf * 23) % 400)
elseif name == "type" then
if pf == 1 then io:AddMousePosEvent(pos.x + 120, pos.y + 37); io:AddMouseButtonEvent(0, true)
elseif pf == 2 then io:AddMouseButtonEvent(0, false)
else io:AddInputCharacter(string.byte(typing, (pf - 3) % #typing + 1)) end
elseif name == "fonts" then fsize = 13 + pf * 0.75
elseif name == "color" then
local p = ImVec2(700 + pf * 4, 120 + pf * 3)
io:AddMousePosEvent(p.x, p.y); io:AddMouseButtonEvent(0, pf < n)
end
end
ImGui.NewFrame()
ImGui.SetNextWindowPos(pos, G.ImGuiCond.Always); ImGui.SetNextWindowSize(size, G.ImGuiCond.Always)
ImGui.Begin("Stress")
ImGui.InputText("##in", buf, 128)
if name == "scroll" or scroll > 0 then ImGui.SetScrollY(scroll) end
for i = 1, 12 do ImGui.Button("Button " .. i); ImGui.SameLine(); slider = ImGui.SliderFloat("##s" .. i, slider, 0, 1) end
if ImGui.BeginTable("tbl", 4, bit32.bor(G.ImGuiTableFlags.Borders, G.ImGuiTableFlags.RowBg)) then
for r = 1, 30 do ImGui.TableNextRow(); for c = 1, 4 do ImGui.TableNextColumn(); ImGui.Text("r%d c%d", r, c) end end
ImGui.EndTable()
end
ImGui.PushFont(nil, fsize); ImGui.Text("Font size %.2f AaBbCc", fsize); ImGui.PopFont()
for i = 1, 30 do ImGui.TextWrapped("Line %d: the quick brown fox jumps over the lazy dog.", i) end
ImGui.End()
ImGui.SetNextWindowPos(ImVec2(660, 60), G.ImGuiCond.Always); ImGui.SetNextWindowSize(ImVec2(500, 620), G.ImGuiCond.Always)
ImGui.Begin("Pickers")
ImGui.ColorPicker4("bar", col, G.ImGuiColorEditFlags.PickerHueBar)
ImGui.End()
ImGui.Render()
local clk = os.realclock or os.clock
local t0 = clk()
G.ImGui_ImplRoblox.RenderDrawData(ImGui.GetDrawData())
if act then PT[name] = (PT[name] or 0) + (clk() - t0) * 1000 end
if pf == n then -- mid-action (not settled): only exact without a redraw budget
print("PIXA", name, frame, composite(game:GetService("Players").LocalPlayer.PlayerGui:FindFirstChild("ImGui")))
end
if pf == n + STILL then
local gui = game:GetService("Players").LocalPlayer.PlayerGui:FindFirstChild("ImGui")
print("PIX", name, frame, composite(gui))
pi += 1; pf = 0
if not PHASES[pi] then
for _, q in PHASES do print(string.format("PIXT %-10s %7.2f ms/frame", q[1], PT[q[1]] / q[2])) end
print("PIXELS DONE")
end
end
end)
+13 -1
View File
@@ -124,12 +124,24 @@ local function MyTool()
end end
-- [/snippet] -- [/snippet]
-- [snippet:cached]
local stats = { version = 0, kills = 0 }
local function StatsWindow()
ImGui.SetNextWindowCached(stats.version) -- bump stats.version whenever stats change
local _, visible = ImGui.Begin("Stats")
if visible then -- false while the cached contents are reused
ImGui.Text("Kills: %d", stats.kills)
end
ImGui.End()
end
-- [/snippet]
local frame = 0 local frame = 0
RunService.Stepped:Connect(function() RunService.Stepped:Connect(function()
frame += 1 frame += 1
G.ImGui_ImplRoblox.NewFrame() G.ImGui_ImplRoblox.NewFrame()
ImGui.NewFrame() ImGui.NewFrame()
MyFirstTool(); MyTool(); Conventions(); MenusAndTable(); Plot(); CustomDrawing() MyFirstTool(); MyTool(); Conventions(); MenusAndTable(); Plot(); CustomDrawing(); StatsWindow()
ImGui.Render() ImGui.Render()
G.ImGui_ImplRoblox.RenderDrawData(ImGui.GetDrawData()) G.ImGui_ImplRoblox.RenderDrawData(ImGui.GetDrawData())
if frame == 20 then if frame == 20 then
+1 -1
View File
@@ -2,7 +2,7 @@
local G = require(game:GetService("ReplicatedStorage"):WaitForChild("ImGui")) local G = require(game:GetService("ReplicatedStorage"):WaitForChild("ImGui"))
local ImGui, ImVec2 = G.ImGui, G.ImVec2 local ImGui, ImVec2 = G.ImGui, G.ImVec2
local UIS = game:GetService("UserInputService") local UIS = game:GetService("UserInputService")
ImGui.CreateContext(); G.ImGui_ImplRoblox.Init() ImGui.CreateContext(); G.ImGui_ImplRoblox.Init(); G.ImGui_ImplRoblox.SetTextProxy(true)
local io = ImGui.GetIO() local io = ImGui.GetIO()
local buf = ImGui.NewTextBuffer("", 64) local buf = ImGui.NewTextBuffer("", 64)
local proxy local proxy
+10
View File
@@ -0,0 +1,10 @@
[package]
name = "4dbug/imgui-luau"
version = "0.2.0"
registry = "https://github.com/UpliftGames/wally-index"
realm = "shared"
license = "MIT"
description = "Dear ImGui (docking branch) ported to Luau for Roblox"
repository = "https://github.com/4DBug/imgui-luau"
[dependencies]