diff --git a/openpilot/tools/cabana/ui/chart/chart.cc b/openpilot/tools/cabana/ui/chart/chart.cc index 169a588eaa..8dc5d627d0 100644 --- a/openpilot/tools/cabana/ui/chart/chart.cc +++ b/openpilot/tools/cabana/ui/chart/chart.cc @@ -6,7 +6,6 @@ #include #include #include -#include #include "tools/cabana/core/settings.h" #include "tools/cabana/settings.h" @@ -139,7 +138,7 @@ void ChartView::updateLayout() { const ImVec2 top_left = layout_.rect.Min + ImVec2(LAYOUT_MARGINS.x, LAYOUT_MARGINS.y); layout_.move_icon_rect = ImRect(top_left, top_left + grip); const ImVec2 btn_size(iconButtonWidth(), iconButtonWidth()); - const ImVec2 close_min(layout_.rect.Max.x - std::max(LAYOUT_MARGINS.z, CONTROL_OUTLINE_PADDING) - btn_size.x, top_left.y); + const ImVec2 close_min(layout_.rect.Max.x - ImGui::GetStyle().WindowPadding.x - btn_size.x, top_left.y); layout_.close_btn_rect = ImRect(close_min, close_min + btn_size); const ImVec2 manage_min(close_min.x - btn_size.x - ImGui::GetStyle().ItemInnerSpacing.x, top_left.y); layout_.manage_btn_rect = ImRect(manage_min, manage_min + btn_size); @@ -150,7 +149,7 @@ void ChartView::updateLayout() { const int marker_size = markerSize(); const int row_height = std::max(marker_size, fm_height) + fm_height + 3; // + the signal value line const int legend_left = layout_.move_icon_rect.Max.x + LEGEND_SPACING; - const int legend_right = std::max(layout_.manage_btn_rect.Min.x - LAYOUT_MARGINS.z, legend_left + 10); + const int legend_right = std::max(layout_.manage_btn_rect.Min.x - ImGui::GetStyle().ItemInnerSpacing.x, legend_left + 10); // layout legend entries left-to-right, wrapping between the move icon and the buttons layout_.legend_rects.clear(); @@ -158,6 +157,9 @@ void ChartView::updateLayout() { for (auto &s : sigs_) { int w = marker_size + LEGEND_SPACING + bold->CalcTextSizeA(font_size, FLT_MAX, 0.0f, s.sig->name.c_str()).x + ImGui::CalcTextSize(msgLabel(s.msg_id).c_str()).x; + pushMonoFont(font_size); + w = std::max(w, (int)std::ceil(ImGui::CalcTextSize("-0.00000e+000").x)); + popMonoFont(); w = std::min(w, legend_right - legend_left); // keep oversized entries clear of the header buttons if (x + w > legend_right && x > legend_left) { x = legend_left; @@ -262,13 +264,14 @@ void ChartView::updateAxisY() { auto [first, last] = visibleRange(s.vals); s.min = std::numeric_limits::max(); s.max = std::numeric_limits::lowest(); + if (first == last) continue; if (can->liveStreaming()) { for (auto it = first; it != last; ++it) { if (it->y < s.min) s.min = it->y; if (it->y > s.max) s.max = it->y; } } else { - std::tie(s.min, s.max) = s.segment_tree.minmax(std::distance(s.vals.cbegin(), first), std::distance(s.vals.cbegin(), last)); + std::tie(s.min, s.max) = s.segment_tree.minmax(std::distance(s.vals.cbegin(), first), std::distance(s.vals.cbegin(), last) - 1); } min = std::min(min, s.min); max = std::max(max, s.max); @@ -278,7 +281,8 @@ void ChartView::updateAxisY() { y_unit_ = unit; - double delta = std::abs(max - min) < 1e-3 ? 1 : (max - min) * 0.05; + const double magnitude = std::max(std::abs(min), std::abs(max)); + double delta = max - min <= magnitude * 1e-9 ? (magnitude > 0 ? magnitude * 0.05 : 1) : (max - min) * 0.05; auto [min_y, max_y, tick_count] = getNiceAxisNumbers(min - delta, max + delta, 3); if (min_y != y_min_ || max_y != y_max_) { y_min_ = min_y; @@ -412,10 +416,10 @@ void ChartView::handleMouseRelease() { // Prevent zooming/seeking past the end of the route double min = std::clamp(secondsAtPoint(rubber_rect_.Min), can->minSeconds(), can->maxSeconds()); double max = std::clamp(secondsAtPoint(rubber_rect_.Max), can->minSeconds(), can->maxSeconds()); - if (rubber_rect_.GetWidth() <= 0) { - // no rubber dragged, seek to mouse position + if (rubber_rect_.GetWidth() <= 10) { + // Small movements are still clicks; use the same threshold as drag-to-zoom. can->seekTo(std::clamp(secondsAtPoint(press_pos_), can->minSeconds(), can->maxSeconds())); - } else if (rubber_rect_.GetWidth() > 10 && (max - min) > MIN_ZOOM_SECONDS) { + } else if ((max - min) > MIN_ZOOM_SECONDS) { charts_widget_->zoom_undo_stack_.push(new ZoomCommand({min, max})); } rubber_rect_ = ImRect(); @@ -434,8 +438,8 @@ void ChartView::handleMouseRelease() { void ChartView::takeSignalsFrom(ChartView *source) { for (auto &s : source->sigs_) { + s.color = uniqueColor(s.color); sigs_.push_back(std::move(s)); - sigs_.back().color = uniqueColor(sigs_.back().color, sigs_.back().sig); } source->sigs_.clear(); updateAxisY(); @@ -478,17 +482,17 @@ void ChartView::showTip(double sec) { s.track_pt = *pt; x = std::max(x, xPos(pt->x)); } - std::string name = sigs_.size() > 1 ? s.sig->name + ": " : ""; + std::string name = s.sig->name; std::string min = s.min == std::numeric_limits::max() ? "--" : utils::toString(s.min); std::string max = s.max == std::numeric_limits::lowest() ? "--" : utils::toString(s.max); - text_list.push_back({.has_marker = true, .marker = toImU32(s.color), .name = name, .bold = value, .rest = " (" + min + ", " + max + ")"}); + text_list.push_back({.has_marker = true, .marker = toImU32(s.color), .name = name, .value = value, .min = min, .max = max}); } } if (x < 0) { x = tooltip_x_; } ImVec2 pt(x, layout_.plot_area.Min.y); - text_list.insert(text_list.begin(), TipLine{.name = formatNumber(secondsAtPoint({x, 0}), 2)}); + text_list.insert(text_list.begin(), TipLine{.name = formatNumber(sec, 2) + " s"}); tip_label_.showText(pt, text_list, visible_rect); } @@ -747,6 +751,7 @@ void ChartView::drawTimeline() { } void ChartView::drawSignalValue() { + pushMonoFont(ImGui::GetFontSize()); ImDrawList *painter = ImGui::GetWindowDrawList(); const ImU32 color = ImGui::GetColorU32(ImGuiCol_Text); for (int i = 0; i < sigs_.size() && i < layout_.legend_rects.size(); ++i) { @@ -757,27 +762,35 @@ void ChartView::drawSignalValue() { ImRect value_rect(value_min, value_min + layout_.legend_rects[i].GetSize()); float w = ImGui::CalcTextSize(value.c_str()).x; if (w <= value_rect.GetWidth()) { - painter->AddText(ImVec2(value_rect.GetCenter().x - w / 2, value_rect.Min.y), color, value.c_str()); + painter->AddText(value_rect.Min, color, value.c_str()); } else { addTextEllipsis(painter, ImGui::GetFont(), color, value_rect.Min, value_rect.Max.x, value); } } + popMonoFont(); } CabanaColor ChartView::uniqueColor(CabanaColor color, const cabana::Signal *exclude) const { - for (auto &s : sigs_) { - if (s.sig != exclude && std::abs(color.hsv().hue - s.color.hsv().hue) < 0.1) { - // use different color to distinguish it from others. - auto last_color = sigs_.back().color; - static thread_local std::mt19937 rng{std::random_device{}()}; - std::uniform_int_distribution sat(35, 99); - std::uniform_int_distribution val(85, 99); - color = CabanaColor::fromHsv(std::fmod(last_color.hsv().hue + 60 / 360.0, 1.0), - sat(rng) / 100.0, - val(rng) / 100.0, - color.a / 255.0f); - break; + auto separation = [&](float hue) { + float distance = 1.0f; + for (const auto &s : sigs_) { + if (exclude && s.sig == exclude) continue; + const float delta = std::abs(hue - s.color.hsv().hue); + distance = std::min(distance, std::min(delta, 1.0f - delta)); + } + return distance; + }; + const float original_hue = color.hsv().hue; + if (separation(original_hue) >= 0.1f) return color; + + float best_hue = original_hue, best_distance = -1; + for (int i = 0; i < 36; ++i) { + const float hue = std::fmod(original_hue + i / 36.0f, 1.0f); + const float distance = separation(hue); + if (distance > best_distance) { + best_hue = hue; + best_distance = distance; } } - return color; + return CabanaColor::fromHsv(best_hue, 0.8f, 0.9f, color.a / 255.0f); } diff --git a/openpilot/tools/cabana/ui/chart/chart.h b/openpilot/tools/cabana/ui/chart/chart.h index fc2b63712c..d32906390a 100644 --- a/openpilot/tools/cabana/ui/chart/chart.h +++ b/openpilot/tools/cabana/ui/chart/chart.h @@ -120,7 +120,7 @@ private: double y_min_ = 0; double y_max_ = 1; int y_tick_count_ = 3; - int y_precision_ = 0; + int y_precision_ = 1; std::string y_unit_; // interaction enum class MouseMode { None, Rubber, Scrub }; diff --git a/openpilot/tools/cabana/ui/chart/chartswidget.cc b/openpilot/tools/cabana/ui/chart/chartswidget.cc index d10300cbe8..a5845cefdc 100644 --- a/openpilot/tools/cabana/ui/chart/chartswidget.cc +++ b/openpilot/tools/cabana/ui/chart/chartswidget.cc @@ -206,12 +206,13 @@ void ChartsWidget::drawToolBar() { const std::string range_lb = is_zoomed ? std::string() : utils::formatSeconds(max_chart_range_); std::string reset_zoom_text; if (!is_zoomed) { - items.push_back({ImGui::CalcTextSize(range_lb.c_str()).x, [&range_lb]() { + // the range label and the slider are one unit: drawn inline and moved to the overflow menu together + slider_index = items.size(); + const float label_width = ImGui::CalcTextSize(range_lb.c_str()).x + ImGui::GetStyle().ItemInnerSpacing.x; + items.push_back({label_width + slider_width, [this, &range_lb, &slider_width]() { ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted(range_lb.c_str()); - }}); - slider_index = items.size(); - items.push_back({slider_width, [this, &slider_width]() { + ImGui::SameLine(0.0f, ImGui::GetStyle().ItemInnerSpacing.x); // Restore the slider width in overflow; the toolbar may have shrunk it. const bool in_menu = ImGui::GetCurrentWindow()->Flags & ImGuiWindowFlags_Popup; const float width = in_menu ? std::max(ImGui::GetContentRegionAvail().x, 150.0f) : slider_width; @@ -219,9 +220,17 @@ void ChartsWidget::drawToolBar() { ImGui::SetItemTooltip("Set the chart range"); }}); } else { + const auto &range = *can->timeRange(); char buf[64]; - snprintf(buf, sizeof(buf), "%.2f-%.2f", can->timeRange()->first, can->timeRange()->second); + snprintf(buf, sizeof(buf), "%.2f-%.2f", range.first, range.second); reset_zoom_text = buf; + // The undo/redo/reset buttons form one group. The reset button has a fixed width in the mono font, + // sized for the longest range the stream can show, so its neighbors do not shift as the range changes. + const int digits = std::max({1, (int)std::to_string((long long)can->maxSeconds()).size(), (int)std::to_string((long long)range.second).size()}); + const std::string widest = std::string(digits, '0') + ".00"; + pushMonoFont(ImGui::GetFontSize()); + const float reset_zoom_width = iconTextButtonWidth(icon::ZOOM_OUT, widest + "-" + widest); + popMonoFont(); items.push_back({iconButtonWidth(), [this]() { ImGui::BeginDisabled(!zoom_undo_stack_.canUndo()); if (iconButton("undo_zoom", icon::ARROW_COUNTERCLOCKWISE, "Undo Zoom")) zoom_undo_stack_.undo(); @@ -232,10 +241,15 @@ void ChartsWidget::drawToolBar() { if (iconButton("redo_zoom", icon::ARROW_CLOCKWISE, "Redo Zoom")) zoom_undo_stack_.redo(); ImGui::EndDisabled(); }}); - items.push_back({toolbarButtonWidth(std::string(icon::ZOOM_OUT) + " " + reset_zoom_text), [this, &reset_zoom_text]() { - if (ImGui::Button((std::string(icon::ZOOM_OUT) + " " + reset_zoom_text + "###reset_zoom_btn").c_str())) zoomReset(); + items.back().tight = true; + items.push_back({reset_zoom_width, [this, &reset_zoom_text, reset_zoom_width]() { + pushMonoFont(ImGui::GetFontSize()); + const bool clicked = iconTextButton("reset_zoom_btn", icon::ZOOM_OUT, reset_zoom_text, reset_zoom_width); + popMonoFont(); + if (clicked) zoomReset(); ImGui::SetItemTooltip("Reset Zoom"); }}); + items.back().tight = true; } items.push_back(toolbarAction("remove_all_btn", icon::TRASH, "Remove all charts", [this]() { removeAll(); }, !charts_.empty())); const char *dock_btn_icon = is_docked_ ? icon::BOX_ARROW_UP_RIGHT : icon::BOX_ARROW_IN_DOWN_LEFT; @@ -247,7 +261,7 @@ void ChartsWidget::drawToolBar() { const float shrink = std::min(slider_width - MIN_RANGE_SLIDER_WIDTH, toolbarWidth(items, spacer_index) - ImGui::GetContentRegionAvail().x); if (shrink > 0.0f) { slider_width -= shrink; - items[slider_index].width = slider_width; + items[slider_index].width -= shrink; } } drawToolbar(items, spacer_index); @@ -273,7 +287,8 @@ ChartView *ChartsWidget::createChart(int pos) { ChartView *ptr = chart.get(); pos = std::clamp(pos, 0, (int)charts_.size()); charts_.insert(charts_.begin() + pos, std::move(chart)); - currentCharts().insert(currentCharts().begin() + pos, ptr); + auto ¤t = currentCharts(); + current.insert(current.begin() + std::min(pos, (int)current.size()), ptr); updateLayout(); return ptr; } @@ -291,8 +306,8 @@ void ChartsWidget::showChart(const MessageId &id, const cabana::Signal *sig, boo void ChartsWidget::splitChart(ChartView *src_chart) { if (src_chart->signals().size() > 1) { - auto it = std::find_if(charts_.begin(), charts_.end(), [src_chart](auto &c) { return c.get() == src_chart; }); - const int pos = it - charts_.begin() + 1; + auto ¤t = currentCharts(); + const int pos = std::find(current.begin(), current.end(), src_chart) - current.begin() + 1; for (auto &s : src_chart->takeExtraSignals()) { createChart(pos)->adoptSignal(std::move(s)); } @@ -606,9 +621,8 @@ void ChartsWidget::draw() { } void ChartsContainer::draw() { - ImGuiWindow *window = ImGui::GetCurrentWindow(); const ImVec2 start = ImGui::GetCursorScreenPos(); - const float width_avail = window->InnerRect.GetWidth() - (window->ScrollbarY ? ImGui::GetStyle().ItemInnerSpacing.x : 0.0f); + const float width_avail = ImGui::GetContentRegionAvail().x; geometry_ = ImRect(start, start + ImVec2(width_avail, 0)); charts_widget_->updateLayout(); diff --git a/openpilot/tools/cabana/ui/chart/tiplabel.cc b/openpilot/tools/cabana/ui/chart/tiplabel.cc index 68889b7886..0efc4e7ad6 100644 --- a/openpilot/tools/cabana/ui/chart/tiplabel.cc +++ b/openpilot/tools/cabana/ui/chart/tiplabel.cc @@ -2,35 +2,49 @@ #include "tools/cabana/ui/chart/tiplabel.h" #include -#include +#include #include "tools/cabana/ui/util.h" ImVec2 TipLabel::layoutLines(ImDrawList *p, const ImVec2 &origin, ImU32 fg) const { - ImFont *bold = boldFont(); const float font_size = ImGui::GetFontSize(); - const float line_height = ImGui::GetTextLineHeight(); - ImVec2 size(0, 0); - float y = origin.y; + const float line_height = std::ceil(ImGui::GetTextLineHeight() + 2); + const float marker = std::floor(markerSize()); + const float gap = 8; + const float width = column_widths_[0] + column_widths_[1] + column_widths_[2] + column_widths_[3] + gap * 3; + const ImU32 muted = ImGui::GetColorU32(ImGuiCol_TextDisabled); + float y = std::round(origin.y); + auto draw = [&](float x, const std::string &text, ImU32 color) { + if (p) p->AddText(ImVec2(std::round(x), y), color, text.c_str()); + }; + const char *heading = !text_.empty() && !text_[0].has_marker ? text_[0].name.c_str() : "Signal"; + const char *headers[] = {heading, "Value", "Min", "Max"}; + float x = origin.x; + for (int i = 0; i < 4; ++i) { + draw(i ? x + column_widths_[i] - ImGui::CalcTextSize(headers[i]).x : x, headers[i], muted); + x += column_widths_[i] + gap; + } + y += line_height; + if (p) p->AddLine(ImVec2(origin.x, y - 2), ImVec2(origin.x + width, y - 2), ImGui::GetColorU32(ImGuiCol_Border)); for (const auto &line : text_) { - float x = origin.x; - if (line.has_marker) { - if (p) drawColorMarker(p, ImVec2(x, y), line.marker); - x += markerSize() + 4; + if (!line.has_marker) continue; + if (p) { + const ImVec2 marker_pos(std::round(origin.x), std::round(y + (font_size - marker) * 0.5f)); + p->AddRectFilled(marker_pos, marker_pos + ImVec2(marker, marker), line.marker); } - if (p) p->AddText(ImVec2(x, y), fg, line.name.c_str()); - x += ImGui::CalcTextSize(line.name.c_str()).x; - if (!line.bold.empty()) { - if (p) p->AddText(bold, font_size, ImVec2(x, y), fg, line.bold.c_str()); - x += bold->CalcTextSizeA(font_size, FLT_MAX, 0.0f, line.bold.c_str()).x; + if (p) drawElidedText(p, ImRect(ImVec2(origin.x + marker + 6, y), + ImVec2(origin.x + column_widths_[0], y + font_size)), line.name, fg); + x = origin.x + column_widths_[0] + gap; + pushMonoFont(font_size); + const std::string *values[] = {&line.value, &line.min, &line.max}; + for (int i = 0; i < 3; ++i) { + draw(x + column_widths_[i + 1] - ImGui::CalcTextSize(values[i]->c_str()).x, *values[i], i == 0 ? fg : muted); + x += column_widths_[i + 1] + gap; } - if (p) p->AddText(ImVec2(x, y), fg, line.rest.c_str()); - x += ImGui::CalcTextSize(line.rest.c_str()).x; - size.x = std::max(size.x, x - origin.x); + popMonoFont(); y += line_height; } - size.y = y - origin.y; - return size; + return ImVec2(width, y - origin.y); } ImVec2 TipLabel::sizeHint() const { @@ -38,25 +52,53 @@ ImVec2 TipLabel::sizeHint() const { } void TipLabel::showText(const ImVec2 &pt, const std::vector &text, const ImRect &rect) { + bool same_signals = text.size() == text_.size(); + for (size_t i = 1; same_signals && i < text.size(); ++i) same_signals = text[i].name == text_[i].name; + if (!same_signals) column_widths_ = {}; text_ = text; + anchor_ = pt; + area_ = rect; + visible_ = !text_.empty(); +} + +void TipLabel::updateLayout() { + // Playback notifications can update the text outside an ImGui window scope. + // Measure and position it only while the owning chart is being drawn. + column_widths_[0] = std::max(column_widths_[0], ImGui::CalcTextSize(text_.empty() ? "Signal" : text_[0].name.c_str()).x); + for (const auto &line : text_) { + if (line.has_marker) column_widths_[0] = std::max(column_widths_[0], std::ceil(markerSize() + 6 + ImGui::CalcTextSize(line.name.c_str()).x)); + } + pushMonoFont(ImGui::GetFontSize()); + const float number_width = std::ceil(ImGui::CalcTextSize("-0.00000").x); + for (int i = 1; i < 4; ++i) column_widths_[i] = std::max(column_widths_[i], number_width); + for (const auto &line : text_) { + const std::string *values[] = {&line.value, &line.min, &line.max}; + for (int i = 0; i < 3; ++i) column_widths_[i + 1] = std::max(column_widths_[i + 1], std::ceil(ImGui::CalcTextSize(values[i]->c_str()).x)); + } + popMonoFont(); + const ImGuiViewport *viewport = ImGui::GetWindowViewport(); + const ImRect bounds(viewport->WorkPos, viewport->WorkPos + viewport->WorkSize); + const float numeric_width = column_widths_[1] + column_widths_[2] + column_widths_[3] + 24 + MARGIN * 2 + 1; + column_widths_[0] = std::min(column_widths_[0], std::max(40.0f, std::min(ImGui::GetFontSize() * 16, bounds.GetWidth() - numeric_width))); if (!text_.empty()) { ImVec2 extra(1, 1); size_ = sizeHint() + extra; - ImVec2 tip_pos(pt.x + 8, rect.Min.y + 2); - if (tip_pos.x + size_.x >= rect.Max.x) { - tip_pos.x = pt.x - size_.x - 8; - } - if (rect.Contains(ImRect(tip_pos, tip_pos + size_))) { - pos_ = tip_pos; - visible_ = true; - return; + ImVec2 tip_pos(anchor_.x + 8, area_.Min.y + 2); + if (anchor_.x >= area_.GetCenter().x) { + tip_pos.x = anchor_.x - size_.x - 8; } + tip_pos.x = std::clamp(tip_pos.x, bounds.Min.x, std::max(bounds.Min.x, bounds.Max.x - size_.x)); + tip_pos.y = std::clamp(tip_pos.y, bounds.Min.y, std::max(bounds.Min.y, bounds.Max.y - size_.y)); + pos_ = tip_pos; + visible_ = true; + return; } visible_ = false; } void TipLabel::draw() { if (!visible_) return; + updateLayout(); ImDrawList *p = ImGui::GetForegroundDrawList(); // filled panel with a 1px frame diff --git a/openpilot/tools/cabana/ui/chart/tiplabel.h b/openpilot/tools/cabana/ui/chart/tiplabel.h index 6aa80538e8..fe7b099d37 100644 --- a/openpilot/tools/cabana/ui/chart/tiplabel.h +++ b/openpilot/tools/cabana/ui/chart/tiplabel.h @@ -1,18 +1,18 @@ #pragma once +#include #include #include #include "imgui.h" #include "imgui_internal.h" -// one line of the tip: [square] name value (min, max) +// A signal row, or a time heading when has_marker is false. struct TipLine { bool has_marker = false; ImU32 marker = 0; std::string name; - std::string bold; - std::string rest; + std::string value, min, max; }; class TipLabel { @@ -26,9 +26,13 @@ private: // lays the lines out from origin, drawing them when p is given; returns the size of the text block ImVec2 layoutLines(ImDrawList *p, const ImVec2 &origin, ImU32 fg) const; ImVec2 sizeHint() const; + void updateLayout(); - static constexpr float MARGIN = 2.0f; // 1 + PM_ToolTipLabelFrameWidth + static constexpr float MARGIN = 6.0f; std::vector text_; + std::array column_widths_{}; + ImVec2 anchor_; + ImRect area_; ImVec2 pos_; ImVec2 size_; bool visible_ = false; diff --git a/openpilot/tools/cabana/ui/util.cc b/openpilot/tools/cabana/ui/util.cc index 6269bd11c3..30b6dc2c76 100644 --- a/openpilot/tools/cabana/ui/util.cc +++ b/openpilot/tools/cabana/ui/util.cc @@ -214,6 +214,26 @@ bool iconButton(const char *id, const char *icon, const char *tooltip) { return clicked; } +float iconTextButtonWidth(const char *icon, const std::string &text) { + const ImGuiStyle &style = ImGui::GetStyle(); + return ImGui::CalcTextSize(icon).x + style.ItemInnerSpacing.x + ImGui::CalcTextSize(text.c_str(), nullptr, true).x + style.FramePadding.x * 2; +} + +bool iconTextButton(const char *id, const char *icon, const std::string &text, float width) { + const ImGuiStyle &style = ImGui::GetStyle(); + if (width <= 0.0f) width = iconTextButtonWidth(icon, text); + const bool clicked = ImGui::Button((std::string("###") + id).c_str(), ImVec2(width, 0.0f)); + const ImVec2 min = ImGui::GetItemRectMin(), max = ImGui::GetItemRectMax(); + const ImU32 color = ImGui::GetColorU32(ImGuiCol_Text); + auto *draw_list = ImGui::GetWindowDrawList(); + draw_list->AddText(ImVec2(min.x + style.FramePadding.x, min.y + style.FramePadding.y), color, icon); + // the text is centered between the icon and the right padding + const float left = min.x + style.FramePadding.x + ImGui::CalcTextSize(icon).x + style.ItemInnerSpacing.x; + const float slack = max.x - style.FramePadding.x - left - ImGui::CalcTextSize(text.c_str(), nullptr, true).x; + draw_list->AddText(ImVec2(left + std::max(0.0f, slack * 0.5f), min.y + style.FramePadding.y), color, text.c_str()); + return clicked; +} + void disabledItemTooltip(const char *text) { if (ImGui::IsItemHovered(ImGuiHoveredFlags_ForTooltip | ImGuiHoveredFlags_AllowWhenDisabled)) ImGui::SetTooltip("%s", text); } diff --git a/openpilot/tools/cabana/ui/util.h b/openpilot/tools/cabana/ui/util.h index 98c814b6c4..576f197e88 100644 --- a/openpilot/tools/cabana/ui/util.h +++ b/openpilot/tools/cabana/ui/util.h @@ -74,6 +74,8 @@ int nonWhitespaceValidator(ImGuiInputTextCallbackData *data); // Use ItemInnerSpacing between related buttons and ItemSpacing between groups. bool iconButton(const char *id, const char *icon, const char *tooltip = nullptr); float iconButtonWidth(); +bool iconTextButton(const char *id, const char *icon, const std::string &text, float width = 0.0f); +float iconTextButtonWidth(const char *icon, const std::string &text); // tooltip for the last item that also shows while the item is disabled void disabledItemTooltip(const char *text);