multitasking: one owner for the transition, and the release travels
ZoneTransition owns the card rect, the four overview verbs, the end-target table and the gesture clock. ZoneOverview and the rail read it; neither derives a geometry. Deletes poseActiveZone/clearPose/_posed, zoneCard.scale and zonePullProgress. The card's ColumnLayout went too — its margin was a second opinion about where a window sits in its zone; panes are the compositor's tiling scaled. The swipe felt wrong because the release committed where the thumb left it. settleTo() travels to the destination first and commits on arrival, with quickstep's numbers (350ms cap, min(1/0.7, 1/0.3)). Abandoned half-swipes settle to last_zone instead of snapping. Dwell gate restored — quickstep gates the same way and "ever paused" is a latch. Card box is measured from the surface, not assumed from the screen: the overview panel respects exclusive zones and its height moves. Strand repro passes on hardware.
This commit is contained in:
parent
72171accec
commit
16c6ee1033
8 changed files with 914 additions and 225 deletions
|
|
@ -164,30 +164,31 @@ Singleton {
|
||||||
// (app launcher/search) — this is the running-work view.
|
// (app launcher/search) — this is the running-work view.
|
||||||
property bool missionControlOpen: false
|
property bool missionControlOpen: false
|
||||||
|
|
||||||
// WRITER: navigation rail, during an upward drag only. 0 at rest, 1 at the
|
// GONE: zonePullProgress. Its job moved to `ZoneTransition` (TASK-60).
|
||||||
// multitasking detent.
|
|
||||||
//
|
//
|
||||||
// The one progress value the transition and the destination share. The rail
|
// It was the right idea — one level the transition and the destination
|
||||||
// shrinks the *real* windows through `pose` as the thumb climbs, and
|
// share — and the wrong home. The motion needs *two* curves over one clock:
|
||||||
// `ZoneOverview` enters against this same number, so the card the drag was
|
// the carry keeps travelling as the thumb climbs past the multitasking
|
||||||
// pulling toward is already the size the window had reached when it takes
|
// detent toward home, while the destination has to recede, or the preview
|
||||||
// over. Two surfaces animating the same motion from two clocks is the thing
|
// shows somewhere the release will not take you. Two curves derived in two
|
||||||
// that made the first attempt read as a scale effect with a view bolted
|
// files is how this task's original bug was built, so both now live with
|
||||||
// after it — TASK-60's acceptance names it: *"no frame where a window is
|
// the owner: `ZoneTransition.clock`, `.shift`, `.presence`.
|
||||||
// scaled by one and laid out by the other."*
|
|
||||||
//
|
//
|
||||||
// A plain number rather than a signal because it is a level: a surface that
|
// Not left here as a mirror. A state property nobody writes is a shadow
|
||||||
// appears mid-drag needs to know where the drag *is*, not to have missed
|
// copy waiting for a reader, and this file has a rule about those.
|
||||||
// the edge where it started.
|
|
||||||
property real zonePullProgress: 0
|
|
||||||
|
|
||||||
// WRITER: navigation rail, while a pull dwells past the multitasking
|
// WRITER: navigation rail, for the whole of an upward pull. The cards are
|
||||||
// detent. The cards are DRAWN, nothing is committed — release still decides.
|
// DRAWN, nothing is committed — release still decides. Gates presentation
|
||||||
// Gates presentation only (visibility, mask, which body the loader builds);
|
// only (visibility, mask, which body the loader builds); never focus,
|
||||||
// never focus, dismissal or state, or a preview would be a decision.
|
// dismissal or state, or a preview would be a decision.
|
||||||
//
|
//
|
||||||
// Dwell rather than any pull, so a fast flick to Home never flashes the
|
// Any pull, not a dwell. It used to wait 140 ms of holding still, which
|
||||||
// blur on its way past.
|
// meant an ordinary swipe shrank the real window over the live app and then
|
||||||
|
// the entire destination appeared in one frame — the "swipe to this swap is
|
||||||
|
// awkward" complaint, and TASK-60's unmet acceptance. The old worry, that a
|
||||||
|
// fast flick to Home would flash the blur, is answered by the fade being
|
||||||
|
// continuous and receding past the detent rather than by hiding the surface
|
||||||
|
// until the thumb stops.
|
||||||
property bool missionPeek: false
|
property bool missionPeek: false
|
||||||
|
|
||||||
// WRITER: Dock.qml IPC (swipeDown). A rail swipe-down on a visible
|
// WRITER: Dock.qml IPC (swipeDown). A rail swipe-down on a visible
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,7 @@ modules/souveraine/windowSheet/PowerMenu.qml souveraine/modules/souveraine/windo
|
||||||
modules/souveraine/windowSheet/qmldir souveraine/modules/souveraine/windowSheet/qmldir
|
modules/souveraine/windowSheet/qmldir souveraine/modules/souveraine/windowSheet/qmldir
|
||||||
services/Face.qml souveraine/services/Face.qml
|
services/Face.qml souveraine/services/Face.qml
|
||||||
services/ViewtopControl.qml souveraine/services/ViewtopControl.qml
|
services/ViewtopControl.qml souveraine/services/ViewtopControl.qml
|
||||||
|
services/ZoneTransition.qml souveraine/services/ZoneTransition.qml
|
||||||
services/Hyprsunset.qml souveraine/services/Hyprsunset.qml
|
services/Hyprsunset.qml souveraine/services/Hyprsunset.qml
|
||||||
modules/souveraine/subconscious/SubconsciousTicker.qml souveraine/modules/souveraine/subconscious/SubconsciousTicker.qml
|
modules/souveraine/subconscious/SubconsciousTicker.qml souveraine/modules/souveraine/subconscious/SubconsciousTicker.qml
|
||||||
modules/souveraine/subconscious/SubconsciousEventPanel.qml souveraine/modules/souveraine/subconscious/SubconsciousEventPanel.qml
|
modules/souveraine/subconscious/SubconsciousEventPanel.qml souveraine/modules/souveraine/subconscious/SubconsciousEventPanel.qml
|
||||||
|
|
|
||||||
|
|
@ -247,6 +247,31 @@ Scope {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
z: -1
|
z: -1
|
||||||
|
|
||||||
|
// The backdrop arrives with the climb rather than at the end of it.
|
||||||
|
//
|
||||||
|
// This was a hard on/off gated on a 140 ms dwell, so an ordinary
|
||||||
|
// swipe shrank the real window over the live app and then the whole
|
||||||
|
// destination — blur, sheet, cards — appeared in one frame. That
|
||||||
|
// discontinuity is the "swipe to this swap is awkward" Casey has
|
||||||
|
// named repeatedly, and it is TASK-60's own acceptance: *"the
|
||||||
|
// scale-on-drag either continues into the view or is gone. Not
|
||||||
|
// both."*
|
||||||
|
//
|
||||||
|
// Same curve as the cards, from the same owner, so at the commit
|
||||||
|
// frame the backdrop is already opaque and the card's picture is
|
||||||
|
// already exactly under the real window — releasing the carry
|
||||||
|
// changes nothing on the glass. A fast flick to Home shows a faint
|
||||||
|
// wash rather than a hard flash, because `presence` recedes past
|
||||||
|
// the multitasking detent instead of latching on.
|
||||||
|
opacity: GlobalStates.missionControlOpen ? 1 : ZoneTransition.presence
|
||||||
|
Behavior on opacity {
|
||||||
|
enabled: GlobalStates.missionControlOpen || ZoneTransition.travel === 0
|
||||||
|
NumberAnimation {
|
||||||
|
duration: 260
|
||||||
|
easing.type: Easing.OutCubic
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Image {
|
Image {
|
||||||
id: homeWall
|
id: homeWall
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
|
@ -366,6 +391,28 @@ Scope {
|
||||||
// things, and could not show a zone you were not on. A zone is
|
// things, and could not show a zone you were not on. A zone is
|
||||||
// the destination; its windows live inside its card.
|
// the destination; its windows live inside its card.
|
||||||
ZoneOverview {
|
ZoneOverview {
|
||||||
|
id: zoneOverview
|
||||||
|
|
||||||
|
// Tell the owner where this surface actually is, rather than
|
||||||
|
// letting it assume the screen. The panel respects exclusive
|
||||||
|
// zones, so the bar's 40 px makes it 1040 tall on a 1080
|
||||||
|
// screen and puts its origin 40 px down the output — and the
|
||||||
|
// compositor's carry targets are in output coordinates.
|
||||||
|
//
|
||||||
|
// The inset is what was lost to reservations, which are
|
||||||
|
// top-anchored for this surface. It should equal the `at.y`
|
||||||
|
// the compositor reports for any tiled window; if a future
|
||||||
|
// surface reserves along the bottom this becomes wrong, and
|
||||||
|
// the cross-check is how it would be caught.
|
||||||
|
function report() {
|
||||||
|
ZoneTransition.measuredAt(0,
|
||||||
|
(panelWindow.screen?.height ?? panelWindow.height) - panelWindow.height,
|
||||||
|
width, height);
|
||||||
|
}
|
||||||
|
onWidthChanged: zoneOverview.report()
|
||||||
|
onHeightChanged: zoneOverview.report()
|
||||||
|
Component.onCompleted: zoneOverview.report()
|
||||||
|
|
||||||
// The panel's width, NOT the column's.
|
// The panel's width, NOT the column's.
|
||||||
//
|
//
|
||||||
// `overviewLoader.parent` is the Column, and a Column is
|
// `overviewLoader.parent` is the Column, and a Column is
|
||||||
|
|
@ -393,9 +440,17 @@ Scope {
|
||||||
// Going to a zone is not "activate a toplevel" — that
|
// Going to a zone is not "activate a toplevel" — that
|
||||||
// was the old model's verb and it could not express
|
// was the old model's verb and it could not express
|
||||||
// "this place, which happens to hold two windows".
|
// "this place, which happens to hold two windows".
|
||||||
ViewtopControl.zone(zone);
|
//
|
||||||
GlobalStates.overviewOpen = false;
|
// Through the end-target table, which is what makes the
|
||||||
GlobalStates.missionControlOpen = false;
|
// strand repro pass (TASK-60). This used to move the
|
||||||
|
// canvas and clear two flags by hand — three steps, none
|
||||||
|
// of which released a transform, and this tap never
|
||||||
|
// touches the rail where every release-the-pose path had
|
||||||
|
// been built. So btop came back as a small card in the
|
||||||
|
// middle of the screen. A tapped card is now the same
|
||||||
|
// machinery as a released pill: one destination, and
|
||||||
|
// arriving at it is what lets go.
|
||||||
|
ZoneTransition.commit(ZoneTransition.zoneTarget(zone));
|
||||||
}
|
}
|
||||||
onClosed: id => ViewtopControl.close(id)
|
onClosed: id => ViewtopControl.close(id)
|
||||||
}
|
}
|
||||||
|
|
@ -524,6 +579,78 @@ Scope {
|
||||||
GlobalStates.missionControlOpen = !GlobalStates.missionControlOpen;
|
GlobalStates.missionControlOpen = !GlobalStates.missionControlOpen;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The multitasking gesture, without a finger.
|
||||||
|
//
|
||||||
|
// TASK-60's acceptance: *"The gesture is reachable as a verb. An agent
|
||||||
|
// can drive `shift` and pick an end target well enough to demo
|
||||||
|
// multitasking."* Casey, 2026-08-07 — there is a point where she is
|
||||||
|
// asked for a tour of the phone, *"and that does mean even these little
|
||||||
|
// gesture steps will be possible."*
|
||||||
|
//
|
||||||
|
// Three steps rather than one opaque call, because a tour narrates: she
|
||||||
|
// can hold the windows half-carried while she says what multitasking
|
||||||
|
// is, and then land them. `swipe` is the whole motion for when the
|
||||||
|
// narration is not the point.
|
||||||
|
//
|
||||||
|
// No contact is synthesized, so the compositor never sees input at all
|
||||||
|
// and the run cannot raise `observed_confidence` or feed the idle
|
||||||
|
// budget — the machine must not believe a human is present because she
|
||||||
|
// moved a window. Step-up stays human-only by the same absence:
|
||||||
|
// `input.rs:432` routes `Origin::Agent` to `Route::Withheld`, and this
|
||||||
|
// path does not go near it.
|
||||||
|
function carryBegin(): string {
|
||||||
|
return ZoneTransition.begin() ? "carrying"
|
||||||
|
: "nothing on this zone to carry";
|
||||||
|
}
|
||||||
|
|
||||||
|
function carryShift(shift: real): void {
|
||||||
|
ZoneTransition.progress(shift);
|
||||||
|
}
|
||||||
|
|
||||||
|
// `target` is an end target by name: home, overview, last_zone, or a
|
||||||
|
// zone number. Same four the pill commits, same table.
|
||||||
|
function carryCommit(target: string): void {
|
||||||
|
const n = parseInt(target, 10);
|
||||||
|
ZoneTransition.commit(isNaN(n) ? target : ZoneTransition.zoneTarget(n));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Begin, travel, and land — the whole gesture in one call.
|
||||||
|
function swipe(target: string): void {
|
||||||
|
ZoneTransition.begin();
|
||||||
|
tour.target = target;
|
||||||
|
tour.step = 0;
|
||||||
|
tour.restart();
|
||||||
|
}
|
||||||
|
|
||||||
|
function carryState(): string {
|
||||||
|
return JSON.stringify({
|
||||||
|
inFlight: ZoneTransition.inFlight,
|
||||||
|
cardScale: ZoneTransition.cardScale,
|
||||||
|
card: {
|
||||||
|
x: ZoneTransition.cardX,
|
||||||
|
y: ZoneTransition.cardY,
|
||||||
|
w: ZoneTransition.cardWidth,
|
||||||
|
h: ZoneTransition.cardHeight
|
||||||
|
},
|
||||||
|
panel: { w: ZoneTransition.panelWidth, h: ZoneTransition.panelHeight },
|
||||||
|
// What the surface reported, beside what the panel actually is.
|
||||||
|
// The card rect is derived from these, so a card in the wrong
|
||||||
|
// place is diagnosed by reading them rather than by theorising —
|
||||||
|
// the same reason `state` exists at all (TASK-60: `item.w: 0`
|
||||||
|
// beside `column.w: 0` located the zero-width card in one step).
|
||||||
|
surface: {
|
||||||
|
left: ZoneTransition.surfaceLeft,
|
||||||
|
top: ZoneTransition.surfaceTop,
|
||||||
|
w: ZoneTransition.surfaceWidth,
|
||||||
|
h: ZoneTransition.surfaceHeight
|
||||||
|
},
|
||||||
|
panelWindow: { w: panelWindow.width, h: panelWindow.height },
|
||||||
|
loaderItem: overviewLoader.item
|
||||||
|
? { w: overviewLoader.item.width, h: overviewLoader.item.height }
|
||||||
|
: null
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function state(): string {
|
function state(): string {
|
||||||
return JSON.stringify({
|
return JSON.stringify({
|
||||||
overview: GlobalStates.overviewOpen,
|
overview: GlobalStates.overviewOpen,
|
||||||
|
|
@ -549,6 +676,38 @@ Scope {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The travel half of `overview swipe`. Sixteen steps over ~320 ms is the
|
||||||
|
// settle `ZoneOverview` already animates against, so an agent's swipe and a
|
||||||
|
// thumb's arrive at the same speed — a demo that moved at a different rate
|
||||||
|
// than the real gesture would be showing something the phone does not do.
|
||||||
|
Timer {
|
||||||
|
id: tour
|
||||||
|
property string target: "overview"
|
||||||
|
property int step: 0
|
||||||
|
readonly property int steps: 16
|
||||||
|
interval: 20
|
||||||
|
repeat: true
|
||||||
|
onTriggered: {
|
||||||
|
tour.step += 1;
|
||||||
|
if (tour.step >= tour.steps) {
|
||||||
|
tour.stop();
|
||||||
|
const n = parseInt(tour.target, 10);
|
||||||
|
ZoneTransition.commit(isNaN(n) ? tour.target
|
||||||
|
: ZoneTransition.zoneTarget(n));
|
||||||
|
ZoneTransition.rest();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Drives the clock, exactly as the rail does — so the carry, the
|
||||||
|
// backdrop and the cards all move on the demo for the same reason
|
||||||
|
// they move under a thumb. A tour that ran its own animation would
|
||||||
|
// be showing something the phone does not actually do.
|
||||||
|
//
|
||||||
|
// A detent of `steps` makes one step one unit of travel, so the
|
||||||
|
// demo lands precisely on the multitasking detent at the last step.
|
||||||
|
ZoneTransition.pullTo(tour.step, tour.steps);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
IpcHandler {
|
IpcHandler {
|
||||||
target: "search"
|
target: "search"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,11 +20,12 @@
|
||||||
// it presented, and the swap detent has to cost a deliberate reach, not a
|
// it presented, and the swap detent has to cost a deliberate reach, not a
|
||||||
// near miss. Keyboard down, the two swipe modes are the whole of it.
|
// near miss. Keyboard down, the two swipe modes are the whole of it.
|
||||||
//
|
//
|
||||||
// The upward gesture is progressive, not a single threshold, and the progress
|
// The upward gesture is progressive, not a single threshold, and the travel is
|
||||||
// is *published* (`GlobalStates.zonePullProgress`) rather than kept: the real
|
// *reported* to `ZoneTransition` rather than interpreted here: the compositor
|
||||||
// windows shrink through `pose` as the thumb climbs, and `ZoneOverview` enters
|
// carries the real windows onto their cards, and the destination fades in over
|
||||||
// against the same number, so the transition and the destination are one
|
// the same climb, so the transition and the destination are one motion driven
|
||||||
// motion. The handle tracks it too — it grows, brightens and lifts toward
|
// by one clock. This file measures a thumb and derives nothing from it.
|
||||||
|
// The handle tracks it too — it grows, brightens and lifts toward
|
||||||
// whichever stage the travel has reached, so the pill reads as "on top" of the
|
// whichever stage the travel has reached, so the pill reads as "on top" of the
|
||||||
// motion rather than a passive strip. Release commits the stage the drag last
|
// motion rather than a passive strip. Release commits the stage the drag last
|
||||||
// held; a flick past the mission line commits multitasking even from a
|
// held; a flick past the mission line commits multitasking even from a
|
||||||
|
|
@ -170,21 +171,21 @@ PanelWindow {
|
||||||
property real dragTravel: 0
|
property real dragTravel: 0
|
||||||
property bool dragging: false
|
property bool dragging: false
|
||||||
|
|
||||||
// The pose is a function of `dragging`, not a thing each exit path
|
// One edge, and everything rides on it. A drag ends by setting `dragging`
|
||||||
// remembers to undo. DUMP-bugs 8: a partial slide left the app scaled small
|
// false and by nothing else. DUMP-bugs 8: a partial slide left the app
|
||||||
// and only another pull brought it back, and the cause was never found
|
// scaled small and only another pull brought it back, and the cause was
|
||||||
// because every path that was *looked at* did call `endPull` — release,
|
// never found because every path that was *looked at* did call `endPull` —
|
||||||
// cancel and the swap all did. Something ended a drag without going through
|
// release, cancel and the swap all did. Something ended a drag without
|
||||||
// any of them, and searching for which one is the wrong shape of fix: as
|
// going through any of them, and searching for which one is the wrong shape
|
||||||
// long as restoring is a step, there is a path that skips the step.
|
// of fix: as long as restoring is a step, there is a path that skips it.
|
||||||
//
|
//
|
||||||
// So there is one edge and everything rides on it. A drag ends by setting
|
// Restoring is no longer a step (TASK-60). The compositor holds the
|
||||||
// `dragging` false and by nothing else; this restores. `endPull` is
|
// transform and releases it on `commit`/`cancel`, so what this edge cleans
|
||||||
// idempotent, so a path that also calls it directly costs a redundant write
|
// up is the shell's own chrome plus a carry that no destination claimed —
|
||||||
// and cannot strand. Ordering is preserved by the release handler setting
|
// an abandoned drag, or one the compositor took away mid-flight. The
|
||||||
// `dragging` false *last*, after it has committed — clearing the pose first
|
// release handler still sets `dragging` false *last*, after it has
|
||||||
// would pop the app back to full size for a frame before Mission Control
|
// committed, so `ZoneTransition.inFlight` is already clear by then and the
|
||||||
// took the screen.
|
// cancel here is a no-op rather than a second decision.
|
||||||
onDraggingChanged: if (!rail.dragging) gestureArea.endPull()
|
onDraggingChanged: if (!rail.dragging) gestureArea.endPull()
|
||||||
|
|
||||||
// The thumb has stopped climbing without letting go. iOS/Android both read
|
// The thumb has stopped climbing without letting go. iOS/Android both read
|
||||||
|
|
@ -201,15 +202,67 @@ PanelWindow {
|
||||||
onTriggered: rail.dwelled = true
|
onTriggered: rail.dwelled = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only in multitasking range. Climb on toward home and the cards recede, so
|
// The destination is drawn for the whole climb, not only after a dwell.
|
||||||
// the preview never shows a destination the release would not commit.
|
//
|
||||||
readonly property bool peekWanted: rail.dragging && rail.dwelled && rail.dragStage === 1
|
// Casey has said this six times and it was the acceptance criterion left
|
||||||
|
// unmet: *"the visual style from swiping up into this mode… we lost the
|
||||||
|
// consistency… the swipe to this swap is awkward."* The cause was here. The
|
||||||
|
// surface used to require `dwelled` — 140 ms of holding still — so an
|
||||||
|
// ordinary pull shrank the real window over the live app with no backdrop
|
||||||
|
// and no cards behind it, and then the entire destination appeared at once
|
||||||
|
// on commit. Two visual events for one motion.
|
||||||
|
//
|
||||||
|
// Now it draws from the first climbing pixel and fades in against
|
||||||
|
// `zonePullProgress` — the same number the compositor is carrying the window
|
||||||
|
// on. At the commit frame the backdrop is already opaque and the card's
|
||||||
|
// picture is already exactly under the real window, so releasing the carry
|
||||||
|
// changes nothing on the glass. That is TASK-60's *"no frame where a window
|
||||||
|
// is scaled by one and laid out by the other"*, and its *"the scale-on-drag
|
||||||
|
// either continues into the view or is gone. Not both."*
|
||||||
|
//
|
||||||
|
// The dwell has not been deleted, it has been demoted: it still fires the
|
||||||
|
// haptic that says "release here and you land in multitasking", which is
|
||||||
|
// what it was actually good for. What it must not do is gate whether the
|
||||||
|
// destination exists.
|
||||||
|
//
|
||||||
|
// The old worry — *"a fast flick to Home never flashes the blur on its way
|
||||||
|
// past"* — is answered by the fade being continuous rather than by hiding
|
||||||
|
// the surface. A flick spends a few frames at low progress, so it shows a
|
||||||
|
// faint wash instead of a hard flash; and past the multitasking detent the
|
||||||
|
// presence recedes (see `overviewPresence`), so a home-bound pull never
|
||||||
|
// arrives at a destination the release would not commit.
|
||||||
|
// The rail reports the clock — how far the thumb has climbed — and reads
|
||||||
|
// none of the curves derived from it. `ZoneTransition` turns one travel into
|
||||||
|
// the carry's shift and the destination's presence, because they are two
|
||||||
|
// strategies over one clock and putting them in two files is how this task's
|
||||||
|
// original bug was built (TASK-52: one attention model, one clock, effects
|
||||||
|
// as strategies over it).
|
||||||
|
//
|
||||||
|
// The destination draws once the motion has *ever* paused, or once the climb
|
||||||
|
// is deep enough to be heading there. Straight from quickstep, which decides
|
||||||
|
// the same thing the same way (`AbsSwipeUpHandler`):
|
||||||
|
//
|
||||||
|
// recentsAttachedToAppWindow = mHasMotionEverBeenPaused
|
||||||
|
// || mIsLikelyToStartNewTask;
|
||||||
|
//
|
||||||
|
// "Ever", not "currently" — `dwelled` latches for the rest of the drag, so a
|
||||||
|
// thumb that trembles after pausing does not strobe the blur. This was
|
||||||
|
// briefly removed here on the theory that the dwell gate caused the awkward
|
||||||
|
// hand-off; it did not. The cut was the release committing *at* wherever the
|
||||||
|
// thumb left it, which `ZoneTransition.settleTo` now travels through. A fast
|
||||||
|
// unpaused flick still shows no destination on its way past, which is both
|
||||||
|
// what Android does and what the original comment here wanted.
|
||||||
|
readonly property bool peekWanted: rail.dragging && !GlobalStates.oskOpen
|
||||||
|
&& (rail.dwelled || rail.dragStage >= 1)
|
||||||
|
|
||||||
|
// The haptic the dwell is still for: a tick when the thumb settles in
|
||||||
|
// multitasking range, so the detent can be found without looking.
|
||||||
|
onDwelledChanged: if (rail.dwelled && rail.dragStage === 1) Haptics.tick()
|
||||||
|
|
||||||
onPeekWantedChanged: {
|
onPeekWantedChanged: {
|
||||||
if (rail.peekWanted) {
|
if (rail.peekWanted) {
|
||||||
peekFade.stop()
|
peekFade.stop()
|
||||||
GlobalStates.missionPeek = true
|
GlobalStates.missionPeek = true
|
||||||
Haptics.tick()
|
|
||||||
} else if (GlobalStates.missionPeek) {
|
} else if (GlobalStates.missionPeek) {
|
||||||
// `endPull` zeroes the progress the cards are laid out against, and
|
// `endPull` zeroes the progress the cards are laid out against, and
|
||||||
// that settle is animated. Dropping the surface on the release frame
|
// that settle is animated. Dropping the surface on the release frame
|
||||||
|
|
@ -338,31 +391,19 @@ PanelWindow {
|
||||||
// A single tap is Home. Delay it by the double-tap window so the
|
// A single tap is Home. Delay it by the double-tap window so the
|
||||||
// existing fullscreen gesture remains unambiguous; the second tap
|
// existing fullscreen gesture remains unambiguous; the second tap
|
||||||
// cancels this timer before toggling fullscreen.
|
// cancels this timer before toggling fullscreen.
|
||||||
function goHome(): void {
|
// Square one has to be reachable: it is the state you get back to when
|
||||||
GlobalStates.missionControlOpen = false
|
// everything else is confusing, and a phone whose Home does nothing is
|
||||||
GlobalStates.overviewOpen = false
|
// one wrong gesture from being stuck.
|
||||||
GlobalStates.dockRevealed = false
|
|
||||||
GlobalStates.oskOpen = false
|
|
||||||
// The compositor's own verb, not `hyprctl dispatch` — that binary
|
|
||||||
// is not running under viewtop, so this call went nowhere and Home
|
|
||||||
// silently stopped existing. Square one has to be reachable: it is
|
|
||||||
// the state you get back to when everything else is confusing, and
|
|
||||||
// a phone whose Home does nothing is one wrong gesture from being
|
|
||||||
// stuck. `workspace` is served on the control socket and takes the
|
|
||||||
// zone to go to.
|
|
||||||
//
|
//
|
||||||
// `ViewtopControl.homeZone`, not a config lookup. This read
|
// The body moved to `ZoneTransition.arrive("home")` (TASK-60), which is
|
||||||
// `Config.options.navigation?.homeZone ?? 1`, and there is no
|
// the one table where an end target becomes an action. It clears the
|
||||||
// `navigation` block in `Config.qml` — so it was always 1, while
|
// furniture and calls the compositor's own `workspace` verb — not
|
||||||
// home is 0 (`workspace::HOME_ZONE`, and the same constant the
|
// `hyprctl dispatch`, which is not running under viewtop, so it went
|
||||||
// dock's pinned rule and the overview's "Home" label both test
|
// nowhere and Home silently stopped existing — with `homeZone` rather
|
||||||
// against). Home therefore landed on the first *app* zone, and the
|
// than a config lookup that resolved to 1 and put Home on the first
|
||||||
// dock — whose whole new rule is "pinned on home" — correctly
|
// *app* zone.
|
||||||
// refused to appear there. It looked like it worked because the
|
function goHome(): void {
|
||||||
// compositor clamps `to` against `count - 1`, so with only home
|
ZoneTransition.settleTo("home")
|
||||||
// open, 1 becomes 0; it broke the moment a second zone existed.
|
|
||||||
// Casey's "zone one" is the strip's first zone, which is index 0.
|
|
||||||
ViewtopControl.zone(ViewtopControl.homeZone)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Commit an upward gesture. `stage` is the stage the drag settled on
|
// Commit an upward gesture. `stage` is the stage the drag settled on
|
||||||
|
|
@ -381,12 +422,14 @@ PanelWindow {
|
||||||
// Short *toggles*. Reaching multitasking with a gesture that cannot
|
// Short *toggles*. Reaching multitasking with a gesture that cannot
|
||||||
// also leave it means the way out is a different gesture than the way
|
// also leave it means the way out is a different gesture than the way
|
||||||
// in, which is the thing that makes a phone feel stuck.
|
// in, which is the thing that makes a phone feel stuck.
|
||||||
|
// 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): void {
|
||||||
if (stage >= 2) {
|
if (stage >= 2) {
|
||||||
// Square one. `goHome` clears multitasking, the overview, the
|
// Square one. The arrival clears multitasking, the overview,
|
||||||
// dock and the OSK before moving, so the zone is not arrived at
|
// the dock and the OSK before moving, so the zone is not
|
||||||
// with the last screen's furniture still up.
|
// arrived at with the last screen's furniture still up.
|
||||||
//
|
|
||||||
goHome()
|
goHome()
|
||||||
shortSwipes = 0
|
shortSwipes = 0
|
||||||
return
|
return
|
||||||
|
|
@ -395,10 +438,20 @@ PanelWindow {
|
||||||
// Multitasking only. The dock is not touched here any more: it
|
// Multitasking only. The dock is not touched here any more: it
|
||||||
// belongs to zone one and is shown by being *there*, not by a
|
// belongs to zone one and is shown by being *there*, not by a
|
||||||
// gesture that reveals it over whatever else is on screen.
|
// gesture that reveals it over whatever else is on screen.
|
||||||
GlobalStates.missionControlOpen = !GlobalStates.missionControlOpen
|
//
|
||||||
if (GlobalStates.missionControlOpen
|
// Short *toggles*: a way in that cannot also be the way out is
|
||||||
&& !Persistent.states.navigation.missionControlDiscovered)
|
// what makes a phone feel stuck. Leaving is `last_zone` — a
|
||||||
|
// destination with a name, so the release runs the same
|
||||||
|
// machinery as every other exit rather than being the one path
|
||||||
|
// that only unsets a flag.
|
||||||
|
if (GlobalStates.missionControlOpen) {
|
||||||
|
ZoneTransition.settleTo("last_zone")
|
||||||
|
GlobalStates.missionControlOpen = false
|
||||||
|
} else {
|
||||||
|
ZoneTransition.settleTo("overview")
|
||||||
|
if (!Persistent.states.navigation.missionControlDiscovered)
|
||||||
Persistent.states.navigation.missionControlDiscovered = true
|
Persistent.states.navigation.missionControlDiscovered = true
|
||||||
|
}
|
||||||
shortSwipes = 0
|
shortSwipes = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -458,9 +511,30 @@ PanelWindow {
|
||||||
// which is exactly the wrong read (observed 2026-08-06). The
|
// which is exactly the wrong read (observed 2026-08-06). The
|
||||||
// keyboard's climb touches nothing; only the swap commits.
|
// keyboard's climb touches nothing; only the swap commits.
|
||||||
if (!GlobalStates.oskOpen) {
|
if (!GlobalStates.oskOpen) {
|
||||||
const progress = Math.min(1.0, rail.dragTravel / Math.max(1, rail.missionAt))
|
// Name the destination once, at the moment this becomes an
|
||||||
GlobalStates.zonePullProgress = progress
|
// upward pull — quickstep's `getSwipeUpDestinationAndLength`
|
||||||
ViewtopControl.poseActiveZone(1.0 - 0.4 * progress)
|
// returns the rect and the length together for the same reason.
|
||||||
|
// From here the compositor owns these windows' geometry until
|
||||||
|
// something commits.
|
||||||
|
//
|
||||||
|
// On the first climbing pixels rather than on `onPressed`,
|
||||||
|
// which is where this started. `focus_border` yields while a
|
||||||
|
// window is carried, so beginning on every press flashed the
|
||||||
|
// focus frame off and back on for a tap or a swipe *down* —
|
||||||
|
// gestures that are not this one and must cost nothing. Eight
|
||||||
|
// pixels is well under the first detent at 48, so the carry is
|
||||||
|
// running long before anything could commit and the motion is
|
||||||
|
// still continuous from the start of the climb.
|
||||||
|
//
|
||||||
|
// Not with a keyboard up: there the climb is the swap pull and
|
||||||
|
// touches nothing. An app shrinking under a thumb that was
|
||||||
|
// reaching for a different keyboard is the wrong read, and was
|
||||||
|
// the keyboard-swap complaint as it presented (2026-08-06).
|
||||||
|
if (!ZoneTransition.inFlight && rail.dragTravel >= 8)
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -468,23 +542,31 @@ PanelWindow {
|
||||||
// — because every destination wants the same thing and the difference
|
// — because every destination wants the same thing and the difference
|
||||||
// was never in the action.
|
// was never in the action.
|
||||||
//
|
//
|
||||||
// It used to take a `committed` flag it did not read. Committed means
|
// It used to take a `committed` flag it did not read, and it used to
|
||||||
// the multitasking view is taking over and draws the zone the pose was
|
// restore the windows. Neither is true now: the destinations differ,
|
||||||
// shrinking, so a pose left on puts two shrunken pictures of one app on
|
// but the *cleanup* never did, and the windows are the compositor's to
|
||||||
// the glass. Abandoned means the windows have to spring back or a
|
// put back.
|
||||||
// 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
|
// What is left is the shell's own chrome, plus `cancel` as the safety
|
||||||
// window left at 0.6 on the zone behind you is one you find shrunken
|
// net for a drag that ended without reaching any destination — a
|
||||||
// next time you swipe back to it. Three reasons, one action — so the
|
// sideways smudge, or the compositor taking the sequence away after a
|
||||||
// parameter only ever documented a distinction the body did not make.
|
// wake. Every real commit has already cleared `inFlight`, so this is a
|
||||||
|
// no-op on those paths rather than a second writer racing the first.
|
||||||
//
|
//
|
||||||
// Idempotent on purpose: it is the safety net, and a net that cannot be
|
// Idempotent on purpose: it is the safety net, and a net that cannot be
|
||||||
// touched twice is not one.
|
// touched twice is not one.
|
||||||
function endPull(): void {
|
function endPull(): void {
|
||||||
dwellTimer.stop()
|
dwellTimer.stop()
|
||||||
rail.dwelled = false
|
rail.dwelled = false
|
||||||
GlobalStates.zonePullProgress = 0
|
// A settle is a release that is still travelling. Zeroing the clock
|
||||||
ViewtopControl.clearPose()
|
// or cancelling under it would be the second writer again — the
|
||||||
|
// animation is already carrying the window to a named destination
|
||||||
|
// and will commit when it arrives.
|
||||||
|
if (ZoneTransition.settling)
|
||||||
|
return
|
||||||
|
ZoneTransition.rest()
|
||||||
|
if (ZoneTransition.inFlight)
|
||||||
|
ZoneTransition.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
onReleased: mouse => {
|
onReleased: mouse => {
|
||||||
|
|
@ -542,9 +624,18 @@ PanelWindow {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Short of the first detent the destination is the app you were
|
// Short of the first detent the destination is the app you were
|
||||||
// already in — a commit like the others, not a pending state.
|
// already in — a commit like the others, not a pending state. And
|
||||||
// Restoring rides the `dragging` edge, so it happens whether or not
|
// it *travels* there: an abandoned half-swipe used to snap the
|
||||||
// this handler is the thing that ends the drag.
|
// window back from wherever the thumb left it, which is the same cut
|
||||||
|
// the commit paths had and the one felt most often, because a pull
|
||||||
|
// that changes its mind is the commonest gesture of all. `last_zone`
|
||||||
|
// settles to shift 0, so the window walks back down to full size.
|
||||||
|
//
|
||||||
|
// Only when there was something to carry. A tap, a sideways smudge
|
||||||
|
// 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")
|
||||||
rail.dragging = false
|
rail.dragging = false
|
||||||
|
|
||||||
// Downward: peel the nearest surface (keyboard, then dock).
|
// Downward: peel the nearest surface (keyboard, then dock).
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,6 @@
|
||||||
// group cards and is deliberately written down as approximate rather than
|
// group cards and is deliberately written down as approximate rather than
|
||||||
// presented as correct.
|
// presented as correct.
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Layouts
|
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Wayland
|
import Quickshell.Wayland
|
||||||
import qs
|
import qs
|
||||||
|
|
@ -128,14 +127,12 @@ Item {
|
||||||
// reason: *"no frame where a window is scaled by one and laid out by the
|
// reason: *"no frame where a window is scaled by one and laid out by the
|
||||||
// other."*
|
// other."*
|
||||||
readonly property bool open: GlobalStates.overviewOpen || GlobalStates.missionControlOpen
|
readonly property bool open: GlobalStates.overviewOpen || GlobalStates.missionControlOpen
|
||||||
property real progress: root.open
|
property real progress: root.open ? 1 : ZoneTransition.presence
|
||||||
? 1
|
|
||||||
: Math.min(1, GlobalStates.zonePullProgress)
|
|
||||||
Behavior on progress {
|
Behavior on progress {
|
||||||
// Only the settle is animated. Following the thumb is not an animation
|
// Only the settle is animated. Following the thumb is not an animation
|
||||||
// and must not be smoothed, or the cards lag the fingers that are
|
// and must not be smoothed, or the cards lag the fingers that are
|
||||||
// moving them.
|
// moving them.
|
||||||
enabled: root.open || GlobalStates.zonePullProgress === 0
|
enabled: root.open || ZoneTransition.travel === 0
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
duration: 260
|
duration: 260
|
||||||
easing.type: Easing.OutCubic
|
easing.type: Easing.OutCubic
|
||||||
|
|
@ -208,31 +205,47 @@ Item {
|
||||||
return Math.max(0, Math.min(1, (root.progress - start) / (1 - start)));
|
return Math.max(0, Math.min(1, (root.progress - start) / (1 - start)));
|
||||||
}
|
}
|
||||||
opacity: zoneCard.share
|
opacity: zoneCard.share
|
||||||
// The card is the window's size, at every point in the pull.
|
|
||||||
|
// No `scale` here, and that absence is the point of TASK-60.
|
||||||
//
|
//
|
||||||
// This read `0.6 + 0.4 * share`, which grows as the window shrinks:
|
// This used to read `1.0 - 0.4 * root.progress`, tuned to match the
|
||||||
// `pose` runs 1.0 -> 0.6 against the same progress, so at the handoff
|
// curve the rail was running through `pose` on the real windows.
|
||||||
// the window was at 0.6 of the glass and the card arrived near
|
// Two writers over one geometry: the numbers were made to agree by
|
||||||
// full-bleed. Two sizes for one object, which is the pop.
|
// hand, and at the handoff they did not — the window sat at 0.6 of
|
||||||
|
// the glass while the card arrived near full bleed. A transform one
|
||||||
|
// surface applies and another must remember to undo always has a
|
||||||
|
// path that forgets, and tapping a card was that path.
|
||||||
//
|
//
|
||||||
// Same curve as the rail's pose (`1.0 - 0.4 * progress`), so the
|
// The compositor now carries the real window onto `frame`'s rect
|
||||||
// card and the real window are the same rectangle throughout and the
|
// (`ZoneTransition`). A card that also scaled would be the second
|
||||||
// swap is invisible. It rests at 0.6 because that is where the pose
|
// writer all over again, so the card is chrome — the frame, the
|
||||||
// ends — nothing settles to a different size afterwards.
|
// label, the close control, the neighbours either side — and never
|
||||||
scale: 1.0 - 0.4 * root.progress
|
// a competing transform.
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
anchors.fill: parent
|
id: frame
|
||||||
// Room for the zone label; the sides and top are not inset.
|
// The zone, drawn small — the same rect the compositor is
|
||||||
anchors.bottomMargin: 34
|
// carrying this zone's windows onto, from the same arithmetic,
|
||||||
// The scale above is the inset now.
|
// because it *is* the same arithmetic. `ZoneTransition` owns it;
|
||||||
|
// nothing here recomputes a card size.
|
||||||
//
|
//
|
||||||
// 26/12 of hard margin on top of a 0.6 scale insets the card
|
// Keeping the zone's aspect is not a taste call: `carried` takes
|
||||||
// twice, and the second one is what stopped it lining up with
|
// its scale from the width alone and applies it uniformly, so a
|
||||||
// the window — it also squashed the aspect, since the bottom
|
// card of any other shape would letterbox the live window inside
|
||||||
// label margin has no counterpart at the top. A card at 0.6 of
|
// the card it is supposed to be.
|
||||||
// the glass already leaves both neighbours showing, which is the
|
//
|
||||||
// only thing the inset was ever for.
|
// Centred in the delegate, which leaves both neighbours showing
|
||||||
|
// through — full bleed reads as "you are looking at that app"
|
||||||
|
// rather than "here are the places you can go".
|
||||||
|
// The list is inset by `listMargin` inside this surface, so the
|
||||||
|
// delegate's origin is that much in from the panel's. Both
|
||||||
|
// offsets come off the shared rect rather than being re-derived,
|
||||||
|
// so the card and the carry stay the same rectangle even if the
|
||||||
|
// frame constants move.
|
||||||
|
x: ZoneTransition.cardX - ZoneTransition.listMargin
|
||||||
|
y: ZoneTransition.cardY - ZoneTransition.listMargin
|
||||||
|
width: ZoneTransition.cardWidth
|
||||||
|
height: ZoneTransition.cardHeight
|
||||||
radius: 18
|
radius: 18
|
||||||
color: Appearance.colors.colLayer1
|
color: Appearance.colors.colLayer1
|
||||||
// The zone you are on is named by its frame rather than by
|
// The zone you are on is named by its frame rather than by
|
||||||
|
|
@ -244,48 +257,55 @@ Item {
|
||||||
: Appearance.colors.colLayer1Active
|
: Appearance.colors.colLayer1Active
|
||||||
clip: true
|
clip: true
|
||||||
|
|
||||||
// The windows of this zone, laid out the way the compositor
|
// The windows of this zone, at the compositor's own tiling
|
||||||
// tiles them: one fills, two split top and bottom. Not a
|
// scaled by the card's own factor — not a layout that guesses
|
||||||
// guess — `placement.rs` splits on the short axis for a phone,
|
// at it.
|
||||||
// and a preview that disagreed with the real layout would make
|
//
|
||||||
// the overview a picture of a desktop that does not exist.
|
// This was a `ColumnLayout` with an 8 px margin, which was a
|
||||||
ColumnLayout {
|
// second opinion about where a window sits inside its zone: one
|
||||||
anchors.fill: parent
|
// fills, two split, and a margin nudges both off. Now each pane
|
||||||
anchors.margins: 8
|
// is the window's reported rect through `cardScale`, so the card
|
||||||
spacing: 8
|
// is a photograph of the zone rather than a reconstruction of
|
||||||
visible: zoneCard.modelData.windows.length > 0
|
// it — and the live window the compositor carries onto this same
|
||||||
|
// rect lands exactly on its own picture.
|
||||||
|
//
|
||||||
|
// `at` is zone-local (measured 2026-08-07: two windows on
|
||||||
|
// different zones both report `x: 0` with the second zone in
|
||||||
|
// front), so this one formula is right for every card, not just
|
||||||
|
// the one in front.
|
||||||
Repeater {
|
Repeater {
|
||||||
model: zoneCard.modelData.windows
|
model: zoneCard.modelData.windows
|
||||||
|
|
||||||
delegate: Item {
|
delegate: Item {
|
||||||
id: pane
|
id: pane
|
||||||
required property var modelData
|
required property var modelData
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.fillHeight: true
|
x: (pane.modelData.at?.x ?? 0) * ZoneTransition.cardScale
|
||||||
|
y: (pane.modelData.at?.y ?? 0) * ZoneTransition.cardScale
|
||||||
|
width: (pane.modelData.size?.width ?? 0) * ZoneTransition.cardScale
|
||||||
|
height: (pane.modelData.size?.height ?? 0) * ZoneTransition.cardScale
|
||||||
|
|
||||||
readonly property var toplevel: root.toplevelFor(pane.modelData)
|
readonly property var toplevel: root.toplevelFor(pane.modelData)
|
||||||
// A window that has left its zone's confinement.
|
// A window that has left its zone's confinement. Said
|
||||||
// Said on the card because a float is precisely the
|
// on the card because a float is precisely the window
|
||||||
// window you can no longer find by remembering
|
// you can no longer find by remembering which zone you
|
||||||
// which zone you put it on.
|
// put it on.
|
||||||
readonly property bool floating: pane.modelData.floating === true
|
readonly property bool floating: pane.modelData.floating === true
|
||||||
|
|
||||||
ScreencopyView {
|
ScreencopyView {
|
||||||
id: shot
|
id: shot
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
// Never captures behind a closed overview, and
|
// Never captures behind a closed overview, and only
|
||||||
// only *keeps* capturing on the card being
|
// *keeps* capturing on the card being looked at. The
|
||||||
// looked at. The others hold the frame they
|
// others hold the frame they arrived with, which is
|
||||||
// arrived with, which is what a card off to the
|
// what a card off to the side is worth.
|
||||||
// side is worth.
|
|
||||||
captureSource: root.progress > 0 ? pane.toplevel : null
|
captureSource: root.progress > 0 ? pane.toplevel : null
|
||||||
live: root.progress > 0 && zoneCard.resting
|
live: root.progress > 0 && zoneCard.resting
|
||||||
visible: pane.toplevel !== null
|
visible: pane.toplevel !== null
|
||||||
}
|
}
|
||||||
|
|
||||||
// The join failed. Said out loud, because a blank
|
// The join failed. Said out loud, because a blank card
|
||||||
// card and a broken overview must not look alike.
|
// and a broken overview must not look alike.
|
||||||
StyledText {
|
StyledText {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
visible: pane.toplevel === null
|
visible: pane.toplevel === null
|
||||||
|
|
@ -318,7 +338,6 @@ Item {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Home, and any zone that is empty. Home is empty on purpose —
|
// Home, and any zone that is empty. Home is empty on purpose —
|
||||||
// it is the widget space — so this is a destination, not a
|
// it is the widget space — so this is a destination, not a
|
||||||
|
|
@ -341,8 +360,11 @@ Item {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Under the card, not under the delegate: the frame is a fixed rect
|
||||||
|
// now rather than the delegate less a bottom margin, so the label
|
||||||
|
// follows the thing it names.
|
||||||
StyledText {
|
StyledText {
|
||||||
anchors.bottom: parent.bottom
|
anchors.top: frame.bottom
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
anchors.margins: 6
|
anchors.margins: 6
|
||||||
|
|
|
||||||
|
|
@ -342,52 +342,39 @@ Singleton {
|
||||||
root.scene("drop", {});
|
root.scene("drop", {});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scale every window on the zone in front, live, as a drag progresses.
|
// The overview carry: the compositor takes the real windows and puts them
|
||||||
|
// on their cards.
|
||||||
//
|
//
|
||||||
// This is the iOS/Android "the app shrinks into a card while your thumb
|
// These replace `poseActiveZone` / `clearPose` / `_posed`, which were the
|
||||||
// climbs" feel, and it is the first thing in the tree to actually drive
|
// shell's half of TASK-60's two-writer bug — the rail scaled the windows
|
||||||
// `pose`. The verb and its inverse input-mapping have been implemented and
|
// here while `ZoneOverview` scaled its cards independently, and a transform
|
||||||
// tested in the compositor for weeks with nothing calling them, which is
|
// the shell applied is a transform some path out of the gesture has to
|
||||||
// the whole reason the behaviour has never been seen on the device.
|
// remember to undo. There was always a path that forgot, and tapping a card
|
||||||
|
// was it.
|
||||||
//
|
//
|
||||||
// Throttled on the value, not on a timer: a drag emits a motion event per
|
// What makes this different is not that it is tidier: the transform is
|
||||||
// frame and each one would otherwise be a socket round-trip. Anything
|
// released *by the compositor*, on `commit` or `cancel`, and there is no
|
||||||
// smaller than a percent of scale is invisible and not worth a write.
|
// third way for a carry to end. A window cannot outlive the gesture that
|
||||||
property real _lastPose: 1.0
|
// carried it.
|
||||||
|
//
|
||||||
// Every id this has actually posed, so the restore is not at the mercy of
|
// `ZoneTransition` is the one caller. It owns the rects, the in-flight
|
||||||
// what the 2 s poll happens to be reporting when the drag ends.
|
// flag, and the end-target table; this is only the door.
|
||||||
property var _posed: ({})
|
function overviewBegin(targets) {
|
||||||
|
root._send({ op: "scene", intent: "overview_begin", to: targets });
|
||||||
function poseActiveZone(scale) {
|
|
||||||
if (Math.abs(scale - root._lastPose) < 0.01)
|
|
||||||
return;
|
|
||||||
root._lastPose = scale;
|
|
||||||
for (const w of root.windows) {
|
|
||||||
if (w.workspace === root.activeZone) {
|
|
||||||
root.pose(w.id, scale, 0.0, 0.5, 0.5);
|
|
||||||
root._posed[w.id] = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Put them back. Called on every path out of a drag — commit, abandon and
|
function overviewProgress(shift) {
|
||||||
// cancel — because a window left posed by a gesture that ended is a window
|
root._send({ op: "scene", intent: "overview_progress", progress: shift });
|
||||||
// the user cannot restore without knowing a verb exists.
|
}
|
||||||
// Restore what was posed, plus whatever is currently known. Going home from
|
|
||||||
// a pull left the app shrunk until the pill was dragged back down: the zone
|
// `target` is the wire's `EndTarget`: "home", "overview", "last_zone", or
|
||||||
// change moves the strip, and a window the poll no longer names was never
|
// {zone: {zone: N}}.
|
||||||
// sent its 1.0 — a window nobody can restore without knowing a verb exists.
|
function overviewCommit(target) {
|
||||||
function clearPose() {
|
root._send({ op: "scene", intent: "overview_commit", target: target });
|
||||||
root._lastPose = 1.0;
|
}
|
||||||
const ids = new Set();
|
|
||||||
for (const id of Object.keys(root._posed))
|
function overviewCancel() {
|
||||||
ids.add(Number(id));
|
root._send({ op: "scene", intent: "overview_cancel" });
|
||||||
for (const w of root.windows)
|
|
||||||
ids.add(w.id);
|
|
||||||
for (const id of ids)
|
|
||||||
root.pose(id, 1.0, 0.0, 0.5, 0.5);
|
|
||||||
root._posed = ({});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function raise(id) {
|
function raise(id) {
|
||||||
|
|
|
||||||
427
surfaces/quickshell/services/ZoneTransition.qml
Normal file
427
surfaces/quickshell/services/ZoneTransition.qml
Normal file
|
|
@ -0,0 +1,427 @@
|
||||||
|
// The multitasking transition, and the one thing that owns it.
|
||||||
|
//
|
||||||
|
// TASK-60. The fault this replaces was **two writers over one geometry**: the
|
||||||
|
// rail scaled the real windows through `pose` while `ZoneOverview` scaled its
|
||||||
|
// cards through its own `scale`, both reading `zonePullProgress` and running
|
||||||
|
// opposite curves. At the handoff the window was at 0.6 of the glass and the
|
||||||
|
// card arrived near full bleed. Every symptom — the size pop, the doubled
|
||||||
|
// frame, and the strand — was that one fault.
|
||||||
|
//
|
||||||
|
// So there is one owner and it is this file. It holds:
|
||||||
|
//
|
||||||
|
// 1. **The card rect**, computed once here and read by both ends. `ZoneOverview`
|
||||||
|
// lays its card out at `cardRect`; the carry names `cardRect` as the
|
||||||
|
// destination. They cannot disagree, because there is nothing to keep in
|
||||||
|
// agreement — it is the same number.
|
||||||
|
// 2. **The four verbs**, so the compositor carries the *real* window onto that
|
||||||
|
// rect. Nothing in the shell transforms a window any more.
|
||||||
|
// 3. **The end-target table**, so arriving somewhere is one function with one
|
||||||
|
// branch per destination rather than a set of booleans each caller sets in
|
||||||
|
// its own order.
|
||||||
|
//
|
||||||
|
// ## Releasing is arriving
|
||||||
|
//
|
||||||
|
// quickstep's `GestureState` resolves every swipe to exactly one of
|
||||||
|
// `HOME / RECENTS / NEW_TASK / LAST_TASK / ALL_APPS`, and that is structurally
|
||||||
|
// why Android cannot strand a window and we could. The old shape was a
|
||||||
|
// transform one surface applied and a *different* surface had to remember to
|
||||||
|
// undo — so tapping a card, which never touches the rail, left a window shrunk
|
||||||
|
// with no way back (Casey's strand repro: btop, multitasking, Home,
|
||||||
|
// multitasking, tap btop, and btop is a small card in the middle of the
|
||||||
|
// screen).
|
||||||
|
//
|
||||||
|
// Here the transform is released by `commit`/`cancel` inside the compositor,
|
||||||
|
// and *every* exit is one of those two. `LastZone` is a destination with a
|
||||||
|
// name, not an abandon branch, for the same reason.
|
||||||
|
//
|
||||||
|
// ## Why the geometry is a scaled zone and not a rectangle that looks nice
|
||||||
|
//
|
||||||
|
// `Viewtop::carried` takes its scale from the width alone
|
||||||
|
// (`leg.size.width / size.width`) and applies it uniformly. A destination rect
|
||||||
|
// of some other aspect would therefore letterbox the real window inside the
|
||||||
|
// card it is supposed to *be*. So a card is the whole zone drawn small — one
|
||||||
|
// scale factor, origin included — and a window's target is its own position
|
||||||
|
// and size run through that same factor. The window lands exactly where the
|
||||||
|
// card's picture of it is, by construction rather than by tuning.
|
||||||
|
//
|
||||||
|
// ## It is a verb, because she gives the tour
|
||||||
|
//
|
||||||
|
// Casey, 2026-08-07: an agent asked for a tour of the phone means *"even these
|
||||||
|
// little gesture steps will be possible."* `shift` and the end target are both
|
||||||
|
// reachable from the compositor's verb table, and `Overview.qml`'s `swipe` IPC
|
||||||
|
// drives this whole file without a finger. An agent-driven run stays
|
||||||
|
// `Origin::Agent` throughout — the compositor never sees a contact — so it
|
||||||
|
// raises no evidence and buys no idle budget, which is the rule TASK-60 sets
|
||||||
|
// for the tour and `input.rs` already enforces for her hand.
|
||||||
|
pragma Singleton
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import qs
|
||||||
|
import qs.services
|
||||||
|
|
||||||
|
Singleton {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
// --- The geometry, defined once ------------------------------------------
|
||||||
|
//
|
||||||
|
// Logical pixels, in the panel's coordinates — which are the output's,
|
||||||
|
// because the overview's `PanelWindow` is anchored to all four edges. The
|
||||||
|
// compositor's `at`/`size` are in the same space, so a rect computed here
|
||||||
|
// is directly a `TransitionTarget` with no conversion to get wrong.
|
||||||
|
//
|
||||||
|
// `Quickshell.screens[0]`, not a window's `screen`: this is a singleton and
|
||||||
|
// has no window, and the phone has one output. The fallbacks are the
|
||||||
|
// Pixel's logical size so a first frame before the screen list populates
|
||||||
|
// lays out at the right scale rather than at zero.
|
||||||
|
readonly property var screen: Quickshell.screens.length > 0 ? Quickshell.screens[0] : null
|
||||||
|
readonly property real panelWidth: root.screen ? root.screen.width : 540
|
||||||
|
readonly property real panelHeight: root.screen ? root.screen.height : 1080
|
||||||
|
|
||||||
|
// The overview surface's *measured* geometry, reported by the surface
|
||||||
|
// itself. Assuming it equalled the screen was wrong and wrong in the way
|
||||||
|
// that hides: the overview `PanelWindow` respects exclusive zones, so the
|
||||||
|
// status bar's 40 px makes it 1040 tall on a 1080 screen and puts its origin
|
||||||
|
// 40 px down the output. Computing the card against the raw screen made it
|
||||||
|
// about 4% too large and 40 px low — close enough to look right in a still
|
||||||
|
// frame and wrong in exactly the way that reads as a bad hand-off in motion.
|
||||||
|
//
|
||||||
|
// Measured 2026-08-07: `loaderPos.h = 811.2`, which is `1040 * 0.78`, not
|
||||||
|
// `1080 * 0.78`. The surface knows; nothing here should guess.
|
||||||
|
//
|
||||||
|
// `surfaceTop` is the output-space y of the surface's origin. Derived from
|
||||||
|
// what the surface lost to exclusive zones, which is top-anchored here (the
|
||||||
|
// bar reserves, the pill reserves nothing, the dock is on Overlay). It
|
||||||
|
// should equal the `at.y` the compositor reports for any tiled window — a
|
||||||
|
// free cross-check if this is ever suspected.
|
||||||
|
property real surfaceWidth: root.panelWidth
|
||||||
|
property real surfaceHeight: root.panelHeight
|
||||||
|
property real surfaceTop: 0
|
||||||
|
property real surfaceLeft: 0
|
||||||
|
|
||||||
|
// Report the overview surface's real geometry. Called by the surface; the
|
||||||
|
// only writer of these four.
|
||||||
|
function measuredAt(left, top, width, height) {
|
||||||
|
root.surfaceLeft = left;
|
||||||
|
root.surfaceTop = top;
|
||||||
|
root.surfaceWidth = Math.max(1, width);
|
||||||
|
root.surfaceHeight = Math.max(1, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
// `ZoneOverview`'s own frame, restated here because the destination and the
|
||||||
|
// layout have to be the same arithmetic. Changing one of these moves the
|
||||||
|
// card and the window it carries together; that is the point.
|
||||||
|
//
|
||||||
|
// `listMargin` is the `ListView`'s inset, `labelHeight` the strip under the
|
||||||
|
// card that names the zone. The loader's height is reported rather than
|
||||||
|
// derived from a share, for the reason above.
|
||||||
|
readonly property real listMargin: 12
|
||||||
|
readonly property real labelHeight: 34
|
||||||
|
|
||||||
|
// 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
|
||||||
|
|
||||||
|
// 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)))
|
||||||
|
|
||||||
|
// 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
|
||||||
|
|
||||||
|
// Where one window goes: its own place in the zone, drawn small.
|
||||||
|
//
|
||||||
|
// `cardX`/`cardY` are surface-local because that is what `ZoneOverview` lays
|
||||||
|
// out in; the compositor speaks output coordinates, so the surface's origin
|
||||||
|
// 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.
|
||||||
|
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
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
width: w.size.width * root.cardScale,
|
||||||
|
height: w.size.height * root.cardScale
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- One clock, two strategies -------------------------------------------
|
||||||
|
//
|
||||||
|
// The gesture reports one number: how far the thumb has climbed. Everything
|
||||||
|
// visible is a curve over it, and every curve lives here.
|
||||||
|
//
|
||||||
|
// This is TASK-52's rule — *one attention model, one clock, effects as
|
||||||
|
// strategies over it* — and it is the same rule as TASK-60's "one owner",
|
||||||
|
// one level up. The carry's `shift` and the destination's `presence` are
|
||||||
|
// genuinely different curves: the window keeps travelling as the climb
|
||||||
|
// continues past the multitasking detent toward home, while the destination
|
||||||
|
// has to *recede*, because a preview that stayed would be showing somewhere
|
||||||
|
// the release is no longer going to take you. Two curves is correct. Two
|
||||||
|
// *places* is the bug, and it is exactly how the original was built — a
|
||||||
|
// scale on the rail and a scale on the card, tuned by hand to agree.
|
||||||
|
//
|
||||||
|
// `pose` is a gravity well borrowed from the atmosphere primitives, so this
|
||||||
|
// is her felt environment and not merely navigation chrome. It answers to
|
||||||
|
// the same discipline.
|
||||||
|
|
||||||
|
// How far the thumb has climbed, and where the multitasking detent sits.
|
||||||
|
// The rail owns the gesture's geometry and reports both; nothing here
|
||||||
|
// measures a finger.
|
||||||
|
property real travel: 0
|
||||||
|
property real detent: 1
|
||||||
|
|
||||||
|
// The clock. 0 at rest, 1 at the multitasking detent, and it keeps counting
|
||||||
|
// past it — the climb toward home is more of the same motion, not a second
|
||||||
|
// 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 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
|
||||||
|
// symmetric and a home-bound pull never arrives at a multitasking view the
|
||||||
|
// release would not commit.
|
||||||
|
readonly property real presence: root.clock <= 1
|
||||||
|
? Math.max(0, root.clock)
|
||||||
|
: Math.max(0, 1 - (root.clock - 1))
|
||||||
|
|
||||||
|
// Advance the gesture. One call per motion event.
|
||||||
|
function pullTo(travel, detent) {
|
||||||
|
root.detent = Math.max(1, detent);
|
||||||
|
root.travel = Math.max(0, travel);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Back to rest. The chrome settles through its own Behavior; the carry is
|
||||||
|
// released by whatever destination the gesture arrived at.
|
||||||
|
function rest() {
|
||||||
|
root.travel = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The compositor is written from the derived value, not from the callers.
|
||||||
|
//
|
||||||
|
// A level, not an edge: `travel` moves under a thumb *and* under the settle
|
||||||
|
// animation below, and both must reach the glass. Pushing from each caller
|
||||||
|
// instead would mean the settle silently did nothing — which is precisely
|
||||||
|
// the class of bug where a transform is applied by one path and undone by
|
||||||
|
// another that forgot.
|
||||||
|
onShiftChanged: root.progress(root.shift)
|
||||||
|
|
||||||
|
// --- The settle ----------------------------------------------------------
|
||||||
|
//
|
||||||
|
// **Releasing continues the motion.** This is the piece that was missing,
|
||||||
|
// and it is why the swipe into multitasking read as awkward no matter how
|
||||||
|
// the fades were tuned: a lift at 60% of the climb committed *at* 60%, so
|
||||||
|
// the window jumped from wherever the thumb left it to its final state with
|
||||||
|
// no travel in between. Two motions with a cut between them.
|
||||||
|
//
|
||||||
|
// quickstep does not do that. `AbsSwipeUpHandler.handleNormalGestureEnd`:
|
||||||
|
//
|
||||||
|
// float endShift = endTarget.isLauncher ? 1 : 0;
|
||||||
|
// long expectedDuration = Math.abs(Math.round((endShift - currentShift)
|
||||||
|
// * MAX_SWIPE_DURATION * SWIPE_DURATION_MULTIPLIER));
|
||||||
|
// duration = Math.min(MAX_SWIPE_DURATION, expectedDuration);
|
||||||
|
// startShift = currentShift;
|
||||||
|
//
|
||||||
|
// The shift animates from where the thumb left it to where the destination
|
||||||
|
// is, over a duration proportional to the distance still to cover, and the
|
||||||
|
// gesture lands only when it gets there. Same numbers here: 350 ms cap, and
|
||||||
|
// the multiplier is `min(1/0.7, 1/0.3)` from `MIN_PROGRESS_FOR_OVERVIEW`.
|
||||||
|
//
|
||||||
|
// `last_zone` is the one target whose end shift is 0 — going back to the app
|
||||||
|
// 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))
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Release the gesture at a named destination, travelling there first.
|
||||||
|
function settleTo(target) {
|
||||||
|
const endShift = (target === "last_zone") ? 0 : 1;
|
||||||
|
const from = root.shift;
|
||||||
|
settleAnim.stop();
|
||||||
|
root._settleTarget = target;
|
||||||
|
root.settling = true;
|
||||||
|
settleAnim.from = root.travel;
|
||||||
|
settleAnim.to = endShift * root.detent;
|
||||||
|
settleAnim.duration = Math.min(root.maxSwipeDuration,
|
||||||
|
Math.abs(Math.round((endShift - from)
|
||||||
|
* root.maxSwipeDuration * root.swipeDurationMultiplier)));
|
||||||
|
settleAnim.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- The carry -----------------------------------------------------------
|
||||||
|
|
||||||
|
// True between `begin` and the `commit`/`cancel` that releases it. Held so
|
||||||
|
// a progress update with nothing in flight is silence rather than a refusal
|
||||||
|
// per motion event — the compositor is right to refuse it, and a drag emits
|
||||||
|
// 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.
|
||||||
|
property real _lastShift: -1
|
||||||
|
|
||||||
|
// Take the windows on the zone in front and name where each is going.
|
||||||
|
//
|
||||||
|
// The active zone only, which is quickstep's model too: `TaskViewSimulator`
|
||||||
|
// transforms the *live* window of the running task onto its card, and every
|
||||||
|
// other task in the strip is a snapshot. Carrying all of them would also be
|
||||||
|
// wrong here for a concrete reason — the rects are named once at `begin`,
|
||||||
|
// and the strip scrolls, so a window carried onto a card that then slides
|
||||||
|
// sideways would come adrift from it.
|
||||||
|
//
|
||||||
|
// An empty zone carries nothing and says so: home has no windows, and the
|
||||||
|
// compositor refuses an empty `begin` on purpose ("a carry with no windows
|
||||||
|
// would release nothing on commit"). The gesture still works — the overview
|
||||||
|
// is drawn, there is simply no window to bring with it.
|
||||||
|
function begin() {
|
||||||
|
const targets = [];
|
||||||
|
for (const w of ViewtopControl.windows) {
|
||||||
|
if (w.workspace === ViewtopControl.activeZone && w.at && w.size)
|
||||||
|
targets.push(root.targetFor(w));
|
||||||
|
}
|
||||||
|
if (targets.length === 0)
|
||||||
|
return false;
|
||||||
|
root.inFlight = true;
|
||||||
|
root._lastShift = -1;
|
||||||
|
ViewtopControl.overviewBegin(targets);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 0 is where the windows live, 1 is each on its card.
|
||||||
|
function progress(shift) {
|
||||||
|
if (!root.inFlight)
|
||||||
|
return;
|
||||||
|
const s = Math.max(0, Math.min(1, shift));
|
||||||
|
if (Math.abs(s - root._lastShift) < 0.01)
|
||||||
|
return;
|
||||||
|
root._lastShift = s;
|
||||||
|
ViewtopControl.overviewProgress(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finish at a named destination: release the carry, then go there.
|
||||||
|
//
|
||||||
|
// Both halves, always, and in that order. The compositor releases the
|
||||||
|
// transforms and reports back where it was told to arrive, but it does not
|
||||||
|
// navigate — performing the arrival is the caller's, which is this. Doing
|
||||||
|
// it here rather than at each call site is what makes a card tap and a pill
|
||||||
|
// release the same machinery, which is precisely what the strand repro
|
||||||
|
// broke.
|
||||||
|
//
|
||||||
|
// Safe with nothing in flight: a card tapped from an already-open overview
|
||||||
|
// has no carry to release, and still has somewhere to go.
|
||||||
|
function commit(target) {
|
||||||
|
if (root.inFlight) {
|
||||||
|
root.inFlight = false;
|
||||||
|
root._lastShift = -1;
|
||||||
|
ViewtopControl.overviewCommit(target);
|
||||||
|
}
|
||||||
|
root.arrive(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Give up. Named apart from `commit("last_zone")` so the caller says which
|
||||||
|
// one it meant, matching the compositor's own `cancel`.
|
||||||
|
function cancel() {
|
||||||
|
if (root.inFlight) {
|
||||||
|
root.inFlight = false;
|
||||||
|
root._lastShift = -1;
|
||||||
|
ViewtopControl.overviewCancel();
|
||||||
|
}
|
||||||
|
root.arrive("last_zone");
|
||||||
|
}
|
||||||
|
|
||||||
|
// The end-target table: one branch per destination, and every gesture
|
||||||
|
// resolves to exactly one of them.
|
||||||
|
//
|
||||||
|
// `target` is the wire's own shape — `"home"`, `"overview"`, `"last_zone"`,
|
||||||
|
// or `{zone: {zone: N}}` — so the thing sent to the compositor and the
|
||||||
|
// thing branched on here cannot drift apart into two vocabularies.
|
||||||
|
function arrive(target) {
|
||||||
|
if (target === "home") {
|
||||||
|
// Square one, with the last screen's furniture cleared before the
|
||||||
|
// strip moves rather than arrived at still wearing it.
|
||||||
|
GlobalStates.missionControlOpen = false;
|
||||||
|
GlobalStates.overviewOpen = false;
|
||||||
|
GlobalStates.dockRevealed = false;
|
||||||
|
GlobalStates.oskOpen = false;
|
||||||
|
// `homeZone`, not a config lookup and not the literal 1: home is
|
||||||
|
// zone 0 (`workspace::HOME_ZONE`), and pointing this at 1 put Home
|
||||||
|
// on the first *app* zone — invisible while only home existed,
|
||||||
|
// because the compositor clamps `to` against `count - 1`.
|
||||||
|
ViewtopControl.zone(ViewtopControl.homeZone);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (target === "overview") {
|
||||||
|
GlobalStates.missionControlOpen = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (target === "last_zone") {
|
||||||
|
// Already there. The windows went back where they live when the
|
||||||
|
// compositor released them, and that is the whole of it — this
|
||||||
|
// branch exists so that "the swipe was given up" is a destination
|
||||||
|
// with a name rather than the one path nobody wrote.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (target && target.zone !== undefined) {
|
||||||
|
ViewtopControl.zone(target.zone.zone);
|
||||||
|
GlobalStates.missionControlOpen = false;
|
||||||
|
GlobalStates.overviewOpen = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log("[ZoneTransition] arrival with no destination:", JSON.stringify(target));
|
||||||
|
}
|
||||||
|
|
||||||
|
// A zone, in the wire's shape. Spelled once so no call site hand-builds the
|
||||||
|
// nesting and gets it subtly wrong.
|
||||||
|
function zoneTarget(zone) {
|
||||||
|
return { zone: { zone: zone } };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -69,3 +69,4 @@ singleton WallpaperDownload 1.0 WallpaperDownload.qml
|
||||||
singleton Wallpapers 1.0 Wallpapers.qml
|
singleton Wallpapers 1.0 Wallpapers.qml
|
||||||
singleton Weather 1.0 Weather.qml
|
singleton Weather 1.0 Weather.qml
|
||||||
singleton Ydotool 1.0 Ydotool.qml
|
singleton Ydotool 1.0 Ydotool.qml
|
||||||
|
singleton ZoneTransition 1.0 ZoneTransition.qml
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue