From 5caf3700d51acb9baefc7600f7d5f0b2a2b859d1 Mon Sep 17 00:00:00 2001 From: Dipal Date: Tue, 1 Sep 2026 16:18:02 -0400 Subject: [PATCH 1/5] View: Limit table column width and auto-fit to content Long values (e.g. demangled kernel names up to a few KB) previously stretched auto-sized table columns to thousands of pixels. Cap each column's fitted width and elide overflowing cell text to the live column width, keeping the full value in a hover tooltip (copy/export unaffected). Re-fit columns to their content when the result set changes (track select/deselect, filter, time-range selection), leaving columns the user manually resized alone and resetting to auto-fit once the table is emptied. Sorting and scrolling no longer re-fit, so widths stay consistent while reordering/paging. Also make gui_helpers' ElideWithEllipsis measure in a single pass (instead of popping one char at a time), give it an optional character cap, and share a TEXT_ELLIPSIS constant. Fixes #1023 --- .../src/widgets/rocprofvis_gui_helpers.cpp | 27 ++- src/view/src/widgets/rocprofvis_gui_helpers.h | 10 +- .../rocprofvis_infinite_scroll_table.cpp | 169 +++++++++++++++++- .../rocprofvis_infinite_scroll_table.h | 12 ++ 4 files changed, 203 insertions(+), 15 deletions(-) diff --git a/src/view/src/widgets/rocprofvis_gui_helpers.cpp b/src/view/src/widgets/rocprofvis_gui_helpers.cpp index 4974bfefd..bc667b4e6 100644 --- a/src/view/src/widgets/rocprofvis_gui_helpers.cpp +++ b/src/view/src/widgets/rocprofvis_gui_helpers.cpp @@ -456,16 +456,33 @@ ElidedText(const char* text, float available_width, float tooltip_width, std::string ElideWithEllipsis(const std::string& text, float max_width, size_t max_chars) { - std::string out = text.substr(0, max_chars); - bool truncated = text.size() > max_chars; - while(!out.empty() && ImGui::CalcTextSize((out + "...").c_str()).x > max_width) + // Optional hard character cap first. + const std::string capped = + (max_chars < text.size()) ? text.substr(0, max_chars) : text; + bool truncated = capped.size() < text.size(); + + // Trim to fit max_width in one pass (CalcTextSizeA reports where it stopped). + const char* begin = capped.c_str(); + const char* end = begin + capped.size(); + const float ellipsis_w = ImGui::CalcTextSize(TEXT_ELLIPSIS).x; + const char* remaining = begin; + ImGui::GetFont()->CalcTextSizeA(ImGui::GetFontSize(), + std::max(max_width - ellipsis_w, 0.0f), 0.0f, begin, + end, &remaining); + if(remaining < end) { - out.pop_back(); truncated = true; } + // Keep at least one character so a shortened value is not just the ellipsis. + if(remaining == begin && end > begin) + { + remaining = begin + 1; + } + + std::string out(begin, remaining); if(truncated) { - out += "..."; + out += TEXT_ELLIPSIS; } return out; } diff --git a/src/view/src/widgets/rocprofvis_gui_helpers.h b/src/view/src/widgets/rocprofvis_gui_helpers.h index 5efcf32eb..09db98245 100644 --- a/src/view/src/widgets/rocprofvis_gui_helpers.h +++ b/src/view/src/widgets/rocprofvis_gui_helpers.h @@ -114,15 +114,19 @@ enum Alignment Alignment_Right, }; +// Ellipsis appended to shortened text, so every caller renders the same marker. +inline constexpr const char* TEXT_ELLIPSIS = "..."; + bool ElidedText(const char* text, float available_width, float tooltip_width = 0.0f, Alignment alignment = Alignment_Left, bool imgui_AlignTextToFramePadding = false); -// Trims text to a single line fitting max_width (and max_chars), appending "..." -// when shortened. Uses the current font for measurement. +// Trims text to fit max_width (current font), appending TEXT_ELLIPSIS when +// shortened. max_chars optionally caps the character count. std::string -ElideWithEllipsis(const std::string& text, float max_width, size_t max_chars); +ElideWithEllipsis(const std::string& text, float max_width, + size_t max_chars = std::string::npos); void CenterNextTextItem(const char* text); diff --git a/src/view/src/widgets/rocprofvis_infinite_scroll_table.cpp b/src/view/src/widgets/rocprofvis_infinite_scroll_table.cpp index 862c4b1e8..ea388adbd 100644 --- a/src/view/src/widgets/rocprofvis_infinite_scroll_table.cpp +++ b/src/view/src/widgets/rocprofvis_infinite_scroll_table.cpp @@ -11,6 +11,7 @@ #include "spdlog/spdlog.h" #include "widgets/rocprofvis_gui_helpers.h" #include "widgets/rocprofvis_notification_manager.h" +#include "imgui_internal.h" #include #include @@ -29,6 +30,9 @@ constexpr const char* FILTER_TEXT_HINT_NUMERICAL = "Filter: {>, <, =, >=, <=, != constexpr const char* FILTER_TEXT_HINT_TIME = "Filter: {>, <, =, >=, <=, !=} {Nanoseconds}"; +// Cap on a column's fitted width, in multiples of the current font size. +constexpr float MAX_COLUMN_FIT_WIDTH_EM = 40.0f; + InfiniteScrollTable::InfiniteScrollTable( DataProvider& dp, TableType table_type, rocprofvis_controller_table_type_t request_table_type, uint64_t request_id, @@ -62,6 +66,9 @@ InfiniteScrollTable::InfiniteScrollTable( , m_update_filter_row(false) , m_reset_filter_row(false) , m_data_changed(true) +, m_refit_pending(false) +, m_refit_requested(false) +, m_columns_emptied(false) , m_filter_requested(false) , m_fetch_data(false) , m_fetch_cancelled(false) @@ -248,6 +255,17 @@ InfiniteScrollTable::HandleNewTableData(std::shared_ptr e) m_data_changed = true; m_update_filter_row = m_display_filter_row; IndexColumns(); + + // Re-fit columns only for a content change (select/filter), not a sort or + // scroll page - else sorting would fit to whatever loaded at the top. + std::shared_ptr table_event = + std::dynamic_pointer_cast(e); + if(table_event && table_event->GetRequestID() == m_request_id && + m_refit_requested) + { + m_refit_pending = true; + m_refit_requested = false; + } } } @@ -292,6 +310,12 @@ InfiniteScrollTable::Render() m_last_total_row_count = total_row_count; } + // Emptying the table forgets manual sizes so the next content re-fits fresh. + if(total_row_count == 0) + { + m_columns_emptied = true; + } + uint64_t row_count = 0; uint64_t start_row = 0; if(table_params) @@ -416,6 +440,15 @@ InfiniteScrollTable::Render() ImGui::TableHeadersRow(); + // Note manual resizes before re-fitting so it skips user-sized columns. + DetectUserColumnResizes(); + + if(m_refit_pending) + { + FitColumnsToContent(); + m_refit_pending = false; + } + if(m_display_filter_row) { ImGui::TableNextRow(); @@ -701,10 +734,117 @@ InfiniteScrollTable::FetchData() } } +void +InfiniteScrollTable::FitColumnsToContent() +{ + ImGuiTable* table = ImGui::GetCurrentTable(); + if(!table) + { + return; + } + + const std::vector& column_names = + m_table_model().GetTableHeader(m_table_type); + const std::vector>& table_data = + m_table_model().GetTableData(m_table_type); + const std::vector& formatted = + m_table_model().GetFormattedTableData(m_table_type); + + // Keep current widths on an empty result instead of collapsing to headers. + if(table_data.empty()) + { + return; + } + + // Forget manual sizes when the column set changes or the table was emptied. + const int cols = table->ColumnsCount; + if(m_columns_emptied || static_cast(m_user_sized_columns.size()) != cols) + { + m_user_sized_columns.assign(cols, false); + m_column_fit_widths.assign(cols, -1.0f); + m_columns_emptied = false; + } + + const float max_width = ImGui::GetFontSize() * MAX_COLUMN_FIT_WIDTH_EM; + const float padding = ImGui::GetStyle().ItemSpacing.x; + const int column_count = std::min(static_cast(column_names.size()), cols); + + for(int c = 0; c < column_count; c++) + { + if(column_names[c].empty() || column_names[c][0] == '_') + { + continue; // Internal / hidden column. + } + if(m_user_sized_columns[c]) + { + continue; // Respect the user's manual width. + } + + // Header label plus room for the sort arrow, then the widest cached cell. + float width = ImGui::CalcTextSize(column_names[c].c_str()).x + ImGui::GetFontSize(); + + const FormattedColumnInfo* formatting = + (c < static_cast(formatted.size())) ? &formatted[c] : nullptr; + for(size_t row = 0; row < table_data.size(); row++) + { + if(c >= static_cast(table_data[row].size())) + { + continue; + } + const std::string* value = &table_data[row][c]; + if(formatting && formatting->needs_formatting && + row < formatting->formatted_row_value.size()) + { + value = &formatting->formatted_row_value[row]; + } + width = std::max(width, ImGui::CalcTextSize(value->c_str()).x); + } + + width = std::min(width, max_width) + padding; + table->Columns[c].WidthRequest = width; + table->Columns[c].AutoFitQueue = 0; + m_column_fit_widths[c] = width; + } +} + +void +InfiniteScrollTable::DetectUserColumnResizes() +{ + ImGuiTable* table = ImGui::GetCurrentTable(); + if(!table) + { + return; + } + + const int count = + std::min(table->ColumnsCount, static_cast(m_column_fit_widths.size())); + for(int c = 0; c < count; c++) + { + if(m_user_sized_columns[c] || m_column_fit_widths[c] < 0.0f) + { + continue; + } + // WidthRequest only diverges from our applied value on a user drag. + const float delta = table->Columns[c].WidthRequest - m_column_fit_widths[c]; + if(delta > 0.5f || delta < -0.5f) + { + m_user_sized_columns[c] = true; + } + } +} + void InfiniteScrollTable::RenderCell(const std::string* cell_text, int row, int column) { - if(CopyableTextUnformatted(cell_text->c_str(), + // Elide to the live column width (widths are set by FitColumnsToContent); the + // full value stays in the tooltip and copy actions. + const float avail_width = ImGui::GetContentRegionAvail().x; + const bool is_elided = ImGui::CalcTextSize(cell_text->c_str()).x > avail_width; + std::string elided_text = is_elided ? ElideWithEllipsis(*cell_text, avail_width) + : std::string(); + const std::string* display_text = is_elided ? &elided_text : cell_text; + + if(CopyableTextUnformatted(display_text->c_str(), std::to_string(row) + ":" + std::to_string(column), COPY_DATA_NOTIFICATION, false, false)) { @@ -713,6 +853,11 @@ InfiniteScrollTable::RenderCell(const std::string* cell_text, int row, int colum RowSelected(ImGuiMouseButton_Left); } + if(is_elided && ImGui::IsItemHovered()) + { + SetTooltipStyled("%s", cell_text->c_str()); + } + if(ImGui::IsItemClicked(ImGuiMouseButton_Right)) { m_selected_row = row; @@ -862,13 +1007,18 @@ InfiniteScrollTable::ProcessSortOrFilterRequest(uint64_t frame_count) m_filter_options.group_columns.clear(); } + // Only a filter/group change (not a sort) re-fits the columns. + const bool filter_changed = + table_params->m_filter != m_filter_options.filter || + table_params->m_group != m_filter_options.group_by || + table_params->m_group_columns != m_filter_options.group_columns; + const bool sort_changed = + table_params->m_sort_order != m_sort_order || + table_params->m_sort_column_index != m_sort_column_index; + // check that requested actually are different from the // current values before fetching - if(table_params->m_sort_order != m_sort_order || - table_params->m_sort_column_index != m_sort_column_index || - table_params->m_filter != m_filter_options.filter || - table_params->m_group != m_filter_options.group_by || - table_params->m_group_columns != m_filter_options.group_columns) + if(sort_changed || filter_changed) { // if filtering changed reset the start row as current row // may be beyond result length causing an assertion in controller @@ -876,8 +1026,12 @@ InfiniteScrollTable::ProcessSortOrFilterRequest(uint64_t frame_count) { m_fetch_start_row = 0; } + if(filter_changed) + { + m_refit_requested = true; + } - spdlog::debug("Fetching data for sort, frame count: {}", frame_count); + spdlog::debug("Fetching data for sort/filter, frame count: {}", frame_count); // Fetch the event table with the updated params m_fetch_data = true; @@ -1107,6 +1261,7 @@ InfiniteScrollTable::RequestFetch() { m_fetch_data = true; m_fetch_start_row = 0; + m_refit_requested = true; // New content: re-fit columns. } void diff --git a/src/view/src/widgets/rocprofvis_infinite_scroll_table.h b/src/view/src/widgets/rocprofvis_infinite_scroll_table.h index 7df110fa0..9d8b3fa84 100644 --- a/src/view/src/widgets/rocprofvis_infinite_scroll_table.h +++ b/src/view/src/widgets/rocprofvis_infinite_scroll_table.h @@ -143,6 +143,13 @@ class InfiniteScrollTable : public RocWidget void RenderContextMenu(); void ProcessSortOrFilterRequest(uint64_t frame_count); + // Sizes non-user-resized columns to their widest cached value (capped). Call + // while the table is active. + void FitColumnsToContent(); + + // Flags columns the user manually resized so FitColumnsToContent skips them. + void DetectUserColumnResizes(); + int m_fetch_pad_items; int m_fetch_threshold_items; uint64_t m_fetch_start_row; @@ -154,6 +161,11 @@ class InfiniteScrollTable : public RocWidget // Internal state flags below bool m_open_context_menu; bool m_skip_data_fetch; + bool m_refit_pending; // Fresh content-change data is loaded; re-fit columns. + bool m_refit_requested; // A content change (filter/selection) is in flight. + bool m_columns_emptied; // Table went empty; the next fit forgets manual sizes. + std::vector m_user_sized_columns; // Columns the user manually resized. + std::vector m_column_fit_widths; // Width last auto-fit per column (-1 = none). uint64_t m_last_total_row_count; ImVec2 m_last_table_size; From 92010f9baabe0f5ffdbbdce19931ae803c0b54c8 Mon Sep 17 00:00:00 2001 From: Dipal Date: Thu, 3 Sep 2026 12:20:36 -0400 Subject: [PATCH 2/5] View: refine table column auto-fit and filter hints - Key column sizing by name so adding/removing tracks keeps manual widths - Clamp fit width to [min, max] and only grow on scroll page-in - Replace verbose filter placeholders with a "Filter" hint + hover help tooltip --- .../rocprofvis_infinite_scroll_table.cpp | 159 +++++++++++------- .../rocprofvis_infinite_scroll_table.h | 20 ++- 2 files changed, 110 insertions(+), 69 deletions(-) diff --git a/src/view/src/widgets/rocprofvis_infinite_scroll_table.cpp b/src/view/src/widgets/rocprofvis_infinite_scroll_table.cpp index ea388adbd..8dccecb41 100644 --- a/src/view/src/widgets/rocprofvis_infinite_scroll_table.cpp +++ b/src/view/src/widgets/rocprofvis_infinite_scroll_table.cpp @@ -25,12 +25,10 @@ constexpr uint64_t FETCH_CHUNK_SIZE = 1000; constexpr const char* START_TS_COLUMN_NAME = "start"; constexpr const char* END_TS_COLUMN_NAME = "end"; constexpr const char* DURATION_COLUMN_NAME = "duration"; -constexpr const char* FILTER_TEXT_HINT_STR = "Filter: hipLaunchKernel"; -constexpr const char* FILTER_TEXT_HINT_NUMERICAL = "Filter: {>, <, =, >=, <=, !=} 30"; -constexpr const char* FILTER_TEXT_HINT_TIME = - "Filter: {>, <, =, >=, <=, !=} {Nanoseconds}"; +constexpr const char* FILTER_INPUT_PLACEHOLDER = "Filter"; +constexpr const char* FILTER_OPERATORS = "> < = >= <= !="; -// Cap on a column's fitted width, in multiples of the current font size. +constexpr float MIN_COLUMN_WIDTH_EM = 6.0f; constexpr float MAX_COLUMN_FIT_WIDTH_EM = 40.0f; InfiniteScrollTable::InfiniteScrollTable( @@ -68,6 +66,7 @@ InfiniteScrollTable::InfiniteScrollTable( , m_data_changed(true) , m_refit_pending(false) , m_refit_requested(false) +, m_grow_pending(false) , m_columns_emptied(false) , m_filter_requested(false) , m_fetch_data(false) @@ -165,7 +164,6 @@ InfiniteScrollTable::Update() ROCPROFVIS_ASSERT(columns.size() == column_types.size()); m_displayed_filter_row_inputs.resize(columns.size()); std::unordered_set active_filter_row_inputs; - size_t j = 0; for(size_t i = 0; i < m_displayed_filter_row_inputs.size(); i++) { if(m_filter_row_inputs.count(columns[i])) @@ -177,36 +175,12 @@ InfiniteScrollTable::Update() } else { - const char* tooltip; - switch(column_types[i]) - { - case kRPVControllerPrimitiveTypeUInt64: - { - if((j < m_time_column_indices.size() && - m_time_column_indices[j] == i)) - { - tooltip = FILTER_TEXT_HINT_TIME; - j++; - } - else - { - tooltip = FILTER_TEXT_HINT_NUMERICAL; - } - break; - } - case kRPVControllerPrimitiveTypeDouble: - { - tooltip = FILTER_TEXT_HINT_NUMERICAL; - break; - } - default: - { - tooltip = FILTER_TEXT_HINT_STR; - break; - } - } + const bool is_time = + i == m_time_column_indices[kTimeStartNs] || + i == m_time_column_indices[kTimeEndNs] || + i == m_time_column_indices[kDurationNs]; m_filter_row_inputs[columns[i]] = { columns[i], column_types[i], "", - tooltip }; + is_time }; } m_displayed_filter_row_inputs[i] = &m_filter_row_inputs.at(columns[i]); } @@ -256,15 +230,21 @@ InfiniteScrollTable::HandleNewTableData(std::shared_ptr e) m_update_filter_row = m_display_filter_row; IndexColumns(); - // Re-fit columns only for a content change (select/filter), not a sort or - // scroll page - else sorting would fit to whatever loaded at the top. + // A content change (select/filter) re-fits the columns fresh; any other + // page-in of this table's data just lets columns grow to newly loaded values. std::shared_ptr table_event = std::dynamic_pointer_cast(e); - if(table_event && table_event->GetRequestID() == m_request_id && - m_refit_requested) + if(table_event && table_event->GetRequestID() == m_request_id) { - m_refit_pending = true; - m_refit_requested = false; + if(m_refit_requested) + { + m_refit_pending = true; + m_refit_requested = false; + } + else + { + m_grow_pending = true; + } } } } @@ -310,8 +290,9 @@ InfiniteScrollTable::Render() m_last_total_row_count = total_row_count; } - // Emptying the table forgets manual sizes so the next content re-fits fresh. - if(total_row_count == 0) + // Only a fully cleared table (all tracks deselected -> no columns) forgets + // manual widths. A filter that matches no rows keeps them. + if(column_names.empty()) { m_columns_emptied = true; } @@ -445,8 +426,14 @@ InfiniteScrollTable::Render() if(m_refit_pending) { - FitColumnsToContent(); + FitColumnsToContent(false); // fresh fit (may shrink) m_refit_pending = false; + m_grow_pending = false; + } + else if(m_grow_pending) + { + FitColumnsToContent(true); // scroll page-in: grow only + m_grow_pending = false; } if(m_display_filter_row) @@ -459,11 +446,15 @@ InfiniteScrollTable::Render() ImGui::TableNextColumn(); ImGui::PushID(static_cast(i)); std::pair filter_input = InputTextWithClear( - "", m_displayed_filter_row_inputs[i]->tooltip, + "", FILTER_INPUT_PLACEHOLDER, m_displayed_filter_row_inputs[i]->input, m_settings.GetFontManager().GetFont(FontType::kIcon), m_settings.GetColor(Colors::kBgMain), style, ImGui::GetContentRegionAvail().x); + if(ImGui::IsItemHovered()) + { + RenderFilterHelpTooltip(*m_displayed_filter_row_inputs[i]); + } if(filter_input.second) { m_displayed_filter_row_inputs[i]->input.clear(); @@ -735,7 +726,7 @@ InfiniteScrollTable::FetchData() } void -InfiniteScrollTable::FitColumnsToContent() +InfiniteScrollTable::FitColumnsToContent(bool grow_only) { ImGuiTable* table = ImGui::GetCurrentTable(); if(!table) @@ -756,32 +747,35 @@ InfiniteScrollTable::FitColumnsToContent() return; } - // Forget manual sizes when the column set changes or the table was emptied. - const int cols = table->ColumnsCount; - if(m_columns_emptied || static_cast(m_user_sized_columns.size()) != cols) + // Only a full table clear (all tracks deselected) forgets manual widths; + // adding or removing columns keeps every surviving column's sizing by name. + if(m_columns_emptied) { - m_user_sized_columns.assign(cols, false); - m_column_fit_widths.assign(cols, -1.0f); + m_user_sized_columns.clear(); + m_column_fit_widths.clear(); m_columns_emptied = false; } + const float min_width = ImGui::GetFontSize() * MIN_COLUMN_WIDTH_EM; const float max_width = ImGui::GetFontSize() * MAX_COLUMN_FIT_WIDTH_EM; const float padding = ImGui::GetStyle().ItemSpacing.x; - const int column_count = std::min(static_cast(column_names.size()), cols); + const int column_count = + std::min(static_cast(column_names.size()), table->ColumnsCount); for(int c = 0; c < column_count; c++) { - if(column_names[c].empty() || column_names[c][0] == '_') + const std::string& name = column_names[c]; + if(name.empty() || name[0] == '_') { continue; // Internal / hidden column. } - if(m_user_sized_columns[c]) + if(m_user_sized_columns.count(name)) { continue; // Respect the user's manual width. } - // Header label plus room for the sort arrow, then the widest cached cell. - float width = ImGui::CalcTextSize(column_names[c].c_str()).x + ImGui::GetFontSize(); + // Header label plus room for the sort arrow, then the widest loaded cell. + float content = ImGui::CalcTextSize(name.c_str()).x + ImGui::GetFontSize(); const FormattedColumnInfo* formatting = (c < static_cast(formatted.size())) ? &formatted[c] : nullptr; @@ -797,13 +791,21 @@ InfiniteScrollTable::FitColumnsToContent() { value = &formatting->formatted_row_value[row]; } - width = std::max(width, ImGui::CalcTextSize(value->c_str()).x); + content = std::max(content, ImGui::CalcTextSize(value->c_str()).x); + } + + const float width = std::min(std::max(content + padding, min_width), max_width); + + // grow_only: keep the wider width so columns never shrink mid-scroll. + const auto it = m_column_fit_widths.find(name); + if(grow_only && it != m_column_fit_widths.end() && width <= it->second) + { + continue; } - width = std::min(width, max_width) + padding; table->Columns[c].WidthRequest = width; table->Columns[c].AutoFitQueue = 0; - m_column_fit_widths[c] = width; + m_column_fit_widths[name] = width; } } @@ -816,19 +818,27 @@ InfiniteScrollTable::DetectUserColumnResizes() return; } + const std::vector& column_names = + m_table_model().GetTableHeader(m_table_type); const int count = - std::min(table->ColumnsCount, static_cast(m_column_fit_widths.size())); + std::min(table->ColumnsCount, static_cast(column_names.size())); for(int c = 0; c < count; c++) { - if(m_user_sized_columns[c] || m_column_fit_widths[c] < 0.0f) + const std::string& name = column_names[c]; + if(name.empty() || m_user_sized_columns.count(name)) { continue; } + const auto it = m_column_fit_widths.find(name); + if(it == m_column_fit_widths.end()) + { + continue; // No auto-fit applied yet; nothing to compare against. + } // WidthRequest only diverges from our applied value on a user drag. - const float delta = table->Columns[c].WidthRequest - m_column_fit_widths[c]; + const float delta = table->Columns[c].WidthRequest - it->second; if(delta > 0.5f || delta < -0.5f) { - m_user_sized_columns[c] = true; + m_user_sized_columns.insert(name); } } } @@ -872,6 +882,29 @@ InfiniteScrollTable::RenderCell(const std::string* cell_text, int row, int colum } } +void +InfiniteScrollTable::RenderFilterHelpTooltip(const FilterInput& input) const +{ + if(input.is_time) + { + SetTooltipStyled("Compare against a time in nanoseconds.\n" + "Operators: %s\nExample: > 1000000", + FILTER_OPERATORS); + } + else if(input.column_type == kRPVControllerPrimitiveTypeUInt64 || + input.column_type == kRPVControllerPrimitiveTypeDouble) + { + SetTooltipStyled("Compare against a numeric value.\n" + "Operators: %s\nExample: >= 30", + FILTER_OPERATORS); + } + else + { + SetTooltipStyled("Match rows that contain this text (case-insensitive).\n" + "Example: hipLaunchKernel"); + } +} + void InfiniteScrollTable::RenderContextMenu() { diff --git a/src/view/src/widgets/rocprofvis_infinite_scroll_table.h b/src/view/src/widgets/rocprofvis_infinite_scroll_table.h index 9d8b3fa84..74d94102f 100644 --- a/src/view/src/widgets/rocprofvis_infinite_scroll_table.h +++ b/src/view/src/widgets/rocprofvis_infinite_scroll_table.h @@ -11,6 +11,8 @@ #include #include #include +#include +#include #include namespace RocProfVis @@ -135,7 +137,7 @@ class InfiniteScrollTable : public RocWidget std::string column_name; rocprofvis_controller_primitive_type_t column_type; std::string input; - const char* tooltip; + bool is_time; // Value is a timestamp (ns). }; void FetchData(); @@ -143,9 +145,13 @@ class InfiniteScrollTable : public RocWidget void RenderContextMenu(); void ProcessSortOrFilterRequest(uint64_t frame_count); - // Sizes non-user-resized columns to their widest cached value (capped). Call - // while the table is active. - void FitColumnsToContent(); + // Hover tooltip explaining how to filter a column, with an example. + void RenderFilterHelpTooltip(const FilterInput& input) const; + + // Sizes non-user-resized columns to their widest loaded value, clamped to + // [MIN_COLUMN_WIDTH_EM, MAX_COLUMN_FIT_WIDTH_EM]. With grow_only, columns only + // grow (scroll page-in); otherwise they fit fresh. Call while the table is active. + void FitColumnsToContent(bool grow_only); // Flags columns the user manually resized so FitColumnsToContent skips them. void DetectUserColumnResizes(); @@ -163,9 +169,11 @@ class InfiniteScrollTable : public RocWidget bool m_skip_data_fetch; bool m_refit_pending; // Fresh content-change data is loaded; re-fit columns. bool m_refit_requested; // A content change (filter/selection) is in flight. + bool m_grow_pending; // A scroll page loaded; grow columns to fit it. bool m_columns_emptied; // Table went empty; the next fit forgets manual sizes. - std::vector m_user_sized_columns; // Columns the user manually resized. - std::vector m_column_fit_widths; // Width last auto-fit per column (-1 = none). + // Keyed by column name so adding/removing columns never disturbs existing sizing. + std::unordered_set m_user_sized_columns; // User-resized columns. + std::unordered_map m_column_fit_widths; // Last auto-fit width. uint64_t m_last_total_row_count; ImVec2 m_last_table_size; From 233221656887818758495bd11177f6ede91cbac7 Mon Sep 17 00:00:00 2001 From: Dipal Date: Mon, 14 Sep 2026 17:34:11 -0400 Subject: [PATCH 3/5] View: cap elided-cell tooltip width in infinite scroll table Long cell values (e.g. kernel names) now wrap at a fixed max width instead of stretching across the screen, matching the Compute view's Kernel Details table tooltip behaviour. --- .../src/widgets/rocprofvis_infinite_scroll_table.cpp | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/view/src/widgets/rocprofvis_infinite_scroll_table.cpp b/src/view/src/widgets/rocprofvis_infinite_scroll_table.cpp index 0571663ae..7a11c305b 100644 --- a/src/view/src/widgets/rocprofvis_infinite_scroll_table.cpp +++ b/src/view/src/widgets/rocprofvis_infinite_scroll_table.cpp @@ -14,7 +14,6 @@ #include "spdlog/spdlog.h" #include "widgets/rocprofvis_gui_helpers.h" #include "widgets/rocprofvis_notification_manager.h" -#include "imgui_internal.h" #include #include @@ -34,6 +33,8 @@ constexpr const char* FILTER_OPERATORS = "> < = >= <= !="; constexpr float MIN_COLUMN_WIDTH_EM = 6.0f; constexpr float MAX_COLUMN_FIT_WIDTH_EM = 40.0f; +constexpr float TOOLTIP_MAX_WIDTH = 600.0f; + InfiniteScrollTable::InfiniteScrollTable( DataProvider& dp, TableType table_type, rocprofvis_controller_table_type_t request_table_type, uint64_t request_id, @@ -979,7 +980,13 @@ InfiniteScrollTable::RenderCell(const std::string* cell_text, int row, int colum if(is_elided && ImGui::IsItemHovered()) { - SetTooltipStyled("%s", cell_text->c_str()); + ImGui::SetNextWindowSizeConstraints(ImVec2(0, 0), + ImVec2(TOOLTIP_MAX_WIDTH, FLT_MAX)); + BeginTooltipStyled(); + ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + TOOLTIP_MAX_WIDTH); + ImGui::TextUnformatted(cell_text->c_str()); + ImGui::PopTextWrapPos(); + EndTooltipStyled(); } if(ImGui::IsItemClicked(ImGuiMouseButton_Right)) From 8a21e9660f7bdafc707388d7b70cf179422061fe Mon Sep 17 00:00:00 2001 From: Dipal Date: Fri, 18 Sep 2026 15:11:39 -0400 Subject: [PATCH 4/5] View: fix column auto-fit detection and time-column tooltips Address PR review feedback on infinite scroll table column widths: - Detect manual resizes via ImGui's LastResizedColumn instead of a width diff, so font-size and time-format changes no longer strip a column's auto-fit. - Merge the elided-value and raw-ns tooltips for time columns into one tooltip so neither overrides the other on cut-off cells. - Drop the shadowing table_event redeclaration in HandleNewTableData (CodeQL: declaration hides variable). --- .../rocprofvis_infinite_scroll_table.cpp | 91 ++++++++++--------- .../rocprofvis_infinite_scroll_table.h | 5 +- 2 files changed, 51 insertions(+), 45 deletions(-) diff --git a/src/view/src/widgets/rocprofvis_infinite_scroll_table.cpp b/src/view/src/widgets/rocprofvis_infinite_scroll_table.cpp index 7a11c305b..840dea693 100644 --- a/src/view/src/widgets/rocprofvis_infinite_scroll_table.cpp +++ b/src/view/src/widgets/rocprofvis_infinite_scroll_table.cpp @@ -318,19 +318,14 @@ InfiniteScrollTable::HandleNewTableData(std::shared_ptr e) // A content change (select/filter) re-fits the columns fresh; any other // page-in of this table's data just lets columns grow to newly loaded values. - std::shared_ptr table_event = - std::dynamic_pointer_cast(e); - if(table_event && table_event->GetRequestID() == m_request_id) + if(m_refit_requested) { - if(m_refit_requested) - { - m_refit_pending = true; - m_refit_requested = false; - } - else - { - m_grow_pending = true; - } + m_refit_pending = true; + m_refit_requested = false; + } + else + { + m_grow_pending = true; } } } @@ -653,19 +648,20 @@ InfiniteScrollTable::Render() place_hitbox = false; } - // Render actual cells after the row hit-box - RenderCell(display_value, row_n, column); - if(m_time_column_indices[kTimeEndNs] == column || - m_time_column_indices[kTimeStartNs] == column || - m_time_column_indices[kDurationNs] == column) + // Time columns add their raw ns value to the cell tooltip. + const bool is_time_column = + m_time_column_indices[kTimeEndNs] == column || + m_time_column_indices[kTimeStartNs] == column || + m_time_column_indices[kDurationNs] == column; + std::string time_tooltip; + if(is_time_column) { - // show raw value as tooltip for time columns if hovered - if(ImGui::IsItemHovered()) - { - SetTooltipStyled("%s ns", col.c_str()); - } + time_tooltip = col + " ns"; } + RenderCell(display_value, row_n, column, + is_time_column ? &time_tooltip : nullptr); + column++; } ImGui::PopID(); @@ -933,33 +929,31 @@ InfiniteScrollTable::DetectUserColumnResizes() return; } + // LastResizedColumn is only set by a resize-grip drag, so font-size or + // time-format width changes are never mistaken for a manual resize. + const int resized = table->LastResizedColumn; + if(resized < 0 || resized >= table->ColumnsCount) + { + return; + } + const std::vector& column_names = m_table_model().GetTableHeader(m_table_type); - const int count = - std::min(table->ColumnsCount, static_cast(column_names.size())); - for(int c = 0; c < count; c++) + if(resized >= static_cast(column_names.size())) { - const std::string& name = column_names[c]; - if(name.empty() || m_user_sized_columns.count(name)) - { - continue; - } - const auto it = m_column_fit_widths.find(name); - if(it == m_column_fit_widths.end()) - { - continue; // No auto-fit applied yet; nothing to compare against. - } - // WidthRequest only diverges from our applied value on a user drag. - const float delta = table->Columns[c].WidthRequest - it->second; - if(delta > 0.5f || delta < -0.5f) - { - m_user_sized_columns.insert(name); - } + return; + } + + const std::string& name = column_names[resized]; + if(!name.empty()) + { + m_user_sized_columns.insert(name); } } void -InfiniteScrollTable::RenderCell(const std::string* cell_text, int row, int column) +InfiniteScrollTable::RenderCell(const std::string* cell_text, int row, int column, + const std::string* tooltip_extra) { // Elide to the live column width (widths are set by FitColumnsToContent); the // full value stays in the tooltip and copy actions. @@ -978,13 +972,22 @@ InfiniteScrollTable::RenderCell(const std::string* cell_text, int row, int colum RowSelected(ImGuiMouseButton_Left); } - if(is_elided && ImGui::IsItemHovered()) + const bool has_extra = tooltip_extra && !tooltip_extra->empty(); + if((is_elided || has_extra) && ImGui::IsItemHovered()) { + // Full value and extra line share one tooltip so neither overrides the other. ImGui::SetNextWindowSizeConstraints(ImVec2(0, 0), ImVec2(TOOLTIP_MAX_WIDTH, FLT_MAX)); BeginTooltipStyled(); ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + TOOLTIP_MAX_WIDTH); - ImGui::TextUnformatted(cell_text->c_str()); + if(is_elided) + { + ImGui::TextUnformatted(cell_text->c_str()); + } + if(has_extra) + { + ImGui::TextUnformatted(tooltip_extra->c_str()); + } ImGui::PopTextWrapPos(); EndTooltipStyled(); } diff --git a/src/view/src/widgets/rocprofvis_infinite_scroll_table.h b/src/view/src/widgets/rocprofvis_infinite_scroll_table.h index 80b8fe781..3d713d736 100644 --- a/src/view/src/widgets/rocprofvis_infinite_scroll_table.h +++ b/src/view/src/widgets/rocprofvis_infinite_scroll_table.h @@ -178,7 +178,10 @@ class InfiniteScrollTable : public RocWidget }; void FetchData(); - void RenderCell(const std::string* cell_text, int row, int column); + // Draws a table cell, eliding to the column width. tooltip_extra (e.g. a time + // column's raw ns value) is appended to the same hover tooltip as elided text. + void RenderCell(const std::string* cell_text, int row, int column, + const std::string* tooltip_extra = nullptr); void RenderContextMenu(); void ProcessSortOrFilterRequest(uint64_t frame_count); From 0697112b01d6b66d167e6a4648f719f601923f3c Mon Sep 17 00:00:00 2001 From: Dipal Date: Fri, 18 Sep 2026 16:17:49 -0400 Subject: [PATCH 5/5] View: fix same-frame table column cap, time-format grow, and UTF-8 elide --- .../src/widgets/rocprofvis_gui_helpers.cpp | 27 ++++++++++++- .../rocprofvis_infinite_scroll_table.cpp | 39 ++++++++++++------- 2 files changed, 49 insertions(+), 17 deletions(-) diff --git a/src/view/src/widgets/rocprofvis_gui_helpers.cpp b/src/view/src/widgets/rocprofvis_gui_helpers.cpp index db95f8864..73a31501f 100644 --- a/src/view/src/widgets/rocprofvis_gui_helpers.cpp +++ b/src/view/src/widgets/rocprofvis_gui_helpers.cpp @@ -35,6 +35,29 @@ StringResizeCallback(ImGuiInputTextCallbackData* data) } return 0; } + +// Byte length of the UTF-8 sequence at `begin` (from the lead byte), clamped to +// `end`. Returns >= 1 when begin < end so callers always advance. +size_t +Utf8SequenceLength(const char* begin, const char* end) +{ + if(begin >= end) + { + return 0; + } + const unsigned char lead = static_cast(*begin); + size_t len = 1; + if((lead & 0x80u) == 0x00u) // 0xxxxxxx + len = 1; + else if((lead & 0xE0u) == 0xC0u) // 110xxxxx + len = 2; + else if((lead & 0xF0u) == 0xE0u) // 1110xxxx + len = 3; + else if((lead & 0xF8u) == 0xF0u) // 11110xxx + len = 4; + // A continuation/invalid lead byte falls through as a single byte. + return std::min(len, static_cast(end - begin)); +} } // namespace bool @@ -473,10 +496,10 @@ ElideWithEllipsis(const std::string& text, float max_width, size_t max_chars) { truncated = true; } - // Keep at least one character so a shortened value is not just the ellipsis. + // Keep at least one whole codepoint so a multibyte character is never split. if(remaining == begin && end > begin) { - remaining = begin + 1; + remaining = begin + Utf8SequenceLength(begin, end); } std::string out(begin, remaining); diff --git a/src/view/src/widgets/rocprofvis_infinite_scroll_table.cpp b/src/view/src/widgets/rocprofvis_infinite_scroll_table.cpp index 840dea693..41245169f 100644 --- a/src/view/src/widgets/rocprofvis_infinite_scroll_table.cpp +++ b/src/view/src/widgets/rocprofvis_infinite_scroll_table.cpp @@ -103,6 +103,8 @@ InfiniteScrollTable::InfiniteScrollTable( (void) e; // Reformat time columns this->FormatData(); + // Grow columns next render so longer timestamps are not left elided. + m_grow_pending = true; }; m_format_changed_token = EventManager::GetInstance()->Subscribe( static_cast(RocEvents::kTimeFormatChanged), format_changed_handler); @@ -480,6 +482,22 @@ InfiniteScrollTable::Render() ImGui::TableSetupColumn(column_names[i].c_str(), col_flags); } + // Fit before TableGetSortSpecs()/TableHeadersRow() lock the layout + // so the capped width lands this frame instead of flashing ImGui's + // uncapped auto-fit. Detect resizes first so the fit skips them. + DetectUserColumnResizes(); + if(m_refit_pending) + { + FitColumnsToContent(false); // fresh fit (may shrink) + m_refit_pending = false; + m_grow_pending = false; + } + else if(m_grow_pending) + { + FitColumnsToContent(true); // scroll page-in: grow only + m_grow_pending = false; + } + // Sorting is off while a request is in flight, and the specs of a // table that cannot sort ignore this, so hold it until it can land. if(m_pending_sort && (table_flags & ImGuiTableFlags_Sortable)) @@ -523,21 +541,6 @@ InfiniteScrollTable::Render() ImGui::TableHeadersRow(); - // Note manual resizes before re-fitting so it skips user-sized columns. - DetectUserColumnResizes(); - - if(m_refit_pending) - { - FitColumnsToContent(false); // fresh fit (may shrink) - m_refit_pending = false; - m_grow_pending = false; - } - else if(m_grow_pending) - { - FitColumnsToContent(true); // scroll page-in: grow only - m_grow_pending = false; - } - if(m_display_filter_row) { ImGui::TableNextRow(); @@ -903,6 +906,12 @@ InfiniteScrollTable::FitColumnsToContent(bool grow_only) value = &formatting->formatted_row_value[row]; } content = std::max(content, ImGui::CalcTextSize(value->c_str()).x); + // Width is clamped to max_width; once we hit it no later row can widen + // the column, so stop (and skip CalcTextSize on long names). + if(content + padding >= max_width) + { + break; + } } const float width = std::min(std::max(content + padding, min_width), max_width);