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
// idle timer to be reset by real input only.
// `get_idle_notification`, which **respects idle inhibitors** — not
// `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(
QUIET_PERIOD.as_millis() as u32,
&seat,

View file

@ -230,8 +230,14 @@ PanelWindow {
GlobalStates.overviewOpen = false
GlobalStates.dockRevealed = false
GlobalStates.oskOpen = false
Quickshell.execDetached(["hyprctl", "dispatch",
"hl.dsp.focus({ workspace = 1 })"])
// 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.zone(Config.options.navigation?.homeZone ?? 1)
}
// 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.
property int target: 0
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,
// short enough that a stuck app does not feel like a negotiation.
@ -64,6 +66,7 @@ Scope {
function dismiss() {
scope.sheetOpen = false;
scope.choosing = false;
scope.target = 0;
}
@ -98,6 +101,12 @@ Scope {
// The window is gone or moved; the sheet has nothing left to be
// about. Kept open for `pose`-style verbs would mean a sheet
// 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")
scope.dismiss();
}
@ -137,8 +146,68 @@ Scope {
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.
RowLayout {
visible: !scope.choosing
anchors.horizontalCenter: parent.horizontalCenter
y: parent.height * 0.38
spacing: 28
@ -159,16 +228,18 @@ Scope {
SheetButton {
icon: "splitscreen"
label: "Split"
// Split pins this window to one half and the other half
// wants a chooser open apps from the overview, or the
// app list. That chooser is the missing half and it has no
// verb yet, so this places the window and says so rather
// than pretending the flow is finished.
// Pin this window to the top half, then offer the other
// half to something already open. Casey's shape: "the
// second waiting one possibly having a button to add an
// already opened one from the multitasking section".
//
// 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: {
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";
refusal.opacity = 1;
refusalFade.restart();
ViewtopControl.refreshWindows();
scope.choosing = true;
}
}

View file

@ -38,6 +38,17 @@ Singleton {
signal refused(string intent, string reason)
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
// 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.
@ -114,6 +125,19 @@ Singleton {
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) {
root.scene("focus", { id: id });
}
@ -165,6 +189,13 @@ Singleton {
root.refused(intent, why);
} else {
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._drain();