├── .github ├── FUNDING.yml └── workflows │ └── rust.yml ├── .gitignore ├── img ├── perf.png └── gizmo.png ├── Cargo.toml ├── rustfmt.toml ├── examples ├── sandbox │ ├── index.html │ ├── Cargo.toml │ └── src │ │ ├── elm_button.rs │ │ └── lib.rs └── todomvc │ ├── index.html │ ├── src │ ├── utils.rs │ ├── store.rs │ ├── lib.rs │ ├── app │ │ └── item.rs │ └── app.rs │ ├── Cargo.toml │ └── todo.css ├── mogwai ├── src │ ├── prelude.rs │ ├── lib.rs │ ├── component │ │ └── subscriber.rs │ ├── utils.rs │ ├── gizmo │ │ └── html.rs │ ├── an_introduction.rs │ ├── component.rs │ ├── gizmo.rs │ └── txrx.rs └── Cargo.toml ├── scripts └── build.sh └── README.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: schell 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock 4 | -------------------------------------------------------------------------------- /img/perf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erikdesjardins/mogwai/master/img/perf.png -------------------------------------------------------------------------------- /img/gizmo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erikdesjardins/mogwai/master/img/gizmo.png -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | 3 | members = [ 4 | "mogwai", 5 | "examples/sandbox", 6 | "examples/todomvc" 7 | ] -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | blank_lines_upper_bound = 2 2 | combine_control_expr = false 3 | format_code_in_doc_comments = true 4 | max_width = 80 5 | tab_spaces = 2 6 | wrap_long_multiline_chains = true -------------------------------------------------------------------------------- /examples/sandbox/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Mogwai • Sandbox 6 | 7 | 8 | 9 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/todomvc/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Mogwai • TodoMVC 6 | 7 | 8 | 9 | 10 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /mogwai/src/prelude.rs: -------------------------------------------------------------------------------- 1 | //! All of Mogwai in one easy place. 2 | pub use super::component::subscriber::Subscriber; 3 | pub use super::component::*; 4 | pub use super::gizmo::*; 5 | pub use super::gizmo::html::*; 6 | pub use super::txrx::{ 7 | new_shared, recv, trns, txrx, txrx_filter_fold, txrx_filter_map, txrx_fold, txrx_fold_shared, 8 | txrx_map, wrap_future, Receiver, Transmitter, 9 | }; 10 | pub use super::utils::*; 11 | pub use super::*; 12 | pub use wasm_bindgen::JsCast; 13 | pub use wasm_bindgen_futures::JsFuture; 14 | pub use web_sys::{Element, Event, EventTarget, HtmlElement, HtmlInputElement, Node}; 15 | -------------------------------------------------------------------------------- /examples/todomvc/src/utils.rs: -------------------------------------------------------------------------------- 1 | use web_sys::{Event, HtmlElement, HtmlInputElement}; 2 | use wasm_bindgen::JsCast; 3 | 4 | 5 | pub fn input_value(input:&HtmlElement) -> Option { 6 | let input:&HtmlInputElement = input.unchecked_ref(); 7 | Some( 8 | input 9 | .value() 10 | .trim() 11 | .to_string() 12 | ) 13 | } 14 | 15 | 16 | pub fn event_input_value(ev:&Event) -> Option { 17 | let target = ev.target()?; 18 | let input:&HtmlInputElement = target.unchecked_ref(); 19 | Some( 20 | input 21 | .value() 22 | .trim() 23 | .to_string() 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /mogwai/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # Mogwai 2 | //! 3 | //! Mogwai is library for frontend web development using Rust-to-Wasm 4 | //! compilation. Its goals are simple: 5 | //! * provide a declarative approach to creating and managing DOM nodes 6 | //! * encapsulate component state and compose components easily 7 | //! * explicate DOM updates 8 | //! * feel snappy 9 | //! 10 | //! ## Learn more 11 | //! If you're new to Mogwai, check out the [introduction](an_introduction) module. 12 | pub mod an_introduction; 13 | pub mod component; 14 | pub mod gizmo; 15 | pub mod prelude; 16 | pub mod txrx; 17 | pub mod utils; 18 | 19 | #[cfg(doctest)] 20 | doc_comment::doctest!("../../README.md"); 21 | -------------------------------------------------------------------------------- /examples/sandbox/Cargo.toml: -------------------------------------------------------------------------------- 1 | [lib] 2 | crate-type = ["cdylib", "rlib"] 3 | 4 | [package] 5 | name = "sandbox" 6 | version = "0.1.0" 7 | authors = ["Schell Scivally "] 8 | edition = "2018" 9 | 10 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 11 | 12 | [dependencies] 13 | console_log = "0.1.2" 14 | console_error_panic_hook = "0.1.6" 15 | wasm-bindgen = "0.2" 16 | futures = "0.3" 17 | wasm-bindgen-futures = "0.4" 18 | log = "0.4" 19 | 20 | [dependencies.mogwai] 21 | path = "../../mogwai" 22 | 23 | [dependencies.web-sys] 24 | version = "0.3.31" 25 | features = [ 26 | "Request", 27 | "RequestInit", 28 | "RequestMode", 29 | "Response", 30 | ] -------------------------------------------------------------------------------- /scripts/build.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | export PATH=$PATH:$HOME/.cargo/bin 4 | 5 | if hash rustup 2>/dev/null; then 6 | echo "Have rustup, skipping installation..." 7 | else 8 | echo "Installing rustup..." 9 | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh 10 | fi 11 | 12 | if hash wasm-pack 2>/dev/null; then 13 | echo "Have wasm-pack, skipping installation..." 14 | else 15 | echo "Installing wasm-pack..." 16 | curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh 17 | fi 18 | 19 | cargo build || exit 1 20 | cargo test || exit 1 21 | cargo doc || exit 1 22 | cd mogwai 23 | cargo publish --dry-run || exit 1 24 | cd .. 25 | 26 | echo "Done building on ref ${GITHUB_REF}" 27 | -------------------------------------------------------------------------------- /mogwai/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mogwai" 3 | version = "0.2.2" 4 | authors = ["Schell Scivally "] 5 | edition = "2018" 6 | license = "MIT" 7 | description = "The minimal, obvious, graphical, web application interface." 8 | documentation = "https://docs.rs/mogwai/" 9 | repository = "https://github.com/schell/mogwai" 10 | readme = "../README.md" 11 | keywords = ["ui", "dom", "app", "reactive", "frontend"] 12 | categories = ["gui", "wasm", "web-programming"] 13 | 14 | [dependencies] 15 | console_log = "0.1.2" 16 | futures = "0.3" 17 | js-sys = "0.3.27" 18 | log = "0.4" 19 | wasm-bindgen = "0.2" 20 | wasm-bindgen-futures = "0.4" 21 | 22 | [dependencies.web-sys] 23 | version = "0.3.31" 24 | features = [ 25 | "CharacterData", 26 | "CssStyleDeclaration", 27 | "Document", 28 | "Element", 29 | "Event", 30 | "EventTarget", 31 | "HtmlElement", 32 | "HtmlInputElement", 33 | "Node", 34 | "Text", 35 | "Window" 36 | ] 37 | 38 | [dev-dependencies] 39 | doc-comment = "0.3" 40 | wasm-bindgen-test = "^0.3" 41 | wasm-bindgen-futures = "^0.4" 42 | 43 | [profile.release] 44 | lto = true 45 | opt-level = 3 -------------------------------------------------------------------------------- /examples/sandbox/src/elm_button.rs: -------------------------------------------------------------------------------- 1 | use web_sys::HtmlElement; 2 | use mogwai::component::{subscriber::Subscriber, Component}; 3 | use mogwai::gizmo::Gizmo; 4 | use mogwai::gizmo::html::button; 5 | use mogwai::txrx::{Receiver, Transmitter}; 6 | 7 | pub struct Button { 8 | pub clicks: i32, 9 | } 10 | 11 | #[derive(Clone)] 12 | pub enum ButtonIn { 13 | Click, 14 | } 15 | 16 | #[derive(Clone)] 17 | pub enum ButtonOut { 18 | Clicks(i32), 19 | } 20 | 21 | impl Component for Button { 22 | type ModelMsg = ButtonIn; 23 | type ViewMsg = ButtonOut; 24 | type DomNode = HtmlElement; 25 | 26 | fn update( 27 | &mut self, 28 | msg: &ButtonIn, 29 | tx_view: &Transmitter, 30 | _subscriber: &Subscriber, 31 | ) { 32 | match msg { 33 | ButtonIn::Click => { 34 | self.clicks += 1; 35 | tx_view.send(&ButtonOut::Clicks(self.clicks)) 36 | } 37 | } 38 | } 39 | 40 | fn view( 41 | &self, 42 | tx: Transmitter, 43 | rx: Receiver, 44 | ) -> Gizmo { 45 | button() 46 | .rx_text( 47 | "Clicked 0 times", 48 | rx.branch_map(|msg| match msg { 49 | ButtonOut::Clicks(n) => format!("Clicked {} times", n), 50 | }), 51 | ) 52 | .tx_on("click", tx.contra_map(|_| ButtonIn::Click)) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /examples/todomvc/src/store.rs: -------------------------------------------------------------------------------- 1 | use wasm_bindgen::JsValue; 2 | use web_sys::Storage; 3 | use serde::{Serialize, Deserialize}; 4 | use serde_json; 5 | use mogwai::utils; 6 | 7 | 8 | #[derive(Serialize, Deserialize)] 9 | pub struct Item { 10 | pub title: String, 11 | pub completed: bool 12 | } 13 | 14 | const KEY: &str = "todomvc-mogwai"; 15 | 16 | pub fn write_items(items: Vec) -> Result<(), JsValue> { 17 | let str_value = 18 | serde_json::to_string(&items) 19 | .expect("Could not serialize items"); 20 | utils::window() 21 | .local_storage()? 22 | .into_iter() 23 | .for_each(|storage:Storage| { 24 | storage 25 | .set_item(KEY, &str_value) 26 | .expect("could not store serialized items"); 27 | }); 28 | Ok(()) 29 | } 30 | 31 | pub fn read_items() -> Result, JsValue> { 32 | let storage = 33 | utils::window() 34 | .local_storage()? 35 | .expect("Could not get local storage"); 36 | 37 | let may_item_str: Option = 38 | storage 39 | .get_item(KEY) 40 | .expect("Error using storage get_item"); 41 | 42 | let items = 43 | may_item_str 44 | .map(|json_str:String| { 45 | let items:Vec = 46 | serde_json::from_str(&json_str) 47 | .expect("Could not deserialize items"); 48 | items 49 | }) 50 | .unwrap_or(vec![]); 51 | 52 | Ok(items) 53 | } 54 | -------------------------------------------------------------------------------- /examples/todomvc/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "todomvc" 3 | version = "0.1.0" 4 | authors = ["Schell Scivally "] 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type = ["cdylib", "rlib"] 9 | 10 | [features] 11 | default = ["console_error_panic_hook"] 12 | 13 | [dependencies] 14 | console_log = "0.1.2" 15 | log = "0.4" 16 | serde = { version = "1.0", features = ["derive"] } 17 | serde_json = "1.0" 18 | wasm-bindgen = "0.2" 19 | 20 | # The `console_error_panic_hook` crate provides better debugging of panics by 21 | # logging them with `console.error`. This is great for development, but requires 22 | # all the `std::fmt` and `std::panicking` infrastructure, so isn't great for 23 | # code size when deploying. 24 | console_error_panic_hook = { version = "0.1.6", optional = true } 25 | 26 | # `wee_alloc` is a tiny allocator for wasm that is only ~1K in code size 27 | # compared to the default allocator's ~10K. It is slower than the default 28 | # allocator, however. 29 | # 30 | # Unfortunately, `wee_alloc` requires nightly Rust when targeting wasm for now. 31 | wee_alloc = { version = "0.4.2", optional = true } 32 | 33 | [dependencies.mogwai] 34 | path = "../../mogwai" 35 | 36 | [dependencies.web-sys] 37 | version = "0.3" 38 | features = [ 39 | "HashChangeEvent", 40 | "HtmlInputElement", 41 | "KeyboardEvent", 42 | "Location", 43 | "Storage" 44 | ] 45 | 46 | [dev-dependencies] 47 | wasm-bindgen-test = "0.2" 48 | 49 | [profile.release] 50 | # Tell `rustc` to optimize for small code size. 51 | lto = true 52 | opt-level = 3 53 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: cicd 2 | 3 | on: push 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v1 12 | 13 | # cacheing 14 | - name: Cache cargo registry 15 | uses: actions/cache@v1 16 | with: 17 | path: ~/.cargo/registry 18 | key: ${{ runner.os }}-cargo-registry-${{ github.ref }} 19 | restore-keys: | 20 | ${{ runner.os }}-cargo-registry-refs/heads/master 21 | ${{ runner.os }}-cargo-registry- 22 | - name: Cache cargo index 23 | uses: actions/cache@v1 24 | with: 25 | path: ~/.cargo/git 26 | key: ${{ runner.os }}-cargo-index-${{ github.ref }} 27 | restore-keys: | 28 | ${{ runner.os }}-cargo-index-refs/heads/master 29 | ${{ runner.os }}-cargo-index- 30 | - name: Cache cargo build 31 | uses: actions/cache@v1 32 | with: 33 | path: target 34 | key: ${{ runner.os }}-cargo-build-target-${{ github.ref }} 35 | restore-keys: | 36 | ${{ runner.os }}-cargo-build-target-refs/heads/master 37 | ${{ runner.os }}-cargo-build-target- 38 | - name: Cache global cargo bin 39 | uses: actions/cache@v1 40 | with: 41 | path: /usr/share/rust/.cargo/bin 42 | key: ${{ runner.os }}-cargo-global-bin-${{ github.ref }} 43 | restore-keys: | 44 | ${{ runner.os }}-cargo-global-bin-refs/heads/master 45 | ${{ runner.os }}-cargo-global-bin- 46 | 47 | 48 | - name: build 49 | run: scripts/build.sh 50 | 51 | - name: release 52 | if: github.ref == 'refs/heads/release' 53 | run: cd mogwai && cargo publish --token ${{ secrets.cargo_token }} 54 | -------------------------------------------------------------------------------- /mogwai/src/component/subscriber.rs: -------------------------------------------------------------------------------- 1 | //! A very limited transmitter used to map messages. 2 | use std::future::Future; 3 | use super::super::txrx::{Transmitter, Receiver}; 4 | 5 | 6 | /// A subscriber allows a component to subscribe to other components' messages 7 | /// without having explicit access to both Transmitter and Receiver. This allows 8 | /// the parent component to map child component messages into its own updates 9 | /// without needing its own transmitter. This is good because if `send` is called 10 | /// on a component's own ModelMsg transmitter during its Component::update it 11 | /// triggers a lock contetion. So a subscriber allows forwarding and wiring 12 | /// without enabling sending. 13 | #[derive(Clone)] 14 | pub struct Subscriber { 15 | tx: Transmitter 16 | } 17 | 18 | 19 | impl Subscriber { 20 | pub fn new(tx: &Transmitter) -> Subscriber { 21 | Subscriber { tx: tx.clone() } 22 | } 23 | 24 | /// Subscribe to a receiver by forwarding messages from it using a filter map 25 | /// function. 26 | pub fn subscribe_filter_map(&self, rx: &Receiver, f:F) 27 | where 28 | F: Fn(&ChildMsg) -> Option + 'static 29 | { 30 | rx.branch().forward_filter_map(&self.tx, f) 31 | } 32 | 33 | /// Subscribe to a receiver by forwarding messages from it using a map function. 34 | pub fn subscribe_map(&self, rx: &Receiver, f:F) 35 | where 36 | F: Fn(&ChildMsg) -> Msg + 'static 37 | { 38 | rx.branch().forward_filter_map(&self.tx, move |msg| Some(f(msg))) 39 | } 40 | 41 | /// Subscribe to a receiver by forwarding messages from it. 42 | pub fn subscribe(&self, rx: &Receiver) { 43 | rx.branch().forward_map(&self.tx, |msg| msg.clone()) 44 | } 45 | 46 | /// Send a one-time asynchronous message. 47 | pub fn send_async(&self, f:F) 48 | where 49 | F: Future + 'static 50 | { 51 | self.tx.send_async(f); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /examples/todomvc/src/lib.rs: -------------------------------------------------------------------------------- 1 | use log::{Level, trace}; 2 | use std::panic; 3 | use mogwai::prelude::*; 4 | use wasm_bindgen::prelude::*; 5 | 6 | mod utils; 7 | mod store; 8 | 9 | mod app; 10 | use app::{App, In}; 11 | 12 | 13 | 14 | // When the `wee_alloc` feature is enabled, use `wee_alloc` as the global 15 | // allocator. 16 | #[cfg(feature = "wee_alloc")] 17 | #[global_allocator] 18 | static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; 19 | 20 | 21 | #[wasm_bindgen] 22 | pub fn main() -> Result<(), JsValue> { 23 | panic::set_hook(Box::new(console_error_panic_hook::hook)); 24 | console_log::init_with_level(Level::Trace) 25 | .expect("could not init console_log"); 26 | 27 | if cfg!(debug_assertions) { 28 | trace!("Hello from debug mogwai-todo"); 29 | } else { 30 | trace!("Hello from release mogwai-todo"); 31 | } 32 | 33 | // Get the any items stored from a previous visit 34 | let mut msgs = 35 | store::read_items()? 36 | .into_iter() 37 | .map(|item| In::NewTodo(item.title, item.completed)) 38 | .collect::>(); 39 | 40 | // Get the hash for "routing" 41 | let hash = 42 | window() 43 | .location() 44 | .hash()?; 45 | 46 | App::url_to_filter_msg(hash) 47 | .into_iter() 48 | .for_each(|msg| msgs.push(msg)); 49 | 50 | App::new() 51 | .into_component() 52 | .run_init(msgs)?; 53 | 54 | // The footer has no relation to the rest of the app and is simply a view 55 | // attached to the body 56 | footer() 57 | .class("info") 58 | .with( 59 | p() 60 | .text("Double click to edit a todo") 61 | ) 62 | .with( 63 | p() 64 | .text("Written by ") 65 | .with( 66 | a() 67 | .attribute("href", "https://github.com/schell") 68 | .text("Schell Scivally") 69 | ) 70 | ) 71 | .with( 72 | p() 73 | .text("Part of ") 74 | .with( 75 | a() 76 | .attribute("href", "http://todomvc.com") 77 | .text("TodoMVC") 78 | ) 79 | ) 80 | .run() 81 | 82 | } 83 | -------------------------------------------------------------------------------- /mogwai/src/utils.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | use std::rc::Rc; 3 | use wasm_bindgen::closure::Closure; 4 | use wasm_bindgen::{JsCast, JsValue, UnwrapThrowExt}; 5 | use web_sys; 6 | 7 | use super::txrx::Transmitter; 8 | 9 | 10 | pub fn window() -> web_sys::Window { 11 | web_sys::window() 12 | .expect("no global `window` exists") 13 | } 14 | 15 | pub fn document() -> web_sys::Document { 16 | window() 17 | .document() 18 | .expect("no global `document` exists") 19 | } 20 | 21 | pub fn body() -> web_sys::HtmlElement { 22 | document() 23 | .body() 24 | .expect("document does not have a body") 25 | } 26 | 27 | pub fn set_checkup_interval(millis: i32, f: &Closure) -> i32 { 28 | window() 29 | .set_timeout_with_callback_and_timeout_and_arguments_0(f.as_ref().unchecked_ref(), millis) 30 | .expect("should register `setInterval` OK") 31 | } 32 | 33 | pub fn timeout(millis: i32, mut logic: F) -> i32 34 | where 35 | F: FnMut() -> bool + 'static 36 | { 37 | // https://rustwasm.github.io/wasm-bindgen/examples/request-animation-frame.html#srclibrs 38 | let f = Rc::new(RefCell::new(None)); 39 | let g = f.clone(); 40 | 41 | *g.borrow_mut() 42 | = Some(Closure::wrap(Box::new(move || { 43 | let should_continue = logic(); 44 | if should_continue { 45 | set_checkup_interval(millis, f.borrow().as_ref().unwrap_throw()); 46 | } 47 | }) as Box)); 48 | 49 | let invalidate = set_checkup_interval(millis, g.borrow().as_ref().unwrap_throw()); 50 | invalidate 51 | } 52 | 53 | fn req_animation_frame(f: &Closure) { 54 | window() 55 | .request_animation_frame(f.as_ref().unchecked_ref()) 56 | .expect("should register `requestAnimationFrame` OK"); 57 | } 58 | 59 | pub fn request_animation_frame(mut logic: F) 60 | where 61 | F: FnMut() -> bool + 'static 62 | { 63 | // https://rustwasm.github.io/wasm-bindgen/examples/request-animation-frame.html#srclibrs 64 | let f = Rc::new(RefCell::new(None)); 65 | let g = f.clone(); 66 | 67 | *g.borrow_mut() 68 | = Some(Closure::wrap(Box::new(move || { 69 | let should_continue = logic(); 70 | if should_continue { 71 | req_animation_frame(f.borrow().as_ref().unwrap_throw()); 72 | } 73 | }) as Box)); 74 | 75 | req_animation_frame(g.borrow().as_ref().unwrap_throw()); 76 | return; 77 | } 78 | 79 | 80 | pub fn add_event( 81 | ev_name: &str, 82 | target: &web_sys::EventTarget, 83 | tx: Transmitter 84 | ) -> Rc> { 85 | let cb = 86 | Closure::wrap(Box::new(move |val:JsValue| { 87 | let ev = val.unchecked_into(); 88 | tx.send(&ev); 89 | }) as Box); 90 | target 91 | .add_event_listener_with_callback(ev_name, cb.as_ref().unchecked_ref()) 92 | .unwrap_throw(); 93 | Rc::new(cb) 94 | } 95 | -------------------------------------------------------------------------------- /mogwai/src/gizmo/html.rs: -------------------------------------------------------------------------------- 1 | //! Contains [`Gizmo`] constructors for html5 tags. 2 | //! 3 | //! Each of these constructor functions is shorthand for 4 | //! ```rust,ignore 5 | //! Gizmo::element("...") 6 | //! .downcast::().ok().unwrap() 7 | //! ``` 8 | //! 9 | //! [`Gizmo`]: ../struct.Gizmo.html 10 | use super::Gizmo; 11 | use super::super::utils as utils; 12 | use wasm_bindgen::JsCast; 13 | use web_sys::{HtmlElement, HtmlInputElement}; 14 | 15 | /// From https://doc.rust-lang.org/rust-by-example/macros/designators.html 16 | macro_rules! tag_constructor { 17 | ( $func_name:ident ) => { 18 | pub fn $func_name() -> Gizmo { 19 | let element = 20 | utils::document() 21 | .create_element(stringify!($func_name)) 22 | .expect("element") 23 | .unchecked_into(); 24 | Gizmo::wrapping(element) 25 | } 26 | }; 27 | 28 | ( $e:ident, $($es:ident),+ ) => { 29 | tag_constructor!{$e} 30 | tag_constructor!{$($es),+} 31 | } 32 | } 33 | 34 | // structural 35 | tag_constructor!{ 36 | a, 37 | article, 38 | aside, 39 | body, 40 | br, 41 | details, 42 | div, 43 | h1, 44 | h2, 45 | h3, 46 | h4, 47 | h5, 48 | h6, 49 | head, 50 | header, 51 | hgroup, 52 | hr, 53 | html, 54 | footer, 55 | nav, 56 | p, 57 | section, 58 | span, 59 | summary 60 | } 61 | 62 | // metadata 63 | tag_constructor!{ 64 | base, 65 | basefont, 66 | link, 67 | meta, 68 | style, 69 | title 70 | } 71 | 72 | // form 73 | tag_constructor!{ 74 | button, 75 | datalist, 76 | fieldset, 77 | form, 78 | keygen, 79 | label, 80 | legend, 81 | meter, 82 | optgroup, 83 | option, 84 | select, 85 | textarea 86 | } 87 | 88 | pub fn input() -> Gizmo { 89 | let element:HtmlInputElement = 90 | utils::document() 91 | .create_element("input") 92 | .expect("can't create element") 93 | .unchecked_into(); 94 | Gizmo::wrapping(element) 95 | } 96 | 97 | // formatting 98 | tag_constructor!{ 99 | abbr, 100 | acronym, 101 | address, 102 | b, 103 | bdi, 104 | bdo, 105 | big, 106 | blockquote, 107 | center, 108 | cite, 109 | code, 110 | del, 111 | dfn, 112 | em, 113 | font, 114 | i, 115 | ins, 116 | kbd, 117 | mark, 118 | output, 119 | pre, 120 | progress, 121 | q, 122 | rp, 123 | rt, 124 | ruby, 125 | s, 126 | samp, 127 | small, 128 | strike, 129 | strong, 130 | sub, 131 | sup, 132 | tt, 133 | u, 134 | var, 135 | wbr 136 | } 137 | 138 | // list 139 | tag_constructor!{ 140 | dd, 141 | dir, 142 | dl, 143 | dt, 144 | li, 145 | ol, 146 | menu, 147 | ul 148 | } 149 | // table 150 | tag_constructor!{ 151 | caption, 152 | col, 153 | colgroup, 154 | table, 155 | tbody, 156 | td, 157 | tfoot, 158 | thead, 159 | th, 160 | tr 161 | } 162 | 163 | // scripting 164 | tag_constructor!{ 165 | noscript, 166 | script 167 | } 168 | 169 | // embedded content 170 | tag_constructor!{ 171 | applet, 172 | area, 173 | audio, 174 | canvas, 175 | embed, 176 | figcaption, 177 | figure, 178 | frame, 179 | frameset, 180 | iframe, 181 | img, 182 | map, 183 | noframes, 184 | object, 185 | param, 186 | source, 187 | time, 188 | video 189 | } 190 | -------------------------------------------------------------------------------- /mogwai/src/an_introduction.rs: -------------------------------------------------------------------------------- 1 | //! An introduction to the minimal, obvious, graphical web application interface. 2 | //! 3 | //! # Welcome! 4 | //! Mogwai is a cute little library for building browser interfaces. It is 5 | //! cognitively small and runtime fast. It acheives these goals by doing very few 6 | //! things, but doing those things well. 7 | //! 8 | //! The following is a short introduction to the concepts of Mogwai. 9 | //! 10 | //! ## Building Gizmos (aka constructing DOM widgets) 11 | //! Building DOM is one of the main authorship modes in Mogwai. DOM nodes 12 | //! are created using a builder pattern. The builder itself is called a 13 | //! [`Gizmo`] and it defines a user interface widget. `Gizmo`s can be run or 14 | //! attached to other `Gizmo`s. The builder pattern looks like this: 15 | //! 16 | //! ```rust, no_run 17 | //! extern crate mogwai; 18 | //! use::mogwai::prelude::*; 19 | //! 20 | //! div() 21 | //! .class("my-div") 22 | //! .with( 23 | //! a() 24 | //! .attribute("href", "http://zyghost.com") 25 | //! .text("Schellsan's website") 26 | //! ) 27 | //! .run().unwrap_throw() 28 | //! ``` 29 | //! 30 | //! The example above would create a DOM node with a link inside it and append it 31 | //! to the document body. It would look like this: 32 | //! 33 | //! ```html 34 | //!
35 | //! Schell's website 36 | //!
37 | //! ``` 38 | //! 39 | //! ### Running Gizmos and removing gizmos 40 | //! 41 | //! Note that the [`Gizmo`] is added to the body automatically with the 42 | //! [`Gizmo::run`] function. This `run` function is special. It hands off the 43 | //! gizmo to be owned by the window - otherwise the gizmo would go out of scope 44 | //! and be dropped. This is important when a gizmo is dropped and all references 45 | //! to its inner DOM node are no longer in scope, that DOM nodeis removed from 46 | //! the DOM. 47 | //! 48 | //! You may have noticed that we can use the [`Gizmo::class`] method to set 49 | //! the class of our `div` tag, but we use the [`Gizmo::attribute`] method 50 | //! to set the href attribute of our `a` tag. That's because [`Gizmo::class`] 51 | //! is a convenience method that simply calls [`Gizmo::attribute`] under the 52 | //! hood. Some DOM attributes have these conveniences and others do not. See the 53 | //! documentation for [`Gizmo`] for more info. If you don't see a method that 54 | //! you think should be there, I welcome you to 55 | //! [add it in a pull request](https://github.com/schell/mogwai) :) 56 | //! 57 | //! ### Wiring DOM 58 | //! 59 | //! `Gizmo`s can be static like the one above, or they can change over time. 60 | //! `Gizmo`s get their dynamic values from the receiving end of a channel 61 | //! called a [`Receiver`]. The transmitting end of the channel is called a 62 | //! [`Transmitter`]. This should be somewhat familiar if you've ever used a 63 | //! channel in other rust libraries. 64 | //! 65 | //! Creating a channel is easy using the [txrx()] function. Then we "wire" it 66 | //! into the `Gizmo` with one of a number of `rx_` flavored `Gizmo` 67 | //! methods. 68 | //! 69 | //! Whenever the `Transmitter` of the channel sends a value, the DOM is 70 | //! updated. 71 | //! 72 | //! ```rust, no_run 73 | //! extern crate mogwai; 74 | //! use::mogwai::prelude::*; 75 | //! 76 | //! let (tx, rx) = txrx(); 77 | //! 78 | //! div() 79 | //! .class("my-div") 80 | //! .with( 81 | //! a() 82 | //! .attribute("href", "https://zyghost.com") 83 | //! .rx_text("Schellsan's website", rx) 84 | //! ) 85 | //! .run().unwrap_throw(); 86 | //! 87 | //! tx.send(&"Gizmo's website".into()); 88 | //! ``` 89 | //! 90 | //! Just like previously, this builds a DOM node and appends it to the document 91 | //! body, but this time we've already updated the link's text to "Gizmo's website": 92 | //! 93 | //! ```html 94 | //!
95 | //! Gizmo's website 96 | //!
97 | //! ``` 98 | //! 99 | //! ### Components and more advanced wiring 100 | //! 101 | //! In bigger applications we often have circular dependencies between buttons, 102 | //! fields and other interface elements. When these complex situations arise 103 | //! we compartmentalize concerns into [`Component`]s. 104 | //! 105 | //! Other times we don't need a full component with an update cycle and instead 106 | //! we simply require 107 | //! [transmitters, receivers and some handy folds and maps](super::txrx). 108 | //! 109 | //! ## JavaScript interoperability 110 | //! The library itself is a thin layer on top of the 111 | //! [web-sys](https://crates.io/crates/web-sys) crate which provides raw bindings 112 | //! to _tons_ of browser web APIs. Many of the DOM specific structs, enums and 113 | //! traits come from `web-sys`. Some of those types are re-exported by Mogwai's 114 | //! [prelude](../prelude). The most important trait to understand for the 115 | //! purposes of this introduction (and for writing web apps in Rust, in general) 116 | //! is the [`JsCast`](../prelude/trait.JsCast.html) trait. Its `dyn_into` and 117 | //! `dyn_ref` functions are the primary way to cast JavaScript values as specific 118 | //! types. 119 | //! 120 | //! [`Gizmo::build`]: Gizmo::build 121 | //! [`Gizmo::run`]: Gizmo::method@run 122 | //! [`Gizmo`]: struct@Gizmo 123 | //! [`Gizmo`]: struct@Gizmo 124 | //! [`Transmitter`]: struct@Transmitter 125 | //! [`Receiver`]: struct@Receiver 126 | //! [`HtmlElement`]: struct@HtmlElement 127 | //! [`Component`]: trait@Component 128 | use super::gizmo::*; 129 | use super::txrx::*; 130 | use super::component::*; 131 | use super::component::subscriber::*; 132 | 133 | 134 | struct Unit {} 135 | 136 | impl Component for Unit { 137 | type ModelMsg = (); 138 | type ViewMsg = (); 139 | type DomNode = Element; 140 | 141 | fn view(&self, _: Transmitter<()>, _: Receiver<()>) -> Gizmo { 142 | Gizmo::element("") as Gizmo 143 | } 144 | fn update(&mut self, _: &(), _: &Transmitter<()>, _sub: &Subscriber<()>) {} 145 | } 146 | 147 | // This is here just for the documentation links. 148 | fn _not_used() { 149 | let _element = Gizmo::element(""); 150 | let (_tx, _rx) = txrx::<()>(); 151 | } 152 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

3 | 4 | 5 | 6 |
7 | mogwai 8 |

9 |
10 | 11 | 12 | > **m**inimalist, **o**bvious, **g**raphical **w**eb **a**pplication **i**nterface 13 | 14 | 15 | release: [![Crates.io][ci]][cl] ![cicd](https://github.com/schell/mogwai/workflows/cicd/badge.svg?branch=release) 16 | 17 | master: ![cicd](https://github.com/schell/mogwai/workflows/cicd/badge.svg?branch=master) 18 | 19 | [ci]: https://img.shields.io/crates/v/mogwai.svg 20 | [cl]: https://crates.io/crates/mogwai/ 21 | 22 | 23 | `mogwai` is a frontend DOM library for creating web applications. 24 | It is written in Rust and runs in your browser. It is an alternative 25 | to React, Backbone, Ember, Elm, Purescript, etc. 26 | 27 | ## goals 28 | 29 | * provide a declarative approach to creating and managing DOM nodes 30 | * encapsulate component state and compose components easily 31 | * explicate DOM updates 32 | * be small and fast (snappy af) 33 | 34 | If mogwai achieves these goals, which I think it does, then maintaining 35 | application state, composing widgets and reasoning about your program will be 36 | easy. Furthermore, your users will be happy because their UI is snappy! 37 | 38 | ## example 39 | Here is an example of a button that counts its own clicks. 40 | 41 | ```rust, no_run 42 | extern crate mogwai; 43 | use mogwai::prelude::*; 44 | 45 | let (tx, rx) = 46 | txrx_fold( 47 | 0, 48 | |n:&mut i32, _:&Event| -> String { 49 | *n += 1; 50 | if *n == 1 { 51 | "Clicked 1 time".to_string() 52 | } else { 53 | format!("Clicked {} times", *n) 54 | } 55 | } 56 | ); 57 | 58 | button() 59 | .rx_text("Clicked 0 times", rx) 60 | .tx_on("click", tx) 61 | .run().unwrap_throw() 62 | ``` 63 | 64 | Here's that same example using the elm-like `Component` trait: 65 | 66 | ```rust, no_run 67 | use mogwai::prelude::*; 68 | 69 | pub struct Button { 70 | pub clicks: i32 71 | } 72 | 73 | #[derive(Clone)] 74 | pub enum ButtonIn { 75 | Click 76 | } 77 | 78 | #[derive(Clone)] 79 | pub enum ButtonOut { 80 | Clicks(i32) 81 | } 82 | 83 | impl Component for Button { 84 | type ModelMsg = ButtonIn; 85 | type ViewMsg = ButtonOut; 86 | type DomNode = HtmlElement; 87 | 88 | fn update( 89 | &mut self, 90 | msg: &ButtonIn, 91 | tx_view: &Transmitter, 92 | _subscriber: &Subscriber 93 | ) { 94 | match msg { 95 | ButtonIn::Click => { 96 | self.clicks += 1; 97 | tx_view.send(&ButtonOut::Clicks(self.clicks)) 98 | } 99 | } 100 | } 101 | 102 | fn view( 103 | &self, 104 | tx: Transmitter, 105 | rx: Receiver 106 | ) -> Gizmo { 107 | button() 108 | .rx_text("Clicked 0 times", rx.branch_map(|msg| { 109 | match msg { 110 | ButtonOut::Clicks(n) => format!("Clicked {} times", n) 111 | } 112 | })) 113 | .tx_on("click", tx.contra_map(|_| ButtonIn::Click)) 114 | } 115 | } 116 | 117 | Button{ clicks: 0 } 118 | .into_component() 119 | .run().unwrap_throw() 120 | ``` 121 | 122 | ## introduction 123 | If you're interested in learning more - please read the [introduction and 124 | documentation](https://docs.rs/mogwai/). 125 | 126 | ## why 127 | Rust is beginning to have a good number of frontend libraries. Most however, 128 | encorporate a virtual DOM with a magical update phase. Even in a languague that 129 | has performance to spare this step can cause unwanted slowness. 130 | 131 | `mogwai` lives in a happy space just above "bare metal". It does this by 132 | providing the tools needed to declare exactly which parts of the DOM change and 133 | when. 134 | 135 | These same tools encourage functional progamming patterns like encapsulation over 136 | inheritance (or traits, in this case). 137 | 138 | Channel-like primitives and a declarative html builder are used to define 139 | components and then wire them together. Once the interface is defined and built, 140 | the channels are effectively erased and it's functions all the way down. There's 141 | no performance overhead from vdom, shadow dom, polling or patching. So if you 142 | prefer a functional style of programming with lots of maps and folds - or if 143 | you're looking to go _vroom!_ then maybe `mogwai` is right for you and your 144 | team :) 145 | 146 | ### made for rustaceans, by a rustacean 147 | Another benefit of `mogwai` is that it is Rust-first. There is no requirement 148 | that you have `npm` or `node`. Getting your project up and running without 149 | writing any javascript is easy enough. 150 | 151 | ### performance 152 | `mogwai` is snappy! Here is a very handwavey and sketchy todomvc benchmark: 153 | 154 | ![mogwai performance benchmarking](img/perf.png) 155 | 156 | ## ok - where do i start? 157 | First you'll need new(ish) version of the rust toolchain. For that you can visit 158 | https://rustup.rs/ and follow the installation instructions. 159 | 160 | Then you'll need [wasm-pack](https://rustwasm.github.io/wasm-pack/installer/). 161 | 162 | For starting a new mogwai project we'll use the wonderful `cargo-generate`, which 163 | can be installed using `cargo install cargo-generate`. 164 | 165 | Then run 166 | ```shell 167 | cargo generate --git https://github.com/schell/mogwai-template.git 168 | ``` 169 | and give the command line a project name. Then `cd` into your sparkling new 170 | project and 171 | ```shell 172 | wasm-pack build --target no-modules 173 | ``` 174 | Then, if you don't already have it, `cargo install basic-http-server` or use your 175 | favorite alternative to serve your app: 176 | ```shell 177 | basic-http-server -a 127.0.0.1:8888 178 | ``` 179 | Happy hacking! :coffee: :coffee: :coffee: 180 | 181 | ## more examples please 182 | For more examples, check out 183 | 184 | [the sandbox](https://github.com/schell/mogwai/blob/master/examples/sandbox/) 185 | 186 | [the todomvc app](https://github.com/schell/mogwai/blob/master/examples/todomvc) 187 | 188 | [the benchmark suite](https://github.com/schell/todo-mvc-bench/) 189 | 190 | To build the examples use: 191 | ```shell 192 | cd examples/whatever && wasm-pack build --target no-modules 193 | ``` 194 | 195 | ## sponsorship 196 | Please consider sponsoring the development of this library! 197 | 198 | * [sponsor me on github](https://github.com/sponsors/schell/) 199 | -------------------------------------------------------------------------------- /examples/todomvc/src/app/item.rs: -------------------------------------------------------------------------------- 1 | use mogwai::prelude::*; 2 | use web_sys::KeyboardEvent; 3 | 4 | use super::utils; 5 | 6 | 7 | #[derive(Clone)] 8 | pub struct Todo { 9 | pub index: usize, 10 | pub is_done: bool, 11 | pub name: String, 12 | is_editing: bool, 13 | edit_input: Option, 14 | toggle_input: Option, 15 | } 16 | 17 | 18 | impl Todo { 19 | pub fn new(index: usize, name: String) -> Todo { 20 | Todo { 21 | index, 22 | name, 23 | is_done: false, 24 | is_editing: false, 25 | edit_input: None, 26 | toggle_input: None, 27 | } 28 | } 29 | } 30 | 31 | 32 | #[derive(Clone)] 33 | pub enum TodoIn { 34 | CompletionToggleInput(HtmlInputElement), 35 | EditInput(HtmlInputElement), 36 | ToggleCompletion, 37 | SetCompletion(bool), 38 | StartEditing, 39 | StopEditing(Option), 40 | SetVisible(bool), 41 | Remove, 42 | } 43 | 44 | 45 | #[derive(Clone)] 46 | pub enum TodoOut { 47 | UpdateEditComplete(bool, bool), 48 | SetName(String), 49 | SetVisible(bool), 50 | Remove, 51 | } 52 | 53 | 54 | impl TodoOut { 55 | fn as_list_class(&self) -> Option { 56 | match self { 57 | TodoOut::UpdateEditComplete(editing, completed) => Some( 58 | if *editing { 59 | "editing" 60 | } else if *completed { 61 | "completed" 62 | } else { 63 | "" 64 | } 65 | .to_string(), 66 | ), 67 | _ => None, 68 | } 69 | } 70 | } 71 | 72 | 73 | impl Component for Todo { 74 | type ModelMsg = TodoIn; 75 | type ViewMsg = TodoOut; 76 | type DomNode = HtmlElement; 77 | 78 | fn update( 79 | &mut self, 80 | msg: &TodoIn, 81 | tx_view: &Transmitter, 82 | _: &Subscriber, 83 | ) { 84 | match msg { 85 | TodoIn::SetVisible(visible) => { 86 | tx_view.send(&TodoOut::SetVisible(*visible)); 87 | } 88 | TodoIn::CompletionToggleInput(el) => { 89 | self.toggle_input = Some(el.clone()); 90 | } 91 | TodoIn::EditInput(el) => { 92 | self.edit_input = Some(el.clone()); 93 | } 94 | TodoIn::ToggleCompletion => { 95 | self.is_done = !self.is_done; 96 | tx_view 97 | .send(&TodoOut::UpdateEditComplete(self.is_editing, self.is_done)); 98 | } 99 | TodoIn::SetCompletion(completed) => { 100 | self.is_done = *completed; 101 | self 102 | .toggle_input 103 | .iter() 104 | .for_each(|input| input.set_checked(*completed)); 105 | tx_view 106 | .send(&TodoOut::UpdateEditComplete(self.is_editing, self.is_done)); 107 | } 108 | TodoIn::StartEditing => { 109 | self.is_editing = true; 110 | let input: HtmlInputElement = 111 | self.edit_input.as_ref().expect("no input").clone(); 112 | timeout(1, move || { 113 | input.focus().expect("can't focus"); 114 | false 115 | }); 116 | tx_view 117 | .send(&TodoOut::UpdateEditComplete(self.is_editing, self.is_done)); 118 | } 119 | TodoIn::StopEditing(may_ev) => { 120 | self.is_editing = false; 121 | 122 | let input: &HtmlInputElement = self.edit_input.as_ref().expect("no input"); 123 | 124 | if let Some(ev) = may_ev { 125 | // This came from a key event 126 | let kev = ev.unchecked_ref::(); 127 | let key = kev.key(); 128 | if key == "Enter" { 129 | utils::input_value(input) 130 | .into_iter() 131 | .for_each(|name| self.name = name); 132 | } else if key == "Escape" { 133 | self 134 | .edit_input 135 | .iter() 136 | .for_each(|input| input.set_value(&self.name)); 137 | } 138 | } else { 139 | // This came from an input change event 140 | utils::input_value(input) 141 | .into_iter() 142 | .for_each(|name| self.name = name); 143 | } 144 | tx_view.send(&TodoOut::SetName(self.name.clone())); 145 | tx_view 146 | .send(&TodoOut::UpdateEditComplete(self.is_editing, self.is_done)); 147 | } 148 | TodoIn::Remove => { 149 | // A todo cannot remove itself - its gizmo is owned by the parent App. 150 | // So we'll fire out a TodoOut::Remove and let App's update function 151 | // handle that. 152 | tx_view.send(&TodoOut::Remove); 153 | } 154 | } 155 | } 156 | 157 | fn view( 158 | &self, 159 | tx: Transmitter, 160 | rx: Receiver, 161 | ) -> Gizmo { 162 | li() 163 | .rx_class("", rx.branch_filter_map(|msg| msg.as_list_class())) 164 | .rx_style( 165 | "display", 166 | "block", 167 | rx.branch_filter_map(|msg| match msg { 168 | TodoOut::SetVisible(visible) => { 169 | Some(if *visible { "block" } else { "none" }.to_string()) 170 | } 171 | _ => None, 172 | }), 173 | ) 174 | .with( 175 | div() 176 | .class("view") 177 | .with( 178 | input() 179 | .class("toggle") 180 | .attribute("type", "checkbox") 181 | .style("cursor", "pointer") 182 | .tx_post_build(tx.contra_map(|el: &HtmlInputElement| { 183 | TodoIn::CompletionToggleInput(el.clone()) 184 | })) 185 | .tx_on( 186 | "click", 187 | tx.contra_map(|_: &Event| TodoIn::ToggleCompletion), 188 | ), 189 | ) 190 | .with( 191 | label() 192 | .rx_text( 193 | &self.name, 194 | rx.branch_filter_map(|msg| match msg { 195 | TodoOut::SetName(name) => Some(name.clone()), 196 | _ => None, 197 | }), 198 | ) 199 | .tx_on( 200 | "dblclick", 201 | tx.contra_map(|_: &Event| TodoIn::StartEditing), 202 | ), 203 | ) 204 | .with( 205 | button() 206 | .class("destroy") 207 | .style("cursor", "pointer") 208 | .tx_on("click", tx.contra_map(|_: &Event| TodoIn::Remove)), 209 | ), 210 | ) 211 | .with( 212 | input() 213 | .class("edit") 214 | .value(&self.name) 215 | .tx_post_build( 216 | tx.contra_map(|el: &HtmlInputElement| { 217 | TodoIn::EditInput(el.clone()) 218 | }), 219 | ) 220 | .tx_on("blur", tx.contra_map(|_: &Event| TodoIn::StopEditing(None))) 221 | .tx_on( 222 | "keyup", 223 | tx.contra_map(|ev: &Event| TodoIn::StopEditing(Some(ev.clone()))), 224 | ), 225 | ) 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /examples/sandbox/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod elm_button; 2 | 3 | use log::Level; 4 | use wasm_bindgen::prelude::*; 5 | use web_sys::{Request, RequestMode, RequestInit, Response}; 6 | use mogwai::prelude::*; 7 | use std::panic; 8 | 9 | 10 | /// Defines a button that changes its text every time it is clicked. 11 | /// Once built, the button will also transmit clicks into the given transmitter. 12 | pub fn new_button_gizmo(mut tx_click: Transmitter) -> Gizmo { 13 | // Create a receiver for our button to get its text from. 14 | let rx_text = Receiver::::new(); 15 | 16 | // Create the button that gets its text from our receiver. 17 | // 18 | // The button text will start out as "Click me" and then change to whatever 19 | // comes in on the receiver. 20 | let button = 21 | button() 22 | .style("cursor", "pointer") 23 | // The button receives its text 24 | .rx_text("Click me", rx_text.branch()) 25 | // The button transmits its clicks 26 | .tx_on("click", tx_click.clone()); 27 | 28 | // Now that the routing is done, we can define how the signal changes from 29 | // transmitter to receiver over each occurance. 30 | // We do this by wiring the two together, along with some internal state in the 31 | // form of a fold function. 32 | tx_click.wire_fold( 33 | &rx_text, 34 | true, // our initial folding state 35 | |is_red, _| { 36 | 37 | let out = 38 | if *is_red { 39 | "Turn me blue".into() 40 | } else { 41 | "Turn me red".into() 42 | }; 43 | 44 | *is_red = !*is_red; 45 | out 46 | } 47 | ); 48 | 49 | button 50 | } 51 | 52 | 53 | /// Creates a h1 heading that changes its color. 54 | pub fn new_h1_gizmo(mut tx_click:Transmitter) -> Gizmo { 55 | // Create a receiver for our heading to get its color from. 56 | let rx_color = Receiver::::new(); 57 | 58 | // Create the gizmo for our heading, giving it the receiver. 59 | let h1 = 60 | h1() 61 | .attribute("id", "header") 62 | .attribute("class", "my-header") 63 | .rx_style("color", "green", rx_color.branch()) 64 | .text("Hello from mogwai!"); 65 | 66 | // Now that the routing is done, let's define the logic 67 | // The h1's color will change every click back and forth between blue and red 68 | // after the initial green. 69 | tx_click.wire_fold( 70 | &rx_color, 71 | false, // the intial value for is_red 72 | |is_red, _| { 73 | 74 | let out = 75 | if *is_red { 76 | "blue".into() 77 | } else { 78 | "red".into() 79 | }; 80 | *is_red = !*is_red; 81 | out 82 | }); 83 | 84 | h1 85 | } 86 | 87 | 88 | async fn request_to_text(req:Request) -> Result { 89 | let resp:Response = 90 | JsFuture::from( 91 | window() 92 | .fetch_with_request(&req) 93 | ) 94 | .await 95 | .map_err(|_| "request failed".to_string())? 96 | .dyn_into() 97 | .map_err(|_| "response is malformed")?; 98 | let text:String = 99 | JsFuture::from( 100 | resp 101 | .text() 102 | .map_err(|_| "could not get response text")? 103 | ) 104 | .await 105 | .map_err(|_| "getting text failed")? 106 | .as_string() 107 | .ok_or("couldn't get text as string".to_string())?; 108 | Ok(text) 109 | } 110 | 111 | 112 | async fn click_to_text() -> Option { 113 | let mut opts = 114 | RequestInit::new(); 115 | opts.method("GET"); 116 | opts.mode(RequestMode::Cors); 117 | 118 | let req = 119 | Request::new_with_str_and_init( 120 | "https://worldtimeapi.org/api/timezone/Europe/London.txt", 121 | &opts 122 | ) 123 | .unwrap_throw(); 124 | 125 | let result = 126 | match request_to_text(req).await { 127 | Ok(s) => { s } 128 | Err(s) => { s } 129 | }; 130 | Some(result) 131 | } 132 | 133 | 134 | /// Creates a button that when clicked requests the time in london and sends 135 | /// it down a receiver. 136 | pub fn time_req_button_and_pre() -> Gizmo { 137 | let (req_tx, req_rx) = txrx::(); 138 | let (resp_tx, resp_rx) = txrx::(); 139 | 140 | req_rx 141 | .forward_filter_fold_async( 142 | &resp_tx, 143 | false, 144 | |is_in_flight:&mut bool, _| { 145 | // When we receive a click event from the button and we're not already 146 | // sending a request, we'll set one up and send it. 147 | if !*is_in_flight { 148 | // Change the state to tell later invocations that a request is in 149 | // flight 150 | *is_in_flight = true; 151 | // Return a future to be excuted which possibly produces a value to 152 | // send downstream to resp_tx 153 | wrap_future(async {click_to_text().await}) 154 | } else { 155 | 156 | // Don't change the state and don't send anything downstream to 157 | // resp_tx 158 | None 159 | } 160 | }, 161 | |is_in_flight, _| { 162 | // the cleanup function reports that the request is no longer in flight 163 | *is_in_flight = false; 164 | } 165 | ); 166 | 167 | let btn = 168 | button() 169 | .style("cursor", "pointer") 170 | .text("Get the time (london)") 171 | .tx_on("click", req_tx); 172 | let pre = pre().rx_text("(waiting)", resp_rx); 173 | div() 174 | .with(btn) 175 | .with(pre) 176 | } 177 | 178 | 179 | /// Creates a gizmo that ticks a count upward every second. 180 | pub fn counter() -> Gizmo { 181 | // Create a transmitter to send ticks every second 182 | let mut tx = Transmitter::<()>::new(); 183 | 184 | // Create a receiver for a string to accept our counter's text 185 | let rx = Receiver::::new(); 186 | 187 | let timeout_tx = tx.clone(); 188 | timeout(1000, move || { 189 | // Once a second send a unit down the pipe 190 | timeout_tx.send(&()); 191 | // Always reschedule this timeout 192 | true 193 | }); 194 | 195 | // Wire the tx to the rx using a fold function 196 | tx.wire_fold( 197 | &rx, 198 | 0, 199 | |n:&mut i32, &()| { 200 | *n += 1; 201 | format!("Count: {}", *n) 202 | } 203 | ); 204 | 205 | Gizmo::element("h3") 206 | .rx_text("Awaiting first count", rx) 207 | } 208 | 209 | 210 | #[wasm_bindgen] 211 | pub fn main() -> Result<(), JsValue> { 212 | panic::set_hook(Box::new(console_error_panic_hook::hook)); 213 | console_log::init_with_level(Level::Trace) 214 | .unwrap_throw(); 215 | 216 | // Create a transmitter to send button clicks into. 217 | let tx_click = Transmitter::new(); 218 | let h1 = new_h1_gizmo(tx_click.clone()); 219 | let btn = new_button_gizmo(tx_click); 220 | let req = time_req_button_and_pre(); 221 | let counter = counter(); 222 | 223 | // Put it all in a parent gizmo and run it right now 224 | div() 225 | .with(h1) 226 | .with(btn) 227 | .with(elm_button::Button{ clicks: 0 }) 228 | .with(Gizmo::element("br")) 229 | .with(Gizmo::element("br")) 230 | .with(req) 231 | .with(counter) 232 | .run() 233 | } 234 | -------------------------------------------------------------------------------- /examples/todomvc/todo.css: -------------------------------------------------------------------------------- 1 | hr { 2 | margin: 20px 0; 3 | border: 0; 4 | border-top: 1px dashed #c5c5c5; 5 | border-bottom: 1px dashed #f7f7f7; 6 | } 7 | 8 | .learn a { 9 | font-weight: normal; 10 | text-decoration: none; 11 | color: #b83f45; 12 | } 13 | 14 | .learn a:hover { 15 | text-decoration: underline; 16 | color: #787e7e; 17 | } 18 | 19 | .learn h3, 20 | .learn h4, 21 | .learn h5 { 22 | margin: 10px 0; 23 | font-weight: 500; 24 | line-height: 1.2; 25 | color: #000; 26 | } 27 | 28 | .learn h3 { 29 | font-size: 24px; 30 | } 31 | 32 | .learn h4 { 33 | font-size: 18px; 34 | } 35 | 36 | .learn h5 { 37 | margin-bottom: 0; 38 | font-size: 14px; 39 | } 40 | 41 | .learn ul { 42 | padding: 0; 43 | margin: 0 0 30px 25px; 44 | } 45 | 46 | .learn li { 47 | line-height: 20px; 48 | } 49 | 50 | .learn p { 51 | font-size: 15px; 52 | font-weight: 300; 53 | line-height: 1.3; 54 | margin-top: 0; 55 | margin-bottom: 0; 56 | } 57 | 58 | #issue-count { 59 | display: none; 60 | } 61 | 62 | .quote { 63 | border: none; 64 | margin: 20px 0 60px 0; 65 | } 66 | 67 | .quote p { 68 | font-style: italic; 69 | } 70 | 71 | .quote p:before { 72 | content: '“'; 73 | font-size: 50px; 74 | opacity: .15; 75 | position: absolute; 76 | top: -20px; 77 | left: 3px; 78 | } 79 | 80 | .quote p:after { 81 | content: '”'; 82 | font-size: 50px; 83 | opacity: .15; 84 | position: absolute; 85 | bottom: -42px; 86 | right: 3px; 87 | } 88 | 89 | .quote footer { 90 | position: absolute; 91 | bottom: -40px; 92 | right: 0; 93 | } 94 | 95 | .quote footer img { 96 | border-radius: 3px; 97 | } 98 | 99 | .quote footer a { 100 | margin-left: 5px; 101 | vertical-align: middle; 102 | } 103 | 104 | .speech-bubble { 105 | position: relative; 106 | padding: 10px; 107 | background: rgba(0, 0, 0, .04); 108 | border-radius: 5px; 109 | } 110 | 111 | .speech-bubble:after { 112 | content: ''; 113 | position: absolute; 114 | top: 100%; 115 | right: 30px; 116 | border: 13px solid transparent; 117 | border-top-color: rgba(0, 0, 0, .04); 118 | } 119 | 120 | .learn-bar > .learn { 121 | position: absolute; 122 | width: 272px; 123 | top: 8px; 124 | left: -300px; 125 | padding: 10px; 126 | border-radius: 5px; 127 | background-color: rgba(255, 255, 255, .6); 128 | transition-property: left; 129 | transition-duration: 500ms; 130 | } 131 | 132 | @media (min-width: 899px) { 133 | .learn-bar { 134 | width: auto; 135 | padding-left: 300px; 136 | } 137 | 138 | .learn-bar > .learn { 139 | left: 8px; 140 | } 141 | } 142 | 143 | html, 144 | body { 145 | margin: 0; 146 | padding: 0; 147 | } 148 | 149 | button { 150 | margin: 0; 151 | padding: 0; 152 | border: 0; 153 | background: none; 154 | font-size: 100%; 155 | vertical-align: baseline; 156 | font-family: inherit; 157 | font-weight: inherit; 158 | color: inherit; 159 | -webkit-appearance: none; 160 | appearance: none; 161 | -webkit-font-smoothing: antialiased; 162 | -moz-osx-font-smoothing: grayscale; 163 | } 164 | 165 | body { 166 | font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; 167 | line-height: 1.4em; 168 | background: #f5f5f5; 169 | color: #4d4d4d; 170 | min-width: 230px; 171 | max-width: 550px; 172 | margin: 0 auto; 173 | -webkit-font-smoothing: antialiased; 174 | -moz-osx-font-smoothing: grayscale; 175 | font-weight: 300; 176 | } 177 | 178 | :focus { 179 | outline: 0; 180 | } 181 | 182 | .hidden { 183 | display: none; 184 | } 185 | 186 | .todoapp { 187 | background: #fff; 188 | margin: 130px 0 40px 0; 189 | position: relative; 190 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 191 | 0 25px 50px 0 rgba(0, 0, 0, 0.1); 192 | } 193 | 194 | .todoapp input::-webkit-input-placeholder { 195 | font-style: italic; 196 | font-weight: 300; 197 | color: #e6e6e6; 198 | } 199 | 200 | .todoapp input::-moz-placeholder { 201 | font-style: italic; 202 | font-weight: 300; 203 | color: #e6e6e6; 204 | } 205 | 206 | .todoapp input::input-placeholder { 207 | font-style: italic; 208 | font-weight: 300; 209 | color: #e6e6e6; 210 | } 211 | 212 | .todoapp h1 { 213 | position: absolute; 214 | top: -155px; 215 | width: 100%; 216 | font-size: 100px; 217 | font-weight: 100; 218 | text-align: center; 219 | color: rgba(175, 47, 47, 0.15); 220 | -webkit-text-rendering: optimizeLegibility; 221 | -moz-text-rendering: optimizeLegibility; 222 | text-rendering: optimizeLegibility; 223 | } 224 | 225 | .new-todo, 226 | .edit { 227 | position: relative; 228 | margin: 0; 229 | width: 100%; 230 | font-size: 24px; 231 | font-family: inherit; 232 | font-weight: inherit; 233 | line-height: 1.4em; 234 | border: 0; 235 | color: inherit; 236 | padding: 6px; 237 | border: 1px solid #999; 238 | box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); 239 | box-sizing: border-box; 240 | -webkit-font-smoothing: antialiased; 241 | -moz-osx-font-smoothing: grayscale; 242 | } 243 | 244 | .new-todo { 245 | padding: 16px 16px 16px 60px; 246 | border: none; 247 | background: rgba(0, 0, 0, 0.003); 248 | box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03); 249 | } 250 | 251 | .main { 252 | position: relative; 253 | z-index: 2; 254 | border-top: 1px solid #e6e6e6; 255 | } 256 | 257 | .toggle-all { 258 | text-align: center; 259 | border: none; /* Mobile Safari */ 260 | opacity: 0; 261 | position: absolute; 262 | } 263 | 264 | .toggle-all + label { 265 | width: 60px; 266 | height: 34px; 267 | font-size: 0; 268 | position: absolute; 269 | top: -52px; 270 | left: -13px; 271 | -webkit-transform: rotate(90deg); 272 | transform: rotate(90deg); 273 | } 274 | 275 | .toggle-all + label:before { 276 | content: '❯'; 277 | font-size: 22px; 278 | color: #e6e6e6; 279 | padding: 10px 27px 10px 27px; 280 | } 281 | 282 | .toggle-all:checked + label:before { 283 | color: #737373; 284 | } 285 | 286 | .todo-list { 287 | margin: 0; 288 | padding: 0; 289 | list-style: none; 290 | } 291 | 292 | .todo-list li { 293 | position: relative; 294 | font-size: 24px; 295 | border-bottom: 1px solid #ededed; 296 | } 297 | 298 | .todo-list li:last-child { 299 | border-bottom: none; 300 | } 301 | 302 | .todo-list li.editing { 303 | border-bottom: none; 304 | padding: 0; 305 | } 306 | 307 | .todo-list li.editing .edit { 308 | display: block; 309 | width: 506px; 310 | padding: 12px 16px; 311 | margin: 0 0 0 43px; 312 | } 313 | 314 | .todo-list li.editing .view { 315 | display: none; 316 | } 317 | 318 | .todo-list li .toggle { 319 | text-align: center; 320 | width: 40px; 321 | /* auto, since non-WebKit browsers doesn't support input styling */ 322 | height: auto; 323 | position: absolute; 324 | top: 0; 325 | bottom: 0; 326 | margin: auto 0; 327 | border: none; /* Mobile Safari */ 328 | -webkit-appearance: none; 329 | appearance: none; 330 | } 331 | 332 | .todo-list li .toggle { 333 | opacity: 0; 334 | } 335 | 336 | .todo-list li .toggle + label { 337 | /* 338 | Firefox requires `#` to be escaped - https://bugzilla.mozilla.org/show_bug.cgi?id=922433 339 | IE and Edge requires *everything* to be escaped to render, so we do that instead of just the `#` - https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7157459/ 340 | */ 341 | background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E'); 342 | background-repeat: no-repeat; 343 | background-position: center left; 344 | } 345 | 346 | .todo-list li .toggle:checked + label { 347 | background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E'); 348 | } 349 | 350 | .todo-list li label { 351 | word-break: break-all; 352 | padding: 15px 15px 15px 60px; 353 | display: block; 354 | line-height: 1.2; 355 | transition: color 0.4s; 356 | } 357 | 358 | .todo-list li.completed label { 359 | color: #d9d9d9; 360 | text-decoration: line-through; 361 | } 362 | 363 | .todo-list li .destroy { 364 | display: none; 365 | position: absolute; 366 | top: 0; 367 | right: 10px; 368 | bottom: 0; 369 | width: 40px; 370 | height: 40px; 371 | margin: auto 0; 372 | font-size: 30px; 373 | color: #cc9a9a; 374 | margin-bottom: 11px; 375 | transition: color 0.2s ease-out; 376 | } 377 | 378 | .todo-list li .destroy:hover { 379 | color: #af5b5e; 380 | } 381 | 382 | .todo-list li .destroy:after { 383 | content: '×'; 384 | } 385 | 386 | .todo-list li:hover .destroy { 387 | display: block; 388 | } 389 | 390 | .todo-list li .edit { 391 | display: none; 392 | } 393 | 394 | .todo-list li.editing:last-child { 395 | margin-bottom: -1px; 396 | } 397 | 398 | .footer { 399 | color: #777; 400 | padding: 10px 15px; 401 | height: 20px; 402 | text-align: center; 403 | border-top: 1px solid #e6e6e6; 404 | } 405 | 406 | .footer:before { 407 | content: ''; 408 | position: absolute; 409 | right: 0; 410 | bottom: 0; 411 | left: 0; 412 | height: 50px; 413 | overflow: hidden; 414 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 415 | 0 8px 0 -3px #f6f6f6, 416 | 0 9px 1px -3px rgba(0, 0, 0, 0.2), 417 | 0 16px 0 -6px #f6f6f6, 418 | 0 17px 2px -6px rgba(0, 0, 0, 0.2); 419 | } 420 | 421 | .todo-count { 422 | float: left; 423 | text-align: left; 424 | } 425 | 426 | .todo-count strong { 427 | font-weight: 300; 428 | } 429 | 430 | .filters { 431 | margin: 0; 432 | padding: 0; 433 | list-style: none; 434 | position: absolute; 435 | right: 0; 436 | left: 0; 437 | } 438 | 439 | .filters li { 440 | display: inline; 441 | } 442 | 443 | .filters li a { 444 | color: inherit; 445 | margin: 3px; 446 | padding: 3px 7px; 447 | text-decoration: none; 448 | border: 1px solid transparent; 449 | border-radius: 3px; 450 | } 451 | 452 | .filters li a:hover { 453 | border-color: rgba(175, 47, 47, 0.1); 454 | } 455 | 456 | .filters li a.selected { 457 | border-color: rgba(175, 47, 47, 0.2); 458 | } 459 | 460 | .clear-completed, 461 | html .clear-completed:active { 462 | float: right; 463 | position: relative; 464 | line-height: 20px; 465 | text-decoration: none; 466 | cursor: pointer; 467 | } 468 | 469 | .clear-completed:hover { 470 | text-decoration: underline; 471 | } 472 | 473 | .info { 474 | margin: 65px auto 0; 475 | color: #bfbfbf; 476 | font-size: 10px; 477 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); 478 | text-align: center; 479 | } 480 | 481 | .info p { 482 | line-height: 1; 483 | } 484 | 485 | .info a { 486 | color: inherit; 487 | text-decoration: none; 488 | font-weight: 400; 489 | } 490 | 491 | .info a:hover { 492 | text-decoration: underline; 493 | } 494 | 495 | /* 496 | Hack to remove background from Mobile Safari. 497 | Can't use it globally since it destroys checkboxes in Firefox 498 | */ 499 | @media screen and (-webkit-min-device-pixel-ratio:0) { 500 | .toggle-all, 501 | .todo-list li .toggle { 502 | background: none; 503 | } 504 | 505 | .todo-list li .toggle { 506 | height: 40px; 507 | } 508 | } 509 | 510 | @media (max-width: 430px) { 511 | .footer { 512 | height: 50px; 513 | } 514 | 515 | .filters { 516 | bottom: 10px; 517 | } 518 | } 519 | -------------------------------------------------------------------------------- /mogwai/src/component.rs: -------------------------------------------------------------------------------- 1 | //! Elmesque components through model and view message passing. 2 | //! 3 | //! Sometimes an application can get so entangled that it's hard to follow the 4 | //! path of messages through `Transmitter`s, `Receiver`s and fold functions. For 5 | //! situations like these where complexity is unavoidable, Mogwai provides the 6 | //! [Component] trait and the helper struct [`GizmoComponent`]. 7 | //! 8 | //! Many rust web app libraries use a message passing pattern made famous by 9 | //! the Elm architecture to wrangle complexity. Mogwai is similar, but different 10 | //! - Like other libraries, messages come out of the DOM into your component's 11 | //! model by way of the [Component::update] function. 12 | //! - The model is updated according to the value of the model message. 13 | //! - _Unlike_ Elm-like libraries, view updates are sent out of the update 14 | //! function by hand! This sounds tedious but it's actually no big deal. You'll 15 | //! soon understand how easy this is in practice. 16 | //! 17 | //! Mogwai lacks a virtual DOM implementation. One might think that this is a 18 | //! disadvantage but to the contrary this is a strength, as it obviates the 19 | //! entire diffing phase of rendering DOM. This is where Mogwai gets its speed 20 | //! advantage. 21 | //! 22 | //! Instead of a virtual DOM Mogwai uses one more step in its model update. The 23 | //! `Component::update` method is given a `Transmitter` with which 24 | //! to send _view update messages_. Messages sent on this transmitter will in 25 | //! turn be sent out to the view to update the DOM. This forms a cycle. Messages 26 | //! come into the model from the view, update, messages go into the view from the 27 | //! model. In this way DOM updates are obvious. You know exactly where, when and 28 | //! why updates are made (both to the model and the view). 29 | //! 30 | //! Here is a minimal example of a `Component` that counts its own clicks. 31 | //! 32 | //! ```rust, no_run 33 | //! extern crate mogwai; 34 | //! use mogwai::prelude::*; 35 | //! 36 | //! #[derive(Clone)] 37 | //! enum In { 38 | //! Click 39 | //! } 40 | //! 41 | //! #[derive(Clone)] 42 | //! enum Out { 43 | //! DrawClicks(i32) 44 | //! } 45 | //! 46 | //! struct App { 47 | //! num_clicks: i32 48 | //! } 49 | //! 50 | //! impl Component for App { 51 | //! type ModelMsg = In; 52 | //! type ViewMsg = Out; 53 | //! type DomNode = HtmlElement; 54 | //! 55 | //! fn view(&self, tx: Transmitter, rx:Receiver) -> Gizmo { 56 | //! button() 57 | //! .tx_on("click", tx.contra_map(|_| In::Click)) 58 | //! .rx_text("clicks = 0", rx.branch_map(|msg| { 59 | //! match msg { 60 | //! Out::DrawClicks(n) => { 61 | //! format!("clicks = {}", n) 62 | //! } 63 | //! } 64 | //! })) 65 | //! } 66 | //! 67 | //! fn update(&mut self, msg: &In, tx_view: &Transmitter, _sub: &Subscriber) { 68 | //! match msg { 69 | //! In::Click => { 70 | //! self.num_clicks += 1; 71 | //! tx_view.send(&Out::DrawClicks(self.num_clicks)); 72 | //! } 73 | //! } 74 | //! } 75 | //! } 76 | //! 77 | //! 78 | //! pub fn main() -> Result<(), JsValue> { 79 | //! App{ num_clicks: 0 } 80 | //! .into_component() 81 | //! .run() 82 | //! } 83 | //! ``` 84 | //! 85 | //! The first step is to define the incoming messages that will update the model. 86 | //! Next we define the outgoing messages that will update our view. The `Component::view` 87 | //! trait method uses these message types to build the view. It does this by 88 | //! consuming a `Transmitter` and a `Receiver`. 89 | //! These represent the inputs and the outputs of your component. Roughly, 90 | //! `Self::ModelMsg` comes into the `update` function and `Self::ViewMsg`s go out 91 | //! of the `update` function. 92 | //! 93 | //! ## Communicating to components 94 | //! 95 | //! If your component is owned by another, the parent component can communicate to 96 | //! the child through its messages, either by calling [`GizmoComponent::update`] 97 | //! on the child component within its own `update` function or by subscribing to 98 | //! the child component's messages when the child component is created (see 99 | //! [`Subscriber`]). 100 | //! 101 | //! ## Placing components 102 | //! 103 | //! Components may be used within a [`Gizmo`] using the 104 | //! [`Gizmo::with`] function. 105 | use std::any::Any; 106 | use std::rc::Rc; 107 | use std::cell::RefCell; 108 | use std::ops::Deref; 109 | use wasm_bindgen::{JsCast, JsValue}; 110 | use web_sys::Node; 111 | 112 | use super::gizmo::{Gizmo, SubGizmo}; 113 | use super::txrx::{txrx, Receiver, Transmitter}; 114 | use super::utils; 115 | 116 | pub mod subscriber; 117 | use subscriber::Subscriber; 118 | 119 | 120 | /// Defines a component with distinct input (model update) and output 121 | /// (view update) messages. 122 | /// 123 | /// See the [module level documentation](super::component) for more details. 124 | pub trait Component 125 | where 126 | Self: Any + Sized, 127 | Self::ModelMsg: Clone, 128 | Self::ViewMsg: Clone, 129 | Self::DomNode: JsCast + AsRef + Clone, 130 | { 131 | /// A model message comes out from the view through a tx_on function into your 132 | /// component's update function. 133 | type ModelMsg; 134 | 135 | /// A view message comes out from your component's update function and changes 136 | /// the view by being used in an rx_* function. 137 | type ViewMsg; 138 | 139 | /// The type of DOM node that represents the root of this component. 140 | type DomNode; 141 | 142 | /// Update this component in response to any received model messages. 143 | /// This is essentially the component's fold function. 144 | fn update( 145 | &mut self, 146 | msg: &Self::ModelMsg, 147 | tx_view: &Transmitter, 148 | sub: &Subscriber, 149 | ); 150 | 151 | /// Produce this component's gizmo using inputs and outputs. 152 | fn view( 153 | &self, 154 | tx: Transmitter, 155 | rx: Receiver, 156 | ) -> Gizmo; 157 | 158 | /// Helper function for constructing a GizmoComponent for a type that 159 | /// implements Component. 160 | fn into_component(self) -> GizmoComponent { 161 | GizmoComponent::new(self) 162 | } 163 | } 164 | 165 | 166 | impl From for Gizmo 167 | where 168 | T: Component, 169 | T::DomNode: AsRef, 170 | D: JsCast + 'static 171 | { 172 | fn from(component: T) -> Gizmo { 173 | let gizmo:Gizmo = 174 | component 175 | .into_component() 176 | .gizmo; 177 | gizmo.upcast::() 178 | } 179 | } 180 | 181 | 182 | impl SubGizmo for T 183 | where 184 | T: Component, 185 | T::DomNode: AsRef 186 | { 187 | fn into_sub_gizmo(self) -> Result, Node> { 188 | let component:GizmoComponent = self.into_component(); 189 | component.into_sub_gizmo() 190 | } 191 | } 192 | 193 | 194 | /// A component and all of its pieces. 195 | /// 196 | /// TODO: Think about renaming Gizmo to Dom and GizmoComponent to Gizmo. 197 | /// I think people will use this GizmoComponent more often. 198 | pub struct GizmoComponent { 199 | pub trns: Transmitter, 200 | pub recv: Receiver, 201 | 202 | pub(crate) gizmo: Gizmo, 203 | pub(crate) state: Rc>, 204 | } 205 | 206 | 207 | impl Deref for GizmoComponent { 208 | type Target = Gizmo; 209 | 210 | fn deref(&self) -> &Gizmo { 211 | self.gizmo_ref() 212 | } 213 | } 214 | 215 | 216 | impl GizmoComponent 217 | where 218 | T: Component + 'static, 219 | T::ViewMsg: Clone, 220 | T::DomNode: AsRef + Clone 221 | { 222 | pub fn new(init: T) -> GizmoComponent { 223 | let component_var = Rc::new(RefCell::new(init)); 224 | let state = component_var.clone(); 225 | let (tx_out, rx_out) = txrx(); 226 | let (tx_in, rx_in) = txrx(); 227 | let subscriber = Subscriber::new(&tx_in); 228 | 229 | let (tx_view, rx_view) = txrx(); 230 | rx_in.respond(move |msg: &T::ModelMsg| { 231 | let mut t = state.borrow_mut(); 232 | T::update(&mut t, msg, &tx_view, &subscriber); 233 | }); 234 | 235 | let out_msgs = Rc::new(RefCell::new(vec![])); 236 | rx_view.respond(move |msg: &T::ViewMsg| { 237 | let should_schedule = 238 | { 239 | let mut msgs = out_msgs.borrow_mut(); 240 | msgs.push(msg.clone()); 241 | // If there is more than just this message in the queue, this 242 | // responder has already been run this frame and a timer has 243 | // already been scheduled, so there's no need to schedule another 244 | msgs.len() == 1 245 | }; 246 | if should_schedule { 247 | let out_msgs_async = out_msgs.clone(); 248 | let tx_out_async = tx_out.clone(); 249 | utils::timeout(0, move || { 250 | let msgs = 251 | { 252 | out_msgs_async 253 | .borrow_mut() 254 | .drain(0..) 255 | .collect::>() 256 | }; 257 | if msgs.len() > 0 { 258 | msgs.iter().for_each(|out_msg| { 259 | tx_out_async.send(out_msg); 260 | }); 261 | } 262 | false 263 | }); 264 | } 265 | }); 266 | 267 | let gizmo = { 268 | let component = component_var.borrow(); 269 | component.view(tx_in.clone(), rx_out.branch()) 270 | }; 271 | 272 | GizmoComponent { 273 | trns: tx_in, 274 | recv: rx_out, 275 | gizmo, 276 | state: component_var, 277 | } 278 | } 279 | 280 | /// A reference to the DomNode. 281 | pub fn dom_ref(&self) -> &T::DomNode { 282 | let gizmo:&Gizmo = &self.gizmo; 283 | gizmo.element.unchecked_ref() 284 | } 285 | 286 | /// A reference to the Gizmo. 287 | pub fn gizmo_ref(&self) -> &Gizmo { 288 | &self.gizmo 289 | } 290 | 291 | /// Send model messages into this component from a `Receiver`. 292 | /// This is helpful for sending messages to this component from 293 | /// a parent component. 294 | pub fn rx_from(self, rx: Receiver) -> GizmoComponent { 295 | rx.forward_map(&self.trns, |msg| msg.clone()); 296 | self 297 | } 298 | 299 | /// Send view messages from this component into a `Transmitter`. 300 | /// This is helpful for sending messages to this component from 301 | /// a parent component. 302 | pub fn tx_into(self, tx: &Transmitter) -> GizmoComponent { 303 | self.recv.branch().forward_map(&tx, |msg| msg.clone()); 304 | self 305 | } 306 | 307 | /// Run and initialize the component with a list of messages. 308 | /// This is equivalent to calling `run` and `update` with each message. 309 | pub fn run_init(mut self, msgs: Vec) -> Result<(), JsValue> { 310 | msgs.into_iter().for_each(|msg| { 311 | self.update(&msg); 312 | }); 313 | self.run() 314 | } 315 | 316 | /// Run this component forever 317 | pub fn run(self) -> Result<(), JsValue> { 318 | self.gizmo.run() 319 | } 320 | 321 | /// Update the component with the given message. 322 | /// This how a parent component communicates down to its child components. 323 | pub fn update(&mut self, msg: &T::ModelMsg) { 324 | self.trns.send(msg); 325 | } 326 | 327 | /// Access the component's underlying state. 328 | pub fn with_state(&self, f: F) -> N 329 | where 330 | F: Fn(&T) -> N, 331 | { 332 | let t = self.state.borrow(); 333 | f(&t) 334 | } 335 | } 336 | 337 | 338 | impl SubGizmo for GizmoComponent 339 | where 340 | T: Component, 341 | T::DomNode: AsRef 342 | { 343 | fn into_sub_gizmo(self) -> Result, Node> { 344 | self.gizmo.into_sub_gizmo() 345 | } 346 | } 347 | 348 | 349 | /// The type of function that uses a txrx pair and returns a Gizmo. 350 | pub type BuilderFn = dyn Fn(Transmitter, Receiver) -> Gizmo; 351 | 352 | 353 | /// A simple component made from a [BuilderFn]. 354 | /// 355 | /// Any function that takes a transmitter and receiver of the same type and 356 | /// returns a Gizmo can be made into a component that holds no internal 357 | /// state. It forwards all of its incoming messages to its view. 358 | /// 359 | /// ```rust,no_run 360 | /// extern crate mogwai; 361 | /// use mogwai::prelude::*; 362 | /// 363 | /// let component: SimpleComponent<(), HtmlElement> = 364 | /// (Box::new( 365 | /// |tx: Transmitter<()>, rx: Receiver<()>| -> Gizmo { 366 | /// button() 367 | /// .style("cursor", "pointer") 368 | /// .rx_text("Click me", rx.branch_map(|()| "Clicked!".to_string())) 369 | /// .tx_on("click", tx.contra_map(|_| ())) 370 | /// }, 371 | /// ) as Box>) 372 | /// .into_component(); 373 | /// ``` 374 | pub type SimpleComponent = GizmoComponent>>; 375 | 376 | 377 | impl Component for Box> 378 | where 379 | T: Any + Clone, 380 | D: JsCast + AsRef + Clone + 'static 381 | { 382 | type ModelMsg = T; 383 | type ViewMsg = T; 384 | type DomNode = D; 385 | 386 | fn update( 387 | &mut self, 388 | msg: &T, 389 | tx_view: &Transmitter, 390 | _sub: &Subscriber, 391 | ) { 392 | tx_view.send(msg); 393 | } 394 | 395 | fn view(&self, tx: Transmitter, rx: Receiver) -> Gizmo { 396 | self(tx, rx) 397 | } 398 | } 399 | -------------------------------------------------------------------------------- /examples/todomvc/src/app.rs: -------------------------------------------------------------------------------- 1 | use mogwai::prelude::*; 2 | use web_sys::HashChangeEvent; 3 | 4 | use super::store; 5 | use super::store::Item; 6 | use super::utils; 7 | 8 | mod item; 9 | use item::{Todo, TodoIn, TodoOut}; 10 | 11 | 12 | #[derive(Clone, Debug, PartialEq)] 13 | pub enum FilterShow { 14 | All, 15 | Completed, 16 | Active, 17 | } 18 | 19 | 20 | #[derive(Clone, Debug)] 21 | pub enum In { 22 | NewTodo(String, bool), 23 | NewTodoInput(HtmlInputElement), 24 | Filter(FilterShow), 25 | CompletionToggleInput(HtmlInputElement), 26 | ChangedCompletion(usize, bool), 27 | ToggleCompleteAll, 28 | TodoListUl(HtmlElement), 29 | Remove(usize), 30 | RemoveCompleted, 31 | } 32 | 33 | 34 | #[derive(Clone)] 35 | pub enum Out { 36 | ClearNewTodoInput, 37 | ShouldShowTodoList(bool), 38 | NumItems(usize), 39 | ShouldShowCompleteButton(bool), 40 | SelectedFilter(FilterShow), 41 | } 42 | 43 | 44 | pub struct App { 45 | next_index: usize, 46 | todos: Vec>, 47 | todo_input: Option, 48 | todo_toggle_input: Option, 49 | todo_list_ul: Option, 50 | has_completed: bool 51 | } 52 | 53 | 54 | impl App { 55 | pub fn new() -> App { 56 | App { 57 | next_index: 0, 58 | todos: vec![], 59 | todo_input: None, 60 | todo_toggle_input: None, 61 | todo_list_ul: None, 62 | has_completed: false 63 | } 64 | } 65 | 66 | fn num_items_left(&self) -> usize { 67 | self.todos.iter().fold(0, |n, todo| { 68 | n + todo.with_state(|t| if t.is_done { 0 } else { 1 }) 69 | }) 70 | } 71 | 72 | fn are_any_complete(&self) -> bool { 73 | for todo in self.todos.iter() { 74 | if todo.with_state(|t| t.is_done) { 75 | return true; 76 | } 77 | } 78 | return false 79 | } 80 | 81 | fn are_all_complete(&self) -> bool { 82 | self.todos.iter().fold(true, |complete, todo| { 83 | complete && todo.with_state(|t| t.is_done) 84 | }) 85 | } 86 | 87 | fn items(&self) -> Vec { 88 | self 89 | .todos 90 | .iter() 91 | .map(|component| { 92 | component.with_state(|todo| Item { 93 | title: todo.name.clone(), 94 | completed: todo.is_done, 95 | }) 96 | }) 97 | .collect() 98 | } 99 | 100 | pub fn url_to_filter_msg(url: String) -> Option { 101 | let ndx = url.find('#').unwrap_or(0); 102 | let (_, hash) = url.split_at(ndx); 103 | match hash { 104 | "#/" => Some(In::Filter(FilterShow::All)), 105 | "#/active" => Some(In::Filter(FilterShow::Active)), 106 | "#/completed" => Some(In::Filter(FilterShow::Completed)), 107 | _ => None, 108 | } 109 | } 110 | 111 | fn filter_selected(msg: &Out, show: FilterShow) -> Option { 112 | match msg { 113 | Out::SelectedFilter(msg_show) => Some( 114 | if *msg_show == show { 115 | "selected".to_string() 116 | } else { 117 | "".to_string() 118 | }, 119 | ), 120 | _ => None, 121 | } 122 | } 123 | 124 | fn maybe_update_completed(&mut self, tx: &Transmitter) { 125 | let has_completed = self.are_any_complete(); 126 | if self.has_completed != has_completed { 127 | self.has_completed = has_completed; 128 | tx.send(&Out::ShouldShowCompleteButton(self.are_any_complete())); 129 | } 130 | } 131 | } 132 | 133 | 134 | impl Component for App { 135 | type ModelMsg = In; 136 | type ViewMsg = Out; 137 | type DomNode = HtmlElement; 138 | 139 | fn update( 140 | &mut self, 141 | msg: &In, 142 | tx_view: &Transmitter, 143 | sub: &Subscriber, 144 | ) { 145 | match msg { 146 | In::NewTodo(name, complete) => { 147 | let index = self.next_index; 148 | // Turn the new todo into a sub-component. 149 | let mut component = Todo::new(index, name.to_string()).into_component(); 150 | // Subscribe to some of its view messages 151 | sub.subscribe_filter_map(&component.recv, move |todo_out_msg| { 152 | match todo_out_msg { 153 | TodoOut::UpdateEditComplete(_, is_complete) => { 154 | Some(In::ChangedCompletion(index, *is_complete)) 155 | } 156 | TodoOut::Remove => Some(In::Remove(index)), 157 | _ => None, 158 | } 159 | }); 160 | if *complete { 161 | component.update(&TodoIn::SetCompletion(true)); 162 | } 163 | // If we have a ul, add the component to it. 164 | if let Some(ul) = self.todo_list_ul.as_ref() { 165 | let _ = ul.append_child(&component); 166 | } 167 | self.todos.push(component); 168 | self.next_index += 1; 169 | 170 | tx_view.send(&Out::ClearNewTodoInput); 171 | tx_view.send(&Out::NumItems(self.todos.len())); 172 | tx_view.send(&Out::ShouldShowTodoList(true)); 173 | } 174 | In::NewTodoInput(input) => { 175 | self.todo_input = Some(input.clone()); 176 | let input = input.clone(); 177 | timeout(0, move || { 178 | input.focus().expect("focus"); 179 | // Never reschedule the timeout 180 | false 181 | }); 182 | } 183 | In::Filter(show) => { 184 | self.todos.iter_mut().for_each(|component| { 185 | let is_done = component.with_state(|t| t.is_done); 186 | let is_visible = *show == FilterShow::All 187 | || (*show == FilterShow::Completed && is_done) 188 | || (*show == FilterShow::Active && !is_done); 189 | component.update(&TodoIn::SetVisible(is_visible)); 190 | }); 191 | tx_view.send(&Out::SelectedFilter(show.clone())); 192 | } 193 | In::CompletionToggleInput(input) => { 194 | self.todo_toggle_input = Some(input.clone()); 195 | self.maybe_update_completed(tx_view); 196 | } 197 | In::ChangedCompletion(_index, _is_complete) => { 198 | let items_left = self.num_items_left(); 199 | self 200 | .todo_toggle_input 201 | .iter() 202 | .for_each(|input| input.set_checked(items_left == 0)); 203 | tx_view.send(&Out::NumItems(items_left)); 204 | self.maybe_update_completed(tx_view); 205 | } 206 | In::ToggleCompleteAll => { 207 | let input = self.todo_toggle_input.as_ref().expect("toggle input"); 208 | 209 | let should_complete = input.checked(); 210 | for todo in self.todos.iter_mut() { 211 | todo.update(&TodoIn::SetCompletion(should_complete)); 212 | } 213 | } 214 | In::TodoListUl(ul) => { 215 | self.todo_list_ul = Some(ul.clone()); 216 | // If we have todos already created (from local storage), add them to 217 | // the ul. 218 | self 219 | .todos 220 | .iter() 221 | .for_each(|component| { 222 | let _ = ul.append_child(&component); 223 | }); 224 | } 225 | In::Remove(index) => { 226 | // Removing the gizmo drops its shared state, transmitters and receivers. 227 | // This causes its Drop implementation to run, which removes its 228 | // html_element from the parent. 229 | self.todos.retain(|todo| { 230 | let keep = todo.with_state(|t| t.index != *index); 231 | if !keep { 232 | if let Some(parent) = todo.dom_ref().parent_element() { 233 | let _ = parent.remove_child(todo.dom_ref()); 234 | } 235 | } 236 | keep 237 | }); 238 | 239 | if self.todos.len() == 0 { 240 | // Update the toggle input checked state by hand 241 | if let Some(input) = self.todo_toggle_input.as_ref() { 242 | input.set_checked(!self.are_all_complete()); 243 | } 244 | tx_view.send(&Out::ShouldShowTodoList(false)); 245 | } 246 | tx_view.send(&Out::NumItems(self.num_items_left())); 247 | self.maybe_update_completed(tx_view); 248 | } 249 | In::RemoveCompleted => { 250 | let num_items_before = self.todos.len(); 251 | self.todos.retain(|todo| todo.with_state(|t| !t.is_done)); 252 | self 253 | .todo_toggle_input 254 | .iter() 255 | .for_each(|input| input.set_checked(!self.are_all_complete())); 256 | tx_view.send(&Out::NumItems(self.num_items_left())); 257 | self.maybe_update_completed(tx_view); 258 | if self.todos.len() == 0 && num_items_before != 0 { 259 | tx_view.send(&Out::ShouldShowTodoList(false)); 260 | } 261 | } 262 | }; 263 | 264 | // In any case, serialize the current todo items. 265 | let items = self.items(); 266 | store::write_items(items).expect("Could not store todos"); 267 | } 268 | 269 | fn view(&self, tx: Transmitter, rx: Receiver) -> Gizmo { 270 | let rx_display = 271 | rx.branch_filter_map(|msg| match msg { 272 | Out::ShouldShowTodoList(should) => { 273 | Some(if *should { "block" } else { "none" }.to_string()) 274 | } 275 | _ => None, 276 | }); 277 | 278 | section() 279 | .class("todoapp") 280 | .with( 281 | header().class("header").with(h1().text("todos")).with( 282 | input() 283 | .class("new-todo") 284 | .attribute("id", "new-todo") 285 | .attribute("placeholder", "What needs to be done?") 286 | .tx_on( 287 | "change", 288 | tx.contra_filter_map(|ev: &Event| { 289 | let todo_name = 290 | utils::event_input_value(ev).expect("event input value"); 291 | if todo_name.is_empty() { 292 | None 293 | } else { 294 | Some(In::NewTodo(todo_name, false)) 295 | } 296 | }), 297 | ) 298 | .rx_value( 299 | "", 300 | rx.branch_filter_map(|msg| match msg { 301 | Out::ClearNewTodoInput => Some("".to_string()), 302 | _ => None, 303 | }), 304 | ) 305 | .tx_post_build(tx.contra_map(|el: &HtmlInputElement| { 306 | In::NewTodoInput(el.clone()) 307 | })), 308 | ), 309 | ) 310 | .with( 311 | section() 312 | .class("main") 313 | .rx_style("display", "none", rx_display.branch()) 314 | .with( 315 | // This is the "check all as complete" toggle 316 | input() 317 | .attribute("id", "toggle-all") 318 | .attribute("type", "checkbox") 319 | .class("toggle-all") 320 | .tx_post_build(tx.contra_map(|el: &HtmlInputElement| { 321 | In::CompletionToggleInput(el.clone()) 322 | })) 323 | .tx_on("click", tx.contra_map(|_| In::ToggleCompleteAll)), 324 | ) 325 | .with( 326 | label() 327 | .attribute("for", "toggle-all") 328 | .text("Mark all as complete"), 329 | ) 330 | .with( 331 | ul() 332 | .class("todo-list") 333 | .rx_style("display", "none", rx_display.branch()) 334 | .tx_post_build( 335 | tx.contra_map(|el: &HtmlElement| In::TodoListUl(el.clone())), 336 | ), 337 | ), 338 | ) 339 | .with( 340 | footer() 341 | .class("footer") 342 | .rx_style("display", "none", rx_display) 343 | .with(span().class("todo-count").with(strong().rx_text( 344 | "0 items left", 345 | rx.branch_filter_map(|msg| match msg { 346 | Out::NumItems(n) => { 347 | let items = if *n == 1 { "item" } else { "items" }; 348 | Some(format!("{} {} left", n, items)) 349 | } 350 | _ => None, 351 | }), 352 | ))) 353 | .with( 354 | ul() 355 | .class("filters") 356 | .with( 357 | li().with( 358 | a() 359 | .rx_class( 360 | "", 361 | rx.branch_filter_map(|msg| { 362 | App::filter_selected(msg, FilterShow::All) 363 | }), 364 | ) 365 | .attribute("href", "#/") 366 | .text("All"), 367 | ), 368 | ) 369 | .with( 370 | li().with( 371 | a() 372 | .rx_class( 373 | "", 374 | rx.branch_filter_map(|msg| { 375 | App::filter_selected(msg, FilterShow::Active) 376 | }), 377 | ) 378 | .attribute("href", "#/active") 379 | .text("Active"), 380 | ), 381 | ) 382 | .with( 383 | li().with( 384 | a() 385 | .rx_class( 386 | "", 387 | rx.branch_filter_map(|msg| { 388 | App::filter_selected(msg, FilterShow::Completed) 389 | }), 390 | ) 391 | .attribute("href", "#/completed") 392 | .text("Completed"), 393 | ), 394 | ) 395 | .tx_on_window( 396 | "hashchange", 397 | tx.contra_filter_map(|ev: &Event| { 398 | let ev: &HashChangeEvent = 399 | ev 400 | .dyn_ref::() 401 | .expect("not hash event"); 402 | let url = ev.new_url(); 403 | App::url_to_filter_msg(url) 404 | }), 405 | ), 406 | ) 407 | .with( 408 | button() 409 | .class("clear-completed") 410 | .text("Clear completed") 411 | .rx_style( 412 | "display", 413 | "none", 414 | rx.branch_filter_map(|msg| match msg { 415 | Out::ShouldShowCompleteButton(should) => { 416 | Some(if *should { "block" } else { "none" }.to_string()) 417 | } 418 | _ => None, 419 | }), 420 | ) 421 | .tx_on("click", tx.contra_map(|_: &Event| In::RemoveCompleted)), 422 | ), 423 | ) 424 | } 425 | } 426 | -------------------------------------------------------------------------------- /mogwai/src/gizmo.rs: -------------------------------------------------------------------------------- 1 | //! A widget. 2 | use std::cell::RefCell; 3 | use std::collections::HashMap; 4 | use std::marker::PhantomData; 5 | use std::ops::Deref; 6 | use std::rc::Rc; 7 | use wasm_bindgen::closure::Closure; 8 | use web_sys::{HtmlElement, Node, Text}; 9 | 10 | //use super::builder::Gizmo; 11 | use super::txrx::{hand_clone, Receiver, Transmitter}; 12 | pub use super::utils; 13 | pub use wasm_bindgen::{JsCast, JsValue, UnwrapThrowExt}; 14 | pub use web_sys::{Element, Event, EventTarget, HtmlInputElement}; 15 | 16 | pub mod html; 17 | 18 | 19 | /// A widget that may contain a bundled network of html elements, callback 20 | /// closures and receivers. 21 | pub struct Gizmo { 22 | pub(crate) element: Rc, 23 | pub(crate) phantom: PhantomData, 24 | pub(crate) callbacks: HashMap>>, 25 | pub(crate) window_callbacks: 26 | HashMap>>, 27 | pub(crate) document_callbacks: 28 | HashMap>>, 29 | pub(crate) opt_string_rxs: Vec>>, 30 | pub(crate) string_rxs: Vec>, 31 | pub(crate) bool_rxs: Vec>, 32 | //pub(crate) gizmo_rxs: Vec>>>, 33 | pub(crate) static_gizmos: Vec>, 34 | } 35 | 36 | 37 | impl Clone for Gizmo { 38 | fn clone(&self) -> Self { 39 | self.clone_as() 40 | } 41 | } 42 | 43 | 44 | impl AsRef for Gizmo 45 | where 46 | T: JsCast + AsRef, 47 | S: JsCast, 48 | { 49 | fn as_ref(&self) -> &S { 50 | self.element.unchecked_ref::() 51 | } 52 | } 53 | 54 | 55 | impl Deref for Gizmo { 56 | type Target = T; 57 | 58 | fn deref(&self) -> &T { 59 | self.element.unchecked_ref::() 60 | } 61 | } 62 | 63 | 64 | impl> Gizmo { 65 | /// Sends an event into the given transmitter when the given dom event happens. 66 | pub fn tx_on(mut self, ev_name: &str, tx: Transmitter) -> Gizmo { 67 | let target: &EventTarget = self.as_ref(); 68 | let cb = utils::add_event(ev_name, target, tx); 69 | self.callbacks.insert(ev_name.to_string(), cb); 70 | self 71 | } 72 | 73 | 74 | /// Sends an event into the given transmitter when the given dom event happens 75 | /// on `window`. 76 | pub fn tx_on_window( 77 | mut self, 78 | ev_name: &str, 79 | tx: Transmitter, 80 | ) -> Gizmo { 81 | let window = utils::window(); 82 | let target: &EventTarget = window.as_ref(); 83 | let cb = utils::add_event(ev_name, &target, tx); 84 | self.window_callbacks.insert(ev_name.to_string(), cb); 85 | self 86 | } 87 | 88 | /// Sends an event into the given transmitter when the given dom event happens. 89 | pub fn tx_on_document( 90 | mut self, 91 | ev_name: &str, 92 | tx: Transmitter, 93 | ) -> Gizmo { 94 | let doc = utils::document(); 95 | let target: &EventTarget = doc.as_ref(); 96 | let cb = utils::add_event(ev_name, target, tx); 97 | self.document_callbacks.insert(ev_name.to_string(), cb); 98 | self 99 | } 100 | } 101 | 102 | 103 | /// Anything that can be nested within a Gizmo. 104 | pub trait SubGizmo 105 | where 106 | Self: Sized, 107 | { 108 | /// Attempts to turn the Gizmo into a child gizmo who's inner element is a 109 | /// Node, if possible. Otherwise this will return a Node. 110 | fn into_sub_gizmo(self) -> Result, Node>; 111 | } 112 | 113 | 114 | impl + Clone> SubGizmo for Gizmo { 115 | fn into_sub_gizmo(self) -> Result, Node> { 116 | Ok(self.upcast()) 117 | } 118 | } 119 | 120 | 121 | impl + Clone> SubGizmo for &Gizmo { 122 | fn into_sub_gizmo(self) -> Result, Node> { 123 | let node: &Node = self.as_ref(); 124 | Err(node.clone()) 125 | } 126 | } 127 | 128 | 129 | impl> Gizmo { 130 | /// Create a text node and insert it into the Gizmo being built. 131 | pub fn text(self, s: &str) -> Gizmo { 132 | let text: Text = Text::new_with_data(s).unwrap_throw(); 133 | (self.as_ref() as &Node) 134 | .append_child(text.as_ref()) 135 | .expect("Could not add text node to gizmo element"); 136 | self 137 | } 138 | 139 | /// Create a text node that changes its text to anything that is sent on the 140 | /// given receiver and insert it into the Gizmo being built. 141 | pub fn rx_text(mut self, init: &str, rx: Receiver) -> Gizmo { 142 | // Save a clone so we can drop_responder if this gizmo goes out of scope 143 | self.string_rxs.push(hand_clone(&rx)); 144 | 145 | let text: Text = Text::new_with_data(init).unwrap_throw(); 146 | 147 | (self.as_ref() as &Node) 148 | .append_child(text.as_ref()) 149 | .expect("Could not add text node to gizmo"); 150 | rx.respond(move |s| { 151 | text.set_data(s); 152 | }); 153 | self 154 | } 155 | 156 | /// Append a child that implements SubGizmo to this Gizmo. 157 | pub fn with(mut self, child: Child) -> Gizmo 158 | where 159 | Child: SubGizmo, 160 | { 161 | // If this thing is a gizmo then store it and append it, otherwise just 162 | // append it 163 | match child.into_sub_gizmo() { 164 | Ok(gizmo) => { 165 | (self.as_ref() as &Node) 166 | .append_child(&gizmo) 167 | .expect("Gizmo::with could not append gizmo"); 168 | self.static_gizmos.push(gizmo); 169 | } 170 | Err(node) => { 171 | (self.as_ref() as &Node) 172 | .append_child(&node) 173 | .expect("Gizmo::with could not append node"); 174 | } 175 | } 176 | 177 | self 178 | } 179 | } 180 | 181 | 182 | impl> Gizmo { 183 | /// Create a static attribute on the Gizmo being built. 184 | pub fn attribute(self, name: &str, value: &str) -> Gizmo { 185 | (self.as_ref() as &Element) 186 | .set_attribute(name, value) 187 | .unwrap_throw(); 188 | self 189 | } 190 | 191 | /// If `condition` is `true`, create a static boolean attribute on the Gizmo 192 | /// being built. 193 | /// 194 | /// For background on `condition` See 195 | /// https://github.com/schell/mogwai/issues/19 196 | pub fn boolean_attribute(self, name: &str, condition: bool) -> Gizmo { 197 | if condition { 198 | (self.as_ref() as &Element) 199 | .set_attribute(name, "") 200 | .unwrap_throw(); 201 | self 202 | } else { 203 | self 204 | } 205 | } 206 | 207 | /// Create a class attribute on the Gizmo being built. 208 | /// 209 | /// This represents all the classes for this gizmo. If you'd like to specify 210 | /// more than one class call this as: 211 | /// ```rust, no_run 212 | /// extern crate mogwai; 213 | /// use mogwai::prelude::*; 214 | /// 215 | /// let gizmo_the_long_way = 216 | /// Gizmo::element("div") 217 | /// .downcast::().ok().unwrap() 218 | /// .class("class1 class2 class3 etc"); 219 | /// 220 | /// let gizmo = 221 | /// div() 222 | /// .class("class1 class2 class3 etc"); 223 | /// ``` 224 | pub fn class(self, value: &str) -> Gizmo { 225 | self.attribute("class", value) 226 | } 227 | 228 | /// Create an id attribute on the Gizmo being built. 229 | pub fn id(self, value: &str) -> Gizmo { 230 | self.attribute("id", value) 231 | } 232 | 233 | /// Create an attribute on the Gizmo being built that changes its value every 234 | /// time the given receiver receives a message. If the receiver receives `None` 235 | /// it will respond by removing the attribute until it receives `Some(...)`. 236 | pub fn rx_attribute( 237 | mut self, 238 | name: &str, 239 | init: Option<&str>, 240 | rx: Receiver>, 241 | ) -> Gizmo { 242 | // Save a clone so we can drop_responder if this gizmo goes out of scope 243 | self.opt_string_rxs.push(hand_clone(&rx)); 244 | 245 | let element: &Element = self.as_ref(); 246 | if let Some(init) = init { 247 | element 248 | .set_attribute(name, &init) 249 | .expect("Could not set attribute"); 250 | } 251 | 252 | let element = element.clone(); 253 | let name = name.to_string(); 254 | 255 | rx.respond(move |s| { 256 | if let Some(s) = s { 257 | element 258 | .set_attribute(&name, s) 259 | .expect("Could not set attribute"); 260 | } else { 261 | element 262 | .remove_attribute(&name) 263 | .expect("Could not remove attribute"); 264 | } 265 | }); 266 | 267 | self 268 | } 269 | 270 | /// Create a boolean attribute on the Gizmo being built that changes its value every 271 | /// time the given receiver receives a message. If the receiver receives `None` 272 | /// it will respond by removing the attribute until it receives `Some(...)`. 273 | pub fn rx_boolean_attribute( 274 | mut self, 275 | name: &str, 276 | init: bool, 277 | rx: Receiver, 278 | ) -> Gizmo { 279 | // Save a clone so we can drop_responder if this gizmo goes out of scope 280 | self.bool_rxs.push(hand_clone(&rx)); 281 | 282 | let element: &Element = self.as_ref(); 283 | 284 | if init { 285 | element 286 | .set_attribute(name, "") 287 | .expect("Could not set attribute"); 288 | } 289 | 290 | let element = element.clone(); 291 | let name = name.to_string(); 292 | 293 | rx.respond(move |b| { 294 | if *b { 295 | element 296 | .set_attribute(&name, "") 297 | .expect("Could not set boolean attribute"); 298 | } else { 299 | element 300 | .remove_attribute(&name) 301 | .expect("Could not remove boolean attribute") 302 | } 303 | }); 304 | 305 | self 306 | } 307 | 308 | 309 | /// Create a class attribute on the Gizmo being built that changes its value 310 | /// every time the given receiver receives a message. 311 | pub fn rx_class(self, init: &str, rx: Receiver) -> Gizmo { 312 | self.rx_attribute("class", Some(init), rx.branch_map(|s| Some(s.clone()))) 313 | } 314 | } 315 | 316 | 317 | impl> Gizmo { 318 | /// Set a CSS property in the style attribute of the Gizmo being built. 319 | pub fn style(self, name: &str, value: &str) -> Gizmo { 320 | (self.as_ref() as &HtmlElement) 321 | .style() 322 | .set_property(name, value) 323 | .unwrap_throw(); 324 | self 325 | } 326 | 327 | /// Set a CSS property in the style attribute of the Gizmo being built that 328 | /// updates its value every time a message is received on the given `Receiver`. 329 | pub fn rx_style( 330 | mut self, 331 | s: &str, 332 | init: &str, 333 | rx: Receiver, 334 | ) -> Gizmo { 335 | // Save a clone so we can drop_responder if this gizmo goes out of scope 336 | self.string_rxs.push(hand_clone(&rx)); 337 | 338 | let element: &HtmlElement = self.as_ref(); 339 | let style = element.style(); 340 | let name = s.to_string(); 341 | 342 | style 343 | .set_property(&name, init) 344 | .expect("Could not set initial style property"); 345 | 346 | rx.respond(move |s| { 347 | style.set_property(&name, s).expect("Could not set style"); 348 | }); 349 | 350 | self 351 | } 352 | } 353 | 354 | 355 | impl> Gizmo { 356 | /// Set the value of the Gizmo input. 357 | pub fn value(self, s: &str) -> Gizmo { 358 | let input: &HtmlInputElement = self.as_ref(); 359 | input.set_value(s); 360 | self 361 | } 362 | 363 | /// Set the value of the Gizmo input that updates every time a message is 364 | /// received on the given Receiver. 365 | pub fn rx_value(mut self, init: &str, rx: Receiver) -> Gizmo { 366 | // Save a clone so we can drop_responder if this gizmo goes out of scope 367 | self.string_rxs.push(hand_clone(&rx)); 368 | let input: &HtmlInputElement = self.as_ref(); 369 | input.set_value(init); 370 | 371 | let input = input.clone(); 372 | rx.respond(move |val: &String| { 373 | input.set_value(val); 374 | }); 375 | 376 | self 377 | } 378 | 379 | /// Set the checked value of the Gizmo input. 380 | pub fn checked(self, checked:bool) -> Gizmo { 381 | let input: &HtmlInputElement = self.as_ref(); 382 | input.set_checked(checked); 383 | self 384 | } 385 | 386 | /// Set the value of the Gizmo input that updates every time a message is 387 | /// received on the given Receiver. 388 | pub fn rx_checked(mut self, checked: bool, rx: Receiver) -> Gizmo { 389 | // Save a clone so we can drop_responder if this gizmo goes out of scope 390 | self.bool_rxs.push(hand_clone(&rx)); 391 | let input: &HtmlInputElement = self.as_ref(); 392 | input.set_checked(checked); 393 | 394 | let input = input.clone(); 395 | rx.respond(move |val: &bool| { 396 | input.set_checked(*val); 397 | }); 398 | 399 | self 400 | } 401 | } 402 | 403 | 404 | impl Gizmo { 405 | /// Create a new `Gizmo` wrapping a `T` that can be dereferenced to a `Node`. 406 | pub fn wrapping(element: T) -> Gizmo { 407 | Gizmo { 408 | element: Rc::new(element.into()), 409 | phantom: PhantomData, 410 | callbacks: HashMap::new(), 411 | window_callbacks: HashMap::new(), 412 | document_callbacks: HashMap::new(), 413 | opt_string_rxs: vec![], 414 | string_rxs: vec![], 415 | //gizmo_rxs: vec![], 416 | bool_rxs: vec![], 417 | static_gizmos: vec![], 418 | } 419 | } 420 | } 421 | 422 | 423 | impl Gizmo { 424 | /// Creates a new gizmo with data cloned from the first, but with a null 425 | /// element. 426 | fn clone_as(&self) -> Gizmo 427 | where 428 | D: JsCast, 429 | { 430 | Gizmo { 431 | element: self.element.clone(), 432 | phantom: PhantomData, 433 | callbacks: self.callbacks.clone(), 434 | window_callbacks: self.window_callbacks.clone(), 435 | document_callbacks: self.document_callbacks.clone(), 436 | opt_string_rxs: self 437 | .opt_string_rxs 438 | .iter() 439 | .map(|rx| hand_clone(rx)) 440 | .collect(), 441 | string_rxs: self.string_rxs.iter().map(|rx| hand_clone(rx)).collect(), 442 | bool_rxs: self.bool_rxs.iter().map(|rx| hand_clone(rx)).collect(), 443 | //gizmo_rxs: self.gizmo_rxs.iter().map(|rx| hand_clone(rx)).collect(), 444 | static_gizmos: self.static_gizmos.clone(), 445 | } 446 | } 447 | 448 | /// Cast the given Gizmo to contain the inner DOM node of another type. 449 | /// That type must be dereferencable from the given Gizmo. 450 | pub fn upcast(self) -> Gizmo 451 | where 452 | T: AsRef, 453 | D: JsCast, 454 | { 455 | self.clone_as() 456 | } 457 | 458 | /// Attempt to downcast the inner element. 459 | pub fn downcast>( 460 | self, 461 | ) -> Result, Gizmo> { 462 | if self.element.has_type::() { 463 | Ok(self.clone_as()) 464 | } else { 465 | Err(self) 466 | } 467 | } 468 | } 469 | 470 | 471 | impl Gizmo { 472 | /// Run this gizmo forever without appending it to anything. 473 | pub fn forget(self) -> Result<(), JsValue> { 474 | if cfg!(target_arch = "wasm32") { 475 | let gizmo_var = RefCell::new(self); 476 | utils::timeout(1000, move || { 477 | gizmo_var.borrow_mut(); 478 | true 479 | }); 480 | Ok(()) 481 | } else { 482 | Err("forgetting and running a gizmo is only supported on wasm".into()) 483 | } 484 | } 485 | } 486 | 487 | 488 | impl + Clone + 'static> Gizmo { 489 | /// Run this gizmo in a parent container forever, never dropping it. 490 | pub fn run_in_container(self, container: &Node) -> Result<(), JsValue> { 491 | if cfg!(target_arch = "wasm32") { 492 | let _ = container.append_child(&self.as_ref()); 493 | self.forget() 494 | } else { 495 | Err("running gizmos is only supported on wasm".into()) 496 | } 497 | } 498 | 499 | /// Run this gizmo in the document body forever, never dropping it. 500 | pub fn run(self) -> Result<(), JsValue> { 501 | if cfg!(target_arch = "wasm32") { 502 | self.run_in_container(&utils::body()) 503 | } else { 504 | Err("running gizmos is only supported on wasm".into()) 505 | } 506 | } 507 | 508 | /// After the gizmo is built, send a clone of T on the given transmitter. 509 | /// This allows you to construct component behaviors that operate on the T 510 | /// directly, while still keeping the Gizmo in its place within your view 511 | /// function. For example, you may want to use `input.focus()` within the 512 | /// `update` function of your component. This method allows you to store the 513 | /// input's `HtmlInputElement` once it is built. 514 | pub fn tx_post_build(self, tx: Transmitter) -> Gizmo { 515 | let t: &T = self.element.unchecked_ref(); 516 | let t: T = t.clone(); 517 | tx.send_async(async move { t }); 518 | self 519 | } 520 | } 521 | 522 | 523 | impl Gizmo { 524 | /// Create a new gizmo with the given element tag. 525 | /// ```rust,ignore 526 | /// Gizmo::element("div") 527 | /// ``` 528 | pub fn element(tag: &str) -> Self { 529 | let element: Element = utils::document().create_element(tag).unwrap_throw(); 530 | Gizmo::wrapping(element) 531 | } 532 | 533 | 534 | /// Create a new gizmo with the given element tag. 535 | /// ```rust,ignore 536 | /// Gizmo::element("div") 537 | /// ``` 538 | pub fn element_ns(tag: &str, ns: &str) -> Self { 539 | let element: Element = 540 | utils::document() 541 | .create_element_ns(Some(ns), tag) 542 | .unwrap_throw(); 543 | Gizmo::wrapping(element) 544 | } 545 | 546 | 547 | /// Create a new Gizmo from an existing Element with the given id. 548 | /// Returns None if it cannot be found. 549 | pub fn from_element_by_id(id: &str) -> Option> { 550 | let el = utils::document().get_element_by_id(id)?; 551 | Some(Gizmo::wrapping(el)) 552 | } 553 | } 554 | 555 | 556 | /// Gizmo's Drop implementation insures that responders no longer attempt to 557 | /// update the gizmo. It also removes its element from the DOM. 558 | impl Drop for Gizmo { 559 | fn drop(&mut self) { 560 | let count = Rc::strong_count(&self.element); 561 | if count <= 1 { 562 | let node:&Node = self.element.unchecked_ref(); 563 | if let Some(parent) = node.parent_node() { 564 | let _ = parent.remove_child(&node); 565 | } 566 | 567 | self 568 | .opt_string_rxs 569 | .iter_mut() 570 | .for_each(|rx| rx.drop_responder()); 571 | 572 | self 573 | .string_rxs 574 | .iter_mut() 575 | .for_each(|rx| rx.drop_responder()); 576 | 577 | self.bool_rxs.iter_mut().for_each(|rx| rx.drop_responder()); 578 | } 579 | } 580 | } 581 | 582 | 583 | #[cfg(test)] 584 | mod gizmo_tests { 585 | use super::html::{div, pre}; 586 | use super::SubGizmo; 587 | use wasm_bindgen::JsCast; 588 | use wasm_bindgen_test::*; 589 | use web_sys::{console, Element}; 590 | 591 | wasm_bindgen_test_configure!(run_in_browser); 592 | 593 | #[wasm_bindgen_test] 594 | fn can_into_sub_gizmo() { 595 | let tag = div().id("sub-gizmo"); 596 | let ref_res = (&tag).into_sub_gizmo(); 597 | assert!( 598 | ref_res.is_err(), 599 | "gizmo reference does not sub-gizmo into Err(Node)" 600 | ); 601 | 602 | let self_res = tag.into_sub_gizmo(); 603 | assert!( 604 | self_res.is_ok(), 605 | "gizmo does not sub-gizmo into Ok(Gizmo)" 606 | ); 607 | console::log_1(&"dropping sub-gizmo".into()); 608 | } 609 | 610 | #[wasm_bindgen_test] 611 | fn gizmo_ref_as_child() { 612 | // Since the pre tag is dropped after the scope block the last assert should 613 | // show that the div tag has no children. 614 | let div = 615 | { 616 | let pre = pre().text("this has text"); 617 | let div = div().id("parent").with(&pre); 618 | assert!( 619 | div.first_child().is_some(), 620 | "parent does not contain in-scope child" 621 | ); 622 | console::log_1(&"dropping pre".into()); 623 | div 624 | }; 625 | assert!( 626 | div.first_child().is_none(), 627 | "parent does not maintain out-of-scope child" 628 | ); 629 | console::log_1(&"dropping parent".into()); 630 | } 631 | 632 | #[wasm_bindgen_test] 633 | fn gizmo_as_child() { 634 | // Since the pre tag is *not* dropped after the scope block the last assert 635 | // should show that the div tag has a child. 636 | let div = 637 | { 638 | let pre = pre().text("some text"); 639 | let div = div().id("parent-div").with(pre); 640 | assert!(div.first_child().is_some(), "could not add child gizmo"); 641 | div 642 | }; 643 | assert!( 644 | div.first_child().is_some(), 645 | "could not keep hold of child gizmo" 646 | ); 647 | assert_eq!(div.static_gizmos.len(), 1, "parent is missing static_gizmo"); 648 | console::log_1(&"dropping div and pre".into()); 649 | } 650 | 651 | #[wasm_bindgen_test] 652 | fn gizmo_tree() { 653 | let root = 654 | { 655 | let leaf = pre().id("leaf").text("leaf"); 656 | let branch = div().id("branch").with(leaf); 657 | let root = div().id("root").with(branch); 658 | root 659 | }; 660 | if let Some(branch) = root.first_child() { 661 | if let Some(leaf) = branch.first_child() { 662 | if let Some(leaf) = leaf.dyn_ref::() { 663 | assert_eq!(leaf.id(), "leaf"); 664 | } else { 665 | panic!("leaf is not an Element"); 666 | } 667 | } else { 668 | panic!("branch has no leaf"); 669 | } 670 | } else { 671 | panic!("root has no branch"); 672 | } 673 | } 674 | } 675 | -------------------------------------------------------------------------------- /mogwai/src/txrx.rs: -------------------------------------------------------------------------------- 1 | //! # Instant channels. 2 | //! 3 | //! Just add water! ;) 4 | //! 5 | //! ## Creating channels 6 | //! There are a number of ways to create a channel in this module. The most 7 | //! straight forward is to use the function [txrx]. This will create a linked 8 | //! [Transmitter] + [Receiver] pair: 9 | //! 10 | //! ```rust 11 | //! extern crate mogwai; 12 | //! use mogwai::prelude::*; 13 | //! 14 | //! let (tx, rx):(Transmitter<()>, Receiver<()>) = txrx(); 15 | //! ``` 16 | //! 17 | //! Or maybe you prefer an alternative syntax: 18 | //! 19 | //! ```rust 20 | //! extern crate mogwai; 21 | //! use mogwai::prelude::*; 22 | //! 23 | //! let (tx, rx) = txrx::<()>(); 24 | //! ``` 25 | //! 26 | //! Or simply let the compiler try to figure it out: 27 | //! 28 | //! ```rust, ignore 29 | //! extern crate mogwai; 30 | //! use mogwai::prelude::*; 31 | //! 32 | //! let (tx, rx) = txrx(); 33 | //! // ... 34 | //! ``` 35 | //! 36 | //! This pair makes a linked channel. Messages you send on the [Transmitter] 37 | //! will be sent directly to the [Receiver] on the other end. 38 | //! 39 | //! You can create separate terminals using the [trns] and [recv] functions. Then 40 | //! later in your code you can spawn new linked partners from them: 41 | //! 42 | //! ```rust 43 | //! extern crate mogwai; 44 | //! use mogwai::prelude::*; 45 | //! 46 | //! let mut tx = trns(); 47 | //! let rx = tx.spawn_recv(); 48 | //! tx.send(&()); // rx will receive the message 49 | //! ``` 50 | //! ```rust 51 | //! extern crate mogwai; 52 | //! use mogwai::prelude::*; 53 | //! 54 | //! let rx = recv(); 55 | //! let tx = rx.new_trns(); 56 | //! tx.send(&()); // rx will receive the message 57 | //! ``` 58 | //! 59 | //! Note that [Transmitter::spawn_recv] mutates the transmitter its called on, 60 | //! while [Receiver::new_trns] requires no such mutation. 61 | //! 62 | //! ## Sending messages 63 | //! 64 | //! Once you have a txrx pair you can start sending messages: 65 | //! 66 | //! ```rust 67 | //! extern crate mogwai; 68 | //! use mogwai::prelude::*; 69 | //! 70 | //! let (tx, rx) = txrx(); 71 | //! tx.send(&()); 72 | //! tx.send(&()); 73 | //! tx.send(&()); 74 | //! ``` 75 | //! 76 | //! Notice that we send references. This is because neither the transmitter nor 77 | //! the receiver own the messages. 78 | //! 79 | //! It's also possible to send asynchronous messages! We can do this with 80 | //! [Transmitter::send_async], which takes a type that implements [Future]. Here 81 | //! is an example of running an async web request to send some text from an 82 | //! `async` block: 83 | //! 84 | //! ```rust, no_run 85 | //! extern crate mogwai; 86 | //! extern crate web_sys; 87 | //! use mogwai::prelude::*; 88 | //! use web_sys::{Request, RequestMode, RequestInit, Response}; 89 | //! 90 | //! // Here's our async function that fetches a text response from a server, 91 | //! // or returns an error string. 92 | //! async fn request_to_text(req:Request) -> Result { 93 | //! let resp:Response = 94 | //! JsFuture::from( 95 | //! window() 96 | //! .fetch_with_request(&req) 97 | //! ) 98 | //! .await 99 | //! .map_err(|_| "request failed".to_string())? 100 | //! .dyn_into() 101 | //! .map_err(|_| "response is malformed")?; 102 | //! let text:String = 103 | //! JsFuture::from( 104 | //! resp 105 | //! .text() 106 | //! .map_err(|_| "could not get response text")? 107 | //! ) 108 | //! .await 109 | //! .map_err(|_| "getting text failed")? 110 | //! .as_string() 111 | //! .ok_or("couldn't get text as string".to_string())?; 112 | //! Ok(text) 113 | //! } 114 | //! 115 | //! let (tx, rx) = txrx(); 116 | //! tx.send_async(async { 117 | //! let mut opts = RequestInit::new(); 118 | //! opts.method("GET"); 119 | //! opts.mode(RequestMode::Cors); 120 | //! 121 | //! let req = 122 | //! Request::new_with_str_and_init( 123 | //! "https://worldtimeapi.org/api/timezone/Europe/London.txt", 124 | //! &opts 125 | //! ) 126 | //! .unwrap_throw(); 127 | //! 128 | //! request_to_text(req) 129 | //! .await 130 | //! .unwrap_or_else(|e| e) 131 | //! }); 132 | //! ``` 133 | //! 134 | //! ## Responding to messages 135 | //! 136 | //! [Receiver]s can respond immediately to the messages that are sent to them. 137 | //! There is no polling and no internal message buffer. These channels are 138 | //! instant! Receivers do this by invoking their response function when they 139 | //! receive a message. The response function can be set using 140 | //! [Receiver::respond]: 141 | //! 142 | //! ```rust 143 | //! extern crate mogwai; 144 | //! use mogwai::prelude::*; 145 | //! 146 | //! let (tx, rx) = txrx(); 147 | //! rx.respond(|&()| { 148 | //! println!("Message received!"); 149 | //! }); 150 | //! tx.send(&()); 151 | //! ``` 152 | //! 153 | //! For convenience we also have the [Receiver::respond_shared] method and the 154 | //! [new_shared] function that together allow you to respond using a shared 155 | //! mutable variable. Inside your fold function you can simply mutate this shared 156 | //! variable as normal. This makes it easy to encapsulate a little bit of shared 157 | //! state in your responder without requiring much knowledge about thread-safe 158 | //! asynchronous programming: 159 | //! 160 | //! ```rust 161 | //! extern crate mogwai; 162 | //! use mogwai::prelude::*; 163 | //! 164 | //! let shared_count = new_shared(0); 165 | //! let (tx, rx) = txrx(); 166 | //! rx.respond_shared(shared_count.clone(), |count: &mut i32, &()| { 167 | //! *count += 1; 168 | //! println!("{} messages received!", *count); 169 | //! }); 170 | //! tx.send(&()); 171 | //! tx.send(&()); 172 | //! tx.send(&()); 173 | //! assert_eq!(*shared_count.borrow(), 3); 174 | //! ``` 175 | //! 176 | //! ## Composing channels 177 | //! 178 | //! Sending messages into a transmitter and having it pop out automatically is 179 | //! great, but wait, there's more! What if we have a `tx_a:Transmitter` and a 180 | //! `rx_b:Receiver`, but we want to send `A`s on `tx_a` and have `B`s pop out 181 | //! of `rx_b`? We could use the machinery we have and write something like: 182 | //! 183 | //! ```rust, ignore 184 | //! extern crate mogwai; 185 | //! use mogwai::prelude::*; 186 | //! 187 | //! let (tx_a, rx_b) = { 188 | //! let (tx_a, rx_a) = txrx(); 189 | //! let (tx_b, rx_b) = txrx(); 190 | //! let f = |a| { a.turn_into_b() }; 191 | //! rx_a.respond(move |a| { 192 | //! tx_b.send(f(a)); 193 | //! }); 194 | //! (tx_a, rx_b) 195 | //! }; 196 | //! ``` 197 | //! 198 | //! And indeed, it works! But that's an awful lot of boilerplate just to get a 199 | //! channel of `A`s to `B`s. Instead we can use the `txrx_map` function, which 200 | //! does all of this for us given the map function. Here's an example using 201 | //! a `Transmitter<()>` that sends to a `Receiver`: 202 | //! 203 | //! ```rust 204 | //! extern crate mogwai; 205 | //! use mogwai::prelude::*; 206 | //! 207 | //! // For every unit that gets sent, map it to `1:i32`. 208 | //! let (tx_a, rx_b) = txrx_map(|&()| 1 ); 209 | //! let shared_count = new_shared(0); 210 | //! rx_b.respond_shared(shared_count.clone(), |count: &mut i32, n: &i32| { 211 | //! *count += n; 212 | //! println!("Current count is {}", *count); 213 | //! }); 214 | //! 215 | //! tx_a.send(&()); 216 | //! tx_a.send(&()); 217 | //! tx_a.send(&()); 218 | //! assert_eq!(*shared_count.borrow(), 3); 219 | //! ``` 220 | //! 221 | //! That is useful, but we can also do much more than simple maps! We can fold 222 | //! over an internal state or a shared state, we can filter some of the sent 223 | //! messages and we can do all those things together! Check out the `txrx_*` 224 | //! family of functions: 225 | //! 226 | //! * [txrx] 227 | //! * [txrx_filter_fold] 228 | //! * [txrx_filter_fold_shared] 229 | //! * [txrx_filter_map] 230 | //! * [txrx_fold] 231 | //! * [txrx_fold_shared] 232 | //! * [txrx_map] 233 | //! 234 | //! You'll also find functions with these flavors in [Transmitter] and 235 | //! [Receiver]. 236 | //! 237 | //! ## Wiring [Transmitter]s and forwading [Receiver]s 238 | //! 239 | //! Another way to get a txrx pair of different types is to create each side 240 | //! separately using [trns] and [recv] and then wire them together: 241 | //! 242 | //! ```rust 243 | //! extern crate mogwai; 244 | //! use mogwai::prelude::*; 245 | //! 246 | //! let mut tx = trns::<()>(); 247 | //! let rx = recv::(); 248 | //! tx.wire_map(&rx, |&()| 1); 249 | //! ``` 250 | //! 251 | //! The following make up the `wire_*` family of functions on [Transmitter]: 252 | //! 253 | //! * [Transmitter::wire_filter_fold] 254 | //! * [Transmitter::wire_filter_fold_async] 255 | //! * [Transmitter::wire_filter_fold_shared] 256 | //! * [Transmitter::wire_filter_map] 257 | //! * [Transmitter::wire_fold] 258 | //! * [Transmitter::wire_fold_shared] 259 | //! * [Transmitter::wire_map] 260 | //! 261 | //! Note that they all mutate the [Transmitter] they are called on. 262 | //! 263 | //! Conversely, if you would like to forward messages from a receiver into a 264 | //! transmitter of a different type you can "forward" messages from the receiver 265 | //! to the transmitter: 266 | //! 267 | //! ```rust 268 | //! extern crate mogwai; 269 | //! use mogwai::prelude::*; 270 | //! 271 | //! let (tx, rx) = txrx::<()>(); 272 | //! let (mut tx_i32, rx_i32) = txrx::(); 273 | //! rx.forward_map(&tx_i32, |&()| 1); 274 | //! 275 | //! let shared_got_it = new_shared(false); 276 | //! rx_i32.respond_shared(shared_got_it.clone(), |got_it: &mut bool, n: &i32| { 277 | //! println!("Got {}", *n); 278 | //! *got_it = true; 279 | //! }); 280 | //! 281 | //! tx.send(&()); 282 | //! assert_eq!(*shared_got_it.borrow(), true); 283 | //! ``` 284 | //! 285 | //! These make up the `forward_*` family of functions on [Receiver]: 286 | //! 287 | //! * [Receiver::forward_filter_fold] 288 | //! * [Receiver::forward_filter_fold_async] 289 | //! * [Receiver::forward_filter_fold_shared] 290 | //! * [Receiver::forward_filter_map] 291 | //! * [Receiver::forward_fold] 292 | //! * [Receiver::forward_fold_shared] 293 | //! * [Receiver::forward_map] 294 | //! 295 | //! Note that they all consume the [Receiver] they are called on. 296 | //! 297 | //! ## Cloning, branching, etc 298 | //! 299 | //! [Transmitter]s may be cloned. Once a transmitter is cloned a message sent on 300 | //! either the clone or the original will pop out on any linked receivers: 301 | //! 302 | //! ```rust 303 | //! extern crate mogwai; 304 | //! use mogwai::prelude::*; 305 | //! 306 | //! let (tx1, rx) = txrx(); 307 | //! let tx2 = tx1.clone(); 308 | //! let shared_count = new_shared(0); 309 | //! rx.respond_shared(shared_count.clone(), |count: &mut i32, &()| { 310 | //! *count += 1; 311 | //! }); 312 | //! tx1.send(&()); 313 | //! tx2.send(&()); 314 | //! assert_eq!(*shared_count.borrow(), 2); 315 | //! ``` 316 | //! 317 | //! [Receiver]s are a bit different from [Transmitter]s, though. They are _not_ 318 | //! clonable because they house a responder, which must be unique. Instead we can 319 | //! use [Receiver::branch] to create a new receiver that is linked to the same 320 | //! transmitters as the original, but owns its own unique response to messages: 321 | //! 322 | //! ```rust 323 | //! extern crate mogwai; 324 | //! use mogwai::prelude::*; 325 | //! 326 | //! let (tx, rx1) = txrx(); 327 | //! let rx2 = rx1.branch(); 328 | //! let shared_count = new_shared(0); 329 | //! rx1.respond_shared(shared_count.clone(), |count: &mut i32, &()| { 330 | //! *count += 1; 331 | //! }); 332 | //! rx2.respond_shared(shared_count.clone(), |count: &mut i32, &()| { 333 | //! *count += 1; 334 | //! }); 335 | //! tx.send(&()); 336 | //! assert_eq!(*shared_count.borrow(), 2); 337 | //! ``` 338 | //! 339 | //! Both [Transmitter]s and [Receiver]s can be "branched" so that multiple 340 | //! transmitters may send to the same receiver and multiple receivers may respond 341 | //! to the same transmitter. These use the `contra_*` family of functions on 342 | //! [Transmitter] and the `branch_*` family of functions on [Receiver]. 343 | //! 344 | //! ### Transmitter's contra_* family 345 | //! 346 | //! This family of functions are named after Haskell's [contramap]. That's 347 | //! because these functions take a transmitter of `B`s, some flavor of function 348 | //! that transforms `B`s into `A`s and returns a new transmitter of `A`s. 349 | //! Essentially - the newly created transmitter extends the original _backward_, 350 | //! allowing you to send `A`s into it and have `B`s automatically sent on the 351 | //! original. 352 | //! 353 | //! * [Transmitter::contra_filter_fold] 354 | //! * [Transmitter::contra_filter_fold_shared] 355 | //! * [Transmitter::contra_filter_map] 356 | //! * [Transmitter::contra_fold] 357 | //! * [Transmitter::contra_map] 358 | //! 359 | //! ### Receiver's branch_* family 360 | //! 361 | //! This family of functions all extend new receivers off of an original and 362 | //! can transform messages of `A`s received on the original into messages of `B`s 363 | //! received on the newly created receiver. This is analogous to Haskell's 364 | //! [fmap]. 365 | //! 366 | //! * [Receiver::branch] 367 | //! * [Receiver::branch_filter_fold] 368 | //! * [Receiver::branch_filter_fold_shared] 369 | //! * [Receiver::branch_filter_map] 370 | //! * [Receiver::branch_fold] 371 | //! * [Receiver::branch_fold_shared] 372 | //! * [Receiver::branch_map] 373 | //! 374 | //! ### [Receiver::merge] 375 | //! 376 | //! If you have many receivers that you would like to merge you can use the 377 | //! [Receiver::merge] function. 378 | //! 379 | //! ## Done! 380 | //! 381 | //! The channels defined here are the backbone of this library. Getting to 382 | //! know the many constructors and combinators may seem like a daunting task but 383 | //! don't worry - the patterns of branching, mapping and folding are functional 384 | //! programming's bread and butter. Once you get a taste for this flavor of 385 | //! development you'll want more (and it will get easier). But remember, 386 | //! no matter how much it begs, no matter how much it cries, [NEVER feed Mogwai 387 | //! after midnight](https://youtu.be/OrHdo-v9mRA) ;) 388 | //! 389 | //! [contramap]: https://hackage.haskell.org/package/base-4.12.0.0/docs/Data-Functor-Contravariant.html#v:contramap 390 | //! [fmap]: https://hackage.haskell.org/package/base-4.12.0.0/docs/Data-Functor.html#v:fmap 391 | use std::rc::Rc; 392 | use std::cell::RefCell; 393 | use std::future::Future; 394 | use std::any::Any; 395 | use std::pin::Pin; 396 | use std::collections::HashMap; 397 | use wasm_bindgen_futures::spawn_local; 398 | 399 | type RecvResponders = Rc>>>; 400 | 401 | 402 | pub type RecvFuture = Pin>>>; 403 | 404 | 405 | pub fn wrap_future(future:F) -> Option> 406 | where 407 | F: Future> + 'static 408 | { 409 | Some(Box::pin(future)) 410 | } 411 | 412 | 413 | fn recv_from( 414 | next_k: Rc>, 415 | branches: RecvResponders 416 | ) -> Receiver { 417 | let k = { 418 | let mut next_k = next_k.borrow_mut(); 419 | let k = *next_k; 420 | *next_k += 1; 421 | k 422 | }; 423 | 424 | Receiver { 425 | k, 426 | next_k: next_k.clone(), 427 | branches: branches.clone() 428 | } 429 | } 430 | 431 | 432 | /// Send messages instantly. 433 | pub struct Transmitter { 434 | next_k: Rc>, 435 | branches: Rc>>>, 436 | } 437 | 438 | 439 | impl Clone for Transmitter { 440 | fn clone(&self) -> Self { 441 | Transmitter { 442 | next_k: self.next_k.clone(), 443 | branches: self.branches.clone() 444 | } 445 | } 446 | } 447 | 448 | 449 | impl Transmitter { 450 | /// Create a new transmitter. 451 | pub fn new() -> Transmitter { 452 | Transmitter { 453 | next_k: Rc::new(RefCell::new(0)), 454 | branches: Rc::new(RefCell::new(HashMap::new())) 455 | } 456 | } 457 | 458 | /// Spawn a receiver for this transmitter. 459 | pub fn spawn_recv(&mut self) -> Receiver { 460 | recv_from(self.next_k.clone(), self.branches.clone()) 461 | } 462 | 463 | /// Send a message to any and all receivers of this transmitter. 464 | pub fn send(&self, a:&A) { 465 | let mut branches = self.branches.borrow_mut(); 466 | branches 467 | .iter_mut() 468 | .for_each(|(_, f)| { 469 | f(a); 470 | }); 471 | } 472 | 473 | /// Execute a future that results in a message, then send it. 474 | pub fn send_async(&self, fa:FutureA) 475 | where 476 | FutureA: Future + 'static 477 | { 478 | let tx = self.clone(); 479 | spawn_local(async move { 480 | let a:A = fa.await; 481 | tx.send(&a); 482 | }); 483 | } 484 | 485 | /// Extend this transmitter with a new transmitter using a filtering fold 486 | /// function. The given function folds messages of `B` over a shared state `T` 487 | /// and optionally sends `A`s down into this transmitter. 488 | pub fn contra_filter_fold_shared( 489 | &self, 490 | var: Rc>, 491 | f:F 492 | ) -> Transmitter 493 | where 494 | B: 'static, 495 | T: 'static, 496 | F: Fn(&mut T, &B) -> Option + 'static 497 | { 498 | let tx = self.clone(); 499 | let (tev, rev) = txrx(); 500 | rev.respond(move |ev| { 501 | let result = { 502 | let mut t = var.borrow_mut(); 503 | f(&mut t, ev) 504 | }; 505 | result 506 | .into_iter() 507 | .for_each(|b| { 508 | tx.send(&b); 509 | }); 510 | }); 511 | tev 512 | } 513 | 514 | /// Extend this transmitter with a new transmitter using a filtering fold 515 | /// function. The given function folds messages of `B` over a state `T` and 516 | /// optionally sends `A`s into this transmitter. 517 | pub fn contra_filter_fold( 518 | &self, 519 | init:X, 520 | f:F 521 | ) -> Transmitter 522 | where 523 | B: 'static, 524 | T: 'static, 525 | X: Into, 526 | F: Fn(&mut T, &B) -> Option + 'static 527 | { 528 | let tx = self.clone(); 529 | let (tev, rev) = txrx(); 530 | let mut t = init.into(); 531 | rev.respond(move |ev| { 532 | f(&mut t, ev) 533 | .into_iter() 534 | .for_each(|b| { 535 | tx.send(&b); 536 | }); 537 | }); 538 | tev 539 | } 540 | 541 | /// Extend this transmitter with a new transmitter using a fold function. 542 | /// The given function folds messages of `B` into a state `T` and sends `A`s 543 | /// into this transmitter. 544 | pub fn contra_fold( 545 | &self, 546 | init:X, 547 | f:F 548 | ) -> Transmitter 549 | where 550 | B: 'static, 551 | T: 'static, 552 | X: Into, 553 | F: Fn(&mut T, &B) -> A + 'static 554 | { 555 | self.contra_filter_fold(init, move |t, ev| Some(f(t, ev))) 556 | } 557 | 558 | /// Extend this transmitter with a new transmitter using a filter function. 559 | /// The given function maps messages of `B` and optionally sends `A`s into this 560 | /// transmitter. 561 | pub fn contra_filter_map( 562 | &self, 563 | f:F 564 | ) -> Transmitter 565 | where 566 | B: 'static, 567 | F: Fn(&B) -> Option + 'static 568 | { 569 | self.contra_filter_fold((), move |&mut (), ev| f(ev)) 570 | } 571 | 572 | /// Extend this transmitter with a new transmitter using a map function. 573 | /// The given function maps messages of `B` into `A`s and sends them all into 574 | /// this transmitter. This is much like Haskell's 575 | /// [contramap](https://hackage.haskell.org/package/base-4.12.0.0/docs/Data-Functor-Contravariant.html#v:contramap), 576 | /// hence the `contra_` prefix on this family of methods. 577 | pub fn contra_map( 578 | &self, 579 | f:F 580 | ) -> Transmitter 581 | where 582 | B: 'static, 583 | F: Fn(&B) -> A + 'static 584 | { 585 | self.contra_filter_map(move |ev| Some(f(ev))) 586 | } 587 | 588 | /// Wires the transmitter to send to the given receiver using a stateful fold 589 | /// function, where the state is a shared mutex. 590 | /// 591 | /// The fold function returns an `Option`. In the case that the value of 592 | /// `Option` is `None`, no message will be sent to the receiver. 593 | pub fn wire_filter_fold_shared(&mut self, rb: &Receiver, var:Rc>, f:F) 594 | where 595 | B: Any, 596 | T: Any, 597 | F: Fn(&mut T, &A) -> Option + 'static 598 | { 599 | let tb = rb.new_trns(); 600 | let ra = self.spawn_recv(); 601 | ra.forward_filter_fold_shared(&tb, var, f); 602 | } 603 | 604 | /// Wires the transmitter to send to the given receiver using a stateful fold 605 | /// function. 606 | /// 607 | /// The fold function returns an `Option`. In the case that the value of 608 | /// `Option` is `None`, no message will be sent to the receiver. 609 | pub fn wire_filter_fold(&mut self, rb: &Receiver, init:X, f:F) 610 | where 611 | B: Any, 612 | T: Any, 613 | X: Into, 614 | F: Fn(&mut T, &A) -> Option + 'static 615 | { 616 | let tb = rb.new_trns(); 617 | let ra = self.spawn_recv(); 618 | ra.forward_filter_fold(&tb, init, f); 619 | } 620 | 621 | /// Wires the transmitter to send to the given receiver using a stateful fold 622 | /// function. 623 | pub fn wire_fold(&mut self, rb: &Receiver, init:X, f:F) 624 | where 625 | B: Any, 626 | T: Any, 627 | X: Into, 628 | F: Fn(&mut T, &A) -> B + 'static 629 | { 630 | let tb = rb.new_trns(); 631 | let ra = self.spawn_recv(); 632 | ra.forward_fold(&tb, init, f); 633 | } 634 | 635 | /// Wires the transmitter to send to the given receiver using a stateful fold 636 | /// function, where the state is a shared mutex. 637 | pub fn wire_fold_shared(&mut self, rb: &Receiver, var:Rc>, f:F) 638 | where 639 | B: Any, 640 | T: Any, 641 | F: Fn(&mut T, &A) -> B + 'static 642 | { 643 | let tb = rb.new_trns(); 644 | let ra = self.spawn_recv(); 645 | ra.forward_filter_fold_shared(&tb, var, move |t, a| Some(f(t, a))); 646 | } 647 | 648 | /// Wires the transmitter to the given receiver using a stateless map function. 649 | /// If the map function returns `None` for any messages those messages will 650 | /// *not* be sent to the given transmitter. 651 | pub fn wire_filter_map(&mut self, rb: &Receiver, f:F) 652 | where 653 | B: Any, 654 | F: Fn(&A) -> Option + 'static 655 | { 656 | let tb = rb.new_trns(); 657 | let ra = self.spawn_recv(); 658 | ra.forward_filter_map(&tb, f); 659 | } 660 | 661 | /// Wires the transmitter to the given receiver using a stateless map function. 662 | pub fn wire_map(&mut self, rb: &Receiver, f:F) 663 | where 664 | B: Any, 665 | F: Fn(&A) -> B + 'static 666 | { 667 | let tb = rb.new_trns(); 668 | let ra = self.spawn_recv(); 669 | ra.forward_map(&tb, f); 670 | } 671 | 672 | /// Wires the transmitter to the given receiver using a stateful fold function 673 | /// that returns an optional future. The future, if available, results in an 674 | /// `Option`. In the case that the value of the future's result is `None`, 675 | /// no message will be sent to the given receiver. 676 | /// 677 | /// Lastly, a clean up function is ran at the completion of the future with its 678 | /// result. 679 | /// 680 | /// To aid in returning a viable future in your fold function, use 681 | /// `wrap_future`. 682 | pub fn wire_filter_fold_async( 683 | &mut self, 684 | rb: &Receiver, 685 | init:X, 686 | f:F, 687 | h:H 688 | ) 689 | where 690 | B: Any, 691 | T: Any, 692 | X: Into, 693 | F: Fn(&mut T, &A) -> Option> + 'static, 694 | H: Fn(&mut T, &Option) + 'static 695 | { 696 | let tb = rb.new_trns(); 697 | let ra = self.spawn_recv(); 698 | ra.forward_filter_fold_async(&tb, init, f, h); 699 | } 700 | } 701 | 702 | 703 | /// Receive messages instantly. 704 | pub struct Receiver { 705 | k: usize, 706 | next_k: Rc>, 707 | branches: Rc>>>, 708 | } 709 | 710 | 711 | /// Clone a receiver. 712 | /// 713 | /// # Warning! 714 | /// Be careful with this function. Because of magic, calling 715 | /// [Receiver::respond] on a clone of a receiver sets the responder for both of 716 | /// those receivers. **Under the hood they are the same responder**. This is why 717 | /// [Receiver] has no [Clone] trait implementation. 718 | /// 719 | /// Instead of cloning, if you need a new receiver that receives from the same 720 | /// transmitter you should use [Receiver::branch], which comes in many flavors. 721 | pub fn hand_clone(rx: &Receiver) -> Receiver { 722 | Receiver { 723 | k: rx.k, 724 | next_k: rx.next_k.clone(), 725 | branches: rx.branches.clone() 726 | } 727 | } 728 | 729 | 730 | impl Receiver { 731 | pub fn new() -> Receiver { 732 | Receiver { 733 | k: 0, 734 | next_k: Rc::new(RefCell::new(1)), 735 | branches: Rc::new(RefCell::new(HashMap::new())) 736 | } 737 | } 738 | 739 | /// Set the response this receiver has to messages. Upon receiving a message 740 | /// the response will run immediately. 741 | /// 742 | pub fn respond(self, f:F) 743 | where 744 | F: FnMut(&A) + 'static 745 | { 746 | let k = self.k; 747 | let mut branches = self.branches.borrow_mut(); 748 | branches.insert(k, Box::new(f)); 749 | } 750 | 751 | /// Set the response this receiver has to messages. Upon receiving a message 752 | /// the response will run immediately. 753 | /// 754 | /// Folds mutably over a shared Rc>. 755 | pub fn respond_shared(self, val:Rc>, f:F) 756 | where 757 | F: Fn(&mut T, &A) + 'static 758 | { 759 | let k = self.k; 760 | let mut branches = self.branches.borrow_mut(); 761 | branches.insert(k, Box::new(move |a:&A| { 762 | let mut t = val.borrow_mut(); 763 | f(&mut t, a); 764 | })); 765 | } 766 | 767 | /// Removes the responder from the receiver. 768 | /// This drops anything owned by the responder. 769 | pub fn drop_responder(&mut self) { 770 | let mut branches = self.branches.borrow_mut(); 771 | let _ = branches.remove(&self.k); 772 | } 773 | 774 | pub fn new_trns(&self) -> Transmitter { 775 | Transmitter { 776 | next_k: self.next_k.clone(), 777 | branches: self.branches.clone() 778 | } 779 | } 780 | 781 | /// Branch a receiver off of the original. 782 | /// Each branch will receive from the same transmitter. 783 | /// The new branch has no initial response to messages. 784 | pub fn branch(&self) -> Receiver { 785 | recv_from(self.next_k.clone(), self.branches.clone()) 786 | } 787 | 788 | /// Branch a new receiver off of an original and wire any messages sent to the 789 | /// original by using a stateful fold function. 790 | /// 791 | /// The fold function returns an `Option`. In the case that the value of 792 | /// `Option` is `None`, no message will be sent to the new receiver. 793 | /// 794 | /// Each branch will receive from the same transmitter. 795 | pub fn branch_filter_fold(&self, init:X, f:F) -> Receiver 796 | where 797 | B: Any, 798 | X: Into, 799 | T: Any, 800 | F: Fn(&mut T, &A) -> Option + 'static 801 | { 802 | let ra = self.branch(); 803 | let (tb, rb) = txrx(); 804 | ra.forward_filter_fold(&tb, init, f); 805 | rb 806 | } 807 | 808 | /// Branch a new receiver off of an original and wire any messages sent to the 809 | /// original by using a stateful fold function, where the state is a shared 810 | /// mutex. 811 | /// 812 | /// The fold function returns an `Option`. In the case that the value of 813 | /// `Option` is `None`, no message will be sent to the new receiver. 814 | /// 815 | /// Each branch will receive from the same transmitter. 816 | pub fn branch_filter_fold_shared(&self, state:Rc>, f:F) -> Receiver 817 | where 818 | B: Any, 819 | T: Any, 820 | F: Fn(&mut T, &A) -> Option + 'static 821 | { 822 | let ra = self.branch(); 823 | let (tb, rb) = txrx(); 824 | ra.forward_filter_fold_shared(&tb, state, f); 825 | rb 826 | } 827 | 828 | /// Branch a new receiver off of an original and wire any messages sent to the 829 | /// original by using a stateful fold function. 830 | /// 831 | /// All output of the fold function is sent to the new receiver. 832 | /// 833 | /// Each branch will receive from the same transmitter(s). 834 | pub fn branch_fold(&self, init:X, f:F) -> Receiver 835 | where 836 | B: Any, 837 | X: Into, 838 | T: Any, 839 | F: Fn(&mut T, &A) -> B + 'static 840 | { 841 | let ra = self.branch(); 842 | let (tb, rb) = txrx(); 843 | ra.forward_fold(&tb, init, f); 844 | rb 845 | } 846 | 847 | /// Branch a new receiver off of an original and wire any messages sent to the 848 | /// original by using a stateful fold function, where the state is a shared 849 | /// mutex. 850 | /// 851 | /// All output of the fold function is sent to the new receiver. 852 | /// 853 | /// Each branch will receive from the same transmitter(s). 854 | pub fn branch_fold_shared(&self, t:Rc>, f:F) -> Receiver 855 | where 856 | B: Any, 857 | T: Any, 858 | F: Fn(&mut T, &A) -> B + 'static 859 | { 860 | let ra = self.branch(); 861 | let (tb, rb) = txrx(); 862 | ra.forward_fold_shared(&tb, t, f); 863 | rb 864 | } 865 | 866 | /// Branch a new receiver off of an original and wire any messages sent to the 867 | /// original by using a stateless map function. 868 | /// 869 | /// The map function returns an `Option`, representing an optional message 870 | /// to send to the new receiver. 871 | /// In the case that the result value of the map function is `None`, no message 872 | /// will be sent to the new receiver. 873 | /// 874 | /// Each branch will receive from the same transmitter. 875 | pub fn branch_filter_map(&self, f:F) -> Receiver 876 | where 877 | B: Any, 878 | F: Fn(&A) -> Option + 'static 879 | { 880 | let ra = self.branch(); 881 | let (tb, rb) = txrx(); 882 | ra.forward_filter_map(&tb, f); 883 | rb 884 | } 885 | 886 | /// Branch a new receiver off of an original and wire any messages sent to the 887 | /// original by using a stateless map function. 888 | /// 889 | /// All output of the map function is sent to the new receiver. 890 | /// 891 | /// Each branch will receive from the same transmitter. 892 | pub fn branch_map(&self, f:F) -> Receiver 893 | where 894 | B: Any, 895 | F: Fn(&A) -> B + 'static 896 | { 897 | let ra = self.branch(); 898 | let (tb, rb) = txrx(); 899 | ra.forward_map(&tb, f); 900 | rb 901 | } 902 | 903 | /// Forwards messages on the given receiver to the given transmitter using a 904 | /// stateful fold function, where the state is a shared mutex. 905 | /// 906 | /// The fold function returns an `Option`. In the case that the value of 907 | /// `Option` is `None`, no message will be sent to the transmitter. 908 | pub fn forward_filter_fold_shared(self, tx: &Transmitter, var:Rc>, f:F) 909 | where 910 | B: Any, 911 | T: Any, 912 | F: Fn(&mut T, &A) -> Option + 'static 913 | { 914 | let tx = tx.clone(); 915 | self.respond(move |a:&A| { 916 | let result = { 917 | let mut t = var.borrow_mut(); 918 | f(&mut t, a) 919 | }; 920 | result 921 | .into_iter() 922 | .for_each(|b| { 923 | tx.send(&b); 924 | }); 925 | }); 926 | } 927 | 928 | /// Forwards messages on the given receiver to the given transmitter using a 929 | /// stateful fold function. 930 | /// 931 | /// The fold function returns an `Option`. In the case that the value of 932 | /// `Option` is `None`, no message will be sent to the transmitter. 933 | pub fn forward_filter_fold(self, tx: &Transmitter, init:X, f:F) 934 | where 935 | B: Any, 936 | T: Any, 937 | X: Into, 938 | F: Fn(&mut T, &A) -> Option + 'static 939 | { 940 | let var = Rc::new(RefCell::new(init.into())); 941 | self.forward_filter_fold_shared(tx, var, f); 942 | } 943 | 944 | /// Forwards messages on the given receiver to the given transmitter using a 945 | /// stateful fold function. All output of the fold 946 | /// function is sent to the given transmitter. 947 | pub fn forward_fold(self, tx: &Transmitter, init:X, f:F) 948 | where 949 | B: Any, 950 | T: Any, 951 | X: Into, 952 | F: Fn(&mut T, &A) -> B + 'static 953 | { 954 | self.forward_filter_fold(tx, init, move |t:&mut T, a:&A| { 955 | Some(f(t, a)) 956 | }) 957 | } 958 | 959 | /// Forwards messages on the given receiver to the given transmitter using a 960 | /// stateful fold function, where the state is a shared mutex. All output of 961 | /// the fold function is sent to the given transmitter. 962 | pub fn forward_fold_shared(self, tx: &Transmitter, t:Rc>, f:F) 963 | where 964 | B: Any, 965 | T: Any, 966 | F: Fn(&mut T, &A) -> B + 'static 967 | { 968 | self.forward_filter_fold_shared(tx, t, move |t:&mut T, a:&A| { 969 | Some(f(t, a)) 970 | }) 971 | } 972 | 973 | /// Forwards messages on the given receiver to the given transmitter using a 974 | /// stateless map function. If the map function returns `None` for any messages 975 | /// those messages will *not* be sent to the given transmitter. 976 | pub fn forward_filter_map(self, tx: &Transmitter, f:F) 977 | where 978 | B: Any, 979 | F: Fn(&A) -> Option + 'static 980 | { 981 | self.forward_filter_fold(tx, (), move |&mut (), a| f(a)) 982 | } 983 | 984 | /// Forwards messages on the given receiver to the given transmitter using a 985 | /// stateless map function. All output of the map function is sent to the given 986 | /// transmitter. 987 | pub fn forward_map(self, tx: &Transmitter, f:F) 988 | where 989 | B: Any, 990 | F: Fn(&A) -> B + 'static 991 | { 992 | self.forward_filter_map(tx, move |a| Some(f(a))) 993 | } 994 | 995 | /// Forwards messages on the given receiver to the given transmitter using a 996 | /// stateful fold function that returns an optional future. The future, if 997 | /// returned, is executed. The future results in an `Option`. In the case 998 | /// that the value of the future's result is `None`, no message will be sent to 999 | /// the transmitter. 1000 | /// 1001 | /// Lastly, a clean up function is ran at the completion of the future with its 1002 | /// result. 1003 | /// 1004 | /// To aid in returning a viable future in your fold function, use 1005 | /// `wrap_future`. 1006 | // TODO: Examples of fold functions. 1007 | pub fn forward_filter_fold_async( 1008 | self, 1009 | tb: &Transmitter, 1010 | init:X, 1011 | f:F, 1012 | h:H 1013 | ) 1014 | where 1015 | B: Any, 1016 | T: Any, 1017 | X: Into, 1018 | F: Fn(&mut T, &A) -> Option> + 'static, 1019 | H: Fn(&mut T, &Option) + 'static 1020 | { 1021 | let state = Rc::new(RefCell::new(init.into())); 1022 | let cleanup = Rc::new(Box::new(h)); 1023 | let tb = tb.clone(); 1024 | self.respond(move |a:&A| { 1025 | let may_async = { 1026 | let mut block_state = state.borrow_mut(); 1027 | f(&mut block_state, a) 1028 | }; 1029 | may_async 1030 | .into_iter() 1031 | .for_each(|block:RecvFuture| { 1032 | let tb_clone = tb.clone(); 1033 | let state_clone = state.clone(); 1034 | let cleanup_clone = cleanup.clone(); 1035 | let future = 1036 | async move { 1037 | let opt:Option = 1038 | block.await; 1039 | opt 1040 | .iter() 1041 | .for_each(|b| tb_clone.send(&b)); 1042 | let mut inner_state = state_clone.borrow_mut(); 1043 | cleanup_clone(&mut inner_state, &opt); 1044 | }; 1045 | spawn_local(future); 1046 | 1047 | }); 1048 | }); 1049 | } 1050 | 1051 | /// Merge all the receivers into one. Any time a message is received on any 1052 | /// receiver, it will be sent to the returned receiver. 1053 | pub fn merge(rxs: Vec>) -> Receiver { 1054 | let (tx, rx) = txrx(); 1055 | rxs 1056 | .into_iter() 1057 | .for_each(|rx_inc| { 1058 | let tx = tx.clone(); 1059 | rx_inc 1060 | .branch() 1061 | .respond(move |a| { 1062 | tx.send(a); 1063 | }); 1064 | }); 1065 | rx 1066 | } 1067 | } 1068 | 1069 | 1070 | /// Create a new unlinked `Receiver`. 1071 | pub fn recv() -> Receiver { 1072 | Receiver::new() 1073 | } 1074 | 1075 | 1076 | /// Create a new unlinked `Transmitter`. 1077 | pub fn trns() -> Transmitter { 1078 | Transmitter::new() 1079 | } 1080 | 1081 | 1082 | /// Create a linked `Transmitter` and `Receiver` pair. 1083 | pub fn txrx() -> (Transmitter, Receiver) { 1084 | let mut trns = Transmitter::new(); 1085 | let recv = trns.spawn_recv(); 1086 | (trns, recv) 1087 | } 1088 | 1089 | /// Create a linked, filtering `Transmitter` and `Receiver` pair with 1090 | /// internal state. 1091 | /// 1092 | /// Using the given filtering fold function, messages sent on the transmitter 1093 | /// will be folded into the given internal state and output messages may or may 1094 | /// not be sent to the receiver. 1095 | /// 1096 | /// In the case that the return value of the given function is `None`, no message 1097 | /// will be sent to the receiver. 1098 | pub fn txrx_filter_fold(t:T, f:F) -> (Transmitter, Receiver) 1099 | where 1100 | A:Any, 1101 | B:Any, 1102 | T:Any, 1103 | F:Fn(&mut T, &A) -> Option + 'static, 1104 | { 1105 | let (ta, ra) = txrx(); 1106 | let (tb, rb) = txrx(); 1107 | ra.forward_filter_fold(&tb, t, f); 1108 | (ta, rb) 1109 | } 1110 | 1111 | /// Create a linked, filtering `Transmitter` and `Receiver` pair with 1112 | /// shared state. 1113 | /// 1114 | /// Using the given filtering fold function, messages sent on the transmitter 1115 | /// will be folded into the given shared state and output messages may or may 1116 | /// not be sent to the receiver. 1117 | /// 1118 | /// In the case that the return value of the given function is `None`, no message 1119 | /// will be sent to the receiver. 1120 | pub fn txrx_filter_fold_shared( 1121 | var:Rc>, 1122 | f:F 1123 | ) -> (Transmitter, Receiver) 1124 | where 1125 | A:Any, 1126 | B:Any, 1127 | T:Any, 1128 | F:Fn(&mut T, &A) -> Option + 'static, 1129 | { 1130 | let (ta, ra) = txrx(); 1131 | let (tb, rb) = txrx(); 1132 | ra.forward_filter_fold_shared(&tb, var, f); 1133 | (ta, rb) 1134 | } 1135 | 1136 | /// Create a linked `Transmitter` and `Receiver` pair with internal state. 1137 | /// 1138 | /// Using the given fold function, messages sent on the transmitter will be 1139 | /// folded into the given internal state and all output messages will be sent to 1140 | /// the receiver. 1141 | pub fn txrx_fold(t:T, f:F) -> (Transmitter, Receiver) 1142 | where 1143 | A:Any, 1144 | B:Any, 1145 | T:Any, 1146 | F:Fn(&mut T, &A) -> B + 'static, 1147 | { 1148 | let (ta, ra) = txrx(); 1149 | let (tb, rb) = txrx(); 1150 | ra.forward_fold(&tb, t, f); 1151 | (ta, rb) 1152 | } 1153 | 1154 | /// Create a linked `Transmitter` and `Receiver` pair with shared state. 1155 | /// 1156 | /// Using the given fold function, messages sent on the transmitter are folded 1157 | /// into the given internal state and all output messages will be sent to the 1158 | /// receiver. 1159 | pub fn txrx_fold_shared(t:Rc>, f:F) -> (Transmitter, Receiver) 1160 | where 1161 | A:Any, 1162 | B:Any, 1163 | T:Any, 1164 | F:Fn(&mut T, &A) -> B + 'static, 1165 | { 1166 | let (ta, ra) = txrx(); 1167 | let (tb, rb) = txrx(); 1168 | ra.forward_fold_shared(&tb, t, f); 1169 | (ta, rb) 1170 | } 1171 | 1172 | /// Create a linked, filtering `Transmitter` and `Receiver` pair. 1173 | /// 1174 | /// Using the given filtering map function, messages sent on the transmitter 1175 | /// are mapped to output messages that may or may not be sent to the receiver. 1176 | /// 1177 | /// In the case that the return value of the given function is `None`, no message 1178 | /// will be sent to the receiver. 1179 | pub fn txrx_filter_map(f:F) -> (Transmitter, Receiver) 1180 | where 1181 | A:Any, 1182 | B:Any, 1183 | F:Fn(&A) -> Option + 'static, 1184 | { 1185 | let (ta, ra) = txrx(); 1186 | let (tb, rb) = txrx(); 1187 | ra.forward_filter_map(&tb, f); 1188 | (ta, rb) 1189 | } 1190 | 1191 | 1192 | /// Create a linked `Transmitter` and `Receiver` pair. 1193 | /// 1194 | /// Using the given map function, messages sent on the transmitter are mapped 1195 | /// to output messages that will be sent to the receiver. 1196 | pub fn txrx_map(f:F) -> (Transmitter, Receiver) 1197 | where 1198 | A:Any, 1199 | B:Any, 1200 | F:Fn(&A) -> B + 'static, 1201 | { 1202 | let (ta, ra) = txrx(); 1203 | let (tb, rb) = txrx(); 1204 | ra.forward_map(&tb, f); 1205 | (ta, rb) 1206 | } 1207 | 1208 | 1209 | /// Helper for making thread-safe shared mutable variables. 1210 | /// 1211 | /// Use this as a short hand for creating variables to pass to 1212 | /// the many `*_shared` flavored fold functions in the [txrx](index.html) 1213 | /// module. 1214 | pub fn new_shared>(init:X) -> Rc> { 1215 | Rc::new(RefCell::new(init.into())) 1216 | } 1217 | 1218 | 1219 | 1220 | #[cfg(test)] 1221 | mod range { 1222 | #[test] 1223 | fn range() { 1224 | let mut n = 0; 1225 | for i in 0..3 { 1226 | n = i; 1227 | } 1228 | 1229 | assert_eq!(n, 2); 1230 | } 1231 | } 1232 | 1233 | 1234 | #[cfg(test)] 1235 | mod instant_txrx { 1236 | use super::*; 1237 | 1238 | #[test] 1239 | fn txrx_test() { 1240 | let count = Rc::new(RefCell::new(0)); 1241 | let (tx_unit, rx_unit) = txrx::<()>(); 1242 | let (tx_i32, rx_i32) = txrx::(); 1243 | { 1244 | let my_count = count.clone(); 1245 | rx_i32.respond(move |n:&i32| { 1246 | println!("Got message: {:?}", n); 1247 | let mut c = my_count.borrow_mut(); 1248 | *c = *n; 1249 | }); 1250 | 1251 | let mut n = 0; 1252 | rx_unit.respond(move |()| { 1253 | n += 1; 1254 | tx_i32.send(&n); 1255 | }) 1256 | } 1257 | 1258 | tx_unit.send(&()); 1259 | tx_unit.send(&()); 1260 | tx_unit.send(&()); 1261 | 1262 | let final_count:i32 = *count.borrow(); 1263 | assert_eq!(final_count, 3); 1264 | } 1265 | 1266 | #[test] 1267 | fn wire_txrx() { 1268 | let mut tx_unit = Transmitter::<()>::new(); 1269 | let rx_str = Receiver::::new(); 1270 | tx_unit.wire_filter_fold(&rx_str, 0, |n:&mut i32, &()| -> Option { 1271 | *n += 1; 1272 | if *n > 2 { 1273 | Some(format!("Passed 3 incoming messages ({})", *n)) 1274 | } else { 1275 | None 1276 | } 1277 | }); 1278 | 1279 | let got_called = Rc::new(RefCell::new(false)); 1280 | let remote_got_called = got_called.clone(); 1281 | rx_str.respond(move |s: &String| { 1282 | println!("got: {:?}", s); 1283 | let mut called = remote_got_called.borrow_mut(); 1284 | *called = true; 1285 | }); 1286 | 1287 | tx_unit.send(&()); 1288 | tx_unit.send(&()); 1289 | tx_unit.send(&()); 1290 | 1291 | let ever_called = *got_called.borrow(); 1292 | assert!(ever_called); 1293 | } 1294 | 1295 | #[test] 1296 | fn branch_map() { 1297 | let (tx, rx) = txrx::<()>(); 1298 | let ry:Receiver = 1299 | rx.branch_map(|_| 0); 1300 | 1301 | let done = 1302 | Rc::new(RefCell::new(false)); 1303 | 1304 | let cdone = done.clone(); 1305 | ry.respond(move |n| { 1306 | if *n == 0 { 1307 | *cdone.borrow_mut() = true; 1308 | } 1309 | }); 1310 | 1311 | tx.send(&()); 1312 | 1313 | assert!(*done.borrow()); 1314 | } 1315 | } 1316 | --------------------------------------------------------------------------------