quickshell: bury a full joint's depth, toast keeps one width

Toast is back to a fixed 320 — it is the same card wherever it appears
and should not restyle itself around whatever menu is open.

The residual indent was never the width step: two rounded boxes stacked
on one edge each contribute a corner that curves away over exactly one
radius, so the outline pinches and bulges at the joint. The curves only
cancel at 2*radius of overlap, where the corner circles coincide.

Burying that deep would drive the cards together, so a fused panel grows
by what it buries. Content is pinned 12px from the panel top, not
centred, so the extra height lands entirely below the cards and the
clearance stays 12px. Geometry now derives from baseH, keeping height
(which depends on fused) out of the position that decides it.
This commit is contained in:
rope 2026-08-30 14:42:46 +01:00
parent 9eb728ca10
commit 7caf7f2541

View file

@ -4983,49 +4983,54 @@ in
// it in (see meltTop/meltBot). Outer max wins when a panel // it in (see meltTop/meltBot). Outer max wins when a panel
// is taller than the space (yMax < yMin). // is taller than the space (yMax < yMin).
readonly property real yMin: Theme.frameWidth readonly property real yMin: Theme.frameWidth
readonly property real yMax: bar.height - Theme.frameWidth - height readonly property real yMax: bar.height - Theme.frameWidth - baseH
// 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 // Height with no toast in play. `height` adds the burial on
// actually on shrinks; the other one is still the frame. // 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 bool toastBlocks: toastItem.visible && toastItem.isPrimary
readonly property real toastMerge: Theme.radius / 2 // Last clause: a panel with no room above the toast
// Only fuse with a toast that will match this width. // can't clear it however it is placed, so it stops trying
readonly property bool canFuse: fullWidth >= toastItem.minFuseW // it takes the frame and the toast rides over it for its
readonly property real lo: !toastBlocks || toastItem.atBottom ? yMin // five seconds rather than being shoved off screen.
: Math.max(yMin, canFuse readonly property bool fused: toastBlocks && toastItem.atBottom
? toastItem.y + toastItem.height - 16 - toastMerge && snappedY + baseH - 8 > toastItem.y + 8
: toastItem.y + toastItem.height + Theme.panelGap) && toastItem.y + 16 - baseH >= yMin
// 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))
x: Theme.barWidth x: Theme.barWidth
// Snap to whichever band is within reach, and only then. // Snapping (in snappedY) is proximity to the FRAME rather
// Proximity rather than an alignTop/alignBottom flag per // than an alignTop/alignBottom flag per dropdown: a panel
// dropdown: a panel that gains a card later starts fusing // that gains a card later starts fusing with the frame on
// with the frame on its own, and one that loses a card // its own, and one that loses a card stops. A panel taller
// stops no flag to remember to flip. A panel taller than // than the screen has yMax < yMin, so freeY pins to yMin
// the screen has yMax < yMin, so yTracked pins to yMin and // and the first branch catches it.
// the first branch catches it. y: Math.round(fused ? toastItem.y + 16 - baseH : snappedY)
// 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)
// No Behavior here on purpose: _dropdownRect is positioned // No Behavior here on purpose: _dropdownRect is positioned
// off chrome.y, and chrome already eases to whatever this // 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 // bottom, so +16 left the panel edge flush with the content
// (0 padding) while the growth axis overhung by 4. The // (0 padding) while the growth axis overhung by 4. The
// extra 8 gives 4px above and below, matching the right. // extra 8 gives 4px above and below, matching the right.
height: fullHeight + 24 height: baseH + (fused ? fuseDepth : 0)
visible: false visible: false
onVisibleChanged: { onVisibleChanged: {
@ -7633,27 +7638,10 @@ in
readonly property var mutedApps: ["discord", "Discord", "Vesktop", "vesktop", "Spotify", "spotify", "vlc", "mpv"] readonly property var mutedApps: ["discord", "Discord", "Vesktop", "vesktop", "Spotify", "spotify", "vlc", "mpv"]
readonly property bool isPrimary: bar.screen === Quickshell.screens[0] readonly property bool isPrimary: bar.screen === Quickshell.screens[0]
// The toast and an open dropdown are both left-anchored to // One width, always. The toast is the same card wherever
// the bar, so any width difference between them is a step // it appears; a panel fusing with it is the side that
// on the shared right edge and smin fillets a step into // gives (see BarDropdown.fuseDepth).
// an S-curve waist, which is the warp at the junction. readonly property real cardW: 320
// 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;
}
// Which frame band the toast lives against: the one the // Which frame band the toast lives against: the one the
// clock sits nearest, so it lands on the same side as 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 // Reveals rightward now, so width is the animated axis
// and the card inside keeps a fixed width (otherwise it // and the card inside keeps a fixed width (otherwise it
// would squash instead of being wiped in). // 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 height: toastCard.height + 2 * Theme.panelGap
clip: true clip: true
@ -7763,7 +7751,7 @@ in
anchors.top: parent.top anchors.top: parent.top
anchors.left: parent.left anchors.left: parent.left
anchors.margins: Theme.panelGap 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 height: Math.max(toastPreview.visible ? 48 : 0, toastCol.height) + 16
radius: Theme.radiusCard radius: Theme.radiusCard
color: Theme.cardBg color: Theme.cardBg