improve: UIGraph with new features by oen (#984)

UIGraph widget got massive update including:

Multiple graphs in a single widget
Showing value of a closest point on a line when widget is hovered
Better performance due to A LOT of caching

author: Oen
This commit is contained in:
InnerCircleTFS 2024-12-09 17:04:32 -03:00 committed by GitHub
parent 0700333f63
commit 5c5dc582a9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 477 additions and 93 deletions

View file

@ -467,6 +467,7 @@ Beyond of it's flexibility with scripts, otclient comes with tons of other featu
- Module Shop
- Module Oufit
- Placeholder
- UIGraph
## <a name="themobileproject"><img height="32" src="https://raw.githubusercontent.com/github/explore/80688e429a7d4ef2fca1e82350fe8e3517d3494d/topics/android/android.png" alt="Android"> The Mobile Project </a>
The Mobile Project

View file

@ -133,7 +133,12 @@ local toggle = function()
end
local drawGraph = function(graph, value)
graph:addValue(value)
if graph:getGraphsCount() == 0 then
graph:createGraph()
graph:setLineWidth(1, 1)
graph:setLineColor(1, "#FF0000")
end
graph:addValue(1, value)
end
local toggleAnalyzer = function(window)

View file

@ -226,8 +226,6 @@ AnalyzerLootItem < UIItem
AnalyzerGraph < UIGraph
height: 140
capacity: 400
line-width: 1
color: red
margin-top: 5
margin-left: 5
margin-right: 5

View file

@ -1063,13 +1063,21 @@ void Client::registerLuaFunctions()
g_lua.bindClassMemberFunction<UIProgressRect>("getPercent", &UIProgressRect::getPercent);
g_lua.registerClass<UIGraph, UIWidget>();
g_lua.bindClassStaticFunction<UIGraph>("create", [] { return UIGraphPtr(new UIGraph); });
g_lua.bindClassMemberFunction<UIGraph>("addValue", &UIGraph::addValue);
g_lua.bindClassStaticFunction<UIGraph>("create", [] { return std::make_shared<UIGraph>(); });
g_lua.bindClassMemberFunction<UIGraph>("clear", &UIGraph::clear);
g_lua.bindClassMemberFunction<UIGraph>("setLineWidth", &UIGraph::setLineWidth);
g_lua.bindClassMemberFunction<UIGraph>("createGraph", &UIGraph::createGraph);
g_lua.bindClassMemberFunction<UIGraph>("getGraphsCount", &UIGraph::getGraphsCount);
g_lua.bindClassMemberFunction<UIGraph>("addValue", &UIGraph::addValue);
g_lua.bindClassMemberFunction<UIGraph>("setCapacity", &UIGraph::setCapacity);
g_lua.bindClassMemberFunction<UIGraph>("setTitle", &UIGraph::setTitle);
g_lua.bindClassMemberFunction<UIGraph>("setShowLabels", &UIGraph::setShowLabels);
g_lua.bindClassMemberFunction<UIGraph>("setShowInfo", &UIGraph::setShowInfo);
g_lua.bindClassMemberFunction<UIGraph>("setLineWidth", &UIGraph::setLineWidth);
g_lua.bindClassMemberFunction<UIGraph>("setLineColor", &UIGraph::setLineColor);
g_lua.bindClassMemberFunction<UIGraph>("setInfoText", &UIGraph::setInfoText);
g_lua.bindClassMemberFunction<UIGraph>("setInfoLineColor", &UIGraph::setInfoLineColor);
g_lua.bindClassMemberFunction<UIGraph>("setTextBackground", &UIGraph::setTextBackground);
g_lua.bindClassMemberFunction<UIGraph>("setGraphVisible", &UIGraph::setGraphVisible);
g_lua.registerClass<UIMapAnchorLayout, UIAnchorLayout>();
}

View file

@ -1,93 +1,428 @@
#include "uigraph.h"
#include <framework/graphics/fontmanager.h>
#include <framework/core/eventdispatcher.h>
#include <framework/graphics/drawpoolmanager.h>
#include <framework/platform/platformwindow.h>
void UIGraph::drawSelf(DrawPoolType drawPane)
{
if (drawPane != DrawPoolType::FOREGROUND)
return;
if (drawPane != DrawPoolType::FOREGROUND)
return;
UIWidget::drawSelf(drawPane);
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);
}
Rect dest = getPaddingRect();
drawImage(m_rect);
float offsetX = dest.left();
float offsetY = dest.top();
size_t elements = std::min<size_t>(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<float>(1, (maxVal - minVal));
std::vector<Point> points;
for (size_t i = start, j = 0; i < m_values.size(); ++i) {
points.push_back(Point(offsetX + j * m_width, offsetY + 1 + (maxVal - m_values[i]) * step));
j += 2;
}
if (m_needsUpdate) {
cacheGraphs();
}
if (elements > 0) {
g_drawPool.addAction([points = std::move(points), width = m_width, color = m_color] {
static std::vector<float> vertices(1024, 0);
if (vertices.size() < points.size())
vertices.resize(points.size());
if (!m_graphs.empty()) {
Rect dest = getPaddingRect();
int i = 0;
for (const auto& point : points) {
vertices[i++] = point.x;
vertices[i++] = point.y;
}
// draw graph first
for (auto& graph : m_graphs) {
if (!graph.visible) continue;
g_painter->setColor(color);
g_painter->drawLine(vertices, i / 2, width);
});
g_drawPool.addAction([points = graph.points, width = graph.width, color = graph.lineColor] {
static std::vector<float> vertices;
vertices.resize(points.size() * 2);
int i = 0;
for (const auto& point : points) {
vertices[i++] = static_cast<float>(point.x);
vertices[i++] = static_cast<float>(point.y);
}
g_painter->setColor(color);
g_painter->drawLine(vertices, points.size(), width);
});
}
if (!m_title.empty())
m_font->drawText(m_title, dest, Color::white, Fw::AlignTopCenter);
if (m_showLabes) {
m_font->drawText(std::to_string(m_values.back()), dest, Color::white, Fw::AlignTopRight);
m_font->drawText(std::to_string(maxVal), dest, Color::white, Fw::AlignTopLeft);
m_font->drawText(std::to_string(minVal), dest, Color::white, Fw::AlignBottomLeft);
}
}
// then update if needed and draw vertical line if hovered
bool updated = false;
for (auto& graph : m_graphs) {
if (!graph.visible) continue;
if (m_showInfo && isHovered()) {
updateGraph(graph, updated);
g_drawPool.addAction([line = graph.infoLine, color = graph.infoLineColor] {
g_painter->setColor(color);
std::vector<float> vertices = {
static_cast<float>(line[0].x), static_cast<float>(line[0].y),
static_cast<float>(line[1].x), static_cast<float>(line[1].y)
};
g_painter->drawLine(vertices, vertices.size() / 2, 1);
});
}
}
// reposition intersecting rects and keep them within rect bounds
if (updated) {
updateInfoBoxes();
}
// now we draw indicators on the graph lines
for (auto& graph : m_graphs) {
if (!graph.visible) continue;
if (m_showInfo && isHovered()) {
g_drawPool.addFilledRect(graph.infoIndicatorBg, Color::white);
g_drawPool.addFilledRect(graph.infoIndicator, graph.lineColor);
}
}
// lastly we can draw info boxes with value
for (auto& graph : m_graphs) {
if (!graph.visible) continue;
if (m_showInfo && isHovered()) {
g_drawPool.addFilledRect(graph.infoRectBg, graph.infoTextBg);
g_drawPool.addFilledRect(graph.infoRectIcon, graph.lineColor);
m_font->drawText(graph.infoValue, graph.infoRect, Color::black, Fw::AlignLeftCenter);
}
}
if (!m_title.empty())
m_font->drawText(m_title, dest, Color::white, Fw::AlignTopCenter);
if (m_showLabes) {
m_font->drawText(m_lastValue, dest, Color::white, Fw::AlignTopRight);
m_font->drawText(m_maxValue, dest, Color::white, Fw::AlignTopLeft);
m_font->drawText(m_minValue, dest, Color::white, Fw::AlignBottomLeft);
}
}
drawBorder(m_rect);
drawIcon(m_rect);
drawText(m_rect);
}
void UIGraph::clear()
{
m_values.clear();
m_graphs.clear();
}
void UIGraph::addValue(int value, bool ignoreSmallValues)
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()
{
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 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. Use:\n"
" if graph:getGraphsCount() == 0 then\n"
" graph:createGraph()\n"
" graph:setLineWidth(1, 1)\n"
" graph:setLineColor(1, \"#FF0000\")\n"
" end\n"
" graph:addValue(1,value)",
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<float>(rect.x());
float paddingY = static_cast<float>(rect.y());
float graphWidth = static_cast<float>(rect.width());
float graphHeight = static_cast<float>(rect.height());
float minValue = 0.0f;
float maxValue = 0.0f;
for (auto& graph : m_graphs) {
if (graph.values.empty())
continue;
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<int>(static_cast<int>(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<int>(x), static_cast<int>(y) });
}
}
m_minValue = std::to_string(static_cast<int>(minValue));
m_maxValue = std::to_string(static_cast<int>(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::updateGraph(Graph& graph, bool& updated)
{
if (graph.values.empty())
return;
auto dest = getPaddingRect();
auto mousePos = g_window.getMousePosition();
float graphWidth = static_cast<float>(dest.width());
float graphHeight = static_cast<float>(dest.height());
float pointSpacing = graphWidth / std::max<int>(static_cast<int>(graph.values.size()) - 1, 1);
int dataIndex = static_cast<int>((mousePos.x - dest.left()) / pointSpacing + 0.5f);
dataIndex = std::clamp(dataIndex, 0, static_cast<int>(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<float>(*minValueIter);
float maxValue = static_cast<float>(*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<Rect> 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());
}
}
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<int>());
else if (node->tag() == "capacity")
setCapacity(node->value<int>());
else if (node->tag() == "title")
setTitle(node->value<std::string>());
else if (node->tag() == "show-labels")
setShowLabels(node->value<bool>());
}
for (const OTMLNodePtr& node : styleNode->children()) {
if (node->tag() == "capacity")
setCapacity(node->value<int>());
else if (node->tag() == "title")
setTitle(node->value());
else if (node->tag() == "show-labels")
setShowLabels(node->value<bool>());
else if (node->tag() == "show-info") // draw info (vertical line, labels with values) on mouse position
setShowInfo(node->value<bool>());
}
}
void UIGraph::onGeometryChange(const Rect& oldRect, const Rect& newRect)
{
UIWidget::onGeometryChange(oldRect, newRect);
m_needsUpdate = true;
}
void UIGraph::onLayoutUpdate()
{
UIWidget::onLayoutUpdate();
m_needsUpdate = true;
}
void UIGraph::onVisibilityChange(bool visible)
{
UIWidget::onVisibilityChange(visible);
m_needsUpdate = visible;
}

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2010-2022 OTClient <https://github.com/edubart/otclient>
* Copyright (c) 2010-2025 OTClient <https://github.com/edubart/otclient>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
@ -25,6 +25,27 @@
#include "declarations.h"
#include <framework/ui/uiwidget.h>
struct Graph
{
std::vector<Point> points;
std::deque<int> 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:
@ -33,32 +54,48 @@ public:
void drawSelf(DrawPoolType drawPane);
void clear();
void addValue(int value, bool ignoreSmallValues = false);
void setLineWidth(int width)
{
m_width = width;
}
void setCapacity(int capacity)
{
size_t createGraph();
size_t getGraphsCount() { return m_graphs.size(); }
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);
void onGeometryChange(const Rect& oldRect, const Rect& newRect);
void onLayoutUpdate();
void onVisibilityChange(bool visible);
void cacheGraphs();
void updateGraph(Graph& graph, bool& updated);
void updateInfoBoxes();
private:
// cache
std::string m_minValue;
std::string m_maxValue;
std::string m_lastValue;
std::string m_title;
bool m_showLabes{ true };
bool m_showInfo{ true };
bool m_needsUpdate{false};
size_t m_capacity = 100;
size_t m_ignores = 0;
int m_width = 1;
bool m_showLabes = true;
std::deque<int> m_values;
std::vector<Graph> m_graphs;
};