1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-05-02 02:22:06 -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

View File

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

View File

@@ -82,7 +82,7 @@ func (ds *DoctorStatus) OKCount() int {
}
var (
quickshellVersionRegex = regexp.MustCompile(`quickshell (\d+\.\d+\.\d+)`)
quickshellVersionRegex = regexp.MustCompile(`(?i)quickshell (\d+\.\d+\.\d+)`)
hyprlandVersionRegex = regexp.MustCompile(`v?(\d+\.\d+\.\d+)`)
niriVersionRegex = regexp.MustCompile(`niri (\d+\.\d+)`)
swayVersionRegex = regexp.MustCompile(`sway version (\d+\.\d+)`)

View File

@@ -39,11 +39,10 @@ type LayerSurface struct {
wlSurface *client.Surface
layerSurf *wlr_layer_shell.ZwlrLayerSurfaceV1
viewport *wp_viewporter.WpViewport
wlPool *client.ShmPool
wlBuffer *client.Buffer
bufferBusy bool
oldPool *client.ShmPool
oldBuffer *client.Buffer
wlPools [2]*client.ShmPool
wlBuffers [2]*client.Buffer
slotBusy [2]bool
needsRedraw bool
scopyBuffer *client.Buffer
configured bool
hidden bool
@@ -136,6 +135,7 @@ func (p *Picker) Run() (*Color, error) {
break
}
p.flushRedraws()
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 {
display, err := client.Connect("")
if err != nil {
@@ -507,47 +516,45 @@ func (p *Picker) captureForSurface(ls *LayerSurface) {
}
func (p *Picker) redrawSurface(ls *LayerSurface) {
slot := ls.state.FrontIndex()
if ls.slotBusy[slot] {
ls.needsRedraw = true
return
}
var renderBuf *ShmBuffer
if ls.hidden {
switch {
case ls.hidden:
renderBuf = ls.state.RedrawScreenOnly()
} else {
default:
renderBuf = ls.state.Redraw()
}
if renderBuf == nil {
return
}
if ls.oldBuffer != nil {
ls.oldBuffer.Destroy()
ls.oldBuffer = nil
}
if ls.oldPool != nil {
ls.oldPool.Destroy()
ls.oldPool = nil
ls.needsRedraw = false
if ls.wlPools[slot] == nil {
pool, err := p.shm.CreatePool(renderBuf.Fd(), int32(renderBuf.Size()))
if err != nil {
return
}
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.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
ls.slotBusy[slot] = true
logicalW, logicalH := ls.state.LogicalSize()
if logicalW == 0 || logicalH == 0 {
@@ -566,7 +573,7 @@ func (p *Picker) redrawSurface(ls *LayerSurface) {
}
_ = 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.Commit()
@@ -634,7 +641,7 @@ func (p *Picker) setupPointerHandlers() {
}
p.activeSurface.state.OnPointerMotion(e.SurfaceX, e.SurfaceY)
p.redrawSurface(p.activeSurface)
p.activeSurface.needsRedraw = true
})
p.pointer.SetLeaveHandler(func(e client.PointerLeaveEvent) {
@@ -655,7 +662,7 @@ func (p *Picker) setupPointerHandlers() {
return
}
p.activeSurface.state.OnPointerMotion(e.SurfaceX, e.SurfaceY)
p.redrawSurface(p.activeSurface)
p.activeSurface.needsRedraw = true
})
p.pointer.SetButtonHandler(func(e client.PointerButtonEvent) {
@@ -679,17 +686,13 @@ func (p *Picker) cleanup() {
if ls.scopyBuffer != nil {
ls.scopyBuffer.Destroy()
}
if ls.oldBuffer != nil {
ls.oldBuffer.Destroy()
}
if ls.oldPool != nil {
ls.oldPool.Destroy()
}
if ls.wlBuffer != nil {
ls.wlBuffer.Destroy()
}
if ls.wlPool != nil {
ls.wlPool.Destroy()
for i := range ls.wlBuffers {
if ls.wlBuffers[i] != nil {
ls.wlBuffers[i].Destroy()
}
if ls.wlPools[i] != nil {
ls.wlPools[i].Destroy()
}
}
if ls.viewport != nil {
ls.viewport.Destroy()

View File

@@ -274,6 +274,12 @@ func (s *SurfaceState) FrontRenderBuffer() *ShmBuffer {
return s.renderBufs[s.front]
}
func (s *SurfaceState) FrontIndex() int {
s.mu.Lock()
defer s.mu.Unlock()
return s.front
}
func (s *SurfaceState) SwapBuffers() {
s.mu.Lock()
s.front ^= 1

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)
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
if len(systemPkgs) > 0 {
progressChan <- InstallProgressMsg{
@@ -441,6 +448,37 @@ func (a *ArchDistribution) categorizePackages(dependencies []deps.Dependency, wm
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 {
if len(packages) == 0 {
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, ", ")))
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...)
progressChan <- InstallProgressMsg{

View File

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

View File

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

View File

@@ -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;
}
}

View File

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

View File

@@ -124,6 +124,8 @@ Singleton {
property string vpnLastConnected: ""
property string lastPlayerIdentity: ""
property var deviceMaxVolumes: ({})
property var hiddenOutputDeviceNames: []
property var hiddenInputDeviceNames: []

View File

@@ -14,7 +14,7 @@ import "settings/SettingsStore.js" as Store
Singleton {
id: root
readonly property int settingsConfigVersion: 5
readonly property int settingsConfigVersion: 11
readonly property bool isGreeterMode: Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true"
@@ -37,6 +37,18 @@ Singleton {
Custom
}
enum AnimationVariant {
Material,
Fluent,
Dynamic
}
enum AnimationEffect {
Standard, // 0 — M3: scale-in, rises from below
Directional, // 1 — pure large slide, no scale
Depth // 2 — medium slide with deep depth scale pop
}
enum SuspendBehavior {
Suspend,
Hibernate,
@@ -168,6 +180,12 @@ Singleton {
property int modalCustomAnimationDuration: 150
property bool enableRippleEffects: true
onEnableRippleEffectsChanged: saveSettings()
property int animationVariant: SettingsData.AnimationVariant.Material
onAnimationVariantChanged: saveSettings()
property int motionEffect: SettingsData.AnimationEffect.Standard
onMotionEffectChanged: saveSettings()
property int directionalAnimationMode: 0
onDirectionalAnimationModeChanged: saveSettings()
property bool m3ElevationEnabled: true
onM3ElevationEnabledChanged: saveSettings()
property int m3ElevationIntensity: 12
@@ -186,6 +204,7 @@ Singleton {
onPopoutElevationEnabledChanged: saveSettings()
property bool barElevationEnabled: true
onBarElevationEnabledChanged: saveSettings()
property bool blurEnabled: false
onBlurEnabledChanged: saveSettings()
property string blurBorderColor: "outline"
@@ -198,6 +217,50 @@ Singleton {
property bool blurredWallpaperLayer: false
property bool blurWallpaperOnOverview: false
property bool frameEnabled: false
onFrameEnabledChanged: saveSettings()
property real frameThickness: 16
onFrameThicknessChanged: saveSettings()
property real frameRounding: 23
onFrameRoundingChanged: saveSettings()
property string frameColor: ""
onFrameColorChanged: saveSettings()
property real frameOpacity: 1.0
onFrameOpacityChanged: saveSettings()
property var frameScreenPreferences: ["all"]
onFrameScreenPreferencesChanged: saveSettings()
property real frameBarSize: 40
onFrameBarSizeChanged: saveSettings()
property bool frameShowOnOverview: false
onFrameShowOnOverviewChanged: saveSettings()
property bool frameBlurEnabled: true
onFrameBlurEnabledChanged: saveSettings()
property int previousDirectionalMode: 1
onPreviousDirectionalModeChanged: saveSettings()
property var connectedFrameBarStyleBackups: ({})
onConnectedFrameBarStyleBackupsChanged: saveSettings()
readonly property bool connectedFrameModeActive: frameEnabled
&& motionEffect === SettingsData.AnimationEffect.Directional
&& directionalAnimationMode === 3
onConnectedFrameModeActiveChanged: {
if (_loading)
return;
if (connectedFrameModeActive) {
_captureConnectedFrameBarStyleBackups(barConfigs, true);
_enforceConnectedModeBarStyleReset();
} else {
_restoreConnectedFrameBarStyleBackups();
}
}
readonly property color effectiveFrameColor: {
const fc = frameColor;
if (!fc || fc === "default") return Theme.surfaceContainer;
if (fc === "primary") return Theme.primary;
if (fc === "surface") return Theme.surface;
return fc;
}
property bool showLauncherButton: true
property bool showWorkspaceSwitcher: true
property bool showFocusedWindow: true
@@ -1285,6 +1348,7 @@ Singleton {
_loading = false;
}
loadPluginSettings();
Qt.callLater(() => _reconcileConnectedFrameBarStyles());
}
property var _pendingMigration: null
@@ -1398,6 +1462,149 @@ Singleton {
pluginSettingsFile.setText(JSON.stringify(pluginSettings, null, 2));
}
function _connectedFrameBarStyleSnapshot(config) {
return {
"shadowIntensity": config?.shadowIntensity ?? 0,
"squareCorners": config?.squareCorners ?? false,
"gothCornersEnabled": config?.gothCornersEnabled ?? false,
"borderEnabled": config?.borderEnabled ?? false
};
}
function _hasConnectedFrameBarStyleBackups() {
return connectedFrameBarStyleBackups && Object.keys(connectedFrameBarStyleBackups).length > 0;
}
function _captureConnectedFrameBarStyleBackups(configs, overwriteExisting) {
if (!Array.isArray(configs))
return;
const nextBackups = JSON.parse(JSON.stringify(connectedFrameBarStyleBackups || {}));
const validIds = {};
let changed = false;
for (let i = 0; i < configs.length; i++) {
const config = configs[i];
if (!config?.id)
continue;
validIds[config.id] = true;
if (!overwriteExisting && nextBackups[config.id] !== undefined)
continue;
const snapshot = _connectedFrameBarStyleSnapshot(config);
if (JSON.stringify(nextBackups[config.id]) !== JSON.stringify(snapshot)) {
nextBackups[config.id] = snapshot;
changed = true;
}
}
if (overwriteExisting) {
for (const barId in nextBackups) {
if (validIds[barId])
continue;
delete nextBackups[barId];
changed = true;
}
}
if (changed)
connectedFrameBarStyleBackups = nextBackups;
}
function _restoreConnectedFrameBarStyleBackups() {
if (!_hasConnectedFrameBarStyleBackups())
return;
const backups = connectedFrameBarStyleBackups || {};
const configs = JSON.parse(JSON.stringify(barConfigs));
let changed = false;
for (let i = 0; i < configs.length; i++) {
const backup = backups[configs[i].id];
if (!backup)
continue;
for (const key in backup) {
if (configs[i][key] === backup[key])
continue;
configs[i][key] = backup[key];
changed = true;
}
}
if (changed)
barConfigs = configs;
connectedFrameBarStyleBackups = ({});
if (changed)
updateBarConfigs();
}
function _reconcileConnectedFrameBarStyles() {
if (connectedFrameModeActive) {
if (!_hasConnectedFrameBarStyleBackups())
_captureConnectedFrameBarStyleBackups(barConfigs, true);
_enforceConnectedModeBarStyleReset();
return;
}
_restoreConnectedFrameBarStyleBackups();
}
function _sanitizeBarConfigForConnectedFrame(config) {
if (!connectedFrameModeActive || !config)
return config;
let changed = false;
const sanitized = Object.assign({}, config);
if ((sanitized.shadowIntensity ?? 0) !== 0) {
sanitized.shadowIntensity = 0;
changed = true;
}
if (sanitized.squareCorners ?? false) {
sanitized.squareCorners = false;
changed = true;
}
if (sanitized.gothCornersEnabled ?? false) {
sanitized.gothCornersEnabled = false;
changed = true;
}
if (sanitized.borderEnabled ?? false) {
sanitized.borderEnabled = false;
changed = true;
}
return changed ? sanitized : config;
}
function _sanitizeBarConfigsForConnectedFrame(configs) {
if (!connectedFrameModeActive || !Array.isArray(configs))
return {
"configs": configs,
"changed": false
};
let changed = false;
const sanitizedConfigs = configs.map(config => {
const sanitized = _sanitizeBarConfigForConnectedFrame(config);
if (sanitized !== config)
changed = true;
return sanitized;
});
return {
"configs": changed ? sanitizedConfigs : configs,
"changed": changed
};
}
function _enforceConnectedModeBarStyleReset() {
const result = _sanitizeBarConfigsForConnectedFrame(barConfigs);
if (!result.changed)
return;
barConfigs = result.configs;
updateBarConfigs();
}
function detectAvailableIconThemes() {
const xdgDataDirs = Quickshell.env("XDG_DATA_DIRS") || "";
const localData = Paths.strip(StandardPaths.writableLocation(StandardPaths.GenericDataLocation));
@@ -1545,35 +1752,37 @@ Singleton {
const spacing = barSpacing !== undefined ? barSpacing : (defaultBar?.spacing ?? 4);
const position = barPosition !== undefined ? barPosition : (defaultBar?.position ?? SettingsData.Position.Top);
const rawBottomGap = barConfig ? (barConfig.bottomGap !== undefined ? barConfig.bottomGap : (defaultBar?.bottomGap ?? 0)) : (defaultBar?.bottomGap ?? 0);
const bottomGap = Math.max(0, rawBottomGap);
const isConnected = connectedFrameModeActive;
const bottomGap = isConnected ? 0 : Math.max(0, rawBottomGap);
const useAutoGaps = (barConfig && barConfig.popupGapsAuto !== undefined) ? barConfig.popupGapsAuto : (defaultBar?.popupGapsAuto ?? true);
const manualGapValue = (barConfig && barConfig.popupGapsManual !== undefined) ? barConfig.popupGapsManual : (defaultBar?.popupGapsManual ?? 4);
const popupGap = useAutoGaps ? Math.max(4, spacing) : manualGapValue;
const popupGap = isConnected ? 0 : (useAutoGaps ? Math.max(4, spacing) : manualGapValue);
const edgeSpacing = isConnected ? 0 : spacing;
switch (position) {
case SettingsData.Position.Left:
return {
"x": barThickness + spacing + popupGap,
"x": barThickness + edgeSpacing + popupGap,
"y": relativeY,
"width": widgetWidth
};
case SettingsData.Position.Right:
return {
"x": (screen?.width || 0) - (barThickness + spacing + popupGap),
"x": (screen?.width || 0) - (barThickness + edgeSpacing + popupGap),
"y": relativeY,
"width": widgetWidth
};
case SettingsData.Position.Bottom:
return {
"x": relativeX,
"y": (screen?.height || 0) - (barThickness + spacing + bottomGap + popupGap),
"y": (screen?.height || 0) - (barThickness + edgeSpacing + bottomGap + popupGap),
"width": widgetWidth
};
default:
return {
"x": relativeX,
"y": barThickness + spacing + bottomGap + popupGap,
"y": barThickness + edgeSpacing + bottomGap + popupGap,
"width": widgetWidth
};
}
@@ -1667,7 +1876,9 @@ Singleton {
const screenWidth = screen.width;
const screenHeight = screen.height;
const position = barPosition !== undefined ? barPosition : (defaultBar?.position ?? SettingsData.Position.Top);
const bottomGap = barConfig ? (barConfig.bottomGap !== undefined ? barConfig.bottomGap : (defaultBar?.bottomGap ?? 0)) : (defaultBar?.bottomGap ?? 0);
const isConnected = connectedFrameModeActive;
const rawBottomGap = barConfig ? (barConfig.bottomGap !== undefined ? barConfig.bottomGap : (defaultBar?.bottomGap ?? 0)) : (defaultBar?.bottomGap ?? 0);
const bottomGap = isConnected ? 0 : rawBottomGap;
let topOffset = 0;
let bottomOffset = 0;
@@ -1689,7 +1900,7 @@ Singleton {
const otherSpacing = other.spacing !== undefined ? other.spacing : (defaultBar?.spacing ?? 4);
const otherPadding = other.innerPadding !== undefined ? other.innerPadding : (defaultBar?.innerPadding ?? 4);
const otherThickness = Math.max(26 + otherPadding * 0.6, Theme.barHeight - 4 - (8 - otherPadding)) + otherSpacing + wingSize;
const otherBottomGap = other.bottomGap !== undefined ? other.bottomGap : (defaultBar?.bottomGap ?? 0);
const otherBottomGap = isConnected ? 0 : (other.bottomGap !== undefined ? other.bottomGap : (defaultBar?.bottomGap ?? 0));
switch (other.position) {
case SettingsData.Position.Top:
@@ -1780,7 +1991,9 @@ Singleton {
function addBarConfig(config) {
const configs = JSON.parse(JSON.stringify(barConfigs));
configs.push(config);
barConfigs = configs;
if (connectedFrameModeActive)
_captureConnectedFrameBarStyleBackups(configs, false);
barConfigs = _sanitizeBarConfigsForConnectedFrame(configs).configs;
updateBarConfigs();
}
@@ -1792,7 +2005,7 @@ Singleton {
const positionChanged = updates.position !== undefined && configs[index].position !== updates.position;
Object.assign(configs[index], updates);
barConfigs = configs;
barConfigs = _sanitizeBarConfigsForConnectedFrame(configs).configs;
updateBarConfigs();
if (positionChanged) {
@@ -1846,6 +2059,11 @@ Singleton {
return;
const configs = barConfigs.filter(cfg => cfg.id !== barId);
barConfigs = configs;
if (connectedFrameBarStyleBackups?.[barId] !== undefined) {
const nextBackups = JSON.parse(JSON.stringify(connectedFrameBarStyleBackups || {}));
delete nextBackups[barId];
connectedFrameBarStyleBackups = nextBackups;
}
updateBarConfigs();
}
@@ -1940,6 +2158,66 @@ Singleton {
return filtered;
}
function getFrameFilteredScreens() {
var prefs = frameScreenPreferences || ["all"];
if (!prefs || prefs.length === 0 || prefs.includes("all")) {
return Quickshell.screens;
}
return Quickshell.screens.filter(screen => isScreenInPreferences(screen, prefs));
}
function getActiveBarEdgeForScreen(screen) {
if (!screen) return "";
for (var i = 0; i < barConfigs.length; i++) {
var bc = barConfigs[i];
if (!bc.enabled) continue;
var prefs = bc.screenPreferences || ["all"];
if (!prefs.includes("all") && !isScreenInPreferences(screen, prefs)) continue;
switch (bc.position ?? 0) {
case SettingsData.Position.Top: return "top";
case SettingsData.Position.Bottom: return "bottom";
case SettingsData.Position.Left: return "left";
case SettingsData.Position.Right: return "right";
}
}
return "";
}
function getActiveBarEdgesForScreen(screen) {
if (!screen) return [];
var edges = [];
for (var i = 0; i < barConfigs.length; i++) {
var bc = barConfigs[i];
if (!bc.enabled) continue;
var prefs = bc.screenPreferences || ["all"];
if (!prefs.includes("all") && !isScreenInPreferences(screen, prefs)) continue;
switch (bc.position ?? 0) {
case SettingsData.Position.Top: edges.push("top"); break;
case SettingsData.Position.Bottom: edges.push("bottom"); break;
case SettingsData.Position.Left: edges.push("left"); break;
case SettingsData.Position.Right: edges.push("right"); break;
}
}
return edges;
}
function getActiveBarThicknessForScreen(screen) {
if (frameEnabled) return frameBarSize;
if (!screen) return frameThickness;
for (var i = 0; i < barConfigs.length; i++) {
var bc = barConfigs[i];
if (!bc.enabled) continue;
var prefs = bc.screenPreferences || ["all"];
if (!prefs.includes("all") && !isScreenInPreferences(screen, prefs)) continue;
const innerPadding = bc.innerPadding ?? 4;
const barT = Math.max(26 + innerPadding * 0.6, Theme.barHeight - 4 - (8 - innerPadding));
const spacing = bc.spacing ?? 4;
const bottomGap = bc.bottomGap ?? 0;
return barT + spacing + bottomGap;
}
return frameThickness;
}
function sendTestNotifications() {
NotificationService.dismissAllPopups();
sendTestNotification(0);

View File

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

View File

@@ -75,6 +75,8 @@ var SPEC = {
vpnLastConnected: { def: "" },
lastPlayerIdentity: { def: "" },
deviceMaxVolumes: { def: {} },
hiddenOutputDeviceNames: { def: [] },
hiddenInputDeviceNames: { def: [] },

View File

@@ -49,6 +49,10 @@ var SPEC = {
modalAnimationSpeed: { def: 1 },
modalCustomAnimationDuration: { def: 150 },
enableRippleEffects: { def: true },
animationVariant: { def: 0 },
motionEffect: { def: 0 },
directionalAnimationMode: { def: 0 },
previousDirectionalMode: { def: 1 },
m3ElevationEnabled: { def: true },
m3ElevationIntensity: { def: 12 },
m3ElevationOpacity: { def: 30 },
@@ -443,6 +447,7 @@ var SPEC = {
displayProfileAutoSelect: { def: false },
displayShowDisconnected: { def: false },
displaySnapToEdge: { def: true },
connectedFrameBarStyleBackups: { def: {} },
barConfigs: {
def: [{
@@ -549,7 +554,17 @@ var SPEC = {
clipboardEnterToPaste: { def: false },
launcherPluginVisibility: { def: {} },
launcherPluginOrder: { def: [] }
launcherPluginOrder: { def: [] },
frameEnabled: { def: false },
frameThickness: { def: 16 },
frameRounding: { def: 23 },
frameColor: { def: "" },
frameOpacity: { def: 1.0 },
frameScreenPreferences: { def: ["all"] },
frameBarSize: { def: 40 },
frameShowOnOverview: { def: false },
frameBlurEnabled: { def: true }
};
function getValidKeys() {

View File

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

View File

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

View File

@@ -64,11 +64,19 @@ DankModal {
activeImageLoads = 0;
shouldHaveFocus = true;
ClipboardService.reset();
if (clipboardAvailable)
ClipboardService.refresh();
keyboardController.reset();
Qt.callLater(function () {
if (clipboardAvailable) {
if (Theme.isConnectedEffect) {
Qt.callLater(() => {
if (clipboardHistoryModal.shouldBeVisible)
ClipboardService.refresh();
});
} else {
ClipboardService.refresh();
}
}
if (contentLoader.item?.searchField) {
contentLoader.item.searchField.text = "";
contentLoader.item.searchField.forceActiveFocus();

View File

@@ -53,8 +53,6 @@ DankPopout {
open();
activeImageLoads = 0;
ClipboardService.reset();
if (clipboardAvailable)
ClipboardService.refresh();
keyboardController.reset();
Qt.callLater(function () {
@@ -121,8 +119,16 @@ DankPopout {
onShouldBeVisibleChanged: {
if (!shouldBeVisible)
return;
if (clipboardAvailable)
ClipboardService.refresh();
if (clipboardAvailable) {
if (Theme.isConnectedEffect) {
Qt.callLater(() => {
if (root.shouldBeVisible)
ClipboardService.refresh();
});
} else {
ClipboardService.refresh();
}
}
keyboardController.reset();
Qt.callLater(function () {
if (contentLoader.item?.searchField) {

View File

@@ -26,15 +26,22 @@ Item {
property bool closeOnEscapeKey: true
property bool closeOnBackgroundClick: true
property string animationType: "scale"
property int animationDuration: Theme.modalAnimationDuration
property real animationScaleCollapsed: 0.96
property real animationOffset: Theme.spacingL
property list<real> animationEnterCurve: Theme.expressiveCurves.expressiveDefaultSpatial
property list<real> animationExitCurve: Theme.expressiveCurves.emphasized
property color backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
readonly property bool connectedMotionParity: Theme.isConnectedEffect
property int animationDuration: connectedMotionParity ? Theme.popoutAnimationDuration : Theme.modalAnimationDuration
property real animationScaleCollapsed: Theme.effectScaleCollapsed
property real animationOffset: Theme.effectAnimOffset
property list<real> animationEnterCurve: connectedMotionParity ? Theme.variantPopoutEnterCurve : Theme.variantModalEnterCurve
property list<real> animationExitCurve: connectedMotionParity ? Theme.variantPopoutExitCurve : Theme.variantModalExitCurve
property color backgroundColor: Theme.surfaceContainer
property color borderColor: Theme.outlineMedium
property real borderWidth: 0
property real cornerRadius: Theme.cornerRadius
readonly property bool connectedSurfaceOverride: Theme.isConnectedEffect
readonly property color effectiveBackgroundColor: connectedSurfaceOverride ? Theme.connectedSurfaceColor : backgroundColor
readonly property color effectiveBorderColor: connectedSurfaceOverride ? "transparent" : borderColor
readonly property real effectiveBorderWidth: connectedSurfaceOverride ? 0 : borderWidth
readonly property real effectiveCornerRadius: connectedSurfaceOverride ? Theme.connectedSurfaceRadius : cornerRadius
readonly property bool effectiveBlurEnabled: Theme.connectedSurfaceBlurEnabled
property bool enableShadow: true
property alias modalFocusScope: focusScope
property bool shouldBeVisible: false
@@ -45,11 +52,13 @@ Item {
property bool keepPopoutsOpen: false
property var customKeyboardFocus: null
property bool useOverlayLayer: false
property real frozenMotionOffsetX: 0
property real frozenMotionOffsetY: 0
readonly property alias contentWindow: contentWindow
readonly property alias clickCatcher: clickCatcher
readonly property bool useHyprlandFocusGrab: CompositorService.useHyprlandFocusGrab
readonly property bool useBackground: showBackground && SettingsData.modalDarkenBackground
readonly property bool useSingleWindow: CompositorService.isHyprland || useBackground
readonly property bool useSingleWindow: CompositorService.isHyprland
signal opened
signal dialogClosed
@@ -59,33 +68,34 @@ Item {
function open() {
closeTimer.stop();
const focusedScreen = CompositorService.getFocusedScreen();
const screenChanged = focusedScreen && contentWindow.screen !== focusedScreen;
if (focusedScreen) {
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();
}
}
animationsEnabled = false;
frozenMotionOffsetX = modalContainer ? modalContainer.offsetX : 0;
frozenMotionOffsetY = modalContainer ? modalContainer.offsetY : animationOffset;
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);
shouldBeVisible = true;
if (!useSingleWindow)
clickCatcher.visible = true;
contentWindow.visible = true;
shouldHaveFocus = false;
Qt.callLater(() => shouldHaveFocus = Qt.binding(() => shouldBeVisible));
Qt.callLater(() => {
animationsEnabled = true;
shouldBeVisible = true;
if (!useSingleWindow && !clickCatcher.visible)
clickCatcher.visible = true;
if (!contentWindow.visible)
contentWindow.visible = true;
shouldHaveFocus = false;
Qt.callLater(() => shouldHaveFocus = Qt.binding(() => shouldBeVisible));
});
}
function close() {
@@ -146,7 +156,7 @@ Item {
Timer {
id: closeTimer
interval: animationDuration + 50
interval: Theme.variantCloseInterval(animationDuration)
onTriggered: {
if (shouldBeVisible)
return;
@@ -160,7 +170,19 @@ Item {
readonly property var shadowLevel: Theme.elevationLevel3
readonly property real shadowFallbackOffset: 6
readonly property real shadowRenderPadding: (root.enableShadow && Theme.elevationEnabled && SettingsData.modalElevationEnabled) ? Theme.elevationRenderPadding(shadowLevel, Theme.elevationLightDirection, shadowFallbackOffset, 8, 16) : 0
readonly property real shadowMotionPadding: animationType === "slide" ? 30 : Math.max(0, animationOffset)
readonly property real shadowMotionPadding: {
if (Theme.isConnectedEffect)
return 0;
if (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode > 0 && Theme.isDirectionalEffect)
return 0; // Wayland native overlap mask
if (animationType === "slide")
return 30;
if (Theme.isDirectionalEffect)
return Math.max(Math.max(0, animationOffset), Math.max(alignedWidth, alignedHeight) * 0.9);
if (Theme.isDepthEffect)
return Math.max(Math.max(0, animationOffset), Math.max(alignedWidth, alignedHeight) * 0.35);
return Math.max(0, animationOffset);
}
readonly property real shadowBuffer: Theme.snap(shadowRenderPadding + shadowMotionPadding, dpr)
readonly property real alignedWidth: Theme.px(modalWidth, dpr)
readonly property real alignedHeight: Theme.px(modalHeight, dpr)
@@ -220,9 +242,26 @@ Item {
MouseArea {
anchors.fill: parent
enabled: root.closeOnBackgroundClick && root.shouldBeVisible
enabled: !root.useSingleWindow && root.closeOnBackgroundClick && root.shouldBeVisible
onClicked: root.backgroundClicked()
}
Rectangle {
anchors.fill: parent
z: -1
color: "black"
opacity: (!root.useSingleWindow && root.useBackground) ? (root.shouldBeVisible ? root.backgroundOpacity : 0) : 0
visible: opacity > 0
Behavior on opacity {
enabled: root.animationsEnabled && (!Theme.isDirectionalEffect || Theme.isConnectedEffect)
NumberAnimation {
duration: Math.round(Theme.variantDuration(root.animationDuration, root.shouldBeVisible) * Theme.variantOpacityDurationScale)
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
}
}
}
}
PanelWindow {
@@ -232,12 +271,13 @@ Item {
WindowBlur {
targetWindow: contentWindow
blurEnabled: root.effectiveBlurEnabled
readonly property real s: Math.min(1, modalContainer.scaleValue)
blurX: modalContainer.x + modalContainer.width * (1 - s) * 0.5 + Theme.snap(modalContainer.animX, root.dpr)
blurY: modalContainer.y + modalContainer.height * (1 - s) * 0.5 + Theme.snap(modalContainer.animY, root.dpr)
blurWidth: (shouldBeVisible && animatedContent.opacity > 0) ? modalContainer.width * s : 0
blurHeight: (shouldBeVisible && animatedContent.opacity > 0) ? modalContainer.height * s : 0
blurRadius: root.cornerRadius
blurWidth: (root.shouldBeVisible && animatedContent.opacity > 0) ? modalContainer.width * s : 0
blurHeight: (root.shouldBeVisible && animatedContent.opacity > 0) ? modalContainer.height * s : 0
blurRadius: root.effectiveCornerRadius
}
WlrLayershell.namespace: root.layerNamespace
@@ -275,9 +315,12 @@ Item {
bottom: root.useSingleWindow
}
readonly property real actualMarginLeft: root.useSingleWindow ? 0 : Math.max(0, Theme.snap(root.alignedX - shadowBuffer, dpr))
readonly property real actualMarginTop: root.useSingleWindow ? 0 : Math.max(0, Theme.snap(root.alignedY - shadowBuffer, dpr))
WlrLayershell.margins {
left: root.useSingleWindow ? 0 : Math.max(0, Theme.snap(root.alignedX - shadowBuffer, dpr))
top: root.useSingleWindow ? 0 : Math.max(0, Theme.snap(root.alignedY - shadowBuffer, dpr))
left: actualMarginLeft
top: actualMarginTop
right: 0
bottom: 0
}
@@ -307,13 +350,14 @@ Item {
anchors.fill: parent
z: -1
color: "black"
opacity: root.useBackground ? (root.shouldBeVisible ? root.backgroundOpacity : 0) : 0
visible: root.useBackground
opacity: (root.useSingleWindow && root.useBackground) ? (root.shouldBeVisible ? root.backgroundOpacity : 0) : 0
visible: opacity > 0
Behavior on opacity {
enabled: root.animationsEnabled
DankAnim {
duration: root.animationDuration
enabled: root.animationsEnabled && (!Theme.isDirectionalEffect || Theme.isConnectedEffect)
NumberAnimation {
duration: Math.round(Theme.variantDuration(root.animationDuration, root.shouldBeVisible) * Theme.variantOpacityDurationScale)
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
}
}
@@ -321,8 +365,8 @@ Item {
Item {
id: modalContainer
x: root.useSingleWindow ? root.alignedX : shadowBuffer
y: root.useSingleWindow ? root.alignedY : shadowBuffer
x: (root.useSingleWindow ? root.alignedX : (root.alignedX - contentWindow.actualMarginLeft)) + Theme.snap(animX, root.dpr)
y: (root.useSingleWindow ? root.alignedY : (root.alignedY - contentWindow.actualMarginTop)) + Theme.snap(animY, root.dpr)
width: root.alignedWidth
height: root.alignedHeight
@@ -338,45 +382,117 @@ Item {
}
readonly property bool slide: root.animationType === "slide"
readonly property real offsetX: slide ? 15 : 0
readonly property real offsetY: slide ? -30 : root.animationOffset
property real animX: 0
property real animY: 0
property real scaleValue: root.animationScaleCollapsed
onOffsetXChanged: animX = Theme.snap(root.shouldBeVisible ? 0 : offsetX, root.dpr)
onOffsetYChanged: animY = Theme.snap(root.shouldBeVisible ? 0 : offsetY, root.dpr)
Connections {
target: root
function onShouldBeVisibleChanged() {
modalContainer.animX = Theme.snap(root.shouldBeVisible ? 0 : modalContainer.offsetX, root.dpr);
modalContainer.animY = Theme.snap(root.shouldBeVisible ? 0 : modalContainer.offsetY, root.dpr);
modalContainer.scaleValue = root.shouldBeVisible ? 1.0 : root.animationScaleCollapsed;
readonly property bool directionalEffect: Theme.isDirectionalEffect
readonly property bool depthEffect: Theme.isDepthEffect
readonly property real directionalTravel: Math.max(root.animationOffset, Math.max(root.alignedWidth, root.alignedHeight) * 0.8)
readonly property real depthTravel: Math.max(root.animationOffset * 0.8, 36)
readonly property real customAnchorX: root.alignedX + root.alignedWidth * 0.5
readonly property real customAnchorY: root.alignedY + root.alignedHeight * 0.5
readonly property real customDistLeft: customAnchorX
readonly property real customDistRight: root.screenWidth - customAnchorX
readonly property real customDistTop: customAnchorY
readonly property real customDistBottom: root.screenHeight - customAnchorY
readonly property real offsetX: {
if (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 && Theme.isDirectionalEffect)
return 0;
if (slide && !directionalEffect && !depthEffect)
return 15;
if (directionalEffect) {
switch (root.positioning) {
case "top-right":
return 0;
case "custom":
if (customDistLeft <= customDistRight && customDistLeft <= customDistTop && customDistLeft <= customDistBottom)
return -directionalTravel;
if (customDistRight <= customDistTop && customDistRight <= customDistBottom)
return directionalTravel;
return 0;
default:
return 0;
}
}
if (depthEffect) {
switch (root.positioning) {
case "top-right":
return 0;
case "custom":
if (customDistLeft <= customDistRight && customDistLeft <= customDistTop && customDistLeft <= customDistBottom)
return -depthTravel;
if (customDistRight <= customDistTop && customDistRight <= customDistBottom)
return depthTravel;
return 0;
default:
return 0;
}
}
return 0;
}
readonly property real offsetY: {
if (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 && Theme.isDirectionalEffect)
return 0;
if (slide && !directionalEffect && !depthEffect)
return -30;
if (directionalEffect) {
switch (root.positioning) {
case "top-right":
return -Math.max(directionalTravel * 0.65, 96);
case "custom":
if (customDistTop <= customDistBottom && customDistTop <= customDistLeft && customDistTop <= customDistRight)
return -directionalTravel;
if (customDistBottom <= customDistLeft && customDistBottom <= customDistRight)
return directionalTravel;
return 0;
default:
// Default to sliding down from top when centered
return -Math.max(directionalTravel, root.screenHeight * 0.24);
}
}
if (depthEffect) {
switch (root.positioning) {
case "top-right":
return -depthTravel * 0.75;
case "custom":
if (customDistTop <= customDistBottom && customDistTop <= customDistLeft && customDistTop <= customDistRight)
return -depthTravel;
if (customDistBottom <= customDistLeft && customDistBottom <= customDistRight)
return depthTravel;
return depthTravel * 0.45;
default:
return -depthTravel;
}
}
return root.animationOffset;
}
property real animX: root.shouldBeVisible ? 0 : root.frozenMotionOffsetX
property real animY: root.shouldBeVisible ? 0 : root.frozenMotionOffsetY
readonly property real computedScaleCollapsed: (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 && Theme.isDirectionalEffect) ? 0.0 : root.animationScaleCollapsed
property real scaleValue: root.shouldBeVisible ? 1.0 : computedScaleCollapsed
Behavior on animX {
enabled: root.animationsEnabled
DankAnim {
duration: root.animationDuration
NumberAnimation {
duration: Theme.variantDuration(root.animationDuration, root.shouldBeVisible)
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
}
}
Behavior on animY {
enabled: root.animationsEnabled
DankAnim {
duration: root.animationDuration
NumberAnimation {
duration: Theme.variantDuration(root.animationDuration, root.shouldBeVisible)
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
}
}
Behavior on scaleValue {
enabled: root.animationsEnabled
DankAnim {
duration: root.animationDuration
NumberAnimation {
duration: Theme.variantDuration(root.animationDuration, root.shouldBeVisible)
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
}
}
@@ -392,15 +508,14 @@ Item {
id: animatedContent
anchors.fill: parent
clip: false
opacity: root.shouldBeVisible ? 1 : 0
opacity: (Theme.isDirectionalEffect && !Theme.isConnectedEffect) ? 1 : (root.shouldBeVisible ? 1 : 0)
scale: modalContainer.scaleValue
x: Theme.snap(modalContainer.animX, root.dpr) + (parent.width - width) * (1 - modalContainer.scaleValue) * 0.5
y: Theme.snap(modalContainer.animY, root.dpr) + (parent.height - height) * (1 - modalContainer.scaleValue) * 0.5
transformOrigin: Item.Center
Behavior on opacity {
enabled: root.animationsEnabled
enabled: root.animationsEnabled && (!Theme.isDirectionalEffect || Theme.isConnectedEffect)
NumberAnimation {
duration: animationDuration
duration: Math.round(Theme.variantDuration(animationDuration, root.shouldBeVisible) * Theme.variantOpacityDurationScale)
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
}
@@ -411,19 +526,19 @@ Item {
anchors.fill: parent
level: root.shadowLevel
fallbackOffset: root.shadowFallbackOffset
targetRadius: root.cornerRadius
targetColor: root.backgroundColor
borderColor: root.borderColor
borderWidth: root.borderWidth
targetRadius: root.effectiveCornerRadius
targetColor: root.effectiveBackgroundColor
borderColor: root.effectiveBorderColor
borderWidth: root.effectiveBorderWidth
shadowEnabled: root.enableShadow && Theme.elevationEnabled && SettingsData.modalElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1"
}
Rectangle {
anchors.fill: parent
radius: root.cornerRadius
radius: root.effectiveCornerRadius
color: "transparent"
border.color: BlurService.borderColor
border.width: BlurService.borderWidth
border.color: root.connectedSurfaceOverride ? "transparent" : BlurService.borderColor
border.width: root.connectedSurfaceOverride ? 0 : BlurService.borderWidth
z: 100
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -23,6 +23,31 @@ Item {
readonly property real innerPadding: barConfig?.innerPadding ?? 4
readonly property real outlineThickness: (barConfig?.widgetOutlineEnabled ?? false) ? (barConfig?.widgetOutlineThickness ?? 1) : 0
readonly property real _frameLeftInset: {
if (!SettingsData.frameEnabled || barWindow.isVertical) return 0
return barWindow.hasAdjacentLeftBar
? SettingsData.frameBarSize
: 0
}
readonly property real _frameRightInset: {
if (!SettingsData.frameEnabled || barWindow.isVertical) return 0
return barWindow.hasAdjacentRightBar
? SettingsData.frameBarSize
: 0
}
readonly property real _frameTopInset: {
if (!SettingsData.frameEnabled || !barWindow.isVertical) return 0
return barWindow.hasAdjacentTopBar
? SettingsData.frameThickness
: 0
}
readonly property real _frameBottomInset: {
if (!SettingsData.frameEnabled || !barWindow.isVertical) return 0
return barWindow.hasAdjacentBottomBar
? SettingsData.frameThickness
: 0
}
property alias hLeftSection: hLeftSection
property alias hCenterSection: hCenterSection
property alias hRightSection: hRightSection
@@ -31,10 +56,14 @@ Item {
property alias vRightSection: vRightSection
anchors.fill: parent
anchors.leftMargin: Math.max(Theme.spacingXS, innerPadding * 0.8)
anchors.rightMargin: Math.max(Theme.spacingXS, innerPadding * 0.8)
anchors.topMargin: barWindow.isVertical ? (barWindow.hasAdjacentTopBar ? outlineThickness : Theme.spacingXS) : 0
anchors.bottomMargin: barWindow.isVertical ? (barWindow.hasAdjacentBottomBar ? outlineThickness : Theme.spacingXS) : 0
anchors.leftMargin: Math.max(Theme.spacingXS, innerPadding * 0.8) + _frameLeftInset
anchors.rightMargin: Math.max(Theme.spacingXS, innerPadding * 0.8) + _frameRightInset
anchors.topMargin: (barWindow.isVertical
? (barWindow.hasAdjacentTopBar ? outlineThickness : Theme.spacingXS)
: 0) + _frameTopInset
anchors.bottomMargin: (barWindow.isVertical
? (barWindow.hasAdjacentBottomBar ? outlineThickness : Theme.spacingXS)
: 0) + _frameBottomInset
clip: false
property int componentMapRevision: 0
@@ -1156,6 +1185,7 @@ Item {
if (!notificationCenterLoader.item) {
return;
}
notificationCenterLoader.item.triggerScreen = barWindow.screen;
const effectiveBarConfig = topBarContent.barConfig;
const barPosition = barWindow.axis?.edge === "left" ? 2 : (barWindow.axis?.edge === "right" ? 3 : (barWindow.axis?.edge === "top" ? 0 : 1));
if (notificationCenterLoader.item.setBarContext) {
@@ -1438,12 +1468,21 @@ Item {
parentScreen: barWindow.screen
onClicked: {
systemUpdateLoader.active = true;
if (!systemUpdateLoader.item)
return;
const popout = systemUpdateLoader.item;
const effectiveBarConfig = topBarContent.barConfig;
const barPosition = barWindow.axis?.edge === "left" ? 2 : (barWindow.axis?.edge === "right" ? 3 : (barWindow.axis?.edge === "top" ? 0 : 1));
if (systemUpdateLoader.item && systemUpdateLoader.item.setBarContext) {
systemUpdateLoader.item.setBarContext(barPosition, effectiveBarConfig?.bottomGap ?? 0);
if (popout.setBarContext) {
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");
}
}
}

View File

@@ -133,6 +133,11 @@ PanelWindow {
teardown();
if (!BlurService.enabled || !BlurService.available)
return;
// In frame mode, FrameWindow owns the blur region for the entire screen edge
// (including the bar area). The bar must not set its own competing blur region
// so that frameBlurEnabled acts as the single control for all blur in frame mode.
if (SettingsData.frameEnabled)
return;
const widgets = barWindow._blurWidgetItems.filter(w => w && w.visible && w.width > 0 && w.height > 0);
const hasBar = barHasTransparency;
@@ -187,6 +192,11 @@ PanelWindow {
}
}
Connections {
target: SettingsData
function onFrameEnabledChanged() { barBlur.rebuild(); }
}
Connections {
target: topBarSlide
function onXChanged() {
@@ -238,7 +248,9 @@ PanelWindow {
readonly property color _surfaceContainer: Theme.surfaceContainer
readonly property string _barId: barConfig?.id ?? "default"
property real _backgroundAlpha: barConfig?.transparency ?? 1.0
readonly property color _bgColor: Theme.withAlpha(_surfaceContainer, _backgroundAlpha)
readonly property color _bgColor: SettingsData.frameEnabled
? Qt.rgba(SettingsData.effectiveFrameColor.r, SettingsData.effectiveFrameColor.g, SettingsData.effectiveFrameColor.b, SettingsData.frameOpacity)
: Theme.withAlpha(_surfaceContainer, _backgroundAlpha)
function _updateBackgroundAlpha() {
const live = SettingsData.barConfigs.find(c => c.id === _barId);
@@ -384,7 +396,7 @@ PanelWindow {
shouldHideForWindows = filtered.length > 0;
}
property real effectiveSpacing: hasMaximizedToplevel ? 0 : (barConfig?.spacing ?? 4)
property real effectiveSpacing: SettingsData.frameEnabled ? 0 : (hasMaximizedToplevel ? 0 : (barConfig?.spacing ?? 4))
Behavior on effectiveSpacing {
enabled: barWindow.visible
@@ -395,7 +407,12 @@ PanelWindow {
}
readonly property int notificationCount: NotificationService.notifications.length
readonly property real effectiveBarThickness: Theme.snap(Math.max(barWindow.widgetThickness + (barConfig?.innerPadding ?? 4) + 4, Theme.barHeight - 4 - (8 - (barConfig?.innerPadding ?? 4))), _dpr)
readonly property real effectiveBarThickness: SettingsData.frameEnabled
? SettingsData.frameBarSize
: Theme.snap(Math.max(barWindow.widgetThickness + (barConfig?.innerPadding ?? 4) + 4, Theme.barHeight - 4 - (8 - (barConfig?.innerPadding ?? 4))), _dpr)
readonly property bool effectiveOpenOnOverview: SettingsData.frameEnabled
? SettingsData.frameShowOnOverview
: (barConfig?.openOnOverview ?? false)
readonly property real widgetThickness: Theme.snap(Math.max(20, 26 + (barConfig?.innerPadding ?? 4) * 0.6), _dpr)
readonly property bool hasAdjacentTopBar: {
@@ -644,14 +661,14 @@ PanelWindow {
anchors.left: !isVertical ? true : (barPos === SettingsData.Position.Left)
anchors.right: !isVertical ? true : (barPos === SettingsData.Position.Right)
exclusiveZone: (!(barConfig?.visible ?? true) || topBarCore.autoHide) ? -1 : (barWindow.effectiveBarThickness + effectiveSpacing + (barConfig?.bottomGap ?? 0))
exclusiveZone: (!(barConfig?.visible ?? true) || topBarCore.autoHide) ? -1 : (barWindow.effectiveBarThickness + effectiveSpacing + (Theme.isConnectedEffect ? 0 : (barConfig?.bottomGap ?? 0)))
Item {
id: inputMask
readonly property int barThickness: Theme.px(barWindow.effectiveBarThickness + barWindow.effectiveSpacing, barWindow._dpr)
readonly property bool inOverviewWithShow: CompositorService.isNiri && NiriService.inOverview && (barConfig?.openOnOverview ?? false)
readonly property bool inOverviewWithShow: CompositorService.isNiri && NiriService.inOverview && barWindow.effectiveOpenOnOverview
readonly property bool effectiveVisible: (barConfig?.visible ?? true) || inOverviewWithShow
readonly property bool showing: effectiveVisible && (topBarCore.reveal || inOverviewWithShow || !topBarCore.autoHide)
@@ -792,7 +809,7 @@ PanelWindow {
}
property bool reveal: {
const inOverviewWithShow = CompositorService.isNiri && NiriService.inOverview && (barConfig?.openOnOverview ?? false);
const inOverviewWithShow = CompositorService.isNiri && NiriService.inOverview && barWindow.effectiveOpenOnOverview;
if (inOverviewWithShow)
return true;
@@ -889,7 +906,7 @@ PanelWindow {
top: barWindow.isVertical ? parent.top : undefined
bottom: barWindow.isVertical ? parent.bottom : undefined
}
readonly property bool inOverview: CompositorService.isNiri && NiriService.inOverview && (barConfig?.openOnOverview ?? false)
readonly property bool inOverview: CompositorService.isNiri && NiriService.inOverview && barWindow.effectiveOpenOnOverview
hoverEnabled: (barConfig?.autoHide ?? false) && !inOverview && !topBarCore.hasActivePopout
acceptedButtons: Qt.NoButton
enabled: (barConfig?.autoHide ?? false) && !inOverview

View File

@@ -271,7 +271,7 @@ BasePill {
radius: Theme.cornerRadius
color: {
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";
}
@@ -526,7 +526,7 @@ BasePill {
radius: Theme.cornerRadius
color: {
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";
}

View File

@@ -16,7 +16,6 @@ DankPopout {
popupHeight: contentLoader.item ? contentLoader.item.implicitHeight : 500
triggerWidth: 80
screen: triggerScreen
shouldBeVisible: dashVisible
property bool __focusArmed: false
property bool __contentReady: false
@@ -100,7 +99,7 @@ DankPopout {
if (currentPlayer && currentPlayer !== player && currentPlayer.canPause) {
currentPlayer.pause();
}
MprisController.activePlayer = player;
MprisController.setActivePlayer(player);
root.__hideDropdowns();
}
onDeviceSelected: device => {

View File

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

View File

@@ -19,11 +19,12 @@ Variants {
WindowBlur {
targetWindow: dock
blurEnabled: dock.effectiveBlurEnabled && !SettingsData.connectedFrameModeActive
blurX: dockBackground.x + dockContainer.x + dockMouseArea.x + dockCore.x + dockSlide.x
blurY: dockBackground.y + dockContainer.y + dockMouseArea.y + dockCore.y + dockSlide.y
blurWidth: dock.hasApps && dock.reveal ? dockBackground.width : 0
blurHeight: dock.hasApps && dock.reveal ? dockBackground.height : 0
blurRadius: Theme.cornerRadius
blurRadius: Theme.isConnectedEffect ? Theme.connectedCornerRadius : dock.surfaceRadius
}
WlrLayershell.namespace: "dms:dock"
@@ -42,6 +43,23 @@ Variants {
property real backgroundTransparency: SettingsData.dockTransparency
property bool groupByApp: SettingsData.dockGroupByApp
readonly property int borderThickness: SettingsData.dockBorderEnabled ? SettingsData.dockBorderThickness : 0
readonly property string connectedBarSide: SettingsData.dockPosition === SettingsData.Position.Top ? "top" : SettingsData.dockPosition === SettingsData.Position.Bottom ? "bottom" : SettingsData.dockPosition === SettingsData.Position.Left ? "left" : "right"
readonly property bool connectedBarActiveOnEdge: Theme.isConnectedEffect && !!(dock.screen || modelData) && SettingsData.getActiveBarEdgesForScreen(dock.screen || modelData).includes(connectedBarSide)
readonly property real connectedJoinInset: {
if (!Theme.isConnectedEffect)
return 0;
return connectedBarActiveOnEdge ? SettingsData.frameBarSize : SettingsData.frameThickness;
}
readonly property real surfaceRadius: Theme.connectedSurfaceRadius
readonly property color surfaceColor: Theme.isConnectedEffect ? Theme.connectedSurfaceColor : Theme.withAlpha(Theme.surfaceContainer, backgroundTransparency)
readonly property color surfaceBorderColor: Theme.isConnectedEffect ? "transparent" : BlurService.borderColor
readonly property real surfaceBorderWidth: Theme.isConnectedEffect ? 0 : BlurService.borderWidth
readonly property real surfaceTopLeftRadius: Theme.isConnectedEffect && (SettingsData.dockPosition === SettingsData.Position.Top || SettingsData.dockPosition === SettingsData.Position.Left) ? 0 : surfaceRadius
readonly property real surfaceTopRightRadius: Theme.isConnectedEffect && (SettingsData.dockPosition === SettingsData.Position.Top || SettingsData.dockPosition === SettingsData.Position.Right) ? 0 : surfaceRadius
readonly property real surfaceBottomLeftRadius: Theme.isConnectedEffect && (SettingsData.dockPosition === SettingsData.Position.Bottom || SettingsData.dockPosition === SettingsData.Position.Left) ? 0 : surfaceRadius
readonly property real surfaceBottomRightRadius: Theme.isConnectedEffect && (SettingsData.dockPosition === SettingsData.Position.Bottom || SettingsData.dockPosition === SettingsData.Position.Right) ? 0 : surfaceRadius
readonly property real horizontalConnectorExtent: Theme.isConnectedEffect && !isVertical ? Theme.connectedCornerRadius : 0
readonly property real verticalConnectorExtent: Theme.isConnectedEffect && isVertical ? Theme.connectedCornerRadius : 0
readonly property int hasApps: dockApps.implicitWidth > 0 || dockApps.implicitHeight > 0
@@ -113,13 +131,102 @@ Variants {
return getBarHeight(leftBar);
}
readonly property real dockMargin: SettingsData.dockSpacing
readonly property real positionSpacing: barSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin
readonly property real dockMargin: SettingsData.dockMargin
readonly property bool effectiveBlurEnabled: Theme.connectedSurfaceBlurEnabled
readonly property real effectiveDockBottomGap: Theme.isConnectedEffect ? 0 : SettingsData.dockBottomGap
readonly property real effectiveDockMargin: Theme.isConnectedEffect ? 0 : SettingsData.dockMargin
readonly property real positionSpacing: barSpacing + effectiveDockBottomGap + effectiveDockMargin
readonly property real joinedEdgeMargin: Theme.isConnectedEffect ? 0 : (barSpacing + effectiveDockMargin + 1 + dock.borderThickness)
readonly property real _dpr: (dock.screen && dock.screen.devicePixelRatio) ? dock.screen.devicePixelRatio : 1
function px(v) {
return Math.round(v * _dpr) / _dpr;
}
function connectorWidth(spacing) {
return dock.isVertical ? (spacing + Theme.connectedCornerRadius) : Theme.connectedCornerRadius;
}
function connectorHeight(spacing) {
return dock.isVertical ? Theme.connectedCornerRadius : (spacing + Theme.connectedCornerRadius);
}
function connectorSeamX(baseX, bodyWidth, placement) {
if (!dock.isVertical)
return placement === "left" ? baseX : baseX + bodyWidth;
return SettingsData.dockPosition === SettingsData.Position.Left ? baseX : baseX + bodyWidth;
}
function connectorSeamY(baseY, bodyHeight, placement) {
if (SettingsData.dockPosition === SettingsData.Position.Top)
return baseY;
if (SettingsData.dockPosition === SettingsData.Position.Bottom)
return baseY + bodyHeight;
return placement === "left" ? baseY : baseY + bodyHeight;
}
function connectorX(baseX, bodyWidth, placement, spacing) {
const seamX = connectorSeamX(baseX, bodyWidth, placement);
const width = connectorWidth(spacing);
if (!dock.isVertical)
return placement === "left" ? seamX - width : seamX;
return SettingsData.dockPosition === SettingsData.Position.Left ? seamX : seamX - width;
}
function connectorY(baseY, bodyHeight, placement, spacing) {
const seamY = connectorSeamY(baseY, bodyHeight, placement);
const height = connectorHeight(spacing);
if (SettingsData.dockPosition === SettingsData.Position.Top)
return seamY;
if (SettingsData.dockPosition === SettingsData.Position.Bottom)
return seamY - height;
return placement === "left" ? seamY - height : seamY;
}
// ─── 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 revealSticky: false
@@ -130,7 +237,7 @@ Variants {
return false;
const screenName = dock.modelData?.name ?? "";
const dockThickness = effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin;
const dockThickness = dock.connectedJoinInset + effectiveBarHeight + SettingsData.dockSpacing + dock.effectiveDockBottomGap + dock.effectiveDockMargin;
const screenWidth = dock.screen?.width ?? 0;
const screenHeight = dock.screen?.height ?? 0;
@@ -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 {
target: SettingsData
function onDockTransparencyChanged() {
@@ -302,13 +426,13 @@ Variants {
return -1;
if (barSpacing > 0)
return -1;
return px(effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin);
return px(connectedJoinInset + effectiveBarHeight + SettingsData.dockSpacing + effectiveDockBottomGap + effectiveDockMargin);
}
property real animationHeadroom: Math.ceil(SettingsData.dockIconSize * 0.35)
implicitWidth: isVertical ? (px(effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockMargin + SettingsData.dockIconSize * 0.3) + animationHeadroom) : 0
implicitHeight: !isVertical ? (px(effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockMargin + SettingsData.dockIconSize * 0.3) + animationHeadroom) : 0
implicitWidth: isVertical ? (px(connectedJoinInset + effectiveBarHeight + SettingsData.dockSpacing + effectiveDockMargin + SettingsData.dockIconSize * 0.3) + animationHeadroom) : 0
implicitHeight: !isVertical ? (px(connectedJoinInset + effectiveBarHeight + SettingsData.dockSpacing + effectiveDockMargin + SettingsData.dockIconSize * 0.3) + animationHeadroom) : 0
Item {
id: maskItem
@@ -318,17 +442,17 @@ Variants {
x: {
const baseX = dockCore.x + dockMouseArea.x;
if (isVertical && SettingsData.dockPosition === SettingsData.Position.Right)
return baseX - (expanded ? animationHeadroom + borderThickness : 0);
return baseX - (expanded ? borderThickness : 0);
return baseX - (expanded ? animationHeadroom + borderThickness + dock.horizontalConnectorExtent : 0);
return baseX - (expanded ? borderThickness + dock.horizontalConnectorExtent : 0);
}
y: {
const baseY = dockCore.y + dockMouseArea.y;
if (!isVertical && SettingsData.dockPosition === SettingsData.Position.Bottom)
return baseY - (expanded ? animationHeadroom + borderThickness : 0);
return baseY - (expanded ? borderThickness : 0);
return baseY - (expanded ? animationHeadroom + borderThickness + dock.verticalConnectorExtent : 0);
return baseY - (expanded ? borderThickness + dock.verticalConnectorExtent : 0);
}
width: dockMouseArea.width + (isVertical && expanded ? animationHeadroom : 0) + (expanded ? borderThickness * 2 : 0)
height: dockMouseArea.height + (!isVertical && expanded ? animationHeadroom : 0) + (expanded ? borderThickness * 2 : 0)
width: dockMouseArea.width + (isVertical && expanded ? animationHeadroom : 0) + (expanded ? borderThickness * 2 + dock.horizontalConnectorExtent * 2 : 0)
height: dockMouseArea.height + (!isVertical && expanded ? animationHeadroom : 0) + (expanded ? borderThickness * 2 + dock.verticalConnectorExtent * 2 : 0)
}
mask: Region {
@@ -388,7 +512,7 @@ Variants {
const screenHeight = dock.screen ? dock.screen.height : 0;
const gap = Theme.spacingS;
const bgMargin = barSpacing + SettingsData.dockMargin + 1 + dock.borderThickness;
const bgMargin = dock.joinedEdgeMargin + dock.connectedJoinInset;
const btnW = dock.hoveredButton.width;
const btnH = dock.hoveredButton.height;
@@ -459,11 +583,11 @@ Variants {
// Keep the taller hit area regardless of the reveal state to prevent shrinking loop
return Math.min(Math.max(dockBackground.height + 64, 200), maxDockHeight);
}
return dock.reveal ? px(dock.effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin) : 1;
return dock.reveal ? px(dock.connectedJoinInset + dock.effectiveBarHeight + SettingsData.dockSpacing + dock.effectiveDockBottomGap + dock.effectiveDockMargin) : 1;
}
width: {
if (dock.isVertical) {
return dock.reveal ? px(dock.effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin) : 1;
return dock.reveal ? px(dock.connectedJoinInset + dock.effectiveBarHeight + SettingsData.dockSpacing + dock.effectiveDockBottomGap + dock.effectiveDockMargin) : 1;
}
// Keep the wider hit area regardless of the reveal state to prevent shrinking loop
return Math.min(dockBackground.width + 8 + dock.borderThickness, maxDockWidth);
@@ -505,7 +629,11 @@ Variants {
return 0;
if (dock.reveal)
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) {
return hideDistance;
} else {
@@ -517,7 +645,11 @@ Variants {
return 0;
if (dock.reveal)
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) {
return hideDistance;
} else {
@@ -528,18 +660,29 @@ Variants {
Behavior on x {
NumberAnimation {
id: slideXAnimation
duration: Theme.shortDuration
easing.type: Easing.OutCubic
duration: Theme.isConnectedEffect ? Theme.variantDuration(Theme.popoutAnimationDuration, dock.reveal) : Theme.shortDuration
easing.type: Theme.isConnectedEffect ? Easing.BezierSpline : Easing.OutCubic
easing.bezierCurve: Theme.isConnectedEffect ? (dock.reveal ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve) : []
onRunningChanged: if (!running) dock._syncDockChromeState()
}
}
Behavior on y {
NumberAnimation {
id: slideYAnimation
duration: Theme.shortDuration
easing.type: Easing.OutCubic
duration: Theme.isConnectedEffect ? Theme.variantDuration(Theme.popoutAnimationDuration, dock.reveal) : Theme.shortDuration
easing.type: Theme.isConnectedEffect ? Easing.BezierSpline : Easing.OutCubic
easing.bezierCurve: Theme.isConnectedEffect ? (dock.reveal ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve) : []
onRunningChanged: if (!running) dock._syncDockChromeState()
}
}
onXChanged: {
dock._syncDockSlide();
}
onYChanged: {
dock._syncDockSlide();
}
}
Item {
@@ -553,33 +696,72 @@ Variants {
right: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Right ? parent.right : undefined) : undefined
verticalCenter: dock.isVertical ? parent.verticalCenter : undefined
}
anchors.topMargin: !dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Top ? barSpacing + SettingsData.dockMargin + 1 + dock.borderThickness : 0
anchors.bottomMargin: !dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Bottom ? barSpacing + SettingsData.dockMargin + 1 + dock.borderThickness : 0
anchors.leftMargin: dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Left ? barSpacing + SettingsData.dockMargin + 1 + dock.borderThickness : 0
anchors.rightMargin: dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Right ? barSpacing + SettingsData.dockMargin + 1 + dock.borderThickness : 0
anchors.topMargin: !dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Top ? (dock.connectedJoinInset + dock.joinedEdgeMargin) : 0
anchors.bottomMargin: !dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Bottom ? (dock.connectedJoinInset + dock.joinedEdgeMargin) : 0
anchors.leftMargin: dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Left ? (dock.connectedJoinInset + dock.joinedEdgeMargin) : 0
anchors.rightMargin: dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Right ? (dock.connectedJoinInset + dock.joinedEdgeMargin) : 0
implicitWidth: dock.isVertical ? (dockApps.implicitHeight + SettingsData.dockSpacing * 2) : (dockApps.implicitWidth + SettingsData.dockSpacing * 2)
implicitHeight: dock.isVertical ? (dockApps.implicitWidth + SettingsData.dockSpacing * 2) : (dockApps.implicitHeight + SettingsData.dockSpacing * 2)
width: implicitWidth
height: implicitHeight
layer.enabled: true
// Avoid an offscreen texture seam where the connected dock meets the frame.
layer.enabled: !Theme.isConnectedEffect
clip: false
Rectangle {
anchors.fill: parent
color: Theme.withAlpha(Theme.surfaceContainer, backgroundTransparency)
radius: Theme.cornerRadius
visible: !SettingsData.connectedFrameModeActive
color: dock.surfaceColor
topLeftRadius: dock.surfaceTopLeftRadius
topRightRadius: dock.surfaceTopRightRadius
bottomLeftRadius: dock.surfaceBottomLeftRadius
bottomRightRadius: dock.surfaceBottomRightRadius
}
Rectangle {
anchors.fill: parent
visible: !SettingsData.connectedFrameModeActive
color: "transparent"
radius: Theme.cornerRadius
border.color: BlurService.borderColor
border.width: BlurService.borderWidth
topLeftRadius: dock.surfaceTopLeftRadius
topRightRadius: dock.surfaceTopRightRadius
bottomLeftRadius: dock.surfaceBottomLeftRadius
bottomRightRadius: dock.surfaceBottomRightRadius
border.color: dock.surfaceBorderColor
border.width: dock.surfaceBorderWidth
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 {
@@ -588,12 +770,12 @@ Variants {
y: dockBackground.y - borderThickness
width: dockBackground.width + borderThickness * 2
height: dockBackground.height + borderThickness * 2
visible: SettingsData.dockBorderEnabled && dock.hasApps
visible: SettingsData.dockBorderEnabled && dock.hasApps && !Theme.isConnectedEffect
preferredRendererType: Shape.CurveRenderer
readonly property real borderThickness: Math.max(1, dock.borderThickness)
readonly property real i: borderThickness / 2
readonly property real cr: Theme.cornerRadius
readonly property real cr: dock.surfaceRadius
readonly property real w: dockBackground.width
readonly property real h: dockBackground.height

View File

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

View File

@@ -0,0 +1,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
}
}
}

View File

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

View File

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

View File

@@ -0,0 +1,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
}
}
}
}

View File

@@ -34,11 +34,12 @@ Rectangle {
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 baseCardHeight: cardPadding * 2 + collapsedContentHeight + actionButtonHeight + contentSpacing
readonly property bool connectedFrameMode: SettingsData.connectedFrameModeActive
width: parent ? parent.width : 400
height: 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
readonly property bool shadowsAllowed: Theme.elevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1"
readonly property var shadowElevation: Theme.elevationLevel1
@@ -100,6 +101,8 @@ Rectangle {
if (keyboardNavigationActive && expanded && selectedNotificationIndex >= 0) {
return Theme.primaryHoverLight;
}
if (connectedFrameMode)
return Theme.popupLayerColor(Theme.surfaceContainerHigh);
return Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency);
}
border.color: {
@@ -959,9 +962,9 @@ Rectangle {
Behavior on height {
enabled: root.__initialized && root.userInitiatedExpansion && root.animateExpansion
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.bezierCurve: Theme.expressiveCurves.emphasized
easing.bezierCurve: root.connectedFrameMode ? (root.expanded ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve) : Theme.expressiveCurves.emphasized
onRunningChanged: {
if (running) {
root.isAnimating = true;

View File

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

View File

@@ -10,13 +10,29 @@ import qs.Widgets
PanelWindow {
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 {
targetWindow: win
blurX: content.x + content.cardInset + swipeTx.x + tx.x
blurY: content.y + content.cardInset + swipeTx.y + tx.y
blurWidth: !win._finalized ? Math.max(0, content.width - content.cardInset * 2) : 0
blurHeight: !win._finalized ? Math.max(0, content.height - content.cardInset * 2) : 0
blurRadius: Theme.cornerRadius
blurWidth: !win._finalized && !win.connectedFrameMode ? Math.max(0, content.width - content.cardInset * 2) : 0
blurHeight: !win._finalized && !win.connectedFrameMode ? Math.max(0, content.height - content.cardInset * 2) : 0
blurRadius: win.connectedFrameMode ? Theme.connectedSurfaceRadius : Theme.cornerRadius
}
WlrLayershell.namespace: "dms:notification-popup"
@@ -32,6 +48,29 @@ PanelWindow {
property real _lastReportedAlignedHeight: -1
property real _storedTopMargin: 0
property real _storedBottomMargin: 0
readonly property bool directionalEffect: Theme.isDirectionalEffect
readonly property bool depthEffect: Theme.isDepthEffect
readonly property real entryTravel: {
const base = Math.abs(Theme.effectAnimOffset);
if (directionalEffect) {
if (isCenterPosition)
return Math.max(base, Math.round(content.height * 1.1));
return Math.max(base, Math.round(content.width * 0.95));
}
if (depthEffect)
return Math.max(base, 44);
return base;
}
readonly property real exitTravel: {
if (directionalEffect) {
if (isCenterPosition)
return content.height + entryTravel;
return content.width + entryTravel;
}
if (depthEffect)
return Math.round(entryTravel * 1.35);
return Anims.slidePx;
}
readonly property string clearText: I18n.tr("Dismiss")
property bool descriptionExpanded: false
readonly property bool hasExpandableBody: (notificationData?.htmlBody || "").replace(/<[^>]*>/g, "").trim().length > 0
@@ -61,6 +100,7 @@ PanelWindow {
signal exitStarted
signal exitFinished
signal popupHeightChanged
signal popupChromeGeometryChanged
function startExit() {
if (exiting || _isDestroying) {
@@ -68,6 +108,7 @@ PanelWindow {
}
exiting = true;
exitStarted();
popupChromeGeometryChanged();
exitAnim.restart();
exitWatchdog.restart();
if (NotificationService.removeFromVisibleNotifications)
@@ -145,9 +186,10 @@ PanelWindow {
enabled: !exiting && !_isDestroying
NumberAnimation {
id: implicitHeightAnim
duration: descriptionExpanded ? Theme.notificationExpandDuration : Theme.notificationCollapseDuration
duration: Theme.variantDuration(descriptionExpanded ? Theme.notificationExpandDuration : Theme.notificationCollapseDuration, descriptionExpanded)
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.emphasized
easing.bezierCurve: descriptionExpanded ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
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() {
const popupPos = SettingsData.notificationPopupPosition;
const isTop = isTopCenter || popupPos === SettingsData.Position.Top || popupPos === SettingsData.Position.Left;
if (!isTop)
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 base = barInfo.topBar > 0 ? barInfo.topBar : Theme.popupDistance;
return base + screenY;
@@ -257,6 +311,10 @@ PanelWindow {
if (!isBottom)
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 base = barInfo.bottomBar > 0 ? barInfo.bottomBar : Theme.popupDistance;
return base + screenY;
@@ -271,6 +329,8 @@ PanelWindow {
if (!isLeft)
return 0;
if (connectedFrameMode)
return _frameEdgeInset("left");
const barInfo = getBarInfo();
return barInfo.leftBar > 0 ? barInfo.leftBar : Theme.popupDistance;
}
@@ -284,6 +344,8 @@ PanelWindow {
if (!isRight)
return 0;
if (connectedFrameMode)
return _frameEdgeInset("right");
const barInfo = getBarInfo();
return barInfo.rightBar > 0 ? barInfo.rightBar : Theme.popupDistance;
}
@@ -328,10 +390,64 @@ PanelWindow {
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 real dpr: screenValid ? CompositorService.getScreenScale(win.screen) : 1
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)
onScreenYChanged: popupChromeGeometryChanged()
onScreenChanged: popupChromeGeometryChanged()
onConnectedFrameModeChanged: popupChromeGeometryChanged()
onAlignedWidthChanged: popupChromeGeometryChanged()
onAlignedHeightChanged: popupChromeGeometryChanged()
Item {
id: content
@@ -340,7 +456,7 @@ PanelWindow {
y: Theme.snap(windowShadowPad, dpr)
width: alignedWidth
height: alignedHeight
visible: !win._finalized
visible: !win._finalized && !chromeOnlyExit
scale: cardHoverHandler.hovered ? 1.01 : 1.0
transformOrigin: Item.Center
@@ -352,13 +468,25 @@ PanelWindow {
}
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 swipeTravelDistance: isCenterPosition ? height : width
readonly property real swipeTravelDistance: width
readonly property real swipeFadeStartOffset: swipeTravelDistance * swipeFadeStartRatio
readonly property real swipeFadeDistance: Math.max(1, swipeTravelDistance - swipeFadeStartOffset)
readonly property bool swipeActive: swipeDragHandler.active
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 var elevLevel: cardHoverHandler.hovered ? Theme.elevationLevel4 : Theme.elevationLevel3
@@ -399,7 +527,7 @@ PanelWindow {
shadowOffsetX: content.shadowOffsetX
shadowOffsetY: content.shadowOffsetY
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.textureMirroring: ShaderEffectSource.MirrorVertically
@@ -408,8 +536,12 @@ PanelWindow {
sourceRect.y: content.shadowRenderPadding + content.cardInset
sourceRect.width: Math.max(0, content.width - (content.cardInset * 2))
sourceRect.height: Math.max(0, content.height - (content.cardInset * 2))
sourceRect.radius: Theme.cornerRadius
sourceRect.color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
sourceRect.radius: win.connectedFrameMode ? Theme.connectedSurfaceRadius : Theme.cornerRadius
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.width: notificationData && notificationData.urgency === NotificationUrgency.Critical ? 2 : 0
@@ -447,10 +579,10 @@ PanelWindow {
Rectangle {
anchors.fill: parent
anchors.margins: content.cardInset
radius: Theme.cornerRadius
radius: win.connectedFrameMode ? Theme.connectedSurfaceRadius : Theme.cornerRadius
color: "transparent"
border.color: BlurService.borderColor
border.width: BlurService.borderWidth
border.color: win.connectedFrameMode ? "transparent" : BlurService.borderColor
border.width: win.connectedFrameMode ? 0 : BlurService.borderWidth
z: 100
}
@@ -850,14 +982,15 @@ PanelWindow {
DragHandler {
id: swipeDragHandler
target: null
xAxis.enabled: !isCenterPosition
yAxis.enabled: isCenterPosition
xAxis.enabled: true
yAxis.enabled: false
onActiveChanged: {
if (active || win.exiting || content.swipeDismissing)
return;
if (Math.abs(content.swipeOffset) > content.dismissThreshold) {
content.swipeDismissDirection = content.swipeOffset < 0 ? -1 : 1;
content.swipeDismissing = true;
swipeDismissAnim.start();
} else {
@@ -869,15 +1002,7 @@ PanelWindow {
if (win.exiting)
return;
const raw = isCenterPosition ? translation.y : 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);
}
content.swipeOffset = translation.x;
}
}
@@ -908,20 +1033,28 @@ PanelWindow {
id: swipeDismissAnim
target: content
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
easing.type: Easing.OutCubic
onStopped: {
NotificationService.dismissNotification(notificationData);
win.forceExit();
const inwardConnectedExit = win.connectedFrameMode && !win.isCenterPosition && !win._swipeDismissesTowardFrameEdge();
if (inwardConnectedExit)
content.chromeOnlyExit = true;
if (win.connectedFrameMode && (win.isCenterPosition || inwardConnectedExit)) {
win.startExit();
NotificationService.dismissNotification(notificationData);
} else {
NotificationService.dismissNotification(notificationData);
win.forceExit();
}
}
}
transform: [
Translate {
id: swipeTx
x: isCenterPosition ? 0 : content.swipeOffset
y: isCenterPosition ? content.swipeOffset : 0
x: content.swipeOffset
y: 0
},
Translate {
id: tx
@@ -929,9 +1062,17 @@ PanelWindow {
if (isCenterPosition)
return 0;
const isLeft = SettingsData.notificationPopupPosition === SettingsData.Position.Left || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom;
return isLeft ? -Anims.slidePx : Anims.slidePx;
return isLeft ? -entryTravel : entryTravel;
}
y: isTopCenter ? -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"
from: {
if (isTopCenter)
return -Anims.slidePx;
return -entryTravel;
if (isBottomCenter)
return Anims.slidePx;
return entryTravel;
const isLeft = SettingsData.notificationPopupPosition === SettingsData.Position.Left || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom;
return isLeft ? -Anims.slidePx : Anims.slidePx;
return isLeft ? -entryTravel : entryTravel;
}
to: 0
duration: Theme.notificationEnterDuration
duration: Theme.variantDuration(Theme.notificationEnterDuration, true)
easing.type: Easing.BezierSpline
easing.bezierCurve: isCenterPosition ? Theme.expressiveCurves.standardDecel : Theme.expressiveCurves.emphasizedDecel
easing.bezierCurve: Theme.variantPopoutEnterCurve
onStopped: {
if (!win.exiting && !win._isDestroying) {
if (isCenterPosition) {
@@ -977,35 +1118,35 @@ PanelWindow {
from: 0
to: {
if (isTopCenter)
return -Anims.slidePx;
return -exitTravel;
if (isBottomCenter)
return Anims.slidePx;
return exitTravel;
const isLeft = SettingsData.notificationPopupPosition === SettingsData.Position.Left || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom;
return isLeft ? -Anims.slidePx : Anims.slidePx;
return isLeft ? -exitTravel : exitTravel;
}
duration: Theme.notificationExitDuration
duration: Theme.variantDuration(Theme.notificationExitDuration, false)
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.emphasizedAccel
easing.bezierCurve: Theme.variantPopoutExitCurve
}
NumberAnimation {
target: content
property: "opacity"
from: 1
to: 0
duration: Theme.notificationExitDuration
to: Theme.isDirectionalEffect ? 1 : 0
duration: Theme.variantDuration(Theme.notificationExitDuration, false)
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.standardAccel
easing.bezierCurve: Theme.variantPopoutExitCurve
}
NumberAnimation {
target: content
property: "scale"
from: 1
to: 0.98
duration: Theme.notificationExitDuration
to: Theme.isDirectionalEffect ? 1 : Theme.effectScaleCollapsed
duration: Theme.variantDuration(Theme.notificationExitDuration, false)
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.emphasizedAccel
easing.bezierCurve: Theme.variantPopoutExitCurve
}
}

View File

@@ -8,23 +8,40 @@ QtObject {
property var modelData
property int topMargin: 0
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 popupIconSize: compactMode ? Theme.notificationIconSizeCompact : Theme.notificationIconSizeNormal
readonly property real actionButtonHeight: compactMode ? 20 : 24
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 int baseNotificationHeight: cardPadding * 2 + collapsedContentHeight + actionButtonHeight + contentSpacing + popupSpacing
property var popupWindows: []
property var destroyingWindows: new Set()
property var pendingDestroys: []
property int destroyDelayMs: 100
property bool _chromeSyncPending: false
property Component popupComponent
popupComponent: Component {
NotificationPopup {
onExitFinished: manager._onPopupExitFinished(this)
onPopupHeightChanged: manager._onPopupHeightChanged(this)
onPopupChromeGeometryChanged: manager._onPopupChromeGeometryChanged(this)
}
}
@@ -108,6 +125,14 @@ QtObject {
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() {
if (!SettingsData.notificationFocusedMonitor)
return true;
@@ -116,18 +141,24 @@ QtObject {
}
function _sync(newWrappers) {
let needsReposition = false;
for (const p of popupWindows.slice()) {
if (!_isValidWindow(p) || p.exiting)
continue;
if (p.notificationData && newWrappers.indexOf(p.notificationData) === -1) {
p.notificationData.removedByLimit = true;
p.notificationData.popup = false;
needsReposition = true;
}
}
for (const w of newWrappers) {
if (w && !_hasWindowFor(w) && _isFocusedScreen())
if (w && !_hasWindowFor(w) && _isFocusedScreen()) {
_insertAtTop(w);
needsReposition = false;
}
}
if (needsReposition)
_repositionAll();
}
function _popupHeight(p) {
@@ -157,7 +188,7 @@ QtObject {
}
function _repositionAll() {
const active = popupWindows.filter(p => _isValidWindow(p) && p.notificationData?.popup && !p.exiting);
const active = _layoutWindows();
const pinnedSlots = [];
for (const p of active) {
@@ -181,6 +212,168 @@ QtObject {
win.screenY = currentY;
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) {
@@ -227,8 +420,15 @@ QtObject {
}
popupWindows = [];
destroyingWindows.clear();
_chromeSyncPending = false;
_syncNotificationChromeState();
}
onNotificationConnectedModeChanged: _scheduleNotificationChromeSync()
onNotifBarSideChanged: _scheduleNotificationChromeSync()
onModelDataChanged: _scheduleNotificationChromeSync()
onTopMarginChanged: _repositionAll()
onPopupWindowsChanged: {
if (popupWindows.length > 0 && !sweeper.running) {
sweeper.start();

View File

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

View File

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

View File

@@ -0,0 +1,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)
}
}
}
}
}

View File

@@ -11,6 +11,7 @@ import qs.Modules.Settings.Widgets
Item {
id: themeColorsTab
readonly property bool connectedFrameModeActive: SettingsData.connectedFrameModeActive
property var cachedIconThemes: SettingsData.availableIconThemes
property var cachedCursorThemes: SettingsData.availableCursorThemes
property var cachedMatugenSchemes: Theme.availableMatugenSchemes.map(option => option.label)
@@ -1615,10 +1616,14 @@ Item {
SettingsSliderRow {
tab: "theme"
tags: ["popup", "transparency", "opacity", "modal"]
tags: ["surface", "popup", "transparency", "opacity", "modal"]
settingKey: "popupTransparency"
text: I18n.tr("Popup Transparency")
description: I18n.tr("Controls opacity of all popouts, modals, and their content layers")
text: I18n.tr("Surface Opacity")
description: themeColorsTab.connectedFrameModeActive
? I18n.tr("Connected Frame mode follows Surface Opacity from the Frame tab for connected popouts, docks, and modal surfaces")
: I18n.tr("Controls opacity of all popouts, modals, and their content layers")
enabled: !themeColorsTab.connectedFrameModeActive
opacity: themeColorsTab.connectedFrameModeActive ? 0.5 : 1.0
value: Math.round(SettingsData.popupTransparency * 100)
minimum: 0
maximum: 100
@@ -1632,7 +1637,9 @@ Item {
tags: ["corner", "radius", "rounded", "square"]
settingKey: "cornerRadius"
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
minimum: 0
maximum: 32
@@ -1837,7 +1844,11 @@ Item {
tags: ["blur", "background", "transparency", "glass", "frosted"]
settingKey: "blurEnabled"
text: I18n.tr("Background Blur")
description: BlurService.available ? I18n.tr("Blur the background behind bars, popouts, modals, and notifications. Requires compositor support and configuration.") : I18n.tr("Requires a newer version of Quickshell")
description: !BlurService.available
? I18n.tr("Requires a newer version of Quickshell")
: (themeColorsTab.connectedFrameModeActive
? I18n.tr("Connected Frame mode follows Frame Blur for connected surfaces while this remains the master blur availability toggle")
: I18n.tr("Blur the background behind bars, popouts, modals, and notifications. Requires compositor support and configuration."))
checked: SettingsData.blurEnabled ?? false
enabled: BlurService.available
onToggled: checked => SettingsData.set("blurEnabled", checked)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,12 +4,61 @@ pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Services.Mpris
import qs.Common
Singleton {
id: root
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 {
interval: 1000

View File

@@ -6,6 +6,7 @@ import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Services.Pipewire
import qs.Services
Singleton {
id: root
@@ -58,6 +59,10 @@ Singleton {
}
readonly property bool screensharingActive: {
if (CompositorService.isNiri && NiriService.hasActiveCast) {
return true
}
if (!Pipewire.ready || !Pipewire.nodes?.values) {
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") {
const mediaName = (node.properties["media.name"] || "").toLowerCase()
const appName = (node.properties["application.name"] || "").toLowerCase()
@@ -110,8 +121,9 @@ Singleton {
}
const appName = (node.properties && node.properties["application.name"] || "").toLowerCase()
const nodeName = (node.name || "").toLowerCase()
const combined = appName + " " + nodeName
return /xdg-desktop-portal|xdpw|screencast|screen|gnome shell|kwin|obs/.test(combined)
const mediaName = (node.properties && node.properties["media.name"] || "").toLowerCase()
const combined = appName + " " + nodeName + " " + mediaName
return /xdg-desktop-portal|xdpw|screencast|screen-cast|screen|gnome shell|kwin|obs|niri/.test(combined)
}
function getMicrophoneStatus() {

View File

@@ -231,7 +231,10 @@ Singleton {
return;
isChecking = true;
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);
} else {
updateChecker.command = [pkgManager].concat(packageManagerParams[pkgManager].listUpdatesSettings.params);

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
}
}
}
}

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
}
}
}
}

View File

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

View File

@@ -8,13 +8,122 @@ Item {
id: root
property MprisPlayer activePlayer
property real value: {
if (!activePlayer || activePlayer.length <= 0) return 0
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 seekPreviewRatio: -1
readonly property real playerValue: {
if (!activePlayer || activePlayer.length <= 0)
return 0;
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 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
@@ -29,58 +138,35 @@ Item {
M3WaveProgress {
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
MouseArea {
id: waveMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
enabled: activePlayer && activePlayer.canSeek && activePlayer.length > 0
property real pendingSeekPosition: -1
property real pressX: 0
Timer {
id: waveSeekDebounceTimer
interval: 150
id: waveHoldIndicatorTimer
interval: root.holdIndicatorDelay
repeat: false
onTriggered: {
if (parent.pendingSeekPosition >= 0 && activePlayer && activePlayer.canSeek && activePlayer.length > 0) {
const clamped = Math.min(parent.pendingSeekPosition, activePlayer.length * 0.99)
activePlayer.position = clamped
parent.pendingSeekPosition = -1
}
if (parent.pressed && root.isSeeking)
root.isDraggingSeek = true;
}
}
onPressed: (mouse) => {
root.isSeeking = true
if (activePlayer && activePlayer.length > 0 && activePlayer.canSeek) {
const r = Math.max(0, Math.min(1, mouse.x / parent.width))
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
}
}
onPressed: mouse => root.handleSeekPressed(mouse, parent.width, waveMouseArea, waveHoldIndicatorTimer)
onReleased: root.handleSeekReleased(waveMouseArea, waveHoldIndicatorTimer)
onPositionChanged: mouse => root.handleSeekPositionChanged(mouse, parent.width, waveMouseArea)
onCanceled: root.handleSeekCanceled(waveMouseArea, waveHoldIndicatorTimer)
}
}
}
@@ -93,6 +179,7 @@ Item {
property color trackColor: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.40)
property color fillColor: 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
Rectangle {
@@ -110,7 +197,22 @@ Item {
anchors.verticalCenter: parent.verticalCenter
color: parent.fillColor
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 {
@@ -122,59 +224,37 @@ Item {
x: Math.max(0, Math.min(parent.width, parent.width * root.value)) - width / 2
y: parent.midY - height / 2
z: 3
Behavior on x { NumberAnimation { duration: 80 } }
Behavior on x {
NumberAnimation {
duration: 80
}
}
}
MouseArea {
id: flatMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
enabled: activePlayer && activePlayer.canSeek && activePlayer.length > 0
property real pendingSeekPosition: -1
property real pressX: 0
Timer {
id: flatSeekDebounceTimer
interval: 150
id: flatHoldIndicatorTimer
interval: root.holdIndicatorDelay
repeat: false
onTriggered: {
if (parent.pendingSeekPosition >= 0 && activePlayer && activePlayer.canSeek && activePlayer.length > 0) {
const clamped = Math.min(parent.pendingSeekPosition, activePlayer.length * 0.99)
activePlayer.position = clamped
parent.pendingSeekPosition = -1
}
if (parent.pressed && root.isSeeking)
root.isDraggingSeek = true;
}
}
onPressed: (mouse) => {
root.isSeeking = true
if (activePlayer && activePlayer.length > 0 && activePlayer.canSeek) {
const r = Math.max(0, Math.min(1, mouse.x / parent.width))
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
}
}
onPressed: mouse => root.handleSeekPressed(mouse, parent.width, flatMouseArea, flatHoldIndicatorTimer)
onReleased: root.handleSeekReleased(flatMouseArea, flatHoldIndicatorTimer)
onPositionChanged: mouse => root.handleSeekPositionChanged(mouse, parent.width, flatMouseArea)
onCanceled: root.handleSeekCanceled(flatMouseArea, flatHoldIndicatorTimer)
}
}
}

View File

@@ -6,6 +6,8 @@ Item {
id: root
property real value: 0
property real actualValue: value
property bool showActualPlaybackState: false
property real lineWidth: 2
property real wavelength: 20
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 fillColor: 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)
function snap(v) {
@@ -22,7 +25,12 @@ Item {
}
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 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 {
NumberAnimation {
@@ -65,7 +73,9 @@ Item {
readonly property real startX: snap(root.lineWidth / 2)
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 {
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 {
id: startCap
width: snap(root.lineWidth)
@@ -107,7 +148,7 @@ Item {
radius: width / 2
color: root.fillColor
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
z: 2
}
@@ -119,10 +160,34 @@ Item {
radius: width / 2
color: root.fillColor
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
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 {
@@ -141,6 +206,10 @@ Item {
let r = a % m;
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)
FrameAnimation {
@@ -148,8 +217,9 @@ Item {
onTriggered: {
if (root.isPlaying)
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);
endCap.y = root.midY - endCap.height / 2 + root.currentAmp * Math.sin((waveClip.endX / root.wavelength) * 2 * Math.PI + root.phase);
startCap.y = waveY(waveClip.startX) - startCap.height / 2;
endCap.y = waveY(waveClip.endX) - endCap.height / 2;
actualEndCap.y = waveY(waveClip.gapEndX) - actualEndCap.height / 2;
}
}

View File

@@ -1,4 +1,5 @@
import QtQuick
import qs.Common
import qs.Services
Item {
@@ -8,6 +9,7 @@ Item {
required property var targetWindow
property var blurItem: null
property bool blurEnabled: Theme.connectedSurfaceBlurEnabled
property real blurX: 0
property real blurY: 0
property real blurWidth: 0
@@ -17,7 +19,7 @@ Item {
property var _region: null
function _apply() {
if (!BlurService.enabled || !targetWindow) {
if (!blurEnabled || !BlurService.enabled || !targetWindow) {
_cleanup();
return;
}
@@ -43,6 +45,8 @@ Item {
_region = null;
}
onBlurEnabledChanged: _apply()
Connections {
target: BlurService
function onEnabledChanged() {

View File

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