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 radiusTiny: Math.round(radius * 0.5) // hover rows, action chips
readonly property int cardPad: 8 // card inner padding (inset = 2×) 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) 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) // Animation duration tokens (ms)
readonly property int animMorph: 280 // panel grow / slide (OutExpo) readonly property int animMorph: 280 // panel grow / slide (OutExpo)
@ -2266,10 +2271,9 @@ in
readonly property real meltTop: Theme.frameWidth - meltInto readonly property real meltTop: Theme.frameWidth - meltInto
readonly property real meltBot: bar.height - Theme.frameWidth + meltInto readonly property real meltBot: bar.height - Theme.frameWidth + meltInto
// Trigger distance is about proximity, not burial depth. // Trigger distance is about proximity, not burial depth.
readonly property real snapDist: Theme.panelGap + 4 readonly property real panelTop: rawTop <= Theme.frameWidth + Theme.snapDist
readonly property real panelTop: rawTop <= Theme.frameWidth + snapDist
? meltTop : rawTop ? 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 ? meltBot : rawBottom
// Left edge lands at (barWidth meltInto): the surface // Left edge lands at (barWidth meltInto): the surface
// buries a full corner radius inside the bar column. // buries a full corner radius inside the bar column.
@ -2285,9 +2289,9 @@ in
: Qt.vector4d(0, 0, 0, 0) : Qt.vector4d(0, 0, 0, 0)
// Toast melts into the band it sits against toastItem // Toast melts into the band it sits against toastItem
// picks the side, this just buries the matching edge. // 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 ? toastItem.y + 8 : meltTop
readonly property real toastBot: toastItem.atBottom readonly property real toastBot: toastItem.atBottom && !toastItem.floating
? meltBot : toastItem.y + 8 + _toastRect.height ? meltBot : toastItem.y + 8 + _toastRect.height
property vector4d toast: toastItem.visible && _toastRect.width > 0.5 property vector4d toast: toastItem.visible && _toastRect.width > 0.5
? Qt.vector4d(surfLeftX + _toastRect.width / 2, (toastTop + toastBot) / 2, ? Qt.vector4d(surfLeftX + _toastRect.width / 2, (toastTop + toastBot) / 2,
@ -4980,19 +4984,20 @@ in
// 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 - height
// A panel whose opener sits high on the bar looks adrift // Where the panel lands when it just tracks its widget.
// when it merely centres on it pin it to the frame, the readonly property real yTracked: Math.max(yMin, Math.min(yMax, dropdownY - height / 2))
// 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
x: Theme.barWidth x: Theme.barWidth
y: alignTop ? Math.round(yMin) // Snap to whichever band is within reach, and only then.
: alignBottom ? Math.round(Math.max(yMin, yMax)) // Proximity rather than an alignTop/alignBottom flag per
: Math.round(Math.max(yMin, Math.min(yMax, dropdownY - height / 2))) // 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 width: fullWidth + 4
// +24, not +16: the shader insets the panel 8px top and // +24, not +16: the shader insets the panel 8px top and
// bottom, so +16 left the panel edge flush with the content // bottom, so +16 left the panel edge flush with the content
@ -5359,7 +5364,6 @@ in
// no rebuild and no restart. // no rebuild and no restart.
BarDropdown { BarDropdown {
id: wallDropdown id: wallDropdown
alignTop: true
fullWidth: wallCard.width + 2 * Theme.panelGap fullWidth: wallCard.width + 2 * Theme.panelGap
fullHeight: wallCard.height + 2 * Theme.panelGap fullHeight: wallCard.height + 2 * Theme.panelGap
@ -6011,7 +6015,6 @@ in
id: batteryDropdown id: batteryDropdown
fullWidth: batteryDropdownCol.width + 2 * Theme.panelGap fullWidth: batteryDropdownCol.width + 2 * Theme.panelGap
fullHeight: batteryDropdownCol.height + 2 * Theme.panelGap fullHeight: batteryDropdownCol.height + 2 * Theme.panelGap
alignBottom: true
Column { Column {
id: batteryDropdownCol id: batteryDropdownCol
@ -7607,9 +7610,34 @@ in
// Flush to that band (it melts into it), never centred on // Flush to that band (it melts into it), never centred on
// the clock a short toast floating mid-column reads as a // the clock a short toast floating mid-column reads as a
// stray card instead of a surface growing out of the frame. // 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 ? 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 width: _toastRect.width + 4
height: _toastRect.height + 16 height: _toastRect.height + 16