diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index d58dab71..0db6c33b 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -34,7 +34,7 @@ if (CHECK_UPDATES)
add_subdirectory(openpgp)
endif()
-qt_add_resources(RESOURCES assets.qrc assets_tor.qrc assets_wownerod.qrc assets_docs.qrc mining.qrc silly.qrc)
+qt_add_resources(RESOURCES assets.qrc assets_tor.qrc assets_wownerod.qrc assets_docs.qrc mining.qrc silly.qrc scene.qrc)
# Compile source files (.h/.cpp)
file(GLOB SOURCE_FILES
diff --git a/src/assets/images/scene/dog.gif b/src/assets/images/scene/dog.gif
new file mode 100644
index 00000000..433ee586
Binary files /dev/null and b/src/assets/images/scene/dog.gif differ
diff --git a/src/assets/images/scene/dog2.gif b/src/assets/images/scene/dog2.gif
new file mode 100644
index 00000000..f773aeed
Binary files /dev/null and b/src/assets/images/scene/dog2.gif differ
diff --git a/src/assets/images/scene/flyingmoney.gif b/src/assets/images/scene/flyingmoney.gif
new file mode 100644
index 00000000..7ff0bf4c
Binary files /dev/null and b/src/assets/images/scene/flyingmoney.gif differ
diff --git a/src/assets/images/scene/goldcoin.gif b/src/assets/images/scene/goldcoin.gif
new file mode 100644
index 00000000..010c087c
Binary files /dev/null and b/src/assets/images/scene/goldcoin.gif differ
diff --git a/src/assets/images/scene/grass.png b/src/assets/images/scene/grass.png
new file mode 100644
index 00000000..d3f74dd2
Binary files /dev/null and b/src/assets/images/scene/grass.png differ
diff --git a/src/assets/qml/scene.qml b/src/assets/qml/scene.qml
new file mode 100644
index 00000000..2edc2f3b
--- /dev/null
+++ b/src/assets/qml/scene.qml
@@ -0,0 +1,49 @@
+import QtQuick
+
+// wowlet ambient scene — dusk sky, tiled grass, a running doge, bouncing WOW
+// coins, drifting money. All motion gates on `scene.reducedMotion` (exposed by
+// the host SceneBackend); frozen it is the reduced-motion still.
+Rectangle {
+ id: scene
+ property bool reducedMotion: (typeof sceneCtx !== 'undefined' && sceneCtx) ? sceneCtx.reducedMotion : false
+
+ gradient: Gradient {
+ GradientStop { position: 0.0; color: "#160d2b" }
+ GradientStop { position: 0.5; color: "#5b2f6e" }
+ GradientStop { position: 0.82; color: "#c96aa0" }
+ GradientStop { position: 1.0; color: "#f2adc9" }
+ }
+
+ Rectangle { x: parent.width*0.74; y: parent.height*0.14; width: 62; height: 62; radius: 31; color: "#ffe9b0"; opacity: 0.9 }
+
+ AnimatedImage {
+ source: "qrc:/scene/flyingmoney.gif"; playing: !scene.reducedMotion
+ width: 70; height: 64; y: 30; x: 120; opacity: 0.9
+ NumberAnimation on x { running: !scene.reducedMotion; from: -80; to: scene.width; duration: 11000; loops: Animation.Infinite }
+ }
+
+ Row {
+ anchors.bottom: parent.bottom
+ Repeater { model: Math.ceil(scene.width/180)+1
+ Image { source: "qrc:/scene/grass.png"; width: 180; height: 46; smooth: false } }
+ }
+
+ Repeater {
+ model: 3
+ AnimatedImage {
+ source: "qrc:/scene/goldcoin.gif"; playing: !scene.reducedMotion
+ width: 34; height: 34; x: 150 + index*170; y: scene.height*0.46
+ SequentialAnimation on y {
+ running: !scene.reducedMotion; loops: Animation.Infinite
+ NumberAnimation { to: scene.height*0.36; duration: 1050 + index*170; easing.type: Easing.OutQuad }
+ NumberAnimation { to: scene.height*0.5; duration: 1050 + index*170; easing.type: Easing.InQuad }
+ }
+ }
+ }
+
+ AnimatedImage {
+ source: "qrc:/scene/dog.gif"; playing: !scene.reducedMotion
+ width: 104; height: 83; y: scene.height - 46 - 62; x: 70
+ NumberAnimation on x { running: !scene.reducedMotion; from: -110; to: scene.width; duration: 6500; loops: Animation.Infinite }
+ }
+}
diff --git a/src/scene.qrc b/src/scene.qrc
new file mode 100644
index 00000000..3e864792
--- /dev/null
+++ b/src/scene.qrc
@@ -0,0 +1,10 @@
+
+
+ assets/qml/scene.qml
+ assets/images/scene/dog2.gif
+ assets/images/scene/dog.gif
+ assets/images/scene/flyingmoney.gif
+ assets/images/scene/goldcoin.gif
+ assets/images/scene/grass.png
+
+
diff --git a/src/widgets/SceneWidget.cpp b/src/widgets/SceneWidget.cpp
new file mode 100644
index 00000000..32c7c98c
--- /dev/null
+++ b/src/widgets/SceneWidget.cpp
@@ -0,0 +1,22 @@
+// SPDX-License-Identifier: BSD-3-Clause
+// SPDX-FileCopyrightText: The Monero Project
+
+#include "SceneWidget.h"
+
+#include
+
+#include "utils/config.h"
+
+SceneWidget::SceneWidget(QWidget *parent)
+ : QQuickWidget(parent)
+ , m_backend(new SceneBackend(this))
+{
+ m_backend->setReducedMotion(conf()->get(Config::reducedMotion).toBool());
+ this->rootContext()->setContextProperty("sceneCtx", m_backend);
+ this->setResizeMode(QQuickWidget::SizeRootObjectToView);
+ this->setSource(QUrl("qrc:/qml/scene.qml"));
+}
+
+void SceneWidget::refreshReducedMotion() {
+ m_backend->setReducedMotion(conf()->get(Config::reducedMotion).toBool());
+}
diff --git a/src/widgets/SceneWidget.h b/src/widgets/SceneWidget.h
new file mode 100644
index 00000000..635be1b9
--- /dev/null
+++ b/src/widgets/SceneWidget.h
@@ -0,0 +1,37 @@
+// SPDX-License-Identifier: BSD-3-Clause
+// SPDX-FileCopyrightText: The Monero Project
+
+#ifndef WOWLET_SCENEWIDGET_H
+#define WOWLET_SCENEWIDGET_H
+
+#include
+#include
+
+// Context object exposed to scene.qml as `sceneCtx`. Carries the reduced-motion
+// setting so the scene freezes into its still (reduced-motion) form.
+class SceneBackend : public QObject {
+Q_OBJECT
+ Q_PROPERTY(bool reducedMotion READ reducedMotion NOTIFY reducedMotionChanged)
+public:
+ explicit SceneBackend(QObject *parent = nullptr) : QObject(parent) {}
+ bool reducedMotion() const { return m_reducedMotion; }
+ void setReducedMotion(bool r) { if (m_reducedMotion != r) { m_reducedMotion = r; emit reducedMotionChanged(); } }
+signals:
+ void reducedMotionChanged();
+private:
+ bool m_reducedMotion = false;
+};
+
+// wowlet: reusable ambient-scene surface. Hosts scene.qml and reads the
+// reduced-motion preference from Config. Drop it behind a form, into a banner,
+// or over the window as an idle screensaver.
+class SceneWidget : public QQuickWidget {
+Q_OBJECT
+public:
+ explicit SceneWidget(QWidget *parent = nullptr);
+ void refreshReducedMotion();
+private:
+ SceneBackend *m_backend;
+};
+
+#endif //WOWLET_SCENEWIDGET_H
diff --git a/src/widgets/WalletUnlockWidget.cpp b/src/widgets/WalletUnlockWidget.cpp
index 1c29fe08..6157113f 100644
--- a/src/widgets/WalletUnlockWidget.cpp
+++ b/src/widgets/WalletUnlockWidget.cpp
@@ -9,6 +9,7 @@
#include "utils/Utils.h"
#include "libwalletqt/Wallet.h"
+#include "widgets/SceneWidget.h"
WalletUnlockWidget::WalletUnlockWidget(QWidget *parent, Wallet *wallet)
: QWidget(parent)
@@ -18,6 +19,13 @@ WalletUnlockWidget::WalletUnlockWidget(QWidget *parent, Wallet *wallet)
ui->setupUi(this);
this->reset();
+ // wowlet: ambient scene banner above the unlock form (freezes itself into a
+ // still when "Prefer reduced motion" is set).
+ auto *scene = new SceneWidget(this);
+ scene->setMinimumHeight(120);
+ scene->setMaximumHeight(140);
+ ui->verticalLayout->insertWidget(0, scene);
+
ui->buttonBox->button(QDialogButtonBox::Ok)->setAutoDefault(true);
connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &WalletUnlockWidget::tryUnlock);