From 701d1d344992a2da744b43bd95a43479fa34a0f5 Mon Sep 17 00:00:00 2001 From: 4DBug <4DBug@github.com> Date: Thu, 8 Oct 2026 06:53:00 -0500 Subject: [PATCH] tables: full port of imgui_tables.cpp (layout, borders, draw channel merge, sorting, headers, angled headers, context menu, .ini settings, reconcile, GC) + legacy Columns API; demo: ShowUserGuide, DemoWindowColumns; misc widget aliases --- build/ImGui.linemap.json | 2 +- build/ImGui.luau | 4575 +++++++++++++++++++++++++++++- imgui/imgui_demo.lua | 44 + imgui/imgui_demo_2.lua | 190 +- imgui/imgui_internal.lua | 1 + imgui/imgui_tables.lua | 4310 +++++++++++++++++++++++++++- imgui/imgui_widgets.lua | 16 + tools/test/scripts/t_tables.luau | 136 + 8 files changed, 9260 insertions(+), 14 deletions(-) create mode 100644 tools/test/scripts/t_tables.luau diff --git a/build/ImGui.linemap.json b/build/ImGui.linemap.json index 94e3b84..04db120 100644 --- a/build/ImGui.linemap.json +++ b/build/ImGui.linemap.json @@ -1 +1 @@ -[[12, "imgui.lua"], [13676, "imgui_demo.lua"], [14569, "imgui_demo_2.lua"], [17457, "imgui_docking.lua"], [20585, "imgui_dragdrop.lua"], [20877, "imgui_draw.lua"], [25872, "imgui_h.lua"], [28026, "imgui_impl_roblox.lua"], [28791, "imgui_impl_roblox_fonts.lua"], [29569, "imgui_internal.lua"], [32724, "imgui_tables.lua"], [32771, "imgui_tabs.lua"], [33938, "imgui_widgets.lua"], [42397, "imstb_rectpack.lua"], [42843, "imstb_textedit.lua"], [44084, "imstb_truetype.lua"], [46552, "imstd_minstdio.lua"]] \ No newline at end of file +[[12, "imgui.lua"], [13676, "imgui_demo.lua"], [14613, "imgui_demo_2.lua"], [17688, "imgui_docking.lua"], [20816, "imgui_dragdrop.lua"], [21108, "imgui_draw.lua"], [26103, "imgui_h.lua"], [28257, "imgui_impl_roblox.lua"], [29022, "imgui_impl_roblox_fonts.lua"], [29800, "imgui_internal.lua"], [32956, "imgui_tables.lua"], [37303, "imgui_tabs.lua"], [38470, "imgui_widgets.lua"], [46945, "imstb_rectpack.lua"], [47391, "imstb_textedit.lua"], [48632, "imstb_truetype.lua"], [51100, "imstd_minstdio.lua"]] \ No newline at end of file diff --git a/build/ImGui.luau b/build/ImGui.luau index 6f5926a..dbdcebd 100644 --- a/build/ImGui.luau +++ b/build/ImGui.luau @@ -14562,6 +14562,50 @@ function DemoWindowWidgets(data) end end +---------------------------------------------------------------- +-- [SECTION] User Guide / ShowUserGuide() +---------------------------------------------------------------- + +function ImGui.ShowUserGuide() + local io = ImGui.GetIO() + ImGui.BulletText("Double-click on title bar to collapse window.") + ImGui.BulletText( + "Click and drag on lower corner or border to resize window.\n" .. + "(double-click to auto fit window to its contents)") + ImGui.BulletText("Ctrl+Click on a slider or drag box to input value as text.") + ImGui.BulletText("Tab/Shift+Tab to cycle through keyboard editable fields.") + ImGui.BulletText("Ctrl+Tab/Ctrl+Shift+Tab to focus windows.") + if io.FontAllowUserScaling then + ImGui.BulletText("Ctrl+Mouse Wheel to zoom window contents.") + end + ImGui.BulletText("While inputting text:\n") + ImGui.Indent() + ImGui.BulletText("Ctrl+Left/Right to word jump.") + ImGui.BulletText("Ctrl+A or double-click to select all.") + ImGui.BulletText("Ctrl+X/C/V to use clipboard cut/copy/paste.") + ImGui.BulletText("Ctrl+Z to undo, Ctrl+Y/Ctrl+Shift+Z to redo.") + ImGui.BulletText("Escape to revert.") + ImGui.Unindent() + ImGui.BulletText("With Keyboard controls enabled:") + ImGui.Indent() + ImGui.BulletText("Arrow keys or Home/End/PageUp/PageDown to navigate.") + ImGui.BulletText("Space to activate a widget.") + ImGui.BulletText("Return to input text into a widget.") + ImGui.BulletText("Escape to deactivate a widget, close popup,\nexit a child window or the menu layer, clear focus.") + ImGui.BulletText("Alt to jump to the menu layer of a window.") + ImGui.BulletText("Menu or Shift+F10 to open a context menu.") + ImGui.Unindent() + ImGui.BulletText("With Gamepad controls enabled:") + ImGui.Indent() + ImGui.BulletText("D-Pad: Navigate / Tweak / Resize (in Windowing mode).") + ImGui.BulletText("%s Face button: Activate / Open / Toggle. Hold: activate with text input.", io.ConfigNavSwapGamepadButtons and "East" or "South") + ImGui.BulletText("%s Face button: Cancel / Close / Exit.", io.ConfigNavSwapGamepadButtons and "South" or "East") + ImGui.BulletText("West Face button: Toggle Menu. Hold for Windowing mode (Focus/Move/Resize windows).") + ImGui.BulletText("North Face button: Open Context Menu.") + ImGui.BulletText("L1/R1: Tweak Slower/Faster, Focus Previous/Next (in Windowing Mode).") + ImGui.Unindent() +end + return true -- [Roblox] ModuleScripts must return exactly one value end @@ -17372,6 +17416,195 @@ local function Tables_Advanced() end end +local DCS = { selected = -1, h_borders = true, v_borders = true, columns_count = 4, foo = 1.0, bar = 1.0 } + +local function DemoWindowColumns() + local open = ImGui.TreeNode("Legacy Columns API") + ImGui.SameLine() + DemoHelpMarker("Columns() is an old API! Prefer using the more flexible and powerful BeginTable() API!") + if not open then + return + end + + -- Basic columns + if ImGui.TreeNode("Basic") then + IMGUI_DEMO_MARKER("Columns (legacy API)/Basic") + ImGui.Text("Without border:") + ImGui.Columns(3, "mycolumns3", false) -- 3-ways, no border + ImGui.Separator() + for n = 0, 13 do + local label = string.format("Item %d", n) + if ImGui.Selectable(label) then end + ImGui.NextColumn() + end + ImGui.Columns(1) + ImGui.Separator() + + ImGui.Text("With border:") + ImGui.Columns(4, "mycolumns") -- 4-ways, with border + ImGui.Separator() + ImGui.Text("ID"); ImGui.NextColumn() + ImGui.Text("Name"); ImGui.NextColumn() + ImGui.Text("Path"); ImGui.NextColumn() + ImGui.Text("Hovered"); ImGui.NextColumn() + ImGui.Separator() + local names = { "One", "Two", "Three" } + local paths = { "/path/one", "/path/two", "/path/three" } + for i = 0, 2 do + local label = string.format("%04d", i) + if ImGui.Selectable(label, DCS.selected == i, ImGuiSelectableFlags.SpanAllColumns) then + DCS.selected = i + end + local hovered = ImGui.IsItemHovered() + ImGui.NextColumn() + ImGui.Text(names[i + 1]); ImGui.NextColumn() + ImGui.Text(paths[i + 1]); ImGui.NextColumn() + ImGui.Text("%d", hovered and 1 or 0); ImGui.NextColumn() + end + ImGui.Columns(1) + ImGui.Separator() + ImGui.TreePop() + end + + if ImGui.TreeNode("Borders") then + IMGUI_DEMO_MARKER("Columns (legacy API)/Borders") + -- NB: Future columns API should allow automatic horizontal borders. + local lines_count = 3 + ImGui.SetNextItemWidth(ImGui.GetFontSize() * 8) + DCS.columns_count = ImGui.DragInt("##columns_count", DCS.columns_count, 0.1, 2, 10, "%d columns") + if DCS.columns_count < 2 then + DCS.columns_count = 2 + end + ImGui.SameLine() + _, DCS.h_borders = ImGui.Checkbox("horizontal", DCS.h_borders) + ImGui.SameLine() + _, DCS.v_borders = ImGui.Checkbox("vertical", DCS.v_borders) + ImGui.Columns(DCS.columns_count, nil, DCS.v_borders) + for i = 0, DCS.columns_count * lines_count - 1 do + if DCS.h_borders and ImGui.GetColumnIndex() == 0 then + ImGui.Separator() + end + ImGui.PushID(i) + local c = string.char(string.byte("a") + i) + ImGui.Text("%s%s%s", c, c, c) + ImGui.Text("Width %.2f", ImGui.GetColumnWidth()) + ImGui.Text("Avail %.2f", ImGui.GetContentRegionAvail().x) + ImGui.Text("Offset %.2f", ImGui.GetColumnOffset()) + ImGui.Text("Long text that is likely to clip") + ImGui.Button("Button", ImVec2(-FLT_MIN, 0.0)) + ImGui.PopID() + ImGui.NextColumn() + end + ImGui.Columns(1) + if DCS.h_borders then + ImGui.Separator() + end + ImGui.TreePop() + end + + -- Create multiple items in a same cell before switching to next column + if ImGui.TreeNode("Mixed items") then + IMGUI_DEMO_MARKER("Columns (legacy API)/Mixed items") + ImGui.Columns(3, "mixed") + ImGui.Separator() + + ImGui.Text("Hello") + ImGui.Button("Banana") + ImGui.NextColumn() + + ImGui.Text("ImGui") + ImGui.Button("Apple") + DCS.foo = ImGui.InputFloat("red", DCS.foo, 0.05, 0, "%.3f") + ImGui.Text("An extra line here.") + ImGui.NextColumn() + + ImGui.Text("Sailor") + ImGui.Button("Corniflower") + DCS.bar = ImGui.InputFloat("blue", DCS.bar, 0.05, 0, "%.3f") + ImGui.NextColumn() + + if ImGui.CollapsingHeader("Category A") then ImGui.Text("Blah blah blah") end ImGui.NextColumn() + if ImGui.CollapsingHeader("Category B") then ImGui.Text("Blah blah blah") end ImGui.NextColumn() + if ImGui.CollapsingHeader("Category C") then ImGui.Text("Blah blah blah") end ImGui.NextColumn() + ImGui.Columns(1) + ImGui.Separator() + ImGui.TreePop() + end + + -- Word wrapping + if ImGui.TreeNode("Word-wrapping") then + IMGUI_DEMO_MARKER("Columns (legacy API)/Word-wrapping") + ImGui.Columns(2, "word-wrapping") + ImGui.Separator() + ImGui.TextWrapped("The quick brown fox jumps over the lazy dog.") + ImGui.TextWrapped("Hello Left") + ImGui.NextColumn() + ImGui.TextWrapped("The quick brown fox jumps over the lazy dog.") + ImGui.TextWrapped("Hello Right") + ImGui.Columns(1) + ImGui.Separator() + ImGui.TreePop() + end + + if ImGui.TreeNode("Horizontal Scrolling") then + IMGUI_DEMO_MARKER("Columns (legacy API)/Horizontal Scrolling") + ImGui.SetNextWindowContentSize(ImVec2(1500.0, 0.0)) + local child_size = ImVec2(0, ImGui.GetFontSize() * 20.0) + ImGui.BeginChild("##ScrollingRegion", child_size, ImGuiChildFlags.None, ImGuiWindowFlags.HorizontalScrollbar) + ImGui.Columns(10) + + -- Also demonstrate using clipper for large vertical lists + local ITEMS_COUNT = 2000 + local clipper = ImGuiListClipper() + clipper:Begin(ITEMS_COUNT) + while clipper:Step() do + for i = clipper.DisplayStart, clipper.DisplayEnd - 1 do + for j = 0, 9 do + ImGui.Text("Line %d Column %d...", i, j) + ImGui.NextColumn() + end + end + end + ImGui.Columns(1) + ImGui.EndChild() + ImGui.TreePop() + end + + if ImGui.TreeNode("Tree") then + IMGUI_DEMO_MARKER("Columns (legacy API)/Tree") + ImGui.Columns(2, "tree", true) + for x = 0, 2 do + local open1 = ImGui.TreeNode(x, "Node%d", x) + ImGui.NextColumn() + ImGui.Text("Node contents") + ImGui.NextColumn() + if open1 then + for y = 0, 2 do + local open2 = ImGui.TreeNode(y, "Node%d.%d", x, y) + ImGui.NextColumn() + ImGui.Text("Node contents") + if open2 then + ImGui.Text("Even more contents") + if ImGui.TreeNode("Tree in column") then + ImGui.Text("The quick brown fox jumps over the lazy dog") + ImGui.TreePop() + end + end + ImGui.NextColumn() + if open2 then + ImGui.TreePop() + end + end + ImGui.TreePop() + end + end + ImGui.Columns(1) + ImGui.TreePop() + end + + ImGui.TreePop() +end + local tables_sections = { { "Basic", Tables_Basic }, { "Borders, background", Tables_BordersBackground }, @@ -17449,8 +17682,6 @@ function DemoWindowTables() ImGui.PopStyleVar() end end - ---@@COLUMNS@@ end __FILES['imgui_docking.lua'] = function() @@ -31841,6 +32072,7 @@ local function ImGuiWindowTempData() IsSameLine = false, IsSetPos = false, + NavLayersActiveMask = 0, NavLayersActiveMaskNext = 0, Indent = ImVec1(), ColumnsOffset = ImVec1(), @@ -32723,10 +32955,23 @@ end __FILES['imgui_tables.lua'] = function() --- ImGui Sincerely WIP -- (Tables and Columns Code) +-- 1:1 port of imgui_tables.cpp (docking branch, 1.93 WIP) +-- +-- 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? @@ -32734,34 +32979,4321 @@ 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) +IMGUI_TABLE_MAX_COLUMNS = 512 + +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 + +--- @class ImGuiTableColumnSortSpecs +function ImGuiTableColumnSortSpecs() + return { ColumnUserID = 0, ColumnIndex = 0, SortOrder = 0, SortDirection = ImGuiSortDirection.None } +end + +--- @class ImGuiTableSortSpecs +function ImGuiTableSortSpecs() + return { Specs = nil, SpecsCount = 0, SpecsDirty = false } +end + +--- @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 + +--- @class ImGuiTableInstanceData +function ImGuiTableInstanceData() + return { TableInstanceID = 0, LastOuterHeight = 0.0, LastTopHeadersRowHeight = 0.0, LastFrozenHeight = 0.0, HoveredRowLast = -1, HoveredRowNext = -1 } +end + +--- @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 + +--- @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 + +--- @class ImGuiTableSettings +function ImGuiTableSettings() + return { ID = 0, SaveFlags = 0, RefScale = 0.0, ColumnsCount = 0, ColumnsCountMax = 0, LastUsedDate = 0, WantApply = false, Columns = {} } +end + +--- @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 + +ImGuiOldColumnFlags = ImGuiOldColumnFlags or { + None = 0, + NoBorder = lshift(1, 0), + NoResize = lshift(1, 1), + NoPreserveWidths = lshift(1, 2), + NoForceWithinWindow = lshift(1, 3), + GrowParentContentsSize = lshift(1, 4), +} + +ImGuiTableColumnFlags.NoDirectResize_ = ImGuiTableColumnFlags.NoDirectResize_ or lshift(1, 30) + +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 +---------------------------------------------------------------- + +-- 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 - -- TODO: + 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. + 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) + 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 + 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). + 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. + 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) + 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) + if inner_window ~= outer_window then + 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 + 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.. + inner_window.SkipItems = true + + -- Clear names + 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 + 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. + if table_.AutoFitSingleColumn ~= -1 then + ImGui.TableSetColumnWidth(table_.AutoFitSingleColumn, table_.Columns[table_.AutoFitSingleColumn].WidthAuto) + table_.AutoFitSingleColumn = -1 + end + end + + -- Handle reordering request + 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 + 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 + + -- Display order is stored in both columns->IndexDisplayOrder and table->DisplayOrder[]. Rebuild later from the former. + 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. + 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 + + -- 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. +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 + local new_ref_scale_unit = g.FontSize + 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. + local count_fixed = 0 + local count_stretch = 0 + 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. + 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) + 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. + 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 + local stretch_sum_weights = 0.0 + 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 + 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 + 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 + 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.0f * number of Stretch column). + 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. + 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 + 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) + 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. + 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 + 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) + 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 --&& 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) + 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 + + -- 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. + 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) + 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. + 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 + 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. + 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() +function ImGui.TableUpdateBorders(table_) + local g = GImGui + IM_ASSERT(band(table_.Flags, ImGuiTableFlags.Resizable) ~= 0) + + 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) + + 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() - -- TODO: + local g = GImGui + local table_ = g.CurrentTable + IM_ASSERT_USER_ERROR_RET(table_ ~= nil, "EndTable() call should only be done while in BeginTable() scope!") + + -- If the user never got to call TableNextRow() or TableNextColumn(), we call layout ourselves. + 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 + 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 + 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 + 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 + 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 + 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 + 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.0f) 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. + 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 + 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!") + IM_ASSERT(band(flags, ImGuiTableColumnFlags.StatusMask_) == 0, "Illegal to pass StatusMask values to TableSetupColumn()") + + -- Store name + 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(). + 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) + 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 +---------------------------------------------------------------- + +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) +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. +function ImGui.TableGetCellBgRect(table_, column_n) + local column = table_.Columns[column_n] + local x1 = column.MinX + local x2 = column.MaxX + 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. +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 +---------------------------------------------------------------- + +-- [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 + 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. + 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) + 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) + 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 +---------------------------------------------------------------- + +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() - -- TODO: + 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. +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) + 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 + table_.RowTextBaseline = ImMax(table_.RowTextBaseline, window.DC.PrevLineTextBaseOffset) +end + +---------------------------------------------------------------- +-- [SECTION] Tables: Columns width management +---------------------------------------------------------------- + +-- 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. + 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. + max_width = table_.WorkRect.Max.x - (table_.ColumnsEnabledCount - column.IndexWithinEnabledSet - 1) * min_column_distance - column.MinX + 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 + 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 + + local column_1 = (column_0.NextEnabledColumn ~= -1) and table_.Columns[column_0.NextEnabledColumn] or nil + + -- 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 +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 +---------------------------------------------------------------- + +-- Bg2 is used by Selectable (and possibly other widgets) to render to the background. function ImGui.TablePushBackgroundChannel() - -- TODO: + 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() - -- TODO: + 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() +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. + 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(). +-- (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 + 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 + 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_.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. + 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 + 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 + if band(table_.Flags, ImGuiTableFlags.BordersOuter) ~= 0 then + 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 +---------------------------------------------------------------- + +-- Return nil if no sort specs (most often when ImGuiTableFlags_Sortable is not set) +-- When 'sort_specs.SpecsDirty == true' you should sort your data. 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 +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. + 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 + 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 +---------------------------------------------------------------- + +function ImGui.TableGetHeaderRowHeight() + -- Calculate row height, for the unlikely case that some labels may be taller than others. + 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(). +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. +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. + 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 + + -- 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)) + 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. + if held and band(table_.Flags, ImGuiTableFlags.Reorderable) ~= 0 and ImGui.IsMouseDragging(0) and not g.DragDropActive then + 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 + 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. + 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. +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() + 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 + 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 + 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 + + 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 +---------------------------------------------------------------- + +-- 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 + 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) +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 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 + + -- 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. + 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 = "" + 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 + 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) +---------------------------------------------------------------- + +-- 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) + 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, NULL 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. + 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. -1 (unsigned short 0xFFFF) always sorts 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) + 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 + 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] + 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 +---------------------------------------------------------------- + +-- Remove Table data (currently only used by TestEngine) +function ImGui.TableRemove(table_) + 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 + 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 +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!) +---------------------------------------------------------------- +-- (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. + 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. + 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 + 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() + 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) + 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 + + 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 + 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 + 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 @@ -40744,6 +45276,22 @@ end -- [SECTION] TREES ---------------------------------------------------------------- +-- va_list variants: identical in Lua (varargs) +ImGui.TextColoredV = function(...) return ImGui.TextColored(...) end +ImGui.TextDisabledV = function(...) return ImGui.TextDisabled(...) end +ImGui.TextWrappedV = function(...) return ImGui.TextWrapped(...) end +ImGui.TextAlignedV = function(...) return ImGui.TextAligned(...) end +ImGui.LabelTextV = function(...) return ImGui.LabelText(...) end +ImGui.BulletTextV = function(...) return ImGui.BulletText(...) end + +--- @param data_type ImGuiDataType +--- @param v number +function ImGui.SetNextItemRefVal(data_type, v) + local g = GImGui + g.NextItemData.HasFlags = bit32.bor(g.NextItemData.HasFlags, ImGuiNextItemDataFlags.HasRefVal) + g.NextItemData.RefVal = v +end + -- Overloads: TreeNode(label) / TreeNode(str_id, fmt, ...) --- @param label string function ImGui.TreeNode(label, fmt, ...) @@ -46831,6 +51379,7 @@ return { IMGUI_FONT_SIZE_MAX = IMGUI_FONT_SIZE_MAX, IMGUI_FONT_SIZE_THRESHOLD_FOR_LOADADVANCEXONLYMODE = IMGUI_FONT_SIZE_THRESHOLD_FOR_LOADADVANCEXONLYMODE, IMGUI_PAYLOAD_TYPE_WINDOW = IMGUI_PAYLOAD_TYPE_WINDOW, + IMGUI_TABLE_MAX_COLUMNS = IMGUI_TABLE_MAX_COLUMNS, IMGUI_WINDOW_HARD_MIN_SIZE = IMGUI_WINDOW_HARD_MIN_SIZE, IMSTB_TEXTEDIT_UNDOSTATECOUNT = IMSTB_TEXTEDIT_UNDOSTATECOUNT, IM_ALLOC = IM_ALLOC, @@ -46846,6 +51395,7 @@ return { IM_COL32_BLACK = IM_COL32_BLACK, IM_COL32_BLACK_TRANS = IM_COL32_BLACK_TRANS, IM_COL32_B_SHIFT = IM_COL32_B_SHIFT, + IM_COL32_DISABLE = IM_COL32_DISABLE, IM_COL32_G_SHIFT = IM_COL32_G_SHIFT, IM_COL32_R_SHIFT = IM_COL32_R_SHIFT, IM_COL32_WHITE = IM_COL32_WHITE, @@ -47049,6 +51599,9 @@ return { ImGuiNextItemDataFlags = ImGuiNextItemDataFlags, ImGuiNextWindowData = ImGuiNextWindowData, ImGuiNextWindowDataFlags = ImGuiNextWindowDataFlags, + ImGuiOldColumnData = ImGuiOldColumnData, + ImGuiOldColumnFlags = ImGuiOldColumnFlags, + ImGuiOldColumns = ImGuiOldColumns, ImGuiPayload = ImGuiPayload, ImGuiPayload_Clear = ImGuiPayload_Clear, ImGuiPayload_IsDataType = ImGuiPayload_IsDataType, @@ -47080,10 +51633,18 @@ return { ImGuiTabBarFlags = ImGuiTabBarFlags, ImGuiTabItem = ImGuiTabItem, ImGuiTabItemFlags = ImGuiTabItemFlags, + ImGuiTable = ImGuiTable, ImGuiTableBgTarget = ImGuiTableBgTarget, + ImGuiTableColumn = ImGuiTableColumn, ImGuiTableColumnFlags = ImGuiTableColumnFlags, + ImGuiTableColumnSettings = ImGuiTableColumnSettings, + ImGuiTableColumnSortSpecs = ImGuiTableColumnSortSpecs, ImGuiTableFlags = ImGuiTableFlags, + ImGuiTableInstanceData = ImGuiTableInstanceData, ImGuiTableRowFlags = ImGuiTableRowFlags, + ImGuiTableSettings = ImGuiTableSettings, + ImGuiTableSortSpecs = ImGuiTableSortSpecs, + ImGuiTableTempData = ImGuiTableTempData, ImGuiTextBuffer = ImGuiTextBuffer, ImGuiTextFilter = ImGuiTextFilter, ImGuiTextFlags = ImGuiTextFlags, diff --git a/imgui/imgui_demo.lua b/imgui/imgui_demo.lua index 2079f43..73dc797 100644 --- a/imgui/imgui_demo.lua +++ b/imgui/imgui_demo.lua @@ -887,4 +887,48 @@ function DemoWindowWidgets(data) end end +---------------------------------------------------------------- +-- [SECTION] User Guide / ShowUserGuide() +---------------------------------------------------------------- + +function ImGui.ShowUserGuide() + local io = ImGui.GetIO() + ImGui.BulletText("Double-click on title bar to collapse window.") + ImGui.BulletText( + "Click and drag on lower corner or border to resize window.\n" .. + "(double-click to auto fit window to its contents)") + ImGui.BulletText("Ctrl+Click on a slider or drag box to input value as text.") + ImGui.BulletText("Tab/Shift+Tab to cycle through keyboard editable fields.") + ImGui.BulletText("Ctrl+Tab/Ctrl+Shift+Tab to focus windows.") + if io.FontAllowUserScaling then + ImGui.BulletText("Ctrl+Mouse Wheel to zoom window contents.") + end + ImGui.BulletText("While inputting text:\n") + ImGui.Indent() + ImGui.BulletText("Ctrl+Left/Right to word jump.") + ImGui.BulletText("Ctrl+A or double-click to select all.") + ImGui.BulletText("Ctrl+X/C/V to use clipboard cut/copy/paste.") + ImGui.BulletText("Ctrl+Z to undo, Ctrl+Y/Ctrl+Shift+Z to redo.") + ImGui.BulletText("Escape to revert.") + ImGui.Unindent() + ImGui.BulletText("With Keyboard controls enabled:") + ImGui.Indent() + ImGui.BulletText("Arrow keys or Home/End/PageUp/PageDown to navigate.") + ImGui.BulletText("Space to activate a widget.") + ImGui.BulletText("Return to input text into a widget.") + ImGui.BulletText("Escape to deactivate a widget, close popup,\nexit a child window or the menu layer, clear focus.") + ImGui.BulletText("Alt to jump to the menu layer of a window.") + ImGui.BulletText("Menu or Shift+F10 to open a context menu.") + ImGui.Unindent() + ImGui.BulletText("With Gamepad controls enabled:") + ImGui.Indent() + ImGui.BulletText("D-Pad: Navigate / Tweak / Resize (in Windowing mode).") + ImGui.BulletText("%s Face button: Activate / Open / Toggle. Hold: activate with text input.", io.ConfigNavSwapGamepadButtons and "East" or "South") + ImGui.BulletText("%s Face button: Cancel / Close / Exit.", io.ConfigNavSwapGamepadButtons and "South" or "East") + ImGui.BulletText("West Face button: Toggle Menu. Hold for Windowing mode (Focus/Move/Resize windows).") + ImGui.BulletText("North Face button: Open Context Menu.") + ImGui.BulletText("L1/R1: Tweak Slower/Faster, Focus Previous/Next (in Windowing Mode).") + ImGui.Unindent() +end + return true -- [Roblox] ModuleScripts must return exactly one value diff --git a/imgui/imgui_demo_2.lua b/imgui/imgui_demo_2.lua index 62b6b3c..a3da301 100644 --- a/imgui/imgui_demo_2.lua +++ b/imgui/imgui_demo_2.lua @@ -2804,6 +2804,195 @@ local function Tables_Advanced() end end +local DCS = { selected = -1, h_borders = true, v_borders = true, columns_count = 4, foo = 1.0, bar = 1.0 } + +local function DemoWindowColumns() + local open = ImGui.TreeNode("Legacy Columns API") + ImGui.SameLine() + DemoHelpMarker("Columns() is an old API! Prefer using the more flexible and powerful BeginTable() API!") + if not open then + return + end + + -- Basic columns + if ImGui.TreeNode("Basic") then + IMGUI_DEMO_MARKER("Columns (legacy API)/Basic") + ImGui.Text("Without border:") + ImGui.Columns(3, "mycolumns3", false) -- 3-ways, no border + ImGui.Separator() + for n = 0, 13 do + local label = string.format("Item %d", n) + if ImGui.Selectable(label) then end + ImGui.NextColumn() + end + ImGui.Columns(1) + ImGui.Separator() + + ImGui.Text("With border:") + ImGui.Columns(4, "mycolumns") -- 4-ways, with border + ImGui.Separator() + ImGui.Text("ID"); ImGui.NextColumn() + ImGui.Text("Name"); ImGui.NextColumn() + ImGui.Text("Path"); ImGui.NextColumn() + ImGui.Text("Hovered"); ImGui.NextColumn() + ImGui.Separator() + local names = { "One", "Two", "Three" } + local paths = { "/path/one", "/path/two", "/path/three" } + for i = 0, 2 do + local label = string.format("%04d", i) + if ImGui.Selectable(label, DCS.selected == i, ImGuiSelectableFlags.SpanAllColumns) then + DCS.selected = i + end + local hovered = ImGui.IsItemHovered() + ImGui.NextColumn() + ImGui.Text(names[i + 1]); ImGui.NextColumn() + ImGui.Text(paths[i + 1]); ImGui.NextColumn() + ImGui.Text("%d", hovered and 1 or 0); ImGui.NextColumn() + end + ImGui.Columns(1) + ImGui.Separator() + ImGui.TreePop() + end + + if ImGui.TreeNode("Borders") then + IMGUI_DEMO_MARKER("Columns (legacy API)/Borders") + -- NB: Future columns API should allow automatic horizontal borders. + local lines_count = 3 + ImGui.SetNextItemWidth(ImGui.GetFontSize() * 8) + DCS.columns_count = ImGui.DragInt("##columns_count", DCS.columns_count, 0.1, 2, 10, "%d columns") + if DCS.columns_count < 2 then + DCS.columns_count = 2 + end + ImGui.SameLine() + _, DCS.h_borders = ImGui.Checkbox("horizontal", DCS.h_borders) + ImGui.SameLine() + _, DCS.v_borders = ImGui.Checkbox("vertical", DCS.v_borders) + ImGui.Columns(DCS.columns_count, nil, DCS.v_borders) + for i = 0, DCS.columns_count * lines_count - 1 do + if DCS.h_borders and ImGui.GetColumnIndex() == 0 then + ImGui.Separator() + end + ImGui.PushID(i) + local c = string.char(string.byte("a") + i) + ImGui.Text("%s%s%s", c, c, c) + ImGui.Text("Width %.2f", ImGui.GetColumnWidth()) + ImGui.Text("Avail %.2f", ImGui.GetContentRegionAvail().x) + ImGui.Text("Offset %.2f", ImGui.GetColumnOffset()) + ImGui.Text("Long text that is likely to clip") + ImGui.Button("Button", ImVec2(-FLT_MIN, 0.0)) + ImGui.PopID() + ImGui.NextColumn() + end + ImGui.Columns(1) + if DCS.h_borders then + ImGui.Separator() + end + ImGui.TreePop() + end + + -- Create multiple items in a same cell before switching to next column + if ImGui.TreeNode("Mixed items") then + IMGUI_DEMO_MARKER("Columns (legacy API)/Mixed items") + ImGui.Columns(3, "mixed") + ImGui.Separator() + + ImGui.Text("Hello") + ImGui.Button("Banana") + ImGui.NextColumn() + + ImGui.Text("ImGui") + ImGui.Button("Apple") + DCS.foo = ImGui.InputFloat("red", DCS.foo, 0.05, 0, "%.3f") + ImGui.Text("An extra line here.") + ImGui.NextColumn() + + ImGui.Text("Sailor") + ImGui.Button("Corniflower") + DCS.bar = ImGui.InputFloat("blue", DCS.bar, 0.05, 0, "%.3f") + ImGui.NextColumn() + + if ImGui.CollapsingHeader("Category A") then ImGui.Text("Blah blah blah") end ImGui.NextColumn() + if ImGui.CollapsingHeader("Category B") then ImGui.Text("Blah blah blah") end ImGui.NextColumn() + if ImGui.CollapsingHeader("Category C") then ImGui.Text("Blah blah blah") end ImGui.NextColumn() + ImGui.Columns(1) + ImGui.Separator() + ImGui.TreePop() + end + + -- Word wrapping + if ImGui.TreeNode("Word-wrapping") then + IMGUI_DEMO_MARKER("Columns (legacy API)/Word-wrapping") + ImGui.Columns(2, "word-wrapping") + ImGui.Separator() + ImGui.TextWrapped("The quick brown fox jumps over the lazy dog.") + ImGui.TextWrapped("Hello Left") + ImGui.NextColumn() + ImGui.TextWrapped("The quick brown fox jumps over the lazy dog.") + ImGui.TextWrapped("Hello Right") + ImGui.Columns(1) + ImGui.Separator() + ImGui.TreePop() + end + + if ImGui.TreeNode("Horizontal Scrolling") then + IMGUI_DEMO_MARKER("Columns (legacy API)/Horizontal Scrolling") + ImGui.SetNextWindowContentSize(ImVec2(1500.0, 0.0)) + local child_size = ImVec2(0, ImGui.GetFontSize() * 20.0) + ImGui.BeginChild("##ScrollingRegion", child_size, ImGuiChildFlags.None, ImGuiWindowFlags.HorizontalScrollbar) + ImGui.Columns(10) + + -- Also demonstrate using clipper for large vertical lists + local ITEMS_COUNT = 2000 + local clipper = ImGuiListClipper() + clipper:Begin(ITEMS_COUNT) + while clipper:Step() do + for i = clipper.DisplayStart, clipper.DisplayEnd - 1 do + for j = 0, 9 do + ImGui.Text("Line %d Column %d...", i, j) + ImGui.NextColumn() + end + end + end + ImGui.Columns(1) + ImGui.EndChild() + ImGui.TreePop() + end + + if ImGui.TreeNode("Tree") then + IMGUI_DEMO_MARKER("Columns (legacy API)/Tree") + ImGui.Columns(2, "tree", true) + for x = 0, 2 do + local open1 = ImGui.TreeNode(x, "Node%d", x) + ImGui.NextColumn() + ImGui.Text("Node contents") + ImGui.NextColumn() + if open1 then + for y = 0, 2 do + local open2 = ImGui.TreeNode(y, "Node%d.%d", x, y) + ImGui.NextColumn() + ImGui.Text("Node contents") + if open2 then + ImGui.Text("Even more contents") + if ImGui.TreeNode("Tree in column") then + ImGui.Text("The quick brown fox jumps over the lazy dog") + ImGui.TreePop() + end + end + ImGui.NextColumn() + if open2 then + ImGui.TreePop() + end + end + ImGui.TreePop() + end + end + ImGui.Columns(1) + ImGui.TreePop() + end + + ImGui.TreePop() +end + local tables_sections = { { "Basic", Tables_Basic }, { "Borders, background", Tables_BordersBackground }, @@ -2882,4 +3071,3 @@ function DemoWindowTables() end end ---@@COLUMNS@@ diff --git a/imgui/imgui_internal.lua b/imgui/imgui_internal.lua index 291f68f..24cd6b1 100644 --- a/imgui/imgui_internal.lua +++ b/imgui/imgui_internal.lua @@ -2273,6 +2273,7 @@ local function ImGuiWindowTempData() IsSameLine = false, IsSetPos = false, + NavLayersActiveMask = 0, NavLayersActiveMaskNext = 0, Indent = ImVec1(), ColumnsOffset = ImVec1(), diff --git a/imgui/imgui_tables.lua b/imgui/imgui_tables.lua index 46b5943..25f1c78 100644 --- a/imgui/imgui_tables.lua +++ b/imgui/imgui_tables.lua @@ -1,9 +1,22 @@ --- ImGui Sincerely WIP -- (Tables and Columns Code) +-- 1:1 port of imgui_tables.cpp (docking branch, 1.93 WIP) +-- +-- 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? @@ -11,34 +24,4321 @@ 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) +IMGUI_TABLE_MAX_COLUMNS = 512 + +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 + +--- @class ImGuiTableColumnSortSpecs +function ImGuiTableColumnSortSpecs() + return { ColumnUserID = 0, ColumnIndex = 0, SortOrder = 0, SortDirection = ImGuiSortDirection.None } +end + +--- @class ImGuiTableSortSpecs +function ImGuiTableSortSpecs() + return { Specs = nil, SpecsCount = 0, SpecsDirty = false } +end + +--- @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 + +--- @class ImGuiTableInstanceData +function ImGuiTableInstanceData() + return { TableInstanceID = 0, LastOuterHeight = 0.0, LastTopHeadersRowHeight = 0.0, LastFrozenHeight = 0.0, HoveredRowLast = -1, HoveredRowNext = -1 } +end + +--- @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 + +--- @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 + +--- @class ImGuiTableSettings +function ImGuiTableSettings() + return { ID = 0, SaveFlags = 0, RefScale = 0.0, ColumnsCount = 0, ColumnsCountMax = 0, LastUsedDate = 0, WantApply = false, Columns = {} } +end + +--- @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 + +ImGuiOldColumnFlags = ImGuiOldColumnFlags or { + None = 0, + NoBorder = lshift(1, 0), + NoResize = lshift(1, 1), + NoPreserveWidths = lshift(1, 2), + NoForceWithinWindow = lshift(1, 3), + GrowParentContentsSize = lshift(1, 4), +} + +ImGuiTableColumnFlags.NoDirectResize_ = ImGuiTableColumnFlags.NoDirectResize_ or lshift(1, 30) + +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 +---------------------------------------------------------------- + +-- 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 - -- TODO: + 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. + 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) + 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 + 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). + 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. + 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) + 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) + if inner_window ~= outer_window then + 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 + 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.. + inner_window.SkipItems = true + + -- Clear names + 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 + 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. + if table_.AutoFitSingleColumn ~= -1 then + ImGui.TableSetColumnWidth(table_.AutoFitSingleColumn, table_.Columns[table_.AutoFitSingleColumn].WidthAuto) + table_.AutoFitSingleColumn = -1 + end + end + + -- Handle reordering request + 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 + 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 + + -- Display order is stored in both columns->IndexDisplayOrder and table->DisplayOrder[]. Rebuild later from the former. + 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. + 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 + + -- 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. +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 + local new_ref_scale_unit = g.FontSize + 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. + local count_fixed = 0 + local count_stretch = 0 + 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. + 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) + 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. + 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 + local stretch_sum_weights = 0.0 + 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 + 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 + 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 + 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.0f * number of Stretch column). + 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. + 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 + 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) + 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. + 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 + 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) + 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 --&& 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) + 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 + + -- 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. + 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) + 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. + 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 + 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. + 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() +function ImGui.TableUpdateBorders(table_) + local g = GImGui + IM_ASSERT(band(table_.Flags, ImGuiTableFlags.Resizable) ~= 0) + + 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) + + 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() - -- TODO: + local g = GImGui + local table_ = g.CurrentTable + IM_ASSERT_USER_ERROR_RET(table_ ~= nil, "EndTable() call should only be done while in BeginTable() scope!") + + -- If the user never got to call TableNextRow() or TableNextColumn(), we call layout ourselves. + 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 + 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 + 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 + 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 + 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 + 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 + 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.0f) 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. + 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 + 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!") + IM_ASSERT(band(flags, ImGuiTableColumnFlags.StatusMask_) == 0, "Illegal to pass StatusMask values to TableSetupColumn()") + + -- Store name + 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(). + 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) + 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 +---------------------------------------------------------------- + +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) +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. +function ImGui.TableGetCellBgRect(table_, column_n) + local column = table_.Columns[column_n] + local x1 = column.MinX + local x2 = column.MaxX + 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. +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 +---------------------------------------------------------------- + +-- [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 + 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. + 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) + 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) + 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 +---------------------------------------------------------------- + +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() - -- TODO: + 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. +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) + 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 + table_.RowTextBaseline = ImMax(table_.RowTextBaseline, window.DC.PrevLineTextBaseOffset) +end + +---------------------------------------------------------------- +-- [SECTION] Tables: Columns width management +---------------------------------------------------------------- + +-- 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. + 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. + max_width = table_.WorkRect.Max.x - (table_.ColumnsEnabledCount - column.IndexWithinEnabledSet - 1) * min_column_distance - column.MinX + 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 + 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 + + local column_1 = (column_0.NextEnabledColumn ~= -1) and table_.Columns[column_0.NextEnabledColumn] or nil + + -- 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 +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 +---------------------------------------------------------------- + +-- Bg2 is used by Selectable (and possibly other widgets) to render to the background. function ImGui.TablePushBackgroundChannel() - -- TODO: + 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() - -- TODO: + 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() +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. + 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(). +-- (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 + 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 + 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_.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. + 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 + 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 + if band(table_.Flags, ImGuiTableFlags.BordersOuter) ~= 0 then + 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 +---------------------------------------------------------------- + +-- Return nil if no sort specs (most often when ImGuiTableFlags_Sortable is not set) +-- When 'sort_specs.SpecsDirty == true' you should sort your data. 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 +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. + 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 + 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 +---------------------------------------------------------------- + +function ImGui.TableGetHeaderRowHeight() + -- Calculate row height, for the unlikely case that some labels may be taller than others. + 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(). +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. +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. + 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 + + -- 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)) + 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. + if held and band(table_.Flags, ImGuiTableFlags.Reorderable) ~= 0 and ImGui.IsMouseDragging(0) and not g.DragDropActive then + 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 + 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. + 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. +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() + 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 + 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 + 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 + + 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 +---------------------------------------------------------------- + +-- 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 + 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) +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 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 + + -- 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. + 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 = "" + 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 + 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) +---------------------------------------------------------------- + +-- 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) + 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, NULL 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. + 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. -1 (unsigned short 0xFFFF) always sorts 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) + 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 + 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] + 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 +---------------------------------------------------------------- + +-- Remove Table data (currently only used by TestEngine) +function ImGui.TableRemove(table_) + 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 + 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 +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!) +---------------------------------------------------------------- +-- (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. + 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. + 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 + 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() + 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) + 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 + + 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 + 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 + 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 diff --git a/imgui/imgui_widgets.lua b/imgui/imgui_widgets.lua index 194c85d..c3914cf 100644 --- a/imgui/imgui_widgets.lua +++ b/imgui/imgui_widgets.lua @@ -6807,6 +6807,22 @@ end -- [SECTION] TREES ---------------------------------------------------------------- +-- va_list variants: identical in Lua (varargs) +ImGui.TextColoredV = function(...) return ImGui.TextColored(...) end +ImGui.TextDisabledV = function(...) return ImGui.TextDisabled(...) end +ImGui.TextWrappedV = function(...) return ImGui.TextWrapped(...) end +ImGui.TextAlignedV = function(...) return ImGui.TextAligned(...) end +ImGui.LabelTextV = function(...) return ImGui.LabelText(...) end +ImGui.BulletTextV = function(...) return ImGui.BulletText(...) end + +--- @param data_type ImGuiDataType +--- @param v number +function ImGui.SetNextItemRefVal(data_type, v) + local g = GImGui + g.NextItemData.HasFlags = bit32.bor(g.NextItemData.HasFlags, ImGuiNextItemDataFlags.HasRefVal) + g.NextItemData.RefVal = v +end + -- Overloads: TreeNode(label) / TreeNode(str_id, fmt, ...) --- @param label string function ImGui.TreeNode(label, fmt, ...) diff --git a/tools/test/scripts/t_tables.luau b/tools/test/scripts/t_tables.luau new file mode 100644 index 0000000..0026330 --- /dev/null +++ b/tools/test/scripts/t_tables.luau @@ -0,0 +1,136 @@ +-- Tables: basic, borders/rowbg, resizable+reorderable+hideable+sortable headers, scroll freeze + clipper, angled headers, legacy Columns, ini round trip +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local RunService = game:GetService("RunService") +local G = require(ReplicatedStorage:WaitForChild("ImGui")) +local ImGui, ImVec2 = G.ImGui, G.ImVec2 +local TF, CF = G.ImGuiTableFlags, G.ImGuiTableColumnFlags +local bor = bit32.bor +ImGui.CreateContext() +G.ImGui_ImplRoblox.Init() +local frame = 0 +local sorted_specs = nil +local visible_rows = 0 +local hovered_col = nil +RunService.Stepped:Connect(function() + frame += 1 + G.ImGui_ImplRoblox.NewFrame() + local io = ImGui.GetIO() + -- drag the border between column 0 and 1 of "sortable" on frames 10..20 + ImGui.NewFrame() + ImGui.SetNextWindowPos(ImVec2(10, 10), G.ImGuiCond.Always) + ImGui.SetNextWindowSize(ImVec2(700, 900), G.ImGuiCond.Always) + ImGui.Begin("Tables") + + if ImGui.BeginTable("basic", 3) then + for row = 0, 3 do + ImGui.TableNextRow() + for col = 0, 2 do + ImGui.TableSetColumnIndex(col) + ImGui.Text("Row %d Column %d", row, col) + end + end + ImGui.EndTable() + end + + if ImGui.BeginTable("borders", 3, bor(TF.Borders, TF.RowBg)) then + for row = 0, 4 do + ImGui.TableNextRow() + for col = 0, 2 do + ImGui.TableNextColumn() + ImGui.Text("Hello %d,%d", col, row) + if row == 2 and col == 1 then ImGui.TableSetBgColor(G.ImGuiTableBgTarget.CellBg, G.IM_COL32(255, 0, 0, 255)) end + end + end + ImGui.EndTable() + end + + if ImGui.BeginTable("sortable", 4, bor(TF.Resizable, TF.Reorderable, TF.Hideable, TF.Sortable, TF.SortMulti, TF.BordersOuter, TF.BordersV)) then + ImGui.TableSetupColumn("ID", bor(CF.DefaultSort, CF.WidthFixed), 0.0, 0) + ImGui.TableSetupColumn("Name", CF.WidthFixed, 0.0, 1) + ImGui.TableSetupColumn("Action", bor(CF.NoSort, CF.WidthFixed), 0.0, 2) + ImGui.TableSetupColumn("Quantity", bor(CF.PreferSortDescending, CF.WidthStretch), 0.0, 3) + ImGui.TableHeadersRow() + local specs = ImGui.TableGetSortSpecs() + if specs and specs.SpecsDirty then sorted_specs = specs.SpecsCount; specs.SpecsDirty = false end + for row = 0, 5 do + ImGui.TableNextRow() + ImGui.TableNextColumn(); ImGui.Text("%04d", row) + ImGui.TableNextColumn(); ImGui.TextUnformatted("Item " .. row) + ImGui.TableNextColumn(); ImGui.SmallButton("None##" .. row) + ImGui.TableNextColumn(); ImGui.Text("%d", row * 3) + end + hovered_col = ImGui.TableGetHoveredColumn() + ImGui.EndTable() + end + + if ImGui.BeginTable("scroll", 3, bor(TF.ScrollY, TF.ScrollX, TF.RowBg, TF.BordersOuter, TF.BordersV, TF.Resizable, TF.SizingFixedFit), ImVec2(0, 200)) then + ImGui.TableSetupScrollFreeze(1, 1) + ImGui.TableSetupColumn("One", CF.None, 100) + ImGui.TableSetupColumn("Two", CF.None, 200) + ImGui.TableSetupColumn("Three", CF.None, 300) + ImGui.TableHeadersRow() + local clipper = G.ImGuiListClipper() + clipper:Begin(1000) + visible_rows = 0 + while clipper:Step() do + for row = clipper.DisplayStart, clipper.DisplayEnd - 1 do + visible_rows += 1 + ImGui.TableNextRow() + for column = 0, 2 do + ImGui.TableSetColumnIndex(column) + ImGui.Text("Hello %d,%d", column, row) + end + end + end + if frame == 4 then ImGui.SetScrollY(500) end + ImGui.EndTable() + end + + if ImGui.BeginTable("angled", 4, bor(TF.Borders, TF.SizingFixedFit, TF.ScrollX), ImVec2(0, 150)) then + ImGui.TableSetupColumn("Name", CF.NoHide) + for i = 1, 3 do ImGui.TableSetupColumn("Angled " .. i, bor(CF.AngledHeader, CF.WidthFixed)) end + ImGui.TableAngledHeadersRow() + ImGui.TableHeadersRow() + for row = 0, 3 do + ImGui.TableNextRow() + ImGui.TableNextColumn(); ImGui.Text("Track %d", row) + for i = 1, 3 do ImGui.TableNextColumn(); ImGui.Checkbox("##c" .. row .. i, false) end + end + ImGui.EndTable() + end + + -- nested + tree in table + if ImGui.BeginTable("tree", 2, bor(TF.BordersV, TF.Resizable, TF.RowBg)) then + ImGui.TableSetupColumn("Name", CF.NoHide) + ImGui.TableSetupColumn("Size", CF.WidthFixed, 80) + ImGui.TableHeadersRow() + ImGui.TableNextRow(); ImGui.TableNextColumn() + if ImGui.TreeNodeEx("Root", bor(G.ImGuiTreeNodeFlags.SpanAllColumns, G.ImGuiTreeNodeFlags.DefaultOpen)) then + ImGui.TableNextColumn(); ImGui.TextDisabled("--") + ImGui.TableNextRow(); ImGui.TableNextColumn() + ImGui.TreeNodeEx("Leaf", bor(G.ImGuiTreeNodeFlags.Leaf, G.ImGuiTreeNodeFlags.NoTreePushOnOpen, G.ImGuiTreeNodeFlags.SpanAllColumns)) + ImGui.TableNextColumn(); ImGui.Text("123") + ImGui.TreePop() + end + ImGui.EndTable() + end + + ImGui.Columns(3, "legacy") + for i = 0, 5 do ImGui.Text("col item %d", i); ImGui.NextColumn() end + ImGui.Columns(1) + + ImGui.End() + ImGui.Render() + G.ImGui_ImplRoblox.RenderDrawData(ImGui.GetDrawData()) + + if frame == 8 then + assert(sorted_specs == 1, "sort specs " .. tostring(sorted_specs)) + assert(visible_rows > 3 and visible_rows < 30, "clipped rows " .. visible_rows) + local ini = ImGui.SaveIniSettingsToMemory() + assert(string.find(ini, "[Table][", 1, true), "table ini missing") + print(ini) + ImGui.LoadIniSettingsFromMemory(ini) + assert(ImGui.SaveIniSettingsToMemory() == ini, "ini round trip") + print("visible rows", visible_rows, "TABLES OK") + end +end)