diff --git a/openpilot/tools/cabana/ui/theme.cc b/openpilot/tools/cabana/ui/theme.cc index 20cf2919ff..eda301a566 100644 --- a/openpilot/tools/cabana/ui/theme.cc +++ b/openpilot/tools/cabana/ui/theme.cc @@ -208,8 +208,10 @@ CabanaColor byteColor(const CabanaColor &color) { CabanaColor signalFill(const CabanaColor &color, bool defined) { const bool dark = palette().text.x > 0.5f; + // Constrain the full-strength color before blending to preserve the activity ramp. + if (defined) return composite(boundLuminance(color, 0.18, !dark), fromImVec4(palette().surface)); auto fill = composite(color, fromImVec4(palette().surface)); - fill = composite(dark ? CabanaColor(0, 0, 0, 115) : CabanaColor(255, 255, 255, defined ? 20 : 40), fill); + fill = composite(dark ? CabanaColor(0, 0, 0, 115) : CabanaColor(255, 255, 255, 40), fill); return boundLuminance(fill, 0.18, !dark); // 4.5:1 with the theme's text } diff --git a/openpilot/tools/cabana/ui/widgets/binaryview.cc b/openpilot/tools/cabana/ui/widgets/binaryview.cc index eb8b77b43c..56f67075f4 100644 --- a/openpilot/tools/cabana/ui/widgets/binaryview.cc +++ b/openpilot/tools/cabana/ui/widgets/binaryview.cc @@ -19,7 +19,7 @@ namespace { const int CELL_HEIGHT = 36; const float CELL_FONT_SIZE = UI_FONT_SIZE + 2.0f; const float SMALL_FONT_SIZE = 10.0f; // Inter needs 10 px for a 7 px cap height -const int VERTICAL_HEADER_WIDTH = 30; +const int GRID_COLUMN_COUNT = BinaryView::COLUMN_COUNT + 1; inline int get_bit_pos(const BinaryIndex &index) { return flipBitPos(index.row * 8 + index.column); } inline ImU32 paletteBase() { return ImGui::GetColorU32(ImGuiCol_ChildBg); } @@ -126,7 +126,7 @@ ImVec2 BinaryView::minimumSizeHint() const { pushMonoFont(CELL_FONT_SIZE); const float min_section_size = std::ceil(ImGui::CalcTextSize("FF").x) + 10.0f; popMonoFont(); - return {(min_section_size + 1) * 9 + VERTICAL_HEADER_WIDTH + 2, + return {min_section_size * GRID_COLUMN_COUNT + 2, static_cast(CELL_HEIGHT * std::min(row_count_, 10) + 2)}; } @@ -173,6 +173,8 @@ void BinaryView::highlightPosition(const ImVec2 &pos) { auto item = &cellAt(index); const cabana::Signal *sig = item->sigs.empty() ? nullptr : item->sigs.back(); highlight(sig); + } else { + highlight(nullptr); } } @@ -292,18 +294,18 @@ std::tuple BinaryView::getSelection(BinaryIndex index) { } BinaryIndex BinaryView::indexAt(const ImVec2 &pos) const { - if (column_width_ <= 0 || pos.x < grid_pos_.x + VERTICAL_HEADER_WIDTH || pos.y < grid_pos_.y) return {}; - int column = static_cast((pos.x - grid_pos_.x - VERTICAL_HEADER_WIDTH) / column_width_); + if (column_width_ <= 0 || pos.x < grid_pos_.x + IM_ROUND(column_width_) || pos.y < grid_pos_.y) return {}; + int column = 0; + while (column < COLUMN_COUNT && pos.x >= grid_pos_.x + IM_ROUND((column + 2) * column_width_)) ++column; int row = static_cast((pos.y - grid_pos_.y) / CELL_HEIGHT); if (column >= COLUMN_COUNT || row >= row_count_) return {}; return {row, column}; } ImRect BinaryView::visualRect(const BinaryIndex &index) const { - // sections are integral: round the edges so neighboring cells share them exactly and the cells are - // painted edge to edge with no grid line between them - const float x0 = grid_pos_.x + VERTICAL_HEADER_WIDTH + IM_ROUND(index.column * column_width_); - const float x1 = grid_pos_.x + VERTICAL_HEADER_WIDTH + IM_ROUND((index.column + 1) * column_width_); + // Round shared edges consistently across the byte index, bits, and hex column. + const float x0 = grid_pos_.x + IM_ROUND((index.column + 1) * column_width_); + const float x1 = grid_pos_.x + IM_ROUND((index.column + 2) * column_width_); const float y = grid_pos_.y + index.row * CELL_HEIGHT; return ImRect(x0, y, x1, y + CELL_HEIGHT); } @@ -320,17 +322,16 @@ void BinaryView::draw() { pushMonoFont(CELL_FONT_SIZE); const float min_column_width = std::ceil(ImGui::CalcTextSize("FF").x) + 10.0f; popMonoFont(); - 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); + const float width = std::max(ImGui::GetContentRegionAvail().x, min_column_width * GRID_COLUMN_COUNT); + column_width_ = std::max(min_column_width, width / GRID_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(); painter->AddRectFilled(grid_pos_, ImVec2(grid_pos_.x + width, grid_pos_.y + rows * CELL_HEIGHT), paletteBase()); for (int row = 0; row < rows; ++row) { - const ImRect r(grid_pos_.x, grid_pos_.y + row * CELL_HEIGHT, grid_pos_.x + VERTICAL_HEADER_WIDTH, grid_pos_.y + (row + 1) * CELL_HEIGHT); - painter->AddRectFilled(r.Min, r.Max, ImGui::GetColorU32(ImGuiCol_WindowBg)); // plain header background - drawText(painter, r, std::to_string(row).c_str(), paletteText()); + const ImRect r(grid_pos_.x, grid_pos_.y + row * CELL_HEIGHT, grid_pos_.x + IM_ROUND(column_width_), grid_pos_.y + (row + 1) * CELL_HEIGHT); + drawText(painter, r, std::to_string(row).c_str(), paletteText(), nullptr, CELL_FONT_SIZE); } for (int row = 0; row < rows; ++row) { for (int column = 0; column < COLUMN_COUNT; ++column) { @@ -339,6 +340,29 @@ void BinaryView::draw() { } } + // One consistent grid over every cell; hover never changes its geometry. + const ImU32 grid_color = ImGui::GetColorU32(ImGuiCol_Border, 0.35f); + const float right = grid_pos_.x + IM_ROUND(GRID_COLUMN_COUNT * column_width_); + const float bottom = grid_pos_.y + rows * CELL_HEIGHT; + for (int column = 0; column <= GRID_COLUMN_COUNT; ++column) { + const float x = std::clamp(grid_pos_.x + IM_ROUND(column * column_width_), grid_pos_.x + 0.5f, right - 0.5f); + painter->AddLine(ImVec2(x, grid_pos_.y), ImVec2(x, bottom), grid_color); + } + for (int row = 0; row <= rows; ++row) { + const float y = grid_pos_.y + row * CELL_HEIGHT; + painter->AddLine(ImVec2(grid_pos_.x, y), ImVec2(right, y), grid_color); + } + + // Signal definitions are a separate layer from heatmap activity. + for (int row = 0; row < rows; ++row) { + for (int column = 0; column < HEX_COLUMN; ++column) { + const BinaryIndex index{row, column}; + for (const auto *sig : cellAt(index).sigs) { + if (!hasSelection() || sig != resize_sig_) drawSignalOutline(painter, visualRect(index), index, sig); + } + } + } + const ImVec2 mouse = ImGui::GetMousePos(); const bool hovered = ImGui::IsItemHovered(); const bool active = ImGui::IsItemActive(); @@ -443,19 +467,12 @@ const std::vector> &BinaryView::bitFlipChanges(size_t ms return bit_flip_tracker_.counts.bits; } -bool BinaryView::hasSignal(const BinaryIndex &index, int dx, int dy, const cabana::Signal *sig) const { - if (!index.isValid()) return false; - int idx = (index.row + dy) * COLUMN_COUNT + index.column + dx; - if (idx < 0 || idx >= (int)cells_.size()) return false; - auto &s = cells_[idx].sigs; - return std::find(s.begin(), s.end(), sig) != s.end(); -} - void BinaryView::paintCell(ImDrawList *painter, const ImRect &rect, const BinaryIndex &index) const { auto item = &cellAt(index); ImFont *font = ImGui::GetFont(); float font_size = CELL_FONT_SIZE; ImU32 pen = paletteText(); + const bool hovered = hovered_sig_ && std::find(item->sigs.begin(), item->sigs.end(), hovered_sig_) != item->sigs.end(); if (index.column == HEX_COLUMN) { if (item->valid) { @@ -473,12 +490,12 @@ void BinaryView::paintCell(ImDrawList *painter, const ImRect &rect, const Binary for (auto &s : item->sigs) { drawSignalCell(painter, rect, index, s); } + // Hover covers the entire signal, including bits shared with another definition. + if (hovered) painter->AddRectFilled(rect.Min, rect.Max, toImU32(signalHighlight(hovered_sig_->color))); } else if (item->valid) { if (item->bg_color.alpha() > 0) painter->AddRectFilled(rect.Min, rect.Max, toImU32(signalFill(item->bg_color, false))); } - bool bright = std::find(item->sigs.begin(), item->sigs.end(), hovered_sig_) != item->sigs.end(); - if (bright) painter->AddRectFilled(rect.Min, rect.Max, toImU32(signalHighlight(hovered_sig_->color))); - pen = bright ? IM_COL32_WHITE : paletteText(); + pen = hovered ? IM_COL32_WHITE : paletteText(); } const auto text = fromImVec4(ImGui::ColorConvertU32ToFloat4(pen)); @@ -488,6 +505,7 @@ void BinaryView::paintCell(ImDrawList *painter, const ImRect &rect, const Binary } else if (!item->valid) { fillBDiagPattern(painter, rect, pattern); } + if (item->valid) { if (index.column == HEX_COLUMN) { drawBoldText(painter, rect, utils::hexByte(item->val), pen, font, font_size); @@ -501,64 +519,38 @@ void BinaryView::paintCell(ImDrawList *painter, const ImRect &rect, const Binary } } -// Draw border on edge of signal -void BinaryView::drawSignalCell(ImDrawList *painter, const ImRect &rect, const BinaryIndex &index, const cabana::Signal *sig) const { - bool draw_left = !hasSignal(index, -1, 0, sig); - bool draw_top = !hasSignal(index, 0, -1, sig); - bool draw_right = !hasSignal(index, 1, 0, sig); - bool draw_bottom = !hasSignal(index, 0, 1, sig); +bool BinaryView::hasSignal(const BinaryIndex &index, int dx, int dy, const cabana::Signal *sig) const { + const int row = index.row + dy; + const int column = index.column + dx; + if (row < 0 || row >= row_count_ || column < 0 || column >= HEX_COLUMN) return false; + const auto &signals = cellAt({row, column}).sigs; + return std::find(signals.begin(), signals.end(), sig) != signals.end(); +} - const int spacing = 2; - ImRect rc(rect.Min.x + draw_left * 3, rect.Min.y + draw_top * spacing, rect.Max.x - draw_right * 3, rect.Max.y - draw_bottom * spacing); - std::vector subtract; - if (!draw_top) { - if (!draw_left && !hasSignal(index, -1, -1, sig)) { - subtract.emplace_back(rc.Min.x, rc.Min.y, rc.Min.x + 3, rc.Min.y + spacing); - } else if (!draw_right && !hasSignal(index, 1, -1, sig)) { - subtract.emplace_back(rc.Max.x - 3, rc.Min.y, rc.Max.x, rc.Min.y + spacing); - } - } - if (!draw_bottom) { - if (!draw_left && !hasSignal(index, -1, 1, sig)) { - subtract.emplace_back(rc.Min.x, rc.Max.y - spacing, rc.Min.x + 3, rc.Max.y); - } else if (!draw_right && !hasSignal(index, 1, 1, sig)) { - subtract.emplace_back(rc.Max.x - 3, rc.Max.y - spacing, rc.Max.x, rc.Max.y); - } - } - // rc split into horizontal bands with the notch corners removed: at most one notch in the top band and - // one in the bottom band - const ImRect *top_notch = !subtract.empty() && subtract.front().Min.y == rc.Min.y ? &subtract.front() : nullptr; - const ImRect *bottom_notch = !subtract.empty() && subtract.back().Min.y != rc.Min.y ? &subtract.back() : nullptr; - std::vector region; - auto band = [&](const ImRect *notch, float y0, float y1) { - const float x0 = notch && notch->Min.x == rc.Min.x ? notch->Max.x : rc.Min.x; - const float x1 = notch && notch->Min.x != rc.Min.x ? notch->Min.x : rc.Max.x; - if (x1 > x0 && y1 > y0) region.emplace_back(x0, y0, x1, y1); +void BinaryView::drawSignalOutline(ImDrawList *painter, const ImRect &rect, const BinaryIndex &index, + const cabana::Signal *sig) const { + // Definition boundaries remain visible even at zero activity. Keep them above the + // fills and grid, at the same fixed position before and during hover. + const ImU32 edge = toImU32(signalOutline(sig->color, sig == hovered_sig_)); + const bool left = !hasSignal(index, -1, 0, sig); + const bool right = !hasSignal(index, 1, 0, sig); + const bool top = !hasSignal(index, 0, -1, sig); + const bool bottom = !hasSignal(index, 0, 1, sig); + // Extend one pixel into adjacent cells to join inward corners. + auto offset = [&](bool boundary, int dx, int dy) { + return boundary ? 1.0f : hasSignal(index, dx, dy, sig) ? -1.0f : 0.0f; }; - if (top_notch) band(top_notch, rc.Min.y, rc.Min.y + spacing); - band(nullptr, rc.Min.y + (top_notch ? spacing : 0), rc.Max.y - (bottom_notch ? spacing : 0)); - if (bottom_notch) band(bottom_notch, rc.Max.y - spacing, rc.Max.y); + const ImVec2 min = rect.Min, max = rect.Max; + if (left) painter->AddLine(ImVec2(min.x + 1, min.y + offset(top, -1, -1)), ImVec2(min.x + 1, max.y - offset(bottom, -1, 1)), edge); + if (right) painter->AddLine(ImVec2(max.x - 1, min.y + offset(top, 1, -1)), ImVec2(max.x - 1, max.y - offset(bottom, 1, 1)), edge); + if (top) painter->AddLine(ImVec2(min.x + offset(left, -1, -1), min.y + 1), ImVec2(max.x - offset(right, 1, -1), min.y + 1), edge); + if (bottom) painter->AddLine(ImVec2(min.x + offset(left, -1, 1), max.y - 1), ImVec2(max.x - offset(right, 1, 1), max.y - 1), edge); +} - auto item = &cellAt(index); +// Signal intensity and hover share the same full-cell geometry. +void BinaryView::drawSignalCell(ImDrawList *painter, const ImRect &rect, const BinaryIndex &index, const cabana::Signal *sig) const { CabanaColor color = sig->color; - color.a = 96 + item->bg_color.alpha() * (255 - 96) / 255; - const ImU32 edge = toImU32(signalOutline(sig->color)); - - for (const ImRect &clip : region) { - painter->PushClipRect(clip.Min, clip.Max, true); - // mix the signal color with the background to fade it - painter->AddRectFilled(rc.Min, rc.Max, paletteBase()); - painter->AddRectFilled(rc.Min, rc.Max, toImU32(signalFill(color))); - - if (draw_left) painter->AddLine(ImVec2(rc.Min.x + 0.5f, rc.Min.y), ImVec2(rc.Min.x + 0.5f, rc.Max.y), edge, 1.0f); - if (draw_right) painter->AddLine(ImVec2(rc.Max.x - 0.5f, rc.Min.y), ImVec2(rc.Max.x - 0.5f, rc.Max.y), edge, 1.0f); - if (draw_bottom) painter->AddLine(ImVec2(rc.Min.x, rc.Max.y - 0.5f), ImVec2(rc.Max.x, rc.Max.y - 0.5f), edge, 1.0f); - if (draw_top) painter->AddLine(ImVec2(rc.Min.x, rc.Min.y + 0.5f), ImVec2(rc.Max.x, rc.Min.y + 0.5f), edge, 1.0f); - - // fill gaps inside corners: the 2px stroke is clipped to the region, only the half outside the notch is painted - for (auto &r : subtract) { - painter->AddRect(r.Min, r.Max, edge, 0.0f, 0, 2.0f); - } - painter->PopClipRect(); - } + color.a = 96 + cellAt(index).bg_color.alpha() * (255 - 96) / 255; + painter->AddRectFilled(rect.Min, rect.Max, paletteBase()); + painter->AddRectFilled(rect.Min, rect.Max, toImU32(signalFill(color))); } diff --git a/openpilot/tools/cabana/ui/widgets/binaryview.h b/openpilot/tools/cabana/ui/widgets/binaryview.h index 507d05811c..4f1afa1032 100644 --- a/openpilot/tools/cabana/ui/widgets/binaryview.h +++ b/openpilot/tools/cabana/ui/widgets/binaryview.h @@ -76,6 +76,7 @@ private: void paintCell(ImDrawList *painter, const ImRect &rect, const BinaryIndex &index) const; bool hasSignal(const BinaryIndex &index, int dx, int dy, const cabana::Signal *sig) const; + void drawSignalOutline(ImDrawList *painter, const ImRect &rect, const BinaryIndex &index, const cabana::Signal *sig) const; void drawSignalCell(ImDrawList *painter, const ImRect &rect, const BinaryIndex &index, const cabana::Signal *sig) const; MessageId msg_id_;