From b5e5ef171aa9f8986fb4ab04d793eb63f557c4ae Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 2 Jul 2021 15:48:19 -0400 Subject: [PATCH] feat: two calculator examples --- Cargo.toml | 2 + examples/calculator.rs | 148 +++++++++++++ examples/doggo.rs | 0 examples/model.rs | 226 +++++++++++++++++++ packages/core-macro/src/rsx/component.rs | 40 +++- packages/core/src/events.rs | 266 ++++++++++++++++++++++- 6 files changed, 677 insertions(+), 5 deletions(-) create mode 100644 examples/calculator.rs create mode 100644 examples/doggo.rs create mode 100644 examples/model.rs diff --git a/Cargo.toml b/Cargo.toml index 7a0ae8b11..160efff13 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,7 @@ split-debuginfo = "unpacked" [dev-dependencies] futures = "0.3.15" log = "0.4.14" +num-format = "0.4.0" # For the tide ssr examples # async-std = { version="1.9.0", features=["attributes"] } # tide = { version="0.16.0" } @@ -47,6 +48,7 @@ log = "0.4.14" # dioxus-webview = { path="./packages/webview", version="0.0.0" } # dioxus-hooks = { path="./packages/hooks", version="0.0.0" } rand = "0.8.4" +separator = "0.4.1" serde = { version="1.0.126", features=["derive"] } surf = "2.2.0" diff --git a/examples/calculator.rs b/examples/calculator.rs new file mode 100644 index 000000000..db1c422dd --- /dev/null +++ b/examples/calculator.rs @@ -0,0 +1,148 @@ +//! Example: Calculator +//! ------------------------- + +// use dioxus::events::on::*; +// use dioxus::prelude::*; + +fn main() { + dioxus::webview::launch(App); +} + +use dioxus::events::on::*; +use dioxus::prelude::*; + +enum Operator { + Add, + Sub, + Mul, + Div, +} + +static App: FC<()> = |cx| { + let cur_val = use_state_new(&cx, || 0.0_f64); + let operator = use_state_new(&cx, || None as Option); + let display_value = use_state_new(&cx, || "".to_string()); + + let clear_display = display_value.eq("0"); + let clear_text = if clear_display { "C" } else { "AC" }; + + let input_digit = + move |num: u8| display_value.modify(move |f| f.push_str(num.to_string().as_str())); + + let input_dot = move || display_value.modify(move |f| f.push_str(".")); + + let perform_operation = move || { + if let Some(op) = operator.as_ref() { + let rhs = display_value.parse::().unwrap(); + let new_val = match op { + Operator::Add => **cur_val + rhs, + Operator::Sub => **cur_val - rhs, + Operator::Mul => **cur_val * rhs, + Operator::Div => **cur_val / rhs, + }; + cur_val.set(new_val); + display_value.set(new_val.to_string()); + operator.set(None); + } + }; + + let toggle_sign = move |_| { + if display_value.starts_with("-") { + display_value.set(display_value.trim_start_matches("-").to_string()) + } else { + display_value.set(format!("-{}", **display_value)) + } + }; + let toggle_percent = move |_| todo!(); + + let clear_key = move |_| { + display_value.set("0".to_string()); + if !clear_display { + operator.set(None); + cur_val.set(0.0); + } + }; + + let keydownhandler = move |evt: KeyboardEvent| match evt.key_code() { + KeyCode::Backspace => display_value.modify(|f| { + if !f.as_str().eq("0") { + f.pop(); + } + }), + KeyCode::_0 => input_digit(0), + KeyCode::_1 => input_digit(1), + KeyCode::_2 => input_digit(2), + KeyCode::_3 => input_digit(3), + KeyCode::_4 => input_digit(4), + KeyCode::_5 => input_digit(5), + KeyCode::_6 => input_digit(6), + KeyCode::_7 => input_digit(7), + KeyCode::_8 => input_digit(8), + KeyCode::_9 => input_digit(9), + KeyCode::Add => operator.set(Some(Operator::Add)), + KeyCode::Subtract => operator.set(Some(Operator::Sub)), + KeyCode::Divide => operator.set(Some(Operator::Div)), + KeyCode::Multiply => operator.set(Some(Operator::Mul)), + _ => {} + }; + + cx.render(rsx! { + div { class: "calculator", onkeydown: {keydownhandler} + div { class: "input-keys" + div { class: "function-keys" + CalculatorKey { name: "key-clear", onclick: {clear_key} "{clear_text}" } + CalculatorKey { name: "key-sign", onclick: {toggle_sign}, "±"} + CalculatorKey { name: "key-percent", onclick: {toggle_percent} "%"} + } + div { class: "digit-keys" + CalculatorKey { name: "key-0", onclick: move |_| input_digit(0), "0" } + CalculatorKey { name: "key-dot", onclick: move |_| input_dot(), "●" } + + {(1..9).map(|k| rsx!{ + CalculatorKey { key: "{k}", name: "key-{k}", onclick: move |_| input_digit(k), "{k}" } + })} + } + div { class: "operator-keys" + CalculatorKey { name:"key-divide", onclick: move |_| operator.set(Some(Operator::Div)) "÷" } + CalculatorKey { name:"key-multiply", onclick: move |_| operator.set(Some(Operator::Mul)) "×" } + CalculatorKey { name:"key-subtract", onclick: move |_| operator.set(Some(Operator::Sub)) "−" } + CalculatorKey { name:"key-add", onclick: move |_| operator.set(Some(Operator::Add)) "+" } + CalculatorKey { name:"key-equals", onclick: move |_| perform_operation() "=" } + } + } + } + }) +}; + +#[derive(Props)] +struct CalculatorKeyProps<'a> { + name: &'static str, + onclick: &'a dyn Fn(MouseEvent), +} + +fn CalculatorKey<'a, 'r>(cx: Context<'a, CalculatorKeyProps<'r>>) -> VNode<'a> { + cx.render(rsx! { + button { + class: "calculator-key {cx.name}" + onclick: {cx.onclick} + {cx.children()} + } + }) +} + +#[derive(Props, PartialEq)] +struct CalculatorDisplayProps { + val: f64, +} + +fn CalculatorDisplay(cx: Context) -> VNode { + use separator::Separatable; + // Todo, add float support to the num-format crate + let formatted = cx.val.separated_string(); + // TODO: make it autoscaling with css + cx.render(rsx! { + div { class: "calculator-display" + "{formatted}" + } + }) +} diff --git a/examples/doggo.rs b/examples/doggo.rs new file mode 100644 index 000000000..e69de29bb diff --git a/examples/model.rs b/examples/model.rs new file mode 100644 index 000000000..727b3a28a --- /dev/null +++ b/examples/model.rs @@ -0,0 +1,226 @@ +//! Example: Calculator +//! ------------------- +//! +//! Some components benefit through the use of "Models". Models are a single block of encapsulated state that allow mutative +//! methods to be performed on them. Dioxus exposes the ability to use the model pattern through the "use_model" hook. +//! +//! `use_model` is basically just a fancy wrapper around set_state, but saves a "working copy" of the new state behind a +//! RefCell. To modify the working copy, you need to call "modify" which returns the RefMut. This makes it easy to write +//! fully encapsulated apps that retain a certain feel of native Rusty-ness. A calculator app is a good example of when this +//! is useful. + +use dioxus::events::on::*; +use dioxus::prelude::*; +use std::{ + cell::RefCell, + ops::{Deref, DerefMut}, +}; + +fn main() { + dioxus::webview::launch(App) +} + +static App: FC<()> = |cx| { + let state = use_model(&cx, || CalculatorState::new()); + + let clear_display = state.display_value.eq("0"); + let clear_text = if clear_display { "C" } else { "AC" }; + let formatted = state.formatted(); + + cx.render(rsx! { + div { class: "calculator", onkeydown: move |evt| state.modify().handle_keydown(evt), + div { class: "calculator-display", "{formatted}"} + div { class: "input-keys" + div { class: "function-keys" + CalculatorKey { name: "key-clear", onclick: move |_| state.modify().clear_display() "{clear_text}" } + CalculatorKey { name: "key-sign", onclick: move |_| state.modify().toggle_sign(), "±"} + CalculatorKey { name: "key-percent", onclick: move |_| state.modify().toggle_percent() "%"} + } + div { class: "digit-keys" + CalculatorKey { name: "key-0", onclick: move |_| state.modify().input_digit(0), "0" } + CalculatorKey { name: "key-dot", onclick: move |_| state.modify().input_dot(), "●" } + {(1..9).map(|k| rsx!{ + CalculatorKey { key: "{k}", name: "key-{k}", onclick: move |_| state.modify().input_digit(k), "{k}" } + })} + } + div { class: "operator-keys" + CalculatorKey { name:"key-divide", onclick: move |_| state.modify().set_operator(Operator::Div) "÷" } + CalculatorKey { name:"key-multiply", onclick: move |_| state.modify().set_operator(Operator::Mul) "×" } + CalculatorKey { name:"key-subtract", onclick: move |_| state.modify().set_operator(Operator::Sub) "−" } + CalculatorKey { name:"key-add", onclick: move |_| state.modify().set_operator(Operator::Add) "+" } + CalculatorKey { name:"key-equals", onclick: move |_| state.modify().perform_operation() "=" } + } + } + } + }) +}; + +#[derive(Clone)] +struct CalculatorState { + display_value: String, + operator: Option, + cur_val: f64, +} +#[derive(Clone)] +enum Operator { + Add, + Sub, + Mul, + Div, +} +impl CalculatorState { + fn new() -> Self { + CalculatorState { + display_value: "0".to_string(), + operator: None, + cur_val: 0.0, + } + } + fn formatted(&self) -> String { + use separator::Separatable; + self.cur_val.separated_string() + } + fn clear_display(&mut self) { + self.display_value = "0".to_string(); + } + fn input_digit(&mut self, digit: u8) { + self.display_value.push_str(digit.to_string().as_str()) + } + fn input_dot(&mut self) { + if self.display_value.find(".").is_none() { + self.display_value.push_str("."); + } + } + fn perform_operation(&mut self) { + if let Some(op) = &self.operator { + let rhs = self.display_value.parse::().unwrap(); + let new_val = match op { + Operator::Add => self.cur_val + rhs, + Operator::Sub => self.cur_val - rhs, + Operator::Mul => self.cur_val * rhs, + Operator::Div => self.cur_val / rhs, + }; + self.cur_val = new_val; + self.display_value = new_val.to_string(); + self.operator = None; + } + } + fn toggle_sign(&mut self) { + if self.display_value.starts_with("-") { + self.display_value = self.display_value.trim_start_matches("-").to_string(); + } else { + self.display_value = format!("-{}", self.display_value); + } + } + fn toggle_percent(&mut self) {} + fn backspace(&mut self) { + if !self.display_value.as_str().eq("0") { + self.display_value.pop(); + } + } + fn set_operator(&mut self, operator: Operator) {} + fn handle_keydown(&mut self, evt: KeyboardEvent) { + match evt.key_code() { + KeyCode::Backspace => self.backspace(), + KeyCode::_0 => self.input_digit(0), + KeyCode::_1 => self.input_digit(1), + KeyCode::_2 => self.input_digit(2), + KeyCode::_3 => self.input_digit(3), + KeyCode::_4 => self.input_digit(4), + KeyCode::_5 => self.input_digit(5), + KeyCode::_6 => self.input_digit(6), + KeyCode::_7 => self.input_digit(7), + KeyCode::_8 => self.input_digit(8), + KeyCode::_9 => self.input_digit(9), + KeyCode::Add => self.operator = Some(Operator::Add), + KeyCode::Subtract => self.operator = Some(Operator::Sub), + KeyCode::Divide => self.operator = Some(Operator::Div), + KeyCode::Multiply => self.operator = Some(Operator::Mul), + _ => {} + } + } +} + +#[derive(Props)] +struct CalculatorKeyProps<'a> { + name: &'static str, + onclick: &'a dyn Fn(MouseEvent), +} + +fn CalculatorKey<'a, 'r>(cx: Context<'a, CalculatorKeyProps<'r>>) -> VNode<'a> { + cx.render(rsx! { + button { + class: "calculator-key {cx.name}" + onclick: {cx.onclick} + {cx.children()} + } + }) +} + +use odl::use_model; +mod odl { + use super::*; + /// Use model makes it easy to use "models" as state for components. To modify the model, call "modify" and a clone of the + /// current model will be made, with a RefMut lock on it. Dioxus will never run your components multithreaded, so you can + /// be relatively sure that this won't fail in practice + pub fn use_model<'a, T: Clone>(cx: &impl Scoped<'a>, f: impl FnOnce() -> T) -> &'a UseModel { + todo!() + } + + trait ModelAble { + type Out; + } + fn make_model(f: F) -> F::Out { + todo!() + } + + // fn use_model(w: &mut ModelWrapper) { + // let mut g1 = move || { + // // + // w.eat(); + // }; + // let mut g2 = move || { + // // + // w.eat(); + // }; + // g1(); + // g2(); + // } + + pub struct UseModel { + real: T, + wip: RefCell, + } + + use std::cell::{Ref, RefMut}; + impl Deref for UseModel { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.real + } + } + + impl UseModel { + pub fn new(t: T) -> Self { + Self { + real: t.clone(), + wip: RefCell::new(t), + } + } + pub fn modify(&self) -> RefMut<'_, T> { + self.wip.borrow_mut() + } + } + + #[derive(Clone, Copy)] + struct MyModel { + hunger: u32, + } + + impl MyModel { + fn eat(&mut self) { + self.hunger -= 1; + } + } +} diff --git a/packages/core-macro/src/rsx/component.rs b/packages/core-macro/src/rsx/component.rs index 24fc74d8e..4ed442729 100644 --- a/packages/core-macro/src/rsx/component.rs +++ b/packages/core-macro/src/rsx/component.rs @@ -19,7 +19,7 @@ use quote::{quote, ToTokens, TokenStreamExt}; use syn::{ ext::IdentExt, parse::{Parse, ParseBuffer, ParseStream}, - token, Expr, Ident, Result, Token, + token, Expr, ExprClosure, Ident, Result, Token, }; pub struct Component { @@ -153,14 +153,48 @@ impl ToTokens for Component { // the struct's fields info pub struct ComponentField { name: Ident, - content: Expr, + content: ContentField, +} + +enum ContentField { + ManExpr(Expr), + OnHandler(ExprClosure), + + // A handler was provided in {} tokens + OnHandlerRaw(Expr), +} + +impl ToTokens for ContentField { + fn to_tokens(&self, tokens: &mut TokenStream2) { + match self { + ContentField::ManExpr(e) => e.to_tokens(tokens), + ContentField::OnHandler(e) => tokens.append_all(quote! { + __cx.bump().alloc(#e) + }), + ContentField::OnHandlerRaw(e) => tokens.append_all(quote! { + __cx.bump().alloc(#e) + }), + } + } } impl Parse for ComponentField { fn parse(input: ParseStream) -> Result { let name = Ident::parse_any(input)?; input.parse::()?; - let content = input.parse()?; + + let name_str = name.to_string(); + let content = if name_str.starts_with("on") { + if input.peek(token::Brace) { + let content; + syn::braced!(content in input); + ContentField::OnHandlerRaw(content.parse()?) + } else { + ContentField::OnHandler(input.parse()?) + } + } else { + ContentField::ManExpr(input.parse::()?) + }; Ok(Self { name, content }) } diff --git a/packages/core/src/events.rs b/packages/core/src/events.rs index 4002c183d..e7e963217 100644 --- a/packages/core/src/events.rs +++ b/packages/core/src/events.rs @@ -177,10 +177,53 @@ pub mod on { } pub trait KeyboardEventInner: Debug { - fn char_code(&self) -> usize; + fn char_code(&self) -> u32; + + /// Get the key code as an enum Variant. + /// + /// This is intended for things like arrow keys, escape keys, function keys, and other non-international keys. + /// To match on unicode sequences, use the [`key`] method - this will return a string identifier instead of a limited enum. + /// + /// + /// ## Example + /// + /// ```rust + /// use dioxus::KeyCode; + /// match event.key_code() { + /// KeyCode::Escape => {} + /// KeyCode::LeftArrow => {} + /// KeyCode::RightArrow => {} + /// _ => {} + /// } + /// ``` + /// + fn key_code(&self) -> KeyCode; + + /// Check if the ctrl key was pressed down fn ctrl_key(&self) -> bool; + + /// Identify which "key" was entered. + /// + /// This is the best method to use for all languages. They key gets mapped to a String sequence which you can match on. + /// The key isn't an enum because there are just so many context-dependent keys. + /// + /// A full list on which keys to use is available at: + /// https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values + /// + /// # Example + /// + /// ```rust + /// match event.key().as_str() { + /// "Esc" | "Escape" => {} + /// "ArrowDown" => {} + /// "ArrowLeft" => {} + /// _ => {} + /// } + /// ``` + /// fn key(&self) -> String; - fn key_code(&self) -> usize; + + // fn key(&self) -> String; fn locale(&self) -> String; fn location(&self) -> usize; fn meta_key(&self) -> bool; @@ -285,6 +328,225 @@ pub mod on { } pub trait ToggleEventInner: Debug {} + + pub use util::KeyCode; + mod util { + + #[derive(Clone, Copy)] + pub enum KeyCode { + Backspace = 8, + Tab = 9, + Enter = 13, + Shift = 16, + Ctrl = 17, + Alt = 18, + Pause = 19, + CapsLock = 20, + Escape = 27, + PageUp = 33, + PageDown = 34, + End = 35, + Home = 36, + LeftArrow = 37, + UpArrow = 38, + RightArrow = 39, + DownArrow = 40, + Insert = 45, + Delete = 46, + _0 = 48, + _1 = 49, + _2 = 50, + _3 = 51, + _4 = 52, + _5 = 53, + _6 = 54, + _7 = 55, + _8 = 56, + _9 = 57, + A = 65, + B = 66, + C = 67, + D = 68, + E = 69, + F = 70, + G = 71, + H = 72, + I = 73, + J = 74, + K = 75, + L = 76, + M = 77, + N = 78, + O = 79, + P = 80, + Q = 81, + R = 82, + S = 83, + T = 84, + U = 85, + V = 86, + W = 87, + X = 88, + Y = 89, + Z = 90, + LeftWindow = 91, + RightWindow = 92, + SelectKey = 93, + Numpad0 = 96, + Numpad1 = 97, + Numpad2 = 98, + Numpad3 = 99, + Numpad4 = 100, + Numpad5 = 101, + Numpad6 = 102, + Numpad7 = 103, + Numpad8 = 104, + Numpad9 = 105, + Multiply = 106, + Add = 107, + Subtract = 109, + DecimalPoint = 110, + Divide = 111, + F1 = 112, + F2 = 113, + F3 = 114, + F4 = 115, + F5 = 116, + F6 = 117, + F7 = 118, + F8 = 119, + F9 = 120, + F10 = 121, + F11 = 122, + F12 = 123, + NumLock = 144, + ScrollLock = 145, + Semicolon = 186, + EqualSign = 187, + Comma = 188, + Dash = 189, + Period = 190, + ForwardSlash = 191, + GraveAccent = 192, + OpenBracket = 219, + BackSlash = 220, + CloseBraket = 221, + SingleQuote = 222, + Unknown, + } + + impl KeyCode { + pub fn from_raw_code(i: u8) -> Self { + use KeyCode::*; + match i { + 8 => Backspace, + 9 => Tab, + 13 => Enter, + 16 => Shift, + 17 => Ctrl, + 18 => Alt, + 19 => Pause, + 20 => CapsLock, + 27 => Escape, + 33 => PageUp, + 34 => PageDown, + 35 => End, + 36 => Home, + 37 => LeftArrow, + 38 => UpArrow, + 39 => RightArrow, + 40 => DownArrow, + 45 => Insert, + 46 => Delete, + 48 => _0, + 49 => _1, + 50 => _2, + 51 => _3, + 52 => _4, + 53 => _5, + 54 => _6, + 55 => _7, + 56 => _8, + 57 => _9, + 65 => A, + 66 => B, + 67 => C, + 68 => D, + 69 => E, + 70 => F, + 71 => G, + 72 => H, + 73 => I, + 74 => J, + 75 => K, + 76 => L, + 77 => M, + 78 => N, + 79 => O, + 80 => P, + 81 => Q, + 82 => R, + 83 => S, + 84 => T, + 85 => U, + 86 => V, + 87 => W, + 88 => X, + 89 => Y, + 90 => Z, + 91 => LeftWindow, + 92 => RightWindow, + 93 => SelectKey, + 96 => Numpad0, + 97 => Numpad1, + 98 => Numpad2, + 99 => Numpad3, + 100 => Numpad4, + 101 => Numpad5, + 102 => Numpad6, + 103 => Numpad7, + 104 => Numpad8, + 105 => Numpad9, + 106 => Multiply, + 107 => Add, + 109 => Subtract, + 110 => DecimalPoint, + 111 => Divide, + 112 => F1, + 113 => F2, + 114 => F3, + 115 => F4, + 116 => F5, + 117 => F6, + 118 => F7, + 119 => F8, + 120 => F9, + 121 => F10, + 122 => F11, + 123 => F12, + 144 => NumLock, + 145 => ScrollLock, + 186 => Semicolon, + 187 => EqualSign, + 188 => Comma, + 189 => Dash, + 190 => Period, + 191 => ForwardSlash, + 192 => GraveAccent, + 219 => OpenBracket, + 220 => BackSlash, + 221 => CloseBraket, + 222 => SingleQuote, + _ => Unknown, + } + } + + // get the raw code + fn raw_code(&self) -> u32 { + *self as u32 + } + } + } } mod tests {