Skip to content

Commit 356667c

Browse files
committed
ipc/ui: support custom transition shaders and animation config
Adds the shader_path request (hyprpaper_wallpaper v2) so a custom fragment shader can be supplied per wallpaper apply, and an existing monitor wallpaper now transitions to the new path instead of replacing outright. - IPC::CWallpaperObject stores and forwards the shader path. - CConfigManager::getAnimationConfig reads fadeLayersIn/fadeLayersOut (enabled + duration) from hyprland.conf, cached per name. - CWallpaperTarget::transitionTo / replaceImmediate delegate to the image element; CUI reuses the existing target when one already exists for the monitor, animating only when an animation config is enabled. - Example custom shaders under shaders/ (circle-reveal, diagonal-wipe).
1 parent 20fc0fa commit 356667c

9 files changed

Lines changed: 239 additions & 4 deletions

File tree

hw-protocols/hyprpaper_core.xml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@
8888
<value idx="2" name="unknown_error" description="unknown error"/>
8989
</enum>
9090

91-
<object name="hyprpaper_wallpaper" version="1">
91+
<object name="hyprpaper_wallpaper" version="2">
9292
<description summary="wallpaper object">
9393
This is an object describing a wallpaper
9494
</description>
@@ -147,6 +147,14 @@
147147
Destroys this object.
148148
</description>
149149
</c2s>
150+
151+
<c2s name="shader_path">
152+
<description summary="Set a custom transition shader">
153+
Set a custom fragment shader used for the wallpaper transition animation.
154+
If unset, the default transition shader is used.
155+
</description>
156+
<arg name="shader_path" type="varchar" summary="absolute path to .frag shader file"/>
157+
</c2s>
150158
</object>
151159

152160
<object name="hyprpaper_status" version="2">

shaders/circle-reveal.frag

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
#version 300 es
2+
3+
precision highp float;
4+
5+
in vec2 v_texcoord;
6+
7+
uniform sampler2D tex1; // Start frame (fit pre-resolved in C++)
8+
uniform sampler2D tex2; // End frame (fit pre-resolved in C++)
9+
uniform float progress; // 0.0 = fully start, 1.0 = fully end
10+
uniform float alpha;
11+
12+
uniform vec2 fullSize; // Output size in pixels
13+
uniform vec2 randomPixel; // Circle center (0-1 normalized)
14+
uniform float u_duration; // Host transition duration in seconds (from hyprland config)
15+
16+
// Shader-defined visual duration override: the circle effect completes in 0.5s
17+
// regardless of the host's transition duration. The host's u_duration is used
18+
// to remap progress so the effect fits within this window.
19+
const float SHADER_DURATION = 0.5;
20+
21+
layout(location = 0) out vec4 fragColor;
22+
23+
void main() {
24+
// Remap progress so the effect completes in SHADER_DURATION seconds.
25+
float p = (u_duration > 0.0)
26+
? clamp(progress * (u_duration / SHADER_DURATION), 0.0, 1.0)
27+
: progress;
28+
29+
vec4 startColor = texture(tex1, v_texcoord);
30+
vec4 endColor = texture(tex2, v_texcoord);
31+
32+
// Circle reveal: a radial mask expanding from randomPixel.
33+
vec2 pixelPos = v_texcoord * fullSize;
34+
vec2 centerPixel = randomPixel * fullSize;
35+
36+
float dist = distance(pixelPos, centerPixel);
37+
38+
// Farthest corner from the center, in pixel space (aspect-ratio correct).
39+
float maxDist = 0.0;
40+
maxDist = max(maxDist, distance(vec2(0.0, 0.0), centerPixel));
41+
maxDist = max(maxDist, distance(vec2(fullSize.x, 0.0), centerPixel));
42+
maxDist = max(maxDist, distance(vec2(0.0, fullSize.y), centerPixel));
43+
maxDist = max(maxDist, distance(fullSize, centerPixel));
44+
45+
float normalizedDist = dist / maxDist;
46+
47+
// circleMask: 0 = show start, 1 = show end.
48+
float edgeWidth = 0.02;
49+
float circleMask = smoothstep(p - edgeWidth, p + edgeWidth, normalizedDist);
50+
51+
vec4 blended = mix(endColor, startColor, circleMask);
52+
53+
fragColor = blended * alpha;
54+
}

shaders/diagonal-wipe.frag

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
#version 300 es
2+
3+
precision highp float;
4+
5+
in vec2 v_texcoord;
6+
7+
uniform sampler2D tex1; // Start frame (fit pre-resolved in C++)
8+
uniform sampler2D tex2; // End frame (fit pre-resolved in C++)
9+
uniform float progress; // 0.0 = fully start, 1.0 = fully end
10+
uniform float alpha;
11+
12+
layout(location = 0) out vec4 fragColor;
13+
14+
void main() {
15+
vec4 startColor = texture(tex1, v_texcoord);
16+
vec4 endColor = texture(tex2, v_texcoord);
17+
18+
// Diagonal wipe expanding from the top-left corner as progress increases.
19+
float diagonal = (v_texcoord.x + v_texcoord.y) * 0.5;
20+
float edgeWidth = 0.05;
21+
22+
// wipeMask: 0 = show end, 1 = show start.
23+
float wipeMask = smoothstep(progress - edgeWidth, progress + edgeWidth, diagonal);
24+
25+
vec4 blended = mix(endColor, startColor, wipeMask);
26+
27+
fragColor = blended * alpha;
28+
}

src/config/ConfigManager.cpp

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@
33
#include <filesystem>
44
#include <glob.h>
55
#include <random>
6+
#include <sstream>
67
#include <hyprlang.hpp>
78
#include <hyprutils/path/Path.hpp>
89
#include <hyprutils/string/String.hpp>
910
#include <hyprutils/utils/ScopeGuard.hpp>
1011
#include <string>
1112
#include "../helpers/Logger.hpp"
13+
#include "../ipc/HyprlandSocket.hpp"
1214
#include "WallpaperMatcher.hpp"
1315

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

291293
return result;
292294
}
295+
296+
CConfigManager::SAnimationConfig CConfigManager::getAnimationConfig(const std::string& name) {
297+
if (m_animationCache.contains(name))
298+
return m_animationCache[name];
299+
300+
SAnimationConfig config;
301+
302+
// Query hyprland's live animation config over its IPC socket — the same channel
303+
// hyprctl uses — rather than parsing hyprland.conf (deprecated, and blind to
304+
// runtime `keyword` overrides). The reply lists one block per animation:
305+
// name: fadeLayersIn
306+
// overriden: 0
307+
// bezier: almostLinear
308+
// enabled: 1
309+
// speed: 1.79
310+
// style: popin
311+
const auto REPLY = HyprlandSocket::getFromSocket("animations");
312+
if (!REPLY) {
313+
g_logger->log(LOG_WARN, "Could not query hyprland animations over IPC: {}", REPLY.error());
314+
m_animationCache[name] = config;
315+
return config;
316+
}
317+
318+
try {
319+
std::istringstream stream(*REPLY);
320+
std::string line;
321+
bool inBlock = false;
322+
323+
while (std::getline(stream, line)) {
324+
const auto TRIMMED = Hyprutils::String::trim(line);
325+
326+
if (TRIMMED.starts_with("beziers:"))
327+
break;
328+
329+
if (TRIMMED.starts_with("name:")) {
330+
inBlock = Hyprutils::String::trim(TRIMMED.substr(5)) == name;
331+
continue;
332+
}
333+
334+
if (!inBlock)
335+
continue;
336+
337+
if (TRIMMED.starts_with("enabled:"))
338+
config.enabled = std::stoi(Hyprutils::String::trim(TRIMMED.substr(8))) != 0;
339+
else if (TRIMMED.starts_with("speed:"))
340+
config.duration = std::stof(Hyprutils::String::trim(TRIMMED.substr(6)));
341+
}
342+
} catch (const std::exception& e) {
343+
g_logger->log(LOG_ERR, "Error parsing hyprland animation config: {}", e.what());
344+
}
345+
346+
m_animationCache[name] = config;
347+
return config;
348+
}

src/config/ConfigManager.hpp

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
#include "../helpers/Memory.hpp"
44
#include <hyprlang.hpp>
55
#include <vector>
6+
#include <string>
7+
#include <unordered_map>
68

79
class CConfigManager {
810
public:
@@ -19,6 +21,12 @@ class CConfigManager {
1921
std::string order = "default";
2022
int timeout = 0;
2123
uint32_t id = 0;
24+
std::string shaderPath;
25+
};
26+
27+
struct SAnimationConfig {
28+
bool enabled = true;
29+
float duration = 1.0f;
2230
};
2331

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

3139
const std::string& getCurrentConfigPath() const;
3240

41+
// Queries hyprland's live animation settings over its IPC socket.
42+
SAnimationConfig getAnimationConfig(const std::string& name);
43+
3344
private:
3445
Hyprlang::CConfig m_config;
3546

3647
std::string m_currentConfigPath;
48+
49+
std::unordered_map<std::string, SAnimationConfig> m_animationCache;
3750
};
3851

3952
inline UP<CConfigManager> g_config;

src/ipc/IPC.cpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,13 @@ CWallpaperObject::CWallpaperObject(SP<CHyprpaperWallpaperObject>&& obj) : m_obje
4646

4747
apply();
4848
});
49+
50+
m_object->setShaderPath([this](const char* s) {
51+
if (m_inert)
52+
m_object->error(HYPRPAPER_CORE_WALLPAPER_ERRORS_INERT_WALLPAPER_OBJECT, "Object is inert");
53+
54+
m_shaderPath = s;
55+
});
4956
}
5057

5158
static std::string fitModeToStr(hyprpaperCoreWallpaperFitMode m) {
@@ -87,6 +94,7 @@ void CWallpaperObject::apply() {
8794
.monitor = std::move(m_monitor),
8895
.fitMode = fitModeToStr(m_fitMode),
8996
.paths = std::vector{std::move(m_path)},
97+
.shaderPath = std::move(m_shaderPath),
9098
});
9199

92100
m_object->sendSuccess();

src/ipc/IPC.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ namespace IPC {
1919
std::string m_path;
2020
hyprpaperCoreWallpaperFitMode m_fitMode = HYPRPAPER_CORE_WALLPAPER_FIT_MODE_COVER;
2121
std::string m_monitor;
22+
std::string m_shaderPath;
2223

2324
bool m_inert = false;
2425
};

src/ui/UI.cpp

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#include "../config/WallpaperMatcher.hpp"
88

99
#include <algorithm>
10+
#include <fstream>
1011
#include <random>
1112
#include <hyprtoolkit/core/Output.hpp>
1213

@@ -143,6 +144,37 @@ void CWallpaperTarget::onRepeatTimer() {
143144
IPC::g_IPCSocket->onWallpaperChanged(m_monitorName, m_lastPath);
144145
}
145146

147+
void CWallpaperTarget::transitionTo(const std::string& path,
148+
Hyprtoolkit::eImageFitMode fitMode,
149+
float duration,
150+
const std::string& shaderPath) {
151+
m_lastPath = path;
152+
153+
std::string shaderSource;
154+
if (!shaderPath.empty()) {
155+
std::ifstream file(shaderPath);
156+
if (file.is_open())
157+
shaderSource = std::string((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
158+
else
159+
g_logger->log(LOG_ERR, "Could not open transition shader: {}", shaderPath);
160+
}
161+
162+
m_image->transitionTo(path, fitMode, duration, shaderSource);
163+
}
164+
165+
void CWallpaperTarget::replaceImmediate(const std::string& path,
166+
Hyprtoolkit::eImageFitMode fitMode) {
167+
m_lastPath = path;
168+
169+
m_image->rebuild()
170+
->path(std::string{path})
171+
->size({Hyprtoolkit::CDynamicSize::HT_SIZE_PERCENT,
172+
Hyprtoolkit::CDynamicSize::HT_SIZE_PERCENT, {1.F, 1.F}})
173+
->sync(true)
174+
->fitMode(fitMode)
175+
->commence();
176+
}
177+
146178
void CUI::registerOutput(const SP<Hyprtoolkit::IOutput>& mon) {
147179
g_matcher->registerOutput(mon->port(), pruneDesc(mon->desc()));
148180
if (IPC::g_IPCSocket)
@@ -237,11 +269,37 @@ void CUI::targetChanged(const SP<Hyprtoolkit::IOutput>& mon) {
237269
return;
238270
}
239271

240-
std::erase_if(m_targets, [&mon](const auto& e) { return e->m_monitorName == mon->port(); });
241-
242-
m_targets.emplace_back(makeShared<CWallpaperTarget>(m_backend, mon, TARGET->get().paths, toFitMode(TARGET->get().fitMode), TARGET->get().timeout, TARGET->get().order));
272+
auto existingTarget = findTarget(mon->port());
273+
274+
if (existingTarget) {
275+
auto outConfig = g_config->getAnimationConfig("fadeLayersOut");
276+
auto inConfig = g_config->getAnimationConfig("fadeLayersIn");
277+
278+
if (inConfig.enabled || outConfig.enabled) {
279+
existingTarget->transitionTo(
280+
TARGET->get().paths.front(),
281+
toFitMode(TARGET->get().fitMode),
282+
std::max(outConfig.duration, inConfig.duration),
283+
TARGET->get().shaderPath);
284+
} else {
285+
// Animations disabled, swap without a transition.
286+
existingTarget->replaceImmediate(
287+
TARGET->get().paths.front(),
288+
toFitMode(TARGET->get().fitMode));
289+
}
290+
} else {
291+
m_targets.emplace_back(makeShared<CWallpaperTarget>(m_backend, mon, TARGET->get().paths, toFitMode(TARGET->get().fitMode), TARGET->get().timeout, TARGET->get().order));
292+
}
243293
}
244294

245295
const std::vector<SP<CWallpaperTarget>>& CUI::targets() {
246296
return m_targets;
247297
}
298+
299+
SP<CWallpaperTarget> CUI::findTarget(const std::string& monitorName) {
300+
for (const auto& target : m_targets) {
301+
if (target->m_monitorName == monitorName)
302+
return target;
303+
}
304+
return nullptr;
305+
}

src/ui/UI.hpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,14 @@ class CWallpaperTarget {
2626

2727
std::string m_monitorName, m_lastPath;
2828

29+
void transitionTo(const std::string& path,
30+
Hyprtoolkit::eImageFitMode fitMode,
31+
float duration,
32+
const std::string& shaderPath = "");
33+
34+
void replaceImmediate(const std::string& path,
35+
Hyprtoolkit::eImageFitMode fitMode);
36+
2937
private:
3038
void onRepeatTimer();
3139

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

5867
SP<Hyprtoolkit::IBackend> m_backend;
5968

0 commit comments

Comments
 (0)