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

Issue:
MacOS Slint Preview option for window to 'stay on top'.
### Feature Description

This is often a menu item in the native macos menu system. Short term use is for recording slint videos it's not acceptable to make vscode smaller so it doesn't overlap the preview. So we can make video content far faster. The inbuilt live preview doesn't cut it as it's not skia, not debug output to vscode, etc. Also not much difference to making vscode smaller as you still lose all the screen space.

But there are probably many people that would use this in general use as it reduces the need to flip focus between live preview and vscode to see your live changes.

### Product Impact

_No response_


Discussion / hints:
![Screenshot 2025-01-15 at 14 50 37](https://github.com/user-attachments/assets/d7dfde0f-f71f-479d-99f8-3344af645a2d)
Following Apple's own apps this would be a toggle menu under 'window'. It's a bit odd looking having a menu with only a single item, but this is the way.

Fix patch:
diff --git a/tools/lsp/preview/native.rs b/tools/lsp/preview/native.rs
index 9568aed8f7d..09bb90d3a38 100644
--- a/tools/lsp/preview/native.rs
+++ b/tools/lsp/preview/native.rs
@@ -219,6 +219,19 @@ fn close_ui_impl(preview_state: &mut PreviewState) {
     }
 }
 
+#[cfg(target_vendor = "apple")]
+fn toggle_always_on_top() {
+    i_slint_core::api::invoke_from_event_loop(move || {
+        super::PREVIEW_STATE.with(move |preview_state| {
+            let preview_state = preview_state.borrow_mut();
+            let Some(ui) = preview_state.ui.as_ref() else { return };
+            let api = ui.global::<crate::preview::ui::Api>();
+            api.set_always_on_top(!api.get_always_on_top());
+        });
+    })
+    .unwrap(); // TODO: Handle Error
+}
+
 static SERVER_NOTIFIER: Mutex<Option<ServerNotifier>> = Mutex::new(None);
 
 /// Give the UI thread a handle to send message back to the LSP thread
@@ -270,9 +283,10 @@ pub fn send_message_to_lsp(message: PreviewToLspMessage) {
 // be kept alive for the duration of the event loop, as otherwise muda crashes.
 #[cfg(target_vendor = "apple")]
 fn init_apple_platform(
-) -> Result<(muda::MenuItem, muda::MenuItem), i_slint_core::api::PlatformError> {
+) -> Result<(muda::MenuItem, muda::MenuItem, muda::CheckMenuItem), i_slint_core::api::PlatformError>
+{
     use i_slint_backend_winit::winit;
-    use muda::{accelerator, Menu, MenuItem, PredefinedMenuItem, Submenu};
+    use muda::{accelerator, CheckMenuItem, Menu, MenuItem, PredefinedMenuItem, Submenu};
     use winit::platform::macos::EventLoopBuilderExtMacOS;
     let mut builder = winit::event_loop::EventLoop::with_user_event();
     builder.with_default_menu(false);
@@ -301,12 +315,15 @@ fn init_apple_platform(
             accelerator::Code::KeyR,
         )),
     );
+    let keep_on_top_menu_item = CheckMenuItem::new(format!("Keep on Top"), true, false, None);
 
     let menu_bar = Menu::new();
     menu_bar.init_for_nsapp();
     let app_m = Submenu::new("App", true);
+    let window_m = Submenu::new("&Window", true);
     menu_bar
         .append(&app_m)
+        .and_then(|_| menu_bar.append(&window_m))
         .and_then(|_| {
             app_m.append_items(&[
                 &PredefinedMenuItem::services(None),
@@ -318,20 +335,24 @@ fn init_apple_platform(
                 &close_app_menu_item,
             ])
         })
+        .and_then(|_| window_m.append_items(&[&keep_on_top_menu_item]))
         .map_err(|menu_bar_err| {
             i_slint_core::api::PlatformError::Other(menu_bar_err.to_string())
         })?;
 
     let close_id = close_app_menu_item.id().clone();
     let reload_id = reload_menu_item.id().clone();
+    let keep_on_top_id = keep_on_top_menu_item.id().clone();
 
     muda::MenuEvent::set_event_handler(Some(move |menu_event: muda::MenuEvent| {
         if menu_event.id == close_id {
             close_ui();
         } else if menu_event.id == reload_id {
             super::reload_preview();
+        } else if menu_event.id == keep_on_top_id {
+            toggle_always_on_top();
         }
     }));
 
-    Ok((close_app_menu_item, reload_menu_item))
+    Ok((close_app_menu_item, reload_menu_item, keep_on_top_menu_item))
 }
diff --git a/tools/lsp/ui/api.slint b/tools/lsp/ui/api.slint
index 69df517d997..cd277e55f0d 100644
--- a/tools/lsp/ui/api.slint
+++ b/tools/lsp/ui/api.slint
@@ -182,6 +182,7 @@ export global Api {
     in property <bool> show-preview-ui: true;
     // std-widgets are used (=> show style dropdown)
     in-out property <bool> uses-widgets;
+    in-out property <bool> always-on-top;
 
     // ## Component Data for ComponentList:
     // All the components
diff --git a/tools/lsp/ui/main.slint b/tools/lsp/ui/main.slint
index 4ed7d555078..fd5afc35437 100644
--- a/tools/lsp/ui/main.slint
+++ b/tools/lsp/ui/main.slint
@@ -31,6 +31,7 @@ export component PreviewUi inherits Window {
 
     title: "Slint Live-Preview";
     icon: @image-url("assets/slint-logo-small-light.png");
+    always-on-top <=> Api.always-on-top;
 
     VerticalLayout {
         if !Api.show-preview-ui: no-ui-drawing-rect := Rectangle {


Test patch: