cabana: standardize button spacing (#38873)

* cabana: standardize button spacing and control alignment

* cabana: center shared step controls and inset chart content

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