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

This commit is contained in:
4DBug
2026-10-15 03:49:53 -05:00
parent 33ab5a1d84
commit 049e160852
13 changed files with 5881 additions and 598 deletions
+195
View File
@@ -386,6 +386,16 @@ end
--@hoist
function ImSaturate(f) return ((f < 0.0 and 0.0) or (f > 1.0 and 1.0) or f) end
--- @param x float
--- @return float # Sign operator - returns -1, 0 or 1 based on sign of argument
function ImSign(x) if x < 0.0 then return -1.0 elseif x > 0.0 then return 1.0 end return 0.0 end
function ImLinearRemapClamp(s0, s1, d0, d1, x) return ImSaturate((x - s0) / (s1 - s0)) * (d1 - d0) + d0 end
--- @param c int # byte
--- @return int
function ImToUpper(c) if c >= 97 and c <= 122 then return bit32.band(c, bit32.bnot(32)) end return c end
IM_F32_TO_INT8_UNBOUND = function(val) return math.floor(val * 255.0 + (val >= 0 and 0.5 or -0.5)) end -- Unsaturated, for display purpose
IM_F32_TO_INT8_SAT = function(val) return math.floor(ImSaturate(val) * 255.0 + 0.5) end -- Saturated, always output 0..255
@@ -658,6 +668,12 @@ end
--- @nodiscard
function ImGui.WindowPosAbsToRel(window, p) local off = window.DC.CursorStartPos return ImVec2(p.x - off.x, p.y - off.y) end
--- @param window ImGuiWindow
--- @param p ImVec2
--- @return ImVec2
--- @nodiscard
function ImGui.WindowPosRelToAbs(window, p) local off = window.DC.CursorStartPos return ImVec2(p.x + off.x, p.y + off.y) end
--- @param c char
--- @return bool # True if this character is a ' ' or '\t'
function ImCharIsBlankA(c) return c == 32 or c == 9 end
@@ -880,6 +896,9 @@ function IM_RECT:Add(p)
end
end
function IM_RECT:AddX(x) if self.Min.x > x then self.Min.x = x end if self.Max.x < x then self.Max.x = x end end
function IM_RECT:AddY(y) if self.Min.y > y then self.Min.y = y end if self.Max.y < y then self.Max.y = y end end
--- @param amount float|ImVec2
@native
function IM_RECT:Expand(amount)
@@ -1848,6 +1867,164 @@ local function ImGuiNavItemData()
return this
end
-------------------------------------------------------------------------------
-- [SECTION] Typing-select support
-------------------------------------------------------------------------------
-- Flags for GetTypingSelectRequest()
--- @enum ImGuiTypingSelectFlags
ImGuiTypingSelectFlags = {
None = 0,
AllowBackspace = bitLShift(1, 0), -- Backspace to delete character inputs. If using: ensure GetTypingSelectRequest() is not called more than once per frame (filter by e.g. focus state)
AllowSingleCharMode = bitLShift(1, 1), -- Allow "single char" search mode which is activated when pressing the same character multiple times.
}
-- Returned by GetTypingSelectRequest(), designed to eventually be public.
--- @class ImGuiTypingSelectRequest
--- @field Flags ImGuiTypingSelectFlags # Flags passed to GetTypingSelectRequest()
--- @field SearchBufferLen int
--- @field SearchBuffer string # Search buffer contents (use full string. unless SingleCharMode is set, in which case use SingleCharSize).
--- @field SelectRequest bool # Set when buffer was modified this frame, requesting a selection.
--- @field SingleCharMode bool # Notify when buffer contains same character repeated, to implement special mode. In this situation it preferred to not display any on-screen search indication.
--- @field SingleCharSize int # Length in bytes of first letter codepoint (1 for ascii, 2-4 for UTF-8). If (SearchBufferLen==RepeatCharSize) only 1 letter has been input.
local function ImGuiTypingSelectRequest()
return { Flags = 0, SearchBufferLen = 0, SearchBuffer = "", SelectRequest = false, SingleCharMode = false, SingleCharSize = 0 }
end
-- Storage for GetTypingSelectRequest()
-- [Port] SearchBuffer is an immutable Lua string (C++: char[64], 63 bytes max + zero terminator). SearchBuffer[0] != 0 -> #SearchBuffer ~= 0.
--- @class ImGuiTypingSelectState
--- @field Request ImGuiTypingSelectRequest # User-facing data
--- @field SearchBuffer string # Search buffer: no need to make dynamic as this search is very transient.
--- @field FocusScope ImGuiID
--- @field LastRequestFrame int
--- @field LastRequestTime float
--- @field SingleCharModeLock bool # After a certain single char repeat count we lock into SingleCharMode. Two benefits: 1) buffer never fill, 2) we can provide an immediate SingleChar mode without timer elapsing.
MT.ImGuiTypingSelectState = {}
MT.ImGuiTypingSelectState.__index = MT.ImGuiTypingSelectState
--- @return ImGuiTypingSelectState
function ImGuiTypingSelectState()
return setmetatable({ Request = ImGuiTypingSelectRequest(), SearchBuffer = "", FocusScope = 0, LastRequestFrame = 0, LastRequestTime = 0.0, SingleCharModeLock = false }, MT.ImGuiTypingSelectState)
end
function MT.ImGuiTypingSelectState:Clear() self.SearchBuffer = ""; self.SingleCharModeLock = false end -- We preserve remaining data for easier debugging
-------------------------------------------------------------------------------
-- [SECTION] Box-select support
-------------------------------------------------------------------------------
--- @class ImGuiBoxSelectState
--- @field ID ImGuiID
--- @field IsActive bool
--- @field IsStarting bool
--- @field IsStartedFromVoid bool # Starting click was not from an item.
--- @field IsStartedSetNavIdOnce bool
--- @field RequestClear bool
--- @field KeyMods ImGuiKeyChord # Latched key-mods for box-select logic.
--- @field StartPosRel ImVec2 # Start position in window-contents relative space (to support scrolling)
--- @field EndPosRel ImVec2 # End position in window-contents relative space
--- @field ScrollAccum ImVec2 # Scrolling accumulator (to behave at high-frame spaces)
--- @field Window ImGuiWindow?
--- @field UnclipMode bool # (Temp/Transient, here in hot area). Set/cleared by the BeginMultiSelect()/EndMultiSelect() owning active box-select.
--- @field UnclipRect ImRect # Rectangle where ItemAdd() clipping may be temporarily disabled. Need support by multi-select supporting widgets.
--- @field UnclipRects ImRect[] # Per-axis versions. [Port] 1-based: [1] = upstream [0] (X), [2] = upstream [1] (Y)
--- @field BoxSelectRectPrev ImRect # Selection rectangle in absolute coordinates (derived every frame from BoxSelectStartPosRel and MousePos)
--- @field BoxSelectRectCurr ImRect
--- @return ImGuiBoxSelectState
function ImGuiBoxSelectState()
return {
-- Active box-selection data (persistent, 1 active at a time)
ID = 0, IsActive = false, IsStarting = false, IsStartedFromVoid = false, IsStartedSetNavIdOnce = false, RequestClear = false,
KeyMods = 0,
StartPosRel = ImVec2(), EndPosRel = ImVec2(), ScrollAccum = ImVec2(),
Window = nil,
-- Temporary/Transient data
UnclipMode = false,
UnclipRect = ImRect(),
UnclipRects = { ImRect(), ImRect() },
BoxSelectRectPrev = ImRect(),
BoxSelectRectCurr = ImRect(),
}
end
-------------------------------------------------------------------------------
-- [SECTION] Multi-select support
-------------------------------------------------------------------------------
-- We always assume that -1 is an invalid value (which works for indices and pointers)
-- (ImGuiSelectionUserData_Invalid is defined above with the ImGuiSelectionUserData alias)
-- Temporary storage for multi-select
--- @class ImGuiMultiSelectTempData
--- @field IO ImGuiMultiSelectIO # MUST BE FIRST FIELD. Requests are set and returned by BeginMultiSelect()/EndMultiSelect() + written to by user during the loop.
--- @field Storage ImGuiMultiSelectState?
--- @field FocusScopeId ImGuiID # Copied from g.CurrentFocusScopeId (unless another selection scope was pushed manually)
--- @field Flags ImGuiMultiSelectFlags
--- @field ScopeRectMin ImVec2
--- @field BackupCursorMaxPos ImVec2
--- @field BoxSelectId ImGuiID
--- @field KeyMods ImGuiKeyChord
--- @field LoopRequestSetAll int # -1: no operation, 0: clear all, 1: select all.
--- @field IsEndIO bool # Set when switching IO from BeginMultiSelect() to EndMultiSelect() state.
--- @field IsFocused bool # Set if currently focusing the selection scope (any item of the selection). May be used if you have custom shortcut associated to selection.
--- @field IsKeyboardSetRange bool # Set by BeginMultiSelect() when using Shift+Navigation. Because scrolling may be affected we can't afford a frame of lag with Shift+Navigation.
--- @field NavIdPassedBy bool
--- @field RangeSrcPassedBy bool # Set by the item that matches RangeSrcItem.
--- @field RangeDstPassedBy bool # Set by the item that matches NavJustMovedToId when IsSetRange is set.
--- @field IsSoleOrUnknownSelectionSize bool
MT.ImGuiMultiSelectTempData = {}
MT.ImGuiMultiSelectTempData.__index = MT.ImGuiMultiSelectTempData
--- @return ImGuiMultiSelectTempData
function ImGuiMultiSelectTempData()
local this = setmetatable({ IO = ImGuiMultiSelectIO(), ScopeRectMin = ImVec2(), BackupCursorMaxPos = ImVec2() }, MT.ImGuiMultiSelectTempData)
this:Clear()
return this
end
-- Zero-clear except IO as we preserve IO.Requests[] buffer allocation.
function MT.ImGuiMultiSelectTempData:Clear()
self:ClearIO()
self.Storage = nil
self.FocusScopeId = 0
self.Flags = 0
ImVec2_CopyV(self.ScopeRectMin, 0.0, 0.0)
ImVec2_CopyV(self.BackupCursorMaxPos, 0.0, 0.0)
self.BoxSelectId = 0
self.KeyMods = 0
self.LoopRequestSetAll = 0
self.IsEndIO = false
self.IsFocused = false
self.IsKeyboardSetRange = false
self.NavIdPassedBy = false
self.RangeSrcPassedBy = false
self.RangeDstPassedBy = false
self.IsSoleOrUnknownSelectionSize = false
end
function MT.ImGuiMultiSelectTempData:ClearIO() local io = self.IO; io.Requests:resize(0); io.RangeSrcItem = ImGuiSelectionUserData_Invalid; io.NavIdItem = ImGuiSelectionUserData_Invalid; io.NavIdSelected = false; io.RangeSrcReset = false end
-- Persistent storage for multi-select (as long as selection is alive)
--- @class ImGuiMultiSelectState
--- @field Window ImGuiWindow?
--- @field ID ImGuiID
--- @field LastFrameActive int # Last used frame-count, for GC.
--- @field LastSelectionSize int # Set by BeginMultiSelect() based on optional info provided by user. May be -1 if unknown.
--- @field RangeSelected int # -1 (don't have) or true/false
--- @field NavIdSelected int # -1 (don't have) or true/false
--- @field RangeSrcItem ImGuiSelectionUserData #
--- @field NavIdItem ImGuiSelectionUserData # SetNextItemSelectionUserData() value for NavId (if part of submitted items)
--- @return ImGuiMultiSelectState
function ImGuiMultiSelectState()
return { Window = nil, ID = 0, LastFrameActive = 0, LastSelectionSize = 0, RangeSelected = -1, NavIdSelected = -1, RangeSrcItem = ImGuiSelectionUserData_Invalid, NavIdItem = ImGuiSelectionUserData_Invalid }
end
--- @param id ImGuiID
--- @return ImGuiBoxSelectState?
function ImGui.GetBoxSelectState(id) local g = GImGui; if id ~= 0 and g.BoxSelectState.ID == id and g.BoxSelectState.IsActive then return g.BoxSelectState end return nil end
--- @param id ImGuiID
--- @return ImGuiMultiSelectState?
function ImGui.GetMultiSelectState(id) local g = GImGui; return g.MultiSelectStorage:GetByKey(id) end
-------------------------------------------------------------------------------
-- [SECTION] ImGuiContext (main Dear ImGui context)
-------------------------------------------------------------------------------
@@ -2129,6 +2306,7 @@ function ImGuiContext(shared_font_atlas) -- TODO: tidy up / complete this struct
NavHighlightItemUnderNav = false, -- Disable mouse hovering highlight. Highlight navigation focused item instead of mouse hovered item.
NavIdIsAlive = false, -- Nav widget has been seen this frame ~~ NavRectRel is valid
NavInputSource = ImGuiInputSource.Keyboard, -- Keyboard or Gamepad mode? THIS CAN ONLY BE ImGuiInputSource.Keyboard or ImGuiInputSource.Gamepad
NavLastValidSelectionUserData = ImGuiSelectionUserData_Invalid, -- Last valid data passed to SetNextItemSelectionUser(), or -1. For current window. Not reset when focusing an item that doesn't have selection data.
NavWindowingTarget = nil, NavWindowingTargetAnim = nil, NavWindowingListWindow = nil, -- Target window when doing Ctrl+Tab (or Pad Menu + FocusPrev/Next), this window is temporarily displayed top-most!
NavWindowingTimer = 0.0, NavWindowingHighlightAlpha = 0.0,
NavWindowingToggleLayer = false, -- Set while Alt or GamepadMenu is held, may be cleared by other operations, and processed when releasing the key.
@@ -2168,6 +2346,14 @@ function ImGuiContext(shared_font_atlas) -- TODO: tidy up / complete this struct
NavActivateId = 0, NavActivateDownId = 0, NavActivatePressedId = 0, -- ~~ (g.ActiveId == 0) && (IsKeyPressed(ImGuiKey.Space) || IsKeyDown(ImGuiKey.Enter) || IsKeyPressed(ImGuiKey.NavGamepadActivate)) ? NavId : 0, also set when calling ActivateItemByID()
NavActivateFlags = ImGuiActivateFlags.None,
-- Navigation: record of last move request
NavJustMovedFromFocusScopeId = 0, -- Just navigated from this focus scope id (result of a successfully MoveRequest).
NavJustMovedToId = 0, -- Just navigated to this id (result of a successfully MoveRequest).
NavJustMovedToFocusScopeId = 0, -- Just navigated to this focus scope id (result of a successfully MoveRequest).
NavJustMovedToKeyMods = 0,
NavJustMovedToIsTabbing = false, -- Copy of ImGuiNavMoveFlags.IsTabbing. Maybe we should store whole flags.
NavJustMovedToHasSelectionData = false, -- Copy of move result's ItemFlags & ImGuiItemFlags.HasSelectionUserData). Maybe we should just store ImGuiNavItemData.
FrameCount = 0,
FrameCountEnded = -1,
@@ -2228,6 +2414,8 @@ function ImGuiContext(shared_font_atlas) -- TODO: tidy up / complete this struct
MouseCursor = ImGuiMouseCursor.Arrow,
MouseStationaryTimer = 0.0, -- Time the mouse has been stationary (with some loose heuristic)
TypingSelectState = ImGuiTypingSelectState(), -- State for GetTypingSelectRequest()
InputTextState = ImGuiInputTextState(),
InputTextLineIndex = ImGuiTextIndex(), -- Temporary storage
InputTextDeactivatedState = ImGuiInputTextDeactivatedState(),
@@ -2249,6 +2437,13 @@ function ImGuiContext(shared_font_atlas) -- TODO: tidy up / complete this struct
CurrentTabBarStack = ImVector(), -- holds ImGuiTabBar references directly (no ImGuiPtrOrIndex needed in Lua)
ShrinkWidthBuffer = ImVector(), -- ImGuiShrinkWidthItem
-- Multi-Select state
BoxSelectState = ImGuiBoxSelectState(),
CurrentMultiSelect = nil,
MultiSelectTempDataStacked = 0, -- Temporary multi-select data size (because we leave previous instances undestructed, we generally don't use MultiSelectTempData.Size)
MultiSelectTempData = ImVector(), -- ImVector<ImGuiMultiSelectTempData>, 1-based
MultiSelectStorage = ImPool(ImGuiMultiSelectState), -- ImPool<ImGuiMultiSelectState>
WindowResizeBorderExpectedRect = ImRect(), -- Expected border rect, switch to relative edit if moving
WindowResizeRelativeMode = false,