feat: improve DX of events (#12)

* feat: improve DX of events

* Update global_shortcut.rs

* Update event.rs

* deploy docs to gh pages

* Delete rustdoc.yml

* add tests for global shortcut

* improve logs produced by tauri_log

* wip docs

* update docs

* move error to separate module

* feat: simplify functions returning array backed iterators

* rebase and cleanup

* fixes
This commit is contained in:
Jonas Kruckenberg 2022-11-19 20:33:06 +01:00 committed by GitHub
parent 300fe18d22
commit e28a0bb749
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 655 additions and 289 deletions

15
Cargo.lock generated
View file

@ -1030,6 +1030,18 @@ dependencies = [
"regex",
]
[[package]]
name = "gloo-timers"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fb7d06c1c8cc2a29bee7ec961009a0b2caa0793ee4900c2ffb348734ba1c8f9"
dependencies = [
"futures-channel",
"futures-core",
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "gobject-sys"
version = "0.15.10"
@ -3031,6 +3043,7 @@ dependencies = [
name = "tauri-sys"
version = "0.1.0"
dependencies = [
"futures",
"js-sys",
"log",
"semver 1.0.14",
@ -3061,6 +3074,8 @@ version = "0.0.0"
dependencies = [
"anyhow",
"console_error_panic_hook",
"futures",
"gloo-timers",
"log",
"serde",
"sycamore",

View file

@ -6,6 +6,7 @@ version = "0.1.0"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
futures = { version = "0.3.25", optional = true }
js-sys = "0.3.59"
log = "0.4.17"
semver = {version = "1.0.14", optional = true, features = ["serde"]}
@ -28,16 +29,16 @@ all = ["app", "clipboard", "event", "mocks", "tauri", "window", "process", "dial
app = ["dep:semver"]
clipboard = []
dialog = []
event = []
event = ["dep:futures"]
global_shortcut = []
mocks = []
tauri = ["dep:url"]
window = []
process = []
os = []
notification = []
os = []
path = []
updater = []
process = []
tauri = ["dep:url"]
updater = ["dep:futures"]
window = ["dep:futures"]
[workspace]
members = ["examples/test", "examples/test/src-tauri"]

View file

@ -12,6 +12,8 @@ console_error_panic_hook = "0.1.7"
wasm-bindgen-futures = "0.4.32"
serde = { version = "1.0.147", features = ["derive"] }
log = { version = "0.4.17", features = ["serde"] }
futures = "0.3.25"
gloo-timers = { version = "0.2.4", features = ["futures"] }
[features]
ci = []

View file

@ -3,5 +3,6 @@
<head>
<meta charset="utf-8" />
<title>Tauri + Yew App</title>
<link data-trunk rel="css" href="./styles.css">
</head>
</html>

View file

@ -4,7 +4,7 @@
)]
use std::sync::atomic::{AtomicBool, Ordering};
use tauri::{Manager, State};
use tauri::{Manager, Runtime, State, Window};
use tauri_plugin_log::{LogTarget, LoggerBuilder};
struct Received(AtomicBool);
@ -14,6 +14,22 @@ fn verify_receive(emitted: State<Received>) -> bool {
emitted.0.load(Ordering::Relaxed)
}
#[tauri::command]
async fn emit_event<R: Runtime>(win: Window<R>) -> Result<(), ()> {
let _ = win.emit("rust-event-once", "Hello World from Rust!");
Ok(())
}
#[tauri::command]
async fn emit_event_5_times<R: Runtime>(win: Window<R>) -> Result<(), ()> {
for i in 0..5 {
let _ = win.emit("rust-event-listen", i);
}
Ok(())
}
#[tauri::command]
fn exit_with_error(e: &str) -> bool {
eprintln!("{}", e);
@ -35,12 +51,12 @@ fn main() {
tauri::Builder::default()
.plugin(log_plugin)
.invoke_handler(tauri::generate_handler![verify_receive, exit_with_error])
.invoke_handler(tauri::generate_handler![verify_receive, emit_event, emit_event_5_times, exit_with_error])
.setup(|app| {
app.manage(Received(AtomicBool::new(false)));
let app_handle = app.handle();
app.listen_global("foo", move |_| {
app.listen_global("javascript-event", move |_| {
app_handle
.state::<Received>()
.0

View file

@ -53,7 +53,7 @@ pub async fn pick_files() -> anyhow::Result<()> {
.await?;
ensure!(file.is_some());
ensure!(file.unwrap().len() > 1);
ensure!(file.unwrap().count() > 1);
Ok(())
}
@ -76,7 +76,7 @@ pub async fn pick_folders() -> anyhow::Result<()> {
.await?;
ensure!(file.is_some());
ensure!(file.unwrap().len() > 1);
ensure!(file.unwrap().count() > 1);
Ok(())
}

View file

@ -1,10 +1,38 @@
use anyhow::ensure;
use futures::StreamExt;
use tauri_sys::{event, tauri};
pub async fn emit() -> anyhow::Result<()> {
event::emit("foo", &"bar").await?;
event::emit("javascript-event", &"bar").await?;
ensure!(tauri::invoke::<_, bool>("verify_receive", &()).await?);
Ok(())
}
pub async fn listen() -> anyhow::Result<()> {
let events = event::listen::<u32>("rust-event-listen").await?;
tauri::invoke::<_, ()>("emit_event_5_times", &()).await?;
let events: Vec<u32> = events
.take(5)
.map(|e| e.payload)
.collect()
.await;
ensure!(events == vec![0, 1, 2, 3, 4]);
Ok(())
}
pub async fn once() -> anyhow::Result<()> {
// this causes enough delay for `once` to register it's event listener before the event gets triggered
wasm_bindgen_futures::spawn_local(async {
tauri::invoke::<_, ()>("emit_event", &()).await.unwrap();
});
let event = event::once::<String>("rust-event-once").await?;
ensure!(event.payload == "Hello World from Rust!");
Ok(())
}

View file

@ -0,0 +1,31 @@
use std::time::Duration;
use futures::StreamExt;
use tauri_sys::global_shortcut;
pub async fn register_all() -> anyhow::Result<()> {
let task = async {
let shortcuts = ["CommandOrControl+Shift+C", "Ctrl+Alt+F12"];
let streams = futures::future::try_join_all(shortcuts.map(|s| async move {
let stream = global_shortcut::register(s).await?;
anyhow::Ok(stream.map(move |_| s))
}))
.await?;
let mut events = futures::stream::select_all(streams);
while let Some(shortcut) = events.next().await {
log::debug!("Shortcut {} triggered", shortcut)
}
anyhow::Ok(())
};
let timeout = gloo_timers::future::sleep(Duration::from_secs(20));
futures::future::select(Box::pin(task), timeout).await;
Ok(())
}

View file

@ -6,6 +6,7 @@ mod notification;
mod os;
mod tauri_log;
mod window;
mod global_shortcut;
extern crate console_error_panic_hook;
use log::LevelFilter;
@ -39,7 +40,7 @@ where
}
#[component]
pub async fn Test<'a, G: Html, F>(cx: Scope<'a>, props: TestProps<'a, F>) -> View<G>
pub async fn TestInner<'a, G: Html, F>(cx: Scope<'a>, props: TestProps<'a, F>) -> View<G>
where
F: Future<Output = anyhow::Result<()>> + 'a,
{
@ -64,9 +65,30 @@ where
}
}
#[component]
pub fn Test<'a, G: Html, F>(cx: Scope<'a>, props: TestProps<'a, F>) -> View<G>
where
F: Future<Output = anyhow::Result<()>> + 'a,
{
let fallback = view! { cx,
tr {
td { code { (props.name.to_string()) } }
td {
span(class="loader") { "" }
}
}
};
view! { cx,
Suspense(fallback=fallback) {
TestInner(name=props.name, test=props.test)
}
}
}
#[cfg(not(feature = "ci"))]
#[component]
pub async fn InteractiveTest<'a, G: Html, F>(cx: Scope<'a>, props: TestProps<'a, F>) -> View<G>
pub fn InteractiveTest<'a, G: Html, F>(cx: Scope<'a>, props: TestProps<'a, F>) -> View<G>
where
F: Future<Output = anyhow::Result<()>> + 'a,
{
@ -81,19 +103,8 @@ where
(if *render_test.get() {
let test = test.take().unwrap();
let fallback = view! { cx,
tr {
td { code { (props.name.to_string()) } }
td {
"Running Test..."
}
}
};
view! { cx,
Suspense(fallback=fallback) {
Test(name=props.name, test=test)
}
Test(name=props.name, test=test)
}
} else {
view! { cx,
@ -148,12 +159,14 @@ fn main() {
view! { cx,
table {
tbody {
Suspense(fallback=view!{ cx, "Running Tests..." }) {
// Suspense(fallback=view!{ cx, "Running Tests..." }) {
Test(name="app::get_name",test=app::get_name())
Test(name="app::get_version",test=app::get_version())
Test(name="app::get_tauri_version",test=app::get_tauri_version())
Test(name="clipboard::read_text | clipboard::write_text",test=clipboard::test())
Test(name="event::emit",test=event::emit())
Test(name="event::listen",test=event::listen())
Test(name="event::once",test=event::once())
InteractiveTest(name="dialog::message",test=dialog::message())
InteractiveTest(name="dialog::ask",test=dialog::ask())
InteractiveTest(name="dialog::confirm",test=dialog::confirm())
@ -170,11 +183,12 @@ fn main() {
Test(name="notification::is_permission_granted",test=notification::is_permission_granted())
Test(name="notification::request_permission",test=notification::request_permission())
InteractiveTest(name="notification::show_notification",test=notification::show_notification())
InteractiveTest(name="global_shortcut::register_all",test=global_shortcut::register_all())
// Test(name="window::WebviewWindow::new",test=window::create_window())
Terminate
}
// }
}
}
}

View file

@ -6,6 +6,7 @@ use tauri_sys::tauri;
struct LogArgs {
level: Level,
message: String,
location: String,
file: Option<String>,
line: Option<u32>,
}
@ -56,6 +57,7 @@ impl log::Log for TauriLogger {
if self.enabled(record.metadata()) {
let args = LogArgs {
level: record.level().into(),
location: record.target().to_string(),
message: format!("{}", record.args()),
file: record.file().map(ToString::to_string),
line: record.line(),

15
examples/test/styles.css Normal file
View file

@ -0,0 +1,15 @@
.loader {
transform-origin: baseline;
display: inline-block;
box-sizing: border-box;
animation: rotation 1.3s linear infinite;
}
@keyframes rotation {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}

View file

@ -19,9 +19,11 @@
//! ```
//! It is recommended to allowlist only the APIs you use for optimal bundle size and security.
use js_sys::Array;
use serde::Serialize;
use std::path::{Path, PathBuf};
use crate::utils::ArrayIterator;
#[derive(Debug, Clone, Copy, Hash, Serialize)]
struct DialogFilter<'a> {
extensions: &'a [&'a str],
@ -162,12 +164,19 @@ impl<'a> FileDialogBuilder<'a> {
/// ```
///
/// Requires [`allowlist > dialog > open`](https://tauri.app/v1/api/config#dialogallowlistconfig.open) to be enabled.
pub async fn pick_files(&mut self) -> crate::Result<Option<Vec<PathBuf>>> {
pub async fn pick_files(&mut self) -> crate::Result<Option<impl Iterator<Item = PathBuf>>> {
self.multiple = true;
let raw = inner::open(serde_wasm_bindgen::to_value(&self)?).await?;
Ok(serde_wasm_bindgen::from_value(raw)?)
if let Ok(files) = Array::try_from(raw) {
let files =
ArrayIterator::new(files).map(|raw| serde_wasm_bindgen::from_value(raw).unwrap());
Ok(Some(files))
} else {
Ok(None)
}
}
/// Shows the dialog to select a single folder.
@ -206,13 +215,20 @@ impl<'a> FileDialogBuilder<'a> {
/// ```
///
/// Requires [`allowlist > dialog > open`](https://tauri.app/v1/api/config#dialogallowlistconfig.open) to be enabled.
pub async fn pick_folders(&mut self) -> crate::Result<Option<Vec<PathBuf>>> {
pub async fn pick_folders(&mut self) -> crate::Result<Option<impl Iterator<Item = PathBuf>>> {
self.directory = true;
self.multiple = true;
let raw = inner::open(serde_wasm_bindgen::to_value(&self)?).await?;
Ok(serde_wasm_bindgen::from_value(raw)?)
if let Ok(files) = Array::try_from(raw) {
let files =
ArrayIterator::new(files).map(|raw| serde_wasm_bindgen::from_value(raw).unwrap());
Ok(Some(files))
} else {
Ok(None)
}
}
/// Open a file/directory save dialog.

25
src/error.rs Normal file
View file

@ -0,0 +1,25 @@
use wasm_bindgen::JsValue;
#[derive(Clone, Eq, PartialEq, Debug, thiserror::Error)]
pub enum Error {
#[error("TODO.")]
Binding(String),
#[error("TODO.")]
Serde(String),
#[cfg(any(feature = "event", feature = "window"))]
#[error("TODO.")]
OneshotCanceled(#[from] futures::channel::oneshot::Canceled)
}
impl From<serde_wasm_bindgen::Error> for Error {
fn from(e: serde_wasm_bindgen::Error) -> Self {
Self::Serde(e.to_string())
}
}
impl From<JsValue> for Error {
fn from(e: JsValue) -> Self {
Self::Binding(format!("{:?}", e))
}
}

View file

@ -1,15 +1,20 @@
//! The event system allows you to emit events to the backend and listen to events from it.
use futures::{
channel::{mpsc, oneshot},
Future, FutureExt, Stream, StreamExt,
};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use std::fmt::Debug;
use wasm_bindgen::{prelude::Closure, JsValue};
#[derive(Debug, Clone, Hash, Deserialize)]
#[derive(Debug, Clone, PartialEq, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Event<T> {
/// Event name
pub event: String,
/// Event identifier used to unlisten
pub id: u32,
pub id: f32,
/// Event payload
pub payload: T,
/// The label of the window that emitted this event
@ -42,47 +47,69 @@ pub async fn emit<T: Serialize>(event: &str, payload: &T) -> crate::Result<()> {
}
/// Listen to an event from the backend.
///
/// The returned Future will automatically clean up it's underlying event listener when dropped, so no manual unlisten function needs to be called.
/// See [Differences to the JavaScript API](../index.html#differences-to-the-javascript-api) for details.
///
/// # Example
///
/// ```rust,no_run
/// use tauri_api::event::{emit, listen};
/// use tauri_api::event::listen;
/// use web_sys::console;
///
/// const unlisten = listen::<String>("error", |event| {
/// console::log_1(&format!("Got error in window {}, payload: {}", event.window_label, event.payload).into());
/// }).await;
/// let events = listen::<String>("error");
///
/// // you need to call unlisten if your handler goes out of scope e.g. the component is unmounted
/// unlisten();
/// while let Some(event) = events.next().await {
/// console::log_1(&format!("Got error in window {}, payload: {}", event.window_label, event.payload).into());
/// }
/// ```
///
/// @param event Event name. Must include only alphanumeric characters, `-`, `/`, `:` and `_`.
/// @param handler Event handler callback.
///
/// Note that removing the listener is required if your listener goes out of scope e.g. the component is unmounted.
#[inline(always)]
pub async fn listen<T, H>(event: &str, mut handler: H) -> crate::Result<impl FnOnce()>
pub async fn listen<T>(event: &str) -> crate::Result<impl Stream<Item = Event<T>>>
where
T: DeserializeOwned,
H: FnMut(Event<T>) + 'static,
T: DeserializeOwned + 'static,
{
let (tx, rx) = mpsc::unbounded::<Event<T>>();
let closure = Closure::<dyn FnMut(JsValue)>::new(move |raw| {
(handler)(serde_wasm_bindgen::from_value(raw).unwrap())
let _ = tx.unbounded_send(serde_wasm_bindgen::from_value(raw).unwrap());
});
let unlisten = inner::listen(event, &closure).await?;
closure.forget();
let unlisten = js_sys::Function::from(unlisten);
Ok(move || {
unlisten.call0(&wasm_bindgen::JsValue::NULL).unwrap();
Ok(Listen {
rx,
unlisten: js_sys::Function::from(unlisten),
})
}
pub(crate) struct Listen<T> {
pub rx: mpsc::UnboundedReceiver<T>,
pub unlisten: js_sys::Function,
}
impl<T> Drop for Listen<T> {
fn drop(&mut self) {
log::debug!("Calling unlisten for listen callback");
self.unlisten.call0(&wasm_bindgen::JsValue::NULL).unwrap();
}
}
impl<T> Stream for Listen<T> {
type Item = T;
fn poll_next(
mut self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Option<Self::Item>> {
self.rx.poll_next_unpin(cx)
}
}
/// Listen to an one-off event from the backend.
///
/// The returned Future will automatically clean up it's underlying event listener when dropped, so no manual unlisten function needs to be called.
/// See [Differences to the JavaScript API](../index.html#differences-to-the-javascript-api) for details.
///
/// # Example
///
/// ```rust,no_run
@ -95,35 +122,57 @@ where
/// logged_in: bool,
/// token: String
/// }
/// const unlisten = once::<LoadedPayload>("loaded", |event| {
/// console::log_1!(&format!("App is loaded, loggedIn: {}, token: {}", event.payload.logged_in, event.payload.token).into());
/// }).await;
///
/// // you need to call unlisten if your handler goes out of scope e.g. the component is unmounted
/// unlisten();
///
/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
/// const event = once::<LoadedPayload>("loaded").await?;
///
/// console::log_1!(&format!("App is loaded, loggedIn: {}, token: {}", event.payload.logged_in, event.payload.token).into());
/// # Ok(())
/// # }
/// ```
///
/// @param event Event name. Must include only alphanumeric characters, `-`, `/`, `:` and `_`.
///
/// Note that removing the listener is required if your listener goes out of scope e.g. the component is unmounted.
#[inline(always)]
pub async fn once<T, H>(event: &str, mut handler: H) -> crate::Result<impl FnOnce()>
pub async fn once<T>(event: &str) -> crate::Result<Event<T>>
where
T: DeserializeOwned,
H: FnMut(Event<T>) + 'static,
T: DeserializeOwned + 'static,
{
let closure = Closure::<dyn FnMut(JsValue)>::new(move |raw| {
(handler)(serde_wasm_bindgen::from_value(raw).unwrap())
let (tx, rx) = oneshot::channel::<Event<T>>();
let closure: Closure<dyn FnMut(JsValue)> = Closure::once(move |raw| {
let _ = tx.send(serde_wasm_bindgen::from_value(raw).unwrap());
});
let unlisten = inner::once(event, &closure).await?;
closure.forget();
let unlisten = js_sys::Function::from(unlisten);
Ok(move || {
unlisten.call0(&wasm_bindgen::JsValue::NULL).unwrap();
})
let fut = Once {
rx,
unlisten: js_sys::Function::from(unlisten),
};
fut.await
}
pub(crate) struct Once<T> {
pub rx: oneshot::Receiver<Event<T>>,
pub unlisten: js_sys::Function,
}
impl<T> Drop for Once<T> {
fn drop(&mut self) {
self.rx.close();
log::debug!("Calling unlisten for once callback");
self.unlisten.call0(&wasm_bindgen::JsValue::NULL).unwrap();
}
}
impl<T> Future for Once<T> {
type Output = crate::Result<Event<T>>;
fn poll(
mut self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Self::Output> {
self.rx.poll_unpin(cx).map_err(Into::into)
}
}
mod inner {

View file

@ -1,7 +1,31 @@
//! Register global shortcuts.
//!
//!
//! ## Differences to the JavaScript API
//!
//! ## `registerAll`
//!
//! ```rust,no_run
//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
//! let shortcuts = ["CommandOrControl+Shift+C", "Ctrl+Alt+F12"];
//!
//! let streams = futures::future::try_join_all(shortcuts.map(|s| async move {
//! let stream = global_shortcut::register(s).await?;
//!
//! anyhow::Ok(stream.map(move |_| s))
//! }))
//! .await?;
//!
//! let mut events = futures::stream::select_all(streams);
//!
//! while let Some(shortcut) = events.next().await {
//! log::debug!("Shortcut {} triggered", shortcut)
//! }
//! # Ok(())
//! # }
//! ```
//!
//! The APIs must be added to tauri.allowlist.globalShortcut in tauri.conf.json:
//!
//!
//! ```json
//! {
//! "tauri": {
@ -15,15 +39,16 @@
//! ```
//! It is recommended to allowlist only the APIs you use for optimal bundle size and security.
use futures::{channel::mpsc, Stream, StreamExt};
use wasm_bindgen::{prelude::Closure, JsValue};
/// Determines whether the given shortcut is registered by this application or not.
///
///
/// # Example
///
///
/// ```rust,no_run
/// use tauri_sys::global_shortcut::is_registered;
///
///
/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let registered = is_registered("CommandOrControl+P").await?;
/// # Ok(())
@ -36,107 +61,120 @@ pub async fn is_registered(shortcut: &str) -> crate::Result<bool> {
}
/// Register a global shortcut.
///
/// # Example
///
///
/// The returned Future will automatically clean up it's underlying event listener when dropped, so no manual unlisten function needs to be called.
/// See [Differences to the JavaScript API](../index.html#differences-to-the-javascript-api) for details.
///
/// # Examples
///
/// ```rust,no_run
/// use tauri_sys::global_shortcut::register;
/// use web_sys::console;
///
///
/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
/// register("CommandOrControl+Shift+C", |_| {
/// let events = register("CommandOrControl+Shift+C").await?;
///
/// while let Some(_) in events.next().await {
/// console::log_1(&"Shortcut triggered".into());
/// }).await?;
/// }
/// # Ok(())
/// # }
/// ```
pub async fn register<H>(shortcut: &str, mut handler: H) -> crate::Result<()>
where
H: FnMut(&str) + 'static,
{
let closure = Closure::<dyn FnMut(JsValue)>::new(move |raw: JsValue| {
let raw = raw.as_string().unwrap();
(handler)(raw.as_str())
pub async fn register(shortcut: &str) -> crate::Result<impl Stream<Item = ()>> {
let (tx, rx) = mpsc::unbounded();
let closure = Closure::<dyn FnMut(JsValue)>::new(move |_| {
let _ = tx.unbounded_send(());
});
inner::register(shortcut, &closure).await?;
closure.forget();
Ok(())
Ok(Listen {
shortcut: JsValue::from_str(shortcut),
rx,
})
}
struct Listen<T> {
pub shortcut: JsValue,
pub rx: mpsc::UnboundedReceiver<T>,
}
impl<T> Drop for Listen<T> {
fn drop(&mut self) {
log::debug!("Unregistering shortcut {:?}", self.shortcut);
inner::unregister(self.shortcut.clone());
}
}
impl<T> Stream for Listen<T> {
type Item = T;
fn poll_next(
mut self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Option<Self::Item>> {
self.rx.poll_next_unpin(cx)
}
}
/// Register a collection of global shortcuts.
///
///
/// # Example
///
///
/// ```rust,no_run
/// use tauri_sys::global_shortcut::register_all;
///
/// use tauri_sys::global_shortcut::register;
/// use web_sys::console;
///
/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let registered = register_all(["CommandOrControl+Shift+C", "Ctrl+Alt+F12"], |shortcut| {
/// let events = register_all(["CommandOrControl+Shift+C", "Ctrl+Alt+F12"]).await?;
///
/// while let Some(shortcut) in events.next().await {
/// console::log_1(&format!("Shortcut {} triggered", shortcut).into());
/// }).await?;
/// }
/// # Ok(())
/// # }
/// ```
pub async fn register_all<'a, I, H>(shortcuts: I, mut handler: H) -> crate::Result<()>
where
I: IntoIterator<Item = &'a str>,
H: FnMut(&str) + 'static,
{
let shortcuts = shortcuts.into_iter().map(JsValue::from_str).collect();
// pub async fn register_all<I>(shortcuts: impl IntoIterator<Item = &str>) -> crate::Result<impl Stream<Item = String>>
// {
// let shortcuts: Array = shortcuts.into_iter().map(JsValue::from_str).collect();
// let (tx, rx) = mpsc::unbounded::<String>();
let closure = Closure::<dyn FnMut(JsValue)>::new(move |raw: JsValue| {
let raw = raw.as_string().unwrap();
(handler)(raw.as_str())
});
// let closure = Closure::<dyn FnMut(JsValue)>::new(move |raw| {
// let _ = tx.unbounded_send(serde_wasm_bindgen::from_value(raw).unwrap());
// });
// inner::registerAll(shortcuts.clone(), &closure).await?;
// closure.forget();
inner::registerAll(shortcuts, &closure).await?;
// Ok(ListenAll { shortcuts, rx })
// }
closure.forget();
// struct ListenAll<T> {
// pub shortcuts: js_sys::Array,
// pub rx: mpsc::UnboundedReceiver<T>,
// }
Ok(())
}
// impl<T> Drop for ListenAll<T> {
// fn drop(&mut self) {
// for shortcut in self.shortcuts.iter() {
// inner::unregister(shortcut);
// }
// }
// }
/// Unregister a global shortcut.
///
/// # Example
///
/// ```rust,no_run
/// use tauri_sys::global_shortcut::unregister;
///
/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
/// unregister("CmdOrControl+Space").await?;
/// # Ok(())
/// # }
/// ```
pub async fn unregister(shortcut: &str) -> crate::Result<()> {
inner::unregister(shortcut).await?;
// impl<T> Stream for ListenAll<T> {
// type Item = T;
Ok(())
}
/// Unregisters all shortcuts registered by the application.
///
/// # Example
///
/// ```rust,no_run
/// use tauri_sys::global_shortcut::unregister_all;
///
/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
/// unregister_all().await?;
/// # Ok(())
/// # }
/// ```
pub async fn unregister_all() -> crate::Result<()> {
inner::unregisterAll().await?;
Ok(())
}
// fn poll_next(
// mut self: std::pin::Pin<&mut Self>,
// cx: &mut std::task::Context<'_>,
// ) -> std::task::Poll<Option<Self::Item>> {
// self.rx.poll_next_unpin(cx)
// }
// }
mod inner {
use js_sys::Array;
// use js_sys::Array;
use wasm_bindgen::{
prelude::{wasm_bindgen, Closure},
JsValue,
@ -151,14 +189,11 @@ mod inner {
shortcut: &str,
handler: &Closure<dyn FnMut(JsValue)>,
) -> Result<(), JsValue>;
#[wasm_bindgen(catch)]
pub async fn registerAll(
shortcuts: Array,
handler: &Closure<dyn FnMut(JsValue)>,
) -> Result<(), JsValue>;
#[wasm_bindgen(catch)]
pub async fn unregister(shortcut: &str) -> Result<(), JsValue>;
#[wasm_bindgen(catch)]
pub async fn unregisterAll() -> Result<(), JsValue>;
// #[wasm_bindgen(catch)]
// pub async fn registerAll(
// shortcuts: Array,
// handler: &Closure<dyn FnMut(JsValue)>,
// ) -> Result<(), JsValue>;
pub fn unregister(shortcut: JsValue);
}
}

View file

@ -1,4 +1,122 @@
use wasm_bindgen::JsValue;
//! Bindings to the [`Tauri API`](https://tauri.app/v1/api/js/) for projects using [`wasm-bindgen`](https://github.com/rustwasm/wasm-bindgen)
//!
//! Tauri is a framework for building tiny, blazing fast, and secure cross-platform applications.
//! Developers can integrate any front-end framework that compiles to HTML, JS and CSS for building their user interface.
//! The backend of the application is a rust binary, leveraging the [`tauri`] crate.
//!
//! This crate contains idiomatic rust bindings to the backend, for usage within Rust projects that target wasm32-unknown-unknown,
//! for example Rust frontend frameworks such as [`yew`](https://yew.rs), [`sycamore`](https://sycamore-rs.netlify.app) or [`dominator`](https://github.com/Pauan/rust-dominator).
//!
//! The wasmtime crate has similar concepts to the the JS WebAssembly API as well as the proposed C API, but the Rust API is designed for efficiency, ergonomics, and expressivity in Rust. As with all other Rust code youre guaranteed that programs will be safe (not have undefined behavior or segfault) so long as you dont use unsafe in your own program.
//!
//! # Differences to the JavaScript API
//!
//! ## Event Listeners
//!
//! Event Listeners, such as [`event::listen`] module or [`window::WebviewWindow::listen`],
//! are modeled as async streams of data using the [`futures::Stream`] trait instead of using callbacks.
//! Streams have multiple advantages over callbacks:
//!
//! #### Stream Combinators
//!
//! Streams are essentially the async equivalent of the standard [`Iterator`] and therefore expose a very similar set of combinator functions.
//! This means streams are much more versatile and ergonomic than simple callbacks.
//!
//! For example, we can use Stream combinators and various utility functions
//! to replicate the `registerAll` function that unregisters the shortcuts after 20 seconds:
//!
//! ```rust
//! use futures::{future, stream, Stream, StreamExt};
//! use std::time::Duration;
//! use tauri_sys::global_shortcut;
//!
//! async fn register_with_shortcut<'a>(
//! shortcut: &'a str,
//! ) -> anyhow::Result<impl Stream<Item = &'a str>> {
//! let stream = global_shortcut::register(shortcut).await?;
//!
//! Ok(stream.map(move |_| shortcut))
//! }
//!
//! async fn register_all() {
//! let shortcuts = ["CommandOrControl+Shift+C", "Ctrl+Alt+F12"];
//!
//! let timeout = gloo_timers::future::sleep(Duration::from_secs(20));
//!
//! // await the futures that creates the streams, exiting early if any future resolves with an error
//! let streams = future::try_join_all(shortcuts.map(register_with_shortcut)).await?;
//!
//! // combine all streams into one
//! let mut events = stream::select_all(streams).take_until(timeout);
//!
//! while let Some(shortcut) = events.next().await {
//! log::debug!("Shortcut {} triggered", shortcut);
//! }
//! }
//! ```
//!
//! #### Automatic cleanup
//!
//! Streams follow Rust's RAII idiom as they automatically clean up after themselves when being dropped.
//! No need to manually call `unlisten` like in the JS API to avoid memory leaks or double-listens.
//!
//! ```rust
//! async fn process_some_errors() {
//! let win = WebviewWindow::get_by_label("main").unwrap();
//!
//! let errors = win.listen("tauri://error").await?
//! .take(3);
//!
//! while let Some(err) = errors.next().await {
//! log::error!("Something bad happened! {}", err)
//! }
//!
//! // the stream is dropped here and the underlying listener automatically detached.
//! }
//! ```
//!
//! #### Streams are buffered
//!
//! Streams, much like iterators, are poll-based meaning the caller is responsible for advancing it.
//! This allows greater flexibilty as you can freely decide *when* to process events.
//! Event streams are internally backed by an unbounded queue so events are buffered until read,
//! so no events are getting lost even if you temporarily pause processing.
//!
//! Being unbounded means the memory consumption will grow if the stream is kept around, but not read from.
//! This is rarely a concern in practice, but if you need to suspend processing of events for a long time,
//! you should rather drop the entire stream and re-create it as needed later.
//!
//! ### Cancelling Streams
//!
//! One usecase of the `unlisten` function might intuitively not map well to streams: Cancellation.
//! In JavaScript you can do this when you want to detach an event listener:
//!
//! ```js
//! import { listen } from '@tauri-apps/api/event'
//!
//! const unlisten = await listen('rust-event', (ev) => console.log(ev))
//!
//! // Some time later. We are no longer interested in listening to the event
//! unlisten()
//! ```
//!
//! But if the Rust event stream only gets detached when the stream get's dropped, how can we cancel the stream at will?
//! We can make use of the combinators and utility functions offered by the [`futures`] crate again, namely the [`futures::stream::Abortable`] type:
//!
//! ```rust
//! use tauri_sys::event::listen;
//!
//! let events = listen::<()>("rust-event").await?
//! // abort handle behaves identical to the JavaScript `unlisten` function
//! let (events, abort_handle) = futures::stream::abortable(events);
//!
//! while let Some(_) = events.next().await {
//! log::debug!("Received event!");
//! }
//!
//! // in some other task, when we're done with listening to the events
//! abort_handle.abort();
//! ```
#[cfg(feature = "app")]
pub mod app;
@ -6,6 +124,7 @@ pub mod app;
pub mod clipboard;
#[cfg(feature = "dialog")]
pub mod dialog;
mod error;
#[cfg(feature = "event")]
pub mod event;
#[cfg(feature = "global_shortcut")]
@ -27,32 +146,35 @@ pub mod updater;
#[cfg(feature = "window")]
pub mod window;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("{0}")]
Serde(String),
#[error("Unknown Theme \"{0}\". Expected one of \"light\",\"dark\"")]
UnknownTheme(String),
#[error("{0}")]
Other(String),
#[cfg(feature = "tauri")]
#[error("Invalid Url {0}")]
InvalidUrl(#[from] url::ParseError),
#[cfg(feature = "app")]
#[error("Invalid Version {0}")]
InvalidVersion(#[from] semver::Error),
}
pub(crate) use error::Error;
pub(crate) type Result<T> = core::result::Result<T, Error>;
impl From<serde_wasm_bindgen::Error> for Error {
fn from(e: serde_wasm_bindgen::Error) -> Self {
Self::Serde(format!("{:?}", e))
#[cfg(any(feature = "dialog", feature = "window"))]
pub(crate) mod utils {
pub struct ArrayIterator {
pos: u32,
arr: js_sys::Array,
}
impl ArrayIterator {
pub fn new(arr: js_sys::Array) -> Self {
Self { pos: 0, arr }
}
}
impl Iterator for ArrayIterator {
type Item = wasm_bindgen::JsValue;
fn next(&mut self) -> Option<Self::Item> {
let raw = self.arr.get(self.pos);
if raw.is_undefined() {
None
} else {
self.pos += 1;
Some(raw)
}
}
}
}
impl From<JsValue> for Error {
fn from(e: JsValue) -> Self {
Self::Serde(format!("{:?}", e))
}
}
pub(crate) type Result<T> = std::result::Result<T, Error>;

View file

@ -1,7 +1,9 @@
//! Customize the auto updater flow.
use futures::{Stream, channel::mpsc};
use serde::Deserialize;
use wasm_bindgen::{prelude::Closure, JsValue};
use crate::event::Listen;
#[derive(Deserialize, Debug, Clone)]
pub struct UpdateManifest {
@ -11,6 +13,7 @@ pub struct UpdateManifest {
}
#[derive(Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct UpdateResult {
pub manifest: Option<UpdateManifest>,
pub should_update: bool,
@ -77,46 +80,45 @@ pub async fn install_update() -> crate::Result<()> {
/// Listen to an updater event.
///
/// The returned Future will automatically clean up it's underlying event listener when dropped, so no manual unlisten function needs to be called.
/// See [Differences to the JavaScript API](../index.html#differences-to-the-javascript-api) for details.
///
/// # Example
///
/// ```rust,no_run
/// use tauri_sys::updater::on_updater_event;
/// use tauri_sys::updater::updater_events;
/// use web_sys::console;
///
/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let unlisten = on_updater_event(|event| {
/// log::debug!("Updater event {:?}", event);
/// }).await?;
/// let events = updater_events();
///
/// // you need to call unlisten if your handler goes out of scope e.g. the component is unmounted
/// unlisten();
/// while let Some(event) = events.next().await {
/// console::log_1(&format!("Updater event {:?}", event).into());
/// }
/// # Ok(())
/// # }
/// ```
/// Note that removing the listener is required if your listener goes out of scope e.g. the component is unmounted.
#[inline(always)]
pub async fn on_updater_event<H>(mut handler: H) -> crate::Result<impl FnOnce()>
where
H: FnMut(Result<UpdateStatus, String>) + 'static,
{
pub async fn updater_events() -> crate::Result<impl Stream<Item = Result<UpdateStatus, String>>> {
let (tx, rx) = mpsc::unbounded::<Result<UpdateStatus, String>>();
let closure = Closure::<dyn FnMut(JsValue)>::new(move |raw| {
let raw: UpdateStatusResult = serde_wasm_bindgen::from_value(raw).unwrap();
let result = if let Some(error) = raw.error {
let msg = if let Some(error) = raw.error {
Err(error)
} else {
Ok(raw.status)
};
(handler)(result)
let _ = tx.unbounded_send(msg);
});
let unlisten = inner::onUpdaterEvent(&closure).await?;
closure.forget();
let unlisten = js_sys::Function::from(unlisten);
Ok(move || {
unlisten.call0(&wasm_bindgen::JsValue::NULL).unwrap();
Ok(Listen {
rx,
unlisten: js_sys::Function::from(unlisten),
})
}

View file

@ -1,5 +1,5 @@
//! Provides APIs to create windows, communicate with other windows and manipulate the current window.
//!
//!
//! The APIs must be added to tauri.allowlist.window in tauri.conf.json:
//! ```json
//! {
@ -43,7 +43,12 @@
//! ```
//! It is recommended to allowlist only the APIs you use for optimal bundle size and security.
use crate::event::Event;
use crate::{event::{Event, Listen, Once}, utils::ArrayIterator};
use futures::{
channel::{mpsc, oneshot},
Stream,
};
use js_sys::Array;
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use std::fmt::Display;
use wasm_bindgen::{prelude::Closure, JsCast, JsValue};
@ -178,6 +183,7 @@ impl Display for CursorIcon {
}
#[derive(Debug, Default, Clone, Serialize)]
#[serde(rename = "camelCase")]
struct WebviewWindowOptions<'a> {
url: Option<&'a str>,
center: bool,
@ -379,7 +385,7 @@ impl<'a> WebviewWindowBuilder<'a> {
}
/// Creates a new webview window.
///
///
/// Requires [`allowlist > window > create`](https://tauri.app/v1/api/config#windowallowlistconfig.create) to be enabled.
pub fn build(&self) -> crate::Result<WebviewWindow> {
let opts = serde_wasm_bindgen::to_value(&self.inner)?;
@ -485,7 +491,7 @@ impl WebviewWindow {
}
/// Centers the window.
///
///
/// Requires [`allowlist > window > center`](https://tauri.app/v1/api/config#windowallowlistconfig.center) to be enabled.
pub async fn center(&self) -> crate::Result<()> {
Ok(self.0.center().await?)
@ -498,7 +504,7 @@ impl WebviewWindow {
/// #### Platform-specific
/// - macOS: None has no effect.
/// - Linux: Urgency levels have the same effect.
///
///
/// Requires [`allowlist > window > requestUserAttention`](https://tauri.app/v1/api/config#windowallowlistconfig.requestuserattention) to be enabled.
pub async fn request_user_attention(
&self,
@ -507,38 +513,38 @@ impl WebviewWindow {
Ok(self.0.requestUserAttention(request_type as u32).await?)
}
/// Opens the dialog to prints the contents of the webview.
///
/// Opens the dialog to prints the contents of the webview.
///
/// Currently only supported on macOS on wry. window.print() works on all platforms.
///
///
/// Requires [`allowlist > window > print`](https://tauri.app/v1/api/config#windowallowlistconfig.print) to be enabled.
pub fn print(&self) -> crate::Result<()> {
todo!()
}
/// Determines if this window should be resizable.
///
///
/// Requires [`allowlist > window > setResizable`](https://tauri.app/v1/api/config#windowallowlistconfig.setresizable) to be enabled.
pub async fn set_resizable(&self, resizable: bool) -> crate::Result<()> {
Ok(self.0.setResizable(resizable).await?)
}
/// Set this windows title.
///
///
/// Requires [`allowlist > window > setTitle`](https://tauri.app/v1/api/config#windowallowlistconfig.settitle) to be enabled.
pub async fn set_title(&self, title: impl AsRef<str>) -> crate::Result<()> {
Ok(self.0.setTitle(title.as_ref()).await?)
}
/// Maximizes this window.
///
///
/// Requires [`allowlist > window > maximize`](https://tauri.app/v1/api/config#windowallowlistconfig.maximize) to be enabled.
pub async fn maximize(&self) -> crate::Result<()> {
Ok(self.0.maximize().await?)
}
/// Un-maximizes this window.
///
///
/// Requires [`allowlist > window > unmaximize`](https://tauri.app/v1/api/config#windowallowlistconfig.unmaximize) to be enabled.
pub async fn unmaximize(&self) -> crate::Result<()> {
Ok(self.0.unmaximize().await?)
@ -549,56 +555,56 @@ impl WebviewWindow {
}
/// Minimizes this window.
///
///
/// Requires [`allowlist > window > minimize`](https://tauri.app/v1/api/config#windowallowlistconfig.minimize) to be enabled.
pub async fn minimize(&self) -> crate::Result<()> {
Ok(self.0.minimize().await?)
}
/// Un-minimizes this window.
///
///
/// Requires [`allowlist > window > unminimize`](https://tauri.app/v1/api/config#windowallowlistconfig.unminimize) to be enabled.
pub async fn unminimize(&self) -> crate::Result<()> {
Ok(self.0.unminimize().await?)
}
/// Show this window.
///
///
/// Requires [`allowlist > window > show`](https://tauri.app/v1/api/config#windowallowlistconfig.show) to be enabled.
pub async fn show(&self) -> crate::Result<()> {
Ok(self.0.show().await?)
}
/// Hide this window.
///
///
/// Requires [`allowlist > window > hide`](https://tauri.app/v1/api/config#windowallowlistconfig.hide) to be enabled.
pub async fn hide(&self) -> crate::Result<()> {
Ok(self.0.hide().await?)
}
/// Closes this window.
///
///
/// Requires [`allowlist > window > close`](https://tauri.app/v1/api/config#windowallowlistconfig.close) to be enabled.
pub async fn close(&self) -> crate::Result<()> {
Ok(self.0.close().await?)
}
/// Determines if this window should be [decorated](https://en.wikipedia.org/wiki/Window_(computing)#Window_decoration).
///
///
/// Requires [`allowlist > window > setDecorations`](https://tauri.app/v1/api/config#windowallowlistconfig.setdecorations) to be enabled.
pub async fn set_decorations(&self, decorations: bool) -> crate::Result<()> {
Ok(self.0.setDecorations(decorations).await?)
}
/// Determines if this window should always be on top of other windows.
///
///
/// Requires [`allowlist > window > setAlwaysOnTop`](https://tauri.app/v1/api/config#windowallowlistconfig.setalwaysontop) to be enabled.
pub async fn set_always_on_top(&self, always_on_top: bool) -> crate::Result<()> {
Ok(self.0.setAlwaysOnTop(always_on_top).await?)
}
/// Resizes this window.
///
///
/// Requires [`allowlist > window > setSize`](https://tauri.app/v1/api/config#windowallowlistconfig.setsize) to be enabled.
pub async fn set_size(&self, size: impl Into<Size>) -> crate::Result<()> {
match size.into() {
@ -610,7 +616,7 @@ impl WebviewWindow {
}
/// Sets this windows minimum size.
///
///
/// Requires [`allowlist > window > setMinSize`](https://tauri.app/v1/api/config#windowallowlistconfig.setminsize) to be enabled.
pub async fn set_min_size(&self, size: Option<impl Into<Size>>) -> crate::Result<()> {
match size.map(Into::into) {
@ -623,7 +629,7 @@ impl WebviewWindow {
}
/// Sets this windows maximum size.
///
///
/// Requires [`allowlist > window > setMaxSize`](https://tauri.app/v1/api/config#windowallowlistconfig.setmaxsize) to be enabled.
pub async fn set_max_size(&self, size: Option<impl Into<Size>>) -> crate::Result<()> {
match size.map(Into::into) {
@ -636,7 +642,7 @@ impl WebviewWindow {
}
/// Sets this windows position.
///
///
/// Requires [`allowlist > window > setPosition`](https://tauri.app/v1/api/config#windowallowlistconfig.setposition) to be enabled.
pub async fn set_position(&self, position: impl Into<Position>) -> crate::Result<()> {
match position.into() {
@ -648,28 +654,28 @@ impl WebviewWindow {
}
/// Determines if this window should be fullscreen.
///
///
/// Requires [`allowlist > window > setFullscreen`](https://tauri.app/v1/api/config#windowallowlistconfig.setfullscreen) to be enabled.
pub async fn set_fullscreen(&self, fullscreen: bool) -> crate::Result<()> {
Ok(self.0.setFullscreen(fullscreen).await?)
}
/// Bring the window to front and focus.
///
///
/// Requires [`allowlist > window > setFocus`](https://tauri.app/v1/api/config#windowallowlistconfig.setfocus) to be enabled.
pub async fn set_focus(&self) -> crate::Result<()> {
Ok(self.0.setFocus().await?)
}
/// Sets this window icon.
///
///
/// Requires [`allowlist > window > setIcon`](https://tauri.app/v1/api/config#windowallowlistconfig.seticon) to be enabled.
pub async fn set_icon(&self, icon: &[u8]) -> crate::Result<()> {
Ok(self.0.setIcon(icon).await?)
}
/// Whether to show the window icon in the task bar or not.
///
///
/// Requires [`allowlist > window > setSkipTaskbar`](https://tauri.app/v1/api/config#windowallowlistconfig.setskiptaskbar) to be enabled.
pub async fn set_skip_taskbar(&self, skip: bool) -> crate::Result<()> {
Ok(self.0.setSkipTaskbar(skip).await?)
@ -682,7 +688,7 @@ impl WebviewWindow {
/// #### Platform-specific
/// - Linux: Unsupported.
/// - macOS: This locks the cursor in a fixed location, which looks visually awkward.
///
///
/// Requires [`allowlist > window > setCursorGrab`](https://tauri.app/v1/api/config#windowallowlistconfig.setcursorgrab) to be enabled.
pub async fn set_cursor_grab(&self, grab: bool) -> crate::Result<()> {
Ok(self.0.setCursorGrab(grab).await?)
@ -695,21 +701,21 @@ impl WebviewWindow {
/// #### Platform-specific
/// - Windows: The cursor is only hidden within the confines of the window.
/// - macOS: The cursor is hidden as long as the window has input focus, even if the cursor is outside of the window.
///
///
/// Requires [`allowlist > window > setCursorVisible`](https://tauri.app/v1/api/config#windowallowlistconfig.setcursorvisible) to be enabled.
pub async fn set_cursor_visible(&self, visible: bool) -> crate::Result<()> {
Ok(self.0.setCursorVisible(visible).await?)
}
/// Modifies the cursor icon of the window.
///
///
/// Requires [`allowlist > window > setCursorIcon`](https://tauri.app/v1/api/config#windowallowlistconfig.setcursoricon) to be enabled.
pub async fn set_cursor_icon(&self, icon: CursorIcon) -> crate::Result<()> {
Ok(self.0.setCursorIcon(&icon.to_string()).await?)
}
/// Changes the position of the cursor in window coordinates.
///
///
/// Requires [`allowlist > window > setCursorPosition`](https://tauri.app/v1/api/config#windowallowlistconfig.setcursorposition) to be enabled.
pub async fn set_cursor_position(&self, position: Position) -> crate::Result<()> {
match position {
@ -721,14 +727,14 @@ impl WebviewWindow {
}
/// Ignores the window cursor events.
///
///
/// Requires [`allowlist > window > setIgnoreCursorEvents`](https://tauri.app/v1/api/config#windowallowlistconfig.setignorecursorevents) to be enabled.
pub async fn set_ignore_cursor_events(&self, ignore: bool) -> crate::Result<()> {
Ok(self.0.setIgnoreCursorEvents(ignore).await?)
}
/// Starts dragging the window.
///
///
/// Requires [`allowlist > window > startDragging`](https://tauri.app/v1/api/config#windowallowlistconfig.startdragging) to be enabled.
pub async fn start_dragging(&self) -> crate::Result<()> {
Ok(self.0.startDragging().await?)
@ -745,45 +751,51 @@ impl WebviewWindow {
}
/// Listen to an event emitted by the backend that is tied to the webview window.
///
/// The returned Future will automatically clean up it's underlying event listener when dropped, so no manual unlisten function needs to be called.
/// See [Differences to the JavaScript API](../index.html#differences-to-the-javascript-api) for details.
#[inline(always)]
pub async fn listen<T, H>(&self, event: &str, mut handler: H) -> crate::Result<impl FnOnce()>
pub async fn listen<T, H>(&self, event: &str) -> crate::Result<impl Stream<Item = Event<T>>>
where
T: DeserializeOwned,
H: FnMut(Event<T>) + 'static,
T: DeserializeOwned + 'static,
{
let (tx, rx) = mpsc::unbounded::<Event<T>>();
let closure = Closure::<dyn FnMut(JsValue)>::new(move |raw| {
(handler)(serde_wasm_bindgen::from_value(raw).unwrap())
let _ = tx.unbounded_send(serde_wasm_bindgen::from_value(raw).unwrap());
});
let unlisten = self.0.listen(event, &closure).await?;
closure.forget();
let unlisten = js_sys::Function::from(unlisten);
Ok(move || {
unlisten.call0(&wasm_bindgen::JsValue::NULL).unwrap();
Ok(Listen {
rx,
unlisten: js_sys::Function::from(unlisten),
})
}
/// Listen to an one-off event emitted by the backend that is tied to the webview window.
///
/// The returned Future will automatically clean up it's underlying event listener when dropped, so no manual unlisten function needs to be called.
/// See [Differences to the JavaScript API](../index.html#differences-to-the-javascript-api) for details.
#[inline(always)]
pub async fn once<T, H>(&self, event: &str, mut handler: H) -> crate::Result<impl FnOnce()>
pub async fn once<T, H>(&self, event: &str) -> crate::Result<Event<T>>
where
T: DeserializeOwned,
H: FnMut(Event<T>) + 'static,
T: DeserializeOwned + 'static,
{
let closure = Closure::<dyn FnMut(JsValue)>::new(move |raw| {
(handler)(serde_wasm_bindgen::from_value(raw).unwrap())
let (tx, rx) = oneshot::channel::<Event<T>>();
let closure: Closure<dyn FnMut(JsValue)> = Closure::once(move |raw| {
let _ = tx.send(serde_wasm_bindgen::from_value(raw).unwrap());
});
let unlisten = self.0.once(event, &closure).await?;
closure.forget();
let unlisten = js_sys::Function::from(unlisten);
Ok(move || {
unlisten.call0(&wasm_bindgen::JsValue::NULL).unwrap();
})
let fut = Once {
rx,
unlisten: js_sys::Function::from(unlisten),
};
fut.await
}
}
@ -1001,8 +1013,10 @@ pub fn current_window() -> WebviewWindow {
/// # Ok(())
/// # }
/// ```
pub fn all_windows() -> Vec<WebviewWindow> {
inner::getAll().into_iter().map(WebviewWindow).collect()
pub fn all_windows() -> impl IntoIterator<Item = WebviewWindow> {
let raw = inner::getAll();
ArrayIterator::new(raw).map(|r| WebviewWindow(inner::WebviewWindow::from(r)))
}
/// Returns the monitor on which the window currently resides.
@ -1053,29 +1067,6 @@ pub async fn primary_monitor() -> crate::Result<Option<Monitor>> {
}
}
#[derive(Debug, Clone)]
pub struct AvailableMonitors {
idx: u32,
array: js_sys::Array,
}
impl Iterator for AvailableMonitors {
type Item = Monitor;
fn next(&mut self) -> Option<Self::Item> {
let raw = self.array.get(self.idx);
if raw.is_undefined() {
None
} else {
let monitor = Monitor(raw);
self.idx += 1;
Some(monitor)
}
}
}
/// Returns the list of all the monitors available on the system.
///
/// # Example
@ -1093,16 +1084,17 @@ impl Iterator for AvailableMonitors {
/// # Ok(())
/// # }
/// ```
pub async fn available_monitors() -> crate::Result<AvailableMonitors> {
pub async fn available_monitors() -> crate::Result<impl Iterator<Item = Monitor>> {
let raw = inner::availableMonitors().await?;
let raw = Array::try_from(raw).unwrap();
Ok(AvailableMonitors {
idx: 0,
array: raw.unchecked_into(),
})
let monitors = ArrayIterator::new(raw).map(Monitor);
Ok(monitors)
}
mod inner {
use js_sys::Array;
use wasm_bindgen::{
prelude::{wasm_bindgen, Closure},
JsValue,
@ -1351,7 +1343,7 @@ mod inner {
#[wasm_bindgen(module = "/src/window.js")]
extern "C" {
pub fn getCurrent() -> WebviewWindow;
pub fn getAll() -> Vec<WebviewWindow>;
pub fn getAll() -> Array;
#[wasm_bindgen(catch)]
pub async fn currentMonitor() -> Result<JsValue, JsValue>;
#[wasm_bindgen(catch)]