Watch
1
0
Fork
You've already forked souveraine
0

overview: draw the zone, opaque, with a tray under it

The card was the whole panel scaled, so the bar's reservation rode inside it as
dead space above every window. Subtracted in targetFor and the pane layout both,
or the carry lands a bar-height off its own picture. Minting a zone needed no new
verb: Request::Workspace already pushes one when asked for the zone past the end.
This commit is contained in:
Fimeg 2026-08-16 10:31:18 -04:00
commit f06bb8394e
2 changed files with 298 additions and 13 deletions

View file

@ -72,6 +72,22 @@ Scope { // Scope
}
function computeDockState() {
// Multitasking owns the screen. The dock is *home's* furniture, and
// home is one of the cards a dock over the strip is one destination's
// furniture drawn on top of the picker for all of them. Casey,
// 2026-08-16: *"remove the dock it should not be there."*
//
// First in the ladder, above the home check, because the gesture is
// most often made **from** home: `activeZone` is still 0 for the whole
// time the overview is up, so every rung below this returns PINNED.
//
// The peek counts. The destination draws from the first climbing pixel
// (`SystemGestureRail.peekWanted`), so the dock leaving as the cards
// arrive is one motion rather than two events.
if (GlobalStates.missionControlOpen || GlobalStates.overviewOpen
|| GlobalStates.missionPeek)
return Dock.DockState.Hidden;
// A visible keyboard owns the bottom edge. This must precede the home
// zone's unconditional PINNED return below; otherwise home stacks the
// dock's exclusive zone under the OSK and charges the display twice.

View file

@ -56,6 +56,7 @@
// presented as correct.
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import qs
import qs.services
@ -145,8 +146,10 @@ Item {
// what makes the surface correct on a phone whose compositor predates the
// push channel.
onOpenChanged: {
if (root.open)
if (root.open) {
ViewtopControl.refreshWindows();
list.seat();
}
}
// A peek draws these cards without ever setting `open`, so it owes the same
@ -162,18 +165,92 @@ Item {
implicitWidth: parent ? parent.width : 540
implicitHeight: parent ? parent.height : 800
// --- The line she speaks on ----------------------------------------------
//
// Deliberately **not** model-dependent. It carries a real, locally computed
// fact about what is open, and it is a place a model can later write to
// rather than a place that only exists once one can. Casey, 2026-08-16:
// *"It should sorta initially be less than model dependant but we'll build
// out that whole system."*
//
// Doctrine §13 is why the band is here at all: multitasking is the one
// surface that is purely about operation what is running, where it lives
// and operation is hers. A verb she cannot reach is a defect; a screen
// about her half of the device with nowhere for her to speak is the same
// defect wearing a different face.
property string spoken: ""
readonly property string note: {
if (root.spoken.length > 0)
return root.spoken;
const zones = Math.max(1, ViewtopControl.zoneCount);
const wins = ViewtopControl.windows.length;
const floats = ViewtopControl.windows.filter(w => w.floating === true).length;
const parts = [qsTr("%n zone(s)", "", zones), qsTr("%n window(s)", "", wins)];
if (floats > 0)
parts.push(qsTr("%n floating", "", floats));
return parts.join(" · ");
}
// `qs -c souveraine ipc call overviewNote say ""`. One verb, so the thing
// that writes here is a caller with a name rather than a global somebody
// sets from four places.
IpcHandler {
target: "overviewNote"
function say(text: string): void {
root.spoken = text;
}
function clear(): void {
root.spoken = "";
}
function state(): string {
return JSON.stringify({
spoken: root.spoken,
shown: root.note,
subject: root.subject ? root.subject.id : null,
restingZone: root.restingZone ? root.restingZone.zone : null
});
}
}
// The zone the strip has come to rest on, and the window inside it a verb
// would act on. Focused first, else the first one there a split has two
// and the tray has to name one of them without asking.
readonly property var restingZone: root.zones.length === 0 ? null
: root.zones[Math.max(0, Math.min(root.zones.length - 1, list.currentIndex))]
readonly property var subject: {
const z = root.restingZone;
if (!z || z.windows.length === 0)
return null;
for (const w of z.windows)
if (w.focused)
return w;
return z.windows[0];
}
ListView {
id: list
anchors.fill: parent
anchors.margins: 12
anchors.bottomMargin: 12 + ZoneTransition.trayHeight
transform: Translate { y: (1 - root.progress) * 24 }
model: root.zones
orientation: ListView.Horizontal
snapMode: ListView.SnapOneItem
highlightRangeMode: ListView.StrictlyEnforceRange
preferredHighlightBegin: 0
preferredHighlightEnd: width
spacing: 12
// Centred, not pinned left. The delegate is one card wide plus its
// gutter, so both neighbours show and the strip reads as cards you
// scroll between rather than screens you page through. `ZoneTransition`
// owns these numbers because it also owns where the carry lands.
preferredHighlightBegin: ZoneTransition.highlightBegin
preferredHighlightEnd: ZoneTransition.highlightBegin + ZoneTransition.delegateWidth
// The gutter is inside the delegate; two sources of separation would be
// two numbers to keep agreeing.
spacing: 0
clip: true
// Nothing off the sides is built. A card that is not in frame is a
// window capture nobody can see see the header.
@ -181,17 +258,40 @@ Item {
// Open on the zone you are actually on, so the first card is where you
// came from rather than wherever the strip happens to start.
Component.onCompleted: list.currentIndex =
Math.max(0, root.zones.findIndex(z => z.zone === ViewtopControl.activeZone))
//
// Re-seated on every model change, not set once at construction.
// `root.zones` rebuilds into a **new array** whenever the compositor's
// window list moves, a ListView resets `currentIndex` to 0 on a model
// change, and `onOpenChanged` asks for a refresh the instant this
// opens so the reset was guaranteed and the strip always came to rest
// on Home. Measured 2026-08-16: three zones, Firefox in front, and the
// card you were shown was the empty one.
//
// `callLater` so the delegates exist to be seated.
function seat(): void {
list.currentIndex = Math.max(0,
root.zones.findIndex(z => z.zone === ViewtopControl.activeZone));
}
onModelChanged: Qt.callLater(list.seat)
Component.onCompleted: list.seat()
delegate: Item {
id: zoneCard
required property var modelData
required property int index
width: list.width
width: ZoneTransition.delegateWidth
height: list.height
// How far this card is from the middle of the strip, in delegates.
// 0 is the one you are resting on.
readonly property real offCentre: {
const centre = list.contentX + list.width / 2;
const mine = zoneCard.x + zoneCard.width / 2;
return Math.abs(mine - centre)
/ Math.max(1, ZoneTransition.delegateWidth);
}
readonly property bool isHome: zoneCard.modelData.zone === ViewtopControl.homeZone
readonly property bool isActive: zoneCard.modelData.zone === ViewtopControl.activeZone
// The card the strip has come to rest on. This one, and only this
@ -204,9 +304,25 @@ Item {
const start = Math.min(zoneCard.index, 5) * 0.045;
return Math.max(0, Math.min(1, (root.progress - start) / (1 - start)));
}
opacity: zoneCard.share
// Depth, and only where it cannot become a second writer.
//
// A card off to the side sits back and dims, so the strip reads as
// cards floating at different distances rather than a filmstrip.
// **Gated on `inFlight`:** while the compositor is carrying a real
// window onto one of these rects, a card that also scales is
// exactly the fault TASK-60 was written to remove, so during a
// carry every card sits at its true rect and the depth eases in on
// the commit frame through the Behavior below.
readonly property real depth: ZoneTransition.inFlight
? 0 : Math.min(1.4, zoneCard.offCentre)
scale: 1 - 0.07 * zoneCard.depth
opacity: zoneCard.share * (1 - 0.4 * Math.min(1, zoneCard.depth))
Behavior on scale {
NumberAnimation { duration: 180; easing.type: Easing.OutCubic }
}
// No `scale` here, and that absence is the point of TASK-60.
// No `scale` tied to the *gesture*, and that absence is the point of
// TASK-60.
//
// This used to read `1.0 - 0.4 * root.progress`, tuned to match the
// curve the rail was running through `pose` on the real windows.
@ -242,12 +358,23 @@ Item {
// 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
x: ZoneTransition.cardInsetX
y: ZoneTransition.cardInsetY
width: ZoneTransition.cardWidth
height: ZoneTransition.cardHeight
radius: 18
color: Appearance.colors.colLayer1
// Opaque, deliberately, and the theme colour is not.
//
// The backdrop behind this is the wallpaper blurred which
// stays, it is the room these sit in but a translucent card
// let it through, so every card was a coloured smear of the
// wallpaper instead of a card with something on it. Measured
// 2026-08-16 on the Home card: the whole frame read as one
// pink-and-blue blur. Cards float *on* the room; they are not
// made of it.
color: Qt.rgba(Appearance.colors.colLayer1.r,
Appearance.colors.colLayer1.g,
Appearance.colors.colLayer1.b, 1)
// The zone you are on is named by its frame rather than by
// moving it: a card that jumps out of the row is a card whose
// neighbours shift under the thumb mid-swipe.
@ -280,8 +407,15 @@ Item {
id: pane
required property var modelData
// `contentTop` off the y before scaling: the card is a
// picture of the usable zone, so a window that starts
// below the bar's reservation starts at the card's top
// edge. `ZoneTransition.targetFor` makes the identical
// subtraction that is what puts the carried window
// exactly on its own picture.
x: (pane.modelData.at?.x ?? 0) * ZoneTransition.cardScale
y: (pane.modelData.at?.y ?? 0) * ZoneTransition.cardScale
y: ((pane.modelData.at?.y ?? 0) - ZoneTransition.contentTop)
* ZoneTransition.cardScale
width: (pane.modelData.size?.width ?? 0) * ZoneTransition.cardScale
height: (pane.modelData.size?.height ?? 0) * ZoneTransition.cardScale
@ -388,4 +522,139 @@ Item {
}
}
}
// --- The tray ------------------------------------------------------------
//
// A predicate, not a launcher. Every control here acts on the card in
// front Android's instinct with the screenshot button under recents, and
// the reason it reads as a sentence rather than a toolbar. An app grid
// would make this a second home screen; the drawer already owns that
// (TASK-14), and a verb in two places is two places to fix it.
//
// Screenshot is deliberately absent for the same reason: it is already in
// the right-hand sidebar. Casey noticed that before I did.
Item {
id: tray
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.margins: 12
height: ZoneTransition.trayHeight - 12
opacity: root.progress
transform: Translate { y: (1 - root.progress) * 24 }
Row {
id: verbs
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
spacing: 18
// Float is first because it is the verb this surface owes. A float
// has left its zone's confinement, so the strip stops counting it
// and it becomes the one window you cannot find by remembering
// where you left it which makes multitasking the only place it is
// findable, and therefore the only honest place to turn it off.
TrayVerb {
symbol: root.subject && root.subject.floating === true
? "picture_in_picture_off" : "picture_in_picture"
caption: root.subject && root.subject.floating === true
? qsTr("Unfloat") : qsTr("Float")
active: root.subject !== null
onTriggered: {
if (root.subject.floating === true)
ViewtopControl.unfloat(root.subject.id);
else
ViewtopControl.float(root.subject.id);
ViewtopControl.refreshWindows();
}
}
// Minting a zone has never had a control anywhere in this OS,
// because zones are born from need rather than created and that
// left "put this somewhere of its own" unreachable. The compositor
// has always answered it: `Request::Workspace` reads
// `Some(w) if w >= count => self.workspaces.push()`, so asking for
// the zone past the end *is* the mint. One button, no new verb.
TrayVerb {
symbol: "add_to_queue"
caption: qsTr("New zone")
active: root.subject !== null
&& root.restingZone.windows.length > 0
onTriggered: {
ViewtopControl.moveToZone(root.subject.id,
Math.max(1, ViewtopControl.zoneCount));
ViewtopControl.refreshWindows();
}
}
// Auxo's one contribution that everybody kept. `close` and not
// `kill`: this is a request the client may refuse or prompt on, and
// a row of cards is the wrong place to take that choice away.
// Force lives on the sheet, behind a hold, where it is deliberate.
TrayVerb {
symbol: "clear_all"
caption: qsTr("Close all")
active: root.restingZone !== null
&& root.restingZone.windows.length > 0
onTriggered: {
for (const w of root.restingZone.windows)
ViewtopControl.close(w.id);
}
}
}
StyledText {
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.bottomMargin: 4
horizontalAlignment: Text.AlignHCenter
elide: Text.ElideRight
opacity: root.spoken.length > 0 ? 0.95 : 0.55
text: root.note
}
}
// One verb in the tray: a symbol, a caption under it, and an off state that
// reads as unavailable rather than as broken. Local because it is the
// tray's own idiom and nothing else wants it yet; promote it to
// `common/widgets` the second something does.
component TrayVerb: Item {
id: verb
required property string symbol
required property string caption
property bool active: true
signal triggered
width: 84
height: 78
opacity: verb.active ? 1 : 0.35
RippleButton {
id: hit
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
implicitWidth: 52
implicitHeight: 52
buttonRadius: 26
enabled: verb.active
onClicked: verb.triggered()
contentItem: MaterialSymbol {
anchors.centerIn: parent
text: verb.symbol
iconSize: 24
}
}
StyledText {
anchors.top: hit.bottom
anchors.topMargin: 4
anchors.horizontalCenter: parent.horizontalCenter
width: parent.width
horizontalAlignment: Text.AlignHCenter
elide: Text.ElideRight
opacity: 0.8
text: verb.caption
}
}
}