split chooser, home reaches the compositor, idle-inhibit comment was backwards
This commit is contained in:
parent
f7eba9f658
commit
b0688bc30b
4 changed files with 130 additions and 12 deletions
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue