diff --git a/openpilot/tools/cabana/ui/chart/chart.cc b/openpilot/tools/cabana/ui/chart/chart.cc index 5d1389822e..ea54fc819b 100644 --- a/openpilot/tools/cabana/ui/chart/chart.cc +++ b/openpilot/tools/cabana/ui/chart/chart.cc @@ -19,7 +19,8 @@ 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{8, 6, 8, 6}; // left, top, right, bottom +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; } @@ -70,10 +71,10 @@ void ChartView::drawMenuActions() { // the buttons and their menus are drawn every frame, at the rects updateLayout() placed them at void ChartView::createToolButtons() { ImGui::SetCursorScreenPos(layout_.close_btn_rect.Min); - bool close_clicked = toolButton("close_btn", icon::X, "Remove Chart"); + bool close_clicked = iconButton("close_btn", icon::X_LG, "Remove Chart"); ImGui::SetCursorScreenPos(layout_.manage_btn_rect.Min); - if (toolButton("manage_btn", icon::LIST, "")) ImGui::OpenPopup("manage_menu"); + if (iconButton("manage_btn", icon::THREE_DOTS_VERTICAL, "")) ImGui::OpenPopup("manage_menu"); if (ImGui::BeginPopup("manage_menu")) { drawMenuActions(); ImGui::EndPopup(); @@ -136,27 +137,25 @@ 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); layout_.move_icon_rect = ImRect(top_left, top_left + grip); - const ImVec2 pad = ImGui::GetStyle().FramePadding * 2; - const ImVec2 close_size = ImGui::CalcTextSize(icon::X) + pad; - const ImVec2 manage_size = ImGui::CalcTextSize(icon::LIST) + pad; - const ImVec2 close_min(layout_.rect.Max.x - LAYOUT_MARGINS.z - close_size.x, top_left.y); - layout_.close_btn_rect = ImRect(close_min, close_min + close_size); - const ImVec2 manage_min(close_min.x - manage_size.x - ImGui::GetStyle().ItemSpacing.x, top_left.y); - layout_.manage_btn_rect = ImRect(manage_min, manage_min + manage_size); + const ImVec2 btn_size(iconButtonWidth(), iconButtonWidth()); + const ImVec2 close_min(layout_.rect.Max.x - LAYOUT_MARGINS.z - 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); 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 + LAYOUT_MARGINS.x; + 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); // 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 + 5 + bold->CalcTextSizeA(font_size, FLT_MAX, 0.0f, s.sig->name.c_str()).x + + 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; w = std::min(w, legend_right - legend_left); // keep oversized entries clear of the header buttons if (x + w > legend_right && x > legend_left) { @@ -531,13 +530,13 @@ void ChartView::paint() { drawStaticLayer(); if (can_drop_) { - ImGui::GetWindowDrawList()->AddRect(layout_.rect.Min, layout_.rect.Max, ImGui::GetColorU32(ImGuiCol_Header), 0.0f, 0, 4.0f); + ImGui::GetWindowDrawList()->AddRect(layout_.rect.Min, layout_.rect.Max, ImGui::GetColorU32(ImGuiCol_Header), ImGui::GetStyle().ChildRounding, 0, 4.0f); } } void ChartView::drawStaticLayer() { ImDrawList *painter = ImGui::GetWindowDrawList(); - painter->AddRectFilled(layout_.rect.Min, layout_.rect.Max, ImGui::GetColorU32(ImGuiCol_ChildBg)); + painter->AddRectFilled(layout_.rect.Min, layout_.rect.Max, ImGui::GetColorU32(ImGuiCol_ChildBg), ImGui::GetStyle().ChildRounding); ImGui::SetCursorScreenPos(layout_.move_icon_rect.Min); ImGui::InvisibleButton("grip", layout_.move_icon_rect.GetSize()); if (ImGui::IsItemActivated()) charts_widget_->startChartDrag(this, ImGui::GetMousePos()); @@ -555,23 +554,11 @@ void ChartView::drawAxes() { ImPlot::PushStyleVar(ImPlotStyleVar_PlotPadding, ImVec2(LAYOUT_MARGINS.x, AXIS_X_TOP_MARGIN)); ImPlot::PushStyleColor(ImPlotCol_PlotBg, ImVec4(0, 0, 0, 0)); ImPlot::PushStyleColor(ImPlotCol_FrameBg, ImVec4(0, 0, 0, 0)); - // every tick is a 1 px line in the text color at alpha 50, the edge ticks close the box, no tick marks. - // that alpha washes out on the dark base, so the dark theme draws opaque guides in a mid gray instead. - const bool dark = isDarkTheme(); - ImVec4 grid_color; - if (dark) { - grid_color = colorRgb(DarkTheme::light.r, DarkTheme::light.g, DarkTheme::light.b); - } else { - grid_color = ImGui::GetStyleColorVec4(ImGuiCol_Text); - grid_color.w = 50.0f / 255.0f; - } - ImPlot::PushStyleColor(ImPlotCol_AxisGrid, grid_color); - ImPlot::PushStyleColor(ImPlotCol_PlotBorder, grid_color); + ImPlot::PushStyleColor(ImPlotCol_PlotBorder, palette().grid); ImPlot::PushStyleColor(ImPlotCol_AxisTick, ImVec4(0, 0, 0, 0)); ImPlot::PushStyleColor(ImPlotCol_AxisText, ImGui::GetStyleColorVec4(ImGuiCol_Text)); ImPlot::PushStyleVar(ImPlotStyleVar_MajorTickLen, ImVec2(0, 0)); - // MajorGridSize is the per-axis line thickness; thicker guides read better on the dark base - ImPlot::PushStyleVar(ImPlotStyleVar_MajorGridSize, dark ? ImVec2(2.0f, 2.0f) : ImVec2(1.0f, 1.0f)); + ImPlot::PushStyleVar(ImPlotStyleVar_MajorGridSize, ImVec2(1.0f, 1.0f)); const ImPlotFlags flags = ImPlotFlags_NoTitle | ImPlotFlags_NoLegend | ImPlotFlags_NoMenus | ImPlotFlags_NoMouseText | ImPlotFlags_NoBoxSelect | ImPlotFlags_NoInputs | ImPlotFlags_NoFrame; const ImPlotAxisFlags axis_flags = ImPlotAxisFlags_NoMenus | ImPlotAxisFlags_NoHighlight | ImPlotAxisFlags_NoSideSwitch | ImPlotAxisFlags_Lock; @@ -599,7 +586,7 @@ void ChartView::drawAxes() { drawForeground(); ImPlot::EndPlot(); } - ImPlot::PopStyleColor(6); + ImPlot::PopStyleColor(5); ImPlot::PopStyleVar(3); } @@ -632,7 +619,7 @@ void ChartView::drawLegend() { drawColorMarker(painter, r.Min, toImU32(s.color)); } - float x = r.Min.x + marker_size + 5; + float x = r.Min.x + marker_size + LEGEND_SPACING; 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); @@ -641,7 +628,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 + 5, 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 + LEGEND_SPACING, y), ImVec2(std::min(x + ImGui::CalcTextSize(msg.c_str()).x, r.Max.x), y), title_color); } } } @@ -722,19 +709,19 @@ void ChartView::drawRubberBandTimeRange() { ImDrawList *painter = ImPlot::GetPlotDrawList(); // ImGuiCol_Header is translucent, so the 1px selection outline is drawn at full alpha const ImU32 highlight = withAlpha(ImGui::GetColorU32(ImGuiCol_Header), 255); - painter->AddRectFilled(rubber_rect_.Min, rubber_rect_.Max, withAlpha(highlight, 50)); - painter->AddRect(rubber_rect_.Min, rubber_rect_.Max, highlight); + painter->AddRectFilled(rubber_rect_.Min, rubber_rect_.Max, withAlpha(highlight, 50), ImGui::GetStyle().FrameRounding); + painter->AddRect(rubber_rect_.Min, rubber_rect_.Max, highlight, ImGui::GetStyle().FrameRounding); // time labels at the bottom corners (below the plot, so clip to the widget instead of the plot) const ImU32 white = IM_COL32_WHITE; - const ImU32 gray = IM_COL32(0xa0, 0xa0, 0xa4, 0xff); + const ImU32 badge = ImGui::GetColorU32(palette().badge); painter = ImGui::GetWindowDrawList(); 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 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, gray); + 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->PopClipRect(); @@ -748,8 +735,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); - const bool dark = isDarkTheme(); - painter->AddRectFilled(time_str_pos, time_str_pos + time_str_size, dark ? IM_COL32(0x80, 0x80, 0x80, 0xff) : IM_COL32(0xa0, 0xa0, 0xa4, 0xff), 3.0f); + 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/chartswidget.cc b/openpilot/tools/cabana/ui/chart/chartswidget.cc index 546705d395..d10300cbe8 100644 --- a/openpilot/tools/cabana/ui/chart/chartswidget.cc +++ b/openpilot/tools/cabana/ui/chart/chartswidget.cc @@ -17,7 +17,6 @@ const int MAX_COLUMN_COUNT = 4; const int CHART_SPACING = 4; const int START_DRAG_DISTANCE = 10; -const float LAYOUT_HORIZONTAL_SPACING = 6.0f; const float MIN_RANGE_SLIDER_WIDTH = 40.0f; bool LogSlider::draw(const char *label, float width) { @@ -161,52 +160,44 @@ void ChartsWidget::setIsDocked(bool docked) { } void ChartsWidget::drawToolBar() { - beginToolbar(); float slider_width = 150.0f; const bool is_zoomed = can->timeRange().has_value(); // the labels are captured by reference, they outlive the draw calls below std::vector items; - items.push_back({toolbarButtonWidth(icon::PLUS_SQUARE), [this]() { - if (toolButton("new_plot_btn", icon::PLUS_SQUARE, "New Chart")) newChart(); + items.push_back({iconButtonWidth(), [this]() { + if (iconButton("new_plot_btn", icon::PLUS_LG, "New Chart")) newChart(); }}); - items.push_back({toolbarButtonWidth(icon::WINDOW_STACK), [this]() { - if (toolButton("new_tab_btn", icon::WINDOW_STACK, "New Tab")) newTab(); + 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 + LAYOUT_HORIZONTAL_SPACING, [&title_label]() { + items.push_back({ImGui::CalcTextSize(title_label.c_str()).x, [&title_label]() { ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted(title_label.c_str()); - ImGui::SameLine(0.0f, LAYOUT_HORIZONTAL_SPACING); - ImGui::Dummy(ImVec2(0.0f, 0.0f)); }}); const int type_count = (int)std::size(SERIES_TYPE_NAMES); const std::string chart_type_text = std::string("Type: ") + SERIES_TYPE_NAMES[std::clamp(settings.chart_series_type, 0, type_count - 1)]; - items.push_back({menuButtonWidth(chart_type_text), [this, &chart_type_text]() { - menuButton("chart_type", chart_type_text, "chart_type_menu"); - if (ImGui::BeginPopup("chart_type_menu")) { - for (int i = 0; i < type_count; ++i) { - if (ImGui::MenuItem(SERIES_TYPE_NAMES[i])) { - settings.chart_series_type = i; - settingChanged(); - } + auto chart_type_items = [this]() { + for (int i = 0; i < type_count; ++i) { + if (ImGui::MenuItem(SERIES_TYPE_NAMES[i], nullptr, settings.chart_series_type == i)) { + settings.chart_series_type = i; + settingChanged(); } - ImGui::EndPopup(); } - }}); + }; + items.push_back(toolbarMenu("chart_type", chart_type_text, "Type", chart_type_items)); const std::string columns_action_text = "Columns: " + std::to_string(column_count_); if (columns_action_visible_) { - items.push_back({menuButtonWidth(columns_action_text), [this, &columns_action_text]() { - menuButton("columns", columns_action_text, "columns_menu"); - if (ImGui::BeginPopup("columns_menu")) { - for (int i = 0; i < MAX_COLUMN_COUNT; ++i) { - if (ImGui::MenuItem(std::to_string(i + 1).c_str())) setColumnCount(i + 1); - } - ImGui::EndPopup(); + auto column_items = [this]() { + for (int i = 0; i < MAX_COLUMN_COUNT; ++i) { + if (ImGui::MenuItem(std::to_string(i + 1).c_str(), nullptr, column_count_ == i + 1)) setColumnCount(i + 1); } - }}); + }; + items.push_back(toolbarMenu("columns", columns_action_text, "Columns", column_items)); } // the spacer right aligns the rest @@ -221,36 +212,35 @@ void ChartsWidget::drawToolBar() { }}); slider_index = items.size(); items.push_back({slider_width, [this, &slider_width]() { - if (range_slider_.draw("##range_slider", slider_width)) setMaxChartRange(range_slider_.value()); + // 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; + if (range_slider_.draw("##range_slider", width)) setMaxChartRange(range_slider_.value()); ImGui::SetItemTooltip("Set the chart range"); }}); } else { char buf[64]; snprintf(buf, sizeof(buf), "%.2f-%.2f", can->timeRange()->first, can->timeRange()->second); reset_zoom_text = buf; - items.push_back({toolbarButtonWidth(icon::ARROW_COUNTERCLOCKWISE), [this]() { + items.push_back({iconButtonWidth(), [this]() { ImGui::BeginDisabled(!zoom_undo_stack_.canUndo()); - if (toolButton("undo_zoom", icon::ARROW_COUNTERCLOCKWISE, "Undo Zoom")) zoom_undo_stack_.undo(); + if (iconButton("undo_zoom", icon::ARROW_COUNTERCLOCKWISE, "Undo Zoom")) zoom_undo_stack_.undo(); ImGui::EndDisabled(); }}); - items.push_back({toolbarButtonWidth(icon::ARROW_CLOCKWISE), [this]() { + items.push_back({iconButtonWidth(), [this]() { ImGui::BeginDisabled(!zoom_undo_stack_.canRedo()); - if (toolButton("redo_zoom", icon::ARROW_CLOCKWISE, "Redo Zoom")) zoom_undo_stack_.redo(); + 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 (toolButton("reset_zoom_btn", icon::ZOOM_OUT, "Reset Zoom", reset_zoom_text.c_str())) zoomReset(); + if (ImGui::Button((std::string(icon::ZOOM_OUT) + " " + reset_zoom_text + "###reset_zoom_btn").c_str())) zoomReset(); + ImGui::SetItemTooltip("Reset Zoom"); }}); } - items.push_back({toolbarButtonWidth(icon::X_SQUARE), [this]() { - ImGui::BeginDisabled(charts_.empty()); - if (toolButton("remove_all_btn", icon::X_SQUARE, "Remove all charts")) removeAll(); - ImGui::EndDisabled(); - }}); - const char *dock_btn_icon = is_docked_ ? icon::ARROW_UP_RIGHT_SQUARE : icon::ARROW_DOWN_LEFT_SQUARE; - items.push_back({toolbarButtonWidth(dock_btn_icon), [this, dock_btn_icon]() { - if (toolButton("dock_btn", dock_btn_icon, is_docked_ ? "Float the charts window" : "Dock the charts window")) toggleChartsDocking(); - }}); + 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)); // the slider shrinks first, the buttons stay pinned to the right edge if (slider_index != (size_t)-1) { @@ -261,7 +251,6 @@ void ChartsWidget::drawToolBar() { } } drawToolbar(items, spacer_index); - endToolbar(); } void ChartsWidget::settingChanged() { @@ -619,7 +608,8 @@ void ChartsWidget::draw() { void ChartsContainer::draw() { ImGuiWindow *window = ImGui::GetCurrentWindow(); const ImVec2 start = ImGui::GetCursorScreenPos(); - geometry_ = ImRect(start, start + ImVec2(window->InnerRect.GetWidth(), 0)); + const float width_avail = window->InnerRect.GetWidth() - (window->ScrollbarY ? ImGui::GetStyle().ItemInnerSpacing.x : 0.0f); + geometry_ = ImRect(start, start + ImVec2(width_avail, 0)); charts_widget_->updateLayout(); const int n = std::max(charts_widget_->current_column_count_, 1); @@ -653,7 +643,7 @@ void ChartsContainer::drawDropIndicator() { r.Max.y = r.Min.y + h; } - ImGui::GetWindowDrawList()->AddRectFilled(r.Min, r.Max, ImGui::GetColorU32(ImGuiCol_Header)); + ImGui::GetWindowDrawList()->AddRectFilled(r.Min, r.Max, ImGui::GetColorU32(ImGuiCol_Header), ImGui::GetStyle().FrameRounding); } } diff --git a/openpilot/tools/cabana/ui/chart/signalselector.cc b/openpilot/tools/cabana/ui/chart/signalselector.cc index f0f427222e..cab1de0816 100644 --- a/openpilot/tools/cabana/ui/chart/signalselector.cc +++ b/openpilot/tools/cabana/ui/chart/signalselector.cc @@ -32,7 +32,7 @@ bool SignalSelector::draw() { return false; } - const float btn_w = ImGui::GetFrameHeight() + 8.0f; + const float btn_w = iconButtonWidth(); const float column_w = (ImGui::GetContentRegionAvail().x - btn_w - ImGui::GetStyle().ItemSpacing.x * 2) / 2; // the selected list spans the combo row too; both lists end above the Ok/Cancel row const float lists_h = ImGui::GetContentRegionAvail().y - ImGui::GetFrameHeightWithSpacing() * 3; @@ -67,10 +67,10 @@ bool SignalSelector::draw() { ImGui::BeginGroup(); ImGui::Dummy(ImVec2(btn_w, (lists_h + ImGui::GetFrameHeightWithSpacing() * 2) / 2 - ImGui::GetFrameHeight())); ImGui::BeginDisabled(available_row_ == -1); - bool add_clicked = ImGui::Button(icon::CHEVRON_RIGHT, ImVec2(btn_w, 0)); + bool add_clicked = iconButton("add", icon::CHEVRON_RIGHT, "Add"); ImGui::EndDisabled(); ImGui::BeginDisabled(selected_row_ == -1); - bool remove_clicked = ImGui::Button(icon::CHEVRON_LEFT, ImVec2(btn_w, 0)); + bool remove_clicked = iconButton("remove", icon::CHEVRON_LEFT, "Remove"); ImGui::EndDisabled(); ImGui::EndGroup(); diff --git a/openpilot/tools/cabana/ui/chart/tiplabel.cc b/openpilot/tools/cabana/ui/chart/tiplabel.cc index 896abb7479..68889b7886 100644 --- a/openpilot/tools/cabana/ui/chart/tiplabel.cc +++ b/openpilot/tools/cabana/ui/chart/tiplabel.cc @@ -59,11 +59,8 @@ void TipLabel::draw() { if (!visible_) return; ImDrawList *p = ImGui::GetForegroundDrawList(); - const bool dark = isDarkTheme(); - const ImU32 bg = dark ? ImGui::GetColorU32(ImGuiCol_PopupBg) : ImGui::GetColorU32(ImGuiCol_ChildBg); - const ImU32 fg = dark ? ImGui::GetColorU32(ImGuiCol_Text) : IM_COL32(0x40, 0x40, 0x44, 0xff); // filled panel with a 1px frame - p->AddRectFilled(pos_, pos_ + size_, bg); - p->AddRect(pos_, pos_ + size_, ImGui::GetColorU32(ImGuiCol_Border)); - layoutLines(p, pos_ + ImVec2(MARGIN, MARGIN), fg); + p->AddRectFilled(pos_, pos_ + size_, ImGui::GetColorU32(ImGuiCol_PopupBg), ImGui::GetStyle().PopupRounding); + p->AddRect(pos_, pos_ + size_, ImGui::GetColorU32(ImGuiCol_Border), ImGui::GetStyle().PopupRounding); + layoutLines(p, pos_ + ImVec2(MARGIN, MARGIN), ImGui::GetColorU32(ImGuiCol_Text)); } diff --git a/openpilot/tools/cabana/ui/helpoverlay.cc b/openpilot/tools/cabana/ui/helpoverlay.cc index 5ae3dab6f6..1fe061a382 100644 --- a/openpilot/tools/cabana/ui/helpoverlay.cc +++ b/openpilot/tools/cabana/ui/helpoverlay.cc @@ -170,9 +170,8 @@ void HelpOverlay::draw() { if (!work_rect.Contains(center)) continue; // a torn off panel is in another viewport const ImVec2 min(center.x - size.x * 0.5f - 8.0f, center.y - size.y * 0.5f - 8.0f); const ImVec2 max(center.x + size.x * 0.5f + 8.0f, center.y + size.y * 0.5f + 8.0f); - // pale yellow in the light theme - const ImU32 tooltip_base = isDarkTheme() ? ImGui::GetColorU32(ImGuiCol_PopupBg) : IM_COL32(255, 255, 220, 255); - dl->AddRectFilled(min, max, tooltip_base); + dl->AddRectFilled(min, max, ImGui::GetColorU32(ImGuiCol_PopupBg), ImGui::GetStyle().PopupRounding); + dl->AddRect(min, max, ImGui::GetColorU32(ImGuiCol_Border), ImGui::GetStyle().PopupRounding); float y = min.y + 8.0f; for (const auto &line : lines) { float x = min.x + 8.0f; @@ -182,7 +181,7 @@ void HelpOverlay::draw() { if (r.swatch) { dl->AddRectFilled(ImVec2(x + 2, y + 3), ImVec2(x + font_size - 2, y + font_size - 1), color); } else { - if (r.chip) dl->AddRectFilled(ImVec2(x, y), ImVec2(x + w, y + font_size), IM_COL32(211, 211, 211, 255)); // lightGray + if (r.chip) dl->AddRectFilled(ImVec2(x, y), ImVec2(x + w, y + font_size), ImGui::GetColorU32(ImGuiCol_Button), 3.0f); dl->AddText(r.bold ? bold_font : font, font_size, ImVec2(x, y), color, r.text.c_str()); } x += w; diff --git a/openpilot/tools/cabana/ui/icons.h b/openpilot/tools/cabana/ui/icons.h index 7a61d02c09..89b8300862 100644 --- a/openpilot/tools/cabana/ui/icons.h +++ b/openpilot/tools/cabana/ui/icons.h @@ -1,42 +1,40 @@ #pragma once -// bootstrap icon glyphs, merged into the fonts by style.cc +// Bootstrap Icons codepoints; loadFonts() merges the icon font into each text font. namespace icon { constexpr const char ARROW_CLOCKWISE[] = "\xef\x84\x96"; constexpr const char ARROW_COUNTERCLOCKWISE[] = "\xef\x84\x97"; -constexpr const char ARROW_DOWN_LEFT_SQUARE[] = "\xef\x84\x9c"; -constexpr const char ARROW_UP_RIGHT_SQUARE[] = "\xef\x85\x82"; constexpr const char CHEVRON_LEFT[] = "\xef\x8a\x84"; constexpr const char CHEVRON_RIGHT[] = "\xef\x8a\x85"; constexpr const char ASPECT_RATIO[] = "\xef\x85\x90"; constexpr const char ASPECT_RATIO_FILL[] = "\xef\x85\x8f"; -constexpr const char DASH[] = "\xef\x8b\xaa"; -constexpr const char DASH_SQUARE[] = "\xef\x8b\xa8"; constexpr const char EXCLAMATION_TRIANGLE[] = "\xef\x8c\xba"; constexpr const char FAST_FORWARD[] = "\xef\x9f\xb3"; constexpr const char FILETYPE_CSV[] = "\xef\x9d\x83"; constexpr const char FOLDER[] = "\xef\x8f\x91"; constexpr const char FILE_EARMARK[] = "\xef\x8d\xa9"; constexpr const char FILE_EARMARK_RULED[] = "\xef\x8e\x84"; -constexpr const char PLUS_SQUARE[] = "\xef\x93\xbc"; constexpr const char GRAPH_UP[] = "\xef\x8f\xb2"; constexpr const char GRIP_HORIZONTAL[] = "\xef\x8f\xbd"; constexpr const char INFO_CIRCLE[] = "\xef\x90\xb0"; -constexpr const char LIST[] = "\xef\x91\xb9"; constexpr const char PAUSE[] = "\xef\x93\x83"; constexpr const char PENCIL[] = "\xef\x93\x8b"; constexpr const char PLAY[] = "\xef\x93\xb4"; -constexpr const char PLUS[] = "\xef\x93\xbe"; -constexpr const char RAQUO[] = "\xc2\xbb"; // U+00BB, not a bootstrap icon: the toolbar extension button constexpr const char REPEAT[] = "\xef\xa0\x93"; constexpr const char REPEAT_1[] = "\xef\xa0\x92"; constexpr const char REWIND[] = "\xef\xa0\x98"; constexpr const char SKIP_END[] = "\xef\x95\x97"; constexpr const char STOPWATCH[] = "\xef\x96\x96"; constexpr const char THREE_DOTS[] = "\xef\x97\x94"; -constexpr const char WINDOW_STACK[] = "\xef\x9b\x92"; -constexpr const char X[] = "\xef\x98\xaa"; constexpr const char X_LG[] = "\xef\x99\x99"; -constexpr const char X_SQUARE[] = "\xef\x98\xa8"; constexpr const char ZOOM_OUT[] = "\xef\x98\xad"; +constexpr const char PLUS_LG[] = "\xef\x99\x8d"; +constexpr const char DASH_LG[] = "\xef\x98\xbb"; +constexpr const char TRASH[] = "\xef\x9e\x8b"; +constexpr const char ARROWS_COLLAPSE[] = "\xef\x85\x8b"; +constexpr const char BOX_ARROW_UP_RIGHT[] = "\xef\x87\x85"; +constexpr const char BOX_ARROW_IN_DOWN_LEFT[] = "\xef\x86\xba"; +constexpr const char WINDOW_PLUS[] = "\xef\x9b\x90"; +constexpr const char CHEVRON_DOUBLE_RIGHT[] = "\xef\x8a\x80"; +constexpr const char THREE_DOTS_VERTICAL[] = "\xef\x97\x93"; } // namespace icon diff --git a/openpilot/tools/cabana/ui/mainwin.cc b/openpilot/tools/cabana/ui/mainwin.cc index a2969defcc..e5490432a2 100644 --- a/openpilot/tools/cabana/ui/mainwin.cc +++ b/openpilot/tools/cabana/ui/mainwin.cc @@ -132,14 +132,46 @@ void MainWindow::drawFileMenu() { if (ImGui::MenuItem("Exit", "Ctrl+Q")) close(); } +namespace { +bool beginTopMenu(const char *label, bool enabled = true) { + ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImGui::GetColorU32(ImGuiCol_Header)); + const bool open = ImGui::BeginMenu(label, enabled); + ImGui::PopStyleColor(); + if (open) { + // Erase the popup's rounded top border so it joins the menu bar's separator. + const ImGuiStyle &style = ImGui::GetStyle(); + const ImGuiWindow *w = ImGui::GetCurrentWindow(); + const float r = style.PopupRounding, b = style.PopupBorderSize; + const ImVec2 min = w->Pos, max(w->Pos.x + w->Size.x, w->Pos.y + w->Size.y); + const ImU32 bg = ImGui::GetColorU32(ImGuiCol_PopupBg), border = ImGui::GetColorU32(ImGuiCol_Border); + ImDrawList *dl = w->DrawList; + dl->PushClipRect(min, max, false); // the window's own clip rect excludes its border + dl->AddRectFilled(min, ImVec2(max.x, min.y + r), bg); + dl->AddRectFilled(ImVec2(min.x, min.y), ImVec2(min.x + b, min.y + r), border); + dl->AddRectFilled(ImVec2(max.x - b, min.y), ImVec2(max.x, min.y + r), border); + dl->PopClipRect(); + } + return open; +} +} // namespace + void MainWindow::drawMenuBar() { - if (!ImGui::BeginMainMenuBar()) return; - if (ImGui::BeginMenu("File")) { + // Avoid a double border with the separator drawn below. + ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f); + const bool open = ImGui::BeginMainMenuBar(); + ImGui::PopStyleVar(); + if (!open) return; + { + const ImVec2 min = ImGui::GetWindowPos(); + const ImVec2 max(min.x + ImGui::GetWindowWidth(), min.y + ImGui::GetWindowHeight()); + ImGui::GetWindowDrawList()->AddRectFilled(ImVec2(min.x, max.y - 1.0f), max, ImGui::GetColorU32(ImGuiCol_Border)); + } + if (beginTopMenu("File")) { drawFileMenu(); ImGui::EndMenu(); } - if (ImGui::BeginMenu("Edit")) { + if (beginTopMenu("Edit")) { auto stack = UndoStack::instance(); const std::string undo_text = stack->canUndo() ? "Undo " + stack->undoText() : "Undo"; const std::string redo_text = stack->canRedo() ? "Redo " + stack->redoText() : "Redo"; @@ -148,7 +180,7 @@ void MainWindow::drawMenuBar() { ImGui::EndMenu(); } - if (ImGui::BeginMenu("View")) { + if (beginTopMenu("View")) { if (ImGui::MenuItem("Full Screen", "Ctrl+F11")) toggleFullScreen(); ImGui::Separator(); ImGui::MenuItem(messages_widget_ ? messages_widget_->title().c_str() : "MESSAGES", nullptr, &messages_visible_); @@ -162,13 +194,13 @@ void MainWindow::drawMenuBar() { ImGui::EndMenu(); } - if (ImGui::BeginMenu("Tools", hasStream())) { + if (beginTopMenu("Tools", hasStream())) { if (ImGui::MenuItem("Find Similar Bits")) findSimilarBits(); if (ImGui::MenuItem("Find Signal")) findSignal(); ImGui::EndMenu(); } - if (ImGui::BeginMenu("Help")) { + if (beginTopMenu("Help")) { if (ImGui::MenuItem("Help", "F1")) toggleHelp(); ImGui::EndMenu(); } @@ -710,6 +742,8 @@ void MainWindow::handleShortcuts() { void MainWindow::drawStatusBar() { ImGui::PushStyleColor(ImGuiCol_ChildBg, ImGui::GetStyle().Colors[ImGuiCol_MenuBarBg]); ImGui::BeginChild("status_bar", ImVec2(0, ImGui::GetFrameHeight()), ImGuiChildFlags_None, ImGuiWindowFlags_NoScrollbar); + const ImVec2 min = ImGui::GetWindowPos(); + ImGui::GetWindowDrawList()->AddRectFilled(min, ImVec2(min.x + ImGui::GetWindowWidth(), min.y + 1.0f), ImGui::GetColorU32(ImGuiCol_Border)); // a borderless child gets no WindowPadding, so both ends sit flush against the edge and clip. Inset by // WindowPadding.x, which lines the text up with the content of the docked panels above (the messages table). const float width = ImGui::GetContentRegionAvail().x; @@ -771,6 +805,8 @@ void MainWindow::drawDockspace() { // the status bar sits below the dockspace: reserve its height plus the item spacing between the two, // otherwise the host window is a few pixels taller than the viewport and scrolls const float status_height = full_screen_ ? 0.0f : ImGui::GetFrameHeight() + ImGui::GetStyle().ItemSpacing.y; + const float top_gap = full_screen_ ? 0.0f : ImGui::GetStyle().ItemSpacing.y; + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + top_gap); const ImVec2 dock_size(ImGui::GetContentRegionAvail().x, ImGui::GetContentRegionAvail().y - status_height); const ImGuiID dock_id = ImGui::GetID("cabana_dockspace"); if (reset_layout_ || ImGui::DockBuilderGetNode(dock_id) == nullptr) { @@ -810,12 +846,19 @@ void setNextPanelClass() { window_class.DockNodeFlagsOverrideSet = ImGuiDockNodeFlags_NoWindowMenuButton; ImGui::SetNextWindowClass(&window_class); } + +bool beginPanel(const char *name, bool *open, ImGuiWindowFlags flags = 0) { + ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f); + const bool visible = ImGui::Begin(name, open, flags); + ImGui::PopStyleVar(); + return visible; +} } // namespace void MainWindow::drawMessagesPanel() { const std::string name = messages_widget_->title() + MESSAGES_PANEL_ID; setNextPanelClass(); - if (ImGui::Begin(name.c_str(), &messages_visible_)) { + if (beginPanel(name.c_str(), &messages_visible_)) { help_overlay_.add(messages_widget_->whatsThis(), ImGui::GetCurrentWindow()->Rect()); messages_widget_->draw(); } @@ -827,7 +870,7 @@ void MainWindow::drawMessagesPanel() { void MainWindow::drawVideoPanel() { const std::string name = video_dock_title_ + VIDEO_PANEL; setNextPanelClass(); - const bool video_open = ImGui::Begin(name.c_str(), &video_visible_); + const bool video_open = beginPanel(name.c_str(), &video_visible_); const bool floating = floatingOut(); if (!video_open) { video_widget_->setVisible(false); // the dock is collapsed or tabbed behind another one, like hideEvent @@ -841,11 +884,23 @@ void MainWindow::drawVideoPanel() { const float video_hint = video_splitter_ratio_ >= 0.0f ? avail.y * video_splitter_ratio_ : default_h; float video_h = charts_floating_ ? avail.y : std::clamp(video_hint, 0.0f, avail.y - 1.0f); if (live) video_h = default_h; // display video at minimum size. - // dragging below half of the minimum size collapses the video, it never shrinks below it otherwise + // Collapse panes below half their minimum height to keep partially clipped controls out of view. + bool charts_collapsed = false; + const float splitter_h = ImGui::GetStyle().WindowPadding.x * 2.0f + 2.0f; if (!charts_floating_ && !live) { const float min_h = std::min(video_widget_->sizeHintHeight() + video_padding, avail.y - 1.0f); video_h = video_h < min_h / 2 ? 0.0f : std::max(video_h, min_h); + const float charts_min_h = ImGui::GetFrameHeight() + video_padding + ImGui::GetStyle().ChildBorderSize * 2.0f; + const float charts_h = avail.y - video_h - splitter_h; + if (charts_h < charts_min_h / 2) { + charts_collapsed = true; + video_h = avail.y - splitter_h; + } else if (charts_h < charts_min_h) { + video_h = avail.y - splitter_h - charts_min_h; + } } + // The splitter provides the gap; extra ItemSpacing would leave an undraggable strip. + if (!charts_floating_) 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()); @@ -855,21 +910,26 @@ void MainWindow::drawVideoPanel() { video_widget_->setVisible(false); // the splitter collapsed the video: stop the vipc thread } if (!charts_floating_) { - // the gap between the video and the charts is the same as the padding at the sides - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(ImGui::GetStyle().ItemSpacing.x, 0.0f)); - ImGui::InvisibleButton("##splitter", ImVec2(-1.0f, ImGui::GetStyle().WindowPadding.x)); - if (ImGui::IsItemActive() && !live) { + ImGui::InvisibleButton("##splitter", ImVec2(-1.0f, splitter_h)); + const bool splitter_hovered = ImGui::IsItemHovered() && !live, splitter_active = ImGui::IsItemActive() && !live; + if (splitter_active) { // the size of the video is the position of the handle inside the splitter const float top = ImGui::GetWindowPos().y + ImGui::GetCursorStartPos().y; video_splitter_ratio_ = std::clamp((ImGui::GetMousePos().y - top) / avail.y, 0.0f, 1.0f); } - if (ImGui::IsItemHovered() && !live) ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeNS); - // the chart list scrolls in its own child, the container itself never scrolls - ImGui::BeginChild("charts", ImVec2(0, 0), ImGuiChildFlags_Borders, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse); + if (splitter_hovered) ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeNS); + const ImRect splitter(ImGui::GetItemRectMin(), ImGui::GetItemRectMax()); + const float line_y = std::floor(splitter.GetCenter().y) - 1.0f; + ImGui::GetWindowDrawList()->AddRectFilled(ImVec2(splitter.Min.x, line_y), ImVec2(splitter.Max.x, line_y + 2.0f), + ImGui::GetColorU32(splitter_active ? ImGuiCol_SeparatorActive : splitter_hovered ? ImGuiCol_SeparatorHovered : ImGuiCol_Border)); ImGui::PopStyleVar(); - help_overlay_.add(charts_widget_->whatsThis(), ImGui::GetCurrentWindow()->Rect()); - charts_widget_->draw(); - ImGui::EndChild(); + if (!charts_collapsed) { + // the chart list scrolls in its own child, the container itself never scrolls + ImGui::BeginChild("charts", ImVec2(0, 0), ImGuiChildFlags_Borders, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse); + help_overlay_.add(charts_widget_->whatsThis(), ImGui::GetCurrentWindow()->Rect()); + charts_widget_->draw(); + ImGui::EndChild(); + } } } ImGui::End(); @@ -893,11 +953,13 @@ void MainWindow::draw() { drawDockspace(); // the central widget has no scrollbars of its own (the views inside scroll) - if (ImGui::Begin(CENTER_PANEL, nullptr, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse)) { + if (beginPanel(CENTER_PANEL, nullptr, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse)) { + ImGui::BeginChild("center", ImVec2(0, 0), ImGuiChildFlags_Borders, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse); center_widget_.draw(); if (auto *detail = center_widget_.getDetailWidget(); detail && help_overlay_.visible()) { for (const auto &[text, rect] : detail->helpRects()) help_overlay_.add(text, rect); } + ImGui::EndChild(); } ImGui::End(); if (messages_widget_ && messages_visible_) drawMessagesPanel(); diff --git a/openpilot/tools/cabana/ui/style.cc b/openpilot/tools/cabana/ui/style.cc deleted file mode 100644 index a006dbc66d..0000000000 --- a/openpilot/tools/cabana/ui/style.cc +++ /dev/null @@ -1,284 +0,0 @@ -#include "tools/cabana/ui/app.h" - -#include -#include -#include - -#include "implot.h" -#include "tools/cabana/core/settings.h" -#include "tools/cabana/settings.h" -#include "tools/cabana/ui/util.h" -#include "tools/cabana/utils/util.h" - -namespace fs = std::filesystem; - -namespace { -bool g_dark = false; -ImFont *g_ui_font = nullptr; -ImFont *g_bold_font = nullptr; -ImFont *g_mono_font = nullptr; -ImFont *g_large_font = nullptr; - -void addIconFont(float size, ImFont *base) { - ImFontConfig cfg; - cfg.MergeMode = base != nullptr; - cfg.GlyphMinAdvanceX = size; - if (base != nullptr) { - ImFontBaked *baked = base->GetFontBaked(size); - const float center = baked != nullptr ? (baked->Ascent + baked->Descent) * 0.5f : size * 0.5f; - cfg.GlyphOffset.y = std::round(size * 0.5f - center); - } - static const ImWchar ranges[] = {0xF000, 0xF8FF, 0}; - ImGui::GetIO().Fonts->AddFontFromFileTTF(BOOTSTRAP_ICONS_TTF, size, &cfg, ranges); -} - -ImFont *addFont(const fs::path &path, float size) { - ImFontConfig cfg; - cfg.OversampleH = 2; - cfg.OversampleV = 2; - ImFont *font = ImGui::GetIO().Fonts->AddFontFromFileTTF(path.c_str(), size, &cfg); - if (font != nullptr) addIconFont(size, font); - return font; -} -} // namespace - -void loadFonts() { - ImGuiIO &io = ImGui::GetIO(); - const fs::path fonts = fs::path(CABANA_FONTS_DIR); - g_ui_font = addFont(fonts / "Inter-Regular.ttf", 16.0f); - g_bold_font = addFont(fonts / "Inter-SemiBold.ttf", 16.0f); - g_mono_font = addFont(fonts / "JetBrainsMono-Medium.ttf", 15.0f); - g_large_font = addFont(fonts / "Inter-Bold.ttf", 50.0f); - if (g_ui_font != nullptr) io.FontDefault = g_ui_font; - if (g_bold_font == nullptr) g_bold_font = g_ui_font; - if (g_mono_font == nullptr) g_mono_font = g_ui_font; - if (g_large_font == nullptr) g_large_font = g_bold_font; -} - -void applyTheme(int theme) { - const bool dark = theme == DARK_THEME; - g_dark = dark; - if (dark) { - ImGui::StyleColorsDark(); - ImPlot::StyleColorsDark(); - } else { - ImGui::StyleColorsLight(); - ImPlot::StyleColorsLight(); - } - - ImGuiStyle &style = ImGui::GetStyle(); - style.WindowRounding = 0.0f; - style.ChildRounding = 0.0f; - style.PopupRounding = 0.0f; - style.FrameRounding = 2.0f; - style.GrabRounding = 2.0f; - style.ScrollbarRounding = 2.0f; - style.TabRounding = 2.0f; - style.WindowBorderSize = 1.0f; - style.FrameBorderSize = 1.0f; - style.TabBorderSize = 1.0f; - style.WindowPadding = ImVec2(8.0f, 7.0f); - style.FramePadding = ImVec2(6.0f, 3.0f); - style.ItemSpacing = ImVec2(8.0f, 5.0f); - style.ScrollbarSize = 14.0f; - style.GrabMinSize = 13.0f; - - auto c = [](const CabanaColor &col, float a = 1.0f) { return colorRgb(col.r, col.g, col.b, a); }; - ImVec4 *colors = style.Colors; - if (dark) { - const ImVec4 highlight = c(DarkTheme::highlight); - const ImVec4 outline = colorRgb(0x26, 0x26, 0x26); - colors[ImGuiCol_WindowBg] = c(DarkTheme::window); - colors[ImGuiCol_ChildBg] = c(DarkTheme::base); - colors[ImGuiCol_PopupBg] = c(DarkTheme::window); - colors[ImGuiCol_MenuBarBg] = c(DarkTheme::window); - colors[ImGuiCol_DockingEmptyBg] = c(DarkTheme::window); - colors[ImGuiCol_Text] = c(DarkTheme::text); - colors[ImGuiCol_TextDisabled] = c(DarkTheme::disabled_text); - colors[ImGuiCol_Border] = outline; - colors[ImGuiCol_BorderShadow] = colorRgb(0, 0, 0, 0.0f); - colors[ImGuiCol_FrameBg] = c(DarkTheme::base); - colors[ImGuiCol_FrameBgHovered] = colorRgb(0x1f, 0x1f, 0x1f); - colors[ImGuiCol_FrameBgActive] = colorRgb(0x24, 0x24, 0x24); - colors[ImGuiCol_Button] = colorRgb(0x5e, 0x5e, 0x5e); - colors[ImGuiCol_ButtonHovered] = colorRgb(0x6a, 0x6a, 0x6a); - colors[ImGuiCol_ButtonActive] = colorRgb(0x52, 0x52, 0x52); - colors[ImGuiCol_Header] = highlight; - colors[ImGuiCol_HeaderHovered] = c(DarkTheme::highlight, 0.8f); - colors[ImGuiCol_HeaderActive] = highlight; - colors[ImGuiCol_CheckMark] = c(DarkTheme::bright_text); - colors[ImGuiCol_SliderGrab] = colorRgb(0x6a, 0x6a, 0x6a); - colors[ImGuiCol_SliderGrabActive] = colorRgb(0x80, 0x80, 0x80); - colors[ImGuiCol_ScrollbarBg] = c(DarkTheme::window); - colors[ImGuiCol_ScrollbarGrab] = colorRgb(0x5a, 0x5a, 0x5a); - colors[ImGuiCol_ScrollbarGrabHovered] = colorRgb(0x6a, 0x6a, 0x6a); - colors[ImGuiCol_ScrollbarGrabActive] = colorRgb(0x7a, 0x7a, 0x7a); - colors[ImGuiCol_Separator] = outline; - colors[ImGuiCol_SeparatorHovered] = c(DarkTheme::highlight, 0.6f); - colors[ImGuiCol_SeparatorActive] = highlight; - colors[ImGuiCol_ResizeGrip] = colorRgb(0, 0, 0, 0.0f); - colors[ImGuiCol_ResizeGripHovered] = c(DarkTheme::highlight, 0.6f); - colors[ImGuiCol_ResizeGripActive] = highlight; - colors[ImGuiCol_Tab] = colorRgb(0x2c, 0x2c, 0x2c); - colors[ImGuiCol_TabHovered] = colorRgb(0x3a, 0x3a, 0x3a); - colors[ImGuiCol_TabSelected] = c(DarkTheme::base); - colors[ImGuiCol_TabSelectedOverline] = highlight; - colors[ImGuiCol_TabDimmed] = colorRgb(0x2c, 0x2c, 0x2c); - colors[ImGuiCol_TabDimmedSelected] = c(DarkTheme::base); - colors[ImGuiCol_TabDimmedSelectedOverline] = colorRgb(0, 0, 0, 0.0f); - colors[ImGuiCol_TitleBg] = c(DarkTheme::window); - colors[ImGuiCol_TitleBgActive] = c(DarkTheme::window); - colors[ImGuiCol_TitleBgCollapsed] = c(DarkTheme::window); - colors[ImGuiCol_TableHeaderBg] = c(DarkTheme::window); - colors[ImGuiCol_TableBorderStrong] = outline; - colors[ImGuiCol_TableBorderLight] = colorRgb(0x2c, 0x2c, 0x2c); - colors[ImGuiCol_TableRowBg] = colorRgb(0, 0, 0, 0.0f); - colors[ImGuiCol_TableRowBgAlt] = colorRgb(0xff, 0xff, 0xff, 0.06f); - colors[ImGuiCol_TextSelectedBg] = c(DarkTheme::highlight, 0.6f); - colors[ImGuiCol_DockingPreview] = c(DarkTheme::highlight, 0.5f); - colors[ImGuiCol_NavCursor] = highlight; - colors[ImGuiCol_PlotLines] = c(DarkTheme::text); - colors[ImGuiCol_PlotHistogram] = highlight; - colors[ImGuiCol_DragDropTarget] = highlight; - } else { - const ImVec4 window = colorRgb(0xef, 0xef, 0xef); - const ImVec4 base = colorRgb(0xff, 0xff, 0xff); - const ImVec4 outline = colorRgb(0xab, 0xab, 0xab); - const ImVec4 highlight = colorRgb(0x30, 0x8c, 0xc6); - colors[ImGuiCol_WindowBg] = window; - colors[ImGuiCol_ChildBg] = base; - colors[ImGuiCol_PopupBg] = colorRgb(0xf8, 0xf8, 0xf8); - colors[ImGuiCol_MenuBarBg] = window; - colors[ImGuiCol_DockingEmptyBg] = window; - colors[ImGuiCol_Text] = colorRgb(0x00, 0x00, 0x00); - colors[ImGuiCol_TextDisabled] = colorRgb(0xbe, 0xbe, 0xbe); - colors[ImGuiCol_Border] = outline; - colors[ImGuiCol_BorderShadow] = colorRgb(0, 0, 0, 0.0f); - colors[ImGuiCol_FrameBg] = base; - colors[ImGuiCol_FrameBgHovered] = colorRgb(0xf7, 0xf7, 0xf7); - colors[ImGuiCol_FrameBgActive] = colorRgb(0xef, 0xef, 0xef); - colors[ImGuiCol_Button] = colorRgb(0xe4, 0xe4, 0xe4); - colors[ImGuiCol_ButtonHovered] = colorRgb(0xec, 0xec, 0xec); - colors[ImGuiCol_ButtonActive] = colorRgb(0xd0, 0xd0, 0xd0); - colors[ImGuiCol_Header] = highlight; - colors[ImGuiCol_HeaderHovered] = colorRgb(0x30, 0x8c, 0xc6, 0.8f); - colors[ImGuiCol_HeaderActive] = highlight; - colors[ImGuiCol_CheckMark] = colorRgb(0x3b, 0x3b, 0x3b); - colors[ImGuiCol_SliderGrab] = colorRgb(0xd8, 0xd8, 0xd8); - colors[ImGuiCol_SliderGrabActive] = colorRgb(0xc4, 0xc4, 0xc4); - colors[ImGuiCol_ScrollbarBg] = window; - colors[ImGuiCol_ScrollbarGrab] = colorRgb(0xc8, 0xc8, 0xc8); - colors[ImGuiCol_ScrollbarGrabHovered] = colorRgb(0xb4, 0xb4, 0xb4); - colors[ImGuiCol_ScrollbarGrabActive] = colorRgb(0xa0, 0xa0, 0xa0); - colors[ImGuiCol_Separator] = outline; - colors[ImGuiCol_SeparatorHovered] = colorRgb(0x30, 0x8c, 0xc6, 0.6f); - colors[ImGuiCol_SeparatorActive] = highlight; - colors[ImGuiCol_ResizeGrip] = colorRgb(0, 0, 0, 0.0f); - colors[ImGuiCol_ResizeGripHovered] = colorRgb(0x30, 0x8c, 0xc6, 0.6f); - colors[ImGuiCol_ResizeGripActive] = highlight; - colors[ImGuiCol_Tab] = colorRgb(0xdc, 0xdc, 0xdc); - colors[ImGuiCol_TabHovered] = colorRgb(0xf5, 0xf5, 0xf5); - colors[ImGuiCol_TabSelected] = base; - colors[ImGuiCol_TabSelectedOverline] = highlight; - colors[ImGuiCol_TabDimmed] = colorRgb(0xdc, 0xdc, 0xdc); - colors[ImGuiCol_TabDimmedSelected] = base; - colors[ImGuiCol_TabDimmedSelectedOverline] = colorRgb(0, 0, 0, 0.0f); - colors[ImGuiCol_TitleBg] = window; - colors[ImGuiCol_TitleBgActive] = window; - colors[ImGuiCol_TitleBgCollapsed] = window; - colors[ImGuiCol_TableHeaderBg] = colorRgb(0xf2, 0xf2, 0xf2); - colors[ImGuiCol_TableBorderStrong] = outline; - colors[ImGuiCol_TableBorderLight] = colorRgb(0xc7, 0xc7, 0xc7); - colors[ImGuiCol_TableRowBg] = colorRgb(0, 0, 0, 0.0f); - colors[ImGuiCol_TableRowBgAlt] = colorRgb(0, 0, 0, 0.03f); - colors[ImGuiCol_TextSelectedBg] = colorRgb(0x30, 0x8c, 0xc6, 0.35f); - colors[ImGuiCol_DockingPreview] = colorRgb(0x30, 0x8c, 0xc6, 0.5f); - colors[ImGuiCol_NavCursor] = highlight; - colors[ImGuiCol_PlotLines] = colorRgb(0x3b, 0x3b, 0x3b); - colors[ImGuiCol_PlotHistogram] = highlight; - colors[ImGuiCol_DragDropTarget] = highlight; - } - // imgui fades the modal dim in over several frames, which reads as the dialog lagging - colors[ImGuiCol_ModalWindowDimBg] = ImVec4(0, 0, 0, 0); - colors[ImGuiCol_NavWindowingDimBg] = ImVec4(0, 0, 0, 0); -} - -bool isDarkTheme() { return g_dark; } - -CabanaColor signalFillColor(const CabanaColor &c) { - if (!g_dark) return c; - auto [h, s, v] = c.hsv(); - return CabanaColor::fromHsv(h, std::min(1.0f, s * 1.4f), v * 0.8f, c.a / 255.0f); -} - -ImU32 highlightedTextColor() { - return g_dark ? IM_COL32(DarkTheme::window_text.r, DarkTheme::window_text.g, DarkTheme::window_text.b, 255) - : IM_COL32(255, 255, 255, 255); -} - -ImU32 paletteBrightText() { - return g_dark ? IM_COL32(DarkTheme::bright_text.r, DarkTheme::bright_text.g, DarkTheme::bright_text.b, 255) - : IM_COL32(255, 255, 255, 255); -} - -void drawSliderHandle(ImDrawList *p, const ImRect &r) { - const bool dark = isDarkTheme(); - const ImU32 top = dark ? IM_COL32(0x41, 0x41, 0x41, 255) : IM_COL32(255, 255, 255, 255); - const ImU32 bottom = dark ? IM_COL32(0x36, 0x36, 0x36, 255) : IM_COL32(0xf0, 0xf0, 0xf0, 255); - // the top/left edge is one step lighter than the bottom/right edge - const ImU32 outline_top = dark ? IM_COL32(0x5c, 0x5c, 0x5c, 255) : IM_COL32(0xab, 0xab, 0xab, 255); - const ImU32 outline_bottom = dark ? IM_COL32(0x26, 0x26, 0x26, 255) : IM_COL32(0xa4, 0xa4, 0xa4, 255); - p->AddRectFilled(r.Min, r.Max, top, 2.0f); - p->AddRectFilled(ImVec2(r.Min.x, r.GetCenter().y), r.Max, bottom, 2.0f, ImDrawFlags_RoundCornersBottom); - p->AddRect(r.Min, r.Max, outline_bottom, 2.0f, 0, 1.0f); - // the straight edges are drawn as crisp 1 px rects: an antialiased outline washes out to a much lighter grey - const float c = 2.0f; // corner radius - p->AddRectFilled(ImVec2(r.Min.x + c, r.Min.y), ImVec2(r.Max.x - c, r.Min.y + 1.0f), outline_top); - p->AddRectFilled(ImVec2(r.Min.x, r.Min.y + c), ImVec2(r.Min.x + 1.0f, r.Max.y - c), outline_top); - p->AddRectFilled(ImVec2(r.Min.x + c, r.Max.y - 1.0f), ImVec2(r.Max.x - c, r.Max.y), outline_bottom); - p->AddRectFilled(ImVec2(r.Max.x - 1.0f, r.Min.y + c), ImVec2(r.Max.x, r.Max.y - c), outline_bottom); -} - -bool fusionSliderInt(const char *label, int *v, int min, int max, float width) { - // a grey groove over the full width with the part left of the handle filled, and a 13x13 handle on top - const ImU32 groove_col = isDarkTheme() ? IM_COL32(0x2b, 0x2b, 0x2b, 255) : IM_COL32(0xc4, 0xc4, 0xc4, 255); - const ImU32 fill_col = ImGui::GetColorU32(ImGuiCol_Header); - ImGui::PushStyleColor(ImGuiCol_FrameBg, IM_COL32_BLACK_TRANS); - ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, IM_COL32_BLACK_TRANS); - ImGui::PushStyleColor(ImGuiCol_FrameBgActive, IM_COL32_BLACK_TRANS); - ImGui::PushStyleColor(ImGuiCol_SliderGrab, IM_COL32_BLACK_TRANS); - ImGui::PushStyleColor(ImGuiCol_SliderGrabActive, IM_COL32_BLACK_TRANS); - ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f); // the slider has no frame - ImGui::SetNextItemWidth(width); - bool changed = ImGui::SliderInt(label, v, min, max, "", ImGuiSliderFlags_NoInput); - ImGui::PopStyleVar(); - ImGui::PopStyleColor(5); - - const ImVec2 bb_min = ImGui::GetItemRectMin(), bb_max = ImGui::GetItemRectMax(); - const float cy = (bb_min.y + bb_max.y) * 0.5f; - const float groove_h = SLIDER_THICKNESS * 0.5f; - const float handle_h = std::min(SLIDER_THICKNESS, bb_max.y - bb_min.y); - const float x0 = bb_min.x + SLIDER_LENGTH * 0.5f, x1 = bb_max.x - SLIDER_LENGTH * 0.5f; - const float t = max > min ? (float)(*v - min) / (float)(max - min) : 0.0f; - const float hx = x0 + (x1 - x0) * t; - ImDrawList *dl = ImGui::GetWindowDrawList(); - const float groove_y0 = cy - groove_h * 0.5f, groove_y1 = cy + groove_h * 0.5f; - dl->AddRectFilled(ImVec2(bb_min.x, groove_y0), ImVec2(bb_max.x, groove_y1), groove_col, groove_h * 0.5f); - dl->AddRectFilled(ImVec2(bb_min.x, groove_y0), ImVec2(hx, groove_y1), fill_col, groove_h * 0.5f); - drawSliderHandle(dl, ImRect(ImVec2(hx - SLIDER_LENGTH * 0.5f, cy - handle_h * 0.5f), - ImVec2(hx + SLIDER_LENGTH * 0.5f, cy + handle_h * 0.5f))); - return changed; -} - -ImFont *boldFont() { return g_bold_font; } -ImFont *monoFont() { return g_mono_font; } - -void pushMonoFont(float size) { - if (!g_mono_font) return; - size > 0.0f ? ImGui::PushFont(g_mono_font, size) : ImGui::PushFont(g_mono_font); -} -void popMonoFont() { if (g_mono_font) ImGui::PopFont(); } -void pushBoldFont() { if (g_bold_font) ImGui::PushFont(g_bold_font); } -void popBoldFont() { if (g_bold_font) ImGui::PopFont(); } -void pushLargeFont() { if (g_large_font) ImGui::PushFont(g_large_font); } -void popLargeFont() { if (g_large_font) ImGui::PopFont(); } diff --git a/openpilot/tools/cabana/ui/theme.cc b/openpilot/tools/cabana/ui/theme.cc new file mode 100644 index 0000000000..ff02ca24c7 --- /dev/null +++ b/openpilot/tools/cabana/ui/theme.cc @@ -0,0 +1,174 @@ +#include "tools/cabana/ui/theme.h" + +#include +#include +#include + +#include "implot.h" +#include "tools/cabana/core/settings.h" + +namespace fs = std::filesystem; + +namespace { + +constexpr Palette DARK_PALETTE = { + .text = rgb(0xf8f9f9), .text_disabled = rgb(0xb8c0c4), + .window = rgb(0x1d2225), .surface = rgb(0x30373b), + .frame = rgb(0x1e2224), .frame_hovered = rgb(0x394044), .frame_active = rgb(0x424a4f), + .button = rgb(0x424a4f), .button_hovered = rgb(0x535f64), .button_active = rgb(0x175886), + .header = rgb(0x175886), .header_hovered = rgb(0x24455e), .header_active = rgb(0x1c6ea8), + .accent = rgb(0x57a9e3), + .border = rgb(0x65737a), .separator = rgb(0x4b5559), + .tab = rgb(0x272c2f), .tab_hovered = rgb(0x424a4f), .table_header = rgb(0x424a4f), + .grid = rgb(0x65737a, 0.45f), .badge = rgb(0x808080), +}; + +constexpr Palette LIGHT_PALETTE = { + .text = rgb(0x1e2224), .text_disabled = rgb(0x535f64), + .window = rgb(0xeeeff0), .surface = rgb(0xffffff), + .frame = rgb(0xf8f9f9), .frame_hovered = rgb(0xeeeff0), .frame_active = rgb(0xddeef9), + .button = rgb(0xe3e6e8), .button_hovered = rgb(0xd8dcdf), .button_active = rgb(0xbcddf4), + .header = rgb(0xbcddf4), .header_hovered = rgb(0xddeef9), .header_active = rgb(0x9fcbec), + .accent = rgb(0x1c6ea8), + .border = rgb(0x98a3a9), .separator = rgb(0xcdd3d6), + .tab = rgb(0xe3e6e8), .tab_hovered = rgb(0xddeef9), .table_header = rgb(0xd8dcdf), + .grid = rgb(0x98a3a9, 0.4f), .badge = rgb(0xa0a0a4), +}; + +bool g_dark = false; +const Palette *g_palette = &LIGHT_PALETTE; +ImFont *g_ui_font = nullptr; +ImFont *g_bold_font = nullptr; +ImFont *g_mono_font = nullptr; +ImFont *g_large_font = nullptr; + +void addIconFont(float size, ImFont *base) { + ImFontConfig cfg; + cfg.MergeMode = base != nullptr; + cfg.GlyphMinAdvanceX = size; + if (base != nullptr) { + ImFontBaked *baked = base->GetFontBaked(size); + const float center = baked != nullptr ? (baked->Ascent + baked->Descent) * 0.5f : size * 0.5f; + cfg.GlyphOffset.y = std::round(size * 0.5f - center); + } + static const ImWchar ranges[] = {0xF000, 0xF8FF, 0}; + ImGui::GetIO().Fonts->AddFontFromFileTTF(BOOTSTRAP_ICONS_TTF, size, &cfg, ranges); +} + +ImFont *addFont(const fs::path &path, float size) { + ImFontConfig cfg; + cfg.OversampleH = 2; + cfg.OversampleV = 2; + ImFont *font = ImGui::GetIO().Fonts->AddFontFromFileTTF(path.c_str(), size, &cfg); + if (font != nullptr) addIconFont(size, font); + return font; +} + +ImVec4 alpha(ImVec4 c, float a) { return ImVec4(c.x, c.y, c.z, a); } + +} // namespace + +void loadFonts() { + ImGuiIO &io = ImGui::GetIO(); + const fs::path fonts = fs::path(CABANA_FONTS_DIR); + g_ui_font = addFont(fonts / "Inter-Regular.ttf", UI_FONT_SIZE); + g_bold_font = addFont(fonts / "Inter-SemiBold.ttf", UI_FONT_SIZE); + g_mono_font = addFont(fonts / "JetBrainsMono-Medium.ttf", 15.0f); + g_large_font = addFont(fonts / "Inter-Bold.ttf", 50.0f); + if (g_ui_font != nullptr) io.FontDefault = g_ui_font; + if (g_bold_font == nullptr) g_bold_font = g_ui_font; + if (g_mono_font == nullptr) g_mono_font = g_ui_font; + if (g_large_font == nullptr) g_large_font = g_bold_font; +} + +void applyTheme(int theme) { + g_dark = theme == DARK_THEME; + g_palette = g_dark ? &DARK_PALETTE : &LIGHT_PALETTE; + const Palette &p = *g_palette; + const ImVec4 none(0, 0, 0, 0); + + ImGuiStyle &style = ImGui::GetStyle(); + style = ImGuiStyle(); + style.WindowRounding = 6.0f; // dialogs and tooltips; docked panels and os windows ignore it + style.ChildRounding = 6.0f; + style.PopupRounding = 6.0f; + style.FrameRounding = 4.0f; + style.GrabRounding = 4.0f; + style.ScrollbarRounding = 4.0f; + style.TabRounding = 4.0f; + 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.CellPadding = ImVec2(6.0f, 4.0f); + style.ScrollbarSize = 14.0f; + style.GrabMinSize = 13.0f; + + ImVec4 *c = style.Colors; + c[ImGuiCol_Text] = p.text; + c[ImGuiCol_TextDisabled] = p.text_disabled; + c[ImGuiCol_WindowBg] = c[ImGuiCol_ScrollbarBg] = c[ImGuiCol_DockingEmptyBg] = p.window; + c[ImGuiCol_MenuBarBg] = p.surface; + c[ImGuiCol_TitleBg] = c[ImGuiCol_TitleBgActive] = c[ImGuiCol_TitleBgCollapsed] = p.window; + c[ImGuiCol_ChildBg] = c[ImGuiCol_PopupBg] = p.surface; + c[ImGuiCol_Border] = c[ImGuiCol_TableBorderStrong] = p.border; + c[ImGuiCol_Separator] = c[ImGuiCol_TableBorderLight] = p.separator; + c[ImGuiCol_BorderShadow] = c[ImGuiCol_ResizeGrip] = c[ImGuiCol_TableRowBg] = none; + c[ImGuiCol_FrameBg] = p.frame; + c[ImGuiCol_FrameBgHovered] = p.frame_hovered; + c[ImGuiCol_FrameBgActive] = p.frame_active; + c[ImGuiCol_Button] = p.button; + c[ImGuiCol_ButtonHovered] = c[ImGuiCol_SliderGrab] = p.button_hovered; + c[ImGuiCol_ScrollbarGrab] = p.border; + c[ImGuiCol_ScrollbarGrabHovered] = p.text_disabled; + c[ImGuiCol_ButtonActive] = p.button_active; + c[ImGuiCol_Header] = p.header; + c[ImGuiCol_HeaderHovered] = p.header_hovered; + c[ImGuiCol_HeaderActive] = p.header_active; + c[ImGuiCol_CheckMark] = c[ImGuiCol_NavCursor] = c[ImGuiCol_SliderGrabActive] = c[ImGuiCol_ScrollbarGrabActive] = p.accent; + c[ImGuiCol_SeparatorActive] = c[ImGuiCol_ResizeGripActive] = c[ImGuiCol_DragDropTarget] = p.accent; + c[ImGuiCol_TabSelectedOverline] = c[ImGuiCol_PlotHistogram] = p.accent; + c[ImGuiCol_SeparatorHovered] = c[ImGuiCol_ResizeGripHovered] = alpha(p.accent, 0.6f); + c[ImGuiCol_DockingPreview] = alpha(p.accent, 0.5f); + c[ImGuiCol_TextSelectedBg] = alpha(p.accent, 0.35f); + c[ImGuiCol_Tab] = c[ImGuiCol_TabDimmed] = p.tab; + c[ImGuiCol_TabHovered] = p.tab_hovered; + c[ImGuiCol_TabSelected] = c[ImGuiCol_TabDimmedSelected] = p.surface; + c[ImGuiCol_TabDimmedSelectedOverline] = none; + c[ImGuiCol_TableHeaderBg] = p.table_header; + c[ImGuiCol_TableRowBgAlt] = g_dark ? ImVec4(1, 1, 1, 0.065f) : ImVec4(0, 0, 0, 0.045f); + c[ImGuiCol_PlotLines] = p.text; + // ImGuiStyle() seeds every slot from the dark theme: set the rest so the light theme does not keep a white caret. + c[ImGuiCol_InputTextCursor] = c[ImGuiCol_UnsavedMarker] = p.text; + c[ImGuiCol_TextLink] = c[ImGuiCol_PlotLinesHovered] = c[ImGuiCol_PlotHistogramHovered] = c[ImGuiCol_NavWindowingHighlight] = p.accent; + c[ImGuiCol_TreeLines] = p.separator; + c[ImGuiCol_DragDropTargetBg] = alpha(p.accent, 0.2f); + // Disable the modal dim fade to make dialogs appear immediately. + c[ImGuiCol_ModalWindowDimBg] = c[ImGuiCol_NavWindowingDimBg] = none; + + // ChartView::drawAxes() pushes the other plot colors it needs; the rest are auto colors derived from the ImGui style. + ImPlot::GetStyle().Colors[ImPlotCol_AxisGrid] = p.grid; +} + +bool isDarkTheme() { return g_dark; } +const Palette &palette() { return *g_palette; } + +CabanaColor signalFillColor(const CabanaColor &c) { + if (!g_dark) return c; + auto [h, s, v] = c.hsv(); + return CabanaColor::fromHsv(h, std::min(1.0f, s * 1.4f), v * 0.8f, c.a / 255.0f); +} + +ImFont *boldFont() { return g_bold_font; } + +void pushMonoFont(float size) { + if (!g_mono_font) return; + size > 0.0f ? ImGui::PushFont(g_mono_font, size) : ImGui::PushFont(g_mono_font); +} +void popMonoFont() { if (g_mono_font) ImGui::PopFont(); } +void pushBoldFont() { if (g_bold_font) ImGui::PushFont(g_bold_font); } +void popBoldFont() { if (g_bold_font) ImGui::PopFont(); } +void pushLargeFont() { if (g_large_font) ImGui::PushFont(g_large_font); } +void popLargeFont() { if (g_large_font) ImGui::PopFont(); } diff --git a/openpilot/tools/cabana/ui/theme.h b/openpilot/tools/cabana/ui/theme.h new file mode 100644 index 0000000000..91f2a0740f --- /dev/null +++ b/openpilot/tools/cabana/ui/theme.h @@ -0,0 +1,49 @@ +#pragma once + +#include "imgui.h" +#include "imgui_internal.h" + +#include "tools/cabana/core/color.h" + +// Palette source: commaai/connect src/{colors,theme}.js at 7091050. +// Dark colors follow connect; light colors use its lightGrey and lightBlue families. +struct Palette { + ImVec4 text, text_disabled; + ImVec4 window; // the background behind panels and docked windows + ImVec4 surface; // panels, popups, table bodies: what content is drawn on + ImVec4 frame, frame_hovered, frame_active; + ImVec4 button, button_hovered, button_active; + ImVec4 header, header_hovered, header_active; // selections + ImVec4 accent; + ImVec4 border, separator; + ImVec4 tab, tab_hovered, table_header; + ImVec4 grid; + ImVec4 badge; // the fill behind the time labels drawn over a chart +}; + +constexpr ImVec4 rgb(unsigned hex, float alpha = 1.0f) { + return ImVec4(((hex >> 16) & 255) / 255.0f, ((hex >> 8) & 255) / 255.0f, (hex & 255) / 255.0f, alpha); +} +inline ImVec4 colorRgb(int r, int g, int b, float alpha = 1.0f) { + return ImVec4(r / 255.0f, g / 255.0f, b / 255.0f, alpha); +} +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); } + +constexpr float UI_FONT_SIZE = 16.0f; + +void loadFonts(); +void applyTheme(int theme); // Safe to call at runtime. +bool isDarkTheme(); +const Palette &palette(); + +CabanaColor signalFillColor(const CabanaColor &c); + +ImFont *boldFont(); +void pushMonoFont(float size = 0.0f); +void popMonoFont(); +void pushBoldFont(); +void popBoldFont(); +void pushLargeFont(); +void popLargeFont(); diff --git a/openpilot/tools/cabana/ui/util.cc b/openpilot/tools/cabana/ui/util.cc index 2e538e16f1..f646f33da4 100644 --- a/openpilot/tools/cabana/ui/util.cc +++ b/openpilot/tools/cabana/ui/util.cc @@ -3,7 +3,10 @@ #include #include #include +#include +#include #include +#include #include #include "imgui.h" @@ -22,6 +25,10 @@ void objc_msgSend(void); #include "tools/cabana/ui/icons.h" +namespace { +ImU32 u32(const ImVec4 &c) { return ImGui::ColorConvertFloat4ToU32(c); } +} // namespace + int inputCallback(ImGuiInputTextCallbackData *data) { auto *ctx = static_cast(data->UserData); if (data->EventFlag == ImGuiInputTextFlags_CallbackCharFilter) { @@ -58,9 +65,9 @@ bool inputTextMultiline(const char *label, std::string *s, const ImVec2 &size, I bool clearableInput(const char *label, std::string *s, const char *hint, ImGuiInputTextCallback validator) { bool changed = validatedInput(label, s, validator, hint); if (!s->empty()) { - ImGui::SameLine(0.0f, 0.0f); + ImGui::SameLine(0.0f, ImGui::GetStyle().ItemInnerSpacing.x); ImGui::PushID(label); - if (toolButton("clear", icon::X)) { + if (iconButton("clear", icon::X_LG)) { s->clear(); changed = true; } @@ -128,14 +135,68 @@ int nonWhitespaceValidator(ImGuiInputTextCallbackData *data) { return (data->EventChar < 128 && std::isspace((int)data->EventChar)) ? 1 : 0; } -bool toolButton(const char *id, const char *icon, const char *tooltip, const char *text) { - std::string label = text && *text ? std::string(icon) + " " + text + "###" + id : std::string(icon) + "###" + id; - // no frame, transparent until hovered - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0)); - ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f); - bool clicked = ImGui::Button(label.c_str()); - ImGui::PopStyleVar(); - ImGui::PopStyleColor(); +float iconButtonWidth() { return ImGui::GetFrameHeight(); } + +namespace { +constexpr float ICON_BUTTON_GLYPH_SCALE = 0.8f; + +// Exclude rasterization padding when centering icons; it varies between glyphs. +struct GlyphInk { float x0, y0, x1, y1; }; +GlyphInk glyphInk(const ImFontGlyph *g) { + ImTextureData *tex = ImGui::GetIO().Fonts->TexData; + const int px0 = (int)std::lround(g->U0 * tex->Width), px1 = (int)std::lround(g->U1 * tex->Width); + const int py0 = (int)std::lround(g->V0 * tex->Height), py1 = (int)std::lround(g->V1 * tex->Height); + int ix0 = px1, iy0 = py1, ix1 = px0, iy1 = py0; + for (int y = py0; y < py1; ++y) { + for (int x = px0; x < px1; ++x) { + const unsigned char *p = (const unsigned char *)tex->GetPixelsAt(x, y); + const unsigned char alpha = tex->Format == ImTextureFormat_Alpha8 ? p[0] : p[3]; + if (alpha < 64) continue; + ix0 = std::min(ix0, x); ix1 = std::max(ix1, x + 1); + iy0 = std::min(iy0, y); iy1 = std::max(iy1, y + 1); + } + } + if (ix0 >= ix1 || py1 <= py0 || px1 <= px0) return {g->X0, g->Y0, g->X1, g->Y1}; + const float sx = (g->X1 - g->X0) / (px1 - px0), sy = (g->Y1 - g->Y0) / (py1 - py0); + return {g->X0 + (ix0 - px0) * sx, g->Y0 + (iy0 - py0) * sy, g->X0 + (ix1 - px0) * sx, g->Y0 + (iy1 - py0) * sy}; +} + +// The scan reads the atlas pixels: do it once per icon, and again when the atlas repacked the glyph. +const GlyphInk &cachedGlyphInk(const ImFontGlyph *g, float size, unsigned int codepoint) { + struct Entry { ImVec4 uv; GlyphInk ink; }; + static std::unordered_map cache; + const uint64_t key = ((uint64_t)(uint32_t)size << 32) | codepoint; + const ImVec4 uv(g->U0, g->V0, g->U1, g->V1); + auto it = cache.find(key); + if (it == cache.end() || memcmp(&it->second.uv, &uv, sizeof(uv)) != 0) it = cache.insert_or_assign(key, Entry{uv, glyphInk(g)}).first; + return it->second.ink; +} + +bool squareIconButton(const char *id, const char *icon) { + const bool clicked = ImGui::Button((std::string("###") + id).c_str(), ImVec2(iconButtonWidth(), 0.0f)); + const ImRect r(ImGui::GetItemRectMin(), ImGui::GetItemRectMax()); + unsigned int codepoint = 0; + ImTextCharFromUtf8(&codepoint, icon, nullptr); + // Leave a margin even for icons that fill the glyph bounds. + const float size = std::round(ImGui::GetFontSize() * ICON_BUTTON_GLYPH_SCALE); + 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. + 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)); + } + return clicked; +} +} // namespace + +bool iconButton(const char *id, const char *icon, const char *tooltip) { + const bool clicked = squareIconButton(id, icon); if (tooltip && *tooltip) ImGui::SetItemTooltip("%s", tooltip); return clicked; } @@ -240,7 +301,7 @@ bool viewSelectable(const char *label, bool selected, ImGuiSelectableFlags flags } bool checkBox(const char *label, bool *v) { - const float box = 16.0f; + const float box = CHECKBOX_SIZE; ImGuiWindow *window = ImGui::GetCurrentWindow(); if (window->SkipItems) return false; const ImGuiStyle &style = ImGui::GetStyle(); @@ -345,21 +406,41 @@ bool beginDialog(const char *id, PopupOwner *owner, const ImVec2 &size, ImGuiWin // tool bar -void beginToolbar() { - // the items sit next to each other, the buttons only carry the auto raise margin - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(TOOLBAR_ITEM_SPACING, ImGui::GetStyle().ItemSpacing.y)); - ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(TOOLBAR_BUTTON_PADDING, ImGui::GetStyle().FramePadding.y)); +ToolbarItem toolbarAction(const char *id, const char *icon, const char *label, std::function trigger, bool enabled, bool tight) { + return {iconButtonWidth(), [=]() { + ImGui::BeginDisabled(!enabled); + if (iconButton(id, icon)) trigger(); + ImGui::EndDisabled(); + disabledItemTooltip(label); + }, label, trigger, enabled, true, tight}; } -void endToolbar() { ImGui::PopStyleVar(2); } +ToolbarItem toolbarMenu(const char *id, const std::string &text, const char *label, std::function items, bool bold, bool tight, 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"; + menuButton(id, text, popup_id.c_str(), bold, width); + if (ImGui::BeginPopup(popup_id.c_str())) { + items(); + ImGui::EndPopup(); + } + }, label}; + item.tight = tight; + item.submenu = std::move(items); + return item; +} 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 ? ImGui::GetStyle().ItemSpacing.x : 0); + for (size_t i = begin; i < end; ++i) w += items[i].width + (i > begin ? toolbarSpacing(items[i]) : 0); return w; } @@ -370,24 +451,25 @@ float toolbarWidth(const std::vector &items, size_t spacer_index) { return w; } -void drawToolbar(const std::vector &items, size_t spacer_index) { +void drawToolbar(const std::vector &items, size_t spacer_index, float width) { const ImGuiStyle &style = ImGui::GetStyle(); spacer_index = std::min(spacer_index, items.size()); const float right_width = toolbarGroupWidth(items, spacer_index, items.size()); const float start_x = ImGui::GetCursorPosX(); - const float avail = ImGui::GetContentRegionAvail().x; + const float avail = width < 0.0f ? ImGui::GetContentRegionAvail().x : width; const float right_edge = start_x + avail; - const float extension_width = toolbarButtonWidth(icon::RAQUO); + const float extension_width = iconButtonWidth(); // when everything fits the spacer takes the slack, otherwise the extension button is reserved at the // right edge and the items are packed from the left until the next one does not fit - const bool fits = toolbarWidth(items, spacer_index) <= avail; + // a caller may size a flexible item from the same available width: allow for the float error of the round trip + const bool fits = toolbarWidth(items, spacer_index) <= avail + 0.5f; size_t visible = items.size(); if (!fits) { 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 ? style.ItemSpacing.x : 0); + const float w = items[visible].width + (visible ? toolbarSpacing(items[visible]) : 0); if (used + w > usable) break; used += w; } @@ -396,7 +478,7 @@ void drawToolbar(const std::vector &items, size_t spacer_index) { 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(); + else ImGui::SameLine(0.0f, toolbarSpacing(items[i])); items[i].draw(); } @@ -404,9 +486,7 @@ void drawToolbar(const std::vector &items, size_t spacer_index) { // the extension button sits fully inside the toolbar: its right edge is the content region right edge const float extension_x = std::max(start_x, right_edge - extension_width); visible == 0 ? ImGui::SetCursorPosX(extension_x) : ImGui::SameLine(extension_x); - if (ImGui::Button((std::string(icon::RAQUO) + "###toolbar_extension").c_str(), ImVec2(extension_width, 0))) - ImGui::OpenPopup("toolbar_extension_menu"); - ImGui::SetItemTooltip("More"); + if (iconButton("toolbar_extension", icon::CHEVRON_DOUBLE_RIGHT, "More")) ImGui::OpenPopup("toolbar_extension_menu"); // the popup opens inward: its right edge is aligned with the button so it stays inside the window ImGui::SetNextWindowPos(ImVec2(ImGui::GetItemRectMax().x, ImGui::GetItemRectMax().y), ImGuiCond_Always, ImVec2(1, 0)); if (ImGui::BeginPopup("toolbar_extension_menu")) { @@ -414,6 +494,11 @@ void drawToolbar(const std::vector &items, size_t spacer_index) { if (!items[i].in_menu) continue; if (items[i].menu_label.empty()) { items[i].draw(); + } else if (items[i].submenu) { + if (ImGui::BeginMenu(items[i].menu_label.c_str(), items[i].enabled)) { + items[i].submenu(); + ImGui::EndMenu(); + } } else if (ImGui::MenuItem(items[i].menu_label.c_str(), nullptr, false, items[i].enabled)) { items[i].trigger(); } @@ -438,20 +523,17 @@ bool menuButton(const char *id, const std::string &text, const char *popup_id, b const ImGuiStyle &style = ImGui::GetStyle(); const bool popup_open = ImGui::IsPopupOpen(popup_id); if (width <= 0.0f) width = menuButtonWidth(text, bold); - // no frame, transparent until hovered; the button is drawn pressed while the menu is open. The menu opens - // on press; a press while it is open toggles it closed (imgui closes the popup at the end of the frame of - // a click outside it, so only open when it is not already open) + // ImGui closes popups at frame end on outside clicks. Only open a closed popup so a second press toggles it off. if (bold) pushBoldFont(); 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); - ImGui::PushStyleColor(ImGuiCol_Button, popup_open ? style.Colors[ImGuiCol_ButtonActive] : ImVec4(0, 0, 0, 0)); - ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f); + 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)); const bool clicked = ImGui::ButtonEx((text + "###" + id).c_str(), ImVec2(width, 0.0f), ImGuiButtonFlags_PressedOnClick); - ImGui::PopStyleVar(3); + ImGui::PopStyleVar(2); ImGui::PopStyleColor(); if (bold) popBoldFont(); // a 6 px arrow right after the text, sitting on the text baseline @@ -467,3 +549,39 @@ bool menuButton(const char *id, const std::string &text, const char *popup_id, b ImGui::SetNextWindowPos(ImVec2(min.x, ImGui::GetItemRectMax().y), ImGuiCond_Always); return clicked; } + +void drawSliderHandle(ImDrawList *p, const ImRect &r) { + const Palette &pal = palette(); + p->AddRectFilled(r.Min, r.Max, u32(pal.button_hovered), 2.0f); + p->AddRectFilled(ImVec2(r.Min.x, r.GetCenter().y), r.Max, u32(pal.button), 2.0f, ImDrawFlags_RoundCornersBottom); + p->AddRect(r.Min, r.Max, u32(pal.border), 2.0f, 0, 1.0f); +} + +bool fusionSliderInt(const char *label, int *v, int min, int max, float width) { + // Keep ImGui slider input handling, but replace its frame and grab with custom drawing. + ImGui::PushStyleColor(ImGuiCol_FrameBg, IM_COL32_BLACK_TRANS); + ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, IM_COL32_BLACK_TRANS); + ImGui::PushStyleColor(ImGuiCol_FrameBgActive, IM_COL32_BLACK_TRANS); + ImGui::PushStyleColor(ImGuiCol_SliderGrab, IM_COL32_BLACK_TRANS); + ImGui::PushStyleColor(ImGuiCol_SliderGrabActive, IM_COL32_BLACK_TRANS); + ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f); + ImGui::SetNextItemWidth(width); + bool changed = ImGui::SliderInt(label, v, min, max, "", ImGuiSliderFlags_NoInput); + ImGui::PopStyleVar(); + ImGui::PopStyleColor(5); + + const ImVec2 bb_min = ImGui::GetItemRectMin(), bb_max = ImGui::GetItemRectMax(); + const float cy = (bb_min.y + bb_max.y) * 0.5f; + const float groove_h = SLIDER_THICKNESS * 0.5f; + const float handle_h = std::min(SLIDER_THICKNESS, bb_max.y - bb_min.y); + const float x0 = bb_min.x + SLIDER_LENGTH * 0.5f, x1 = bb_max.x - SLIDER_LENGTH * 0.5f; + const float t = max > min ? (float)(*v - min) / (float)(max - min) : 0.0f; + const float hx = x0 + (x1 - x0) * t; + ImDrawList *dl = ImGui::GetWindowDrawList(); + const float groove_y0 = cy - groove_h * 0.5f, groove_y1 = cy + groove_h * 0.5f; + dl->AddRectFilled(ImVec2(bb_min.x, groove_y0), ImVec2(bb_max.x, groove_y1), u32(palette().separator), groove_h * 0.5f); + dl->AddRectFilled(ImVec2(bb_min.x, groove_y0), ImVec2(hx, groove_y1), u32(palette().accent), groove_h * 0.5f); + drawSliderHandle(dl, ImRect(ImVec2(hx - SLIDER_LENGTH * 0.5f, cy - handle_h * 0.5f), + ImVec2(hx + SLIDER_LENGTH * 0.5f, cy + handle_h * 0.5f))); + return changed; +} diff --git a/openpilot/tools/cabana/ui/util.h b/openpilot/tools/cabana/ui/util.h index d180c073b7..87afb28f74 100644 --- a/openpilot/tools/cabana/ui/util.h +++ b/openpilot/tools/cabana/ui/util.h @@ -4,22 +4,11 @@ #include #include -#include "imgui.h" -#include "imgui_internal.h" - -#include "tools/cabana/core/color.h" +#include "tools/cabana/ui/theme.h" #include "tools/cabana/utils/util.h" struct GLFWwindow; -inline ImVec4 colorRgb(int r, int g, int b, float alpha = 1.0f) { - return ImVec4(r / 255.0f, g / 255.0f, b / 255.0f, alpha); -} - -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); } - // the dock window identity of the messages panel (the visible title changes, the part after ### is the id) constexpr const char *MESSAGES_PANEL_ID = "###MessagesPanel"; @@ -78,8 +67,9 @@ int doubleValidator(ImGuiInputTextCallbackData *data); int ipValidator(ImGuiInputTextCallbackData *data); int nonWhitespaceValidator(ImGuiInputTextCallbackData *data); -// auto-raise icon button with a tooltip -bool toolButton(const char *id, const char *icon, const char *tooltip = nullptr, const char *text = nullptr); +// Use ItemInnerSpacing between related buttons and ItemSpacing between groups. +bool iconButton(const char *id, const char *icon, const char *tooltip = nullptr); +float iconButtonWidth(); // tooltip for the last item that also shows while the item is disabled void disabledItemTooltip(const char *text); @@ -121,6 +111,7 @@ bool viewSelectable(const char *label, bool selected, ImGuiSelectableFlags flags // a 16px box vertically centered in the frame height so rows keep their layout; ImGui::Checkbox draws a // frame height (22 px) square. bool checkBox(const char *label, bool *v); +constexpr float CHECKBOX_SIZE = 16.0f; // the next items on the line are right aligned as a block `width` wide void alignRight(float width); @@ -134,14 +125,6 @@ void drawElidedText(ImDrawList *dl, const ImRect &rect, const std::string &text, float markerSize(); void drawColorMarker(ImDrawList *dl, const ImVec2 &pos, ImU32 col); -void loadFonts(); -void applyTheme(int theme); // safe to call at runtime -bool isDarkTheme(); // the theme applyTheme() resolved -CabanaColor signalFillColor(const CabanaColor &c); - -ImU32 highlightedTextColor(); -ImU32 paletteBrightText(); - // the next window is a real OS window instead of being drawn inside the main one void setNextWindowFloatsOut(); #ifdef __APPLE__ @@ -158,8 +141,6 @@ void setNextDialogWindow(const ImVec2 &size); // centered modal dialog. false when the popup is not submitted this frame. bool beginDialog(const char *id, PopupOwner *owner, const ImVec2 &size, ImGuiWindowFlags flags = ImGuiWindowFlags_NoResize); -const float TOOLBAR_ITEM_SPACING = 1.0f; -const float TOOLBAR_BUTTON_PADDING = 4.0f; // auto raise button horizontal margin const float SLIDER_LENGTH = 13.0f; const float SLIDER_THICKNESS = 13.0f; @@ -172,31 +153,27 @@ 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 }; -void beginToolbar(); // item spacing and button padding of a tool bar, until endToolbar() -void endToolbar(); +ToolbarItem toolbarAction(const char *id, const char *icon, const char *label, std::function trigger, + bool enabled = true, bool tight = false); +// 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); 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); // items before spacer_index sit at the left, the rest are right aligned; the overflow goes into the ">>" menu -void drawToolbar(const std::vector &items, size_t spacer_index); +// width < 0 uses the available content width. +void drawToolbar(const std::vector &items, size_t spacer_index, float width = -1.0f); -// an auto-raise button that opens `popup_id` below itself, with a dropdown arrow after the text. width 0: +// Opens `popup_id` below the button on press. width 0: // sized to the text, otherwise the text and the arrow are centered in the button float menuButtonWidth(const std::string &text, bool bold = false); bool menuButton(const char *id, const std::string &text, const char *popup_id, bool bold = false, float width = 0.0f); -// a 13x13 handle filled with a subtle vertical gradient and a mid grey outline void drawSliderHandle(ImDrawList *p, const ImRect &r); -// full width groove, filled left of the handle, 13x13 handle (style.cc) bool fusionSliderInt(const char *label, int *v, int min, int max, float width); - -ImFont *boldFont(); -ImFont *monoFont(); -void pushMonoFont(float size = 0.0f); // 0: the size the font was loaded at -void popMonoFont(); -void pushBoldFont(); -void popBoldFont(); -void pushLargeFont(); -void popLargeFont(); diff --git a/openpilot/tools/cabana/ui/widgets/binaryview.cc b/openpilot/tools/cabana/ui/widgets/binaryview.cc index 7f9ac8b7b2..4469273607 100644 --- a/openpilot/tools/cabana/ui/widgets/binaryview.cc +++ b/openpilot/tools/cabana/ui/widgets/binaryview.cc @@ -296,8 +296,10 @@ void BinaryView::draw() { } const int rows = row_count_; - const float width = ImGui::GetContentRegionAvail().x; - column_width_ = std::max(1.0f, (width - VERTICAL_HEADER_WIDTH) / COLUMN_COUNT); + // Keep hex bytes readable in narrow panels by scrolling instead of shrinking further. + const float min_column_width = std::ceil(ImGui::CalcTextSize("FF").x) + 10.0f; + const float width = std::max(ImGui::GetContentRegionAvail().x, VERTICAL_HEADER_WIDTH + min_column_width * COLUMN_COUNT); + column_width_ = std::max(min_column_width, (width - VERTICAL_HEADER_WIDTH) / COLUMN_COUNT); grid_pos_ = ImGui::GetCursorScreenPos(); ImGui::InvisibleButton("##binary_view", ImVec2(std::max(width, 1.0f), std::max(static_cast(rows * CELL_HEIGHT), 1.0f))); ImDrawList *painter = ImGui::GetWindowDrawList(); @@ -448,9 +450,8 @@ void BinaryView::paintCell(ImDrawList *painter, const ImRect &rect, const Binary painter->AddRectFilled(rect.Min, rect.Max, toImU32(item->bg_color)); } } else if (isSelected(index)) { - auto color = resize_sig_ ? toImU32(resize_sig_->color) : paletteHighlight(); - painter->AddRectFilled(rect.Min, rect.Max, color); - pen = paletteBrightText(); + painter->AddRectFilled(rect.Min, rect.Max, resize_sig_ ? toImU32(resize_sig_->color) : paletteHighlight()); + if (resize_sig_) pen = IM_COL32_WHITE; } else if (!hasSelection() || std::find(item->sigs.begin(), item->sigs.end(), resize_sig_) == item->sigs.end()) { // not resizing if (item->sigs.size() > 0) { for (auto &s : item->sigs) { @@ -465,7 +466,7 @@ void BinaryView::paintCell(ImDrawList *painter, const ImRect &rect, const Binary if (item->bg_color.alpha() > 0) painter->AddRectFilled(rect.Min, rect.Max, toImU32(item->bg_color)); } bool bright = std::find(item->sigs.begin(), item->sigs.end(), hovered_sig_) != item->sigs.end(); - pen = bright ? paletteBrightText() : paletteText(is_message_active_); + pen = bright ? IM_COL32_WHITE : paletteText(is_message_active_); } if (item->sigs.size() > 1) { diff --git a/openpilot/tools/cabana/ui/widgets/cameraview.cc b/openpilot/tools/cabana/ui/widgets/cameraview.cc index 6ffd099fe9..1f730344d5 100644 --- a/openpilot/tools/cabana/ui/widgets/cameraview.cc +++ b/openpilot/tools/cabana/ui/widgets/cameraview.cc @@ -3,6 +3,7 @@ #include #include #include +#include #include #include "imgui_impl_opengl3_loader.h" @@ -62,7 +63,7 @@ CameraWidget::~CameraWidget() { void CameraWidget::startVipcThread() { if (!vipc_thread_.joinable()) { - clearFrames(); + // Preserve the last frame when restoring a collapsed video; paused replay sends no replacement. vipc_exit_ = false; vipc_thread_ = std::thread(&CameraWidget::vipcThread, this); } @@ -99,7 +100,7 @@ float CameraWidget::frameAspectRatio() const { void CameraWidget::paint() { ImDrawList *p = ImGui::GetWindowDrawList(); - p->AddRectFilled(rect_.Min, rect_.Max, bg_); + p->AddRectFilled(rect_.Min, rect_.Max, bg_, ImGui::GetStyle().ChildRounding); std::lock_guard lk(frame_lock_); if (rgb_frame_.isNull()) return; @@ -113,24 +114,26 @@ void CameraWidget::paint() { // mirror cabin camera horizontally std::swap(placement.uv0.x, placement.uv1.x); } - p->AddImage(frame_texture_.ref(), placement.min, placement.max, placement.uv0, placement.uv1); + p->AddImageRounded(frame_texture_.ref(), placement.min, placement.max, placement.uv0, placement.uv1, IM_COL32_WHITE, ImGui::GetStyle().ChildRounding); } void CameraWidget::vipcThread() { VisionStreamType cur_stream = requested_stream_type_; std::unique_ptr vipc_client; VisionIpcBufExtra frame_meta = {}; + bool was_connected = false; while (!vipc_exit_) { if (!vipc_client || cur_stream != requested_stream_type_) { - clearFrames(); + if (cur_stream != requested_stream_type_) clearFrames(); cur_stream = requested_stream_type_; vipc_client.reset(new VisionIpcClient(stream_name_, cur_stream, false)); } active_stream_type_ = cur_stream; if (!vipc_client->connected) { - clearFrames(); + // the server changed (a new route): the last frame is stale. A fresh thread keeps it, see startVipcThread(). + if (std::exchange(was_connected, false)) clearFrames(); auto streams = VisionIpcClient::getAvailableStreams(stream_name_, false); if (streams.empty()) { std::this_thread::sleep_for(std::chrono::milliseconds(100)); @@ -142,6 +145,7 @@ void CameraWidget::vipcThread() { std::this_thread::sleep_for(std::chrono::milliseconds(100)); continue; } + was_connected = true; } if (VisionBuf *buf = vipc_client->recv(&frame_meta, 100)) { diff --git a/openpilot/tools/cabana/ui/widgets/detailwidget.cc b/openpilot/tools/cabana/ui/widgets/detailwidget.cc index ebb74ffa4a..00ed44b09d 100644 --- a/openpilot/tools/cabana/ui/widgets/detailwidget.cc +++ b/openpilot/tools/cabana/ui/widgets/detailwidget.cc @@ -74,41 +74,40 @@ DetailWidget::DetailWidget(ChartsWidget *charts) : charts_(charts) { void DetailWidget::drawToolBar() { const ImGuiStyle &style = ImGui::GetStyle(); - auto radio_width = [&](const char *label) { return ImGui::GetFrameHeight() + style.ItemInnerSpacing.x + ImGui::CalcTextSize(label).x; }; - auto button_width = [&](const char *label) { return ImGui::CalcTextSize(label).x + style.FramePadding.x * 2; }; - const float right_width = ImGui::CalcTextSize("Heatmap:").x + style.ItemSpacing.x + radio_width("Live") + style.ItemSpacing.x + - radio_width(heatmap_all_text_.c_str()) + style.ItemSpacing.x * 3 + 1.0f + - button_width(icon::PENCIL) + style.ItemSpacing.x + button_width(icon::X_LG); - const float avail = ImGui::GetContentRegionAvail().x; + std::vector items; + float name_width = 0.0f; + items.push_back({0.0f, [this, &name_width]() { + ImGui::AlignTextToFramePadding(); + pushBoldFont(); + name_label_.draw(name_width); + popBoldFont(); + }}); + items.back().in_menu = false; + const size_t spacer_index = items.size(); + const std::string heatmap_text = "Heatmap: " + (heatmap_live_ ? std::string("Live") : heatmap_all_text_); + auto heatmap_items = [this]() { + if (ImGui::MenuItem("Live", nullptr, heatmap_live_) && !heatmap_live_) { + heatmap_live_ = true; + binary_view_->setHeatmapLiveMode(true); + } + if (ImGui::MenuItem(heatmap_all_text_.c_str(), nullptr, !heatmap_live_) && heatmap_live_) { + heatmap_live_ = false; + binary_view_->setHeatmapLiveMode(false); + } + }; + items.push_back(toolbarMenu("heatmap", heatmap_text, "Heatmap", heatmap_items)); + items.push_back({1.0f, []() { ImGui::SeparatorEx(ImGuiSeparatorFlags_Vertical); }}); + items.back().in_menu = false; + // Capture the panel width before the action can run inside the overflow popup. + 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)); - ImGui::AlignTextToFramePadding(); - pushBoldFont(); - name_label_.draw(std::max(1.0f, avail - right_width - style.ItemSpacing.x)); - popBoldFont(); - - alignRight(right_width); - ImGui::TextUnformatted("Heatmap:"); - ImGui::SameLine(); - if (ImGui::RadioButton("Live##heatmap_live_", heatmap_live_) && !heatmap_live_) { - heatmap_live_ = true; - binary_view_->setHeatmapLiveMode(true); - } - ImGui::SameLine(); - if (ImGui::RadioButton((heatmap_all_text_ + "##heatmap_all").c_str(), !heatmap_live_) && heatmap_live_) { - heatmap_live_ = false; - binary_view_->setHeatmapLiveMode(false); - } - - ImGui::SameLine(); - ImGui::SeparatorEx(ImGuiSeparatorFlags_Vertical); - ImGui::SameLine(); - if (ImGui::Button(icon::PENCIL)) editMsg(); - ImGui::SetItemTooltip("Edit Message"); - ImGui::SameLine(); - ImGui::BeginDisabled(!action_remove_msg_enabled_); - if (ImGui::Button(icon::X_LG)) UndoStack::instance()->push(new RemoveMsgCommand(msg_id_)); - ImGui::EndDisabled(); - disabledItemTooltip("Remove Message"); + 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); + items[0].width = name_width; + drawToolbar(items, spacer_index); } void DetailWidget::showTabBarContextMenu(int index) { @@ -203,34 +202,32 @@ void DetailWidget::updateState(const std::set *msgs) { history_log_->updateState(); } -void DetailWidget::editMsg() { +void DetailWidget::editMsg(float parent_width) { auto msg = dbc()->msg(msg_id_); int size = msg ? msg->size : can->lastMessage(msg_id_).dat.size(); - edit_dlg_ = std::make_unique(msg_id_, msgName(msg_id_), size, ImGui::GetWindowWidth()); + edit_dlg_ = std::make_unique(msg_id_, msgName(msg_id_), size, parent_width); } void DetailWidget::drawTabWidget() { - // the pages first, the tab bar below them - const float tab_height = ImGui::GetFrameHeight(); - const float content_height = ImGui::GetContentRegionAvail().y - tab_height - ImGui::GetStyle().ItemSpacing.y; - ImGui::BeginChild("tab_widget", ImVec2(0, std::max(content_height, 1.0f)), ImGuiChildFlags_None, + const ImGuiStyle &style = ImGui::GetStyle(); + const float pad = style.ItemInnerSpacing.x, pill_height = ImGui::GetFrameHeight() + pad * 2; + ImGui::BeginChild("tab_widget", ImVec2(0, 0), ImGuiChildFlags_None, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse); + const ImRect page_rect = ImGui::GetCurrentWindow()->Rect(); + const float gap = style.WindowPadding.y; + ImGui::BeginChild("page", ImVec2(0, std::max(page_rect.GetHeight() - pill_height - gap, 1.0f)), + ImGuiChildFlags_None, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse); if (tab_widget_index_ == 0) { // 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 height = std::clamp(min_height, 1.0f, max_height); - ImGui::BeginChild("binary_view", ImVec2(0, 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)); - const float spacing = ImGui::GetStyle().ItemSpacing.y; - const ImRect child_rect = ImGui::GetCurrentWindow()->Rect(); - ImGui::GetWindowDrawList()->AddRectFilled(ImVec2(child_rect.Min.x, ImGui::GetItemRectMin().y - spacing), - ImVec2(child_rect.Max.x, ImGui::GetItemRectMax().y + spacing), - ImGui::GetColorU32(ImGuiCol_WindowBg)); ImGui::BeginChild("signal_view", ImVec2(0, 0)); signal_view_rect_ = ImGui::GetCurrentWindow()->Rect(); signal_view_->draw(); @@ -240,33 +237,44 @@ void DetailWidget::drawTabWidget() { } ImGui::EndChild(); - const std::string labels[] = {std::string(icon::FILE_EARMARK_RULED) + " Messages", std::string(icon::STOPWATCH) + " Logs"}; - // the tabs are centered in the bar: the bar itself starts at the first tab, so its separator only spans the - // tabs and the full width one is drawn underneath it - const ImGuiStyle &style = ImGui::GetStyle(); - float tabs_width = 0.0f; + std::string labels[] = {std::string(icon::FILE_EARMARK_RULED) + " Messages", std::string(icon::STOPWATCH) + " Logs"}; + auto pill_width = [&]() { + float w = pad; + for (const auto &label : labels) w += ImGui::CalcTextSize(label.c_str()).x + style.FramePadding.x * 2 + pad; + return w; + }; + float width = pill_width(); + if (width > page_rect.GetWidth()) { + labels[0] = icon::FILE_EARMARK_RULED; + labels[1] = icon::STOPWATCH; + width = pill_width(); + } + const ImVec2 size(width, pill_height); + const ImVec2 min(std::round(page_rect.GetCenter().x - width * 0.5f), page_rect.Max.y - size.y); + ImGui::SetNextWindowPos(min); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(pad, pad)); + ImGui::PushStyleColor(ImGuiCol_ChildBg, ImGui::GetColorU32(ImGuiCol_PopupBg)); + ImGui::BeginChild("page_switch", size, ImGuiChildFlags_Borders | ImGuiChildFlags_AlwaysUseWindowPadding, + ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse); + ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(pad, 0.0f)); for (int i = 0; i < 2; ++i) { - tabs_width += ImGui::TabItemCalcSize(labels[i].c_str(), false).x + (i ? style.ItemInnerSpacing.x : 0.0f); - } - ImGuiWindow *window = ImGui::GetCurrentWindow(); - const float separator_y = ImGui::GetCursorScreenPos().y + ImGui::GetFrameHeight() - 1.0f; - window->DrawList->AddLine(ImVec2(window->WorkRect.Min.x, separator_y), ImVec2(window->WorkRect.Max.x, separator_y), - ImGui::GetColorU32(ImGuiCol_TabSelected), style.TabBarBorderSize); - ImGui::SetCursorPosX(ImGui::GetCursorPosX() + std::max(0.0f, (ImGui::GetContentRegionAvail().x - tabs_width) * 0.5f)); - - if (ImGui::BeginTabBar("tab_widget_tabs")) { - for (int i = 0; i < 2; ++i) { - if (ImGui::BeginTabItem(labels[i].c_str())) { - if (tab_widget_index_ != i) { - tab_widget_index_ = i; - if (i == 1) history_log_->onShown(); - updateState(); - } - ImGui::EndTabItem(); - } + const bool selected = tab_widget_index_ == i; + ImGui::PushStyleColor(ImGuiCol_Button, ImGui::GetColorU32(selected ? ImGuiCol_Header : ImGuiCol_Button, selected ? 1.0f : 0.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::GetColorU32(selected ? ImGuiCol_HeaderActive : ImGuiCol_ButtonHovered)); + if (i) ImGui::SameLine(); + if (ImGui::Button(labels[i].c_str()) && !selected) { + tab_widget_index_ = i; + if (i == 1) history_log_->onShown(); + updateState(); } - ImGui::EndTabBar(); + ImGui::PopStyleColor(2); } + ImGui::PopStyleVar(2); + ImGui::EndChild(); + ImGui::PopStyleColor(); + ImGui::PopStyleVar(); + ImGui::EndChild(); } void DetailWidget::draw() { @@ -321,8 +329,11 @@ bool EditMessageDialog::draw() { ImGui::OpenPopup(window_title_.c_str()); opened_ = true; } - setNextDialogWindow(ImVec2(0.0f, 0.0f)); - ImGui::SetNextWindowSize(ImVec2(width_, 0.0f), ImGuiCond_Always); // fixed width, the height fits the form + // The form needs room for message names and comments even when its panel is narrow. + const float max_width = std::max(1.0f, ImGui::GetMainViewport()->WorkSize.x - ImGui::GetStyle().WindowPadding.x * 2); + const float min_width = std::min(600.0f, max_width); + ImGui::SetNextWindowSizeConstraints(ImVec2(min_width, 0.0f), ImVec2(max_width, FLT_MAX)); + 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; @@ -401,9 +412,6 @@ void CenterWidget::draw() { } void CenterWidget::drawWelcomeWidget() { - const ImVec2 win_pos = ImGui::GetWindowPos(), win_size = ImGui::GetWindowSize(); - ImGui::GetWindowDrawList()->AddRectFilled(win_pos, ImVec2(win_pos.x + win_size.x, win_pos.y + win_size.y), ImGui::GetColorU32(ImGuiCol_ChildBg)); - const ImVec2 avail = ImGui::GetContentRegionAvail(); const ImVec2 origin = ImGui::GetCursorPos(); auto centered = [&](const char *text, float y) { diff --git a/openpilot/tools/cabana/ui/widgets/detailwidget.h b/openpilot/tools/cabana/ui/widgets/detailwidget.h index 08d3d7db41..7f8efde99c 100644 --- a/openpilot/tools/cabana/ui/widgets/detailwidget.h +++ b/openpilot/tools/cabana/ui/widgets/detailwidget.h @@ -73,7 +73,7 @@ private: void drawTabWidget(); int findOrAddTab(const MessageId& message_id); void showTabBarContextMenu(int index); - void editMsg(); + void editMsg(float parent_width); void updateState(const std::set *msgs = nullptr); MessageId msg_id_; diff --git a/openpilot/tools/cabana/ui/widgets/historylog.cc b/openpilot/tools/cabana/ui/widgets/historylog.cc index ec2ef844ff..ba51c4d6ee 100644 --- a/openpilot/tools/cabana/ui/widgets/historylog.cc +++ b/openpilot/tools/cabana/ui/widgets/historylog.cc @@ -133,9 +133,9 @@ void LogsWidget::draw() { const ImGuiStyle &style = ImGui::GetStyle(); // toolbar: the export button is right aligned and never clipped, the value input shrinks first - const float export_w = ImGui::CalcTextSize(icon::FILETYPE_CSV).x + style.FramePadding.x * 2; + const float export_w = iconButtonWidth(); if (!sigs_.empty()) { - const float clear_w = value_edit_.empty() ? 0.0f : ImGui::CalcTextSize(icon::X).x + style.FramePadding.x * 2; + const float clear_w = value_edit_.empty() ? 0.0f : iconButtonWidth(); const float fixed = DISPLAY_TYPE_WIDTH + SIGNALS_WIDTH + COMPARE_WIDTH + clear_w + style.ItemSpacing.x * 4 + export_w; const float value_w = std::clamp(ImGui::GetContentRegionAvail().x - fixed, 30.0f, 120.0f); @@ -164,9 +164,9 @@ void LogsWidget::draw() { filterChanged(); } } - alignRight(export_w); + alignRight(iconButtonWidth()); ImGui::BeginDisabled(!export_btn_enabled_); - if (ImGui::Button(icon::FILETYPE_CSV)) exportToCSV(); + if (iconButton("export_csv", icon::FILETYPE_CSV)) exportToCSV(); ImGui::EndDisabled(); disabledItemTooltip("Export to CSV file..."); @@ -196,10 +196,10 @@ void LogsWidget::drawHeaderCell(ImDrawList *dl, const ImRect &rect, int column) if (column > 0 && !hexMode()) { CabanaColor bg = sigs_[column - 1]->color; bg.a = 128; - dl->AddRectFilled(rect.Min, rect.Max, toImU32(bg)); + dl->AddRectFilled(rect.Min, rect.Max, toImU32(bg), ImGui::GetStyle().FrameRounding); } const std::string text = headerText(column); - const ImU32 color = isDarkTheme() ? toImU32(DarkTheme::bright_text) : ImGui::GetColorU32(ImGuiCol_Text); + const ImU32 color = ImGui::GetColorU32(ImGuiCol_Text); // right aligned and word wrapped, one line at a time const ImRect r(rect.Min.x + 5, rect.Min.y + 3, rect.Max.x - 5, rect.Max.y - 3); ImFont *font = ImGui::GetFont(); @@ -243,7 +243,7 @@ void LogsWidget::drawTable() { // fixed section sizes and a horizontal scrollbar, no alternating row colors; the grid is drawn between // rows and columns - ImGuiTableFlags flags = ImGuiTableFlags_ScrollY | ImGuiTableFlags_ScrollX | ImGuiTableFlags_BordersInner | + ImGuiTableFlags flags = ImGuiTableFlags_ScrollY | ImGuiTableFlags_ScrollX | ImGuiTableFlags_Borders | ImGuiTableFlags_SizingFixedFit; // an empty viewport draws no grid if (messages_.empty()) flags &= ~ImGuiTableFlags_BordersInnerV; @@ -279,13 +279,14 @@ void LogsWidget::drawTable() { if (!ImGui::TableSetColumnIndex(col)) continue; // cells are selected, not rows; there is no hover highlight, only the selection background const bool cell_selected = selected_row_ == row && selected_col_ == col; + const ImVec2 pos = ImGui::GetCursorScreenPos(); + const ImRect rect(pos, ImVec2(pos.x + ImGui::GetContentRegionAvail().x, pos.y + row_height - style.CellPadding.y * 2)); ImGui::PushID(col); - if (viewSelectable("##cell", cell_selected, ImGuiSelectableFlags_AllowOverlap, ImVec2(0, row_height - style.CellPadding.y * 2))) { + if (viewSelectable("##cell", cell_selected, ImGuiSelectableFlags_AllowOverlap, ImVec2(0, rect.GetHeight()))) { selected_row_ = row; selected_col_ = col; } ImGui::PopID(); - const ImRect rect = ImGui::TableGetCellBgRect(table, col); if (col == 0) { drawTextCell(painter, rect, formatTime(m.mono_time), cell_selected, false); } else if (hexMode()) { diff --git a/openpilot/tools/cabana/ui/widgets/messagebytes.cc b/openpilot/tools/cabana/ui/widgets/messagebytes.cc index 9052949eef..97261039ee 100644 --- a/openpilot/tools/cabana/ui/widgets/messagebytes.cc +++ b/openpilot/tools/cabana/ui/widgets/messagebytes.cc @@ -24,7 +24,7 @@ ImVec2 bytesCellSize(int n, bool multiple_lines) { } ImU32 cellTextColor(bool selected, bool inactive) { - if (selected) return inactive ? withAlpha(highlightedTextColor(), 100) : highlightedTextColor(); + if (selected && inactive) return withAlpha(ImGui::GetColorU32(ImGuiCol_Text), 100); return ImGui::GetColorU32(inactive ? ImGuiCol_TextDisabled : ImGuiCol_Text); } diff --git a/openpilot/tools/cabana/ui/widgets/messageswidget.cc b/openpilot/tools/cabana/ui/widgets/messageswidget.cc index 102960b9a9..6de1735d79 100644 --- a/openpilot/tools/cabana/ui/widgets/messageswidget.cc +++ b/openpilot/tools/cabana/ui/widgets/messageswidget.cc @@ -240,26 +240,30 @@ std::string MessagesWidget::whatsThis() const { } void MessagesWidget::drawToolBar() { - ImGui::Dummy(ImVec2(0, std::max(0.0f, 9 - ImGui::GetStyle().ItemSpacing.y))); - if (ImGui::Button("Suppress Highlighted")) suppressHighlighted(true); - ImGui::SameLine(); - ImGui::BeginDisabled(!suppress_clear_enabled_); - const std::string clear_label = suppress_clear_text_ + "##suppress_clear"; - if (ImGui::Button(clear_label.c_str())) suppressHighlighted(false); - ImGui::EndDisabled(); - disabledItemTooltip("Clear suppressed"); - const ImGuiStyle &style = ImGui::GetStyle(); - const float checkbox_width = ImGui::CalcTextSize("Suppress Signals").x + ImGui::GetFrameHeight() + style.ItemInnerSpacing.x; - const float view_button_width = ImGui::CalcTextSize(icon::THREE_DOTS).x + style.FramePadding.x * 2; - alignRight(checkbox_width + style.ItemSpacing.x + view_button_width); + // Reserve space for View so it remains accessible when other controls overflow. + const std::string clear_label = suppress_clear_text_ + "##suppress_clear"; + std::vector items; + items.push_back({toolbarButtonWidth("Suppress Highlighted"), [this]() { + if (ImGui::Button("Suppress Highlighted")) suppressHighlighted(true); + }, "Suppress Highlighted", [this]() { suppressHighlighted(true); }}); + items.push_back({toolbarButtonWidth(suppress_clear_text_), [this, &clear_label]() { + ImGui::BeginDisabled(!suppress_clear_enabled_); + if (ImGui::Button(clear_label.c_str())) suppressHighlighted(false); + ImGui::EndDisabled(); + disabledItemTooltip("Clear suppressed"); + }, suppress_clear_text_, [this]() { suppressHighlighted(false); }, suppress_clear_enabled_}); + const size_t spacer_index = items.size(); + items.push_back({ImGui::CalcTextSize("Suppress Signals").x + CHECKBOX_SIZE + style.ItemInnerSpacing.x, []() { + bool suppress_defined_signals = settings.suppress_defined_signals; + if (checkBox("Suppress Signals", &suppress_defined_signals)) can->suppressDefinedSignals(suppress_defined_signals); + ImGui::SetItemTooltip("Suppress defined signals"); + }}); - bool suppress_defined_signals = settings.suppress_defined_signals; - if (checkBox("Suppress Signals", &suppress_defined_signals)) can->suppressDefinedSignals(suppress_defined_signals); - ImGui::SetItemTooltip("Suppress defined signals"); + const float reserved = iconButtonWidth() + style.ItemSpacing.x; + drawToolbar(items, spacer_index, std::max(0.0f, ImGui::GetContentRegionAvail().x - reserved)); ImGui::SameLine(); - - if (toolButton("view_btn", icon::THREE_DOTS, "View...")) ImGui::OpenPopup("menu"); + if (iconButton("view_btn", icon::THREE_DOTS, "View...")) ImGui::OpenPopup("menu"); } void MessagesWidget::updateTitle() { @@ -430,7 +434,7 @@ void MessagesWidget::drawHeader() { } // the filter editors under the header - const float clear_width = ImGui::CalcTextSize(icon::X).x + ImGui::GetStyle().FramePadding.x * 2; + const float clear_width = iconButtonWidth(); ImGui::TableNextRow(); for (int i = 0; i < MessageList::COLUMN_COUNT; i++) { if (!ImGui::TableSetColumnIndex(i)) continue; diff --git a/openpilot/tools/cabana/ui/widgets/scrollabletabbar.cc b/openpilot/tools/cabana/ui/widgets/scrollabletabbar.cc index feda7ce34d..75d7c42788 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 ImGui::GetFrameHeight() * 2.0f + style.ItemInnerSpacing.x + style.ItemSpacing.x * 2.0f; + return style.ItemSpacing.x + ImGui::GetFrameHeight() * 2.0f + style.ItemInnerSpacing.x; } void drawScrollButtons(ImGuiTabBar *tab_bar) { diff --git a/openpilot/tools/cabana/ui/widgets/signalview.cc b/openpilot/tools/cabana/ui/widgets/signalview.cc index 1eff979b02..c0ed091568 100644 --- a/openpilot/tools/cabana/ui/widgets/signalview.cc +++ b/openpilot/tools/cabana/ui/widgets/signalview.cc @@ -24,7 +24,6 @@ constexpr float SIGNAL_ROW_EXTRA = 5.0f; // the tool button in the row makes it constexpr float SIGNAL_ROW_SCALE = 1.25f; constexpr float FILTER_WIDTH = 160.0f; constexpr float SPARKLINE_SLIDER_WIDTH = 120.0f; -constexpr float COLLAPSE_ICON_SIZE = 12.0f; // WARNING: increasing the maximum range can result in severe performance degradation. // 30s is a reasonable value at present. constexpr int SPARKLINE_RANGE_MAX = 30; @@ -261,13 +260,13 @@ void SignalView::paintCell(ImDrawList *painter, const ImRect &option_rect, const ImRect rect(option_rect.Min.x + h_margin, option_rect.Min.y + v_margin, option_rect.Max.x - h_margin, option_rect.Max.y - v_margin); // selection background is painted by the row's Selectable - const ImU32 text_color = selected ? highlightedTextColor() : ImGui::GetColorU32(ImGuiCol_Text); + const ImU32 text_color = ImGui::GetColorU32(ImGuiCol_Text); if (column == 0) { if (item->type == SignalModel::Item::Sig) { // color label ImRect icon_rect(rect.Min.x, rect.Min.y, rect.Min.x + COLOR_LABEL_WIDTH, rect.Max.y); - painter->AddRectFilled(icon_rect.Min, icon_rect.Max, toImU32(signalFillColor(item->sig->color).darker(item->highlight ? 125 : 0)), 3.0f); + painter->AddRectFilled(icon_rect.Min, icon_rect.Max, toImU32(signalFillColor(item->sig->color).darker(item->highlight ? 125 : 0)), ImGui::GetStyle().FrameRounding); drawText(painter, icon_rect, std::to_string(item->row() + 1).c_str(), item->highlight ? IM_COL32_WHITE : IM_COL32_BLACK, nullptr, LABEL_FONT); @@ -276,7 +275,7 @@ void SignalView::paintCell(ImDrawList *painter, const ImRect &option_rect, const if (item->sig->type != cabana::Signal::Type::Normal) { const std::string indicator = multiplexIndicator(item->sig); ImRect indicator_rect(rect.Min.x, rect.Min.y, rect.Min.x + ImGui::CalcTextSize(indicator.c_str()).x, rect.Max.y); - painter->AddRectFilled(indicator_rect.Min, indicator_rect.Max, IM_COL32(160, 160, 164, 255), 3.0f); + painter->AddRectFilled(indicator_rect.Min, indicator_rect.Max, IM_COL32(160, 160, 164, 255), ImGui::GetStyle().FrameRounding); drawElidedText(painter, indicator_rect, indicator, IM_COL32_WHITE, false); rect.Min.x = indicator_rect.Max.x + h_margin * 2; } @@ -313,13 +312,12 @@ void SignalView::paintCell(ImDrawList *painter, const ImRect &option_rect, const } // signal value rect.Min.x += value_adjust; - rect.Max.x -= button_size_.x; - if (rect.GetWidth() > 0) drawElidedText(painter, rect, text, text_color, true); - } else { - // no sparkline yet: the value still belongs against the buttons, where it sits once there is one - rect.Max.x -= button_size_.x; - if (rect.GetWidth() > 0) drawElidedText(painter, rect, text, text_color, true); } + // Monospaced digits prevent the value width from changing during playback. + rect.Max.x -= button_size_.x; + pushMonoFont(ImGui::GetFontSize()); + if (rect.GetWidth() > 0) drawElidedText(painter, rect, text, text_color, true); + popMonoFont(); } } @@ -385,7 +383,7 @@ void SignalView::drawEditor(SignalModel::Item *item) { const bool clicked = ImGui::Selectable("##editor", false, 0, ImVec2(0, rowHeight())); ImGui::PopStyleColor(); drawElidedText(ImGui::GetWindowDrawList(), ImRect(ImGui::GetItemRectMin(), ImGui::GetItemRectMax()), model_.valueText(item), - highlightedTextColor(), false); + ImGui::GetColorU32(ImGuiCol_Text), false); if (clicked || take_focus) { desc_dlg_ = std::make_unique(item->sig->val_desc); desc_dlg_->title = item->sig->name; @@ -493,12 +491,15 @@ void SignalView::drawValueDescriptionDlg() { desc_sig_ = nullptr; } +static ImVec2 indexButtonsSize(float button) { + return ImVec2(button * 2 + ImGui::GetStyle().ItemInnerSpacing.x * 2, button); +} + SignalView::SignalView(ChartsWidget *charts) : charts_(charts) { settings.sparkline_range = std::clamp(settings.sparkline_range, 1, SPARKLINE_RANGE_MAX); - // seed the size of the [plot][remove] widget (two 22px tool buttons plus the spacing) so the first - // updateState() calls already leave room for the sparklines - button_size_ = ImVec2(22 * 2 + TOOLBAR_ITEM_SPACING, 22); + // Reserve button space for updateState() calls before the first draw (no frame yet: derive the frame height). + button_size_ = indexButtonsSize(UI_FONT_SIZE + ImGui::GetStyle().FramePadding.y * 2.0f); updateToolBar(); connections_.push_back(model_.rowsChanged.connect([this]() { rowsChanged(); })); @@ -617,12 +618,14 @@ float SignalView::widestValueWidth(const cabana::Signal *sig) { const double raw_max = sig->is_signed ? std::ldexp(1.0, sig->size - 1) - 1 : std::ldexp(1.0, sig->size) - 1; const double raw_min = sig->is_signed ? -std::ldexp(1.0, sig->size - 1) : 0.0; float width = 0; + pushMonoFont(ImGui::GetFontSize()); for (double raw : {raw_min, raw_max}) { width = std::max(width, textWidth(sig->formatValue(raw * sig->factor + sig->offset))); } for (const auto &[_, desc] : sig->val_desc) { width = std::max(width, textWidth(desc)); } + popMonoFont(); return width; } @@ -672,8 +675,7 @@ void SignalView::updateState(const std::set *msgs) { // the sparkline label, the range slider and the collapse button float SignalView::toolBarRightWidth(const std::string &range_label) { const ImGuiStyle &style = ImGui::GetStyle(); - return ImGui::CalcTextSize(range_label.c_str()).x + style.ItemSpacing.x + SPARKLINE_SLIDER_WIDTH + style.ItemSpacing.x + - ImGui::GetFont()->CalcTextSizeA(COLLAPSE_ICON_SIZE, FLT_MAX, 0.0f, icon::DASH_SQUARE).x + style.FramePadding.x * 2; + return ImGui::CalcTextSize(range_label.c_str()).x + style.ItemSpacing.x + SPARKLINE_SLIDER_WIDTH + style.ItemSpacing.x + iconButtonWidth(); } // the width at which the tool bar stops squishing: the signal count and the filter box on the left, the @@ -686,8 +688,10 @@ float SignalView::minimumWidth() { } void SignalView::draw() { + ImGui::PushStyleColor(ImGuiCol_ChildBg, ImGui::GetStyleColorVec4(ImGuiCol_WindowBg)); if (!ImGui::BeginChild("SignalView", ImVec2(0, 0), ImGuiChildFlags_Borders)) { ImGui::EndChild(); + ImGui::PopStyleColor(); return; } @@ -710,11 +714,7 @@ void SignalView::draw() { } ImGui::SetItemTooltip("Sparkline time range"); ImGui::SameLine(); - // auto-raise tool button with a 12x12 icon - ImGui::PushFont(ImGui::GetFont(), COLLAPSE_ICON_SIZE); - const bool collapse = toolButton("collapse_all", icon::DASH_SQUARE, "Collapse All"); - ImGui::PopFont(); - if (collapse) collapseAll(); + if (iconButton("collapse_all", icon::ARROWS_COLLAPSE, "Collapse All")) collapseAll(); drawTree(); drawValueDescriptionDlg(); @@ -724,6 +724,7 @@ void SignalView::draw() { current_row_ = model_.signalRow(current_sig_); // used when the row is removed ImGui::EndChild(); + ImGui::PopStyleColor(); } void SignalView::collapseAll() { @@ -872,27 +873,24 @@ bool SignalView::drawItem(SignalModel::Item *item, int depth, DrawContext &ctx) } void SignalView::drawIndexWidget(SignalModel::Item *item, const ImRect &rect) { - // plot_btn + remove_btn, right aligned in the value column - ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(3.0f, 2.0f)); - const ImVec2 btn_size(ImGui::CalcTextSize(icon::GRAPH_UP).x + 6.0f, ImGui::GetFrameHeight()); - const ImVec2 size(btn_size.x * 2 + TOOLBAR_ITEM_SPACING, btn_size.y); + const float spacing = ImGui::GetStyle().ItemInnerSpacing.x; + const ImVec2 size = indexButtonsSize(iconButtonWidth()); ImGui::SetCursorScreenPos(ImVec2(rect.Max.x - size.x, rect.Min.y + (rect.GetHeight() - size.y) * 0.5f)); const auto sig = item->sig; const bool checked = item->chart_opened; if (checked) ImGui::PushStyleColor(ImGuiCol_Button, ImGui::GetStyleColorVec4(ImGuiCol_ButtonActive)); - if (ImGui::Button((std::string(icon::GRAPH_UP) + "##plot").c_str(), btn_size) && !editor_open_on_press_) { + if (iconButton("plot", icon::GRAPH_UP) && !editor_open_on_press_) { item->chart_opened = !checked; showChart(model_.msgId(), sig, item->chart_opened, ImGui::GetIO().KeyShift); } if (checked) ImGui::PopStyleColor(); ImGui::SetItemTooltip("%s", checked ? "Close Plot" : "Show Plot\nSHIFT click to add to previous opened plot"); - ImGui::SameLine(0.0f, TOOLBAR_ITEM_SPACING); - if (ImGui::Button((std::string(icon::X) + "##remove").c_str(), btn_size) && !editor_open_on_press_) { + ImGui::SameLine(0.0f, spacing); + if (iconButton("remove", icon::X_LG) && !editor_open_on_press_) { pending_action_ = [this, sig]() { UndoStack::instance()->push(new RemoveSigCommand(model_.msgId(), sig)); }; } ImGui::SetItemTooltip("Remove signal"); - ImGui::PopStyleVar(); button_size_ = size; } @@ -914,12 +912,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 (ImGui::Button(icon::PLUS)) { + if (iconButton("add", icon::PLUS_LG, "Add")) { table_.emplace_back("", ""); } - ImGui::SameLine(); + ImGui::SameLine(0.0f, ImGui::GetStyle().ItemInnerSpacing.x); ImGui::BeginDisabled(current_row_ == -1); - if (ImGui::Button(icon::DASH) && current_row_ < table_.size()) { + if (iconButton("remove", icon::DASH_LG, "Remove") && current_row_ < table_.size()) { table_.erase(table_.begin() + current_row_); current_row_ = -1; } diff --git a/openpilot/tools/cabana/ui/widgets/videowidget.cc b/openpilot/tools/cabana/ui/widgets/videowidget.cc index 7cabac840a..91b41184c0 100644 --- a/openpilot/tools/cabana/ui/widgets/videowidget.cc +++ b/openpilot/tools/cabana/ui/widgets/videowidget.cc @@ -141,7 +141,6 @@ std::string VideoWidget::whatsThis() const { static float toolbarHeight() { return TOOLBAR_MARGIN_Y + ImGui::GetFrameHeight(); } void VideoWidget::drawPlaybackController() { - beginToolbar(); ImGui::SetCursorPosY(ImGui::GetCursorPosY() + TOOLBAR_MARGIN_Y); const float speed_width = menuButtonWidth("0.05x", true); @@ -152,37 +151,29 @@ void VideoWidget::drawPlaybackController() { : formatTime(can->currentSec(), true); const char *time_tooltip = settings.absolute_time ? "Elapsed time" : "Absolute time"; - auto seek_backward = []() { can->seekTo(can->currentSec() - 1); }; - auto toggle_play = []() { can->pause(!can->isPaused()); }; - auto seek_forward = []() { can->seekTo(can->currentSec() + 1); }; - std::vector items = { - {toolbarButtonWidth(icon::REWIND), [&]() { if (toolButton("rewind", icon::REWIND, "Seek backward")) seek_backward(); }, - "Seek backward", seek_backward}, - {toolbarButtonWidth(play_icon), [&]() { if (toolButton("play", play_icon, play_tooltip)) toggle_play(); }, - play_tooltip, toggle_play}, - {toolbarButtonWidth(icon::FAST_FORWARD), [&]() { if (toolButton("fast-forward", icon::FAST_FORWARD, "Seek forward")) seek_forward(); }, - "Seek forward", seek_forward}, + toolbarAction("rewind", icon::REWIND, "Seek backward", []() { can->seekTo(can->currentSec() - 1); }), + toolbarAction("play", play_icon, play_tooltip, []() { can->pause(!can->isPaused()); }, true, true), + toolbarAction("fast-forward", icon::FAST_FORWARD, "Seek forward", []() { can->seekTo(can->currentSec() + 1); }, true, true), }; if (can->liveStreaming()) { - items.push_back({toolbarButtonWidth(icon::SKIP_END), [&]() { - ImGui::BeginDisabled(!skip_to_end_enabled_); - if (toolButton("skip-end", icon::SKIP_END, "Skip to the end")) skipToEnd(); - ImGui::EndDisabled(); - }, "Skip to the end", [this]() { skipToEnd(); }, skip_to_end_enabled_}); + items.push_back(toolbarAction("skip-end", icon::SKIP_END, "Skip to the end", [this]() { skipToEnd(); }, skip_to_end_enabled_, true)); } if (slider_ || msgs_received_) { // a mono font: with proportional digits the time changed width as it ticked and the items after it moved pushMonoFont(ImGui::GetFontSize()); - const float time_width = toolbarButtonWidth(time_text); + const float time_width = ImGui::CalcTextSize(time_text.c_str()).x; popMonoFont(); items.push_back({time_width, [&]() { pushMonoFont(ImGui::GetFontSize()); - if (toolButton("time_display", time_text.c_str(), time_tooltip)) toggleTimeDisplay(); + ImGui::AlignTextToFramePadding(); + ImGui::TextUnformatted(time_text.c_str()); popMonoFont(); + if (ImGui::IsItemClicked()) toggleTimeDisplay(); + ImGui::SetItemTooltip("%s", time_tooltip); }, - time_text, [this]() { toggleTimeDisplay(); }}); + time_tooltip, [this]() { toggleTimeDisplay(); }}); } // the expanding spacer: the items after it are right aligned as long as everything fits const size_t spacer_index = items.size(); @@ -195,27 +186,22 @@ void VideoWidget::drawPlaybackController() { ImGui::GetWindowDrawList()->AddLine(ImVec2(x, min.y + 4.0f), ImVec2(x, min.y + ImGui::GetFrameHeight() - 4.0f), 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; - items.push_back({toolbarButtonWidth(aspect_ratio_icon), [&]() { - if (toolButton("crop_video", aspect_ratio_icon, "Crop to fill")) cropVideoClicked(); - }, "Crop to fill", [this]() { cropVideoClicked(); }}); + items.push_back(toolbarAction("crop_video", aspect_ratio_icon, "Crop to fill", [this]() { cropVideoClicked(); })); if (!can->liveStreaming()) { items.push_back(separator()); - items.push_back({toolbarButtonWidth(loop_icon), [&]() { if (toolButton("loop", loop_icon, "Loop playback")) loopPlaybackClicked(); }, - "Loop playback", [this]() { loopPlaybackClicked(); }}); + items.push_back(toolbarAction("loop", loop_icon, "Loop playback", [this]() { loopPlaybackClicked(); }, true, true)); } - items.push_back({speed_width, [&]() { drawSpeedDropdown(speed_width); }}); + items.push_back(toolbarMenu("speed_btn", speed_text_, "Speed", [this]() { drawSpeedMenuItems(); }, true, true, speed_width)); if (!can->liveStreaming()) { items.push_back(separator()); - items.push_back({toolbarButtonWidth(icon::INFO_CIRCLE), - [&]() { if (toolButton("route_info", icon::INFO_CIRCLE, "View route details")) showRouteInfo(); }, - "View route details", [this]() { showRouteInfo(); }}); + items.push_back(toolbarAction("route_info", icon::INFO_CIRCLE, "View route details", [this]() { showRouteInfo(); }, true, true)); } drawToolbar(items, spacer_index); - endToolbar(); } void VideoWidget::skipToEnd() { @@ -241,14 +227,6 @@ void VideoWidget::createSpeedDropdown() { speed_text_ = speedText(speeds[speed_index_]); } -void VideoWidget::drawSpeedDropdown(float width) { - menuButton("speed_btn", speed_text_, "speed_menu", true, width); - if (ImGui::BeginPopup("speed_menu")) { - drawSpeedMenuItems(); - ImGui::EndPopup(); - } -} - void VideoWidget::drawSpeedMenuItems() { // every row declares the same width, so the popup is exactly as wide as the widest one and all the // highlights reach both edges; the label is padded on the right as much as the check column on the left @@ -436,7 +414,7 @@ void Slider::paint(double thumbnail_time) { groove_rect.Min.y = std::floor(center_y - groove_height / 2); groove_rect.Max.y = groove_rect.Min.y + groove_height; - p->AddRectFilled(groove_rect.Min, groove_rect.Max, timeline_colors[(int)TimelineType::None]); + p->AddRectFilled(groove_rect.Min, groove_rect.Max, timeline_colors[(int)TimelineType::None], groove_height * 0.5f); double min = minimum() / factor; double max = maximum() / factor; @@ -471,7 +449,7 @@ void Slider::paint(double thumbnail_time) { if (thumbnail_time >= 0) { float left = rect_.Min.x + (float)((thumbnail_time - min) * width() / span) - 1; ImRect rc(ImVec2(left, rect_.Min.y + 1), ImVec2(left + 2, rect_.Max.y - 1)); - p->AddRectFilled(rc.Min, rc.Max, ImGui::GetColorU32(ImGuiCol_Header), 1.5f); // ImGuiCol_Header is the theme highlight + p->AddRectFilled(rc.Min, rc.Max, ImGui::GetColorU32(ImGuiCol_Header), 1.0f); } } @@ -558,10 +536,10 @@ const RgbImage *StreamCameraView::thumbnailAt(double sec) { } void StreamCameraView::drawScrubThumbnail(ImDrawList *p, double sec) { - p->AddRectFilled(rect().Min, rect().Max, IM_COL32(0, 0, 0, 255)); + p->AddRectFilled(rect().Min, rect().Max, IM_COL32(0, 0, 0, 255), ImGui::GetStyle().ChildRounding); if (const RgbImage *image = thumbnailAt(sec)) { const VideoPlacement placement = videoPlacement(rect(), (float)image->width / image->height, settings.crop_video); - p->AddImage(big_thumbnail_texture_.ref(), placement.min, placement.max, placement.uv0, placement.uv1); + p->AddImageRounded(big_thumbnail_texture_.ref(), placement.min, placement.max, placement.uv0, placement.uv1, IM_COL32_WHITE, ImGui::GetStyle().ChildRounding); drawTime(p, rect(), sec); } } @@ -578,8 +556,8 @@ void StreamCameraView::drawThumbnail(ImDrawList *p, double sec) { int y = height() - h - THUMBNAIL_MARGIN; ImRect thumb_rect(ImVec2(rect().Min.x + x, rect().Min.y + y), ImVec2(rect().Min.x + x + w, rect().Min.y + y + h)); - p->AddImage(big_thumbnail_texture_.ref(), thumb_rect.Min, thumb_rect.Max); - p->AddRect(thumb_rect.Min, thumb_rect.Max, paletteBrightText(), 0.0f, 0, 2.0f); + p->AddImageRounded(big_thumbnail_texture_.ref(), thumb_rect.Min, thumb_rect.Max, ImVec2(0, 0), ImVec2(1, 1), IM_COL32_WHITE, ImGui::GetStyle().FrameRounding); + p->AddRect(thumb_rect.Min, thumb_rect.Max, IM_COL32_WHITE, ImGui::GetStyle().FrameRounding, 0, 2.0f); // look up the alert at the hovered time, the thumbnail frame itself can be seconds away if (auto alert = getReplay()->findAlertAtTime(sec)) { drawAlert(p, thumb_rect, *alert, POINT_10_FONT_SIZE); @@ -595,11 +573,11 @@ void StreamCameraView::drawTime(ImDrawList *p, const ImRect &rect, double second const ImVec2 text_size = font->CalcTextSizeA(POINT_10_FONT_SIZE, FLT_MAX, 0.0f, text); // centered horizontally, above the bottom margin p->AddText(font, POINT_10_FONT_SIZE, ImVec2(rect.GetCenter().x - text_size.x / 2, rect.Max.y - THUMBNAIL_MARGIN - text_size.y), - paletteBrightText(), text); + IM_COL32_WHITE, text); } void StreamCameraView::drawAlert(ImDrawList *p, const ImRect &rect, const Timeline::Entry &alert, float font_size) { - const ImU32 pen = paletteBrightText(); + const ImU32 pen = IM_COL32_WHITE; ImU32 color = withAlpha(timeline_colors[int(alert.type)], 128); std::string text = alert.text1; if (!alert.text2.empty()) text += "\n" + alert.text2; @@ -608,7 +586,7 @@ void StreamCameraView::drawAlert(ImDrawList *p, const ImRect &rect, const Timeli ImFont *font = ImGui::GetFont(); const float wrap_width = std::max(1.0f, text_rect.GetWidth()); const ImVec2 r = font->CalcTextSizeA(font_size, FLT_MAX, wrap_width, text.c_str()); - p->AddRectFilled(ImVec2(text_rect.Min.x, text_rect.Min.y), ImVec2(text_rect.Max.x, text_rect.Min.y + r.y), color); + p->AddRectFilled(ImVec2(text_rect.Min.x, text_rect.Min.y), ImVec2(text_rect.Max.x, text_rect.Min.y + r.y), color, ImGui::GetStyle().FrameRounding, ImDrawFlags_RoundCornersTop); // each line is centered, wrapped continuations stay left aligned float y = text_rect.Min.y; for (const auto &line : utils::split(text, '\n')) { diff --git a/openpilot/tools/cabana/ui/widgets/videowidget.h b/openpilot/tools/cabana/ui/widgets/videowidget.h index 90058850c3..a1f6076a64 100644 --- a/openpilot/tools/cabana/ui/widgets/videowidget.h +++ b/openpilot/tools/cabana/ui/widgets/videowidget.h @@ -102,7 +102,6 @@ private: void skipToEnd(); void toggleTimeDisplay(); void createSpeedDropdown(); - void drawSpeedDropdown(float width); void drawSpeedMenuItems(); void loopPlaybackClicked(); void cropVideoClicked(); diff --git a/openpilot/tools/cabana/utils/util.h b/openpilot/tools/cabana/utils/util.h index 203c504c44..3570320f34 100644 --- a/openpilot/tools/cabana/utils/util.h +++ b/openpilot/tools/cabana/utils/util.h @@ -60,20 +60,6 @@ ValidState validateIpAddress(const std::string &input); // C-locale floating-point ValidState validateDouble(const std::string &input); -struct DarkTheme { - static constexpr CabanaColor window{0x35, 0x35, 0x35}; - static constexpr CabanaColor window_text{0xff, 0xff, 0xff}; - static constexpr CabanaColor base{0x19, 0x19, 0x19}; - static constexpr CabanaColor tooltip_text{0xff, 0xff, 0xff}; - static constexpr CabanaColor text{0xff, 0xff, 0xff}; - static constexpr CabanaColor button{0x35, 0x35, 0x35}; - static constexpr CabanaColor highlight{0x2a, 0x82, 0xda}; - static constexpr CabanaColor bright_text{0xff, 0xff, 0xff}; - static constexpr CabanaColor disabled_text{0x7f, 0x7f, 0x7f}; - static constexpr CabanaColor light{0x50, 0x50, 0x50}; - static constexpr CabanaColor dark{0x23, 0x23, 0x23}; -}; - namespace utils { bool isMainThread();