diff --git a/examples/counter.rs b/examples/counter.rs index eedf93f..a625389 100644 --- a/examples/counter.rs +++ b/examples/counter.rs @@ -1,22 +1,19 @@ use iocraft::prelude::*; use std::time::Duration; -#[state] -struct CounterState { - count: Signal, -} - #[component] -fn Counter(mut state: CounterState, mut hooks: Hooks) -> impl Into> { +fn Counter(mut hooks: Hooks) -> impl Into> { + let mut count = hooks.use_state(|| 0); + hooks.use_future(async move { loop { smol::Timer::after(Duration::from_millis(100)).await; - state.count += 1; + count += 1; } }); element! { - Text(color: Color::Blue, content: format!("counter: {}", state.count)) + Text(color: Color::Blue, content: format!("counter: {}", count)) } } diff --git a/examples/form.rs b/examples/form.rs index b7faa45..27721a1 100644 --- a/examples/form.rs +++ b/examples/form.rs @@ -3,7 +3,7 @@ use iocraft::prelude::*; #[props] struct FormFieldProps { label: String, - value: Option>, + value: Option>, has_focus: bool, } @@ -42,14 +42,6 @@ struct FormContext<'a> { system: &'a mut SystemContext, } -#[state] -struct FormState { - first_name: Signal, - last_name: Signal, - focus: Signal, - should_submit: Signal, -} - #[props] struct FormProps<'a> { first_name_out: Option<&'a mut String>, @@ -59,29 +51,31 @@ struct FormProps<'a> { #[component] fn Form<'a>( props: &mut FormProps<'a>, - state: FormState, mut hooks: Hooks, context: FormContext, ) -> impl Into> { + let first_name = hooks.use_state(|| "".to_string()); + let last_name = hooks.use_state(|| "".to_string()); + let focus = hooks.use_state(|| 0); + let should_submit = hooks.use_state(|| false); + hooks.use_terminal_events(move |event| match event { TerminalEvent::Key(KeyEvent { code, kind, .. }) if kind != KeyEventKind::Release => { match code { - KeyCode::Enter => state.should_submit.set(true), - KeyCode::Tab | KeyCode::Up | KeyCode::Down => { - state.focus.set((state.focus + 1) % 2) - } + KeyCode::Enter => should_submit.set(true), + KeyCode::Tab | KeyCode::Up | KeyCode::Down => focus.set((focus + 1) % 2), _ => {} } } _ => {} }); - if state.should_submit.get() { + if should_submit.get() { if let Some(first_name_out) = props.first_name_out.as_mut() { - **first_name_out = state.first_name.to_string(); + **first_name_out = first_name.to_string(); } if let Some(last_name_out) = props.last_name_out.as_mut() { - **last_name_out = state.last_name.to_string(); + **last_name_out = last_name.to_string(); } context.system.exit(); element!(Box) @@ -93,15 +87,15 @@ fn Form<'a>( margin: 2, ) { Box( - padding_bottom: if state.focus == 0 { 1 } else { 2 }, + padding_bottom: if focus == 0 { 1 } else { 2 }, flex_direction: FlexDirection::Column, align_items: AlignItems::Center, ) { Text(content: "What's your name?", color: Color::White, weight: Weight::Bold) Text(content: "Press tab to cycle through fields.\nPress enter to submit.", color: Color::Grey, align: TextAlign::Center) } - FormField(label: "First Name", value: state.first_name, has_focus: state.focus == 0) - FormField(label: "Last Name", value: state.last_name, has_focus: state.focus == 1) + FormField(label: "First Name", value: first_name, has_focus: focus == 0) + FormField(label: "Last Name", value: last_name, has_focus: focus == 1) } } } diff --git a/examples/progress_bar.rs b/examples/progress_bar.rs index a4eb3fd..c7986dd 100644 --- a/examples/progress_bar.rs +++ b/examples/progress_bar.rs @@ -6,35 +6,28 @@ struct ProgressBarContext<'a> { system: &'a mut SystemContext, } -#[state] -struct ProgressBarState { - progress: Signal, -} - #[component] -fn ProgressBar( - state: ProgressBarState, - mut hooks: Hooks, - context: ProgressBarContext, -) -> impl Into> { +fn ProgressBar(mut hooks: Hooks, context: ProgressBarContext) -> impl Into> { + let progress = hooks.use_state::(|| 0.0); + hooks.use_future(async move { loop { smol::Timer::after(Duration::from_millis(100)).await; - state.progress.set((state.progress.get() + 2.0).min(100.0)); + progress.set((progress.get() + 2.0).min(100.0)); } }); - if state.progress >= 100.0 { + if progress >= 100.0 { context.system.exit(); } element! { Box { Box(border_style: BorderStyle::Round, border_color: Color::Blue, width: 60) { - Box(width: Percent(state.progress.get()), height: 1, background_color: Color::Green) + Box(width: Percent(progress.get()), height: 1, background_color: Color::Green) } Box(padding: 1) { - Text(content: format!("{:.0}%", state.progress)) + Text(content: format!("{:.0}%", progress)) } } } diff --git a/examples/use_input.rs b/examples/use_input.rs index 1cb0faf..82009c1 100644 --- a/examples/use_input.rs +++ b/examples/use_input.rs @@ -6,30 +6,21 @@ struct ExampleContext<'a> { system: &'a mut SystemContext, } -#[state] -struct ExampleState { - should_exit: Signal, - x: Signal, - y: Signal, -} - const AREA_WIDTH: u32 = 80; const AREA_HEIGHT: u32 = 11; const FACE: &str = "👾"; #[component] -fn Example( - context: ExampleContext, - state: ExampleState, - mut hooks: Hooks, -) -> impl Into> { +fn Example(context: ExampleContext, mut hooks: Hooks) -> impl Into> { + let x = hooks.use_state(|| 0); + let y = hooks.use_state(|| 0); + let should_exit = hooks.use_state(|| false); + hooks.use_terminal_events({ - let x = state.x; - let y = state.y; move |event| match event { TerminalEvent::Key(KeyEvent { code, kind, .. }) if kind != KeyEventKind::Release => { match code { - KeyCode::Char('q') => state.should_exit.set(true), + KeyCode::Char('q') => should_exit.set(true), KeyCode::Up => y.set((y.get() as i32 - 1).max(0) as _), KeyCode::Down => y.set((y.get() + 1).min(AREA_HEIGHT - 1)), KeyCode::Left => x.set((x.get() as i32 - 1).max(0) as _), @@ -41,7 +32,7 @@ fn Example( } }); - if state.should_exit.get() { + if should_exit.get() { context.system.exit(); } @@ -58,7 +49,7 @@ fn Example( height: AREA_HEIGHT + 2, width: AREA_WIDTH + 2, ) { - #(if state.should_exit.get() { + #(if should_exit.get() { element! { Box( width: 100pct, @@ -72,8 +63,8 @@ fn Example( } else { element! { Box( - padding_left: state.x.get(), - padding_top: state.y.get(), + padding_left: x.get(), + padding_top: y.get(), ) { Text(content: FACE) } diff --git a/packages/iocraft-macros/src/lib.rs b/packages/iocraft-macros/src/lib.rs index be710d0..a8aa768 100644 --- a/packages/iocraft-macros/src/lib.rs +++ b/packages/iocraft-macros/src/lib.rs @@ -272,48 +272,6 @@ pub fn props(_attr: TokenStream, item: TokenStream) -> TokenStream { quote!(#props).into() } -struct ParsedState { - state: ItemStruct, -} - -impl Parse for ParsedState { - fn parse(input: ParseStream) -> Result { - let state: ItemStruct = input.parse()?; - Ok(Self { state }) - } -} - -impl ToTokens for ParsedState { - fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { - let state = &self.state; - let name = &state.ident; - let field_assignments = state.fields.iter().map(|field| { - let field_name = &field.ident; - quote! { #field_name: owner.new_signal_with_default() } - }); - - tokens.extend(quote! { - #[derive(Clone, Copy)] - #state - - impl #name { - fn new(owner: &mut ::iocraft::SignalOwner) -> Self { - Self { - #(#field_assignments,)* - } - } - } - }); - } -} - -/// Defines a struct containing state to be made available to components. -#[proc_macro_attribute] -pub fn state(_attr: TokenStream, item: TokenStream) -> TokenStream { - let state = parse_macro_input!(item as ParsedState); - quote!(#state).into() -} - struct ParsedContext { context: ItemStruct, } @@ -400,7 +358,6 @@ pub fn context(_attr: TokenStream, item: TokenStream) -> TokenStream { struct ParsedComponent { f: ItemFn, props_type: Option>, - state_type: Option>, context_type: Option>, impl_args: Vec, } @@ -410,7 +367,6 @@ impl Parse for ParsedComponent { let f: ItemFn = input.parse()?; let mut props_type = None; - let mut state_type = None; let mut context_type = None; let mut impl_args = Vec::new(); @@ -444,22 +400,6 @@ impl Parse for ParsedComponent { } _ => return Err(Error::new(arg.ty.span(), "invalid `hooks` type")), }, - "state" | "_state" => { - if state_type.is_some() { - return Err(Error::new(arg.span(), "duplicate `state` argument")); - } - match &*arg.ty { - Type::Reference(r) => { - impl_args.push(quote!(&mut self.state)); - state_type = Some(r.elem.clone()); - } - Type::Path(_) => { - impl_args.push(quote!(self.state.clone())); - state_type = Some(arg.ty.clone()); - } - _ => return Err(Error::new(arg.ty.span(), "invalid `state` type")), - } - } "context" | "_context" => { if context_type.is_some() { return Err(Error::new(arg.span(), "duplicate `context` argument")); @@ -487,7 +427,6 @@ impl Parse for ParsedComponent { Ok(Self { f, props_type, - state_type, context_type, impl_args, }) @@ -504,12 +443,6 @@ impl ToTokens for ParsedComponent { let generics = &self.f.sig.generics; let impl_args = &self.impl_args; - let state_decl = self.state_type.as_ref().map(|ty| quote!(state: #ty,)); - let state_init = self - .state_type - .as_ref() - .map(|ty| quote!(state: #ty::new(&mut signal_owner),)); - let props_type_name = self .props_type .as_ref() @@ -524,10 +457,8 @@ impl ToTokens for ParsedComponent { tokens.extend(quote! { #vis struct #name { - signal_owner: ::iocraft::SignalOwner, hooks: Vec>, first_update: bool, - #state_decl } impl #name { @@ -538,12 +469,9 @@ impl ToTokens for ParsedComponent { type Props<'a> = #props_type_name; fn new(_props: &Self::Props<'_>) -> Self { - let mut signal_owner = ::iocraft::SignalOwner::new(); Self { - #state_init hooks: Vec::new(), first_update: true, - signal_owner, } } @@ -569,15 +497,7 @@ impl ToTokens for ParsedComponent { fn poll_change(mut self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> std::task::Poll<()> { use ::iocraft::Hook; - - let signals_status = std::pin::Pin::new(&mut self.signal_owner).poll_change(cx); - let hooks_status = std::pin::Pin::new(&mut self.hooks).poll_change(cx); - - if signals_status.is_ready() || hooks_status.is_ready() { - std::task::Poll::Ready(()) - } else { - std::task::Poll::Pending - } + std::pin::Pin::new(&mut self.hooks).poll_change(cx) } } }); diff --git a/packages/iocraft-macros/tests/component.rs b/packages/iocraft-macros/tests/component.rs index 2cb7e69..5b7a3f8 100644 --- a/packages/iocraft-macros/tests/component.rs +++ b/packages/iocraft-macros/tests/component.rs @@ -1,34 +1,9 @@ #![allow(dead_code)] -use iocraft::{components::Box, AnyElement, Signal}; -use iocraft_macros::{component, element, state}; +use iocraft::{components::Box, AnyElement}; +use iocraft_macros::{component, element}; #[component] fn MyComponent() -> impl Into> { element!(Box) } - -#[state] -struct MyState { - foo: Signal, -} - -#[component] -fn MyComponentWithStateCopy(_state: MyState) -> impl Into> { - element!(Box) -} - -#[component] -fn MyComponentWithMutStateCopy(mut _state: MyState) -> impl Into> { - element!(Box) -} - -#[component] -fn MyComponentWithStateRef(_state: &MyState) -> impl Into> { - element!(Box) -} - -#[component] -fn MyComponentWithMutState(_state: &mut MyState) -> impl Into> { - element!(Box) -} diff --git a/packages/iocraft-macros/tests/state.rs b/packages/iocraft-macros/tests/state.rs deleted file mode 100644 index 468df73..0000000 --- a/packages/iocraft-macros/tests/state.rs +++ /dev/null @@ -1,9 +0,0 @@ -#![allow(dead_code)] - -use iocraft::Signal; -use iocraft_macros::state; - -#[state] -struct MyState { - foo: Signal, -} diff --git a/packages/iocraft/src/hooks/mod.rs b/packages/iocraft/src/hooks/mod.rs index 43e6eee..d8e849b 100644 --- a/packages/iocraft/src/hooks/mod.rs +++ b/packages/iocraft/src/hooks/mod.rs @@ -1,6 +1,8 @@ mod use_future; pub use use_future::*; -mod use_input; -pub use use_input::*; mod use_output; pub use use_output::*; +mod use_state; +pub use use_state::*; +mod use_terminal_events; +pub use use_terminal_events::*; diff --git a/packages/iocraft/src/hooks/use_state.rs b/packages/iocraft/src/hooks/use_state.rs new file mode 100644 index 0000000..7090962 --- /dev/null +++ b/packages/iocraft/src/hooks/use_state.rs @@ -0,0 +1,280 @@ +use crate::{Hook, Hooks}; +use generational_box::{AnyStorage, GenerationalBox, Owner, SyncStorage}; +use std::{ + cmp, + fmt::{self, Debug, Display, Formatter}, + ops, + pin::Pin, + task::{Context, Poll, Waker}, +}; + +/// `UseState` is a hook that allows you to store state in a component. +/// +/// When the state changes, the component will be re-rendered. +pub trait UseState { + /// Creates a new state with its initial value computed by the given function. + /// + /// When the state changes, the component will be re-rendered. + fn use_state(&mut self, initial_value: F) -> State + where + T: Unpin + Sync + Send + 'static, + F: FnOnce() -> T; +} + +impl UseState for Hooks<'_> { + fn use_state(&mut self, initial_value: F) -> State + where + T: Unpin + Sync + Send + 'static, + F: FnOnce() -> T, + { + self.use_hook(move || UseStateImpl::new(initial_value())) + .state + } +} + +struct UseStateImpl { + _storage: Owner, + state: State, +} + +impl UseStateImpl { + pub fn new(initial_value: T) -> Self { + let storage = Owner::default(); + UseStateImpl { + state: State { + inner: storage.insert(StateValue { + did_change: false, + waker: None, + value: initial_value, + }), + }, + _storage: storage, + } + } +} + +impl Hook for UseStateImpl { + fn poll_change(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> { + if let Ok(mut value) = self.state.inner.try_write() { + if value.did_change { + value.did_change = false; + Poll::Ready(()) + } else { + value.waker = Some(cx.waker().clone()); + Poll::Pending + } + } else { + Poll::Pending + } + } +} + +struct StateValue { + did_change: bool, + waker: Option, + value: T, +} + +/// A reference to the value of a [`State`]. +pub struct StateRef { + inner: ::Ref<'static, StateValue>, +} + +impl ops::Deref for StateRef { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.inner.value + } +} + +/// `State` is a copyable wrapper for a value that can be observed for changes. States used by a +/// component will cause the component to be re-rendered when its value changes. +pub struct State { + inner: GenerationalBox, SyncStorage>, +} + +impl Clone for State { + fn clone(&self) -> Self { + *self + } +} + +impl Copy for State {} + +impl State { + /// Gets a copy of the current value of the state. + pub fn get(&self) -> T { + self.inner.read().value + } +} + +impl State { + /// Sets the value of the state. + pub fn set(&self, value: T) { + self.modify(|v| *v = value); + } + + /// Returns a reference to the state's value. + pub fn read(&self) -> StateRef { + StateRef { + inner: self.inner.read(), + } + } + + fn modify(&self, f: F) + where + F: FnOnce(&mut T), + { + let mut inner = self.inner.write(); + f(&mut inner.value); + inner.did_change = true; + if let Some(waker) = inner.waker.take() { + waker.wake(); + } + } +} + +impl Debug for State { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + self.inner.read().value.fmt(f) + } +} + +impl Display for State { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + self.inner.read().value.fmt(f) + } +} + +impl + Copy + Sync + Send + 'static> ops::Add for State { + type Output = T; + + fn add(self, rhs: T) -> Self::Output { + self.get() + rhs + } +} + +impl + Copy + Sync + Send + 'static> ops::AddAssign for State { + fn add_assign(&mut self, rhs: T) { + self.modify(|v| *v += rhs); + } +} + +impl + Copy + Sync + Send + 'static> ops::Sub for State { + type Output = T; + + fn sub(self, rhs: T) -> Self::Output { + self.get() - rhs + } +} + +impl + Copy + Sync + Send + 'static> ops::SubAssign for State { + fn sub_assign(&mut self, rhs: T) { + self.modify(|v| *v -= rhs); + } +} + +impl + Copy + Sync + Send + 'static> ops::Mul for State { + type Output = T; + + fn mul(self, rhs: T) -> Self::Output { + self.get() * rhs + } +} + +impl + Copy + Sync + Send + 'static> ops::MulAssign for State { + fn mul_assign(&mut self, rhs: T) { + self.modify(|v| *v *= rhs); + } +} + +impl + Copy + Sync + Send + 'static> ops::Div for State { + type Output = T; + + fn div(self, rhs: T) -> Self::Output { + self.get() / rhs + } +} + +impl + Copy + Sync + Send + 'static> ops::DivAssign for State { + fn div_assign(&mut self, rhs: T) { + self.modify(|v| *v /= rhs); + } +} + +impl + Sync + Send + 'static> cmp::PartialEq for State { + fn eq(&self, other: &T) -> bool { + self.inner.read().value == *other + } +} + +impl + Sync + Send + 'static> cmp::PartialOrd for State { + fn partial_cmp(&self, other: &T) -> Option { + self.inner.read().value.partial_cmp(other) + } +} + +impl + Sync + Send + 'static> cmp::PartialEq> for State { + fn eq(&self, other: &State) -> bool { + self.inner.read().value == other.inner.read().value + } +} + +impl + Sync + Send + 'static> cmp::PartialOrd> for State { + fn partial_cmp(&self, other: &State) -> Option { + self.inner + .read() + .value + .partial_cmp(&other.inner.read().value) + } +} + +impl cmp::Eq for State {} + +#[cfg(test)] +mod tests { + use super::*; + use futures::task::noop_waker; + use std::pin::Pin; + + #[test] + fn test_state() { + let mut hook = UseStateImpl::new(42); + let mut state = hook.state; + assert_eq!(state.get(), 42); + + state.set(43); + assert_eq!(state, 43); + assert_eq!( + Pin::new(&mut hook).poll_change(&mut Context::from_waker(&noop_waker())), + Poll::Ready(()) + ); + assert_eq!( + Pin::new(&mut hook).poll_change(&mut Context::from_waker(&noop_waker())), + Poll::Pending + ); + + assert_eq!(state.to_string(), "43"); + + assert_eq!(state.clone() + 1, 44); + state += 1; + assert_eq!(state, 44); + + assert_eq!(state.clone() - 1, 43); + state -= 1; + assert_eq!(state, 43); + + assert_eq!(state.clone() * 2, 86); + state *= 2; + assert_eq!(state, 86); + + assert_eq!(state.clone() / 2, 43); + state /= 2; + assert_eq!(state, 43); + + assert!(state > 42); + assert!(state >= 43); + assert!(state < 44); + } +} diff --git a/packages/iocraft/src/hooks/use_input.rs b/packages/iocraft/src/hooks/use_terminal_events.rs similarity index 79% rename from packages/iocraft/src/hooks/use_input.rs rename to packages/iocraft/src/hooks/use_terminal_events.rs index f228427..6136907 100644 --- a/packages/iocraft/src/hooks/use_input.rs +++ b/packages/iocraft/src/hooks/use_terminal_events.rs @@ -5,32 +5,32 @@ use std::{ task::{Context, Poll}, }; -/// `UseInput` is a hook that allows you to listen for user input such as key strokes. -pub trait UseInput { +/// `UseTerminalEvents` is a hook that allows you to listen for user input such as key strokes. +pub trait UseTerminalEvents { /// Defines a callback to be invoked whenever a terminal event occurs. fn use_terminal_events(&mut self, f: F) where F: FnMut(TerminalEvent) + Send + 'static; } -impl UseInput for Hooks<'_> { +impl UseTerminalEvents for Hooks<'_> { fn use_terminal_events(&mut self, f: F) where F: FnMut(TerminalEvent) + Send + 'static, { - self.use_hook(move || UseInputImpl { + self.use_hook(move || UseTerminalEventsImpl { events: None, f: Box::new(f), }); } } -struct UseInputImpl { +struct UseTerminalEventsImpl { events: Option, f: Box, } -impl Hook for UseInputImpl { +impl Hook for UseTerminalEventsImpl { fn poll_change(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> { while let Some(Poll::Ready(Some(event))) = self .events diff --git a/packages/iocraft/src/lib.rs b/packages/iocraft/src/lib.rs index 93d1281..c215838 100644 --- a/packages/iocraft/src/lib.rs +++ b/packages/iocraft/src/lib.rs @@ -20,7 +20,6 @@ mod handler; mod hook; mod props; mod render; -mod signal; mod style; mod terminal; @@ -33,7 +32,6 @@ mod flattened_exports { pub use crate::hook::*; pub use crate::props::*; pub use crate::render::*; - pub use crate::signal::*; pub use crate::style::*; pub use crate::terminal::*; diff --git a/packages/iocraft/src/render.rs b/packages/iocraft/src/render.rs index d55b507..4c64384 100644 --- a/packages/iocraft/src/render.rs +++ b/packages/iocraft/src/render.rs @@ -407,11 +407,6 @@ mod tests { use macro_rules_attribute::apply; use smol_macros::test; - #[state] - struct MyComponentState { - counter: Signal, - } - #[context] struct MyComponentContext<'a> { system: &'a mut SystemContext, @@ -419,20 +414,21 @@ mod tests { #[component] fn MyComponent( - mut state: MyComponentState, mut hooks: Hooks, context: MyComponentContext, ) -> impl Into> { + let mut counter = hooks.use_state(|| 0); + hooks.use_future(async move { - state.counter += 1; + counter += 1; }); - if state.counter == 1 { + if counter == 1 { context.system.exit(); } element! { - Text(content: format!("count: {}", state.counter)) + Text(content: format!("count: {}", counter)) } } diff --git a/packages/iocraft/src/signal.rs b/packages/iocraft/src/signal.rs deleted file mode 100644 index 8b8acaf..0000000 --- a/packages/iocraft/src/signal.rs +++ /dev/null @@ -1,290 +0,0 @@ -use generational_box::{AnyStorage, GenerationalBox, Owner, SyncStorage}; -use std::{ - cmp, - fmt::{self, Debug, Display, Formatter}, - ops, - pin::Pin, - task::{Context, Poll, Waker}, -}; - -trait SignalValueGenerationalBox { - fn poll_change_unpin(&mut self, cx: &mut Context<'_>) -> Poll<()>; -} - -impl SignalValueGenerationalBox - for GenerationalBox, SyncStorage> -{ - fn poll_change_unpin(&mut self, cx: &mut Context<'_>) -> Poll<()> { - if let Ok(mut value) = self.try_write() { - if value.did_change { - value.did_change = false; - Poll::Ready(()) - } else { - value.waker = Some(cx.waker().clone()); - Poll::Pending - } - } else { - Poll::Pending - } - } -} - -struct SignalValue { - did_change: bool, - waker: Option, - value: T, -} - -#[doc(hidden)] -#[derive(Default)] -pub struct SignalOwner { - storage: Owner, - signals: Vec>, -} - -impl SignalOwner { - pub fn poll_change(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> { - let mut is_ready = false; - for signal in self.signals.iter_mut() { - if signal.poll_change_unpin(cx).is_ready() { - is_ready = true; - } - } - if is_ready { - Poll::Ready(()) - } else { - Poll::Pending - } - } -} - -impl SignalOwner { - pub fn new() -> Self { - Self::default() - } - - pub fn new_signal(&mut self, value: T) -> Signal { - let key = self.storage.insert(SignalValue { - did_change: false, - waker: None, - value, - }); - self.signals.push(Box::new(key)); - Signal { inner: key } - } - - pub fn new_signal_with_default(&mut self) -> Signal { - self.new_signal(T::default()) - } -} - -/// A reference to the value of a [`Signal`]. -pub struct SignalRef { - inner: ::Ref<'static, SignalValue>, -} - -impl ops::Deref for SignalRef { - type Target = T; - - fn deref(&self) -> &Self::Target { - &self.inner.value - } -} - -/// `Signal` is a clonable wrapper for a value that can be observed for changes. Signals used as -/// part of a component's state will cause the component to be re-rendered when the signal's value -/// changes. -pub struct Signal { - inner: GenerationalBox, SyncStorage>, -} - -impl Clone for Signal { - fn clone(&self) -> Self { - *self - } -} - -impl Copy for Signal {} - -impl Signal { - /// Gets a copy of the current value of the signal. - pub fn get(&self) -> T { - self.inner.read().value - } -} - -impl Signal { - /// Sets the value of the signal. - pub fn set(&self, value: T) { - self.modify(|v| *v = value); - } - - /// Returns a reference to the signal's value. - pub fn read(&self) -> SignalRef { - SignalRef { - inner: self.inner.read(), - } - } - - fn modify(&self, f: F) - where - F: FnOnce(&mut T), - { - let mut inner = self.inner.write(); - f(&mut inner.value); - inner.did_change = true; - if let Some(waker) = inner.waker.take() { - waker.wake(); - } - } -} - -impl Debug for Signal { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - self.inner.read().value.fmt(f) - } -} - -impl Display for Signal { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - self.inner.read().value.fmt(f) - } -} - -impl + Copy + Sync + Send + 'static> ops::Add for Signal { - type Output = T; - - fn add(self, rhs: T) -> Self::Output { - self.get() + rhs - } -} - -impl + Copy + Sync + Send + 'static> ops::AddAssign for Signal { - fn add_assign(&mut self, rhs: T) { - self.modify(|v| *v += rhs); - } -} - -impl + Copy + Sync + Send + 'static> ops::Sub for Signal { - type Output = T; - - fn sub(self, rhs: T) -> Self::Output { - self.get() - rhs - } -} - -impl + Copy + Sync + Send + 'static> ops::SubAssign for Signal { - fn sub_assign(&mut self, rhs: T) { - self.modify(|v| *v -= rhs); - } -} - -impl + Copy + Sync + Send + 'static> ops::Mul for Signal { - type Output = T; - - fn mul(self, rhs: T) -> Self::Output { - self.get() * rhs - } -} - -impl + Copy + Sync + Send + 'static> ops::MulAssign for Signal { - fn mul_assign(&mut self, rhs: T) { - self.modify(|v| *v *= rhs); - } -} - -impl + Copy + Sync + Send + 'static> ops::Div for Signal { - type Output = T; - - fn div(self, rhs: T) -> Self::Output { - self.get() / rhs - } -} - -impl + Copy + Sync + Send + 'static> ops::DivAssign for Signal { - fn div_assign(&mut self, rhs: T) { - self.modify(|v| *v /= rhs); - } -} - -impl + Sync + Send + 'static> cmp::PartialEq for Signal { - fn eq(&self, other: &T) -> bool { - self.inner.read().value == *other - } -} - -impl + Sync + Send + 'static> cmp::PartialOrd for Signal { - fn partial_cmp(&self, other: &T) -> Option { - self.inner.read().value.partial_cmp(other) - } -} - -impl + Sync + Send + 'static> cmp::PartialEq> for Signal { - fn eq(&self, other: &Signal) -> bool { - self.inner.read().value == other.inner.read().value - } -} - -impl + Sync + Send + 'static> cmp::PartialOrd> for Signal { - fn partial_cmp(&self, other: &Signal) -> Option { - self.inner - .read() - .value - .partial_cmp(&other.inner.read().value) - } -} - -impl cmp::Eq for Signal {} - -#[cfg(test)] -mod tests { - use super::*; - use futures::task::noop_waker; - use std::pin::Pin; - - #[test] - fn test_signal() { - let mut owner = SignalOwner::new(); - assert_eq!( - Pin::new(&mut owner).poll_change(&mut Context::from_waker(&noop_waker())), - Poll::Pending - ); - - let mut signal = owner.new_signal(42); - assert_eq!(signal.get(), 42); - - signal.set(43); - assert_eq!(signal, 43); - assert_eq!( - Pin::new(&mut owner).poll_change(&mut Context::from_waker(&noop_waker())), - Poll::Ready(()) - ); - assert_eq!( - Pin::new(&mut owner).poll_change(&mut Context::from_waker(&noop_waker())), - Poll::Pending - ); - - assert_eq!(signal.to_string(), "43"); - - assert_eq!(signal.clone() + 1, 44); - signal += 1; - assert_eq!(signal, 44); - - assert_eq!(signal.clone() - 1, 43); - signal -= 1; - assert_eq!(signal, 43); - - assert_eq!(signal.clone() * 2, 86); - signal *= 2; - assert_eq!(signal, 86); - - assert_eq!(signal.clone() / 2, 43); - signal /= 2; - assert_eq!(signal, 43); - - assert!(signal > 42); - assert!(signal >= 43); - assert!(signal < 44); - assert!(signal <= owner.new_signal(100)); - } -}