1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-05-10 22:39:37 -04:00

Compare commits

...

12 Commits
blur ... frame

Author SHA1 Message Date
purian23
1217b25de5 (frame): Add blur support & cleanup 2026-03-31 21:25:51 -04:00
purian23
e913630f90 (frame): Multi-monitor support 2026-03-31 15:31:43 -04:00
purian23
220bb2708b Connected frames & defaults 2026-03-31 15:31:43 -04:00
purian23
e57ab3e1f3 Continue frame implementation 2026-03-31 15:31:43 -04:00
purian23
952ab9b753 Initial framework 2026-03-31 15:31:43 -04:00
bbedward
28f9aabcd9 screenshot: fix scaling of global coordinate space when using all
screens
2026-03-31 15:13:10 -04:00
bbedward
3d9bd73336 launcher: some polishes for blur 2026-03-31 11:04:18 -04:00
bbedward
3497d5f523 blur: stylize control center for blur mode 2026-03-31 09:42:08 -04:00
bbedward
8ef1d95e65 popout: fix inconsistent transparency 2026-03-31 09:06:48 -04:00
bbedward
e9aeb9ac60 blur: add probe to check compositor for ext-bg-effect 2026-03-30 15:18:44 -04:00
bbedward
fb02f7294d workspace: fix mouse area to edges
fixes #2108
2026-03-30 15:16:17 -04:00
bbedward
f15d49d80a blur: add blur support with ext-bg-effect 2026-03-30 11:52:35 -04:00
62 changed files with 2070 additions and 265 deletions

View File

@@ -0,0 +1,40 @@
package main
import (
"fmt"
"os"
"github.com/AvengeMedia/DankMaterialShell/core/internal/blur"
"github.com/spf13/cobra"
)
var blurCmd = &cobra.Command{
Use: "blur",
Short: "Background blur utilities",
}
var blurCheckCmd = &cobra.Command{
Use: "check",
Short: "Check if the compositor supports background blur (ext-background-effect-v1)",
Args: cobra.NoArgs,
Run: runBlurCheck,
}
func init() {
blurCmd.AddCommand(blurCheckCmd)
}
func runBlurCheck(cmd *cobra.Command, args []string) {
supported, err := blur.ProbeSupport()
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
switch supported {
case true:
fmt.Println("supported")
default:
fmt.Println("unsupported")
}
}

View File

@@ -525,5 +525,6 @@ func getCommonCommands() []*cobra.Command {
configCmd, configCmd,
dlCmd, dlCmd,
randrCmd, randrCmd,
blurCmd,
} }
} }

View File

@@ -0,0 +1,35 @@
package blur
import (
wlhelpers "github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/client"
client "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
)
const extBackgroundEffectInterface = "ext_background_effect_manager_v1"
func ProbeSupport() (bool, error) {
display, err := client.Connect("")
if err != nil {
return false, err
}
defer display.Context().Close()
registry, err := display.GetRegistry()
if err != nil {
return false, err
}
found := false
registry.SetGlobalHandler(func(e client.RegistryGlobalEvent) {
switch e.Interface {
case extBackgroundEffectInterface:
found = true
}
})
if err := wlhelpers.Roundtrip(display, display.Context()); err != nil {
return false, err
}
return found, nil
}

View File

@@ -444,20 +444,21 @@ func GetFocusedMonitor() string {
type outputInfo struct { type outputInfo struct {
x, y int32 x, y int32
scale float64
transform int32 transform int32
} }
func getOutputInfo(outputName string) (*outputInfo, bool) { func getAllOutputInfos() map[string]*outputInfo {
display, err := client.Connect("") display, err := client.Connect("")
if err != nil { if err != nil {
return nil, false return nil
} }
ctx := display.Context() ctx := display.Context()
defer ctx.Close() defer ctx.Close()
registry, err := display.GetRegistry() registry, err := display.GetRegistry()
if err != nil { if err != nil {
return nil, false return nil
} }
var outputManager *wlr_output_management.ZwlrOutputManagerV1 var outputManager *wlr_output_management.ZwlrOutputManagerV1
@@ -476,16 +477,17 @@ func getOutputInfo(outputName string) (*outputInfo, bool) {
}) })
if err := wlhelpers.Roundtrip(display, ctx); err != nil { if err := wlhelpers.Roundtrip(display, ctx); err != nil {
return nil, false return nil
} }
if outputManager == nil { if outputManager == nil {
return nil, false return nil
} }
type headState struct { type headState struct {
name string name string
x, y int32 x, y int32
scale float64
transform int32 transform int32
} }
heads := make(map[*wlr_output_management.ZwlrOutputHeadV1]*headState) heads := make(map[*wlr_output_management.ZwlrOutputHeadV1]*headState)
@@ -501,6 +503,9 @@ func getOutputInfo(outputName string) (*outputInfo, bool) {
state.x = pe.X state.x = pe.X
state.y = pe.Y state.y = pe.Y
}) })
e.Head.SetScaleHandler(func(se wlr_output_management.ZwlrOutputHeadV1ScaleEvent) {
state.scale = se.Scale
})
e.Head.SetTransformHandler(func(te wlr_output_management.ZwlrOutputHeadV1TransformEvent) { e.Head.SetTransformHandler(func(te wlr_output_management.ZwlrOutputHeadV1TransformEvent) {
state.transform = te.Transform state.transform = te.Transform
}) })
@@ -511,21 +516,32 @@ func getOutputInfo(outputName string) (*outputInfo, bool) {
for !done { for !done {
if err := ctx.Dispatch(); err != nil { if err := ctx.Dispatch(); err != nil {
return nil, false return nil
} }
} }
result := make(map[string]*outputInfo, len(heads))
for _, state := range heads { for _, state := range heads {
if state.name == outputName { if state.name == "" {
return &outputInfo{ continue
x: state.x, }
y: state.y, result[state.name] = &outputInfo{
transform: state.transform, x: state.x,
}, true y: state.y,
scale: state.scale,
transform: state.transform,
} }
} }
return result
}
return nil, false func getOutputInfo(outputName string) (*outputInfo, bool) {
infos := getAllOutputInfos()
if infos == nil {
return nil, false
}
info, ok := infos[outputName]
return info, ok
} }
func getDWLActiveWindow() (*WindowGeometry, error) { func getDWLActiveWindow() (*WindowGeometry, error) {

View File

@@ -2,6 +2,7 @@ package screenshot
import ( import (
"fmt" "fmt"
"math"
"sync" "sync"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
@@ -304,22 +305,20 @@ func (s *Screenshoter) captureAllScreens() (*CaptureResult, error) {
if len(outputs) == 0 { if len(outputs) == 0 {
return nil, fmt.Errorf("no outputs available") return nil, fmt.Errorf("no outputs available")
} }
if len(outputs) == 1 { if len(outputs) == 1 {
return s.captureWholeOutput(outputs[0]) return s.captureWholeOutput(outputs[0])
} }
// Capture all outputs first to get actual buffer sizes wlrInfos := getAllOutputInfos()
type capturedOutput struct {
output *WaylandOutput
result *CaptureResult
physX int
physY int
}
captured := make([]capturedOutput, 0, len(outputs))
var minX, minY, maxX, maxY int type pendingOutput struct {
first := true result *CaptureResult
logX float64
logY float64
scale float64
}
var pending []pendingOutput
maxScale := 1.0
for _, output := range outputs { for _, output := range outputs {
result, err := s.captureWholeOutput(output) result, err := s.captureWholeOutput(output)
@@ -328,50 +327,74 @@ func (s *Screenshoter) captureAllScreens() (*CaptureResult, error) {
continue continue
} }
outX, outY := output.x, output.y logX, logY := float64(output.x), float64(output.y)
scale := float64(output.scale) scale := float64(output.scale)
switch DetectCompositor() { switch DetectCompositor() {
case CompositorHyprland: case CompositorHyprland:
if hx, hy, _, _, ok := GetHyprlandMonitorGeometry(output.name); ok { if hx, hy, _, _, ok := GetHyprlandMonitorGeometry(output.name); ok {
outX, outY = hx, hy logX, logY = float64(hx), float64(hy)
} }
if s := GetHyprlandMonitorScale(output.name); s > 0 { if hs := GetHyprlandMonitorScale(output.name); hs > 0 {
scale = s scale = hs
} }
case CompositorDWL: default:
if info, ok := getOutputInfo(output.name); ok { if wlrInfos != nil {
outX, outY = info.x, info.y if info, ok := wlrInfos[output.name]; ok {
logX, logY = float64(info.x), float64(info.y)
if info.scale > 0 {
scale = info.scale
}
}
} }
} }
if scale <= 0 { if scale <= 0 {
scale = 1.0 scale = 1.0
} }
physX := int(float64(outX) * scale) pending = append(pending, pendingOutput{result: result, logX: logX, logY: logY, scale: scale})
physY := int(float64(outY) * scale) if scale > maxScale {
maxScale = scale
}
}
captured = append(captured, capturedOutput{ if len(pending) == 0 {
output: output, return nil, fmt.Errorf("failed to capture any outputs")
result: result, }
physX: physX, if len(pending) == 1 {
physY: physY, return pending[0].result, nil
}) }
right := physX + result.Buffer.Width type layoutEntry struct {
bottom := physY + result.Buffer.Height result *CaptureResult
canvasX int
canvasY int
canvasW int
canvasH int
}
entries := make([]layoutEntry, len(pending))
var minX, minY, maxX, maxY int
if first { for i, p := range pending {
minX, minY = physX, physY cx := int(math.Round(p.logX * maxScale))
maxX, maxY = right, bottom cy := int(math.Round(p.logY * maxScale))
first = false cw := int(math.Round(float64(p.result.Buffer.Width) * maxScale / p.scale))
ch := int(math.Round(float64(p.result.Buffer.Height) * maxScale / p.scale))
entries[i] = layoutEntry{result: p.result, canvasX: cx, canvasY: cy, canvasW: cw, canvasH: ch}
right := cx + cw
bottom := cy + ch
if i == 0 {
minX, minY, maxX, maxY = cx, cy, right, bottom
continue continue
} }
if cx < minX {
if physX < minX { minX = cx
minX = physX
} }
if physY < minY { if cy < minY {
minY = physY minY = cy
} }
if right > maxX { if right > maxX {
maxX = right maxX = right
@@ -381,35 +404,26 @@ func (s *Screenshoter) captureAllScreens() (*CaptureResult, error) {
} }
} }
if len(captured) == 0 {
return nil, fmt.Errorf("failed to capture any outputs")
}
if len(captured) == 1 {
return captured[0].result, nil
}
totalW := maxX - minX totalW := maxX - minX
totalH := maxY - minY totalH := maxY - minY
composite, err := CreateShmBuffer(totalW, totalH, totalW*4)
compositeStride := totalW * 4
composite, err := CreateShmBuffer(totalW, totalH, compositeStride)
if err != nil { if err != nil {
for _, c := range captured { for _, e := range entries {
c.result.Buffer.Close() e.result.Buffer.Close()
} }
return nil, fmt.Errorf("create composite buffer: %w", err) return nil, fmt.Errorf("create composite buffer: %w", err)
} }
composite.Clear() composite.Clear()
var format uint32 var format uint32
for _, c := range captured { for _, e := range entries {
if format == 0 { if format == 0 {
format = c.result.Format format = e.result.Format
} }
s.blitBuffer(composite, c.result.Buffer, c.physX-minX, c.physY-minY, c.result.YInverted) s.blitBufferScaled(composite, e.result.Buffer,
c.result.Buffer.Close() e.canvasX-minX, e.canvasY-minY, e.canvasW, e.canvasH,
e.result.YInverted)
e.result.Buffer.Close()
} }
return &CaptureResult{ return &CaptureResult{
@@ -419,32 +433,44 @@ func (s *Screenshoter) captureAllScreens() (*CaptureResult, error) {
}, nil }, nil
} }
func (s *Screenshoter) blitBuffer(dst, src *ShmBuffer, dstX, dstY int, yInverted bool) { func (s *Screenshoter) blitBufferScaled(dst, src *ShmBuffer, dstX, dstY, dstW, dstH int, yInverted bool) {
if dstW <= 0 || dstH <= 0 {
return
}
srcData := src.Data() srcData := src.Data()
dstData := dst.Data() dstData := dst.Data()
for srcY := 0; srcY < src.Height; srcY++ { for dy := 0; dy < dstH; dy++ {
actualSrcY := srcY canvasY := dstY + dy
if yInverted { if canvasY < 0 || canvasY >= dst.Height {
actualSrcY = src.Height - 1 - srcY
}
dy := dstY + srcY
if dy < 0 || dy >= dst.Height {
continue continue
} }
srcRowOff := actualSrcY * src.Stride srcY := dy * src.Height / dstH
dstRowOff := dy * dst.Stride if yInverted {
srcY = src.Height - 1 - srcY
}
if srcY < 0 || srcY >= src.Height {
continue
}
for srcX := 0; srcX < src.Width; srcX++ { srcRowOff := srcY * src.Stride
dx := dstX + srcX dstRowOff := canvasY * dst.Stride
if dx < 0 || dx >= dst.Width {
for dx := 0; dx < dstW; dx++ {
canvasX := dstX + dx
if canvasX < 0 || canvasX >= dst.Width {
continue
}
srcX := dx * src.Width / dstW
if srcX >= src.Width {
continue continue
} }
si := srcRowOff + srcX*4 si := srcRowOff + srcX*4
di := dstRowOff + dx*4 di := dstRowOff + canvasX*4
if si+3 >= len(srcData) || di+3 >= len(dstData) { if si+3 >= len(srcData) || di+3 >= len(dstData) {
continue continue

View File

@@ -14,7 +14,7 @@ import "settings/SettingsStore.js" as Store
Singleton { Singleton {
id: root id: root
readonly property int settingsConfigVersion: 5 readonly property int settingsConfigVersion: 11
readonly property bool isGreeterMode: Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true" readonly property bool isGreeterMode: Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true"
@@ -186,10 +186,46 @@ Singleton {
onPopoutElevationEnabledChanged: saveSettings() onPopoutElevationEnabledChanged: saveSettings()
property bool barElevationEnabled: true property bool barElevationEnabled: true
onBarElevationEnabledChanged: saveSettings() onBarElevationEnabledChanged: saveSettings()
property bool blurEnabled: false
onBlurEnabledChanged: saveSettings()
property string blurBorderColor: "outline"
onBlurBorderColorChanged: saveSettings()
property string blurBorderCustomColor: "#ffffff"
onBlurBorderCustomColorChanged: saveSettings()
property real blurBorderOpacity: 1.0
onBlurBorderOpacityChanged: saveSettings()
property string wallpaperFillMode: "Fill" property string wallpaperFillMode: "Fill"
property bool blurredWallpaperLayer: false property bool blurredWallpaperLayer: false
property bool blurWallpaperOnOverview: false property bool blurWallpaperOnOverview: false
property bool frameEnabled: false
onFrameEnabledChanged: saveSettings()
property real frameThickness: 16
onFrameThicknessChanged: saveSettings()
property real frameRounding: 23
onFrameRoundingChanged: saveSettings()
property string frameColor: ""
onFrameColorChanged: saveSettings()
property real frameOpacity: 1.0
onFrameOpacityChanged: saveSettings()
property var frameScreenPreferences: ["all"]
onFrameScreenPreferencesChanged: saveSettings()
property real frameBarSize: 40
onFrameBarSizeChanged: saveSettings()
property bool frameShowOnOverview: false
onFrameShowOnOverviewChanged: saveSettings()
property bool frameBlurEnabled: true
onFrameBlurEnabledChanged: saveSettings()
readonly property color effectiveFrameColor: {
const fc = frameColor;
if (!fc || fc === "default") return Theme.surfaceContainer;
if (fc === "primary") return Theme.primary;
if (fc === "surface") return Theme.surface;
return fc;
}
property bool showLauncherButton: true property bool showLauncherButton: true
property bool showWorkspaceSwitcher: true property bool showWorkspaceSwitcher: true
property bool showFocusedWindow: true property bool showFocusedWindow: true
@@ -1930,6 +1966,66 @@ Singleton {
return filtered; return filtered;
} }
function getFrameFilteredScreens() {
var prefs = frameScreenPreferences || ["all"];
if (!prefs || prefs.length === 0 || prefs.includes("all")) {
return Quickshell.screens;
}
return Quickshell.screens.filter(screen => isScreenInPreferences(screen, prefs));
}
function getActiveBarEdgeForScreen(screen) {
if (!screen) return "";
for (var i = 0; i < barConfigs.length; i++) {
var bc = barConfigs[i];
if (!bc.enabled) continue;
var prefs = bc.screenPreferences || ["all"];
if (!prefs.includes("all") && !isScreenInPreferences(screen, prefs)) continue;
switch (bc.position ?? 0) {
case SettingsData.Position.Top: return "top";
case SettingsData.Position.Bottom: return "bottom";
case SettingsData.Position.Left: return "left";
case SettingsData.Position.Right: return "right";
}
}
return "";
}
function getActiveBarEdgesForScreen(screen) {
if (!screen) return [];
var edges = [];
for (var i = 0; i < barConfigs.length; i++) {
var bc = barConfigs[i];
if (!bc.enabled) continue;
var prefs = bc.screenPreferences || ["all"];
if (!prefs.includes("all") && !isScreenInPreferences(screen, prefs)) continue;
switch (bc.position ?? 0) {
case SettingsData.Position.Top: edges.push("top"); break;
case SettingsData.Position.Bottom: edges.push("bottom"); break;
case SettingsData.Position.Left: edges.push("left"); break;
case SettingsData.Position.Right: edges.push("right"); break;
}
}
return edges;
}
function getActiveBarThicknessForScreen(screen) {
if (frameEnabled) return frameBarSize;
if (!screen) return frameThickness;
for (var i = 0; i < barConfigs.length; i++) {
var bc = barConfigs[i];
if (!bc.enabled) continue;
var prefs = bc.screenPreferences || ["all"];
if (!prefs.includes("all") && !isScreenInPreferences(screen, prefs)) continue;
const innerPadding = bc.innerPadding ?? 4;
const barT = Math.max(26 + innerPadding * 0.6, Theme.barHeight - 4 - (8 - innerPadding));
const spacing = bc.spacing ?? 4;
const bottomGap = bc.bottomGap ?? 0;
return barT + spacing + bottomGap;
}
return frameThickness;
}
function sendTestNotifications() { function sendTestNotifications() {
NotificationService.dismissAllPopups(); NotificationService.dismissAllPopups();
sendTestNotification(0); sendTestNotification(0);

View File

@@ -58,6 +58,10 @@ var SPEC = {
modalElevationEnabled: { def: true }, modalElevationEnabled: { def: true },
popoutElevationEnabled: { def: true }, popoutElevationEnabled: { def: true },
barElevationEnabled: { def: true }, barElevationEnabled: { def: true },
blurEnabled: { def: false },
blurBorderColor: { def: "outline" },
blurBorderCustomColor: { def: "#ffffff" },
blurBorderOpacity: { def: 1.0, coerce: percentToUnit },
wallpaperFillMode: { def: "Fill" }, wallpaperFillMode: { def: "Fill" },
blurredWallpaperLayer: { def: false }, blurredWallpaperLayer: { def: false },
blurWallpaperOnOverview: { def: false }, blurWallpaperOnOverview: { def: false },
@@ -543,7 +547,17 @@ var SPEC = {
clipboardEnterToPaste: { def: false }, clipboardEnterToPaste: { def: false },
launcherPluginVisibility: { def: {} }, launcherPluginVisibility: { def: {} },
launcherPluginOrder: { def: [] } launcherPluginOrder: { def: [] },
frameEnabled: { def: false },
frameThickness: { def: 16 },
frameRounding: { def: 23 },
frameColor: { def: "" },
frameOpacity: { def: 1.0 },
frameScreenPreferences: { def: ["all"] },
frameBarSize: { def: 40 },
frameShowOnOverview: { def: false },
frameBlurEnabled: { def: true }
}; };
function getValidKeys() { function getValidKeys() {

View File

@@ -248,6 +248,10 @@ function migrateToVersion(obj, targetVersion) {
settings.configVersion = 6; settings.configVersion = 6;
} }
if (currentVersion < 11) {
settings.configVersion = 11;
}
return settings; return settings;
} }

View File

@@ -21,6 +21,7 @@ import qs.Modules.OSD
import qs.Modules.ProcessList import qs.Modules.ProcessList
import qs.Modules.DankBar import qs.Modules.DankBar
import qs.Modules.DankBar.Popouts import qs.Modules.DankBar.Popouts
import qs.Modules.Frame
import qs.Modules.WorkspaceOverlays import qs.Modules.WorkspaceOverlays
import qs.Services import qs.Services
@@ -176,6 +177,8 @@ Item {
} }
} }
Frame {}
Repeater { Repeater {
id: dankBarRepeater id: dankBarRepeater
model: ScriptModel { model: ScriptModel {

View File

@@ -3,6 +3,7 @@ import Quickshell
import Quickshell.Wayland import Quickshell.Wayland
import qs.Common import qs.Common
import qs.Services import qs.Services
import qs.Widgets
Item { Item {
id: root id: root
@@ -30,7 +31,7 @@ Item {
property real animationOffset: Theme.spacingL property real animationOffset: Theme.spacingL
property list<real> animationEnterCurve: Theme.expressiveCurves.expressiveDefaultSpatial property list<real> animationEnterCurve: Theme.expressiveCurves.expressiveDefaultSpatial
property list<real> animationExitCurve: Theme.expressiveCurves.emphasized property list<real> animationExitCurve: Theme.expressiveCurves.emphasized
property color backgroundColor: Theme.surfaceContainer property color backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
property color borderColor: Theme.outlineMedium property color borderColor: Theme.outlineMedium
property real borderWidth: 0 property real borderWidth: 0
property real cornerRadius: Theme.cornerRadius property real cornerRadius: Theme.cornerRadius
@@ -59,11 +60,25 @@ Item {
function open() { function open() {
closeTimer.stop(); closeTimer.stop();
const focusedScreen = CompositorService.getFocusedScreen(); const focusedScreen = CompositorService.getFocusedScreen();
const screenChanged = focusedScreen && contentWindow.screen !== focusedScreen;
if (focusedScreen) { if (focusedScreen) {
if (screenChanged)
contentWindow.visible = false;
contentWindow.screen = focusedScreen; contentWindow.screen = focusedScreen;
if (!useSingleWindow) if (!useSingleWindow) {
if (screenChanged)
clickCatcher.visible = false;
clickCatcher.screen = focusedScreen; clickCatcher.screen = focusedScreen;
}
} }
if (screenChanged) {
Qt.callLater(() => root._finishOpen());
} else {
_finishOpen();
}
}
function _finishOpen() {
ModalManager.openModal(root); ModalManager.openModal(root);
shouldBeVisible = true; shouldBeVisible = true;
if (!useSingleWindow) if (!useSingleWindow)
@@ -215,6 +230,16 @@ Item {
visible: false visible: false
color: "transparent" color: "transparent"
WindowBlur {
targetWindow: contentWindow
readonly property real s: Math.min(1, modalContainer.scaleValue)
blurX: modalContainer.x + modalContainer.width * (1 - s) * 0.5 + Theme.snap(modalContainer.animX, root.dpr)
blurY: modalContainer.y + modalContainer.height * (1 - s) * 0.5 + Theme.snap(modalContainer.animY, root.dpr)
blurWidth: (shouldBeVisible && animatedContent.opacity > 0) ? modalContainer.width * s : 0
blurHeight: (shouldBeVisible && animatedContent.opacity > 0) ? modalContainer.height * s : 0
blurRadius: root.cornerRadius
}
WlrLayershell.namespace: root.layerNamespace WlrLayershell.namespace: root.layerNamespace
WlrLayershell.layer: { WlrLayershell.layer: {
if (root.useOverlayLayer) if (root.useOverlayLayer)
@@ -393,6 +418,15 @@ Item {
shadowEnabled: root.enableShadow && Theme.elevationEnabled && SettingsData.modalElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1" shadowEnabled: root.enableShadow && Theme.elevationEnabled && SettingsData.modalElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1"
} }
Rectangle {
anchors.fill: parent
radius: root.cornerRadius
color: "transparent"
border.color: BlurService.borderColor
border.width: BlurService.borderWidth
z: 100
}
FocusScope { FocusScope {
anchors.fill: parent anchors.fill: parent
focus: root.shouldBeVisible focus: root.shouldBeVisible

View File

@@ -132,7 +132,7 @@ DankModal {
modalWidth: 680 modalWidth: 680
modalHeight: contentLoader.item ? contentLoader.item.implicitHeight + Theme.spacingM * 2 : 680 modalHeight: contentLoader.item ? contentLoader.item.implicitHeight + Theme.spacingM * 2 : 680
backgroundColor: Theme.surfaceContainer backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
cornerRadius: Theme.cornerRadius cornerRadius: Theme.cornerRadius
borderColor: Theme.outlineMedium borderColor: Theme.outlineMedium
borderWidth: 1 borderWidth: 1

View File

@@ -4,6 +4,7 @@ import Quickshell.Wayland
import Quickshell.Hyprland import Quickshell.Hyprland
import qs.Common import qs.Common
import qs.Services import qs.Services
import qs.Widgets
Item { Item {
id: root id: root
@@ -134,40 +135,47 @@ Item {
} }
} }
function show() { function _finishShow(query, mode) {
closeCleanupTimer.stop(); spotlightOpen = true;
isClosing = false; isClosing = false;
openedFromOverview = false; openedFromOverview = false;
var focusedScreen = CompositorService.getFocusedScreen();
if (focusedScreen)
launcherWindow.screen = focusedScreen;
spotlightOpen = true;
keyboardActive = true; keyboardActive = true;
ModalManager.openModal(root); ModalManager.openModal(root);
if (useHyprlandFocusGrab) if (useHyprlandFocusGrab)
focusGrab.active = true; focusGrab.active = true;
_ensureContentLoadedAndInitialize("", ""); _ensureContentLoadedAndInitialize(query || "", mode || "");
}
function show() {
closeCleanupTimer.stop();
var focusedScreen = CompositorService.getFocusedScreen();
if (focusedScreen && launcherWindow.screen !== focusedScreen) {
spotlightOpen = false;
isClosing = false;
launcherWindow.screen = focusedScreen;
Qt.callLater(() => root._finishShow("", ""));
return;
}
_finishShow("", "");
} }
function showWithQuery(query) { function showWithQuery(query) {
closeCleanupTimer.stop(); closeCleanupTimer.stop();
isClosing = false;
openedFromOverview = false;
var focusedScreen = CompositorService.getFocusedScreen(); var focusedScreen = CompositorService.getFocusedScreen();
if (focusedScreen) if (focusedScreen && launcherWindow.screen !== focusedScreen) {
spotlightOpen = false;
isClosing = false;
launcherWindow.screen = focusedScreen; launcherWindow.screen = focusedScreen;
Qt.callLater(() => root._finishShow(query, ""));
return;
}
spotlightOpen = true; _finishShow(query, "");
keyboardActive = true;
ModalManager.openModal(root);
if (useHyprlandFocusGrab)
focusGrab.active = true;
_ensureContentLoadedAndInitialize(query, "");
} }
function hide() { function hide() {
@@ -191,14 +199,20 @@ Item {
function showWithMode(mode) { function showWithMode(mode) {
closeCleanupTimer.stop(); closeCleanupTimer.stop();
var focusedScreen = CompositorService.getFocusedScreen();
if (focusedScreen && launcherWindow.screen !== focusedScreen) {
spotlightOpen = false;
isClosing = false;
launcherWindow.screen = focusedScreen;
Qt.callLater(() => root._finishShow("", mode));
return;
}
spotlightOpen = true;
isClosing = false; isClosing = false;
openedFromOverview = false; openedFromOverview = false;
var focusedScreen = CompositorService.getFocusedScreen();
if (focusedScreen)
launcherWindow.screen = focusedScreen;
spotlightOpen = true;
keyboardActive = true; keyboardActive = true;
ModalManager.openModal(root); ModalManager.openModal(root);
if (useHyprlandFocusGrab) if (useHyprlandFocusGrab)
@@ -295,6 +309,16 @@ Item {
color: "transparent" color: "transparent"
exclusionMode: ExclusionMode.Ignore exclusionMode: ExclusionMode.Ignore
WindowBlur {
targetWindow: launcherWindow
readonly property real s: Math.min(1, modalContainer.scale)
blurX: root.modalX + root.modalWidth * (1 - s) * 0.5
blurY: root.modalY + root.modalHeight * (1 - s) * 0.5
blurWidth: (contentVisible && modalContainer.opacity > 0) ? root.modalWidth * s : 0
blurHeight: (contentVisible && modalContainer.opacity > 0) ? root.modalHeight * s : 0
blurRadius: root.cornerRadius
}
WlrLayershell.namespace: "dms:spotlight" WlrLayershell.namespace: "dms:spotlight"
WlrLayershell.layer: { WlrLayershell.layer: {
switch (Quickshell.env("DMS_MODAL_LAYER")) { switch (Quickshell.env("DMS_MODAL_LAYER")) {
@@ -428,6 +452,14 @@ Item {
event.accepted = true; event.accepted = true;
} }
} }
Rectangle {
anchors.fill: parent
radius: root.cornerRadius
color: "transparent"
border.color: BlurService.borderColor
border.width: BlurService.borderWidth
}
} }
} }
} }

View File

@@ -311,7 +311,7 @@ FocusScope {
Item { Item {
anchors.fill: parent anchors.fill: parent
visible: !editMode visible: !editMode && !(root.parentModal?.isClosing ?? false)
Item { Item {
id: footerBar id: footerBar
@@ -737,8 +737,6 @@ FocusScope {
Item { Item {
width: parent.width width: parent.width
height: parent.height - searchField.height - categoryRow.height - fileFilterRow.height - actionPanel.height - Theme.spacingXS * ((categoryRow.visible ? 1 : 0) + (fileFilterRow.visible ? 1 : 0) + 2) height: parent.height - searchField.height - categoryRow.height - fileFilterRow.height - actionPanel.height - Theme.spacingXS * ((categoryRow.visible ? 1 : 0) + (fileFilterRow.visible ? 1 : 0) + 2)
opacity: root.parentModal?.isClosing ? 0 : 1
ResultsList { ResultsList {
id: resultsList id: resultsList
anchors.fill: parent anchors.fill: parent

View File

@@ -324,6 +324,8 @@ Item {
height: 24 height: 24
z: 100 z: 100
visible: { visible: {
if (BlurService.enabled)
return false;
if (mainListView.contentHeight <= mainListView.height) if (mainListView.contentHeight <= mainListView.height)
return false; return false;
var atBottom = mainListView.contentY >= mainListView.contentHeight - mainListView.height + mainListView.originY - 5; var atBottom = mainListView.contentY >= mainListView.contentHeight - mainListView.height + mainListView.originY - 5;
@@ -449,7 +451,7 @@ Item {
case "apps": case "apps":
return "apps"; return "apps";
default: default:
return root.controller?.searchQuery?.length > 0 ? "search_off" : "search"; return "search_off";
} }
} }
} }
@@ -485,9 +487,9 @@ Item {
case "plugins": case "plugins":
return hasQuery ? I18n.tr("No plugin results") : I18n.tr("Browse or search plugins"); return hasQuery ? I18n.tr("No plugin results") : I18n.tr("Browse or search plugins");
case "apps": case "apps":
return hasQuery ? I18n.tr("No apps found") : I18n.tr("Type to search apps"); return I18n.tr("No apps found");
default: default:
return hasQuery ? I18n.tr("No results found") : I18n.tr("Type to search"); return I18n.tr("No results found");
} }
} }
} }

View File

@@ -518,5 +518,20 @@ FocusScope {
Qt.callLater(() => item.forceActiveFocus()); Qt.callLater(() => item.forceActiveFocus());
} }
} }
Loader {
id: frameLoader
anchors.fill: parent
active: root.currentIndex === 33
visible: active
focus: active
sourceComponent: FrameTab {}
onActiveChanged: {
if (active && item)
Qt.callLater(() => item.forceActiveFocus());
}
}
} }
} }

View File

@@ -120,6 +120,12 @@ Rectangle {
"text": I18n.tr("Widgets"), "text": I18n.tr("Widgets"),
"icon": "widgets", "icon": "widgets",
"tabIndex": 22 "tabIndex": 22
},
{
"id": "frame",
"text": I18n.tr("Frame"),
"icon": "frame_source",
"tabIndex": 33
} }
] ]
}, },

View File

@@ -133,7 +133,7 @@ DankPopout {
QtObject { QtObject {
id: modalAdapter id: modalAdapter
property bool spotlightOpen: appDrawerPopout.shouldBeVisible property bool spotlightOpen: appDrawerPopout.shouldBeVisible
property bool isClosing: false readonly property bool isClosing: !appDrawerPopout.shouldBeVisible
function hide() { function hide() {
appDrawerPopout.close(); appDrawerPopout.close();

View File

@@ -34,7 +34,7 @@ PluginComponent {
id: detailRoot id: detailRoot
implicitHeight: detailColumn.implicitHeight + Theme.spacingM * 2 implicitHeight: detailColumn.implicitHeight + Theme.spacingM * 2
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
DankActionButton { DankActionButton {
anchors.top: parent.top anchors.top: parent.top
@@ -252,7 +252,7 @@ PluginComponent {
width: parent ? parent.width : 300 width: parent ? parent.width : 300
height: 50 height: 50
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Theme.surfaceContainerHighest color: Theme.surfaceLight
border.width: 1 border.width: 1
border.color: Theme.outlineLight border.color: Theme.outlineLight
opacity: 1.0 opacity: 1.0

View File

@@ -33,7 +33,7 @@ Row {
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
background: Rectangle { background: Rectangle {
color: Theme.surfaceContainer color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
border.color: Theme.primarySelected border.color: Theme.primarySelected
border.width: 0 border.width: 0
radius: Theme.cornerRadius radius: Theme.cornerRadius

View File

@@ -207,9 +207,9 @@ Rectangle {
width: parent.width width: parent.width
height: 50 height: 50
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: deviceMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency) color: deviceMouseArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceLight
border.color: modelData === AudioService.source ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) border.color: modelData === AudioService.source ? Theme.primary : Theme.outlineLight
border.width: 0 border.width: modelData === AudioService.source ? 2 : 1
Row { Row {
anchors.left: parent.left anchors.left: parent.left

View File

@@ -218,9 +218,9 @@ Rectangle {
width: parent.width width: parent.width
height: 50 height: 50
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: deviceMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency) color: deviceMouseArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceLight
border.color: modelData === AudioService.sink ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) border.color: modelData === AudioService.sink ? Theme.primary : Theme.outlineLight
border.width: 0 border.width: modelData === AudioService.sink ? 2 : 1
DankRipple { DankRipple {
id: deviceRipple id: deviceRipple
@@ -397,9 +397,9 @@ Rectangle {
width: parent.width width: parent.width
height: 50 height: 50
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency) color: Theme.surfaceLight
border.color: modelData === AudioService.sink ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) border.color: modelData === AudioService.sink ? Theme.primary : Theme.outlineLight
border.width: 0 border.width: modelData === AudioService.sink ? 2 : 1
Row { Row {
anchors.left: parent.left anchors.left: parent.left

View File

@@ -129,8 +129,9 @@ Rectangle {
width: (parent.width - Theme.spacingM) / 2 width: (parent.width - Theme.spacingM) / 2
height: 64 height: 64
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency) color: Theme.surfaceLight
border.width: 0 border.color: Theme.outlineLight
border.width: 1
Column { Column {
anchors.centerIn: parent anchors.centerIn: parent
@@ -164,8 +165,9 @@ Rectangle {
width: (parent.width - Theme.spacingM) / 2 width: (parent.width - Theme.spacingM) / 2
height: 64 height: 64
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency) color: Theme.surfaceLight
border.width: 0 border.color: Theme.outlineLight
border.width: 1
Column { Column {
anchors.centerIn: parent anchors.centerIn: parent

View File

@@ -153,7 +153,7 @@ Item {
width: 320 width: 320
height: contentColumn.implicitHeight + Theme.spacingL * 2 height: contentColumn.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Theme.surfaceContainer color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 0 border.width: 0
opacity: modalVisible ? 1 : 0 opacity: modalVisible ? 1 : 0

View File

@@ -229,7 +229,6 @@ Rectangle {
width: parent.width width: parent.width
height: 50 height: 50
radius: Theme.cornerRadius radius: Theme.cornerRadius
border.width: 0
Component.onCompleted: { Component.onCompleted: {
if (!isConnected) if (!isConnected)
@@ -243,8 +242,8 @@ Rectangle {
if (isConnecting) if (isConnecting)
return Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.12); return Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.12);
if (deviceMouseArea.containsMouse) if (deviceMouseArea.containsMouse)
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08); return Theme.primaryHoverLight;
return Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency); return Theme.surfaceLight;
} }
border.color: { border.color: {
@@ -252,8 +251,9 @@ Rectangle {
return Theme.warning; return Theme.warning;
if (isConnected) if (isConnected)
return Theme.primary; return Theme.primary;
return Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12); return Theme.outlineLight;
} }
border.width: (isConnecting || isConnected) ? 2 : 1
Row { Row {
anchors.left: parent.left anchors.left: parent.left
@@ -490,9 +490,9 @@ Rectangle {
width: parent.width width: parent.width
height: 50 height: 50
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: availableMouseArea.containsMouse && isInteractive ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency) color: availableMouseArea.containsMouse && isInteractive ? Theme.primaryHoverLight : Theme.surfaceLight
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) border.color: Theme.outlineLight
border.width: 0 border.width: 1
opacity: isInteractive ? 1 : 0.6 opacity: isInteractive ? 1 : 0.6
Row { Row {

View File

@@ -79,9 +79,9 @@ Rectangle {
width: parent.width width: parent.width
height: 80 height: 80
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency) color: Theme.surfaceLight
border.color: modelData.mount === currentMountPath ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) border.color: modelData.mount === currentMountPath ? Theme.primary : Theme.outlineLight
border.width: modelData.mount === currentMountPath ? 2 : 0 border.width: modelData.mount === currentMountPath ? 2 : 1
Row { Row {
anchors.left: parent.left anchors.left: parent.left

View File

@@ -308,9 +308,9 @@ Rectangle {
width: parent.width width: parent.width
height: wiredContentRow.implicitHeight + Theme.spacingM * 2 height: wiredContentRow.implicitHeight + Theme.spacingM * 2
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: wiredNetworkMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency) color: wiredNetworkMouseArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceLight
border.color: Theme.primary border.color: isActive ? Theme.primary : Theme.outlineLight
border.width: 0 border.width: isActive ? 2 : 1
Row { Row {
id: wiredContentRow id: wiredContentRow
@@ -565,9 +565,9 @@ Rectangle {
width: wifiContent.width width: wifiContent.width
height: wifiContentRow.implicitHeight + Theme.spacingM * 2 height: wifiContentRow.implicitHeight + Theme.spacingM * 2
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: networkMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency) color: networkMouseArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceLight
border.color: wifiDelegate.isConnected ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) border.color: wifiDelegate.isConnected ? Theme.primary : Theme.outlineLight
border.width: 0 border.width: wifiDelegate.isConnected ? 2 : 1
Row { Row {
id: wifiContentRow id: wifiContentRow

View File

@@ -10,6 +10,8 @@ Item {
required property var axis required property var axis
required property var barConfig required property var barConfig
visible: !SettingsData.frameEnabled
anchors.fill: parent anchors.fill: parent
anchors.left: parent.left anchors.left: parent.left
@@ -37,6 +39,8 @@ Item {
} }
property real rt: { property real rt: {
if (SettingsData.frameEnabled)
return SettingsData.frameRounding;
if (barConfig?.squareCorners ?? false) if (barConfig?.squareCorners ?? false)
return 0; return 0;
if (barWindow.hasMaximizedToplevel) if (barWindow.hasMaximizedToplevel)
@@ -255,11 +259,12 @@ Item {
h = h - wing; h = h - wing;
const r = wing; const r = wing;
const cr = rt; const cr = rt;
const crE = SettingsData.frameEnabled ? 0 : cr;
let d = `M ${cr} 0`; let d = `M ${crE} 0`;
d += ` L ${w - cr} 0`; d += ` L ${w - crE} 0`;
if (cr > 0) if (crE > 0)
d += ` A ${cr} ${cr} 0 0 1 ${w} ${cr}`; d += ` A ${crE} ${crE} 0 0 1 ${w} ${crE}`;
if (r > 0) { if (r > 0) {
d += ` L ${w} ${h + r}`; d += ` L ${w} ${h + r}`;
d += ` A ${r} ${r} 0 0 0 ${w - r} ${h}`; d += ` A ${r} ${r} 0 0 0 ${w - r} ${h}`;
@@ -273,9 +278,9 @@ Item {
if (cr > 0) if (cr > 0)
d += ` A ${cr} ${cr} 0 0 1 0 ${h - cr}`; d += ` A ${cr} ${cr} 0 0 1 0 ${h - cr}`;
} }
d += ` L 0 ${cr}`; d += ` L 0 ${crE}`;
if (cr > 0) if (crE > 0)
d += ` A ${cr} ${cr} 0 0 1 ${cr} 0`; d += ` A ${crE} ${crE} 0 0 1 ${crE} 0`;
d += " Z"; d += " Z";
return d; return d;
} }
@@ -285,11 +290,12 @@ Item {
h = h - wing; h = h - wing;
const r = wing; const r = wing;
const cr = rt; const cr = rt;
const crE = SettingsData.frameEnabled ? 0 : cr;
let d = `M ${cr} ${fullH}`; let d = `M ${crE} ${fullH}`;
d += ` L ${w - cr} ${fullH}`; d += ` L ${w - crE} ${fullH}`;
if (cr > 0) if (crE > 0)
d += ` A ${cr} ${cr} 0 0 0 ${w} ${fullH - cr}`; d += ` A ${crE} ${crE} 0 0 0 ${w} ${fullH - crE}`;
if (r > 0) { if (r > 0) {
d += ` L ${w} 0`; d += ` L ${w} 0`;
d += ` A ${r} ${r} 0 0 1 ${w - r} ${r}`; d += ` A ${r} ${r} 0 0 1 ${w - r} ${r}`;
@@ -303,9 +309,9 @@ Item {
if (cr > 0) if (cr > 0)
d += ` A ${cr} ${cr} 0 0 0 0 ${cr}`; d += ` A ${cr} ${cr} 0 0 0 0 ${cr}`;
} }
d += ` L 0 ${fullH - cr}`; d += ` L 0 ${fullH - crE}`;
if (cr > 0) if (crE > 0)
d += ` A ${cr} ${cr} 0 0 0 ${cr} ${fullH}`; d += ` A ${crE} ${crE} 0 0 0 ${crE} ${fullH}`;
d += " Z"; d += " Z";
return d; return d;
} }
@@ -314,11 +320,12 @@ Item {
w = w - wing; w = w - wing;
const r = wing; const r = wing;
const cr = rt; const cr = rt;
const crE = SettingsData.frameEnabled ? 0 : cr;
let d = `M 0 ${cr}`; let d = `M 0 ${crE}`;
d += ` L 0 ${h - cr}`; d += ` L 0 ${h - crE}`;
if (cr > 0) if (crE > 0)
d += ` A ${cr} ${cr} 0 0 0 ${cr} ${h}`; d += ` A ${crE} ${crE} 0 0 0 ${crE} ${h}`;
if (r > 0) { if (r > 0) {
d += ` L ${w + r} ${h}`; d += ` L ${w + r} ${h}`;
d += ` A ${r} ${r} 0 0 1 ${w} ${h - r}`; d += ` A ${r} ${r} 0 0 1 ${w} ${h - r}`;
@@ -332,9 +339,9 @@ Item {
if (cr > 0) if (cr > 0)
d += ` A ${cr} ${cr} 0 0 0 ${w - cr} 0`; d += ` A ${cr} ${cr} 0 0 0 ${w - cr} 0`;
} }
d += ` L ${cr} 0`; d += ` L ${crE} 0`;
if (cr > 0) if (crE > 0)
d += ` A ${cr} ${cr} 0 0 0 0 ${cr}`; d += ` A ${crE} ${crE} 0 0 0 0 ${crE}`;
d += " Z"; d += " Z";
return d; return d;
} }
@@ -344,11 +351,12 @@ Item {
w = w - wing; w = w - wing;
const r = wing; const r = wing;
const cr = rt; const cr = rt;
const crE = SettingsData.frameEnabled ? 0 : cr;
let d = `M ${fullW} ${cr}`; let d = `M ${fullW} ${crE}`;
d += ` L ${fullW} ${h - cr}`; d += ` L ${fullW} ${h - crE}`;
if (cr > 0) if (crE > 0)
d += ` A ${cr} ${cr} 0 0 1 ${fullW - cr} ${h}`; d += ` A ${crE} ${crE} 0 0 1 ${fullW - crE} ${h}`;
if (r > 0) { if (r > 0) {
d += ` L 0 ${h}`; d += ` L 0 ${h}`;
d += ` A ${r} ${r} 0 0 0 ${r} ${h - r}`; d += ` A ${r} ${r} 0 0 0 ${r} ${h - r}`;
@@ -362,9 +370,9 @@ Item {
if (cr > 0) if (cr > 0)
d += ` A ${cr} ${cr} 0 0 1 ${cr} 0`; d += ` A ${cr} ${cr} 0 0 1 ${cr} 0`;
} }
d += ` L ${fullW - cr} 0`; d += ` L ${fullW - crE} 0`;
if (cr > 0) if (crE > 0)
d += ` A ${cr} ${cr} 0 0 1 ${fullW} ${cr}`; d += ` A ${crE} ${crE} 0 0 1 ${fullW} ${crE}`;
d += " Z"; d += " Z";
return d; return d;
} }

View File

@@ -14,6 +14,7 @@ Item {
property real barThickness: 48 property real barThickness: 48
property real barSpacing: 4 property real barSpacing: 4
property var barConfig: null property var barConfig: null
property var blurBarWindow: null
property bool overrideAxisLayout: false property bool overrideAxisLayout: false
property bool forceVerticalLayout: false property bool forceVerticalLayout: false
@@ -357,6 +358,7 @@ Item {
barThickness: root.barThickness barThickness: root.barThickness
barSpacing: root.barSpacing barSpacing: root.barSpacing
barConfig: root.barConfig barConfig: root.barConfig
blurBarWindow: root.blurBarWindow
isFirst: index === 0 isFirst: index === 0
isLast: index === centerRepeater.count - 1 isLast: index === centerRepeater.count - 1
sectionSpacing: parent.itemSpacing sectionSpacing: parent.itemSpacing

View File

@@ -14,6 +14,8 @@ Item {
required property var rootWindow required property var rootWindow
required property var barConfig required property var barConfig
readonly property var blurBarWindow: barWindow
property var leftWidgetsModel property var leftWidgetsModel
property var centerWidgetsModel property var centerWidgetsModel
property var rightWidgetsModel property var rightWidgetsModel
@@ -21,6 +23,31 @@ Item {
readonly property real innerPadding: barConfig?.innerPadding ?? 4 readonly property real innerPadding: barConfig?.innerPadding ?? 4
readonly property real outlineThickness: (barConfig?.widgetOutlineEnabled ?? false) ? (barConfig?.widgetOutlineThickness ?? 1) : 0 readonly property real outlineThickness: (barConfig?.widgetOutlineEnabled ?? false) ? (barConfig?.widgetOutlineThickness ?? 1) : 0
readonly property real _frameLeftInset: {
if (!SettingsData.frameEnabled || barWindow.isVertical) return 0
return barWindow.hasAdjacentLeftBar
? SettingsData.frameBarSize
: 0
}
readonly property real _frameRightInset: {
if (!SettingsData.frameEnabled || barWindow.isVertical) return 0
return barWindow.hasAdjacentRightBar
? SettingsData.frameBarSize
: 0
}
readonly property real _frameTopInset: {
if (!SettingsData.frameEnabled || !barWindow.isVertical) return 0
return barWindow.hasAdjacentTopBar
? SettingsData.frameThickness
: 0
}
readonly property real _frameBottomInset: {
if (!SettingsData.frameEnabled || !barWindow.isVertical) return 0
return barWindow.hasAdjacentBottomBar
? SettingsData.frameThickness
: 0
}
property alias hLeftSection: hLeftSection property alias hLeftSection: hLeftSection
property alias hCenterSection: hCenterSection property alias hCenterSection: hCenterSection
property alias hRightSection: hRightSection property alias hRightSection: hRightSection
@@ -29,10 +56,14 @@ Item {
property alias vRightSection: vRightSection property alias vRightSection: vRightSection
anchors.fill: parent anchors.fill: parent
anchors.leftMargin: Math.max(Theme.spacingXS, innerPadding * 0.8) anchors.leftMargin: Math.max(Theme.spacingXS, innerPadding * 0.8) + _frameLeftInset
anchors.rightMargin: Math.max(Theme.spacingXS, innerPadding * 0.8) anchors.rightMargin: Math.max(Theme.spacingXS, innerPadding * 0.8) + _frameRightInset
anchors.topMargin: barWindow.isVertical ? (barWindow.hasAdjacentTopBar ? outlineThickness : Theme.spacingXS) : 0 anchors.topMargin: (barWindow.isVertical
anchors.bottomMargin: barWindow.isVertical ? (barWindow.hasAdjacentBottomBar ? outlineThickness : Theme.spacingXS) : 0 ? (barWindow.hasAdjacentTopBar ? outlineThickness : Theme.spacingXS)
: 0) + _frameTopInset
anchors.bottomMargin: (barWindow.isVertical
? (barWindow.hasAdjacentBottomBar ? outlineThickness : Theme.spacingXS)
: 0) + _frameBottomInset
clip: false clip: false
property int componentMapRevision: 0 property int componentMapRevision: 0
@@ -408,6 +439,12 @@ Item {
value: topBarContent.barConfig value: topBarContent.barConfig
restoreMode: Binding.RestoreNone restoreMode: Binding.RestoreNone
} }
Binding {
target: hLeftSection
property: "blurBarWindow"
value: topBarContent.blurBarWindow
restoreMode: Binding.RestoreNone
}
RightSection { RightSection {
id: hRightSection id: hRightSection
@@ -434,6 +471,12 @@ Item {
value: topBarContent.barConfig value: topBarContent.barConfig
restoreMode: Binding.RestoreNone restoreMode: Binding.RestoreNone
} }
Binding {
target: hRightSection
property: "blurBarWindow"
value: topBarContent.blurBarWindow
restoreMode: Binding.RestoreNone
}
CenterSection { CenterSection {
id: hCenterSection id: hCenterSection
@@ -460,6 +503,12 @@ Item {
value: topBarContent.barConfig value: topBarContent.barConfig
restoreMode: Binding.RestoreNone restoreMode: Binding.RestoreNone
} }
Binding {
target: hCenterSection
property: "blurBarWindow"
value: topBarContent.blurBarWindow
restoreMode: Binding.RestoreNone
}
} }
Item { Item {
@@ -493,6 +542,12 @@ Item {
value: topBarContent.barConfig value: topBarContent.barConfig
restoreMode: Binding.RestoreNone restoreMode: Binding.RestoreNone
} }
Binding {
target: vLeftSection
property: "blurBarWindow"
value: topBarContent.blurBarWindow
restoreMode: Binding.RestoreNone
}
CenterSection { CenterSection {
id: vCenterSection id: vCenterSection
@@ -520,6 +575,12 @@ Item {
value: topBarContent.barConfig value: topBarContent.barConfig
restoreMode: Binding.RestoreNone restoreMode: Binding.RestoreNone
} }
Binding {
target: vCenterSection
property: "blurBarWindow"
value: topBarContent.blurBarWindow
restoreMode: Binding.RestoreNone
}
RightSection { RightSection {
id: vRightSection id: vRightSection
@@ -548,6 +609,12 @@ Item {
value: topBarContent.barConfig value: topBarContent.barConfig
restoreMode: Binding.RestoreNone restoreMode: Binding.RestoreNone
} }
Binding {
target: vRightSection
property: "blurBarWindow"
value: topBarContent.blurBarWindow
restoreMode: Binding.RestoreNone
}
} }
} }

View File

@@ -97,6 +97,122 @@ PanelWindow {
} }
} }
property var blurRegion: null
property var _blurWidgetItems: []
function registerBlurWidget(item) {
if (_blurWidgetItems.indexOf(item) >= 0)
return;
_blurWidgetItems = _blurWidgetItems.concat([item]);
_blurRebuildTimer.restart();
}
function unregisterBlurWidget(item) {
const idx = _blurWidgetItems.indexOf(item);
if (idx < 0)
return;
const arr = _blurWidgetItems.slice();
arr.splice(idx, 1);
_blurWidgetItems = arr;
_blurRebuildTimer.restart();
}
Timer {
id: _blurRebuildTimer
interval: 1
onTriggered: barBlur.rebuild()
}
Item {
id: barBlur
visible: false
readonly property bool barHasTransparency: barWindow._backgroundAlpha > 0 && barWindow._backgroundAlpha < 1
function rebuild() {
teardown();
if (!BlurService.enabled || !BlurService.available)
return;
// In frame mode, FrameWindow owns the blur region for the entire screen edge
// (including the bar area). The bar must not set its own competing blur region
// so that frameBlurEnabled acts as the single control for all blur in frame mode.
if (SettingsData.frameEnabled)
return;
const widgets = barWindow._blurWidgetItems.filter(w => w && w.visible && w.width > 0 && w.height > 0);
const hasBar = barHasTransparency;
if (!hasBar && widgets.length === 0)
return;
const cr = Theme.cornerRadius;
let qml = 'import QtQuick; import Quickshell; Region {';
for (let i = 0; i < widgets.length; i++) {
qml += ` property Item w${i}; Region { item: w${i}; radius: ${cr} }`;
}
qml += '}';
try {
const region = Qt.createQmlObject(qml, barWindow, "BarBlurRegion");
if (hasBar) {
region.x = Qt.binding(() => topBarMouseArea.x + barUnitInset.x + topBarSlide.x);
region.y = Qt.binding(() => topBarMouseArea.y + barUnitInset.y + topBarSlide.y);
region.width = Qt.binding(() => barUnitInset.width);
region.height = Qt.binding(() => barUnitInset.height);
region.radius = Qt.binding(() => barBackground.rt);
}
for (let i = 0; i < widgets.length; i++) {
region[`w${i}`] = widgets[i];
}
barWindow.BackgroundEffect.blurRegion = region;
barWindow.blurRegion = region;
} catch (e) {
console.warn("BarBlur: Failed to create blur region:", e);
}
}
function teardown() {
if (!barWindow.blurRegion)
return;
try {
barWindow.BackgroundEffect.blurRegion = null;
} catch (e) {}
barWindow.blurRegion.destroy();
barWindow.blurRegion = null;
}
onBarHasTransparencyChanged: _blurRebuildTimer.restart()
Connections {
target: BlurService
function onEnabledChanged() {
barBlur.rebuild();
}
}
Connections {
target: SettingsData
function onFrameEnabledChanged() { barBlur.rebuild(); }
}
Connections {
target: topBarSlide
function onXChanged() {
if (barWindow.blurRegion)
barWindow.blurRegion.changed();
}
function onYChanged() {
if (barWindow.blurRegion)
barWindow.blurRegion.changed();
}
}
Component.onCompleted: rebuild()
Component.onDestruction: teardown()
}
WlrLayershell.layer: dBarLayer WlrLayershell.layer: dBarLayer
WlrLayershell.namespace: "dms:bar" WlrLayershell.namespace: "dms:bar"
@@ -132,7 +248,9 @@ PanelWindow {
readonly property color _surfaceContainer: Theme.surfaceContainer readonly property color _surfaceContainer: Theme.surfaceContainer
readonly property string _barId: barConfig?.id ?? "default" readonly property string _barId: barConfig?.id ?? "default"
property real _backgroundAlpha: barConfig?.transparency ?? 1.0 property real _backgroundAlpha: barConfig?.transparency ?? 1.0
readonly property color _bgColor: Theme.withAlpha(_surfaceContainer, _backgroundAlpha) readonly property color _bgColor: SettingsData.frameEnabled
? Qt.rgba(SettingsData.effectiveFrameColor.r, SettingsData.effectiveFrameColor.g, SettingsData.effectiveFrameColor.b, SettingsData.frameOpacity)
: Theme.withAlpha(_surfaceContainer, _backgroundAlpha)
function _updateBackgroundAlpha() { function _updateBackgroundAlpha() {
const live = SettingsData.barConfigs.find(c => c.id === _barId); const live = SettingsData.barConfigs.find(c => c.id === _barId);
@@ -278,7 +396,7 @@ PanelWindow {
shouldHideForWindows = filtered.length > 0; shouldHideForWindows = filtered.length > 0;
} }
property real effectiveSpacing: hasMaximizedToplevel ? 0 : (barConfig?.spacing ?? 4) property real effectiveSpacing: SettingsData.frameEnabled ? 0 : (hasMaximizedToplevel ? 0 : (barConfig?.spacing ?? 4))
Behavior on effectiveSpacing { Behavior on effectiveSpacing {
enabled: barWindow.visible enabled: barWindow.visible
@@ -289,7 +407,12 @@ PanelWindow {
} }
readonly property int notificationCount: NotificationService.notifications.length readonly property int notificationCount: NotificationService.notifications.length
readonly property real effectiveBarThickness: Theme.snap(Math.max(barWindow.widgetThickness + (barConfig?.innerPadding ?? 4) + 4, Theme.barHeight - 4 - (8 - (barConfig?.innerPadding ?? 4))), _dpr) readonly property real effectiveBarThickness: SettingsData.frameEnabled
? SettingsData.frameBarSize
: Theme.snap(Math.max(barWindow.widgetThickness + (barConfig?.innerPadding ?? 4) + 4, Theme.barHeight - 4 - (8 - (barConfig?.innerPadding ?? 4))), _dpr)
readonly property bool effectiveOpenOnOverview: SettingsData.frameEnabled
? SettingsData.frameShowOnOverview
: (barConfig?.openOnOverview ?? false)
readonly property real widgetThickness: Theme.snap(Math.max(20, 26 + (barConfig?.innerPadding ?? 4) * 0.6), _dpr) readonly property real widgetThickness: Theme.snap(Math.max(20, 26 + (barConfig?.innerPadding ?? 4) * 0.6), _dpr)
readonly property bool hasAdjacentTopBar: { readonly property bool hasAdjacentTopBar: {
@@ -545,7 +668,7 @@ PanelWindow {
readonly property int barThickness: Theme.px(barWindow.effectiveBarThickness + barWindow.effectiveSpacing, barWindow._dpr) readonly property int barThickness: Theme.px(barWindow.effectiveBarThickness + barWindow.effectiveSpacing, barWindow._dpr)
readonly property bool inOverviewWithShow: CompositorService.isNiri && NiriService.inOverview && (barConfig?.openOnOverview ?? false) readonly property bool inOverviewWithShow: CompositorService.isNiri && NiriService.inOverview && barWindow.effectiveOpenOnOverview
readonly property bool effectiveVisible: (barConfig?.visible ?? true) || inOverviewWithShow readonly property bool effectiveVisible: (barConfig?.visible ?? true) || inOverviewWithShow
readonly property bool showing: effectiveVisible && (topBarCore.reveal || inOverviewWithShow || !topBarCore.autoHide) readonly property bool showing: effectiveVisible && (topBarCore.reveal || inOverviewWithShow || !topBarCore.autoHide)
@@ -686,7 +809,7 @@ PanelWindow {
} }
property bool reveal: { property bool reveal: {
const inOverviewWithShow = CompositorService.isNiri && NiriService.inOverview && (barConfig?.openOnOverview ?? false); const inOverviewWithShow = CompositorService.isNiri && NiriService.inOverview && barWindow.effectiveOpenOnOverview;
if (inOverviewWithShow) if (inOverviewWithShow)
return true; return true;
@@ -711,7 +834,8 @@ PanelWindow {
onHasActivePopoutChanged: evaluateReveal() onHasActivePopoutChanged: evaluateReveal()
function updateActivePopoutState() { function updateActivePopoutState() {
if (!barWindow.screen) return; if (!barWindow.screen)
return;
const screenName = barWindow.screen.name; const screenName = barWindow.screen.name;
const activePopout = PopoutManager.currentPopoutsByScreen[screenName]; const activePopout = PopoutManager.currentPopoutsByScreen[screenName];
const activeTrayMenu = TrayMenuManager.activeTrayMenus[screenName]; const activeTrayMenu = TrayMenuManager.activeTrayMenus[screenName];
@@ -782,7 +906,7 @@ PanelWindow {
top: barWindow.isVertical ? parent.top : undefined top: barWindow.isVertical ? parent.top : undefined
bottom: barWindow.isVertical ? parent.bottom : undefined bottom: barWindow.isVertical ? parent.bottom : undefined
} }
readonly property bool inOverview: CompositorService.isNiri && NiriService.inOverview && (barConfig?.openOnOverview ?? false) readonly property bool inOverview: CompositorService.isNiri && NiriService.inOverview && barWindow.effectiveOpenOnOverview
hoverEnabled: (barConfig?.autoHide ?? false) && !inOverview && !topBarCore.hasActivePopout hoverEnabled: (barConfig?.autoHide ?? false) && !inOverview && !topBarCore.hasActivePopout
acceptedButtons: Qt.NoButton acceptedButtons: Qt.NoButton
enabled: (barConfig?.autoHide ?? false) && !inOverview enabled: (barConfig?.autoHide ?? false) && !inOverview

View File

@@ -13,6 +13,7 @@ Item {
property real barThickness: 48 property real barThickness: 48
property real barSpacing: 4 property real barSpacing: 4
property var barConfig: null property var barConfig: null
property var blurBarWindow: null
property bool overrideAxisLayout: false property bool overrideAxisLayout: false
property bool forceVerticalLayout: false property bool forceVerticalLayout: false
@@ -59,6 +60,7 @@ Item {
barThickness: root.barThickness barThickness: root.barThickness
barSpacing: root.barSpacing barSpacing: root.barSpacing
barConfig: root.barConfig barConfig: root.barConfig
blurBarWindow: root.blurBarWindow
isFirst: index === 0 isFirst: index === 0
isLast: index === rowRepeater.count - 1 isLast: index === rowRepeater.count - 1
sectionSpacing: parent.rowSpacing sectionSpacing: parent.rowSpacing
@@ -103,6 +105,7 @@ Item {
barThickness: root.barThickness barThickness: root.barThickness
barSpacing: root.barSpacing barSpacing: root.barSpacing
barConfig: root.barConfig barConfig: root.barConfig
blurBarWindow: root.blurBarWindow
isFirst: index === 0 isFirst: index === 0
isLast: index === columnRepeater.count - 1 isLast: index === columnRepeater.count - 1
sectionSpacing: parent.columnSpacing sectionSpacing: parent.columnSpacing

View File

@@ -13,6 +13,7 @@ Item {
property real barThickness: 48 property real barThickness: 48
property real barSpacing: 4 property real barSpacing: 4
property var barConfig: null property var barConfig: null
property var blurBarWindow: null
property bool overrideAxisLayout: false property bool overrideAxisLayout: false
property bool forceVerticalLayout: false property bool forceVerticalLayout: false
@@ -61,6 +62,7 @@ Item {
barThickness: root.barThickness barThickness: root.barThickness
barSpacing: root.barSpacing barSpacing: root.barSpacing
barConfig: root.barConfig barConfig: root.barConfig
blurBarWindow: root.blurBarWindow
isFirst: index === 0 isFirst: index === 0
isLast: index === rowRepeater.count - 1 isLast: index === rowRepeater.count - 1
sectionSpacing: parent.rowSpacing sectionSpacing: parent.rowSpacing
@@ -105,6 +107,7 @@ Item {
barThickness: root.barThickness barThickness: root.barThickness
barSpacing: root.barSpacing barSpacing: root.barSpacing
barConfig: root.barConfig barConfig: root.barConfig
blurBarWindow: root.blurBarWindow
isFirst: index === 0 isFirst: index === 0
isLast: index === columnRepeater.count - 1 isLast: index === columnRepeater.count - 1
sectionSpacing: parent.columnSpacing sectionSpacing: parent.columnSpacing

View File

@@ -16,6 +16,7 @@ Loader {
property real barThickness: 48 property real barThickness: 48
property real barSpacing: 4 property real barSpacing: 4
property var barConfig: null property var barConfig: null
property var blurBarWindow: null
property bool isFirst: false property bool isFirst: false
property bool isLast: false property bool isLast: false
property real sectionSpacing: 0 property real sectionSpacing: 0
@@ -92,6 +93,14 @@ Loader {
restoreMode: Binding.RestoreNone restoreMode: Binding.RestoreNone
} }
Binding {
target: root.item
when: root.item && "blurBarWindow" in root.item
property: "blurBarWindow"
value: root.blurBarWindow
restoreMode: Binding.RestoreNone
}
Binding { Binding {
target: root.item target: root.item
when: root.item && "axis" in root.item when: root.item && "axis" in root.item

View File

@@ -630,7 +630,7 @@ BasePill {
if (appItem.isFocused && colorizeEnabled) { if (appItem.isFocused && colorizeEnabled) {
return mouseArea.containsMouse ? Theme.withAlpha(Qt.lighter(appItem.activeOverlayColor, 1.3), 0.4) : Theme.withAlpha(appItem.activeOverlayColor, 0.3); return mouseArea.containsMouse ? Theme.withAlpha(Qt.lighter(appItem.activeOverlayColor, 1.3), 0.4) : Theme.withAlpha(appItem.activeOverlayColor, 0.3);
} }
return mouseArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent"; return mouseArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent";
} }
border.width: dragHandler.dragging ? 2 : 0 border.width: dragHandler.dragging ? 2 : 0

View File

@@ -3,6 +3,7 @@ import Quickshell
import Quickshell.Wayland import Quickshell.Wayland
import qs.Common import qs.Common
import qs.Modules.Plugins import qs.Modules.Plugins
import qs.Services
import qs.Widgets import qs.Widgets
BasePill { BasePill {
@@ -93,6 +94,15 @@ BasePill {
PanelWindow { PanelWindow {
id: contextMenuWindow id: contextMenuWindow
WindowBlur {
targetWindow: contextMenuWindow
blurX: menuContainer.x
blurY: menuContainer.y
blurWidth: contextMenuWindow.visible ? menuContainer.width : 0
blurHeight: contextMenuWindow.visible ? menuContainer.height : 0
blurRadius: Theme.cornerRadius
}
WlrLayershell.namespace: "dms:clipboard-context-menu" WlrLayershell.namespace: "dms:clipboard-context-menu"
property bool isVertical: false property bool isVertical: false
@@ -187,8 +197,8 @@ BasePill {
height: Math.max(64, menuColumn.implicitHeight + Theme.spacingS * 2) height: Math.max(64, menuColumn.implicitHeight + Theme.spacingS * 2)
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
radius: Theme.cornerRadius radius: Theme.cornerRadius
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) border.color: BlurService.enabled ? BlurService.borderColor : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1 border.width: BlurService.enabled ? BlurService.borderWidth : 1
opacity: contextMenuWindow.visible ? 1 : 0 opacity: contextMenuWindow.visible ? 1 : 0
visible: opacity > 0 visible: opacity > 0
@@ -224,7 +234,7 @@ BasePill {
width: parent.width width: parent.width
height: 30 height: 30
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: clearAllArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent" color: clearAllArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
Row { Row {
anchors.fill: parent anchors.fill: parent
@@ -264,7 +274,7 @@ BasePill {
width: parent.width width: parent.width
height: 30 height: 30
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: savedItemsArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent" color: savedItemsArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
Row { Row {
anchors.fill: parent anchors.fill: parent

View File

@@ -354,7 +354,7 @@ BasePill {
height: 20 height: 20
radius: 10 radius: 10
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
color: prevArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent" color: prevArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
visible: root.playerAvailable visible: root.playerAvailable
opacity: (activePlayer && activePlayer.canGoPrevious) ? 1 : 0.3 opacity: (activePlayer && activePlayer.canGoPrevious) ? 1 : 0.3
@@ -411,7 +411,7 @@ BasePill {
height: 20 height: 20
radius: 10 radius: 10
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
color: nextArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent" color: nextArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
visible: playerAvailable visible: playerAvailable
opacity: (activePlayer && activePlayer.canGoNext) ? 1 : 0.3 opacity: (activePlayer && activePlayer.canGoNext) ? 1 : 0.3

View File

@@ -285,7 +285,7 @@ BasePill {
width: parent.width width: parent.width
height: 30 height: 30
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: tabArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent" color: tabArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
Row { Row {
anchors.fill: parent anchors.fill: parent
@@ -327,7 +327,7 @@ BasePill {
width: parent.width width: parent.width
height: 30 height: 30
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: newNoteArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent" color: newNoteArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
Row { Row {
anchors.fill: parent anchors.fill: parent

View File

@@ -273,7 +273,7 @@ BasePill {
if (isFocused) { if (isFocused) {
return mouseArea.containsMouse ? Theme.primarySelected : Theme.withAlpha(Theme.primary, 0.2); return mouseArea.containsMouse ? Theme.primarySelected : Theme.withAlpha(Theme.primary, 0.2);
} }
return mouseArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent"; return mouseArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent";
} }
// App icon // App icon
@@ -528,7 +528,7 @@ BasePill {
if (isFocused) { if (isFocused) {
return mouseArea.containsMouse ? Theme.primarySelected : Theme.withAlpha(Theme.primary, 0.2); return mouseArea.containsMouse ? Theme.primarySelected : Theme.withAlpha(Theme.primary, 0.2);
} }
return mouseArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent"; return mouseArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent";
} }
IconImage { IconImage {
@@ -738,6 +738,15 @@ BasePill {
sourceComponent: PanelWindow { sourceComponent: PanelWindow {
id: contextMenuWindow id: contextMenuWindow
WindowBlur {
targetWindow: contextMenuWindow
blurX: contextMenuRect.x
blurY: contextMenuRect.y
blurWidth: contextMenuWindow.isVisible ? contextMenuRect.width : 0
blurHeight: contextMenuWindow.isVisible ? contextMenuRect.height : 0
blurRadius: Theme.cornerRadius
}
property var currentWindow: null property var currentWindow: null
property bool isVisible: false property bool isVisible: false
property point anchorPos: Qt.point(0, 0) property point anchorPos: Qt.point(0, 0)
@@ -830,6 +839,7 @@ BasePill {
} }
Rectangle { Rectangle {
id: contextMenuRect
x: { x: {
if (contextMenuWindow.isVertical) { if (contextMenuWindow.isVertical) {
if (contextMenuWindow.edge === "left") { if (contextMenuWindow.edge === "left") {
@@ -858,13 +868,13 @@ BasePill {
height: 32 height: 32
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
radius: Theme.cornerRadius radius: Theme.cornerRadius
border.width: 1 border.width: BlurService.enabled ? BlurService.borderWidth : 1
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) border.color: BlurService.enabled ? BlurService.borderColor : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
radius: parent.radius radius: parent.radius
color: closeMouseArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent" color: closeMouseArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
} }
StyledText { StyledText {

View File

@@ -287,7 +287,7 @@ BasePill {
height: root.trayItemSize height: root.trayItemSize
anchors.centerIn: parent anchors.centerIn: parent
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: trayItemArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent" color: trayItemArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
border.width: dragHandler.dragging ? 2 : 0 border.width: dragHandler.dragging ? 2 : 0
border.color: Theme.primary border.color: Theme.primary
opacity: dragHandler.dragging ? 0.8 : 1.0 opacity: dragHandler.dragging ? 0.8 : 1.0
@@ -425,7 +425,7 @@ BasePill {
height: root.trayItemSize height: root.trayItemSize
anchors.centerIn: parent anchors.centerIn: parent
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: caretArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent" color: caretArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
DankIcon { DankIcon {
anchors.centerIn: parent anchors.centerIn: parent
@@ -547,7 +547,7 @@ BasePill {
height: root.trayItemSize height: root.trayItemSize
anchors.centerIn: parent anchors.centerIn: parent
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: trayItemArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent" color: trayItemArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
border.width: dragHandler.dragging ? 2 : 0 border.width: dragHandler.dragging ? 2 : 0
border.color: Theme.primary border.color: Theme.primary
opacity: dragHandler.dragging ? 0.8 : 1.0 opacity: dragHandler.dragging ? 0.8 : 1.0
@@ -685,7 +685,7 @@ BasePill {
height: root.trayItemSize height: root.trayItemSize
anchors.centerIn: parent anchors.centerIn: parent
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: caretAreaVert.containsMouse ? Theme.widgetBaseHoverColor : "transparent" color: caretAreaVert.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
DankIcon { DankIcon {
anchors.centerIn: parent anchors.centerIn: parent
@@ -723,6 +723,16 @@ BasePill {
PanelWindow { PanelWindow {
id: overflowMenu id: overflowMenu
WindowBlur {
targetWindow: overflowMenu
blurX: menuContainer.x
blurY: menuContainer.y
blurWidth: root.menuOpen ? menuContainer.width : 0
blurHeight: root.menuOpen ? menuContainer.height : 0
blurRadius: Theme.cornerRadius
}
visible: root.menuOpen visible: root.menuOpen
screen: root.parentScreen screen: root.parentScreen
WlrLayershell.layer: WlrLayershell.Top WlrLayershell.layer: WlrLayershell.Top
@@ -990,6 +1000,15 @@ BasePill {
layer.samples: 4 layer.samples: 4
} }
Rectangle {
anchors.fill: parent
color: "transparent"
radius: Theme.cornerRadius
border.color: BlurService.borderColor
border.width: BlurService.borderWidth
z: 100
}
Grid { Grid {
id: menuGrid id: menuGrid
anchors.centerIn: parent anchors.centerIn: parent
@@ -1030,7 +1049,7 @@ BasePill {
width: root.trayItemSize + 4 width: root.trayItemSize + 4
height: root.trayItemSize + 4 height: root.trayItemSize + 4
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: itemArea.containsMouse ? Theme.widgetBaseHoverColor : Theme.withAlpha(Theme.surfaceContainer, 0) color: itemArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : Theme.withAlpha(Theme.surfaceContainer, 0)
IconImage { IconImage {
id: menuIconImg id: menuIconImg
@@ -1191,6 +1210,15 @@ BasePill {
PanelWindow { PanelWindow {
id: menuWindow id: menuWindow
WindowBlur {
targetWindow: menuWindow
blurX: trayMenuContainer.x
blurY: trayMenuContainer.y
blurWidth: menuRoot.showMenu ? trayMenuContainer.width : 0
blurHeight: menuRoot.showMenu ? trayMenuContainer.height : 0
blurRadius: Theme.cornerRadius
}
WlrLayershell.namespace: "dms:tray-menu-window" WlrLayershell.namespace: "dms:tray-menu-window"
visible: menuRoot.showMenu && (menuRoot.trayItem?.hasMenu ?? false) visible: menuRoot.showMenu && (menuRoot.trayItem?.hasMenu ?? false)
WlrLayershell.layer: WlrLayershell.Top WlrLayershell.layer: WlrLayershell.Top
@@ -1302,7 +1330,7 @@ BasePill {
onClicked: mouse => { onClicked: mouse => {
const clickX = mouse.x + menuWindow.maskX; const clickX = mouse.x + menuWindow.maskX;
const clickY = mouse.y + menuWindow.maskY; const clickY = mouse.y + menuWindow.maskY;
const outsideContent = clickX < menuContainer.x || clickX > menuContainer.x + menuContainer.width || clickY < menuContainer.y || clickY > menuContainer.y + menuContainer.height; const outsideContent = clickX < trayMenuContainer.x || clickX > trayMenuContainer.x + trayMenuContainer.width || clickY < trayMenuContainer.y || clickY > trayMenuContainer.y + trayMenuContainer.height;
if (!outsideContent) if (!outsideContent)
return; return;
@@ -1360,7 +1388,7 @@ BasePill {
} }
Item { Item {
id: menuContainer id: trayMenuContainer
readonly property real rawWidth: Math.min(500, Math.max(250, menuColumn.implicitWidth + Theme.spacingS * 2)) readonly property real rawWidth: Math.min(500, Math.max(250, menuColumn.implicitWidth + Theme.spacingS * 2))
readonly property real rawHeight: Math.max(40, menuColumn.implicitHeight + Theme.spacingS * 2) readonly property real rawHeight: Math.max(40, menuColumn.implicitHeight + Theme.spacingS * 2)
@@ -1438,6 +1466,15 @@ BasePill {
layer.textureMirroring: ShaderEffectSource.MirrorVertically layer.textureMirroring: ShaderEffectSource.MirrorVertically
} }
Rectangle {
anchors.fill: parent
color: "transparent"
radius: Theme.cornerRadius
border.color: BlurService.borderColor
border.width: BlurService.borderWidth
z: 100
}
QsMenuAnchor { QsMenuAnchor {
id: submenuHydrator id: submenuHydrator
anchor.window: menuWindow anchor.window: menuWindow
@@ -1470,7 +1507,7 @@ BasePill {
width: parent.width width: parent.width
height: 28 height: 28
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: visibilityToggleArea.containsMouse ? Theme.widgetBaseHoverColor : Theme.withAlpha(Theme.surfaceContainer, 0) color: visibilityToggleArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : Theme.withAlpha(Theme.surfaceContainer, 0)
StyledText { StyledText {
anchors.left: parent.left anchors.left: parent.left
@@ -1523,7 +1560,7 @@ BasePill {
width: parent.width width: parent.width
height: 28 height: 28
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: backArea.containsMouse ? Theme.widgetBaseHoverColor : Theme.withAlpha(Theme.surfaceContainer, 0) color: backArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : Theme.withAlpha(Theme.surfaceContainer, 0)
Row { Row {
anchors.left: parent.left anchors.left: parent.left
@@ -1574,7 +1611,7 @@ BasePill {
color: { color: {
if (menuEntry?.isSeparator) if (menuEntry?.isSeparator)
return Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2); return Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2);
return itemArea.containsMouse ? Theme.widgetBaseHoverColor : Theme.withAlpha(Theme.surfaceContainer, 0); return itemArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : Theme.withAlpha(Theme.surfaceContainer, 0);
} }
MouseArea { MouseArea {

View File

@@ -17,8 +17,49 @@ Item {
property real widgetHeight: 30 property real widgetHeight: 30
property real barThickness: 48 property real barThickness: 48
property var barConfig: null property var barConfig: null
property var blurBarWindow: null
property var hyprlandOverviewLoader: null property var hyprlandOverviewLoader: null
property var parentScreen: null property var parentScreen: null
readonly property real _leftMargin: {
if (isVertical)
return 0;
root.x;
if (!root.parent)
return 0;
const gap = root.mapToItem(null, 0, 0).x;
return (gap > 0 && gap < 30) ? gap + 5 : 0;
}
readonly property real _rightMargin: {
if (isVertical)
return 0;
root.x;
root.width;
if (!root.parent || !blurBarWindow)
return 0;
const gap = blurBarWindow.width - root.mapToItem(null, root.width, 0).x;
return (gap > 0 && gap < 30) ? gap + 5 : 0;
}
readonly property real _topMargin: {
if (!isVertical)
return 0;
root.y;
if (!root.parent)
return 0;
const gap = root.mapToItem(null, 0, 0).y;
return (gap > 0 && gap < 30) ? gap + 5 : 0;
}
readonly property real _bottomMargin: {
if (!isVertical)
return 0;
root.y;
root.height;
if (!root.parent || !blurBarWindow)
return 0;
const gap = blurBarWindow.height - root.mapToItem(null, 0, root.height).y;
return (gap > 0 && gap < 30) ? gap + 5 : 0;
}
property int _desktopEntriesUpdateTrigger: 0 property int _desktopEntriesUpdateTrigger: 0
readonly property var sortedToplevels: { readonly property var sortedToplevels: {
return CompositorService.filterCurrentWorkspace(CompositorService.sortedToplevels, screenName); return CompositorService.filterCurrentWorkspace(CompositorService.sortedToplevels, screenName);
@@ -538,6 +579,60 @@ Item {
}); });
} }
function switchToWorkspaceByModelData(data) {
if (!data)
return;
if (root.useExtWorkspace && (data.id || data.name)) {
ExtWorkspaceService.activateWorkspace(data.id || data.name, data.groupID || "");
return;
}
switch (CompositorService.compositor) {
case "niri":
if (data.idx !== undefined)
NiriService.switchToWorkspace(data.idx);
break;
case "hyprland":
if (data.id)
Hyprland.dispatch(`workspace ${data.id}`);
break;
case "dwl":
if (data.tag !== undefined)
DwlService.switchToTag(root.screenName, data.tag);
break;
case "sway":
case "scroll":
case "miracle":
if (data.num)
try {
I3.dispatch(`workspace number ${data.num}`);
} catch (_) {}
break;
}
}
function findClosestWorkspaceIndex(localX, localY) {
if (workspaceRepeater.count === 0)
return -1;
let closestIdx = -1;
let closestDist = Infinity;
for (let i = 0; i < workspaceRepeater.count; i++) {
const item = workspaceRepeater.itemAt(i);
if (!item)
continue;
const center = item.mapToItem(root, item.width / 2, item.height / 2);
const dist = isVertical ? Math.abs(localY - center.y) : Math.abs(localX - center.x);
if (dist < closestDist) {
closestDist = dist;
closestIdx = i;
}
}
return closestIdx;
}
function switchWorkspace(direction) { function switchWorkspace(direction) {
if (useExtWorkspace) { if (useExtWorkspace) {
const realWorkspaces = getRealWorkspaces(); const realWorkspaces = getRealWorkspaces();
@@ -751,8 +846,15 @@ Item {
} }
MouseArea { MouseArea {
anchors.fill: parent id: edgeMouseArea
acceptedButtons: Qt.RightButton z: -1
x: -root._leftMargin
y: -root._topMargin
width: root.width + root._leftMargin + root._rightMargin
height: root.height + root._topMargin + root._bottomMargin
acceptedButtons: Qt.LeftButton | Qt.RightButton
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
property real touchpadAccumulator: 0 property real touchpadAccumulator: 0
property real mouseAccumulator: 0 property real mouseAccumulator: 0
@@ -765,12 +867,20 @@ Item {
} }
onClicked: mouse => { onClicked: mouse => {
if (mouse.button === Qt.RightButton) { const rootPos = edgeMouseArea.mapToItem(root, mouse.x, mouse.y);
switch (mouse.button) {
case Qt.RightButton:
if (CompositorService.isNiri) { if (CompositorService.isNiri) {
NiriService.toggleOverview(); NiriService.toggleOverview();
} else if (CompositorService.isHyprland && root.hyprlandOverviewLoader?.item) { } else if (CompositorService.isHyprland && root.hyprlandOverviewLoader?.item) {
root.hyprlandOverviewLoader.item.overviewOpen = !root.hyprlandOverviewLoader.item.overviewOpen; root.hyprlandOverviewLoader.item.overviewOpen = !root.hyprlandOverviewLoader.item.overviewOpen;
} }
break;
case Qt.LeftButton:
const idx = root.findClosestWorkspaceIndex(rootPos.x, rootPos.y);
if (idx >= 0)
root.switchToWorkspaceByModelData(root.workspaceList[idx]);
break;
} }
} }
@@ -1845,5 +1955,27 @@ Item {
if (useExtWorkspace && !DMSService.activeSubscriptions.includes("extworkspace")) { if (useExtWorkspace && !DMSService.activeSubscriptions.includes("extworkspace")) {
DMSService.addSubscription("extworkspace"); DMSService.addSubscription("extworkspace");
} }
_updateBlurRegistration();
}
property bool _blurRegistered: false
readonly property bool _shouldBlur: BlurService.enabled && blurBarWindow && blurBarWindow.registerBlurWidget && !(barConfig?.noBackground ?? false) && root.visible && root.width > 0
on_ShouldBlurChanged: _updateBlurRegistration()
function _updateBlurRegistration() {
if (_shouldBlur && !_blurRegistered) {
blurBarWindow.registerBlurWidget(visualBackground);
_blurRegistered = true;
} else if (!_shouldBlur && _blurRegistered) {
if (blurBarWindow && blurBarWindow.unregisterBlurWidget)
blurBarWindow.unregisterBlurWidget(visualBackground);
_blurRegistered = false;
}
}
Component.onDestruction: {
if (_blurRegistered && blurBarWindow && blurBarWindow.unregisterBlurWidget)
blurBarWindow.unregisterBlurWidget(visualBackground);
} }
} }

View File

@@ -17,6 +17,15 @@ Variants {
delegate: PanelWindow { delegate: PanelWindow {
id: dock id: dock
WindowBlur {
targetWindow: dock
blurX: dockBackground.x + dockContainer.x + dockMouseArea.x + dockCore.x + dockSlide.x
blurY: dockBackground.y + dockContainer.y + dockMouseArea.y + dockCore.y + dockSlide.y
blurWidth: dock.hasApps && dock.reveal ? dockBackground.width : 0
blurHeight: dock.hasApps && dock.reveal ? dockBackground.height : 0
blurRadius: Theme.cornerRadius
}
WlrLayershell.namespace: "dms:dock" WlrLayershell.namespace: "dms:dock"
readonly property bool isVertical: SettingsData.dockPosition === SettingsData.Position.Left || SettingsData.dockPosition === SettingsData.Position.Right readonly property bool isVertical: SettingsData.dockPosition === SettingsData.Position.Left || SettingsData.dockPosition === SettingsData.Position.Right
@@ -562,6 +571,15 @@ Variants {
color: Theme.withAlpha(Theme.surfaceContainer, backgroundTransparency) color: Theme.withAlpha(Theme.surfaceContainer, backgroundTransparency)
radius: Theme.cornerRadius radius: Theme.cornerRadius
} }
Rectangle {
anchors.fill: parent
color: "transparent"
radius: Theme.cornerRadius
border.color: BlurService.borderColor
border.width: BlurService.borderWidth
z: 100
}
} }
Shape { Shape {

View File

@@ -9,6 +9,15 @@ import qs.Widgets
PanelWindow { PanelWindow {
id: root id: root
WindowBlur {
targetWindow: root
blurX: menuContainer.x
blurY: menuContainer.y
blurWidth: root.visible ? menuContainer.width : 0
blurHeight: root.visible ? menuContainer.height : 0
blurRadius: Theme.cornerRadius
}
WlrLayershell.namespace: "dms:dock-context-menu" WlrLayershell.namespace: "dms:dock-context-menu"
property var appData: null property var appData: null
@@ -168,8 +177,8 @@ PanelWindow {
height: menuColumn.implicitHeight + Theme.spacingS * 2 height: menuColumn.implicitHeight + Theme.spacingS * 2
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
radius: Theme.cornerRadius radius: Theme.cornerRadius
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) border.color: BlurService.enabled ? BlurService.borderColor : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1 border.width: BlurService.enabled ? BlurService.borderWidth : 1
opacity: root.visible ? 1 : 0 opacity: root.visible ? 1 : 0
visible: opacity > 0 visible: opacity > 0

View File

@@ -0,0 +1,17 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import qs.Common
Variants {
id: root
model: Quickshell.screens
FrameInstance {
required property var modelData
screen: modelData
}
}

View File

@@ -0,0 +1,59 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Effects
import qs.Common
Item {
id: root
anchors.fill: parent
required property real cutoutTopInset
required property real cutoutBottomInset
required property real cutoutLeftInset
required property real cutoutRightInset
required property real cutoutRadius
Rectangle {
id: borderRect
anchors.fill: parent
// Bake frameOpacity into the color alpha rather than using the `opacity` property.
// Qt Quick can skip layer.effect processing on items with opacity < 1 as an
// optimization, causing the MultiEffect inverted mask to stop working and the
// Rectangle to render as a plain square at low opacity values.
color: Qt.rgba(SettingsData.effectiveFrameColor.r,
SettingsData.effectiveFrameColor.g,
SettingsData.effectiveFrameColor.b,
SettingsData.frameOpacity)
layer.enabled: true
layer.effect: MultiEffect {
maskSource: cutoutMask
maskEnabled: true
maskInverted: true
maskThresholdMin: 0.5
maskSpreadAtMin: 1
}
}
Item {
id: cutoutMask
anchors.fill: parent
layer.enabled: true
visible: false
Rectangle {
anchors {
fill: parent
topMargin: root.cutoutTopInset
bottomMargin: root.cutoutBottomInset
leftMargin: root.cutoutLeftInset
rightMargin: root.cutoutRightInset
}
radius: root.cutoutRadius
}
}
}

View File

@@ -0,0 +1,87 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Wayland
import qs.Common
Scope {
id: root
required property var screen
readonly property var barEdges: {
SettingsData.barConfigs; // force re-eval when bar configs change
return SettingsData.getActiveBarEdgesForScreen(screen);
}
// One thin invisible PanelWindow per edge.
// Skips any edge where a bar already provides its own exclusiveZone.
readonly property bool screenEnabled: SettingsData.frameEnabled && SettingsData.isScreenInPreferences(root.screen, SettingsData.frameScreenPreferences)
Loader {
active: root.screenEnabled && !root.barEdges.includes("top")
sourceComponent: EdgeExclusion {
targetScreen: root.screen
anchorTop: true
anchorLeft: true
anchorRight: true
}
}
Loader {
active: root.screenEnabled && !root.barEdges.includes("bottom")
sourceComponent: EdgeExclusion {
targetScreen: root.screen
anchorBottom: true
anchorLeft: true
anchorRight: true
}
}
Loader {
active: root.screenEnabled && !root.barEdges.includes("left")
sourceComponent: EdgeExclusion {
targetScreen: root.screen
anchorLeft: true
anchorTop: true
anchorBottom: true
}
}
Loader {
active: root.screenEnabled && !root.barEdges.includes("right")
sourceComponent: EdgeExclusion {
targetScreen: root.screen
anchorRight: true
anchorTop: true
anchorBottom: true
}
}
component EdgeExclusion: PanelWindow {
required property var targetScreen
screen: targetScreen
property bool anchorTop: false
property bool anchorBottom: false
property bool anchorLeft: false
property bool anchorRight: false
WlrLayershell.namespace: "dms:frame-exclusion"
WlrLayershell.layer: WlrLayer.Top
exclusiveZone: SettingsData.frameThickness
color: "transparent"
mask: Region {}
implicitWidth: 1
implicitHeight: 1
anchors {
top: anchorTop
bottom: anchorBottom
left: anchorLeft
right: anchorRight
}
}
}

View File

@@ -0,0 +1,18 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
Item {
id: root
required property var screen
FrameWindow {
targetScreen: root.screen
}
FrameExclusions {
screen: root.screen
}
}

View File

@@ -0,0 +1,169 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Wayland
import qs.Common
import qs.Services
PanelWindow {
id: win
required property var targetScreen
screen: targetScreen
visible: true
WlrLayershell.namespace: "dms:frame"
WlrLayershell.layer: WlrLayer.Top
WlrLayershell.exclusionMode: ExclusionMode.Ignore
anchors {
top: true
bottom: true
left: true
right: true
}
color: "transparent"
// No input — pass everything through to apps and bar
mask: Region {}
readonly property var barEdges: {
SettingsData.barConfigs;
return SettingsData.getActiveBarEdgesForScreen(win.screen);
}
readonly property real _dpr: CompositorService.getScreenScale(win.screen)
readonly property bool _frameActive: SettingsData.frameEnabled
&& SettingsData.isScreenInPreferences(win.screen, SettingsData.frameScreenPreferences)
readonly property int _windowRegionWidth: win._regionInt(win.width)
readonly property int _windowRegionHeight: win._regionInt(win.height)
function _regionInt(value) {
return Math.max(0, Math.round(Theme.px(value, win._dpr)));
}
readonly property int cutoutTopInset: win._regionInt(barEdges.includes("top") ? SettingsData.frameBarSize : SettingsData.frameThickness)
readonly property int cutoutBottomInset: win._regionInt(barEdges.includes("bottom") ? SettingsData.frameBarSize : SettingsData.frameThickness)
readonly property int cutoutLeftInset: win._regionInt(barEdges.includes("left") ? SettingsData.frameBarSize : SettingsData.frameThickness)
readonly property int cutoutRightInset: win._regionInt(barEdges.includes("right") ? SettingsData.frameBarSize : SettingsData.frameThickness)
readonly property int cutoutWidth: Math.max(0, win._windowRegionWidth - win.cutoutLeftInset - win.cutoutRightInset)
readonly property int cutoutHeight: Math.max(0, win._windowRegionHeight - win.cutoutTopInset - win.cutoutBottomInset)
readonly property int cutoutRadius: {
const requested = win._regionInt(SettingsData.frameRounding);
const maxRadius = Math.floor(Math.min(win.cutoutWidth, win.cutoutHeight) / 2);
return Math.max(0, Math.min(requested, maxRadius));
}
// Slightly expand the subtractive blur cutout at very low opacity levels
readonly property int _blurCutoutCompensation: SettingsData.frameOpacity <= 0.2 ? 1 : 0
readonly property int _blurCutoutLeft: Math.max(0, win.cutoutLeftInset - win._blurCutoutCompensation)
readonly property int _blurCutoutTop: Math.max(0, win.cutoutTopInset - win._blurCutoutCompensation)
readonly property int _blurCutoutRight: Math.min(win._windowRegionWidth, win._windowRegionWidth - win.cutoutRightInset + win._blurCutoutCompensation)
readonly property int _blurCutoutBottom: Math.min(win._windowRegionHeight, win._windowRegionHeight - win.cutoutBottomInset + win._blurCutoutCompensation)
readonly property int _blurCutoutRadius: {
const requested = win.cutoutRadius + win._blurCutoutCompensation;
const maxRadius = Math.floor(Math.min(_blurCutout.width, _blurCutout.height) / 2);
return Math.max(0, Math.min(requested, maxRadius));
}
// Must stay visible so Region.item can resolve scene coordinates.
Item {
id: _blurCutout
x: win._blurCutoutLeft
y: win._blurCutoutTop
width: Math.max(0, win._blurCutoutRight - win._blurCutoutLeft)
height: Math.max(0, win._blurCutoutBottom - win._blurCutoutTop)
}
property var _frameBlurRegion: null
function _buildBlur() {
_teardownBlur();
// Follow the global blur toggle
if (!BlurService.enabled || !SettingsData.frameBlurEnabled || !win._frameActive || !win.visible)
return;
try {
const region = Qt.createQmlObject(
'import QtQuick; import Quickshell; Region {' +
' property Item cutoutItem;' +
' property int cutoutRadius: 0;' +
' Region {' +
' item: cutoutItem;' +
' intersection: Intersection.Subtract;' +
' radius: cutoutRadius;' +
' }' +
'}',
win, "FrameBlurRegion");
region.x = Qt.binding(() => 0);
region.y = Qt.binding(() => 0);
region.width = Qt.binding(() => win._windowRegionWidth);
region.height = Qt.binding(() => win._windowRegionHeight);
region.cutoutItem = _blurCutout;
region.cutoutRadius = Qt.binding(() => win._blurCutoutRadius);
win.BackgroundEffect.blurRegion = region;
win._frameBlurRegion = region;
} catch (e) {
console.warn("FrameWindow: Failed to create blur region:", e);
}
}
function _teardownBlur() {
if (!win._frameBlurRegion)
return;
try {
win.BackgroundEffect.blurRegion = null;
} catch (e) {}
win._frameBlurRegion.destroy();
win._frameBlurRegion = null;
}
Timer {
id: _blurRebuildTimer
interval: 1
onTriggered: win._buildBlur()
}
Connections {
target: SettingsData
function onFrameBlurEnabledChanged() { _blurRebuildTimer.restart(); }
function onFrameEnabledChanged() { _blurRebuildTimer.restart(); }
function onFrameThicknessChanged() { _blurRebuildTimer.restart(); }
function onFrameBarSizeChanged() { _blurRebuildTimer.restart(); }
function onFrameOpacityChanged() { _blurRebuildTimer.restart(); }
function onFrameRoundingChanged() { _blurRebuildTimer.restart(); }
function onFrameScreenPreferencesChanged() { _blurRebuildTimer.restart(); }
function onBarConfigsChanged() { _blurRebuildTimer.restart(); }
}
Connections {
target: BlurService
function onEnabledChanged() { _blurRebuildTimer.restart(); }
}
onVisibleChanged: {
if (visible) {
win._frameBlurRegion = null;
_blurRebuildTimer.restart();
} else {
_teardownBlur();
}
}
Component.onCompleted: Qt.callLater(() => win._buildBlur())
Component.onDestruction: win._teardownBlur()
FrameBorder {
anchors.fill: parent
visible: win._frameActive
cutoutTopInset: win.cutoutTopInset
cutoutBottomInset: win.cutoutBottomInset
cutoutLeftInset: win.cutoutLeftInset
cutoutRightInset: win.cutoutRightInset
cutoutRadius: win.cutoutRadius
}
}

View File

@@ -1,6 +1,5 @@
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Effects
import Quickshell import Quickshell
import Quickshell.Wayland import Quickshell.Wayland
import Quickshell.Services.Notifications import Quickshell.Services.Notifications
@@ -11,6 +10,15 @@ import qs.Widgets
PanelWindow { PanelWindow {
id: win id: win
WindowBlur {
targetWindow: win
blurX: content.x + content.cardInset + swipeTx.x + tx.x
blurY: content.y + content.cardInset + swipeTx.y + tx.y
blurWidth: !win._finalized ? Math.max(0, content.width - content.cardInset * 2) : 0
blurHeight: !win._finalized ? Math.max(0, content.height - content.cardInset * 2) : 0
blurRadius: Theme.cornerRadius
}
WlrLayershell.namespace: "dms:notification-popup" WlrLayershell.namespace: "dms:notification-popup"
required property var notificationData required property var notificationData
@@ -436,6 +444,16 @@ PanelWindow {
} }
} }
Rectangle {
anchors.fill: parent
anchors.margins: content.cardInset
radius: Theme.cornerRadius
color: "transparent"
border.color: BlurService.borderColor
border.width: BlurService.borderWidth
z: 100
}
Item { Item {
id: backgroundContainer id: backgroundContainer
anchors.fill: parent anchors.fill: parent

View File

@@ -14,6 +14,7 @@ Item {
property real barThickness: 48 property real barThickness: 48
property real barSpacing: 4 property real barSpacing: 4
property var barConfig: null property var barConfig: null
property var blurBarWindow: null
property alias content: contentLoader.sourceComponent property alias content: contentLoader.sourceComponent
property bool isVerticalOrientation: axis?.isVertical ?? false property bool isVerticalOrientation: axis?.isVertical ?? false
property bool isFirst: false property bool isFirst: false
@@ -106,7 +107,7 @@ Item {
const rawTransparency = (root.barConfig && root.barConfig.widgetTransparency !== undefined) ? root.barConfig.widgetTransparency : 1.0; const rawTransparency = (root.barConfig && root.barConfig.widgetTransparency !== undefined) ? root.barConfig.widgetTransparency : 1.0;
const isHovered = root.enableBackgroundHover && (mouseArea.containsMouse || (root.isHovered || false)); const isHovered = root.enableBackgroundHover && (mouseArea.containsMouse || (root.isHovered || false));
const transparency = isHovered ? Math.max(0.3, rawTransparency) : rawTransparency; const transparency = isHovered ? Math.max(0.3, rawTransparency) : rawTransparency;
const baseColor = isHovered ? Theme.widgetBaseHoverColor : Theme.widgetBaseBackgroundColor; const baseColor = isHovered ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : Theme.widgetBaseBackgroundColor;
if (Theme.widgetBackgroundHasAlpha) { if (Theme.widgetBackgroundHasAlpha) {
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * transparency); return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * transparency);
@@ -169,4 +170,26 @@ Item {
root.wheel(wheelEvent); root.wheel(wheelEvent);
} }
} }
property bool _blurRegistered: false
readonly property bool _shouldBlur: BlurService.enabled && blurBarWindow && blurBarWindow.registerBlurWidget && !(barConfig?.noBackground ?? false) && root.visible && root.width > 0
on_ShouldBlurChanged: _updateBlurRegistration()
function _updateBlurRegistration() {
if (_shouldBlur && !_blurRegistered) {
blurBarWindow.registerBlurWidget(visualContent);
_blurRegistered = true;
} else if (!_shouldBlur && _blurRegistered) {
if (blurBarWindow && blurBarWindow.unregisterBlurWidget)
blurBarWindow.unregisterBlurWidget(visualContent);
_blurRegistered = false;
}
}
Component.onCompleted: _updateBlurRegistration()
Component.onDestruction: {
if (_blurRegistered && blurBarWindow && blurBarWindow.unregisterBlurWidget)
blurBarWindow.unregisterBlurWidget(visualContent);
}
} }

View File

@@ -14,6 +14,7 @@ Item {
property real barThickness: 48 property real barThickness: 48
property real barSpacing: 4 property real barSpacing: 4
property var barConfig: null property var barConfig: null
property var blurBarWindow: null
property string pluginId: "" property string pluginId: ""
property var pluginService: null property var pluginService: null
@@ -182,6 +183,7 @@ Item {
barThickness: root.barThickness barThickness: root.barThickness
barSpacing: root.barSpacing barSpacing: root.barSpacing
barConfig: root.barConfig barConfig: root.barConfig
blurBarWindow: root.blurBarWindow
content: root.horizontalBarPill content: root.horizontalBarPill
states: State { states: State {
@@ -241,6 +243,7 @@ Item {
barThickness: root.barThickness barThickness: root.barThickness
barSpacing: root.barSpacing barSpacing: root.barSpacing
barConfig: root.barConfig barConfig: root.barConfig
blurBarWindow: root.blurBarWindow
content: root.verticalBarPill content: root.verticalBarPill
isVerticalOrientation: true isVerticalOrientation: true

View File

@@ -2,6 +2,7 @@ import QtQuick
import QtQuick.Controls import QtQuick.Controls
import Quickshell import Quickshell
import qs.Common import qs.Common
import qs.Services
import qs.Widgets import qs.Widgets
Popup { Popup {
@@ -186,8 +187,8 @@ Popup {
contentItem: Rectangle { contentItem: Rectangle {
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
radius: Theme.cornerRadius radius: Theme.cornerRadius
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) border.color: BlurService.enabled ? BlurService.borderColor : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1 border.width: BlurService.enabled ? BlurService.borderWidth : 1
Item { Item {
id: keyboardHandler id: keyboardHandler

View File

@@ -693,6 +693,8 @@ Item {
SettingsToggleRow { SettingsToggleRow {
visible: CompositorService.isNiri visible: CompositorService.isNiri
enabled: !SettingsData.frameEnabled
opacity: SettingsData.frameEnabled ? 0.5 : 1.0
text: I18n.tr("Show on Overview") text: I18n.tr("Show on Overview")
checked: selectedBarConfig?.openOnOverview ?? false checked: selectedBarConfig?.openOnOverview ?? false
onToggled: toggled => { onToggled: toggled => {
@@ -798,11 +800,42 @@ Item {
} }
} }
Item {
visible: SettingsData.frameEnabled
width: parent.width
implicitHeight: frameNote.implicitHeight + Theme.spacingS * 2
Row {
id: frameNote
x: Theme.spacingM
width: parent.width - Theme.spacingM * 2
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
name: "frame_source"
size: Theme.fontSizeMedium
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Spacing and size are managed by Frame mode")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width - Theme.fontSizeMedium - Theme.spacingS
}
}
}
SettingsCard { SettingsCard {
iconName: "space_bar" iconName: "space_bar"
title: I18n.tr("Spacing") title: I18n.tr("Spacing")
settingKey: "barSpacing" settingKey: "barSpacing"
visible: selectedBarConfig?.enabled visible: selectedBarConfig?.enabled
enabled: !SettingsData.frameEnabled
opacity: SettingsData.frameEnabled ? 0.5 : 1.0
SettingsSliderRow { SettingsSliderRow {
id: edgeSpacingSlider id: edgeSpacingSlider
@@ -1003,6 +1036,8 @@ Item {
SettingsSliderRow { SettingsSliderRow {
id: barTransparencySlider id: barTransparencySlider
enabled: !SettingsData.frameEnabled
opacity: SettingsData.frameEnabled ? 0.5 : 1.0
text: I18n.tr("Bar Transparency") text: I18n.tr("Bar Transparency")
value: (selectedBarConfig?.transparency ?? 1.0) * 100 value: (selectedBarConfig?.transparency ?? 1.0) * 100
minimum: 0 minimum: 0
@@ -1044,6 +1079,35 @@ Item {
restoreMode: Binding.RestoreBinding restoreMode: Binding.RestoreBinding
} }
} }
Item {
visible: SettingsData.frameEnabled
width: parent.width
implicitHeight: transparencyFrameNote.implicitHeight + Theme.spacingS * 2
Row {
id: transparencyFrameNote
x: Theme.spacingM
width: parent.width - Theme.spacingM * 2
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
name: "frame_source"
size: Theme.fontSizeMedium
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Opacity is controlled by Frame Border Opacity in Frame settings")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width - Theme.fontSizeMedium - Theme.spacingS
}
}
}
} }
SettingsCard { SettingsCard {
@@ -1287,6 +1351,8 @@ Item {
SettingsToggleRow { SettingsToggleRow {
text: I18n.tr("Square Corners") text: I18n.tr("Square Corners")
enabled: !SettingsData.frameEnabled
opacity: SettingsData.frameEnabled ? 0.5 : 1.0
checked: selectedBarConfig?.squareCorners ?? false checked: selectedBarConfig?.squareCorners ?? false
onToggled: checked => SettingsData.updateBarConfig(selectedBarId, { onToggled: checked => SettingsData.updateBarConfig(selectedBarId, {
squareCorners: checked squareCorners: checked
@@ -1334,6 +1400,8 @@ Item {
SettingsToggleRow { SettingsToggleRow {
text: I18n.tr("Goth Corners") text: I18n.tr("Goth Corners")
enabled: !SettingsData.frameEnabled
opacity: SettingsData.frameEnabled ? 0.5 : 1.0
checked: selectedBarConfig?.gothCornersEnabled ?? false checked: selectedBarConfig?.gothCornersEnabled ?? false
onToggled: checked => SettingsData.updateBarConfig(selectedBarId, { onToggled: checked => SettingsData.updateBarConfig(selectedBarId, {
gothCornersEnabled: checked gothCornersEnabled: checked

View File

@@ -0,0 +1,295 @@
pragma ComponentBehavior: Bound
import QtQuick
import qs.Common
import qs.Services
import qs.Widgets
import qs.Modules.Settings.Widgets
Item {
id: root
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
DankFlickable {
anchors.fill: parent
clip: true
contentHeight: mainColumn.height + Theme.spacingXL
contentWidth: width
Column {
id: mainColumn
topPadding: 4
width: Math.min(550, parent.width - Theme.spacingL * 2)
anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingXL
// ── Enable Frame ──────────────────────────────────────────────────
SettingsCard {
width: parent.width
iconName: "frame_source"
title: I18n.tr("Frame")
settingKey: "frameEnabled"
SettingsToggleRow {
settingKey: "frameEnable"
tags: ["frame", "border", "outline", "display"]
text: I18n.tr("Enable Frame")
description: I18n.tr("Draw a connected picture-frame border around the entire display")
checked: SettingsData.frameEnabled
onToggled: checked => SettingsData.set("frameEnabled", checked)
}
}
// ── Border ────────────────────────────────────────────────────────
SettingsCard {
width: parent.width
iconName: "border_outer"
title: I18n.tr("Border")
settingKey: "frameBorder"
collapsible: true
visible: SettingsData.frameEnabled
SettingsSliderRow {
id: roundingSlider
settingKey: "frameRounding"
tags: ["frame", "border", "rounding", "radius", "corner"]
text: I18n.tr("Border Radius")
unit: "px"
minimum: 0
maximum: 100
step: 1
defaultValue: 23
value: SettingsData.frameRounding
onSliderDragFinished: v => SettingsData.set("frameRounding", v)
Binding {
target: roundingSlider
property: "value"
value: SettingsData.frameRounding
}
}
SettingsSliderRow {
id: thicknessSlider
settingKey: "frameThickness"
tags: ["frame", "border", "thickness", "size", "width"]
text: I18n.tr("Border Width")
unit: "px"
minimum: 2
maximum: 100
step: 1
defaultValue: 16
value: SettingsData.frameThickness
onSliderDragFinished: v => SettingsData.set("frameThickness", v)
Binding {
target: thicknessSlider
property: "value"
value: SettingsData.frameThickness
}
}
SettingsSliderRow {
id: barThicknessSlider
settingKey: "frameBarSize"
tags: ["frame", "bar", "thickness", "size", "height", "width"]
text: I18n.tr("Size")
description: I18n.tr("Height of horizontal bars / width of vertical bars in frame mode")
unit: "px"
minimum: 24
maximum: 100
step: 1
defaultValue: 40
value: SettingsData.frameBarSize
onSliderDragFinished: v => SettingsData.set("frameBarSize", v)
Binding {
target: barThicknessSlider
property: "value"
value: SettingsData.frameBarSize
}
}
SettingsSliderRow {
id: opacitySlider
settingKey: "frameOpacity"
tags: ["frame", "border", "opacity", "transparency"]
text: I18n.tr("Frame Opacity")
unit: "%"
minimum: 0
maximum: 100
defaultValue: 100
value: SettingsData.frameOpacity * 100
onSliderDragFinished: v => SettingsData.set("frameOpacity", v / 100)
Binding {
target: opacitySlider
property: "value"
value: SettingsData.frameOpacity * 100
}
}
SettingsToggleRow {
id: frameBlurToggle
settingKey: "frameBlurEnabled"
tags: ["frame", "blur", "background", "glass", "transparency", "frosted"]
text: I18n.tr("Frame Blur")
description: !BlurService.available
? I18n.tr("Requires a newer version of Quickshell")
: I18n.tr("Apply compositor blur behind the frame border")
checked: SettingsData.frameBlurEnabled
onToggled: checked => SettingsData.set("frameBlurEnabled", checked)
enabled: BlurService.available && SettingsData.blurEnabled
opacity: enabled ? 1.0 : 0.5
visible: BlurService.available
}
Item {
visible: BlurService.available && !SettingsData.blurEnabled
width: parent.width
height: blurToggleNote.height + Theme.spacingM * 2
Row {
id: blurToggleNote
x: Theme.spacingM
width: parent.width - Theme.spacingM * 2
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
name: "blur_on"
size: Theme.fontSizeMedium
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Frame Blur is controlled by Background Blur in Theme & Colors")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width - Theme.fontSizeMedium - Theme.spacingS
}
}
}
// Color mode buttons
SettingsButtonGroupRow {
settingKey: "frameColor"
tags: ["frame", "border", "color", "theme", "primary", "surface", "default"]
text: I18n.tr("Border color")
model: [I18n.tr("Default"), I18n.tr("Primary"), I18n.tr("Surface"), I18n.tr("Custom")]
currentIndex: {
const fc = SettingsData.frameColor;
if (!fc || fc === "default") return 0;
if (fc === "primary") return 1;
if (fc === "surface") return 2;
return 3;
}
onSelectionChanged: (index, selected) => {
if (!selected) return;
switch (index) {
case 0: SettingsData.set("frameColor", ""); break;
case 1: SettingsData.set("frameColor", "primary"); break;
case 2: SettingsData.set("frameColor", "surface"); break;
case 3:
const cur = SettingsData.frameColor;
const isPreset = !cur || cur === "primary" || cur === "surface";
if (isPreset) SettingsData.set("frameColor", "#2a2a2a");
break;
}
}
}
// Custom color swatch — only visible when a hex color is stored (Custom mode)
Item {
visible: {
const fc = SettingsData.frameColor;
return !!(fc && fc !== "primary" && fc !== "surface");
}
width: parent.width
height: customColorRow.height + Theme.spacingM * 2
Row {
id: customColorRow
width: parent.width - Theme.spacingM * 2
x: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
StyledText {
anchors.verticalCenter: parent.verticalCenter
text: I18n.tr("Custom color")
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
}
Rectangle {
id: colorSwatch
anchors.verticalCenter: parent.verticalCenter
width: 32
height: 32
radius: 16
color: SettingsData.effectiveFrameColor
border.color: Theme.outline
border.width: 1
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
PopoutService.colorPickerModal.selectedColor = SettingsData.effectiveFrameColor;
PopoutService.colorPickerModal.pickerTitle = I18n.tr("Frame Border Color");
PopoutService.colorPickerModal.onColorSelectedCallback = function (color) {
SettingsData.set("frameColor", color.toString());
};
PopoutService.colorPickerModal.show();
}
}
}
}
}
}
// ── Bar Integration ───────────────────────────────────────────────
SettingsCard {
width: parent.width
iconName: "toolbar"
title: I18n.tr("Bar Integration")
settingKey: "frameBarIntegration"
collapsible: true
expanded: false
visible: SettingsData.frameEnabled
SettingsToggleRow {
visible: CompositorService.isNiri
settingKey: "frameShowOnOverview"
tags: ["frame", "overview", "show", "hide", "niri"]
text: I18n.tr("Show on Overview")
description: I18n.tr("Show the bar and frame during Niri overview mode")
checked: SettingsData.frameShowOnOverview
onToggled: checked => SettingsData.set("frameShowOnOverview", checked)
}
}
// ── Display Assignment ────────────────────────────────────────────
SettingsCard {
width: parent.width
iconName: "monitor"
title: I18n.tr("Display Assignment")
settingKey: "frameDisplays"
collapsible: true
expanded: false
visible: SettingsData.frameEnabled
SettingsDisplayPicker {
displayPreferences: SettingsData.frameScreenPreferences
onPreferencesChanged: prefs => SettingsData.set("frameScreenPreferences", prefs)
}
}
}
}
}

View File

@@ -125,6 +125,15 @@ Item {
return Theme.warning; return Theme.warning;
} }
function openBlurBorderColorPicker() {
PopoutService.colorPickerModal.selectedColor = SettingsData.blurBorderCustomColor ?? "#ffffff";
PopoutService.colorPickerModal.pickerTitle = I18n.tr("Blur Border Color");
PopoutService.colorPickerModal.onColorSelectedCallback = function (color) {
SettingsData.set("blurBorderCustomColor", color.toString());
};
PopoutService.colorPickerModal.open();
}
function openM3ShadowColorPicker() { function openM3ShadowColorPicker() {
PopoutService.colorPickerModal.selectedColor = SettingsData.m3ElevationCustomColor ?? "#000000"; PopoutService.colorPickerModal.selectedColor = SettingsData.m3ElevationCustomColor ?? "#000000";
PopoutService.colorPickerModal.pickerTitle = I18n.tr("Shadow Color"); PopoutService.colorPickerModal.pickerTitle = I18n.tr("Shadow Color");
@@ -1816,6 +1825,77 @@ Item {
} }
} }
SettingsCard {
tab: "theme"
tags: ["blur", "background", "transparency", "glass", "frosted"]
title: I18n.tr("Background Blur")
settingKey: "blurEnabled"
iconName: "blur_on"
SettingsToggleRow {
tab: "theme"
tags: ["blur", "background", "transparency", "glass", "frosted"]
settingKey: "blurEnabled"
text: I18n.tr("Background Blur")
description: BlurService.available ? I18n.tr("Blur the background behind bars, popouts, modals, and notifications. Requires compositor support and configuration.") : I18n.tr("Requires a newer version of Quickshell")
checked: SettingsData.blurEnabled ?? false
enabled: BlurService.available
onToggled: checked => SettingsData.set("blurEnabled", checked)
}
SettingsDropdownRow {
tab: "theme"
tags: ["blur", "border", "outline", "edge"]
settingKey: "blurBorderColor"
text: I18n.tr("Blur Border Color")
description: I18n.tr("Border color around blurred surfaces")
visible: SettingsData.blurEnabled
options: [I18n.tr("Outline", "blur border color"), I18n.tr("Primary", "blur border color"), I18n.tr("Secondary", "blur border color"), I18n.tr("Text Color", "blur border color"), I18n.tr("Custom", "blur border color")]
currentValue: {
switch (SettingsData.blurBorderColor) {
case "primary":
return I18n.tr("Primary", "blur border color");
case "secondary":
return I18n.tr("Secondary", "blur border color");
case "surfaceText":
return I18n.tr("Text Color", "blur border color");
case "custom":
return I18n.tr("Custom", "blur border color");
default:
return I18n.tr("Outline", "blur border color");
}
}
onValueChanged: value => {
if (value === I18n.tr("Primary", "blur border color")) {
SettingsData.set("blurBorderColor", "primary");
} else if (value === I18n.tr("Secondary", "blur border color")) {
SettingsData.set("blurBorderColor", "secondary");
} else if (value === I18n.tr("Text Color", "blur border color")) {
SettingsData.set("blurBorderColor", "surfaceText");
} else if (value === I18n.tr("Custom", "blur border color")) {
SettingsData.set("blurBorderColor", "custom");
openBlurBorderColorPicker();
} else {
SettingsData.set("blurBorderColor", "outline");
}
}
}
SettingsSliderRow {
tab: "theme"
tags: ["blur", "border", "opacity"]
settingKey: "blurBorderOpacity"
text: I18n.tr("Blur Border Opacity")
visible: SettingsData.blurEnabled
value: Math.round((SettingsData.blurBorderOpacity ?? 1.0) * 100)
minimum: 0
maximum: 100
unit: "%"
defaultValue: 100
onSliderValueChanged: newValue => SettingsData.set("blurBorderOpacity", newValue / 100)
}
}
SettingsCard { SettingsCard {
tab: "theme" tab: "theme"
tags: ["niri", "layout", "gaps", "radius", "window", "border"] tags: ["niri", "layout", "gaps", "radius", "window", "border"]
@@ -2602,7 +2682,6 @@ Item {
onToggled: checked => SettingsData.set("matugenTemplateNeovim", checked) onToggled: checked => SettingsData.set("matugenTemplateNeovim", checked)
} }
SettingsDropdownRow { SettingsDropdownRow {
text: I18n.tr("Dark mode base") text: I18n.tr("Dark mode base")
tab: "theme" tab: "theme"

View File

@@ -83,7 +83,6 @@ Item {
description: modelData.width + "×" + modelData.height description: modelData.width + "×" + modelData.height
checked: localChecked checked: localChecked
onToggled: isChecked => { onToggled: isChecked => {
localChecked = isChecked;
var prefs = JSON.parse(JSON.stringify(root.displayPreferences)); var prefs = JSON.parse(JSON.stringify(root.displayPreferences));
if (!Array.isArray(prefs) || prefs.includes("all")) if (!Array.isArray(prefs) || prefs.includes("all"))
prefs = []; prefs = [];
@@ -94,6 +93,11 @@ Item {
model: modelData.model || "" model: modelData.model || ""
}); });
} }
if (prefs.length === 0) {
localChecked = true;
return;
}
localChecked = isChecked;
root.preferencesChanged(prefs); root.preferencesChanged(prefs);
} }
} }

View File

@@ -1,6 +1,5 @@
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Layouts
import qs.Common import qs.Common
import qs.Widgets import qs.Widgets
import qs.Services import qs.Services
@@ -52,15 +51,14 @@ Column {
height: implicitHeight height: implicitHeight
spacing: Theme.spacingM spacing: Theme.spacingM
RowLayout { Row {
width: parent.width
spacing: Theme.spacingM spacing: Theme.spacingM
DankIcon { DankIcon {
name: root.titleIcon name: root.titleIcon
size: Theme.iconSize size: Theme.iconSize
color: Theme.primary color: Theme.primary
Layout.alignment: Qt.AlignVCenter anchors.verticalCenter: parent.verticalCenter
} }
StyledText { StyledText {
@@ -68,7 +66,7 @@ Column {
font.pixelSize: Theme.fontSizeLarge font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium font.weight: Font.Medium
color: Theme.surfaceText color: Theme.surfaceText
Layout.alignment: Qt.AlignVCenter anchors.verticalCenter: parent.verticalCenter
} }
} }
@@ -981,10 +979,26 @@ Column {
Repeater { Repeater {
model: [ model: [
{ label: I18n.tr("Percentage"), mode: 0, icon: "percent" }, {
{ label: I18n.tr("Total"), mode: 1, icon: "storage" }, label: I18n.tr("Percentage"),
{ label: I18n.tr("Remaining"), mode: 2, icon: "hourglass_empty" }, mode: 0,
{ label: I18n.tr("Remaining / Total"), mode: 3, icon: "pie_chart" } icon: "percent"
},
{
label: I18n.tr("Total"),
mode: 1,
icon: "storage"
},
{
label: I18n.tr("Remaining"),
mode: 2,
icon: "hourglass_empty"
},
{
label: I18n.tr("Remaining / Total"),
mode: 3,
icon: "pie_chart"
}
] ]
delegate: Rectangle { delegate: Rectangle {
@@ -1316,20 +1330,7 @@ Column {
id: longestControlCenterLabelMetrics id: longestControlCenterLabelMetrics
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
text: { text: {
const labels = [ const labels = [I18n.tr("Network"), I18n.tr("VPN"), I18n.tr("Bluetooth"), I18n.tr("Audio"), I18n.tr("Volume"), I18n.tr("Microphone"), I18n.tr("Microphone Volume"), I18n.tr("Brightness"), I18n.tr("Brightness Value"), I18n.tr("Battery"), I18n.tr("Printer"), I18n.tr("Screen Sharing")];
I18n.tr("Network"),
I18n.tr("VPN"),
I18n.tr("Bluetooth"),
I18n.tr("Audio"),
I18n.tr("Volume"),
I18n.tr("Microphone"),
I18n.tr("Microphone Volume"),
I18n.tr("Brightness"),
I18n.tr("Brightness Value"),
I18n.tr("Battery"),
I18n.tr("Printer"),
I18n.tr("Screen Sharing")
];
let longest = ""; let longest = "";
for (let i = 0; i < labels.length; i++) { for (let i = 0; i < labels.length; i++) {
if (labels[i].length > longest.length) if (labels[i].length > longest.length)
@@ -1340,6 +1341,7 @@ Column {
} }
Repeater { Repeater {
id: groupRepeater
model: controlCenterContextMenu.controlCenterGroups model: controlCenterContextMenu.controlCenterGroups
delegate: Item { delegate: Item {
@@ -1569,8 +1571,6 @@ Column {
} }
} }
} }
id: groupRepeater
} }
} }
} }

View File

@@ -0,0 +1,113 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Wayland // ! Import is needed despite what qmlls says
import qs.Common
Singleton {
id: root
property bool quickshellSupported: false
property bool compositorSupported: false
property bool available: quickshellSupported && compositorSupported
readonly property bool enabled: available && (SettingsData.blurEnabled ?? false)
readonly property color borderColor: {
if (!enabled)
return "transparent";
const opacity = SettingsData.blurBorderOpacity ?? 0.5;
switch (SettingsData.blurBorderColor ?? "outline") {
case "primary":
return Theme.withAlpha(Theme.primary, opacity);
case "secondary":
return Theme.withAlpha(Theme.secondary, opacity);
case "surfaceText":
return Theme.withAlpha(Theme.surfaceText, opacity);
case "custom":
return Theme.withAlpha(SettingsData.blurBorderCustomColor ?? "#ffffff", opacity);
default:
return Theme.withAlpha(Theme.outline, opacity);
}
}
readonly property int borderWidth: enabled ? 1 : 0
function hoverColor(baseColor, hoverAlpha) {
if (!enabled)
return baseColor;
return Theme.withAlpha(baseColor, hoverAlpha ?? 0.15);
}
function createBlurRegion(targetWindow) {
if (!available)
return null;
try {
const region = Qt.createQmlObject(`
import Quickshell
Region {}
`, targetWindow, "BlurRegion");
targetWindow.BackgroundEffect.blurRegion = region;
return region;
} catch (e) {
console.warn("BlurService: Failed to create blur region:", e);
return null;
}
}
function reapplyBlurRegion(targetWindow, region) {
if (!region || !available)
return;
try {
targetWindow.BackgroundEffect.blurRegion = region;
region.changed();
} catch (e) {}
}
function destroyBlurRegion(targetWindow, region) {
if (!region)
return;
try {
targetWindow.BackgroundEffect.blurRegion = null;
} catch (e) {}
region.destroy();
}
Process {
id: blurProbe
running: false
command: ["dms", "blur", "check"]
stdout: StdioCollector {
onStreamFinished: {
root.compositorSupported = text.trim() === "supported";
if (root.compositorSupported)
console.info("BlurService: Compositor supports ext-background-effect-v1");
else
console.info("BlurService: Compositor does not support ext-background-effect-v1");
}
}
onExited: exitCode => {
if (exitCode !== 0)
console.warn("BlurService: blur probe failed with code:", exitCode);
}
}
Component.onCompleted: {
try {
const test = Qt.createQmlObject(`
import Quickshell
Region { radius: 0 }
`, root, "BlurAvailabilityTest");
test.destroy();
quickshellSupported = true;
console.info("BlurService: Quickshell blur support available");
blurProbe.running = true;
} catch (e) {
console.info("BlurService: BackgroundEffect not available - blur disabled. Requires a newer version of Quickshell.");
}
}
}

View File

@@ -89,7 +89,7 @@ Row {
width: Math.max(contentItem.implicitWidth + root.buttonPadding * 2, root.minButtonWidth) + (selected ? 4 : 0) width: Math.max(contentItem.implicitWidth + root.buttonPadding * 2, root.minButtonWidth) + (selected ? 4 : 0)
height: root.buttonHeight height: root.buttonHeight
color: selected ? Theme.buttonBg : Theme.surfaceVariant color: selected ? Theme.buttonBg : Theme.withAlpha(Theme.surfaceVariant, Theme.popupTransparency)
border.color: "transparent" border.color: "transparent"
border.width: 0 border.width: 0

View File

@@ -107,6 +107,16 @@ PanelWindow {
} }
WlrLayershell.exclusiveZone: -1 WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
WindowBlur {
targetWindow: root
blurX: shadowBuffer
blurY: shadowBuffer
blurWidth: shouldBeVisible ? alignedWidth : 0
blurHeight: shouldBeVisible ? alignedHeight : 0
blurRadius: Theme.cornerRadius
}
color: "transparent" color: "transparent"
readonly property real dpr: CompositorService.getScreenScale(screen) readonly property real dpr: CompositorService.getScreenScale(screen)
@@ -256,15 +266,15 @@ PanelWindow {
scale: shouldBeVisible ? 1 : 0.9 scale: shouldBeVisible ? 1 : 0.9
property bool childHovered: false property bool childHovered: false
readonly property real popupSurfaceAlpha: SettingsData.popupTransparency readonly property real popupSurfaceAlpha: Theme.popupTransparency
Rectangle { Rectangle {
id: background id: background
anchors.fill: parent anchors.fill: parent
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainer, osdContainer.popupSurfaceAlpha) color: Theme.withAlpha(Theme.surfaceContainer, osdContainer.popupSurfaceAlpha)
border.color: Theme.outlineMedium border.color: BlurService.enabled ? BlurService.borderColor : Theme.outlineMedium
border.width: 1 border.width: BlurService.enabled ? BlurService.borderWidth : 1
z: -1 z: -1
} }
@@ -276,7 +286,7 @@ PanelWindow {
level: Theme.elevationLevel3 level: Theme.elevationLevel3
fallbackOffset: 6 fallbackOffset: 6
targetRadius: Theme.cornerRadius targetRadius: Theme.cornerRadius
targetColor: Theme.surfaceContainer targetColor: Theme.withAlpha(Theme.surfaceContainer, osdContainer.popupSurfaceAlpha)
borderColor: Theme.outlineMedium borderColor: Theme.outlineMedium
borderWidth: 1 borderWidth: 1
shadowEnabled: Theme.elevationEnabled && SettingsData.popoutElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1" shadowEnabled: Theme.elevationEnabled && SettingsData.popoutElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1"

View File

@@ -398,6 +398,17 @@ Item {
visible: false visible: false
color: "transparent" color: "transparent"
WindowBlur {
id: popoutBlur
targetWindow: contentWindow
readonly property real s: Math.min(1, contentContainer.scaleValue)
blurX: contentContainer.x + contentContainer.width * (1 - s) * 0.5 + Theme.snap(contentContainer.animX, root.dpr)
blurY: contentContainer.y + contentContainer.height * (1 - s) * 0.5 + Theme.snap(contentContainer.animY, root.dpr)
blurWidth: (shouldBeVisible && contentWrapper.opacity > 0) ? contentContainer.width * s : 0
blurHeight: (shouldBeVisible && contentWrapper.opacity > 0) ? contentContainer.height * s : 0
blurRadius: Theme.cornerRadius
}
WlrLayershell.namespace: root.layerNamespace WlrLayershell.namespace: root.layerNamespace
WlrLayershell.layer: { WlrLayershell.layer: {
switch (Quickshell.env("DMS_POPOUT_LAYER")) { switch (Quickshell.env("DMS_POPOUT_LAYER")) {
@@ -565,14 +576,6 @@ Item {
} }
} }
Rectangle {
anchors.fill: parent
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
border.color: Theme.outlineMedium
border.width: 0
}
Loader { Loader {
id: contentLoader id: contentLoader
anchors.fill: parent anchors.fill: parent
@@ -580,6 +583,21 @@ Item {
asynchronous: false asynchronous: false
} }
} }
Rectangle {
width: parent.width
height: parent.height
x: contentWrapper.x
y: contentWrapper.y
opacity: contentWrapper.opacity
scale: contentWrapper.scale
visible: contentWrapper.visible
radius: Theme.cornerRadius
color: "transparent"
border.color: BlurService.enabled ? BlurService.borderColor : Theme.outlineMedium
border.width: BlurService.borderWidth
z: 100
}
} }
Item { Item {

View File

@@ -238,7 +238,7 @@ Rectangle {
width: fieldContent.width + Theme.spacingM * 2 width: fieldContent.width + Theme.spacingM * 2
height: 32 height: 32
radius: Theme.cornerRadius - 2 radius: Theme.cornerRadius - 2
color: Theme.surfaceContainerHigh color: Theme.surfaceLight
border.width: 1 border.width: 1
border.color: Theme.outlineLight border.color: Theme.outlineLight
@@ -272,7 +272,9 @@ Rectangle {
checked: configData ? (configData.autoconnect || false) : false checked: configData ? (configData.autoconnect || false) : false
visible: !VPNService.configLoading && configData !== null visible: !VPNService.configLoading && configData !== null
onToggled: checked => { onToggled: checked => {
VPNService.updateConfig(profile.uuid, {autoconnect: checked}); VPNService.updateConfig(profile.uuid, {
autoconnect: checked
});
} }
} }

View File

@@ -0,0 +1,65 @@
import QtQuick
import qs.Services
Item {
id: root
visible: false
required property var targetWindow
property var blurItem: null
property real blurX: 0
property real blurY: 0
property real blurWidth: 0
property real blurHeight: 0
property real blurRadius: 0
property var _region: null
function _apply() {
if (!BlurService.enabled || !targetWindow) {
_cleanup();
return;
}
if (!_region)
_region = BlurService.createBlurRegion(targetWindow);
if (!_region)
return;
_region.item = Qt.binding(() => root.blurItem);
_region.x = Qt.binding(() => root.blurX);
_region.y = Qt.binding(() => root.blurY);
_region.width = Qt.binding(() => root.blurWidth);
_region.height = Qt.binding(() => root.blurHeight);
_region.radius = Qt.binding(() => root.blurRadius);
}
function _cleanup() {
if (!_region)
return;
BlurService.destroyBlurRegion(targetWindow, _region);
_region = null;
}
Connections {
target: BlurService
function onEnabledChanged() {
root._apply();
}
}
Connections {
target: root.targetWindow
function onVisibleChanged() {
if (root.targetWindow && root.targetWindow.visible) {
root._region = null;
root._apply();
}
}
}
Component.onCompleted: _apply()
Component.onDestruction: _cleanup()
}