rail: the pose rides the drag, not each exit path
A partial slide left the app scaled small until another pull brought it back (DUMP-bugs 8). Every path anyone checked did call endPull, so the hunt for the one that didn't was the wrong shape: while restoring is a step, some path skips it. It is now a function of `dragging`. Release gates on speed and length as one projected number instead of a distance test with a flick promotion bolted on. That promotion had never fired anyway — it read rail.tapSlop, which is undefined, so the test was `travel >= undefined`. Every commit on this rail has been distance-only.
This commit is contained in:
parent
14c3c80b14
commit
57208c886a
1 changed files with 107 additions and 38 deletions
|
|
@ -158,12 +158,74 @@ PanelWindow {
|
|||
// reach rather than a near-miss of Mission Control.
|
||||
readonly property int swapAt: 300
|
||||
|
||||
// Speed and length become one number: how far the thumb would have carried
|
||||
// had it kept going. 130 ms is where the old rule sat — "1.1 px/ms promotes
|
||||
// one stage" over a 146 px stage gap — so the feel is the same minus the
|
||||
// cliff between promoted and not.
|
||||
readonly property int projectMs: 130
|
||||
|
||||
// Live upward travel of the in-flight drag (0 while idle, grows as the
|
||||
// finger climbs). Drives the handle's appearance so the pill tracks the
|
||||
// motion. Only ever set by the MouseArea below.
|
||||
property real dragTravel: 0
|
||||
property bool dragging: false
|
||||
|
||||
// The pose is a function of `dragging`, not a thing each exit path
|
||||
// remembers to undo. DUMP-bugs 8: a partial slide left the app scaled small
|
||||
// and only another pull brought it back, and the cause was never found
|
||||
// because every path that was *looked at* did call `endPull` — release,
|
||||
// cancel and the swap all did. Something ended a drag without going through
|
||||
// any of them, and searching for which one is the wrong shape of fix: as
|
||||
// long as restoring is a step, there is a path that skips the step.
|
||||
//
|
||||
// So there is one edge and everything rides on it. A drag ends by setting
|
||||
// `dragging` false and by nothing else; this restores. `endPull` is
|
||||
// idempotent, so a path that also calls it directly costs a redundant write
|
||||
// and cannot strand. Ordering is preserved by the release handler setting
|
||||
// `dragging` false *last*, after it has committed — clearing the pose first
|
||||
// would pop the app back to full size for a frame before Mission Control
|
||||
// took the screen.
|
||||
onDraggingChanged: if (!rail.dragging) gestureArea.endPull()
|
||||
|
||||
// The thumb has stopped climbing without letting go. iOS/Android both read
|
||||
// that as "show me where I'd land" and slide the switcher in before the
|
||||
// release; distance still decides what commits, this only draws it.
|
||||
//
|
||||
// 140 ms: past a 60 Hz stutter, under the 260 ms settle, so the preview
|
||||
// leads the commit instead of racing it. Latching — once dwelled it stays
|
||||
// for the rest of the drag, or a thumb that trembles would strobe the blur.
|
||||
property bool dwelled: false
|
||||
Timer {
|
||||
id: dwellTimer
|
||||
interval: 140
|
||||
onTriggered: rail.dwelled = true
|
||||
}
|
||||
|
||||
// Only in multitasking range. Climb on toward home and the cards recede, so
|
||||
// the preview never shows a destination the release would not commit.
|
||||
readonly property bool peekWanted: rail.dragging && rail.dwelled && rail.dragStage === 1
|
||||
|
||||
onPeekWantedChanged: {
|
||||
if (rail.peekWanted) {
|
||||
peekFade.stop()
|
||||
GlobalStates.missionPeek = true
|
||||
Haptics.tick()
|
||||
} else if (GlobalStates.missionPeek) {
|
||||
// `endPull` zeroes the progress the cards are laid out against, and
|
||||
// that settle is animated. Dropping the surface on the release frame
|
||||
// would pop them off mid-animation, so it outlives the gesture just
|
||||
// long enough to shrink away. On a commit `missionControlOpen` has
|
||||
// already taken over and this expires unnoticed.
|
||||
peekFade.restart()
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: peekFade
|
||||
interval: 300
|
||||
onTriggered: GlobalStates.missionPeek = false
|
||||
}
|
||||
|
||||
// Stage the current travel has reached: 0 none, 1 reveal, 2 mission.
|
||||
// With a keyboard up the navigation stages do not exist — the climb means
|
||||
// only the swap, and an app mid-sentence has no business shrinking under
|
||||
|
|
@ -256,9 +318,6 @@ PanelWindow {
|
|||
// reaching Mission Control — feeds the discovery nudge.
|
||||
property int shortSwipes: 0
|
||||
readonly property int tapSlop: 24
|
||||
// Fast throws commit the stage above their travel: a short-but-quick
|
||||
// flick past this speed (px/ms) escalates one stage.
|
||||
readonly property real flickSpeed: 1.1
|
||||
|
||||
// Double-tap used to toggle fullscreen through `hyprctl dispatch`, and
|
||||
// has therefore done nothing since the viewtop move — START-HERE §3's
|
||||
|
|
@ -349,6 +408,8 @@ PanelWindow {
|
|||
startAt = Date.now()
|
||||
rail.dragging = true
|
||||
rail.dragTravel = 0
|
||||
rail.dwelled = false
|
||||
dwellTimer.stop()
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -359,6 +420,11 @@ PanelWindow {
|
|||
// the handle doesn't chase a dismiss gesture.
|
||||
rail.dragTravel = Math.max(0, startY - mouse.y)
|
||||
|
||||
// Restart while still moving: the timer only reaches its interval
|
||||
// once the thumb holds still.
|
||||
if (!rail.dwelled)
|
||||
dwellTimer.restart()
|
||||
|
||||
// The app shrinks under the thumb, and it is the *real* app.
|
||||
// Casey, 2026-08-05: *"swipe up just a bit makes the app sorta
|
||||
// scale and you fall into a multi tasking area"*, and on the
|
||||
|
|
@ -398,31 +464,37 @@ PanelWindow {
|
|||
}
|
||||
}
|
||||
|
||||
// End an upward pull, one way or the other.
|
||||
// End an upward pull. Called from one place — `rail.onDraggingChanged`
|
||||
// — because every destination wants the same thing and the difference
|
||||
// was never in the action.
|
||||
//
|
||||
// `committed` means the multitasking view is taking over: it draws the
|
||||
// zone the pose was shrinking, so leaving the pose on would put two
|
||||
// shrunken pictures of the same app on the glass — the card and the
|
||||
// window behind it. Not committed means the gesture was abandoned and
|
||||
// the windows have to spring back, or a half-swipe leaves a permanently
|
||||
// smaller app with nothing on screen to explain it.
|
||||
// It used to take a `committed` flag it did not read. Committed means
|
||||
// the multitasking view is taking over and draws the zone the pose was
|
||||
// shrinking, so a pose left on puts two shrunken pictures of one app on
|
||||
// the glass. Abandoned means the windows have to spring back or a
|
||||
// half-swipe leaves a permanently smaller app with nothing on screen to
|
||||
// explain it. Home is the same again: `goHome` moves the strip, and a
|
||||
// window left at 0.6 on the zone behind you is one you find shrunken
|
||||
// next time you swipe back to it. Three reasons, one action — so the
|
||||
// parameter only ever documented a distinction the body did not make.
|
||||
//
|
||||
// Either way the pose is cleared, and the *reason* differs rather than
|
||||
// the action. Home takes the same path: `goHome` moves the strip, and a
|
||||
// window left at 0.6 on the zone behind you is one you would find
|
||||
// shrunken next time you swiped back to it.
|
||||
function endPull(committed: bool): void {
|
||||
// Idempotent on purpose: it is the safety net, and a net that cannot be
|
||||
// touched twice is not one.
|
||||
function endPull(): void {
|
||||
dwellTimer.stop()
|
||||
rail.dwelled = false
|
||||
GlobalStates.zonePullProgress = 0
|
||||
ViewtopControl.clearPose()
|
||||
}
|
||||
|
||||
onReleased: mouse => {
|
||||
const wasDragging = rail.dragging
|
||||
rail.dragging = false
|
||||
const travel = Math.max(0, startY - mouse.y)
|
||||
const deltaY = mouse.y - startY
|
||||
const elapsed = Math.max(1, Date.now() - startAt)
|
||||
const speed = travel / elapsed // px/ms, upward only
|
||||
// The handle lights by raw travel — where the thumb is — and the
|
||||
// commit reads the projection — where it was going.
|
||||
const projected = travel + speed * rail.projectMs
|
||||
rail.dragTravel = 0
|
||||
|
||||
// TASK-38: past the swap detent with a keyboard up, this gesture IS
|
||||
|
|
@ -443,7 +515,7 @@ PanelWindow {
|
|||
// the new exclusive zone lands, so the height nobody measures
|
||||
// is the height that cannot be wrong.
|
||||
Gestures.deliver("osk-swap", Gestures.oskSwapAction)
|
||||
endPull(false)
|
||||
rail.dragging = false
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -457,23 +529,23 @@ PanelWindow {
|
|||
// flipped the app into multitasking was the keyboard-swap complaint
|
||||
// as it actually presented (observed 2026-08-06) — the swap path
|
||||
// above is the *only* thing a keyboard-up climb can commit.
|
||||
if (!GlobalStates.oskOpen
|
||||
&& (travel >= rail.revealAt || (speed >= flickSpeed && travel >= rail.tapSlop))) {
|
||||
let stage = travel >= rail.missionAt ? 2 : travel >= rail.revealAt ? 1 : 0
|
||||
if (speed >= flickSpeed && stage < 2)
|
||||
stage += 1
|
||||
commitUp(stage)
|
||||
endPull(true)
|
||||
// `travel` floors it: projection alone would turn a 10 px smudge
|
||||
// flicked in 5 ms into a 270 px throw, i.e. Home. Bare `tapSlop` —
|
||||
// it is the MouseArea's. The old flick rule read `rail.tapSlop`,
|
||||
// which is undefined, so `travel >= undefined` was always false and
|
||||
// that whole promotion path had never once fired.
|
||||
if (!GlobalStates.oskOpen && travel >= tapSlop
|
||||
&& projected >= rail.revealAt) {
|
||||
commitUp(projected >= rail.missionAt ? 2 : 1)
|
||||
rail.dragging = false
|
||||
return
|
||||
}
|
||||
|
||||
// An abandoned climb: the app was shrinking under the thumb and
|
||||
// has to spring back. Only ever after a drag *this rail started* —
|
||||
// geometry is the agent's outright (doctrine §13), and a rail that
|
||||
// reset poses as a general safety net would be the shell overruling
|
||||
// what she had composed because a finger moved.
|
||||
if (wasDragging)
|
||||
endPull(false)
|
||||
// Short of the first detent the destination is the app you were
|
||||
// already in — a commit like the others, not a pending state.
|
||||
// Restoring rides the `dragging` edge, so it happens whether or not
|
||||
// this handler is the thing that ends the drag.
|
||||
rail.dragging = false
|
||||
|
||||
// Downward: peel the nearest surface (keyboard, then dock).
|
||||
if (deltaY >= rail.revealAt) {
|
||||
|
|
@ -519,14 +591,11 @@ PanelWindow {
|
|||
}
|
||||
}
|
||||
|
||||
// The compositor taking the sequence away mid-drag — the FTS controller
|
||||
// does this after a wake. Same edge, same restore.
|
||||
onCanceled: {
|
||||
rail.dragging = false
|
||||
rail.dragTravel = 0
|
||||
// A cancel is the compositor taking the sequence away mid-drag —
|
||||
// the FTS controller does exactly this after a wake. The windows
|
||||
// are really scaled at this point, so something has to bring them
|
||||
// home; nothing else will.
|
||||
endPull(false)
|
||||
rail.dragging = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue