From d7bac545eb4aad037ef808fb305625cddb20de6d Mon Sep 17 00:00:00 2001 From: Raphael Amorim Date: Thu, 16 Apr 2026 22:41:59 +0200 Subject: [PATCH] support native quit dialog for windows --- frontends/rioterm/src/application.rs | 14 ++++- rio-window/src/event_loop.rs | 2 +- .../src/platform_impl/windows/event_loop.rs | 56 ++++++++++++++++++- .../windows/event_loop/runner.rs | 17 ++++++ 4 files changed, 82 insertions(+), 7 deletions(-) diff --git a/frontends/rioterm/src/application.rs b/frontends/rioterm/src/application.rs index 2d897c1f..49a2d8c0 100644 --- a/frontends/rioterm/src/application.rs +++ b/frontends/rioterm/src/application.rs @@ -902,10 +902,18 @@ impl ApplicationHandler for Application<'_> { match event { WindowEvent::CloseRequested => { - // On macOS, just close the window. Quit confirmation is - // handled by Rio's Cmd+Q keybinding (RioEvent::Exit). - if cfg!(target_os = "macos") { + // macOS: Cmd+Q quit confirmation is handled by + // `applicationShouldTerminate` in rio-window. + // Windows: per-window close confirmation is handled + // by `MessageBoxW` in rio-window's WM_CLOSE handler + // (see `set_confirm_before_quit` plumbing). + // Either way, by the time we see `CloseRequested` + // the user has already confirmed — just close. + if cfg!(any(target_os = "macos", target_os = "windows")) { self.router.routes.remove(&window_id); + if self.router.routes.is_empty() { + event_loop.exit(); + } return; } diff --git a/rio-window/src/event_loop.rs b/rio-window/src/event_loop.rs index 42b9c2f7..2ee39aae 100644 --- a/rio-window/src/event_loop.rs +++ b/rio-window/src/event_loop.rs @@ -309,7 +309,7 @@ impl EventLoop { } #[inline] - #[cfg(target_os = "macos")] + #[cfg(any(target_os = "macos", target_os = "windows"))] pub fn set_confirm_before_quit(&self, confirmation: bool) { self.event_loop.set_confirm_before_quit(confirmation) } diff --git a/rio-window/src/platform_impl/windows/event_loop.rs b/rio-window/src/platform_impl/windows/event_loop.rs index d5c36734..31d7a0ef 100644 --- a/rio-window/src/platform_impl/windows/event_loop.rs +++ b/rio-window/src/platform_impl/windows/event_loop.rs @@ -50,9 +50,10 @@ use windows_sys::Win32::UI::Input::{ }; use windows_sys::Win32::UI::WindowsAndMessaging::{ CreateWindowExW, DefWindowProcW, DestroyWindow, DispatchMessageW, GetClientRect, - GetCursorPos, GetMenu, LoadCursorW, MsgWaitForMultipleObjectsEx, PeekMessageW, - PostMessageW, RegisterClassExW, RegisterWindowMessageA, SetCursor, SetWindowPos, - TranslateMessage, CREATESTRUCTW, GIDC_ARRIVAL, GIDC_REMOVAL, GWL_STYLE, GWL_USERDATA, + GetCursorPos, GetMenu, LoadCursorW, MessageBoxW, MsgWaitForMultipleObjectsEx, + PeekMessageW, PostMessageW, RegisterClassExW, RegisterWindowMessageA, SetCursor, + SetWindowPos, TranslateMessage, CREATESTRUCTW, GIDC_ARRIVAL, GIDC_REMOVAL, GWL_STYLE, + GWL_USERDATA, IDYES, MB_ICONQUESTION, MB_TASKMODAL, MB_YESNO, HTCAPTION, HTCLIENT, MINMAXINFO, MNC_CLOSE, MSG, MWMO_INPUTAVAILABLE, NCCALCSIZE_PARAMS, PM_REMOVE, PT_PEN, PT_TOUCH, QS_ALLINPUT, RI_MOUSE_HWHEEL, RI_MOUSE_WHEEL, SC_MINIMIZE, SC_RESTORE, SIZE_MAXIMIZED, SWP_NOACTIVATE, SWP_NOMOVE, @@ -255,6 +256,17 @@ impl EventLoop { &self.window_target } + /// When enabled, a native `MessageBoxW` confirmation prompt + /// is shown before `WindowEvent::CloseRequested` fires for + /// any window. Mirrors the macOS implementation in + /// `app_delegate.rs`. + pub fn set_confirm_before_quit(&self, confirmation: bool) { + self.window_target + .p + .runner_shared + .set_confirm_before_quit(confirmation); + } + pub fn run(mut self, event_handler: F) -> Result<(), EventLoopError> where F: FnMut(Event, &RootAEL), @@ -1322,6 +1334,17 @@ unsafe fn public_window_callback_inner( WM_CLOSE => { use crate::event::WindowEvent::CloseRequested; + // Mirrors macOS's `applicationShouldTerminate` NSAlert + // (rio-window/src/platform_impl/macos/app_delegate.rs). + // Frontends just see `CloseRequested` — confirmation + // is handled here. + if userdata.event_loop_runner.confirm_before_quit() + && !unsafe { confirm_close_native(window) } + { + // User dismissed the dialog; suppress the close. + result = ProcResult::Value(0); + return; + } userdata.send_event(Event::WindowEvent { window_id: RootWindowId(WindowId(window)), event: CloseRequested, @@ -2879,3 +2902,30 @@ fn get_pointer_move_kind( PointerMoveKind::None } } + +/// Show a modal Yes/No `MessageBoxW` parented to `hwnd`. Returns +/// `true` if the user confirmed the close. Mirrors macOS's NSAlert +/// in `app_delegate.rs:applicationShouldTerminate`. +unsafe fn confirm_close_native(hwnd: HWND) -> bool { + use std::ffi::OsStr; + use std::iter::once; + use std::os::windows::ffi::OsStrExt; + + let title: Vec = OsStr::new("Close Rio terminal?") + .encode_wide() + .chain(once(0)) + .collect(); + let message: Vec = OsStr::new("All sessions in this window will be closed.") + .encode_wide() + .chain(once(0)) + .collect(); + let response = unsafe { + MessageBoxW( + hwnd, + message.as_ptr(), + title.as_ptr(), + MB_YESNO | MB_ICONQUESTION | MB_TASKMODAL, + ) + }; + response == IDYES as i32 +} diff --git a/rio-window/src/platform_impl/windows/event_loop/runner.rs b/rio-window/src/platform_impl/windows/event_loop/runner.rs index 7bbae2c9..8f5580fe 100644 --- a/rio-window/src/platform_impl/windows/event_loop/runner.rs +++ b/rio-window/src/platform_impl/windows/event_loop/runner.rs @@ -37,6 +37,11 @@ pub(crate) struct EventLoopRunner { event_buffer: RefCell>>, panic_error: Cell>, + + /// When true, the WM_CLOSE handler shows a native MessageBox + /// before emitting `WindowEvent::CloseRequested`. Mirrors + /// macOS's `applicationShouldTerminate` NSAlert path. + confirm_before_quit: Cell, } pub type PanicError = Box; @@ -72,9 +77,20 @@ impl EventLoopRunner { last_events_cleared: Cell::new(Instant::now()), event_handler: Cell::new(None), event_buffer: RefCell::new(VecDeque::new()), + confirm_before_quit: Cell::new(false), } } + #[inline] + pub(crate) fn set_confirm_before_quit(&self, confirm: bool) { + self.confirm_before_quit.set(confirm); + } + + #[inline] + pub(crate) fn confirm_before_quit(&self) -> bool { + self.confirm_before_quit.get() + } + /// Associate the application's event handler with the runner /// /// # Safety @@ -116,6 +132,7 @@ impl EventLoopRunner { last_events_cleared: _, event_handler, event_buffer: _, + confirm_before_quit: _, } = self; interrupt_msg_dispatch.set(false); runner_state.set(RunnerState::Uninitialized); -- 2.51.2