Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add tray and menu javascript APIs and events, closes #6617 #7709

Merged
merged 70 commits into from
Nov 19, 2023
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
70 commits
Select commit Hold shift + click to select a range
4795408
feat: add tray and menu javascript APIs and events
amrbashir Aug 16, 2023
004a2e5
some progress on menu and submenu
amrbashir Aug 18, 2023
ae721ce
implement the rest, needs testing
amrbashir Aug 19, 2023
a8f64d6
implement js menu events
amrbashir Aug 26, 2023
cfe3b35
update example api to latest vite-svelte template
amrbashir Aug 28, 2023
f918bdb
fix ipc protocol for empty args
amrbashir Aug 28, 2023
4f2cdb6
add anyhow error variant
amrbashir Aug 28, 2023
3daf2ec
fix menu js implementation
amrbashir Aug 29, 2023
2bd395e
remove console.log
amrbashir Aug 29, 2023
cff777d
lint and clippy
amrbashir Aug 29, 2023
d24a48a
docs
amrbashir Aug 29, 2023
4cdf9d2
fix cargo.toml
amrbashir Aug 29, 2023
b4f8ce6
generated files
amrbashir Aug 29, 2023
99398f2
fix check license header script
amrbashir Aug 29, 2023
46c0e7e
fix android and ios build
amrbashir Aug 29, 2023
cb3122b
change file
amrbashir Aug 29, 2023
7e046d8
change tag
amrbashir Aug 30, 2023
28646c3
Merge branch 'dev' into feat/tray-menu-js-apis
amrbashir Sep 18, 2023
432c5cd
fmt
amrbashir Sep 18, 2023
ebcafa2
Apply suggestions from code review
amrbashir Oct 11, 2023
efdb6ec
Update core/tauri/src/resources/mod.rs
amrbashir Oct 11, 2023
7ac1157
Update core/tauri-macros/src/menu.rs
amrbashir Oct 11, 2023
ff7b46f
Update core/tauri/src/resources/mod.rs
amrbashir Oct 11, 2023
11e6e21
Update core/tauri/src/resources/mod.rs
amrbashir Oct 11, 2023
97af23a
Update core/tauri-macros/src/menu.rs
amrbashir Oct 11, 2023
0945ba1
lints, safety comments, remove unsused code
amrbashir Oct 12, 2023
94ee625
whitespace and removed clippy lint
amrbashir Oct 12, 2023
3869388
Merge remote-tracking branch 'origin/dev' into feat/tray-menu-js-apis
lucasfernog Oct 23, 2023
c4b3ae4
typo
lucasfernog Oct 23, 2023
90030a4
use channels
lucasfernog Oct 23, 2023
6ab30d1
lint
lucasfernog Oct 23, 2023
d894b16
private constructors
lucasfernog Oct 23, 2023
e974000
basic tray example
lucasfernog Oct 23, 2023
37fb32d
Update core/tauri/src/resources/mod.rs [skip ci]
lucasfernog Oct 24, 2023
7a50dc6
basic menu example
lucasfernog Oct 24, 2023
03cee83
process tray icon
lucasfernog Oct 24, 2023
25d6a64
fix menu events
lucasfernog Oct 24, 2023
1db6d62
resolve TODOs, add popup example [skip ci]
lucasfernog Oct 24, 2023
cf155cc
Merge remote-tracking branch 'origin/dev' into feat/tray-menu-js-apis
lucasfernog Oct 24, 2023
c235e6f
camelcase [skip ci]
lucasfernog Oct 26, 2023
2182548
popup freezes on macos :( gotta be async
lucasfernog Oct 26, 2023
0d32edf
fix menu example on macos
lucasfernog Oct 26, 2023
5126d30
Merge remote-tracking branch 'origin/dev' into feat/tray-menu-js-apis
lucasfernog Oct 26, 2023
b415c98
fix popup on macos
lucasfernog Oct 26, 2023
a402a0e
lint [skip ci]
lucasfernog Oct 29, 2023
46a38d1
Merge remote-tracking branch 'origin/dev' into feat/tray-menu-js-apis
lucasfernog Nov 12, 2023
1a7c737
chore: cleanup
amrbashir Nov 14, 2023
b4838c5
cleanup menu JS APIs
amrbashir Nov 15, 2023
7ad44e6
dedup package and run ts:check in CI
amrbashir Nov 15, 2023
d6d87f0
Merge branch 'dev' into feat/tray-menu-js-apis
amrbashir Nov 15, 2023
4f92ec4
lciense headers and fix lint
amrbashir Nov 15, 2023
482d0e7
generate bundle
amrbashir Nov 15, 2023
dcf231d
use explicit idents
amrbashir Nov 15, 2023
872da53
fix api example port
amrbashir Nov 15, 2023
e512ce9
fix circular modules dependency
amrbashir Nov 15, 2023
1cca05e
update api example
amrbashir Nov 15, 2023
423c811
licesne and lint
amrbashir Nov 15, 2023
6d210be
fix build
amrbashir Nov 15, 2023
1bbc2a2
update api dist [skip ci]
lucasfernog Nov 18, 2023
dcae92e
fix predefined
lucasfernog Nov 18, 2023
d0eeaba
allow using objects directly for menu item construction for static menus
lucasfernog Nov 18, 2023
5f8b9d7
fix mobile channel load
lucasfernog Nov 18, 2023
4c36993
avoid panics
lucasfernog Nov 18, 2023
b36a771
inline MenuItemKind
lucasfernog Nov 18, 2023
61a3639
cleanup new channel interface
lucasfernog Nov 18, 2023
d97086d
fix pre-commit hook
lucasfernog Nov 18, 2023
8eb4cda
camel case
lucasfernog Nov 18, 2023
8883bf5
merge set_icon and set_native_icon
lucasfernog Nov 18, 2023
2a156f2
fix items(), get()
lucasfernog Nov 18, 2023
5369a9b
fix example [skip ci]
lucasfernog Nov 18, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
10 changes: 8 additions & 2 deletions .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 All @@ -26,7 +28,10 @@ const ignore = [
]

async function checkFile(file) {
if (extensions.some((e) => file.endsWith(e)) && !ignore.some((i) => file.includes(`/${i}/`))) {
if (
extensions.some((e) => file.endsWith(e)) &&
!ignore.some((i) => file.includes(`/${i}/`))
) {
const fileStream = fs.createReadStream(file)
const rl = readline.createInterface({
input: fileStream,
Expand All @@ -41,7 +46,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()
}

/// Accepets a closure-like syntax to call arbitray code on a menu item
amrbashir marked this conversation as resolved.
Show resolved Hide resolved
/// after mathcing against `kind` and retreiveing it from `resources_table` using `rid`.
amrbashir marked this conversation as resolved.
Show resolved Hide resolved
///
/// You can optionally pass a third paraemeter to select which item kinds
amrbashir marked this conversation as resolved.
Show resolved Hide resolved
/// to match agains, by providing a `|` separated list of item kinds
amrbashir marked this conversation as resolved.
Show resolved Hide resolved
/// ```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()
}
104 changes: 104 additions & 0 deletions core/tauri-macros/src/menu.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// 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 {
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 _: 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 {
var,
expr,
kinds: kinds.into_iter().collect(),
})
}
}

pub fn do_menu_item(input: DoMenuItemInput) -> TokenStream {
let DoMenuItemInput {
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 mut has_negated: bool = false;
for NegatedIdent(negated, _) in &kinds {
if *negated && !has_negated {
has_negated = true;
}
}
amrbashir marked this conversation as resolved.
Show resolved Hide resolved

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| match nident.1 {
i if i == "MenuItem" && !nident.0 => Some((i, Ident::new("MenuItem", Span::call_site()))),
i if i == "Submenu" && !nident.0 => Some((i, Ident::new("Submenu", Span::call_site()))),
i if i == "Predefined" && !nident.0 => {
Some((i, Ident::new("PredefinedMenuItem", Span::call_site())))
}
i if i == "Check" && !nident.0 => Some((i, Ident::new("CheckMenuItem", Span::call_site()))),
i if i == "Icon" && !nident.0 => Some((i, Ident::new("IconMenuItem", Span::call_site()))),
_ => None,
})
amrbashir marked this conversation as resolved.
Show resolved Hide resolved
.unzip();

quote! {
match kind {
amrbashir marked this conversation as resolved.
Show resolved Hide resolved
#(
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 @@ -71,8 +71,8 @@ ico = { version = "0.3.0", optional = true }
http-range = { version = "0.1.4", 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.8", default-features = false }
tray-icon = { version = "0.8", default-features = false, optional = true }
muda = { version = "0.8", default-features = false, features = [ "serde" ]}
tray-icon = { version = "0.8", 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.16", 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 @@ -798,7 +798,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.inner.tray_icons.lock().unwrap().clear()
self.manager.inner.tray_icons.lock().unwrap().clear();
self.manager.resources_table().clear();
}
}
};
Expand All @@ -811,6 +812,11 @@ impl<R: Runtime> App<R> {
fn register_core_plugins(&self) -> crate::Result<()> {
self.handle.plugin(crate::path::init())?;
self.handle.plugin(crate::event::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 @@ -110,4 +110,10 @@ pub enum Error {
#[cfg(all(desktop, feature = "tray-icon"))]
#[cfg_attr(doc_cfg, doc(cfg(all(desktop, feature = "tray-icon"))))]
BadTrayIcon(#[from] tray_icon::BadIcon),
/// 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),
}
37 changes: 36 additions & 1 deletion core/tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ mod manager;
mod pattern;
pub mod plugin;
pub(crate) mod protocol;
mod resources;
mod vibrancy;
pub mod window;
use tauri_runtime as runtime;
Expand Down Expand Up @@ -168,7 +169,7 @@ pub type Result<T> = std::result::Result<T, Error>;
/// A task to run on the main thread.
pub type SyncTask = Box<dyn FnOnce() + Send>;

use serde::Serialize;
use serde::{Deserialize, Serialize};
use std::{
collections::HashMap,
fmt::{self, Debug},
Expand Down Expand Up @@ -910,6 +911,40 @@ mod tests {
}
}

#[derive(Deserialize)]
#[serde(untagged)]
pub(crate) enum IconDto {
#[cfg(any(feature = "icon-png", feature = "icon-ico"))]
File(std::path::PathBuf),
#[cfg(any(feature = "icon-png", feature = "icon-ico"))]
Raw(Vec<u8>),
Rgba {
rgba: Vec<u8>,
width: u32,
height: u32,
},
}

impl From<IconDto> for Icon {
fn from(icon: IconDto) -> Self {
match icon {
#[cfg(any(feature = "icon-png", feature = "icon-ico"))]
IconDto::File(path) => Self::File(path),
#[cfg(any(feature = "icon-png", feature = "icon-ico"))]
IconDto::Raw(raw) => Self::Raw(raw),
IconDto::Rgba {
rgba,
width,
height,
} => Self::Rgba {
rgba,
width,
height,
},
}
}
}

#[allow(unused)]
macro_rules! run_main_thread {
($self:ident, $ex:expr) => {{
Expand Down
13 changes: 13 additions & 0 deletions core/tauri/src/manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ use crate::{
ipc::{Invoke, InvokeHandler, InvokeResponder},
pattern::PatternJavascript,
plugin::PluginStore,
resources::ResourceTable,
runtime::{
webview::WindowBuilder,
window::{
Expand Down Expand Up @@ -273,6 +274,8 @@ pub struct InnerWindowManager<R: Runtime> {
invoke_initialization_script: String,
/// Application pattern.
pub(crate) pattern: Pattern,
/// Application Resources Table
resources_table: Arc<Mutex<ResourceTable>>,
}

impl<R: Runtime> fmt::Debug for InnerWindowManager<R> {
Expand Down Expand Up @@ -363,6 +366,7 @@ impl<R: Runtime> WindowManager<R> {
tray_icon: context.tray_icon,
package_info: context.package_info,
pattern: context.pattern,
resources_table: Arc::default(),
uri_scheme_protocols,
#[cfg(desktop)]
menus: Default::default(),
Expand All @@ -389,6 +393,15 @@ impl<R: Runtime> WindowManager<R> {
&self.inner.pattern
}

/// Resources table managed by the application.
pub(crate) fn resources_table(&self) -> MutexGuard<'_, ResourceTable> {
self
.inner
.resources_table
.lock()
.expect("poisoned window manager")
}

/// Get a locked handle to the windows.
pub(crate) fn windows_lock(&self) -> MutexGuard<'_, HashMap<String, Window<R>>> {
self.inner.windows.lock().expect("poisoned window manager")
Expand Down
10 changes: 7 additions & 3 deletions core/tauri/src/menu/check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT

use crate::{menu::MenuId, run_main_thread, AppHandle, Manager, Runtime};
use crate::{menu::MenuId, resources::Resource, run_main_thread, AppHandle, Manager, Runtime};

/// A menu item inside a [`Menu`] or [`Submenu`] and contains only text.
/// A menu item inside a [`Menu`] or [`Submenu`]
/// and usually contains a text and a check mark or a similar toggle
/// that corresponds to a checked and unchecked states.
///
/// [`Menu`]: super::Menu
/// [`Submenu`]: super::Submenu
Expand All @@ -31,7 +33,7 @@ unsafe impl<R: Runtime> Sync for CheckMenuItem<R> {}
unsafe impl<R: Runtime> Send for CheckMenuItem<R> {}

impl<R: Runtime> super::sealed::IsMenuItemBase for CheckMenuItem<R> {
fn inner(&self) -> &dyn muda::IsMenuItem {
fn inner_muda(&self) -> &dyn muda::IsMenuItem {
&self.inner
}
}
Expand Down Expand Up @@ -146,3 +148,5 @@ impl<R: Runtime> CheckMenuItem<R> {
run_main_thread!(self, |self_: Self| self_.inner.set_checked(checked))
}
}

impl<R: Runtime> Resource for CheckMenuItem<R> {}
Loading
Loading