Files
DankMaterialShell/quickshell/Modules/Settings/CompositorLayoutTab.qml
T
Marlon Kranz 72c23161e7 fix(hyprland): write window rules Hyprland 0.56 accepts, keep DMS windows opaque (#3655)
* fix(hyprland): write window rules Hyprland 0.56 accepts, keep DMS windows opaque

The Lua window-rule writer emitted match keys and effects Hyprland 0.56
does not know: `floating`/`pinned`/`initialised` as match keys and
`noborder`/`norounding` as effects. It now writes `float`/`pin` and
`border_size = 0`/`rounding = 0`; the parser still reads the old
spellings, so existing files load and are rewritten on the next save.
"No Rounding" now suppresses the corner radius so a rule has one
`rounding` key.

The rule editor no longer offers fields the Hyprland writer drops
(Window Opening output/workspace, Size Constraints, the Initialised
match), and Save is disabled while only one half of a move or size
pair is filled. Every window-rule write now reloads the compositor
through one helper, which also covers Hyprland.

DMS windows draw their own translucent background, so any compositor
opacity rule or decoration opacity multiplied on top of it. DMS now
keeps a hidden rule for its own app id: `opaque = true` on Hyprland
(applied after rule and decoration opacity, independent of order, and
leaving the window's own alpha alone) and `opacity 1.0` on niri, which
the store keeps last. It is re-added on start when missing and hidden
from the rules list and the focused-window menu.

Compositor layout gains a Hyprland "Window opacity" slider writing
decoration.active_opacity/inactive_opacity; 100 leaves the user's own
config in charge.

HyprlandService.reloadConfig queues callbacks: Proc keeps one callback
per id, so a reload requested while the layout writer's reload was
pending dropped its callback and left layout generation stuck.

* fix(windowrules): cap corner radius at 20

Hyprland 0.56's Lua window_rule rejects rounding above 20 ("value 24
is more than the maximum of 20") and the rule loses its rounding. The
Hyprland writer now clamps to 20, and the editor slider stops at 20
for every compositor.
2026-10-07 14:09:49 -04:00

567 lines
26 KiB
QML

import QtCore
import QtQuick
import qs.Common
import qs.Services
import qs.DCommon.Widgets
import qs.Modules.Settings.Widgets
Item {
id: root
property var parentModal: null
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
property string xrayConflictSource: ""
ConfigInclude {
id: layoutInclude
includeKind: "layout"
onFixed: SettingsData.updateCompositorLayout()
}
function checkXrayConflicts() {
if (!CompositorService.isNiri)
return;
const configDir = Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation));
const script = `cd "${configDir}/niri" 2>/dev/null || exit 0
files="config.kdl"
for f in $(sed -nE 's/^[[:space:]]*include[[:space:]]+"([^"]+)".*/\\1/p' config.kdl 2>/dev/null); do
case "$f" in dms/*|/*dms/*) continue ;; esac
[ -f "$f" ] && files="$files $f"
done
awk '$1 == "xray" { print FILENAME ":" FNR; exit }' $files 2>/dev/null`;
Proc.runCommand("check-xray-conflict", ["sh", "-c", script], (output, exitCode) => {
xrayConflictSource = exitCode === 0 ? output.trim() : "";
});
}
Component.onCompleted: checkXrayConflicts()
SettingsPage {
id: layoutColumn
IncludeSetupBanner {
include: layoutInclude
visibleCondition: layoutInclude.compositorSupported
}
StyledRect {
width: parent.width
height: xrayConflictRow.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.primary, 0.15)
border.color: Theme.withAlpha(Theme.primary, 0.3)
border.width: Theme.outlineWidth
visible: root.xrayConflictSource !== ""
Row {
id: xrayConflictRow
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
DIcon {
name: "warning"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
width: parent.width - Theme.iconSize - Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
text: I18n.tr("An xray rule at %1 may conflict with the Xray settings below", "compositor layout warning, %1 is a config location").arg(root.xrayConflictSource)
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
}
}
}
SettingsCard {
width: parent.width
tags: ["niri", "layout", "gaps", "radius", "window", "border"]
title: I18n.tr("Layout overrides")
settingKey: "niriLayout"
iconName: "layers"
visible: CompositorService.isNiri
SettingsButtonGroupRow {
tags: ["niri", "gaps", "override", "unmanaged"]
settingKey: "niriLayoutGapsMode"
resetKeys: ["niriLayoutGapsOverride"]
text: I18n.tr("Gaps", "noun, window gaps mode in compositor layout settings")
description: I18n.tr("Auto follows bar spacing, Off keeps your %1 config", "gaps mode description, %1 is the compositor name").arg("niri")
model: [I18n.tr("Auto"), I18n.tr("Custom"), I18n.tr("Off")]
currentIndex: {
if (SettingsData.niriLayoutGapsOverride === -2)
return 2;
return SettingsData.niriLayoutGapsOverride >= 0 ? 1 : 0;
}
onSelectionChanged: (index, selected) => {
if (!selected)
return;
switch (index) {
case 1:
SettingsData.set("niriLayoutGapsOverride", Math.max(4, (SettingsData.getPrimaryBarConfig()?.spacing ?? 4)));
return;
case 2:
SettingsData.set("niriLayoutGapsOverride", -2);
return;
default:
SettingsData.set("niriLayoutGapsOverride", -1);
}
}
}
SettingsSliderRow {
tags: ["niri", "gaps", "override"]
settingKey: "niriLayoutGapsOverride"
resetKeys: []
text: I18n.tr("Window gaps")
visible: SettingsData.niriLayoutGapsOverride >= 0
value: Math.max(0, SettingsData.niriLayoutGapsOverride)
minimum: 0
maximum: 50
unit: "px"
onSliderValueChanged: newValue => SettingsData.set("niriLayoutGapsOverride", newValue)
}
SettingsControlledBy {
target: "surfaces"
parentModal: root.parentModal
section: "windowRadius"
settingLabel: I18n.tr("Window radius")
reason: Math.round(Theme.windowRadius) + "px"
}
SettingsToggleSliderRow {
tags: ["niri", "border", "override", "focus-ring"]
settingKey: "niriLayoutBorderSizeEnabled"
resetKeys: ["niriLayoutBorderSize"]
text: I18n.tr("Override border size")
checked: SettingsData.niriLayoutBorderSize >= 0
value: Math.max(0, SettingsData.niriLayoutBorderSize)
minimum: 0
maximum: 10
unit: "px"
onToggled: checked => SettingsData.set("niriLayoutBorderSize", checked ? 2 : -1)
onSliderValueChanged: newValue => SettingsData.set("niriLayoutBorderSize", newValue)
}
SettingsToggleRow {
visible: CompositorService.isNiri
tags: ["niri", "xray", "blur", "background-effect", "performance"]
settingKey: "niriLayoutXrayEnabled"
text: I18n.tr("Xray blur")
description: I18n.tr("Blurred surfaces show the wallpaper, not the windows beneath")
checked: NiriService.layoutXrayEnabled
onToggled: checked => NiriService.setLayoutXray(checked)
}
SettingsToggleRow {
visible: CompositorService.isNiri && !SettingsData.connectedFrameModeActive
tags: ["niri", "xray", "bar", "frame", "performance"]
settingKey: "niriLayoutBarXrayEnabled"
text: SettingsData.frameEnabled ? I18n.tr("Frame xray") : I18n.tr("Bar xray")
description: I18n.tr("Blur against the wallpaper even with xray off")
checked: NiriService.layoutBarXrayEnabled
onToggled: checked => NiriService.setLayoutBarXray(checked)
}
}
SettingsCard {
width: parent.width
tags: ["compositor", "toast"]
title: I18n.tr("Notifications")
settingKey: "compositorNotifications"
iconName: "notifications"
visible: CompositorService.isNiri || CompositorService.isMango
SettingsToggleRow {
tags: ["compositor", "toast", "reload"]
settingKey: "showConfigReloadToast"
text: I18n.tr("Show \"config reloaded\" toast")
checked: SessionData.showConfigReloadToast
onToggled: checked => SessionData.showConfigReloadToast = checked
}
}
SettingsCard {
width: parent.width
tags: ["niri", "overview", "window", "focus", "launch", "launcher", "dock", "settings"]
title: I18n.tr("Overview")
settingKey: "niriOverview"
iconName: "overview"
visible: CompositorService.isNiri
SettingsToggleRow {
tags: ["niri", "overview", "window", "focus", "launch", "launcher", "dock", "settings"]
settingKey: "closeNiriOverviewOnWindowFocus"
text: I18n.tr("Close on window focus")
description: I18n.tr("Leaves the overview when DMS launches an app or focuses a window", "niri overview close on window focus toggle description")
checked: SettingsData.closeNiriOverviewOnWindowFocus
onToggled: checked => SettingsData.set("closeNiriOverviewOnWindowFocus", checked)
}
}
SettingsCard {
id: hyprTilingCard
width: parent.width
tags: ["hyprland", "layout", "tiling", "dwindle", "master", "scrolling", "general:layout"]
title: I18n.tr("Tiling layout", "Hyprland settings card title")
settingKey: "hyprlandTilingLayout"
iconName: "view_quilt"
visible: CompositorService.isHyprland
readonly property var layoutIds: ["", "dwindle", "master", "scrolling"]
SettingsButtonGroupRow {
tags: ["hyprland", "layout", "tiling", "dwindle", "master", "scrolling"]
settingKey: "hyprlandTilingLayout"
text: I18n.tr("Layout")
model: [I18n.tr("Off"), I18n.tr("Dwindle", "Hyprland tiling layout name"), I18n.tr("Master", "Hyprland tiling layout name"), I18n.tr("Scrolling", "Hyprland tiling layout name")]
currentIndex: Math.max(0, hyprTilingCard.layoutIds.indexOf(SettingsData.hyprlandTilingLayout))
onSelectionChanged: (index, selected) => {
if (!selected)
return;
SettingsData.set("hyprlandTilingLayout", hyprTilingCard.layoutIds[index]);
}
}
SettingsToggleRow {
tags: ["hyprland", "dwindle", "preserve", "split"]
settingKey: "hyprlandDwindlePreserveSplit"
visible: SettingsData.hyprlandTilingLayout === "dwindle"
text: I18n.tr("Preserve split", "Hyprland dwindle layout toggle")
description: I18n.tr("Split direction stays fixed when the container resizes", "Hyprland preserve split toggle description")
checked: SettingsData.hyprlandDwindlePreserveSplit
onToggled: checked => SettingsData.set("hyprlandDwindlePreserveSplit", checked)
}
SettingsToggleRow {
tags: ["hyprland", "dwindle", "smart", "split", "cursor"]
settingKey: "hyprlandDwindleSmartSplit"
visible: SettingsData.hyprlandTilingLayout === "dwindle"
text: I18n.tr("Smart split", "Hyprland dwindle layout toggle")
description: I18n.tr("Split direction follows the cursor position in the window", "Hyprland smart split toggle description")
checked: SettingsData.hyprlandDwindleSmartSplit
onToggled: checked => SettingsData.set("hyprlandDwindleSmartSplit", checked)
}
SettingsButtonGroupRow {
tags: ["hyprland", "dwindle", "force", "split", "direction"]
settingKey: "hyprlandDwindleForceSplit"
visible: SettingsData.hyprlandTilingLayout === "dwindle"
text: I18n.tr("Force split", "Hyprland dwindle layout dropdown, which side new windows split to")
model: [I18n.tr("Follow mouse", "Hyprland force split option, the split side follows the pointer"), I18n.tr("Left"), I18n.tr("Right")]
currentIndex: SettingsData.hyprlandDwindleForceSplit
onSelectionChanged: (index, selected) => {
if (!selected)
return;
SettingsData.set("hyprlandDwindleForceSplit", index);
}
}
SettingsDropdownRow {
readonly property var ids: ["left", "right", "top", "bottom", "center"]
tags: ["hyprland", "master", "orientation", "position"]
settingKey: "hyprlandMasterOrientation"
visible: SettingsData.hyprlandTilingLayout === "master"
text: I18n.tr("Master position", "Hyprland master layout dropdown, where the master window sits")
options: [I18n.tr("Left"), I18n.tr("Right"), I18n.tr("Top"), I18n.tr("Bottom"), I18n.tr("Center")]
currentValue: options[Math.max(0, ids.indexOf(SettingsData.hyprlandMasterOrientation))]
onValueChanged: value => SettingsData.set("hyprlandMasterOrientation", ids[Math.max(0, options.indexOf(value))])
}
SettingsButtonGroupRow {
readonly property var ids: ["slave", "master", "inherit"]
tags: ["hyprland", "master", "new", "window", "status", "slave"]
settingKey: "hyprlandMasterNewStatus"
visible: SettingsData.hyprlandTilingLayout === "master"
text: I18n.tr("New windows", "Hyprland master layout dropdown, where new windows go")
model: [I18n.tr("Stack", "Hyprland master layout option, new windows join the stack"), I18n.tr("Master", "Hyprland tiling layout name"), I18n.tr("Inherit")]
currentIndex: Math.max(0, ids.indexOf(SettingsData.hyprlandMasterNewStatus))
onSelectionChanged: (index, selected) => {
if (!selected)
return;
SettingsData.set("hyprlandMasterNewStatus", ids[index]);
}
}
SettingsToggleRow {
tags: ["hyprland", "master", "new", "top", "stack"]
settingKey: "hyprlandMasterNewOnTop"
visible: SettingsData.hyprlandTilingLayout === "master"
text: I18n.tr("New windows on top of the stack", "Hyprland master layout toggle")
checked: SettingsData.hyprlandMasterNewOnTop
onToggled: checked => SettingsData.set("hyprlandMasterNewOnTop", checked)
}
SettingsSliderRow {
tags: ["hyprland", "master", "size", "mfact", "ratio"]
settingKey: "hyprlandMasterSize"
visible: SettingsData.hyprlandTilingLayout === "master"
text: I18n.tr("Master size", "Hyprland master layout slider, share of the screen the master window takes")
value: SettingsData.hyprlandMasterSize
minimum: 10
maximum: 90
onSliderValueChanged: newValue => SettingsData.set("hyprlandMasterSize", newValue)
}
SettingsButtonGroupRow {
readonly property var ids: ["right", "left", "down", "up"]
tags: ["hyprland", "scrolling", "direction"]
settingKey: "hyprlandScrollingDirection"
visible: SettingsData.hyprlandTilingLayout === "scrolling"
text: I18n.tr("Direction")
model: [I18n.tr("Right"), I18n.tr("Left"), I18n.tr("Down", "Hyprland scrolling layout direction"), I18n.tr("Up", "Hyprland scrolling layout direction")]
currentIndex: Math.max(0, ids.indexOf(SettingsData.hyprlandScrollingDirection))
onSelectionChanged: (index, selected) => {
if (!selected)
return;
SettingsData.set("hyprlandScrollingDirection", ids[index]);
}
}
SettingsSliderRow {
tags: ["hyprland", "scrolling", "column", "width"]
settingKey: "hyprlandScrollingColumnWidth"
visible: SettingsData.hyprlandTilingLayout === "scrolling"
text: I18n.tr("Column Width")
value: SettingsData.hyprlandScrollingColumnWidth
minimum: 10
maximum: 100
onSliderValueChanged: newValue => SettingsData.set("hyprlandScrollingColumnWidth", newValue)
}
SettingsToggleRow {
tags: ["hyprland", "scrolling", "fullscreen", "single", "column"]
settingKey: "hyprlandScrollingFullscreenOneColumn"
visible: SettingsData.hyprlandTilingLayout === "scrolling"
text: I18n.tr("Fullscreen single column", "Hyprland scrolling layout toggle, a lone column fills the screen")
checked: SettingsData.hyprlandScrollingFullscreenOneColumn
onToggled: checked => SettingsData.set("hyprlandScrollingFullscreenOneColumn", checked)
}
SettingsToggleRow {
tags: ["hyprland", "scrolling", "follow", "focus", "scroll"]
settingKey: "hyprlandScrollingFollowFocus"
visible: SettingsData.hyprlandTilingLayout === "scrolling"
text: I18n.tr("Follow focus")
checked: SettingsData.hyprlandScrollingFollowFocus
onToggled: checked => SettingsData.set("hyprlandScrollingFollowFocus", checked)
}
}
SettingsCard {
width: parent.width
tags: ["hyprland", "layout", "gaps", "radius", "window", "border", "rounding"]
title: I18n.tr("Layout overrides")
settingKey: "hyprlandLayout"
iconName: "crop_square"
visible: CompositorService.isHyprland
SettingsButtonGroupRow {
tags: ["hyprland", "gaps", "override", "inner", "outer", "unmanaged"]
settingKey: "hyprlandLayoutGapsMode"
resetKeys: ["hyprlandLayoutGapsOverride"]
text: I18n.tr("Gaps")
model: [I18n.tr("Auto"), I18n.tr("Custom"), I18n.tr("Off")]
currentIndex: {
if (SettingsData.hyprlandLayoutGapsOverride === -2)
return 2;
return SettingsData.hyprlandLayoutGapsOverride >= 0 ? 1 : 0;
}
onSelectionChanged: (index, selected) => {
if (!selected)
return;
switch (index) {
case 1:
SettingsData.set("hyprlandLayoutGapsOverride", Math.max(4, (SettingsData.getPrimaryBarConfig()?.spacing ?? 4)));
return;
case 2:
SettingsData.set("hyprlandLayoutGapsOverride", -2);
return;
default:
SettingsData.set("hyprlandLayoutGapsOverride", -1);
}
}
}
SettingsSliderRow {
tags: ["hyprland", "gaps", "override", "inner", "gaps_in"]
settingKey: "hyprlandLayoutGapsOverride"
resetKeys: []
text: I18n.tr("Inner gaps")
visible: SettingsData.hyprlandLayoutGapsOverride >= 0
value: Math.max(0, SettingsData.hyprlandLayoutGapsOverride)
minimum: 0
maximum: 50
unit: "px"
onSliderValueChanged: newValue => SettingsData.set("hyprlandLayoutGapsOverride", newValue)
}
SettingsSliderRow {
tags: ["hyprland", "gaps", "override", "outer", "edge", "gaps_out"]
settingKey: "hyprlandLayoutGapsOutOverride"
text: I18n.tr("Outer gaps")
visible: SettingsData.hyprlandLayoutGapsOverride >= 0
value: SettingsData.hyprlandLayoutGapsOutOverride >= 0 ? SettingsData.hyprlandLayoutGapsOutOverride : Math.max(0, SettingsData.hyprlandLayoutGapsOverride)
minimum: 0
maximum: 50
unit: "px"
onSliderValueChanged: newValue => SettingsData.set("hyprlandLayoutGapsOutOverride", newValue)
}
SettingsControlledBy {
target: "surfaces"
parentModal: root.parentModal
section: "windowRadius"
settingLabel: I18n.tr("Window radius")
reason: Math.round(Theme.windowRadius) + "px"
}
SettingsToggleSliderRow {
tags: ["hyprland", "border", "override", "border_size"]
settingKey: "hyprlandLayoutBorderSizeEnabled"
resetKeys: ["hyprlandLayoutBorderSize"]
text: I18n.tr("Override border size")
checked: SettingsData.hyprlandLayoutBorderSize >= 0
value: Math.max(0, SettingsData.hyprlandLayoutBorderSize)
minimum: 0
maximum: 10
unit: "px"
onToggled: checked => SettingsData.set("hyprlandLayoutBorderSize", checked ? 2 : -1)
onSliderValueChanged: newValue => SettingsData.set("hyprlandLayoutBorderSize", newValue)
}
SettingsToggleRow {
tags: ["hyprland", "resize", "border", "mouse", "drag"]
settingKey: "hyprlandResizeOnBorder"
text: I18n.tr("Resize on border")
checked: SettingsData.hyprlandResizeOnBorder
onToggled: checked => SettingsData.set("hyprlandResizeOnBorder", checked)
}
SettingsSliderRow {
tags: ["hyprland", "opacity", "transparency", "window"]
settingKey: "hyprlandWindowOpacity"
text: I18n.tr("Window opacity", "Hyprland opacity applied to app windows")
value: SettingsData.hyprlandWindowOpacity
minimum: 10
maximum: 100
unit: "%"
onSliderValueChanged: newValue => SettingsData.set("hyprlandWindowOpacity", newValue)
}
SettingsToggleRow {
visible: CompositorService.isHyprland
tags: ["hyprland", "xray", "blur", "background-effect", "performance"]
settingKey: "hyprlandLayoutXrayEnabled"
text: I18n.tr("Xray blur")
description: I18n.tr("Blurred surfaces show the wallpaper, not the windows beneath")
checked: HyprlandService.layoutXrayEnabled
onToggled: checked => HyprlandService.setLayoutXray(checked)
}
SettingsToggleRow {
visible: CompositorService.isHyprland && !SettingsData.connectedFrameModeActive
tags: ["hyprland", "xray", "bar", "frame", "performance"]
settingKey: "hyprlandLayoutBarXrayEnabled"
text: SettingsData.frameEnabled ? I18n.tr("Frame xray") : I18n.tr("Bar xray")
description: I18n.tr("Blur against the wallpaper even with xray off")
checked: HyprlandService.layoutBarXrayEnabled
onToggled: checked => HyprlandService.setLayoutBarXray(checked)
}
}
SettingsCard {
width: parent.width
tags: ["mangowc", "mango", "dwl", "layout", "gaps", "radius", "window", "border"]
title: I18n.tr("Layout overrides")
settingKey: "mangoLayout"
iconName: "crop_square"
visible: CompositorService.isMango
SettingsButtonGroupRow {
tags: ["mangowc", "mango", "gaps", "override", "inner", "outer", "unmanaged"]
settingKey: "mangoLayoutGapsMode"
resetKeys: ["mangoLayoutGapsOverride"]
text: I18n.tr("Gaps")
description: I18n.tr("Auto follows bar spacing, Off keeps your %1 config").arg("MangoWC")
model: [I18n.tr("Auto"), I18n.tr("Custom"), I18n.tr("Off")]
currentIndex: {
if (SettingsData.mangoLayoutGapsOverride === -2)
return 2;
return SettingsData.mangoLayoutGapsOverride >= 0 ? 1 : 0;
}
onSelectionChanged: (index, selected) => {
if (!selected)
return;
switch (index) {
case 1:
SettingsData.set("mangoLayoutGapsOverride", Math.max(4, (SettingsData.getPrimaryBarConfig()?.spacing ?? 4)));
return;
case 2:
SettingsData.set("mangoLayoutGapsOverride", -2);
return;
default:
SettingsData.set("mangoLayoutGapsOverride", -1);
}
}
}
SettingsSliderRow {
tags: ["mangowc", "mango", "gaps", "override", "inner", "gappih", "gappiv"]
settingKey: "mangoLayoutGapsOverride"
resetKeys: []
text: I18n.tr("Inner gaps")
visible: SettingsData.mangoLayoutGapsOverride >= 0
value: Math.max(0, SettingsData.mangoLayoutGapsOverride)
minimum: 0
maximum: 50
unit: "px"
onSliderValueChanged: newValue => SettingsData.set("mangoLayoutGapsOverride", newValue)
}
SettingsSliderRow {
tags: ["mangowc", "mango", "gaps", "override", "outer", "edge", "gappoh", "gappov"]
settingKey: "mangoLayoutGapsOutOverride"
text: I18n.tr("Outer gaps")
visible: SettingsData.mangoLayoutGapsOverride >= 0
value: SettingsData.mangoLayoutGapsOutOverride >= 0 ? SettingsData.mangoLayoutGapsOutOverride : Math.max(0, SettingsData.mangoLayoutGapsOverride)
minimum: 0
maximum: 50
unit: "px"
onSliderValueChanged: newValue => SettingsData.set("mangoLayoutGapsOutOverride", newValue)
}
SettingsControlledBy {
target: "surfaces"
parentModal: root.parentModal
section: "windowRadius"
settingLabel: I18n.tr("Window radius")
reason: Math.round(Theme.windowRadius) + "px"
}
SettingsToggleSliderRow {
tags: ["mangowc", "mango", "border", "override", "borderpx"]
settingKey: "mangoLayoutBorderSizeEnabled"
resetKeys: ["mangoLayoutBorderSize"]
text: I18n.tr("Override border size")
checked: SettingsData.mangoLayoutBorderSize >= 0
value: Math.max(0, SettingsData.mangoLayoutBorderSize)
minimum: 0
maximum: 10
unit: "px"
onToggled: checked => SettingsData.set("mangoLayoutBorderSize", checked ? 2 : -1)
onSliderValueChanged: newValue => SettingsData.set("mangoLayoutBorderSize", newValue)
}
}
}
}