Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 25 additions & 6 deletions apps/terminal/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,11 +144,11 @@ impl MatchEvent for App {
};
// The person said yes to ending the running jobs: close now.
if let ModuleCloseAction::Confirmed = wa.cast::<ModuleCloseAction>() {
cx.quit();
close_self(cx);
}
match wa.cast::<TermTabsAction>() {
// Ctrl+Shift+W on the last tab closes the window.
TermTabsAction::LastTabClosed => cx.quit(),
TermTabsAction::LastTabClosed => close_self(cx),
TermTabsAction::None => {}
}
match wa.cast::<MpTermAction>() {
Expand All @@ -166,6 +166,16 @@ impl MatchEvent for App {
}
}

/// End this terminal. Standalone it quits. Hosted, the window manager owns
/// the process and its tile, and the hosted runtime does not act on
/// `cx.quit()`: the terminal asks the WM to close it (`WmRequest::Close`,
/// "the app finished"), which the WM carries out without asking back.
fn close_self(cx: &mut Cx) {
if !makepad_wm_api::send(cx, &makepad_wm_api::WmRequest::Close) {
cx.quit();
}
}

impl App {
/// The selected tab's terminal.
fn term(&self, cx: &mut Cx) -> WidgetRef {
Expand Down Expand Up @@ -272,11 +282,20 @@ impl App {
term.unload(cx);
}
}
WmEvent::CloseRequested => {
if self.ask_before_closing(cx) == CloseDecision::Allow {
cx.quit();
}
// The WM greets a hosted app: tell it a close is answered here,
// so it waits for the answer instead of killing running jobs.
WmEvent::Hosted { .. } => {
makepad_wm_api::asks_before_close(cx);
}
// Allow: close. Veto: the bar asks the person; the WM hears
// "asking" so it neither kills nor gives up on us, and every
// repeated close is answered again (a live terminal re-asks).
WmEvent::CloseRequested => match self.ask_before_closing(cx) {
CloseDecision::Allow => close_self(cx),
CloseDecision::Veto => {
makepad_wm_api::close_refused(cx);
}
},
// Adopted into a real tile: now it is a running Terminal.
WmEvent::Adopted => self.open_ai_port(cx),
_ => {}
Expand Down
3 changes: 3 additions & 0 deletions apps/wm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1334,6 +1334,9 @@ impl App {
log!("wm: notify from client {}: {} — {}", client, title, body);
}
WmRequest::Close => self.request_close(cx, client),
// This WM still closes with CloseRequested + Kill: it neither
// waits for an answer nor needs the app's refusal.
WmRequest::AsksBeforeClose | WmRequest::CloseRefused => {}
WmRequest::SetFloating { floating } => {
let area = self.desk_area(cx);
let gap = self.state_mut().gap;
Expand Down
36 changes: 36 additions & 0 deletions libs/wm_api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,19 @@ pub enum WmRequest {
/// Ask the WM to float / tile / fullscreen this window.
SetFloating { floating: bool },
SetFullscreen { fullscreen: bool },
/// This app answers [`WmEvent::CloseRequested`] itself: it agrees with
/// [`WmRequest::Close`], or refuses with [`WmRequest::CloseRefused`]
/// while it asks the person (the terminal, with jobs running). Sent in
/// reply to [`WmEvent::Hosted`]. A WM that heard it sends
/// `CloseRequested` alone and waits for the answer instead of following
/// it with a kill; a WM that did not keeps closing the app as before.
AsksBeforeClose,
/// The answer to a [`WmEvent::CloseRequested`]: not now — the app is
/// showing the person a question in its own window. On a yes it sends
/// [`WmRequest::Close`] (a hosted app cannot end its own process with
/// `cx.quit()`); a no leaves it running. Sent on every refusal, so a WM
/// can tell an app that is asking from one that stopped answering.
CloseRefused,
}

/// What the window manager tells an app.
Expand Down Expand Up @@ -143,6 +156,18 @@ pub fn send(cx: &Cx, req: &WmRequest) -> bool {
true
}

/// Tell the WM this app answers `CloseRequested` itself
/// ([`WmRequest::AsksBeforeClose`]); call it on [`WmEvent::Hosted`].
pub fn asks_before_close(cx: &Cx) -> bool {
send(cx, &WmRequest::AsksBeforeClose)
}

/// Answer a `CloseRequested` with "not now": the app is asking the person
/// ([`WmRequest::CloseRefused`]). No-op standalone.
pub fn close_refused(cx: &Cx) -> bool {
send(cx, &WmRequest::CloseRefused)
}

/// Quick Look `path`. Hosted: the WM floats the associated viewer over the
/// desk. Standalone: spawn the viewer's binary with `--preview` next to
/// this executable (best effort; false when nothing could be started).
Expand Down Expand Up @@ -292,6 +317,17 @@ mod tests {
assert_eq!(WmEvent::parse(&WmEvent::CloseRequested.to_json()), Some(WmEvent::CloseRequested));
}

#[test]
fn close_answers_round_trip() {
// The wire form is part of the contract: a WM pinned before these
// variants recognises them by this exact text.
assert_eq!(WmRequest::AsksBeforeClose.to_json(), r#"{"wm":{"AsksBeforeClose":[]}}"#);
assert_eq!(WmRequest::CloseRefused.to_json(), r#"{"wm":{"CloseRefused":[]}}"#);
for req in [WmRequest::AsksBeforeClose, WmRequest::CloseRefused] {
assert_eq!(WmRequest::parse(&req.to_json()), Some(req));
}
}

#[test]
fn associations() {
assert_eq!(viewer_for(Path::new("a/B.PNG")), "image");
Expand Down