41 Commits
Author SHA1 Message Date
4DBug c1696c799f 0.3.0
Build & Release / build (push) Waiting to run
2026-10-15 11:04:45 -05:00
4DBug 447230e688 CI/releases: drop the nodemo bundles (--no-demo build flag kept); remove examples/example_shared; Roblox backend RenderRate defaults to 60 (0 = every frame); README + TODO.txt updated 2026-10-15 10:57:00 -05:00
4DBug a1aa5cbb2d Fix: long TextUnformatted (>2000 bytes) rendered each visible line to the end of the text (RenderText end was nil, upstream passes line_end); PushFont() saved the new font instead of the current one, so PopFont() never restored it (font preview leaked into all later text); t_fixes covers both 2026-10-15 10:54:56 -05:00
4DBug dad1a0c0f1 Port leftovers: BringWindowToDisplayBehind, ImGuiSizeCallbackData + CalcWindowSizeAfterConstraint, ErrorCheckUsingSetCursorPosToExtendParentBoundaries, PopFocusScope user error (+ SetCurrentWindow stack sizes fix), log text decorations (Button/Menu/TreeNode/Drag/Slider/InputText/Checkbox/Radio), DataTypeFormatString display, ImGuiDataType S64/U64 (upstream enum layout; doubles), sscanf %u/length modifiers, mouse arrays sized ImGuiMouseButton.COUNT, AddCallback userdata copy; TODO.txt refreshed 2026-10-15 10:51:20 -05:00
4DBug b71e62b4d9 Integration: debug/context fields in ImGuiContext ctor, ImGuiContextHookType, DebugFlashStyleColorIdx=COUNT, CurrentPixelDensity, clipper range/data helpers, full Shutdown cleanup, settings dates (ImGuiSettingsCleanupArgs, CleanupIniSettings, LastUsed=, Platform_SessionDate) + metrics Settings section, ImGuiStyleVar SeparatorSize/DockingSeparatorSize (fixes off-by-one style var table), LogRenderedText restored in RenderText*, combo preview 1.93, InputScalar ValidatedThisFrame, ImGuiListClipperFlags/LogFlags/OldColumnFlags, ImGuiKey digit aliases, backend names 2026-10-15 09:39:17 -05:00
4DBug 92dbd9a580 compare_upstream: skip IM_MSVC_RUNTIME_CHECKS_*/template<> when naming structs; [=] status for intentional Lua replacements (ImGuiStorage, ImStr*/ImFormatString*, allocators, C++ container internals, sizeof/ABI helpers...) with evidence regexes, reported separately 2026-10-15 09:39:17 -05:00
4DBug e3840a938a Port debug tools (item picker, flash style color, debug locate, DebugDraw*, keyboard preview, debug break, DrawCmd mesh view), context hooks, SetContextName, AddInputCharactersUTF8, sanity checks, window resize ids, Register/UnregisterUserTexture, SetPixelDensity, TestShortcutRouting, DemoMarker; InputTextState selection API (fixes undo crash, callbacks get 0-based positions), ShortcutsForCancel (Esc/right-click reverts drags/sliders), ImParseFormatSanitizeForPrinting, ImDrawList CloneOutput/PrimQuadUV/AddImageQuad, ImDrawData DeIndexAllBuffers/ScaleClipRects, internal helpers (ImBitVector, ImGuiPackedDate, inline ImGui getters...); t_debugtools, t_helpers 2026-10-15 08:52:30 -05:00
4DBug 9be05aeb5c Fonts: ImFontGlyphRangesBuilder + GetGlyphRangesDefault, ImFont:CalcWordWrapPosition, stb_decompress + Decode85 + AddFontFromMemoryCompressed(Base85)TTF, atlas helpers (RemoveFont, RemoveCustomRect, AddRemapCodepoint (remap never triggered before), IsGlyphRangeUnused, texture status/format names...); t_fonts 2026-10-15 08:05:51 -05:00
4DBug f2c694bac5 Port ImTriangulator + upstream AddConcavePolyFilled (AA fringe, no per-triangle seams); [Luau] handles CCW winding and duplicate/collinear points that upstream mis-triangulates 2026-10-15 07:43:21 -05:00
4DBug 93a3c4faf5 Library draw call sites match 1.93: AddLineH/AddLineV + StrokeCenterBiased/StrokeInside (window/menu borders, separators, text cursor), tab border PathStroke, check mark AALineEnds, CloseButton/TreeNode lines/ColorButton/alpha checkerboard 1.93, Ctrl+Tab highlight AddRect arg order, metrics AA push/pop 2026-10-15 07:24:13 -05:00
4DBug 9872d43fd6 Draw list 1.93: baked round corners in the font atlas (ImFontAtlasBuildUpdateTexDataCorners, fill radii 1..16 + 3 stroke thicknesses) used by AddRect/AddRectFilled when pixel-aligned (_AddRectBaked/_AddRectFilledBaked) 2026-10-15 06:55:57 -05:00
4DBug af2026113a Draw list 1.93 strokes: _AddPolyline ported (texture AA lines, joins/miter/bevel, CapSquare, AALineEnds, Stroke Inside/Center/CenterBiased/Outside/Legacy), AddConvexPolyFilled AAFill, all primitives with 1.93 signatures/flags, PathRect/PathArcTo 1.93, baked line textures; Custom Rendering demo flags UI restored. check_all: t_pixels incremental==full redraw, headless screenshot (tools/test/shot.py) 2026-10-15 06:37:49 -05:00
4DBug a111187591 Draw list 1.93 foundation (no output change): ImDrawFlags 1.93 values (Round*/Stroke*/AA*/JoinMiter/CapSquare/TextNoPixelSnap/UseTexForRoundCorners...; RoundCorners* kept as obsolete aliases), ImDrawList.Flags + PushDrawFlag/PopDrawFlag/_SetPixelDensity, SetupDrawListSharedData 1.93, style.CurveTessellationMaxError/AntiAliasedLineEnds, _AddPolyline entry point 2026-10-15 05:13:58 -05:00
4DBug 1e63710e33 compare_upstream: match XxxMT:Method and Class_Method globals to C++ class members (demo 100% ported by name) 2026-10-15 04:35:23 -05:00
4DBug d18bc714fc Demo: Selection State & Multi-Select section (ExampleSelectionWithDeletion, ExampleDualListBox, all 11 sub-demos) and Assets Browser ported; fix ImFloor() for negative non-integers (was floor-1) 2026-10-15 04:13:40 -05:00
4DBug 049e160852 Port multi-select, box-select and typing-select (BeginMultiSelect/EndMultiSelect, SetNextItemSelectionUserData, MultiSelectItemHeader/Footer, BeginBoxSelect/EndBoxSelect, GetTypingSelectRequest/TypingSelectFind*, ImGuiSelectionBasicStorage/ExternalStorage, Metrics nodes); wired into Selectable/Checkbox/TreeNode/clipper/tables; t_multiselect 2026-10-15 03:49:53 -05:00
4DBug 33ab5a1d84 Fix: InputTextMultiline crashed when drawn (InputTextLineIndexBuild/line offsets 0-based like upstream, inverted buf_display_end), InputText LiveEdit flag check; Shortcut()/IsKeyChordPressed() upstream defaults; FrameCount starts at 0 (IsWindowAppearing on first frame); missing ImGuiIO config fields with upstream defaults (+ ConfigDebugBeginReturnValueOnce/Loop); ColorButton drag source + ColorEdit4 drop target, IMGUI_PAYLOAD_TYPE_COLOR_3F/4F; t_fixes covers them 2026-10-15 03:03:10 -05:00
4DBug 9c8c1b018e Demo: re-ported imgui_demo.cpp line by line in upstream order (one function per upstream function, same labels/HelpMarkers/comments, 176/186 IMGUI_DEMO_MARKERs; port table driver/Sub()/Demo* helpers removed, statics in S.<Function>). New: DemoWindowInputs (Inputs & Focus), Image Viewer, ShowExampleAppDockSpaceBasic. Selection/Assets Browser stubbed until multi-select is ported. ImColor helper (imgui_h.lua). PORT_STATUS demo similarity 29% -> 51% 2026-10-15 02:32:58 -05:00
4DBug b929de5206 Fix: ImDrawList:PushClipRectFullScreen() was missing (drop target outside the window clip rect crashed); CalcItemSize() no longer writes to the caller's size (by value like upstream; fixes BeginTable UserOuterSize), scalar _CalcItemSizeXY for internal callers; t_fixes covers both 2026-10-15 02:05:43 -05:00
4DBug ac4398ee2a tools/test: check_all.sh (CI mirror + all scripted tests + demo forced open + LÖVE), hashes.sh (draw checksums vs a git rev), t_libhash (library-only draw checksum); regenerate PORT_STATUS 2026-10-15 02:03:40 -05:00
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) Canceled after 0s
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) Canceled after 0s
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) Canceled after 0s
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
56 changed files with 99254 additions and 39387 deletions
+43 -28
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.
| [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) |
| [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).
### 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
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)
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`.
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.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 `60`; `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.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. |
@@ -182,6 +164,27 @@ io.Fonts:AddFontDefault()
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. Redraws are capped at 60 per second by default (`SetRenderRate(hz)`, `0` = uncapped), optionally lower while busy with `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
| Backend | File | Build | Renderer | Example |
@@ -202,12 +205,10 @@ love examples/example_love
Roblox stays the primary target: the Roblox build is untouched by this (no shims, `@native` kept); the LÖVE build strips `@native` and adds small `bit32`/`buffer`/`utf8`/`table` shims (the COMPAT section of [backends/imgui_impl_love.lua](/backends/imgui_impl_love.lua)). `tools/test/love/run.sh` checks that LuaJIT produces exactly the same draw data as Luau.
[examples/example_shared/app.lua](/examples/example_shared/app.lua) is one UI that runs unchanged on both: `roblox.client.luau` (Rojo: `examples/example_shared/default.project.json`) and `main.lua` (`love examples/example_shared`) are the only platform code. Write shared UI code in plain Lua 5.1 syntax (no `+=`, `//`, `continue`).
#### Prebuilt releases
Every push to `master` publishes a rolling `latest` release, and every `v*` tag a versioned release ([.github/workflows/release.yml](/.github/workflows/release.yml)), with:
`ImGui.luau` / `ImGui-nodemo.luau` (Roblox ModuleScript), `imgui_love.lua` / `imgui_love-nodemo.lua` (LÖVE), and `imgui-luau-roblox-*.zip` / `imgui-luau-love-*.zip` (bundle + examples).
`ImGui.luau` (Roblox ModuleScript), `imgui_love.lua` (LÖVE), their minified `.min` variants, and `imgui-luau-roblox-*.zip` / `imgui-luau-love-*.zip` (bundle + examples). For a library-only build without the demo, build it yourself with `--no-demo`.
### Differences from C++ Dear ImGui
@@ -242,6 +243,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.
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
`tools/test/run.py` runs any LocalScript headless against a mocked Roblox (`tools/test/roblox_mock.luau`):
@@ -283,7 +298,7 @@ Multi-select / box-select, typing-select, the Assets Browser example, the Item P
### 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
+40 -13
View File
@@ -30,24 +30,39 @@ jobs:
unzip -q luau.zip -d luau && chmod +x luau/* && echo "$PWD/luau" >> "$GITHUB_PATH"
- name: Build
env:
IMGUI_BUNDLE_STRICT: "1" # fail if enum folding (needs the luau CLI above) can't run
run: |
mkdir -p dist
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 --target love dist/love # imgui_love.lua (library + demo)
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/love/imgui_love.lua dist/imgui_love.min.lua
- name: Check
run: |
python3 tools/sync_github_readme.py --check
luau-compile --null dist/roblox/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-nodemo/imgui_love.lua > /dev/null
python3 tools/minify.py --test
luau-compile --null dist/ImGui.min.luau
luajit -bl dist/imgui_love.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
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 55 --quiet --roblox --timeout 300 | tee /tmp/f.txt
grep -q "FIXES OK" /tmp/f.txt
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
- name: Package
@@ -55,16 +70,14 @@ jobs:
V="${GITHUB_REF_NAME}"
cd dist
cp roblox/ImGui.luau ImGui.luau
cp roblox-nodemo/ImGui.luau ImGui-nodemo.luau
cp love/imgui_love.lua imgui_love.lua
cp love-nodemo/imgui_love.lua imgui_love-nodemo.lua
# 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/examples && cp -r ../examples/example_roblox ../examples/example_shared pkg-roblox/examples/
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 pkg-roblox/examples/
(cd pkg-roblox && zip -qr ../imgui-luau-roblox-$V.zip .)
# LÖVE: bundle + fonts + examples (run: love examples/example_love)
mkdir -p pkg-love/build && cp imgui_love.lua pkg-love/build/ && cp -r ../fonts pkg-love/ && rm -f pkg-love/fonts/*.lua
mkdir -p pkg-love/examples && cp -r ../examples/example_love ../examples/example_shared pkg-love/examples/
mkdir -p pkg-love/examples && cp -r ../examples/example_love pkg-love/examples/
(cd pkg-love && zip -qr ../imgui-luau-love-$V.zip .)
ls -la
@@ -74,12 +87,26 @@ jobs:
with:
files: |
dist/ImGui.luau
dist/ImGui-nodemo.luau
dist/imgui_love.lua
dist/imgui_love-nodemo.lua
dist/*.min.luau
dist/*.min.lua
dist/*.zip
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)
if: "!startsWith(github.ref, 'refs/tags/v')"
env:
@@ -88,4 +115,4 @@ jobs:
gh release delete latest --yes --cleanup-tag || true
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." \
dist/ImGui.luau dist/ImGui-nodemo.luau dist/imgui_love.lua dist/imgui_love-nodemo.lua dist/*.zip
dist/ImGui.luau dist/imgui_love.lua dist/*.min.luau dist/*.min.lua dist/*.zip
+4
View File
@@ -0,0 +1,4 @@
.opencode/
Packages/
wally.lock
__pycache__/
+43 -28
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.
| [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) |
| [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).
### 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
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)
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`.
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.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 `60`; `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.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. |
@@ -179,6 +161,27 @@ io.Fonts:AddFontDefault()
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. Redraws are capped at 60 per second by default (`SetRenderRate(hz)`, `0` = uncapped), optionally lower while busy with `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
| Backend | File | Build | Renderer | Example |
@@ -199,12 +202,10 @@ love examples/example_love
Roblox stays the primary target: the Roblox build is untouched by this (no shims, `@native` kept); the LÖVE build strips `@native` and adds small `bit32`/`buffer`/`utf8`/`table` shims (the COMPAT section of [backends/imgui_impl_love.lua](backends/imgui_impl_love.lua)). `tools/test/love/run.sh` checks that LuaJIT produces exactly the same draw data as Luau.
[examples/example_shared/app.lua](examples/example_shared/app.lua) is one UI that runs unchanged on both: `roblox.client.luau` (Rojo: `examples/example_shared/default.project.json`) and `main.lua` (`love examples/example_shared`) are the only platform code. Write shared UI code in plain Lua 5.1 syntax (no `+=`, `//`, `continue`).
#### Prebuilt releases
Every push to `master` publishes a rolling `latest` release, and every `v*` tag a versioned release ([.github/workflows/release.yml](.github/workflows/release.yml)), with:
`ImGui.luau` / `ImGui-nodemo.luau` (Roblox ModuleScript), `imgui_love.lua` / `imgui_love-nodemo.lua` (LÖVE), and `imgui-luau-roblox-*.zip` / `imgui-luau-love-*.zip` (bundle + examples).
`ImGui.luau` (Roblox ModuleScript), `imgui_love.lua` (LÖVE), their minified `.min` variants, and `imgui-luau-roblox-*.zip` / `imgui-luau-love-*.zip` (bundle + examples). For a library-only build without the demo, build it yourself with `--no-demo`.
### Differences from C++ Dear ImGui
@@ -239,6 +240,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.
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
`tools/test/run.py` runs any LocalScript headless against a mocked Roblox (`tools/test/roblox_mock.luau`):
@@ -280,7 +295,7 @@ Multi-select / box-select, typing-select, the Assets Browser example, the Item P
### 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
+4
View File
@@ -431,6 +431,8 @@ function ImGui_ImplLOVE_Init(opts)
local io = ImGui.GetIO()
local bd = { Time = love.timer.getTime(), Root = opts.root or "", Textures = {}, TextureCount = 0, Meshes = {}, FrameNo = 0 }
io.BackendPlatformUserData = bd
io.BackendPlatformName = "imgui_impl_love"
io.BackendRendererName = "imgui_impl_love"
io.BackendFlags = bit32.bor(io.BackendFlags, ImGuiBackendFlags.RendererHasTextures, ImGuiBackendFlags.RendererHasVtxOffset, ImGuiBackendFlags.HasMouseCursors)
local platform_io = ImGui.GetPlatformIO()
@@ -449,6 +451,8 @@ function ImGui_ImplLOVE_Shutdown()
for _, m in pairs(bd.Meshes) do m.Mesh:release() end
for _, t in pairs(bd.Textures) do t:release() end
io.BackendPlatformUserData = nil
io.BackendPlatformName = nil
io.BackendRendererName = nil
io.BackendFlags = bit32.band(io.BackendFlags, bit32.bnot(bit32.bor(ImGuiBackendFlags.RendererHasTextures, ImGuiBackendFlags.RendererHasVtxOffset, ImGuiBackendFlags.HasMouseCursors)))
end
+400 -80
View File
@@ -142,6 +142,7 @@ end
---------------------------------------------------------
-- 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 bpp, tex_w = tex.BytesPerPixel, tex.Width
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.
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
-- 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 tw, th, tbuf = t.Width, t.Height, t.Buffer
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
local ty = floor((v0 + (y + 0.5 - y0) * dv) * th)
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
for x = px0, px1 - 1 do
local tx = floor((u0 + (x + 0.5 - x0) * du) * tw)
if tx < 0 then tx = 0 elseif tx >= tw then tx = tw - 1 end
local to = (trow + tx) * 4
for c = 1, ncol do
local to = trow4 + cols[c]
local ta = readu8(tbuf, to + 3)
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
o = o + 4
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 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 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)
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_k = {} -- layer kind (1..3)
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
local function HashVertex(h1, h2, vbuf, v, ox, oy)
@@ -540,7 +557,7 @@ local function GetLayer(bd, draw_list, kind)
else
frame = NewFrame(bd.Gui, false); root = frame
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 }
set[kind] = layer
return layer
@@ -567,7 +584,8 @@ local function GetTile(bd, layer, key, tx, ty)
label.Visible = false
label.Active = bd.TilesActive
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
return tile
end
@@ -587,31 +605,129 @@ local function DestroyTiles(bd)
bd.Layers = {}
end
-- Exact signature of everything in a draw list that affects its layers (nil = can't skip: user callbacks)
local sig_buf = buffer.create(4096)
-- Snapshot of everything in a draw list that affects its layers, compared exactly with the next frame's list:
-- 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
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 ncmd = draw_list.CmdBuffer.Size
local need = ni * 4 + ncmd * 64 + 64
if buffer.len(sig_buf) < need then sig_buf = buffer.create(need * 2) end
local b, o = sig_buf, 0
for i = 1, ni do writeu32(b, o, idx[i]); o += 4 end
local sv, si, sc = set.SV, set.SI, set.SC
if not sv or buffer.len(sv) < nv then sv = buffer.create(max(nv, 64) * 2); set.SV = sv end
if not si or buffer.len(si) < ni * 4 then si = buffer.create(max(ni, 16) * 8); set.SI = si 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
if pcmd.UserCallback ~= nil then return nil end
if pcmd.UserCallback ~= nil then set.SnapOK = false; return end
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)
writeu32(b, o + 32, pcmd.ElemCount); writeu32(b, o + 36, pcmd.IdxOffset); writeu32(b, o + 40, pcmd.VtxOffset)
buffer.writef64(b, o + 44, pcmd:GetTexID())
o += 52
writef64(sc, o, c.x); writef64(sc, o + 8, c.y); writef64(sc, o + 16, c.z); writef64(sc, o + 24, c.w)
writeu32(sc, o + 32, pcmd.ElemCount); writeu32(sc, o + 36, pcmd.IdxOffset); writeu32(sc, o + 40, pcmd.VtxOffset)
writef64(sc, o + 44, pcmd:GetTexID())
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
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
-- 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
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 deferred = false
-- Pass 2: hash primitives into layer-relative tiles
PB("ImGui tile hash")
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
local nt = 0
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)
-- 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 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]
h1, h2 = HashVertex(h1, h2, vbuf, pr_a[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)
local vd = pr_d[p]
if vd then h1, h2 = HashVertex(h1, h2, vbuf, vd, vax, vay) end
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_c[p], vax, vay)
end
end
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
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 k = tn[key]
if not k or k == 0 then
k = 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
touched[nt] = key
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
tn[key] = k
tl[key][k] = p
tl1[key][k], tl2[key][k], tlf[key][k] = h1, h2, fsig
th1[key] = (th1[key] * 48271 + h1) % P1
th2[key] = (th2[key] * 16807 + h2 + k) % P2
end
@@ -758,7 +909,7 @@ local function DrawLayer(bd, layer, plist, np, lx0, ly0, ox, oy, force, z, clip_
PB("ImGui raster+upload")
for key, tile in pairs(layer.Tiles) do
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
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 tx, ty = layer.TX[key], layer.TY[key]
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]
local x0, y0 = lx0 + tx * TILE, ly0 + ty * TILE
local x1, y1 = x0 + TILE, y0 + TILE
buf, buf_w = tile.Buffer, TILE
local list = tl[key]
-- 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).
local list, lh1, lh2 = tl[key], tl1[key], tl2[key]
local oh1, oh2, ob0, ob1, ob2, ob3, on = tile.OH1, tile.OH2, tile.OB0, tile.OB1, tile.OB2, tile.OB3, tile.ON
-- New content bounds (union of the primitives' bounds in this tile)
local nx0, ny0, nx1, ny1 = TILE, TILE, 0, 0
for k = 1, count do
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
end
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
-- 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
if rw == TILE then
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
for k = 1, count do
local p = list[k]
local cx0, cy0 = max(pr_cx0[p], x0) - x0, max(pr_cy0[p], y0) - y0
local cx1, cy1 = min(pr_cx1[p], x1) - x0, min(pr_cy1[p], y1) - y0
-- clip to the dirty region (only those pixels are redrawn)
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]
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
@@ -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)
elseif rw > 0 and ry1 > ry0 then
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
tile.Image:WritePixelsBuffer(Vector2.new(rx0, ry0), Vector2.new(rw, rh), sub)
end
PE()
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.Label.Visible = true
end
end
PE()
return deferred
end
@native
function ImGui_ImplRoblox_RenderDrawData(draw_data)
local bd = ImGui_ImplRoblox_GetBackendData()
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")
if draw_data.Textures ~= nil then
for _, tex in draw_data.Textures:iter() do
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)
bd.ForceRedraw = true -- texture contents changed: tiles sampling it must be redrawn
end
end
end
@@ -862,7 +1094,8 @@ function ImGui_ImplRoblox_RenderDrawData(draw_data)
end
if rate > 0 then
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)
end
@@ -877,20 +1110,27 @@ function ImGui_ImplRoblox_RenderDrawData(draw_data)
local n = 0
local z = 0
local any_deferred = false
for _, draw_list in draw_data.CmdLists:iter() do
-- Unchanged draw list (same vertex bytes, indices, commands, origin and scroll): its layers are already
-- correct, only restack them. Static windows then cost one byte compare instead of collect + hash.
-- Unchanged draw list (same vertices, indices, commands, origin and scroll): its layers are already correct,
-- only restack them. Moved by whole pixels (a dragged window): only move its layers. Either way no collect + hash.
PB("ImGui unchanged check")
local sig = DrawListSignature(draw_list, ox, oy)
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
local layer = set[kind]
if layer and layer.Visible then
if same == 2 then ShiftLayer(layer, sdx, sdy, bd.Scale) end
z += 1
if layer.Z ~= z then layer.Z = z; layer.Root.ZIndex = z 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
z = PlaceOverlays(bd, draw_list, z, ox, oy)
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)
end
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
ci += 1
cm_free[ci], cm_kind[ci] = false, 0
if pcmd.UserCallback ~= nil then
pcmd.UserCallback(draw_list, pcmd)
elseif pcmd.ElemCount > 0 then
local clip = pcmd.ClipRect
local cx0, cy0 = max(floor(clip.x - ox), lim_x0), max(floor(clip.y - oy), lim_y0)
local cx1, cy1 = min(floor(clip.z - ox), lim_x1), min(floor(clip.w - oy), lim_y1)
local rcx0, rcy0, rcx1, rcy1 = floor(clip.x - ox), floor(clip.y - oy), floor(clip.z - ox), floor(clip.w - oy)
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
if ccx0 and clip.x >= ccx0 and clip.y >= ccy0 and clip.z <= ccx1 and clip.w <= ccy1 then
kind = 2; seen_content = true
@@ -925,6 +1182,8 @@ function ImGui_ImplRoblox_RenderDrawData(draw_data)
elseif seen_content then
kind = 3
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
local tex_id = pcmd:GetTexID()
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
i = i + 3
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 by0 < cy0 then by0 = cy0 end
if bx1 > cx1 then bx1 = cx1 end
@@ -983,12 +1252,16 @@ function ImGui_ImplRoblox_RenderDrawData(draw_data)
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
PE()
-- Split into layer kinds and place each one
set = bd.Layers[draw_list]
local list_deferred = false
local nk1, nk2, nk3 = 0, 0, 0
local o1x, o1y, o3x, o3y = math.huge, math.huge, math.huge, math.huge
for p = first, n do
@@ -1007,19 +1280,28 @@ function ImGui_ImplRoblox_RenderDrawData(draw_data)
end
if nk1 > 0 then
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
if nk2 > 0 then
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,
content_x0, content_y0, content_x1, content_y1)
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) then list_deferred = true end
elseif set and set[2] then HideLayer(set[2]) end
if nk3 > 0 then
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
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)
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)
for draw_list, set in pairs(bd.Layers) do
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
if frame_no - set.LastUsed > LAYER_KEEP_FRAMES then
DestroyLayerSet(set)
@@ -1044,6 +1326,8 @@ function ImGui_ImplRoblox_RenderDrawData(draw_data)
-- 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
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()
end
@@ -1069,9 +1353,11 @@ function ImGui_ImplRoblox_Init(parent, render_scale)
local bd = {
Time = os.clock(), Gui = gui, Connections = {},
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 = 60, BusyRenderRate = 0, RedrawBudget = 0, HB = 0, NextRender = 0, LastPrimCount = 0, ForceRedraw = true,
}
io.BackendPlatformUserData = bd
io.BackendPlatformName = "imgui_impl_roblox"
io.BackendRendererName = "imgui_impl_roblox"
io.BackendFlags = bit32.bor(io.BackendFlags, ImGuiBackendFlags.RendererHasTextures, ImGuiBackendFlags.RendererHasVtxOffset)
@@ -1127,8 +1413,36 @@ function ImGui_ImplRoblox_Init(parent, render_scale)
end
end)
-- Text proxy: 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 the proxy's text = imgui's current selection).
bd.PendingChars = {}
-- 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")
proxy.Name = "TextProxy"
proxy.BackgroundTransparency = 1
@@ -1140,12 +1454,11 @@ function ImGui_ImplRoblox_Init(parent, render_scale)
proxy.ClearTextOnFocus = false
proxy.TextEditable = true
proxy.Text = ""
proxy.Parent = gui
bd.Proxy, bd.ProxyText, bd.PendingChars = proxy, "", {}
proxy.Parent = bd.Gui
bd.Proxy, bd.ProxyText = proxy, ""
-- 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.)
connect(proxy.FocusLost, function()
if bd.ProxyDebug then print("[ImGui text proxy] FocusLost, mouse over imgui:", ImGui.GetIO().WantCaptureMouse) end
proxy.FocusLost:Connect(function()
if bd.ProxyReleasing then return end
-- 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.
@@ -1154,9 +1467,6 @@ function ImGui_ImplRoblox_Init(parent, render_scale)
local g = ImGui.GetCurrentContext()
if g and g.ActiveId ~= 0 and g.ActiveId == bd.ProxyActiveId then ImGui.ClearActiveID() end
end)
connect(UserInputService.WindowFocused, function() ImGui.GetIO():AddFocusEvent(true) end)
connect(UserInputService.WindowFocusReleased, function() ImGui.GetIO():AddFocusEvent(false) end)
end
function ImGui_ImplRoblox_Shutdown()
@@ -1167,6 +1477,8 @@ function ImGui_ImplRoblox_Shutdown()
bd.Gui:Destroy()
io.BackendPlatformUserData = nil
io.BackendPlatformName = nil
io.BackendRendererName = nil
io.BackendFlags = bit32.band(io.BackendFlags, bit32.bnot(bit32.bor(
ImGuiBackendFlags.RendererHasTextures,
ImGuiBackendFlags.RendererHasVtxOffset
@@ -1177,6 +1489,8 @@ function ImGui_ImplRoblox_NewFrame()
PB("ImGui NewFrame (backend)")
local io = ImGui.GetIO()
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 w, h = floor(screen.X * bd.Scale), floor(screen.Y * bd.Scale)
@@ -1201,15 +1515,16 @@ function ImGui_ImplRoblox_NewFrame()
-- otherwise the KeyCode fallback keeps typing working.
local proxy = bd.Proxy
local g = ImGui.GetCurrentContext()
local focused_box = UserInputService:GetFocusedTextBox()
if io.WantTextInput and g.ActiveId ~= 0 then
local focused_box = proxy and UserInputService:GetFocusedTextBox()
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
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
bd.ProxyText = proxy.Text -- what it already holds is not new input (else re-typing it would collapse imgui's selection)
proxy:CaptureFocus()
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
elseif focused_box == proxy then
bd.ProxyReleasing = true
@@ -1217,13 +1532,11 @@ function ImGui_ImplRoblox_NewFrame()
bd.ProxyReleasing = false
focused_box = nil
end
local was_focused = proxy_focused
proxy_focused = focused_box == proxy
proxy_focused = proxy ~= nil and focused_box == proxy
local used_proxy_text = false
if proxy_focused then
-- 1) whatever the OS put in the proxy since last frame (typing, paste, cut) replaces imgui's selection
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)
bd.ProxyLastRaw = text
-- InputText ignores characters while Ctrl is held (upstream, so shortcuts don't type); a paste arrives with
@@ -1259,7 +1572,6 @@ function ImGui_ImplRoblox_NewFrame()
proxy.Text = sel
bd.ProxyLastRaw = nil
bd.ProxyText = sel
if bd.ProxyDebug then print("[ImGui text proxy] mirror selection", string.format("%q", sel)) end
end
-- 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
@@ -1275,7 +1587,6 @@ function ImGui_ImplRoblox_NewFrame()
if not used_proxy_text then for _, c in ipairs(pending) do io:AddInputCharacter(c) end end
table.clear(pending)
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.)
local want = io.WantCaptureMouse
@@ -1286,7 +1597,15 @@ function ImGui_ImplRoblox_NewFrame()
PE()
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 60; 0 = every frame, limited by the redraw budget instead). imgui logic still runs every frame.
function ImGui_ImplRoblox_SetRenderRate(hz)
ImGui_ImplRoblox_GetBackendData().RenderRate = hz or 0
end
@@ -1316,8 +1635,9 @@ ImGui_ImplRoblox = {
AddFile = ImGui_ImplRoblox_AddFile,
SetRenderScale = ImGui_ImplRoblox_SetRenderScale,
SetRenderRate = ImGui_ImplRoblox_SetRenderRate,
SetRedrawBudget = ImGui_ImplRoblox_SetRedrawBudget,
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,
DestroyTexture = ImGui_ImplRoblox_DestroyTexture,
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"], [22675, "imgui_demo.lua"], [34533, "imgui_draw.lua"], [42860, "imgui_h.lua"], [45857, "imgui_impl_roblox.lua"], [47509, "imgui_impl_roblox_fonts.lua"], [53912, "imgui_internal.lua"], [57884, "imgui_tables.lua"], [62976, "imgui_widgets.lua"], [75210, "imstb_rectpack.lua"], [75874, "imstb_textedit.lua"], [77416, "imstb_truetype.lua"], [80204, "imstd_minstdio.lua"]]
+32164 -14186
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+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"], [22812, "imgui_demo.lua"], [34670, "imgui_draw.lua"], [42997, "imgui_h.lua"], [45994, "imgui_impl_love.lua"], [46538, "imgui_internal.lua"], [50510, "imgui_tables.lua"], [55602, "imgui_widgets.lua"], [67836, "imstb_rectpack.lua"], [68500, "imstb_textedit.lua"], [70042, "imstb_truetype.lua"], [72830, "imstd_minstdio.lua"]]
+31753 -14093
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -3,7 +3,7 @@
"tree": {
"$className": "DataModel",
"ReplicatedStorage": {
"ImGui": { "$path": "build/ImGui.luau" }
"ImGui": { "$path": "build/ImGuiTyped.luau", "Impl": { "$path": "build/ImGui.luau" } }
},
"StarterPlayer": {
"StarterPlayerScripts": {
+1 -1
View File
@@ -65,5 +65,5 @@ there, but keep this port's existing data structures and conventions. Read the s
- **ImGuiAxis is 1-based in this port** (`None=0, X=1, Y=2`; upstream `None=-1, X=0, Y=1`). Use `v[axis]` to index an ImVec2 and `3 - axis` for the other axis. `ImGuiDir` keeps upstream values (`None=-1, Left=0, Right=1, Up=2, Down=3`).
- Never write `cond and nil or x` in Lua: it always evaluates `x`. Use an `if`.
- Port signature quirk: `AddRect(min, max, col, rounding, thickness, flags)` (thickness BEFORE flags; upstream has flags first). Check the port's signature before porting any draw call that takes both.\n
- Draw calls taking both thickness and flags use upstream's 1.92.8+ order: thickness BEFORE flags, e.g. `AddRect(min, max, col, rounding, thickness, flags)`, `PathStroke(col, thickness, flags)`, `AddPolyline(points, count, col, thickness, flags)`.
- C++ default arguments (e.g. `bool x = true`) must be reproduced with `if x == nil then x = true end`; a missing Lua argument is nil/falsy.
+2622
View File
File diff suppressed because it is too large Load Diff
+19 -39
View File
@@ -1,53 +1,33 @@
Dear ImGui for Roblox - not yet ported / known gaps
====================================================
Upstream reference: tools/test/upstream/ (ocornut/imgui docking branch, 1.93 WIP).
Re-check with: python3 tools/test/missing.py [imgui|widgets|tables|draw] and python3 tools/test/undefined.py --no-demo
(missing.py also lists names the port defines differently, e.g. the *V printf variants and DragFloat2..4 / DragInt2..4,
which exist as ImGui.DragFloat2 = DragFloatN(2) etc. Those are NOT gaps.)
Re-check with: python3 tools/compare_upstream.py (writes docs/PORT_STATUS.txt: [ ] = not ported, [=] = replaced by a
Lua equivalent, "missing:" = enum values / struct fields) and python3 tools/test/undefined.py --no-demo
FEATURES NOT PORTED
-------------------
- Multi-select / box-select: BeginMultiSelect, EndMultiSelect, MultiSelectItemHeader/Footer, MultiSelectAddSetAll,
MultiSelectAddSetRange, SetNextItemSelectionUserData, BeginBoxSelect/EndBoxSelect, DebugNodeMultiSelectState.
(Selectable/TreeNode call sites are stubbed with TODOs; the demo "Selection State & Multi-Select" section is not ported.)
- Typing-select (type to jump in lists): GetTypingSelectRequest, TypingSelectFind*, DebugNodeTypingSelectState.
- Examples > Assets Browser (needs multi-select + box-select).
- Metrics > Tools > Item Picker (UpdateDebugToolItemPicker).
- DebugFlashStyleColor / UpdateDebugToolFlashStyleColor (Style Editor "?" flash button).
- DebugNodeInputTextState (Metrics > Internal state > InputText).
- BringWindowToDisplayBehind: stub, does nothing.
- ColorEdit4 / ColorPicker4: drag & drop target (dropping a color onto a color widget).
- ImFontAtlas:AddFontFromMemoryCompressedTTF: stub (binary_to_compressed_c fonts can't be loaded).
- ImGuiSizeCallbackData: SetNextWindowSizeConstraints custom callbacks only partially supported
(Examples > Constrained Resize).
- LogSetNextTextDecoration (Selectable "[ ]" decoration in log/clipboard output).
- stb_truetype: cmap high-byte mapping (some CJK fonts assert).
- Context hooks removal (RemoveContextHook), SetContextName.
- RegisterUserTexture / UnregisterUserTexture.
- ErrorCheckNewFrameSanityChecks / ErrorCheckEndFrameSanityChecks (fewer usage-error asserts than upstream).
- GetAllocatorFunctions (n/a in Luau).
NOT PORTED / PARTIAL
--------------------
- ImFont::AddRemapCodepointToGlyphIndex / ImGui_ImplStbTrueType_FontSrcGetGlyphIndexFromCodepoint (the port's font
loader still checks glyphs by codepoint; 1.93 split this into codepoint -> glyph index -> glyph).
- ImGui_GetDefaultCompressedFontDataProggyClean/ProggyForever: AddFontFromMemoryCompressedTTF works, but the default
fonts are loaded from fonts/ instead of embedded compressed data.
- ImFontAtlasDebugLogTextureRequests, ImHashSkipUncontributingPrefix.
- DebugLogSkippedErrors / DebugDrawIdConflicts* (error-recovery and ID-conflict debug tools).
LUA LIMITATIONS
---------------
- ImGuiDataType.S64/U64: values are Luau doubles, exact only within +-2^53 (LLONG_MAX/ULLONG_MAX round up).
- ImDrawList::AddCallback(userdata_size > 0): the "copy" is a shallow copy of a table userdata (no raw bytes).
WON'T DO (platform)
-------------------
- Multi-viewports / platform windows: Roblox has one window. Viewport API exists for the main viewport only.
- Multi-viewports / platform windows: Roblox has one window. The viewport API covers the main viewport only.
- Gamepad navigation via a real gamepad backend (keyboard navigation works).
- IME / non-ASCII keyboard text input (backend sends printable ASCII only).
NOT VERIFIED LINE BY LINE AGAINST UPSTREAM
------------------------------------------
- Demo: Layout & Scrolling, Popups & Modal windows, Inputs & Focus.
KNOWN ISSUES
------------
- Tables > "Columns flags": disabling every column asserts in TableUpdateLayout (upstream asserts too).
- Style Editor > Colors uses a ListClipper (port-only change, upstream submits every row).
PERFORMANCE IDEAS (not done)
----------------------------
- Opt-in cached windows (skip user code for idle windows).
- Cheaper visible ColorEdit4 rows; cheaper Begin() for child windows.
PLANNED EXTENSIONS
------------------
- ImGuizmo, ImNodes, ImGuiColorTextEdit, ImPlot, ImPlot3D (as separate extensions/, built with a --with flag).
- stb_truetype: cmap format 2 (high-byte mapping) asserts (same "@TODO" as upstream stb_truetype).
- ImTriangulator differs from upstream (imgui_draw.lua, [Luau]): CCW input is reversed first and only strictly
counter-clockwise corners are reflex, so CCW and duplicate-point polygons triangulate differently from upstream.
+452 -293
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 RunService = game:GetService("RunService")
local AssetService = game:GetService("AssetService")
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
-- Roblox UI points are multiplied by the OS display scale (e.g. 150%), which makes imgui look large.
-- Set this to your display scale to render imgui at physical-pixel resolution (same size as a desktop imgui app).
local render_scale = 1.0
local clear_color = ImVec4(0.45, 0.55, 0.60, 1.00)
local show_demo_window = true
local show_another_window = false
local f = 0.0
local counter = 0
local MainScale = 1.0
local RenderScale = 1.0
local RenderRate = 60 -- max tile redraws per second (0 = every frame, limited by the redraw budget instead)
local ThumbnailSize = 420
local FontSize = 13.0
local FontNames = { "Cousine-Regular", "DroidSans", "Karla-Regular", "ProggyTiny", "Roboto-Medium" }
local State = {
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()
local io = ImGui.GetIO()
io.ConfigFlags = bit32.bor(io.ConfigFlags, ImGuiConfigFlags.NavEnableKeyboard, ImGuiConfigFlags.DockingEnable)
local IO = ImGui.GetIO()
IO.ConfigFlags = bit32.bor(IO.ConfigFlags, ImGuiConfigFlags.NavEnableKeyboard, ImGuiConfigFlags.DockingEnable)
-- Catppuccin Mocha, Mauve accent (https://catppuccin.com/palette)
local function StyleColorsCatppuccinMocha(style)
local function hex(h, a)
return ImVec4(bit32.extract(h, 16, 8) / 255, bit32.extract(h, 8, 8) / 255, bit32.extract(h, 0, 8) / 255, a 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 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)
-- catppuccin mocha mauve https://catppuccin.com/palette
local function StyleColorsCatppuccinMocha(Style)
local function Hex(Value, Alpha)
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 style = ImGui.GetStyle()
StyleColorsCatppuccinMocha(style)
style:ScaleAllSizes(main_scale)
style.FontScaleDpi = main_scale
local Style = ImGui.GetStyle()
StyleColorsCatppuccinMocha(Style)
Style:ScaleAllSizes(MainScale)
Style.FontScaleDpi = MainScale
-- Fonts: ProggyClean stays the default (added first); the rest show up in Style Editor > Fonts
io.Fonts:AddFontDefault()
for _, name in ipairs({ "Cousine-Regular", "DroidSans", "Karla-Regular", "ProggyTiny", "Roboto-Medium" }) do
io.Fonts:AddFontFromFileTTF("fonts/" .. name .. ".ttf", 13.0)
IO.Fonts:AddFontDefault()
for _, Name in ipairs(FontNames) do
IO.Fonts:AddFontFromFileTTF("fonts/" .. Name .. ".ttf", FontSize)
end
ImGui_ImplRoblox.Init(nil, render_scale)
ImGui_ImplRoblox.SetRenderRate(60) -- tile redraws per second; imgui logic + your code still run every frame
-- 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.Init(nil, RenderScale)
ImGui_ImplRoblox.SetRenderRate(RenderRate)
-- ImGui_ImplRoblox.SetBusyRenderRate(30) -- Optional: lower redraw rate while scrolling/resizing/moving windows
-- 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 = {
None = 0,
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
NoBorder = 4, -- no style border
NoPadding = 8, -- no FramePadding inside the Frame (UIPadding)
NoRounding = 16, -- no FrameRounding corners (UICorner)
None = 0,
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
NoBorder = 4, -- No style border
NoPadding = 8, -- No FramePadding inside the Frame (UIPadding)
NoRounding = 16, -- No FrameRounding corners (UICorner)
}
G.ImGuiEmbedFlags = EmbedFlags
local embeds = {} -- [ImGuiID] = { Frame, Padding, Corner }
local Embeds = {}
function ImGui.Embed(id, size, flags, init)
flags = flags or EmbedFlags.None
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
local function HasFlag(Flags, Flag)
return bit32.band(Flags, Flag) ~= 0
end
function ImGui.DestroyEmbed(id)
local gid = ImGui.GetCurrentWindow():GetID(id)
local e = embeds[gid]
if e then e.Frame:Destroy(); embeds[gid] = nil end
local function CreateEmbed(Id)
local Frame = Instance.new("Frame")
Frame.Name = tostring(Id)
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
-- Example: Decal Viewer. Loads a decal/image asset's pixels into an imgui texture and shows it in the demo's
-- image viewer (zoom with the wheel, drag to pan, toggleable pixel grid).
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 }
function ImGui.Embed(Id, Size, Flags, Init)
Flags = Flags or EmbedFlags.None
-- A Decal asset wraps an Image asset with a different id; the image is almost always uploaded just before it
-- (same creator, id slightly lower). ponytail: heuristic scan of 10 ids, InsertService:LoadAsset when owned is exact.
local function ResolveImageId(id)
local ok, info = pcall(MarketplaceService.GetProductInfo, MarketplaceService, id)
if not ok then return nil, "GetProductInfo failed: " .. tostring(info) end
if info.AssetTypeId == 1 then return id end -- already an Image
if info.AssetTypeId ~= 13 then return nil, "asset is not a decal or image" end
for cand = id - 1, id - 10, -1 do
local ok2, ci = pcall(MarketplaceService.GetProductInfo, MarketplaceService, cand)
if ok2 and ci.AssetTypeId == 1 and ci.Creator and info.Creator and ci.Creator.Id == info.Creator.Id then return cand end
end
return nil, "couldn't find the decal's image id (try the image id directly)"
local Window = ImGui.GetCurrentWindow()
local GuiId = Window:GetID(Id)
local Embed = Embeds[GuiId]
if not Embed then
Embed = CreateEmbed(Id)
Embeds[GuiId] = Embed
if Init then
Init(Embed.Frame)
end
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
local function LoadDecal(id)
decal.loading = true
decal.status = "Loading " .. id .. "..."
task.spawn(function()
local image_id, err = ResolveImageId(id)
decal.thumb = nil
if image_id then
-- 1) the image itself (needs permission), 2) its 420x420 thumbnail (may be allowed when 1 isn't)
local ok, img = pcall(AssetService.CreateEditableImageAsync, AssetService, Content.fromUri("rbxassetid://" .. image_id))
if not ok then
local ok2, img2 = pcall(AssetService.CreateEditableImageAsync, AssetService, Content.fromUri("rbxthumb://type=Asset&id=" .. id .. "&w=420&h=420"))
if ok2 then ok, img = true, img2 end
end
if ok then
local size = img.Size
local pixels = img:ReadPixelsBuffer(Vector2.zero, size)
img:Destroy()
if decal.tex then ImGui_ImplRoblox.DestroyTexture(decal.tex) end
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
-- 3) no pixel access: Roblox still displays any asset, so show its thumbnail in an ImageLabel (no grid)
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
else
decal.thumb = "rbxthumb://type=Asset&id=" .. id .. "&w=420&h=420"
decal.status = (err or "") .. " - showing thumbnail"
end
decal.loading = false
end)
function ImGui.DestroyEmbed(Id)
local GuiId = ImGui.GetCurrentWindow():GetID(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()
local Uri = string.format("rbxthumb://type=Asset&id=%d&w=%d&h=%d", Id, ThumbnailSize, ThumbnailSize)
local Success, Result = pcall(AssetService.CreateEditableImageAsync, AssetService, Content.fromUri(Uri))
if Success then
local Size = Result.Size
local Pixels = Result:ReadPixelsBuffer(Vector2.zero, Size)
Result:Destroy()
if Decal.Texture then
ImGui_ImplRoblox.DestroyTexture(Decal.Texture)
end
Decal.Texture = ImGui_ImplRoblox.CreateTexture(Pixels, Size.X, Size.Y)
Decal.Width, Decal.Height = Size.X, Size.Y
Decal.Viewer.ViewReset = true
Decal.Status = string.format("Thumbnail %d: %dx%d", Id, Size.X, Size.Y)
else
Decal.Status = "Failed to load thumbnail: " .. tostring(Result)
end
Decal.Loading = false
end)
end
local function ShowDecalViewer()
if not decal.show then return end
decal.show = ImGui.Begin("Decal Viewer", decal.show)
ImGui.SetNextItemWidth(ImGui.GetFontSize() * 12)
local enter = ImGui.InputTextWithHint("##id", "decal id", decal.input, 32, G.ImGuiInputTextFlags.CharsDecimal + G.ImGuiInputTextFlags.EnterReturnsTrue)
ImGui.SameLine()
ImGui.BeginDisabled(decal.loading)
if (ImGui.Button("Load") or enter) and not decal.loading then
local id = tonumber(ImGui.TextBufferToString(decal.input))
if id then LoadDecal(id) else decal.status = "Not a number" end
end
ImGui.EndDisabled()
ImGui.SameLine(); ImGui.TextDisabled(decal.status)
if decal.thumb then
local avail = ImGui.GetContentRegionAvail()
local frame = ImGui.Embed("decal_thumb", G.ImVec2(-1, math.max(avail.y, 50)), G.ImGuiEmbedFlags.None, function(f)
local img = Instance.new("ImageLabel")
img.Name = "Image"
img.Size = UDim2.fromScale(1, 1)
img.BackgroundTransparency = 1
img.ScaleType = Enum.ScaleType.Fit
img.Parent = f
end)
if frame.Image.Image ~= decal.thumb then frame.Image.Image = decal.thumb end
elseif decal.tex then
ExampleImageViewer_DrawOptions(decal.viewer)
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
ImGui.End()
if not Decal.Show then
return
end
Decal.Show = ImGui.Begin("Decal Viewer", Decal.Show)
ImGui.SetNextItemWidth(ImGui.GetFontSize() * 12)
local EnterPressed = ImGui.InputTextWithHint(
"##id",
"decal id",
Decal.Input,
32,
ImGuiInputTextFlags.CharsDecimal + ImGuiInputTextFlags.EnterReturnsTrue
)
ImGui.SameLine()
ImGui.BeginDisabled(Decal.Loading)
if (ImGui.Button("Load") or EnterPressed) and not Decal.Loading then
local Id = tonumber(ImGui.TextBufferToString(Decal.Input))
if Id and Id == math.floor(Id) then
LoadDecal(Id)
else
Decal.Status = "Not a valid id"
end
end
ImGui.EndDisabled()
ImGui.SameLine()
ImGui.TextDisabled(Decal.Status)
if Decal.Texture then
ExampleImageViewer_DrawOptions(Decal.Viewer)
local Available = ImGui.GetContentRegionAvail()
ExampleImageViewer_DrawCanvas(
Decal.Viewer,
ImVec2(math.max(Available.x, 50), math.max(Available.y, 50)),
Decal.Texture,
Decal.Width,
Decal.Height
)
end
ImGui.End()
end
-- Example: a ViewportFrame with a spinning part, edited live from imgui widgets
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
local function ShowHelloWindow()
ImGui.Begin("Hello, world!")
ImGui.Text("This is some useful text.")
local _
_, 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
State.Counter += 1
end
ImGui.SameLine()
ImGui.Text("counter = %d", State.Counter)
ImGui.Text("Application average %.3f ms/frame (%.1f FPS)", 1000.0 / IO.Framerate, IO.Framerate)
ImGui.End()
end
local function ShowAnotherWindow()
if not State.ShowAnotherWindow then
return
end
State.ShowAnotherWindow = ImGui.Begin("Another Window", State.ShowAnotherWindow)
ImGui.Text("Hello from another window!")
if ImGui.Button("Close Me") then
State.ShowAnotherWindow = false
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
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()
P("ImGui frame")
ImGui_ImplRoblox.NewFrame()
P("ImGui.NewFrame"); ImGui.NewFrame(); PE()
P("ImGui UI")
ProfileBegin("ImGui frame")
ImGui_ImplRoblox.NewFrame()
-- Dockspace covering the whole screen; the centre stays transparent so the game shows through
ImGui.DockSpaceOverViewport(0, nil, G.ImGuiDockNodeFlags.PassthruCentralNode)
ProfileBegin("ImGui.NewFrame")
ImGui.NewFrame()
ProfileEnd()
if show_demo_window then
ImGui.ShowDemoWindow()
end
ProfileBegin("ImGui UI")
ImGui.DockSpaceOverViewport(0, nil, ImGuiDockNodeFlags.PassthruCentralNode)
do
ImGui.Begin("Hello, world!")
if State.ShowDemoWindow then
-- For reference: https://github.com/4DBug/imgui-luau/blob/master/imgui_demo.lua
ImGui.ShowDemoWindow()
end
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)
ShowHelloWindow()
ShowAnotherWindow()
ShowDecalViewer()
ShowViewportWindow()
ProfileEnd()
f = ImGui.SliderFloat("float", f, 0.0, 1.0)
ImGui.ColorEdit3("clear color", clear_color)
ProfileBegin("ImGui.Render")
ImGui.EndFrame()
ImGui.Render()
ProfileEnd()
if ImGui.Button("Button") then
counter = counter + 1
end
ImGui.SameLine()
ImGui.Text("counter = %d", counter)
ImGui.Text("Application average %.3f ms/frame (%.1f FPS)", 1000.0 / io.Framerate, io.Framerate)
ImGui.End()
end
if show_another_window then
show_another_window = ImGui.Begin("Another Window", show_another_window)
ImGui.Text("Hello from another window!")
if ImGui.Button("Close Me") then
show_another_window = false
end
ImGui.End()
end
PE()
ShowDecalViewer()
if show_viewport_window then
show_viewport_window = ImGui.Begin("Viewport", show_viewport_window)
spin_speed = ImGui.SliderFloat("speed", spin_speed, 0.0, 5.0)
ImGui.ColorEdit3("color", part_color)
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())
PE()
ImGui_ImplRoblox.RenderDrawData(ImGui.GetDrawData())
ProfileEnd()
end)
-53
View File
@@ -1,53 +0,0 @@
-- One UI that runs unchanged on Roblox and LÖVE: only the Dear ImGui API is used, and the code is plain Lua 5.1
-- (no Luau-only syntax), so both Luau and LuaJIT run it. The platform launchers are roblox.client.luau and main.lua.
-- local App = require(app)(G) -- G = the table returned by the ImGui bundle
-- every frame, between ImGui.NewFrame() and ImGui.Render(): App.Frame()
return function(G)
local ImGui, ImVec2, ImVec4 = G.ImGui, G.ImVec2, G.ImVec4
local App = {
Platform = "?", -- set by the launcher ("Roblox" / "LÖVE")
ClearColor = ImVec4(0.45, 0.55, 0.60, 1.00), -- launchers may use it as the background colour
}
local show_demo, show_table = false, true
local counter, speed = 0, 1.0
local name = ImGui.NewTextBuffer("Player", 64)
local samples, t = {}, 0.0
local items = { { "Sword", 1, 120 }, { "Potion", 5, 15 }, { "Arrow", 64, 1 }, { "Shield", 1, 80 } }
function App.Frame()
local io = ImGui.GetIO()
t = t + io.DeltaTime * speed
for i = 1, 90 do samples[i] = math.sin(i / 90 * math.pi * 4 + t * 3) end
ImGui.SetNextWindowPos(ImVec2(20, 20), G.ImGuiCond.FirstUseEver)
ImGui.SetNextWindowSize(ImVec2(420, 360), G.ImGuiCond.FirstUseEver)
ImGui.Begin("Shared Example")
ImGui.Text("Running on %s (%.1f FPS)", App.Platform, io.Framerate)
ImGui.Separator()
ImGui.InputText("name", name, 64)
if ImGui.Button("Click me") then counter = counter + 1 end
ImGui.SameLine(); ImGui.Text("%s clicked %d times", ImGui.TextBufferToString(name), counter)
speed = ImGui.SliderFloat("speed", speed, 0.0, 4.0)
ImGui.ColorEdit3("background", App.ClearColor)
ImGui.PlotLines("wave", samples, nil, #samples, 0, nil, -1.0, 1.0, ImVec2(0, 60))
local _
_, show_table = ImGui.Checkbox("Inventory table", show_table)
ImGui.SameLine(); _, show_demo = ImGui.Checkbox("Demo window", show_demo)
if show_table and ImGui.BeginTable("inv", 3, bit32.bor(G.ImGuiTableFlags.Borders, G.ImGuiTableFlags.RowBg, G.ImGuiTableFlags.Sortable)) then
ImGui.TableSetupColumn("Item"); ImGui.TableSetupColumn("Count"); ImGui.TableSetupColumn("Value")
ImGui.TableHeadersRow()
for _, it in ipairs(items) do
ImGui.TableNextRow()
ImGui.TableNextColumn(); ImGui.Text(it[1])
ImGui.TableNextColumn(); ImGui.Text("%d", it[2])
ImGui.TableNextColumn(); ImGui.Text("%d", it[3])
end
ImGui.EndTable()
end
ImGui.End()
if show_demo and ImGui.ShowDemoWindow then show_demo = ImGui.ShowDemoWindow(show_demo) end
end
return App
end
-5
View File
@@ -1,5 +0,0 @@
function love.conf(t)
t.window.title = "Dear ImGui - shared example (LÖVE)"
t.window.width, t.window.height = 1280, 800
t.window.resizable = true
end
@@ -1,15 +0,0 @@
{
"name": "imgui-shared-example",
"tree": {
"$className": "DataModel",
"ReplicatedStorage": {
"ImGui": { "$path": "../../build/ImGui.luau" },
"SharedApp": { "$path": "app.lua" }
},
"StarterPlayer": {
"StarterPlayerScripts": {
"SharedExample": { "$path": "roblox.client.luau" }
}
}
}
}
-43
View File
@@ -1,43 +0,0 @@
-- LÖVE launcher for app.lua: python3 tools/bundle.py --target love then love examples/example_shared
local ROOT = love.filesystem.getSource() .. "/../../" -- repo root (build/, fonts/)
local function load_bundle()
local f = assert(io.open(ROOT .. "build/imgui_love.lua", "rb"), "run: python3 tools/bundle.py --target love")
local src = f:read("*a"); f:close()
return assert(loadstring(src, "@imgui_love.lua"))()
end
local G, ImGui, App
function love.load()
G = load_bundle()
ImGui = G.ImGui
ImGui.CreateContext()
local io = ImGui.GetIO()
io.ConfigFlags = bit32.bor(io.ConfigFlags, G.ImGuiConfigFlags.NavEnableKeyboard, G.ImGuiConfigFlags.DockingEnable)
io.IniFilename = nil
G.ImGui_ImplLove.Init({ root = ROOT })
App = love.filesystem.load("app.lua")()(G)
App.Platform = "LÖVE"
end
function love.update(dt)
G.ImGui_ImplLove.NewFrame()
ImGui.NewFrame()
App.Frame()
ImGui.Render()
end
function love.draw()
local c = App.ClearColor
love.graphics.clear(c.x, c.y, c.z, 1)
G.ImGui_ImplLove.RenderDrawData(ImGui.GetDrawData())
end
function love.mousemoved(x, y) G.ImGui_ImplLove.MouseMoved(x, y) end
function love.mousepressed(x, y, b) G.ImGui_ImplLove.MousePressed(x, y, b) end
function love.mousereleased(x, y, b) G.ImGui_ImplLove.MouseReleased(x, y, b) end
function love.wheelmoved(x, y) G.ImGui_ImplLove.WheelMoved(x, y) end
function love.keypressed(k) G.ImGui_ImplLove.KeyPressed(k) end
function love.keyreleased(k) G.ImGui_ImplLove.KeyReleased(k) end
function love.textinput(t) G.ImGui_ImplLove.TextInput(t) end
function love.focus(f) G.ImGui_ImplLove.Focus(f) end
@@ -1,21 +0,0 @@
-- Roblox launcher for app.lua (sync with examples/example_shared/default.project.json).
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local G = require(ReplicatedStorage:WaitForChild("ImGui"))
local ImGui = G.ImGui
ImGui.CreateContext()
local io = ImGui.GetIO()
io.ConfigFlags = bit32.bor(io.ConfigFlags, G.ImGuiConfigFlags.NavEnableKeyboard, G.ImGuiConfigFlags.DockingEnable)
G.ImGui_ImplRoblox.Init()
local App = require(ReplicatedStorage:WaitForChild("SharedApp"))(G)
App.Platform = "Roblox"
RunService.Stepped:Connect(function()
G.ImGui_ImplRoblox.NewFrame()
ImGui.NewFrame()
App.Frame()
ImGui.Render()
G.ImGui_ImplRoblox.RenderDrawData(ImGui.GetDrawData())
end)
+4992 -823
View File
File diff suppressed because it is too large Load Diff
+11036 -6463
View File
File diff suppressed because it is too large Load Diff
+3941 -852
View File
File diff suppressed because it is too large Load Diff
+1536 -737
View File
File diff suppressed because it is too large Load Diff
+1467 -667
View File
File diff suppressed because it is too large Load Diff
+965 -220
View File
File diff suppressed because it is too large Load Diff
+3070 -591
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!
-- 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
--- @diagnostic disable
@@ -13,7 +70,13 @@ local STBRP_SORT = table.sort
local STBRP_ASSERT = assert
local STBRP__NOTUSED = function(_) end
-------------------------------------------------------------------------------
--
-- INCLUDE SECTION
--
local STBRP__MAXVAL = 0x7fffffff
-- Mostly for internal use, but this is the maximum supported coordinate value.
--- @enum STBRP_HEURISTIC_Skyline
local STBRP_HEURISTIC_Skyline = {
@@ -44,12 +107,17 @@ local STBRP__PREV_LINK_TYPE = {
--- @nodiscard
local function stbrp_rect()
return {
-- reserved for your use:
id = 0,
-- input:
w = 0,
h = 0,
-- output:
x = 0,
y = 0,
was_packed = 0
was_packed = 0 -- non-zero if valid packing
}
end
@@ -92,7 +160,7 @@ local function stbrp_context()
active_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
@@ -113,13 +181,14 @@ local function stbrp__findresult()
}
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 heuristic int
local function stbrp_setup_heuristic(context, heuristic)
@@ -131,16 +200,49 @@ local function stbrp_setup_heuristic(context, heuristic)
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 allow_out_of_mem boolean
local function stbrp_setup_allow_out_of_mem(context, allow_out_of_mem)
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
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
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 width int
--- @param height int
@@ -160,6 +262,7 @@ function stbrp_init_target(context, width, height, nodes, num_nodes)
context.num_nodes = num_nodes
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].y = 0
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
end
-- find minimum y position if it starts at x1
-- Returns (min_y, waste_area)
--- @param c stbrp_context
--- @param first stbrp_node
--- @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(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)
@@ -190,14 +295,19 @@ local function stbrp__skyline_find_min_y(c, first, x0, width)
local visited_width = 0
while node.x < x1 do
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)
min_y = node.y
-- the first time through, visited_width might be reduced
if node.x < x0 then
visited_width = visited_width + node.next.x - x0
else
visited_width = visited_width + node.next.x - node.x
end
else
-- add waste area
local under_width = node.next.x - node.x
if under_width + visited_width > width then
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_type = nil
-- align to multiple of c.align
width = width + c.align - 1
width = width - width % c.align
STBRP_ASSERT(width % c.align == 0)
-- if it can't possibly fit, bail immediately
if width > c.width or height > c.height then
fr.prev_link = 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
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
best_y = y
best_prev_link = prev_link
@@ -250,7 +363,9 @@ local function stbrp__skyline_find_best_pos(c, width, height)
best_x = node.x
end
else
-- best-fit
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
best_y = y
best_waste = waste
@@ -266,12 +381,30 @@ local function stbrp__skyline_find_best_pos(c, width, height)
node = node.next
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
local tail = c.active_head
local node_scan = c.active_head
local prev_link_scan = c
local prev_link_scan_type = STBRP__PREV_LINK_TYPE.ACTIVE_HEAD
-- find first node that's admissible
while tail and tail.x < width do
tail = tail.next
end
@@ -280,6 +413,7 @@ local function stbrp__skyline_find_best_pos(c, width, height)
local xpos = tail.x - width
STBRP_ASSERT(xpos >= 0)
-- find the left position that matches this
while node_scan.next and node_scan.next.x <= xpos do
prev_link_scan = node_scan
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 or waste < best_waste or (waste == best_waste and xpos < best_x) then
best_x = xpos
-- STBRP_ASSERT(y <= best_y); [DEAR IMGUI]
best_y = y
best_waste = waste
best_prev_link = prev_link_scan
@@ -320,19 +455,29 @@ end
--- @param height int
--- @return stbrp__findresult
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)
-- 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
res.prev_link = nil
res.prev_link_type = nil
return res
end
-- on success, create new node
local node = context.free_head
node.x = res.x
node.y = res.y + height
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
if res.prev_link_type == STBRP__PREV_LINK_TYPE.ACTIVE_HEAD then
cur = res.prev_link.active_head
@@ -341,6 +486,7 @@ local function stbrp__skyline_pack_rectangle(context, width, height)
end
if cur.x < res.x then
-- preserve the existing one, so start testing with the next one
local next = cur.next
cur.next = node
cur = next
@@ -352,13 +498,17 @@ local function stbrp__skyline_pack_rectangle(context, width, height)
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
local next = cur.next
-- move the current node to the free list
cur.next = context.free_head
context.free_head = cur
cur = next
end
-- stitch the list back in
node.next = cur
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
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 rects stbrp_rect[]
--- @param num_rects int
@@ -397,15 +570,17 @@ end
local function stbrp_pack_rects(context, rects, num_rects)
local all_rects_packed = 1
-- we use the 'was_packed' field internally to allow sorting/unsorting
for i = 1, num_rects do
rects[i].was_packed = (i - 1)
end
-- sort according to heuristic
STBRP_SORT(rects, rect_height_compare)
for i = 1, num_rects do
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
else
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
-- unsort
STBRP_SORT(rects, rect_original_order)
-- set was_packed flags and all_rects_packed status
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
if rects[i].was_packed == 0 then
@@ -428,6 +605,7 @@ local function stbrp_pack_rects(context, rects, num_rects)
end
end
-- return the all_rects_packed status
return all_rects_packed
end
@@ -441,3 +619,43 @@ return {
setup_allow_out_of_mem = stbrp_setup_allow_out_of_mem,
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
-- 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!
-- Character Indexing:
-- 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_GETWIDTH = ImStb.TEXTEDIT_GETWIDTH
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_GETPREVCHARINDEX = ImStb.TEXTEDIT_GETPREVCHARINDEX
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_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_RIGHT = 0x200001 -- keyboard input to move cursor right
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_LINESTART = 0x200004 -- keyboard input to move cursor to start of line
STB_TEXTEDIT_K_LINEEND = 0x200005 -- keyboard input to move cursor to end of line
STB_TEXTEDIT_K_TEXTSTART = 0x200006 -- keyboard input to move cursor to start of text
STB_TEXTEDIT_K_TEXTEND = 0x200007 -- keyboard input to move cursor to end of text
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 -- e.g. END
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 -- e.g. ctrl-END
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_UNDO = 0x20000A -- keyboard input to perform undo
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_WORDRIGHT = 0x20000D -- keyboard input to move cursor right 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 -- e.g. ctrl-RIGHT
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_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
local IMSTB_TEXTEDIT_UNDOCHARCOUNT = 999
IMSTB_TEXTEDIT_UNDOSTATECOUNT = 99 -- the number of undo states to allow
local IMSTB_TEXTEDIT_UNDOCHARCOUNT = 999 -- the number of characters to store in the undo buffer
local IMSTB_TEXTEDIT_memmove = ImStb.TEXTEDIT_memmove
-- forward declarations
local stb_text_undo
local stb_text_redo
----------------------------------------------------------------
----------------------------------------------------------------
-------------------------------------------------------------------------------
-------------------------------------------------------------------------------
---
--- STB_TexteditState
---
@@ -72,6 +292,7 @@ local stb_text_redo
--- @nodiscard
local function StbUndoRecord()
return {
-- private data
where = 1,
insert_length = 0,
delete_length = 0,
@@ -101,6 +322,7 @@ local function StbUndoState()
end
return {
-- private data
undo_rec = undo_rec,
undo_char = undo_char,
@@ -131,16 +353,34 @@ end
--- @nodiscard
function STB_TexteditState()
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,
-- 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,
-- 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,
-- 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,
has_preferred_x = false,
single_line = false,
@@ -148,7 +388,7 @@ function STB_TexteditState()
padding2 = 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()
}
@@ -159,12 +399,20 @@ local function STB_TEXT_HAS_SELECTION(s)
return s.select_start ~= s.select_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
--- @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 baseline_y_delta float # position of baseline relative to previous row's baseline
--- @field ymin float # height of row above baseline
--- @field ymax float # height of row below baseline
--- @field ymin float # height of row above and below baseline
--- @field ymax float # height of row above and below baseline
--- @field num_chars int
--- @return StbTexteditRow
@@ -196,10 +444,15 @@ end
----------------------------------------
----------------------------------------
---
--- Implementation
--- Implementation mode
---
---
-------------------------------------------------------------------------------
---
--- Mouse input handling
---
local stb_text_locate_coord do
-- only create once, reuse later
@@ -351,6 +604,7 @@ end
---
---
-- forward declarations
local stb_text_makeundo_insert
local stb_text_makeundo_delete
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 first
-- special case if it's at the end (may not be needed?)
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)
find.y = 0
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
break
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
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
break
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 -- [DEAR IMGUI]
end
prev_start = i
i = i + r.num_chars
find.y = find.y + r.baseline_y_delta
if i == z + 1 then -- [IMGUI]
r.num_chars = 0
break
if i == z + 1 then -- [DEAR IMGUI]
r.num_chars = 0 -- [DEAR IMGUI]
break -- [DEAR IMGUI]
end
end
@@ -479,7 +733,7 @@ local function stb_textedit_delete(str, state, where, len)
state.has_preferred_x = false
end
-- delete the selection
-- delete the section
--- @param str IMSTB_TEXTEDIT_STRING
--- @param state STB_TexteditState
local function stb_textedit_delete_selection(str, state)
@@ -498,7 +752,7 @@ local function stb_textedit_delete_selection(str, state)
end
end
-- canonicalize the selection so start <= end
-- canoncialize the selection so start <= end
--- @param state STB_TexteditState
local function stb_textedit_sortselection(state)
if state.select_end < state.select_start then
@@ -579,7 +833,7 @@ local function stb_textedit_paste_internal(str, state, text, len)
end
-- 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 state STB_TexteditState
--- @param text IMSTB_TEXTEDIT_CHARTYPE[]
@@ -732,7 +986,7 @@ function stb_textedit_key(str, state, key)
break
end
-- [IMGUI]
-- [DEAR IMGUI]
-- 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
-- break
@@ -929,8 +1183,8 @@ end
-----------------------------------------------------
---
--- Undo processing
--- OPTIMIZE: the undo/redo buffer should be circular
---
--- @OPTIMIZE: the undo/redo buffer should be circular
--- @param state StbUndoState
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)
for i = 1, state.undo_point do
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
@@ -983,7 +1237,7 @@ local function stb_textedit_discard_redo(state)
end
-- 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
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 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.insert_length = r.delete_length
u.where = r.where
@@ -1137,6 +1394,7 @@ function stb_text_redo(str, state)
if r.delete_length > 0 then
-- the redo record requires us to delete characters, so the undo record
-- needs to store the characters
if s.undo_char_point + u.insert_length > s.redo_char_point then
u.insert_length = 0
u.delete_length = 0
@@ -1144,6 +1402,7 @@ function stb_text_redo(str, state)
u.char_storage = s.undo_char_point
s.undo_char_point = s.undo_char_point + u.insert_length
-- now save the characters
for i = 0, u.insert_length - 1 do
s.undo_char[u.char_storage + i] = STB_TEXTEDIT_GETCHAR(str, u.where + i)
end
@@ -1153,6 +1412,7 @@ function stb_text_redo(str, state)
end
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)
s.redo_char_point = s.redo_char_point + r.insert_length
end
@@ -1199,6 +1459,7 @@ function stb_text_makeundo_replace(str, state, where, old_length, new_length)
end
end
-- reset the state to default
--- @param state STB_TexteditState
--- @param is_single_line bool
local function stb_textedit_clear_state(state, is_single_line)
@@ -1222,7 +1483,7 @@ return {
click = stb_textedit_click,
drag = stb_textedit_drag,
createundo = stb_text_createundo,
initialize_state = stb_textedit_clear_state,
initialize_state = stb_textedit_clear_state, -- API initialize
text = stb_textedit_text,
key = stb_textedit_key,
@@ -1236,3 +1497,43 @@ return {
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
-- 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
local stbtt_fontinfo
@@ -21,7 +179,7 @@ local stbtt_FindGlyphIndex
local STBTT_MAX_OVERSAMPLE = 8
--- @enum STBTT_PLATFORM_ID
local STBTT_PLATFORM_ID = {
local STBTT_PLATFORM_ID = { -- platformID
UNICODE = 0,
MAC = 1,
ISO = 2,
@@ -29,7 +187,7 @@ local STBTT_PLATFORM_ID = {
}
--- @enum STBTT_MS_EID
local STBTT_MS_EID = {
local STBTT_MS_EID = { -- encodingID for STBTT_PLATFORM_ID_MICROSOFT
SYMBOL = 0,
UNICODE_BMP = 1,
SHIFTJIS = 2,
@@ -75,6 +233,7 @@ local stbtt_int8 = ImS8
local stbtt_uint8 = ImU8
local unsigned_char = stbtt_uint8
-- private structure
--- @class stbtt__buf
--- @field data? table # 1-based byte table
--- @field offset int # Every access to `data` will be offset by this
@@ -93,6 +252,8 @@ local function stbtt__buf(initial_offset)
}
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
--- @field userdata any
--- @field data table # pointer to .ttf file
@@ -283,6 +444,7 @@ local function stbtt__new_active(e, off_x, start_point)
return z
end
-- @TODO: don't expose this structure
--- @class stbtt__bitmap
--- @field w 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__isfont(font)
if stbtt_tag4(font, 49, 0, 0, 0) then return true end
if stbtt_tag(font, "typ1") then return true end
if stbtt_tag(font, "OTTO") then return true end
if stbtt_tag4(font, 0, 1, 0, 0) then return true end
if stbtt_tag(font, "true") then return true end
-- check the version number
if stbtt_tag4(font, 49, 0, 0, 0) then return true end -- TrueType 1
if stbtt_tag(font, "typ1") then return true end -- TrueType with type 1 font -- we don't support this!
if stbtt_tag(font, "OTTO") then return true end -- OpenType with CFF
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
end
-- @OPTIMIZE: binary search
local function stbtt__find_table(data, font_start, tag)
local num_tables = ttUSHORT(data, font_start + 4)
local tabledir = font_start + 12
@@ -556,11 +720,14 @@ local function stbtt__find_table(data, font_start, tag)
end
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 index == 0 then return 0 else return -1 end
end
-- check if it's a TTC
if stbtt_tag(font_collection, "ttcf") then
-- version 1?
if ttULONG(font_collection, 4) == 0x00010000 or ttULONG(font_collection, 4) == 0x00020000 then
local n = ttLONG(font_collection, 8)
if index >= n then
@@ -598,23 +765,25 @@ local function stbtt_InitFont_internal(info, data, fontstart)
info.fontstart = fontstart
info.cff = stbtt__new_buf(nil, 0)
cmap = stbtt__find_table(data, fontstart, "cmap")
info.loca = stbtt__find_table(data, fontstart, "loca")
info.head = stbtt__find_table(data, fontstart, "head")
info.glyf = stbtt__find_table(data, fontstart, "glyf")
info.hhea = stbtt__find_table(data, fontstart, "hhea")
info.hmtx = stbtt__find_table(data, fontstart, "hmtx")
info.kern = stbtt__find_table(data, fontstart, "kern")
info.gpos = stbtt__find_table(data, fontstart, "GPOS")
cmap = stbtt__find_table(data, fontstart, "cmap") -- required
info.loca = stbtt__find_table(data, fontstart, "loca") -- required
info.head = stbtt__find_table(data, fontstart, "head") -- required
info.glyf = stbtt__find_table(data, fontstart, "glyf") -- required
info.hhea = stbtt__find_table(data, fontstart, "hhea") -- required
info.hmtx = stbtt__find_table(data, fontstart, "hmtx") -- required
info.kern = stbtt__find_table(data, fontstart, "kern") -- not required
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
return false
end
if info.glyf ~= 0 then
-- required for truetype
if info.loca == 0 then
return false
end
else
-- initialization for CFF / Type2 fonts (OTF)
local cff = stbtt__find_table(data, fontstart, "CFF ")
if cff == 0 then
return false
@@ -623,14 +792,16 @@ local function stbtt_InitFont_internal(info, data, fontstart)
info.fontdicts = 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
-- read the header
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
local topdictidx = stbtt__cff_get_index(b)
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)
info.subrs = stbtt__get_subrs(b, topdict)
-- we only support Type 2 charstrings
if cstype[1] ~= 2 then
return false
end
@@ -651,6 +823,7 @@ local function stbtt_InitFont_internal(info, data, fontstart)
end
if fdarrayoff[1] ~= 0 then
-- looks like a CID font
if fdselectoff[1] == 0 then
return false
end
@@ -673,19 +846,26 @@ local function stbtt_InitFont_internal(info, data, fontstart)
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)
info.index_map = 0
for i = 0, numTables - 1 do
local encoding_record = cmap + 4 + 8 * i
-- find an encoding we understand:
local platform_id = ttUSHORT(data, encoding_record)
if platform_id == STBTT_PLATFORM_ID.MICROSOFT then
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
-- MS/Unicode
info.index_map = cmap + ttULONG(data, encoding_record + 4)
end
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)
end
end
@@ -697,6 +877,11 @@ local function stbtt_InitFont_internal(info, data, fontstart)
return true
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 unicode_codepoint int
function stbtt_FindGlyphIndex(info, unicode_codepoint)
@@ -704,7 +889,7 @@ function stbtt_FindGlyphIndex(info, unicode_codepoint)
local index_map = info.index_map
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)
if unicode_codepoint < bytes - 6 then
return ttBYTE(data, index_map + 6 + unicode_codepoint)
@@ -718,14 +903,15 @@ function stbtt_FindGlyphIndex(info, unicode_codepoint)
end
return 0
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
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 searchRange = bit32.rshift(ttUSHORT(data, index_map + 8), 1)
local entrySelector = ttUSHORT(data, index_map + 10)
local rangeShift = bit32.rshift(ttUSHORT(data, index_map + 12), 1)
-- do a binary search of the segments
local endCount = index_map + 14
local search = endCount
@@ -733,10 +919,13 @@ function stbtt_FindGlyphIndex(info, unicode_codepoint)
return 0
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
search = search + rangeShift * 2
end
-- now decrement to bias correctly to find smallest
search = search - 2
while entrySelector ~= 0 do
searchRange = bit32.rshift(searchRange, 1)
@@ -768,8 +957,9 @@ function stbtt_FindGlyphIndex(info, unicode_codepoint)
local ngroups = ttULONG(data, index_map + 12)
local low = 0
local high = ngroups
-- Binary search the right group.
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 end_char = ttULONG(data, index_map + 16 + mid * 12 + 4)
if unicode_codepoint < start_char then
@@ -785,10 +975,10 @@ function stbtt_FindGlyphIndex(info, unicode_codepoint)
end
end
end
return 0
return 0 -- not found
end
-- TODO:
-- @TODO
STBTT_assert(false)
return 0
end
@@ -810,8 +1000,8 @@ end
local function stbtt__GetGlyfOffset(info, glyph_index)
STBTT_assert(info.cff.size == 0)
if glyph_index >= info.numGlyphs then return -1 end
if info.indexToLocFormat >= 2 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 -- unknown index->glyph map format
local g1, g2
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)
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
-- Gets the bounding box of the visible part of the glyph, in unscaled coordinates
--- @return integer, integer?, integer?, integer?, integer?
local function stbtt_GetGlyphBox(info, glyph_index)
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 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 = {}
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 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
local flags = 0
@@ -934,7 +1129,7 @@ local function stbtt__GetGlyphShapeTT(info, glyph_index)
if bit32.band(flags, 2) ~= 0 then
local dx = data[points]
points = points + 1
if bit32.band(flags, 16) ~= 0 then
if bit32.band(flags, 16) ~= 0 then -- ???
x = x + dx
else
x = x - dx
@@ -956,7 +1151,7 @@ local function stbtt__GetGlyphShapeTT(info, glyph_index)
if bit32.band(flags, 4) ~= 0 then
local dy = data[points]
points = points + 1
if bit32.band(flags, 32) ~= 0 then
if bit32.band(flags, 32) ~= 0 then -- ???
y = y + dy
else
y = y - dy
@@ -987,15 +1182,19 @@ local function stbtt__GetGlyphShapeTT(info, glyph_index)
-- now start the new one
start_off = (bit32.band(flags, 1) == 0)
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
scy = y
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)
sy = bit32.rshift(y + stbtt_int32(vertices[off + i + 1].y), 1)
else
-- otherwise just use the next point as our start point
sx = stbtt_int32(vertices[off + i + 1].x)
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
else
sx = x
@@ -1007,8 +1206,8 @@ local function stbtt__GetGlyphShapeTT(info, glyph_index)
next_move = 1 + ttUSHORT(data, endPtsOfContours + j * 2)
j = j + 1
else
if bit32.band(flags, 1) == 0 then
if was_off then
if bit32.band(flags, 1) == 0 then -- if it's a curve
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)
num_vertices = num_vertices + 1
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)
elseif numberOfContours < 0 then
-- Compound shapes.
local more = 1
local comp = g + 10
num_vertices = 0
@@ -1047,8 +1247,8 @@ local function stbtt__GetGlyphShapeTT(info, glyph_index)
flags = ttSHORT(data, comp) comp = comp + 2
gidx = ttSHORT(data, comp) comp = comp + 2
if bit32.band(flags, 2) ~= 0 then
if bit32.band(flags, 1) ~= 0 then
if bit32.band(flags, 2) ~= 0 then -- XY values
if bit32.band(flags, 1) ~= 0 then -- shorts
mtx[5] = ttSHORT(data, comp) comp = comp + 2
mtx[6] = ttSHORT(data, comp) comp = comp + 2
else
@@ -1056,7 +1256,7 @@ local function stbtt__GetGlyphShapeTT(info, glyph_index)
mtx[6] = ttCHAR(data, comp) comp = comp + 1
end
else
-- TODO: handle matching point
-- @TODO handle matching point
STBTT_assert(false)
end
@@ -1083,11 +1283,14 @@ local function stbtt__GetGlyphShapeTT(info, glyph_index)
mtx[4] = ttSHORT(data, comp) / 16384.0 comp = comp + 2
end
-- Find transformation scales.
m = STBTT_sqrt(mtx[1] * mtx[1] + mtx[2] * mtx[2])
n = STBTT_sqrt(mtx[3] * mtx[3] + mtx[4] * mtx[4])
-- Get indexed glyph.
comp_num_vertices, comp_verts = stbtt_GetGlyphShape(info, gidx)
if comp_num_vertices > 0 then
-- Transform vertices.
for i = 1, comp_num_vertices do
local v = comp_verts[i]
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]))
end
-- Append vertices.
if num_vertices == 0 then -- LUA: No malloc
vertices = comp_verts
else
@@ -1111,6 +1315,7 @@ local function stbtt__GetGlyphShapeTT(info, glyph_index)
num_vertices = num_vertices + comp_num_vertices
end
-- More components ?
more = bit32.band(flags, bit32.lshift(1, 5))
end
end
@@ -1266,7 +1471,7 @@ local function stbtt__run_charstring(info, glyph_index, c)
clear_stack = 1
b0 = stbtt__buf_get8(b)
-- TODO: implement hinting
-- @TODO implement hinting
if b0 == 0x13 or b0 == 0x14 then -- hintmask or cntrmask
if in_header then
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
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 sp < 7 then return STBTT__CSERR("hflex stack") end
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
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 glyph_index int
function stbtt_GetGlyphShape(info, glyph_index)
@@ -1557,6 +1773,9 @@ function stbtt_GetGlyphShape(info, glyph_index)
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 glyph_index int
function stbtt_GetGlyphHMetrics(info, glyph_index)
@@ -1574,6 +1793,12 @@ function stbtt_GetGlyphHMetrics(info, glyph_index)
return advanceWidth, leftSideBearing
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
function stbtt_GetFontVMetrics(info)
local ascent = ttSHORT(info.data, info.hhea + 4)
@@ -1583,6 +1808,12 @@ function stbtt_GetFontVMetrics(info)
return ascent, descent, lineGap
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 height float
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 glyph int
--- @param scale_x float
@@ -1624,6 +1857,11 @@ function stbtt_GetGlyphBitmapBoxSubpixel(font, glyph, scale_x, scale_y, shift_x,
return ix0, iy0, ix1, iy1
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 glyph int
--- @param scale_x float
@@ -1646,6 +1884,8 @@ end
--- stbtt_MakeGlyphBitmap
--- 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 x int
--- @param e stbtt__active_edge
@@ -1687,7 +1927,7 @@ local function stbtt__handle_clipped_edge(scanline, x, e, x0, y0, x1, y1)
-- do nothing
else
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
@@ -1725,6 +1965,7 @@ local function stbtt__fill_active_edges_new(scanline, scanline_fill, len, e, y_t
while e do
-- brute force every pixel
-- compute intersection points with top & bottom
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
-- from here on, we don't have to range check x values
if trunc(x_top) == trunc(x_bottom) then
-- simple case, only spans one pixel
local height
-- simple case, only spans one pixel
local x = trunc(x_top)
height = (sy1 - sy0) * e.direction
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
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
-- TODO: maybe test against sy1 rather than y_bottom?
-- @TODO: maybe test against sy1 rather than y_bottom?
if y_crossing > y_bottom then
y_crossing = y_bottom
end
@@ -1824,6 +2082,16 @@ local function stbtt__fill_active_edges_new(scanline, scanline_fill, len, e, y_t
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,
-- 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
@@ -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)
end
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
-- 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
local y0 = y_top
local x1 = x
@@ -1887,6 +2175,7 @@ local function stbtt__fill_active_edges_new(scanline, scanline_fill, len, e, y_t
end
end
-- directly AA rasterize edges w/o supersampling
--- @param result stbtt__bitmap
--- @param e stbtt__edge
--- @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 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 curr = active
while curr do
local next = curr.next
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
curr.direction = 0
else
prev = curr
prev = curr -- advance through list
end
curr = next
end
-- insert all edges that start before the bottom of this scanline
local edge = e[p]
while edge.y0 <= scan_y_bottom do
if edge.y0 ~= edge.y1 then
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
-- this can happen due to subpixel positioning and some kind of fp rounding error i think
z.ey = scan_y_top
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
active = z
end
@@ -1943,6 +2237,7 @@ local function stbtt__rasterize_sorted_edges(result, e, n, vsubsample, off_x, of
edge = e[p]
end
-- now process all active edges
if active then
stbtt__fill_active_edges_new(scanline, scanline2, result.w, active, scan_y_top)
end
@@ -1961,10 +2256,11 @@ local function stbtt__rasterize_sorted_edges(result, e, n, vsubsample, off_x, of
end
end
-- advance all the edges
step = active
while step do
step.fx = step.fx + step.fdx
step = step.next
step.fx = step.fx + step.fdx -- advance to position for current scanline
step = step.next -- advance through list
end
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 y_scale_inv = (invert) and -scale_y or scale_y
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
local n = 0
@@ -2066,8 +2363,7 @@ local function stbtt__add_point(points, n, x, y)
points[n + 1].y = y
end
-- tessellate until threshold p is happy...
-- TODO: warped to compensate for non-linear stretching
-- tessellate until threshold p is happy... @TODO warped to compensate for non-linear stretching
--- @param points? stbtt__point[]
--- @param num_points int
--- @param x0 float
@@ -2116,7 +2412,7 @@ end
--- @param n int
--- @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)
-- 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 dy0 = y1 - y0
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
end
-- returns number of contours
--- @param vertices stbtt_vertex[]
--- @param num_verts int
--- @param objspace_flatness float
@@ -2235,6 +2532,7 @@ local function stbtt_FlattenCurves(vertices, num_verts, objspace_flatness)
return points, num_contours, contour_lengths
end
-- rasterize a shape with quadratic beziers into a bitmap
--- @param result stbtt__bitmap
--- @param flatness_in_pixels float
--- @param vertices stbtt_vertex[]
@@ -2258,6 +2556,11 @@ local function stbtt_Rasterize(result, flatness_in_pixels, vertices, num_verts,
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 output unsigned_char[]
--- @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
return 0.0
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)
end
-- same as stbtt_MakeGlyphBitmapSubpixel, but prefiltering
-- is performed. Returns (sub_x, sub_y).
--- @param info stbtt_fontinfo
--- @param output unsigned_char[]
--- @param out_w int
@@ -2415,7 +2725,7 @@ end
-------------------
-------------------
---
--- SDF computation
--- sdf computation
---
---
@@ -2441,10 +2751,20 @@ end
--- stbtt__matchpair
--- 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 index int
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 data table
--- @param offset int
+31 -14
View File
@@ -3,7 +3,8 @@
-- Supports:
-- - %x, %X, %nx, %nX (where n is a number)
-- - %d, %nd (where n is a number)
-- - %d, %nd, %u, %nu (where n is a number)
-- - length modifiers h, hh, l, ll, I64 on integers: ll/I64 = 64-bit range (doubles: exact only within +-2^53)
-- - %f, %lf (very limited)
--
-- Returns items_matched instead of EOF on error
@@ -17,6 +18,9 @@ local CHAR_0, CHAR_9 = _byte'0', _byte'9'
local CHAR_a, CHAR_A = _byte'a', _byte'A'
local CHAR_d = _byte'd'
local CHAR_l = _byte'l'
local CHAR_h = _byte'h'
local CHAR_u = _byte'u'
local CHAR_I = _byte'I'
local CHAR_f, CHAR_F = _byte'f', _byte'F'
local CHAR_x, CHAR_X = _byte'x', _byte'X'
@@ -37,10 +41,15 @@ local function hex_digit_to_int(c)
end
-- we force this behavior by default
local function clamp_if_overflow(spec, val)
local function clamp_if_overflow(spec, val, is_64)
if spec == CHAR_d or spec == CHAR_x or spec == CHAR_X then
if val > INT_MAX then return INT_MAX end
if val < INT_MIN then return INT_MIN end
local max = is_64 and 9223372036854775807 or INT_MAX
local min = is_64 and -9223372036854775808 or INT_MIN
if val > max then return max end
if val < min then return min end
elseif spec == CHAR_u then
local max = is_64 and 18446744073709551615 or UINT_MAX
if val > max then return max end
end
return val
@@ -104,22 +113,27 @@ local function sscanf(buffer, buffer_begin, format, result)
width = math.huge
end
-- length modifiers
local n_l = 0
while _byte(format, f, f) == CHAR_l or _byte(format, f, f) == CHAR_h do
if _byte(format, f, f) == CHAR_l then n_l = n_l + 1 end
f = f + 1
end
if _byte(format, f, f) == CHAR_I and string.sub(format, f, f + 2) == "I64" then
n_l = 2
f = f + 3
end
local is_64 = n_l >= 2
local spec = _byte(format, f, f)
local base
local is_float = false
if spec == CHAR_x or spec == CHAR_X then
base = 16
elseif spec == CHAR_d then
elseif spec == CHAR_d or spec == CHAR_u then
base = 10
elseif spec == CHAR_f or spec == CHAR_F then
is_float = true
elseif spec == CHAR_l then
f = f + 1
if _byte(format, f, f) == CHAR_f then
is_float = true -- TODO: distinguish from float?
else
return items_matched
end
is_float = true -- TODO: distinguish %lf from %f?
else
return items_matched
end
@@ -197,11 +211,14 @@ local function sscanf(buffer, buffer_begin, format, result)
val = val * base + sign * hex_digit_to_int(c)
local old_val = val
val = clamp_if_overflow(spec, val)
val = clamp_if_overflow(spec, val, is_64)
if old_val ~= val then
break
end
end
if spec == CHAR_u and val < 0 then
val = val + (is_64 and 18446744073709551616 or 4294967296) -- unsigned wrap-around like strtoul()
end
end
result[assigned + 1] = val
+225 -12
View File
@@ -6,15 +6,35 @@
# 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.
# 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
LOVE = False
if "--target" in sys.argv:
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"]
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"
SKIP = {"imgui_impl_roblox.lua", "imgui_impl_roblox_fonts.lua"} if LOVE else {"imgui_impl_love.lua"}
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
FILES[f] = src
ORIG = dict(FILES) # for gen_types
ENTRY = ["imgui.lua", BACKEND] + ([] if NO_DEMO else ["imgui_demo.lua"]) # run order
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"^([A-Za-z_]\w*)\s*=[^=]", src, re.M))
# ---- lexer helpers ----
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())
def lex(src):
"""[(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)",
" local r = __LOADED[name]",
" if r == nil then r = __FILES[name](); if r == nil then r = true end; __LOADED[name] = r end",
" return r",
"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), ""]
out += [
"local __FILES, __LOADED = {}, {}",
"function IM_INCLUDE(name)",
" local r = __LOADED[name]",
" if r == nil then r = __FILES[name](); if r == nil then r = true end; __LOADED[name] = r end",
" return r",
"end", ""]
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)
for name, src in sorted(FILES.items()):
out.append("__FILES[%r] = function()" % name)
@@ -57,11 +269,12 @@ for n in sorted(names):
out.append(" %s = %s," % (n, n))
out.append("}")
import sys
OUT = sys.argv[1] if len(sys.argv) > 1 else os.path.join(ROOT, "build")
os.makedirs(OUT, exist_ok=True)
NAME = "imgui_love.lua" if LOVE else "ImGui.luau"
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"))
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)))
+645
View File
@@ -0,0 +1,645 @@
#!/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
from fnmatch import fnmatchcase
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"}
# ---------------------------------------------------------------------------------------------------------------------
# Intentional Lua replacements: [=] rows, not counted as missing. (glob over the upstream key "Class::name" / "Struct::field",
# reason, regex that must match the port sources - if the port's mechanism disappears, the entry falls back to [ ] / missing).
# Only applies when no same-named port definition / field exists.
# ---------------------------------------------------------------------------------------------------------------------
REPLACED = [
# functions
("ImGuiStorage::*", "Lua table keyed by ID (e.g. window.StateStorage = {})", r"StateStorage\s*=\s*\{\}"),
("ImGuiStoragePair::*", "Lua table keyed by ID", r"StateStorage\s*=\s*\{\}"),
("PairComparerByID", "table.sort over Lua table keys", r"table\.sort\("),
("ImStr*", "Lua string library (string.find/sub/lower, immutable strings)", r"string\.find\("),
("ImMemdup", "Lua strings/tables are values, no copy needed", r"string\.find\("),
("ImFormatString*", "string.format", r"ImFormatString\s*=\s*string\.format"),
("ImText*Utf8*", "strings stay UTF-8 bytes: ImStd.ImTextCharFromUtf8 decoder + utf8 library", r"function ImStd\.ImTextCharFromUtf8"),
("MallocWrapper", "GC; IM_ALLOC = ImGui.MemAlloc", r"IM_ALLOC\s*=\s*ImGui\.MemAlloc"),
("FreeWrapper", "GC; IM_ALLOC = ImGui.MemAlloc", r"IM_ALLOC\s*=\s*ImGui\.MemAlloc"),
("ImGui::GetAllocatorFunctions", "GC, no allocator to query", r"IM_ALLOC\s*=\s*ImGui\.MemAlloc"),
("ImFileWrite", "no filesystem on Roblox: SaveIniSettingsToDisk is a no-op, use SaveIniSettingsToMemory", r"Roblox: no disk"),
("ImSwap", "Lua multiple assignment a, b = b, a", r"\b([\w.]+), ([\w.]+) = \2, \1\b"),
("ImGui::DebugCheckVersionAndDataLayout", "C++ ABI / sizeof check", None),
("TableSettingsCalcChunkSize", "sizeof; table settings are a plain ImVector", r"SettingsTables\s*=\s*ImVector\(\)"),
("ImVector::size", "v.Size field", r"function IM_VECTOR:"),
("ImVector::capacity", "v.Capacity field", r"function IM_VECTOR:"),
("ImVector::front", "v.Data[1]", r"function IM_VECTOR:"),
("ImVector::begin", "v.Data / v:iter()", r"function IM_VECTOR:iter\("),
("ImVector::end", "v.Data / v:iter()", r"function IM_VECTOR:iter\("),
("ImVector::size_in_bytes", "sizeof", None),
("ImVector::max_size", "sizeof", None),
("ImVector::clear_destruct", "GC: clear_delete / clear", r"function IM_VECTOR:clear_delete\("),
("ImVector::find", "find_index (no element pointers)", r"function IM_VECTOR:find_index\("),
("ImSpan::*", "plain Lua tables (e.g. table.Columns = {})", r"Columns = \{\}, ColumnsSize = 0"),
("ImSpanAllocator::*", "plain Lua tables, no arena", r"Columns = \{\}, ColumnsSize = 0"),
("ImStableVector::*", "ImVector (Lua references are stable)", r"BakedPool\s*=\s*ImVector\(\)"),
("ImChunkStream::*", "plain ImVector of settings tables", r"SettingsTables\s*=\s*ImVector\(\)"),
("ImPool::GetAliveCount", "pool.AliveCount field", r"\.AliveCount\)"),
("ImPool::GetBufSize", "pool.BufSize field / GetMapSize()", r"function IM_POOL:GetMapSize\(\) return self\.BufSize"),
("ImPool::TryGetMapData", "GetByIndex (removed slots hold false)", r"function IM_POOL:GetByIndex\("),
("ImGuiTextBuffer::begin", "chunk-list string builder: c_str()", r"function ImGuiTextBufferMT:c_str\("),
("ImGuiTextBuffer::end", "chunk-list string builder: c_str()", r"function ImGuiTextBufferMT:c_str\("),
("ImGuiTextBuffer::resize", "chunk-list string builder, no byte buffer", r"function ImGuiTextBufferMT:c_str\("),
("ImGuiTextBuffer::reserve", "chunk-list string builder, no byte buffer", r"function ImGuiTextBufferMT:c_str\("),
("ImGuiTextBuffer::appendfv", "appendf(fmt, ...) (Lua varargs)", r"function ImGuiTextBufferMT:appendf\("),
("ImGuiTextIndex::append", "debug log keeps a Lua line list (g.DebugLogLines)", r"g\.DebugLogLines\s*="),
# struct fields (ImSpan*/ImStableVector/ImGuiStoragePair fields: covered by the globs above)
("ImGuiTextBuffer::EmptyString", "c_str() of an empty chunk list", r"function ImGuiTextBufferMT:c_str\("),
("ImGuiWindow::NameBufLen", "window.Name is an immutable Lua string", None),
("ImGuiTextFilterItem::Begin", "item.Text substring", r"Text = string\.sub\(buf, b, e - 1\)"),
("ImGuiTextFilterItem::Len", "item.Text substring", r"Text = string\.sub\(buf, b, e - 1\)"),
("ImDrawVert::uv", "vertices packed into VtxBuffer.Buf (VTX_STRIDE bytes per vertex)", r"VTX_STRIDE\s*=\s*\d+"),
("ImDrawVert::col", "vertices packed into VtxBuffer.Buf (VTX_STRIDE bytes per vertex)", r"VTX_STRIDE\s*=\s*\d+"),
("ImGuiContext::DebugLogIndex", "Lua line list g.DebugLogLines", r"g\.DebugLogLines\s*="),
("ImGuiContext::TempKeychordName", "GetKeyChordName returns a Lua string", r"\.\. \(\(key ~= ImGuiKey\.None"),
("ImGuiContext::DragDropPayloadBuf*", "payload.Data holds the Lua value", r"payload\.Data = data"),
("ImGuiTabBar::TabsNames", "names stored on the tab (tab.Name)", r"Tab names are stored on the tab itself"),
]
# ---------------------------------------------------------------------------------------------------------------------
# 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
CPP_MACROS = {"IM_MSVC_RUNTIME_CHECKS_OFF", "IM_MSVC_RUNTIME_CHECKS_RESTORE"} # argument-less macro lines, no trailing ';'
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:
kw = max(q for q, x in enumerate(pre) if x in ("struct", "class", "union")) # skips macro lines / template<...> before it
names = [x for x in pre[kw:] 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:]
while txt and txt[0] in CPP_MACROS: st, txt = st[1:], txt[1:]
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"):
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
if cls and cls.endswith("MT") and len(cls) > 2: cls = cls[:-2] # method tables: function ExampleAppLogMT:AddLog()
if cls is None and re.match(r"[A-Z]\w*_[A-Z]\w*$", name) and "__" not in name: # Class_Method globals (C++ static members)
c, _, m = name.partition("_")
defs.append(PortFunc(f, c, m, line, toks[body_start:j - 1]))
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}
port_class_files = {}
for d in defs:
if d.cls: port_class_files.setdefault(d.cls, set()).add(d.file)
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: # only a classless helper in the class's own file (local function _grow_capacity)
loose = [d for d in cands if d.cls is None and d.file in port_class_files[u.cls]] if len(cands) == 1 else []
return loose[0] if loose else 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
port_src = "\n".join(open(os.path.join(ROOT, f), encoding="utf-8").read() for f in PORT_FILES)
ev_ok = {ev: ev is None or re.search(ev, port_src) is not None for _, _, ev in REPLACED}
def replaced(key):
return next((why for pat, why, ev in REPLACED if ev_ok[ev] and fnmatchcase(key, pat)), None)
tot = {"n": 0, "have": 0, "stub": 0, "size": 0, "size_have": 0, "simw": 0.0, "size_sim": 0, "repl": 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, "repl": 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
p = find(u)
why = replaced(u.key) if p is None else None
if why:
st["repl"] += 1; rows.append((u.section, "[=]", "", u, None, why)); continue
st["n"] += 1; st["size"] += len(un)
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, field_repl = [], 0, 0, 0
for nm, (fl, line, sec) in sorted(structs_all.items()):
fl = [x for x in fl if not x[2]]
repl = [x[0] for x in fl if x[0] not in lua_fields and replaced("%s::%s" % (nm, x[0]))]
fl = [x for x in fl if x[0] not in repl]
if not fl and not repl: 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_repl += len(repl)
field_rows.append((nm, len(fl) - len(missing), len(fl), missing, repl))
api_rows, api_repl = [], 0
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]}
r = [k for k in d if k not in port_names and replaced("ImGui::" + k)]
api_repl += len(r)
for k in r: del d[k]
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 %9s" % ("upstream file", "funcs", "ported", "by size", "similarity", "stubs", "replaced"))
for f, s in summary:
head.append("%-22s %9d %9s %9s %11s %9d %9d" % (f, s["n"], pct(s["have"], s["n"]), pct(s["size_have"], s["size"]),
pct(s["simw"], s["size_sim"]), s["stub"], s["repl"]))
head.append("%-22s %9d %9s %9s %11s %9d %9d" % ("TOTAL", tot["n"], pct(tot["have"], tot["n"]), pct(tot["size_have"], tot["size"]),
pct(tot["simw"], tot["size_sim"]), tot["stub"], tot["repl"]))
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)))
head.append("%d intentionally replaced by Lua equivalents [=] (%d functions, %d API declarations, %d struct fields), not counted above"
% (tot["repl"] + api_repl + field_repl, tot["repl"], api_repl, field_repl))
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(" replaced = [=] rows: intentionally replaced by a Lua mechanism (REPLACED table), excluded from funcs")
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 [=] replaced by Lua equivalent (reason) [-] 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; [=] replacements excluded)")
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; [=] replaced by Lua equivalent, excluded from total). Field names are matched globally, so common names may over-count.")
L.append("=" * 100)
for nm, h, n, missing, repl in field_rows:
L.append(" %-40s %4d / %4d%s%s" % (nm, h, n, (" missing: " + ", ".join(missing)) if missing else "",
(" [=] " + ", ".join("%s (%s)" % (x, replaced("%s::%s" % (nm, x))) for x in repl)) if repl 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)))
+52
View File
@@ -0,0 +1,52 @@
#!/usr/bin/env bash
# All local checks: CI's "Check" step (release.yml) + every scripted test + whole demo forced open + LÖVE.
# Usage: tools/test/check_all.sh (prints PASS/FAIL per check, exit 1 if any failed)
# HASHES=1 tools/test/check_all.sh (also prints the draw-data checksums, see hashes.sh)
cd "$(dirname "$0")/../.."
export PATH="$PATH:/nix/store/2m2ja613nndihc8qm10f2rhq6znp1nh4-luau-0.734/bin:/nix/store/4qxq4arv32ly13kl3g0m5q80gm3a35xr-luajit-2.1.1741730670/bin"
T=$(mktemp -d /tmp/imgui_check.XXXX)
R="python3 tools/test/run.py --quiet --roblox --timeout 600"
# name | expected output | command
CHECKS=(
"readme|README OK|$R --main tools/test/scripts/t_readme.luau --frames 30"
"readme-min|README OK|MINIFY=1 $R --main tools/test/scripts/t_readme.luau --frames 30"
"cached|CACHED OK|$R --main tools/test/scripts/t_cached_window.luau --frames 130"
"fixes|FIXES OK|$R --main tools/test/scripts/t_fixes.luau --frames 55"
"debugtools|DEBUGTOOLS OK|$R --main tools/test/scripts/t_debugtools.luau --frames 65"
"helpers|HELPERS OK|$R --main tools/test/scripts/t_helpers.luau --frames 45"
"multiselect|MULTISELECT OK|$R --main tools/test/scripts/t_multiselect.luau --frames 380"
"nodemo|NODEMO OK|NO_DEMO=1 $R --main tools/test/scripts/t_nodemo.luau --frames 25"
"widgets|OK|$R --main tools/test/scripts/t_widgets.luau --frames 80"
"tables|OK|$R --main tools/test/scripts/t_tables.luau --frames 150"
"tabs|scrollbtn.true|$R --main tools/test/scripts/t_tabs.luau --frames 60"
"modal|round trip ok|$R --main tools/test/scripts/t_modal.luau --frames 40"
"dock|DOCK OK|$R --main tools/test/scripts/t_dock.luau --frames 60"
"dock_close|DOCK CLOSE OK|$R --main tools/test/scripts/t_dock_close.luau --frames 60"
"dock_drag|OK|$R --main tools/test/scripts/t_dock_drag.luau --frames 80"
"dock_ini|ROUNDTRIP OK|$R --main tools/test/scripts/t_dock_ini.luau --frames 60"
"dock_move|OK|$R --main tools/test/scripts/t_dock_move.luau --frames 80"
"passthru|PASSTHRU OK|$R --main tools/test/scripts/t_passthru.luau --frames 40"
"fonts|FONTS OK|$R --main tools/test/scripts/t_fonts.luau --frames 8"
"textproxy|PROXY OK|$R --main tools/test/scripts/t_textproxy.luau --frames 30"
"style_editor|SE OK|$R --main tools/test/scripts/t_style_editor.luau --frames 20"
"style_leak|STYLE OK|$R --main tools/test/scripts/t_style_leak.luau --frames 60 --open"
"apps|APPS OK|$R --main tools/test/scripts/t_apps.luau --frames 120 --open"
"libhash|LIBHASH|NO_DEMO=1 $R --main tools/test/scripts/t_libhash.luau --frames 120"
"demo_open|DRAWHASH|$R --main tools/test/scripts/t_drawhash.luau --frames 150 --open"
"demo_random|^|python3 tools/test/run.py --roblox --timeout 600 --frames 400 --open"
"pixels|PIXELS SAME|a=\$($R --main tools/test/scripts/t_pixels.luau --frames 600 | grep -P '^PIX\t'); b=\$(RUN_FLAGS=ARG_FORCE=true $R --main tools/test/scripts/t_pixels.luau --frames 600 | grep -P '^PIX\t'); [ -n \"\$a\" ] && [ \"\$a\" = \"\$b\" ] && echo PIXELS SAME"
"shot|wrote|python3 tools/test/shot.py /tmp/imgui_shot.png --app"
"love|DRAWHASH|python3 tools/bundle.py --target love >/dev/null && luajit tools/test/love/t_drawhash.lua 60 Widgets,Basic && luajit tools/test/love/t_examples.lua"
)
run() { # $1=name $2=expect $3=cmd
bash -c "$3" > "$T/$1.txt" 2>&1; code=$?
if [ $code -eq 0 ] && grep -q "$2" "$T/$1.txt" && ! grep -qiE "error|traceback|stack begin|FAIL" "$T/$1.txt"; then echo "PASS $1"
else echo "FAIL $1 (exit $code, log $T/$1.txt)"; tail -5 "$T/$1.txt" | sed 's/^/ /'; fi
}
for c in "${CHECKS[@]}"; do IFS='|' read -r n e cmd <<< "$c"; run "$n" "$e" "$cmd" & done > "$T/summary.txt"
wait
sort "$T/summary.txt"
[ -n "$HASHES" ] && tools/test/hashes.sh
! grep -q "^FAIL" "$T/summary.txt"
+23
View File
@@ -0,0 +1,23 @@
#!/usr/bin/env bash
# Draw-data checksums of the working tree vs a git rev (default HEAD), to prove a change renders identically.
# tools/test/hashes.sh [rev]
# lib = t_libhash (library only, no demo): must stay equal unless rendering is meant to change
# demo = t_drawhash with Widgets,Basic / whole demo open: changes with any demo edit
cd "$(dirname "$0")/../.."
export PATH="$PATH:/nix/store/2m2ja613nndihc8qm10f2rhq6znp1nh4-luau-0.734/bin:/nix/store/4qxq4arv32ly13kl3g0m5q80gm3a35xr-luajit-2.1.1741730670/bin"
REV=${1:-HEAD}
hashes() { # $1 = repo dir
( cd "$1"
R="python3 tools/test/run.py --quiet --roblox --timeout 600"
echo "lib $(NO_DEMO=1 $R --main "$OLDPWD/tools/test/scripts/t_libhash.luau" --frames 120 | grep -o 'LIBHASH.*')" &
echo "demo-basic $($R --main tools/test/scripts/t_drawhash.luau --frames 150 --open-only Widgets,Basic | grep -o 'DRAWHASH.*')" &
echo "demo-all $($R --main tools/test/scripts/t_drawhash.luau --frames 150 --open | grep -o 'DRAWHASH.*')" &
echo "love-basic $(python3 tools/bundle.py --target love >/dev/null; luajit tools/test/love/t_drawhash.lua 60 Widgets,Basic | grep -o 'DRAWHASH.*')" &
wait ) | tr "\t" " " | sort
}
W=$(mktemp -d /tmp/imgui_rev.XXXX)
git worktree add -q --detach "$W" "$REV"
hashes "$W" > "$W.old" & hashes . > "$W.new"; wait
git worktree remove --force "$W"
echo "check $REV working tree"
paste "$W.old" "$W.new" | awk -F'\t' '{split($1,a," "); split($2,b," "); printf "%-12s %-30s %-30s %s\n", a[1], a[3], b[3], (a[3]==b[3]?"same":"CHANGED")}'
+1 -1
View File
@@ -4,4 +4,4 @@ set -e
cd "$(dirname "$0")/../../.."
python3 tools/bundle.py --target love >/dev/null
nix-shell -p luajit --run "luajit tools/test/love/t_drawhash.lua 150 Widgets,Basic && luajit tools/test/love/t_drawhash.lua 150 all && luajit tools/test/love/t_examples.lua"
echo "expected (Luau, tools/test/scripts/t_drawhash.luau): 1385106188 / 1292122782"
echo "expected (Luau, tools/test/scripts/t_drawhash.luau): 154977593 / 263656315"
+2 -2
View File
@@ -1,5 +1,5 @@
-- Runs examples/example_love and examples/example_shared main.lua headless under LuaJIT (mock love) for some frames.
for _, ex in ipairs({ "examples/example_love", "examples/example_shared" }) do
-- Runs examples/example_love main.lua headless under LuaJIT (mock love) for some frames.
for _, ex in ipairs({ "examples/example_love" }) do
dofile("tools/test/love/mock_love.lua")
love.filesystem.getSource = function() return ex end
love.filesystem.load = function(p) return assert(loadfile(ex .. "/" .. p)) end
+3 -1
View File
@@ -16,6 +16,8 @@ G = "game, workspace, Instance, Enum, Vector2, Vector3, UDim2, Color3, Content,
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)
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 = {}"]
def add(key, path):
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)")
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:])
sys.exit(code)
+1 -1
View File
@@ -7,7 +7,7 @@ io.ConfigFlags = bit32.bor(io.ConfigFlags, G.ImGuiConfigFlags.DockingEnable)
G.ImGui_ImplRoblox.Init()
local names = { "ShowExampleAppConsole", "ShowExampleAppLog", "ShowExampleAppLayout", "ShowExampleAppPropertyEditor", "ShowExampleAppLongText",
"ShowExampleAppAutoResize", "ShowExampleAppConstrainedResize", "ShowExampleAppSimpleOverlay", "ShowExampleAppWindowTitles",
"ShowExampleAppCustomRendering", "ShowExampleAppDocuments", "ShowExampleAppDockSpace", "ShowExampleAppFullscreen", "ShowExampleAppAssetsBrowser", "ShowExampleAppMainMenuBar" }
"ShowExampleAppCustomRendering", "ShowExampleAppDocuments", "ShowExampleAppDockSpace", "ShowExampleAppFullscreen", "ShowExampleAppAssetsBrowser", "ShowExampleAppMainMenuBar", "ShowExampleAppImageViewer" }
local tools = { "ShowMetricsWindow", "ShowDebugLogWindow", "ShowIDStackToolWindow", "ShowAboutWindow" }
local open = {}
local frame = 0
+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)
+92
View File
@@ -0,0 +1,92 @@
-- Debug tools + misc imgui.cpp ports: item picker, DebugFlashStyleColor, DebugLocateItem, context hooks, AddInputCharactersUTF8, ImBezierCubicClosestPoint
local G = require(game:GetService("ReplicatedStorage"):WaitForChild("ImGui"))
local ImGui, ImVec2 = G.ImGui, G.ImVec2
local ctx = 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
-- ImBezierCubicClosestPoint: straight-line cubic (0,0)-(30,0)-(60,0)-(90,0): closest point to (45,10) is (45,0)
local p1, p2, p3, p4 = ImVec2(0, 0), ImVec2(30, 0), ImVec2(60, 0), ImVec2(90, 0)
local cp = G.ImBezierCubicClosestPoint(p1, p2, p3, p4, ImVec2(45, 10), 20)
check(math.abs(cp.x - 45) < 1e-3 and math.abs(cp.y) < 1e-6, ("ImBezierCubicClosestPoint %g,%g"):format(cp.x, cp.y))
-- Curved: result must lie on the curve (distance to a dense sampling of the curve ~0) and beat the endpoints
local q1, q2, q3, q4 = ImVec2(0, 0), ImVec2(0, 100), ImVec2(100, 100), ImVec2(100, 0)
local target = ImVec2(50, 120)
for _, r in { G.ImBezierCubicClosestPoint(q1, q2, q3, q4, target, 64), G.ImBezierCubicClosestPointCasteljau(q1, q2, q3, q4, target, 0.25) } do
check(math.abs(r.x - 50) < 0.5 and math.abs(r.y - 75) < 0.5, ("closest point on curve %g,%g (expected ~50,75)"):format(r.x, r.y))
end
-- AddInputCharactersUTF8 queues one Text event per codepoint
io:AddInputCharactersUTF8("a\u{e9}\u{20ac}\u{1F600}")
local chars = {}
for _, e in ctx.InputEventsQueue.Data do if e.Type == G.ImGuiInputEventType.Text then table.insert(chars, e.Text.Char) end end
-- (this build has IM_UNICODE_CODEPOINT_MAX = 0xFFFF like upstream's default ImWchar16: U+1F600 becomes IM_UNICODE_CODEPOINT_INVALID)
check(#chars == 4 and chars[1] == 0x61 and chars[2] == 0xE9 and chars[3] == 0x20AC and chars[4] == ((G.IM_UNICODE_CODEPOINT_MAX >= 0x1F600) and 0x1F600 or 0xFFFD), "AddInputCharactersUTF8 " .. table.concat(chars, ","))
-- Context hooks
local log = {}
local function hook(t, name)
local h = G.ImGuiContextHook(); h.Type = t; h.Callback = function(c, hk) table.insert(log, name) end
return ImGui.AddContextHook(ctx, h)
end
local T = G.ImGuiContextHookType
hook(T.NewFramePre, "nfpre"); hook(T.NewFramePost, "nfpost"); hook(T.RenderPre, "rpre"); local rpost = hook(T.RenderPost, "rpost")
-- Context defaults (ImGuiContext()/ImGuiIO() constructors, upstream values)
check(ctx.DebugFlashStyleColorIdx == G.ImGuiCol.COUNT and ctx.ActiveIdSource == G.ImGuiInputSource.None and io.MouseSource == G.ImGuiMouseSource.Mouse
and ctx.DebugBreakKeyChord == G.ImGuiKey.Pause and ctx.DebugItemPickerActive == false and ctx.DebugLocateId == 0 and ctx.CurrentPixelDensity == 1.0, "context defaults")
check(T.PendingRemoval_ == 7 and T.Shutdown == 6, "ImGuiContextHookType values")
check(io.BackendPlatformName == "imgui_impl_roblox" and io.BackendRendererName == "imgui_impl_roblox", "BackendPlatformName/BackendRendererName")
check((os.date == nil) == (ImGui.GetPlatformIO().Platform_SessionDate == 0), "Platform_SessionDate set from os.date() when available") -- (the test sandbox has no os.date)
ImGui.GetPlatformIO().Platform_SessionDate = 20261015 -- exercise the metrics Settings section date code
-- Style color flash
local col = ImGui.GetStyle().Colors[G.ImGuiCol.Button]
local orig = { col.x, col.y, col.z, col.w }
local f, rect, picked, broke, button_id = 0, nil, nil, 0, 0
ctx.DebugBreakCallback = function() broke += 1 end
game:GetService("RunService").Stepped:Connect(function()
f += 1
G.ImGui_ImplRoblox.NewFrame()
if rect and f == 5 then io:AddMousePosEvent((rect[1].x + rect[2].x) / 2, (rect[1].y + rect[2].y) / 2) end
if f == 6 then ImGui.DebugStartItemPicker() end
if f == 8 then io:AddMouseButtonEvent(0, true) elseif f == 9 then io:AddMouseButtonEvent(0, false) end
ImGui.NewFrame()
if f == 1 then check(table.concat(log, " ") == "nfpre nfpost", "hooks after NewFrame: " .. table.concat(log, " ")) end
if f == 8 then picked = ctx.DebugItemPickerBreakId end
if f == 3 then ImGui.DebugFlashStyleColor(G.ImGuiCol.Button) end
ImGui.SetNextWindowPos(ImVec2(10, 10), G.ImGuiCond.Always)
ImGui.Begin("DebugTools")
ImGui.Button("Target")
if f == 2 then local a, b = ImGui.GetItemRectMin(), ImGui.GetItemRectMax(); rect = { ImVec2(a.x, a.y), ImVec2(b.x, b.y) } end
local target_id = ctx.LastItemData.ID
button_id = target_id
if f == 12 then ImGui.DebugLocateItem(target_id) end
if f == 13 then check(ctx.DebugLocateId == 0, "DebugLocateItem resolved by ItemAdd()") end
ImGui.DebugDrawItemRect(); ImGui.DebugDrawCursorPos(); ImGui.DebugDrawLineExtents()
ImGui.DebugTextUnformattedWithLocateItem(("id 0x%08X here"):format(target_id))
ImGui.End()
if f == 3 then -- open the metrics Settings/InputText sections
local mw = ImGui.FindWindowByName("Dear ImGui Metrics/Debugger")
for _, lbl in { "Settings", "SettingsWindows", "SettingsTables", "InputText" } do mw.DC.StateStorage[mw:GetID(lbl)] = true end
ctx.DebugMetricsConfig.SettingsHighlightOldEntries = true
end
ImGui.SetNextWindowPos(ImVec2(400, 10), G.ImGuiCond.Always); ImGui.ShowMetricsWindow()
ImGui.SetNextWindowPos(ImVec2(400, 300), G.ImGuiCond.Always); ImGui.ShowDebugLogWindow()
if f == 4 then
local c = ImGui.GetStyle().Colors[G.ImGuiCol.Button]
check(c.w == 1.0 and ctx.DebugFlashStyleColorIdx == G.ImGuiCol.Button, "flash active")
end
ImGui.Render(); G.ImGui_ImplRoblox.RenderDrawData(ImGui.GetDrawData())
if f == 1 then check(table.concat(log, " ") == "nfpre nfpost rpre rpost", "hooks after Render: " .. table.concat(log, " ")); ImGui.RemoveContextHook(ctx, rpost) end
if f == 2 then check(table.concat(log, " ") == "nfpre nfpost rpre rpost nfpre nfpost rpre", "RemoveContextHook: " .. table.concat(log, " ")) end
if f == 60 then
local c = ImGui.GetStyle().Colors[G.ImGuiCol.Button]
check(c.x == orig[1] and c.y == orig[2] and c.z == orig[3] and c.w == orig[4] and ctx.DebugFlashStyleColorIdx == G.ImGuiCol.COUNT, "flash restored color exactly")
check(picked ~= nil and picked ~= 0 and picked == button_id, ("item picker set DebugItemPickerBreakId (%s vs %s)"):format(tostring(picked), tostring(button_id)))
check(not ctx.DebugItemPickerActive and broke >= 1, ("item picker break hook called (%d)"):format(broke))
if #failed == 0 then print("DEBUGTOOLS OK") end
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)
+142
View File
@@ -0,0 +1,142 @@
-- Regressions vs upstream: clipboard paste filter (InputTextEx), ImParseFormatTrimDecorations (Ctrl+click temp input), ShrinkWidths tie order,
-- CalcItemSize by value, PushClipRectFullScreen (drop target outside clip rect), long TextUnformatted, PushFont/PopFont, InputTextMultiline line index (render/scroll/edit), Shortcut() default
-- args, IsWindowAppearing() on frame 1 (FrameCount starts at 0), ColorButton -> ColorEdit4 drag and drop
local G = require(game:GetService("ReplicatedStorage"):WaitForChild("ImGui"))
local ImGui, ImVec2 = G.ImGui, G.ImVec2
ImGui.CreateContext()
ImGui.GetIO().Fonts:AddFontDefault()
local font2 = ImGui.GetIO().Fonts:AddFontDefault() -- second font for the PushFont/PopFont check
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]))
check(G.ImFloor(-1.5) == -2 and G.ImFloor(-0.2) == -1 and G.ImFloor(2.7) == 2, "ImFloor negative")
local long_text = {}
for i = 1, 300 do long_text[i] = string.format("line %04d", i) end
long_text = table.concat(long_text, "\n") -- 2999 bytes
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
local function center(name) local r = rects[name]; return (r[1].x + r[2].x) / 2, (r[1].y + r[2].y) / 2 end
local ml_text = "line1\nline2\nline3\nline4\nline5\nline6"
local mbuf = ImGui.NewTextBuffer(ml_text, 256)
local wbuf = ImGui.NewTextBuffer("a long line that has to be word wrapped several times inside the narrow box\nsecond", 256)
local shortcut_frames = {}
local cb_col, ce_col = G.ImVec4(1, 0, 0, 1), { 0, 0, 0, 1 }
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
-- multi-line: click, Ctrl+End, type "x", Enter, "y"
if f == 21 then click("m") elseif f == 22 then release() end
if f == 23 then io:AddKeyEvent(G.ImGuiMod_Ctrl, true); io:AddKeyEvent(G.ImGuiKey.End, true) end
if f == 24 then io:AddKeyEvent(G.ImGuiKey.End, false); io:AddKeyEvent(G.ImGuiMod_Ctrl, false) end
if f == 25 then io:AddInputCharacter(string.byte("x")) end
if f == 26 then key(G.ImGuiKey.Enter) end
if f == 28 then io:AddInputCharacter(string.byte("y")) end
if f == 29 then io:AddKeyEvent(G.ImGuiMod_Ctrl, true); io:AddKeyEvent(G.ImGuiKey.A, true) end -- select all: renders the multi-line selection
if f == 30 then io:AddKeyEvent(G.ImGuiKey.A, false); io:AddKeyEvent(G.ImGuiMod_Ctrl, false) end
if f == 31 then key(G.ImGuiKey.Escape) end
-- Shortcut(chord) with default flags/owner
if f == 33 then io:AddKeyEvent(G.ImGuiMod_Ctrl, true); io:AddKeyEvent(G.ImGuiKey.S, true) end
if f == 34 then io:AddKeyEvent(G.ImGuiKey.S, false); io:AddKeyEvent(G.ImGuiMod_Ctrl, false) end
-- drag the ColorButton onto the ColorEdit4
if f == 36 then io:AddMousePosEvent(center("cb")) end
if f == 37 then io:AddMouseButtonEvent(0, true) end
if f == 38 then local x, y = center("cb"); io:AddMousePosEvent(x + 10, y + 5) end
if f == 39 then io:AddMousePosEvent(center("ce")) end
if f == 42 then io:AddMouseButtonEvent(0, false) end
-- word-wrapped multi-line: click, Ctrl+End, type "z"
if f == 45 then click("w") elseif f == 46 then release() end
if f == 47 then io:AddKeyEvent(G.ImGuiMod_Ctrl, true); io:AddKeyEvent(G.ImGuiKey.End, true) end
if f == 48 then io:AddKeyEvent(G.ImGuiKey.End, false); io:AddKeyEvent(G.ImGuiMod_Ctrl, false) end
if f == 49 then io:AddInputCharacter(string.byte("z")) end
ImGui.NewFrame()
ImGui.SetNextWindowPos(ImVec2(10, 10)); ImGui.SetNextWindowSize(ImVec2(300, 200))
ImGui.Begin("T")
if f == 1 then check(ImGui.GetFrameCount() == 1 and ImGui.IsWindowAppearing(), "IsWindowAppearing() on frame 1") end
if f == 2 then check(not ImGui.IsWindowAppearing(), "IsWindowAppearing() on frame 2") end
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()
-- CalcItemSize takes 'size' by value: the caller's ImVec2 must not be written to
local bsz, csz = ImVec2(0, 0), ImVec2(-1, 40)
ImGui.Button("sz", bsz); check(bsz.x == 0 and bsz.y == 0, "Button wrote size_arg")
ImGui.BeginChild("c", csz); ImGui.EndChild(); check(csz.x == -1 and csz.y == 40, "BeginChild wrote size_arg")
-- drop target rect sticking out of the window clip rect pushes a full-screen clip rect
ImGui.RenderDragDropTargetRectForItem(G.ImRect(-50, -50, 900, 900))
ImGui.End()
ImGui.SetNextWindowPos(ImVec2(320, 10)); ImGui.SetNextWindowSize(ImVec2(300, 200))
ImGui.Begin("M")
ImGui.InputTextMultiline("##m", mbuf, 256, ImVec2(200, ImGui.GetTextLineHeight() * 3)); rects.m = rect()
if f == 30 then
local st = ImGui.GetCurrentContext().InputTextState
check(st.LineCount == 7, "multiline LineCount " .. tostring(st.LineCount))
local cw -- the multi-line child window: cursor on the last line scrolled it
for _, w in ImGui.GetCurrentContext().Windows.Data do if w.Name:find("^M/##m_") then cw = w end end
check(cw ~= nil and cw.Scroll.y > 0, "multiline scroll " .. tostring(cw and cw.Scroll.y))
end
ImGui.InputTextMultiline("##w", wbuf, 256, ImVec2(120, 80), G.ImGuiInputTextFlags.WordWrap); rects.w = rect()
if f == 30 then check(ImGui.GetCurrentContext().InputTextState:HasSelection(), "multiline select all") end
if ImGui.Shortcut(bit32.bor(G.ImGuiMod_Ctrl, G.ImGuiKey.S)) then shortcut_frames[f] = true end
ImGui.End()
ImGui.SetNextWindowPos(ImVec2(10, 220)); ImGui.SetNextWindowSize(ImVec2(300, 150))
ImGui.Begin("D")
ImGui.ColorButton("cb", cb_col); rects.cb = rect()
ImGui.ColorEdit4("##ce", ce_col); rects.ce = rect()
ImGui.End()
-- TextUnformatted() > 2000 bytes (coarse-clipped long text path): each visible line renders only up to its '\n'
ImGui.SetNextWindowPos(ImVec2(320, 220)); ImGui.SetNextWindowSize(ImVec2(300, 150))
ImGui.Begin("L")
local dl = ImGui.GetWindowDrawList()
local v0 = dl.VtxBuffer.Size
ImGui.TextUnformatted(long_text)
local nv = dl.VtxBuffer.Size - v0
if f == 3 then check(nv > 0 and nv <= 14 * 9 * 4, "long TextUnformatted vertices " .. nv) end
-- PushFont() saves the CURRENT font: PopFont() must restore it
local font1 = ImGui.GetFont()
check(font2 ~= font1, "font2 is the default font"); ImGui.PushFont(font2, 0.0); ImGui.PopFont()
if f == 3 then check(ImGui.GetFont() == font1, "PopFont() did not restore the previous font") end
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 == 30 then check(ImGui.TextBufferToString(mbuf) == ml_text .. "x\ny", "multiline buffer: '" .. ImGui.TextBufferToString(mbuf) .. "'") end
if f == 35 then check(shortcut_frames[33] and not shortcut_frames[32] and not shortcut_frames[34], "Shortcut(Ctrl+S) default args") end
if f == 44 then check(ce_col[1] == 1 and ce_col[2] == 0 and ce_col[3] == 0 and ce_col[4] == 1, ("color drop: %s %s %s %s"):format(ce_col[1], ce_col[2], ce_col[3], ce_col[4])) end
if f == 51 then check(ImGui.TextBufferToString(wbuf):sub(-7) == "secondz", "word-wrap buffer: '" .. ImGui.TextBufferToString(wbuf) .. "'") end
if f == 52 and #failed == 0 then print("FIXES OK") end
end)
+174
View File
@@ -0,0 +1,174 @@
-- Font pieces: ImFontGlyphRangesBuilder, ImFont:CalcWordWrapPosition() vs TextWrapped(), IsGlyphRangeUnused(),
-- stb_decompress + Decode85 via AddFontFromMemoryCompressedBase85TTF() (same glyphs/metrics as the raw TTF).
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
-- GlyphRangesBuilder
local builder = G.ImFontGlyphRangesBuilder()
builder:AddText("Hello \u{e9}")
local ranges = G.ImVector()
builder:BuildRanges(ranges)
local got = table.concat(table.move(ranges.Data, 1, ranges.Size, 1, {}), ",")
check(got == "32,32,72,72,101,101,108,108,111,111,233,233,0", "BuildRanges " .. got)
builder:Clear()
builder:AddRanges(io.Fonts:GetGlyphRangesDefault())
builder:AddChar(0x41) -- already in range
ranges = G.ImVector(); builder:BuildRanges(ranges)
check(ranges.Size == 3 and ranges.Data[1] == 0x20 and ranges.Data[2] == 0xFF and ranges.Data[3] == 0, "AddRanges default")
check(builder:GetBit(0x20) and not builder:GetBit(0x1F) and not builder:GetBit(0x100), "GetBit")
-- stb-compressed + base85 ProggyClean.ttf (generated with an stb_compress-format encoder + binary_to_compressed_c's base85)
local B85 = ([==[
7])#######hV0qs#####]h?>#/:jl&Ql#v#[Zre=N=K_o(-(##mQ%##B>A0F_%#g(g/'##r[(##PF6'I</,GM*BiC$v%###s,0%J*RB8O+d68%eKRrQ(.d<BW_*vCL%###E%###:d@UC%)BE#=.>>#'####<_[FH
O97MKO->>#i6YY#i1JuB5eI`[VRUV$=.<T7oH[^ICAS5#T.>>#K####+>00FnH/5`S7g1TS->>#k%S+Hot3>MYJGiTLPUV$>eFVCUwS>#9G:;$n%###Sh(a1aA.;6BE-hAeI1=#au8eZ2D$CI[Mn20Gb?=iMPrIL
DSGkFI5>##Q+`AMB%]`ac<%##CFgn*<WFX:&M'^#3@:B#`2B2#[0b30lOj)#-####.%*=Cvf./LVSiHdaT_n#X$###csu8.r:%)34Dk>$<,;.OOMDgEKnIS7EM9>ZoSAZ$*3)qMQv//L^MkA#wmCAMUnG<-7F>HM
DHw>Pt*p+M[ul=Q<O]CPUoINNUm+p7S<oG76Mqr)twv9Mt0[0#=S&r;QDqk+SEqFR@0p--.iPDN+Y')$PP;vnQbxRMqJQ*OD:H-M#C]GQJ-jBPO&%^Qmus+M3@C58qNO&#sNk,%?$xu#Fle&,W6=G2,Hb;-;wcP$
tPBuuE1V:vm3SML+x0B#/Dabh^KfW-j(.`&V8WQ-2$ZtQQpp5#l,$R#=`i['#l+S#Q*IX#6GMk#op/i#:n*x#tJV0$t#F=$X6iC$1/oh$TrKb$S/kt$&,)O%=q]E%o]n[%6dRd%lGTq%_#`C&$T1_&>Y^Y&2wbq&
TB-K'@IZC'%]#V'Uv'n'[R%1(D6?c(OD1$)e>Lr(Z*24)C5Yb)R/uY)#PPj)uaFu)pEt**@7KY*j<MS*5Nki*SAC?+x/x5+ugwH+;f6w+**;8,V?'+,3Y6W,pGkM,sgEa,%5h7-QbL,-Z8sL-.[r<-r;tI-5hnl-
Cnqc-CX86.QEKF.,hI9.1W`S%mQrV[C4s&#t3&KDU+DvZ)$SpA>O*dskh5??pb?^Y44[W7m@O9&/?o&YNgT?-'aPdsd(-qA0[9_#WMT9AcO#wm%e;e3?oWq/R1,XIghlWnL4mEX_v(R^+$4:A6.*x6Y+w_,ED)`,
PIDRpbf::fN$F.N4Bv:/>80xm6wgLiuWsFXrv)AHt/sl19::#%T1?Ynm49;SFb8/N7n.a><s3T0OpXm(4@4sf)_W/WEcIZ70Ne5_QYMTBLMbT0%t4Tph<HT^;n@*>$P%O)]J/0j$)macqO]pg2V2u$&t@##5f1$#
E@%%#Uqn%#fKb&#v&U'#0WH(#@2<)#Pc/*#a=#+#qnl+#+I`,#;$S-#KTF.#[/:/#l`-0#&;w0#6lj1#oiFJ('Moe2;4HZ$mgfX-b/g/HIt71#DRT<Lq8%dM$bLDNoaVV-Pm^dOJ@6##XaYx')_#&4Za=sQ-,7&4
Oe)>Pn<<>P0]15/$[K<Lrhci9h&@2LaX:9$;$Ne?L*'?76@Vp.%XK<LdcPe6%l^>$?:b1#^QZ`*uqmV-8lt.ZRwbVRqjtH*PhBQ(-t]N-0*aYMiEVq7a=W4#`6_c)$/*JL(L-^(]$wIMlcs(UsvHv,,rbA#/2u]O
hdcHMucWU-k^n:$VGR;oFHnq7e(Q&#uDdE&];bLcqC/^daSxFNtTGw.P3w.Ln1k,O7$22%r]<WS?OY)NJ[vI:DJNGokW_'6Q;I28n8X,Okie;)sJl_fawt-$II`;-sINN$tre+MLFg;-cG,-M<vp%N`tb48]_c6s
:#^#$J?HC$,>2U%qS#n8as:jrQ=$##/)>uuN?Sq7^fV2ivEjJN-MT;-UJ;QMM=6##Cni&O1/Ut'7#[UN>+]K3M@2+8UkW1#k.WUN4DU;-j8(1$)[888aOHpgh<7r7#n8I?%R7p7tGF]u%OtlSE4$##6We'8SdfFI
WVZ>nZ6c8MPhc(.i10p7x0'.=;lxb-4r'E>qC0%%?].e;I#72LclTAO0qD>$Vvnq2h^fW-LL4OiYW-mNFJuH&O(35&85c58@<iv%VwpGMX#$*&WFjp'jZCx$5N:q8/sgd4LhC^++re_8de;m8?37w%RICm8o4wd+
*OhaW?_m.'3XYjM/gh.(xEe58>Y31#_Lx-[?u>q7:5i8K5(;mOM8#=Q'`+%&7'6i#BtxA?0KPM-1PHM&/esr7gC>B$1wGJ(>Q5t7]#:xT:tl.$Tvx;-%&26&`JT58^V_,FWN-c0F####HSI@1K<8FN&?bO'2[IfM
j'YWN,<hn3bqwJN0:kC:(Yf>et`e;)LBf68(24gj1WXW'afQfFv7V.8R02?[LUlDN3cha'&]T&#b96$2*T5s7L5jv[&tsR%3dG2M0.(58=RK=`37;ENkWl9)Y7dp7v_k;fk&Z31P>5r'ZC$M&7&i)<U/5F%<bj/:
b5vAmK4$##&J<j$&6G<QMA&D:Hbkxu.NTkM6ev.:XCF&#iR7S82tuF%k$IAOvjw:$ku0p7sTMBm9pUh$KAuu#5[,qpn%(.*;?>5SkFGm;i'72Ljb]1#+BcK:Fgs.F8`aBNT($28/7*.H1GrvqoTa2`b:EO-=sKj9
crw0#.4D.M(s<A+,DPc=mTGQ:uaAL:Wjp#(R^uw9KvsDN781]&6tgA=_$Dc%.7vcMO>m;51/tW8Y@@S/#1jn8BZT/-Y,lJa-2Ht$i=(HMv'(P8Y>4AuNb6j9hP6(d?^OE23b<Q8)H*nkb$M#1kJAqgUJSk8kK'dk
5#uk%xHA88V`;3;KicD%RE-RLwIsJ%#Et,krIK64v,N/Vn@^5+,(;eMs7jf%Pa64:0nhEYGlk%(I9x9Mk3AN9PV*1#m1OL;Y@0qb]2``]dhsp8HNDLPQ(C-M(NbS85^wek`P@'$fCAV8_hGWSE<?tO(=ZU*EK949
9ux0#2nKA=dT'^#>sR&ZePWV&-=i>RM-']:uc%4;r>8R$tZgSN$fo1#8&n/;%G7@[Jf4x+.bjq7rlBEYk>h-%(X8j9VCxj2OM:/1_>qs7fEt-$n17tQE&9A.S+Ne+3]a:%$MLp7t]Wx'>3Ks7wtpDP.%L)$Q]BDN
NOZf:iskA#j:8T@)BC[.<2N=8c4G`&EW<a&`aQd+s_Pp'a_&6(M@M.$:t1W?4@0#AKhL&OZr_G5Ndap''9nq&@EQkTL0>@$%,m/:PIh)P<e=K8`cI4;5/hu0$qVv[#aKm<Vh;BmVxk4%x3Kn9+Nkxu6Ro94fPQ:8
P_)edQ/G0MPJKU11j?NWE?N(80m^&v+J?J:DRPx^YwdHMNf4-:K3<p^FSfo7kKtWSvQjN9)v[B739$^*i-It]VKVv/>Eq3:+x&'-t0sc;>[:</nIMS2He60Vdig`&6]Hj9QO_&RvTf'&mZ]mBRJ3SabaG'?hIi.$
$./32CCP)NBxoO(9L(c%jxcp7a.-e4Y5n3-qg-a/9c639%wJ+G5X1e&?5V?-a?>Q-;4*88df9dk@]?Y'/ehS@A:R&m2_(:)QYJwTme-LEcuko$4B:T1=^J$&2(+@-]Ziu,ae_-tbUGDN`-Ra$;hhjW/v[lA3pKq^
H<;&(8#DwP3c)/-SoDu7GaHQ:AX=W%+7k,4dQ,N11A>2Bv%03M_/wU&oNi5A)(&3MslOf-.brg=5SHx#7jg]O.F`c)6jK788At1#UfqV-`K<h=Vdb-8U,$90QK@I-E8o3%$grI;4HtdX.Qs$9nga.-c;@v><=ijV
anS,,diDack,uG%(q-&>^Ep&?uTh&UIULu:N*DkDaoK>0T>R@@ho/P+uw<T8#pekXXLZ)GN?Hf,CHuU8&GTpTdx:O3h,p583DQa5frln2-eHrS`V[r7V<$+%Ej0HN,NbN&WQKP9p9qDP-L%r'r(Mj9P+3*GJ*2$/
4_R;-22R2$*LKj:':c_-%4+X.)=a78Q<x0#JP;'PY?=(&BgGp8r;7F%@<PWJYRkH,+GX&#9ePr^o1%/8JE#QUU8jn'S=uPA$TS'J)G=@>^$r?%=FY%8=cFU_Kffo7m-u8NI/5'$NF81.taNq7)?Kq^T+DB)Wg>jV
(ckS*I,1qAb-<p^7Mjf%NcuFNgX12)80EuK,1)g0jPmd;%aZp0ggZdM)j&Z$v>ZQS/U@)2.KDgU#Kk95sor>e@>(QYI<%Z*uB-x0*0K39$uQRX^T]M9_U?^?XD-:8t`MqM0pLP9BmS6(xp?e9dca,bq<3K*^w@T[
bub7%Vu;S8/G5F%0'8rVUDCnNj9%dMllOf-B)(Wf2(73)oGHH;IoZp0Q1Gp7%Wc&#v6D39^P()HBs(2';aFq72v6b6P77'&h2da<TJ_pT`rE)-rf,78tAk)>6Rm3/Zt8#QHo4`)XmW7h'<>e+2NgM9dboLavtx$$
KIK$>1fAoU4NP^0whIt7^iWx''x:Q:`PRB$lhV29)-.K<)dFu3^;mU8UO1S*KTJ/M$=Iw3qi6JN4AFoOalkGNhxNv'HM)?-0(Oh;0:aJsj[lT'5kWm83sdC%hHV7;LSb<S7du7IeKD'mS9JC9Jxr0$t8l>Pxwh9.
L,hs7Elx2)+C30*5d'q7#L?T0W#H^Zths.L2*578H]]^-nO,X&2/Z&FM9%%B8&E#>OgR?RC5'T*;Xx9RhdA7(L01H;Pk1NV87(p7Qd41#fFQ;-xe=C$uf['$C+RPs51b-<8x.4rJ;pt-aA,q7Z6QYq]s/FN6PT[%
gdf68Omx0#HA2s-tGPSIU*@w^dr,X/kJm;?T$dF#IwGP$hn1a*]p7,M.S?M9a$nx0;,Gp74,IpgKNdm'@Bt784N&:DUix*=bc^<Vh+&G[/;dFGR$=I$_Y##PPEV+8=uWd=5KWB%j&h'FPb84=_lDQ(Red/:KD/F.
-o'^#nvkQ8'FjZSZ96Y/o(,,M[jxJjk)lWNSaX%(x11$8SF=[.P8k2B>_sR8w[gJj7*:6:5`)QLb:^i?lw%rMgu>q78aq5M?^w>%IVPp7M8td=/H+s7>&d:;k:&qK=?ex%a*sYPO[+<%&K2;8;fD.$Wk@vPTlC:R
,9R>%-u#6AQ]m&6e32?%THYs%m-n58^<(0E=f2#$wonB$QB]m8R(7mMSpse*)jK29*O$90+$bk)VgB^m/i9R$)8LV4iPpdFBFX)-Q6s58&a3dX&TB59&@_[KAuJmSp13/%8Pp'5D2ZJ:j3:`f+5d+Nc2r.([59p7
6A#'v>J6u'm[2Q8CS+5rbs14$nK%B>m%6Q:`%P1):BA*N1a-A)96D3#1TCt7#IgE#U/VR=`(qQ(U:c58:RRPDZ+,58@`EdtKP>[PQd/,M9u#>9d*-g)&%B@-j<1e(>ih29via.-S?5`8FO:;B&?ch'PMk8@YG@*n
P@st7QGt-$,``wP[ZNC(uTEq70l)?[nS$i$FKgS8jJN9B<kU';c$/32mbBD$4;qeM?5R=MB:K'd@-K^&L.M'Oq4n2$SK%+dwY&(m7/HJ.emw-MLS]0)75,S32BtQ8f3;nqGb=<%Ckb69s#Z&Q^OVv-/vOp7:Mh,F
s2K1:'.vhOt3QA%7rt:(?h+G%2V:<@?O#Qqkl2$;YqE#1:xK,M&%uW)DLo;oA^4e1cnV2([,HKDk)U/dN+dP8:N-Up5d1$1LNP^=EMv^ZR7cP8$Obx%xL>p'CUH-MgU,3D7)D_0'?D_=g>d&QMFJk*7&Pp7Gt3[T
TwY+Y0v2o0DvI^#s-%g:26KE>o_`h*%L^VH1LbC=3;xji-O7r'Lf7KW4N+[&i7PW87O,i8DUjTBFRS?$UG1o*(VRd;4QG1#mpl%$>iraIcDKQ(d%g4([email protected]?^O&gCAr7t$.Ae*b8k&;1=6`d=_d'-05fD.D)E.
uFxZ(d4vQ8.(J(8.%NX-X3RpK:'oLabx4I-1q.:)0.<q0/jF88;0ESCEE8SRN0]u:kpB+$eu*a*2XTe;?@)%IZ[./$HD]BfZ1jk9iCJ32e9R78a=)c<)]<T^'Kc'@lKLKE,LNn%%cl-MpehT$W2`l2b'nj959tT'
1wv*$&9#[JNAC]&[mtvPGwA5;ckQdb`;]R$I&gjDXVfh<NTX1#a8T;-*F:`$qqu,MVqgZ$R(*@II4?uuW%QW@4LoKYVb%R8tks-$HG<f;^31ObK;lR(U@k+MLa'VIt)LE%IrM7)wU:EN&W[39ijMG%#w4g:,Xw0#
w0[588iGJ$ps_>.,umo%x+B7$S)0p71m99pnERN93)9I$)#(IM_Yd>#6x;XCg]5<-3efW85,/n1#S]C'qqvcD2n4`8u9j<-E`Y%&`Qhg*tt2e$N$t49<#oW&^a*@,$^^dk7xK)$GJP/M>KO0&ND_j9>>P&#JuF.3
S<r>I:OPE*XF>dOo#P-(I7Vj;vrlm968R,tB,cS(]FeW&&O`9$-L-5#aS-Ak*o#iSuvYmTPpq9&E*n&%lBap7aS=r7jnP7Ah-]pK>xo_N]mjvPjtJ.2/*'W$4BFQ8bN((&WpULN`$^3i'@+G%.[#.$_6>3)jr[PA
&Wp5Uk_uk%Hk@bNH`(S%T)p/MdebV$4iQr7(QMG%k%8bRfU/L.bn/mSCf>SI$`G%h9JSpB%>xsF,^-Ehgn*Q8&j]K<l(Md8m/9MmPZ4j`a4K2`eI,H:m*Zeohdc,X+fTuYcjEE%M^5t7YsvK3FWHI9$lM4#0*joT
?tM56R'Aq7$d95#n3nAI&'cm3Y(L-MnV#x'qj+l+liD.-8W;v>@wMQh=6)m$1_b-;I?Q5;7>`*6lg3G;sv&:`[JLN9w<gPqa/S/Mx@aJ%NNa,MG>gr'mKDKNrkdkasN/x0,l$q71^?Xh*X[8^PL*q%6cv58_S*1#
bX`0MITc080x^@ilsnlf[L7I%(3mLC(BZE5+?,P@)$+1#DqsJaN-U:/-u?68)p<9j7E%>i?Ia;-kH/D,v(mn8=0'.$Nu49@Bm*1#XYs5CvbQa86j-bS`c5m8RiE1#4l^&#pCa$UOs068Z1-E5RhYl$Oh%TW*)LTA
.#$]UnM;TK9(Dl%8`?6B<MiRWdaTl9'OGG.SAd[IOMvu>6E>WgZ3of$O1Up@XbS2L[OtL:aqtqK:M2j*_@-:)LF5F%sd40:3Fr_&]?'k9W@dF<A]r<J?&]lAL2j&-ntJ<(=skq7w^+Ls<q9et?<a^%+$Y98_&Zob
vsUqpARc=<ROu7c=ebbOHwO/6Pm@*Ww0]%X*io?<8IQ<iLwrMg?r?eO%E:^-aB#KWu<7b'tRLdt#'-;?A1^FEMk82bPU#-MT>uw&hMM8Ia^vr'sj#j1>f:^OuVh29)g09Tq-.78(S%?7NCU29fbbgNaEJ)<jB'4r
(OSP3WS698I9B.$?KS58QV`g$p=@`HW?U/5S&64;1n%:>7rU2`cGs_*,K)1#FCR(Q=Q7i8sDwUjLe.0M2('B$dwW88%vrv@bNOG%K8tMB`6XF-:/::;-:`6r0U=0$``Xp9Knp*-cUpeHl%w,;s+e<_3cs?-#s&r0
1B^]02M(wYNshr00dW_ZG]0a;Gx4ND*&9;-HM%VqIL6GDsDZki/S0K)KF,:pgT4,%4K]dM*x8h8WVl6`g#gG$mhmF,,i2v*wBS1MFM0`&,:PP:[J'LPk%']h5(Wxd>i9UXuQk&18(&H;%Ug'hT6S</?xNx1fa5u7
DW2QL',t`-Cx#okPutL<%cY'2pP)xPGHHiS^x6Z:R91ObU-iT9O(4m'+%2a*N9pP9(B.-bX%`XNve$U$Bht]=h6rJW<3VM%_j/'Q_7M3,ej5/-Ncf/sSl1]M(Q:['>v_Q93J04r7<,vNP%2d97xbEgJKQGVgB#r3
FO4;89*^EfckAhDPC)c<6v690M?r&(GDc^$Ki+58v&LnF)iRTRpw<8Up^,(dPss3,0Z)kT#1i]$-KIZ7LI)3S`ig1(p^^URhRi*N,%$c4L4udO(jG9%[vg2V7I(e&NY<dX,WKw#_&Yj9>hMBQ4T:r2`e0l:lJB$h
.`Pg+e]wY%jfNf$)']6K8lDKaC90I.p<3q7dQtA`Q`5?`(qQvc_6b,3O3)9gwixH-]$SR8vjMhdwMD$[V3xR&vCIpT$Vbk1f=RRblisvHe#Zq9Ske(9iR6XLQES29JsZIM-2>rDjiGr^89KO0-^'q718r_&61D58
w+r_&.lCW/6iG<-`*wQ%8DQ^OmBjE%9X`E.1EDS@#j/q'J5x8-cxfJssL4($Uflq7hxad=4O:C4K9+hER0^F9G+UQc8W:uQ&/0<A7t3fWC[J90AEOn%&:+kCUYXjd*c-^O.<(d%&0)1#XrV'POoOS$AX+q8IZ4)6
hxEFM,hSgN0aEW+x/;N3xJk0&j`S.;edM2i9D+*0GJlB$x:(29wPX&#?l%oS.cb&#`i[`*lQ/,MH@-##FEV6NTF6##$l84N1w?AO>'IAOl%4a*0Y^GM#RcGM9#ult_Ks$#B@`T.1rC$#`@`T.,Mc##T@`T.74i$#
WrG<-N2:W.2`($#irG<-<q@u-?Wd`*eC;,M6WYdM6WYdM*?`T.HEX&#`YYdM6WYdM6WYdM%w@(#.E-(#cPc-NZxtk24Y($Jt1^oD0ZKKFg`]oD+,2eGc:@bHcFx.>@nuc<)0o6D]bdUCFdg+>?Wae3;BR12A::MB
h]8;-?(DYGUq`uGNDw1B];8;-SHpw'r?Z.#K0f-#=ic+#E<x-#ZZO.#L)1/#wtu+#F:F&#;)1/#O<x-#YSq/#(ZO.#wc-2MVC/$$;^?w^Gm/aNs*p+M&fsD#*J(v#.c_V$2%@8%6=wo%:UWP&>n82'B0pi'FHPJ(
Ja1,)N#ic)R;ID*VS*&+Zla]+_.B>,cF#v,_REmh1W@`$0u68%47no%8ONP&<h/2'@*gi'DBGJ(HZ(,)Ls_c)P5@D*TMw%+XfW]+](9>,a@pu,eXPV-iq18.m3io.qKIP/ud*20#'bi0'?BJ19if,=(N^o232;D3
7Jr%4;cR]4?%4>5C=ku5GUKV6Kn,87O0do7SHDP8Wa%29[#]i9`;=J:dSt+;hlTc;l.6D<pFm%=t_M]=xw.>>&:fu>*RFV?.k'8@2-_o@6E?PA:^v1Bikur-m-VS.qE75/u^nl/#wNM0'90/1+Qgf1/jGG23,))3
7D``3;]@A4?uwx4C7XY5GO9;6Khpr6O*QS7hPn:ZKe0GMgc>c`Mq&Sn8h?YPLS-JUrBB`WH+,AO,Wm+MTm6fqOR(>PCeG`NqHn@k_;LfUox3creaQ(st)7VZ`5Quc4R>MBmeu:QS(f7eQfi:di++PfiuIoe1#Don
^w>igqOb1g:-r+DPn?VQ$(_.hbS8GDRI]1pm*m(EM(1DER=L`E2m(GVYq)>G['EYGbKAVHdW]rH0%]4ol2u4Jn>:PJsSUlJ(UFcVwl6MK%53JLTUxLpbwnWH#N#hF@0*K2,4IL2fCd<-gCd<-hCd<-iCd<-jCd<-
kCd<-lCd<-mCd<-nCd<-@Dd<-ADd<-BDd<-CDd<-DDd<-EJ)X-v0P]'Gt.g2Gt.g2Gt.g2Gt.g2Gt.g2Gt.g2Gt.g2Gt.g2Gt.g2Gt.g2Gt.g2Gt.g2Gt.g2TmOh2Y#oTC$NXVCThmfGY4&>-K*IT-K*IT-K*IT-
K*IT-K*IT-K*IT-K*IT-K*IT-K*IT-K*IT-K*IT-K*IT-K*IT-K*IT-18ZI'Et,gjm,(DPO/(DPO/(DPO/(DPO/(DPO/(DPO/(DPO/(DPO/(DPO/(DPO/(DPO/(DPO/(DPO/(DP+-XlC#m8Atkt>Ns
]==]):gsub("\n", "")
local raw_data, raw_size = G.ImStd.ImFileLoadToMemory("fonts/ProggyClean.ttf", "rb")
local raw = io.Fonts:AddFontFromMemoryTTF(raw_data, raw_size, 13.0)
local comp = io.Fonts:AddFontFromMemoryCompressedBase85TTF(B85, 13.0)
check(comp ~= nil, "AddFontFromMemoryCompressedBase85TTF returned nil")
local comp_data = comp.Sources.Data[1].FontData
local same = comp.Sources.Data[1].FontDataSize == raw_size
for n = 1, raw_size do if not same then break end same = comp_data[n] == raw_data[n] end
check(same, "decompressed bytes differ from fonts/ProggyClean.ttf")
local TEXT = "The quick brown fox jumps over the lazy dog again and again"
local W = 100
local f = 0
game:GetService("RunService").Stepped:Connect(function()
f += 1
G.ImGui_ImplRoblox.NewFrame()
ImGui.NewFrame()
ImGui.SetNextWindowPos(ImVec2(10, 10)); ImGui.SetNextWindowSize(ImVec2(400, 300))
ImGui.Begin("Fonts")
ImGui.PushFont(raw, 13.0)
ImGui.PushTextWrapPos(ImGui.GetCursorPosX() + W)
ImGui.TextWrapped(TEXT)
local wrapped_h = ImGui.GetItemRectSize().y
ImGui.PopTextWrapPos()
local line_h = ImGui.GetTextLineHeight()
ImGui.PopFont()
if f <= 3 then
ImGui.PushFont(comp, 13.0)
ImGui.Text("Hello \u{e9}")
ImGui.PopFont()
end
ImGui.End()
ImGui.Render(); G.ImGui_ImplRoblox.RenderDrawData(ImGui.GetDrawData())
if f == 3 then
-- CalcWordWrapPosition(): same line breaks as TextWrapped(). ProggyClean 13px = 7px per char -> 14 chars per 100px line
local te = #TEXT + 1
local s, lines, breaks = 1, 0, {}
while s < te do
local e = raw:CalcWordWrapPosition(13.0, TEXT, s, te, W)
lines += 1; table.insert(breaks, e)
s = e
while s < te and TEXT:sub(s, s) == " " do s += 1 end -- ImTextCalcWordWrapNextLineStart(): skip blanks
end
check(breaks[1] == 10, "first wrap after 'The quick': " .. tostring(breaks[1])) -- "The quick" | "brown fox" | ...
check(TEXT:sub(11, breaks[2] - 1) == "brown fox", "second line '" .. TEXT:sub(11, breaks[2] - 1) .. "'")
check(wrapped_h == lines * line_h, ("TextWrapped height %s vs %d lines * %s"):format(wrapped_h, lines, line_h))
-- Compressed font: same glyphs and metrics as the raw TTF
local br, bc = raw:GetFontBaked(13.0), comp:GetFontBaked(13.0)
check(br.Ascent == bc.Ascent and br.Descent == bc.Descent, "ascent/descent")
for _, c in { 0x48, 0x65, 0x6C, 0xE9, 0x41, 0x7E } do
local gr, gc = br:FindGlyph(c), bc:FindGlyph(c)
check(gc.Visible and gc.X1 > gc.X0 and gc.Y1 > gc.Y0, "compressed glyph empty " .. c)
check(gr.AdvanceX == gc.AdvanceX and gr.X0 == gc.X0 and gr.Y0 == gc.Y0 and gr.X1 == gc.X1 and gr.Y1 == gc.Y1, "glyph metrics " .. c)
end
local str = "Hello \u{e9}"
local sr, sc = raw:CalcTextSizeA(13.0, 1e30, 0, str, 1, #str + 1), comp:CalcTextSizeA(13.0, 1e30, 0, str, 1, #str + 1)
check(sr.x == sc.x and sr.y == sc.y and sc.x > 0, "CalcTextSizeA")
check(not comp:IsGlyphRangeUnused(0, 255) and comp:IsGlyphRangeUnused(0x4E00, 0x9FFF), "IsGlyphRangeUnused")
-- AddRemapCodepoint(): '$' loads the 'A' glyph (applies to glyphs loaded afterwards: new 20px baked)
comp:AddRemapCodepoint(0x24, 0x41)
local b20 = comp:GetFontBaked(20.0)
local gd, ga = b20:FindGlyph(0x24), raw:GetFontBaked(20.0):FindGlyph(0x41)
check(gd.X0 == ga.X0 and gd.Y0 == ga.Y0 and gd.X1 == ga.X1 and gd.Y1 == ga.Y1, "AddRemapCodepoint")
-- RemoveCustomRect()
local atlas = io.Fonts
local id = atlas:AddCustomRect(8, 8)
check(atlas:GetCustomRect(id, G.ImFontAtlasRect()) == true, "AddCustomRect")
atlas:RemoveCustomRect(id)
check(not atlas:GetCustomRect(id, G.ImFontAtlasRect()), "RemoveCustomRect")
-- RemoveFont()
local nfonts, nsrc = atlas.Fonts.Size, atlas.Sources.Size
atlas:RemoveFont(comp)
check(atlas.Fonts.Size == nfonts - 1 and atlas.Sources.Size == nsrc - 1 and not atlas.Fonts:contains(comp), "RemoveFont")
-- ImFontAtlasTextureBlockFill(), ImTextureDataGetXXXName()
local tex = G.ImTextureData(); tex:Create(G.ImTextureFormat.RGBA32, 4, 4)
G.ImFontAtlasTextureBlockFill(tex, 1, 2, 2, 1, G.IM_COL32(1, 2, 3, 4))
local px = table.concat(tex.Pixels, ",", 33, 48)
check(px == "0,0,0,0,1,2,3,4,1,2,3,4,0,0,0,0" and tex.Pixels[29] == 0 and tex.Pixels[53] == 0, "TextureBlockFill " .. px)
check(G.ImTextureDataGetStatusName(tex.Status) == "WantCreate" and G.ImTextureDataGetFormatName(tex.Format) == "RGBA32", "texture names")
end
if f == 5 and #failed == 0 then print("FONTS OK") end
end)
+301
View File
@@ -0,0 +1,301 @@
-- Small upstream helpers: ImGuiInputTextState selection API + ReloadUserBuf*, ImGuiInputTextCallbackData:SetSelection, ImDrawList AddImageQuad/PrimQuadUV/
-- CloneOutput, ImDrawData DeIndexAllBuffers/ScaleClipRects, ImBitVector/ImBitArray*, ImGuiPackedDate, ImParseFormatSanitizeForPrinting, drag cancel (ShortcutsForCancel),
-- log decorations, ImGuiDataType.S64/U64, BringWindowToDisplayBehind, ImGuiSizeCallbackData
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
-- Pure helpers
check(G.ImParseFormatSanitizeForPrinting("%.3f%%", nil, 32) == "%.3f", "sanitize %.3f%%")
check(G.ImParseFormatSanitizeForPrinting("%'_5.2f units", nil, 32) == "%5.2f", "sanitize stb flags: " .. G.ImParseFormatSanitizeForPrinting("%'_5.2f units", nil, 32))
local d = G.ImGuiPackedDate(20260315)
check(d:IsValid() and d:Unpack() == 20260315, "PackedDate round trip " .. d:Unpack())
d:SubtractMonths(3)
check(d:Unpack() == 20251215, "PackedDate SubtractMonths " .. d:Unpack())
check(not G.ImGuiPackedDate():IsValid() and G.ImGuiPackedDate():Unpack() == 0, "PackedDate empty")
local bv = G.ImBitVector()
bv:Create(70)
check(bv.Storage.Size == 3, "ImBitVector size")
bv:SetBit(1); bv:SetBit(70); bv:SetBit(33)
check(bv:TestBit(1) and bv:TestBit(70) and bv:TestBit(33) and not bv:TestBit(2) and not bv:TestBit(69), "ImBitVector SetBit/TestBit")
bv:ClearBit(33)
check(not bv:TestBit(33) and bv:TestBit(70), "ImBitVector ClearBit")
local arr = {}
G.ImBitArrayClearAllBits(arr, 96)
G.ImBitArraySetBitRange(arr, 3, 70) -- [3..70)
local ok_range = true
for n = 1, 96 do if G.ImBitArrayTestBit(arr, n) ~= (n >= 3 and n < 70) then ok_range = false end end
check(ok_range, "ImBitArraySetBitRange")
check(G.ImBitArrayGetStorageSizeInBytes(70) == 12, "ImBitArrayGetStorageSizeInBytes")
check(G.ImModPositive(-1, 5) == 4 and G.ImRoundSigned64(-1.5) == -2 and G.ImCharIsXdigitA(string.byte("F")), "math helpers")
local buf = ImGui.NewTextBuffer("hello world", 64)
local cbuf = ImGui.NewTextBuffer("abcdef", 64)
local cb_calls = 0
local function cb(data)
cb_calls += 1
if cb_calls == 1 then data:SetSelection(1, 3) end
return 0
end
local df, rect_df = 10.0, nil
local s64_v = 2 ^ 40
local f = 0
local function center(r) return (r[1].x + r[2].x) / 2, (r[1].y + r[2].y) / 2 end
local function vtx(dl, i) -- 1-based vertex
local b, o = dl.VtxBuffer.Buf, (i - 1) * G.VTX_STRIDE
return buffer.readf64(b, o), buffer.readf64(b, o + 8), buffer.readf64(b, o + 16), buffer.readf64(b, o + 24), buffer.readu32(b, o + 32)
end
game:GetService("RunService").Stepped:Connect(function()
f += 1
G.ImGui_ImplRoblox.NewFrame()
-- drag + Escape cancels and reverts (ShortcutsForCancel)
if f == 20 then io:AddMousePosEvent(center(rect_df)) end
if f == 21 then io:AddMouseButtonEvent(0, true) end
if f == 22 then local x, y = center(rect_df); io:AddMousePosEvent(x + 40, y) end
if f == 24 then io:AddKeyEvent(G.ImGuiKey.Escape, true) end
if f == 25 then io:AddKeyEvent(G.ImGuiKey.Escape, false) end
if f == 26 then io:AddMouseButtonEvent(0, false) end
ImGui.NewFrame()
ImGui.SetNextWindowPos(ImVec2(10, 10)); ImGui.SetNextWindowSize(ImVec2(400, 300))
ImGui.Begin("H")
if f == 2 then ImGui.SetKeyboardFocusHere() end
ImGui.InputText("##t", buf, 64)
local id = ImGui.GetItemID()
local st = ImGui.GetInputTextState(id)
if f == 5 then
check(st ~= nil, "InputText active")
if st then
st:SetSelection(3, 6)
check(st:GetSelectionStart() == 3 and st:GetSelectionEnd() == 6 and st:GetCursorPos() == 6 and st:HasSelection(), "SetSelection/Get*")
st:ClearSelection()
check(st:GetSelectionStart() == 6 and st:GetSelectionEnd() == 6 and not st:HasSelection(), "ClearSelection")
check(st:GetPreferredOffsetX() == -1, "GetPreferredOffsetX")
-- modify the user buffer externally then reload
local s = "bye"; for i = 1, #s do buf[i] = string.byte(s, i) end; buf[#s + 1] = 0
st:ReloadUserBufAndSelectAll()
end
end
if f == 7 and st then
check(G.ImGui._ByteArrayToString(st:GetText(), 1, st.TextLen + 1) == "bye", "ReloadUserBufAndSelectAll text")
check(st:GetSelectionStart() == 1 and st:GetSelectionEnd() == 4, ("ReloadUserBufAndSelectAll sel %d..%d"):format(st:GetSelectionStart(), st:GetSelectionEnd()))
st:SetSelection(2, 3); st:ReloadUserBufAndKeepSelection()
end
if f == 9 and st then
check(st:GetSelectionStart() == 2 and st:GetSelectionEnd() == 3, "ReloadUserBufAndKeepSelection")
st:ReloadUserBufAndMoveToEnd()
end
if f == 11 and st then
check(st:GetCursorPos() == 4 and not st:HasSelection(), "ReloadUserBufAndMoveToEnd " .. st:GetCursorPos())
end
-- callback SetSelection (0-based callback positions -> 1-based stb)
if f == 12 then ImGui.SetKeyboardFocusHere() end
ImGui.InputText("##c", cbuf, 64, G.ImGuiInputTextFlags.CallbackAlways, cb)
if f == 15 then
local cst = ImGui.GetInputTextState(ImGui.GetItemID())
check(cst ~= nil and cb_calls > 0 and cst:GetSelectionStart() == 2 and cst:GetSelectionEnd() == 4, "callback SetSelection " .. (cst and (cst:GetSelectionStart() .. ".." .. cst:GetSelectionEnd()) or "nil"))
end
df = ImGui.DragFloat("##df", df, 1.0)
local a, b = ImGui.GetItemRectMin(), ImGui.GetItemRectMax(); rect_df = { ImVec2(a.x, a.y), ImVec2(b.x, b.y) }
if f == 23 then check(df ~= 10.0, "drag changed value " .. df) end
if f == 25 then check(df == 10.0 and ImGui.GetActiveID() == 0, "Escape reverts drag " .. df) end
if f == 30 then
local dl = ImGui.GetWindowDrawList()
local v0, i0 = dl.VtxBuffer.Size, dl.IdxBuffer.Size
local tex = G.ImTextureRef(1234)
dl:AddImageQuad(tex, ImVec2(10, 10), ImVec2(50, 12), ImVec2(48, 60), ImVec2(8, 58), ImVec2(0, 0), ImVec2(1, 0), ImVec2(1, 1), ImVec2(0, 1), 0xFF00FF00)
check(dl.VtxBuffer.Size == v0 + 4 and dl.IdxBuffer.Size == i0 + 6, "AddImageQuad counts")
local x, y, u, v, col = vtx(dl, v0 + 3)
check(x == 48 and y == 60 and u == 1 and v == 1 and col == 0xFF00FF00, ("PrimQuadUV vertex %s %s %s %s %x"):format(x, y, u, v, col))
local idx = dl.IdxBuffer.Data
check(idx[i0 + 1] == v0 + 1 and idx[i0 + 5] == v0 + 3 and idx[i0 + 6] == v0 + 4, "PrimQuadUV indices")
local cmin, cmax = dl:GetClipRectMin(), dl:GetClipRectMax()
check(cmax.x > cmin.x and cmax.y > cmin.y, "GetClipRectMin/Max")
local clone = dl:CloneOutput()
check(clone.VtxBuffer.Size == dl.VtxBuffer.Size and clone.IdxBuffer.Size == dl.IdxBuffer.Size and clone.CmdBuffer.Size == dl.CmdBuffer.Size
and clone.VtxBuffer.Buf ~= dl.VtxBuffer.Buf and clone.CmdBuffer.Data[1] ~= dl.CmdBuffer.Data[1], "CloneOutput copies buffers")
local cx = vtx(clone, v0 + 3)
check(cx == 48, "CloneOutput vertex data")
local dd = G.ImDrawData()
dd.CmdLists:push_back(clone)
local n_idx = clone.IdxBuffer.Size
local ex, ey = vtx(clone, clone.IdxBuffer.Data[n_idx])
dd:DeIndexAllBuffers()
check(clone.IdxBuffer.Size == 0 and clone.VtxBuffer.Size == n_idx and dd.TotalVtxCount == n_idx and dd.TotalIdxCount == 0, "DeIndexAllBuffers counts")
local lx, ly = vtx(clone, n_idx)
check(lx == ex and ly == ey, "DeIndexAllBuffers vertex data")
local cr = clone.CmdBuffer.Data[1].ClipRect
local x1, y1, z1, w1 = cr.x, cr.y, cr.z, cr.w
dd:ScaleClipRects(ImVec2(2, 3))
local cr2 = clone.CmdBuffer.Data[1].ClipRect
check(cr2.x == x1 * 2 and cr2.y == y1 * 3 and cr2.z == z1 * 2 and cr2.w == w1 * 3 and dl.CmdBuffer.Data[1].ClipRect.z == z1, "ScaleClipRects")
end
if f == 31 then
local g = ImGui.GetCurrentContext()
local style = ImGui.GetStyle()
local window = ImGui.GetCurrentWindow()
check(G.ImGuiKey["0"] == G.ImGuiKey.K0 and G.ImGuiKey["9"] == G.ImGuiKey.K9, "ImGuiKey digit aliases")
-- Style vars (enum values must match GStyleVarsInfo order)
local sv = G.ImGuiStyleVar
local b1, b2, b3 = style.SeparatorSize, style.SeparatorTextBorderSize, style.DockingSeparatorSize
ImGui.PushStyleVar(sv.SeparatorSize, 5); ImGui.PushStyleVar(sv.SeparatorTextBorderSize, 6); ImGui.PushStyleVar(sv.DockingSeparatorSize, 7)
check(style.SeparatorSize == 5 and style.SeparatorTextBorderSize == 6 and style.DockingSeparatorSize == 7, "PushStyleVar SeparatorSize/SeparatorTextBorderSize/DockingSeparatorSize")
ImGui.PushStyleVar(sv.SeparatorTextPadding, ImVec2(3, 4))
check(style.SeparatorTextPadding.x == 3 and style.SeparatorTextPadding.y == 4, "PushStyleVar SeparatorTextPadding")
ImGui.PopStyleVar(4)
check(style.SeparatorSize == b1 and style.SeparatorTextBorderSize == b2 and style.DockingSeparatorSize == b3, "PopStyleVar restores")
-- Log flags
ImGui.LogToBuffer()
check(g.LogEnabled and g.LogFlags == G.ImGuiLogFlags.OutputBuffer, "LogToBuffer sets ImGuiLogFlags.OutputBuffer")
ImGui.Text("logged")
check(string.find(g.LogBuffer:c_str(), "logged", 1, true) ~= nil, "LogToBuffer captures text")
ImGui.LogFinish()
check(g.LogFlags == G.ImGuiLogFlags.None and not g.LogEnabled, "LogFinish clears LogFlags")
-- Combo preview (no ImGuiComboFlags.CustomPreview needed since 1.93)
if ImGui.BeginCombo("##cb", "") then ImGui.EndCombo() end
local pd = g.ComboPreviewData
local work_max_x, content_max_x = window.WorkRect.Max.x, window.ContentRegionRect.Max.x
local opened = ImGui.BeginComboPreview()
check(opened and pd.WithinPreview and window.WorkRect.Max.x == pd.PreviewRect.Max.x - style.FramePadding.x and window.ContentRegionRect.Max.x == window.WorkRect.Max.x, "BeginComboPreview sets WorkRect")
if opened then
ImGui.Text("preview")
ImGui.EndComboPreview()
end
check(not pd.WithinPreview and window.WorkRect.Max.x == work_max_x and window.ContentRegionRect.Max.x == content_max_x, "EndComboPreview restores WorkRect")
if ImGui.BeginCombo("##cb2", "x", G.ImGuiComboFlags.NoPreview) then ImGui.EndCombo() end
check(not ImGui.BeginComboPreview(), "BeginComboPreview with NoPreview returns false")
-- ListClipper in a table: ImGuiListClipperFlags.NoSetTableRowCounters
local rows = {}
ImGui.SetNextWindowPos(ImVec2(500, 10)); ImGui.SetNextWindowSize(ImVec2(300, 400))
ImGui.Begin("Clip")
for pass = 1, 2 do
if ImGui.BeginTable("##clip" .. pass, 1, G.ImGuiTableFlags.ScrollY, ImVec2(0, 60)) then
local clipper = G.ImGuiListClipper()
if pass == 2 then clipper.Flags = G.ImGuiListClipperFlags.NoSetTableRowCounters end
clipper:Begin(1000)
while clipper:Step() do
for i = clipper.DisplayStart, clipper.DisplayEnd - 1 do ImGui.TableNextRow(); ImGui.TableNextColumn(); ImGui.Text("%d", i) end
end
clipper:End()
rows[pass] = ImGui.GetCurrentTable().CurrentRow
local tbl = ImGui.GetCurrentTable()
ImGui.EndTable()
if pass == 1 then ImGui.SetNextItemOpen(true); ImGui.DebugNodeTable(tbl) end -- DebugNodeTableGetSizingPolicyDesc()
end
end
ImGui.End()
check(rows[1] ~= nil and rows[2] ~= nil and rows[1] >= 990 and rows[2] < 100, ("clipper row counters %s / NoSetTableRowCounters %s"):format(tostring(rows[1]), tostring(rows[2])))
-- .ini LastUsed dates + CleanupIniSettings()
local pio = ImGui.GetPlatformIO()
pio.Platform_SessionDate = 20261015
g.SessionDate = G.ImGuiPackedDate(pio.Platform_SessionDate) -- (normally done by NewFrame())
io.ConfigIniSettingsAutoDiscardMonths = 6
ImGui.LoadIniSettingsFromMemory("[Window][Old]\nPos=1,1\nLastUsed=20250101\n\n[Window][NoDate]\nPos=2,2\n\n[Window][Recent]\nPos=3,3\nLastUsed=20260901\n\n")
io.ConfigIniSettingsAutoDiscardMonths = 0
local function ws(name) for _, st in g.SettingsWindows.Data do if st.Name == name then return st end end; return nil end
check(ws("Old").WantDelete and ws("NoDate").WantDelete and not ws("Recent").WantDelete and ws("Recent").LastUsedDate == 20260901, "ConfigIniSettingsAutoDiscardMonths discards old/undated entries")
local args = G.ImGuiSettingsCleanupArgs()
args.SetCurrentSessionDateToAll = true
args.TypeHashFilter = G.ImHashStr("Window")
ImGui.CleanupIniSettings(args)
check(ws("Recent").LastUsedDate == 20261015, "CleanupIniSettings SetCurrentSessionDateToAll")
local ini = ImGui.SaveIniSettingsToMemory()
check(not string.find(ini, "[Window][Old]", 1, true) and string.find(ini, "[Window][Recent]\nPos=3,3\nCollapsed=0\nLastUsed=20261015\n", 1, true) ~= nil
and string.find(ini, "[Window][H]\nPos=10,10\nSize=400,300\nCollapsed=0\nLastUsed=20261015\n", 1, true) ~= nil, "LastUsed written to .ini:\n" .. ini)
io.ConfigIniSettingsSaveLastUsedDate = false
check(not string.find(ImGui.SaveIniSettingsToMemory(), "LastUsed", 1, true), "ConfigIniSettingsSaveLastUsedDate = false")
io.ConfigIniSettingsSaveLastUsedDate = true
pio.Platform_SessionDate = 0
-- Log decorations: Selectable/Button/Checkbox/MenuItem shortcut
ImGui.LogToBuffer()
ImGui.Selectable("sel item")
ImGui.Button("btn")
ImGui.Checkbox("chk", true)
ImGui.MenuItem("mi", "Ctrl+M")
local log = g.LogBuffer:c_str()
ImGui.LogFinish()
check(string.find(log, "[sel item]", 1, true) == nil and string.find(log, "sel item", 1, true) ~= nil, "Selectable logs plain label: " .. log)
check(string.find(log, "[ btn ]", 1, true) ~= nil and string.find(log, "[x]", 1, true) ~= nil and string.find(log, "mi ( Ctrl+M )", 1, true) ~= nil, "LogSetNextTextDecoration [ ]: " .. log)
-- DragScalar S64/U64 (doubles) + DataTypeFormatString
local v64, ch = ImGui.DragScalar("##s64", G.ImGuiDataType.S64, 2 ^ 40, 1.0)
check(v64 == 2 ^ 40 and ch == false, "DragScalar S64 idle")
local b64 = {}
local n = ImGui.DataTypeFormatString(b64, 64, G.ImGuiDataType.S64, -(2 ^ 40), "%lld")
check(G.ImGui._ByteArrayToString(b64, 1, n + 1) == "-1099511627776", "DataTypeFormatString S64")
n = ImGui.DataTypeFormatString(b64, 64, G.ImGuiDataType.U64, 18446744073709551615, "%llu")
check(G.ImGui._ByteArrayToString(b64, 1, n + 1) == "18446744073709551615", "DataTypeFormatString U64 max")
n = ImGui.DataTypeFormatString(b64, 64, G.ImGuiDataType.S32, -5, "%X")
check(G.ImGui._ByteArrayToString(b64, 1, n + 1) == "FFFFFFFB", "DataTypeFormatString S32 hex")
local tb = ImGui.NewTextBuffer("-123456789012", 64)
local r64, c64 = ImGui.DataTypeApplyFromText(tb, G.ImGuiDataType.S64, 0, "%lld")
check(r64 == -123456789012 and c64, "DataTypeApplyFromText S64 " .. tostring(r64))
check(ImGui.DataTypeApplyOp(G.ImGuiDataType.S64, 43, 2 ^ 62, 2 ^ 62) == 9223372036854775807, "DataTypeApplyOp S64 clamp")
check(G.ImGuiDataType.Float == 8 and G.ImGuiDataType.COUNT == 12 and ImGui.DataTypeGetInfo(G.ImGuiDataType.Float).Name == "float", "ImGuiDataType layout")
end
-- DragScalar S64: drag with the mouse
local s64_rect
s64_v = ImGui.DragScalar("##drag_s64", G.ImGuiDataType.S64, s64_v, 1.0)
do local a, b = ImGui.GetItemRectMin(), ImGui.GetItemRectMax(); s64_rect = { ImVec2(a.x, a.y), ImVec2(b.x, b.y) } end
if f == 33 then io:AddMousePosEvent(center(s64_rect)) end
if f == 34 then io:AddMouseButtonEvent(0, true) end
if f == 35 then local x, y = center(s64_rect); io:AddMousePosEvent(x + 30, y) end
if f == 37 then io:AddMouseButtonEvent(0, false); check(s64_v > 2 ^ 40 and s64_v == math.floor(s64_v), "DragScalar S64 drag " .. s64_v) end
ImGui.End()
-- BringWindowToDisplayBehind
if f == 38 then
local g = ImGui.GetCurrentContext()
local wa, wb, wc = ImGui.FindWindowByName("WA"), ImGui.FindWindowByName("WB"), ImGui.FindWindowByName("WC")
local function idx(w) return ImGui.FindWindowDisplayIndex(w) end
ImGui.BringWindowToDisplayFront(wa); ImGui.BringWindowToDisplayFront(wb); ImGui.BringWindowToDisplayFront(wc)
local n0 = g.Windows.Size
ImGui.BringWindowToDisplayBehind(wc, wa) -- move forward-placed window behind: WC WA WB
check(idx(wc) + 1 == idx(wa) and idx(wa) + 1 == idx(wb) and g.Windows.Size == n0, ("BringWindowToDisplayBehind back %d %d %d"):format(idx(wa), idx(wb), idx(wc)))
ImGui.BringWindowToDisplayBehind(wa, wb) -- already directly behind: unchanged
check(idx(wc) + 1 == idx(wa) and idx(wa) + 1 == idx(wb), "BringWindowToDisplayBehind no-op")
ImGui.BringWindowToDisplayBehind(wc, wb) -- WA WC WB
check(idx(wa) + 1 == idx(wc) and idx(wc) + 1 == idx(wb) and g.Windows.Size == n0, ("BringWindowToDisplayBehind fwd %d %d %d"):format(idx(wa), idx(wb), idx(wc)))
end
for _, name in { "WA", "WB", "WC" } do
ImGui.SetNextWindowPos(ImVec2(820, 10)); ImGui.SetNextWindowSize(ImVec2(80, 60))
ImGui.Begin(name); ImGui.End()
end
-- Size callback (Step + AspectRatio)
ImGui.SetNextWindowPos(ImVec2(10, 420))
if f <= 3 then ImGui.SetNextWindowSize(ImVec2(237, 160)) end
ImGui.SetNextWindowSizeConstraints(ImVec2(0, 0), ImVec2(G.FLT_MAX, G.FLT_MAX), function(data)
local step = data.UserData
check(data.Pos ~= nil and data.CurrentSize ~= nil and not rawequal(data.DesiredSize, data.CurrentSize), "ImGuiSizeCallbackData fields")
data.DesiredSize = ImVec2(math.floor(data.DesiredSize.x / step + 0.5) * step, math.floor(data.DesiredSize.y / step + 0.5) * step)
end, 100.0)
ImGui.Begin("Step"); local step_size = ImGui.GetWindowSize(); ImGui.End()
ImGui.SetNextWindowPos(ImVec2(300, 420))
if f <= 3 then ImGui.SetNextWindowSize(ImVec2(320, 100)) end
ImGui.SetNextWindowSizeConstraints(ImVec2(0, 0), ImVec2(G.FLT_MAX, G.FLT_MAX), function(data)
data.DesiredSize.y = math.floor(data.DesiredSize.x / data.UserData)
end, 16 / 9)
ImGui.Begin("Aspect"); local asp_size = ImGui.GetWindowSize(); ImGui.End()
if f == 5 then
check(step_size.x == 200 and step_size.y == 200, ("size callback Step %gx%g"):format(step_size.x, step_size.y))
check(asp_size.x == 320 and asp_size.y == 180, ("size callback AspectRatio %gx%g"):format(asp_size.x, asp_size.y))
end
ImGui.Render(); G.ImGui_ImplRoblox.RenderDrawData(ImGui.GetDrawData())
if f == 40 and #failed == 0 then print("HELPERS OK") end
end)
+94
View File
@@ -0,0 +1,94 @@
-- Library-only draw-data checksum (no demo, run with NO_DEMO=1): widgets + raw ImDrawList primitives with scripted input.
-- Must stay identical across demo edits; changes only when library rendering changes. Prints "LIBHASH <n>".
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)
local io = ImGui.GetIO()
local frame, h = 0, 0
local function mix(v) h = (h * 31 + math.floor(v * 64 + 0.5)) % 2147483647 end
local st = { b = false, f = 0.25, i = 42, c = ImVec4(0.2, 0.7, 0.4, 1.0), buf = ImGui.NewTextBuffer("hello", 64), r = 0, tab = 0, combo = 1, list = 2 }
local plot = {}
for i = 1, 40 do plot[i] = math.sin(i * 0.3) end
local poly = { ImVec2(0, 0), ImVec2(60, 10), ImVec2(30, 30), ImVec2(70, 60), ImVec2(0, 50) }
game:GetService("RunService").Stepped:Connect(function()
frame += 1
G.ImGui_ImplRoblox.NewFrame()
io.DeltaTime = 1 / 60
io:AddMousePosEvent(20 + (frame * 37) % 560, 20 + (frame * 53) % 560)
if frame % 7 == 0 then io:AddMouseButtonEvent(0, frame % 14 == 0) end
if frame % 11 == 0 then io:AddMouseWheelEvent(0, -1) end
ImGui.NewFrame()
ImGui.SetNextWindowPos(ImVec2(10, 10), G.ImGuiCond.Always); ImGui.SetNextWindowSize(ImVec2(320, 560), G.ImGuiCond.Always)
ImGui.Begin("Widgets")
ImGui.Text("Text %d", 123); ImGui.TextColored(ImVec4(1, 0.5, 0, 1), "Colored"); ImGui.TextWrapped("Wrapped text that is long enough to wrap around the window edge at least once.")
ImGui.Button("Button"); ImGui.SameLine(); ImGui.SmallButton("Small"); ImGui.SameLine(); ImGui.ArrowButton("##a", G.ImGuiDir.Left)
st.b = ImGui.Checkbox("Checkbox", st.b)
st.r = ImGui.RadioButton("R0", st.r, 0); ImGui.SameLine(); st.r = ImGui.RadioButton("R1", st.r, 1)
st.f = ImGui.SliderFloat("Slider", st.f, 0, 1); st.i = ImGui.DragInt("Drag", st.i, 1, 0, 100)
ImGui.InputText("Input", st.buf, 64)
ImGui.ColorEdit4("Color", st.c)
st.combo = ImGui.Combo("Combo", st.combo, { "AAA", "BBB", "CCC" })
st.list = ImGui.ListBox("List", st.list, { "one", "two", "three", "four" }, 4, 3)
ImGui.ProgressBar(0.4, ImVec2(-1, 0)); ImGui.ProgressBar(-1.0 * frame / 60, ImVec2(-1, 0), "busy")
ImGui.PlotLines("Lines", plot, nil, #plot); ImGui.PlotHistogram("Hist", plot, nil, #plot, 0, nil, -1, 1, ImVec2(0, 40))
ImGui.Bullet(); ImGui.Text("bullet"); ImGui.Separator(); ImGui.SeparatorText("Sep")
if ImGui.TreeNode("Tree") then ImGui.Text("inside"); ImGui.TreePop() end
ImGui.SetNextItemOpen(true, G.ImGuiCond.Once)
if ImGui.CollapsingHeader("Header") then
if ImGui.BeginTabBar("tabs") then
if ImGui.BeginTabItem("Tab A") then ImGui.Text("A"); ImGui.EndTabItem() end
if ImGui.BeginTabItem("Tab B") then ImGui.Text("B"); ImGui.EndTabItem() end
ImGui.EndTabBar()
end
if ImGui.BeginTable("t", 3, bit32.bor(G.ImGuiTableFlags.Borders, G.ImGuiTableFlags.RowBg, G.ImGuiTableFlags.Resizable)) then
ImGui.TableSetupColumn("One"); ImGui.TableSetupColumn("Two"); ImGui.TableSetupColumn("Three"); ImGui.TableHeadersRow()
for row = 0, 4 do ImGui.TableNextRow(); for col = 0, 2 do ImGui.TableSetColumnIndex(col); ImGui.Text("r%d c%d", row, col) end end
ImGui.EndTable()
end
end
ImGui.End()
ImGui.SetNextWindowPos(ImVec2(340, 10), G.ImGuiCond.Always); ImGui.SetNextWindowSize(ImVec2(300, 560), G.ImGuiCond.Always)
ImGui.Begin("Draw")
local dl, p = ImGui.GetWindowDrawList(), ImGui.GetCursorScreenPos()
local x, y, col = p.x + 4, p.y + 4, 0xFF40C0FF
for i, th in { 1.0, 2.5, 6.0 } do
local yy = y + (i - 1) * 70
dl:AddRect(ImVec2(x, yy), ImVec2(x + 50, yy + 50), col, 10.0, th)
dl:AddRectFilled(ImVec2(x + 60, yy), ImVec2(x + 110, yy + 50), col, 10.0, G.ImDrawFlags.RoundTopLeft or G.ImDrawFlags.RoundCornersTopLeft) -- RoundCornersTopLeft: pre-1.93 revs (hashes.sh)
dl:AddCircle(ImVec2(x + 145, yy + 25), 25, col, 0, th)
dl:AddNgon(ImVec2(x + 200, yy + 25), 25, col, 6, th)
dl:AddTriangle(ImVec2(x + 230, yy + 50), ImVec2(x + 255, yy), ImVec2(x + 280, yy + 50), col, th)
dl:AddLine(ImVec2(x, yy + 60), ImVec2(x + 280, yy + 55), col, th)
end
y += 210
dl:AddBezierCubic(ImVec2(x, y), ImVec2(x + 60, y - 30), ImVec2(x + 120, y + 60), ImVec2(x + 180, y), col, 3.0)
dl:AddBezierQuadratic(ImVec2(x, y + 30), ImVec2(x + 90, y - 10), ImVec2(x + 180, y + 30), col, 2.0)
dl:AddEllipse(ImVec2(x + 230, y + 15), ImVec2(40, 15), col, 0.5, 0, 2.0)
y += 50
local pts = {}
for i, v in poly do pts[i] = ImVec2(x + v.x, y + v.y) end
dl:AddConcavePolyFilled(pts, #pts, 0xFF8080FF)
for i, v in poly do pts[i] = ImVec2(x + 100 + v.x, y + v.y) end
dl:AddPolyline(pts, #pts, col, 4.0, G.ImDrawFlags.Closed)
for i, v in poly do pts[i] = ImVec2(x + 200 + v.x, y + v.y) end
dl:AddPolyline(pts, #pts, col, 1.0, 0)
y += 80
dl:AddRectFilledMultiColor(ImVec2(x, y), ImVec2(x + 100, y + 40), 0xFF0000FF, 0xFF00FF00, 0xFFFF0000, 0xFFFFFFFF)
dl:AddCircleFilled(ImVec2(x + 140, y + 20), 20, col)
dl:AddEllipseFilled(ImVec2(x + 210, y + 20), ImVec2(30, 15), col, 0.3)
dl:AddText(ImVec2(x, y + 50), 0xFFFFFFFF, "AddText")
ImGui.End()
ImGui.Render()
for _, dl in ImGui.GetDrawData().CmdLists:iter() do
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
end
G.ImGui_ImplRoblox.RenderDrawData(ImGui.GetDrawData())
if frame == ARG_FRAMES - 1 then print("LIBHASH", h) end
end)
+265
View File
@@ -0,0 +1,265 @@
-- Multi-select / box-select / typing-select: ImGuiSelectionBasicStorage + Selectable list (click, Ctrl+click, Shift+click, Ctrl+A,
-- Escape, box-select drag, keyboard nav + Shift, typing-select), ImGuiListClipper + IncludeItemByIndex(RangeSrcItem),
-- ImGuiSelectionExternalStorage on a bool array, checkboxes and tree nodes inside a multi-select scope.
local G = require(game:GetService("ReplicatedStorage"):WaitForChild("ImGui"))
local ImGui, ImVec2 = G.ImGui, G.ImVec2
local MSF = G.ImGuiMultiSelectFlags
ImGui.CreateContext(); G.ImGui_ImplRoblox.Init()
local io = ImGui.GetIO()
io.ConfigFlags = bit32.bor(io.ConfigFlags, G.ImGuiConfigFlags.NavEnableKeyboard)
local failed = {}
local function check(ok, msg) if not ok then table.insert(failed, msg); print("FAIL " .. msg) end end
local NAMES = { "Apple", "Apricot", "Banana", "Blueberry", "Cherry", "Coconut", "Date", "Durian", "Elderberry", "Fig",
"Grape", "Guava", "Kiwi", "Lemon", "Lime", "Mango", "Melon", "Orange", "Papaya", "Peach" }
local N = #NAMES
-- Iterate with GetNextSelectedItem() (sorted by key) -> "a,b,c"
local function sel_str(sel)
local out, it, id, has_more = {}, nil, 0, false
while true do
has_more, it, id = sel:GetNextSelectedItem(it)
if not has_more then break end
table.insert(out, tostring(id))
end
return table.concat(out, ",")
end
local function bools_str(t, n)
local out = {}
for i = 1, n do if t[i] then table.insert(out, tostring(i - 1)) end end
return table.concat(out, ",")
end
local sel_list = G.ImGuiSelectionBasicStorage()
local sel_clip = G.ImGuiSelectionBasicStorage()
local sel_tree = G.ImGuiSelectionBasicStorage()
local ext_bools = {}
local sel_ext = G.ImGuiSelectionExternalStorage()
sel_ext.UserData = ext_bools
sel_ext.AdapterSetItemSelected = function(self, idx, selected) self.UserData[idx + 1] = selected end
local chk_bools = {}
local sel_chk = G.ImGuiSelectionExternalStorage()
sel_chk.UserData = chk_bools
sel_chk.AdapterSetItemSelected = function(self, idx, selected) self.UserData[idx + 1] = selected end
local rects = {} -- [name] = { min, max } (copies)
local ids = {} -- [name] = item id
local function rect() local a, b = ImGui.GetItemRectMin(), ImGui.GetItemRectMax(); return { ImVec2(a.x, a.y), ImVec2(b.x, b.y) } end
local function center(name) local r = rects[name]; return (r[1].x + r[2].x) / 2, (r[1].y + r[2].y) / 2 end
local function key(k) io:AddKeyEvent(k, true); io:AddKeyEvent(k, false) end
local function mods(ctrl, shift) io:AddKeyEvent(G.ImGuiMod_Ctrl, ctrl == true); io:AddKeyEvent(G.ImGuiMod_Shift, shift == true) end
-- Schedule: each step = { input = fn (before NewFrame), check = fn (after Render) }. Steps run on consecutive frames.
local steps = {}
local function step(input, chk) table.insert(steps, { input = input, check = chk }) end
local function idle(n) for _ = 1, n or 1 do step(nil, nil) end end
-- click: press (+mods) on frame A, release on frame B, then one idle frame for EndMultiSelect requests to be applied
local function click(name, ctrl, shift)
step(function() mods(ctrl, shift); io:AddMousePosEvent(center(name)); io:AddMouseButtonEvent(0, true) end)
step(function() io:AddMouseButtonEvent(0, false) end)
step(function() mods(false, false) end)
end
local typed_focus_idx = -1
local clip_scroll_request = false
local f = 0
idle(3)
-- 1. List: click selects one
click("L2"); step(nil, function() check(sel_str(sel_list) == "2", "click: " .. sel_str(sel_list)) end)
-- 2. Ctrl+click toggles
click("L5", true); step(nil, function() check(sel_str(sel_list) == "2,5", "ctrl+click add: " .. sel_str(sel_list)) end)
click("L2", true); step(nil, function() check(sel_str(sel_list) == "5", "ctrl+click remove: " .. sel_str(sel_list)) end)
-- 3. Shift+click range (from RangeSrc = 2, the last Ctrl+clicked item)
click("L8", false, true); step(nil, function() check(sel_str(sel_list) == "2,3,4,5,6,7,8", "shift+click range: " .. sel_str(sel_list)) end)
-- 4. Ctrl+A
step(function() io:AddKeyEvent(G.ImGuiMod_Ctrl, true); io:AddKeyEvent(G.ImGuiKey.A, true) end)
step(function() io:AddKeyEvent(G.ImGuiKey.A, false); io:AddKeyEvent(G.ImGuiMod_Ctrl, false) end)
step(nil, function() check(sel_list.Size == N, "ctrl+A size " .. sel_list.Size) end)
-- 5. Escape clears (ClearOnEscape)
step(function() key(G.ImGuiKey.Escape) end)
step(nil, function() check(sel_list.Size == 0 and sel_str(sel_list) == "", "escape clear: " .. sel_str(sel_list)) end)
-- 6. Box-select drag (BoxSelect1d) from item 2 to item 5
step(function() io:AddMousePosEvent(center("L2")); io:AddMouseButtonEvent(0, true) end)
step(function() local x, y = center("L2"); io:AddMousePosEvent(x + 2, y + 10) end)
step(function() local x, y = center("L4"); io:AddMousePosEvent(x + 4, y) end)
step(function() local x, y = center("L5"); io:AddMousePosEvent(x + 4, y) end,
function() check(ImGui.GetCurrentContext().BoxSelectState.IsActive, "box-select active while dragging") end)
step(nil)
step(function() io:AddMouseButtonEvent(0, false) end)
step(nil, function() check(sel_str(sel_list) == "2,3,4,5", "box-select: " .. sel_str(sel_list)); check(not ImGui.GetCurrentContext().BoxSelectState.IsActive, "box-select ended") end)
-- 7. Keyboard nav: click 3, Down -> 4 (auto-select), Shift+Down x2 -> 4..6
click("L3"); step(nil, function() check(sel_str(sel_list) == "3", "click before nav: " .. sel_str(sel_list)) end)
step(function() key(G.ImGuiKey.DownArrow) end); idle(2)
step(nil, function() check(sel_str(sel_list) == "4", "nav down auto-select: " .. sel_str(sel_list)) end)
step(function() io:AddKeyEvent(G.ImGuiMod_Shift, true); key(G.ImGuiKey.DownArrow) end); idle(1)
step(function() key(G.ImGuiKey.DownArrow) end); idle(1)
step(function() io:AddKeyEvent(G.ImGuiMod_Shift, false) end); idle(1)
step(nil, function() check(sel_str(sel_list) == "4,5,6", "shift+nav range: " .. sel_str(sel_list)) end)
-- 8. Typing-select: "ch" -> Cherry (5), then single-char mode "b" "b" -> Banana (2) then Blueberry (3)
step(function() io:AddInputCharacter(string.byte("c")); io:AddInputCharacter(string.byte("h")) end); idle(2)
step(nil, function() check(ImGui.GetCurrentContext().NavId == ids.L4, "typing 'ch' -> Cherry"); check(sel_str(sel_list) == "4", "typing focus selects: " .. sel_str(sel_list)) end)
idle(120) -- TYPING_SELECT_RESET_TIMER
step(function() io:AddInputCharacter(string.byte("b")) end); idle(2)
step(nil, function() check(ImGui.GetCurrentContext().NavId == ids.L2, "typing 'b' -> Banana") end)
step(function() io:AddInputCharacter(string.byte("b")) end); idle(2)
step(nil, function() check(ImGui.GetCurrentContext().NavId == ids.L3, "typing 'bb' -> Blueberry") end)
idle(120)
-- 9. Clipper (1000 items): click 5, scroll down, Shift+click item 40 -> 5..40 (RangeSrcItem 5 kept submitted by IncludeItemByIndex)
click("C5")
-- move NavId away with Ctrl+Down (no selection change, RangeSrc stays 5) so the clipper doesn't keep item 5 alive as the nav item
step(function() io:AddKeyEvent(G.ImGuiMod_Ctrl, true); key(G.ImGuiKey.DownArrow) end); idle(1)
step(function() key(G.ImGuiKey.DownArrow) end); idle(1)
step(function() key(G.ImGuiKey.DownArrow) end); idle(1)
step(function() io:AddKeyEvent(G.ImGuiMod_Ctrl, false) end)
step(nil, function() check(sel_str(sel_clip) == "5", "clipper ctrl+nav keeps selection: " .. sel_str(sel_clip)) end)
step(function() clip_scroll_request = true end); idle(2)
step(nil, function() check(rects.C40 ~= nil and rects.C5 == nil, "clipper scrolled (item 5 not visible)") end)
click("C40", false, true)
step(nil, function() check(sel_clip.Size == 36, "clipper shift+click range size " .. sel_clip.Size); check(sel_clip:Contains(5) and sel_clip:Contains(40) and not sel_clip:Contains(4) and not sel_clip:Contains(41), "clipper range bounds") end)
-- 10. External storage on bool array: click 1, Shift+click 3, Ctrl+click 2
click("E1"); click("E3", false, true)
step(nil, function() check(bools_str(ext_bools, 10) == "1,2,3", "external storage range: " .. bools_str(ext_bools, 10)) end)
click("E2", true)
step(nil, function() check(bools_str(ext_bools, 10) == "1,3", "external storage ctrl+click: " .. bools_str(ext_bools, 10)) end)
-- 11. Checkboxes (NoAutoSelect|NoAutoClear): click 1 toggles on, Shift+click 4 -> 1..4 on, click 2 toggles off
click("K1"); step(nil, function() check(bools_str(chk_bools, 8) == "1", "checkbox click: " .. bools_str(chk_bools, 8)) end)
click("K4", false, true); step(nil, function() check(bools_str(chk_bools, 8) == "1,2,3,4", "checkbox shift+click: " .. bools_str(chk_bools, 8)) end)
click("K2"); step(nil, function() check(bools_str(chk_bools, 8) == "1,3,4", "checkbox toggle off: " .. bools_str(chk_bools, 8)) end)
-- 12. Tree nodes: click 0, Ctrl+click 2, Shift+click 4 (range from 2)
click("T0"); click("T2", true)
step(nil, function() check(sel_str(sel_tree) == "0,2", "tree ctrl+click: " .. sel_str(sel_tree)) end)
click("T4", false, true)
step(nil, function() check(sel_str(sel_tree) == "2,3,4", "tree shift+click: " .. sel_str(sel_tree)) end)
-- 13. Metrics window renders the TypingSelect / MultiSelect debug nodes
local show_metrics = false
step(function() show_metrics = true end); idle(3)
step(nil, function() if #failed == 0 then print("MULTISELECT OK") end end)
local function list_window()
ImGui.SetNextWindowPos(ImVec2(10, 10)); ImGui.SetNextWindowSize(ImVec2(200, 420))
ImGui.Begin("L")
local flags = bit32.bor(MSF.ClearOnEscape, MSF.BoxSelect1d)
local ms_io = ImGui.BeginMultiSelect(flags, sel_list.Size, N)
sel_list:ApplyRequests(ms_io)
-- typing-select
local focus_idx = -1
if ImGui.IsWindowFocused() then
local req = ImGui.GetTypingSelectRequest(G.ImGuiTypingSelectFlags.AllowSingleCharMode)
local nav_idx = (type(ms_io.NavIdItem) == "number") and ms_io.NavIdItem or -1
focus_idx = ImGui.TypingSelectFindMatch(req, N, function(_, idx) return NAMES[idx + 1] end, nil, nav_idx)
end
for n = 0, N - 1 do
if n == focus_idx then ImGui.SetKeyboardFocusHere() end
ImGui.SetNextItemSelectionUserData(n)
ImGui.Selectable(NAMES[n + 1], sel_list:Contains(n))
rects["L" .. n] = rect(); ids["L" .. n] = ImGui.GetItemID()
end
ms_io = ImGui.EndMultiSelect()
sel_list:ApplyRequests(ms_io)
ImGui.End()
end
local function clipper_window()
ImGui.SetNextWindowPos(ImVec2(220, 10)); ImGui.SetNextWindowSize(ImVec2(200, 300))
ImGui.Begin("C")
if clip_scroll_request then ImGui.SetScrollY(ImGui.GetTextLineHeightWithSpacing() * 30); clip_scroll_request = false end
local COUNT = 1000
local ms_io = ImGui.BeginMultiSelect(MSF.ClearOnEscape, sel_clip.Size, COUNT)
sel_clip:ApplyRequests(ms_io)
local clipper = G.ImGuiListClipper()
clipper:Begin(COUNT)
if ms_io.RangeSrcItem ~= -1 then clipper:IncludeItemByIndex(ms_io.RangeSrcItem) end -- Ensure RangeSrc item is not clipped.
for k in rects do if string.sub(k, 1, 1) == "C" then rects[k] = nil end end
while clipper:Step() do
for n = clipper.DisplayStart, clipper.DisplayEnd - 1 do
ImGui.SetNextItemSelectionUserData(n)
ImGui.Selectable(("Object %04d"):format(n), sel_clip:Contains(n))
if ImGui.IsItemVisible() then rects["C" .. n] = rect() end
end
end
ms_io = ImGui.EndMultiSelect()
sel_clip:ApplyRequests(ms_io)
ImGui.End()
end
local function external_window()
ImGui.SetNextWindowPos(ImVec2(430, 10)); ImGui.SetNextWindowSize(ImVec2(180, 220))
ImGui.Begin("E")
local ms_io = ImGui.BeginMultiSelect(MSF.None, -1, 10)
sel_ext:ApplyRequests(ms_io)
for n = 0, 9 do
ImGui.SetNextItemSelectionUserData(n)
ImGui.Selectable("Ext " .. n, ext_bools[n + 1] == true)
rects["E" .. n] = rect()
end
ms_io = ImGui.EndMultiSelect()
sel_ext:ApplyRequests(ms_io)
ImGui.End()
end
local function checkbox_window()
ImGui.SetNextWindowPos(ImVec2(430, 240)); ImGui.SetNextWindowSize(ImVec2(180, 200))
ImGui.Begin("K")
local ms_io = ImGui.BeginMultiSelect(bit32.bor(MSF.NoAutoSelect, MSF.NoAutoClear, MSF.ClearOnEscape), -1, 8)
sel_chk:ApplyRequests(ms_io)
for n = 0, 7 do
ImGui.SetNextItemSelectionUserData(n)
local _, v = ImGui.Checkbox("Check " .. n, chk_bools[n + 1] == true)
rects["K" .. n] = rect()
local _ = v -- the selection is updated through requests (ApplyRequests), like upstream's demo
end
ms_io = ImGui.EndMultiSelect()
sel_chk:ApplyRequests(ms_io)
ImGui.End()
end
local function tree_window()
ImGui.SetNextWindowPos(ImVec2(220, 320)); ImGui.SetNextWindowSize(ImVec2(200, 160))
ImGui.Begin("T")
local ms_io = ImGui.BeginMultiSelect(MSF.None, sel_tree.Size, 5)
sel_tree:ApplyRequests(ms_io)
for n = 0, 4 do
local flags = bit32.bor(G.ImGuiTreeNodeFlags.Leaf, G.ImGuiTreeNodeFlags.NoTreePushOnOpen, G.ImGuiTreeNodeFlags.SpanAvailWidth)
if sel_tree:Contains(n) then flags = bit32.bor(flags, G.ImGuiTreeNodeFlags.Selected) end
ImGui.SetNextItemSelectionUserData(n)
ImGui.TreeNodeEx("Node " .. n, flags)
rects["T" .. n] = rect()
end
ms_io = ImGui.EndMultiSelect()
sel_tree:ApplyRequests(ms_io)
ImGui.End()
end
game:GetService("RunService").Stepped:Connect(function()
f += 1
G.ImGui_ImplRoblox.NewFrame()
local s = steps[f]
if s and s.input then s.input() end
ImGui.NewFrame()
list_window()
clipper_window()
external_window()
checkbox_window()
tree_window()
if show_metrics then
ImGui.SetNextWindowPos(ImVec2(620, 10)); ImGui.SetNextWindowSize(ImVec2(400, 600))
ImGui.ShowMetricsWindow()
local ctx = ImGui.GetCurrentContext()
ImGui.Begin("DebugNodes")
ImGui.DebugNodeTypingSelectState(ctx.TypingSelectState)
for n = 0, ctx.MultiSelectStorage:GetMapSize() - 1 do
local st = ctx.MultiSelectStorage:GetByIndex(n)
if st then ImGui.SetNextItemOpen(true); ImGui.DebugNodeMultiSelectState(st) end
end
ImGui.End()
end
ImGui.Render(); G.ImGui_ImplRoblox.RenderDrawData(ImGui.GetDrawData())
if s and s.check then s.check() end
end)
print(("t_multiselect: %d steps"):format(#steps))
+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
-- [/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
RunService.Stepped:Connect(function()
frame += 1
G.ImGui_ImplRoblox.NewFrame()
ImGui.NewFrame()
MyFirstTool(); MyTool(); Conventions(); MenusAndTable(); Plot(); CustomDrawing()
MyFirstTool(); MyTool(); Conventions(); MenusAndTable(); Plot(); CustomDrawing(); StatsWindow()
ImGui.Render()
G.ImGui_ImplRoblox.RenderDrawData(ImGui.GetDrawData())
if frame == 20 then
+138
View File
@@ -0,0 +1,138 @@
-- Screenshot of a fixed scene (t_libhash's widgets + primitives, optionally the Custom Rendering example):
-- composites the ScreenGui like t_composite and prints "ROW y hex". tools/test/shot.py turns it into a PNG.
-- RUN_FLAGS: ARG_APP=true also opens ShowExampleAppCustomRendering (needs the demo).
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)
local io = ImGui.GetIO()
local W, H = 1280, 760
local frame = 0
local st = { b = false, f = 0.25, i = 42, c = ImVec4(0.2, 0.7, 0.4, 1.0), buf = ImGui.NewTextBuffer("hello", 64), r = 0, tab = 0, combo = 1, list = 2 }
local plot = {}
for i = 1, 40 do plot[i] = math.sin(i * 0.3) end
local poly = { ImVec2(0, 0), ImVec2(60, 10), ImVec2(30, 30), ImVec2(70, 60), ImVec2(0, 50) }
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) -- table.sort isn't stable, but siblings with equal ZIndex never overlap here
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 >= ky0 and sy < ky1 then
for tx = 0, sz - 1 do
local sx = x + tx
if sx >= kx0 and sx < kx1 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)
return screen
end
game:GetService("RunService").Stepped:Connect(function()
frame += 1
G.ImGui_ImplRoblox.NewFrame()
io.DeltaTime = 1 / 60
ImGui.NewFrame()
ImGui.SetNextWindowPos(ImVec2(10, 10), G.ImGuiCond.Always); ImGui.SetNextWindowSize(ImVec2(320, 560), G.ImGuiCond.Always)
ImGui.Begin("Widgets")
ImGui.Text("Text %d", 123); ImGui.TextColored(ImVec4(1, 0.5, 0, 1), "Colored"); ImGui.TextWrapped("Wrapped text that is long enough to wrap around the window edge at least once.")
ImGui.Button("Button"); ImGui.SameLine(); ImGui.SmallButton("Small"); ImGui.SameLine(); ImGui.ArrowButton("##a", G.ImGuiDir.Left)
st.b = ImGui.Checkbox("Checkbox", st.b)
st.r = ImGui.RadioButton("R0", st.r, 0); ImGui.SameLine(); st.r = ImGui.RadioButton("R1", st.r, 1)
st.f = ImGui.SliderFloat("Slider", st.f, 0, 1); st.i = ImGui.DragInt("Drag", st.i, 1, 0, 100)
ImGui.InputText("Input", st.buf, 64)
ImGui.ColorEdit4("Color", st.c)
st.combo = ImGui.Combo("Combo", st.combo, { "AAA", "BBB", "CCC" })
st.list = ImGui.ListBox("List", st.list, { "one", "two", "three", "four" }, 4, 3)
ImGui.ProgressBar(0.4, ImVec2(-1, 0)); ImGui.ProgressBar(-1.0 * frame / 60, ImVec2(-1, 0), "busy")
ImGui.PlotLines("Lines", plot, nil, #plot); ImGui.PlotHistogram("Hist", plot, nil, #plot, 0, nil, -1, 1, ImVec2(0, 40))
ImGui.Bullet(); ImGui.Text("bullet"); ImGui.Separator(); ImGui.SeparatorText("Sep")
if ImGui.TreeNode("Tree") then ImGui.Text("inside"); ImGui.TreePop() end
ImGui.SetNextItemOpen(true, G.ImGuiCond.Once)
if ImGui.CollapsingHeader("Header") then
if ImGui.BeginTabBar("tabs") then
if ImGui.BeginTabItem("Tab A") then ImGui.Text("A"); ImGui.EndTabItem() end
if ImGui.BeginTabItem("Tab B") then ImGui.Text("B"); ImGui.EndTabItem() end
ImGui.EndTabBar()
end
if ImGui.BeginTable("t", 3, bit32.bor(G.ImGuiTableFlags.Borders, G.ImGuiTableFlags.RowBg, G.ImGuiTableFlags.Resizable)) then
ImGui.TableSetupColumn("One"); ImGui.TableSetupColumn("Two"); ImGui.TableSetupColumn("Three"); ImGui.TableHeadersRow()
for row = 0, 4 do ImGui.TableNextRow(); for col = 0, 2 do ImGui.TableSetColumnIndex(col); ImGui.Text("r%d c%d", row, col) end end
ImGui.EndTable()
end
end
ImGui.End()
ImGui.SetNextWindowPos(ImVec2(340, 10), G.ImGuiCond.Always); ImGui.SetNextWindowSize(ImVec2(300, 560), G.ImGuiCond.Always)
ImGui.Begin("Draw")
local dl, p = ImGui.GetWindowDrawList(), ImGui.GetCursorScreenPos()
local x, y, col = p.x + 4, p.y + 4, 0xFF40C0FF
for i, th in { 1.0, 2.5, 6.0 } do
local yy = y + (i - 1) * 70
dl:AddRect(ImVec2(x, yy), ImVec2(x + 50, yy + 50), col, 10.0, th)
dl:AddRectFilled(ImVec2(x + 60, yy), ImVec2(x + 110, yy + 50), col, 10.0, G.ImDrawFlags.RoundTopLeft or G.ImDrawFlags.RoundCornersTopLeft) -- RoundCornersTopLeft: pre-1.93 revs (hashes.sh)
dl:AddCircle(ImVec2(x + 145, yy + 25), 25, col, 0, th)
dl:AddNgon(ImVec2(x + 200, yy + 25), 25, col, 6, th)
dl:AddTriangle(ImVec2(x + 230, yy + 50), ImVec2(x + 255, yy), ImVec2(x + 280, yy + 50), col, th)
dl:AddLine(ImVec2(x, yy + 60), ImVec2(x + 280, yy + 55), col, th)
end
y += 210
dl:AddBezierCubic(ImVec2(x, y), ImVec2(x + 60, y - 30), ImVec2(x + 120, y + 60), ImVec2(x + 180, y), col, 3.0)
dl:AddBezierQuadratic(ImVec2(x, y + 30), ImVec2(x + 90, y - 10), ImVec2(x + 180, y + 30), col, 2.0)
dl:AddEllipse(ImVec2(x + 230, y + 15), ImVec2(40, 15), col, 0.5, 0, 2.0)
y += 50
local pts = {}
for i, v in poly do pts[i] = ImVec2(x + v.x, y + v.y) end
dl:AddConcavePolyFilled(pts, #pts, 0xFF8080FF)
for i, v in poly do pts[i] = ImVec2(x + 100 + v.x, y + v.y) end
dl:AddPolyline(pts, #pts, col, 4.0, G.ImDrawFlags.Closed)
for i, v in poly do pts[i] = ImVec2(x + 200 + v.x, y + v.y) end
dl:AddPolyline(pts, #pts, col, 1.0, 0)
y += 80
dl:AddRectFilledMultiColor(ImVec2(x, y), ImVec2(x + 100, y + 40), 0xFF0000FF, 0xFF00FF00, 0xFFFF0000, 0xFFFFFFFF)
dl:AddCircleFilled(ImVec2(x + 140, y + 20), 20, col)
dl:AddEllipseFilled(ImVec2(x + 210, y + 20), ImVec2(30, 15), col, 0.3)
dl:AddText(ImVec2(x, y + 50), 0xFFFFFFFF, "AddText")
ImGui.End()
if ARG_APP then ImGui.SetNextWindowPos(ImVec2(650, 10), G.ImGuiCond.Always); ImGui.SetNextWindowSize(ImVec2(620, 740), G.ImGuiCond.Always); G.ShowExampleAppCustomRendering(true) end
ImGui.Render()
G.ImGui_ImplRoblox.RenderDrawData(ImGui.GetDrawData())
if frame == ARG_FRAMES - 1 then
local screen = composite(game:GetService("Players").LocalPlayer.PlayerGui:FindFirstChild("ImGui"))
for y = 0, H - 1 do
local t = table.create(W * 3)
for i = 0, W * 3 - 1 do t[i + 1] = string.format("%02x", buffer.readu8(screen, y * W * 3 + i)) end
print("ROW", y, table.concat(t))
end
end
end)
+1 -1
View File
@@ -2,7 +2,7 @@
local G = require(game:GetService("ReplicatedStorage"):WaitForChild("ImGui"))
local ImGui, ImVec2 = G.ImGui, G.ImVec2
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 buf = ImGui.NewTextBuffer("", 64)
local proxy
+14
View File
@@ -0,0 +1,14 @@
#!/usr/bin/env python3
"""Headless screenshot: tools/test/shot.py out.png [--app] (runs scripts/t_shot.luau, writes a PNG; stdlib only)"""
import os, subprocess, sys, struct, zlib
HERE = os.path.dirname(os.path.abspath(__file__))
env = dict(os.environ, FULL_OUTPUT="1", RUN_FLAGS="ARG_APP=true" if "--app" in sys.argv else "")
out = subprocess.run([sys.executable, os.path.join(HERE, "run.py"), "--quiet", "--roblox", "--frames", "30", "--timeout", "600",
"--main", os.path.join(HERE, "scripts", "t_shot.luau")], env=env, capture_output=True, text=True).stdout
rows = {int(l.split()[1]): bytes.fromhex(l.split()[2]) for l in out.splitlines() if l.startswith("ROW")}
assert rows, out[-2000:]
w, h = len(rows[0]) // 3, len(rows)
raw = b"".join(b"\0" + rows[y] for y in range(h))
chunk = lambda t, d: struct.pack(">I", len(d)) + t + d + struct.pack(">I", zlib.crc32(t + d) & 0xffffffff)
open(sys.argv[1], "wb").write(b"\x89PNG\r\n\x1a\n" + chunk(b"IHDR", struct.pack(">IIBBBBB", w, h, 8, 2, 0, 0, 0)) + chunk(b"IDAT", zlib.compress(raw, 6)) + chunk(b"IEND", b""))
print("wrote", sys.argv[1], w, h)
+10
View File
@@ -0,0 +1,10 @@
[package]
name = "4dbug/imgui-luau"
version = "0.3.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]