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

Issue:
Implement system window menu
The system window menu normally shows as the context menu of title-bars, also shows when clicking in the window icon (Windows).

Add a  `WINDOWS` method and view API and  to cause this menu to show on demand. Winit provides the [`show_window_menu`] method.


[`show_window_menu`]: https://docs.rs/winit/latest/winit/window/struct.Window.html#method.show_window_menu


Discussion / hints:

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 20219a344..574721d59 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,8 @@
 # Unpublished
 
+* **Breaking** Add `open_title_bar_context_menu` to view API.
+    - Note that this is only a breaking change for direct dependents of `zng-view-api`.
+* Add `OPEN_TITLE_BAR_CONTEXT_MENU_CMD` to window API.
 
 # 0.4.0
 
@@ -26,7 +29,7 @@
 * Fix `AnsiText!` not resetting style.
 * `Markdown!` widget now uses `AnsiText!` for ```console code block.
 * Fix `auto_size` not using the min/max_size constraints.
-* **Braking:** Change return type of `SCROLL.vertical_offset`, `SCROLL.horizontal_offset` and `SCROLL.zoom_scale`.
+* **Breaking:** Change return type of `SCROLL.vertical_offset`, `SCROLL.horizontal_offset` and `SCROLL.zoom_scale`.
     - Changed only from `ReadOnlyContextVar<Factor>` to `ContextVar<Factor>` so it has minimal impact.
 * Add `vertical_offset`, `horizontal_offset` and `zoom_scale` properties in `Scroll!`.
     - Users should prefer using scroll commands over these properties, but they are useful for implementing features
diff --git a/crates/zng-app/src/view_process.rs b/crates/zng-app/src/view_process.rs
index fa8cc7320..e07f359cb 100644
--- a/crates/zng-app/src/view_process.rs
+++ b/crates/zng-app/src/view_process.rs
@@ -777,6 +777,11 @@ impl ViewWindow {
         self.0.call(|id, p| p.set_focus_indicator(id, indicator))
     }
 
+    /// Open system title bar context menu.
+    pub fn open_title_bar_context_menu(&self, position: DipPoint) -> Result<()> {
+        self.0.call(|id, p| p.open_title_bar_context_menu(id, position))
+    }
+
     /// Shows a native message dialog for the window.
     ///
     /// The window is not interactive while the dialog is visible and the dialog may be modal in the view-process.
diff --git a/crates/zng-ext-window/src/cmd.rs b/crates/zng-ext-window/src/cmd.rs
index 0467a06f9..354be9321 100644
--- a/crates/zng-ext-window/src/cmd.rs
+++ b/crates/zng-ext-window/src/cmd.rs
@@ -73,6 +73,17 @@ command! {
     ///
     /// If any IME preview is active close it without committing.
     pub static CANCEL_IME_CMD;
+
+    /// Represents the window **open title bar context menu** action.
+    ///
+    /// # Parameter
+    ///
+    /// This command supports an optional parameter, it can be a [`DipPoint`] or [`PxPoint`] that defines
+    /// the menu position.
+    ///
+    /// [`DipPoint`]: zng_layout::unit::DipPoint
+    /// [`PxPoint`]: zng_layout::unit::PxPoint
+    pub static OPEN_TITLE_BAR_CONTEXT_MENU_CMD;
 }
 
 pub(super) struct WindowCommands {
@@ -93,7 +104,6 @@ impl WindowCommands {
             restore_handle: RESTORE_CMD.scoped(window_id).subscribe(false),
             fullscreen_handle: FULLSCREEN_CMD.scoped(window_id).subscribe(true),
             exclusive_handle: EXCLUSIVE_FULLSCREEN_CMD.scoped(window_id).subscribe(true),
-
             close_handle: CLOSE_CMD.scoped(window_id).subscribe(true),
         }
     }
diff --git a/crates/zng-ext-window/src/control.rs b/crates/zng-ext-window/src/control.rs
index a2ab2abcc..96bbf6fae 100644
--- a/crates/zng-ext-window/src/control.rs
+++ b/crates/zng-ext-window/src/control.rs
@@ -30,8 +30,8 @@ use zng_ext_image::{ImageRenderArgs, ImageSource, ImageVar, Img, IMAGES};
 use zng_layout::{
     context::{LayoutMetrics, LayoutPassId, DIRECTION_VAR, LAYOUT},
     unit::{
-        Dip, DipRect, DipSize, DipToPx, Factor, FactorUnits, Layout1d, Layout2d, Length, Ppi, Px, PxConstraints, PxPoint, PxRect, PxSize,
-        PxToDip, PxVector, TimeUnits,
+        Dip, DipPoint, DipRect, DipSize, DipToPx, Factor, FactorUnits, Layout1d, Layout2d, Length, Ppi, Px, PxConstraints, PxPoint, PxRect,
+        PxSize, PxToDip, PxVector, TimeUnits,
     },
 };
 use zng_var::{AnyVar, ReadOnlyArcVar, Var, VarHandle, VarHandles};
@@ -106,6 +106,7 @@ struct HeadedCtrl {
     render_access_update: Option<WidgetInfoTree>, // previous info tree
     ime_info: Option<ImeInfo>,
     cancel_ime_handle: CommandHandle,
+    open_title_menu_handle: CommandHandle,
 }
 impl HeadedCtrl {
     pub fn new(vars: &WindowVars, commands: WindowCommands, content: WindowRoot) -> Self {
@@ -136,6 +137,7 @@ impl HeadedCtrl {
             render_access_update: None,
             ime_info: None,
             cancel_ime_handle: CommandHandle::dummy(),
+            open_title_menu_handle: CommandHandle::dummy(),
         }
     }
 
@@ -671,6 +673,7 @@ impl HeadedCtrl {
 
                 self.window = Some(args.window.clone());
                 self.cancel_ime_handle = super::cmd::CANCEL_IME_CMD.scoped(WINDOW.id()).subscribe(true);
+                self.open_title_menu_handle = super::cmd::OPEN_TITLE_BAR_CONTEXT_MENU_CMD.scoped(WINDOW.id()).subscribe(true);
 
                 self.vars.0.render_mode.set(args.data.render_mode);
                 self.vars.state().set(args.data.state.state);
@@ -768,6 +771,7 @@ impl HeadedCtrl {
 
                     self.window = None;
                     self.cancel_ime_handle = CommandHandle::dummy();
+                    self.open_title_menu_handle = CommandHandle::dummy();
                     self.waiting_view = false;
                     self.delayed_view_updates = vec![];
                     self.respawned = true;
@@ -789,7 +793,23 @@ impl HeadedCtrl {
             if let Some(w) = &self.window {
                 let _ = w.set_ime_area(None);
             }
-        }
+        } else if let Some(args) = super::cmd::OPEN_TITLE_BAR_CONTEXT_MENU_CMD.scoped(WINDOW.id()).on(update) {
+            let pos = args.handle_enabled(&self.open_title_menu_handle, |args| {
+                let pos = if let Some(p) = args.param::<DipPoint>() {
+                    *p
+                } else if let Some(p) = args.param::<PxPoint>() {
+                    p.to_dip(self.vars.scale_factor().get())
+                } else {
+                    DipPoint::splat(Dip::new(24))
+                };
+                pos
+            });
+            if let Some(pos) = pos {
+                self.view_task(Box::new(move |w| {
+                    let _ = w.unwrap().open_title_bar_context_menu(pos);
+                }));
+            }
+        };
     }
 
     pub fn ui_event(&mut self, update: &EventUpdate) {
@@ -1289,6 +1309,7 @@ impl HeadedCtrl {
         self.content.close();
         self.window = None;
         self.cancel_ime_handle = CommandHandle::dummy();
+        self.cancel_ime_handle = CommandHandle::dummy();
     }
 
     fn view_task(&mut self, task: Box<dyn FnOnce(Option<&ViewWindow>) + Send>) {
diff --git a/crates/zng-view-api/src/lib.rs b/crates/zng-view-api/src/lib.rs
index 289fcec4d..cbcae8b27 100644
--- a/crates/zng-view-api/src/lib.rs
+++ b/crates/zng-view-api/src/lib.rs
@@ -58,7 +58,7 @@ use font::{FontFaceId, FontId, FontOptions, FontVariationName};
 use image::{ImageId, ImageMaskMode, ImageRequest, ImageTextureId};
 use ipc::{IpcBytes, IpcBytesReceiver};
 use window::WindowId;
-use zng_unit::{DipRect, DipSize, Factor, Px, PxRect};
+use zng_unit::{DipPoint, DipRect, DipSize, Factor, Px, PxRect};
 
 /// Packaged API request.
 #[derive(Debug)]
@@ -339,6 +339,9 @@ declare_api! {
     /// This request can steal focus from other apps disrupting the user, be careful with it.
     pub fn focus_window(&mut self, id: WindowId);
 
+    /// Open the system title bar context menu.
+    pub fn open_title_bar_context_menu(&mut self, id: WindowId, position: DipPoint);
+
     /// Cache an image resource.
     ///
     /// The image is decoded asynchronously, the events [`Event::ImageMetadataLoaded`], [`Event::ImageLoaded`]
diff --git a/crates/zng-view/src/lib.rs b/crates/zng-view/src/lib.rs
index 219c323c9..81c6fcbdf 100644
--- a/crates/zng-view/src/lib.rs
+++ b/crates/zng-view/src/lib.rs
@@ -1626,6 +1626,10 @@ impl Api for App {
         }
     }
 
+    fn open_title_bar_context_menu(&mut self, id: WindowId, position: DipPoint) {
+        self.with_window(id, |w| w.open_title_bar_context_menu(position), || ())
+    }
+
     fn set_cursor(&mut self, id: WindowId, icon: Option<CursorIcon>) {
         self.with_window(id, |w| w.set_cursor(icon), || ())
     }
diff --git a/crates/zng-view/src/window.rs b/crates/zng-view/src/window.rs
index 603a6c7ec..7e3cb08d9 100644
--- a/crates/zng-view/src/window.rs
+++ b/crates/zng-view/src/window.rs
@@ -1098,6 +1098,11 @@ impl Window {
         true
     }
 
+    /// Open windows title bar context menu.
+    pub fn open_title_bar_context_menu(&self, pos: DipPoint) {
+        self.window.show_window_menu(pos.to_winit())
+    }
+
     fn apply_state(&mut self, new_state: WindowStateAll, force: bool) {
         if self.state.chrome_visible != new_state.chrome_visible {
             self.window.set_decorations(new_state.chrome_visible);
diff --git a/examples/window.rs b/examples/window.rs
index 5fdfe14cb..827198f6e 100644
--- a/examples/window.rs
+++ b/examples/window.rs
@@ -68,6 +68,11 @@ async fn main_window() -> window::WindowRoot {
         background_color = color_scheme_map(colors::BLACK, colors::WHITE);
         padding = 4;
         corner_radius = (0, 0, 5, 5);
+        gesture::on_context_click = hn!(|args: &gesture::ClickArgs| {
+            if let Some(p) = args.position() {
+                window::cmd::OPEN_TITLE_BAR_CONTEXT_MENU_CMD.scoped(WINDOW.id()).notify_param(p);
+            }
+        });
     };
     LAYERS.insert(LayerIndex::TOP_MOST, custom_chrome);
 


Test patch: