diff --git a/settings/quickshell.nix b/settings/quickshell.nix index 23a482a..6dab4c9 100644 --- a/settings/quickshell.nix +++ b/settings/quickshell.nix @@ -4983,49 +4983,54 @@ in // it in (see meltTop/meltBot). Outer max wins when a panel // is taller than the space (yMax < yMin). readonly property real yMin: Theme.frameWidth - readonly property real yMax: bar.height - Theme.frameWidth - height - // A visible toast owns its band, so the space a panel has - // to sit in ends short of it. Only the band the toast is - // actually on shrinks; the other one is still the frame. + readonly property real yMax: bar.height - Theme.frameWidth - baseH + + // Height with no toast in play. `height` adds the burial on + // top of this, so every position below is worked out from + // baseH — using `height` would make it depend on `fused`, + // which depends on the position. That is the loop. + readonly property real baseH: fullHeight + 24 + + // Where the panel sits with no toast: on its widget, + // clamped, then snapped to a band it very nearly reaches. + readonly property real freeY: Math.max(yMin, Math.min(yMax, dropdownY - baseH / 2)) + readonly property real snappedY: + freeY - yMin <= Theme.snapDist ? yMin + : yMax - freeY <= Theme.snapDist ? Math.max(yMin, yMax) + : freeY + + // Two rounded boxes stacked on one edge leave a waist: each + // contributes a corner that curves away over exactly one + // radius, so the outline pinches in and bulges back out at + // the joint. The curves only cancel when the boxes overlap + // by 2×radius — then the two corner circles coincide and + // the shared edge runs straight through. Anything less is + // the indent, no matter how the shader smins it. + readonly property real fuseDepth: 2 * Theme.radius + + // Burying that deep would drive the panel's cards into the + // notification's, so the panel grows by the same amount it + // buries. Content is pinned 12px from the panel's TOP (see + // dropdownContent.y), not centred, so the extra height all + // lands below the cards: the surface reaches further down + // and the clearance between the two sets of cards stays put + // at 12px. y then works out free of fuseDepth entirely. readonly property bool toastBlocks: toastItem.visible && toastItem.isPrimary - readonly property real toastMerge: Theme.radius / 2 - // Only fuse with a toast that will match this width. - readonly property bool canFuse: fullWidth >= toastItem.minFuseW - readonly property real lo: !toastBlocks || toastItem.atBottom ? yMin - : Math.max(yMin, canFuse - ? toastItem.y + toastItem.height - 16 - toastMerge - : toastItem.y + toastItem.height + Theme.panelGap) - // Panel surface spans [y + 8, y + height - 8]; the toast's - // starts at its y + 8. Landing the first ON the second (plus - // a half-radius of burial, the fillet scale the shader smins - // at) merges them into one outline with a shared waist, - // rather than parking two surfaces a hairline apart. The - // cards inside still clear each other by ~14px: 12 of panel - // padding, 8 of toast inset, less the burial. - readonly property real hi: !toastBlocks || !toastItem.atBottom ? yMax - : Math.min(yMax, canFuse - ? toastItem.y + 16 + toastMerge - height - : toastItem.y - Theme.panelGap - height) - // Where the panel lands when it just tracks its widget. - // hi < lo means the panel is taller than what the toast - // leaves — nothing to be done, so it takes the frame and - // the toast rides over it for its five seconds. - readonly property real yTracked: Math.max(lo, Math.min(hi, dropdownY - height / 2)) + // Last clause: a panel with no room above the toast + // can't clear it however it is placed, so it stops trying — + // it takes the frame and the toast rides over it for its + // five seconds rather than being shoved off screen. + readonly property bool fused: toastBlocks && toastItem.atBottom + && snappedY + baseH - 8 > toastItem.y + 8 + && toastItem.y + 16 - baseH >= yMin x: Theme.barWidth - // Snap to whichever band is within reach, and only then. - // Proximity rather than an alignTop/alignBottom flag per - // dropdown: a panel that gains a card later starts fusing - // with the frame on its own, and one that loses a card - // stops — no flag to remember to flip. A panel taller than - // the screen has yMax < yMin, so yTracked pins to yMin and - // the first branch catches it. - // Snapping still tests against the FRAME. A panel held off - // a band by a toast is already fused to the toast, which is - // itself fused to that band — one outline either way. - y: Math.round( - yTracked - yMin <= Theme.snapDist ? yMin - : yMax - yTracked <= Theme.snapDist ? Math.max(yMin, yMax) - : yTracked) + // Snapping (in snappedY) is proximity to the FRAME rather + // than an alignTop/alignBottom flag per dropdown: a panel + // that gains a card later starts fusing with the frame on + // its own, and one that loses a card stops. A panel taller + // than the screen has yMax < yMin, so freeY pins to yMin + // and the first branch catches it. + y: Math.round(fused ? toastItem.y + 16 - baseH : snappedY) // No Behavior here on purpose: _dropdownRect is positioned // off chrome.y, and chrome already eases to whatever this @@ -5037,7 +5042,7 @@ in // bottom, so +16 left the panel edge flush with the content // (0 padding) while the growth axis overhung by 4. The // extra 8 gives 4px above and below, matching the right. - height: fullHeight + 24 + height: baseH + (fused ? fuseDepth : 0) visible: false onVisibleChanged: { @@ -7633,27 +7638,10 @@ in readonly property var mutedApps: ["discord", "Discord", "Vesktop", "vesktop", "Spotify", "spotify", "vlc", "mpv"] readonly property bool isPrimary: bar.screen === Quickshell.screens[0] - // The toast and an open dropdown are both left-anchored to - // the bar, so any width difference between them is a step - // on the shared right edge — and smin fillets a step into - // an S-curve waist, which is the warp at the junction. - // Take the panel's drawn width (fullWidth + 4, what chrome - // renders) and the fused outline is a straight edge. - // - // Keyed off the dropdown's DECLARED fullWidth and nothing - // else. Reading its live y or height here would close a - // loop: a panel's y depends on this toast's height, which - // depends on its width, which would depend on that y. - // - // Below the floor there is no width worth matching — the - // power menu is a 64px icon strip — so those keep their - // own width and stand off instead (BarDropdown.canFuse). - readonly property real minFuseW: 200 - readonly property real fuseW: { - const d = bar.activeDropdown; - if (!d || !d.visible || d.fullWidth < minFuseW) return 320; - return d.fullWidth + 4; - } + // One width, always. The toast is the same card wherever + // it appears; a panel fusing with it is the side that + // gives (see BarDropdown.fuseDepth). + readonly property real cardW: 320 // Which frame band the toast lives against: the one the // clock sits nearest, so it lands on the same side as the @@ -7748,7 +7736,7 @@ in // Reveals rightward now, so width is the animated axis // and the card inside keeps a fixed width (otherwise it // would squash instead of being wiped in). - width: toastItem.toastOpen ? toastItem.fuseW : 0 + width: toastItem.toastOpen ? toastItem.cardW : 0 height: toastCard.height + 2 * Theme.panelGap clip: true @@ -7763,7 +7751,7 @@ in anchors.top: parent.top anchors.left: parent.left anchors.margins: Theme.panelGap - width: toastItem.fuseW - 2 * Theme.panelGap + width: toastItem.cardW - 2 * Theme.panelGap height: Math.max(toastPreview.visible ? 48 : 0, toastCol.height) + 16 radius: Theme.radiusCard color: Theme.cardBg