refactor: shared re-position mechanism

complement
This commit is contained in:
felixzsh
2026-08-19 21:12:23 -05:00
parent aa11f5f7a2
commit d586678d1d
+119 -25
View File
@@ -43,6 +43,18 @@ Item {
// Written by the panel D-pad buttons; reset when a dropdown preset is chosen. // Written by the panel D-pad buttons; reset when a dropdown preset is chosen.
property int offsetX: 0 property int offsetX: 0
property int offsetY: 0 property int offsetY: 0
// Adaptive vertical anchor: when the newest row's Y is in the upper half the
// group is top-anchored (history grows down, newest at top) so the newest
// chip stays at a stable Y; in the lower half it is bottom-anchored (grows
// up). offsetY is ALWAYS the absolute Y of the newest row's top edge, so the
// D-pad and the drag both mean the same thing and never diverge. The card's
// Y is derived from offsetY below, so crossing the half never jumps.
property bool isTopHalf: false
function updateIsTopHalf() {
if (!panel) return
var n = root.offsetY < panel.height / 2
if (n !== isTopHalf) isTopHalf = n
}
// Combo mode — "game mode". Toggle in the panel. Chords that contain a // Combo mode — "game mode". Toggle in the panel. Chords that contain a
// modifier (Super/Ctrl/Alt/Shift/Menu/AltGr) are COMBOs: they build a // modifier (Super/Ctrl/Alt/Shift/Menu/AltGr) are COMBOs: they build a
@@ -111,6 +123,16 @@ Item {
// Whether the SUPER+mouse binds are currently unbound by us (avoids redundant // Whether the SUPER+mouse binds are currently unbound by us (avoids redundant
// hyprctl eval calls on every hover transition). // hyprctl eval calls on every hover transition).
property bool dragArmed: false property bool dragArmed: false
// Debug overlay: shows the live position/dimensions/quadrant of the card
// next to it, while dragging and after release. Toggled via the plugin CLI.
property bool debugOverlay: false
Component.onCompleted: updateIsTopHalf()
// Latch the vertical anchor (top/bottom half) whenever the offset changes
// (drag or D-pad). Typing only changes card height/entries, not offsetY, so
// this does not fire while a combo is being held.
onOffsetYChanged: updateIsTopHalf()
// Pause flag shared with the bar widget: while the file holds "1" the // Pause flag shared with the bar widget: while the file holds "1" the
// display is frozen (keys are ignored). The bar button writes/removes the // display is frozen (keys are ignored). The bar button writes/removes the
@@ -187,13 +209,27 @@ Item {
return Math.max(card.width, (card.width + banner.width) / 2) return Math.max(card.width, (card.width + banner.width) / 2)
} }
// Returns the y of the topmost element of the visual group (banner for // Y of the card's top edge derived from offsetY (the newest row's top Y).
// top positions, card for bottom), clamped so the whole group stays on screen. // In the top half the newest is the first row (card top); in the bottom half
function groupY0() { // it is the last row (card bottom). This keeps the newest at offsetY in both
var gH = root.groupHeight() // halves regardless of how many entries are stacked.
var top = root.position.indexOf("top") !== -1 function cardTopY() {
var raw = top ? root.margin + root.offsetY : panel.height - gH - root.margin + root.offsetY var borderTop = card ? card.borderTop : 0
return root.clamp(raw, 0, panel.height - gH) var pad = root.cardPad
if (root.isTopHalf) return root.offsetY - borderTop - pad
return root.offsetY - borderTop - pad - (root.contentHeight() - root.chipHeight)
}
// Default newest-row Y for the current preset. offsetY==0 means "preset
// default" (the dropdown resets it), so we map it to where the preset anchors
// the group: top presets put the newest near the top edge, bottom presets
// near the bottom edge.
function defaultOffsetY() {
if (!panel) return 0
var borderTop = card ? card.borderTop : 0
var borderBottom = card ? card.borderBottom : 0
if (root.position.indexOf("top") !== -1) return root.margin + borderTop + root.cardPad
return panel.height - root.margin - borderBottom - root.cardPad - root.chipHeight
} }
// Combo-banner horizontal anchor, computed from the card's *target* position // Combo-banner horizontal anchor, computed from the card's *target* position
@@ -216,11 +252,10 @@ Item {
return "center" return "center"
} }
// History stacks downward (newest at the top) when the card sits in the top // History stacking direction derived from the adaptive anchor. Recomputed
// half of the screen, upward when it sits in the bottom half. Recomputed from // after drag release / offset changes so it follows the card.
// the live card position so it follows D-pad nudges.
function stackDown() { function stackDown() {
return card.y + card.height / 2 < panel.height / 2 return root.isTopHalf
} }
// Stack fade: the current combo is fully opaque and every older entry // Stack fade: the current combo is fully opaque and every older entry
@@ -611,7 +646,20 @@ Item {
if (isFinite(cfg.historyCount)) root.historyCount = Math.max(1, Math.min(5, Math.round(cfg.historyCount))) if (isFinite(cfg.historyCount)) root.historyCount = Math.max(1, Math.min(5, Math.round(cfg.historyCount)))
root.comboMode = cfg.comboMode === true root.comboMode = cfg.comboMode === true
if (isFinite(cfg.offsetX)) root.offsetX = Math.round(root.clamp(cfg.offsetX, -2000, 2000)) if (isFinite(cfg.offsetX)) root.offsetX = Math.round(root.clamp(cfg.offsetX, -2000, 2000))
if (isFinite(cfg.offsetY)) root.offsetY = Math.round(root.clamp(cfg.offsetY, -2000, 2000)) if (isFinite(cfg.offsetY)) {
var oy = Math.round(root.clamp(cfg.offsetY, -2000, 2000))
// offsetY==0 means "preset default" (the dropdown resets it), so map it
// to the preset's anchor and write the real value back so the bar panel
// and the config stay in sync.
if (oy === 0) {
var def = root.defaultOffsetY()
root.offsetY = def
root.persistConfig()
} else {
root.offsetY = oy
}
}
root.updateIsTopHalf()
} }
// Round-trips the current options to the shared config. Used by the SUPER+drag // Round-trips the current options to the shared config. Used by the SUPER+drag
@@ -784,6 +832,8 @@ Item {
function pause(): string { root.setPaused(true); return "ok" } function pause(): string { root.setPaused(true); return "ok" }
function resume(): string { root.setPaused(false); return "ok" } function resume(): string { root.setPaused(false); return "ok" }
function toggle(): string { root.setPaused(!root.paused); return "ok" } function toggle(): string { root.setPaused(!root.paused); return "ok" }
function debug(): string { root.debugOverlay = !root.debugOverlay; return root.debugOverlay ? "on" : "off" }
function debugState(): string { return root.debugOverlay ? "on" : "off" }
} }
// ------------------------------------------------------------- display // ------------------------------------------------------------- display
@@ -839,15 +889,16 @@ Item {
else base = Math.round((panel.width - gW) / 2) else base = Math.round((panel.width - gW) / 2)
return root.clamp(base + root.offsetX, lo, hi) + root.shakeX return root.clamp(base + root.offsetX, lo, hi) + root.shakeX
} }
// The group's top Y (banner for top positions, card for bottom) // The card's Y is derived so the newest row sits at offsetY (stable in
// follows the preset + offset and is clamped on screen; the card // both halves); the group (card + combo banner) is then clamped on screen.
// sits below the banner on top positions, at the group top on bottom.
y: { y: {
var p = root.position var cy = root.cardTopY()
var y0 = root.groupY0() var bannerH = root.bannerVisible() ? root.bannerHeight + root.bannerGap : 0
var y = y0 var groupTop = root.isTopHalf ? cy - bannerH : cy
if (root.bannerVisible() && p.indexOf("top") !== -1) y = y0 + root.bannerHeight + root.bannerGap var groupBottom = root.isTopHalf ? cy + card.height : cy + card.height + bannerH
return y + root.shakeY if (groupTop < 0) cy += -groupTop
else if (groupBottom > panel.height) cy -= (groupBottom - panel.height)
return cy + root.shakeY
} }
color: Util.alpha(Color.popups.background, 0.97) color: Util.alpha(Color.popups.background, 0.97)
borderSpec: Border.surfaceSpec("popups", "border", Color.popups.border, Math.max(1, Style.space(2))) borderSpec: Border.surfaceSpec("popups", "border", Color.popups.border, Math.max(1, Style.space(2)))
@@ -933,10 +984,54 @@ Item {
root.offsetX = Math.round(root.clamp(grabOffsetX + (p.x - grabX), -2000, 2000)) root.offsetX = Math.round(root.clamp(grabOffsetX + (p.x - grabX), -2000, 2000))
root.offsetY = Math.round(root.clamp(grabOffsetY + (p.y - grabY), -2000, 2000)) root.offsetY = Math.round(root.clamp(grabOffsetY + (p.y - grabY), -2000, 2000))
} }
onReleased: { dragging = false; root.persistConfig(); root.updateSuperDrag() } onReleased: { dragging = false; root.persistConfig(); root.updateIsTopHalf(); root.updateSuperDrag() }
onCanceled: { dragging = false; root.persistConfig(); root.updateSuperDrag() } onCanceled: { dragging = false; root.persistConfig(); root.updateIsTopHalf(); root.updateSuperDrag() }
} }
// Debug overlay: live readout of the card's position/dimensions and the
// movement state, shown next to the card while moving and after release.
// Toggle with: omarchy-shell key-visualizer debug
BorderSurface {
id: debugOverlay
visible: root.debugOverlay
width: debugFontMetrics.advanceWidth(debugText.text) + root.cardPad * 2
height: debugFontMetrics.height + root.cardPad * 2
x: card.x + card.width + Style.space(12)
y: card.y
z: 10
color: Util.alpha(Color.popups.background, 0.95)
borderSpec: Border.surfaceSpec("popups", "border", Color.popups.border, Math.max(1, Style.space(1)))
radius: Style.cornerRadius
Text {
id: debugText
anchors.fill: parent
anchors.margins: root.cardPad
verticalAlignment: Text.AlignVCenter
font: debugFont
color: Color.popups.text
text: {
var lb = "\n"
return "x=" + Math.round(card.x) + " y=" + Math.round(card.y)
+ lb + "w=" + Math.round(card.width) + " h=" + Math.round(card.height)
+ lb + "offX=" + root.offsetX + " offY=" + root.offsetY
+ lb + "half=" + (root.isTopHalf ? "top" : "bottom")
+ (root.debugDragging ? " DRAG" : "")
}
}
}
readonly property bool debugDragging: dragArea.dragging
FontMetrics {
id: debugFontMetrics
font: debugFont
}
readonly property var debugFont: Qt.font({
family: Style.font.family,
pixelSize: Style.font.bodySmall,
bold: false
})
// Combo mode banner — a separate visual stacked against the history // Combo mode banner — a separate visual stacked against the history
// card (below it for bottom positions, above it for top positions). // card (below it for bottom positions, above it for top positions).
// Shows the combo counter, multiplier and running score; hue and // Shows the combo counter, multiplier and running score; hue and
@@ -959,9 +1054,8 @@ Item {
return card.x + (card.width - width) / 2 return card.x + (card.width - width) / 2
} }
y: { y: {
var y0 = root.groupY0() if (root.isTopHalf) return card.y - height - root.bannerGap + root.shakeY
var by = root.position.indexOf("top") !== -1 ? y0 : y0 + card.height + root.bannerGap return card.y + card.height + root.bannerGap + root.shakeY
return by + root.shakeY
} }
color: Util.alpha(Color.popups.background, 0.97) color: Util.alpha(Color.popups.background, 0.97)
borderSpec: Border.surfaceSpec("popups", "border", root.comboBannerColor(), Math.max(1, Style.space(2))) borderSpec: Border.surfaceSpec("popups", "border", root.comboBannerColor(), Math.max(1, Style.space(2)))