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

This commit is contained in:
4DBug
2026-10-15 08:52:30 -05:00
parent 9be05aeb5c
commit e3840a938a
16 changed files with 7058 additions and 2073 deletions
+180 -42
View File
@@ -2598,12 +2598,14 @@ function ImGui.RoundScalarWithFormatT(format, data_type, v)
return v -- Don't apply if the value is not visible in the format string
end
-- TODO: Sanitize format
-- Sanitize format
local fmt_sanitized = ImParseFormatSanitizeForPrinting(string.sub(format, fmt_start), nil, 32)
-- Format value with our rounding, and read back
local str = ImFormatString(format, v)
local v_str = ImFormatString(fmt_sanitized, v)
-- [Luau] leading spaces: ImAtof() skips them
v = ImAtof(str) or v
v = ImAtof(v_str) or v
return v
end
@@ -2689,6 +2691,30 @@ local function ImParseFormatTrimDecorations(fmt)
return string.sub(fmt, fmt_start, fmt_end - 1)
end
-- Sanitize format
-- - Zero terminate so extra characters after format (e.g. "%f123") don't confuse atof/atoi
-- - stb_sprintf.h supports several new modifiers which format numbers in a way that also makes them incompatible atof/atoi.
-- [Luau] fmt_out is a byte table (may be nil); returns the sanitized format as a string
--- @param fmt_in string
--- @param fmt_out char[]?
--- @param fmt_out_size size_t
function ImParseFormatSanitizeForPrinting(fmt_in, fmt_out, fmt_out_size)
local fmt_end = ImParseFormatFindEnd(fmt_in, 1)
-- IM_UNUSED(fmt_out_size)
IM_ASSERT(fmt_end < fmt_out_size) -- Format is too long, let us know if this happens to you!
fmt_out = fmt_out or {}
local n = 0
for i = 1, fmt_end - 1 do
local c = string.byte(fmt_in, i)
if c ~= 39 and c ~= 36 and c ~= 95 then -- Custom flags provided by stb_sprintf.h. POSIX 2008 also supports '.
n = n + 1
fmt_out[n] = c
end
end
fmt_out[n + 1] = 0 -- Zero-terminate
return string.char(unpack(fmt_out, 1, n))
end
-- - For scanning we need to remove all width and precision fields and flags "%+3.7f" -> "%f". BUT don't strip types like "%I64d" which includes digits. ! "%07I64d" -> "%I64d"
--- @param fmt_in string
--- @param fmt_out char[]
@@ -3290,6 +3316,18 @@ function ImGui.DragBehaviorT(TYPE, SIGNEDTYPE, data_type, v, v_speed, v_min, v_m
return v, true
end
local function ShortcutsForCancel(id)
local g = GImGui
local is_cancel_with_keyboard = ImGui.Shortcut(ImGuiKey.Escape, ImGuiInputFlags.None, id)
-- [Luau] ImGuiKey_NavGamepadCancel macro inlined (io.ConfigNavSwapGamepadButtons is not ported: always FaceRight)
local is_cancel_with_gamepad = bit32.band(g.IO.ConfigFlags, ImGuiConfigFlags.NavEnableGamepad) ~= 0 and bit32.band(g.IO.BackendFlags, ImGuiBackendFlags.HasGamepad) ~= 0 and ImGui.Shortcut(ImGuiKey.GamepadFaceRight, ImGuiInputFlags.None, id)
local is_cancel_with_mouse = ImGui.IsMouseReleased(ImGuiMouseButton.Right, id)
if is_cancel_with_mouse then
ImGui.SetKeyOwner(ImGuiKey.MouseRight, id)
end
return is_cancel_with_keyboard or is_cancel_with_gamepad or is_cancel_with_mouse
end
--- @param id ImGuiID
--- @param data_type ImGuiDataType
--- @param v number
@@ -3311,6 +3349,13 @@ function ImGui.DragBehavior(id, data_type, v, v_speed, min, max, format, flags)
elseif (g.ActiveIdSource == ImGuiInputSource.Keyboard or g.ActiveIdSource == ImGuiInputSource.Gamepad) and g.NavActivatePressedId == id and not g.ActiveIdIsJustActivated then
ImGui.ClearActiveID()
end
if ShortcutsForCancel(id) and g.ActiveId == id then
-- Canceling action reverts to initial value
local value_changed = (v ~= g.ActiveIdValueOnActivation)
v = g.ActiveIdValueOnActivation
ImGui.ClearActiveID()
return v, value_changed
end
end
if g.ActiveId ~= id then
return v, false
@@ -3869,9 +3914,7 @@ function ImGui.SliderBehaviorT(TYPE, SIGNEDTYPE, bb, id, data_type, v, v_min, v_
local set_new_value = false
local clicked_t = 0.0
if g.ActiveIdSource == ImGuiInputSource.Mouse then
if not g.IO.MouseDown[0] then
ImGui.ClearActiveID()
else
do
local mouse_abs_pos = g.IO.MousePos[axis_k]
if g.ActiveIdIsJustActivated then
local grab_t = ImGui.ScaleRatioFromValueT(data_type, v, v_min, v_max, logarithmic_zero_epsilon, zero_deadzone_halfsize)
@@ -3891,11 +3934,6 @@ function ImGui.SliderBehaviorT(TYPE, SIGNEDTYPE, bb, id, data_type, v, v_min, v_
set_new_value = true
end
elseif g.ActiveIdSource == ImGuiInputSource.Keyboard or g.ActiveIdSource == ImGuiInputSource.Gamepad then
if g.ActiveIdIsJustActivated then
g.SliderCurrentAccum = 0.0 -- Reset any stored nav delta upon activation
g.SliderCurrentAccumDirty = false
end
local input_delta = (axis == ImGuiAxis.X) and ImGui.GetNavTweakPressedAmount(axis) or -ImGui.GetNavTweakPressedAmount(axis)
if input_delta ~= 0.0 then
local tweak_slow = ImGui.IsKeyDown((g.NavInputSource == ImGuiInputSource.Gamepad) and ImGuiKey.NavGamepadTweakSlow or ImGuiKey.NavKeyboardTweakSlow)
@@ -3922,9 +3960,7 @@ function ImGui.SliderBehaviorT(TYPE, SIGNEDTYPE, bb, id, data_type, v, v_min, v_
end
local delta = g.SliderCurrentAccum
if g.NavActivatePressedId == id and not g.ActiveIdIsJustActivated then
ImGui.ClearActiveID()
elseif g.SliderCurrentAccumDirty then
if g.SliderCurrentAccumDirty then
clicked_t = ImGui.ScaleRatioFromValueT(data_type, v, v_min, v_max, logarithmic_zero_epsilon, zero_deadzone_halfsize)
if (clicked_t >= 1.0 and delta > 0.0) or (clicked_t <= 0.0 and delta < 0.0) then -- This is to avoid applying the saturation when already past the limits
@@ -4016,6 +4052,30 @@ function ImGui.SliderBehavior(bb, id, data_type, v, min, max, format, flags, out
IM_ASSERT((flags == 1 or bit32.band(flags, ImGuiSliderFlags.InvalidMask_) == 0), "Invalid ImGuiSliderFlags flags! Has the legacy 'float power' argument been mistakenly cast to flags? Call function with ImGuiSliderFlags_Logarithmic flags instead.")
IM_ASSERT(bit32.band(flags, ImGuiSliderFlags.WrapAround) == 0) -- Not supported by SliderXXX(), only by DragXXX()
-- Things we can do easily outside the SliderBehaviorT<> template, saves code generation.
local g = GImGui
if g.ActiveId == id then
if g.ActiveIdSource == ImGuiInputSource.Mouse and not g.IO.MouseDown[0] then
ImGui.ClearActiveID()
end
if g.ActiveIdSource == ImGuiInputSource.Keyboard or g.ActiveIdSource == ImGuiInputSource.Gamepad then
if g.ActiveIdIsJustActivated then
g.SliderCurrentAccum = 0.0 -- Reset any stored nav delta upon activation
g.SliderCurrentAccumDirty = false
end
if g.NavActivatePressedId == id and not g.ActiveIdIsJustActivated then
ImGui.ClearActiveID()
end
end
if ShortcutsForCancel(id) and g.ActiveId == id then
-- Canceling action reverts to initial value
local value_changed = (v ~= g.ActiveIdValueOnActivation)
v = g.ActiveIdValueOnActivation
ImGui.ClearActiveID()
return v, value_changed
end
end
if data_type == ImGuiDataType.S8 then
local v32 = (ImS32)((ImS8)(v)); local r
v32, r = ImGui.SliderBehaviorT(ImS32, ImS32, bb, id, ImGuiDataType.S32, v32, min, max, format, flags, out_grab_bb)
@@ -4602,7 +4662,7 @@ end
--- @param obj ImGuiInputTextState
--- @param idx int
local function STB_TEXTEDIT_MOVEWORDRIGHT_MAC(obj, idx)
function ImStb.TEXTEDIT_MOVEWORDRIGHT_MAC(obj, idx)
local len = obj.TextLen
idx = ImStb.TEXTEDIT_GETNEXTCHARINDEX(obj, idx)
while idx <= len and not ImStb.is_word_boundary_from_left(obj, idx) do
@@ -4613,7 +4673,7 @@ end
--- @param obj ImGuiInputTextState
--- @param idx int
local function STB_TEXTEDIT_MOVEWORDRIGHT_WIN(obj, idx)
function ImStb.TEXTEDIT_MOVEWORDRIGHT_WIN(obj, idx)
idx = ImStb.TEXTEDIT_GETNEXTCHARINDEX(obj, idx)
local len = obj.TextLen
while idx <= len and not ImStb.is_word_boundary_from_right(obj, idx) do
@@ -4627,9 +4687,9 @@ end
function ImStb.TEXTEDIT_MOVEWORDRIGHT(obj, idx)
local g = obj.Ctx
if g.IO.ConfigMacOSXBehaviors then
return STB_TEXTEDIT_MOVEWORDRIGHT_MAC(obj, idx)
return ImStb.TEXTEDIT_MOVEWORDRIGHT_MAC(obj, idx)
else
return STB_TEXTEDIT_MOVEWORDRIGHT_WIN(obj, idx)
return ImStb.TEXTEDIT_MOVEWORDRIGHT_WIN(obj, idx)
end
end
@@ -4830,6 +4890,13 @@ function MT.ImGuiInputTextState:CursorClamp()
end
function MT.ImGuiInputTextState:HasSelection() return self.Stb.select_start ~= self.Stb.select_end end
function MT.ImGuiInputTextState:ClearSelection() self.Stb.select_start = self.Stb.cursor; self.Stb.select_end = self.Stb.cursor end
-- [Luau] positions are 1-based byte indices (upstream: 0-based)
function MT.ImGuiInputTextState:GetCursorPos() return self.Stb.cursor end
function MT.ImGuiInputTextState:GetSelectionStart() return self.Stb.select_start end
function MT.ImGuiInputTextState:GetSelectionEnd() return self.Stb.select_end end
function MT.ImGuiInputTextState:SetSelection(start, _end) self.Stb.select_start = start; self.Stb.cursor = _end; self.Stb.select_end = _end end
function MT.ImGuiInputTextState:GetPreferredOffsetX() if self.Stb.has_preferred_x then return self.Stb.preferred_x else return -1 end end
function MT.ImGuiInputTextState:SelectAll()
self.Stb.select_start = 1
@@ -4838,6 +4905,10 @@ function MT.ImGuiInputTextState:SelectAll()
self.Stb.has_preferred_x = false
end
function MT.ImGuiInputTextState:ReloadUserBufAndSelectAll() self.WantReloadUserBuf = true; self.ReloadSelectionStart = 1; self.ReloadSelectionEnd = INT_MAX end
function MT.ImGuiInputTextState:ReloadUserBufAndKeepSelection() self.WantReloadUserBuf = true; self.ReloadSelectionStart = self.Stb.select_start; self.ReloadSelectionEnd = self.Stb.select_end end
function MT.ImGuiInputTextState:ReloadUserBufAndMoveToEnd() self.WantReloadUserBuf = true; self.ReloadSelectionStart = INT_MAX; self.ReloadSelectionEnd = INT_MAX end
end
function ImGui.PushPasswordFont()
@@ -4989,9 +5060,9 @@ local function InputTextFilterCharacter(ctx, state, char, callback, user_data, i
callback_data.EventFlag = ImGuiInputTextFlags.CallbackCharFilter
callback_data.EventChar = c
callback_data.EventActivated = (state ~= nil and g.ActiveId == state.ID and g.ActiveIdIsJustActivated)
callback_data.CursorPos = state.Stb.cursor
callback_data.SelectionStart = state.Stb.select_start
callback_data.SelectionEnd = state.Stb.select_end
callback_data.CursorPos = state.Stb.cursor - 1 -- [Luau] callback positions are 0-based
callback_data.SelectionStart = state.Stb.select_start - 1
callback_data.SelectionEnd = state.Stb.select_end - 1
callback_data.UserData = user_data
if callback(callback_data) ~= 0 then
return char, false
@@ -5573,7 +5644,7 @@ function ImGui.InputTextEx(label, hint, buf, buf_size, size_arg, flags, callback
if not stbte.HAS_SELECTION(state.Stb) then
stbte.prep_selection_at_cursor(state.Stb)
end
state.Stb.cursor = STB_TEXTEDIT_MOVEWORDRIGHT_MAC(state, state.Stb.cursor)
state.Stb.cursor = ImStb.TEXTEDIT_MOVEWORDRIGHT_MAC(state, state.Stb.cursor)
state.Stb.select_end = state.Stb.cursor
stbte.clamp(state, state.Stb)
else
@@ -5902,9 +5973,9 @@ function ImGui.InputTextEx(label, hint, buf, buf_size, size_arg, flags, callback
callback_data.BufTextLen = state.TextLen
callback_data.BufSize = state.BufCapacity
callback_data.BufDirty = false
callback_data.CursorPos = state.Stb.cursor
callback_data.SelectionStart = state.Stb.select_start
callback_data.SelectionEnd = state.Stb.select_end
callback_data.CursorPos = state.Stb.cursor - 1 -- [Luau] callback positions are 0-based, stb ones 1-based
callback_data.SelectionStart = state.Stb.select_start - 1
callback_data.SelectionEnd = state.Stb.select_end - 1
-- Call user code
callback(callback_data)
@@ -5914,12 +5985,12 @@ function ImGui.InputTextEx(label, hint, buf, buf_size, size_arg, flags, callback
IM_ASSERT(callback_data.Buf == callback_buf) -- Invalid to modify those fields
IM_ASSERT(callback_data.BufSize == state.BufCapacity)
IM_ASSERT(callback_data.Flags == flags)
if callback_data.BufDirty or callback_data.CursorPos ~= state.Stb.cursor then
if callback_data.BufDirty or callback_data.CursorPos ~= state.Stb.cursor - 1 then
state.CursorFollow = true
end
state.Stb.cursor = ImClamp(callback_data.CursorPos, 0, callback_data.BufTextLen)
state.Stb.select_start = ImClamp(callback_data.SelectionStart, 0, callback_data.BufTextLen)
state.Stb.select_end = ImClamp(callback_data.SelectionEnd, 0, callback_data.BufTextLen)
state.Stb.cursor = ImClamp(callback_data.CursorPos, 0, callback_data.BufTextLen) + 1
state.Stb.select_start = ImClamp(callback_data.SelectionStart, 0, callback_data.BufTextLen) + 1
state.Stb.select_end = ImClamp(callback_data.SelectionEnd, 0, callback_data.BufTextLen) + 1
if callback_data.BufDirty then
-- Callback may update buffer and thus set buf_dirty even in read-only mode.
IM_ASSERT(callback_data.BufTextLen == ImStd.ImStrlen(callback_data.Buf)) -- You need to maintain BufTextLen if you change the text!
@@ -6283,6 +6354,47 @@ function ImGui.InputTextWithHint(label, hint, buf, buf_size, flags, callback, us
return ImGui.InputTextEx(label, hint, buf, buf_size, ImVec2(0, 0), flags, callback, user_data)
end
-- [Luau] stb positions/undo indices are 1-based in this port (displayed as-is)
--- @param state ImGuiInputTextState
function ImGui.DebugNodeInputTextState(state)
local g = GImGui
local stb_state = state.Stb
local undo_state = stb_state.undostate
ImGui.Text("ID: 0x%08X, ActiveID: 0x%08X", state.ID, g.ActiveId)
if ImGui.DebugLocateItemOnHover then ImGui.DebugLocateItemOnHover(state.ID) end -- [Luau] not ported yet
local wrap_dir = ""
if bit32.band(state.Flags, ImGuiInputTextFlags.WordWrap) ~= 0 then
if state.LastMoveDirectionLR == ImGuiDir.Left then wrap_dir = " (L)" else wrap_dir = " (R)" end
end
ImGui.Text("TextLen: %d, Cursor: %d%s, Selection: %d..%d", state.TextLen, stb_state.cursor, wrap_dir, stb_state.select_start, stb_state.select_end)
ImGui.Text("BufCapacity: %d, LineCount: %d", state.BufCapacity, state.LineCount)
ImGui.Text("(Internal Buffer: TextA Size: %d, Capacity: %d)", state.TextA.Size, state.TextA.Capacity)
ImGui.Text("has_preferred_x: %d (%.2f)", stb_state.has_preferred_x and 1 or 0, stb_state.preferred_x)
ImGui.Text("undo_point: %d, redo_point: %d, undo_char_point: %d, redo_char_point: %d", undo_state.undo_point, undo_state.redo_point, undo_state.undo_char_point, undo_state.redo_char_point)
if ImGui.BeginChild("undopoints", ImVec2(0.0, ImGui.GetTextLineHeight() * 10), bit32.bor(ImGuiChildFlags.Borders, ImGuiChildFlags.ResizeY)) then -- Visualize undo state
ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, ImVec2(0, 0))
for n = 1, IMSTB_TEXTEDIT_UNDOSTATECOUNT do
local undo_rec = undo_state.undo_rec[n]
local undo_rec_type = (n < undo_state.undo_point) and "u" or ((n >= undo_state.redo_point) and "r" or " ")
if undo_rec_type == " " then
ImGui.BeginDisabled()
end
local buf_preview_len = (undo_rec_type ~= " " and undo_rec.char_storage ~= -1) and undo_rec.insert_length or 0
local buf_preview_str = ""
if buf_preview_len > 0 then
buf_preview_str = string.char(unpack(undo_state.undo_char, undo_rec.char_storage, undo_rec.char_storage + buf_preview_len - 1))
end
ImGui.Text("%s [%02d] where %03d, insert %03d, delete %03d, char_storage %03d \"%s\"",
undo_rec_type, n, undo_rec.where, undo_rec.insert_length, undo_rec.delete_length, undo_rec.char_storage, buf_preview_str)
if undo_rec_type == " " then
ImGui.EndDisabled()
end
end
ImGui.PopStyleVar()
end
ImGui.EndChild()
end
-------------------------------------------------------------------------------
-- [SECTION] Widgets: ColorEdit, ColorPicker, ColorButton, etc.
-------------------------------------------------------------------------------
@@ -10648,14 +10760,37 @@ local function TabItemGetSectionIdx(tab)
return 1
end
-- [Luau] comparers return a boolean "less than" for table.sort (upstream: ImQsort int comparers)
local function TabItemComparerBySection(a, b)
if a == nil or b == nil then return false end
local a_section = TabItemGetSectionIdx(a)
local b_section = TabItemGetSectionIdx(b)
if a_section ~= b_section then
return a_section < b_section
end
return a.IndexDuringLayout < b.IndexDuringLayout
end
local function TabItemComparerByBeginOrder(a, b)
return a.BeginOrder < b.BeginOrder
end
local function GetTabBarFromTabBarRef(ref)
local g = GImGui
if ref.Ptr then return ref.Ptr else return g.TabBars:GetByIndex(ref.Index) end
end
local function GetTabBarRefFromTabBar(tab_bar)
local g = GImGui
if g.TabBars:Contains(tab_bar) then
return ImGuiPtrOrIndex(g.TabBars:GetIndex(tab_bar))
end
return ImGuiPtrOrIndex(tab_bar)
end
-- Stable sorts (C++ ImQsort is not stable, but the comparers are total orders in practice)
local function SortTabsBySection(tab_bar)
table.sort(tab_bar.Tabs.Data, function(a, b)
if a == nil or b == nil then return false end
local sa, sb = TabItemGetSectionIdx(a), TabItemGetSectionIdx(b)
if sa ~= sb then return sa < sb end
return a.IndexDuringLayout < b.IndexDuringLayout
end)
table.sort(tab_bar.Tabs.Data, TabItemComparerBySection)
end
local function SortTabsByBeginOrder(tab_bar)
@@ -10663,7 +10798,7 @@ local function SortTabsByBeginOrder(tab_bar)
if not data then return end
local arr = {}
for i = 1, tab_bar.Tabs.Size do arr[i] = data[i] end
table.sort(arr, function(a, b) return a.BeginOrder < b.BeginOrder end)
table.sort(arr, TabItemComparerByBeginOrder)
for i = 1, tab_bar.Tabs.Size do data[i] = arr[i] end
end
@@ -10677,6 +10812,12 @@ end
-- Spacing: Horizontal spacing at the end of the section.
local function ImGuiTabBarSection() return { TabCount = 0, Width = 0.0, WidthAfterShrinkMinWidth = 0.0, Spacing = 0.0 } end
-- [Luau] boolean "less than" for table.sort. Width compared exactly: upstream's (int) truncation isn't a strict weak order, which table.sort rejects
local function ShrinkWidthItemComparer(a, b)
if a.Width ~= b.Width then return a.Width > b.Width end
return a.Index > b.Index -- b->Index - a->Index
end
-- Shrink excess width from a set of item, by removing width from the larger items first.
-- Set items Width to -1.0f to disable shrinking this item.
-- items: 1-based array of ImGuiShrinkWidthItem, `first` = 1-based index of first item, `count` items
@@ -10690,10 +10831,7 @@ function ImGui.ShrinkWidths(items, first, count, width_excess, width_min)
-- Sort largest first, smallest last.
local slice = {}
for n = 1, count do slice[n] = items[first + n - 1] end
table.sort(slice, function(a, b)
if a.Width ~= b.Width then return a.Width > b.Width end
return a.Index > b.Index -- ShrinkWidthItemComparer: b->Index - a->Index (Width compared exactly: upstream's (int) truncation isn't a strict weak order, which table.sort rejects)
end)
table.sort(slice, ShrinkWidthItemComparer)
for n = 1, count do items[first + n - 1] = slice[n] end
local it = slice
local count_same_width = 1
@@ -10804,7 +10942,7 @@ function ImGui.BeginTabBarEx(tab_bar, tab_bar_bb, flags)
end
-- Add to stack
g.CurrentTabBarStack:push_back(tab_bar)
g.CurrentTabBarStack:push_back(GetTabBarRefFromTabBar(tab_bar))
g.CurrentTabBar = tab_bar
tab_bar.Window = window
@@ -10886,7 +11024,7 @@ function ImGui.EndTabBar()
end
g.CurrentTabBarStack:pop_back()
if g.CurrentTabBarStack.Size == 0 then g.CurrentTabBar = nil else g.CurrentTabBar = g.CurrentTabBarStack.Data[g.CurrentTabBarStack.Size] end
if g.CurrentTabBarStack.Size == 0 then g.CurrentTabBar = nil else g.CurrentTabBar = GetTabBarFromTabBarRef(g.CurrentTabBarStack:back()) end
end
-- This is called only once a frame before by the first call to ItemTab()