Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion cmake/meta_info/desktop_settings.template.h.in
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,13 @@ static constexpr const char* EARLY_SETTINGS_TEMPLATE = R"({
static constexpr const char* WALLPAPER_CONFIG_TEMPLATE = R"({
"source_path": "",
"scaling": "fill",
"background_color": "#1c1b1f"
"background_color": "#1c1b1f",
"switch_mode": "movement",
"switch_selector": "sequential",
"switch_interval_ms": 20000,
"animation_duration_ms": 2000,
"switch_easing": "inoutcubic",
"disable_animation": false
}
)";
} // namespace cf::desktop::early_stage
175 changes: 161 additions & 14 deletions desktop/ui/components/shell_layer_impl/WallpaperShellLayerStrategy.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,42 @@
#include "IShellLayer.h"
#include "cflog.h"
#include "qt_format.h"
#include "wallpaper/WallPaperAccessStorage.h"
#include "wallpaper/WallPaperEngine.h"
#include "wallpaper/WallPaperLayer.h"

#include <QEasingCurve>
#include <QImage>
#include <QPainter>
#include <QVariantAnimation>
#include <algorithm>
#include <random>

namespace cf::desktop {

namespace {
/// Log tag for the wallpaper shell layer strategy.
constexpr const char* kLogTag = "WallpaperShellLayerStrategy";
} // namespace

// ============================================================
// WallpaperShellLayerStrategy::Private
// ============================================================
struct WallpaperShellLayerStrategy::Private {
std::unique_ptr<wallpaper::WallPaperLayer> wallpaper_layer;
std::unique_ptr<wallpaper::WallPaperEngine> engine;
QImage cached_scaled_image;
QRect current_geometry;
aex::WeakPtr<IShellLayer> layer;
aex::WeakPtr<WindowManager> window_manager;

// Transition state.
bool transitioning{false};
wallpaper::SwitchingMode pending_mode{wallpaper::SwitchingMode::Movement};
QImage previous_scaled; ///< Outgoing frame (already scaled to geometry).
QImage current_scaled; ///< Incoming frame during a transition.
std::unique_ptr<QVariantAnimation> anim;
std::mt19937 rng;
};

// ============================================================
Expand All @@ -31,42 +50,58 @@ WallpaperShellLayerStrategy::WallpaperShellLayerStrategy(
d->wallpaper_layer = std::move(wallpaper_layer);
if (d->wallpaper_layer) {
d->wallpaper_layer->setImageChangedCallback([this]() { onWallpaperChanged(); });
// The engine drives timed rotation; on each tick it asks us to begin a
// transition. It needs a non-owning layer pointer only for its size guard.
wallpaper::WallPaperLayer* raw = d->wallpaper_layer.get();
d->engine = std::make_unique<wallpaper::WallPaperEngine>(
raw, [this](wallpaper::SwitchingMode mode) { beginTransition(mode); }, nullptr);
}
log::infoftag("WallpaperShellLayerStrategy", "Created with wallpaper layer");
log::infoftag(kLogTag, "Created with wallpaper layer and rotation engine");
}

WallpaperShellLayerStrategy::~WallpaperShellLayerStrategy() = default;

void WallpaperShellLayerStrategy::activate(aex::WeakPtr<IShellLayer> layer,
aex::WeakPtr<WindowManager> wm) {
log::traceftag("WallpaperShellLayerStrategy", "Activated");
log::traceftag(kLogTag, "Activated");
d->layer = layer;
d->window_manager = wm;

// Load initial image if available
// Load initial image if available.
if (d->wallpaper_layer && !d->wallpaper_layer->currentImage().isNull()) {
rescaleImage();
}
if (d->engine) {
d->engine->start();
}
}

void WallpaperShellLayerStrategy::deactivate() {
log::traceftag("WallpaperShellLayerStrategy", "Deactivated");
log::traceftag(kLogTag, "Deactivated");
if (d->engine) {
d->engine->stop();
}
resetTransition();
d->layer.Reset();
d->window_manager.Reset();
d->cached_scaled_image = {};
}

void WallpaperShellLayerStrategy::onGeometryChanged(const QRect& available) {
log::traceftag("WallpaperShellLayerStrategy", "Geometry changed: QRect({}, {}, {}, {})",
available.x(), available.y(), available.width(), available.height());
log::traceftag(kLogTag, "Geometry changed: QRect({}, {}, {}, {})", available.x(), available.y(),
available.width(), available.height());
if (d->current_geometry == available) {
return;
}
// A geometry change invalidates the pre-scaled transition buffers; abort
// any in-flight transition, then re-scale the current image for the new
// geometry (handled below by rescaleImage()).
if (d->transitioning) {
resetTransition();
}
d->current_geometry = available;
rescaleImage();
if (auto l = d->layer.Get()) {
l->requestRepaint();
}
requestLayerRepaint();
}

QImage WallpaperShellLayerStrategy::currentBackgroundImage() const {
Expand Down Expand Up @@ -97,7 +132,7 @@ void WallpaperShellLayerStrategy::rescaleImage() {
QImage scaled;
switch (mode) {
case wallpaper::ScalingMode::Fill: {
// Scale to cover, then crop center
// Scale to cover, then crop center.
QSize scaled_size = raw.size().scaled(target, Qt::KeepAspectRatioByExpanding);
scaled = raw.scaled(scaled_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
int x = (scaled.width() - target.width()) / 2;
Expand All @@ -106,10 +141,10 @@ void WallpaperShellLayerStrategy::rescaleImage() {
return;
}
case wallpaper::ScalingMode::Fit: {
// Scale to fit within, preserving aspect ratio
// Scale to fit within, preserving aspect ratio.
QSize scaled_size = raw.size().scaled(target, Qt::KeepAspectRatio);
scaled = raw.scaled(scaled_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
// Create background with letterbox bars
// Create background with letterbox bars.
QImage result(target, QImage::Format_RGB32);
result.fill(d->wallpaper_layer->backgroundColor());
QPainter painter(&result);
Expand All @@ -120,7 +155,7 @@ void WallpaperShellLayerStrategy::rescaleImage() {
return;
}
case wallpaper::ScalingMode::Stretch: {
// Scale to exact dimensions
// Scale to exact dimensions.
d->cached_scaled_image =
raw.scaled(target, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
return;
Expand All @@ -130,11 +165,123 @@ void WallpaperShellLayerStrategy::rescaleImage() {
}

void WallpaperShellLayerStrategy::onWallpaperChanged() {
log::traceftag("WallpaperShellLayerStrategy", "Wallpaper image changed, rescaling");
log::traceftag(kLogTag, "Wallpaper image changed, rescaling");
if (d->transitioning) {
// The layer now holds the new image: re-scale it into the cache, stash
// it as the incoming frame, and start the per-frame animation.
rescaleImage();
d->current_scaled = d->cached_scaled_image;
if (d->current_scaled.isNull()) {
// Target image failed to load: abort and restore the outgoing frame.
log::warningftag(kLogTag, "Transition target image is null; aborting transition");
d->cached_scaled_image = d->previous_scaled;
resetTransition();
requestLayerRepaint();
return;
}
startTransitionAnim();
return;
}
// Instant (non-animated) path.
rescaleImage();
requestLayerRepaint();
}

void WallpaperShellLayerStrategy::requestLayerRepaint() {
if (auto l = d->layer.Get()) {
l->requestRepaint();
}
}

void WallpaperShellLayerStrategy::triggerNextWallpaper() {
if (d->engine) {
beginTransition(d->engine->mode());
}
}

void WallpaperShellLayerStrategy::beginTransition(wallpaper::SwitchingMode mode) {
if (d->transitioning) {
return; // Already mid-transition; ignore the new request.
}
if (!d->wallpaper_layer || d->cached_scaled_image.isNull()) {
return; // Nothing to transition from.
}
d->pending_mode = mode;
d->previous_scaled = d->cached_scaled_image;
d->transitioning = true;
if (!advanceLayerToNext()) {
// No switch occurred (e.g. storage shrank to <=1); roll back state.
d->transitioning = false;
d->previous_scaled = {};
}
// Otherwise onWallpaperChanged() (fired by the layer switch) takes over.
}

bool WallpaperShellLayerStrategy::advanceLayerToNext() {
if (!d->wallpaper_layer) {
return false;
}
auto& storage = d->wallpaper_layer->tokenStorage();
if (storage.size() <= 1) {
return false;
}
const auto ids = storage.tokenIds();
auto current = storage.current();
const wallpaper::wallpaper_token_id_t current_id = current ? current->id() : QString{};
const auto selector = d->engine ? d->engine->selector() : wallpaper::Selector::Sequential;
const wallpaper::wallpaper_token_id_t next_id =
wallpaper::selectNextWallpaper(ids, current_id, selector, d->rng);
if (next_id.isEmpty()) {
return false;
}
return d->wallpaper_layer->showTargetOne(next_id);
}

void WallpaperShellLayerStrategy::startTransitionAnim() {
d->anim = std::make_unique<QVariantAnimation>();
d->anim->setStartValue(qreal(0.0));
d->anim->setEndValue(qreal(1.0));
d->anim->setDuration(d->engine ? d->engine->animationDurationMs() : 2000);
d->anim->setEasingCurve(d->engine ? d->engine->easing() : QEasingCurve::InOutCubic);
QObject::connect(d->anim.get(), &QVariantAnimation::valueChanged,
[this](const QVariant& value) { composeFrame(value.toReal()); });
QObject::connect(d->anim.get(), &QVariantAnimation::finished, [this]() { finishTransition(); });
composeFrame(0.0); // Publish the first frame immediately.
d->anim->start();
}

void WallpaperShellLayerStrategy::composeFrame(qreal progress) {
if (!d->transitioning || d->cached_scaled_image.isNull()) {
return;
}
// Compose in-place into the cache buffer to avoid a per-frame QImage
// allocation (~8MB at 1080p). The first write detaches once; subsequent
// frames reuse the buffer in place.
wallpaper::composeTransitionFrameInto(d->cached_scaled_image, d->previous_scaled,
d->current_scaled, progress, d->pending_mode);
requestLayerRepaint();
}

void WallpaperShellLayerStrategy::finishTransition() {
if (!d->transitioning) {
return;
}
d->cached_scaled_image = d->current_scaled; // Settle on the new frame.
d->previous_scaled = {};
d->current_scaled = {};
d->transitioning = false;
d->anim.reset();
requestLayerRepaint();
}

void WallpaperShellLayerStrategy::resetTransition() {
if (d->anim) {
d->anim->stop();
}
d->anim.reset();
d->transitioning = false;
d->previous_scaled = {};
d->current_scaled = {};
}

} // namespace cf::desktop
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@
* image scaling on geometry changes and triggers repaints when the
* wallpaper image changes.
*
* A WallPaperEngine drives timed rotation. When a switch is due the
* strategy runs a per-frame transition: the outgoing frame is captured,
* the layer advances, and a QVariantAnimation blends old and new images
* into cached_scaled_image via composeTransitionFrame() until the new
* image is fully revealed.
*
* @author Charliechen114514 (chengh1922@mails.jlu.edu.cn)
* @date 2026-04-09
* @version 0.1
Expand All @@ -16,6 +22,7 @@

#pragma once
#include "../IShellLayerStrategy.h"
#include "../wallpaper/TransitionComposer.h"
#include <QColor>
#include <QImage>
#include <QRect>
Expand All @@ -28,7 +35,8 @@ class WindowManager;

namespace wallpaper {
class WallPaperLayer;
}
class WallPaperEngine;
} // namespace wallpaper

/**
* @brief Shell layer strategy with wallpaper background support.
Expand Down Expand Up @@ -122,6 +130,22 @@ class WallpaperShellLayerStrategy : public IShellLayerStrategy {
*/
QColor backgroundColor() const override;

/**
* @brief Manually triggers a transition to the next wallpaper.
*
* Uses the engine's configured switching mode and selector. Provided
* as the future hook for a wallpaper chooser UI; the same path is
* used by automatic timed rotation.
*
* @throws None.
*
* @note None.
* @warning None.
* @since 0.19
* @ingroup components
*/
void triggerNextWallpaper();

private:
/**
* @brief Re-scales the raw image to fit the current geometry.
Expand All @@ -133,10 +157,59 @@ class WallpaperShellLayerStrategy : public IShellLayerStrategy {
/**
* @brief Callback invoked when the wallpaper layer's image changes.
*
* Reloads the raw image, re-scales it, and requests a repaint.
* Reloads the raw image, re-scales it, and requests a repaint. During
* an active transition it instead captures the new frame and starts
* the per-frame animation.
*/
void onWallpaperChanged();

/**
* @brief Requests a repaint from the bound shell layer (no-op if unset).
*/
void requestLayerRepaint();

/**
* @brief Begins a transition to a new wallpaper.
*
* Captures the current frame as the outgoing image, arms the
* transition state, and advances the layer to the next wallpaper.
*
* @param[in] mode Compositing mode for this transition.
*/
void beginTransition(wallpaper::SwitchingMode mode);

/**
* @brief Advances the layer to the next wallpaper per the selector.
*
* @return True if the layer switched (and thus fired onWallpaperChanged);
* false if there was nothing to switch to.
*/
bool advanceLayerToNext();

/**
* @brief Builds and starts the per-frame transition animation.
*/
void startTransitionAnim();

/**
* @brief Composes and publishes one transition frame at @p progress.
*
* @param[in] progress Transition progress in [0, 1].
*/
void composeFrame(qreal progress);

/**
* @brief Finalizes a transition: settles on the new frame, clears state.
*/
void finishTransition();

/**
* @brief Aborts any in-flight transition and clears buffers.
*
* Used by deactivate() and on geometry changes mid-transition.
*/
void resetTransition();

struct Private;
std::unique_ptr<Private> d;
};
Expand Down
Loading
Loading