├── .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