Files
IQ.Pilot/iqpilot/tools/cabana/ui/chart/chartswidget.cc
2026-09-02 16:46:45 -05:00

675 lines
23 KiB
C++

#define IMGUI_DEFINE_MATH_OPERATORS
#include "tools/cabana/ui/chart/chartswidget.h"
#include "tools/cabana/ui/threadpool.h"
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <future>
#include "tools/cabana/settings.h"
#include "tools/cabana/ui/chart/chart.h"
#include "tools/cabana/ui/icons.h"
#include "tools/cabana/ui/util.h"
#include "tools/cabana/utils/strings.h"
const int MAX_COLUMN_COUNT = 4;
const int CHART_SPACING = 4;
const int START_DRAG_DISTANCE = 10;
const float LAYOUT_HORIZONTAL_SPACING = 6.0f;
const float MIN_RANGE_SLIDER_WIDTH = 40.0f;
bool LogSlider::draw(const char *label, float width) {
return fusionSliderInt(label, &pos_, min_, max_, width);
}
ChartsWidget::ChartsWidget() {
range_slider_.setRange(1, settings.max_cached_minutes * 60);
tabbar_.setAutoHide(true);
tabbar_.setUsesScrollButtons(true);
tabbar_.setTabsClosable(true);
column_count_ = std::clamp(settings.chart_column_count, 1, MAX_COLUMN_COUNT);
max_chart_range_ = std::clamp(settings.chart_range, 1, settings.max_cached_minutes * 60);
display_range_ = std::make_pair(can->minSeconds(), can->minSeconds() + max_chart_range_);
range_slider_.setValue(max_chart_range_);
connections_.push_back(dbc()->fileChanged.connect([this]() { removeAll(); }));
connections_.push_back(can->eventsMerged.connect([this](const MessageEventsMap &events) { eventsMerged(events); }));
connections_.push_back(can->msgsReceived.connect([this](const std::set<MessageId> *, bool) { updateState(); }));
connections_.push_back(can->seeking.connect([this](double) { updateState(); }));
connections_.push_back(can->timeRangeChanged.connect([this](const auto &) { updateState(); }));
connections_.push_back(settings.changed.connect([this]() { settingChanged(); }));
connections_.push_back(seriesChanged.connect([this]() { updateTabBar(); }));
connections_.push_back(tabbar_.tabCloseRequested.connect([this](int index) { removeTab(index); }));
connections_.push_back(tabbar_.tabContextMenu.connect([this](int index) {
if (ImGui::BeginPopupContextItem()) {
if (ImGui::MenuItem("Close Other Tabs")) {
tabbar_.moveTab(index, 0);
tabbar_.setCurrentIndex(0);
while (tabbar_.count() > 1) removeTab(1);
}
ImGui::EndPopup();
}
}));
connections_.push_back(tabbar_.currentChanged.connect([this](int index) {
if (index != -1) updateLayout();
}));
setIsDocked(true);
newTab();
}
ChartsWidget::~ChartsWidget() = default;
std::string ChartsWidget::whatsThis() const {
return R"(
<b>Chart View</b><br />
<b>Click</b>: Click to seek to a corresponding time.<br />
<b>Drag</b>: Zoom into the chart.<br />
<b>Shift + Drag</b>: Scrub through the chart to view values.<br />
<b>Right Mouse</b>: Open the context menu.<br />
)";
}
void ChartsWidget::newTab() {
static int tab_unique_id = 0;
int idx = tabbar_.addTab("");
tabbar_.setTabData(idx, tab_unique_id++);
tabbar_.setCurrentIndex(idx);
updateTabBar();
}
void ChartsWidget::removeTab(int index) {
int id = tabbar_.tabData(index);
for (auto &c : std::vector<ChartView *>(tab_charts_[id])) {
removeChart(c);
}
tab_charts_.erase(id);
tabbar_.removeTab(index);
updateTabBar();
}
void ChartsWidget::updateTabBar() {
for (int i = 0; i < tabbar_.count(); ++i) {
const auto &charts_in_tab = tab_charts_[tabbar_.tabData(i)];
tabbar_.setTabText(i, "Tab " + std::to_string(i + 1) + " (" + std::to_string((int)charts_in_tab.size()) + ")");
}
}
void ChartsWidget::eventsMerged(const MessageEventsMap &new_events) {
std::vector<std::future<void>> futures;
for (auto &c : charts_) {
futures.push_back(ThreadPool::instance().run([c = c.get(), &new_events]() { c->updateSeries(nullptr, &new_events); }));
}
for (auto &f : futures) f.get();
}
void ChartsWidget::zoomReset() {
can->setTimeRange(std::nullopt);
zoom_undo_stack_.clear();
}
ImRect ChartsWidget::chartVisibleRect(ChartView *chart) {
ImRect r = chart->rect();
r.ClipWith(charts_scroll_viewport_);
return r;
}
void ChartsWidget::showValueTip(double sec) {
if (chartDragActive()) sec = -1;
showTip(sec);
if (sec < 0 && !value_tip_visible_) return;
value_tip_visible_ = sec >= 0;
for (auto c : currentCharts()) {
value_tip_visible_ ? c->showTip(sec) : c->hideTip();
}
}
void ChartsWidget::updateState() {
if (charts_.empty()) return;
const auto &time_range = can->timeRange();
const double cur_sec = can->currentSec();
if (!time_range.has_value()) {
double pos = (cur_sec - display_range_.first) / std::max<float>(1.0, max_chart_range_);
if (pos < 0 || pos > 0.8) {
display_range_.first = std::max(can->minSeconds(), cur_sec - max_chart_range_ * 0.1);
}
double max_sec = std::min(display_range_.first + max_chart_range_, can->maxSeconds());
display_range_.first = std::max(can->minSeconds(), max_sec - max_chart_range_);
display_range_.second = display_range_.first + max_chart_range_;
}
const auto &range = time_range ? *time_range : display_range_;
for (auto &c : charts_) {
c->updatePlot(cur_sec, range.first, range.second);
}
}
void ChartsWidget::setMaxChartRange(int value) {
max_chart_range_ = settings.chart_range = value;
updateState();
}
void ChartsWidget::setIsDocked(bool docked) {
is_docked_ = docked;
if (!docked) float_window_init_ = true;
}
void ChartsWidget::drawToolBar() {
beginToolbar();
float slider_width = 150.0f;
const bool is_zoomed = can->timeRange().has_value();
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({toolbarButtonWidth(icon::WINDOW_STACK), [this]() {
if (toolButton("new_tab_btn", icon::WINDOW_STACK, "New Tab")) newTab();
}});
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]() {
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();
}
}
ImGui::EndPopup();
}
}});
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();
}
}});
}
const size_t spacer_index = items.size();
size_t slider_index = (size_t)-1;
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]() {
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted(range_lb.c_str());
}});
slider_index = items.size();
items.push_back({slider_width, [this, &slider_width]() {
if (range_slider_.draw("##range_slider", 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]() {
ImGui::BeginDisabled(!zoom_undo_stack_.canUndo());
if (toolButton("undo_zoom", icon::ARROW_COUNTERCLOCKWISE, "Undo Zoom")) zoom_undo_stack_.undo();
ImGui::EndDisabled();
}});
items.push_back({toolbarButtonWidth(icon::ARROW_CLOCKWISE), [this]() {
ImGui::BeginDisabled(!zoom_undo_stack_.canRedo());
if (toolButton("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();
}});
}
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();
}});
if (slider_index != (size_t)-1) {
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;
}
}
drawToolbar(items, spacer_index);
endToolbar();
}
void ChartsWidget::settingChanged() {
if (range_slider_.maximum() != settings.max_cached_minutes * 60) {
range_slider_.setRange(1, settings.max_cached_minutes * 60);
}
for (auto &c : charts_) {
c->setSeriesType((SeriesType)settings.chart_series_type);
}
}
ChartView *ChartsWidget::findChart(const MessageId &id, const cabana::Signal *sig) {
for (auto &c : charts_)
if (c->hasSignal(id, sig)) return c.get();
return nullptr;
}
ChartView *ChartsWidget::createChart(int pos) {
auto chart = std::make_unique<ChartView>(can->timeRange().value_or(display_range_), this);
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);
updateLayout();
return ptr;
}
void ChartsWidget::showChart(const MessageId &id, const cabana::Signal *sig, bool show, bool merge) {
ChartView *chart = findChart(id, sig);
if (show && !chart) {
chart = merge && currentCharts().size() > 0 ? currentCharts().front() : createChart();
chart->addSignal(id, sig);
updateState();
} else if (!show && chart) {
chart->removeIf([&](auto &s) { return s.msg_id == id && s.sig == sig; });
}
}
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;
for (auto &s : src_chart->takeExtraSignals()) {
createChart(pos)->adoptSignal(std::move(s));
}
updateState();
}
}
std::vector<std::string> ChartsWidget::serializeChartIds() const {
std::vector<std::string> chart_ids;
for (auto &c : charts_) {
std::string ids;
for (const auto &s : c->signals()) {
if (!ids.empty()) ids += ',';
ids += s.msg_id.toString() + "|" + s.sig->name;
}
chart_ids.push_back(ids);
}
std::reverse(chart_ids.begin(), chart_ids.end());
return chart_ids;
}
void ChartsWidget::restoreChartsFromIds(const std::vector<std::string> &chart_ids) {
for (const auto &chart_id : chart_ids) {
int index = 0;
for (const auto &part : utils::split(chart_id, ',')) {
const size_t sep = part.find('|');
if (sep == std::string::npos) continue;
MessageId msg_id = MessageId::fromString(part.substr(0, sep));
if (auto *msg = dbc()->msg(msg_id))
if (auto *sig = msg->sig(part.substr(sep + 1)))
showChart(msg_id, sig, true, index++ > 0);
}
}
}
void ChartsWidget::setColumnCount(int n) {
n = std::clamp(n, 1, MAX_COLUMN_COUNT);
if (column_count_ != n) {
column_count_ = settings.chart_column_count = n;
updateLayout();
}
}
void ChartsWidget::updateLayout() {
const float container_width = charts_container_.geometry().GetWidth();
if (container_width <= 0) return;
int n = MAX_COLUMN_COUNT;
for (; n > 1; --n) {
if ((n * CHART_MIN_WIDTH + (n - 1) * CHART_SPACING) < container_width) break;
}
columns_action_visible_ = n > 1;
current_column_count_ = std::min(column_count_, n);
}
void ChartsWidget::startChartDrag(ChartView *chart, const ImVec2 &global_pos) {
stopAutoScroll();
drag_ = {.source = chart, .press_pos = global_pos};
showValueTip(-1);
drag_preview_size_ = ImVec2(CHART_MIN_WIDTH, (float)settings.chart_height);
}
void ChartsWidget::dragChartMove(const ImVec2 &global_pos) {
if (!drag_.active) {
ImVec2 d = global_pos - drag_.press_pos;
if (std::abs(d.x) + std::abs(d.y) < START_DRAG_DISTANCE) return;
drag_.active = true;
drag_preview_visible_ = true;
}
drag_preview_pos_ = global_pos + ImVec2(5, 5);
int tab = tabbar_.tabAt(global_pos);
if (tab >= 0 && tab != tabbar_.currentIndex()) {
tabbar_.setCurrentIndex(tab);
}
ChartView *target = nullptr;
for (auto c : currentCharts()) {
if (c != drag_.source && c->rect().Contains(global_pos)) {
target = c;
break;
}
}
if (std::exchange(drop_target_, target) != target) {
for (auto &c : charts_) c->setDropHighlight(c.get() == target);
}
bool in_viewport = charts_scroll_viewport_.Contains(global_pos);
bool on_background = !target && in_viewport && !charts_container_.childAt(global_pos);
charts_container_.setDropIndicator(on_background ? global_pos : ImVec2());
if (in_viewport) {
startAutoScroll(global_pos);
}
}
void ChartsWidget::cancelChartDrag() {
drag_ = {};
stopAutoScroll();
drag_preview_visible_ = false;
charts_container_.setDropIndicator({});
if (auto target = std::exchange(drop_target_, nullptr)) target->setDropHighlight(false);
}
void ChartsWidget::dragChartRelease(const ImVec2 &global_pos) {
ChartView *source = drag_.source;
bool active = drag_.active;
ChartView *target = drop_target_;
cancelChartDrag();
if (!active) return;
bool in_viewport = charts_scroll_viewport_.Contains(global_pos);
if (target) {
target->takeSignalsFrom(source);
} else if (in_viewport && !charts_container_.childAt(global_pos)) {
auto w = charts_container_.getDropAfter(global_pos);
if (w != source) {
for (auto &[_, list] : tab_charts_) {
list.erase(std::remove(list.begin(), list.end(), source), list.end());
}
auto &cur = currentCharts();
int to = w ? std::find(cur.begin(), cur.end(), w) - cur.begin() + 1 : 0;
cur.insert(cur.begin() + to, source);
updateLayout();
updateTabBar();
}
}
}
void ChartsWidget::drawDragPreview() {
if (!drag_preview_visible_ || !drag_.source) return;
ImGui::SetNextWindowPos(drag_preview_pos_);
ImGui::SetNextWindowSize(drag_preview_size_);
ImGui::PushStyleVar(ImGuiStyleVar_Alpha, 0.5f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0));
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
const ImGuiWindowFlags flags = ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoDecoration |
ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoFocusOnAppearing |
ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoDocking;
if (ImGui::Begin("##chart_drag_ghost", nullptr, flags)) {
drag_.source->drawGhost(drag_preview_size_.x);
}
ImGui::End();
ImGui::PopStyleVar(3);
}
void ChartsWidget::startAutoScroll(const ImVec2 &global_pos) {
auto_scroll_pos_ = global_pos;
if (!auto_scroll_timer_active_) auto_scroll_timer_next_ = ImGui::GetTime() + 0.05;
auto_scroll_timer_active_ = true;
}
void ChartsWidget::stopAutoScroll() {
auto_scroll_timer_active_ = false;
auto_scroll_count_ = 0;
}
void ChartsWidget::doAutoScroll() {
if (!charts_scroll_) return;
const int page_step = charts_scroll_viewport_.GetHeight();
if (auto_scroll_count_ < page_step) {
++auto_scroll_count_;
}
int value = charts_scroll_->Scroll.y;
ImVec2 pos = auto_scroll_pos_;
ImRect area = charts_scroll_viewport_;
int new_value = value;
if (pos.y - area.Min.y < settings.chart_height / 2) {
new_value = value - auto_scroll_count_;
} else if (area.Max.y - pos.y < settings.chart_height / 2) {
new_value = value + auto_scroll_count_;
}
new_value = std::clamp<int>(new_value, 0, charts_scroll_->ScrollMax.y);
if (new_value != value) ImGui::SetScrollY(charts_scroll_, new_value);
if (value == new_value) {
stopAutoScroll();
} else if (chartDragActive()) {
dragChartMove(auto_scroll_pos_);
}
}
void ChartsWidget::newChart() {
execSignalSelector(std::make_unique<SignalSelector>("New Chart"), nullptr, [this](SignalSelector &dlg) {
const auto &items = dlg.selectedItems();
if (!items.empty()) {
auto c = createChart();
for (const auto &it : items) {
c->addSignal(it.msg_id, it.sig);
}
updateState();
}
});
}
void ChartsWidget::execSignalSelector(std::unique_ptr<SignalSelector> dlg, ChartView *owner, std::function<void(SignalSelector &)> accepted) {
signal_selector_ = std::move(dlg);
signal_selector_owner_ = owner;
signal_selector_accepted_ = std::move(accepted);
signal_selector_->open();
}
void ChartsWidget::removeChart(ChartView *chart) {
if (drag_.source == chart) cancelChartDrag();
if (drop_target_ == chart) drop_target_ = nullptr;
if (signal_selector_owner_ == chart) {
signal_selector_owner_ = nullptr;
signal_selector_accepted_ = nullptr;
}
auto it = std::find_if(charts_.begin(), charts_.end(), [chart](auto &c) { return c.get() == chart; });
if (it != charts_.end()) {
deleted_charts_.push_back(std::move(*it));
charts_.erase(it);
}
for (auto &[_, list] : tab_charts_) {
list.erase(std::remove(list.begin(), list.end(), chart), list.end());
}
updateLayout();
seriesChanged();
}
void ChartsWidget::removeAll() {
while (tabbar_.count() > 1) {
tabbar_.removeTab(1);
}
std::vector<ChartView *> all;
for (auto &c : charts_) all.push_back(c.get());
for (auto c : all) removeChart(c);
tab_charts_.clear();
zoomReset();
}
void ChartsWidget::handleEvents() {
if (ImGui::IsMouseClicked(3) && ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows)) {
zoom_undo_stack_.undo();
}
if (!ImGui::IsWindowFocused(ImGuiFocusedFlags_AnyWindow)) {
if (chartDragActive()) cancelChartDrag();
showValueTip(-1);
}
if (chartDragActive()) {
if (ImGui::IsMouseDown(ImGuiMouseButton_Left)) {
dragChartMove(ImGui::GetMousePos());
} else {
dragChartRelease(ImGui::GetMousePos());
}
}
if (!value_tip_visible_) return;
const ImVec2 delta = ImGui::GetIO().MouseDelta;
if (!any_plot_hovered_ &&
(delta.x != 0 || delta.y != 0 || !ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows))) {
showValueTip(-1);
}
}
void ChartsWidget::draw() {
deleted_charts_.clear();
if (float_window_init_ && !is_docked_) {
float_window_init_ = false;
const ImGuiViewport *viewport = ImGui::GetMainViewport();
const ImVec2 size(viewport->WorkSize.x * 0.6f, viewport->WorkSize.y * 0.6f);
ImGui::SetWindowSize(size);
ImGui::SetWindowPos(viewport->WorkPos + (viewport->WorkSize - size) * 0.5f);
}
ImGui::PushID(this);
if (auto_scroll_timer_active_ && ImGui::GetTime() >= auto_scroll_timer_next_) {
auto_scroll_timer_next_ = ImGui::GetTime() + 0.05;
doAutoScroll();
}
handleEvents();
drawToolBar();
tabbar_.draw();
any_plot_hovered_ = false;
if (ImGui::BeginChild("charts_scroll", ImVec2(0, 0), ImGuiChildFlags_None, 0)) {
charts_scroll_ = ImGui::GetCurrentWindow();
charts_scroll_viewport_ = charts_scroll_->InnerRect;
charts_container_.draw();
}
ImGui::EndChild();
drawDragPreview();
if (signal_selector_ && !signal_selector_->draw()) {
auto dlg = std::move(signal_selector_);
auto accepted = std::move(signal_selector_accepted_);
signal_selector_owner_ = nullptr;
if (dlg->accepted() && accepted) accepted(*dlg);
}
ImGui::PopID();
}
void ChartsContainer::draw() {
ImGuiWindow *window = ImGui::GetCurrentWindow();
const ImVec2 start = ImGui::GetCursorScreenPos();
geometry_ = ImRect(start, start + ImVec2(window->InnerRect.GetWidth(), 0));
charts_widget_->updateLayout();
const int n = std::max(charts_widget_->current_column_count_, 1);
const float spacing = CHART_SPACING;
const float width = (geometry_.GetWidth() - (n - 1) * spacing) / n;
const ImVec2 origin = ImGui::GetCursorScreenPos() + ImVec2(0, CHART_SPACING);
auto current_charts = charts_widget_->currentCharts();
float bottom = origin.y;
const bool aligned = ImPlot::BeginAlignedPlots("charts_align", true);
for (int i = 0; i < current_charts.size(); ++i) {
ImVec2 pos = origin + ImVec2((i % n) * (width + spacing), (i / n) * (settings.chart_height + spacing));
ImGui::SetCursorScreenPos(pos);
current_charts[i]->draw(width);
bottom = std::max(bottom, pos.y + settings.chart_height);
if (current_charts[i]->plotHovered()) charts_widget_->any_plot_hovered_ = true;
}
if (aligned) ImPlot::EndAlignedPlots();
ImGui::SetCursorScreenPos(ImVec2(origin.x, bottom));
ImGui::Dummy(ImVec2(geometry_.GetWidth(), CHART_SPACING));
geometry_.Max.y = bottom + CHART_SPACING;
drawDropIndicator();
}
void ChartsContainer::drawDropIndicator() {
if (!(drop_indicator_pos_.x == 0 && drop_indicator_pos_.y == 0) && !childAt(drop_indicator_pos_)) {
ImRect r = geometry_;
r.Max.y = r.Min.y + CHART_SPACING;
if (auto insert_after = getDropAfter(drop_indicator_pos_)) {
float h = r.GetHeight();
r.Min.y = insert_after->rect().Max.y;
r.Max.y = r.Min.y + h;
}
ImGui::GetWindowDrawList()->AddRectFilled(r.Min, r.Max, ImGui::GetColorU32(ImGuiCol_Header));
}
}
ChartView *ChartsContainer::getDropAfter(const ImVec2 &pos) const {
const auto &charts = charts_widget_->currentCharts();
auto it = std::find_if(charts.crbegin(), charts.crend(), [&pos](auto c) {
const ImRect &area = c->rect();
return pos.x >= area.Min.x && pos.x <= area.Max.x && pos.y >= area.Max.y;
});
return it == charts.crend() ? nullptr : *it;
}
ChartView *ChartsContainer::childAt(const ImVec2 &pos) const {
for (auto c : charts_widget_->currentCharts()) {
if (c->rect().Contains(pos)) return c;
}
return nullptr;
}