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

Make bound event listeners Copy #85

Merged
merged 2 commits into from
May 25, 2023
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
6 changes: 3 additions & 3 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 crates/kobold/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "kobold"
version = "0.8.1"
version = "0.9.0"
authors = ["Maciej Hirsz <[email protected]>"]
edition = "2021"
license = "MPL-2.0"
Expand All @@ -19,7 +19,7 @@ stateful = []
wasm-bindgen = "0.2.84"
wasm-bindgen-futures = "0.4.34"
itoa = "1.0.6"
kobold_macros = { version = "0.8.0", path = "../kobold_macros" }
kobold_macros = { version = "0.9.0", path = "../kobold_macros" }
console_error_panic_hook = "0.1.7"
rlsf = { version = "0.2.1", optional = true }

Expand Down
18 changes: 18 additions & 0 deletions crates/kobold/src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,24 @@ event! {
MouseEvent,
}

pub trait IntoListener<E: EventCast> {
type Listener: Listener<E>;

fn into_listener(self) -> Self::Listener;
}

impl<E, L> IntoListener<E> for L
where
L: Listener<E>,
E: EventCast,
{
type Listener = L;

fn into_listener(self) -> L {
self
}
}

pub trait Listener<E>
where
E: EventCast,
Expand Down
4 changes: 2 additions & 2 deletions crates/kobold/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -585,7 +585,7 @@ macro_rules! class {
/// let increment = move |_| *count += 1;
/// let decrement = move |_| *count -= 1;
/// }
/// # fn throwaway(_: impl kobold::event::Listener<kobold::reexport::web_sys::Event>) {}
/// # fn throwaway(_: kobold::stateful::Bound<i32, impl FnMut(&mut i32, kobold::reexport::web_sys::Event)>) {}
/// # throwaway(increment);
/// # throwaway(decrement);
/// # }
Expand All @@ -597,7 +597,7 @@ macro_rules! class {
/// # fn test(count: &Hook<i32>) {
/// let increment = count.bind(move |count, _| *count += 1);
/// let decrement = count.bind(move |count, _| *count -= 1);
/// # fn throwaway(_: impl kobold::event::Listener<kobold::reexport::web_sys::Event>) {}
/// # fn throwaway(_: kobold::stateful::Bound<i32, impl FnMut(&mut i32, kobold::reexport::web_sys::Event)>) {}
/// # throwaway(increment);
/// # throwaway(decrement);
/// # }
Expand Down
2 changes: 1 addition & 1 deletion crates/kobold/src/stateful.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ mod should_render;
use cell::WithCell;
use product::{Product, ProductHandler};

pub use hook::{Hook, Signal};
pub use hook::{Bound, Hook, Signal};
pub use into_state::IntoState;
pub use should_render::{ShouldRender, Then};

Expand Down
113 changes: 91 additions & 22 deletions crates/kobold/src/stateful/hook.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ impl<S> Hook<S> {

/// Binds a closure to a mutable reference of the state. While this method is public
/// it's recommended to use the [`bind!`](crate::bind) macro instead.
pub fn bind<E, F, O>(&self, callback: F) -> impl Listener<E>
pub fn bind<E, F, O>(&self, callback: F) -> Bound<S, F>
where
S: 'static,
E: EventCast,
Expand All @@ -102,25 +102,7 @@ impl<S> Hook<S> {
{
let inner = &self.inner as *const Inner<S>;

let bound = move |e| {
// ⚠️ Safety:
// ==========
//
// This is fired only as event listener from the DOM, which guarantees that
// state is not currently borrowed, as events cannot interrupt normal
// control flow, and `Signal`s cannot borrow state across .await points.
let inner = unsafe { &*inner };
let state = unsafe { inner.state.mut_unchecked() };

if callback(state, e).should_render() {
inner.update();
}
};

Bound {
bound,
_unbound: PhantomData::<F>,
}
Bound { inner, callback }
}

pub fn bind_async<E, F, T>(&self, callback: F) -> impl Listener<E>
Expand Down Expand Up @@ -162,12 +144,63 @@ impl<S> Hook<S> {
}
}

struct Bound<B, U> {
pub struct Bound<S, F> {
inner: *const Inner<S>,
callback: F,
}

impl<S, F> Bound<S, F> {
pub fn into_listener<E, O>(self) -> impl Listener<E>
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once return position impl Trait in traits becomes stable this can become part of the IntoListener<E> trait.

where
S: 'static,
E: EventCast,
F: Fn(&mut S, E) -> O + 'static,
O: ShouldRender,
{
let Bound { inner, callback } = self;

let bound = move |e| {
// ⚠️ Safety:
// ==========
//
// This is fired only as event listener from the DOM, which guarantees that
// state is not currently borrowed, as events cannot interrupt normal
// control flow, and `Signal`s cannot borrow state across .await points.
let inner = unsafe { &*inner };
let state = unsafe { inner.state.mut_unchecked() };

if callback(state, e).should_render() {
inner.update();
}
};

BoundListener {
bound,
_unbound: PhantomData::<F>,
}
}
}

impl<S, F> Clone for Bound<S, F>
where
F: Clone,
{
fn clone(&self) -> Self {
Bound {
inner: self.inner,
callback: self.callback.clone(),
}
}
}

impl<S, F> Copy for Bound<S, F> where F: Copy {}

struct BoundListener<B, U> {
bound: B,
_unbound: PhantomData<U>,
}

impl<B, U, E> Listener<E> for Bound<B, U>
impl<B, U, E> Listener<E> for BoundListener<B, U>
where
B: Listener<E>,
E: EventCast,
Expand Down Expand Up @@ -216,3 +249,39 @@ where
(**self).update(p)
}
}

#[cfg(test)]
mod test {
use std::cell::UnsafeCell;
use wasm_bindgen::JsCast;

use crate::stateful::cell::WithCell;
use crate::stateful::product::ProductHandler;
use crate::value::TextProduct;

use super::*;

#[test]
fn bound_callback_is_copy() {
let inner = Inner {
state: WithCell::new(0_i32),
prod: UnsafeCell::new(ProductHandler::mock(
|_, _| {},
TextProduct {
memo: 0,
node: wasm_bindgen::JsValue::UNDEFINED.unchecked_into(),
},
)),
};

let mock = Bound {
inner: &inner as *const _,
callback: |state: &mut i32, _: web_sys::Event| {
*state += 1;
},
};

// Make sure we can copy the mock twice
drop([mock, mock]);
}
}
11 changes: 11 additions & 0 deletions crates/kobold/src/stateful/product.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,17 @@ pub struct ProductHandler<S, P, F> {
_state: PhantomData<S>,
}

#[cfg(test)]
impl<S, P, F> ProductHandler<S, P, F> {
pub(crate) fn mock(updater: F, product: P) -> Self {
ProductHandler {
updater,
product,
_state: PhantomData,
}
}
}

impl<S, P, F> ProductHandler<S, P, F> {
pub fn build<V>(updater: F, view: V, p: In<Self>) -> Out<Self>
where
Expand Down
4 changes: 2 additions & 2 deletions crates/kobold/src/value.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@ impl_value!(bool: bool);
impl_value!(f64: u8, u16, u32, usize, i8, i16, i32, isize, f32, f64);

pub struct TextProduct<M> {
memo: M,
node: Node,
pub(crate) memo: M,
pub(crate) node: Node,
}

impl<M> Anchor for TextProduct<M> {
Expand Down
2 changes: 1 addition & 1 deletion crates/kobold_macros/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "kobold_macros"
version = "0.8.0"
version = "0.9.0"
authors = ["Maciej Hirsz <[email protected]>"]
edition = "2021"
license = "MPL-2.0"
Expand Down
2 changes: 1 addition & 1 deletion crates/kobold_macros/src/gen/element.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ impl IntoGenerator for HtmlElement {
expr.stream,
)
} else {
expr.stream
(expr.stream, ".into_listener()").tokenize()
};

let value = gen.add_field(coerce).event(event, el.typ).name;
Expand Down
1 change: 1 addition & 0 deletions crates/kobold_macros/src/gen/transient.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ impl Tokenize for Transient {
"\
use ::kobold::dom::Mountable as _;\
use ::kobold::event::ListenerHandle as _;\
use ::kobold::event::IntoListener as _;\
use ::kobold::reexport::wasm_bindgen;\
",
self.js,
Expand Down
4 changes: 2 additions & 2 deletions crates/kobold_qr/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "kobold_qr"
version = "0.8.0"
version = "0.9.0"
authors = ["Maciej Hirsz <[email protected]>"]
edition = "2021"
license = "MPL-2.0"
Expand All @@ -10,7 +10,7 @@ description = "QR code component for Kobold"

[dependencies]
fast_qr = "0.8.5"
kobold = { version = "0.8.0", path = "../kobold" }
kobold = { version = "0.9.0", path = "../kobold" }
wasm-bindgen = "0.2.84"

[dependencies.web-sys]
Expand Down