diff --git a/imgui.cpp b/imgui.cpp index ad219eb9f..c7be36245 100644 --- a/imgui.cpp +++ b/imgui.cpp @@ -947,6 +947,7 @@ ImGuiStyle::ImGuiStyle() FrameBorderSize = 0.0f; // Thickness of border around frames. Generally set to 0.0f or 1.0f. Other values not well tested. ItemSpacing = ImVec2(8,4); // Horizontal and vertical spacing between widgets/lines ItemInnerSpacing = ImVec2(4,4); // Horizontal and vertical spacing between within elements of a composed widget (e.g. a slider and its label) + CellPadding = ImVec2(4,2); // Padding within a table cell TouchExtraPadding = ImVec2(0,0); // Expand reactive bounding box for touch-based system where touch position is not accurate enough. Unfortunately we don't sort widgets so priority on overlap will always be given to the first widget. So don't grow this too much! IndentSpacing = 21.0f; // Horizontal spacing when e.g. entering a tree node. Generally == (FontSize + FramePadding.x*2). ColumnsMinSpacing = 6.0f; // Minimum horizontal spacing between two columns. Preferably > (FramePadding.x + 1). @@ -987,6 +988,7 @@ void ImGuiStyle::ScaleAllSizes(float scale_factor) FrameRounding = ImFloor(FrameRounding * scale_factor); ItemSpacing = ImFloor(ItemSpacing * scale_factor); ItemInnerSpacing = ImFloor(ItemInnerSpacing * scale_factor); + CellPadding = ImFloor(CellPadding * scale_factor); TouchExtraPadding = ImFloor(TouchExtraPadding * scale_factor); IndentSpacing = ImFloor(IndentSpacing * scale_factor); ColumnsMinSpacing = ImFloor(ColumnsMinSpacing * scale_factor); @@ -2135,6 +2137,14 @@ void ImGuiTextBuffer::appendfv(const char* fmt, va_list args) // the API mid-way through development and support two ways to using the clipper, needs some rework (see TODO) //----------------------------------------------------------------------------- +// FIXME-TABLE: This prevents us from using ImGuiListClipper _inside_ a table cell. +// The problem we have is that without a Begin/End scheme for rows using the clipper is ambiguous. +static bool GetSkipItemForListClipping() +{ + ImGuiContext& g = *GImGui; + return (g.CurrentTable ? g.CurrentTable->BackupSkipItems : g.CurrentWindow->SkipItems); +} + // Helper to calculate coarse clipping of large list of evenly sized items. // NB: Prefer using the ImGuiListClipper higher-level helper if you can! Read comments and instructions there on how those use this sort of pattern. // NB: 'items_count' is only used to clamp the result, if you don't know your count you can use INT_MAX @@ -2149,7 +2159,7 @@ void ImGui::CalcListClipping(int items_count, float items_height, int* out_items *out_items_display_end = items_count; return; } - if (window->SkipItems) + if (GetSkipItemForListClipping()) { *out_items_display_start = *out_items_display_end = 0; return; @@ -2185,12 +2195,20 @@ static void SetCursorPosYAndSetupForPrevLine(float pos_y, float line_height) // The clipper should probably have a 4th step to display the last item in a regular manner. ImGuiContext& g = *GImGui; ImGuiWindow* window = g.CurrentWindow; + float off_y = pos_y - window->DC.CursorPos.y; window->DC.CursorPos.y = pos_y; window->DC.CursorMaxPos.y = ImMax(window->DC.CursorMaxPos.y, pos_y); window->DC.CursorPosPrevLine.y = window->DC.CursorPos.y - line_height; // Setting those fields so that SetScrollHereY() can properly function after the end of our clipper usage. window->DC.PrevLineSize.y = (line_height - g.Style.ItemSpacing.y); // If we end up needing more accurate data (to e.g. use SameLine) we may as well make the clipper have a fourth step to let user process and display the last item in their list. if (ImGuiOldColumns* columns = window->DC.CurrentColumns) - columns->LineMinY = window->DC.CursorPos.y; // Setting this so that cell Y position are set properly + columns->LineMinY = window->DC.CursorPos.y; // Setting this so that cell Y position are set properly // FIXME-TABLE + if (ImGuiTable* table = g.CurrentTable) + { + if (table->IsInsideRow) + ImGui::TableEndRow(table); + table->RowPosY2 = window->DC.CursorPos.y; + table->RowBgColorCounter += (int)((off_y / line_height) + 0.5f); + } } ImGuiListClipper::ImGuiListClipper() @@ -2212,6 +2230,10 @@ void ImGuiListClipper::Begin(int items_count, float items_height) ImGuiContext& g = *GImGui; ImGuiWindow* window = g.CurrentWindow; + if (ImGuiTable* table = g.CurrentTable) + if (table->IsInsideRow) + ImGui::TableEndRow(table); + StartPosY = window->DC.CursorPos.y; ItemsHeight = items_height; ItemsCount = items_count; @@ -2237,8 +2259,12 @@ bool ImGuiListClipper::Step() ImGuiContext& g = *GImGui; ImGuiWindow* window = g.CurrentWindow; + ImGuiTable* table = g.CurrentTable; + if (table && table->IsInsideRow) + ImGui::TableEndRow(table); + // Reached end of list - if (DisplayEnd >= ItemsCount || window->SkipItems) + if (DisplayEnd >= ItemsCount || GetSkipItemForListClipping()) { End(); return false; @@ -2266,7 +2292,17 @@ bool ImGuiListClipper::Step() if (StepNo == 1) { IM_ASSERT(ItemsHeight <= 0.0f); - ItemsHeight = window->DC.CursorPos.y - StartPosY; + if (table) + { + const float pos_y1 = table->RowPosY1; // Using this instead of StartPosY to handle clipper straddling the frozen row + const float pos_y2 = table->RowPosY2; // Using this instead of CursorPos.y to take account of tallest cell. + ItemsHeight = pos_y2 - pos_y1; + window->DC.CursorPos.y = pos_y2; + } + else + { + ItemsHeight = window->DC.CursorPos.y - StartPosY; + } IM_ASSERT(ItemsHeight > 0.0f && "Unable to calculate item height! First item hasn't moved the cursor vertically!"); StepNo = 2; } @@ -2405,6 +2441,7 @@ static const ImGuiStyleVarInfo GStyleVarInfo[] = { ImGuiDataType_Float, 2, (ImU32)IM_OFFSETOF(ImGuiStyle, ItemSpacing) }, // ImGuiStyleVar_ItemSpacing { ImGuiDataType_Float, 2, (ImU32)IM_OFFSETOF(ImGuiStyle, ItemInnerSpacing) }, // ImGuiStyleVar_ItemInnerSpacing { ImGuiDataType_Float, 1, (ImU32)IM_OFFSETOF(ImGuiStyle, IndentSpacing) }, // ImGuiStyleVar_IndentSpacing + { ImGuiDataType_Float, 2, (ImU32)IM_OFFSETOF(ImGuiStyle, CellPadding) }, // ImGuiStyleVar_CellPadding { ImGuiDataType_Float, 1, (ImU32)IM_OFFSETOF(ImGuiStyle, ScrollbarSize) }, // ImGuiStyleVar_ScrollbarSize { ImGuiDataType_Float, 1, (ImU32)IM_OFFSETOF(ImGuiStyle, ScrollbarRounding) }, // ImGuiStyleVar_ScrollbarRounding { ImGuiDataType_Float, 1, (ImU32)IM_OFFSETOF(ImGuiStyle, GrabMinSize) }, // ImGuiStyleVar_GrabMinSize @@ -2512,6 +2549,9 @@ const char* ImGui::GetStyleColorName(ImGuiCol idx) case ImGuiCol_PlotLinesHovered: return "PlotLinesHovered"; case ImGuiCol_PlotHistogram: return "PlotHistogram"; case ImGuiCol_PlotHistogramHovered: return "PlotHistogramHovered"; + case ImGuiCol_TableHeaderBg: return "TableHeaderBg"; + case ImGuiCol_TableRowBg: return "TableRowBg"; + case ImGuiCol_TableRowBgAlt: return "TableRowBgAlt"; case ImGuiCol_TextSelectedBg: return "TextSelectedBg"; case ImGuiCol_DragDropTarget: return "DragDropTarget"; case ImGuiCol_NavHighlight: return "NavHighlight"; @@ -3998,6 +4038,10 @@ void ImGui::Shutdown(ImGuiContext* context) g.CurrentTabBarStack.clear(); g.ShrinkWidthBuffer.clear(); + g.Tables.Clear(); + g.CurrentTableStack.clear(); + g.DrawChannelsTempMergeBuffer.clear(); + g.ClipboardHandlerData.clear(); g.MenusIdSubmittedThisFrame.clear(); g.InputTextState.ClearFreeMemory(); @@ -7374,7 +7418,7 @@ ImVec2 ImGui::GetContentRegionMax() ImGuiContext& g = *GImGui; ImGuiWindow* window = g.CurrentWindow; ImVec2 mx = window->ContentRegionRect.Max - window->Pos; - if (window->DC.CurrentColumns) + if (window->DC.CurrentColumns || g.CurrentTable) mx.x = window->WorkRect.Max.x - window->Pos.x; return mx; } @@ -7385,7 +7429,7 @@ ImVec2 ImGui::GetContentRegionMaxAbs() ImGuiContext& g = *GImGui; ImGuiWindow* window = g.CurrentWindow; ImVec2 mx = window->ContentRegionRect.Max; - if (window->DC.CurrentColumns) + if (window->DC.CurrentColumns || g.CurrentTable) mx.x = window->WorkRect.Max.x; return mx; } @@ -10459,6 +10503,23 @@ void ImGui::ShowMetricsWindow(bool* p_open) struct Funcs { + static ImRect GetTableRect(ImGuiTable* table, int rect_type, int n) + { + if (rect_type == TRT_OuterRect) { return table->OuterRect; } + else if (rect_type == TRT_WorkRect) { return table->WorkRect; } + else if (rect_type == TRT_HostClipRect) { return table->HostClipRect; } + else if (rect_type == TRT_InnerClipRect) { return table->InnerClipRect; } + else if (rect_type == TRT_BackgroundClipRect) { return table->BackgroundClipRect; } + else if (rect_type == TRT_ColumnsRect) { ImGuiTableColumn* c = &table->Columns[n]; return ImRect(c->MinX, table->InnerClipRect.Min.y, c->MaxX, table->InnerClipRect.Min.y + table->LastOuterHeight); } + else if (rect_type == TRT_ColumnsClipRect) { ImGuiTableColumn* c = &table->Columns[n]; return c->ClipRect; } + else if (rect_type == TRT_ColumnsContentHeadersUsed) { ImGuiTableColumn* c = &table->Columns[n]; return ImRect(c->MinX, table->InnerClipRect.Min.y, c->MinX + c->ContentWidthHeadersUsed, table->InnerClipRect.Min.y + table->LastFirstRowHeight); } // Note: y1/y2 not always accurate + else if (rect_type == TRT_ColumnsContentHeadersIdeal) { ImGuiTableColumn* c = &table->Columns[n]; return ImRect(c->MinX, table->InnerClipRect.Min.y, c->MinX + c->ContentWidthHeadersDesired, table->InnerClipRect.Min.y + table->LastFirstRowHeight); } // " + else if (rect_type == TRT_ColumnsContentRowsFrozen) { ImGuiTableColumn* c = &table->Columns[n]; return ImRect(c->MinX, table->InnerClipRect.Min.y, c->MinX + c->ContentWidthRowsFrozen, table->InnerClipRect.Min.y + table->LastFirstRowHeight); } // " + else if (rect_type == TRT_ColumnsContentRowsUnfrozen) { ImGuiTableColumn* c = &table->Columns[n]; return ImRect(c->MinX, table->InnerClipRect.Min.y + table->LastFirstRowHeight, c->MinX + c->ContentWidthRowsUnfrozen, table->InnerClipRect.Max.y); } // " + IM_ASSERT(0); + return ImRect(); + } + static ImRect GetWindowRect(ImGuiWindow* window, int rect_type) { if (rect_type == WRT_OuterRect) { return window->Rect(); } @@ -10500,6 +10561,49 @@ void ImGui::ShowMetricsWindow(bool* p_open) } Checkbox("Show ImDrawCmd mesh when hovering", &cfg->ShowDrawCmdMesh); Checkbox("Show ImDrawCmd bounding boxes when hovering", &cfg->ShowDrawCmdBoundingBoxes); + + Checkbox("Show tables rectangles", &cfg->ShowTablesRects); + SameLine(); + SetNextItemWidth(GetFontSize() * 12); + cfg->ShowTablesRects |= Combo("##show_table_rects_type", &cfg->ShowTablesRectsType, trt_rects_names, TRT_Count, TRT_Count); + if (cfg->ShowTablesRects && g.NavWindow != NULL) + { + for (int table_n = 0; table_n < g.Tables.GetSize(); table_n++) + { + ImGuiTable* table = g.Tables.GetByIndex(table_n); + if (table->LastFrameActive < g.FrameCount - 1 || table->OuterWindow != g.NavWindow) + continue; + + BulletText("Table 0x%08X (%d columns, in '%s')", table->ID, table->ColumnsCount, table->OuterWindow->Name); + if (IsItemHovered()) + GetForegroundDrawList()->AddRect(table->OuterRect.Min - ImVec2(1, 1), table->OuterRect.Max + ImVec2(1, 1), IM_COL32(255, 255, 0, 255), 0.0f, ~0, 2.0f); + Indent(); + for (int rect_n = 0; rect_n < TRT_Count; rect_n++) + { + if (rect_n >= TRT_ColumnsRect) + { + if (rect_n != TRT_ColumnsRect && rect_n != TRT_ColumnsClipRect) + continue; + for (int column_n = 0; column_n < table->ColumnsCount; column_n++) + { + ImRect r = Funcs::GetTableRect(table, rect_n, column_n); + Text("(%6.1f,%6.1f) (%6.1f,%6.1f) Size (%6.1f,%6.1f) Col %d %s", r.Min.x, r.Min.y, r.Max.x, r.Max.y, r.GetWidth(), r.GetHeight(), column_n, trt_rects_names[rect_n]); + if (IsItemHovered()) + GetForegroundDrawList()->AddRect(r.Min - ImVec2(1, 1), r.Max + ImVec2(1, 1), IM_COL32(255, 255, 0, 255), 0.0f, ~0, 2.0f); + } + } + else + { + ImRect r = Funcs::GetTableRect(table, rect_n, -1); + Text("(%6.1f,%6.1f) (%6.1f,%6.1f) Size (%6.1f,%6.1f) %s", r.Min.x, r.Min.y, r.Max.x, r.Max.y, r.GetWidth(), r.GetHeight(), trt_rects_names[rect_n]); + if (IsItemHovered()) + GetForegroundDrawList()->AddRect(r.Min - ImVec2(1, 1), r.Max + ImVec2(1, 1), IM_COL32(255, 255, 0, 255), 0.0f, ~0, 2.0f); + } + } + Unindent(); + } + } + TreePop(); } @@ -10533,7 +10637,6 @@ void ImGui::ShowMetricsWindow(bool* p_open) } // Details for Tables - IM_UNUSED(trt_rects_names); #ifdef IMGUI_HAS_TABLE if (TreeNode("Tables", "Tables (%d)", g.Tables.GetSize())) { @@ -10584,8 +10687,8 @@ void ImGui::ShowMetricsWindow(bool* p_open) #ifdef IMGUI_HAS_TABLE if (TreeNode("SettingsTables", "Settings packed data: Tables: %d bytes", g.SettingsTables.size())) { - for (ImGuiTableSettings* settings = g.SettingsTables.begin(); settings != NULL; settings = g.SettingsTables.next_chunk(settings)) - DebugNodeTableSettings(settings); + //for (ImGuiTableSettings* settings = g.SettingsTables.begin(); settings != NULL; settings = g.SettingsTables.next_chunk(settings)) + // DebugNodeTableSettings(settings); TreePop(); } #endif // #ifdef IMGUI_HAS_TABLE @@ -10664,11 +10767,29 @@ void ImGui::ShowMetricsWindow(bool* p_open) #ifdef IMGUI_HAS_TABLE // Overlay: Display Tables Rectangles - if (show_tables_rects) + if (cfg->ShowTablesRects) { for (int table_n = 0; table_n < g.Tables.GetSize(); table_n++) { ImGuiTable* table = g.Tables.GetByIndex(table_n); + if (table->LastFrameActive < g.FrameCount - 1) + continue; + ImDrawList* draw_list = GetForegroundDrawList(table->OuterWindow); + if (cfg->ShowTablesRectsType >= TRT_ColumnsRect) + { + for (int column_n = 0; column_n < table->ColumnsCount; column_n++) + { + ImRect r = Funcs::GetTableRect(table, cfg->ShowTablesRectsType, column_n); + ImU32 col = (table->HoveredColumnBody == column_n) ? IM_COL32(255, 255, 128, 255) : IM_COL32(255, 0, 128, 255); + float thickness = (table->HoveredColumnBody == column_n) ? 3.0f : 1.0f; + draw_list->AddRect(r.Min, r.Max, col, 0.0f, ~0, thickness); + } + } + else + { + ImRect r = Funcs::GetTableRect(table, cfg->ShowTablesRectsType, -1); + draw_list->AddRect(r.Min, r.Max, IM_COL32(255, 0, 128, 255)); + } } } #endif // #ifdef IMGUI_HAS_TABLE diff --git a/imgui.h b/imgui.h index d1d42b3c2..69ef0d428 100644 --- a/imgui.h +++ b/imgui.h @@ -33,6 +33,8 @@ Index of this file: // Draw List API (ImDrawCallback, ImDrawCmd, ImDrawIdx, ImDrawVert, ImDrawChannel, ImDrawListSplitter, ImDrawListFlags, ImDrawList, ImDrawData) // Font API (ImFontConfig, ImFontGlyph, ImFontGlyphRangesBuilder, ImFontAtlasFlags, ImFontAtlas, ImFont) +// FIXME-TABLE: Add ImGuiTableSortSpecsColumn and ImGuiTableSortSpecs in "Misc data structures" section above (we don't do it right now to facilitate merging various branches) + */ #pragma once @@ -136,6 +138,8 @@ struct ImGuiPayload; // User data payload for drag and drop opera struct ImGuiSizeCallbackData; // Callback data when using SetNextWindowSizeConstraints() (rare/advanced use) struct ImGuiStorage; // Helper for key->value storage struct ImGuiStyle; // Runtime data for styling/colors +struct ImGuiTableSortSpecs; // Sorting specifications for a table (often handling sort specs for a single column, occasionally more) +struct ImGuiTableSortSpecsColumn; // Sorting specification for one column of a table struct ImGuiTextBuffer; // Helper to hold and append into a text buffer (~string builder) struct ImGuiTextFilter; // Helper to parse and apply text filters (e.g. "aaaaa[,bbbbb][,ccccc]") @@ -151,6 +155,7 @@ typedef int ImGuiKey; // -> enum ImGuiKey_ // Enum: A typedef int ImGuiNavInput; // -> enum ImGuiNavInput_ // Enum: An input identifier for navigation typedef int ImGuiMouseButton; // -> enum ImGuiMouseButton_ // Enum: A mouse button identifier (0=left, 1=right, 2=middle) typedef int ImGuiMouseCursor; // -> enum ImGuiMouseCursor_ // Enum: A mouse cursor identifier +typedef int ImGuiSortDirection; // -> enum ImGuiSortDirection_ // Enum: A sorting direction (ascending or descending) typedef int ImGuiStyleVar; // -> enum ImGuiStyleVar_ // Enum: A variable identifier for styling typedef int ImDrawCornerFlags; // -> enum ImDrawCornerFlags_ // Flags: for ImDrawList::AddRect(), AddRectFilled() etc. typedef int ImDrawListFlags; // -> enum ImDrawListFlags_ // Flags: for ImDrawList @@ -170,6 +175,9 @@ typedef int ImGuiSelectableFlags; // -> enum ImGuiSelectableFlags_ // Flags: f typedef int ImGuiSliderFlags; // -> enum ImGuiSliderFlags_ // Flags: for DragFloat(), DragInt(), SliderFloat(), SliderInt() etc. typedef int ImGuiTabBarFlags; // -> enum ImGuiTabBarFlags_ // Flags: for BeginTabBar() typedef int ImGuiTabItemFlags; // -> enum ImGuiTabItemFlags_ // Flags: for BeginTabItem() +typedef int ImGuiTableFlags; // -> enum ImGuiTableFlags_ // Flags: For BeginTable() +typedef int ImGuiTableColumnFlags; // -> enum ImGuiTableColumnFlags_// Flags: For TableSetupColumn() +typedef int ImGuiTableRowFlags; // -> enum ImGuiTableRowFlags_ // Flags: For TableNextRow() typedef int ImGuiTreeNodeFlags; // -> enum ImGuiTreeNodeFlags_ // Flags: for TreeNode(), TreeNodeEx(), CollapsingHeader() typedef int ImGuiWindowFlags; // -> enum ImGuiWindowFlags_ // Flags: for Begin(), BeginChild() @@ -657,6 +665,35 @@ namespace ImGui IMGUI_API void SetColumnOffset(int column_index, float offset_x); // set position of column line (in pixels, from the left side of the contents region). pass -1 to use current column IMGUI_API int GetColumnsCount(); + // Tables + // [ALPHA API] API will evolve! (FIXME-TABLE) + // - Full-featured replacement for old Columns API + // - In most situations you can use TableNextRow() + TableSetColumnIndex() to populate a table. + // - If you are using tables as a sort of grid, populating every columns with the same type of contents, + // you may prefer using TableNextCell() instead of TableNextRow() + TableSetColumnIndex(). + #define IMGUI_HAS_TABLE 1 + IMGUI_API bool BeginTable(const char* str_id, int columns_count, ImGuiTableFlags flags = 0, const ImVec2& outer_size = ImVec2(0, 0), float inner_width = 0.0f); + IMGUI_API void EndTable(); // only call EndTable() if BeginTable() returns true! + IMGUI_API void TableNextRow(ImGuiTableRowFlags row_flags = 0, float min_row_height = 0.0f); // append into the first cell of a new row. + IMGUI_API bool TableNextCell(); // append into the next column (next column, or next row if currently in last column). Return true if column is visible. + IMGUI_API bool TableSetColumnIndex(int column_n); // append into the specified column. Return true if column is visible. + IMGUI_API int TableGetColumnIndex(); // return current column index. + IMGUI_API const char* TableGetColumnName(int column_n = -1); // return NULL if column didn't have a name declared by TableSetupColumn(). Use pass -1 to use current column. + IMGUI_API bool TableGetColumnIsVisible(int column_n = -1); // return true if column is visible. Same value is also returned by TableNextCell() and TableSetColumnIndex(). Use pass -1 to use current column. + IMGUI_API bool TableGetColumnIsSorted(int column_n = -1); // return true if column is included in the sort specs. Rarely used, can be useful to tell if a data change should trigger resort. Equivalent to test ImGuiTableSortSpecs's ->ColumnsMask & (1 << column_n). Use pass -1 to use current column. + // Tables: Headers & Columns declaration + // - Use TableSetupColumn() to specify resizing policy, default width, name, id, specific flags etc. + // - The name passed to TableSetupColumn() is used by TableAutoHeaders() and by the context-menu + // - Use TableAutoHeaders() to submit the whole header row, otherwise you may treat the header row as a regular row, manually call TableHeader() and other widgets. + // - Headers are required to perform some interactions: reordering, sorting, context menu // FIXME-TABLES: remove context from this list! + IMGUI_API void TableSetupColumn(const char* label, ImGuiTableColumnFlags flags = 0, float init_width_or_weight = -1.0f, ImU32 user_id = 0); + IMGUI_API void TableAutoHeaders(); // submit all headers cells based on data provided to TableSetupColumn() + submit context menu + IMGUI_API void TableHeader(const char* label); // submit one header cell manually. + // Tables: Sorting + // - Call TableGetSortSpecs() to retrieve latest sort specs for the table. Return value will be NULL if no sorting. + // - Read ->SpecsChanged to tell if the specs have changed since last call. + IMGUI_API const ImGuiTableSortSpecs* TableGetSortSpecs(); // get latest sort specs for the table (NULL if not sorting). + // Tab Bars, Tabs IMGUI_API bool BeginTabBar(const char* str_id, ImGuiTabBarFlags flags = 0); // create and append into a TabBar IMGUI_API void EndTabBar(); // only call EndTabBar() if BeginTabBar() returns true! @@ -967,6 +1004,85 @@ enum ImGuiTabItemFlags_ ImGuiTabItemFlags_Trailing = 1 << 7 // Enforce the tab position to the right of the tab bar (before the scrolling buttons) }; +// Flags for ImGui::BeginTable() +enum ImGuiTableFlags_ +{ + // Features + ImGuiTableFlags_None = 0, + ImGuiTableFlags_Resizable = 1 << 0, // Allow resizing columns. + ImGuiTableFlags_Reorderable = 1 << 1, // Allow reordering columns (need calling TableSetupColumn() + TableAutoHeaders() or TableHeaders() to display headers) + ImGuiTableFlags_Hideable = 1 << 2, // Allow hiding columns (with right-click on header) (FIXME-TABLE: allow without headers). + ImGuiTableFlags_Sortable = 1 << 3, // Allow sorting on one column (sort_specs_count will always be == 1). Call TableGetSortSpecs() to obtain sort specs. + ImGuiTableFlags_MultiSortable = 1 << 4, // Allow sorting on multiple columns by holding Shift (sort_specs_count may be > 1). Call TableGetSortSpecs() to obtain sort specs. + ImGuiTableFlags_NoSavedSettings = 1 << 5, // Disable persisting columns order, width and sort settings in the .ini file. + // Decoration + ImGuiTableFlags_RowBg = 1 << 6, // Use ImGuiCol_TableRowBg and ImGuiCol_TableRowBgAlt colors behind each rows. + ImGuiTableFlags_BordersOuter = 1 << 7, // Draw outer borders. + ImGuiTableFlags_BordersV = 1 << 8, // Draw vertical borders between columns. + ImGuiTableFlags_BordersH = 1 << 9, // Draw horizontal borders between rows. + ImGuiTableFlags_BordersFullHeight = 1 << 10, // Borders covers all lines even when Headers are being used, allow resizing all rows. + ImGuiTableFlags_Borders = ImGuiTableFlags_BordersOuter | ImGuiTableFlags_BordersV | ImGuiTableFlags_BordersH, + // Padding, Sizing + ImGuiTableFlags_NoClipX = 1 << 12, // Disable pushing clipping rectangle for every individual columns (reduce draw command count, items with be able to overflow) + ImGuiTableFlags_SizingPolicyStretchX = 1 << 13, // (Default if ScrollX is off) Columns will default to use ImGuiTableColumnFlags_WidthStretch. Fit all columns within available width. Fixed and Weighted columns allowed. + ImGuiTableFlags_SizingPolicyFixedX = 1 << 14, // (Default if ScrollX is on) Columns will default to use ImGuiTableColumnFlags_WidthFixed or WidthAuto. Enlarge as needed: enable scrollbar if ScrollX is enabled, otherwise extend parent window's contents rect. Only Fixed columns allowed. Weighted columns will calculate their width assuming no scrolling. + ImGuiTableFlags_NoHeadersWidth = 1 << 15, // Disable header width contribute to automatic width calculation for every columns. + ImGuiTableFlags_NoHostExtendY = 1 << 16, // (FIXME-TABLE: Reword as SizingPolicy?) Disable extending past the limit set by outer_size.y, only meaningful when neither of ScrollX|ScrollY are set (data below the limit will be clipped and not visible) + // Scrolling + ImGuiTableFlags_ScrollX = 1 << 17, // Enable horizontal scrolling. Require 'outer_size' parameter of BeginTable() to specify the container size. Because this create a child window, ScrollY is currently generally recommended when using ScrollX. + ImGuiTableFlags_ScrollY = 1 << 18, // Enable vertical scrolling. Require 'outer_size' parameter of BeginTable() to specify the container size. + ImGuiTableFlags_Scroll = ImGuiTableFlags_ScrollX | ImGuiTableFlags_ScrollY, + ImGuiTableFlags_ScrollFreezeRowsShift_ = 19, // We can lock 1 to 3 rows (starting from the top). Encode each of those values as dedicated flags. + ImGuiTableFlags_ScrollFreezeTopRow = 1 << ImGuiTableFlags_ScrollFreezeRowsShift_, + ImGuiTableFlags_ScrollFreeze2Rows = 2 << ImGuiTableFlags_ScrollFreezeRowsShift_, + ImGuiTableFlags_ScrollFreeze3Rows = 3 << ImGuiTableFlags_ScrollFreezeRowsShift_, + ImGuiTableFlags_ScrollFreezeColumnsShift_ = 21, // We can lock 1 to 3 columns (starting from the left). Encode each of those values as dedicated flags. + ImGuiTableFlags_ScrollFreezeLeftColumn = 1 << ImGuiTableFlags_ScrollFreezeColumnsShift_, + ImGuiTableFlags_ScrollFreeze2Columns = 2 << ImGuiTableFlags_ScrollFreezeColumnsShift_, + ImGuiTableFlags_ScrollFreeze3Columns = 3 << ImGuiTableFlags_ScrollFreezeColumnsShift_, + + // Combinations and masks + ImGuiTableFlags_SizingPolicyMaskX_ = ImGuiTableFlags_SizingPolicyStretchX | ImGuiTableFlags_SizingPolicyFixedX, + ImGuiTableFlags_ScrollFreezeRowsMask_ = 0x03 << ImGuiTableFlags_ScrollFreezeRowsShift_, + ImGuiTableFlags_ScrollFreezeColumnsMask_ = 0x03 << ImGuiTableFlags_ScrollFreezeColumnsShift_ +}; + +// Flags for ImGui::TableSetupColumn() +// FIXME-TABLE: Rename to ImGuiColumns_*, stick old columns api flags in there under an obsolete api block +enum ImGuiTableColumnFlags_ +{ + ImGuiTableColumnFlags_None = 0, + ImGuiTableColumnFlags_DefaultHide = 1 << 0, // Default as a hidden column. + ImGuiTableColumnFlags_DefaultSort = 1 << 1, // Default as a sorting column. + ImGuiTableColumnFlags_WidthFixed = 1 << 2, // Column will keep a fixed size, preferable with horizontal scrolling enabled (default if table sizing policy is SizingPolicyFixedX). + ImGuiTableColumnFlags_WidthStretch = 1 << 3, // Column will stretch, preferable with horizontal scrolling disabled (default if table sizing policy is SizingPolicyStretchX). + ImGuiTableColumnFlags_WidthAlwaysAutoResize = 1 << 4, // Column will keep resizing based on submitted contents (with a one frame delay) == Fixed with auto resize + ImGuiTableColumnFlags_NoResize = 1 << 5, // Disable manual resizing. + ImGuiTableColumnFlags_NoClipX = 1 << 6, // Disable clipping for this column (all NoClipX columns will render in a same draw command). + ImGuiTableColumnFlags_NoSort = 1 << 7, // Disable ability to sort on this field (even if ImGuiTableFlags_Sortable is set on the table). + ImGuiTableColumnFlags_NoSortAscending = 1 << 8, // Disable ability to sort in the ascending direction. + ImGuiTableColumnFlags_NoSortDescending = 1 << 9, // Disable ability to sort in the descending direction. + ImGuiTableColumnFlags_NoHide = 1 << 10, // Disable hiding this column. + ImGuiTableColumnFlags_NoHeaderWidth = 1 << 11, // Header width don't contribute to automatic column width. + ImGuiTableColumnFlags_PreferSortAscending = 1 << 12, // Make the initial sort direction Ascending when first sorting on this column (default). + ImGuiTableColumnFlags_PreferSortDescending = 1 << 13, // Make the initial sort direction Descending when first sorting on this column. + //ImGuiTableColumnFlags_AlignLeft = 1 << 14, + //ImGuiTableColumnFlags_AlignCenter = 1 << 15, + //ImGuiTableColumnFlags_AlignRight = 1 << 16, + + // Combinations and masks + ImGuiTableColumnFlags_WidthMask_ = ImGuiTableColumnFlags_WidthFixed | ImGuiTableColumnFlags_WidthStretch | ImGuiTableColumnFlags_WidthAlwaysAutoResize, + ImGuiTableColumnFlags_NoDirectResize_ = 1 << 20 // [Internal] Disable user resizing this column directly (it may however we resized indirectly from its left edge) + //ImGuiTableColumnFlags_AlignMask_ = ImGuiTableColumnFlags_AlignLeft | ImGuiTableColumnFlags_AlignCenter | ImGuiTableColumnFlags_AlignRight +}; + +// Flags for ImGui::TableNextRow() +enum ImGuiTableRowFlags_ +{ + ImGuiTableRowFlags_None = 0, + ImGuiTableRowFlags_Headers = 1 << 0 // Identify header row (set default background color + width of its contents accounted different for auto column width) +}; + // Flags for ImGui::IsWindowFocused() enum ImGuiFocusedFlags_ { @@ -1044,6 +1160,14 @@ enum ImGuiDir_ ImGuiDir_COUNT }; +// A sorting direction +enum ImGuiSortDirection_ +{ + ImGuiSortDirection_None = 0, + ImGuiSortDirection_Ascending = 1, // Ascending = 0->9, A->Z etc. + ImGuiSortDirection_Descending = 2 // Descending = 9->0, Z->A etc. +}; + // User fill ImGuiIO.KeyMap[] array with indices into the ImGuiIO.KeysDown[512] array enum ImGuiKey_ { @@ -1188,6 +1312,9 @@ enum ImGuiCol_ ImGuiCol_PlotLinesHovered, ImGuiCol_PlotHistogram, ImGuiCol_PlotHistogramHovered, + ImGuiCol_TableHeaderBg, // Table header background + ImGuiCol_TableRowBg, // Table row background (even rows) + ImGuiCol_TableRowBgAlt, // Table row background (odd rows) ImGuiCol_TextSelectedBg, ImGuiCol_DragDropTarget, ImGuiCol_NavHighlight, // Gamepad/keyboard: current highlighted item @@ -1228,6 +1355,7 @@ enum ImGuiStyleVar_ ImGuiStyleVar_ItemSpacing, // ImVec2 ItemSpacing ImGuiStyleVar_ItemInnerSpacing, // ImVec2 ItemInnerSpacing ImGuiStyleVar_IndentSpacing, // float IndentSpacing + ImGuiStyleVar_CellPadding, // ImVec2 CellPadding ImGuiStyleVar_ScrollbarSize, // float ScrollbarSize ImGuiStyleVar_ScrollbarRounding, // float ScrollbarRounding ImGuiStyleVar_GrabMinSize, // float GrabMinSize @@ -1464,6 +1592,7 @@ struct ImGuiStyle float FrameBorderSize; // Thickness of border around frames. Generally set to 0.0f or 1.0f. (Other values are not well tested and more CPU/GPU costly). ImVec2 ItemSpacing; // Horizontal and vertical spacing between widgets/lines. ImVec2 ItemInnerSpacing; // Horizontal and vertical spacing between within elements of a composed widget (e.g. a slider and its label). + ImVec2 CellPadding; // Padding within a table cell ImVec2 TouchExtraPadding; // Expand reactive bounding box for touch-based system where touch position is not accurate enough. Unfortunately we don't sort widgets so priority on overlap will always be given to the first widget. So don't grow this too much! float IndentSpacing; // Horizontal indentation when e.g. entering a tree node. Generally == (FontSize + FramePadding.x*2). float ColumnsMinSpacing; // Minimum horizontal spacing between two columns. Preferably > (FramePadding.x + 1). @@ -1700,6 +1829,30 @@ struct ImGuiPayload bool IsDelivery() const { return Delivery; } }; +// Sorting specification for one column of a table (sizeof == 8 bytes) +struct ImGuiTableSortSpecsColumn +{ + ImGuiID ColumnUserID; // User id of the column (if specified by a TableSetupColumn() call) + ImU8 ColumnIndex; // Index of the column + ImU8 SortOrder; // Index within parent ImGuiTableSortSpecs (always stored in order starting from 0, tables sorted on a single criteria will always have a 0 here) + ImS8 SortSign; // +1 or -1 (you can use this or SortDirection, whichever is more convenient for your sort function) + ImS8 SortDirection; // ImGuiSortDirection_Ascending or ImGuiSortDirection_Descending (you can use this or SortSign, whichever is more convenient for your sort function) + + ImGuiTableSortSpecsColumn() { ColumnUserID = 0; ColumnIndex = 0; SortOrder = 0; SortSign = +1; SortDirection = ImGuiSortDirection_Ascending; } +}; + +// Sorting specifications for a table (often handling sort specs for a single column, occasionally more) +// Obtained by calling TableGetSortSpecs() +struct ImGuiTableSortSpecs +{ + const ImGuiTableSortSpecsColumn* Specs; // Pointer to sort spec array. + int SpecsCount; // Sort spec count. Most often 1 unless e.g. ImGuiTableFlags_MultiSortable is enabled. + bool SpecsChanged; // Set to true by TableGetSortSpecs() call if the specs have changed since the previous call. + ImU64 ColumnsMask; // Set to the mask of column indexes included in the Specs array. e.g. (1 << N) when column N is sorted. + + ImGuiTableSortSpecs() { Specs = NULL; SpecsCount = 0; SpecsChanged = false; ColumnsMask = 0x00; } +}; + //----------------------------------------------------------------------------- // Obsolete functions (Will be removed! Read 'API BREAKING CHANGES' section in imgui.cpp for details) // Please keep your copy of dear imgui up to date! Occasionally set '#define IMGUI_DISABLE_OBSOLETE_FUNCTIONS' in imconfig.h to stay ahead. diff --git a/imgui_draw.cpp b/imgui_draw.cpp index 93eb23f20..e915dd76c 100644 --- a/imgui_draw.cpp +++ b/imgui_draw.cpp @@ -222,6 +222,9 @@ void ImGui::StyleColorsDark(ImGuiStyle* dst) colors[ImGuiCol_PlotLinesHovered] = ImVec4(1.00f, 0.43f, 0.35f, 1.00f); colors[ImGuiCol_PlotHistogram] = ImVec4(0.90f, 0.70f, 0.00f, 1.00f); colors[ImGuiCol_PlotHistogramHovered] = ImVec4(1.00f, 0.60f, 0.00f, 1.00f); + colors[ImGuiCol_TableHeaderBg] = ImVec4(0.19f, 0.19f, 0.20f, 1.00f); + colors[ImGuiCol_TableRowBg] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f); + colors[ImGuiCol_TableRowBgAlt] = ImVec4(1.00f, 1.00f, 1.00f, 0.07f); colors[ImGuiCol_TextSelectedBg] = ImVec4(0.26f, 0.59f, 0.98f, 0.35f); colors[ImGuiCol_DragDropTarget] = ImVec4(1.00f, 1.00f, 0.00f, 0.90f); colors[ImGuiCol_NavHighlight] = ImVec4(0.26f, 0.59f, 0.98f, 1.00f); @@ -277,6 +280,9 @@ void ImGui::StyleColorsClassic(ImGuiStyle* dst) colors[ImGuiCol_PlotLinesHovered] = ImVec4(0.90f, 0.70f, 0.00f, 1.00f); colors[ImGuiCol_PlotHistogram] = ImVec4(0.90f, 0.70f, 0.00f, 1.00f); colors[ImGuiCol_PlotHistogramHovered] = ImVec4(1.00f, 0.60f, 0.00f, 1.00f); + colors[ImGuiCol_TableHeaderBg] = ImVec4(0.27f, 0.27f, 0.38f, 1.00f); + colors[ImGuiCol_TableRowBg] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f); + colors[ImGuiCol_TableRowBgAlt] = ImVec4(1.00f, 1.00f, 1.00f, 0.07f); colors[ImGuiCol_TextSelectedBg] = ImVec4(0.00f, 0.00f, 1.00f, 0.35f); colors[ImGuiCol_DragDropTarget] = ImVec4(1.00f, 1.00f, 0.00f, 0.90f); colors[ImGuiCol_NavHighlight] = colors[ImGuiCol_HeaderHovered]; @@ -333,6 +339,9 @@ void ImGui::StyleColorsLight(ImGuiStyle* dst) colors[ImGuiCol_PlotLinesHovered] = ImVec4(1.00f, 0.43f, 0.35f, 1.00f); colors[ImGuiCol_PlotHistogram] = ImVec4(0.90f, 0.70f, 0.00f, 1.00f); colors[ImGuiCol_PlotHistogramHovered] = ImVec4(1.00f, 0.45f, 0.00f, 1.00f); + colors[ImGuiCol_TableHeaderBg] = ImVec4(0.78f, 0.87f, 0.98f, 1.00f); + colors[ImGuiCol_TableRowBg] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f); + colors[ImGuiCol_TableRowBgAlt] = ImVec4(0.30f, 0.30f, 0.30f, 0.07f); colors[ImGuiCol_TextSelectedBg] = ImVec4(0.26f, 0.59f, 0.98f, 0.35f); colors[ImGuiCol_DragDropTarget] = ImVec4(0.26f, 0.59f, 0.98f, 0.95f); colors[ImGuiCol_NavHighlight] = colors[ImGuiCol_HeaderHovered]; diff --git a/imgui_internal.h b/imgui_internal.h index 33cc991e4..5ec83845d 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -111,6 +111,10 @@ struct ImGuiStackSizes; // Storage of stack sizes for debugging/asse struct ImGuiStyleMod; // Stacked style modifier, backup of modified data so we can restore it struct ImGuiTabBar; // Storage for a tab bar struct ImGuiTabItem; // Storage for a tab item (within a tab bar) +struct ImGuiTable; // Storage for a table +struct ImGuiTableColumn; // Storage for one column of a table +struct ImGuiTableSettings; // Storage for a table .ini settings +struct ImGuiTableColumnsSettings; // Storage for a column .ini settings struct ImGuiWindow; // Storage for one window struct ImGuiWindowTempData; // Temporary storage for one window (that's the data which in theory we could ditch at the end of the frame) struct ImGuiWindowSettings; // Storage for a window .ini settings (we keep one of those even if the actual window wasn't instanced during this session) @@ -264,6 +268,7 @@ IMGUI_API ImU32 ImAlphaBlendColors(ImU32 col_a, ImU32 col_b); // Helpers: Bit manipulation static inline bool ImIsPowerOfTwo(int v) { return v != 0 && (v & (v - 1)) == 0; } +static inline bool ImIsPowerOfTwo(ImU64 v) { return v != 0 && (v & (v - 1)) == 0; } static inline int ImUpperPowerOfTwo(int v) { v--; v |= v >> 1; v |= v >> 2; v |= v >> 4; v |= v >> 8; v |= v >> 16; v++; return v; } // Helpers: String, Formatting @@ -1329,6 +1334,12 @@ struct ImGuiContext ImVector DragDropPayloadBufHeap; // We don't expose the ImVector<> directly, ImGuiPayload only holds pointer+size unsigned char DragDropPayloadBufLocal[16]; // Local buffer for small payloads + // Table + ImGuiTable* CurrentTable; + ImPool Tables; + ImVector CurrentTableStack; + ImVector DrawChannelsTempMergeBuffer; + // Tab bars ImGuiTabBar* CurrentTabBar; ImPool TabBars; @@ -1366,6 +1377,7 @@ struct ImGuiContext ImGuiTextBuffer SettingsIniData; // In memory .ini settings ImVector SettingsHandlers; // List of .ini settings handlers ImChunkStream SettingsWindows; // ImGuiWindow .ini settings entries + ImChunkStream SettingsTables; // ImGuiTable .ini settings entries ImVector Hooks; // Hooks for extensions (e.g. test engine) // Capture/Logging @@ -1496,6 +1508,7 @@ struct ImGuiContext DragDropHoldJustPressedId = 0; memset(DragDropPayloadBufLocal, 0, sizeof(DragDropPayloadBufLocal)); + CurrentTable = NULL; CurrentTabBar = NULL; LastValidMousePos = ImVec2(0.0f, 0.0f); @@ -1581,6 +1594,7 @@ struct IMGUI_API ImGuiWindowTempData ImVector ChildWindows; ImGuiStorage* StateStorage; // Current persistent per-window storage (store e.g. tree node open/close state) ImGuiOldColumns* CurrentColumns; // Current columns set + ImGuiTable* CurrentTable; // Current table set ImGuiLayoutType LayoutType; ImGuiLayoutType ParentLayoutType; // Layout type of parent window at the time of Begin() int FocusCounterRegular; // (Legacy Focus/Tabbing system) Sequential counter, start at -1 and increase as assigned via FocusableItemRegister() (FIXME-NAV: Needs redesign) @@ -1805,7 +1819,185 @@ struct ImGuiTabBar //----------------------------------------------------------------------------- #ifdef IMGUI_HAS_TABLE -// + +#define IM_COL32_DISABLE IM_COL32(0,0,0,1) // Special sentinel code +#define IMGUI_TABLE_MAX_COLUMNS 64 // sizeof(ImU64) * 8. This is solely because we frequently encode columns set in a ImU64. + +// [Internal] sizeof() ~ 96 +struct ImGuiTableColumn +{ + ImGuiID UserID; // Optional, value passed to TableSetupColumn() + ImGuiTableColumnFlags FlagsIn; // Flags as input by user. See ImGuiTableColumnFlags_ + ImGuiTableColumnFlags Flags; // Effective flags. See ImGuiTableColumnFlags_ + float ResizeWeight; // ~1.0f. Master width data when (Flags & _WidthStretch) + float MinX; // Absolute positions + float MaxX; + float WidthRequested; // Master width data when !(Flags & _WidthStretch) + float WidthGiven; // == (MaxX - MinX). FIXME-TABLE: Store all persistent width in multiple of FontSize? + float StartXRows; // Start position for the frame, currently ~(MinX + CellPaddingX) + float StartXHeaders; + ImS16 ContentWidthRowsFrozen; // Contents width. Because freezing is non correlated from headers we need all 4 variants (ImDrawCmd merging uses different data than alignment code). + ImS16 ContentWidthRowsUnfrozen; // (encoded as ImS16 because we actually rarely use those width) + ImS16 ContentWidthHeadersUsed; // TableHeader() automatically softclip itself + report ideal desired size, to avoid creating extraneous draw calls + ImS16 ContentWidthHeadersDesired; + float ContentMaxPosRowsFrozen; // Submitted contents absolute maximum position, from which we can infer width. + float ContentMaxPosRowsUnfrozen; // (kept as float because we need to manipulate those between each cell change) + float ContentMaxPosHeadersUsed; + float ContentMaxPosHeadersDesired; + ImRect ClipRect; + ImS16 NameOffset; // Offset into parent ColumnsName[] + bool IsActive; // Is the column not marked Hidden by the user (regardless of clipping). We're not calling this "Visible" here because visibility also depends on clipping. + bool NextIsActive; + ImS8 IndexDisplayOrder; // Index within DisplayOrder[] (column may be reordered by users) + ImS8 IndexWithinActiveSet; // Index within active set (<= IndexOrder) + ImS8 DrawChannelCurrent; // Index within DrawSplitter.Channels[] + ImS8 DrawChannelRowsBeforeFreeze; + ImS8 DrawChannelRowsAfterFreeze; + ImS8 PrevActiveColumn; // Index of prev active column within Columns[], -1 if first active column + ImS8 NextActiveColumn; // Index of next active column within Columns[], -1 if last active column + ImS8 AutoFitFrames; + ImS8 SortOrder; // -1: Not sorting on this column + ImS8 SortDirection; // enum ImGuiSortDirection_ + + ImGuiTableColumn() + { + memset(this, 0, sizeof(*this)); + ResizeWeight = 1.0f; + WidthRequested = WidthGiven = -1.0f; + NameOffset = -1; + IsActive = NextIsActive = true; + IndexDisplayOrder = IndexWithinActiveSet = -1; + DrawChannelCurrent = DrawChannelRowsBeforeFreeze = DrawChannelRowsAfterFreeze = -1; + PrevActiveColumn = NextActiveColumn = -1; + AutoFitFrames = 3; + SortOrder = -1; + SortDirection = ImGuiSortDirection_Ascending; + } +}; + +// FIXME-OPT: Since CountColumns is invariant, we could use a single alloc for ImGuiTable + the three vectors it is carrying. +struct ImGuiTable +{ + ImGuiID ID; + ImGuiTableFlags Flags; + ImVector Columns; + ImVector DisplayOrder; // Store display order of columns (when not reordered, the values are 0...Count-1) + ImU64 ActiveMaskByIndex; // Column Index -> IsActive map (Active == not hidden by user/api) in a format adequate for iterating column without touching cold data + ImU64 ActiveMaskByDisplayOrder; // Column DisplayOrder -> IsActive map + ImGuiTableFlags SettingsSaveFlags; // Pre-compute which data we are going to save into the .ini file (e.g. when order is not altered we won't save order) + int SettingsOffset; // Offset in g.SettingsTables + int LastFrameActive; + int ColumnsCount; // Number of columns declared in BeginTable() + int ColumnsActiveCount; // Number of non-hidden columns (<= ColumnsCount) + int CurrentColumn; + int CurrentRow; + float RowPosY1; + float RowPosY2; + float RowTextBaseline; + ImGuiTableRowFlags RowFlags : 16; // Current row flags, see ImGuiTableRowFlags_ + ImGuiTableRowFlags LastRowFlags : 16; + int RowBgColorCounter; // Counter for alternating background colors (can be fast-forwarded by e.g clipper) + ImU32 RowBgColor; // Request for current row background color + ImU32 BorderOuterColor; + ImU32 BorderInnerColor; + float BorderX1; + float BorderX2; + float CellPaddingX1; // Padding from each borders + float CellPaddingX2; + float CellPaddingY; + float CellSpacingX; // Spacing between non-bordered cells + float LastOuterHeight; // Outer height from last frame + float LastFirstRowHeight; // Height of first row from last frame + float ColumnsTotalWidth; + float InnerWidth; + ImRect OuterRect; // Note: OuterRect.Max.y is often FLT_MAX until EndTable(), unless a height has been specified in BeginTable(). + ImRect WorkRect; + ImRect HostClipRect; // This is used to check if we can eventually merge our columns draw calls into the current draw call of the current window. + ImRect InnerClipRect; + ImRect BackgroundClipRect; // We use this to cpu-clip cell background color fill + ImGuiWindow* OuterWindow; // Parent window for the table + ImGuiWindow* InnerWindow; // Window holding the table data (== OuterWindow or a child window) + ImGuiTextBuffer ColumnsNames; // Contiguous buffer holding columns names + ImDrawListSplitter DrawSplitter; // We carry our own ImDrawList splitter to allow recursion (could be stored outside?) + ImVector SortSpecsData; // FIXME-OPT: Fixed-size array / small-vector pattern, optimize for single sort spec + ImGuiTableSortSpecs SortSpecs; // Public facing sorts specs, this is what we return in TableGetSortSpecs() + ImS8 SortSpecsCount; + ImS8 DeclColumnsCount; // Count calls to TableSetupColumn() + ImS8 HoveredColumnBody; // [DEBUG] Unlike HoveredColumnBorder this doesn't fulfill all Hovering rules properly. Used for debugging/tools for now. + ImS8 HoveredColumnBorder; // Index of column whose right-border is being hovered (for resizing). + ImS8 ResizedColumn; // Index of column being resized. + ImS8 LastResizedColumn; + ImS8 ReorderColumn; // Index of column being reordered. (not cleared) + ImS8 ReorderColumnDir; // -1 or +1 + ImS8 RightMostActiveColumn; // Index of right-most non-hidden column. + ImS8 LeftMostStretchedColumnDisplayOrder; // Display order of left-most stretched column. + ImS8 ContextPopupColumn; // Column right-clicked on, of -1 if opening context menu from a neutral/empty spot + ImS8 DummyDrawChannel; // Redirect non-visible columns here. + ImS8 FreezeRowsRequest; // Requested frozen rows count + ImS8 FreezeRowsCount; // Actual frozen row count (== FreezeRowsRequest, or == 0 when no scrolling offset) + ImS8 FreezeColumnsRequest; // Requested frozen columns count + ImS8 FreezeColumnsCount; // Actual frozen columns count (== FreezeColumnsRequest, or == 0 when no scrolling offset) + bool IsLayoutLocked; // Set by TableUpdateLayout() which is called when beginning the first row. + bool IsInsideRow; // Set if inside TableBeginRow()/TableEndRow(). + bool IsFirstFrame; + bool IsSortSpecsDirty; + bool IsUsingHeaders; // Set if the first row had the ImGuiTableRowFlags_Headers flag. + bool IsContextPopupOpen; + bool IsSettingsRequestLoad; + bool IsSettingsLoaded; + bool IsSettingsDirty; // Set when table settings have changed and needs to be reported into ImGuiTableSetttings data. + bool IsDefaultDisplayOrder; // Set when display order is unchanged from default (DisplayOrder contains 0...Count-1) + bool IsResetDisplayOrderRequest; + bool IsFreezeRowsPassed; // Set when we got past the frozen row (the first one). + bool BackupSkipItems; // Backup of InnerWindow->SkipItem at the end of BeginTable(), because we will overwrite InnerWindow->SkipItem on a per-column basis + ImRect BackupWorkRect; // Backup of InnerWindow->WorkRect at the end of BeginTable() + ImVec2 BackupCursorMaxPos; // Backup of InnerWindow->DC.CursorMaxPos at the end of BeginTable() + + ImGuiTable() + { + memset(this, 0, sizeof(*this)); + SettingsOffset = -1; + LastFrameActive = -1; + LastResizedColumn = -1; + ContextPopupColumn = -1; + ReorderColumn = -1; + } +}; + +// sizeof() ~ 12 +struct ImGuiTableColumnSettings +{ + float WidthOrWeight; + ImGuiID UserID; + ImS8 Index; + ImS8 DisplayOrder; + ImS8 SortOrder; + ImS8 SortDirection : 7; + ImU8 Visible : 1; // This is called Active in ImGuiTableColumn, in .ini file we call it Visible. + + ImGuiTableColumnSettings() + { + WidthOrWeight = 0.0f; + UserID = 0; + Index = -1; + DisplayOrder = SortOrder = -1; + SortDirection = ImGuiSortDirection_None; + Visible = 1; + } +}; + +// This is designed to be stored in a single ImChunkStream (1 header followed by N ImGuiTableColumnSettings, etc.) +struct ImGuiTableSettings +{ + ImGuiID ID; // Set to 0 to invalidate/delete the setting + ImGuiTableFlags SaveFlags; + ImS8 ColumnsCount; + ImS8 ColumnsCountMax; + + ImGuiTableSettings() { memset(this, 0, sizeof(*this)); } + ImGuiTableColumnSettings* GetColumnSettings() { return (ImGuiTableColumnSettings*)(this + 1); } +}; + #endif // #ifdef IMGUI_HAS_TABLE //----------------------------------------------------------------------------- @@ -1977,6 +2169,35 @@ namespace ImGui IMGUI_API float GetColumnOffsetFromNorm(const ImGuiOldColumns* columns, float offset_norm); IMGUI_API float GetColumnNormFromOffset(const ImGuiOldColumns* columns, float offset); + // Tables + //IMGUI_API int GetTableColumnNo(); + //IMGUI_API bool SetTableColumnNo(int column_n); + //IMGUI_API int GetTableLineNo(); + IMGUI_API void TableBeginInitVisibility(ImGuiTable* table); + IMGUI_API void TableBeginInitDrawChannels(ImGuiTable* table); + IMGUI_API void TableUpdateLayout(ImGuiTable* table); + IMGUI_API void TableUpdateBorders(ImGuiTable* table); + IMGUI_API void TableSetColumnWidth(ImGuiTable* table, ImGuiTableColumn* column, float width); + IMGUI_API void TableDrawBorders(ImGuiTable* table); + IMGUI_API void TableDrawMergeChannels(ImGuiTable* table); + IMGUI_API void TableDrawContextMenu(ImGuiTable* table, int column_n); + IMGUI_API void TableSortSpecsClickColumn(ImGuiTable* table, ImGuiTableColumn* column, bool add_to_existing_sort_orders); + IMGUI_API void TableSortSpecsSanitize(ImGuiTable* table); + IMGUI_API void TableBeginRow(ImGuiTable* table); + IMGUI_API void TableEndRow(ImGuiTable* table); + IMGUI_API void TableBeginCell(ImGuiTable* table, int column_no); + IMGUI_API void TableEndCell(ImGuiTable* table); + IMGUI_API ImRect TableGetCellRect(); + IMGUI_API const char* TableGetColumnName(ImGuiTable* table, int column_no); + IMGUI_API void PushTableBackground(); + IMGUI_API void PopTableBackground(); + IMGUI_API void TableLoadSettings(ImGuiTable* table); + IMGUI_API void TableSaveSettings(ImGuiTable* table); + IMGUI_API ImGuiTableSettings* TableFindSettings(ImGuiTable* table); + IMGUI_API void* TableSettingsHandler_ReadOpen(ImGuiContext*, ImGuiSettingsHandler*, const char* name); + IMGUI_API void TableSettingsHandler_ReadLine(ImGuiContext*, ImGuiSettingsHandler*, void* entry, const char* line); + IMGUI_API void TableSettingsHandler_WriteAll(ImGuiContext*, ImGuiSettingsHandler*, ImGuiTextBuffer* buf); + // Tab Bars IMGUI_API bool BeginTabBarEx(ImGuiTabBar* tab_bar, const ImRect& bb, ImGuiTabBarFlags flags); IMGUI_API ImGuiTabItem* TabBarFindTabByID(ImGuiTabBar* tab_bar, ImGuiID tab_id); @@ -2094,6 +2315,7 @@ namespace ImGui IMGUI_API void DebugNodeDrawCmdShowMeshAndBoundingBox(ImGuiWindow* window, const ImDrawList* draw_list, const ImDrawCmd* draw_cmd, bool show_mesh, bool show_aabb); IMGUI_API void DebugNodeStorage(ImGuiStorage* storage, const char* label); IMGUI_API void DebugNodeTabBar(ImGuiTabBar* tab_bar, const char* label); + IMGUI_API void DebugNodeTable(ImGuiTable* table); IMGUI_API void DebugNodeWindow(ImGuiWindow* window, const char* label); IMGUI_API void DebugNodeWindowSettings(ImGuiWindowSettings* settings); IMGUI_API void DebugNodeWindowsList(ImVector* windows, const char* label); diff --git a/imgui_tables.cpp b/imgui_tables.cpp index 67b625daa..657ccad40 100644 --- a/imgui_tables.cpp +++ b/imgui_tables.cpp @@ -61,6 +61,2241 @@ // [SECTION] Widgets: BeginTable, EndTable, etc. //----------------------------------------------------------------------------- +// Typical call flow: (root level is public API): +// - BeginTable() user begin into a table +// - BeginChild() - (if ScrollX/ScrollY is set) +// - TableBeginInitVisibility() - lock columns visibility +// - TableBeginInitDrawChannels() - setup ImDrawList channels +// - TableSetupColumn() user submit columns details (optional) +// - TableAutoHeaders() or TableHeader() user submit a headers row (optional) +// - TableSortSpecsClickColumn() +// - TableGetSortSpecs() user queries updated sort specs (optional) +// - TableNextRow() / TableNextCell() user begin into the first row, also automatically called by TableAutoHeaders() +// - TableUpdateLayout() - called by the FIRST call to TableNextRow() +// - TableUpdateBorders() - detect hovering columns for resize, ahead of contents submission +// - TableDrawContextMenu() - draw right-click context menu +// - [...] user emit contents +// - EndTable() user ends the table +// - TableDrawBorders() - draw outer borders, inner vertical borders +// - TableDrawMergeChannels() - merge draw channels if clipping isn't required +// - TableSetColumnWidth() - apply resizing width +// - TableUpdateColumnsWeightFromWidth() +// - EndChild() - (if ScrollX/ScrollY is set) + +// Configuration +static const float TABLE_RESIZE_SEPARATOR_HALF_THICKNESS = 4.0f; // Extend outside inner borders. +static const float TABLE_RESIZE_SEPARATOR_FEEDBACK_TIMER = 0.06f; // Delay/timer before making the hover feedback (color+cursor) visible because tables/columns tends to be more cramped. + +// Helper +inline ImGuiTableFlags TableFixFlags(ImGuiTableFlags flags) +{ + // Adjust flags: set default sizing policy + if ((flags & ImGuiTableFlags_SizingPolicyMaskX_) == 0) + flags |= (flags & ImGuiTableFlags_ScrollX) ? ImGuiTableFlags_SizingPolicyFixedX : ImGuiTableFlags_SizingPolicyStretchX; + + // Adjust flags: MultiSortable automatically enable Sortable + if (flags & ImGuiTableFlags_MultiSortable) + flags |= ImGuiTableFlags_Sortable; + + // Adjust flags: disable saved settings if there's nothing to save + if ((flags & (ImGuiTableFlags_Resizable | ImGuiTableFlags_Hideable | ImGuiTableFlags_Reorderable | ImGuiTableFlags_Sortable)) == 0) + flags |= ImGuiTableFlags_NoSavedSettings; + + // Adjust flags: enforce borders when resizable + if (flags & ImGuiTableFlags_Resizable) + flags |= ImGuiTableFlags_BordersV; + + // Adjust flags: disable top rows freezing if there's no scrolling + if ((flags & ImGuiTableFlags_ScrollX) == 0) + flags &= ~ImGuiTableFlags_ScrollFreezeColumnsMask_; + if ((flags & ImGuiTableFlags_ScrollY) == 0) + flags &= ~ImGuiTableFlags_ScrollFreezeRowsMask_; + + // Adjust flags: disable NoHostExtendY if we have any scrolling going on + if ((flags & ImGuiTableFlags_NoHostExtendY) && (flags & (ImGuiTableFlags_ScrollX | ImGuiTableFlags_ScrollY)) != 0) + flags &= ~ImGuiTableFlags_NoHostExtendY; + + // Adjust flags: we don't support NoClipX with (FreezeColumns > 0), we could with some work but it doesn't appear to be worth the effort + if (flags & ImGuiTableFlags_ScrollFreezeColumnsMask_) + flags &= ~ImGuiTableFlags_NoClipX; + + return flags; +} + +// About 'outer_size': +// The meaning of outer_size needs to differ slightly depending of if we are using ScrollX/ScrollY flags. +// With ScrollX/ScrollY: using a child window for scrolling: +// - outer_size.y < 0.0f -> bottom align +// - outer_size.y = 0.0f -> bottom align: consistent with BeginChild(), best to preserve (0,0) default arg +// - outer_size.y > 0.0f -> fixed child height +// Without scrolling, we output table directly in parent window: +// - outer_size.y < 0.0f -> bottom align (will auto extend, unless NoHostExtendV is set) +// - outer_size.y = 0.0f -> zero minimum height (will auto extend, unless NoHostExtendV is set) +// - outer_size.y > 0.0f -> minimum height (will auto extend, unless NoHostExtendV is set) +// About: 'inner_width': +// With ScrollX: +// - inner_width < 0.0f -> *illegal* fit in known width (right align from outer_size.x) <-- weird +// - inner_width = 0.0f -> auto enlarge: *only* fixed size columns, which will take space they need (proportional columns becomes fixed columns) <-- desired default :( +// - inner_width > 0.0f -> fit in known width: fixed column take space they need if possible (otherwise shrink down), proportional columns share remaining space. +// Without ScrollX: +// - inner_width < 0.0f -> fit in known width (right align from outer_size.x) <-- desired default +// - inner_width = 0.0f -> auto enlarge: will emit contents size in parent window +// - inner_width > 0.0f -> fit in known width (bypass outer_size.x, permitted but not useful, should instead alter outer_width) +// FIXME-TABLE: This is currently not very useful. +// FIXME-TABLE: Replace enlarge vs fixed width by a flag. +// Even if not really useful, we allow 'inner_scroll_width < outer_size.x' for consistency and to facilitate understanding of what the value does. +bool ImGui::BeginTable(const char* str_id, int columns_count, ImGuiTableFlags flags, const ImVec2& outer_size, float inner_width) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* outer_window = GetCurrentWindow(); + IM_ASSERT(columns_count > 0 && columns_count < IMGUI_TABLE_MAX_COLUMNS && "Only 0..63 columns allowed!"); + if (flags & ImGuiTableFlags_ScrollX) + IM_ASSERT(inner_width >= 0.0f); + ImGuiID id = GetID(str_id); + + const bool use_child_window = (flags & (ImGuiTableFlags_ScrollX | ImGuiTableFlags_ScrollY)) != 0; + const ImVec2 avail_size = GetContentRegionAvail(); + ImVec2 actual_outer_size = CalcItemSize(outer_size, ImMax(avail_size.x, 1.0f), use_child_window ? ImMax(avail_size.y, 1.0f) : 0.0f); + ImRect outer_rect(outer_window->DC.CursorPos, outer_window->DC.CursorPos + actual_outer_size); + + // If an outer size is specified ahead we will be able to early out when not visible, + // The exact rules here can evolve. + if (use_child_window && IsClippedEx(outer_rect, id, false)) + { + ItemSize(outer_rect); + return false; + } + + flags = TableFixFlags(flags); + if (outer_window->Flags & ImGuiWindowFlags_NoSavedSettings) + flags |= ImGuiTableFlags_NoSavedSettings; + + // Acquire storage for the table + ImGuiTable* table = g.Tables.GetOrAddByKey(id); + const ImGuiTableFlags table_last_flags = table->Flags; + table->ID = id; + table->Flags = flags; + table->IsFirstFrame = (table->LastFrameActive == -1); + table->LastFrameActive = g.FrameCount; + table->OuterWindow = table->InnerWindow = outer_window; + table->ColumnsCount = columns_count; + table->ColumnsNames.Buf.resize(0); + table->IsLayoutLocked = false; + table->InnerWidth = inner_width; + table->OuterRect = outer_rect; + table->WorkRect = outer_rect; + + if (use_child_window) + { + // Ensure no vertical scrollbar appears if we only want horizontal one, to make flag consistent (we have no other way to disable vertical scrollbar of a window while keeping the horizontal one showing) + ImVec2 override_content_size(FLT_MAX, FLT_MAX); + if ((flags & ImGuiTableFlags_ScrollX) && !(flags & ImGuiTableFlags_ScrollY)) + override_content_size.y = FLT_MIN; + + // Ensure specified width (when not specified, Stretched columns will act as if the width == OuterWidth and never lead to any scrolling) + // We don't handle inner_width < 0.0f, we could potentially use it to right-align based on the right side of the child window work rect, + // which would require knowing ahead if we are going to have decoration taking horizontal spaces (typically a vertical scrollbar). + if (inner_width != 0.0f) + override_content_size.x = inner_width; + + if (override_content_size.x != FLT_MAX || override_content_size.y != FLT_MAX) + SetNextWindowContentSize(ImVec2(override_content_size.x != FLT_MAX ? override_content_size.x : 0.0f, override_content_size.y != FLT_MAX ? override_content_size.y : 0.0f)); + + // Create scrolling region (without border = zero window padding) + ImGuiWindowFlags child_flags = (flags & ImGuiTableFlags_ScrollX) ? ImGuiWindowFlags_HorizontalScrollbar : ImGuiWindowFlags_None; + BeginChildEx(str_id, id, table->OuterRect.GetSize(), false, child_flags); + table->InnerWindow = g.CurrentWindow; + table->WorkRect = table->InnerWindow->WorkRect; + table->OuterRect = table->InnerWindow->Rect(); + } + else + { + // WorkRect.Max will grow as we append contents. + PushID(id); + } + + const bool has_cell_padding_x = (flags & (ImGuiTableFlags_BordersOuter | ImGuiTableFlags_BordersV)) != 0; + ImGuiWindow* inner_window = table->InnerWindow; + table->CurrentColumn = -1; + table->CurrentRow = -1; + table->RowBgColorCounter = 0; + table->LastRowFlags = ImGuiTableRowFlags_None; + + table->CellPaddingX1 = has_cell_padding_x ? g.Style.CellPadding.x + 1.0f : 0.0f; + table->CellPaddingX2 = has_cell_padding_x ? g.Style.CellPadding.x : 0.0f; + table->CellPaddingY = g.Style.CellPadding.y; + table->CellSpacingX = has_cell_padding_x ? 0.0f : g.Style.CellPadding.x; + + table->HostClipRect = inner_window->ClipRect; + table->InnerClipRect = (inner_window == outer_window) ? table->WorkRect : inner_window->ClipRect; + table->InnerClipRect.ClipWith(table->WorkRect); // We need this to honor inner_width + table->InnerClipRect.ClipWith(table->HostClipRect); + table->InnerClipRect.Max.y = (flags & ImGuiTableFlags_NoHostExtendY) ? table->WorkRect.Max.y : inner_window->ClipRect.Max.y; + table->BackgroundClipRect = table->InnerClipRect; + table->RowPosY1 = table->RowPosY2 = table->WorkRect.Min.y; // This is needed somehow + table->RowTextBaseline = 0.0f; // This will be cleared again by TableBeginRow() + table->FreezeRowsRequest = (ImS8)((flags & ImGuiTableFlags_ScrollFreezeRowsMask_) >> ImGuiTableFlags_ScrollFreezeRowsShift_); + table->FreezeRowsCount = (inner_window->Scroll.y != 0.0f) ? table->FreezeRowsRequest : 0; + table->FreezeColumnsRequest = (ImS8)((flags & ImGuiTableFlags_ScrollFreezeColumnsMask_) >> ImGuiTableFlags_ScrollFreezeColumnsShift_); + table->FreezeColumnsCount = (inner_window->Scroll.x != 0.0f) ? table->FreezeColumnsRequest : 0; + table->IsFreezeRowsPassed = (table->FreezeRowsCount == 0); + table->DeclColumnsCount = 0; + table->LastResizedColumn = table->ResizedColumn; + table->HoveredColumnBody = -1; + table->HoveredColumnBorder = -1; + table->RightMostActiveColumn = -1; + table->LeftMostStretchedColumnDisplayOrder = -1; + table->IsFirstFrame = false; + + // FIXME-TABLE FIXME-STYLE: Using opaque colors facilitate overlapping elements of the grid + //table->BorderOuterColor = GetColorU32(ImGuiCol_Separator, 1.00f); + //table->BorderInnerColor = GetColorU32(ImGuiCol_Separator, 0.60f); + table->BorderOuterColor = GetColorU32(ImVec4(0.31f, 0.31f, 0.35f, 1.00f)); + table->BorderInnerColor = GetColorU32(ImVec4(0.23f, 0.23f, 0.25f, 1.00f)); + //table->BorderOuterColor = IM_COL32(255, 0, 0, 255); + //table->BorderInnerColor = IM_COL32(255, 255, 0, 255); + table->BorderX1 = table->InnerClipRect.Min.x;// +((table->Flags & ImGuiTableFlags_BordersOuter) ? 0.0f : -1.0f); + table->BorderX2 = table->InnerClipRect.Max.x;// +((table->Flags & ImGuiTableFlags_BordersOuter) ? 0.0f : +1.0f); + + // Make table current + g.CurrentTableStack.push_back(ImGuiPtrOrIndex(g.Tables.GetIndex(table))); + g.CurrentTable = table; + outer_window->DC.CurrentTable = table; + if ((table_last_flags & ImGuiTableFlags_Reorderable) && !(flags & ImGuiTableFlags_Reorderable)) + table->IsResetDisplayOrderRequest = true; + + // Clear data if columns count changed + if (table->Columns.Size != 0 && table->Columns.Size != columns_count) + { + table->Columns.resize(0); + table->DisplayOrder.resize(0); + } + + // Setup default columns state + if (table->Columns.Size == 0) + { + table->IsFirstFrame = true; + table->IsSortSpecsDirty = true; + table->Columns.reserve(columns_count); + table->DisplayOrder.reserve(columns_count); + for (int n = 0; n < columns_count; n++) + { + ImGuiTableColumn column; + column.IndexDisplayOrder = (ImS8)n; + table->Columns.push_back(column); + table->DisplayOrder.push_back(column.IndexDisplayOrder); + } + } + + // Load settings + if (table->IsFirstFrame || table->IsSettingsRequestLoad) + TableLoadSettings(table); + + // Handle reordering request + // Note: we don't clear ReorderColumn after handling the request. + if (table->ReorderColumn != -1 && table->ReorderColumnDir != 0) + { + IM_ASSERT(table->ReorderColumnDir == -1 || table->ReorderColumnDir == +1); + IM_ASSERT(table->Flags & ImGuiTableFlags_Reorderable); + ImGuiTableColumn* dragged_column = &table->Columns[table->ReorderColumn]; + ImGuiTableColumn* target_column = &table->Columns[(table->ReorderColumnDir == -1) ? dragged_column->PrevActiveColumn : dragged_column->NextActiveColumn]; + ImSwap(table->DisplayOrder[dragged_column->IndexDisplayOrder], table->DisplayOrder[target_column->IndexDisplayOrder]); + ImSwap(dragged_column->IndexDisplayOrder, target_column->IndexDisplayOrder); + table->ReorderColumnDir = 0; + table->IsSettingsDirty = true; + } + + // Handle display order reset request + if (table->IsResetDisplayOrderRequest) + { + for (int n = 0; n < columns_count; n++) + table->DisplayOrder[n] = table->Columns[n].IndexDisplayOrder = (ImU8)n; + table->IsResetDisplayOrderRequest = false; + table->IsSettingsDirty = true; + } + + TableBeginInitVisibility(table); + TableBeginInitDrawChannels(table); + + // Grab a copy of window fields we will modify + table->BackupSkipItems = inner_window->SkipItems; + table->BackupWorkRect = inner_window->WorkRect; + table->BackupCursorMaxPos = inner_window->DC.CursorMaxPos; + + if (flags & ImGuiTableFlags_NoClipX) + table->DrawSplitter.SetCurrentChannel(inner_window->DrawList, 1); + else + inner_window->DrawList->PushClipRect(inner_window->ClipRect.Min, inner_window->ClipRect.Max, false); + + return true; +} + +void ImGui::TableBeginInitVisibility(ImGuiTable* table) +{ + // Setup and lock Active state and order + table->ColumnsActiveCount = 0; + table->IsDefaultDisplayOrder = true; + ImGuiTableColumn* last_active_column = NULL; + bool want_column_auto_fit = false; + for (int order_n = 0; order_n < table->ColumnsCount; order_n++) + { + const int column_n = table->DisplayOrder[order_n]; + if (column_n != order_n) + table->IsDefaultDisplayOrder = false; + ImGuiTableColumn* column = &table->Columns[column_n]; + column->NameOffset = -1; + if (!(table->Flags & ImGuiTableFlags_Hideable) || (column->Flags & ImGuiTableColumnFlags_NoHide)) + column->NextIsActive = true; + if (column->IsActive != column->NextIsActive) + { + column->IsActive = column->NextIsActive; + table->IsSettingsDirty = true; + if (!column->IsActive && column->SortOrder != -1) + table->IsSortSpecsDirty = true; + } + if (column->SortOrder > 0 && !(table->Flags & ImGuiTableFlags_MultiSortable)) + table->IsSortSpecsDirty = true; + if (column->AutoFitFrames > 0) + want_column_auto_fit = true; + + ImU64 index_mask = (ImU64)1 << column_n; + ImU64 display_order_mask = (ImU64)1 << column->IndexDisplayOrder; + if (column->IsActive) + { + column->PrevActiveColumn = column->NextActiveColumn = -1; + if (last_active_column) + { + last_active_column->NextActiveColumn = (ImS8)column_n; + column->PrevActiveColumn = (ImS8)table->Columns.index_from_ptr(last_active_column); + } + column->IndexWithinActiveSet = (ImS8)table->ColumnsActiveCount; + table->ColumnsActiveCount++; + table->ActiveMaskByIndex |= index_mask; + table->ActiveMaskByDisplayOrder |= display_order_mask; + last_active_column = column; + } + else + { + column->IndexWithinActiveSet = -1; + table->ActiveMaskByIndex &= ~index_mask; + table->ActiveMaskByDisplayOrder &= ~display_order_mask; + } + IM_ASSERT(column->IndexWithinActiveSet <= column->IndexDisplayOrder); + } + table->RightMostActiveColumn = (ImS8)(last_active_column ? table->Columns.index_from_ptr(last_active_column) : -1); + + // Disable child window clipping while fitting columns. This is not strictly necessary but makes it possible to avoid + // the column fitting to wait until the first visible frame of the child container (may or not be a good thing). + if (want_column_auto_fit && table->OuterWindow != table->InnerWindow) + table->InnerWindow->SkipItems = false; +} + +void ImGui::TableBeginInitDrawChannels(ImGuiTable* table) +{ + // Allocate draw channels. + // - We allocate them following the storage order instead of the display order so reordering won't needlessly increase overall dormant memory cost + // - We isolate headers draw commands in their own channels instead of just altering clip rects. This is in order to facilitate merging of draw commands. + // - After crossing FreezeRowsCount, all columns see their current draw channel increased. + // - We only use the dummy draw channel so we can push a null clipping rectangle into it without affecting other channels, while simplifying per-row/per-cell overhead. It will be empty and discarded when merged. + // Draw channel allocation (before merging): + // - NoClip --> 1+1 channels: background + foreground (same clip rect == 1 draw call) + // - Clip --> 1+N channels + // - FreezeRows || FreezeColumns --> 1+N*2 (unless scrolling value is zero) + // - FreezeRows && FreezeColunns --> 2+N*2 (unless scrolling value is zero) + const int freeze_row_multiplier = (table->FreezeRowsCount > 0) ? 2 : 1; + const int channels_for_row = (table->Flags & ImGuiTableFlags_NoClipX) ? 1 : table->ColumnsActiveCount; + const int channels_for_background = 1; + const int channels_for_dummy = (table->ColumnsActiveCount < table->ColumnsCount) ? +1 : 0; + const int channels_total = channels_for_background + (channels_for_row * freeze_row_multiplier) + channels_for_dummy; + table->DrawSplitter.Split(table->InnerWindow->DrawList, channels_total); + table->DummyDrawChannel = channels_for_dummy ? (ImS8)(channels_total - 1) : -1; + + int draw_channel_current = 1; + for (int column_n = 0; column_n < table->ColumnsCount; column_n++) + { + ImGuiTableColumn* column = &table->Columns[column_n]; + if (column->IsActive) + { + column->DrawChannelRowsBeforeFreeze = (ImS8)(draw_channel_current); + column->DrawChannelRowsAfterFreeze = (ImS8)(draw_channel_current + (table->FreezeRowsCount > 0 ? channels_for_row : 0)); + if (!(table->Flags & ImGuiTableFlags_NoClipX)) + draw_channel_current++; + } + else + { + column->DrawChannelRowsBeforeFreeze = column->DrawChannelRowsAfterFreeze = table->DummyDrawChannel; + } + column->DrawChannelCurrent = column->DrawChannelRowsBeforeFreeze; + } +} + +// Adjust flags: default width mode + weighted columns are not allowed when auto extending +static ImGuiTableColumnFlags TableFixColumnFlags(ImGuiTable* table, ImGuiTableColumnFlags flags) +{ + // Sizing Policy + if ((flags & ImGuiTableColumnFlags_WidthMask_) == 0) + { + if (table->Flags & ImGuiTableFlags_SizingPolicyFixedX) + flags |= ((table->Flags & ImGuiTableFlags_Resizable) && !(flags & ImGuiTableColumnFlags_NoResize)) ? ImGuiTableColumnFlags_WidthFixed : ImGuiTableColumnFlags_WidthAlwaysAutoResize; + else + flags |= ImGuiTableColumnFlags_WidthStretch; + } + IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiTableColumnFlags_WidthMask_)); // Check that only 1 of each set is used. + if ((flags & ImGuiTableColumnFlags_WidthAlwaysAutoResize))// || ((flags & ImGuiTableColumnFlags_WidthStretch) && (table->Flags & ImGuiTableFlags_SizingPolicyStretchX))) + flags |= ImGuiTableColumnFlags_NoResize; + //if ((flags & ImGuiTableColumnFlags_WidthStretch) && (table->Flags & ImGuiTableFlags_SizingPolicyFixedX)) + // flags = (flags & ~ImGuiTableColumnFlags_WidthMask_) | ImGuiTableColumnFlags_WidthFixed; + + // Sorting + if ((flags & ImGuiTableColumnFlags_NoSortAscending) && (flags & ImGuiTableColumnFlags_NoSortDescending)) + flags |= ImGuiTableColumnFlags_NoSort; + + // Alignment + //if ((flags & ImGuiTableColumnFlags_AlignMask_) == 0) + // flags |= ImGuiTableColumnFlags_AlignCenter; + //IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiTableColumnFlags_AlignMask_)); // Check that only 1 of each set is used. + + return flags; +} + +static void TableFixColumnSortDirection(ImGuiTableColumn* column) +{ + // Handle NoSortAscending/NoSortDescending + if (column->SortDirection == ImGuiSortDirection_Ascending && (column->Flags & ImGuiTableColumnFlags_NoSortAscending)) + column->SortDirection = ImGuiSortDirection_Descending; + else if (column->SortDirection == ImGuiSortDirection_Descending && (column->Flags & ImGuiTableColumnFlags_NoSortDescending)) + column->SortDirection = ImGuiSortDirection_Ascending; +} + +static float TableGetMinColumnWidth() +{ + ImGuiContext& g = *GImGui; + // return g.Style.ColumnsMinSpacing; + return g.Style.FramePadding.x * 3.0f; +} + +// Layout columns for the frame +// Runs on the first call to TableNextRow(), to give a chance for TableSetupColumn() to be called first. +// FIXME-TABLE: Our width (and therefore our WorkRect) will be minimal in the first frame for WidthAuto columns, +// increase feedback side-effect with widgets relying on WorkRect.Max.x. Maybe provide a default distribution for WidthAuto columns? +void ImGui::TableUpdateLayout(ImGuiTable* table) +{ + IM_ASSERT(table->IsLayoutLocked == false); + + // Compute offset, clip rect for the frame + const ImRect work_rect = table->WorkRect; + const float padding_auto_x = table->CellPaddingX1; // Can't make auto padding larger than what WorkRect knows about so right-alignment matches. + const float min_column_width = TableGetMinColumnWidth(); + + int count_fixed = 0; + float width_fixed = 0.0f; + float total_weights = 0.0f; + table->LeftMostStretchedColumnDisplayOrder = -1; + for (int order_n = 0; order_n < table->ColumnsCount; order_n++) + { + if (!(table->ActiveMaskByDisplayOrder & ((ImU64)1 << order_n))) + continue; + const int column_n = table->DisplayOrder[order_n]; + ImGuiTableColumn* column = &table->Columns[column_n]; + + // Adjust flags: default width mode + weighted columns are not allowed when auto extending + column->Flags = TableFixColumnFlags(table, column->FlagsIn); + + // We have a unusual edge case where if the user doesn't call TableGetSortSpecs() but has sorting enabled + // or varying sorting flags, we still want the sorting arrows to honor those flags. + if (table->Flags & ImGuiTableFlags_Sortable) + TableFixColumnSortDirection(column); + + if (column->Flags & (ImGuiTableColumnFlags_WidthAlwaysAutoResize | ImGuiTableColumnFlags_WidthFixed)) + { + // Latch initial size for fixed columns + count_fixed += 1; + const bool init_size = (column->AutoFitFrames > 0) || (column->Flags & ImGuiTableColumnFlags_WidthAlwaysAutoResize); + if (init_size) + { + // Combine width from regular rows + width from headers unless requested not to + float width_request = (float)ImMax(column->ContentWidthRowsFrozen, column->ContentWidthRowsUnfrozen); + if (!(table->Flags & ImGuiTableFlags_NoHeadersWidth) && !(column->Flags & ImGuiTableColumnFlags_NoHeaderWidth)) + width_request = ImMax(width_request, (float)column->ContentWidthHeadersDesired); + column->WidthRequested = ImMax(width_request + padding_auto_x, min_column_width); + + // FIXME-TABLE: Increase minimum size during init frame so avoid biasing auto-fitting widgets (e.g. TextWrapped) too much. + // Otherwise what tends to happen is that TextWrapped would output a very large height (= first frame scrollbar display very off + clipper would skip lots of items) + // This is merely making the side-effect less extreme, but doesn't properly fixes it. + if (column->AutoFitFrames > 1 && table->IsFirstFrame) + column->WidthRequested = ImMax(column->WidthRequested, min_column_width * 4.0f); + } + width_fixed += column->WidthRequested; + } + else + { + IM_ASSERT(column->Flags & ImGuiTableColumnFlags_WidthStretch); + IM_ASSERT(column->ResizeWeight > 0.0f); + total_weights += column->ResizeWeight; + if (table->LeftMostStretchedColumnDisplayOrder == -1) + table->LeftMostStretchedColumnDisplayOrder = (ImS8)column->IndexDisplayOrder; + } + + // Don't increment auto-fit until container window got a chance to submit its items + if (column->AutoFitFrames > 0 && table->BackupSkipItems == false) + column->AutoFitFrames--; + } + + // Layout + const float width_spacings = table->CellSpacingX * (table->ColumnsActiveCount - 1); + float width_avail; + if ((table->Flags & ImGuiTableFlags_ScrollX) && (table->InnerWidth == 0.0f)) + width_avail = table->InnerClipRect.GetWidth() - width_spacings - 1.0f; + else + width_avail = work_rect.GetWidth() - width_spacings - 1.0f; // Remove -1.0f to cancel out the +1.0f we are doing in EndTable() to make last column line visible + const float width_avail_for_stretched_columns = width_avail - width_fixed; + float width_remaining_for_stretched_columns = width_avail_for_stretched_columns; + + // Apply final width based on requested widths + // Mark some columns as not resizable + int count_resizable = 0; + table->ColumnsTotalWidth = width_spacings; + for (int order_n = 0; order_n < table->ColumnsCount; order_n++) + { + if (!(table->ActiveMaskByDisplayOrder & ((ImU64)1 << order_n))) + continue; + ImGuiTableColumn* column = &table->Columns[table->DisplayOrder[order_n]]; + + // Allocate width for stretched/weighted columns + if (column->Flags & ImGuiTableColumnFlags_WidthStretch) + { + float weight_ratio = column->ResizeWeight / total_weights; + column->WidthRequested = IM_FLOOR(ImMax(width_avail_for_stretched_columns * weight_ratio, min_column_width) + 0.01f); + width_remaining_for_stretched_columns -= column->WidthRequested; + + // [Resize Rule 2] Resizing from right-side of a weighted column before a fixed column froward sizing to left-side of fixed column + // We also need to copy the NoResize flag.. + if (column->NextActiveColumn != -1) + if (ImGuiTableColumn* next_column = &table->Columns[column->NextActiveColumn]) + if (next_column->Flags & ImGuiTableColumnFlags_WidthFixed) + column->Flags |= (next_column->Flags & ImGuiTableColumnFlags_NoDirectResize_); + } + + // [Resize Rule 1] The right-most active column is not resizable if there is at least one Stretch column (see comments in TableResizeColumn().) + if (column->NextActiveColumn == -1 && table->LeftMostStretchedColumnDisplayOrder != -1) + column->Flags |= ImGuiTableColumnFlags_NoDirectResize_; + + if (!(column->Flags & ImGuiTableColumnFlags_NoResize)) + count_resizable++; + + // Assign final width, record width in case we will need to shrink + column->WidthGiven = ImFloor(ImMax(column->WidthRequested, min_column_width)); + table->ColumnsTotalWidth += column->WidthGiven; + } + +#if 0 + const float width_excess = table->ColumnsTotalWidth - work_rect.GetWidth(); + if ((table->Flags & ImGuiTableFlags_SizingPolicyStretchX) && width_excess > 0.0f) + { + // Shrink widths when the total does not fit + // FIXME-TABLE: This is working but confuses/conflicts with manual resizing. + // FIXME-TABLE: Policy to shrink down below below ideal/requested width if there's no room? + g.ShrinkWidthBuffer.resize(table->ColumnsActiveCount); + for (int order_n = 0, active_n = 0; order_n < table->ColumnsCount; order_n++) + { + if (!(table->ActiveMaskByDisplayOrder & ((ImU64)1 << order_n))) + continue; + const int column_n = table->DisplayOrder[order_n]; + g.ShrinkWidthBuffer[active_n].Index = column_n; + g.ShrinkWidthBuffer[active_n].Width = table->Columns[column_n].WidthGiven; + active_n++; + } + ShrinkWidths(g.ShrinkWidthBuffer.Data, g.ShrinkWidthBuffer.Size, width_excess); + for (int n = 0; n < g.ShrinkWidthBuffer.Size; n++) + table->Columns[g.ShrinkWidthBuffer.Data[n].Index].WidthGiven = ImMax(g.ShrinkWidthBuffer.Data[n].Width, min_column_size); + // FIXME: Need to alter table->ColumnsTotalWidth + } + else +#endif + + // Redistribute remainder width due to rounding (remainder width is < 1.0f * number of Stretch column) + // Using right-to-left distribution (more likely to match resizing cursor), could be adjusted depending where the mouse cursor is and/or relative weights. + // FIXME-TABLE: May be simpler to store floating width and floor final positions only + // FIXME-TABLE: Make it optional? User might prefer to preserve pixel perfect same size? + if (width_remaining_for_stretched_columns >= 1.0f) + for (int order_n = table->ColumnsCount - 1; total_weights > 0.0f && width_remaining_for_stretched_columns >= 1.0f && order_n >= 0; order_n--) + { + if (!(table->ActiveMaskByDisplayOrder & ((ImU64)1 << order_n))) + continue; + ImGuiTableColumn* column = &table->Columns[table->DisplayOrder[order_n]]; + if (!(column->Flags & ImGuiTableColumnFlags_WidthStretch)) + continue; + column->WidthRequested += 1.0f; + column->WidthGiven += 1.0f; + width_remaining_for_stretched_columns -= 1.0f; + } + + // Setup final position, offset and clipping rectangles + int active_n = 0; + float offset_x = (table->FreezeColumnsCount > 0) ? table->OuterRect.Min.x : work_rect.Min.x; + ImRect host_clip_rect = table->InnerClipRect; + for (int order_n = 0; order_n < table->ColumnsCount; order_n++) + { + const int column_n = table->DisplayOrder[order_n]; + ImGuiTableColumn* column = &table->Columns[column_n]; + + if (table->FreezeColumnsCount > 0 && table->FreezeColumnsCount == active_n) + offset_x += work_rect.Min.x - table->OuterRect.Min.x; + + if (!(table->ActiveMaskByDisplayOrder & ((ImU64)1 << order_n))) + { + // Hidden column: clear a few fields and we are done with it for the remainder of the function. + // We set a zero-width clip rect however we pay attention to set Min.y/Max.y properly to not interfere with the clipper. + column->MinX = column->MaxX = offset_x; + column->StartXRows = column->StartXHeaders = offset_x; + column->WidthGiven = 0.0f; + column->ClipRect.Min.x = offset_x; + column->ClipRect.Min.y = work_rect.Min.y; + column->ClipRect.Max.x = offset_x; + column->ClipRect.Max.y = FLT_MAX; + column->ClipRect.ClipWithFull(host_clip_rect); + continue; + } + + // If horizontal scrolling if disabled, we apply a final lossless shrinking of columns in order to make sure they are all visible. + // Because of this we also know that all of the columns will always fit in table->WorkRect and therefore in table->InnerRect (because ScrollX is off) + if (!(table->Flags & ImGuiTableFlags_ScrollX)) + { + float max_x = table->WorkRect.Max.x - (table->ColumnsActiveCount - (column->IndexWithinActiveSet + 1)) * min_column_width; + if (offset_x + column->WidthGiven > max_x) + column->WidthGiven = ImMax(max_x - offset_x, min_column_width); + } + + column->MinX = offset_x; + column->MaxX = column->MinX + column->WidthGiven; + + const float initial_max_pos_x = column->MinX + table->CellPaddingX1; + column->ContentMaxPosRowsFrozen = column->ContentMaxPosRowsUnfrozen = initial_max_pos_x; + column->ContentMaxPosHeadersUsed = column->ContentMaxPosHeadersDesired = initial_max_pos_x; + + // Starting cursor position + column->StartXRows = column->StartXHeaders = column->MinX + table->CellPaddingX1; + + // Alignment + // FIXME-TABLE: This align based on the whole column width, not per-cell, and therefore isn't useful in many cases. + // (To be able to honor this we might be able to store a log of cells width, per row, for visible rows, but nav/programmatic scroll would have visible artifacts.) + //if (column->Flags & ImGuiTableColumnFlags_AlignRight) + // column->StartXRows = ImMax(column->StartXRows, column->MaxX - column->WidthContent[0]); + //else if (column->Flags & ImGuiTableColumnFlags_AlignCenter) + // column->StartXRows = ImLerp(column->StartXRows, ImMax(column->StartXRows, column->MaxX - column->WidthContent[0]), 0.5f); + + //// A one pixel padding on the right side makes clipping more noticeable and contents look less cramped. + column->ClipRect.Min.x = column->MinX; + column->ClipRect.Min.y = work_rect.Min.y; + column->ClipRect.Max.x = column->MaxX;// -1.0f; + column->ClipRect.Max.y = FLT_MAX; + column->ClipRect.ClipWithFull(host_clip_rect); + + if (active_n < table->FreezeColumnsCount) + host_clip_rect.Min.x = ImMax(host_clip_rect.Min.x, column->MaxX + 2.0f); + + offset_x += column->WidthGiven + table->CellSpacingX; + active_n++; + } + + // Clear Resizable flag if none of our column are actually resizable (either via an explicit _NoResize flag, either because of using _WidthAlwaysAutoResize/_WidthStretch) + // This will hide the resizing option from the context menu. + if (count_resizable == 0 && (table->Flags & ImGuiTableFlags_Resizable)) + table->Flags &= ~ImGuiTableFlags_Resizable; + + // Borders + if (table->Flags & ImGuiTableFlags_Resizable) + TableUpdateBorders(table); + + // Reset fields after we used them in TableSetupResize() + table->LastFirstRowHeight = 0.0f; + table->IsLayoutLocked = true; + table->IsUsingHeaders = false; + + // Context menu + if (table->IsContextPopupOpen) + { + if (BeginPopup("##TableContextMenu")) + { + TableDrawContextMenu(table, table->ContextPopupColumn); + EndPopup(); + } + else + { + table->IsContextPopupOpen = false; + } + } +} + +// Process interaction on resizing borders. Actual size change will be applied in EndTable() +// - Set table->HoveredColumnBorder with a short delay/timer to reduce feedback noise +// - Submit ahead of table contents and header, use ImGuiButtonFlags_AllowItemOverlap to prioritize widgets overlapping the same area. +void ImGui::TableUpdateBorders(ImGuiTable* table) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(table->Flags & ImGuiTableFlags_Resizable); + + // At this point OuterRect height may be zero or under actual final height, so we rely on temporal coherency and use + // the final height from last frame. Because this is only affecting _interaction_ with columns, it is not really problematic. + // (whereas the actual visual will be displayed in EndTable() and using the current frame height) + // Actual columns highlight/render will be performed in EndTable() and not be affected. + const bool borders_full_height = (table->IsUsingHeaders == false) || (table->Flags & ImGuiTableFlags_BordersFullHeight); + const float hit_half_width = TABLE_RESIZE_SEPARATOR_HALF_THICKNESS; + const float hit_y1 = table->OuterRect.Min.y; + const float hit_y2_full = ImMax(table->OuterRect.Max.y, hit_y1 + table->LastOuterHeight); + const float hit_y2 = borders_full_height ? hit_y2_full : (hit_y1 + table->LastFirstRowHeight); + const float mouse_x_hover_body = (g.IO.MousePos.y >= hit_y1 && g.IO.MousePos.y < hit_y2_full) ? g.IO.MousePos.x : FLT_MAX; + + for (int order_n = 0; order_n < table->ColumnsCount; order_n++) + { + if (!(table->ActiveMaskByDisplayOrder & ((ImU64)1 << order_n))) + continue; + + const int column_n = table->DisplayOrder[order_n]; + ImGuiTableColumn* column = &table->Columns[column_n]; + + // Detect hovered column: + // - we perform an unusually low-level check here.. not using IsMouseHoveringRect() to avoid touch padding. + // - we don't care about the full set of IsItemHovered() feature either. + if (mouse_x_hover_body >= column->MinX && mouse_x_hover_body < column->MaxX) + table->HoveredColumnBody = (ImS8)column_n; + + if (column->Flags & (ImGuiTableColumnFlags_NoResize | ImGuiTableColumnFlags_NoDirectResize_)) + continue; + + ImGuiID column_id = table->ID + (ImGuiID)column_n; + ImRect hit_rect(column->MaxX - hit_half_width, hit_y1, column->MaxX + hit_half_width, hit_y2); + //GetForegroundDrawList()->AddRect(hit_rect.Min, hit_rect.Max, IM_COL32(255, 0, 0, 100)); + KeepAliveID(column_id); + + bool hovered = false, held = false; + ButtonBehavior(hit_rect, column_id, &hovered, &held, ImGuiButtonFlags_FlattenChildren | ImGuiButtonFlags_AllowItemOverlap); + if (held) + table->ResizedColumn = (ImS8)column_n; + if ((hovered && g.HoveredIdTimer > TABLE_RESIZE_SEPARATOR_FEEDBACK_TIMER) || held) + { + table->HoveredColumnBorder = (ImS8)column_n; + SetMouseCursor(ImGuiMouseCursor_ResizeEW); + } + } +} + +void ImGui::EndTable() +{ + ImGuiContext& g = *GImGui; + ImGuiTable* table = g.CurrentTable; + IM_ASSERT(table != NULL); + + const ImGuiTableFlags flags = table->Flags; + ImGuiWindow* inner_window = table->InnerWindow; + ImGuiWindow* outer_window = table->OuterWindow; + IM_ASSERT(inner_window == g.CurrentWindow); + IM_ASSERT(outer_window == inner_window || outer_window == inner_window->ParentWindow); + + if (table->IsInsideRow) + TableEndRow(table); + + // Finalize table height + inner_window->SkipItems = table->BackupSkipItems; + inner_window->DC.CursorMaxPos = table->BackupCursorMaxPos; + if (inner_window != outer_window) + { + table->OuterRect.Max.y = ImMax(table->OuterRect.Max.y, inner_window->Pos.y + inner_window->Size.y); + inner_window->DC.CursorMaxPos.y = table->RowPosY2; + } + else if (!(flags & ImGuiTableFlags_NoHostExtendY)) + { + table->OuterRect.Max.y = ImMax(table->OuterRect.Max.y, inner_window->DC.CursorPos.y); + inner_window->DC.CursorMaxPos.y = table->RowPosY2; + } + table->WorkRect.Max.y = ImMax(table->WorkRect.Max.y, table->OuterRect.Max.y); + table->LastOuterHeight = table->OuterRect.GetHeight(); + + // Store content width reference for each column + float max_pos_x = inner_window->DC.CursorMaxPos.x; + for (int column_n = 0; column_n < table->ColumnsCount; column_n++) + { + ImGuiTableColumn* column = &table->Columns[column_n]; + + // Store content width (for both Headers and Rows) + //float ref_x = column->MinX; + float ref_x_rows = column->StartXRows - table->CellPaddingX1; + float ref_x_headers = column->StartXHeaders - table->CellPaddingX1; + column->ContentWidthRowsFrozen = (ImS16)ImMax(0.0f, column->ContentMaxPosRowsFrozen - ref_x_rows); + column->ContentWidthRowsUnfrozen = (ImS16)ImMax(0.0f, column->ContentMaxPosRowsUnfrozen - ref_x_rows); + column->ContentWidthHeadersUsed = (ImS16)ImMax(0.0f, column->ContentMaxPosHeadersUsed - ref_x_headers); + column->ContentWidthHeadersDesired = (ImS16)ImMax(0.0f, column->ContentMaxPosHeadersDesired - ref_x_headers); + + if (table->ActiveMaskByIndex & ((ImU64)1 << column_n)) + max_pos_x = ImMax(max_pos_x, column->MaxX); + } + + // Add an extra 1 pixel so we can see the last column vertical line if it lies on the right-most edge. + inner_window->DC.CursorMaxPos.x = max_pos_x + 1; + + if (!(flags & ImGuiTableFlags_NoClipX)) + inner_window->DrawList->PopClipRect(); + inner_window->ClipRect = inner_window->DrawList->_ClipRectStack.back(); + + // Draw borders + if ((flags & ImGuiTableFlags_Borders) != 0) + TableDrawBorders(table); + + // Flatten channels and merge draw calls + table->DrawSplitter.SetCurrentChannel(inner_window->DrawList, 0); + TableDrawMergeChannels(table); + + // When releasing a column being resized, scroll to keep the resulting column in sight + const float min_column_width = TableGetMinColumnWidth(); + if (!(table->Flags & ImGuiTableFlags_ScrollX) && inner_window != outer_window) + { + inner_window->Scroll.x = 0.0f; + } + else if (table->LastResizedColumn != -1 && table->ResizedColumn == -1 && inner_window->ScrollbarX) + { + ImGuiTableColumn* column = &table->Columns[table->LastResizedColumn]; + if (column->MaxX < table->InnerClipRect.Min.x) + SetScrollFromPosX(inner_window, column->MaxX - inner_window->Pos.x - min_column_width, 1.0f); + else if (column->MaxX > table->InnerClipRect.Max.x) + SetScrollFromPosX(inner_window, column->MaxX - inner_window->Pos.x + min_column_width, 1.0f); + } + + // Apply resizing/dragging at the end of the frame + // FIXME-TABLE: Preserve contents width _while resizing down_ until releasing. + // FIXME-TABLE: Contains columns if our work area doesn't allow for scrolling. + if (table->ResizedColumn != -1) + { + ImGuiTableColumn* column = &table->Columns[table->ResizedColumn]; + const float new_x2 = (g.IO.MousePos.x - g.ActiveIdClickOffset.x + TABLE_RESIZE_SEPARATOR_HALF_THICKNESS); + const float new_width = ImFloor(new_x2 - column->MinX); + TableSetColumnWidth(table, column, new_width); + } + + // Layout in outer window + inner_window->WorkRect = table->BackupWorkRect; + inner_window->SkipItems = table->BackupSkipItems; + outer_window->DC.CursorPos = table->OuterRect.Min; + outer_window->DC.ColumnsOffset.x = 0.0f; + if (inner_window != outer_window) + { + // Override EndChild's ItemSize with our own to enable auto-resize on the X axis when possible + float backup_outer_cursor_pos_x = outer_window->DC.CursorPos.x; + EndChild(); + outer_window->DC.CursorMaxPos.x = backup_outer_cursor_pos_x + table->ColumnsTotalWidth + 1.0f + inner_window->ScrollbarSizes.x; + } + else + { + PopID(); + ImVec2 item_size = table->OuterRect.GetSize(); + item_size.x = table->ColumnsTotalWidth; + ItemSize(item_size); + } + + // Save settings + if (table->IsSettingsDirty) + TableSaveSettings(table); + + // Clear or restore current table, if any + IM_ASSERT(g.CurrentWindow == outer_window); + IM_ASSERT(g.CurrentTable == table); + outer_window->DC.CurrentTable = NULL; + g.CurrentTableStack.pop_back(); + g.CurrentTable = g.CurrentTableStack.Size ? g.Tables.GetByIndex(g.CurrentTableStack.back().Index) : NULL; +} + +void ImGui::TableDrawBorders(ImGuiTable* table) +{ + ImGuiWindow* inner_window = table->InnerWindow; + ImGuiWindow* outer_window = table->OuterWindow; + table->DrawSplitter.SetCurrentChannel(inner_window->DrawList, 0); + if (inner_window->Hidden || !table->HostClipRect.Overlaps(table->InnerClipRect)) + return; + + // Draw inner border and resizing feedback + const float draw_y1 = table->OuterRect.Min.y; + float draw_y2_base = (table->FreezeRowsCount >= 1 ? table->OuterRect.Min.y : table->WorkRect.Min.y) + table->LastFirstRowHeight; + float draw_y2_full = table->OuterRect.Max.y; + ImU32 border_base_col; + if (!table->IsUsingHeaders || (table->Flags & ImGuiTableFlags_BordersFullHeight)) + { + draw_y2_base = draw_y2_full; + border_base_col = table->BorderInnerColor; + } + else + { + border_base_col = table->BorderOuterColor; + } + + if (table->Flags & ImGuiTableFlags_BordersV) + { + const bool draw_left_most_border = (table->Flags & ImGuiTableFlags_BordersOuter) == 0; + if (draw_left_most_border) + inner_window->DrawList->AddLine(ImVec2(table->OuterRect.Min.x, draw_y1), ImVec2(table->OuterRect.Min.x, draw_y2_base), border_base_col, 1.0f); + + for (int order_n = 0; order_n < table->ColumnsCount; order_n++) + { + if (!(table->ActiveMaskByDisplayOrder & ((ImU64)1 << order_n))) + continue; + + const int column_n = table->DisplayOrder[order_n]; + ImGuiTableColumn* column = &table->Columns[column_n]; + const bool is_hovered = (table->HoveredColumnBorder == column_n); + const bool is_resized = (table->ResizedColumn == column_n); + const bool draw_right_border = (column->MaxX <= table->InnerClipRect.Max.x) || (is_resized || is_hovered); + if (draw_right_border && column->MaxX > column->ClipRect.Min.x) // FIXME-TABLE FIXME-STYLE: Assume BorderSize==1, this is problematic if we want to increase the border size.. + { + // Draw in outer window so right-most column won't be clipped + // Always draw full height border when: + // - not using headers + // - user specify ImGuiTableFlags_BordersFullHeight + // - being interacted with + // - on the delimitation of frozen column scrolling + const ImU32 col = is_resized ? GetColorU32(ImGuiCol_SeparatorActive) : is_hovered ? GetColorU32(ImGuiCol_SeparatorHovered) : border_base_col; + float draw_y2 = draw_y2_base; + if (is_hovered || is_resized || (table->FreezeColumnsCount != -1 && table->FreezeColumnsCount == order_n + 1)) + draw_y2 = draw_y2_full; + inner_window->DrawList->AddLine(ImVec2(column->MaxX, draw_y1), ImVec2(column->MaxX, draw_y2), col, 1.0f); + } + } + } + + // Draw outer border + if (table->Flags & ImGuiTableFlags_BordersOuter) + { + // Display outer border offset by 1 which is a simple way to display it without adding an extra draw call + // (Without the offset, in outer_window it would be rendered behind cells, because child windows are above their parent. + // In inner_window, it won't reach out over scrollbars. Another weird solution would be to display part of it in inner window, + // and the part that's over scrollbars in the outer window..) + // Either solution currently won't allow us to use a larger border size: the border would clipped. + ImRect outer_border = table->OuterRect; + if (inner_window != outer_window) + outer_border.Expand(1.0f); + outer_window->DrawList->AddRect(outer_border.Min, outer_border.Max, table->BorderOuterColor); // IM_COL32(255, 0, 0, 255)); + } + else if (table->Flags & ImGuiTableFlags_BordersH) + { + // Draw bottom-most border + const float border_y = table->RowPosY2; + if (border_y >= table->BackgroundClipRect.Min.y && border_y < table->BackgroundClipRect.Max.y) + inner_window->DrawList->AddLine(ImVec2(table->BorderX1, border_y), ImVec2(table->BorderX2, border_y), table->BorderOuterColor); + } +} + +static void TableUpdateColumnsWeightFromWidth(ImGuiTable* table) +{ + IM_ASSERT(table->LeftMostStretchedColumnDisplayOrder != -1); + + // Measure existing quantity + float visible_weight = 0.0f; + float visible_width = 0.0f; + for (int column_n = 0; column_n < table->ColumnsCount; column_n++) + { + ImGuiTableColumn* column = &table->Columns[column_n]; + if (!column->IsActive || !(column->Flags & ImGuiTableColumnFlags_WidthStretch)) + continue; + visible_weight += column->ResizeWeight; + visible_width += column->WidthRequested; + } + IM_ASSERT(visible_weight > 0.0f && visible_width > 0.0f); + + // Apply new weights + for (int column_n = 0; column_n < table->ColumnsCount; column_n++) + { + ImGuiTableColumn* column = &table->Columns[column_n]; + if (!column->IsActive || !(column->Flags & ImGuiTableColumnFlags_WidthStretch)) + continue; + column->ResizeWeight = (column->WidthRequested + 0.0f) / visible_width; + } +} + +void ImGui::TableSetColumnWidth(ImGuiTable* table, ImGuiTableColumn* column_0, float column_0_width) +{ + // Constraints + float min_width = TableGetMinColumnWidth(); + float max_width_0 = FLT_MAX; + if (!(table->Flags & ImGuiTableFlags_ScrollX)) + max_width_0 = (table->WorkRect.Max.x - column_0->MinX) - (table->ColumnsActiveCount - (column_0->IndexWithinActiveSet + 1)) * min_width; + column_0_width = ImClamp(column_0_width, min_width, max_width_0); + + // Compare both requested and actual given width to avoid overwriting requested width when column is stuck (minimum size, bounded) + if (column_0->WidthGiven == column_0_width || column_0->WidthRequested == column_0_width) + return; + + ImGuiTableColumn* column_1 = (column_0->NextActiveColumn != -1) ? &table->Columns[column_0->NextActiveColumn] : NULL; + + // In this surprisingly not simple because of how we support mixing Fixed and Stretch columns. + // When forwarding resize from Wn| to Fn+1| we need to be considerate of the _NoResize flag on Fn+1. + // FIXME-TABLE: Find a way to rewrite all of this so interactions feel more consistent for the user. + // Scenarios: + // - F1 F2 F3 resize from F1| or F2| --> ok: alter ->WidthRequested of Fixed column. Subsequent columns will be offset. + // - F1 F2 F3 resize from F3| --> ok: alter ->WidthRequested of Fixed column. If active, ScrollX extent can be altered. + // - F1 F2 W3 resize from F1| or F2| --> ok: alter ->WidthRequested of Fixed column. If active, ScrollX extent can be altered, but it doesn't make much sense as the Weighted column will always be minimal size. + // - F1 F2 W3 resize from W3| --> ok: no-op (disabled by Resize Rule 1) + // - W1 W2 W3 resize from W1| or W2| --> FIXME + // - W1 W2 W3 resize from W3| --> ok: no-op (disabled by Resize Rule 1) + // - W1 F2 F3 resize from F3| --> ok: no-op (disabled by Resize Rule 1) + // - W1 F2 resize from F2| --> ok: no-op (disabled by Resize Rule 1) + // - W1 W2 F3 resize from W1| or W2| --> ok + // - W1 F2 W3 resize from W1| or F2| --> FIXME + // - F1 W2 F3 resize from W2| --> ok + // - W1 F2 F3 resize from W1| --> ok: equivalent to resizing |F2. F3 will not move. (forwarded by Resize Rule 2) + // - W1 F2 F3 resize from F2| --> FIXME should resize F2, F3 and not have effect on W1 (Stretch columns are _before_ the Fixed column). + + // Rules: + // - [Resize Rule 1] Can't resize from right of right-most visible column if there is any Stretch column. Implemented in TableSetupLayout(). + // - [Resize Rule 2] Resizing from right-side of a Stretch column before a fixed column froward sizing to left-side of fixed column. + // - [Resize Rule 3] If we are are followed by a fixed column and we have a Stretch column before, we need to ensure that our left border won't move. + + if (column_0->Flags & ImGuiTableColumnFlags_WidthFixed) + { + // [Resize Rule 3] If we are are followed by a fixed column and we have a Stretch column before, we need to + // ensure that our left border won't move, which we can do by making sure column_a/column_b resizes cancels each others. + if (column_1 && (column_1->Flags & ImGuiTableColumnFlags_WidthFixed)) + if (table->LeftMostStretchedColumnDisplayOrder != -1 && table->LeftMostStretchedColumnDisplayOrder < column_0->IndexDisplayOrder) + { + // (old_a + old_b == new_a + new_b) --> (new_a == old_a + old_b - new_b) + float column_1_width = ImMax(column_1->WidthRequested - (column_0_width - column_0->WidthRequested), min_width); + column_0_width = column_0->WidthRequested + column_1->WidthRequested - column_1_width; + column_1->WidthRequested = column_1_width; + } + + // Apply + //IMGUI_DEBUG_LOG("TableSetColumnWidth(%d, %.1f->%.1f)\n", column_0_idx, column_0->WidthRequested, column_0_width); + column_0->WidthRequested = column_0_width; + } + else if (column_0->Flags & ImGuiTableColumnFlags_WidthStretch) + { + // [Resize Rule 2] + if (column_1 && (column_1->Flags & ImGuiTableColumnFlags_WidthFixed)) + { + float off = (column_0->WidthGiven - column_0_width); + float column_1_width = column_1->WidthGiven + off; + column_1->WidthRequested = ImMax(min_width, column_1_width); + return; + } + + // (old_a + old_b == new_a + new_b) --> (new_a == old_a + old_b - new_b) + float column_1_width = ImMax(column_1->WidthRequested - (column_0_width - column_0->WidthRequested), min_width); + column_0_width = column_0->WidthRequested + column_1->WidthRequested - column_1_width; + column_1->WidthRequested = column_1_width; + column_0->WidthRequested = column_0_width; + TableUpdateColumnsWeightFromWidth(table); + } + table->IsSettingsDirty = true; +} + +// Columns where the contents didn't stray off their local clip rectangle can be merged into a same draw command. +// To achieve this we merge their clip rect and make them contiguous in the channel list so they can be merged. +// So here we'll reorder the draw cmd which can be merged, by arranging them into a maximum of 4 distinct groups: +// +// 1 group: 2 groups: 2 groups: 4 groups: +// [ 0. ] no freeze [ 0. ] row freeze [ 01 ] col freeze [ 01 ] row+col freeze +// [ .. ] or no scroll [ 1. ] and v-scroll [ .. ] and h-scroll [ 23 ] and v+h-scroll +// +// Each column itself can use 1 channel (row freeze disabled) or 2 channels (row freeze enabled). +// When the contents of a column didn't stray off its limit, we move its channels into the corresponding group +// based on its position (within frozen rows/columns set or not). +// At the end of the operation our 1-4 groups will each have a ImDrawCmd using the same ClipRect, and they will be merged by the DrawSplitter.Merge() call. +// +// Column channels will not be merged into one of the 1-4 groups in the following cases: +// - The contents stray off its clipping rectangle (we only compare the MaxX value, not the MinX value). +// Direct ImDrawList calls won't be noticed so if you use them make sure the ImGui:: bounds matches, by e.g. calling SetCursorScreenPos(). +// - The channel uses more than one draw command itself (we drop all our merging stuff here.. we could do better but it's going to be rare) +// +// This function is particularly tricky to understand.. take a breath. +void ImGui::TableDrawMergeChannels(ImGuiTable* table) +{ + ImGuiContext& g = *GImGui; + ImDrawListSplitter* splitter = &table->DrawSplitter; + const bool is_frozen_v = (table->FreezeRowsCount > 0); + const bool is_frozen_h = (table->FreezeColumnsCount > 0); + + int merge_set_mask = 0; + int merge_set_channels_count[4] = { 0 }; + ImU64 merge_set_channels_mask[4] = { 0 }; + ImRect merge_set_clip_rect[4]; + for (int n = 0; n < IM_ARRAYSIZE(merge_set_clip_rect); n++) + merge_set_clip_rect[n] = ImVec4(+FLT_MAX, +FLT_MAX, -FLT_MAX, -FLT_MAX); + bool merge_set_all_fit_within_inner_rect = (table->Flags & ImGuiTableFlags_NoHostExtendY) == 0; + + // 1. Scan channels and take note of those who can be merged + for (int order_n = 0; order_n < table->ColumnsCount; order_n++) + { + if (!(table->ActiveMaskByDisplayOrder & ((ImU64)1 << order_n))) + continue; + const int column_n = table->DisplayOrder[order_n]; + ImGuiTableColumn* column = &table->Columns[column_n]; + + const int merge_set_sub_count = is_frozen_v ? 2 : 1; + for (int merge_set_sub_n = 0; merge_set_sub_n < merge_set_sub_count; merge_set_sub_n++) + { + const int channel_no = (merge_set_sub_n == 0) ? column->DrawChannelRowsBeforeFreeze : column->DrawChannelRowsAfterFreeze; + + // Don't attempt to merge if there are multiple calls within the column + ImDrawChannel* src_channel = &splitter->_Channels[channel_no]; + if (src_channel->_CmdBuffer.Size > 0 && src_channel->_CmdBuffer.back().ElemCount == 0) + src_channel->_CmdBuffer.pop_back(); + if (src_channel->_CmdBuffer.Size != 1) + continue; + + // Find out the width of this merge set and check if it will fit in our column. + float width_contents; + if (merge_set_sub_count == 1) // No row freeze (same as testing !is_frozen_v) + width_contents = ImMax(column->ContentWidthRowsUnfrozen, column->ContentWidthHeadersUsed); + else if (merge_set_sub_n == 0) // Row freeze: use width before freeze + width_contents = ImMax(column->ContentWidthRowsFrozen, column->ContentWidthHeadersUsed); + else // Row freeze: use width after freeze + width_contents = column->ContentWidthRowsUnfrozen; + if (width_contents > column->WidthGiven && !(column->Flags & ImGuiTableColumnFlags_NoClipX)) + continue; + + const int dst_merge_set_n = (is_frozen_h && column_n < table->FreezeColumnsCount ? 0 : 2) + (is_frozen_v ? merge_set_sub_n : 1); + IM_ASSERT(merge_set_channels_count[dst_merge_set_n] < (int)sizeof(merge_set_channels_mask[dst_merge_set_n]) * 8); + merge_set_mask |= (1 << dst_merge_set_n); + merge_set_channels_mask[dst_merge_set_n] |= (ImU64)1 << channel_no; + merge_set_channels_count[dst_merge_set_n]++; + merge_set_clip_rect[dst_merge_set_n].Add(src_channel->_CmdBuffer[0].ClipRect); + + // If we end with a single set and hosted by the outer window, we'll attempt to merge our draw command with + // the existing outer window command. But we can only do so if our columns all fit within the expected clip rect, + // otherwise clipping will be incorrect when ScrollX is disabled. + // FIXME-TABLE FIXME-WORKRECT: We are wasting a merge opportunity on tables without scrolling if column don't fit within host clip rect, solely because of the half-padding difference between window->WorkRect and window->InnerClipRect + + // 2019/10/22: (1) This is breaking table_2_draw_calls but I cannot seem to repro what it is attempting to fix... + // cf git fce2e8dc "Fixed issue with clipping when outerwindow==innerwindow / support ScrollH without ScrollV." + // 2019/10/22: (2) Clamping code in TableSetupLayout() seemingly made this not necessary... +#if 0 + if (column->MinX < table->InnerClipRect.Min.x || column->MaxX > table->InnerClipRect.Max.x) + merge_set_all_fit_within_inner_rect = false; +#endif + } + + // Invalidate current draw channel (we don't clear DrawChannelBeforeRowFreeze/DrawChannelAfterRowFreeze solely to facilitate debugging) + column->DrawChannelCurrent = -1; + } + + // 2. Rewrite channel list in our preferred order + if (merge_set_mask != 0) + { + // Use shared temporary storage so the allocation gets amortized + g.DrawChannelsTempMergeBuffer.resize(splitter->_Count - 1); + ImDrawChannel* dst_tmp = g.DrawChannelsTempMergeBuffer.Data; + ImU64 remaining_mask = ((splitter->_Count < 64) ? ((ImU64)1 << splitter->_Count) - 1 : ~(ImU64)0) & ~1; + const bool may_extend_clip_rect_to_host_rect = ImIsPowerOfTwo(merge_set_mask); + for (int merge_set_n = 0; merge_set_n < 4; merge_set_n++) + if (merge_set_channels_count[merge_set_n]) + { + ImU64 merge_channels_mask = merge_set_channels_mask[merge_set_n]; + ImRect merge_clip_rect = merge_set_clip_rect[merge_set_n]; + if (may_extend_clip_rect_to_host_rect) + { + //GetOverlayDrawList()->AddRect(table->HostClipRect.Min, table->HostClipRect.Max, IM_COL32(255, 0, 0, 200), 0.0f, ~0, 3.0f); + //GetOverlayDrawList()->AddRect(table->InnerClipRect.Min, table->InnerClipRect.Max, IM_COL32(0, 255, 0, 200), 0.0f, ~0, 1.0f); + //GetOverlayDrawList()->AddRect(merge_clip_rect.Min, merge_clip_rect.Max, IM_COL32(255, 0, 0, 200), 0.0f, ~0, 2.0f); + merge_clip_rect.Add(merge_set_all_fit_within_inner_rect ? table->HostClipRect : table->InnerClipRect); + //GetOverlayDrawList()->AddRect(merge_clip_rect.Min, merge_clip_rect.Max, IM_COL32(0, 255, 0, 200)); + } + remaining_mask &= ~merge_channels_mask; + for (int n = 0; n < splitter->_Count && merge_channels_mask != 0; n++) + { + // Copy + overwrite new clip rect + const ImU64 n_mask = (ImU64)1 << n; + if ((merge_channels_mask & n_mask) == 0) + continue; + ImDrawChannel* channel = &splitter->_Channels[n]; + IM_ASSERT(channel->_CmdBuffer.Size == 1 && merge_clip_rect.Contains(ImRect(channel->_CmdBuffer[0].ClipRect))); + channel->_CmdBuffer[0].ClipRect = *(ImVec4*)&merge_clip_rect; + memcpy(dst_tmp++, channel, sizeof(ImDrawChannel)); + merge_channels_mask &= ~n_mask; + } + } + + // Append channels that we didn't reorder at the end of the list + for (int n = 0; n < splitter->_Count && remaining_mask != 0; n++) + { + const ImU64 n_mask = (ImU64)1 << n; + if ((remaining_mask & n_mask) == 0) + continue; + ImDrawChannel* channel = &splitter->_Channels[n]; + memcpy(dst_tmp++, channel, sizeof(ImDrawChannel)); + remaining_mask &= ~n_mask; + } + IM_ASSERT(dst_tmp == g.DrawChannelsTempMergeBuffer.Data + g.DrawChannelsTempMergeBuffer.Size); + memcpy(splitter->_Channels.Data + 1, g.DrawChannelsTempMergeBuffer.Data, (splitter->_Count - 1) * sizeof(ImDrawChannel)); + } + + // 3. Actually merge (channels using the same clip rect will be contiguous and naturally merged) + splitter->Merge(table->InnerWindow->DrawList); +} + +// We use a default parameter of 'init_width_or_weight == -1' +// ImGuiTableColumnFlags_WidthFixed, width <= 0 --> init width == auto +// ImGuiTableColumnFlags_WidthFixed, width > 0 --> init width == manual +// ImGuiTableColumnFlags_WidthStretch, weight < 0 --> init weight == 1.0f +// ImGuiTableColumnFlags_WidthStretch, weight >= 0 --> init weight == custom +// Use a different API? +void ImGui::TableSetupColumn(const char* label, ImGuiTableColumnFlags flags, float init_width_or_weight, ImGuiID user_id) +{ + ImGuiContext& g = *GImGui; + ImGuiTable* table = g.CurrentTable; + IM_ASSERT(table != NULL && "Can only call TableSetupColumn() after BeginTable()!"); + IM_ASSERT(!table->IsLayoutLocked && "Can only call TableSetupColumn() before first row!"); + IM_ASSERT(table->DeclColumnsCount >= 0 && table->DeclColumnsCount < table->ColumnsCount && "Called TableSetupColumn() too many times!"); + + ImGuiTableColumn* column = &table->Columns[table->DeclColumnsCount]; + table->DeclColumnsCount++; + + column->UserID = user_id; + column->FlagsIn = flags; + column->Flags = TableFixColumnFlags(table, column->FlagsIn); + flags = column->Flags; + + // Initialize defaults + if (table->IsFirstFrame && !table->IsSettingsLoaded) + { + // Init width or weight + // Disable auto-fit if a default fixed width has been specified + if ((flags & ImGuiTableColumnFlags_WidthFixed) && init_width_or_weight > 0.0f) + { + column->WidthRequested = init_width_or_weight; + column->AutoFitFrames = 0; + } + if (flags & ImGuiTableColumnFlags_WidthStretch) + { + IM_ASSERT(init_width_or_weight < 0.0f || init_width_or_weight > 0.0f); + column->ResizeWeight = (init_width_or_weight < 0.0f ? 1.0f : init_width_or_weight); + } + else + { + column->ResizeWeight = 1.0f; + } + + // Init default visibility/sort state + if (flags & ImGuiTableColumnFlags_DefaultHide) + column->IsActive = column->NextIsActive = false; + if (flags & ImGuiTableColumnFlags_DefaultSort) + column->SortOrder = 0; // Multiple columns using _DefaultSort will be reordered when building the sort specs. + } + + // Store name (append with zero-terminator in contiguous buffer) + IM_ASSERT(column->NameOffset == -1); + if (label != NULL) + { + column->NameOffset = (ImS16)table->ColumnsNames.size(); + table->ColumnsNames.append(label, label + strlen(label) + 1); + } +} + +// Starts into the first cell of a new row +void ImGui::TableNextRow(ImGuiTableRowFlags row_flags, float min_row_height) +{ + ImGuiContext& g = *GImGui; + ImGuiTable* table = g.CurrentTable; + + if (table->CurrentRow == -1) + TableUpdateLayout(table); + else if (table->IsInsideRow) + TableEndRow(table); + + table->LastRowFlags = table->RowFlags; + table->RowFlags = row_flags; + TableBeginRow(table); + + // We honor min_height requested by user, but cannot guarantee per-row maximum height as that would essentially require a unique clipping rectangle per-cell. + table->RowPosY2 += min_row_height; + + TableBeginCell(table, 0); +} + +// [Internal] +void ImGui::TableBeginRow(ImGuiTable* table) +{ + ImGuiWindow* window = table->InnerWindow; + IM_ASSERT(!table->IsInsideRow); + + // New row + table->CurrentRow++; + table->CurrentColumn = -1; + table->RowBgColor = IM_COL32_DISABLE; + table->IsInsideRow = true; + + // Begin frozen rows + float next_y1 = table->RowPosY2; + if (table->CurrentRow == 0 && table->FreezeRowsCount > 0) + next_y1 = window->DC.CursorPos.y = table->OuterRect.Min.y; + + table->RowPosY1 = table->RowPosY2 = next_y1; + table->RowTextBaseline = 0.0f; + 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 (table->RowFlags & ImGuiTableRowFlags_Headers) + { + table->RowBgColor = GetColorU32(ImGuiCol_TableHeaderBg); + if (table->CurrentRow == 0) + table->IsUsingHeaders = true; + } +} + +// [Internal] +void ImGui::TableEndRow(ImGuiTable* table) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + IM_ASSERT(window == table->InnerWindow); + IM_ASSERT(table->IsInsideRow); + + TableEndCell(table); + + table->RowPosY2 += table->CellPaddingY; + + // Position cursor at the bottom of our row so it can be used for e.g. clipping calculation. + // However it is likely that the next call to TableBeginCell() will reposition the cursor to take account of vertical padding. + window->DC.CursorPos.y = table->RowPosY2; + + // Row background fill + const float bg_y1 = table->RowPosY1; + const float bg_y2 = table->RowPosY2; + + if (table->CurrentRow == 0) + table->LastFirstRowHeight = bg_y2 - bg_y1; + + if (table->CurrentRow >= 0 && bg_y2 >= table->InnerClipRect.Min.y && bg_y1 <= table->InnerClipRect.Max.y) + { + // Decide of background color for the row + ImU32 bg_col = 0; + if (table->RowBgColor != IM_COL32_DISABLE) + bg_col = table->RowBgColor; + else if (table->Flags & ImGuiTableFlags_RowBg) + bg_col = GetColorU32((table->RowBgColorCounter & 1) ? ImGuiCol_TableRowBgAlt : ImGuiCol_TableRowBg); + + // Decide of separating border color + ImU32 border_col = 0; + if (table->CurrentRow != 0 || table->InnerWindow == table->OuterWindow) + { + if (table->Flags & ImGuiTableFlags_BordersH) + { + if (table->CurrentRow == 0 && table->InnerWindow == table->OuterWindow) + border_col = table->BorderOuterColor; + else if (!(table->LastRowFlags & ImGuiTableRowFlags_Headers)) + border_col = table->BorderInnerColor; + } + else + { + if (table->RowFlags & ImGuiTableRowFlags_Headers) + border_col = table->BorderOuterColor; + } + } + + if (bg_col != 0 || border_col != 0) + table->DrawSplitter.SetCurrentChannel(window->DrawList, 0); + + // Draw background + // We soft/cpu clip this so all backgrounds and borders can share the same clipping rectangle + if (bg_col) + { + ImRect bg_rect(table->WorkRect.Min.x, bg_y1, table->WorkRect.Max.x, bg_y2); + bg_rect.ClipWith(table->BackgroundClipRect); + if (bg_rect.Min.y < bg_rect.Max.y) + window->DrawList->AddRectFilledMultiColor(bg_rect.Min, bg_rect.Max, bg_col, bg_col, bg_col, bg_col); + } + + // Draw top border + const float border_y = bg_y1; + if (border_col && border_y >= table->BackgroundClipRect.Min.y && border_y < table->BackgroundClipRect.Max.y) + window->DrawList->AddLine(ImVec2(table->BorderX1, border_y), ImVec2(table->BorderX2, border_y), border_col); + } + + const bool unfreeze_rows = (table->CurrentRow + 1 == table->FreezeRowsCount && table->FreezeRowsCount > 0); + + // Draw bottom border (always strong) + const bool draw_separating_border = unfreeze_rows || (table->RowFlags & ImGuiTableRowFlags_Headers); + if (draw_separating_border) + if (bg_y2 >= table->BackgroundClipRect.Min.y && bg_y2 < table->BackgroundClipRect.Max.y) + window->DrawList->AddLine(ImVec2(table->BorderX1, bg_y2), ImVec2(table->BorderX2, bg_y2), table->BorderOuterColor); + + // End frozen rows (when we are past the last frozen row line, teleport cursor and alter clipping rectangle) + // We need to do that in TableEndRow() instead of TableBeginRow() so the list clipper can mark end of row and get the new cursor position. + if (unfreeze_rows) + { + IM_ASSERT(table->IsFreezeRowsPassed == false); + table->IsFreezeRowsPassed = true; + table->DrawSplitter.SetCurrentChannel(window->DrawList, 0); + + ImRect r; + r.Min.x = table->InnerClipRect.Min.x; + r.Min.y = ImMax(table->RowPosY2 + 1, window->InnerClipRect.Min.y); + r.Max.x = table->InnerClipRect.Max.x; + r.Max.y = window->InnerClipRect.Max.y; + table->BackgroundClipRect = r; + + float row_height = table->RowPosY2 - table->RowPosY1; + table->RowPosY2 = window->DC.CursorPos.y = table->WorkRect.Min.y + table->RowPosY2 - table->OuterRect.Min.y; + table->RowPosY1 = table->RowPosY2 - row_height; + for (int column_n = 0; column_n < table->ColumnsCount; column_n++) + { + ImGuiTableColumn* column = &table->Columns[column_n]; + column->DrawChannelCurrent = column->DrawChannelRowsAfterFreeze; + column->ClipRect.Min.y = r.Min.y; + } + } + + if (!(table->RowFlags & ImGuiTableRowFlags_Headers)) + table->RowBgColorCounter++; + table->IsInsideRow = false; +} + +// [Internal] This is called a lot, so we need to be mindful of unnecessary overhead! +void ImGui::TableBeginCell(ImGuiTable* table, int column_no) +{ + table->CurrentColumn = column_no; + ImGuiTableColumn* column = &table->Columns[column_no]; + ImGuiWindow* window = table->InnerWindow; + + const float start_x = (table->RowFlags & ImGuiTableRowFlags_Headers) ? column->StartXHeaders : column->StartXRows; + + window->DC.LastItemId = 0; + window->DC.CursorPos = ImVec2(start_x, table->RowPosY1 + table->CellPaddingY); + window->DC.CursorMaxPos.x = window->DC.CursorPos.x; + window->DC.ColumnsOffset.x = start_x - window->Pos.x - window->DC.Indent.x; // FIXME-WORKRECT // FIXME-TABLE: Recurse + window->DC.CurrLineTextBaseOffset = table->RowTextBaseline; + + window->WorkRect.Min.y = window->DC.CursorPos.y; + window->WorkRect.Min.x = column->MinX + table->CellPaddingX1; + window->WorkRect.Max.x = column->MaxX - table->CellPaddingX2; + + // To allow ImGuiListClipper to function we propagate our row height + if (!column->IsActive) + window->DC.CursorPos.y = ImMax(window->DC.CursorPos.y, table->RowPosY2); + + // FIXME-COLUMNS: Setup baseline, preserve across columns (how can we obtain first line baseline tho..) + // window->DC.CurrLineTextBaseOffset = ImMax(window->DC.CurrLineTextBaseOffset, g.Style.FramePadding.y); + + window->SkipItems = column->IsActive ? table->BackupSkipItems : true; + if (table->Flags & ImGuiTableFlags_NoClipX) + { + table->DrawSplitter.SetCurrentChannel(window->DrawList, 1); + } + else + { + table->DrawSplitter.SetCurrentChannel(window->DrawList, column->DrawChannelCurrent); + //window->ClipRect = column->ClipRect; + //IM_ASSERT(column->ClipRect.Max.x > column->ClipRect.Min.x && column->ClipRect.Max.y > column->ClipRect.Min.y); + //window->DrawList->_ClipRectStack.back() = ImVec4(column->ClipRect.Min.x, column->ClipRect.Min.y, column->ClipRect.Max.x, column->ClipRect.Max.y); + //window->DrawList->UpdateClipRect(); + window->DrawList->PopClipRect(); + window->DrawList->PushClipRect(column->ClipRect.Min, column->ClipRect.Max, false); + //IMGUI_DEBUG_LOG("%d (%.0f,%.0f)(%.0f,%.0f)\n", column_no, column->ClipRect.Min.x, column->ClipRect.Min.y, column->ClipRect.Max.x, column->ClipRect.Max.y); + window->ClipRect = window->DrawList->_ClipRectStack.back(); + } +} + +// [Internal] +void ImGui::TableEndCell(ImGuiTable* table) +{ + ImGuiTableColumn* column = &table->Columns[table->CurrentColumn]; + ImGuiWindow* window = table->InnerWindow; + + // Report maximum position so we can infer content size per column. + float* p_max_pos_x; + if (table->RowFlags & ImGuiTableRowFlags_Headers) + p_max_pos_x = &column->ContentMaxPosHeadersUsed; // Useful in case user submit contents in header row that is not a TableHeader() call + else + p_max_pos_x = table->IsFreezeRowsPassed ? &column->ContentMaxPosRowsUnfrozen : &column->ContentMaxPosRowsFrozen; + *p_max_pos_x = ImMax(*p_max_pos_x, window->DC.CursorMaxPos.x); + table->RowPosY2 = ImMax(table->RowPosY2, window->DC.CursorMaxPos.y); + + // Propagate text baseline for the entire row + // FIXME-TABLE: Here we propagate text baseline from the last line of the cell.. instead of the first one. + table->RowTextBaseline = ImMax(table->RowTextBaseline, window->DC.PrevLineTextBaseOffset); +} + +// Append into the next cell +// FIXME-TABLE: Wrapping to next row should be optional? +bool ImGui::TableNextCell() +{ + ImGuiContext& g = *GImGui; + ImGuiTable* table = g.CurrentTable; + + if (table->CurrentColumn != -1 && table->CurrentColumn + 1 < table->ColumnsCount) + { + TableEndCell(table); + TableBeginCell(table, table->CurrentColumn + 1); + } + else + { + TableNextRow(); + } + + ImGuiTableColumn* column = &table->Columns[table->CurrentColumn]; + return column->IsActive; +} + +const char* ImGui::TableGetColumnName(int column_n) +{ + ImGuiContext& g = *GImGui; + ImGuiTable* table = g.CurrentTable; + if (!table) + return NULL; + if (column_n < 0) + column_n = table->CurrentColumn; + return TableGetColumnName(table, column_n); +} + +bool ImGui::TableGetColumnIsVisible(int column_n) +{ + ImGuiContext& g = *GImGui; + ImGuiTable* table = g.CurrentTable; + if (!table) + return false; + if (column_n < 0) + column_n = table->CurrentColumn; + return (table->ActiveMaskByIndex & ((ImU64)1 << column_n)) != 0; +} + +int ImGui::TableGetColumnIndex() +{ + ImGuiContext& g = *GImGui; + ImGuiTable* table = g.CurrentTable; + if (!table) + return 0; + return table->CurrentColumn; +} + +bool ImGui::TableSetColumnIndex(int column_idx) +{ + ImGuiContext& g = *GImGui; + ImGuiTable* table = g.CurrentTable; + if (!table) + return false; + + if (table->CurrentColumn != column_idx) + { + if (table->CurrentColumn != -1) + TableEndCell(table); + IM_ASSERT(column_idx >= 0 && table->ColumnsCount); + TableBeginCell(table, column_idx); + } + + return (table->ActiveMaskByIndex & ((ImU64)1 << column_idx)) != 0; +} + +ImRect ImGui::TableGetCellRect() +{ + ImGuiContext& g = *GImGui; + ImGuiTable* table = g.CurrentTable; + ImGuiTableColumn* column = &table->Columns[table->CurrentColumn]; + return ImRect(column->MinX, table->RowPosY1, column->MaxX, table->RowPosY2); +} + +const char* ImGui::TableGetColumnName(ImGuiTable* table, int column_no) +{ + ImGuiTableColumn* column = &table->Columns[column_no]; + if (column->NameOffset == -1) + return NULL; + return &table->ColumnsNames.Buf[column->NameOffset]; +} + +void ImGui::PushTableBackground() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + ImGuiTable* table = g.CurrentTable; + table->DrawSplitter.SetCurrentChannel(window->DrawList, 0); + PushClipRect(table->HostClipRect.Min, table->HostClipRect.Max, false); +} + +void ImGui::PopTableBackground() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + ImGuiTable* table = g.CurrentTable; + ImGuiTableColumn* column = &table->Columns[table->CurrentColumn]; + table->DrawSplitter.SetCurrentChannel(window->DrawList, column->DrawChannelCurrent); + PopClipRect(); +} + +// FIXME-TABLE: Ideally this should be writable by the user. Full programmatic access to that data? +void ImGui::TableDrawContextMenu(ImGuiTable* table, int selected_column_n) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + if (window->SkipItems) + return; + + bool want_separator = false; + selected_column_n = ImClamp(selected_column_n, -1, table->ColumnsCount - 1); + + // Sizing + if (table->Flags & ImGuiTableFlags_Resizable) + { + if (ImGuiTableColumn* selected_column = (selected_column_n != -1) ? &table->Columns[selected_column_n] : NULL) + { + const bool can_resize = !(selected_column->Flags & (ImGuiTableColumnFlags_NoResize | ImGuiTableColumnFlags_WidthStretch)) && selected_column->IsActive; + if (MenuItem("Size column to fit", NULL, false, can_resize)) + selected_column->AutoFitFrames = 1; + } + + if (MenuItem("Size all columns to fit", NULL)) + { + for (int column_n = 0; column_n < table->ColumnsCount; column_n++) + { + ImGuiTableColumn* column = &table->Columns[column_n]; + if (column->IsActive) + column->AutoFitFrames = 1; + } + } + want_separator = true; + } + + // Ordering + if (table->Flags & ImGuiTableFlags_Reorderable) + { + if (MenuItem("Reset order", NULL, false, !table->IsDefaultDisplayOrder)) + table->IsResetDisplayOrderRequest = true; + want_separator = true; + } + + // Hiding / Visibility + if (table->Flags & ImGuiTableFlags_Hideable) + { + if (want_separator) + Separator(); + want_separator = false; + + PushItemFlag(ImGuiItemFlags_SelectableDontClosePopup, true); + for (int column_n = 0; column_n < table->ColumnsCount; column_n++) + { + ImGuiTableColumn* column = &table->Columns[column_n]; + const char* name = TableGetColumnName(table, column_n); + if (name == NULL) + name = ""; + + // Make sure we can't hide the last active column + bool menu_item_active = (column->Flags & ImGuiTableColumnFlags_NoHide) ? false : true; + if (column->IsActive && table->ColumnsActiveCount <= 1) + menu_item_active = false; + if (MenuItem(name, NULL, column->IsActive, menu_item_active)) + column->NextIsActive = !column->IsActive; + } + PopItemFlag(); + } +} + +// This is a helper to output headers based on the column names declared in TableSetupColumn() +// The intent is that advanced users would not need to use this helper and may create their own. +void ImGui::TableAutoHeaders() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + if (window->SkipItems) + return; + + ImGuiTable* table = g.CurrentTable; + IM_ASSERT(table && table->CurrentRow == -1); + + int open_context_popup = INT_MAX; + + // This for loop is constructed to not make use of internal functions, + // as this is intended to be a base template to copy and build from. + TableNextRow(ImGuiTableRowFlags_Headers, GetTextLineHeight()); + const int columns_count = table->ColumnsCount; + for (int column_n = 0; column_n < columns_count; column_n++) + { + if (!TableSetColumnIndex(column_n)) + continue; + + const char* name = TableGetColumnName(column_n); + + // FIXME-TABLE: Test custom user elements +#if 0 + if (column_n < 2) + { + static bool b[10] = {}; + PushID(column_n); + PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0, 0)); + Checkbox("##", &b[column_n]); + PopStyleVar(); + PopID(); + SameLine(0.0f, g.Style.ItemInnerSpacing.x); + } +#endif + + // [DEBUG] + //if (g.IO.KeyCtrl) { static char buf[32]; name = buf; ImGuiTableColumn* c = &table->Columns[column_n]; if (c->Flags & ImGuiTableColumnFlags_WidthStretch) ImFormatString(buf, 32, "%.3f>%.1f", c->ResizeWeight, c->WidthGiven); else ImFormatString(buf, 32, "%.1f", c->WidthGiven); } + + PushID(column_n); // Allow unnamed labels (generally accidental, but let's behave nicely with them) + TableHeader(name); + PopID(); + + // We don't use BeginPopupContextItem() because we want the popup to stay up even after the column is hidden + if (IsMouseReleased(1) && IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup)) + open_context_popup = column_n; + } + + // FIXME-TABLE: This is not user-land code any more... + window->SkipItems = table->BackupSkipItems; + + // Allow opening popup from the right-most section after the last column + // FIXME-TABLE: This is not user-land code any more... perhaps instead we should expose hovered column. + // and allow some sort of row-centric IsItemHovered() for full flexibility? + const float unused_x1 = (table->RightMostActiveColumn != -1) ? table->Columns[table->RightMostActiveColumn].MaxX : table->WorkRect.Min.x; + if (unused_x1 < table->WorkRect.Max.x) + { + // FIXME: We inherit ClipRect/SkipItem from last submitted column (active or not), let's override + window->ClipRect = table->InnerClipRect; + + ImVec2 backup_cursor_max_pos = window->DC.CursorMaxPos; + window->DC.CursorPos = ImVec2(unused_x1, table->RowPosY1); + ImVec2 size = ImVec2(table->WorkRect.Max.x - window->DC.CursorPos.x, table->RowPosY2 - table->RowPosY1); + if (size.x > 0.0f && size.y > 0.0f) + { + InvisibleButton("##RemainingSpace", size); + window->DC.CursorPos.y -= g.Style.ItemSpacing.y; + window->DC.CursorMaxPos = backup_cursor_max_pos; // Don't feed back into the width of the Header row + + // We don't use BeginPopupContextItem() because we want the popup to stay up even after the column is hidden + if (IsMouseReleased(1) && IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup)) + open_context_popup = -1; + } + + window->ClipRect = window->DrawList->_ClipRectStack.back(); + } + + // Context Menu + if (open_context_popup != INT_MAX) + { + table->IsContextPopupOpen = true; + table->ContextPopupColumn = (ImS8)open_context_popup; + OpenPopup("##TableContextMenu"); + } +} + +// 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. +// FIXME-TABLE: Should hold a selection state. +void ImGui::TableHeader(const char* label) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + if (window->SkipItems) + return; + + ImGuiTable* table = g.CurrentTable; + IM_ASSERT(table->CurrentColumn != -1); + const int column_n = table->CurrentColumn; + ImGuiTableColumn* column = &table->Columns[column_n]; + + float row_height = GetTextLineHeight(); + ImRect cell_r = TableGetCellRect(); + ImRect work_r = cell_r; + work_r.Min.x = window->DC.CursorPos.x; + work_r.Max.y = work_r.Min.y + row_height; + + // Label + if (label == NULL) + label = ""; + const char* label_end = FindRenderedTextEnd(label); + ImVec2 label_size = CalcTextSize(label, label_end, true); + ImVec2 label_pos = window->DC.CursorPos; + float ellipsis_max = work_r.Max.x; + + // Selectable + PushID(label); + + // FIXME-TABLE: Fix when padding are disabled. + //window->DC.CursorPos.x = column->MinX + table->CellPadding.x; + + // Keep header highlighted when context menu is open. (FIXME-TABLE: however we cannot assume the ID of said popup if it has been created by the user...) + const bool selected = (table->IsContextPopupOpen && table->ContextPopupColumn == column_n); + const bool pressed = Selectable("", selected, ImGuiSelectableFlags_DrawHoveredWhenHeld, ImVec2(0.0f, row_height)); + const bool held = IsItemActive(); + window->DC.CursorPos.y -= g.Style.ItemSpacing.y * 0.5f; + + // Drag and drop: re-order columns. Frozen columns are not reorderable. + // FIXME-TABLE: Scroll request while reordering a column and it lands out of the scrolling zone. + if (held && (table->Flags & ImGuiTableFlags_Reorderable) && IsMouseDragging(0) && !g.DragDropActive) + { + // While moving a column it will jump on the other side of the mouse, so we also test for MouseDelta.x + table->ReorderColumn = (ImS8)column_n; + if (g.IO.MouseDelta.x < 0.0f && g.IO.MousePos.x < cell_r.Min.x) + if (column->PrevActiveColumn != -1 && (column->IndexWithinActiveSet < table->FreezeColumnsRequest) == (table->Columns[column->PrevActiveColumn].IndexWithinActiveSet < table->FreezeColumnsRequest)) + table->ReorderColumnDir = -1; + if (g.IO.MouseDelta.x > 0.0f && g.IO.MousePos.x > cell_r.Max.x) + if (column->NextActiveColumn != -1 && (column->IndexWithinActiveSet < table->FreezeColumnsRequest) == (table->Columns[column->NextActiveColumn].IndexWithinActiveSet < table->FreezeColumnsRequest)) + table->ReorderColumnDir = +1; + } + + // Sort order arrow + float w_arrow = 0.0f; + float w_sort_text = 0.0f; + if ((table->Flags & ImGuiTableFlags_Sortable) && !(column->Flags & ImGuiTableColumnFlags_NoSort)) + { + const float ARROW_SCALE = 0.75f; + w_arrow = ImFloor(g.FontSize * ARROW_SCALE + g.Style.FramePadding.x);// table->CellPadding.x); + if (column->SortOrder != -1) + { + w_sort_text = 0.0f; + + char sort_order_suf[8]; + if (column->SortOrder > 0) + { + ImFormatString(sort_order_suf, IM_ARRAYSIZE(sort_order_suf), "%d", column->SortOrder + 1); + w_sort_text = g.Style.ItemInnerSpacing.x + CalcTextSize(sort_order_suf).x; + } + + float x = ImMax(cell_r.Min.x, work_r.Max.x - w_arrow - w_sort_text); + ellipsis_max -= w_arrow + w_sort_text; + + float y = label_pos.y; + ImU32 col = GetColorU32(ImGuiCol_Text); + if (column->SortOrder > 0) + { + PushStyleColor(ImGuiCol_Text, GetColorU32(ImGuiCol_Text, 0.70f)); + RenderText(ImVec2(x + g.Style.ItemInnerSpacing.x, y), sort_order_suf); + PopStyleColor(); + x += w_sort_text; + } + RenderArrow(window->DrawList, ImVec2(x, y), col, column->SortDirection == ImGuiSortDirection_Ascending ? ImGuiDir_Down : ImGuiDir_Up, ARROW_SCALE); + } + + // Handle clicking on column header to adjust Sort Order + if (pressed && table->ReorderColumn != column_n) + TableSortSpecsClickColumn(table, column, g.IO.KeyShift); + } + if (!held && table->ReorderColumn == column_n) + table->ReorderColumn = -1; + + // Render clipped label + // Clipping here ensure that in the majority of situations, all our header cells will be merged into a single draw call. + //window->DrawList->AddCircleFilled(ImVec2(ellipsis_max, label_pos.y), 40, IM_COL32_WHITE); + RenderTextEllipsis(window->DrawList, label_pos, ImVec2(ellipsis_max, label_pos.y + row_height + g.Style.FramePadding.y), ellipsis_max, ellipsis_max, label, label_end, &label_size); + + // We feed our unclipped width to the column without writing on CursorMaxPos, so that column is still considering for merging. + // FIXME-TABLE: Clarify policies of how label width and potential decorations (arrows) fit into auto-resize of the column + float max_pos_x = label_pos.x + label_size.x + w_sort_text + w_arrow; + column->ContentMaxPosHeadersUsed = ImMax(column->ContentMaxPosHeadersUsed, work_r.Max.x);// ImMin(max_pos_x, work_r.Max.x)); + column->ContentMaxPosHeadersDesired = ImMax(column->ContentMaxPosHeadersDesired, max_pos_x); + + PopID(); +} + +void ImGui::TableSortSpecsClickColumn(ImGuiTable* table, ImGuiTableColumn* clicked_column, bool add_to_existing_sort_orders) +{ + if (!(table->Flags & ImGuiTableFlags_MultiSortable)) + add_to_existing_sort_orders = false; + + ImS8 sort_order_max = 0; + if (add_to_existing_sort_orders) + for (int column_n = 0; column_n < table->ColumnsCount; column_n++) + sort_order_max = ImMax(sort_order_max, table->Columns[column_n].SortOrder); + + for (int column_n = 0; column_n < table->ColumnsCount; column_n++) + { + ImGuiTableColumn* column = &table->Columns[column_n]; + if (column == clicked_column) + { + // Set new sort direction and sort order + // - If the PreferSortDescending flag is set, we will default to a Descending direction on the first click. + // - Note that the PreferSortAscending flag is never checked, it is essentially the default and therefore a no-op. + // - 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. + if (column->SortOrder == -1) + column->SortDirection = (column->Flags & ImGuiTableColumnFlags_PreferSortDescending) ? (ImS8)ImGuiSortDirection_Descending : (ImU8)(ImGuiSortDirection_Ascending); + else + column->SortDirection = (ImU8)((column->SortDirection == ImGuiSortDirection_Ascending) ? ImGuiSortDirection_Descending : ImGuiSortDirection_Ascending); + if (column->SortOrder == -1 || !add_to_existing_sort_orders) + column->SortOrder = add_to_existing_sort_orders ? sort_order_max + 1 : 0; + } + else + { + if (!add_to_existing_sort_orders) + column->SortOrder = -1; + } + TableFixColumnSortDirection(column); + } + table->IsSettingsDirty = true; + table->IsSortSpecsDirty = true; +} + +// Return NULL if no sort specs. +// Return ->WantSort == true when the specs have changed since the last query. +const ImGuiTableSortSpecs* ImGui::TableGetSortSpecs() +{ + ImGuiContext& g = *GImGui; + ImGuiTable* table = g.CurrentTable; + IM_ASSERT(table != NULL); + + if (!(table->Flags & ImGuiTableFlags_Sortable)) + return NULL; + + // Flatten sort specs into user facing data + const bool was_dirty = table->IsSortSpecsDirty; + if (was_dirty) + { + TableSortSpecsSanitize(table); + + // Write output + table->SortSpecsData.resize(table->SortSpecsCount); + table->SortSpecs.ColumnsMask = 0x00; + for (int column_n = 0; column_n < table->ColumnsCount; column_n++) + { + ImGuiTableColumn* column = &table->Columns[column_n]; + if (column->SortOrder == -1) + continue; + ImGuiTableSortSpecsColumn* sort_spec = &table->SortSpecsData[column->SortOrder]; + sort_spec->ColumnUserID = column->UserID; + sort_spec->ColumnIndex = (ImU8)column_n; + sort_spec->SortOrder = (ImU8)column->SortOrder; + sort_spec->SortSign = (column->SortDirection == ImGuiSortDirection_Ascending) ? +1 : -1; + sort_spec->SortDirection = column->SortDirection; + table->SortSpecs.ColumnsMask |= (ImU64)1 << column_n; + } + } + + // User facing data + table->SortSpecs.Specs = table->SortSpecsData.Data; + table->SortSpecs.SpecsCount = table->SortSpecsData.Size; + table->SortSpecs.SpecsChanged = was_dirty; + table->IsSortSpecsDirty = false; + return table->SortSpecs.SpecsCount ? &table->SortSpecs : NULL; +} + +bool ImGui::TableGetColumnIsSorted(int column_n) +{ + ImGuiContext& g = *GImGui; + ImGuiTable* table = g.CurrentTable; + if (!table) + return false; + if (column_n < 0) + column_n = table->CurrentColumn; + ImGuiTableColumn* column = &table->Columns[column_n]; + return (column->SortOrder != -1); +} + +void ImGui::TableSortSpecsSanitize(ImGuiTable* table) +{ + // Clear SortOrder from hidden column and verify that there's no gap or duplicate. + int sort_order_count = 0; + ImU64 sort_order_mask = 0x00; + for (int column_n = 0; column_n < table->ColumnsCount; column_n++) + { + ImGuiTableColumn* column = &table->Columns[column_n]; + if (column->SortOrder != -1 && !column->IsActive) + column->SortOrder = -1; + if (column->SortOrder == -1) + continue; + sort_order_count++; + sort_order_mask |= ((ImU64)1 << column->SortOrder); + IM_ASSERT(sort_order_count < (int)sizeof(sort_order_mask) * 8); + } + + const bool need_fix_linearize = ((ImU64)1 << sort_order_count) != (sort_order_mask + 1); + const bool need_fix_single_sort_order = (sort_order_count > 1) && !(table->Flags & ImGuiTableFlags_MultiSortable); + if (need_fix_linearize || need_fix_single_sort_order) + { + ImU64 fixed_mask = 0x00; + for (int sort_n = 0; sort_n < sort_order_count; sort_n++) + { + // Fix: Rewrite sort order fields if needed so they have no gap or duplicate. + // (e.g. SortOrder 0 disappeared, SortOrder 1..2 exists --> rewrite then as SortOrder 0..1) + int column_with_smallest_sort_order = -1; + for (int column_n = 0; column_n < table->ColumnsCount; column_n++) + if ((fixed_mask & ((ImU64)1 << (ImU64)column_n)) == 0 && table->Columns[column_n].SortOrder != -1) + if (column_with_smallest_sort_order == -1 || table->Columns[column_n].SortOrder < table->Columns[column_with_smallest_sort_order].SortOrder) + column_with_smallest_sort_order = column_n; + IM_ASSERT(column_with_smallest_sort_order != -1); + fixed_mask |= ((ImU64)1 << column_with_smallest_sort_order); + table->Columns[column_with_smallest_sort_order].SortOrder = (ImS8)sort_n; + + // Fix: Make sure only one column has a SortOrder if ImGuiTableFlags_MultiSortable is not set. + if (need_fix_single_sort_order) + { + for (int column_n = 0; column_n < table->ColumnsCount; column_n++) + if (column_n != column_with_smallest_sort_order) + table->Columns[column_n].SortOrder = -1; + break; + } + } + } + + // Fallback default sort order (if no column has the ImGuiTableColumnFlags_DefaultSort flag) + if (sort_order_count == 0 && table->IsFirstFrame) + for (int column_n = 0; column_n < table->ColumnsCount; column_n++) + { + ImGuiTableColumn* column = &table->Columns[column_n]; + if (!(column->Flags & ImGuiTableColumnFlags_NoSort) && column->IsActive) + { + sort_order_count = 1; + column->SortOrder = 0; + break; + } + } + + table->SortSpecsCount = (ImS8)sort_order_count; +} + +//------------------------------------------------------------------------- +// TABLE - .ini settings +//------------------------------------------------------------------------- +// [Init] 1: TableSettingsHandler_ReadXXXX() Load and parse .ini file into TableSettings. +// [Main] 2: TableLoadSettings() When table is created, bind Table to TableSettings, serialize TableSettings data into Table. +// [Main] 3: TableSaveSettings() When table properties are modified, serialize Table data into bound or new TableSettings, mark .ini as dirty. +// [Main] 4: TableSettingsHandler_WriteAll() When .ini file is dirty (which can come from other source), save TableSettings into .ini file. +//------------------------------------------------------------------------- + +static ImGuiTableSettings* CreateTableSettings(ImGuiID id, int columns_count) +{ + ImGuiContext& g = *GImGui; + ImGuiTableSettings* settings = g.SettingsTables.alloc_chunk(sizeof(ImGuiTableSettings) + (size_t)columns_count * sizeof(ImGuiTableColumnSettings)); + IM_PLACEMENT_NEW(settings) ImGuiTableSettings(); + ImGuiTableColumnSettings* settings_column = settings->GetColumnSettings(); + for (int n = 0; n < columns_count; n++, settings_column++) + IM_PLACEMENT_NEW(settings_column) ImGuiTableColumnSettings(); + settings->ID = id; + settings->ColumnsCount = settings->ColumnsCountMax = (ImS8)columns_count; + return settings; +} + +static ImGuiTableSettings* FindTableSettingsByID(ImGuiID id) +{ + // FIXME-OPT: Might want to store a lookup map for this? + ImGuiContext& g = *GImGui; + for (ImGuiTableSettings* settings = g.SettingsTables.begin(); settings != NULL; settings = g.SettingsTables.next_chunk(settings)) + if (settings->ID == id) + return settings; + return NULL; +} + +ImGuiTableSettings* ImGui::TableFindSettings(ImGuiTable* table) +{ + if (table->SettingsOffset == -1) + return NULL; + + ImGuiContext& g = *GImGui; + ImGuiTableSettings* settings = g.SettingsTables.ptr_from_offset(table->SettingsOffset); + IM_ASSERT(settings->ID == table->ID); + if (settings->ColumnsCountMax < table->ColumnsCount) + { + settings->ID = 0; // Ditch storage if we won't fit because of a count change + return NULL; + } + return settings; +} + +void ImGui::TableSaveSettings(ImGuiTable* table) +{ + table->IsSettingsDirty = false; + if (table->Flags & ImGuiTableFlags_NoSavedSettings) + return; + + // Bind or create settings data + ImGuiContext& g = *GImGui; + ImGuiTableSettings* settings = TableFindSettings(table); + if (settings == NULL) + { + settings = CreateTableSettings(table->ID, table->ColumnsCount); + table->SettingsOffset = g.SettingsTables.offset_from_ptr(settings); + } + settings->ColumnsCount = (ImS8)table->ColumnsCount; + + // Serialize ImGuiTableSettings/ImGuiTableColumnSettings --> ImGuiTable/ImGuiTableColumn + IM_ASSERT(settings->ID == table->ID); + IM_ASSERT(settings->ColumnsCount == table->ColumnsCount && settings->ColumnsCountMax >= settings->ColumnsCount); + ImGuiTableColumn* column = table->Columns.Data; + ImGuiTableColumnSettings* column_settings = settings->GetColumnSettings(); + + // FIXME-TABLE: Logic to avoid saving default widths? + settings->SaveFlags = ImGuiTableFlags_Resizable; + for (int n = 0; n < table->ColumnsCount; n++, column++, column_settings++) + { + //column_settings->WidthOrWeight = column->WidthRequested; // FIXME-WIP + column_settings->Index = (ImS8)n; + column_settings->DisplayOrder = column->IndexDisplayOrder; + column_settings->SortOrder = column->SortOrder; + column_settings->SortDirection = column->SortDirection; + column_settings->Visible = column->IsActive; + + // We skip saving some data in the .ini file when they are unnecessary to restore our state + // FIXME-TABLE: We don't have logic to easily compare SortOrder to DefaultSortOrder yet. + if (column->IndexDisplayOrder != n) + settings->SaveFlags |= ImGuiTableFlags_Reorderable;; + if (column_settings->SortOrder != -1) + settings->SaveFlags |= ImGuiTableFlags_Sortable; + if (column_settings->Visible != ((column->Flags & ImGuiTableColumnFlags_DefaultHide) == 0)) + settings->SaveFlags |= ImGuiTableFlags_Hideable; + } + settings->SaveFlags &= table->Flags; + + MarkIniSettingsDirty(); +} + +void ImGui::TableLoadSettings(ImGuiTable* table) +{ + ImGuiContext& g = *GImGui; + table->IsSettingsRequestLoad = false; + if (table->Flags & ImGuiTableFlags_NoSavedSettings) + return; + + // Bind settings + ImGuiTableSettings* settings; + if (table->SettingsOffset == -1) + { + settings = FindTableSettingsByID(table->ID); + if (settings == NULL) + return; + table->SettingsOffset = g.SettingsTables.offset_from_ptr(settings); + } + else + { + settings = g.SettingsTables.ptr_from_offset(table->SettingsOffset); + } + table->IsSettingsLoaded = true; + settings->SaveFlags = table->Flags; + + // Serialize ImGuiTable/ImGuiTableColumn --> ImGuiTableSettings/ImGuiTableColumnSettings + ImGuiTableColumnSettings* column_settings = settings->GetColumnSettings(); + for (int data_n = 0; data_n < settings->ColumnsCount; data_n++, column_settings++) + { + int column_n = column_settings->Index; + if (column_n < 0 || column_n >= table->ColumnsCount) + continue; + ImGuiTableColumn* column = &table->Columns[column_n]; + //column->WidthRequested = column_settings->WidthOrWeight; // FIXME-WIP + if (column_settings->DisplayOrder != -1) + column->IndexDisplayOrder = column_settings->DisplayOrder; + if (column_settings->SortOrder != -1) + { + column->SortOrder = column_settings->SortOrder; + column->SortDirection = column_settings->SortDirection; + } + column->IsActive = column->NextIsActive = column_settings->Visible; + } + + // FIXME-TABLE: Need to validate .ini data + for (int column_n = 0; column_n < table->ColumnsCount; column_n++) + table->DisplayOrder[table->Columns[column_n].IndexDisplayOrder] = (ImU8)column_n; +} + +void* ImGui::TableSettingsHandler_ReadOpen(ImGuiContext*, ImGuiSettingsHandler*, const char* name) +{ + ImGuiID id = 0; + int columns_count = 0; + if (sscanf(name, "0x%08X,%d", &id, &columns_count) < 2) + return NULL; + return CreateTableSettings(id, columns_count); +} + +void ImGui::TableSettingsHandler_ReadLine(ImGuiContext*, ImGuiSettingsHandler*, void* entry, const char* line) +{ + // "Column 0 UserID=0x42AD2D21 Width=100 Visible=1 Order=0 Sort=0v" + ImGuiTableSettings* settings = (ImGuiTableSettings*)entry; + int column_n = 0, r = 0, n = 0; + if (sscanf(line, "Column %d%n", &column_n, &r) == 1) { line = ImStrSkipBlank(line + r); } else { return; } + if (column_n < 0 || column_n >= settings->ColumnsCount) + return; + + char c = 0; + ImGuiTableColumnSettings* column = settings->GetColumnSettings() + column_n; + column->Index = (ImS8)column_n; + if (sscanf(line, "UserID=0x%08X%n", (ImU32*)&n, &r) == 1) { line = ImStrSkipBlank(line + r); column->UserID = (ImGuiID)n; } + if (sscanf(line, "Width=%d%n", &n, &r) == 1) { line = ImStrSkipBlank(line + r); /* .. */ settings->SaveFlags |= ImGuiTableFlags_Resizable; } + if (sscanf(line, "Visible=%d%n", &n, &r) == 1) { line = ImStrSkipBlank(line + r); column->Visible = (ImU8)n; settings->SaveFlags |= ImGuiTableFlags_Hideable; } + if (sscanf(line, "Order=%d%n", &n, &r) == 1) { line = ImStrSkipBlank(line + r); column->DisplayOrder = (ImS8)n; settings->SaveFlags |= ImGuiTableFlags_Reorderable; } + if (sscanf(line, "Sort=%d%c%n", &n, &c, &r) == 2) { line = ImStrSkipBlank(line + r); column->SortOrder = (ImS8)n; column->SortDirection = (c == '^') ? ImGuiSortDirection_Descending : ImGuiSortDirection_Ascending; settings->SaveFlags |= ImGuiTableFlags_Sortable; } +} + +void ImGui::TableSettingsHandler_WriteAll(ImGuiContext* ctx, ImGuiSettingsHandler* handler, ImGuiTextBuffer* buf) +{ + ImGuiContext& g = *ctx; + for (ImGuiTableSettings* settings = g.SettingsTables.begin(); settings != NULL; settings = g.SettingsTables.next_chunk(settings)) + { + if (settings->ID == 0) // Skip ditched settings + continue; + + // TableSaveSettings() may clear some of those flags when we establish that the data can be stripped (e.g. Order was unchanged) + const bool save_size = (settings->SaveFlags & ImGuiTableFlags_Resizable) != 0; + const bool save_visible = (settings->SaveFlags & ImGuiTableFlags_Hideable) != 0; + const bool save_order = (settings->SaveFlags & ImGuiTableFlags_Reorderable) != 0; + const bool save_sort = (settings->SaveFlags & ImGuiTableFlags_Sortable) != 0; + if (!save_size && !save_visible && !save_order && !save_sort) + continue; + + buf->reserve(buf->size() + 30 + settings->ColumnsCount * 50); // ballpark reserve + buf->appendf("[%s][0x%08X,%d]\n", handler->TypeName, settings->ID, settings->ColumnsCount); + ImGuiTableColumnSettings* column = settings->GetColumnSettings(); + for (int column_n = 0; column_n < settings->ColumnsCount; column_n++, column++) + { + // "Column 0 UserID=0x42AD2D21 Width=100 Visible=1 Order=0 Sort=0v" + if (column->UserID != 0) + buf->appendf("Column %-2d UserID=%08X", column_n, column->UserID); + else + buf->appendf("Column %-2d", column_n); + if (save_size) buf->appendf(" Width=%d", 0);// (int)settings_column->WidthOrWeight); // FIXME-TABLE + if (save_visible) buf->appendf(" Visible=%d", column->Visible); + if (save_order) buf->appendf(" Order=%d", column->DisplayOrder); + if (save_sort && column->SortOrder != -1) buf->appendf(" Sort=%d%c", column->SortOrder, (column->SortDirection == ImGuiSortDirection_Ascending) ? 'v' : '^'); + buf->append("\n"); + } + buf->append("\n"); + } +} + +//------------------------------------------------------------------------- +// TABLE - Debugging +//------------------------------------------------------------------------- +// - DebugNodeTable() [Internal] +//------------------------------------------------------------------------- + +void ImGui::DebugNodeTable(ImGuiTable* table) +{ + char buf[256]; + char* p = buf; + const char* buf_end = buf + IM_ARRAYSIZE(buf); + ImFormatString(p, buf_end - p, "Table 0x%08X (%d columns, in '%s')", table->ID, table->ColumnsCount, table->OuterWindow->Name); + bool open = TreeNode(table, "%s", buf); + if (IsItemHovered()) + GetForegroundDrawList()->AddRect(table->OuterRect.Min, table->OuterRect.Max, IM_COL32(255, 255, 0, 255)); + if (open) + { + for (int n = 0; n < table->ColumnsCount; n++) + { + ImGuiTableColumn* column = &table->Columns[n]; + const char* name = TableGetColumnName(table, n); + BulletText("Column %d order %d name '%s': +%.1f to +%.1f\n" + "Active: %d, DrawChannels: %d,%d\n" + "WidthGiven/Requested: %.1f/%.1f, Weight: %.2f\n" + "UserID: 0x%08X, Flags: 0x%04X: %s%s%s%s..", + n, column->IndexDisplayOrder, name ? name : "NULL", column->MinX - table->WorkRect.Min.x, column->MaxX - table->WorkRect.Min.x, + column->IsActive, column->DrawChannelRowsBeforeFreeze, column->DrawChannelRowsAfterFreeze, + column->WidthGiven, column->WidthRequested, column->ResizeWeight, + column->UserID, column->Flags, + (column->Flags & ImGuiTableColumnFlags_WidthFixed) ? "WidthFixed " : "", + (column->Flags & ImGuiTableColumnFlags_WidthStretch) ? "WidthStretch " : "", + (column->Flags & ImGuiTableColumnFlags_WidthAlwaysAutoResize) ? "WidthAlwaysAutoResize " : "", + (column->Flags & ImGuiTableColumnFlags_NoResize) ? "NoResize " : ""); + } + ImGuiTableSettings* settings = TableFindSettings(table); + if (settings && TreeNode("Settings")) + { + BulletText("SaveFlags: 0x%08X", settings->SaveFlags); + BulletText("ColumnsCount: %d (max %d)", settings->ColumnsCount, settings->ColumnsCountMax); + for (int n = 0; n < settings->ColumnsCount; n++) + { + ImGuiTableColumnSettings* column_settings = &settings->GetColumnSettings()[n]; + BulletText("Column %d Order %d SortOrder %d Visible %d UserID 0x%08X WidthOrWeight %.3f", + n, column_settings->DisplayOrder, column_settings->SortOrder, column_settings->Visible, column_settings->UserID, column_settings->WidthOrWeight); + } + TreePop(); + } + TreePop(); + } +} + +//------------------------------------------------------------------------- + + + //------------------------------------------------------------------------- // [SECTION] Widgets: Columns, BeginColumns, EndColumns, etc. diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 3a30847d6..6954960b6 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -5999,6 +5999,8 @@ bool ImGui::Selectable(const char* label, bool selected, ImGuiSelectableFlags fl // which would be advantageous since most selectable are not selected. if (span_all_columns && window->DC.CurrentColumns) PushColumnsBackground(); + else if ((flags & ImGuiSelectableFlags_SpanAllColumns) && g.CurrentTable) + PushTableBackground(); // We use NoHoldingActiveID on menus so user can click and _hold_ on a menu then drag to browse child entries ImGuiButtonFlags button_flags = 0; @@ -6047,6 +6049,8 @@ bool ImGui::Selectable(const char* label, bool selected, ImGuiSelectableFlags fl if (span_all_columns && window->DC.CurrentColumns) PopColumnsBackground(); + else if (span_all_columns && g.CurrentTable) + PopTableBackground(); if (flags & ImGuiSelectableFlags_Disabled) PushStyleColor(ImGuiCol_Text, style.Colors[ImGuiCol_TextDisabled]); RenderTextClipped(text_min, text_max, label, NULL, &label_size, style.SelectableTextAlign, &bb);