Skip to content

Commit d5f0d58

Browse files
committed
feat: derive(Events) to generate listen bindings
1 parent 65849f4 commit d5f0d58

File tree

3 files changed

+240
-8
lines changed

3 files changed

+240
-8
lines changed

Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[package]
22
edition = "2021"
33
name = "tauri-bindgen-rs-macros"
4-
version = "0.1.0"
4+
version = "0.1.1"
55

66
[lib]
77
proc-macro = true

README.md

+29-4
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ I couldn't find a comfortable way of defining commands that would maintain type
1010

1111
## Usage
1212

13-
1. Create an intermediary crate in the workspace of your Tauri app to house traits defining your commands and generated IPC bindings to import into the Rust frontend, e.g:
13+
1. Create an intermediary crate in the workspace of your Tauri app to house traits defining your commands, events, and generated IPC bindings to import into the Rust frontend, e.g:
1414

1515
```toml
1616
[package]
@@ -19,7 +19,7 @@ I couldn't find a comfortable way of defining commands that would maintain type
1919
version = "0.1.0"
2020

2121
[dependencies]
22-
tauri-bindgen-rs-macros = { version = "0.1.0", git = "https://github.com/jvatic/tauri-bindgen-rs-macros.git" }
22+
tauri-bindgen-rs-macros = { version = "0.1.1", git = "https://github.com/jvatic/tauri-bindgen-rs-macros.git" }
2323
serde = { version = "1.0.204", features = ["derive"] }
2424
serde-wasm-bindgen = "0.6"
2525
wasm-bindgen = "0.2"
@@ -32,8 +32,17 @@ I couldn't find a comfortable way of defining commands that would maintain type
3232
pub trait Commands {
3333
async hello(name: String) -> Result<String, String>;
3434
}
35+
36+
#[derive(tauri_bindgen_rs_macros::Events, Debug, Clone, ::serde::Serialize, ::serde::Deserialize)]
37+
enum Event {
38+
SomethingHappened { payload: Vec<u8> },
39+
SomeoneSaidHello(String),
40+
NoPayload,
41+
}
3542
```
3643

44+
**NOTE:** If you have multiple enums deriving `Events`, these will need to be in separate modules since there's some common boilerplate types that are included currently (that will be moved into another crate at some point).
45+
3746
And if you're using a plugin on the frontend and want bindings generated for it, you can do so by defining a trait for it, e.g:
3847

3948
```rust
@@ -46,9 +55,9 @@ I couldn't find a comfortable way of defining commands that would maintain type
4655
}
4756
```
4857

49-
**NOTE:** If you have multiple traits implementing `invoke_bindings` they'll each need to be in their own `mod` since an `invoke` WASM binding will be derived in scope of where the trait is defined.
58+
**NOTE:** If you have multiple traits implementing `invoke_bindings` they'll each need to be in their own `mod` since an `invoke` WASM binding will be derived in scope of where the trait is defined (this will be moved into another crate at some point).
5059

51-
2. Import the trait into your Tauri backend and wrap your command definitions in the `impl_trait` macro, e.g:
60+
2. Import the commands trait into your Tauri backend and wrap your command definitions in the `impl_trait` macro, e.g:
5261

5362
```rust
5463
use my_commands::Commands;
@@ -62,6 +71,15 @@ I couldn't find a comfortable way of defining commands that would maintain type
6271

6372
This will define a shadow struct with an `impl Commands` block with all the functions passed into the macro minus any fn generics or arguments where the type starts with `tauri::`, and spits out the actual fns untouched. The Rust compiler will then emit helpful errors if the defined commands are different (after being processed) from those in the trait, yay!
6473

74+
3. Import the event enum into your Tauri backend if you wish to emit events from there, e.g.:
75+
76+
```rust
77+
use my_commands::Event;
78+
fn emit_event(app_handle: tauri::AppHandle, event: Event) -> anyhow::Result<()> {
79+
Ok(app_handle.emit(event.event_name(), event)?)
80+
}
81+
```
82+
6583
3. Use the generated IPC bindings in your Rust frontend, eg:
6684

6785
```rust
@@ -71,4 +89,11 @@ I couldn't find a comfortable way of defining commands that would maintain type
7189
set_greeting(greeting);
7290
});
7391
// ...
92+
spawn_local(async move {
93+
let listener = my_commands::EventBinding::SomethingHappened.listen(|event: my_commands::Event| {
94+
// ...
95+
}).await;
96+
drop(listener); // unlisten
97+
});
98+
// ...
7499
```

src/lib.rs

+210-3
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
use proc_macro::TokenStream;
2-
use proc_macro2::Span;
2+
use proc_macro2::{Span, TokenStream as TokenStream2};
33
use quote::{quote, ToTokens, TokenStreamExt};
44
use syn::{
55
self, braced,
66
parse::Parse,
77
parse_macro_input, parse_quote,
88
punctuated::{Pair, Punctuated},
99
token::{self, Comma},
10-
Field, FieldMutability, FnArg, Ident, ItemFn, ItemTrait, LitStr, Pat, Signature, Token,
11-
TraitItem, Type, Visibility,
10+
Field, FieldMutability, Fields, FnArg, Generics, Ident, ItemEnum, ItemFn, ItemTrait, LitStr,
11+
Pat, Signature, Token, TraitItem, Type, Variant, Visibility,
1212
};
1313

1414
#[derive(Default)]
@@ -138,6 +138,213 @@ pub fn invoke_bindings(attrs: TokenStream, tokens: TokenStream) -> TokenStream {
138138
TokenStream::from(ret)
139139
}
140140

141+
/// # Examples
142+
///
143+
/// ```ignore
144+
/// #[derive(Events, Debug, Clone, ::serde::Serialize, ::serde::Deserialize)]
145+
/// enum Event {
146+
/// SomethingHappened { payload: Vec<u8> },
147+
/// SomeoneSaidHello(String),
148+
/// NoPayload,
149+
/// }
150+
///
151+
/// fn emit_event(app_handle: tauri::AppHandle, event: Event) -> anyhow::Result<()> {
152+
/// Ok(app_handle.emit(event.event_name(), event)?)
153+
/// }
154+
///
155+
/// // ...
156+
///
157+
/// let listener = EventBinding::SomethingHappened.listen(|event: Event| {
158+
/// // ...
159+
/// }).await;
160+
/// drop(listener); // unlisten
161+
/// ```
162+
#[proc_macro_derive(Events)]
163+
pub fn derive_event(tokens: TokenStream) -> TokenStream {
164+
let item_enum = parse_macro_input!(tokens as ItemEnum);
165+
let ItemEnum {
166+
attrs: _,
167+
vis,
168+
enum_token: _,
169+
ident,
170+
generics,
171+
brace_token: _,
172+
variants,
173+
} = item_enum;
174+
175+
fn derive_impl_display(
176+
vis: Visibility,
177+
_generics: Generics, // TODO: support generics
178+
ident: Ident,
179+
variants: Punctuated<Variant, Comma>,
180+
) -> TokenStream2 {
181+
let match_arms: Punctuated<TokenStream2, Comma> = variants
182+
.iter()
183+
.map(|v| -> TokenStream2 {
184+
let ident = ident.clone();
185+
let v_ident = &v.ident;
186+
let v_ident_str = v_ident.to_string();
187+
let fields: TokenStream2 = match &v.fields {
188+
Fields::Unit => quote! {}.into(),
189+
Fields::Unnamed(fields) => {
190+
let placeholders: Punctuated<TokenStream2, Comma> = fields
191+
.unnamed
192+
.iter()
193+
.map(|_| -> TokenStream2 { quote! { _ }.into() })
194+
.collect();
195+
quote! { (#placeholders) }.into()
196+
}
197+
Fields::Named(fields) => {
198+
let placeholders: Punctuated<TokenStream2, Comma> = fields
199+
.named
200+
.iter()
201+
.map(|f| -> TokenStream2 {
202+
let ident = f.ident.as_ref().unwrap();
203+
quote! { #ident: _ }.into()
204+
})
205+
.collect();
206+
quote! { {#placeholders} }.into()
207+
}
208+
};
209+
quote! {
210+
#ident::#v_ident #fields => #v_ident_str
211+
}
212+
.into()
213+
})
214+
.collect();
215+
let ret = quote! {
216+
impl #ident {
217+
#vis fn event_name(&self) -> &'static str {
218+
match self {
219+
#match_arms
220+
}
221+
}
222+
}
223+
};
224+
TokenStream2::from(ret)
225+
}
226+
227+
fn derive_event_binding(
228+
_generics: Generics, // TODO: support generics
229+
ident: Ident,
230+
variants: Punctuated<Variant, Comma>,
231+
) -> TokenStream2 {
232+
let event_binding_ident =
233+
Ident::new(&format!("{}Binding", ident.to_string()), Span::call_site());
234+
let variant_names: Punctuated<Ident, Comma> =
235+
variants.iter().map(|v| v.ident.clone()).collect();
236+
let variant_to_str_match_arms: Punctuated<TokenStream2, Comma> = variants
237+
.iter()
238+
.map(|v| -> TokenStream2 {
239+
let ident = &v.ident;
240+
let ident_str = ident.to_string();
241+
quote! {
242+
#event_binding_ident::#ident => #ident_str
243+
}
244+
.into()
245+
})
246+
.collect();
247+
let ret = quote! {
248+
pub enum #event_binding_ident {
249+
#variant_names
250+
}
251+
252+
impl #event_binding_ident {
253+
pub async fn listen<F>(&self, handler: F) -> Result<EventListener, JsValue>
254+
where
255+
F: Fn(#ident) + 'static,
256+
{
257+
let event_name = self.as_str();
258+
EventListener::new(event_name, move |event| {
259+
let event: TauriEvent<#ident> = ::serde_wasm_bindgen::from_value(event).unwrap();
260+
handler(event.payload);
261+
})
262+
.await
263+
}
264+
265+
fn as_str(&self) -> &str {
266+
match self {
267+
#variant_to_str_match_arms
268+
}
269+
}
270+
}
271+
};
272+
TokenStream2::from(ret)
273+
}
274+
275+
// TODO: break this out into another crate (it doesn't need to be in a macro)
276+
fn events_mod(vis: Visibility) -> TokenStream2 {
277+
quote! {
278+
use wasm_bindgen::prelude::*;
279+
280+
#[wasm_bindgen]
281+
extern "C" {
282+
#[wasm_bindgen(js_namespace = ["window", "__TAURI__", "event"], catch)]
283+
async fn listen(
284+
event_name: &str,
285+
handler: &Closure<dyn FnMut(JsValue)>,
286+
) -> Result<JsValue, JsValue>;
287+
}
288+
289+
#vis struct EventListener {
290+
event_name: String,
291+
_closure: Closure<dyn FnMut(JsValue)>,
292+
unlisten: js_sys::Function,
293+
}
294+
295+
impl EventListener {
296+
pub async fn new<F>(event_name: &str, handler: F) -> Result<Self, JsValue>
297+
where
298+
F: Fn(JsValue) + 'static,
299+
{
300+
let closure = Closure::new(handler);
301+
let unlisten = listen(event_name, &closure).await?;
302+
let unlisten = js_sys::Function::from(unlisten);
303+
304+
tracing::trace!("EventListener created for {event_name}");
305+
306+
Ok(Self {
307+
event_name: event_name.to_string(),
308+
_closure: closure,
309+
unlisten,
310+
})
311+
}
312+
}
313+
314+
impl Drop for EventListener {
315+
fn drop(&mut self) {
316+
tracing::trace!("EventListener dropped for {}", self.event_name);
317+
let context = JsValue::null();
318+
self.unlisten.call0(&context).unwrap();
319+
}
320+
}
321+
322+
#[derive(::serde::Deserialize)]
323+
struct TauriEvent<T> {
324+
pub payload: T,
325+
}
326+
}
327+
}
328+
329+
let impl_display = derive_impl_display(
330+
vis.clone(),
331+
generics.clone(),
332+
ident.clone(),
333+
variants.clone(),
334+
);
335+
let event_binding = derive_event_binding(generics, ident, variants);
336+
let events_mod = events_mod(vis);
337+
338+
let ret = quote! {
339+
#impl_display
340+
341+
#event_binding
342+
343+
#events_mod
344+
};
345+
TokenStream::from(ret)
346+
}
347+
141348
struct ImplTrait {
142349
trait_ident: Ident,
143350
fns: ItemList<ItemFn>,

0 commit comments

Comments
 (0)