mirror of
https://github.com/sunnypilot/sunnypilot.git
synced 2026-09-18 10:23:43 +08:00
cabana: fix chart ranges and control layout (#38797)
This commit is contained in:
@@ -6,7 +6,6 @@
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <limits>
|
||||
#include <random>
|
||||
|
||||
#include "tools/cabana/core/settings.h"
|
||||
#include "tools/cabana/settings.h"
|
||||
@@ -139,7 +138,7 @@ void ChartView::updateLayout() {
|
||||
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 btn_size(iconButtonWidth(), iconButtonWidth());
|
||||
const ImVec2 close_min(layout_.rect.Max.x - std::max(LAYOUT_MARGINS.z, CONTROL_OUTLINE_PADDING) - btn_size.x, top_left.y);
|
||||
const ImVec2 close_min(layout_.rect.Max.x - ImGui::GetStyle().WindowPadding.x - btn_size.x, top_left.y);
|
||||
layout_.close_btn_rect = ImRect(close_min, close_min + btn_size);
|
||||
const ImVec2 manage_min(close_min.x - btn_size.x - ImGui::GetStyle().ItemInnerSpacing.x, top_left.y);
|
||||
layout_.manage_btn_rect = ImRect(manage_min, manage_min + btn_size);
|
||||
@@ -150,7 +149,7 @@ void ChartView::updateLayout() {
|
||||
const int marker_size = markerSize();
|
||||
const int row_height = std::max<int>(marker_size, fm_height) + fm_height + 3; // + the signal value line
|
||||
const int legend_left = layout_.move_icon_rect.Max.x + LEGEND_SPACING;
|
||||
const int legend_right = std::max<int>(layout_.manage_btn_rect.Min.x - LAYOUT_MARGINS.z, legend_left + 10);
|
||||
const int legend_right = std::max<int>(layout_.manage_btn_rect.Min.x - ImGui::GetStyle().ItemInnerSpacing.x, legend_left + 10);
|
||||
|
||||
// layout legend entries left-to-right, wrapping between the move icon and the buttons
|
||||
layout_.legend_rects.clear();
|
||||
@@ -158,6 +157,9 @@ void ChartView::updateLayout() {
|
||||
for (auto &s : sigs_) {
|
||||
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;
|
||||
pushMonoFont(font_size);
|
||||
w = std::max(w, (int)std::ceil(ImGui::CalcTextSize("-0.00000e+000").x));
|
||||
popMonoFont();
|
||||
w = std::min(w, legend_right - legend_left); // keep oversized entries clear of the header buttons
|
||||
if (x + w > legend_right && x > legend_left) {
|
||||
x = legend_left;
|
||||
@@ -262,13 +264,14 @@ void ChartView::updateAxisY() {
|
||||
auto [first, last] = visibleRange(s.vals);
|
||||
s.min = std::numeric_limits<double>::max();
|
||||
s.max = std::numeric_limits<double>::lowest();
|
||||
if (first == last) continue;
|
||||
if (can->liveStreaming()) {
|
||||
for (auto it = first; it != last; ++it) {
|
||||
if (it->y < s.min) s.min = it->y;
|
||||
if (it->y > s.max) s.max = it->y;
|
||||
}
|
||||
} else {
|
||||
std::tie(s.min, s.max) = s.segment_tree.minmax(std::distance(s.vals.cbegin(), first), std::distance(s.vals.cbegin(), last));
|
||||
std::tie(s.min, s.max) = s.segment_tree.minmax(std::distance(s.vals.cbegin(), first), std::distance(s.vals.cbegin(), last) - 1);
|
||||
}
|
||||
min = std::min(min, s.min);
|
||||
max = std::max(max, s.max);
|
||||
@@ -278,7 +281,8 @@ void ChartView::updateAxisY() {
|
||||
|
||||
y_unit_ = unit;
|
||||
|
||||
double delta = std::abs(max - min) < 1e-3 ? 1 : (max - min) * 0.05;
|
||||
const double magnitude = std::max(std::abs(min), std::abs(max));
|
||||
double delta = max - min <= magnitude * 1e-9 ? (magnitude > 0 ? magnitude * 0.05 : 1) : (max - min) * 0.05;
|
||||
auto [min_y, max_y, tick_count] = getNiceAxisNumbers(min - delta, max + delta, 3);
|
||||
if (min_y != y_min_ || max_y != y_max_) {
|
||||
y_min_ = min_y;
|
||||
@@ -412,10 +416,10 @@ void ChartView::handleMouseRelease() {
|
||||
// Prevent zooming/seeking past the end of the route
|
||||
double min = std::clamp(secondsAtPoint(rubber_rect_.Min), can->minSeconds(), can->maxSeconds());
|
||||
double max = std::clamp(secondsAtPoint(rubber_rect_.Max), can->minSeconds(), can->maxSeconds());
|
||||
if (rubber_rect_.GetWidth() <= 0) {
|
||||
// no rubber dragged, seek to mouse position
|
||||
if (rubber_rect_.GetWidth() <= 10) {
|
||||
// Small movements are still clicks; use the same threshold as drag-to-zoom.
|
||||
can->seekTo(std::clamp(secondsAtPoint(press_pos_), can->minSeconds(), can->maxSeconds()));
|
||||
} else if (rubber_rect_.GetWidth() > 10 && (max - min) > MIN_ZOOM_SECONDS) {
|
||||
} else if ((max - min) > MIN_ZOOM_SECONDS) {
|
||||
charts_widget_->zoom_undo_stack_.push(new ZoomCommand({min, max}));
|
||||
}
|
||||
rubber_rect_ = ImRect();
|
||||
@@ -434,8 +438,8 @@ void ChartView::handleMouseRelease() {
|
||||
|
||||
void ChartView::takeSignalsFrom(ChartView *source) {
|
||||
for (auto &s : source->sigs_) {
|
||||
s.color = uniqueColor(s.color);
|
||||
sigs_.push_back(std::move(s));
|
||||
sigs_.back().color = uniqueColor(sigs_.back().color, sigs_.back().sig);
|
||||
}
|
||||
source->sigs_.clear();
|
||||
updateAxisY();
|
||||
@@ -478,17 +482,17 @@ void ChartView::showTip(double sec) {
|
||||
s.track_pt = *pt;
|
||||
x = std::max(x, xPos(pt->x));
|
||||
}
|
||||
std::string name = sigs_.size() > 1 ? s.sig->name + ": " : "";
|
||||
std::string name = s.sig->name;
|
||||
std::string min = s.min == std::numeric_limits<double>::max() ? "--" : utils::toString(s.min);
|
||||
std::string max = s.max == std::numeric_limits<double>::lowest() ? "--" : utils::toString(s.max);
|
||||
text_list.push_back({.has_marker = true, .marker = toImU32(s.color), .name = name, .bold = value, .rest = " (" + min + ", " + max + ")"});
|
||||
text_list.push_back({.has_marker = true, .marker = toImU32(s.color), .name = name, .value = value, .min = min, .max = max});
|
||||
}
|
||||
}
|
||||
if (x < 0) {
|
||||
x = tooltip_x_;
|
||||
}
|
||||
ImVec2 pt(x, layout_.plot_area.Min.y);
|
||||
text_list.insert(text_list.begin(), TipLine{.name = formatNumber(secondsAtPoint({x, 0}), 2)});
|
||||
text_list.insert(text_list.begin(), TipLine{.name = formatNumber(sec, 2) + " s"});
|
||||
tip_label_.showText(pt, text_list, visible_rect);
|
||||
}
|
||||
|
||||
@@ -747,6 +751,7 @@ void ChartView::drawTimeline() {
|
||||
}
|
||||
|
||||
void ChartView::drawSignalValue() {
|
||||
pushMonoFont(ImGui::GetFontSize());
|
||||
ImDrawList *painter = ImGui::GetWindowDrawList();
|
||||
const ImU32 color = ImGui::GetColorU32(ImGuiCol_Text);
|
||||
for (int i = 0; i < sigs_.size() && i < layout_.legend_rects.size(); ++i) {
|
||||
@@ -757,27 +762,35 @@ void ChartView::drawSignalValue() {
|
||||
ImRect value_rect(value_min, value_min + layout_.legend_rects[i].GetSize());
|
||||
float w = ImGui::CalcTextSize(value.c_str()).x;
|
||||
if (w <= value_rect.GetWidth()) {
|
||||
painter->AddText(ImVec2(value_rect.GetCenter().x - w / 2, value_rect.Min.y), color, value.c_str());
|
||||
painter->AddText(value_rect.Min, color, value.c_str());
|
||||
} else {
|
||||
addTextEllipsis(painter, ImGui::GetFont(), color, value_rect.Min, value_rect.Max.x, value);
|
||||
}
|
||||
}
|
||||
popMonoFont();
|
||||
}
|
||||
|
||||
CabanaColor ChartView::uniqueColor(CabanaColor color, const cabana::Signal *exclude) const {
|
||||
for (auto &s : sigs_) {
|
||||
if (s.sig != exclude && std::abs(color.hsv().hue - s.color.hsv().hue) < 0.1) {
|
||||
// use different color to distinguish it from others.
|
||||
auto last_color = sigs_.back().color;
|
||||
static thread_local std::mt19937 rng{std::random_device{}()};
|
||||
std::uniform_int_distribution<int> sat(35, 99);
|
||||
std::uniform_int_distribution<int> val(85, 99);
|
||||
color = CabanaColor::fromHsv(std::fmod(last_color.hsv().hue + 60 / 360.0, 1.0),
|
||||
sat(rng) / 100.0,
|
||||
val(rng) / 100.0,
|
||||
color.a / 255.0f);
|
||||
break;
|
||||
auto separation = [&](float hue) {
|
||||
float distance = 1.0f;
|
||||
for (const auto &s : sigs_) {
|
||||
if (exclude && s.sig == exclude) continue;
|
||||
const float delta = std::abs(hue - s.color.hsv().hue);
|
||||
distance = std::min(distance, std::min(delta, 1.0f - delta));
|
||||
}
|
||||
return distance;
|
||||
};
|
||||
const float original_hue = color.hsv().hue;
|
||||
if (separation(original_hue) >= 0.1f) return color;
|
||||
|
||||
float best_hue = original_hue, best_distance = -1;
|
||||
for (int i = 0; i < 36; ++i) {
|
||||
const float hue = std::fmod(original_hue + i / 36.0f, 1.0f);
|
||||
const float distance = separation(hue);
|
||||
if (distance > best_distance) {
|
||||
best_hue = hue;
|
||||
best_distance = distance;
|
||||
}
|
||||
}
|
||||
return color;
|
||||
return CabanaColor::fromHsv(best_hue, 0.8f, 0.9f, color.a / 255.0f);
|
||||
}
|
||||
|
||||
@@ -120,7 +120,7 @@ private:
|
||||
double y_min_ = 0;
|
||||
double y_max_ = 1;
|
||||
int y_tick_count_ = 3;
|
||||
int y_precision_ = 0;
|
||||
int y_precision_ = 1;
|
||||
std::string y_unit_;
|
||||
// interaction
|
||||
enum class MouseMode { None, Rubber, Scrub };
|
||||
|
||||
@@ -206,12 +206,13 @@ void ChartsWidget::drawToolBar() {
|
||||
const std::string range_lb = is_zoomed ? std::string() : utils::formatSeconds(max_chart_range_);
|
||||
std::string reset_zoom_text;
|
||||
if (!is_zoomed) {
|
||||
items.push_back({ImGui::CalcTextSize(range_lb.c_str()).x, [&range_lb]() {
|
||||
// the range label and the slider are one unit: drawn inline and moved to the overflow menu together
|
||||
slider_index = items.size();
|
||||
const float label_width = ImGui::CalcTextSize(range_lb.c_str()).x + ImGui::GetStyle().ItemInnerSpacing.x;
|
||||
items.push_back({label_width + slider_width, [this, &range_lb, &slider_width]() {
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextUnformatted(range_lb.c_str());
|
||||
}});
|
||||
slider_index = items.size();
|
||||
items.push_back({slider_width, [this, &slider_width]() {
|
||||
ImGui::SameLine(0.0f, ImGui::GetStyle().ItemInnerSpacing.x);
|
||||
// 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;
|
||||
@@ -219,9 +220,17 @@ void ChartsWidget::drawToolBar() {
|
||||
ImGui::SetItemTooltip("Set the chart range");
|
||||
}});
|
||||
} else {
|
||||
const auto &range = *can->timeRange();
|
||||
char buf[64];
|
||||
snprintf(buf, sizeof(buf), "%.2f-%.2f", can->timeRange()->first, can->timeRange()->second);
|
||||
snprintf(buf, sizeof(buf), "%.2f-%.2f", range.first, range.second);
|
||||
reset_zoom_text = buf;
|
||||
// The undo/redo/reset buttons form one group. The reset button has a fixed width in the mono font,
|
||||
// sized for the longest range the stream can show, so its neighbors do not shift as the range changes.
|
||||
const int digits = std::max({1, (int)std::to_string((long long)can->maxSeconds()).size(), (int)std::to_string((long long)range.second).size()});
|
||||
const std::string widest = std::string(digits, '0') + ".00";
|
||||
pushMonoFont(ImGui::GetFontSize());
|
||||
const float reset_zoom_width = iconTextButtonWidth(icon::ZOOM_OUT, widest + "-" + widest);
|
||||
popMonoFont();
|
||||
items.push_back({iconButtonWidth(), [this]() {
|
||||
ImGui::BeginDisabled(!zoom_undo_stack_.canUndo());
|
||||
if (iconButton("undo_zoom", icon::ARROW_COUNTERCLOCKWISE, "Undo Zoom")) zoom_undo_stack_.undo();
|
||||
@@ -232,10 +241,15 @@ void ChartsWidget::drawToolBar() {
|
||||
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 (ImGui::Button((std::string(icon::ZOOM_OUT) + " " + reset_zoom_text + "###reset_zoom_btn").c_str())) zoomReset();
|
||||
items.back().tight = true;
|
||||
items.push_back({reset_zoom_width, [this, &reset_zoom_text, reset_zoom_width]() {
|
||||
pushMonoFont(ImGui::GetFontSize());
|
||||
const bool clicked = iconTextButton("reset_zoom_btn", icon::ZOOM_OUT, reset_zoom_text, reset_zoom_width);
|
||||
popMonoFont();
|
||||
if (clicked) zoomReset();
|
||||
ImGui::SetItemTooltip("Reset Zoom");
|
||||
}});
|
||||
items.back().tight = true;
|
||||
}
|
||||
items.push_back(toolbarAction("remove_all_btn", icon::TRASH, "Remove all charts", [this]() { removeAll(); }, !charts_.empty()));
|
||||
const char *dock_btn_icon = is_docked_ ? icon::BOX_ARROW_UP_RIGHT : icon::BOX_ARROW_IN_DOWN_LEFT;
|
||||
@@ -247,7 +261,7 @@ void ChartsWidget::drawToolBar() {
|
||||
const float shrink = std::min(slider_width - MIN_RANGE_SLIDER_WIDTH, toolbarWidth(items, spacer_index) - ImGui::GetContentRegionAvail().x);
|
||||
if (shrink > 0.0f) {
|
||||
slider_width -= shrink;
|
||||
items[slider_index].width = slider_width;
|
||||
items[slider_index].width -= shrink;
|
||||
}
|
||||
}
|
||||
drawToolbar(items, spacer_index);
|
||||
@@ -273,7 +287,8 @@ ChartView *ChartsWidget::createChart(int pos) {
|
||||
ChartView *ptr = chart.get();
|
||||
pos = std::clamp(pos, 0, (int)charts_.size());
|
||||
charts_.insert(charts_.begin() + pos, std::move(chart));
|
||||
currentCharts().insert(currentCharts().begin() + pos, ptr);
|
||||
auto ¤t = currentCharts();
|
||||
current.insert(current.begin() + std::min(pos, (int)current.size()), ptr);
|
||||
updateLayout();
|
||||
return ptr;
|
||||
}
|
||||
@@ -291,8 +306,8 @@ void ChartsWidget::showChart(const MessageId &id, const cabana::Signal *sig, boo
|
||||
|
||||
void ChartsWidget::splitChart(ChartView *src_chart) {
|
||||
if (src_chart->signals().size() > 1) {
|
||||
auto it = std::find_if(charts_.begin(), charts_.end(), [src_chart](auto &c) { return c.get() == src_chart; });
|
||||
const int pos = it - charts_.begin() + 1;
|
||||
auto ¤t = currentCharts();
|
||||
const int pos = std::find(current.begin(), current.end(), src_chart) - current.begin() + 1;
|
||||
for (auto &s : src_chart->takeExtraSignals()) {
|
||||
createChart(pos)->adoptSignal(std::move(s));
|
||||
}
|
||||
@@ -606,9 +621,8 @@ void ChartsWidget::draw() {
|
||||
}
|
||||
|
||||
void ChartsContainer::draw() {
|
||||
ImGuiWindow *window = ImGui::GetCurrentWindow();
|
||||
const ImVec2 start = ImGui::GetCursorScreenPos();
|
||||
const float width_avail = window->InnerRect.GetWidth() - (window->ScrollbarY ? ImGui::GetStyle().ItemInnerSpacing.x : 0.0f);
|
||||
const float width_avail = ImGui::GetContentRegionAvail().x;
|
||||
geometry_ = ImRect(start, start + ImVec2(width_avail, 0));
|
||||
charts_widget_->updateLayout();
|
||||
|
||||
|
||||
@@ -2,35 +2,49 @@
|
||||
#include "tools/cabana/ui/chart/tiplabel.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cfloat>
|
||||
#include <cmath>
|
||||
|
||||
#include "tools/cabana/ui/util.h"
|
||||
|
||||
ImVec2 TipLabel::layoutLines(ImDrawList *p, const ImVec2 &origin, ImU32 fg) const {
|
||||
ImFont *bold = boldFont();
|
||||
const float font_size = ImGui::GetFontSize();
|
||||
const float line_height = ImGui::GetTextLineHeight();
|
||||
ImVec2 size(0, 0);
|
||||
float y = origin.y;
|
||||
const float line_height = std::ceil(ImGui::GetTextLineHeight() + 2);
|
||||
const float marker = std::floor(markerSize());
|
||||
const float gap = 8;
|
||||
const float width = column_widths_[0] + column_widths_[1] + column_widths_[2] + column_widths_[3] + gap * 3;
|
||||
const ImU32 muted = ImGui::GetColorU32(ImGuiCol_TextDisabled);
|
||||
float y = std::round(origin.y);
|
||||
auto draw = [&](float x, const std::string &text, ImU32 color) {
|
||||
if (p) p->AddText(ImVec2(std::round(x), y), color, text.c_str());
|
||||
};
|
||||
const char *heading = !text_.empty() && !text_[0].has_marker ? text_[0].name.c_str() : "Signal";
|
||||
const char *headers[] = {heading, "Value", "Min", "Max"};
|
||||
float x = origin.x;
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
draw(i ? x + column_widths_[i] - ImGui::CalcTextSize(headers[i]).x : x, headers[i], muted);
|
||||
x += column_widths_[i] + gap;
|
||||
}
|
||||
y += line_height;
|
||||
if (p) p->AddLine(ImVec2(origin.x, y - 2), ImVec2(origin.x + width, y - 2), ImGui::GetColorU32(ImGuiCol_Border));
|
||||
for (const auto &line : text_) {
|
||||
float x = origin.x;
|
||||
if (line.has_marker) {
|
||||
if (p) drawColorMarker(p, ImVec2(x, y), line.marker);
|
||||
x += markerSize() + 4;
|
||||
if (!line.has_marker) continue;
|
||||
if (p) {
|
||||
const ImVec2 marker_pos(std::round(origin.x), std::round(y + (font_size - marker) * 0.5f));
|
||||
p->AddRectFilled(marker_pos, marker_pos + ImVec2(marker, marker), line.marker);
|
||||
}
|
||||
if (p) p->AddText(ImVec2(x, y), fg, line.name.c_str());
|
||||
x += ImGui::CalcTextSize(line.name.c_str()).x;
|
||||
if (!line.bold.empty()) {
|
||||
if (p) p->AddText(bold, font_size, ImVec2(x, y), fg, line.bold.c_str());
|
||||
x += bold->CalcTextSizeA(font_size, FLT_MAX, 0.0f, line.bold.c_str()).x;
|
||||
if (p) drawElidedText(p, ImRect(ImVec2(origin.x + marker + 6, y),
|
||||
ImVec2(origin.x + column_widths_[0], y + font_size)), line.name, fg);
|
||||
x = origin.x + column_widths_[0] + gap;
|
||||
pushMonoFont(font_size);
|
||||
const std::string *values[] = {&line.value, &line.min, &line.max};
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
draw(x + column_widths_[i + 1] - ImGui::CalcTextSize(values[i]->c_str()).x, *values[i], i == 0 ? fg : muted);
|
||||
x += column_widths_[i + 1] + gap;
|
||||
}
|
||||
if (p) p->AddText(ImVec2(x, y), fg, line.rest.c_str());
|
||||
x += ImGui::CalcTextSize(line.rest.c_str()).x;
|
||||
size.x = std::max(size.x, x - origin.x);
|
||||
popMonoFont();
|
||||
y += line_height;
|
||||
}
|
||||
size.y = y - origin.y;
|
||||
return size;
|
||||
return ImVec2(width, y - origin.y);
|
||||
}
|
||||
|
||||
ImVec2 TipLabel::sizeHint() const {
|
||||
@@ -38,25 +52,53 @@ ImVec2 TipLabel::sizeHint() const {
|
||||
}
|
||||
|
||||
void TipLabel::showText(const ImVec2 &pt, const std::vector<TipLine> &text, const ImRect &rect) {
|
||||
bool same_signals = text.size() == text_.size();
|
||||
for (size_t i = 1; same_signals && i < text.size(); ++i) same_signals = text[i].name == text_[i].name;
|
||||
if (!same_signals) column_widths_ = {};
|
||||
text_ = text;
|
||||
anchor_ = pt;
|
||||
area_ = rect;
|
||||
visible_ = !text_.empty();
|
||||
}
|
||||
|
||||
void TipLabel::updateLayout() {
|
||||
// Playback notifications can update the text outside an ImGui window scope.
|
||||
// Measure and position it only while the owning chart is being drawn.
|
||||
column_widths_[0] = std::max(column_widths_[0], ImGui::CalcTextSize(text_.empty() ? "Signal" : text_[0].name.c_str()).x);
|
||||
for (const auto &line : text_) {
|
||||
if (line.has_marker) column_widths_[0] = std::max(column_widths_[0], std::ceil(markerSize() + 6 + ImGui::CalcTextSize(line.name.c_str()).x));
|
||||
}
|
||||
pushMonoFont(ImGui::GetFontSize());
|
||||
const float number_width = std::ceil(ImGui::CalcTextSize("-0.00000").x);
|
||||
for (int i = 1; i < 4; ++i) column_widths_[i] = std::max(column_widths_[i], number_width);
|
||||
for (const auto &line : text_) {
|
||||
const std::string *values[] = {&line.value, &line.min, &line.max};
|
||||
for (int i = 0; i < 3; ++i) column_widths_[i + 1] = std::max(column_widths_[i + 1], std::ceil(ImGui::CalcTextSize(values[i]->c_str()).x));
|
||||
}
|
||||
popMonoFont();
|
||||
const ImGuiViewport *viewport = ImGui::GetWindowViewport();
|
||||
const ImRect bounds(viewport->WorkPos, viewport->WorkPos + viewport->WorkSize);
|
||||
const float numeric_width = column_widths_[1] + column_widths_[2] + column_widths_[3] + 24 + MARGIN * 2 + 1;
|
||||
column_widths_[0] = std::min(column_widths_[0], std::max(40.0f, std::min(ImGui::GetFontSize() * 16, bounds.GetWidth() - numeric_width)));
|
||||
if (!text_.empty()) {
|
||||
ImVec2 extra(1, 1);
|
||||
size_ = sizeHint() + extra;
|
||||
ImVec2 tip_pos(pt.x + 8, rect.Min.y + 2);
|
||||
if (tip_pos.x + size_.x >= rect.Max.x) {
|
||||
tip_pos.x = pt.x - size_.x - 8;
|
||||
}
|
||||
if (rect.Contains(ImRect(tip_pos, tip_pos + size_))) {
|
||||
pos_ = tip_pos;
|
||||
visible_ = true;
|
||||
return;
|
||||
ImVec2 tip_pos(anchor_.x + 8, area_.Min.y + 2);
|
||||
if (anchor_.x >= area_.GetCenter().x) {
|
||||
tip_pos.x = anchor_.x - size_.x - 8;
|
||||
}
|
||||
tip_pos.x = std::clamp(tip_pos.x, bounds.Min.x, std::max(bounds.Min.x, bounds.Max.x - size_.x));
|
||||
tip_pos.y = std::clamp(tip_pos.y, bounds.Min.y, std::max(bounds.Min.y, bounds.Max.y - size_.y));
|
||||
pos_ = tip_pos;
|
||||
visible_ = true;
|
||||
return;
|
||||
}
|
||||
visible_ = false;
|
||||
}
|
||||
|
||||
void TipLabel::draw() {
|
||||
if (!visible_) return;
|
||||
updateLayout();
|
||||
|
||||
ImDrawList *p = ImGui::GetForegroundDrawList();
|
||||
// filled panel with a 1px frame
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "imgui.h"
|
||||
#include "imgui_internal.h"
|
||||
|
||||
// one line of the tip: [square] name <b>value</b> (min, max)
|
||||
// A signal row, or a time heading when has_marker is false.
|
||||
struct TipLine {
|
||||
bool has_marker = false;
|
||||
ImU32 marker = 0;
|
||||
std::string name;
|
||||
std::string bold;
|
||||
std::string rest;
|
||||
std::string value, min, max;
|
||||
};
|
||||
|
||||
class TipLabel {
|
||||
@@ -26,9 +26,13 @@ private:
|
||||
// lays the lines out from origin, drawing them when p is given; returns the size of the text block
|
||||
ImVec2 layoutLines(ImDrawList *p, const ImVec2 &origin, ImU32 fg) const;
|
||||
ImVec2 sizeHint() const;
|
||||
void updateLayout();
|
||||
|
||||
static constexpr float MARGIN = 2.0f; // 1 + PM_ToolTipLabelFrameWidth
|
||||
static constexpr float MARGIN = 6.0f;
|
||||
std::vector<TipLine> text_;
|
||||
std::array<float, 4> column_widths_{};
|
||||
ImVec2 anchor_;
|
||||
ImRect area_;
|
||||
ImVec2 pos_;
|
||||
ImVec2 size_;
|
||||
bool visible_ = false;
|
||||
|
||||
@@ -214,6 +214,26 @@ bool iconButton(const char *id, const char *icon, const char *tooltip) {
|
||||
return clicked;
|
||||
}
|
||||
|
||||
float iconTextButtonWidth(const char *icon, const std::string &text) {
|
||||
const ImGuiStyle &style = ImGui::GetStyle();
|
||||
return ImGui::CalcTextSize(icon).x + style.ItemInnerSpacing.x + ImGui::CalcTextSize(text.c_str(), nullptr, true).x + style.FramePadding.x * 2;
|
||||
}
|
||||
|
||||
bool iconTextButton(const char *id, const char *icon, const std::string &text, float width) {
|
||||
const ImGuiStyle &style = ImGui::GetStyle();
|
||||
if (width <= 0.0f) width = iconTextButtonWidth(icon, text);
|
||||
const bool clicked = ImGui::Button((std::string("###") + id).c_str(), ImVec2(width, 0.0f));
|
||||
const ImVec2 min = ImGui::GetItemRectMin(), max = ImGui::GetItemRectMax();
|
||||
const ImU32 color = ImGui::GetColorU32(ImGuiCol_Text);
|
||||
auto *draw_list = ImGui::GetWindowDrawList();
|
||||
draw_list->AddText(ImVec2(min.x + style.FramePadding.x, min.y + style.FramePadding.y), color, icon);
|
||||
// the text is centered between the icon and the right padding
|
||||
const float left = min.x + style.FramePadding.x + ImGui::CalcTextSize(icon).x + style.ItemInnerSpacing.x;
|
||||
const float slack = max.x - style.FramePadding.x - left - ImGui::CalcTextSize(text.c_str(), nullptr, true).x;
|
||||
draw_list->AddText(ImVec2(left + std::max(0.0f, slack * 0.5f), min.y + style.FramePadding.y), color, text.c_str());
|
||||
return clicked;
|
||||
}
|
||||
|
||||
void disabledItemTooltip(const char *text) {
|
||||
if (ImGui::IsItemHovered(ImGuiHoveredFlags_ForTooltip | ImGuiHoveredFlags_AllowWhenDisabled)) ImGui::SetTooltip("%s", text);
|
||||
}
|
||||
|
||||
@@ -74,6 +74,8 @@ int nonWhitespaceValidator(ImGuiInputTextCallbackData *data);
|
||||
// Use ItemInnerSpacing between related buttons and ItemSpacing between groups.
|
||||
bool iconButton(const char *id, const char *icon, const char *tooltip = nullptr);
|
||||
float iconButtonWidth();
|
||||
bool iconTextButton(const char *id, const char *icon, const std::string &text, float width = 0.0f);
|
||||
float iconTextButtonWidth(const char *icon, const std::string &text);
|
||||
|
||||
// tooltip for the last item that also shows while the item is disabled
|
||||
void disabledItemTooltip(const char *text);
|
||||
|
||||
Reference in New Issue
Block a user