feat(wallpaper): add support for solid color backgrounds

This commit is contained in:
Ly-sec
2026-04-26 22:57:37 +02:00
parent 5ef66eb203
commit 27e770bcc5
17 changed files with 131 additions and 10 deletions
+1
View File
@@ -9,6 +9,7 @@ A ready-to-use starting config with all defaults is at [`example.toml`](example.
Notification daemon toggle: use `[notification].enable_daemon` (documented in [`config/services.md`](config/services.md)).
Weather location visibility toggle: use `[shell].show_location` (documented in [`config/shell.md`](config/shell.md)).
Wallpaper automation: use `[wallpaper.automation]` (documented in [`config/wallpaper.md`](config/wallpaper.md)).
Wallpaper single-color fallback/fill: use `[wallpaper].fill_color` (documented in [`config/wallpaper.md`](config/wallpaper.md)).
---
+3
View File
@@ -4,6 +4,7 @@
[wallpaper]
enabled = true
fill_mode = "crop" # center | crop | fit | stretch | repeat
fill_color = "#111111" # optional fallback/fill color; image wallpapers take priority
transition = ["fade", "wipe", "disc", "stripes", "zoom", "honeycomb"]
# array of effects picked at random each transition
# omit to use all effects
@@ -32,6 +33,8 @@ directory_dark = "/home/user/Wallpapers/Vertical/Dark"
The wallpaper picker panel lists images in `directory` as a grid of thumbnails. Selecting a monitor in the panel toolbar switches to that monitor's override directory (falling back to the base `directory`). Clicking a tile writes the path to `state.toml` and applies it immediately. Picking a wallpaper while **ALL** is selected applies it to every connected output.
`fill_color` accepts hex colors (`#RGB`, `#RGBA`, `#RRGGBB`, `#RRGGBBAA`) or theme role names such as `surface` and `primary`. It is used behind image wallpapers, in uncovered areas for `center`/`fit`, and as the full wallpaper when no image path is selected.
When automation is enabled, Noctalia picks one image from `directory` on the configured interval and applies it to all connected outputs in sync.
`order = "random"` chooses a random image each cycle.
`order = "alphabetical"` sorts paths case-insensitively and advances to the next image each cycle (wrapping at the end).
+1
View File
@@ -31,6 +31,7 @@ alpha = 0.55 # multiplied by each component's background
[wallpaper]
enabled = true
fill_mode = "crop" # center | crop | fit | stretch | repeat
fill_color = "" # optional fallback/fill color: #RRGGBB, #RRGGBBAA, or theme role
transition = ["fade", "wipe", "disc", "stripes", "zoom", "honeycomb"]
transition_duration = 1500 # milliseconds
edge_smoothness = 0.3
+13 -1
View File
@@ -243,7 +243,12 @@ namespace {
constexpr Logger kLog("config");
ThemeColor themeColorFromRoleString(const std::string& raw) {
if (auto role = colorRoleFromToken(raw)) {
const std::string trimmed = StringUtils::trim(raw);
Color fixed;
if (tryParseHexColor(trimmed, fixed)) {
return fixedColor(fixed);
}
if (auto role = colorRoleFromToken(trimmed)) {
return roleColor(*role);
}
kLog.warn("unknown theme color role \"{}\", using surface_variant", raw);
@@ -1357,6 +1362,13 @@ void ConfigService::parseTable(const toml::table& tbl) {
else if (*v == "repeat")
wp.fillMode = WallpaperFillMode::Repeat;
}
if (auto v = (*wpTbl)["fill_color"].value<std::string>()) {
if (StringUtils::trim(*v).empty()) {
wp.fillColor = std::nullopt;
} else {
wp.fillColor = themeColorFromConfigString(*v);
}
}
auto parseTransition = [](const std::string& s) -> std::optional<WallpaperTransition> {
if (s == "fade")
return WallpaperTransition::Fade;
+1
View File
@@ -183,6 +183,7 @@ struct WallpaperAutomationConfig {
struct WallpaperConfig {
bool enabled = true;
WallpaperFillMode fillMode = WallpaperFillMode::Crop;
std::optional<ThemeColor> fillColor;
std::vector<WallpaperTransition> transitions = {WallpaperTransition::Fade, WallpaperTransition::Wipe,
WallpaperTransition::Disc, WallpaperTransition::Stripes,
WallpaperTransition::Zoom, WallpaperTransition::Honeycomb};
+2 -2
View File
@@ -375,7 +375,7 @@ void WallpaperProgram::initProgram(std::size_t index, const char* fragSource) {
void WallpaperProgram::draw(WallpaperTransition type, GLuint texture1, GLuint texture2, float surfaceWidth,
float surfaceHeight, float quadWidth, float quadHeight, float imageWidth1,
float imageHeight1, float imageWidth2, float imageHeight2, float progress, float fillMode,
const TransitionParams& params, const Mat3& transform) const {
const TransitionParams& params, const Color& fillColor, const Mat3& transform) const {
auto idx = static_cast<std::size_t>(type);
if (idx >= kTransitionCount || !m_programs[idx].program.isValid() || texture1 == 0 || quadWidth <= 0.0f ||
quadHeight <= 0.0f) {
@@ -421,7 +421,7 @@ void WallpaperProgram::draw(WallpaperTransition type, GLuint texture1, GLuint te
if (pd.screenHeightLoc >= 0)
glUniform1f(pd.screenHeightLoc, quadHeight);
if (pd.fillColorLoc >= 0)
glUniform4f(pd.fillColorLoc, 0.0f, 0.0f, 0.0f, 1.0f);
glUniform4f(pd.fillColorLoc, fillColor.r, fillColor.g, fillColor.b, fillColor.a);
// Per-transition uniforms
if (pd.directionLoc >= 0)
+2 -1
View File
@@ -1,6 +1,7 @@
#pragma once
#include "config/config_service.h"
#include "render/core/color.h"
#include "render/core/mat3.h"
#include "render/core/shader_program.h"
@@ -33,7 +34,7 @@ public:
void draw(WallpaperTransition type, GLuint texture1, GLuint texture2, float surfaceWidth, float surfaceHeight,
float quadWidth, float quadHeight, float imageWidth1, float imageHeight1, float imageWidth2,
float imageHeight2, float progress, float fillMode, const TransitionParams& params,
const Mat3& transform = Mat3::identity()) const;
const Color& fillColor = rgba(0.0f, 0.0f, 0.0f, 1.0f), const Mat3& transform = Mat3::identity()) const;
private:
static constexpr std::size_t kTransitionCount = 6;
+1 -1
View File
@@ -298,7 +298,7 @@ void RenderContext::renderNode(const Node* node, const Mat3& parentTransform, fl
m_wallpaperProgram.draw(wallpaper->transition(), wallpaper->texture1(), texture2, sw, sh, node->width(),
node->height(), wallpaper->imageWidth1(), wallpaper->imageHeight1(), imageWidth2,
imageHeight2, progress, static_cast<float>(wallpaper->fillMode()),
wallpaper->transitionParams(), worldTransform);
wallpaper->transitionParams(), wallpaper->fillColor(), worldTransform);
}
break;
}
+10
View File
@@ -19,6 +19,7 @@ public:
[[nodiscard]] float progress() const noexcept { return m_progress; }
[[nodiscard]] WallpaperTransition transition() const noexcept { return m_transition; }
[[nodiscard]] WallpaperFillMode fillMode() const noexcept { return m_fillMode; }
[[nodiscard]] const Color& fillColor() const noexcept { return m_fillColor; }
[[nodiscard]] const TransitionParams& transitionParams() const noexcept { return m_params; }
void setTextures(std::uint32_t texture1, std::uint32_t texture2, float imageWidth1, float imageHeight1,
@@ -58,6 +59,14 @@ public:
markPaintDirty();
}
void setFillColor(const Color& fillColor) {
if (m_fillColor == fillColor) {
return;
}
m_fillColor = fillColor;
markPaintDirty();
}
private:
std::uint32_t m_texture1 = 0;
std::uint32_t m_texture2 = 0;
@@ -68,5 +77,6 @@ private:
float m_progress = 0.0f;
WallpaperTransition m_transition = WallpaperTransition::Fade;
WallpaperFillMode m_fillMode = WallpaperFillMode::Crop;
Color m_fillColor = rgba(0.0f, 0.0f, 0.0f, 1.0f);
TransitionParams m_params;
};
+2 -2
View File
@@ -99,7 +99,7 @@ void WallpaperRenderer::render() {
float progress = (m_tex2 != 0) ? m_progress : 0.0f;
m_program.draw(m_transition, m_tex1, tex2, sw, sh, sw, sh, m_imgW1, m_imgH1, m_imgW2, m_imgH2, progress,
static_cast<float>(m_fillMode), m_params);
static_cast<float>(m_fillMode), m_params, m_fillColor);
eglSwapBuffers(m_eglDisplay, m_eglSurface);
}
@@ -125,7 +125,7 @@ void WallpaperRenderer::renderToFbo(GLuint targetFbo) {
float progress = (m_tex2 != 0) ? m_progress : 0.0f;
m_program.draw(m_transition, m_tex1, tex2, sw, sh, sw, sh, m_imgW1, m_imgH1, m_imgW2, m_imgH2, progress,
static_cast<float>(m_fillMode), m_params);
static_cast<float>(m_fillMode), m_params, m_fillColor);
// No eglSwapBuffers — caller is responsible for presentation
}
+2
View File
@@ -1,6 +1,7 @@
#pragma once
#include "config/config_service.h"
#include "render/core/color.h"
#include "render/programs/wallpaper_program.h"
#include <EGL/egl.h>
@@ -56,5 +57,6 @@ private:
float m_progress = 0.0f;
WallpaperTransition m_transition = WallpaperTransition::Fade;
WallpaperFillMode m_fillMode = WallpaperFillMode::Crop;
Color m_fillColor = rgba(0.0f, 0.0f, 0.0f, 1.0f);
TransitionParams m_params;
};
+13
View File
@@ -6,6 +6,7 @@
#include "ipc/ipc_service.h"
#include "render/render_context.h"
#include "shell/lockscreen/lock_surface.h"
#include "ui/palette.h"
#include "wayland/wayland_connection.h"
#include "wayland/wayland_seat.h"
@@ -18,6 +19,13 @@ namespace {
constexpr Logger kLog("lockscreen");
Color resolveWallpaperFillColor(const WallpaperConfig& config) {
if (!config.fillColor) {
return rgba(0.0f, 0.0f, 0.0f, 0.0f);
}
return resolveThemeColor(*config.fillColor);
}
const ext_session_lock_v1_listener kSessionLockListener = {
.locked = &LockScreen::handleLocked,
.finished = &LockScreen::handleFinished,
@@ -160,6 +168,9 @@ void LockScreen::onThemeChanged() {
}
for (auto& instance : m_instances) {
if (instance.surface != nullptr) {
if (m_configService != nullptr) {
instance.surface->setWallpaperFillColor(resolveWallpaperFillColor(m_configService->config().wallpaper));
}
instance.surface->onThemeChanged();
}
}
@@ -177,6 +188,7 @@ void LockScreen::onWallpaperChanged() {
const std::string connectorName = output != nullptr ? output->connectorName : std::string{};
instance.surface->setWallpaperPath(m_configService->getWallpaperPath(connectorName));
instance.surface->setWallpaperFillMode(m_configService->config().wallpaper.fillMode);
instance.surface->setWallpaperFillColor(resolveWallpaperFillColor(m_configService->config().wallpaper));
}
}
@@ -323,6 +335,7 @@ void LockScreen::createInstance(const WaylandOutput& output) {
if (m_configService != nullptr) {
surface->setWallpaperPath(m_configService->getWallpaperPath(output.connectorName));
surface->setWallpaperFillMode(m_configService->config().wallpaper.fillMode);
surface->setWallpaperFillColor(resolveWallpaperFillColor(m_configService->config().wallpaper));
}
surface->setOnLogin([this]() { tryAuthenticate(); });
surface->setOnPasswordChanged([this](const std::string& value) { handlePasswordEdited(value); });
+25 -1
View File
@@ -170,6 +170,24 @@ void LockSurface::setWallpaperFillMode(WallpaperFillMode fillMode) {
requestRedraw();
}
void LockSurface::setWallpaperFillColor(Color fillColor) {
if (m_wallpaperFillColor == fillColor) {
return;
}
m_wallpaperFillColor = fillColor;
if (m_wallpaper != nullptr) {
m_wallpaper->setFillColor(m_wallpaperFillColor);
}
if (m_backdrop != nullptr) {
m_backdrop->setVisible(m_wallpaperFillColor.a > 0.0f);
m_backdrop->setStyle(RoundedRectStyle{
.fill = m_wallpaperFillColor,
.fillMode = FillMode::Solid,
});
}
requestRedraw();
}
void LockSurface::setOnLogin(std::function<void()> onLogin) { m_onLogin = std::move(onLogin); }
void LockSurface::setOnPasswordChanged(std::function<void(const std::string&)> onPasswordChanged) {
@@ -288,10 +306,15 @@ void LockSurface::layoutScene(std::uint32_t width, std::uint32_t height) {
m_wallpaper->setPosition(0.0f, 0.0f);
m_wallpaper->setSize(sw, sh);
m_wallpaper->setFillMode(m_wallpaperFillMode);
m_wallpaper->setFillColor(m_wallpaperFillColor);
m_backdrop->setPosition(0.0f, 0.0f);
m_backdrop->setSize(sw, sh);
m_backdrop->setVisible(false);
m_backdrop->setVisible(m_wallpaperFillColor.a > 0.0f);
m_backdrop->setStyle(RoundedRectStyle{
.fill = m_wallpaperFillColor,
.fillMode = FillMode::Solid,
});
constexpr float kClockFontSize = 64.0f;
m_clock->setFontSize(kClockFontSize);
@@ -353,6 +376,7 @@ void LockSurface::applyWallpaperTexture() {
static_cast<float>(m_wallpaperTexture.height), 0.0f, 0.0f);
m_wallpaper->setTransition(WallpaperTransition::Fade, 0.0f, TransitionParams{});
m_wallpaper->setFillMode(m_wallpaperFillMode);
m_wallpaper->setFillColor(m_wallpaperFillColor);
} else {
m_wallpaperTexture = {};
m_wallpaper->setTextures(0, 0, 0.0f, 0.0f, 0.0f, 0.0f);
+3
View File
@@ -1,6 +1,7 @@
#pragma once
#include "config/config_service.h"
#include "render/core/color.h"
#include "render/core/texture_manager.h"
#include "render/scene/input_dispatcher.h"
#include "render/scene/node.h"
@@ -37,6 +38,7 @@ public:
void setTextureCache(SharedTextureCache* cache) noexcept { m_textureCache = cache; }
void setWallpaperPath(std::string wallpaperPath);
void setWallpaperFillMode(WallpaperFillMode fillMode);
void setWallpaperFillColor(Color fillColor);
void setOnLogin(std::function<void()> onLogin);
void setOnPasswordChanged(std::function<void(const std::string&)> onPasswordChanged);
void selectAllPassword();
@@ -71,6 +73,7 @@ private:
TextureHandle m_wallpaperTexture{};
std::string m_wallpaperPath;
WallpaperFillMode m_wallpaperFillMode = WallpaperFillMode::Crop;
Color m_wallpaperFillColor = rgba(0.0f, 0.0f, 0.0f, 0.0f);
bool m_wallpaperDirty = false;
InputDispatcher m_inputDispatcher;
std::function<void()> m_onLogin;
+47 -2
View File
@@ -4,7 +4,10 @@
#include "core/log.h"
#include "core/random.h"
#include "render/core/shared_texture_cache.h"
#include "render/programs/rect_program.h"
#include "render/render_context.h"
#include "render/scene/rect_node.h"
#include "ui/palette.h"
#include "wayland/wayland_connection.h"
#include <algorithm>
@@ -164,6 +167,13 @@ namespace {
constexpr Logger kLog("wallpaper");
Color resolveWallpaperFillColor(const WallpaperConfig& config) {
if (!config.fillColor) {
return rgba(0.0f, 0.0f, 0.0f, 0.0f);
}
return resolveThemeColor(*config.fillColor);
}
} // namespace
Wallpaper::Wallpaper() = default;
@@ -187,6 +197,14 @@ bool Wallpaper::initialize(WaylandConnection& wayland, ConfigService* config, Re
}
m_config->addReloadCallback([this]() { reload(); });
m_paletteConn = paletteChanged().connect([this] {
for (auto& inst : m_instances) {
updateRendererState(*inst);
if (inst->surface != nullptr) {
inst->surface->requestRedraw();
}
}
});
resetAutomationState();
syncInstances();
@@ -233,11 +251,26 @@ void Wallpaper::onStateChange() {
for (auto& inst : m_instances) {
auto newPath = m_config->getWallpaperPath(inst->connectorName);
if (newPath.empty()) {
if (inst->surface == nullptr || inst->wallpaperNode == nullptr) {
continue;
}
if (inst->surface == nullptr || inst->wallpaperNode == nullptr) {
if (newPath.empty()) {
if (!inst->currentPath.empty() || inst->currentTexture.id != 0 || inst->nextTexture.id != 0) {
if (inst->transitionAnimId != 0) {
inst->animations.cancel(inst->transitionAnimId);
inst->transitionAnimId = 0;
}
releaseInstanceTextures(*inst);
inst->currentTexture = {};
inst->nextTexture = {};
inst->currentPath.clear();
inst->pendingPath.clear();
inst->queuedPath.clear();
inst->transitioning = false;
updateRendererState(*inst);
inst->surface->requestRedraw();
}
continue;
}
@@ -401,6 +434,8 @@ void Wallpaper::createInstance(const WaylandOutput& output) {
instance->sceneRoot = std::make_unique<Node>();
instance->sceneRoot->setAnimationManager(&instance->animations);
auto fillNode = std::make_unique<RectNode>();
instance->fillNode = static_cast<RectNode*>(instance->sceneRoot->addChild(std::move(fillNode)));
auto wallpaperNode = std::make_unique<WallpaperNode>();
instance->wallpaperNode = static_cast<WallpaperNode*>(instance->sceneRoot->addChild(std::move(wallpaperNode)));
instance->surface->setSceneRoot(instance->sceneRoot.get());
@@ -410,6 +445,8 @@ void Wallpaper::createInstance(const WaylandOutput& output) {
const float sw = static_cast<float>(width);
const float sh = static_cast<float>(height);
inst->sceneRoot->setSize(sw, sh);
inst->fillNode->setPosition(0.0f, 0.0f);
inst->fillNode->setSize(sw, sh);
inst->wallpaperNode->setPosition(0.0f, 0.0f);
inst->wallpaperNode->setSize(sw, sh);
@@ -529,11 +566,19 @@ void Wallpaper::updateRendererState(WallpaperInstance& instance) {
}
const auto& wpConfig = m_config->config().wallpaper;
const Color fillColor = resolveWallpaperFillColor(wpConfig);
if (instance.fillNode != nullptr) {
instance.fillNode->setStyle(RoundedRectStyle{
.fill = fillColor,
.fillMode = FillMode::Solid,
});
}
wallpaperNode->setTextures(
instance.currentTexture.id, instance.nextTexture.id, static_cast<float>(instance.currentTexture.width),
static_cast<float>(instance.currentTexture.height), static_cast<float>(instance.nextTexture.width),
static_cast<float>(instance.nextTexture.height));
wallpaperNode->setTransition(instance.activeTransition, instance.transitionProgress, instance.transitionParams);
wallpaperNode->setFillMode(wpConfig.fillMode);
wallpaperNode->setFillColor(fillColor);
}
+2
View File
@@ -1,6 +1,7 @@
#pragma once
#include "shell/wallpaper/wallpaper_instance.h"
#include "ui/signal.h"
#include <cstdint>
#include <memory>
@@ -41,5 +42,6 @@ private:
SharedTextureCache* m_textureCache = nullptr;
std::int64_t m_lastAutomationMinuteStamp = -1;
std::int64_t m_lastAutomationSwitchMinute = -1;
Signal<>::ScopedConnection m_paletteConn;
std::vector<std::unique_ptr<WallpaperInstance>> m_instances;
};
+3
View File
@@ -11,6 +11,8 @@
#include <memory>
#include <string>
class RectNode;
struct WallpaperInstance {
std::uint32_t outputName = 0;
struct wl_output* output = nullptr;
@@ -19,6 +21,7 @@ struct WallpaperInstance {
std::unique_ptr<LayerSurface> surface;
std::unique_ptr<Node> sceneRoot;
RectNode* fillNode = nullptr;
WallpaperNode* wallpaperNode = nullptr;
AnimationManager animations;