Watch
1
0
Fork
You've already forked souveraine
0

split chooser, home reaches the compositor, idle-inhibit comment was backwards

This commit is contained in:
Fimeg 2026-08-05 14:52:48 -04:00
commit b0688bc30b
4 changed files with 130 additions and 12 deletions

View file

@ -82,8 +82,18 @@ pub fn run(sink: Arc<dyn Fn(IdleEvent) + Send + Sync>) -> Result<()> {
); );
}; };
// `get_idle_notification` (not the input-ignoring variant): we want the // `get_idle_notification`, which **respects idle inhibitors** — not
// idle timer to be reset by real input only. // `get_input_idle_notification`, which ignores them and reports pure input
// silence. An earlier comment here had this backwards.
//
// The difference is video. Input alone cannot see a film playing: nobody
// touches the glass for two hours and the machine would blank on a screen
// someone is watching. A player takes a `zwp_idle_inhibitor_v1`, viewtop
// reports it through `set_is_inhibited`, and this notification simply never
// idles while it is held. So the answer to "does playback keep the phone
// awake" is yes, through the client's inhibitor rather than through faked
// input — which also means a client that lies is visible as an inhibitor
// rather than as phantom activity.
let _notification = notifier.get_idle_notification( let _notification = notifier.get_idle_notification(
QUIET_PERIOD.as_millis() as u32, QUIET_PERIOD.as_millis() as u32,
&seat, &seat,

View file

@ -230,8 +230,14 @@ PanelWindow {
GlobalStates.overviewOpen = false GlobalStates.overviewOpen = false
GlobalStates.dockRevealed = false GlobalStates.dockRevealed = false
GlobalStates.oskOpen = false GlobalStates.oskOpen = false
Quickshell.execDetached(["hyprctl", "dispatch", // The compositor's own verb, not `hyprctl dispatch` — that binary
"hl.dsp.focus({ workspace = 1 })"]) // 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.zone(Config.options.navigation?.homeZone ?? 1)
} }
// Commit an upward gesture. `stage` is the stage the drag settled on // Commit an upward gesture. `stage` is the stage the drag settled on

View file

@ -48,6 +48,8 @@ Scope {
// a blank sheet nobody can explain. // a blank sheet nobody can explain.
property int target: 0 property int target: 0
property bool sheetOpen: false property bool sheetOpen: false
// Split has pinned a window and is waiting for the other half to be filled.
property bool choosing: false
// How long a hold on Close means kill. Longer than a tap could ever be, // 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. // short enough that a stuck app does not feel like a negotiation.
@ -64,6 +66,7 @@ Scope {
function dismiss() { function dismiss() {
scope.sheetOpen = false; scope.sheetOpen = false;
scope.choosing = false;
scope.target = 0; scope.target = 0;
} }
@ -98,6 +101,12 @@ Scope {
// The window is gone or moved; the sheet has nothing left to be // The window is gone or moved; the sheet has nothing left to be
// about. Kept open for `pose`-style verbs would mean a sheet // about. Kept open for `pose`-style verbs would mean a sheet
// pointing at a window that is no longer where it was. // pointing at a window that is no longer where it was.
//
// Except mid-Split: that `place` is the *first half* of a flow, and
// dismissing on it would close the sheet before the chooser could
// ask which window fills the other half.
if (scope.choosing)
return;
if (intent === "close" || intent === "kill" || intent === "place") if (intent === "close" || intent === "kill" || intent === "place")
scope.dismiss(); scope.dismiss();
} }
@ -137,8 +146,68 @@ Scope {
Behavior on opacity { NumberAnimation { duration: 120 } } Behavior on opacity { NumberAnimation { duration: 120 } }
} }
// The chooser for Split's other half. Sits in the bottom half —
// where the window it is choosing for will land — so the gesture
// reads as "put something here" rather than as a menu that happens
// to be on screen.
ColumnLayout {
visible: scope.choosing
anchors.horizontalCenter: parent.horizontalCenter
y: parent.height * 0.58
spacing: 14
StyledText {
Layout.alignment: Qt.AlignHCenter
text: ViewtopControl.windows.length > 1
? "fill the other half"
: "nothing else is open"
color: "#e6ffffff"
font.pixelSize: Appearance.font.pixelSize.normal
}
Repeater {
model: ViewtopControl.windows.filter(w => w.id !== scope.target)
delegate: Rectangle {
required property var modelData
Layout.alignment: Qt.AlignHCenter
implicitWidth: 220
implicitHeight: 56
radius: 14
color: pick.pressed
? Appearance.colors.colLayer2
: Appearance.colors.colLayer1
border.width: 1
border.color: Appearance.colors.colLayer1Active
StyledText {
anchors.centerIn: parent
// The compositor answers with ids, not titles —
// `workspaces` carries {id, workspace}. A title
// needs foreign-toplevel, which is a separate
// lookup; showing the id is honest until then.
text: "window " + parent.modelData.id
color: Appearance.colors.colOnLayer1
font.pixelSize: Appearance.font.pixelSize.small
}
MouseArea {
id: pick
anchors.fill: parent
onClicked: {
ViewtopControl.place(parent.modelData.id, 0,
win.height / 2, win.width, win.height / 2);
scope.choosing = false;
scope.dismiss();
}
}
}
}
}
// The row. Above centre so a thumb does not cover the window. // The row. Above centre so a thumb does not cover the window.
RowLayout { RowLayout {
visible: !scope.choosing
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
y: parent.height * 0.38 y: parent.height * 0.38
spacing: 28 spacing: 28
@ -159,16 +228,18 @@ Scope {
SheetButton { SheetButton {
icon: "splitscreen" icon: "splitscreen"
label: "Split" label: "Split"
// Split pins this window to one half and the other half // Pin this window to the top half, then offer the other
// wants a chooser — open apps from the overview, or the // half to something already open. Casey's shape: "the
// app list. That chooser is the missing half and it has no // second waiting one possibly having a button to add an
// verb yet, so this places the window and says so rather // already opened one from the multitasking section".
// than pretending the flow is finished. //
// The list is queried, not assumed — a chooser offering a
// window that has since closed would `place` a dead id and
// refuse, which reads as the button being broken.
onTapped: { onTapped: {
ViewtopControl.place(scope.target, 0, 0, win.width, win.height / 2); ViewtopControl.place(scope.target, 0, 0, win.width, win.height / 2);
refusal.text = "pinned to the top half — chooser for the other half is not built yet"; ViewtopControl.refreshWindows();
refusal.opacity = 1; scope.choosing = true;
refusalFade.restart();
} }
} }

View file

@ -38,6 +38,17 @@ Singleton {
signal refused(string intent, string reason) signal refused(string intent, string reason)
signal succeeded(string intent) signal succeeded(string intent)
// What the compositor last said is on the canvas: `[{id, workspace}, …]`.
// Queried, never cached across opens — a chooser showing a window that
// closed a minute ago is worse than one that takes a moment to fill.
property var windows: []
signal windowsChanged_()
// Ask what is open. Answers into `windows`.
function refreshWindows() {
root._send({ op: "workspaces" });
}
// Requests waiting on a connection. A queue rather than SessiondPolicy's // Requests waiting on a connection. A queue rather than SessiondPolicy's
// single slot: the sheet can fire two verbs in a row (kill after a close // single slot: the sheet can fire two verbs in a row (kill after a close
// that was refused), and dropping the second would be silent. // that was refused), and dropping the second would be silent.
@ -114,6 +125,19 @@ Singleton {
root.scene("raise", { id: id }); root.scene("raise", { id: id });
} }
// Go to a zone. Not a `scene` intent — zones are the canvas, not a surface
// on it, so the compositor serves this as its own op.
//
// "Zone", not "workspace", in everything we name: viewtop's canvas is a
// large scalable space of states rather than Hyprland's numbered desks, and
// the vocabulary is being moved off Hyprland's deliberately. The wire op is
// still spelled `workspace` — renaming that is a separate sweep, and doing
// it halfway would leave the shell calling an op the compositor does not
// serve.
function zone(to) {
root._send({ op: "workspace", to: to });
}
function focus(id) { function focus(id) {
root.scene("focus", { id: id }); root.scene("focus", { id: id });
} }
@ -165,6 +189,13 @@ Singleton {
root.refused(intent, why); root.refused(intent, why);
} else { } else {
root.lastError = ""; root.lastError = "";
// A `workspaces` reply carries the canvas rather than a
// verb's outcome. Captured here so a chooser has something
// real to list instead of a guess at what is open.
if (reply.windows !== undefined) {
root.windows = reply.windows;
root.windowsChanged_();
}
root.succeeded(intent); root.succeeded(intent);
} }
root._drain(); root._drain();