From dc338c2831b8066412e5dd0ed452b2ff12b81846 Mon Sep 17 00:00:00 2001 From: Trey Moen <50057480+greatgitsby@users.noreply.github.com> Date: Sat, 12 Sep 2026 20:37:01 -0700 Subject: [PATCH] cabana: standardize button spacing (#38873) * cabana: standardize button spacing and control alignment * cabana: center shared step controls and inset chart content * cabana: use uniform per-chart content insets --- openpilot/tools/cabana/ui/chart/chart.cc | 48 +++++----- openpilot/tools/cabana/ui/chart/chart.h | 1 + .../tools/cabana/ui/chart/chartswidget.cc | 25 +++--- .../tools/cabana/ui/chart/signalselector.cc | 2 + .../tools/cabana/ui/dialogs/filedialog.cc | 6 +- .../tools/cabana/ui/dialogs/settingsdialog.cc | 20 +---- .../tools/cabana/ui/dialogs/streamselector.cc | 11 +-- openpilot/tools/cabana/ui/mainwin.cc | 4 +- openpilot/tools/cabana/ui/theme.cc | 7 +- openpilot/tools/cabana/ui/theme.h | 8 ++ openpilot/tools/cabana/ui/tools/findsignal.cc | 33 ++++--- .../tools/cabana/ui/tools/findsimilarbits.cc | 20 +++-- openpilot/tools/cabana/ui/util.cc | 87 ++++++++++++++----- openpilot/tools/cabana/ui/util.h | 12 ++- .../tools/cabana/ui/widgets/detailwidget.cc | 9 +- .../cabana/ui/widgets/scrollabletabbar.cc | 4 +- .../tools/cabana/ui/widgets/signalview.cc | 21 +++-- .../tools/cabana/ui/widgets/signalview.h | 1 + .../tools/cabana/ui/widgets/videowidget.cc | 32 +++---- 19 files changed, 205 insertions(+), 146 deletions(-) diff --git a/openpilot/tools/cabana/ui/chart/chart.cc b/openpilot/tools/cabana/ui/chart/chart.cc index 8dc5d627d0..41256845c1 100644 --- a/openpilot/tools/cabana/ui/chart/chart.cc +++ b/openpilot/tools/cabana/ui/chart/chart.cc @@ -15,12 +15,9 @@ #include "tools/cabana/ui/util.h" #include "tools/cabana/utils/strings.h" -const int AXIS_X_TOP_MARGIN = 4; const int X_TICK_COUNT = 5; const double MIN_ZOOM_SECONDS = 0.01; // 10ms const double EPSILON = 1e-6; -constexpr ImVec4 LAYOUT_MARGINS{0, 6, 0, 6}; // left, top, right, bottom -constexpr int LEGEND_SPACING = 5; static inline bool xLessThan(const ImPlotPoint &p, double x) { return p.x < (x - EPSILON); } static inline bool isNull(const ImPlotPoint &p) { return p.x == 0 && p.y == 0; } @@ -135,27 +132,32 @@ void ChartView::manageSignals() { void ChartView::updateLayout() { const ImVec2 grip = ImGui::CalcTextSize(icon::GRIP_HORIZONTAL); - const ImVec2 top_left = layout_.rect.Min + ImVec2(LAYOUT_MARGINS.x, LAYOUT_MARGINS.y); + const ImGuiStyle &style = ImGui::GetStyle(); + // WindowPadding can be zero in a borderless pane or drag preview. Chart + // content always uses the shared control gap, independently of its parent. + layout_.content_rect = layout_.rect; + layout_.content_rect.Expand(-style.ItemSpacing.x); + const ImVec2 top_left = layout_.content_rect.Min; layout_.move_icon_rect = ImRect(top_left, top_left + grip); const ImVec2 btn_size(iconButtonWidth(), iconButtonWidth()); - const ImVec2 close_min(layout_.rect.Max.x - ImGui::GetStyle().WindowPadding.x - btn_size.x, top_left.y); + const ImVec2 close_min(layout_.content_rect.Max.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); + const ImVec2 manage_min(close_min.x - btn_size.x - ImGui::GetStyle().ItemSpacing.x, top_left.y); layout_.manage_btn_rect = ImRect(manage_min, manage_min + btn_size); ImFont *bold = boldFont(); const float font_size = ImGui::GetFontSize(); const float fm_height = ImGui::GetTextLineHeight(); 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 - ImGui::GetStyle().ItemInnerSpacing.x, legend_left + 10); + const int row_height = std::max(marker_size, fm_height) + fm_height + style.ItemInnerSpacing.y; // + the signal value line + const int legend_left = layout_.move_icon_rect.Max.x + style.ItemSpacing.x; + const int legend_right = std::max(layout_.manage_btn_rect.Min.x - ImGui::GetStyle().ItemSpacing.x, legend_left + 10); // layout legend entries left-to-right, wrapping between the move icon and the buttons layout_.legend_rects.clear(); int x = legend_left, y = top_left.y; for (auto &s : sigs_) { - int w = marker_size + LEGEND_SPACING + bold->CalcTextSizeA(font_size, FLT_MAX, 0.0f, s.sig->name.c_str()).x + + int w = marker_size + style.ItemInnerSpacing.x + 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)); @@ -166,13 +168,11 @@ void ChartView::updateLayout() { y += row_height; } layout_.legend_rects.emplace_back(ImVec2(x, y), ImVec2(x + w, y + std::max(marker_size, fm_height))); - x += w + 12; + x += w + style.ItemSpacing.x; } // add top space for the legend and signal values - int adjust_top = (y + row_height) - top_left.y; - adjust_top = std::max(adjust_top, layout_.manage_btn_rect.Max.y - layout_.rect.Min.y + LAYOUT_MARGINS.y); - layout_.header_bottom = layout_.rect.Min.y + adjust_top + LAYOUT_MARGINS.y; + layout_.header_bottom = std::max(y + row_height, layout_.manage_btn_rect.Max.y) + ImGui::GetStyle().ItemSpacing.y; } void ChartView::updatePlot(double cur, double min, double max) { @@ -554,9 +554,9 @@ void ChartView::drawStaticLayer() { } void ChartView::drawAxes() { - ImGui::SetCursorScreenPos(ImVec2(layout_.rect.Min.x, layout_.header_bottom)); - const float plot_h = std::max(layout_.rect.Max.y - layout_.header_bottom - LAYOUT_MARGINS.w, 10.0f); - ImPlot::PushStyleVar(ImPlotStyleVar_PlotPadding, ImVec2(LAYOUT_MARGINS.x, AXIS_X_TOP_MARGIN)); + ImGui::SetCursorScreenPos(ImVec2(layout_.content_rect.Min.x, layout_.header_bottom)); + const float plot_h = std::max(layout_.content_rect.Max.y - layout_.header_bottom, 10.0f); + ImPlot::PushStyleVar(ImPlotStyleVar_PlotPadding, ImVec2(0.0f, ImGui::GetStyle().ItemInnerSpacing.y)); ImPlot::PushStyleColor(ImPlotCol_PlotBg, ImVec4(0, 0, 0, 0)); ImPlot::PushStyleColor(ImPlotCol_FrameBg, ImVec4(0, 0, 0, 0)); ImPlot::PushStyleColor(ImPlotCol_PlotBorder, palette().grid); @@ -568,8 +568,8 @@ void ChartView::drawAxes() { ImPlotFlags_NoBoxSelect | ImPlotFlags_NoInputs | ImPlotFlags_NoFrame; const ImPlotAxisFlags axis_flags = ImPlotAxisFlags_NoMenus | ImPlotAxisFlags_NoHighlight | ImPlotAxisFlags_NoSideSwitch | ImPlotAxisFlags_Lock; // reserve room for the right half of the last x tick label - const float x_label_width = ImGui::CalcTextSize(formatNumber(x_max_, xAxisPrecision()).c_str()).x + 5; - if (ImPlot::BeginPlot("##plot", ImVec2(layout_.rect.GetWidth() - x_label_width / 2, plot_h), flags)) { + const float x_label_width = ImGui::CalcTextSize(formatNumber(x_max_, xAxisPrecision()).c_str()).x + ImGui::GetStyle().ItemInnerSpacing.x; + if (ImPlot::BeginPlot("##plot", ImVec2(layout_.content_rect.GetWidth() - x_label_width / 2, plot_h), flags)) { ImPlot::SetupAxis(ImAxis_X1, nullptr, axis_flags); ImPlot::SetupAxis(ImAxis_Y1, y_unit_.empty() ? nullptr : y_unit_.c_str(), axis_flags); ImPlot::SetupAxisLimits(ImAxis_X1, x_min_, x_max_, ImPlotCond_Always); @@ -624,7 +624,7 @@ void ChartView::drawLegend() { drawColorMarker(painter, r.Min, toImU32(s.color)); } - float x = r.Min.x + marker_size + LEGEND_SPACING; + float x = r.Min.x + marker_size + ImGui::GetStyle().ItemInnerSpacing.x; const float text_y = r.GetCenter().y - font_size / 2.0f; addTextEllipsis(painter, bold, title_color, ImVec2(x, text_y), r.Max.x, s.sig->name); float name_w = std::min(bold->CalcTextSizeA(font_size, FLT_MAX, 0.0f, s.sig->name.c_str()).x, r.Max.x - x); @@ -633,7 +633,7 @@ void ChartView::drawLegend() { addTextEllipsis(painter, normal, msg_color, ImVec2(x, text_y), r.Max.x, msg); if (!s.visible) { // strike out const float y = r.GetCenter().y; - painter->AddLine(ImVec2(r.Min.x + marker_size + LEGEND_SPACING, y), ImVec2(std::min(x + ImGui::CalcTextSize(msg.c_str()).x, r.Max.x), y), title_color); + painter->AddLine(ImVec2(r.Min.x + marker_size + ImGui::GetStyle().ItemInnerSpacing.x, y), ImVec2(std::min(x + ImGui::CalcTextSize(msg.c_str()).x, r.Max.x), y), title_color); } } } @@ -730,10 +730,10 @@ void ChartView::drawRubberBandTimeRange() { painter->PushClipRect(layout_.rect.Min, layout_.rect.Max); for (const auto &pt : {rubber_rect_.GetBL(), rubber_rect_.GetBR()}) { std::string sec = formatNumber(secondsAtPoint(pt), 2); - ImVec2 size = ImGui::CalcTextSize(sec.c_str()) + ImVec2(12, AXIS_X_TOP_MARGIN * 2); + ImVec2 size = ImGui::CalcTextSize(sec.c_str()) + ImVec2(12, ImGui::GetStyle().ItemInnerSpacing.y * 2); ImVec2 top_left = pt.x == rubber_rect_.Min.x ? ImVec2(pt.x - size.x, pt.y + 2) : ImVec2(pt.x, pt.y + 2); painter->AddRectFilled(top_left, top_left + size, badge, ImGui::GetStyle().FrameRounding); - painter->AddText(top_left + ImVec2(6, AXIS_X_TOP_MARGIN), white, sec.c_str()); + painter->AddText(top_left + ImVec2(6, ImGui::GetStyle().ItemInnerSpacing.y), white, sec.c_str()); } painter->PopClipRect(); } @@ -745,7 +745,7 @@ void ChartView::drawTimeline() { std::string time_str = formatNumber(cur_sec_, 2); ImVec2 time_str_size = ImGui::CalcTextSize(time_str.c_str()) + ImVec2(8, 2); - ImVec2 time_str_pos(x - time_str_size.x / 2.0f, layout_.plot_area.Max.y + AXIS_X_TOP_MARGIN); + ImVec2 time_str_pos(x - time_str_size.x / 2.0f, layout_.plot_area.Max.y + ImGui::GetStyle().ItemInnerSpacing.y); painter->AddRectFilled(time_str_pos, time_str_pos + time_str_size, ImGui::GetColorU32(palette().badge), ImGui::GetStyle().FrameRounding); painter->AddText(time_str_pos + ImVec2(4, 1), IM_COL32_WHITE, time_str.c_str()); } diff --git a/openpilot/tools/cabana/ui/chart/chart.h b/openpilot/tools/cabana/ui/chart/chart.h index d32906390a..7e7152f6df 100644 --- a/openpilot/tools/cabana/ui/chart/chart.h +++ b/openpilot/tools/cabana/ui/chart/chart.h @@ -106,6 +106,7 @@ private: // layout struct Layout { ImRect rect; // the whole chart widget, screen coordinates + ImRect content_rect; // the same inset on all four sides, including during a drag ImRect plot_area; ImRect move_icon_rect; ImRect close_btn_rect; diff --git a/openpilot/tools/cabana/ui/chart/chartswidget.cc b/openpilot/tools/cabana/ui/chart/chartswidget.cc index 24ce32aa25..94a5fa81b2 100644 --- a/openpilot/tools/cabana/ui/chart/chartswidget.cc +++ b/openpilot/tools/cabana/ui/chart/chartswidget.cc @@ -15,7 +15,6 @@ #include "tools/cabana/utils/strings.h" const int MAX_COLUMN_COUNT = 4; -const int CHART_SPACING = 4; const int START_DRAG_DISTANCE = 10; const float MIN_RANGE_SLIDER_WIDTH = 40.0f; @@ -166,12 +165,11 @@ void ChartsWidget::drawToolBar() { // the labels are captured by reference, they outlive the draw calls below std::vector items; items.push_back({iconButtonWidth(), [this]() { - if (iconButton("new_plot_btn", icon::PLUS_LG, "New Chart")) newChart(); + if (stepButton("new_plot_btn", true, "New Chart")) newChart(); }}); items.push_back({iconButtonWidth(), [this]() { if (iconButton("new_tab_btn", icon::WINDOW_PLUS, "New Tab")) newTab(); }}); - items.back().tight = true; const std::string title_label = "Charts: " + std::to_string(charts_.size()); items.push_back({ImGui::CalcTextSize(title_label.c_str()).x, [&title_label]() { ImGui::AlignTextToFramePadding(); @@ -231,11 +229,11 @@ void ChartsWidget::drawToolBar() { pushMonoFont(ImGui::GetFontSize()); const float reset_zoom_width = iconTextButtonWidth(icon::ZOOM_OUT, widest + "-" + widest); popMonoFont(); - items.push_back({iconButtonWidth() * 2 + ImGui::GetStyle().ItemInnerSpacing.x, [this]() { + items.push_back({iconButtonWidth() * 2 + ImGui::GetStyle().ItemSpacing.x, [this]() { ImGui::BeginDisabled(!zoom_undo_stack_.canUndo()); if (iconButton("undo_zoom", icon::ARROW_COUNTERCLOCKWISE, "Undo Zoom")) zoom_undo_stack_.undo(); ImGui::EndDisabled(); - ImGui::SameLine(0.0f, ImGui::GetStyle().ItemInnerSpacing.x); + ImGui::SameLine(); ImGui::BeginDisabled(!zoom_undo_stack_.canRedo()); if (iconButton("redo_zoom", icon::ARROW_CLOCKWISE, "Redo Zoom")) zoom_undo_stack_.redo(); ImGui::EndDisabled(); @@ -247,12 +245,11 @@ void ChartsWidget::drawToolBar() { 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; const char *dock_label = is_docked_ ? "Float the charts window" : "Dock the charts window"; - items.push_back(toolbarAction("dock_btn", dock_btn_icon, dock_label, [this]() { toggleChartsDocking(); }, true, true)); + items.push_back(toolbarAction("dock_btn", dock_btn_icon, dock_label, [this]() { toggleChartsDocking(); })); // the slider shrinks first, the buttons stay pinned to the right edge if (slider_index != (size_t)-1) { @@ -356,7 +353,7 @@ void ChartsWidget::updateLayout() { int n = MAX_COLUMN_COUNT; for (; n > 1; --n) { - if ((n * CHART_MIN_WIDTH + (n - 1) * CHART_SPACING) < container_width) break; + if ((n * CHART_MIN_WIDTH + (n - 1) * ImGui::GetStyle().ItemSpacing.x) < container_width) break; } columns_action_visible_ = n > 1; @@ -625,14 +622,14 @@ void ChartsContainer::draw() { charts_widget_->updateLayout(); const int n = std::max(charts_widget_->current_column_count_, 1); - const float spacing = CHART_SPACING; + const float spacing = ImGui::GetStyle().ItemSpacing.x; const float width = (geometry_.GetWidth() - (n - 1) * spacing) / n; - const ImVec2 origin = ImGui::GetCursorScreenPos() + ImVec2(0, CHART_SPACING); + const ImVec2 origin = ImGui::GetCursorScreenPos(); auto current_charts = charts_widget_->currentCharts(); // copy: drawing may remove charts float bottom = origin.y; const bool aligned = ImPlot::BeginAlignedPlots("charts_align", true); for (int i = 0; i < current_charts.size(); ++i) { - ImVec2 pos = origin + ImVec2((i % n) * (width + spacing), (i / n) * (settings.chart_height + spacing)); + ImVec2 pos = origin + ImVec2((i % n) * (width + spacing), (i / n) * (settings.chart_height + ImGui::GetStyle().ItemSpacing.y)); ImGui::SetCursorScreenPos(pos); current_charts[i]->draw(width); bottom = std::max(bottom, pos.y + settings.chart_height); @@ -640,15 +637,15 @@ void ChartsContainer::draw() { } if (aligned) ImPlot::EndAlignedPlots(); ImGui::SetCursorScreenPos(ImVec2(origin.x, bottom)); - ImGui::Dummy(ImVec2(geometry_.GetWidth(), CHART_SPACING)); - geometry_.Max.y = bottom + CHART_SPACING; + ImGui::Dummy(ImVec2(geometry_.GetWidth(), ImGui::GetStyle().ItemSpacing.y)); + geometry_.Max.y = bottom + ImGui::GetStyle().ItemSpacing.y; drawDropIndicator(); } void ChartsContainer::drawDropIndicator() { if (!(drop_indicator_pos_.x == 0 && drop_indicator_pos_.y == 0) && !childAt(drop_indicator_pos_)) { ImRect r = geometry_; - r.Max.y = r.Min.y + CHART_SPACING; + r.Max.y = r.Min.y + ImGui::GetStyle().ItemSpacing.y; if (auto insert_after = getDropAfter(drop_indicator_pos_)) { float h = r.GetHeight(); r.Min.y = insert_after->rect().Max.y; diff --git a/openpilot/tools/cabana/ui/chart/signalselector.cc b/openpilot/tools/cabana/ui/chart/signalselector.cc index 245603d035..1f29a99ac7 100644 --- a/openpilot/tools/cabana/ui/chart/signalselector.cc +++ b/openpilot/tools/cabana/ui/chart/signalselector.cc @@ -41,6 +41,7 @@ bool SignalSelector::draw() { const float lists_h = ImGui::GetContentRegionAvail().y - ImGui::GetFrameHeightWithSpacing() * 3; ImGui::BeginGroup(); + ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted("Available Signals"); // a combo popup with a filter box const char *preview = msgs_combo_index_ >= 0 ? msgs_combo_[msgs_combo_index_].text.c_str() : "Select a message..."; @@ -79,6 +80,7 @@ bool SignalSelector::draw() { ImGui::SameLine(); ImGui::BeginGroup(); + ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted("Selected Signals"); bool remove_dbl = false; drawList("##selected_list", selected_list_, &selected_row_, true, &remove_dbl, ImVec2(column_w, lists_h + ImGui::GetFrameHeightWithSpacing())); diff --git a/openpilot/tools/cabana/ui/dialogs/filedialog.cc b/openpilot/tools/cabana/ui/dialogs/filedialog.cc index 7ac420e499..c03dcad205 100644 --- a/openpilot/tools/cabana/ui/dialogs/filedialog.cc +++ b/openpilot/tools/cabana/ui/dialogs/filedialog.cc @@ -188,10 +188,12 @@ void draw() { if (!pending_dir.empty()) setDir(pending_dir); if (s.mode != Mode::Directory) { - ImGui::SetNextItemWidth(-90.0f); + const std::string filter = s.extension.empty() ? "*" : "*" + s.extension; + ImGui::SetNextItemWidth(-(ImGui::CalcTextSize(filter.c_str()).x + ImGui::GetStyle().ItemSpacing.x)); if (inputText("##name", &s.filename, "File name", ImGuiInputTextFlags_EnterReturnsTrue)) ok = true; ImGui::SameLine(); - ImGui::TextDisabled("%s", s.extension.empty() ? "*" : ("*" + s.extension).c_str()); + ImGui::AlignTextToFramePadding(); + ImGui::TextDisabled("%s", filter.c_str()); } const char *accept_label = s.mode == Mode::SaveFile ? "Save" : (s.mode == Mode::Directory ? "Choose" : "Open"); dialogButtons(accept_label, &ok, &cancel); diff --git a/openpilot/tools/cabana/ui/dialogs/settingsdialog.cc b/openpilot/tools/cabana/ui/dialogs/settingsdialog.cc index f67e2f2ad8..96f10cff1a 100644 --- a/openpilot/tools/cabana/ui/dialogs/settingsdialog.cc +++ b/openpilot/tools/cabana/ui/dialogs/settingsdialog.cc @@ -23,7 +23,7 @@ const char *FORM_LABELS[FORM_LABEL_COUNT] = {"Color Theme", "Max Cached Minutes" float formLabelWidth() { float w = 0.0f; for (const char *label : FORM_LABELS) w = std::max(w, ImGui::CalcTextSize(label).x); - return w + ImGui::GetStyle().ItemSpacing.x * 2; // horizontal spacing between label and field + return ImGui::GetCursorPosX() + w + ImGui::GetStyle().ItemSpacing.x; } void formRow(FormLabel label, float label_width) { @@ -34,22 +34,8 @@ void formRow(FormLabel label, float label_width) { } void settingInputInt(const char *id, int *value, int step, int step_fast, int minimum, int maximum) { - const float spacing = ImGui::GetStyle().ItemInnerSpacing.x; - const float width = ImGui::CalcItemWidth(); - ImGui::PushID(id); - ImGui::BeginGroup(); - ImGui::SetNextItemWidth(width - 2 * (iconButtonWidth() + spacing)); - ImGui::InputInt("##value", value, 0); + inputInt((std::string("##") + id).c_str(), value, step, step_fast); *value = std::clamp(*value, minimum, maximum); - const int increment = ImGui::GetIO().KeyCtrl ? step_fast : step; - ImGui::PushItemFlag(ImGuiItemFlags_ButtonRepeat, true); - ImGui::SameLine(0.0f, spacing); - if (iconButton("decrement", icon::DASH_LG)) *value = std::max(minimum, *value - increment); - ImGui::SameLine(0.0f, spacing); - if (iconButton("increment", icon::PLUS_LG)) *value = std::min(maximum, *value + increment); - ImGui::PopItemFlag(); - ImGui::EndGroup(); - ImGui::PopID(); } } // namespace @@ -89,7 +75,7 @@ void SettingsDialog::draw() { checkBox("Enable live stream logging", &log_livestream_); ImGui::BeginDisabled(!log_livestream_); - ImGui::SetNextItemWidth(-90.0f); + ImGui::SetNextItemWidth(-(toolbarButtonWidth("Browse...") + ImGui::GetStyle().ItemSpacing.x)); inputText("##log_path", &log_path_, "", ImGuiInputTextFlags_ReadOnly); ImGui::SameLine(); if (ImGui::Button("Browse...")) { diff --git a/openpilot/tools/cabana/ui/dialogs/streamselector.cc b/openpilot/tools/cabana/ui/dialogs/streamselector.cc index f1d2b1da13..17cae9635c 100644 --- a/openpilot/tools/cabana/ui/dialogs/streamselector.cc +++ b/openpilot/tools/cabana/ui/dialogs/streamselector.cc @@ -18,7 +18,8 @@ void OpenReplayWidget::draw() { ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted("Route"); ImGui::SameLine(); - ImGui::SetNextItemWidth(-250.0f); + ImGui::SetNextItemWidth(-(toolbarButtonWidth("Remote Route...") + toolbarButtonWidth("Local Route...") + + ImGui::GetStyle().ItemSpacing.x * 2)); inputText("##route", &route_, "Enter a route name or browse for a local or remote route"); ImGui::SameLine(); if (ImGui::Button("Remote Route...")) { @@ -134,7 +135,7 @@ void OpenPandaWidget::draw() { ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted("Serial"); ImGui::SameLine(); - ImGui::SetNextItemWidth(-100.0f); + ImGui::SetNextItemWidth(-(toolbarButtonWidth("Refresh") + ImGui::GetStyle().ItemSpacing.x)); if (comboBox("##serial", &serial_index_, serials_)) buildConfigForm(); ImGui::SameLine(); if (ImGui::Button("Refresh")) { @@ -228,10 +229,10 @@ void OpenSocketCanWidget::draw() { ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted("Device"); ImGui::SameLine(); - ImGui::SetNextItemWidth(300.0f); + ImGui::SetNextItemWidth(-(toolbarButtonWidth("Refresh") + ImGui::GetStyle().ItemSpacing.x)); if (comboBox("##device", &device_index_, devices_)) config.device = devices_[device_index_]; ImGui::SameLine(); - if (ImGui::Button("Refresh", ImVec2(100.0f, 0.0f))) refreshDevices(); + if (ImGui::Button("Refresh")) refreshDevices(); } std::unique_ptr OpenSocketCanWidget::open() { @@ -289,7 +290,7 @@ void StreamSelector::draw() { ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted("DBC File"); ImGui::SameLine(); - ImGui::SetNextItemWidth(-90.0f); + ImGui::SetNextItemWidth(-(toolbarButtonWidth("Browse...") + ImGui::GetStyle().ItemSpacing.x)); inputText("##dbc", &dbc_file_, "Choose a DBC file to open", ImGuiInputTextFlags_ReadOnly); ImGui::SameLine(); if (ImGui::Button("Browse...")) { diff --git a/openpilot/tools/cabana/ui/mainwin.cc b/openpilot/tools/cabana/ui/mainwin.cc index 3f8c815e77..343494a03c 100644 --- a/openpilot/tools/cabana/ui/mainwin.cc +++ b/openpilot/tools/cabana/ui/mainwin.cc @@ -904,16 +904,18 @@ void MainWindow::drawVideoPanel() { } } // Replay uses a splitter for the gap; live streams use normal item spacing. - if (!charts_floating_ && !live) ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(ImGui::GetStyle().ItemSpacing.x, 0.0f)); if (video_h > 0.0f) { ImGui::BeginChild("video", ImVec2(0, video_h), ImGuiChildFlags_Borders); help_overlay_.add(video_widget_->whatsThis(), ImGui::GetCurrentWindow()->Rect()); video_widget_->draw(); ImGui::EndChild(); + // The splitter supplies the pane gap; keep normal spacing inside the video child. + if (!charts_floating_ && !live) ImGui::SetCursorPosY(ImGui::GetCursorPosY() - ImGui::GetStyle().ItemSpacing.y); } else { video_widget_->setVisible(false); // the splitter collapsed the video: stop the vipc thread } if (!charts_floating_ && !live) { + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(ImGui::GetStyle().ItemSpacing.x, 0.0f)); ImGui::InvisibleButton("##splitter", ImVec2(-1.0f, splitter_h)); const bool splitter_hovered = ImGui::IsItemHovered() && !live, splitter_active = ImGui::IsItemActive() && !live; if (splitter_active) { diff --git a/openpilot/tools/cabana/ui/theme.cc b/openpilot/tools/cabana/ui/theme.cc index 9a2a6741bc..696a933b82 100644 --- a/openpilot/tools/cabana/ui/theme.cc +++ b/openpilot/tools/cabana/ui/theme.cc @@ -103,9 +103,10 @@ void applyTheme(int theme) { style.WindowBorderSize = 1.0f; style.FrameBorderSize = 1.0f; style.TabBorderSize = 1.0f; - style.WindowPadding = ImVec2(12.0f, 10.0f); - style.FramePadding = ImVec2(9.0f, 5.0f); - style.ItemSpacing = ImVec2(10.0f, 8.0f); + style.WindowPadding = ImVec2(spacing::CONTROL, spacing::CONTROL); + style.FramePadding = ImVec2(spacing::CONTROL, spacing::INNER); + style.ItemSpacing = ImVec2(spacing::CONTROL, spacing::CONTROL); + style.ItemInnerSpacing = ImVec2(spacing::INNER, spacing::INNER); style.CellPadding = ImVec2(6.0f, 4.0f); style.ScrollbarSize = 14.0f; style.GrabMinSize = 13.0f; diff --git a/openpilot/tools/cabana/ui/theme.h b/openpilot/tools/cabana/ui/theme.h index dbd0d97814..020b137c62 100644 --- a/openpilot/tools/cabana/ui/theme.h +++ b/openpilot/tools/cabana/ui/theme.h @@ -32,6 +32,14 @@ inline ImU32 toImU32(const CabanaColor &c) { return IM_COL32(c.r, c.g, c.b, c.a) inline ImVec4 toImVec4(const CabanaColor &c) { return ImVec4(c.r / 255.0f, c.g / 255.0f, c.b / 255.0f, c.a / 255.0f); } inline ImU32 withAlpha(ImU32 c, int alpha) { return (c & ~IM_COL32_A_MASK) | ((ImU32)alpha << IM_COL32_A_SHIFT); } +// Logical pixels. External control gaps are equal on both axes; inner spacing +// is reserved for parts of one control (icon/label, checkbox/label, dropdown arrow). +namespace spacing { +constexpr float CONTROL = 8.0f; +constexpr float INNER = 4.0f; +constexpr float DIALOG_BUTTON_MIN_WIDTH = 80.0f; +} // namespace spacing + constexpr float UI_FONT_SIZE = 16.0f; void loadFonts(); diff --git a/openpilot/tools/cabana/ui/tools/findsignal.cc b/openpilot/tools/cabana/ui/tools/findsignal.cc index c3ce025f4c..734107aa9f 100644 --- a/openpilot/tools/cabana/ui/tools/findsignal.cc +++ b/openpilot/tools/cabana/ui/tools/findsignal.cc @@ -76,12 +76,15 @@ bool FindSignalDlg::draw() { } searching_ = search_future_.valid(); if (begin(ImVec2(900, 650))) { - float group_w = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) / 2; - ImGui::BeginChild("Messages", ImVec2(group_w, 0), ImGuiChildFlags_Borders | ImGuiChildFlags_AutoResizeY); + const ImGuiStyle &style = ImGui::GetStyle(); + const float group_w = (ImGui::GetContentRegionAvail().x - style.ItemSpacing.x) / 2; + const float group_h = ImGui::GetTextLineHeightWithSpacing() + ImGui::GetFrameHeightWithSpacing() * 4 + + style.WindowPadding.y * 2 - style.ItemSpacing.y; + ImGui::BeginChild("Messages", ImVec2(group_w, group_h), ImGuiChildFlags_Borders); drawMessageGroup(); ImGui::EndChild(); ImGui::SameLine(); - ImGui::BeginChild("Signal", ImVec2(group_w, 0), ImGuiChildFlags_Borders | ImGuiChildFlags_AutoResizeY); + ImGui::BeginChild("Signal", ImVec2(group_w, group_h), ImGuiChildFlags_Borders); drawPropertiesGroup(); ImGui::EndChild(); float footer = searched_ ? ImGui::GetTextLineHeightWithSpacing() : 0; @@ -98,20 +101,21 @@ bool FindSignalDlg::draw() { void FindSignalDlg::drawMessageGroup() { ImGui::BeginDisabled(searching_ || !search_.histories.empty()); + const float field_x = ImGui::GetCursorPosX() + ImGui::CalcTextSize("Address").x + ImGui::GetStyle().ItemSpacing.x; ImGui::TextUnformatted("Messages"); ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted("Bus"); - ImGui::SameLine(80); + ImGui::SameLine(field_x); ImGui::SetNextItemWidth(-1); inputText("##bus", &bus_, "Comma-separated values. Leave blank for all."); ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted("Address"); - ImGui::SameLine(80); + ImGui::SameLine(field_x); ImGui::SetNextItemWidth(-1); inputText("##address", &address_, "Comma-separated hex values. Leave blank for all."); ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted("Time"); - ImGui::SameLine(80); + ImGui::SameLine(field_x); ImGui::SetNextItemWidth(70); validatedText("##first_time", &first_time_, validateDouble); ImGui::SameLine(); @@ -126,29 +130,30 @@ void FindSignalDlg::drawMessageGroup() { void FindSignalDlg::drawPropertiesGroup() { ImGui::BeginDisabled(searching_ || !search_.histories.empty()); + const float field_x = ImGui::GetCursorPosX() + ImGui::CalcTextSize("Factor").x + ImGui::GetStyle().ItemSpacing.x; ImGui::TextUnformatted("Signal"); ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted("Size"); - ImGui::SameLine(80); - ImGui::SetNextItemWidth(70); - if (ImGui::InputInt("##min_size", &min_size_, 1, 10)) min_size_ = std::clamp(min_size_, 1, 64); + ImGui::SameLine(field_x); + ImGui::SetNextItemWidth(inputIntWidth(2)); + if (inputInt("##min_size", &min_size_, 1, 10)) min_size_ = std::clamp(min_size_, 1, 64); ImGui::SameLine(); ImGui::TextUnformatted("-"); ImGui::SameLine(); - ImGui::SetNextItemWidth(70); - if (ImGui::InputInt("##max_size", &max_size_, 1, 10)) max_size_ = std::clamp(max_size_, 1, 64); - ImGui::SameLine(); + ImGui::SetNextItemWidth(inputIntWidth(2)); + if (inputInt("##max_size", &max_size_, 1, 10)) max_size_ = std::clamp(max_size_, 1, 64); + ImGui::SetCursorPosX(field_x); checkBox("Little Endian", &little_endian_); ImGui::SameLine(); checkBox("Signed", &is_signed_); ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted("Factor"); - ImGui::SameLine(80); + ImGui::SameLine(field_x); ImGui::SetNextItemWidth(100); validatedText("##factor", &factor_, validateDouble); ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted("Offset"); - ImGui::SameLine(80); + ImGui::SameLine(field_x); ImGui::SetNextItemWidth(100); validatedText("##offset", &offset_, validateDouble); ImGui::EndDisabled(); diff --git a/openpilot/tools/cabana/ui/tools/findsimilarbits.cc b/openpilot/tools/cabana/ui/tools/findsimilarbits.cc index daffc5408c..1761fd0f51 100644 --- a/openpilot/tools/cabana/ui/tools/findsimilarbits.cc +++ b/openpilot/tools/cabana/ui/tools/findsimilarbits.cc @@ -30,9 +30,10 @@ void FindSimilarBitsDlg::updateMessages() { bool FindSimilarBitsDlg::draw() { if (begin(ImVec2(700, 500))) { + const float field_x = ImGui::GetCursorPosX() + ImGui::CalcTextSize("Find From:").x + ImGui::GetStyle().ItemSpacing.x; ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted("Find From:"); - ImGui::SameLine(90); + ImGui::SameLine(field_x); ImGui::TextUnformatted("Bus"); ImGui::SameLine(); ImGui::SetNextItemWidth(60); @@ -40,20 +41,21 @@ bool FindSimilarBitsDlg::draw() { ImGui::SameLine(); ImGui::SetNextItemWidth(200); comboBox("##msg", &msg_index_, msg_names_); - ImGui::SameLine(); + ImGui::SetCursorPosX(field_x); + ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted("Byte Index"); ImGui::SameLine(); - ImGui::SetNextItemWidth(80); - if (ImGui::InputInt("##byte_idx", &byte_idx_, 1, 10)) byte_idx_ = std::clamp(byte_idx_, 0, 63); + ImGui::SetNextItemWidth(inputIntWidth(2)); + if (inputInt("##byte_idx", &byte_idx_, 1, 10)) byte_idx_ = std::clamp(byte_idx_, 0, 63); ImGui::SameLine(); ImGui::TextUnformatted("Bit Index"); ImGui::SameLine(); - ImGui::SetNextItemWidth(80); - if (ImGui::InputInt("##bit_idx", &bit_idx_, 1, 10)) bit_idx_ = std::clamp(bit_idx_, 0, 7); + ImGui::SetNextItemWidth(inputIntWidth(1)); + if (inputInt("##bit_idx", &bit_idx_, 1, 10)) bit_idx_ = std::clamp(bit_idx_, 0, 7); ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted("Find In:"); - ImGui::SameLine(90); + ImGui::SameLine(field_x); ImGui::TextUnformatted("Bus"); ImGui::SameLine(); ImGui::SetNextItemWidth(60); @@ -66,8 +68,8 @@ bool FindSimilarBitsDlg::draw() { ImGui::SameLine(); ImGui::TextUnformatted("Minimum Message Count"); ImGui::SameLine(); - ImGui::SetNextItemWidth(80); - if (ImGui::InputInt("##min_msgs", &min_msgs_, 1, 10)) min_msgs_ = std::max(min_msgs_, 0); + ImGui::SetNextItemWidth(inputIntWidth(4)); + if (inputInt("##min_msgs", &min_msgs_, 1, 10)) min_msgs_ = std::max(min_msgs_, 0); ImGui::SameLine(); if (ImGui::Button("Find")) find(); diff --git a/openpilot/tools/cabana/ui/util.cc b/openpilot/tools/cabana/ui/util.cc index 50553c2d47..69eb94980b 100644 --- a/openpilot/tools/cabana/ui/util.cc +++ b/openpilot/tools/cabana/ui/util.cc @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -71,13 +72,13 @@ bool beginControlChild(const char *id, const ImVec2 &size, ImGuiWindowFlags flag bool clearableInput(const char *label, std::string *s, const char *hint, ImGuiInputTextCallback validator) { const float width = ImGui::CalcItemWidth(); - const float clear_width = iconButtonWidth() + ImGui::GetStyle().ItemInnerSpacing.x; + const float clear_width = iconButtonWidth() + ImGui::GetStyle().ItemSpacing.x; const bool show_clear = !s->empty() && width >= clear_width + ImGui::GetFrameHeight(); ImGui::SetNextItemWidth(show_clear ? width - clear_width : width); ImGui::BeginGroup(); bool changed = validatedInput(label, s, validator, hint); if (show_clear) { - ImGui::SameLine(0.0f, ImGui::GetStyle().ItemInnerSpacing.x); + ImGui::SameLine(); ImGui::PushID(label); if (iconButton("clear", icon::X_LG)) { s->clear(); @@ -205,11 +206,10 @@ bool squareIconButton(const char *id, const char *icon) { ImFontBaked *baked = ImGui::GetFont()->GetFontBaked(size); if (const ImFontGlyph *g = baked->FindGlyph((ImWchar)codepoint)) { const GlyphInk ink = cachedGlyphInk(g, size, codepoint); - // Preserve half-logical-pixel positions on HiDPI displays. - const float snap = std::max(1.0f, ImGui::GetIO().DisplayFramebufferScale.x); - auto snapped = [snap](float v) { return std::round(v * snap) / snap; }; - const ImVec2 pos(snapped(r.GetCenter().x - (ink.x0 + ink.x1) * 0.5f), snapped(r.GetCenter().y - (ink.y0 + ink.y1) * 0.5f)); - // AddText truncates to whole logical pixels, undoing the framebuffer snapping above. + // Keep the exact ink center, including half pixels when glyph and button sizes + // have different parity. Rounding the origin shifts small icons off center. + const ImVec2 pos(r.GetCenter().x - (ink.x0 + ink.x1) * 0.5f, r.GetCenter().y - (ink.y0 + ink.y1) * 0.5f); + // AddText truncates the origin, so draw the atlas glyph directly. ImGui::GetWindowDrawList()->AddImage(ImGui::GetIO().Fonts->TexRef, ImVec2(pos.x + g->X0, pos.y + g->Y0), ImVec2(pos.x + g->X1, pos.y + g->Y1), ImVec2(g->U0, g->V0), ImVec2(g->U1, g->V1), ImGui::GetColorU32(ImGuiCol_Text)); @@ -290,7 +290,8 @@ bool dialogEscapePressed() { bool dialogButtons(const char *accept_label, bool *accepted, bool *rejected, bool accept_enabled, const char *reject_label) { - const float button_width = 80.0f; + const float button_width = std::max({spacing::DIALOG_BUTTON_MIN_WIDTH, toolbarButtonWidth(accept_label), + reject_label ? toolbarButtonWidth(reject_label) : 0.0f}); const int count = reject_label ? 2 : 1; const float total = button_width * count + ImGui::GetStyle().ItemSpacing.x * (count - 1); const float avail = ImGui::GetContentRegionAvail().x; @@ -316,6 +317,52 @@ bool dialogButtons(const char *accept_label, bool *accepted, bool *rejected, boo return pressed; } +float inputIntWidth(int digits) { + const ImGuiStyle &style = ImGui::GetStyle(); + return ImGui::CalcTextSize(std::string(digits, '0').c_str()).x + style.FramePadding.x * 2 + + (ImGui::GetFrameHeight() + style.ItemSpacing.x) * 2; +} + +bool stepButton(const char *id, bool increment, const char *tooltip) { + return iconButton(id, increment ? icon::PLUS_LG : icon::DASH_LG, tooltip); +} + +bool inputInt(const char *label, int *value, int step, int step_fast, ImGuiInputTextFlags flags) { + if (step <= 0) return ImGui::InputInt(label, value, 0, 0, flags); + + const float spacing = ImGui::GetStyle().ItemSpacing.x; + const float width = ImGui::CalcItemWidth(); + ImGui::BeginGroup(); + ImGui::PushID(label); + ImGui::SetNextItemWidth(std::max(1.0f, width - 2 * (iconButtonWidth() + spacing))); + bool changed = ImGui::InputInt("##value", value, 0, 0, flags); + ImGui::BeginDisabled(flags & ImGuiInputTextFlags_ReadOnly); + ImGui::PushItemFlag(ImGuiItemFlags_ButtonRepeat, true); + for (bool increment : {false, true}) { + ImGui::SameLine(0.0f, spacing); + if (stepButton(increment ? "increment" : "decrement", increment)) { + const int amount = ImGui::GetIO().KeyCtrl && step_fast > 0 ? step_fast : step; + const int next = std::clamp(int64_t(*value) + (increment ? int64_t(amount) : -int64_t(amount)), + std::numeric_limits::min(), std::numeric_limits::max()); + if (next != *value) { + *value = next; + changed = true; + } + } + } + ImGui::PopItemFlag(); + ImGui::EndDisabled(); + const char *label_end = ImGui::FindRenderedTextEnd(label); + if (label != label_end) { + ImGui::SameLine(0.0f, ImGui::GetStyle().ItemInnerSpacing.x); + ImGui::TextUnformatted(label, label_end); + } + ImGui::PopID(); + ImGui::EndGroup(); + if (changed) ImGui::MarkItemEdited(ImGui::GetItemID()); + return changed; +} + int tableHeadersRow() { int clicked = -1; ImGui::TableNextRow(ImGuiTableRowFlags_Headers); @@ -449,16 +496,16 @@ bool beginDialog(const char *id, PopupOwner *owner, const ImVec2 &size, ImGuiWin // tool bar -ToolbarItem toolbarAction(const char *id, const char *icon, const char *label, std::function trigger, bool enabled, bool tight) { +ToolbarItem toolbarAction(const char *id, const char *icon, const char *label, std::function trigger, bool enabled) { return {iconButtonWidth(), [=]() { ImGui::BeginDisabled(!enabled); if (iconButton(id, icon)) trigger(); ImGui::EndDisabled(); disabledItemTooltip(label); - }, label, trigger, enabled, true, tight}; + }, label, trigger, enabled, true}; } -ToolbarItem toolbarMenu(const char *id, const std::string &text, const char *label, std::function items, bool bold, bool tight, float width) { +ToolbarItem toolbarMenu(const char *id, const std::string &text, const char *label, std::function items, bool bold, float width) { if (width <= 0.0f) width = menuButtonWidth(text, bold); ToolbarItem item{width, [id, text, items, bold, width]() { const std::string popup_id = std::string(id) + "_menu"; @@ -468,7 +515,6 @@ ToolbarItem toolbarMenu(const char *id, const std::string &text, const char *lab ImGui::EndPopup(); } }, label}; - item.tight = tight; item.submenu = std::move(items); return item; } @@ -477,13 +523,9 @@ float toolbarButtonWidth(const std::string &label) { return ImGui::CalcTextSize(label.c_str(), nullptr, true).x + ImGui::GetStyle().FramePadding.x * 2; } -static float toolbarSpacing(const ToolbarItem &item) { - return item.tight ? ImGui::GetStyle().ItemInnerSpacing.x : ImGui::GetStyle().ItemSpacing.x; -} - static float toolbarGroupWidth(const std::vector &items, size_t begin, size_t end) { float w = 0; - for (size_t i = begin; i < end; ++i) w += items[i].width + (i > begin ? toolbarSpacing(items[i]) : 0); + for (size_t i = begin; i < end; ++i) w += items[i].width + (i > begin ? ImGui::GetStyle().ItemSpacing.x : 0); return w; } @@ -512,7 +554,7 @@ void drawToolbar(const std::vector &items, size_t spacer_index, flo const float usable = avail - (extension_width + style.ItemSpacing.x); float used = 0; for (visible = 0; visible < items.size(); ++visible) { - const float w = items[visible].width + (visible ? toolbarSpacing(items[visible]) : 0); + const float w = items[visible].width + (visible ? style.ItemSpacing.x : 0); if (used + w > usable) break; used += w; } @@ -521,7 +563,7 @@ void drawToolbar(const std::vector &items, size_t spacer_index, flo for (size_t i = 0; i < visible; ++i) { if (i == 0) ImGui::SetCursorPosX(start_x); else if (fits && i == spacer_index) ImGui::SameLine(right_edge - right_width); - else ImGui::SameLine(0.0f, toolbarSpacing(items[i])); + else ImGui::SameLine(0.0f, ImGui::GetStyle().ItemSpacing.x); items[i].draw(); } @@ -557,11 +599,10 @@ void drawToolbar(const std::vector &items, size_t spacer_index, flo } const float MENU_ARROW_SIZE = 6.0f; // dropdown arrow on a menu button -const float MENU_ARROW_SPACING = 5.0f; // gap between the label and the dropdown arrow float menuButtonWidth(const std::string &text, bool bold) { if (bold) pushBoldFont(); - const float w = ImGui::CalcTextSize(text.c_str(), nullptr, true).x + MENU_ARROW_SPACING + MENU_ARROW_SIZE + + const float w = ImGui::CalcTextSize(text.c_str(), nullptr, true).x + ImGui::GetStyle().ItemInnerSpacing.x + MENU_ARROW_SIZE + ImGui::GetStyle().FramePadding.x * 2; if (bold) popBoldFont(); return w; @@ -576,7 +617,7 @@ bool menuButton(const char *id, const std::string &text, const char *popup_id, b const float text_width = ImGui::CalcTextSize(text.c_str(), nullptr, true).x; const float ascent = ImGui::GetFontBaked()->Ascent; // the text and the arrow are centered as a group in the button - const float padding_x = std::max(style.FramePadding.x, (width - (text_width + MENU_ARROW_SPACING + MENU_ARROW_SIZE)) * 0.5f); + const float padding_x = std::max(style.FramePadding.x, (width - (text_width + ImGui::GetStyle().ItemInnerSpacing.x + MENU_ARROW_SIZE)) * 0.5f); ImGui::PushStyleColor(ImGuiCol_Button, popup_open ? style.Colors[ImGuiCol_ButtonActive] : style.Colors[ImGuiCol_Button]); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(padding_x, style.FramePadding.y)); ImGui::PushStyleVar(ImGuiStyleVar_ButtonTextAlign, ImVec2(0.0f, 0.5f)); @@ -588,7 +629,7 @@ bool menuButton(const char *id, const std::string &text, const char *popup_id, b if (bold) popBoldFont(); // a 6 px arrow right after the text, sitting on the text baseline const ImVec2 min = ImGui::GetItemRectMin(); - const float x = min.x + padding_x + text_width + MENU_ARROW_SPACING; + const float x = min.x + padding_x + text_width + ImGui::GetStyle().ItemInnerSpacing.x; const float baseline = min.y + style.FramePadding.y + ascent; ImGui::GetWindowDrawList()->AddTriangleFilled(ImVec2(x, baseline - MENU_ARROW_SIZE * 0.5f), ImVec2(x + MENU_ARROW_SIZE, baseline - MENU_ARROW_SIZE * 0.5f), diff --git a/openpilot/tools/cabana/ui/util.h b/openpilot/tools/cabana/ui/util.h index e1f368e1ac..f431c9a338 100644 --- a/openpilot/tools/cabana/ui/util.h +++ b/openpilot/tools/cabana/ui/util.h @@ -83,6 +83,7 @@ inline std::string shortcut(const char *keys) { return std::string(MOD_KEY) + "+ // Use ItemInnerSpacing between related buttons and ItemSpacing between groups. bool iconButton(const char *id, const char *icon, const char *tooltip = nullptr); float iconButtonWidth(); +bool stepButton(const char *id, bool increment, const char *tooltip = nullptr); 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); @@ -115,6 +116,12 @@ ImGuiWindow *topPopupWindow(); bool dialogButtons(const char *accept_label, bool *accepted, bool *rejected, bool accept_enabled = true, const char *reject_label = "Cancel"); +// Numeric inputs keep the same external gaps as other button rows. Width includes +// a readable value plus both step buttons; use for compact fixed-width fields. +float inputIntWidth(int digits); +bool inputInt(const char *label, int *value, int step = 1, int step_fast = 100, + ImGuiInputTextFlags flags = ImGuiInputTextFlags_None); + // horizontal header labels are centered. Returns the column a right click was released on, or -1. int tableHeadersRow(); @@ -168,15 +175,14 @@ struct ToolbarItem { std::function trigger; bool enabled = true; bool in_menu = true; // false: left out of the ">>" menu (a separator) - bool tight = false; // true: ItemInnerSpacing before it, it belongs to the previous item's group std::function submenu; // set: the ">>" entry is a submenu with these items instead of an action }; ToolbarItem toolbarAction(const char *id, const char *icon, const char *label, std::function trigger, - bool enabled = true, bool tight = false); + bool enabled = true); // A drop-down button that opens `items` in a popup; in the overflow menu they become a submenu. // width 0: sized to the text. ToolbarItem toolbarMenu(const char *id, const std::string &text, const char *label, std::function items, - bool bold = false, bool tight = false, float width = 0.0f); + bool bold = false, float width = 0.0f); float toolbarButtonWidth(const std::string &label); // the width of every item plus the spacing between neighbors and the two groups float toolbarWidth(const std::vector &items, size_t spacer_index); diff --git a/openpilot/tools/cabana/ui/widgets/detailwidget.cc b/openpilot/tools/cabana/ui/widgets/detailwidget.cc index cd3d3378a8..39fc68683e 100644 --- a/openpilot/tools/cabana/ui/widgets/detailwidget.cc +++ b/openpilot/tools/cabana/ui/widgets/detailwidget.cc @@ -102,7 +102,7 @@ void DetailWidget::drawToolBar() { const float panel_width = ImGui::GetWindowWidth(); items.push_back(toolbarAction("edit_msg", icon::PENCIL, "Edit Message", [this, panel_width]() { editMsg(panel_width); })); items.push_back(toolbarAction("remove_msg", icon::TRASH, "Remove Message", - [this]() { UndoStack::instance()->push(new RemoveMsgCommand(msg_id_)); }, action_remove_msg_enabled_, true)); + [this]() { UndoStack::instance()->push(new RemoveMsgCommand(msg_id_)); }, action_remove_msg_enabled_)); const float right_width = toolbarWidth(items, spacer_index) - style.ItemSpacing.x; name_width = std::max(ImGui::CalcTextSize("MMMMMM").x, ImGui::GetContentRegionAvail().x - right_width - style.ItemSpacing.x); @@ -221,13 +221,12 @@ void DetailWidget::drawTabWidget() { // binary_view_ keeps its size hint, signal_view_ takes the rest const float min_height = binary_view_->minimumSizeHint().y; const float avail = ImGui::GetContentRegionAvail().y; - const float max_height = std::max(avail - 6.0f - ImGui::GetStyle().ItemSpacing.y * 2 - 1.0f, 1.0f); + const float max_height = std::max(avail - style.ItemSpacing.y - 1.0f, 1.0f); const float height = std::clamp(min_height, 1.0f, max_height); ImGui::BeginChild("binary_view", ImVec2(0, height), ImGuiChildFlags_None, ImGuiWindowFlags_HorizontalScrollbar); binary_view_rect_ = ImGui::GetCurrentWindow()->Rect(); binary_view_->draw(); ImGui::EndChild(); - ImGui::Dummy(ImVec2(0.0f, 6.0f)); ImGui::BeginChild("signal_view", ImVec2(0, 0)); signal_view_rect_ = ImGui::GetCurrentWindow()->Rect(); signal_view_->draw(); @@ -338,7 +337,7 @@ bool EditMessageDialog::draw() { setNextDialogWindow(ImVec2(std::clamp(width_, min_width, max_width), 0.0f)); bool open = true; if (ImGui::BeginPopupModal(window_title_.c_str(), &open)) { - const float label_width = ImGui::CalcTextSize("Comment").x + ImGui::GetStyle().ItemSpacing.x * 2; + const float label_width = ImGui::GetCursorPosX() + ImGui::CalcTextSize("Comment").x + ImGui::GetStyle().ItemSpacing.x; auto row = [&](const char *label) { ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted(label); @@ -356,7 +355,7 @@ bool EditMessageDialog::draw() { } row("Size"); - if (ImGui::InputInt("##size", &size_spin_)) size_spin_ = std::clamp(size_spin_, 1, CAN_MAX_DATA_BYTES); + if (inputInt("##size", &size_spin_)) size_spin_ = std::clamp(size_spin_, 1, CAN_MAX_DATA_BYTES); row("Node"); validatedInput("##node", &node_, nameValidator); diff --git a/openpilot/tools/cabana/ui/widgets/scrollabletabbar.cc b/openpilot/tools/cabana/ui/widgets/scrollabletabbar.cc index 75d7c42788..c35e649148 100644 --- a/openpilot/tools/cabana/ui/widgets/scrollabletabbar.cc +++ b/openpilot/tools/cabana/ui/widgets/scrollabletabbar.cc @@ -9,7 +9,7 @@ namespace { float scrollButtonsWidth() { const ImGuiStyle &style = ImGui::GetStyle(); - return style.ItemSpacing.x + ImGui::GetFrameHeight() * 2.0f + style.ItemInnerSpacing.x; + return (ImGui::GetFrameHeight() + style.ItemSpacing.x) * 2.0f; } void drawScrollButtons(ImGuiTabBar *tab_bar) { @@ -22,7 +22,7 @@ void drawScrollButtons(ImGuiTabBar *tab_bar) { ImGui::PushItemFlag(ImGuiItemFlags_ButtonRepeat, true); for (int i = 0; i < 2; ++i) { const bool left = i == 0; - ImGui::SetCursorScreenPos(ImVec2(start_x + i * (size + style.ItemInnerSpacing.x), tab_bar->BarRect.Min.y)); + ImGui::SetCursorScreenPos(ImVec2(start_x + i * (size + style.ItemSpacing.x), tab_bar->BarRect.Min.y)); ImGui::BeginDisabled(left ? tab_bar->ScrollingTarget <= 0.0f : tab_bar->ScrollingTarget >= max_scroll); if (ImGui::Button(left ? "###scroll_left" : "###scroll_right", ImVec2(size, size))) { const float step = (left ? -4.0f : 4.0f) * ImGui::GetFontSize(); diff --git a/openpilot/tools/cabana/ui/widgets/signalview.cc b/openpilot/tools/cabana/ui/widgets/signalview.cc index ef18357184..344bf7a4b3 100644 --- a/openpilot/tools/cabana/ui/widgets/signalview.cc +++ b/openpilot/tools/cabana/ui/widgets/signalview.cc @@ -334,15 +334,18 @@ void SignalView::drawEditor(SignalModel::Item *item) { drawLineEditor(item, validator, take_focus); } else if (item->type == SignalModel::Item::Size) { - int v = item->sig->size; - if (take_focus) ImGui::SetKeyboardFocusHere(); - bool changed = ImGui::InputInt("##editor", &v, 1, 100, ImGuiInputTextFlags_AutoSelectAll); + if (take_focus) { + edit_int_ = item->sig->size; + ImGui::SetKeyboardFocusHere(); + } + bool changed = inputInt("##editor", &edit_int_, 1, 100, ImGuiInputTextFlags_AutoSelectAll); if (ImGui::IsItemDeactivated() && ImGui::IsKeyPressed(ImGuiKey_Escape, false)) { open_item_ = nullptr; // InputInt already reverted the value; only the commit has to be skipped return; } if (ImGui::IsItemDeactivatedAfterEdit() || (changed && !ImGui::IsItemActive())) { - queueCommit(item, std::clamp(v, 1, CAN_MAX_DATA_BYTES)); + edit_int_ = std::clamp(edit_int_, 1, CAN_MAX_DATA_BYTES); + queueCommit(item, edit_int_); } // Enter, Escape and a click outside close the editor; the step buttons keep it open if (ImGui::IsItemDeactivated() && (!ImGui::IsItemHovered() || ImGui::IsKeyPressed(ImGuiKey_Enter, false) || @@ -492,7 +495,7 @@ void SignalView::drawValueDescriptionDlg() { } static ImVec2 indexButtonsSize(float button) { - return ImVec2(button * 2 + ImGui::GetStyle().ItemInnerSpacing.x * 2, button); + return ImVec2(button * 2 + ImGui::GetStyle().ItemSpacing.x, button); } SignalView::SignalView(ChartsWidget *charts) : charts_(charts) { @@ -871,7 +874,7 @@ bool SignalView::drawItem(SignalModel::Item *item, int depth, DrawContext &ctx) } void SignalView::drawIndexWidget(SignalModel::Item *item, const ImRect &rect) { - const float spacing = ImGui::GetStyle().ItemInnerSpacing.x; + const float spacing = ImGui::GetStyle().ItemSpacing.x; const ImVec2 size = indexButtonsSize(iconButtonWidth()); ImGui::SetCursorScreenPos(ImVec2(rect.Max.x - size.x, rect.Min.y + (rect.GetHeight() - size.y) * 0.5f)); @@ -910,12 +913,12 @@ bool ValueDescriptionDlg::draw() { if (!ImGui::BeginPopupModal(popup_id.c_str(), &open, ImGuiWindowFlags_NoSavedSettings)) return ImGui::IsPopupOpen(popup_id.c_str()); bool closing = false; - if (iconButton("add", icon::PLUS_LG, "Add")) { + if (stepButton("add", true, "Add")) { table_.emplace_back("", ""); } - ImGui::SameLine(0.0f, ImGui::GetStyle().ItemInnerSpacing.x); + ImGui::SameLine(); ImGui::BeginDisabled(current_row_ == -1); - if (iconButton("remove", icon::DASH_LG, "Remove") && current_row_ < table_.size()) { + if (stepButton("remove", false, "Remove") && current_row_ < table_.size()) { table_.erase(table_.begin() + current_row_); current_row_ = -1; } diff --git a/openpilot/tools/cabana/ui/widgets/signalview.h b/openpilot/tools/cabana/ui/widgets/signalview.h index eaeb6d2aed..dc77ad7b37 100644 --- a/openpilot/tools/cabana/ui/widgets/signalview.h +++ b/openpilot/tools/cabana/ui/widgets/signalview.h @@ -190,6 +190,7 @@ private: std::function pending_commit_; SignalModel::Item *editing_item_ = nullptr; // the open text editor std::string edit_text_; + int edit_int_ = 0; bool editor_active_ = false; // editor had the keyboard focus last frame bool refocus_editor_ = false; // reopen the editor rejected by the validator bool enter_pressed_ = false; diff --git a/openpilot/tools/cabana/ui/widgets/videowidget.cc b/openpilot/tools/cabana/ui/widgets/videowidget.cc index 5122ca96fa..0896951bab 100644 --- a/openpilot/tools/cabana/ui/widgets/videowidget.cc +++ b/openpilot/tools/cabana/ui/widgets/videowidget.cc @@ -25,8 +25,7 @@ const int MIN_VIDEO_HEIGHT = 100; const int THUMBNAIL_MARGIN = 3; const float POINT_10_FONT_SIZE = 13.0f; // 10 pt at 96 dpi const float POINT_16_FONT_SIZE = 21.0f; // 16 pt at 96 dpi -const float TOOLBAR_MARGIN_Y = 6.0f; // between the slider and the buttons, which are as tall as the ones in the charts toolbar -const float TOOLBAR_SEPARATOR_EXTENT = 6.0f; +constexpr float TOOLBAR_SEPARATOR_EXTENT = 1.0f; const float SLIDER_HEIGHT = 15.0f; // the handle plus a 1 px margin // Indexed by TimelineType: None, Engaged, AlertInfo, AlertWarning, AlertCritical, UserBookmark @@ -138,11 +137,11 @@ std::string VideoWidget::whatsThis() const { "Pause/Resume:  space "; } -static float toolbarHeight() { return TOOLBAR_MARGIN_Y + ImGui::GetFrameHeight(); } +static float toolbarHeight() { return ImGui::GetStyle().ItemSpacing.y + ImGui::GetFrameHeight(); } void VideoWidget::drawPlaybackController() { if (!can->liveStreaming()) - ImGui::SetCursorPosY(ImGui::GetCursorPosY() + TOOLBAR_MARGIN_Y); + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + ImGui::GetStyle().ItemSpacing.y); const float speed_width = menuButtonWidth("0.05x", true); const char *play_icon = can->isPaused() ? icon::PLAY : icon::PAUSE; @@ -156,11 +155,11 @@ void VideoWidget::drawPlaybackController() { if (!can->liveStreaming()) { items.push_back(toolbarAction("rewind", icon::REWIND, "Seek backward", []() { can->seekTo(can->currentSec() - 1); })); } - items.push_back(toolbarAction("play", play_icon, play_tooltip, []() { can->pause(!can->isPaused()); }, true, true)); + items.push_back(toolbarAction("play", play_icon, play_tooltip, []() { can->pause(!can->isPaused()); })); if (can->liveStreaming()) { - items.push_back(toolbarAction("skip-end", icon::SKIP_END, "Go live", [this]() { skipToEnd(); }, skip_to_end_enabled_, true)); + items.push_back(toolbarAction("skip-end", icon::SKIP_END, "Go live", [this]() { skipToEnd(); }, skip_to_end_enabled_)); } else { - items.push_back(toolbarAction("fast-forward", icon::FAST_FORWARD, "Seek forward", []() { can->seekTo(can->currentSec() + 1); }, true, true)); + items.push_back(toolbarAction("fast-forward", icon::FAST_FORWARD, "Seek forward", []() { can->seekTo(can->currentSec() + 1); })); } if (slider_ || msgs_received_) { // a mono font: with proportional digits the time changed width as it ticked and the items after it moved @@ -186,20 +185,21 @@ void VideoWidget::drawPlaybackController() { const ImVec2 min = ImGui::GetCursorScreenPos(); ImGui::Dummy(ImVec2(TOOLBAR_SEPARATOR_EXTENT, ImGui::GetFrameHeight())); const float x = std::floor(min.x + TOOLBAR_SEPARATOR_EXTENT * 0.5f); - ImGui::GetWindowDrawList()->AddLine(ImVec2(x, min.y + 4.0f), ImVec2(x, min.y + ImGui::GetFrameHeight() - 4.0f), ImGui::GetColorU32(ImGuiCol_Separator)); + const float inset = ImGui::GetStyle().FramePadding.y; + ImGui::GetWindowDrawList()->AddLine(ImVec2(x, min.y + inset), ImVec2(x, min.y + ImGui::GetFrameHeight() - inset), + ImGui::GetColorU32(ImGuiCol_Separator)); }}; item.in_menu = false; - item.tight = true; return item; }; const char *aspect_ratio_icon = settings.crop_video ? icon::ASPECT_RATIO_FILL : icon::ASPECT_RATIO; if (!can->liveStreaming()) { items.push_back(toolbarAction("crop_video", aspect_ratio_icon, "Crop to fill", [this]() { cropVideoClicked(); })); items.push_back(separator()); - items.push_back(toolbarAction("loop", loop_icon, "Loop playback", [this]() { loopPlaybackClicked(); }, true, true)); - items.push_back(toolbarMenu("speed_btn", speed_text_, "Speed", [this]() { drawSpeedMenuItems(); }, true, true, speed_width)); + items.push_back(toolbarAction("loop", loop_icon, "Loop playback", [this]() { loopPlaybackClicked(); })); + items.push_back(toolbarMenu("speed_btn", speed_text_, "Speed", [this]() { drawSpeedMenuItems(); }, true, speed_width)); items.push_back(separator()); - items.push_back(toolbarAction("route_info", icon::INFO_CIRCLE, "View route details", [this]() { showRouteInfo(); }, true, true)); + items.push_back(toolbarAction("route_info", icon::INFO_CIRCLE, "View route details", [this]() { showRouteInfo(); })); } drawToolbar(items, spacer_index); @@ -275,16 +275,20 @@ void VideoWidget::createCameraWidget() { } void VideoWidget::drawCameraWidget() { + const float toolbar_height = toolbarHeight(); + // Camera tabs, video and timeline touch; restore the normal gap for the controls. + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(ImGui::GetStyle().ItemSpacing.x, 0.0f)); camera_tab_->draw(); // cam_widget_: minimum height MIN_VIDEO_HEIGHT, takes the space left by the slider and the toolbar const ImVec2 avail = ImGui::GetContentRegionAvail(); - const float cam_height = std::max((float)MIN_VIDEO_HEIGHT, avail.y - SLIDER_HEIGHT - toolbarHeight()); + const float cam_height = std::max((float)MIN_VIDEO_HEIGHT, avail.y - SLIDER_HEIGHT - toolbar_height); cam_widget_->draw(ImVec2(avail.x, cam_height), thumbnail_display_time_); if (!slider_->isSliderDown()) slider_->setCurrentSecond(can->currentSec()); slider_->draw(thumbnail_display_time_); updateSliderThumbnail(); + ImGui::PopStyleVar(); } void VideoWidget::vipcAvailableStreamsUpdated(std::set streams) { @@ -364,12 +368,10 @@ float VideoWidget::defaultHeight(float width) const { } void VideoWidget::draw() { - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(ImGui::GetStyle().ItemSpacing.x, 0.0f)); if (!can->liveStreaming()) drawCameraWidget(); drawPlaybackController(); - ImGui::PopStyleVar(); for (auto it = route_info_dlgs_.begin(); it != route_info_dlgs_.end();) { it = (*it)->draw() ? it + 1 : route_info_dlgs_.erase(it);