Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Feature: Initial value #28

Merged
merged 5 commits into from
Mar 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@ jobs:
- name: Run tests for crate
run: |
cd tauri-interop-macro
cargo test
cargo test --features event,leptos,initial_value

- name: Run tests for crate
run: cargo test
run: cargo test --all-features

- name: Build test-project (wasm)
run: |
Expand Down
15 changes: 8 additions & 7 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
[workspace]
members = [ "tauri-interop-macro" ]
members = ["tauri-interop-macro"]
package.edition = "2021"
package.version = "2.0.0-dev"
package.keywords = [ "wasm", "tauri", "command", "event", "leptos" ]
package.authors = [ "photovoltex" ]
package.keywords = ["wasm", "tauri", "command", "event", "leptos"]
package.authors = ["photovoltex"]
package.repository = "https://github.com/photovoltex/tauri-interop.git"
package.license = "MIT OR Apache-2.0"

Expand Down Expand Up @@ -39,14 +39,15 @@ 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]
tauri = "1.5"

[features]
# todo: remove default feature before publish
default = [ "event" ]
event = [ "tauri-interop-macro/event" ]
leptos = [ "dep:leptos", "tauri-interop-macro/leptos" ]
default = ["event"]
event = ["tauri-interop-macro/event"]
initial_value = ["tauri-interop-macro/initial_value"]
leptos = ["dep:leptos", "tauri-interop-macro/leptos"]
2 changes: 1 addition & 1 deletion src/command/type_aliases.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use tauri::{AppHandle, State, Window};

#[allow(unused_imports)]
#[cfg(doc)]
use tauri_interop_macro::command;

/// Type alias to easier identify [State] via [command] macro
Expand Down
36 changes: 17 additions & 19 deletions src/event.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use serde::{de::DeserializeOwned, Serialize};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
#[cfg(not(target_family = "wasm"))]
use tauri::{AppHandle, Error, Wry};

Expand All @@ -15,35 +15,25 @@ mod emit;
#[cfg(any(target_family = "wasm", doc))]
mod listen;

/// The trait which needs to be implemented for a [Field]
///
/// Conditionally changes between [Listen] and [Emit]
///
/// When compiled to "target_family = wasm" then following is true.
/// ```ignore
/// trait Parent = listen::Listen;
/// ```
#[cfg(not(target_family = "wasm"))]
pub trait Parent = Emit;

/// The trait which needs to be implemented for a [Field]
///
/// Conditionally changes between [Listen] and [Emit]
#[cfg(target_family = "wasm")]
pub trait Parent = Listen;

/// Trait defining a [Field] to a related struct implementing [Parent] with the related [Field::Type]
pub trait Field<P>
where
P: Parent,
<Self as Field<P>>::Type: Clone + Serialize + DeserializeOwned,
Self::Type: Default + Clone + Serialize + DeserializeOwned,
{
/// The type of the field
type Type;

/// The event of the field
const EVENT_NAME: &'static str;

/// Tries to retrieve the current value from the backend
///
/// only in wasm available
#[allow(async_fn_in_trait)]
#[cfg(any(all(target_family = "wasm", feature = "initial_value"), doc))]
async fn get_value() -> Result<Self::Type, EventError>;

#[cfg(not(target_family = "wasm"))]
/// Emits event of the related field with their value
///
Expand All @@ -56,3 +46,11 @@ where
/// not in wasm available
fn update(s: &mut P, handle: &AppHandle<Wry>, v: Self::Type) -> Result<(), Error>;
}

/// General errors that can happen during event exchange
#[derive(Debug, Serialize, Deserialize, thiserror::Error)]
pub enum EventError {
/// The given name (struct) is not as tauri::State registered
#[error("{0} is not as tauri state registered")]
StateIsNotRegistered(String),
}
54 changes: 52 additions & 2 deletions src/event/emit.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,49 @@
use tauri::{AppHandle, Error, Wry};

use super::Field;
#[cfg(doc)]
use super::Listen;

/// The trait which needs to be implemented for a [Field]
///
/// Conditionally changes between [Listen] and [Emit] or [ManagedEmit]
///
/// - When compiled to "target_family = wasm", the trait alias is set to [Listen]
/// - When feature "initial_value" is enabled, the trait alias is set to [ManagedEmit]
/// - Otherwise the trait alias is set to [Emit]
#[cfg(any(not(feature = "initial_value"), doc))]
pub trait Parent = Emit;

/// The trait which needs to be implemented for a [Field]
#[cfg(all(feature = "initial_value", not(doc)))]
pub trait Parent = ManagedEmit;

/// Extension of [Emit] to additionally require [Self] to be managed by tauri
#[cfg(feature = "initial_value")]
pub trait ManagedEmit: Emit
where
Self: 'static,
{
/// Gets the value of a [Field] from [AppHandle]
///
/// The default implementation acquires [Self] directly. Override the provided
/// method when [Self] is not directly managed. For example, this could be the
/// case when the [interior mutability](https://doc.rust-lang.org/reference/interior-mutability.html)
/// pattern is used to allow mutation of [Self] while being managed by tauri.
fn get_value<F: Field<Self>>(
handle: &AppHandle,
get_field_value: impl Fn(&Self) -> F::Type,
) -> Option<F::Type>
where
Self: Sized + Send + Sync,
{
use tauri::Manager;

let state = handle.try_state::<Self>()?;
let state = get_field_value(&state);
Some(state)
}
}

/// Trait that defines the available event emitting methods
pub trait Emit {
Expand All @@ -16,6 +59,8 @@ pub trait Emit {
/// foo: String,
/// pub bar: bool,
/// }
///
/// impl tauri_interop::event::ManagedEmit for Test {}
///
/// #[tauri_interop::command]
/// fn emit_bar(handle: TauriAppHandle) {
Expand All @@ -39,6 +84,8 @@ pub trait Emit {
/// pub bar: bool,
/// }
///
/// impl tauri_interop::event::ManagedEmit for Test {}
///
/// #[tauri_interop::command]
/// fn emit_bar(handle: TauriAppHandle) {
/// Test::default().emit::<test::Foo>(&handle).expect("emitting failed");
Expand All @@ -48,7 +95,7 @@ pub trait Emit {
/// ```
fn emit<F: Field<Self>>(&self, handle: &AppHandle<Wry>) -> Result<(), Error>
where
Self: Sized + Emit;
Self: Sized + Parent;

/// Update a single field and emit it afterward
///
Expand All @@ -63,6 +110,9 @@ pub trait Emit {
/// pub bar: bool,
/// }
///
/// // require because we compile
/// impl tauri_interop::event::ManagedEmit for Test {}
///
/// #[tauri_interop::command]
/// fn emit_bar(handle: TauriAppHandle) {
/// Test::default().update::<test::Bar>(&handle, true).expect("emitting failed");
Expand All @@ -76,5 +126,5 @@ pub trait Emit {
field: F::Type,
) -> Result<(), Error>
where
Self: Sized + Emit;
Self: Sized + Parent;
}
34 changes: 26 additions & 8 deletions src/event/listen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,16 @@ use wasm_bindgen::{closure::Closure, JsCast, JsValue};

use crate::command::bindings::listen;

#[cfg(doc)]
use super::Emit;
use super::Field;

/// The trait which needs to be implemented for a [Field]
///
/// Conditionally changes between [Listen] and [Emit]
#[cfg(target_family = "wasm")]
pub trait Parent = Listen;

/// The result type that is returned by [ListenHandle::register]
pub type ListenResult = Result<ListenHandle, ListenError>;

Expand Down Expand Up @@ -87,22 +95,32 @@ impl ListenHandle {

/// Registers a given event and binds a returned signal to these event changes
///
/// Providing [None] will unwrap into the default value. When feature `initial_value`
/// is enabled [None] will try to get the value from tauri.
///
/// Internally it stores a created [ListenHandle] for `event` in a [leptos::RwSignal] to hold it in
/// scope, while it is used in a leptos [component](https://docs.rs/leptos_macro/0.5.2/leptos_macro/attr.component.html)
#[cfg(feature = "leptos")]
pub fn use_register<T>(event: &'static str, initial_value: T) -> ReadSignal<T>
where
T: DeserializeOwned,
pub fn use_register<P, F: Field<P>>(initial_value: Option<F::Type>) -> ReadSignal<F::Type>
where P: Sized + super::Parent
{
use leptos::SignalSet;

let (signal, set_signal) = leptos::create_signal(initial_value);
let acquire_initial_value = initial_value.is_none();
let (signal, set_signal) = leptos::create_signal(initial_value.unwrap_or_default());

// creating this signal in a leptos component holds the value in scope, and drops it automatically
let handle = leptos::create_rw_signal(None);
leptos::spawn_local(async move {
let listen_handle = ListenHandle::register(event, move |value: T| {
log::trace!("update for {}", event);
if cfg!(feature = "initial_value") && acquire_initial_value {
match F::get_value().await {
Ok(value) => set_signal.set(value),
Err(why) => log::error!("{why}")
}
}

let listen_handle = ListenHandle::register(F::EVENT_NAME, move |value: F::Type| {
log::trace!("update for {}", F::EVENT_NAME);
set_signal.set(value)
})
.await
Expand Down Expand Up @@ -172,10 +190,10 @@ pub trait Listen {
/// }
/// ```
#[cfg(feature = "leptos")]
fn use_field<F: Field<Self>>(initial: F::Type) -> ReadSignal<F::Type>
fn use_field<F: Field<Self>>(initial: Option<F::Type>) -> ReadSignal<F::Type>
where
Self: Sized + super::Parent,
{
ListenHandle::use_register(F::EVENT_NAME, initial)
ListenHandle::use_register::<Self, F>(initial)
}
}
5 changes: 3 additions & 2 deletions tauri-interop-macro/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,13 @@ log = "^0.4"
serde = "^1.0"
# required because the intented usage is to use the main crate,
# for testing we need the reexported macros from tauri-interop
tauri-interop = { path = ".." }
tauri-interop = { path = "..", features = ["initial_value"] }

[features]
# todo: remove default feature before publish
default = [ "event" ]
leptos = [ "event" ]
event = []
leptos = []
initial_value = []
# feature to get info that context is wasm
_wasm = []
3 changes: 3 additions & 0 deletions tauri-interop-macro/src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ struct Field {
name: Ident,
attributes: FieldAttributes,
event_name: String,
get_cmd: Ident,
}

struct FieldAttributes {
Expand Down Expand Up @@ -117,10 +118,12 @@ fn prepare_field(derive_input: DeriveInput) -> Field {
let name = derive_input.ident.clone();
let attributes = get_field_values(derive_input.attrs);
let event_name = format!("{}::{}", &attributes.parent, &name);
let get_cmd = format_ident!("get_{}_{}", &attributes.parent, name);

Field {
event_name,
name,
attributes,
get_cmd
}
}
27 changes: 25 additions & 2 deletions tauri-interop-macro/src/event/emit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,21 @@ pub fn derive(stream: TokenStream) -> TokenStream {
});

let event_fields = fields.iter().map(|field| &field.field_name);
let commands_attr = cfg!(feature = "initial_value")
.then_some(quote!(#[::tauri_interop::commands]))
.unwrap_or_default();
let collect_command = cfg!(feature = "initial_value")
.then_some(quote!(::tauri_interop::collect_commands!();))
.unwrap_or_default();

let stream = quote! {
#commands_attr
pub mod #mod_name {
use super::#name;
use tauri_interop::event::{Field, Emit};

#( #emit_fields )*

#collect_command
}

impl ::tauri_interop::event::Emit for #name {
Expand Down Expand Up @@ -78,6 +86,7 @@ pub fn derive_field(stream: TokenStream) -> TokenStream {
name,
attributes,
event_name,
get_cmd,
} = super::prepare_field(derive_input);

let FieldAttributes {
Expand All @@ -90,8 +99,20 @@ pub fn derive_field(stream: TokenStream) -> TokenStream {
.as_ref()
.expect("name attribute was expected");

let get_cmd = cfg!(feature = "initial_value").then_some(quote! {
#[allow(non_snake_case)]
#[tauri_interop::command]
pub fn #get_cmd(handle: ::tauri::AppHandle) -> Result<#parent_field_ty, ::tauri_interop::event::EventError> {
use ::tauri::Manager;
use ::tauri_interop::event::{Field, ManagedEmit, EventError};

#parent::get_value::<#name>(&handle, |parent| parent.#parent_field_name.clone())
.ok_or(EventError::StateIsNotRegistered(stringify!(#parent).into()))
}
}).unwrap_or_default();

let stream = quote! {
impl Field<#parent> for #name {
impl ::tauri_interop::event::Field<#parent> for #name {
type Type = #parent_field_ty;

const EVENT_NAME: &'static str = #event_name;
Expand All @@ -109,6 +130,8 @@ pub fn derive_field(stream: TokenStream) -> TokenStream {
Self::emit(parent, handle)
}
}

#get_cmd
};

TokenStream::from(stream.to_token_stream())
Expand Down
Loading