geopro/src/app/Glyphs.cpp

231 lines
9.1 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "Glyphs.hpp"
#include <QAbstractButton>
#include <QByteArray>
#include <QDir>
#include <QImage>
#include <QLabel>
#include <QObject>
#include <QPainter>
#include <QPen>
#include <QPixmap>
#include <QPointF>
#include <QRectF>
#include <QSize>
#include <QString>
#include <QSvgRenderer>
#include "Theme.hpp"
namespace geopro::app {
namespace {
// 每个图标的 SVG 内容线性风格24x24 视窗,描边由外层注入颜色)。
// 路径取自 Lucide 图标集MIT 许可,业界通用),保证专业、清晰、语义正确。
QString svgPathFor(Glyph t)
{
switch (t) {
case Glyph::Tree:
return QStringLiteral(
"<path d='M21 12h-8'/><path d='M21 6H8'/><path d='M21 18h-8'/>"
"<path d='M3 6v4c0 1.1.9 2 2 2h3'/><path d='M3 10v6c0 1.1.9 2 2 2h3'/>");
case Glyph::Dataset:
return QStringLiteral(
"<ellipse cx='12' cy='5' rx='9' ry='3'/>"
"<path d='M3 5v14a9 3 0 0 0 18 0V5'/><path d='M3 12a9 3 0 0 0 18 0'/>");
case Glyph::Map:
return QStringLiteral(
"<path d='m12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 "
"0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83Z'/>"
"<path d='m22 17.65-9.17 4.16a2 2 0 0 1-1.66 0L2 17.65'/>"
"<path d='m22 12.65-9.17 4.16a2 2 0 0 1-1.66 0L2 12.65'/>");
case Glyph::Detail:
return QStringLiteral(
"<path d='M3 3v18h18'/><path d='M18 17V9'/><path d='M13 17V5'/><path d='M8 17v-3'/>");
case Glyph::Anomaly:
return QStringLiteral(
"<path d='m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 "
"1.73-3Z'/><path d='M12 9v4'/><path d='M12 17h.01'/>");
case Glyph::Property:
return QStringLiteral(
"<circle cx='12' cy='12' r='10'/><path d='M12 16v-4'/><path d='M12 8h.01'/>");
case Glyph::Plus:
return QStringLiteral("<path d='M5 12h14'/><path d='M12 5v14'/>");
case Glyph::Filter:
return QStringLiteral(
"<polygon points='22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3'/>");
case Glyph::Upload:
return QStringLiteral(
"<path d='M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4'/>"
"<path d='M17 8l-5-5-5 5'/><path d='M12 3v12'/>");
case Glyph::Download:
return QStringLiteral(
"<path d='M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4'/>"
"<path d='M7 10l5 5 5-5'/><path d='M12 15V3'/>");
case Glyph::Collapse:
return QStringLiteral("<path d='m17 11-5-5-5 5'/><path d='m17 18-5-5-5 5'/>");
case Glyph::Workspace:
return QStringLiteral(
"<rect width='7' height='7' x='3' y='3' rx='1'/>"
"<rect width='7' height='7' x='14' y='3' rx='1'/>"
"<rect width='7' height='7' x='14' y='14' rx='1'/>"
"<rect width='7' height='7' x='3' y='14' rx='1'/>");
case Glyph::Folder:
return QStringLiteral(
"<path d='M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 "
"0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z'/>");
case Glyph::Help:
return QStringLiteral(
"<circle cx='12' cy='12' r='10'/>"
"<path d='M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3'/><path d='M12 17h.01'/>");
case Glyph::Bell:
return QStringLiteral(
"<path d='M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9'/>"
"<path d='M10.3 21a1.94 1.94 0 0 0 3.4 0'/>");
case Glyph::Gear:
return QStringLiteral(
"<path d='M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 "
"0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 "
"1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73 "
".73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 "
"2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 "
"2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 "
".73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 "
"1-1-1.73V4a2 2 0 0 0-2-2z'/><circle cx='12' cy='12' r='3'/>");
}
return QString();
}
} // namespace
QIcon makeGlyph(Glyph type, const QColor& color, int px, int padRight)
{
const QString svg =
QStringLiteral("<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' "
"stroke='%1' stroke-width='2' stroke-linecap='round' "
"stroke-linejoin='round'>%2</svg>")
.arg(color.name(), svgPathFor(type));
QSvgRenderer renderer(svg.toUtf8());
// 以 3x 超采样渲染再设 devicePixelRatio保证在任意缩放/DPI 下都清晰。
constexpr qreal kSuper = 3.0;
const int dim = qRound(px * kSuper); // 图标本体边长(方形)
const int dimW = qRound((px + padRight) * kSuper); // 含右透明内边距的画布宽
QImage img(dimW, dim, QImage::Format_ARGB32_Premultiplied);
img.fill(Qt::transparent);
QPainter p(&img);
p.setRenderHint(QPainter::Antialiasing, true);
renderer.render(&p, QRectF(0, 0, dim, dim)); // 图标居左方形渲染,右侧 padRight 留透明
p.end();
QPixmap pm = QPixmap::fromImage(img);
pm.setDevicePixelRatio(kSuper);
return QIcon(pm);
}
QString writeChevronIcon(bool open, const QColor& color)
{
constexpr int px = 16;
constexpr int kScale = 3;
QPixmap pm(px * kScale, px * kScale);
pm.fill(Qt::transparent);
QPainter p(&pm);
p.setRenderHint(QPainter::Antialiasing, true);
QPen pen(color, px * kScale * 0.1);
pen.setCapStyle(Qt::RoundCap);
pen.setJoinStyle(Qt::RoundJoin);
p.setPen(pen);
const double w = px * kScale;
const double h = px * kScale;
if (open) { // ▼ 向下
p.drawLine(QPointF(w * 0.32, h * 0.42), QPointF(w * 0.5, h * 0.60));
p.drawLine(QPointF(w * 0.5, h * 0.60), QPointF(w * 0.68, h * 0.42));
} else { // ▶ 向右
p.drawLine(QPointF(w * 0.42, h * 0.32), QPointF(w * 0.60, h * 0.5));
p.drawLine(QPointF(w * 0.60, h * 0.5), QPointF(w * 0.42, h * 0.68));
}
p.end();
const QString path =
QDir(QDir::tempPath()).filePath(open ? QStringLiteral("geopro_tree_open.png")
: QStringLiteral("geopro_tree_closed.png"));
pm.save(path, "PNG");
return path;
}
QString writeCheckboxIcon(bool checked, const QColor& border, const QColor& fill, const QColor& check,
const QString& tag)
{
constexpr int px = 16;
constexpr int kScale = 3;
QPixmap pm(px * kScale, px * kScale);
pm.fill(Qt::transparent);
QPainter p(&pm);
p.setRenderHint(QPainter::Antialiasing, true);
const double s = px * kScale;
const double inset = s * 0.14;
const QRectF box(inset, inset, s - 2 * inset, s - 2 * inset);
const double r = s * 0.14;
if (checked) {
p.setPen(Qt::NoPen);
p.setBrush(fill);
p.drawRoundedRect(box, r, r);
QPen cpen(check, s * 0.12);
cpen.setCapStyle(Qt::RoundCap);
cpen.setJoinStyle(Qt::RoundJoin);
p.setPen(cpen);
p.drawLine(QPointF(s * 0.30, s * 0.52), QPointF(s * 0.44, s * 0.66));
p.drawLine(QPointF(s * 0.44, s * 0.66), QPointF(s * 0.70, s * 0.34));
} else {
QPen bpen(border, s * 0.085);
p.setPen(bpen);
p.setBrush(fill);
p.drawRoundedRect(box, r, r);
}
p.end();
const QString path = QDir(QDir::tempPath())
.filePath(QStringLiteral("geopro_chk_%1_%2.png")
.arg(tag, checked ? QStringLiteral("on")
: QStringLiteral("off")));
pm.save(path, "PNG");
return path;
}
namespace {
// 当前主题下的 chrome 图标色:取主题主文本色(暗=浅、亮=深),保证两种模式都清晰。
QColor themedIconColor()
{
return isDarkTheme() ? QColor(0xE6, 0xE8, 0xEB) : QColor(0x1F, 0x2A, 0x3D); // 主文字明/暗
}
} // namespace
void setThemedGlyph(QLabel* label, Glyph type, int px)
{
if (!label) return;
auto apply = [label, type, px]() {
label->setPixmap(makeGlyph(type, themedIconColor(), px).pixmap(px, px));
};
apply();
QObject::connect(&ThemeManager::instance(), &ThemeManager::changed, label, [apply]() { apply(); });
}
void setThemedGlyph(QAbstractButton* button, Glyph type, int px, int padRight)
{
if (!button) return;
auto apply = [button, type, px, padRight]() {
button->setIcon(makeGlyph(type, themedIconColor(), px, padRight));
if (padRight > 0) button->setIconSize(QSize(px + padRight, px)); // 含右内边距,文字被右推
};
apply();
QObject::connect(&ThemeManager::instance(), &ThemeManager::changed, button, [apply]() { apply(); });
}
} // namespace geopro::app