From da6a9cc16a81fe012cfe5110f1b9f54760e16c88 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lach Date: Sat, 7 Dec 2024 01:46:03 +0100 Subject: [PATCH] UIGraph improved with new features --- src/client/luafunctions_client.cpp | 10 +- src/client/uigraph.cpp | 468 +++++++++++++++++++++++------ src/client/uigraph.h | 68 +++-- 3 files changed, 421 insertions(+), 125 deletions(-) diff --git a/src/client/luafunctions_client.cpp b/src/client/luafunctions_client.cpp index 12cf59de..055707dc 100644 --- a/src/client/luafunctions_client.cpp +++ b/src/client/luafunctions_client.cpp @@ -968,12 +968,18 @@ void Client::registerLuaFunctions() g_lua.registerClass(); g_lua.bindClassStaticFunction("create", [] { return std::make_shared(); }); - g_lua.bindClassMemberFunction("addValue", &UIGraph::addValue); g_lua.bindClassMemberFunction("clear", &UIGraph::clear); - g_lua.bindClassMemberFunction("setLineWidth", &UIGraph::setLineWidth); + g_lua.bindClassMemberFunction("createGraph", &UIGraph::createGraph); + g_lua.bindClassMemberFunction("addValue", &UIGraph::addValue); g_lua.bindClassMemberFunction("setCapacity", &UIGraph::setCapacity); g_lua.bindClassMemberFunction("setTitle", &UIGraph::setTitle); g_lua.bindClassMemberFunction("setShowLabels", &UIGraph::setShowLabels); + g_lua.bindClassMemberFunction("setLineWidth", &UIGraph::setLineWidth); + g_lua.bindClassMemberFunction("setLineColor", &UIGraph::setLineColor); + g_lua.bindClassMemberFunction("setInfoText", &UIGraph::setInfoText); + g_lua.bindClassMemberFunction("setInfoLineColor", &UIGraph::setInfoLineColor); + g_lua.bindClassMemberFunction("setTextBackground", &UIGraph::setTextBackground); + g_lua.bindClassMemberFunction("setGraphVisible", &UIGraph::setGraphVisible); g_lua.registerClass(); g_lua.bindClassStaticFunction("create", [] { return std::make_shared(); }); diff --git a/src/client/uigraph.cpp b/src/client/uigraph.cpp index 68356f15..4625e722 100644 --- a/src/client/uigraph.cpp +++ b/src/client/uigraph.cpp @@ -1,138 +1,406 @@ - #include "uigraph.h" #include -#include -#include -#include +#include + +UIGraph::UIGraph() +{ + m_needsUpdate = false; + m_showLabes = true; + m_showInfo = false; -UIGraph::UIGraph() : m_needsUpdate(false) {} + m_capacity = 100; + m_ignores = 0; +} void UIGraph::drawSelf(Fw::DrawPane drawPane) { - if (drawPane != Fw::ForegroundPane) - return; + if (drawPane != Fw::ForegroundPane) + return; + + if (m_backgroundColor.aF() > Fw::MIN_ALPHA) { + Rect backgroundDestRect = m_rect; + backgroundDestRect.expand(-m_borderWidth.top, -m_borderWidth.right, -m_borderWidth.bottom, -m_borderWidth.left); + drawBackground(m_rect); + } + + drawImage(m_rect); + + if (m_needsUpdate) { + cacheGraphs(); + } + + if (!m_graphs.empty()) { + Rect dest = getPaddingRect(); + + // draw graph first + for (auto& graph : m_graphs) { + if (!graph.visible) continue; + + g_drawQueue->addLine(graph.points, graph.width, graph.lineColor); + } + + // then update if needed and draw vertical line if hovered + bool updated = false; + for (auto& graph : m_graphs) { + if (!graph.visible) continue; - if (m_backgroundColor.aF() > Fw::MIN_ALPHA) { - Rect backgroundDestRect = m_rect; - backgroundDestRect.expand(-m_borderWidth.top, -m_borderWidth.right, -m_borderWidth.bottom, -m_borderWidth.left); - drawBackground(m_rect); - } + if (m_showInfo && isHovered()) { + updateGraph(graph, updated); + g_drawQueue->addLine({ graph.infoLine[0], graph.infoLine[1] }, 1, graph.infoLineColor); + } + } - drawImage(m_rect); + // reposition intersecting rects and keep them within rect bounds + if (updated) { + updateInfoBoxes(); + } - if (m_needsUpdate) { - updateGraph(); - } + // now we draw indicators on the graph lines + for (auto& graph : m_graphs) { + if (!graph.visible) continue; - if (!m_points.empty()) { - g_drawQueue->addLine(m_points, m_width, m_color); + if (m_showInfo && isHovered()) { + g_drawQueue->addFilledRect(graph.infoIndicatorBg, Color::white); + g_drawQueue->addFilledRect(graph.infoIndicator, graph.lineColor); + } + } - Rect dest = getPaddingRect(); - if (!m_title.empty()) - g_drawQueue->addText(m_font, m_title, dest, Fw::AlignTopCenter); - if (m_showLabes) { - g_drawQueue->addText(m_font, m_lastValue, dest, Fw::AlignTopRight); - g_drawQueue->addText(m_font, m_maxValue, dest, Fw::AlignTopLeft); - g_drawQueue->addText(m_font, m_minValue, dest, Fw::AlignBottomLeft); - } - } + // lastly we can draw info boxes with value + for (auto& graph : m_graphs) { + if (!graph.visible) continue; - drawBorder(m_rect); - drawIcon(m_rect); - drawText(m_rect); + if (m_showInfo && isHovered()) { + g_drawQueue->addFilledRect(graph.infoRectBg, graph.infoTextBg); + g_drawQueue->addFilledRect(graph.infoRectIcon, graph.lineColor); + m_font->drawText(graph.infoValue, graph.infoRect, Fw::AlignLeftCenter, Color::black, m_shadow); + } + } + + if (!m_title.empty()) + g_drawQueue->addText(m_font, m_title, dest, Fw::AlignTopCenter); + if (m_showLabes) { + g_drawQueue->addText(m_font, m_lastValue, dest, Fw::AlignTopRight); + g_drawQueue->addText(m_font, m_maxValue, dest, Fw::AlignTopLeft); + g_drawQueue->addText(m_font, m_minValue, dest, Fw::AlignBottomLeft); + } + } + + drawBorder(m_rect); + drawIcon(m_rect); + drawText(m_rect); } void UIGraph::clear() { - m_values.clear(); - m_points.clear(); + m_graphs.clear(); +} + +void UIGraph::setLineWidth(size_t index, int width) { + if (m_graphs.size() <= index - 1) { + g_logger.warning(stdext::format("[UIGraph::setLineWidth (%s)] Graph of index %d out of bounds.", getId(), index)); + return; + } + + auto& graph = m_graphs[index - 1]; + graph.width = width; + m_needsUpdate = true; +} + +size_t UIGraph::createGraph() +{ + auto graph = Graph(); + + graph.points = {}; + graph.values = {}; + + graph.infoLine[0] = Point(); + graph.infoLine[1] = Point(); + + graph.originalInfoRect = Rect(); + graph.infoRect = Rect(); + graph.infoRectBg = Rect(); + graph.infoRectIcon = Rect(0, 0, 10, 10); + graph.infoIndicator = Rect(0, 0, 5, 5); + graph.infoIndicatorBg = Rect(0, 0, 7, 7); + + graph.infoText = "Value: "; + + graph.infoLineColor = Color::white; + graph.infoTextBg = Color(0, 0, 0, 100); + graph.lineColor = Color::white; + + graph.width = 1; + graph.infoIndex = -1; + + graph.visible = true; + + m_graphs.push_back(graph); + return m_graphs.size(); +} + +void UIGraph::addValue(size_t index, int value, bool ignoreSmallValues) +{ + if (m_graphs.size() <= index - 1) { + g_logger.warning(stdext::format("[UIGraph::addValue (%s)] Graph of index %d out of bounds.", getId(), index)); + return; + } + + auto& graph = m_graphs[index - 1]; + + if (ignoreSmallValues) { + if (!graph.values.empty() && graph.values.back() <= 2 && value <= 2 && ++m_ignores <= 10) + return; + m_ignores = 0; + } + + graph.values.push_back(value); + while (graph.values.size() > m_capacity) + graph.values.pop_front(); + + m_needsUpdate = true; +} + +void UIGraph::setLineColor(size_t index, const Color& color) +{ + if (m_graphs.size() <= index - 1) { + g_logger.warning(stdext::format("[UIGraph::setLineColor (%s)] Graph of index %d out of bounds.", getId(), index)); + return; + } + + auto& graph = m_graphs[index - 1]; + graph.lineColor = color; +} + +void UIGraph::setInfoText(size_t index, const std::string& text) +{ + if (m_graphs.size() <= index - 1) { + g_logger.warning(stdext::format("[UIGraph::setInfoText (%s)] Graph of index %d out of bounds.", getId(), index)); + return; + } + + auto& graph = m_graphs[index - 1]; + graph.infoText = text; +} + +void UIGraph::setGraphVisible(size_t index, bool visible) +{ + if (m_graphs.size() <= index - 1) { + g_logger.warning(stdext::format("[UIGraph::setGraphVisible (%s)] Graph of index %d out of bounds.", getId(), index)); + return; + } + + auto& graph = m_graphs[index - 1]; + graph.visible = visible; +} + +void UIGraph::setInfoLineColor(size_t index, const Color& color) +{ + if (m_graphs.size() <= index - 1) { + g_logger.warning(stdext::format("[UIGraph::setInfoLineColor (%s)] Graph of index %d out of bounds.", getId(), index)); + return; + } + + auto& graph = m_graphs[index - 1]; + graph.infoLineColor = color; +} + +void UIGraph::setTextBackground(size_t index, const Color& color) +{ + if (m_graphs.size() <= index - 1) { + g_logger.warning(stdext::format("[UIGraph::setTextBackground (%s)] Graph of index %d out of bounds.", getId(), index)); + return; + } + + auto& graph = m_graphs[index - 1]; + graph.infoTextBg = color; +} + +void UIGraph::cacheGraphs() +{ + if (!m_needsUpdate) + return; + + if (!m_rect.isEmpty() && m_rect.isValid()) { + if (!m_graphs.empty()) { + Rect rect = getPaddingRect(); + + float paddingX = static_cast(rect.x()); + float paddingY = static_cast(rect.y()); + float graphWidth = static_cast(rect.width()); + float graphHeight = static_cast(rect.height()); + + float minValue = 0.0f; + float maxValue = 0.0f; + for (auto& graph : m_graphs) { + graph.points.clear(); + + auto [minValueIter, maxValueIter] = std::minmax_element(graph.values.begin(), graph.values.end()); + minValue = *minValueIter; + maxValue = *maxValueIter; + float range = maxValue - minValue; + if (range == 0.0f) + range = 1.0f; + + float pointSpacing = graphWidth / std::max(static_cast(graph.values.size()) - 1, 1); + for (size_t i = 0; i < graph.values.size(); ++i) { + float x = paddingX + i * pointSpacing; + float y = paddingY + graphHeight - ((graph.values[i] - minValue) / range) * graphHeight; + graph.points.push_back({ static_cast(x), static_cast(y) }); + } + } + + m_minValue = std::to_string(static_cast(minValue)); + m_maxValue = std::to_string(static_cast(maxValue)); + if (!m_graphs[0].values.empty()) + m_lastValue = std::to_string(m_graphs[0].values.back()); + else + m_lastValue = "0"; + } + + m_needsUpdate = false; + } } -void UIGraph::addValue(int value, bool ignoreSmallValues) +void UIGraph::updateGraph(Graph& graph, bool& updated) { - if (ignoreSmallValues) { - if (!m_values.empty() && m_values.back() <= 2 && value <= 2 && ++m_ignores <= 10) - return; - m_ignores = 0; - } - m_values.push_back(value); - while (m_values.size() > m_capacity) - m_values.pop_front(); + auto dest = getPaddingRect(); + auto mousePos = g_window.getMousePosition(); + float graphWidth = static_cast(dest.width()); + float graphHeight = static_cast(dest.height()); + float pointSpacing = graphWidth / std::max(static_cast(graph.values.size()) - 1, 1); + + int dataIndex = static_cast((mousePos.x - dest.left()) / pointSpacing + 0.5f); + dataIndex = std::clamp(dataIndex, 0, static_cast(graph.values.size()) - 1); + + if (graph.infoIndex != dataIndex) { + graph.infoIndex = dataIndex; + + float snappedX = dest.left() + dataIndex * pointSpacing; + int value = graph.values[graph.infoIndex]; + + graph.infoLine[0] = Point(snappedX, dest.top()); + graph.infoLine[1] = Point(snappedX, dest.bottom()); + + graph.infoValue = stdext::format("%s %d", graph.infoText, value); + + auto [minValueIter, maxValueIter] = std::minmax_element(graph.values.begin(), graph.values.end()); + float minValue = static_cast(*minValueIter); + float maxValue = static_cast(*maxValueIter); + float range = maxValue - minValue; + if (range == 0.0f) + range = 1.0f; + + float pointY = dest.top() + graphHeight - ((value - minValue) / range) * graphHeight; + + auto textSize = m_font->calculateTextRectSize(graph.infoValue); + graph.infoRectBg.setWidth(textSize.width() + 16); + graph.infoRectBg.setHeight(textSize.height()); + graph.infoRectBg.expand(4); + graph.infoRectBg.moveTop(pointY - graph.infoRectBg.height() / 2.0); + if (snappedX >= dest.horizontalCenter()) + graph.infoRectBg.moveRight(snappedX - 10); + else + graph.infoRectBg.moveLeft(snappedX + 10); + + graph.infoRect.setWidth(textSize.width()); + graph.infoRect.setHeight(textSize.height()); + graph.infoRect.moveRight(graph.infoRectBg.right() - 4); + graph.infoRect.moveVerticalCenter(graph.infoRectBg.verticalCenter()); + + int iconPadding = graph.infoRectBg.height() - graph.infoRectIcon.width(); + graph.infoRectIcon.moveLeft(graph.infoRectBg.left() + (iconPadding / 2.0)); + graph.infoRectIcon.moveVerticalCenter(graph.infoRectBg.verticalCenter()); + + graph.infoIndicator.moveLeft(snappedX - 3); + graph.infoIndicator.moveTop(pointY - 3); + graph.infoIndicatorBg.moveCenter(graph.infoIndicator.center()); + + graph.originalInfoRect = graph.infoRectBg; + updated = true; + } +} + +void UIGraph::updateInfoBoxes() +{ + auto dest = getPaddingRect(); + std::vector occupiedSpaces(m_graphs.size()); + for (size_t i = 0; i < m_graphs.size(); ++i) { + auto& graph = m_graphs[i]; + + graph.infoRectBg = graph.originalInfoRect; + graph.infoRect.moveVerticalCenter(graph.infoRectBg.verticalCenter()); + graph.infoRectIcon.moveVerticalCenter(graph.infoRectBg.verticalCenter()); + + occupiedSpaces[i] = graph.infoRectBg; + + for (size_t j = 0; j < occupiedSpaces.size(); ++j) { + if (i == j) continue; // graph's space, ignore + + auto& space = occupiedSpaces[j]; + // first check if this graph occupies another graph's space and move it above the space + if (space.intersects(graph.infoRectBg)) { + graph.infoRectBg.moveBottom(space.top() - 2); + graph.infoRect.moveVerticalCenter(graph.infoRectBg.verticalCenter()); + graph.infoRectIcon.moveVerticalCenter(graph.infoRectBg.verticalCenter()); + } + + // lets make sure its within bounds of this widget + if (graph.infoRectBg.top() < dest.top()) { + graph.infoRectBg.moveTop(dest.top()); + graph.infoRect.moveVerticalCenter(graph.infoRectBg.verticalCenter()); + graph.infoRectIcon.moveVerticalCenter(graph.infoRectBg.verticalCenter()); + } + + // if we just moved due to bounds check, we have to make sure we are not occuping another graph + // this time move it below the occupied space + if (space.intersects(graph.infoRectBg)) { + graph.infoRectBg.moveTop(space.bottom() + 2); + graph.infoRect.moveVerticalCenter(graph.infoRectBg.verticalCenter()); + graph.infoRectIcon.moveVerticalCenter(graph.infoRectBg.verticalCenter()); + } + + // and check again if we are within bounds + if (graph.infoRectBg.bottom() > dest.bottom()) { + graph.infoRectBg.moveBottom(dest.bottom()); + graph.infoRect.moveVerticalCenter(graph.infoRectBg.verticalCenter()); + graph.infoRectIcon.moveVerticalCenter(graph.infoRectBg.verticalCenter()); + } + } - m_needsUpdate = true; + occupiedSpaces[i] = graph.infoRectBg; + } } void UIGraph::onStyleApply(const std::string& styleName, const OTMLNodePtr& styleNode) { - UIWidget::onStyleApply(styleName, styleNode); + UIWidget::onStyleApply(styleName, styleNode); - for (const OTMLNodePtr& node : styleNode->children()) { - if (node->tag() == "line-width") - setLineWidth(node->value()); - else if (node->tag() == "capacity") - setCapacity(node->value()); - else if (node->tag() == "title") - setTitle(node->value()); - else if (node->tag() == "show-labels") - setShowLabels(node->value()); - } + for (const OTMLNodePtr& node : styleNode->children()) { + if (node->tag() == "capacity") + setCapacity(node->value()); + else if (node->tag() == "title") + setTitle(node->value()); + else if (node->tag() == "show-labels") + setShowLabels(node->value()); + else if (node->tag() == "show-info") // draw info (vertical line, labels with values) on mouse position + setShowInfo(node->value()); + } } void UIGraph::onGeometryChange(const Rect& oldRect, const Rect& newRect) { - UIWidget::onGeometryChange(oldRect, newRect); - m_needsUpdate = true; + UIWidget::onGeometryChange(oldRect, newRect); + m_needsUpdate = true; } void UIGraph::onLayoutUpdate() { - UIWidget::onLayoutUpdate(); - m_needsUpdate = true; + UIWidget::onLayoutUpdate(); + m_needsUpdate = true; } void UIGraph::onVisibilityChange(bool visible) { - UIWidget::onVisibilityChange(visible); - m_needsUpdate = visible; -} - -void UIGraph::updateGraph() -{ - if (!m_needsUpdate) - return; - - m_points.clear(); - - if (!m_rect.isEmpty() && m_rect.isValid()) { - if (!m_values.empty()) { - Rect dest = getPaddingRect(); - - float offsetX = dest.left(); - float offsetY = dest.top(); - size_t elements = std::min(m_values.size(), dest.width() / (m_width * 2) - 1); - size_t start = m_values.size() - elements; - int minVal = 0xFFFFFF, maxVal = -0xFFFFFF; - for (size_t i = start; i < m_values.size(); ++i) { - if (minVal > m_values[i]) - minVal = m_values[i]; - if (maxVal < m_values[i]) - maxVal = m_values[i]; - } - - // round - maxVal = (1 + maxVal / 10) * 10; - minVal = (minVal / 10) * 10; - float step = (float)(dest.height()) / std::max(1, (maxVal - minVal)); - for (size_t i = start, j = 0; i < m_values.size(); ++i) { - m_points.push_back(Point(offsetX + j * m_width, offsetY + 1 + (maxVal - m_values[i]) * step)); - j += 2; - } - - m_minValue = std::to_string(minVal); - m_maxValue = std::to_string(maxVal); - m_lastValue = std::to_string(m_values.back()); - } - - m_needsUpdate = false; - } + UIWidget::onVisibilityChange(visible); + m_needsUpdate = visible; } diff --git a/src/client/uigraph.h b/src/client/uigraph.h index 3ce2c8b9..544dc06c 100644 --- a/src/client/uigraph.h +++ b/src/client/uigraph.h @@ -4,6 +4,26 @@ #include "declarations.h" #include +struct Graph { + std::vector points; + std::deque values; + Point infoLine[2]; + Rect originalInfoRect; + Rect infoRect; + Rect infoRectBg; + Rect infoRectIcon; + Rect infoIndicator; + Rect infoIndicatorBg; + std::string infoValue; + std::string infoText; + Color infoLineColor; + Color infoTextBg; + Color lineColor; + int width; + int infoIndex; + bool visible; +}; + class UIGraph : public UIWidget { public: UIGraph(); @@ -11,25 +31,23 @@ class UIGraph : public UIWidget { void drawSelf(Fw::DrawPane drawPane); void clear(); - void addValue(int value, bool ignoreSmallValues = false); - void setLineWidth(int width) - { - m_width = width; - m_needsUpdate = true; - } - void setCapacity(int capacity) - { + size_t createGraph(); + void addValue(size_t index, int value, bool ignoreSmallValues = false); + + void setCapacity(int capacity) { m_capacity = capacity; m_needsUpdate = true; } - void setTitle(const std::string& title) - { - m_title = title; - } - void setShowLabels(bool value) - { - m_showLabes = value; - } + void setTitle(const std::string& title) { m_title = title; } + void setShowLabels(bool value) { m_showLabes = value; } + void setShowInfo(bool value) { m_showInfo = value; } + + void setLineWidth(size_t index, int width); + void setLineColor(size_t index, const Color& color); + void setInfoText(size_t index, const std::string& text); + void setInfoLineColor(size_t index, const Color& color); + void setTextBackground(size_t index, const Color& color); + void setGraphVisible(size_t index, bool visible); protected: void onStyleApply(const std::string& styleName, const OTMLNodePtr& styleNode); @@ -37,21 +55,25 @@ class UIGraph : public UIWidget { void onLayoutUpdate(); void onVisibilityChange(bool visible); - void updateGraph(); + void cacheGraphs(); + void updateGraph(Graph& graph, bool& updated); + void updateInfoBoxes(); private: + // cache bool m_needsUpdate; - std::vector m_points; std::string m_minValue; std::string m_maxValue; std::string m_lastValue; std::string m_title; - size_t m_capacity = 100; - size_t m_ignores = 0; - int m_width = 1; - bool m_showLabes = true; - std::deque m_values; + bool m_showLabes; + bool m_showInfo; + + size_t m_capacity; + size_t m_ignores; + + std::vector m_graphs; }; #endif \ No newline at end of file