1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-05-16 09:12:47 -04:00

Compare commits

...

22 Commits

Author SHA1 Message Date
purian23 cc858c5557 frame(ConnectedMode): Wire up Notifications 2026-04-11 22:12:09 -04:00
purian23 b49730a01b (frame): Update connected mode animation & motion logic 2026-04-11 22:12:09 -04:00
purian23 4335403d19 (frame): implement ConnectedModeState to better handle component sync 2026-04-11 22:12:09 -04:00
purian23 e3483bf88a (frameMode): Restore user settings when exiting frame mode
- Align blur settings in non-FrameMode motion settings
2026-04-11 22:12:09 -04:00
purian23 c29082c6cf (frame): Update connected mode with blur 2026-04-11 22:12:09 -04:00
purian23 771c0f3cff (frame): Update connected mode & opacity connection settings 2026-04-11 22:12:09 -04:00
purian23 2e42cfd7c4 (frameInMotion): Initial Unified Frame Connected Mode 2026-04-11 22:12:09 -04:00
purian23 8d72d86256 Add Directional Motion options 2026-04-11 22:12:09 -04:00
purian23 93f37456b6 Initial staging for Animation & Motion effects 2026-04-11 22:12:09 -04:00
purian23 5f4f1b0b22 (frame): Add blur support & cleanup 2026-04-11 22:12:09 -04:00
purian23 48a4ee1ed5 (frame): Multi-monitor support 2026-04-11 22:12:09 -04:00
purian23 8419c560b1 Connected frames & defaults 2026-04-11 22:12:09 -04:00
purian23 2b2b6f7a55 Continue frame implementation 2026-04-11 22:12:09 -04:00
purian23 117d248cc7 Initial framework 2026-04-11 22:12:09 -04:00
bbedward f61438e11f doctor: fix quickshell regex
fixes #2204
2026-04-11 12:44:58 -04:00
bbedward 8f78163941 dankinstall: workarounds for arch/extra change 2026-04-11 12:24:08 -04:00
mihem f894d338fc feat(running-apps): stronger active app highlight + indicator bar (#2190)
The focused app background used 20% primary opacity which was barely
visible. Increase to 45% to make the active window unambiguous at a glance.
2026-04-11 12:01:21 -04:00
bbedward f2df53afcd colorpicker: re-use Wayland buffer pools 2026-04-09 12:08:43 -04:00
Thomas Kroll 4179fcee83 fix(privacy): detect screen casting on Niri via PipeWire (#2185)
Screen sharing was not detected by PrivacyService on Niri because:

1. Niri creates the screencast as a Stream/Output/Video node, but
   screensharingActive only checked PwNodeType.VideoSource nodes.

2. looksLikeScreencast() only inspected application.name and
   node.name, missing Niri's node which has an empty application.name
   but identifies itself via media.name (niri-screen-cast-src).

Add Stream/Output/Video to the checked media classes and include
media.name in the screencast heuristic. Also add a forward-compatible
check for NiriService.hasActiveCast for when Niri gains cast tracking
in its IPC.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 11:50:39 -04:00
Andrey Yugai a0c9af1ee7 feature: persist last active player (#2184) 2026-04-09 11:30:04 -04:00
Thomas Kroll 049266271a fix(system-update): popout first-click and AUR package listing (#2183)
* fix(system-update): open popout on first click

The SystemUpdate widget required two clicks to open its popout.

On the first click, the LazyLoader was activated but popoutTarget
(bound to the loader's item) was still null in the MouseArea handler,
so setTriggerPosition was never called. The popout's open() then
returned early because screen was unset.

Restructure the onClicked handler to call setTriggerPosition directly
on the loaded item (matching the pattern used by Clock, Clipboard, and
other bar widgets) and use PopoutManager.requestPopout() instead of
toggle() for consistent popout management.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(system-update): include AUR packages in update list

When paru or yay is the package manager, the update list only showed
official repo packages (via checkupdates or -Qu) while the upgrade
command (paru/yay -Syu) also processes AUR packages. This mismatch
meant AUR updates appeared as a surprise during the upgrade.

Combine the repo update listing with the AUR helper's -Qua flag so
both official and AUR packages are shown in the popout before the
user triggers the upgrade. The output format is identical for both
sources, so the existing parser works unchanged.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 10:49:15 -04:00
Ron Harel 0eabda3164 Improve seek and scrub indicator/ animations in the media controls widget. (#2181) 2026-04-09 10:35:56 -04:00
60 changed files with 5323 additions and 868 deletions
+4 -17
View File
@@ -1,26 +1,13 @@
repos: repos:
- repo: local - repo: https://github.com/golangci/golangci-lint
rev: v2.10.1
hooks: hooks:
- id: golangci-lint-fmt - 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 require_serial: true
types: [go]
pass_filenames: false
- id: golangci-lint-full - 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 - id: golangci-lint-config-verify
name: golangci-lint-config-verify - repo: local
entry: go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.11.3 config verify hooks:
language: system
files: \.golangci\.(?:yml|yaml|toml|json)
pass_filenames: false
- id: go-test - id: go-test
name: go test name: go test
entry: go test ./... entry: go test ./...
+1 -1
View File
@@ -82,7 +82,7 @@ func (ds *DoctorStatus) OKCount() int {
} }
var ( var (
quickshellVersionRegex = regexp.MustCompile(`quickshell (\d+\.\d+\.\d+)`) quickshellVersionRegex = regexp.MustCompile(`(?i)quickshell (\d+\.\d+\.\d+)`)
hyprlandVersionRegex = regexp.MustCompile(`v?(\d+\.\d+\.\d+)`) hyprlandVersionRegex = regexp.MustCompile(`v?(\d+\.\d+\.\d+)`)
niriVersionRegex = regexp.MustCompile(`niri (\d+\.\d+)`) niriVersionRegex = regexp.MustCompile(`niri (\d+\.\d+)`)
swayVersionRegex = regexp.MustCompile(`sway version (\d+\.\d+)`) swayVersionRegex = regexp.MustCompile(`sway version (\d+\.\d+)`)
+53 -50
View File
@@ -39,11 +39,10 @@ type LayerSurface struct {
wlSurface *client.Surface wlSurface *client.Surface
layerSurf *wlr_layer_shell.ZwlrLayerSurfaceV1 layerSurf *wlr_layer_shell.ZwlrLayerSurfaceV1
viewport *wp_viewporter.WpViewport viewport *wp_viewporter.WpViewport
wlPool *client.ShmPool wlPools [2]*client.ShmPool
wlBuffer *client.Buffer wlBuffers [2]*client.Buffer
bufferBusy bool slotBusy [2]bool
oldPool *client.ShmPool needsRedraw bool
oldBuffer *client.Buffer
scopyBuffer *client.Buffer scopyBuffer *client.Buffer
configured bool configured bool
hidden bool hidden bool
@@ -136,6 +135,7 @@ func (p *Picker) Run() (*Color, error) {
break break
} }
p.flushRedraws()
p.checkDone() p.checkDone()
} }
@@ -164,6 +164,15 @@ func (p *Picker) checkDone() {
} }
} }
func (p *Picker) flushRedraws() {
for _, ls := range p.surfaces {
if !ls.needsRedraw {
continue
}
p.redrawSurface(ls)
}
}
func (p *Picker) connect() error { func (p *Picker) connect() error {
display, err := client.Connect("") display, err := client.Connect("")
if err != nil { if err != nil {
@@ -507,47 +516,45 @@ func (p *Picker) captureForSurface(ls *LayerSurface) {
} }
func (p *Picker) redrawSurface(ls *LayerSurface) { func (p *Picker) redrawSurface(ls *LayerSurface) {
slot := ls.state.FrontIndex()
if ls.slotBusy[slot] {
ls.needsRedraw = true
return
}
var renderBuf *ShmBuffer var renderBuf *ShmBuffer
if ls.hidden { switch {
case ls.hidden:
renderBuf = ls.state.RedrawScreenOnly() renderBuf = ls.state.RedrawScreenOnly()
} else { default:
renderBuf = ls.state.Redraw() renderBuf = ls.state.Redraw()
} }
if renderBuf == nil { if renderBuf == nil {
return return
} }
if ls.oldBuffer != nil { ls.needsRedraw = false
ls.oldBuffer.Destroy()
ls.oldBuffer = nil if ls.wlPools[slot] == nil {
} pool, err := p.shm.CreatePool(renderBuf.Fd(), int32(renderBuf.Size()))
if ls.oldPool != nil { if err != nil {
ls.oldPool.Destroy() return
ls.oldPool = nil }
ls.wlPools[slot] = pool
wlBuffer, err := pool.CreateBuffer(0, int32(renderBuf.Width), int32(renderBuf.Height), int32(renderBuf.Stride), uint32(ls.state.ScreenFormat()))
if err != nil {
return
}
ls.wlBuffers[slot] = wlBuffer
s := slot
wlBuffer.SetReleaseHandler(func(e client.BufferReleaseEvent) {
ls.slotBusy[s] = false
})
} }
ls.oldPool = ls.wlPool ls.slotBusy[slot] = true
ls.oldBuffer = ls.wlBuffer
ls.wlPool = nil
ls.wlBuffer = nil
pool, err := p.shm.CreatePool(renderBuf.Fd(), int32(renderBuf.Size()))
if err != nil {
return
}
ls.wlPool = pool
wlBuffer, err := pool.CreateBuffer(0, int32(renderBuf.Width), int32(renderBuf.Height), int32(renderBuf.Stride), uint32(ls.state.ScreenFormat()))
if err != nil {
return
}
ls.wlBuffer = wlBuffer
lsRef := ls
wlBuffer.SetReleaseHandler(func(e client.BufferReleaseEvent) {
lsRef.bufferBusy = false
})
ls.bufferBusy = true
logicalW, logicalH := ls.state.LogicalSize() logicalW, logicalH := ls.state.LogicalSize()
if logicalW == 0 || logicalH == 0 { if logicalW == 0 || logicalH == 0 {
@@ -566,7 +573,7 @@ func (p *Picker) redrawSurface(ls *LayerSurface) {
} }
_ = ls.wlSurface.SetBufferScale(bufferScale) _ = ls.wlSurface.SetBufferScale(bufferScale)
} }
_ = ls.wlSurface.Attach(wlBuffer, 0, 0) _ = ls.wlSurface.Attach(ls.wlBuffers[slot], 0, 0)
_ = ls.wlSurface.Damage(0, 0, int32(logicalW), int32(logicalH)) _ = ls.wlSurface.Damage(0, 0, int32(logicalW), int32(logicalH))
_ = ls.wlSurface.Commit() _ = ls.wlSurface.Commit()
@@ -634,7 +641,7 @@ func (p *Picker) setupPointerHandlers() {
} }
p.activeSurface.state.OnPointerMotion(e.SurfaceX, e.SurfaceY) p.activeSurface.state.OnPointerMotion(e.SurfaceX, e.SurfaceY)
p.redrawSurface(p.activeSurface) p.activeSurface.needsRedraw = true
}) })
p.pointer.SetLeaveHandler(func(e client.PointerLeaveEvent) { p.pointer.SetLeaveHandler(func(e client.PointerLeaveEvent) {
@@ -655,7 +662,7 @@ func (p *Picker) setupPointerHandlers() {
return return
} }
p.activeSurface.state.OnPointerMotion(e.SurfaceX, e.SurfaceY) p.activeSurface.state.OnPointerMotion(e.SurfaceX, e.SurfaceY)
p.redrawSurface(p.activeSurface) p.activeSurface.needsRedraw = true
}) })
p.pointer.SetButtonHandler(func(e client.PointerButtonEvent) { p.pointer.SetButtonHandler(func(e client.PointerButtonEvent) {
@@ -679,17 +686,13 @@ func (p *Picker) cleanup() {
if ls.scopyBuffer != nil { if ls.scopyBuffer != nil {
ls.scopyBuffer.Destroy() ls.scopyBuffer.Destroy()
} }
if ls.oldBuffer != nil { for i := range ls.wlBuffers {
ls.oldBuffer.Destroy() if ls.wlBuffers[i] != nil {
} ls.wlBuffers[i].Destroy()
if ls.oldPool != nil { }
ls.oldPool.Destroy() if ls.wlPools[i] != nil {
} ls.wlPools[i].Destroy()
if ls.wlBuffer != nil { }
ls.wlBuffer.Destroy()
}
if ls.wlPool != nil {
ls.wlPool.Destroy()
} }
if ls.viewport != nil { if ls.viewport != nil {
ls.viewport.Destroy() ls.viewport.Destroy()
+6
View File
@@ -274,6 +274,12 @@ func (s *SurfaceState) FrontRenderBuffer() *ShmBuffer {
return s.renderBufs[s.front] return s.renderBufs[s.front]
} }
func (s *SurfaceState) FrontIndex() int {
s.mu.Lock()
defer s.mu.Unlock()
return s.front
}
func (s *SurfaceState) SwapBuffers() { func (s *SurfaceState) SwapBuffers() {
s.mu.Lock() s.mu.Lock()
s.front ^= 1 s.front ^= 1
+41
View File
@@ -324,6 +324,13 @@ func (a *ArchDistribution) InstallPackages(ctx context.Context, dependencies []d
systemPkgs, aurPkgs, manualPkgs, variantMap := a.categorizePackages(dependencies, wm, reinstallFlags, disabledFlags) systemPkgs, aurPkgs, manualPkgs, variantMap := a.categorizePackages(dependencies, wm, reinstallFlags, disabledFlags)
if slices.Contains(aurPkgs, "quickshell-git") && slices.Contains(systemPkgs, "dms-shell") {
if err := a.preinstallQuickshellGit(ctx, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to preinstall quickshell-git: %w", err)
}
aurPkgs = slices.DeleteFunc(aurPkgs, func(p string) bool { return p == "quickshell-git" })
}
// Phase 3: System Packages // Phase 3: System Packages
if len(systemPkgs) > 0 { if len(systemPkgs) > 0 {
progressChan <- InstallProgressMsg{ progressChan <- InstallProgressMsg{
@@ -441,6 +448,37 @@ func (a *ArchDistribution) categorizePackages(dependencies []deps.Dependency, wm
return systemPkgs, aurPkgs, manualPkgs, variantMap return systemPkgs, aurPkgs, manualPkgs, variantMap
} }
func (a *ArchDistribution) preinstallQuickshellGit(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
if a.packageInstalled("quickshell-git") {
return nil
}
if a.packageInstalled("quickshell") {
progressChan <- InstallProgressMsg{
Phase: PhaseAURPackages,
Progress: 0.15,
Step: "Removing stable quickshell...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: "sudo pacman -Rdd --noconfirm quickshell",
LogOutput: "Removing stable quickshell so quickshell-git can be installed",
}
cmd := ExecSudoCommand(ctx, sudoPassword, "pacman -Rdd --noconfirm quickshell")
if err := a.runWithProgress(cmd, progressChan, PhaseAURPackages, 0.15, 0.18); err != nil {
return fmt.Errorf("failed to remove stable quickshell: %w", err)
}
}
progressChan <- InstallProgressMsg{
Phase: PhaseAURPackages,
Progress: 0.18,
Step: "Building quickshell-git before system packages...",
IsComplete: false,
CommandInfo: "Installing quickshell-git ahead of dms-shell to avoid conflict",
}
return a.installSingleAURPackage(ctx, "quickshell-git", sudoPassword, progressChan, 0.18, 0.32)
}
func (a *ArchDistribution) installSystemPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error { func (a *ArchDistribution) installSystemPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
if len(packages) == 0 { if len(packages) == 0 {
return nil return nil
@@ -449,6 +487,9 @@ func (a *ArchDistribution) installSystemPackages(ctx context.Context, packages [
a.log(fmt.Sprintf("Installing system packages: %s", strings.Join(packages, ", "))) a.log(fmt.Sprintf("Installing system packages: %s", strings.Join(packages, ", ")))
args := []string{"pacman", "-S", "--needed", "--noconfirm"} args := []string{"pacman", "-S", "--needed", "--noconfirm"}
if slices.Contains(packages, "dms-shell") {
args = append(args, "--assume-installed", "dms-shell-compositor=1")
}
args = append(args, packages...) args = append(args, packages...)
progressChan <- InstallProgressMsg{ progressChan <- InstallProgressMsg{
+179
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;
}
}
}
+5
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 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 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] 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]
} }
+204
View File
@@ -0,0 +1,204 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
Singleton {
id: root
readonly property var emptyDockState: ({
"reveal": false,
"barSide": "bottom",
"bodyX": 0,
"bodyY": 0,
"bodyW": 0,
"bodyH": 0,
"slideX": 0,
"slideY": 0
})
// Popout state (updated by DankPopout when connectedFrameModeActive)
property string popoutOwnerId: ""
property bool popoutVisible: false
property string popoutBarSide: "top"
property real popoutBodyX: 0
property real popoutBodyY: 0
property real popoutBodyW: 0
property real popoutBodyH: 0
property real popoutAnimX: 0
property real popoutAnimY: 0
property string popoutScreen: ""
// Dock state (updated by Dock when connectedFrameModeActive), keyed by screen.name
property var dockStates: ({})
// Dock slide offsets — hot-path updates separated from full geometry state
property var dockSlides: ({})
function hasPopoutOwner(claimId) {
return !!claimId && popoutOwnerId === claimId;
}
function claimPopout(claimId, state) {
if (!claimId)
return false;
popoutOwnerId = claimId;
return updatePopout(claimId, state);
}
function updatePopout(claimId, state) {
if (!hasPopoutOwner(claimId) || !state)
return false;
if (state.visible !== undefined)
popoutVisible = !!state.visible;
if (state.barSide !== undefined)
popoutBarSide = state.barSide || "top";
if (state.bodyX !== undefined)
popoutBodyX = Number(state.bodyX);
if (state.bodyY !== undefined)
popoutBodyY = Number(state.bodyY);
if (state.bodyW !== undefined)
popoutBodyW = Number(state.bodyW);
if (state.bodyH !== undefined)
popoutBodyH = Number(state.bodyH);
if (state.animX !== undefined)
popoutAnimX = Number(state.animX);
if (state.animY !== undefined)
popoutAnimY = Number(state.animY);
if (state.screen !== undefined)
popoutScreen = state.screen || "";
return true;
}
function releasePopout(claimId) {
if (!hasPopoutOwner(claimId))
return false;
popoutOwnerId = "";
popoutVisible = false;
popoutBarSide = "top";
popoutBodyX = 0;
popoutBodyY = 0;
popoutBodyW = 0;
popoutBodyH = 0;
popoutAnimX = 0;
popoutAnimY = 0;
popoutScreen = "";
return true;
}
function _cloneDockStates() {
const next = {};
for (const screenName in dockStates)
next[screenName] = dockStates[screenName];
return next;
}
function _normalizeDockState(state) {
return {
"reveal": !!(state && state.reveal),
"barSide": state && state.barSide ? state.barSide : "bottom",
"bodyX": Number(state && state.bodyX !== undefined ? state.bodyX : 0),
"bodyY": Number(state && state.bodyY !== undefined ? state.bodyY : 0),
"bodyW": Number(state && state.bodyW !== undefined ? state.bodyW : 0),
"bodyH": Number(state && state.bodyH !== undefined ? state.bodyH : 0),
"slideX": Number(state && state.slideX !== undefined ? state.slideX : 0),
"slideY": Number(state && state.slideY !== undefined ? state.slideY : 0)
};
}
function setDockState(screenName, state) {
if (!screenName || !state)
return false;
const next = _cloneDockStates();
next[screenName] = _normalizeDockState(state);
dockStates = next;
return true;
}
function clearDockState(screenName) {
if (!screenName || !dockStates[screenName])
return false;
const next = _cloneDockStates();
delete next[screenName];
dockStates = next;
// Also clear corresponding slide
if (dockSlides[screenName]) {
const nextSlides = {};
for (const k in dockSlides)
nextSlides[k] = dockSlides[k];
delete nextSlides[screenName];
dockSlides = nextSlides;
}
return true;
}
function setDockSlide(screenName, x, y) {
if (!screenName)
return false;
const next = {};
for (const k in dockSlides)
next[k] = dockSlides[k];
next[screenName] = { "x": Number(x), "y": Number(y) };
dockSlides = next;
return true;
}
// ─── Notification state (per screen, updated by NotificationSurface) ──────
readonly property var emptyNotificationState: ({
"visible": false,
"barSide": "top",
"bodyX": 0,
"bodyY": 0,
"bodyW": 0,
"bodyH": 0
})
property var notificationStates: ({})
function _cloneNotificationStates() {
const next = {};
for (const screenName in notificationStates)
next[screenName] = notificationStates[screenName];
return next;
}
function _normalizeNotificationState(state) {
return {
"visible": !!(state && state.visible),
"barSide": state && state.barSide ? state.barSide : "top",
"bodyX": Number(state && state.bodyX !== undefined ? state.bodyX : 0),
"bodyY": Number(state && state.bodyY !== undefined ? state.bodyY : 0),
"bodyW": Number(state && state.bodyW !== undefined ? state.bodyW : 0),
"bodyH": Number(state && state.bodyH !== undefined ? state.bodyH : 0)
};
}
function setNotificationState(screenName, state) {
if (!screenName || !state)
return false;
const next = _cloneNotificationStates();
next[screenName] = _normalizeNotificationState(state);
notificationStates = next;
return true;
}
function clearNotificationState(screenName) {
if (!screenName || !notificationStates[screenName])
return false;
const next = _cloneNotificationStates();
delete next[screenName];
notificationStates = next;
return true;
}
}
+10 -1
View File
@@ -13,8 +13,13 @@ Item {
property color targetColor: "white" property color targetColor: "white"
property real targetRadius: Theme.cornerRadius 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 color borderColor: "transparent"
property real borderWidth: 0 property real borderWidth: 0
property bool useCustomSource: false
property bool shadowEnabled: Theme.elevationEnabled property bool shadowEnabled: Theme.elevationEnabled
property real shadowBlurPx: level && level.blurPx !== undefined ? level.blurPx : 0 property real shadowBlurPx: level && level.blurPx !== undefined ? level.blurPx : 0
@@ -46,7 +51,11 @@ Item {
Rectangle { Rectangle {
id: sourceRect id: sourceRect
anchors.fill: parent anchors.fill: parent
radius: root.targetRadius visible: !root.useCustomSource
topLeftRadius: root.topLeftRadius
topRightRadius: root.topRightRadius
bottomLeftRadius: root.bottomLeftRadius
bottomRightRadius: root.bottomRightRadius
color: root.targetColor color: root.targetColor
border.color: root.borderColor border.color: root.borderColor
border.width: root.borderWidth border.width: root.borderWidth
+2
View File
@@ -124,6 +124,8 @@ Singleton {
property string vpnLastConnected: "" property string vpnLastConnected: ""
property string lastPlayerIdentity: ""
property var deviceMaxVolumes: ({}) property var deviceMaxVolumes: ({})
property var hiddenOutputDeviceNames: [] property var hiddenOutputDeviceNames: []
property var hiddenInputDeviceNames: [] property var hiddenInputDeviceNames: []
+289 -11
View File
@@ -14,7 +14,7 @@ import "settings/SettingsStore.js" as Store
Singleton { Singleton {
id: root id: root
readonly property int settingsConfigVersion: 5 readonly property int settingsConfigVersion: 11
readonly property bool isGreeterMode: Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true" readonly property bool isGreeterMode: Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true"
@@ -37,6 +37,18 @@ Singleton {
Custom 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 { enum SuspendBehavior {
Suspend, Suspend,
Hibernate, Hibernate,
@@ -168,6 +180,12 @@ Singleton {
property int modalCustomAnimationDuration: 150 property int modalCustomAnimationDuration: 150
property bool enableRippleEffects: true property bool enableRippleEffects: true
onEnableRippleEffectsChanged: saveSettings() 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 property bool m3ElevationEnabled: true
onM3ElevationEnabledChanged: saveSettings() onM3ElevationEnabledChanged: saveSettings()
property int m3ElevationIntensity: 12 property int m3ElevationIntensity: 12
@@ -186,6 +204,7 @@ Singleton {
onPopoutElevationEnabledChanged: saveSettings() onPopoutElevationEnabledChanged: saveSettings()
property bool barElevationEnabled: true property bool barElevationEnabled: true
onBarElevationEnabledChanged: saveSettings() onBarElevationEnabledChanged: saveSettings()
property bool blurEnabled: false property bool blurEnabled: false
onBlurEnabledChanged: saveSettings() onBlurEnabledChanged: saveSettings()
property string blurBorderColor: "outline" property string blurBorderColor: "outline"
@@ -198,6 +217,50 @@ Singleton {
property bool blurredWallpaperLayer: false property bool blurredWallpaperLayer: false
property bool blurWallpaperOnOverview: false property bool blurWallpaperOnOverview: false
property bool frameEnabled: false
onFrameEnabledChanged: saveSettings()
property real frameThickness: 16
onFrameThicknessChanged: saveSettings()
property real frameRounding: 23
onFrameRoundingChanged: saveSettings()
property string frameColor: ""
onFrameColorChanged: saveSettings()
property real frameOpacity: 1.0
onFrameOpacityChanged: saveSettings()
property var frameScreenPreferences: ["all"]
onFrameScreenPreferencesChanged: saveSettings()
property real frameBarSize: 40
onFrameBarSizeChanged: saveSettings()
property bool frameShowOnOverview: false
onFrameShowOnOverviewChanged: saveSettings()
property bool frameBlurEnabled: true
onFrameBlurEnabledChanged: saveSettings()
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 showLauncherButton: true
property bool showWorkspaceSwitcher: true property bool showWorkspaceSwitcher: true
property bool showFocusedWindow: true property bool showFocusedWindow: true
@@ -1285,6 +1348,7 @@ Singleton {
_loading = false; _loading = false;
} }
loadPluginSettings(); loadPluginSettings();
Qt.callLater(() => _reconcileConnectedFrameBarStyles());
} }
property var _pendingMigration: null property var _pendingMigration: null
@@ -1398,6 +1462,149 @@ Singleton {
pluginSettingsFile.setText(JSON.stringify(pluginSettings, null, 2)); 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() { function detectAvailableIconThemes() {
const xdgDataDirs = Quickshell.env("XDG_DATA_DIRS") || ""; const xdgDataDirs = Quickshell.env("XDG_DATA_DIRS") || "";
const localData = Paths.strip(StandardPaths.writableLocation(StandardPaths.GenericDataLocation)); const localData = Paths.strip(StandardPaths.writableLocation(StandardPaths.GenericDataLocation));
@@ -1545,35 +1752,37 @@ Singleton {
const spacing = barSpacing !== undefined ? barSpacing : (defaultBar?.spacing ?? 4); const spacing = barSpacing !== undefined ? barSpacing : (defaultBar?.spacing ?? 4);
const position = barPosition !== undefined ? barPosition : (defaultBar?.position ?? SettingsData.Position.Top); 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 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 useAutoGaps = (barConfig && barConfig.popupGapsAuto !== undefined) ? barConfig.popupGapsAuto : (defaultBar?.popupGapsAuto ?? true);
const manualGapValue = (barConfig && barConfig.popupGapsManual !== undefined) ? barConfig.popupGapsManual : (defaultBar?.popupGapsManual ?? 4); 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) { switch (position) {
case SettingsData.Position.Left: case SettingsData.Position.Left:
return { return {
"x": barThickness + spacing + popupGap, "x": barThickness + edgeSpacing + popupGap,
"y": relativeY, "y": relativeY,
"width": widgetWidth "width": widgetWidth
}; };
case SettingsData.Position.Right: case SettingsData.Position.Right:
return { return {
"x": (screen?.width || 0) - (barThickness + spacing + popupGap), "x": (screen?.width || 0) - (barThickness + edgeSpacing + popupGap),
"y": relativeY, "y": relativeY,
"width": widgetWidth "width": widgetWidth
}; };
case SettingsData.Position.Bottom: case SettingsData.Position.Bottom:
return { return {
"x": relativeX, "x": relativeX,
"y": (screen?.height || 0) - (barThickness + spacing + bottomGap + popupGap), "y": (screen?.height || 0) - (barThickness + edgeSpacing + bottomGap + popupGap),
"width": widgetWidth "width": widgetWidth
}; };
default: default:
return { return {
"x": relativeX, "x": relativeX,
"y": barThickness + spacing + bottomGap + popupGap, "y": barThickness + edgeSpacing + bottomGap + popupGap,
"width": widgetWidth "width": widgetWidth
}; };
} }
@@ -1667,7 +1876,9 @@ Singleton {
const screenWidth = screen.width; const screenWidth = screen.width;
const screenHeight = screen.height; const screenHeight = screen.height;
const position = barPosition !== undefined ? barPosition : (defaultBar?.position ?? SettingsData.Position.Top); 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 topOffset = 0;
let bottomOffset = 0; let bottomOffset = 0;
@@ -1689,7 +1900,7 @@ Singleton {
const otherSpacing = other.spacing !== undefined ? other.spacing : (defaultBar?.spacing ?? 4); const otherSpacing = other.spacing !== undefined ? other.spacing : (defaultBar?.spacing ?? 4);
const otherPadding = other.innerPadding !== undefined ? other.innerPadding : (defaultBar?.innerPadding ?? 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 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) { switch (other.position) {
case SettingsData.Position.Top: case SettingsData.Position.Top:
@@ -1780,7 +1991,9 @@ Singleton {
function addBarConfig(config) { function addBarConfig(config) {
const configs = JSON.parse(JSON.stringify(barConfigs)); const configs = JSON.parse(JSON.stringify(barConfigs));
configs.push(config); configs.push(config);
barConfigs = configs; if (connectedFrameModeActive)
_captureConnectedFrameBarStyleBackups(configs, false);
barConfigs = _sanitizeBarConfigsForConnectedFrame(configs).configs;
updateBarConfigs(); updateBarConfigs();
} }
@@ -1792,7 +2005,7 @@ Singleton {
const positionChanged = updates.position !== undefined && configs[index].position !== updates.position; const positionChanged = updates.position !== undefined && configs[index].position !== updates.position;
Object.assign(configs[index], updates); Object.assign(configs[index], updates);
barConfigs = configs; barConfigs = _sanitizeBarConfigsForConnectedFrame(configs).configs;
updateBarConfigs(); updateBarConfigs();
if (positionChanged) { if (positionChanged) {
@@ -1846,6 +2059,11 @@ Singleton {
return; return;
const configs = barConfigs.filter(cfg => cfg.id !== barId); const configs = barConfigs.filter(cfg => cfg.id !== barId);
barConfigs = configs; barConfigs = configs;
if (connectedFrameBarStyleBackups?.[barId] !== undefined) {
const nextBackups = JSON.parse(JSON.stringify(connectedFrameBarStyleBackups || {}));
delete nextBackups[barId];
connectedFrameBarStyleBackups = nextBackups;
}
updateBarConfigs(); updateBarConfigs();
} }
@@ -1940,6 +2158,66 @@ Singleton {
return filtered; return filtered;
} }
function getFrameFilteredScreens() {
var prefs = frameScreenPreferences || ["all"];
if (!prefs || prefs.length === 0 || prefs.includes("all")) {
return Quickshell.screens;
}
return Quickshell.screens.filter(screen => isScreenInPreferences(screen, prefs));
}
function getActiveBarEdgeForScreen(screen) {
if (!screen) return "";
for (var i = 0; i < barConfigs.length; i++) {
var bc = barConfigs[i];
if (!bc.enabled) continue;
var prefs = bc.screenPreferences || ["all"];
if (!prefs.includes("all") && !isScreenInPreferences(screen, prefs)) continue;
switch (bc.position ?? 0) {
case SettingsData.Position.Top: return "top";
case SettingsData.Position.Bottom: return "bottom";
case SettingsData.Position.Left: return "left";
case SettingsData.Position.Right: return "right";
}
}
return "";
}
function getActiveBarEdgesForScreen(screen) {
if (!screen) return [];
var edges = [];
for (var i = 0; i < barConfigs.length; i++) {
var bc = barConfigs[i];
if (!bc.enabled) continue;
var prefs = bc.screenPreferences || ["all"];
if (!prefs.includes("all") && !isScreenInPreferences(screen, prefs)) continue;
switch (bc.position ?? 0) {
case SettingsData.Position.Top: edges.push("top"); break;
case SettingsData.Position.Bottom: edges.push("bottom"); break;
case SettingsData.Position.Left: edges.push("left"); break;
case SettingsData.Position.Right: edges.push("right"); break;
}
}
return edges;
}
function getActiveBarThicknessForScreen(screen) {
if (frameEnabled) return frameBarSize;
if (!screen) return frameThickness;
for (var i = 0; i < barConfigs.length; i++) {
var bc = barConfigs[i];
if (!bc.enabled) continue;
var prefs = bc.screenPreferences || ["all"];
if (!prefs.includes("all") && !isScreenInPreferences(screen, prefs)) continue;
const innerPadding = bc.innerPadding ?? 4;
const barT = Math.max(26 + innerPadding * 0.6, Theme.barHeight - 4 - (8 - innerPadding));
const spacing = bc.spacing ?? 4;
const bottomGap = bc.bottomGap ?? 0;
return barT + spacing + bottomGap;
}
return frameThickness;
}
function sendTestNotifications() { function sendTestNotifications() {
NotificationService.dismissAllPopups(); NotificationService.dismissAllPopups();
sendTestNotification(0); sendTestNotification(0);
+47 -1
View File
@@ -960,6 +960,40 @@ Singleton {
"expressiveEffects": [0.34, 0.8, 0.34, 1, 1, 1] "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.connectedFrameModeActive ? 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: { readonly property var animationPresetDurations: {
"none": 0, "none": 0,
"short": 250, "short": 250,
@@ -1125,7 +1159,13 @@ Singleton {
property real iconSizeLarge: 32 property real iconSizeLarge: 32
property real panelTransparency: 0.85 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() { function screenTransition() {
if (CompositorService.isNiri) { if (CompositorService.isNiri) {
@@ -1824,6 +1864,12 @@ Singleton {
return Qt.rgba(c.r, c.g, c.b, a); 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) { function blendAlpha(c, a) {
return Qt.rgba(c.r, c.g, c.b, c.a * a); return Qt.rgba(c.r, c.g, c.b, c.a * a);
} }
@@ -75,6 +75,8 @@ var SPEC = {
vpnLastConnected: { def: "" }, vpnLastConnected: { def: "" },
lastPlayerIdentity: { def: "" },
deviceMaxVolumes: { def: {} }, deviceMaxVolumes: { def: {} },
hiddenOutputDeviceNames: { def: [] }, hiddenOutputDeviceNames: { def: [] },
hiddenInputDeviceNames: { def: [] }, hiddenInputDeviceNames: { def: [] },
+16 -1
View File
@@ -49,6 +49,10 @@ var SPEC = {
modalAnimationSpeed: { def: 1 }, modalAnimationSpeed: { def: 1 },
modalCustomAnimationDuration: { def: 150 }, modalCustomAnimationDuration: { def: 150 },
enableRippleEffects: { def: true }, enableRippleEffects: { def: true },
animationVariant: { def: 0 },
motionEffect: { def: 0 },
directionalAnimationMode: { def: 0 },
previousDirectionalMode: { def: 1 },
m3ElevationEnabled: { def: true }, m3ElevationEnabled: { def: true },
m3ElevationIntensity: { def: 12 }, m3ElevationIntensity: { def: 12 },
m3ElevationOpacity: { def: 30 }, m3ElevationOpacity: { def: 30 },
@@ -443,6 +447,7 @@ var SPEC = {
displayProfileAutoSelect: { def: false }, displayProfileAutoSelect: { def: false },
displayShowDisconnected: { def: false }, displayShowDisconnected: { def: false },
displaySnapToEdge: { def: true }, displaySnapToEdge: { def: true },
connectedFrameBarStyleBackups: { def: {} },
barConfigs: { barConfigs: {
def: [{ def: [{
@@ -549,7 +554,17 @@ var SPEC = {
clipboardEnterToPaste: { def: false }, clipboardEnterToPaste: { def: false },
launcherPluginVisibility: { def: {} }, launcherPluginVisibility: { def: {} },
launcherPluginOrder: { def: [] } launcherPluginOrder: { def: [] },
frameEnabled: { def: false },
frameThickness: { def: 16 },
frameRounding: { def: 23 },
frameColor: { def: "" },
frameOpacity: { def: 1.0 },
frameScreenPreferences: { def: ["all"] },
frameBarSize: { def: 40 },
frameShowOnOverview: { def: false },
frameBlurEnabled: { def: true }
}; };
function getValidKeys() { function getValidKeys() {
@@ -248,6 +248,10 @@ function migrateToVersion(obj, targetVersion) {
settings.configVersion = 6; settings.configVersion = 6;
} }
if (currentVersion < 11) {
settings.configVersion = 11;
}
return settings; return settings;
} }
+3
View File
@@ -21,6 +21,7 @@ import qs.Modules.OSD
import qs.Modules.ProcessList import qs.Modules.ProcessList
import qs.Modules.DankBar import qs.Modules.DankBar
import qs.Modules.DankBar.Popouts import qs.Modules.DankBar.Popouts
import qs.Modules.Frame
import qs.Modules.WorkspaceOverlays import qs.Modules.WorkspaceOverlays
import qs.Services import qs.Services
@@ -176,6 +177,8 @@ Item {
} }
} }
Frame {}
Repeater { Repeater {
id: dankBarRepeater id: dankBarRepeater
model: ScriptModel { model: ScriptModel {
@@ -64,11 +64,19 @@ DankModal {
activeImageLoads = 0; activeImageLoads = 0;
shouldHaveFocus = true; shouldHaveFocus = true;
ClipboardService.reset(); ClipboardService.reset();
if (clipboardAvailable)
ClipboardService.refresh();
keyboardController.reset(); keyboardController.reset();
Qt.callLater(function () { Qt.callLater(function () {
if (clipboardAvailable) {
if (Theme.isConnectedEffect) {
Qt.callLater(() => {
if (clipboardHistoryModal.shouldBeVisible)
ClipboardService.refresh();
});
} else {
ClipboardService.refresh();
}
}
if (contentLoader.item?.searchField) { if (contentLoader.item?.searchField) {
contentLoader.item.searchField.text = ""; contentLoader.item.searchField.text = "";
contentLoader.item.searchField.forceActiveFocus(); contentLoader.item.searchField.forceActiveFocus();
@@ -53,8 +53,6 @@ DankPopout {
open(); open();
activeImageLoads = 0; activeImageLoads = 0;
ClipboardService.reset(); ClipboardService.reset();
if (clipboardAvailable)
ClipboardService.refresh();
keyboardController.reset(); keyboardController.reset();
Qt.callLater(function () { Qt.callLater(function () {
@@ -121,8 +119,16 @@ DankPopout {
onShouldBeVisibleChanged: { onShouldBeVisibleChanged: {
if (!shouldBeVisible) if (!shouldBeVisible)
return; return;
if (clipboardAvailable) if (clipboardAvailable) {
ClipboardService.refresh(); if (Theme.isConnectedEffect) {
Qt.callLater(() => {
if (root.shouldBeVisible)
ClipboardService.refresh();
});
} else {
ClipboardService.refresh();
}
}
keyboardController.reset(); keyboardController.reset();
Qt.callLater(function () { Qt.callLater(function () {
if (contentLoader.item?.searchField) { if (contentLoader.item?.searchField) {
+196 -81
View File
@@ -26,15 +26,22 @@ Item {
property bool closeOnEscapeKey: true property bool closeOnEscapeKey: true
property bool closeOnBackgroundClick: true property bool closeOnBackgroundClick: true
property string animationType: "scale" property string animationType: "scale"
property int animationDuration: Theme.modalAnimationDuration readonly property bool connectedMotionParity: Theme.isConnectedEffect
property real animationScaleCollapsed: 0.96 property int animationDuration: connectedMotionParity ? Theme.popoutAnimationDuration : Theme.modalAnimationDuration
property real animationOffset: Theme.spacingL property real animationScaleCollapsed: Theme.effectScaleCollapsed
property list<real> animationEnterCurve: Theme.expressiveCurves.expressiveDefaultSpatial property real animationOffset: Theme.effectAnimOffset
property list<real> animationExitCurve: Theme.expressiveCurves.emphasized property list<real> animationEnterCurve: connectedMotionParity ? Theme.variantPopoutEnterCurve : Theme.variantModalEnterCurve
property color backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) property list<real> animationExitCurve: connectedMotionParity ? Theme.variantPopoutExitCurve : Theme.variantModalExitCurve
property color backgroundColor: Theme.surfaceContainer
property color borderColor: Theme.outlineMedium property color borderColor: Theme.outlineMedium
property real borderWidth: 0 property real borderWidth: 0
property real cornerRadius: Theme.cornerRadius property real cornerRadius: Theme.cornerRadius
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 bool enableShadow: true
property alias modalFocusScope: focusScope property alias modalFocusScope: focusScope
property bool shouldBeVisible: false property bool shouldBeVisible: false
@@ -45,11 +52,13 @@ Item {
property bool keepPopoutsOpen: false property bool keepPopoutsOpen: false
property var customKeyboardFocus: null property var customKeyboardFocus: null
property bool useOverlayLayer: false property bool useOverlayLayer: false
property real frozenMotionOffsetX: 0
property real frozenMotionOffsetY: 0
readonly property alias contentWindow: contentWindow readonly property alias contentWindow: contentWindow
readonly property alias clickCatcher: clickCatcher readonly property alias clickCatcher: clickCatcher
readonly property bool useHyprlandFocusGrab: CompositorService.useHyprlandFocusGrab readonly property bool useHyprlandFocusGrab: CompositorService.useHyprlandFocusGrab
readonly property bool useBackground: showBackground && SettingsData.modalDarkenBackground readonly property bool useBackground: showBackground && SettingsData.modalDarkenBackground
readonly property bool useSingleWindow: CompositorService.isHyprland || useBackground readonly property bool useSingleWindow: CompositorService.isHyprland
signal opened signal opened
signal dialogClosed signal dialogClosed
@@ -59,33 +68,34 @@ Item {
function open() { function open() {
closeTimer.stop(); closeTimer.stop();
const focusedScreen = CompositorService.getFocusedScreen(); animationsEnabled = false;
const screenChanged = focusedScreen && contentWindow.screen !== focusedScreen; frozenMotionOffsetX = modalContainer ? modalContainer.offsetX : 0;
if (focusedScreen) { frozenMotionOffsetY = modalContainer ? modalContainer.offsetY : animationOffset;
if (screenChanged)
contentWindow.visible = false;
contentWindow.screen = focusedScreen;
if (!useSingleWindow) {
if (screenChanged)
clickCatcher.visible = false;
clickCatcher.screen = focusedScreen;
}
}
if (screenChanged) {
Qt.callLater(() => root._finishOpen());
} else {
_finishOpen();
}
}
function _finishOpen() { 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); ModalManager.openModal(root);
shouldBeVisible = true;
if (!useSingleWindow) Qt.callLater(() => {
clickCatcher.visible = true; animationsEnabled = true;
contentWindow.visible = true; shouldBeVisible = true;
shouldHaveFocus = false; if (!useSingleWindow && !clickCatcher.visible)
Qt.callLater(() => shouldHaveFocus = Qt.binding(() => shouldBeVisible)); clickCatcher.visible = true;
if (!contentWindow.visible)
contentWindow.visible = true;
shouldHaveFocus = false;
Qt.callLater(() => shouldHaveFocus = Qt.binding(() => shouldBeVisible));
});
} }
function close() { function close() {
@@ -146,7 +156,7 @@ Item {
Timer { Timer {
id: closeTimer id: closeTimer
interval: animationDuration + 50 interval: Theme.variantCloseInterval(animationDuration)
onTriggered: { onTriggered: {
if (shouldBeVisible) if (shouldBeVisible)
return; return;
@@ -160,7 +170,19 @@ Item {
readonly property var shadowLevel: Theme.elevationLevel3 readonly property var shadowLevel: Theme.elevationLevel3
readonly property real shadowFallbackOffset: 6 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 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 shadowBuffer: Theme.snap(shadowRenderPadding + shadowMotionPadding, dpr)
readonly property real alignedWidth: Theme.px(modalWidth, dpr) readonly property real alignedWidth: Theme.px(modalWidth, dpr)
readonly property real alignedHeight: Theme.px(modalHeight, dpr) readonly property real alignedHeight: Theme.px(modalHeight, dpr)
@@ -220,9 +242,26 @@ Item {
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
enabled: root.closeOnBackgroundClick && root.shouldBeVisible enabled: !root.useSingleWindow && root.closeOnBackgroundClick && root.shouldBeVisible
onClicked: root.backgroundClicked() 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 { PanelWindow {
@@ -232,12 +271,13 @@ Item {
WindowBlur { WindowBlur {
targetWindow: contentWindow targetWindow: contentWindow
blurEnabled: root.effectiveBlurEnabled
readonly property real s: Math.min(1, modalContainer.scaleValue) readonly property real s: Math.min(1, modalContainer.scaleValue)
blurX: modalContainer.x + modalContainer.width * (1 - s) * 0.5 + Theme.snap(modalContainer.animX, root.dpr) 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) blurY: modalContainer.y + modalContainer.height * (1 - s) * 0.5 + Theme.snap(modalContainer.animY, root.dpr)
blurWidth: (shouldBeVisible && animatedContent.opacity > 0) ? modalContainer.width * s : 0 blurWidth: (root.shouldBeVisible && animatedContent.opacity > 0) ? modalContainer.width * s : 0
blurHeight: (shouldBeVisible && animatedContent.opacity > 0) ? modalContainer.height * s : 0 blurHeight: (root.shouldBeVisible && animatedContent.opacity > 0) ? modalContainer.height * s : 0
blurRadius: root.cornerRadius blurRadius: root.effectiveCornerRadius
} }
WlrLayershell.namespace: root.layerNamespace WlrLayershell.namespace: root.layerNamespace
@@ -275,9 +315,12 @@ Item {
bottom: root.useSingleWindow 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 { WlrLayershell.margins {
left: root.useSingleWindow ? 0 : Math.max(0, Theme.snap(root.alignedX - shadowBuffer, dpr)) left: actualMarginLeft
top: root.useSingleWindow ? 0 : Math.max(0, Theme.snap(root.alignedY - shadowBuffer, dpr)) top: actualMarginTop
right: 0 right: 0
bottom: 0 bottom: 0
} }
@@ -307,13 +350,14 @@ Item {
anchors.fill: parent anchors.fill: parent
z: -1 z: -1
color: "black" color: "black"
opacity: root.useBackground ? (root.shouldBeVisible ? root.backgroundOpacity : 0) : 0 opacity: (root.useSingleWindow && root.useBackground) ? (root.shouldBeVisible ? root.backgroundOpacity : 0) : 0
visible: root.useBackground visible: opacity > 0
Behavior on opacity { Behavior on opacity {
enabled: root.animationsEnabled enabled: root.animationsEnabled && (!Theme.isDirectionalEffect || Theme.isConnectedEffect)
DankAnim { NumberAnimation {
duration: root.animationDuration duration: Math.round(Theme.variantDuration(root.animationDuration, root.shouldBeVisible) * Theme.variantOpacityDurationScale)
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
} }
} }
@@ -321,8 +365,8 @@ Item {
Item { Item {
id: modalContainer id: modalContainer
x: root.useSingleWindow ? root.alignedX : shadowBuffer x: (root.useSingleWindow ? root.alignedX : (root.alignedX - contentWindow.actualMarginLeft)) + Theme.snap(animX, root.dpr)
y: root.useSingleWindow ? root.alignedY : shadowBuffer y: (root.useSingleWindow ? root.alignedY : (root.alignedY - contentWindow.actualMarginTop)) + Theme.snap(animY, root.dpr)
width: root.alignedWidth width: root.alignedWidth
height: root.alignedHeight height: root.alignedHeight
@@ -338,45 +382,117 @@ Item {
} }
readonly property bool slide: root.animationType === "slide" readonly property bool slide: root.animationType === "slide"
readonly property real offsetX: slide ? 15 : 0 readonly property bool directionalEffect: Theme.isDirectionalEffect
readonly property real offsetY: slide ? -30 : root.animationOffset readonly property bool depthEffect: Theme.isDepthEffect
readonly property real directionalTravel: Math.max(root.animationOffset, Math.max(root.alignedWidth, root.alignedHeight) * 0.8)
property real animX: 0 readonly property real depthTravel: Math.max(root.animationOffset * 0.8, 36)
property real animY: 0 readonly property real customAnchorX: root.alignedX + root.alignedWidth * 0.5
property real scaleValue: root.animationScaleCollapsed readonly property real customAnchorY: root.alignedY + root.alignedHeight * 0.5
readonly property real customDistLeft: customAnchorX
onOffsetXChanged: animX = Theme.snap(root.shouldBeVisible ? 0 : offsetX, root.dpr) readonly property real customDistRight: root.screenWidth - customAnchorX
onOffsetYChanged: animY = Theme.snap(root.shouldBeVisible ? 0 : offsetY, root.dpr) readonly property real customDistTop: customAnchorY
readonly property real customDistBottom: root.screenHeight - customAnchorY
Connections { readonly property real offsetX: {
target: root if (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 && Theme.isDirectionalEffect)
function onShouldBeVisibleChanged() { return 0;
modalContainer.animX = Theme.snap(root.shouldBeVisible ? 0 : modalContainer.offsetX, root.dpr); if (slide && !directionalEffect && !depthEffect)
modalContainer.animY = Theme.snap(root.shouldBeVisible ? 0 : modalContainer.offsetY, root.dpr); return 15;
modalContainer.scaleValue = root.shouldBeVisible ? 1.0 : root.animationScaleCollapsed; 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 { Behavior on animX {
enabled: root.animationsEnabled enabled: root.animationsEnabled
DankAnim { NumberAnimation {
duration: root.animationDuration duration: Theme.variantDuration(root.animationDuration, root.shouldBeVisible)
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
} }
} }
Behavior on animY { Behavior on animY {
enabled: root.animationsEnabled enabled: root.animationsEnabled
DankAnim { NumberAnimation {
duration: root.animationDuration duration: Theme.variantDuration(root.animationDuration, root.shouldBeVisible)
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
} }
} }
Behavior on scaleValue { Behavior on scaleValue {
enabled: root.animationsEnabled enabled: root.animationsEnabled
DankAnim { NumberAnimation {
duration: root.animationDuration duration: Theme.variantDuration(root.animationDuration, root.shouldBeVisible)
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
} }
} }
@@ -392,15 +508,14 @@ Item {
id: animatedContent id: animatedContent
anchors.fill: parent anchors.fill: parent
clip: false clip: false
opacity: root.shouldBeVisible ? 1 : 0 opacity: (Theme.isDirectionalEffect && !Theme.isConnectedEffect) ? 1 : (root.shouldBeVisible ? 1 : 0)
scale: modalContainer.scaleValue scale: modalContainer.scaleValue
x: Theme.snap(modalContainer.animX, root.dpr) + (parent.width - width) * (1 - modalContainer.scaleValue) * 0.5 transformOrigin: Item.Center
y: Theme.snap(modalContainer.animY, root.dpr) + (parent.height - height) * (1 - modalContainer.scaleValue) * 0.5
Behavior on opacity { Behavior on opacity {
enabled: root.animationsEnabled enabled: root.animationsEnabled && (!Theme.isDirectionalEffect || Theme.isConnectedEffect)
NumberAnimation { NumberAnimation {
duration: animationDuration duration: Math.round(Theme.variantDuration(animationDuration, root.shouldBeVisible) * Theme.variantOpacityDurationScale)
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
} }
@@ -411,19 +526,19 @@ Item {
anchors.fill: parent anchors.fill: parent
level: root.shadowLevel level: root.shadowLevel
fallbackOffset: root.shadowFallbackOffset fallbackOffset: root.shadowFallbackOffset
targetRadius: root.cornerRadius targetRadius: root.effectiveCornerRadius
targetColor: root.backgroundColor targetColor: root.effectiveBackgroundColor
borderColor: root.borderColor borderColor: root.effectiveBorderColor
borderWidth: root.borderWidth borderWidth: root.effectiveBorderWidth
shadowEnabled: root.enableShadow && Theme.elevationEnabled && SettingsData.modalElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1" shadowEnabled: root.enableShadow && Theme.elevationEnabled && SettingsData.modalElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1"
} }
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
radius: root.cornerRadius radius: root.effectiveCornerRadius
color: "transparent" color: "transparent"
border.color: BlurService.borderColor border.color: root.connectedSurfaceOverride ? "transparent" : BlurService.borderColor
border.width: BlurService.borderWidth border.width: root.connectedSurfaceOverride ? 0 : BlurService.borderWidth
z: 100 z: 100
} }
@@ -1,4 +1,5 @@
import QtQuick import QtQuick
import QtQuick.Effects
import Quickshell import Quickshell
import Quickshell.Wayland import Quickshell.Wayland
import Quickshell.Hyprland import Quickshell.Hyprland
@@ -14,16 +15,24 @@ Item {
property bool spotlightOpen: false property bool spotlightOpen: false
property bool keyboardActive: false property bool keyboardActive: false
property bool contentVisible: false property bool contentVisible: false
readonly property bool launcherMotionVisible: Theme.isConnectedEffect ? _motionActive : (Theme.isDirectionalEffect ? spotlightOpen : _motionActive)
property var spotlightContent: launcherContentLoader.item property var spotlightContent: launcherContentLoader.item
property bool openedFromOverview: false property bool openedFromOverview: false
property bool isClosing: false property bool isClosing: false
property bool _windowEnabled: true
property bool _pendingInitialize: false property bool _pendingInitialize: false
property string _pendingQuery: "" property string _pendingQuery: ""
property string _pendingMode: "" property string _pendingMode: ""
readonly property bool unloadContentOnClose: SettingsData.dankLauncherV2UnloadOnClose 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 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 screenWidth: effectiveScreen?.width ?? 1920
readonly property real screenHeight: effectiveScreen?.height ?? 1080 readonly property real screenHeight: effectiveScreen?.height ?? 1080
readonly property real dpr: effectiveScreen ? CompositorService.getScreenScale(effectiveScreen) : 1 readonly property real dpr: effectiveScreen ? CompositorService.getScreenScale(effectiveScreen) : 1
@@ -57,8 +66,12 @@ Item {
readonly property real modalX: (screenWidth - modalWidth) / 2 readonly property real modalX: (screenWidth - modalWidth) / 2
readonly property real modalY: (screenHeight - modalHeight) / 2 readonly property real modalY: (screenHeight - modalHeight) / 2
readonly property color backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) readonly property bool connectedSurfaceOverride: Theme.isConnectedEffect
readonly property real cornerRadius: Theme.cornerRadius readonly property int launcherAnimationDuration: Theme.isConnectedEffect ? Theme.popoutAnimationDuration : Theme.modalAnimationDuration
readonly property list<real> launcherEnterCurve: Theme.isConnectedEffect ? Theme.variantPopoutEnterCurve : Theme.variantModalEnterCurve
readonly property list<real> launcherExitCurve: Theme.isConnectedEffect ? Theme.variantPopoutExitCurve : Theme.variantModalExitCurve
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: { readonly property color borderColor: {
if (!SettingsData.dankLauncherV2BorderEnabled) if (!SettingsData.dankLauncherV2BorderEnabled)
return Theme.outlineMedium; return Theme.outlineMedium;
@@ -76,6 +89,37 @@ Item {
} }
} }
readonly property int borderWidth: SettingsData.dankLauncherV2BorderEnabled ? SettingsData.dankLauncherV2BorderThickness : 0 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 signal dialogClosed
@@ -96,18 +140,11 @@ Item {
if (!spotlightContent) if (!spotlightContent)
return; return;
contentVisible = true; contentVisible = true;
spotlightContent.searchField.forceActiveFocus(); // NOTE: forceActiveFocus() is deliberately NOT called here.
// It is deferred to after animation starts to avoid compositor IPC stalls.
var targetQuery = "";
if (query) {
targetQuery = query;
} else if (SettingsData.rememberLastQuery) {
targetQuery = SessionData.launcherLastQuery || "";
}
if (spotlightContent.searchField) { if (spotlightContent.searchField) {
spotlightContent.searchField.text = targetQuery; spotlightContent.searchField.text = query;
} }
if (spotlightContent.controller) { if (spotlightContent.controller) {
var targetMode = mode || SessionData.launcherLastMode || "all"; var targetMode = mode || SessionData.launcherLastMode || "all";
@@ -122,10 +159,12 @@ Item {
spotlightContent.controller.collapsedSections = {}; spotlightContent.controller.collapsedSections = {};
spotlightContent.controller.selectedFlatIndex = 0; spotlightContent.controller.selectedFlatIndex = 0;
spotlightContent.controller.selectedItem = null; spotlightContent.controller.selectedItem = null;
spotlightContent.controller.historyIndex = -1; if (query) {
spotlightContent.controller.searchQuery = targetQuery; spotlightContent.controller.setSearchQuery(query);
} else {
spotlightContent.controller.performSearch(); spotlightContent.controller.searchQuery = "";
spotlightContent.controller.performSearch();
}
} }
if (spotlightContent.resetScroll) { if (spotlightContent.resetScroll) {
spotlightContent.resetScroll(); spotlightContent.resetScroll();
@@ -135,47 +174,59 @@ Item {
} }
} }
function _finishShow(query, mode) { function _openCommon(query, mode) {
spotlightOpen = true; closeCleanupTimer.stop();
isClosing = false; isClosing = false;
openedFromOverview = false; openedFromOverview = false;
keyboardActive = true; // Disable animations so the snap is instant
animationsEnabled = false;
// 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); ModalManager.openModal(root);
spotlightOpen = true;
backgroundWindow.visible = true;
contentWindow.visible = true;
if (useHyprlandFocusGrab) if (useHyprlandFocusGrab)
focusGrab.active = true; focusGrab.active = true;
// Load content and initialize (but no forceActiveFocus — that's deferred)
_ensureContentLoadedAndInitialize(query || "", mode || ""); _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() { function show() {
closeCleanupTimer.stop(); _openCommon("", "");
var focusedScreen = CompositorService.getFocusedScreen();
if (focusedScreen && launcherWindow.screen !== focusedScreen) {
spotlightOpen = false;
isClosing = false;
launcherWindow.screen = focusedScreen;
Qt.callLater(() => root._finishShow("", ""));
return;
}
_finishShow("", "");
} }
function showWithQuery(query) { function showWithQuery(query) {
closeCleanupTimer.stop(); _openCommon(query, "");
var focusedScreen = CompositorService.getFocusedScreen();
if (focusedScreen && launcherWindow.screen !== focusedScreen) {
spotlightOpen = false;
isClosing = false;
launcherWindow.screen = focusedScreen;
Qt.callLater(() => root._finishShow(query, ""));
return;
}
_finishShow(query, "");
} }
function hide() { function hide() {
@@ -183,13 +234,17 @@ Item {
return; return;
openedFromOverview = false; openedFromOverview = false;
isClosing = true; 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; keyboardActive = false;
spotlightOpen = false; spotlightOpen = false;
focusGrab.active = false; focusGrab.active = false;
ModalManager.closeModal(root); ModalManager.closeModal(root);
closeCleanupTimer.start(); closeCleanupTimer.start();
} }
@@ -198,27 +253,7 @@ Item {
} }
function showWithMode(mode) { function showWithMode(mode) {
closeCleanupTimer.stop(); _openCommon("", mode);
var focusedScreen = CompositorService.getFocusedScreen();
if (focusedScreen && launcherWindow.screen !== focusedScreen) {
spotlightOpen = false;
isClosing = false;
launcherWindow.screen = focusedScreen;
Qt.callLater(() => root._finishShow("", mode));
return;
}
spotlightOpen = true;
isClosing = false;
openedFromOverview = false;
keyboardActive = true;
ModalManager.openModal(root);
if (useHyprlandFocusGrab)
focusGrab.active = true;
_ensureContentLoadedAndInitialize("", mode);
} }
function toggleWithMode(mode) { function toggleWithMode(mode) {
@@ -239,10 +274,13 @@ Item {
Timer { Timer {
id: closeCleanupTimer id: closeCleanupTimer
interval: Theme.modalAnimationDuration + 50 interval: Theme.variantCloseInterval(root.launcherAnimationDuration)
repeat: false repeat: false
onTriggered: { onTriggered: {
isClosing = false; isClosing = false;
contentVisible = false;
contentWindow.visible = false;
backgroundWindow.visible = false;
if (root.unloadContentOnClose) if (root.unloadContentOnClose)
launcherContentLoader.active = false; launcherContentLoader.active = false;
dialogClosed(); dialogClosed();
@@ -251,7 +289,6 @@ Item {
Connections { Connections {
target: spotlightContent?.controller ?? null target: spotlightContent?.controller ?? null
function onModeChanged(mode) { function onModeChanged(mode) {
if (spotlightContent.controller.autoSwitchedToFiles) if (spotlightContent.controller.autoSwitchedToFiles)
return; return;
@@ -261,7 +298,7 @@ Item {
HyprlandFocusGrab { HyprlandFocusGrab {
id: focusGrab id: focusGrab
windows: [launcherWindow] windows: [contentWindow]
active: false active: false
onCleared: { onCleared: {
@@ -286,55 +323,53 @@ Item {
if (Quickshell.screens.length === 0) if (Quickshell.screens.length === 0)
return; return;
const screenName = launcherWindow.screen?.name; const screen = contentWindow.screen;
if (screenName) { const screenName = screen?.name;
let needsReset = !screen || !screenName;
if (!needsReset) {
needsReset = true;
for (let i = 0; i < Quickshell.screens.length; i++) { for (let i = 0; i < Quickshell.screens.length; i++) {
if (Quickshell.screens[i].name === screenName) if (Quickshell.screens[i].name === screenName) {
return; needsReset = false;
break;
}
} }
} }
if (spotlightOpen) if (!needsReset)
hide(); return;
const newScreen = CompositorService.getFocusedScreen() ?? Quickshell.screens[0]; const newScreen = CompositorService.getFocusedScreen() ?? Quickshell.screens[0];
if (newScreen) if (!newScreen)
launcherWindow.screen = 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 { PanelWindow {
id: launcherWindow id: backgroundWindow
visible: spotlightOpen || isClosing visible: false
color: "transparent" color: "transparent"
exclusionMode: ExclusionMode.Ignore
WindowBlur { WlrLayershell.namespace: "dms:spotlight:bg"
targetWindow: launcherWindow WlrLayershell.layer: WlrLayershell.Top
readonly property real s: Math.min(1, modalContainer.scale) WlrLayershell.exclusiveZone: -1
blurX: root.modalX + root.modalWidth * (1 - s) * 0.5 WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
blurY: root.modalY + root.modalHeight * (1 - s) * 0.5
blurWidth: (contentVisible && modalContainer.opacity > 0) ? root.modalWidth * s : 0
blurHeight: (contentVisible && modalContainer.opacity > 0) ? root.modalHeight * s : 0
blurRadius: root.cornerRadius
}
WlrLayershell.namespace: "dms:spotlight" WlrLayershell.margins {
WlrLayershell.layer: { top: contentContainer.dockTop ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 0 ? Theme.px(42, root.dpr) : 0)
switch (Quickshell.env("DMS_MODAL_LAYER")) { bottom: contentContainer.dockBottom ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 1 ? Theme.px(42, root.dpr) : 0)
case "bottom": left: contentContainer.dockLeft ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 2 ? Theme.px(42, root.dpr) : 0)
console.error("DankModal: 'bottom' layer is not valid for modals. Defaulting to 'top' layer."); right: contentContainer.dockRight ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 3 ? Theme.px(42, root.dpr) : 0)
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.keyboardFocus: keyboardActive ? (root.useHyprlandFocusGrab ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.Exclusive) : WlrKeyboardFocus.None
anchors { anchors {
top: true top: true
@@ -344,11 +379,11 @@ Item {
} }
mask: Region { mask: Region {
item: spotlightOpen ? fullScreenMask : null item: (spotlightOpen || isClosing) ? bgFullScreenMask : null
} }
Item { Item {
id: fullScreenMask id: bgFullScreenMask
anchors.fill: parent anchors.fill: parent
} }
@@ -356,13 +391,14 @@ Item {
id: backgroundDarken id: backgroundDarken
anchors.fill: parent anchors.fill: parent
color: "black" color: "black"
opacity: contentVisible && SettingsData.modalDarkenBackground ? 0.5 : 0 opacity: launcherMotionVisible && SettingsData.modalDarkenBackground ? 0.5 : 0
visible: contentVisible || opacity > 0 visible: launcherMotionVisible || opacity > 0
Behavior on opacity { Behavior on opacity {
enabled: root.animationsEnabled && (!Theme.isDirectionalEffect || Theme.isConnectedEffect)
DankAnim { DankAnim {
duration: Theme.modalAnimationDuration duration: Math.round(Theme.variantDuration(root.launcherAnimationDuration, launcherMotionVisible) * Theme.variantOpacityDurationScale)
easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized easing.bezierCurve: launcherMotionVisible ? root.launcherEnterCurve : root.launcherExitCurve
} }
} }
} }
@@ -370,96 +406,236 @@ Item {
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
enabled: spotlightOpen enabled: spotlightOpen
onClicked: mouse => { onClicked: root.hide()
var contentX = modalContainer.x; }
var contentY = modalContainer.y; }
var contentW = modalContainer.width;
var contentH = modalContainer.height;
if (mouse.x < contentX || mouse.x > contentX + contentW || mouse.y < contentY || mouse.y > contentY + contentH) { // ── Content window: SMALL, positioned with margins — only renders the modal area ──
root.hide(); 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 { Item {
id: modalContainer id: contentInputMask
x: root.modalX visible: false
y: root.modalY x: contentContainer.x + contentWrapper.x
width: root.modalWidth y: contentContainer.y + contentWrapper.y
height: root.modalHeight width: root.alignedWidth
visible: contentVisible || opacity > 0 height: root.alignedHeight
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;
}
}
Rectangle {
anchors.fill: parent
radius: root.cornerRadius
color: "transparent"
border.color: BlurService.borderColor
border.width: BlurService.borderWidth
}
} }
}
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);
}
// Declarative bindings — snap applied at render layer (contentWrapper x/y)
property real animX: root._motionActive ? 0 : root._frozenMotionX
property real animY: root._motionActive ? 0 : root._frozenMotionY
property real 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(root.launcherAnimationDuration, root._motionActive)
easing.bezierCurve: root._motionActive ? root.launcherEnterCurve : root.launcherExitCurve
}
}
Behavior on animY {
enabled: root.animationsEnabled
DankAnim {
duration: Theme.variantDuration(root.launcherAnimationDuration, root._motionActive)
easing.bezierCurve: root._motionActive ? root.launcherEnterCurve : root.launcherExitCurve
}
}
Behavior on scaleValue {
enabled: root.animationsEnabled && (!Theme.isDirectionalEffect || (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2))
DankAnim {
duration: Theme.variantDuration(root.launcherAnimationDuration, root._motionActive)
easing.bezierCurve: root._motionActive ? root.launcherEnterCurve : root.launcherExitCurve
}
}
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(root.launcherAnimationDuration, launcherMotionVisible) * Theme.variantOpacityDurationScale)
easing.bezierCurve: launcherMotionVisible ? root.launcherEnterCurve : root.launcherExitCurve
}
}
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
} }
@@ -41,7 +41,6 @@ FocusScope {
editCommentField.text = existing?.comment || ""; editCommentField.text = existing?.comment || "";
editEnvVarsField.text = existing?.envVars || ""; editEnvVarsField.text = existing?.envVars || "";
editExtraFlagsField.text = existing?.extraFlags || ""; editExtraFlagsField.text = existing?.extraFlags || "";
editDgpuToggle.checked = existing?.launchOnDgpu || false;
editMode = true; editMode = true;
Qt.callLater(() => editNameField.forceActiveFocus()); Qt.callLater(() => editNameField.forceActiveFocus());
} }
@@ -65,8 +64,6 @@ FocusScope {
override.envVars = editEnvVarsField.text.trim(); override.envVars = editEnvVarsField.text.trim();
if (editExtraFlagsField.text.trim()) if (editExtraFlagsField.text.trim())
override.extraFlags = editExtraFlagsField.text.trim(); override.extraFlags = editExtraFlagsField.text.trim();
if (editDgpuToggle.checked)
override.launchOnDgpu = true;
SessionData.setAppOverride(editAppId, override); SessionData.setAppOverride(editAppId, override);
closeEditMode(); closeEditMode();
} }
@@ -89,7 +86,7 @@ FocusScope {
Controller { Controller {
id: controller id: controller
active: root.parentModal?.spotlightOpen ?? true active: root.parentModal ? (root.parentModal.spotlightOpen || root.parentModal.isClosing) : true
viewModeContext: root.viewModeContext viewModeContext: root.viewModeContext
onItemExecuted: { onItemExecuted: {
@@ -149,18 +146,10 @@ FocusScope {
event.accepted = false; event.accepted = false;
return; return;
case Qt.Key_Down: case Qt.Key_Down:
if (hasCtrl) { controller.selectNext();
controller.navigateHistory("down");
} else {
controller.selectNext();
}
return; return;
case Qt.Key_Up: case Qt.Key_Up:
if (hasCtrl) { controller.selectPrevious();
controller.navigateHistory("up");
} else {
controller.selectPrevious();
}
return; return;
case Qt.Key_PageDown: case Qt.Key_PageDown:
controller.selectPageDown(8); controller.selectPageDown(8);
@@ -169,10 +158,6 @@ FocusScope {
controller.selectPageUp(8); controller.selectPageUp(8);
return; return;
case Qt.Key_Right: case Qt.Key_Right:
if (hasCtrl) {
controller.cycleMode();
return;
}
if (controller.getCurrentSectionViewMode() !== "list") { if (controller.getCurrentSectionViewMode() !== "list") {
controller.selectRight(); controller.selectRight();
return; return;
@@ -180,25 +165,12 @@ FocusScope {
event.accepted = false; event.accepted = false;
return; return;
case Qt.Key_Left: case Qt.Key_Left:
if (hasCtrl) {
const reverse = true;
controller.cycleMode(reverse);
return;
}
if (controller.getCurrentSectionViewMode() !== "list") { if (controller.getCurrentSectionViewMode() !== "list") {
controller.selectLeft(); controller.selectLeft();
return; return;
} }
event.accepted = false; event.accepted = false;
return; return;
case Qt.Key_H:
if (hasCtrl) {
const reverse = true;
controller.cycleMode(reverse);
return;
}
event.accepted = false;
return;
case Qt.Key_J: case Qt.Key_J:
if (hasCtrl) { if (hasCtrl) {
controller.selectNext(); controller.selectNext();
@@ -213,13 +185,6 @@ FocusScope {
} }
event.accepted = false; event.accepted = false;
return; return;
case Qt.Key_L:
if (hasCtrl) {
controller.cycleMode();
return;
}
event.accepted = false;
return;
case Qt.Key_N: case Qt.Key_N:
if (hasCtrl) { if (hasCtrl) {
controller.selectNextSection(); controller.selectNextSection();
@@ -235,19 +200,13 @@ FocusScope {
event.accepted = false; event.accepted = false;
return; return;
case Qt.Key_Tab: case Qt.Key_Tab:
if (hasCtrl && actionPanel.hasActions) { if (actionPanel.hasActions) {
actionPanel.expanded ? actionPanel.cycleAction() : actionPanel.show(); actionPanel.expanded ? actionPanel.cycleAction() : actionPanel.show();
return;
} }
controller.selectNext();
return; return;
case Qt.Key_Backtab: case Qt.Key_Backtab:
if (hasCtrl && actionPanel.expanded) { if (actionPanel.expanded)
const reverse = true; actionPanel.hide();
actionPanel.expanded ? actionPanel.cycleAction(reverse) : actionPanel.show();
return;
}
controller.selectPrevious();
return; return;
case Qt.Key_Return: case Qt.Key_Return:
case Qt.Key_Enter: case Qt.Key_Enter:
@@ -311,7 +270,7 @@ FocusScope {
Item { Item {
anchors.fill: parent anchors.fill: parent
visible: !editMode && !(root.parentModal?.isClosing ?? false) visible: !editMode
Item { Item {
id: footerBar id: footerBar
@@ -429,7 +388,7 @@ FocusScope {
StyledText { StyledText {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: "Ctrl-Tab " + I18n.tr("actions") text: "Tab " + I18n.tr("actions")
font.pixelSize: Theme.fontSizeSmall - 1 font.pixelSize: Theme.fontSizeSmall - 1
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
visible: actionPanel.hasActions visible: actionPanel.hasActions
@@ -503,7 +462,7 @@ FocusScope {
showClearButton: true showClearButton: true
textColor: Theme.surfaceText textColor: Theme.surfaceText
font.pixelSize: Theme.fontSizeLarge font.pixelSize: Theme.fontSizeLarge
enabled: root.parentModal ? root.parentModal.spotlightOpen : true enabled: root.parentModal ? (root.parentModal.spotlightOpen || root.parentModal.isClosing) : true
placeholderText: "" placeholderText: ""
ignoreUpDownKeys: true ignoreUpDownKeys: true
ignoreTabKeys: true ignoreTabKeys: true
@@ -737,6 +696,14 @@ FocusScope {
Item { Item {
width: parent.width width: parent.width
height: parent.height - searchField.height - categoryRow.height - fileFilterRow.height - actionPanel.height - Theme.spacingXS * ((categoryRow.visible ? 1 : 0) + (fileFilterRow.visible ? 1 : 0) + 2) height: parent.height - searchField.height - categoryRow.height - fileFilterRow.height - actionPanel.height - Theme.spacingXS * ((categoryRow.visible ? 1 : 0) + (fileFilterRow.visible ? 1 : 0) + 2)
opacity: {
if (!root.parentModal)
return 1;
if (Theme.isDirectionalEffect && root.parentModal.isClosing)
return 1;
return root.parentModal.isClosing ? 0 : 1;
}
ResultsList { ResultsList {
id: resultsList id: resultsList
anchors.fill: parent anchors.fill: parent
@@ -769,7 +736,6 @@ FocusScope {
} }
function onSearchQueryRequested(query) { function onSearchQueryRequested(query) {
searchField.text = query; searchField.text = query;
searchField.cursorPosition = query.length;
} }
function onModeChanged() { function onModeChanged() {
extFilterField.text = ""; extFilterField.text = "";
@@ -980,15 +946,6 @@ FocusScope {
keyNavigationBacktab: editEnvVarsField 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
}
} }
} }
@@ -518,5 +518,20 @@ FocusScope {
Qt.callLater(() => item.forceActiveFocus()); Qt.callLater(() => item.forceActiveFocus());
} }
} }
Loader {
id: frameLoader
anchors.fill: parent
active: root.currentIndex === 33
visible: active
focus: active
sourceComponent: FrameTab {}
onActiveChanged: {
if (active && item)
Qt.callLater(() => item.forceActiveFocus());
}
}
} }
} }
@@ -120,6 +120,12 @@ Rectangle {
"text": I18n.tr("Widgets"), "text": I18n.tr("Widgets"),
"icon": "widgets", "icon": "widgets",
"tabIndex": 22 "tabIndex": 22
},
{
"id": "frame",
"text": I18n.tr("Frame"),
"icon": "frame_source",
"tabIndex": 33
} }
] ]
}, },
@@ -8,9 +8,6 @@ DankPopout {
layerNamespace: "dms:app-launcher" layerNamespace: "dms:app-launcher"
readonly property real screenWidth: screen?.width ?? 1920
readonly property real screenHeight: screen?.height ?? 1080
property string _pendingMode: "" property string _pendingMode: ""
property string _pendingQuery: "" property string _pendingQuery: ""
@@ -44,35 +41,8 @@ DankPopout {
openWithQuery(query); openWithQuery(query);
} }
readonly property int _baseWidth: { popupWidth: 560
switch (SettingsData.dankLauncherV2Size) { popupHeight: 640
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)
triggerWidth: 40 triggerWidth: 40
positioning: "" positioning: ""
contentHandlesKeys: contentLoader.item?.launcherContent?.editMode ?? false contentHandlesKeys: contentLoader.item?.launcherContent?.editMode ?? false
@@ -90,7 +60,7 @@ DankPopout {
if (!lc) if (!lc)
return; return;
const query = _pendingQuery || (SettingsData.rememberLastQuery ? SessionData.launcherLastQuery : "") || ""; const query = _pendingQuery;
const mode = _pendingMode || SessionData.appDrawerLastMode || "apps"; const mode = _pendingMode || SessionData.appDrawerLastMode || "apps";
_pendingMode = ""; _pendingMode = "";
_pendingQuery = ""; _pendingQuery = "";
@@ -102,9 +72,12 @@ DankPopout {
if (lc.controller) { if (lc.controller) {
lc.controller.searchMode = mode; lc.controller.searchMode = mode;
lc.controller.pluginFilter = ""; lc.controller.pluginFilter = "";
lc.controller.searchQuery = query; lc.controller.searchQuery = "";
if (query) {
lc.controller.performSearch(); lc.controller.setSearchQuery(query);
} else {
lc.controller.performSearch();
}
} }
lc.resetScroll?.(); lc.resetScroll?.();
lc.actionPanel?.hide(); lc.actionPanel?.hide();
@@ -133,7 +106,7 @@ DankPopout {
QtObject { QtObject {
id: modalAdapter id: modalAdapter
property bool spotlightOpen: appDrawerPopout.shouldBeVisible property bool spotlightOpen: appDrawerPopout.shouldBeVisible
readonly property bool isClosing: !appDrawerPopout.shouldBeVisible property bool isClosing: appDrawerPopout.isClosing
function hide() { function hide() {
appDrawerPopout.close(); appDrawerPopout.close();
@@ -136,9 +136,11 @@ DankPopout {
z: 5000 z: 5000
Behavior on opacity { Behavior on opacity {
enabled: !Theme.isDirectionalEffect
NumberAnimation { NumberAnimation {
duration: 200 duration: Theme.shortDuration
easing.type: Easing.OutCubic easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
} }
} }
} }
+36 -28
View File
@@ -10,6 +10,8 @@ Item {
required property var axis required property var axis
required property var barConfig required property var barConfig
visible: !SettingsData.frameEnabled
anchors.fill: parent anchors.fill: parent
anchors.left: parent.left anchors.left: parent.left
@@ -37,6 +39,8 @@ Item {
} }
property real rt: { property real rt: {
if (SettingsData.frameEnabled)
return SettingsData.frameRounding;
if (barConfig?.squareCorners ?? false) if (barConfig?.squareCorners ?? false)
return 0; return 0;
if (barWindow.hasMaximizedToplevel) if (barWindow.hasMaximizedToplevel)
@@ -255,11 +259,12 @@ Item {
h = h - wing; h = h - wing;
const r = wing; const r = wing;
const cr = rt; const cr = rt;
const crE = SettingsData.frameEnabled ? 0 : cr;
let d = `M ${cr} 0`; let d = `M ${crE} 0`;
d += ` L ${w - cr} 0`; d += ` L ${w - crE} 0`;
if (cr > 0) if (crE > 0)
d += ` A ${cr} ${cr} 0 0 1 ${w} ${cr}`; d += ` A ${crE} ${crE} 0 0 1 ${w} ${crE}`;
if (r > 0) { if (r > 0) {
d += ` L ${w} ${h + r}`; d += ` L ${w} ${h + r}`;
d += ` A ${r} ${r} 0 0 0 ${w - r} ${h}`; d += ` A ${r} ${r} 0 0 0 ${w - r} ${h}`;
@@ -273,9 +278,9 @@ Item {
if (cr > 0) if (cr > 0)
d += ` A ${cr} ${cr} 0 0 1 0 ${h - cr}`; d += ` A ${cr} ${cr} 0 0 1 0 ${h - cr}`;
} }
d += ` L 0 ${cr}`; d += ` L 0 ${crE}`;
if (cr > 0) if (crE > 0)
d += ` A ${cr} ${cr} 0 0 1 ${cr} 0`; d += ` A ${crE} ${crE} 0 0 1 ${crE} 0`;
d += " Z"; d += " Z";
return d; return d;
} }
@@ -285,11 +290,12 @@ Item {
h = h - wing; h = h - wing;
const r = wing; const r = wing;
const cr = rt; const cr = rt;
const crE = SettingsData.frameEnabled ? 0 : cr;
let d = `M ${cr} ${fullH}`; let d = `M ${crE} ${fullH}`;
d += ` L ${w - cr} ${fullH}`; d += ` L ${w - crE} ${fullH}`;
if (cr > 0) if (crE > 0)
d += ` A ${cr} ${cr} 0 0 0 ${w} ${fullH - cr}`; d += ` A ${crE} ${crE} 0 0 0 ${w} ${fullH - crE}`;
if (r > 0) { if (r > 0) {
d += ` L ${w} 0`; d += ` L ${w} 0`;
d += ` A ${r} ${r} 0 0 1 ${w - r} ${r}`; d += ` A ${r} ${r} 0 0 1 ${w - r} ${r}`;
@@ -303,9 +309,9 @@ Item {
if (cr > 0) if (cr > 0)
d += ` A ${cr} ${cr} 0 0 0 0 ${cr}`; d += ` A ${cr} ${cr} 0 0 0 0 ${cr}`;
} }
d += ` L 0 ${fullH - cr}`; d += ` L 0 ${fullH - crE}`;
if (cr > 0) if (crE > 0)
d += ` A ${cr} ${cr} 0 0 0 ${cr} ${fullH}`; d += ` A ${crE} ${crE} 0 0 0 ${crE} ${fullH}`;
d += " Z"; d += " Z";
return d; return d;
} }
@@ -314,11 +320,12 @@ Item {
w = w - wing; w = w - wing;
const r = wing; const r = wing;
const cr = rt; const cr = rt;
const crE = SettingsData.frameEnabled ? 0 : cr;
let d = `M 0 ${cr}`; let d = `M 0 ${crE}`;
d += ` L 0 ${h - cr}`; d += ` L 0 ${h - crE}`;
if (cr > 0) if (crE > 0)
d += ` A ${cr} ${cr} 0 0 0 ${cr} ${h}`; d += ` A ${crE} ${crE} 0 0 0 ${crE} ${h}`;
if (r > 0) { if (r > 0) {
d += ` L ${w + r} ${h}`; d += ` L ${w + r} ${h}`;
d += ` A ${r} ${r} 0 0 1 ${w} ${h - r}`; d += ` A ${r} ${r} 0 0 1 ${w} ${h - r}`;
@@ -332,9 +339,9 @@ Item {
if (cr > 0) if (cr > 0)
d += ` A ${cr} ${cr} 0 0 0 ${w - cr} 0`; d += ` A ${cr} ${cr} 0 0 0 ${w - cr} 0`;
} }
d += ` L ${cr} 0`; d += ` L ${crE} 0`;
if (cr > 0) if (crE > 0)
d += ` A ${cr} ${cr} 0 0 0 0 ${cr}`; d += ` A ${crE} ${crE} 0 0 0 0 ${crE}`;
d += " Z"; d += " Z";
return d; return d;
} }
@@ -344,11 +351,12 @@ Item {
w = w - wing; w = w - wing;
const r = wing; const r = wing;
const cr = rt; const cr = rt;
const crE = SettingsData.frameEnabled ? 0 : cr;
let d = `M ${fullW} ${cr}`; let d = `M ${fullW} ${crE}`;
d += ` L ${fullW} ${h - cr}`; d += ` L ${fullW} ${h - crE}`;
if (cr > 0) if (crE > 0)
d += ` A ${cr} ${cr} 0 0 1 ${fullW - cr} ${h}`; d += ` A ${crE} ${crE} 0 0 1 ${fullW - crE} ${h}`;
if (r > 0) { if (r > 0) {
d += ` L 0 ${h}`; d += ` L 0 ${h}`;
d += ` A ${r} ${r} 0 0 0 ${r} ${h - r}`; d += ` A ${r} ${r} 0 0 0 ${r} ${h - r}`;
@@ -362,9 +370,9 @@ Item {
if (cr > 0) if (cr > 0)
d += ` A ${cr} ${cr} 0 0 1 ${cr} 0`; d += ` A ${cr} ${cr} 0 0 1 ${cr} 0`;
} }
d += ` L ${fullW - cr} 0`; d += ` L ${fullW - crE} 0`;
if (cr > 0) if (crE > 0)
d += ` A ${cr} ${cr} 0 0 1 ${fullW} ${cr}`; d += ` A ${crE} ${crE} 0 0 1 ${fullW} ${crE}`;
d += " Z"; d += " Z";
return d; return d;
} }
+46 -7
View File
@@ -23,6 +23,31 @@ Item {
readonly property real innerPadding: barConfig?.innerPadding ?? 4 readonly property real innerPadding: barConfig?.innerPadding ?? 4
readonly property real outlineThickness: (barConfig?.widgetOutlineEnabled ?? false) ? (barConfig?.widgetOutlineThickness ?? 1) : 0 readonly property real outlineThickness: (barConfig?.widgetOutlineEnabled ?? false) ? (barConfig?.widgetOutlineThickness ?? 1) : 0
readonly property real _frameLeftInset: {
if (!SettingsData.frameEnabled || barWindow.isVertical) return 0
return barWindow.hasAdjacentLeftBar
? SettingsData.frameBarSize
: 0
}
readonly property real _frameRightInset: {
if (!SettingsData.frameEnabled || barWindow.isVertical) return 0
return barWindow.hasAdjacentRightBar
? SettingsData.frameBarSize
: 0
}
readonly property real _frameTopInset: {
if (!SettingsData.frameEnabled || !barWindow.isVertical) return 0
return barWindow.hasAdjacentTopBar
? SettingsData.frameThickness
: 0
}
readonly property real _frameBottomInset: {
if (!SettingsData.frameEnabled || !barWindow.isVertical) return 0
return barWindow.hasAdjacentBottomBar
? SettingsData.frameThickness
: 0
}
property alias hLeftSection: hLeftSection property alias hLeftSection: hLeftSection
property alias hCenterSection: hCenterSection property alias hCenterSection: hCenterSection
property alias hRightSection: hRightSection property alias hRightSection: hRightSection
@@ -31,10 +56,14 @@ Item {
property alias vRightSection: vRightSection property alias vRightSection: vRightSection
anchors.fill: parent anchors.fill: parent
anchors.leftMargin: Math.max(Theme.spacingXS, innerPadding * 0.8) anchors.leftMargin: Math.max(Theme.spacingXS, innerPadding * 0.8) + _frameLeftInset
anchors.rightMargin: Math.max(Theme.spacingXS, innerPadding * 0.8) anchors.rightMargin: Math.max(Theme.spacingXS, innerPadding * 0.8) + _frameRightInset
anchors.topMargin: barWindow.isVertical ? (barWindow.hasAdjacentTopBar ? outlineThickness : Theme.spacingXS) : 0 anchors.topMargin: (barWindow.isVertical
anchors.bottomMargin: barWindow.isVertical ? (barWindow.hasAdjacentBottomBar ? outlineThickness : Theme.spacingXS) : 0 ? (barWindow.hasAdjacentTopBar ? outlineThickness : Theme.spacingXS)
: 0) + _frameTopInset
anchors.bottomMargin: (barWindow.isVertical
? (barWindow.hasAdjacentBottomBar ? outlineThickness : Theme.spacingXS)
: 0) + _frameBottomInset
clip: false clip: false
property int componentMapRevision: 0 property int componentMapRevision: 0
@@ -1156,6 +1185,7 @@ Item {
if (!notificationCenterLoader.item) { if (!notificationCenterLoader.item) {
return; return;
} }
notificationCenterLoader.item.triggerScreen = barWindow.screen;
const effectiveBarConfig = topBarContent.barConfig; const effectiveBarConfig = topBarContent.barConfig;
const barPosition = barWindow.axis?.edge === "left" ? 2 : (barWindow.axis?.edge === "right" ? 3 : (barWindow.axis?.edge === "top" ? 0 : 1)); const barPosition = barWindow.axis?.edge === "left" ? 2 : (barWindow.axis?.edge === "right" ? 3 : (barWindow.axis?.edge === "top" ? 0 : 1));
if (notificationCenterLoader.item.setBarContext) { if (notificationCenterLoader.item.setBarContext) {
@@ -1438,12 +1468,21 @@ Item {
parentScreen: barWindow.screen parentScreen: barWindow.screen
onClicked: { onClicked: {
systemUpdateLoader.active = true; systemUpdateLoader.active = true;
if (!systemUpdateLoader.item)
return;
const popout = systemUpdateLoader.item;
const effectiveBarConfig = topBarContent.barConfig; const effectiveBarConfig = topBarContent.barConfig;
const barPosition = barWindow.axis?.edge === "left" ? 2 : (barWindow.axis?.edge === "right" ? 3 : (barWindow.axis?.edge === "top" ? 0 : 1)); const barPosition = barWindow.axis?.edge === "left" ? 2 : (barWindow.axis?.edge === "right" ? 3 : (barWindow.axis?.edge === "top" ? 0 : 1));
if (systemUpdateLoader.item && systemUpdateLoader.item.setBarContext) { if (popout.setBarContext) {
systemUpdateLoader.item.setBarContext(barPosition, effectiveBarConfig?.bottomGap ?? 0); popout.setBarContext(barPosition, effectiveBarConfig?.bottomGap ?? 0);
} }
systemUpdateLoader.item?.toggle(); if (popout.setTriggerPosition) {
const globalPos = visualContent.mapToItem(null, 0, 0);
const currentScreen = parentScreen || Screen;
const pos = SettingsData.getPopupTriggerPosition(globalPos, currentScreen, barWindow.effectiveBarThickness, visualWidth, effectiveBarConfig?.spacing ?? 4, barPosition, effectiveBarConfig);
popout.setTriggerPosition(pos.x, pos.y, pos.width, section, currentScreen, barPosition, barWindow.effectiveBarThickness, effectiveBarConfig?.spacing ?? 4, effectiveBarConfig);
}
PopoutManager.requestPopout(popout, undefined, "systemUpdate");
} }
} }
} }
+24 -7
View File
@@ -133,6 +133,11 @@ PanelWindow {
teardown(); teardown();
if (!BlurService.enabled || !BlurService.available) if (!BlurService.enabled || !BlurService.available)
return; 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 widgets = barWindow._blurWidgetItems.filter(w => w && w.visible && w.width > 0 && w.height > 0);
const hasBar = barHasTransparency; const hasBar = barHasTransparency;
@@ -187,6 +192,11 @@ PanelWindow {
} }
} }
Connections {
target: SettingsData
function onFrameEnabledChanged() { barBlur.rebuild(); }
}
Connections { Connections {
target: topBarSlide target: topBarSlide
function onXChanged() { function onXChanged() {
@@ -238,7 +248,9 @@ PanelWindow {
readonly property color _surfaceContainer: Theme.surfaceContainer readonly property color _surfaceContainer: Theme.surfaceContainer
readonly property string _barId: barConfig?.id ?? "default" readonly property string _barId: barConfig?.id ?? "default"
property real _backgroundAlpha: barConfig?.transparency ?? 1.0 property real _backgroundAlpha: barConfig?.transparency ?? 1.0
readonly property color _bgColor: Theme.withAlpha(_surfaceContainer, _backgroundAlpha) readonly property color _bgColor: SettingsData.frameEnabled
? Qt.rgba(SettingsData.effectiveFrameColor.r, SettingsData.effectiveFrameColor.g, SettingsData.effectiveFrameColor.b, SettingsData.frameOpacity)
: Theme.withAlpha(_surfaceContainer, _backgroundAlpha)
function _updateBackgroundAlpha() { function _updateBackgroundAlpha() {
const live = SettingsData.barConfigs.find(c => c.id === _barId); const live = SettingsData.barConfigs.find(c => c.id === _barId);
@@ -384,7 +396,7 @@ PanelWindow {
shouldHideForWindows = filtered.length > 0; shouldHideForWindows = filtered.length > 0;
} }
property real effectiveSpacing: hasMaximizedToplevel ? 0 : (barConfig?.spacing ?? 4) property real effectiveSpacing: SettingsData.frameEnabled ? 0 : (hasMaximizedToplevel ? 0 : (barConfig?.spacing ?? 4))
Behavior on effectiveSpacing { Behavior on effectiveSpacing {
enabled: barWindow.visible enabled: barWindow.visible
@@ -395,7 +407,12 @@ PanelWindow {
} }
readonly property int notificationCount: NotificationService.notifications.length readonly property int notificationCount: NotificationService.notifications.length
readonly property real effectiveBarThickness: Theme.snap(Math.max(barWindow.widgetThickness + (barConfig?.innerPadding ?? 4) + 4, Theme.barHeight - 4 - (8 - (barConfig?.innerPadding ?? 4))), _dpr) readonly property real effectiveBarThickness: SettingsData.frameEnabled
? SettingsData.frameBarSize
: Theme.snap(Math.max(barWindow.widgetThickness + (barConfig?.innerPadding ?? 4) + 4, Theme.barHeight - 4 - (8 - (barConfig?.innerPadding ?? 4))), _dpr)
readonly property bool effectiveOpenOnOverview: SettingsData.frameEnabled
? SettingsData.frameShowOnOverview
: (barConfig?.openOnOverview ?? false)
readonly property real widgetThickness: Theme.snap(Math.max(20, 26 + (barConfig?.innerPadding ?? 4) * 0.6), _dpr) readonly property real widgetThickness: Theme.snap(Math.max(20, 26 + (barConfig?.innerPadding ?? 4) * 0.6), _dpr)
readonly property bool hasAdjacentTopBar: { readonly property bool hasAdjacentTopBar: {
@@ -644,14 +661,14 @@ PanelWindow {
anchors.left: !isVertical ? true : (barPos === SettingsData.Position.Left) anchors.left: !isVertical ? true : (barPos === SettingsData.Position.Left)
anchors.right: !isVertical ? true : (barPos === SettingsData.Position.Right) 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 { Item {
id: inputMask id: inputMask
readonly property int barThickness: Theme.px(barWindow.effectiveBarThickness + barWindow.effectiveSpacing, barWindow._dpr) readonly property int barThickness: Theme.px(barWindow.effectiveBarThickness + barWindow.effectiveSpacing, barWindow._dpr)
readonly property bool inOverviewWithShow: CompositorService.isNiri && NiriService.inOverview && (barConfig?.openOnOverview ?? false) readonly property bool inOverviewWithShow: CompositorService.isNiri && NiriService.inOverview && barWindow.effectiveOpenOnOverview
readonly property bool effectiveVisible: (barConfig?.visible ?? true) || inOverviewWithShow readonly property bool effectiveVisible: (barConfig?.visible ?? true) || inOverviewWithShow
readonly property bool showing: effectiveVisible && (topBarCore.reveal || inOverviewWithShow || !topBarCore.autoHide) readonly property bool showing: effectiveVisible && (topBarCore.reveal || inOverviewWithShow || !topBarCore.autoHide)
@@ -792,7 +809,7 @@ PanelWindow {
} }
property bool reveal: { property bool reveal: {
const inOverviewWithShow = CompositorService.isNiri && NiriService.inOverview && (barConfig?.openOnOverview ?? false); const inOverviewWithShow = CompositorService.isNiri && NiriService.inOverview && barWindow.effectiveOpenOnOverview;
if (inOverviewWithShow) if (inOverviewWithShow)
return true; return true;
@@ -889,7 +906,7 @@ PanelWindow {
top: barWindow.isVertical ? parent.top : undefined top: barWindow.isVertical ? parent.top : undefined
bottom: barWindow.isVertical ? parent.bottom : undefined bottom: barWindow.isVertical ? parent.bottom : undefined
} }
readonly property bool inOverview: CompositorService.isNiri && NiriService.inOverview && (barConfig?.openOnOverview ?? false) readonly property bool inOverview: CompositorService.isNiri && NiriService.inOverview && barWindow.effectiveOpenOnOverview
hoverEnabled: (barConfig?.autoHide ?? false) && !inOverview && !topBarCore.hasActivePopout hoverEnabled: (barConfig?.autoHide ?? false) && !inOverview && !topBarCore.hasActivePopout
acceptedButtons: Qt.NoButton acceptedButtons: Qt.NoButton
enabled: (barConfig?.autoHide ?? false) && !inOverview enabled: (barConfig?.autoHide ?? false) && !inOverview
@@ -271,7 +271,7 @@ BasePill {
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: { color: {
if (isFocused) { if (isFocused) {
return mouseArea.containsMouse ? Theme.primarySelected : Theme.withAlpha(Theme.primary, 0.2); return mouseArea.containsMouse ? Theme.primarySelected : Theme.withAlpha(Theme.primary, 0.45);
} }
return mouseArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"; return mouseArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent";
} }
@@ -526,7 +526,7 @@ BasePill {
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: { color: {
if (isFocused) { if (isFocused) {
return mouseArea.containsMouse ? Theme.primarySelected : Theme.withAlpha(Theme.primary, 0.2); return mouseArea.containsMouse ? Theme.primarySelected : Theme.withAlpha(Theme.primary, 0.45);
} }
return mouseArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"; return mouseArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent";
} }
@@ -16,7 +16,6 @@ DankPopout {
popupHeight: contentLoader.item ? contentLoader.item.implicitHeight : 500 popupHeight: contentLoader.item ? contentLoader.item.implicitHeight : 500
triggerWidth: 80 triggerWidth: 80
screen: triggerScreen screen: triggerScreen
shouldBeVisible: dashVisible
property bool __focusArmed: false property bool __focusArmed: false
property bool __contentReady: false property bool __contentReady: false
@@ -100,7 +99,7 @@ DankPopout {
if (currentPlayer && currentPlayer !== player && currentPlayer.canPause) { if (currentPlayer && currentPlayer !== player && currentPlayer.canPause) {
currentPlayer.pause(); currentPlayer.pause();
} }
MprisController.activePlayer = player; MprisController.setActivePlayer(player);
root.__hideDropdowns(); root.__hideDropdowns();
} }
onDeviceSelected: device => { onDeviceSelected: device => {
@@ -44,6 +44,43 @@ Item {
property int __volumeHoverCount: 0 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() { function volumeAreaEntered() {
__volumeHoverCount++; __volumeHoverCount++;
panelEntered(); panelEntered();
@@ -62,30 +99,47 @@ Item {
visible: dropdownType === 1 && volumeAvailable visible: dropdownType === 1 && volumeAvailable
width: 60 width: 60
height: 180 height: 180
x: isRightEdge ? anchorPos.x : anchorPos.x - width x: (isRightEdge ? anchorPos.x : anchorPos.x - width) + panelMotionX(width, dropdownType === 1)
y: anchorPos.y - height / 2 y: anchorPos.y - height / 2 + panelMotionY(1, height, dropdownType === 1)
radius: Theme.cornerRadius * 2 radius: Theme.cornerRadius * 2
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) 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.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
border.width: 1 border.width: 1
opacity: dropdownType === 1 ? 1 : 0 opacity: Theme.isDirectionalEffect ? 1 : (dropdownType === 1 ? 1 : 0)
scale: dropdownType === 1 ? 1 : 0.96 scale: Theme.isDirectionalEffect ? 1 : (dropdownType === 1 ? 1 : Theme.effectScaleCollapsed)
transformOrigin: isRightEdge ? Item.Left : Item.Right transformOrigin: isRightEdge ? Item.Left : Item.Right
Behavior on opacity { Behavior on opacity {
NumberAnimation { enabled: !Theme.isDirectionalEffect
duration: Theme.expressiveDurations.expressiveDefaultSpatial DankAnim {
easing.type: Easing.BezierSpline duration: Math.round(Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 1) * Theme.variantOpacityDurationScale)
easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial easing.bezierCurve: dropdownType === 1 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
} }
} }
Behavior on scale { Behavior on scale {
enabled: !Theme.isDirectionalEffect
NumberAnimation { NumberAnimation {
duration: Theme.expressiveDurations.expressiveDefaultSpatial duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 1)
easing.type: Easing.BezierSpline 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 { Rectangle {
id: audioDevicesPanel id: audioDevicesPanel
visible: dropdownType === 2 visible: dropdownType === 2 && activePlayer !== null
width: 280 width: 280
height: Math.max(200, Math.min(280, availableDevices.length * 50 + 100)) height: Math.max(200, Math.min(280, availableDevices.length * 50 + 100))
x: isRightEdge ? anchorPos.x : anchorPos.x - width x: (isRightEdge ? anchorPos.x : anchorPos.x - width) + panelMotionX(width, dropdownType === 2)
y: anchorPos.y - height / 2 y: anchorPos.y - height / 2 + panelMotionY(2, height, dropdownType === 2)
radius: Theme.cornerRadius * 2 radius: Theme.cornerRadius * 2
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.98) 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.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.6)
border.width: 2 border.width: 2
opacity: dropdownType === 2 ? 1 : 0 opacity: Theme.isDirectionalEffect ? 1 : (dropdownType === 2 ? 1 : 0)
scale: dropdownType === 2 ? 1 : 0.96 scale: Theme.isDirectionalEffect ? 1 : (dropdownType === 2 ? 1 : Theme.effectScaleCollapsed)
transformOrigin: isRightEdge ? Item.Left : Item.Right transformOrigin: isRightEdge ? Item.Left : Item.Right
Behavior on opacity { Behavior on opacity {
NumberAnimation { enabled: !Theme.isDirectionalEffect
duration: Theme.expressiveDurations.expressiveDefaultSpatial DankAnim {
easing.type: Easing.BezierSpline duration: Math.round(Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 2) * Theme.variantOpacityDurationScale)
easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial easing.bezierCurve: dropdownType === 2 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
} }
} }
Behavior on scale { Behavior on scale {
enabled: !Theme.isDirectionalEffect
NumberAnimation { NumberAnimation {
duration: Theme.expressiveDurations.expressiveDefaultSpatial duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 2)
easing.type: Easing.BezierSpline 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 visible: dropdownType === 3
width: 240 width: 240
height: Math.max(180, Math.min(240, (allPlayers?.length || 0) * 50 + 80)) height: Math.max(180, Math.min(240, (allPlayers?.length || 0) * 50 + 80))
x: isRightEdge ? anchorPos.x : anchorPos.x - width x: (isRightEdge ? anchorPos.x : anchorPos.x - width) + panelMotionX(width, dropdownType === 3)
y: anchorPos.y - height / 2 y: anchorPos.y - height / 2 + panelMotionY(3, height, dropdownType === 3)
radius: Theme.cornerRadius * 2 radius: Theme.cornerRadius * 2
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.98) 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.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.6)
border.width: 2 border.width: 2
opacity: dropdownType === 3 ? 1 : 0 opacity: Theme.isDirectionalEffect ? 1 : (dropdownType === 3 ? 1 : 0)
scale: dropdownType === 3 ? 1 : 0.96 scale: Theme.isDirectionalEffect ? 1 : (dropdownType === 3 ? 1 : Theme.effectScaleCollapsed)
transformOrigin: isRightEdge ? Item.Left : Item.Right transformOrigin: isRightEdge ? Item.Left : Item.Right
Behavior on opacity { Behavior on opacity {
NumberAnimation { enabled: !Theme.isDirectionalEffect
duration: Theme.expressiveDurations.expressiveDefaultSpatial DankAnim {
easing.type: Easing.BezierSpline duration: Math.round(Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 3) * Theme.variantOpacityDurationScale)
easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial easing.bezierCurve: dropdownType === 3 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
} }
} }
Behavior on scale { Behavior on scale {
enabled: !Theme.isDirectionalEffect
NumberAnimation { NumberAnimation {
duration: Theme.expressiveDurations.expressiveDefaultSpatial duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 3)
easing.type: Easing.BezierSpline 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
} }
} }
+216 -34
View File
@@ -19,11 +19,12 @@ Variants {
WindowBlur { WindowBlur {
targetWindow: dock targetWindow: dock
blurEnabled: dock.effectiveBlurEnabled && !SettingsData.connectedFrameModeActive
blurX: dockBackground.x + dockContainer.x + dockMouseArea.x + dockCore.x + dockSlide.x blurX: dockBackground.x + dockContainer.x + dockMouseArea.x + dockCore.x + dockSlide.x
blurY: dockBackground.y + dockContainer.y + dockMouseArea.y + dockCore.y + dockSlide.y blurY: dockBackground.y + dockContainer.y + dockMouseArea.y + dockCore.y + dockSlide.y
blurWidth: dock.hasApps && dock.reveal ? dockBackground.width : 0 blurWidth: dock.hasApps && dock.reveal ? dockBackground.width : 0
blurHeight: dock.hasApps && dock.reveal ? dockBackground.height : 0 blurHeight: dock.hasApps && dock.reveal ? dockBackground.height : 0
blurRadius: Theme.cornerRadius blurRadius: Theme.isConnectedEffect ? Theme.connectedCornerRadius : dock.surfaceRadius
} }
WlrLayershell.namespace: "dms:dock" WlrLayershell.namespace: "dms:dock"
@@ -42,6 +43,23 @@ Variants {
property real backgroundTransparency: SettingsData.dockTransparency property real backgroundTransparency: SettingsData.dockTransparency
property bool groupByApp: SettingsData.dockGroupByApp property bool groupByApp: SettingsData.dockGroupByApp
readonly property int borderThickness: SettingsData.dockBorderEnabled ? SettingsData.dockBorderThickness : 0 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 readonly property int hasApps: dockApps.implicitWidth > 0 || dockApps.implicitHeight > 0
@@ -113,13 +131,102 @@ Variants {
return getBarHeight(leftBar); return getBarHeight(leftBar);
} }
readonly property real dockMargin: SettingsData.dockSpacing readonly property real dockMargin: SettingsData.dockMargin
readonly property real positionSpacing: barSpacing + SettingsData.dockBottomGap + 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 readonly property real _dpr: (dock.screen && dock.screen.devicePixelRatio) ? dock.screen.devicePixelRatio : 1
function px(v) { function px(v) {
return Math.round(v * _dpr) / _dpr; 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;
}
// ConnectedModeState sync
// Dock window origin in screen-relative coordinates (FrameWindow space).
function _dockWindowOriginX() {
if (!dock.isVertical)
return 0;
if (SettingsData.dockPosition === SettingsData.Position.Right)
return (dock.screen ? dock.screen.width : 0) - dock.width;
return 0;
}
function _dockWindowOriginY() {
if (dock.isVertical)
return 0;
if (SettingsData.dockPosition === SettingsData.Position.Bottom)
return (dock.screen ? dock.screen.height : 0) - dock.height;
return 0;
}
readonly property string _dockScreenName: dock.modelData ? dock.modelData.name : (dock.screen ? dock.screen.name : "")
function _syncDockChromeState() {
if (!dock._dockScreenName)
return;
if (!SettingsData.connectedFrameModeActive) {
ConnectedModeState.clearDockState(dock._dockScreenName);
return;
}
ConnectedModeState.setDockState(dock._dockScreenName, {
"reveal": dock.visible && (dock.reveal || slideXAnimation.running || slideYAnimation.running) && dock.hasApps,
"barSide": dock.connectedBarSide,
"bodyX": dock._dockWindowOriginX() + dockBackground.x + dockContainer.x + dockMouseArea.x + dockCore.x,
"bodyY": dock._dockWindowOriginY() + dockBackground.y + dockContainer.y + dockMouseArea.y + dockCore.y,
"bodyW": dock.hasApps ? dockBackground.width : 0,
"bodyH": dock.hasApps ? dockBackground.height : 0,
"slideX": dockSlide.x,
"slideY": dockSlide.y
});
}
function _syncDockSlide() {
if (!dock._dockScreenName || !SettingsData.connectedFrameModeActive)
return;
ConnectedModeState.setDockSlide(dock._dockScreenName, dockSlide.x, dockSlide.y);
}
property bool contextMenuOpen: (dockVariants.contextMenu && dockVariants.contextMenu.visible && dockVariants.contextMenu.screen === modelData) property bool contextMenuOpen: (dockVariants.contextMenu && dockVariants.contextMenu.visible && dockVariants.contextMenu.screen === modelData)
property bool revealSticky: false property bool revealSticky: false
@@ -130,7 +237,7 @@ Variants {
return false; return false;
const screenName = dock.modelData?.name ?? ""; 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 screenWidth = dock.screen?.width ?? 0;
const screenHeight = dock.screen?.height ?? 0; const screenHeight = dock.screen?.height ?? 0;
@@ -281,6 +388,23 @@ Variants {
} }
} }
Component.onCompleted: Qt.callLater(() => dock._syncDockChromeState())
Component.onDestruction: ConnectedModeState.clearDockState(dock._dockScreenName)
onRevealChanged: dock._syncDockChromeState()
onWidthChanged: dock._syncDockChromeState()
onHeightChanged: dock._syncDockChromeState()
onVisibleChanged: dock._syncDockChromeState()
onHasAppsChanged: dock._syncDockChromeState()
onConnectedBarSideChanged: dock._syncDockChromeState()
Connections {
target: SettingsData
function onConnectedFrameModeActiveChanged() {
dock._syncDockChromeState();
}
}
Connections { Connections {
target: SettingsData target: SettingsData
function onDockTransparencyChanged() { function onDockTransparencyChanged() {
@@ -302,13 +426,13 @@ Variants {
return -1; return -1;
if (barSpacing > 0) if (barSpacing > 0)
return -1; 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) property real animationHeadroom: Math.ceil(SettingsData.dockIconSize * 0.35)
implicitWidth: 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(effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockMargin + SettingsData.dockIconSize * 0.3) + animationHeadroom) : 0 implicitHeight: !isVertical ? (px(connectedJoinInset + effectiveBarHeight + SettingsData.dockSpacing + effectiveDockMargin + SettingsData.dockIconSize * 0.3) + animationHeadroom) : 0
Item { Item {
id: maskItem id: maskItem
@@ -318,17 +442,17 @@ Variants {
x: { x: {
const baseX = dockCore.x + dockMouseArea.x; const baseX = dockCore.x + dockMouseArea.x;
if (isVertical && SettingsData.dockPosition === SettingsData.Position.Right) if (isVertical && SettingsData.dockPosition === SettingsData.Position.Right)
return baseX - (expanded ? animationHeadroom + borderThickness : 0); return baseX - (expanded ? animationHeadroom + borderThickness + dock.horizontalConnectorExtent : 0);
return baseX - (expanded ? borderThickness : 0); return baseX - (expanded ? borderThickness + dock.horizontalConnectorExtent : 0);
} }
y: { y: {
const baseY = dockCore.y + dockMouseArea.y; const baseY = dockCore.y + dockMouseArea.y;
if (!isVertical && SettingsData.dockPosition === SettingsData.Position.Bottom) if (!isVertical && SettingsData.dockPosition === SettingsData.Position.Bottom)
return baseY - (expanded ? animationHeadroom + borderThickness : 0); return baseY - (expanded ? animationHeadroom + borderThickness + dock.verticalConnectorExtent : 0);
return baseY - (expanded ? borderThickness : 0); return baseY - (expanded ? borderThickness + dock.verticalConnectorExtent : 0);
} }
width: dockMouseArea.width + (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 : 0) height: dockMouseArea.height + (!isVertical && expanded ? animationHeadroom : 0) + (expanded ? borderThickness * 2 + dock.verticalConnectorExtent * 2 : 0)
} }
mask: Region { mask: Region {
@@ -388,7 +512,7 @@ Variants {
const screenHeight = dock.screen ? dock.screen.height : 0; const screenHeight = dock.screen ? dock.screen.height : 0;
const gap = Theme.spacingS; const gap = Theme.spacingS;
const bgMargin = barSpacing + SettingsData.dockMargin + 1 + dock.borderThickness; const bgMargin = dock.joinedEdgeMargin + dock.connectedJoinInset;
const btnW = dock.hoveredButton.width; const btnW = dock.hoveredButton.width;
const btnH = dock.hoveredButton.height; const btnH = dock.hoveredButton.height;
@@ -459,11 +583,11 @@ Variants {
// Keep the taller hit area regardless of the reveal state to prevent shrinking loop // 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 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: { width: {
if (dock.isVertical) { 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 // Keep the wider hit area regardless of the reveal state to prevent shrinking loop
return Math.min(dockBackground.width + 8 + dock.borderThickness, maxDockWidth); return Math.min(dockBackground.width + 8 + dock.borderThickness, maxDockWidth);
@@ -505,7 +629,11 @@ Variants {
return 0; return 0;
if (dock.reveal) if (dock.reveal)
return 0; return 0;
const hideDistance = dock.effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin + 10; if (Theme.isConnectedEffect) {
const retractDist = dockBackground.width + SettingsData.dockSpacing + 10;
return SettingsData.dockPosition === SettingsData.Position.Right ? retractDist : -retractDist;
}
const hideDistance = dock.connectedJoinInset + dock.effectiveBarHeight + SettingsData.dockSpacing + dock.effectiveDockBottomGap + dock.effectiveDockMargin + 10;
if (SettingsData.dockPosition === SettingsData.Position.Right) { if (SettingsData.dockPosition === SettingsData.Position.Right) {
return hideDistance; return hideDistance;
} else { } else {
@@ -517,7 +645,11 @@ Variants {
return 0; return 0;
if (dock.reveal) if (dock.reveal)
return 0; return 0;
const hideDistance = dock.effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin + 10; if (Theme.isConnectedEffect) {
const retractDist = dockBackground.height + SettingsData.dockSpacing + 10;
return SettingsData.dockPosition === SettingsData.Position.Bottom ? retractDist : -retractDist;
}
const hideDistance = dock.connectedJoinInset + dock.effectiveBarHeight + SettingsData.dockSpacing + dock.effectiveDockBottomGap + dock.effectiveDockMargin + 10;
if (SettingsData.dockPosition === SettingsData.Position.Bottom) { if (SettingsData.dockPosition === SettingsData.Position.Bottom) {
return hideDistance; return hideDistance;
} else { } else {
@@ -528,18 +660,29 @@ Variants {
Behavior on x { Behavior on x {
NumberAnimation { NumberAnimation {
id: slideXAnimation id: slideXAnimation
duration: Theme.shortDuration duration: Theme.isConnectedEffect ? Theme.variantDuration(Theme.popoutAnimationDuration, dock.reveal) : Theme.shortDuration
easing.type: Easing.OutCubic easing.type: Theme.isConnectedEffect ? Easing.BezierSpline : Easing.OutCubic
easing.bezierCurve: Theme.isConnectedEffect ? (dock.reveal ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve) : []
onRunningChanged: if (!running) dock._syncDockChromeState()
} }
} }
Behavior on y { Behavior on y {
NumberAnimation { NumberAnimation {
id: slideYAnimation id: slideYAnimation
duration: Theme.shortDuration duration: Theme.isConnectedEffect ? Theme.variantDuration(Theme.popoutAnimationDuration, dock.reveal) : Theme.shortDuration
easing.type: Easing.OutCubic easing.type: Theme.isConnectedEffect ? Easing.BezierSpline : Easing.OutCubic
easing.bezierCurve: Theme.isConnectedEffect ? (dock.reveal ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve) : []
onRunningChanged: if (!running) dock._syncDockChromeState()
} }
} }
onXChanged: {
dock._syncDockSlide();
}
onYChanged: {
dock._syncDockSlide();
}
} }
Item { Item {
@@ -553,33 +696,72 @@ Variants {
right: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Right ? parent.right : undefined) : undefined right: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Right ? parent.right : undefined) : undefined
verticalCenter: dock.isVertical ? parent.verticalCenter : undefined verticalCenter: dock.isVertical ? parent.verticalCenter : undefined
} }
anchors.topMargin: !dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Top ? 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 ? barSpacing + SettingsData.dockMargin + 1 + dock.borderThickness : 0 anchors.bottomMargin: !dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Bottom ? (dock.connectedJoinInset + dock.joinedEdgeMargin) : 0
anchors.leftMargin: dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Left ? barSpacing + SettingsData.dockMargin + 1 + dock.borderThickness : 0 anchors.leftMargin: dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Left ? (dock.connectedJoinInset + dock.joinedEdgeMargin) : 0
anchors.rightMargin: dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Right ? barSpacing + SettingsData.dockMargin + 1 + dock.borderThickness : 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) 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) implicitHeight: dock.isVertical ? (dockApps.implicitWidth + SettingsData.dockSpacing * 2) : (dockApps.implicitHeight + SettingsData.dockSpacing * 2)
width: implicitWidth width: implicitWidth
height: implicitHeight height: implicitHeight
layer.enabled: true // Avoid an offscreen texture seam where the connected dock meets the frame.
layer.enabled: !Theme.isConnectedEffect
clip: false clip: false
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
color: Theme.withAlpha(Theme.surfaceContainer, backgroundTransparency) visible: !SettingsData.connectedFrameModeActive
radius: Theme.cornerRadius color: dock.surfaceColor
topLeftRadius: dock.surfaceTopLeftRadius
topRightRadius: dock.surfaceTopRightRadius
bottomLeftRadius: dock.surfaceBottomLeftRadius
bottomRightRadius: dock.surfaceBottomRightRadius
} }
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
visible: !SettingsData.connectedFrameModeActive
color: "transparent" color: "transparent"
radius: Theme.cornerRadius topLeftRadius: dock.surfaceTopLeftRadius
border.color: BlurService.borderColor topRightRadius: dock.surfaceTopRightRadius
border.width: BlurService.borderWidth bottomLeftRadius: dock.surfaceBottomLeftRadius
bottomRightRadius: dock.surfaceBottomRightRadius
border.color: dock.surfaceBorderColor
border.width: dock.surfaceBorderWidth
z: 100 z: 100
} }
// Sync dockBackground geometry to ConnectedModeState
onXChanged: dock._syncDockChromeState()
onYChanged: dock._syncDockChromeState()
onWidthChanged: dock._syncDockChromeState()
onHeightChanged: dock._syncDockChromeState()
}
ConnectedCorner {
visible: Theme.isConnectedEffect && dock.reveal && !SettingsData.connectedFrameModeActive
barSide: dock.connectedBarSide
placement: "left"
spacing: 0
connectorRadius: Theme.connectedCornerRadius
color: dock.surfaceColor
dpr: dock._dpr
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 && !SettingsData.connectedFrameModeActive
barSide: dock.connectedBarSide
placement: "right"
spacing: 0
connectorRadius: Theme.connectedCornerRadius
color: dock.surfaceColor
dpr: dock._dpr
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 { Shape {
@@ -588,12 +770,12 @@ Variants {
y: dockBackground.y - borderThickness y: dockBackground.y - borderThickness
width: dockBackground.width + borderThickness * 2 width: dockBackground.width + borderThickness * 2
height: dockBackground.height + borderThickness * 2 height: dockBackground.height + borderThickness * 2
visible: SettingsData.dockBorderEnabled && dock.hasApps visible: SettingsData.dockBorderEnabled && dock.hasApps && !Theme.isConnectedEffect
preferredRendererType: Shape.CurveRenderer preferredRendererType: Shape.CurveRenderer
readonly property real borderThickness: Math.max(1, dock.borderThickness) readonly property real borderThickness: Math.max(1, dock.borderThickness)
readonly property real i: borderThickness / 2 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 w: dockBackground.width
readonly property real h: dockBackground.height readonly property real h: dockBackground.height
+17
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
}
}
+54
View File
@@ -0,0 +1,54 @@
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
property color borderColor: Qt.rgba(SettingsData.effectiveFrameColor.r, SettingsData.effectiveFrameColor.g, SettingsData.effectiveFrameColor.b, SettingsData.frameOpacity)
Rectangle {
id: borderRect
anchors.fill: parent
// Bake frameOpacity into the color alpha rather than using the `opacity` property
color: root.borderColor
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
}
}
}
@@ -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
}
}
}
@@ -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
}
}
+698
View File
@@ -0,0 +1,698 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Wayland
import qs.Common
import qs.Services
import qs.Widgets
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"
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)
readonly property string _screenName: win.screen ? win.screen.name : ""
readonly property var _dockState: ConnectedModeState.dockStates[win._screenName] || ConnectedModeState.emptyDockState
readonly property var _dockSlide: ConnectedModeState.dockSlides[win._screenName] || ({
"x": 0,
"y": 0
})
readonly property var _notifState: ConnectedModeState.notificationStates[win._screenName] || ConnectedModeState.emptyNotificationState
// Connected chrome convenience properties
readonly property bool _connectedActive: win._frameActive && SettingsData.connectedFrameModeActive
readonly property string _barSide: {
const edges = win.barEdges;
if (edges.includes("top"))
return "top";
if (edges.includes("bottom"))
return "bottom";
if (edges.includes("left"))
return "left";
return "right";
}
readonly property real _ccr: Theme.connectedCornerRadius
readonly property real _effectivePopoutCcr: {
const extent = win._popoutArcExtent();
const isHoriz = ConnectedModeState.popoutBarSide === "top" || ConnectedModeState.popoutBarSide === "bottom";
const crossSize = isHoriz ? _popoutBodyBlurAnchor.width : _popoutBodyBlurAnchor.height;
return Math.max(0, Math.min(win._ccr, extent, crossSize / 2));
}
readonly property color _surfaceColor: Theme.connectedSurfaceColor
readonly property real _surfaceOpacity: _surfaceColor.a
readonly property color _opaqueSurfaceColor: Qt.rgba(_surfaceColor.r, _surfaceColor.g, _surfaceColor.b, 1)
readonly property real _surfaceRadius: Theme.connectedSurfaceRadius
readonly property real _seamOverlap: Theme.hairline(win._dpr)
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));
}
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));
}
// Invisible items providing scene coordinates for blur Region anchors
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)
}
Item {
id: _popoutBodyBlurAnchor
visible: false
readonly property bool _active: ConnectedModeState.popoutVisible && ConnectedModeState.popoutScreen === win._screenName
readonly property real _dyClamp: (ConnectedModeState.popoutBarSide === "top" || ConnectedModeState.popoutBarSide === "bottom") ? Math.max(-ConnectedModeState.popoutBodyH, Math.min(ConnectedModeState.popoutAnimY * 1.02, ConnectedModeState.popoutBodyH)) : 0
readonly property real _dxClamp: (ConnectedModeState.popoutBarSide === "left" || ConnectedModeState.popoutBarSide === "right") ? Math.max(-ConnectedModeState.popoutBodyW, Math.min(ConnectedModeState.popoutAnimX * 1.02, ConnectedModeState.popoutBodyW)) : 0
x: _active ? ConnectedModeState.popoutBodyX + (ConnectedModeState.popoutBarSide === "right" ? _dxClamp : 0) : 0
y: _active ? ConnectedModeState.popoutBodyY + (ConnectedModeState.popoutBarSide === "bottom" ? _dyClamp : 0) : 0
width: _active ? Math.max(0, ConnectedModeState.popoutBodyW - Math.abs(_dxClamp)) : 0
height: _active ? Math.max(0, ConnectedModeState.popoutBodyH - Math.abs(_dyClamp)) : 0
}
Item {
id: _dockBodyBlurAnchor
visible: false
readonly property bool _active: win._dockState.reveal && win._dockState.bodyW > 0 && win._dockState.bodyH > 0
x: _active ? win._dockState.bodyX + (win._dockSlide.x || 0) : 0
y: _active ? win._dockState.bodyY + (win._dockSlide.y || 0) : 0
width: _active ? win._dockState.bodyW : 0
height: _active ? win._dockState.bodyH : 0
}
Item {
id: _popoutBodyBlurCap
opacity: 0
readonly property string _side: ConnectedModeState.popoutBarSide
readonly property real _capThickness: win._popoutBlurCapThickness()
readonly property bool _active: _popoutBodyBlurAnchor._active && _capThickness > 0 && _popoutBodyBlurAnchor.width > 0 && _popoutBodyBlurAnchor.height > 0
readonly property real _capWidth: (_side === "left" || _side === "right") ? Math.min(_capThickness, _popoutBodyBlurAnchor.width) : _popoutBodyBlurAnchor.width
readonly property real _capHeight: (_side === "top" || _side === "bottom") ? Math.min(_capThickness, _popoutBodyBlurAnchor.height) : _popoutBodyBlurAnchor.height
x: !_active ? 0 : (_side === "right" ? _popoutBodyBlurAnchor.x + _popoutBodyBlurAnchor.width - _capWidth : _popoutBodyBlurAnchor.x)
y: !_active ? 0 : (_side === "bottom" ? _popoutBodyBlurAnchor.y + _popoutBodyBlurAnchor.height - _capHeight : _popoutBodyBlurAnchor.y)
width: _active ? _capWidth : 0
height: _active ? _capHeight : 0
}
Item {
id: _dockBodyBlurCap
opacity: 0
readonly property string _side: win._dockState.barSide
readonly property bool _active: _dockBodyBlurAnchor._active && _dockBodyBlurAnchor.width > 0 && _dockBodyBlurAnchor.height > 0
readonly property real _capWidth: (_side === "left" || _side === "right") ? Math.min(win._dockConnectorRadius(), _dockBodyBlurAnchor.width) : _dockBodyBlurAnchor.width
readonly property real _capHeight: (_side === "top" || _side === "bottom") ? Math.min(win._dockConnectorRadius(), _dockBodyBlurAnchor.height) : _dockBodyBlurAnchor.height
x: !_active ? 0 : (_side === "right" ? _dockBodyBlurAnchor.x + _dockBodyBlurAnchor.width - _capWidth : _dockBodyBlurAnchor.x)
y: !_active ? 0 : (_side === "bottom" ? _dockBodyBlurAnchor.y + _dockBodyBlurAnchor.height - _capHeight : _dockBodyBlurAnchor.y)
width: _active ? _capWidth : 0
height: _active ? _capHeight : 0
}
Item {
id: _dockLeftConnectorBlurAnchor
opacity: 0
readonly property bool _active: _dockBodyBlurAnchor._active && win._dockConnectorRadius() > 0
readonly property real _w: win._dockConnectorWidth(0)
readonly property real _h: win._dockConnectorHeight(0)
x: _active ? Theme.snap(win._dockConnectorX(_dockBodyBlurAnchor.x, _dockBodyBlurAnchor.width, "left", 0), win._dpr) : 0
y: _active ? Theme.snap(win._dockConnectorY(_dockBodyBlurAnchor.y, _dockBodyBlurAnchor.height, "left", 0), win._dpr) : 0
width: _active ? _w : 0
height: _active ? _h : 0
}
Item {
id: _dockRightConnectorBlurAnchor
opacity: 0
readonly property bool _active: _dockBodyBlurAnchor._active && win._dockConnectorRadius() > 0
readonly property real _w: win._dockConnectorWidth(0)
readonly property real _h: win._dockConnectorHeight(0)
x: _active ? Theme.snap(win._dockConnectorX(_dockBodyBlurAnchor.x, _dockBodyBlurAnchor.width, "right", 0), win._dpr) : 0
y: _active ? Theme.snap(win._dockConnectorY(_dockBodyBlurAnchor.y, _dockBodyBlurAnchor.height, "right", 0), win._dpr) : 0
width: _active ? _w : 0
height: _active ? _h : 0
}
Item {
id: _dockLeftConnectorCutout
opacity: 0
readonly property bool _active: _dockLeftConnectorBlurAnchor.width > 0 && _dockLeftConnectorBlurAnchor.height > 0
readonly property string _arcCorner: win._connectorArcCorner(win._dockState.barSide, "left")
x: _active ? win._connectorCutoutX(_dockLeftConnectorBlurAnchor.x, _dockLeftConnectorBlurAnchor.width, _arcCorner, win._dockConnectorRadius()) : 0
y: _active ? win._connectorCutoutY(_dockLeftConnectorBlurAnchor.y, _dockLeftConnectorBlurAnchor.height, _arcCorner, win._dockConnectorRadius()) : 0
width: _active ? win._dockConnectorRadius() * 2 : 0
height: _active ? win._dockConnectorRadius() * 2 : 0
}
Item {
id: _dockRightConnectorCutout
opacity: 0
readonly property bool _active: _dockRightConnectorBlurAnchor.width > 0 && _dockRightConnectorBlurAnchor.height > 0
readonly property string _arcCorner: win._connectorArcCorner(win._dockState.barSide, "right")
x: _active ? win._connectorCutoutX(_dockRightConnectorBlurAnchor.x, _dockRightConnectorBlurAnchor.width, _arcCorner, win._dockConnectorRadius()) : 0
y: _active ? win._connectorCutoutY(_dockRightConnectorBlurAnchor.y, _dockRightConnectorBlurAnchor.height, _arcCorner, win._dockConnectorRadius()) : 0
width: _active ? win._dockConnectorRadius() * 2 : 0
height: _active ? win._dockConnectorRadius() * 2 : 0
}
Item {
id: _notifBodyBlurAnchor
visible: false
readonly property bool _active: win._frameActive && win._notifState.visible && win._notifState.bodyW > 0 && win._notifState.bodyH > 0
x: _active ? Theme.snap(win._notifState.bodyX, win._dpr) : 0
y: _active ? Theme.snap(win._notifState.bodyY, win._dpr) : 0
width: _active ? Theme.snap(win._notifState.bodyW, win._dpr) : 0
height: _active ? Theme.snap(win._notifState.bodyH, win._dpr) : 0
}
Region {
id: _staticBlurRegion
x: 0
y: 0
width: win._windowRegionWidth
height: win._windowRegionHeight
// Frame cutout (always active when frame is on)
Region {
item: _blurCutout
intersection: Intersection.Subtract
radius: win._blurCutoutRadius
}
// Connected popout blur regions
Region {
item: _popoutBodyBlurAnchor
radius: win._surfaceRadius
}
Region {
item: _popoutBodyBlurCap
}
// Connected dock blur regions
Region {
item: _dockBodyBlurAnchor
radius: win._dockBodyBlurRadius()
}
Region {
item: _dockBodyBlurCap
}
Region {
item: _dockLeftConnectorBlurAnchor
radius: win._dockConnectorRadius()
Region {
item: _dockLeftConnectorCutout
intersection: Intersection.Subtract
radius: win._dockConnectorRadius()
}
}
Region {
item: _dockRightConnectorBlurAnchor
radius: win._dockConnectorRadius()
Region {
item: _dockRightConnectorCutout
intersection: Intersection.Subtract
radius: win._dockConnectorRadius()
}
}
Region {
item: _notifBodyBlurAnchor
radius: win._surfaceRadius
}
}
// Connector position helpers (dock)
function _dockBodyBlurRadius() {
return _dockBodyBlurAnchor._active ? Math.max(0, Math.min(win._surfaceRadius, _dockBodyBlurAnchor.width / 2, _dockBodyBlurAnchor.height / 2)) : win._surfaceRadius;
}
function _dockConnectorRadius() {
if (!_dockBodyBlurAnchor._active)
return win._ccr;
const dockSide = win._dockState.barSide;
const thickness = (dockSide === "left" || dockSide === "right") ? _dockBodyBlurAnchor.width : _dockBodyBlurAnchor.height;
const bodyRadius = win._dockBodyBlurRadius();
const maxConnectorRadius = Math.max(0, thickness - bodyRadius - win._seamOverlap);
return Math.max(0, Math.min(win._ccr, bodyRadius, maxConnectorRadius));
}
function _dockConnectorWidth(spacing) {
const isVert = win._dockState.barSide === "left" || win._dockState.barSide === "right";
const radius = win._dockConnectorRadius();
return isVert ? (spacing + radius) : radius;
}
function _dockConnectorHeight(spacing) {
const isVert = win._dockState.barSide === "left" || win._dockState.barSide === "right";
const radius = win._dockConnectorRadius();
return isVert ? radius : (spacing + radius);
}
function _dockConnectorX(baseX, bodyWidth, placement, spacing) {
const dockSide = win._dockState.barSide;
const isVert = dockSide === "left" || dockSide === "right";
const seamX = !isVert ? (placement === "left" ? baseX : baseX + bodyWidth) : (dockSide === "left" ? baseX : baseX + bodyWidth);
const w = _dockConnectorWidth(spacing);
if (!isVert)
return placement === "left" ? seamX - w : seamX;
return dockSide === "left" ? seamX : seamX - w;
}
function _dockConnectorY(baseY, bodyHeight, placement, spacing) {
const dockSide = win._dockState.barSide;
const seamY = dockSide === "top" ? baseY : dockSide === "bottom" ? baseY + bodyHeight : (placement === "left" ? baseY : baseY + bodyHeight);
const h = _dockConnectorHeight(spacing);
if (dockSide === "top")
return seamY;
if (dockSide === "bottom")
return seamY - h;
return placement === "left" ? seamY - h : seamY;
}
function _popoutFillOverlapX() {
return (ConnectedModeState.popoutBarSide === "top" || ConnectedModeState.popoutBarSide === "bottom") ? win._seamOverlap : 0;
}
function _popoutFillOverlapY() {
return (ConnectedModeState.popoutBarSide === "left" || ConnectedModeState.popoutBarSide === "right") ? win._seamOverlap : 0;
}
function _dockFillOverlapX() {
return (win._dockState.barSide === "top" || win._dockState.barSide === "bottom") ? win._seamOverlap : 0;
}
function _dockFillOverlapY() {
return (win._dockState.barSide === "left" || win._dockState.barSide === "right") ? win._seamOverlap : 0;
}
function _popoutArcExtent() {
return (ConnectedModeState.popoutBarSide === "top" || ConnectedModeState.popoutBarSide === "bottom") ? _popoutBodyBlurAnchor.height : _popoutBodyBlurAnchor.width;
}
function _popoutArcVisible() {
if (!_popoutBodyBlurAnchor._active || _popoutBodyBlurAnchor.width <= 0 || _popoutBodyBlurAnchor.height <= 0)
return false;
return win._popoutArcExtent() >= win._ccr * (1 + win._ccr * 0.02);
}
function _popoutBlurCapThickness() {
const extent = win._popoutArcExtent();
return Math.max(0, Math.min(win._effectivePopoutCcr, extent - win._surfaceRadius));
}
function _popoutChromeX() {
const barSide = ConnectedModeState.popoutBarSide;
return ConnectedModeState.popoutBodyX - ((barSide === "top" || barSide === "bottom") ? win._effectivePopoutCcr : 0);
}
function _popoutChromeY() {
const barSide = ConnectedModeState.popoutBarSide;
return ConnectedModeState.popoutBodyY - ((barSide === "left" || barSide === "right") ? win._effectivePopoutCcr : 0);
}
function _popoutChromeWidth() {
const barSide = ConnectedModeState.popoutBarSide;
return ConnectedModeState.popoutBodyW + ((barSide === "top" || barSide === "bottom") ? win._effectivePopoutCcr * 2 : 0);
}
function _popoutChromeHeight() {
const barSide = ConnectedModeState.popoutBarSide;
return ConnectedModeState.popoutBodyH + ((barSide === "left" || barSide === "right") ? win._effectivePopoutCcr * 2 : 0);
}
function _popoutClipX() {
return _popoutBodyBlurAnchor.x - win._popoutChromeX() - win._popoutFillOverlapX();
}
function _popoutClipY() {
return _popoutBodyBlurAnchor.y - win._popoutChromeY() - win._popoutFillOverlapY();
}
function _popoutClipWidth() {
return _popoutBodyBlurAnchor.width + win._popoutFillOverlapX() * 2;
}
function _popoutClipHeight() {
return _popoutBodyBlurAnchor.height + win._popoutFillOverlapY() * 2;
}
function _popoutBodyXInClip() {
return (ConnectedModeState.popoutBarSide === "left" ? _popoutBodyBlurAnchor._dxClamp : 0) - win._popoutFillOverlapX();
}
function _popoutBodyYInClip() {
return (ConnectedModeState.popoutBarSide === "top" ? _popoutBodyBlurAnchor._dyClamp : 0) - win._popoutFillOverlapY();
}
function _popoutBodyFullWidth() {
return ConnectedModeState.popoutBodyW + win._popoutFillOverlapX() * 2;
}
function _popoutBodyFullHeight() {
return ConnectedModeState.popoutBodyH + win._popoutFillOverlapY() * 2;
}
function _dockChromeX() {
const dockSide = win._dockState.barSide;
return _dockBodyBlurAnchor.x - ((dockSide === "top" || dockSide === "bottom") ? win._dockConnectorRadius() : 0);
}
function _dockChromeY() {
const dockSide = win._dockState.barSide;
return _dockBodyBlurAnchor.y - ((dockSide === "left" || dockSide === "right") ? win._dockConnectorRadius() : 0);
}
function _dockChromeWidth() {
const dockSide = win._dockState.barSide;
return _dockBodyBlurAnchor.width + ((dockSide === "top" || dockSide === "bottom") ? win._dockConnectorRadius() * 2 : 0);
}
function _dockChromeHeight() {
const dockSide = win._dockState.barSide;
return _dockBodyBlurAnchor.height + ((dockSide === "left" || dockSide === "right") ? win._dockConnectorRadius() * 2 : 0);
}
function _dockBodyXInChrome() {
return ((win._dockState.barSide === "top" || win._dockState.barSide === "bottom") ? win._dockConnectorRadius() : 0) - win._dockFillOverlapX();
}
function _dockBodyYInChrome() {
return ((win._dockState.barSide === "left" || win._dockState.barSide === "right") ? win._dockConnectorRadius() : 0) - win._dockFillOverlapY();
}
function _connectorArcCorner(barSide, placement) {
if (barSide === "top")
return placement === "left" ? "bottomLeft" : "bottomRight";
if (barSide === "bottom")
return placement === "left" ? "topLeft" : "topRight";
if (barSide === "left")
return placement === "left" ? "topRight" : "bottomRight";
return placement === "left" ? "topLeft" : "bottomLeft";
}
function _connectorCutoutX(connectorX, connectorWidth, arcCorner, radius) {
const r = radius === undefined ? win._effectivePopoutCcr : radius;
return (arcCorner === "topLeft" || arcCorner === "bottomLeft") ? connectorX - r : connectorX + connectorWidth - r;
}
function _connectorCutoutY(connectorY, connectorHeight, arcCorner, radius) {
const r = radius === undefined ? win._effectivePopoutCcr : radius;
return (arcCorner === "topLeft" || arcCorner === "topRight") ? connectorY - r : connectorY + connectorHeight - r;
}
// Blur build / teardown
function _buildBlur() {
try {
if (!BlurService.enabled || !SettingsData.frameBlurEnabled || !win._frameActive || !win.visible) {
win.BackgroundEffect.blurRegion = null;
return;
}
win.BackgroundEffect.blurRegion = _staticBlurRegion;
} catch (e) {
console.warn("FrameWindow: Failed to set blur region:", e);
}
}
function _teardownBlur() {
try {
win.BackgroundEffect.blurRegion = null;
} catch (e) {}
}
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();
}
function onConnectedFrameModeActiveChanged() {
_blurRebuildTimer.restart();
}
}
Connections {
target: BlurService
function onEnabledChanged() {
_blurRebuildTimer.restart();
}
}
onVisibleChanged: {
if (visible) {
_blurRebuildTimer.restart();
} else {
_teardownBlur();
}
}
Component.onCompleted: Qt.callLater(() => win._buildBlur())
Component.onDestruction: win._teardownBlur()
// Frame border
FrameBorder {
anchors.fill: parent
visible: win._frameActive && !win._connectedActive
cutoutTopInset: win.cutoutTopInset
cutoutBottomInset: win.cutoutBottomInset
cutoutLeftInset: win.cutoutLeftInset
cutoutRightInset: win.cutoutRightInset
cutoutRadius: win.cutoutRadius
}
// Connected chrome fills
Item {
id: _connectedSurfaceLayer
anchors.fill: parent
visible: win._connectedActive
opacity: win._surfaceOpacity
layer.enabled: opacity < 1
layer.smooth: false
FrameBorder {
anchors.fill: parent
borderColor: win._opaqueSurfaceColor
cutoutTopInset: win.cutoutTopInset
cutoutBottomInset: win.cutoutBottomInset
cutoutLeftInset: win.cutoutLeftInset
cutoutRightInset: win.cutoutRightInset
cutoutRadius: win.cutoutRadius
}
Item {
id: _connectedChrome
anchors.fill: parent
visible: true
Item {
id: _popoutChrome
visible: ConnectedModeState.popoutVisible && ConnectedModeState.popoutScreen === win._screenName
x: win._popoutChromeX()
y: win._popoutChromeY()
width: win._popoutChromeWidth()
height: win._popoutChromeHeight()
Item {
id: _popoutClip
readonly property bool _barHoriz: ConnectedModeState.popoutBarSide === "top" || ConnectedModeState.popoutBarSide === "bottom"
// Expand clip by ccr on bar axis to include arc columns
x: win._popoutClipX() - (_barHoriz ? win._effectivePopoutCcr : 0)
y: win._popoutClipY() - (_barHoriz ? 0 : win._effectivePopoutCcr)
width: win._popoutClipWidth() + (_barHoriz ? win._effectivePopoutCcr * 2 : 0)
height: win._popoutClipHeight() + (_barHoriz ? 0 : win._effectivePopoutCcr * 2)
clip: true
ConnectedShape {
id: _popoutShape
visible: _popoutBodyBlurAnchor._active && _popoutBodyBlurAnchor.width > 0 && _popoutBodyBlurAnchor.height > 0
barSide: ConnectedModeState.popoutBarSide
bodyWidth: win._popoutClipWidth()
bodyHeight: win._popoutClipHeight()
connectorRadius: win._effectivePopoutCcr
surfaceRadius: win._surfaceRadius
fillColor: win._opaqueSurfaceColor
x: 0
y: 0
}
}
}
Item {
id: _dockChrome
visible: _dockBodyBlurAnchor._active
x: win._dockChromeX()
y: win._dockChromeY()
width: win._dockChromeWidth()
height: win._dockChromeHeight()
Rectangle {
id: _dockFill
x: win._dockBodyXInChrome()
y: win._dockBodyYInChrome()
width: _dockBodyBlurAnchor.width + win._dockFillOverlapX() * 2
height: _dockBodyBlurAnchor.height + win._dockFillOverlapY() * 2
color: win._opaqueSurfaceColor
z: 1
readonly property string _dockSide: win._dockState.barSide
readonly property real _dockRadius: win._dockBodyBlurRadius()
topLeftRadius: (_dockSide === "top" || _dockSide === "left") ? 0 : _dockRadius
topRightRadius: (_dockSide === "top" || _dockSide === "right") ? 0 : _dockRadius
bottomLeftRadius: (_dockSide === "bottom" || _dockSide === "left") ? 0 : _dockRadius
bottomRightRadius: (_dockSide === "bottom" || _dockSide === "right") ? 0 : _dockRadius
}
ConnectedCorner {
id: _connDockLeft
visible: _dockBodyBlurAnchor._active
barSide: win._dockState.barSide
placement: "left"
spacing: 0
connectorRadius: win._dockConnectorRadius()
color: win._opaqueSurfaceColor
dpr: win._dpr
x: Theme.snap(win._dockConnectorX(_dockBodyBlurAnchor.x, _dockBodyBlurAnchor.width, "left", 0) - _dockChrome.x, win._dpr)
y: Theme.snap(win._dockConnectorY(_dockBodyBlurAnchor.y, _dockBodyBlurAnchor.height, "left", 0) - _dockChrome.y, win._dpr)
}
ConnectedCorner {
id: _connDockRight
visible: _dockBodyBlurAnchor._active
barSide: win._dockState.barSide
placement: "right"
spacing: 0
connectorRadius: win._dockConnectorRadius()
color: win._opaqueSurfaceColor
dpr: win._dpr
x: Theme.snap(win._dockConnectorX(_dockBodyBlurAnchor.x, _dockBodyBlurAnchor.width, "right", 0) - _dockChrome.x, win._dpr)
y: Theme.snap(win._dockConnectorY(_dockBodyBlurAnchor.y, _dockBodyBlurAnchor.height, "right", 0) - _dockChrome.y, win._dpr)
}
}
}
Item {
id: _notifChrome
visible: _notifBodyBlurAnchor._active
readonly property string _notifSide: win._notifState.barSide
readonly property bool _isHoriz: _notifSide === "top" || _notifSide === "bottom"
readonly property real _notifCcr: Theme.snap(Math.max(0, Math.min(win._ccr, win._surfaceRadius, (_isHoriz ? _notifBodyBlurAnchor.width : _notifBodyBlurAnchor.height) / 2)), win._dpr)
readonly property real _sideUnderlap: _isHoriz ? 0 : win._seamOverlap
readonly property real _bodyW: Theme.snap(_notifBodyBlurAnchor.width + _sideUnderlap, win._dpr)
readonly property real _bodyH: Theme.snap(_notifBodyBlurAnchor.height, win._dpr)
z: _isHoriz ? 0 : -1
x: Theme.snap(_notifBodyBlurAnchor.x - (_isHoriz ? _notifCcr : (_notifSide === "left" ? _sideUnderlap : 0)), win._dpr)
y: Theme.snap(_notifBodyBlurAnchor.y - (_isHoriz ? 0 : _notifCcr), win._dpr)
width: _isHoriz ? Theme.snap(_bodyW + _notifCcr * 2, win._dpr) : _bodyW
height: Theme.snap(_bodyH + (_isHoriz ? 0 : _notifCcr * 2), win._dpr)
ConnectedShape {
visible: _notifBodyBlurAnchor._active && _notifBodyBlurAnchor.width > 0 && _notifBodyBlurAnchor.height > 0
barSide: _notifChrome._notifSide
bodyWidth: _notifChrome._bodyW
bodyHeight: _notifChrome._bodyH
connectorRadius: _notifChrome._notifCcr
surfaceRadius: win._surfaceRadius
fillColor: win._opaqueSurfaceColor
x: 0
y: 0
}
}
}
}
@@ -34,11 +34,12 @@ Rectangle {
readonly property real actionButtonHeight: compactMode ? 20 : 24 readonly property real actionButtonHeight: compactMode ? 20 : 24
readonly property real collapsedContentHeight: Math.max(iconSize, Theme.fontSizeSmall * 1.2 + Theme.fontSizeMedium * 1.2 + Theme.fontSizeSmall * 1.2 * (compactMode ? 1 : 2)) readonly property real collapsedContentHeight: Math.max(iconSize, Theme.fontSizeSmall * 1.2 + Theme.fontSizeMedium * 1.2 + Theme.fontSizeSmall * 1.2 * (compactMode ? 1 : 2))
readonly property real baseCardHeight: cardPadding * 2 + collapsedContentHeight + actionButtonHeight + contentSpacing readonly property real baseCardHeight: cardPadding * 2 + collapsedContentHeight + actionButtonHeight + contentSpacing
readonly property bool connectedFrameMode: SettingsData.connectedFrameModeActive
width: parent ? parent.width : 400 width: parent ? parent.width : 400
height: expanded ? (expandedContent.height + cardPadding * 2) : (baseCardHeight + collapsedContent.extraHeight) height: expanded ? (expandedContent.height + cardPadding * 2) : (baseCardHeight + collapsedContent.extraHeight)
readonly property real targetHeight: expanded ? (expandedContent.height + cardPadding * 2) : (baseCardHeight + collapsedContent.extraHeight) readonly property real targetHeight: expanded ? (expandedContent.height + cardPadding * 2) : (baseCardHeight + collapsedContent.extraHeight)
radius: Theme.cornerRadius radius: connectedFrameMode ? Theme.connectedSurfaceRadius : Theme.cornerRadius
scale: (cardHoverHandler.hovered ? 1.004 : 1.0) * listLevelAdjacentScaleInfluence scale: (cardHoverHandler.hovered ? 1.004 : 1.0) * listLevelAdjacentScaleInfluence
readonly property bool shadowsAllowed: Theme.elevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1" readonly property bool shadowsAllowed: Theme.elevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1"
readonly property var shadowElevation: Theme.elevationLevel1 readonly property var shadowElevation: Theme.elevationLevel1
@@ -100,6 +101,8 @@ Rectangle {
if (keyboardNavigationActive && expanded && selectedNotificationIndex >= 0) { if (keyboardNavigationActive && expanded && selectedNotificationIndex >= 0) {
return Theme.primaryHoverLight; return Theme.primaryHoverLight;
} }
if (connectedFrameMode)
return Theme.popupLayerColor(Theme.surfaceContainerHigh);
return Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency); return Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency);
} }
border.color: { border.color: {
@@ -959,9 +962,9 @@ Rectangle {
Behavior on height { Behavior on height {
enabled: root.__initialized && root.userInitiatedExpansion && root.animateExpansion enabled: root.__initialized && root.userInitiatedExpansion && root.animateExpansion
NumberAnimation { NumberAnimation {
duration: root.expanded ? Theme.notificationExpandDuration : Theme.notificationCollapseDuration duration: root.connectedFrameMode ? Theme.variantDuration(Theme.popoutAnimationDuration, root.expanded) : (root.expanded ? Theme.notificationExpandDuration : Theme.notificationCollapseDuration)
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.emphasized easing.bezierCurve: root.connectedFrameMode ? (root.expanded ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve) : Theme.expressiveCurves.emphasized
onRunningChanged: { onRunningChanged: {
if (running) { if (running) {
root.isAnimating = true; root.isAnimating = true;
@@ -39,11 +39,9 @@ DankPopout {
} }
} }
popupWidth: triggerScreen ? Math.min(500, Math.max(380, triggerScreen.width - 48)) : 400 popupWidth: 400
popupHeight: stablePopupHeight popupHeight: stablePopupHeight
positioning: "" positioning: ""
animationScaleCollapsed: 0.94
animationOffset: 0
suspendShadowWhileResizing: false suspendShadowWhileResizing: false
screen: triggerScreen screen: triggerScreen
@@ -10,13 +10,29 @@ import qs.Widgets
PanelWindow { PanelWindow {
id: win id: win
readonly property bool connectedFrameMode: SettingsData.frameEnabled
&& Theme.isConnectedEffect
&& SettingsData.isScreenInPreferences(win.screen, SettingsData.frameScreenPreferences)
readonly property string notifBarSide: {
const pos = SettingsData.notificationPopupPosition;
if (pos === -1) return "top";
switch (pos) {
case SettingsData.Position.Top: return "right";
case SettingsData.Position.Left: return "left";
case SettingsData.Position.BottomCenter: return "bottom";
case SettingsData.Position.Right: return "right";
case SettingsData.Position.Bottom: return "left";
default: return "top";
}
}
WindowBlur { WindowBlur {
targetWindow: win targetWindow: win
blurX: content.x + content.cardInset + swipeTx.x + tx.x blurX: content.x + content.cardInset + swipeTx.x + tx.x
blurY: content.y + content.cardInset + swipeTx.y + tx.y blurY: content.y + content.cardInset + swipeTx.y + tx.y
blurWidth: !win._finalized ? Math.max(0, content.width - content.cardInset * 2) : 0 blurWidth: !win._finalized && !win.connectedFrameMode ? Math.max(0, content.width - content.cardInset * 2) : 0
blurHeight: !win._finalized ? Math.max(0, content.height - content.cardInset * 2) : 0 blurHeight: !win._finalized && !win.connectedFrameMode ? Math.max(0, content.height - content.cardInset * 2) : 0
blurRadius: Theme.cornerRadius blurRadius: win.connectedFrameMode ? Theme.connectedSurfaceRadius : Theme.cornerRadius
} }
WlrLayershell.namespace: "dms:notification-popup" WlrLayershell.namespace: "dms:notification-popup"
@@ -32,6 +48,29 @@ PanelWindow {
property real _lastReportedAlignedHeight: -1 property real _lastReportedAlignedHeight: -1
property real _storedTopMargin: 0 property real _storedTopMargin: 0
property real _storedBottomMargin: 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") readonly property string clearText: I18n.tr("Dismiss")
property bool descriptionExpanded: false property bool descriptionExpanded: false
readonly property bool hasExpandableBody: (notificationData?.htmlBody || "").replace(/<[^>]*>/g, "").trim().length > 0 readonly property bool hasExpandableBody: (notificationData?.htmlBody || "").replace(/<[^>]*>/g, "").trim().length > 0
@@ -61,6 +100,7 @@ PanelWindow {
signal exitStarted signal exitStarted
signal exitFinished signal exitFinished
signal popupHeightChanged signal popupHeightChanged
signal popupChromeGeometryChanged
function startExit() { function startExit() {
if (exiting || _isDestroying) { if (exiting || _isDestroying) {
@@ -68,6 +108,7 @@ PanelWindow {
} }
exiting = true; exiting = true;
exitStarted(); exitStarted();
popupChromeGeometryChanged();
exitAnim.restart(); exitAnim.restart();
exitWatchdog.restart(); exitWatchdog.restart();
if (NotificationService.removeFromVisibleNotifications) if (NotificationService.removeFromVisibleNotifications)
@@ -145,9 +186,10 @@ PanelWindow {
enabled: !exiting && !_isDestroying enabled: !exiting && !_isDestroying
NumberAnimation { NumberAnimation {
id: implicitHeightAnim id: implicitHeightAnim
duration: descriptionExpanded ? Theme.notificationExpandDuration : Theme.notificationCollapseDuration duration: Theme.variantDuration(descriptionExpanded ? Theme.notificationExpandDuration : Theme.notificationCollapseDuration, descriptionExpanded)
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.emphasized easing.bezierCurve: descriptionExpanded ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
onFinished: win.popupHeightChanged()
} }
} }
@@ -240,12 +282,24 @@ PanelWindow {
}); });
} }
function _frameEdgeInset(side) {
if (!screen)
return 0;
const edges = SettingsData.getActiveBarEdgesForScreen(screen);
const raw = edges.includes(side) ? SettingsData.frameBarSize : SettingsData.frameThickness;
return Math.max(0, Math.round(Theme.px(raw, dpr)));
}
function getTopMargin() { function getTopMargin() {
const popupPos = SettingsData.notificationPopupPosition; const popupPos = SettingsData.notificationPopupPosition;
const isTop = isTopCenter || popupPos === SettingsData.Position.Top || popupPos === SettingsData.Position.Left; const isTop = isTopCenter || popupPos === SettingsData.Position.Top || popupPos === SettingsData.Position.Left;
if (!isTop) if (!isTop)
return 0; return 0;
if (connectedFrameMode) {
const cornerClear = isCenterPosition ? 0 : (Theme.px(SettingsData.frameRounding, dpr) + Theme.px(Theme.connectedCornerRadius, dpr));
return _frameEdgeInset("top") + cornerClear + screenY;
}
const barInfo = getBarInfo(); const barInfo = getBarInfo();
const base = barInfo.topBar > 0 ? barInfo.topBar : Theme.popupDistance; const base = barInfo.topBar > 0 ? barInfo.topBar : Theme.popupDistance;
return base + screenY; return base + screenY;
@@ -257,6 +311,10 @@ PanelWindow {
if (!isBottom) if (!isBottom)
return 0; return 0;
if (connectedFrameMode) {
const cornerClear = isCenterPosition ? 0 : (Theme.px(SettingsData.frameRounding, dpr) + Theme.px(Theme.connectedCornerRadius, dpr));
return _frameEdgeInset("bottom") + cornerClear + screenY;
}
const barInfo = getBarInfo(); const barInfo = getBarInfo();
const base = barInfo.bottomBar > 0 ? barInfo.bottomBar : Theme.popupDistance; const base = barInfo.bottomBar > 0 ? barInfo.bottomBar : Theme.popupDistance;
return base + screenY; return base + screenY;
@@ -271,6 +329,8 @@ PanelWindow {
if (!isLeft) if (!isLeft)
return 0; return 0;
if (connectedFrameMode)
return _frameEdgeInset("left");
const barInfo = getBarInfo(); const barInfo = getBarInfo();
return barInfo.leftBar > 0 ? barInfo.leftBar : Theme.popupDistance; return barInfo.leftBar > 0 ? barInfo.leftBar : Theme.popupDistance;
} }
@@ -284,6 +344,8 @@ PanelWindow {
if (!isRight) if (!isRight)
return 0; return 0;
if (connectedFrameMode)
return _frameEdgeInset("right");
const barInfo = getBarInfo(); const barInfo = getBarInfo();
return barInfo.rightBar > 0 ? barInfo.rightBar : Theme.popupDistance; return barInfo.rightBar > 0 ? barInfo.rightBar : Theme.popupDistance;
} }
@@ -328,10 +390,64 @@ PanelWindow {
return Theme.snap(getContentY() - windowShadowPad, dpr); return Theme.snap(getContentY() - windowShadowPad, dpr);
} }
function _swipeDismissTarget() {
return (content.swipeDismissDirection < 0 ? -1 : 1) * content.width;
}
function _frameEdgeSwipeDirection() {
const popupPos = SettingsData.notificationPopupPosition;
return (popupPos === SettingsData.Position.Left || popupPos === SettingsData.Position.Bottom) ? -1 : 1;
}
function _swipeDismissesTowardFrameEdge() {
return content.swipeDismissDirection === _frameEdgeSwipeDirection();
}
function popupChromeMotionActive() {
return exiting || content.swipeActive || content.swipeDismissing || Math.abs(content.swipeOffset) > 0.5;
}
function popupLayoutReservesSlot() {
return !content.swipeDismissing;
}
function popupChromeReservesSlot() {
return !content.swipeDismissing;
}
function popupChromeReleaseProgress() {
if (content.swipeDismissing)
return Math.max(0, Math.min(1, Math.abs(content.swipeOffset) / Math.max(1, content.swipeTravelDistance)));
if (!exiting)
return 0;
const exitOffset = isCenterPosition ? tx.y : tx.x;
return Math.max(0, Math.min(1, Math.abs(exitOffset) / Math.max(1, exitTravel)));
}
function popupChromeMotionX() {
if (!popupChromeMotionActive() || isCenterPosition)
return 0;
const motion = content.swipeOffset + tx.x;
if (content.swipeDismissing && !_swipeDismissesTowardFrameEdge())
return exiting ? Theme.snap(tx.x, dpr) : 0;
if (content.swipeActive && motion * _frameEdgeSwipeDirection() < 0)
return 0;
return Theme.snap(motion, dpr);
}
function popupChromeMotionY() {
return popupChromeMotionActive() ? Theme.snap(tx.y, dpr) : 0;
}
readonly property bool screenValid: win.screen && !_isDestroying readonly property bool screenValid: win.screen && !_isDestroying
readonly property real dpr: screenValid ? CompositorService.getScreenScale(win.screen) : 1 readonly property real dpr: screenValid ? CompositorService.getScreenScale(win.screen) : 1
readonly property real alignedWidth: Theme.px(Math.max(0, implicitWidth - (windowShadowPad * 2)), dpr) readonly property real alignedWidth: Theme.px(Math.max(0, implicitWidth - (windowShadowPad * 2)), dpr)
readonly property real alignedHeight: Theme.px(Math.max(0, implicitHeight - (windowShadowPad * 2)), dpr) readonly property real alignedHeight: Theme.px(Math.max(0, implicitHeight - (windowShadowPad * 2)), dpr)
onScreenYChanged: popupChromeGeometryChanged()
onScreenChanged: popupChromeGeometryChanged()
onConnectedFrameModeChanged: popupChromeGeometryChanged()
onAlignedWidthChanged: popupChromeGeometryChanged()
onAlignedHeightChanged: popupChromeGeometryChanged()
Item { Item {
id: content id: content
@@ -340,7 +456,7 @@ PanelWindow {
y: Theme.snap(windowShadowPad, dpr) y: Theme.snap(windowShadowPad, dpr)
width: alignedWidth width: alignedWidth
height: alignedHeight height: alignedHeight
visible: !win._finalized visible: !win._finalized && !chromeOnlyExit
scale: cardHoverHandler.hovered ? 1.01 : 1.0 scale: cardHoverHandler.hovered ? 1.01 : 1.0
transformOrigin: Item.Center transformOrigin: Item.Center
@@ -352,13 +468,25 @@ PanelWindow {
} }
property real swipeOffset: 0 property real swipeOffset: 0
readonly property real dismissThreshold: isCenterPosition ? height * 0.4 : width * 0.35 property real swipeDismissDirection: 1
property bool chromeOnlyExit: false
readonly property real dismissThreshold: width * 0.35
readonly property real swipeFadeStartRatio: 0.75 readonly property real swipeFadeStartRatio: 0.75
readonly property real swipeTravelDistance: isCenterPosition ? height : width readonly property real swipeTravelDistance: width
readonly property real swipeFadeStartOffset: swipeTravelDistance * swipeFadeStartRatio readonly property real swipeFadeStartOffset: swipeTravelDistance * swipeFadeStartRatio
readonly property real swipeFadeDistance: Math.max(1, swipeTravelDistance - swipeFadeStartOffset) readonly property real swipeFadeDistance: Math.max(1, swipeTravelDistance - swipeFadeStartOffset)
readonly property bool swipeActive: swipeDragHandler.active readonly property bool swipeActive: swipeDragHandler.active
property bool swipeDismissing: false property bool swipeDismissing: false
onSwipeDismissingChanged: {
if (!win.connectedFrameMode)
return;
win.popupHeightChanged();
win.popupChromeGeometryChanged();
}
onSwipeOffsetChanged: {
if (win.connectedFrameMode)
win.popupChromeGeometryChanged();
}
readonly property bool shadowsAllowed: Theme.elevationEnabled && SettingsData.notificationPopupShadowEnabled readonly property bool shadowsAllowed: Theme.elevationEnabled && SettingsData.notificationPopupShadowEnabled
readonly property var elevLevel: cardHoverHandler.hovered ? Theme.elevationLevel4 : Theme.elevationLevel3 readonly property var elevLevel: cardHoverHandler.hovered ? Theme.elevationLevel4 : Theme.elevationLevel3
@@ -399,7 +527,7 @@ PanelWindow {
shadowOffsetX: content.shadowOffsetX shadowOffsetX: content.shadowOffsetX
shadowOffsetY: content.shadowOffsetY shadowOffsetY: content.shadowOffsetY
shadowColor: content.shadowsAllowed && content.elevLevel ? Theme.elevationShadowColor(content.elevLevel) : "transparent" shadowColor: content.shadowsAllowed && content.elevLevel ? Theme.elevationShadowColor(content.elevLevel) : "transparent"
shadowEnabled: !win._isDestroying && win.screenValid && content.shadowsAllowed shadowEnabled: !win._isDestroying && win.screenValid && content.shadowsAllowed && !win.connectedFrameMode
layer.textureSize: Qt.size(Math.round(width * win.dpr), Math.round(height * win.dpr)) layer.textureSize: Qt.size(Math.round(width * win.dpr), Math.round(height * win.dpr))
layer.textureMirroring: ShaderEffectSource.MirrorVertically layer.textureMirroring: ShaderEffectSource.MirrorVertically
@@ -408,8 +536,12 @@ PanelWindow {
sourceRect.y: content.shadowRenderPadding + content.cardInset sourceRect.y: content.shadowRenderPadding + content.cardInset
sourceRect.width: Math.max(0, content.width - (content.cardInset * 2)) sourceRect.width: Math.max(0, content.width - (content.cardInset * 2))
sourceRect.height: Math.max(0, content.height - (content.cardInset * 2)) sourceRect.height: Math.max(0, content.height - (content.cardInset * 2))
sourceRect.radius: Theme.cornerRadius sourceRect.radius: win.connectedFrameMode ? Theme.connectedSurfaceRadius : Theme.cornerRadius
sourceRect.color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) sourceRect.color: win.connectedFrameMode ? Theme.popupLayerColor(Theme.surfaceContainer) : Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
sourceRect.antialiasing: true
sourceRect.layer.enabled: win.connectedFrameMode
sourceRect.layer.smooth: true
sourceRect.layer.textureSize: win.connectedFrameMode && win.dpr > 1 ? Qt.size(Math.ceil(sourceRect.width * win.dpr), Math.ceil(sourceRect.height * win.dpr)) : Qt.size(0, 0)
sourceRect.border.color: notificationData && notificationData.urgency === NotificationUrgency.Critical ? Theme.withAlpha(Theme.primary, 0.3) : Theme.withAlpha(Theme.outline, 0.08) sourceRect.border.color: notificationData && notificationData.urgency === NotificationUrgency.Critical ? Theme.withAlpha(Theme.primary, 0.3) : Theme.withAlpha(Theme.outline, 0.08)
sourceRect.border.width: notificationData && notificationData.urgency === NotificationUrgency.Critical ? 2 : 0 sourceRect.border.width: notificationData && notificationData.urgency === NotificationUrgency.Critical ? 2 : 0
@@ -447,10 +579,10 @@ PanelWindow {
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
anchors.margins: content.cardInset anchors.margins: content.cardInset
radius: Theme.cornerRadius radius: win.connectedFrameMode ? Theme.connectedSurfaceRadius : Theme.cornerRadius
color: "transparent" color: "transparent"
border.color: BlurService.borderColor border.color: win.connectedFrameMode ? "transparent" : BlurService.borderColor
border.width: BlurService.borderWidth border.width: win.connectedFrameMode ? 0 : BlurService.borderWidth
z: 100 z: 100
} }
@@ -850,14 +982,15 @@ PanelWindow {
DragHandler { DragHandler {
id: swipeDragHandler id: swipeDragHandler
target: null target: null
xAxis.enabled: !isCenterPosition xAxis.enabled: true
yAxis.enabled: isCenterPosition yAxis.enabled: false
onActiveChanged: { onActiveChanged: {
if (active || win.exiting || content.swipeDismissing) if (active || win.exiting || content.swipeDismissing)
return; return;
if (Math.abs(content.swipeOffset) > content.dismissThreshold) { if (Math.abs(content.swipeOffset) > content.dismissThreshold) {
content.swipeDismissDirection = content.swipeOffset < 0 ? -1 : 1;
content.swipeDismissing = true; content.swipeDismissing = true;
swipeDismissAnim.start(); swipeDismissAnim.start();
} else { } else {
@@ -869,15 +1002,7 @@ PanelWindow {
if (win.exiting) if (win.exiting)
return; return;
const raw = isCenterPosition ? translation.y : translation.x; content.swipeOffset = translation.x;
if (isTopCenter) {
content.swipeOffset = Math.min(0, raw);
} else if (isBottomCenter) {
content.swipeOffset = Math.max(0, raw);
} else {
const isLeft = SettingsData.notificationPopupPosition === SettingsData.Position.Left || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom;
content.swipeOffset = isLeft ? Math.min(0, raw) : Math.max(0, raw);
}
} }
} }
@@ -908,20 +1033,28 @@ PanelWindow {
id: swipeDismissAnim id: swipeDismissAnim
target: content target: content
property: "swipeOffset" property: "swipeOffset"
to: isTopCenter ? -content.height : isBottomCenter ? content.height : (SettingsData.notificationPopupPosition === SettingsData.Position.Left || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom ? -content.width : content.width) to: win._swipeDismissTarget()
duration: Theme.notificationExitDuration duration: Theme.notificationExitDuration
easing.type: Easing.OutCubic easing.type: Easing.OutCubic
onStopped: { onStopped: {
NotificationService.dismissNotification(notificationData); const inwardConnectedExit = win.connectedFrameMode && !win.isCenterPosition && !win._swipeDismissesTowardFrameEdge();
win.forceExit(); if (inwardConnectedExit)
content.chromeOnlyExit = true;
if (win.connectedFrameMode && (win.isCenterPosition || inwardConnectedExit)) {
win.startExit();
NotificationService.dismissNotification(notificationData);
} else {
NotificationService.dismissNotification(notificationData);
win.forceExit();
}
} }
} }
transform: [ transform: [
Translate { Translate {
id: swipeTx id: swipeTx
x: isCenterPosition ? 0 : content.swipeOffset x: content.swipeOffset
y: isCenterPosition ? content.swipeOffset : 0 y: 0
}, },
Translate { Translate {
id: tx id: tx
@@ -929,9 +1062,17 @@ PanelWindow {
if (isCenterPosition) if (isCenterPosition)
return 0; return 0;
const isLeft = SettingsData.notificationPopupPosition === SettingsData.Position.Left || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom; const isLeft = SettingsData.notificationPopupPosition === SettingsData.Position.Left || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom;
return isLeft ? -Anims.slidePx : Anims.slidePx; return isLeft ? -entryTravel : entryTravel;
}
y: isTopCenter ? -entryTravel : isBottomCenter ? entryTravel : 0
onXChanged: {
if (win.connectedFrameMode)
win.popupChromeGeometryChanged();
}
onYChanged: {
if (win.connectedFrameMode)
win.popupChromeGeometryChanged();
} }
y: isTopCenter ? -Anims.slidePx : isBottomCenter ? Anims.slidePx : 0
} }
] ]
} }
@@ -943,16 +1084,16 @@ PanelWindow {
property: isCenterPosition ? "y" : "x" property: isCenterPosition ? "y" : "x"
from: { from: {
if (isTopCenter) if (isTopCenter)
return -Anims.slidePx; return -entryTravel;
if (isBottomCenter) if (isBottomCenter)
return Anims.slidePx; return entryTravel;
const isLeft = SettingsData.notificationPopupPosition === SettingsData.Position.Left || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom; const isLeft = SettingsData.notificationPopupPosition === SettingsData.Position.Left || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom;
return isLeft ? -Anims.slidePx : Anims.slidePx; return isLeft ? -entryTravel : entryTravel;
} }
to: 0 to: 0
duration: Theme.notificationEnterDuration duration: Theme.variantDuration(Theme.notificationEnterDuration, true)
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: isCenterPosition ? Theme.expressiveCurves.standardDecel : Theme.expressiveCurves.emphasizedDecel easing.bezierCurve: Theme.variantPopoutEnterCurve
onStopped: { onStopped: {
if (!win.exiting && !win._isDestroying) { if (!win.exiting && !win._isDestroying) {
if (isCenterPosition) { if (isCenterPosition) {
@@ -977,35 +1118,35 @@ PanelWindow {
from: 0 from: 0
to: { to: {
if (isTopCenter) if (isTopCenter)
return -Anims.slidePx; return -exitTravel;
if (isBottomCenter) if (isBottomCenter)
return Anims.slidePx; return exitTravel;
const isLeft = SettingsData.notificationPopupPosition === SettingsData.Position.Left || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom; 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.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.emphasizedAccel easing.bezierCurve: Theme.variantPopoutExitCurve
} }
NumberAnimation { NumberAnimation {
target: content target: content
property: "opacity" property: "opacity"
from: 1 from: 1
to: 0 to: Theme.isDirectionalEffect ? 1 : 0
duration: Theme.notificationExitDuration duration: Theme.variantDuration(Theme.notificationExitDuration, false)
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.standardAccel easing.bezierCurve: Theme.variantPopoutExitCurve
} }
NumberAnimation { NumberAnimation {
target: content target: content
property: "scale" property: "scale"
from: 1 from: 1
to: 0.98 to: Theme.isDirectionalEffect ? 1 : Theme.effectScaleCollapsed
duration: Theme.notificationExitDuration duration: Theme.variantDuration(Theme.notificationExitDuration, false)
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.emphasizedAccel easing.bezierCurve: Theme.variantPopoutExitCurve
} }
} }
@@ -8,23 +8,40 @@ QtObject {
property var modelData property var modelData
property int topMargin: 0 property int topMargin: 0
readonly property bool compactMode: SettingsData.notificationCompactMode readonly property bool compactMode: SettingsData.notificationCompactMode
readonly property bool notificationConnectedMode: SettingsData.frameEnabled
&& Theme.isConnectedEffect
&& SettingsData.isScreenInPreferences(manager.modelData, SettingsData.frameScreenPreferences)
readonly property string notifBarSide: {
const pos = SettingsData.notificationPopupPosition;
if (pos === -1) return "top";
switch (pos) {
case SettingsData.Position.Top: return "right";
case SettingsData.Position.Left: return "left";
case SettingsData.Position.BottomCenter: return "bottom";
case SettingsData.Position.Right: return "right";
case SettingsData.Position.Bottom: return "left";
default: return "top";
}
}
readonly property real cardPadding: compactMode ? Theme.notificationCardPaddingCompact : Theme.notificationCardPadding readonly property real cardPadding: compactMode ? Theme.notificationCardPaddingCompact : Theme.notificationCardPadding
readonly property real popupIconSize: compactMode ? Theme.notificationIconSizeCompact : Theme.notificationIconSizeNormal readonly property real popupIconSize: compactMode ? Theme.notificationIconSizeCompact : Theme.notificationIconSizeNormal
readonly property real actionButtonHeight: compactMode ? 20 : 24 readonly property real actionButtonHeight: compactMode ? 20 : 24
readonly property real contentSpacing: compactMode ? Theme.spacingXS : Theme.spacingS readonly property real contentSpacing: compactMode ? Theme.spacingXS : Theme.spacingS
readonly property real popupSpacing: compactMode ? 0 : Theme.spacingXS readonly property real popupSpacing: notificationConnectedMode ? 0 : (compactMode ? 0 : Theme.spacingXS)
readonly property real collapsedContentHeight: Math.max(popupIconSize, Theme.fontSizeSmall * 1.2 + Theme.fontSizeMedium * 1.2 + Theme.fontSizeSmall * 1.2 * (compactMode ? 1 : 2)) readonly property real collapsedContentHeight: Math.max(popupIconSize, Theme.fontSizeSmall * 1.2 + Theme.fontSizeMedium * 1.2 + Theme.fontSizeSmall * 1.2 * (compactMode ? 1 : 2))
readonly property int baseNotificationHeight: cardPadding * 2 + collapsedContentHeight + actionButtonHeight + contentSpacing + popupSpacing readonly property int baseNotificationHeight: cardPadding * 2 + collapsedContentHeight + actionButtonHeight + contentSpacing + popupSpacing
property var popupWindows: [] property var popupWindows: []
property var destroyingWindows: new Set() property var destroyingWindows: new Set()
property var pendingDestroys: [] property var pendingDestroys: []
property int destroyDelayMs: 100 property int destroyDelayMs: 100
property bool _chromeSyncPending: false
property Component popupComponent property Component popupComponent
popupComponent: Component { popupComponent: Component {
NotificationPopup { NotificationPopup {
onExitFinished: manager._onPopupExitFinished(this) onExitFinished: manager._onPopupExitFinished(this)
onPopupHeightChanged: manager._onPopupHeightChanged(this) onPopupHeightChanged: manager._onPopupHeightChanged(this)
onPopupChromeGeometryChanged: manager._onPopupChromeGeometryChanged(this)
} }
} }
@@ -108,6 +125,14 @@ QtObject {
return p && p.status !== Component.Null && !p._isDestroying && p.hasValidData; return p && p.status !== Component.Null && !p._isDestroying && p.hasValidData;
} }
function _layoutWindows() {
return popupWindows.filter(p => _isValidWindow(p) && p.notificationData?.popup && !p.exiting && (!p.popupLayoutReservesSlot || p.popupLayoutReservesSlot()));
}
function _chromeWindows() {
return popupWindows.filter(p => p && p.status !== Component.Null && p.visible && !p._finalized && p.hasValidData && (p.notificationData?.popup || p.exiting));
}
function _isFocusedScreen() { function _isFocusedScreen() {
if (!SettingsData.notificationFocusedMonitor) if (!SettingsData.notificationFocusedMonitor)
return true; return true;
@@ -116,18 +141,24 @@ QtObject {
} }
function _sync(newWrappers) { function _sync(newWrappers) {
let needsReposition = false;
for (const p of popupWindows.slice()) { for (const p of popupWindows.slice()) {
if (!_isValidWindow(p) || p.exiting) if (!_isValidWindow(p) || p.exiting)
continue; continue;
if (p.notificationData && newWrappers.indexOf(p.notificationData) === -1) { if (p.notificationData && newWrappers.indexOf(p.notificationData) === -1) {
p.notificationData.removedByLimit = true; p.notificationData.removedByLimit = true;
p.notificationData.popup = false; p.notificationData.popup = false;
needsReposition = true;
} }
} }
for (const w of newWrappers) { for (const w of newWrappers) {
if (w && !_hasWindowFor(w) && _isFocusedScreen()) if (w && !_hasWindowFor(w) && _isFocusedScreen()) {
_insertAtTop(w); _insertAtTop(w);
needsReposition = false;
}
} }
if (needsReposition)
_repositionAll();
} }
function _popupHeight(p) { function _popupHeight(p) {
@@ -157,7 +188,7 @@ QtObject {
} }
function _repositionAll() { function _repositionAll() {
const active = popupWindows.filter(p => _isValidWindow(p) && p.notificationData?.popup && !p.exiting); const active = _layoutWindows();
const pinnedSlots = []; const pinnedSlots = [];
for (const p of active) { for (const p of active) {
@@ -181,6 +212,168 @@ QtObject {
win.screenY = currentY; win.screenY = currentY;
currentY += _popupHeight(win); currentY += _popupHeight(win);
} }
_scheduleNotificationChromeSync();
}
function _scheduleNotificationChromeSync() {
if (_chromeSyncPending)
return;
_chromeSyncPending = true;
Qt.callLater(() => {
_chromeSyncPending = false;
_syncNotificationChromeState();
});
}
function _popupChromeRect(p, useMotionOffset) {
if (!p || !p.screen)
return null;
const motionX = useMotionOffset && p.popupChromeMotionX ? p.popupChromeMotionX() : 0;
const motionY = useMotionOffset && p.popupChromeMotionY ? p.popupChromeMotionY() : 0;
const x = (p.getContentX ? p.getContentX() : 0) + motionX;
const y = (p.getContentY ? p.getContentY() : 0) + motionY;
const w = p.alignedWidth || 0;
const h = Math.max(p.alignedHeight || 0, baseNotificationHeight);
if (w <= 0 || h <= 0)
return null;
return {
x: x,
y: y,
right: x + w,
bottom: y + h
};
}
function _popupChromeBoundsRect(p, trailing, useMotionOffset) {
const rect = _popupChromeRect(p, useMotionOffset);
if (!rect || p !== trailing || !p.popupChromeReleaseProgress)
return rect;
const progress = Math.max(0, Math.min(1, p.popupChromeReleaseProgress()));
if (progress <= 0)
return rect;
const anchorsTop = _stackAnchorsTop();
const h = Math.max(0, rect.bottom - rect.y);
const shrink = h * progress;
if (anchorsTop)
rect.bottom = Math.max(rect.y, rect.bottom - shrink);
else
rect.y = Math.min(rect.bottom, rect.y + shrink);
return rect;
}
function _stackAnchorsTop() {
const pos = SettingsData.notificationPopupPosition;
return pos === -1 || pos === SettingsData.Position.Top || pos === SettingsData.Position.Left;
}
function _trailingChromeWindow(candidates) {
const anchorsTop = _stackAnchorsTop();
let trailing = null;
let edge = anchorsTop ? -Infinity : Infinity;
for (const p of candidates) {
const rect = _popupChromeRect(p, false);
if (!rect)
continue;
const candidateEdge = anchorsTop ? rect.bottom : rect.y;
if ((anchorsTop && candidateEdge > edge) || (!anchorsTop && candidateEdge < edge)) {
edge = candidateEdge;
trailing = p;
}
}
return trailing;
}
function _chromeWindowReservesSlot(p, trailing) {
if (p === trailing)
return true;
return !p.popupChromeReservesSlot || p.popupChromeReservesSlot();
}
function _stackAnchoredChromeEdge(candidates) {
const anchorsTop = _stackAnchorsTop();
let edge = anchorsTop ? Infinity : -Infinity;
for (const p of candidates) {
const rect = _popupChromeRect(p, false);
if (!rect)
continue;
if (anchorsTop && rect.y < edge)
edge = rect.y;
if (!anchorsTop && rect.bottom > edge)
edge = rect.bottom;
}
if (edge === Infinity || edge === -Infinity)
return null;
return {
anchorsTop: anchorsTop,
edge: edge
};
}
function _syncNotificationChromeState() {
const screenName = manager.modelData?.name || "";
if (!screenName)
return;
if (!notificationConnectedMode) {
ConnectedModeState.clearNotificationState(screenName);
return;
}
const chromeCandidates = _chromeWindows();
if (chromeCandidates.length === 0) {
ConnectedModeState.clearNotificationState(screenName);
return;
}
const trailing = chromeCandidates.length > 1 ? _trailingChromeWindow(chromeCandidates) : null;
let active = chromeCandidates;
if (chromeCandidates.length > 1) {
const reserving = chromeCandidates.filter(p => _chromeWindowReservesSlot(p, trailing));
if (reserving.length > 0)
active = reserving;
}
let minX = Infinity;
let minY = Infinity;
let maxXEnd = -Infinity;
let maxYEnd = -Infinity;
const useMotionOffset = active.length === 1 && active[0].popupChromeMotionActive && active[0].popupChromeMotionActive();
for (const p of active) {
const rect = _popupChromeBoundsRect(p, trailing, useMotionOffset);
if (!rect)
continue;
if (rect.x < minX)
minX = rect.x;
if (rect.y < minY)
minY = rect.y;
if (rect.right > maxXEnd)
maxXEnd = rect.right;
if (rect.bottom > maxYEnd)
maxYEnd = rect.bottom;
}
const stackEdge = _stackAnchoredChromeEdge(chromeCandidates);
if (stackEdge !== null) {
if (stackEdge.anchorsTop && stackEdge.edge < minY)
minY = stackEdge.edge;
if (!stackEdge.anchorsTop && stackEdge.edge > maxYEnd)
maxYEnd = stackEdge.edge;
}
if (minX === Infinity || minY === Infinity || maxXEnd <= minX || maxYEnd <= minY) {
ConnectedModeState.clearNotificationState(screenName);
return;
}
ConnectedModeState.setNotificationState(screenName, {
visible: true,
barSide: notifBarSide,
bodyX: minX,
bodyY: minY,
bodyW: maxXEnd - minX,
bodyH: maxYEnd - minY
});
}
function _onPopupChromeGeometryChanged(p) {
if (!p || popupWindows.indexOf(p) === -1)
return;
_scheduleNotificationChromeSync();
} }
function _onPopupHeightChanged(p) { function _onPopupHeightChanged(p) {
@@ -227,8 +420,15 @@ QtObject {
} }
popupWindows = []; popupWindows = [];
destroyingWindows.clear(); destroyingWindows.clear();
_chromeSyncPending = false;
_syncNotificationChromeState();
} }
onNotificationConnectedModeChanged: _scheduleNotificationChromeSync()
onNotifBarSideChanged: _scheduleNotificationChromeSync()
onModelDataChanged: _scheduleNotificationChromeSync()
onTopMarginChanged: _repositionAll()
onPopupWindowsChanged: { onPopupWindowsChanged: {
if (popupWindows.length > 0 && !sweeper.running) { if (popupWindows.length > 0 && !sweeper.running) {
sweeper.start(); sweeper.start();
+102
View File
@@ -27,6 +27,7 @@ Item {
const pos = selectedBarConfig?.position ?? SettingsData.Position.Top; const pos = selectedBarConfig?.position ?? SettingsData.Position.Top;
return pos === SettingsData.Position.Left || pos === SettingsData.Position.Right; return pos === SettingsData.Position.Left || pos === SettingsData.Position.Right;
} }
readonly property bool connectedFrameModeActive: SettingsData.connectedFrameModeActive
Timer { Timer {
id: horizontalBarChangeDebounce id: horizontalBarChangeDebounce
@@ -693,6 +694,8 @@ Item {
SettingsToggleRow { SettingsToggleRow {
visible: CompositorService.isNiri visible: CompositorService.isNiri
enabled: !SettingsData.frameEnabled
opacity: SettingsData.frameEnabled ? 0.5 : 1.0
text: I18n.tr("Show on Overview") text: I18n.tr("Show on Overview")
checked: selectedBarConfig?.openOnOverview ?? false checked: selectedBarConfig?.openOnOverview ?? false
onToggled: toggled => { onToggled: toggled => {
@@ -798,11 +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 { SettingsCard {
iconName: "space_bar" iconName: "space_bar"
title: I18n.tr("Spacing") title: I18n.tr("Spacing")
settingKey: "barSpacing" settingKey: "barSpacing"
visible: selectedBarConfig?.enabled visible: selectedBarConfig?.enabled
enabled: !SettingsData.frameEnabled
opacity: SettingsData.frameEnabled ? 0.5 : 1.0
SettingsSliderRow { SettingsSliderRow {
id: edgeSpacingSlider id: edgeSpacingSlider
@@ -1003,6 +1037,8 @@ Item {
SettingsSliderRow { SettingsSliderRow {
id: barTransparencySlider id: barTransparencySlider
enabled: !SettingsData.frameEnabled
opacity: SettingsData.frameEnabled ? 0.5 : 1.0
text: I18n.tr("Bar Transparency") text: I18n.tr("Bar Transparency")
value: (selectedBarConfig?.transparency ?? 1.0) * 100 value: (selectedBarConfig?.transparency ?? 1.0) * 100
minimum: 0 minimum: 0
@@ -1044,6 +1080,64 @@ Item {
restoreMode: Binding.RestoreBinding restoreMode: Binding.RestoreBinding
} }
} }
Item {
visible: SettingsData.frameEnabled
width: parent.width
implicitHeight: transparencyFrameNote.implicitHeight + Theme.spacingS * 2
Row {
id: transparencyFrameNote
x: Theme.spacingM
width: parent.width - Theme.spacingM * 2
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
name: "frame_source"
size: Theme.fontSizeMedium
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Opacity is controlled by Frame Border Opacity in Frame settings")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width - Theme.fontSizeMedium - Theme.spacingS
}
}
}
}
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 { SettingsCard {
@@ -1054,6 +1148,8 @@ Item {
collapsible: true collapsible: true
expanded: true expanded: true
visible: selectedBarConfig?.enabled visible: selectedBarConfig?.enabled
enabled: !dankBarTab.connectedFrameModeActive
opacity: dankBarTab.connectedFrameModeActive ? 0.5 : 1.0
readonly property bool shadowActive: (selectedBarConfig?.shadowIntensity ?? 0) > 0 readonly property bool shadowActive: (selectedBarConfig?.shadowIntensity ?? 0) > 0
readonly property bool isCustomColor: (selectedBarConfig?.shadowColorMode ?? "default") === "custom" readonly property bool isCustomColor: (selectedBarConfig?.shadowColorMode ?? "default") === "custom"
@@ -1287,6 +1383,8 @@ Item {
SettingsToggleRow { SettingsToggleRow {
text: I18n.tr("Square Corners") text: I18n.tr("Square Corners")
enabled: !SettingsData.frameEnabled
opacity: SettingsData.frameEnabled ? 0.5 : 1.0
checked: selectedBarConfig?.squareCorners ?? false checked: selectedBarConfig?.squareCorners ?? false
onToggled: checked => SettingsData.updateBarConfig(selectedBarId, { onToggled: checked => SettingsData.updateBarConfig(selectedBarId, {
squareCorners: checked squareCorners: checked
@@ -1334,6 +1432,8 @@ Item {
SettingsToggleRow { SettingsToggleRow {
text: I18n.tr("Goth Corners") text: I18n.tr("Goth Corners")
enabled: !SettingsData.frameEnabled
opacity: SettingsData.frameEnabled ? 0.5 : 1.0
checked: selectedBarConfig?.gothCornersEnabled ?? false checked: selectedBarConfig?.gothCornersEnabled ?? false
onToggled: checked => SettingsData.updateBarConfig(selectedBarId, { onToggled: checked => SettingsData.updateBarConfig(selectedBarId, {
gothCornersEnabled: checked gothCornersEnabled: checked
@@ -1383,6 +1483,8 @@ Item {
iconName: "border_style" iconName: "border_style"
title: I18n.tr("Border") title: I18n.tr("Border")
visible: selectedBarConfig?.enabled visible: selectedBarConfig?.enabled
enabled: !dankBarTab.connectedFrameModeActive
opacity: dankBarTab.connectedFrameModeActive ? 0.5 : 1.0
checked: selectedBarConfig?.borderEnabled ?? false checked: selectedBarConfig?.borderEnabled ?? false
onToggled: checked => SettingsData.updateBarConfig(selectedBarId, { onToggled: checked => SettingsData.updateBarConfig(selectedBarId, {
borderEnabled: checked borderEnabled: checked
+40
View File
@@ -7,6 +7,9 @@ import qs.Modules.Settings.Widgets
Item { Item {
id: root id: root
readonly property bool connectedFrameModeActive: SettingsData.frameEnabled
&& SettingsData.motionEffect === 1
&& SettingsData.directionalAnimationMode === 3
FileBrowserModal { FileBrowserModal {
id: dockLogoFileBrowser id: dockLogoFileBrowser
@@ -544,6 +547,8 @@ Item {
SettingsSliderRow { SettingsSliderRow {
text: I18n.tr("Exclusive Zone Offset") text: I18n.tr("Exclusive Zone Offset")
enabled: !root.connectedFrameModeActive
opacity: root.connectedFrameModeActive ? 0.5 : 1.0
value: SettingsData.dockBottomGap value: SettingsData.dockBottomGap
minimum: -100 minimum: -100
maximum: 100 maximum: 100
@@ -553,6 +558,8 @@ Item {
SettingsSliderRow { SettingsSliderRow {
text: I18n.tr("Margin") text: I18n.tr("Margin")
enabled: !root.connectedFrameModeActive
opacity: root.connectedFrameModeActive ? 0.5 : 1.0
value: SettingsData.dockMargin value: SettingsData.dockMargin
minimum: 0 minimum: 0
maximum: 100 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 { SettingsCard {
width: parent.width width: parent.width
iconName: "opacity" iconName: "opacity"
title: I18n.tr("Transparency") title: I18n.tr("Transparency")
settingKey: "dockTransparency" settingKey: "dockTransparency"
enabled: !root.connectedFrameModeActive
opacity: root.connectedFrameModeActive ? 0.5 : 1.0
SettingsSliderRow { SettingsSliderRow {
text: I18n.tr("Dock Transparency") text: I18n.tr("Dock Transparency")
@@ -585,6 +623,8 @@ Item {
settingKey: "dockBorder" settingKey: "dockBorder"
collapsible: true collapsible: true
expanded: false expanded: false
enabled: !root.connectedFrameModeActive
opacity: root.connectedFrameModeActive ? 0.5 : 1.0
SettingsToggleRow { SettingsToggleRow {
text: I18n.tr("Border") text: I18n.tr("Border")
+324
View File
@@ -0,0 +1,324 @@
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")
description: SettingsData.connectedFrameModeActive
? I18n.tr("Controls the radius of the frame and all connected popout, dock, and modal surfaces while Connected Mode is active")
: I18n.tr("Controls the frame border radius. This also becomes the connected surface radius whenever Connected Mode is active")
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)
}
}
}
}
}
+16 -5
View File
@@ -11,6 +11,7 @@ import qs.Modules.Settings.Widgets
Item { Item {
id: themeColorsTab id: themeColorsTab
readonly property bool connectedFrameModeActive: SettingsData.connectedFrameModeActive
property var cachedIconThemes: SettingsData.availableIconThemes property var cachedIconThemes: SettingsData.availableIconThemes
property var cachedCursorThemes: SettingsData.availableCursorThemes property var cachedCursorThemes: SettingsData.availableCursorThemes
property var cachedMatugenSchemes: Theme.availableMatugenSchemes.map(option => option.label) property var cachedMatugenSchemes: Theme.availableMatugenSchemes.map(option => option.label)
@@ -1615,10 +1616,14 @@ Item {
SettingsSliderRow { SettingsSliderRow {
tab: "theme" tab: "theme"
tags: ["popup", "transparency", "opacity", "modal"] tags: ["surface", "popup", "transparency", "opacity", "modal"]
settingKey: "popupTransparency" settingKey: "popupTransparency"
text: I18n.tr("Popup Transparency") text: I18n.tr("Surface Opacity")
description: I18n.tr("Controls opacity of all popouts, modals, and their content layers") 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) value: Math.round(SettingsData.popupTransparency * 100)
minimum: 0 minimum: 0
maximum: 100 maximum: 100
@@ -1632,7 +1637,9 @@ Item {
tags: ["corner", "radius", "rounded", "square"] tags: ["corner", "radius", "rounded", "square"]
settingKey: "cornerRadius" settingKey: "cornerRadius"
text: I18n.tr("Corner Radius") text: I18n.tr("Corner Radius")
description: I18n.tr("0 = square corners") description: themeColorsTab.connectedFrameModeActive
? I18n.tr("Controls general UI rounding. Connected frame popouts, docks, and modal surfaces follow Border Radius in the Frame tab while Connected Frame mode is active")
: I18n.tr("0 = square corners")
value: SettingsData.cornerRadius value: SettingsData.cornerRadius
minimum: 0 minimum: 0
maximum: 32 maximum: 32
@@ -1837,7 +1844,11 @@ Item {
tags: ["blur", "background", "transparency", "glass", "frosted"] tags: ["blur", "background", "transparency", "glass", "frosted"]
settingKey: "blurEnabled" settingKey: "blurEnabled"
text: I18n.tr("Background Blur") text: I18n.tr("Background Blur")
description: BlurService.available ? I18n.tr("Blur the background behind bars, popouts, modals, and notifications. Requires compositor support and configuration.") : I18n.tr("Requires a newer version of Quickshell") 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 checked: SettingsData.blurEnabled ?? false
enabled: BlurService.available enabled: BlurService.available
onToggled: checked => SettingsData.set("blurEnabled", checked) onToggled: checked => SettingsData.set("blurEnabled", checked)
@@ -55,6 +55,192 @@ Item {
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingXL 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 { SettingsCard {
tab: "typography" tab: "typography"
tags: ["font", "family", "text", "typography"] tags: ["font", "family", "text", "typography"]
@@ -83,7 +83,6 @@ Item {
description: modelData.width + "×" + modelData.height description: modelData.width + "×" + modelData.height
checked: localChecked checked: localChecked
onToggled: isChecked => { onToggled: isChecked => {
localChecked = isChecked;
var prefs = JSON.parse(JSON.stringify(root.displayPreferences)); var prefs = JSON.parse(JSON.stringify(root.displayPreferences));
if (!Array.isArray(prefs) || prefs.includes("all")) if (!Array.isArray(prefs) || prefs.includes("all"))
prefs = []; prefs = [];
@@ -94,6 +93,11 @@ Item {
model: modelData.model || "" model: modelData.model || ""
}); });
} }
if (prefs.length === 0) {
localChecked = true;
return;
}
localChecked = isChecked;
root.preferencesChanged(prefs); root.preferencesChanged(prefs);
} }
} }
@@ -121,9 +121,9 @@ Scope {
Behavior on opacity { Behavior on opacity {
NumberAnimation { NumberAnimation {
duration: Theme.expressiveDurations.expressiveDefaultSpatial duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewScope.overviewOpen)
easing.type: Easing.BezierSpline 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 id: scaleTransform
origin.x: contentContainer.width / 2 origin.x: contentContainer.width / 2
origin.y: contentContainer.height / 2 origin.y: contentContainer.height / 2
xScale: overviewScope.overviewOpen ? 1 : 0.96 xScale: overviewScope.overviewOpen ? 1 : Theme.effectScaleCollapsed
yScale: overviewScope.overviewOpen ? 1 : 0.96 yScale: overviewScope.overviewOpen ? 1 : Theme.effectScaleCollapsed
Behavior on xScale { Behavior on xScale {
NumberAnimation { NumberAnimation {
duration: Theme.expressiveDurations.expressiveDefaultSpatial duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewScope.overviewOpen)
easing.type: Easing.BezierSpline 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 { Behavior on yScale {
NumberAnimation { NumberAnimation {
duration: Theme.expressiveDurations.expressiveDefaultSpatial duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewScope.overviewOpen)
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: overviewScope.overviewOpen ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized easing.bezierCurve: overviewScope.overviewOpen ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
} }
} }
} }
Translate { Translate {
id: motionTransform id: motionTransform
x: 0 x: {
y: overviewScope.overviewOpen ? 0 : Theme.spacingL 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 { Behavior on y {
NumberAnimation { NumberAnimation {
duration: Theme.expressiveDurations.expressiveDefaultSpatial duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewScope.overviewOpen)
easing.type: Easing.BezierSpline 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 { Behavior on opacity {
NumberAnimation { NumberAnimation {
duration: Theme.expressiveDurations.expressiveDefaultSpatial duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewScope.overviewOpen)
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: overviewScope.overviewOpen ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized easing.bezierCurve: overviewScope.overviewOpen ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
} }
} }
@@ -202,8 +202,18 @@ Scope {
Item { Item {
id: spotlightContainer id: spotlightContainer
x: Theme.snap((parent.width - width) / 2, overlayWindow.dpr) readonly property bool directionalEffect: Theme.isDirectionalEffect
y: Theme.snap((parent.height - height) / 2, overlayWindow.dpr) 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: { readonly property int baseWidth: {
switch (SettingsData.dankLauncherV2Size) { switch (SettingsData.dankLauncherV2Size) {
@@ -234,8 +244,8 @@ Scope {
readonly property bool animatingOut: niriOverviewScope.isClosing && overlayWindow.isSpotlightScreen readonly property bool animatingOut: niriOverviewScope.isClosing && overlayWindow.isSpotlightScreen
scale: overlayWindow.shouldShowSpotlight ? 1.0 : 0.96 scale: Theme.isDirectionalEffect ? 1 : (overlayWindow.shouldShowSpotlight ? 1.0 : Theme.effectScaleCollapsed)
opacity: overlayWindow.shouldShowSpotlight ? 1 : 0 opacity: Theme.isDirectionalEffect ? 1 : (overlayWindow.shouldShowSpotlight ? 1 : 0)
visible: overlayWindow.shouldShowSpotlight || animatingOut visible: overlayWindow.shouldShowSpotlight || animatingOut
enabled: overlayWindow.shouldShowSpotlight enabled: overlayWindow.shouldShowSpotlight
@@ -245,10 +255,11 @@ Scope {
Behavior on scale { Behavior on scale {
id: scaleAnimation id: scaleAnimation
enabled: !Theme.isDirectionalEffect
NumberAnimation { NumberAnimation {
duration: Theme.expressiveDurations.fast duration: Theme.variantDuration(Theme.expressiveDurations.fast, overlayWindow.shouldShowSpotlight)
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: spotlightContainer.visible ? Theme.expressiveCurves.expressiveFastSpatial : Theme.expressiveCurves.standardAccel easing.bezierCurve: spotlightContainer.visible ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
onRunningChanged: { onRunningChanged: {
if (running || !spotlightContainer.animatingOut) if (running || !spotlightContainer.animatingOut)
return; return;
@@ -258,10 +269,27 @@ Scope {
} }
Behavior on opacity { Behavior on opacity {
enabled: !Theme.isDirectionalEffect
NumberAnimation { NumberAnimation {
duration: Theme.expressiveDurations.fast duration: Theme.variantDuration(Theme.expressiveDurations.fast, overlayWindow.shouldShowSpotlight)
easing.type: Easing.BezierSpline 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
} }
} }
@@ -62,30 +62,30 @@ Item {
Behavior on x { Behavior on x {
NumberAnimation { NumberAnimation {
duration: Theme.expressiveDurations.expressiveDefaultSpatial duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen)
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel easing.bezierCurve: Theme.variantModalEnterCurve
} }
} }
Behavior on y { Behavior on y {
NumberAnimation { NumberAnimation {
duration: Theme.expressiveDurations.expressiveDefaultSpatial duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen)
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel easing.bezierCurve: Theme.variantModalEnterCurve
} }
} }
Behavior on width { Behavior on width {
NumberAnimation { NumberAnimation {
duration: Theme.expressiveDurations.expressiveDefaultSpatial duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen)
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel easing.bezierCurve: Theme.variantModalEnterCurve
} }
} }
Behavior on height { Behavior on height {
NumberAnimation { NumberAnimation {
duration: Theme.expressiveDurations.expressiveDefaultSpatial duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen)
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel easing.bezierCurve: Theme.variantModalEnterCurve
} }
} }
@@ -124,16 +124,16 @@ Item {
Behavior on width { Behavior on width {
NumberAnimation { NumberAnimation {
duration: Theme.expressiveDurations.expressiveDefaultSpatial duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen)
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel easing.bezierCurve: Theme.variantModalEnterCurve
} }
} }
Behavior on height { Behavior on height {
NumberAnimation { NumberAnimation {
duration: Theme.expressiveDurations.expressiveDefaultSpatial duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen)
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel easing.bezierCurve: Theme.variantModalEnterCurve
} }
} }
} }
+50 -1
View File
@@ -4,12 +4,61 @@ pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import Quickshell import Quickshell
import Quickshell.Services.Mpris import Quickshell.Services.Mpris
import qs.Common
Singleton { Singleton {
id: root id: root
readonly property list<MprisPlayer> availablePlayers: Mpris.players.values readonly property list<MprisPlayer> availablePlayers: Mpris.players.values
property MprisPlayer activePlayer: availablePlayers.find(p => p.isPlaying) ?? availablePlayers.find(p => p.canControl && p.canPlay) ?? null property MprisPlayer activePlayer: null
onAvailablePlayersChanged: _resolveActivePlayer()
Component.onCompleted: _resolveActivePlayer()
Instantiator {
model: root.availablePlayers
delegate: Connections {
required property MprisPlayer modelData
target: modelData
function onIsPlayingChanged() {
if (modelData.isPlaying)
root._resolveActivePlayer();
}
}
}
function _resolveActivePlayer(): void {
const playing = availablePlayers.find(p => p.isPlaying);
if (playing) {
activePlayer = playing;
_persistIdentity(playing.identity);
return;
}
if (activePlayer && availablePlayers.indexOf(activePlayer) >= 0)
return;
const savedId = SessionData.lastPlayerIdentity;
if (savedId) {
const match = availablePlayers.find(p => p.identity === savedId);
if (match) {
activePlayer = match;
return;
}
}
activePlayer = availablePlayers.find(p => p.canControl && p.canPlay) ?? null;
if (activePlayer)
_persistIdentity(activePlayer.identity);
}
function setActivePlayer(player: MprisPlayer): void {
activePlayer = player;
if (player)
_persistIdentity(player.identity);
}
function _persistIdentity(identity: string): void {
if (identity && SessionData.lastPlayerIdentity !== identity)
SessionData.set("lastPlayerIdentity", identity);
}
Timer { Timer {
interval: 1000 interval: 1000
+14 -2
View File
@@ -6,6 +6,7 @@ import QtQuick
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
import Quickshell.Services.Pipewire import Quickshell.Services.Pipewire
import qs.Services
Singleton { Singleton {
id: root id: root
@@ -58,6 +59,10 @@ Singleton {
} }
readonly property bool screensharingActive: { readonly property bool screensharingActive: {
if (CompositorService.isNiri && NiriService.hasActiveCast) {
return true
}
if (!Pipewire.ready || !Pipewire.nodes?.values) { if (!Pipewire.ready || !Pipewire.nodes?.values) {
return false return false
} }
@@ -74,6 +79,12 @@ Singleton {
} }
} }
if (node.properties && node.properties["media.class"] === "Stream/Output/Video") {
if (looksLikeScreencast(node)) {
return true
}
}
if (node.properties && node.properties["media.class"] === "Stream/Input/Audio") { if (node.properties && node.properties["media.class"] === "Stream/Input/Audio") {
const mediaName = (node.properties["media.name"] || "").toLowerCase() const mediaName = (node.properties["media.name"] || "").toLowerCase()
const appName = (node.properties["application.name"] || "").toLowerCase() const appName = (node.properties["application.name"] || "").toLowerCase()
@@ -110,8 +121,9 @@ Singleton {
} }
const appName = (node.properties && node.properties["application.name"] || "").toLowerCase() const appName = (node.properties && node.properties["application.name"] || "").toLowerCase()
const nodeName = (node.name || "").toLowerCase() const nodeName = (node.name || "").toLowerCase()
const combined = appName + " " + nodeName const mediaName = (node.properties && node.properties["media.name"] || "").toLowerCase()
return /xdg-desktop-portal|xdpw|screencast|screen|gnome shell|kwin|obs/.test(combined) const combined = appName + " " + nodeName + " " + mediaName
return /xdg-desktop-portal|xdpw|screencast|screen-cast|screen|gnome shell|kwin|obs|niri/.test(combined)
} }
function getMicrophoneStatus() { function getMicrophoneStatus() {
+4 -1
View File
@@ -231,7 +231,10 @@ Singleton {
return; return;
isChecking = true; isChecking = true;
hasError = false; hasError = false;
if (updChecker.length > 0) { if (pkgManager === "paru" || pkgManager === "yay") {
const repoCmd = updChecker.length > 0 ? updChecker : `${pkgManager} -Qu`;
updateChecker.command = ["sh", "-c", `(${repoCmd} 2>/dev/null; ${pkgManager} -Qua 2>/dev/null) || true`];
} else if (updChecker.length > 0) {
updateChecker.command = [updChecker].concat(updateCheckerParams[updChecker].listUpdatesSettings.params); updateChecker.command = [updChecker].concat(updateCheckerParams[updChecker].listUpdatesSettings.params);
} else { } else {
updateChecker.command = [pkgManager].concat(packageManagerParams[pkgManager].listUpdatesSettings.params); updateChecker.command = [pkgManager].concat(packageManagerParams[pkgManager].listUpdatesSettings.params);
+159
View File
@@ -0,0 +1,159 @@
import QtQuick
import QtQuick.Shapes
import qs.Common
// Concave arc connector filling the gap between a bar corner and an adjacent surface.
//
// NOTE: FrameWindow now uses ConnectedShape.qml for frame-owned connected chrome
// (unified single-path rendering). This component is still used by DankPopout's
// own shadow source for non-frame-owned chrome (popouts on non-frame screens).
Item {
id: root
property string barSide: "top"
property string placement: "left"
property real spacing: 4
property real connectorRadius: 12
property color color: "transparent"
property real edgeStrokeWidth: 0
property color edgeStrokeColor: color
property real dpr: 1
readonly property bool isHorizontalBar: barSide === "top" || barSide === "bottom"
readonly property bool isPlacementLeft: placement === "left"
readonly property real _edgeStrokeWidth: Math.max(0, edgeStrokeWidth)
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;
}
}
width: isHorizontalBar ? connectorRadius : (spacing + connectorRadius)
height: isHorizontalBar ? (spacing + connectorRadius) : connectorRadius
Shape {
x: -root._edgeStrokeWidth
y: -root._edgeStrokeWidth
width: root.width + root._edgeStrokeWidth * 2
height: root.height + root._edgeStrokeWidth * 2
asynchronous: false
antialiasing: true
preferredRendererType: Shape.CurveRenderer
layer.enabled: true
layer.smooth: true
layer.textureSize: root.dpr > 1 ? Qt.size(Math.ceil(width * root.dpr), Math.ceil(height * root.dpr)) : Qt.size(0, 0)
ShapePath {
fillColor: root.color
strokeColor: root._edgeStrokeWidth > 0 ? root.edgeStrokeColor : "transparent"
strokeWidth: root._edgeStrokeWidth * 2
joinStyle: ShapePath.RoundJoin
capStyle: ShapePath.RoundCap
fillRule: ShapePath.WindingFill
startX: root.pathStartX + root._edgeStrokeWidth
startY: root.pathStartY + root._edgeStrokeWidth
PathLine {
x: root.firstLineX + root._edgeStrokeWidth
y: root.firstLineY + root._edgeStrokeWidth
}
PathLine {
x: root.secondLineX + root._edgeStrokeWidth
y: root.secondLineY + root._edgeStrokeWidth
}
PathAngleArc {
centerX: root.arcCenterX + root._edgeStrokeWidth
centerY: root.arcCenterY + root._edgeStrokeWidth
radiusX: root.connectorRadius
radiusY: root.connectorRadius
startAngle: root.arcStartAngle
sweepAngle: root.arcSweepAngle
}
}
}
}
+286
View File
@@ -0,0 +1,286 @@
import QtQuick
import QtQuick.Shapes
import qs.Common
// Unified connected silhouette: body + concave arcs as one ShapePath.
// PathArc pattern 4 arcs + 4 lines, no sibling alignment.
Item {
id: root
property string barSide: "top"
property real bodyWidth: 0
property real bodyHeight: 0
property real connectorRadius: 12
property real surfaceRadius: 12
property color fillColor: "transparent"
// Derived layout
readonly property bool _horiz: barSide === "top" || barSide === "bottom"
readonly property real _cr: Math.max(0, connectorRadius)
readonly property real _sr: Math.max(0, Math.min(surfaceRadius, (_horiz ? bodyWidth : bodyHeight) / 2, (_horiz ? bodyHeight : bodyWidth) / 2))
// Root-level aliases PathArc/PathLine elements can't use `parent`.
readonly property real _bw: bodyWidth
readonly property real _bh: bodyHeight
readonly property real _totalW: _horiz ? _bw + _cr * 2 : _bw
readonly property real _totalH: _horiz ? _bh : _bh + _cr * 2
width: _totalW
height: _totalH
readonly property real bodyX: _horiz ? _cr : 0
readonly property real bodyY: _horiz ? 0 : _cr
Shape {
anchors.fill: parent
asynchronous: false
preferredRendererType: Shape.CurveRenderer
ShapePath {
fillColor: root.fillColor
strokeWidth: -1
fillRule: ShapePath.WindingFill
// CW path: bar edge concave arc body convex arc far edge convex arc body concave arc
startX: root.barSide === "right" ? root._totalW : 0
startY: {
switch (root.barSide) {
case "bottom":
return root._totalH;
case "left":
return root._totalH;
case "right":
return 0;
default:
return 0;
}
}
// Bar edge
PathLine {
x: {
switch (root.barSide) {
case "left":
return 0;
case "right":
return root._totalW;
default:
return root._totalW;
}
}
y: {
switch (root.barSide) {
case "bottom":
return root._totalH;
case "left":
return 0;
case "right":
return root._totalH;
default:
return 0;
}
}
}
// Concave arc 1
PathArc {
relativeX: {
switch (root.barSide) {
case "left":
return root._cr;
case "right":
return -root._cr;
default:
return -root._cr;
}
}
relativeY: {
switch (root.barSide) {
case "bottom":
return -root._cr;
case "left":
return root._cr;
case "right":
return -root._cr;
default:
return root._cr;
}
}
radiusX: root._cr
radiusY: root._cr
direction: root.barSide === "bottom" ? PathArc.Clockwise : PathArc.Counterclockwise
}
// Body edge to first convex corner
PathLine {
x: {
switch (root.barSide) {
case "left":
return root._bw - root._sr;
case "right":
return root._sr;
default:
return root._totalW - root._cr;
}
}
y: {
switch (root.barSide) {
case "bottom":
return root._sr;
case "left":
return root._cr;
case "right":
return root._cr + root._bh;
default:
return root._totalH - root._sr;
}
}
}
// Convex arc 1
PathArc {
relativeX: {
switch (root.barSide) {
case "left":
return root._sr;
case "right":
return -root._sr;
default:
return -root._sr;
}
}
relativeY: {
switch (root.barSide) {
case "bottom":
return -root._sr;
case "left":
return root._sr;
case "right":
return -root._sr;
default:
return root._sr;
}
}
radiusX: root._sr
radiusY: root._sr
direction: root.barSide === "bottom" ? PathArc.Counterclockwise : PathArc.Clockwise
}
// Far edge
PathLine {
x: {
switch (root.barSide) {
case "left":
return root._bw;
case "right":
return 0;
default:
return root._cr + root._sr;
}
}
y: {
switch (root.barSide) {
case "bottom":
return 0;
case "left":
return root._cr + root._bh - root._sr;
case "right":
return root._cr + root._sr;
default:
return root._totalH;
}
}
}
// Convex arc 2
PathArc {
relativeX: {
switch (root.barSide) {
case "left":
return -root._sr;
case "right":
return root._sr;
default:
return -root._sr;
}
}
relativeY: {
switch (root.barSide) {
case "bottom":
return root._sr;
case "left":
return root._sr;
case "right":
return -root._sr;
default:
return -root._sr;
}
}
radiusX: root._sr
radiusY: root._sr
direction: root.barSide === "bottom" ? PathArc.Counterclockwise : PathArc.Clockwise
}
// Body edge to second concave arc
PathLine {
x: {
switch (root.barSide) {
case "left":
return root._cr;
case "right":
return root._bw - root._cr;
default:
return root._cr;
}
}
y: {
switch (root.barSide) {
case "bottom":
return root._totalH - root._cr;
case "left":
return root._cr + root._bh;
case "right":
return root._cr;
default:
return root._cr;
}
}
}
// Concave arc 2
PathArc {
relativeX: {
switch (root.barSide) {
case "left":
return -root._cr;
case "right":
return root._cr;
default:
return -root._cr;
}
}
relativeY: {
switch (root.barSide) {
case "bottom":
return root._cr;
case "left":
return root._cr;
case "right":
return -root._cr;
default:
return -root._cr;
}
}
radiusX: root._cr
radiusY: root._cr
direction: root.barSide === "bottom" ? PathArc.Clockwise : PathArc.Counterclockwise
}
}
}
}
+584 -103
View File
@@ -20,10 +20,10 @@ Item {
property string triggerSection: "" property string triggerSection: ""
property string positioning: "center" property string positioning: "center"
property int animationDuration: Theme.popoutAnimationDuration property int animationDuration: Theme.popoutAnimationDuration
property real animationScaleCollapsed: 0.96 property real animationScaleCollapsed: Theme.effectScaleCollapsed
property real animationOffset: Theme.spacingL property real animationOffset: Theme.effectAnimOffset
property list<real> animationEnterCurve: Theme.expressiveCurves.expressiveDefaultSpatial property list<real> animationEnterCurve: Theme.variantPopoutEnterCurve
property list<real> animationExitCurve: Theme.expressiveCurves.emphasized property list<real> animationExitCurve: Theme.variantPopoutExitCurve
property bool suspendShadowWhileResizing: false property bool suspendShadowWhileResizing: false
property bool shouldBeVisible: false property bool shouldBeVisible: false
property var customKeyboardFocus: null property var customKeyboardFocus: null
@@ -34,6 +34,8 @@ Item {
property bool _resizeActive: false property bool _resizeActive: false
property real _surfaceMarginLeft: 0 property real _surfaceMarginLeft: 0
property real _surfaceW: 0 property real _surfaceW: 0
property string _chromeClaimId: ""
property int _connectedChromeSerial: 0
property real storedBarThickness: Theme.barHeight - 4 property real storedBarThickness: Theme.barHeight - 4
property real storedBarSpacing: 4 property real storedBarSpacing: 4
@@ -47,6 +49,8 @@ Item {
property var screen: null property var screen: null
readonly property real effectiveBarThickness: { readonly property real effectiveBarThickness: {
if (Theme.isConnectedEffect)
return Math.max(0, storedBarThickness);
const padding = storedBarConfig ? (storedBarConfig.innerPadding !== undefined ? storedBarConfig.innerPadding : 4) : 4; const padding = storedBarConfig ? (storedBarConfig.innerPadding !== undefined ? storedBarConfig.innerPadding : 4) : 4;
return Math.max(26 + padding * 0.6, Theme.barHeight - 4 - (8 - padding)) + storedBarSpacing; return Math.max(26 + padding * 0.6, Theme.barHeight - 4 - (8 - padding)) + storedBarSpacing;
} }
@@ -68,12 +72,14 @@ Item {
readonly property real barWidth: barBounds.width readonly property real barWidth: barBounds.width
readonly property real barHeight: barBounds.height readonly property real barHeight: barBounds.height
readonly property real barWingSize: barBounds.wingSize readonly property real barWingSize: barBounds.wingSize
readonly property bool effectiveSurfaceBlurEnabled: Theme.connectedSurfaceBlurEnabled
signal opened signal opened
signal popoutClosed signal popoutClosed
signal backgroundClicked signal backgroundClicked
property var _lastOpenedScreen: null property var _lastOpenedScreen: null
property bool isClosing: false
property int effectiveBarPosition: 0 property int effectiveBarPosition: 0
property real effectiveBarBottomGap: 0 property real effectiveBarBottomGap: 0
@@ -147,7 +153,102 @@ Item {
setBarContext(pos, bottomGap); setBarContext(pos, bottomGap);
} }
function _nextChromeClaimId() {
_connectedChromeSerial += 1;
return layerNamespace + ":" + _connectedChromeSerial + ":" + (new Date()).getTime();
}
function _connectedChromeState(visibleOverride) {
const visible = visibleOverride !== undefined ? !!visibleOverride : contentWindow.visible;
return {
"visible": visible,
"barSide": contentContainer.connectedBarSide,
"bodyX": root.alignedX,
"bodyY": root.alignedY,
"bodyW": root.alignedWidth,
"bodyH": root.alignedHeight,
"animX": contentContainer.animX,
"animY": contentContainer.animY,
"screen": root.screen ? root.screen.name : ""
};
}
function _publishConnectedChromeState(forceClaim, visibleOverride) {
if (!root.frameOwnsConnectedChrome || !root.screen || !_chromeClaimId)
return;
const state = _connectedChromeState(visibleOverride);
if (forceClaim || !ConnectedModeState.hasPopoutOwner(_chromeClaimId)) {
ConnectedModeState.claimPopout(_chromeClaimId, state);
} else {
ConnectedModeState.updatePopout(_chromeClaimId, state);
}
}
function _releaseConnectedChromeState() {
if (_chromeClaimId)
ConnectedModeState.releasePopout(_chromeClaimId);
_chromeClaimId = "";
}
// Exposed animation state for ConnectedModeState
readonly property real contentAnimX: contentContainer.animX
readonly property real contentAnimY: contentContainer.animY
// ConnectedModeState sync
function _syncPopoutChromeState() {
if (!root.frameOwnsConnectedChrome) {
_releaseConnectedChromeState();
return;
}
if (!root.screen) {
_releaseConnectedChromeState();
return;
}
if (!contentWindow.visible && !shouldBeVisible)
return;
if (!_chromeClaimId)
_chromeClaimId = _nextChromeClaimId();
_publishConnectedChromeState(contentWindow.visible && !ConnectedModeState.hasPopoutOwner(_chromeClaimId));
}
onAlignedXChanged: _syncPopoutChromeState()
onAlignedYChanged: _syncPopoutChromeState()
onAlignedWidthChanged: _syncPopoutChromeState()
onContentAnimXChanged: _syncPopoutChromeState()
onContentAnimYChanged: _syncPopoutChromeState()
onScreenChanged: _syncPopoutChromeState()
onEffectiveBarPositionChanged: _syncPopoutChromeState()
Connections {
target: contentWindow
function onVisibleChanged() {
if (contentWindow.visible)
root._publishConnectedChromeState(true);
else
root._releaseConnectedChromeState();
}
}
Connections {
target: SettingsData
function onConnectedFrameModeActiveChanged() {
if (root.frameOwnsConnectedChrome) {
if (contentWindow.visible || root.shouldBeVisible) {
if (!root._chromeClaimId)
root._chromeClaimId = root._nextChromeClaimId();
root._publishConnectedChromeState(true);
}
} else {
root._releaseConnectedChromeState();
}
}
}
readonly property bool useBackgroundWindow: !CompositorService.isHyprland || CompositorService.useHyprlandFocusGrab readonly property bool useBackgroundWindow: !CompositorService.isHyprland || CompositorService.useHyprlandFocusGrab
readonly property bool frameOwnsConnectedChrome: SettingsData.connectedFrameModeActive
&& !!root.screen
&& SettingsData.isScreenInPreferences(root.screen, SettingsData.frameScreenPreferences)
function updateSurfacePosition() { function updateSurfacePosition() {
if (useBackgroundWindow && shouldBeVisible) { if (useBackgroundWindow && shouldBeVisible) {
@@ -156,10 +257,14 @@ Item {
} }
} }
property bool animationsEnabled: true
function open() { function open() {
if (!screen) if (!screen)
return; return;
closeTimer.stop(); closeTimer.stop();
isClosing = false;
animationsEnabled = false;
// Snapshot mask geometry // Snapshot mask geometry
_frozenMaskX = maskX; _frozenMaskX = maskX;
@@ -174,12 +279,29 @@ Item {
} }
_lastOpenedScreen = screen; _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 (root.frameOwnsConnectedChrome) {
_chromeClaimId = _nextChromeClaimId();
_publishConnectedChromeState(true, true);
} else {
_chromeClaimId = "";
}
if (useBackgroundWindow) { if (useBackgroundWindow) {
_surfaceMarginLeft = alignedX - shadowBuffer; _surfaceMarginLeft = alignedX - shadowBuffer;
_surfaceW = alignedWidth + shadowBuffer * 2; _surfaceW = alignedWidth + shadowBuffer * 2;
backgroundWindow.visible = true;
} }
contentWindow.visible = true;
Qt.callLater(() => { Qt.callLater(() => {
animationsEnabled = true;
shouldBeVisible = true;
if (shouldBeVisible && screen) { if (shouldBeVisible && screen) {
if (useBackgroundWindow) if (useBackgroundWindow)
backgroundWindow.visible = true; backgroundWindow.visible = true;
@@ -191,6 +313,7 @@ Item {
} }
function close() { function close() {
isClosing = true;
shouldBeVisible = false; shouldBeVisible = false;
_primeContent = false; _primeContent = false;
PopoutManager.popoutChanged(); PopoutManager.popoutChanged();
@@ -222,9 +345,10 @@ Item {
Timer { Timer {
id: closeTimer id: closeTimer
interval: animationDuration interval: Theme.variantCloseInterval(animationDuration)
onTriggered: { onTriggered: {
if (!shouldBeVisible) { if (!shouldBeVisible) {
isClosing = false;
contentWindow.visible = false; contentWindow.visible = false;
if (useBackgroundWindow) if (useBackgroundWindow)
backgroundWindow.visible = false; backgroundWindow.visible = false;
@@ -234,19 +358,80 @@ Item {
} }
} }
Component.onDestruction: _releaseConnectedChromeState()
readonly property real screenWidth: screen ? screen.width : 0 readonly property real screenWidth: screen ? screen.width : 0
readonly property real screenHeight: screen ? screen.height : 0 readonly property real screenHeight: screen ? screen.height : 0
readonly property real dpr: screen ? screen.devicePixelRatio : 1 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 var shadowLevel: Theme.elevationLevel3
readonly property real shadowFallbackOffset: 6 readonly property real shadowFallbackOffset: 6
readonly property real shadowRenderPadding: (Theme.elevationEnabled && SettingsData.popoutElevationEnabled) ? Theme.elevationRenderPadding(shadowLevel, effectiveShadowDirection, shadowFallbackOffset, 8, 16) : 0 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 shadowBuffer: Theme.snap(shadowRenderPadding + shadowMotionPadding, dpr)
readonly property real alignedWidth: Theme.px(popupWidth, dpr) readonly property real alignedWidth: Theme.px(popupWidth, dpr)
readonly property real alignedHeight: Theme.px(popupHeight, 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: { onAlignedHeightChanged: {
_syncPopoutChromeState();
if (!suspendShadowWhileResizing || !shouldBeVisible) if (!suspendShadowWhileResizing || !shouldBeVisible)
return; return;
_resizeActive = true; _resizeActive = true;
@@ -269,17 +454,22 @@ Item {
readonly property real alignedX: Theme.snap((() => { readonly property real alignedX: Theme.snap((() => {
const useAutoGaps = storedBarConfig?.popupGapsAuto !== undefined ? storedBarConfig.popupGapsAuto : true; const useAutoGaps = storedBarConfig?.popupGapsAuto !== undefined ? storedBarConfig.popupGapsAuto : true;
const manualGapValue = storedBarConfig?.popupGapsManual !== undefined ? storedBarConfig.popupGapsManual : 4; 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) { switch (effectiveBarPosition) {
case SettingsData.Position.Left: 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: 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: default:
const rawX = triggerX + (triggerWidth / 2) - (popupWidth / 2); const rawX = triggerX + (triggerWidth / 2) - (popupWidth / 2);
const minX = adjacentBarInfo.leftBar > 0 ? adjacentBarInfo.leftBar : popupGap; const minX = Math.max(edgeGap, adjacentBarClearance(adjacentBarInfo.leftBar));
const maxX = screenWidth - popupWidth - (adjacentBarInfo.rightBar > 0 ? adjacentBarInfo.rightBar : popupGap); const maxX = screenWidth - popupWidth - Math.max(edgeGap, adjacentBarClearance(adjacentBarInfo.rightBar));
return Math.max(minX, Math.min(maxX, rawX)); return Math.max(minX, Math.min(maxX, rawX));
} }
})(), dpr) })(), dpr)
@@ -287,17 +477,22 @@ Item {
readonly property real alignedY: Theme.snap((() => { readonly property real alignedY: Theme.snap((() => {
const useAutoGaps = storedBarConfig?.popupGapsAuto !== undefined ? storedBarConfig.popupGapsAuto : true; const useAutoGaps = storedBarConfig?.popupGapsAuto !== undefined ? storedBarConfig.popupGapsAuto : true;
const manualGapValue = storedBarConfig?.popupGapsManual !== undefined ? storedBarConfig.popupGapsManual : 4; 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) { switch (effectiveBarPosition) {
case SettingsData.Position.Bottom: 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: 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: default:
const rawY = triggerY - (popupHeight / 2); const rawY = triggerY - (popupHeight / 2);
const minY = adjacentBarInfo.topBar > 0 ? adjacentBarInfo.topBar : popupGap; const minY = Math.max(edgeGap, adjacentBarClearance(adjacentBarInfo.topBar));
const maxY = screenHeight - popupHeight - (adjacentBarInfo.bottomBar > 0 ? adjacentBarInfo.bottomBar : popupGap); const maxY = screenHeight - popupHeight - Math.max(edgeGap, adjacentBarClearance(adjacentBarInfo.bottomBar));
return Math.max(minY, Math.min(maxY, rawY)); return Math.max(minY, Math.min(maxY, rawY));
} }
})(), dpr) })(), dpr)
@@ -353,6 +548,10 @@ Item {
mask: Region { mask: Region {
item: maskRect item: maskRect
Region {
item: contentExclusionRect
intersection: Intersection.Subtract
}
} }
Rectangle { Rectangle {
@@ -361,26 +560,70 @@ Item {
color: "transparent" color: "transparent"
x: root._frozenMaskX x: root._frozenMaskX
y: root._frozenMaskY y: root._frozenMaskY
width: (shouldBeVisible && backgroundInteractive) ? root._frozenMaskWidth : 0 width: (backgroundWindow.visible && backgroundInteractive) ? root._frozenMaskWidth : 0
height: (shouldBeVisible && backgroundInteractive) ? root._frozenMaskHeight : 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 x: root._frozenMaskX
y: root._frozenMaskY y: root._frozenMaskY
width: root._frozenMaskWidth width: root._frozenMaskWidth
height: root._frozenMaskHeight height: root._frozenMaskHeight
hoverEnabled: false enabled: root.shouldBeVisible && root.backgroundInteractive
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;
if (!outsideContent) readonly property real contentLeft: Math.max(0, root.alignedX - x)
return; readonly property real contentTop: Math.max(0, root.alignedY - y)
backgroundClicked(); 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()
} }
} }
@@ -401,12 +644,21 @@ Item {
WindowBlur { WindowBlur {
id: popoutBlur id: popoutBlur
targetWindow: contentWindow targetWindow: contentWindow
blurEnabled: root.effectiveSurfaceBlurEnabled && !root.frameOwnsConnectedChrome
readonly property real s: Math.min(1, contentContainer.scaleValue) readonly property real s: Math.min(1, contentContainer.scaleValue)
blurX: contentContainer.x + contentContainer.width * (1 - s) * 0.5 + Theme.snap(contentContainer.animX, root.dpr) readonly property bool trackBlurFromBarEdge: Theme.isConnectedEffect || (typeof SettingsData !== "undefined" && Theme.isDirectionalEffect && SettingsData.directionalAnimationMode !== 2)
blurY: contentContainer.y + contentContainer.height * (1 - s) * 0.5 + Theme.snap(contentContainer.animY, root.dpr)
blurWidth: (shouldBeVisible && contentWrapper.opacity > 0) ? contentContainer.width * s : 0 // Directional popouts clip to the bar edge, so the blur needs to grow from
blurHeight: (shouldBeVisible && contentWrapper.opacity > 0) ? contentContainer.height * s : 0 // that same edge instead of translating through the bar before settling.
blurRadius: Theme.cornerRadius 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.namespace: root.layerNamespace
@@ -436,7 +688,6 @@ Item {
} }
readonly property bool _fullHeight: useBackgroundWindow && root.fullHeightSurface readonly property bool _fullHeight: useBackgroundWindow && root.fullHeightSurface
anchors { anchors {
left: true left: true
top: true top: true
@@ -462,10 +713,10 @@ Item {
Item { Item {
id: contentMaskRect id: contentMaskRect
visible: false visible: false
x: contentContainer.x x: contentContainer.x - contentContainer.horizontalConnectorExtent
y: contentContainer.y y: contentContainer.y - contentContainer.verticalConnectorExtent
width: shouldBeVisible ? root.alignedWidth : 0 width: root.alignedWidth + contentContainer.horizontalConnectorExtent * 2
height: shouldBeVisible ? root.alignedHeight : 0 height: root.alignedHeight + contentContainer.verticalConnectorExtent * 2
} }
MouseArea { MouseArea {
@@ -494,12 +745,122 @@ Item {
readonly property bool barBottom: effectiveBarPosition === SettingsData.Position.Bottom readonly property bool barBottom: effectiveBarPosition === SettingsData.Position.Bottom
readonly property bool barLeft: effectiveBarPosition === SettingsData.Position.Left readonly property bool barLeft: effectiveBarPosition === SettingsData.Position.Left
readonly property bool barRight: effectiveBarPosition === SettingsData.Position.Right readonly property bool barRight: effectiveBarPosition === SettingsData.Position.Right
readonly property real offsetX: barLeft ? root.animationOffset : (barRight ? -root.animationOffset : 0) readonly property string connectedBarSide: barTop ? "top" : (barBottom ? "bottom" : (barLeft ? "left" : "right"))
readonly property real offsetY: barBottom ? -root.animationOffset : (barTop ? root.animationOffset : 0) 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 animX: 0
property real animY: 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) onOffsetXChanged: animX = Theme.snap(root.shouldBeVisible ? 0 : offsetX, root.dpr)
onOffsetYChanged: animY = Theme.snap(root.shouldBeVisible ? 0 : offsetY, root.dpr) onOffsetYChanged: animY = Theme.snap(root.shouldBeVisible ? 0 : offsetY, root.dpr)
@@ -509,96 +870,216 @@ Item {
function onShouldBeVisibleChanged() { function onShouldBeVisibleChanged() {
contentContainer.animX = Theme.snap(root.shouldBeVisible ? 0 : contentContainer.offsetX, root.dpr); contentContainer.animX = Theme.snap(root.shouldBeVisible ? 0 : contentContainer.offsetX, root.dpr);
contentContainer.animY = Theme.snap(root.shouldBeVisible ? 0 : contentContainer.offsetY, 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 { Behavior on animX {
enabled: root.animationsEnabled
NumberAnimation { NumberAnimation {
duration: root.animationDuration duration: Theme.variantDuration(root.animationDuration, root.shouldBeVisible)
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
} }
} }
Behavior on animY { Behavior on animY {
enabled: root.animationsEnabled
NumberAnimation { NumberAnimation {
duration: root.animationDuration duration: Theme.variantDuration(root.animationDuration, root.shouldBeVisible)
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
} }
} }
Behavior on scaleValue { Behavior on scaleValue {
enabled: root.animationsEnabled
NumberAnimation { NumberAnimation {
duration: root.animationDuration duration: Theme.variantDuration(root.animationDuration, root.shouldBeVisible)
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve 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 { Item {
id: contentWrapper id: directionalClipMask
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)
layer.enabled: contentWrapper.opacity < 1 readonly property bool shouldClip: Theme.isDirectionalEffect && typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode > 0
layer.smooth: false readonly property real clipOversize: 1000
layer.textureSize: root.dpr > 1 ? Qt.size(Math.ceil(width * root.dpr), Math.ceil(height * root.dpr)) : Qt.size(0, 0) readonly property real connectedClipAllowance: Theme.isConnectedEffect ? Math.ceil(root.shadowRenderPadding + BlurService.borderWidth + 2) : 0
Behavior on opacity { clip: shouldClip
NumberAnimation {
duration: animationDuration // Bound the clipping strictly to the bar side, allowing massive overflow on the other 3 sides for shadows
easing.type: Easing.BezierSpline x: shouldClip ? (contentContainer.barLeft ? -connectedClipAllowance : -clipOversize) : 0
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve 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;
} }
Loader { Item {
id: contentLoader id: aligner
anchors.fill: parent readonly property real baseWidth: contentContainer.width
active: root._primeContent || shouldBeVisible || contentWindow.visible readonly property real baseHeight: contentContainer.height
asynchronous: false readonly property bool isRollOut: typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 && Theme.isDirectionalEffect
}
}
Rectangle { x: (directionalClipMask.x !== 0 ? -directionalClipMask.x : 0) + (isRollOut && contentContainer.barRight ? baseWidth * (1 - contentContainer.scaleValue) : 0)
width: parent.width y: (directionalClipMask.y !== 0 ? -directionalClipMask.y : 0) + (isRollOut && contentContainer.barBottom ? baseHeight * (1 - contentContainer.scaleValue) : 0)
height: parent.height width: isRollOut && (contentContainer.barLeft || contentContainer.barRight) ? Math.max(0, baseWidth * contentContainer.scaleValue) : baseWidth
x: contentWrapper.x height: isRollOut && (contentContainer.barTop || contentContainer.barBottom) ? Math.max(0, baseHeight * contentContainer.scaleValue) : baseHeight
y: contentWrapper.y
opacity: contentWrapper.opacity clip: isRollOut
scale: contentWrapper.scale
visible: contentWrapper.visible Item {
radius: Theme.cornerRadius id: unrollCounteract
color: "transparent" x: aligner.isRollOut && contentContainer.barRight ? -(aligner.baseWidth * (1 - contentContainer.scaleValue)) : 0
border.color: BlurService.enabled ? BlurService.borderColor : Theme.outlineMedium y: aligner.isRollOut && contentContainer.barBottom ? -(aligner.baseHeight * (1 - contentContainer.scaleValue)) : 0
border.width: BlurService.borderWidth width: aligner.baseWidth
z: 100 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 && !root.frameOwnsConnectedChrome
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
dpr: root.dpr
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
dpr: root.dpr
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
}
}
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 { Item {
id: focusHelper id: focusHelper
+161 -81
View File
@@ -8,13 +8,122 @@ Item {
id: root id: root
property MprisPlayer activePlayer property MprisPlayer activePlayer
property real value: { property real seekPreviewRatio: -1
if (!activePlayer || activePlayer.length <= 0) return 0 readonly property real playerValue: {
const pos = (activePlayer.position || 0) % Math.max(1, activePlayer.length) if (!activePlayer || activePlayer.length <= 0)
const calculatedRatio = pos / activePlayer.length return 0;
return Math.max(0, Math.min(1, calculatedRatio)) const pos = (activePlayer.position || 0) % Math.max(1, activePlayer.length);
const calculatedRatio = pos / activePlayer.length;
return Math.max(0, Math.min(1, calculatedRatio));
} }
property real value: seekPreviewRatio >= 0 ? seekPreviewRatio : playerValue
property bool isSeeking: false property bool isSeeking: false
property bool isDraggingSeek: false
property real committedSeekRatio: -1
property int previewSettleChecksRemaining: 0
property real dragThreshold: 4
property int holdIndicatorDelay: 180
function clampRatio(ratio) {
return Math.max(0, Math.min(1, ratio));
}
function ratioForPosition(position) {
if (!activePlayer || activePlayer.length <= 0)
return 0;
return clampRatio(position / activePlayer.length);
}
function positionForRatio(ratio) {
if (!activePlayer || activePlayer.length <= 0)
return 0;
const rawPosition = clampRatio(ratio) * activePlayer.length;
return Math.min(rawPosition, activePlayer.length * 0.99);
}
function updatePreviewFromMouse(mouseX, width) {
if (!activePlayer || activePlayer.length <= 0 || width <= 0)
return;
seekPreviewRatio = clampRatio(mouseX / width);
}
function clearCommittedSeekPreview() {
previewSettleTimer.stop();
committedSeekRatio = -1;
previewSettleChecksRemaining = 0;
if (!isSeeking)
seekPreviewRatio = -1;
}
function beginCommittedSeekPreview(position) {
seekPreviewRatio = ratioForPosition(position);
committedSeekRatio = seekPreviewRatio;
previewSettleChecksRemaining = 15;
previewSettleTimer.restart();
}
function handleSeekPressed(mouse, width, mouseArea, holdTimer) {
isSeeking = true;
isDraggingSeek = false;
mouseArea.pressX = mouse.x;
clearCommittedSeekPreview();
holdTimer.restart();
if (activePlayer && activePlayer.length > 0 && activePlayer.canSeek) {
updatePreviewFromMouse(mouse.x, width);
mouseArea.pendingSeekPosition = positionForRatio(seekPreviewRatio);
}
}
function handleSeekReleased(mouseArea, holdTimer) {
holdTimer.stop();
isSeeking = false;
isDraggingSeek = false;
if (mouseArea.pendingSeekPosition >= 0 && activePlayer && activePlayer.canSeek && activePlayer.length > 0) {
const clamped = Math.min(mouseArea.pendingSeekPosition, activePlayer.length * 0.99);
activePlayer.position = clamped;
mouseArea.pendingSeekPosition = -1;
beginCommittedSeekPreview(clamped);
} else {
seekPreviewRatio = -1;
}
}
function handleSeekPositionChanged(mouse, width, mouseArea) {
if (mouseArea.pressed && isSeeking && activePlayer && activePlayer.length > 0 && activePlayer.canSeek) {
if (!isDraggingSeek && Math.abs(mouse.x - mouseArea.pressX) >= dragThreshold)
isDraggingSeek = true;
updatePreviewFromMouse(mouse.x, width);
mouseArea.pendingSeekPosition = positionForRatio(seekPreviewRatio);
}
}
function handleSeekCanceled(mouseArea, holdTimer) {
holdTimer.stop();
isSeeking = false;
isDraggingSeek = false;
mouseArea.pendingSeekPosition = -1;
clearCommittedSeekPreview();
}
Timer {
id: previewSettleTimer
interval: 80
repeat: true
onTriggered: {
if (root.isSeeking || root.committedSeekRatio < 0) {
stop();
return;
}
const previewSettled = Math.abs(root.playerValue - root.committedSeekRatio) <= 0.0015;
if (previewSettled || root.previewSettleChecksRemaining <= 0) {
root.clearCommittedSeekPreview();
return;
}
root.previewSettleChecksRemaining -= 1;
}
}
implicitHeight: 20 implicitHeight: 20
@@ -29,58 +138,35 @@ Item {
M3WaveProgress { M3WaveProgress {
value: root.value value: root.value
actualValue: root.playerValue
showActualPlaybackState: root.isSeeking
actualProgressColor: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.45)
isPlaying: activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing isPlaying: activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing
MouseArea { MouseArea {
id: waveMouseArea
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
enabled: activePlayer && activePlayer.canSeek && activePlayer.length > 0 enabled: activePlayer && activePlayer.canSeek && activePlayer.length > 0
property real pendingSeekPosition: -1 property real pendingSeekPosition: -1
property real pressX: 0
Timer { Timer {
id: waveSeekDebounceTimer id: waveHoldIndicatorTimer
interval: 150 interval: root.holdIndicatorDelay
repeat: false
onTriggered: { onTriggered: {
if (parent.pendingSeekPosition >= 0 && activePlayer && activePlayer.canSeek && activePlayer.length > 0) { if (parent.pressed && root.isSeeking)
const clamped = Math.min(parent.pendingSeekPosition, activePlayer.length * 0.99) root.isDraggingSeek = true;
activePlayer.position = clamped
parent.pendingSeekPosition = -1
}
} }
} }
onPressed: (mouse) => { onPressed: mouse => root.handleSeekPressed(mouse, parent.width, waveMouseArea, waveHoldIndicatorTimer)
root.isSeeking = true onReleased: root.handleSeekReleased(waveMouseArea, waveHoldIndicatorTimer)
if (activePlayer && activePlayer.length > 0 && activePlayer.canSeek) { onPositionChanged: mouse => root.handleSeekPositionChanged(mouse, parent.width, waveMouseArea)
const r = Math.max(0, Math.min(1, mouse.x / parent.width)) onCanceled: root.handleSeekCanceled(waveMouseArea, waveHoldIndicatorTimer)
pendingSeekPosition = r * activePlayer.length
waveSeekDebounceTimer.restart()
}
}
onReleased: {
root.isSeeking = false
waveSeekDebounceTimer.stop()
if (pendingSeekPosition >= 0 && activePlayer && activePlayer.canSeek && activePlayer.length > 0) {
const clamped = Math.min(pendingSeekPosition, activePlayer.length * 0.99)
activePlayer.position = clamped
pendingSeekPosition = -1
}
}
onPositionChanged: (mouse) => {
if (pressed && root.isSeeking && activePlayer && activePlayer.length > 0 && activePlayer.canSeek) {
const r = Math.max(0, Math.min(1, mouse.x / parent.width))
pendingSeekPosition = r * activePlayer.length
waveSeekDebounceTimer.restart()
}
}
onClicked: (mouse) => {
if (activePlayer && activePlayer.length > 0 && activePlayer.canSeek) {
const r = Math.max(0, Math.min(1, mouse.x / parent.width))
activePlayer.position = r * activePlayer.length
}
}
} }
} }
} }
@@ -93,6 +179,7 @@ Item {
property color trackColor: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.40) property color trackColor: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.40)
property color fillColor: Theme.primary property color fillColor: Theme.primary
property color playheadColor: Theme.primary property color playheadColor: Theme.primary
property color actualProgressColor: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.45)
readonly property real midY: height / 2 readonly property real midY: height / 2
Rectangle { Rectangle {
@@ -110,7 +197,22 @@ Item {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
color: parent.fillColor color: parent.fillColor
radius: height / 2 radius: height / 2
Behavior on width { NumberAnimation { duration: 80 } } Behavior on width {
NumberAnimation {
duration: 80
}
}
}
Rectangle {
visible: root.isDraggingSeek
width: 2
height: Math.max(parent.lineWidth + 4, 10)
radius: width / 2
color: parent.actualProgressColor
x: Math.max(0, Math.min(parent.width, parent.width * root.playerValue)) - width / 2
y: parent.midY - height / 2
z: 2
} }
Rectangle { Rectangle {
@@ -122,59 +224,37 @@ Item {
x: Math.max(0, Math.min(parent.width, parent.width * root.value)) - width / 2 x: Math.max(0, Math.min(parent.width, parent.width * root.value)) - width / 2
y: parent.midY - height / 2 y: parent.midY - height / 2
z: 3 z: 3
Behavior on x { NumberAnimation { duration: 80 } } Behavior on x {
NumberAnimation {
duration: 80
}
}
} }
MouseArea { MouseArea {
id: flatMouseArea
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
enabled: activePlayer && activePlayer.canSeek && activePlayer.length > 0 enabled: activePlayer && activePlayer.canSeek && activePlayer.length > 0
property real pendingSeekPosition: -1 property real pendingSeekPosition: -1
property real pressX: 0
Timer { Timer {
id: flatSeekDebounceTimer id: flatHoldIndicatorTimer
interval: 150 interval: root.holdIndicatorDelay
repeat: false
onTriggered: { onTriggered: {
if (parent.pendingSeekPosition >= 0 && activePlayer && activePlayer.canSeek && activePlayer.length > 0) { if (parent.pressed && root.isSeeking)
const clamped = Math.min(parent.pendingSeekPosition, activePlayer.length * 0.99) root.isDraggingSeek = true;
activePlayer.position = clamped
parent.pendingSeekPosition = -1
}
} }
} }
onPressed: (mouse) => { onPressed: mouse => root.handleSeekPressed(mouse, parent.width, flatMouseArea, flatHoldIndicatorTimer)
root.isSeeking = true onReleased: root.handleSeekReleased(flatMouseArea, flatHoldIndicatorTimer)
if (activePlayer && activePlayer.length > 0 && activePlayer.canSeek) { onPositionChanged: mouse => root.handleSeekPositionChanged(mouse, parent.width, flatMouseArea)
const r = Math.max(0, Math.min(1, mouse.x / parent.width)) onCanceled: root.handleSeekCanceled(flatMouseArea, flatHoldIndicatorTimer)
pendingSeekPosition = r * activePlayer.length
flatSeekDebounceTimer.restart()
}
}
onReleased: {
root.isSeeking = false
flatSeekDebounceTimer.stop()
if (pendingSeekPosition >= 0 && activePlayer && activePlayer.canSeek && activePlayer.length > 0) {
const clamped = Math.min(pendingSeekPosition, activePlayer.length * 0.99)
activePlayer.position = clamped
pendingSeekPosition = -1
}
}
onPositionChanged: (mouse) => {
if (pressed && root.isSeeking && activePlayer && activePlayer.length > 0 && activePlayer.canSeek) {
const r = Math.max(0, Math.min(1, mouse.x / parent.width))
pendingSeekPosition = r * activePlayer.length
flatSeekDebounceTimer.restart()
}
}
onClicked: (mouse) => {
if (activePlayer && activePlayer.length > 0 && activePlayer.canSeek) {
const r = Math.max(0, Math.min(1, mouse.x / parent.width))
activePlayer.position = r * activePlayer.length
}
}
} }
} }
} }
+75 -5
View File
@@ -6,6 +6,8 @@ Item {
id: root id: root
property real value: 0 property real value: 0
property real actualValue: value
property bool showActualPlaybackState: false
property real lineWidth: 2 property real lineWidth: 2
property real wavelength: 20 property real wavelength: 20
property real amp: 1.6 property real amp: 1.6
@@ -15,6 +17,7 @@ Item {
property color trackColor: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.40) property color trackColor: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.40)
property color fillColor: Theme.primary property color fillColor: Theme.primary
property color playheadColor: Theme.primary property color playheadColor: Theme.primary
property color actualProgressColor: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.45)
property real dpr: (root.window ? root.window.devicePixelRatio : 1) property real dpr: (root.window ? root.window.devicePixelRatio : 1)
function snap(v) { function snap(v) {
@@ -22,7 +25,12 @@ Item {
} }
readonly property real playX: snap(root.width * root.value) readonly property real playX: snap(root.width * root.value)
readonly property real actualX: snap(root.width * root.actualValue)
readonly property real midY: snap(height / 2) readonly property real midY: snap(height / 2)
readonly property bool previewAhead: root.showActualPlaybackState && root.value > root.actualValue
readonly property bool previewBehind: root.showActualPlaybackState && root.value < root.actualValue
readonly property real previewGapStartX: Math.min(root.playX, root.actualX)
readonly property real previewGapEndX: Math.max(root.playX, root.actualX)
Behavior on currentAmp { Behavior on currentAmp {
NumberAnimation { NumberAnimation {
@@ -65,7 +73,9 @@ Item {
readonly property real startX: snap(root.lineWidth / 2) readonly property real startX: snap(root.lineWidth / 2)
readonly property real aaBias: (0.25 / root.dpr) readonly property real aaBias: (0.25 / root.dpr)
readonly property real endX: Math.max(startX, Math.min(root.playX - startX - aaBias, width)) readonly property real endX: root.previewAhead ? Math.max(startX, Math.min(root.actualX - aaBias, width)) : Math.max(startX, Math.min(root.playX - startX - aaBias, width))
readonly property real gapStartX: root.previewAhead ? Math.max(startX, Math.min(root.actualX + aaBias, width)) : Math.max(startX, Math.min(root.playX + playhead.width / 2, width))
readonly property real gapEndX: root.previewAhead ? Math.max(gapStartX, Math.min(root.playX - playhead.width / 2 - aaBias, width)) : Math.max(gapStartX, Math.min(root.actualX - aaBias, width))
Rectangle { Rectangle {
id: mask id: mask
@@ -100,6 +110,37 @@ Item {
} }
} }
Rectangle {
id: actualMask
anchors.top: parent.top
anchors.bottom: parent.bottom
x: waveClip.gapStartX
width: Math.max(0, waveClip.gapEndX - waveClip.gapStartX)
color: "transparent"
clip: true
visible: (root.previewBehind || root.previewAhead) && width > 0
Shape {
anchors.top: parent.top
anchors.bottom: parent.bottom
width: root.width + 4 * root.wavelength
antialiasing: true
preferredRendererType: Shape.CurveRenderer
x: waveOffsetX
ShapePath {
strokeColor: root.actualProgressColor
strokeWidth: snap(root.lineWidth)
capStyle: ShapePath.RoundCap
joinStyle: ShapePath.RoundJoin
fillColor: "transparent"
PathSvg {
path: waveSvg.path
}
}
}
}
Rectangle { Rectangle {
id: startCap id: startCap
width: snap(root.lineWidth) width: snap(root.lineWidth)
@@ -107,7 +148,7 @@ Item {
radius: width / 2 radius: width / 2
color: root.fillColor color: root.fillColor
x: waveClip.startX - width / 2 x: waveClip.startX - width / 2
y: root.midY - height / 2 + root.currentAmp * Math.sin((waveClip.startX / root.wavelength) * 2 * Math.PI + root.phase) y: waveY(waveClip.startX) - height / 2
visible: waveClip.endX > waveClip.startX visible: waveClip.endX > waveClip.startX
z: 2 z: 2
} }
@@ -119,10 +160,34 @@ Item {
radius: width / 2 radius: width / 2
color: root.fillColor color: root.fillColor
x: waveClip.endX - width / 2 x: waveClip.endX - width / 2
y: root.midY - height / 2 + root.currentAmp * Math.sin((waveClip.endX / root.wavelength) * 2 * Math.PI + root.phase) y: waveY(waveClip.endX) - height / 2
visible: waveClip.endX > waveClip.startX visible: waveClip.endX > waveClip.startX
z: 2 z: 2
} }
Rectangle {
id: actualEndCap
width: snap(root.lineWidth)
height: snap(root.lineWidth)
radius: width / 2
color: root.actualProgressColor
x: waveClip.gapEndX - width / 2
y: waveY(waveClip.gapEndX) - height / 2
visible: (root.previewBehind || root.previewAhead) && actualMask.width > 0
z: 2
}
Rectangle {
id: actualMarker
width: 2
height: Math.max(root.lineWidth + 4, 10)
radius: width / 2
color: root.actualProgressColor
x: root.actualX - width / 2
y: root.midY - height / 2
visible: root.showActualPlaybackState
z: 2
}
} }
Rectangle { Rectangle {
@@ -141,6 +206,10 @@ Item {
let r = a % m; let r = a % m;
return r < 0 ? r + m : r; return r < 0 ? r + m : r;
} }
function waveY(x, amplitude = root.currentAmp, phaseOffset = root.phase) {
return root.midY + amplitude * Math.sin((x / root.wavelength) * 2 * Math.PI + phaseOffset);
}
readonly property real waveOffsetX: -wrapMod(phase / k, wavelength) readonly property real waveOffsetX: -wrapMod(phase / k, wavelength)
FrameAnimation { FrameAnimation {
@@ -148,8 +217,9 @@ Item {
onTriggered: { onTriggered: {
if (root.isPlaying) if (root.isPlaying)
root.phase += 0.03 * frameTime * 60; root.phase += 0.03 * frameTime * 60;
startCap.y = root.midY - startCap.height / 2 + root.currentAmp * Math.sin((waveClip.startX / root.wavelength) * 2 * Math.PI + root.phase); startCap.y = waveY(waveClip.startX) - startCap.height / 2;
endCap.y = root.midY - endCap.height / 2 + root.currentAmp * Math.sin((waveClip.endX / root.wavelength) * 2 * Math.PI + root.phase); endCap.y = waveY(waveClip.endX) - endCap.height / 2;
actualEndCap.y = waveY(waveClip.gapEndX) - actualEndCap.height / 2;
} }
} }
+5 -1
View File
@@ -1,4 +1,5 @@
import QtQuick import QtQuick
import qs.Common
import qs.Services import qs.Services
Item { Item {
@@ -8,6 +9,7 @@ Item {
required property var targetWindow required property var targetWindow
property var blurItem: null property var blurItem: null
property bool blurEnabled: Theme.connectedSurfaceBlurEnabled
property real blurX: 0 property real blurX: 0
property real blurY: 0 property real blurY: 0
property real blurWidth: 0 property real blurWidth: 0
@@ -17,7 +19,7 @@ Item {
property var _region: null property var _region: null
function _apply() { function _apply() {
if (!BlurService.enabled || !targetWindow) { if (!blurEnabled || !BlurService.enabled || !targetWindow) {
_cleanup(); _cleanup();
return; return;
} }
@@ -43,6 +45,8 @@ Item {
_region = null; _region = null;
} }
onBlurEnabledChanged: _apply()
Connections { Connections {
target: BlurService target: BlurService
function onEnabledChanged() { function onEnabledChanged() {
@@ -3504,7 +3504,7 @@
}, },
{ {
"section": "popupTransparency", "section": "popupTransparency",
"label": "Popup Transparency", "label": "Surface Opacity",
"tabIndex": 10, "tabIndex": 10,
"category": "Theme & Colors", "category": "Theme & Colors",
"keywords": [ "keywords": [
@@ -3522,6 +3522,7 @@
"popup", "popup",
"scheme", "scheme",
"style", "style",
"surface",
"their", "their",
"theme", "theme",
"translucent", "translucent",