quickshell: match toast width to the panel it fuses with

The warp was a width step. Panel and toast are both left-anchored to the
bar, so different widths put a step on the shared right edge, and smin
fillets a step into an S-curve waist.

Toast now takes the open dropdown's drawn width, keyed off its declared
fullWidth so nothing closes a loop through the toast's own height. Below
a 200px floor there is no width worth matching (the power menu is a 64px
icon strip), so those stand off with a gap as before.
This commit is contained in:
rope 2026-08-30 14:28:50 +01:00
parent 0ce584a261
commit 9eb728ca10

View file

@ -4989,8 +4989,12 @@ in
// actually on shrinks; the other one is still the frame. // actually on shrinks; the other one is still the frame.
readonly property bool toastBlocks: toastItem.visible && toastItem.isPrimary readonly property bool toastBlocks: toastItem.visible && toastItem.isPrimary
readonly property real toastMerge: Theme.radius / 2 readonly property real toastMerge: Theme.radius / 2
readonly property real lo: toastBlocks && !toastItem.atBottom // Only fuse with a toast that will match this width.
? Math.max(yMin, toastItem.y + toastItem.height - 16 - toastMerge) : yMin 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 // Panel surface spans [y + 8, y + height - 8]; the toast's
// starts at its y + 8. Landing the first ON the second (plus // starts at its y + 8. Landing the first ON the second (plus
// a half-radius of burial, the fillet scale the shader smins // a half-radius of burial, the fillet scale the shader smins
@ -4998,8 +5002,10 @@ in
// rather than parking two surfaces a hairline apart. The // rather than parking two surfaces a hairline apart. The
// cards inside still clear each other by ~14px: 12 of panel // cards inside still clear each other by ~14px: 12 of panel
// padding, 8 of toast inset, less the burial. // padding, 8 of toast inset, less the burial.
readonly property real hi: toastBlocks && toastItem.atBottom readonly property real hi: !toastBlocks || !toastItem.atBottom ? yMax
? Math.min(yMax, toastItem.y + 16 + toastMerge - height) : 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. // Where the panel lands when it just tracks its widget.
// hi < lo means the panel is taller than what the toast // hi < lo means the panel is taller than what the toast
// leaves nothing to be done, so it takes the frame and // leaves nothing to be done, so it takes the frame and
@ -7627,6 +7633,28 @@ 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
// 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;
}
// 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
// calendar popup the clock opens. Move the clock in the // calendar popup the clock opens. Move the clock in the
@ -7720,7 +7748,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 ? 320 : 0 width: toastItem.toastOpen ? toastItem.fuseW : 0
height: toastCard.height + 2 * Theme.panelGap height: toastCard.height + 2 * Theme.panelGap
clip: true clip: true
@ -7735,7 +7763,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: 320 - 2 * Theme.panelGap width: toastItem.fuseW - 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