Files
imgui-luau/imgui_tables.lua
T

4993 lines
261 KiB
Lua

--- Dear ImGui WIP
-- (tables and columns code)
-- 1:1 port of imgui_tables.cpp (docking branch, 1.93 WIP)
-- Index of this file:
--
-- [SECTION] Commentary
-- [SECTION] Structs (port: struct constructors from imgui_internal.h / imgui.h)
-- [SECTION] Tables: Main code
-- [SECTION] Tables: Simple accessors
-- [SECTION] Tables: Row changes
-- [SECTION] Tables: Columns changes
-- [SECTION] Tables: Columns width management
-- [SECTION] Tables: Drawing
-- [SECTION] Tables: Sorting
-- [SECTION] Tables: Headers
-- [SECTION] Tables: Context Menu
-- [SECTION] Tables: Settings (.ini data)
-- [SECTION] Tables: Garbage Collection
-- [SECTION] Columns, BeginColumns, EndColumns, etc.
-------------------------------------------------------------------------------
-- [SECTION] Commentary
-------------------------------------------------------------------------------
-------------------------------------------------------------------------------
-- Typical tables call flow: (root level is generally public API):
-------------------------------------------------------------------------------
-- - BeginTable() user begin into a table
-- | BeginChild() - (if ScrollX/ScrollY is set)
-- | TableBeginInitMemory() - first time table is used
-- | TableResetSettings() - on settings reset
-- | TableLoadSettings() - on settings load
-------------------------------------------------------------------------------
-- - TableSetupColumn() user submit columns details (optional)
-- - TableSetupScrollFreeze() user submit scroll freeze information (optional)
-------------------------------------------------------------------------------
-- - TableUpdateLayout() [Internal] followup to BeginTable(): setup everything: widths, columns positions, clipping rectangles. Automatically called by the FIRST call to TableNextRow() or TableHeadersRow().
-- | TableApplyQueuedRequests() - apply queued resizing/reordering/hiding requests
-- | - TableSetColumnWidth() - apply resizing width (for mouse resize, often requested by previous frame)
-- | - TableUpdateColumnsWeightFromWidth()- recompute columns weights (of stretch columns) from their respective width
-- | - TableSetColumnDisplayOrder() - apply reordering a column
-- | TableSetupDrawChannels() - setup ImDrawList channels
-- | TableUpdateBorders() - detect hovering columns for resize, ahead of contents submission
-- | TableBeginContextMenuPopup()
-- | - TableDrawDefaultContextMenu() - draw right-click context menu contents
-------------------------------------------------------------------------------
-- - TableHeadersRow() or TableHeader() user submit a headers row (optional)
-- | TableSortSpecsClickColumn() - when left-clicked: alter sort order and sort direction
-- | TableOpenContextMenu() - when right-clicked: trigger opening of the default context menu
-- - TableGetSortSpecs() user queries updated sort specs (optional, generally after submitting headers)
-- - TableNextRow() user begin into a new row (also automatically called by TableHeadersRow())
-- | TableEndRow() - finish existing row
-- | TableBeginRow() - add a new row
-- - TableSetColumnIndex() / TableNextColumn() user begin into a cell
-- | TableEndCell() - close existing column/cell
-- | TableBeginCell() - enter into current column/cell
-- - [...] user emit contents
-------------------------------------------------------------------------------
-- - EndTable() user ends the table
-- | TableDrawBorders() - draw outer borders, inner vertical borders
-- | TableMergeDrawChannels() - merge draw channels if clipping isn't required
-- | EndChild() - (if ScrollX/ScrollY is set)
-------------------------------------------------------------------------------
-------------------------------------------------------------------------------
-- TABLE SIZING
-------------------------------------------------------------------------------
-- (Read carefully because this is subtle but it does make sense!)
-------------------------------------------------------------------------------
-- About 'outer_size':
-- Its meaning needs to differ slightly depending on if we are using ScrollX/ScrollY flags.
-- Default value is ImVec2(0.0, 0.0).
-- X
-- - outer_size.x <= 0.0 -> Right-align from window/work-rect right-most edge. With -FLT_MIN or 0.0 will align exactly on right-most edge.
-- - outer_size.x > 0.0 -> Set Fixed width.
-- Y with ScrollX/ScrollY disabled: we output table directly in current window
-- - outer_size.y < 0.0 -> Bottom-align (but will auto extend, unless _NoHostExtendY is set). Not meaningful if parent window can vertically scroll.
-- - outer_size.y = 0.0 -> No minimum height (but will auto extend, unless _NoHostExtendY is set)
-- - outer_size.y > 0.0 -> Set Minimum height (but will auto extend, unless _NoHostExtendY is set)
-- Y with ScrollX/ScrollY enabled: using a child window for scrolling
-- - outer_size.y < 0.0 -> Bottom-align. Not meaningful if parent window can vertically scroll.
-- - outer_size.y = 0.0 -> Bottom-align, consistent with BeginChild(). Not recommended unless table is last item in parent window.
-- - outer_size.y > 0.0 -> Set Exact height. Recommended when using Scrolling on any axis.
-------------------------------------------------------------------------------
-- Outer size is also affected by the NoHostExtendX/NoHostExtendY flags.
-- Important to note how the two flags have slightly different behaviors!
-- - ImGuiTableFlags.NoHostExtendX -> Make outer width auto-fit to columns (overriding outer_size.x value). Only available when ScrollX/ScrollY are disabled and Stretch columns are not used.
-- - ImGuiTableFlags.NoHostExtendY -> Make outer height stop exactly at outer_size.y (prevent auto-extending table past the limit). Only available when ScrollX/ScrollY is disabled. Data below the limit will be clipped and not visible.
-- In theory ImGuiTableFlags.NoHostExtendY could be the default and any non-scrolling tables with outer_size.y ~= 0.0 would use exact height.
-- This would be consistent but perhaps less useful and more confusing (as vertically clipped items are not useful and not easily noticeable).
-------------------------------------------------------------------------------
-- About 'inner_width':
-- With ScrollX disabled:
-- - inner_width -> *ignored*
-- With ScrollX enabled:
-- - inner_width < 0.0 -> *illegal* fit in known width (right align from outer_size.x) <-- weird
-- - inner_width = 0.0 -> fit in outer_width: Fixed size columns will take space they need (if avail, otherwise shrink down), Stretch columns becomes Fixed columns.
-- - inner_width > 0.0 -> override scrolling width, generally to be larger than outer_size.x. Fixed column take space they need (if avail, otherwise shrink down), Stretch columns share remaining space!
-------------------------------------------------------------------------------
-- Details:
-- - If you want to use Stretch columns with ScrollX, you generally need to specify 'inner_width' otherwise the concept
-- of "available space" doesn't make sense.
-- - Even if not really useful, we allow 'inner_width < outer_size.x' for consistency and to facilitate understanding
-- of what the value does.
-------------------------------------------------------------------------------
-------------------------------------------------------------------------------
-- COLUMNS SIZING POLICIES
-- (Reference: ImGuiTableFlags.SizingXXX flags and ImGuiTableColumnFlags.WidthXXX flags)
-------------------------------------------------------------------------------
-- About overriding column sizing policy and width/weight with TableSetupColumn():
-- We use a default parameter of -1 for 'init_width'/'init_weight'.
-- - with ImGuiTableColumnFlags.WidthFixed, init_width <= 0 (default) --> width is automatic
-- - with ImGuiTableColumnFlags.WidthFixed, init_width > 0 (explicit) --> width is custom
-- - with ImGuiTableColumnFlags.WidthStretch, init_weight <= 0 (default) --> weight is 1.0
-- - with ImGuiTableColumnFlags.WidthStretch, init_weight > 0 (explicit) --> weight is custom
-- Widths are specified _without_ CellPadding. If you specify a width of 100.0, the column will be cover (100.0 + Padding * 2.0)
-- and you can fit a 100.0 wide item in it without clipping and with padding honored.
-------------------------------------------------------------------------------
-- About default sizing policy (if you don't specify a ImGuiTableColumnFlags.WidthXXXX flag)
-- - with Table policy ImGuiTableFlags.SizingFixedFit --> default Column policy is ImGuiTableColumnFlags.WidthFixed, default Width is equal to contents width
-- - with Table policy ImGuiTableFlags.SizingFixedSame --> default Column policy is ImGuiTableColumnFlags.WidthFixed, default Width is max of all contents width
-- - with Table policy ImGuiTableFlags.SizingStretchSame --> default Column policy is ImGuiTableColumnFlags.WidthStretch, default Weight is 1.0
-- - with Table policy ImGuiTableFlags.SizingStretchWeight --> default Column policy is ImGuiTableColumnFlags.WidthStretch, default Weight is proportional to contents
-- Default Width and default Weight can be overridden when calling TableSetupColumn().
-------------------------------------------------------------------------------
-- About mixing Fixed/Auto and Stretch columns together:
-- - the typical use of mixing sizing policies is: any number of LEADING Fixed columns, followed by one or two TRAILING Stretch columns.
-- - using mixed policies with ScrollX does not make much sense, as using Stretch columns with ScrollX does not make much sense in the first place!
-- that is, unless 'inner_width' is passed to BeginTable() to explicitly provide a total width to layout columns in.
-- - when using ImGuiTableFlags.SizingFixedSame with mixed columns, only the Fixed/Auto columns will match their widths to the width of the maximum contents.
-- - when using ImGuiTableFlags.SizingStretchSame with mixed columns, only the Stretch columns will match their weights/widths.
-------------------------------------------------------------------------------
-- About using column width:
-- If a column is manually resizable or has a width specified with TableSetupColumn():
-- - you may use GetContentRegionAvail().x to query the width available in a given column.
-- - right-side alignment features such as SetNextItemWidth(-x) or PushItemWidth(-x) will rely on this width.
-- If the column is not resizable and has no width specified with TableSetupColumn():
-- - its width will be automatic and be set to the max of items submitted.
-- - therefore you generally cannot have ALL items of the columns use e.g. SetNextItemWidth(-FLT_MIN).
-- - but if the column has one or more items of known/fixed size, this will become the reference width used by SetNextItemWidth(-FLT_MIN).
-------------------------------------------------------------------------------
-------------------------------------------------------------------------------
-- TABLES CLIPPING/CULLING
-------------------------------------------------------------------------------
-- About clipping/culling of Rows in Tables:
-- - For large numbers of rows, it is recommended you use ImGuiListClipper to submit only visible rows.
-- ImGuiListClipper is reliant on the fact that rows are of equal height.
-- See 'Demo->Tables->Vertical Scrolling' or 'Demo->Tables->Advanced' for a demo of using the clipper.
-- - Note that auto-resizing columns don't play well with using the clipper.
-- By default a table with _ScrollX but without _Resizable will have column auto-resize.
-- So, if you want to use the clipper, make sure to either enable _Resizable, either setup columns width explicitly with _WidthFixed.
-------------------------------------------------------------------------------
-- About clipping/culling of Columns in Tables:
-- - Both TableSetColumnIndex() and TableNextColumn() return true when the column is visible or performing
-- width measurements. Otherwise, you may skip submitting the contents of a cell/column, BUT ONLY if you know
-- it is not going to contribute to row height.
-- In many situations, you may skip submitting contents for every column but one (e.g. the first one).
-- - Case A: column is not hidden by user, and at least partially in sight (most common case).
-- - Case B: column is clipped / out of sight (because of scrolling or parent ClipRect): TableNextColumn() return false as a hint but we still allow layout output.
-- - Case C: column is hidden explicitly by the user (e.g. via the context menu, or _DefaultHide column flag, etc.).
--
-- [A] [B] [C]
-- TableNextColumn(): true false false -> [userland] when TableNextColumn() / TableSetColumnIndex() returns false, user can skip submitting items but only if the column doesn't contribute to row height.
-- SkipItems: false false true -> [internal] when SkipItems is true, most widgets will early out if submitted, resulting is no layout output.
-- ClipRect: normal zero-width zero-width -> [internal] when ClipRect is zero, ItemAdd() will return false and most widgets will early out mid-way.
-- ImDrawList output: normal dummy dummy -> [internal] when using the dummy channel, ImDrawList submissions (if any) will be wasted (because cliprect is zero-width anyway).
--
-- - We need to distinguish those cases because non-hidden columns that are clipped outside of scrolling bounds should still contribute their height to the row.
-- However, in the majority of cases, the contribution to row height is the same for all columns, or the tallest cells are known by the programmer.
-------------------------------------------------------------------------------
-- About clipping/culling of whole Tables:
-- - Scrolling tables with a known outer size can be clipped earlier as BeginTable() will return false.
-------------------------------------------------------------------------------
-- Port notes:
-- - Column indices / display orders / draw channel indices are 0-based VALUES like upstream.
-- Per-column arrays (table.Columns, table.DisplayOrderToIndex, table.RowCellData) are Lua tables with 0-based keys.
-- table.ColumnsSize mirrors upstream `table->Columns.size()`.
-- - ImBitArray masks are Lua tables { [n] = true }.
-- - table.ColumnsNames is a Lua array of strings, column.NameOffset is a 0-based index into it (-1 = none).
-- - g.SettingsTables is a plain vector of ImGuiTableSettings; table.SettingsOffset is a 1-based index into it (-1 = none).
-- Each settings entry owns `Columns` (0-based Lua table of ImGuiTableColumnSettings).
-- - ImGuiTableSortSpecs.Specs is a 1-based Lua array of ImGuiTableColumnSortSpecs.
--- @type ImGuiContext?
local GImGui
local MT = ImGui.GetMetatables()
-- Sets local `GImGui` in this file(imgui_tables.lua).
-- This is currently only used in main code `ImGui.SetCurrentContext()`
--- @param ctx ImGuiContext?
function ImGui._SetCurrentContext_Tables(ctx)
GImGui = ctx
end
local bit32 = bit32
local band, bor, bnot, lshift, rshift = bit32.band, bit32.bor, bit32.bnot, bit32.lshift, bit32.rshift
IM_COL32_DISABLE = IM_COL32(0, 0, 0, 1) -- Special sentinel code which cannot be used as a regular color.
IMGUI_TABLE_MAX_COLUMNS = 512 -- Arbitrary "safety" maximum, may be lifted in the future if needed. Must fit in ImGuiTableColumnIdx/ImGuiTableDrawChannelIdx.
local function IMGUI_DEBUG_LOG_TABLE(fmt, ...)
local g = GImGui
if band(g.DebugLogFlags, ImGuiDebugLogFlags.EventTable) ~= 0 then ImGui.DebugLog(fmt, ...) end
end
-------------------------------------------------------------------------------
-- [SECTION] Structs
-------------------------------------------------------------------------------
local function BitArrayClearAll(arr) table.clear(arr) end
local function BitArraySet(arr, n) arr[n] = true end
local function BitTest(arr, n) return arr[n] == true end
-- Sorting specification for one column of a table
--- @class ImGuiTableColumnSortSpecs
function ImGuiTableColumnSortSpecs()
return { ColumnUserID = 0, ColumnIndex = 0, SortOrder = 0, SortDirection = ImGuiSortDirection.None }
end
-- Sorting specifications for a table (often handling sort specs for a single column, occasionally more)
-- Obtained by calling TableGetSortSpecs().
-- When 'SpecsDirty == true' you can sort your data. It will be true with sorting specs have changed since last call, or the first time.
-- Make sure to set 'SpecsDirty = false' after sorting, else you may wastefully sort your data every frame!
--- @class ImGuiTableSortSpecs
function ImGuiTableSortSpecs()
return { Specs = nil, SpecsCount = 0, SpecsDirty = false }
end
-- We use the terminology "Enabled" to refer to a column that is not Hidden by user/api.
-- We use the terminology "Clipped" to refer to a column that is out of sight because of scrolling/clipping.
-- This is in contrast with some user-facing api such as IsItemVisible() / IsRectVisible() which use "Visible" to mean "not clipped".
--- @class ImGuiTableColumn
function ImGuiTableColumn()
return {
Flags = 0, WidthGiven = 0.0, MinX = 0.0, MaxX = 0.0, WidthRequest = -1.0, WidthAuto = 0.0, WidthMax = 0.0,
StretchWeight = -1.0, InitStretchWeightOrWidth = 0.0, ClipRect = ImRect(), ID = 0, UserData = 0,
WorkMinX = 0.0, WorkMaxX = 0.0, ItemWidth = 0.0, ContentMaxXFrozen = 0.0, ContentMaxXUnfrozen = 0.0,
ContentMaxXHeadersUsed = 0.0, ContentMaxXHeadersIdeal = 0.0, NameOffset = -1, DisplayOrder = -1,
IndexWithinEnabledSet = -1, PrevEnabledColumn = -1, NextEnabledColumn = -1, SortOrder = -1,
DrawChannelCurrent = -1, DrawChannelFrozen = -1, DrawChannelUnfrozen = -1,
IsEnabled = false, IsUserEnabled = false, IsUserEnabledNextFrame = false, IsVisibleX = false, IsVisibleY = false,
IsRequestOutput = false, IsSkipItems = false, IsPreserveWidthAuto = false, IsJustCreated = true,
IsLoadedSettings = false, IsNeedReconcileSrc = false, IsNeedReconcileDst = false,
NavLayerCurrent = 0, AutoFitQueue = 0, CannotSkipItemsQueue = 0, SortDirection = ImGuiSortDirection.None,
SortDirectionsAvailCount = 0, SortDirectionsAvailMask = 0, SortDirectionsAvailList = 0,
}
end
--- `*dst = *src`
local function TableColumnCopy(dst, src)
for k, v in pairs(src) do
if k ~= "ClipRect" then dst[k] = v end
end
ImRect_Copy(dst.ClipRect, src.ClipRect)
return dst
end
local function TableColumnClone(src)
return TableColumnCopy(ImGuiTableColumn(), src)
end
-- Per-instance data that needs preserving across frames (seemingly most others do not need to be preserved aside from debug needs. Does that means they could be moved to ImGuiTableTempData?)
--- @class ImGuiTableInstanceData
function ImGuiTableInstanceData()
return { TableInstanceID = 0, LastOuterHeight = 0.0, LastTopHeadersRowHeight = 0.0, LastFrozenHeight = 0.0, HoveredRowLast = -1, HoveredRowNext = -1 }
end
-- Transient data that are only needed between BeginTable() and EndTable(), those buffers are shared (1 per level of stacked table).
-- - Accessing those requires chasing an extra pointer so for very frequently used data we leave them in the main table structure.
-- - We also leave out of this structure data that tend to be particularly useful for debugging/metrics.
-- FIXME-TABLE: more transient data could be stored in a stacked ImGuiTableTempData: e.g. SortSpecs.
--- @class ImGuiTableTempData
function ImGuiTableTempData()
return {
WindowID = 0, TableIndex = 0, LastTimeActive = -1.0, AngledHeadersExtraWidth = 0.0,
AngledHeadersRequests = ImVector(), ReconcileColumnsRequests = ImVector(),
OldColumnsRawData = nil, OldColumnsData = nil, OldColumnsDataSize = 0,
UserOuterSize = ImVec2(), DrawSplitter = ImDrawListSplitter(),
HostBackupWorkRect = ImRect(), HostBackupParentWorkRect = ImRect(),
HostBackupPrevLineSize = ImVec2(), HostBackupCurrLineSize = ImVec2(), HostBackupCursorMaxPos = ImVec2(),
HostBackupColumnsOffset = 0.0, HostBackupItemWidth = 0.0, HostBackupItemWidthStackSize = 0,
}
end
-- Storage for a table
--- @class ImGuiTable
function ImGuiTable()
return {
ID = 0, Flags = 0, RawData = nil, TempData = nil,
Columns = {}, ColumnsSize = 0, DisplayOrderToIndex = {}, RowCellData = {},
EnabledMaskByDisplayOrder = {}, EnabledMaskByIndex = {}, VisibleMaskByIndex = {},
SettingsLoadedFlags = 0, SettingsOffset = 0, LastFrameActive = -1, ColumnsCount = 0, CurrentRow = 0, CurrentColumn = 0,
InstanceCurrent = 0, InstanceInteracted = 0, RowPosY1 = 0.0, RowPosY2 = 0.0, RowMinHeight = 0.0, RowCellPaddingY = 0.0,
RowTextBaseline = 0.0, RowIndentOffsetX = 0.0, RowFlags = 0, LastRowFlags = 0, RowBgColorCounter = 0,
RowBgColor = { [0] = 0, [1] = 0 }, BorderColorStrong = 0, BorderColorLight = 0, BorderX1 = 0.0, BorderX2 = 0.0,
HostIndentX = 0.0, MinColumnWidth = 0.0, OuterPaddingX = 0.0, CellPaddingX = 0.0, CellSpacingX1 = 0.0, CellSpacingX2 = 0.0,
InnerWidth = 0.0, ColumnsGivenWidth = 0.0, ColumnsAutoFitWidth = 0.0, ColumnsStretchSumWeights = 0.0,
ResizedColumnNextWidth = 0.0, ResizeLockMinContentsX2 = 0.0, RefScale = 0.0, AngledHeadersHeight = 0.0, AngledHeadersSlope = 0.0,
OuterRect = ImRect(), InnerRect = ImRect(), WorkRect = ImRect(), InnerClipRect = ImRect(), BgClipRect = ImRect(),
Bg0ClipRectForDrawCmd = ImRect(), Bg2ClipRectForDrawCmd = ImRect(), HostClipRect = ImRect(), HostBackupInnerClipRect = ImRect(),
OuterWindow = nil, InnerWindow = nil, ColumnsNames = {}, DrawSplitter = nil,
InstanceDataFirst = ImGuiTableInstanceData(), InstanceDataExtra = ImVector(),
SortSpecs = ImGuiTableSortSpecs(), SortSpecsMulti = {}, SortSpecsSingle = ImGuiTableColumnSortSpecs(),
SortSpecsCount = 0, ColumnsEnabledCount = 0, ColumnsEnabledFixedCount = 0, DeclColumnsCount = 0, AngledHeadersCount = 0,
HoveredColumnBody = 0, HoveredColumnBorder = 0, HighlightColumnHeader = 0, AutoFitSingleColumn = 0, ResizedColumn = 0,
LastResizedColumn = 0, HeldHeaderColumn = 0, LastHeldHeaderColumn = 0, ReorderColumn = 0, ReorderColumnDstOrder = 0,
LeftMostEnabledColumn = 0, RightMostEnabledColumn = 0, LeftMostStretchedColumn = 0, RightMostStretchedColumn = 0,
ContextPopupColumn = 0, FreezeRowsRequest = 0, FreezeRowsCount = 0, FreezeColumnsRequest = 0, FreezeColumnsCount = 0,
RowCellDataCurrent = 0, DummyDrawChannel = 0, Bg2DrawChannelCurrent = 0, Bg2DrawChannelUnfrozen = 0, NavLayer = 0,
IsLayoutLocked = false, IsInsideRow = false, IsNewTable = false, IsInitializing = false, IsReconcileMode = false,
IsSortSpecsDirty = false, IsUsingHeaders = false, IsContextPopupOpen = false, DisableDefaultContextMenu = false,
IsSettingsRequestLoad = false, IsSettingsDirty = false, IsDefaultDisplayOrder = false, IsDefaultVisibility = false,
IsResetAllRequest = false, IsResetDisplayOrderRequest = false, IsResetVisibilityRequest = false, IsUnfrozenRows = false,
IsDefaultSizingPolicy = false, IsActiveIdAliveBeforeTable = false, IsActiveIdInTable = false,
HasScrollbarYCurr = false, HasScrollbarYPrev = false, MemoryCompacted = false, HostSkipItems = false,
}
end
--- @class ImGuiTableColumnSettings
function ImGuiTableColumnSettings()
return { WidthOrWeight = 0.0, ID = 0, Index = -1, DisplayOrder = -1, SortOrder = -1, SortDirection = ImGuiSortDirection.None,
IsEnabled = -1, IsStretch = 0, IsLoaded = false }
end
-- Storage for a table .ini settings
--- @class ImGuiTableSettings
function ImGuiTableSettings()
return { ID = 0, SaveFlags = 0, RefScale = 0.0, ColumnsCount = 0, ColumnsCountMax = 0, LastUsedDate = 0, WantApply = false, Columns = {} }
end
-- Storage data for a single column for legacy Columns() api
--- @class ImGuiOldColumnData
function ImGuiOldColumnData()
return { OffsetNorm = 0.0, OffsetNormBeforeResize = 0.0, Flags = 0, ClipRect = ImRect() }
end
--- @class ImGuiOldColumns
function ImGuiOldColumns()
return { ID = 0, Flags = 0, IsFirstFrame = false, IsBeingResized = false, Current = 0, Count = 0, OffMinX = 0.0, OffMaxX = 0.0,
LineMinY = 0.0, LineMaxY = 0.0, HostCursorPosY = 0.0, HostCursorMaxPosX = 0.0, HostInitialClipRect = ImRect(),
HostBackupClipRect = ImRect(), HostBackupParentWorkRect = ImRect(), Columns = ImVector(), Splitter = ImDrawListSplitter() }
end
-- Flags for internal's BeginColumns(). This is an obsolete API. Prefer using BeginTable() nowadays!
ImGuiOldColumnFlags = ImGuiOldColumnFlags or {
None = 0,
NoBorder = lshift(1, 0), -- Disable column dividers
NoResize = lshift(1, 1), -- Disable resizing columns when clicking on the dividers
NoPreserveWidths = lshift(1, 2), -- Disable column width preservation when adjusting columns
NoForceWithinWindow = lshift(1, 3), -- Disable forcing columns to fit within window
GrowParentContentsSize = lshift(1, 4), -- Restore pre-1.51 behavior of extending the parent window contents size but _without affecting the columns width at all_. Will eventually remove.
}
ImGuiTableColumnFlags.NoDirectResize_ = ImGuiTableColumnFlags.NoDirectResize_ or lshift(1, 30) -- [Internal] Disable user resizing this column directly (it may however we resized indirectly from its left edge)
local function Rect(r) return ImRect(r.Min.x, r.Min.y, r.Max.x, r.Max.y) end
local function V2(v) return ImVec2(v.x, v.y) end
-------------------------------------------------------------------------------
-- [SECTION] Tables: Main code
-------------------------------------------------------------------------------
-- - TableFixFlags() [Internal]
-- - TableFindByID() [Internal]
-- - BeginTable()
-- - BeginTableEx() [Internal]
-- - TableBeginInitMemory() [Internal]
-- - TableApplyQueuedRequests() [Internal]
-- - TableSetupColumnFlags() [Internal]
-- - TableUpdateLayout() [Internal]
-- - TableUpdateBorders() [Internal]
-- - EndTable()
-- - TableSetupColumn()
-- - TableSetupScrollFreeze()
-------------------------------------------------------------------------------
-- Configuration
local TABLE_DRAW_CHANNEL_BG0 = 0
local TABLE_DRAW_CHANNEL_BG2_FROZEN = 1
local TABLE_DRAW_CHANNEL_NOCLIP = 2 -- When using ImGuiTableFlags.NoClip (this becomes the last visible channel)
local TABLE_BORDER_SIZE = 1.0 -- FIXME-TABLE: Currently hard-coded because of clipping assumptions with outer borders rendering.
local TABLE_RESIZE_SEPARATOR_HALF_THICKNESS = 4.0 -- Extend outside inner borders.
local TABLE_RESIZE_SEPARATOR_FEEDBACK_TIMER = 0.06 -- Delay/timer before making the hover feedback (color+cursor) visible because tables/columns tends to be more cramped.
-- Helper
local function TableFixFlags(flags, outer_window)
-- Adjust flags: set default sizing policy
if band(flags, ImGuiTableFlags.SizingMask_) == 0 then
flags = bor(flags, (band(flags, ImGuiTableFlags.ScrollX) ~= 0 or band(outer_window.Flags, ImGuiWindowFlags.AlwaysAutoResize) ~= 0) and ImGuiTableFlags.SizingFixedFit or ImGuiTableFlags.SizingStretchSame)
end
-- Adjust flags: enable NoKeepColumnsVisible when using ImGuiTableFlags.SizingFixedSame
if band(flags, ImGuiTableFlags.SizingMask_) == ImGuiTableFlags.SizingFixedSame then
flags = bor(flags, ImGuiTableFlags.NoKeepColumnsVisible)
end
-- Adjust flags: enforce borders when resizable
if band(flags, ImGuiTableFlags.Resizable) ~= 0 then
flags = bor(flags, ImGuiTableFlags.BordersInnerV)
end
-- Adjust flags: disable NoHostExtendX/NoHostExtendY if we have any scrolling going on
if band(flags, bor(ImGuiTableFlags.ScrollX, ImGuiTableFlags.ScrollY)) ~= 0 then
flags = band(flags, bnot(bor(ImGuiTableFlags.NoHostExtendX, ImGuiTableFlags.NoHostExtendY)))
end
-- Adjust flags: NoBordersInBodyUntilResize takes priority over NoBordersInBody
if band(flags, ImGuiTableFlags.NoBordersInBodyUntilResize) ~= 0 then
flags = band(flags, bnot(ImGuiTableFlags.NoBordersInBody))
end
-- Adjust flags: disable saved settings if there's nothing to save
if band(flags, bor(ImGuiTableFlags.Resizable, ImGuiTableFlags.Hideable, ImGuiTableFlags.Reorderable, ImGuiTableFlags.Sortable)) == 0 then
flags = bor(flags, ImGuiTableFlags.NoSavedSettings)
end
-- Inherit _NoSavedSettings from top-level window (child windows always have _NoSavedSettings set)
if band(outer_window.RootWindow.Flags, ImGuiWindowFlags.NoSavedSettings) ~= 0 then
flags = bor(flags, ImGuiTableFlags.NoSavedSettings)
end
return flags
end
function ImGui.TableFindByID(id)
local g = GImGui
return g.Tables:GetByKey(id)
end
--- @param table_ ImGuiTable
--- @param instance_no int
function ImGui.TableGetInstanceData(table_, instance_no)
if instance_no == 0 then
return table_.InstanceDataFirst
end
return table_.InstanceDataExtra.Data[instance_no]
end
function ImGui.TableGetInstanceID(table_, instance_no)
return ImGui.TableGetInstanceData(table_, instance_no).TableInstanceID
end
-- Read about "TABLE SIZING" at the top of this file.
--- @param str_id string
--- @param columns_count int
--- @param flags? ImGuiTableFlags
--- @param outer_size? ImVec2
--- @param inner_width? float
function ImGui.BeginTable(str_id, columns_count, flags, outer_size, inner_width)
local id = ImGui.GetID(str_id)
return ImGui.BeginTableEx(str_id, id, columns_count, flags, outer_size, inner_width)
end
function ImGui.BeginTableEx(name, id, columns_count, flags, outer_size, inner_width)
if flags == nil then flags = 0 end
if outer_size == nil then outer_size = ImVec2(0.0, 0.0) end
if inner_width == nil then inner_width = 0.0 end
local g = GImGui
local outer_window = ImGui.GetCurrentWindow()
if outer_window.SkipItems then -- Consistent with other tables + beneficial side effect that assert on miscalling EndTable() will be more visible.
return false
end
-- Sanity checks
IM_ASSERT(columns_count > 0 and columns_count < IMGUI_TABLE_MAX_COLUMNS)
if band(flags, ImGuiTableFlags.ScrollX) ~= 0 then
IM_ASSERT(inner_width >= 0.0)
end
-- If an outer size is specified ahead we will be able to early out when not visible. Exact clipping criteria may evolve.
-- FIXME: coarse clipping because access to table data causes two issues:
-- - instance numbers varying/unstable. may not be a direct problem for users, but could make outside access broken or confusing, e.g. TestEngine.
-- - can't implement support for ImGuiChildFlags.ResizeY as we need to somehow pull the height data from somewhere. this also needs stable instance numbers.
-- The side-effects of accessing table data on coarse clip would be:
-- - always reserving the pooled ImGuiTable data ahead for a fully clipped table (minor IMHO). Also the 'outer_window_is_measuring_size' criteria may already be defeating this in some situations.
-- - always performing the GetOrAddByKey() O(log N) query in g.Tables.Map[].
local use_child_window = band(flags, bor(ImGuiTableFlags.ScrollX, ImGuiTableFlags.ScrollY)) ~= 0
local avail_size = ImGui.GetContentRegionAvail()
local actual_outer_size = ImTrunc(ImGui.CalcItemSize(outer_size, ImMax(avail_size.x, IMGUI_WINDOW_HARD_MIN_SIZE), use_child_window and ImMax(avail_size.y, IMGUI_WINDOW_HARD_MIN_SIZE) or 0.0))
local outer_rect = ImRect(outer_window.DC.CursorPos, outer_window.DC.CursorPos + actual_outer_size)
local outer_window_is_measuring_size = (outer_window.AutoFitFramesX > 0) or (outer_window.AutoFitFramesY > 0) -- Doesn't apply to AlwaysAutoResize windows!
if use_child_window and ImGui.IsClippedEx(outer_rect, 0) and not outer_window_is_measuring_size then
ImGui.ItemSize(outer_rect)
ImGui.ItemAdd(outer_rect, id)
g.NextWindowData:ClearFlags()
return false
end
-- Acquire storage for the table
local table_ = g.Tables:GetOrAddByKey(id)
if table_.Columns == nil then -- pool created a plain table
for k, v in pairs(ImGuiTable()) do table_[k] = v end
end
-- Acquire temporary buffers
local table_idx = g.Tables:GetIndex(table_)
g.TablesTempDataStacked = g.TablesTempDataStacked + 1
while g.TablesTempDataStacked > g.TablesTempData.Size do
g.TablesTempData:push_back(ImGuiTableTempData())
end
local temp_data = g.TablesTempData.Data[g.TablesTempDataStacked]
table_.TempData = temp_data
temp_data.TableIndex = table_idx
temp_data.ReconcileColumnsRequests:resize(0) -- FIXME: Use shrink(0) everywhere?
table_.DrawSplitter = table_.TempData.DrawSplitter
table_.DrawSplitter:Clear()
-- Fix flags
table_.IsDefaultSizingPolicy = band(flags, ImGuiTableFlags.SizingMask_) == 0
flags = TableFixFlags(flags, outer_window)
-- Initialize
local previous_frame_active = table_.LastFrameActive
local instance_no = (previous_frame_active ~= g.FrameCount) and 0 or (table_.InstanceCurrent + 1)
local previous_flags = table_.Flags
table_.ID = id
table_.Flags = flags
table_.LastFrameActive = g.FrameCount
table_.OuterWindow = outer_window
table_.InnerWindow = outer_window
table_.ColumnsCount = columns_count
table_.IsLayoutLocked = false
table_.InnerWidth = inner_width
table_.NavLayer = outer_window.DC.NavLayerCurrent
table_.IsNewTable = (previous_frame_active == -1)
ImVec2_Copy(temp_data.UserOuterSize, outer_size)
-- Instance data (for instance 0, TableID == TableInstanceID)
local instance_id
table_.InstanceCurrent = instance_no
if instance_no > 0 then
IM_ASSERT(table_.ColumnsCount == columns_count, "BeginTable(): Cannot change columns count mid-frame while preserving same ID")
if table_.InstanceDataExtra.Size < instance_no then
table_.InstanceDataExtra:push_back(ImGuiTableInstanceData())
end
instance_id = ImGui.GetIDWithSeed(instance_no, ImGui.GetIDWithSeed("##Instances", nil, id)) -- Push "##Instances" followed by (int)instance_no in ID stack.
else
instance_id = id
end
local table_instance = ImGui.TableGetInstanceData(table_, table_.InstanceCurrent)
table_instance.TableInstanceID = instance_id
-- When not using a child window, WorkRect.Max will grow as we append contents.
if use_child_window then
-- Ensure no vertical scrollbar appears if we only want horizontal one, to make flag consistent
-- (we have no other way to disable vertical scrollbar of a window while keeping the horizontal one showing)
local override_content_size = ImVec2(FLT_MAX, FLT_MAX)
if band(flags, ImGuiTableFlags.ScrollX) ~= 0 and band(flags, ImGuiTableFlags.ScrollY) == 0 then
override_content_size.y = FLT_MIN
end
-- Ensure specified width (when not specified, Stretched columns will act as if the width == OuterWidth and
-- never lead to any scrolling). We don't handle inner_width < 0.0, we could potentially use it to right-align
-- based on the right side of the child window work rect, which would require knowing ahead if we are going to
-- have decoration taking horizontal spaces (typically a vertical scrollbar).
if band(flags, ImGuiTableFlags.ScrollX) ~= 0 and inner_width > 0.0 then
override_content_size.x = inner_width
end
if override_content_size.x ~= FLT_MAX or override_content_size.y ~= FLT_MAX then
ImGui.SetNextWindowContentSize(ImVec2((override_content_size.x ~= FLT_MAX) and override_content_size.x or 0.0, (override_content_size.y ~= FLT_MAX) and override_content_size.y or 0.0))
end
-- Reset scroll if we are reactivating it
if band(previous_flags, bor(ImGuiTableFlags.ScrollX, ImGuiTableFlags.ScrollY)) == 0 then
if band(g.NextWindowData.HasFlags, ImGuiNextWindowDataFlags.HasScroll) == 0 then
ImGui.SetNextWindowScroll(ImVec2(0.0, 0.0))
end
end
-- Create scrolling region (without border and zero window padding)
local child_window_flags = (band(flags, ImGuiTableFlags.ScrollX) ~= 0) and ImGuiWindowFlags.HorizontalScrollbar or ImGuiWindowFlags.None
ImGui.BeginChildEx(name, instance_id, outer_rect:GetSize(), ImGuiChildFlags.None, child_window_flags)
table_.InnerWindow = g.CurrentWindow
ImRect_Copy(table_.WorkRect, table_.InnerWindow.WorkRect)
ImRect_Copy(table_.OuterRect, table_.InnerWindow:Rect())
ImRect_Copy(table_.InnerRect, table_.InnerWindow.InnerRect)
IM_ASSERT(table_.InnerWindow.WindowPadding.x == 0.0 and table_.InnerWindow.WindowPadding.y == 0.0 and table_.InnerWindow.WindowBorderSize == 0.0)
-- Allow submitting when host is measuring
if table_.InnerWindow.SkipItems and outer_window_is_measuring_size then
table_.InnerWindow.SkipItems = false
end
-- When using multiple instances, ensure they have the same amount of horizontal decorations (aka vertical scrollbar) so stretched columns can be aligned
if instance_no == 0 then
table_.HasScrollbarYPrev = table_.HasScrollbarYCurr
table_.HasScrollbarYCurr = false
end
table_.HasScrollbarYCurr = table_.HasScrollbarYCurr or table_.InnerWindow.ScrollbarY
else
-- For non-scrolling tables, WorkRect == OuterRect == InnerRect.
-- But at this point we do NOT have a correct value for .Max.y (unless a height has been explicitly passed in). It will only be updated in EndTable().
ImRect_Copy(table_.WorkRect, outer_rect)
ImRect_Copy(table_.OuterRect, outer_rect)
ImRect_Copy(table_.InnerRect, outer_rect)
table_.HasScrollbarYPrev = false
table_.HasScrollbarYCurr = false
table_.InnerWindow.DC.TreeDepth = table_.InnerWindow.DC.TreeDepth + 1 -- This is designed to always linking ImGuiTreeNodeFlags.DrawLines linking across a table
end
-- Push a standardized ID for both child-using and not-child-using tables
ImGui.PushOverrideID(id)
if instance_no > 0 then
ImGui.PushOverrideID(instance_id) -- FIXME: Somehow this is not resolved by stack-tool, even tho GetIDWithSeed() submitted the symbol.
end
-- Backup a copy of host window members we will modify
local inner_window = table_.InnerWindow
table_.HostIndentX = inner_window.DC.Indent.x
ImRect_Copy(table_.HostClipRect, inner_window.ClipRect)
table_.HostSkipItems = inner_window.SkipItems
temp_data.WindowID = inner_window.ID
ImRect_Copy(temp_data.HostBackupWorkRect, inner_window.WorkRect)
ImRect_Copy(temp_data.HostBackupParentWorkRect, inner_window.ParentWorkRect)
temp_data.HostBackupColumnsOffset = outer_window.DC.ColumnsOffset.x
ImVec2_Copy(temp_data.HostBackupPrevLineSize, inner_window.DC.PrevLineSize)
ImVec2_Copy(temp_data.HostBackupCurrLineSize, inner_window.DC.CurrLineSize)
ImVec2_Copy(temp_data.HostBackupCursorMaxPos, inner_window.DC.CursorMaxPos)
temp_data.HostBackupItemWidth = outer_window.DC.ItemWidth
temp_data.HostBackupItemWidthStackSize = outer_window.DC.ItemWidthStack.Size
inner_window.DC.PrevLineSize.x = 0.0; inner_window.DC.PrevLineSize.y = 0.0
inner_window.DC.CurrLineSize.x = 0.0; inner_window.DC.CurrLineSize.y = 0.0
-- Make borders not overlap our contents by offsetting HostClipRect (#6765, #7428, #3752)
-- (we normally shouldn't alter HostClipRect as we rely on TableMergeDrawChannels() expanding non-clipped column toward the
-- limits of that rectangle, in order for ImDrawListSplitter::Merge() to merge the draw commands. However since the overlap
-- problem only affect scrolling tables in this case we can get away with doing it without extra cost).
if inner_window ~= outer_window then
-- FIXME: Because inner_window's Scrollbar doesn't know about border size, since it's not encoded in window.WindowBorderSize,
-- it already overlaps it and doesn't need an extra offset. Ideally we should be able to pass custom border size with
-- different x/y values to BeginChild().
local border_size = TABLE_BORDER_SIZE
if band(flags, ImGuiTableFlags.BordersOuterV) ~= 0 then
table_.HostClipRect.Min.x = ImMin(table_.HostClipRect.Min.x + border_size, table_.HostClipRect.Max.x)
if inner_window.DecoOuterSizeX2 == 0.0 then
table_.HostClipRect.Max.x = ImMax(table_.HostClipRect.Max.x - border_size, table_.HostClipRect.Min.x)
end
end
if band(flags, ImGuiTableFlags.BordersOuterH) ~= 0 then
table_.HostClipRect.Min.y = ImMin(table_.HostClipRect.Min.y + border_size, table_.HostClipRect.Max.y)
if inner_window.DecoOuterSizeY2 == 0.0 then
table_.HostClipRect.Max.y = ImMax(table_.HostClipRect.Max.y - border_size, table_.HostClipRect.Min.y)
end
end
end
-- Padding and Spacing
-- - None ........Content..... Pad .....Content........
-- - PadOuter | Pad ..Content..... Pad .....Content.. Pad |
-- - PadInner ........Content.. Pad | Pad ..Content........
-- - PadOuter+PadInner | Pad ..Content.. Pad | Pad ..Content.. Pad |
local pad_outer_x
if band(flags, ImGuiTableFlags.NoPadOuterX) ~= 0 then pad_outer_x = false
elseif band(flags, ImGuiTableFlags.PadOuterX) ~= 0 then pad_outer_x = true
else pad_outer_x = band(flags, ImGuiTableFlags.BordersOuterV) ~= 0 end
local pad_inner_x = band(flags, ImGuiTableFlags.NoPadInnerX) == 0
local inner_spacing_for_border = (band(flags, ImGuiTableFlags.BordersInnerV) ~= 0) and TABLE_BORDER_SIZE or 0.0
local inner_spacing_explicit = (pad_inner_x and band(flags, ImGuiTableFlags.BordersInnerV) == 0) and g.Style.CellPadding.x or 0.0
local inner_padding_explicit = (pad_inner_x and band(flags, ImGuiTableFlags.BordersInnerV) ~= 0) and g.Style.CellPadding.x or 0.0
table_.CellSpacingX1 = inner_spacing_explicit + inner_spacing_for_border
table_.CellSpacingX2 = inner_spacing_explicit
table_.CellPaddingX = inner_padding_explicit
local outer_padding_for_border = (band(flags, ImGuiTableFlags.BordersOuterV) ~= 0) and TABLE_BORDER_SIZE or 0.0
local outer_padding_explicit = pad_outer_x and g.Style.CellPadding.x or 0.0
table_.OuterPaddingX = (outer_padding_for_border + outer_padding_explicit) - table_.CellPaddingX
table_.CurrentColumn = -1
table_.CurrentRow = -1
table_.RowBgColorCounter = 0
table_.LastRowFlags = ImGuiTableRowFlags.None
ImRect_Copy(table_.InnerClipRect, (inner_window == outer_window) and table_.WorkRect or inner_window.ClipRect)
table_.InnerClipRect:ClipWith(table_.WorkRect) -- We need this to honor inner_width
table_.InnerClipRect:ClipWithFull(table_.HostClipRect)
table_.InnerClipRect.Max.y = (band(flags, ImGuiTableFlags.NoHostExtendY) ~= 0) and ImMin(table_.InnerClipRect.Max.y, inner_window.WorkRect.Max.y) or table_.HostClipRect.Max.y
table_.RowPosY1 = table_.WorkRect.Min.y -- This is needed somehow
table_.RowPosY2 = table_.WorkRect.Min.y
table_.RowTextBaseline = 0.0 -- This will be cleared again by TableBeginRow()
table_.RowCellPaddingY = 0.0
table_.FreezeRowsRequest = 0; table_.FreezeRowsCount = 0 -- This will be setup by TableSetupScrollFreeze(), if any
table_.FreezeColumnsRequest = 0; table_.FreezeColumnsCount = 0
table_.IsUnfrozenRows = true
table_.DeclColumnsCount = 0; table_.AngledHeadersCount = 0
if previous_frame_active + 1 < g.FrameCount then
table_.IsActiveIdInTable = false
end
table_.AngledHeadersHeight = 0.0
temp_data.AngledHeadersExtraWidth = 0.0
-- Using opaque colors facilitate overlapping lines of the grid, otherwise we'd need to improve TableDrawBorders()
table_.BorderColorStrong = ImGui.GetColorU32(ImGuiCol.TableBorderStrong)
table_.BorderColorLight = ImGui.GetColorU32(ImGuiCol.TableBorderLight)
-- Make table current
g.CurrentTable = table_
inner_window.DC.NavIsScrollPushableX = false -- Shortcut for NavUpdateCurrentWindowIsScrollPushableX();
outer_window.DC.CurrentTableIdx = table_idx
if inner_window ~= outer_window then -- So EndChild() within the inner window can restore the table properly.
inner_window.DC.CurrentTableIdx = table_idx
end
if band(previous_flags, ImGuiTableFlags.Reorderable) ~= 0 and band(flags, ImGuiTableFlags.Reorderable) == 0 then
table_.IsResetDisplayOrderRequest = true
end
-- Mark as used to avoid GC
while table_idx >= g.TablesLastTimeActive.Size do
g.TablesLastTimeActive:push_back(-1.0)
end
g.TablesLastTimeActive.Data[table_idx + 1] = g.Time
temp_data.LastTimeActive = g.Time
table_.MemoryCompacted = false
-- Setup memory buffer (clear data if columns count changed)
local old_columns_count = table_.ColumnsSize
if old_columns_count ~= 0 and old_columns_count ~= columns_count then
-- Attempt to preserve width and other settings on column count/specs change (#4046, #9108)
IMGUI_DEBUG_LOG_TABLE("[table] Table 0x%08X column count %d -> %d, recreating storage.\n", table_.ID, old_columns_count, columns_count)
IM_ASSERT(temp_data.OldColumnsRawData == nil)
temp_data.OldColumnsRawData = table_.RawData -- Freed during layout
temp_data.OldColumnsData = table_.Columns
temp_data.OldColumnsDataSize = table_.ColumnsSize
for n = 0, temp_data.OldColumnsDataSize - 1 do
temp_data.OldColumnsData[n].IsNeedReconcileSrc = true
end
table_.RawData = nil
end
if table_.RawData == nil then
ImGui.TableBeginInitMemory(table_, columns_count)
table_.IsInitializing = true
end
if table_.IsResetAllRequest then
ImGui.TableResetSettings(table_)
end
if table_.IsInitializing then
-- Initialize
if table_.IsNewTable then
table_.SettingsOffset = -1
table_.IsSettingsRequestLoad = true
end
table_.IsSortSpecsDirty = true
table_.IsSettingsDirty = true -- Records itself into .ini file even when in default state (#7934)
table_.IsReconcileMode = false
table_.InstanceInteracted = -1
table_.ContextPopupColumn = -1
table_.ReorderColumn = -1; table_.ReorderColumnDstOrder = -1; table_.ResizedColumn = -1; table_.LastResizedColumn = -1
table_.AutoFitSingleColumn = -1
table_.HoveredColumnBody = -1; table_.HoveredColumnBorder = -1
for n = 0, columns_count - 1 do
local column = table_.Columns[n]
if temp_data.OldColumnsData and n < temp_data.OldColumnsDataSize then
TableColumnCopy(column, temp_data.OldColumnsData[n])
else
local width_auto = column.WidthAuto
TableColumnCopy(column, ImGuiTableColumn())
column.WidthAuto = width_auto
column.IsPreserveWidthAuto = true -- Preserve WidthAuto when reinitializing a live table: not technically necessary but remove a visible flicker
column.IsEnabled = true; column.IsUserEnabled = true; column.IsUserEnabledNextFrame = true
column.DisplayOrder = n
end
table_.DisplayOrderToIndex[n] = column.DisplayOrder
end
end
-- Load settings
if table_.IsSettingsRequestLoad then
ImGui.TableLoadSettings(table_)
end
-- Disable output until user calls TableNextRow() or TableNextColumn() leading to the TableUpdateLayout() call..
-- This is not strictly necessary but will reduce cases were "out of table" output will be misleading to the user.
-- Because we cannot safely assert in EndTable() when no rows have been created, this seems like our best option.
inner_window.SkipItems = true
-- Clear names
-- At this point the .NameOffset field of each column will be invalid until TableUpdateLayout() or the first call to TableSetupColumn()
if #table_.ColumnsNames > 0 then
table.clear(table_.ColumnsNames)
end
return true
end
function ImGui.TableBeginInitMemory(table_, columns_count)
table_.RawData = true
table_.Columns = {}
table_.DisplayOrderToIndex = {}
table_.RowCellData = {}
for n = 0, columns_count - 1 do
table_.Columns[n] = ImGuiTableColumn()
table_.Columns[n].WidthAuto = 0.0
table_.DisplayOrderToIndex[n] = 0
table_.RowCellData[n] = { BgColor = 0, Column = 0 }
end
table_.ColumnsSize = columns_count
table_.EnabledMaskByDisplayOrder = {}
table_.EnabledMaskByIndex = {}
table_.VisibleMaskByIndex = {}
end
-- Apply queued resizing/reordering/hiding requests
function ImGui.TableApplyQueuedRequests(table_)
-- Handle resizing request
-- (We process this in the TableBegin() of the first instance of each table)
-- FIXME-TABLE: Contains columns if our work area doesn't allow for scrolling?
if table_.InstanceCurrent == 0 then
if table_.ResizedColumn ~= -1 and table_.ResizedColumnNextWidth ~= FLT_MAX then
ImGui.TableSetColumnWidth(table_.ResizedColumn, table_.ResizedColumnNextWidth)
end
table_.LastResizedColumn = table_.ResizedColumn
table_.ResizedColumnNextWidth = FLT_MAX
table_.ResizedColumn = -1
-- Process auto-fit for single column, which is a special case for stretch columns and fixed columns with FixedSame policy.
-- FIXME-TABLE: Would be nice to redistribute available stretch space accordingly to other weights, instead of giving it all to siblings.
if table_.AutoFitSingleColumn ~= -1 then
ImGui.TableSetColumnWidth(table_.AutoFitSingleColumn, table_.Columns[table_.AutoFitSingleColumn].WidthAuto)
table_.AutoFitSingleColumn = -1
end
end
-- Handle reordering request
-- Note: we don't clear ReorderColumn after handling the request (FIXME: clarify why or add a test).
if table_.InstanceCurrent == 0 then
table_.LastHeldHeaderColumn = table_.HeldHeaderColumn
table_.HeldHeaderColumn = -1
if table_.ReorderColumn ~= -1 and table_.ReorderColumnDstOrder ~= -1 then
ImGui.TableSetColumnDisplayOrder(table_, table_.ReorderColumn, table_.ReorderColumnDstOrder)
table_.ReorderColumnDstOrder = -1
end
-- Release
local g = GImGui
if g.ActiveId == 0 then -- FIXME: Need to revisit. See 38f5e5a.
table_.ReorderColumn = -1
end
--if (table.HeldHeaderColumn == -1 and table.ReorderColumn ~= -1)
-- table.ReorderColumn = -1
end
-- Handle display order / visibility reset requests
if table_.IsResetDisplayOrderRequest then
for n = 0, table_.ColumnsCount - 1 do
table_.Columns[n].DisplayOrder = n
table_.DisplayOrderToIndex[n] = n
end
table_.IsResetDisplayOrderRequest = false
table_.IsSettingsDirty = true
end
if table_.IsResetVisibilityRequest then
for n = 0, table_.ColumnsSize - 1 do
local column = table_.Columns[n]
local v = band(column.Flags, ImGuiTableColumnFlags.DefaultHide) == 0
column.IsUserEnabled = v; column.IsUserEnabledNextFrame = v
end
table_.IsResetVisibilityRequest = false
table_.IsSettingsDirty = true
end
end
-- Apply immediately. See TableQueueSetColumnDisplayOrder() for additional checks/constraints.
function ImGui.TableSetColumnDisplayOrder(table_, column_n, dst_order)
IM_ASSERT(column_n >= 0 and column_n < table_.ColumnsCount)
IM_ASSERT(dst_order >= 0 and dst_order < table_.ColumnsCount)
local src_column = table_.Columns[column_n]
local src_order = src_column.DisplayOrder
if src_order == dst_order then
return
end
local reorder_dir = (dst_order < src_order) and -1 or 1
src_column.DisplayOrder = dst_order
local order_n = src_order + reorder_dir
while order_n ~= dst_order + reorder_dir do
local c = table_.Columns[table_.DisplayOrderToIndex[order_n]]
c.DisplayOrder = c.DisplayOrder - reorder_dir
order_n = order_n + reorder_dir
end
--IM_ASSERT(dst_column.DisplayOrder == dst_order - reorder_dir)
-- Display order is stored in both columns.IndexDisplayOrder and table.DisplayOrder[]. Rebuild later from the former.
-- FIXME-OPT: If this is called multiple times we'd effectively have a O(N^2) thing going on.
for n = 0, table_.ColumnsCount - 1 do
table_.DisplayOrderToIndex[table_.Columns[n].DisplayOrder] = n
end
table_.IsSettingsDirty = true
end
local function TableGetMaxDisplayOrderAllowed(table_, src_order, dst_order)
dst_order = ImClamp(dst_order, 0, table_.ColumnsCount - 1)
if src_order == dst_order then
return dst_order
end
-- Cannot cross over the frozen column limit when interactively reordering.
-- TableSetupScrollFreeze() enforce a display order range for frozen columns. Reordering across the frozen column barrier is illegal and will be undone.
if table_.FreezeColumnsRequest > 0 then
dst_order = (src_order < table_.FreezeColumnsRequest) and ImMin(dst_order, table_.FreezeColumnsRequest - 1) or ImMax(dst_order, table_.FreezeColumnsRequest)
end
-- Cannot cross over a column with the ImGuiTableColumnFlags.NoReorder flag.
local reorder_dir = (src_order < dst_order) and 1 or -1
local order_n = src_order
while (src_order < dst_order and order_n <= dst_order) or (dst_order < src_order and order_n >= dst_order) do
if band(table_.Columns[table_.DisplayOrderToIndex[order_n]].Flags, ImGuiTableColumnFlags.NoReorder) ~= 0 then
dst_order = (order_n == src_order) and src_order or (order_n - reorder_dir)
break
end
order_n = order_n + reorder_dir
end
return dst_order
end
-- Reorder requested by user interaction.
function ImGui.TableQueueSetColumnDisplayOrder(table_, column_n, dst_order)
local src_order = table_.Columns[column_n].DisplayOrder
table_.ReorderColumn = column_n
table_.ReorderColumnDstOrder = -1
dst_order = TableGetMaxDisplayOrderAllowed(table_, src_order, dst_order)
if table_.IsLayoutLocked and dst_order == src_order then -- We allow calling the function before layout w/ reconcile so don't early out.
return
end
table_.ReorderColumnDstOrder = dst_order
end
-- Adjust flags: default width mode + stretch columns are not allowed when auto extending
local function TableSetupColumnFlags(table_, column, column_n, flags_in)
local flags = flags_in
-- Sizing Policy
if band(flags, ImGuiTableColumnFlags.WidthMask_) == 0 then
local table_sizing_policy = band(table_.Flags, ImGuiTableFlags.SizingMask_)
if table_sizing_policy == ImGuiTableFlags.SizingFixedFit or table_sizing_policy == ImGuiTableFlags.SizingFixedSame then
flags = bor(flags, ImGuiTableColumnFlags.WidthFixed)
else
flags = bor(flags, ImGuiTableColumnFlags.WidthStretch)
end
else
IM_ASSERT(ImIsPowerOfTwo(band(flags, ImGuiTableColumnFlags.WidthMask_))) -- Check that only 1 of each set is used.
end
-- Resize
if band(table_.Flags, ImGuiTableFlags.Resizable) == 0 then
flags = bor(flags, ImGuiTableColumnFlags.NoResize)
end
-- Sorting
if band(flags, ImGuiTableColumnFlags.NoSortAscending) ~= 0 and band(flags, ImGuiTableColumnFlags.NoSortDescending) ~= 0 then
flags = bor(flags, ImGuiTableColumnFlags.NoSort)
end
-- Indentation
if band(flags, ImGuiTableColumnFlags.IndentMask_) == 0 then
flags = bor(flags, (column_n == 0) and ImGuiTableColumnFlags.IndentEnable or ImGuiTableColumnFlags.IndentDisable)
end
-- Alignment
--if (flags & ImGuiTableColumnFlags.AlignMask_) == 0 then
-- flags = bor(flags, ImGuiTableColumnFlags.AlignCenter)
--IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiTableColumnFlags.AlignMask_)) -- Check that only 1 of each set is used.
-- Preserve status flags
column.Flags = bor(flags, band(column.Flags, ImGuiTableColumnFlags.StatusMask_))
-- Build an ordered list of available sort directions
column.SortDirectionsAvailCount = 0; column.SortDirectionsAvailMask = 0; column.SortDirectionsAvailList = 0
if band(table_.Flags, ImGuiTableFlags.Sortable) ~= 0 then
local count, mask, list = 0, 0, 0
local Asc, Desc = ImGuiSortDirection.Ascending, ImGuiSortDirection.Descending
if band(flags, ImGuiTableColumnFlags.PreferSortAscending) ~= 0 and band(flags, ImGuiTableColumnFlags.NoSortAscending) == 0 then mask = bor(mask, lshift(1, Asc)); list = bor(list, lshift(Asc, lshift(count, 1))); count = count + 1 end
if band(flags, ImGuiTableColumnFlags.PreferSortDescending) ~= 0 and band(flags, ImGuiTableColumnFlags.NoSortDescending) == 0 then mask = bor(mask, lshift(1, Desc)); list = bor(list, lshift(Desc, lshift(count, 1))); count = count + 1 end
if band(flags, ImGuiTableColumnFlags.PreferSortAscending) == 0 and band(flags, ImGuiTableColumnFlags.NoSortAscending) == 0 then mask = bor(mask, lshift(1, Asc)); list = bor(list, lshift(Asc, lshift(count, 1))); count = count + 1 end
if band(flags, ImGuiTableColumnFlags.PreferSortDescending) == 0 and band(flags, ImGuiTableColumnFlags.NoSortDescending) == 0 then mask = bor(mask, lshift(1, Desc)); list = bor(list, lshift(Desc, lshift(count, 1))); count = count + 1 end
if band(table_.Flags, ImGuiTableFlags.SortTristate) ~= 0 or count == 0 then mask = bor(mask, lshift(1, ImGuiSortDirection.None)); count = count + 1 end
column.SortDirectionsAvailList = band(list, 0xFF)
column.SortDirectionsAvailMask = band(mask, 0xFF)
column.SortDirectionsAvailCount = count
ImGui.TableFixColumnSortDirection(table_, column)
end
end
-- Layout columns for the frame. This is in essence the followup to BeginTable() and this is our largest function.
-- Runs on the first call to TableNextRow(), to give a chance for TableSetupColumn() and other TableSetupXXXXX() functions to be called first.
-- FIXME-TABLE: Our width (and therefore our WorkRect) will be minimal in the first frame for _WidthAuto columns.
-- Increase feedback side-effect with widgets relying on WorkRect.Max.x... Maybe provide a default distribution for _WidthAuto columns?
function ImGui.TableUpdateLayout(table_)
local g = GImGui
local temp_data = table_.TempData
IM_ASSERT(table_.IsLayoutLocked == false)
local columns_count = table_.ColumnsCount
-- Reconcile moved columns
if temp_data.ReconcileColumnsRequests.Size > 0 then
ImGui.TableReconcileColumns(table_)
end
if temp_data.OldColumnsRawData then
temp_data.OldColumnsRawData = nil
temp_data.OldColumnsData = nil
temp_data.OldColumnsDataSize = 0
end
-- Apply columns settings
if table_.IsSettingsRequestLoad then
ImGui.TableLoadSettingsForColumns(table_)
end
if table_.IsInitializing or table_.IsSettingsRequestLoad then
for n = 0, table_.ColumnsSize - 1 do
local column = table_.Columns[n]
local init_flags
if table_.IsSettingsRequestLoad then
init_flags = column.IsLoadedSettings and bnot(table_.SettingsLoadedFlags) or bnot(0)
else
init_flags = column.IsJustCreated and bnot(0) or 0
end
ImGui.TableInitColumnDefaults(table_, column, n, init_flags)
end
ImGui.TableFixDisplayOrder(table_) -- Call even for non _Reorderable table as we loaded .ini data.
table_.IsSettingsRequestLoad = false
end
-- Apply queued resizing/reordering/hiding requests
ImGui.TableApplyQueuedRequests(table_)
-- Handle DPI/font resize
-- This is designed to facilitate DPI changes with the assumption that e.g. style.CellPadding has been scaled as well.
-- It will also react to changing fonts with mixed results. It doesn't need to be perfect but merely provide a decent transition.
-- FIXME-DPI: Provide consistent standards for reference size. Perhaps using g.CurrentDpiScale would be more self explanatory.
-- This is will lead us to non-rounded WidthRequest in columns, which should work but is a poorly tested path.
local new_ref_scale_unit = g.FontSize -- g.Font.GetCharAdvance('A') ?
if table_.RefScale ~= 0.0 and table_.RefScale ~= new_ref_scale_unit then
local scale_factor = new_ref_scale_unit / table_.RefScale
for n = 0, columns_count - 1 do
table_.Columns[n].WidthRequest = table_.Columns[n].WidthRequest * scale_factor
end
end
table_.RefScale = new_ref_scale_unit
local table_sizing_policy = band(table_.Flags, ImGuiTableFlags.SizingMask_)
table_.IsDefaultDisplayOrder = true; table_.IsDefaultVisibility = true
table_.ColumnsEnabledCount = 0
BitArrayClearAll(table_.EnabledMaskByIndex)
BitArrayClearAll(table_.EnabledMaskByDisplayOrder)
table_.LeftMostEnabledColumn = -1
table_.MinColumnWidth = ImMax(1.0, g.Style.FramePadding.x * 1.0) -- g.Style.ColumnsMinSpacing -- FIXME-TABLE
-- [Part 1] Apply/lock Enabled and Order states. Calculate auto/ideal width for columns. Count fixed/stretch columns.
-- Process columns in their visible orders as we are building the Prev/Next indices.
local count_fixed = 0 -- Number of columns that have fixed sizing policies
local count_stretch = 0 -- Number of columns that have stretch sizing policies
local prev_visible_column_idx = -1
local has_auto_fit_request = false
local has_resizable = false
local stretch_sum_width_auto = 0.0
local fixed_max_width_auto = 0.0
for order_n = 0, columns_count - 1 do
local column_n = table_.DisplayOrderToIndex[order_n]
local column = table_.Columns[column_n]
-- Clear column setup if not submitted by user. Currently we make it mandatory to call TableSetupColumn() every frame.
-- It would easily work without but we're not ready to guarantee it since e.g. names need resubmission anyway.
-- We take a slight shortcut but in theory we could be calling TableSetupColumn() here with dummy values, it should yield the same effect.
if table_.DeclColumnsCount <= column_n then
TableSetupColumnFlags(table_, column, column_n, ImGuiTableColumnFlags.None)
column.NameOffset = -1
column.ID = 0; column.UserData = 0
column.InitStretchWeightOrWidth = -1.0
end
-- Update Enabled state, mark settings and sort specs dirty
if band(table_.Flags, ImGuiTableFlags.Hideable) == 0 or band(column.Flags, ImGuiTableColumnFlags.NoHide) ~= 0 then
column.IsUserEnabledNextFrame = true
end
if column.IsUserEnabled ~= column.IsUserEnabledNextFrame then
column.IsUserEnabled = column.IsUserEnabledNextFrame
table_.IsSettingsDirty = true
end
column.IsEnabled = column.IsUserEnabled and band(column.Flags, ImGuiTableColumnFlags.Disabled) == 0
column.IsJustCreated = false
if column.IsEnabled ~= (band(column.Flags, ImGuiTableColumnFlags.DefaultHide) == 0) then
table_.IsDefaultVisibility = false
end
if column_n ~= order_n then
table_.IsDefaultDisplayOrder = false
end
if column.SortOrder ~= -1 and not column.IsEnabled then
table_.IsSortSpecsDirty = true
end
if column.SortOrder > 0 and band(table_.Flags, ImGuiTableFlags.SortMulti) == 0 then
table_.IsSortSpecsDirty = true
end
-- Auto-fit unsized columns
local start_auto_fit
if band(column.Flags, ImGuiTableColumnFlags.WidthFixed) ~= 0 then
start_auto_fit = column.WidthRequest < 0.0
else
start_auto_fit = column.StretchWeight < 0.0
end
if start_auto_fit then
column.AutoFitQueue = lshift(1, 3) - 1 -- Fit for three frames
column.CannotSkipItemsQueue = lshift(1, 3) - 1
end
if not column.IsEnabled then
column.IndexWithinEnabledSet = -1
else
-- Mark as enabled and link to previous/next enabled column
column.PrevEnabledColumn = prev_visible_column_idx
column.NextEnabledColumn = -1
if prev_visible_column_idx ~= -1 then
table_.Columns[prev_visible_column_idx].NextEnabledColumn = column_n
else
table_.LeftMostEnabledColumn = column_n
end
column.IndexWithinEnabledSet = table_.ColumnsEnabledCount
table_.ColumnsEnabledCount = table_.ColumnsEnabledCount + 1
BitArraySet(table_.EnabledMaskByIndex, column_n)
BitArraySet(table_.EnabledMaskByDisplayOrder, column.DisplayOrder)
prev_visible_column_idx = column_n
IM_ASSERT(column.IndexWithinEnabledSet <= column.DisplayOrder)
-- Calculate ideal/auto column width (that's the width required for all contents to be visible without clipping)
-- Combine width from regular rows + width from headers unless requested not to.
if not column.IsPreserveWidthAuto and table_.InstanceCurrent == 0 then
column.WidthAuto = ImGui.TableGetColumnWidthAuto(table_, column)
end
-- Non-resizable columns keep their requested width (apply user value regardless of IsPreserveWidthAuto)
local column_is_resizable = band(column.Flags, ImGuiTableColumnFlags.NoResize) == 0
if column_is_resizable then
has_resizable = true
end
if band(column.Flags, ImGuiTableColumnFlags.WidthFixed) ~= 0 and column.InitStretchWeightOrWidth > 0.0 and not column_is_resizable then
column.WidthAuto = column.InitStretchWeightOrWidth
end
if column.AutoFitQueue ~= 0x00 then
has_auto_fit_request = true
end
if band(column.Flags, ImGuiTableColumnFlags.WidthStretch) ~= 0 then
stretch_sum_width_auto = stretch_sum_width_auto + column.WidthAuto
count_stretch = count_stretch + 1
else
fixed_max_width_auto = ImMax(fixed_max_width_auto, column.WidthAuto)
count_fixed = count_fixed + 1
end
end
end
if band(table_.Flags, ImGuiTableFlags.Sortable) ~= 0 and table_.SortSpecsCount == 0 and band(table_.Flags, ImGuiTableFlags.SortTristate) == 0 then
table_.IsSortSpecsDirty = true
end
table_.RightMostEnabledColumn = prev_visible_column_idx
IM_ASSERT(table_.LeftMostEnabledColumn >= 0 and table_.RightMostEnabledColumn >= 0)
-- [Part 2] Disable child window clipping while fitting columns. This is not strictly necessary but makes it possible to avoid
-- the column fitting having to wait until the first visible frame of the child container (may or not be a good thing). Also see #6510.
-- FIXME-TABLE: for always auto-resizing columns may not want to do that all the time.
if has_auto_fit_request and table_.OuterWindow ~= table_.InnerWindow then
table_.InnerWindow.SkipItems = false
end
if has_auto_fit_request then
table_.IsSettingsDirty = true
end
-- [Part 3] Fix column flags and record a few extra information.
local sum_width_requests = 0.0 -- Sum of all width for fixed and auto-resize columns, excluding width contributed by Stretch columns but including spacing/padding.
local stretch_sum_weights = 0.0 -- Sum of all weights for stretch columns.
table_.LeftMostStretchedColumn = -1; table_.RightMostStretchedColumn = -1
for column_n = 0, columns_count - 1 do
if BitTest(table_.EnabledMaskByIndex, column_n) then
local column = table_.Columns[column_n]
local column_is_resizable = band(column.Flags, ImGuiTableColumnFlags.NoResize) == 0
if band(column.Flags, ImGuiTableColumnFlags.WidthFixed) ~= 0 then
-- Apply same widths policy
local width_auto = column.WidthAuto
if table_sizing_policy == ImGuiTableFlags.SizingFixedSame and (column.AutoFitQueue ~= 0x00 or not column_is_resizable) then
width_auto = fixed_max_width_auto
end
-- Apply automatic width
-- Latch initial size for fixed columns and update it constantly for auto-resizing column (unless clipped!)
if column.AutoFitQueue ~= 0x00 then
column.WidthRequest = width_auto
elseif band(column.Flags, ImGuiTableColumnFlags.WidthFixed) ~= 0 and not column_is_resizable and column.IsRequestOutput then
column.WidthRequest = width_auto
end
-- FIXME-TABLE: Increase minimum size during init frame to avoid biasing auto-fitting widgets
-- (e.g. TextWrapped) too much. Otherwise what tends to happen is that TextWrapped would output a very
-- large height (= first frame scrollbar display very off + clipper would skip lots of items).
-- This is merely making the side-effect less extreme, but doesn't properly fixes it.
-- FIXME: Move this to .WidthGiven to avoid temporary lossyness?
-- FIXME: This break IsPreserveWidthAuto from not flickering if the stored WidthAuto was smaller.
if column.AutoFitQueue > 0x01 and table_.IsInitializing and not column.IsPreserveWidthAuto then
column.WidthRequest = ImMax(column.WidthRequest, table_.MinColumnWidth * 4.0) -- FIXME-TABLE: Another constant/scale?
end
sum_width_requests = sum_width_requests + column.WidthRequest
else
-- Initialize stretch weight
if column.AutoFitQueue ~= 0x00 or column.StretchWeight < 0.0 or not column_is_resizable then
if column.InitStretchWeightOrWidth > 0.0 then
column.StretchWeight = column.InitStretchWeightOrWidth
elseif table_sizing_policy == ImGuiTableFlags.SizingStretchProp then
column.StretchWeight = (column.WidthAuto / stretch_sum_width_auto) * count_stretch
else
column.StretchWeight = 1.0
end
end
stretch_sum_weights = stretch_sum_weights + column.StretchWeight
if table_.LeftMostStretchedColumn == -1 or table_.Columns[table_.LeftMostStretchedColumn].DisplayOrder > column.DisplayOrder then
table_.LeftMostStretchedColumn = column_n
end
if table_.RightMostStretchedColumn == -1 or table_.Columns[table_.RightMostStretchedColumn].DisplayOrder < column.DisplayOrder then
table_.RightMostStretchedColumn = column_n
end
end
column.IsPreserveWidthAuto = false
sum_width_requests = sum_width_requests + table_.CellPaddingX * 2.0
end
end
table_.ColumnsEnabledFixedCount = count_fixed
table_.ColumnsStretchSumWeights = stretch_sum_weights
-- [Part 4] Apply final widths based on requested widths
local work_rect = Rect(table_.WorkRect)
local width_spacings = (table_.OuterPaddingX * 2.0) + (table_.CellSpacingX1 + table_.CellSpacingX2) * (table_.ColumnsEnabledCount - 1)
local width_removed = (table_.HasScrollbarYPrev and not table_.InnerWindow.ScrollbarY) and g.Style.ScrollbarSize or 0.0 -- To synchronize decoration width of synced tables with mismatching scrollbar state (#5920)
local width_avail = ImMax(1.0, ((band(table_.Flags, ImGuiTableFlags.ScrollX) ~= 0 and table_.InnerWidth == 0.0) and table_.InnerClipRect:GetWidth() or work_rect:GetWidth()) - width_removed)
local width_avail_for_stretched_columns = width_avail - width_spacings - sum_width_requests
local width_remaining_for_stretched_columns = width_avail_for_stretched_columns
table_.ColumnsGivenWidth = width_spacings + (table_.CellPaddingX * 2.0) * table_.ColumnsEnabledCount
for column_n = 0, columns_count - 1 do
if BitTest(table_.EnabledMaskByIndex, column_n) then
local column = table_.Columns[column_n]
-- Allocate width for stretched/weighted columns (StretchWeight gets converted into WidthRequest)
if band(column.Flags, ImGuiTableColumnFlags.WidthStretch) ~= 0 then
local weight_ratio = column.StretchWeight / stretch_sum_weights
column.WidthRequest = IM_TRUNC(ImMax(width_avail_for_stretched_columns * weight_ratio, table_.MinColumnWidth) + 0.01)
width_remaining_for_stretched_columns = width_remaining_for_stretched_columns - column.WidthRequest
end
-- [Resize Rule 1] The right-most Visible column is not resizable if there is at least one Stretch column
-- See additional comments in TableSetColumnWidth().
if column.NextEnabledColumn == -1 and table_.LeftMostStretchedColumn ~= -1 then
column.Flags = bor(column.Flags, ImGuiTableColumnFlags.NoDirectResize_)
end
-- Assign final width, record width in case we will need to shrink
column.WidthGiven = ImTrunc(ImMax(column.WidthRequest, table_.MinColumnWidth))
table_.ColumnsGivenWidth = table_.ColumnsGivenWidth + column.WidthGiven
end
end
-- [Part 5] Redistribute stretch remainder width due to rounding (remainder width is < 1.0 * number of Stretch column).
-- Using right-to-left distribution (more likely to match resizing cursor).
if width_remaining_for_stretched_columns >= 1.0 and band(table_.Flags, ImGuiTableFlags.PreciseWidths) == 0 then
local order_n = columns_count - 1
while stretch_sum_weights > 0.0 and width_remaining_for_stretched_columns >= 1.0 and order_n >= 0 do
if BitTest(table_.EnabledMaskByDisplayOrder, order_n) then
local column = table_.Columns[table_.DisplayOrderToIndex[order_n]]
if band(column.Flags, ImGuiTableColumnFlags.WidthStretch) ~= 0 then
column.WidthRequest = column.WidthRequest + 1.0
column.WidthGiven = column.WidthGiven + 1.0
width_remaining_for_stretched_columns = width_remaining_for_stretched_columns - 1.0
end
end
order_n = order_n - 1
end
end
-- Determine if table is hovered which will be used to flag columns as hovered.
-- - In principle we'd like to use the equivalent of IsItemHovered(ImGuiHoveredFlags.AllowWhenBlockedByActiveItem),
-- but because our item is partially submitted at this point we use ItemHoverable() and a workaround (temporarily
-- clear ActiveId, which is equivalent to the change provided by _AllowWhenBLockedByActiveItem).
-- - This allows columns to be marked as hovered when e.g. clicking a button inside the column, or using drag and drop.
local table_instance = ImGui.TableGetInstanceData(table_, table_.InstanceCurrent)
table_instance.HoveredRowLast = table_instance.HoveredRowNext
table_instance.HoveredRowNext = -1
table_.HoveredColumnBody = -1; table_.HoveredColumnBorder = -1
local mouse_hit_rect = ImRect(table_.OuterRect.Min.x, table_.OuterRect.Min.y, table_.OuterRect.Max.x, ImMax(table_.OuterRect.Max.y, table_.OuterRect.Min.y + table_instance.LastOuterHeight))
local backup_active_id = g.ActiveId
g.ActiveId = 0
local is_hovering_table = ImGui.ItemHoverable(mouse_hit_rect, 0, ImGuiItemFlags.None)
g.ActiveId = backup_active_id
-- Determine skewed MousePos.x to support angled headers.
local mouse_skewed_x = g.IO.MousePos.x
if table_.AngledHeadersHeight > 0.0 then
if g.IO.MousePos.y >= table_.OuterRect.Min.y and g.IO.MousePos.y <= table_.OuterRect.Min.y + table_.AngledHeadersHeight then
mouse_skewed_x = mouse_skewed_x + ImTrunc((table_.OuterRect.Min.y + table_.AngledHeadersHeight - g.IO.MousePos.y) * table_.AngledHeadersSlope)
end
end
-- [Part 6] Setup final position, offset, skip/clip states and clipping rectangles, detect hovered column
-- Process columns in their visible orders as we are comparing the visible order and adjusting host_clip_rect while looping.
local has_at_least_one_column_requesting_output = false
local offset_x_frozen = (table_.FreezeColumnsCount > 0)
local offset_x = ((table_.FreezeColumnsCount > 0) and table_.OuterRect.Min.x or work_rect.Min.x) + table_.OuterPaddingX - table_.CellSpacingX1
local host_clip_rect = Rect(table_.InnerClipRect)
--host_clip_rect.Max.x += table.CellPaddingX + table.CellSpacingX2
BitArrayClearAll(table_.VisibleMaskByIndex)
for order_n = 0, columns_count - 1 do
local column_n = table_.DisplayOrderToIndex[order_n]
local column = table_.Columns[column_n]
-- Initial nav layer: using FreezeRowsCount, NOT FreezeRowsRequest, so Header line changes layer when frozen
column.NavLayerCurrent = (table_.FreezeRowsCount > 0) and ImGuiNavLayer.Menu or table_.NavLayer
if offset_x_frozen and table_.FreezeColumnsCount == order_n then
offset_x = offset_x + work_rect.Min.x - table_.OuterRect.Min.x
offset_x_frozen = false
end
-- Clear status flags
column.Flags = band(column.Flags, bnot(ImGuiTableColumnFlags.StatusMask_))
if not BitTest(table_.EnabledMaskByDisplayOrder, order_n) then
-- Hidden column: clear a few fields and we are done with it for the remainder of the function.
-- We set a zero-width clip rect but set Min.y/Max.y properly to not interfere with the clipper.
column.MinX = offset_x; column.MaxX = offset_x; column.WorkMinX = offset_x
column.ClipRect.Min.x = offset_x; column.ClipRect.Max.x = offset_x
column.WidthGiven = 0.0
column.ClipRect.Min.y = work_rect.Min.y
column.ClipRect.Max.y = FLT_MAX
column.ClipRect:ClipWithFull(host_clip_rect)
column.IsVisibleX = false; column.IsVisibleY = false; column.IsRequestOutput = false
column.IsSkipItems = true
column.ItemWidth = 1.0
else
-- Lock start position
column.MinX = offset_x
-- Lock width based on start position and minimum/maximum width for this position
column.WidthMax = ImGui.TableCalcMaxColumnWidth(table_, column_n)
column.WidthGiven = ImMin(column.WidthGiven, column.WidthMax)
column.WidthGiven = ImMax(column.WidthGiven, ImMin(column.WidthRequest, table_.MinColumnWidth))
column.MaxX = offset_x + column.WidthGiven + table_.CellSpacingX1 + table_.CellSpacingX2 + table_.CellPaddingX * 2.0
-- Lock other positions
-- - ClipRect.Min.x: Because merging draw commands doesn't compare min boundaries, we make ClipRect.Min.x match left bounds to be consistent regardless of merging.
-- - ClipRect.Max.x: using WorkMaxX instead of MaxX (aka including padding) makes things more consistent when resizing down, tho slightly detrimental to visibility in very-small column.
-- - ClipRect.Max.x: using MaxX makes it easier for header to receive hover highlight with no discontinuity and display sorting arrow.
-- - FIXME-TABLE: We want equal width columns to have equal (ClipRect.Max.x - WorkMinX) width, which means ClipRect.max.x cannot stray off host_clip_rect.Max.x else right-most column may appear shorter.
local previous_instance_work_min_x = column.WorkMinX
column.WorkMinX = column.MinX + table_.CellPaddingX + table_.CellSpacingX1
column.WorkMaxX = column.MaxX - table_.CellPaddingX - table_.CellSpacingX2 -- Expected max
column.ItemWidth = ImTrunc(column.WidthGiven * 0.65)
column.ClipRect.Min.x = column.MinX
column.ClipRect.Min.y = work_rect.Min.y
column.ClipRect.Max.x = column.MaxX --column.WorkMaxX
column.ClipRect.Max.y = FLT_MAX
column.ClipRect:ClipWithFull(host_clip_rect)
-- Mark column as Clipped (not in sight)
-- Note that scrolling tables (where inner_window ~= outer_window) handle Y clipped earlier in BeginTable() so IsVisibleY really only applies to non-scrolling tables.
-- FIXME-TABLE: Because InnerClipRect.Max.y is conservatively ==outer_window.ClipRect.Max.y, we never can mark columns _Above_ the scroll line as not IsVisibleY.
-- Taking advantage of LastOuterHeight would yield good results there...
-- FIXME-TABLE: Y clipping is disabled because it effectively means not submitting will reduce contents width which is fed to outer_window.DC.CursorMaxPos.x,
-- and this may be used (e.g. typically by outer_window using AlwaysAutoResize or outer_window's horizontal scrollbar, but could be something else).
-- Possible solution to preserve last known content width for clipped column. Test 'table_reported_size' fails when enabling Y clipping and window is resized small.
column.IsVisibleX = (column.ClipRect.Max.x > column.ClipRect.Min.x)
column.IsVisibleY = true -- (column.ClipRect.Max.y > column.ClipRect.Min.y)
local is_visible = column.IsVisibleX --and column.IsVisibleY
if is_visible then
BitArraySet(table_.VisibleMaskByIndex, column_n)
end
-- Mark column as requesting output from user. Note that fixed + non-resizable sets are auto-fitting at all times and therefore always request output.
column.IsRequestOutput = is_visible or column.AutoFitQueue ~= 0 or column.CannotSkipItemsQueue ~= 0
-- Mark column as SkipItems (ignoring all items/layout)
-- (table.HostSkipItems is a copy of inner_window.SkipItems before we cleared it above in Part 2)
column.IsSkipItems = not column.IsEnabled or table_.HostSkipItems
if column.IsSkipItems then
IM_ASSERT(not is_visible)
end
if column.IsRequestOutput and not column.IsSkipItems then
has_at_least_one_column_requesting_output = true
end
-- Update status flags
column.Flags = bor(column.Flags, ImGuiTableColumnFlags.IsEnabled)
if is_visible then
column.Flags = bor(column.Flags, ImGuiTableColumnFlags.IsVisible)
end
if column.SortOrder ~= -1 then
column.Flags = bor(column.Flags, ImGuiTableColumnFlags.IsSorted)
end
-- Detect hovered column
if is_hovering_table and mouse_skewed_x >= column.ClipRect.Min.x and mouse_skewed_x < column.ClipRect.Max.x then
column.Flags = bor(column.Flags, ImGuiTableColumnFlags.IsHovered)
table_.HoveredColumnBody = column_n
end
-- Alignment
-- FIXME-TABLE: This align based on the whole column width, not per-cell, and therefore isn't useful in
-- many cases (to be able to honor this we might be able to store a log of cells width, per row, for
-- visible rows, but nav/programmatic scroll would have visible artifacts.)
--if (column.Flags & ImGuiTableColumnFlags.AlignRight)
-- column.WorkMinX = ImMax(column.WorkMinX, column.MaxX - column.ContentWidthRowsUnfrozen)
--else if (column.Flags & ImGuiTableColumnFlags.AlignCenter)
-- column.WorkMinX = ImLerp(column.WorkMinX, ImMax(column.StartX, column.MaxX - column.ContentWidthRowsUnfrozen), 0.5)
-- Reset content width variables
if table_.InstanceCurrent == 0 then
column.ContentMaxXFrozen = column.WorkMinX
column.ContentMaxXUnfrozen = column.WorkMinX
column.ContentMaxXHeadersUsed = column.WorkMinX
column.ContentMaxXHeadersIdeal = column.WorkMinX
else
-- As we store an absolute value to make per-cell updates faster, we need to offset values used for width computation.
local offset_from_previous_instance = column.WorkMinX - previous_instance_work_min_x
column.ContentMaxXFrozen = column.ContentMaxXFrozen + offset_from_previous_instance
column.ContentMaxXUnfrozen = column.ContentMaxXUnfrozen + offset_from_previous_instance
column.ContentMaxXHeadersUsed = column.ContentMaxXHeadersUsed + offset_from_previous_instance
column.ContentMaxXHeadersIdeal = column.ContentMaxXHeadersIdeal + offset_from_previous_instance
end
-- Don't decrement auto-fit counters until container window got a chance to submit its items
if table_.HostSkipItems == false and table_.InstanceCurrent == 0 then
column.AutoFitQueue = rshift(column.AutoFitQueue, 1)
column.CannotSkipItemsQueue = rshift(column.CannotSkipItemsQueue, 1)
end
if order_n < table_.FreezeColumnsCount then
host_clip_rect.Min.x = ImClamp(column.MaxX + TABLE_BORDER_SIZE, host_clip_rect.Min.x, host_clip_rect.Max.x)
end
offset_x = offset_x + column.WidthGiven + table_.CellSpacingX1 + table_.CellSpacingX2 + table_.CellPaddingX * 2.0
end
end
-- In case the table is visible (e.g. decorations) but all columns clipped, we keep a column visible.
-- Else if give no chance to a clipper-savvy user to submit rows and therefore total contents height used by scrollbar.
if has_at_least_one_column_requesting_output == false then
table_.Columns[table_.LeftMostEnabledColumn].IsRequestOutput = true
table_.Columns[table_.LeftMostEnabledColumn].IsSkipItems = false
end
-- [Part 7] Detect/store when we are hovering the unused space after the right-most column (so e.g. context menus can react on it)
-- Clear Resizable flag if none of our column are actually resizable (either via an explicit _NoResize flag, either
-- because of using _WidthAuto/_WidthStretch). This will hide the resizing option from the context menu.
local unused_x1 = ImMax(table_.WorkRect.Min.x, table_.Columns[table_.RightMostEnabledColumn].ClipRect.Max.x)
if is_hovering_table and table_.HoveredColumnBody == -1 then
if mouse_skewed_x >= unused_x1 then
table_.HoveredColumnBody = columns_count
end
end
if has_resizable == false and band(table_.Flags, ImGuiTableFlags.Resizable) ~= 0 then
table_.Flags = band(table_.Flags, bnot(ImGuiTableFlags.Resizable))
end
table_.IsActiveIdAliveBeforeTable = (g.ActiveIdIsAlive ~= 0)
-- [Part 8] Lock actual OuterRect/WorkRect right-most position.
-- This is done late to handle the case of fixed-columns tables not claiming more widths that they need.
-- Because of this we are careful with uses of WorkRect and InnerClipRect before this point.
if table_.RightMostStretchedColumn ~= -1 then
table_.Flags = band(table_.Flags, bnot(ImGuiTableFlags.NoHostExtendX))
end
if band(table_.Flags, ImGuiTableFlags.NoHostExtendX) ~= 0 then
table_.OuterRect.Max.x = unused_x1; table_.WorkRect.Max.x = unused_x1
table_.InnerClipRect.Max.x = ImMin(table_.InnerClipRect.Max.x, unused_x1)
end
ImRect_Copy(table_.InnerWindow.ParentWorkRect, table_.WorkRect)
table_.BorderX1 = table_.InnerClipRect.Min.x
table_.BorderX2 = table_.InnerClipRect.Max.x
-- Setup window's WorkRect.Max.y for GetContentRegionAvail(). Other values will be updated in each TableBeginCell() call.
local window_content_max_y
if band(table_.Flags, ImGuiTableFlags.NoHostExtendY) ~= 0 then
window_content_max_y = table_.OuterRect.Max.y
else
window_content_max_y = ImMax(table_.InnerWindow.ContentRegionRect.Max.y, (band(table_.Flags, ImGuiTableFlags.ScrollY) ~= 0) and 0.0 or table_.OuterRect.Max.y)
end
table_.InnerWindow.WorkRect.Max.y = ImClamp(window_content_max_y - g.Style.CellPadding.y, table_.InnerWindow.WorkRect.Min.y, table_.InnerWindow.WorkRect.Max.y)
-- [Part 9] Allocate draw channels and setup background cliprect
ImGui.TableSetupDrawChannels(table_)
-- [Part 10] Hit testing on borders
if band(table_.Flags, ImGuiTableFlags.Resizable) ~= 0 then
ImGui.TableUpdateBorders(table_)
end
table_instance.LastTopHeadersRowHeight = 0.0
table_.IsLayoutLocked = true
table_.IsUsingHeaders = false
-- Highlight header
table_.HighlightColumnHeader = -1
if table_.IsContextPopupOpen and table_.ContextPopupColumn ~= -1 and table_.InstanceInteracted == table_.InstanceCurrent then
table_.HighlightColumnHeader = table_.ContextPopupColumn
elseif band(table_.Flags, ImGuiTableFlags.HighlightHoveredColumn) ~= 0 and table_.HoveredColumnBody ~= -1 and table_.HoveredColumnBody ~= columns_count and table_.HoveredColumnBorder == -1 then
if g.ActiveId == 0 or (table_.IsActiveIdInTable or g.DragDropActive) then
table_.HighlightColumnHeader = table_.HoveredColumnBody
end
end
-- [Part 11] Default context menu
-- - To append to this menu: you can call TableBeginContextMenuPopup()/.../EndPopup().
-- - To modify or replace this: set table.DisableDefaultContextMenu = true, then call TableBeginContextMenuPopup()/.../EndPopup().
-- - You may call TableDrawDefaultContextMenu() with selected flags to display specific sections of the default menu,
-- e.g. TableDrawDefaultContextMenu(table, table.Flags & ~ImGuiTableFlags.Hideable) will display everything EXCEPT columns visibility options.
if table_.DisableDefaultContextMenu == false and ImGui.TableBeginContextMenuPopup(table_) then
ImGui.TableDrawDefaultContextMenu(table_, table_.Flags)
ImGui.EndPopup()
end
-- [Part 12] Sanitize and build sort specs before we have a chance to use them for display.
-- This path will only be exercised when sort specs are modified before header rows (e.g. init or visibility change)
if table_.IsSortSpecsDirty and band(table_.Flags, ImGuiTableFlags.Sortable) ~= 0 then
ImGui.TableSortSpecsBuild(table_)
end
-- [Part 13] Setup inner window decoration size (for scrolling / nav tracking to properly take account of frozen rows/columns)
if table_.FreezeColumnsRequest > 0 then
table_.InnerWindow.DecoInnerSizeX1 = table_.Columns[table_.DisplayOrderToIndex[table_.FreezeColumnsRequest - 1]].MaxX - table_.OuterRect.Min.x -- FIXME-FROZEN
end
if table_.FreezeRowsRequest > 0 then
table_.InnerWindow.DecoInnerSizeY1 = table_instance.LastFrozenHeight
end
table_instance.LastFrozenHeight = 0.0
local inner_window = table_.InnerWindow
local bs = g.BoxSelectState
if bs and bs.Window == inner_window and bs.UnclipMode then
ImGui.TableApplyExternalUnclipRect(table_, bs.UnclipRect)
end
-- Initial state
if band(table_.Flags, ImGuiTableFlags.NoClip) ~= 0 then
table_.DrawSplitter:SetCurrentChannel(inner_window.DrawList, TABLE_DRAW_CHANNEL_NOCLIP)
else
inner_window.DrawList:PushClipRect(inner_window.InnerClipRect.Min, inner_window.InnerClipRect.Max, false) -- FIXME: use table.InnerClipRect?
end
end
-- When starting a BeginMultiSelect() after table has been layout we update IsRequestOutput fields.
function ImGui.TableApplyExternalUnclipRect(table_, rect)
if rect:IsInverted() then
return
end
for column_n = 0, table_.ColumnsCount - 1 do
local column = table_.Columns[column_n]
if not column.IsRequestOutput then
if rect:Overlaps(ImRect(column.MinX, table_.WorkRect.Min.y, column.MaxX, FLT_MAX)) then
column.IsRequestOutput = true
end
end
end
end
-- Process hit-testing on resizing borders. Actual size change will be applied in EndTable()
-- - Set table.HoveredColumnBorder with a short delay/timer to reduce visual feedback noise.
function ImGui.TableUpdateBorders(table_)
local g = GImGui
IM_ASSERT(band(table_.Flags, ImGuiTableFlags.Resizable) ~= 0)
-- At this point OuterRect height may be zero or under actual final height, so we rely on temporal coherency and
-- use the final height from last frame. Because this is only affecting _interaction_ with columns, it is not
-- really problematic (whereas the actual visual will be displayed in EndTable() and using the current frame height).
-- Actual columns highlight/render will be performed in EndTable() and not be affected.
local table_instance = ImGui.TableGetInstanceData(table_, table_.InstanceCurrent)
local hit_half_width = ImTrunc(TABLE_RESIZE_SEPARATOR_HALF_THICKNESS * g.CurrentDpiScale)
local hit_y1 = ((table_.FreezeRowsCount >= 1) and table_.OuterRect.Min.y or table_.WorkRect.Min.y) + table_.AngledHeadersHeight
local hit_y2_body = ImMax(table_.OuterRect.Max.y, hit_y1 + table_instance.LastOuterHeight - table_.AngledHeadersHeight)
local hit_y2_head = hit_y1 + table_instance.LastTopHeadersRowHeight
for order_n = 0, table_.ColumnsCount - 1 do
repeat
if not BitTest(table_.EnabledMaskByDisplayOrder, order_n) then
break
end
local column_n = table_.DisplayOrderToIndex[order_n]
local column = table_.Columns[column_n]
if band(column.Flags, bor(ImGuiTableColumnFlags.NoResize, ImGuiTableColumnFlags.NoDirectResize_)) ~= 0 then
break
end
-- ImGuiTableFlags.NoBordersInBodyUntilResize will be honored in TableDrawBorders()
local border_y2_hit = (band(table_.Flags, ImGuiTableFlags.NoBordersInBody) ~= 0) and hit_y2_head or hit_y2_body
if band(table_.Flags, ImGuiTableFlags.NoBordersInBody) ~= 0 and table_.IsUsingHeaders == false then
break
end
if not column.IsVisibleX and table_.LastResizedColumn ~= column_n then
break
end
local column_id = ImGui.TableGetColumnResizeID(table_, column_n, table_.InstanceCurrent)
local hit_rect = ImRect(column.MaxX - hit_half_width, hit_y1, column.MaxX + hit_half_width, border_y2_hit)
ImGui.ItemAdd(hit_rect, column_id, nil, ImGuiItemFlags.NoNav)
--ImGui.GetForegroundDrawList():AddRect(hit_rect.Min, hit_rect.Max, IM_COL32(255, 0, 0, 100))
local pressed, hovered, held = ImGui.ButtonBehavior(hit_rect, column_id, bor(ImGuiButtonFlags.FlattenChildren, ImGuiButtonFlags.PressedOnClick, ImGuiButtonFlags.PressedOnDoubleClick, ImGuiButtonFlags.NoNavFocus))
if pressed and ImGui.IsMouseDoubleClicked(0) then
ImGui.TableSetColumnWidthAutoSingle(table_, column_n)
ImGui.ClearActiveID()
held = false
end
if held then
if table_.LastResizedColumn == -1 then
table_.ResizeLockMinContentsX2 = (table_.RightMostEnabledColumn ~= -1) and table_.Columns[table_.RightMostEnabledColumn].MaxX or -FLT_MAX
end
table_.ResizedColumn = column_n
table_.InstanceInteracted = table_.InstanceCurrent
end
if (hovered and g.HoveredIdTimer > TABLE_RESIZE_SEPARATOR_FEEDBACK_TIMER) or held then
table_.HoveredColumnBorder = column_n
ImGui.SetMouseCursor(ImGuiMouseCursor.ResizeEW)
end
until true
end
end
function ImGui.EndTable()
local g = GImGui
local table_ = g.CurrentTable
IM_ASSERT_USER_ERROR_RET(table_ ~= nil, "EndTable() call should only be done while in BeginTable() scope!")
-- This assert would be very useful to catch a common error... unfortunately it would probably trigger in some
-- cases, and for consistency user may sometimes output empty tables (and still benefit from e.g. outer border)
--IM_ASSERT(table.IsLayoutLocked, "Table unused: never called TableNextRow(), is that the intent?")
-- If the user never got to call TableNextRow() or TableNextColumn(), we call layout ourselves to ensure all our
-- code paths are consistent (instead of just hoping that TableBegin/TableEnd will work), get borders drawn, etc.
if not table_.IsLayoutLocked then
ImGui.TableUpdateLayout(table_)
end
local flags = table_.Flags
local inner_window = table_.InnerWindow
local outer_window = table_.OuterWindow
local temp_data = table_.TempData
IM_ASSERT(inner_window == g.CurrentWindow and inner_window.ID == temp_data.WindowID)
IM_ASSERT(outer_window == inner_window or outer_window == inner_window.ParentWindow)
if table_.IsInsideRow then
ImGui.TableEndRow(table_)
end
-- Context menu in columns body
if band(flags, ImGuiTableFlags.ContextMenuInBody) ~= 0 then
if table_.HoveredColumnBody ~= -1 and not ImGui.IsAnyItemHovered() and ImGui.IsMouseReleased(ImGuiMouseButton.Right) then
ImGui.TableOpenContextMenu(table_.HoveredColumnBody)
end
end
-- Finalize table height
local table_instance = ImGui.TableGetInstanceData(table_, table_.InstanceCurrent)
ImVec2_Copy(inner_window.DC.PrevLineSize, temp_data.HostBackupPrevLineSize)
ImVec2_Copy(inner_window.DC.CurrLineSize, temp_data.HostBackupCurrLineSize)
ImVec2_Copy(inner_window.DC.CursorMaxPos, temp_data.HostBackupCursorMaxPos)
local inner_content_max_y = ImCeil(table_.RowPosY2) -- Rounding final position is important as we currently don't round row height ('Demo->Tables->Outer Size' demo uses non-integer heights)
IM_ASSERT(table_.RowPosY2 == inner_window.DC.CursorPos.y)
if inner_window ~= outer_window then
inner_window.DC.CursorMaxPos.y = inner_content_max_y
elseif band(flags, ImGuiTableFlags.NoHostExtendY) == 0 then
local v = ImMax(table_.OuterRect.Max.y, inner_content_max_y) -- Patch OuterRect/InnerRect height
table_.OuterRect.Max.y = v; table_.InnerRect.Max.y = v
end
table_.WorkRect.Max.y = ImMax(table_.WorkRect.Max.y, table_.OuterRect.Max.y)
table_instance.LastOuterHeight = table_.OuterRect:GetHeight()
-- Setup inner scrolling range
-- FIXME: This ideally should be done earlier, in BeginTable() SetNextWindowContentSize call, just like writing to inner_window.DC.CursorMaxPos.y,
-- but since the later is likely to be impossible to do we'd rather update both axes together.
if band(table_.Flags, ImGuiTableFlags.ScrollX) ~= 0 then
local outer_padding_for_border = (band(table_.Flags, ImGuiTableFlags.BordersOuterV) ~= 0) and TABLE_BORDER_SIZE or 0.0
local max_pos_x = inner_window.DC.CursorMaxPos.x
if table_.RightMostEnabledColumn ~= -1 then
max_pos_x = ImMax(max_pos_x, table_.Columns[table_.RightMostEnabledColumn].WorkMaxX + table_.CellPaddingX + table_.OuterPaddingX - outer_padding_for_border)
end
if table_.ResizedColumn ~= -1 then
max_pos_x = ImMax(max_pos_x, table_.ResizeLockMinContentsX2)
end
inner_window.DC.CursorMaxPos.x = max_pos_x + table_.TempData.AngledHeadersExtraWidth
end
-- Pop clipping rect
if band(flags, ImGuiTableFlags.NoClip) == 0 then
inner_window.DrawList:PopClipRect()
end
ImRect_CopyFromV4(inner_window.ClipRect, inner_window.DrawList._ClipRectStack:back())
-- Draw borders
if band(flags, ImGuiTableFlags.Borders) ~= 0 then
ImGui.TableDrawBorders(table_)
end
-- Flatten channels and merge draw calls
local splitter = table_.DrawSplitter
splitter:SetCurrentChannel(inner_window.DrawList, 0)
if band(table_.Flags, ImGuiTableFlags.NoClip) == 0 then
ImGui.TableMergeDrawChannels(table_)
end
splitter:Merge(inner_window.DrawList)
-- Update ColumnsAutoFitWidth to get us ahead for host using our size to auto-resize without waiting for next BeginTable()
local auto_fit_width_for_fixed = 0.0
local auto_fit_width_for_stretched = 0.0
local auto_fit_width_for_stretched_min = 0.0
for column_n = 0, table_.ColumnsCount - 1 do
if BitTest(table_.EnabledMaskByIndex, column_n) then
local column = table_.Columns[column_n]
local column_width_request
if band(column.Flags, ImGuiTableColumnFlags.WidthFixed) ~= 0 and band(column.Flags, ImGuiTableColumnFlags.NoResize) == 0 then
column_width_request = column.WidthRequest
else
column_width_request = ImGui.TableGetColumnWidthAuto(table_, column)
end
if band(column.Flags, ImGuiTableColumnFlags.WidthFixed) ~= 0 then
auto_fit_width_for_fixed = auto_fit_width_for_fixed + column_width_request
else
auto_fit_width_for_stretched = auto_fit_width_for_stretched + column_width_request
end
if band(column.Flags, ImGuiTableColumnFlags.WidthStretch) ~= 0 and band(column.Flags, ImGuiTableColumnFlags.NoResize) ~= 0 then
auto_fit_width_for_stretched_min = ImMax(auto_fit_width_for_stretched_min, column_width_request / (column.StretchWeight / table_.ColumnsStretchSumWeights))
end
end
end
local width_spacings = (table_.OuterPaddingX * 2.0) + (table_.CellSpacingX1 + table_.CellSpacingX2) * (table_.ColumnsEnabledCount - 1)
table_.ColumnsAutoFitWidth = width_spacings + (table_.CellPaddingX * 2.0) * table_.ColumnsEnabledCount + auto_fit_width_for_fixed + ImMax(auto_fit_width_for_stretched, auto_fit_width_for_stretched_min)
-- Update scroll
if band(table_.Flags, ImGuiTableFlags.ScrollX) == 0 and inner_window ~= outer_window then
inner_window.Scroll.x = 0.0
elseif table_.LastResizedColumn ~= -1 and table_.ResizedColumn == -1 and inner_window.ScrollbarX and table_.InstanceInteracted == table_.InstanceCurrent then
-- When releasing a column being resized, scroll to keep the resulting column in sight
local neighbor_width_to_keep_visible = table_.MinColumnWidth + table_.CellPaddingX * 2.0
local column = table_.Columns[table_.LastResizedColumn]
if column.MaxX < table_.InnerClipRect.Min.x then
ImGui.SetScrollFromPosX(inner_window, column.MaxX - inner_window.Pos.x - neighbor_width_to_keep_visible, 1.0)
elseif column.MaxX > table_.InnerClipRect.Max.x then
ImGui.SetScrollFromPosX(inner_window, column.MaxX - inner_window.Pos.x + neighbor_width_to_keep_visible, 1.0)
end
end
-- Apply resizing/dragging at the end of the frame
if table_.ResizedColumn ~= -1 and table_.InstanceCurrent == table_.InstanceInteracted then
local column = table_.Columns[table_.ResizedColumn]
local new_x2 = (g.IO.MousePos.x - g.ActiveIdClickOffset.x + ImTrunc(TABLE_RESIZE_SEPARATOR_HALF_THICKNESS * g.CurrentDpiScale))
local new_width = ImTrunc(new_x2 - column.MinX - table_.CellSpacingX1 - table_.CellPaddingX * 2.0)
table_.ResizedColumnNextWidth = new_width
end
table_.IsActiveIdInTable = (g.ActiveIdIsAlive ~= 0 and table_.IsActiveIdAliveBeforeTable == false)
-- Pop from id stack
IM_ASSERT_USER_ERROR(inner_window.IDStack:back() == table_instance.TableInstanceID, "Mismatching PushID/PopID!")
IM_ASSERT_USER_ERROR(outer_window.DC.ItemWidthStack.Size >= temp_data.HostBackupItemWidthStackSize, "Too many PopItemWidth!")
if table_.InstanceCurrent > 0 then
ImGui.PopID()
end
ImGui.PopID()
-- Restore window data that we modified
local backup_outer_max_pos = V2(outer_window.DC.CursorMaxPos)
ImRect_Copy(inner_window.WorkRect, temp_data.HostBackupWorkRect)
ImRect_Copy(inner_window.ParentWorkRect, temp_data.HostBackupParentWorkRect)
inner_window.SkipItems = table_.HostSkipItems
ImVec2_Copy(outer_window.DC.CursorPos, table_.OuterRect.Min)
outer_window.DC.ItemWidth = temp_data.HostBackupItemWidth
outer_window.DC.ItemWidthStack:resize(temp_data.HostBackupItemWidthStackSize)
outer_window.DC.ColumnsOffset.x = temp_data.HostBackupColumnsOffset
-- Layout in outer window
-- (FIXME: To allow auto-fit and allow desirable effect of SameLine() we dissociate 'used' vs 'ideal' size by overriding
-- CursorPosPrevLine and CursorMaxPos manually. That should be a more general layout feature, see same problem e.g. #3414)
if inner_window ~= outer_window then
local backup_nav_layers_active_mask = inner_window.DC.NavLayersActiveMask
inner_window.DC.NavLayersActiveMask = bor(inner_window.DC.NavLayersActiveMask, lshift(1, table_.NavLayer)) -- So empty table don't appear to navigate differently.
g.CurrentTable = nil -- To avoid error recovery recursing
ImGui.EndChild()
g.CurrentTable = table_
inner_window.DC.NavLayersActiveMask = backup_nav_layers_active_mask
else
inner_window.DC.TreeDepth = inner_window.DC.TreeDepth - 1
ImGui.ItemSize(table_.OuterRect:GetSize())
ImGui.ItemAdd(table_.OuterRect, 0)
end
-- Override declared contents width/height to enable auto-resize while not needlessly adding a scrollbar
if band(table_.Flags, ImGuiTableFlags.NoHostExtendX) ~= 0 then
-- FIXME-TABLE: Could we remove this section?
-- ColumnsAutoFitWidth may be one frame ahead here since for Fixed+NoResize is calculated from latest contents
IM_ASSERT(band(table_.Flags, ImGuiTableFlags.ScrollX) == 0)
outer_window.DC.CursorMaxPos.x = ImMax(backup_outer_max_pos.x, table_.OuterRect.Min.x + table_.ColumnsAutoFitWidth)
elseif temp_data.UserOuterSize.x <= 0.0 then
-- Some references for this: #7651 + tests "table_reported_size", "table_reported_size_outer" equivalent Y block, #9352
-- - FIXME-TABLE: Would make sense to pre-compute expected scrollbar visibility/sizes to generally save a frame of feedback? See broken test in 'table_reported_size_outer'
local outer_content_max_x = table_.OuterRect.Min.x + table_.ColumnsAutoFitWidth
local decoration_size = table_.TempData.AngledHeadersExtraWidth + ((inner_window ~= outer_window) and inner_window.ScrollbarSizes.x or 0.0)
outer_window.DC.IdealMaxPos.x = ImMax(outer_window.DC.IdealMaxPos.x, outer_content_max_x + decoration_size - temp_data.UserOuterSize.x)
outer_window.DC.CursorMaxPos.x = ImMax(backup_outer_max_pos.x, ImMin(table_.OuterRect.Max.x, outer_content_max_x + decoration_size))
else
outer_window.DC.CursorMaxPos.x = ImMax(backup_outer_max_pos.x, table_.OuterRect.Max.x)
end
if temp_data.UserOuterSize.y <= 0.0 then
-- (same comment as above)
local outer_content_size_y = (inner_window == outer_window) and (inner_content_max_y - table_.InnerRect.Min.y) or (inner_content_max_y - inner_window.DC.CursorStartPos.y)
local outer_content_max_y = table_.OuterRect.Min.y + outer_content_size_y
local decoration_size = (inner_window ~= outer_window) and inner_window.ScrollbarSizes.y or 0.0
outer_window.DC.IdealMaxPos.y = ImMax(outer_window.DC.IdealMaxPos.y, outer_content_max_y + decoration_size - temp_data.UserOuterSize.y)
outer_window.DC.CursorMaxPos.y = ImMax(backup_outer_max_pos.y, ImMin(table_.OuterRect.Max.y, outer_content_max_y + decoration_size))
else
-- OuterRect.Max.y may already have been pushed downward from the initial value (unless ImGuiTableFlags.NoHostExtendY is set)
outer_window.DC.CursorMaxPos.y = ImMax(backup_outer_max_pos.y, table_.OuterRect.Max.y)
end
-- Save settings
if table_.IsSettingsDirty then
ImGui.TableSaveSettings(table_)
end
table_.IsInitializing = false
-- Clear or restore current table, if any
IM_ASSERT(g.CurrentWindow == outer_window and g.CurrentTable == table_)
IM_ASSERT(g.TablesTempDataStacked > 0)
g.TablesTempDataStacked = g.TablesTempDataStacked - 1
temp_data = (g.TablesTempDataStacked > 0) and g.TablesTempData.Data[g.TablesTempDataStacked] or nil
g.CurrentTable = (temp_data and (temp_data.WindowID == outer_window.ID)) and g.Tables:GetByIndex(temp_data.TableIndex) or nil
if g.CurrentTable then
g.CurrentTable.TempData = temp_data
g.CurrentTable.DrawSplitter = temp_data.DrawSplitter
end
outer_window.DC.CurrentTableIdx = g.CurrentTable and g.Tables:GetIndex(g.CurrentTable) or -1
ImGui.NavUpdateCurrentWindowIsScrollPushableX()
end
-- Called in TableUpdateLayout() when initializing/after loading settings.
-- 'init_mask' specify which fields to initialize.
--- @param column_n int # index of `column` (C++: Columns.index_from_ptr(column))
function ImGui.TableInitColumnDefaults(table_, column, column_n, init_mask)
local flags = column.Flags
if band(init_mask, ImGuiTableFlags.Resizable) ~= 0 then
local init_width_or_weight = column.InitStretchWeightOrWidth
column.WidthRequest = (band(flags, ImGuiTableColumnFlags.WidthFixed) ~= 0 and init_width_or_weight > 0.0) and init_width_or_weight or -1.0
column.StretchWeight = (init_width_or_weight > 0.0 and band(flags, ImGuiTableColumnFlags.WidthStretch) ~= 0) and init_width_or_weight or -1.0
if init_width_or_weight > 0.0 then -- Disable auto-fit if an explicit width/weight has been specified
column.AutoFitQueue = 0x00
end
end
if band(init_mask, ImGuiTableFlags.Reorderable) ~= 0 then
column.DisplayOrder = (band(table_.Flags, ImGuiTableFlags.Reorderable) ~= 0) and -1 or column_n
end
if band(init_mask, ImGuiTableFlags.Hideable) ~= 0 then
local v = band(flags, ImGuiTableColumnFlags.DefaultHide) == 0
column.IsUserEnabled = v; column.IsUserEnabledNextFrame = v
end
if band(init_mask, ImGuiTableFlags.Sortable) ~= 0 then
-- Multiple columns using _DefaultSort will be reassigned unique SortOrder values when building the sort specs.
column.SortOrder = (band(flags, ImGuiTableColumnFlags.DefaultSort) ~= 0) and 0 or -1
if band(flags, ImGuiTableColumnFlags.DefaultSort) ~= 0 then
column.SortDirection = (band(flags, ImGuiTableColumnFlags.PreferSortDescending) ~= 0) and ImGuiSortDirection.Descending or ImGuiSortDirection.Ascending
else
column.SortDirection = ImGuiSortDirection.None
end
end
end
-- See "COLUMNS SIZING POLICIES" comments at the top of this file
-- If (init_width_or_weight <= 0.0) it is ignored
local function TableSetupColumnApply(table_, idx, id, name_offset, flags, init_width_or_weight, user_data)
local column = table_.Columns[idx]
-- Assert when passing a width or weight if policy is entirely left to default, to avoid storing width into weight and vice-versa.
-- Give a grace to users of ImGuiTableFlags.ScrollX.
if table_.IsDefaultSizingPolicy and band(flags, ImGuiTableColumnFlags.WidthMask_) == 0 and band(flags, ImGuiTableFlags.ScrollX) == 0 then
IM_ASSERT_USER_ERROR_RET(init_width_or_weight <= 0.0, "TableSetupColumn(): can only specify width/weight if sizing policy is set explicitly in either Table or Column.")
end
-- When passing a width automatically enforce WidthFixed policy
-- (whereas TableSetupColumnFlags would default to WidthAuto if table is not resizable)
if band(flags, ImGuiTableColumnFlags.WidthMask_) == 0 and init_width_or_weight > 0.0 then
if band(table_.Flags, ImGuiTableFlags.SizingMask_) == ImGuiTableFlags.SizingFixedFit or band(table_.Flags, ImGuiTableFlags.SizingMask_) == ImGuiTableFlags.SizingFixedSame then
flags = bor(flags, ImGuiTableColumnFlags.WidthFixed)
end
end
if band(flags, ImGuiTableColumnFlags.AngledHeader) ~= 0 then
flags = bor(flags, ImGuiTableColumnFlags.NoHeaderLabel)
table_.AngledHeadersCount = table_.AngledHeadersCount + 1
end
TableSetupColumnFlags(table_, column, idx, flags)
column.ID = id
column.UserData = user_data
column.NameOffset = name_offset
column.InitStretchWeightOrWidth = init_width_or_weight
end
--- @param label string?
--- @param flags? ImGuiTableColumnFlags
--- @param init_width_or_weight? float
--- @param user_data? ImGuiID
function ImGui.TableSetupColumn(label, flags, init_width_or_weight, user_data)
if flags == nil then flags = 0 end
if init_width_or_weight == nil then init_width_or_weight = 0.0 end
if user_data == nil then user_data = 0 end
local g = GImGui
local table_ = g.CurrentTable
IM_ASSERT_USER_ERROR_RET(table_ ~= nil, "Call should only be done while in BeginTable() scope!")
IM_ASSERT_USER_ERROR_RET(table_.DeclColumnsCount < table_.ColumnsCount, "TableSetupColumn(): called too many times!")
IM_ASSERT_USER_ERROR_RET(table_.IsLayoutLocked == false, "TableSetupColumn(): need to call before first row!") -- Table layout is locked when submitting a row or when calling BeginMultiSelect() with box-select.
IM_ASSERT(band(flags, ImGuiTableColumnFlags.StatusMask_) == 0, "Illegal to pass StatusMask values to TableSetupColumn()")
-- Store name
-- FIXME: If we recorded the number of \n in names we could compute header row height
local name_offset = -1
if label ~= nil and label ~= "" then
name_offset = #table_.ColumnsNames
table_.ColumnsNames[name_offset + 1] = label
end
local column_id = (label ~= nil and label ~= "") and ImHashStr(label) or 0
-- When ID changed or a column moved: defer the request until layout where we will process full reconcile.
local column_idx = table_.DeclColumnsCount
table_.DeclColumnsCount = table_.DeclColumnsCount + 1
local column = table_.Columns[column_idx]
-- If topology change goes into reconcile mode
if table_.IsReconcileMode == false and column.ID ~= column_id and not table_.IsNewTable then
table_.IsReconcileMode = true
end
-- Fast/common path
if table_.IsReconcileMode == false then
TableSetupColumnApply(table_, column_idx, column_id, name_offset, flags, init_width_or_weight, user_data)
column.IsNeedReconcileSrc = false; column.IsNeedReconcileDst = false
local old = table_.TempData.OldColumnsData
if old and column_idx < table_.TempData.OldColumnsDataSize then
old[column_idx].IsNeedReconcileSrc = false
end
return
end
-- Reconcile path: defer applying data to TableUpdateLayout() -> TableReconcileMovedColumns() -> TableSetupColumnApply().
-- On column count change: search the previous columns:
-- - the live Columns[] array is truncated on shrink, so a moved column may only exist in old data.
-- - the live Columns[] array may already be partially reset.
local reconcile_data = {
ID = column_id, NameOffset = name_offset, Flags = flags, InitWidthOrWeight = init_width_or_weight, UserData = user_data,
ColumnNewIdx = column_idx, ColumnOldIdx = -1, ColumnOldData = ImGuiTableColumn(),
}
table_.TempData.ReconcileColumnsRequests:push_back(reconcile_data)
column.NameOffset = name_offset -- Allow TableGetColumnName() to work before layout
column.IsNeedReconcileSrc = true; column.IsNeedReconcileDst = true
end
-- NB: This was written to be similar to the logic in TableLoadSettingsForColumns().
function ImGui.TableReconcileColumns(table_)
local temp_data = table_.TempData
IMGUI_DEBUG_LOG_TABLE("[table] Reconcile columns for table 0x%08X\n", table_.ID)
local dst_columns = table_.Columns
local src_columns, src_count
if temp_data.OldColumnsData == nil then
src_columns, src_count = table_.Columns, table_.ColumnsSize
else
src_columns, src_count = temp_data.OldColumnsData, temp_data.OldColumnsDataSize
end
-- Find matches for named columns.
local matches = 0
local reconcile_requests = temp_data.ReconcileColumnsRequests
for _, reconcile_data in reconcile_requests:iter() do
if reconcile_data.ID ~= 0 then
for src_n = 0, src_count - 1 do
local src_column = src_columns[src_n]
if src_column.ID == reconcile_data.ID and src_column.IsNeedReconcileSrc then
local dst_column = dst_columns[reconcile_data.ColumnNewIdx]
IM_ASSERT(src_column.IsNeedReconcileSrc and dst_column.IsNeedReconcileDst)
src_column.IsNeedReconcileSrc = false; dst_column.IsNeedReconcileDst = false
reconcile_data.ColumnOldIdx = src_n
reconcile_data.ColumnOldData = TableColumnClone(src_column)
matches = matches + 1
break
end
end
end
end
-- Remaining entries are matched sequentially.
local dst_idx = 1 -- index in reconcile array (1-based)
if matches ~= reconcile_requests.Size then
for src_n = 0, src_count - 1 do
local src_column = src_columns[src_n]
if src_column.IsNeedReconcileSrc then
while dst_idx <= reconcile_requests.Size and reconcile_requests.Data[dst_idx].ColumnOldIdx ~= -1 do
dst_idx = dst_idx + 1
end
if dst_idx > reconcile_requests.Size then
break
end
local reconcile_data = reconcile_requests.Data[dst_idx]
local dst_column = dst_columns[reconcile_data.ColumnNewIdx]
IM_ASSERT(src_column.IsNeedReconcileSrc and dst_column.IsNeedReconcileDst)
src_column.IsNeedReconcileSrc = false; dst_column.IsNeedReconcileDst = false
reconcile_data.ColumnOldIdx = src_n
reconcile_data.ColumnOldData = TableColumnClone(src_column)
end
end
end
-- Apply in the final pass. Because it is possible that src_columns == table.Columns we went through a temporary copy.
for _, reconcile_data in reconcile_requests:iter() do
TableColumnCopy(table_.Columns[reconcile_data.ColumnNewIdx], reconcile_data.ColumnOldData) -- When old column was not found clear anyway with default-constructed data (which will set IsJustCreated=true)
TableSetupColumnApply(table_, reconcile_data.ColumnNewIdx, reconcile_data.ID, reconcile_data.NameOffset, reconcile_data.Flags, reconcile_data.InitWidthOrWeight, reconcile_data.UserData)
end
ImGui.TableFixDisplayOrder(table_)
table_.IsSettingsDirty = true -- FIXME-RECONCILE: Necessary?
table_.IsReconcileMode = false
reconcile_requests:resize(0) -- GC-ed once in NewFrame()
end
-- [Public]
function ImGui.TableSetupScrollFreeze(columns, rows)
local g = GImGui
local table_ = g.CurrentTable
IM_ASSERT_USER_ERROR_RET(table_ ~= nil, "Call should only be done while in BeginTable() scope!")
IM_ASSERT(table_.IsLayoutLocked == false, "TableSetupColumn(): need to call before first row!")
IM_ASSERT(columns >= 0 and columns < IMGUI_TABLE_MAX_COLUMNS)
IM_ASSERT(rows >= 0 and rows < 128) -- Arbitrary limit
table_.FreezeColumnsRequest = (band(table_.Flags, ImGuiTableFlags.ScrollX) ~= 0) and ImMin(columns, table_.ColumnsCount) or 0
table_.FreezeColumnsCount = (table_.InnerWindow.Scroll.x ~= 0.0) and table_.FreezeColumnsRequest or 0
table_.FreezeRowsRequest = (band(table_.Flags, ImGuiTableFlags.ScrollY) ~= 0) and rows or 0
table_.FreezeRowsCount = (table_.InnerWindow.Scroll.y ~= 0.0) and table_.FreezeRowsRequest or 0
table_.IsUnfrozenRows = (table_.FreezeRowsCount == 0) -- Make sure this is set before TableUpdateLayout() so ImGuiListClipper can benefit from it.
end
-------------------------------------------------------------------------------
-- [SECTION] Tables: Simple accessors
-------------------------------------------------------------------------------
-- - TableGetColumnCount()
-- - TableGetColumnName()
-- - TableGetColumnName() [Internal]
-- - TableSetColumnEnabled()
-- - TableGetColumnFlags()
-- - TableGetCellBgRect() [Internal]
-- - TableGetColumnResizeID() [Internal]
-- - TableGetHoveredColumn() [Internal]
-- - TableGetHoveredRow() [Internal]
-- - TableSetBgColor()
-------------------------------------------------------------------------------
-- [Internal] Small optimization to avoid calls to PopClipRect/SetCurrentChannel/PushClipRect in sequences,
-- they would meddle many times with the underlying ImDrawCmd.
-- Instead, we do a preemptive overwrite of clipping rectangle _without_ altering the command-buffer and let
-- the subsequent single call to SetCurrentChannel() does it things once.
function ImGui.SetWindowClipRectBeforeSetChannel(window, clip_rect)
local clip_rect_vec4 = clip_rect:ToVec4()
ImRect_Copy(window.ClipRect, clip_rect)
ImVec4_Copy(window.DrawList._CmdHeader.ClipRect, clip_rect_vec4)
window.DrawList._ClipRectStack.Data[window.DrawList._ClipRectStack.Size] = clip_rect_vec4
end
function ImGui.TableGetColumnCount()
local g = GImGui
local table_ = g.CurrentTable
return table_ and table_.ColumnsCount or 0
end
-- Overloads: TableGetColumnName(column_n?) / TableGetColumnName(table, column_n)
function ImGui.TableGetColumnName(a, b)
local table_, column_n
if type(a) == "table" then
table_, column_n = a, b
else
local g = GImGui
table_ = g.CurrentTable
if not table_ then
return nil
end
column_n = a or -1
if column_n < 0 then
column_n = table_.CurrentColumn
end
end
if table_.IsLayoutLocked == false and column_n >= table_.DeclColumnsCount then
return "" -- NameOffset is invalid at this point
end
local column = table_.Columns[column_n]
if column.NameOffset == -1 then
return ""
end
return table_.ColumnsNames[column.NameOffset + 1]
end
-- Change user accessible enabled/disabled state of a column (often perceived as "showing/hiding" from users point of view)
-- Note that end-user can use the context menu to change this themselves (right-click in headers, or right-click in columns body with ImGuiTableFlags.ContextMenuInBody)
-- - Require table to have the ImGuiTableFlags.Hideable flag because we are manipulating user accessible state.
-- - Request will be applied during next layout, which happens on the first call to TableNextRow() after BeginTable().
-- - For the getter you can test (TableGetColumnFlags() & ImGuiTableColumnFlags.IsEnabled) ~= 0.
-- - Alternative: the ImGuiTableColumnFlags.Disabled is an overriding/master disable flag which will also hide the column from context menu.
function ImGui.TableSetColumnEnabled(column_n, enabled)
local g = GImGui
local table_ = g.CurrentTable
IM_ASSERT_USER_ERROR_RET(table_ ~= nil, "Call should only be done while in BeginTable() scope!")
IM_ASSERT(band(table_.Flags, ImGuiTableFlags.Hideable) ~= 0) -- See comments above
if column_n < 0 then
column_n = table_.CurrentColumn
end
IM_ASSERT(column_n >= 0 and column_n < table_.ColumnsCount)
local column = table_.Columns[column_n]
column.IsUserEnabledNextFrame = enabled
end
-- We allow querying for an extra column in order to poll the IsHovered state of the right-most section
function ImGui.TableGetColumnFlags(column_n)
if column_n == nil then column_n = -1 end
local g = GImGui
local table_ = g.CurrentTable
if not table_ then
return ImGuiTableColumnFlags.None
end
if column_n < 0 then
column_n = table_.CurrentColumn
end
if column_n == table_.ColumnsCount then
return (table_.HoveredColumnBody == column_n) and ImGuiTableColumnFlags.IsHovered or ImGuiTableColumnFlags.None
end
return table_.Columns[column_n].Flags
end
-- Return the cell rectangle based on currently known height.
-- - Important: we generally don't know our row height until the end of the row, so Max.y will be incorrect in many situations.
-- The only case where this is correct is if we provided a min_row_height to TableNextRow() and don't go below it, or in TableEndRow() when we locked that height.
-- - Important: if ImGuiTableFlags.PadOuterX is set but ImGuiTableFlags.PadInnerX is not set, the outer-most left and right
-- columns report a small offset so their CellBgRect can extend up to the outer border.
-- FIXME: But the rendering code in TableEndRow() nullifies that with clamping required for scrolling.
function ImGui.TableGetCellBgRect(table_, column_n)
local column = table_.Columns[column_n]
local x1 = column.MinX
local x2 = column.MaxX
--if column.PrevEnabledColumn == -1 then
-- x1 = x1 - table.OuterPaddingX
--if column.NextEnabledColumn == -1 then
-- x2 = x2 + table.OuterPaddingX
x1 = ImMax(x1, table_.WorkRect.Min.x)
x2 = ImMin(x2, table_.WorkRect.Max.x)
return ImRect(x1, table_.RowPosY1, x2, table_.RowPosY2)
end
-- Return the resizing ID for the right-side of the given column.
function ImGui.TableGetColumnResizeID(table_, column_n, instance_no)
if instance_no == nil then instance_no = 0 end
IM_ASSERT(column_n >= 0 and column_n < table_.ColumnsCount)
local instance_id = ImGui.TableGetInstanceID(table_, instance_no)
return band(instance_id + 1 + column_n, 0xFFFFFFFF) -- FIXME: #6140: still not ideal
end
-- Return -1 when table is not hovered. return columns_count if hovering the unused space at the right of the right-most visible column.
function ImGui.TableGetHoveredColumn()
local g = GImGui
local table_ = g.CurrentTable
if not table_ then
return -1
end
return table_.HoveredColumnBody
end
-- Return -1 when table is not hovered. Return maxrow+1 if in table but below last submitted row.
-- *IMPORTANT* Unlike TableGetHoveredColumn(), this has a one frame latency in updating the value.
-- This difference with is the reason why this is not public yet.
function ImGui.TableGetHoveredRow()
local g = GImGui
local table_ = g.CurrentTable
if not table_ then
return -1
end
local table_instance = ImGui.TableGetInstanceData(table_, table_.InstanceCurrent)
return table_instance.HoveredRowLast
end
function ImGui.TableSetBgColor(target, color, column_n)
if column_n == nil then column_n = -1 end
local g = GImGui
local table_ = g.CurrentTable
IM_ASSERT_USER_ERROR_RET(table_ ~= nil, "Call should only be done while in BeginTable() scope!")
IM_ASSERT(target ~= ImGuiTableBgTarget.None)
if color == IM_COL32_DISABLE then
color = 0
end
-- We cannot draw neither the cell or row background immediately as we don't know the row height at this point in time.
if target == ImGuiTableBgTarget.CellBg then
if table_.RowPosY1 > table_.InnerClipRect.Max.y then -- Discard
return
end
if column_n == -1 then
column_n = table_.CurrentColumn
end
if not BitTest(table_.VisibleMaskByIndex, column_n) then
return
end
if table_.RowCellDataCurrent < 0 or table_.RowCellData[table_.RowCellDataCurrent].Column ~= column_n then
table_.RowCellDataCurrent = table_.RowCellDataCurrent + 1
end
local cell_data = table_.RowCellData[table_.RowCellDataCurrent]
cell_data.BgColor = color
cell_data.Column = column_n
elseif target == ImGuiTableBgTarget.RowBg0 or target == ImGuiTableBgTarget.RowBg1 then
if table_.RowPosY1 > table_.InnerClipRect.Max.y then -- Discard
return
end
IM_ASSERT(column_n == -1)
local bg_idx = (target == ImGuiTableBgTarget.RowBg1) and 1 or 0
table_.RowBgColor[bg_idx] = color
else
IM_ASSERT(false)
end
end
-------------------------------------------------------------------------------
-- [SECTION] Tables: Row changes
-------------------------------------------------------------------------------
-- - TableGetRowIndex()
-- - TableNextRow()
-- - TableBeginRow() [Internal]
-- - TableEndRow() [Internal]
-------------------------------------------------------------------------------
-- [Public] Note: for row coloring we use .RowBgColorCounter which is the same value without counting header rows
function ImGui.TableGetRowIndex()
local g = GImGui
local table_ = g.CurrentTable
if not table_ then
return 0
end
return table_.CurrentRow
end
-- [Public] Starts into the first cell of a new row
--- @param row_flags? ImGuiTableRowFlags
--- @param row_min_height? float
function ImGui.TableNextRow(row_flags, row_min_height)
if row_flags == nil then row_flags = 0 end
if row_min_height == nil then row_min_height = 0.0 end
local g = GImGui
local table_ = g.CurrentTable
if not table_.IsLayoutLocked then
ImGui.TableUpdateLayout(table_)
end
if table_.IsInsideRow then
ImGui.TableEndRow(table_)
end
table_.LastRowFlags = table_.RowFlags
table_.RowFlags = row_flags
table_.RowCellPaddingY = g.Style.CellPadding.y
table_.RowMinHeight = row_min_height
ImGui.TableBeginRow(table_)
-- We honor min_row_height requested by user, but cannot guarantee per-row maximum height,
-- because that would essentially require a unique clipping rectangle per-cell.
table_.RowPosY2 = table_.RowPosY2 + table_.RowCellPaddingY * 2.0
table_.RowPosY2 = ImMax(table_.RowPosY2, table_.RowPosY1 + row_min_height)
-- Disable output until user calls TableNextColumn()
table_.InnerWindow.SkipItems = true
end
-- [Internal] Only called by TableNextRow()
function ImGui.TableBeginRow(table_)
local window = table_.InnerWindow
IM_ASSERT(not table_.IsInsideRow)
-- New row
table_.CurrentRow = table_.CurrentRow + 1
table_.CurrentColumn = -1
table_.RowBgColor[0] = IM_COL32_DISABLE; table_.RowBgColor[1] = IM_COL32_DISABLE
table_.RowCellDataCurrent = -1
table_.IsInsideRow = true
-- Begin frozen rows
local next_y1 = table_.RowPosY2
if table_.CurrentRow == 0 and table_.FreezeRowsCount > 0 then
next_y1 = table_.OuterRect.Min.y
window.DC.CursorPos.y = next_y1
end
table_.RowPosY1 = next_y1; table_.RowPosY2 = next_y1
table_.RowTextBaseline = 0.0
table_.RowIndentOffsetX = window.DC.Indent.x - table_.HostIndentX -- Lock indent
window.DC.PrevLineTextBaseOffset = 0.0
window.DC.CursorPosPrevLine.x = window.DC.CursorPos.x
window.DC.CursorPosPrevLine.y = window.DC.CursorPos.y + table_.RowCellPaddingY -- This allows users to call SameLine() to share LineSize between columns.
window.DC.PrevLineSize.x = 0.0; window.DC.PrevLineSize.y = 0.0 -- This allows users to call SameLine() to share LineSize between columns, and to call it from first column too.
window.DC.CurrLineSize.x = 0.0; window.DC.CurrLineSize.y = 0.0
window.DC.IsSameLine = false; window.DC.IsSetPos = false
window.DC.CursorMaxPos.y = next_y1
-- Making the header BG color non-transparent will allow us to overlay it multiple times when handling smooth dragging.
if band(table_.RowFlags, ImGuiTableRowFlags.Headers) ~= 0 then
ImGui.TableSetBgColor(ImGuiTableBgTarget.RowBg0, ImGui.GetColorU32(ImGuiCol.TableHeaderBg))
if table_.CurrentRow == 0 then
table_.IsUsingHeaders = true
end
end
end
-- [Internal] Called by TableNextRow()
function ImGui.TableEndRow(table_)
local g = GImGui
local window = g.CurrentWindow
IM_ASSERT(window == table_.InnerWindow)
IM_ASSERT(table_.IsInsideRow)
if table_.CurrentColumn ~= -1 then
ImGui.TableEndCell(table_)
table_.CurrentColumn = -1
end
-- Logging
if g.LogEnabled then
ImGui.LogRenderedText(nil, "|")
end
-- Position cursor at the bottom of our row so it can be used for e.g. clipping calculation. However it is
-- likely that the next call to TableBeginCell() will reposition the cursor to take account of vertical padding.
window.DC.CursorPos.y = table_.RowPosY2
-- Row background fill
local bg_y1 = table_.RowPosY1
local bg_y2 = table_.RowPosY2
local unfreeze_rows_actual = (table_.CurrentRow + 1 == table_.FreezeRowsCount)
local unfreeze_rows_request = (table_.CurrentRow + 1 == table_.FreezeRowsRequest)
local table_instance = ImGui.TableGetInstanceData(table_, table_.InstanceCurrent)
if band(table_.RowFlags, ImGuiTableRowFlags.Headers) ~= 0 and (table_.CurrentRow == 0 or band(table_.LastRowFlags, ImGuiTableRowFlags.Headers) ~= 0) then
table_instance.LastTopHeadersRowHeight = table_instance.LastTopHeadersRowHeight + bg_y2 - bg_y1
end
local is_visible = (bg_y2 >= table_.InnerClipRect.Min.y and bg_y1 <= table_.InnerClipRect.Max.y)
if is_visible then
-- Update data for TableGetHoveredRow()
if table_.HoveredColumnBody ~= -1 and g.IO.MousePos.y >= bg_y1 and g.IO.MousePos.y < bg_y2 and table_instance.HoveredRowNext < 0 then
table_instance.HoveredRowNext = table_.CurrentRow
end
-- Decide of background color for the row
local bg_col0 = 0
local bg_col1 = 0
if table_.RowBgColor[0] ~= IM_COL32_DISABLE then
bg_col0 = table_.RowBgColor[0]
elseif band(table_.Flags, ImGuiTableFlags.RowBg) ~= 0 then
bg_col0 = ImGui.GetColorU32((band(table_.RowBgColorCounter, 1) ~= 0) and ImGuiCol.TableRowBgAlt or ImGuiCol.TableRowBg)
end
if table_.RowBgColor[1] ~= IM_COL32_DISABLE then
bg_col1 = table_.RowBgColor[1]
end
-- Decide of top border color
local top_border_col = 0
local border_size = TABLE_BORDER_SIZE
if table_.CurrentRow > 0 and band(table_.Flags, ImGuiTableFlags.BordersInnerH) ~= 0 then
top_border_col = (band(table_.LastRowFlags, ImGuiTableRowFlags.Headers) ~= 0) and table_.BorderColorStrong or table_.BorderColorLight
end
local draw_cell_bg_color = table_.RowCellDataCurrent >= 0
local draw_strong_bottom_border = unfreeze_rows_actual
if bor(bg_col0, bg_col1, top_border_col) ~= 0 or draw_strong_bottom_border or draw_cell_bg_color then
-- In theory we could call SetWindowClipRectBeforeSetChannel() but since we know TableEndRow() is
-- always followed by a change of clipping rectangle we perform the smallest overwrite possible here.
if band(table_.Flags, ImGuiTableFlags.NoClip) == 0 then
ImVec4_Copy(window.DrawList._CmdHeader.ClipRect, table_.Bg0ClipRectForDrawCmd:ToVec4())
end
table_.DrawSplitter:SetCurrentChannel(window.DrawList, TABLE_DRAW_CHANNEL_BG0)
end
-- Draw row background
-- We soft/cpu clip this so all backgrounds and borders can share the same clipping rectangle
if bg_col0 ~= 0 or bg_col1 ~= 0 then
local row_rect = ImRect(table_.WorkRect.Min.x, bg_y1, table_.WorkRect.Max.x, bg_y2)
row_rect:ClipWith(table_.BgClipRect)
if bg_col0 ~= 0 and row_rect.Min.y < row_rect.Max.y then
window.DrawList:AddRectFilled(row_rect.Min, row_rect.Max, bg_col0)
end
if bg_col1 ~= 0 and row_rect.Min.y < row_rect.Max.y then
window.DrawList:AddRectFilled(row_rect.Min, row_rect.Max, bg_col1)
end
end
-- Draw cell background color
if draw_cell_bg_color then
for cell_n = 0, table_.RowCellDataCurrent do
local cell_data = table_.RowCellData[cell_n]
-- As we render the BG here we need to clip things (for layout we would not)
-- FIXME: This cancels the OuterPadding addition done by TableGetCellBgRect(), need to keep it while rendering correctly while scrolling.
local column = table_.Columns[cell_data.Column]
local cell_bg_rect = ImGui.TableGetCellBgRect(table_, cell_data.Column)
cell_bg_rect:ClipWith(table_.BgClipRect)
cell_bg_rect.Min.x = ImMax(cell_bg_rect.Min.x, column.ClipRect.Min.x) -- So that first column after frozen one gets clipped when scrolling
cell_bg_rect.Max.x = ImMin(cell_bg_rect.Max.x, column.MaxX)
if cell_bg_rect.Min.y < cell_bg_rect.Max.y then
window.DrawList:AddRectFilled(cell_bg_rect.Min, cell_bg_rect.Max, cell_data.BgColor)
end
end
end
-- Draw top border
if top_border_col ~= 0 and bg_y1 >= table_.BgClipRect.Min.y and bg_y1 < table_.BgClipRect.Max.y then
window.DrawList:AddLineH(table_.BorderX1, table_.BorderX2, bg_y1, top_border_col, border_size)
end
-- Draw bottom border at the row unfreezing mark (always strong)
if draw_strong_bottom_border and bg_y2 >= table_.BgClipRect.Min.y and bg_y2 < table_.BgClipRect.Max.y then
window.DrawList:AddLineH(table_.BorderX1, table_.BorderX2, bg_y2, table_.BorderColorStrong, border_size)
end
end
-- End frozen rows (when we are past the last frozen row line, teleport cursor and alter clipping rectangle)
-- - We need to do that in TableEndRow() instead of TableBeginRow() so the list clipper can mark
-- end of row and get the new cursor position.
if unfreeze_rows_request then
IM_ASSERT(table_.FreezeRowsRequest > 0)
for column_n = 0, table_.ColumnsCount - 1 do
table_.Columns[column_n].NavLayerCurrent = table_.NavLayer
end
local y0 = ImMax(table_.RowPosY2 + 1, table_.InnerClipRect.Min.y)
table_instance.LastFrozenHeight = y0 - table_.OuterRect.Min.y
if unfreeze_rows_actual then
IM_ASSERT(table_.IsUnfrozenRows == false)
table_.IsUnfrozenRows = true
-- BgClipRect starts as table.InnerClipRect, reduce it now and make BgClipRectForDrawCmd == BgClipRect
local min_y = ImMin(y0, table_.InnerClipRect.Max.y)
table_.BgClipRect.Min.y = min_y; table_.Bg2ClipRectForDrawCmd.Min.y = min_y
table_.BgClipRect.Max.y = table_.InnerClipRect.Max.y; table_.Bg2ClipRectForDrawCmd.Max.y = table_.InnerClipRect.Max.y
table_.Bg2DrawChannelCurrent = table_.Bg2DrawChannelUnfrozen
IM_ASSERT(table_.Bg2ClipRectForDrawCmd.Min.y <= table_.Bg2ClipRectForDrawCmd.Max.y)
local row_height = table_.RowPosY2 - table_.RowPosY1
table_.RowPosY2 = table_.WorkRect.Min.y + table_.RowPosY2 - table_.OuterRect.Min.y
window.DC.CursorPos.y = table_.RowPosY2
table_.RowPosY1 = table_.RowPosY2 - row_height
for column_n = 0, table_.ColumnsCount - 1 do
local column = table_.Columns[column_n]
column.DrawChannelCurrent = column.DrawChannelUnfrozen
column.ClipRect.Min.y = table_.Bg2ClipRectForDrawCmd.Min.y
end
-- Update cliprect ahead of TableBeginCell() so clipper can access to new ClipRect.Min.y
ImGui.SetWindowClipRectBeforeSetChannel(window, table_.Columns[0].ClipRect)
table_.DrawSplitter:SetCurrentChannel(window.DrawList, table_.Columns[0].DrawChannelCurrent)
end
end
if band(table_.RowFlags, ImGuiTableRowFlags.Headers) == 0 then
table_.RowBgColorCounter = table_.RowBgColorCounter + 1
end
table_.IsInsideRow = false
end
-------------------------------------------------------------------------------
-- [SECTION] Tables: Columns changes
-------------------------------------------------------------------------------
-- - TableGetColumnIndex()
-- - TableSetColumnIndex()
-- - TableNextColumn()
-- - TableBeginCell() [Internal]
-- - TableEndCell() [Internal]
-------------------------------------------------------------------------------
function ImGui.TableGetColumnIndex()
local g = GImGui
local table_ = g.CurrentTable
if not table_ then
return 0
end
return table_.CurrentColumn
end
-- [Public] Append into a specific column
function ImGui.TableSetColumnIndex(column_n)
local g = GImGui
local table_ = g.CurrentTable
if not table_ then
return false
end
if table_.CurrentColumn ~= column_n then
if table_.CurrentColumn ~= -1 then
ImGui.TableEndCell(table_)
end
if not (column_n >= 0 and column_n < table_.ColumnsCount) then
IM_ASSERT_USER_ERROR(false, "TableSetColumnIndex() invalid column index!")
return false
end
ImGui.TableBeginCell(table_, column_n)
end
-- Return whether the column is visible. User may choose to skip submitting items based on this return value,
-- however they shouldn't skip submitting for columns that may have the tallest contribution to row height.
return table_.Columns[column_n].IsRequestOutput
end
-- [Public] Append into the next column, wrap and create a new row when already on last column
function ImGui.TableNextColumn()
local g = GImGui
local table_ = g.CurrentTable
if not table_ then
return false
end
if table_.IsInsideRow and table_.CurrentColumn + 1 < table_.ColumnsCount then
if table_.CurrentColumn ~= -1 then
ImGui.TableEndCell(table_)
end
ImGui.TableBeginCell(table_, table_.CurrentColumn + 1)
else
ImGui.TableNextRow()
ImGui.TableBeginCell(table_, 0)
end
-- Return whether the column is visible. User may choose to skip submitting items based on this return value,
-- however they shouldn't skip submitting for columns that may have the tallest contribution to row height.
return table_.Columns[table_.CurrentColumn].IsRequestOutput
end
-- [Internal] Called by TableSetColumnIndex()/TableNextColumn()
-- This is called very frequently, so we need to be mindful of unnecessary overhead.
-- FIXME-TABLE FIXME-OPT: Could probably shortcut some things for non-active or clipped columns.
function ImGui.TableBeginCell(table_, column_n)
local g = GImGui
local column = table_.Columns[column_n]
local window = table_.InnerWindow
table_.CurrentColumn = column_n
-- Start position is roughly ~~ CellRect.Min + CellPadding + Indent
local start_x = column.WorkMinX
if band(column.Flags, ImGuiTableColumnFlags.IndentEnable) ~= 0 then
start_x = start_x + table_.RowIndentOffsetX -- ~~ += window.DC.Indent.x - table.HostIndentX, except we locked it for the row.
end
window.DC.CursorPos.x = start_x
window.DC.CursorPos.y = table_.RowPosY1 + table_.RowCellPaddingY
window.DC.CursorMaxPos.x = window.DC.CursorPos.x
window.DC.ColumnsOffset.x = start_x - window.Pos.x - window.DC.Indent.x -- FIXME-WORKRECT
window.DC.CursorPosPrevLine.x = window.DC.CursorPos.x -- PrevLine.y is preserved. This allows users to call SameLine() to share LineSize between columns.
window.DC.CurrLineTextBaseOffset = table_.RowTextBaseline
window.DC.NavLayerCurrent = column.NavLayerCurrent
-- Note how WorkRect.Max.y is only set once during layout
window.WorkRect.Min.y = window.DC.CursorPos.y
window.WorkRect.Min.x = column.WorkMinX
window.WorkRect.Max.x = column.WorkMaxX
window.DC.ItemWidth = column.ItemWidth
window.SkipItems = column.IsSkipItems
if column.IsSkipItems then
g.LastItemData.ID = 0
g.LastItemData.StatusFlags = 0
end
-- Also see TablePushColumnChannel()
if band(table_.Flags, ImGuiTableFlags.NoClip) ~= 0 then
-- FIXME: if we end up drawing all borders/bg in EndTable, could remove this and just assert that channel hasn't changed.
table_.DrawSplitter:SetCurrentChannel(window.DrawList, TABLE_DRAW_CHANNEL_NOCLIP)
--IM_ASSERT(table.DrawSplitter._Current == TABLE_DRAW_CHANNEL_NOCLIP)
else
-- FIXME-TABLE: Could avoid this if draw channel is dummy channel?
ImGui.SetWindowClipRectBeforeSetChannel(window, column.ClipRect)
table_.DrawSplitter:SetCurrentChannel(window.DrawList, column.DrawChannelCurrent)
end
-- Logging
if g.LogEnabled and not column.IsSkipItems then
ImGui.LogRenderedText(window.DC.CursorPos, "|")
g.LogLinePosY = FLT_MAX
end
end
-- [Internal] Called by TableNextRow()/TableSetColumnIndex()/TableNextColumn()
function ImGui.TableEndCell(table_)
local column = table_.Columns[table_.CurrentColumn]
local window = table_.InnerWindow
if window.DC.IsSetPos then
ImGui.ErrorCheckUsingSetCursorPosToExtendParentBoundaries()
end
-- Report maximum position so we can infer content size per column.
local x = window.DC.CursorMaxPos.x
if band(table_.RowFlags, ImGuiTableRowFlags.Headers) ~= 0 then
column.ContentMaxXHeadersUsed = ImMax(column.ContentMaxXHeadersUsed, x) -- Useful in case user submit contents in header row that is not a TableHeader() call
elseif table_.IsUnfrozenRows then
column.ContentMaxXUnfrozen = ImMax(column.ContentMaxXUnfrozen, x)
else
column.ContentMaxXFrozen = ImMax(column.ContentMaxXFrozen, x)
end
if column.IsEnabled then
table_.RowPosY2 = ImMax(table_.RowPosY2, window.DC.CursorMaxPos.y + table_.RowCellPaddingY)
end
column.ItemWidth = window.DC.ItemWidth
-- Propagate text baseline for the entire row
-- FIXME-TABLE: Here we propagate text baseline from the last line of the cell.. instead of the first one.
table_.RowTextBaseline = ImMax(table_.RowTextBaseline, window.DC.PrevLineTextBaseOffset)
end
-------------------------------------------------------------------------------
-- [SECTION] Tables: Columns width management
-------------------------------------------------------------------------------
-- - TableGetMaxColumnWidth() [Internal]
-- - TableGetColumnWidthAuto() [Internal]
-- - TableSetColumnWidth()
-- - TableSetColumnWidthAutoSingle() [Internal]
-- - TableSetColumnWidthAutoAll() [Internal]
-- - TableUpdateColumnsWeightFromWidth() [Internal]
-------------------------------------------------------------------------------
-- Note that actual columns widths are computed in TableUpdateLayout().
-------------------------------------------------------------------------------
-- Maximum column content width given current layout. Use column.MinX so this value differs on a per-column basis.
function ImGui.TableCalcMaxColumnWidth(table_, column_n)
local column = table_.Columns[column_n]
local max_width = FLT_MAX
local min_column_distance = table_.MinColumnWidth + table_.CellPaddingX * 2.0 + table_.CellSpacingX1 + table_.CellSpacingX2
if band(table_.Flags, ImGuiTableFlags.ScrollX) ~= 0 then
-- Frozen columns can't reach beyond visible width else scrolling will naturally break.
-- (we use DisplayOrder as within a set of multiple frozen column reordering is possible)
if column.DisplayOrder < table_.FreezeColumnsRequest then
max_width = (table_.InnerClipRect.Max.x - (table_.FreezeColumnsRequest - column.DisplayOrder) * min_column_distance) - column.MinX
max_width = max_width - table_.OuterPaddingX - table_.CellPaddingX - table_.CellSpacingX2
end
elseif band(table_.Flags, ImGuiTableFlags.NoKeepColumnsVisible) == 0 then
-- If horizontal scrolling if disabled, we apply a final lossless shrinking of columns in order to make
-- sure they are all visible. Because of this we also know that all of the columns will always fit in
-- table.WorkRect and therefore in table.InnerRect (because ScrollX is off)
-- FIXME-TABLE: This is solved incorrectly but also quite a difficult problem to fix as we also want ClipRect width to match.
-- See "table_width_distrib" and "table_width_keep_visible" tests
max_width = table_.WorkRect.Max.x - (table_.ColumnsEnabledCount - column.IndexWithinEnabledSet - 1) * min_column_distance - column.MinX
--max_width = max_width - table.CellSpacingX1
max_width = max_width - table_.CellSpacingX2
max_width = max_width - table_.CellPaddingX * 2.0
max_width = max_width - table_.OuterPaddingX
end
return max_width
end
-- Note this is meant to be stored in column.WidthAuto, please generally use the WidthAuto field
function ImGui.TableGetColumnWidthAuto(table_, column)
local content_width_body = ImMax(column.ContentMaxXFrozen, column.ContentMaxXUnfrozen) - column.WorkMinX
local content_width_headers = column.ContentMaxXHeadersIdeal - column.WorkMinX
local width_auto = content_width_body
if band(column.Flags, ImGuiTableColumnFlags.NoHeaderWidth) == 0 then
width_auto = ImMax(width_auto, content_width_headers)
end
-- Non-resizable fixed columns preserve their requested width
if band(column.Flags, ImGuiTableColumnFlags.WidthFixed) ~= 0 and column.InitStretchWeightOrWidth > 0.0 then
if band(table_.Flags, ImGuiTableFlags.Resizable) == 0 or band(column.Flags, ImGuiTableColumnFlags.NoResize) ~= 0 then
width_auto = column.InitStretchWeightOrWidth
end
end
return ImMax(width_auto, table_.MinColumnWidth)
end
-- 'width' = inner column width, without padding
function ImGui.TableSetColumnWidth(column_n, width)
local g = GImGui
local table_ = g.CurrentTable
IM_ASSERT(table_ ~= nil and table_.IsLayoutLocked == false)
IM_ASSERT(column_n >= 0 and column_n < table_.ColumnsCount)
local column_0 = table_.Columns[column_n]
local column_0_width = width
-- Apply constraints early
-- Compare both requested and actual given width to avoid overwriting requested width when column is stuck (minimum size, bounded)
IM_ASSERT(table_.MinColumnWidth > 0.0)
local min_width = table_.MinColumnWidth
local max_width = ImMax(min_width, column_0.WidthMax) -- Don't use TableCalcMaxColumnWidth() here as it would rely on MinX from last instance (#7933)
column_0_width = ImClamp(column_0_width, min_width, max_width)
if column_0.WidthGiven == column_0_width or column_0.WidthRequest == column_0_width then
return
end
--IMGUI_DEBUG_PRINT("TableSetColumnWidth(%d, %.1f->%.1f)\n", column_0_idx, column_0.WidthGiven, column_0_width)
local column_1 = (column_0.NextEnabledColumn ~= -1) and table_.Columns[column_0.NextEnabledColumn] or nil
-- In this surprisingly not simple because of how we support mixing Fixed and multiple Stretch columns.
-- - All fixed: easy.
-- - All stretch: easy.
-- - One or more fixed + one stretch: easy.
-- - One or more fixed + more than one stretch: tricky.
-- Qt when manual resize is enabled only supports a single _trailing_ stretch column, we support more cases here.
-- When forwarding resize from Wn| to Fn+1| we need to be considerate of the _NoResize flag on Fn+1.
-- FIXME-TABLE: Find a way to rewrite all of this so interactions feel more consistent for the user.
-- Scenarios:
-- - F1 F2 F3 resize from F1| or F2| --> ok: alter .WidthRequested of Fixed column. Subsequent columns will be offset.
-- - F1 F2 F3 resize from F3| --> ok: alter .WidthRequested of Fixed column. If active, ScrollX extent can be altered.
-- - F1 F2 W3 resize from F1| or F2| --> ok: alter .WidthRequested of Fixed column. If active, ScrollX extent can be altered, but it doesn't make much sense as the Stretch column will always be minimal size.
-- - F1 F2 W3 resize from W3| --> ok: no-op (disabled by Resize Rule 1)
-- - W1 W2 W3 resize from W1| or W2| --> ok
-- - W1 W2 W3 resize from W3| --> ok: no-op (disabled by Resize Rule 1)
-- - W1 F2 F3 resize from F3| --> ok: no-op (disabled by Resize Rule 1)
-- - W1 F2 resize from F2| --> ok: no-op (disabled by Resize Rule 1)
-- - W1 W2 F3 resize from W1| or W2| --> ok
-- - W1 F2 W3 resize from W1| or F2| --> ok
-- - F1 W2 F3 resize from W2| --> ok
-- - F1 W3 F2 resize from W3| --> ok
-- - W1 F2 F3 resize from W1| --> ok: equivalent to resizing |F2. F3 will not move.
-- - W1 F2 F3 resize from F2| --> ok
-- All resizes from a Wx columns are locking other columns.
-- Possible improvements:
-- - W1 W2 W3 resize W1| --> to not be stuck, both W2 and W3 would stretch down. Seems possible to fix. Would be most beneficial to simplify resize of all-weighted columns.
-- - W3 F1 F2 resize W3| --> to not be stuck past F1|, both F1 and F2 would need to stretch down, which would be lossy or ambiguous. Seems hard to fix.
-- [Resize Rule 1] Can't resize from right of right-most visible column if there is any Stretch column. Implemented in TableUpdateLayout().
-- If we have all Fixed columns OR resizing a Fixed column that doesn't come after a Stretch one, we can do an offsetting resize.
-- This is the preferred resize path
if band(column_0.Flags, ImGuiTableColumnFlags.WidthFixed) ~= 0 then
if not column_1 or table_.LeftMostStretchedColumn == -1 or table_.Columns[table_.LeftMostStretchedColumn].DisplayOrder >= column_0.DisplayOrder then
column_0.WidthRequest = column_0_width
table_.IsSettingsDirty = true
return
end
end
-- We can also use previous column if there's no next one (this is used when doing an auto-fit on the right-most stretch column)
if column_1 == nil then
column_1 = (column_0.PrevEnabledColumn ~= -1) and table_.Columns[column_0.PrevEnabledColumn] or nil
end
if column_1 == nil then
return
end
-- Resizing from right-side of a Stretch column before a Fixed column forward sizing to left-side of fixed column.
-- (old_a + old_b == new_a + new_b) --> (new_a == old_a + old_b - new_b)
local column_1_width = ImMax(column_1.WidthRequest - (column_0_width - column_0.WidthRequest), min_width)
column_0_width = column_0.WidthRequest + column_1.WidthRequest - column_1_width
IM_ASSERT(column_0_width > 0.0 and column_1_width > 0.0)
column_0.WidthRequest = column_0_width
column_1.WidthRequest = column_1_width
if band(bor(column_0.Flags, column_1.Flags), ImGuiTableColumnFlags.WidthStretch) ~= 0 then
ImGui.TableUpdateColumnsWeightFromWidth(table_)
end
table_.IsSettingsDirty = true
end
-- Disable clipping then auto-fit, will take 2 frames
-- (we don't take a shortcut for unclipped columns to reduce inconsistencies when e.g. resizing multiple columns)
function ImGui.TableSetColumnWidthAutoSingle(table_, column_n)
-- Single auto width uses auto-fit
local column = table_.Columns[column_n]
if not column.IsEnabled then
return
end
column.CannotSkipItemsQueue = lshift(1, 0)
table_.AutoFitSingleColumn = column_n
end
function ImGui.TableSetColumnWidthAutoAll(table_)
for column_n = 0, table_.ColumnsCount - 1 do
local column = table_.Columns[column_n]
if column.IsEnabled or band(column.Flags, ImGuiTableColumnFlags.WidthStretch) ~= 0 then -- Cannot reset weight of hidden stretch column
column.CannotSkipItemsQueue = lshift(1, 0)
column.AutoFitQueue = lshift(1, 1)
end
end
end
function ImGui.TableUpdateColumnsWeightFromWidth(table_)
IM_ASSERT(table_.LeftMostStretchedColumn ~= -1 and table_.RightMostStretchedColumn ~= -1)
-- Measure existing quantities
local visible_weight = 0.0
local visible_width = 0.0
for column_n = 0, table_.ColumnsCount - 1 do
local column = table_.Columns[column_n]
if column.IsEnabled and band(column.Flags, ImGuiTableColumnFlags.WidthStretch) ~= 0 then
IM_ASSERT(column.StretchWeight > 0.0)
visible_weight = visible_weight + column.StretchWeight
visible_width = visible_width + column.WidthRequest
end
end
IM_ASSERT(visible_weight > 0.0 and visible_width > 0.0)
-- Apply new weights
for column_n = 0, table_.ColumnsCount - 1 do
local column = table_.Columns[column_n]
if column.IsEnabled and band(column.Flags, ImGuiTableColumnFlags.WidthStretch) ~= 0 then
column.StretchWeight = (column.WidthRequest / visible_width) * visible_weight
IM_ASSERT(column.StretchWeight > 0.0)
end
end
end
-------------------------------------------------------------------------------
-- [SECTION] Tables: Drawing
-------------------------------------------------------------------------------
-- - TablePushBackgroundChannel() [Internal]
-- - TablePopBackgroundChannel() [Internal]
-- - TableSetupDrawChannels() [Internal]
-- - TableMergeDrawChannels() [Internal]
-- - TableGetColumnBorderCol() [Internal]
-- - TableDrawBorders() [Internal]
-------------------------------------------------------------------------------
-- FIXME: This could be abstracted and merged with PushColumnsBackground(), by creating a generic struct
-- with storage for backup cliprect + backup channel + storage for splitter pointer, new clip rect.
-- This would slightly simplify caller code.
-- Bg2 is used by Selectable (and possibly other widgets) to render to the background.
-- Unlike our Bg0/1 channel which we uses for RowBg/CellBg/Borders and where we guarantee all shapes to be CPU-clipped, the Bg2 channel being widgets-facing will rely on regular ClipRect.
function ImGui.TablePushBackgroundChannel()
local g = GImGui
local window = g.CurrentWindow
local table_ = g.CurrentTable
-- Optimization: avoid SetCurrentChannel() + PushClipRect()
ImRect_Copy(table_.HostBackupInnerClipRect, window.ClipRect)
ImGui.SetWindowClipRectBeforeSetChannel(window, table_.Bg2ClipRectForDrawCmd)
table_.DrawSplitter:SetCurrentChannel(window.DrawList, table_.Bg2DrawChannelCurrent)
end
function ImGui.TablePopBackgroundChannel()
local g = GImGui
local window = g.CurrentWindow
local table_ = g.CurrentTable
-- Optimization: avoid PopClipRect() + SetCurrentChannel()
ImGui.SetWindowClipRectBeforeSetChannel(window, table_.HostBackupInnerClipRect)
table_.DrawSplitter:SetCurrentChannel(window.DrawList, table_.Columns[table_.CurrentColumn].DrawChannelCurrent)
end
-- Also see TableBeginCell()
function ImGui.TablePushColumnChannel(column_n)
local g = GImGui
local table_ = g.CurrentTable
-- Optimization: avoid SetCurrentChannel() + PushClipRect()
if band(table_.Flags, ImGuiTableFlags.NoClip) ~= 0 then
return
end
local window = g.CurrentWindow
local column = table_.Columns[column_n]
ImGui.SetWindowClipRectBeforeSetChannel(window, column.ClipRect)
table_.DrawSplitter:SetCurrentChannel(window.DrawList, column.DrawChannelCurrent)
end
function ImGui.TablePopColumnChannel()
local g = GImGui
local table_ = g.CurrentTable
-- Optimization: avoid PopClipRect() + SetCurrentChannel()
if band(table_.Flags, ImGuiTableFlags.NoClip) ~= 0 or (table_.CurrentColumn == -1) then -- Calling TreePop() after TableNextRow() is supported.
return
end
local window = g.CurrentWindow
local column = table_.Columns[table_.CurrentColumn]
ImGui.SetWindowClipRectBeforeSetChannel(window, column.ClipRect)
table_.DrawSplitter:SetCurrentChannel(window.DrawList, column.DrawChannelCurrent)
end
-- Allocate draw channels. Called by TableUpdateLayout()
-- - We allocate them following storage order instead of display order so reordering columns won't needlessly
-- increase overall dormant memory cost.
-- - We isolate headers draw commands in their own channels instead of just altering clip rects.
-- This is in order to facilitate merging of draw commands.
-- - After crossing FreezeRowsCount, all columns see their current draw channel changed to a second set of channels.
-- - We only use the dummy draw channel so we can push a null clipping rectangle into it without affecting other
-- channels, while simplifying per-row/per-cell overhead. It will be empty and discarded when merged.
-- - We allocate 1 or 2 background draw channels. This is because we know TablePushBackgroundChannel() is only used for
-- horizontal spanning. If we allowed vertical spanning we'd need one background draw channel per merge group (1-4).
-- Draw channel allocation (before merging):
-- - NoClip --> 2+D+1 channels: bg0/1 + bg2 + foreground (same clip rect == always 1 draw call)
-- - Clip --> 2+D+N channels
-- - FreezeRows --> 2+D+N*2 (unless scrolling value is zero)
-- - FreezeRows || FreezeColumns --> 3+D+N*2 (unless scrolling value is zero)
-- Where D is 1 if any column is clipped or hidden (dummy channel) otherwise 0.
function ImGui.TableSetupDrawChannels(table_)
local freeze_row_multiplier = (table_.FreezeRowsCount > 0) and 2 or 1
local channels_for_row = (band(table_.Flags, ImGuiTableFlags.NoClip) ~= 0) and 1 or table_.ColumnsEnabledCount
local channels_for_bg = 1 + 1 * freeze_row_multiplier
local masks_differ = false
for n = 0, table_.ColumnsCount - 1 do
if (table_.VisibleMaskByIndex[n] == true) ~= (table_.EnabledMaskByIndex[n] == true) then
masks_differ = true
break
end
end
local channels_for_dummy = (table_.ColumnsEnabledCount < table_.ColumnsCount or masks_differ) and 1 or 0
local channels_total = channels_for_bg + (channels_for_row * freeze_row_multiplier) + channels_for_dummy
table_.DrawSplitter:Split(table_.InnerWindow.DrawList, channels_total)
table_.DummyDrawChannel = (channels_for_dummy > 0) and (channels_total - 1) or -1
table_.Bg2DrawChannelCurrent = TABLE_DRAW_CHANNEL_BG2_FROZEN
table_.Bg2DrawChannelUnfrozen = (table_.FreezeRowsCount > 0) and (2 + channels_for_row) or TABLE_DRAW_CHANNEL_BG2_FROZEN
local draw_channel_current = 2
for column_n = 0, table_.ColumnsCount - 1 do
local column = table_.Columns[column_n]
if column.IsVisibleX and column.IsVisibleY then
column.DrawChannelFrozen = draw_channel_current
column.DrawChannelUnfrozen = draw_channel_current + ((table_.FreezeRowsCount > 0) and (channels_for_row + 1) or 0)
if band(table_.Flags, ImGuiTableFlags.NoClip) == 0 then
draw_channel_current = draw_channel_current + 1
end
else
column.DrawChannelFrozen = table_.DummyDrawChannel
column.DrawChannelUnfrozen = table_.DummyDrawChannel
end
column.DrawChannelCurrent = column.DrawChannelFrozen
end
-- Initial draw cmd starts with a BgClipRect that matches the one of its host, to facilitate merge draw commands by default.
-- All our cell highlight are manually clipped with BgClipRect. When unfreezing it will be made smaller to fit scrolling rect.
-- (This technically isn't part of setting up draw channels, but is reasonably related to be done here)
ImRect_Copy(table_.BgClipRect, table_.InnerClipRect)
ImRect_Copy(table_.Bg0ClipRectForDrawCmd, table_.OuterWindow.ClipRect)
ImRect_Copy(table_.Bg2ClipRectForDrawCmd, table_.HostClipRect)
IM_ASSERT(table_.BgClipRect.Min.y <= table_.BgClipRect.Max.y)
end
-- This function reorder draw channels based on matching clip rectangle, to facilitate merging them. Called by EndTable().
-- For simplicity we call it TableMergeDrawChannels() but in fact it only reorder channels + overwrite ClipRect,
-- actual merging is done by table.DrawSplitter:Merge() which is called right after TableMergeDrawChannels().
--
-- Columns where the contents didn't stray off their local clip rectangle can be merged. To achieve
-- this we merge their clip rect and make them contiguous in the channel list, so they can be merged
-- by the call to DrawSplitter:Merge() following to the call to this function.
-- We reorder draw commands by arranging them into a maximum of 4 distinct groups:
--
-- 1 group: 2 groups: 2 groups: 4 groups:
-- [ 0. ] no freeze [ 0. ] row freeze [ 01 ] col freeze [ 01 ] row+col freeze
-- [ .. ] or no scroll [ 2. ] and v-scroll [ .. ] and h-scroll [ 23 ] and v+h-scroll
--
-- Each column itself can use 1 channel (row freeze disabled) or 2 channels (row freeze enabled).
-- When the contents of a column didn't stray off its limit, we move its channels into the corresponding group
-- based on its position (within frozen rows/columns groups or not).
-- At the end of the operation our 1-4 groups will each have a ImDrawCmd using the same ClipRect.
-- This function assume that each column are pointing to a distinct draw channel,
-- otherwise merge_group.ChannelsCount will not match set bit count of merge_group.ChannelsMask.
--
-- Column channels will not be merged into one of the 1-4 groups in the following cases:
-- - The contents stray off its clipping rectangle (we only compare the MaxX value, not the MinX value).
-- Direct ImDrawList calls won't be taken into account by default, if you use them make sure the ImGui.* bounds
-- matches, by e.g. calling SetCursorScreenPos().
-- - The channel uses more than one draw command itself. We drop all our attempt at merging stuff here..
-- we could do better but it's going to be rare and probably not worth the hassle.
-- Columns for which the draw channel(s) haven't been merged with other will use their own ImDrawCmd.
--
-- This function is particularly tricky to understand.. take a breath.
-- (Lua: splitter channel n lives in splitter._Channels.Data[n + 1]; bit arrays are Lua sets)
function ImGui.TableMergeDrawChannels(table_)
local splitter = table_.DrawSplitter
local channels = splitter._Channels.Data
local has_freeze_v = (table_.FreezeRowsCount > 0)
local has_freeze_h = (table_.FreezeColumnsCount > 0)
IM_ASSERT(splitter._Current == 0)
-- Track which groups we are going to attempt to merge, and which channels goes into each group.
local merge_group_mask = 0x00
local merge_groups = {}
for n = 0, 3 do
merge_groups[n] = { ClipRect = ImRect(), ChannelsCount = 0, ChannelsMask = {} }
end
local max_draw_channels = (4 + table_.ColumnsCount * 2)
local remaining_mask = {}
-- 1. Scan channels and take note of those which can be merged
for column_n = 0, table_.ColumnsCount - 1 do
if BitTest(table_.VisibleMaskByIndex, column_n) then
local column = table_.Columns[column_n]
local merge_group_sub_count = has_freeze_v and 2 or 1
for merge_group_sub_n = 0, merge_group_sub_count - 1 do
repeat
local channel_no = (merge_group_sub_n == 0) and column.DrawChannelFrozen or column.DrawChannelUnfrozen
-- Don't attempt to merge if there are multiple draw calls within the column
local src_channel = channels[channel_no + 1]
local cmds = src_channel._CmdBuffer
if cmds.Size > 0 and cmds.Data[cmds.Size].ElemCount == 0 and cmds.Data[cmds.Size].UserCallback == nil then -- Equivalent of PopUnusedDrawCmd()
cmds:pop_back()
end
if cmds.Size ~= 1 then
break
end
-- Find out the width of this merge group and check if it will fit in our column
-- (note that we assume that rendering didn't stray on the left direction. we should need a CursorMinPos to detect it)
if band(column.Flags, ImGuiTableColumnFlags.NoClip) == 0 then
local content_max_x
if not has_freeze_v then
content_max_x = ImMax(column.ContentMaxXUnfrozen, column.ContentMaxXHeadersUsed) -- No row freeze
elseif merge_group_sub_n == 0 then
content_max_x = ImMax(column.ContentMaxXFrozen, column.ContentMaxXHeadersUsed) -- Row freeze: use width before freeze
else
content_max_x = column.ContentMaxXUnfrozen -- Row freeze: use width after freeze
end
if content_max_x > column.ClipRect.Max.x then
break
end
end
local merge_group_n = ((has_freeze_h and column_n < table_.FreezeColumnsCount) and 0 or 1) + ((has_freeze_v and merge_group_sub_n == 0) and 0 or 2)
IM_ASSERT(channel_no < max_draw_channels)
local merge_group = merge_groups[merge_group_n]
if merge_group.ChannelsCount == 0 then
merge_group.ClipRect = ImRect(FLT_MAX, FLT_MAX, -FLT_MAX, -FLT_MAX)
end
merge_group.ChannelsMask[channel_no] = true
merge_group.ChannelsCount = merge_group.ChannelsCount + 1
local cr = cmds.Data[1].ClipRect
merge_group.ClipRect:Add(ImRect(cr.x, cr.y, cr.z, cr.w))
merge_group_mask = bor(merge_group_mask, lshift(1, merge_group_n))
until true
end
-- Invalidate current draw channel
-- (we don't clear DrawChannelFrozen/DrawChannelUnfrozen solely to facilitate debugging/later inspection of data)
column.DrawChannelCurrent = -1
end
end
-- 2. Rewrite channel list in our preferred order
if merge_group_mask ~= 0 then
-- We skip channel 0 (Bg0/Bg1) and 1 (Bg2 frozen) from the shuffling since they won't move - see channels allocation in TableSetupDrawChannels().
local LEADING_DRAW_CHANNELS = 2
local dst_tmp = {}
for n = LEADING_DRAW_CHANNELS, splitter._Count - 1 do
remaining_mask[n] = true
end
remaining_mask[table_.Bg2DrawChannelUnfrozen] = nil
IM_ASSERT(has_freeze_v == false or table_.Bg2DrawChannelUnfrozen ~= TABLE_DRAW_CHANNEL_BG2_FROZEN)
local remaining_count = splitter._Count - (has_freeze_v and (LEADING_DRAW_CHANNELS + 1) or LEADING_DRAW_CHANNELS)
--local host_rect = (table.InnerWindow == table.OuterWindow) and table.InnerClipRect or table.HostClipRect
local host_rect = table_.HostClipRect
for merge_group_n = 0, 3 do
local merge_channels_count = merge_groups[merge_group_n].ChannelsCount
if merge_channels_count ~= 0 then
local merge_group = merge_groups[merge_group_n]
local merge_clip_rect = Rect(merge_group.ClipRect)
-- Extend outer-most clip limits to match those of host, so draw calls can be merged even if
-- outer-most columns have some outer padding offsetting them from their parent ClipRect.
-- The principal cases this is dealing with are:
-- - On a same-window table (not scrolling = single group), all fitting columns ClipRect -> will extend and match host ClipRect -> will merge
-- - Columns can use padding and have left-most ClipRect.Min.x and right-most ClipRect.Max.x != from host ClipRect -> will extend and match host ClipRect -> will merge
-- FIXME-TABLE FIXME-WORKRECT: We are wasting a merge opportunity on tables without scrolling if column doesn't fit
-- within host clip rect, solely because of the half-padding difference between window.WorkRect and window.InnerClipRect.
if band(merge_group_n, 1) == 0 or not has_freeze_h then
merge_clip_rect.Min.x = ImMin(merge_clip_rect.Min.x, host_rect.Min.x)
end
if band(merge_group_n, 2) == 0 or not has_freeze_v then
merge_clip_rect.Min.y = ImMin(merge_clip_rect.Min.y, host_rect.Min.y)
end
if band(merge_group_n, 1) ~= 0 then
merge_clip_rect.Max.x = ImMax(merge_clip_rect.Max.x, host_rect.Max.x)
end
if band(merge_group_n, 2) ~= 0 and band(table_.Flags, ImGuiTableFlags.NoHostExtendY) == 0 then
merge_clip_rect.Max.y = ImMax(merge_clip_rect.Max.y, host_rect.Max.y)
end
--ImGui.GetForegroundDrawList():AddRect(merge_group.ClipRect.Min, merge_group.ClipRect.Max, IM_COL32(255, 0, 0, 200), 0.0, 0, 1.0) -- [DEBUG]
--ImGui.GetForegroundDrawList():AddLine(merge_group.ClipRect.Min, merge_clip_rect.Min, IM_COL32(255, 100, 0, 200))
--ImGui.GetForegroundDrawList():AddLine(merge_group.ClipRect.Max, merge_clip_rect.Max, IM_COL32(255, 100, 0, 200))
remaining_count = remaining_count - merge_group.ChannelsCount
for n in pairs(merge_group.ChannelsMask) do
remaining_mask[n] = nil
end
local n = 0
while n < splitter._Count and merge_channels_count ~= 0 do
-- Copy + overwrite new clip rect
if merge_group.ChannelsMask[n] then
merge_group.ChannelsMask[n] = nil
merge_channels_count = merge_channels_count - 1
local channel = channels[n + 1]
IM_ASSERT(channel._CmdBuffer.Size == 1)
ImVec4_Copy(channel._CmdBuffer.Data[1].ClipRect, merge_clip_rect:ToVec4())
dst_tmp[#dst_tmp + 1] = channel
end
n = n + 1
end
end
-- Make sure Bg2DrawChannelUnfrozen appears in the middle of our groups (whereas Bg0/Bg1 and Bg2 frozen are fixed to 0 and 1)
if merge_group_n == 1 and has_freeze_v then
dst_tmp[#dst_tmp + 1] = channels[table_.Bg2DrawChannelUnfrozen + 1]
end
end
-- Append unmergeable channels that we didn't reorder at the end of the list
local n = 0
while n < splitter._Count and remaining_count ~= 0 do
if remaining_mask[n] then
dst_tmp[#dst_tmp + 1] = channels[n + 1]
remaining_count = remaining_count - 1
end
n = n + 1
end
IM_ASSERT(#dst_tmp == splitter._Count - LEADING_DRAW_CHANNELS)
for i = 1, #dst_tmp do
channels[LEADING_DRAW_CHANNELS + i] = dst_tmp[i]
end
end
end
local function TableGetColumnBorderCol(table_, order_n, column_n)
local is_hovered = (table_.HoveredColumnBorder == column_n)
local is_resized = (table_.ResizedColumn == column_n) and (table_.InstanceInteracted == table_.InstanceCurrent)
local is_frozen_separator = (table_.FreezeColumnsCount == order_n + 1)
if is_resized or is_hovered then
return ImGui.GetColorU32(is_resized and ImGuiCol.SeparatorActive or ImGuiCol.SeparatorHovered)
end
if is_frozen_separator or band(table_.Flags, bor(ImGuiTableFlags.NoBordersInBody, ImGuiTableFlags.NoBordersInBodyUntilResize)) ~= 0 then
return table_.BorderColorStrong
end
return table_.BorderColorLight
end
-- FIXME-TABLE: This is a mess, need to redesign how we render borders (as some are also done in TableEndRow)
function ImGui.TableDrawBorders(table_)
local inner_window = table_.InnerWindow
if not table_.OuterWindow.ClipRect:Overlaps(table_.OuterRect) then
return
end
local inner_drawlist = inner_window.DrawList
table_.DrawSplitter:SetCurrentChannel(inner_drawlist, TABLE_DRAW_CHANNEL_BG0)
inner_drawlist:PushClipRect(table_.Bg0ClipRectForDrawCmd.Min, table_.Bg0ClipRectForDrawCmd.Max, false)
-- Draw inner border and resizing feedback
local table_instance = ImGui.TableGetInstanceData(table_, table_.InstanceCurrent)
local border_size = TABLE_BORDER_SIZE
local draw_y1 = ImMax(table_.InnerRect.Min.y, ((table_.FreezeRowsCount >= 1) and table_.InnerRect.Min.y or table_.WorkRect.Min.y) + table_.AngledHeadersHeight) + ((band(table_.Flags, ImGuiTableFlags.BordersOuterH) ~= 0) and border_size or 0.0)
local draw_y2_body = table_.InnerRect.Max.y
local draw_y2_head = table_.IsUsingHeaders and ImMin(table_.InnerRect.Max.y, ((table_.FreezeRowsCount >= 1) and table_.InnerRect.Min.y or table_.WorkRect.Min.y) + table_instance.LastTopHeadersRowHeight) or draw_y1
if band(table_.Flags, ImGuiTableFlags.BordersInnerV) ~= 0 then
for order_n = 0, table_.ColumnsCount - 1 do
repeat
if not BitTest(table_.EnabledMaskByDisplayOrder, order_n) then
break
end
local column_n = table_.DisplayOrderToIndex[order_n]
local column = table_.Columns[column_n]
local is_hovered = (table_.HoveredColumnBorder == column_n)
local is_resized = (table_.ResizedColumn == column_n) and (table_.InstanceInteracted == table_.InstanceCurrent)
local is_resizable = band(column.Flags, bor(ImGuiTableColumnFlags.NoResize, ImGuiTableColumnFlags.NoDirectResize_)) == 0
local is_frozen_separator = (table_.FreezeColumnsCount == order_n + 1)
if column.MaxX > table_.InnerClipRect.Max.x and not is_resized then
break
end
-- Decide whether right-most column is visible
if column.NextEnabledColumn == -1 and not is_resizable then
if band(table_.Flags, ImGuiTableFlags.SizingMask_) ~= ImGuiTableFlags.SizingFixedSame or band(table_.Flags, ImGuiTableFlags.NoHostExtendX) ~= 0 then
break
end
end
if column.MaxX <= column.ClipRect.Min.x then -- FIXME-TABLE FIXME-STYLE: Assume BorderSize==1, this is problematic if we want to increase the border size..
break
end
-- Draw in outer window so right-most column won't be clipped
local draw_y2 = draw_y2_head
if is_frozen_separator then
draw_y2 = draw_y2_body
elseif band(table_.Flags, ImGuiTableFlags.NoBordersInBodyUntilResize) ~= 0 and (is_hovered or is_resized) then
draw_y2 = draw_y2_body
elseif band(table_.Flags, bor(ImGuiTableFlags.NoBordersInBodyUntilResize, ImGuiTableFlags.NoBordersInBody)) == 0 then
draw_y2 = draw_y2_body
end
if draw_y2 > draw_y1 then
inner_drawlist:AddLineV(column.MaxX, draw_y1, draw_y2, TableGetColumnBorderCol(table_, order_n, column_n), border_size)
end
until true
end
end
-- Draw outer border
-- FIXME: could use AddRect or explicit VLine/HLine helper?
if band(table_.Flags, ImGuiTableFlags.BordersOuter) ~= 0 then
-- Display outer border offset by 1 which is a simple way to display it without adding an extra draw call
-- (Without the offset, in outer_window it would be rendered behind cells, because child windows are above their
-- parent. In inner_window, it won't reach out over scrollbars. Another weird solution would be to display part
-- of it in inner window, and the part that's over scrollbars in the outer window..)
-- Either solution currently won't allow us to use a larger border size: the border would clipped.
local outer_border = table_.OuterRect
local outer_col = table_.BorderColorStrong
if band(table_.Flags, ImGuiTableFlags.BordersOuter) == ImGuiTableFlags.BordersOuter then
inner_drawlist:AddRect(outer_border.Min, outer_border.Max, outer_col, 0.0, border_size)
elseif band(table_.Flags, ImGuiTableFlags.BordersOuterV) ~= 0 then
inner_drawlist:AddLineV(outer_border.Min.x, outer_border.Min.y, outer_border.Max.y, outer_col, border_size)
inner_drawlist:AddLineV(outer_border.Max.x - border_size, outer_border.Min.y, outer_border.Max.y, outer_col, border_size)
elseif band(table_.Flags, ImGuiTableFlags.BordersOuterH) ~= 0 then
inner_drawlist:AddLineH(outer_border.Min.x, outer_border.Max.x, outer_border.Min.y, outer_col, border_size)
inner_drawlist:AddLineH(outer_border.Min.x, outer_border.Max.x, outer_border.Max.y - border_size, outer_col, border_size)
end
end
if band(table_.Flags, ImGuiTableFlags.BordersInnerH) ~= 0 and table_.RowPosY2 < table_.OuterRect.Max.y then
-- Draw bottom-most row border between it is above outer border.
local h_inset = (band(table_.Flags, ImGuiTableFlags.BordersOuterV) ~= 0) and border_size or 0.0
local border_y = table_.RowPosY2
if border_y >= table_.BgClipRect.Min.y and border_y < table_.BgClipRect.Max.y then
inner_drawlist:AddLineH(table_.BorderX1 + h_inset, table_.BorderX2 - h_inset, border_y, table_.BorderColorLight, border_size)
end
end
inner_drawlist:PopClipRect()
end
-------------------------------------------------------------------------------
-- [SECTION] Tables: Sorting
-------------------------------------------------------------------------------
-- - TableGetSortSpecs()
-- - TableFixColumnSortDirection() [Internal]
-- - TableGetColumnNextSortDirection() [Internal]
-- - TableSetColumnSortDirection() [Internal]
-- - TableSortSpecsSanitize() [Internal]
-- - TableSortSpecsBuild() [Internal]
-------------------------------------------------------------------------------
-- Return nil if no sort specs (most often when ImGuiTableFlags.Sortable is not set)
-- When 'sort_specs.SpecsDirty == true' you should sort your data. It will be true when sorting specs have
-- changed since last call, or the first time. Make sure to set 'SpecsDirty = false' after sorting,
-- else you may wastefully sort your data every frame!
-- Lifetime: don't hold on this table over multiple frames or past any subsequent call to BeginTable()!
-- PORT: Specs is a 1-based Lua array.
function ImGui.TableGetSortSpecs()
local g = GImGui
local table_ = g.CurrentTable
if table_ == nil or band(table_.Flags, ImGuiTableFlags.Sortable) == 0 then
return nil
end
-- Require layout (in case TableHeadersRow() hasn't been called) as it may alter IsSortSpecsDirty in some paths.
if not table_.IsLayoutLocked then
ImGui.TableUpdateLayout(table_)
end
ImGui.TableSortSpecsBuild(table_)
return table_.SortSpecs
end
local function TableGetColumnAvailSortDirection(column, n)
IM_ASSERT(n < column.SortDirectionsAvailCount)
return band(rshift(column.SortDirectionsAvailList, lshift(n, 1)), 0x03)
end
-- Fix sort direction if currently set on a value which is unavailable (e.g. activating NoSortAscending/NoSortDescending)
function ImGui.TableFixColumnSortDirection(table_, column)
if column.SortOrder == -1 or band(column.SortDirectionsAvailMask, lshift(1, column.SortDirection)) ~= 0 then
return
end
column.SortDirection = TableGetColumnAvailSortDirection(column, 0)
table_.IsSortSpecsDirty = true
end
-- Calculate next sort direction that would be set after clicking the column
-- - If the PreferSortDescending flag is set, we will default to a Descending direction on the first click.
-- - Note that the PreferSortAscending flag is never checked, it is essentially the default and therefore a no-op.
function ImGui.TableGetColumnNextSortDirection(column)
IM_ASSERT(column.SortDirectionsAvailCount > 0)
if column.SortOrder == -1 then
return TableGetColumnAvailSortDirection(column, 0)
end
for n = 0, 2 do
if column.SortDirection == TableGetColumnAvailSortDirection(column, n) then
return TableGetColumnAvailSortDirection(column, (n + 1) % column.SortDirectionsAvailCount)
end
end
IM_ASSERT(false)
return ImGuiSortDirection.None
end
-- Note that the NoSortAscending/NoSortDescending flags are processed in TableSortSpecsSanitize(), and they may change/revert
-- the value of SortDirection. We could technically also do it here but it would be unnecessary and duplicate code.
function ImGui.TableSetColumnSortDirection(column_n, sort_direction, append_to_sort_specs)
local g = GImGui
local table_ = g.CurrentTable
if band(table_.Flags, ImGuiTableFlags.SortMulti) == 0 then
append_to_sort_specs = false
end
if band(table_.Flags, ImGuiTableFlags.SortTristate) == 0 then
IM_ASSERT(sort_direction ~= ImGuiSortDirection.None)
end
local sort_order_max = 0
if append_to_sort_specs then
for other_column_n = 0, table_.ColumnsCount - 1 do
sort_order_max = ImMax(sort_order_max, table_.Columns[other_column_n].SortOrder)
end
end
local column = table_.Columns[column_n]
column.SortDirection = sort_direction
if column.SortDirection == ImGuiSortDirection.None then
column.SortOrder = -1
elseif column.SortOrder == -1 or not append_to_sort_specs then
column.SortOrder = append_to_sort_specs and (sort_order_max + 1) or 0
end
for other_column_n = 0, table_.ColumnsCount - 1 do
local other_column = table_.Columns[other_column_n]
if other_column ~= column and not append_to_sort_specs then
other_column.SortOrder = -1
end
ImGui.TableFixColumnSortDirection(table_, other_column)
end
table_.IsSettingsDirty = true
table_.IsSortSpecsDirty = true
end
function ImGui.TableSortSpecsSanitize(table_)
IM_ASSERT(band(table_.Flags, ImGuiTableFlags.Sortable) ~= 0)
-- Clear SortOrder from hidden column and verify that there's no gap or duplicate.
local sort_order_count = 0
local sort_order_set = {} -- C++: ImU64 sort_order_mask
local sort_order_dup = false
for column_n = 0, table_.ColumnsCount - 1 do
local column = table_.Columns[column_n]
if column.SortOrder ~= -1 and not column.IsEnabled then
column.SortOrder = -1
end
if column.SortOrder ~= -1 then
sort_order_count = sort_order_count + 1
if sort_order_set[column.SortOrder] then sort_order_dup = true end
sort_order_set[column.SortOrder] = true
IM_ASSERT(sort_order_count < 64)
end
end
-- need_fix_linearize: (1 << count) != (mask + 1) <=> the set of sort orders isn't exactly {0..count-1}
local need_fix_linearize = sort_order_dup
for n = 0, sort_order_count - 1 do
if not sort_order_set[n] then need_fix_linearize = true end
end
local need_fix_single_sort_order = (sort_order_count > 1) and band(table_.Flags, ImGuiTableFlags.SortMulti) == 0
if need_fix_linearize or need_fix_single_sort_order then
local fixed_mask = {}
for sort_n = 0, sort_order_count - 1 do
-- Fix: Rewrite sort order fields if needed so they have no gap or duplicate.
-- (e.g. SortOrder 0 disappeared, SortOrder 1..2 exists --> rewrite then as SortOrder 0..1)
local column_with_smallest_sort_order = -1
for column_n = 0, table_.ColumnsCount - 1 do
if not fixed_mask[column_n] and table_.Columns[column_n].SortOrder ~= -1 then
if column_with_smallest_sort_order == -1 or table_.Columns[column_n].SortOrder < table_.Columns[column_with_smallest_sort_order].SortOrder then
column_with_smallest_sort_order = column_n
end
end
end
IM_ASSERT(column_with_smallest_sort_order ~= -1)
fixed_mask[column_with_smallest_sort_order] = true
table_.Columns[column_with_smallest_sort_order].SortOrder = sort_n
-- Fix: Make sure only one column has a SortOrder if ImGuiTableFlags.MultiSortable is not set.
if need_fix_single_sort_order then
sort_order_count = 1
for column_n = 0, table_.ColumnsCount - 1 do
if column_n ~= column_with_smallest_sort_order then
table_.Columns[column_n].SortOrder = -1
end
end
break
end
end
end
-- Fallback default sort order (if no column with the ImGuiTableColumnFlags.DefaultSort flag)
if sort_order_count == 0 and band(table_.Flags, ImGuiTableFlags.SortTristate) == 0 then
for column_n = 0, table_.ColumnsCount - 1 do
local column = table_.Columns[column_n]
if column.IsEnabled and band(column.Flags, ImGuiTableColumnFlags.NoSort) == 0 then
sort_order_count = 1
column.SortOrder = 0
column.SortDirection = TableGetColumnAvailSortDirection(column, 0)
break
end
end
end
table_.SortSpecsCount = sort_order_count
end
function ImGui.TableSortSpecsBuild(table_)
local dirty = table_.IsSortSpecsDirty
if dirty then
ImGui.TableSortSpecsSanitize(table_)
local multi = table_.SortSpecsMulti
local want = (table_.SortSpecsCount <= 1) and 0 or table_.SortSpecsCount
for n = #multi + 1, want do multi[n] = ImGuiTableColumnSortSpecs() end
for n = #multi, want + 1, -1 do multi[n] = nil end
table_.SortSpecs.SpecsDirty = true -- Mark as dirty for user
table_.IsSortSpecsDirty = false -- Mark as not dirty for us
end
-- Write output
-- May be able to move all SortSpecs data from table (48 bytes) to ImGuiTableTempData if we decide to write it back on every BeginTable()
local sort_specs
if table_.SortSpecsCount == 0 then
sort_specs = nil
elseif table_.SortSpecsCount == 1 then
table_.SortSpecsSingleArray = table_.SortSpecsSingleArray or { table_.SortSpecsSingle }
sort_specs = table_.SortSpecsSingleArray
else
sort_specs = table_.SortSpecsMulti
end
if dirty and sort_specs ~= nil then
for column_n = 0, table_.ColumnsCount - 1 do
local column = table_.Columns[column_n]
if column.SortOrder ~= -1 then
IM_ASSERT(column.SortOrder < table_.SortSpecsCount)
local sort_spec = sort_specs[column.SortOrder + 1]
sort_spec.ColumnUserID = column.UserData
sort_spec.ColumnIndex = column_n
sort_spec.SortOrder = column.SortOrder
sort_spec.SortDirection = column.SortDirection
end
end
end
table_.SortSpecs.Specs = sort_specs
table_.SortSpecs.SpecsCount = table_.SortSpecsCount
end
-------------------------------------------------------------------------------
-- [SECTION] Tables: Headers
-------------------------------------------------------------------------------
-- - TableGetHeaderRowHeight() [Internal]
-- - TableGetHeaderAngledMaxLabelWidth() [Internal]
-- - TableHeadersRow()
-- - TableHeader()
-- - TableAngledHeadersRow()
-- - TableAngledHeadersRowEx() [Internal]
-------------------------------------------------------------------------------
function ImGui.TableGetHeaderRowHeight()
-- Caring for a minor edge case:
-- Calculate row height, for the unlikely case that some labels may be taller than others.
-- If we didn't do that, uneven header height would highlight but smaller one before the tallest wouldn't catch input for all height.
-- In your custom header row you may omit this all together and just call TableNextRow() without a height...
local g = GImGui
local table_ = g.CurrentTable
local row_height = g.FontSize
for column_n = 0, table_.ColumnsCount - 1 do
if BitTest(table_.EnabledMaskByIndex, column_n) then
if band(table_.Columns[column_n].Flags, ImGuiTableColumnFlags.NoHeaderLabel) == 0 then
row_height = ImMax(row_height, ImGui.CalcTextSize(ImGui.TableGetColumnName(table_, column_n)).y)
end
end
end
return row_height + g.Style.CellPadding.y * 2.0
end
function ImGui.TableGetHeaderAngledMaxLabelWidth()
local g = GImGui
local table_ = g.CurrentTable
local width = 0.0
for column_n = 0, table_.ColumnsCount - 1 do
if BitTest(table_.EnabledMaskByIndex, column_n) then
if band(table_.Columns[column_n].Flags, ImGuiTableColumnFlags.AngledHeader) ~= 0 then
width = ImMax(width, ImGui.CalcTextSize(ImGui.TableGetColumnName(table_, column_n), nil, true).x)
end
end
end
return width + g.Style.CellPadding.y * 2.0 -- Swap padding
end
-- [Public] This is a helper to output TableHeader() calls based on the column names declared in TableSetupColumn().
-- The intent is that advanced users willing to create customized headers would not need to use this helper
-- and can create their own! For example: TableHeader() may be preceded by Checkbox() or other custom widgets.
-- See 'Demo->Tables->Custom headers' for a demonstration of implementing a custom version of this.
-- This code is intentionally written to not make much use of internal functions, to give you better direction
-- if you need to write your own.
-- FIXME-TABLE: TableOpenContextMenu() and TableGetHeaderRowHeight() are not public.
function ImGui.TableHeadersRow()
local g = GImGui
local table_ = g.CurrentTable
IM_ASSERT_USER_ERROR_RET(table_ ~= nil, "Call should only be done while in BeginTable() scope!")
-- Call layout if not already done. This is automatically done by TableNextRow: we do it here _only_ to make
-- it easier to debug-step in TableUpdateLayout(). Your own version of this function doesn't need this.
if not table_.IsLayoutLocked then
ImGui.TableUpdateLayout(table_)
end
-- Open row
local row_height = ImGui.TableGetHeaderRowHeight()
ImGui.TableNextRow(ImGuiTableRowFlags.Headers, row_height)
local row_y1 = ImGui.GetCursorScreenPos().y
if table_.HostSkipItems then -- Merely an optimization, you may skip in your own code.
return
end
local columns_count = ImGui.TableGetColumnCount()
for column_n = 0, columns_count - 1 do
if ImGui.TableSetColumnIndex(column_n) or table_.LastHeldHeaderColumn == column_n then
-- Push an id to allow empty/unnamed headers. This is also idiomatic as it ensure there is a consistent ID path to access columns (for e.g. automation)
local name = (band(ImGui.TableGetColumnFlags(column_n), ImGuiTableColumnFlags.NoHeaderLabel) ~= 0) and "" or ImGui.TableGetColumnName(column_n)
ImGui.PushID(column_n)
ImGui.TableHeader(name)
ImGui.PopID()
end
end
-- Allow opening popup from the right-most section after the last column.
local mouse_pos = ImGui.GetMousePos()
if ImGui.IsMouseReleased(1) and ImGui.TableGetHoveredColumn() == columns_count then
if mouse_pos.y >= row_y1 and mouse_pos.y < row_y1 + row_height then
ImGui.TableOpenContextMenu(columns_count) -- Will open a non-column-specific popup.
end
end
end
-- Emit a column header (text + optional sort order)
-- We cpu-clip text here so that all columns headers can be merged into a same draw call.
-- Note that because of how we cpu-clip and display sorting indicators, you _cannot_ use SameLine() after a TableHeader()
function ImGui.TableHeader(label)
local g = GImGui
local window = g.CurrentWindow
if window.SkipItems then
return
end
local table_ = g.CurrentTable
IM_ASSERT_USER_ERROR_RET(table_ ~= nil, "Call should only be done while in BeginTable() scope!")
IM_ASSERT(table_.CurrentColumn ~= -1)
local column_n = table_.CurrentColumn
local column = table_.Columns[column_n]
-- Label
if label == nil then
label = ""
end
local label_end = ImGui.FindRenderedTextEnd(label)
local label_size = ImGui.CalcTextSize(label, label_end, false)
local label_pos = V2(window.DC.CursorPos)
-- If we already got a row height, there's use that.
-- FIXME-TABLE: Padding problem if the correct outer-padding CellBgRect strays off our ClipRect?
local cell_r = ImGui.TableGetCellBgRect(table_, column_n)
local label_height = ImMax(label_size.y, table_.RowMinHeight - table_.RowCellPaddingY * 2.0)
-- Calculate ideal size for sort order arrow
local w_arrow = 0.0
local w_sort_text = 0.0
local sort_arrow = false
local sort_order_suf = ""
local ARROW_SCALE = 0.65
if band(table_.Flags, ImGuiTableFlags.Sortable) ~= 0 and band(column.Flags, ImGuiTableColumnFlags.NoSort) == 0 then
w_arrow = ImTrunc(g.FontSize * ARROW_SCALE + g.Style.FramePadding.x)
if column.SortOrder ~= -1 then
sort_arrow = true
end
if column.SortOrder > 0 then
sort_order_suf = string.format("%d", column.SortOrder + 1)
w_sort_text = g.Style.ItemInnerSpacing.x + ImGui.CalcTextSize(sort_order_suf).x
end
end
-- We feed our unclipped width to the column without writing on CursorMaxPos, so that column is still considered for merging.
local max_pos_x = label_pos.x + label_size.x + w_sort_text + w_arrow
column.ContentMaxXHeadersUsed = ImMax(column.ContentMaxXHeadersUsed, sort_arrow and cell_r.Max.x or ImMin(max_pos_x, cell_r.Max.x))
column.ContentMaxXHeadersIdeal = ImMax(column.ContentMaxXHeadersIdeal, max_pos_x)
-- Keep header highlighted when context menu is open.
local id = window:GetID(label)
local bb = ImRect(cell_r.Min.x, cell_r.Min.y, cell_r.Max.x, ImMax(cell_r.Max.y, cell_r.Min.y + label_height + g.Style.CellPadding.y * 2.0))
ImGui.ItemSize(ImVec2(0.0, label_height)) -- Don't declare unclipped width, it'll be fed ContentMaxPosHeadersIdeal
if not ImGui.ItemAdd(bb, id) then
return
end
--GetForegroundDrawList():AddRect(cell_r.Min, cell_r.Max, IM_COL32(255, 0, 0, 255)) -- [DEBUG]
--GetForegroundDrawList():AddRect(bb.Min, bb.Max, IM_COL32(255, 0, 0, 255)) -- [DEBUG]
-- Using AllowOverlap mode because we cover the whole cell, and we want user to be able to submit subsequent items.
local highlight = (table_.HighlightColumnHeader == column_n)
local pressed, hovered, held = ImGui.ButtonBehavior(bb, id, ImGuiButtonFlags.AllowOverlap)
if held or hovered or highlight then
local col = ImGui.GetColorU32(held and ImGuiCol.HeaderActive or (hovered and ImGuiCol.HeaderHovered or ImGuiCol.Header))
--RenderFrame(bb.Min, bb.Max, col, false, 0.0)
ImGui.TableSetBgColor(ImGuiTableBgTarget.CellBg, col, table_.CurrentColumn)
else
-- Submit single cell bg color in the case we didn't submit a full header row
if band(table_.RowFlags, ImGuiTableRowFlags.Headers) == 0 then
ImGui.TableSetBgColor(ImGuiTableBgTarget.CellBg, ImGui.GetColorU32(ImGuiCol.TableHeaderBg), table_.CurrentColumn)
end
end
ImGui.RenderNavCursor(bb, id, ImGuiNavRenderCursorFlags.Compact, 0.0)
if held then
table_.HeldHeaderColumn = column_n
end
window.DC.CursorPos.y = window.DC.CursorPos.y - g.Style.ItemSpacing.y * 0.5
-- Drag and drop to re-order columns.
-- FIXME-TABLE: Scroll request while reordering a column and it lands out of the scrolling zone.
if held and band(table_.Flags, ImGuiTableFlags.Reorderable) ~= 0 and ImGui.IsMouseDragging(0) and not g.DragDropActive then
-- - While moving a column it will jump on the other side of the mouse, so we also test for MouseDelta.x
-- - We need to handle reordering across hidden columns.
-- In the configuration below, moving C to the right of E will lead to:
-- ... C [D] E ---> ... [D] E C (Column name/index)
-- ... 2 3 4 ... 2 3 4 (Display order)
-- - The other constraints are enforced by TableQueueSetColumnDisplayOrder() which might early out.
table_.InstanceInteracted = table_.InstanceCurrent
if g.IO.MouseDelta.x < 0.0 and g.IO.MousePos.x < cell_r.Min.x then
local prev_column = (column.PrevEnabledColumn ~= -1) and table_.Columns[column.PrevEnabledColumn] or nil
if prev_column then
ImGui.TableQueueSetColumnDisplayOrder(table_, column_n, prev_column.DisplayOrder)
end
end
if g.IO.MouseDelta.x > 0.0 and g.IO.MousePos.x > cell_r.Max.x then
local next_column = (column.NextEnabledColumn ~= -1) and table_.Columns[column.NextEnabledColumn] or nil
if next_column then
ImGui.TableQueueSetColumnDisplayOrder(table_, column_n, next_column.DisplayOrder)
end
end
end
-- Sort order arrow
-- - Only clip label for a visible arrow. Auto-fit still accounts for a possible arrow, but when
-- manually sized smaller we don't clip the label for the sake of an arrow that isn't displayed.
local ellipsis_max = ImMax(cell_r.Max.x - (sort_arrow and (w_arrow + w_sort_text) or 0.0), label_pos.x)
if band(table_.Flags, ImGuiTableFlags.Sortable) ~= 0 and band(column.Flags, ImGuiTableColumnFlags.NoSort) == 0 then
if column.SortOrder ~= -1 then
local x = ImMax(cell_r.Min.x, cell_r.Max.x - w_arrow - w_sort_text)
local y = label_pos.y
if column.SortOrder > 0 then
ImGui.PushStyleColor(ImGuiCol.Text, ImGui.GetColorU32(ImGuiCol.Text, 0.70))
ImGui.RenderText(ImVec2(x + g.Style.ItemInnerSpacing.x, y), sort_order_suf)
ImGui.PopStyleColor()
x = x + w_sort_text
end
ImGui.RenderArrow(window.DrawList, ImVec2(x, y), ImGui.GetColorU32(ImGuiCol.Text), (column.SortDirection == ImGuiSortDirection.Ascending) and ImGuiDir.Up or ImGuiDir.Down, ARROW_SCALE)
end
-- Handle clicking on column header to adjust Sort Order
if pressed and table_.ReorderColumn ~= column_n then
local sort_direction = ImGui.TableGetColumnNextSortDirection(column)
ImGui.TableSetColumnSortDirection(column_n, sort_direction, g.IO.KeyShift)
end
end
-- Render clipped label. Clipping here ensure that in the majority of situations, all our header cells will
-- be merged into a single draw call.
--window.DrawList:AddCircleFilled(ImVec2(ellipsis_max, label_pos.y), 40, IM_COL32_WHITE)
ImGui.RenderTextEllipsis(window.DrawList, label_pos, ImVec2(ellipsis_max, bb.Max.y), ellipsis_max, label, label_end, label_size)
local text_clipped = label_size.x > (ellipsis_max - label_pos.x)
if text_clipped and hovered and g.ActiveId == 0 then
ImGui.SetItemTooltip("%s", string.sub(label, 1, label_end - 1))
end
-- We don't use BeginPopupContextItem() because we want the popup to stay up even after the column is hidden
if ImGui.IsPopupOpenRequestForItem(ImGuiPopupFlags.None, id) then
ImGui.TableOpenContextMenu(column_n)
end
end
-- Unlike TableHeadersRow() it is not expected that you can reimplement or customize this with custom widgets.
-- FIXME: No hit-testing/button on the angled header.
function ImGui.TableAngledHeadersRow()
local g = GImGui
local table_ = g.CurrentTable
local temp_data = table_.TempData
temp_data.AngledHeadersRequests:resize(0)
-- Which column needs highlight?
local row_id = ImGui.GetID("##AngledHeaders")
local table_instance = ImGui.TableGetInstanceData(table_, table_.InstanceCurrent)
local highlight_column_n = (table_.LastHeldHeaderColumn ~= -1) and table_.LastHeldHeaderColumn or table_.HighlightColumnHeader
if highlight_column_n == -1 and table_.HoveredColumnBody ~= -1 then
if table_instance.HoveredRowLast == 0 and table_.HoveredColumnBorder == -1 and (g.ActiveId == 0 or g.ActiveId == row_id or (table_.IsActiveIdInTable or g.DragDropActive)) then
highlight_column_n = table_.HoveredColumnBody
end
end
-- Build up request
local col_header_bg = ImGui.GetColorU32(ImGuiCol.TableHeaderBg)
local col_text = ImGui.GetColorU32(ImGuiCol.Text)
for order_n = 0, table_.ColumnsCount - 1 do
if BitTest(table_.EnabledMaskByDisplayOrder, order_n) then
local column_n = table_.DisplayOrderToIndex[order_n]
local column = table_.Columns[column_n]
if band(column.Flags, ImGuiTableColumnFlags.AngledHeader) ~= 0 then -- Note: can't rely on ImGuiTableColumnFlags.IsVisible test here.
temp_data.AngledHeadersRequests:push_back({ Index = column_n, TextColor = col_text, BgColor0 = col_header_bg, BgColor1 = (column_n == highlight_column_n) and ImGui.GetColorU32(ImGuiCol.Header) or 0 })
end
end
end
-- Render row
ImGui.TableAngledHeadersRowEx(row_id, g.Style.TableAngledHeadersAngle, 0.0, temp_data.AngledHeadersRequests.Data, temp_data.AngledHeadersRequests.Size)
end
local function ImTextCountLines(text, text_begin, text_end)
local count = 0
local p = text_begin
while p < text_end do
local line_end = string.find(text, "\n", p, true)
if line_end == nil or line_end >= text_end then p = text_end else p = line_end + 1 end
count = count + 1
end
return count
end
-- Important: data must be fed left to right
--- @param data ImGuiTableHeaderData[] # 1-based array
--- @param data_count int
function ImGui.TableAngledHeadersRowEx(row_id, angle, max_label_width, data, data_count)
local g = GImGui
local table_ = g.CurrentTable
local window = g.CurrentWindow
local draw_list = window.DrawList
IM_ASSERT_USER_ERROR_RET(table_ ~= nil, "Call should only be done while in BeginTable() scope!")
IM_ASSERT(table_.CurrentRow == -1, "Must be first row")
if max_label_width == 0.0 then
max_label_width = ImGui.TableGetHeaderAngledMaxLabelWidth()
end
-- Angle argument expressed in (-IM_PI/2 .. +IM_PI/2) as it is easier to think about for user.
local flip_label = (angle < 0.0)
angle = angle - IM_PI * 0.5
local cos_a = ImCos(angle)
local sin_a = ImSin(angle)
local label_cos_a = flip_label and ImCos(angle + IM_PI) or cos_a
local label_sin_a = flip_label and ImSin(angle + IM_PI) or sin_a
local unit_right = ImVec2(cos_a, sin_a)
-- Calculate our base metrics and set angled headers data _before_ the first call to TableNextRow()
-- FIXME-STYLE: Would it be better for user to submit 'max_label_width' or 'row_height' ? One can be derived from the other.
local header_height = g.FontSize + g.Style.CellPadding.x * 2.0
local row_height = ImTrunc(ImAbs(ImRotate(ImVec2(max_label_width, flip_label and header_height or -header_height), cos_a, sin_a).y))
table_.AngledHeadersHeight = row_height
table_.AngledHeadersSlope = (sin_a ~= 0.0) and (cos_a / sin_a) or 0.0
local header_angled_vector = unit_right * (row_height / -sin_a) -- vector from bottom-left to top-left, and from bottom-right to top-right
-- Declare row, override and draw our own background
-- FIXME-TABLE: Generally broken when overlapping frozen columns limit.
ImGui.TableNextRow(ImGuiTableRowFlags.Headers, row_height)
ImGui.TableNextColumn()
local row_r = ImRect(table_.WorkRect.Min.x, table_.BgClipRect.Min.y, table_.WorkRect.Max.x, table_.RowPosY2)
table_.DrawSplitter:SetCurrentChannel(draw_list, TABLE_DRAW_CHANNEL_BG0)
local clip_rect_min_x = table_.BgClipRect.Min.x
if table_.FreezeColumnsCount > 0 then
clip_rect_min_x = ImMax(clip_rect_min_x, table_.Columns[table_.DisplayOrderToIndex[table_.FreezeColumnsCount - 1]].MaxX)
end
ImGui.TableSetBgColor(ImGuiTableBgTarget.RowBg0, 0) -- Cancel
ImGui.PushClipRect(table_.BgClipRect.Min, table_.BgClipRect.Max, false) -- Span all columns
draw_list:AddRectFilled(ImVec2(table_.BgClipRect.Min.x, row_r.Min.y), ImVec2(table_.BgClipRect.Max.x, row_r.Max.y), ImGui.GetColorU32(ImGuiCol.TableHeaderBg, 0.25)) -- FIXME-STYLE: Change row background with an arbitrary color.
ImGui.PushClipRect(ImVec2(clip_rect_min_x, table_.BgClipRect.Min.y), table_.BgClipRect.Max, true) -- Span all columns
ImGui.ButtonBehavior(row_r, row_id)
ImGui.KeepAliveID(row_id)
local ascent_scaled = g.FontBaked.Ascent * g.FontBakedScale -- FIXME: Standardize those scaling factors better
local line_off_for_ascent_x = (ImMax((g.FontSize - ascent_scaled) * 0.5, 0.0) / -sin_a) * (flip_label and -1.0 or 1.0)
local padding = g.Style.CellPadding -- We will always use swapped component
local align = g.Style.TableAngledHeadersTextAlign
-- Draw background and labels in first pass, then all borders.
local max_x = -FLT_MAX
for pass = 0, 1 do
for order_n = 0, data_count - 1 do
local request = data[order_n + 1]
local column_n = request.Index
local column = table_.Columns[column_n]
local bg_shape = {}
bg_shape[0] = ImVec2(column.MaxX, row_r.Max.y)
bg_shape[1] = ImVec2(column.MinX, row_r.Max.y)
bg_shape[2] = bg_shape[1] + header_angled_vector
bg_shape[3] = bg_shape[0] + header_angled_vector
if pass == 0 then
-- Draw shape
draw_list:AddQuadFilled(bg_shape[0], bg_shape[1], bg_shape[2], bg_shape[3], request.BgColor0)
draw_list:AddQuadFilled(bg_shape[0], bg_shape[1], bg_shape[2], bg_shape[3], request.BgColor1) -- Optional highlight
max_x = ImMax(max_x, bg_shape[3].x)
-- Draw label
-- - First draw at an offset where RenderTextXXX() function won't meddle with applying current ClipRect, then transform to final offset.
-- - Handle multiple lines manually, as we want each lines to follow on the horizontal border, rather than see a whole block rotated.
local label_name = ImGui.TableGetColumnName(table_, column_n)
local label_name_end = ImGui.FindRenderedTextEnd(label_name)
local line_off_step_x = (g.FontSize / -sin_a)
local label_lines = ImTextCountLines(label_name, 1, label_name_end)
-- Left<>Right alignment
local line_off_curr_x = flip_label and ((label_lines - 1) * line_off_step_x) or 0.0
local line_off_for_align_x = ImFloor(ImMax((((column.MaxX - column.MinX) - padding.x * 2.0) - (label_lines * line_off_step_x)), 0.0) * align.x)
line_off_curr_x = line_off_curr_x + line_off_for_align_x - line_off_for_ascent_x
-- Register header width
local w = column.WorkMinX + ImCeil(label_lines * line_off_step_x - line_off_for_align_x)
column.ContentMaxXHeadersUsed = w; column.ContentMaxXHeadersIdeal = w
local p = 1
while p < label_name_end do
local label_name_eol = string.find(label_name, "\n", p, true)
if label_name_eol == nil or label_name_eol > label_name_end then
label_name_eol = label_name_end
end
local line = string.sub(label_name, p, label_name_eol - 1)
-- FIXME: Individual line clipping for right-most column is broken for negative angles.
local label_size = ImGui.CalcTextSize(line)
local clip_width = max_label_width - padding.y -- Using padding.y*2.0f would be symmetrical but hide more text.
local clip_height = ImMin(label_size.y, column.ClipRect.Max.x - column.WorkMinX - line_off_curr_x)
local clip_r = ImRect(window.ClipRect.Min, window.ClipRect.Min + ImVec2(clip_width, clip_height))
local vtx_idx_begin = draw_list._VtxCurrentIdx
ImGui.PushStyleColor(ImGuiCol.Text, request.TextColor)
ImGui.RenderTextEllipsis(draw_list, clip_r.Min, clip_r.Max, clip_r.Max.x, line, #line + 1, label_size)
ImGui.PopStyleColor()
local vtx_idx_end = draw_list._VtxCurrentIdx
-- Up<>Down alignment
local available_space = ImMax(clip_width - label_size.x + ImAbs(padding.x * cos_a) * 2.0 - ImAbs(padding.y * sin_a) * 2.0, 0.0)
local vertical_offset = available_space * align.y * (flip_label and -1.0 or 1.0)
-- Rotate and offset label
local pivot_in = ImVec2(window.ClipRect.Min.x - vertical_offset, window.ClipRect.Min.y + label_size.y)
local pivot_out = ImVec2(column.WorkMinX, row_r.Max.y)
line_off_curr_x = line_off_curr_x + (flip_label and -line_off_step_x or line_off_step_x)
pivot_out = pivot_out + unit_right * padding.y
if flip_label then
pivot_out = pivot_out + unit_right * (clip_width - ImMax(0.0, clip_width - label_size.x))
end
pivot_out.x = pivot_out.x + (flip_label and (line_off_curr_x + line_off_step_x) or line_off_curr_x)
ImGui.ShadeVertsTransformPos(draw_list, vtx_idx_begin, vtx_idx_end, pivot_in, label_cos_a, label_sin_a, pivot_out) -- Rotate and offset
--if g.IO.KeyShift then local fg_dl = GetForegroundDrawList(); vtx_idx_begin = fg_dl._VtxCurrentIdx; fg_dl:AddRect(clip_r.Min, clip_r.Max, IM_COL32(0, 255, 0, 255), 0.0, 0, 1.0); ShadeVertsTransformPos(fg_dl, vtx_idx_begin, fg_dl._VtxCurrentIdx, pivot_in, label_cos_a, label_sin_a, pivot_out) end
p = label_name_eol + 1
end
end
if pass == 1 then
-- Draw border
local border_size = TABLE_BORDER_SIZE
draw_list:AddLine(bg_shape[0] + ImVec2(border_size * 0.5, 0.0), bg_shape[3] + ImVec2(border_size * 0.5, 0.0), TableGetColumnBorderCol(table_, order_n, column_n), border_size)
end
end
end
ImGui.PopClipRect()
ImGui.PopClipRect()
table_.TempData.AngledHeadersExtraWidth = ImMax(0.0, max_x - table_.Columns[table_.RightMostEnabledColumn].MaxX)
end
-------------------------------------------------------------------------------
-- [SECTION] Tables: Context Menu
-------------------------------------------------------------------------------
-- - TableOpenContextMenu() [Internal]
-- - TableBeginContextMenuPopup() [Internal]
-- - TableDrawDefaultContextMenu() [Internal]
-------------------------------------------------------------------------------
-- Use -1 to open menu not specific to a given column.
function ImGui.TableOpenContextMenu(column_n)
if column_n == nil then column_n = -1 end
local g = GImGui
local table_ = g.CurrentTable
if column_n == -1 and table_.CurrentColumn ~= -1 then -- When called within a column automatically use this one (for consistency)
column_n = table_.CurrentColumn
end
if column_n == table_.ColumnsCount then -- To facilitate using with TableGetHoveredColumn()
column_n = -1
end
IM_ASSERT(column_n >= -1 and column_n < table_.ColumnsCount)
if band(table_.Flags, bor(ImGuiTableFlags.Resizable, ImGuiTableFlags.Reorderable, ImGuiTableFlags.Hideable)) ~= 0 then
table_.IsContextPopupOpen = true
table_.ContextPopupColumn = column_n
table_.InstanceInteracted = table_.InstanceCurrent
local context_menu_id = ImHashStr("##ContextMenu", nil, table_.ID)
ImGui.OpenPopupEx(context_menu_id, ImGuiPopupFlags.None)
end
end
function ImGui.TableBeginContextMenuPopup(table_)
if not table_.IsContextPopupOpen or table_.InstanceCurrent ~= table_.InstanceInteracted then
return false
end
local context_menu_id = ImHashStr("##ContextMenu", nil, table_.ID)
if ImGui.BeginPopupEx(context_menu_id, bor(ImGuiWindowFlags.AlwaysAutoResize, ImGuiWindowFlags.NoTitleBar, ImGuiWindowFlags.NoSavedSettings)) then
return true
end
table_.IsContextPopupOpen = false
return false
end
-- FIXME: Copied from MenuItem() for the purpose of being able to pass _SelectOnRelease (#9312)
local function MenuItemForColumnReorder(label, selected, enabled)
local g = GImGui
local window = g.CurrentWindow
local label_size = ImGui.CalcTextSize(label, nil, true)
local offsets = window.DC.MenuColumns
local checkmark_w = IM_TRUNC(g.FontSize * 1.20)
local min_w = offsets:DeclColumns(0.0, label_size.x, 0.0, checkmark_w) -- Feedback for next frame
local stretch_w = ImMax(0.0, ImGui.GetContentRegionAvail().x - min_w)
local text_pos = ImVec2(window.DC.CursorPos.x, window.DC.CursorPos.y + window.DC.CurrLineTextBaseOffset)
local id = ImGui.GetID(label)
local selectable_flags = bor(ImGuiSelectableFlags.SelectOnRelease, ImGuiSelectableFlags.SpanAvailWidth)
if g.ActiveId == id then
selectable_flags = bor(selectable_flags, ImGuiSelectableFlags.Highlight) -- Stays highlighted while dragging.
end
local has_been_moved = (g.ActiveId == id) and g.ActiveIdHasBeenEditedBefore -- But disable toggling once moved.
ImGui.BeginDisabled(not enabled) -- Don't use ImGuiSelectableFlags.Disabled so that Check mark is also affected.
local ret = ImGui.Selectable(label, false, selectable_flags, ImVec2(min_w, label_size.y)) and not has_been_moved -- Can't use IsMouseDragging(0) as button is released already.
if band(g.LastItemData.StatusFlags, ImGuiItemStatusFlags.Visible) ~= 0 and selected then
ImGui.RenderCheckMark(window.DrawList, text_pos + ImVec2(offsets.OffsetMark + stretch_w + g.FontSize * 0.40, g.FontSize * 0.134 * 0.5), ImGui.GetColorU32(ImGuiCol.Text), g.FontSize * 0.866)
end
ImGui.EndDisabled()
return ret
end
-- Output context menu into current window (generally a popup)
-- FIXME-TABLE: Ideally this should be writable by the user. Full programmatic access to that data?
-- Sections to display are pulled from 'flags_for_section_to_display', which is typically == table.Flags.
-- - ImGuiTableFlags.Resizable -> display Sizing menu items
-- - ImGuiTableFlags.Reorderable -> display "Reset Order"
---- ImGuiTableFlags.Sortable -> display sorting options (disabled)
-- - ImGuiTableFlags.Hideable -> display columns visibility menu items
-- It means if you have a custom context menus you can call this section and omit some sections, and add your own.
function ImGui.TableDrawDefaultContextMenu(table_, flags_for_section_to_display)
local g = GImGui
local window = g.CurrentWindow
if window.SkipItems then
return
end
local want_separator = false
local context_column_n = (table_.ContextPopupColumn >= 0 and table_.ContextPopupColumn < table_.ColumnsCount) and table_.ContextPopupColumn or -1
local context_column = (context_column_n ~= -1) and table_.Columns[context_column_n] or nil
-- Sizing
if band(flags_for_section_to_display, ImGuiTableFlags.Resizable) ~= 0 then
if context_column ~= nil then
local can_resize = band(context_column.Flags, ImGuiTableColumnFlags.NoResize) == 0 and context_column.IsEnabled
if ImGui.MenuItem(ImGui.LocalizeGetMsg(ImGuiLocKey.TableSizeOne), nil, false, can_resize) then -- "###SizeOne"
ImGui.TableSetColumnWidthAutoSingle(table_, context_column_n)
end
end
local size_all_desc
if table_.ColumnsEnabledFixedCount == table_.ColumnsEnabledCount and band(table_.Flags, ImGuiTableFlags.SizingMask_) ~= ImGuiTableFlags.SizingFixedSame then
size_all_desc = ImGui.LocalizeGetMsg(ImGuiLocKey.TableSizeAllFit) -- "###SizeAll" All fixed
else
size_all_desc = ImGui.LocalizeGetMsg(ImGuiLocKey.TableSizeAllDefault) -- "###SizeAll" All stretch or mixed
end
if ImGui.MenuItem(size_all_desc, nil) then
ImGui.TableSetColumnWidthAutoAll(table_)
end
want_separator = true
end
-- Reset Order/Visibility etc.
if band(flags_for_section_to_display, bor(ImGuiTableFlags.Reorderable, ImGuiTableFlags.Hideable)) ~= 0 then
if ImGui.BeginMenu(ImGui.LocalizeGetMsg(ImGuiLocKey.TableReset)) then
--if ImGui.MenuItem(ImGui.LocalizeGetMsg(ImGuiLocKey.TableResetAll)) then -- FIXME: Hiding because altering Sort Order + requesting auto-fit simultaneously has a tendency to exhibit a sizing glitch.
-- table_.IsResetAllRequest = true
--end
if band(flags_for_section_to_display, ImGuiTableFlags.Reorderable) ~= 0 then
if ImGui.MenuItem(ImGui.LocalizeGetMsg(ImGuiLocKey.TableResetOrder), nil, false, not table_.IsDefaultDisplayOrder) then -- PS: cannot be hidden because it would mess with drag reordering.
table_.IsResetDisplayOrderRequest = true
end
end
if band(flags_for_section_to_display, ImGuiTableFlags.Hideable) ~= 0 then
if ImGui.MenuItem(ImGui.LocalizeGetMsg(ImGuiLocKey.TableResetVisibility), nil, false, not table_.IsDefaultVisibility) then
table_.IsResetVisibilityRequest = true
end
end
ImGui.EndMenu()
end
end
-- Sorting
-- (modify TableOpenContextMenu() to add _Sortable flag if enabling this)
-- #if 0
--if band(flags_for_section_to_display, ImGuiTableFlags.Sortable) ~= 0 and column ~= nil and band(column.Flags, ImGuiTableColumnFlags.NoSort) == 0 then
-- if want_separator then ImGui.Separator() end
-- want_separator = true
-- local append_to_sort_specs = g.IO.KeyShift
-- if ImGui.MenuItem("Sort in Ascending Order", nil, column.SortOrder ~= -1 and column.SortDirection == ImGuiSortDirection.Ascending, band(column.Flags, ImGuiTableColumnFlags.NoSortAscending) == 0) then
-- ImGui.TableSetColumnSortDirection(table_, column_n, ImGuiSortDirection.Ascending, append_to_sort_specs)
-- end
-- if ImGui.MenuItem("Sort in Descending Order", nil, column.SortOrder ~= -1 and column.SortDirection == ImGuiSortDirection.Descending, band(column.Flags, ImGuiTableColumnFlags.NoSortDescending) == 0) then
-- ImGui.TableSetColumnSortDirection(table_, column_n, ImGuiSortDirection.Descending, append_to_sort_specs)
-- end
--end
-- #endif
-- Hiding / Visibility
if band(flags_for_section_to_display, ImGuiTableFlags.Hideable) ~= 0 then
if want_separator then
ImGui.Separator()
end
want_separator = true
-- While reordering: we calculate min/max allowed range once here so we can avoid a O(N log N) in the loop (because the query itself does a sweep scan).
-- This assume that reordering constraints output a single range, otherwise would need to either call TableGetMaxDisplayOrderAllowed() for each item below, or cache this once per frame into columns.
local is_reordering = (g.ActiveId ~= 0 and g.ActiveIdWindow == g.CurrentWindow and table_.ReorderColumn ~= -1 and g.ActiveIdHasBeenEditedBefore) -- FIXME: This is a bit of a hack.
local reorder_src_order = is_reordering and table_.Columns[table_.ReorderColumn].DisplayOrder or -1
local reorder_min_order = is_reordering and TableGetMaxDisplayOrderAllowed(table_, reorder_src_order, 0) or 0
local reorder_max_order = is_reordering and TableGetMaxDisplayOrderAllowed(table_, reorder_src_order, table_.ColumnsCount - 1) or (table_.ColumnsCount - 1)
ImGui.PushItemFlag(ImGuiItemFlags.AutoClosePopups, false)
for order_n = 0, table_.ColumnsCount - 1 do
local column_n = table_.DisplayOrderToIndex[order_n]
local column = table_.Columns[column_n]
if band(column.Flags, ImGuiTableColumnFlags.Disabled) == 0 then
local name = ImGui.TableGetColumnName(table_, column_n)
if name == nil or name == "" then
name = "<Unknown>"
end
-- Make sure we can't hide the last active column
local menu_item_enabled = band(column.Flags, ImGuiTableColumnFlags.NoHide) == 0
if column.IsUserEnabled and table_.ColumnsEnabledCount <= 1 then
menu_item_enabled = false
end
if is_reordering and (column.DisplayOrder < reorder_min_order or column.DisplayOrder > reorder_max_order) then
menu_item_enabled = false
end
if MenuItemForColumnReorder(name, column.IsUserEnabled, menu_item_enabled) then
column.IsUserEnabledNextFrame = not column.IsUserEnabled
end
-- Drag to reorder
-- FIXME: It is currently not possible to reorder columns marked with ImGuiTableColumnFlags.NoHide.
if ImGui.IsItemActive() and ImGui.IsMouseDragging(0) and g.ActiveIdSource == ImGuiInputSource.Mouse and band(table_.Flags, ImGuiTableFlags.Reorderable) ~= 0 then
g.ActiveIdHasBeenEditedBefore = true -- Disable toggle in MenuItemForColumnReorder() + start dimming to display allowed reorder targets.
table_.ReorderColumn = column_n
if not ImGui.IsItemHovered() then
local reorder_dir = (g.IO.MousePos.y < (g.LastItemData.Rect.Min.y + g.LastItemData.Rect.Max.y) * 0.5) and -1 or 1
local reorder_amount = ((reorder_dir < 0) and (g.LastItemData.Rect.Min.y - g.IO.MousePos.y) or (g.IO.MousePos.y - g.LastItemData.Rect.Max.y)) / g.LastItemData.Rect:GetHeight()
local dst_order = column.DisplayOrder + ImTrunc(ImCeil(reorder_amount)) * reorder_dir -- Estimated target order, will be validated and clamped.
ImGui.TableQueueSetColumnDisplayOrder(table_, column_n, dst_order)
end
end
end
end
ImGui.PopItemFlag()
end
end
-------------------------------------------------------------------------------
-- [SECTION] Tables: Settings (.ini data)
-------------------------------------------------------------------------------
-- FIXME: The binding/finding/creating flow are too confusing.
-------------------------------------------------------------------------------
-- - TableSettingsInit() [Internal]
-- - TableSettingsCreate() [Internal]
-- - TableSettingsFindByID() [Internal]
-- - TableGetBoundSettings() [Internal]
-- - TableResetSettings()
-- - TableSaveSettings() [Internal]
-- - TableLoadSettings() [Internal]
-- - TableLoadSettingsForColumns() [Internal]
-- - TableSettingsHandler_ClearAll() [Internal]
-- - TableSettingsHandler_ApplyAll() [Internal]
-- - TableSettingsHandler_ReadOpen() [Internal]
-- - TableSettingsHandler_ReadLine() [Internal]
-- - TableSettingsHandler_WriteAll() [Internal]
-- - TableSettingsAddSettingsHandler() [Internal]
-------------------------------------------------------------------------------
-- [Init] 1: TableSettingsHandler_ReadXXXX() Load and parse .ini file into TableSettings.
-- [Main] 2: TableLoadSettings() When table is created, bind Table to TableSettings, serialize TableSettings data into Table.
-- [Main] 3: TableSaveSettings() When table properties are modified, serialize Table data into bound or new TableSettings, mark .ini as dirty.
-- [Main] 4: TableSettingsHandler_WriteAll() When .ini file is dirty (which can come from other source), save TableSettings into .ini file.
-------------------------------------------------------------------------------
-- Clear and initialize empty settings instance
local function TableSettingsInit(settings, id, columns_count, columns_count_max)
for k, v in pairs(ImGuiTableSettings()) do settings[k] = v end
for n = 0, columns_count_max - 1 do
settings.Columns[n] = ImGuiTableColumnSettings()
end
settings.ID = id
settings.ColumnsCount = columns_count
settings.ColumnsCountMax = columns_count_max
settings.WantApply = true
end
function ImGui.TableSettingsCreate(id, columns_count)
local g = GImGui
local settings = ImGui.TableSettingsFindByID(id)
if settings then
if settings.ColumnsCountMax >= columns_count then
TableSettingsInit(settings, id, columns_count, settings.ColumnsCountMax) -- Recycle
return settings
end
settings.ID = 0 -- Invalidate storage, we won't fit because of a count change
end
settings = ImGuiTableSettings()
g.SettingsTables:push_back(settings)
TableSettingsInit(settings, id, columns_count, columns_count)
return settings
end
-- Find existing settings
function ImGui.TableSettingsFindByID(id)
-- FIXME-OPT: Might want to store a lookup map for this?
local g = GImGui
for _, settings in g.SettingsTables:iter() do
if settings.ID == id then
return settings
end
end
return nil
end
local function SettingsTablesOffsetFromPtr(settings)
local g = GImGui
return g.SettingsTables:find_index(settings) or -1
end
-- Get settings for a given table, nil if none
function ImGui.TableGetBoundSettings(table_)
if table_.SettingsOffset == -1 then
return nil
end
local g = GImGui
local settings = g.SettingsTables.Data[table_.SettingsOffset]
IM_ASSERT(settings.ID == table_.ID)
return settings
end
-- Restore initial state of table (with or without saved settings)
function ImGui.TableResetSettings(table_)
table_.IsInitializing = true; table_.IsSettingsDirty = true
table_.IsResetAllRequest = false
table_.IsSettingsRequestLoad = false -- Don't reload from ini
table_.SettingsLoadedFlags = ImGuiTableFlags.None -- Mark as nothing loaded so our initialized data becomes authoritative
end
function ImGui.TableSaveSettings(table_)
table_.IsSettingsDirty = false
if band(table_.Flags, ImGuiTableFlags.NoSavedSettings) ~= 0 then
return
end
-- Bind or create settings data
local settings = ImGui.TableGetBoundSettings(table_)
if settings ~= nil and table_.ColumnsCount > settings.ColumnsCountMax then
settings.ID = 0 -- Invalidate storage, we won't fit because of a count change
settings = nil
end
if settings == nil then
settings = ImGui.TableSettingsCreate(table_.ID, table_.ColumnsCount)
table_.SettingsOffset = SettingsTablesOffsetFromPtr(settings)
end
settings.ColumnsCount = table_.ColumnsCount
-- Serialize ImGuiTable/ImGuiTableColumn into ImGuiTableSettings/ImGuiTableColumnSettings
IM_ASSERT(settings.ID == table_.ID)
IM_ASSERT(settings.ColumnsCount == table_.ColumnsCount and settings.ColumnsCountMax >= settings.ColumnsCount)
local save_ref_scale = false
settings.SaveFlags = ImGuiTableFlags.None
for n = 0, table_.ColumnsCount - 1 do
local column = table_.Columns[n]
local column_settings = settings.Columns[n]
local width_or_weight = (band(column.Flags, ImGuiTableColumnFlags.WidthStretch) ~= 0) and column.StretchWeight or column.WidthRequest
column_settings.ID = column.ID
column_settings.WidthOrWeight = width_or_weight
column_settings.Index = n
column_settings.DisplayOrder = column.DisplayOrder
column_settings.SortOrder = column.SortOrder
column_settings.SortDirection = column.SortDirection
column_settings.IsEnabled = column.IsUserEnabled and 1 or 0
column_settings.IsStretch = (band(column.Flags, ImGuiTableColumnFlags.WidthStretch) ~= 0) and 1 or 0
if band(column.Flags, ImGuiTableColumnFlags.WidthStretch) == 0 then
save_ref_scale = true
end
-- We skip saving some data in the .ini file when they are unnecessary to restore our state.
-- Note that fixed width where initial width was derived from auto-fit will always be saved as InitStretchWeightOrWidth will be 0.0f.
-- FIXME-TABLE: We don't have logic to easily compare SortOrder to DefaultSortOrder yet so it's always saved when present.
if width_or_weight ~= column.InitStretchWeightOrWidth then
settings.SaveFlags = bor(settings.SaveFlags, ImGuiTableFlags.Resizable)
end
if column.DisplayOrder ~= n then
settings.SaveFlags = bor(settings.SaveFlags, ImGuiTableFlags.Reorderable)
end
if column.SortOrder ~= -1 then
settings.SaveFlags = bor(settings.SaveFlags, ImGuiTableFlags.Sortable, ImGuiTableFlags.Reorderable) -- Because SortOrder saving itself is gated, make sure every column is saved (#9519)
end
if column.IsUserEnabled ~= (band(column.Flags, ImGuiTableColumnFlags.DefaultHide) == 0) then
settings.SaveFlags = bor(settings.SaveFlags, ImGuiTableFlags.Hideable)
end
end
settings.SaveFlags = band(settings.SaveFlags, table_.Flags)
settings.RefScale = save_ref_scale and table_.RefScale or 0.0
ImGui.MarkIniSettingsDirty()
end
function ImGui.TableLoadSettings(table_)
if band(table_.Flags, ImGuiTableFlags.NoSavedSettings) ~= 0 then
table_.IsSettingsRequestLoad = false -- Done
return
end
-- Bind settings
local settings
if table_.SettingsOffset == -1 then
settings = ImGui.TableSettingsFindByID(table_.ID)
if settings == nil then
return
end
if settings.ColumnsCount ~= table_.ColumnsCount then -- Allow settings if columns count changed. We could otherwise decide to return...
table_.IsSettingsDirty = true
end
table_.SettingsOffset = SettingsTablesOffsetFromPtr(settings)
else
settings = ImGui.TableGetBoundSettings(table_)
end
table_.SettingsLoadedFlags = settings.SaveFlags
table_.RefScale = settings.RefScale
-- TableUpdateLayout() will then call TableLoadSettingsForColumns() to apply the data.
end
-- NB: This was written to be similar to the logic in TableReconcileColumns().
function ImGui.TableLoadSettingsForColumns(table_)
for n = 0, table_.ColumnsSize - 1 do
table_.Columns[n].IsLoadedSettings = false
end
local settings = ImGui.TableGetBoundSettings(table_)
if settings == nil then
return
end
table_.SettingsLoadedFlags = bor(table_.SettingsLoadedFlags, ImGuiTableFlags.Reorderable) -- We handle above in code above.
-- Serialize ImGuiTableSettings/ImGuiTableColumnSettings into ImGuiTable/ImGuiTableColumn
local column_settings = settings.Columns
-- Fast path
local matches = 0
for n = 0, table_.ColumnsCount - 1 do
if n >= settings.ColumnsCount or column_settings[n].ID ~= table_.Columns[n].ID then
break
end
ImGui.TableLoadSettingsForColumn(table_.Columns[n], column_settings[n], settings.SaveFlags)
matches = matches + 1
end
if matches == settings.ColumnsCount then
return
end
local settings_start_n = matches -- Small optimization
for n = settings_start_n, settings.ColumnsCount - 1 do
column_settings[n].IsLoaded = false
end
-- Find matches for named columns
for c = 0, table_.ColumnsSize - 1 do
local column = table_.Columns[c]
if column.ID ~= 0 and not column.IsLoadedSettings then
for n = settings_start_n, settings.ColumnsCount - 1 do
if column_settings[n].ID == column.ID and not column_settings[n].IsLoaded then
ImGui.TableLoadSettingsForColumn(column, column_settings[n], settings.SaveFlags)
matches = matches + 1
break
end
end
end
end
-- Remaining entries are matched sequentially
local dst_idx = 0
for n = settings_start_n, settings.ColumnsCount - 1 do
if not column_settings[n].IsLoaded then
while dst_idx < table_.ColumnsCount and table_.Columns[dst_idx].IsLoadedSettings do
dst_idx = dst_idx + 1
end
if dst_idx >= table_.ColumnsCount then
break
end
ImGui.TableLoadSettingsForColumn(table_.Columns[dst_idx], column_settings[n], settings.SaveFlags)
dst_idx = dst_idx + 1
end
end
end
function ImGui.TableLoadSettingsForColumn(column, column_settings, load_flags)
column.IsLoadedSettings = true
column_settings.IsLoaded = true
if band(load_flags, ImGuiTableFlags.Resizable) ~= 0 then
if column_settings.IsStretch ~= 0 then
column.StretchWeight = column_settings.WidthOrWeight
else
column.WidthRequest = column_settings.WidthOrWeight
end
column.AutoFitQueue = 0x00
end
if band(load_flags, ImGuiTableFlags.Reorderable) ~= 0 then
column.DisplayOrder = column_settings.DisplayOrder
else
column.DisplayOrder = column_settings.Index -- Because default depends on previous Index, we need to set that up and cannot rely on TableInitColumnDefaults()
end
if band(load_flags, ImGuiTableFlags.Hideable) ~= 0 and column_settings.IsEnabled ~= -1 then
local v = (column_settings.IsEnabled == 1)
column.IsUserEnabled = v; column.IsUserEnabledNextFrame = v
end
column.SortOrder = column_settings.SortOrder
column.SortDirection = column_settings.SortDirection
end
-- Fix invalid display order data: compact values (0,1,3 -> 0,1,2); preserve relative order (0,3,1 -> 0,2,1); deduplicate (0,4,1,1 -> 0,3,1,2)
function ImGui.TableFixDisplayOrder(table_)
if band(table_.Flags, ImGuiTableFlags.Reorderable) == 0 then
for n = 0, table_.ColumnsCount - 1 do
table_.Columns[n].DisplayOrder = n
table_.DisplayOrderToIndex[n] = n
end
return
end
local fdo_columns = {}
for n = 0, table_.ColumnsCount - 1 do
fdo_columns[n + 1] = n
end
-- Sort by DisplayOrder and then Index
-- Assume ImGuiTableColumnIdx == signed short, to turn -1 into a large value so that it always sort after.
local function key(idx)
local o = table_.Columns[idx].DisplayOrder
if o < 0 then o = o + 0x10000 end
return o
end
table.sort(fdo_columns, function(lhs_idx, rhs_idx)
local a, b = key(lhs_idx), key(rhs_idx)
if a ~= b then return a < b end
return lhs_idx < rhs_idx
end)
for n = 0, table_.ColumnsCount - 1 do
table_.Columns[fdo_columns[n + 1]].DisplayOrder = n
end
for n = 0, table_.ColumnsCount - 1 do
table_.DisplayOrderToIndex[table_.Columns[n].DisplayOrder] = n
end
end
local function ForEachTable(g, fn)
for i = 0, g.Tables:GetMapSize() - 1 do
local t = g.Tables:GetByIndex(i)
if t then fn(t) end
end
end
local function TableSettingsHandler_ClearAll(ctx, handler)
local g = ctx
ForEachTable(g, function(t) t.SettingsOffset = -1 end)
g.SettingsTables:clear()
end
-- Apply to existing windows (if any)
local function TableSettingsHandler_ApplyAll(ctx, handler)
local g = ctx
ForEachTable(g, function(t)
t.IsSettingsRequestLoad = true
t.SettingsOffset = -1
end)
end
local function TableSettingsHandler_ReadOpen(ctx, handler, name)
-- FIXME: As topology changes are allowed, strictly speaking a >= IMGUI_TABLE_MAX_COLUMNS tables stored in .ini file
-- that was emitted with a higher max count could still be meaningful in some unlikely cases.
-- We might want to use another MAX defined as (1<<(sizeof(ImGuiTableColumnIdx)*8-1))-1.
local id, columns_count = string.match(name, "^0x(%x+),(%-?%d+)")
if id == nil then
return nil
end
id = tonumber(id, 16); columns_count = tonumber(columns_count)
if columns_count <= 0 or columns_count >= IMGUI_TABLE_MAX_COLUMNS then
return nil
end
return ImGui.TableSettingsCreate(id, columns_count)
end
local function TableSettingsHandler_ReadLine(ctx, handler, settings, line)
-- "Column 0 UserID=0x42AD2D21 Width=100 Visible=1 Order=0 Sort=0v"
if settings == nil then return end
local f = string.match(line, "^RefScale=([%-%d%.eE+]+)")
if f then settings.RefScale = tonumber(f); return end
local n = string.match(line, "^LastUsed=(%d+)")
if n then settings.LastUsedDate = tonumber(n); return end
local column_n, rest = string.match(line, "^Column (%-?%d+)(.*)$")
if column_n then
column_n = tonumber(column_n)
if column_n < 0 or column_n >= settings.ColumnsCount then
return
end
local column = settings.Columns[column_n]
column.Index = column_n
local function take(pattern)
local a, b, r = string.match(rest, "^%s*" .. pattern .. "()")
if a == nil then return nil end
if r == nil then r = b; b = nil end
rest = string.sub(rest, r)
return a, b
end
take("UserID=0x(%x+)") -- FIXME-LEGACY: Removed 2025/11/12, was never properly set.
local v = take("Width=(%-?%d+)")
if v then column.WidthOrWeight = tonumber(v); column.IsStretch = 0; settings.SaveFlags = bor(settings.SaveFlags, ImGuiTableFlags.Resizable) end
v = take("Weight=([%-%d%.eE+]+)")
if v then column.WidthOrWeight = tonumber(v); column.IsStretch = 1; settings.SaveFlags = bor(settings.SaveFlags, ImGuiTableFlags.Resizable) end
v = take("Visible=(%-?%d+)")
if v then column.IsEnabled = tonumber(v); settings.SaveFlags = bor(settings.SaveFlags, ImGuiTableFlags.Hideable) end
v = take("Order=(%-?%d+)")
if v then column.DisplayOrder = tonumber(v); settings.SaveFlags = bor(settings.SaveFlags, ImGuiTableFlags.Reorderable) end
local c
v, c = take("Sort=(%-?%d+)(.)")
if v then column.SortOrder = tonumber(v); column.SortDirection = (c == "^") and ImGuiSortDirection.Descending or ImGuiSortDirection.Ascending; settings.SaveFlags = bor(settings.SaveFlags, ImGuiTableFlags.Sortable) end
v = take("ID=0x(%x+)")
if v then column.ID = tonumber(v, 16) end
end
end
local function TableSettingsHandler_WriteAll(ctx, handler, buf)
local g = ctx
for _, settings in g.SettingsTables:iter() do
if settings.ID ~= 0 then -- Skip ditched settings
-- TableSaveSettings() may clear some of those flags when we establish that the data can be stripped
-- (e.g. Order was unchanged)
local save_size = band(settings.SaveFlags, ImGuiTableFlags.Resizable) ~= 0
local save_visible = band(settings.SaveFlags, ImGuiTableFlags.Hideable) ~= 0
local save_order = band(settings.SaveFlags, ImGuiTableFlags.Reorderable) ~= 0
local save_sort = band(settings.SaveFlags, ImGuiTableFlags.Sortable) ~= 0
-- We need to save the [Table] entry even if all the bools are false, since this records a table with "default settings".
buf:appendf("[%s][0x%08X,%d]\n", handler.TypeName, settings.ID, settings.ColumnsCount)
if settings.RefScale ~= 0.0 then
buf:appendf("RefScale=%g\n", settings.RefScale)
end
for column_n = 0, settings.ColumnsCount - 1 do
local column = settings.Columns[column_n]
-- "Column 0 UserID=0x42AD2D21 Width=100 Visible=1 Order=0 Sort=0v"
local save_column = save_size or save_visible or save_order or (save_sort and column.SortOrder ~= -1)
if save_column then
buf:appendf("Column %-2d", column_n)
if save_size and column.IsStretch ~= 0 then buf:appendf(" Weight=%.4f", column.WidthOrWeight) end
if save_size and column.IsStretch == 0 then buf:appendf(" Width=%d", ImTrunc(column.WidthOrWeight)) end
if save_visible then buf:appendf(" Visible=%d", column.IsEnabled) end
if save_order then buf:appendf(" Order=%d", column.DisplayOrder) end
if save_sort and column.SortOrder ~= -1 then buf:appendf(" Sort=%d%s", column.SortOrder, (column.SortDirection == ImGuiSortDirection.Ascending) and "v" or "^") end
if column.ID ~= 0 then buf:appendf(" ID=0x%08X", column.ID) end
buf:append("\n")
end
end
if g.IO.ConfigIniSettingsSaveLastUsedDate and settings.LastUsedDate ~= 0 then
buf:appendf("LastUsed=%08d\n", settings.LastUsedDate)
end
buf:append("\n")
end
end
end
function ImGui.TableSettingsAddSettingsHandler()
local ini_handler = ImGuiSettingsHandler()
ini_handler.TypeName = "Table"
ini_handler.TypeHash = ImHashStr("Table")
ini_handler.ClearAllFn = TableSettingsHandler_ClearAll
ini_handler.ReadOpenFn = TableSettingsHandler_ReadOpen
ini_handler.ReadLineFn = TableSettingsHandler_ReadLine
ini_handler.ApplyAllFn = TableSettingsHandler_ApplyAll
ini_handler.WriteAllFn = TableSettingsHandler_WriteAll
ImGui.AddSettingsHandler(ini_handler)
end
-------------------------------------------------------------------------------
-- [SECTION] Tables: Garbage Collection
-------------------------------------------------------------------------------
-- - TableRemove() [Internal]
-- - TableGcCompactTransientBuffers() [Internal]
-- - TableGcCompactSettings() [Internal]
-------------------------------------------------------------------------------
-- Remove Table data (currently only used by TestEngine)
function ImGui.TableRemove(table_)
--IMGUI_DEBUG_PRINT("TableRemove() id=0x%08X\n", table_.ID)
local g = GImGui
local table_idx = g.Tables:GetIndex(table_)
g.Tables:Remove(table_.ID, table_)
g.TablesLastTimeActive.Data[table_idx + 1] = -1.0
end
-- Free up/compact internal Table buffers for when it gets unused
-- Overloads: (ImGuiTable) / (ImGuiTableTempData)
function ImGui.TableGcCompactTransientBuffers(t)
if not t then return end
if t.TableIndex ~= nil then -- ImGuiTableTempData
t.AngledHeadersRequests:clear()
t.DrawSplitter:ClearFreeMemory()
t.LastTimeActive = -1.0
return
end
--IMGUI_DEBUG_PRINT("TableGcCompactTransientBuffers() id=0x%08X\n", t.ID)
local g = GImGui
local table_ = t
IM_ASSERT(table_.MemoryCompacted == false)
table_.SortSpecs.Specs = nil
table.clear(table_.SortSpecsMulti)
table_.IsSortSpecsDirty = true -- FIXME: In theory shouldn't have to leak into user performing a sort on resume.
table.clear(table_.ColumnsNames)
table_.MemoryCompacted = true
for n = 0, table_.ColumnsCount - 1 do
table_.Columns[n].NameOffset = -1
end
g.TablesLastTimeActive.Data[g.Tables:GetIndex(table_) + 1] = -1.0
end
-- Compact and remove unused or resize settings data (only TestEngine mark unused, but resizing table down can lead to compaction)
function ImGui.TableGcCompactSettings()
local g = GImGui
local has_dead = false
for _, settings in g.SettingsTables:iter() do
if settings.ID == 0 then has_dead = true end
end
if not has_dead then
return
end
local new_list = ImVector()
for _, settings in g.SettingsTables:iter() do
if settings.ID ~= 0 then new_list:push_back(settings) end
end
g.SettingsTables = new_list
ForEachTable(g, function(t) t.SettingsOffset = -1 end)
end
-------------------------------------------------------------------------------
-- [SECTION] Columns, BeginColumns, EndColumns, etc.
-- (This is a legacy API, prefer using BeginTable/EndTable!)
-------------------------------------------------------------------------------
-- FIXME: sizing is lossy when columns width is very small (default width may turn negative etc.)
-------------------------------------------------------------------------------
-- - GetColumnIndex()
-- - GetColumnsCount()
-- - GetColumnOffset()
-- - GetColumnWidth()
-- - SetColumnOffset()
-- - SetColumnWidth()
-- - PushColumnClipRect() [Internal]
-- - PushColumnsBackground() [Internal]
-- - PopColumnsBackground() [Internal]
-- - FindOrCreateColumns() [Internal]
-- - GetColumnsID() [Internal]
-- - BeginColumns()
-- - NextColumn()
-- - EndColumns()
-- - Columns()
-------------------------------------------------------------------------------
-- (Lua: columns.Columns is an ImVector, column n lives in .Data[n + 1])
function ImGui.GetColumnIndex()
local window = ImGui.GetCurrentWindowRead()
return window.DC.CurrentColumns and window.DC.CurrentColumns.Current or 0
end
function ImGui.GetColumnsCount()
local window = ImGui.GetCurrentWindowRead()
return window.DC.CurrentColumns and window.DC.CurrentColumns.Count or 1
end
function ImGui.GetColumnOffsetFromNorm(columns, offset_norm)
return offset_norm * (columns.OffMaxX - columns.OffMinX)
end
function ImGui.GetColumnNormFromOffset(columns, offset)
return offset / (columns.OffMaxX - columns.OffMinX)
end
local COLUMNS_HIT_RECT_HALF_THICKNESS = 4.0
local function GetDraggedColumnOffset(columns, column_index)
-- Active (dragged) column always follow mouse. The reason we need this is that dragging a column to the right edge of an auto-resizing
-- window creates a feedback loop because we store normalized positions. So while dragging we enforce absolute positioning.
local g = GImGui
local window = g.CurrentWindow
IM_ASSERT(column_index > 0) -- We are not supposed to drag column 0.
IM_ASSERT(g.ActiveId == band(columns.ID + column_index, 0xFFFFFFFF))
local x = g.IO.MousePos.x - g.ActiveIdClickOffset.x + ImTrunc(COLUMNS_HIT_RECT_HALF_THICKNESS * g.CurrentDpiScale) - window.Pos.x
x = ImMax(x, ImGui.GetColumnOffset(column_index - 1) + g.Style.ColumnsMinSpacing)
if band(columns.Flags, ImGuiOldColumnFlags.NoPreserveWidths) ~= 0 then
x = ImMin(x, ImGui.GetColumnOffset(column_index + 1) - g.Style.ColumnsMinSpacing)
end
return x
end
function ImGui.GetColumnOffset(column_index)
if column_index == nil then column_index = -1 end
local window = ImGui.GetCurrentWindowRead()
local columns = window.DC.CurrentColumns
if columns == nil then
return 0.0
end
if column_index < 0 then
column_index = columns.Current
end
IM_ASSERT(column_index < columns.Columns.Size)
local t = columns.Columns.Data[column_index + 1].OffsetNorm
local x_offset = ImLerp(columns.OffMinX, columns.OffMaxX, t)
return x_offset
end
local function GetColumnWidthEx(columns, column_index, before_resize)
if column_index < 0 then
column_index = columns.Current
end
local d = columns.Columns.Data
local offset_norm
if before_resize then
offset_norm = d[column_index + 2].OffsetNormBeforeResize - d[column_index + 1].OffsetNormBeforeResize
else
offset_norm = d[column_index + 2].OffsetNorm - d[column_index + 1].OffsetNorm
end
return ImGui.GetColumnOffsetFromNorm(columns, offset_norm)
end
function ImGui.GetColumnWidth(column_index)
if column_index == nil then column_index = -1 end
local g = GImGui
local window = g.CurrentWindow
local columns = window.DC.CurrentColumns
if columns == nil then
return ImGui.GetContentRegionAvail().x
end
if column_index < 0 then
column_index = columns.Current
end
local d = columns.Columns.Data
return ImGui.GetColumnOffsetFromNorm(columns, d[column_index + 2].OffsetNorm - d[column_index + 1].OffsetNorm)
end
function ImGui.SetColumnOffset(column_index, offset)
local g = GImGui
local window = g.CurrentWindow
local columns = window.DC.CurrentColumns
IM_ASSERT(columns ~= nil)
if column_index < 0 then
column_index = columns.Current
end
IM_ASSERT(column_index < columns.Columns.Size)
local preserve_width = band(columns.Flags, ImGuiOldColumnFlags.NoPreserveWidths) == 0 and (column_index < columns.Count - 1)
local width = preserve_width and GetColumnWidthEx(columns, column_index, columns.IsBeingResized) or 0.0
if band(columns.Flags, ImGuiOldColumnFlags.NoForceWithinWindow) == 0 then
offset = ImMin(offset, columns.OffMaxX - g.Style.ColumnsMinSpacing * (columns.Count - column_index))
end
columns.Columns.Data[column_index + 1].OffsetNorm = ImGui.GetColumnNormFromOffset(columns, offset - columns.OffMinX)
if preserve_width then
ImGui.SetColumnOffset(column_index + 1, offset + ImMax(g.Style.ColumnsMinSpacing, width))
end
end
function ImGui.SetColumnWidth(column_index, width)
local window = ImGui.GetCurrentWindowRead()
local columns = window.DC.CurrentColumns
IM_ASSERT(columns ~= nil)
if column_index < 0 then
column_index = columns.Current
end
ImGui.SetColumnOffset(column_index + 1, ImGui.GetColumnOffset(column_index) + width)
end
function ImGui.PushColumnClipRect(column_index)
local window = ImGui.GetCurrentWindowRead()
local columns = window.DC.CurrentColumns
if column_index < 0 then
column_index = columns.Current
end
local column = columns.Columns.Data[column_index + 1]
ImGui.PushClipRect(column.ClipRect.Min, column.ClipRect.Max, false)
end
-- Get into the columns background draw command (which is generally the same draw command as before we called BeginColumns)
function ImGui.PushColumnsBackground()
local window = ImGui.GetCurrentWindowRead()
local columns = window.DC.CurrentColumns
if columns.Count == 1 then
return
end
-- Optimization: avoid SetCurrentChannel() + PushClipRect()
ImRect_Copy(columns.HostBackupClipRect, window.ClipRect)
ImGui.SetWindowClipRectBeforeSetChannel(window, columns.HostInitialClipRect)
columns.Splitter:SetCurrentChannel(window.DrawList, 0)
end
function ImGui.PopColumnsBackground()
local window = ImGui.GetCurrentWindowRead()
local columns = window.DC.CurrentColumns
if columns.Count == 1 then
return
end
-- Optimization: avoid PopClipRect() + SetCurrentChannel()
ImGui.SetWindowClipRectBeforeSetChannel(window, columns.HostBackupClipRect)
columns.Splitter:SetCurrentChannel(window.DrawList, columns.Current + 1)
end
function ImGui.FindOrCreateColumns(window, id)
-- We have few columns per window so for now we don't need bother much with turning this into a faster lookup.
for _, c in window.ColumnsStorage:iter() do
if c.ID == id then
return c
end
end
local columns = ImGuiOldColumns()
window.ColumnsStorage:push_back(columns)
columns.ID = id
return columns
end
function ImGui.GetColumnsID(str_id, columns_count)
local window = ImGui.GetCurrentWindow()
-- Differentiate column ID with an arbitrary prefix for cases where users name their columns set the same as another widget.
-- In addition, when an identifier isn't explicitly provided we include the number of columns in the hash to make it uniquer.
ImGui.PushID(0x11223347 + (str_id and 0 or columns_count))
local id = window:GetID(str_id or "columns")
ImGui.PopID()
return id
end
function ImGui.BeginColumns(str_id, columns_count, flags)
if flags == nil then flags = 0 end
local g = GImGui
local window = ImGui.GetCurrentWindow()
IM_ASSERT(columns_count >= 1)
IM_ASSERT(window.DC.CurrentColumns == nil) -- Nested columns are currently not supported
-- Acquire storage for the columns set
local id = ImGui.GetColumnsID(str_id, columns_count)
local columns = ImGui.FindOrCreateColumns(window, id)
IM_ASSERT(columns.ID == id)
columns.Current = 0
columns.Count = columns_count
columns.Flags = flags
window.DC.CurrentColumns = columns
window.DC.NavIsScrollPushableX = false -- Shortcut for NavUpdateCurrentWindowIsScrollPushableX();
columns.HostCursorPosY = window.DC.CursorPos.y
columns.HostCursorMaxPosX = window.DC.CursorMaxPos.x
ImRect_Copy(columns.HostInitialClipRect, window.ClipRect)
ImRect_Copy(columns.HostBackupParentWorkRect, window.ParentWorkRect)
ImRect_Copy(window.ParentWorkRect, window.WorkRect)
-- Set state for first column
-- We aim so that the right-most column will have the same clipping width as other after being clipped by parent ClipRect
local column_padding = g.Style.ItemSpacing.x
local half_clip_extend_x = ImTrunc(ImMax(window.WindowPadding.x * 0.5, window.WindowBorderSize))
local max_1 = window.WorkRect.Max.x + column_padding - ImMax(column_padding - window.WindowPadding.x, 0.0)
local max_2 = window.WorkRect.Max.x + half_clip_extend_x
columns.OffMinX = window.DC.Indent.x - column_padding + ImMax(column_padding - window.WindowPadding.x, 0.0)
columns.OffMaxX = ImMax(ImMin(max_1, max_2) - window.Pos.x, columns.OffMinX + 1.0)
columns.LineMinY = window.DC.CursorPos.y; columns.LineMaxY = window.DC.CursorPos.y
-- Clear data if columns count changed
if columns.Columns.Size ~= 0 and columns.Columns.Size ~= columns_count + 1 then
columns.Columns:resize(0)
end
-- Initialize default widths
columns.IsFirstFrame = (columns.Columns.Size == 0)
if columns.Columns.Size == 0 then
for n = 0, columns_count do
local column = ImGuiOldColumnData()
column.OffsetNorm = n / columns_count
columns.Columns:push_back(column)
end
end
for n = 0, columns_count - 1 do
-- Compute clipping rectangle
local column = columns.Columns.Data[n + 1]
local clip_x1 = IM_ROUND(window.Pos.x + ImGui.GetColumnOffset(n))
local clip_x2 = IM_ROUND(window.Pos.x + ImGui.GetColumnOffset(n + 1) - 1.0)
column.ClipRect = ImRect(clip_x1, -FLT_MAX, clip_x2, FLT_MAX)
column.ClipRect:ClipWithFull(window.ClipRect)
end
if columns.Count > 1 then
columns.Splitter:Split(window.DrawList, 1 + columns.Count)
columns.Splitter:SetCurrentChannel(window.DrawList, 1)
ImGui.PushColumnClipRect(0)
end
-- We don't generally store Indent.x inside ColumnsOffset because it may be manipulated by the user.
local offset_0 = ImGui.GetColumnOffset(columns.Current)
local offset_1 = ImGui.GetColumnOffset(columns.Current + 1)
local width = offset_1 - offset_0
ImGui.PushItemWidth(width * 0.65)
window.DC.ColumnsOffset.x = ImMax(column_padding - window.WindowPadding.x, 0.0)
window.DC.CursorPos.x = IM_TRUNC(window.Pos.x + window.DC.Indent.x + window.DC.ColumnsOffset.x)
window.WorkRect.Max.x = window.Pos.x + offset_1 - column_padding
window.WorkRect.Max.y = window.ContentRegionRect.Max.y
end
function ImGui.NextColumn()
local window = ImGui.GetCurrentWindow()
if window.SkipItems or window.DC.CurrentColumns == nil then
return
end
local g = GImGui
local columns = window.DC.CurrentColumns
if columns.Count == 1 then
window.DC.CursorPos.x = IM_TRUNC(window.Pos.x + window.DC.Indent.x + window.DC.ColumnsOffset.x)
IM_ASSERT(columns.Current == 0)
return
end
-- Next column
columns.Current = columns.Current + 1
if columns.Current == columns.Count then
columns.Current = 0
end
ImGui.PopItemWidth()
-- Optimization: avoid PopClipRect() + SetCurrentChannel() + PushClipRect()
-- (which would needlessly attempt to update commands in the wrong channel, then pop or overwrite them),
local column = columns.Columns.Data[columns.Current + 1]
ImGui.SetWindowClipRectBeforeSetChannel(window, column.ClipRect)
columns.Splitter:SetCurrentChannel(window.DrawList, columns.Current + 1)
local column_padding = g.Style.ItemSpacing.x
columns.LineMaxY = ImMax(columns.LineMaxY, window.DC.CursorPos.y)
if columns.Current > 0 then
-- Columns 1+ ignore IndentX (by canceling it out)
-- FIXME-COLUMNS: Unnecessary, could be locked?
window.DC.ColumnsOffset.x = ImGui.GetColumnOffset(columns.Current) - window.DC.Indent.x + column_padding
else
-- New row/line: column 0 honor IndentX.
window.DC.ColumnsOffset.x = ImMax(column_padding - window.WindowPadding.x, 0.0)
window.DC.IsSameLine = false
columns.LineMinY = columns.LineMaxY
end
window.DC.CursorPos.x = IM_TRUNC(window.Pos.x + window.DC.Indent.x + window.DC.ColumnsOffset.x)
window.DC.CursorPos.y = columns.LineMinY
window.DC.CurrLineSize.x = 0.0; window.DC.CurrLineSize.y = 0.0
window.DC.CurrLineTextBaseOffset = 0.0
-- FIXME-COLUMNS: Share code with BeginColumns() - move code on columns setup.
local offset_0 = ImGui.GetColumnOffset(columns.Current)
local offset_1 = ImGui.GetColumnOffset(columns.Current + 1)
local width = offset_1 - offset_0
ImGui.PushItemWidth(width * 0.65)
window.WorkRect.Max.x = window.Pos.x + offset_1 - column_padding
end
function ImGui.EndColumns()
local g = GImGui
local window = ImGui.GetCurrentWindow()
local columns = window.DC.CurrentColumns
IM_ASSERT(columns ~= nil)
ImGui.PopItemWidth()
if columns.Count > 1 then
ImGui.PopClipRect()
columns.Splitter:Merge(window.DrawList)
end
local flags = columns.Flags
columns.LineMaxY = ImMax(columns.LineMaxY, window.DC.CursorPos.y)
window.DC.CursorPos.y = columns.LineMaxY
if band(flags, ImGuiOldColumnFlags.GrowParentContentsSize) == 0 then
window.DC.CursorMaxPos.x = columns.HostCursorMaxPosX -- Restore cursor max pos, as columns don't grow parent
end
-- Draw columns borders and handle resize
-- The IsBeingResized flag ensure we preserve pre-resize columns width so back-and-forth are not lossy
local is_being_resized = false
if band(flags, ImGuiOldColumnFlags.NoBorder) == 0 and not window.SkipItems then
-- We clip Y boundaries CPU side because very long triangles are mishandled by some GPU drivers.
local y1 = ImMax(columns.HostCursorPosY, window.ClipRect.Min.y)
local y2 = ImMin(window.DC.CursorPos.y, window.ClipRect.Max.y)
local dragging_column = -1
for n = 1, columns.Count - 1 do
local column = columns.Columns.Data[n + 1]
local x = window.Pos.x + ImGui.GetColumnOffset(n)
local column_id = band(columns.ID + n, 0xFFFFFFFF)
local column_hit_hw = ImTrunc(COLUMNS_HIT_RECT_HALF_THICKNESS * g.CurrentDpiScale)
local column_hit_rect = ImRect(ImVec2(x - column_hit_hw, y1), ImVec2(x + column_hit_hw, y2))
if ImGui.ItemAdd(column_hit_rect, column_id, nil, ImGuiItemFlags.NoNav) then
local hovered, held = false, false
if band(flags, ImGuiOldColumnFlags.NoResize) == 0 then
local _
_, hovered, held = ImGui.ButtonBehavior(column_hit_rect, column_id)
if hovered or held then
ImGui.SetMouseCursor(ImGuiMouseCursor.ResizeEW)
end
if held and band(column.Flags, ImGuiOldColumnFlags.NoResize) == 0 then
dragging_column = n
end
end
-- Draw column
local col = ImGui.GetColorU32(held and ImGuiCol.SeparatorActive or (hovered and ImGuiCol.SeparatorHovered or ImGuiCol.Separator))
local xi = IM_TRUNC(x)
window.DrawList:AddLineV(xi, y1 + 1.0, y2, col)
end
end
-- Apply dragging after drawing the column lines, so our rendered lines are in sync with how items were displayed during the frame.
if dragging_column ~= -1 then
if not columns.IsBeingResized then
for n = 0, columns.Count do
local c = columns.Columns.Data[n + 1]
c.OffsetNormBeforeResize = c.OffsetNorm
end
end
columns.IsBeingResized = true; is_being_resized = true
local x = GetDraggedColumnOffset(columns, dragging_column)
ImGui.SetColumnOffset(dragging_column, x)
end
end
columns.IsBeingResized = is_being_resized
ImRect_Copy(window.WorkRect, window.ParentWorkRect)
ImRect_Copy(window.ParentWorkRect, columns.HostBackupParentWorkRect)
window.DC.CurrentColumns = nil
window.DC.ColumnsOffset.x = 0.0
window.DC.CursorPos.x = IM_TRUNC(window.Pos.x + window.DC.Indent.x + window.DC.ColumnsOffset.x)
ImGui.NavUpdateCurrentWindowIsScrollPushableX()
end
--- @param columns_count? int
--- @param id? string
--- @param borders? bool
function ImGui.Columns(columns_count, id, borders)
if columns_count == nil then columns_count = 1 end
if borders == nil then borders = true end
local window = ImGui.GetCurrentWindow()
IM_ASSERT(columns_count >= 1)
local flags = borders and 0 or ImGuiOldColumnFlags.NoBorder
--flags = bor(flags, ImGuiOldColumnFlags.NoPreserveWidths) -- NB: Legacy behavior
local columns = window.DC.CurrentColumns
if columns ~= nil and columns.Count == columns_count and columns.Flags == flags then
return
end
if columns ~= nil then
ImGui.EndColumns()
end
if columns_count ~= 1 then
ImGui.BeginColumns(id, columns_count, flags)
end
end
return true -- [Roblox] ModuleScripts must return exactly one value