diff --git a/Cargo.lock b/Cargo.lock index ca2c8a5..f1eef24 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -666,15 +666,15 @@ dependencies = [ [[package]] name = "derive_more" -version = "0.99.17" +version = "0.99.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" +checksum = "5f33878137e4dafd7fa914ad4e259e18a4e8e532b9617a2d0150262bf53abfce" dependencies = [ "convert_case", "proc-macro2", "quote", "rustc_version 0.4.0", - "syn 1.0.103", + "syn 2.0.68", ] [[package]] @@ -3713,6 +3713,7 @@ dependencies = [ name = "tauri-sys" version = "0.2.0" dependencies = [ + "derive_more", "futures", "js-sys", "log", diff --git a/Cargo.toml b/Cargo.toml index c9ae3c1..cdfba62 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ version = "0.2.0" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +derive_more = "0.99.18" futures = { version = "0.3.30", optional = true } js-sys = "0.3.69" log = "0.4.21" @@ -24,10 +25,11 @@ wasm-bindgen-test = "0.3.42" all-features = true [features] -all = ["core", "dpi", "event", "window"] +all = ["core", "dpi", "event", "menu", "window"] core = [] dpi = [] event = ["dep:futures"] +menu = ["core", "window"] window = ["dpi", "event"] [workspace] diff --git a/README.md b/README.md index 7d196bd..d0f4964 100644 --- a/README.md +++ b/README.md @@ -51,8 +51,9 @@ fn main() { All modules are gated by accordingly named Cargo features. It is recommended you keep this synced with the features enabled in your [Tauri Allowlist] but no automated tool for this exists (yet). - **all**: Enables all modules. -- **core**: Enables the `core` module. (Only `invoke` and `convertFileSrc` currently implemented.) +- **core**: Enables the `core` module. (~70% implmented) - **event**: Enables the `event` module. +- **menu**: Enables the `menu` module. (~20% implemented) - **window**: Enables the `windows` module. (~20% implemented) ## Are we Tauri yet? @@ -64,7 +65,7 @@ These API bindings are not completely on-par with `@tauri-apps/api` yet, but her - [x] `dpi` - [x] `event` - [ ] `image` -- [ ] `menu` +- [x] `menu` (partial implementation) - [ ] `mocks` - [ ] `path` - [ ] `tray` @@ -74,6 +75,9 @@ These API bindings are not completely on-par with `@tauri-apps/api` yet, but her The current API also very closely mirrors the JS API even though that might not be the most ergonomic choice, ideas for improving the API with quality-of-life features beyond the regular JS API interface are very welcome. +## Examples +The [`examples/leptos`] crate provides examples of how to use most of the implemented functionality. + [wasm-bindgen]: https://github.com/rustwasm/wasm-bindgen [tauri allowlist]: https://tauri.app/v1/api/config#allowlistconfig [`esbuild`]: https://esbuild.github.io/getting-started/#install-esbuild diff --git a/examples/leptos/.gitignore b/examples/leptos/.gitignore new file mode 100644 index 0000000..48c3ca4 --- /dev/null +++ b/examples/leptos/.gitignore @@ -0,0 +1,3 @@ +/dist/ +/target/ +/Cargo.lock diff --git a/examples/leptos/.taurignore b/examples/leptos/.taurignore new file mode 100644 index 0000000..1ebdc6d --- /dev/null +++ b/examples/leptos/.taurignore @@ -0,0 +1,3 @@ +/src +/public +/Cargo.toml \ No newline at end of file diff --git a/examples/leptos/.vscode/extensions.json b/examples/leptos/.vscode/extensions.json new file mode 100644 index 0000000..24d7cc6 --- /dev/null +++ b/examples/leptos/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["tauri-apps.tauri-vscode", "rust-lang.rust-analyzer"] +} diff --git a/examples/leptos/.vscode/settings.json b/examples/leptos/.vscode/settings.json new file mode 100644 index 0000000..e6d9d21 --- /dev/null +++ b/examples/leptos/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "emmet.includeLanguages": { + "rust": "html" + } +} diff --git a/examples/leptos/Cargo.toml b/examples/leptos/Cargo.toml new file mode 100644 index 0000000..412b34f --- /dev/null +++ b/examples/leptos/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "test-tauri-events-ui" +version = "0.0.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[dependencies] +leptos = { version = "0.6", features = ["csr", "nightly"] } +wasm-bindgen = "0.2" +wasm-bindgen-futures = "0.4" +js-sys = "0.3" +serde = { version = "1", features = ["derive"] } +serde-wasm-bindgen = "0.6" +console_error_panic_hook = "0.1.7" +tauri-sys = { path = "../tauri-sys", features = ["all"] } +futures = "0.3" +tracing = "0.1.40" +tracing-subscriber = "0.3.18" +tracing-web = "0.1.3" + +[workspace] +members = ["src-tauri"] diff --git a/examples/leptos/README.md b/examples/leptos/README.md new file mode 100644 index 0000000..77812aa --- /dev/null +++ b/examples/leptos/README.md @@ -0,0 +1,7 @@ +# Tauri + Leptos + +This template should help get you started developing with Tauri and Leptos. + +## Recommended IDE Setup + +[VS Code](https://code.visualstudio.com/) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer). diff --git a/examples/leptos/Trunk.toml b/examples/leptos/Trunk.toml new file mode 100644 index 0000000..27a1f84 --- /dev/null +++ b/examples/leptos/Trunk.toml @@ -0,0 +1,10 @@ +[build] +target = "./index.html" + +[watch] +ignore = ["./src-tauri"] + +[serve] +address = "127.0.0.1" +port = 1420 +open = false diff --git a/examples/leptos/index.html b/examples/leptos/index.html new file mode 100644 index 0000000..ae0bf14 --- /dev/null +++ b/examples/leptos/index.html @@ -0,0 +1,11 @@ + + + + + Tauri + Leptos App + + + + + + diff --git a/examples/leptos/public/leptos.svg b/examples/leptos/public/leptos.svg new file mode 100644 index 0000000..7fc2154 --- /dev/null +++ b/examples/leptos/public/leptos.svg @@ -0,0 +1,64 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/examples/leptos/public/tauri.svg b/examples/leptos/public/tauri.svg new file mode 100644 index 0000000..31b62c9 --- /dev/null +++ b/examples/leptos/public/tauri.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/examples/leptos/src-tauri/.gitignore b/examples/leptos/src-tauri/.gitignore new file mode 100644 index 0000000..b21bd68 --- /dev/null +++ b/examples/leptos/src-tauri/.gitignore @@ -0,0 +1,7 @@ +# Generated by Cargo +# will have compiled files and executables +/target/ + +# Generated by Tauri +# will have schema files for capabilities auto-completion +/gen/schemas diff --git a/examples/leptos/src-tauri/Cargo.toml b/examples/leptos/src-tauri/Cargo.toml new file mode 100644 index 0000000..395cc0c --- /dev/null +++ b/examples/leptos/src-tauri/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "test-tauri-events" +version = "0.0.0" +description = "A Tauri App" +authors = ["you"] +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[build-dependencies] +# tauri-build = { version = "2.0.0-beta", features = [] } +tauri-build = { path = "../../tauri/core/tauri-build", features = [] } + +[dependencies] +tauri = { path = "../../tauri/core/tauri", features = [] } +tauri-plugin-shell = { path = "../../plugins-workspace/plugins/shell" } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +tracing = "0.1.40" +tracing-subscriber = "0.3.18" diff --git a/examples/leptos/src-tauri/build.rs b/examples/leptos/src-tauri/build.rs new file mode 100644 index 0000000..d860e1e --- /dev/null +++ b/examples/leptos/src-tauri/build.rs @@ -0,0 +1,3 @@ +fn main() { + tauri_build::build() +} diff --git a/examples/leptos/src-tauri/capabilities/default.json b/examples/leptos/src-tauri/capabilities/default.json new file mode 100644 index 0000000..1914afc --- /dev/null +++ b/examples/leptos/src-tauri/capabilities/default.json @@ -0,0 +1,17 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "default", + "description": "Capability for the main window", + "windows": ["main"], + "permissions": [ + "core:path:default", + "core:event:default", + "core:window:default", + "core:app:default", + "core:image:default", + "core:resources:default", + "core:menu:default", + "core:tray:default", + "shell:allow-open" + ] +} diff --git a/examples/leptos/src-tauri/icons/128x128.png b/examples/leptos/src-tauri/icons/128x128.png new file mode 100644 index 0000000..6be5e50 Binary files /dev/null and b/examples/leptos/src-tauri/icons/128x128.png differ diff --git a/examples/leptos/src-tauri/icons/128x128@2x.png b/examples/leptos/src-tauri/icons/128x128@2x.png new file mode 100644 index 0000000..e81bece Binary files /dev/null and b/examples/leptos/src-tauri/icons/128x128@2x.png differ diff --git a/examples/leptos/src-tauri/icons/32x32.png b/examples/leptos/src-tauri/icons/32x32.png new file mode 100644 index 0000000..a437dd5 Binary files /dev/null and b/examples/leptos/src-tauri/icons/32x32.png differ diff --git a/examples/leptos/src-tauri/icons/Square107x107Logo.png b/examples/leptos/src-tauri/icons/Square107x107Logo.png new file mode 100644 index 0000000..0ca4f27 Binary files /dev/null and b/examples/leptos/src-tauri/icons/Square107x107Logo.png differ diff --git a/examples/leptos/src-tauri/icons/Square142x142Logo.png b/examples/leptos/src-tauri/icons/Square142x142Logo.png new file mode 100644 index 0000000..b81f820 Binary files /dev/null and b/examples/leptos/src-tauri/icons/Square142x142Logo.png differ diff --git a/examples/leptos/src-tauri/icons/Square150x150Logo.png b/examples/leptos/src-tauri/icons/Square150x150Logo.png new file mode 100644 index 0000000..624c7bf Binary files /dev/null and b/examples/leptos/src-tauri/icons/Square150x150Logo.png differ diff --git a/examples/leptos/src-tauri/icons/Square284x284Logo.png b/examples/leptos/src-tauri/icons/Square284x284Logo.png new file mode 100644 index 0000000..c021d2b Binary files /dev/null and b/examples/leptos/src-tauri/icons/Square284x284Logo.png differ diff --git a/examples/leptos/src-tauri/icons/Square30x30Logo.png b/examples/leptos/src-tauri/icons/Square30x30Logo.png new file mode 100644 index 0000000..6219700 Binary files /dev/null and b/examples/leptos/src-tauri/icons/Square30x30Logo.png differ diff --git a/examples/leptos/src-tauri/icons/Square310x310Logo.png b/examples/leptos/src-tauri/icons/Square310x310Logo.png new file mode 100644 index 0000000..f9bc048 Binary files /dev/null and b/examples/leptos/src-tauri/icons/Square310x310Logo.png differ diff --git a/examples/leptos/src-tauri/icons/Square44x44Logo.png b/examples/leptos/src-tauri/icons/Square44x44Logo.png new file mode 100644 index 0000000..d5fbfb2 Binary files /dev/null and b/examples/leptos/src-tauri/icons/Square44x44Logo.png differ diff --git a/examples/leptos/src-tauri/icons/Square71x71Logo.png b/examples/leptos/src-tauri/icons/Square71x71Logo.png new file mode 100644 index 0000000..63440d7 Binary files /dev/null and b/examples/leptos/src-tauri/icons/Square71x71Logo.png differ diff --git a/examples/leptos/src-tauri/icons/Square89x89Logo.png b/examples/leptos/src-tauri/icons/Square89x89Logo.png new file mode 100644 index 0000000..f3f705a Binary files /dev/null and b/examples/leptos/src-tauri/icons/Square89x89Logo.png differ diff --git a/examples/leptos/src-tauri/icons/StoreLogo.png b/examples/leptos/src-tauri/icons/StoreLogo.png new file mode 100644 index 0000000..4556388 Binary files /dev/null and b/examples/leptos/src-tauri/icons/StoreLogo.png differ diff --git a/examples/leptos/src-tauri/icons/icon.icns b/examples/leptos/src-tauri/icons/icon.icns new file mode 100644 index 0000000..12a5bce Binary files /dev/null and b/examples/leptos/src-tauri/icons/icon.icns differ diff --git a/examples/leptos/src-tauri/icons/icon.ico b/examples/leptos/src-tauri/icons/icon.ico new file mode 100644 index 0000000..b3636e4 Binary files /dev/null and b/examples/leptos/src-tauri/icons/icon.ico differ diff --git a/examples/leptos/src-tauri/icons/icon.png b/examples/leptos/src-tauri/icons/icon.png new file mode 100644 index 0000000..e1cd261 Binary files /dev/null and b/examples/leptos/src-tauri/icons/icon.png differ diff --git a/examples/leptos/src-tauri/src/main.rs b/examples/leptos/src-tauri/src/main.rs new file mode 100644 index 0000000..9791f93 --- /dev/null +++ b/examples/leptos/src-tauri/src/main.rs @@ -0,0 +1,41 @@ +// Prevents additional console window on Windows in release, DO NOT REMOVE!! +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +use tauri::{Emitter, Manager}; + +fn main() { + logging::enable(); + tauri::Builder::default() + .invoke_handler(tauri::generate_handler![trigger_listen_events,]) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} + +#[tauri::command] +fn trigger_listen_events(app: tauri::AppHandle) { + tracing::debug!("trigger_listen_event"); + std::thread::spawn({ + move || { + for i in 1..=100 { + app.emit("event::listen", i).unwrap(); + std::thread::sleep(std::time::Duration::from_millis(500)); + } + } + }); +} + +mod logging { + use tracing_subscriber::{filter::LevelFilter, fmt, prelude::*, Layer, Registry}; + + const MAX_LOG_LEVEL: LevelFilter = LevelFilter::DEBUG; + + pub fn enable() { + let console_logger = fmt::layer() + .with_writer(std::io::stdout) + .pretty() + .with_filter(MAX_LOG_LEVEL); + + let subscriber = Registry::default().with(console_logger); + tracing::subscriber::set_global_default(subscriber).unwrap(); + } +} diff --git a/examples/leptos/src-tauri/tauri.conf.json b/examples/leptos/src-tauri/tauri.conf.json new file mode 100644 index 0000000..2a5bdb4 --- /dev/null +++ b/examples/leptos/src-tauri/tauri.conf.json @@ -0,0 +1,35 @@ +{ + "productName": "test-tauri-events", + "version": "0.0.0", + "identifier": "com.tauri.dev", + "build": { + "beforeDevCommand": "trunk serve", + "devUrl": "http://localhost:1420", + "beforeBuildCommand": "trunk build", + "frontendDist": "../dist" + }, + "app": { + "withGlobalTauri": true, + "windows": [ + { + "title": "test-tauri-events", + "width": 800, + "height": 600 + } + ], + "security": { + "csp": null + } + }, + "bundle": { + "active": true, + "targets": "all", + "icon": [ + "icons/32x32.png", + "icons/128x128.png", + "icons/128x128@2x.png", + "icons/icon.icns", + "icons/icon.ico" + ] + } +} diff --git a/examples/leptos/src/app.rs b/examples/leptos/src/app.rs new file mode 100644 index 0000000..6b397aa --- /dev/null +++ b/examples/leptos/src/app.rs @@ -0,0 +1,517 @@ +use futures::stream::StreamExt; +use leptos::{ev::MouseEvent, *}; +use std::rc::Rc; + +#[component] +pub fn App() -> impl IntoView { + view! { +
+
+

"core"

+ +
+ +
+

"events"

+ +
+ +
+

"window"

+ +
+ +
+

"menu"

+ +
+
+ } +} + +#[component] +fn Core() -> impl IntoView { + let (convert_path, set_convert_path) = create_signal("".to_string()); + let (converted_path, set_converted_path) = create_signal("".to_string()); + + let do_convert_path = move |_| { + let converted = tauri_sys::core::convert_file_src(convert_path()); + set_converted_path(converted); + }; + + view! { +
+
+ + +
+
{converted_path}
+
+ } +} + +#[component] +fn Events() -> impl IntoView { + let (listen_event, set_listen_event) = create_signal(None); + let (emit_count, set_emit_count) = create_signal(0); + + spawn_local(async move { + let mut listener = tauri_sys::event::listen::("event::listen") + .await + .unwrap(); + + while let Some(event) = listener.next().await { + tracing::debug!(?event); + let tauri_sys::event::Event { + event: _, + id: _, + payload, + } = event; + set_listen_event.set(Some(payload)); + } + }); + + spawn_local(async move { + let mut listener = tauri_sys::event::listen::("event::emit") + .await + .unwrap(); + + while let Some(event) = listener.next().await { + tracing::debug!(?event); + let tauri_sys::event::Event { + event: _, + id: _, + payload, + } = event; + set_emit_count.set(payload); + } + }); + + let trigger_listen_events = move |_| { + spawn_local(async move { + tauri_sys::core::invoke::<()>("trigger_listen_events", &()).await; + }); + }; + + let trigger_emit_event = move |_| { + spawn_local(async move { + tauri_sys::event::emit("event::emit", &emit_count.with_untracked(|n| n + 1)) + .await + .unwrap(); + }); + }; + + view! { +
+
+ +
+ "Last listen event: " + {move || listen_event()} +
+
+ +
+ +
+ "Events emitted: " + {move || emit_count()} +
+
+
+ } +} + +#[component] +fn Window() -> impl IntoView { + view! { +
+
+

"Windows"

+ +
+ +
+

"Monitors"

+ +
+ +
+

"Events"

+ +
+
+ } +} + +#[component] +fn WindowWindows() -> impl IntoView { + let current_window = create_action(|_| async move { tauri_sys::window::get_current() }); + let all_windows = create_action(|_| async move { tauri_sys::window::get_all() }); + + let refresh = move |_| { + current_window.dispatch(()); + all_windows.dispatch(()); + }; + + current_window.dispatch(()); + all_windows.dispatch(()); + + view! { +
+
+
"Current window:"
+ {move || { + current_window + .value() + .with(|window| match window { + None => "Loading".to_string(), + Some(window) => window.label().clone(), + }) + }} + +
+
+
"All windows:"
+ {move || { + all_windows + .value() + .with(|windows| match windows { + None => "Loading".to_string(), + Some(windows) => { + let out = windows + .iter() + .map(|window| { window.label().clone() }) + .collect::>() + .join(", "); + format!("[{out}]") + } + }) + }} + +
+ +
+ } +} + +#[component] +fn WindowMonitors() -> impl IntoView { + let current_monitor = + create_action(|_| async move { tauri_sys::window::current_monitor().await }); + + let primary_monitor = + create_action(|_| async move { tauri_sys::window::primary_monitor().await }); + + let available_monitors = + create_action(|_| async move { tauri_sys::window::available_monitors().await }); + + let monitor_from_point = create_action(|(x, y): &(isize, isize)| { + let x = x.clone(); + let y = y.clone(); + async move { tauri_sys::window::monitor_from_point(x, y).await } + }); + + // let cursor_position = + // create_action(|_| async move { tauri_sys::window::cursor_position().await }); + + let refresh = move |_| { + current_monitor.dispatch(()); + primary_monitor.dispatch(()); + available_monitors.dispatch(()); + }; + + let oninput_monitor_from_point = move |e| { + let value = event_target_value(&e); + let Some((x, y)) = value.split_once(',') else { + return; + }; + + let Ok(x) = x.parse::() else { + return; + }; + + let Ok(y) = y.parse::() else { + return; + }; + + monitor_from_point.dispatch((x, y)); + }; + + current_monitor.dispatch(()); + primary_monitor.dispatch(()); + available_monitors.dispatch(()); + + view! { +
+
+
+
"Current monitor:"
+ {move || { + current_monitor + .value() + .with(|monitor| match monitor { + None => "Loading".into_view(), + Some(Some(monitor)) => view! { }.into_view(), + Some(None) => "Could not detect monitor.".into_view(), + }) + }} + +
+
+
"Primary monitor:"
+ {move || { + primary_monitor + .value() + .with(|monitor| match monitor { + None => "Loading".into_view(), + Some(Some(monitor)) => view! { }.into_view(), + Some(None) => "Could not detect monitor.".into_view(), + }) + }} + +
+
+
"Available monitors:"
+ {move || { + available_monitors + .value() + .with(|monitors| match monitors { + None => "Loading".into_view(), + Some(monitors) => { + view! { + {monitors + .iter() + .map(|monitor| view! { }) + .collect::>()} + } + .into_view() + } + }) + }} + +
+ +
+
+ +
+ {move || { + monitor_from_point + .value() + .with(|monitor| match monitor { + None => "Enter an `x, y` coordinate.".into_view(), + Some(Some(monitor)) => view! { }.into_view(), + Some(None) => "Could not detect monitor.".into_view(), + }) + }} + +
+
+ +
+ // {move || { + // cursor_position + // .value() + // .with(|position| { + // position + // .as_ref() + // .map(|position| { + // view! { + // {position.x()} + // ", " + // {position.y()} + // } + // }) + // }) + // }} +
"Cursor position: "
+
+ // on:mousemove=move |_| cursor_position.dispatch(()) + "TODO (See https://github.com/tauri-apps/tauri/issues/10340)" +
+
+
+ } +} + +#[component] +fn WindowEvents() -> impl IntoView { + use tauri_sys::window::{DragDropEvent, DragDropPayload, DragOverPayload}; + + let (count, set_count) = create_signal(0); + let increment_count = create_action(|count: &usize| { + let count = count.clone(); + let window = tauri_sys::window::get_current(); + async move { + web_sys::console::debug_1(&"0".into()); + window.emit("count", count).await.unwrap(); + } + }); + + let (drag_drop, set_drag_drop) = create_signal(().into_view()); + + spawn_local(async move { + let mut window = tauri_sys::window::get_current(); + let mut listener = window.listen::("count").await.unwrap(); + while let Some(event) = listener.next().await { + set_count(event.payload); + } + }); + + spawn_local(async move { + let window = tauri_sys::window::get_current(); + let mut listener = window.on_drag_drop_event().await.unwrap(); + while let Some(event) = listener.next().await { + match event.payload { + DragDropEvent::Enter(payload) => { + let out = view! { +
+ "Enter" +
+ "Paths: [" + {payload + .paths() + .iter() + .map(|path| path.to_string_lossy().to_string()) + .collect::>() + .join(", ")} "]" +
+
+ "Position: " {payload.position().x()} ", " {payload.position().y()} +
+
+ }; + + set_drag_drop(out.into_view()); + } + DragDropEvent::Over(payload) => { + let out = view! { +
+ "Over" +
+ "Position: " {payload.position().x()} ", " {payload.position().y()} +
+
+ }; + + set_drag_drop(out.into_view()); + } + DragDropEvent::Drop(payload) => { + let out = view! { +
+ "Drop" +
+ "Paths: [" + {payload + .paths() + .iter() + .map(|path| path.to_string_lossy().to_string()) + .collect::>() + .join(", ")} "]" +
+
+ "Position: " {payload.position().x()} ", " {payload.position().y()} +
+
+ }; + + set_drag_drop(out.into_view()); + } + DragDropEvent::Leave => { + let out = view! { "Leave" }; + set_drag_drop(out.into_view()); + } + } + } + }); + + view! { +
+
+ "Count: " {count} + +
+ +
+

"Drag drop event"

+
{drag_drop}
+
+
+ } +} + +#[component] +fn Monitor<'a>(monitor: &'a tauri_sys::window::Monitor) -> impl IntoView { + view! { +
+
"Name: " {monitor.name().clone()}
+
"Size: " {monitor.size().width()} " x " {monitor.size().height()}
+
"Position: " {monitor.position().x()} ", " {monitor.position().y()}
+
"Scale: " {monitor.scale_factor()}
+
+ } +} + +#[component] +fn Menu() -> impl IntoView { + let (event, set_event) = create_signal::>(None); + let menu = create_local_resource( + || (), + move |_| async move { + let menu = tauri_sys::menu::Menu::with_id("tauri-sys-menu").await; + let mut item_open = tauri_sys::menu::item::MenuItem::with_id("Open", "open").await; + let mut item_close = tauri_sys::menu::item::MenuItem::with_id("Close", "close").await; + menu.append_item(&item_open).await.unwrap(); + menu.append_item(&item_close).await.unwrap(); + + spawn_local(async move { + let mut listener_item_open = item_open.listen().fuse(); + let mut listener_item_close = item_close.listen().fuse(); + + loop { + futures::select! { + event = listener_item_open.next() => match event{ + None => continue, + Some(event) => set_event(Some((*event).clone())), + }, + event = listener_item_close.next() => match event{ + None => continue, + Some(event) => set_event(Some((*event).clone())), + }, + } + } + }); + + Rc::new(menu) + }, + ); + + let default_menu = move |e: MouseEvent| { + spawn_local(async move { + let menu = tauri_sys::menu::Menu::default().await; + }); + }; + + let open_menu = move |e: MouseEvent| { + let menu = menu.get().unwrap(); + spawn_local(async move { + menu.popup().await.unwrap(); + }); + }; + + view! { +
+ {event} +
+ } +} diff --git a/examples/leptos/src/main.rs b/examples/leptos/src/main.rs new file mode 100644 index 0000000..edee634 --- /dev/null +++ b/examples/leptos/src/main.rs @@ -0,0 +1,33 @@ +mod app; + +use app::*; +use leptos::*; + +fn main() { + #[cfg(debug_assertions)] + tracing::enable(); + console_error_panic_hook::set_once(); + mount_to_body(|| { + view! { } + }) +} + +#[cfg(debug_assertions)] +mod tracing { + use tracing::level_filters::LevelFilter; + use tracing_subscriber::prelude::*; + use tracing_web::MakeConsoleWriter; + + const MAX_LOG_LEVEL: LevelFilter = LevelFilter::DEBUG; + + pub fn enable() { + let fmt_layer = tracing_subscriber::fmt::layer() + .with_ansi(false) // Only partially supported across browsers + .pretty() + .without_time() + .with_writer(MakeConsoleWriter) // write events to the console + .with_filter(MAX_LOG_LEVEL); + + tracing_subscriber::registry().with(fmt_layer).init(); + } +} diff --git a/examples/leptos/styles.css b/examples/leptos/styles.css new file mode 100644 index 0000000..9e7814c --- /dev/null +++ b/examples/leptos/styles.css @@ -0,0 +1,112 @@ +.logo.leptos:hover { + filter: drop-shadow(0 0 2em #a82e20); +} +:root { + font-family: Inter, Avenir, Helvetica, Arial, sans-serif; + font-size: 16px; + line-height: 24px; + font-weight: 400; + + color: #0f0f0f; + background-color: #f6f6f6; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-text-size-adjust: 100%; +} + +.container { + margin: 0; + padding-top: 10vh; + display: flex; + flex-direction: column; + justify-content: center; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: 0.75s; +} + +.logo.tauri:hover { + filter: drop-shadow(0 0 2em #24c8db); +} + +.row { + display: flex; + justify-content: center; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} + +a:hover { + color: #535bf2; +} + +h1 { + text-align: center; +} + +input, +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + color: #0f0f0f; + background-color: #ffffff; + transition: border-color 0.25s; + box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2); +} + +button { + cursor: pointer; +} + +button:hover { + border-color: #396cd8; +} +button:active { + border-color: #396cd8; + background-color: #e8e8e8; +} + +input, +button { + outline: none; +} + +#greet-input { + margin-right: 5px; +} + +@media (prefers-color-scheme: dark) { + :root { + color: #f6f6f6; + background-color: #2f2f2f; + } + + a:hover { + color: #24c8db; + } + + input, + button { + color: #ffffff; + background-color: #0f0f0f98; + } + button:active { + background-color: #0f0f0f69; + } +} diff --git a/src/core.js b/src/core.js index 18db3e2..d45de3d 100644 --- a/src/core.js +++ b/src/core.js @@ -1,4 +1,7 @@ // tauri/tooling/api/src/core.ts +function transformCallback(callback, once = false) { + return window.__TAURI_INTERNALS__.transformCallback(callback, once) +} async function invoke(cmd, args = {}) { // NB: `options` ignored as not used here. return window.__TAURI_INTERNALS__.invoke(cmd, args) @@ -8,5 +11,6 @@ function convertFileSrc(filePath, protocol = 'asset') { } export { invoke, - convertFileSrc + convertFileSrc, + transformCallback, } diff --git a/src/core.rs b/src/core.rs index b2d8555..8a99275 100644 --- a/src/core.rs +++ b/src/core.rs @@ -1,8 +1,10 @@ //! Common functionality -use std::path::PathBuf; +use std::collections::HashMap; -use serde::{de::DeserializeOwned, Serialize}; +use futures::{channel::mpsc, future::FusedFuture, Stream, StreamExt}; +use serde::{de::DeserializeOwned, ser::SerializeStruct, Serialize}; use serde_wasm_bindgen as swb; +use wasm_bindgen::{prelude::Closure, JsValue}; pub async fn invoke(command: &str, args: impl Serialize) -> T where @@ -38,8 +40,62 @@ pub fn convert_file_src_with_protocol( .unwrap() } +// TODO: Could cause memory leak because handler is never released. +#[derive(Debug)] +pub struct Channel { + id: usize, + rx: mpsc::UnboundedReceiver, +} + +impl Channel { + pub fn new() -> Self + where + T: DeserializeOwned + 'static, + { + let (tx, rx) = mpsc::unbounded::(); + let closure = Closure::::new(move |raw| { + let _ = tx.unbounded_send(serde_wasm_bindgen::from_value(raw).unwrap()); + }); + + let id = inner::transform_callback(&closure, false); + closure.forget(); + + Channel { id, rx } + } + + pub fn id(&self) -> usize { + self.id + } +} + +impl Serialize for Channel { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let mut map = serializer.serialize_struct("Channel", 2)?; + map.serialize_field("__TAURI_CHANNEL_MARKER__", &true)?; + map.serialize_field("id", &self.id)?; + map.end() + } +} + +impl Stream for Channel { + type Item = T; + + fn poll_next( + mut self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + self.rx.poll_next_unpin(cx) + } +} + mod inner { - use wasm_bindgen::{prelude::wasm_bindgen, JsValue}; + use wasm_bindgen::{ + prelude::{wasm_bindgen, Closure}, + JsValue, + }; #[wasm_bindgen(module = "/src/core.js")] extern "C" { @@ -48,5 +104,7 @@ mod inner { pub async fn invoke_result(cmd: &str, args: JsValue) -> Result; #[wasm_bindgen(js_name = "convertFileSrc")] pub fn convert_file_src(filePath: &str, protocol: &str) -> JsValue; + #[wasm_bindgen(js_name = "transformCallback")] + pub fn transform_callback(callback: &Closure, once: bool) -> usize; } } diff --git a/src/lib.rs b/src/lib.rs index 13f36a5..674a9ef 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -95,6 +95,9 @@ pub mod core; #[cfg(feature = "dpi")] pub mod dpi; +#[cfg(feature = "menu")] +pub mod menu; + #[cfg(feature = "window")] pub mod window; diff --git a/src/menu.js b/src/menu.js new file mode 100644 index 0000000..e69de29 diff --git a/src/menu.rs b/src/menu.rs new file mode 100644 index 0000000..ec2da01 --- /dev/null +++ b/src/menu.rs @@ -0,0 +1,315 @@ +//! # See also +//! + `tauri::menu` +use crate::{core, window}; +use serde::{ser::SerializeStruct, Deserialize, Serialize}; +use std::collections::HashMap; + +type Rid = usize; + +#[derive(Debug)] +pub struct Menu { + rid: Rid, + id: MenuId, + channel: Option>>, +} + +impl Menu { + pub async fn with_id(id: impl Into) -> Self { + #[derive(Serialize)] + struct Args { + kind: String, + options: MenuOptions, + handler: ChannelId, + } + + let options = MenuOptions { + id: Some(id.into()), + }; + + let channel = core::Channel::new(); + + let (rid, id) = core::invoke::<(Rid, String)>( + "plugin:menu|new", + Args { + kind: ItemKind::Menu.as_str().to_string(), + options, + handler: ChannelId::from(&channel), + }, + ) + .await; + + Self { + rid, + id: id.into(), + channel: Some(channel), + } + } + + pub async fn default() -> Self { + let (rid, id) = core::invoke::<(Rid, String)>("plugin:menu|create_default", ()).await; + Self { + rid, + id: id.into(), + channel: None, + } + } +} + +impl Menu { + pub fn rid(&self) -> Rid { + self.rid + } + + pub fn kind() -> &'static str { + ItemKind::Menu.as_str() + } +} + +impl Menu { + pub async fn append_item(&self, item: &item::MenuItem) -> Result<(), ()> { + core::invoke_result( + "plugin:menu|append", + AppendItemArgs { + rid: self.rid, + kind: Self::kind().to_string(), + items: vec![(item.rid(), item::MenuItem::kind().to_string())], + }, + ) + .await + } + + /// Popup this menu as a context menu on the specified window. + /// If the position, is provided, it is relative to the window's top-left corner. + pub async fn popup(&self) -> Result<(), ()> { + #[derive(Serialize)] + struct Position { + x: isize, + y: isize, + } + + #[derive(Serialize)] + struct Args { + rid: Rid, + kind: String, + window: Option, + at: Option>, + } + + core::invoke_result( + "plugin:menu|popup", + Args { + rid: self.rid, + kind: Self::kind().to_string(), + window: None, + at: None, + }, + ) + .await + } +} + +impl Menu { + pub fn listen(&mut self) -> Option<&mut core::Channel>> { + self.channel.as_mut() + } +} + +#[derive(Serialize)] +struct AppendItemArgs { + rid: Rid, + kind: String, + items: Vec<(Rid, String)>, +} + +#[derive(Serialize, Clone, derive_more::From, Debug)] +#[serde(transparent)] +pub struct MenuId(pub String); + +impl From<&'static str> for MenuId { + fn from(value: &'static str) -> Self { + Self(value.to_string()) + } +} + +#[derive(derive_more::Deref, Deserialize, Debug)] +pub struct Message { + id: usize, + + #[deref] + message: T, +} + +impl Message { + pub fn id(&self) -> usize { + self.id + } +} + +#[derive(Serialize)] +struct MenuOptions { + id: Option, +} + +enum ItemKind { + MenuItem, + Predefined, + Check, + Icon, + Submenu, + Menu, +} + +impl ItemKind { + pub fn as_str(&self) -> &'static str { + match self { + Self::MenuItem => "MenuItem", + Self::Predefined => "Predefined", + Self::Check => "Check", + Self::Icon => "Icon", + Self::Submenu => "Submenu", + Self::Menu => "Menu", + } + } +} + +struct ChannelId { + id: usize, +} + +impl ChannelId { + pub fn from(channel: &core::Channel) -> Self { + Self { id: channel.id() } + } +} + +impl Serialize for ChannelId { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let mut map = serializer.serialize_struct("ChannelId", 2)?; + map.serialize_field("__TAURI_CHANNEL_MARKER__", &true)?; + map.serialize_field("id", &self.id)?; + map.end() + } +} + +pub mod item { + use super::{ChannelId, ItemKind, MenuId, Message, Rid}; + use crate::core; + use futures::{Stream, StreamExt}; + use serde::Serialize; + + pub struct MenuItem { + rid: Rid, + id: MenuId, + channel: core::Channel>, + } + + impl MenuItem { + pub async fn with_id(text: impl Into, id: impl Into) -> Self { + let mut options = MenuItemOptions::new(text); + options.set_id(id); + + Self::with_options(options).await + } + + pub async fn with_options(options: MenuItemOptions) -> Self { + #[derive(Serialize)] + struct Args { + kind: String, + options: MenuItemOptions, + handler: ChannelId, + } + + let channel = core::Channel::new(); + + let (rid, id) = core::invoke::<(Rid, String)>( + "plugin:menu|new", + Args { + kind: ItemKind::MenuItem.as_str().to_string(), + options, + handler: ChannelId::from(&channel), + }, + ) + .await; + + Self { + rid, + id: id.into(), + channel, + } + } + } + + impl MenuItem { + pub fn rid(&self) -> Rid { + self.rid + } + + pub fn kind() -> &'static str { + ItemKind::MenuItem.as_str() + } + } + + impl MenuItem { + // pub fn listen(&mut self) -> impl Stream> { + // self.channel.map(|message| message.message) + // } + + pub fn listen(&mut self) -> &mut core::Channel> { + &mut self.channel + } + } + + #[derive(Serialize)] + pub struct MenuItemOptions { + /// Specify an id to use for the new menu item. + id: Option, + + /// The text of the new menu item. + text: String, + + /// Whether the new menu item is enabled or not. + enabled: Option, + + /// Specify an accelerator for the new menu item. + accelerator: Option, + } + + impl MenuItemOptions { + pub fn new(text: impl Into) -> Self { + Self { + id: None, + text: text.into(), + enabled: None, + accelerator: None, + } + } + + pub fn set_id(&mut self, id: impl Into) -> &mut Self { + let _ = self.id.insert(id.into()); + self + } + + pub fn set_enabled(&mut self, enabled: bool) -> &mut Self { + let _ = self.enabled.insert(enabled); + self + } + + pub fn set_accelerator(&mut self, accelerator: impl Into) -> &mut Self { + let _ = self.accelerator.insert(accelerator.into()); + self + } + } +} + +mod inner { + use wasm_bindgen::{prelude::wasm_bindgen, JsValue}; + + #[wasm_bindgen(module = "/src/menu.js")] + extern "C" { + #[wasm_bindgen(js_name = "getCurrent")] + pub fn get_current() -> JsValue; + } +} diff --git a/src/window.rs b/src/window.rs index 56935ba..5de44a4 100644 --- a/src/window.rs +++ b/src/window.rs @@ -1,4 +1,4 @@ -//! Provides APIs to create windows, communicate with other windows and manipulate the current window. +//! Provides APIs to create windows, communicate with other windows, and manipulate the current window. //! //! ## Window events //! Events can be listened to using [`Window::listen`]. @@ -90,8 +90,8 @@ impl Stream for DragDropListen { } } -#[derive(Deserialize)] -struct WindowLabel { +#[derive(Serialize, Deserialize)] +pub(crate) struct WindowLabel { label: String, }