diff --git a/YACReaderLibrary/comic_flow_widget.cpp b/YACReaderLibrary/comic_flow_widget.cpp index b75fdb52..0c2b603e 100644 --- a/YACReaderLibrary/comic_flow_widget.cpp +++ b/YACReaderLibrary/comic_flow_widget.cpp @@ -24,6 +24,7 @@ void ComicFlowWidget::applyTheme(const Theme &theme) { setBackgroundColor(theme.comicFlow.backgroundColor); setTextColor(theme.comicFlow.textColor); + flow->setRibbonImages(theme.comicFlow.readPixmap.toImage(), theme.comicFlow.readingPixmap.toImage()); } void ComicFlowWidget::setBackgroundColor(const QColor &color) diff --git a/YACReaderLibrary/images.qrc b/YACReaderLibrary/images.qrc index 1f898934..b221181e 100644 --- a/YACReaderLibrary/images.qrc +++ b/YACReaderLibrary/images.qrc @@ -78,8 +78,8 @@ ../images/notCover.png ../images/library_dialogs/openLibrary.svg ../images/metadata_dialog/previousCoverPage.svg - ../images/readingRibbon.png - ../images/readRibbon.png + ../images/readingRibbon.svg + ../images/readRibbon.svg ../images/metadata_dialog/resetCover.svg ../images/search_result.svg ../images/serverConfigBackground.svg diff --git a/YACReaderLibrary/themes/builtin_classic.json b/YACReaderLibrary/themes/builtin_classic.json index e3dc64ac..10c301cd 100644 --- a/YACReaderLibrary/themes/builtin_classic.json +++ b/YACReaderLibrary/themes/builtin_classic.json @@ -1,6 +1,9 @@ { "comicFlow": { "backgroundColor": "#000000", + "readMainColor": "#db4725", + "readTickColor": "#8a2c17", + "readingColor": "#e6b90f", "textColor": "#4c4c4c" }, "comicsViewTable": { diff --git a/YACReaderLibrary/themes/builtin_dark.json b/YACReaderLibrary/themes/builtin_dark.json index b625814d..2f5a4f41 100644 --- a/YACReaderLibrary/themes/builtin_dark.json +++ b/YACReaderLibrary/themes/builtin_dark.json @@ -1,6 +1,9 @@ { "comicFlow": { "backgroundColor": "#111111", + "readMainColor": "#db4725", + "readTickColor": "#8a2c17", + "readingColor": "#e6b90f", "textColor": "#888888" }, "comicsViewTable": { diff --git a/YACReaderLibrary/themes/builtin_light.json b/YACReaderLibrary/themes/builtin_light.json index cf6916ea..25d88e86 100644 --- a/YACReaderLibrary/themes/builtin_light.json +++ b/YACReaderLibrary/themes/builtin_light.json @@ -1,6 +1,9 @@ { "comicFlow": { "backgroundColor": "#dcdcdc", + "readMainColor": "#db4725", + "readTickColor": "#8a2c17", + "readingColor": "#e6b90f", "textColor": "#303030" }, "comicsViewTable": { diff --git a/YACReaderLibrary/themes/theme.h b/YACReaderLibrary/themes/theme.h index 9ade45fa..54ba16ad 100644 --- a/YACReaderLibrary/themes/theme.h +++ b/YACReaderLibrary/themes/theme.h @@ -92,9 +92,11 @@ struct MetadataScraperDialogThemeTemplates { QSize rowIconSize = QSize(8, 7); }; -struct ComicFlowColors { +struct ComicFlowTheme { QColor backgroundColor; QColor textColor; + QPixmap readPixmap; + QPixmap readingPixmap; }; struct ComicsViewTableThemeTemplates { @@ -484,7 +486,7 @@ struct Theme { ThemeMeta meta; QJsonObject sourceJson; - ComicFlowColors comicFlow; + ComicFlowTheme comicFlow; MetadataScraperDialogTheme metadataScraperDialog; HelpAboutDialogTheme helpAboutDialog; WhatsNewDialogTheme whatsNewDialog; diff --git a/YACReaderLibrary/themes/theme_factory.cpp b/YACReaderLibrary/themes/theme_factory.cpp index a65359dc..22f21611 100644 --- a/YACReaderLibrary/themes/theme_factory.cpp +++ b/YACReaderLibrary/themes/theme_factory.cpp @@ -330,10 +330,18 @@ struct WhatsNewDialogParams { QColor headerDecorationColor; }; +struct ComicFlowParams { + QColor backgroundColor; + QColor textColor; + QColor readMainColor; // Main ribbon color for read state (#f0f in readRibbon.svg) + QColor readTickColor; // Tick color for read state (#0ff in readRibbon.svg) + QColor readingColor; // Main ribbon color for reading state (#f0f in readingRibbon.svg) +}; + struct ThemeParams { ThemeMeta meta; - ComicFlowColors comicFlowColors; + ComicFlowParams comicFlowParams; MetadataScraperDialogParams metadataScraperDialogParams; HelpAboutDialogTheme helpAboutDialogParams; EmptyContainerParams emptyContainerParams; @@ -361,10 +369,22 @@ Theme makeTheme(const ThemeParams ¶ms) Theme theme; // Comic Flow - const auto &cf = params.comicFlowColors; + const auto &cf = params.comicFlowParams; theme.comicFlow.backgroundColor = cf.backgroundColor; theme.comicFlow.textColor = cf.textColor; + { + const qreal dpr = qApp->devicePixelRatio(); + // readRibbon: #f0f (main) + #0ff (tick) + theme.comicFlow.readPixmap = renderSvgToPixmap( + recoloredSvgToThemeFile(":/images/readRibbon.svg", cf.readMainColor, cf.readTickColor, params.meta.id), + 100, 136, dpr); + // readingRibbon: #f0f (main) + theme.comicFlow.readingPixmap = renderSvgToPixmap( + recoloredSvgToThemeFile(":/images/readingRibbon.svg", cf.readingColor, params.meta.id), + 100, 136, dpr); + } + // MetadataScraperDialog const auto &msd = params.metadataScraperDialogParams; const auto &t = msd.t; @@ -918,8 +938,11 @@ Theme makeTheme(const QJsonObject &json) if (json.contains("comicFlow")) { const auto o = json["comicFlow"].toObject(); - p.comicFlowColors.backgroundColor = colorFromJson(o, "backgroundColor", p.comicFlowColors.backgroundColor); - p.comicFlowColors.textColor = colorFromJson(o, "textColor", p.comicFlowColors.textColor); + p.comicFlowParams.backgroundColor = colorFromJson(o, "backgroundColor", p.comicFlowParams.backgroundColor); + p.comicFlowParams.textColor = colorFromJson(o, "textColor", p.comicFlowParams.textColor); + p.comicFlowParams.readMainColor = colorFromJson(o, "readMainColor", p.comicFlowParams.readMainColor); + p.comicFlowParams.readTickColor = colorFromJson(o, "readTickColor", p.comicFlowParams.readTickColor); + p.comicFlowParams.readingColor = colorFromJson(o, "readingColor", p.comicFlowParams.readingColor); } if (json.contains("metadataScraperDialog")) { diff --git a/common/rhi/yacreader_flow_rhi.cpp b/common/rhi/yacreader_flow_rhi.cpp index 5d1a7405..7db94666 100644 --- a/common/rhi/yacreader_flow_rhi.cpp +++ b/common/rhi/yacreader_flow_rhi.cpp @@ -138,28 +138,8 @@ void YACReaderFlow3D::initialize(QRhiCommandBuffer *cb) qDebug() << "YACReaderFlow3D: Created defaultTexture" << defaultImage.size(); } -#ifdef YACREADER_LIBRARY - // Initialize mark textures - if (!scene.markTexture) { - QImage markImage = QImage(":/images/readRibbon.png").convertToFormat(QImage::Format_RGBA8888); - if (!markImage.isNull()) { - scene.markTexture.reset(m_rhi->newTexture(QRhiTexture::RGBA8, markImage.size(), 1, QRhiTexture::MipMapped | QRhiTexture::UsedWithGenerateMips)); - scene.markTexture->create(); - getResourceBatch()->uploadTexture(scene.markTexture.get(), markImage); - getResourceBatch()->generateMips(scene.markTexture.get()); - } - } - - if (!scene.readingTexture) { - QImage readingImage = QImage(":/images/readingRibbon.png").convertToFormat(QImage::Format_RGBA8888); - if (!readingImage.isNull()) { - scene.readingTexture.reset(m_rhi->newTexture(QRhiTexture::RGBA8, readingImage.size(), 1, QRhiTexture::MipMapped | QRhiTexture::UsedWithGenerateMips)); - scene.readingTexture->create(); - getResourceBatch()->uploadTexture(scene.readingTexture.get(), readingImage); - getResourceBatch()->generateMips(scene.readingTexture.get()); - } - } -#endif + if (ribbonTexturesDirty || (!readRibbonImage.isNull() && !scene.markTexture) || (!readingRibbonImage.isNull() && !scene.readingTexture)) + syncRibbonTextures(getResourceBatch()); // Create vertex buffer (quad geometry) if (!scene.vertexBuffer) { @@ -373,9 +353,35 @@ void YACReaderFlow3D::ensurePipeline() void YACReaderFlow3D::render(QRhiCommandBuffer *cb) { - if (!m_rhi || numObjects == 0) + if (!m_rhi) return; + QRhiResourceUpdateBatch *batch = scene.resourceUpdates; + scene.resourceUpdates = nullptr; + + auto ensureBatch = [this, &batch]() { + if (!batch) + batch = m_rhi->nextResourceUpdateBatch(); + return batch; + }; + auto deferBatch = [this, &batch]() { + if (batch) + scene.resourceUpdates = batch; + }; + +#ifdef YACREADER_LIBRARY + if (ribbonTexturesDirty || (!readRibbonImage.isNull() && !scene.markTexture) || (!readingRibbonImage.isNull() && !scene.readingTexture)) + syncRibbonTextures(ensureBatch()); +#endif + + // Even without draw calls, pending uploads still have to reach the GPU. + // Otherwise recreated ribbon textures would exist but never receive pixels. + if (numObjects == 0) { + if (batch) + cb->resourceUpdate(batch); + return; + } + const QSize outputSize = renderTarget()->pixelSize(); const QColor clearColor = backgroundColor; @@ -552,6 +558,7 @@ void YACReaderFlow3D::render(QRhiCommandBuffer *cb) ensureUniformBufferCapacity(draws.size()); if (!scene.uniformBuffer) { + deferBatch(); qWarning() << "YACReaderFlow3D: No uniform buffer available for rendering"; return; } @@ -560,6 +567,7 @@ void YACReaderFlow3D::render(QRhiCommandBuffer *cb) ensurePipeline(); if (!scene.pipeline) { + deferBatch(); qWarning() << "YACReaderFlow3D: No pipeline available for rendering"; return; } @@ -569,6 +577,7 @@ void YACReaderFlow3D::render(QRhiCommandBuffer *cb) if (!scene.instanceBuffer || scene.instanceBuffer->size() < requiredInstanceSize) { scene.instanceBuffer.reset(m_rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::VertexBuffer, requiredInstanceSize)); if (!scene.instanceBuffer->create()) { + deferBatch(); qWarning() << "YACReaderFlow3D: Failed to create instance buffer of size" << requiredInstanceSize; return; } @@ -576,7 +585,7 @@ void YACReaderFlow3D::render(QRhiCommandBuffer *cb) // === PHASE 1: PREPARE (BEFORE PASS) === // Update ALL uniform and instance data for ALL draws in one batch - QRhiResourceUpdateBatch *batch = m_rhi->nextResourceUpdateBatch(); + batch = ensureBatch(); // Process pending texture uploads if (!pendingTextureUploads.isEmpty()) { @@ -603,6 +612,7 @@ void YACReaderFlow3D::render(QRhiCommandBuffer *cb) // === PHASE 2: RENDER (DURING PASS) === cb->beginPass(renderTarget(), clearColor, { 1.0f, 0 }, batch); + batch = nullptr; cb->setGraphicsPipeline(scene.pipeline.get()); cb->setViewport(QRhiViewport(0, 0, outputSize.width(), outputSize.height())); @@ -762,6 +772,65 @@ void YACReaderFlow3D::executeDrawWithOffset(QRhiCommandBuffer *cb, QRhiTexture * cb->draw(6); } +void YACReaderFlow3D::removeCachedShaderBindings(QRhiTexture *texture) +{ + if (!texture) + return; + + auto it = scene.shaderBindingsCache.find(texture); + if (it != scene.shaderBindingsCache.end()) { + delete it.value(); + scene.shaderBindingsCache.erase(it); + } +} + +void YACReaderFlow3D::syncRibbonTextures(QRhiResourceUpdateBatch *batch) +{ +#ifndef YACREADER_LIBRARY + Q_UNUSED(batch); + ribbonTexturesDirty = false; +#else + if (!m_rhi || !batch) + return; + + bool allTexturesReady = true; + + auto syncTexture = [this, batch, &allTexturesReady](std::unique_ptr &texture, const QImage &image, const char *name) { + if (image.isNull()) { + if (texture) { + removeCachedShaderBindings(texture.get()); + texture.reset(); + } + return; + } + + if (texture && !ribbonTexturesDirty) + return; + + if (texture) { + removeCachedShaderBindings(texture.get()); + texture.reset(); + } + + std::unique_ptr newTexture(m_rhi->newTexture(QRhiTexture::RGBA8, image.size(), 1, QRhiTexture::MipMapped | QRhiTexture::UsedWithGenerateMips)); + if (!newTexture->create()) { + qWarning() << "YACReaderFlow3D: Failed to create" << name << "ribbon texture"; + allTexturesReady = false; + return; + } + + batch->uploadTexture(newTexture.get(), image); + batch->generateMips(newTexture.get()); + texture = std::move(newTexture); + }; + + syncTexture(scene.markTexture, readRibbonImage, "read"); + syncTexture(scene.readingTexture, readingRibbonImage, "reading"); + + ribbonTexturesDirty = !allTexturesReady; +#endif +} + void YACReaderFlow3D::releaseResources() { scene.reset(); @@ -1278,6 +1347,19 @@ void YACReaderFlow3D::setTextColor(const QColor &color) update(); } +void YACReaderFlow3D::setRibbonImages(const QImage &readImage, const QImage &readingImage) +{ + readRibbonImage = readImage.convertToFormat(QImage::Format_RGBA8888); + readingRibbonImage = readingImage.convertToFormat(QImage::Format_RGBA8888); +#ifdef YACREADER_LIBRARY + ribbonTexturesDirty = true; +#else + ribbonTexturesDirty = false; +#endif + + update(); +} + // Event handlers void YACReaderFlow3D::wheelEvent(QWheelEvent *event) { diff --git a/common/rhi/yacreader_flow_rhi.h b/common/rhi/yacreader_flow_rhi.h index ca7184dd..a1062bbb 100644 --- a/common/rhi/yacreader_flow_rhi.h +++ b/common/rhi/yacreader_flow_rhi.h @@ -179,6 +179,10 @@ protected: QColor backgroundColor; QColor textColor; + QImage readRibbonImage; + QImage readingRibbonImage; + bool ribbonTexturesDirty = false; + /*** System info ***/ float viewRotate; @@ -202,6 +206,8 @@ protected: // Helper methods QRhiTexture *createTextureFromImage(QRhiCommandBuffer *cb, const QImage &image); + void removeCachedShaderBindings(QRhiTexture *texture); + void syncRibbonTextures(QRhiResourceUpdateBatch *batch); void updateUniformBuffer(QRhiCommandBuffer *cb, const UniformData &data); void prepareMarkInstanceData(const YACReader3DImageRHI &image, QVector &data); void ensureUniformBufferCapacity(int requiredSlots); @@ -260,6 +266,9 @@ public slots: void setBackgroundColor(const QColor &color); void setTextColor(const QColor &color); + // Ribbon image setters (for themed SVG rasterized images) + void setRibbonImages(const QImage &readImage, const QImage &readingImage); + virtual void updateImageData() = 0; void reset(); diff --git a/images/readRibbon.png b/images/readRibbon.png deleted file mode 100644 index 4dcd32e2..00000000 Binary files a/images/readRibbon.png and /dev/null differ diff --git a/images/readRibbon.svg b/images/readRibbon.svg new file mode 100644 index 00000000..7eced57e --- /dev/null +++ b/images/readRibbon.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/images/readingRibbon.png b/images/readingRibbon.png deleted file mode 100644 index 476912ef..00000000 Binary files a/images/readingRibbon.png and /dev/null differ diff --git a/images/readingRibbon.svg b/images/readingRibbon.svg new file mode 100644 index 00000000..1cd07eaa --- /dev/null +++ b/images/readingRibbon.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + \ No newline at end of file