├── .gitignore ├── rustfmt.toml ├── Cargo.toml ├── crates ├── smithy_types │ ├── src │ │ ├── refs.rs │ │ ├── lib.rs │ │ ├── unwrapped_promise.rs │ │ ├── component_impls.rs │ │ ├── collapsed_node.rs │ │ ├── core.rs │ │ └── events.rs │ ├── README.md │ └── Cargo.toml ├── smd_macro │ ├── src │ │ ├── parsers │ │ │ ├── mod.rs │ │ │ ├── window_event_handlers.rs │ │ │ ├── attributes.rs │ │ │ ├── many_custom.rs │ │ │ ├── util.rs │ │ │ ├── event_names.rs │ │ │ ├── core.rs │ │ │ └── make_smithy_tokens.rs │ │ ├── lib.rs │ │ └── types.rs │ └── Cargo.toml ├── smd_tests │ ├── src │ │ ├── lib.rs │ │ ├── basic_post_rendering_tests.rs │ │ ├── basic_rendering_tests.rs │ │ └── basic_event_handler_tests.rs │ └── Cargo.toml ├── smithy_core │ ├── src │ │ ├── js_fns.rs │ │ ├── with_inner_value.rs │ │ ├── zip_util.rs │ │ ├── lib.rs │ │ ├── attach_event_listeners.rs │ │ └── node_diff.rs │ └── Cargo.toml └── smithy │ ├── src │ └── lib.rs │ └── Cargo.toml ├── index.md ├── Makefile ├── CHANGELOG.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | .vscode 3 | *.swp 4 | Cargo.lock 5 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | tab_spaces = 2 2 | imports_layout = "Vertical" 3 | match_block_trailing_comma = true 4 | merge_imports = true 5 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | 3 | members = [ 4 | "crates/smd_macro", 5 | "crates/smithy_types", 6 | "crates/smithy_core", 7 | "crates/smithy", 8 | ] 9 | -------------------------------------------------------------------------------- /crates/smithy_types/src/refs.rs: -------------------------------------------------------------------------------- 1 | use web_sys::HtmlElement; 2 | 3 | pub type DomRef = Option; 4 | 5 | pub type DomRefWithPath<'a> = (Vec, &'a mut DomRef); 6 | -------------------------------------------------------------------------------- /crates/smd_macro/src/parsers/mod.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | mod many_custom; 3 | mod attributes; 4 | mod core; 5 | mod event_names; 6 | mod make_smithy_tokens; 7 | mod util; 8 | mod window_event_handlers; 9 | 10 | pub use self::core::match_html_component; 11 | -------------------------------------------------------------------------------- /crates/smithy_types/README.md: -------------------------------------------------------------------------------- 1 | # smithy_types 2 | 3 | > This crate contains the core smithy types. 4 | 5 | ## TODO 6 | 7 | * How to handle stuff like `onhashchange` 8 | * This should be splittable into `smithy_types` and `smithy_dom_types`. 9 | However, right now, it seems like if we did that, `smithy_dom_types` would 10 | contain everything. -------------------------------------------------------------------------------- /crates/smithy_types/src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate custom_derive; 2 | extern crate enum_derive; 3 | 4 | mod collapsed_node; 5 | mod component_impls; 6 | mod core; 7 | mod events; 8 | mod refs; 9 | mod unwrapped_promise; 10 | 11 | pub use self::{ 12 | collapsed_node::*, 13 | core::*, 14 | events::*, 15 | refs::*, 16 | unwrapped_promise::*, 17 | }; 18 | -------------------------------------------------------------------------------- /index.md: -------------------------------------------------------------------------------- 1 | # Smithy 2 | 3 | Welcome to the documentation for Smithy. The documentation you are looking for is [here](/smithy/). 4 | 5 | Smithy is a framework for writing WebAssembly applications entirely 6 | in Rust. 7 | Its goal is to allow you to do so using ergonomic, idiomatic Rust, 8 | without giving up any of the compiler’s safety guarantees. 9 | 10 | -------------------------------------------------------------------------------- /crates/smd_tests/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![feature(proc_macro_hygiene, slice_patterns)] 2 | 3 | mod basic_event_handler_tests; 4 | mod basic_post_rendering_tests; 5 | mod basic_rendering_tests; 6 | 7 | // TODO figure out how to test handle_ref_assignment without erroring: 8 | // thread 'basic_ref_assignment_tests::tests::stuff' panicked at 9 | // 'cannot call wasm-bindgen imported functions on non-wasm targets', 10 | // test basic_post_rendering_tests::tests::basic_post_render ... ok 11 | -------------------------------------------------------------------------------- /crates/smithy_core/src/js_fns.rs: -------------------------------------------------------------------------------- 1 | extern crate wasm_bindgen; 2 | use wasm_bindgen::prelude::*; 3 | 4 | use web_sys::Event; 5 | 6 | #[wasm_bindgen] 7 | extern "C" { 8 | #[wasm_bindgen(js_namespace = console, js_name=log)] 9 | pub fn log(msg: &str); 10 | 11 | pub type HTMLElement; 12 | 13 | #[wasm_bindgen(method, js_name=addEventListener)] 14 | pub fn attach_event_listener( 15 | this: &HTMLElement, 16 | event_name: &str, 17 | cb: &Closure, 18 | should_bubble: bool, 19 | ); 20 | 21 | pub type WINDOW; 22 | 23 | #[wasm_bindgen(method, js_name=addEventListener)] 24 | pub fn attach_event_listener(this: &WINDOW, event_name: &str, cb: &Closure); 25 | } 26 | -------------------------------------------------------------------------------- /crates/smd_macro/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "smd_macro" 3 | description = "A crate for the smd! macro" 4 | license = "MIT/Apache-2.0" 5 | version = "0.0.7" 6 | authors = ["Robert Balicki "] 7 | edition = "2018" 8 | repository = "https://github.com/rbalicki2/smithy" 9 | homepage = "https://www.smithy.rs" 10 | documentation = "https://docs.smithy.rs/smd_macro" 11 | 12 | [dependencies] 13 | smithy_types = { path = "../smithy_types", version = "0.0.7" } 14 | proc-macro2 = { version = "0.4.27", features = ["span-locations"] } 15 | quote = "0.6.10" 16 | nom = "4.1.1" 17 | lazy_static = "1.2.0" 18 | serde = "1.0.94" 19 | serde_json = "1.0.39" 20 | serde_derive = "1.0.94" 21 | 22 | [lib] 23 | proc-macro = true 24 | 25 | [features] 26 | smd-logs = [] 27 | cache-logs = [] 28 | do-not-cache-smd = [] 29 | -------------------------------------------------------------------------------- /crates/smd_tests/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "smd_tests" 3 | description = "Tests for the smd! macro" 4 | license = "MIT/Apache-2.0" 5 | version = "0.0.7" 6 | authors = ["Robert Balicki "] 7 | edition = "2018" 8 | repository = "https://github.com/rbalicki2/smithy" 9 | homepage = "https://www.smithy.rs" 10 | documentation = "https://docs.smithy.rs/smd_tests" 11 | 12 | [dependencies] 13 | smd_macro = { path = "../smd_macro", version = "0.0.7" } 14 | smithy_types = { path = "../smithy_types", version = "0.0.7" } 15 | smithy = { path = "../smithy", version = "0.0.7" } 16 | wasm-bindgen = { version = "0.2.28", features = ["nightly"] } 17 | web-sys = { version = "0.3.5", features = [] } 18 | shellexpand = "1.0.0" 19 | 20 | [workspace] 21 | members = ["."] 22 | 23 | [features] 24 | cache-logs = ["smd_macro/cache-logs"] 25 | -------------------------------------------------------------------------------- /crates/smithy/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Smithy is a framework for writing WebAssembly applications entirely 2 | //! in Rust. 3 | //! Its goal is to allow you to do so using ergonomic, idiomatic Rust, 4 | //! without giving up any of the compiler’s safety guarantees. 5 | //! 6 | //! # Example 7 | //! 8 | //! ```rs 9 | //! let app = smd!(
hello world
); 10 | //! let el_opt = web_sys::window() 11 | //! .and_then(|w| w.document()) 12 | //! .query_selector("#app"); 13 | //! if let Some(el) = el_opt { 14 | //! smithy::mount(app, el); 15 | //! } 16 | //! ``` 17 | //! 18 | //! **N.B.** these docs omit `smd!` and `smd_borrowed!`, which are 19 | //! re-exported from the [`smd_macro`](/smd_macro/) crate. 20 | 21 | /// A module that re-exports useful Smithy types, and some others. 22 | pub mod types { 23 | pub use smithy_types::*; 24 | } 25 | 26 | pub use smd_macro::*; 27 | pub use smithy_core::*; 28 | -------------------------------------------------------------------------------- /crates/smithy_core/src/with_inner_value.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | cell::RefCell, 3 | thread::LocalKey, 4 | }; 5 | 6 | pub trait WithInnerValue { 7 | fn with_inner_value(&'static self, callback: impl FnMut(&mut T) -> R) -> R; 8 | fn store(&'static self, val: T); 9 | // TODO implement 10 | // fn replace_inner_value(&'static self, callback: impl Fn(T)); 11 | } 12 | 13 | impl WithInnerValue for LocalKey>> { 14 | fn with_inner_value(&'static self, callback: impl FnOnce(&mut T) -> R) -> R { 15 | self.with(|rc| { 16 | let val_opt = rc.replace(None); 17 | // TODO don't unwrap here, but what to do instead? 18 | let mut val = val_opt.unwrap(); 19 | let new_val = callback(&mut val); 20 | rc.replace(Some(val)); 21 | new_val 22 | }) 23 | } 24 | 25 | fn store(&'static self, val: T) { 26 | self.with(|rc| { 27 | rc.replace(Some(val)); 28 | }); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | format : 2 | cargo fmt && cargo fmt -- crates/smd_tests/src/* 3 | 4 | watch : 5 | cargo watch -s 'make format' -x '+nightly build' 6 | 7 | watch-test : 8 | cargo watch -s 'make format' -s 'cd crates/smd_tests && cargo +nightly test' 9 | 10 | watch-test-nc : 11 | cargo watch -s 'make format' -s 'cd crates/smd_tests && cargo +nightly test --features=cache-logs -- --nocapture' 12 | 13 | watch-docs : 14 | RUSTDOCFLAGS="-Z unstable-options --index-page $$(pwd)/index.md" cargo watch -s 'rm -f target/doc/index.html && cargo doc -p smithy --no-deps --all-features' -w ./crates/ -w ./index.md 15 | 16 | build-docs : 17 | RUSTDOCFLAGS="-Z unstable-options --index-page $$(pwd)/index.md" cargo doc -p smithy --no-deps --all-features 18 | 19 | clear-docs : 20 | rm -rf target/doc 21 | 22 | upload-docs : 23 | aws s3 sync ./target/doc s3://smithy-rs-site/docs/prod/current --cache-control max-age=0,no-cache --acl public-read 24 | aws cloudfront create-invalidation --distribution-id E1159YR865AV4M --paths "/*" 25 | -------------------------------------------------------------------------------- /crates/smithy_core/src/zip_util.rs: -------------------------------------------------------------------------------- 1 | use std::iter::repeat_with; 2 | 3 | fn optionalize_and_extend_with_none( 4 | iter: impl Iterator, 5 | ) -> impl Iterator> { 6 | iter.map(|item| Some(item)).chain(repeat_with(|| None)) 7 | } 8 | 9 | pub fn optionalize_and_zip( 10 | left_iter: impl ExactSizeIterator, 11 | right_iter: impl ExactSizeIterator, 12 | ) -> impl Iterator, Option)> { 13 | let max_len = std::cmp::max(left_iter.len(), right_iter.len()); 14 | let left_optionalized = optionalize_and_extend_with_none(left_iter); 15 | let right_optionalized = optionalize_and_extend_with_none(right_iter); 16 | left_optionalized.zip(right_optionalized).take(max_len) 17 | } 18 | 19 | // TODO figure out why this doesn't compile 20 | // pub fn optionalize_zip_reverse_and_enumerate ( 21 | // left_iter: impl ExactSizeIterator, 22 | // right_iter: impl ExactSizeIterator, 23 | // ) -> impl Iterator, Option))> { 24 | // let should_reverse = left_iter.len() > right_iter.len(); 25 | // let zipped = optionalize_and_zip(left_iter, right_iter).enumerate(); 26 | 27 | // let ret: Box, Option))>> = if should_reverse { 28 | // let mut vec: Vec<(usize, (Option, Option))> = zipped.collect(); 29 | // vec.reverse(); 30 | // Box::new(vec.into_iter()) 31 | // } else { 32 | // Box::new(zipped) 33 | // }; 34 | 35 | // ret.map(|(i, (l, r))| (i, (l, r))) 36 | // } 37 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Version 0.0.7 (2019-09-02) 2 | 3 | * Update the README to indicate with which version of the nightly compiler Smithy is compatible. 4 | * Make Smithy more efficient: remove unnecessary code in the expansion of the `smd!` and `smd_borrowed!` macros, and move some path manipulation to compile time. 5 | 6 | # Version 0.0.6 (2019-08-08) 7 | 8 | * Update the README to point users to the `create-smithy-app`[https://github.com/rbalicki2/create-smithy-app/] 9 | repository. 10 | * Got rid of some compiler warnings. 11 | * Do not call `console_error_panic_hook` in Smithy. 12 | 13 | # Version 0.0.5 (2019-07-08) 14 | 15 | # Features 16 | 17 | * Now works with rustc 1.37.0-nightly (8ebd67e4e 2019-06-27) due to 18 | the smd! macro using a file cache instead of an in-memory cache 19 | 20 | # Version 0.0.4 (2019-06-26) 21 | 22 | ## Features and breaking changes 23 | 24 | * Renamed `smd_no_move!` to `smd_borrowed!` 25 | * Cache calls to `smd!` and `smd_borrowed!` to improve compile times 26 | * Rename the "debug-logs" feature to "browser-logs" 27 | * Additional events documentation 28 | 29 | ## Bugs 30 | 31 | * Fix a bug that caused smithy to panic when updating some interpolated 32 | variables (e.g. if `count` was updated in `
{ count }
` 33 | * Fix a bug allowing certain event related features to be enabled 34 | (including all-features) 35 | 36 | # Version 0.0.3 (2019-04-29) 37 | 38 | * Add the `smd_no_move!` macro 39 | * Add documentation 40 | * fix smd!() not compiling 41 | * fix unused import compiler warnings 42 | * add features for global events: `before-unload-events`, `hash-change-events`, `pop-state-events`, and `promise-rejection-events` 43 | * add post-rendering tests 44 | -------------------------------------------------------------------------------- /crates/smd_macro/src/parsers/window_event_handlers.rs: -------------------------------------------------------------------------------- 1 | use super::{ 2 | event_names::{ 3 | LIFECYCLE_EVENT_NAMES, 4 | WINDOW_EVENT_NAMES, 5 | }, 6 | util, 7 | }; 8 | use crate::types::{ 9 | GlobalEventHandlingInfo, 10 | LifecycleEventHandlingInfo, 11 | TokenTreeSlice, 12 | WindowEventHandlingInfo, 13 | }; 14 | 15 | #[allow(unused_imports)] 16 | use nom::{ 17 | error_position, 18 | tuple_parser, 19 | }; 20 | 21 | use nom::{ 22 | alt, 23 | apply, 24 | map, 25 | named, 26 | tuple, 27 | }; 28 | use proc_macro2::{ 29 | Delimiter, 30 | Spacing, 31 | }; 32 | use quote::quote; 33 | 34 | named!( 35 | pub match_window_event_handlers , 36 | alt!( 37 | map!( 38 | tuple!( 39 | apply!(util::match_string_from_hashmap, &WINDOW_EVENT_NAMES), 40 | apply!(util::match_punct, Some('='), Some(Spacing::Alone), vec![]), 41 | apply!(util::match_group, Some(Delimiter::Brace)), 42 | apply!(util::match_punct, Some(';'), None, vec![]) 43 | ), 44 | |(event, _, callback, _)| { 45 | GlobalEventHandlingInfo::Window(WindowEventHandlingInfo { 46 | event, 47 | callback: quote!(#callback), 48 | }) 49 | } 50 | ) 51 | | map!( 52 | tuple!( 53 | apply!(util::match_string_from_hashmap, &LIFECYCLE_EVENT_NAMES), 54 | apply!(util::match_punct, Some('='), Some(Spacing::Alone), vec![]), 55 | apply!(util::match_group, Some(Delimiter::Brace)), 56 | apply!(util::match_punct, Some(';'), None, vec![]) 57 | ), 58 | |(lifecycle_event, _, callback, _)| { 59 | GlobalEventHandlingInfo::Lifecycle(LifecycleEventHandlingInfo { 60 | lifecycle_event, 61 | callback: quote!(#callback), 62 | }) 63 | } 64 | ) 65 | ) 66 | ); 67 | -------------------------------------------------------------------------------- /crates/smd_tests/src/basic_post_rendering_tests.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod tests { 3 | use smithy::{ 4 | self, 5 | smd, 6 | types::Component, 7 | }; 8 | use std::{ 9 | cell::RefCell, 10 | rc::Rc, 11 | }; 12 | 13 | #[test] 14 | fn basic_post_render() { 15 | let post_render_has_been_called = Rc::new(RefCell::new(false)); 16 | let post_render_has_been_called_2 = post_render_has_been_called.clone(); 17 | let mut app = smd!( 18 | post_render={|| { *post_render_has_been_called.borrow_mut() = true; }}; 19 |
20 | ); 21 | 22 | app.handle_post_render(); 23 | assert!(*post_render_has_been_called_2.borrow()); 24 | } 25 | 26 | #[test] 27 | fn nested_post_render() { 28 | let post_render_has_been_called = Rc::new(RefCell::new(false)); 29 | let post_render_has_been_called_2 = post_render_has_been_called.clone(); 30 | let mut inner = smd!( 31 | post_render={|| { *post_render_has_been_called.borrow_mut() = true; }}; 32 |
33 | ); 34 | let mut outer = smd!({ &mut inner }); 35 | 36 | outer.handle_post_render(); 37 | assert!(*post_render_has_been_called_2.borrow()); 38 | } 39 | 40 | #[test] 41 | fn post_render_happens_in_order() { 42 | let post_render: Rc>> = Rc::new(RefCell::new(vec![])); 43 | let post_render_2 = post_render.clone(); 44 | let post_render_3 = post_render.clone(); 45 | 46 | let mut first = smd!( 47 | post_render={|| { post_render.borrow_mut().push("first"); }}; 48 |
49 | ); 50 | let mut second = smd!( 51 | post_render={|| { post_render_2.borrow_mut().push("second"); }}; 52 |
53 | ); 54 | let mut outer = smd!({ &mut first }{ &mut second }); 55 | 56 | outer.handle_post_render(); 57 | 58 | assert_eq!(*post_render_3.borrow(), vec!["first", "second"]); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /crates/smithy_core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "smithy_core" 3 | description = "The core Smithy functionality" 4 | license = "MIT/Apache-2.0" 5 | version = "0.0.7" 6 | authors = ["Robert Balicki "] 7 | edition = "2018" 8 | repository = "https://github.com/rbalicki2/smithy" 9 | homepage = "https://www.smithy.rs" 10 | documentation = "https://docs.smithy.rs/smithy_core" 11 | 12 | [dependencies] 13 | smithy_types = { path = "../smithy_types", version = "0.0.7" } 14 | wasm-bindgen = { version = "0.2.28", features = ["nightly"] } 15 | web-sys = { version = "0.3.5", features = [ 16 | "Element", 17 | "HtmlCollection", 18 | "Event", 19 | ] } 20 | js-sys = "0.3.5" 21 | futures = "0.1.25" 22 | wasm-bindgen-futures = "0.3.5" 23 | 24 | [features] 25 | # dom events 26 | clipboard-events = ["any-ui-events", "web-sys/ClipboardEvent"] 27 | keyboard-events = ["any-ui-events", "web-sys/KeyboardEvent"] 28 | focus-events = ["any-ui-events", "web-sys/FocusEvent"] 29 | input-events = ["any-ui-events", "web-sys/InputEvent"] 30 | mouse-events = ["any-ui-events", "web-sys/MouseEvent"] 31 | pointer-events = ["any-ui-events", "web-sys/PointerEvent"] 32 | select-events = ["web-sys-ui-events"] 33 | touch-events = ["any-ui-events", "web-sys/TouchEvent"] 34 | scroll-events = ["any-ui-events", "web-sys/ScrollAreaEvent"] 35 | image-events = ["web-sys-ui-events"] 36 | animation-events = ["any-ui-events", "web-sys/AnimationEvent"] 37 | transition-events = ["any-ui-events", "web-sys/TransitionEvent"] 38 | toggle-events = ["web-sys-ui-events"] 39 | 40 | # global events 41 | before-unload-events = ["web-sys/BeforeUnloadEvent"] 42 | hash-change-events = ["web-sys/HashChangeEvent"] 43 | pop-state-events = ["web-sys/PopStateEvent"] 44 | promise-rejection-events = ["web-sys/PromiseRejectionEvent"] 45 | 46 | # Private cfg flags... these should probably not be used by you. 47 | web-sys-ui-events = ["any-ui-events", "web-sys/UiEvent"] 48 | any-ui-events = [] 49 | 50 | browser-logs = ["web-sys/console"] 51 | event-logs = ["web-sys/console"] 52 | -------------------------------------------------------------------------------- /crates/smithy_types/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "smithy_types" 3 | description = "types related to Smithy" 4 | license = "MIT/Apache-2.0" 5 | version = "0.0.7" 6 | authors = ["Robert Balicki "] 7 | edition = "2018" 8 | repository = "https://github.com/rbalicki2/smithy" 9 | homepage = "https://www.smithy.rs" 10 | documentation = "https://docs.smithy.rs/smithy_types" 11 | 12 | [dependencies] 13 | web-sys = { version = "0.3.5", features = [ 14 | "Document", 15 | "DocumentFragment", 16 | "Element", 17 | "Window", 18 | "HtmlElement", 19 | "EventTarget", 20 | "Node", 21 | "NodeList", 22 | "Event", 23 | ] } 24 | enum_derive = "0.1.7" 25 | custom_derive = "0.1.7" 26 | futures = "0.1.25" 27 | wasm-bindgen = { version = "0.2.29", features = ["nightly"] } 28 | wasm-bindgen-futures = "0.3.5" 29 | lazy_static = "1.2.0" 30 | 31 | [features] 32 | # dom events 33 | clipboard-events = ["any-ui-events", "web-sys/ClipboardEvent"] 34 | keyboard-events = ["any-ui-events", "web-sys/KeyboardEvent"] 35 | focus-events = ["any-ui-events", "web-sys/FocusEvent"] 36 | input-events = ["any-ui-events", "web-sys/InputEvent"] 37 | mouse-events = ["any-ui-events", "web-sys/MouseEvent"] 38 | pointer-events = ["any-ui-events", "web-sys/PointerEvent"] 39 | select-events = ["web-sys-ui-events"] 40 | touch-events = ["any-ui-events", "web-sys/TouchEvent"] 41 | scroll-events = ["any-ui-events", "web-sys/ScrollAreaEvent"] 42 | image-events = ["web-sys-ui-events"] 43 | animation-events = ["any-ui-events", "web-sys/AnimationEvent"] 44 | transition-events = ["any-ui-events", "web-sys/TransitionEvent"] 45 | toggle-events = ["web-sys-ui-events"] 46 | 47 | # global events 48 | before-unload-events = ["web-sys/BeforeUnloadEvent"] 49 | hash-change-events = ["web-sys/HashChangeEvent"] 50 | pop-state-events = ["web-sys/PopStateEvent"] 51 | promise-rejection-events = ["web-sys/PromiseRejectionEvent"] 52 | 53 | # Private cfg flags... these should probably not be used. 54 | web-sys-ui-events = ["any-ui-events", "web-sys/UiEvent"] 55 | any-ui-events = ["web-sys/Event"] 56 | -------------------------------------------------------------------------------- /crates/smithy_types/src/unwrapped_promise.rs: -------------------------------------------------------------------------------- 1 | use futures::Future; 2 | use std::{ 3 | cell::RefCell, 4 | ops::Deref, 5 | rc::Rc, 6 | }; 7 | use wasm_bindgen::JsValue; 8 | use wasm_bindgen_futures::{ 9 | future_to_promise, 10 | JsFuture, 11 | }; 12 | 13 | /// A wrapper around a future that can easily be rendered with 14 | /// a match statement. 15 | /// 16 | /// It is used by Smithy to create promises that also cause smithy 17 | /// to re-render when they are completed. 18 | pub struct UnwrappedPromise { 19 | promise_state: Rc>>, 20 | #[allow(dead_code)] // future must not be dropped before promise_state 21 | future: Box>, 22 | } 23 | 24 | impl UnwrappedPromise { 25 | pub fn new( 26 | future: impl Future + 'static, 27 | callback: Option, 28 | ) -> Self { 29 | let data = Rc::new(RefCell::new(PromiseState::Pending)); 30 | let data_1 = data.clone(); 31 | let data_2 = data.clone(); 32 | 33 | let callback = Rc::new(RefCell::new(callback)); 34 | let callback_1 = callback.clone(); 35 | 36 | let future = future 37 | .map(move |s| { 38 | *data_1.borrow_mut() = PromiseState::Success(s); 39 | if let Some(ref cb) = *callback.borrow() { 40 | cb(); 41 | }; 42 | JsValue::NULL 43 | }) 44 | .map_err(move |e| { 45 | *data_2.borrow_mut() = PromiseState::Error(e); 46 | if let Some(ref cb) = *callback_1.borrow() { 47 | cb(); 48 | }; 49 | JsValue::NULL 50 | }); 51 | // execute the future 52 | let future = Box::new(JsFuture::from(future_to_promise(future))); 53 | let unwrapped_promise = UnwrappedPromise { 54 | promise_state: data, 55 | future, 56 | }; 57 | unwrapped_promise 58 | } 59 | } 60 | 61 | impl Deref for UnwrappedPromise { 62 | type Target = Rc>>; 63 | fn deref(&self) -> &Rc>> { 64 | &self.promise_state 65 | } 66 | } 67 | 68 | /// An enum representing the three states of a Javascript promise. 69 | #[derive(Clone, Debug)] 70 | pub enum PromiseState { 71 | Pending, 72 | Success(S), 73 | Error(E), 74 | } 75 | -------------------------------------------------------------------------------- /crates/smithy_types/src/component_impls.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | Component, 3 | EventHandled, 4 | Node, 5 | Path, 6 | UiEvent, 7 | }; 8 | 9 | /** 10 | * N.B. this is subject to change! We want to be smart about how we 11 | * impl Component for common types, especially related to container types 12 | * (Vec, Option, Box, etc.) 13 | */ 14 | 15 | macro_rules! basic_impl_component { 16 | ($type:ty) => { 17 | impl Component for $type { 18 | fn render(&mut self) -> Node { 19 | Node::Text(self.to_string()) 20 | } 21 | } 22 | }; 23 | } 24 | 25 | basic_impl_component!(&str); 26 | 27 | // TODO determine whether basic_impl_component! will handle these cases. 28 | impl Component for String { 29 | fn render(&mut self) -> Node { 30 | Node::Text(self.clone()) 31 | } 32 | } 33 | 34 | impl Component for &String { 35 | fn render(&mut self) -> Node { 36 | Node::Text((*self).to_string()) 37 | } 38 | } 39 | 40 | impl Component for Vec 41 | where 42 | T: Component, 43 | { 44 | fn render(&mut self) -> Node { 45 | let nodes = self.iter_mut().map(|i| i.render()).collect::>(); 46 | Node::Vec(nodes) 47 | } 48 | 49 | fn handle_ui_event(&mut self, event: &UiEvent, path: &Path) -> EventHandled { 50 | // TODO maybe make this more functional 51 | if let Some((first, rest)) = path.split_first() { 52 | if let Some(target_node) = self.get_mut(*first) { 53 | target_node.handle_ui_event(event, rest) 54 | } else { 55 | false 56 | } 57 | } else { 58 | false 59 | } 60 | } 61 | } 62 | 63 | impl Component for Option 64 | where 65 | T: Component, 66 | { 67 | fn render(&mut self) -> Node { 68 | match self { 69 | Some(t) => t.render(), 70 | None => Node::Comment(None), 71 | } 72 | } 73 | 74 | fn handle_ui_event(&mut self, event: &UiEvent, path: &Path) -> EventHandled { 75 | match self { 76 | Some(t) => t.handle_ui_event(event, path), 77 | None => false, 78 | } 79 | } 80 | } 81 | 82 | basic_impl_component!(bool); 83 | basic_impl_component!(char); 84 | basic_impl_component!(i8); 85 | basic_impl_component!(i16); 86 | basic_impl_component!(i32); 87 | basic_impl_component!(i64); 88 | basic_impl_component!(isize); 89 | basic_impl_component!(u8); 90 | basic_impl_component!(u16); 91 | basic_impl_component!(u32); 92 | basic_impl_component!(u64); 93 | basic_impl_component!(usize); 94 | basic_impl_component!(f32); 95 | basic_impl_component!(f64); 96 | -------------------------------------------------------------------------------- /crates/smithy/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "smithy" 3 | description = "Smithy, a framework for web development" 4 | version = "0.0.7" 5 | authors = ["Robert Balicki "] 6 | edition = "2018" 7 | keywords = ["web", "javascript", "webassembly", "wasm", "frontend"] 8 | categories = ["gui", "web-programming"] 9 | license = "MIT/Apache-2.0" 10 | repository = "https://github.com/rbalicki2/smithy" 11 | homepage = "https://www.smithy.rs" 12 | documentation = "https://docs.smithy.rs/smithy" 13 | 14 | [dependencies] 15 | smd_macro = { path = "../smd_macro", version = "0.0.7" } 16 | smithy_core = { path = "../smithy_core", version = "0.0.7" } 17 | smithy_types = { path = "../smithy_types", version = "0.0.7" } 18 | 19 | [features] 20 | default = ["keyboard-events", "input-events"] 21 | 22 | # dom events 23 | clipboard-events = ["smithy_core/clipboard-events", "smithy_types/clipboard-events"] 24 | keyboard-events = ["smithy_core/keyboard-events", "smithy_types/keyboard-events"] 25 | focus-events = ["smithy_core/focus-events", "smithy_types/focus-events"] 26 | input-events = ["smithy_core/input-events", "smithy_types/input-events"] 27 | mouse-events = ["smithy_core/mouse-events", "smithy_types/mouse-events"] 28 | pointer-events = ["smithy_core/pointer-events", "smithy_types/pointer-events"] 29 | select-events = ["smithy_core/select-events", "smithy_types/select-events"] 30 | touch-events = ["smithy_core/touch-events", "smithy_types/touch-events"] 31 | scroll-events = ["smithy_core/scroll-events", "smithy_types/scroll-events"] 32 | image-events = ["smithy_core/image-events", "smithy_types/image-events"] 33 | animation-events = ["smithy_core/animation-events", "smithy_types/animation-events"] 34 | transition-events = ["smithy_core/transition-events", "smithy_types/transition-events"] 35 | toggle-events = ["smithy_core/toggle-events", "smithy_types/toggle-events"] 36 | 37 | # window events 38 | before-unload-events = ["smithy_core/before-unload-events", "smithy_types/before-unload-events"] 39 | hash-change-events = ["smithy_core/hash-change-events", "smithy_types/hash-change-events"] 40 | pop-state-events = ["smithy_core/pop-state-events", "smithy_types/pop-state-events"] 41 | promise-rejection-events = ["smithy_core/promise-rejection-events", "smithy_types/promise-rejection-events"] 42 | 43 | all-events = [ 44 | "clipboard-events", 45 | "keyboard-events", 46 | "focus-events", 47 | "input-events", 48 | "mouse-events", 49 | "pointer-events", 50 | "select-events", 51 | "touch-events", 52 | "scroll-events", 53 | "image-events", 54 | "animation-events", 55 | "transition-events", 56 | "toggle-events", 57 | 58 | "before-unload-events", 59 | "hash-change-events", 60 | "pop-state-events", 61 | "promise-rejection-events", 62 | ] 63 | 64 | smd-logs = ["smd_macro/smd-logs"] 65 | do-not-cache-smd = ["smd_macro/do-not-cache-smd"] 66 | cache-logs = ["smd_macro/cache-logs"] 67 | 68 | browser-logs = ["smithy_core/browser-logs"] 69 | event-logs = ["smithy_core/event-logs"] 70 | -------------------------------------------------------------------------------- /crates/smd_tests/src/basic_rendering_tests.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod tests { 3 | use smithy::{ 4 | self, 5 | smd, 6 | types::*, 7 | }; 8 | use std::collections::HashMap; 9 | 10 | fn get_bare_div() -> Node { 11 | Node::Dom(HtmlToken { 12 | node_type: "div".into(), 13 | attributes: HashMap::new(), 14 | children: vec![], 15 | }) 16 | } 17 | 18 | fn get_bare_div_as_vec() -> Node { 19 | Node::Vec(vec![get_bare_div()]) 20 | } 21 | 22 | #[test] 23 | fn self_closing_div() { 24 | let mut div = smd!(
); 25 | assert_eq!(div.render(), get_bare_div_as_vec()); 26 | } 27 | 28 | #[test] 29 | fn div() { 30 | let mut div = smd!(
); 31 | assert_eq!(div.render(), get_bare_div_as_vec()); 32 | } 33 | 34 | #[test] 35 | fn div_with_html_children() { 36 | let mut div = smd!(

); 37 | let result = Node::Vec(vec![Node::Dom(HtmlToken { 38 | node_type: "div".into(), 39 | attributes: HashMap::new(), 40 | children: vec![Node::Dom(HtmlToken { 41 | node_type: "h1".into(), 42 | attributes: HashMap::new(), 43 | children: vec![], 44 | })], 45 | })]); 46 | assert_eq!(div.render(), result); 47 | } 48 | 49 | #[test] 50 | fn div_with_text_children() { 51 | let mut div = smd!(
hello
); 52 | let result = Node::Vec(vec![Node::Dom(HtmlToken { 53 | node_type: "div".into(), 54 | attributes: HashMap::new(), 55 | children: vec![Node::Text("hello".into())], 56 | })]); 57 | assert_eq!(div.render(), result); 58 | } 59 | 60 | #[test] 61 | fn div_with_group_component_children() { 62 | let mut inner = smd!(); 63 | let mut outer = smd!({ &mut inner }); 64 | let result = Node::Vec(vec![Node::Dom(HtmlToken { 65 | node_type: "outer".into(), 66 | attributes: HashMap::new(), 67 | children: vec![Node::Vec(vec![Node::Dom(HtmlToken { 68 | node_type: "inner".into(), 69 | attributes: HashMap::new(), 70 | children: vec![], 71 | })])], 72 | })]); 73 | assert_eq!(outer.render(), result); 74 | } 75 | 76 | #[test] 77 | fn div_with_group_text_children() { 78 | let mut outer = smd!({ "inner" }); 79 | let result = Node::Vec(vec![Node::Dom(HtmlToken { 80 | node_type: "outer".into(), 81 | attributes: HashMap::new(), 82 | children: vec![Node::Text("inner".into())], 83 | })]); 84 | assert_eq!(outer.render(), result); 85 | } 86 | 87 | #[test] 88 | fn multiple_adjacent_divs() { 89 | let mut divs = smd!(
); 90 | let result = Node::Vec(vec![get_bare_div(), get_bare_div()]); 91 | assert_eq!(divs.render(), result); 92 | } 93 | 94 | #[test] 95 | fn empty_macro() { 96 | let mut no_dom = smd!(); 97 | let result = Node::Vec(vec![]); 98 | assert_eq!(no_dom.render(), result); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /crates/smd_macro/src/parsers/attributes.rs: -------------------------------------------------------------------------------- 1 | use super::{ 2 | event_names::UI_EVENT_NAMES, 3 | util, 4 | }; 5 | use crate::types::{ 6 | AttributeOrEventHandler, 7 | TokenTreeSlice, 8 | }; 9 | 10 | #[allow(unused_imports)] 11 | use nom::{ 12 | error_position, 13 | tuple_parser, 14 | }; 15 | 16 | use nom::{ 17 | alt, 18 | apply, 19 | call, 20 | map, 21 | named, 22 | tuple, 23 | }; 24 | use proc_macro2::{ 25 | Delimiter, 26 | TokenStream, 27 | }; 28 | 29 | // N.B. there are three types of attributes values supported: 30 | // groups, empty and literals. Obviously, only groups can hold event handlers 31 | // (callbacks), so we either call assert_is_not_event_handler or we 32 | // switch on whether it is an event handler. 33 | // 34 | // In the EventHandler case, the string *is changed to upper camel case* 35 | // to match the Event type! Beware! 36 | // (Should we use a newtype?) 37 | 38 | fn assert_is_not_event_handler(string: &String) { 39 | assert!( 40 | !UI_EVENT_NAMES.contains_key(string), 41 | format!( 42 | "attribute {} is an event name, but was not followed by curly braces", 43 | string 44 | ) 45 | ); 46 | } 47 | 48 | impl AttributeOrEventHandler { 49 | fn create_from_string(string: String, t: TokenStream) -> AttributeOrEventHandler { 50 | match UI_EVENT_NAMES.get(&string) { 51 | Some((event_name, _should_include_rest_param)) => { 52 | AttributeOrEventHandler::EventHandler((event_name.to_string(), t)) 53 | }, 54 | None => { 55 | // TODO make this less awkward 56 | if string == "ref" { 57 | AttributeOrEventHandler::DomRef(t) 58 | } else { 59 | AttributeOrEventHandler::Attribute((string, t)) 60 | } 61 | }, 62 | } 63 | } 64 | } 65 | 66 | named!( 67 | pub match_attribute , 68 | alt!( 69 | alt!( 70 | map!( 71 | tuple!( 72 | apply!(util::match_ident, None, false), 73 | apply!(util::match_punct, Some('='), None, vec![]), 74 | map!(apply!(util::match_group, Some(Delimiter::Brace)), super::util::enquote) 75 | ), 76 | |val| { 77 | AttributeOrEventHandler::create_from_string(val.0, val.2) 78 | } 79 | ) 80 | | map!( 81 | tuple!( 82 | apply!(util::match_ident, None, false), 83 | apply!(util::match_punct, Some('='), None, vec![]), 84 | map!(call!(util::match_literal), super::util::enquote) 85 | ), 86 | |val| { 87 | assert_is_not_event_handler(&val.0); 88 | AttributeOrEventHandler::Attribute((val.0, val.2)) 89 | } 90 | ) 91 | | map!( 92 | apply!(util::match_ident, None, false), 93 | |attr_name| { 94 | assert_is_not_event_handler(&attr_name); 95 | AttributeOrEventHandler::Attribute((attr_name, quote::quote!(""))) 96 | } 97 | ) 98 | ) 99 | ) 100 | ); 101 | -------------------------------------------------------------------------------- /crates/smd_macro/src/parsers/many_custom.rs: -------------------------------------------------------------------------------- 1 | macro_rules! many_0_custom( 2 | ($i:expr, $submac:ident!( $($args:tt)* )) => ( 3 | { 4 | use ::nom::lib::std::result::Result::*; 5 | // use ::nom::{Err,AtEof}; 6 | use ::nom::Err; 7 | 8 | // ret is Ok|Err 9 | let ret; 10 | let mut vec_of_responses = ::nom::lib::std::vec::Vec::new(); 11 | let mut input = $i.clone(); 12 | 13 | loop { 14 | let input_ = input.clone(); 15 | match $submac!(input_, $($args)*) { 16 | Ok((i, o)) => { 17 | // i is remaining 18 | // o is matched 19 | 20 | // N.B. I don't know if this is actually solves the infinite loops... 21 | 22 | // loop trip must always consume (otherwise infinite loops) 23 | if i.len() == 0 || i.len() == input.len() { 24 | vec_of_responses.push(o); 25 | ret = Ok((input, vec_of_responses)); 26 | break; 27 | } 28 | // if i == input { 29 | // if i.at_eof() { 30 | // ret = Ok((input, res)); 31 | // } else { 32 | // ret = Err(Err::Error(error_position!(input, ::nom::ErrorKind::Many0))); 33 | // } 34 | // break; 35 | // } 36 | vec_of_responses.push(o); 37 | 38 | input = i; 39 | }, 40 | Err(Err::Error(_)) => { 41 | ret = Ok((input, vec_of_responses)); 42 | break; 43 | }, 44 | Err(e) => { 45 | ret = Err(e); 46 | break; 47 | }, 48 | } 49 | } 50 | 51 | ret 52 | } 53 | ); 54 | ($i:expr, $f:expr) => ( 55 | many_0_custom!($i, call!($f)); 56 | ); 57 | ); 58 | 59 | /// `many1!(I -> IResult) => I -> IResult>` 60 | /// Applies the parser 1 or more times and returns the list of results in a Vec 61 | /// 62 | /// the embedded parser may return Incomplete 63 | /// 64 | /// ``` 65 | /// # #[macro_use] extern crate nom; 66 | /// # use nom::Err; 67 | /// # use nom::ErrorKind; 68 | /// # fn main() { 69 | /// named!(multi<&[u8], Vec<&[u8]> >, many1!( tag!( "abcd" ) ) ); 70 | /// 71 | /// let a = b"abcdabcdefgh"; 72 | /// let b = b"azerty"; 73 | /// 74 | /// let res = vec![&b"abcd"[..], &b"abcd"[..]]; 75 | /// assert_eq!(multi(&a[..]),Ok((&b"efgh"[..], res))); 76 | /// assert_eq!(multi(&b[..]), Err(Err::Error(error_position!(&b[..], ErrorKind::Many1)))); 77 | /// # } 78 | /// ``` 79 | macro_rules! many_1_custom( 80 | // N.B. I added a nom:: here before $submac. What is going on? 81 | ($i:expr, nom::$submac:ident!( $($args:tt)* )) => ( 82 | { 83 | use nom::lib::std::result::Result::*; 84 | use nom::Err; 85 | 86 | use nom::InputLength; 87 | let i_ = $i.clone(); 88 | match $submac!(i_, $($args)*) { 89 | Err(Err::Error(_)) => Err(Err::Error( 90 | nom::error_position!(i_, nom::ErrorKind::Many1) 91 | )), 92 | Err(Err::Failure(_)) => Err(Err::Failure( 93 | nom::error_position!(i_, nom::ErrorKind::Many1) 94 | )), 95 | Err(i) => Err(i), 96 | Ok((i1,o1)) => { 97 | let mut res = nom::lib::std::vec::Vec::with_capacity(4); 98 | res.push(o1); 99 | let mut input = i1; 100 | let mut error = nom::lib::std::option::Option::None; 101 | loop { 102 | let input_ = input.clone(); 103 | match $submac!(input_, $($args)*) { 104 | Err(Err::Error(_)) => { 105 | break; 106 | }, 107 | Err(e) => { 108 | error = nom::lib::std::option::Option::Some(e); 109 | break; 110 | }, 111 | Ok((i, o)) => { 112 | if i.input_len() == input.input_len() { 113 | break; 114 | } 115 | res.push(o); 116 | input = i; 117 | } 118 | } 119 | } 120 | 121 | match error { 122 | nom::lib::std::option::Option::Some(e) => Err(e), 123 | nom::lib::std::option::Option::None => Ok((input, res)) 124 | } 125 | } 126 | } 127 | } 128 | ); 129 | ($i:expr, $f:expr) => ( 130 | many_1_custom!($i, nom::call!($f)); 131 | ); 132 | ); 133 | -------------------------------------------------------------------------------- /crates/smithy_types/src/collapsed_node.rs: -------------------------------------------------------------------------------- 1 | use crate::Node; 2 | 3 | type Path = Vec; 4 | 5 | /// An enum representing the types of nodes that can be present in the DOM. 6 | /// 7 | /// A `Vec` is generated from a `Node` by calling 8 | /// `node.into_collapsed_node` on it. This will concatenate adjacent strings, 9 | /// and flattening any `Node::Vec`'s. 10 | /// 11 | /// That is, a `CollapsedNode` is meant to be a closer representation of the 12 | /// DOM than a `Node`. 13 | #[derive(Debug, Clone, Eq, PartialEq)] 14 | pub enum CollapsedNode { 15 | Dom(CollapsedHtmlToken), 16 | Text(String), 17 | Comment(Option), 18 | } 19 | 20 | /// A struct representing an element node in the DOM. 21 | #[derive(Debug, Clone, Eq, PartialEq)] 22 | pub struct CollapsedHtmlToken { 23 | pub node_type: String, 24 | pub children: Vec, 25 | pub attributes: crate::Attributes, 26 | pub path: Vec, 27 | } 28 | 29 | impl CollapsedHtmlToken { 30 | pub fn get_attributes_including_path(&self) -> crate::Attributes { 31 | let mut attributes = self.attributes.clone(); 32 | attributes.insert( 33 | "data-smithy-path".to_string(), 34 | self 35 | .path 36 | .iter() 37 | .map(|u| u.to_string()) 38 | .collect::>() 39 | .join(","), 40 | ); 41 | attributes 42 | } 43 | } 44 | 45 | fn clone_and_extend(path: &Path, next_item: usize) -> Path { 46 | let mut path = path.clone(); 47 | path.extend(&[next_item]); 48 | path 49 | } 50 | 51 | impl Node { 52 | pub fn into_collapsed_node(self, path: Path) -> Vec { 53 | // Here be monsters... 54 | // 55 | // What are we doing? 56 | // 1. If Node is a Dom/Vec (i.e. iterable), we flat_map over each child 57 | // and collect that into a vec of CollapsedNode's. 58 | // If Node is a Text/Comment, we collect that into a vec of length 1. 59 | let node_vec = match self { 60 | Node::Dom(html_token) => vec![CollapsedNode::Dom(CollapsedHtmlToken { 61 | path: path.clone(), 62 | node_type: html_token.node_type, 63 | attributes: html_token.attributes, 64 | children: { 65 | // this is weird. We're wrapping children in a Node::Vec and collapsing 66 | // that. It would make more sense to implement Into> on 67 | // Vec, presumably. 68 | Node::Vec(html_token.children).into_collapsed_node(path) 69 | }, 70 | })], 71 | Node::Text(text) => vec![CollapsedNode::Text(text)], 72 | Node::Comment(comment_opt) => vec![CollapsedNode::Comment(comment_opt)], 73 | Node::Vec(vec) => vec 74 | .into_iter() 75 | .enumerate() 76 | .flat_map(|(i, node)| node.into_collapsed_node(clone_and_extend(&path, i))) 77 | .collect(), 78 | }; 79 | 80 | // 2. We *super jankily* combine all adjacent CollapsedNode::Text's into single 81 | // CollapsedNode's. 82 | let len = node_vec.len(); 83 | let (mut node_vec, str_opt) = node_vec.into_iter().fold( 84 | (Vec::with_capacity(len), None), 85 | |(vec_so_far, str_opt), node| { 86 | let mut push = false; 87 | let mut ret = match (&node, &str_opt) { 88 | (CollapsedNode::Text(text), Some(s)) => (vec_so_far, Some(format!("{}{}", s, text))), 89 | (CollapsedNode::Text(text), None) => (vec_so_far, Some(text.to_string())), 90 | _ => { 91 | push = true; 92 | (vec_so_far, str_opt) 93 | }, 94 | }; 95 | let ret = if push { 96 | if let Some(s) = ret.1 { 97 | ret.0.push(CollapsedNode::Text(s)); 98 | }; 99 | ret.0.push(node); 100 | (ret.0, None) 101 | } else { 102 | ret 103 | }; 104 | 105 | ret 106 | }, 107 | ); 108 | 109 | // 3. If there were terminal CollapsedNode::Text's, we need to push those onto the vec. 110 | if let Some(s) = str_opt { 111 | node_vec.push(CollapsedNode::Text(s)); 112 | } 113 | 114 | node_vec 115 | } 116 | } 117 | 118 | impl Into> for Node { 119 | // TODO collapse text nodes... but maybe we do? 120 | // N.B. this is only correct if this is being called on the top level. 121 | // TODO remove this and call into_collapsed_node(vec![]) directly to be more explicit 122 | fn into(self) -> Vec { 123 | self.into_collapsed_node(vec![]) 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /crates/smd_macro/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! A crate containing the `smd` and `smd_borrowed` macros, which are the 2 | //! workhorses that generate `SmithyComponent`s. 3 | 4 | #![feature(proc_macro_span, proc_macro_raw_ident, slice_patterns)] 5 | #![recursion_limit = "128"] 6 | #![feature(drain_filter)] 7 | 8 | #[cfg(not(feature = "do-not-cache-smd"))] 9 | use std::{ 10 | collections::HashMap, 11 | fs::{ 12 | create_dir_all, 13 | read_to_string, 14 | write, 15 | }, 16 | path::Path, 17 | }; 18 | 19 | extern crate proc_macro; 20 | 21 | mod parsers; 22 | mod types; 23 | 24 | /// proc-macro to take a `SmithyComponent`, capturing referenced variables. 25 | #[proc_macro] 26 | pub fn smd(input: proc_macro::TokenStream) -> proc_macro::TokenStream { 27 | smd_inner(input, true) 28 | } 29 | 30 | /// proc-macro to take a `SmithyComponent`, not capturing referenced variables. 31 | /// 32 | /// A call to `smd_borrowed!` should usually be inside of a call to `smd!`. 33 | #[proc_macro] 34 | pub fn smd_borrowed(input: proc_macro::TokenStream) -> proc_macro::TokenStream { 35 | smd_inner(input, false) 36 | } 37 | 38 | // TODO rename should_move to is_borrowed and invert the value 39 | // since that makes more sense. 40 | #[cfg(not(feature = "do-not-cache-smd"))] 41 | fn get_file_path(should_move: bool) -> String { 42 | format!( 43 | "{}/.smd/{}{}", 44 | std::env::var("HOME").unwrap(), 45 | env!("CARGO_PKG_VERSION"), 46 | if should_move { "" } else { "-borrowed" } 47 | ) 48 | } 49 | 50 | #[cfg(not(feature = "do-not-cache-smd"))] 51 | type StringMap = HashMap; 52 | 53 | #[cfg(not(feature = "do-not-cache-smd"))] 54 | fn read_hash_map(should_move: bool) -> Result { 55 | let path = get_file_path(should_move); 56 | read_to_string(path) 57 | .map_err(|_| ()) 58 | .and_then(|s| serde_json::from_str(&s).map_err(|_| ())) 59 | } 60 | 61 | /// Attempts to write a hash map to the appropriate location. 62 | /// May fail silently, we don't really care. 63 | #[cfg(not(feature = "do-not-cache-smd"))] 64 | fn write_hash_map(map: &StringMap, should_move: bool) { 65 | let path = get_file_path(should_move); 66 | let parent = Path::new(&path).parent().unwrap(); 67 | 68 | let _ = create_dir_all(parent); 69 | let _ = write(path, serde_json::to_string(map).unwrap()); 70 | } 71 | 72 | fn smd_inner(input: proc_macro::TokenStream, should_move: bool) -> proc_macro::TokenStream { 73 | #[cfg(not(feature = "do-not-cache-smd"))] 74 | let input_as_str = input.to_string(); 75 | 76 | let parse_input = || { 77 | let input: proc_macro2::TokenStream = input.into(); 78 | let vec_of_trees: Vec = input.into_iter().collect(); 79 | let parsed = parsers::match_html_component(&vec_of_trees, should_move); 80 | 81 | let unwrapped = parsed.unwrap(); 82 | #[cfg(feature = "smd-logs")] 83 | println!("\nlet mut app = {};\n", unwrapped.1); 84 | 85 | let proc_macro_result: proc_macro::TokenStream = unwrapped.1.into(); 86 | proc_macro_result 87 | }; 88 | 89 | #[cfg(not(feature = "do-not-cache-smd"))] 90 | match read_hash_map(should_move) { 91 | Ok(mut map) => match map.get(&input_as_str) { 92 | Some(cached_item) => match cached_item.parse() { 93 | Ok(item) => { 94 | #[cfg(feature = "cache-logs")] 95 | println!("Item found in hashmap"); 96 | item 97 | }, 98 | Err(_) => { 99 | #[cfg(feature = "cache-logs")] 100 | println!("Error parsing item from hashmap"); 101 | // We encountered an item that was improperly written. We need to 102 | // overwrite that item and re-write the hashmap to disk. 103 | let proc_macro_result = parse_input(); 104 | map.insert(input_as_str, proc_macro_result.to_string()); 105 | write_hash_map(&map, should_move); 106 | proc_macro_result 107 | }, 108 | }, 109 | None => { 110 | #[cfg(feature = "cache-logs")] 111 | println!("Cached item not found"); 112 | let proc_macro_result = parse_input(); 113 | map.insert(input_as_str, proc_macro_result.to_string()); 114 | write_hash_map(&map, should_move); 115 | proc_macro_result 116 | }, 117 | }, 118 | Err(_) => { 119 | #[cfg(feature = "cache-logs")] 120 | println!("Could not deserialize hashmap!!!"); 121 | let proc_macro_result = parse_input(); 122 | let map = { 123 | let mut map = HashMap::new(); 124 | map.insert(input_as_str, proc_macro_result.to_string()); 125 | map 126 | }; 127 | write_hash_map(&map, should_move); 128 | proc_macro_result 129 | }, 130 | } 131 | 132 | #[cfg(feature = "do-not-cache-smd")] 133 | parse_input() 134 | } 135 | -------------------------------------------------------------------------------- /crates/smithy_core/src/lib.rs: -------------------------------------------------------------------------------- 1 | use smithy_types::{ 2 | AsInnerHtml, 3 | CollapsedNode, 4 | Component, 5 | Path, 6 | UiEvent, 7 | UnwrappedPromise, 8 | WindowEvent, 9 | }; 10 | use web_sys::{ 11 | Element, 12 | Window, 13 | }; 14 | mod with_inner_value; 15 | use self::with_inner_value::*; 16 | use futures::Future; 17 | use std::{ 18 | cell::RefCell, 19 | mem::transmute, 20 | }; 21 | 22 | mod attach_event_listeners; 23 | mod js_fns; 24 | mod node_diff; 25 | mod zip_util; 26 | 27 | use self::node_diff::{ 28 | ApplicableTo, 29 | Diffable, 30 | }; 31 | 32 | // TODO this should not be thread-local, but should be instantiated inside of 33 | // mount(). This will *probably* require us to not call crate::rerender in some 34 | // callbacks. 35 | thread_local! { 36 | static ROOT_ELEMENT: RefCell> = RefCell::new(None); 37 | static LAST_RENDERED_NODE: RefCell>> = RefCell::new(None); 38 | static ROOT_COMPONENT: RefCell>> = RefCell::new(None); 39 | static EVENT_DEPTH: RefCell = RefCell::new(0); 40 | } 41 | 42 | fn get_window() -> Window { 43 | web_sys::window().unwrap() 44 | } 45 | 46 | fn render_initially(component: &mut Box, el: &Element) { 47 | let node: Vec = component.render().into(); 48 | el.set_inner_html(&node.as_inner_html()); 49 | LAST_RENDERED_NODE.store(node); 50 | } 51 | 52 | fn with_increased_event_depth(f: impl Fn() -> T) -> T { 53 | EVENT_DEPTH.with(|depth| { 54 | let existing_depth = *depth.borrow(); 55 | *depth.borrow_mut() = existing_depth + 1; 56 | }); 57 | let ret = f(); 58 | EVENT_DEPTH.with(|depth| { 59 | let existing_depth = *depth.borrow(); 60 | *depth.borrow_mut() = existing_depth - 1; 61 | }); 62 | ret 63 | } 64 | 65 | fn get_event_depth() -> u32 { 66 | EVENT_DEPTH.with(|depth| *depth.borrow()) 67 | } 68 | 69 | fn event_handling_phase_is_ongoing() -> bool { 70 | get_event_depth() > 0 71 | } 72 | 73 | #[allow(dead_code)] 74 | fn handle_window_event(w: &WindowEvent) -> bool { 75 | with_increased_event_depth(|| { 76 | ROOT_COMPONENT.with_inner_value(|root_component| root_component.handle_window_event(w)) 77 | }) 78 | } 79 | 80 | #[allow(dead_code)] 81 | fn handle_ui_event(ui_event: &UiEvent, path: &Path) -> bool { 82 | with_increased_event_depth(|| { 83 | ROOT_COMPONENT 84 | .with_inner_value(|root_component| root_component.handle_ui_event(ui_event, &path)) 85 | }) 86 | } 87 | 88 | fn attach_listeners(el: &Element) { 89 | let html_el = unsafe { transmute::<&Element, &js_fns::HTMLElement>(el) }; 90 | attach_event_listeners::attach_ui_event_listeners(&html_el); 91 | 92 | let window = get_window(); 93 | let window = unsafe { transmute::(window) }; 94 | attach_event_listeners::attach_window_event_listeners(&window); 95 | } 96 | 97 | /// Forces the currently mounted smithy app to re-render. 98 | pub fn rerender() { 99 | ROOT_COMPONENT.with_inner_value(|root_component| { 100 | let newly_rendered_nodes: Vec = root_component.render().into(); 101 | 102 | LAST_RENDERED_NODE.with_inner_value(|last_rendered_node| { 103 | let diff = last_rendered_node.get_diff_with(&newly_rendered_nodes); 104 | #[cfg(feature = "browser-logs")] 105 | web_sys::console::log_1(&wasm_bindgen::JsValue::from_str(&format!( 106 | "\n\n\nrerender\n------------------------\n\nfrom {:?}\n\nto {:?}\n\ndiff {:#?}\n\n", 107 | last_rendered_node.as_inner_html(), 108 | newly_rendered_nodes.as_inner_html(), 109 | diff 110 | ))); 111 | ROOT_ELEMENT.with_inner_value(|el| { 112 | #[cfg(feature = "browser-logs")] 113 | web_sys::console::log_1(&wasm_bindgen::JsValue::from_str(&format!( 114 | "\n\nroot el inner {:?}", 115 | el.inner_html() 116 | ))); 117 | for diff_item in diff.iter() { 118 | diff_item.apply_to(el); 119 | } 120 | }); 121 | }); 122 | 123 | root_component.handle_ref_assignment(vec![]); 124 | root_component.handle_post_render(); 125 | LAST_RENDERED_NODE.store(newly_rendered_nodes); 126 | }); 127 | } 128 | 129 | /// Mounts the smithy app at the specified element. 130 | /// 131 | /// # Examples 132 | /// 133 | /// ```rs 134 | /// let app = smd!(
hello world
); 135 | /// let el_opt = web_sys::window() 136 | /// .and_then(|w| w.document()) 137 | /// .query_selector("#app"); 138 | /// if let Some(el) = el_opt { 139 | /// smithy::mount(app, el); 140 | /// } 141 | /// ``` 142 | pub fn mount(mut component: Box, el: Element) { 143 | render_initially(&mut component, &el); 144 | attach_listeners(&el); 145 | ROOT_ELEMENT.store(el); 146 | component.handle_ref_assignment(vec![]); 147 | component.handle_post_render(); 148 | ROOT_COMPONENT.store(component); 149 | } 150 | 151 | /// Converts a future into an `UnwrappedPromise`, which causes the 152 | /// app to re-render when the future succeeds or fails. 153 | pub fn unwrapped_promise_from_future( 154 | future: impl Future + 'static, 155 | ) -> UnwrappedPromise { 156 | UnwrappedPromise::new(future, Some(rerender)) 157 | } 158 | -------------------------------------------------------------------------------- /crates/smd_macro/src/types.rs: -------------------------------------------------------------------------------- 1 | pub use proc_macro2::{ 2 | TokenStream, 3 | TokenTree, 4 | }; 5 | use quote::quote; 6 | 7 | #[derive(Debug)] 8 | pub struct UIEventHandlingInfo { 9 | pub reversed_path: Vec, 10 | /// if event is None, this implies that this is a UIEventHandlingInfo 11 | /// for a group (e.g. { &mut child_el }). 12 | pub event: Option, 13 | /// callback is actually the TokenStream group... it's a really bad name :( 14 | pub callback: TokenStream, 15 | } 16 | 17 | impl UIEventHandlingInfo { 18 | pub fn from_string_token_stream_pair((event, callback): StringTokenStreamPair) -> Self { 19 | UIEventHandlingInfo { 20 | reversed_path: vec![], 21 | event: Some(event), 22 | callback, 23 | } 24 | } 25 | 26 | /// N.B. this also reverses the path 27 | pub fn get_path_match(&self, include_rest_param: bool) -> TokenStream { 28 | let inner = self 29 | .reversed_path 30 | .iter() 31 | .rev() 32 | .fold(quote! {}, |accum, path_item| { 33 | quote! { #accum #path_item, } 34 | }); 35 | let additional_dot_dot = if include_rest_param { 36 | quote! { rest @ .. } 37 | } else { 38 | quote! {} 39 | }; 40 | quote! { 41 | [ #inner #additional_dot_dot ] 42 | } 43 | } 44 | 45 | pub fn is_group(&self) -> bool { 46 | !self.event.is_some() 47 | } 48 | 49 | /// N.B. what should this be called??? 50 | /// TODO have two different types, Group and TrueUIEventHandlingInfo 51 | /// except better named... 52 | pub fn split_into_groups( 53 | mut vec: Vec, 54 | ) -> (Vec, Vec) { 55 | let groups = vec.drain_filter(|info| info.is_group()).collect(); 56 | (groups, vec) 57 | } 58 | } 59 | 60 | #[derive(Debug)] 61 | pub struct DomRefInfo { 62 | // TODO is this path actually reversed...? 63 | pub reversed_path: Vec, 64 | pub dom_ref: TokenStream, 65 | } 66 | 67 | impl DomRefInfo { 68 | pub fn from_token_stream(t: TokenStream) -> DomRefInfo { 69 | DomRefInfo { 70 | dom_ref: t, 71 | reversed_path: vec![], 72 | } 73 | } 74 | } 75 | 76 | pub type TokenTreeSlice<'a> = &'a [TokenTree]; 77 | 78 | // TODO rename, perhaps to TokenStreamEventHandlingInfoDomRefOptTrio 79 | // ... or something 80 | pub type TokenStreamEventHandlingInfoPair = 81 | (TokenStream, Vec, Vec); 82 | 83 | pub type StringTokenStreamPair = (String, TokenStream); 84 | pub enum AttributeOrEventHandler { 85 | Attribute(StringTokenStreamPair), 86 | EventHandler(StringTokenStreamPair), 87 | DomRef(TokenStream), 88 | } 89 | 90 | pub struct SplitAttributeOrEventHandlers( 91 | pub Vec, 92 | pub Vec, 93 | pub Vec, 94 | ); 95 | impl Into for Vec { 96 | fn into(self) -> SplitAttributeOrEventHandlers { 97 | let len = self.len(); 98 | let attributes = Vec::with_capacity(len); 99 | let event_handlers = Vec::with_capacity(len); 100 | self.into_iter().fold( 101 | SplitAttributeOrEventHandlers(attributes, event_handlers, vec![]), 102 | |SplitAttributeOrEventHandlers(mut attributes, mut event_handlers, mut dom_ref), next_val| { 103 | match next_val { 104 | AttributeOrEventHandler::Attribute(attr) => attributes.push(attr), 105 | AttributeOrEventHandler::EventHandler(event_handler) => { 106 | event_handlers.push(event_handler) 107 | }, 108 | AttributeOrEventHandler::DomRef(dom_ref_token) => { 109 | dom_ref.push(DomRefInfo::from_token_stream(dom_ref_token)) 110 | }, 111 | }; 112 | SplitAttributeOrEventHandlers(attributes, event_handlers, dom_ref) 113 | }, 114 | ) 115 | } 116 | } 117 | 118 | pub struct SplitTokenStreamEventHandlingInfoPairs( 119 | pub Vec, 120 | pub Vec, 121 | pub Vec, 122 | ); 123 | impl Into for Vec { 124 | fn into(self) -> SplitTokenStreamEventHandlingInfoPairs { 125 | let child_token_streams = Vec::with_capacity(self.len()); 126 | let child_event_handling_infos = vec![]; 127 | let child_dom_ref_token_streams = vec![]; 128 | self.into_iter().enumerate().fold( 129 | SplitTokenStreamEventHandlingInfoPairs( 130 | child_token_streams, 131 | child_event_handling_infos, 132 | child_dom_ref_token_streams, 133 | ), 134 | |SplitTokenStreamEventHandlingInfoPairs( 135 | mut child_token_streams, 136 | mut child_event_handling_infos, 137 | mut child_dom_ref_token_streams, 138 | ), 139 | (i, item)| { 140 | child_token_streams.push(item.0); 141 | for mut current_event_handling_info in item.1.into_iter() { 142 | current_event_handling_info.reversed_path.push(i); 143 | child_event_handling_infos.push(current_event_handling_info); 144 | } 145 | for mut current_dom_ref_info in item.2.into_iter() { 146 | current_dom_ref_info.reversed_path.push(i); 147 | child_dom_ref_token_streams.push(current_dom_ref_info); 148 | } 149 | SplitTokenStreamEventHandlingInfoPairs( 150 | child_token_streams, 151 | child_event_handling_infos, 152 | child_dom_ref_token_streams, 153 | ) 154 | }, 155 | ) 156 | } 157 | } 158 | 159 | #[derive(Debug)] 160 | pub struct WindowEventHandlingInfo { 161 | pub event: String, 162 | pub callback: TokenStream, 163 | } 164 | 165 | #[derive(Debug)] 166 | pub struct LifecycleEventHandlingInfo { 167 | pub lifecycle_event: String, 168 | pub callback: TokenStream, 169 | } 170 | 171 | #[derive(Debug)] 172 | pub enum GlobalEventHandlingInfo { 173 | Window(WindowEventHandlingInfo), 174 | Lifecycle(LifecycleEventHandlingInfo), 175 | } 176 | -------------------------------------------------------------------------------- /crates/smd_macro/src/parsers/util.rs: -------------------------------------------------------------------------------- 1 | use crate::types::*; 2 | use proc_macro2::{ 3 | Delimiter, 4 | Group, 5 | Literal, 6 | Spacing, 7 | TokenStream, 8 | }; 9 | use quote::{ 10 | quote, 11 | ToTokens, 12 | }; 13 | use std::iter; 14 | 15 | pub type TtsIResult<'a, T> = nom::IResult, T>; 16 | pub type StringResult<'a> = TtsIResult<'a, String>; 17 | 18 | pub fn match_string_from_hashmap<'a>( 19 | input: TokenTreeSlice<'a>, 20 | map: &std::collections::HashMap, 21 | ) -> TtsIResult<'a, String> { 22 | let get_err = || { 23 | Err(nom::Err::Error(nom::error_position!( 24 | input, 25 | nom::ErrorKind::Custom(42) 26 | ))) 27 | }; 28 | 29 | match input.split_first() { 30 | Some((first, rest)) => match first { 31 | TokenTree::Ident(ref ident) => { 32 | let s = ident.to_string(); 33 | let val_opt = map.get(&s); 34 | match val_opt { 35 | Some(val) => Ok((rest, val.to_string())), 36 | None => get_err(), 37 | } 38 | }, 39 | _ => get_err(), 40 | }, 41 | None => get_err(), 42 | } 43 | } 44 | 45 | pub fn match_punct( 46 | input: TokenTreeSlice, 47 | c_opt: Option, 48 | spacing_opt: Option, 49 | excluded_chars: Vec, 50 | ) -> StringResult { 51 | let get_err = || { 52 | Err(nom::Err::Error(nom::error_position!( 53 | input, 54 | nom::ErrorKind::Custom(42) 55 | ))) 56 | }; 57 | let filler_spaces = get_filler_spaces(input); 58 | 59 | match input.split_first() { 60 | Some((first, rest)) => match first { 61 | TokenTree::Punct(ref punct) => { 62 | let wrong_char = c_opt.map(|c| punct.as_char() != c).unwrap_or(false); 63 | let wrong_spacing = spacing_opt 64 | .map(|spacing| punct.spacing() != spacing) 65 | .unwrap_or(false); 66 | let contains_excluded_char = excluded_chars.contains(&punct.as_char()); 67 | 68 | if wrong_char || wrong_spacing || contains_excluded_char { 69 | get_err() 70 | } else { 71 | Ok((rest, format!("{}{}", punct.as_char(), filler_spaces))) 72 | } 73 | }, 74 | _ => get_err(), 75 | }, 76 | None => get_err(), 77 | } 78 | } 79 | 80 | pub fn match_ident( 81 | input: TokenTreeSlice, 82 | sym_opt: Option, 83 | include_filler: bool, 84 | ) -> StringResult { 85 | let get_err = || { 86 | Err(nom::Err::Error(nom::error_position!( 87 | input, 88 | nom::ErrorKind::Custom(42) 89 | ))) 90 | }; 91 | 92 | let filler_spaces = if include_filler { 93 | get_filler_spaces(input) 94 | } else { 95 | "".into() 96 | }; 97 | match input.split_first() { 98 | Some((first, rest)) => match first { 99 | TokenTree::Ident(ref ident) => { 100 | let get_success = || Ok((rest, format!("{}{}", ident, filler_spaces))); 101 | match sym_opt { 102 | Some(s) => { 103 | if s == format!("{}", ident) { 104 | get_success() 105 | } else { 106 | get_err() 107 | } 108 | }, 109 | None => get_success(), 110 | } 111 | }, 112 | _ => get_err(), 113 | }, 114 | None => get_err(), 115 | } 116 | } 117 | 118 | pub type GroupResult<'a> = TtsIResult<'a, Group>; 119 | 120 | pub fn match_group(input: TokenTreeSlice, delimiter_opt: Option) -> GroupResult { 121 | let get_err = || { 122 | Err(nom::Err::Error(nom::error_position!( 123 | input, 124 | nom::ErrorKind::Custom(42) 125 | ))) 126 | }; 127 | 128 | match input.split_first() { 129 | Some((first, rest)) => match first { 130 | TokenTree::Group(ref group) => { 131 | let get_success = || Ok((rest, group.clone())); 132 | match delimiter_opt { 133 | Some(delimiter) => { 134 | if group.delimiter() == delimiter { 135 | get_success() 136 | } else { 137 | get_err() 138 | } 139 | }, 140 | None => get_success(), 141 | } 142 | }, 143 | _ => get_err(), 144 | }, 145 | None => get_err(), 146 | } 147 | } 148 | 149 | pub fn match_literal(input: TokenTreeSlice) -> TtsIResult { 150 | let get_err = || { 151 | Err(nom::Err::Error(nom::error_position!( 152 | input, 153 | nom::ErrorKind::Custom(42) 154 | ))) 155 | }; 156 | 157 | match input.split_first() { 158 | Some((first, rest)) => match first { 159 | TokenTree::Literal(literal) => Ok((rest, literal.clone())), 160 | _ => get_err(), 161 | }, 162 | None => get_err(), 163 | } 164 | } 165 | 166 | pub fn match_literal_as_string(input: TokenTreeSlice) -> TtsIResult { 167 | let filler_spaces = get_filler_spaces(input); 168 | match_literal(input).map(|(rest, lit)| (rest, format!("{}{}", lit.to_string(), filler_spaces))) 169 | } 170 | 171 | pub fn match_empty(input: TokenTreeSlice) -> TtsIResult> { 172 | if input.len() == 0 { 173 | Ok((input, vec![])) 174 | } else { 175 | Err(nom::Err::Error(nom::error_position!( 176 | input, 177 | nom::ErrorKind::Custom(42) 178 | ))) 179 | } 180 | } 181 | 182 | pub fn get_filler_spaces(input: TokenTreeSlice) -> String { 183 | let first_opt = input.get(0).map(|i| i.span().end()); 184 | let second_opt = input.get(1).map(|i| i.span().start()); 185 | match (first_opt, second_opt) { 186 | (Some(first), Some(second)) => { 187 | if first.line != second.line { 188 | "".into() 189 | } else { 190 | iter::repeat(" ") 191 | .take(second.column - first.column) 192 | .collect::() 193 | } 194 | }, 195 | _ => "".into(), 196 | } 197 | } 198 | 199 | // Poorly named function... takes a vec of TokenStreams and combines them into a TokenStream 200 | // like { let mut vec = vec![]; push each item; vec } 201 | fn reduce_vec_to_tokens(v: &Vec) -> proc_macro2::TokenStream { 202 | let vec_contents = v 203 | .iter() 204 | .fold(quote!(), |existing, token| quote!(#existing vec.push(#token);)); 205 | let len = v.len(); 206 | quote!({ 207 | let mut vec = Vec::with_capacity(#len); 208 | #vec_contents 209 | vec 210 | }) 211 | } 212 | 213 | pub fn reduce_vec_to_node(v: &Vec) -> proc_macro2::TokenStream { 214 | let inner = reduce_vec_to_tokens(v); 215 | quote! { 216 | smithy::types::Node::Vec(#inner) 217 | } 218 | } 219 | 220 | pub fn enquote(t: T) -> TokenStream { 221 | quote!(#t) 222 | } 223 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Smithy 2 | 3 | > [Smithy](https://www.smithy.rs) is a front-end framework for Rust 4 | 5 | [Home page](https://www.smithy.rs) ◇ [API Docs](https://docs.smithy.rs/smithy/) ◇ [Repository](https://github.com/rbalicki2/smithy) ◇ [Example Sites](https://www.smithy.rs/examples) 6 | 7 | ## What is Smithy? 8 | 9 | Smithy is a framework for writing WebAssembly applications entirely in Rust. Its goal is to allow you to do so using idiomatic Rust, without giving up any of the compiler's safety guarantees. 10 | 11 | ## Smithy works on nightly 12 | 13 | Smithy v0.0.7 currently works on `1.39.0-nightly (dfd43f0fd 2019-09-01)`. 14 | 15 | ## Getting started 16 | 17 | Getting started in Smithy is easy! 18 | 19 | ```sh 20 | npm init smithy-app my_smithy_app 21 | cd my_smithy_app 22 | npm start 23 | ``` 24 | 25 | Navigate to `localhost:8080` to see your app in action! 26 | 27 | See [the create-smithy-app repository](https://github.com/rbalicki2/create-smithy-app/) for more details. 28 | 29 | ## A simple Smithy app 30 | 31 | A simple click counter is as follows: 32 | 33 | ```rust 34 | #[wasm_bindgen(start)] 35 | pub fn start() -> Result<(), wasm_bindgen::JsValue> { 36 | let root_element = get_root_element()?; 37 | let mut count = 0; 38 | let app = smithy::smd!( 39 |
40 | I have been clicked {count}{' '}times. 41 |
42 | ); 43 | smithy::mount(Box::new(app), root_element); 44 | Ok(()) 45 | } 46 | 47 | fn get_root_element() -> Result { 48 | let document = web_sys::window().unwrap().document().unwrap(); 49 | document.get_element_by_id("app") 50 | .ok_or(wasm_bindgen::JsValue::NULL) 51 | } 52 | ``` 53 | 54 | ## How Smithy works 55 | 56 | ### `smd!` macro 57 | 58 | The `smd!` and `smd_borrowed!` macros convert something that looks like JSX into a wrapper around an `FnMut(smithy::types::Phase) -> smithy::types::PhaseResult`. For example, the `smd!` call in: 59 | 60 | ```rust 61 | let mut count = 0; 62 | let app = smithy::smd!( 63 |
64 | I have been clicked {count}{' '}times. 65 |
66 | ); 67 | ``` 68 | 69 | is converted into 70 | 71 | ```rust 72 | let mut app = { 73 | #[allow(dead_code)] 74 | use smithy::types::Component; 75 | let component: smithy::types::SmithyComponent = 76 | smithy::types::SmithyComponent(Box::new(move |phase| match phase { 77 | smithy::types::Phase::Rendering => { 78 | smithy::types::PhaseResult::Rendering(smithy::types::Node::Vec(vec![ 79 | smithy::types::Node::Dom(smithy::types::HtmlToken { 80 | node_type: "div".into(), 81 | attributes: std::collections::HashMap::new(), 82 | children: { 83 | let mut children = Vec::with_capacity(4usize); 84 | children.push(smithy::types::Node::Text("I have been clicked ".into())); 85 | children.push({ count }.render()); 86 | children.push({ ' ' }.render()); 87 | children.push(smithy::types::Node::Text("times.".into())); 88 | children 89 | }, 90 | }), 91 | ])) 92 | }, 93 | smithy::types::Phase::UiEventHandling(ui_event_handling) => match ui_event_handling { 94 | (evt, [0usize, 1usize, rest @ ..]) => { 95 | smithy::types::PhaseResult::UiEventHandling({ count }.handle_ui_event(evt, rest)) 96 | }, 97 | (evt, [0usize, 2usize, rest @ ..]) => { 98 | smithy::types::PhaseResult::UiEventHandling({ ' ' }.handle_ui_event(evt, rest)) 99 | }, 100 | (smithy::types::UiEvent::OnClick(val), [0usize, rest @ ..]) => { 101 | ({ |_| count = count + 1 })(val); 102 | smithy::types::PhaseResult::UiEventHandling(true) 103 | }, 104 | _ => smithy::types::PhaseResult::UiEventHandling(false), 105 | }, 106 | smithy::types::Phase::WindowEventHandling(window_event) => { 107 | let mut event_handled = false; 108 | event_handled = ({ count }).handle_window_event(window_event) || event_handled; 109 | event_handled = ({ ' ' }).handle_window_event(window_event) || event_handled; 110 | match window_event { 111 | _ => smithy::types::PhaseResult::WindowEventHandling(event_handled), 112 | } 113 | }, 114 | smithy::types::Phase::PostRendering => { 115 | { 116 | { 117 | ({ count }).handle_post_render(); 118 | } 119 | ({ ' ' }).handle_post_render(); 120 | } 121 | smithy::types::PhaseResult::PostRendering 122 | }, 123 | smithy::types::Phase::RefAssignment(path_so_far) => { 124 | let new_path = path_so_far 125 | .clone() 126 | .into_iter() 127 | .chain(vec![0usize, 1usize]) 128 | .collect(); 129 | ({ count }).handle_ref_assignment(new_path); 130 | let new_path = path_so_far 131 | .clone() 132 | .into_iter() 133 | .chain(vec![0usize, 2usize]) 134 | .collect(); 135 | ({ ' ' }).handle_ref_assignment(new_path); 136 | smithy::types::PhaseResult::RefAssignment 137 | }, 138 | })); 139 | component 140 | }; 141 | ``` 142 | 143 | Notice that the `|_| count = count + 1` and `{count}` are in separate branches of the match arm. If they had not been (e.g. if `smd!` created a struct instead of an `FnMut`), this would not have compiled. The borrow checker would have complained that you cannot immutably borrow `count`, as it is already borrowed mutably in the `on_click` callback. 144 | 145 | ### Smithy phases 146 | 147 | As you can see from the expansion of the `smd!` macro above, phases are a core concept in Smithy. In particular, an app is driven through five phases: 148 | 149 | * rendering, in which the app is asked to return a struct containing the information about what it will write to the DOM. 150 | * ref assignment, in which any app with `ref={&mut optional_web_sys_html_element}` will have `Some(some_html_element)` assigned to that ref. 151 | * post rendering, in which any `post_render={|_| ...}` callbacks will be executed. These callbacks are guaranteed to have all refs already assigned, thus allowing you to do any direct DOM manipulation you need to do. 152 | * UI event handling and window event handling, in which Smithy executes callbacks in response to events. After a callback is executed, Smithy will re-run the app through the different phases. 153 | * (UI event handling and window event handling are treated as separate phases, though conceptually they are very similar.) 154 | 155 | ## `smd!` vs `smd_borrowed!` 156 | 157 | As you can see in the macro expansion above, the `smd!` macro creates a move closure. This is not always desirable. If you do not wish to create a move closure, use `smd_borrowed!` instead. 158 | 159 | ## How to get involved 160 | 161 | Smithy is always looking for contributors! Please tweet at me `@statisticsftw` or take a look at the [Smithy roadmap](https://github.com/rbalicki2/smithy/issues/2). 162 | 163 | In addition, please take Smithy out for a spin using [create-smithy-app](https://github.com/rbalicki2/create-smithy-app/). 164 | 165 | Thanks! Happy coding! 166 | -------------------------------------------------------------------------------- /crates/smithy_types/src/core.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | CollapsedHtmlToken, 3 | CollapsedNode, 4 | }; 5 | use custom_derive::custom_derive; 6 | use enum_derive::{ 7 | enum_derive_util, 8 | EnumFromInner, 9 | }; 10 | 11 | pub type Attributes = std::collections::HashMap; 12 | 13 | custom_derive! { 14 | /// An enum representing the different types of nodes, plus a special 15 | /// wrapper `Node::Vec`. 16 | /// 17 | /// A `Node` represents the result of a call to `.render()` from the 18 | /// `Component` interface. It does not exactly represent node tree in 19 | /// the DOM. Rather, `CollapsedNode` is a closer representation of the 20 | /// DOM. 21 | #[derive(Debug, Clone, EnumFromInner, Eq, PartialEq)] 22 | pub enum Node { 23 | Dom(HtmlToken), 24 | Text(String), 25 | Vec(Vec), 26 | Comment(Option), 27 | } 28 | } 29 | 30 | pub trait AsInnerHtml { 31 | fn as_inner_html(&self) -> String; 32 | } 33 | 34 | impl AsInnerHtml for Vec { 35 | fn as_inner_html(&self) -> String { 36 | self.iter().map(|node| node.as_inner_html()).collect() 37 | } 38 | } 39 | 40 | impl AsInnerHtml for CollapsedNode { 41 | fn as_inner_html(&self) -> String { 42 | match self { 43 | CollapsedNode::Dom(token) => token.as_inner_html(), 44 | CollapsedNode::Text(s) => s.to_string(), 45 | CollapsedNode::Comment(str_opt) => match str_opt { 46 | Some(s) => format!("", s), 47 | None => "".into(), 48 | }, 49 | } 50 | } 51 | } 52 | 53 | /// A struct representing an HTML element. 54 | #[derive(Debug, Clone, Eq, PartialEq)] 55 | pub struct HtmlToken { 56 | pub node_type: String, 57 | pub children: Vec, 58 | pub attributes: Attributes, 59 | } 60 | 61 | fn format_attributes(attr: &Attributes) -> String { 62 | // TODO is calling `format!` bad from a package size perspective? 63 | attr.iter().fold("".to_string(), |accum, (key, val)| { 64 | if val != "" { 65 | format!("{} {}=\"{}\"", accum, key, val) 66 | } else { 67 | format!("{} {}", accum, key) 68 | } 69 | }) 70 | } 71 | 72 | fn format_path(path: &Path) -> String { 73 | // the take() function takes a usize, which cannot be negative, thus this might 74 | // panic if not for this check. 75 | if path.len() > 0 { 76 | let path_str = path.iter().fold("".to_string(), |accum, path_segment| { 77 | format!("{}{},", accum, path_segment) 78 | }); 79 | path_str.chars().take(path_str.len() - 1).collect() 80 | } else { 81 | "".to_string() 82 | } 83 | } 84 | 85 | use lazy_static::lazy_static; 86 | lazy_static! { 87 | static ref VOID_TAGS: std::collections::HashSet = { 88 | // see https://www.w3.org/TR/2011/WD-html-markup-20110113/syntax.html#syntax-elements 89 | // area, base, br, col, command, embed, hr, img, input, keygen, link, meta, param, source, track, wbr 90 | let mut void_tags = std::collections::HashSet::new(); 91 | void_tags.insert("area".to_string()); 92 | void_tags.insert("base".to_string()); 93 | void_tags.insert("br".to_string()); 94 | void_tags.insert("col".to_string()); 95 | void_tags.insert("command".to_string()); 96 | void_tags.insert("embed".to_string()); 97 | void_tags.insert("hr".to_string()); 98 | void_tags.insert("img".to_string()); 99 | void_tags.insert("input".to_string()); 100 | void_tags.insert("keygen".to_string()); 101 | void_tags.insert("link".to_string()); 102 | void_tags.insert("meta".to_string()); 103 | void_tags.insert("param".to_string()); 104 | void_tags.insert("source".to_string()); 105 | void_tags.insert("track".to_string()); 106 | void_tags.insert("wbr".to_string()); 107 | void_tags 108 | }; 109 | } 110 | 111 | impl AsInnerHtml for CollapsedHtmlToken { 112 | fn as_inner_html(&self) -> String { 113 | let path_string = format!(" data-smithy-path=\"{}\"", format_path(&self.path)); 114 | let attributes_string = if self.attributes.len() > 0 { 115 | format!(" {}", format_attributes(&self.attributes)) 116 | } else { 117 | "".to_string() 118 | }; 119 | 120 | if !VOID_TAGS.contains(&self.node_type) { 121 | let child_html = self 122 | .children 123 | .iter() 124 | .map(|node| node.as_inner_html()) 125 | .collect::>() 126 | .join(""); 127 | format!( 128 | "<{}{}{}>{}", 129 | self.node_type, attributes_string, path_string, child_html, self.node_type 130 | ) 131 | } else { 132 | format!("<{}{}{} />", self.node_type, attributes_string, path_string) 133 | } 134 | } 135 | } 136 | 137 | pub type Path = [usize]; 138 | 139 | /// An enum representing the different phases that a Smithy app can go through. 140 | /// 141 | /// A call to `smd!` is a `SmithyComponent`, which is a wrapper around a 142 | /// `Box PhaseResult>`. The content of this 143 | /// function is a match statement over the `Phase` parameter. 144 | pub enum Phase<'a> { 145 | Rendering, 146 | PostRendering, 147 | UiEventHandling((&'a crate::UiEvent, &'a Path)), 148 | WindowEventHandling(&'a crate::WindowEvent), 149 | RefAssignment(Vec), 150 | } 151 | 152 | pub type EventHandled = bool; 153 | 154 | /// An enum representing the results of a `SmithyComponent` handling a `Phase`. 155 | /// 156 | /// A call to `smd!` is a `SmithyComponent`, which is a wrapper around a 157 | /// `Box PhaseResult>`. 158 | /// 159 | /// The data contained in the `PhaseResult` will inform the future behavior of 160 | /// the app. For example, when responding to an event, the app will re-render 161 | /// as long as there was at least one handler for that event. That information 162 | /// is contained in the `EventHandled` data. 163 | #[derive(Debug)] 164 | pub enum PhaseResult { 165 | // TODO make this an Option 166 | Rendering(Node), 167 | PostRendering, 168 | UiEventHandling(EventHandled), 169 | WindowEventHandling(EventHandled), 170 | RefAssignment, 171 | } 172 | 173 | impl PhaseResult { 174 | pub fn unwrap_node(self) -> Node { 175 | match self { 176 | PhaseResult::Rendering(node) => node, 177 | _ => panic!("unwrap_node called on PhaseResult that was not of variant Rendering"), 178 | } 179 | } 180 | 181 | pub fn unwrap_event_handled(self) -> EventHandled { 182 | match self { 183 | PhaseResult::UiEventHandling(event_handled) => event_handled, 184 | PhaseResult::WindowEventHandling(event_handled) => event_handled, 185 | _ => { 186 | panic!("unwrap_event_handled called on PhaseResult that was not of variant UiEventHandling or WindowEventHandling") 187 | }, 188 | } 189 | } 190 | } 191 | 192 | /// The results of calling the `smd!` macro is a vector of `SmithyComponent`s. 193 | pub struct SmithyComponent<'a>(pub Box PhaseResult + 'a>); 194 | 195 | /// The main trait of Smithy. 196 | pub trait Component { 197 | fn render(&mut self) -> Node; 198 | fn handle_post_render(&mut self) {} 199 | fn handle_ref_assignment(&mut self, _path_so_far: Vec) {} 200 | fn handle_ui_event(&mut self, _event: &crate::UiEvent, _path: &Path) -> EventHandled { 201 | false 202 | } 203 | fn handle_window_event(&mut self, _event: &crate::WindowEvent) -> EventHandled { 204 | false 205 | } 206 | } 207 | 208 | impl<'a> Component for SmithyComponent<'a> { 209 | fn handle_ui_event(&mut self, event: &crate::UiEvent, path: &Path) -> EventHandled { 210 | self.0(Phase::UiEventHandling((event, path))).unwrap_event_handled() 211 | } 212 | 213 | fn handle_window_event(&mut self, event: &crate::WindowEvent) -> EventHandled { 214 | self.0(Phase::WindowEventHandling(event)).unwrap_event_handled() 215 | } 216 | 217 | fn render(&mut self) -> Node { 218 | self.0(Phase::Rendering).unwrap_node() 219 | } 220 | 221 | fn handle_post_render(&mut self) { 222 | self.0(Phase::PostRendering); 223 | } 224 | 225 | fn handle_ref_assignment(&mut self, path_so_far: Vec) { 226 | self.0(Phase::RefAssignment(path_so_far)); 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /crates/smd_macro/src/parsers/event_names.rs: -------------------------------------------------------------------------------- 1 | use lazy_static::lazy_static; 2 | use std::collections::HashMap; 3 | // see attributes.rs for an explanation 4 | 5 | lazy_static! { 6 | pub static ref UI_EVENT_NAMES: HashMap = { 7 | let mut event_names = HashMap::new(); 8 | 9 | // TODO figure out why I can't wrap this in if (cfg![test]) 10 | event_names.insert("on_test".into(), ("OnTest".into(), false)); 11 | // --Clipboard 12 | // #[cfg(feature = "clipboard-events")] 13 | { 14 | event_names.insert("on_copy".into(), ("OnCopy".into(), false)); 15 | event_names.insert("on_cut".into(), ("OnCut".into(), false)); 16 | event_names.insert("on_paste".into(), (("OnPaste".into()), false)); 17 | } 18 | // --Composition 19 | // onCompositionEnd 20 | // onCompositionStart 21 | // onCompositionUpdate 22 | // --Keyboard 23 | // #[cfg(feature = "keyboard-events")] 24 | { 25 | event_names.insert("on_key_down".into(), ("OnKeyDown".into(), false)); 26 | event_names.insert("on_key_press".into(), ("OnKeyPress".into(), false)); 27 | event_names.insert("on_key_up".into(), ("OnKeyUp".into(), false)); 28 | } 29 | // --Focus 30 | // #[cfg(feature = "focus-events")] 31 | { 32 | event_names.insert("on_focus".into(), ("OnFocus".into(), false)); 33 | event_names.insert("on_blur".into(), ("OnBlur".into(), false)); 34 | } 35 | // --Form 36 | // #[cfg(feature = "input-events")] 37 | { 38 | event_names.insert("on_change".into(), ("OnChange".into(), false)); 39 | event_names.insert("on_input".into(), ("OnInput".into(), false)); 40 | event_names.insert("on_invalid".into(), ("OnInvalid".into(), false)); 41 | event_names.insert("on_submit".into(), ("OnSubmit".into(), false)); 42 | } 43 | // --Mouse 44 | // #[cfg(feature = "mouse-events")] 45 | { 46 | event_names.insert("on_click".into(), ("OnClick".into(), true)); 47 | event_names.insert("on_context_menu".into(), ("OnContextMenu".into(), true)); 48 | event_names.insert("on_dbl_click".into(), ("OnDblClick".into(), true)); 49 | 50 | event_names.insert("on_drag".into(), ("OnDrag".into(), false)); 51 | event_names.insert("on_drag_end".into(), ("OnDragEnd".into(), false)); 52 | event_names.insert("on_drag_enter".into(), ("OnDragEnter".into(), false)); 53 | event_names.insert("on_drag_exit".into(), ("OnDragExit".into(), false)); 54 | event_names.insert("on_drag_leave".into(), ("OnDragLeave".into(), false)); 55 | event_names.insert("on_drag_over".into(), ("OnDragOver".into(), true)); 56 | event_names.insert("on_drag_start".into(), ("OnDragStart".into(), true)); 57 | event_names.insert("on_drop".into(), ("OnDrop".into(), true)); 58 | 59 | event_names.insert("on_mouse_down".into(), ("OnMouseDown".into(), true)); 60 | event_names.insert("on_mouse_enter".into(), ("OnMouseEnter".into(), true)); 61 | event_names.insert("on_mouse_leave".into(), ("OnMouseLeave".into(), true)); 62 | event_names.insert("on_mouse_move".into(), ("OnMouseMove".into(), true)); 63 | event_names.insert("on_mouse_over".into(), ("OnMouseOver".into(), true)); 64 | event_names.insert("on_mouse_out".into(), ("OnMouseOut".into(), true)); 65 | event_names.insert("on_mouse_up".into(), ("OnMouseUp".into(), true)); 66 | } 67 | // --Pointer 68 | // #[cfg(feature = "pointer-events")] 69 | { 70 | event_names.insert("on_pointer_down".into(), ("OnPointerDown".into(), true)); 71 | event_names.insert("on_pointer_move".into(), ("OnPointerMove".into(), true)); 72 | event_names.insert("on_pointer_up".into(), ("OnPointerUp".into(), true)); 73 | event_names.insert("on_pointer_cancel".into(), ("OnPointerCancel".into(), true)); 74 | event_names.insert( 75 | "on_got_pointer_capture".into(), 76 | ("OnGotPointerCapture".into(), true), 77 | ); 78 | event_names.insert( 79 | "on_lost_pointer_capture".into(), 80 | ("OnLostPointerCapture".into(), true), 81 | ); 82 | event_names.insert("on_pointer_enter".into(), ("OnPointerEnter".into(), true)); 83 | event_names.insert("on_pointer_leave".into(), ("OnPointerLeave".into(), true)); 84 | event_names.insert("on_pointer_over".into(), ("OnPointerOver".into(), true)); 85 | event_names.insert("on_pointer_out".into(), ("OnPointerOut".into(), true)); 86 | } 87 | // --Selection 88 | // #[cfg(feature = "select-events")] 89 | { 90 | event_names.insert("on_select".into(), ("OnSelect".into(), false)); 91 | } 92 | // --Touch 93 | // #[cfg(feature = "touch-events")] 94 | { 95 | event_names.insert("on_touch_cancel".into(), ("OnTouchCancel".into(), true)); 96 | event_names.insert("on_touch_end".into(), ("OnTouchEnd".into(), true)); 97 | event_names.insert("on_touch_move".into(), ("OnTouchMove".into(), true)); 98 | event_names.insert("on_touch_start".into(), ("OnTouchStart".into(), true)); 99 | } 100 | // #[cfg(feature = "scroll-events")] 101 | { 102 | event_names.insert("on_scroll".into(), ("OnScroll".into(),false)); 103 | } 104 | // --Wheel 105 | // onWheel 106 | // --Media 107 | // onAbort 108 | // onCanPlay 109 | // onCanPlayThrough 110 | // onDurationChange 111 | // onEmptied 112 | // onEncrypted 113 | // onEnded 114 | // onError 115 | // onLoadedData 116 | // onLoadedMetadata 117 | // onLoadStart 118 | // onPause 119 | // onPlay 120 | // onPlaying 121 | // onProgress 122 | // onRateChange 123 | // onSeeked 124 | // onSeeking 125 | // onStalled 126 | // onSuspend 127 | // onTimeUpdate 128 | // onVolumeChange 129 | // onWaiting 130 | // --Image 131 | // #[cfg(feature = "image-events")] 132 | { 133 | event_names.insert("on_load".into(), ("OnLoad".into(), false)); 134 | event_names.insert("on_error".into(), ("OnError".into(), false)); 135 | } 136 | // --Animation 137 | // #[cfg(feature = "animation-events")] 138 | { 139 | event_names.insert("on_animation_start".into(), ("OnAnimationStart".into(), false)); 140 | event_names.insert("on_animation_end".into(), ("OnAnimationEnd".into(), false)); 141 | event_names.insert( 142 | "on_animation_iteration".into(), 143 | ("OnAnimationIteration".into(), false), 144 | ); 145 | } 146 | // --Transition 147 | // #[cfg(feature = "transition-events")] 148 | { 149 | event_names.insert("on_transition_end".into(), ("OnTransitionEnd".into(), false)); 150 | } 151 | // --Other 152 | // #[cfg(feature = "toggle-events")] 153 | { 154 | event_names.insert("on_toggle".into(), ("OnToggle".into(), false)); 155 | } 156 | event_names 157 | }; 158 | 159 | pub static ref WINDOW_EVENT_NAMES: HashMap = { 160 | // Remember to update this as you add more! 161 | let mut event_names = HashMap::with_capacity(4); 162 | event_names.insert("on_before_unload".into(), "OnBeforeUnload".into()); 163 | event_names.insert("on_hash_change".into(), "OnHashChange".into()); 164 | event_names.insert("on_pop_state".into(), "OnPopState".into()); 165 | event_names.insert("on_unhandled_rejection".into(), "OnUnhandledRejection".into()); 166 | event_names 167 | }; 168 | 169 | pub static ref LIFECYCLE_EVENT_NAMES: HashMap = { 170 | let mut lifecycle_event_names = HashMap::with_capacity(1); 171 | lifecycle_event_names.insert("post_render".into(), "PostRender".into()); 172 | lifecycle_event_names 173 | }; 174 | } 175 | 176 | pub fn should_include_rest_param(opt: &Option) -> bool { 177 | opt 178 | .as_ref() 179 | .and_then(|provided_event_name| { 180 | UI_EVENT_NAMES 181 | .iter() 182 | .find_map(|(_key, (event_name, should_include_rest_param))| { 183 | if provided_event_name != event_name { 184 | return None; 185 | } 186 | return Some(*should_include_rest_param); 187 | }) 188 | }) 189 | .unwrap_or(false) 190 | } 191 | -------------------------------------------------------------------------------- /crates/smd_tests/src/basic_event_handler_tests.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod tests { 3 | use smithy::{ 4 | smd, 5 | types::{ 6 | Component, 7 | HtmlToken, 8 | Node, 9 | UiEvent, 10 | }, 11 | }; 12 | use std::collections::HashMap; 13 | 14 | #[test] 15 | fn simple_app_state() { 16 | struct SimpleAppState { 17 | pub is_transitioned: bool, 18 | } 19 | 20 | let mut app_state = SimpleAppState { 21 | is_transitioned: false, 22 | }; 23 | 24 | let mut div = smd!(
25 | { if app_state.is_transitioned { "yes" } else { "no" } } 26 |
); 27 | 28 | let render_result = Node::Vec(vec![Node::Dom(HtmlToken { 29 | node_type: "div".into(), 30 | attributes: HashMap::new(), 31 | children: vec![Node::Text("no".into())], 32 | })]); 33 | assert_eq!(div.render(), render_result); 34 | 35 | let event_handled = div.handle_ui_event(&UiEvent::OnTest(true), &[0]); 36 | assert_eq!(event_handled, true); 37 | 38 | let render_result = Node::Vec(vec![Node::Dom(HtmlToken { 39 | node_type: "div".into(), 40 | attributes: HashMap::new(), 41 | children: vec![Node::Text("yes".into())], 42 | })]); 43 | assert_eq!(div.render(), render_result); 44 | } 45 | 46 | #[test] 47 | fn event_handler_basic_div() { 48 | // N.B. these tests are un-ergonomic! No one would use Smithy if this was the 49 | // recommended way to do things. However, as you can see from simple_app_state, 50 | // the general use case (that does not involve snooping at the app state) 51 | // doesn't require as many workarounds. 52 | 53 | struct AppState { 54 | called: bool, 55 | param: Option, 56 | } 57 | let app_state = std::rc::Rc::new(std::cell::RefCell::new(AppState { 58 | called: false, 59 | param: None, 60 | })); 61 | let app_state_2 = app_state.clone(); 62 | 63 | // This div only responds to events with a path of [0] 64 | let mut div = smd!(
); 71 | 72 | // an event called with path: [] should not affect anything 73 | let handled = div.handle_ui_event(&UiEvent::OnTest(true), &[]); 74 | assert_eq!(handled, false); 75 | { 76 | let app_state_2 = app_state_2.borrow(); 77 | assert_eq!(app_state_2.called, false); 78 | assert_eq!(app_state_2.param, None); 79 | } 80 | 81 | // an event called with [0, 0] should not affect anything 82 | let handled = div.handle_ui_event(&UiEvent::OnTest(true), &[0, 0]); 83 | assert_eq!(handled, false); 84 | { 85 | let app_state_2 = app_state_2.borrow(); 86 | assert_eq!(app_state_2.called, false); 87 | assert_eq!(app_state_2.param, None); 88 | } 89 | 90 | // // an event called with [1] should not affect anything 91 | let handled = div.handle_ui_event(&UiEvent::OnTest(true), &[1]); 92 | assert_eq!(handled, false); 93 | { 94 | let app_state_2 = app_state_2.borrow(); 95 | assert_eq!(app_state_2.called, false); 96 | assert_eq!(app_state_2.param, None); 97 | } 98 | 99 | // However, [0] will work! 100 | let handled = div.handle_ui_event(&UiEvent::OnTest(true), &[0]); 101 | assert_eq!(handled, true); 102 | { 103 | let app_state_2 = app_state_2.borrow(); 104 | assert_eq!(app_state_2.called, true); 105 | assert_eq!(app_state_2.param, Some(true)); 106 | } 107 | } 108 | 109 | #[test] 110 | fn event_handler_child_div() { 111 | struct AppState { 112 | called: bool, 113 | param: Option, 114 | } 115 | let app_state = std::rc::Rc::new(std::cell::RefCell::new(AppState { 116 | called: false, 117 | param: None, 118 | })); 119 | let app_state_2 = app_state.clone(); 120 | 121 | // this div will only respond to events with path: [0, 0] 122 | let mut div = smd!(
123 |
130 |
); 131 | 132 | // an event called with path: [] should not affect anything 133 | let handled = div.handle_ui_event(&UiEvent::OnTest(true), &[]); 134 | assert_eq!(handled, false); 135 | { 136 | let app_state_2 = app_state_2.borrow(); 137 | assert_eq!(app_state_2.called, false); 138 | assert_eq!(app_state_2.param, None); 139 | } 140 | 141 | // an event called with [0] should not affect anything 142 | let handled = div.handle_ui_event(&UiEvent::OnTest(true), &[0]); 143 | assert_eq!(handled, false); 144 | { 145 | let app_state_2 = app_state_2.borrow(); 146 | assert_eq!(app_state_2.called, false); 147 | assert_eq!(app_state_2.param, None); 148 | } 149 | 150 | // // an event called with [1] should not affect anything 151 | let handled = div.handle_ui_event(&UiEvent::OnTest(true), &[1]); 152 | assert_eq!(handled, false); 153 | { 154 | let app_state_2 = app_state_2.borrow(); 155 | assert_eq!(app_state_2.called, false); 156 | assert_eq!(app_state_2.param, None); 157 | } 158 | 159 | // However, [0, 0] will work! 160 | let handled = div.handle_ui_event(&UiEvent::OnTest(true), &[0, 0]); 161 | assert_eq!(handled, true); 162 | { 163 | let app_state_2 = app_state_2.borrow(); 164 | assert_eq!(app_state_2.called, true); 165 | assert_eq!(app_state_2.param, Some(true)); 166 | } 167 | } 168 | 169 | #[test] 170 | fn event_handler_child_div_in_group() { 171 | struct AppState { 172 | called: bool, 173 | param: Option, 174 | } 175 | let app_state = std::rc::Rc::new(std::cell::RefCell::new(AppState { 176 | called: false, 177 | param: None, 178 | })); 179 | let app_state_2 = app_state.clone(); 180 | 181 | // this div will only respond to events with path: [0, 0] 182 | let mut inner = smd!(
); 189 | let mut div = smd!(
{ &mut inner }
); 190 | 191 | // an event called with path: [] should not affect anything 192 | let handled = div.handle_ui_event(&UiEvent::OnTest(true), &[]); 193 | assert_eq!(handled, false); 194 | { 195 | let app_state_2 = app_state_2.borrow(); 196 | assert_eq!(app_state_2.called, false); 197 | assert_eq!(app_state_2.param, None); 198 | } 199 | 200 | // an event called with [0] should not affect anything 201 | let handled = div.handle_ui_event(&UiEvent::OnTest(true), &[0]); 202 | assert_eq!(handled, false); 203 | { 204 | let app_state_2 = app_state_2.borrow(); 205 | assert_eq!(app_state_2.called, false); 206 | assert_eq!(app_state_2.param, None); 207 | } 208 | 209 | // // an event called with [0, 0] should not affect anything 210 | let handled = div.handle_ui_event(&UiEvent::OnTest(true), &[0, 0]); 211 | assert_eq!(handled, false); 212 | { 213 | let app_state_2 = app_state_2.borrow(); 214 | assert_eq!(app_state_2.called, false); 215 | assert_eq!(app_state_2.param, None); 216 | } 217 | 218 | // However, [0, 0, 0] will work! 219 | // Why three zeroes? 220 | // * div = smd! is a vec (hence why smd!(
) works), so 221 | // the first zero references the first item in this vec. 222 | // * The div's first child is { ... } hence the second zero 223 | // * The results of inner = smd! is another vec, hence the last zero 224 | // 225 | // we cannot omit the second step (i.e. this cannot respond to [0, 0]) 226 | // because otherwise
{ first }{ second }
would need to know 227 | // the length of { first } at compile time, in order to properly dispatch 228 | // events to { second }. Of course, this info is not available. 229 | let handled = div.handle_ui_event(&UiEvent::OnTest(true), &[0, 0, 0]); 230 | assert_eq!(handled, true); 231 | { 232 | let app_state_2 = app_state_2.borrow(); 233 | assert_eq!(app_state_2.called, true); 234 | assert_eq!(app_state_2.param, Some(true)); 235 | } 236 | } 237 | 238 | #[test] 239 | fn strings_do_not_handle_ui_events() { 240 | let inner = "inner"; 241 | let mut div = smd!(
{ inner }
); 242 | assert_eq!(div.handle_ui_event(&UiEvent::OnTest(false), &[0, 0]), false); 243 | } 244 | 245 | #[test] 246 | fn text_nodes_do_not_handle_ui_events() { 247 | let mut div = smd!(
inner
); 248 | assert_eq!(div.handle_ui_event(&UiEvent::OnTest(false), &[0, 0]), false); 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /crates/smd_macro/src/parsers/core.rs: -------------------------------------------------------------------------------- 1 | use crate::types::{ 2 | AttributeOrEventHandler, 3 | DomRefInfo, 4 | GlobalEventHandlingInfo, 5 | LifecycleEventHandlingInfo, 6 | SplitAttributeOrEventHandlers, 7 | SplitTokenStreamEventHandlingInfoPairs, 8 | TokenStreamEventHandlingInfoPair, 9 | TokenTreeSlice, 10 | UIEventHandlingInfo, 11 | WindowEventHandlingInfo, 12 | }; 13 | 14 | #[allow(unused_imports)] 15 | use nom::{ 16 | error_position, 17 | tuple_parser, 18 | }; 19 | 20 | use nom::{ 21 | alt, 22 | apply, 23 | call, 24 | delimited, 25 | map, 26 | named, 27 | named_args, 28 | tuple, 29 | }; 30 | use proc_macro2::{ 31 | Delimiter, 32 | Spacing, 33 | TokenStream, 34 | }; 35 | use quote::quote; 36 | use std::iter::Extend; 37 | 38 | use super::{ 39 | make_smithy_tokens::{ 40 | make_html_tokens, 41 | make_text_node, 42 | }, 43 | util, 44 | window_event_handlers::match_window_event_handlers, 45 | }; 46 | 47 | named!( 48 | match_self_closing_token , 49 | map!( 50 | delimited!( 51 | apply!(util::match_punct, Some('<'), Some(Spacing::Alone), vec![]), 52 | tuple!( 53 | apply!(util::match_ident, None, false), 54 | many_0_custom!(super::attributes::match_attribute) 55 | ), 56 | tuple!( 57 | apply!(util::match_punct, Some('/'), Some(Spacing::Joint), vec![]), 58 | apply!(util::match_punct, Some('>'), None, vec![]) 59 | ) 60 | ), 61 | |(name, attributes_and_event_handlers)| { 62 | let SplitAttributeOrEventHandlers(attributes, event_handlers, dom_ref_vec) 63 | = attributes_and_event_handlers.into(); 64 | 65 | let component = make_html_tokens(name, attributes, vec![]); 66 | 67 | let event_handlers = event_handlers 68 | .into_iter() 69 | .map(UIEventHandlingInfo::from_string_token_stream_pair) 70 | .collect(); 71 | (component, event_handlers, dom_ref_vec) 72 | } 73 | ) 74 | ); 75 | 76 | named!( 77 | match_opening_tag )>, 78 | delimited!( 79 | apply!(util::match_punct, Some('<'), Some(Spacing::Alone), vec![]), 80 | tuple!( 81 | apply!(util::match_ident, None, false), 82 | many_0_custom!(super::attributes::match_attribute) 83 | ), 84 | apply!(util::match_punct, Some('>'), None, vec![]) 85 | ) 86 | ); 87 | 88 | named!( 89 | match_closing_tag , 90 | delimited!( 91 | tuple!( 92 | apply!(util::match_punct, Some('<'), Some(Spacing::Joint), vec![]), 93 | apply!(util::match_punct, Some('/'), Some(Spacing::Alone), vec![]) 94 | ), 95 | apply!(util::match_ident, None, false), 96 | apply!(util::match_punct, Some('>'), None, vec![]) 97 | ) 98 | ); 99 | 100 | named!( 101 | match_regular_token , 102 | map!( 103 | tuple!( 104 | match_opening_tag, 105 | many_0_custom!(match_node), 106 | match_closing_tag 107 | ), 108 | |((name, attributes_and_event_handlers), children_and_events, closing_tag_name)| { 109 | // TODO an Option for closing_tag_name and only run this assertion if 110 | // closing_tag_name.is_some(), allowing for
foo. 111 | assert_eq!( 112 | name, 113 | closing_tag_name, 114 | "Opening and closing tag names ({} and {}) do not match", 115 | name, 116 | closing_tag_name, 117 | ); 118 | 119 | let SplitAttributeOrEventHandlers(attributes, event_handlers, dom_ref_opt) 120 | = attributes_and_event_handlers.into(); 121 | let SplitTokenStreamEventHandlingInfoPairs(children, child_event_infos, mut dom_ref_vec) 122 | = children_and_events.into(); 123 | // children: Vec 124 | // child_event_infos: Vec 125 | // dom_ref_vec: Vec 126 | 127 | let token_stream = make_html_tokens(name, attributes, children); 128 | 129 | let mut event_infos: Vec = event_handlers 130 | .into_iter() 131 | .map(UIEventHandlingInfo::from_string_token_stream_pair) 132 | .collect(); 133 | event_infos.extend(child_event_infos.into_iter()); 134 | 135 | dom_ref_vec.extend(dom_ref_opt.into_iter()); 136 | 137 | (token_stream, event_infos, dom_ref_vec) 138 | } 139 | ) 140 | ); 141 | 142 | named!( 143 | match_html_token , 144 | alt!( 145 | match_self_closing_token 146 | | match_regular_token 147 | ) 148 | ); 149 | 150 | // N.B. this is separated because there seems to be a bug 151 | // in many_1_custom. TODO look at this 152 | named!( 153 | match_ident_2 , 154 | alt!( 155 | apply!(util::match_ident, None, true) 156 | | apply!(util::match_punct, None, None, vec!['<']) 157 | | call!(util::match_literal_as_string) 158 | ) 159 | ); 160 | 161 | named!( 162 | match_string_as_node , 163 | map!( 164 | many_1_custom!(match_ident_2), 165 | |vec| { 166 | let joined = vec.iter().map(|ident| ident.to_string()).collect::>().join(""); 167 | (make_text_node(joined), vec![], vec![]) 168 | } 169 | ) 170 | ); 171 | 172 | named!( 173 | match_group , 174 | map!( 175 | apply!(util::match_group, Some(Delimiter::Brace)), 176 | |x| (quote!(#x.render()), vec![ 177 | UIEventHandlingInfo { 178 | reversed_path: vec![], 179 | event: None, 180 | callback: quote!(#x), 181 | } 182 | ], vec![]) 183 | ) 184 | ); 185 | 186 | named!( 187 | match_node , 188 | alt!( 189 | match_html_token 190 | | match_string_as_node 191 | | match_group 192 | ) 193 | ); 194 | 195 | named_args!( 196 | pub match_html_component(should_move: bool) , 197 | map!( 198 | tuple!( 199 | many_0_custom!(match_window_event_handlers), 200 | // N.B. we would rather call many_0_custom!(match_node) here, but that 201 | // does not appear to work. This is a workaround. 202 | alt!( 203 | many_1_custom!(match_node) 204 | | call!(util::match_empty) 205 | ) 206 | ), 207 | | 208 | (global_event_handling_infos, dom_vec): 209 | ( 210 | Vec, 211 | Vec<(TokenStream, Vec, Vec)> 212 | ) 213 | | { 214 | // What are we doing here? We're flattening vectors and appending 215 | // the current index to some reversed paths. 216 | let (vec_of_node_tokens, event_handling_infos, dom_ref_vec) = dom_vec 217 | .into_iter() 218 | .enumerate() 219 | .fold( 220 | (vec![], vec![], vec![]), 221 | | 222 | (mut vec_of_node_tokens, mut event_handling_infos, mut vec_of_dom_refs): 223 | (Vec, Vec, Vec), 224 | (i, (token, current_event_handling_infos, dom_ref_vec)): 225 | (usize, (TokenStream, Vec, Vec)) 226 | | { 227 | vec_of_node_tokens.push(token); 228 | 229 | // append i to the path of the current_event_handling_infos and 230 | // append that vec to event_handling_infos 231 | let mut current_event_handling_infos = current_event_handling_infos.into_iter().map(|mut info| { 232 | info.reversed_path.push(i); 233 | info 234 | }).collect::>(); 235 | event_handling_infos.append(&mut current_event_handling_infos); 236 | 237 | // and do the same thing for dom_ref's 238 | let mut dom_ref_vec = dom_ref_vec.into_iter().map(|mut dom_ref| { 239 | dom_ref.reversed_path.push(i); 240 | dom_ref 241 | }).collect::>(); 242 | vec_of_dom_refs.append(&mut dom_ref_vec); 243 | 244 | (vec_of_node_tokens, event_handling_infos, vec_of_dom_refs) 245 | } 246 | ); 247 | let rendered_node = util::reduce_vec_to_node(&vec_of_node_tokens); 248 | 249 | let (window_event_handler_infos, lifecycle_event_handling_infos): 250 | (Vec, Vec) 251 | = global_event_handling_infos 252 | .into_iter() 253 | .fold( 254 | (vec![], vec![]), 255 | | 256 | (mut window_event_handler_infos, mut lifecycle_event_handling_infos): 257 | (Vec, Vec), 258 | current_global_event_handling_info: GlobalEventHandlingInfo 259 | | { 260 | // Can this be done more parsimoniously, e.g. using a library? 261 | match current_global_event_handling_info { 262 | GlobalEventHandlingInfo::Window(window_event_handling_info) => { 263 | window_event_handler_infos.push(window_event_handling_info); 264 | }, 265 | GlobalEventHandlingInfo::Lifecycle(lifecycle_event_handling_info) => { 266 | lifecycle_event_handling_infos.push(lifecycle_event_handling_info); 267 | }, 268 | }; 269 | (window_event_handler_infos, lifecycle_event_handling_infos) 270 | } 271 | ); 272 | 273 | super::make_smithy_tokens::make_component( 274 | rendered_node, 275 | event_handling_infos, 276 | window_event_handler_infos, 277 | lifecycle_event_handling_infos, 278 | dom_ref_vec, 279 | should_move 280 | ) 281 | } 282 | ) 283 | ); 284 | -------------------------------------------------------------------------------- /crates/smd_macro/src/parsers/make_smithy_tokens.rs: -------------------------------------------------------------------------------- 1 | use crate::types::{ 2 | DomRefInfo, 3 | LifecycleEventHandlingInfo, 4 | StringTokenStreamPair, 5 | UIEventHandlingInfo, 6 | WindowEventHandlingInfo, 7 | }; 8 | use proc_macro2::{ 9 | Ident, 10 | Span, 11 | TokenStream, 12 | }; 13 | use quote::{ 14 | quote, 15 | ToTokens, 16 | }; 17 | 18 | pub fn make_html_tokens( 19 | name: String, 20 | attributes: Vec, 21 | children: Vec, 22 | ) -> TokenStream { 23 | let attribute_initialization = if attributes.len() > 0 { 24 | let attribute_insertion = attributes.into_iter().fold(quote!(), |accum, (key, val)| { 25 | quote!( 26 | #accum 27 | map.insert(#key.into(), #val.into()); 28 | ) 29 | }); 30 | quote!({ 31 | let mut map = std::collections::HashMap::new(); 32 | #attribute_insertion 33 | map 34 | }) 35 | } else { 36 | quote!(std::collections::HashMap::new()) 37 | }; 38 | 39 | // children: Vec where the TokenStream is a Node 40 | let child_initialization = if children.len() > 0 { 41 | let len = children.len(); 42 | let child_insertion = children.into_iter().fold(quote!(), |accum, child| { 43 | // child: TokenStream 44 | // it is the result of a call to .render() 45 | quote!( 46 | #accum 47 | children.push(#child); 48 | ) 49 | }); 50 | quote!({ 51 | let mut children = Vec::with_capacity(#len); 52 | #child_insertion 53 | children 54 | }) 55 | } else { 56 | quote!(vec![]) 57 | }; 58 | 59 | // TODO implement and call .flatten_children 60 | quote!(smithy::types::Node::Dom(smithy::types::HtmlToken { 61 | node_type: #name.into(), 62 | attributes: #attribute_initialization, 63 | children: #child_initialization, 64 | })) 65 | } 66 | 67 | fn vec_to_quote(v: Vec) -> TokenStream 68 | where 69 | X: ToTokens, 70 | { 71 | let ret = v 72 | .into_iter() 73 | .fold(quote! {}, |accum, item| quote!(#accum #item,)); 74 | quote!(vec![#ret]) 75 | } 76 | 77 | pub fn make_component( 78 | rendered_node: TokenStream, 79 | ui_event_handling_infos: Vec, 80 | window_event_handling_infos: Vec, 81 | lifecycle_event_handling_infos: Vec, 82 | dom_ref_infos: Vec, 83 | should_move: bool, 84 | ) -> TokenStream { 85 | // TODO split ui_event_handling_infos into a vec of groups and a vec of non-groups 86 | // and deal with them separately in this function. 87 | // TODO even later: don't conflate these two! It's super weird that we have 88 | // groups represented as UIEventHandlingInfo 89 | 90 | let (groups, mut ui_event_handling_infos) = 91 | UIEventHandlingInfo::split_into_groups(ui_event_handling_infos); 92 | // reverse the non-groups 93 | ui_event_handling_infos.reverse(); 94 | 95 | let (child_ref_assignment, group_window_event_handling) = groups 96 | .iter() 97 | .map(|info| (info.reversed_path.clone(), info.callback.clone())) 98 | .fold( 99 | (quote! {}, quote! {}), 100 | |(ref_accum, group_accum), (mut reversed_path, group)| { 101 | reversed_path.reverse(); 102 | let quotable_path = vec_to_quote(reversed_path); 103 | ( 104 | quote! { 105 | #ref_accum 106 | let new_path = path_so_far.clone().into_iter().chain(#quotable_path).collect(); 107 | (#group).handle_ref_assignment(new_path); 108 | }, 109 | quote! { 110 | #group_accum 111 | event_handled = (#group).handle_window_event(window_event) || event_handled; 112 | }, 113 | ) 114 | }, 115 | ); 116 | 117 | let ref_assignment_quote = if dom_ref_infos.len() > 0 { 118 | let dom_ref_infos = dom_ref_infos 119 | .into_iter() 120 | .fold(quote! {}, |accum, dom_ref_info| { 121 | let dom_ref = dom_ref_info.dom_ref; 122 | let path = vec_to_quote(dom_ref_info.reversed_path); 123 | quote! { 124 | #accum 125 | (#path, #dom_ref), 126 | } 127 | }); 128 | let dom_ref_infos = quote! { { let dom_refs: Vec = vec![#dom_ref_infos]; dom_refs }}; 129 | quote! { 130 | let document = web_sys::window().unwrap().document().unwrap(); 131 | for (path, dom_ref) in (#dom_ref_infos).into_iter() { 132 | use wasm_bindgen::JsCast; 133 | let strs = path_so_far 134 | .clone() 135 | .into_iter() 136 | .chain(path) 137 | .map(|x| x.to_string()) 138 | .collect::>(); 139 | 140 | let selector = strs.join(","); 141 | // TODO avoid unwrapping here, and try to avoid calling .query_selector 142 | // every time. 143 | let el_opt: Option = document 144 | .query_selector(&format!("[data-smithy-path=\"{}\"]", selector)) 145 | .unwrap() 146 | .map(JsCast::unchecked_into); 147 | 148 | *dom_ref = el_opt; 149 | } 150 | #child_ref_assignment 151 | } 152 | } else { 153 | child_ref_assignment 154 | }; 155 | 156 | let group_lifecycle_event_handling = 157 | groups 158 | .iter() 159 | .map(|info| info.callback.clone()) 160 | .fold(quote! {}, |accum, group| { 161 | quote! {{ 162 | #accum 163 | (#group).handle_post_render(); 164 | }} 165 | }); 166 | 167 | // inner_ui_event_handling is made in two parts. 168 | // Part 1: handle groups 169 | let inner_ui_event_handling = groups.into_iter().fold(quote! {}, |accum, group| { 170 | let path = group.get_path_match(true); 171 | let callback = group.callback; 172 | 173 | quote! { 174 | #accum 175 | (evt, #path) => smithy::types::PhaseResult::UiEventHandling( 176 | #callback.handle_ui_event(evt, rest) 177 | ), 178 | } 179 | }); 180 | 181 | // Part 2: handle events for non-groups (true ui event handlers) 182 | let inner_ui_event_handling = ui_event_handling_infos.into_iter().fold( 183 | inner_ui_event_handling, 184 | |accum, ui_event_handling_info| { 185 | let path = ui_event_handling_info.get_path_match( 186 | crate::parsers::event_names::should_include_rest_param(&ui_event_handling_info.event), 187 | ); 188 | let callback = ui_event_handling_info.callback; 189 | match ui_event_handling_info.event { 190 | Some(event) => { 191 | let event = Ident::new(&event, Span::call_site()); 192 | quote! { 193 | #accum 194 | (smithy::types::UiEvent::#event(val), #path) => { 195 | (#callback)(val); 196 | smithy::types::PhaseResult::UiEventHandling(true) 197 | }, 198 | } 199 | }, 200 | None => panic!("should not happen, this is ensured by split_into_groups"), 201 | } 202 | }, 203 | ); 204 | 205 | // N.B. this is incorrect. A group would receive an event, *regardless of whether 206 | // it would handle it.* 207 | // 208 | // Thus, click_handler in
{ child_component }
209 | // would never be called, though it could be in 210 | //
whatever
211 | 212 | let inner_window_event_handling = 213 | window_event_handling_infos 214 | .into_iter() 215 | .fold(quote! {}, |accum, window_event_handling_info| { 216 | let WindowEventHandlingInfo { event, callback } = window_event_handling_info; 217 | let event = Ident::new(&event, Span::call_site()); 218 | quote! { 219 | #accum 220 | smithy::types::WindowEvent::#event(val) => { 221 | (#callback)(val); 222 | smithy::types::PhaseResult::WindowEventHandling(true) 223 | } 224 | } 225 | }); 226 | 227 | // TODO disambiguate this 228 | // N.B. right now "lifecycle" == "post_render", but that needs to be disambiguated 229 | let inner_lifecycle_event_handling = 230 | lifecycle_event_handling_infos 231 | .into_iter() 232 | .fold(quote! {}, |accum, lifecycle_info| { 233 | let cb = lifecycle_info.callback; 234 | quote! { 235 | #accum 236 | (#cb)(); 237 | } 238 | }); 239 | 240 | // whether to move is a flag that we pass, and it depends on whether the macro invoked 241 | // is smd! or smd_borrowed! 242 | let maybe_move = if should_move { quote!(move) } else { quote!() }; 243 | quote!({ 244 | #[allow(dead_code)] 245 | use smithy::types::Component; 246 | let component: smithy::types::SmithyComponent = smithy::types::SmithyComponent(Box::new(#maybe_move |phase| { 247 | match phase { 248 | smithy::types::Phase::Rendering => smithy::types::PhaseResult::Rendering(#rendered_node), 249 | smithy::types::Phase::UiEventHandling(ui_event_handling) => { 250 | match ui_event_handling { 251 | #inner_ui_event_handling 252 | _ => smithy::types::PhaseResult::UiEventHandling(false) 253 | } 254 | }, 255 | smithy::types::Phase::WindowEventHandling(window_event) => { 256 | let mut event_handled = false; 257 | #group_window_event_handling 258 | match window_event { 259 | #inner_window_event_handling 260 | _ => smithy::types::PhaseResult::WindowEventHandling(event_handled), 261 | } 262 | }, 263 | smithy::types::Phase::PostRendering => { 264 | #group_lifecycle_event_handling 265 | #inner_lifecycle_event_handling 266 | smithy::types::PhaseResult::PostRendering 267 | }, 268 | smithy::types::Phase::RefAssignment(path_so_far) => { 269 | #ref_assignment_quote 270 | smithy::types::PhaseResult::RefAssignment 271 | }, 272 | } 273 | })); 274 | component 275 | }) 276 | } 277 | 278 | pub fn make_text_node(s: String) -> TokenStream { 279 | quote!(smithy::types::Node::Text(#s.into())) 280 | } 281 | -------------------------------------------------------------------------------- /crates/smithy_core/src/attach_event_listeners.rs: -------------------------------------------------------------------------------- 1 | use crate::js_fns; 2 | use smithy_types::UiEvent; 3 | #[allow(unused_imports)] 4 | use smithy_types::WindowEvent; 5 | use wasm_bindgen::{ 6 | closure::Closure, 7 | JsCast, 8 | }; 9 | 10 | #[cfg(feature = "animation-events")] 11 | use web_sys::AnimationEvent; 12 | #[cfg(feature = "clipboard-events")] 13 | use web_sys::ClipboardEvent; 14 | #[cfg(feature = "focus-events")] 15 | use web_sys::FocusEvent; 16 | #[cfg(feature = "input-events")] 17 | use web_sys::InputEvent; 18 | #[cfg(feature = "keyboard-events")] 19 | use web_sys::KeyboardEvent; 20 | #[cfg(feature = "mouse-events")] 21 | use web_sys::MouseEvent; 22 | #[cfg(feature = "pointer-events")] 23 | use web_sys::PointerEvent; 24 | #[cfg(feature = "scroll-events")] 25 | use web_sys::ScrollAreaEvent; 26 | #[cfg(feature = "touch-events")] 27 | use web_sys::TouchEvent; 28 | #[cfg(feature = "transition-events")] 29 | use web_sys::TransitionEvent; 30 | #[cfg(feature = "web-sys-ui-events")] 31 | use web_sys::UiEvent as WebSysUiEvent; 32 | 33 | #[cfg(feature = "before-unload-events")] 34 | use web_sys::BeforeUnloadEvent; 35 | #[cfg(feature = "hash-change-events")] 36 | use web_sys::HashChangeEvent; 37 | #[cfg(feature = "pop-state-events")] 38 | use web_sys::PopStateEvent; 39 | #[cfg(feature = "promise-rejection-events")] 40 | use web_sys::PromiseRejectionEvent; 41 | 42 | use web_sys::{ 43 | Event, 44 | HtmlElement, 45 | }; 46 | 47 | fn derive_path(s: String) -> Result, std::num::ParseIntError> { 48 | s.split(",").map(|s| s.parse::()).collect() 49 | } 50 | 51 | const DATA_SMITHY_PATH: &'static str = "data-smithy-path"; 52 | 53 | #[allow(unused_macros)] 54 | macro_rules! attach_ui_event_listener { 55 | ( 56 | $html_el:expr, 57 | $web_sys_event_type:ident, 58 | $smithy_event_type:ident, 59 | $event_name:expr, 60 | $should_bubble:expr 61 | ) => { 62 | let event_handler_cb = Closure::new(move |evt: Event| { 63 | let evt: $web_sys_event_type = evt.unchecked_into(); 64 | if let Some(path) = evt 65 | .target() 66 | .and_then(|target| target.dyn_into::().ok()) 67 | .and_then(|el| el.get_attribute(DATA_SMITHY_PATH)) 68 | .and_then(|attr| derive_path(attr).ok()) 69 | { 70 | #[cfg(feature = "event-logs")] 71 | web_sys::console::log_1(&wasm_bindgen::JsValue::from_str(&format!( 72 | "\nEvent: {}, Path: {:?}", 73 | $event_name, path 74 | ))); 75 | 76 | let event_wrapped = UiEvent::$smithy_event_type(evt); 77 | let handle_event = move || { 78 | let handled = crate::handle_ui_event(&event_wrapped, &path); 79 | if handled { 80 | crate::rerender(); 81 | } 82 | }; 83 | 84 | if crate::event_handling_phase_is_ongoing() { 85 | let request_animation_frame_cb = 86 | Closure::wrap(Box::new(handle_event) as Box); 87 | let window = web_sys::window().unwrap(); 88 | 89 | let _ = 90 | window.request_animation_frame(request_animation_frame_cb.as_ref().unchecked_ref()); 91 | request_animation_frame_cb.forget(); 92 | } else { 93 | handle_event(); 94 | } 95 | } 96 | }); 97 | $html_el.attach_event_listener($event_name, &event_handler_cb, $should_bubble); 98 | event_handler_cb.forget(); 99 | }; 100 | } 101 | 102 | pub fn attach_ui_event_listeners(html_el: &js_fns::HTMLElement) { 103 | // --Clipboard 104 | #[cfg(feature = "clipboard-events")] 105 | { 106 | attach_ui_event_listener!(html_el, ClipboardEvent, OnCopy, "copy", true); 107 | attach_ui_event_listener!(html_el, ClipboardEvent, OnCut, "cut", true); 108 | attach_ui_event_listener!(html_el, ClipboardEvent, OnPaste, "paste", true); 109 | } 110 | 111 | // --Composition 112 | 113 | // --Keyboard 114 | #[cfg(feature = "keyboard-events")] 115 | { 116 | attach_ui_event_listener!(html_el, KeyboardEvent, OnKeyDown, "keydown", false); 117 | attach_ui_event_listener!(html_el, KeyboardEvent, OnKeyPress, "keypress", false); 118 | attach_ui_event_listener!(html_el, KeyboardEvent, OnKeyUp, "keyup", false); 119 | } 120 | 121 | // --Focus 122 | #[cfg(feature = "focus-events")] 123 | { 124 | attach_ui_event_listener!(html_el, FocusEvent, OnFocus, "focus", false); 125 | attach_ui_event_listener!(html_el, FocusEvent, OnBlur, "blur", false); 126 | } 127 | 128 | // --Form 129 | #[cfg(feature = "input-events")] 130 | { 131 | attach_ui_event_listener!(html_el, InputEvent, OnChange, "change", false); 132 | attach_ui_event_listener!(html_el, InputEvent, OnInput, "input", false); 133 | attach_ui_event_listener!(html_el, InputEvent, OnInvalid, "invalid", false); 134 | attach_ui_event_listener!(html_el, InputEvent, OnSubmit, "submit", false); 135 | } 136 | 137 | // --Mouse 138 | #[cfg(feature = "mouse-events")] 139 | { 140 | attach_ui_event_listener!(html_el, MouseEvent, OnClick, "click", false); 141 | attach_ui_event_listener!(html_el, MouseEvent, OnContextMenu, "contextmenu", false); 142 | attach_ui_event_listener!(html_el, MouseEvent, OnDblClick, "dblclick", false); 143 | 144 | attach_ui_event_listener!(html_el, MouseEvent, OnDrag, "drag", false); 145 | attach_ui_event_listener!(html_el, MouseEvent, OnDragEnd, "dragend", false); 146 | attach_ui_event_listener!(html_el, MouseEvent, OnDragEnter, "dragenter", false); 147 | attach_ui_event_listener!(html_el, MouseEvent, OnDragExit, "dragexit", false); 148 | attach_ui_event_listener!(html_el, MouseEvent, OnDragLeave, "dragleave", false); 149 | attach_ui_event_listener!(html_el, MouseEvent, OnDragOver, "dragover", false); 150 | attach_ui_event_listener!(html_el, MouseEvent, OnDragStart, "dragstart", false); 151 | attach_ui_event_listener!(html_el, MouseEvent, OnDrop, "drop", false); 152 | 153 | attach_ui_event_listener!(html_el, MouseEvent, OnMouseDown, "mousedown", false); 154 | attach_ui_event_listener!(html_el, MouseEvent, OnMouseEnter, "mouseenter", false); 155 | attach_ui_event_listener!(html_el, MouseEvent, OnMouseLeave, "mouseleave", false); 156 | attach_ui_event_listener!(html_el, MouseEvent, OnMouseMove, "mousemove", false); 157 | attach_ui_event_listener!(html_el, MouseEvent, OnMouseOver, "mouseover", false); 158 | attach_ui_event_listener!(html_el, MouseEvent, OnMouseOut, "mouseout", false); 159 | attach_ui_event_listener!(html_el, MouseEvent, OnMouseUp, "mouseup", false); 160 | } 161 | 162 | // --Pointer 163 | #[cfg(feature = "pointer-events")] 164 | { 165 | attach_ui_event_listener!(html_el, PointerEvent, OnPointerDown, "pointerdown", false); 166 | attach_ui_event_listener!(html_el, PointerEvent, OnPointerMove, "pointermove", false); 167 | attach_ui_event_listener!(html_el, PointerEvent, OnPointerUp, "pointerup", false); 168 | attach_ui_event_listener!( 169 | html_el, 170 | PointerEvent, 171 | OnPointerCancel, 172 | "pointercancel", 173 | false 174 | ); 175 | attach_ui_event_listener!( 176 | html_el, 177 | PointerEvent, 178 | OnGotPointerCapture, 179 | "gotpointercapture", 180 | false 181 | ); 182 | attach_ui_event_listener!( 183 | html_el, 184 | PointerEvent, 185 | OnLostPointerCapture, 186 | "lostpointercapture", 187 | false 188 | ); 189 | attach_ui_event_listener!(html_el, PointerEvent, OnPointerEnter, "pointerenter", false); 190 | attach_ui_event_listener!(html_el, PointerEvent, OnPointerLeave, "pointerleave", false); 191 | attach_ui_event_listener!(html_el, PointerEvent, OnPointerOver, "pointerover", false); 192 | attach_ui_event_listener!(html_el, PointerEvent, OnPointerOut, "pointerout", false); 193 | } 194 | 195 | // --Selection 196 | #[cfg(feature = "select-events")] 197 | { 198 | attach_ui_event_listener!(html_el, WebSysUiEvent, OnSelect, "onselect", false); 199 | } 200 | 201 | // --Touch 202 | #[cfg(feature = "touch-events")] 203 | { 204 | attach_ui_event_listener!(html_el, TouchEvent, OnTouchCancel, "touchcancel", false); 205 | attach_ui_event_listener!(html_el, TouchEvent, OnTouchEnd, "touchend", false); 206 | attach_ui_event_listener!(html_el, TouchEvent, OnTouchMove, "touchmove", false); 207 | attach_ui_event_listener!(html_el, TouchEvent, OnTouchStart, "touchstart", false); 208 | } 209 | 210 | // --Scroll 211 | #[cfg(feature = "scroll-events")] 212 | { 213 | attach_ui_event_listener!(html_el, ScrollAreaEvent, OnScroll, "scroll", false); 214 | } 215 | 216 | // --Image 217 | #[cfg(feature = "image-events")] 218 | { 219 | attach_ui_event_listener!(html_el, WebSysUiEvent, OnLoad, "load", false); 220 | attach_ui_event_listener!(html_el, WebSysUiEvent, OnError, "error", false); 221 | } 222 | 223 | // --Animation 224 | #[cfg(feature = "animation-events")] 225 | { 226 | attach_ui_event_listener!( 227 | html_el, 228 | AnimationEvent, 229 | OnAnimationStart, 230 | "animationstart", 231 | false 232 | ); 233 | attach_ui_event_listener!( 234 | html_el, 235 | AnimationEvent, 236 | OnAnimationEnd, 237 | "animationend", 238 | false 239 | ); 240 | attach_ui_event_listener!( 241 | html_el, 242 | AnimationEvent, 243 | OnAnimationIteration, 244 | "animationiteration", 245 | false 246 | ); 247 | } 248 | 249 | // --Transition 250 | #[cfg(feature = "transition-events")] 251 | { 252 | attach_ui_event_listener!( 253 | html_el, 254 | TransitionEvent, 255 | OnTransitionEnd, 256 | "transitionend", 257 | false 258 | ); 259 | } 260 | 261 | // --Other 262 | #[cfg(feature = "toggle-events")] 263 | { 264 | attach_ui_event_listener!(html_el, WebSysUiEvent, OnToggle, "toggle", false); 265 | } 266 | } 267 | 268 | #[allow(unused_macros)] 269 | macro_rules! attach_window_event_listener { 270 | ( 271 | $window:expr, 272 | $web_sys_event_type:ident, 273 | $smithy_event_type:ident, 274 | $event_name:expr 275 | ) => { 276 | let handle_event_cb = Closure::new(move |evt: Event| { 277 | let evt: $web_sys_event_type = evt.unchecked_into(); 278 | let event_wrapped = WindowEvent::$smithy_event_type(evt); 279 | let handle_event = move || { 280 | let handled = crate::handle_window_event(&event_wrapped); 281 | if handled { 282 | crate::rerender(); 283 | } 284 | }; 285 | if crate::event_handling_phase_is_ongoing() { 286 | let request_animation_frame_cb = Closure::wrap(Box::new(handle_event) as Box); 287 | let window = web_sys::window().unwrap(); 288 | let _ = window.request_animation_frame(request_animation_frame_cb.as_ref().unchecked_ref()); 289 | request_animation_frame_cb.forget(); 290 | } else { 291 | handle_event(); 292 | } 293 | }); 294 | 295 | $window.attach_event_listener($event_name, &handle_event_cb); 296 | handle_event_cb.forget(); 297 | }; 298 | } 299 | 300 | #[allow(unused_variables)] 301 | pub fn attach_window_event_listeners(window: &js_fns::WINDOW) { 302 | #[cfg(feature = "before-unload-events")] 303 | attach_window_event_listener!(window, BeforeUnloadEvent, OnBeforeUnload, "beforeunload"); 304 | #[cfg(feature = "hash-change-events")] 305 | attach_window_event_listener!(window, HashChangeEvent, OnHashChange, "hashchange"); 306 | #[cfg(feature = "pop-state-events")] 307 | attach_window_event_listener!(window, PopStateEvent, OnPopState, "popstate"); 308 | #[cfg(feature = "promise-rejection-events")] 309 | attach_window_event_listener!( 310 | window, 311 | PromiseRejectionEvent, 312 | OnUnhandledRejection, 313 | "unhandledrejection" 314 | ); 315 | } 316 | -------------------------------------------------------------------------------- /crates/smithy_core/src/node_diff.rs: -------------------------------------------------------------------------------- 1 | use smithy_types::{ 2 | AsInnerHtml, 3 | Attributes, 4 | CollapsedHtmlToken, 5 | CollapsedNode, 6 | }; 7 | 8 | type NewInnerHtml = String; 9 | 10 | pub type Path = Vec; 11 | 12 | #[derive(Debug)] 13 | pub struct ReplaceChildOperation { 14 | pub new_inner_html: NewInnerHtml, 15 | pub child_index: usize, 16 | } 17 | 18 | #[derive(Debug)] 19 | pub struct InsertChildOperation { 20 | pub new_inner_html: NewInnerHtml, 21 | pub child_index: usize, 22 | } 23 | 24 | #[derive(Debug)] 25 | pub struct DeleteChildOperation { 26 | child_index: usize, 27 | } 28 | 29 | #[derive(Debug)] 30 | pub struct UpdateAttributesOperation { 31 | pub new_attributes: Attributes, 32 | } 33 | 34 | #[derive(Debug)] 35 | pub enum DiffOperation { 36 | ReplaceChild(ReplaceChildOperation), 37 | InsertChild(InsertChildOperation), 38 | DeleteChild(DeleteChildOperation), 39 | UpdateAttributes(UpdateAttributesOperation), 40 | } 41 | 42 | pub type DiffItem = (Path, DiffOperation); 43 | pub type Diff = Vec; 44 | 45 | pub trait Diffable { 46 | fn get_diff_with(&self, other: &Self) -> Diff; 47 | } 48 | 49 | pub trait ApplicableTo { 50 | fn apply_to(&self, other: E); 51 | } 52 | 53 | fn node_from_str(s: &str) -> web_sys::Node { 54 | let doc = web_sys::window().unwrap().document().unwrap(); 55 | let new_container_el = doc.create_element("div").unwrap(); 56 | new_container_el.set_inner_html(s); 57 | new_container_el.first_child().unwrap() 58 | } 59 | 60 | fn apply_diff_item_to_element_ref(diff_op: &DiffOperation, target_el: &web_sys::Element) { 61 | #[cfg(feature = "browser-logs")] 62 | web_sys::console::log_1(&wasm_bindgen::JsValue::from_str(&format!( 63 | "apply diff {:?}", 64 | diff_op 65 | ))); 66 | match &diff_op { 67 | DiffOperation::ReplaceChild(replace_child_operation) => { 68 | let child_opt = target_el 69 | .child_nodes() 70 | .get(replace_child_operation.child_index as u32); 71 | #[cfg(feature = "browser-logs")] 72 | web_sys::console::log_1(&wasm_bindgen::JsValue::from_str(&format!( 73 | "replace op {:?} {:?}\nexisting inner {:?}", 74 | child_opt.is_some(), 75 | target_el.child_nodes().length(), 76 | target_el.inner_html() 77 | ))); 78 | 79 | match child_opt { 80 | Some(child) => { 81 | let new_node = node_from_str(&replace_child_operation.new_inner_html); 82 | let _ = target_el.replace_child(&new_node, &child); 83 | }, 84 | _ => panic!("no child found"), 85 | } 86 | }, 87 | DiffOperation::InsertChild(insert_child_operation) => { 88 | let new_inner_dom = node_from_str(&insert_child_operation.new_inner_html); 89 | 90 | let child_opt = target_el 91 | .child_nodes() 92 | .get(insert_child_operation.child_index as u32); 93 | 94 | // TODO figure out how to get child_opt.map(|x| &x) to compile 95 | let _ = match child_opt { 96 | Some(child) => target_el.insert_before(&new_inner_dom, Some(&child)), 97 | None => target_el.insert_before(&new_inner_dom, None), 98 | }; 99 | }, 100 | DiffOperation::DeleteChild(delete_child_operation) => { 101 | #[cfg(feature = "browser-logs")] 102 | web_sys::console::log_1(&wasm_bindgen::JsValue::from_str(&format!( 103 | "delete {:?} {:?}", 104 | target_el 105 | .child_nodes() 106 | .get(delete_child_operation.child_index as u32) 107 | .is_some(), 108 | target_el.child_nodes().length() 109 | ))); 110 | let child = target_el 111 | .child_nodes() 112 | .get(delete_child_operation.child_index as u32) 113 | .unwrap(); 114 | 115 | let _ = target_el.remove_child(&child); 116 | }, 117 | DiffOperation::UpdateAttributes(update_attributes_operation) => { 118 | for (attr, attr_value) in &update_attributes_operation.new_attributes { 119 | let _ = target_el.set_attribute(&attr, &attr_value); 120 | } 121 | }, 122 | }; 123 | } 124 | 125 | impl ApplicableTo<&web_sys::Element> for DiffItem { 126 | fn apply_to(&self, el: &web_sys::Element) { 127 | #[cfg(feature = "browser-logs")] 128 | web_sys::console::log_1(&wasm_bindgen::JsValue::from_str(&format!( 129 | "apply to {:?}", 130 | self 131 | ))); 132 | 133 | if self.0.len() == 0 { 134 | apply_diff_item_to_element_ref(&self.1, el); 135 | } else { 136 | let target_el = { 137 | let path_to_parent = &self.0; 138 | let path_selector = format!( 139 | "[data-smithy-path=\"{}\"]", 140 | path_to_parent 141 | .iter() 142 | .map(|u| u.to_string()) 143 | .collect::>() 144 | .join(",") 145 | ); 146 | // this should never fail, the path_to_parent should always point to an 147 | // existing node... 148 | // TODO don't unwrap 149 | 150 | #[cfg(feature = "browser-logs")] 151 | web_sys::console::log_1(&wasm_bindgen::JsValue::from_str(&format!( 152 | "inner {:?}\n\nis_some {:?}\nselector {:?}", 153 | el.inner_html(), 154 | el.query_selector(&path_selector).unwrap().is_some(), 155 | path_selector 156 | ))); 157 | 158 | let target_el = el.query_selector(&path_selector).unwrap().unwrap(); 159 | target_el 160 | }; 161 | apply_diff_item_to_element_ref(&self.1, &target_el); 162 | } 163 | } 164 | } 165 | 166 | /** 167 | * New diffing algo 168 | * 169 | * - Wrap the outermost Vec 170 | * in another CollapsedNode, representing
171 | * 172 | * Diffing Algo: 173 | * 174 | * - Starting with the
, keep track of its path (aka []) 175 | * - For each zipped optionalized child, match: 176 | * - (Some(original), Some(new)) => 177 | * - If node_type is the same 178 | * - Change attributes 179 | * - Recurse 180 | * - Else 181 | * - ReplaceChildChild 182 | * - (Some(original), None) => 183 | * - RemoveChild 184 | * - (None, Some(new)) => 185 | * - DeleteChild 186 | */ 187 | 188 | impl Diffable for Vec { 189 | fn get_diff_with(&self, other: &Self) -> Diff { 190 | get_vec_path_diff(self, other) 191 | } 192 | } 193 | 194 | fn get_i(i: usize, max_len: usize, potentially_deleting: bool) -> usize { 195 | if potentially_deleting { 196 | max_len - i - 1 197 | } else { 198 | i 199 | } 200 | } 201 | 202 | fn get_vec_path_diff(old_nodes: &Vec, new_nodes: &Vec) -> Diff { 203 | let potentially_deleting = old_nodes.len() > new_nodes.len(); 204 | let max_len = std::cmp::max(old_nodes.len(), new_nodes.len()); 205 | let path = vec![]; 206 | 207 | let zipped: Box, Option<&CollapsedNode>)>> = 208 | if potentially_deleting { 209 | let zipped = crate::zip_util::optionalize_and_zip(old_nodes.iter(), new_nodes.iter()); 210 | let mut vec = zipped.collect::, Option<&CollapsedNode>)>>(); 211 | vec.reverse(); 212 | Box::new(vec.into_iter()) 213 | } else { 214 | Box::new(crate::zip_util::optionalize_and_zip( 215 | old_nodes.iter(), 216 | new_nodes.iter(), 217 | )) 218 | }; 219 | 220 | zipped 221 | .enumerate() 222 | .flat_map(|(i, (current, new))| { 223 | let real_i = get_i(i, max_len, potentially_deleting); 224 | match (current, new) { 225 | (Some(old_node), Some(new_node)) => { 226 | get_diff_between_tokens(old_node, new_node, &path, real_i) 227 | }, 228 | (Some(_old_node), None) => vec![( 229 | path.clone(), 230 | DiffOperation::DeleteChild(DeleteChildOperation { 231 | child_index: real_i, 232 | }), 233 | )], 234 | (None, Some(new_node)) => vec![( 235 | path.clone(), 236 | DiffOperation::InsertChild(InsertChildOperation { 237 | new_inner_html: new_node.as_inner_html(), 238 | child_index: real_i, 239 | }), 240 | )], 241 | (None, None) => panic!("Should not happen - we should not encounter two none's here"), 242 | } 243 | }) 244 | .collect() 245 | } 246 | 247 | fn get_diff_between_tokens( 248 | old_node: &CollapsedNode, 249 | new_node: &CollapsedNode, 250 | path_to_parent: &Path, 251 | child_index: usize, 252 | ) -> Diff { 253 | match (old_node, new_node) { 254 | (CollapsedNode::Dom(ref old_token), CollapsedNode::Dom(ref new_token)) => { 255 | get_html_token_diff(old_token, new_token, path_to_parent, child_index) 256 | }, 257 | (CollapsedNode::Text(ref old_text), CollapsedNode::Text(ref new_text)) => { 258 | get_text_diff(old_text, new_text, path_to_parent.to_vec(), child_index) 259 | }, 260 | (CollapsedNode::Comment(ref old_comment), CollapsedNode::Comment(ref new_comment)) => { 261 | get_comment_diff( 262 | old_comment, 263 | new_comment, 264 | path_to_parent.to_vec(), 265 | child_index, 266 | ) 267 | }, 268 | _ => get_replace_diff(new_node, path_to_parent, child_index), 269 | } 270 | } 271 | 272 | fn get_html_token_diff( 273 | old_token: &CollapsedHtmlToken, 274 | new_token: &CollapsedHtmlToken, 275 | path_to_parent: &Path, 276 | child_index: usize, 277 | ) -> Diff { 278 | // If the node_type's are different, we replace 279 | // If they're the same, we potentially change attributes 280 | // And call get_path_diff on each zipped child 281 | let old_node_type = &old_token.node_type; 282 | let new_node_type = &new_token.node_type; 283 | if old_node_type != new_node_type { 284 | let new_inner_html = new_token.as_inner_html(); 285 | 286 | vec![( 287 | path_to_parent.to_vec(), 288 | DiffOperation::ReplaceChild(ReplaceChildOperation { 289 | new_inner_html, 290 | child_index, 291 | }), 292 | )] 293 | } else { 294 | // node types are the same, so we iterate over children 295 | let potentially_deleting = old_token.children.len() > new_token.children.len(); 296 | let max_len = std::cmp::max(old_token.children.len(), new_token.children.len()); 297 | 298 | let zipped: Box, Option<&CollapsedNode>)>> = 299 | if potentially_deleting { 300 | let zipped = crate::zip_util::optionalize_and_zip( 301 | old_token.children.iter(), 302 | new_token.children.iter(), 303 | ); 304 | let mut vec = zipped.collect::, Option<&CollapsedNode>)>>(); 305 | vec.reverse(); 306 | Box::new(vec.into_iter()) 307 | } else { 308 | Box::new(crate::zip_util::optionalize_and_zip( 309 | old_token.children.iter(), 310 | new_token.children.iter(), 311 | )) 312 | }; 313 | 314 | let mut diff = zipped 315 | .enumerate() 316 | .flat_map(|(i, zipped)| match zipped { 317 | (Some(old_child), Some(new_child)) => get_diff_between_tokens( 318 | old_child, 319 | new_child, 320 | &old_token.path, 321 | get_i(i, max_len, potentially_deleting), 322 | ), 323 | (Some(_old_child), None) => vec![( 324 | old_token.path.clone(), 325 | DiffOperation::DeleteChild(DeleteChildOperation { 326 | child_index: get_i(i, max_len, potentially_deleting), 327 | }), 328 | )], 329 | (None, Some(new_child)) => vec![( 330 | old_token.path.clone(), 331 | DiffOperation::InsertChild(InsertChildOperation { 332 | new_inner_html: new_child.as_inner_html(), 333 | child_index: get_i(i, max_len, potentially_deleting), 334 | }), 335 | )], 336 | _ => panic!("We should not encounter two None's in get_html_token_diff"), 337 | }) 338 | .collect::>(); 339 | 340 | if old_token.attributes != new_token.attributes || old_token.path != new_token.path { 341 | diff.push(( 342 | old_token.path.clone(), 343 | DiffOperation::UpdateAttributes(UpdateAttributesOperation { 344 | new_attributes: new_token.get_attributes_including_path(), 345 | }), 346 | )); 347 | }; 348 | 349 | diff 350 | } 351 | } 352 | 353 | fn get_text_diff(old_text: &String, new_text: &String, path: Path, child_index: usize) -> Diff { 354 | if old_text != new_text { 355 | vec![( 356 | path, 357 | DiffOperation::ReplaceChild(ReplaceChildOperation { 358 | new_inner_html: new_text.to_string(), 359 | child_index, 360 | }), 361 | )] 362 | } else { 363 | vec![] 364 | } 365 | } 366 | 367 | fn get_comment_diff( 368 | old_comment_opt: &Option, 369 | new_comment_opt: &Option, 370 | path: Path, 371 | child_index: usize, 372 | ) -> Diff { 373 | match (old_comment_opt, new_comment_opt) { 374 | (Some(old_comment), Some(new_comment)) => { 375 | get_text_diff(old_comment, new_comment, path, child_index) 376 | }, 377 | (Some(_old_comment), None) => vec![( 378 | path, 379 | DiffOperation::ReplaceChild(ReplaceChildOperation { 380 | // I think? 381 | new_inner_html: "".to_string(), 382 | child_index, 383 | }), 384 | )], 385 | (None, Some(new_comment)) => vec![( 386 | path, 387 | DiffOperation::ReplaceChild(ReplaceChildOperation { 388 | new_inner_html: format!("", new_comment), 389 | child_index, 390 | }), 391 | )], 392 | (None, None) => vec![], 393 | } 394 | } 395 | 396 | fn get_replace_diff(new_node: &CollapsedNode, path_to_parent: &Path, child_index: usize) -> Diff { 397 | let new_inner_html = new_node.as_inner_html(); 398 | vec![( 399 | path_to_parent.to_vec(), 400 | DiffOperation::ReplaceChild(ReplaceChildOperation { 401 | new_inner_html, 402 | child_index, 403 | }), 404 | )] 405 | } 406 | -------------------------------------------------------------------------------- /crates/smithy_types/src/events.rs: -------------------------------------------------------------------------------- 1 | // TODO custom_derive iter_variant_names 2 | // or https://github.com/Lolirofle/enum_traits 3 | 4 | /// An enum of events that a DOM element can potentially 5 | /// handle. 6 | /// 7 | /// These are included on dom elements as follows: 8 | /// ```rs 9 | /// smd!( 10 | /// 11 | /// ) 12 | /// ``` 13 | /// 14 | /// e.g. 15 | /// ```rs 16 | /// smd!( 17 | ///
18 | /// ) 19 | /// ``` 20 | pub enum UiEvent { 21 | // --Clipboard 22 | /// Usage: 23 | /// ```rs 24 | /// on_copy={|e: web_sys::ClipboardEvent| { /* ... */ }} 25 | /// ``` 26 | /// 27 | /// Requires the `clipboard-events` feature. 28 | #[cfg(feature = "clipboard-events")] 29 | OnCopy(web_sys::ClipboardEvent), 30 | /// Usage: 31 | /// ```rs 32 | /// on_cut={|e: web_sys::ClipboardEvent| { /* ... */ }} 33 | /// ``` 34 | /// 35 | /// Requires the `clipboard-events` feature. 36 | #[cfg(feature = "clipboard-events")] 37 | OnCut(web_sys::ClipboardEvent), 38 | /// Usage: 39 | /// ```rs 40 | /// on_paste={|e: web_sys::ClipboardEvent| { /* ... */ }} 41 | /// ``` 42 | /// 43 | /// Requires the `clipboard-events` feature. 44 | #[cfg(feature = "clipboard-events")] 45 | OnPaste(web_sys::ClipboardEvent), 46 | // --Composition 47 | // onCompositionEnd 48 | // onCompositionStart 49 | // onCompositionUpdate 50 | // --Keyboard 51 | /// Usage: 52 | /// ```rs 53 | /// on_key_down={|e: web_sys::KeyboardEvent| { /* ... */ }} 54 | /// ``` 55 | /// 56 | /// Requires the `keyboard-events` feature. 57 | #[cfg(feature = "keyboard-events")] 58 | OnKeyDown(web_sys::KeyboardEvent), 59 | /// Usage: 60 | /// ```rs 61 | /// on_key_press={|e: web_sys::KeyboardEvent| { /* ... */ }} 62 | /// ``` 63 | /// 64 | /// Requires the `keyboard-events` feature. 65 | #[cfg(feature = "keyboard-events")] 66 | OnKeyPress(web_sys::KeyboardEvent), 67 | /// Usage: 68 | /// ```rs 69 | /// on_key_up={|e: web_sys::KeyboardEvent| { /* ... */ }} 70 | /// ``` 71 | /// 72 | /// Requires the `keyboard-events` feature. 73 | #[cfg(feature = "keyboard-events")] 74 | OnKeyUp(web_sys::KeyboardEvent), 75 | // --Focus 76 | /// Usage: 77 | /// ```rs 78 | /// on_focus={|e: web_sys::FocusEvent| { /* ... */ }} 79 | /// ``` 80 | /// 81 | /// Requires the `focus-events` feature. 82 | #[cfg(feature = "focus-events")] 83 | OnFocus(web_sys::FocusEvent), 84 | /// Usage: 85 | /// ```rs 86 | /// on_blur={|e: web_sys::FocusEvent| { /* ... */ }} 87 | /// ``` 88 | /// 89 | /// Requires the `focus-events` feature. 90 | #[cfg(feature = "focus-events")] 91 | OnBlur(web_sys::FocusEvent), 92 | // --Form 93 | /// Usage: 94 | /// ```rs 95 | /// on_change={|e: web_sys::InputEvent| { /* ... */ }} 96 | /// ``` 97 | /// 98 | /// Requires the `input-events` feature. 99 | #[cfg(feature = "input-events")] 100 | OnChange(web_sys::InputEvent), 101 | /// Usage: 102 | /// ```rs 103 | /// on_input={|e: web_sys::InputEvent| { /* ... */ }} 104 | /// ``` 105 | /// 106 | /// Requires the `input-events` feature. 107 | #[cfg(feature = "input-events")] 108 | OnInput(web_sys::InputEvent), 109 | /// Usage: 110 | /// ```rs 111 | /// on_invalid={|e: web_sys::InputEvent| { /* ... */ }} 112 | /// ``` 113 | /// 114 | /// Requires the `input-events` feature. 115 | #[cfg(feature = "input-events")] 116 | OnInvalid(web_sys::InputEvent), 117 | /// Usage: 118 | /// ```rs 119 | /// on_submit={|e: web_sys::InputEvent| { /* ... */ }} 120 | /// ``` 121 | /// 122 | /// Requires the `input-events` feature. 123 | #[cfg(feature = "input-events")] 124 | OnSubmit(web_sys::InputEvent), 125 | // --Mouse 126 | /// Usage: 127 | /// ```rs 128 | /// on_click={|e: web_sys::MouseEvent| { /* ... */ }} 129 | /// ``` 130 | /// 131 | /// Requires the `mouse-events` feature. 132 | #[cfg(feature = "mouse-events")] 133 | OnClick(web_sys::MouseEvent), 134 | /// Usage: 135 | /// ```rs 136 | /// on_context_menu={|e: web_sys::MouseEvent| { /* ... */ }} 137 | /// ``` 138 | /// 139 | /// Requires the `mouse-events` feature. 140 | #[cfg(feature = "mouse-events")] 141 | OnContextMenu(web_sys::MouseEvent), 142 | /// Usage: 143 | /// ```rs 144 | /// on_dbl_cilck={|e: web_sys::MouseEvent| { /* ... */ }} 145 | /// ``` 146 | /// 147 | /// Requires the `mouse-events` feature. 148 | #[cfg(feature = "mouse-events")] 149 | OnDblClick(web_sys::MouseEvent), 150 | /// Usage: 151 | /// ```rs 152 | /// on_drag={|e: web_sys::MouseEvent| { /* ... */ }} 153 | /// ``` 154 | /// 155 | /// Requires the `mouse-events` feature. 156 | #[cfg(feature = "mouse-events")] 157 | OnDrag(web_sys::MouseEvent), 158 | /// Usage: 159 | /// ```rs 160 | /// on_drag_end={|e: web_sys::MouseEvent| { /* ... */ }} 161 | /// ``` 162 | /// 163 | /// Requires the `mouse-events` feature. 164 | #[cfg(feature = "mouse-events")] 165 | OnDragEnd(web_sys::MouseEvent), 166 | /// Usage: 167 | /// ```rs 168 | /// on_drag_enter={|e: web_sys::MouseEvent| { /* ... */ }} 169 | /// ``` 170 | /// 171 | /// Requires the `mouse-events` feature. 172 | #[cfg(feature = "mouse-events")] 173 | OnDragEnter(web_sys::MouseEvent), 174 | /// Usage: 175 | /// ```rs 176 | /// on_drag_exit={|e: web_sys::MouseEvent| { /* ... */ }} 177 | /// ``` 178 | /// 179 | /// Requires the `mouse-events` feature. 180 | #[cfg(feature = "mouse-events")] 181 | OnDragExit(web_sys::MouseEvent), 182 | /// Usage: 183 | /// ```rs 184 | /// on_drag_leave={|e: web_sys::MouseEvent| { /* ... */ }} 185 | /// ``` 186 | /// 187 | /// Requires the `mouse-events` feature. 188 | #[cfg(feature = "mouse-events")] 189 | OnDragLeave(web_sys::MouseEvent), 190 | /// Usage: 191 | /// ```rs 192 | /// on_drag_over={|e: web_sys::MouseEvent| { /* ... */ }} 193 | /// ``` 194 | /// 195 | /// Requires the `mouse-events` feature. 196 | #[cfg(feature = "mouse-events")] 197 | OnDragOver(web_sys::MouseEvent), 198 | /// Usage: 199 | /// ```rs 200 | /// on_drag_start={|e: web_sys::MouseEvent| { /* ... */ }} 201 | /// ``` 202 | /// 203 | /// Requires the `mouse-events` feature. 204 | #[cfg(feature = "mouse-events")] 205 | OnDragStart(web_sys::MouseEvent), 206 | /// Usage: 207 | /// ```rs 208 | /// on_drop={|e: web_sys::MouseEvent| { /* ... */ }} 209 | /// ``` 210 | /// 211 | /// Requires the `mouse-events` feature. 212 | #[cfg(feature = "mouse-events")] 213 | OnDrop(web_sys::MouseEvent), 214 | 215 | /// Usage: 216 | /// ```rs 217 | /// on_mouse_down={|e: web_sys::MouseEvent| { /* ... */ }} 218 | /// ``` 219 | /// 220 | /// Requires the `mouse-events` feature. 221 | #[cfg(feature = "mouse-events")] 222 | OnMouseDown(web_sys::MouseEvent), 223 | /// Usage: 224 | /// ```rs 225 | /// on_mouse_enter={|e: web_sys::MouseEvent| { /* ... */ }} 226 | /// ``` 227 | /// 228 | /// Requires the `mouse-events` feature. 229 | #[cfg(feature = "mouse-events")] 230 | OnMouseEnter(web_sys::MouseEvent), 231 | /// Usage: 232 | /// ```rs 233 | /// on_mouse_leave={|e: web_sys::MouseEvent| { /* ... */ }} 234 | /// ``` 235 | /// 236 | /// Requires the `mouse-events` feature. 237 | #[cfg(feature = "mouse-events")] 238 | OnMouseLeave(web_sys::MouseEvent), 239 | /// Usage: 240 | /// ```rs 241 | /// on_move_move={|e: web_sys::MouseEvent| { /* ... */ }} 242 | /// ``` 243 | /// 244 | /// Requires the `mouse-events` feature. 245 | #[cfg(feature = "mouse-events")] 246 | OnMouseMove(web_sys::MouseEvent), 247 | /// Usage: 248 | /// ```rs 249 | /// on_mouse_over={|e: web_sys::MouseEvent| { /* ... */ }} 250 | /// ``` 251 | /// 252 | /// Requires the `mouse-events` feature. 253 | #[cfg(feature = "mouse-events")] 254 | OnMouseOver(web_sys::MouseEvent), 255 | /// Usage: 256 | /// ```rs 257 | /// on_mouse_out={|e: web_sys::MouseEvent| { /* ... */ }} 258 | /// ``` 259 | /// 260 | /// Requires the `mouse-events` feature. 261 | #[cfg(feature = "mouse-events")] 262 | OnMouseOut(web_sys::MouseEvent), 263 | /// Usage: 264 | /// ```rs 265 | /// on_mouse_up={|e: web_sys::MouseEvent| { /* ... */ }} 266 | /// ``` 267 | /// 268 | /// Requires the `mouse-events` feature. 269 | #[cfg(feature = "mouse-events")] 270 | OnMouseUp(web_sys::MouseEvent), 271 | // --Pointer 272 | /// Usage: 273 | /// ```rs 274 | /// on_pointer_down={|e: web_sys::PointerEvent| { /* ... */ }} 275 | /// ``` 276 | /// 277 | /// Requires the `pointer-events` feature. 278 | #[cfg(feature = "pointer-events")] 279 | OnPointerDown(web_sys::PointerEvent), 280 | /// Usage: 281 | /// ```rs 282 | /// on_pointer_move={|e: web_sys::PointerEvent| { /* ... */ }} 283 | /// ``` 284 | /// 285 | /// Requires the `pointer-events` feature. 286 | #[cfg(feature = "pointer-events")] 287 | OnPointerMove(web_sys::PointerEvent), 288 | /// Usage: 289 | /// ```rs 290 | /// on_pointer_up={|e: web_sys::PointerEvent| { /* ... */ }} 291 | /// ``` 292 | /// 293 | /// Requires the `pointer-events` feature. 294 | #[cfg(feature = "pointer-events")] 295 | OnPointerUp(web_sys::PointerEvent), 296 | /// Usage: 297 | /// ```rs 298 | /// on_pointer_cancel={|e: web_sys::PointerEvent| { /* ... */ }} 299 | /// ``` 300 | /// 301 | /// Requires the `pointer-events` feature. 302 | #[cfg(feature = "pointer-events")] 303 | OnPointerCancel(web_sys::PointerEvent), 304 | /// Usage: 305 | /// ```rs 306 | /// on_got_pointer_capture={|e: web_sys::PointerEvent| { /* ... */ }} 307 | /// ``` 308 | /// 309 | /// Requires the `pointer-events` feature. 310 | #[cfg(feature = "pointer-events")] 311 | OnGotPointerCapture(web_sys::PointerEvent), 312 | /// Usage: 313 | /// ```rs 314 | /// on_lost_pointer_capture={|e: web_sys::PointerEvent| { /* ... */ }} 315 | /// ``` 316 | /// 317 | /// Requires the `pointer-events` feature. 318 | #[cfg(feature = "pointer-events")] 319 | OnLostPointerCapture(web_sys::PointerEvent), 320 | /// Usage: 321 | /// ```rs 322 | /// on_pointer_enter={|e: web_sys::PointerEvent| { /* ... */ }} 323 | /// ``` 324 | /// 325 | /// Requires the `pointer-events` feature. 326 | #[cfg(feature = "pointer-events")] 327 | OnPointerEnter(web_sys::PointerEvent), 328 | /// Usage: 329 | /// ```rs 330 | /// on_pointer_leave={|e: web_sys::PointerEvent| { /* ... */ }} 331 | /// ``` 332 | /// 333 | /// Requires the `pointer-events` feature. 334 | #[cfg(feature = "pointer-events")] 335 | OnPointerLeave(web_sys::PointerEvent), 336 | /// Usage: 337 | /// ```rs 338 | /// on_pointer_over={|e: web_sys::PointerEvent| { /* ... */ }} 339 | /// ``` 340 | /// 341 | /// Requires the `pointer-events` feature. 342 | #[cfg(feature = "pointer-events")] 343 | OnPointerOver(web_sys::PointerEvent), 344 | /// Usage: 345 | /// ```rs 346 | /// on_pointer_out={|e: web_sys::PointerEvent| { /* ... */ }} 347 | /// ``` 348 | /// 349 | /// Requires the `pointer-events` feature. 350 | #[cfg(feature = "pointer-events")] 351 | OnPointerOut(web_sys::PointerEvent), 352 | // --Selection 353 | /// Usage: 354 | /// ```rs 355 | /// on_select={|e: web_sys::UiEvent| { /* ... */ }} 356 | /// ``` 357 | /// 358 | /// Requires the `select-events` feature. 359 | #[cfg(feature = "select-events")] 360 | OnSelect(web_sys::UiEvent), 361 | // --Touch 362 | /// Usage: 363 | /// ```rs 364 | /// on_touch_cancel={|e: web_sys::TouchEvent| { /* ... */ }} 365 | /// ``` 366 | /// 367 | /// Requires the `touch-events` feature. 368 | #[cfg(feature = "touch-events")] 369 | OnTouchCancel(web_sys::TouchEvent), 370 | /// Usage: 371 | /// ```rs 372 | /// on_touch_end={|e: web_sys::TouchEvent| { /* ... */ }} 373 | /// ``` 374 | /// 375 | /// Requires the `touch-events` feature. 376 | #[cfg(feature = "touch-events")] 377 | OnTouchEnd(web_sys::TouchEvent), 378 | /// Usage: 379 | /// ```rs 380 | /// on_touch_move={|e: web_sys::TouchEvent| { /* ... */ }} 381 | /// ``` 382 | /// 383 | /// Requires the `touch-events` feature. 384 | #[cfg(feature = "touch-events")] 385 | OnTouchMove(web_sys::TouchEvent), 386 | /// Usage: 387 | /// ```rs 388 | /// on_touch_start={|e: web_sys::TouchEvent| { /* ... */ }} 389 | /// ``` 390 | /// 391 | /// Requires the `touch-events` feature. 392 | #[cfg(feature = "touch-events")] 393 | OnTouchStart(web_sys::TouchEvent), 394 | // --Scroll 395 | /// Usage: 396 | /// ```rs 397 | /// on_scroll={|e: web_sys::ScrollAreaEvent| { /* ... */ }} 398 | /// ``` 399 | /// 400 | /// Requires the `scroll-events` feature. 401 | #[cfg(feature = "scroll-events")] 402 | OnScroll(web_sys::ScrollAreaEvent), 403 | // --Wheel 404 | // onWheel 405 | // --Media 406 | // onAbort 407 | // onCanPlay 408 | // onCanPlayThrough 409 | // onDurationChange 410 | // onEmptied 411 | // onEncrypted 412 | // onEnded 413 | // onError 414 | // onLoadedData 415 | // onLoadedMetadata 416 | // onLoadStart 417 | // onPause 418 | // onPlay 419 | // onPlaying 420 | // onProgress 421 | // onRateChange 422 | // onSeeked 423 | // onSeeking 424 | // onStalled 425 | // onSuspend 426 | // onTimeUpdate 427 | // onVolumeChange 428 | // onWaiting 429 | // --Image 430 | /// Usage: 431 | /// ```rs 432 | /// on_load={|e: web_sys::UiEvent| { /* ... */ }} 433 | /// ``` 434 | /// 435 | /// Requires the `image-events` feature. 436 | #[cfg(feature = "image-events")] 437 | OnLoad(web_sys::UiEvent), 438 | /// Usage: 439 | /// ```rs 440 | /// on_error={|e: web_sys::UiEvent| { /* ... */ }} 441 | /// ``` 442 | /// 443 | /// Requires the `image-events` feature. 444 | #[cfg(feature = "image-events")] 445 | OnError(web_sys::UiEvent), 446 | // --Animation 447 | /// Usage: 448 | /// ```rs 449 | /// on_animation_start={|e: web_sys::AnimationEvent| { /* ... */ }} 450 | /// ``` 451 | /// 452 | /// Requires the `animation-events` feature. 453 | #[cfg(feature = "animation-events")] 454 | OnAnimationStart(web_sys::AnimationEvent), 455 | /// Usage: 456 | /// ```rs 457 | /// on_animation_end={|e: web_sys::AnimationEvent| { /* ... */ }} 458 | /// ``` 459 | /// 460 | /// Requires the `animation-events` feature. 461 | #[cfg(feature = "animation-events")] 462 | OnAnimationEnd(web_sys::AnimationEvent), 463 | /// Usage: 464 | /// ```rs 465 | /// on_animation_iteration={|e: web_sys::AnimationEvent| { /* ... */ }} 466 | /// ``` 467 | /// 468 | /// Requires the `animation-events` feature. 469 | #[cfg(feature = "animation-events")] 470 | OnAnimationIteration(web_sys::AnimationEvent), 471 | // --Transition 472 | /// Usage: 473 | /// ```rs 474 | /// on_transition_end={|e: web_sys::TransitionEvent| { /* ... */ }} 475 | /// ``` 476 | /// 477 | /// Requires the `transition-events` feature. 478 | #[cfg(feature = "transition-events")] 479 | OnTransitionEnd(web_sys::TransitionEvent), 480 | // --Other 481 | /// Usage: 482 | /// ```rs 483 | /// on_toggle={|e: web_sys::UiEvent| { /* ... */ }} 484 | /// ``` 485 | /// 486 | /// Requires the `toggle-events` feature. 487 | #[cfg(feature = "toggle-events")] 488 | OnToggle(web_sys::UiEvent), 489 | // TODO figure out why cfg(test) does not work here 490 | /// An event used for testing. 491 | OnTest(bool), 492 | } 493 | 494 | /// An enum representing global events that can occur and that a smithy 495 | /// app can potentially handle. 496 | /// 497 | /// Window events are included as part of the `smd!` syntax as follows: 498 | /// ```rs 499 | /// smd!( 500 | /// window_event={window_event_handler}; 501 | /// ) 502 | /// ``` 503 | /// 504 | /// e.g. 505 | /// ```rs 506 | /// smd!( 507 | /// on_hash_change={|e: web_sys::HashChangeEvent| { /* ... */ }}; 508 | /// ) 509 | /// ``` 510 | pub enum WindowEvent { 511 | /// Usage: 512 | /// ```rs 513 | /// on_before_unload={|e: web_sys::BeforeUnloadEvent| { /* ... */ }}; 514 | /// ``` 515 | /// 516 | /// Requires the `before-unload-events` feature. 517 | #[cfg(feature = "before-unload-events")] 518 | OnBeforeUnload(web_sys::BeforeUnloadEvent), 519 | /// Usage: 520 | /// ```rs 521 | /// on_hash_change={|e: web_sys::HashChangeEvent| { /* ... */ }}; 522 | /// ``` 523 | /// 524 | /// Requires the `hash-change-events` feature. 525 | #[cfg(feature = "hash-change-events")] 526 | OnHashChange(web_sys::HashChangeEvent), 527 | /// Usage: 528 | /// ```rs 529 | /// on_pop_state={|e: web_sys::PopStateEvent| { /* ... */ }}; 530 | /// ``` 531 | /// 532 | /// Requires the `pop-state-events` feature. 533 | #[cfg(feature = "pop-state-events")] 534 | OnPopState(web_sys::PopStateEvent), 535 | /// Usage: 536 | /// ```rs 537 | /// on_unhandled_rejection={|e: web_sys::PromiseRejectionEvent| { /* ... */ }}; 538 | /// ``` 539 | /// 540 | /// Requires the `promise-rejection-events` feature. 541 | #[cfg(feature = "promise-rejection-events")] 542 | OnUnhandledRejection(web_sys::PromiseRejectionEvent), 543 | } 544 | --------------------------------------------------------------------------------