diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 05d9cfcbbd..f035d82458 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -6222,8 +6222,6 @@ dependencies = [ [[package]] name = "tray-icon" version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e85aa143ceb072062fc4d6356c1b520a51d636e7bc8e77ec94be3608e5e80c" dependencies = [ "crossbeam-channel", "dirs 6.0.0", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 9136b8ac74..95ff0b87a1 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -104,3 +104,11 @@ strip = "symbols" [dev-dependencies] serial_test = "3" tempfile = "3" + +# Local fork of tray-icon 0.21.3 patched for tauri-apps/tray-icon#251 — +# the upstream `performClick(None)` synthesizes a click with no NSEvent +# context, so the menu silently fails to pop up on a secondary display while a +# full-screen Space is active there. The vendored copy drives the popup via +# NSStatusItem directly. Remove this section once the upstream library ships a fix. +[patch.crates-io] +tray-icon = { path = "vendor/tray-icon" } diff --git a/src-tauri/vendor/tray-icon/Cargo.toml b/src-tauri/vendor/tray-icon/Cargo.toml new file mode 100644 index 0000000000..80dcaf842a --- /dev/null +++ b/src-tauri/vendor/tray-icon/Cargo.toml @@ -0,0 +1,162 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2021" +rust-version = "1.71" +name = "tray-icon" +version = "0.21.3" +build = false +include = [ + "README.md", + "src/**/*.rs", + "Cargo.toml", + "LICENSE-APACHE", + "LICENSE-MIT", + "LICENSE.spdx", +] +autolib = false +autobins = false +autoexamples = false +autotests = false +autobenches = false +description = "Create tray icons for desktop applications" +homepage = "https://github.com/tauri-apps/tray-icon" +readme = "README.md" +categories = ["gui"] +license = "MIT OR Apache-2.0" +repository = "https://github.com/tauri-apps/tray-icon" + +[features] +common-controls-v6 = ["muda/common-controls-v6"] +default = ["libxdo"] +libxdo = ["muda/libxdo"] +serde = [ + "muda/serde", + "dep:serde", +] + +[lib] +name = "tray_icon" +path = "src/lib.rs" + +[dependencies.crossbeam-channel] +version = "0.5" + +[dependencies.muda] +version = "0.17" +features = ["gtk"] +default-features = false + +[dependencies.once_cell] +version = "1" + +[dependencies.serde] +version = "1" +optional = true + +[dependencies.thiserror] +version = "2" + +[dev-dependencies.eframe] +version = "0.31" + +[dev-dependencies.image] +version = "0.25" +features = ["png"] +default-features = false + +[dev-dependencies.serde_json] +version = "1" + +[dev-dependencies.tao] +version = "0.34" + +[dev-dependencies.winit] +version = "0.30" + +[target.'cfg(any(target_os = "linux", target_os = "macos"))'.dependencies.png] +version = "0.17" + +[target.'cfg(target_os = "linux")'.dependencies.dirs] +version = "6" + +[target.'cfg(target_os = "linux")'.dependencies.libappindicator] +version = "0.9" + +[target.'cfg(target_os = "linux")'.dev-dependencies.gtk] +version = "0.18" + +[target.'cfg(target_os = "macos")'.dependencies.objc2] +version = "0.6" + +[target.'cfg(target_os = "macos")'.dependencies.objc2-app-kit] +version = "0.3" +features = [ + "std", + "objc2-core-foundation", + "NSButton", + "NSCell", + "NSControl", + "NSEvent", + "NSImage", + "NSMenu", + "NSResponder", + "NSStatusBar", + "NSStatusBarButton", + "NSStatusItem", + "NSTrackingArea", + "NSView", + "NSWindow", +] +default-features = false + +[target.'cfg(target_os = "macos")'.dependencies.objc2-core-foundation] +version = "0.3" +features = [ + "std", + "CFCGTypes", + "CFRunLoop", +] +default-features = false + +[target.'cfg(target_os = "macos")'.dependencies.objc2-core-graphics] +version = "0.3" +features = [ + "std", + "CGDirectDisplay", +] +default-features = false + +[target.'cfg(target_os = "macos")'.dependencies.objc2-foundation] +version = "0.3" +features = [ + "std", + "block2", + "objc2-core-foundation", + "NSArray", + "NSData", + "NSEnumerator", + "NSGeometry", + "NSString", + "NSThread", +] +default-features = false + +[target.'cfg(target_os = "windows")'.dependencies.windows-sys] +version = "0.60" +features = [ + "Win32_UI_WindowsAndMessaging", + "Win32_Foundation", + "Win32_System_SystemServices", + "Win32_Graphics_Gdi", + "Win32_UI_Shell", +] diff --git a/src-tauri/vendor/tray-icon/LICENSE-APACHE b/src-tauri/vendor/tray-icon/LICENSE-APACHE new file mode 100644 index 0000000000..16fe87b06e --- /dev/null +++ b/src-tauri/vendor/tray-icon/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/src-tauri/vendor/tray-icon/LICENSE-MIT b/src-tauri/vendor/tray-icon/LICENSE-MIT new file mode 100644 index 0000000000..da5faffb87 --- /dev/null +++ b/src-tauri/vendor/tray-icon/LICENSE-MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022-2022 Tauri Programme within The Commons Conservancy + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src-tauri/vendor/tray-icon/LICENSE.spdx b/src-tauri/vendor/tray-icon/LICENSE.spdx new file mode 100644 index 0000000000..c6f204fdb0 --- /dev/null +++ b/src-tauri/vendor/tray-icon/LICENSE.spdx @@ -0,0 +1,19 @@ +SPDXVersion: SPDX-2.1 +DataLicense: CC0-1.0 +PackageName: tray-icon +DataFormat: SPDXRef-1 +PackageSupplier: Organization: The Tauri Programme in the Commons Conservancy +PackageHomePage: https://tauri.app +PackageLicenseDeclared: Apache-2.0 +PackageLicenseDeclared: MIT +PackageCopyrightText: 2020-2022, The Tauri Programme in the Commons Conservancy +PackageSummary: Create tray icons for desktop applications. + +PackageComment: The package includes the following libraries; see +Relationship information. + +Created: 2022-12-05T09:00:00Z +PackageDownloadLocation: git://github.com/tauri-apps/tray-icon +PackageDownloadLocation: git+https://github.com/tauri-apps/tray-icon.git +PackageDownloadLocation: git+ssh://github.com/tauri-apps/tray-icon.git +Creator: Person: Daniel Thompson-Yvetot \ No newline at end of file diff --git a/src-tauri/vendor/tray-icon/README.md b/src-tauri/vendor/tray-icon/README.md new file mode 100644 index 0000000000..f131af1811 --- /dev/null +++ b/src-tauri/vendor/tray-icon/README.md @@ -0,0 +1,123 @@ +tray-icon lets you create tray icons for desktop applications. + +## Platforms supported: + +- Windows +- macOS +- Linux (gtk Only) + +## Platform-specific notes: + +- On Windows and Linux, an event loop must be running on the thread, on Windows, a win32 event loop and on Linux, a gtk event loop. It doesn't need to be the main thread but you have to create the tray icon on the same thread as the event loop. +- On macOS, an event loop must be running on the main thread so you also need to create the tray icon on the main thread. + +### Cargo Features + +- `common-controls-v6`: Use `TaskDialogIndirect` API from `ComCtl32.dll` v6 on Windows for showing the predefined `About` menu item dialog. +- `libxdo`: Enables linking to `libxdo` which is used for the predfined `Copy`, `Cut`, `Paste` and `SelectAll` menu item, see https://github.com/tauri-apps/muda#cargo-features +- `serde`: Enables de/serializing derives. + +## Dependencies (Linux Only) + +On Linux, `gtk`, `libxdo` is used to make the predfined `Copy`, `Cut`, `Paste` and `SelectAll` menu items work and `libappindicator` or `libayatnat-appindicator` are used to create the tray icon, so make sure to install them on your system. + +#### Arch Linux / Manjaro: + +```sh +pacman -S gtk3 xdotool libappindicator-gtk3 #or libayatana-appindicator +``` + +#### Debian / Ubuntu: + +```sh +sudo apt install libgtk-3-dev libxdo-dev libappindicator3-dev #or libayatana-appindicator3-dev +``` + +## Examples + +#### Create a tray icon without a menu. + +```rs +use tray_icon::TrayIconBuilder; + +let tray_icon = TrayIconBuilder::new() + .with_tooltip("system-tray - tray icon library!") + .with_icon(icon) + .build() + .unwrap(); +``` + +#### Create a tray icon with a menu. + +```rs +use tray_icon::{TrayIconBuilder, menu::Menu}; + +let tray_menu = Menu::new(); +let tray_icon = TrayIconBuilder::new() + .with_menu(Box::new(tray_menu)) + .with_tooltip("system-tray - tray icon library!") + .with_icon(icon) + .build() + .unwrap(); +``` + +## Processing tray events + +You can use `TrayIconEvent::receiver` to get a reference to the `TrayIconEventReceiver` +which you can use to listen to events when a click happens on the tray icon + +```rs +use tray_icon::TrayIconEvent; + +if let Ok(event) = TrayIconEvent::receiver().try_recv() { + println!("{:?}", event); +} +``` + +You can also listen for the menu events using `MenuEvent::receiver` to get events for the tray context menu. + +```rs +use tray_icon::{TrayIconEvent, menu::{MenuEvent}}; + +if let Ok(event) = TrayIconEvent::receiver().try_recv() { + println!("tray event: {:?}", event); +} + +if let Ok(event) = MenuEvent::receiver().try_recv() { + println!("menu event: {:?}", event); +} +``` + +### Note for [winit] or [tao] users: + +You should use [`TrayIconEvent::set_event_handler`] and forward +the tray icon events to the event loop by using [`EventLoopProxy`] +so that the event loop is awakened on each tray icon event. +Same can be done for menu events using [`MenuEvent::set_event_handler`]. + +```rust +enum UserEvent { + TrayIconEvent(tray_icon::TrayIconEvent) + MenuEvent(tray_icon::menu::MenuEvent) +} + +let event_loop = EventLoop::::with_user_event().build().unwrap(); + +let proxy = event_loop.create_proxy(); +tray_icon::TrayIconEvent::set_event_handler(Some(move |event| { + proxy.send_event(UserEvent::TrayIconEvent(event)); +})); + +let proxy = event_loop.create_proxy(); +tray_icon::menu::MenuEvent::set_event_handler(Some(move |event| { + proxy.send_event(UserEvent::MenuEvent(event)); +})); +``` + +[`EventLoopProxy`]: https://docs.rs/winit/latest/winit/event_loop/struct.EventLoopProxy.html +[winit]: https://docs.rs/winit +[tao]: https://docs.rs/tao + +## License + +Apache-2.0/MIT diff --git a/src-tauri/vendor/tray-icon/src/counter.rs b/src-tauri/vendor/tray-icon/src/counter.rs new file mode 100644 index 0000000000..f2610ee8c3 --- /dev/null +++ b/src-tauri/vendor/tray-icon/src/counter.rs @@ -0,0 +1,17 @@ +// Copyright 2022-2022 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::sync::atomic::{AtomicU32, Ordering}; + +pub struct Counter(AtomicU32); + +impl Counter { + pub const fn new() -> Self { + Self(AtomicU32::new(1)) + } + + pub fn next(&self) -> u32 { + self.0.fetch_add(1, Ordering::Relaxed) + } +} diff --git a/src-tauri/vendor/tray-icon/src/error.rs b/src-tauri/vendor/tray-icon/src/error.rs new file mode 100644 index 0000000000..e479bc69f9 --- /dev/null +++ b/src-tauri/vendor/tray-icon/src/error.rs @@ -0,0 +1,21 @@ +// Copyright 2022-2022 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use thiserror::Error; + +/// Errors returned by tray-icon. +#[non_exhaustive] +#[derive(Error, Debug)] +pub enum Error { + #[error(transparent)] + OsError(#[from] std::io::Error), + #[cfg(any(target_os = "linux", target_os = "macos"))] + #[error(transparent)] + PngEncodingError(#[from] png::EncodingError), + #[error("not on the main thread")] + NotMainThread, +} + +/// Convenient type alias of Result type for tray-icon. +pub type Result = std::result::Result; diff --git a/src-tauri/vendor/tray-icon/src/icon.rs b/src-tauri/vendor/tray-icon/src/icon.rs new file mode 100644 index 0000000000..192891c87e --- /dev/null +++ b/src-tauri/vendor/tray-icon/src/icon.rs @@ -0,0 +1,187 @@ +// Copyright 2022-2022 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +// taken from https://github.com/rust-windowing/winit/blob/92fdf5ba85f920262a61cee4590f4a11ad5738d1/src/icon.rs + +use crate::platform_impl::PlatformIcon; +use std::{error::Error, fmt, io, mem}; + +#[repr(C)] +#[derive(Debug)] +pub(crate) struct Pixel { + pub(crate) r: u8, + pub(crate) g: u8, + pub(crate) b: u8, + pub(crate) a: u8, +} + +pub(crate) const PIXEL_SIZE: usize = mem::size_of::(); + +#[derive(Debug)] +/// An error produced when using [`Icon::from_rgba`] with invalid arguments. +pub enum BadIcon { + /// Produced when the length of the `rgba` argument isn't divisible by 4, thus `rgba` can't be + /// safely interpreted as 32bpp RGBA pixels. + ByteCountNotDivisibleBy4 { byte_count: usize }, + /// Produced when the number of pixels (`rgba.len() / 4`) isn't equal to `width * height`. + /// At least one of your arguments is incorrect. + DimensionsVsPixelCount { + width: u32, + height: u32, + width_x_height: usize, + pixel_count: usize, + }, + /// Produced when underlying OS functionality failed to create the icon + OsError(io::Error), +} + +impl fmt::Display for BadIcon { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + BadIcon::ByteCountNotDivisibleBy4 { byte_count } => write!(f, + "The length of the `rgba` argument ({:?}) isn't divisible by 4, making it impossible to interpret as 32bpp RGBA pixels.", + byte_count, + ), + BadIcon::DimensionsVsPixelCount { + width, + height, + width_x_height, + pixel_count, + } => write!(f, + "The specified dimensions ({:?}x{:?}) don't match the number of pixels supplied by the `rgba` argument ({:?}). For those dimensions, the expected pixel count is {:?}.", + width, height, pixel_count, width_x_height, + ), + BadIcon::OsError(e) => write!(f, "OS error when instantiating the icon: {:?}", e), + } + } +} + +impl Error for BadIcon { + fn source(&self) -> Option<&(dyn Error + 'static)> { + match self { + BadIcon::OsError(e) => Some(e), + _ => None, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct RgbaIcon { + pub(crate) rgba: Vec, + pub(crate) width: u32, + pub(crate) height: u32, +} + +/// For platforms which don't have window icons (e.g. web) +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct NoIcon; + +#[allow(dead_code)] // These are not used on every platform +mod constructors { + use super::*; + + impl RgbaIcon { + pub fn from_rgba(rgba: Vec, width: u32, height: u32) -> Result { + if rgba.len() % PIXEL_SIZE != 0 { + return Err(BadIcon::ByteCountNotDivisibleBy4 { + byte_count: rgba.len(), + }); + } + let pixel_count = rgba.len() / PIXEL_SIZE; + if pixel_count != (width * height) as usize { + Err(BadIcon::DimensionsVsPixelCount { + width, + height, + width_x_height: (width * height) as usize, + pixel_count, + }) + } else { + Ok(RgbaIcon { + rgba, + width, + height, + }) + } + } + } + + impl NoIcon { + pub fn from_rgba(rgba: Vec, width: u32, height: u32) -> Result { + // Create the rgba icon anyway to validate the input + let _ = RgbaIcon::from_rgba(rgba, width, height)?; + Ok(NoIcon) + } + } +} + +/// An icon used for the window titlebar, taskbar, etc. +#[derive(Clone)] +pub struct Icon { + pub(crate) inner: PlatformIcon, +} + +impl fmt::Debug for Icon { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + fmt::Debug::fmt(&self.inner, formatter) + } +} + +impl Icon { + /// Creates an icon from 32bpp RGBA data. + /// + /// The length of `rgba` must be divisible by 4, and `width * height` must equal + /// `rgba.len() / 4`. Otherwise, this will return a `BadIcon` error. + pub fn from_rgba(rgba: Vec, width: u32, height: u32) -> Result { + Ok(Icon { + inner: PlatformIcon::from_rgba(rgba, width, height)?, + }) + } + + /// Create an icon from a file path. + /// + /// Specify `size` to load a specific icon size from the file, or `None` to load the default + /// icon size from the file. + /// + /// In cases where the specified size does not exist in the file, Windows may perform scaling + /// to get an icon of the desired size. + #[cfg(windows)] + pub fn from_path>( + path: P, + size: Option<(u32, u32)>, + ) -> Result { + let win_icon = PlatformIcon::from_path(path, size)?; + Ok(Icon { inner: win_icon }) + } + + /// Create an icon from a resource embedded in this executable or library. + /// + /// Specify `size` to load a specific icon size from the file, or `None` to load the default + /// icon size from the file. + /// + /// In cases where the specified size does not exist in the file, Windows may perform scaling + /// to get an icon of the desired size. + #[cfg(windows)] + pub fn from_resource(ordinal: u16, size: Option<(u32, u32)>) -> Result { + let win_icon = PlatformIcon::from_resource(ordinal, size)?; + Ok(Icon { inner: win_icon }) + } + + /// This is basically the same as from_resource, but takes a resource name + /// rather than oridinal id. + #[cfg(windows)] + pub fn from_resource_name( + resource_name: &str, + size: Option<(u32, u32)>, + ) -> Result { + let win_icon = PlatformIcon::from_resource_name(resource_name, size)?; + Ok(Icon { inner: win_icon }) + } + + /// Create an icon from an HICON + #[cfg(windows)] + pub fn from_handle(handle: isize) -> Self { + let win_icon = PlatformIcon::from_handle(handle as _); + Icon { inner: win_icon } + } +} diff --git a/src-tauri/vendor/tray-icon/src/lib.rs b/src-tauri/vendor/tray-icon/src/lib.rs new file mode 100644 index 0000000000..f56ca3dd04 --- /dev/null +++ b/src-tauri/vendor/tray-icon/src/lib.rs @@ -0,0 +1,687 @@ +// Copyright 2022-2022 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +#![allow(clippy::uninlined_format_args)] + +//! tray-icon lets you create tray icons for desktop applications. +//! +//! # Platforms supported: +//! +//! - Windows +//! - macOS +//! - Linux (gtk Only) +//! +//! # Platform-specific notes: +//! +//! - On Windows and Linux, an event loop must be running on the thread, on Windows, a win32 event loop and on Linux, a gtk event loop. It doesn't need to be the main thread but you have to create the tray icon on the same thread as the event loop. +//! - On macOS, an event loop must be running on the main thread so you also need to create the tray icon on the main thread. You must make sure that the event loop is already running and not just created before creating a TrayIcon to prevent issues with fullscreen apps. In Winit for example the earliest you can create icons is on [`StartCause::Init`](https://docs.rs/winit/latest/winit/event/enum.StartCause.html#variant.Init). +//! +//! # Dependencies (Linux Only) +//! +//! On Linux, `gtk`, `libxdo` is used to make the predfined `Copy`, `Cut`, `Paste` and `SelectAll` menu items work and `libappindicator` or `libayatnat-appindicator` are used to create the tray icon, so make sure to install them on your system. +//! +//! #### Arch Linux / Manjaro: +//! +//! ```sh +//! pacman -S gtk3 xdotool libappindicator-gtk3 #or libayatana-appindicator +//! ``` +//! +//! #### Debian / Ubuntu: +//! +//! ```sh +//! sudo apt install libgtk-3-dev libxdo-dev libappindicator3-dev #or libayatana-appindicator3-dev +//! ``` +//! +//! # Examples +//! +//! #### Create a tray icon without a menu. +//! +//! ```no_run +//! use tray_icon::{TrayIconBuilder, Icon}; +//! +//! # let icon = Icon::from_rgba(Vec::new(), 0, 0).unwrap(); +//! let tray_icon = TrayIconBuilder::new() +//! .with_tooltip("system-tray - tray icon library!") +//! .with_icon(icon) +//! .build() +//! .unwrap(); +//! ``` +//! +//! #### Create a tray icon with a menu. +//! +//! ```no_run +//! use tray_icon::{TrayIconBuilder, menu::Menu,Icon}; +//! +//! # let icon = Icon::from_rgba(Vec::new(), 0, 0).unwrap(); +//! let tray_menu = Menu::new(); +//! let tray_icon = TrayIconBuilder::new() +//! .with_menu(Box::new(tray_menu)) +//! .with_tooltip("system-tray - tray icon library!") +//! .with_icon(icon) +//! .build() +//! .unwrap(); +//! ``` +//! +//! # Processing tray events +//! +//! You can use [`TrayIconEvent::receiver`] to get a reference to the [`TrayIconEventReceiver`] +//! which you can use to listen to events when a click happens on the tray icon +//! ```no_run +//! use tray_icon::TrayIconEvent; +//! +//! if let Ok(event) = TrayIconEvent::receiver().try_recv() { +//! println!("{:?}", event); +//! } +//! ``` +//! +//! You can also listen for the menu events using [`MenuEvent::receiver`](crate::menu::MenuEvent::receiver) to get events for the tray context menu. +//! +//! ```no_run +//! use tray_icon::{TrayIconEvent, menu::MenuEvent}; +//! +//! if let Ok(event) = TrayIconEvent::receiver().try_recv() { +//! println!("tray event: {:?}", event); +//! } +//! +//! if let Ok(event) = MenuEvent::receiver().try_recv() { +//! println!("menu event: {:?}", event); +//! } +//! ``` +//! +//! ### Note for [winit] or [tao] users: +//! +//! You should use [`TrayIconEvent::set_event_handler`] and forward +//! the tray icon events to the event loop by using [`EventLoopProxy`] +//! so that the event loop is awakened on each tray icon event. +//! Same can be done for menu events using [`MenuEvent::set_event_handler`]. +//! +//! ```no_run +//! # use winit::event_loop::EventLoop; +//! enum UserEvent { +//! TrayIconEvent(tray_icon::TrayIconEvent), +//! MenuEvent(tray_icon::menu::MenuEvent) +//! } +//! +//! let event_loop = EventLoop::::with_user_event().build().unwrap(); +//! +//! let proxy = event_loop.create_proxy(); +//! tray_icon::TrayIconEvent::set_event_handler(Some(move |event| { +//! proxy.send_event(UserEvent::TrayIconEvent(event)); +//! })); +//! +//! let proxy = event_loop.create_proxy(); +//! tray_icon::menu::MenuEvent::set_event_handler(Some(move |event| { +//! proxy.send_event(UserEvent::MenuEvent(event)); +//! })); +//! ``` +//! +//! [`EventLoopProxy`]: https://docs.rs/winit/latest/winit/event_loop/struct.EventLoopProxy.html +//! [winit]: https://docs.rs/winit +//! [tao]: https://docs.rs/tao + +use std::{ + cell::RefCell, + path::{Path, PathBuf}, + rc::Rc, +}; + +use counter::Counter; +use crossbeam_channel::{unbounded, Receiver, Sender}; +use once_cell::sync::{Lazy, OnceCell}; + +mod counter; +mod error; +mod icon; +mod platform_impl; +mod tray_icon_id; + +pub use self::error::*; +pub use self::icon::{BadIcon, Icon}; +pub use self::tray_icon_id::TrayIconId; + +/// Re-export of [muda](::muda) crate and used for tray context menu. +pub mod menu { + pub use muda::*; +} +pub use muda::dpi; + +static COUNTER: Counter = Counter::new(); + +/// Attributes to use when creating a tray icon. +pub struct TrayIconAttributes { + /// Tray icon tooltip + /// + /// ## Platform-specific: + /// + /// - **Linux:** Unsupported. + pub tooltip: Option, + + /// Tray menu + /// + /// ## Platform-specific: + /// + /// - **Linux**: once a menu is set, it cannot be removed. + pub menu: Option>, + + /// Tray icon + /// + /// ## Platform-specific: + /// + /// - **Linux:** Sometimes the icon won't be visible unless a menu is set. + /// Setting an empty [`Menu`](crate::menu::Menu) is enough. + pub icon: Option, + + /// Tray icon temp dir path. **Linux only**. + pub temp_dir_path: Option, + + /// Use the icon as a [template](https://developer.apple.com/documentation/appkit/nsimage/1520017-template?language=objc). **macOS only**. + pub icon_is_template: bool, + + /// Whether to show the tray menu on left click or not, default is `true`. + /// + /// ## Platform-specific: + /// + /// - **Linux:** Unsupported. + pub menu_on_left_click: bool, + + /// Tray icon title. + /// + /// ## Platform-specific + /// + /// - **Linux:** The title will not be shown unless there is an icon + /// as well. The title is useful for numerical and other frequently + /// updated information. In general, it shouldn't be shown unless a + /// user requests it as it can take up a significant amount of space + /// on the user's panel. This may not be shown in all visualizations. + /// - **Windows:** Unsupported. + pub title: Option, +} + +impl Default for TrayIconAttributes { + fn default() -> Self { + Self { + tooltip: None, + menu: None, + icon: None, + temp_dir_path: None, + icon_is_template: false, + menu_on_left_click: true, + title: None, + } + } +} + +/// [`TrayIcon`] builder struct and associated methods. +#[derive(Default)] +pub struct TrayIconBuilder { + id: TrayIconId, + attrs: TrayIconAttributes, +} + +impl TrayIconBuilder { + /// Creates a new [`TrayIconBuilder`] with default [`TrayIconAttributes`]. + /// + /// See [`TrayIcon::new`] for more info. + pub fn new() -> Self { + Self { + id: TrayIconId::new_unique(), + attrs: TrayIconAttributes::default(), + } + } + + /// Sets the unique id to build the tray icon with. + pub fn with_id>(mut self, id: I) -> Self { + self.id = id.into(); + self + } + + /// Set the a menu for this tray icon. + /// + /// ## Platform-specific: + /// + /// - **Linux**: once a menu is set, it cannot be removed or replaced but you can change its content. + pub fn with_menu(mut self, menu: Box) -> Self { + self.attrs.menu = Some(menu); + self + } + + /// Set an icon for this tray icon. + /// + /// ## Platform-specific: + /// + /// - **Linux:** Sometimes the icon won't be visible unless a menu is set. + /// Setting an empty [`Menu`](crate::menu::Menu) is enough. + pub fn with_icon(mut self, icon: Icon) -> Self { + self.attrs.icon = Some(icon); + self + } + + /// Set a tooltip for this tray icon. + /// + /// ## Platform-specific: + /// + /// - **Linux:** Unsupported. + pub fn with_tooltip>(mut self, s: S) -> Self { + self.attrs.tooltip = Some(s.as_ref().to_string()); + self + } + + /// Set the tray icon title. + /// + /// ## Platform-specific + /// + /// - **Linux:** The title will not be shown unless there is an icon + /// as well. The title is useful for numerical and other frequently + /// updated information. In general, it shouldn't be shown unless a + /// user requests it as it can take up a significant amount of space + /// on the user's panel. This may not be shown in all visualizations. + /// - **Windows:** Unsupported. + pub fn with_title>(mut self, title: S) -> Self { + self.attrs.title.replace(title.as_ref().to_string()); + self + } + + /// Set tray icon temp dir path. **Linux only**. + /// + /// On Linux, we need to write the icon to the disk and usually it will + /// be `$XDG_RUNTIME_DIR/tray-icon` or `$TEMP/tray-icon`. + pub fn with_temp_dir_path>(mut self, s: P) -> Self { + self.attrs.temp_dir_path = Some(s.as_ref().to_path_buf()); + self + } + + /// Use the icon as a [template](https://developer.apple.com/documentation/appkit/nsimage/1520017-template?language=objc). **macOS only**. + pub fn with_icon_as_template(mut self, is_template: bool) -> Self { + self.attrs.icon_is_template = is_template; + self + } + + /// Whether to show the tray menu on left click or not, default is `true`. + /// + /// ## Platform-specific: + /// + /// - **Linux:** Unsupported. + pub fn with_menu_on_left_click(mut self, enable: bool) -> Self { + self.attrs.menu_on_left_click = enable; + self + } + + /// Access the unique id that will be assigned to the tray icon + /// this builder will create. + pub fn id(&self) -> &TrayIconId { + &self.id + } + + /// Builds and adds a new [`TrayIcon`] to the system tray. + pub fn build(self) -> Result { + TrayIcon::with_id(self.id, self.attrs) + } +} + +/// Tray icon struct and associated methods. +/// +/// This type is reference-counted and the icon is removed when the last instance is dropped. +#[derive(Clone)] +pub struct TrayIcon { + id: TrayIconId, + tray: Rc>, +} + +impl TrayIcon { + /// Builds and adds a new tray icon to the system tray. + /// + /// ## Platform-specific: + /// + /// - **Linux:** Sometimes the icon won't be visible unless a menu is set. + /// Setting an empty [`Menu`](crate::menu::Menu) is enough. + pub fn new(attrs: TrayIconAttributes) -> Result { + let id = TrayIconId::new_unique(); + Ok(Self { + tray: Rc::new(RefCell::new(platform_impl::TrayIcon::new( + id.clone(), + attrs, + )?)), + id, + }) + } + + /// Builds and adds a new tray icon to the system tray with the specified Id. + /// + /// See [`TrayIcon::new`] for more info. + pub fn with_id>(id: I, attrs: TrayIconAttributes) -> Result { + let id = id.into(); + Ok(Self { + tray: Rc::new(RefCell::new(platform_impl::TrayIcon::new( + id.clone(), + attrs, + )?)), + id, + }) + } + + /// Returns the id associated with this tray icon. + pub fn id(&self) -> &TrayIconId { + &self.id + } + + /// Set new tray icon. If `None` is provided, it will remove the icon. + pub fn set_icon(&self, icon: Option) -> Result<()> { + self.tray.borrow_mut().set_icon(icon) + } + + /// Set new tray menu. + /// + /// ## Platform-specific: + /// + /// - **Linux**: once a menu is set it cannot be removed so `None` has no effect + pub fn set_menu(&self, menu: Option>) { + self.tray.borrow_mut().set_menu(menu) + } + + /// Sets the tooltip for this tray icon. + /// + /// ## Platform-specific: + /// + /// - **Linux:** Unsupported + pub fn set_tooltip>(&self, tooltip: Option) -> Result<()> { + self.tray.borrow_mut().set_tooltip(tooltip) + } + + /// Sets the tooltip for this tray icon. + /// + /// ## Platform-specific: + /// + /// - **Linux:** The title will not be shown unless there is an icon + /// as well. The title is useful for numerical and other frequently + /// updated information. In general, it shouldn't be shown unless a + /// user requests it as it can take up a significant amount of space + /// on the user's panel. This may not be shown in all visualizations. + /// - **Windows:** Unsupported + pub fn set_title>(&self, title: Option) { + self.tray.borrow_mut().set_title(title) + } + + /// Show or hide this tray icon + pub fn set_visible(&self, visible: bool) -> Result<()> { + self.tray.borrow_mut().set_visible(visible) + } + + /// Sets the tray icon temp dir path. **Linux only**. + /// + /// On Linux, we need to write the icon to the disk and usually it will + /// be `$XDG_RUNTIME_DIR/tray-icon` or `$TEMP/tray-icon`. + pub fn set_temp_dir_path>(&self, path: Option

) { + #[cfg(target_os = "linux")] + self.tray.borrow_mut().set_temp_dir_path(path); + #[cfg(not(target_os = "linux"))] + let _ = path; + } + + /// Set the current icon as a [template](https://developer.apple.com/documentation/appkit/nsimage/1520017-template?language=objc). **macOS only**. + pub fn set_icon_as_template(&self, is_template: bool) { + #[cfg(target_os = "macos")] + self.tray.borrow_mut().set_icon_as_template(is_template); + #[cfg(not(target_os = "macos"))] + let _ = is_template; + } + + pub fn set_icon_with_as_template(&self, icon: Option, is_template: bool) -> Result<()> { + #[cfg(target_os = "macos")] + return self + .tray + .borrow_mut() + .set_icon_with_as_template(icon, is_template); + #[cfg(not(target_os = "macos"))] + { + let _ = icon; + let _ = is_template; + Ok(()) + } + } + + /// Disable or enable showing the tray menu on left click. + /// + /// ## Platform-specific: + /// + /// - **Linux:** Unsupported. + pub fn set_show_menu_on_left_click(&self, enable: bool) { + #[cfg(any(target_os = "macos", target_os = "windows"))] + self.tray.borrow_mut().set_show_menu_on_left_click(enable); + #[cfg(not(any(target_os = "macos", target_os = "windows")))] + let _ = enable; + } + + /// Get tray icon rect. + /// + /// ## Platform-specific: + /// + /// - **Linux**: Unsupported. + pub fn rect(&self) -> Option { + self.tray.borrow().rect() + } + + /// Get the tray icon's underlying [window handle](windows_sys::Win32::Foundation::HWND) **Windows only**. + /// + /// This window handle is valid as long as the tray icon. + #[cfg(windows)] + pub fn window_handle(&self) -> windows_sys::Win32::Foundation::HWND { + self.tray.borrow().hwnd() + } + + /// Get the tray icon's underlying [NSStatusItem](objc2_app_kit::NSStatusItem) **macOS only**. + /// + /// Returns `None` if the status item is not available. + #[cfg(target_os = "macos")] + pub fn ns_status_item(&self) -> Option> { + self.tray.borrow().ns_status_item().cloned() + } + + /// Get the tray icon's underlying [AppIndicator](libappindicator::AppIndicator) **Linux only**. + /// + /// # Safety + /// + /// The returned pointer is valid as long as the `TrayIcon` is. + #[cfg(all(unix, not(target_os = "macos")))] + pub unsafe fn app_indicator(&self) -> *const libappindicator::AppIndicator { + self.tray.borrow().app_indicator() as *const _ + } +} + +/// Describes a tray icon event. +/// +/// ## Platform-specific: +/// +/// - **Linux**: Unsupported. The event is not emmited even though the icon is shown +/// and will still show a context menu on right click. +#[derive(Debug, Clone)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(tag = "type"))] +#[non_exhaustive] +pub enum TrayIconEvent { + /// A click happened on the tray icon. + #[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] + Click { + /// Id of the tray icon which triggered this event. + id: TrayIconId, + /// Physical Position of this event. + position: dpi::PhysicalPosition, + /// Position and size of the tray icon. + rect: Rect, + /// Mouse button that triggered this event. + button: MouseButton, + /// Mouse button state when this event was triggered. + button_state: MouseButtonState, + }, + /// A double click happened on the tray icon. **Windows Only** + DoubleClick { + /// Id of the tray icon which triggered this event. + id: TrayIconId, + /// Physical Position of this event. + position: dpi::PhysicalPosition, + /// Position and size of the tray icon. + rect: Rect, + /// Mouse button that triggered this event. + button: MouseButton, + }, + /// The mouse entered the tray icon region. + Enter { + /// Id of the tray icon which triggered this event. + id: TrayIconId, + /// Physical Position of this event. + position: dpi::PhysicalPosition, + /// Position and size of the tray icon. + rect: Rect, + }, + /// The mouse moved over the tray icon region. + Move { + /// Id of the tray icon which triggered this event. + id: TrayIconId, + /// Physical Position of this event. + position: dpi::PhysicalPosition, + /// Position and size of the tray icon. + rect: Rect, + }, + /// The mouse left the tray icon region. + Leave { + /// Id of the tray icon which triggered this event. + id: TrayIconId, + /// Physical Position of this event. + position: dpi::PhysicalPosition, + /// Position and size of the tray icon. + rect: Rect, + }, +} + +/// Describes the mouse button state. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Default)] +pub enum MouseButtonState { + #[default] + Up, + Down, +} + +/// Describes which mouse button triggered the event.. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Default)] +pub enum MouseButton { + #[default] + Left, + Right, + Middle, +} + +/// Describes a rectangle including position (x - y axis) and size. +#[derive(Debug, PartialEq, Clone, Copy)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct Rect { + pub size: dpi::PhysicalSize, + pub position: dpi::PhysicalPosition, +} + +impl Default for Rect { + fn default() -> Self { + Self { + size: dpi::PhysicalSize::new(0, 0), + position: dpi::PhysicalPosition::new(0., 0.), + } + } +} + +/// A reciever that could be used to listen to tray events. +pub type TrayIconEventReceiver = Receiver; +type TrayIconEventHandler = Box; + +static TRAY_CHANNEL: Lazy<(Sender, TrayIconEventReceiver)> = Lazy::new(unbounded); +static TRAY_EVENT_HANDLER: OnceCell> = OnceCell::new(); + +impl TrayIconEvent { + /// Returns the id of the tray icon which triggered this event. + pub fn id(&self) -> &TrayIconId { + match self { + TrayIconEvent::Click { id, .. } => id, + TrayIconEvent::DoubleClick { id, .. } => id, + TrayIconEvent::Enter { id, .. } => id, + TrayIconEvent::Move { id, .. } => id, + TrayIconEvent::Leave { id, .. } => id, + } + } + + /// Gets a reference to the event channel's [`TrayIconEventReceiver`] + /// which can be used to listen for tray events. + /// + /// ## Note + /// + /// This will not receive any events if [`TrayIconEvent::set_event_handler`] has been called with a `Some` value. + pub fn receiver<'a>() -> &'a TrayIconEventReceiver { + &TRAY_CHANNEL.1 + } + + /// Set a handler to be called for new events. Useful for implementing custom event sender. + /// + /// ## Note + /// + /// Calling this function with a `Some` value, + /// will not send new events to the channel associated with [`TrayIconEvent::receiver`] + pub fn set_event_handler(f: Option) { + if let Some(f) = f { + let _ = TRAY_EVENT_HANDLER.set(Some(Box::new(f))); + } else { + let _ = TRAY_EVENT_HANDLER.set(None); + } + } + + #[allow(unused)] + pub(crate) fn send(event: TrayIconEvent) { + if let Some(handler) = TRAY_EVENT_HANDLER.get_or_init(|| None) { + handler(event); + } else { + let _ = TRAY_CHANNEL.0.send(event); + } + } +} + +#[cfg(test)] +mod tests { + + #[cfg(feature = "serde")] + #[test] + fn it_serializes() { + use super::*; + let event = TrayIconEvent::Click { + button: MouseButton::Left, + button_state: MouseButtonState::Down, + id: TrayIconId::new("id"), + position: dpi::PhysicalPosition::default(), + rect: Rect::default(), + }; + + let value = serde_json::to_value(&event).unwrap(); + assert_eq!( + value, + serde_json::json!({ + "type": "Click", + "button": "Left", + "buttonState": "Down", + "id": "id", + "position": { + "x": 0.0, + "y": 0.0, + }, + "rect": { + "size": { + "width": 0, + "height": 0, + }, + "position": { + "x": 0.0, + "y": 0.0, + }, + } + }) + ) + } +} diff --git a/src-tauri/vendor/tray-icon/src/platform_impl/gtk/icon.rs b/src-tauri/vendor/tray-icon/src/platform_impl/gtk/icon.rs new file mode 100644 index 0000000000..9a9c6dcdef --- /dev/null +++ b/src-tauri/vendor/tray-icon/src/platform_impl/gtk/icon.rs @@ -0,0 +1,38 @@ +// Copyright 2022-2022 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::{fs::File, io::BufWriter, path::Path}; + +use crate::icon::BadIcon; + +#[derive(Debug, Clone)] +pub struct PlatformIcon { + rgba: Vec, + width: i32, + height: i32, +} + +impl PlatformIcon { + pub fn from_rgba(rgba: Vec, width: u32, height: u32) -> Result { + Ok(Self { + rgba, + width: width as i32, + height: height as i32, + }) + } + + pub fn write_to_png(&self, path: impl AsRef) -> crate::Result<()> { + let png = File::create(path)?; + let w = &mut BufWriter::new(png); + + let mut encoder = png::Encoder::new(w, self.width as _, self.height as _); + encoder.set_color(png::ColorType::Rgba); + encoder.set_depth(png::BitDepth::Eight); + + let mut writer = encoder.write_header()?; + writer.write_image_data(&self.rgba)?; + + Ok(()) + } +} diff --git a/src-tauri/vendor/tray-icon/src/platform_impl/gtk/mod.rs b/src-tauri/vendor/tray-icon/src/platform_impl/gtk/mod.rs new file mode 100644 index 0000000000..9455443014 --- /dev/null +++ b/src-tauri/vendor/tray-icon/src/platform_impl/gtk/mod.rs @@ -0,0 +1,159 @@ +// Copyright 2022-2022 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +mod icon; +use std::path::{Path, PathBuf}; + +use crate::icon::Icon; +pub(crate) use icon::PlatformIcon; + +use crate::{TrayIconAttributes, TrayIconId}; +use libappindicator::{AppIndicator, AppIndicatorStatus}; + +pub struct TrayIcon { + id: TrayIconId, + indicator: AppIndicator, + temp_dir_path: Option, + path: PathBuf, + counter: u32, + menu: Option>, +} + +impl TrayIcon { + pub fn new(id: TrayIconId, attrs: TrayIconAttributes) -> crate::Result { + let mut indicator = AppIndicator::new(&format!("tray-icon tray app {}", id.as_ref()), ""); + indicator.set_status(AppIndicatorStatus::Active); + + let (parent_path, icon_path) = temp_icon_path(attrs.temp_dir_path.as_ref(), &id, 0)?; + + if let Some(icon) = attrs.icon { + icon.inner.write_to_png(&icon_path)?; + } + + indicator.set_icon_theme_path(&parent_path.to_string_lossy()); + indicator.set_icon_full(&icon_path.to_string_lossy(), "icon"); + + if let Some(menu) = &attrs.menu { + indicator.set_menu(&mut menu.gtk_context_menu()); + } + + if let Some(title) = attrs.title { + indicator.set_label(title.as_str(), ""); + } + + Ok(Self { + id, + indicator, + path: icon_path, + temp_dir_path: attrs.temp_dir_path, + counter: 0, + menu: attrs.menu, + }) + } + pub fn set_icon(&mut self, icon: Option) -> crate::Result<()> { + let _ = std::fs::remove_file(&self.path); + + self.counter += 1; + + let (parent_path, icon_path) = + temp_icon_path(self.temp_dir_path.as_ref(), &self.id, self.counter)?; + + if let Some(icon) = icon { + icon.inner.write_to_png(&icon_path)?; + } + + self.indicator + .set_icon_theme_path(&parent_path.to_string_lossy()); + self.indicator + .set_icon_full(&icon_path.to_string_lossy(), "tray icon"); + self.path = icon_path; + + Ok(()) + } + + pub fn set_menu(&mut self, menu: Option>) { + if let Some(menu) = &menu { + self.indicator.set_menu(&mut menu.gtk_context_menu()); + } + self.menu = menu; + } + + pub fn set_tooltip>(&mut self, _tooltip: Option) -> crate::Result<()> { + Ok(()) + } + + pub fn set_title>(&mut self, title: Option) { + self.indicator + .set_label(title.as_ref().map(|t| t.as_ref()).unwrap_or(""), ""); + } + + pub fn set_visible(&mut self, visible: bool) -> crate::Result<()> { + if visible { + self.indicator.set_status(AppIndicatorStatus::Active); + } else { + self.indicator.set_status(AppIndicatorStatus::Passive); + } + + Ok(()) + } + + pub fn set_temp_dir_path>(&mut self, path: Option

) { + self.temp_dir_path = path.map(|p| p.as_ref().to_path_buf()); + } + + pub fn rect(&self) -> Option { + None + } + + pub fn app_indicator(&self) -> &AppIndicator { + &self.indicator + } +} + +impl Drop for TrayIcon { + fn drop(&mut self) { + self.indicator.set_status(AppIndicatorStatus::Passive); + let _ = std::fs::remove_file(&self.path); + } +} + +/// Generates an icon path in one of the following dirs: +/// 1. If `temp_icon_dir` is `Some` use that. +/// 2. `$XDG_RUNTIME_DIR/tray-icon` +/// 3. `/tmp/tray-icon` +fn temp_icon_path( + temp_icon_dir: Option<&PathBuf>, + id: &TrayIconId, + counter: u32, +) -> std::io::Result<(PathBuf, PathBuf)> { + let parent_path = match temp_icon_dir.as_ref() { + Some(path) => path.to_path_buf(), + None => dirs::runtime_dir() + .unwrap_or_else(std::env::temp_dir) + .join("tray-icon"), + }; + + std::fs::create_dir_all(&parent_path)?; + let icon_path = parent_path.join(format!("tray-icon-{}-{}.png", id.as_ref(), counter)); + Ok((parent_path, icon_path)) +} + +#[test] +fn temp_icon_path_preference_order() { + let runtime_dir = option_env!("XDG_RUNTIME_DIR"); + let override_dir = PathBuf::from("/tmp/tao-tests"); + + let (dir1, _file1) = temp_icon_path(Some(&override_dir), &"00".into(), 00).unwrap(); + let (dir2, _file1) = temp_icon_path(None, &"00".into(), 00).unwrap(); + std::env::remove_var("XDG_RUNTIME_DIR"); + let (dir3, _file2) = temp_icon_path(None, &"00".into(), 00).unwrap(); + + assert_eq!(dir1, override_dir); + if let Some(runtime_dir) = runtime_dir { + std::env::set_var("XDG_RUNTIME_DIR", runtime_dir); + assert_eq!(dir2, PathBuf::from(format!("{}/tray-icon", runtime_dir))); + } + + assert_eq!(dir3, PathBuf::from("/tmp/tray-icon")); +} diff --git a/src-tauri/vendor/tray-icon/src/platform_impl/macos/icon.rs b/src-tauri/vendor/tray-icon/src/platform_impl/macos/icon.rs new file mode 100644 index 0000000000..a639379955 --- /dev/null +++ b/src-tauri/vendor/tray-icon/src/platform_impl/macos/icon.rs @@ -0,0 +1,35 @@ +// Copyright 2022-2022 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use crate::icon::{BadIcon, RgbaIcon}; +use std::io::Cursor; + +#[derive(Debug, Clone)] +pub struct PlatformIcon(RgbaIcon); + +impl PlatformIcon { + pub fn from_rgba(rgba: Vec, width: u32, height: u32) -> Result { + Ok(PlatformIcon(RgbaIcon::from_rgba(rgba, width, height)?)) + } + + pub fn get_size(&self) -> (u32, u32) { + (self.0.width, self.0.height) + } + + pub fn to_png(&self) -> crate::Result> { + let mut png = Vec::new(); + + { + let mut encoder = + png::Encoder::new(Cursor::new(&mut png), self.0.width as _, self.0.height as _); + encoder.set_color(png::ColorType::Rgba); + encoder.set_depth(png::BitDepth::Eight); + + let mut writer = encoder.write_header()?; + writer.write_image_data(&self.0.rgba)?; + } + + Ok(png) + } +} diff --git a/src-tauri/vendor/tray-icon/src/platform_impl/macos/mod.rs b/src-tauri/vendor/tray-icon/src/platform_impl/macos/mod.rs new file mode 100644 index 0000000000..383cd0109c --- /dev/null +++ b/src-tauri/vendor/tray-icon/src/platform_impl/macos/mod.rs @@ -0,0 +1,605 @@ +// Copyright 2022-2022 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +mod icon; +use std::cell::{Cell, RefCell}; + +use objc2::rc::Retained; +use objc2::{define_class, msg_send, AllocAnyThread, DeclaredClass, Message}; +use objc2_app_kit::{ + NSCellImagePosition, NSEvent, NSImage, NSMenu, NSStatusBar, NSStatusItem, NSTrackingArea, + NSTrackingAreaOptions, NSVariableStatusItemLength, NSView, NSWindow, +}; +use objc2_core_foundation::{CGPoint, CGRect, CGSize}; +use objc2_core_graphics::{CGDisplayPixelsHigh, CGMainDisplayID}; +use objc2_foundation::{MainThreadMarker, NSData, NSSize, NSString}; + +pub(crate) use self::icon::PlatformIcon; +use crate::Error; +use crate::{ + icon::Icon, menu, MouseButton, MouseButtonState, Rect, TrayIconAttributes, TrayIconEvent, + TrayIconId, +}; + +pub struct TrayIcon { + ns_status_item: Option>, + tray_target: Option>, + id: TrayIconId, + attrs: TrayIconAttributes, + mtm: MainThreadMarker, +} + +impl TrayIcon { + pub fn new(id: TrayIconId, attrs: TrayIconAttributes) -> crate::Result { + let mtm = MainThreadMarker::new().ok_or(Error::NotMainThread)?; + let (ns_status_item, tray_target) = Self::create(&id, &attrs, mtm)?; + + let tray_icon = Self { + ns_status_item: Some(ns_status_item), + tray_target: Some(tray_target), + id, + attrs, + mtm, + }; + + Ok(tray_icon) + } + + fn create( + id: &TrayIconId, + attrs: &TrayIconAttributes, + mtm: MainThreadMarker, + ) -> crate::Result<(Retained, Retained)> { + let ns_status_item = unsafe { + NSStatusBar::systemStatusBar().statusItemWithLength(NSVariableStatusItemLength) + }; + + set_icon_for_ns_status_item_button( + &ns_status_item, + attrs.icon.clone(), + attrs.icon_is_template, + mtm, + )?; + + if let Some(menu) = &attrs.menu { + unsafe { + ns_status_item.setMenu((menu.ns_menu() as *const NSMenu).as_ref()); + } + } + + Self::set_tooltip_inner(&ns_status_item, attrs.tooltip.clone(), mtm)?; + Self::set_title_inner(&ns_status_item, attrs.title.clone(), mtm); + + let tray_target = unsafe { + let button = ns_status_item.button(mtm).unwrap(); + + let frame = button.frame(); + + let target = mtm.alloc().set_ivars(TrayTargetIvars { + id: NSString::from_str(&id.0), + menu: RefCell::new( + attrs + .menu + .as_deref() + .and_then(|menu| Retained::retain(menu.ns_menu().cast::())), + ), + status_item: ns_status_item.retain(), + menu_on_left_click: Cell::new(attrs.menu_on_left_click), + }); + let tray_target: Retained = msg_send![super(target), initWithFrame: frame]; + tray_target.setWantsLayer(true); + + button.addSubview(&tray_target); + + tray_target + }; + + Ok((ns_status_item, tray_target)) + } + + fn remove(&mut self) { + if let (Some(ns_status_item), Some(tray_target)) = (&self.ns_status_item, &self.tray_target) + { + unsafe { + NSStatusBar::systemStatusBar().removeStatusItem(ns_status_item); + tray_target.removeFromSuperview(); + } + } + + self.ns_status_item = None; + self.tray_target = None; + } + + pub fn set_icon(&mut self, icon: Option) -> crate::Result<()> { + if let (Some(ns_status_item), Some(tray_target)) = (&self.ns_status_item, &self.tray_target) + { + set_icon_for_ns_status_item_button(ns_status_item, icon.clone(), false, self.mtm)?; + tray_target.update_dimensions(); + } + self.attrs.icon = icon; + Ok(()) + } + + pub fn set_menu(&mut self, menu: Option>) { + if let (Some(ns_status_item), Some(tray_target)) = (&self.ns_status_item, &self.tray_target) + { + unsafe { + let menu = menu + .as_ref() + .and_then(|m| m.ns_menu().cast::().as_ref()) + .map(|menu| menu.retain()); + ns_status_item.setMenu(menu.as_deref()); + if let Some(menu) = &menu { + let () = msg_send![menu, setDelegate: &**ns_status_item]; + } + + *tray_target.ivars().menu.borrow_mut() = menu; + } + } + self.attrs.menu = menu; + } + + pub fn set_tooltip>(&mut self, tooltip: Option) -> crate::Result<()> { + let tooltip = tooltip.map(|s| s.as_ref().to_string()); + if let (Some(ns_status_item), Some(tray_target)) = (&self.ns_status_item, &self.tray_target) + { + Self::set_tooltip_inner(ns_status_item, tooltip.clone(), self.mtm)?; + tray_target.update_dimensions(); + } + self.attrs.tooltip = tooltip; + Ok(()) + } + + fn set_tooltip_inner>( + ns_status_item: &NSStatusItem, + tooltip: Option, + mtm: MainThreadMarker, + ) -> crate::Result<()> { + unsafe { + let tooltip = tooltip.map(|tooltip| NSString::from_str(tooltip.as_ref())); + if let Some(button) = ns_status_item.button(mtm) { + button.setToolTip(tooltip.as_deref()); + } + } + Ok(()) + } + + pub fn set_title>(&mut self, title: Option) { + let title = title.map(|s| s.as_ref().to_string()); + if let (Some(ns_status_item), Some(tray_target)) = (&self.ns_status_item, &self.tray_target) + { + Self::set_title_inner(ns_status_item, title.clone(), self.mtm); + tray_target.update_dimensions(); + } + self.attrs.title = title; + } + + fn set_title_inner>( + ns_status_item: &NSStatusItem, + title: Option, + mtm: MainThreadMarker, + ) { + if let Some(title) = title { + unsafe { + if let Some(button) = ns_status_item.button(mtm) { + button.setTitle(&NSString::from_str(title.as_ref())); + } + } + } + } + + pub fn set_visible(&mut self, visible: bool) -> crate::Result<()> { + if visible { + if self.ns_status_item.is_none() { + let (ns_status_item, tray_target) = Self::create(&self.id, &self.attrs, self.mtm)?; + self.ns_status_item = Some(ns_status_item); + self.tray_target = Some(tray_target); + } + } else { + self.remove(); + } + + Ok(()) + } + + pub fn set_icon_as_template(&mut self, is_template: bool) { + if let Some(ns_status_item) = &self.ns_status_item { + unsafe { + let button = ns_status_item.button(self.mtm).unwrap(); + if let Some(nsimage) = button.image() { + nsimage.setTemplate(is_template); + button.setImage(Some(&nsimage)); + } + } + } + self.attrs.icon_is_template = is_template; + } + + pub fn set_icon_with_as_template( + &mut self, + icon: Option, + is_template: bool, + ) -> crate::Result<()> { + if let (Some(ns_status_item), Some(tray_target)) = (&self.ns_status_item, &self.tray_target) + { + set_icon_for_ns_status_item_button( + ns_status_item, + icon.clone(), + is_template, + self.mtm, + )?; + tray_target.update_dimensions(); + } + self.attrs.icon = icon; + self.attrs.icon_is_template = is_template; + Ok(()) + } + + pub fn set_show_menu_on_left_click(&mut self, enable: bool) { + if let Some(tray_target) = &self.tray_target { + tray_target.ivars().menu_on_left_click.set(enable); + } + self.attrs.menu_on_left_click = enable; + } + + pub fn rect(&self) -> Option { + let ns_status_item = self.ns_status_item.as_deref()?; + unsafe { + let button = ns_status_item.button(self.mtm).unwrap(); + let window = button.window(); + window.map(|window| get_tray_rect(&window)) + } + } + + pub fn ns_status_item(&self) -> Option<&Retained> { + self.ns_status_item.as_ref() + } +} + +impl Drop for TrayIcon { + fn drop(&mut self) { + self.remove() + } +} + +fn set_icon_for_ns_status_item_button( + ns_status_item: &NSStatusItem, + icon: Option, + icon_is_template: bool, + mtm: MainThreadMarker, +) -> crate::Result<()> { + let button = unsafe { ns_status_item.button(mtm).unwrap() }; + + if let Some(icon) = icon { + let png_icon = icon.inner.to_png()?; + + let (width, height) = icon.inner.get_size(); + + let icon_height: f64 = 18.0; + let icon_width: f64 = (width as f64) / (height as f64 / icon_height); + + unsafe { + // build our icon + let nsdata = NSData::from_vec(png_icon); + + let nsimage = NSImage::initWithData(NSImage::alloc(), &nsdata).unwrap(); + let new_size = NSSize::new(icon_width, icon_height); + + button.setImage(Some(&nsimage)); + nsimage.setSize(new_size); + // The image is to the right of the title + button.setImagePosition(NSCellImagePosition::ImageLeft); + nsimage.setTemplate(icon_is_template); + } + } else { + unsafe { button.setImage(None) }; + } + + Ok(()) +} + +#[derive(Debug)] +struct TrayTargetIvars { + id: Retained, + menu: RefCell>>, + status_item: Retained, + menu_on_left_click: Cell, +} + +define_class!( + #[unsafe(super(NSView))] + #[name = "TaoTrayTarget"] + #[ivars = TrayTargetIvars] + struct TrayTarget; + + /// Mouse events on NSResponder + impl TrayTarget { + #[unsafe(method(mouseDown:))] + fn on_mouse_down(&self, event: &NSEvent) { + send_mouse_event( + self, + event, + MouseEventType::Click, + Some(MouseClickEvent { + button: MouseButton::Left, + state: MouseButtonState::Down, + }), + ); + on_tray_click(self, MouseButton::Left); + } + + #[unsafe(method(mouseUp:))] + fn on_mouse_up(&self, event: &NSEvent) { + let mtm = MainThreadMarker::from(self); + unsafe { + let button = self.ivars().status_item.button(mtm).unwrap(); + button.highlight(false); + } + send_mouse_event( + self, + event, + MouseEventType::Click, + Some(MouseClickEvent { + button: MouseButton::Left, + state: MouseButtonState::Up, + }), + ); + } + + #[unsafe(method(rightMouseDown:))] + fn on_right_mouse_down(&self, event: &NSEvent) { + send_mouse_event( + self, + event, + MouseEventType::Click, + Some(MouseClickEvent { + button: MouseButton::Right, + state: MouseButtonState::Down, + }), + ); + on_tray_click(self, MouseButton::Right); + } + + #[unsafe(method(rightMouseUp:))] + fn on_right_mouse_up(&self, event: &NSEvent) { + send_mouse_event( + self, + event, + MouseEventType::Click, + Some(MouseClickEvent { + button: MouseButton::Right, + state: MouseButtonState::Up, + }), + ); + } + + #[unsafe(method(otherMouseDown:))] + fn on_other_mouse_down(&self, event: &NSEvent) { + let button_number = unsafe { event.buttonNumber() }; + if button_number == 2 { + send_mouse_event( + self, + event, + MouseEventType::Click, + Some(MouseClickEvent { + button: MouseButton::Middle, + state: MouseButtonState::Down, + }), + ); + } + } + + #[unsafe(method(otherMouseUp:))] + fn on_other_mouse_up(&self, event: &NSEvent) { + let button_number = unsafe { event.buttonNumber() }; + if button_number == 2 { + send_mouse_event( + self, + event, + MouseEventType::Click, + Some(MouseClickEvent { + button: MouseButton::Middle, + state: MouseButtonState::Up, + }), + ); + } + } + + #[unsafe(method(mouseEntered:))] + fn on_mouse_entered(&self, event: &NSEvent) { + send_mouse_event(self, event, MouseEventType::Enter, None); + } + + #[unsafe(method(mouseExited:))] + fn on_mouse_exited(&self, event: &NSEvent) { + send_mouse_event(self, event, MouseEventType::Leave, None); + } + + #[unsafe(method(mouseMoved:))] + fn on_mouse_moved(&self, event: &NSEvent) { + send_mouse_event(self, event, MouseEventType::Move, None); + } + } + + /// Tracking mouse enter/exit/move events + impl TrayTarget { + #[unsafe(method(updateTrackingAreas))] + fn update_tracking_areas(&self) { + unsafe { + let areas = self.trackingAreas(); + for area in areas { + self.removeTrackingArea(&area); + } + + let _: () = msg_send![super(self), updateTrackingAreas]; + + let options = NSTrackingAreaOptions::MouseEnteredAndExited + | NSTrackingAreaOptions::MouseMoved + | NSTrackingAreaOptions::ActiveAlways + | NSTrackingAreaOptions::InVisibleRect; + let rect = CGRect { + origin: CGPoint { x: 0.0, y: 0.0 }, + size: CGSize { + width: 0.0, + height: 0.0, + }, + }; + let area = NSTrackingArea::initWithRect_options_owner_userInfo( + NSTrackingArea::alloc(), + rect, + options, + Some(self), + None, + ); + self.addTrackingArea(&area); + } + } + } +); + +impl TrayTarget { + fn update_dimensions(&self) { + let mtm = MainThreadMarker::from(self); + unsafe { + let button = self.ivars().status_item.button(mtm).unwrap(); + self.setFrame(button.frame()); + } + } +} + +// Local patch for tauri-apps/tray-icon#251: popping the menu via +// `performClick(None)` synthesizes a click without an NSEvent context, so +// AppKit's status-item popup logic loses screen/Space information and silently +// no-ops on a secondary display while a full-screen Space is active there. +// Drive the popup ourselves through NSStatusItem.popUpStatusItemMenu, which is +// deprecated but functionally intact across every supported macOS version and +// works in every monitor/Space combination. +// +// `popUpStatusItemMenu` is modal — it does not return until the menu is +// dismissed. A menu item action selected inside that modal can re-enter +// `TrayIcon::set_menu`, which calls `menu.borrow_mut()` on the same RefCell, +// so we must not hold a `RefCell::borrow()` across the popup. Clone the +// `Retained` (an ObjC retain) and drop the borrow before popping up. +#[allow(deprecated)] +fn on_tray_click(this: &TrayTarget, button: MouseButton) { + let mtm = MainThreadMarker::from(this); + unsafe { + let ns_button = this.ivars().status_item.button(mtm).unwrap(); + + let menu_on_left_click = this.ivars().menu_on_left_click.get(); + if button == MouseButton::Right || (menu_on_left_click && button == MouseButton::Left) { + let menu = this.ivars().menu.borrow().as_ref().map(Retained::clone); + if let Some(menu) = menu { + if menu.numberOfItems() > 0 { + ns_button.highlight(true); + this.ivars().status_item.popUpStatusItemMenu(&menu); + ns_button.highlight(false); + return; + } + } + ns_button.highlight(true); + } else { + ns_button.highlight(true); + } + } +} + +fn get_tray_rect(window: &NSWindow) -> Rect { + let frame = window.frame(); + let scale_factor = window.backingScaleFactor(); + + Rect { + size: crate::dpi::LogicalSize::new(frame.size.width, frame.size.height) + .to_physical(scale_factor), + position: crate::dpi::LogicalPosition::new( + frame.origin.x, + flip_window_screen_coordinates(frame.origin.y) - frame.size.height, + ) + .to_physical(scale_factor), + } +} + +fn send_mouse_event( + this: &TrayTarget, + event: &NSEvent, + mouse_event_type: MouseEventType, + click_event: Option, +) { + let mtm = MainThreadMarker::from(this); + unsafe { + let tray_id = TrayIconId(this.ivars().id.to_string()); + + // icon position & size + let window = event.window(mtm).unwrap(); + let icon_rect = get_tray_rect(&window); + + // cursor position + let mouse_location = NSEvent::mouseLocation(); + let scale_factor = window.backingScaleFactor(); + let cursor_position = crate::dpi::LogicalPosition::new( + mouse_location.x, + flip_window_screen_coordinates(mouse_location.y), + ) + .to_physical(scale_factor); + + let event = match mouse_event_type { + MouseEventType::Click => { + let click_event = click_event.unwrap(); + TrayIconEvent::Click { + id: tray_id, + position: cursor_position, + rect: icon_rect, + button: click_event.button, + button_state: click_event.state, + } + } + MouseEventType::Enter => TrayIconEvent::Enter { + id: tray_id, + position: cursor_position, + rect: icon_rect, + }, + MouseEventType::Leave => TrayIconEvent::Leave { + id: tray_id, + position: cursor_position, + rect: icon_rect, + }, + MouseEventType::Move => TrayIconEvent::Move { + id: tray_id, + position: cursor_position, + rect: icon_rect, + }, + }; + + TrayIconEvent::send(event); + } +} + +#[derive(Debug)] +enum MouseEventType { + Click, + Enter, + Leave, + Move, +} + +#[derive(Debug)] +struct MouseClickEvent { + button: MouseButton, + state: MouseButtonState, +} + +/// Core graphics screen coordinates are relative to the top-left corner of +/// the so-called "main" display, with y increasing downwards - which is +/// exactly what we want in Winit. +/// +/// However, `NSWindow` and `NSScreen` changes these coordinates to: +/// 1. Be relative to the bottom-left corner of the "main" screen. +/// 2. Be relative to the bottom-left corner of the window/screen itself. +/// 3. Have y increasing upwards. +/// +/// This conversion happens to be symmetric, so we only need this one function +/// to convert between the two coordinate systems. +fn flip_window_screen_coordinates(y: f64) -> f64 { + unsafe { CGDisplayPixelsHigh(CGMainDisplayID()) as f64 - y } +} diff --git a/src-tauri/vendor/tray-icon/src/platform_impl/mod.rs b/src-tauri/vendor/tray-icon/src/platform_impl/mod.rs new file mode 100644 index 0000000000..3dabaee870 --- /dev/null +++ b/src-tauri/vendor/tray-icon/src/platform_impl/mod.rs @@ -0,0 +1,15 @@ +// Copyright 2022-2022 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +#[cfg(target_os = "windows")] +#[path = "windows/mod.rs"] +mod platform; +#[cfg(target_os = "linux")] +#[path = "gtk/mod.rs"] +mod platform; +#[cfg(target_os = "macos")] +#[path = "macos/mod.rs"] +mod platform; + +pub(crate) use self::platform::*; diff --git a/src-tauri/vendor/tray-icon/src/platform_impl/windows/icon.rs b/src-tauri/vendor/tray-icon/src/platform_impl/windows/icon.rs new file mode 100644 index 0000000000..deaa50ccb3 --- /dev/null +++ b/src-tauri/vendor/tray-icon/src/platform_impl/windows/icon.rs @@ -0,0 +1,158 @@ +// Copyright 2022-2022 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +// taken from https://github.com/rust-windowing/winit/blob/92fdf5ba85f920262a61cee4590f4a11ad5738d1/src/platform_impl/windows/icon.rs + +use std::{fmt, io, mem, path::Path, sync::Arc}; + +use windows_sys::{ + core::PCWSTR, + Win32::UI::WindowsAndMessaging::{ + CreateIcon, DestroyIcon, LoadImageW, HICON, IMAGE_ICON, LR_DEFAULTSIZE, LR_LOADFROMFILE, + }, +}; + +use crate::icon::*; + +use super::util; + +impl Pixel { + fn convert_to_bgra(&mut self) { + mem::swap(&mut self.r, &mut self.b); + } +} + +impl RgbaIcon { + fn into_windows_icon(self) -> Result { + let rgba = self.rgba; + let pixel_count = rgba.len() / PIXEL_SIZE; + let mut and_mask = Vec::with_capacity(pixel_count); + let pixels = + unsafe { std::slice::from_raw_parts_mut(rgba.as_ptr() as *mut Pixel, pixel_count) }; + for pixel in pixels { + and_mask.push(pixel.a.wrapping_sub(u8::MAX)); // invert alpha channel + pixel.convert_to_bgra(); + } + assert_eq!(and_mask.len(), pixel_count); + let handle = unsafe { + CreateIcon( + std::ptr::null_mut(), + self.width as i32, + self.height as i32, + 1, + (PIXEL_SIZE * 8) as u8, + and_mask.as_ptr(), + rgba.as_ptr(), + ) + }; + if !handle.is_null() { + Ok(WinIcon::from_handle(handle)) + } else { + Err(BadIcon::OsError(io::Error::last_os_error())) + } + } +} + +#[derive(Debug)] +struct RaiiIcon { + handle: HICON, +} + +#[derive(Clone)] +pub(crate) struct WinIcon { + inner: Arc, +} + +unsafe impl Send for WinIcon {} + +impl WinIcon { + pub fn as_raw_handle(&self) -> HICON { + self.inner.handle + } + + pub fn from_rgba(rgba: Vec, width: u32, height: u32) -> Result { + let rgba_icon = RgbaIcon::from_rgba(rgba, width, height)?; + rgba_icon.into_windows_icon() + } + + pub(crate) fn from_handle(handle: HICON) -> Self { + Self { + #[allow(clippy::arc_with_non_send_sync)] + inner: Arc::new(RaiiIcon { handle }), + } + } + + pub(crate) fn from_path>( + path: P, + size: Option<(u32, u32)>, + ) -> Result { + // width / height of 0 along with LR_DEFAULTSIZE tells windows to load the default icon size + let (width, height) = size.unwrap_or((0, 0)); + + let wide_path = util::encode_wide(path.as_ref()); + + let handle = unsafe { + LoadImageW( + std::ptr::null_mut(), + wide_path.as_ptr(), + IMAGE_ICON, + width as i32, + height as i32, + LR_DEFAULTSIZE | LR_LOADFROMFILE, + ) + }; + if !handle.is_null() { + Ok(WinIcon::from_handle(handle as HICON)) + } else { + Err(BadIcon::OsError(io::Error::last_os_error())) + } + } + + fn from_resource_inner_name(name: PCWSTR, size: Option<(u32, u32)>) -> Result { + // width / height of 0 along with LR_DEFAULTSIZE tells windows to load the default icon size + let (width, height) = size.unwrap_or((0, 0)); + let handle = unsafe { + LoadImageW( + util::get_instance_handle(), + name, + IMAGE_ICON, + width as i32, + height as i32, + LR_DEFAULTSIZE, + ) + }; + if !handle.is_null() { + Ok(WinIcon::from_handle(handle as HICON)) + } else { + Err(BadIcon::OsError(io::Error::last_os_error())) + } + } + + pub(crate) fn from_resource( + resource_id: u16, + size: Option<(u32, u32)>, + ) -> Result { + Self::from_resource_inner_name(resource_id as PCWSTR, size) + } + + pub(crate) fn from_resource_name( + resource_name: &str, + size: Option<(u32, u32)>, + ) -> Result { + let wide_name = util::encode_wide(resource_name); + Self::from_resource_inner_name(wide_name.as_ptr(), size) + } +} + +impl Drop for RaiiIcon { + fn drop(&mut self) { + unsafe { DestroyIcon(self.handle) }; + } +} + +impl fmt::Debug for WinIcon { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + (*self.inner).fmt(formatter) + } +} diff --git a/src-tauri/vendor/tray-icon/src/platform_impl/windows/mod.rs b/src-tauri/vendor/tray-icon/src/platform_impl/windows/mod.rs new file mode 100644 index 0000000000..9b6a4cec40 --- /dev/null +++ b/src-tauri/vendor/tray-icon/src/platform_impl/windows/mod.rs @@ -0,0 +1,612 @@ +// Copyright 2022-2022 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +mod icon; +mod util; +use std::ptr; + +use once_cell::sync::Lazy; +use windows_sys::{ + s, + Win32::{ + Foundation::{FALSE, HWND, LPARAM, LRESULT, POINT, RECT, S_OK, TRUE, WPARAM}, + UI::{ + Shell::{ + Shell_NotifyIconGetRect, Shell_NotifyIconW, NIF_ICON, NIF_MESSAGE, NIF_TIP, + NIM_ADD, NIM_DELETE, NIM_MODIFY, NOTIFYICONDATAW, NOTIFYICONIDENTIFIER, + }, + WindowsAndMessaging::{ + ChangeWindowMessageFilterEx, CreateWindowExW, DefWindowProcW, DestroyWindow, + GetCursorPos, KillTimer, RegisterClassW, RegisterWindowMessageA, SendMessageW, + SetForegroundWindow, SetTimer, TrackPopupMenu, CREATESTRUCTW, CW_USEDEFAULT, + GWL_USERDATA, HICON, HMENU, MSGFLT_ALLOW, TPM_BOTTOMALIGN, TPM_LEFTALIGN, + WM_CREATE, WM_DESTROY, WM_LBUTTONDBLCLK, WM_LBUTTONDOWN, WM_LBUTTONUP, + WM_MBUTTONDBLCLK, WM_MBUTTONDOWN, WM_MBUTTONUP, WM_MOUSEMOVE, WM_NCCREATE, + WM_RBUTTONDBLCLK, WM_RBUTTONDOWN, WM_RBUTTONUP, WM_TIMER, WNDCLASSW, WS_EX_LAYERED, + WS_EX_NOACTIVATE, WS_EX_TOOLWINDOW, WS_EX_TRANSPARENT, WS_OVERLAPPED, + }, + }, + }, +}; + +use crate::{ + dpi::PhysicalPosition, icon::Icon, menu, MouseButton, MouseButtonState, Rect, + TrayIconAttributes, TrayIconEvent, TrayIconId, COUNTER, +}; + +pub(crate) use self::icon::WinIcon as PlatformIcon; + +const WM_USER_TRAYICON: u32 = 6002; +const WM_USER_UPDATE_TRAYMENU: u32 = 6003; +const WM_USER_UPDATE_TRAYICON: u32 = 6004; +const WM_USER_SHOW_TRAYICON: u32 = 6005; +const WM_USER_HIDE_TRAYICON: u32 = 6006; +const WM_USER_UPDATE_TRAYTOOLTIP: u32 = 6007; +const WM_USER_LEAVE_TIMER_ID: u32 = 6008; +const WM_USER_SHOW_MENU_ON_LEFT_CLICK: u32 = 6009; +/// When the taskbar is created, it registers a message with the "TaskbarCreated" string and then broadcasts this message to all top-level windows +/// When the application receives this message, it should assume that any taskbar icons it added have been removed and add them again. +static S_U_TASKBAR_RESTART: Lazy = + Lazy::new(|| unsafe { RegisterWindowMessageA(s!("TaskbarCreated")) }); + +struct TrayUserData { + internal_id: u32, + id: TrayIconId, + hwnd: HWND, + hpopupmenu: Option, + icon: Option, + tooltip: Option, + entered: bool, + last_position: Option>, + menu_on_left_click: bool, +} + +pub struct TrayIcon { + hwnd: HWND, + menu: Option>, + internal_id: u32, +} + +impl TrayIcon { + pub fn new(id: TrayIconId, attrs: TrayIconAttributes) -> crate::Result { + let internal_id = COUNTER.next(); + + let class_name = util::encode_wide("tray_icon_app"); + unsafe { + let hinstance = util::get_instance_handle(); + + let wnd_class = WNDCLASSW { + lpfnWndProc: Some(tray_proc), + lpszClassName: class_name.as_ptr(), + hInstance: hinstance, + ..std::mem::zeroed() + }; + + RegisterClassW(&wnd_class); + + let traydata = TrayUserData { + id, + internal_id, + hwnd: std::ptr::null_mut(), + hpopupmenu: attrs.menu.as_ref().map(|m| m.hpopupmenu() as _), + icon: attrs.icon.clone(), + tooltip: attrs.tooltip.clone(), + entered: false, + last_position: None, + menu_on_left_click: attrs.menu_on_left_click, + }; + + let hwnd = CreateWindowExW( + WS_EX_NOACTIVATE | WS_EX_TRANSPARENT | WS_EX_LAYERED | + // WS_EX_TOOLWINDOW prevents this window from ever showing up in the taskbar, which + // we want to avoid. If you remove this style, this window won't show up in the + // taskbar *initially*, but it can show up at some later point. This can sometimes + // happen on its own after several hours have passed, although this has proven + // difficult to reproduce. Alternatively, it can be manually triggered by killing + // `explorer.exe` and then starting the process back up. + // It is unclear why the bug is triggered by waiting for several hours. + WS_EX_TOOLWINDOW, + class_name.as_ptr(), + ptr::null(), + WS_OVERLAPPED, + CW_USEDEFAULT, + 0, + CW_USEDEFAULT, + 0, + std::ptr::null_mut(), + std::ptr::null_mut(), + hinstance, + Box::into_raw(Box::new(traydata)) as _, + ); + if hwnd.is_null() { + return Err(crate::Error::OsError(std::io::Error::last_os_error())); + } + + // Allow "TaskbarCreated" through UIPI so elevated apps can re-register on explorer restart. + ChangeWindowMessageFilterEx(hwnd, *S_U_TASKBAR_RESTART, MSGFLT_ALLOW, ptr::null_mut()); + + let hicon = attrs.icon.as_ref().map(|i| i.inner.as_raw_handle()); + + if !register_tray_icon(hwnd, internal_id, &hicon, &attrs.tooltip) { + // Explorer/taskbar may not be ready yet (e.g., app starts before explorer.exe). + // Keep the window alive and wait for TaskbarCreated to re-register. + } + + if let Some(menu) = &attrs.menu { + menu.attach_menu_subclass_for_hwnd(hwnd as _); + } + + Ok(Self { + hwnd, + internal_id, + menu: attrs.menu, + }) + } + } + + pub fn set_icon(&mut self, icon: Option) -> crate::Result<()> { + unsafe { + let mut nid = NOTIFYICONDATAW { + uFlags: NIF_ICON, + hWnd: self.hwnd, + uID: self.internal_id, + ..std::mem::zeroed() + }; + + if let Some(hicon) = icon.as_ref().map(|i| i.inner.as_raw_handle()) { + nid.hIcon = hicon; + } + + if Shell_NotifyIconW(NIM_MODIFY, &mut nid as _) == 0 { + return Err(crate::Error::OsError(std::io::Error::last_os_error())); + } + + // send the new icon to the subclass proc to store it in the tray data + SendMessageW( + self.hwnd, + WM_USER_UPDATE_TRAYICON, + Box::into_raw(Box::new(icon)) as _, + 0, + ); + } + + Ok(()) + } + + pub fn set_menu(&mut self, menu: Option>) { + // Safety: self.hwnd is valid as long as as the TrayIcon is + if let Some(menu) = &self.menu { + unsafe { menu.detach_menu_subclass_from_hwnd(self.hwnd as _) }; + } + if let Some(menu) = &menu { + unsafe { menu.attach_menu_subclass_for_hwnd(self.hwnd as _) }; + } + + unsafe { + // send the new menu to the subclass proc where we will update there + SendMessageW( + self.hwnd, + WM_USER_UPDATE_TRAYMENU, + Box::into_raw(Box::new(menu.as_ref().map(|m| m.hpopupmenu()))) as _, + 0, + ); + } + + self.menu = menu; + } + + pub fn set_tooltip>(&mut self, tooltip: Option) -> crate::Result<()> { + unsafe { + let mut nid = NOTIFYICONDATAW { + uFlags: NIF_TIP, + hWnd: self.hwnd, + uID: self.internal_id, + ..std::mem::zeroed() + }; + if let Some(tooltip) = &tooltip { + let tip = util::encode_wide(tooltip.as_ref()); + #[allow(clippy::manual_memcpy)] + for i in 0..tip.len().min(128) { + nid.szTip[i] = tip[i]; + } + } + + if Shell_NotifyIconW(NIM_MODIFY, &mut nid as _) == 0 { + return Err(crate::Error::OsError(std::io::Error::last_os_error())); + } + + // send the new tooltip to the subclass proc to store it in the tray data + SendMessageW( + self.hwnd, + WM_USER_UPDATE_TRAYTOOLTIP, + Box::into_raw(Box::new(tooltip.map(|t| t.as_ref().to_string()))) as _, + 0, + ); + } + + Ok(()) + } + + pub fn set_show_menu_on_left_click(&mut self, enable: bool) { + unsafe { + SendMessageW( + self.hwnd, + WM_USER_SHOW_MENU_ON_LEFT_CLICK, + enable as usize, + 0, + ); + } + } + + pub fn set_title>(&mut self, _title: Option) {} + + pub fn set_visible(&mut self, visible: bool) -> crate::Result<()> { + unsafe { + SendMessageW( + self.hwnd, + if visible { + WM_USER_SHOW_TRAYICON + } else { + WM_USER_HIDE_TRAYICON + }, + 0, + 0, + ); + } + + Ok(()) + } + + pub fn rect(&self) -> Option { + get_tray_rect(self.internal_id, self.hwnd).map(Into::into) + } + + pub fn hwnd(&self) -> HWND { + self.hwnd + } +} + +impl Drop for TrayIcon { + fn drop(&mut self) { + unsafe { + remove_tray_icon(self.hwnd, self.internal_id); + + if let Some(menu) = &self.menu { + menu.detach_menu_subclass_from_hwnd(self.hwnd as _); + } + + // destroy the hidden window used by the tray + DestroyWindow(self.hwnd); + } + } +} + +unsafe extern "system" fn tray_proc( + hwnd: HWND, + msg: u32, + wparam: WPARAM, + lparam: LPARAM, +) -> LRESULT { + let userdata_ptr = unsafe { util::get_window_long(hwnd, GWL_USERDATA) }; + let userdata_ptr = match (userdata_ptr, msg) { + (0, WM_NCCREATE) => { + let createstruct = unsafe { &mut *(lparam as *mut CREATESTRUCTW) }; + let userdata = unsafe { &mut *(createstruct.lpCreateParams as *mut TrayUserData) }; + userdata.hwnd = hwnd; + util::set_window_long(hwnd, GWL_USERDATA, createstruct.lpCreateParams as _); + return DefWindowProcW(hwnd, msg, wparam, lparam); + } + // Getting here should quite frankly be impossible, + // but we'll make window creation fail here just in case. + (0, WM_CREATE) => return -1, + (_, WM_CREATE) => return DefWindowProcW(hwnd, msg, wparam, lparam), + (0, _) => return DefWindowProcW(hwnd, msg, wparam, lparam), + _ => userdata_ptr as *mut TrayUserData, + }; + + let userdata = &mut *(userdata_ptr); + + match msg { + WM_DESTROY => { + drop(Box::from_raw(userdata_ptr)); + return 0; + } + WM_USER_UPDATE_TRAYMENU => { + let hpopupmenu = Box::from_raw(wparam as *mut Option); + userdata.hpopupmenu = (*hpopupmenu).map(|h| h as *mut _); + } + WM_USER_UPDATE_TRAYICON => { + let icon = Box::from_raw(wparam as *mut Option); + userdata.icon = *icon; + } + WM_USER_SHOW_TRAYICON => { + register_tray_icon( + userdata.hwnd, + userdata.internal_id, + &userdata.icon.as_ref().map(|i| i.inner.as_raw_handle()), + &userdata.tooltip, + ); + } + WM_USER_HIDE_TRAYICON => { + remove_tray_icon(userdata.hwnd, userdata.internal_id); + } + WM_USER_UPDATE_TRAYTOOLTIP => { + let tooltip = Box::from_raw(wparam as *mut Option); + userdata.tooltip = *tooltip; + } + _ if msg == *S_U_TASKBAR_RESTART => { + register_tray_icon( + userdata.hwnd, + userdata.internal_id, + &userdata.icon.as_ref().map(|i| i.inner.as_raw_handle()), + &userdata.tooltip, + ); + } + WM_USER_SHOW_MENU_ON_LEFT_CLICK => { + userdata.menu_on_left_click = wparam != 0; + } + + WM_USER_TRAYICON + if matches!( + lparam as u32, + WM_LBUTTONDOWN + | WM_RBUTTONDOWN + | WM_MBUTTONDOWN + | WM_LBUTTONUP + | WM_RBUTTONUP + | WM_MBUTTONUP + | WM_LBUTTONDBLCLK + | WM_RBUTTONDBLCLK + | WM_MBUTTONDBLCLK + | WM_MOUSEMOVE + ) => + { + let mut cursor = POINT { x: 0, y: 0 }; + if GetCursorPos(&mut cursor as _) == 0 { + return 0; + } + + let id = userdata.id.clone(); + let position = PhysicalPosition::new(cursor.x as f64, cursor.y as f64); + + let rect = match get_tray_rect(userdata.internal_id, hwnd) { + Some(rect) => Rect::from(rect), + None => return 0, + }; + + let event = match lparam as u32 { + WM_LBUTTONDOWN => TrayIconEvent::Click { + id, + rect, + position, + button: MouseButton::Left, + button_state: MouseButtonState::Down, + }, + WM_RBUTTONDOWN => TrayIconEvent::Click { + id, + rect, + position, + button: MouseButton::Right, + button_state: MouseButtonState::Down, + }, + WM_MBUTTONDOWN => TrayIconEvent::Click { + id, + rect, + position, + button: MouseButton::Middle, + button_state: MouseButtonState::Down, + }, + WM_LBUTTONUP => TrayIconEvent::Click { + id, + rect, + position, + button: MouseButton::Left, + button_state: MouseButtonState::Up, + }, + WM_RBUTTONUP => TrayIconEvent::Click { + id, + rect, + position, + button: MouseButton::Right, + button_state: MouseButtonState::Up, + }, + WM_MBUTTONUP => TrayIconEvent::Click { + id, + rect, + position, + button: MouseButton::Middle, + button_state: MouseButtonState::Up, + }, + WM_LBUTTONDBLCLK => TrayIconEvent::DoubleClick { + id, + rect, + position, + button: MouseButton::Left, + }, + WM_RBUTTONDBLCLK => TrayIconEvent::DoubleClick { + id, + rect, + position, + button: MouseButton::Right, + }, + WM_MBUTTONDBLCLK => TrayIconEvent::DoubleClick { + id, + rect, + position, + button: MouseButton::Middle, + }, + WM_MOUSEMOVE if !userdata.entered => { + userdata.entered = true; + TrayIconEvent::Enter { id, rect, position } + } + WM_MOUSEMOVE if userdata.entered => { + // handle extra WM_MOUSEMOVE events, ignore if position hasn't changed + let cursor_moved = userdata.last_position != Some(position); + userdata.last_position = Some(position); + if cursor_moved { + // Set or update existing timer, where we check if cursor left + SetTimer(hwnd, WM_USER_LEAVE_TIMER_ID as _, 15, Some(tray_timer_proc)); + + TrayIconEvent::Move { id, rect, position } + } else { + return 0; + } + } + + _ => unreachable!(), + }; + + TrayIconEvent::send(event); + + if lparam as u32 == WM_RBUTTONDOWN + || (userdata.menu_on_left_click && lparam as u32 == WM_LBUTTONDOWN) + { + if let Some(menu) = userdata.hpopupmenu { + show_tray_menu(hwnd, menu, cursor.x, cursor.y); + } + } + } + + WM_TIMER if wparam as u32 == WM_USER_LEAVE_TIMER_ID => { + if let Some(position) = userdata.last_position.take() { + let mut cursor = POINT { x: 0, y: 0 }; + if GetCursorPos(&mut cursor as _) == 0 { + return 0; + } + + let rect = match get_tray_rect(userdata.internal_id, hwnd) { + Some(r) => r, + None => return 0, + }; + + let in_x = (rect.left..rect.right).contains(&cursor.x); + let in_y = (rect.top..rect.bottom).contains(&cursor.y); + + if !in_x || !in_y { + KillTimer(hwnd, WM_USER_LEAVE_TIMER_ID as _); + userdata.entered = false; + + TrayIconEvent::send(TrayIconEvent::Leave { + id: userdata.id.clone(), + rect: rect.into(), + position, + }); + } + } + + return 0; + } + + _ => {} + } + + DefWindowProcW(hwnd, msg, wparam, lparam) +} + +unsafe extern "system" fn tray_timer_proc(hwnd: HWND, msg: u32, wparam: WPARAM, lparam: u32) { + tray_proc(hwnd, msg, wparam, lparam as _); +} + +#[inline] +unsafe fn show_tray_menu(hwnd: HWND, menu: HMENU, x: i32, y: i32) { + // bring the hidden window to the foreground so the pop up menu + // would automatically hide on click outside + SetForegroundWindow(hwnd); + TrackPopupMenu( + menu, + // align bottom / right, maybe we could expose this later.. + TPM_BOTTOMALIGN | TPM_LEFTALIGN, + x, + y, + 0, + hwnd, + std::ptr::null_mut(), + ); +} + +#[inline] +unsafe fn register_tray_icon( + hwnd: HWND, + tray_id: u32, + hicon: &Option, + tooltip: &Option, +) -> bool { + let mut h_icon = std::ptr::null_mut(); + let mut flags = NIF_MESSAGE; + let mut sz_tip: [u16; 128] = [0; 128]; + + if let Some(hicon) = hicon { + flags |= NIF_ICON; + h_icon = *hicon; + } + + if let Some(tooltip) = tooltip { + flags |= NIF_TIP; + let tip = util::encode_wide(tooltip); + #[allow(clippy::manual_memcpy)] + for i in 0..tip.len().min(128) { + sz_tip[i] = tip[i]; + } + } + + let mut nid = NOTIFYICONDATAW { + uFlags: flags, + hWnd: hwnd, + uID: tray_id, + uCallbackMessage: WM_USER_TRAYICON, + hIcon: h_icon, + szTip: sz_tip, + ..std::mem::zeroed() + }; + + Shell_NotifyIconW(NIM_ADD, &mut nid as _) == TRUE +} + +#[inline] +unsafe fn remove_tray_icon(hwnd: HWND, id: u32) { + let mut nid = NOTIFYICONDATAW { + uFlags: NIF_ICON, + hWnd: hwnd, + uID: id, + ..std::mem::zeroed() + }; + + if Shell_NotifyIconW(NIM_DELETE, &mut nid as _) == FALSE { + eprintln!("Error removing system tray icon"); + } +} + +#[inline] +fn get_tray_rect(id: u32, hwnd: HWND) -> Option { + let nid = NOTIFYICONIDENTIFIER { + hWnd: hwnd, + cbSize: std::mem::size_of::() as _, + uID: id, + ..unsafe { std::mem::zeroed() } + }; + + let mut rect = RECT { + left: 0, + bottom: 0, + right: 0, + top: 0, + }; + if unsafe { Shell_NotifyIconGetRect(&nid, &mut rect) } == S_OK { + Some(rect) + } else { + None + } +} + +impl From for Rect { + fn from(rect: RECT) -> Self { + Self { + position: crate::dpi::PhysicalPosition::new(rect.left.into(), rect.top.into()), + size: crate::dpi::PhysicalSize::new( + rect.right.saturating_sub(rect.left) as u32, + rect.bottom.saturating_sub(rect.top) as u32, + ), + } + } +} diff --git a/src-tauri/vendor/tray-icon/src/platform_impl/windows/util.rs b/src-tauri/vendor/tray-icon/src/platform_impl/windows/util.rs new file mode 100644 index 0000000000..0088ea7d9d --- /dev/null +++ b/src-tauri/vendor/tray-icon/src/platform_impl/windows/util.rs @@ -0,0 +1,55 @@ +// Copyright 2022-2022 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use windows_sys::Win32::{Foundation::HWND, UI::WindowsAndMessaging::WINDOW_LONG_PTR_INDEX}; + +pub fn encode_wide>(string: S) -> Vec { + std::os::windows::prelude::OsStrExt::encode_wide(string.as_ref()) + .chain(std::iter::once(0)) + .collect() +} + +// taken from winit's code base +// https://github.com/rust-windowing/winit/blob/ee88e38f13fbc86a7aafae1d17ad3cd4a1e761df/src/platform_impl/windows/util.rs#L138 +pub fn get_instance_handle() -> windows_sys::Win32::Foundation::HMODULE { + // Gets the instance handle by taking the address of the + // pseudo-variable created by the microsoft linker: + // https://devblogs.microsoft.com/oldnewthing/20041025-00/?p=37483 + + // This is preferred over GetModuleHandle(NULL) because it also works in DLLs: + // https://stackoverflow.com/questions/21718027/getmodulehandlenull-vs-hinstance + + extern "C" { + static __ImageBase: windows_sys::Win32::System::SystemServices::IMAGE_DOS_HEADER; + } + + unsafe { &__ImageBase as *const _ as _ } +} + +#[inline(always)] +pub unsafe fn get_window_long(hwnd: HWND, nindex: WINDOW_LONG_PTR_INDEX) -> isize { + #[cfg(target_pointer_width = "64")] + return unsafe { windows_sys::Win32::UI::WindowsAndMessaging::GetWindowLongPtrW(hwnd, nindex) }; + #[cfg(target_pointer_width = "32")] + return unsafe { + windows_sys::Win32::UI::WindowsAndMessaging::GetWindowLongW(hwnd, nindex) as isize + }; +} + +#[inline(always)] +pub unsafe fn set_window_long( + hwnd: HWND, + nindex: WINDOW_LONG_PTR_INDEX, + dwnewlong: isize, +) -> isize { + #[cfg(target_pointer_width = "64")] + return unsafe { + windows_sys::Win32::UI::WindowsAndMessaging::SetWindowLongPtrW(hwnd, nindex, dwnewlong) + }; + #[cfg(target_pointer_width = "32")] + return unsafe { + windows_sys::Win32::UI::WindowsAndMessaging::SetWindowLongW(hwnd, nindex, dwnewlong as i32) + as isize + }; +} diff --git a/src-tauri/vendor/tray-icon/src/tray_icon_id.rs b/src-tauri/vendor/tray-icon/src/tray_icon_id.rs new file mode 100644 index 0000000000..8a2d18dabc --- /dev/null +++ b/src-tauri/vendor/tray-icon/src/tray_icon_id.rs @@ -0,0 +1,92 @@ +use std::{convert::Infallible, str::FromStr}; + +use crate::COUNTER; + +/// An unique id that is associated with a tray icon. +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Default, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct TrayIconId(pub String); + +impl TrayIconId { + /// Create a new tray icon id. + pub fn new>(id: S) -> Self { + Self(id.as_ref().to_string()) + } + + /// Generate id based on process id for internal use + pub(crate) fn new_unique() -> Self { + Self(format!("{}-{}", std::process::id(), COUNTER.next())) + } +} + +impl AsRef for TrayIconId { + fn as_ref(&self) -> &str { + self.0.as_ref() + } +} + +impl From for TrayIconId { + fn from(value: T) -> Self { + Self::new(value.to_string()) + } +} + +impl FromStr for TrayIconId { + type Err = Infallible; + + fn from_str(s: &str) -> std::result::Result { + Ok(Self::new(s)) + } +} + +impl PartialEq<&str> for TrayIconId { + fn eq(&self, other: &&str) -> bool { + self.0 == *other + } +} + +impl PartialEq<&str> for &TrayIconId { + fn eq(&self, other: &&str) -> bool { + self.0 == *other + } +} + +impl PartialEq for TrayIconId { + fn eq(&self, other: &String) -> bool { + self.0 == *other + } +} + +impl PartialEq for &TrayIconId { + fn eq(&self, other: &String) -> bool { + self.0 == *other + } +} + +impl PartialEq<&String> for TrayIconId { + fn eq(&self, other: &&String) -> bool { + self.0 == **other + } +} + +impl PartialEq<&TrayIconId> for TrayIconId { + fn eq(&self, other: &&TrayIconId) -> bool { + other.0 == self.0 + } +} + +#[cfg(test)] +mod test { + use crate::TrayIconId; + + #[test] + fn is_eq() { + assert_eq!(TrayIconId::new("t"), "t",); + assert_eq!(TrayIconId::new("t"), String::from("t")); + assert_eq!(TrayIconId::new("t"), &String::from("t")); + assert_eq!(TrayIconId::new("t"), TrayIconId::new("t")); + assert_eq!(TrayIconId::new("t"), &TrayIconId::new("t")); + assert_eq!(&TrayIconId::new("t"), &TrayIconId::new("t")); + assert_eq!(TrayIconId::new("t").as_ref(), "t"); + } +}