perf: improve animation smoothness on Wayland

This commit is contained in:
dela
2026-06-10 16:12:29 +08:00
parent d56e09914f
commit 012d165b81
8 changed files with 338 additions and 102 deletions

View File

@@ -31,6 +31,12 @@ sudo pacman -U qemby-*.pkg.tar.zst
qemby qemby
``` ```
If native Wayland causes windowing or OpenGL issues, run with the XCB fallback:
```fish
QEMBY_FORCE_XCB=1 qemby
```
## Uninstall ## Uninstall
```fish ```fish

View File

@@ -1,8 +1,19 @@
#include "slidingstackedwidget.h" #include "slidingstackedwidget.h"
#include <QWidget> #include <QWidget>
#include <QLabel> #include <QLabel>
#include <QScreen>
#include "../../qEmbyCore/config/configstore.h" #include "../../qEmbyCore/config/configstore.h"
#include "config/config_keys.h" #include "config/config_keys.h"
#include <cmath>
namespace {
QPoint interpolatePoint(const QPoint &start, const QPoint &end, qreal progress)
{
return QPoint(
qRound(start.x() + (end.x() - start.x()) * progress),
qRound(start.y() + (end.y() - start.y()) * progress));
}
}
SlidingStackedWidget::SlidingStackedWidget(QWidget *parent) SlidingStackedWidget::SlidingStackedWidget(QWidget *parent)
: QStackedWidget(parent), : QStackedWidget(parent),
@@ -11,8 +22,9 @@ SlidingStackedWidget::SlidingStackedWidget(QWidget *parent)
m_isAnimating(false), m_isAnimating(false),
m_nextIndex(0) m_nextIndex(0)
{ {
m_animGroup = new QParallelAnimationGroup(this); m_slideTimer.setTimerType(Qt::PreciseTimer);
connect(m_animGroup, &QParallelAnimationGroup::finished, this, &SlidingStackedWidget::animationDoneSlot); connect(&m_slideTimer, &QTimer::timeout, this,
&SlidingStackedWidget::updateSlideAnimation);
} }
void SlidingStackedWidget::setSpeed(int speed) void SlidingStackedWidget::setSpeed(int speed)
@@ -45,22 +57,7 @@ void SlidingStackedWidget::slideInIdx(int idx, SlideDirection direction)
if (m_isAnimating) { if (m_isAnimating) {
m_slideTimer.stop();
bool allTargetsAlive = true;
for (int i = 0; i < m_animGroup->animationCount(); ++i) {
auto *anim = qobject_cast<QPropertyAnimation *>(m_animGroup->animationAt(i));
if (anim && !anim->targetObject()) {
allTargetsAlive = false;
break;
}
}
if (allTargetsAlive) {
m_animGroup->stop();
}
animationDoneSlot(); animationDoneSlot();
} }
@@ -126,45 +123,50 @@ void SlidingStackedWidget::slideInIdx(int idx, SlideDirection direction)
QPoint pNext = widgetNext->pos(); QPoint pNext = widgetNext->pos();
QPoint pNow = widgetNow->pos(); QPoint pNow = widgetNow->pos();
const bool useSnapshot = const bool useSnapshot =
ConfigStore::instance()->get<bool>(ConfigKeys::SnapshotNavigation, false); ConfigStore::instance()->get<bool>(ConfigKeys::SnapshotNavigation, true);
QWidget *animTargetNow = widgetNow;
QWidget *animTargetNext = widgetNext;
bool usingSnapshotAnimation = false;
if (useSnapshot) { if (useSnapshot) {
QPixmap snapshot = widgetNow->grab(); clearSnapshotLabels();
m_snapshotLabel = new QLabel(this);
m_snapshotLabel->setPixmap(snapshot); m_currentSnapshotLabel =
m_snapshotLabel->setGeometry(widgetNow->geometry()); createSnapshotLabel(widgetNow, QRect(pNow, QSize(width, height)));
m_snapshotLabel->show();
widgetNow->hide(); widgetNext->show();
widgetNext->raise();
m_nextSnapshotLabel =
createSnapshotLabel(widgetNext,
QRect(QPoint(pNext.x() + offsetX,
pNext.y() + offsetY),
QSize(width, height)));
usingSnapshotAnimation =
m_currentSnapshotLabel && m_nextSnapshotLabel;
} }
if (usingSnapshotAnimation) {
animTargetNow = m_currentSnapshotLabel;
animTargetNext = m_nextSnapshotLabel;
widgetNow->hide();
widgetNext->hide();
} else {
clearSnapshotLabels();
widgetNext->move(pNext.x() + offsetX, pNext.y() + offsetY); widgetNext->move(pNext.x() + offsetX, pNext.y() + offsetY);
widgetNext->show(); widgetNext->show();
widgetNext->raise(); widgetNext->raise();
}
startSlideAnimation(animTargetNow,
QObject *animTarget = useSnapshot ? static_cast<QObject*>(m_snapshotLabel) pNow,
: static_cast<QObject*>(widgetNow); QPoint(pNow.x() - offsetX, pNow.y() - offsetY),
QPropertyAnimation *animNow = new QPropertyAnimation(animTarget, "pos"); animTargetNext,
animNow->setDuration(m_speed); QPoint(pNext.x() + offsetX, pNext.y() + offsetY),
animNow->setEasingCurve(m_animationType); pNext);
animNow->setStartValue(pNow);
animNow->setEndValue(QPoint(pNow.x() - offsetX, pNow.y() - offsetY));
QPropertyAnimation *animNext = new QPropertyAnimation(widgetNext, "pos");
animNext->setDuration(m_speed);
animNext->setEasingCurve(m_animationType);
animNext->setStartValue(widgetNext->pos());
animNext->setEndValue(pNext);
m_animGroup->clear();
m_animGroup->addAnimation(animNow);
m_animGroup->addAnimation(animNext);
m_animGroup->start();
} }
void SlidingStackedWidget::disposeWidgetWhenSafe(QWidget *widget) void SlidingStackedWidget::disposeWidgetWhenSafe(QWidget *widget)
@@ -186,14 +188,124 @@ void SlidingStackedWidget::disposeWidgetWhenSafe(QWidget *widget)
} }
} }
QLabel *SlidingStackedWidget::createSnapshotLabel(QWidget *source,
const QRect &geometry)
{
if (!source || geometry.size().isEmpty())
{
return nullptr;
}
QPixmap snapshot = source->grab(QRect(QPoint(0, 0), geometry.size()));
if (snapshot.isNull())
{
return nullptr;
}
auto *label = new QLabel(this);
label->setAttribute(Qt::WA_TransparentForMouseEvents, true);
label->setPixmap(snapshot);
label->setGeometry(geometry);
label->show();
label->raise();
return label;
}
void SlidingStackedWidget::clearSnapshotLabels()
{
if (m_currentSnapshotLabel)
{
m_currentSnapshotLabel->hide();
m_currentSnapshotLabel->deleteLater();
m_currentSnapshotLabel = nullptr;
}
if (m_nextSnapshotLabel)
{
m_nextSnapshotLabel->hide();
m_nextSnapshotLabel->deleteLater();
m_nextSnapshotLabel = nullptr;
}
}
void SlidingStackedWidget::startSlideAnimation(QWidget *widgetNow,
const QPoint &nowStart,
const QPoint &nowEnd,
QWidget *widgetNext,
const QPoint &nextStart,
const QPoint &nextEnd)
{
m_animWidgetNow = widgetNow;
m_animWidgetNext = widgetNext;
m_animNowStart = nowStart;
m_animNowEnd = nowEnd;
m_animNextStart = nextStart;
m_animNextEnd = nextEnd;
if (!m_animWidgetNow || !m_animWidgetNext || m_speed <= 0)
{
animationDoneSlot();
return;
}
m_slideTimer.setInterval(animationFrameIntervalMs());
m_slideClock.restart();
updateSlideAnimation();
m_slideTimer.start();
}
void SlidingStackedWidget::updateSlideAnimation()
{
if (!m_animWidgetNow || !m_animWidgetNext)
{
animationDoneSlot();
return;
}
const qreal elapsedMs = m_slideClock.isValid()
? m_slideClock.nsecsElapsed() / 1000000.0
: static_cast<qreal>(m_speed);
const qreal linearProgress =
qBound<qreal>(0.0, elapsedMs / qMax(1, m_speed), 1.0);
const qreal easedProgress =
QEasingCurve(m_animationType).valueForProgress(linearProgress);
m_animWidgetNow->move(
interpolatePoint(m_animNowStart, m_animNowEnd, easedProgress));
m_animWidgetNext->move(
interpolatePoint(m_animNextStart, m_animNextEnd, easedProgress));
if (linearProgress >= 1.0)
{
animationDoneSlot();
}
}
int SlidingStackedWidget::animationFrameIntervalMs() const
{
const QScreen *targetScreen = screen();
if (!targetScreen && window())
{
targetScreen = window()->screen();
}
const qreal refreshRate = targetScreen ? targetScreen->refreshRate() : 0.0;
if (refreshRate <= 0.0)
{
return 16;
}
return qBound(4,
static_cast<int>(std::lround(1000.0 / refreshRate)),
16);
}
void SlidingStackedWidget::animationDoneSlot() void SlidingStackedWidget::animationDoneSlot()
{ {
m_slideTimer.stop();
if (m_snapshotLabel) { m_animWidgetNow.clear();
m_snapshotLabel->hide(); m_animWidgetNext.clear();
m_snapshotLabel->deleteLater(); clearSnapshotLabels();
m_snapshotLabel = nullptr;
}
if (m_nextIndex >= 0 && m_nextIndex < count()) { if (m_nextIndex >= 0 && m_nextIndex < count()) {

View File

@@ -2,11 +2,13 @@
#define SLIDINGSTACKEDWIDGET_H #define SLIDINGSTACKEDWIDGET_H
#include <QStackedWidget> #include <QStackedWidget>
#include <QPropertyAnimation>
#include <QParallelAnimationGroup>
#include <QEasingCurve> #include <QEasingCurve>
#include <QElapsedTimer>
#include <QList> #include <QList>
#include <QPoint>
#include <QPointer> #include <QPointer>
#include <QRect>
#include <QTimer>
class QLabel; class QLabel;
@@ -42,15 +44,31 @@ private Q_SLOTS:
void animationDoneSlot(); void animationDoneSlot();
private: private:
QLabel *createSnapshotLabel(QWidget *source, const QRect &geometry);
void clearSnapshotLabels();
void startSlideAnimation(QWidget *widgetNow, const QPoint &nowStart,
const QPoint &nowEnd, QWidget *widgetNext,
const QPoint &nextStart,
const QPoint &nextEnd);
void updateSlideAnimation();
int animationFrameIntervalMs() const;
void flushPendingWidgetDisposals(); void flushPendingWidgetDisposals();
int m_speed; int m_speed;
QEasingCurve::Type m_animationType; QEasingCurve::Type m_animationType;
QParallelAnimationGroup *m_animGroup; QTimer m_slideTimer;
QElapsedTimer m_slideClock;
QPointer<QWidget> m_animWidgetNow;
QPointer<QWidget> m_animWidgetNext;
QPoint m_animNowStart;
QPoint m_animNowEnd;
QPoint m_animNextStart;
QPoint m_animNextEnd;
bool m_isAnimating; bool m_isAnimating;
int m_nextIndex; int m_nextIndex;
QList<QPointer<QWidget>> m_pendingWidgetDisposals; QList<QPointer<QWidget>> m_pendingWidgetDisposals;
QLabel* m_snapshotLabel = nullptr; QLabel* m_currentSnapshotLabel = nullptr;
QLabel* m_nextSnapshotLabel = nullptr;
}; };
#endif #endif

View File

@@ -27,17 +27,19 @@ int main(int argc, char *argv[]) {
#if defined(Q_OS_LINUX) #if defined(Q_OS_LINUX)
const QByteArray qpaPlatform = qgetenv("QT_QPA_PLATFORM"); const QByteArray qpaPlatform = qgetenv("QT_QPA_PLATFORM");
const QByteArray sessionType = qgetenv("XDG_SESSION_TYPE").toLower(); const QByteArray forceXcb = qgetenv("QEMBY_FORCE_XCB").toLower();
if (qpaPlatform.isEmpty() && sessionType == "wayland" && if (qpaPlatform.isEmpty() &&
!qgetenv("DISPLAY").isEmpty()) { (forceXcb == "1" || forceXcb == "true" || forceXcb == "yes")) {
qputenv("QT_QPA_PLATFORM", "xcb"); qputenv("QT_QPA_PLATFORM", "xcb");
} }
#endif #endif
QSurfaceFormat format; QSurfaceFormat format;
#if !defined(Q_OS_LINUX)
format.setVersion(3, 3); format.setVersion(3, 3);
format.setProfile(QSurfaceFormat::CoreProfile); format.setProfile(QSurfaceFormat::CoreProfile);
#endif
format.setDepthBufferSize(24); format.setDepthBufferSize(24);
format.setStencilBufferSize(8); format.setStencilBufferSize(8);
format.setSwapInterval(1); format.setSwapInterval(1);

View File

@@ -48,6 +48,36 @@
#include <widgetframe/windowbar.h> #include <widgetframe/windowbar.h>
#include <widgetframe/windowbutton.h> #include <widgetframe/windowbutton.h>
namespace {
bool canAnimateTopLevelWindowPosition()
{
#if defined(Q_OS_LINUX)
return !QGuiApplication::platformName().contains(QStringLiteral("wayland"),
Qt::CaseInsensitive);
#else
return true;
#endif
}
QRect centeredRectWithin(const QRect &sourceGeometry, const QSize &targetSize,
const QRect &bounds)
{
QRect target(QPoint(0, 0), targetSize);
target.moveCenter(sourceGeometry.center());
if (target.left() < bounds.left())
target.moveLeft(bounds.left());
if (target.top() < bounds.top())
target.moveTop(bounds.top());
if (target.right() > bounds.right())
target.moveRight(bounds.right());
if (target.bottom() > bounds.bottom())
target.moveBottom(bounds.bottom());
return target;
}
}
static inline void emulateLeaveEvent(QWidget *widget) { static inline void emulateLeaveEvent(QWidget *widget) {
Q_ASSERT(widget); Q_ASSERT(widget);
if (!widget) return; if (!widget) return;
@@ -966,6 +996,51 @@ void MainWindow::navigateToHome()
return; return;
} }
if (!canAnimateTopLevelWindowPosition()) {
hide();
setWindowOpacity(0.0);
setMaximumSize(QWIDGETSIZE_MAX, QWIDGETSIZE_MAX);
setMinimumSize(0, 0);
const QString windowState = ConfigStore::instance()->get<QString>(
ConfigKeys::StartupWindowState, "normal");
const bool shouldMaximize = (windowState == "maximized");
m_loginView->setGraphicsEffect(nullptr);
m_homeView->setGraphicsEffect(nullptr);
m_viewStack->setCurrentWidget(m_homeView);
#if !defined(Q_OS_MACOS) && !defined(Q_OS_MAC)
if (auto* minBtn = findChild<QWidget*>("min-button")) minBtn->show();
if (auto* maxBtn = findChild<QWidget*>("max-button")) maxBtn->show();
#endif
if (shouldMaximize) {
showMaximized();
} else {
resize(1280, 800);
}
setMinimumSize(900, 600);
show();
raise();
activateWindow();
auto *fadeIn = new QPropertyAnimation(this, "windowOpacity", this);
fadeIn->setDuration(180);
fadeIn->setStartValue(0.0);
fadeIn->setEndValue(1.0);
fadeIn->setEasingCurve(QEasingCurve::OutCubic);
connect(fadeIn, &QPropertyAnimation::finished, this, [this]() {
setWindowOpacity(1.0);
m_viewTransitionInProgress = false;
});
QTimer::singleShot(0, fadeIn, [fadeIn]() {
fadeIn->start(QAbstractAnimation::DeleteWhenStopped);
});
return;
}
auto *loginOpacity = new QGraphicsOpacityEffect(m_loginView); auto *loginOpacity = new QGraphicsOpacityEffect(m_loginView);
loginOpacity->setOpacity(1.0); loginOpacity->setOpacity(1.0);
@@ -979,6 +1054,7 @@ void MainWindow::navigateToHome()
connect(fadeOut, &QPropertyAnimation::finished, this, [this]() { connect(fadeOut, &QPropertyAnimation::finished, this, [this]() {
setMaximumSize(QWIDGETSIZE_MAX, QWIDGETSIZE_MAX); setMaximumSize(QWIDGETSIZE_MAX, QWIDGETSIZE_MAX);
setMinimumSize(0, 0);
QString windowState = ConfigStore::instance()->get<QString>( QString windowState = ConfigStore::instance()->get<QString>(
@@ -994,32 +1070,13 @@ void MainWindow::navigateToHome()
int targetWidth = 1280; int targetWidth = 1280;
int targetHeight = 800; int targetHeight = 800;
targetGeo = QRect(
startGeo.x() - (targetWidth - startGeo.width()) / 2,
startGeo.y() - (targetHeight - startGeo.height()) / 2,
targetWidth, targetHeight);
QRect screenGeo = screen()->availableGeometry(); QRect screenGeo = screen()->availableGeometry();
if (targetGeo.left() < screenGeo.left()) targetGeo = centeredRectWithin(startGeo,
targetGeo.moveLeft(screenGeo.left()); QSize(targetWidth, targetHeight),
if (targetGeo.top() < screenGeo.top()) screenGeo);
targetGeo.moveTop(screenGeo.top());
if (targetGeo.right() > screenGeo.right())
targetGeo.moveRight(screenGeo.right());
if (targetGeo.bottom() > screenGeo.bottom())
targetGeo.moveBottom(screenGeo.bottom());
} }
auto *geoAnim = new QPropertyAnimation(this, "geometry", this); auto finishWindowTransition = [this, shouldMaximize]() {
geoAnim->setDuration(shouldMaximize ? 500 : 350);
geoAnim->setStartValue(startGeo);
geoAnim->setEndValue(targetGeo);
geoAnim->setEasingCurve(QEasingCurve::InOutCubic);
connect(geoAnim, &QPropertyAnimation::finished, this, [this, shouldMaximize]() {
setMinimumSize(900, 600); setMinimumSize(900, 600);
@@ -1055,7 +1112,16 @@ void MainWindow::navigateToHome()
m_viewTransitionInProgress = false; m_viewTransitionInProgress = false;
}); });
fadeIn->start(QAbstractAnimation::DeleteWhenStopped); fadeIn->start(QAbstractAnimation::DeleteWhenStopped);
}); };
auto *geoAnim = new QPropertyAnimation(this, "geometry", this);
geoAnim->setDuration(shouldMaximize ? 500 : 350);
geoAnim->setStartValue(startGeo);
geoAnim->setEndValue(targetGeo);
geoAnim->setEasingCurve(QEasingCurve::InOutCubic);
connect(geoAnim, &QPropertyAnimation::finished, this,
finishWindowTransition);
geoAnim->start(QAbstractAnimation::DeleteWhenStopped); geoAnim->start(QAbstractAnimation::DeleteWhenStopped);
}); });
fadeOut->start(QAbstractAnimation::DeleteWhenStopped); fadeOut->start(QAbstractAnimation::DeleteWhenStopped);
@@ -1132,20 +1198,11 @@ void MainWindow::navigateToLogin() {
QRect startGeo = geometry(); QRect startGeo = geometry();
int targetWidth = m_defaultWidth; int targetWidth = m_defaultWidth;
int targetHeight = m_defaultHeight; int targetHeight = m_defaultHeight;
QRect targetGeo = centeredRectWithin(
startGeo, QSize(targetWidth, targetHeight),
screen()->availableGeometry());
auto finishWindowTransition = [this]() {
QRect targetGeo(startGeo.x() + (startGeo.width() - targetWidth) / 2,
startGeo.y() + (startGeo.height() - targetHeight) / 2,
targetWidth, targetHeight);
auto *geoAnim = new QPropertyAnimation(this, "geometry", this);
geoAnim->setDuration(startGeo.width() > 1300 ? 500 : 350);
geoAnim->setStartValue(startGeo);
geoAnim->setEndValue(targetGeo);
geoAnim->setEasingCurve(QEasingCurve::InOutCubic);
connect(geoAnim, &QPropertyAnimation::finished, this, [this]() {
@@ -1172,8 +1229,22 @@ void MainWindow::navigateToLogin() {
m_viewTransitionInProgress = false; m_viewTransitionInProgress = false;
}); });
fadeIn->start(QAbstractAnimation::DeleteWhenStopped); fadeIn->start(QAbstractAnimation::DeleteWhenStopped);
}); };
geoAnim->start(QAbstractAnimation::DeleteWhenStopped);
if (canAnimateTopLevelWindowPosition()) {
auto *geoAnim = new QPropertyAnimation(this, "geometry", this);
geoAnim->setDuration(startGeo.width() > 1300 ? 500 : 350);
geoAnim->setStartValue(startGeo);
geoAnim->setEndValue(targetGeo);
geoAnim->setEasingCurve(QEasingCurve::InOutCubic);
connect(geoAnim, &QPropertyAnimation::finished, this,
finishWindowTransition);
geoAnim->start(QAbstractAnimation::DeleteWhenStopped);
} else {
resize(targetGeo.size());
finishWindowTransition();
}
}); });
fadeOut->start(QAbstractAnimation::DeleteWhenStopped); fadeOut->start(QAbstractAnimation::DeleteWhenStopped);
} }

View File

@@ -1,6 +1,7 @@
#include "smoothscrollcontroller.h" #include "smoothscrollcontroller.h"
#include <QScrollBar> #include <QScrollBar>
#include <QScreen>
#include <QWheelEvent> #include <QWheelEvent>
#include <cmath> #include <cmath>
@@ -150,6 +151,27 @@ int SmoothScrollController::wheelDelta(const QWheelEvent *event,
return orientation == Qt::Horizontal ? angleDelta.x() : angleDelta.y(); return orientation == Qt::Horizontal ? angleDelta.x() : angleDelta.y();
} }
int SmoothScrollController::frameIntervalMs() const
{
if (!m_scrollBar) {
return 16;
}
const QScreen *targetScreen = m_scrollBar->screen();
if (!targetScreen && m_scrollBar->window()) {
targetScreen = m_scrollBar->window()->screen();
}
const qreal refreshRate = targetScreen ? targetScreen->refreshRate() : 0.0;
if (refreshRate <= 0.0) {
return 16;
}
return qBound(4,
static_cast<int>(std::lround(1000.0 / refreshRate)),
16);
}
void SmoothScrollController::startTicker() void SmoothScrollController::startTicker()
{ {
if (!m_scrollBar || m_scrollBar->value() == m_targetValue) { if (!m_scrollBar || m_scrollBar->value() == m_targetValue) {
@@ -157,6 +179,7 @@ void SmoothScrollController::startTicker()
return; return;
} }
m_ticker.setInterval(frameIntervalMs());
if (!m_ticker.isActive()) { if (!m_ticker.isActive()) {
m_frameClock.restart(); m_frameClock.restart();
m_ticker.start(); m_ticker.start();

View File

@@ -29,6 +29,7 @@ private:
int boundedValue(int value) const; int boundedValue(int value) const;
int wheelDelta(const QWheelEvent *event, int wheelDelta(const QWheelEvent *event,
Qt::Orientation orientation) const; Qt::Orientation orientation) const;
int frameIntervalMs() const;
void startTicker(); void startTicker();
QScrollBar *m_scrollBar = nullptr; QScrollBar *m_scrollBar = nullptr;

View File

@@ -66,6 +66,9 @@ ConfigStore::ConfigStore(QObject* parent) : QObject(parent) {
if (!m_settings->contains(ConfigKeys::ImageCacheDuration)) { if (!m_settings->contains(ConfigKeys::ImageCacheDuration)) {
m_settings->setValue(ConfigKeys::ImageCacheDuration, "7"); m_settings->setValue(ConfigKeys::ImageCacheDuration, "7");
} }
if (!m_settings->contains(ConfigKeys::SnapshotNavigation)) {
m_settings->setValue(ConfigKeys::SnapshotNavigation, true);
}
} }
ConfigStore::~ConfigStore() { ConfigStore::~ConfigStore() {