cabana: restyle (#38789)

* cabana: connect palette theme, rounded cards, floating page switch

Move the jotpluggler port's connect palette into a shared theme module
(ui/theme.h/.cc) and apply it to all of cabana: Palette struct for dark
and light, applyTheme fills ImGui and ImPlot styles, readableCurveColor
and sectionTitle for reuse by the analysis workspace. Drop the old
DarkTheme constants and per-theme color branches in widgets.

Docked panels are borderless with content in rounded bordered cards;
custom-drawn rects use the style radii. The detail widget's bottom tab
bar is replaced by a floating Messages/Logs pill.

* cabana: scrollbar and menu contrast, page switch below the page

The scrollbar grab uses the border color on the window-colored track.
Selection colors are stronger so an open menu reads as blue in both
themes and the hovered item is visible in light mode. The Messages/Logs
pill sits in a strip below the page instead of covering its last rows.

* cabana: even spacing around the page switch, steady top menu highlight

* cabana: inner spacing before the clear button of a clearable input

* cabana: center the page switch below the page, space the signal row buttons

* cabana: logs table cells padded and bordered like the messages table

* cabana: recess the signals card in the window color

* cabana: one button vocabulary

Three button shapes, all one frame height with the style padding and
rounding: ImGui::Button for framed text, iconButton for a framed square
icon, toolButton for a flat square icon (or flat text). Adjacent buttons
are ItemInnerSpacing apart, groups ItemSpacing apart. Tool bars drop
their private padding and 1 px spacing, the signal row, chart header,
detail header, logs export and signal selector buttons all use the
shared sizes, and the page switch is built from the same style values.

* cabana: center the glyph ink in square icon buttons

* cabana: framed tool buttons and plain icon glyphs

toolButton is now iconButton with an optional text, so every button in
the app is framed. Boxed glyphs (plus-square, x-square, dash-square,
arrow-*-square, window-stack) are replaced by their plain variants
(plus-lg, trash, arrows-collapse, box-arrow-*, window-plus), the light
x/plus/dash by their -lg forms, the chart menu by three-dots-vertical
and the toolbar overflow by chevron-double-right.

* cabana: draw square button glyphs at 80% so full bleed icons keep a margin

* cabana: signal values in the mono font

* cabana: center square button icons on their atlas ink, snapped to framebuffer pixels

* cabana: draw square button glyph quads directly, AddText truncates the position

* cabana: right align the messages and signals tool bars to the table edge, full size collapse button

* cabana: detail header and tab scroll buttons flush with the right edge

* cabana: header and footer strips, gap above the panels, video splitter as a separator

The menu bar and the status bar are strips in the surface color with a
separator line towards the panels, and the dockspace sits one item
spacing below the menu bar, matching the spacing above the status bar.
The video/charts splitter draws the same 1 px separator line as the
dock splitters, centered in a gap as tall as the side padding, and a
double click snaps the video back to its natural height.

* cabana: top menus flush with the menu bar, 2 px video splitter without double click

* cabana: dropdowns continue the menu bar, splitter in the border color, row button gap

Top level menus draw no top border and square top corners, and the menu
bar leaves its separator out under the open dropdown. The video splitter
uses the border color like the dock splitters. The signal row buttons
keep one inner spacing before the scrollbar.

* cabana: dropdowns share the bar's 1 px border line, messages toolbar spacing matches the video card

* cabana: tool bar groups, framed dropdown buttons, chart content flush with the tool bar

Tool bar items are ItemSpacing apart and the items of a group (a button
pair, a separator and its neighbor) ItemInnerSpacing apart, instead of
a private 1 px spacing. The dropdown buttons are framed like the other
buttons. The video time is a plain mono readout that toggles on click.
Chart cards have no horizontal margin so their header and plot line up
with the tool bar above.

* cabana: chart list spacing: gap above the scroll area, charts clear of the scrollbar, even legend gaps

* cabana: loop and speed buttons grouped like the playback buttons

* cabana: survive narrow panels and small windows

Stress tested by dragging the column and video splitters to their
limits and shrinking the window to 640x480. Fixes: the messages tool
bar and the message detail header overflow into the >> menu instead of
clipping (the view button and the name stay); the binary view keeps a
minimum cell width and scrolls sideways; the Messages/Logs switch drops
to icons when the strip is too narrow; the camera keeps its last frame
across reconnects, so a video restored from a collapsed splitter while
paused is no longer black.

* cabana: range slider spans the charts >> menu, remove all and float as menu entries

* cabana: the charts card keeps its tool bar row or collapses, like the video at the other end

* cabana: uniform >> menus: dropdowns become submenus, the time readout an action entry

* cabana: video/charts splitter gap matches the column splitters

* cabana: drop unused lambda capture in chart type menu

Claude-Session: https://claude.ai/code/session_01XTFaJpf235LeHBmyFRgeJ1

* cabana: the menu bar and the status bar stay in full screen

* cabana: simplify theme helpers and toolbar actions

* cabana: preserve panel width for overflow edit dialog

* cabana: keep message editor usable in narrow panels

* cabana: group heatmap modes in a dropdown

* cabana: trim redundant restyle comments

* cabana: fix review findings from the restyle

- set the caret and other unset colors so the light theme has no white caret
- tolerate float error in the toolbar fit check to stop spurious overflow
- restore the full screen chrome hiding
- clear stale camera frames when the server changes, keep them across a collapse
- cache icon glyph ink bounds instead of scanning the atlas every frame
- drop dead plot_bg and ImPlot globals, add a badge palette role
- add toolbarMenu and use toolbarAction for the remove message item
- derive the signal view button size seed from the font and style

* cabana: remove remaining redundant comments
This commit is contained in:
Trey Moen
2026-09-06 19:46:02 -07:00
committed by GitHub
parent c51e3e5a71
commit 9d1f0d4171
24 changed files with 736 additions and 691 deletions
+23 -37
View File
@@ -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<int>(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<int>(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());
}
+35 -45
View File
@@ -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<ToolbarItem> 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);
}
}
@@ -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();
+3 -6
View File
@@ -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));
}
+3 -4
View File
@@ -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;
+10 -12
View File
@@ -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
+82 -20
View File
@@ -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();
-284
View File
@@ -1,284 +0,0 @@
#include "tools/cabana/ui/app.h"
#include <algorithm>
#include <cmath>
#include <filesystem>
#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(); }
+174
View File
@@ -0,0 +1,174 @@
#include "tools/cabana/ui/theme.h"
#include <algorithm>
#include <cmath>
#include <filesystem>
#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(); }
+49
View File
@@ -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();
+150 -32
View File
@@ -3,7 +3,10 @@
#include <algorithm>
#include <cctype>
#include <cfloat>
#include <cmath>
#include <cstring>
#include <string>
#include <unordered_map>
#include <vector>
#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<InputContext *>(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<uint64_t, Entry> 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<void()> 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<void()> 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<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 ? 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<ToolbarItem> &items, size_t spacer_index) {
return w;
}
void drawToolbar(const std::vector<ToolbarItem> &items, size_t spacer_index) {
void drawToolbar(const std::vector<ToolbarItem> &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<ToolbarItem> &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<ToolbarItem> &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<ToolbarItem> &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;
}
+16 -39
View File
@@ -4,22 +4,11 @@
#include <string>
#include <vector>
#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<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
};
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<void()> 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<void()> 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<ToolbarItem> &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<ToolbarItem> &items, size_t spacer_index);
// width < 0 uses the available content width.
void drawToolbar(const std::vector<ToolbarItem> &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();
@@ -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<float>(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) {
@@ -3,6 +3,7 @@
#include <algorithm>
#include <chrono>
#include <cmath>
#include <utility>
#include <GLFW/glfw3.h>
#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<VisionIpcClient> 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)) {
@@ -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<ToolbarItem> 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<MessageId> *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<EditMessageDialog>(msg_id_, msgName(msg_id_), size, ImGui::GetWindowWidth());
edit_dlg_ = std::make_unique<EditMessageDialog>(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) {
@@ -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<MessageId> *msgs = nullptr);
MessageId msg_id_;
@@ -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()) {
@@ -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);
}
@@ -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<ToolbarItem> 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;
@@ -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) {
+30 -32
View File
@@ -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<ValueDescriptionDlg>(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<MessageId> *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;
}
@@ -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<ToolbarItem> 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')) {
@@ -102,7 +102,6 @@ private:
void skipToEnd();
void toggleTimeDisplay();
void createSpeedDropdown();
void drawSpeedDropdown(float width);
void drawSpeedMenuItems();
void loopPlaybackClicked();
void cropVideoClicked();
-14
View File
@@ -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();