quickshell: snap dropdowns to frame by proximity, toast dodges open menus

alignTop/alignBottom were per-dropdown flags, so the battery menu pinned
to the bottom wherever its icon sat. Both replaced by a distance test
against Theme.snapDist, which the SDF already used to decide melting —
panels now fuse with a band only when they actually reach it, and a menu
that gains or loses a card re-decides on its own.

Toast steps clear of an open dropdown instead of drawing over it, and
stops melting into the frame band once it has.
This commit is contained in:
rope 2026-08-30 13:41:39 +01:00
parent ba4c32d852
commit ddcc5c63be

View file

@ -625,6 +625,11 @@ in
readonly property int radiusTiny: Math.round(radius * 0.5) // hover rows, action chips
readonly property int cardPad: 8 // card inner padding (inset = 2×)
readonly property int panelGap: 8 // gap between panel border and cards (all dropdowns/menus/toast)
// How near a frame band a surface has to land before it fuses
// with it. Read by BarDropdown (where the panel is placed) and
// by the SDF (where it is melted) they must agree, or the
// content sits a few px off the surface it is drawn inside.
readonly property int snapDist: panelGap + 4
// Animation duration tokens (ms)
readonly property int animMorph: 280 // panel grow / slide (OutExpo)
@ -2266,10 +2271,9 @@ in
readonly property real meltTop: Theme.frameWidth - meltInto
readonly property real meltBot: bar.height - Theme.frameWidth + meltInto
// Trigger distance is about proximity, not burial depth.
readonly property real snapDist: Theme.panelGap + 4
readonly property real panelTop: rawTop <= Theme.frameWidth + snapDist
readonly property real panelTop: rawTop <= Theme.frameWidth + Theme.snapDist
? meltTop : rawTop
readonly property real panelBottom: rawBottom >= bar.height - Theme.frameWidth - snapDist
readonly property real panelBottom: rawBottom >= bar.height - Theme.frameWidth - Theme.snapDist
? meltBot : rawBottom
// Left edge lands at (barWidth meltInto): the surface
// buries a full corner radius inside the bar column.
@ -2285,9 +2289,9 @@ in
: Qt.vector4d(0, 0, 0, 0)
// Toast melts into the band it sits against toastItem
// picks the side, this just buries the matching edge.
readonly property real toastTop: toastItem.atBottom
readonly property real toastTop: toastItem.atBottom || toastItem.floating
? toastItem.y + 8 : meltTop
readonly property real toastBot: toastItem.atBottom
readonly property real toastBot: toastItem.atBottom && !toastItem.floating
? meltBot : toastItem.y + 8 + _toastRect.height
property vector4d toast: toastItem.visible && _toastRect.width > 0.5
? Qt.vector4d(surfLeftX + _toastRect.width / 2, (toastTop + toastBot) / 2,
@ -4980,19 +4984,20 @@ in
// is taller than the space (yMax < yMin).
readonly property real yMin: Theme.frameWidth
readonly property real yMax: bar.height - Theme.frameWidth - height
// A panel whose opener sits high on the bar looks adrift
// when it merely centres on it pin it to the frame, the
// way the bottom cluster's menus already land on yMax.
property bool alignTop: false
// Mirror for the bottom cluster: a tall panel opened from a
// low widget reads better fused to the bottom frame than
// floating a few px off it. max(yMin, ...) keeps a panel
// taller than the screen pinned to the top instead.
property bool alignBottom: false
// Where the panel lands when it just tracks its widget.
readonly property real yTracked: Math.max(yMin, Math.min(yMax, dropdownY - height / 2))
x: Theme.barWidth
y: alignTop ? Math.round(yMin)
: alignBottom ? Math.round(Math.max(yMin, yMax))
: Math.round(Math.max(yMin, Math.min(yMax, dropdownY - height / 2)))
// 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.
y: Math.round(
yTracked - yMin <= Theme.snapDist ? yMin
: yMax - yTracked <= Theme.snapDist ? Math.max(yMin, yMax)
: yTracked)
width: fullWidth + 4
// +24, not +16: the shader insets the panel 8px top and
// bottom, so +16 left the panel edge flush with the content
@ -5359,7 +5364,6 @@ in
// no rebuild and no restart.
BarDropdown {
id: wallDropdown
alignTop: true
fullWidth: wallCard.width + 2 * Theme.panelGap
fullHeight: wallCard.height + 2 * Theme.panelGap
@ -6011,7 +6015,6 @@ in
id: batteryDropdown
fullWidth: batteryDropdownCol.width + 2 * Theme.panelGap
fullHeight: batteryDropdownCol.height + 2 * Theme.panelGap
alignBottom: true
Column {
id: batteryDropdownCol
@ -7607,9 +7610,34 @@ in
// Flush to that band (it melts into it), never centred on
// the clock a short toast floating mid-column reads as a
// stray card instead of a surface growing out of the frame.
y: Math.round(atBottom
readonly property real baseY: atBottom
? bar.height - Theme.frameWidth - height - 8
: Theme.frameWidth + 8)
: Theme.frameWidth + 8
// Dropdowns share this column, so a toast arriving under an
// open one lands on top of it. Step away from the band far
// enough to clear the panel, capped by the room left before
// the opposite frame a full-height panel pushes the toast
// as far as it goes and no further, rather than off screen.
readonly property real dodge: {
const d = bar.activeDropdown;
if (!d || !d.visible) return 0;
const want = atBottom
? (d.y + d.height + Theme.panelGap) - baseY
: (baseY + height + Theme.panelGap) - d.y;
const room = atBottom
? baseY - Theme.frameWidth - 8
: bar.height - Theme.frameWidth - 8 - height - baseY;
return Math.max(0, Math.min(want, room));
}
// Once it has stepped away it is no longer against the
// band, so the shader draws it as a free-standing card.
readonly property bool floating: dodge > 0
y: Math.round(atBottom ? baseY - dodge : baseY + dodge)
// Opening a dropdown under a live toast moves it; slide.
Behavior on y {
NumberAnimation { duration: Theme.animContent; easing.type: Easing.OutCubic }
}
width: _toastRect.width + 4
height: _toastRect.height + 16