Watch
1
0
Fork
You've already forked souveraine
0

zone: take the detent from the card, not from 18%

The card's bottom sits at 805 of 1080 logical, so the travel is 275px and the
detent fired at 194 — every release committed with the window short of its card.
Latched at the press. Velocity is the last 100ms rather than the whole-drag mean,
and the settle is a critically damped spring on the frame clock, seeded with it.
This commit is contained in:
Fimeg 2026-08-16 10:31:18 -04:00
commit 46d2601908
2 changed files with 332 additions and 99 deletions

View file

@ -114,23 +114,22 @@ PanelWindow {
// panel arms **home**. The dock is no longer a stage at all: it lives on
// zone one permanently, so there is nothing left to reveal.
readonly property int revealAt: 48
// A share of the panel rather than a pixel count, because "beyond the
// bottom 15-20%" is a proportion of the glass and this rail has to mean the
// same thing on the phone and on a laptop panel. 0.18 sits in the middle of
// what Casey named. The floor keeps it reachable if a tiny output ever
// makes the proportion smaller than the first detent.
// Where the multitasking detent sits: the distance to the destination, asked
// of the destination.
//
// `screen.height`, not `QsWindow.window.height`. The window here is the
// rail itself a 32 px strip along the bottom so asking it for a height
// and taking 18% of it yielded 5.76, the floor won every time, and the
// "long swipe" detent sat at 108 px: a tenth of the panel, sixty pixels
// above the short one. Two destinations that far apart are one destination
// with a coin toss in front of it. The fallback is in *logical* pixels for
// the same reason this panel is 1080 logical tall and 2160 physical, and
// `mouse.y` arrives in the first of those. `.screen.height` is the tree's
// idiom for this (`SelectionHost`, `FullscreenPolkitWindow`).
// This was `screen.height * 0.18` a share of the panel with no
// relationship to where the card actually is, which TASK-60 named as a
// defect and left standing. Measured on the phone 2026-08-16 the card's
// bottom edge sits at 805 logical px, so the window's real travel is 275 px
// and the gesture was committing at 194: the release fired while the window
// was still 40% short of being its card, and the rest of the distance was
// covered by the hand-off. That is the seam Casey has reported six times.
//
// `ZoneTransition` owns it because it owns the card rect, and it is measured
// rather than assumed so it re-derives itself the day the dock reserves.
// Everything downstream is still logical pixels, which is what `mouse.y` is.
readonly property int missionAt: Math.max(revealAt + 60,
Math.round((rail.screen?.height ?? 1080) * 0.18))
Math.round(ZoneTransition.dragLength))
// 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
@ -143,6 +142,19 @@ PanelWindow {
property real dragTravel: 0
property bool dragging: false
// The detent this gesture is running against, taken once at the press.
//
// quickstep names the destination and the distance to it together and once
// `initTransitionEndpoints` sets `mTransitionDragLength` at the start of
// the gesture, not per motion event. Here it also closes a real seam: the
// overview surface reports its geometry from inside a gated Loader, so on
// the first climb of a session `missionAt` changes underneath the drag the
// moment the peek realises the surface. Latched, the clock a gesture starts
// on is the clock it finishes on.
property int gestureDetent: 0
readonly property int detentNow: rail.dragging && rail.gestureDetent > 0
? rail.gestureDetent : rail.missionAt
// One edge, and everything rides on it. A drag ends by setting `dragging`
// false and by nothing else. DUMP-bugs 8: a partial slide left the app
// scaled small and only another pull brought it back, and the cause was
@ -255,7 +267,7 @@ PanelWindow {
// With a keyboard up the navigation stages do not exist; an app
// mid-sentence has no business shrinking under the thumb.
readonly property int dragStage: GlobalStates.oskOpen ? 0
: dragTravel >= missionAt ? 2
: dragTravel >= detentNow ? 2
: dragTravel >= revealAt ? 1 : 0
Rectangle {
@ -320,6 +332,46 @@ PanelWindow {
property int shortSwipes: 0
readonly property int tapSlop: 24
// --- Release velocity ------------------------------------------------
//
// The commit used to read `travel / (now - startAt)` the *mean* speed
// across the whole gesture. A slow climb that ends in a flick therefore
// read as slow, which is the one gesture a projection exists to catch,
// and a long slow drag could never be thrown at all.
//
// quickstep is handed a measured end velocity instead:
// `AbsSwipeUpHandler.onGestureEnded(float endVelocityPxPerMs, PointF)`,
// filled by a VelocityTracker, and its fling test is
// `isFling = mGestureStarted && !mIsMotionPaused
// && |endVelocityPxPerMs| > flingThreshold`.
//
// A ring over the tail of the motion is the same measurement. 100 ms is
// ~6 frames at 60 Hz: long enough to fit a line through, short enough
// that only the end of the gesture is in it.
readonly property int velocityWindowMs: 100
property var samples: []
function sample(y: real): void {
const now = Date.now()
samples.push({ t: now, y: y })
// Always keep two, or a flick that lands entirely inside one frame
// has nothing to measure between.
while (samples.length > 2 && now - samples[0].t > velocityWindowMs)
samples.shift()
}
// px/ms, upward positive.
function endVelocity(): real {
if (samples.length < 2)
return 0
const first = samples[0]
const last = samples[samples.length - 1]
const dt = last.t - first.t
if (dt <= 0)
return 0
return (first.y - last.y) / dt
}
// Double-tap used to toggle fullscreen through `hyprctl dispatch`, and
// has therefore done nothing since the viewtop move START-HERE §3's
// exact shape, and the rule there is that the fix is never to re-add
@ -350,8 +402,8 @@ PanelWindow {
// nowhere and Home silently stopped existing with `homeZone` rather
// than a config lookup that resolved to 1 and put Home on the first
// *app* zone.
function goHome(): void {
ZoneTransition.settleTo("home")
function goHome(velocity: real): void {
ZoneTransition.settleTo("home", velocity ?? 0)
}
// Commit an upward gesture. `stage` is the stage the drag settled on
@ -373,12 +425,12 @@ PanelWindow {
// Every branch here is now a named end target, and each one releases the
// carry by *arriving* which is what makes a stranded window
// impossible rather than merely unlikely (TASK-60).
function commitUp(stage: int): void {
function commitUp(stage: int, velocity: real): void {
if (stage >= 2) {
// Square one. The arrival clears multitasking, the overview,
// the dock and the OSK before moving, so the zone is not
// arrived at with the last screen's furniture still up.
goHome()
goHome(velocity)
shortSwipes = 0
return
}
@ -393,10 +445,10 @@ PanelWindow {
// machinery as every other exit rather than being the one path
// that only unsets a flag.
if (GlobalStates.missionControlOpen) {
ZoneTransition.settleTo("last_zone")
ZoneTransition.settleTo("last_zone", velocity)
GlobalStates.missionControlOpen = false
} else {
ZoneTransition.settleTo("overview")
ZoneTransition.settleTo("overview", velocity)
if (!Persistent.states.navigation.missionControlDiscovered)
Persistent.states.navigation.missionControlDiscovered = true
}
@ -407,6 +459,8 @@ PanelWindow {
onPressed: mouse => {
startY = mouse.y
startAt = Date.now()
samples = [{ t: startAt, y: mouse.y }]
rail.gestureDetent = rail.missionAt
rail.dragging = true
rail.dragTravel = 0
rail.dwelled = false
@ -420,6 +474,7 @@ PanelWindow {
// Upward travel is positive; downward drags leave travel at 0 so
// the handle doesn't chase a dismiss gesture.
rail.dragTravel = Math.max(0, startY - mouse.y)
sample(mouse.y)
// Restart while still moving: the timer only reaches its interval
// once the thumb holds still.
@ -478,7 +533,7 @@ PanelWindow {
ZoneTransition.begin()
// The clock, and nothing derived from it. `pullTo` turns this
// into the carry's shift and the destination's presence.
ZoneTransition.pullTo(rail.dragTravel, rail.missionAt)
ZoneTransition.pullTo(rail.dragTravel, rail.detentNow)
}
}
@ -514,10 +569,10 @@ PanelWindow {
}
onReleased: mouse => {
sample(mouse.y)
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
const speed = Math.max(0, endVelocity()) // 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
@ -537,7 +592,7 @@ PanelWindow {
// that whole promotion path had never once fired.
if (!GlobalStates.oskOpen && travel >= tapSlop
&& projected >= rail.revealAt) {
commitUp(projected >= rail.missionAt ? 2 : 1)
commitUp(projected >= rail.detentNow ? 2 : 1, speed)
rail.dragging = false
return
}
@ -554,7 +609,7 @@ PanelWindow {
// or a swipe down never began one, and `settleTo` on an idle
// transition would animate a clock nothing is reading.
if (!GlobalStates.oskOpen && ZoneTransition.inFlight)
ZoneTransition.settleTo("last_zone")
ZoneTransition.settleTo("last_zone", speed)
rail.dragging = false
// Downward: peel the nearest surface (keyboard, then dock).

View file

@ -98,6 +98,9 @@ Singleton {
property real surfaceHeight: root.panelHeight
property real surfaceTop: 0
property real surfaceLeft: 0
// Whether any of the four above came from the surface rather than from the
// defaults. `dragLength` is the one consumer that must know the difference.
property bool surfaceReported: false
// Report the overview surface's real geometry. Called by the surface; the
// only writer of these four.
@ -106,6 +109,7 @@ Singleton {
root.surfaceTop = top;
root.surfaceWidth = Math.max(1, width);
root.surfaceHeight = Math.max(1, height);
root.surfaceReported = true;
}
// `ZoneOverview`'s own frame, restated here because the destination and the
@ -117,31 +121,115 @@ Singleton {
// derived from a share, for the reason above.
readonly property real listMargin: 12
readonly property real labelHeight: 34
// The band under the strip: verbs that act on the card in front, and the
// line she speaks on. Reserved here rather than overlaid, so the cards
// shrink to make room and `dragLength` re-derives itself the tray moves
// the destination, and the gesture has to know that.
readonly property real trayHeight: 132
// The box a card is laid out inside, in surface-local coordinates.
readonly property real _availX: root.listMargin
readonly property real _availY: root.listMargin
readonly property real _availW: root.surfaceWidth - 2 * root.listMargin
readonly property real _availH: root.surfaceHeight
- 2 * root.listMargin - root.labelHeight
- 2 * root.listMargin - root.labelHeight - root.trayHeight
// One factor. `min` so the card keeps the *zone's* aspect the zone being
// the whole output, which is what a window's reported rect is measured
// against and the carried window fills it exactly. See the header on why
// a uniform scale is not a choice here but a consequence of how `carried`
// works.
readonly property real cardScale: Math.max(0.05, Math.min(
root._availW / Math.max(1, root.panelWidth),
root._availH / Math.max(1, root.panelHeight)))
// --- What a card is a picture *of* ---------------------------------------
//
// The zone's usable area, not the whole output. A card drawn as the full
// panel carries the bar's reserved strip inside it as dead space, and a
// tiled window whose `at.y` starts below that reservation therefore
// sits with a band of empty card above it. Casey, 2026-08-16: *"each window
// currently has a gap on the top for where the top bar section would go."*
// That gap is `surfaceTop * cardScale` and it was drawn faithfully; the
// card was just a picture of the wrong rectangle.
//
// `surfaceTop` is what this surface lost to exclusive zones, and the header
// above already states the invariant that makes it the right number: it
// equals the `at.y` the compositor reports for any tiled window. Assumes
// reservations stay top-anchored, which is true today (the bar reserves,
// the pill reserves nothing, the dock is on Overlay). A bottom reservation
// would need its own term, and the cross-check that would catch it is the
// same one a card whose windows no longer reach its bottom edge.
readonly property real contentTop: root.surfaceTop
readonly property real contentWidth: root.panelWidth
readonly property real contentHeight: Math.max(1, root.panelHeight - root.contentTop)
// Where the card the strip has come to rest on actually sits. The
// `ListView` enforces its current item at x = 0 of the list
// (`StrictlyEnforceRange` with `preferredHighlightBegin: 0`), so the
// resting card is the list's origin plus the centring inset.
readonly property real cardWidth: root.panelWidth * root.cardScale
readonly property real cardHeight: root.panelHeight * root.cardScale
readonly property real cardX: root._availX + (root._availW - root.cardWidth) / 2
readonly property real cardY: root._availY + (root._availH - root.cardHeight) / 2
// How much of the space a card is allowed to fill.
//
// Fit-to-box made a card that nearly touched its neighbours, so the strip
// read as one thing at a time with slivers either side. Casey, 2026-08-16:
// *"scaled a little bit more / at least tighter together we should be able
// to smoothly scroll between them, floating cards and such."* Under one is
// what makes them cards floating in a strip rather than a stack of screens.
readonly property real cardFill: 0.84
// Between neighbours. The ListView's own `spacing` is 0 so this is the one
// number that says how far apart cards sit.
readonly property real cardGutter: 16
// One factor. `min` so the card keeps the *content's* aspect which is what
// a window's reported rect is measured against and the carried window
// fills it exactly. See the header on why a uniform scale is not a choice
// here but a consequence of how `carried` works.
readonly property real cardScale: Math.max(0.05, root.cardFill * Math.min(
root._availW / Math.max(1, root.contentWidth),
root._availH / Math.max(1, root.contentHeight)))
readonly property real cardWidth: root.contentWidth * root.cardScale
readonly property real cardHeight: root.contentHeight * root.cardScale
// --- Where the resting card sits -----------------------------------------
//
// The delegate is the card plus its gutter, and the `ListView` holds the
// current one centred (`StrictlyEnforceRange`, highlight range below), so a
// neighbour shows on each side and the strip scrolls between them. It used
// to be a full-width delegate pinned at x = 0 with the card centred inside
// it, which is why the cards sat a screen apart.
//
// All four numbers live here and nowhere else. `ZoneOverview` lays the card
// out at `cardInset*` and the carry names `cardX`/`cardY`; they are the same
// rectangle in two coordinate spaces, not two rectangles kept in agreement.
readonly property real delegateWidth: root.cardWidth + root.cardGutter
readonly property real highlightBegin: Math.max(0,
(root._availW - root.delegateWidth) / 2)
// Delegate-local: what `ZoneOverview` positions the frame at.
readonly property real cardInsetX: (root.delegateWidth - root.cardWidth) / 2
readonly property real cardInsetY: (root._availH - root.cardHeight) / 2
// Surface-local: what the carry's destination is measured in.
readonly property real cardX: root._availX + root.highlightBegin + root.cardInsetX
readonly property real cardY: root._availY + root.cardInsetY
// How far the thumb has to climb for the window to *become* its card.
//
// The destination and the distance to it are one question, and quickstep
// answers them in one call `getSwipeUpDestinationAndLength(dp, ctx,
// TEMP_RECT, )` returns the task rect and the drag length together, and
// `LauncherActivityInterface` computes that length as `dp.heightPx -
// outRect.bottom`: how far the window's bottom edge travels.
//
// The rail asked `screen.height * 0.18` instead TASK-60 named it *"a
// number with no relationship to where the card actually is"* and left it.
// Measured on blueline 2026-08-16: the card's bottom sits at 805 logical px
// of 1080, so the true travel is 275 px and the old detent fired at 194.
// The gesture committed with the window 40% short of its card and the
// hand-off covered the rest in one frame Casey's *"the swipe to this swap
// is awkward"*, reported six times and tuned at from every direction except
// this one.
//
// Until the surface has reported, this is the old 18% not a floor, and
// not a guess dressed up as arithmetic. `ZoneOverview` lives inside a gated
// Loader, so nothing has measured anything until the overview has been
// realised once, and computing the card against the *defaults* yields a
// card the size of the panel and a drag length near zero. Behaving exactly
// as yesterday until the real number exists is the honest fallback; the
// rail latches whichever it got at the press, so no gesture ever changes
// detent halfway through.
readonly property real dragLength: root.surfaceReported
? Math.max(120, root.panelHeight
- (root.surfaceTop + root.cardY + root.cardHeight))
: root.panelHeight * 0.18
// Where one window goes: its own place in the zone, drawn small.
//
@ -150,12 +238,17 @@ Singleton {
// is added here. One conversion, at the one boundary where the two spaces
// meet the alternative is every caller remembering an offset, which is
// the shape of bug this file exists to stop.
// `contentTop` comes off the window's own `at.y` before scaling, because the
// card is a picture of the usable zone and not of the whole output the
// same subtraction `ZoneOverview` makes when it lays the pane out. Miss it
// in one place and the carried window lands a bar's height off its picture.
function targetFor(w) {
return {
id: w.id,
at: {
x: root.surfaceLeft + root.cardX + w.at.x * root.cardScale,
y: root.surfaceTop + root.cardY + w.at.y * root.cardScale
y: root.surfaceTop + root.cardY
+ (w.at.y - root.contentTop) * root.cardScale
},
size: {
width: w.size.width * root.cardScale,
@ -194,9 +287,34 @@ Singleton {
// gesture.
readonly property real clock: root.travel / Math.max(1, root.detent)
// Strategy one: what the compositor carries the window on. Clamped, because
// a window's destination is its card and there is nothing beyond it.
readonly property real shift: Math.max(0, Math.min(1, root.clock))
// Strategy one: what the compositor carries the window on.
//
// Past the detent this used to be a hard `Math.min(1, )` the thumb kept
// travelling toward home and the window stopped dead. Android never goes
// dead there, it goes *heavy*: `AnimatorControllerWithResistance` is
// literally two playback controllers, one running 01 and a second that
// *"seamlessly continues that animation but starts applying resistance"*,
// with `DECELERATE` on scale (`RECENTS_SCALE_RESIST_INTERPOLATOR`) and
// `FROM_APP(0.75f, 0.5f, 1f, false)` bounding how far it can go.
//
// Same shape. Past 1 the carry keeps extrapolating beyond the card rect
// the window shrinks further as the pull heads for home but on a curve
// that gives back less and less, so the last of the travel is felt as
// weight rather than as a wall.
//
// A compositor older than the overshoot clamps this to 1 and the gesture is
// exactly what it is today, which is what makes it safe to ship ahead of
// the package (TASK-28).
readonly property real maxOvershoot: 1.22
readonly property real shift: root.clock <= 1
? Math.max(0, root.clock)
: 1 + (root.maxOvershoot - 1) * root._decelerate(Math.min(1, root.clock - 1))
// Android's DECELERATE, which is `1 - (1-t)²`.
function _decelerate(t) {
const inv = 1 - t;
return 1 - inv * inv;
}
// Strategy two: how present the destination is. Rises to the detent, then
// falls away over the same distance, so the two halves of the climb are
@ -252,61 +370,109 @@ Singleton {
// means the window travels *down* to full size rather than the view coming
// up. It animates like every other destination instead of being the branch
// that snaps.
readonly property int maxSwipeDuration: 350
readonly property real minProgressForOverview: 0.7
readonly property real swipeDurationMultiplier: Math.min(
1 / root.minProgressForOverview, 1 / (1 - root.minProgressForOverview))
// What was still missing after the travel landed: a duration and an
// easing curve cannot know how fast the thumb was going when it let go, so
// a flick and a drift settled identically. Android's home path is
// `RectFSpringAnim` with `DefaultSpringConfig` (`SwipeUpAnimationLogic:364`)
// a spring takes the release velocity as an *initial condition*, which is
// the whole difference between a window that travels and one that was
// thrown.
//
// So: a critically damped spring, integrated per frame, seeded with the
// velocity the rail measures over the last 100 ms of the gesture.
//
// Per *frame*. `FrameAnimation` ticks on the render clock, so the carry
// advances once per painted frame instead of once per whatever the socket
// managed which is what the old `NumberAnimation` on `travel` amounted
// to, since every step of it went out through `progress()`.
//
// Critically damped, never underdamped: a navigation surface that rings
// reads as a toy. It still overshoots once on a hard throw, and that part
// is the point.
// ω in 1/ms. Critical damping settles in about 6.6/ω, so 0.026 is ~250 ms.
readonly property real settleOmega: 0.026
// Nothing may outlive this. The commit is what releases the carry, so a
// settle that never converged has to arrive anyway or the window strands
// which is the one failure TASK-60 exists to make impossible.
readonly property int maxSettleMs: 600
// px/ms. A thrown release helps; a wild one does not get to launch the
// window off the glass.
readonly property real maxSeedVelocity: 8
property bool settling: false
property var _settleTarget: null
NumberAnimation {
id: settleAnim
target: root
property: "travel"
easing.type: Easing.OutCubic
onFinished: {
root.settling = false;
const t = root._settleTarget;
root._settleTarget = null;
// Commit *before* zeroing, and the order is load-bearing. Zeroing
// first would drive `shift` to 0 with the carry still in flight
// the window snapping back to full size for a frame, which is the
// very pop this whole task exists to remove. After `commit` the
// carry is released, so `progress` refuses and the zero is inert
// bookkeeping for the next gesture.
root.commit(t);
root.travel = 0;
FrameAnimation {
id: settleTick
running: false
property real velocity: 0
property real to: 0
property real elapsed: 0
onTriggered: {
// Clamped: a dropped frame must not integrate a large step and
// fling the window across the panel.
const dt = Math.min(48, settleTick.frameTime * 1000);
settleTick.elapsed += dt;
const w = root.settleOmega;
const offset = root.travel - settleTick.to;
settleTick.velocity += (-2 * w * settleTick.velocity - w * w * offset) * dt;
root.travel = Math.max(0, root.travel + settleTick.velocity * dt);
if ((Math.abs(root.travel - settleTick.to) < 0.5
&& Math.abs(settleTick.velocity) < 0.02)
|| settleTick.elapsed >= root.maxSettleMs) {
settleTick.running = false;
root.travel = settleTick.to;
root._finishSettle();
}
}
}
function _finishSettle() {
root.settling = false;
const t = root._settleTarget;
root._settleTarget = null;
// Commit *before* zeroing, and the order is load-bearing. Zeroing
// first would drive `shift` to 0 with the carry still in flight
// the window snapping back to full size for a frame, which is the
// very pop this whole task exists to remove. After `commit` the
// carry is released, so `progress` refuses and the zero is inert
// bookkeeping for the next gesture.
root.commit(t);
root.travel = 0;
}
// Release the gesture at a named destination, travelling there first.
function settleTo(target) {
// `velocity` is px/ms along the rail, upward positive, as the rail measured
// it over the tail of the gesture. Omitted means a release with no throw in
// it, which is a legitimate way to let go.
function settleTo(target, velocity) {
// Where on the *clock* each destination lives not on `shift`, which
// saturates at 1 and cannot express home at all.
// saturates at the card and cannot express home at all.
//
// `presence` two properties up is written to rise to the detent and then
// fall away "over the same distance", which puts home at clock 2. This
// settled it at 1 for both home and overview, so a long swipe ran the
// short swipe's motion, held the multitasking view fully present, and
// then teleported the release never travelled to its end target, which
// is the same defect `lineage-trebuchet`'s quickstep is cited for.
// Measured on the glass 2026-08-15: *"the long full swipe up is
// completely fucked, only the short swipe partially works."*
// `presence` above is written to rise to the detent and then fall away
// "over the same distance", which puts home at clock 2. This settled it
// at 1 for both home and overview, so a long swipe ran the short
// swipe's motion, held the multitasking view fully present, and then
// teleported. Measured on the glass 2026-08-15: *"the long full swipe up
// is completely fucked, only the short swipe partially works."*
const endClock = target === "last_zone" ? 0 : target === "home" ? 2 : 1;
// The clock, not the clamped carry: past the detent `shift` is pinned at
// 1, so a release from a home-bound pull computed a duration for travel
// it had already done.
const from = root.clock;
settleAnim.stop();
settleTick.running = false;
root._settleTarget = target;
root.settling = true;
settleAnim.from = root.travel;
settleAnim.to = endClock * root.detent;
settleAnim.duration = Math.min(root.maxSwipeDuration,
Math.abs(Math.round((endClock - from)
* root.maxSwipeDuration * root.swipeDurationMultiplier)));
settleAnim.start();
settleTick.to = endClock * root.detent;
settleTick.elapsed = 0;
// Projected onto the way out, not applied raw. The gesture has already
// resolved to one end target, so a hard release means "get there", not
// "carry on past it and be pulled back" which is what a raw upward
// seed would do to a `last_zone` return and would read as a bounce.
const toward = settleTick.to >= root.travel ? 1 : -1;
settleTick.velocity = toward * Math.min(root.maxSeedVelocity,
Math.max(0, velocity ?? 0));
settleTick.running = true;
}
// --- The carry -----------------------------------------------------------
@ -317,10 +483,20 @@ Singleton {
// one of those per frame.
property bool inFlight: false
// Last shift actually written. A drag emits a motion event per frame and
// each one is a socket round-trip; anything under a percent is invisible.
// Same throttle `poseActiveZone` used, kept because the reason was the
// socket and not the pose.
// Last shift actually written, and how little a change has to be before it
// is not worth sending.
//
// This was 0.01 a hundred steps for the whole carry, inherited from
// `poseActiveZone` on the reasoning that a round-trip per motion event was
// expensive. **Measured on blueline 2026-08-16, it is not:** 200 samples of
// connect write event-loop hop reply close, `{"op":"workspaces"}`,
// came back at 0.47 ms median and 1.04 ms worst under 3% of a 16.7 ms
// frame. A hundred steps across 275 px of travel is a step every 2 logical
// pixels, and on a scaling window that staircase is visible.
//
// 500 steps costs the same 0.5 ms per frame, because it is still one send
// per motion event the quantiser was never what bounded the rate.
readonly property real shiftEpsilon: 0.002
property real _lastShift: -1
// Take the windows on the zone in front and name where each is going.
@ -350,12 +526,14 @@ Singleton {
return true;
}
// 0 is where the windows live, 1 is each on its card.
// 0 is where the windows live, 1 is each on its card, and past 1 is the
// resisted overshoot toward home. A compositor that predates the overshoot
// clamps it back to 1 on arrival, which is the old behaviour exactly.
function progress(shift) {
if (!root.inFlight)
return;
const s = Math.max(0, Math.min(1, shift));
if (Math.abs(s - root._lastShift) < 0.01)
const s = Math.max(0, Math.min(root.maxOvershoot, shift));
if (Math.abs(s - root._lastShift) < root.shiftEpsilon)
return;
root._lastShift = s;
ViewtopControl.overviewProgress(s);