Repository: zng-ui/zng (language: rust)

Issue:
Implement window close cancel when OS is shutting down
That is engage the system temporary hold on the shutdown operation.

In Windows this is implemented with the winapi function `ShutdownBlockReasonCreate`.


Discussion / hints:

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5aa8270ec..ef101e77f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,11 +7,13 @@
     - Rename `close_window` to `close`.
     - Rename `focus_window` to `focus`.
     - Add `set_enabled_buttons`.
+    - Add `set_system_shutdown_warn`.
     - Note that this is only a breaking change for direct dependents of `zng-view-api`.
 * Better "custom chrome" example in `examples/window.rs`.
 * Add `OPEN_TITLE_BAR_CONTEXT_MENU_CMD` to window API.
 * Fix `WIDGET.border().offsets()` not including the innermost border offset.
 * Add `WindowVars::enabled_buttons` to window API.
+* Add `WindowVars::system_shutdown_warn` to window API.
 * **Breaking** Fix when/property assign expansion order.
     - When blocks now expand in the same declaration order, before they always expanded after all property assigns.
 ```rust
diff --git a/crates/zng-app/src/view_process.rs b/crates/zng-app/src/view_process.rs
index 9f2bbcffe..cf37e2a06 100644
--- a/crates/zng-app/src/view_process.rs
+++ b/crates/zng-app/src/view_process.rs
@@ -831,6 +831,20 @@ impl ViewWindow {
         self.0.call(|id, p| p.set_ime_area(id, area))
     }
 
+    /// Attempt to set a system wide shutdown warning associated with the window.
+    ///
+    /// Operating systems that support this show the `reason` in a warning for the user, it must be a short text
+    /// that identifies the critical operation that cannot be cancelled.
+    ///
+    /// Note that there is no guarantee that the view-process or operating system will actually set a block, there
+    /// is no error result because operating systems can silently ignore block requests at any moment, even after
+    /// an initial successful block.
+    ///
+    /// Set to an empty text to remove the warning.
+    pub fn set_system_shutdown_warn(&self, reason: Txt) -> Result<()> {
+        self.0.call(move |id, p| p.set_system_shutdown_warn(id, reason))
+    }
+
     /// Drop `self`.
     pub fn close(self) {
         drop(self)
diff --git a/crates/zng-ext-window/src/control.rs b/crates/zng-ext-window/src/control.rs
index 9ad8cc8db..4d2071649 100644
--- a/crates/zng-ext-window/src/control.rs
+++ b/crates/zng-ext-window/src/control.rs
@@ -307,6 +307,12 @@ impl HeadedCtrl {
                 })
             }
 
+            if let Some(reason) = self.vars.system_shutdown_warn().get_new() {
+                self.update_gen(move |view| {
+                    let _: Ignore = view.set_system_shutdown_warn(reason);
+                })
+            }
+
             if prev_state != new_state {
                 self.update_gen(move |view| {
                     let _: Ignore = view.set_state(new_state);
@@ -1124,6 +1130,7 @@ impl HeadedCtrl {
                 let area = WINDOW.info().get(a.target.widget_id())?.ime_area().to_dip(scale_factor);
                 Some(area)
             }),
+            system_shutdown_warn: self.vars.system_shutdown_warn().get(),
 
             extensions: WINDOWS.take_view_extensions_init(window_id),
         };
@@ -1252,6 +1259,7 @@ impl HeadedCtrl {
                 let area = info.get(a.target.widget_id())?.ime_area().to_dip(info.scale_factor());
                 Some(area)
             }),
+            system_shutdown_warn: self.vars.system_shutdown_warn().get(),
 
             extensions: WINDOWS.take_view_extensions_init(window_id),
         };
diff --git a/crates/zng-ext-window/src/service.rs b/crates/zng-ext-window/src/service.rs
index ca2b1ddf8..f1f5125d8 100644
--- a/crates/zng-ext-window/src/service.rs
+++ b/crates/zng-ext-window/src/service.rs
@@ -1,11 +1,11 @@
-use std::{any::Any, fmt, future::Future, mem, sync::Arc};
+use std::{any::Any, future::Future, mem, sync::Arc};
 
 use parking_lot::Mutex;
 use zng_app::{
     app_hn_once,
     event::AnyEventArgs,
     timer::{DeadlineHandle, TIMERS},
-    update::{EventUpdate, InfoUpdates, LayoutUpdates, RenderUpdates, UpdateOp, WidgetUpdates, UPDATES},
+    update::{EventUpdate, InfoUpdates, LayoutUpdates, RenderUpdates, WidgetUpdates, UPDATES},
     view_process::{
         self,
         raw_events::{
@@ -45,9 +45,10 @@ use zng_view_api::{
 };
 
 use crate::{
-    cmd::WindowCommands, control::WindowCtrl, CloseWindowResult, FrameCaptureMode, HeadlessMonitor, StartPosition, WindowCloseArgs,
-    WindowCloseRequestedArgs, WindowFocusChangedArgs, WindowNotFound, WindowOpenArgs, WindowRoot, WindowVars, FRAME_IMAGE_READY_EVENT,
-    MONITORS, WINDOW_CLOSE_EVENT, WINDOW_CLOSE_REQUESTED_EVENT, WINDOW_FOCUS_CHANGED_EVENT, WINDOW_LOAD_EVENT, WINDOW_VARS_ID,
+    cmd::WindowCommands, control::WindowCtrl, CloseWindowResult, FrameCaptureMode, HeadlessMonitor, StartPosition, ViewExtensionError,
+    WindowCloseArgs, WindowCloseRequestedArgs, WindowFocusChangedArgs, WindowLoadingHandle, WindowNotFound, WindowOpenArgs, WindowRoot,
+    WindowVars, FRAME_IMAGE_READY_EVENT, MONITORS, WINDOW_CLOSE_EVENT, WINDOW_CLOSE_REQUESTED_EVENT, WINDOW_FOCUS_CHANGED_EVENT,
+    WINDOW_LOAD_EVENT, WINDOW_VARS_ID,
 };
 
 app_local! {
@@ -654,10 +655,11 @@ impl WINDOWS {
     /// [`VIEW_PROCESS_INITED_EVENT`]: zng_app::view_process::VIEW_PROCESS_INITED_EVENT
     pub fn view_extensions_init(
         &self,
-        window_id: WindowId,
+        window_id: impl Into<WindowId>,
         extension_id: ApiExtensionId,
         request: ApiExtensionPayload,
     ) -> Result<(), WindowNotFound> {
+        let window_id = window_id.into();
         match WINDOWS_SV.write().windows_info.get_mut(&window_id) {
             Some(i) => {
                 i.extensions.push((extension_id, request));
@@ -676,12 +678,13 @@ impl WINDOWS {
     /// Note that unlike most service methods this calls happens immediately.
     pub fn view_window_extension_raw(
         &self,
-        id: WindowId,
+        window_id: impl Into<WindowId>,
         extension_id: ApiExtensionId,
         request: ApiExtensionPayload,
     ) -> Result<ApiExtensionPayload, ViewExtensionError> {
+        let window_id = window_id.into();
         let sv = WINDOWS_SV.read();
-        match WINDOWS_SV.read().windows_info.get(&id) {
+        match WINDOWS_SV.read().windows_info.get(&window_id) {
             Some(i) => match &i.view {
                 Some(r) => match r {
                     ViewWindowOrHeadless::Window(r) => {
@@ -690,24 +693,30 @@ impl WINDOWS {
                         r.window_extension_raw(extension_id, request)
                             .map_err(ViewExtensionError::ViewProcessOffline)
                     }
-                    ViewWindowOrHeadless::Headless(_) => Err(ViewExtensionError::WindowNotHeaded(id)),
+                    ViewWindowOrHeadless::Headless(_) => Err(ViewExtensionError::WindowNotHeaded(window_id)),
                 },
-                None => Err(ViewExtensionError::NotOpenInViewProcess(id)),
+                None => Err(ViewExtensionError::NotOpenInViewProcess(window_id)),
             },
-            None => Err(ViewExtensionError::WindowNotFound(WindowNotFound(id))),
+            None => Err(ViewExtensionError::WindowNotFound(WindowNotFound(window_id))),
         }
     }
 
     /// Call a headed window extension with serialized payload.
     ///
     /// Note that unlike most service methods this call happens immediately.
-    pub fn view_window_extension<I, O>(&self, id: WindowId, extension_id: ApiExtensionId, request: &I) -> Result<O, ViewExtensionError>
+    pub fn view_window_extension<I, O>(
+        &self,
+        window_id: impl Into<WindowId>,
+        extension_id: ApiExtensionId,
+        request: &I,
+    ) -> Result<O, ViewExtensionError>
     where
         I: serde::Serialize,
         O: serde::de::DeserializeOwned,
     {
+        let window_id = window_id.into();
         let sv = WINDOWS_SV.read();
-        match sv.windows_info.get(&id) {
+        match sv.windows_info.get(&window_id) {
             Some(i) => match &i.view {
                 Some(r) => match r {
                     ViewWindowOrHeadless::Window(r) => {
@@ -718,11 +727,11 @@ impl WINDOWS {
                             .map_err(ViewExtensionError::ViewProcessOffline)?;
                         r.map_err(ViewExtensionError::Api)
                     }
-                    ViewWindowOrHeadless::Headless(_) => Err(ViewExtensionError::WindowNotHeaded(id)),
+                    ViewWindowOrHeadless::Headless(_) => Err(ViewExtensionError::WindowNotHeaded(window_id)),
                 },
-                None => Err(ViewExtensionError::NotOpenInViewProcess(id)),
+                None => Err(ViewExtensionError::NotOpenInViewProcess(window_id)),
             },
-            None => Err(ViewExtensionError::WindowNotFound(WindowNotFound(id))),
+            None => Err(ViewExtensionError::WindowNotFound(WindowNotFound(window_id))),
         }
     }
 
@@ -731,12 +740,13 @@ impl WINDOWS {
     /// Note that unlike most service methods this call happens immediately.
     pub fn view_render_extension_raw(
         &self,
-        id: WindowId,
+        window_id: impl Into<WindowId>,
         extension_id: ApiExtensionId,
         request: ApiExtensionPayload,
     ) -> Result<ApiExtensionPayload, ViewExtensionError> {
+        let window_id = window_id.into();
         let sv = WINDOWS_SV.read();
-        match WINDOWS_SV.read().windows_info.get(&id) {
+        match WINDOWS_SV.read().windows_info.get(&window_id) {
             Some(i) => match &i.view {
                 Some(r) => {
                     let r = r.renderer();
@@ -744,22 +754,28 @@ impl WINDOWS {
                     r.render_extension_raw(extension_id, request)
                         .map_err(ViewExtensionError::ViewProcessOffline)
                 }
-                None => Err(ViewExtensionError::NotOpenInViewProcess(id)),
+                None => Err(ViewExtensionError::NotOpenInViewProcess(window_id)),
             },
-            None => Err(ViewExtensionError::WindowNotFound(WindowNotFound(id))),
+            None => Err(ViewExtensionError::WindowNotFound(WindowNotFound(window_id))),
         }
     }
 
     /// Call a render extension with serialized payload for the renderer associated with the window.
     ///
     /// Note that unlike most service methods this call happens immediately.
-    pub fn view_render_extension<I, O>(&self, id: WindowId, extension_id: ApiExtensionId, request: &I) -> Result<O, ViewExtensionError>
+    pub fn view_render_extension<I, O>(
+        &self,
+        window_id: impl Into<WindowId>,
+        extension_id: ApiExtensionId,
+        request: &I,
+    ) -> Result<O, ViewExtensionError>
     where
         I: serde::Serialize,
         O: serde::de::DeserializeOwned,
     {
+        let window_id = window_id.into();
         let sv = WINDOWS_SV.read();
-        match sv.windows_info.get(&id) {
+        match sv.windows_info.get(&window_id) {
             Some(i) => match &i.view {
                 Some(r) => {
                     let r = r.renderer();
@@ -769,9 +785,9 @@ impl WINDOWS {
                         .map_err(ViewExtensionError::ViewProcessOffline)?;
                     r.map_err(ViewExtensionError::Api)
                 }
-                None => Err(ViewExtensionError::NotOpenInViewProcess(id)),
+                None => Err(ViewExtensionError::NotOpenInViewProcess(window_id)),
             },
-            None => Err(ViewExtensionError::WindowNotFound(WindowNotFound(id))),
+            None => Err(ViewExtensionError::WindowNotFound(WindowNotFound(window_id))),
         }
     }
 
@@ -1498,7 +1514,7 @@ impl AppWindow {
 }
 
 struct WindowLoading {
-    handles: Vec<std::sync::Weak<WindowLoadingHandleData>>,
+    handles: Vec<std::sync::Weak<crate::WindowLoadingHandleData>>,
     timer: Option<DeadlineHandle>,
 }
 impl WindowLoading {
@@ -1548,53 +1564,12 @@ impl WindowLoading {
     }
 
     pub fn new_handle(&mut self, update: AppEventSender, deadline: Deadline) -> WindowLoadingHandle {
-        let h = Arc::new(WindowLoadingHandleData { update, deadline });
+        let h = Arc::new(crate::WindowLoadingHandleData { update, deadline });
         self.handles.push(Arc::downgrade(&h));
         WindowLoadingHandle(h)
     }
 }
 
-/// Represents a handle that stops the window from loading while the handle is alive.
-///
-/// A handle can be retrieved using [`WINDOWS.loading_handle`] or [`WINDOW.loading_handle`], the window does not
-/// open until all handles expire or are dropped.
-///
-/// [`WINDOWS.loading_handle`]: WINDOWS::loading_handle
-/// [`WINDOW.loading_handle`]: WINDOW::loading_handle
-#[derive(Clone)]
-pub struct WindowLoadingHandle(Arc<WindowLoadingHandleData>);
-impl WindowLoadingHandle {
-    /// Handle expiration deadline.
-    pub fn deadline(&self) -> Deadline {
-        self.0.deadline
-    }
-}
-struct WindowLoadingHandleData {
-    update: AppEventSender,
-    deadline: Deadline,
-}
-impl Drop for WindowLoadingHandleData {
-    fn drop(&mut self) {
-        let _ = self.update.send_update(UpdateOp::Update, None);
-    }
-}
-impl PartialEq for WindowLoadingHandle {
-    fn eq(&self, other: &Self) -> bool {
-        Arc::ptr_eq(&self.0, &other.0)
-    }
-}
-impl Eq for WindowLoadingHandle {}
-impl std::hash::Hash for WindowLoadingHandle {
-    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
-        (Arc::as_ptr(&self.0) as usize).hash(state);
-    }
-}
-impl fmt::Debug for WindowLoadingHandle {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        write!(f, "WindowLoadingHandle(_)")
-    }
-}
-
 /// Extensions methods for [`WINDOW`] contexts of windows open by [`WINDOWS`].
 ///
 /// [`WINDOW`]: zng_app::window::WINDOW
@@ -1772,43 +1747,3 @@ impl WINDOW_FOCUS {
         FOCUS_SV.get()
     }
 }
-
-/// Error calling a view-process API extension associated with a window or renderer.
-#[derive(Debug)]
-pub enum ViewExtensionError {
-    /// Window is not open in the `WINDOWS` service.
-    WindowNotFound(WindowNotFound),
-    /// Window must be headed to call window extensions.
-    WindowNotHeaded(WindowId),
-    /// Window is not open in the view-process.
-    ///
-    /// If the window is headless without renderer it will never open in view-process, if the window is headed
-    /// headless with renderer the window opens in the view-process after the first layout.
-    NotOpenInViewProcess(WindowId),
-    /// View-process is not running.
-    ViewProcessOffline(ViewProcessOffline),
-    /// Api Error.
-    Api(zng_view_api::api_extension::ApiExtensionRecvError),
-}
-impl fmt::Display for ViewExtensionError {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        match self {
-            ViewExtensionError::WindowNotFound(e) => fmt::Display::fmt(e, f),
-            ViewExtensionError::WindowNotHeaded(id) => write!(f, "window `{id}` is not headed"),
-            ViewExtensionError::NotOpenInViewProcess(id) => write!(f, "window/renderer `{id}` not open in the view-process"),
-            ViewExtensionError::ViewProcessOffline(e) => fmt::Display::fmt(e, f),
-            ViewExtensionError::Api(e) => fmt::Display::fmt(e, f),
-        }
-    }
-}
-impl std::error::Error for ViewExtensionError {
-    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
-        match self {
-            ViewExtensionError::WindowNotFound(e) => Some(e),
-            ViewExtensionError::WindowNotHeaded(_) => None,
-            ViewExtensionError::NotOpenInViewProcess(_) => None,
-            ViewExtensionError::ViewProcessOffline(e) => Some(e),
-            ViewExtensionError::Api(e) => Some(e),
-        }
-    }
-}
diff --git a/crates/zng-ext-window/src/types.rs b/crates/zng-ext-window/src/types.rs
index a025ddf98..d47b995ec 100644
--- a/crates/zng-ext-window/src/types.rs
+++ b/crates/zng-ext-window/src/types.rs
@@ -6,11 +6,13 @@ use std::{
 
 use zng_app::{
     event::{event, event_args},
+    update::UpdateOp,
     widget::{
         node::{BoxedUiNode, UiNode},
         WidgetId,
     },
     window::{WindowId, WINDOW},
+    AppEventSender, Deadline,
 };
 use zng_ext_image::{ImageSource, ImageVar, Img};
 use zng_layout::unit::{DipPoint, DipSize, Point, PxPoint};
@@ -20,6 +22,7 @@ use zng_var::impl_from_and_into_var;
 use zng_view_api::{
     image::{ImageDataFormat, ImageMaskMode},
     window::{EventCause, FrameId},
+    ViewProcessOffline,
 };
 
 pub use zng_view_api::window::{FocusIndicator, RenderMode, VideoMode, WindowButton, WindowState};
@@ -699,3 +702,85 @@ impl fmt::Display for WindowNotFound {
     }
 }
 impl std::error::Error for WindowNotFound {}
+
+/// Represents a handle that stops the window from loading while the handle is alive.
+///
+/// A handle can be retrieved using [`WINDOWS.loading_handle`] or [`WINDOW.loading_handle`], the window does not
+/// open until all handles expire or are dropped.
+///
+/// [`WINDOWS.loading_handle`]: WINDOWS::loading_handle
+/// [`WINDOW.loading_handle`]: WINDOW::loading_handle
+#[derive(Clone)]
+#[must_use = "the window does not await loading if the handle is dropped"]
+pub struct WindowLoadingHandle(pub(crate) Arc<WindowLoadingHandleData>);
+impl WindowLoadingHandle {
+    /// Handle expiration deadline.
+    pub fn deadline(&self) -> Deadline {
+        self.0.deadline
+    }
+}
+pub(crate) struct WindowLoadingHandleData {
+    pub(crate) update: AppEventSender,
+    pub(crate) deadline: Deadline,
+}
+impl Drop for WindowLoadingHandleData {
+    fn drop(&mut self) {
+        let _ = self.update.send_update(UpdateOp::Update, None);
+    }
+}
+impl PartialEq for WindowLoadingHandle {
+    fn eq(&self, other: &Self) -> bool {
+        Arc::ptr_eq(&self.0, &other.0)
+    }
+}
+impl Eq for WindowLoadingHandle {}
+impl std::hash::Hash for WindowLoadingHandle {
+    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+        (Arc::as_ptr(&self.0) as usize).hash(state);
+    }
+}
+impl fmt::Debug for WindowLoadingHandle {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "WindowLoadingHandle(_)")
+    }
+}
+
+/// Error calling a view-process API extension associated with a window or renderer.
+#[derive(Debug)]
+pub enum ViewExtensionError {
+    /// Window is not open in the `WINDOWS` service.
+    WindowNotFound(WindowNotFound),
+    /// Window must be headed to call window extensions.
+    WindowNotHeaded(WindowId),
+    /// Window is not open in the view-process.
+    ///
+    /// If the window is headless without renderer it will never open in view-process, if the window is headed
+    /// headless with renderer the window opens in the view-process after the first layout.
+    NotOpenInViewProcess(WindowId),
+    /// View-process is not running.
+    ViewProcessOffline(ViewProcessOffline),
+    /// Api Error.
+    Api(zng_view_api::api_extension::ApiExtensionRecvError),
+}
+impl fmt::Display for ViewExtensionError {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            Self::WindowNotFound(e) => fmt::Display::fmt(e, f),
+            Self::WindowNotHeaded(id) => write!(f, "window `{id}` is not headed"),
+            Self::NotOpenInViewProcess(id) => write!(f, "window/renderer `{id}` not open in the view-process"),
+            Self::ViewProcessOffline(e) => fmt::Display::fmt(e, f),
+            Self::Api(e) => fmt::Display::fmt(e, f),
+        }
+    }
+}
+impl std::error::Error for ViewExtensionError {
+    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
+        match self {
+            Self::WindowNotFound(e) => Some(e),
+            Self::WindowNotHeaded(_) => None,
+            Self::NotOpenInViewProcess(_) => None,
+            Self::ViewProcessOffline(e) => Some(e),
+            Self::Api(e) => Some(e),
+        }
+    }
+}
diff --git a/crates/zng-ext-window/src/vars.rs b/crates/zng-ext-window/src/vars.rs
index 624e5d123..3411b6e13 100644
--- a/crates/zng-ext-window/src/vars.rs
+++ b/crates/zng-ext-window/src/vars.rs
@@ -75,6 +75,7 @@ pub(super) struct WindowVarsData {
     pub(super) render_mode: ArcVar<RenderMode>,
 
     pub(super) access_enabled: ArcVar<AccessEnabled>,
+    system_shutdown_warn: ArcVar<Txt>,
 }
 
 /// Variables that configure the opening or open window.
@@ -151,6 +152,7 @@ impl WindowVars {
             render_mode: var(default_render_mode),
 
             access_enabled: var(AccessEnabled::empty()),
+            system_shutdown_warn: var(Txt::from("")),
         });
         zng_app::APP.about().map_ref(|i| &i.name).set_bind(&vars.title).perm();
         Self(vars)
@@ -680,6 +682,33 @@ impl WindowVars {
     pub fn access_enabled(&self) -> ReadOnlyArcVar<AccessEnabled> {
         self.0.access_enabled.read_only()
     }
+
+    /// Attempt to set a system wide shutdown warning associated with the window.
+    ///
+    /// Operating systems that support this show the text in a warning for the user, it must be a short text
+    /// that identifies the critical operation that cannot be cancelled.
+    ///
+    /// Set to an empty text to remove the warning.
+    ///
+    /// Note that this does not stop the window from closing or the app from exiting normally, you must
+    /// handle window close requests and show some feedback to the user, the view-process will send a window close
+    /// request when a system shutdown attempt is detected.
+    ///
+    /// Note that there is no guarantee that the view-process or operating system will actually set a block, there
+    /// is no error result because operating systems can silently ignore block requests at any moment, even after
+    /// an initial successful block.
+    ///
+    /// ## Current Limitations
+    ///
+    /// The current `zng::view_process` or `zng-view` only implements this feature on Windows and it will only work properly
+    /// under these conditions:
+    ///
+    /// * It must be running in `run_same_process` mode. Windows kills all other processes, so in a run with `init` the the app-process
+    /// will be lost. Note that this also mean that the crash handler and worker processes are also killed.
+    /// * Must be built with `#![windows_subsystem = "windows"]` and must be running from the Windows Explorer (desktop).
+    pub fn system_shutdown_warn(&self) -> ArcVar<Txt> {
+        self.0.system_shutdown_warn.clone()
+    }
 }
 impl PartialEq for WindowVars {
     fn eq(&self, other: &Self) -> bool {
diff --git a/crates/zng-view-api/src/lib.rs b/crates/zng-view-api/src/lib.rs
index 00d9794b3..d6fd3bd56 100644
--- a/crates/zng-view-api/src/lib.rs
+++ b/crates/zng-view-api/src/lib.rs
@@ -504,6 +504,18 @@ declare_api! {
     /// Enable or disable IME by setting a cursor area.
     pub fn set_ime_area(&mut self, id: WindowId, area: Option<DipRect>);
 
+    /// Attempt to set a system wide shutdown warning associated with the window.
+    ///
+    /// Operating systems that support this show the `reason` in a warning for the user, it must be a short text
+    /// that identifies the critical operation that cannot be cancelled.
+    ///
+    /// Note that there is no guarantee that the view-process or operating system will actually set a block, there
+    /// is no error result because operating systems can silently ignore block requests at any moment, even after
+    /// an initial successful block.
+    ///
+    /// Set to an empty text to remove the warning.
+    pub fn set_system_shutdown_warn(&mut self, id: WindowId, reason: Txt);
+
     /// Licenses that may be required to be displayed in the app about screen.
     ///
     /// This is specially important for prebuilt view users, as the tools that scrap licenses
diff --git a/crates/zng-view-api/src/window.rs b/crates/zng-view-api/src/window.rs
index 83fa13b8c..723f6746c 100644
--- a/crates/zng-view-api/src/window.rs
+++ b/crates/zng-view-api/src/window.rs
@@ -415,6 +415,9 @@ pub struct WindowRequest {
 
     /// Enabled window chrome buttons.
     pub enabled_buttons: WindowButton,
+
+    /// System shutdown warning associated with the window.
+    pub system_shutdown_warn: Txt,
 }
 impl WindowRequest {
     /// Corrects invalid values if [`kiosk`] is `true`.
diff --git a/crates/zng-view/Cargo.toml b/crates/zng-view/Cargo.toml
index ca6503646..727d4ff7a 100644
--- a/crates/zng-view/Cargo.toml
+++ b/crates/zng-view/Cargo.toml
@@ -111,6 +111,7 @@ features = [
     "Win32_Globalization",
     "Win32_System_SystemServices",
     "Win32_System_Power",
+    "Win32_System_Shutdown",
 ]
 [target.'cfg(windows)'.dependencies.windows]
 version = "0.56.0"
diff --git a/crates/zng-view/src/lib.rs b/crates/zng-view/src/lib.rs
index 888e022c0..f51673c89 100644
--- a/crates/zng-view/src/lib.rs
+++ b/crates/zng-view/src/lib.rs
@@ -1945,6 +1945,10 @@ impl Api for App {
         }
     }
 
+    fn set_system_shutdown_warn(&mut self, id: WindowId, reason: Txt) {
+        self.with_window(id, move |w| w.set_system_shutdown_warn(reason), || ())
+    }
+
     fn third_party_licenses(&mut self) -> Vec<zng_tp_licenses::LicenseUsed> {
         #[cfg(feature = "bundle_licenses")]
         {
diff --git a/crates/zng-view/src/window.rs b/crates/zng-view/src/window.rs
index 99fa5c525..3886ec3ae 100644
--- a/crates/zng-view/src/window.rs
+++ b/crates/zng-view/src/window.rs
@@ -252,6 +252,7 @@ impl Window {
 
         // * Extend the winit Windows window to not block the Alt+F4 key press.
         // * Check if the window is actually keyboard focused until first focus.
+        // * Block system shutdown if a block is set.
         #[cfg(windows)]
         {
             let event_sender = event_sender.clone();
@@ -272,23 +273,48 @@ impl Window {
                     let _ = event_sender.send(AppEvent::WinitFocused(window_id, true));
                 }
 
-                if msg == windows_sys::Win32::UI::WindowsAndMessaging::WM_SYSKEYDOWN
-                    && wparam as windows_sys::Win32::UI::Input::KeyboardAndMouse::VIRTUAL_KEY
-                        == windows_sys::Win32::UI::Input::KeyboardAndMouse::VK_F4
-                {
-                    // winit always blocks ALT+F4 we want to allow it so that the shortcut is handled in the same way as other commands.
-
-                    let _ = event_sender.send(AppEvent::Notify(Event::KeyboardInput {
-                        window: id,
-                        device: DeviceId::INVALID, // same as winit
-                        key_code: KeyCode::F4,
-                        state: KeyState::Pressed,
-                        key: Key::F4,
-                        key_modified: Key::F4,
-                        text: Txt::from_static(""),
-                    }));
-                    return Some(0);
+                match msg {
+                    windows_sys::Win32::UI::WindowsAndMessaging::WM_SYSKEYDOWN => {
+                        if wparam as windows_sys::Win32::UI::Input::KeyboardAndMouse::VIRTUAL_KEY
+                            == windows_sys::Win32::UI::Input::KeyboardAndMouse::VK_F4
+                        {
+                            // winit always blocks ALT+F4 we want to allow it so that the shortcut is handled in the same way as other commands.
+
+                            let _ = event_sender.send(AppEvent::Notify(Event::KeyboardInput {
+                                window: id,
+                                device: DeviceId::INVALID, // same as winit
+                                key_code: KeyCode::F4,
+                                state: KeyState::Pressed,
+                                key: Key::F4,
+                                key_modified: Key::F4,
+                                text: Txt::from_static(""),
+                            }));
+                            return Some(0);
+                        }
+                    }
+                    windows_sys::Win32::UI::WindowsAndMessaging::WM_QUERYENDSESSION => {
+                        let mut reason = [0u16; 256];
+                        let mut reason_size = reason.len() as u32;
+                        let ok = unsafe {
+                            windows_sys::Win32::System::Shutdown::ShutdownBlockReasonQuery(hwnd, reason.as_mut_ptr(), &mut reason_size)
+                        };
+                        if ok != 0 {
+                            match windows::core::HSTRING::from_wide(&reason) {
+                                Ok(s) => {
+                                    tracing::warn!("blocked system shutdown, reason: {}", s);
+                                }
+                                Err(e) => {
+                                    tracing::error!("blocked system shutdown, error retrieving reason: {e}");
+                                }
+                            }
+                            // send a close requested to hopefully cause the normal close/cancel dialog to appear.
+                            let _ = event_sender.send(AppEvent::Notify(Event::WindowCloseRequested(id)));
+                            return Some(0);
+                        }
+                    }
+                    _ => {}
                 }
+
                 None
             });
         }
@@ -436,6 +462,8 @@ impl Window {
 
         win.set_enabled_buttons(cfg.enabled_buttons);
 
+        win.set_system_shutdown_warn(cfg.system_shutdown_warn);
+
         if win.ime_area.is_some() {
             win.window.set_ime_allowed(true);
         }
@@ -1790,9 +1818,40 @@ impl Window {
             self.ime_area = None;
         }
     }
+
+    #[cfg(windows)]
+    pub(crate) fn set_system_shutdown_warn(&mut self, reason: Txt) {
+        if !reason.is_empty() {
+            let hwnd = crate::util::winit_to_hwnd(&self.window);
+            let reason = windows::core::HSTRING::from(reason.as_str());
+            // SAFETY: function return handled.
+            let created = unsafe { windows_sys::Win32::System::Shutdown::ShutdownBlockReasonCreate(hwnd, reason.as_ptr()) } != 0;
+            if !created {
+                let error = unsafe { windows_sys::Win32::Foundation::GetLastError() };
+                tracing::error!("failed to set system shutdown warn ({error:#X}), requested warn reason was: {reason}");
+            }
+        } else {
+            let hwnd = crate::util::winit_to_hwnd(&self.window);
+            // SAFETY: function return handled.
+            let destroyed = unsafe { windows_sys::Win32::System::Shutdown::ShutdownBlockReasonDestroy(hwnd) } != 0;
+            if !destroyed {
+                let error = unsafe { windows_sys::Win32::Foundation::GetLastError() };
+                tracing::error!("failed to unset system shutdown warn ({error:#X})");
+            }
+        }
+    }
+
+    #[cfg(not(windows))]
+    pub(crate) fn set_system_shutdown_warn(&mut self, reason: Txt) {
+        if !reason.is_empty() {
+            tracing::error!("cannot set system shutdown warn, not implemented, requested warn reason was: {reason}");
+        }
+    }
 }
 impl Drop for Window {
     fn drop(&mut self) {
+        self.set_system_shutdown_warn(Txt::from(""));
+
         self.api.stop_render_backend();
         self.api.shut_down(true);
 


Test patch: