diff --git a/surfaces/quickshell/modules/souveraine/windowSheet/WindowSheet.qml b/surfaces/quickshell/modules/souveraine/windowSheet/WindowSheet.qml index bf2c57f..7c6c0c5 100644 --- a/surfaces/quickshell/modules/souveraine/windowSheet/WindowSheet.qml +++ b/surfaces/quickshell/modules/souveraine/windowSheet/WindowSheet.qml @@ -59,6 +59,20 @@ Scope { // tapping Resize twice in two visits should continue the cycle, not restart // it and appear to do nothing. property int sizeStep: 0 + // Which pose Zoom last applied, on the scope for the same reason as + // `sizeStep`: the cycle has to survive the sheet closing. + property int poseStep: 0 + + // Whether the subject has left its zone's confinement, as the compositor + // last reported it. Read, never remembered — a shell that kept its own idea + // of this would draw Unfloat on a window something else had already put + // back. + readonly property bool targetFloating: { + for (const w of ViewtopControl.windows) + if (w.id === scope.target) + return w.floating === true; + return false; + } // How long a hold on Close means kill. Longer than a tap could ever be, // short enough that a stuck app does not feel like a negotiation. @@ -364,6 +378,59 @@ Scope { } } + SheetButton { + icon: "zoom_out_map" + label: "Zoom" + // The `pose` verb, reaching a window for the first time. + // + // `pose` has existed in the wire and the compositor since + // 2026-08-05 — it genuinely scales content, not only moves + // it — and **nothing in the shell has ever called it**. + // `poseActiveZone` did, and TASK-60 deleted that wrapper as + // the shell's half of the two-writer bug, which took the + // only caller with it. Casey, 2026-08-16: *"I can move + // windows but the zooming them in and out didn't work + // yet."* It did not work because nobody asked. + // + // Distinct from Resize, and the difference is load-bearing: + // Resize `place`s a different *rectangle* and the client + // relays out inside it. Zoom composes the pixels it already + // drew — the client is never told, its layout does not + // move, and the compositor maps input back through the + // inverse so a shrunken window is still touchable where it + // is drawn. + // + // Identity is a step in the cycle rather than a separate + // Reset: a pose the user cannot get out of with the button + // they got into it with is a trap. + onTapped: { + scope.poseStep = (scope.poseStep + 1) % 3; + const scale = [1.0, 0.85, 0.7][scope.poseStep]; + ViewtopControl.pose(scope.target, scale, 0.0, 0.5, 0.5); + } + } + + SheetButton { + icon: scope.targetFloating ? "picture_in_picture_off" + : "picture_in_picture" + label: scope.targetFloating ? "Unfloat" : "Float" + // A float leaves the zone's confinement — it stops being + // the zone's business, so the strip no longer counts it + // when deciding whether a zone still holds anything. + // + // Which is exactly why the label has to flip: a float is + // the one window you can no longer find by remembering + // which zone you left it on, so the way back out must be + // the same button, in the same place, saying so. + onTapped: { + if (scope.targetFloating) + ViewtopControl.unfloat(scope.target); + else + ViewtopControl.float(scope.target); + ViewtopControl.refreshWindows(); + } + } + SheetButton { icon: "splitscreen" label: "Split"