Skip to content

Commit f93148e

Browse files
amrbashirlucasfernogFabianLars
authored
feat: add tray and menu javascript APIs and events, closes #6617 (#7709)
Co-authored-by: Lucas Fernandes Nogueira <lucas@tauri.app> Co-authored-by: Fabian-Lars <fabianlars@fabianlars.de> Co-authored-by: Lucas Nogueira <lucas@tauri.studio>
1 parent 92b50a3 commit f93148e

65 files changed

Lines changed: 3902 additions & 554 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.changes/api-tray-menu.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@tauri-apps/api': 'minor:feat'
3+
---
4+
5+
Add `tray` and `menu` modules to create and manage tray icons and menus from Javascript.

.github/workflows/lint-js.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ jobs:
4949
- name: install deps via yarn
5050
working-directory: ./tooling/api/
5151
run: yarn
52+
- name: run ts:check
53+
working-directory: ./tooling/api/
54+
run: yarn ts:check
5255
- name: run lint
5356
working-directory: ./tooling/api/
5457
run: yarn lint

.husky/pre-commit

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ if [ -z "$(git diff --name-only tooling/api)" ]; then
1111
else
1212
cd tooling/api
1313
yarn format
14-
yarn lint-fix
14+
yarn lint:fix
1515
cd ../..
1616
fi
1717

.scripts/ci/check-license-header.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ SPDX-License-Identifier: Apache-2.0
1313
SPDX-License-Identifier: MIT`
1414
const bundlerLicense =
1515
'// Copyright 2016-2019 Cargo-Bundle developers <https://github.com/burtonageo/cargo-bundle>'
16+
const denoLicense =
17+
'// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.'
1618

1719
const extensions = ['.rs', '.js', '.ts', '.yml', '.swift', '.kt']
1820
const ignore = [
@@ -43,7 +45,8 @@ async function checkFile(file) {
4345
line.length === 0 ||
4446
line.startsWith('#!') ||
4547
line.startsWith('// swift-tools-version:') ||
46-
line === bundlerLicense
48+
line === bundlerLicense ||
49+
line === denoLicense
4750
) {
4851
continue
4952
}

core/tauri-macros/src/lib.rs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ use proc_macro::TokenStream;
1616
use syn::{parse_macro_input, DeriveInput};
1717

1818
mod command;
19+
mod menu;
1920
mod mobile;
2021
mod runtime;
2122

@@ -89,3 +90,64 @@ pub fn default_runtime(attributes: TokenStream, input: TokenStream) -> TokenStre
8990
let input = parse_macro_input!(input as DeriveInput);
9091
runtime::default_runtime(attributes, input).into()
9192
}
93+
94+
/// Accepts a closure-like syntax to call arbitrary code on a menu item
95+
/// after matching against `kind` and retrieving it from `resources_table` using `rid`.
96+
///
97+
/// You can optionally pass a third parameter to select which item kinds
98+
/// to match against, by providing a `|` separated list of item kinds
99+
/// ```ignore
100+
/// do_menu_item!(|i| i.set_text(text), Check | Submenu);
101+
/// ```
102+
/// You could also provide a negated list
103+
/// ```ignore
104+
/// do_menu_item!(|i| i.set_text(text), !Check);
105+
/// do_menu_item!(|i| i.set_text(text), !Check | !Submenu);
106+
/// ```
107+
/// but you can't have mixed negations and positive kinds.
108+
/// ```ignore
109+
/// do_menu_item!(|i| i.set_text(text), !Check | Submeun);
110+
/// ```
111+
///
112+
/// #### Example
113+
///
114+
/// ```ignore
115+
/// let rid = 23;
116+
/// let kind = ItemKind::Check;
117+
/// let resources_table = app.manager.resources_table();
118+
/// do_menu_item!(|i| i.set_text(text))
119+
/// ```
120+
/// which will expand into:
121+
/// ```ignore
122+
/// let rid = 23;
123+
/// let kind = ItemKind::Check;
124+
/// let resources_table = app.manager.resources_table();
125+
/// match kind {
126+
/// ItemKind::Submenu => {
127+
/// let i = resources_table.get::<Submenu<R>>(rid)?;
128+
/// i.set_text(text)
129+
/// }
130+
/// ItemKind::MenuItem => {
131+
/// let i = resources_table.get::<MenuItem<R>>(rid)?;
132+
/// i.set_text(text)
133+
/// }
134+
/// ItemKind::Predefined => {
135+
/// let i = resources_table.get::<PredefinedMenuItem<R>>(rid)?;
136+
/// i.set_text(text)
137+
/// }
138+
/// ItemKind::Check => {
139+
/// let i = resources_table.get::<CheckMenuItem<R>>(rid)?;
140+
/// i.set_text(text)
141+
/// }
142+
/// ItemKind::Icon => {
143+
/// let i = resources_table.get::<IconMenuItem<R>>(rid)?;
144+
/// i.set_text(text)
145+
/// }
146+
/// _ => unreachable!(),
147+
/// }
148+
/// ```
149+
#[proc_macro]
150+
pub fn do_menu_item(input: TokenStream) -> TokenStream {
151+
let tokens = parse_macro_input!(input as menu::DoMenuItemInput);
152+
menu::do_menu_item(tokens).into()
153+
}

core/tauri-macros/src/menu.rs

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
2+
// SPDX-License-Identifier: Apache-2.0
3+
// SPDX-License-Identifier: MIT
4+
5+
use proc_macro2::{Ident, Span, TokenStream};
6+
use quote::quote;
7+
use syn::{
8+
parse::{Parse, ParseStream},
9+
punctuated::Punctuated,
10+
Expr, Token,
11+
};
12+
13+
pub struct DoMenuItemInput {
14+
resources_table: Ident,
15+
rid: Ident,
16+
kind: Ident,
17+
var: Ident,
18+
expr: Expr,
19+
kinds: Vec<NegatedIdent>,
20+
}
21+
22+
#[derive(Clone)]
23+
struct NegatedIdent(bool, Ident);
24+
25+
impl Parse for NegatedIdent {
26+
fn parse(input: ParseStream) -> syn::Result<Self> {
27+
let t = input.parse::<Token![!]>();
28+
let i: Ident = input.parse()?;
29+
Ok(NegatedIdent(t.is_ok(), i))
30+
}
31+
}
32+
33+
impl Parse for DoMenuItemInput {
34+
fn parse(input: ParseStream) -> syn::Result<Self> {
35+
let resources_table: Ident = input.parse()?;
36+
let _: Token![,] = input.parse()?;
37+
let rid: Ident = input.parse()?;
38+
let _: Token![,] = input.parse()?;
39+
let kind: Ident = input.parse()?;
40+
let _: Token![,] = input.parse()?;
41+
let _: Token![|] = input.parse()?;
42+
let var: Ident = input.parse()?;
43+
let _: Token![|] = input.parse()?;
44+
let expr: Expr = input.parse()?;
45+
let _: syn::Result<Token![,]> = input.parse();
46+
let kinds = Punctuated::<NegatedIdent, Token![|]>::parse_terminated(input)?;
47+
48+
Ok(Self {
49+
resources_table,
50+
rid,
51+
kind,
52+
var,
53+
expr,
54+
kinds: kinds.into_iter().collect(),
55+
})
56+
}
57+
}
58+
59+
pub fn do_menu_item(input: DoMenuItemInput) -> TokenStream {
60+
let DoMenuItemInput {
61+
rid,
62+
resources_table,
63+
kind,
64+
expr,
65+
var,
66+
mut kinds,
67+
} = input;
68+
69+
let defaults = vec![
70+
NegatedIdent(false, Ident::new("Submenu", Span::call_site())),
71+
NegatedIdent(false, Ident::new("MenuItem", Span::call_site())),
72+
NegatedIdent(false, Ident::new("Predefined", Span::call_site())),
73+
NegatedIdent(false, Ident::new("Check", Span::call_site())),
74+
NegatedIdent(false, Ident::new("Icon", Span::call_site())),
75+
];
76+
77+
if kinds.is_empty() {
78+
kinds.extend(defaults.clone());
79+
}
80+
81+
let has_negated = kinds.iter().any(|n| n.0);
82+
83+
if has_negated {
84+
kinds.extend(defaults);
85+
kinds.sort_by(|a, b| a.1.cmp(&b.1));
86+
kinds.dedup_by(|a, b| a.1 == b.1);
87+
}
88+
89+
let (kinds, types): (Vec<Ident>, Vec<Ident>) = kinds
90+
.into_iter()
91+
.filter_map(|nident| {
92+
if nident.0 {
93+
None
94+
} else {
95+
match nident.1 {
96+
i if i == "MenuItem" => Some((i, Ident::new("MenuItem", Span::call_site()))),
97+
i if i == "Submenu" => Some((i, Ident::new("Submenu", Span::call_site()))),
98+
i if i == "Predefined" => Some((i, Ident::new("PredefinedMenuItem", Span::call_site()))),
99+
i if i == "Check" => Some((i, Ident::new("CheckMenuItem", Span::call_site()))),
100+
i if i == "Icon" => Some((i, Ident::new("IconMenuItem", Span::call_site()))),
101+
_ => None,
102+
}
103+
}
104+
})
105+
.unzip();
106+
107+
quote! {
108+
match #kind {
109+
#(
110+
ItemKind::#kinds => {
111+
let #var = #resources_table.get::<#types<R>>(#rid)?;
112+
#expr
113+
}
114+
)*
115+
_ => unreachable!(),
116+
}
117+
}
118+
}

core/tauri/Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,8 @@ ico = { version = "0.3.0", optional = true }
7373
http-range = { version = "0.1.5", optional = true }
7474

7575
[target."cfg(any(target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\", target_os = \"windows\", target_os = \"macos\"))".dependencies]
76-
muda = { version = "0.10", default-features = false }
77-
tray-icon = { version = "0.10", default-features = false, optional = true }
76+
muda = { version = "0.10", default-features = false, features = [ "serde" ] }
77+
tray-icon = { version = "0.10", default-features = false, features = [ "serde" ], optional = true }
7878

7979
[target."cfg(any(target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\"))".dependencies]
8080
gtk = { version = "0.18", features = [ "v3_24" ] }

core/tauri/scripts/bundle.global.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

core/tauri/src/app.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -772,7 +772,8 @@ macro_rules! shared_app_impl {
772772
/// **You should always exit the tauri app immediately after this function returns and not use any tauri-related APIs.**
773773
pub fn cleanup_before_exit(&self) {
774774
#[cfg(all(desktop, feature = "tray-icon"))]
775-
self.manager.tray.icons.lock().unwrap().clear()
775+
self.manager.tray.icons.lock().unwrap().clear();
776+
self.manager.resources_table().clear();
776777
}
777778
}
778779
};
@@ -787,6 +788,11 @@ impl<R: Runtime> App<R> {
787788
self.handle.plugin(crate::event::plugin::init())?;
788789
self.handle.plugin(crate::window::plugin::init())?;
789790
self.handle.plugin(crate::app::plugin::init())?;
791+
self.handle.plugin(crate::resources::plugin::init())?;
792+
#[cfg(desktop)]
793+
self.handle.plugin(crate::menu::plugin::init())?;
794+
#[cfg(all(desktop, feature = "tray-icon"))]
795+
self.handle.plugin(crate::tray::plugin::init())?;
790796
Ok(())
791797
}
792798

core/tauri/src/error.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,12 @@ pub enum Error {
130130
/// window not found.
131131
#[error("window not found")]
132132
WindowNotFound,
133+
/// The resource id is invalid.
134+
#[error("The resource id {0} is invalid.")]
135+
BadResourceId(crate::resources::ResourceId),
136+
/// The anyhow crate error.
137+
#[error(transparent)]
138+
Anyhow(#[from] anyhow::Error),
133139
}
134140

135141
/// `Result<T, ::tauri::Error>`

0 commit comments

Comments
 (0)