Skip to content

Commit

Permalink
feat: add tray and menu javascript APIs and events, closes #6617 (#7709)
Browse files Browse the repository at this point in the history
Co-authored-by: Lucas Fernandes Nogueira <[email protected]>
Co-authored-by: Fabian-Lars <[email protected]>
Co-authored-by: Lucas Nogueira <[email protected]>
  • Loading branch information
4 people committed Nov 19, 2023
1 parent 92b50a3 commit f93148e
Show file tree
Hide file tree
Showing 65 changed files with 3,902 additions and 554 deletions.
5 changes: 5 additions & 0 deletions .changes/api-tray-menu.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tauri-apps/api': 'minor:feat'
---

Add `tray` and `menu` modules to create and manage tray icons and menus from Javascript.
3 changes: 3 additions & 0 deletions .github/workflows/lint-js.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ jobs:
- name: install deps via yarn
working-directory: ./tooling/api/
run: yarn
- name: run ts:check
working-directory: ./tooling/api/
run: yarn ts:check
- name: run lint
working-directory: ./tooling/api/
run: yarn lint
Expand Down
2 changes: 1 addition & 1 deletion .husky/pre-commit
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ if [ -z "$(git diff --name-only tooling/api)" ]; then
else
cd tooling/api
yarn format
yarn lint-fix
yarn lint:fix
cd ../..
fi

Expand Down
5 changes: 4 additions & 1 deletion .scripts/ci/check-license-header.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ SPDX-License-Identifier: Apache-2.0
SPDX-License-Identifier: MIT`
const bundlerLicense =
'// Copyright 2016-2019 Cargo-Bundle developers <https://github.com/burtonageo/cargo-bundle>'
const denoLicense =
'// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.'

const extensions = ['.rs', '.js', '.ts', '.yml', '.swift', '.kt']
const ignore = [
Expand Down Expand Up @@ -43,7 +45,8 @@ async function checkFile(file) {
line.length === 0 ||
line.startsWith('#!') ||
line.startsWith('// swift-tools-version:') ||
line === bundlerLicense
line === bundlerLicense ||
line === denoLicense
) {
continue
}
Expand Down
62 changes: 62 additions & 0 deletions core/tauri-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ use proc_macro::TokenStream;
use syn::{parse_macro_input, DeriveInput};

mod command;
mod menu;
mod mobile;
mod runtime;

Expand Down Expand Up @@ -89,3 +90,64 @@ pub fn default_runtime(attributes: TokenStream, input: TokenStream) -> TokenStre
let input = parse_macro_input!(input as DeriveInput);
runtime::default_runtime(attributes, input).into()
}

/// Accepts a closure-like syntax to call arbitrary code on a menu item
/// after matching against `kind` and retrieving it from `resources_table` using `rid`.
///
/// You can optionally pass a third parameter to select which item kinds
/// to match against, by providing a `|` separated list of item kinds
/// ```ignore
/// do_menu_item!(|i| i.set_text(text), Check | Submenu);
/// ```
/// You could also provide a negated list
/// ```ignore
/// do_menu_item!(|i| i.set_text(text), !Check);
/// do_menu_item!(|i| i.set_text(text), !Check | !Submenu);
/// ```
/// but you can't have mixed negations and positive kinds.
/// ```ignore
/// do_menu_item!(|i| i.set_text(text), !Check | Submeun);
/// ```
///
/// #### Example
///
/// ```ignore
/// let rid = 23;
/// let kind = ItemKind::Check;
/// let resources_table = app.manager.resources_table();
/// do_menu_item!(|i| i.set_text(text))
/// ```
/// which will expand into:
/// ```ignore
/// let rid = 23;
/// let kind = ItemKind::Check;
/// let resources_table = app.manager.resources_table();
/// match kind {
/// ItemKind::Submenu => {
/// let i = resources_table.get::<Submenu<R>>(rid)?;
/// i.set_text(text)
/// }
/// ItemKind::MenuItem => {
/// let i = resources_table.get::<MenuItem<R>>(rid)?;
/// i.set_text(text)
/// }
/// ItemKind::Predefined => {
/// let i = resources_table.get::<PredefinedMenuItem<R>>(rid)?;
/// i.set_text(text)
/// }
/// ItemKind::Check => {
/// let i = resources_table.get::<CheckMenuItem<R>>(rid)?;
/// i.set_text(text)
/// }
/// ItemKind::Icon => {
/// let i = resources_table.get::<IconMenuItem<R>>(rid)?;
/// i.set_text(text)
/// }
/// _ => unreachable!(),
/// }
/// ```
#[proc_macro]
pub fn do_menu_item(input: TokenStream) -> TokenStream {
let tokens = parse_macro_input!(input as menu::DoMenuItemInput);
menu::do_menu_item(tokens).into()
}
118 changes: 118 additions & 0 deletions core/tauri-macros/src/menu.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT

use proc_macro2::{Ident, Span, TokenStream};
use quote::quote;
use syn::{
parse::{Parse, ParseStream},
punctuated::Punctuated,
Expr, Token,
};

pub struct DoMenuItemInput {
resources_table: Ident,
rid: Ident,
kind: Ident,
var: Ident,
expr: Expr,
kinds: Vec<NegatedIdent>,
}

#[derive(Clone)]
struct NegatedIdent(bool, Ident);

impl Parse for NegatedIdent {
fn parse(input: ParseStream) -> syn::Result<Self> {
let t = input.parse::<Token![!]>();
let i: Ident = input.parse()?;
Ok(NegatedIdent(t.is_ok(), i))
}
}

impl Parse for DoMenuItemInput {
fn parse(input: ParseStream) -> syn::Result<Self> {
let resources_table: Ident = input.parse()?;
let _: Token![,] = input.parse()?;
let rid: Ident = input.parse()?;
let _: Token![,] = input.parse()?;
let kind: Ident = input.parse()?;
let _: Token![,] = input.parse()?;
let _: Token![|] = input.parse()?;
let var: Ident = input.parse()?;
let _: Token![|] = input.parse()?;
let expr: Expr = input.parse()?;
let _: syn::Result<Token![,]> = input.parse();
let kinds = Punctuated::<NegatedIdent, Token![|]>::parse_terminated(input)?;

Ok(Self {
resources_table,
rid,
kind,
var,
expr,
kinds: kinds.into_iter().collect(),
})
}
}

pub fn do_menu_item(input: DoMenuItemInput) -> TokenStream {
let DoMenuItemInput {
rid,
resources_table,
kind,
expr,
var,
mut kinds,
} = input;

let defaults = vec![
NegatedIdent(false, Ident::new("Submenu", Span::call_site())),
NegatedIdent(false, Ident::new("MenuItem", Span::call_site())),
NegatedIdent(false, Ident::new("Predefined", Span::call_site())),
NegatedIdent(false, Ident::new("Check", Span::call_site())),
NegatedIdent(false, Ident::new("Icon", Span::call_site())),
];

if kinds.is_empty() {
kinds.extend(defaults.clone());
}

let has_negated = kinds.iter().any(|n| n.0);

if has_negated {
kinds.extend(defaults);
kinds.sort_by(|a, b| a.1.cmp(&b.1));
kinds.dedup_by(|a, b| a.1 == b.1);
}

let (kinds, types): (Vec<Ident>, Vec<Ident>) = kinds
.into_iter()
.filter_map(|nident| {
if nident.0 {
None
} else {
match nident.1 {
i if i == "MenuItem" => Some((i, Ident::new("MenuItem", Span::call_site()))),
i if i == "Submenu" => Some((i, Ident::new("Submenu", Span::call_site()))),
i if i == "Predefined" => Some((i, Ident::new("PredefinedMenuItem", Span::call_site()))),
i if i == "Check" => Some((i, Ident::new("CheckMenuItem", Span::call_site()))),
i if i == "Icon" => Some((i, Ident::new("IconMenuItem", Span::call_site()))),
_ => None,
}
}
})
.unzip();

quote! {
match #kind {
#(
ItemKind::#kinds => {
let #var = #resources_table.get::<#types<R>>(#rid)?;
#expr
}
)*
_ => unreachable!(),
}
}
}
4 changes: 2 additions & 2 deletions core/tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,8 @@ ico = { version = "0.3.0", optional = true }
http-range = { version = "0.1.5", optional = true }

[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]
muda = { version = "0.10", default-features = false }
tray-icon = { version = "0.10", default-features = false, optional = true }
muda = { version = "0.10", default-features = false, features = [ "serde" ] }
tray-icon = { version = "0.10", default-features = false, features = [ "serde" ], optional = true }

[target."cfg(any(target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\"))".dependencies]
gtk = { version = "0.18", features = [ "v3_24" ] }
Expand Down
2 changes: 1 addition & 1 deletion core/tauri/scripts/bundle.global.js

Large diffs are not rendered by default.

8 changes: 7 additions & 1 deletion core/tauri/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -772,7 +772,8 @@ macro_rules! shared_app_impl {
/// **You should always exit the tauri app immediately after this function returns and not use any tauri-related APIs.**
pub fn cleanup_before_exit(&self) {
#[cfg(all(desktop, feature = "tray-icon"))]
self.manager.tray.icons.lock().unwrap().clear()
self.manager.tray.icons.lock().unwrap().clear();
self.manager.resources_table().clear();
}
}
};
Expand All @@ -787,6 +788,11 @@ impl<R: Runtime> App<R> {
self.handle.plugin(crate::event::plugin::init())?;
self.handle.plugin(crate::window::plugin::init())?;
self.handle.plugin(crate::app::plugin::init())?;
self.handle.plugin(crate::resources::plugin::init())?;
#[cfg(desktop)]
self.handle.plugin(crate::menu::plugin::init())?;
#[cfg(all(desktop, feature = "tray-icon"))]
self.handle.plugin(crate::tray::plugin::init())?;
Ok(())
}

Expand Down
6 changes: 6 additions & 0 deletions core/tauri/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,12 @@ pub enum Error {
/// window not found.
#[error("window not found")]
WindowNotFound,
/// The resource id is invalid.
#[error("The resource id {0} is invalid.")]
BadResourceId(crate::resources::ResourceId),
/// The anyhow crate error.
#[error(transparent)]
Anyhow(#[from] anyhow::Error),
}

/// `Result<T, ::tauri::Error>`
Expand Down
Loading

0 comments on commit f93148e

Please sign in to comment.