fix: use app's resource table for storing tray icons (#13316)

* Use app's resource table for storing tray icons

* Clean up

* Move remove tray logic to Resource::close
This commit is contained in:
Tony 2025-05-06 19:11:49 +08:00 committed by GitHub
parent b985eaf0a2
commit 4221124c4e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 91 additions and 103 deletions

View File

@ -1036,7 +1036,7 @@ impl RustAppSettings {
.product_name
.clone()
.unwrap_or_else(|| cargo_package_settings.name.clone()),
version: version.clone(),
version,
description: cargo_package_settings
.description
.clone()

View File

@ -741,7 +741,7 @@ macro_rules! shared_app_impl {
I: ?Sized,
TrayIconId: PartialEq<&'a I>,
{
self.manager.tray.tray_by_id(id)
self.manager.tray.tray_by_id(self.app_handle(), id)
}
/// Removes a tray icon using the provided id from tauri's internal state and returns it.
@ -755,7 +755,7 @@ macro_rules! shared_app_impl {
I: ?Sized,
TrayIconId: PartialEq<&'a I>,
{
self.manager.tray.remove_tray_by_id(id)
self.manager.tray.remove_tray_by_id(self.app_handle(), id)
}
/// Gets the app's configuration, defined on the `tauri.conf.json` file.

View File

@ -2,19 +2,23 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use std::{collections::HashMap, fmt, sync::Mutex};
use std::{
collections::HashMap,
fmt,
sync::{Arc, Mutex},
};
use crate::{
app::GlobalTrayIconEventListener,
image::Image,
tray::{TrayIcon, TrayIconEvent, TrayIconId},
AppHandle, Runtime,
AppHandle, Manager, Resource, ResourceId, Runtime,
};
pub struct TrayManager<R: Runtime> {
pub(crate) icon: Option<Image<'static>>,
/// Tray icons
pub(crate) icons: Mutex<Vec<TrayIcon<R>>>,
pub(crate) icons: Mutex<Vec<(TrayIconId, ResourceId)>>,
/// Global Tray icon event listeners.
pub(crate) global_event_listeners: Mutex<Vec<GlobalTrayIconEventListener<AppHandle<R>>>>,
/// Tray icon event listeners.
@ -41,30 +45,46 @@ impl<R: Runtime> TrayManager<R> {
.push(Box::new(handler));
}
pub fn tray_by_id<'a, I>(&self, id: &'a I) -> Option<TrayIcon<R>>
pub fn tray_by_id<'a, I>(&self, app: &AppHandle<R>, id: &'a I) -> Option<TrayIcon<R>>
where
I: ?Sized,
TrayIconId: PartialEq<&'a I>,
{
self
.icons
.lock()
.unwrap()
.iter()
.find(|t| t.id() == &id)
.cloned()
let icons = self.icons.lock().unwrap();
icons.iter().find_map(|(tray_icon_id, rid)| {
if tray_icon_id == &id {
let icon = app.resources_table().get::<TrayIcon<R>>(*rid).ok()?;
Some(Arc::unwrap_or_clone(icon))
} else {
None
}
})
}
pub fn remove_tray_by_id<'a, I>(&self, id: &'a I) -> Option<TrayIcon<R>>
pub fn tray_resource_by_id<'a, I>(&self, id: &'a I) -> Option<ResourceId>
where
I: ?Sized,
TrayIconId: PartialEq<&'a I>,
{
let mut icons = self.icons.lock().unwrap();
let idx = icons.iter().position(|t| t.id() == &id);
if let Some(idx) = idx {
return Some(icons.swap_remove(idx));
}
None
let icons = self.icons.lock().unwrap();
icons.iter().find_map(|(tray_icon_id, rid)| {
if tray_icon_id == &id {
Some(*rid)
} else {
None
}
})
}
pub fn remove_tray_by_id<'a, I>(&self, app: &AppHandle<R>, id: &'a I) -> Option<TrayIcon<R>>
where
I: ?Sized,
TrayIconId: PartialEq<&'a I>,
{
let rid = self.tray_resource_by_id(id)?;
let icon = app.resources_table().take::<TrayIcon<R>>(rid).ok()?;
let icon_to_return = icon.clone();
icon.close();
Some(Arc::unwrap_or_clone(icon_to_return))
}
}

View File

@ -10,10 +10,10 @@ use crate::app::{GlobalMenuEventListener, GlobalTrayIconEventListener};
use crate::menu::ContextMenu;
use crate::menu::MenuEvent;
use crate::resources::Resource;
use crate::UnsafeSend;
use crate::{
image::Image, menu::run_item_main_thread, AppHandle, Manager, PhysicalPosition, Rect, Runtime,
};
use crate::{ResourceId, UnsafeSend};
use serde::Serialize;
use std::path::Path;
pub use tray_icon::TrayIconId;
@ -358,8 +358,10 @@ impl<R: Runtime> TrayIconBuilder<R> {
self.inner.id()
}
/// Builds and adds a new [`TrayIcon`] to the system tray.
pub fn build<M: Manager<R>>(self, manager: &M) -> crate::Result<TrayIcon<R>> {
pub(crate) fn build_inner(
self,
app_handle: &AppHandle<R>,
) -> crate::Result<(TrayIcon<R>, ResourceId)> {
let id = self.id().clone();
// SAFETY:
@ -368,8 +370,7 @@ impl<R: Runtime> TrayIconBuilder<R> {
let unsafe_builder = UnsafeSend(self.inner);
let (tx, rx) = std::sync::mpsc::channel();
let unsafe_tray = manager
.app_handle()
let unsafe_tray = app_handle
.run_on_main_thread(move || {
// SAFETY: will only be accessed on main thread
let _ = tx.send(unsafe_builder.take().build().map(UnsafeSend));
@ -379,15 +380,21 @@ impl<R: Runtime> TrayIconBuilder<R> {
let icon = TrayIcon {
id,
inner: unsafe_tray.take(),
app_handle: manager.app_handle().clone(),
app_handle: app_handle.clone(),
};
icon.register(
let rid = icon.register(
&icon.app_handle,
self.on_menu_event,
self.on_tray_icon_event,
);
Ok((icon, rid))
}
/// Builds and adds a new [`TrayIcon`] to the system tray.
pub fn build<M: Manager<R>>(self, manager: &M) -> crate::Result<TrayIcon<R>> {
let (icon, _rid) = self.build_inner(manager.app_handle())?;
Ok(icon)
}
}
@ -426,7 +433,7 @@ impl<R: Runtime> TrayIcon<R> {
app_handle: &AppHandle<R>,
on_menu_event: Option<GlobalMenuEventListener<AppHandle<R>>>,
on_tray_icon_event: Option<GlobalTrayIconEventListener<TrayIcon<R>>>,
) {
) -> ResourceId {
if let Some(handler) = on_menu_event {
app_handle
.manager
@ -447,13 +454,15 @@ impl<R: Runtime> TrayIcon<R> {
.insert(self.id.clone(), handler);
}
let rid = app_handle.resources_table().add(self.clone());
app_handle
.manager
.tray
.icons
.lock()
.unwrap()
.push(self.clone());
.push((self.id().clone(), rid));
rid
}
/// The application handle associated with this type.
@ -598,14 +607,13 @@ impl<R: Runtime> TrayIcon<R> {
impl<R: Runtime> Resource for TrayIcon<R> {
fn close(self: std::sync::Arc<Self>) {
self
.app_handle
.state::<plugin::TrayIcons>()
.icons
.lock()
.unwrap()
.remove(&self.id.0);
self.app_handle.remove_tray_by_id(&self.id);
let mut icons = self.app_handle.manager.tray.icons.lock().unwrap();
for (i, (tray_icon_id, _rid)) in icons.iter_mut().enumerate() {
if tray_icon_id == &self.id {
icons.swap_remove(i);
return;
}
}
}
}

View File

@ -2,8 +2,9 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use std::{collections::HashMap, path::PathBuf, sync::Mutex};
use std::path::PathBuf;
use anyhow::Context;
use serde::Deserialize;
use crate::{
@ -14,15 +15,11 @@ use crate::{
plugin::{Builder, TauriPlugin},
resources::ResourceId,
tray::TrayIconBuilder,
AppHandle, Manager, Runtime, State, Webview,
AppHandle, Manager, Runtime, Webview,
};
use super::{TrayIcon, TrayIconEvent};
pub(crate) struct TrayIcons {
pub(crate) icons: Mutex<HashMap<String, ResourceId>>,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct TrayIconOptions {
@ -40,7 +37,6 @@ struct TrayIconOptions {
#[command(root = "crate")]
fn new<R: Runtime>(
webview: Webview<R>,
icons: State<'_, TrayIcons>,
options: TrayIconOptions,
handler: Channel<TrayIconEvent>,
) -> crate::Result<(ResourceId, String)> {
@ -54,7 +50,7 @@ fn new<R: Runtime>(
let _ = handler.send(e);
});
let mut resources_table = webview.resources_table();
let resources_table = webview.resources_table();
if let Some((rid, kind)) = options.menu {
match kind {
@ -92,58 +88,32 @@ fn new<R: Runtime>(
builder = builder.show_menu_on_left_click(show_menu_on_left_click);
}
let tray = builder.build(&webview)?;
let (tray, rid) = builder.build_inner(webview.app_handle())?;
let id = tray.id().as_ref().to_string();
let rid = resources_table.add(tray);
icons.icons.lock().unwrap().insert(id.clone(), rid);
Ok((rid, id))
}
#[command(root = "crate")]
fn get_by_id<R: Runtime>(
app: AppHandle<R>,
webview: Webview<R>,
icons: State<'_, TrayIcons>,
id: &str,
) -> crate::Result<Option<ResourceId>> {
// if the icon was created by this plugin, return the resource id
// this lets a getById call match the rid of a TrayIcon.new call
// which allows it to close a previously created icon
if let Some(rid) = icons.icons.lock().unwrap().get(id) {
return Ok(Some(*rid));
}
let tray = app.tray_by_id(id);
let maybe_rid = tray.map(|tray| {
let mut resources_table = webview.resources_table();
resources_table.add(tray)
});
if let Some(rid) = maybe_rid {
icons.icons.lock().unwrap().insert(id.to_string(), rid);
}
Ok(maybe_rid)
fn get_by_id<R: Runtime>(app: AppHandle<R>, id: &str) -> Option<ResourceId> {
app.manager.tray.tray_resource_by_id(id)
}
#[command(root = "crate")]
fn remove_by_id<R: Runtime>(app: AppHandle<R>, id: &str) -> crate::Result<()> {
app
.remove_tray_by_id(id)
.ok_or_else(|| anyhow::anyhow!("Can't find a tray associated with this id: {id}"))
.map(|_| ())
.map_err(Into::into)
.with_context(|| format!("Can't find a tray associated with this id: {id}"))?;
Ok(())
}
#[command(root = "crate")]
fn set_icon<R: Runtime>(
webview: Webview<R>,
app: AppHandle<R>,
rid: ResourceId,
icon: Option<JsImage>,
) -> crate::Result<()> {
let resources_table = webview.resources_table();
let resources_table = app.resources_table();
let tray = resources_table.get::<TrayIcon<R>>(rid)?;
let icon = match icon {
Some(i) => Some(i.into_img(&resources_table)?.as_ref().clone()),
@ -154,11 +124,11 @@ fn set_icon<R: Runtime>(
#[command(root = "crate")]
fn set_menu<R: Runtime>(
webview: Webview<R>,
app: AppHandle<R>,
rid: ResourceId,
menu: Option<(ResourceId, ItemKind)>,
) -> crate::Result<()> {
let resources_table = webview.resources_table();
let resources_table = app.resources_table();
let tray = resources_table.get::<TrayIcon<R>>(rid)?;
if let Some((rid, kind)) = menu {
match kind {
@ -180,78 +150,68 @@ fn set_menu<R: Runtime>(
#[command(root = "crate")]
fn set_tooltip<R: Runtime>(
webview: Webview<R>,
app: AppHandle<R>,
rid: ResourceId,
tooltip: Option<String>,
) -> crate::Result<()> {
let resources_table = webview.resources_table();
let resources_table = app.resources_table();
let tray = resources_table.get::<TrayIcon<R>>(rid)?;
tray.set_tooltip(tooltip)
}
#[command(root = "crate")]
fn set_title<R: Runtime>(
webview: Webview<R>,
app: AppHandle<R>,
rid: ResourceId,
title: Option<String>,
) -> crate::Result<()> {
let resources_table = webview.resources_table();
let resources_table = app.resources_table();
let tray = resources_table.get::<TrayIcon<R>>(rid)?;
tray.set_title(title)
}
#[command(root = "crate")]
fn set_visible<R: Runtime>(
webview: Webview<R>,
rid: ResourceId,
visible: bool,
) -> crate::Result<()> {
let resources_table = webview.resources_table();
fn set_visible<R: Runtime>(app: AppHandle<R>, rid: ResourceId, visible: bool) -> crate::Result<()> {
let resources_table = app.resources_table();
let tray = resources_table.get::<TrayIcon<R>>(rid)?;
tray.set_visible(visible)
}
#[command(root = "crate")]
fn set_temp_dir_path<R: Runtime>(
webview: Webview<R>,
app: AppHandle<R>,
rid: ResourceId,
path: Option<PathBuf>,
) -> crate::Result<()> {
let resources_table = webview.resources_table();
let resources_table = app.resources_table();
let tray = resources_table.get::<TrayIcon<R>>(rid)?;
tray.set_temp_dir_path(path)
}
#[command(root = "crate")]
fn set_icon_as_template<R: Runtime>(
webview: Webview<R>,
app: AppHandle<R>,
rid: ResourceId,
as_template: bool,
) -> crate::Result<()> {
let resources_table = webview.resources_table();
let resources_table = app.resources_table();
let tray = resources_table.get::<TrayIcon<R>>(rid)?;
tray.set_icon_as_template(as_template)
}
#[command(root = "crate")]
fn set_show_menu_on_left_click<R: Runtime>(
webview: Webview<R>,
app: AppHandle<R>,
rid: ResourceId,
on_left: bool,
) -> crate::Result<()> {
let resources_table = webview.resources_table();
let resources_table = app.resources_table();
let tray = resources_table.get::<TrayIcon<R>>(rid)?;
tray.set_show_menu_on_left_click(on_left)
}
pub(crate) fn init<R: Runtime>() -> TauriPlugin<R> {
Builder::new("tray")
.setup(|app, _api| {
app.manage(TrayIcons {
icons: Default::default(),
});
Ok(())
})
.invoke_handler(crate::generate_handler![
#![plugin(tray)]
new,