From 0bd291ba98dcbd2cdac35b211fa68db1af1fcc44 Mon Sep 17 00:00:00 2001 From: luisangelsm Date: Thu, 26 Feb 2026 16:19:58 +0100 Subject: [PATCH] Extract the layout logic to a view model to simplify the update logic in continuous scroll mode --- YACReader/CMakeLists.txt | 2 + YACReader/continuous_page_widget.cpp | 326 ++++----------------------- YACReader/continuous_page_widget.h | 52 +---- YACReader/continuous_view_model.cpp | 299 ++++++++++++++++++++++++ YACReader/continuous_view_model.h | 79 +++++++ YACReader/viewer.cpp | 115 ++++++---- YACReader/viewer.h | 8 +- 7 files changed, 506 insertions(+), 375 deletions(-) create mode 100644 YACReader/continuous_view_model.cpp create mode 100644 YACReader/continuous_view_model.h diff --git a/YACReader/CMakeLists.txt b/YACReader/CMakeLists.txt index 3f4010cd..51c55b61 100644 --- a/YACReader/CMakeLists.txt +++ b/YACReader/CMakeLists.txt @@ -11,6 +11,8 @@ qt_add_executable(YACReader WIN32 main_window_viewer.cpp continuous_page_widget.h continuous_page_widget.cpp + continuous_view_model.h + continuous_view_model.cpp mouse_handler.h mouse_handler.cpp viewer.h diff --git a/YACReader/continuous_page_widget.cpp b/YACReader/continuous_page_widget.cpp index 59a148ae..0f825cfb 100644 --- a/YACReader/continuous_page_widget.cpp +++ b/YACReader/continuous_page_widget.cpp @@ -1,10 +1,9 @@ #include "continuous_page_widget.h" +#include "continuous_view_model.h" #include "render.h" #include #include -#include -#include ContinuousPageWidget::ContinuousPageWidget(QWidget *parent) : QWidget(parent) @@ -26,80 +25,32 @@ void ContinuousPageWidget::setRender(Render *r) render = r; } -void ContinuousPageWidget::setNumPages(int count) +void ContinuousPageWidget::setViewModel(ContinuousViewModel *viewModel) { - numPages = count; - defaultPageSize = QSize(800, 1200); - pageSizes.fill(QSize(0, 0), count); - relayout(false); -} - -void ContinuousPageWidget::setZoomFactor(int zoom) -{ - if (zoomFactor == zoom) { - return; - } - zoomFactor = zoom; - relayout(true); - update(); -} - -void ContinuousPageWidget::probeBufferedPages() -{ - if (!render || numPages == 0) { + if (continuousViewModel == viewModel) { return; } - bool changed = false; - for (int i = 0; i < numPages; ++i) { - const QImage *img = render->bufferedImage(i); - bool hasKnownSize = pageSizes[i].width() > 0 && pageSizes[i].height() > 0; - if (img && !img->isNull() && !hasKnownSize) { - pageSizes[i] = img->size(); - if (defaultPageSize == QSize(800, 1200)) { - defaultPageSize = img->size(); - } - changed = true; - } + if (continuousViewModel) { + disconnect(continuousViewModel, &ContinuousViewModel::stateChanged, this, QOverload<>::of(&ContinuousPageWidget::update)); } - if (changed) { - relayout(true); - update(); - } -} + continuousViewModel = viewModel; + + if (continuousViewModel) { + connect(continuousViewModel, &ContinuousViewModel::stateChanged, this, QOverload<>::of(&ContinuousPageWidget::update)); + } -void ContinuousPageWidget::reset() -{ - numPages = 0; - pageSizes.clear(); - yPositions.clear(); - currentTotalHeight = 0; - layoutSnapshot = LayoutSnapshot(); - defaultPageSize = QSize(800, 1200); - setMinimumHeight(0); - setMaximumHeight(QWIDGETSIZE_MAX); updateGeometry(); update(); } -int ContinuousPageWidget::centerPage(int scrollY, int viewportHeight) const +void ContinuousPageWidget::reset() { - const int centerY = scrollY + std::max(0, viewportHeight / 2); - return pageAtY(centerY); -} - -int ContinuousPageWidget::yPositionForPage(int pageIndex) const -{ - if (pageIndex < 0 || pageIndex >= yPositions.size()) { - return 0; - } - return yPositions[pageIndex]; -} - -int ContinuousPageWidget::totalHeight() const -{ - return currentTotalHeight; + setMinimumHeight(0); + setMaximumHeight(QWIDGETSIZE_MAX); + updateGeometry(); + update(); } bool ContinuousPageWidget::hasHeightForWidth() const @@ -109,26 +60,24 @@ bool ContinuousPageWidget::hasHeightForWidth() const int ContinuousPageWidget::heightForWidth(int w) const { - if (numPages == 0 || w <= 0) { + if (!continuousViewModel || w <= 0) { return 0; } - - int h = 0; - for (int i = 0; i < numPages; ++i) { - QSize scaled = scaledPageSize(i, w); - h += scaled.height(); - } - return h; + Q_UNUSED(w) + return continuousViewModel->totalHeight(); } QSize ContinuousPageWidget::sizeHint() const { - return QSize(defaultPageSize.width(), currentTotalHeight > 0 ? currentTotalHeight : 0); + if (!continuousViewModel) { + return QSize(800, 0); + } + return QSize(width(), continuousViewModel->totalHeight()); } void ContinuousPageWidget::onPageAvailable(int absolutePageIndex) { - if (!render || absolutePageIndex < 0 || absolutePageIndex >= numPages) { + if (!render || !continuousViewModel || absolutePageIndex < 0 || absolutePageIndex >= continuousViewModel->numPages()) { return; } @@ -137,45 +86,35 @@ void ContinuousPageWidget::onPageAvailable(int absolutePageIndex) return; } - QSize naturalSize = img->size(); - - // update default page size from the first real page we see - if (defaultPageSize == QSize(800, 1200) && !naturalSize.isNull()) { - defaultPageSize = naturalSize; - } - - bool sizeChanged = (pageSizes[absolutePageIndex] != naturalSize); - pageSizes[absolutePageIndex] = naturalSize; - - if (sizeChanged) { - // keep anchor page visually stable while refined page sizes arrive - relayout(true); - } - // repaint the region where this page lives - if (absolutePageIndex < yPositions.size()) { - QSize scaled = scaledPageSize(absolutePageIndex, width()); - QRect pageRect(0, yPositions[absolutePageIndex], scaled.width(), scaled.height()); + if (absolutePageIndex < continuousViewModel->numPages()) { + QSize scaled = continuousViewModel->scaledPageSize(absolutePageIndex); + const int y = continuousViewModel->yPositionForPage(absolutePageIndex); + int x = (width() - scaled.width()) / 2; + if (x < 0) { + x = 0; + } + QRect pageRect(x, y, scaled.width(), scaled.height()); update(pageRect); } } void ContinuousPageWidget::paintEvent(QPaintEvent *event) { - if (numPages == 0 || !render) { + if (!continuousViewModel || continuousViewModel->numPages() == 0 || !render) { return; } QPainter painter(this); QRect visibleRect = event->rect(); - int firstPage = pageAtY(visibleRect.top()); - int lastPage = pageAtY(visibleRect.bottom()); + int firstPage = continuousViewModel->pageAtY(visibleRect.top()); + int lastPage = continuousViewModel->pageAtY(visibleRect.bottom()); int w = width(); - for (int i = firstPage; i <= lastPage && i < numPages; ++i) { - int y = yPositions[i]; - QSize scaled = scaledPageSize(i, w); + for (int i = firstPage; i <= lastPage && i < continuousViewModel->numPages(); ++i) { + int y = continuousViewModel->yPositionForPage(i); + QSize scaled = continuousViewModel->scaledPageSize(i); // center horizontally if page is narrower than widget int x = (w - scaled.width()) / 2; if (x < 0) { @@ -202,192 +141,7 @@ void ContinuousPageWidget::paintEvent(QPaintEvent *event) void ContinuousPageWidget::resizeEvent(QResizeEvent *event) { QWidget::resizeEvent(event); - relayout(true); -} - -void ContinuousPageWidget::setAnchorPage(int page) -{ - anchorPage = page; -} - -void ContinuousPageWidget::setViewportState(int scrollY, int viewportHeight) -{ - viewportScrollY = std::max(0, scrollY); - currentViewportHeight = std::max(0, viewportHeight); - hasViewportState = true; -} - -void ContinuousPageWidget::updateLayout() -{ - relayout(false); -} - -void ContinuousPageWidget::updateLayoutWithAnchor() -{ - relayout(true); -} - -void ContinuousPageWidget::relayout(bool preserveAnchor) -{ - int w = width(); - if (w <= 0) { - w = parentWidget() ? parentWidget()->width() : 0; - } - if (w <= 0) { - w = defaultPageSize.width(); - } - - const LayoutSnapshot oldSnapshot = layoutSnapshot; - - ViewportAnchor anchor; - if (preserveAnchor && hasViewportState && !oldSnapshot.yPositions.isEmpty()) { - anchor = anchorFromViewport(oldSnapshot, viewportScrollY, currentViewportHeight); - } else if (preserveAnchor && anchorPage >= 0) { - anchor.pageIndex = anchorPage; - anchor.offsetRatio = 0.5f; - anchor.valid = true; - } - - layoutSnapshot = buildLayoutSnapshot(w); - -#ifndef NDEBUG - Q_ASSERT(layoutSnapshot.yPositions.size() == numPages); - Q_ASSERT(layoutSnapshot.scaledSizes.size() == numPages); - for (int i = 0; i < layoutSnapshot.scaledSizes.size(); ++i) { - Q_ASSERT(layoutSnapshot.scaledSizes[i].width() > 0); - Q_ASSERT(layoutSnapshot.scaledSizes[i].height() > 0); - if (i > 0) { - Q_ASSERT(layoutSnapshot.yPositions[i] >= layoutSnapshot.yPositions[i - 1]); - } - } -#endif - - yPositions = layoutSnapshot.yPositions; - currentTotalHeight = layoutSnapshot.totalHeight; - - setFixedHeight(currentTotalHeight); - updateGeometry(); - - if (!preserveAnchor || !anchor.valid || currentViewportHeight <= 0) { - return; - } - - const int newScrollForAnchor = resolveAnchorToScrollY(layoutSnapshot, anchor, currentViewportHeight); - emit layoutScrollPositionRequested(newScrollForAnchor); -} - -ContinuousPageWidget::LayoutSnapshot ContinuousPageWidget::buildLayoutSnapshot(int w) const -{ - LayoutSnapshot snapshot; - - if (numPages <= 0 || w <= 0) { - return snapshot; - } - - snapshot.yPositions.resize(numPages); - snapshot.scaledSizes.resize(numPages); - - qint64 y = 0; - for (int i = 0; i < numPages; ++i) { - snapshot.yPositions[i] = static_cast(std::min(y, std::numeric_limits::max())); - QSize scaled = scaledPageSize(i, w); - scaled.setWidth(std::max(1, scaled.width())); - scaled.setHeight(std::max(1, scaled.height())); - snapshot.scaledSizes[i] = scaled; - y += scaled.height(); - } - - snapshot.totalHeight = static_cast(std::min(y, static_cast(QWIDGETSIZE_MAX))); - return snapshot; -} - -int ContinuousPageWidget::pageAtY(const LayoutSnapshot &snapshot, int y) const -{ - if (snapshot.yPositions.isEmpty()) { - return 0; - } - - auto it = std::upper_bound(snapshot.yPositions.constBegin(), snapshot.yPositions.constEnd(), y); - if (it == snapshot.yPositions.constBegin()) { - return 0; - } - --it; - return static_cast(it - snapshot.yPositions.constBegin()); -} - -ContinuousPageWidget::ViewportAnchor ContinuousPageWidget::anchorFromViewport(const LayoutSnapshot &snapshot, int scrollY, int viewportHeight) const -{ - ViewportAnchor anchor; - - if (snapshot.yPositions.isEmpty() || viewportHeight <= 0) { - return anchor; - } - - const int maxScroll = std::max(0, snapshot.totalHeight - viewportHeight); - const int clampedScroll = qBound(0, scrollY, maxScroll); - const int anchorY = clampedScroll + viewportHeight / 2; - const int page = pageAtY(snapshot, anchorY); - - if (page < 0 || page >= snapshot.scaledSizes.size()) { - return anchor; - } - - const int pageTop = snapshot.yPositions[page]; - const int pageHeight = std::max(1, snapshot.scaledSizes[page].height()); - const float ratio = static_cast(anchorY - pageTop) / static_cast(pageHeight); - - anchor.pageIndex = page; - anchor.offsetRatio = qBound(0.0f, ratio, 1.0f); - anchor.valid = true; - return anchor; -} - -int ContinuousPageWidget::resolveAnchorToScrollY(const LayoutSnapshot &snapshot, const ViewportAnchor &anchor, int viewportHeight) const -{ - if (!anchor.valid || viewportHeight <= 0 || snapshot.yPositions.isEmpty()) { - return 0; - } - - if (anchor.pageIndex < 0 || anchor.pageIndex >= snapshot.yPositions.size() || anchor.pageIndex >= snapshot.scaledSizes.size()) { - return 0; - } - - const int pageTop = snapshot.yPositions[anchor.pageIndex]; - const int pageHeight = std::max(1, snapshot.scaledSizes[anchor.pageIndex].height()); - const int anchorY = pageTop + qRound(anchor.offsetRatio * pageHeight); - const int maxScroll = std::max(0, snapshot.totalHeight - viewportHeight); - const int target = anchorY - viewportHeight / 2; - return qBound(0, target, maxScroll); -} - -int ContinuousPageWidget::pageAtY(int y) const -{ - return pageAtY(layoutSnapshot, y); -} - -QSize ContinuousPageWidget::scaledPageSize(int pageIndex, int forWidth) const -{ - QSize natural = (pageIndex < pageSizes.size() && pageSizes[pageIndex].width() > 0 && pageSizes[pageIndex].height() > 0) - ? pageSizes[pageIndex] - : defaultPageSize; - - float scale = scaleForPage(pageIndex, forWidth); - int scaledW = std::max(1, qRound(natural.width() * scale)); - int scaledH = std::max(1, qRound(natural.height() * scale)); - return QSize(scaledW, scaledH); -} - -float ContinuousPageWidget::scaleForPage(int pageIndex, int forWidth) const -{ - QSize natural = (pageIndex < pageSizes.size() && pageSizes[pageIndex].width() > 0 && pageSizes[pageIndex].height() > 0) - ? pageSizes[pageIndex] - : defaultPageSize; - - if (natural.width() <= 0 || forWidth <= 0) { - return 1.0f; - } - - float baseScale = static_cast(forWidth) / natural.width(); - float zoomMultiplier = zoomFactor / 100.0f; - return baseScale * zoomMultiplier; + if (continuousViewModel) { + continuousViewModel->setViewportSize(width(), continuousViewModel->viewportHeight()); + } } diff --git a/YACReader/continuous_page_widget.h b/YACReader/continuous_page_widget.h index 55393a5b..d40d4bd6 100644 --- a/YACReader/continuous_page_widget.h +++ b/YACReader/continuous_page_widget.h @@ -8,6 +8,7 @@ #include "themable.h" class Render; +class ContinuousViewModel; class ContinuousPageWidget : public QWidget, protected Themable { @@ -16,27 +17,13 @@ public: explicit ContinuousPageWidget(QWidget *parent = nullptr); void setRender(Render *r); - void setNumPages(int count); - void setZoomFactor(int zoom); - void probeBufferedPages(); + void setViewModel(ContinuousViewModel *viewModel); void reset(); - int centerPage(int scrollY, int viewportHeight) const; - int yPositionForPage(int pageIndex) const; - int totalHeight() const; - bool hasHeightForWidth() const override; int heightForWidth(int w) const override; QSize sizeHint() const override; - void setAnchorPage(int page); - void setViewportState(int scrollY, int viewportHeight); - -signals: - // emitted after layout recomputation when the preserved viewport anchor - // resolves to an absolute scroll position - void layoutScrollPositionRequested(int scrollY); - public slots: void onPageAvailable(int absolutePageIndex); @@ -46,41 +33,8 @@ protected: void applyTheme(const Theme &theme) override; private: - struct LayoutSnapshot { - QVector yPositions; - QVector scaledSizes; - int totalHeight = 0; - }; - - struct ViewportAnchor { - int pageIndex = -1; - float offsetRatio = 0.0f; - bool valid = false; - }; - - void updateLayout(); - void updateLayoutWithAnchor(); - void relayout(bool preserveAnchor); - LayoutSnapshot buildLayoutSnapshot(int w) const; - int pageAtY(const LayoutSnapshot &snapshot, int y) const; - ViewportAnchor anchorFromViewport(const LayoutSnapshot &snapshot, int scrollY, int viewportHeight) const; - int resolveAnchorToScrollY(const LayoutSnapshot &snapshot, const ViewportAnchor &anchor, int viewportHeight) const; - int pageAtY(int y) const; - QSize scaledPageSize(int pageIndex, int forWidth) const; - float scaleForPage(int pageIndex, int forWidth) const; - Render *render = nullptr; - int numPages = 0; - QVector pageSizes; - QVector yPositions; - int currentTotalHeight = 0; - LayoutSnapshot layoutSnapshot; - QSize defaultPageSize { 800, 1200 }; - int zoomFactor = 100; - int anchorPage = -1; - int viewportScrollY = 0; - int currentViewportHeight = 0; - bool hasViewportState = false; + ContinuousViewModel *continuousViewModel = nullptr; }; #endif // CONTINUOUS_PAGE_WIDGET_H diff --git a/YACReader/continuous_view_model.cpp b/YACReader/continuous_view_model.cpp new file mode 100644 index 00000000..8d88443d --- /dev/null +++ b/YACReader/continuous_view_model.cpp @@ -0,0 +1,299 @@ +#include "continuous_view_model.h" + +#include +#include + +#include +#include + +ContinuousViewModel::ContinuousViewModel(QObject *parent) + : QObject(parent) +{ +} + +void ContinuousViewModel::reset() +{ + numPagesValue = 0; + pageSizes.clear(); + defaultPageSize = QSize(800, 1200); + scrollYValue = 0; + anchorPage = -1; + layoutSnapshot = LayoutSnapshot(); + emit stateChanged(); +} + +void ContinuousViewModel::setNumPages(int count) +{ + numPagesValue = std::max(0, count); + pageSizes.fill(QSize(0, 0), numPagesValue); + defaultPageSize = QSize(800, 1200); + recompute(RecomputePolicy::PreserveScrollClamped); +} + +void ContinuousViewModel::setZoomFactor(int zoom) +{ + if (zoomFactorValue == zoom) { + return; + } + + zoomFactorValue = zoom; + recompute(RecomputePolicy::PreserveViewportAnchor); +} + +void ContinuousViewModel::setViewportSize(int width, int height) +{ + width = std::max(0, width); + height = std::max(0, height); + + if (viewportWidth == width && viewportHeightValue == height) { + return; + } + + viewportWidth = width; + viewportHeightValue = height; + recompute(RecomputePolicy::PreserveViewportAnchor); +} + +void ContinuousViewModel::setScrollYFromUser(int scrollY) +{ + scrollYValue = std::max(0, scrollY); + recompute(RecomputePolicy::PreserveScrollClamped); +} + +void ContinuousViewModel::setAnchorPage(int page) +{ + if (page < 0 || page >= numPagesValue) { + return; + } + + anchorPage = page; +} + +void ContinuousViewModel::setCurrentPage(int page) +{ + if (page < 0 || page >= numPagesValue) { + return; + } + + anchorPage = page; + recompute(RecomputePolicy::ScrollToPageTop, page); +} + +void ContinuousViewModel::setPageNaturalSize(int pageIndex, const QSize &size) +{ + if (pageIndex < 0 || pageIndex >= numPagesValue || size.isEmpty()) { + return; + } + + if (defaultPageSize == QSize(800, 1200)) { + defaultPageSize = size; + } + + if (pageSizes[pageIndex] == size) { + return; + } + + pageSizes[pageIndex] = size; + recompute(RecomputePolicy::PreserveViewportAnchor); +} + +int ContinuousViewModel::numPages() const +{ + return numPagesValue; +} + +int ContinuousViewModel::totalHeight() const +{ + return layoutSnapshot.totalHeight; +} + +int ContinuousViewModel::scrollY() const +{ + return scrollYValue; +} + +int ContinuousViewModel::viewportHeight() const +{ + return viewportHeightValue; +} + +int ContinuousViewModel::zoomFactor() const +{ + return zoomFactorValue; +} + +int ContinuousViewModel::centerPage() const +{ + const int centerY = scrollYValue + std::max(0, viewportHeightValue / 2); + return pageAtY(centerY); +} + +int ContinuousViewModel::yPositionForPage(int pageIndex) const +{ + if (pageIndex < 0 || pageIndex >= layoutSnapshot.yPositions.size()) { + return 0; + } + + return layoutSnapshot.yPositions[pageIndex]; +} + +int ContinuousViewModel::pageAtY(int y) const +{ + return pageAtY(layoutSnapshot, y); +} + +QSize ContinuousViewModel::scaledPageSize(int pageIndex) const +{ + if (pageIndex < 0 || pageIndex >= layoutSnapshot.scaledSizes.size()) { + return QSize(); + } + + return layoutSnapshot.scaledSizes[pageIndex]; +} + +void ContinuousViewModel::recompute(RecomputePolicy policy, int targetPage) +{ + const LayoutSnapshot oldSnapshot = layoutSnapshot; + + const int effectiveWidth = viewportWidth > 0 ? viewportWidth : defaultPageSize.width(); + + ViewportAnchor anchor; + if (policy == RecomputePolicy::PreserveViewportAnchor) { + if (!oldSnapshot.yPositions.isEmpty() && viewportHeightValue > 0) { + anchor = anchorFromViewport(oldSnapshot, scrollYValue, viewportHeightValue); + } else if (anchorPage >= 0) { + anchor.pageIndex = anchorPage; + anchor.offsetRatio = 0.5f; + anchor.valid = true; + } + } + + layoutSnapshot = buildLayoutSnapshot(effectiveWidth); + + if (policy == RecomputePolicy::ScrollToPageTop) { + scrollYValue = yPositionForPage(targetPage); + } else if (policy == RecomputePolicy::PreserveViewportAnchor && anchor.valid && viewportHeightValue > 0) { + scrollYValue = resolveAnchorToScrollY(layoutSnapshot, anchor, viewportHeightValue); + } + + scrollYValue = qBound(0, scrollYValue, maxScrollFor(layoutSnapshot)); + + emit stateChanged(); +} + +ContinuousViewModel::LayoutSnapshot ContinuousViewModel::buildLayoutSnapshot(int width) const +{ + LayoutSnapshot snapshot; + + if (numPagesValue <= 0 || width <= 0) { + return snapshot; + } + + snapshot.yPositions.resize(numPagesValue); + snapshot.scaledSizes.resize(numPagesValue); + + qint64 y = 0; + for (int i = 0; i < numPagesValue; ++i) { + snapshot.yPositions[i] = static_cast(std::min(y, std::numeric_limits::max())); + QSize scaled = scaledPageSizeForWidth(i, width); + scaled.setWidth(std::max(1, scaled.width())); + scaled.setHeight(std::max(1, scaled.height())); + snapshot.scaledSizes[i] = scaled; + y += scaled.height(); + } + + snapshot.totalHeight = static_cast(std::min(y, static_cast(QWIDGETSIZE_MAX))); + return snapshot; +} + +ContinuousViewModel::ViewportAnchor ContinuousViewModel::anchorFromViewport(const LayoutSnapshot &snapshot, int scrollY, int viewportHeight) const +{ + ViewportAnchor anchor; + + if (snapshot.yPositions.isEmpty() || viewportHeight <= 0) { + return anchor; + } + + const int maxScroll = std::max(0, snapshot.totalHeight - viewportHeight); + const int clampedScroll = qBound(0, scrollY, maxScroll); + const int anchorY = clampedScroll + viewportHeight / 2; + const int page = pageAtY(snapshot, anchorY); + + if (page < 0 || page >= snapshot.scaledSizes.size()) { + return anchor; + } + + const int pageTop = snapshot.yPositions[page]; + const int pageHeight = std::max(1, snapshot.scaledSizes[page].height()); + const float ratio = static_cast(anchorY - pageTop) / static_cast(pageHeight); + + anchor.pageIndex = page; + anchor.offsetRatio = qBound(0.0f, ratio, 1.0f); + anchor.valid = true; + return anchor; +} + +int ContinuousViewModel::resolveAnchorToScrollY(const LayoutSnapshot &snapshot, const ViewportAnchor &anchor, int viewportHeight) const +{ + if (!anchor.valid || viewportHeight <= 0 || snapshot.yPositions.isEmpty()) { + return 0; + } + + if (anchor.pageIndex < 0 || anchor.pageIndex >= snapshot.yPositions.size() || anchor.pageIndex >= snapshot.scaledSizes.size()) { + return 0; + } + + const int pageTop = snapshot.yPositions[anchor.pageIndex]; + const int pageHeight = std::max(1, snapshot.scaledSizes[anchor.pageIndex].height()); + const int anchorY = pageTop + qRound(anchor.offsetRatio * pageHeight); + const int maxScroll = std::max(0, snapshot.totalHeight - viewportHeight); + const int target = anchorY - viewportHeight / 2; + return qBound(0, target, maxScroll); +} + +int ContinuousViewModel::pageAtY(const LayoutSnapshot &snapshot, int y) const +{ + if (snapshot.yPositions.isEmpty()) { + return 0; + } + + auto it = std::upper_bound(snapshot.yPositions.constBegin(), snapshot.yPositions.constEnd(), y); + if (it == snapshot.yPositions.constBegin()) { + return 0; + } + + --it; + return static_cast(it - snapshot.yPositions.constBegin()); +} + +int ContinuousViewModel::maxScrollFor(const LayoutSnapshot &snapshot) const +{ + return std::max(0, snapshot.totalHeight - viewportHeightValue); +} + +QSize ContinuousViewModel::scaledPageSizeForWidth(int pageIndex, int width) const +{ + QSize natural = (pageIndex < pageSizes.size() && pageSizes[pageIndex].width() > 0 && pageSizes[pageIndex].height() > 0) + ? pageSizes[pageIndex] + : defaultPageSize; + + const float scale = scaleForPage(pageIndex, width); + const int scaledW = std::max(1, qRound(natural.width() * scale)); + const int scaledH = std::max(1, qRound(natural.height() * scale)); + return QSize(scaledW, scaledH); +} + +float ContinuousViewModel::scaleForPage(int pageIndex, int width) const +{ + QSize natural = (pageIndex < pageSizes.size() && pageSizes[pageIndex].width() > 0 && pageSizes[pageIndex].height() > 0) + ? pageSizes[pageIndex] + : defaultPageSize; + + if (natural.width() <= 0 || width <= 0) { + return 1.0f; + } + + const float baseScale = static_cast(width) / natural.width(); + const float zoomMultiplier = zoomFactorValue / 100.0f; + return baseScale * zoomMultiplier; +} diff --git a/YACReader/continuous_view_model.h b/YACReader/continuous_view_model.h new file mode 100644 index 00000000..228de7ac --- /dev/null +++ b/YACReader/continuous_view_model.h @@ -0,0 +1,79 @@ +#ifndef CONTINUOUS_VIEW_MODEL_H +#define CONTINUOUS_VIEW_MODEL_H + +#include +#include +#include + +class ContinuousViewModel : public QObject +{ + Q_OBJECT +public: + explicit ContinuousViewModel(QObject *parent = nullptr); + + void reset(); + + void setNumPages(int count); + void setZoomFactor(int zoom); + void setViewportSize(int width, int height); + void setScrollYFromUser(int scrollY); + void setAnchorPage(int page); + void setCurrentPage(int page); + void setPageNaturalSize(int pageIndex, const QSize &size); + + int numPages() const; + int totalHeight() const; + int scrollY() const; + int viewportHeight() const; + int zoomFactor() const; + + int centerPage() const; + int yPositionForPage(int pageIndex) const; + int pageAtY(int y) const; + QSize scaledPageSize(int pageIndex) const; + +signals: + void stateChanged(); + +private: + struct LayoutSnapshot { + QVector yPositions; + QVector scaledSizes; + int totalHeight = 0; + }; + + struct ViewportAnchor { + int pageIndex = -1; + float offsetRatio = 0.0f; + bool valid = false; + }; + + enum class RecomputePolicy { + PreserveViewportAnchor, + PreserveScrollClamped, + ScrollToPageTop + }; + + void recompute(RecomputePolicy policy, int targetPage = -1); + LayoutSnapshot buildLayoutSnapshot(int width) const; + ViewportAnchor anchorFromViewport(const LayoutSnapshot &snapshot, int scrollY, int viewportHeight) const; + int resolveAnchorToScrollY(const LayoutSnapshot &snapshot, const ViewportAnchor &anchor, int viewportHeight) const; + int pageAtY(const LayoutSnapshot &snapshot, int y) const; + int maxScrollFor(const LayoutSnapshot &snapshot) const; + QSize scaledPageSizeForWidth(int pageIndex, int width) const; + float scaleForPage(int pageIndex, int width) const; + + int numPagesValue = 0; + QVector pageSizes; + QSize defaultPageSize { 800, 1200 }; + + int zoomFactorValue = 100; + int viewportWidth = 0; + int viewportHeightValue = 0; + int scrollYValue = 0; + int anchorPage = -1; + + LayoutSnapshot layoutSnapshot; +}; + +#endif // CONTINUOUS_VIEW_MODEL_H diff --git a/YACReader/viewer.cpp b/YACReader/viewer.cpp index 32c9911b..6eae05ad 100644 --- a/YACReader/viewer.cpp +++ b/YACReader/viewer.cpp @@ -1,5 +1,6 @@ #include "viewer.h" #include "continuous_page_widget.h" +#include "continuous_view_model.h" #include "configuration.h" #include "magnifying_glass.h" #include "goto_flow_widget.h" @@ -65,6 +66,8 @@ Viewer::Viewer(QWidget *parent) setAlignment(Qt::AlignCenter); continuousWidget = new ContinuousPageWidget(); + continuousViewModel = new ContinuousViewModel(this); + continuousWidget->setViewModel(continuousViewModel); continuousWidget->installEventFilter(this); //--------------------------------------- mglass = new MagnifyingGlass( @@ -153,6 +156,7 @@ Viewer::~Viewer() if (widget() != continuousWidget) { delete continuousWidget; } + delete continuousViewModel; delete hideCursorTimer; delete informationLabel; delete verticalScroller; @@ -204,7 +208,8 @@ void Viewer::createConnections() connect(render, QOverload::of(&Render::imageLoaded), goToFlow, &GoToFlowWidget::setImageReady); connect(render, &Render::currentPageReady, this, &Viewer::updatePage); connect(render, &Render::pageRendered, continuousWidget, &ContinuousPageWidget::onPageAvailable); - connect(continuousWidget, &ContinuousPageWidget::layoutScrollPositionRequested, this, &Viewer::onContinuousLayoutScrollRequested); + connect(render, &Render::pageRendered, this, &Viewer::onContinuousPageRendered); + connect(continuousViewModel, &ContinuousViewModel::stateChanged, this, &Viewer::onContinuousViewModelChanged); connect(render, qOverload(&Render::numPages), this, &Viewer::onNumPagesReady); connect(verticalScrollBar(), &QScrollBar::valueChanged, this, &Viewer::onContinuousScroll); connect(render, &Render::processingPage, this, &Viewer::setLoadingMessage); @@ -342,7 +347,7 @@ void Viewer::goTo(unsigned int page) if (continuousScroll) { lastCenterPage = page; - continuousWidget->setAnchorPage(page); + continuousViewModel->setAnchorPage(static_cast(page)); render->goTo(page); scrollToCurrentContinuousPage(); return; @@ -448,7 +453,7 @@ void Viewer::increaseZoomFactor() zoom = std::min(zoom + 10, 500); if (continuousScroll) { - continuousWidget->setZoomFactor(zoom); + continuousViewModel->setZoomFactor(zoom); } else { updateContentSize(); } @@ -462,7 +467,7 @@ void Viewer::decreaseZoomFactor() zoom = std::max(zoom - 10, 30); if (continuousScroll) { - continuousWidget->setZoomFactor(zoom); + continuousViewModel->setZoomFactor(zoom); } else { updateContentSize(); } @@ -831,7 +836,7 @@ void Viewer::resizeEvent(QResizeEvent *event) QScrollArea::resizeEvent(event); if (continuousScroll) { - continuousWidget->setViewportState(verticalScrollBar()->value(), viewport()->height()); + continuousViewModel->setViewportSize(viewport()->width(), viewport()->height()); } updateContentSize(); @@ -1019,15 +1024,15 @@ void Viewer::setContinuousScroll(bool enabled) Configuration::getConfiguration().setContinuousScroll(continuousScroll); if (continuousScroll) { - continuousWidget->setZoomFactor(zoom); + continuousViewModel->setZoomFactor(zoom); if (render->hasLoadedComic()) { - continuousWidget->setViewportState(verticalScrollBar()->value(), viewport()->height()); - continuousWidget->setNumPages(render->numPages()); + continuousViewModel->setViewportSize(viewport()->width(), viewport()->height()); + continuousViewModel->setNumPages(render->numPages()); // set the current page as model state before any layout/scroll happens lastCenterPage = render->getIndex(); - continuousWidget->setAnchorPage(lastCenterPage); + continuousViewModel->setAnchorPage(lastCenterPage); // pick up sizes of pages already in the buffer - continuousWidget->probeBufferedPages(); + probeContinuousBufferedPages(); // trigger a render cycle so new pages arrive via pageRendered signal render->update(); setActiveWidget(continuousWidget); @@ -1047,17 +1052,17 @@ void Viewer::setContinuousScroll(bool enabled) void Viewer::onContinuousScroll(int value) { - if (!continuousScroll || !render->hasLoadedComic()) { + if (!continuousScroll || !render->hasLoadedComic() || applyingContinuousModelState) { return; } - continuousWidget->setViewportState(value, viewport()->height()); + continuousViewModel->setScrollYFromUser(value); - int center = continuousWidget->centerPage(value, viewport()->height()); + int center = continuousViewModel->centerPage(); if (center != lastCenterPage && center >= 0) { lastCenterPage = center; - continuousWidget->setAnchorPage(center); + continuousViewModel->setAnchorPage(center); syncingRenderFromContinuousScroll = true; render->goTo(center); syncingRenderFromContinuousScroll = false; @@ -1065,20 +1070,65 @@ void Viewer::onContinuousScroll(int value) } } -void Viewer::onContinuousLayoutScrollRequested(int scrollY) +void Viewer::onContinuousViewModelChanged() { if (!continuousScroll) { return; } - auto *sb = verticalScrollBar(); - const int target = qBound(sb->minimum(), scrollY, sb->maximum()); + applyContinuousStateToUi(); +} +void Viewer::onContinuousPageRendered(int absolutePageIndex) +{ + if (!continuousScroll || !render->hasLoadedComic()) { + return; + } + + const QImage *img = render->bufferedImage(absolutePageIndex); + if (!img || img->isNull()) { + return; + } + + continuousViewModel->setPageNaturalSize(absolutePageIndex, img->size()); +} + +void Viewer::probeContinuousBufferedPages() +{ + if (!render->hasLoadedComic()) { + return; + } + + const int totalPages = static_cast(render->numPages()); + for (int i = 0; i < totalPages; ++i) { + const QImage *img = render->bufferedImage(i); + if (img && !img->isNull()) { + continuousViewModel->setPageNaturalSize(i, img->size()); + } + } +} + +void Viewer::applyContinuousStateToUi() +{ + if (!continuousScroll) { + return; + } + + applyingContinuousModelState = true; + + continuousWidget->setFixedHeight(continuousViewModel->totalHeight()); + continuousWidget->updateGeometry(); + + auto *sb = verticalScrollBar(); + const int target = qBound(sb->minimum(), continuousViewModel->scrollY(), sb->maximum()); sb->blockSignals(true); sb->setValue(target); sb->blockSignals(false); - continuousWidget->setViewportState(target, viewport()->height()); + applyingContinuousModelState = false; + + continuousWidget->update(); + viewport()->update(); } void Viewer::scrollToCurrentContinuousPage() @@ -1087,22 +1137,7 @@ void Viewer::scrollToCurrentContinuousPage() return; } - auto applyPosition = [this]() { - auto *sb = verticalScrollBar(); - int targetY = continuousWidget->yPositionForPage(lastCenterPage); - targetY = qBound(sb->minimum(), targetY, sb->maximum()); - - sb->blockSignals(true); - sb->setValue(targetY); - sb->blockSignals(false); - - continuousWidget->setViewportState(targetY, viewport()->height()); - - continuousWidget->update(); - viewport()->update(); - }; - - applyPosition(); + continuousViewModel->setCurrentPage(lastCenterPage); } void Viewer::onNumPagesReady(unsigned int numPages) @@ -1110,8 +1145,9 @@ void Viewer::onNumPagesReady(unsigned int numPages) if (continuousScroll && numPages > 0) { setActiveWidget(continuousWidget); - continuousWidget->setViewportState(verticalScrollBar()->value(), viewport()->height()); - continuousWidget->setNumPages(numPages); + continuousViewModel->setViewportSize(viewport()->width(), viewport()->height()); + continuousViewModel->setNumPages(numPages); + probeContinuousBufferedPages(); int page = lastCenterPage; if (page < 0) { @@ -1119,7 +1155,7 @@ void Viewer::onNumPagesReady(unsigned int numPages) } page = qBound(0, page, static_cast(numPages) - 1); lastCenterPage = page; - continuousWidget->setAnchorPage(page); + continuousViewModel->setAnchorPage(page); scrollToCurrentContinuousPage(); } @@ -1132,7 +1168,7 @@ void Viewer::onRenderPageChanged(int page) } lastCenterPage = page; - continuousWidget->setAnchorPage(page); + continuousViewModel->setAnchorPage(page); scrollToCurrentContinuousPage(); } @@ -1157,6 +1193,7 @@ void Viewer::resetContent() { configureContent(tr("Press 'O' to open comic.")); goToFlow->reset(); + continuousViewModel->reset(); continuousWidget->reset(); lastCenterPage = -1; emit reset(); @@ -1316,7 +1353,7 @@ void Viewer::updateZoomRatio(int ratio) { zoom = ratio; if (continuousScroll) { - continuousWidget->setZoomFactor(zoom); + continuousViewModel->setZoomFactor(zoom); } else { updateContentSize(); } diff --git a/YACReader/viewer.h b/YACReader/viewer.h index d5b852f5..99e6b60a 100644 --- a/YACReader/viewer.h +++ b/YACReader/viewer.h @@ -31,6 +31,7 @@ class YACReaderTranslator; class GoToFlowWidget; class Bookmarks; class ContinuousPageWidget; +class ContinuousViewModel; class PageLabelWidget; class NotificationsLabelWidget; @@ -149,8 +150,10 @@ private: QLabel *content; QLabel *messageLabel; ContinuousPageWidget *continuousWidget; + ContinuousViewModel *continuousViewModel; int lastCenterPage = -1; bool syncingRenderFromContinuousScroll = false; + bool applyingContinuousModelState = false; YACReaderTranslator *translator; int translatorXPos; @@ -191,7 +194,10 @@ private: int animationDuration() const; void animateScroll(QPropertyAnimation &scroller, const QScrollBar &scrollBar, int delta); void onContinuousScroll(int value); - void onContinuousLayoutScrollRequested(int scrollY); + void onContinuousViewModelChanged(); + void onContinuousPageRendered(int absolutePageIndex); + void probeContinuousBufferedPages(); + void applyContinuousStateToUi(); void scrollToCurrentContinuousPage(); void onNumPagesReady(unsigned int numPages); void onRenderPageChanged(int page);