mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-10-20 02:04:01 -04:00
feat(blur): add configurable blur strength slider in all supported compositors
- Updated Hyprland defaults
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
.pragma library
|
||||
|
||||
// One shell-wide blur reach in logical px, solved into each compositor's own knobs.
|
||||
// Hyprland's stock 8/1 reaches 16px and reads as plain transparency, so DMS defaults it near niri.
|
||||
var HYPRLAND_DEFAULT = 48;
|
||||
var NIRI_DEFAULT = 42;
|
||||
var AQUEOUS_DEFAULT = 40;
|
||||
|
||||
function defaultReach(compositor) {
|
||||
switch (compositor) {
|
||||
case "hyprland":
|
||||
return HYPRLAND_DEFAULT;
|
||||
case "niri":
|
||||
return NIRI_DEFAULT;
|
||||
case "aqueous":
|
||||
return AQUEOUS_DEFAULT;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
function clamp(value, min, max) {
|
||||
return Math.min(max, Math.max(min, value));
|
||||
}
|
||||
|
||||
// Dual kawase reach is 2 * size * (2^passes - 1); passes step up so size stays small enough to avoid banding
|
||||
function hyprlandBlur(reach) {
|
||||
const passes = reach <= 20 ? 1 : reach <= 60 ? 2 : reach <= 140 ? 3 : 4;
|
||||
const size = clamp(Math.round(reach / (2 * ((1 << passes) - 1))), 1, 20);
|
||||
return { size, passes };
|
||||
}
|
||||
|
||||
// Same kawase family; offset is free on the GPU while passes are not, so passes stay at niri's default 3
|
||||
function niriBlur(reach) {
|
||||
return { offset: Math.round(reach / 14 * 10) / 10, passes: 3 };
|
||||
}
|
||||
|
||||
// Separable gaussian whose width is independent of passes; sigma is about 0.26 * radius
|
||||
function aqueousRadius(reach) {
|
||||
return clamp(Math.round(reach / 4), 1, 128);
|
||||
}
|
||||
@@ -328,6 +328,7 @@ Singleton {
|
||||
|
||||
property bool blurEnabled: Spec.SPEC.blurEnabled.def
|
||||
onBlurEnabledChanged: saveSettings()
|
||||
property int blurStrength: Spec.SPEC.blurStrength.def
|
||||
property bool blurBorderSeeded: Spec.SPEC.blurBorderSeeded.def
|
||||
onBlurBorderSeededChanged: saveSettings()
|
||||
property bool blurForegroundLayers: Spec.SPEC.blurForegroundLayers.def
|
||||
@@ -1419,6 +1420,7 @@ Singleton {
|
||||
signal widgetDataChanged
|
||||
signal workspaceIconsUpdated
|
||||
signal compositorLayoutRefreshNeeded(bool frame)
|
||||
signal blurStrengthRefreshNeeded
|
||||
signal compositorInputRefreshNeeded
|
||||
signal compositorCursorRefreshNeeded
|
||||
signal notificationPopupsInvalidated
|
||||
@@ -1464,6 +1466,10 @@ Singleton {
|
||||
compositorInputRefreshNeeded();
|
||||
}
|
||||
|
||||
function updateBlurStrength() {
|
||||
blurStrengthRefreshNeeded();
|
||||
}
|
||||
|
||||
function updateFrameCompositorLayout() {
|
||||
compositorLayoutRefreshNeeded(true);
|
||||
FrameTransitionState.begin();
|
||||
@@ -1716,6 +1722,7 @@ Singleton {
|
||||
"regenSystemThemes": regenSystemThemes,
|
||||
"updateCompositorLayout": updateCompositorLayout,
|
||||
"updateCompositorInput": updateCompositorInput,
|
||||
"updateBlurStrength": updateBlurStrength,
|
||||
"applyStoredIconTheme": applyStoredIconTheme,
|
||||
"updateBarConfigs": updateBarConfigs,
|
||||
"updateCompositorCursor": updateCompositorCursor,
|
||||
@@ -2043,7 +2050,7 @@ Singleton {
|
||||
function _saveSettings(userChanges) {
|
||||
let reason = null;
|
||||
if (isGreeterMode) {
|
||||
reason = "running in greeter mode."
|
||||
reason = "running in greeter mode.";
|
||||
} else if (_loading) {
|
||||
reason = "some files are being loaded";
|
||||
} else if (_parseError) {
|
||||
@@ -3736,7 +3743,7 @@ Singleton {
|
||||
id: rightWidgetsModel
|
||||
}
|
||||
|
||||
component SettingsFile : QtObject {
|
||||
component SettingsFile: QtObject {
|
||||
id: settingsFile
|
||||
|
||||
required property string filePath
|
||||
@@ -3823,7 +3830,7 @@ Singleton {
|
||||
_loadSettingsOrStartIfReady();
|
||||
}
|
||||
}
|
||||
onLoadFailed: (error) => {
|
||||
onLoadFailed: error => {
|
||||
if (isGreeterMode) {
|
||||
return;
|
||||
}
|
||||
@@ -3846,7 +3853,7 @@ Singleton {
|
||||
unsavedUserChanges = _anySettingsFile(file => file.fileUnsavedUserChanges);
|
||||
}
|
||||
}
|
||||
onSaveFailed: (error) => {
|
||||
onSaveFailed: error => {
|
||||
selfWrite = false;
|
||||
if (error === FileViewError.PermissionDenied) {
|
||||
isFileReadOnly = true;
|
||||
@@ -3860,7 +3867,12 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
enum Stage { Discovering = 0, Loading = 1, Partial = 2, Ready = 3 }
|
||||
enum Stage {
|
||||
Discovering = 0,
|
||||
Loading = 1,
|
||||
Partial = 2,
|
||||
Ready = 3
|
||||
}
|
||||
property int _settingsStage: SettingsData.Stage.Discovering
|
||||
|
||||
property var _settingsFiles: new Map()
|
||||
@@ -3982,7 +3994,9 @@ Singleton {
|
||||
const filePath = folderModel.get(i, "filePath");
|
||||
folderPathsSet.add(filePath);
|
||||
if (!_settingsFiles.has(filePath)) {
|
||||
listModel.append({ filePath });
|
||||
listModel.append({
|
||||
filePath
|
||||
});
|
||||
}
|
||||
}
|
||||
for (let i = listModel.count - 1; i >= 0; i--) {
|
||||
@@ -3997,7 +4011,7 @@ Singleton {
|
||||
interval: 50
|
||||
repeat: false
|
||||
running: false
|
||||
onTriggered: _syncSettingsFilesModels();
|
||||
onTriggered: _syncSettingsFilesModels()
|
||||
}
|
||||
ListModel {
|
||||
id: settingsFilesListModel
|
||||
@@ -4133,7 +4147,6 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Timer {
|
||||
id: settingsSaveDebounce
|
||||
interval: 50
|
||||
|
||||
@@ -497,6 +497,10 @@ var LOCAL_SPEC = {
|
||||
blurEnabled: {
|
||||
def: false
|
||||
},
|
||||
blurStrength: {
|
||||
def: 0,
|
||||
onChange: "updateBlurStrength"
|
||||
},
|
||||
blurBorderSeeded: {
|
||||
def: false
|
||||
},
|
||||
|
||||
@@ -186,14 +186,19 @@ Column {
|
||||
}
|
||||
}
|
||||
|
||||
SettingsToggleRow {
|
||||
SettingsToggleSliderRow {
|
||||
tab: "theme"
|
||||
tags: ["blur", "background", "transparency", "glass", "frosted"]
|
||||
tags: ["blur", "background", "transparency", "glass", "frosted", "strength", "radius", "size", "passes", "reach"]
|
||||
settingKey: "blurEnabled"
|
||||
valueKeys: ["blurStrength"]
|
||||
text: I18n.tr("Background blur")
|
||||
description: BlurService.available ? "" : I18n.tr("Your compositor does not support background blur (ext-background-effect-v1)")
|
||||
description: BlurService.available ? I18n.tr("Sets the compositor's blur for every window, not just the shell", "Blur strength slider description") : I18n.tr("Your compositor does not support background blur (ext-background-effect-v1)")
|
||||
checked: SettingsData.blurEnabled ?? false
|
||||
enabled: BlurService.available
|
||||
value: BlurService.strength
|
||||
minimum: 16
|
||||
maximum: 160
|
||||
unit: "px"
|
||||
onToggled: checked => {
|
||||
SettingsData.set("blurEnabled", checked);
|
||||
if (!checked || SettingsData.blurBorderSeeded)
|
||||
@@ -202,6 +207,7 @@ Column {
|
||||
SettingsData.set("blurBorderEnabled", true);
|
||||
SettingsSearchService.navigateToSection("blurBorderEnabled");
|
||||
}
|
||||
onSliderValueChanged: newValue => SettingsData.set("blurStrength", newValue)
|
||||
}
|
||||
|
||||
SettingsNavRow {
|
||||
|
||||
@@ -3,6 +3,8 @@ pragma ComponentBehavior: Bound
|
||||
|
||||
import Quickshell
|
||||
import qs.Services
|
||||
import qs.Common
|
||||
import "../Common/BlurStrength.js" as BlurStrength
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
@@ -146,4 +148,24 @@ Singleton {
|
||||
function apply(draft, callback) {
|
||||
request("apply", draft, callback);
|
||||
}
|
||||
|
||||
// 0 restores the schema default so a reset does not pin the last slider value in the user override
|
||||
function applyBlurStrength() {
|
||||
load((snapshot, error) => {
|
||||
if (!snapshot)
|
||||
return;
|
||||
const field = snapshot.fields?.find(f => f.id === "blur.radius");
|
||||
if (!field)
|
||||
return;
|
||||
const strength = SettingsData.blurStrength;
|
||||
const radius = strength > 0 ? BlurStrength.aqueousRadius(strength) : field.default;
|
||||
if (!Number.isFinite(radius) || radius === field.value)
|
||||
return;
|
||||
apply({
|
||||
expected_generation: snapshot.generation,
|
||||
create_user_override: true,
|
||||
changes: [{ id: "blur.radius", value: radius }]
|
||||
}, () => {});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import "../Common/BlurStrength.js" as BlurStrength
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
@@ -20,6 +21,8 @@ Singleton {
|
||||
probe();
|
||||
}
|
||||
readonly property bool enabled: available && (SettingsData.blurEnabled ?? false)
|
||||
readonly property int defaultStrength: BlurStrength.defaultReach(CompositorService.configKey)
|
||||
readonly property int strength: SettingsData.blurStrength || defaultStrength
|
||||
|
||||
// These settings predate non-blurred surface borders, so keep their keys for compatibility.
|
||||
readonly property color borderColor: {
|
||||
|
||||
@@ -2171,6 +2171,14 @@ Singleton {
|
||||
MangoService.generateLayoutConfig();
|
||||
}
|
||||
|
||||
function onBlurStrengthRefreshNeeded() {
|
||||
if (root.isAqueous && typeof AqueousConfigService !== "undefined") {
|
||||
AqueousConfigService.applyBlurStrength();
|
||||
return;
|
||||
}
|
||||
onCompositorLayoutRefreshNeeded(false);
|
||||
}
|
||||
|
||||
function onCompositorInputRefreshNeeded() {
|
||||
if (root.isNiri && typeof NiriService !== "undefined")
|
||||
NiriService.generateNiriInputConfig();
|
||||
|
||||
@@ -9,6 +9,7 @@ import qs.Common
|
||||
import "../Common/ConfigIncludeResolve.js" as ConfigIncludeResolve
|
||||
import qs.Services
|
||||
import "../Common/OutputModel.js" as OutputModel
|
||||
import "../Common/BlurStrength.js" as BlurStrength
|
||||
import "../Common/WorkspaceModel.js" as WorkspaceModel
|
||||
|
||||
Singleton {
|
||||
@@ -548,16 +549,6 @@ hl.config({
|
||||
${sections.join("\n")}
|
||||
})
|
||||
`;
|
||||
// Shell windows stay rigid like the rest of the shell chrome
|
||||
if (SettingsData.hyprlandWobbleEnabled && hyprSupports("decoration:wobble:enabled")) {
|
||||
content += `
|
||||
hl.window_rule({
|
||||
match = { class = "^(com\\\\.danklinux\\\\.dms)$" },
|
||||
no_wobble = true,
|
||||
})
|
||||
`;
|
||||
}
|
||||
|
||||
if (layoutXrayEnabled) {
|
||||
content += `
|
||||
hl.layer_rule({
|
||||
@@ -622,11 +613,17 @@ hl.layer_rule({
|
||||
}
|
||||
|
||||
function blurLines(s) {
|
||||
const blur = BlurStrength.hyprlandBlur(s.blurStrength || BlurStrength.HYPRLAND_DEFAULT);
|
||||
const lines = [];
|
||||
if (blur.size !== 8)
|
||||
lines.push(`size = ${blur.size},`);
|
||||
if (blur.passes !== 1)
|
||||
lines.push(`passes = ${blur.passes},`);
|
||||
const variant = s.hyprlandBlurVariant;
|
||||
if (!variant || variant === "kawase" || !hyprSupports("decoration:blur:variant"))
|
||||
return [];
|
||||
return lines;
|
||||
// A user config with blur disabled would otherwise hide the chosen variant
|
||||
const lines = ["enabled = true,", `variant = ${luaString(variant)},`];
|
||||
lines.push("enabled = true,", `variant = ${luaString(variant)},`);
|
||||
switch (variant) {
|
||||
case "ripple":
|
||||
return lines.concat(luaTable("ripple", [`strength = ${s.hyprlandBlurRippleStrength},`]));
|
||||
|
||||
@@ -9,6 +9,7 @@ import qs.Common
|
||||
import qs.Services
|
||||
import qs.DCommon.Common as DCommon
|
||||
import "../Common/OutputModel.js" as OutputModel
|
||||
import "../Common/BlurStrength.js" as BlurStrength
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
@@ -1214,6 +1215,19 @@ Singleton {
|
||||
configGenerationAction.schedule();
|
||||
}
|
||||
|
||||
// niri's own defaults stay untouched until the user moves the shell-wide slider
|
||||
function niriBlurBlock(strength) {
|
||||
if (!(strength > 0))
|
||||
return "";
|
||||
const blur = BlurStrength.niriBlur(strength);
|
||||
return `
|
||||
|
||||
blur {
|
||||
passes ${blur.passes}
|
||||
offset ${blur.offset}
|
||||
}`;
|
||||
}
|
||||
|
||||
function doGenerateNiriLayoutConfig() {
|
||||
if (writeConfigProcess.running || _awaitingLayoutReloadRevision > _layoutAppliedRevision)
|
||||
return;
|
||||
@@ -1285,7 +1299,7 @@ window-rule {
|
||||
clip-to-geometry true
|
||||
tiled-state true
|
||||
draw-border-with-background false
|
||||
}` + xrayRules;
|
||||
}` + niriBlurBlock(typeof SettingsData !== "undefined" ? SettingsData.blurStrength : 0) + xrayRules;
|
||||
|
||||
const alttabContent = dmsWarning + `recent-windows {
|
||||
highlight {
|
||||
|
||||
@@ -5,6 +5,7 @@ import vm from "node:vm";
|
||||
import { loadScript } from "./qml-script.mjs";
|
||||
|
||||
const OutputModel = loadScript(new URL("../Common/OutputModel.js", import.meta.url));
|
||||
const BlurStrength = loadScript(new URL("../Common/BlurStrength.js", import.meta.url));
|
||||
const SPEC = loadScript(new URL("../Common/settings/SettingsSpec.js", import.meta.url)).SPEC;
|
||||
const source = readFileSync(new URL("../Services/HyprlandService.qml", import.meta.url), "utf8");
|
||||
const extract = name => {
|
||||
@@ -18,6 +19,7 @@ const live = {};
|
||||
// Every option is reported as supported unless a test shrinks the set
|
||||
const scope = vm.createContext({
|
||||
OutputModel,
|
||||
BlurStrength,
|
||||
SettingsData: { displayNameMode: "system" },
|
||||
liveMonitor: name => live[name] ?? null,
|
||||
hyprOptionNames: new Proxy({}, { get: (names, key) => key in names ? names[key] : true })
|
||||
@@ -43,7 +45,7 @@ const settingsFor = prefix => {
|
||||
const defaults = Object.fromEntries(Object.keys(SPEC).filter(key => prefix.test(key)).map(key => [key, SPEC[key].def]));
|
||||
return overrides => Object.assign({}, defaults, overrides);
|
||||
};
|
||||
const layout = settingsFor(/^hyprland/);
|
||||
const layout = settingsFor(/^(hyprland|blurStrength)/);
|
||||
const input = settingsFor(/^(mouse|touchpad|keyboard)[A-Z]/);
|
||||
const table = (lines, name) => {
|
||||
const start = lines.indexOf(`${name} = {`);
|
||||
@@ -108,17 +110,24 @@ test("outputs: written scale snaps to Hyprland's n/120 grid so live float32 scal
|
||||
});
|
||||
|
||||
test("layout: a blur variant forces blur on and writes only its own tuning table", () => {
|
||||
assert.equal(table(decorationLines(layout({ hyprlandBlurVariant: "kawase" }), 0), "blur"), null);
|
||||
const frost = table(decorationLines(layout({ hyprlandBlurVariant: "frost" }), 0), "blur");
|
||||
assert.equal(table(decorationLines(layout({ hyprlandBlurVariant: "kawase", blurStrength: 16 }), 0), "blur"), null);
|
||||
const frost = table(decorationLines(layout({ hyprlandBlurVariant: "frost", blurStrength: 16 }), 0), "blur");
|
||||
assert.deepEqual(frost, ["enabled = true,", 'variant = "frost",']);
|
||||
const aurora = table(decorationLines(layout({ hyprlandBlurVariant: "aurora" }), 0), "blur");
|
||||
const aurora = table(decorationLines(layout({ hyprlandBlurVariant: "aurora", blurStrength: 16 }), 0), "blur");
|
||||
assert.deepEqual(aurora.slice(0, 2), ["enabled = true,", 'variant = "aurora",']);
|
||||
assert.deepEqual(table(aurora, "aurora"), ["intensity = 0.35,", "speed = 1,"]);
|
||||
assert.equal(table(aurora, "haze"), null);
|
||||
});
|
||||
|
||||
test("layout: blur strength writes Hyprland's size and passes, default reach gives two passes and skips the stock size", () => {
|
||||
assert.deepEqual(table(decorationLines(layout(), 0), "blur"), ["passes = 2,"]);
|
||||
assert.equal(table(decorationLines(layout({ blurStrength: 16 }), 0), "blur"), null);
|
||||
assert.deepEqual(table(decorationLines(layout({ blurStrength: 112, hyprlandBlurVariant: "frost" }), 0), "blur"), ["passes = 3,", "enabled = true,", 'variant = "frost",']);
|
||||
});
|
||||
|
||||
test("layout: effects the running Hyprland does not know are not written even when enabled", () => {
|
||||
const enabled = layout({
|
||||
blurStrength: 16,
|
||||
hyprlandBlurVariant: "aurora",
|
||||
hyprlandGlowEnabled: true,
|
||||
hyprlandWobbleEnabled: true,
|
||||
|
||||
@@ -251,18 +251,24 @@
|
||||
"keywords": [
|
||||
"background",
|
||||
"blur",
|
||||
"compositor",
|
||||
"does",
|
||||
"effect",
|
||||
"every",
|
||||
"frosted",
|
||||
"glass",
|
||||
"interface",
|
||||
"just",
|
||||
"passes",
|
||||
"radius",
|
||||
"reach",
|
||||
"sets",
|
||||
"shell",
|
||||
"size",
|
||||
"strength",
|
||||
"style",
|
||||
"support",
|
||||
"transparency"
|
||||
"transparency",
|
||||
"window"
|
||||
],
|
||||
"page": "theme_surfaces",
|
||||
"description": "Your compositor does not support background blur (ext-background-effect-v1)"
|
||||
"description": "Sets the compositor's blur for every window, not just the shell"
|
||||
},
|
||||
{
|
||||
"section": "wallpaperBackgroundColorMode",
|
||||
|
||||
Reference in New Issue
Block a user