1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-05-02 10:32:07 -04:00

Compare commits

...

18 Commits

Author SHA1 Message Date
purian23
2261883426 (frameMode): Restore user settings when exiting frame mode
- Align blur settings in non-FrameMode motion settings
2026-04-06 17:48:59 -04:00
purian23
20d208b8bf (frame): Update connected mode with blur 2026-04-06 14:28:50 -04:00
purian23
f71d2d223e (frame): Update connected mode & opacity connection settings 2026-04-05 11:48:35 -04:00
purian23
eaf350482d (frameInMotion): Initial Unified Frame Connected Mode 2026-04-03 00:44:29 -04:00
purian23
f2bc348b62 Add Directional Motion options 2026-03-31 22:15:51 -04:00
purian23
4784087dc2 Initial staging for Animation & Motion effects 2026-03-31 22:13:07 -04:00
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
78 changed files with 4398 additions and 794 deletions

View File

@@ -1,26 +1,13 @@
repos:
- repo: local
- repo: https://github.com/golangci/golangci-lint
rev: v2.10.1
hooks:
- id: golangci-lint-fmt
name: golangci-lint-fmt
entry: go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.11.3 fmt
language: system
require_serial: true
types: [go]
pass_filenames: false
- id: golangci-lint-full
name: golangci-lint-full
entry: go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.11.3 run --fix
language: system
require_serial: true
types: [go]
pass_filenames: false
- id: golangci-lint-config-verify
name: golangci-lint-config-verify
entry: go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.11.3 config verify
language: system
files: \.golangci\.(?:yml|yaml|toml|json)
pass_filenames: false
- repo: local
hooks:
- id: go-test
name: go test
entry: go test ./...

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,
dlCmd,
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 {
x, y int32
scale float64
transform int32
}
func getOutputInfo(outputName string) (*outputInfo, bool) {
func getAllOutputInfos() map[string]*outputInfo {
display, err := client.Connect("")
if err != nil {
return nil, false
return nil
}
ctx := display.Context()
defer ctx.Close()
registry, err := display.GetRegistry()
if err != nil {
return nil, false
return nil
}
var outputManager *wlr_output_management.ZwlrOutputManagerV1
@@ -476,16 +477,17 @@ func getOutputInfo(outputName string) (*outputInfo, bool) {
})
if err := wlhelpers.Roundtrip(display, ctx); err != nil {
return nil, false
return nil
}
if outputManager == nil {
return nil, false
return nil
}
type headState struct {
name string
x, y int32
scale float64
transform int32
}
heads := make(map[*wlr_output_management.ZwlrOutputHeadV1]*headState)
@@ -501,6 +503,9 @@ func getOutputInfo(outputName string) (*outputInfo, bool) {
state.x = pe.X
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) {
state.transform = te.Transform
})
@@ -511,21 +516,32 @@ func getOutputInfo(outputName string) (*outputInfo, bool) {
for !done {
if err := ctx.Dispatch(); err != nil {
return nil, false
return nil
}
}
result := make(map[string]*outputInfo, len(heads))
for _, state := range heads {
if state.name == outputName {
return &outputInfo{
x: state.x,
y: state.y,
transform: state.transform,
}, true
if state.name == "" {
continue
}
result[state.name] = &outputInfo{
x: state.x,
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) {

View File

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

View File

@@ -0,0 +1,179 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import qs.Common
// AnimVariants — Central tuning for animation and Motion Effects variants
// (Material/Fluent/Dynamic) (Standard/Directional/Depth)
Singleton {
id: root
readonly property list<real> variantEnterCurve: {
if (typeof SettingsData === "undefined")
return Anims.expressiveDefaultSpatial;
switch (SettingsData.animationVariant) {
case 1:
return Anims.standardDecel;
case 2:
return Anims.expressiveFastSpatial;
default:
return Anims.expressiveDefaultSpatial;
}
}
readonly property list<real> variantExitCurve: {
if (typeof SettingsData === "undefined")
return Anims.emphasized;
switch (SettingsData.animationVariant) {
case 1:
return Anims.standard;
case 2:
return Anims.emphasized;
default:
return Anims.emphasized;
}
}
// Modal-specific entry curve
readonly property list<real> variantModalEnterCurve: {
if (typeof SettingsData === "undefined")
return Anims.expressiveDefaultSpatial;
if (isDirectionalEffect) {
if (SettingsData.animationVariant === 1)
return Anims.standardDecel;
if (SettingsData.animationVariant === 2)
return Anims.expressiveFastSpatial;
}
return variantEnterCurve;
}
readonly property list<real> variantModalExitCurve: {
if (typeof SettingsData === "undefined")
return Anims.emphasized;
if (isDirectionalEffect) {
if (SettingsData.animationVariant === 1)
return Anims.emphasizedAccel;
if (SettingsData.animationVariant === 2)
return Anims.emphasizedAccel;
}
return variantExitCurve;
}
// Popout-specific entry curve
readonly property list<real> variantPopoutEnterCurve: {
if (typeof SettingsData === "undefined")
return Anims.expressiveDefaultSpatial;
if (isDirectionalEffect) {
if (SettingsData.animationVariant === 1)
return Anims.standardDecel;
if (SettingsData.animationVariant === 2)
return Anims.expressiveFastSpatial;
return Anims.standardDecel;
}
return variantEnterCurve;
}
readonly property list<real> variantPopoutExitCurve: {
if (typeof SettingsData === "undefined")
return Anims.emphasized;
if (isDirectionalEffect) {
if (SettingsData.animationVariant === 1)
return Anims.emphasizedAccel;
if (SettingsData.animationVariant === 2)
return Anims.emphasizedAccel;
}
return variantExitCurve;
}
readonly property real variantEnterDurationFactor: {
if (typeof SettingsData === "undefined")
return 1.0;
switch (SettingsData.animationVariant) {
case 1:
return 0.9;
case 2:
return 1.08;
default:
return 1.0;
}
}
readonly property real variantExitDurationFactor: {
if (typeof SettingsData === "undefined")
return 1.0;
switch (SettingsData.animationVariant) {
case 1:
return 0.85;
case 2:
return 0.92;
default:
return 1.0;
}
}
// Fluent: opacity at ~55% of duration; Material/Dynamic: 1:1 with position
readonly property real variantOpacityDurationScale: {
if (typeof SettingsData === "undefined")
return 1.0;
return SettingsData.animationVariant === 1 ? 0.55 : 1.0;
}
function variantDuration(baseDuration, entering) {
const factor = entering ? variantEnterDurationFactor : variantExitDurationFactor;
return Math.max(0, Math.round(baseDuration * factor));
}
function variantExitCleanupPadding() {
if (typeof SettingsData === "undefined")
return 50;
switch (SettingsData.motionEffect) {
case 1:
return 8;
case 2:
return 24;
default:
return 50;
}
}
function variantCloseInterval(baseDuration) {
return variantDuration(baseDuration, false) + variantExitCleanupPadding();
}
readonly property bool isDirectionalEffect: isConnectedEffect
|| (typeof SettingsData !== "undefined" && SettingsData.motionEffect === 1)
readonly property bool isDepthEffect: typeof SettingsData !== "undefined" && SettingsData.motionEffect === 2
readonly property bool isConnectedEffect: typeof SettingsData !== "undefined"
&& SettingsData.frameEnabled
&& SettingsData.motionEffect === 1
&& SettingsData.directionalAnimationMode === 3
readonly property real effectScaleCollapsed: {
if (typeof SettingsData === "undefined")
return 0.96;
switch (SettingsData.motionEffect) {
case 1:
return 1.0;
case 2:
return 0.88;
default:
return 0.96;
}
}
readonly property real effectAnimOffset: {
if (typeof SettingsData === "undefined")
return 16;
switch (SettingsData.motionEffect) {
case 1:
return 144;
case 2:
return 56;
default:
return 16;
}
}
}

View File

@@ -22,4 +22,9 @@ Singleton {
readonly property var standard: [0.20, 0.00, 0.00, 1.00, 1.00, 1.00]
readonly property var standardDecel: [0.00, 0.00, 0.00, 1.00, 1.00, 1.00]
readonly property var standardAccel: [0.30, 0.00, 1.00, 1.00, 1.00, 1.00]
// Used by AnimVariants for variant/effect logic
readonly property var expressiveDefaultSpatial: [0.38, 1.21, 0.22, 1, 1, 1]
readonly property var expressiveFastSpatial: [0.34, 1.5, 0.2, 1.0, 1.0, 1.0]
readonly property var expressiveEffects: [0.34, 0.8, 0.34, 1, 1, 1]
}

View File

@@ -13,8 +13,13 @@ Item {
property color targetColor: "white"
property real targetRadius: Theme.cornerRadius
property real topLeftRadius: targetRadius
property real topRightRadius: targetRadius
property real bottomLeftRadius: targetRadius
property real bottomRightRadius: targetRadius
property color borderColor: "transparent"
property real borderWidth: 0
property bool useCustomSource: false
property bool shadowEnabled: Theme.elevationEnabled
property real shadowBlurPx: level && level.blurPx !== undefined ? level.blurPx : 0
@@ -46,7 +51,11 @@ Item {
Rectangle {
id: sourceRect
anchors.fill: parent
radius: root.targetRadius
visible: !root.useCustomSource
topLeftRadius: root.topLeftRadius
topRightRadius: root.topRightRadius
bottomLeftRadius: root.bottomLeftRadius
bottomRightRadius: root.bottomRightRadius
color: root.targetColor
border.color: root.borderColor
border.width: root.borderWidth

View File

@@ -14,7 +14,7 @@ import "settings/SettingsStore.js" as Store
Singleton {
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"
@@ -37,6 +37,18 @@ Singleton {
Custom
}
enum AnimationVariant {
Material,
Fluent,
Dynamic
}
enum AnimationEffect {
Standard, // 0 — M3: scale-in, rises from below
Directional, // 1 — pure large slide, no scale
Depth // 2 — medium slide with deep depth scale pop
}
enum SuspendBehavior {
Suspend,
Hibernate,
@@ -168,6 +180,12 @@ Singleton {
property int modalCustomAnimationDuration: 150
property bool enableRippleEffects: true
onEnableRippleEffectsChanged: saveSettings()
property int animationVariant: SettingsData.AnimationVariant.Material
onAnimationVariantChanged: saveSettings()
property int motionEffect: SettingsData.AnimationEffect.Standard
onMotionEffectChanged: saveSettings()
property int directionalAnimationMode: 0
onDirectionalAnimationModeChanged: saveSettings()
property bool m3ElevationEnabled: true
onM3ElevationEnabledChanged: saveSettings()
property int m3ElevationIntensity: 12
@@ -186,10 +204,63 @@ Singleton {
onPopoutElevationEnabledChanged: saveSettings()
property bool barElevationEnabled: true
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 bool blurredWallpaperLayer: 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()
property int previousDirectionalMode: 1
onPreviousDirectionalModeChanged: saveSettings()
property var connectedFrameBarStyleBackups: ({})
onConnectedFrameBarStyleBackupsChanged: saveSettings()
readonly property bool connectedFrameModeActive: frameEnabled
&& motionEffect === SettingsData.AnimationEffect.Directional
&& directionalAnimationMode === 3
onConnectedFrameModeActiveChanged: {
if (_loading)
return;
if (connectedFrameModeActive) {
_captureConnectedFrameBarStyleBackups(barConfigs, true);
_enforceConnectedModeBarStyleReset();
} else {
_restoreConnectedFrameBarStyleBackups();
}
}
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 showWorkspaceSwitcher: true
property bool showFocusedWindow: true
@@ -1275,6 +1346,7 @@ Singleton {
_loading = false;
}
loadPluginSettings();
Qt.callLater(() => _reconcileConnectedFrameBarStyles());
}
property var _pendingMigration: null
@@ -1388,6 +1460,149 @@ Singleton {
pluginSettingsFile.setText(JSON.stringify(pluginSettings, null, 2));
}
function _connectedFrameBarStyleSnapshot(config) {
return {
"shadowIntensity": config?.shadowIntensity ?? 0,
"squareCorners": config?.squareCorners ?? false,
"gothCornersEnabled": config?.gothCornersEnabled ?? false,
"borderEnabled": config?.borderEnabled ?? false
};
}
function _hasConnectedFrameBarStyleBackups() {
return connectedFrameBarStyleBackups && Object.keys(connectedFrameBarStyleBackups).length > 0;
}
function _captureConnectedFrameBarStyleBackups(configs, overwriteExisting) {
if (!Array.isArray(configs))
return;
const nextBackups = JSON.parse(JSON.stringify(connectedFrameBarStyleBackups || {}));
const validIds = {};
let changed = false;
for (let i = 0; i < configs.length; i++) {
const config = configs[i];
if (!config?.id)
continue;
validIds[config.id] = true;
if (!overwriteExisting && nextBackups[config.id] !== undefined)
continue;
const snapshot = _connectedFrameBarStyleSnapshot(config);
if (JSON.stringify(nextBackups[config.id]) !== JSON.stringify(snapshot)) {
nextBackups[config.id] = snapshot;
changed = true;
}
}
if (overwriteExisting) {
for (const barId in nextBackups) {
if (validIds[barId])
continue;
delete nextBackups[barId];
changed = true;
}
}
if (changed)
connectedFrameBarStyleBackups = nextBackups;
}
function _restoreConnectedFrameBarStyleBackups() {
if (!_hasConnectedFrameBarStyleBackups())
return;
const backups = connectedFrameBarStyleBackups || {};
const configs = JSON.parse(JSON.stringify(barConfigs));
let changed = false;
for (let i = 0; i < configs.length; i++) {
const backup = backups[configs[i].id];
if (!backup)
continue;
for (const key in backup) {
if (configs[i][key] === backup[key])
continue;
configs[i][key] = backup[key];
changed = true;
}
}
if (changed)
barConfigs = configs;
connectedFrameBarStyleBackups = ({});
if (changed)
updateBarConfigs();
}
function _reconcileConnectedFrameBarStyles() {
if (connectedFrameModeActive) {
if (!_hasConnectedFrameBarStyleBackups())
_captureConnectedFrameBarStyleBackups(barConfigs, true);
_enforceConnectedModeBarStyleReset();
return;
}
_restoreConnectedFrameBarStyleBackups();
}
function _sanitizeBarConfigForConnectedFrame(config) {
if (!connectedFrameModeActive || !config)
return config;
let changed = false;
const sanitized = Object.assign({}, config);
if ((sanitized.shadowIntensity ?? 0) !== 0) {
sanitized.shadowIntensity = 0;
changed = true;
}
if (sanitized.squareCorners ?? false) {
sanitized.squareCorners = false;
changed = true;
}
if (sanitized.gothCornersEnabled ?? false) {
sanitized.gothCornersEnabled = false;
changed = true;
}
if (sanitized.borderEnabled ?? false) {
sanitized.borderEnabled = false;
changed = true;
}
return changed ? sanitized : config;
}
function _sanitizeBarConfigsForConnectedFrame(configs) {
if (!connectedFrameModeActive || !Array.isArray(configs))
return {
"configs": configs,
"changed": false
};
let changed = false;
const sanitizedConfigs = configs.map(config => {
const sanitized = _sanitizeBarConfigForConnectedFrame(config);
if (sanitized !== config)
changed = true;
return sanitized;
});
return {
"configs": changed ? sanitizedConfigs : configs,
"changed": changed
};
}
function _enforceConnectedModeBarStyleReset() {
const result = _sanitizeBarConfigsForConnectedFrame(barConfigs);
if (!result.changed)
return;
barConfigs = result.configs;
updateBarConfigs();
}
function detectAvailableIconThemes() {
const xdgDataDirs = Quickshell.env("XDG_DATA_DIRS") || "";
const localData = Paths.strip(StandardPaths.writableLocation(StandardPaths.GenericDataLocation));
@@ -1535,35 +1750,37 @@ Singleton {
const spacing = barSpacing !== undefined ? barSpacing : (defaultBar?.spacing ?? 4);
const position = barPosition !== undefined ? barPosition : (defaultBar?.position ?? SettingsData.Position.Top);
const rawBottomGap = barConfig ? (barConfig.bottomGap !== undefined ? barConfig.bottomGap : (defaultBar?.bottomGap ?? 0)) : (defaultBar?.bottomGap ?? 0);
const bottomGap = Math.max(0, rawBottomGap);
const isConnected = connectedFrameModeActive;
const bottomGap = isConnected ? 0 : Math.max(0, rawBottomGap);
const useAutoGaps = (barConfig && barConfig.popupGapsAuto !== undefined) ? barConfig.popupGapsAuto : (defaultBar?.popupGapsAuto ?? true);
const manualGapValue = (barConfig && barConfig.popupGapsManual !== undefined) ? barConfig.popupGapsManual : (defaultBar?.popupGapsManual ?? 4);
const popupGap = useAutoGaps ? Math.max(4, spacing) : manualGapValue;
const popupGap = isConnected ? 0 : (useAutoGaps ? Math.max(4, spacing) : manualGapValue);
const edgeSpacing = isConnected ? 0 : spacing;
switch (position) {
case SettingsData.Position.Left:
return {
"x": barThickness + spacing + popupGap,
"x": barThickness + edgeSpacing + popupGap,
"y": relativeY,
"width": widgetWidth
};
case SettingsData.Position.Right:
return {
"x": (screen?.width || 0) - (barThickness + spacing + popupGap),
"x": (screen?.width || 0) - (barThickness + edgeSpacing + popupGap),
"y": relativeY,
"width": widgetWidth
};
case SettingsData.Position.Bottom:
return {
"x": relativeX,
"y": (screen?.height || 0) - (barThickness + spacing + bottomGap + popupGap),
"y": (screen?.height || 0) - (barThickness + edgeSpacing + bottomGap + popupGap),
"width": widgetWidth
};
default:
return {
"x": relativeX,
"y": barThickness + spacing + bottomGap + popupGap,
"y": barThickness + edgeSpacing + bottomGap + popupGap,
"width": widgetWidth
};
}
@@ -1657,7 +1874,9 @@ Singleton {
const screenWidth = screen.width;
const screenHeight = screen.height;
const position = barPosition !== undefined ? barPosition : (defaultBar?.position ?? SettingsData.Position.Top);
const bottomGap = barConfig ? (barConfig.bottomGap !== undefined ? barConfig.bottomGap : (defaultBar?.bottomGap ?? 0)) : (defaultBar?.bottomGap ?? 0);
const isConnected = connectedFrameModeActive;
const rawBottomGap = barConfig ? (barConfig.bottomGap !== undefined ? barConfig.bottomGap : (defaultBar?.bottomGap ?? 0)) : (defaultBar?.bottomGap ?? 0);
const bottomGap = isConnected ? 0 : rawBottomGap;
let topOffset = 0;
let bottomOffset = 0;
@@ -1679,7 +1898,7 @@ Singleton {
const otherSpacing = other.spacing !== undefined ? other.spacing : (defaultBar?.spacing ?? 4);
const otherPadding = other.innerPadding !== undefined ? other.innerPadding : (defaultBar?.innerPadding ?? 4);
const otherThickness = Math.max(26 + otherPadding * 0.6, Theme.barHeight - 4 - (8 - otherPadding)) + otherSpacing + wingSize;
const otherBottomGap = other.bottomGap !== undefined ? other.bottomGap : (defaultBar?.bottomGap ?? 0);
const otherBottomGap = isConnected ? 0 : (other.bottomGap !== undefined ? other.bottomGap : (defaultBar?.bottomGap ?? 0));
switch (other.position) {
case SettingsData.Position.Top:
@@ -1770,7 +1989,9 @@ Singleton {
function addBarConfig(config) {
const configs = JSON.parse(JSON.stringify(barConfigs));
configs.push(config);
barConfigs = configs;
if (connectedFrameModeActive)
_captureConnectedFrameBarStyleBackups(configs, false);
barConfigs = _sanitizeBarConfigsForConnectedFrame(configs).configs;
updateBarConfigs();
}
@@ -1782,7 +2003,7 @@ Singleton {
const positionChanged = updates.position !== undefined && configs[index].position !== updates.position;
Object.assign(configs[index], updates);
barConfigs = configs;
barConfigs = _sanitizeBarConfigsForConnectedFrame(configs).configs;
updateBarConfigs();
if (positionChanged) {
@@ -1836,6 +2057,11 @@ Singleton {
return;
const configs = barConfigs.filter(cfg => cfg.id !== barId);
barConfigs = configs;
if (connectedFrameBarStyleBackups?.[barId] !== undefined) {
const nextBackups = JSON.parse(JSON.stringify(connectedFrameBarStyleBackups || {}));
delete nextBackups[barId];
connectedFrameBarStyleBackups = nextBackups;
}
updateBarConfigs();
}
@@ -1930,6 +2156,66 @@ Singleton {
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() {
NotificationService.dismissAllPopups();
sendTestNotification(0);

View File

@@ -960,6 +960,40 @@ Singleton {
"expressiveEffects": [0.34, 0.8, 0.34, 1, 1, 1]
}
// Delegates to AnimVariants.qml for curves, timing, scale, and offsets.
readonly property list<real> variantEnterCurve: AnimVariants.variantEnterCurve
readonly property list<real> variantExitCurve: AnimVariants.variantExitCurve
readonly property list<real> variantModalEnterCurve: AnimVariants.variantModalEnterCurve
readonly property list<real> variantModalExitCurve: AnimVariants.variantModalExitCurve
readonly property list<real> variantPopoutEnterCurve: AnimVariants.variantPopoutEnterCurve
readonly property list<real> variantPopoutExitCurve: AnimVariants.variantPopoutExitCurve
readonly property real variantEnterDurationFactor: AnimVariants.variantEnterDurationFactor
readonly property real variantExitDurationFactor: AnimVariants.variantExitDurationFactor
readonly property real variantOpacityDurationScale: AnimVariants.variantOpacityDurationScale
readonly property bool isDirectionalEffect: AnimVariants.isDirectionalEffect
readonly property bool isDepthEffect: AnimVariants.isDepthEffect
readonly property bool isConnectedEffect: AnimVariants.isConnectedEffect
readonly property real connectedCornerRadius: {
if (typeof SettingsData === "undefined") return 12;
return SettingsData.frameEnabled ? SettingsData.frameRounding : cornerRadius;
}
readonly property color connectedSurfaceColor: {
if (typeof SettingsData === "undefined")
return withAlpha(surfaceContainer, popupTransparency);
return isConnectedEffect
? Qt.rgba(SettingsData.effectiveFrameColor.r, SettingsData.effectiveFrameColor.g, SettingsData.effectiveFrameColor.b, SettingsData.frameOpacity)
: withAlpha(surfaceContainer, popupTransparency);
}
readonly property real connectedSurfaceRadius: isConnectedEffect ? connectedCornerRadius : cornerRadius
readonly property bool connectedSurfaceBlurEnabled: (typeof SettingsData === "undefined")
? true
: (!isConnectedEffect || SettingsData.frameBlurEnabled)
readonly property real effectScaleCollapsed: AnimVariants.effectScaleCollapsed
readonly property real effectAnimOffset: AnimVariants.effectAnimOffset
function variantDuration(baseDuration, entering) { return AnimVariants.variantDuration(baseDuration, entering); }
function variantExitCleanupPadding() { return AnimVariants.variantExitCleanupPadding(); }
function variantCloseInterval(baseDuration) { return AnimVariants.variantCloseInterval(baseDuration); }
readonly property var animationPresetDurations: {
"none": 0,
"short": 250,
@@ -1125,7 +1159,13 @@ Singleton {
property real iconSizeLarge: 32
property real panelTransparency: 0.85
property real popupTransparency: typeof SettingsData !== "undefined" && SettingsData.popupTransparency !== undefined ? SettingsData.popupTransparency : 1.0
property real popupTransparency: {
if (typeof SettingsData === "undefined")
return 1.0;
if (isConnectedEffect)
return SettingsData.frameOpacity !== undefined ? SettingsData.frameOpacity : 1.0;
return SettingsData.popupTransparency !== undefined ? SettingsData.popupTransparency : 1.0;
}
function screenTransition() {
if (CompositorService.isNiri) {
@@ -1824,6 +1864,12 @@ Singleton {
return Qt.rgba(c.r, c.g, c.b, a);
}
function popupLayerColor(baseColor) {
if (isConnectedEffect)
return connectedSurfaceColor;
return withAlpha(baseColor, popupTransparency);
}
function blendAlpha(c, a) {
return Qt.rgba(c.r, c.g, c.b, c.a * a);
}

View File

@@ -49,6 +49,10 @@ var SPEC = {
modalAnimationSpeed: { def: 1 },
modalCustomAnimationDuration: { def: 150 },
enableRippleEffects: { def: true },
animationVariant: { def: 0 },
motionEffect: { def: 0 },
directionalAnimationMode: { def: 0 },
previousDirectionalMode: { def: 1 },
m3ElevationEnabled: { def: true },
m3ElevationIntensity: { def: 12 },
m3ElevationOpacity: { def: 30 },
@@ -58,6 +62,10 @@ var SPEC = {
modalElevationEnabled: { def: true },
popoutElevationEnabled: { def: true },
barElevationEnabled: { def: true },
blurEnabled: { def: false },
blurBorderColor: { def: "outline" },
blurBorderCustomColor: { def: "#ffffff" },
blurBorderOpacity: { def: 1.0, coerce: percentToUnit },
wallpaperFillMode: { def: "Fill" },
blurredWallpaperLayer: { def: false },
blurWallpaperOnOverview: { def: false },
@@ -437,6 +445,7 @@ var SPEC = {
displayProfileAutoSelect: { def: false },
displayShowDisconnected: { def: false },
displaySnapToEdge: { def: true },
connectedFrameBarStyleBackups: { def: {} },
barConfigs: {
def: [{
@@ -543,7 +552,17 @@ var SPEC = {
clipboardEnterToPaste: { def: false },
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() {

View File

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

View File

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

View File

@@ -3,6 +3,7 @@ import Quickshell
import Quickshell.Wayland
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
@@ -26,14 +27,20 @@ Item {
property bool closeOnBackgroundClick: true
property string animationType: "scale"
property int animationDuration: Theme.modalAnimationDuration
property real animationScaleCollapsed: 0.96
property real animationOffset: Theme.spacingL
property list<real> animationEnterCurve: Theme.expressiveCurves.expressiveDefaultSpatial
property list<real> animationExitCurve: Theme.expressiveCurves.emphasized
property real animationScaleCollapsed: Theme.effectScaleCollapsed
property real animationOffset: Theme.effectAnimOffset
property list<real> animationEnterCurve: Theme.variantModalEnterCurve
property list<real> animationExitCurve: Theme.variantModalExitCurve
property color backgroundColor: Theme.surfaceContainer
property color borderColor: Theme.outlineMedium
property real borderWidth: 0
property real cornerRadius: Theme.cornerRadius
readonly property bool connectedSurfaceOverride: Theme.isConnectedEffect
readonly property color effectiveBackgroundColor: connectedSurfaceOverride ? Theme.connectedSurfaceColor : backgroundColor
readonly property color effectiveBorderColor: connectedSurfaceOverride ? "transparent" : borderColor
readonly property real effectiveBorderWidth: connectedSurfaceOverride ? 0 : borderWidth
readonly property real effectiveCornerRadius: connectedSurfaceOverride ? Theme.connectedSurfaceRadius : cornerRadius
readonly property bool effectiveBlurEnabled: Theme.connectedSurfaceBlurEnabled
property bool enableShadow: true
property alias modalFocusScope: focusScope
property bool shouldBeVisible: false
@@ -44,11 +51,13 @@ Item {
property bool keepPopoutsOpen: false
property var customKeyboardFocus: null
property bool useOverlayLayer: false
property real frozenMotionOffsetX: 0
property real frozenMotionOffsetY: 0
readonly property alias contentWindow: contentWindow
readonly property alias clickCatcher: clickCatcher
readonly property bool useHyprlandFocusGrab: CompositorService.useHyprlandFocusGrab
readonly property bool useBackground: showBackground && SettingsData.modalDarkenBackground
readonly property bool useSingleWindow: CompositorService.isHyprland || useBackground
readonly property bool useSingleWindow: CompositorService.isHyprland
signal opened
signal dialogClosed
@@ -58,19 +67,34 @@ Item {
function open() {
closeTimer.stop();
animationsEnabled = false;
frozenMotionOffsetX = modalContainer ? modalContainer.offsetX : 0;
frozenMotionOffsetY = modalContainer ? modalContainer.offsetY : animationOffset;
const focusedScreen = CompositorService.getFocusedScreen();
if (focusedScreen) {
contentWindow.screen = focusedScreen;
if (!useSingleWindow)
clickCatcher.screen = focusedScreen;
}
if (Theme.isDirectionalEffect || root.useBackground) {
if (!useSingleWindow)
clickCatcher.visible = true;
contentWindow.visible = true;
}
ModalManager.openModal(root);
shouldBeVisible = true;
if (!useSingleWindow)
clickCatcher.visible = true;
contentWindow.visible = true;
shouldHaveFocus = false;
Qt.callLater(() => shouldHaveFocus = Qt.binding(() => shouldBeVisible));
Qt.callLater(() => {
animationsEnabled = true;
shouldBeVisible = true;
if (!useSingleWindow && !clickCatcher.visible)
clickCatcher.visible = true;
if (!contentWindow.visible)
contentWindow.visible = true;
shouldHaveFocus = false;
Qt.callLater(() => shouldHaveFocus = Qt.binding(() => shouldBeVisible));
});
}
function close() {
@@ -131,7 +155,7 @@ Item {
Timer {
id: closeTimer
interval: animationDuration + 50
interval: Theme.variantCloseInterval(animationDuration)
onTriggered: {
if (shouldBeVisible)
return;
@@ -145,7 +169,19 @@ Item {
readonly property var shadowLevel: Theme.elevationLevel3
readonly property real shadowFallbackOffset: 6
readonly property real shadowRenderPadding: (root.enableShadow && Theme.elevationEnabled && SettingsData.modalElevationEnabled) ? Theme.elevationRenderPadding(shadowLevel, Theme.elevationLightDirection, shadowFallbackOffset, 8, 16) : 0
readonly property real shadowMotionPadding: animationType === "slide" ? 30 : Math.max(0, animationOffset)
readonly property real shadowMotionPadding: {
if (Theme.isConnectedEffect)
return 0;
if (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode > 0 && Theme.isDirectionalEffect)
return 0; // Wayland native overlap mask
if (animationType === "slide")
return 30;
if (Theme.isDirectionalEffect)
return Math.max(Math.max(0, animationOffset), Math.max(alignedWidth, alignedHeight) * 0.9);
if (Theme.isDepthEffect)
return Math.max(Math.max(0, animationOffset), Math.max(alignedWidth, alignedHeight) * 0.35);
return Math.max(0, animationOffset);
}
readonly property real shadowBuffer: Theme.snap(shadowRenderPadding + shadowMotionPadding, dpr)
readonly property real alignedWidth: Theme.px(modalWidth, dpr)
readonly property real alignedHeight: Theme.px(modalHeight, dpr)
@@ -205,9 +241,26 @@ Item {
MouseArea {
anchors.fill: parent
enabled: root.closeOnBackgroundClick && root.shouldBeVisible
enabled: !root.useSingleWindow && root.closeOnBackgroundClick && root.shouldBeVisible
onClicked: root.backgroundClicked()
}
Rectangle {
anchors.fill: parent
z: -1
color: "black"
opacity: (!root.useSingleWindow && root.useBackground) ? (root.shouldBeVisible ? root.backgroundOpacity : 0) : 0
visible: opacity > 0
Behavior on opacity {
enabled: root.animationsEnabled && (!Theme.isDirectionalEffect || Theme.isConnectedEffect)
NumberAnimation {
duration: Math.round(Theme.variantDuration(root.animationDuration, root.shouldBeVisible) * Theme.variantOpacityDurationScale)
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
}
}
}
}
PanelWindow {
@@ -215,6 +268,17 @@ Item {
visible: false
color: "transparent"
WindowBlur {
targetWindow: contentWindow
blurEnabled: root.effectiveBlurEnabled
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: (root.shouldBeVisible && animatedContent.opacity > 0) ? modalContainer.width * s : 0
blurHeight: (root.shouldBeVisible && animatedContent.opacity > 0) ? modalContainer.height * s : 0
blurRadius: root.effectiveCornerRadius
}
WlrLayershell.namespace: root.layerNamespace
WlrLayershell.layer: {
if (root.useOverlayLayer)
@@ -250,9 +314,12 @@ Item {
bottom: root.useSingleWindow
}
readonly property real actualMarginLeft: root.useSingleWindow ? 0 : Math.max(0, Theme.snap(root.alignedX - shadowBuffer, dpr))
readonly property real actualMarginTop: root.useSingleWindow ? 0 : Math.max(0, Theme.snap(root.alignedY - shadowBuffer, dpr))
WlrLayershell.margins {
left: root.useSingleWindow ? 0 : Math.max(0, Theme.snap(root.alignedX - shadowBuffer, dpr))
top: root.useSingleWindow ? 0 : Math.max(0, Theme.snap(root.alignedY - shadowBuffer, dpr))
left: actualMarginLeft
top: actualMarginTop
right: 0
bottom: 0
}
@@ -282,13 +349,14 @@ Item {
anchors.fill: parent
z: -1
color: "black"
opacity: root.useBackground ? (root.shouldBeVisible ? root.backgroundOpacity : 0) : 0
visible: root.useBackground
opacity: (root.useSingleWindow && root.useBackground) ? (root.shouldBeVisible ? root.backgroundOpacity : 0) : 0
visible: opacity > 0
Behavior on opacity {
enabled: root.animationsEnabled
DankAnim {
duration: root.animationDuration
enabled: root.animationsEnabled && (!Theme.isDirectionalEffect || Theme.isConnectedEffect)
NumberAnimation {
duration: Math.round(Theme.variantDuration(root.animationDuration, root.shouldBeVisible) * Theme.variantOpacityDurationScale)
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
}
}
@@ -296,8 +364,8 @@ Item {
Item {
id: modalContainer
x: root.useSingleWindow ? root.alignedX : shadowBuffer
y: root.useSingleWindow ? root.alignedY : shadowBuffer
x: (root.useSingleWindow ? root.alignedX : (root.alignedX - contentWindow.actualMarginLeft)) + Theme.snap(animX, root.dpr)
y: (root.useSingleWindow ? root.alignedY : (root.alignedY - contentWindow.actualMarginTop)) + Theme.snap(animY, root.dpr)
width: root.alignedWidth
height: root.alignedHeight
@@ -313,45 +381,117 @@ Item {
}
readonly property bool slide: root.animationType === "slide"
readonly property real offsetX: slide ? 15 : 0
readonly property real offsetY: slide ? -30 : root.animationOffset
property real animX: 0
property real animY: 0
property real scaleValue: root.animationScaleCollapsed
onOffsetXChanged: animX = Theme.snap(root.shouldBeVisible ? 0 : offsetX, root.dpr)
onOffsetYChanged: animY = Theme.snap(root.shouldBeVisible ? 0 : offsetY, root.dpr)
Connections {
target: root
function onShouldBeVisibleChanged() {
modalContainer.animX = Theme.snap(root.shouldBeVisible ? 0 : modalContainer.offsetX, root.dpr);
modalContainer.animY = Theme.snap(root.shouldBeVisible ? 0 : modalContainer.offsetY, root.dpr);
modalContainer.scaleValue = root.shouldBeVisible ? 1.0 : root.animationScaleCollapsed;
readonly property bool directionalEffect: Theme.isDirectionalEffect
readonly property bool depthEffect: Theme.isDepthEffect
readonly property real directionalTravel: Math.max(root.animationOffset, Math.max(root.alignedWidth, root.alignedHeight) * 0.8)
readonly property real depthTravel: Math.max(root.animationOffset * 0.8, 36)
readonly property real customAnchorX: root.alignedX + root.alignedWidth * 0.5
readonly property real customAnchorY: root.alignedY + root.alignedHeight * 0.5
readonly property real customDistLeft: customAnchorX
readonly property real customDistRight: root.screenWidth - customAnchorX
readonly property real customDistTop: customAnchorY
readonly property real customDistBottom: root.screenHeight - customAnchorY
readonly property real offsetX: {
if (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 && Theme.isDirectionalEffect)
return 0;
if (slide && !directionalEffect && !depthEffect)
return 15;
if (directionalEffect) {
switch (root.positioning) {
case "top-right":
return 0;
case "custom":
if (customDistLeft <= customDistRight && customDistLeft <= customDistTop && customDistLeft <= customDistBottom)
return -directionalTravel;
if (customDistRight <= customDistTop && customDistRight <= customDistBottom)
return directionalTravel;
return 0;
default:
return 0;
}
}
if (depthEffect) {
switch (root.positioning) {
case "top-right":
return 0;
case "custom":
if (customDistLeft <= customDistRight && customDistLeft <= customDistTop && customDistLeft <= customDistBottom)
return -depthTravel;
if (customDistRight <= customDistTop && customDistRight <= customDistBottom)
return depthTravel;
return 0;
default:
return 0;
}
}
return 0;
}
readonly property real offsetY: {
if (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 && Theme.isDirectionalEffect)
return 0;
if (slide && !directionalEffect && !depthEffect)
return -30;
if (directionalEffect) {
switch (root.positioning) {
case "top-right":
return -Math.max(directionalTravel * 0.65, 96);
case "custom":
if (customDistTop <= customDistBottom && customDistTop <= customDistLeft && customDistTop <= customDistRight)
return -directionalTravel;
if (customDistBottom <= customDistLeft && customDistBottom <= customDistRight)
return directionalTravel;
return 0;
default:
// Default to sliding down from top when centered
return -Math.max(directionalTravel, root.screenHeight * 0.24);
}
}
if (depthEffect) {
switch (root.positioning) {
case "top-right":
return -depthTravel * 0.75;
case "custom":
if (customDistTop <= customDistBottom && customDistTop <= customDistLeft && customDistTop <= customDistRight)
return -depthTravel;
if (customDistBottom <= customDistLeft && customDistBottom <= customDistRight)
return depthTravel;
return depthTravel * 0.45;
default:
return -depthTravel;
}
}
return root.animationOffset;
}
property real animX: root.shouldBeVisible ? 0 : root.frozenMotionOffsetX
property real animY: root.shouldBeVisible ? 0 : root.frozenMotionOffsetY
readonly property real computedScaleCollapsed: (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 && Theme.isDirectionalEffect) ? 0.0 : root.animationScaleCollapsed
property real scaleValue: root.shouldBeVisible ? 1.0 : computedScaleCollapsed
Behavior on animX {
enabled: root.animationsEnabled
DankAnim {
duration: root.animationDuration
NumberAnimation {
duration: Theme.variantDuration(root.animationDuration, root.shouldBeVisible)
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
}
}
Behavior on animY {
enabled: root.animationsEnabled
DankAnim {
duration: root.animationDuration
NumberAnimation {
duration: Theme.variantDuration(root.animationDuration, root.shouldBeVisible)
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
}
}
Behavior on scaleValue {
enabled: root.animationsEnabled
DankAnim {
duration: root.animationDuration
NumberAnimation {
duration: Theme.variantDuration(root.animationDuration, root.shouldBeVisible)
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
}
}
@@ -367,15 +507,14 @@ Item {
id: animatedContent
anchors.fill: parent
clip: false
opacity: root.shouldBeVisible ? 1 : 0
opacity: (Theme.isDirectionalEffect && !Theme.isConnectedEffect) ? 1 : (root.shouldBeVisible ? 1 : 0)
scale: modalContainer.scaleValue
x: Theme.snap(modalContainer.animX, root.dpr) + (parent.width - width) * (1 - modalContainer.scaleValue) * 0.5
y: Theme.snap(modalContainer.animY, root.dpr) + (parent.height - height) * (1 - modalContainer.scaleValue) * 0.5
transformOrigin: Item.Center
Behavior on opacity {
enabled: root.animationsEnabled
enabled: root.animationsEnabled && (!Theme.isDirectionalEffect || Theme.isConnectedEffect)
NumberAnimation {
duration: animationDuration
duration: Math.round(Theme.variantDuration(animationDuration, root.shouldBeVisible) * Theme.variantOpacityDurationScale)
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
}
@@ -386,13 +525,22 @@ Item {
anchors.fill: parent
level: root.shadowLevel
fallbackOffset: root.shadowFallbackOffset
targetRadius: root.cornerRadius
targetColor: root.backgroundColor
borderColor: root.borderColor
borderWidth: root.borderWidth
targetRadius: root.effectiveCornerRadius
targetColor: root.effectiveBackgroundColor
borderColor: root.effectiveBorderColor
borderWidth: root.effectiveBorderWidth
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.effectiveCornerRadius
color: "transparent"
border.color: root.connectedSurfaceOverride ? "transparent" : BlurService.borderColor
border.width: root.connectedSurfaceOverride ? 0 : BlurService.borderWidth
z: 100
}
FocusScope {
anchors.fill: parent
focus: root.shouldBeVisible

View File

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

View File

@@ -1,9 +1,11 @@
import QtQuick
import QtQuick.Effects
import Quickshell
import Quickshell.Wayland
import Quickshell.Hyprland
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
@@ -13,16 +15,24 @@ Item {
property bool spotlightOpen: false
property bool keyboardActive: false
property bool contentVisible: false
readonly property bool launcherMotionVisible: Theme.isDirectionalEffect ? spotlightOpen : _motionActive
property var spotlightContent: launcherContentLoader.item
property bool openedFromOverview: false
property bool isClosing: false
property bool _windowEnabled: true
property bool _pendingInitialize: false
property string _pendingQuery: ""
property string _pendingMode: ""
readonly property bool unloadContentOnClose: SettingsData.dankLauncherV2UnloadOnClose
// Animation state — matches DankPopout/DankModal pattern
property bool animationsEnabled: true
property bool _motionActive: false
property real _frozenMotionX: 0
property real _frozenMotionY: 0
readonly property bool useHyprlandFocusGrab: CompositorService.useHyprlandFocusGrab
readonly property var effectiveScreen: launcherWindow.screen
readonly property var effectiveScreen: contentWindow.screen
readonly property real screenWidth: effectiveScreen?.width ?? 1920
readonly property real screenHeight: effectiveScreen?.height ?? 1080
readonly property real dpr: effectiveScreen ? CompositorService.getScreenScale(effectiveScreen) : 1
@@ -56,8 +66,9 @@ Item {
readonly property real modalX: (screenWidth - modalWidth) / 2
readonly property real modalY: (screenHeight - modalHeight) / 2
readonly property color backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
readonly property real cornerRadius: Theme.cornerRadius
readonly property bool connectedSurfaceOverride: Theme.isConnectedEffect
readonly property color backgroundColor: connectedSurfaceOverride ? Theme.connectedSurfaceColor : Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
readonly property real cornerRadius: connectedSurfaceOverride ? Theme.connectedSurfaceRadius : Theme.cornerRadius
readonly property color borderColor: {
if (!SettingsData.dankLauncherV2BorderEnabled)
return Theme.outlineMedium;
@@ -75,6 +86,37 @@ Item {
}
}
readonly property int borderWidth: SettingsData.dankLauncherV2BorderEnabled ? SettingsData.dankLauncherV2BorderThickness : 0
readonly property color effectiveBorderColor: connectedSurfaceOverride ? "transparent" : borderColor
readonly property int effectiveBorderWidth: connectedSurfaceOverride ? 0 : borderWidth
readonly property bool effectiveBlurEnabled: Theme.connectedSurfaceBlurEnabled
// Shadow padding for the content window (render padding only, no motion padding)
readonly property var shadowLevel: Theme.elevationLevel3
readonly property real shadowFallbackOffset: 6
readonly property real shadowRenderPadding: (Theme.elevationEnabled && SettingsData.modalElevationEnabled) ? Theme.elevationRenderPadding(shadowLevel, Theme.elevationLightDirection, shadowFallbackOffset, 8, 16) : 0
readonly property real shadowPad: Theme.snap(shadowRenderPadding, dpr)
readonly property real alignedWidth: Theme.px(modalWidth, dpr)
readonly property real alignedHeight: Theme.px(modalHeight, dpr)
readonly property real alignedX: Theme.snap(modalX, dpr)
readonly property real alignedY: Theme.snap(modalY, dpr)
// For directional/depth: window extends from screen top (content slides within)
// For standard: small window tightly around the modal + shadow padding
readonly property bool _needsExtendedWindow: (Theme.isDirectionalEffect && !Theme.isConnectedEffect) || Theme.isDepthEffect
// Content window geometry
readonly property real _cwMarginLeft: Theme.snap(alignedX - shadowPad, dpr)
readonly property real _cwMarginTop: _needsExtendedWindow ? 0 : Theme.snap(alignedY - shadowPad, dpr)
readonly property real _cwWidth: alignedWidth + shadowPad * 2
readonly property real _cwHeight: {
if (Theme.isDirectionalEffect && !Theme.isConnectedEffect)
return screenHeight + shadowPad;
if (Theme.isDepthEffect)
return alignedY + alignedHeight + shadowPad;
return alignedHeight + shadowPad * 2;
}
// Where the content container sits inside the content window
readonly property real _ccX: shadowPad
readonly property real _ccY: _needsExtendedWindow ? alignedY : shadowPad
signal dialogClosed
@@ -95,18 +137,11 @@ Item {
if (!spotlightContent)
return;
contentVisible = true;
spotlightContent.searchField.forceActiveFocus();
var targetQuery = "";
if (query) {
targetQuery = query;
} else if (SettingsData.rememberLastQuery) {
targetQuery = SessionData.launcherLastQuery || "";
}
// NOTE: forceActiveFocus() is deliberately NOT called here.
// It is deferred to after animation starts to avoid compositor IPC stalls.
if (spotlightContent.searchField) {
spotlightContent.searchField.text = targetQuery;
spotlightContent.searchField.text = query;
}
if (spotlightContent.controller) {
var targetMode = mode || SessionData.launcherLastMode || "all";
@@ -121,10 +156,12 @@ Item {
spotlightContent.controller.collapsedSections = {};
spotlightContent.controller.selectedFlatIndex = 0;
spotlightContent.controller.selectedItem = null;
spotlightContent.controller.historyIndex = -1;
spotlightContent.controller.searchQuery = targetQuery;
spotlightContent.controller.performSearch();
if (query) {
spotlightContent.controller.setSearchQuery(query);
} else {
spotlightContent.controller.searchQuery = "";
spotlightContent.controller.performSearch();
}
}
if (spotlightContent.resetScroll) {
spotlightContent.resetScroll();
@@ -134,40 +171,59 @@ Item {
}
}
function show() {
function _openCommon(query, mode) {
closeCleanupTimer.stop();
isClosing = false;
openedFromOverview = false;
var focusedScreen = CompositorService.getFocusedScreen();
if (focusedScreen)
launcherWindow.screen = focusedScreen;
// Disable animations so the snap is instant
animationsEnabled = false;
spotlightOpen = true;
keyboardActive = true;
// Freeze the collapsed offsets (they depend on height which could change)
_frozenMotionX = contentContainer ? contentContainer.collapsedMotionX : 0;
_frozenMotionY = contentContainer ? contentContainer.collapsedMotionY : (Theme.isDirectionalEffect ? Math.max(root.screenHeight - root._ccY + root.shadowPad, Theme.effectAnimOffset * 1.1) : -Theme.effectAnimOffset);
var focusedScreen = CompositorService.getFocusedScreen();
if (focusedScreen) {
backgroundWindow.screen = focusedScreen;
contentWindow.screen = focusedScreen;
}
// _motionActive = false ensures motionX/Y snap to frozen collapsed position
_motionActive = false;
// Make windows visible but do NOT request keyboard focus yet
ModalManager.openModal(root);
spotlightOpen = true;
backgroundWindow.visible = true;
contentWindow.visible = true;
if (useHyprlandFocusGrab)
focusGrab.active = true;
_ensureContentLoadedAndInitialize("", "");
// Load content and initialize (but no forceActiveFocus — that's deferred)
_ensureContentLoadedAndInitialize(query || "", mode || "");
// Frame 1: enable animations and trigger enter motion
Qt.callLater(() => {
root.animationsEnabled = true;
root._motionActive = true;
// Frame 2: request keyboard focus + activate search field
// Double-deferred to avoid compositor IPC competing with animation frames
Qt.callLater(() => {
root.keyboardActive = true;
if (root.spotlightContent && root.spotlightContent.searchField)
root.spotlightContent.searchField.forceActiveFocus();
});
});
}
function show() {
_openCommon("", "");
}
function showWithQuery(query) {
closeCleanupTimer.stop();
isClosing = false;
openedFromOverview = false;
var focusedScreen = CompositorService.getFocusedScreen();
if (focusedScreen)
launcherWindow.screen = focusedScreen;
spotlightOpen = true;
keyboardActive = true;
ModalManager.openModal(root);
if (useHyprlandFocusGrab)
focusGrab.active = true;
_ensureContentLoadedAndInitialize(query, "");
_openCommon(query, "");
}
function hide() {
@@ -175,13 +231,17 @@ Item {
return;
openedFromOverview = false;
isClosing = true;
contentVisible = false;
// For directional effects, defer contentVisible=false so content stays rendered during exit slide
if (!Theme.isDirectionalEffect)
contentVisible = false;
// Trigger exit animation — Behaviors will animate motionX/Y to frozen collapsed position
_motionActive = false;
keyboardActive = false;
spotlightOpen = false;
focusGrab.active = false;
ModalManager.closeModal(root);
closeCleanupTimer.start();
}
@@ -190,21 +250,7 @@ Item {
}
function showWithMode(mode) {
closeCleanupTimer.stop();
isClosing = false;
openedFromOverview = false;
var focusedScreen = CompositorService.getFocusedScreen();
if (focusedScreen)
launcherWindow.screen = focusedScreen;
spotlightOpen = true;
keyboardActive = true;
ModalManager.openModal(root);
if (useHyprlandFocusGrab)
focusGrab.active = true;
_ensureContentLoadedAndInitialize("", mode);
_openCommon("", mode);
}
function toggleWithMode(mode) {
@@ -225,10 +271,13 @@ Item {
Timer {
id: closeCleanupTimer
interval: Theme.modalAnimationDuration + 50
interval: Theme.variantCloseInterval(Theme.modalAnimationDuration)
repeat: false
onTriggered: {
isClosing = false;
contentVisible = false;
contentWindow.visible = false;
backgroundWindow.visible = false;
if (root.unloadContentOnClose)
launcherContentLoader.active = false;
dialogClosed();
@@ -237,7 +286,6 @@ Item {
Connections {
target: spotlightContent?.controller ?? null
function onModeChanged(mode) {
if (spotlightContent.controller.autoSwitchedToFiles)
return;
@@ -247,7 +295,7 @@ Item {
HyprlandFocusGrab {
id: focusGrab
windows: [launcherWindow]
windows: [contentWindow]
active: false
onCleared: {
@@ -272,45 +320,53 @@ Item {
if (Quickshell.screens.length === 0)
return;
const screenName = launcherWindow.screen?.name;
if (screenName) {
const screen = contentWindow.screen;
const screenName = screen?.name;
let needsReset = !screen || !screenName;
if (!needsReset) {
needsReset = true;
for (let i = 0; i < Quickshell.screens.length; i++) {
if (Quickshell.screens[i].name === screenName)
return;
if (Quickshell.screens[i].name === screenName) {
needsReset = false;
break;
}
}
}
if (spotlightOpen)
hide();
if (!needsReset)
return;
const newScreen = CompositorService.getFocusedScreen() ?? Quickshell.screens[0];
if (newScreen)
launcherWindow.screen = newScreen;
if (!newScreen)
return;
root._windowEnabled = false;
backgroundWindow.screen = newScreen;
contentWindow.screen = newScreen;
Qt.callLater(() => {
root._windowEnabled = true;
});
}
}
// ── Background window: fullscreen, handles darkening + click-to-dismiss ──
PanelWindow {
id: launcherWindow
visible: spotlightOpen || isClosing
id: backgroundWindow
visible: false
color: "transparent"
exclusionMode: ExclusionMode.Ignore
WlrLayershell.namespace: "dms:spotlight"
WlrLayershell.layer: {
switch (Quickshell.env("DMS_MODAL_LAYER")) {
case "bottom":
console.error("DankModal: 'bottom' layer is not valid for modals. Defaulting to 'top' layer.");
return WlrLayershell.Top;
case "background":
console.error("DankModal: 'background' layer is not valid for modals. Defaulting to 'top' layer.");
return WlrLayershell.Top;
case "overlay":
return WlrLayershell.Overlay;
default:
return WlrLayershell.Top;
}
WlrLayershell.namespace: "dms:spotlight:bg"
WlrLayershell.layer: WlrLayershell.Top
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
WlrLayershell.margins {
top: contentContainer.dockTop ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 0 ? Theme.px(42, root.dpr) : 0)
bottom: contentContainer.dockBottom ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 1 ? Theme.px(42, root.dpr) : 0)
left: contentContainer.dockLeft ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 2 ? Theme.px(42, root.dpr) : 0)
right: contentContainer.dockRight ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 3 ? Theme.px(42, root.dpr) : 0)
}
WlrLayershell.keyboardFocus: keyboardActive ? (root.useHyprlandFocusGrab ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.Exclusive) : WlrKeyboardFocus.None
anchors {
top: true
@@ -320,11 +376,11 @@ Item {
}
mask: Region {
item: spotlightOpen ? fullScreenMask : null
item: (spotlightOpen || isClosing) ? bgFullScreenMask : null
}
Item {
id: fullScreenMask
id: bgFullScreenMask
anchors.fill: parent
}
@@ -332,13 +388,14 @@ Item {
id: backgroundDarken
anchors.fill: parent
color: "black"
opacity: contentVisible && SettingsData.modalDarkenBackground ? 0.5 : 0
visible: contentVisible || opacity > 0
opacity: launcherMotionVisible && SettingsData.modalDarkenBackground ? 0.5 : 0
visible: launcherMotionVisible || opacity > 0
Behavior on opacity {
enabled: root.animationsEnabled && (!Theme.isDirectionalEffect || Theme.isConnectedEffect)
DankAnim {
duration: Theme.modalAnimationDuration
easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
duration: Math.round(Theme.variantDuration(Theme.modalAnimationDuration, launcherMotionVisible) * Theme.variantOpacityDurationScale)
easing.bezierCurve: launcherMotionVisible ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
}
}
}
@@ -346,88 +403,251 @@ Item {
MouseArea {
anchors.fill: parent
enabled: spotlightOpen
onClicked: mouse => {
var contentX = modalContainer.x;
var contentY = modalContainer.y;
var contentW = modalContainer.width;
var contentH = modalContainer.height;
onClicked: root.hide()
}
}
if (mouse.x < contentX || mouse.x > contentX + contentW || mouse.y < contentY || mouse.y > contentY + contentH) {
root.hide();
}
// ── Content window: SMALL, positioned with margins — only renders the modal area ──
PanelWindow {
id: contentWindow
visible: false
color: "transparent"
WindowBlur {
targetWindow: contentWindow
blurEnabled: root.effectiveBlurEnabled
readonly property real s: Math.min(1, contentContainer.scaleValue)
blurX: root._ccX + root.alignedWidth * (1 - s) * 0.5 + Theme.snap(contentContainer.animX, root.dpr)
blurY: root._ccY + root.alignedHeight * (1 - s) * 0.5 + Theme.snap(contentContainer.animY, root.dpr)
blurWidth: (root.spotlightOpen || root.isClosing) && contentWrapper.opacity > 0 ? root.alignedWidth * s : 0
blurHeight: (root.spotlightOpen || root.isClosing) && contentWrapper.opacity > 0 ? root.alignedHeight * s : 0
blurRadius: root.cornerRadius
}
WlrLayershell.namespace: "dms:spotlight"
WlrLayershell.layer: {
switch (Quickshell.env("DMS_MODAL_LAYER")) {
case "bottom":
console.error("DankLauncherV2Modal: 'bottom' layer is not valid for modals. Defaulting to 'top' layer.");
return WlrLayershell.Top;
case "background":
console.error("DankLauncherV2Modal: 'background' layer is not valid for modals. Defaulting to 'top' layer.");
return WlrLayershell.Top;
case "overlay":
return WlrLayershell.Overlay;
default:
return WlrLayershell.Top;
}
}
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: keyboardActive ? (root.useHyprlandFocusGrab ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.Exclusive) : WlrKeyboardFocus.None
anchors {
left: true
top: true
}
WlrLayershell.margins {
left: root._cwMarginLeft
top: root._cwMarginTop
}
implicitWidth: root._cwWidth
implicitHeight: root._cwHeight
mask: Region {
item: contentInputMask
}
Item {
id: modalContainer
x: root.modalX
y: root.modalY
width: root.modalWidth
height: root.modalHeight
visible: contentVisible || opacity > 0
opacity: contentVisible ? 1 : 0
scale: contentVisible ? 1 : 0.96
transformOrigin: Item.Center
Behavior on opacity {
DankAnim {
duration: Theme.modalAnimationDuration
easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
}
}
Behavior on scale {
DankAnim {
duration: Theme.modalAnimationDuration
easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
}
}
ElevationShadow {
id: launcherShadowLayer
anchors.fill: parent
level: Theme.elevationLevel3
fallbackOffset: 6
targetColor: root.backgroundColor
borderColor: root.borderColor
borderWidth: root.borderWidth
targetRadius: root.cornerRadius
shadowEnabled: Theme.elevationEnabled && SettingsData.modalElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1"
}
MouseArea {
anchors.fill: parent
onPressed: mouse => mouse.accepted = true
}
FocusScope {
anchors.fill: parent
focus: keyboardActive
Loader {
id: launcherContentLoader
anchors.fill: parent
active: !root.unloadContentOnClose || root.spotlightOpen || root.isClosing || root.contentVisible || root._pendingInitialize
asynchronous: false
sourceComponent: LauncherContent {
focus: true
parentModal: root
}
onLoaded: {
if (root._pendingInitialize) {
root._initializeAndShow(root._pendingQuery, root._pendingMode);
root._pendingInitialize = false;
}
}
}
Keys.onEscapePressed: event => {
root.hide();
event.accepted = true;
}
}
id: contentInputMask
visible: false
x: contentContainer.x + contentWrapper.x
y: contentContainer.y + contentWrapper.y
width: root.alignedWidth
height: root.alignedHeight
}
}
Item {
id: contentContainer
// For directional/depth: contentContainer is at alignedY from window top (window starts at screen top)
// For standard: contentContainer is at shadowPad from window top (window starts near modal)
x: root._ccX
y: root._ccY
width: root.alignedWidth
height: root.alignedHeight
readonly property int dockEdge: typeof SettingsData !== "undefined" ? SettingsData.dockPosition : 1
readonly property bool dockTop: dockEdge === 0
readonly property bool dockBottom: dockEdge === 1
readonly property bool dockLeft: dockEdge === 2
readonly property bool dockRight: dockEdge === 3
readonly property real dockThickness: typeof SettingsData !== "undefined" && SettingsData.showDock ? Theme.px(SettingsData.dockIconSize + (SettingsData.dockMargin * 2) + SettingsData.dockSpacing + 8, root.dpr) : Theme.px(60, root.dpr)
readonly property bool directionalEffect: Theme.isDirectionalEffect
readonly property bool depthEffect: Theme.isDepthEffect
readonly property real collapsedMotionX: {
if (directionalEffect) {
if (dockLeft)
return -(root._ccX + root.alignedWidth + Theme.effectAnimOffset);
if (dockRight)
return root.screenWidth - root._ccX + Theme.effectAnimOffset;
}
if (depthEffect)
return Theme.effectAnimOffset * 0.25;
return 0;
}
readonly property real collapsedMotionY: {
if (directionalEffect) {
if (dockTop)
return -(root._ccY + root.alignedHeight + Theme.effectAnimOffset);
if (dockBottom)
return root.screenHeight - root._ccY + root.shadowPad + Theme.effectAnimOffset;
return 0;
}
if (depthEffect)
return -Math.max(Theme.effectAnimOffset * 0.85, 34);
return -Math.max((root.shadowPad || 0) + Theme.effectAnimOffset, 40);
}
// animX/animY are Behavior-animated — DankPopout pattern
property real animX: 0
property real animY: 0
property real scaleValue: Theme.isDirectionalEffect && typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 ? Theme.effectScaleCollapsed : (Theme.isDirectionalEffect ? 1 : Theme.effectScaleCollapsed)
Component.onCompleted: {
animX = Theme.snap(root._motionActive ? 0 : collapsedMotionX, root.dpr);
animY = Theme.snap(root._motionActive ? 0 : collapsedMotionY, root.dpr);
scaleValue = root._motionActive ? 1.0 : (Theme.isDirectionalEffect && typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 ? Theme.effectScaleCollapsed : (Theme.isDirectionalEffect ? 1 : Theme.effectScaleCollapsed));
}
Connections {
target: root
function on_MotionActiveChanged() {
contentContainer.animX = Theme.snap(root._motionActive ? 0 : root._frozenMotionX, root.dpr);
contentContainer.animY = Theme.snap(root._motionActive ? 0 : root._frozenMotionY, root.dpr);
contentContainer.scaleValue = root._motionActive ? 1.0 : (Theme.isDirectionalEffect && typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 ? Theme.effectScaleCollapsed : (Theme.isDirectionalEffect ? 1 : Theme.effectScaleCollapsed));
}
}
Behavior on animX {
enabled: root.animationsEnabled
DankAnim {
duration: Theme.variantDuration(Theme.modalAnimationDuration, root._motionActive)
easing.bezierCurve: root._motionActive ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
}
}
Behavior on animY {
enabled: root.animationsEnabled
DankAnim {
duration: Theme.variantDuration(Theme.modalAnimationDuration, root._motionActive)
easing.bezierCurve: root._motionActive ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
}
}
Behavior on scaleValue {
enabled: root.animationsEnabled && (!Theme.isDirectionalEffect || (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2))
DankAnim {
duration: Theme.variantDuration(Theme.modalAnimationDuration, root._motionActive)
easing.bezierCurve: root._motionActive ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
}
}
Item {
id: directionalClipMask
readonly property bool shouldClip: Theme.isDirectionalEffect && typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode > 0
readonly property real clipOversize: 2000
clip: shouldClip
x: shouldClip ? (contentContainer.dockRight ? -clipOversize : (contentContainer.dockLeft ? contentContainer.dockThickness - root._ccX : -clipOversize)) : 0
y: shouldClip ? (contentContainer.dockBottom ? -clipOversize : (contentContainer.dockTop ? contentContainer.dockThickness - root._ccY : -clipOversize)) : 0
width: shouldClip ? parent.width + clipOversize + (contentContainer.dockRight ? (root.screenWidth - contentContainer.dockThickness - root._ccX - parent.width) : (contentContainer.dockLeft ? clipOversize : clipOversize)) : parent.width
height: shouldClip ? parent.height + clipOversize + (contentContainer.dockBottom ? (root.screenHeight - contentContainer.dockThickness - root._ccY - parent.height) : (contentContainer.dockTop ? clipOversize : clipOversize)) : parent.height
Item {
id: aligner
x: directionalClipMask.x !== 0 ? -directionalClipMask.x : 0
y: directionalClipMask.y !== 0 ? -directionalClipMask.y : 0
width: contentContainer.width
height: contentContainer.height
// Shadow mirrors contentWrapper position/scale/opacity
ElevationShadow {
id: launcherShadowLayer
width: parent.width
height: parent.height
opacity: contentWrapper.opacity
scale: contentWrapper.scale
x: contentWrapper.x
y: contentWrapper.y
level: root.shadowLevel
fallbackOffset: root.shadowFallbackOffset
targetColor: root.backgroundColor
borderColor: root.effectiveBorderColor
borderWidth: root.effectiveBorderWidth
targetRadius: root.cornerRadius
shadowEnabled: Theme.elevationEnabled && SettingsData.modalElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1"
}
// contentWrapper moves inside static contentContainer — DankPopout pattern
Item {
id: contentWrapper
width: parent.width
height: parent.height
opacity: (Theme.isDirectionalEffect && !Theme.isConnectedEffect) ? 1 : (launcherMotionVisible ? 1 : 0)
visible: opacity > 0
scale: contentContainer.scaleValue
x: Theme.snap(contentContainer.animX + (parent.width - width) * (1 - contentContainer.scaleValue) * 0.5, root.dpr)
y: Theme.snap(contentContainer.animY + (parent.height - height) * (1 - contentContainer.scaleValue) * 0.5, root.dpr)
Behavior on opacity {
enabled: root.animationsEnabled && (!Theme.isDirectionalEffect || Theme.isConnectedEffect)
DankAnim {
duration: Math.round(Theme.variantDuration(Theme.modalAnimationDuration, launcherMotionVisible) * Theme.variantOpacityDurationScale)
easing.bezierCurve: launcherMotionVisible ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
}
}
MouseArea {
anchors.fill: parent
onPressed: mouse => mouse.accepted = true
}
FocusScope {
anchors.fill: parent
focus: keyboardActive
Loader {
id: launcherContentLoader
anchors.fill: parent
active: !root.unloadContentOnClose || root.spotlightOpen || root.isClosing || root.contentVisible || root._pendingInitialize
asynchronous: false
sourceComponent: LauncherContent {
focus: true
parentModal: root
}
onLoaded: {
if (root._pendingInitialize) {
root._initializeAndShow(root._pendingQuery, root._pendingMode);
root._pendingInitialize = false;
}
}
}
Keys.onEscapePressed: event => {
root.hide();
event.accepted = true;
}
}
} // contentWrapper
} // aligner
} // directionalClipMask
} // contentContainer
} // PanelWindow
}

View File

@@ -41,7 +41,6 @@ FocusScope {
editCommentField.text = existing?.comment || "";
editEnvVarsField.text = existing?.envVars || "";
editExtraFlagsField.text = existing?.extraFlags || "";
editDgpuToggle.checked = existing?.launchOnDgpu || false;
editMode = true;
Qt.callLater(() => editNameField.forceActiveFocus());
}
@@ -65,8 +64,6 @@ FocusScope {
override.envVars = editEnvVarsField.text.trim();
if (editExtraFlagsField.text.trim())
override.extraFlags = editExtraFlagsField.text.trim();
if (editDgpuToggle.checked)
override.launchOnDgpu = true;
SessionData.setAppOverride(editAppId, override);
closeEditMode();
}
@@ -89,7 +86,7 @@ FocusScope {
Controller {
id: controller
active: root.parentModal?.spotlightOpen ?? true
active: root.parentModal ? (root.parentModal.spotlightOpen || root.parentModal.isClosing) : true
viewModeContext: root.viewModeContext
onItemExecuted: {
@@ -149,18 +146,10 @@ FocusScope {
event.accepted = false;
return;
case Qt.Key_Down:
if (hasCtrl) {
controller.navigateHistory("down");
} else {
controller.selectNext();
}
controller.selectNext();
return;
case Qt.Key_Up:
if (hasCtrl) {
controller.navigateHistory("up");
} else {
controller.selectPrevious();
}
controller.selectPrevious();
return;
case Qt.Key_PageDown:
controller.selectPageDown(8);
@@ -169,10 +158,6 @@ FocusScope {
controller.selectPageUp(8);
return;
case Qt.Key_Right:
if (hasCtrl) {
controller.cycleMode();
return;
}
if (controller.getCurrentSectionViewMode() !== "list") {
controller.selectRight();
return;
@@ -180,25 +165,12 @@ FocusScope {
event.accepted = false;
return;
case Qt.Key_Left:
if (hasCtrl) {
const reverse = true;
controller.cycleMode(reverse);
return;
}
if (controller.getCurrentSectionViewMode() !== "list") {
controller.selectLeft();
return;
}
event.accepted = false;
return;
case Qt.Key_H:
if (hasCtrl) {
const reverse = true;
controller.cycleMode(reverse);
return;
}
event.accepted = false;
return;
case Qt.Key_J:
if (hasCtrl) {
controller.selectNext();
@@ -213,13 +185,6 @@ FocusScope {
}
event.accepted = false;
return;
case Qt.Key_L:
if (hasCtrl) {
controller.cycleMode();
return;
}
event.accepted = false;
return;
case Qt.Key_N:
if (hasCtrl) {
controller.selectNextSection();
@@ -235,19 +200,13 @@ FocusScope {
event.accepted = false;
return;
case Qt.Key_Tab:
if (hasCtrl && actionPanel.hasActions) {
if (actionPanel.hasActions) {
actionPanel.expanded ? actionPanel.cycleAction() : actionPanel.show();
return;
}
controller.selectNext();
return;
case Qt.Key_Backtab:
if (hasCtrl && actionPanel.expanded) {
const reverse = true;
actionPanel.expanded ? actionPanel.cycleAction(reverse) : actionPanel.show();
return;
}
controller.selectPrevious();
if (actionPanel.expanded)
actionPanel.hide();
return;
case Qt.Key_Return:
case Qt.Key_Enter:
@@ -429,7 +388,7 @@ FocusScope {
StyledText {
anchors.verticalCenter: parent.verticalCenter
text: "Ctrl-Tab " + I18n.tr("actions")
text: "Tab " + I18n.tr("actions")
font.pixelSize: Theme.fontSizeSmall - 1
color: Theme.surfaceVariantText
visible: actionPanel.hasActions
@@ -503,7 +462,7 @@ FocusScope {
showClearButton: true
textColor: Theme.surfaceText
font.pixelSize: Theme.fontSizeLarge
enabled: root.parentModal ? root.parentModal.spotlightOpen : true
enabled: root.parentModal ? (root.parentModal.spotlightOpen || root.parentModal.isClosing) : true
placeholderText: ""
ignoreUpDownKeys: true
ignoreTabKeys: true
@@ -737,7 +696,13 @@ FocusScope {
Item {
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)
opacity: root.parentModal?.isClosing ? 0 : 1
opacity: {
if (!root.parentModal)
return 1;
if (Theme.isDirectionalEffect && root.parentModal.isClosing)
return 1;
return root.parentModal.isClosing ? 0 : 1;
}
ResultsList {
id: resultsList
@@ -771,7 +736,6 @@ FocusScope {
}
function onSearchQueryRequested(query) {
searchField.text = query;
searchField.cursorPosition = query.length;
}
function onModeChanged() {
extFilterField.text = "";
@@ -982,15 +946,6 @@ FocusScope {
keyNavigationBacktab: editEnvVarsField
}
}
DankToggle {
id: editDgpuToggle
width: parent.width
text: I18n.tr("Launch on dGPU by default")
visible: SessionService.nvidiaCommand.length > 0
checked: false
onToggled: checked => editDgpuToggle.checked = checked
}
}
}

View File

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

View File

@@ -8,9 +8,6 @@ DankPopout {
layerNamespace: "dms:app-launcher"
readonly property real screenWidth: screen?.width ?? 1920
readonly property real screenHeight: screen?.height ?? 1080
property string _pendingMode: ""
property string _pendingQuery: ""
@@ -44,35 +41,8 @@ DankPopout {
openWithQuery(query);
}
readonly property int _baseWidth: {
switch (SettingsData.dankLauncherV2Size) {
case "micro":
return 500;
case "medium":
return 720;
case "large":
return 860;
default:
return 620;
}
}
readonly property int _baseHeight: {
switch (SettingsData.dankLauncherV2Size) {
case "micro":
return 480;
case "medium":
return 720;
case "large":
return 860;
default:
return 600;
}
}
popupWidth: Math.min(_baseWidth, screenWidth - 100)
popupHeight: Math.min(_baseHeight, screenHeight - 100)
popupWidth: 560
popupHeight: 640
triggerWidth: 40
positioning: ""
contentHandlesKeys: contentLoader.item?.launcherContent?.editMode ?? false
@@ -90,7 +60,7 @@ DankPopout {
if (!lc)
return;
const query = _pendingQuery || (SettingsData.rememberLastQuery ? SessionData.launcherLastQuery : "") || "";
const query = _pendingQuery;
const mode = _pendingMode || SessionData.appDrawerLastMode || "apps";
_pendingMode = "";
_pendingQuery = "";
@@ -102,9 +72,12 @@ DankPopout {
if (lc.controller) {
lc.controller.searchMode = mode;
lc.controller.pluginFilter = "";
lc.controller.searchQuery = query;
lc.controller.performSearch();
lc.controller.searchQuery = "";
if (query) {
lc.controller.setSearchQuery(query);
} else {
lc.controller.performSearch();
}
}
lc.resetScroll?.();
lc.actionPanel?.hide();
@@ -133,7 +106,7 @@ DankPopout {
QtObject {
id: modalAdapter
property bool spotlightOpen: appDrawerPopout.shouldBeVisible
property bool isClosing: false
property bool isClosing: appDrawerPopout.isClosing
function hide() {
appDrawerPopout.close();

View File

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

View File

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

View File

@@ -136,9 +136,11 @@ DankPopout {
z: 5000
Behavior on opacity {
enabled: !Theme.isDirectionalEffect
NumberAnimation {
duration: 200
easing.type: Easing.OutCubic
duration: Theme.shortDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
}
}
}

View File

@@ -207,9 +207,9 @@ Rectangle {
width: parent.width
height: 50
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)
border.color: modelData === AudioService.source ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
border.width: 0
color: deviceMouseArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceLight
border.color: modelData === AudioService.source ? Theme.primary : Theme.outlineLight
border.width: modelData === AudioService.source ? 2 : 1
Row {
anchors.left: parent.left

View File

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

View File

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

View File

@@ -153,7 +153,7 @@ Item {
width: 320
height: contentColumn.implicitHeight + Theme.spacingL * 2
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.width: 0
opacity: modalVisible ? 1 : 0

View File

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

View File

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

View File

@@ -308,9 +308,9 @@ Rectangle {
width: parent.width
height: wiredContentRow.implicitHeight + Theme.spacingM * 2
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)
border.color: Theme.primary
border.width: 0
color: wiredNetworkMouseArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceLight
border.color: isActive ? Theme.primary : Theme.outlineLight
border.width: isActive ? 2 : 1
Row {
id: wiredContentRow
@@ -565,9 +565,9 @@ Rectangle {
width: wifiContent.width
height: wifiContentRow.implicitHeight + Theme.spacingM * 2
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)
border.color: wifiDelegate.isConnected ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
border.width: 0
color: networkMouseArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceLight
border.color: wifiDelegate.isConnected ? Theme.primary : Theme.outlineLight
border.width: wifiDelegate.isConnected ? 2 : 1
Row {
id: wifiContentRow

View File

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

View File

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

View File

@@ -14,6 +14,8 @@ Item {
required property var rootWindow
required property var barConfig
readonly property var blurBarWindow: barWindow
property var leftWidgetsModel
property var centerWidgetsModel
property var rightWidgetsModel
@@ -21,6 +23,31 @@ Item {
readonly property real innerPadding: barConfig?.innerPadding ?? 4
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 hCenterSection: hCenterSection
property alias hRightSection: hRightSection
@@ -29,10 +56,14 @@ Item {
property alias vRightSection: vRightSection
anchors.fill: parent
anchors.leftMargin: Math.max(Theme.spacingXS, innerPadding * 0.8)
anchors.rightMargin: Math.max(Theme.spacingXS, innerPadding * 0.8)
anchors.topMargin: barWindow.isVertical ? (barWindow.hasAdjacentTopBar ? outlineThickness : Theme.spacingXS) : 0
anchors.bottomMargin: barWindow.isVertical ? (barWindow.hasAdjacentBottomBar ? outlineThickness : Theme.spacingXS) : 0
anchors.leftMargin: Math.max(Theme.spacingXS, innerPadding * 0.8) + _frameLeftInset
anchors.rightMargin: Math.max(Theme.spacingXS, innerPadding * 0.8) + _frameRightInset
anchors.topMargin: (barWindow.isVertical
? (barWindow.hasAdjacentTopBar ? outlineThickness : Theme.spacingXS)
: 0) + _frameTopInset
anchors.bottomMargin: (barWindow.isVertical
? (barWindow.hasAdjacentBottomBar ? outlineThickness : Theme.spacingXS)
: 0) + _frameBottomInset
clip: false
property int componentMapRevision: 0
@@ -408,6 +439,12 @@ Item {
value: topBarContent.barConfig
restoreMode: Binding.RestoreNone
}
Binding {
target: hLeftSection
property: "blurBarWindow"
value: topBarContent.blurBarWindow
restoreMode: Binding.RestoreNone
}
RightSection {
id: hRightSection
@@ -434,6 +471,12 @@ Item {
value: topBarContent.barConfig
restoreMode: Binding.RestoreNone
}
Binding {
target: hRightSection
property: "blurBarWindow"
value: topBarContent.blurBarWindow
restoreMode: Binding.RestoreNone
}
CenterSection {
id: hCenterSection
@@ -460,6 +503,12 @@ Item {
value: topBarContent.barConfig
restoreMode: Binding.RestoreNone
}
Binding {
target: hCenterSection
property: "blurBarWindow"
value: topBarContent.blurBarWindow
restoreMode: Binding.RestoreNone
}
}
Item {
@@ -493,6 +542,12 @@ Item {
value: topBarContent.barConfig
restoreMode: Binding.RestoreNone
}
Binding {
target: vLeftSection
property: "blurBarWindow"
value: topBarContent.blurBarWindow
restoreMode: Binding.RestoreNone
}
CenterSection {
id: vCenterSection
@@ -520,6 +575,12 @@ Item {
value: topBarContent.barConfig
restoreMode: Binding.RestoreNone
}
Binding {
target: vCenterSection
property: "blurBarWindow"
value: topBarContent.blurBarWindow
restoreMode: Binding.RestoreNone
}
RightSection {
id: vRightSection
@@ -548,6 +609,12 @@ Item {
value: topBarContent.barConfig
restoreMode: Binding.RestoreNone
}
Binding {
target: vRightSection
property: "blurBarWindow"
value: topBarContent.blurBarWindow
restoreMode: Binding.RestoreNone
}
}
}
@@ -1117,6 +1184,7 @@ Item {
if (!notificationCenterLoader.item) {
return;
}
notificationCenterLoader.item.triggerScreen = barWindow.screen;
const effectiveBarConfig = topBarContent.barConfig;
const barPosition = barWindow.axis?.edge === "left" ? 2 : (barWindow.axis?.edge === "right" ? 3 : (barWindow.axis?.edge === "top" ? 0 : 1));
if (notificationCenterLoader.item.setBarContext) {

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.namespace: "dms:bar"
@@ -132,7 +248,9 @@ PanelWindow {
readonly property color _surfaceContainer: Theme.surfaceContainer
readonly property string _barId: barConfig?.id ?? "default"
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() {
const live = SettingsData.barConfigs.find(c => c.id === _barId);
@@ -278,7 +396,7 @@ PanelWindow {
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 {
enabled: barWindow.visible
@@ -289,7 +407,12 @@ PanelWindow {
}
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 bool hasAdjacentTopBar: {
@@ -538,14 +661,14 @@ PanelWindow {
anchors.left: !isVertical ? true : (barPos === SettingsData.Position.Left)
anchors.right: !isVertical ? true : (barPos === SettingsData.Position.Right)
exclusiveZone: (!(barConfig?.visible ?? true) || topBarCore.autoHide) ? -1 : (barWindow.effectiveBarThickness + effectiveSpacing + (barConfig?.bottomGap ?? 0))
exclusiveZone: (!(barConfig?.visible ?? true) || topBarCore.autoHide) ? -1 : (barWindow.effectiveBarThickness + effectiveSpacing + (Theme.isConnectedEffect ? 0 : (barConfig?.bottomGap ?? 0)))
Item {
id: inputMask
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 showing: effectiveVisible && (topBarCore.reveal || inOverviewWithShow || !topBarCore.autoHide)
@@ -686,7 +809,7 @@ PanelWindow {
}
property bool reveal: {
const inOverviewWithShow = CompositorService.isNiri && NiriService.inOverview && (barConfig?.openOnOverview ?? false);
const inOverviewWithShow = CompositorService.isNiri && NiriService.inOverview && barWindow.effectiveOpenOnOverview;
if (inOverviewWithShow)
return true;
@@ -711,7 +834,8 @@ PanelWindow {
onHasActivePopoutChanged: evaluateReveal()
function updateActivePopoutState() {
if (!barWindow.screen) return;
if (!barWindow.screen)
return;
const screenName = barWindow.screen.name;
const activePopout = PopoutManager.currentPopoutsByScreen[screenName];
const activeTrayMenu = TrayMenuManager.activeTrayMenus[screenName];
@@ -782,7 +906,7 @@ PanelWindow {
top: barWindow.isVertical ? parent.top : 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
acceptedButtons: Qt.NoButton
enabled: (barConfig?.autoHide ?? false) && !inOverview

View File

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

View File

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

View File

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

View File

@@ -630,7 +630,7 @@ BasePill {
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.widgetBaseHoverColor : "transparent";
return mouseArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent";
}
border.width: dragHandler.dragging ? 2 : 0

View File

@@ -3,6 +3,7 @@ import Quickshell
import Quickshell.Wayland
import qs.Common
import qs.Modules.Plugins
import qs.Services
import qs.Widgets
BasePill {
@@ -93,6 +94,15 @@ BasePill {
PanelWindow {
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"
property bool isVertical: false
@@ -187,8 +197,8 @@ BasePill {
height: Math.max(64, menuColumn.implicitHeight + Theme.spacingS * 2)
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
radius: Theme.cornerRadius
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
border.color: BlurService.enabled ? BlurService.borderColor : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: BlurService.enabled ? BlurService.borderWidth : 1
opacity: contextMenuWindow.visible ? 1 : 0
visible: opacity > 0
@@ -224,7 +234,7 @@ BasePill {
width: parent.width
height: 30
radius: Theme.cornerRadius
color: clearAllArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent"
color: clearAllArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
Row {
anchors.fill: parent
@@ -264,7 +274,7 @@ BasePill {
width: parent.width
height: 30
radius: Theme.cornerRadius
color: savedItemsArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent"
color: savedItemsArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
Row {
anchors.fill: parent

View File

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

View File

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

View File

@@ -273,7 +273,7 @@ BasePill {
if (isFocused) {
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
@@ -528,7 +528,7 @@ BasePill {
if (isFocused) {
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 {
@@ -738,6 +738,15 @@ BasePill {
sourceComponent: PanelWindow {
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 bool isVisible: false
property point anchorPos: Qt.point(0, 0)
@@ -830,6 +839,7 @@ BasePill {
}
Rectangle {
id: contextMenuRect
x: {
if (contextMenuWindow.isVertical) {
if (contextMenuWindow.edge === "left") {
@@ -858,13 +868,13 @@ BasePill {
height: 32
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
radius: Theme.cornerRadius
border.width: 1
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
border.width: BlurService.enabled ? BlurService.borderWidth : 1
border.color: BlurService.enabled ? BlurService.borderColor : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
Rectangle {
anchors.fill: parent
radius: parent.radius
color: closeMouseArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent"
color: closeMouseArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
}
StyledText {

View File

@@ -287,7 +287,7 @@ BasePill {
height: root.trayItemSize
anchors.centerIn: parent
radius: Theme.cornerRadius
color: trayItemArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent"
color: trayItemArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
border.width: dragHandler.dragging ? 2 : 0
border.color: Theme.primary
opacity: dragHandler.dragging ? 0.8 : 1.0
@@ -425,7 +425,7 @@ BasePill {
height: root.trayItemSize
anchors.centerIn: parent
radius: Theme.cornerRadius
color: caretArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent"
color: caretArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
DankIcon {
anchors.centerIn: parent
@@ -547,7 +547,7 @@ BasePill {
height: root.trayItemSize
anchors.centerIn: parent
radius: Theme.cornerRadius
color: trayItemArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent"
color: trayItemArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
border.width: dragHandler.dragging ? 2 : 0
border.color: Theme.primary
opacity: dragHandler.dragging ? 0.8 : 1.0
@@ -685,7 +685,7 @@ BasePill {
height: root.trayItemSize
anchors.centerIn: parent
radius: Theme.cornerRadius
color: caretAreaVert.containsMouse ? Theme.widgetBaseHoverColor : "transparent"
color: caretAreaVert.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
DankIcon {
anchors.centerIn: parent
@@ -723,6 +723,16 @@ BasePill {
PanelWindow {
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
screen: root.parentScreen
WlrLayershell.layer: WlrLayershell.Top
@@ -990,6 +1000,15 @@ BasePill {
layer.samples: 4
}
Rectangle {
anchors.fill: parent
color: "transparent"
radius: Theme.cornerRadius
border.color: BlurService.borderColor
border.width: BlurService.borderWidth
z: 100
}
Grid {
id: menuGrid
anchors.centerIn: parent
@@ -1030,7 +1049,7 @@ BasePill {
width: root.trayItemSize + 4
height: root.trayItemSize + 4
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 {
id: menuIconImg
@@ -1191,6 +1210,15 @@ BasePill {
PanelWindow {
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"
visible: menuRoot.showMenu && (menuRoot.trayItem?.hasMenu ?? false)
WlrLayershell.layer: WlrLayershell.Top
@@ -1302,7 +1330,7 @@ BasePill {
onClicked: mouse => {
const clickX = mouse.x + menuWindow.maskX;
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)
return;
@@ -1360,7 +1388,7 @@ BasePill {
}
Item {
id: menuContainer
id: trayMenuContainer
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)
@@ -1438,6 +1466,15 @@ BasePill {
layer.textureMirroring: ShaderEffectSource.MirrorVertically
}
Rectangle {
anchors.fill: parent
color: "transparent"
radius: Theme.cornerRadius
border.color: BlurService.borderColor
border.width: BlurService.borderWidth
z: 100
}
QsMenuAnchor {
id: submenuHydrator
anchor.window: menuWindow
@@ -1470,7 +1507,7 @@ BasePill {
width: parent.width
height: 28
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 {
anchors.left: parent.left
@@ -1523,7 +1560,7 @@ BasePill {
width: parent.width
height: 28
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 {
anchors.left: parent.left
@@ -1574,7 +1611,7 @@ BasePill {
color: {
if (menuEntry?.isSeparator)
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 {

View File

@@ -17,8 +17,49 @@ Item {
property real widgetHeight: 30
property real barThickness: 48
property var barConfig: null
property var blurBarWindow: null
property var hyprlandOverviewLoader: 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
readonly property var sortedToplevels: {
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) {
if (useExtWorkspace) {
const realWorkspaces = getRealWorkspaces();
@@ -751,8 +846,15 @@ Item {
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.RightButton
id: edgeMouseArea
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 mouseAccumulator: 0
@@ -765,12 +867,20 @@ Item {
}
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) {
NiriService.toggleOverview();
} else if (CompositorService.isHyprland && root.hyprlandOverviewLoader?.item) {
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")) {
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

@@ -16,7 +16,6 @@ DankPopout {
popupHeight: contentLoader.item ? contentLoader.item.implicitHeight : 500
triggerWidth: 80
screen: triggerScreen
shouldBeVisible: dashVisible
property bool __focusArmed: false
property bool __contentReady: false

View File

@@ -44,6 +44,43 @@ Item {
property int __volumeHoverCount: 0
readonly property bool directionalEffect: Theme.isDirectionalEffect
readonly property bool depthEffect: Theme.isDepthEffect
function panelMotionX(panelWidth, active) {
if (active)
return 0;
if (directionalEffect) {
const travel = Math.max(Theme.effectAnimOffset, panelWidth * 0.85);
return isRightEdge ? -travel : travel;
}
if (depthEffect) {
const travel = Math.max(Theme.effectAnimOffset * 0.7, panelWidth * 0.32);
return isRightEdge ? -travel : travel;
}
return 0;
}
function panelMotionY(panelType, panelHeight, active) {
if (active)
return 0;
if (directionalEffect) {
if (panelType === 2)
return panelHeight * 0.08;
if (panelType === 3)
return -panelHeight * 0.08;
return 0;
}
if (depthEffect) {
if (panelType === 2)
return panelHeight * 0.04;
if (panelType === 3)
return -panelHeight * 0.04;
return 0;
}
return 0;
}
function volumeAreaEntered() {
__volumeHoverCount++;
panelEntered();
@@ -62,30 +99,47 @@ Item {
visible: dropdownType === 1 && volumeAvailable
width: 60
height: 180
x: isRightEdge ? anchorPos.x : anchorPos.x - width
y: anchorPos.y - height / 2
x: (isRightEdge ? anchorPos.x : anchorPos.x - width) + panelMotionX(width, dropdownType === 1)
y: anchorPos.y - height / 2 + panelMotionY(1, height, dropdownType === 1)
radius: Theme.cornerRadius * 2
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
border.width: 1
opacity: dropdownType === 1 ? 1 : 0
scale: dropdownType === 1 ? 1 : 0.96
opacity: Theme.isDirectionalEffect ? 1 : (dropdownType === 1 ? 1 : 0)
scale: Theme.isDirectionalEffect ? 1 : (dropdownType === 1 ? 1 : Theme.effectScaleCollapsed)
transformOrigin: isRightEdge ? Item.Left : Item.Right
Behavior on opacity {
NumberAnimation {
duration: Theme.expressiveDurations.expressiveDefaultSpatial
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial
enabled: !Theme.isDirectionalEffect
DankAnim {
duration: Math.round(Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 1) * Theme.variantOpacityDurationScale)
easing.bezierCurve: dropdownType === 1 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
}
}
Behavior on scale {
enabled: !Theme.isDirectionalEffect
NumberAnimation {
duration: Theme.expressiveDurations.expressiveDefaultSpatial
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 1)
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial
easing.bezierCurve: dropdownType === 1 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
}
}
Behavior on x {
NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 1)
easing.type: Easing.BezierSpline
easing.bezierCurve: dropdownType === 1 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
}
}
Behavior on y {
NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 1)
easing.type: Easing.BezierSpline
easing.bezierCurve: dropdownType === 1 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
}
}
@@ -197,33 +251,50 @@ Item {
Rectangle {
id: audioDevicesPanel
visible: dropdownType === 2
visible: dropdownType === 2 && activePlayer !== null
width: 280
height: Math.max(200, Math.min(280, availableDevices.length * 50 + 100))
x: isRightEdge ? anchorPos.x : anchorPos.x - width
y: anchorPos.y - height / 2
x: (isRightEdge ? anchorPos.x : anchorPos.x - width) + panelMotionX(width, dropdownType === 2)
y: anchorPos.y - height / 2 + panelMotionY(2, height, dropdownType === 2)
radius: Theme.cornerRadius * 2
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.98)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.6)
border.width: 2
opacity: dropdownType === 2 ? 1 : 0
scale: dropdownType === 2 ? 1 : 0.96
opacity: Theme.isDirectionalEffect ? 1 : (dropdownType === 2 ? 1 : 0)
scale: Theme.isDirectionalEffect ? 1 : (dropdownType === 2 ? 1 : Theme.effectScaleCollapsed)
transformOrigin: isRightEdge ? Item.Left : Item.Right
Behavior on opacity {
NumberAnimation {
duration: Theme.expressiveDurations.expressiveDefaultSpatial
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial
enabled: !Theme.isDirectionalEffect
DankAnim {
duration: Math.round(Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 2) * Theme.variantOpacityDurationScale)
easing.bezierCurve: dropdownType === 2 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
}
}
Behavior on scale {
enabled: !Theme.isDirectionalEffect
NumberAnimation {
duration: Theme.expressiveDurations.expressiveDefaultSpatial
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 2)
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial
easing.bezierCurve: dropdownType === 2 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
}
}
Behavior on x {
NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 2)
easing.type: Easing.BezierSpline
easing.bezierCurve: dropdownType === 2 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
}
}
Behavior on y {
NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 2)
easing.type: Easing.BezierSpline
easing.bezierCurve: dropdownType === 2 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
}
}
@@ -354,30 +425,47 @@ Item {
visible: dropdownType === 3
width: 240
height: Math.max(180, Math.min(240, (allPlayers?.length || 0) * 50 + 80))
x: isRightEdge ? anchorPos.x : anchorPos.x - width
y: anchorPos.y - height / 2
x: (isRightEdge ? anchorPos.x : anchorPos.x - width) + panelMotionX(width, dropdownType === 3)
y: anchorPos.y - height / 2 + panelMotionY(3, height, dropdownType === 3)
radius: Theme.cornerRadius * 2
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.98)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.6)
border.width: 2
opacity: dropdownType === 3 ? 1 : 0
scale: dropdownType === 3 ? 1 : 0.96
opacity: Theme.isDirectionalEffect ? 1 : (dropdownType === 3 ? 1 : 0)
scale: Theme.isDirectionalEffect ? 1 : (dropdownType === 3 ? 1 : Theme.effectScaleCollapsed)
transformOrigin: isRightEdge ? Item.Left : Item.Right
Behavior on opacity {
NumberAnimation {
duration: Theme.expressiveDurations.expressiveDefaultSpatial
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial
enabled: !Theme.isDirectionalEffect
DankAnim {
duration: Math.round(Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 3) * Theme.variantOpacityDurationScale)
easing.bezierCurve: dropdownType === 3 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
}
}
Behavior on scale {
enabled: !Theme.isDirectionalEffect
NumberAnimation {
duration: Theme.expressiveDurations.expressiveDefaultSpatial
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 3)
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial
easing.bezierCurve: dropdownType === 3 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
}
}
Behavior on x {
NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 3)
easing.type: Easing.BezierSpline
easing.bezierCurve: dropdownType === 3 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
}
}
Behavior on y {
NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 3)
easing.type: Easing.BezierSpline
easing.bezierCurve: dropdownType === 3 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
}
}

View File

@@ -17,6 +17,16 @@ Variants {
delegate: PanelWindow {
id: dock
WindowBlur {
targetWindow: dock
blurEnabled: dock.effectiveBlurEnabled
blurX: dockBackground.x + dockContainer.x + dockMouseArea.x + dockCore.x + dockSlide.x - dock.horizontalConnectorExtent
blurY: dockBackground.y + dockContainer.y + dockMouseArea.y + dockCore.y + dockSlide.y - dock.verticalConnectorExtent
blurWidth: dock.hasApps && dock.reveal ? dockBackground.width + dock.horizontalConnectorExtent * 2 : 0
blurHeight: dock.hasApps && dock.reveal ? dockBackground.height + dock.verticalConnectorExtent * 2 : 0
blurRadius: dock.surfaceRadius
}
WlrLayershell.namespace: "dms:dock"
readonly property bool isVertical: SettingsData.dockPosition === SettingsData.Position.Left || SettingsData.dockPosition === SettingsData.Position.Right
@@ -33,6 +43,29 @@ Variants {
property real backgroundTransparency: SettingsData.dockTransparency
property bool groupByApp: SettingsData.dockGroupByApp
readonly property int borderThickness: SettingsData.dockBorderEnabled ? SettingsData.dockBorderThickness : 0
readonly property string connectedBarSide: SettingsData.dockPosition === SettingsData.Position.Top ? "top"
: SettingsData.dockPosition === SettingsData.Position.Bottom ? "bottom"
: SettingsData.dockPosition === SettingsData.Position.Left ? "left" : "right"
readonly property bool connectedBarActiveOnEdge: Theme.isConnectedEffect
&& !!(dock.screen || modelData)
&& SettingsData.getActiveBarEdgesForScreen(dock.screen || modelData).includes(connectedBarSide)
readonly property real connectedJoinInset: {
if (!Theme.isConnectedEffect)
return 0;
return connectedBarActiveOnEdge ? SettingsData.frameBarSize : SettingsData.frameThickness;
}
readonly property real surfaceRadius: Theme.connectedSurfaceRadius
readonly property color surfaceColor: Theme.isConnectedEffect
? Theme.connectedSurfaceColor
: Theme.withAlpha(Theme.surfaceContainer, backgroundTransparency)
readonly property color surfaceBorderColor: Theme.isConnectedEffect ? "transparent" : BlurService.borderColor
readonly property real surfaceBorderWidth: Theme.isConnectedEffect ? 0 : BlurService.borderWidth
readonly property real surfaceTopLeftRadius: Theme.isConnectedEffect && (SettingsData.dockPosition === SettingsData.Position.Top || SettingsData.dockPosition === SettingsData.Position.Left) ? 0 : surfaceRadius
readonly property real surfaceTopRightRadius: Theme.isConnectedEffect && (SettingsData.dockPosition === SettingsData.Position.Top || SettingsData.dockPosition === SettingsData.Position.Right) ? 0 : surfaceRadius
readonly property real surfaceBottomLeftRadius: Theme.isConnectedEffect && (SettingsData.dockPosition === SettingsData.Position.Bottom || SettingsData.dockPosition === SettingsData.Position.Left) ? 0 : surfaceRadius
readonly property real surfaceBottomRightRadius: Theme.isConnectedEffect && (SettingsData.dockPosition === SettingsData.Position.Bottom || SettingsData.dockPosition === SettingsData.Position.Right) ? 0 : surfaceRadius
readonly property real horizontalConnectorExtent: Theme.isConnectedEffect && !isVertical ? Theme.connectedCornerRadius : 0
readonly property real verticalConnectorExtent: Theme.isConnectedEffect && isVertical ? Theme.connectedCornerRadius : 0
readonly property int hasApps: dockApps.implicitWidth > 0 || dockApps.implicitHeight > 0
@@ -104,13 +137,57 @@ Variants {
return getBarHeight(leftBar);
}
readonly property real dockMargin: SettingsData.dockSpacing
readonly property real positionSpacing: barSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin
readonly property real dockMargin: SettingsData.dockMargin
readonly property bool effectiveBlurEnabled: Theme.connectedSurfaceBlurEnabled
readonly property real effectiveDockBottomGap: Theme.isConnectedEffect ? 0 : SettingsData.dockBottomGap
readonly property real effectiveDockMargin: Theme.isConnectedEffect ? 0 : SettingsData.dockMargin
readonly property real positionSpacing: barSpacing + effectiveDockBottomGap + effectiveDockMargin
readonly property real joinedEdgeMargin: Theme.isConnectedEffect ? 0 : (barSpacing + effectiveDockMargin + 1 + dock.borderThickness)
readonly property real _dpr: (dock.screen && dock.screen.devicePixelRatio) ? dock.screen.devicePixelRatio : 1
function px(v) {
return Math.round(v * _dpr) / _dpr;
}
function connectorWidth(spacing) {
return dock.isVertical ? (spacing + Theme.connectedCornerRadius) : Theme.connectedCornerRadius;
}
function connectorHeight(spacing) {
return dock.isVertical ? Theme.connectedCornerRadius : (spacing + Theme.connectedCornerRadius);
}
function connectorSeamX(baseX, bodyWidth, placement) {
if (!dock.isVertical)
return placement === "left" ? baseX : baseX + bodyWidth;
return SettingsData.dockPosition === SettingsData.Position.Left ? baseX : baseX + bodyWidth;
}
function connectorSeamY(baseY, bodyHeight, placement) {
if (SettingsData.dockPosition === SettingsData.Position.Top)
return baseY;
if (SettingsData.dockPosition === SettingsData.Position.Bottom)
return baseY + bodyHeight;
return placement === "left" ? baseY : baseY + bodyHeight;
}
function connectorX(baseX, bodyWidth, placement, spacing) {
const seamX = connectorSeamX(baseX, bodyWidth, placement);
const width = connectorWidth(spacing);
if (!dock.isVertical)
return placement === "left" ? seamX - width : seamX;
return SettingsData.dockPosition === SettingsData.Position.Left ? seamX : seamX - width;
}
function connectorY(baseY, bodyHeight, placement, spacing) {
const seamY = connectorSeamY(baseY, bodyHeight, placement);
const height = connectorHeight(spacing);
if (SettingsData.dockPosition === SettingsData.Position.Top)
return seamY;
if (SettingsData.dockPosition === SettingsData.Position.Bottom)
return seamY - height;
return placement === "left" ? seamY - height : seamY;
}
property bool contextMenuOpen: (dockVariants.contextMenu && dockVariants.contextMenu.visible && dockVariants.contextMenu.screen === modelData)
property bool revealSticky: false
@@ -121,7 +198,7 @@ Variants {
return false;
const screenName = dock.modelData?.name ?? "";
const dockThickness = effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin;
const dockThickness = dock.connectedJoinInset + effectiveBarHeight + SettingsData.dockSpacing + dock.effectiveDockBottomGap + dock.effectiveDockMargin;
const screenWidth = dock.screen?.width ?? 0;
const screenHeight = dock.screen?.height ?? 0;
@@ -293,13 +370,13 @@ Variants {
return -1;
if (barSpacing > 0)
return -1;
return px(effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin);
return px(connectedJoinInset + effectiveBarHeight + SettingsData.dockSpacing + effectiveDockBottomGap + effectiveDockMargin);
}
property real animationHeadroom: Math.ceil(SettingsData.dockIconSize * 0.35)
implicitWidth: isVertical ? (px(effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockMargin + SettingsData.dockIconSize * 0.3) + animationHeadroom) : 0
implicitHeight: !isVertical ? (px(effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockMargin + SettingsData.dockIconSize * 0.3) + animationHeadroom) : 0
implicitWidth: isVertical ? (px(connectedJoinInset + effectiveBarHeight + SettingsData.dockSpacing + effectiveDockMargin + SettingsData.dockIconSize * 0.3) + animationHeadroom) : 0
implicitHeight: !isVertical ? (px(connectedJoinInset + effectiveBarHeight + SettingsData.dockSpacing + effectiveDockMargin + SettingsData.dockIconSize * 0.3) + animationHeadroom) : 0
Item {
id: maskItem
@@ -309,17 +386,17 @@ Variants {
x: {
const baseX = dockCore.x + dockMouseArea.x;
if (isVertical && SettingsData.dockPosition === SettingsData.Position.Right)
return baseX - (expanded ? animationHeadroom + borderThickness : 0);
return baseX - (expanded ? borderThickness : 0);
return baseX - (expanded ? animationHeadroom + borderThickness + dock.horizontalConnectorExtent : 0);
return baseX - (expanded ? borderThickness + dock.horizontalConnectorExtent : 0);
}
y: {
const baseY = dockCore.y + dockMouseArea.y;
if (!isVertical && SettingsData.dockPosition === SettingsData.Position.Bottom)
return baseY - (expanded ? animationHeadroom + borderThickness : 0);
return baseY - (expanded ? borderThickness : 0);
return baseY - (expanded ? animationHeadroom + borderThickness + dock.verticalConnectorExtent : 0);
return baseY - (expanded ? borderThickness + dock.verticalConnectorExtent : 0);
}
width: dockMouseArea.width + (isVertical && expanded ? animationHeadroom : 0) + (expanded ? borderThickness * 2 : 0)
height: dockMouseArea.height + (!isVertical && expanded ? animationHeadroom : 0) + (expanded ? borderThickness * 2 : 0)
width: dockMouseArea.width + (isVertical && expanded ? animationHeadroom : 0) + (expanded ? borderThickness * 2 + dock.horizontalConnectorExtent * 2 : 0)
height: dockMouseArea.height + (!isVertical && expanded ? animationHeadroom : 0) + (expanded ? borderThickness * 2 + dock.verticalConnectorExtent * 2 : 0)
}
mask: Region {
@@ -379,7 +456,7 @@ Variants {
const screenHeight = dock.screen ? dock.screen.height : 0;
const gap = Theme.spacingS;
const bgMargin = barSpacing + SettingsData.dockMargin + 1 + dock.borderThickness;
const bgMargin = dock.joinedEdgeMargin + dock.connectedJoinInset;
const btnW = dock.hoveredButton.width;
const btnH = dock.hoveredButton.height;
@@ -450,11 +527,11 @@ Variants {
// Keep the taller hit area regardless of the reveal state to prevent shrinking loop
return Math.min(Math.max(dockBackground.height + 64, 200), maxDockHeight);
}
return dock.reveal ? px(dock.effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin) : 1;
return dock.reveal ? px(dock.connectedJoinInset + dock.effectiveBarHeight + SettingsData.dockSpacing + dock.effectiveDockBottomGap + dock.effectiveDockMargin) : 1;
}
width: {
if (dock.isVertical) {
return dock.reveal ? px(dock.effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin) : 1;
return dock.reveal ? px(dock.connectedJoinInset + dock.effectiveBarHeight + SettingsData.dockSpacing + dock.effectiveDockBottomGap + dock.effectiveDockMargin) : 1;
}
// Keep the wider hit area regardless of the reveal state to prevent shrinking loop
return Math.min(dockBackground.width + 8 + dock.borderThickness, maxDockWidth);
@@ -496,7 +573,7 @@ Variants {
return 0;
if (dock.reveal)
return 0;
const hideDistance = dock.effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin + 10;
const hideDistance = dock.connectedJoinInset + dock.effectiveBarHeight + SettingsData.dockSpacing + dock.effectiveDockBottomGap + dock.effectiveDockMargin + 10;
if (SettingsData.dockPosition === SettingsData.Position.Right) {
return hideDistance;
} else {
@@ -508,7 +585,7 @@ Variants {
return 0;
if (dock.reveal)
return 0;
const hideDistance = dock.effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin + 10;
const hideDistance = dock.connectedJoinInset + dock.effectiveBarHeight + SettingsData.dockSpacing + dock.effectiveDockBottomGap + dock.effectiveDockMargin + 10;
if (SettingsData.dockPosition === SettingsData.Position.Bottom) {
return hideDistance;
} else {
@@ -519,16 +596,26 @@ Variants {
Behavior on x {
NumberAnimation {
id: slideXAnimation
duration: Theme.shortDuration
easing.type: Easing.OutCubic
duration: Theme.isConnectedEffect
? Theme.variantDuration(Theme.popoutAnimationDuration, dock.reveal)
: Theme.shortDuration
easing.type: Theme.isConnectedEffect ? Easing.BezierSpline : Easing.OutCubic
easing.bezierCurve: Theme.isConnectedEffect
? (dock.reveal ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve)
: []
}
}
Behavior on y {
NumberAnimation {
id: slideYAnimation
duration: Theme.shortDuration
easing.type: Easing.OutCubic
duration: Theme.isConnectedEffect
? Theme.variantDuration(Theme.popoutAnimationDuration, dock.reveal)
: Theme.shortDuration
easing.type: Theme.isConnectedEffect ? Easing.BezierSpline : Easing.OutCubic
easing.bezierCurve: Theme.isConnectedEffect
? (dock.reveal ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve)
: []
}
}
}
@@ -544,24 +631,62 @@ Variants {
right: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Right ? parent.right : undefined) : undefined
verticalCenter: dock.isVertical ? parent.verticalCenter : undefined
}
anchors.topMargin: !dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Top ? barSpacing + SettingsData.dockMargin + 1 + dock.borderThickness : 0
anchors.bottomMargin: !dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Bottom ? barSpacing + SettingsData.dockMargin + 1 + dock.borderThickness : 0
anchors.leftMargin: dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Left ? barSpacing + SettingsData.dockMargin + 1 + dock.borderThickness : 0
anchors.rightMargin: dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Right ? barSpacing + SettingsData.dockMargin + 1 + dock.borderThickness : 0
anchors.topMargin: !dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Top ? (dock.connectedJoinInset + dock.joinedEdgeMargin) : 0
anchors.bottomMargin: !dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Bottom ? (dock.connectedJoinInset + dock.joinedEdgeMargin) : 0
anchors.leftMargin: dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Left ? (dock.connectedJoinInset + dock.joinedEdgeMargin) : 0
anchors.rightMargin: dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Right ? (dock.connectedJoinInset + dock.joinedEdgeMargin) : 0
implicitWidth: dock.isVertical ? (dockApps.implicitHeight + SettingsData.dockSpacing * 2) : (dockApps.implicitWidth + SettingsData.dockSpacing * 2)
implicitHeight: dock.isVertical ? (dockApps.implicitWidth + SettingsData.dockSpacing * 2) : (dockApps.implicitHeight + SettingsData.dockSpacing * 2)
width: implicitWidth
height: implicitHeight
layer.enabled: true
// Avoid an offscreen texture seam where the connected dock meets the frame.
layer.enabled: !Theme.isConnectedEffect
clip: false
Rectangle {
anchors.fill: parent
color: Theme.withAlpha(Theme.surfaceContainer, backgroundTransparency)
radius: Theme.cornerRadius
color: dock.surfaceColor
topLeftRadius: dock.surfaceTopLeftRadius
topRightRadius: dock.surfaceTopRightRadius
bottomLeftRadius: dock.surfaceBottomLeftRadius
bottomRightRadius: dock.surfaceBottomRightRadius
}
Rectangle {
anchors.fill: parent
color: "transparent"
topLeftRadius: dock.surfaceTopLeftRadius
topRightRadius: dock.surfaceTopRightRadius
bottomLeftRadius: dock.surfaceBottomLeftRadius
bottomRightRadius: dock.surfaceBottomRightRadius
border.color: dock.surfaceBorderColor
border.width: dock.surfaceBorderWidth
z: 100
}
}
ConnectedCorner {
visible: Theme.isConnectedEffect && dock.reveal
barSide: dock.connectedBarSide
placement: "left"
spacing: 0
connectorRadius: Theme.connectedCornerRadius
color: dock.surfaceColor
x: Theme.snap(dock.connectorX(dockBackground.x, dockBackground.width, placement, spacing), dock._dpr)
y: Theme.snap(dock.connectorY(dockBackground.y, dockBackground.height, placement, spacing), dock._dpr)
}
ConnectedCorner {
visible: Theme.isConnectedEffect && dock.reveal
barSide: dock.connectedBarSide
placement: "right"
spacing: 0
connectorRadius: Theme.connectedCornerRadius
color: dock.surfaceColor
x: Theme.snap(dock.connectorX(dockBackground.x, dockBackground.width, placement, spacing), dock._dpr)
y: Theme.snap(dock.connectorY(dockBackground.y, dockBackground.height, placement, spacing), dock._dpr)
}
Shape {
@@ -570,12 +695,12 @@ Variants {
y: dockBackground.y - borderThickness
width: dockBackground.width + borderThickness * 2
height: dockBackground.height + borderThickness * 2
visible: SettingsData.dockBorderEnabled && dock.hasApps
visible: SettingsData.dockBorderEnabled && dock.hasApps && !Theme.isConnectedEffect
preferredRendererType: Shape.CurveRenderer
readonly property real borderThickness: Math.max(1, dock.borderThickness)
readonly property real i: borderThickness / 2
readonly property real cr: Theme.cornerRadius
readonly property real cr: dock.surfaceRadius
readonly property real w: dockBackground.width
readonly property real h: dockBackground.height

View File

@@ -9,6 +9,15 @@ import qs.Widgets
PanelWindow {
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"
property var appData: null
@@ -168,8 +177,8 @@ PanelWindow {
height: menuColumn.implicitHeight + Theme.spacingS * 2
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
radius: Theme.cornerRadius
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
border.color: BlurService.enabled ? BlurService.borderColor : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: BlurService.enabled ? BlurService.borderWidth : 1
opacity: root.visible ? 1 : 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

@@ -39,11 +39,9 @@ DankPopout {
}
}
popupWidth: triggerScreen ? Math.min(500, Math.max(380, triggerScreen.width - 48)) : 400
popupWidth: 400
popupHeight: stablePopupHeight
positioning: ""
animationScaleCollapsed: 0.94
animationOffset: 0
suspendShadowWhileResizing: false
screen: triggerScreen

View File

@@ -1,6 +1,5 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import Quickshell
import Quickshell.Wayland
import Quickshell.Services.Notifications
@@ -11,6 +10,15 @@ import qs.Widgets
PanelWindow {
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"
required property var notificationData
@@ -24,6 +32,29 @@ PanelWindow {
property real _lastReportedAlignedHeight: -1
property real _storedTopMargin: 0
property real _storedBottomMargin: 0
readonly property bool directionalEffect: Theme.isDirectionalEffect
readonly property bool depthEffect: Theme.isDepthEffect
readonly property real entryTravel: {
const base = Math.abs(Theme.effectAnimOffset);
if (directionalEffect) {
if (isCenterPosition)
return Math.max(base, Math.round(content.height * 1.1));
return Math.max(base, Math.round(content.width * 0.95));
}
if (depthEffect)
return Math.max(base, 44);
return base;
}
readonly property real exitTravel: {
if (directionalEffect) {
if (isCenterPosition)
return content.height + entryTravel;
return content.width + entryTravel;
}
if (depthEffect)
return Math.round(entryTravel * 1.35);
return Anims.slidePx;
}
readonly property string clearText: I18n.tr("Dismiss")
property bool descriptionExpanded: false
readonly property bool hasExpandableBody: (notificationData?.htmlBody || "").replace(/<[^>]*>/g, "").trim().length > 0
@@ -137,9 +168,9 @@ PanelWindow {
enabled: !exiting && !_isDestroying
NumberAnimation {
id: implicitHeightAnim
duration: descriptionExpanded ? Theme.notificationExpandDuration : Theme.notificationCollapseDuration
duration: Theme.variantDuration(descriptionExpanded ? Theme.notificationExpandDuration : Theme.notificationCollapseDuration, descriptionExpanded)
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.emphasized
easing.bezierCurve: descriptionExpanded ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
}
}
@@ -436,6 +467,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 {
id: backgroundContainer
anchors.fill: parent
@@ -911,9 +952,9 @@ PanelWindow {
if (isCenterPosition)
return 0;
const isLeft = SettingsData.notificationPopupPosition === SettingsData.Position.Left || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom;
return isLeft ? -Anims.slidePx : Anims.slidePx;
return isLeft ? -entryTravel : entryTravel;
}
y: isTopCenter ? -Anims.slidePx : isBottomCenter ? Anims.slidePx : 0
y: isTopCenter ? -entryTravel : isBottomCenter ? entryTravel : 0
}
]
}
@@ -925,16 +966,16 @@ PanelWindow {
property: isCenterPosition ? "y" : "x"
from: {
if (isTopCenter)
return -Anims.slidePx;
return -entryTravel;
if (isBottomCenter)
return Anims.slidePx;
return entryTravel;
const isLeft = SettingsData.notificationPopupPosition === SettingsData.Position.Left || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom;
return isLeft ? -Anims.slidePx : Anims.slidePx;
return isLeft ? -entryTravel : entryTravel;
}
to: 0
duration: Theme.notificationEnterDuration
duration: Theme.variantDuration(Theme.notificationEnterDuration, true)
easing.type: Easing.BezierSpline
easing.bezierCurve: isCenterPosition ? Theme.expressiveCurves.standardDecel : Theme.expressiveCurves.emphasizedDecel
easing.bezierCurve: Theme.variantPopoutEnterCurve
onStopped: {
if (!win.exiting && !win._isDestroying) {
if (isCenterPosition) {
@@ -959,35 +1000,35 @@ PanelWindow {
from: 0
to: {
if (isTopCenter)
return -Anims.slidePx;
return -exitTravel;
if (isBottomCenter)
return Anims.slidePx;
return exitTravel;
const isLeft = SettingsData.notificationPopupPosition === SettingsData.Position.Left || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom;
return isLeft ? -Anims.slidePx : Anims.slidePx;
return isLeft ? -exitTravel : exitTravel;
}
duration: Theme.notificationExitDuration
duration: Theme.variantDuration(Theme.notificationExitDuration, false)
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.emphasizedAccel
easing.bezierCurve: Theme.variantPopoutExitCurve
}
NumberAnimation {
target: content
property: "opacity"
from: 1
to: 0
duration: Theme.notificationExitDuration
to: Theme.isDirectionalEffect ? 1 : 0
duration: Theme.variantDuration(Theme.notificationExitDuration, false)
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.standardAccel
easing.bezierCurve: Theme.variantPopoutExitCurve
}
NumberAnimation {
target: content
property: "scale"
from: 1
to: 0.98
duration: Theme.notificationExitDuration
to: Theme.isDirectionalEffect ? 1 : Theme.effectScaleCollapsed
duration: Theme.variantDuration(Theme.notificationExitDuration, false)
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.emphasizedAccel
easing.bezierCurve: Theme.variantPopoutExitCurve
}
}

View File

@@ -14,6 +14,7 @@ Item {
property real barThickness: 48
property real barSpacing: 4
property var barConfig: null
property var blurBarWindow: null
property alias content: contentLoader.sourceComponent
property bool isVerticalOrientation: axis?.isVertical ?? false
property bool isFirst: false
@@ -106,7 +107,7 @@ Item {
const rawTransparency = (root.barConfig && root.barConfig.widgetTransparency !== undefined) ? root.barConfig.widgetTransparency : 1.0;
const isHovered = root.enableBackgroundHover && (mouseArea.containsMouse || (root.isHovered || false));
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) {
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * transparency);
@@ -169,4 +170,26 @@ Item {
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 barSpacing: 4
property var barConfig: null
property var blurBarWindow: null
property string pluginId: ""
property var pluginService: null
@@ -182,6 +183,7 @@ Item {
barThickness: root.barThickness
barSpacing: root.barSpacing
barConfig: root.barConfig
blurBarWindow: root.blurBarWindow
content: root.horizontalBarPill
states: State {
@@ -241,6 +243,7 @@ Item {
barThickness: root.barThickness
barSpacing: root.barSpacing
barConfig: root.barConfig
blurBarWindow: root.blurBarWindow
content: root.verticalBarPill
isVerticalOrientation: true

View File

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

View File

@@ -27,6 +27,7 @@ Item {
const pos = selectedBarConfig?.position ?? SettingsData.Position.Top;
return pos === SettingsData.Position.Left || pos === SettingsData.Position.Right;
}
readonly property bool connectedFrameModeActive: SettingsData.connectedFrameModeActive
Timer {
id: horizontalBarChangeDebounce
@@ -693,6 +694,8 @@ Item {
SettingsToggleRow {
visible: CompositorService.isNiri
enabled: !SettingsData.frameEnabled
opacity: SettingsData.frameEnabled ? 0.5 : 1.0
text: I18n.tr("Show on Overview")
checked: selectedBarConfig?.openOnOverview ?? false
onToggled: toggled => {
@@ -798,11 +801,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 {
iconName: "space_bar"
title: I18n.tr("Spacing")
settingKey: "barSpacing"
visible: selectedBarConfig?.enabled
enabled: !SettingsData.frameEnabled
opacity: SettingsData.frameEnabled ? 0.5 : 1.0
SettingsSliderRow {
id: edgeSpacingSlider
@@ -1003,6 +1037,8 @@ Item {
SettingsSliderRow {
id: barTransparencySlider
enabled: !SettingsData.frameEnabled
opacity: SettingsData.frameEnabled ? 0.5 : 1.0
text: I18n.tr("Bar Transparency")
value: (selectedBarConfig?.transparency ?? 1.0) * 100
minimum: 0
@@ -1044,6 +1080,64 @@ Item {
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
}
}
}
}
Item {
visible: dankBarTab.connectedFrameModeActive
width: parent.width
implicitHeight: connectedFrameStyleNote.implicitHeight + Theme.spacingS * 2
Row {
id: connectedFrameStyleNote
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("Connected Frame mode keeps bar shadow override, border, and corner overrides off while active")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width - Theme.fontSizeMedium - Theme.spacingS
}
}
}
SettingsCard {
@@ -1054,6 +1148,8 @@ Item {
collapsible: true
expanded: true
visible: selectedBarConfig?.enabled
enabled: !dankBarTab.connectedFrameModeActive
opacity: dankBarTab.connectedFrameModeActive ? 0.5 : 1.0
readonly property bool shadowActive: (selectedBarConfig?.shadowIntensity ?? 0) > 0
readonly property bool isCustomColor: (selectedBarConfig?.shadowColorMode ?? "default") === "custom"
@@ -1287,6 +1383,8 @@ Item {
SettingsToggleRow {
text: I18n.tr("Square Corners")
enabled: !SettingsData.frameEnabled
opacity: SettingsData.frameEnabled ? 0.5 : 1.0
checked: selectedBarConfig?.squareCorners ?? false
onToggled: checked => SettingsData.updateBarConfig(selectedBarId, {
squareCorners: checked
@@ -1334,6 +1432,8 @@ Item {
SettingsToggleRow {
text: I18n.tr("Goth Corners")
enabled: !SettingsData.frameEnabled
opacity: SettingsData.frameEnabled ? 0.5 : 1.0
checked: selectedBarConfig?.gothCornersEnabled ?? false
onToggled: checked => SettingsData.updateBarConfig(selectedBarId, {
gothCornersEnabled: checked
@@ -1383,6 +1483,8 @@ Item {
iconName: "border_style"
title: I18n.tr("Border")
visible: selectedBarConfig?.enabled
enabled: !dankBarTab.connectedFrameModeActive
opacity: dankBarTab.connectedFrameModeActive ? 0.5 : 1.0
checked: selectedBarConfig?.borderEnabled ?? false
onToggled: checked => SettingsData.updateBarConfig(selectedBarId, {
borderEnabled: checked

View File

@@ -7,6 +7,9 @@ import qs.Modules.Settings.Widgets
Item {
id: root
readonly property bool connectedFrameModeActive: SettingsData.frameEnabled
&& SettingsData.motionEffect === 1
&& SettingsData.directionalAnimationMode === 3
FileBrowserModal {
id: dockLogoFileBrowser
@@ -544,6 +547,8 @@ Item {
SettingsSliderRow {
text: I18n.tr("Exclusive Zone Offset")
enabled: !root.connectedFrameModeActive
opacity: root.connectedFrameModeActive ? 0.5 : 1.0
value: SettingsData.dockBottomGap
minimum: -100
maximum: 100
@@ -553,6 +558,8 @@ Item {
SettingsSliderRow {
text: I18n.tr("Margin")
enabled: !root.connectedFrameModeActive
opacity: root.connectedFrameModeActive ? 0.5 : 1.0
value: SettingsData.dockMargin
minimum: 0
maximum: 100
@@ -561,11 +568,42 @@ Item {
}
}
Item {
visible: root.connectedFrameModeActive
width: parent.width
implicitHeight: dockConnectedNote.implicitHeight + Theme.spacingS * 2
Row {
id: dockConnectedNote
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("Connected Frame mode manages dock edge offset, transparency, blur, and border styling")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width - Theme.fontSizeMedium - Theme.spacingS
}
}
}
SettingsCard {
width: parent.width
iconName: "opacity"
title: I18n.tr("Transparency")
settingKey: "dockTransparency"
enabled: !root.connectedFrameModeActive
opacity: root.connectedFrameModeActive ? 0.5 : 1.0
SettingsSliderRow {
text: I18n.tr("Dock Transparency")
@@ -585,6 +623,8 @@ Item {
settingKey: "dockBorder"
collapsible: true
expanded: false
enabled: !root.connectedFrameModeActive
opacity: root.connectedFrameModeActive ? 0.5 : 1.0
SettingsToggleRow {
text: I18n.tr("Border")

View File

@@ -0,0 +1,321 @@
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", "surface", "popup", "opacity", "transparency"]
text: I18n.tr("Surface Opacity")
description: I18n.tr("Frame border opacity. Controls all surface opacity globally when Connected Mode is active")
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: true
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)
}
SettingsToggleRow {
visible: SettingsData.frameEnabled
settingKey: "directionalAnimationMode"
tags: ["frame", "connected", "popout", "corner", "animation"]
text: I18n.tr("Connected Mode")
description: I18n.tr("Popouts emerge flush from the bar edge as one continuous piece (based on Slide)")
checked: SettingsData.connectedFrameModeActive
onToggled: checked => {
if (checked) {
if (SettingsData.directionalAnimationMode !== 3)
SettingsData.set("previousDirectionalMode", SettingsData.directionalAnimationMode);
SettingsData.set("motionEffect", 1);
SettingsData.set("directionalAnimationMode", 3);
} else {
SettingsData.set("directionalAnimationMode", SettingsData.previousDirectionalMode);
}
}
Connections {
target: SettingsData
function onDirectionalAnimationModeChanged() {}
function onMotionEffectChanged() {}
}
}
}
// ── 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

@@ -11,6 +11,9 @@ import qs.Modules.Settings.Widgets
Item {
id: themeColorsTab
readonly property bool connectedFrameModeActive: SettingsData.frameEnabled
&& SettingsData.motionEffect === 1
&& SettingsData.directionalAnimationMode === 3
property var cachedIconThemes: SettingsData.availableIconThemes
property var cachedCursorThemes: SettingsData.availableCursorThemes
property var cachedMatugenSchemes: Theme.availableMatugenSchemes.map(option => option.label)
@@ -125,6 +128,15 @@ Item {
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() {
PopoutService.colorPickerModal.selectedColor = SettingsData.m3ElevationCustomColor ?? "#000000";
PopoutService.colorPickerModal.pickerTitle = I18n.tr("Shadow Color");
@@ -1606,10 +1618,14 @@ Item {
SettingsSliderRow {
tab: "theme"
tags: ["popup", "transparency", "opacity", "modal"]
tags: ["surface", "popup", "transparency", "opacity", "modal"]
settingKey: "popupTransparency"
text: I18n.tr("Popup Transparency")
description: I18n.tr("Controls opacity of all popouts, modals, and their content layers")
text: I18n.tr("Surface Opacity")
description: themeColorsTab.connectedFrameModeActive
? I18n.tr("Connected Frame mode follows Surface Opacity from the Frame tab for connected popouts, docks, and modal surfaces")
: I18n.tr("Controls opacity of all popouts, modals, and their content layers")
enabled: !themeColorsTab.connectedFrameModeActive
opacity: themeColorsTab.connectedFrameModeActive ? 0.5 : 1.0
value: Math.round(SettingsData.popupTransparency * 100)
minimum: 0
maximum: 100
@@ -1816,6 +1832,81 @@ 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("Requires a newer version of Quickshell")
: (themeColorsTab.connectedFrameModeActive
? I18n.tr("Connected Frame mode follows Frame Blur for connected surfaces while this remains the master blur availability toggle")
: I18n.tr("Blur the background behind bars, popouts, modals, and notifications. Requires compositor support and configuration."))
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 {
tab: "theme"
tags: ["niri", "layout", "gaps", "radius", "window", "border"]
@@ -2602,7 +2693,6 @@ Item {
onToggled: checked => SettingsData.set("matugenTemplateNeovim", checked)
}
SettingsDropdownRow {
text: I18n.tr("Dark mode base")
tab: "theme"

View File

@@ -55,6 +55,192 @@ Item {
anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingXL
SettingsCard {
tab: "typography"
tags: ["animation", "variant", "style", "slide", "fluent", "dynamic", "motion"]
title: I18n.tr("Animation Style")
settingKey: "animationVariant"
iconName: "auto_awesome_motion"
Item {
width: parent.width
height: animVariantGroup.implicitHeight
clip: true
DankButtonGroup {
id: animVariantGroup
anchors.horizontalCenter: parent.horizontalCenter
buttonPadding: parent.width < 480 ? Theme.spacingS : Theme.spacingL
minButtonWidth: parent.width < 480 ? 64 : 96
textSize: parent.width < 480 ? Theme.fontSizeSmall : Theme.fontSizeMedium
model: [I18n.tr("Material"), I18n.tr("Fluent"), I18n.tr("Dynamic")]
selectionMode: "single"
currentIndex: SettingsData.animationVariant
onSelectionChanged: (index, selected) => {
if (!selected)
return;
SettingsData.set("animationVariant", index);
}
Connections {
target: SettingsData
function onAnimationVariantChanged() {
animVariantGroup.currentIndex = SettingsData.animationVariant;
}
}
}
}
Rectangle {
width: parent.width
height: 1
color: Theme.outline
opacity: 0.15
}
Item {
width: parent.width
height: variantDescription.implicitHeight + Theme.spacingS * 2
StyledText {
id: variantDescription
x: Theme.spacingM
y: Theme.spacingS
width: parent.width - Theme.spacingM * 2
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
text: {
switch (SettingsData.animationVariant) {
case 1:
return I18n.tr("Fluent: Smooth cubic deceleration in, quick snap out — clean, elegant curves.");
case 2:
return I18n.tr("Dynamic: Spring bezier with overshoot — entry briefly exceeds its target then settles. Expressive and alive.");
default:
return I18n.tr("Material: Material Design 3 Expressive bezier curves. The DMS default feel.");
}
}
}
}
}
SettingsCard {
tab: "typography"
tags: ["animation", "motion", "effect", "slide", "directional", "depth", "spring", "physics"]
title: I18n.tr("Motion Effects")
settingKey: "motionEffect"
iconName: "motion_photos_on"
Item {
width: parent.width
height: motionEffectGroup.implicitHeight
clip: true
DankButtonGroup {
id: motionEffectGroup
anchors.horizontalCenter: parent.horizontalCenter
buttonPadding: parent.width < 480 ? Theme.spacingS : Theme.spacingL
minButtonWidth: parent.width < 480 ? 64 : 96
textSize: parent.width < 480 ? Theme.fontSizeSmall : Theme.fontSizeMedium
model: [I18n.tr("Standard"), I18n.tr("Directional"), I18n.tr("Depth")]
selectionMode: "single"
currentIndex: SettingsData.motionEffect
onSelectionChanged: (index, selected) => {
if (!selected)
return;
SettingsData.set("motionEffect", index);
}
Connections {
target: SettingsData
function onMotionEffectChanged() {
motionEffectGroup.currentIndex = SettingsData.motionEffect;
}
}
}
}
Rectangle {
width: parent.width
height: 1
color: Theme.outline
opacity: 0.15
}
Item {
width: parent.width
height: motionEffectDescription.implicitHeight + Theme.spacingS * 2
StyledText {
id: motionEffectDescription
x: Theme.spacingM
y: Theme.spacingS
width: parent.width - Theme.spacingM * 2
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
text: {
switch (SettingsData.motionEffect) {
case 1:
return I18n.tr("Directional: Panels glide in from a larger distance at full size — no scale change, pure clean motion.");
case 2:
return I18n.tr("Depth: Panels scale up from small as they slide in — a dramatic pop-forward depth effect.");
default:
return I18n.tr("Standard: Classic Material Design 3 — panels rise from below with a subtle scale. The DMS default.");
}
}
}
}
Rectangle {
width: parent.width
height: 1
color: Theme.outline
opacity: 0.15
visible: SettingsData.motionEffect === 1
}
SettingsDropdownRow {
visible: SettingsData.motionEffect === 1
tab: "typography"
tags: ["animation", "directional", "behavior", "overlap", "sticky", "roll", "connected"]
settingKey: "directionalAnimationMode"
text: I18n.tr("Directional Behavior")
description: {
if (SettingsData.connectedFrameModeActive)
return I18n.tr("Popouts emerge flush from the bar edge as a single continuous piece, with corner connectors bridging the junction");
return I18n.tr("How the popout emerges from the DankBar");
}
options: SettingsData.frameEnabled
? [I18n.tr("Overlap"), I18n.tr("Slide"), I18n.tr("Roll"), I18n.tr("Connected")]
: [I18n.tr("Overlap"), I18n.tr("Slide"), I18n.tr("Roll")]
currentValue: {
switch (SettingsData.directionalAnimationMode) {
case 1:
return I18n.tr("Slide");
case 2:
return I18n.tr("Roll");
case 3:
return SettingsData.frameEnabled ? I18n.tr("Connected") : I18n.tr("Slide");
default:
return I18n.tr("Overlap");
}
}
onValueChanged: value => {
if (value === I18n.tr("Slide"))
SettingsData.set("directionalAnimationMode", 1);
else if (value === I18n.tr("Roll"))
SettingsData.set("directionalAnimationMode", 2);
else if (value === I18n.tr("Connected") && SettingsData.frameEnabled) {
if (SettingsData.directionalAnimationMode !== 3)
SettingsData.set("previousDirectionalMode", SettingsData.directionalAnimationMode);
SettingsData.set("directionalAnimationMode", 3);
} else
SettingsData.set("directionalAnimationMode", 0);
}
}
}
SettingsCard {
tab: "typography"
tags: ["font", "family", "text", "typography"]

View File

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

View File

@@ -1,6 +1,5 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Common
import qs.Widgets
import qs.Services
@@ -52,15 +51,14 @@ Column {
height: implicitHeight
spacing: Theme.spacingM
RowLayout {
width: parent.width
Row {
spacing: Theme.spacingM
DankIcon {
name: root.titleIcon
size: Theme.iconSize
color: Theme.primary
Layout.alignment: Qt.AlignVCenter
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
@@ -68,7 +66,7 @@ Column {
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
Layout.alignment: Qt.AlignVCenter
anchors.verticalCenter: parent.verticalCenter
}
}
@@ -981,10 +979,26 @@ Column {
Repeater {
model: [
{ label: I18n.tr("Percentage"), mode: 0, 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" }
{
label: I18n.tr("Percentage"),
mode: 0,
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 {
@@ -1316,20 +1330,7 @@ Column {
id: longestControlCenterLabelMetrics
font.pixelSize: Theme.fontSizeSmall
text: {
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")
];
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")];
let longest = "";
for (let i = 0; i < labels.length; i++) {
if (labels[i].length > longest.length)
@@ -1340,6 +1341,7 @@ Column {
}
Repeater {
id: groupRepeater
model: controlCenterContextMenu.controlCenterGroups
delegate: Item {
@@ -1569,8 +1571,6 @@ Column {
}
}
}
id: groupRepeater
}
}
}

View File

@@ -121,9 +121,9 @@ Scope {
Behavior on opacity {
NumberAnimation {
duration: Theme.expressiveDurations.expressiveDefaultSpatial
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewScope.overviewOpen)
easing.type: Easing.BezierSpline
easing.bezierCurve: overviewScope.overviewOpen ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
easing.bezierCurve: overviewScope.overviewOpen ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
}
}
@@ -154,45 +154,69 @@ Scope {
id: scaleTransform
origin.x: contentContainer.width / 2
origin.y: contentContainer.height / 2
xScale: overviewScope.overviewOpen ? 1 : 0.96
yScale: overviewScope.overviewOpen ? 1 : 0.96
xScale: overviewScope.overviewOpen ? 1 : Theme.effectScaleCollapsed
yScale: overviewScope.overviewOpen ? 1 : Theme.effectScaleCollapsed
Behavior on xScale {
NumberAnimation {
duration: Theme.expressiveDurations.expressiveDefaultSpatial
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewScope.overviewOpen)
easing.type: Easing.BezierSpline
easing.bezierCurve: overviewScope.overviewOpen ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
easing.bezierCurve: overviewScope.overviewOpen ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
}
}
Behavior on yScale {
NumberAnimation {
duration: Theme.expressiveDurations.expressiveDefaultSpatial
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewScope.overviewOpen)
easing.type: Easing.BezierSpline
easing.bezierCurve: overviewScope.overviewOpen ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
easing.bezierCurve: overviewScope.overviewOpen ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
}
}
}
Translate {
id: motionTransform
x: 0
y: overviewScope.overviewOpen ? 0 : Theme.spacingL
x: {
if (overviewScope.overviewOpen)
return 0;
if (Theme.isDirectionalEffect)
return 0;
if (Theme.isDepthEffect)
return Theme.effectAnimOffset * 0.25;
return 0;
}
y: {
if (overviewScope.overviewOpen)
return 0;
if (Theme.isDirectionalEffect)
return -Math.max(contentContainer.height * 0.8, Theme.effectAnimOffset * 1.1);
if (Theme.isDepthEffect)
return Math.max(Theme.effectAnimOffset * 0.85, 28);
return Theme.effectAnimOffset;
}
Behavior on x {
NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewScope.overviewOpen)
easing.type: Easing.BezierSpline
easing.bezierCurve: overviewScope.overviewOpen ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
}
}
Behavior on y {
NumberAnimation {
duration: Theme.expressiveDurations.expressiveDefaultSpatial
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewScope.overviewOpen)
easing.type: Easing.BezierSpline
easing.bezierCurve: overviewScope.overviewOpen ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
easing.bezierCurve: overviewScope.overviewOpen ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
}
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.expressiveDurations.expressiveDefaultSpatial
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewScope.overviewOpen)
easing.type: Easing.BezierSpline
easing.bezierCurve: overviewScope.overviewOpen ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
easing.bezierCurve: overviewScope.overviewOpen ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
}
}

View File

@@ -202,8 +202,18 @@ Scope {
Item {
id: spotlightContainer
x: Theme.snap((parent.width - width) / 2, overlayWindow.dpr)
y: Theme.snap((parent.height - height) / 2, overlayWindow.dpr)
readonly property bool directionalEffect: Theme.isDirectionalEffect
readonly property bool depthEffect: Theme.isDepthEffect
readonly property real collapsedMotionX: depthEffect ? Theme.effectAnimOffset * 0.25 : 0
readonly property real collapsedMotionY: {
if (directionalEffect)
return Math.max(height * 0.85, Theme.effectAnimOffset * 1.1);
if (depthEffect)
return Math.max(Theme.effectAnimOffset * 0.8, 30);
return 0;
}
x: Theme.snap((parent.width - width) / 2 + (overlayWindow.shouldShowSpotlight ? 0 : collapsedMotionX), overlayWindow.dpr)
y: Theme.snap((parent.height - height) / 2 + (overlayWindow.shouldShowSpotlight ? 0 : collapsedMotionY), overlayWindow.dpr)
readonly property int baseWidth: {
switch (SettingsData.dankLauncherV2Size) {
@@ -234,8 +244,8 @@ Scope {
readonly property bool animatingOut: niriOverviewScope.isClosing && overlayWindow.isSpotlightScreen
scale: overlayWindow.shouldShowSpotlight ? 1.0 : 0.96
opacity: overlayWindow.shouldShowSpotlight ? 1 : 0
scale: Theme.isDirectionalEffect ? 1 : (overlayWindow.shouldShowSpotlight ? 1.0 : Theme.effectScaleCollapsed)
opacity: Theme.isDirectionalEffect ? 1 : (overlayWindow.shouldShowSpotlight ? 1 : 0)
visible: overlayWindow.shouldShowSpotlight || animatingOut
enabled: overlayWindow.shouldShowSpotlight
@@ -245,10 +255,11 @@ Scope {
Behavior on scale {
id: scaleAnimation
enabled: !Theme.isDirectionalEffect
NumberAnimation {
duration: Theme.expressiveDurations.fast
duration: Theme.variantDuration(Theme.expressiveDurations.fast, overlayWindow.shouldShowSpotlight)
easing.type: Easing.BezierSpline
easing.bezierCurve: spotlightContainer.visible ? Theme.expressiveCurves.expressiveFastSpatial : Theme.expressiveCurves.standardAccel
easing.bezierCurve: spotlightContainer.visible ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
onRunningChanged: {
if (running || !spotlightContainer.animatingOut)
return;
@@ -258,10 +269,27 @@ Scope {
}
Behavior on opacity {
enabled: !Theme.isDirectionalEffect
NumberAnimation {
duration: Theme.expressiveDurations.fast
duration: Theme.variantDuration(Theme.expressiveDurations.fast, overlayWindow.shouldShowSpotlight)
easing.type: Easing.BezierSpline
easing.bezierCurve: spotlightContainer.visible ? Theme.expressiveCurves.expressiveFastSpatial : Theme.expressiveCurves.standardAccel
easing.bezierCurve: spotlightContainer.visible ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
}
}
Behavior on x {
NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.fast, overlayWindow.shouldShowSpotlight)
easing.type: Easing.BezierSpline
easing.bezierCurve: spotlightContainer.visible ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
}
}
Behavior on y {
NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.fast, overlayWindow.shouldShowSpotlight)
easing.type: Easing.BezierSpline
easing.bezierCurve: spotlightContainer.visible ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
}
}

View File

@@ -62,30 +62,30 @@ Item {
Behavior on x {
NumberAnimation {
duration: Theme.expressiveDurations.expressiveDefaultSpatial
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen)
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel
easing.bezierCurve: Theme.variantModalEnterCurve
}
}
Behavior on y {
NumberAnimation {
duration: Theme.expressiveDurations.expressiveDefaultSpatial
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen)
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel
easing.bezierCurve: Theme.variantModalEnterCurve
}
}
Behavior on width {
NumberAnimation {
duration: Theme.expressiveDurations.expressiveDefaultSpatial
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen)
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel
easing.bezierCurve: Theme.variantModalEnterCurve
}
}
Behavior on height {
NumberAnimation {
duration: Theme.expressiveDurations.expressiveDefaultSpatial
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen)
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel
easing.bezierCurve: Theme.variantModalEnterCurve
}
}
@@ -124,16 +124,16 @@ Item {
Behavior on width {
NumberAnimation {
duration: Theme.expressiveDurations.expressiveDefaultSpatial
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen)
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel
easing.bezierCurve: Theme.variantModalEnterCurve
}
}
Behavior on height {
NumberAnimation {
duration: Theme.expressiveDurations.expressiveDefaultSpatial
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen)
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel
easing.bezierCurve: Theme.variantModalEnterCurve
}
}
}

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

@@ -0,0 +1,152 @@
import QtQuick
import QtQuick.Shapes
import qs.Common
// ConnectedCorner — Seam-complement connector that fills the void between
// a bar's rounded corner and a popout's flush edge, creating a seamless junction.
//
// Usage: Place as a sibling to contentWrapper inside unrollCounteract (DankPopout)
// or as a sibling to dockBackground (Dock). Position using contentWrapper.x/y.
//
// barSide: "top" | "bottom" | "left" | "right" — which edge the bar is on
// placement: "left" | "right" — which lateral end of that edge
// spacing: gap between bar surface and popout surface (storedBarSpacing, ~4px)
// connectorRadius: bar corner radius to match (frameRounding or Theme.cornerRadius)
// color: fill color matching the popout surface
Item {
id: root
property string barSide: "top"
property string placement: "left"
property real spacing: 4
property real connectorRadius: 12
property color color: "transparent"
readonly property bool isHorizontalBar: barSide === "top" || barSide === "bottom"
readonly property bool isPlacementLeft: placement === "left"
readonly property string arcCorner: {
if (barSide === "top")
return isPlacementLeft ? "bottomLeft" : "bottomRight";
if (barSide === "bottom")
return isPlacementLeft ? "topLeft" : "topRight";
if (barSide === "left")
return isPlacementLeft ? "topRight" : "bottomRight";
return isPlacementLeft ? "topLeft" : "bottomLeft";
}
readonly property real pathStartX: {
switch (arcCorner) {
case "topLeft":
return width;
case "topRight":
case "bottomLeft":
return 0;
default:
return 0;
}
}
readonly property real pathStartY: {
switch (arcCorner) {
case "bottomRight":
return height;
default:
return 0;
}
}
readonly property real firstLineX: {
switch (arcCorner) {
case "topLeft":
case "bottomLeft":
return width;
default:
return 0;
}
}
readonly property real firstLineY: {
switch (arcCorner) {
case "topLeft":
case "topRight":
return height;
default:
return 0;
}
}
readonly property real secondLineX: {
switch (arcCorner) {
case "topRight":
case "bottomLeft":
case "bottomRight":
return width;
default:
return 0;
}
}
readonly property real secondLineY: {
switch (arcCorner) {
case "topLeft":
case "topRight":
case "bottomLeft":
return height;
default:
return 0;
}
}
readonly property real arcCenterX: arcCorner === "topRight" || arcCorner === "bottomRight" ? width : 0
readonly property real arcCenterY: arcCorner === "bottomLeft" || arcCorner === "bottomRight" ? height : 0
readonly property real arcStartAngle: {
switch (arcCorner) {
case "topLeft":
case "topRight":
return 90;
case "bottomLeft":
return 0;
default:
return -90;
}
}
readonly property real arcSweepAngle: {
switch (arcCorner) {
case "topRight":
return 90;
default:
return -90;
}
}
// Horizontal bar: connector is tall (bridges vertical gap), narrow (corner radius wide)
// Vertical bar: connector is wide (bridges horizontal gap), short (corner radius tall)
width: isHorizontalBar ? connectorRadius : (spacing + connectorRadius)
height: isHorizontalBar ? (spacing + connectorRadius) : connectorRadius
Shape {
anchors.fill: parent
preferredRendererType: Shape.CurveRenderer
ShapePath {
fillColor: root.color
strokeColor: "transparent"
strokeWidth: 0
startX: root.pathStartX
startY: root.pathStartY
PathLine {
x: root.firstLineX
y: root.firstLineY
}
PathLine {
x: root.secondLineX
y: root.secondLineY
}
PathAngleArc {
centerX: root.arcCenterX
centerY: root.arcCenterY
radiusX: root.width
radiusY: root.height
startAngle: root.arcStartAngle
sweepAngle: root.arcSweepAngle
}
}
}
}

View File

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

View File

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

View File

@@ -20,10 +20,10 @@ Item {
property string triggerSection: ""
property string positioning: "center"
property int animationDuration: Theme.popoutAnimationDuration
property real animationScaleCollapsed: 0.96
property real animationOffset: Theme.spacingL
property list<real> animationEnterCurve: Theme.expressiveCurves.expressiveDefaultSpatial
property list<real> animationExitCurve: Theme.expressiveCurves.emphasized
property real animationScaleCollapsed: Theme.effectScaleCollapsed
property real animationOffset: Theme.effectAnimOffset
property list<real> animationEnterCurve: Theme.variantPopoutEnterCurve
property list<real> animationExitCurve: Theme.variantPopoutExitCurve
property bool suspendShadowWhileResizing: false
property bool shouldBeVisible: false
property var customKeyboardFocus: null
@@ -47,6 +47,8 @@ Item {
property var screen: null
readonly property real effectiveBarThickness: {
if (Theme.isConnectedEffect)
return Math.max(0, storedBarThickness);
const padding = storedBarConfig ? (storedBarConfig.innerPadding !== undefined ? storedBarConfig.innerPadding : 4) : 4;
return Math.max(26 + padding * 0.6, Theme.barHeight - 4 - (8 - padding)) + storedBarSpacing;
}
@@ -68,12 +70,14 @@ Item {
readonly property real barWidth: barBounds.width
readonly property real barHeight: barBounds.height
readonly property real barWingSize: barBounds.wingSize
readonly property bool effectiveSurfaceBlurEnabled: Theme.connectedSurfaceBlurEnabled
signal opened
signal popoutClosed
signal backgroundClicked
property var _lastOpenedScreen: null
property bool isClosing: false
property int effectiveBarPosition: 0
property real effectiveBarBottomGap: 0
@@ -156,10 +160,14 @@ Item {
}
}
property bool animationsEnabled: true
function open() {
if (!screen)
return;
closeTimer.stop();
isClosing = false;
animationsEnabled = false;
// Snapshot mask geometry
_frozenMaskX = maskX;
@@ -174,12 +182,22 @@ Item {
}
_lastOpenedScreen = screen;
shouldBeVisible = true;
if (contentContainer) {
contentContainer.animX = Theme.snap(contentContainer.offsetX, root.dpr);
contentContainer.animY = Theme.snap(contentContainer.offsetY, root.dpr);
contentContainer.scaleValue = root.animationScaleCollapsed;
}
if (useBackgroundWindow) {
_surfaceMarginLeft = alignedX - shadowBuffer;
_surfaceW = alignedWidth + shadowBuffer * 2;
backgroundWindow.visible = true;
}
contentWindow.visible = true;
Qt.callLater(() => {
animationsEnabled = true;
shouldBeVisible = true;
if (shouldBeVisible && screen) {
if (useBackgroundWindow)
backgroundWindow.visible = true;
@@ -191,6 +209,7 @@ Item {
}
function close() {
isClosing = true;
shouldBeVisible = false;
_primeContent = false;
PopoutManager.popoutChanged();
@@ -222,9 +241,10 @@ Item {
Timer {
id: closeTimer
interval: animationDuration
interval: Theme.variantCloseInterval(animationDuration)
onTriggered: {
if (!shouldBeVisible) {
isClosing = false;
contentWindow.visible = false;
if (useBackgroundWindow)
backgroundWindow.visible = false;
@@ -237,14 +257,71 @@ Item {
readonly property real screenWidth: screen ? screen.width : 0
readonly property real screenHeight: screen ? screen.height : 0
readonly property real dpr: screen ? screen.devicePixelRatio : 1
readonly property real frameInset: {
if (!SettingsData.frameEnabled) return 0;
const ft = SettingsData.frameThickness;
const fr = SettingsData.frameRounding;
const ccr = Theme.connectedCornerRadius;
if (Theme.isConnectedEffect)
return Math.max(ft * 4, ft + ccr * 2);
const useAutoGaps = storedBarConfig?.popupGapsAuto !== undefined ? storedBarConfig.popupGapsAuto : true;
const manualGapValue = storedBarConfig?.popupGapsManual !== undefined ? storedBarConfig.popupGapsManual : 6;
const gap = useAutoGaps ? Math.max(6, storedBarSpacing) : manualGapValue;
return Math.max(ft + gap, fr);
}
readonly property var shadowLevel: Theme.elevationLevel3
readonly property real shadowFallbackOffset: 6
readonly property real shadowRenderPadding: (Theme.elevationEnabled && SettingsData.popoutElevationEnabled) ? Theme.elevationRenderPadding(shadowLevel, effectiveShadowDirection, shadowFallbackOffset, 8, 16) : 0
readonly property real shadowMotionPadding: Math.max(0, animationOffset)
readonly property real shadowMotionPadding: {
if (Theme.isConnectedEffect)
return Math.max(storedBarSpacing + Theme.connectedCornerRadius + 4, 40);
if (Theme.isDirectionalEffect) {
if (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode !== 0)
return 16; // Slide Behind and Roll Out do not add animationOffset, enabling strict Wayland clipping.
return Math.max(0, animationOffset) + 16;
}
if (Theme.isDepthEffect)
return Math.max(0, animationOffset) + 8;
return Math.max(0, animationOffset);
}
readonly property real shadowBuffer: Theme.snap(shadowRenderPadding + shadowMotionPadding, dpr)
readonly property real alignedWidth: Theme.px(popupWidth, dpr)
readonly property real alignedHeight: Theme.px(popupHeight, dpr)
readonly property real connectedAnchorX: {
if (!Theme.isConnectedEffect)
return triggerX;
switch (effectiveBarPosition) {
case SettingsData.Position.Left:
return barX + barWidth;
case SettingsData.Position.Right:
return barX;
default:
return triggerX;
}
}
readonly property real connectedAnchorY: {
if (!Theme.isConnectedEffect)
return triggerY;
switch (effectiveBarPosition) {
case SettingsData.Position.Top:
return barY + barHeight;
case SettingsData.Position.Bottom:
return barY;
default:
return triggerY;
}
}
function adjacentBarClearance(exclusion) {
if (exclusion <= 0)
return 0;
if (!Theme.isConnectedEffect)
return exclusion;
// In a shared frame corner, the adjacent connected bar already occupies
// one rounded-corner radius before the popout's own connector begins.
return exclusion + Theme.connectedCornerRadius * 2;
}
onAlignedHeightChanged: {
if (!suspendShadowWhileResizing || !shouldBeVisible)
@@ -269,17 +346,22 @@ Item {
readonly property real alignedX: Theme.snap((() => {
const useAutoGaps = storedBarConfig?.popupGapsAuto !== undefined ? storedBarConfig.popupGapsAuto : true;
const manualGapValue = storedBarConfig?.popupGapsManual !== undefined ? storedBarConfig.popupGapsManual : 4;
const popupGap = useAutoGaps ? Math.max(4, storedBarSpacing) : manualGapValue;
const rawPopupGap = useAutoGaps ? Math.max(4, storedBarSpacing) : manualGapValue;
const popupGap = Theme.isConnectedEffect ? 0 : rawPopupGap;
const edgeGap = Math.max(popupGap, frameInset);
const anchorX = Theme.isConnectedEffect ? connectedAnchorX : triggerX;
switch (effectiveBarPosition) {
case SettingsData.Position.Left:
return Math.max(popupGap, Math.min(screenWidth - popupWidth - popupGap, triggerX));
// bar on left: left side is bar-adjacent (popupGap), right side is frame-perpendicular (edgeGap)
return Math.max(popupGap, Math.min(screenWidth - popupWidth - edgeGap, anchorX));
case SettingsData.Position.Right:
return Math.max(popupGap, Math.min(screenWidth - popupWidth - popupGap, triggerX - popupWidth));
// bar on right: right side is bar-adjacent (popupGap), left side is frame-perpendicular (edgeGap)
return Math.max(edgeGap, Math.min(screenWidth - popupWidth - popupGap, anchorX - popupWidth));
default:
const rawX = triggerX + (triggerWidth / 2) - (popupWidth / 2);
const minX = adjacentBarInfo.leftBar > 0 ? adjacentBarInfo.leftBar : popupGap;
const maxX = screenWidth - popupWidth - (adjacentBarInfo.rightBar > 0 ? adjacentBarInfo.rightBar : popupGap);
const minX = Math.max(edgeGap, adjacentBarClearance(adjacentBarInfo.leftBar));
const maxX = screenWidth - popupWidth - Math.max(edgeGap, adjacentBarClearance(adjacentBarInfo.rightBar));
return Math.max(minX, Math.min(maxX, rawX));
}
})(), dpr)
@@ -287,17 +369,22 @@ Item {
readonly property real alignedY: Theme.snap((() => {
const useAutoGaps = storedBarConfig?.popupGapsAuto !== undefined ? storedBarConfig.popupGapsAuto : true;
const manualGapValue = storedBarConfig?.popupGapsManual !== undefined ? storedBarConfig.popupGapsManual : 4;
const popupGap = useAutoGaps ? Math.max(4, storedBarSpacing) : manualGapValue;
const rawPopupGap = useAutoGaps ? Math.max(4, storedBarSpacing) : manualGapValue;
const popupGap = Theme.isConnectedEffect ? 0 : rawPopupGap;
const edgeGap = Math.max(popupGap, frameInset);
const anchorY = Theme.isConnectedEffect ? connectedAnchorY : triggerY;
switch (effectiveBarPosition) {
case SettingsData.Position.Bottom:
return Math.max(popupGap, Math.min(screenHeight - popupHeight - popupGap, triggerY - popupHeight));
// bar on bottom: bottom side is bar-adjacent (popupGap), top side is frame-perpendicular (edgeGap)
return Math.max(edgeGap, Math.min(screenHeight - popupHeight - popupGap, anchorY - popupHeight));
case SettingsData.Position.Top:
return Math.max(popupGap, Math.min(screenHeight - popupHeight - popupGap, triggerY));
// bar on top: top side is bar-adjacent (popupGap), bottom side is frame-perpendicular (edgeGap)
return Math.max(popupGap, Math.min(screenHeight - popupHeight - edgeGap, anchorY));
default:
const rawY = triggerY - (popupHeight / 2);
const minY = adjacentBarInfo.topBar > 0 ? adjacentBarInfo.topBar : popupGap;
const maxY = screenHeight - popupHeight - (adjacentBarInfo.bottomBar > 0 ? adjacentBarInfo.bottomBar : popupGap);
const minY = Math.max(edgeGap, adjacentBarClearance(adjacentBarInfo.topBar));
const maxY = screenHeight - popupHeight - Math.max(edgeGap, adjacentBarClearance(adjacentBarInfo.bottomBar));
return Math.max(minY, Math.min(maxY, rawY));
}
})(), dpr)
@@ -353,6 +440,10 @@ Item {
mask: Region {
item: maskRect
Region {
item: contentExclusionRect
intersection: Intersection.Subtract
}
}
Rectangle {
@@ -361,26 +452,70 @@ Item {
color: "transparent"
x: root._frozenMaskX
y: root._frozenMaskY
width: (shouldBeVisible && backgroundInteractive) ? root._frozenMaskWidth : 0
height: (shouldBeVisible && backgroundInteractive) ? root._frozenMaskHeight : 0
width: (backgroundWindow.visible && backgroundInteractive) ? root._frozenMaskWidth : 0
height: (backgroundWindow.visible && backgroundInteractive) ? root._frozenMaskHeight : 0
}
MouseArea {
Item {
id: contentExclusionRect
visible: false
x: root.alignedX
y: root.alignedY
width: root.alignedWidth
height: root.alignedHeight
}
Item {
id: outsideClickCatcher
x: root._frozenMaskX
y: root._frozenMaskY
width: root._frozenMaskWidth
height: root._frozenMaskHeight
hoverEnabled: false
enabled: shouldBeVisible && backgroundInteractive
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onClicked: mouse => {
const clickX = mouse.x + root._frozenMaskX;
const clickY = mouse.y + root._frozenMaskY;
const outsideContent = clickX < root.alignedX || clickX > root.alignedX + root.alignedWidth || clickY < root.alignedY || clickY > root.alignedY + root.alignedHeight;
enabled: root.shouldBeVisible && root.backgroundInteractive
if (!outsideContent)
return;
backgroundClicked();
readonly property real contentLeft: Math.max(0, root.alignedX - x)
readonly property real contentTop: Math.max(0, root.alignedY - y)
readonly property real contentRight: Math.min(width, contentLeft + root.alignedWidth)
readonly property real contentBottom: Math.min(height, contentTop + root.alignedHeight)
MouseArea {
x: 0
y: 0
width: outsideClickCatcher.width
height: Math.max(0, outsideClickCatcher.contentTop)
enabled: parent.enabled
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onClicked: root.backgroundClicked()
}
MouseArea {
x: 0
y: outsideClickCatcher.contentBottom
width: outsideClickCatcher.width
height: Math.max(0, outsideClickCatcher.height - outsideClickCatcher.contentBottom)
enabled: parent.enabled
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onClicked: root.backgroundClicked()
}
MouseArea {
x: 0
y: outsideClickCatcher.contentTop
width: Math.max(0, outsideClickCatcher.contentLeft)
height: Math.max(0, outsideClickCatcher.contentBottom - outsideClickCatcher.contentTop)
enabled: parent.enabled
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onClicked: root.backgroundClicked()
}
MouseArea {
x: outsideClickCatcher.contentRight
y: outsideClickCatcher.contentTop
width: Math.max(0, outsideClickCatcher.width - outsideClickCatcher.contentRight)
height: Math.max(0, outsideClickCatcher.contentBottom - outsideClickCatcher.contentTop)
enabled: parent.enabled
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onClicked: root.backgroundClicked()
}
}
@@ -398,6 +533,49 @@ Item {
visible: false
color: "transparent"
WindowBlur {
id: popoutBlur
targetWindow: contentWindow
blurEnabled: root.effectiveSurfaceBlurEnabled
readonly property real s: Math.min(1, contentContainer.scaleValue)
readonly property bool trackBlurFromBarEdge: Theme.isConnectedEffect
|| (typeof SettingsData !== "undefined"
&& Theme.isDirectionalEffect
&& SettingsData.directionalAnimationMode !== 2)
// Directional popouts clip to the bar edge, so the blur needs to grow from
// that same edge instead of translating through the bar before settling.
readonly property real _dyClamp: (contentContainer.barTop || contentContainer.barBottom)
? Math.max(-contentContainer.height, Math.min(contentContainer.animY, contentContainer.height))
: 0
readonly property real _dxClamp: (contentContainer.barLeft || contentContainer.barRight)
? Math.max(-contentContainer.width, Math.min(contentContainer.animX, contentContainer.width))
: 0
blurX: trackBlurFromBarEdge
? contentContainer.x + (contentContainer.barRight ? _dxClamp : 0)
: contentContainer.x + contentContainer.width * (1 - s) * 0.5
+ Theme.snap(contentContainer.animX, root.dpr)
- contentContainer.horizontalConnectorExtent * s
blurY: trackBlurFromBarEdge
? contentContainer.y + (contentContainer.barBottom ? _dyClamp : 0)
: contentContainer.y + contentContainer.height * (1 - s) * 0.5
+ Theme.snap(contentContainer.animY, root.dpr)
- contentContainer.verticalConnectorExtent * s
blurWidth: (shouldBeVisible && contentWrapper.opacity > 0)
? (trackBlurFromBarEdge
? Math.max(0, contentContainer.width - Math.abs(_dxClamp))
: (contentContainer.width + contentContainer.horizontalConnectorExtent * 2) * s)
: 0
blurHeight: (shouldBeVisible && contentWrapper.opacity > 0)
? (trackBlurFromBarEdge
? Math.max(0, contentContainer.height - Math.abs(_dyClamp))
: (contentContainer.height + contentContainer.verticalConnectorExtent * 2) * s)
: 0
blurRadius: Theme.isConnectedEffect ? Theme.connectedCornerRadius : Theme.connectedSurfaceRadius
}
WlrLayershell.namespace: root.layerNamespace
WlrLayershell.layer: {
switch (Quickshell.env("DMS_POPOUT_LAYER")) {
@@ -425,7 +603,6 @@ Item {
}
readonly property bool _fullHeight: useBackgroundWindow && root.fullHeightSurface
anchors {
left: true
top: true
@@ -451,10 +628,10 @@ Item {
Item {
id: contentMaskRect
visible: false
x: contentContainer.x
y: contentContainer.y
width: shouldBeVisible ? root.alignedWidth : 0
height: shouldBeVisible ? root.alignedHeight : 0
x: contentContainer.x - contentContainer.horizontalConnectorExtent
y: contentContainer.y - contentContainer.verticalConnectorExtent
width: root.alignedWidth + contentContainer.horizontalConnectorExtent * 2
height: root.alignedHeight + contentContainer.verticalConnectorExtent * 2
}
MouseArea {
@@ -483,12 +660,124 @@ Item {
readonly property bool barBottom: effectiveBarPosition === SettingsData.Position.Bottom
readonly property bool barLeft: effectiveBarPosition === SettingsData.Position.Left
readonly property bool barRight: effectiveBarPosition === SettingsData.Position.Right
readonly property real offsetX: barLeft ? root.animationOffset : (barRight ? -root.animationOffset : 0)
readonly property real offsetY: barBottom ? -root.animationOffset : (barTop ? root.animationOffset : 0)
readonly property string connectedBarSide: barTop ? "top" : (barBottom ? "bottom" : (barLeft ? "left" : "right"))
readonly property real surfaceRadius: Theme.connectedSurfaceRadius
readonly property color surfaceColor: Theme.popupLayerColor(Theme.surfaceContainer)
readonly property color surfaceBorderColor: Theme.isConnectedEffect
? "transparent"
: (BlurService.enabled ? BlurService.borderColor : Theme.outlineMedium)
readonly property real surfaceBorderWidth: Theme.isConnectedEffect ? 0 : BlurService.borderWidth
readonly property real surfaceTopLeftRadius: Theme.isConnectedEffect && (barTop || barLeft) ? 0 : surfaceRadius
readonly property real surfaceTopRightRadius: Theme.isConnectedEffect && (barTop || barRight) ? 0 : surfaceRadius
readonly property real surfaceBottomLeftRadius: Theme.isConnectedEffect && (barBottom || barLeft) ? 0 : surfaceRadius
readonly property real surfaceBottomRightRadius: Theme.isConnectedEffect && (barBottom || barRight) ? 0 : surfaceRadius
readonly property bool directionalEffect: Theme.isDirectionalEffect
readonly property bool depthEffect: Theme.isDepthEffect
readonly property real directionalTravelX: Math.max(root.animationOffset, root.alignedWidth + Theme.spacingL)
readonly property real directionalTravelY: Math.max(root.animationOffset, root.alignedHeight + Theme.spacingL)
readonly property real depthTravel: Math.max(root.animationOffset * 0.7, 28)
readonly property real sectionTilt: (triggerSection === "left" ? -1 : (triggerSection === "right" ? 1 : 0))
readonly property real horizontalConnectorExtent: Theme.isConnectedEffect && (barTop || barBottom) ? Theme.connectedCornerRadius : 0
readonly property real verticalConnectorExtent: Theme.isConnectedEffect && (barLeft || barRight) ? Theme.connectedCornerRadius : 0
function connectorWidth(spacing) {
return (barTop || barBottom) ? Theme.connectedCornerRadius : (spacing + Theme.connectedCornerRadius);
}
function connectorHeight(spacing) {
return (barTop || barBottom) ? (spacing + Theme.connectedCornerRadius) : Theme.connectedCornerRadius;
}
function connectorSeamX(baseX, bodyWidth, placement) {
if (barTop || barBottom)
return placement === "left" ? baseX : baseX + bodyWidth;
return barLeft ? baseX : baseX + bodyWidth;
}
function connectorSeamY(baseY, bodyHeight, placement) {
if (barTop)
return baseY;
if (barBottom)
return baseY + bodyHeight;
return placement === "left" ? baseY : baseY + bodyHeight;
}
function connectorX(baseX, bodyWidth, placement, spacing) {
const seamX = connectorSeamX(baseX, bodyWidth, placement);
const width = connectorWidth(spacing);
if (barTop || barBottom)
return placement === "left" ? seamX - width : seamX;
return barLeft ? seamX : seamX - width;
}
function connectorY(baseY, bodyHeight, placement, spacing) {
const seamY = connectorSeamY(baseY, bodyHeight, placement);
const height = connectorHeight(spacing);
if (barTop)
return seamY;
if (barBottom)
return seamY - height;
return placement === "left" ? seamY - height : seamY;
}
readonly property real offsetX: {
if (directionalEffect) {
if (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2)
return 0;
if (barLeft)
return -directionalTravelX;
if (barRight)
return directionalTravelX;
if (barTop || barBottom)
return 0;
return sectionTilt * directionalTravelX * 0.2;
}
if (depthEffect) {
if (barLeft)
return -depthTravel;
if (barRight)
return depthTravel;
if (barTop || barBottom)
return 0;
return sectionTilt * depthTravel * 0.2;
}
return barLeft ? root.animationOffset : (barRight ? -root.animationOffset : 0);
}
readonly property real offsetY: {
if (directionalEffect) {
if (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2)
return 0;
if (barBottom)
return directionalTravelY;
if (barTop)
return -directionalTravelY;
if (barLeft || barRight)
return 0;
return directionalTravelY;
}
if (depthEffect) {
if (barBottom)
return depthTravel;
if (barTop)
return -depthTravel;
if (barLeft || barRight)
return 0;
return depthTravel;
}
return barBottom ? -root.animationOffset : (barTop ? root.animationOffset : 0);
}
property real animX: 0
property real animY: 0
property real scaleValue: root.animationScaleCollapsed
readonly property real computedScaleCollapsed: (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 && Theme.isDirectionalEffect) ? 0.0 : root.animationScaleCollapsed
property real scaleValue: computedScaleCollapsed
Component.onCompleted: {
animX = Theme.snap(root.shouldBeVisible ? 0 : offsetX, root.dpr);
animY = Theme.snap(root.shouldBeVisible ? 0 : offsetY, root.dpr);
scaleValue = root.shouldBeVisible ? 1.0 : computedScaleCollapsed;
}
onOffsetXChanged: animX = Theme.snap(root.shouldBeVisible ? 0 : offsetX, root.dpr)
onOffsetYChanged: animY = Theme.snap(root.shouldBeVisible ? 0 : offsetY, root.dpr)
@@ -498,89 +787,240 @@ Item {
function onShouldBeVisibleChanged() {
contentContainer.animX = Theme.snap(root.shouldBeVisible ? 0 : contentContainer.offsetX, root.dpr);
contentContainer.animY = Theme.snap(root.shouldBeVisible ? 0 : contentContainer.offsetY, root.dpr);
contentContainer.scaleValue = root.shouldBeVisible ? 1.0 : root.animationScaleCollapsed;
contentContainer.scaleValue = root.shouldBeVisible ? 1.0 : contentContainer.computedScaleCollapsed;
}
}
Behavior on animX {
enabled: root.animationsEnabled
NumberAnimation {
duration: root.animationDuration
duration: Theme.variantDuration(root.animationDuration, root.shouldBeVisible)
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
}
}
Behavior on animY {
enabled: root.animationsEnabled
NumberAnimation {
duration: root.animationDuration
duration: Theme.variantDuration(root.animationDuration, root.shouldBeVisible)
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
}
}
Behavior on scaleValue {
enabled: root.animationsEnabled
NumberAnimation {
duration: root.animationDuration
duration: Theme.variantDuration(root.animationDuration, root.shouldBeVisible)
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
}
}
ElevationShadow {
id: shadowSource
width: parent.width
height: parent.height
opacity: contentWrapper.opacity
scale: contentWrapper.scale
x: contentWrapper.x
y: contentWrapper.y
level: root.shadowLevel
direction: root.effectiveShadowDirection
fallbackOffset: root.shadowFallbackOffset
targetRadius: Theme.cornerRadius
targetColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
shadowEnabled: Theme.elevationEnabled && SettingsData.popoutElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1" && !(root.suspendShadowWhileResizing && root._resizeActive)
}
Item {
id: contentWrapper
anchors.centerIn: parent
width: parent.width
height: parent.height
opacity: shouldBeVisible ? 1 : 0
visible: opacity > 0
scale: contentContainer.scaleValue
x: Theme.snap(contentContainer.animX + (parent.width - width) * (1 - contentContainer.scaleValue) * 0.5, root.dpr)
y: Theme.snap(contentContainer.animY + (parent.height - height) * (1 - contentContainer.scaleValue) * 0.5, root.dpr)
id: directionalClipMask
layer.enabled: contentWrapper.opacity < 1
layer.smooth: false
layer.textureSize: root.dpr > 1 ? Qt.size(Math.ceil(width * root.dpr), Math.ceil(height * root.dpr)) : Qt.size(0, 0)
readonly property bool shouldClip: Theme.isDirectionalEffect
&& typeof SettingsData !== "undefined"
&& SettingsData.directionalAnimationMode > 0
readonly property real clipOversize: 1000
readonly property real connectedClipAllowance: Theme.isConnectedEffect
? Math.ceil(root.shadowRenderPadding + BlurService.borderWidth + 2)
: 0
Behavior on opacity {
NumberAnimation {
duration: animationDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
}
clip: shouldClip
// Bound the clipping strictly to the bar side, allowing massive overflow on the other 3 sides for shadows
x: shouldClip ? (contentContainer.barLeft ? -connectedClipAllowance : -clipOversize) : 0
y: shouldClip ? (contentContainer.barTop ? -connectedClipAllowance : -clipOversize) : 0
width: {
if (!shouldClip)
return parent.width;
if (contentContainer.barLeft)
return parent.width + connectedClipAllowance + clipOversize;
if (contentContainer.barRight)
return parent.width + clipOversize + connectedClipAllowance;
return parent.width + clipOversize * 2;
}
height: {
if (!shouldClip)
return parent.height;
if (contentContainer.barTop)
return parent.height + connectedClipAllowance + clipOversize;
if (contentContainer.barBottom)
return parent.height + clipOversize + connectedClipAllowance;
return parent.height + clipOversize * 2;
}
Rectangle {
anchors.fill: parent
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
border.color: Theme.outlineMedium
border.width: 0
}
Item {
id: aligner
readonly property real baseWidth: contentContainer.width
readonly property real baseHeight: contentContainer.height
readonly property bool isRollOut: typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 && Theme.isDirectionalEffect
Loader {
id: contentLoader
anchors.fill: parent
active: root._primeContent || shouldBeVisible || contentWindow.visible
asynchronous: false
}
}
}
x: (directionalClipMask.x !== 0 ? -directionalClipMask.x : 0) + (isRollOut && contentContainer.barRight ? baseWidth * (1 - contentContainer.scaleValue) : 0)
y: (directionalClipMask.y !== 0 ? -directionalClipMask.y : 0) + (isRollOut && contentContainer.barBottom ? baseHeight * (1 - contentContainer.scaleValue) : 0)
width: isRollOut && (contentContainer.barLeft || contentContainer.barRight) ? Math.max(0, baseWidth * contentContainer.scaleValue) : baseWidth
height: isRollOut && (contentContainer.barTop || contentContainer.barBottom) ? Math.max(0, baseHeight * contentContainer.scaleValue) : baseHeight
clip: isRollOut
Item {
id: unrollCounteract
x: aligner.isRollOut && contentContainer.barRight ? -(aligner.baseWidth * (1 - contentContainer.scaleValue)) : 0
y: aligner.isRollOut && contentContainer.barBottom ? -(aligner.baseHeight * (1 - contentContainer.scaleValue)) : 0
width: aligner.baseWidth
height: aligner.baseHeight
ElevationShadow {
id: shadowSource
readonly property real connectorExtent: Theme.isConnectedEffect ? Theme.connectedCornerRadius : 0
readonly property real extraLeft: Theme.isConnectedEffect && (contentContainer.barTop || contentContainer.barBottom) ? connectorExtent : 0
readonly property real extraRight: Theme.isConnectedEffect && (contentContainer.barTop || contentContainer.barBottom) ? connectorExtent : 0
readonly property real extraTop: Theme.isConnectedEffect && (contentContainer.barLeft || contentContainer.barRight) ? connectorExtent : 0
readonly property real extraBottom: Theme.isConnectedEffect && (contentContainer.barLeft || contentContainer.barRight) ? connectorExtent : 0
readonly property real bodyX: extraLeft
readonly property real bodyY: extraTop
readonly property real bodyWidth: parent.width
readonly property real bodyHeight: parent.height
width: parent.width + extraLeft + extraRight
height: parent.height + extraTop + extraBottom
opacity: contentWrapper.opacity
scale: contentWrapper.scale
x: contentWrapper.x - extraLeft
y: contentWrapper.y - extraTop
level: root.shadowLevel
direction: root.effectiveShadowDirection
fallbackOffset: root.shadowFallbackOffset
targetRadius: contentContainer.surfaceRadius
topLeftRadius: contentContainer.surfaceTopLeftRadius
topRightRadius: contentContainer.surfaceTopRightRadius
bottomLeftRadius: contentContainer.surfaceBottomLeftRadius
bottomRightRadius: contentContainer.surfaceBottomRightRadius
targetColor: contentContainer.surfaceColor
borderColor: contentContainer.surfaceBorderColor
borderWidth: contentContainer.surfaceBorderWidth
useCustomSource: Theme.isConnectedEffect
shadowEnabled: Theme.elevationEnabled && SettingsData.popoutElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1" && !(root.suspendShadowWhileResizing && root._resizeActive)
Item {
anchors.fill: parent
visible: Theme.isConnectedEffect
clip: false
Rectangle {
x: shadowSource.bodyX
y: shadowSource.bodyY
width: shadowSource.bodyWidth
height: shadowSource.bodyHeight
topLeftRadius: contentContainer.surfaceTopLeftRadius
topRightRadius: contentContainer.surfaceTopRightRadius
bottomLeftRadius: contentContainer.surfaceBottomLeftRadius
bottomRightRadius: contentContainer.surfaceBottomRightRadius
color: contentContainer.surfaceColor
}
ConnectedCorner {
visible: Theme.isConnectedEffect
barSide: contentContainer.connectedBarSide
placement: "left"
spacing: 0
connectorRadius: Theme.connectedCornerRadius
color: contentContainer.surfaceColor
x: Theme.snap(contentContainer.connectorX(shadowSource.bodyX, shadowSource.bodyWidth, placement, spacing), root.dpr)
y: Theme.snap(contentContainer.connectorY(shadowSource.bodyY, shadowSource.bodyHeight, placement, spacing), root.dpr)
}
ConnectedCorner {
visible: Theme.isConnectedEffect
barSide: contentContainer.connectedBarSide
placement: "right"
spacing: 0
connectorRadius: Theme.connectedCornerRadius
color: contentContainer.surfaceColor
x: Theme.snap(contentContainer.connectorX(shadowSource.bodyX, shadowSource.bodyWidth, placement, spacing), root.dpr)
y: Theme.snap(contentContainer.connectorY(shadowSource.bodyY, shadowSource.bodyHeight, placement, spacing), root.dpr)
}
}
}
Item {
id: contentWrapper
width: parent.width
height: parent.height
opacity: Theme.isDirectionalEffect ? 1 : (shouldBeVisible ? 1 : 0)
visible: opacity > 0
scale: aligner.isRollOut ? 1.0 : contentContainer.scaleValue
x: Theme.snap(contentContainer.animX + (parent.width - width) * (1 - scale) * 0.5, root.dpr)
y: Theme.snap(contentContainer.animY + (parent.height - height) * (1 - scale) * 0.5, root.dpr)
layer.enabled: contentWrapper.opacity < 1
layer.smooth: false
layer.textureSize: root.dpr > 1 ? Qt.size(Math.ceil(width * root.dpr), Math.ceil(height * root.dpr)) : Qt.size(0, 0)
Behavior on opacity {
enabled: !Theme.isDirectionalEffect
NumberAnimation {
duration: Math.round(Theme.variantDuration(animationDuration, shouldBeVisible) * Theme.variantOpacityDurationScale)
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
}
}
Item {
anchors.fill: parent
clip: false
visible: !Theme.isConnectedEffect
Rectangle {
anchors.fill: parent
topLeftRadius: contentContainer.surfaceTopLeftRadius
topRightRadius: contentContainer.surfaceTopRightRadius
bottomLeftRadius: contentContainer.surfaceBottomLeftRadius
bottomRightRadius: contentContainer.surfaceBottomRightRadius
color: contentContainer.surfaceColor
border.color: contentContainer.surfaceBorderColor
border.width: contentContainer.surfaceBorderWidth
}
ConnectedCorner {
visible: Theme.isConnectedEffect
barSide: contentContainer.connectedBarSide
placement: "left"
spacing: 0
connectorRadius: Theme.connectedCornerRadius
color: contentContainer.surfaceColor
x: Theme.snap(contentContainer.connectorX(0, contentWrapper.width, placement, spacing), root.dpr)
y: Theme.snap(contentContainer.connectorY(0, contentWrapper.height, placement, spacing), root.dpr)
}
ConnectedCorner {
visible: Theme.isConnectedEffect
barSide: contentContainer.connectedBarSide
placement: "right"
spacing: 0
connectorRadius: Theme.connectedCornerRadius
color: contentContainer.surfaceColor
x: Theme.snap(contentContainer.connectorX(0, contentWrapper.width, placement, spacing), root.dpr)
y: Theme.snap(contentContainer.connectorY(0, contentWrapper.height, placement, spacing), root.dpr)
}
}
Loader {
id: contentLoader
anchors.fill: parent
active: root._primeContent || shouldBeVisible || contentWindow.visible
asynchronous: false
}
} // closes contentWrapper
} // closes unrollCounteract
} // closes aligner
} // closes directionalClipMask
} // closes contentContainer
Item {
id: focusHelper

View File

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

View File

@@ -0,0 +1,69 @@
import QtQuick
import qs.Common
import qs.Services
Item {
id: root
visible: false
required property var targetWindow
property var blurItem: null
property bool blurEnabled: Theme.connectedSurfaceBlurEnabled
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 (!blurEnabled || !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;
}
onBlurEnabledChanged: _apply()
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()
}

View File

@@ -3400,7 +3400,7 @@
},
{
"section": "popupTransparency",
"label": "Popup Transparency",
"label": "Surface Opacity",
"tabIndex": 10,
"category": "Theme & Colors",
"keywords": [
@@ -3418,6 +3418,7 @@
"popup",
"scheme",
"style",
"surface",
"their",
"theme",
"translucent",