Browse Source

feat(macOS): Implement tray icon template (#2322)

david 4 years ago
parent
commit
426a6b4996

+ 9 - 0
.changes/tauri-macos-tray-icon-template.md

@@ -0,0 +1,9 @@
+---
+"tauri": patch
+"tauri-runtime": patch
+"tauri-runtime-wry": patch
+---
+
+- Support [macOS tray icon template](https://developer.apple.com/documentation/appkit/nsimage/1520017-template?language=objc) to adjust automatically based on taskbar color.
+
+- Images you mark as template images should consist of only black and clear colors. You can use the alpha channel in the image to adjust the opacity of black content, however.

+ 22 - 0
core/tauri-runtime-wry/src/lib.rs

@@ -25,6 +25,8 @@ use tauri_runtime::{SystemTray, SystemTrayEvent};
 use winapi::shared::windef::HWND;
 use winapi::shared::windef::HWND;
 #[cfg(target_os = "macos")]
 #[cfg(target_os = "macos")]
 use wry::application::platform::macos::WindowExtMacOS;
 use wry::application::platform::macos::WindowExtMacOS;
+#[cfg(all(feature = "system-tray", target_os = "macos"))]
+use wry::application::platform::macos::{SystemTrayBuilderExtMacOS, SystemTrayExtMacOS};
 #[cfg(target_os = "linux")]
 #[cfg(target_os = "linux")]
 use wry::application::platform::unix::{WindowBuilderExtUnix, WindowExtUnix};
 use wry::application::platform::unix::{WindowBuilderExtUnix, WindowExtUnix};
 #[cfg(windows)]
 #[cfg(windows)]
@@ -725,6 +727,8 @@ enum WebviewEvent {
 pub(crate) enum TrayMessage {
 pub(crate) enum TrayMessage {
   UpdateItem(u16, menu::MenuUpdate),
   UpdateItem(u16, menu::MenuUpdate),
   UpdateIcon(Icon),
   UpdateIcon(Icon),
+  #[cfg(target_os = "macos")]
+  UpdateIconAsTemplate(bool),
 }
 }
 
 
 #[derive(Clone)]
 #[derive(Clone)]
@@ -1448,6 +1452,18 @@ impl Runtime for Wry {
 
 
     let mut items = HashMap::new();
     let mut items = HashMap::new();
 
 
+    #[cfg(target_os = "macos")]
+    let tray = SystemTrayBuilder::new(
+      icon,
+      system_tray
+        .menu
+        .map(|menu| to_wry_context_menu(&mut items, menu)),
+    )
+    .with_icon_as_template(system_tray.icon_as_template)
+    .build(&self.event_loop)
+    .map_err(|e| Error::SystemTray(Box::new(e)))?;
+
+    #[cfg(not(target_os = "macos"))]
     let tray = SystemTrayBuilder::new(
     let tray = SystemTrayBuilder::new(
       icon,
       icon,
       system_tray
       system_tray
@@ -1940,6 +1956,12 @@ fn handle_event_loop(
             tray.lock().unwrap().set_icon(icon.into_tray_icon());
             tray.lock().unwrap().set_icon(icon.into_tray_icon());
           }
           }
         }
         }
+        #[cfg(target_os = "macos")]
+        TrayMessage::UpdateIconAsTemplate(is_template) => {
+          if let Some(tray) = &*tray_context.tray.lock().unwrap() {
+            tray.lock().unwrap().set_icon_as_template(is_template);
+          }
+        }
       },
       },
       Message::GlobalShortcut(message) => match message {
       Message::GlobalShortcut(message) => match message {
         GlobalShortcutMessage::IsRegistered(accelerator, tx) => tx
         GlobalShortcutMessage::IsRegistered(accelerator, tx) => tx

+ 9 - 0
core/tauri-runtime-wry/src/menu.rs

@@ -75,6 +75,15 @@ impl TrayHandle for SystemTrayHandle {
       .send_event(Message::Tray(TrayMessage::UpdateItem(id, update)))
       .send_event(Message::Tray(TrayMessage::UpdateItem(id, update)))
       .map_err(|_| Error::FailedToSendMessage)
       .map_err(|_| Error::FailedToSendMessage)
   }
   }
+  #[cfg(target_os = "macos")]
+  fn set_icon_as_template(&self, is_template: bool) -> tauri_runtime::Result<()> {
+    self
+      .proxy
+      .send_event(Message::Tray(TrayMessage::UpdateIconAsTemplate(
+        is_template,
+      )))
+      .map_err(|_| Error::FailedToSendMessage)
+  }
 }
 }
 
 
 #[cfg(target_os = "macos")]
 #[cfg(target_os = "macos")]

+ 11 - 0
core/tauri-runtime/src/lib.rs

@@ -34,6 +34,8 @@ use window::{
 pub struct SystemTray {
 pub struct SystemTray {
   pub icon: Option<Icon>,
   pub icon: Option<Icon>,
   pub menu: Option<menu::SystemTrayMenu>,
   pub menu: Option<menu::SystemTrayMenu>,
+  #[cfg(target_os = "macos")]
+  pub icon_as_template: bool,
 }
 }
 
 
 #[cfg(feature = "system-tray")]
 #[cfg(feature = "system-tray")]
@@ -42,6 +44,8 @@ impl Default for SystemTray {
     Self {
     Self {
       icon: None,
       icon: None,
       menu: None,
       menu: None,
+      #[cfg(target_os = "macos")]
+      icon_as_template: false,
     }
     }
   }
   }
 }
 }
@@ -63,6 +67,13 @@ impl SystemTray {
     self
     self
   }
   }
 
 
+  /// Sets the tray icon as template.
+  #[cfg(target_os = "macos")]
+  pub fn with_icon_as_template(mut self, is_template: bool) -> Self {
+    self.icon_as_template = is_template;
+    self
+  }
+
   /// Sets the menu to show when the system tray is right clicked.
   /// Sets the menu to show when the system tray is right clicked.
   pub fn with_menu(mut self, menu: menu::SystemTrayMenu) -> Self {
   pub fn with_menu(mut self, menu: menu::SystemTrayMenu) -> Self {
     self.menu.replace(menu);
     self.menu.replace(menu);

+ 2 - 0
core/tauri-runtime/src/menu.rs

@@ -148,6 +148,8 @@ pub enum MenuUpdate {
 pub trait TrayHandle {
 pub trait TrayHandle {
   fn set_icon(&self, icon: crate::Icon) -> crate::Result<()>;
   fn set_icon(&self, icon: crate::Icon) -> crate::Result<()>;
   fn update_item(&self, id: u16, update: MenuUpdate) -> crate::Result<()>;
   fn update_item(&self, id: u16, update: MenuUpdate) -> crate::Result<()>;
+  #[cfg(target_os = "macos")]
+  fn set_icon_as_template(&self, is_template: bool) -> crate::Result<()>;
 }
 }
 
 
 /// A window menu.
 /// A window menu.

+ 6 - 1
core/tauri-utils/src/config.rs

@@ -209,6 +209,9 @@ pub struct SystemTrayConfig {
   /// Path to the icon to use on the system tray.
   /// Path to the icon to use on the system tray.
   /// Automatically set to be an `.png` on macOS and Linux, and `.ico` on Windows.
   /// Automatically set to be an `.png` on macOS and Linux, and `.ico` on Windows.
   pub icon_path: PathBuf,
   pub icon_path: PathBuf,
+  /// A Boolean value that determines whether the image represents a [template](https://developer.apple.com/documentation/appkit/nsimage/1520017-template?language=objc) image on macOS.
+  #[serde(default)]
+  pub icon_as_template: bool,
 }
 }
 
 
 /// A CLI argument definition
 /// A CLI argument definition
@@ -391,6 +394,7 @@ pub struct TauriConfig {
   #[serde(default)]
   #[serde(default)]
   pub security: SecurityConfig,
   pub security: SecurityConfig,
   /// System tray configuration.
   /// System tray configuration.
+  #[serde(default)]
   pub system_tray: Option<SystemTrayConfig>,
   pub system_tray: Option<SystemTrayConfig>,
 }
 }
 
 
@@ -866,8 +870,9 @@ mod build {
 
 
   impl ToTokens for SystemTrayConfig {
   impl ToTokens for SystemTrayConfig {
     fn to_tokens(&self, tokens: &mut TokenStream) {
     fn to_tokens(&self, tokens: &mut TokenStream) {
+      let icon_as_template = self.icon_as_template;
       let icon_path = path_buf_lit(&self.icon_path);
       let icon_path = path_buf_lit(&self.icon_path);
-      literal_struct!(tokens, SystemTrayConfig, icon_path);
+      literal_struct!(tokens, SystemTrayConfig, icon_path, icon_as_template);
     }
     }
   }
   }
 
 

+ 29 - 0
core/tauri/src/app.rs

@@ -759,6 +759,15 @@ impl<R: Runtime> Builder<R> {
       icon
       icon
     };
     };
 
 
+    #[cfg(all(feature = "system-tray", target_os = "macos"))]
+    let system_tray_icon_as_template = context
+      .config
+      .tauri
+      .system_tray
+      .as_ref()
+      .map(|t| t.icon_as_template)
+      .unwrap_or_default();
+
     let manager = WindowManager::with_handlers(
     let manager = WindowManager::with_handlers(
       context,
       context,
       self.plugins,
       self.plugins,
@@ -844,6 +853,8 @@ impl<R: Runtime> Builder<R> {
       if let Some(menu) = system_tray.menu {
       if let Some(menu) = system_tray.menu {
         tray = tray.with_menu(menu);
         tray = tray.with_menu(menu);
       }
       }
+
+      #[cfg(not(target_os = "macos"))]
       let tray_handler = app
       let tray_handler = app
         .runtime
         .runtime
         .as_ref()
         .as_ref()
@@ -857,6 +868,24 @@ impl<R: Runtime> Builder<R> {
           ),
           ),
         )
         )
         .expect("failed to run tray");
         .expect("failed to run tray");
+
+      #[cfg(target_os = "macos")]
+      let tray_handler = app
+        .runtime
+        .as_ref()
+        .unwrap()
+        .system_tray(
+          tray
+            .with_icon(
+              system_tray
+                .icon
+                .or(system_tray_icon)
+                .expect("tray icon not found; please configure it on tauri.conf.json"),
+            )
+            .with_icon_as_template(system_tray_icon_as_template),
+        )
+        .expect("failed to run tray");
+
       let tray_handle = tray::SystemTrayHandle {
       let tray_handle = tray::SystemTrayHandle {
         ids: Arc::new(ids.clone()),
         ids: Arc::new(ids.clone()),
         inner: tray_handler,
         inner: tray_handler,

+ 9 - 0
core/tauri/src/app/tray.rs

@@ -126,6 +126,15 @@ impl<R: Runtime> SystemTrayHandle<R> {
   pub fn set_icon(&self, icon: Icon) -> crate::Result<()> {
   pub fn set_icon(&self, icon: Icon) -> crate::Result<()> {
     self.inner.set_icon(icon).map_err(Into::into)
     self.inner.set_icon(icon).map_err(Into::into)
   }
   }
+
+  /// Support [macOS tray icon template](https://developer.apple.com/documentation/appkit/nsimage/1520017-template?language=objc) to adjust automatically based on taskbar color.
+  #[cfg(target_os = "macos")]
+  pub fn set_icon_as_template(&self, is_template: bool) -> crate::Result<()> {
+    self
+      .inner
+      .set_icon_as_template(is_template)
+      .map_err(Into::into)
+  }
 }
 }
 
 
 impl<R: Runtime> SystemTrayMenuItemHandle<R> {
 impl<R: Runtime> SystemTrayMenuItemHandle<R> {

+ 5 - 1
docs/usage/guides/visual/system-tray.md

@@ -12,7 +12,8 @@ Configure the `systemTray` object on `tauri.conf.json`:
 {
 {
   "tauri": {
   "tauri": {
     "systemTray": {
     "systemTray": {
-      "iconPath": "icons/icon.png"
+      "iconPath": "icons/icon.png",
+      "iconAsTemplate": true,
     }
     }
   }
   }
 }
 }
@@ -20,6 +21,9 @@ Configure the `systemTray` object on `tauri.conf.json`:
 
 
 The `iconPath` is pointed to a PNG file on macOS and Linux, and a `.ico` file must exist for Windows support.
 The `iconPath` is pointed to a PNG file on macOS and Linux, and a `.ico` file must exist for Windows support.
 
 
+The `iconAsTemplate` is a boolean value that determines whether the image represents a [template](https://developer.apple.com/documentation/appkit/nsimage/1520017-template?language=objc) image on macOS.
+
+
 ### Creating a system tray
 ### Creating a system tray
 
 
 To create a native system tray, import the `SystemTray` type:
 To create a native system tray, import the `SystemTray` type:

BIN
examples/.icons/tray_icon.png


BIN
examples/.icons/tray_icon_with_transparency.ico


BIN
examples/.icons/tray_icon_with_transparency.png


+ 56 - 1
examples/api/src-tauri/src/main.rs

@@ -14,6 +14,9 @@
 mod cmd;
 mod cmd;
 mod menu;
 mod menu;
 
 
+#[cfg(target_os = "linux")]
+use std::path::PathBuf;
+
 use serde::Serialize;
 use serde::Serialize;
 use tauri::{
 use tauri::{
   CustomMenuItem, Event, Manager, SystemTray, SystemTrayEvent, SystemTrayMenu, WindowBuilder,
   CustomMenuItem, Event, Manager, SystemTray, SystemTrayEvent, SystemTrayMenu, WindowBuilder,
@@ -53,7 +56,9 @@ fn main() {
       SystemTray::new().with_menu(
       SystemTray::new().with_menu(
         SystemTrayMenu::new()
         SystemTrayMenu::new()
           .add_item(CustomMenuItem::new("toggle", "Toggle"))
           .add_item(CustomMenuItem::new("toggle", "Toggle"))
-          .add_item(CustomMenuItem::new("new", "New window")),
+          .add_item(CustomMenuItem::new("new", "New window"))
+          .add_item(CustomMenuItem::new("icon_1", "Tray Icon 1"))
+          .add_item(CustomMenuItem::new("icon_2", "Tray Icon 2")),
       ),
       ),
     )
     )
     .on_system_tray_event(|app, event| match event {
     .on_system_tray_event(|app, event| match event {
@@ -89,6 +94,56 @@ fn main() {
               },
               },
             )
             )
             .unwrap(),
             .unwrap(),
+          #[cfg(target_os = "macos")]
+          "icon_1" => {
+            app.tray_handle().set_icon_as_template(true).unwrap();
+
+            app
+              .tray_handle()
+              .set_icon(tauri::Icon::Raw(
+                include_bytes!("../../../.icons/tray_icon_with_transparency.png").to_vec(),
+              ))
+              .unwrap();
+          }
+          #[cfg(target_os = "macos")]
+          "icon_2" => {
+            app.tray_handle().set_icon_as_template(true).unwrap();
+
+            app
+              .tray_handle()
+              .set_icon(tauri::Icon::Raw(
+                include_bytes!("../../../.icons/tray_icon.png").to_vec(),
+              ))
+              .unwrap();
+          }
+          #[cfg(target_os = "linux")]
+          "icon_1" => app
+            .tray_handle()
+            .set_icon(tauri::Icon::File(PathBuf::from(
+              "../../../.icons/tray_icon_with_transparency.png",
+            )))
+            .unwrap(),
+          #[cfg(target_os = "linux")]
+          "icon_2" => app
+            .tray_handle()
+            .set_icon(tauri::Icon::File(PathBuf::from(
+              "../../../.icons/tray_icon.png",
+            )))
+            .unwrap(),
+          #[cfg(target_os = "windows")]
+          "icon_1" => app
+            .tray_handle()
+            .set_icon(tauri::Icon::Raw(
+              include_bytes!("../../../.icons/tray_icon_with_transparency.ico").to_vec(),
+            ))
+            .unwrap(),
+          #[cfg(target_os = "windows")]
+          "icon_2" => app
+            .tray_handle()
+            .set_icon(tauri::Icon::Raw(
+              include_bytes!("../../../.icons/icon.ico").to_vec(),
+            ))
+            .unwrap(),
           _ => {}
           _ => {}
         }
         }
       }
       }

+ 2 - 1
examples/api/src-tauri/tauri.conf.json

@@ -78,7 +78,8 @@
       "csp": "default-src blob: data: filesystem: ws: wss: http: https: tauri: asset: 'unsafe-eval' 'unsafe-inline' 'self' img-src: 'self'"
       "csp": "default-src blob: data: filesystem: ws: wss: http: https: tauri: asset: 'unsafe-eval' 'unsafe-inline' 'self' img-src: 'self'"
     },
     },
     "systemTray": {
     "systemTray": {
-      "iconPath": "../../.icons/icon.png"
+      "iconPath": "../../.icons/tray_icon_with_transparency.png",
+      "iconAsTemplate": true
     }
     }
   }
   }
 }
 }

+ 3 - 0
tooling/cli.rs/config_definition.rs

@@ -623,6 +623,9 @@ pub struct SystemTrayConfig {
   ///
   ///
   /// It is forced to be a `.png` file on Linux and macOS, and a `.ico` file on Windows.
   /// It is forced to be a `.png` file on Linux and macOS, and a `.ico` file on Windows.
   pub icon_path: PathBuf,
   pub icon_path: PathBuf,
+  /// A Boolean value that determines whether the image represents a [template](https://developer.apple.com/documentation/appkit/nsimage/1520017-template?language=objc) image on macOS.
+  #[serde(default)]
+  pub icon_as_template: bool,
 }
 }
 
 
 // We enable the unnecessary_wraps because we need
 // We enable the unnecessary_wraps because we need

+ 5 - 0
tooling/cli.rs/schema.json

@@ -902,6 +902,11 @@
         "iconPath"
         "iconPath"
       ],
       ],
       "properties": {
       "properties": {
+        "iconAsTemplate": {
+          "description": "A Boolean value that determines whether the image represents a [template](https://developer.apple.com/documentation/appkit/nsimage/1520017-template?language=objc) image on macOS.",
+          "default": false,
+          "type": "boolean"
+        },
         "iconPath": {
         "iconPath": {
           "description": "Path to the icon to use on the system tray.\n\nIt is forced to be a `.png` file on Linux and macOS, and a `.ico` file on Windows.",
           "description": "Path to the icon to use on the system tray.\n\nIt is forced to be a `.png` file on Linux and macOS, and a `.ico` file on Windows.",
           "type": "string"
           "type": "string"