Skip to content
Snippets Groups Projects
Unverified Commit 4bf5e808 authored by John's avatar John Committed by GitHub
Browse files

Merge pull request #45 from MrGVSV/use-effect

Added `use_effect` hook macro
parents 6c01464f beae37c8
No related branches found
No related tags found
No related merge requests found
...@@ -88,7 +88,7 @@ Widget's can create their own state and will re-render when that state changes. ...@@ -88,7 +88,7 @@ Widget's can create their own state and will re-render when that state changes.
```rust ```rust
#[widget] #[widget]
fn Counter(context: &mut KayakContext) { fn Counter(context: &mut KayakContext) {
let (count, set_count) = use_state!(0i32); let (count, set_count, ..) = use_state!(0i32);
let on_event = OnEvent::new(move |_, event| match event.event_type { let on_event = OnEvent::new(move |_, event| match event.event_type {
EventType::Click => set_count(count + 1), EventType::Click => set_count(count + 1),
_ => {} _ => {}
......
...@@ -33,7 +33,7 @@ fn Counter(context: &mut KayakContext) { ...@@ -33,7 +33,7 @@ fn Counter(context: &mut KayakContext) {
..Default::default() ..Default::default()
}; };
let (count, set_count) = use_state!(0i32); let (count, set_count, ..) = use_state!(0i32);
let on_event = OnEvent::new(move |_, event| match event.event_type { let on_event = OnEvent::new(move |_, event| match event.event_type {
EventType::Click => set_count(count + 1), EventType::Click => set_count(count + 1),
_ => {} _ => {}
......
//! This example file demonstrates a few of the most common "hooks" used in this crate. For Kayak, a hook works much like
//! hooks in React: they hook into the lifecycle of their containing widget allowing deeper control over a widget's internal
//! logic.
//!
//! By convention, the macro "hooks" all start with the prefix `use_` (e.g., `use_state`). Another important thing to keep
//! in mind with these hooks are that they are just macros. They internally setup a lot of boilerplate code for you so that
//! you don't have to do it all manually. This means, though, that they may add variables or rely on external ones (many
//! hooks rely on the existence of a `KayakContext` instance named `context`, which is automatically inserted for every
//! widget, but could unintentionally be overwritten by a user-defined `context` variable). So be mindful of this when adding
//! these hooks to your widget— though issues regarding this should be fairly rare.
//!
use bevy::{
prelude::{App as BevyApp, AssetServer, Commands, Res, ResMut},
window::WindowDescriptor,
DefaultPlugins,
};
use kayak_ui::{
bevy::{BevyContext, BevyKayakUIPlugin, FontMapping, UICameraBundle},
core::{EventType, Index, OnEvent, render, rsx, use_effect, use_state, widget},
widgets::{App, Button, Text, Window},
};
/// A simple widget that tracks how many times a button is clicked using simple state data
#[widget]
fn StateCounter() {
// On its own, a widget can't track anything, since every value will just be reset when the widget is re-rendered.
// To get around this, and keep track of a value, we have to use states. States are values that are kept across renders.
// Additionally, anytime a state is updated with a new value, it causes the containing widget to re-render, making it
// useful for updating part of the UI with its value.
// To create a state, we can use the `use_state` macro. This creates a state with a given initial value, returning
// a tuple of its currently stored value and a closure for setting the stored value.
// Here, we create a state with an initial value of 0. Right now the value of `count` is 0. If we call `set_count(10)`,
// then the new value of `count` will be 10.
let (count, set_count, ..) = use_state!(0);
// We can create an event callback that uodates the state using the state variables defined above.
// Keep the borrow checker in mind! We can pass both `count` and `set_count` to this closure because they
// both implement `Copy`. For other types, you may have to clone the state to pass it into a closure like this.
// (You can also clone the setter as well if you need to use it in multiple places.)
let on_event = OnEvent::new(move |_, event| match event.event_type {
EventType::Click => set_count(count + 1),
_ => {}
});
rsx! {
<Window position={(50.0, 50.0)} size={(300.0, 150.0)} title={"State Example".to_string()}>
<Text size={16.0} content={format!("Current Count: {}", count)} />
<Button on_event={Some(on_event)}>
<Text size={24.0} content={"Count!".to_string()} />
</Button>
</Window>
}
}
/// Another widget that tracks how many times a button is clicked using side-effects
#[widget]
fn EffectCounter() {
// In this widget, we're going to implement another counter, but this time using side-effects.
// To put it very simply, a side-effect is when something happens in response to something else happening.
// In our case, we want to create a side-effect that updates a counter when another state is updated.
// In order to create this side-effect, we need access to the raw state binding. This is easily done by using
// the third field in the tuple returned from the `use_state` macro.
let (count, set_count, raw_count) = use_state!(0);
let on_event = OnEvent::new(move |_, event| match event.event_type {
EventType::Click => set_count(count + 1),
_ => {}
});
// This is the state our side-effect will update in response to changes on `raw_count`.
let (effect_count, set_effect_count, ..) = use_state!(0);
// This hook defines a side-effect that calls a function only when one of its dependencies is updated.
// They will also always run upon first render (i.e., when the widget is first added to the layout).
use_effect!(
move || {
// Update the `effect_count` state with the current `raw_count` value, multiplied by 2.
// Notice that we use `raw_count.get()` instead of `count`. This is because the closure is only defined once.
// This means that `count` will always be stuck at 0, as far as this hook is concerned. The solution is to
// use the `get` method on the raw state binding instead, to get the actual value.
set_effect_count(raw_count.get() * 2);
},
// In order to call this side-effect closure whenever `raw_count` updates, we need to pass it in as a dependency.
// Don't worry about the borrow checker here, `raw_count` is automatically cloned internally, so you don't need
// to do that yourself.
[raw_count]
// IMPORTANT:
// If a side-effect updates some other state, make sure you do not pass that state in as a dependency unless you have
// some checks in place to prevent an infinite loop!
);
// Passing an empty dependency array causes the callback to only run a single time: when the widget is first rendered.
use_effect!(|| {
println!("First!");
}, []);
// Additionally, order matters with these side-effects. They will be ran in the order they are defined.
use_effect!(|| {
println!("Second!");
}, []);
rsx! {
<Window position={(50.0, 225.0)} size={(300.0, 150.0)} title={"Effect Example".to_string()}>
<Text size={16.0} content={format!("Actual Count: {}", count)} />
<Text size={16.0} content={format!("Doubled Count: {}", effect_count)} />
<Button on_event={Some(on_event)}>
<Text size={24.0} content={"Count!".to_string()} />
</Button>
</Window>
}
}
fn startup(
mut commands: Commands,
mut font_mapping: ResMut<FontMapping>,
asset_server: Res<AssetServer>,
) {
commands.spawn_bundle(UICameraBundle::new());
font_mapping.add(asset_server.load("roboto.kayak_font"));
let context = BevyContext::new(|context| {
render! {
<App>
<StateCounter />
<EffectCounter />
</App>
}
});
commands.insert_resource(context);
}
fn main() {
BevyApp::new()
.insert_resource(WindowDescriptor {
width: 1270.0,
height: 720.0,
title: String::from("UI Example"),
..Default::default()
})
.add_plugins(DefaultPlugins)
.add_plugin(BevyKayakUIPlugin)
.add_startup_system(startup)
.run();
}
\ No newline at end of file
...@@ -10,7 +10,7 @@ use kayak_ui::widgets::{Background, Text}; ...@@ -10,7 +10,7 @@ use kayak_ui::widgets::{Background, Text};
#[widget] #[widget]
pub fn AddButton(children: Children, styles: Option<Style>) { pub fn AddButton(children: Children, styles: Option<Style>) {
let (color, set_color) = use_state!(Color::new(0.0781, 0.0898, 0.101, 1.0)); let (color, set_color, ..) = use_state!(Color::new(0.0781, 0.0898, 0.101, 1.0));
let base_styles = styles.clone().unwrap_or_default(); let base_styles = styles.clone().unwrap_or_default();
*styles = Some(Style { *styles = Some(Style {
......
...@@ -10,7 +10,7 @@ use kayak_ui::widgets::{Background, Text}; ...@@ -10,7 +10,7 @@ use kayak_ui::widgets::{Background, Text};
#[widget] #[widget]
pub fn DeleteButton(children: Children, styles: Option<Style>) { pub fn DeleteButton(children: Children, styles: Option<Style>) {
let (color, set_color) = use_state!(Color::new(0.0781, 0.0898, 0.101, 1.0)); let (color, set_color, ..) = use_state!(Color::new(0.0781, 0.0898, 0.101, 1.0));
let base_styles = styles.clone().unwrap_or_default(); let base_styles = styles.clone().unwrap_or_default();
*styles = Some(Style { *styles = Some(Style {
......
...@@ -25,7 +25,7 @@ pub struct Todo { ...@@ -25,7 +25,7 @@ pub struct Todo {
#[widget] #[widget]
fn TodoApp() { fn TodoApp() {
let (todos, set_todos) = use_state!(vec![ let (todos, set_todos, ..) = use_state!(vec![
Todo { Todo {
name: "Use bevy to make a game!".to_string(), name: "Use bevy to make a game!".to_string(),
}, },
...@@ -37,7 +37,7 @@ fn TodoApp() { ...@@ -37,7 +37,7 @@ fn TodoApp() {
}, },
]); ]);
let (new_todo_value, set_new_todo_value) = use_state!("".to_string()); let (new_todo_value, set_new_todo_value, ..) = use_state!("".to_string());
let text_box_styles = Style { let text_box_styles = Style {
right: StyleProp::Value(Units::Pixels(10.0)), right: StyleProp::Value(Units::Pixels(10.0)),
......
use std::time::Instant; use std::time::Instant;
pub use flo_binding::{bind, notify, Binding, Bound, Changeable, MutableBound, Releasable}; pub use flo_binding::{bind, computed, notify, Binding, Bound, Changeable, ComputedBinding, MutableBound, Releasable};
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Debug, Clone, Copy, PartialEq)]
pub struct Debouncer { pub struct Debouncer {
......
use crate::{Binding, Changeable}; use crate::{Binding, Changeable};
use std::collections::HashMap; use std::collections::HashMap;
use crate::{multi_state::MultiState, widget_manager::WidgetManager, Index, InputEvent, Releasable}; use crate::{multi_state::MultiState, widget_manager::WidgetManager, Index, InputEvent, MutableBound, Releasable};
use crate::event_dispatcher::EventDispatcher; use crate::event_dispatcher::EventDispatcher;
pub struct KayakContext { pub struct KayakContext {
widget_states: HashMap<crate::Index, resources::Resources>, widget_states: HashMap<crate::Index, resources::Resources>,
widget_effects: HashMap<crate::Index, resources::Resources>,
/// Contains provider state data to be accessed by consumers. /// Contains provider state data to be accessed by consumers.
/// ///
/// Maps the type of the data to a mapping of the provider node's ID to the state data /// Maps the type of the data to a mapping of the provider node's ID to the state data
...@@ -20,6 +21,7 @@ pub struct KayakContext { ...@@ -20,6 +21,7 @@ pub struct KayakContext {
global_state: resources::Resources, global_state: resources::Resources,
last_state_type_id: Option<std::any::TypeId>, last_state_type_id: Option<std::any::TypeId>,
current_state_index: usize, current_state_index: usize,
current_effect_index: usize,
} }
impl std::fmt::Debug for KayakContext { impl std::fmt::Debug for KayakContext {
...@@ -35,6 +37,7 @@ impl KayakContext { ...@@ -35,6 +37,7 @@ impl KayakContext {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
widget_states: HashMap::new(), widget_states: HashMap::new(),
widget_effects: HashMap::new(),
widget_providers: HashMap::new(), widget_providers: HashMap::new(),
global_bindings: HashMap::new(), global_bindings: HashMap::new(),
widget_state_lifetimes: HashMap::new(), widget_state_lifetimes: HashMap::new(),
...@@ -44,6 +47,7 @@ impl KayakContext { ...@@ -44,6 +47,7 @@ impl KayakContext {
global_state: resources::Resources::default(), global_state: resources::Resources::default(),
last_state_type_id: None, last_state_type_id: None,
current_state_index: 0, current_state_index: 0,
current_effect_index: 0,
} }
} }
...@@ -149,6 +153,7 @@ impl KayakContext { ...@@ -149,6 +153,7 @@ impl KayakContext {
pub fn set_current_id(&mut self, id: crate::Index) { pub fn set_current_id(&mut self, id: crate::Index) {
self.current_id = id; self.current_id = id;
self.current_state_index = 0; self.current_state_index = 0;
self.current_effect_index = 0;
self.last_state_type_id = None; self.last_state_type_id = None;
} }
...@@ -210,6 +215,70 @@ impl KayakContext { ...@@ -210,6 +215,70 @@ impl KayakContext {
return self.get_state(); return self.get_state();
} }
/// Creates a callback that runs as a side-effect of its dependencies, running only when one of them is updated.
///
/// All dependencies must be implement the [Changeable](crate::Changeable) trait, which means it will generally
/// work best with [Binding](crate::Binding) values.
///
/// For more details, check out [React's documentation](https://reactjs.org/docs/hooks-effect.html),
/// upon which this method is based.
///
/// # Arguments
///
/// * `effect`: The side-effect function
/// * `dependencies`: The dependencies the effect relies on
///
/// returns: ()
///
/// # Examples
///
/// ```
/// # use kayak_core::{bind, Binding, Bound, KayakContext};
/// # let mut context = KayakContext::new();
///
/// let my_state: Binding<i32> = bind(0i32);
/// let my_state_clone = my_state.clone();
/// context.create_effect(move || {
/// println!("Value: {}", my_state_clone.get());
/// }, &[&my_state]);
/// ```
pub fn create_effect<'a, F: Fn() + Send + Sync + 'static>(&'a mut self, effect: F, dependencies: &[&'a dyn Changeable]) {
// === Bind to Dependencies === //
let notification = crate::notify(effect);
let mut lifetimes = Vec::default();
for dependency in dependencies {
let lifetime = dependency.when_changed(notification.clone());
lifetimes.push(lifetime);
}
// === Create Invoking Function === //
// Create a temporary Binding to allow us to invoke the effect if needed
let notify_clone = notification.clone();
let invoke_effect = move || {
let control = crate::bind(false);
let mut control_life = control.when_changed(notify_clone.clone());
control.set(true);
control_life.done();
};
// === Insert Effect === //
let effects = self.widget_effects.entry(self.current_id).or_insert(resources::Resources::default());
if effects.contains::<MultiState<Vec<Box<dyn Releasable>>>>() {
let mut state = effects.get_mut::<MultiState<Vec<Box<dyn Releasable>>>>().unwrap();
let old_size = state.data.len();
state.get_or_add(lifetimes, &mut self.current_effect_index);
if old_size != state.data.len() {
// Just added -> invoke effect
invoke_effect();
}
} else {
let state = MultiState::new(lifetimes);
effects.insert(state);
invoke_effect();
self.current_effect_index += 1;
}
}
fn get_state<T: resources::Resource + Clone + PartialEq>(&self) -> Option<T> { fn get_state<T: resources::Resource + Clone + PartialEq>(&self) -> Option<T> {
if self.widget_states.contains_key(&self.current_id) { if self.widget_states.contains_key(&self.current_id) {
let states = self.widget_states.get(&self.current_id).unwrap(); let states = self.widget_states.get(&self.current_id).unwrap();
......
...@@ -10,6 +10,7 @@ mod children; ...@@ -10,6 +10,7 @@ mod children;
mod partial_eq; mod partial_eq;
mod widget; mod widget;
mod widget_attributes; mod widget_attributes;
mod use_effect;
use function_component::WidgetArguments; use function_component::WidgetArguments;
use partial_eq::impl_dyn_partial_eq; use partial_eq::impl_dyn_partial_eq;
...@@ -18,6 +19,7 @@ use proc_macro_error::proc_macro_error; ...@@ -18,6 +19,7 @@ use proc_macro_error::proc_macro_error;
use quote::quote; use quote::quote;
use syn::{parse_macro_input, parse_quote}; use syn::{parse_macro_input, parse_quote};
use widget::ConstructedWidget; use widget::ConstructedWidget;
use use_effect::UseEffect;
use crate::widget::Widget; use crate::widget::Widget;
...@@ -112,21 +114,27 @@ pub fn dyn_partial_eq(_: TokenStream, input: TokenStream) -> TokenStream { ...@@ -112,21 +114,27 @@ pub fn dyn_partial_eq(_: TokenStream, input: TokenStream) -> TokenStream {
.into() .into()
} }
/// Create a state and its setter /// Register some state data with an initial value.
///
/// Once the state is created, this macro returns the current value, a closure for updating the current value, and
/// the raw Binding in a tuple.
///
/// For more details, check out [React's documentation](https://reactjs.org/docs/hooks-state.html),
/// upon which this macro is based.
/// ///
/// # Arguments /// # Arguments
/// ///
/// * `initial_state`: The expression /// * `initial_state`: The initial value for the state
/// ///
/// returns: (state, set_state) /// returns: (state, set_state, state_binding)
/// ///
/// # Examples /// # Examples
/// ///
/// ``` /// ```
/// # use kayak_core::{EventType, OnEvent}; /// # use kayak_core::{EventType, OnEvent};
/// use kayak_render_macros::use_state; /// # use kayak_render_macros::use_state;
/// ///
/// let (count, set_count) = use_state!(0); /// let (count, set_count, ..) = use_state!(0);
/// ///
/// let on_event = OnEvent::new(move |_, event| match event.event_type { /// let on_event = OnEvent::new(move |_, event| match event.event_type {
/// EventType::Click => { /// EventType::Click => {
...@@ -147,6 +155,7 @@ pub fn dyn_partial_eq(_: TokenStream, input: TokenStream) -> TokenStream { ...@@ -147,6 +155,7 @@ pub fn dyn_partial_eq(_: TokenStream, input: TokenStream) -> TokenStream {
pub fn use_state(initial_state: TokenStream) -> TokenStream { pub fn use_state(initial_state: TokenStream) -> TokenStream {
let initial_state = parse_macro_input!(initial_state as syn::Expr); let initial_state = parse_macro_input!(initial_state as syn::Expr);
let result = quote! {{ let result = quote! {{
use kayak_core::{Bound, MutableBound};
let state = context.create_state(#initial_state).unwrap(); let state = context.create_state(#initial_state).unwrap();
let cloned_state = state.clone(); let cloned_state = state.clone();
let set_state = move |value| { let set_state = move |value| {
...@@ -155,7 +164,60 @@ pub fn use_state(initial_state: TokenStream) -> TokenStream { ...@@ -155,7 +164,60 @@ pub fn use_state(initial_state: TokenStream) -> TokenStream {
let state_value = state.get(); let state_value = state.get();
(state.get(), set_state) (state.get(), set_state, state)
}}; }};
TokenStream::from(result) TokenStream::from(result)
} }
/// Registers a side-effect callback for a given set of dependencies.
///
/// This macro takes on the form: `use_effect!(callback, dependencies)`. The callback is
/// the closure that's ran whenever one of the Bindings in the dependencies array is changed.
///
/// Dependencies are automatically cloned when added to the dependency array. This allows the
/// original bindings to be used within the callback without having to clone them manually first.
/// This can be seen in the example below where `count_state` is used within the callback and in
/// the dependency array.
///
/// For more details, check out [React's documentation](https://reactjs.org/docs/hooks-effect.html),
/// upon which this macro is based.
///
/// # Arguments
///
/// * `callback`: The side-effect closure
/// * `dependencies`: The dependency array (in the form `[dep_1, dep_2, ...]`)
///
/// returns: ()
///
/// # Examples
///
/// ```
/// # use kayak_core::{EventType, OnEvent};
/// # use kayak_render_macros::{use_effect, use_state};
///
/// let (count, set_count, count_state) = use_state!(0);
///
/// use_effect!(move || {
/// println!("Count: {}", count_state.get());
/// }, [count_state]);
///
/// let on_event = OnEvent::new(move |_, event| match event.event_type {
/// EventType::Click => {
/// set_count(foo + 1);
/// }
/// _ => {}
/// });
///
/// rsx! {
/// <>
/// <Button on_event={Some(on_event)}>
/// <Text size={16.0} content={format!("Count: {}", count)} />
/// </Button>
/// </>
/// }
/// ```
#[proc_macro]
pub fn use_effect(input: TokenStream) -> TokenStream {
let effect = parse_macro_input!(input as UseEffect);
effect.build()
}
use proc_macro::TokenStream;
use proc_macro2::Ident;
use quote::{format_ident, quote};
use syn::{bracketed, Token};
use syn::parse::{Parse, ParseStream};
use syn::punctuated::{Iter, Punctuated};
pub(crate) struct UseEffect {
pub closure: syn::ExprClosure,
pub dependencies: Punctuated<Ident, Token![,]>,
}
impl Parse for UseEffect {
fn parse(input: ParseStream) -> syn::Result<Self> {
let raw_deps;
let closure = input.parse()?;
let _: Token![,] = input.parse()?;
let _ = bracketed!(raw_deps in input);
let dependencies = raw_deps.parse_terminated(Ident::parse)?;
Ok(Self {
closure,
dependencies,
})
}
}
impl UseEffect {
fn get_deps(&self) -> Iter<Ident> {
self.dependencies.iter()
}
fn get_clone_dep_idents(&self) -> impl Iterator<Item=Ident> + '_ {
self.get_deps().map(|dep| format_ident!("{}_dependency_clone", dep))
}
fn create_clone_deps(&self) -> proc_macro2::TokenStream {
let deps = self.get_deps();
let cloned_deps = self.get_clone_dep_idents();
quote! {
#(let #cloned_deps = #deps.clone());*
}
}
fn create_dep_array(&self) -> proc_macro2::TokenStream {
let cloned_deps = self.get_clone_dep_idents();
quote! {
&[#(&#cloned_deps),*]
}
}
/// Build the output token stream, creating the actual use_effect code
pub fn build(self) -> TokenStream {
let dep_array = self.create_dep_array();
let cloned_deps = self.create_clone_deps();
let closure = self.closure;
let result = quote! {{
use kayak_core::{Bound, MutableBound};
#cloned_deps;
context.create_effect(
#closure,
#dep_array
);
}};
TokenStream::from(result)
}
}
pub mod core { pub mod core {
pub use kayak_core::*; pub use kayak_core::*;
pub use kayak_render_macros::{constructor, render, rsx, use_state, widget}; pub use kayak_render_macros::{constructor, render, rsx, use_effect, use_state, widget};
} }
#[cfg(feature = "bevy_renderer")] #[cfg(feature = "bevy_renderer")]
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment