Skip to content
Open
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
10 changes: 9 additions & 1 deletion hw-protocols/hyprpaper_core.xml
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@
<value idx="2" name="unknown_error" description="unknown error"/>
</enum>

<object name="hyprpaper_wallpaper" version="1">
<object name="hyprpaper_wallpaper" version="2">
<description summary="wallpaper object">
This is an object describing a wallpaper
</description>
Expand Down Expand Up @@ -147,6 +147,14 @@
Destroys this object.
</description>
</c2s>

<c2s name="shader_path">
<description summary="Set a custom transition shader">
Set a custom fragment shader used for the wallpaper transition animation.
If unset, the default transition shader is used.
</description>
<arg name="shader_path" type="varchar" summary="absolute path to .frag shader file"/>
</c2s>
</object>

<object name="hyprpaper_status" version="2">
Expand Down
54 changes: 54 additions & 0 deletions shaders/circle-reveal.frag
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
#version 300 es

precision highp float;

in vec2 v_texcoord;

uniform sampler2D tex1; // Start frame (fit pre-resolved in C++)
uniform sampler2D tex2; // End frame (fit pre-resolved in C++)
uniform float progress; // 0.0 = fully start, 1.0 = fully end
uniform float alpha;

uniform vec2 fullSize; // Output size in pixels
uniform vec2 randomPixel; // Circle center (0-1 normalized)
uniform float u_duration; // Host transition duration in seconds (from hyprland config)

// Shader-defined visual duration override: the circle effect completes in 0.5s
// regardless of the host's transition duration. The host's u_duration is used
// to remap progress so the effect fits within this window.
const float SHADER_DURATION = 0.5;

layout(location = 0) out vec4 fragColor;

void main() {
// Remap progress so the effect completes in SHADER_DURATION seconds.
float p = (u_duration > 0.0)
? clamp(progress * (u_duration / SHADER_DURATION), 0.0, 1.0)
: progress;

vec4 startColor = texture(tex1, v_texcoord);
vec4 endColor = texture(tex2, v_texcoord);

// Circle reveal: a radial mask expanding from randomPixel.
vec2 pixelPos = v_texcoord * fullSize;
vec2 centerPixel = randomPixel * fullSize;

float dist = distance(pixelPos, centerPixel);

// Farthest corner from the center, in pixel space (aspect-ratio correct).
float maxDist = 0.0;
maxDist = max(maxDist, distance(vec2(0.0, 0.0), centerPixel));
maxDist = max(maxDist, distance(vec2(fullSize.x, 0.0), centerPixel));
maxDist = max(maxDist, distance(vec2(0.0, fullSize.y), centerPixel));
maxDist = max(maxDist, distance(fullSize, centerPixel));

float normalizedDist = dist / maxDist;

// circleMask: 0 = show start, 1 = show end.
float edgeWidth = 0.02;
float circleMask = smoothstep(p - edgeWidth, p + edgeWidth, normalizedDist);

vec4 blended = mix(endColor, startColor, circleMask);

fragColor = blended * alpha;
}
28 changes: 28 additions & 0 deletions shaders/diagonal-wipe.frag
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#version 300 es

precision highp float;

in vec2 v_texcoord;

uniform sampler2D tex1; // Start frame (fit pre-resolved in C++)
uniform sampler2D tex2; // End frame (fit pre-resolved in C++)
uniform float progress; // 0.0 = fully start, 1.0 = fully end
uniform float alpha;

layout(location = 0) out vec4 fragColor;

void main() {
vec4 startColor = texture(tex1, v_texcoord);
vec4 endColor = texture(tex2, v_texcoord);

// Diagonal wipe expanding from the top-left corner as progress increases.
float diagonal = (v_texcoord.x + v_texcoord.y) * 0.5;
float edgeWidth = 0.05;

// wipeMask: 0 = show end, 1 = show start.
float wipeMask = smoothstep(progress - edgeWidth, progress + edgeWidth, diagonal);

vec4 blended = mix(endColor, startColor, wipeMask);

fragColor = blended * alpha;
}
56 changes: 56 additions & 0 deletions src/config/ConfigManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@
#include <filesystem>
#include <glob.h>
#include <random>
#include <sstream>
#include <hyprlang.hpp>
#include <hyprutils/path/Path.hpp>
#include <hyprutils/string/String.hpp>
#include <hyprutils/utils/ScopeGuard.hpp>
#include <string>
#include "../helpers/Logger.hpp"
#include "../ipc/HyprlandSocket.hpp"
#include "WallpaperMatcher.hpp"

#include <magic.h>
Expand Down Expand Up @@ -290,3 +292,57 @@ static Hyprlang::CParseResult handleSource(const char* COMMAND, const char* VALU

return result;
}

CConfigManager::SAnimationConfig CConfigManager::getAnimationConfig(const std::string& name) {
if (m_animationCache.contains(name))
return m_animationCache[name];

SAnimationConfig config;

// Query hyprland's live animation config over its IPC socket — the same channel
// hyprctl uses — rather than parsing hyprland.conf (deprecated, and blind to
// runtime `keyword` overrides). The reply lists one block per animation:
// name: fadeLayersIn
// overriden: 0
// bezier: almostLinear
// enabled: 1
// speed: 1.79
// style: popin
const auto REPLY = HyprlandSocket::getFromSocket("animations");
if (!REPLY) {
g_logger->log(LOG_WARN, "Could not query hyprland animations over IPC: {}", REPLY.error());
m_animationCache[name] = config;
return config;
}

try {
std::istringstream stream(*REPLY);
std::string line;
bool inBlock = false;

while (std::getline(stream, line)) {
const auto TRIMMED = Hyprutils::String::trim(line);

if (TRIMMED.starts_with("beziers:"))
break;

if (TRIMMED.starts_with("name:")) {
inBlock = Hyprutils::String::trim(TRIMMED.substr(5)) == name;
continue;
}

if (!inBlock)
continue;

if (TRIMMED.starts_with("enabled:"))
config.enabled = std::stoi(Hyprutils::String::trim(TRIMMED.substr(8))) != 0;
else if (TRIMMED.starts_with("speed:"))
config.duration = std::stof(Hyprutils::String::trim(TRIMMED.substr(6)));
}
} catch (const std::exception& e) {
g_logger->log(LOG_ERR, "Error parsing hyprland animation config: {}", e.what());
}

m_animationCache[name] = config;
return config;
}
13 changes: 13 additions & 0 deletions src/config/ConfigManager.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
#include "../helpers/Memory.hpp"
#include <hyprlang.hpp>
#include <vector>
#include <string>
#include <unordered_map>

class CConfigManager {
public:
Expand All @@ -19,6 +21,12 @@ class CConfigManager {
std::string order = "default";
int timeout = 0;
uint32_t id = 0;
std::string shaderPath;
};

struct SAnimationConfig {
bool enabled = true;
float duration = 1.0f;
};

constexpr static const uint32_t SETTING_INVALID = 0;
Expand All @@ -30,10 +38,15 @@ class CConfigManager {

const std::string& getCurrentConfigPath() const;

// Queries hyprland's live animation settings over its IPC socket.
SAnimationConfig getAnimationConfig(const std::string& name);

private:
Hyprlang::CConfig m_config;

std::string m_currentConfigPath;

std::unordered_map<std::string, SAnimationConfig> m_animationCache;
};

inline UP<CConfigManager> g_config;
8 changes: 8 additions & 0 deletions src/ipc/IPC.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,13 @@ CWallpaperObject::CWallpaperObject(SP<CHyprpaperWallpaperObject>&& obj) : m_obje

apply();
});

m_object->setShaderPath([this](const char* s) {
if (m_inert)
m_object->error(HYPRPAPER_CORE_WALLPAPER_ERRORS_INERT_WALLPAPER_OBJECT, "Object is inert");

m_shaderPath = s;
});
}

static std::string fitModeToStr(hyprpaperCoreWallpaperFitMode m) {
Expand Down Expand Up @@ -87,6 +94,7 @@ void CWallpaperObject::apply() {
.monitor = std::move(m_monitor),
.fitMode = fitModeToStr(m_fitMode),
.paths = std::vector{std::move(m_path)},
.shaderPath = std::move(m_shaderPath),
});

m_object->sendSuccess();
Expand Down
1 change: 1 addition & 0 deletions src/ipc/IPC.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ namespace IPC {
std::string m_path;
hyprpaperCoreWallpaperFitMode m_fitMode = HYPRPAPER_CORE_WALLPAPER_FIT_MODE_COVER;
std::string m_monitor;
std::string m_shaderPath;

bool m_inert = false;
};
Expand Down
64 changes: 61 additions & 3 deletions src/ui/UI.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include "../config/WallpaperMatcher.hpp"

#include <algorithm>
#include <fstream>
#include <random>
#include <hyprtoolkit/core/Output.hpp>

Expand Down Expand Up @@ -143,6 +144,37 @@ void CWallpaperTarget::onRepeatTimer() {
IPC::g_IPCSocket->onWallpaperChanged(m_monitorName, m_lastPath);
}

void CWallpaperTarget::transitionTo(const std::string& path,
Hyprtoolkit::eImageFitMode fitMode,
float duration,
const std::string& shaderPath) {
m_lastPath = path;

std::string shaderSource;
if (!shaderPath.empty()) {
std::ifstream file(shaderPath);
if (file.is_open())
shaderSource = std::string((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
else
g_logger->log(LOG_ERR, "Could not open transition shader: {}", shaderPath);
}

m_image->transitionTo(path, fitMode, duration, shaderSource);
}

void CWallpaperTarget::replaceImmediate(const std::string& path,
Hyprtoolkit::eImageFitMode fitMode) {
m_lastPath = path;

m_image->rebuild()
->path(std::string{path})
->size({Hyprtoolkit::CDynamicSize::HT_SIZE_PERCENT,
Hyprtoolkit::CDynamicSize::HT_SIZE_PERCENT, {1.F, 1.F}})
->sync(true)
->fitMode(fitMode)
->commence();
}

void CUI::registerOutput(const SP<Hyprtoolkit::IOutput>& mon) {
g_matcher->registerOutput(mon->port(), pruneDesc(mon->desc()));
if (IPC::g_IPCSocket)
Expand Down Expand Up @@ -237,11 +269,37 @@ void CUI::targetChanged(const SP<Hyprtoolkit::IOutput>& mon) {
return;
}

std::erase_if(m_targets, [&mon](const auto& e) { return e->m_monitorName == mon->port(); });

m_targets.emplace_back(makeShared<CWallpaperTarget>(m_backend, mon, TARGET->get().paths, toFitMode(TARGET->get().fitMode), TARGET->get().timeout, TARGET->get().order));
auto existingTarget = findTarget(mon->port());

if (existingTarget) {
auto outConfig = g_config->getAnimationConfig("fadeLayersOut");
auto inConfig = g_config->getAnimationConfig("fadeLayersIn");

if (inConfig.enabled || outConfig.enabled) {
existingTarget->transitionTo(
TARGET->get().paths.front(),
toFitMode(TARGET->get().fitMode),
std::max(outConfig.duration, inConfig.duration),
TARGET->get().shaderPath);
} else {
// Animations disabled, swap without a transition.
existingTarget->replaceImmediate(
TARGET->get().paths.front(),
toFitMode(TARGET->get().fitMode));
}
} else {
m_targets.emplace_back(makeShared<CWallpaperTarget>(m_backend, mon, TARGET->get().paths, toFitMode(TARGET->get().fitMode), TARGET->get().timeout, TARGET->get().order));
}
}

const std::vector<SP<CWallpaperTarget>>& CUI::targets() {
return m_targets;
}

SP<CWallpaperTarget> CUI::findTarget(const std::string& monitorName) {
for (const auto& target : m_targets) {
if (target->m_monitorName == monitorName)
return target;
}
return nullptr;
}
9 changes: 9 additions & 0 deletions src/ui/UI.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@ class CWallpaperTarget {

std::string m_monitorName, m_lastPath;

void transitionTo(const std::string& path,
Hyprtoolkit::eImageFitMode fitMode,
float duration,
const std::string& shaderPath = "");

void replaceImmediate(const std::string& path,
Hyprtoolkit::eImageFitMode fitMode);

private:
void onRepeatTimer();

Expand Down Expand Up @@ -54,6 +62,7 @@ class CUI {
void targetChanged(const SP<Hyprtoolkit::IOutput>& mon);
void targetChanged(const std::string_view& monName);
void registerOutput(const SP<Hyprtoolkit::IOutput>& mon);
SP<CWallpaperTarget> findTarget(const std::string& monitorName);

SP<Hyprtoolkit::IBackend> m_backend;

Expand Down