Skip to content

Commit

Permalink
use_state
Browse files Browse the repository at this point in the history
  • Loading branch information
ccbrown committed Sep 20, 2024
1 parent 206861f commit 4f50154
Show file tree
Hide file tree
Showing 13 changed files with 334 additions and 487 deletions.
13 changes: 5 additions & 8 deletions examples/counter.rs
Original file line number Diff line number Diff line change
@@ -1,22 +1,19 @@
use iocraft::prelude::*;
use std::time::Duration;

#[state]
struct CounterState {
count: Signal<i32>,
}

#[component]
fn Counter(mut state: CounterState, mut hooks: Hooks) -> impl Into<AnyElement<'static>> {
fn Counter(mut hooks: Hooks) -> impl Into<AnyElement<'static>> {
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))
}
}

Expand Down
34 changes: 14 additions & 20 deletions examples/form.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use iocraft::prelude::*;
#[props]
struct FormFieldProps {
label: String,
value: Option<Signal<String>>,
value: Option<State<String>>,
has_focus: bool,
}

Expand Down Expand Up @@ -42,14 +42,6 @@ struct FormContext<'a> {
system: &'a mut SystemContext,
}

#[state]
struct FormState {
first_name: Signal<String>,
last_name: Signal<String>,
focus: Signal<i32>,
should_submit: Signal<bool>,
}

#[props]
struct FormProps<'a> {
first_name_out: Option<&'a mut String>,
Expand All @@ -59,29 +51,31 @@ struct FormProps<'a> {
#[component]
fn Form<'a>(
props: &mut FormProps<'a>,
state: FormState,
mut hooks: Hooks,
context: FormContext,
) -> impl Into<AnyElement<'static>> {
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)
Expand All @@ -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)
}
}
}
Expand Down
21 changes: 7 additions & 14 deletions examples/progress_bar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,35 +6,28 @@ struct ProgressBarContext<'a> {
system: &'a mut SystemContext,
}

#[state]
struct ProgressBarState {
progress: Signal<f32>,
}

#[component]
fn ProgressBar(
state: ProgressBarState,
mut hooks: Hooks,
context: ProgressBarContext,
) -> impl Into<AnyElement<'static>> {
fn ProgressBar(mut hooks: Hooks, context: ProgressBarContext) -> impl Into<AnyElement<'static>> {
let progress = hooks.use_state::<f32, _>(|| 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))
}
}
}
Expand Down
29 changes: 10 additions & 19 deletions examples/use_input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,30 +6,21 @@ struct ExampleContext<'a> {
system: &'a mut SystemContext,
}

#[state]
struct ExampleState {
should_exit: Signal<bool>,
x: Signal<u32>,
y: Signal<u32>,
}

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<AnyElement<'static>> {
fn Example(context: ExampleContext, mut hooks: Hooks) -> impl Into<AnyElement<'static>> {
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 _),
Expand All @@ -41,7 +32,7 @@ fn Example(
}
});

if state.should_exit.get() {
if should_exit.get() {
context.system.exit();
}

Expand All @@ -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,
Expand All @@ -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)
}
Expand Down
82 changes: 1 addition & 81 deletions packages/iocraft-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Self> {
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,
}
Expand Down Expand Up @@ -400,7 +358,6 @@ pub fn context(_attr: TokenStream, item: TokenStream) -> TokenStream {
struct ParsedComponent {
f: ItemFn,
props_type: Option<Box<Type>>,
state_type: Option<Box<Type>>,
context_type: Option<Box<Type>>,
impl_args: Vec<proc_macro2::TokenStream>,
}
Expand All @@ -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();

Expand Down Expand Up @@ -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"));
Expand Down Expand Up @@ -487,7 +427,6 @@ impl Parse for ParsedComponent {
Ok(Self {
f,
props_type,
state_type,
context_type,
impl_args,
})
Expand All @@ -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()
Expand All @@ -524,10 +457,8 @@ impl ToTokens for ParsedComponent {

tokens.extend(quote! {
#vis struct #name {
signal_owner: ::iocraft::SignalOwner,
hooks: Vec<std::boxed::Box<dyn ::iocraft::AnyHook>>,
first_update: bool,
#state_decl
}

impl #name {
Expand All @@ -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,
}
}

Expand All @@ -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)
}
}
});
Expand Down
Loading

0 comments on commit 4f50154

Please sign in to comment.