Skip to content

Commit

Permalink
Merge pull request #25 from photovoltex/adjust-docs
Browse files Browse the repository at this point in the history
Adjust docs (move examples and explanation into code)
  • Loading branch information
photovoltex authored Mar 7, 2024
2 parents 6ccf8e2 + 2341e33 commit 0f9bb14
Show file tree
Hide file tree
Showing 18 changed files with 381 additions and 307 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ leptos = { version = "0.5", optional = true }
tauri = { version = "1.5", default-features = false, features = ["wry"] }

[target.'cfg(target_family = "wasm")'.dependencies]
tauri-interop-macro = { path = "./tauri-interop-macro", features = [ "wasm" ] }
tauri-interop-macro = { path = "./tauri-interop-macro", features = [ "_wasm" ] }
# tauri-interop-macro = { version = "2", features = [ "wasm" ] }

[target.'cfg(not(target_family = "wasm"))'.dev-dependencies]
Expand All @@ -51,4 +51,4 @@ default = [ "event" ]
event = [ "tauri-interop-macro/event" ]
leptos = [ "dep:leptos", "tauri-interop-macro/leptos" ]
# used to generate the missing documentation, otherwise it's only generated for "target_family = wasm"
wasm = []
_wasm = []
257 changes: 65 additions & 192 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,97 +4,58 @@
[![Documentation](https://docs.rs/tauri-interop/badge.svg)](https://docs.rs/tauri-interop)
![License](https://img.shields.io/crates/l/tauri-interop.svg)

What this crate tries to achieve:
- generate an equal wasm-function for your defined `tauri::command`
- collecting all defined `tauri::command`s without adding them manually
- a convenient way to send events from tauri and receiving them in the frontend

This crate tries to provide a general more enjoyable experience for developing tauri apps with a rust frontend.
> tbf it is a saner approach to write the app in a mix of js + rust, because the frameworks are more mature, there are
> way more devs who have experience with js and their respective frameworks etc...
>
> but tbh... just because something is saner, it doesn't stop us from doing things differently ^ヮ^
Writing an app in a single language gives us the option of building a common crate/module which connects the backend and
frontend. A common model itself can most of the time be easily compiled to both architectures (arch's) when the types
are compatible with both. The commands on the other hand don't have an option to be compiled to wasm. Which means they
need to be handled manually or be called via a wrapper/helper each time.

Repeating the implementation and handling for a function that is already defined properly seems to be a waste of time.
For that reason this crate provides the `tauri_interop::command` macro. This macro is explained in detail in the
[command representation](#command-representation-hostwasm) section. This new macro provides the option to invoke the
command in wasm and by therefore call the defined command in tauri. On the other side, when compiling for tauri in addition
to the tauri logic, the macro provides the option to collect all commands in a single file via the invocation of the
`tauri_interop::collect_commands` macro at the end of the file (see [command](#command-frontend--backend-communication)).

In addition, some quality-of-life macros are provided to ease some inconveniences when compiling to multiple arch's. See
the [QOL](#qol-macros) section.

**Feature `event`**:

Tauri has an [event](https://tauri.app/v1/guides/features/events) mechanic which allows the tauri side to communicate with
the frontend. The usage is not as intuitive and has to some inconveniences that make it quite hard to recommend. To
improve the usage, this crate provides the derive-marcos `Event`, `Emit` and `Listen`. The `Event` macro is just a
conditional wrapper that expands to `Emit` for the tauri compilation and `Listen` for the wasm compilation. It is
the intended way to use this feature. The usage is explained in the documentation of the `Event` macro.
section.

## Basic usage:

> **Disclaimer**:
>
> Some examples in this documentation can't be executed with doctests due to
> required wasm target and tauri modified environment (see [withGlobalTauri](https://tauri.app/v1/api/config/#buildconfig.withglobaltauri))
### QOL macros

This crate also adds some quality-of-life macros. These are intended to ease the drawbacks of compiling to
multiple architectures.

#### Conditional `use`
Because most crates are not intended to be compiled to wasm and most wasm crates are not intended to be compiled to
the host-triplet they have to be excluded in each others compile process. The usual process to exclude uses for a certain
architecture would look something like this:

```rust
#[cfg(not(target_family = "wasm"))]
use tauri::AppHandle;

#[tauri_interop::command]
pub fn empty_invoke(_handle: AppHandle) {}
```

**General usage:**

With the help of `tauri_interop::host_usage!()` and `tauri_interop::wasm_usage!()` we don't need to remember which
attribute we have to add and can just convert the above to the following:

```rust
tauri_interop::host_usage! {
use tauri::AppHandle;
}

#[tauri_interop::command]
pub fn empty_invoke(_handle: AppHandle) {}
```

**Multiple `use` usage:**

When multiple `use` should be excluded, they need to be separated by a single pipe (`|`). For example:

```rust
tauri_interop::host_usage! {
use tauri::State;
| use std::sync::RwLock;
}

#[tauri_interop::command]
pub fn empty_invoke(_state: State<RwLock<String>>) {}
```
> the required wasm target and tauri modified environment (see [withGlobalTauri](https://tauri.app/v1/api/config/#buildconfig.withglobaltauri))
### Command (Frontend => Backend Communication)
> For more examples see [cmd.rs](./test-project/api/src/cmd.rs) in test-project
Definition for both tauri supported triplet and wasm:
```rust , ignore-wasm32-unknown-unknown
#[tauri_interop::command]
fn greet(name: &str) -> String {
format!("Hello, {}! You've been greeted from Rust!", name)
}

// generated the `get_handlers()` function
tauri_interop::collect_commands!();

fn main() {
tauri::Builder::default()
// This is where you pass in the generated handler collector
.invoke_handler(get_handlers());
}
```

Using `tauri_interop::command` does two things:
- it provides the command with two macros which are used depending on the `target_family`
The newly provides macro `tauri_interop::command` does two things:
- it provides the function with two macros which are used depending on the targeted architecture
- `tauri_interop::binding` is used when compiling to `wasm`
- `tauri::command` is used otherwise
- it adds an entry to `tauri_interop::collect_commands!()` (see [collect commands](#collect-commands))
- additionally it provides the possibility to collect all defined commands via `tauri_interop::collect_commands!()`
- for more info see [collect commands](#collect-commands))
- the function is not generated when targeting `wasm`

The defined command above can then be used in wasm as below. Due to receiving data from
tauri via a promise, the command response has to be awaited.
The generated command can then be used in `wasm` like the following:
```rust , ignore
#[tauri_interop::command]
fn greet(name: &str) -> String {
fn greet(name: &str, _handle: tauri::AppHandle) -> String {
format!("Hello, {}! You've been greeted from Rust!", name)
}

Expand All @@ -108,7 +69,7 @@ fn main() {
}
```

#### Command representation Host/Wasm
**Command representation Host/Wasm (and a bit background knowledge)**

- the returned type of the wasm binding should be 1:1 the same type as send from the "backend"
- technically all commands need to be of type `Result<T, E>` because there is always the possibility of a command
Expand All @@ -123,36 +84,6 @@ fn main() {
- that also means, if you create a type alias for `Result<T, E>` and don't include `Result` in the name of the alias,
it will not map the `Result` correctly

```rust , ignore-wasm32-unknown-unknown
// let _: () = trigger_something();
#[tauri_interop::command]
fn trigger_something(name: &str) {
print!("triggers something, but doesn't need to wait for it")
}

// let value: String = wait_for_sync_execution("value").await;
#[tauri_interop::command]
fn wait_for_sync_execution(value: &str) -> String {
format!("Has to wait that the backend completes the computation and returns the {value}")
}

// let result: Result<String, String> = asynchronous_execution(true).await;
#[tauri_interop::command]
async fn asynchronous_execution(change: bool) -> Result<String, String> {
if change {
Ok("asynchronous execution requires result definition".into())
} else {
Err("and ".into())
}
}

// let _wait_for_completion: () = heavy_computation().await;
#[tauri_interop::command]
async fn heavy_computation() {
std::thread::sleep(std::time::Duration::from_millis(5000))
}
```

#### Collect commands

The `tauri_invoke::collect_commands` macro generates a `get_handlers` function in the current mod, which calls the
Expand All @@ -172,106 +103,48 @@ generates the usual `get_handlers` function, but with all "commands" defined ins

For an example see the [test-project/api/src/command.rs](test-project/api/src/command.rs).

### Event (Backend => Frontend Communication)
Definition for both tauri supported triplet and wasm:
```rust
use tauri_interop::Event;

#[derive(Default, Event)]
pub struct Test {
foo: String,
pub bar: bool,
}

// when main isn't defined, `super::Test` results in an error
fn main() {}
```

When using the derive macro `tauri_interop::Event` it expands depending on the `target_family` to
- derive trait `tauri_interop::Listen` (when compiling to `wasm`)
- derive trait `tauri_interop::Emit` (otherwise)
### QOL macros

To emit a variable from the above struct (which is mostly intended to be used as state) in the host triplet
```rust , ignore-wasm32-unknown-unknown
use tauri_interop::{Event, event::emit::Emit};
This crate also adds some quality-of-life macros. These are intended to ease the drawbacks of compiling to
multiple architectures.

#[derive(Default, Event)]
pub struct Test {
foo: String,
pub bar: bool,
}
#### Conditional `use`
Because most crates are not intended to be compiled to wasm and most wasm crates are not intended to be compiled to
the host-triplet they have to be excluded in each others compile process. The usual process to exclude uses for a certain
architecture would look something like this:

// via `tauri_interop::Emit` a new module named after the struct (as snake_case)
// is created where the struct Test is defined, here it creates module `test`
// in this module the related Fields are generated
```rust
#[cfg(not(target_family = "wasm"))]
use tauri::AppHandle;

// one context where `tauri::AppHandle` can be obtained
#[tauri_interop::command]
fn emit_bar(handle: tauri::AppHandle) {
let mut t = Test::default();

t.emit::<test::Foo>(&handle); // emits the current state: `false`
}

// a different context where `tauri::AppHandle` can be obtained
fn main() {
tauri::Builder::default()
.setup(|app| {
let handle: tauri::AppHandle = app.handle();

let mut t = Test::default();

// to emit and update a field an update function for each field is generated
t.update::<test::Foo>(&handle, "Bar".into()); // assigns "Bar" to t.foo and emits the same value

Ok(())
});
}
pub fn empty_invoke(_handle: AppHandle) {}
```

the above emitted value can then be received in wasm as:
```rust , ignore
use tauri_interop::Event;

#[derive(Default, Event)]
pub struct Test {
foo: String,
pub bar: bool,
}
**General usage:**

async fn main() {
use tauri_interop::event::listen::Listen;
With the help of `tauri_interop::host_usage!()` and `tauri_interop::wasm_usage!()` we don't need to remember which
attribute we have to add and can just convert the above to the following:

let _listen_handle: ListenHandle<'_> = Test::listen_to::<test::Foo>(|foo| { /* use received foo: String here */ }).await;
```rust
tauri_interop::host_usage! {
use tauri::AppHandle;
}
```

The `ListenHandle` contains the provided closure and the "unlisten" method. It has to be hold in scope as long
as the event should be received. Dropping it will automatically detach the closure from the event. See
[cmd.rs](./test-project/api/src/cmd.rs) for other example how it could be used.
#[tauri_interop::command]
pub fn empty_invoke(_handle: AppHandle) {}
```

#### Feature: leptos
When the `leptos` feature is enabled the `use_field` method is added to the `Listen` trait when compiling to wasm.
The method takes care of the initial asynchronous call to register the listener and will hold the handle in scope
as long as the leptos component is rendered.
**Multiple `use` usage:**

```rust , ignore
use tauri_interop::Event;
When multiple `use` should be excluded, they need to be separated by a single pipe (`|`). For example:

#[derive(Default, Event)]
pub struct Test {
foo: String,
pub bar: bool,
```rust
tauri_interop::host_usage! {
use tauri::State;
| use std::sync::RwLock;
}

fn main() {
use tauri_interop::event::listen::Listen;

let foo: leptos::ReadSignal<String> = Test::use_field::<test::Foo>(String::default());
}
#[tauri_interop::command]
pub fn empty_invoke(_state: State<RwLock<String>>) {}
```

## Known Issues:
- feature: leptos
- sometimes a closure is accessed after being dropped
- that is probably a race condition where the unlisten function doesn't detach the callback fast enough
2 changes: 1 addition & 1 deletion src/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
pub use type_aliases::*;

/// wasm bindings for tauri's provided js functions (target: `wasm` or feat: `wasm`)
#[cfg(any(target_family = "wasm", feature = "wasm"))]
#[cfg(any(target_family = "wasm", feature = "_wasm"))]
pub mod bindings;

#[cfg(not(target_family = "wasm"))]
Expand Down
12 changes: 12 additions & 0 deletions src/command/bindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,27 @@ use wasm_bindgen::prelude::*;

#[wasm_bindgen]
extern "C" {
/// Fire and forget invoke/command call
///
/// [Tauri Commands](https://tauri.app/v1/guides/features/command)
#[wasm_bindgen(js_name = "invoke", js_namespace = ["window", "__TAURI__", "tauri"])]
pub fn invoke(cmd: &str, args: JsValue);

/// [invoke] variant that awaits the returned value
///
/// [Async Commands](https://tauri.app/v1/guides/features/command/#async-commands)
#[wasm_bindgen(js_name = "invoke", js_namespace = ["window", "__TAURI__", "tauri"])]
pub async fn async_invoke(cmd: &str, args: JsValue) -> JsValue;

/// [async_invoke] variant that additionally returns a possible error
///
/// [Error Handling](https://tauri.app/v1/guides/features/command/#error-handling)
#[wasm_bindgen(catch, js_name = "invoke", js_namespace = ["window", "__TAURI__", "tauri"])]
pub async fn invoke_catch(cmd: &str, args: JsValue) -> Result<JsValue, JsValue>;

/// The binding for the frontend that listens to events
///
/// [Events](https://tauri.app/v1/guides/features/events)
#[cfg(feature = "event")]
#[wasm_bindgen(catch, js_namespace = ["window", "__TAURI__", "event"])]
pub async fn listen(
Expand Down
9 changes: 6 additions & 3 deletions src/command/type_aliases.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
use tauri::{AppHandle, State, Window};

/// Type alias to easier identify [State] via [tauri_interop_macro::command] macro
#[allow(unused_imports)]
use tauri_interop_macro::command;

/// Type alias to easier identify [State] via [command] macro
pub type TauriState<'r, T> = State<'r, T>;

/// Type alias to easier identify [Window] via [tauri_interop_macro::command] macro
/// Type alias to easier identify [Window] via [command] macro
pub type TauriWindow = Window;

/// Type alias to easier identify [AppHandle] via [tauri_interop_macro::command] macro
/// Type alias to easier identify [AppHandle] via [command] macro
pub type TauriAppHandle = AppHandle;
Loading

0 comments on commit 0f9bb14

Please sign in to comment.