new chatlistview component, file attachments, performance & primitive test
This commit is contained in:
+182
-42
@@ -10,9 +10,14 @@
|
||||
#include <QFile>
|
||||
#include <QFontDatabase>
|
||||
#include <QHash>
|
||||
#include <QMutex>
|
||||
#include <QMutexLocker>
|
||||
#include <QPainter>
|
||||
#include <QPointer>
|
||||
#include <QThreadPool>
|
||||
|
||||
#include <QtQuick/QQuickImageProvider>
|
||||
|
||||
namespace ZShell::llm {
|
||||
|
||||
namespace {
|
||||
@@ -71,26 +76,27 @@ const LatinModern& loadLatinModern() {
|
||||
return fonts;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
LlmMathText::LlmMathText(QObject* parent)
|
||||
: QObject(parent)
|
||||
, m_renderer(
|
||||
std::make_shared<JKQTMathText>(nullptr, /* useFontsForGUI */ true)) {
|
||||
void configureLatinModern(JKQTMathText& renderer) {
|
||||
const LatinModern& fonts = loadLatinModern();
|
||||
if (fonts.roman)
|
||||
m_renderer->setFontRomanAndMath(
|
||||
renderer.setFontRomanAndMath(
|
||||
QStringLiteral("LMRoman10"), JKQTMathTextFontEncoding::MTFEUnicode);
|
||||
if (fonts.math) {
|
||||
m_renderer->setFontMathRoman(
|
||||
renderer.setFontMathRoman(
|
||||
QStringLiteral("Latin Modern Math"),
|
||||
JKQTMathTextFontEncoding::MTFEUnicode);
|
||||
m_renderer->setFallbackFontSymbols(
|
||||
renderer.setFallbackFontSymbols(
|
||||
QStringLiteral("Latin Modern Math"),
|
||||
JKQTMathTextFontEncoding::MTFEUnicode);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
LlmMathText::LlmMathText(QObject* parent) : QObject(parent) {
|
||||
loadLatinModern();
|
||||
}
|
||||
|
||||
void LlmMathText::setLatex(const QString& value) {
|
||||
if (m_latex == value) return;
|
||||
m_latex = value;
|
||||
@@ -120,23 +126,88 @@ namespace {
|
||||
struct MathRender {
|
||||
bool ok = false;
|
||||
QImage image;
|
||||
QUrl url;
|
||||
QString id;
|
||||
qreal width = 0;
|
||||
qreal height = 0;
|
||||
};
|
||||
|
||||
QHash<QString, MathRender>& mathCache() {
|
||||
static QHash<QString, MathRender> cache;
|
||||
return cache;
|
||||
struct MathStore {
|
||||
QMutex mutex;
|
||||
QHash<QString, MathRender> entries;
|
||||
QStringList order;
|
||||
QHash<QString, QString> idIndex;
|
||||
QHash<QString, int> idRefs;
|
||||
qint64 bytes = 0;
|
||||
quint64 nextId = 1;
|
||||
};
|
||||
|
||||
MathStore& mathStore() {
|
||||
static MathStore store;
|
||||
return store;
|
||||
}
|
||||
|
||||
qint64 imageBytes(const QImage& image) {
|
||||
return image.isNull() ? 0 : static_cast<qint64>(image.sizeInBytes());
|
||||
}
|
||||
|
||||
void retainMathId(const QString& id) {
|
||||
if (id.isEmpty()) return;
|
||||
auto& store = mathStore();
|
||||
QMutexLocker locker(&store.mutex);
|
||||
++store.idRefs[id];
|
||||
}
|
||||
|
||||
void releaseMathId(const QString& id) {
|
||||
if (id.isEmpty()) return;
|
||||
auto& store = mathStore();
|
||||
QMutexLocker locker(&store.mutex);
|
||||
auto it = store.idRefs.find(id);
|
||||
if (it == store.idRefs.end()) return;
|
||||
if (--it.value() <= 0) store.idRefs.erase(it);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr qint64 kMaxCacheBytes = 64 * 1024 * 1024;
|
||||
|
||||
class MathImageProvider : public QQuickImageProvider {
|
||||
public:
|
||||
MathImageProvider() : QQuickImageProvider(QQuickImageProvider::Image) {}
|
||||
|
||||
QImage requestImage(
|
||||
const QString& id, QSize* size, const QSize& requestedSize) override {
|
||||
Q_UNUSED(requestedSize)
|
||||
auto& store = mathStore();
|
||||
QMutexLocker locker(&store.mutex);
|
||||
const auto keyIt = store.idIndex.constFind(id);
|
||||
if (keyIt == store.idIndex.constEnd()) return {};
|
||||
const QString key = keyIt.value();
|
||||
const auto entryIt = store.entries.constFind(key);
|
||||
if (entryIt == store.entries.constEnd()) return {};
|
||||
if (size) *size = entryIt->image.size();
|
||||
const qsizetype pos = store.order.indexOf(key);
|
||||
if (pos >= 0) store.order.move(pos, store.order.size() - 1);
|
||||
return entryIt->image;
|
||||
}
|
||||
};
|
||||
|
||||
void ensureImageProvider(QObject* owner) {
|
||||
if (auto* engine = qmlEngine(owner))
|
||||
if (!engine->imageProvider(QStringLiteral("llmmath")))
|
||||
engine->addImageProvider(
|
||||
QStringLiteral("llmmath"), new MathImageProvider());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void LlmMathText::reRender() {
|
||||
ensureImageProvider(this);
|
||||
++m_requestId;
|
||||
if (m_latex.trimmed().isEmpty()) {
|
||||
m_image = QImage();
|
||||
m_imageUrl = QUrl();
|
||||
releaseMathId(m_imageId);
|
||||
m_imageId.clear();
|
||||
m_width = 0;
|
||||
m_height = 0;
|
||||
m_ok = false;
|
||||
@@ -147,12 +218,30 @@ void LlmMathText::reRender() {
|
||||
const QString key = m_latex + QLatin1Char(0x1f) + m_color.name() +
|
||||
QLatin1Char(0x1f) + QString::number(m_fontPointSize) +
|
||||
QLatin1Char(0x1f) + QString::number(m_devicePixelRatio);
|
||||
if (auto it = mathCache().find(key); it != mathCache().end()) {
|
||||
m_image = it->image;
|
||||
m_imageUrl = it->url;
|
||||
m_width = it->width;
|
||||
m_height = it->height;
|
||||
m_ok = it->ok;
|
||||
bool cacheHit = false;
|
||||
QString hitId;
|
||||
qreal hitW = 0;
|
||||
qreal hitH = 0;
|
||||
bool hitOk = false;
|
||||
{
|
||||
auto& store = mathStore();
|
||||
QMutexLocker locker(&store.mutex);
|
||||
const auto it = store.entries.constFind(key);
|
||||
if (it != store.entries.constEnd()) {
|
||||
cacheHit = true;
|
||||
hitId = it->id;
|
||||
hitW = it->width;
|
||||
hitH = it->height;
|
||||
hitOk = it->ok;
|
||||
}
|
||||
}
|
||||
if (cacheHit) {
|
||||
releaseMathId(m_imageId);
|
||||
retainMathId(hitId);
|
||||
m_imageId = hitId;
|
||||
m_width = hitW;
|
||||
m_height = hitH;
|
||||
m_ok = hitOk;
|
||||
Q_EMIT changed();
|
||||
return;
|
||||
}
|
||||
@@ -164,33 +253,46 @@ void LlmMathText::reRender() {
|
||||
const QColor color = m_color;
|
||||
const double pointSize = m_fontPointSize;
|
||||
const qreal dpr = m_devicePixelRatio;
|
||||
auto renderer = m_renderer;
|
||||
QThreadPool::globalInstance()->start(
|
||||
[this, renderer, id = m_requestId, key, latex, color, pointSize, dpr]() {
|
||||
[this, id = m_requestId, key, latex, color, pointSize, dpr]() {
|
||||
JKQTMathText renderer(nullptr, true);
|
||||
configureLatinModern(renderer);
|
||||
MathRender render;
|
||||
renderer->setFontPointSize(pointSize);
|
||||
renderer->setFontColor(color);
|
||||
if (renderer->parse(
|
||||
renderer.setFontPointSize(pointSize);
|
||||
renderer.setFontColor(color);
|
||||
if (renderer.parse(
|
||||
latex,
|
||||
JKQTMathText::LatexParser,
|
||||
JKQTMathText::DefaultParseOptions)) {
|
||||
const QImage image = renderer->drawIntoImage(
|
||||
/* drawBoxes */ false,
|
||||
{
|
||||
QImage scratch(1, 1, QImage::Format_ARGB32);
|
||||
QPainter painter(&scratch);
|
||||
const QSizeF size = renderer.getSize(painter);
|
||||
render.width = size.width() + 2 * kRenderMargin;
|
||||
render.height = size.height() + 2 * kRenderMargin;
|
||||
}
|
||||
QPointer<LlmMathText> sizeGuard(this);
|
||||
const qreal sizeW = render.width;
|
||||
const qreal sizeH = render.height;
|
||||
QMetaObject::invokeMethod(
|
||||
QCoreApplication::instance(),
|
||||
[sizeGuard, id, sizeW, sizeH]() {
|
||||
LlmMathText* self = sizeGuard;
|
||||
if (!self || id != self->m_requestId) return;
|
||||
self->m_width = sizeW;
|
||||
self->m_height = sizeH;
|
||||
Q_EMIT self->changed();
|
||||
},
|
||||
Qt::QueuedConnection);
|
||||
|
||||
const QImage image = renderer.drawIntoImage(
|
||||
false,
|
||||
QColor(Qt::transparent),
|
||||
kRenderMargin,
|
||||
dpr,
|
||||
kResolutionDpi);
|
||||
if (!image.isNull()) {
|
||||
QByteArray png;
|
||||
{
|
||||
QBuffer buffer(&png);
|
||||
buffer.open(QIODevice::WriteOnly);
|
||||
image.save(&buffer, "PNG");
|
||||
}
|
||||
render.image = image;
|
||||
render.url = QUrl(
|
||||
QStringLiteral("data:image/png;base64,") +
|
||||
QString::fromLatin1(png.toBase64()));
|
||||
render.width = image.width() / dpr;
|
||||
render.height = image.height() / dpr;
|
||||
render.ok = true;
|
||||
@@ -208,12 +310,46 @@ void LlmMathText::reRender() {
|
||||
return;
|
||||
}
|
||||
if (render.ok) {
|
||||
auto& cache = mathCache();
|
||||
if (cache.size() >= kCacheLimit) cache.clear();
|
||||
cache.insert(key, render);
|
||||
auto& store = mathStore();
|
||||
QMutexLocker locker(&store.mutex);
|
||||
const bool isNew = !store.entries.contains(key);
|
||||
if (isNew)
|
||||
render.id = QString::number(store.nextId++);
|
||||
else
|
||||
render.id = store.entries[key].id;
|
||||
store.idIndex.insert(render.id, key);
|
||||
if (isNew) {
|
||||
const qint64 newBytes = imageBytes(render.image);
|
||||
while (store.order.size() >= kCacheLimit ||
|
||||
store.bytes + newBytes > kMaxCacheBytes) {
|
||||
bool evicted = false;
|
||||
for (int i = 0; i < store.order.size(); ++i) {
|
||||
const QString oldKey = store.order.at(i);
|
||||
const auto oldIt =
|
||||
store.entries.constFind(oldKey);
|
||||
if (oldIt == store.entries.constEnd()) {
|
||||
store.order.removeAt(i);
|
||||
continue;
|
||||
}
|
||||
if (store.idRefs.value(oldIt->id, 0) > 0)
|
||||
continue;
|
||||
store.order.removeAt(i);
|
||||
store.bytes -= imageBytes(oldIt->image);
|
||||
store.idIndex.remove(oldIt->id);
|
||||
store.entries.remove(oldKey);
|
||||
evicted = true;
|
||||
break;
|
||||
}
|
||||
if (!evicted) break;
|
||||
}
|
||||
store.entries.insert(key, render);
|
||||
store.order.append(key);
|
||||
store.bytes += imageBytes(render.image);
|
||||
}
|
||||
}
|
||||
self->m_image = render.image;
|
||||
self->m_imageUrl = render.url;
|
||||
releaseMathId(self->m_imageId);
|
||||
retainMathId(render.id);
|
||||
self->m_imageId = render.id;
|
||||
self->m_width = render.width;
|
||||
self->m_height = render.height;
|
||||
self->m_ok = render.ok;
|
||||
@@ -223,4 +359,8 @@ void LlmMathText::reRender() {
|
||||
});
|
||||
}
|
||||
|
||||
LlmMathText::~LlmMathText() {
|
||||
releaseMathId(m_imageId);
|
||||
}
|
||||
|
||||
} // namespace ZShell::llm
|
||||
|
||||
Reference in New Issue
Block a user