on master help sign in
.gitignore+1
since splitsnapshots
unifiedsplit
@@ -1,3 +1,4 @@
1/target
1/target
2**/*.rs.bk
2**/*.rs.bk
3wasm-pack.log
3wasm-pack.log
4+/.vscode
.travis.yml+19−10
@@ -16,7 +16,6 @@ install:
16 - *INSTALL_NODE_VIA_NVM
16 - *INSTALL_NODE_VIA_NVM
17 - *INSTALL_WASM_PACK
17 - *INSTALL_WASM_PACK
18 - rustup target add wasm32-unknown-unknown
18 - rustup target add wasm32-unknown-unknown
19− - rustup component add rustfmt
20
19
21matrix:
20matrix:
22 include:
21 include:
@@ -26,20 +25,30 @@ matrix:
26 - name: "build (beta)"
25 - name: "build (beta)"
27 rust: beta
26 rust: beta
28 script: INSTALL=1 ./build.sh
27 script: INSTALL=1 ./build.sh
29− - name: "build (nightly)"
30− rust: nightly
31− script: INSTALL=1 ./build.sh
32 - name: "headless browser tests (firefox)"
28 - name: "headless browser tests (firefox)"
33 rust: stable
29 rust: stable
34 addons:
30 addons:
35 firefox: latest
31 firefox: latest
36− script: wasm-pack test --firefox --headless
32+ script: wasm-pack test --firefox --headless -- --features xxx-unstable-internal-use-only
37− - name: "headless browser tests (chrome)"
38− rust: stable
39− addons:
40− chrome: stable
41− script: wasm-pack test --chrome --headless
42 - name: "native tests"
33 - name: "native tests"
43 rust: stable
34 rust: stable
44 install: echo "no install"
35 install: echo "no install"
45 script: cargo test --all
36 script: cargo test --all
37+
38+ # TODO: A bug in `rustc` is making `js-sys` broken on nightly:
39+ # https://github.com/rust-lang/rust/issues/69315
40+ # - name: "build (nightly)"
41+ # rust: nightly
42+ # script: INSTALL=1 ./build.sh
43+ # - name: "check native benches"
44+ # rust: nightly
45+ # install: echo "no install"
46+ # script: cargo check --benches --features xxx-unstable-internal-use-only
47+
48+ # TODO: `chromedriver` is complaining about ports or something. Temporarily
49+ # disable in CI until we figure it out.
50+ # - name: "headless browser tests (chrome)"
51+ # rust: stable
52+ # addons:
53+ # chrome: stable
54+ # script: wasm-pack test --chrome --headless -- --features xxx-unstable-internal-use-only
Cargo.lock+720−260
@@ -1,168 +1,426 @@
1# This file is automatically @generated by Cargo.
1# This file is automatically @generated by Cargo.
2# It is not intended for manual editing.
2# It is not intended for manual editing.
3[[package]]
3[[package]]
4−name = "aho-corasick"
4+name = "anyhow"
5−version = "0.6.9"
5+version = "1.0.25"
6source = "registry+https://github.com/rust-lang/crates.io-index"
6source = "registry+https://github.com/rust-lang/crates.io-index"
7−dependencies = [
8− "memchr 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
9−]
10
7
11[[package]]
8[[package]]
12name = "atty"
9name = "atty"
13−version = "0.2.11"
10+version = "0.2.13"
14source = "registry+https://github.com/rust-lang/crates.io-index"
11source = "registry+https://github.com/rust-lang/crates.io-index"
15dependencies = [
12dependencies = [
16− "libc 0.2.48 (registry+https://github.com/rust-lang/crates.io-index)",
17− "termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
13+ "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)",
18− "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
14+ "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
19]
15]
20
16
21[[package]]
17[[package]]
22name = "autocfg"
18name = "autocfg"
23−version = "0.1.2"
19+version = "0.1.7"
24source = "registry+https://github.com/rust-lang/crates.io-index"
20source = "registry+https://github.com/rust-lang/crates.io-index"
25
21
26[[package]]
22[[package]]
27−name = "backtrace"
23+name = "bitflags"
28−version = "0.3.13"
24+version = "1.2.1"
29source = "registry+https://github.com/rust-lang/crates.io-index"
25source = "registry+https://github.com/rust-lang/crates.io-index"
30−dependencies = [
31− "autocfg 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
32− "backtrace-sys 0.1.28 (registry+https://github.com/rust-lang/crates.io-index)",
33− "cfg-if 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
34− "libc 0.2.48 (registry+https://github.com/rust-lang/crates.io-index)",
35− "rustc-demangle 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)",
36− "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
37−]
38
26
39[[package]]
27[[package]]
40−name = "backtrace-sys"
28+name = "bstr"
41−version = "0.1.28"
29+version = "0.2.8"
42source = "registry+https://github.com/rust-lang/crates.io-index"
30source = "registry+https://github.com/rust-lang/crates.io-index"
43dependencies = [
31dependencies = [
32+ "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
44− "cc 1.0.29 (registry+https://github.com/rust-lang/crates.io-index)",
33+ "memchr 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
34+ "regex-automata 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)",
45− "libc 0.2.48 (registry+https://github.com/rust-lang/crates.io-index)",
35+ "serde 1.0.114 (registry+https://github.com/rust-lang/crates.io-index)",
46]
36]
47
37
48[[package]]
38[[package]]
49name = "bumpalo"
39name = "bumpalo"
50−version = "2.0.0"
40+version = "2.6.0"
51source = "registry+https://github.com/rust-lang/crates.io-index"
41source = "registry+https://github.com/rust-lang/crates.io-index"
52
42
53[[package]]
43[[package]]
54−name = "cc"
44+name = "byteorder"
55−version = "1.0.29"
45+version = "1.3.2"
56source = "registry+https://github.com/rust-lang/crates.io-index"
46source = "registry+https://github.com/rust-lang/crates.io-index"
57
47
48+[[package]]
49+name = "cast"
50+version = "0.2.3"
51+source = "registry+https://github.com/rust-lang/crates.io-index"
52+dependencies = [
53+ "rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
54+]
55+
58[[package]]
56[[package]]
59name = "cfg-if"
57name = "cfg-if"
60−version = "0.1.6"
58+version = "0.1.10"
59+source = "registry+https://github.com/rust-lang/crates.io-index"
60+
61+[[package]]
62+name = "clap"
63+version = "2.33.0"
61source = "registry+https://github.com/rust-lang/crates.io-index"
64source = "registry+https://github.com/rust-lang/crates.io-index"
65+dependencies = [
66+ "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
67+ "textwrap 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)",
68+ "unicode-width 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
69+]
62
70
63[[package]]
71[[package]]
64name = "console_error_panic_hook"
72name = "console_error_panic_hook"
65−version = "0.1.5"
73+version = "0.1.6"
66source = "registry+https://github.com/rust-lang/crates.io-index"
74source = "registry+https://github.com/rust-lang/crates.io-index"
67dependencies = [
75dependencies = [
68− "cfg-if 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
76+ "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
69− "wasm-bindgen 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)",
77+ "wasm-bindgen 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)",
70]
78]
71
79
72[[package]]
80[[package]]
73name = "console_log"
81name = "console_log"
74−version = "0.1.2"
82+version = "0.2.0"
83+source = "registry+https://github.com/rust-lang/crates.io-index"
84+dependencies = [
85+ "log 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)",
86+ "web-sys 0.3.32 (registry+https://github.com/rust-lang/crates.io-index)",
87+]
88+
89+[[package]]
90+name = "criterion"
91+version = "0.3.3"
75source = "registry+https://github.com/rust-lang/crates.io-index"
92source = "registry+https://github.com/rust-lang/crates.io-index"
76dependencies = [
93dependencies = [
94+ "atty 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)",
95+ "cast 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
96+ "clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)",
77− "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
97+ "criterion-plot 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)",
98+ "csv 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
99+ "itertools 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)",
100+ "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
101+ "num-traits 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)",
102+ "oorandom 11.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
103+ "plotters 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)",
104+ "rayon 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
105+ "regex 1.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
106+ "serde 1.0.114 (registry+https://github.com/rust-lang/crates.io-index)",
107+ "serde_cbor 0.11.1 (registry+https://github.com/rust-lang/crates.io-index)",
108+ "serde_derive 1.0.114 (registry+https://github.com/rust-lang/crates.io-index)",
109+ "serde_json 1.0.57 (registry+https://github.com/rust-lang/crates.io-index)",
110+ "tinytemplate 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
111+ "walkdir 2.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
112+]
113+
114+[[package]]
115+name = "criterion-plot"
116+version = "0.4.3"
117+source = "registry+https://github.com/rust-lang/crates.io-index"
118+dependencies = [
119+ "cast 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
120+ "itertools 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)",
121+]
122+
123+[[package]]
124+name = "crossbeam-deque"
125+version = "0.7.2"
126+source = "registry+https://github.com/rust-lang/crates.io-index"
127+dependencies = [
128+ "crossbeam-epoch 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
129+ "crossbeam-utils 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
130+]
131+
132+[[package]]
133+name = "crossbeam-epoch"
134+version = "0.8.0"
135+source = "registry+https://github.com/rust-lang/crates.io-index"
136+dependencies = [
137+ "autocfg 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
138+ "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
139+ "crossbeam-utils 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
140+ "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
141+ "memoffset 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)",
142+ "scopeguard 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
143+]
144+
145+[[package]]
146+name = "crossbeam-queue"
147+version = "0.2.0"
148+source = "registry+https://github.com/rust-lang/crates.io-index"
149+dependencies = [
150+ "crossbeam-utils 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
151+]
152+
153+[[package]]
154+name = "crossbeam-utils"
155+version = "0.7.0"
156+source = "registry+https://github.com/rust-lang/crates.io-index"
157+dependencies = [
158+ "autocfg 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
159+ "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
160+ "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
161+]
162+
163+[[package]]
164+name = "csv"
165+version = "1.1.1"
166+source = "registry+https://github.com/rust-lang/crates.io-index"
167+dependencies = [
168+ "bstr 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
169+ "csv-core 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
170+ "itoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)",
171+ "ryu 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
172+ "serde 1.0.114 (registry+https://github.com/rust-lang/crates.io-index)",
173+]
174+
175+[[package]]
176+name = "csv-core"
177+version = "0.1.6"
178+source = "registry+https://github.com/rust-lang/crates.io-index"
179+dependencies = [
78− "web-sys 0.3.11 (registry+https://github.com/rust-lang/crates.io-index)",
180+ "memchr 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
79]
181]
80
182
81[[package]]
183[[package]]
82name = "dodrio"
184name = "dodrio"
83−version = "0.1.0"
185+version = "0.2.0"
84dependencies = [
186dependencies = [
187+ "bumpalo 2.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
85− "bumpalo 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
188+ "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
189+ "console_log 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
190+ "criterion 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
191+ "dodrio-js-api 0.2.0",
86− "console_log 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
192+ "futures 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
87− "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)",
193+ "fxhash 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
88− "js-sys 0.3.11 (registry+https://github.com/rust-lang/crates.io-index)",
194+ "js-sys 0.3.32 (registry+https://github.com/rust-lang/crates.io-index)",
195+ "log 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)",
196+ "longest-increasing-subsequence 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
89− "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
197+ "serde 1.0.114 (registry+https://github.com/rust-lang/crates.io-index)",
90− "wasm-bindgen 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)",
198+ "wasm-bindgen 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)",
91− "wasm-bindgen-futures 0.3.11 (registry+https://github.com/rust-lang/crates.io-index)",
199+ "wasm-bindgen-futures 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)",
92− "wasm-bindgen-test 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)",
200+ "wasm-bindgen-test 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
93− "web-sys 0.3.11 (registry+https://github.com/rust-lang/crates.io-index)",
201+ "web-sys 0.3.32 (registry+https://github.com/rust-lang/crates.io-index)",
94]
202]
95
203
96[[package]]
204[[package]]
97name = "dodrio-counter"
205name = "dodrio-counter"
98version = "0.1.0"
206version = "0.1.0"
99dependencies = [
207dependencies = [
100− "console_error_panic_hook 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
208+ "console_error_panic_hook 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
101− "console_log 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
209+ "console_log 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
102− "dodrio 0.1.0",
210+ "dodrio 0.2.0",
103− "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
104− "wasm-bindgen 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)",
105− "wasm-bindgen-test 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)",
106− "web-sys 0.3.11 (registry+https://github.com/rust-lang/crates.io-index)",
211+ "log 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)",
212+ "wasm-bindgen 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)",
213+ "wasm-bindgen-test 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
214+ "web-sys 0.3.32 (registry+https://github.com/rust-lang/crates.io-index)",
215+]
216+
217+[[package]]
218+name = "dodrio-game-of-life"
219+version = "0.1.0"
220+dependencies = [
221+ "console_error_panic_hook 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
222+ "dodrio 0.2.0",
223+ "js-sys 0.3.32 (registry+https://github.com/rust-lang/crates.io-index)",
224+ "wasm-bindgen 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)",
225+ "wasm-bindgen-futures 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)",
226+ "wasm-bindgen-test 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
227+ "web-sys 0.3.32 (registry+https://github.com/rust-lang/crates.io-index)",
107]
228]
108
229
109[[package]]
230[[package]]
110name = "dodrio-hello-world"
231name = "dodrio-hello-world"
111version = "0.1.0"
232version = "0.1.0"
112dependencies = [
233dependencies = [
113− "console_error_panic_hook 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
234+ "console_error_panic_hook 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
114− "dodrio 0.1.0",
235+ "dodrio 0.2.0",
115− "wasm-bindgen 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)",
236+ "wasm-bindgen 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)",
116− "wasm-bindgen-test 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)",
237+ "wasm-bindgen-test 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
117− "web-sys 0.3.11 (registry+https://github.com/rust-lang/crates.io-index)",
238+ "web-sys 0.3.32 (registry+https://github.com/rust-lang/crates.io-index)",
118]
239]
119
240
120[[package]]
241[[package]]
121name = "dodrio-input-form"
242name = "dodrio-input-form"
122version = "0.1.0"
243version = "0.1.0"
123dependencies = [
244dependencies = [
124− "console_error_panic_hook 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
245+ "console_error_panic_hook 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
125− "dodrio 0.1.0",
246+ "dodrio 0.2.0",
126− "wasm-bindgen 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)",
247+ "wasm-bindgen 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)",
127− "wasm-bindgen-test 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)",
248+ "wasm-bindgen-test 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
249+ "web-sys 0.3.32 (registry+https://github.com/rust-lang/crates.io-index)",
250+]
251+
252+[[package]]
253+name = "dodrio-js-api"
254+version = "0.2.0"
255+dependencies = [
256+ "dodrio 0.2.0",
257+ "js-sys 0.3.32 (registry+https://github.com/rust-lang/crates.io-index)",
258+ "wasm-bindgen 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)",
259+ "wasm-bindgen-futures 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)",
260+ "wasm-bindgen-test 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
261+]
262+
263+[[package]]
264+name = "dodrio-js-component"
265+version = "0.1.0"
266+dependencies = [
267+ "console_error_panic_hook 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
268+ "dodrio 0.2.0",
269+ "dodrio-js-api 0.2.0",
270+ "js-sys 0.3.32 (registry+https://github.com/rust-lang/crates.io-index)",
271+ "wasm-bindgen 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)",
272+ "web-sys 0.3.32 (registry+https://github.com/rust-lang/crates.io-index)",
273+]
274+
275+[[package]]
276+name = "dodrio-moire"
277+version = "0.1.0"
278+dependencies = [
279+ "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
280+ "console_error_panic_hook 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
281+ "console_log 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
282+ "dodrio 0.2.0",
283+ "js-sys 0.3.32 (registry+https://github.com/rust-lang/crates.io-index)",
128− "web-sys 0.3.11 (registry+https://github.com/rust-lang/crates.io-index)",
284+ "log 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)",
285+ "wasm-bindgen 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)",
286+ "wasm-bindgen-futures 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)",
287+ "wasm-bindgen-test 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
288+ "web-sys 0.3.32 (registry+https://github.com/rust-lang/crates.io-index)",
129]
289]
130
290
131[[package]]
291[[package]]
132−name = "env_logger"
292+name = "dodrio-sierpinski"
133−version = "0.6.0"
293+version = "0.1.0"
294+dependencies = [
295+ "console_error_panic_hook 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
296+ "dodrio 0.2.0",
297+ "js-sys 0.3.32 (registry+https://github.com/rust-lang/crates.io-index)",
298+ "wasm-bindgen 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)",
299+ "wasm-bindgen-futures 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)",
300+ "wasm-bindgen-test 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
301+ "web-sys 0.3.32 (registry+https://github.com/rust-lang/crates.io-index)",
302+]
303+
304+[[package]]
305+name = "dodrio-todomvc"
306+version = "0.1.0"
307+dependencies = [
308+ "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
309+ "console_error_panic_hook 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
310+ "console_log 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
311+ "dodrio 0.2.0",
312+ "js-sys 0.3.32 (registry+https://github.com/rust-lang/crates.io-index)",
313+ "log 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)",
314+ "serde 1.0.114 (registry+https://github.com/rust-lang/crates.io-index)",
315+ "serde_json 1.0.57 (registry+https://github.com/rust-lang/crates.io-index)",
316+ "wasm-bindgen 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)",
317+ "wasm-bindgen-futures 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)",
318+ "wasm-bindgen-test 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
319+ "web-sys 0.3.32 (registry+https://github.com/rust-lang/crates.io-index)",
320+]
321+
322+[[package]]
323+name = "either"
324+version = "1.5.3"
325+source = "registry+https://github.com/rust-lang/crates.io-index"
326+
327+[[package]]
328+name = "futures"
329+version = "0.3.5"
134source = "registry+https://github.com/rust-lang/crates.io-index"
330source = "registry+https://github.com/rust-lang/crates.io-index"
135dependencies = [
331dependencies = [
136− "atty 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
332+ "futures-channel 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
333+ "futures-core 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
137− "humantime 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
334+ "futures-executor 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
138− "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
335+ "futures-io 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
139− "regex 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
336+ "futures-sink 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
337+ "futures-task 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
140− "termcolor 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)",
338+ "futures-util 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
141]
339]
142
340
143[[package]]
341[[package]]
144−name = "failure"
342+name = "futures-channel"
145−version = "0.1.5"
343+version = "0.3.5"
146source = "registry+https://github.com/rust-lang/crates.io-index"
344source = "registry+https://github.com/rust-lang/crates.io-index"
147dependencies = [
345dependencies = [
148− "backtrace 0.3.13 (registry+https://github.com/rust-lang/crates.io-index)",
346+ "futures-core 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
149− "failure_derive 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
347+ "futures-sink 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
150]
348]
151
349
152[[package]]
350[[package]]
153−name = "failure_derive"
351+name = "futures-core"
154−version = "0.1.5"
352+version = "0.3.5"
353+source = "registry+https://github.com/rust-lang/crates.io-index"
354+
355+[[package]]
356+name = "futures-executor"
357+version = "0.3.5"
155source = "registry+https://github.com/rust-lang/crates.io-index"
358source = "registry+https://github.com/rust-lang/crates.io-index"
156dependencies = [
359dependencies = [
157− "proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)",
360+ "futures-core 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
158− "quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)",
361+ "futures-task 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
159− "syn 0.15.26 (registry+https://github.com/rust-lang/crates.io-index)",
160− "synstructure 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)",
362+ "futures-util 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
161]
363]
162
364
163[[package]]
365[[package]]
164−name = "futures"
366+name = "futures-io"
165−version = "0.1.25"
367+version = "0.3.5"
368+source = "registry+https://github.com/rust-lang/crates.io-index"
369+
370+[[package]]
371+name = "futures-macro"
372+version = "0.3.5"
373+source = "registry+https://github.com/rust-lang/crates.io-index"
374+dependencies = [
375+ "proc-macro-hack 0.5.11 (registry+https://github.com/rust-lang/crates.io-index)",
376+ "proc-macro2 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)",
377+ "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
378+ "syn 1.0.33 (registry+https://github.com/rust-lang/crates.io-index)",
379+]
380+
381+[[package]]
382+name = "futures-sink"
383+version = "0.3.5"
384+source = "registry+https://github.com/rust-lang/crates.io-index"
385+
386+[[package]]
387+name = "futures-task"
388+version = "0.3.5"
389+source = "registry+https://github.com/rust-lang/crates.io-index"
390+dependencies = [
391+ "once_cell 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
392+]
393+
394+[[package]]
395+name = "futures-util"
396+version = "0.3.5"
397+source = "registry+https://github.com/rust-lang/crates.io-index"
398+dependencies = [
399+ "futures-channel 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
400+ "futures-core 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
401+ "futures-io 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
402+ "futures-macro 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
403+ "futures-sink 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
404+ "futures-task 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
405+ "memchr 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
406+ "pin-project 0.4.16 (registry+https://github.com/rust-lang/crates.io-index)",
407+ "pin-utils 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
408+ "proc-macro-hack 0.5.11 (registry+https://github.com/rust-lang/crates.io-index)",
409+ "proc-macro-nested 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
410+ "slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
411+]
412+
413+[[package]]
414+name = "fxhash"
415+version = "0.2.1"
416+source = "registry+https://github.com/rust-lang/crates.io-index"
417+dependencies = [
418+ "byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
419+]
420+
421+[[package]]
422+name = "half"
423+version = "1.6.0"
166source = "registry+https://github.com/rust-lang/crates.io-index"
424source = "registry+https://github.com/rust-lang/crates.io-index"
167
425
168[[package]]
426[[package]]
@@ -170,191 +428,352 @@ name = "heck"
170version = "0.3.1"
428version = "0.3.1"
171source = "registry+https://github.com/rust-lang/crates.io-index"
429source = "registry+https://github.com/rust-lang/crates.io-index"
172dependencies = [
430dependencies = [
173− "unicode-segmentation 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
431+ "unicode-segmentation 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
174]
432]
175
433
176[[package]]
434[[package]]
177−name = "humantime"
435+name = "hermit-abi"
178−version = "1.2.0"
436+version = "0.1.3"
179source = "registry+https://github.com/rust-lang/crates.io-index"
437source = "registry+https://github.com/rust-lang/crates.io-index"
180dependencies = [
438dependencies = [
181− "quick-error 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
439+ "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)",
182]
440]
183
441
442+[[package]]
443+name = "itertools"
444+version = "0.9.0"
445+source = "registry+https://github.com/rust-lang/crates.io-index"
446+dependencies = [
447+ "either 1.5.3 (registry+https://github.com/rust-lang/crates.io-index)",
448+]
449+
450+[[package]]
451+name = "itoa"
452+version = "0.4.4"
453+source = "registry+https://github.com/rust-lang/crates.io-index"
454+
184[[package]]
455[[package]]
185name = "js-sys"
456name = "js-sys"
186−version = "0.3.11"
457+version = "0.3.32"
187source = "registry+https://github.com/rust-lang/crates.io-index"
458source = "registry+https://github.com/rust-lang/crates.io-index"
188dependencies = [
459dependencies = [
189− "wasm-bindgen 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)",
460+ "wasm-bindgen 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)",
190]
461]
191
462
192[[package]]
463[[package]]
193name = "lazy_static"
464name = "lazy_static"
194−version = "1.2.0"
465+version = "1.4.0"
195source = "registry+https://github.com/rust-lang/crates.io-index"
466source = "registry+https://github.com/rust-lang/crates.io-index"
196
467
197[[package]]
468[[package]]
198name = "libc"
469name = "libc"
199−version = "0.2.48"
470+version = "0.2.66"
200source = "registry+https://github.com/rust-lang/crates.io-index"
471source = "registry+https://github.com/rust-lang/crates.io-index"
201
472
202[[package]]
473[[package]]
203name = "log"
474name = "log"
204−version = "0.4.6"
475+version = "0.4.11"
205source = "registry+https://github.com/rust-lang/crates.io-index"
476source = "registry+https://github.com/rust-lang/crates.io-index"
206dependencies = [
477dependencies = [
207− "cfg-if 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
478+ "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
208]
479]
209
480
481+[[package]]
482+name = "longest-increasing-subsequence"
483+version = "0.1.0"
484+source = "registry+https://github.com/rust-lang/crates.io-index"
485+
210[[package]]
486[[package]]
211name = "memchr"
487name = "memchr"
212−version = "2.2.0"
488+version = "2.2.1"
213source = "registry+https://github.com/rust-lang/crates.io-index"
489source = "registry+https://github.com/rust-lang/crates.io-index"
490+dependencies = [
491+ "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)",
492+]
493+
494+[[package]]
495+name = "memoffset"
496+version = "0.5.3"
497+source = "registry+https://github.com/rust-lang/crates.io-index"
498+dependencies = [
499+ "rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
500+]
214
501
215[[package]]
502[[package]]
216name = "nom"
503name = "nom"
217−version = "4.2.0"
504+version = "4.2.3"
218source = "registry+https://github.com/rust-lang/crates.io-index"
505source = "registry+https://github.com/rust-lang/crates.io-index"
219dependencies = [
506dependencies = [
220− "memchr 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
507+ "memchr 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
221 "version_check 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
508 "version_check 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
222]
509]
223
510
224[[package]]
511[[package]]
225−name = "proc-macro2"
512+name = "num-traits"
226−version = "0.4.27"
513+version = "0.2.10"
514+source = "registry+https://github.com/rust-lang/crates.io-index"
515+dependencies = [
516+ "autocfg 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
517+]
518+
519+[[package]]
520+name = "num_cpus"
521+version = "1.11.1"
227source = "registry+https://github.com/rust-lang/crates.io-index"
522source = "registry+https://github.com/rust-lang/crates.io-index"
228dependencies = [
523dependencies = [
524+ "hermit-abi 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
229− "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
525+ "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)",
230]
526]
231
527
232[[package]]
528[[package]]
233−name = "quick-error"
529+name = "once_cell"
530+version = "1.3.1"
531+source = "registry+https://github.com/rust-lang/crates.io-index"
532+
533+[[package]]
534+name = "oorandom"
234−version = "1.2.2"
535+version = "11.1.0"
536+source = "registry+https://github.com/rust-lang/crates.io-index"
537+
538+[[package]]
539+name = "pin-project"
540+version = "0.4.16"
235source = "registry+https://github.com/rust-lang/crates.io-index"
541source = "registry+https://github.com/rust-lang/crates.io-index"
542+dependencies = [
543+ "pin-project-internal 0.4.16 (registry+https://github.com/rust-lang/crates.io-index)",
544+]
545+
546+[[package]]
547+name = "pin-project-internal"
548+version = "0.4.16"
549+source = "registry+https://github.com/rust-lang/crates.io-index"
550+dependencies = [
551+ "proc-macro2 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)",
552+ "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
553+ "syn 1.0.33 (registry+https://github.com/rust-lang/crates.io-index)",
554+]
555+
556+[[package]]
557+name = "pin-utils"
558+version = "0.1.0"
559+source = "registry+https://github.com/rust-lang/crates.io-index"
560+
561+[[package]]
562+name = "plotters"
563+version = "0.2.12"
564+source = "registry+https://github.com/rust-lang/crates.io-index"
565+dependencies = [
566+ "js-sys 0.3.32 (registry+https://github.com/rust-lang/crates.io-index)",
567+ "num-traits 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)",
568+ "wasm-bindgen 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)",
569+ "web-sys 0.3.32 (registry+https://github.com/rust-lang/crates.io-index)",
570+]
571+
572+[[package]]
573+name = "proc-macro-hack"
574+version = "0.5.11"
575+source = "registry+https://github.com/rust-lang/crates.io-index"
576+dependencies = [
577+ "proc-macro2 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)",
578+ "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
579+ "syn 1.0.33 (registry+https://github.com/rust-lang/crates.io-index)",
580+]
581+
582+[[package]]
583+name = "proc-macro-nested"
584+version = "0.1.3"
585+source = "registry+https://github.com/rust-lang/crates.io-index"
586+
587+[[package]]
588+name = "proc-macro2"
589+version = "1.0.18"
590+source = "registry+https://github.com/rust-lang/crates.io-index"
591+dependencies = [
592+ "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
593+]
236
594
237[[package]]
595[[package]]
238name = "quote"
596name = "quote"
239−version = "0.6.11"
597+version = "1.0.2"
240source = "registry+https://github.com/rust-lang/crates.io-index"
598source = "registry+https://github.com/rust-lang/crates.io-index"
241dependencies = [
599dependencies = [
242− "proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)",
600+ "proc-macro2 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)",
243]
601]
244
602
245[[package]]
603[[package]]
246−name = "redox_syscall"
604+name = "rayon"
247−version = "0.1.51"
605+version = "1.3.0"
248source = "registry+https://github.com/rust-lang/crates.io-index"
606source = "registry+https://github.com/rust-lang/crates.io-index"
607+dependencies = [
608+ "crossbeam-deque 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)",
609+ "either 1.5.3 (registry+https://github.com/rust-lang/crates.io-index)",
610+ "rayon-core 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
611+]
249
612
250[[package]]
613[[package]]
251−name = "redox_termios"
614+name = "rayon-core"
252−version = "0.1.1"
615+version = "1.7.0"
253source = "registry+https://github.com/rust-lang/crates.io-index"
616source = "registry+https://github.com/rust-lang/crates.io-index"
254dependencies = [
617dependencies = [
618+ "crossbeam-deque 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)",
619+ "crossbeam-queue 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
620+ "crossbeam-utils 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
621+ "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
255− "redox_syscall 0.1.51 (registry+https://github.com/rust-lang/crates.io-index)",
622+ "num_cpus 1.11.1 (registry+https://github.com/rust-lang/crates.io-index)",
256]
623]
257
624
258[[package]]
625[[package]]
259name = "regex"
626name = "regex"
260−version = "1.1.0"
627+version = "1.3.3"
261source = "registry+https://github.com/rust-lang/crates.io-index"
628source = "registry+https://github.com/rust-lang/crates.io-index"
262dependencies = [
629dependencies = [
263− "aho-corasick 0.6.9 (registry+https://github.com/rust-lang/crates.io-index)",
264− "memchr 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
265− "regex-syntax 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)",
630+ "regex-syntax 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)",
266− "thread_local 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
631+]
632+
633+[[package]]
634+name = "regex-automata"
635+version = "0.1.8"
636+source = "registry+https://github.com/rust-lang/crates.io-index"
637+dependencies = [
267− "utf8-ranges 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
638+ "byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
268]
639]
269
640
270[[package]]
641[[package]]
271name = "regex-syntax"
642name = "regex-syntax"
272−version = "0.6.5"
643+version = "0.6.13"
644+source = "registry+https://github.com/rust-lang/crates.io-index"
645+
646+[[package]]
647+name = "rustc_version"
648+version = "0.2.3"
273source = "registry+https://github.com/rust-lang/crates.io-index"
649source = "registry+https://github.com/rust-lang/crates.io-index"
274dependencies = [
650dependencies = [
275− "ucd-util 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
651+ "semver 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)",
276]
652]
277
653
278[[package]]
654[[package]]
279−name = "rustc-demangle"
655+name = "ryu"
280−version = "0.1.13"
656+version = "1.0.2"
657+source = "registry+https://github.com/rust-lang/crates.io-index"
658+
659+[[package]]
660+name = "same-file"
661+version = "1.0.5"
281source = "registry+https://github.com/rust-lang/crates.io-index"
662source = "registry+https://github.com/rust-lang/crates.io-index"
663+dependencies = [
664+ "winapi-util 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
665+]
282
666
283[[package]]
667[[package]]
284name = "scoped-tls"
668name = "scoped-tls"
285−version = "0.1.2"
669+version = "1.0.0"
286source = "registry+https://github.com/rust-lang/crates.io-index"
670source = "registry+https://github.com/rust-lang/crates.io-index"
287
671
288[[package]]
672[[package]]
289−name = "sourcefile"
673+name = "scopeguard"
290−version = "0.1.4"
674+version = "1.0.0"
291source = "registry+https://github.com/rust-lang/crates.io-index"
675source = "registry+https://github.com/rust-lang/crates.io-index"
292
676
293[[package]]
677[[package]]
294−name = "syn"
678+name = "semver"
295−version = "0.15.26"
679+version = "0.9.0"
296source = "registry+https://github.com/rust-lang/crates.io-index"
680source = "registry+https://github.com/rust-lang/crates.io-index"
297dependencies = [
681dependencies = [
298− "proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)",
299− "quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)",
300− "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
682+ "semver-parser 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
301]
683]
302
684
303[[package]]
685[[package]]
304−name = "synstructure"
686+name = "semver-parser"
305−version = "0.10.1"
687+version = "0.7.0"
688+source = "registry+https://github.com/rust-lang/crates.io-index"
689+
690+[[package]]
691+name = "serde"
692+version = "1.0.114"
306source = "registry+https://github.com/rust-lang/crates.io-index"
693source = "registry+https://github.com/rust-lang/crates.io-index"
307dependencies = [
694dependencies = [
308− "proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)",
309− "quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)",
310− "syn 0.15.26 (registry+https://github.com/rust-lang/crates.io-index)",
311− "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
695+ "serde_derive 1.0.114 (registry+https://github.com/rust-lang/crates.io-index)",
312]
696]
313
697
314[[package]]
698[[package]]
315−name = "termcolor"
699+name = "serde_cbor"
316−version = "1.0.4"
700+version = "0.11.1"
317source = "registry+https://github.com/rust-lang/crates.io-index"
701source = "registry+https://github.com/rust-lang/crates.io-index"
318dependencies = [
702dependencies = [
703+ "half 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
319− "wincolor 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
704+ "serde 1.0.114 (registry+https://github.com/rust-lang/crates.io-index)",
320]
705]
321
706
322[[package]]
707[[package]]
323−name = "termion"
708+name = "serde_derive"
324−version = "1.5.1"
709+version = "1.0.114"
325source = "registry+https://github.com/rust-lang/crates.io-index"
710source = "registry+https://github.com/rust-lang/crates.io-index"
326dependencies = [
711dependencies = [
712+ "proc-macro2 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)",
327− "libc 0.2.48 (registry+https://github.com/rust-lang/crates.io-index)",
713+ "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
328− "redox_syscall 0.1.51 (registry+https://github.com/rust-lang/crates.io-index)",
329− "redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
714+ "syn 1.0.33 (registry+https://github.com/rust-lang/crates.io-index)",
330]
715]
331
716
332[[package]]
717[[package]]
333−name = "thread_local"
718+name = "serde_json"
334−version = "0.3.6"
719+version = "1.0.57"
335source = "registry+https://github.com/rust-lang/crates.io-index"
720source = "registry+https://github.com/rust-lang/crates.io-index"
336dependencies = [
721dependencies = [
722+ "itoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)",
723+ "ryu 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
337− "lazy_static 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
724+ "serde 1.0.114 (registry+https://github.com/rust-lang/crates.io-index)",
338]
725]
339
726
340[[package]]
727[[package]]
341−name = "ucd-util"
728+name = "slab"
729+version = "0.4.2"
730+source = "registry+https://github.com/rust-lang/crates.io-index"
731+
732+[[package]]
733+name = "sourcefile"
342−version = "0.1.3"
734+version = "0.1.4"
735+source = "registry+https://github.com/rust-lang/crates.io-index"
736+
737+[[package]]
738+name = "syn"
739+version = "1.0.33"
343source = "registry+https://github.com/rust-lang/crates.io-index"
740source = "registry+https://github.com/rust-lang/crates.io-index"
741+dependencies = [
742+ "proc-macro2 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)",
743+ "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
744+ "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
745+]
746+
747+[[package]]
748+name = "textwrap"
749+version = "0.11.0"
750+source = "registry+https://github.com/rust-lang/crates.io-index"
751+dependencies = [
752+ "unicode-width 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
753+]
754+
755+[[package]]
756+name = "tinytemplate"
757+version = "1.1.0"
758+source = "registry+https://github.com/rust-lang/crates.io-index"
759+dependencies = [
760+ "serde 1.0.114 (registry+https://github.com/rust-lang/crates.io-index)",
761+ "serde_json 1.0.57 (registry+https://github.com/rust-lang/crates.io-index)",
762+]
344
763
345[[package]]
764[[package]]
346name = "unicode-segmentation"
765name = "unicode-segmentation"
347−version = "1.2.1"
766+version = "1.6.0"
348source = "registry+https://github.com/rust-lang/crates.io-index"
767source = "registry+https://github.com/rust-lang/crates.io-index"
349
768
350[[package]]
769[[package]]
351−name = "unicode-xid"
770+name = "unicode-width"
352−version = "0.1.0"
771+version = "0.1.6"
353source = "registry+https://github.com/rust-lang/crates.io-index"
772source = "registry+https://github.com/rust-lang/crates.io-index"
354
773
355[[package]]
774[[package]]
356−name = "utf8-ranges"
775+name = "unicode-xid"
357−version = "1.0.2"
776+version = "0.2.0"
358source = "registry+https://github.com/rust-lang/crates.io-index"
777source = "registry+https://github.com/rust-lang/crates.io-index"
359
778
360[[package]]
779[[package]]
@@ -362,125 +781,136 @@ name = "version_check"
362version = "0.1.5"
781version = "0.1.5"
363source = "registry+https://github.com/rust-lang/crates.io-index"
782source = "registry+https://github.com/rust-lang/crates.io-index"
364
783
784+[[package]]
785+name = "walkdir"
786+version = "2.3.1"
787+source = "registry+https://github.com/rust-lang/crates.io-index"
788+dependencies = [
789+ "same-file 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)",
790+ "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
791+ "winapi-util 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
792+]
793+
365[[package]]
794[[package]]
366name = "wasm-bindgen"
795name = "wasm-bindgen"
367−version = "0.2.34"
796+version = "0.2.55"
368source = "registry+https://github.com/rust-lang/crates.io-index"
797source = "registry+https://github.com/rust-lang/crates.io-index"
369dependencies = [
798dependencies = [
799+ "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
370− "wasm-bindgen-macro 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)",
800+ "wasm-bindgen-macro 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)",
371]
801]
372
802
373[[package]]
803[[package]]
374name = "wasm-bindgen-backend"
804name = "wasm-bindgen-backend"
375−version = "0.2.34"
805+version = "0.2.55"
376source = "registry+https://github.com/rust-lang/crates.io-index"
806source = "registry+https://github.com/rust-lang/crates.io-index"
377dependencies = [
807dependencies = [
808+ "bumpalo 2.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
378− "lazy_static 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
809+ "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
379− "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
810+ "log 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)",
380− "proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)",
811+ "proc-macro2 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)",
381− "quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)",
812+ "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
382− "syn 0.15.26 (registry+https://github.com/rust-lang/crates.io-index)",
813+ "syn 1.0.33 (registry+https://github.com/rust-lang/crates.io-index)",
383− "wasm-bindgen-shared 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)",
814+ "wasm-bindgen-shared 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)",
384]
815]
385
816
386[[package]]
817[[package]]
387name = "wasm-bindgen-futures"
818name = "wasm-bindgen-futures"
388−version = "0.3.11"
819+version = "0.4.5"
389source = "registry+https://github.com/rust-lang/crates.io-index"
820source = "registry+https://github.com/rust-lang/crates.io-index"
390dependencies = [
821dependencies = [
391− "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)",
822+ "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
392− "js-sys 0.3.11 (registry+https://github.com/rust-lang/crates.io-index)",
823+ "js-sys 0.3.32 (registry+https://github.com/rust-lang/crates.io-index)",
824+ "wasm-bindgen 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)",
393− "wasm-bindgen 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)",
825+ "web-sys 0.3.32 (registry+https://github.com/rust-lang/crates.io-index)",
394]
826]
395
827
396[[package]]
828[[package]]
397name = "wasm-bindgen-macro"
829name = "wasm-bindgen-macro"
398−version = "0.2.34"
830+version = "0.2.55"
399source = "registry+https://github.com/rust-lang/crates.io-index"
831source = "registry+https://github.com/rust-lang/crates.io-index"
400dependencies = [
832dependencies = [
401− "quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)",
833+ "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
402− "wasm-bindgen-macro-support 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)",
834+ "wasm-bindgen-macro-support 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)",
403]
835]
404
836
405[[package]]
837[[package]]
406name = "wasm-bindgen-macro-support"
838name = "wasm-bindgen-macro-support"
407−version = "0.2.34"
839+version = "0.2.55"
408source = "registry+https://github.com/rust-lang/crates.io-index"
840source = "registry+https://github.com/rust-lang/crates.io-index"
409dependencies = [
841dependencies = [
410− "proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)",
842+ "proc-macro2 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)",
411− "quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)",
843+ "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
412− "syn 0.15.26 (registry+https://github.com/rust-lang/crates.io-index)",
844+ "syn 1.0.33 (registry+https://github.com/rust-lang/crates.io-index)",
413− "wasm-bindgen-backend 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)",
845+ "wasm-bindgen-backend 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)",
414− "wasm-bindgen-shared 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)",
846+ "wasm-bindgen-shared 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)",
415]
847]
416
848
417[[package]]
849[[package]]
418name = "wasm-bindgen-shared"
850name = "wasm-bindgen-shared"
419−version = "0.2.34"
851+version = "0.2.55"
420source = "registry+https://github.com/rust-lang/crates.io-index"
852source = "registry+https://github.com/rust-lang/crates.io-index"
421
853
422[[package]]
854[[package]]
423name = "wasm-bindgen-test"
855name = "wasm-bindgen-test"
424−version = "0.2.34"
856+version = "0.3.5"
425source = "registry+https://github.com/rust-lang/crates.io-index"
857source = "registry+https://github.com/rust-lang/crates.io-index"
426dependencies = [
858dependencies = [
427− "console_error_panic_hook 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
428− "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)",
859+ "console_error_panic_hook 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
429− "js-sys 0.3.11 (registry+https://github.com/rust-lang/crates.io-index)",
860+ "js-sys 0.3.32 (registry+https://github.com/rust-lang/crates.io-index)",
430− "scoped-tls 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
861+ "scoped-tls 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
431− "wasm-bindgen 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)",
862+ "wasm-bindgen 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)",
432− "wasm-bindgen-futures 0.3.11 (registry+https://github.com/rust-lang/crates.io-index)",
863+ "wasm-bindgen-futures 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)",
433− "wasm-bindgen-test-macro 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)",
864+ "wasm-bindgen-test-macro 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
434]
865]
435
866
436[[package]]
867[[package]]
437name = "wasm-bindgen-test-macro"
868name = "wasm-bindgen-test-macro"
438−version = "0.2.34"
869+version = "0.3.5"
439source = "registry+https://github.com/rust-lang/crates.io-index"
870source = "registry+https://github.com/rust-lang/crates.io-index"
440dependencies = [
871dependencies = [
441− "proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)",
872+ "proc-macro2 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)",
442− "quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)",
873+ "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
443]
874]
444
875
445[[package]]
876[[package]]
446name = "wasm-bindgen-webidl"
877name = "wasm-bindgen-webidl"
447−version = "0.2.28"
878+version = "0.2.55"
448source = "registry+https://github.com/rust-lang/crates.io-index"
879source = "registry+https://github.com/rust-lang/crates.io-index"
449dependencies = [
880dependencies = [
450− "failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
881+ "anyhow 1.0.25 (registry+https://github.com/rust-lang/crates.io-index)",
451 "heck 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
882 "heck 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
452− "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
883+ "log 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)",
453− "proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)",
884+ "proc-macro2 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)",
454− "quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)",
885+ "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
455− "syn 0.15.26 (registry+https://github.com/rust-lang/crates.io-index)",
886+ "syn 1.0.33 (registry+https://github.com/rust-lang/crates.io-index)",
456− "wasm-bindgen-backend 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)",
887+ "wasm-bindgen-backend 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)",
457− "weedle 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
888+ "weedle 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)",
458]
889]
459
890
460[[package]]
891[[package]]
461name = "web-sys"
892name = "web-sys"
462−version = "0.3.11"
893+version = "0.3.32"
463source = "registry+https://github.com/rust-lang/crates.io-index"
894source = "registry+https://github.com/rust-lang/crates.io-index"
464dependencies = [
895dependencies = [
465− "env_logger 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
466− "failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
896+ "anyhow 1.0.25 (registry+https://github.com/rust-lang/crates.io-index)",
467− "js-sys 0.3.11 (registry+https://github.com/rust-lang/crates.io-index)",
897+ "js-sys 0.3.32 (registry+https://github.com/rust-lang/crates.io-index)",
468 "sourcefile 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
898 "sourcefile 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
469− "wasm-bindgen 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)",
899+ "wasm-bindgen 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)",
470− "wasm-bindgen-webidl 0.2.28 (registry+https://github.com/rust-lang/crates.io-index)",
900+ "wasm-bindgen-webidl 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)",
471]
901]
472
902
473[[package]]
903[[package]]
474name = "weedle"
904name = "weedle"
475−version = "0.8.0"
905+version = "0.10.0"
476source = "registry+https://github.com/rust-lang/crates.io-index"
906source = "registry+https://github.com/rust-lang/crates.io-index"
477dependencies = [
907dependencies = [
478− "nom 4.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
908+ "nom 4.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
479]
909]
480
910
481[[package]]
911[[package]]
482name = "winapi"
912name = "winapi"
483−version = "0.3.6"
913+version = "0.3.8"
484source = "registry+https://github.com/rust-lang/crates.io-index"
914source = "registry+https://github.com/rust-lang/crates.io-index"
485dependencies = [
915dependencies = [
486 "winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
916 "winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -497,7 +927,7 @@ name = "winapi-util"
497version = "0.1.2"
927version = "0.1.2"
498source = "registry+https://github.com/rust-lang/crates.io-index"
928source = "registry+https://github.com/rust-lang/crates.io-index"
499dependencies = [
929dependencies = [
500− "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
930+ "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
501]
931]
502
932
503[[package]]
933[[package]]
@@ -505,71 +935,101 @@ name = "winapi-x86_64-pc-windows-gnu"
505version = "0.4.0"
935version = "0.4.0"
506source = "registry+https://github.com/rust-lang/crates.io-index"
936source = "registry+https://github.com/rust-lang/crates.io-index"
507
937
508−[[package]]
509−name = "wincolor"
510−version = "1.0.1"
511−source = "registry+https://github.com/rust-lang/crates.io-index"
512−dependencies = [
513− "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
514− "winapi-util 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
515−]
516−
517[metadata]
938[metadata]
518−"checksum aho-corasick 0.6.9 (registry+https://github.com/rust-lang/crates.io-index)" = "1e9a933f4e58658d7b12defcf96dc5c720f20832deebe3e0a19efd3b6aaeeb9e"
939+"checksum anyhow 1.0.25 (registry+https://github.com/rust-lang/crates.io-index)" = "9267dff192e68f3399525901e709a48c1d3982c9c072fa32f2127a0cb0babf14"
519−"checksum atty 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "9a7d5b8723950951411ee34d271d99dddcc2035a16ab25310ea2c8cfd4369652"
940+"checksum atty 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)" = "1803c647a3ec87095e7ae7acfca019e98de5ec9a7d01343f611cf3152ed71a90"
520−"checksum autocfg 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a6d640bee2da49f60a4068a7fae53acde8982514ab7bae8b8cea9e88cbcfd799"
941+"checksum autocfg 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "1d49d90015b3c36167a20fe2810c5cd875ad504b39cff3d4eae7977e6b7c1cb2"
521−"checksum backtrace 0.3.13 (registry+https://github.com/rust-lang/crates.io-index)" = "b5b493b66e03090ebc4343eb02f94ff944e0cbc9ac6571491d170ba026741eb5"
942+"checksum bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
522−"checksum backtrace-sys 0.1.28 (registry+https://github.com/rust-lang/crates.io-index)" = "797c830ac25ccc92a7f8a7b9862bde440715531514594a6154e3d4a54dd769b6"
943+"checksum bstr 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "8d6c2c5b58ab920a4f5aeaaca34b4488074e8cc7596af94e6f8c6ff247c60245"
523−"checksum bumpalo 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bfa2dc7f1b2cc9e9b0d80ab7765699bd5314653930f7f54ba635a13bdd406165"
944+"checksum bumpalo 2.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ad807f2fc2bf185eeb98ff3a901bd46dc5ad58163d0fa4577ba0d25674d71708"
524−"checksum cc 1.0.29 (registry+https://github.com/rust-lang/crates.io-index)" = "4390a3b5f4f6bce9c1d0c00128379df433e53777fdd30e92f16a529332baec4e"
945+"checksum byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a7c3dd8985a7111efc5c80b44e23ecdd8c007de8ade3b96595387e812b957cf5"
525−"checksum cfg-if 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "082bb9b28e00d3c9d39cc03e64ce4cea0f1bb9b3fde493f0cbc008472d22bdf4"
946+"checksum cast 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "4b9434b9a5aa1450faa3f9cb14ea0e8c53bb5d2b3c1bfd1ab4fc03e9f33fbfb0"
947+"checksum cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
948+"checksum clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5067f5bb2d80ef5d68b4c87db81601f0b75bca627bc2ef76b141d7b846a3c6d9"
526−"checksum console_error_panic_hook 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "6c5dd2c094474ec60a6acaf31780af270275e3153bafff2db5995b715295762e"
949+"checksum console_error_panic_hook 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "b8d976903543e0c48546a91908f21588a680a8c8f984df9a5d69feccb2b2a211"
527−"checksum console_log 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1e7871d2947441b0fdd8e2bd1ce2a2f75304f896582c0d572162d48290683c48"
950+"checksum console_log 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "501a375961cef1a0d44767200e66e4a559283097e91d0730b1d75dfb2f8a1494"
528−"checksum env_logger 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "afb070faf94c85d17d50ca44f6ad076bce18ae92f0037d350947240a36e9d42e"
951+"checksum criterion 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "70daa7ceec6cf143990669a04c7df13391d55fb27bd4079d252fca774ba244d8"
952+"checksum criterion-plot 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "e022feadec601fba1649cfa83586381a4ad31c6bf3a9ab7d408118b05dd9889d"
953+"checksum crossbeam-deque 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "c3aa945d63861bfe624b55d153a39684da1e8c0bc8fba932f7ee3a3c16cea3ca"
954+"checksum crossbeam-epoch 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5064ebdbf05ce3cb95e45c8b086f72263f4166b29b97f6baff7ef7fe047b55ac"
955+"checksum crossbeam-queue 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "dfd6515864a82d2f877b42813d4553292c6659498c9a2aa31bab5a15243c2700"
956+"checksum crossbeam-utils 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ce446db02cdc3165b94ae73111e570793400d0794e46125cc4056c81cbb039f4"
957+"checksum csv 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "37519ccdfd73a75821cac9319d4fce15a81b9fcf75f951df5b9988aa3a0af87d"
958+"checksum csv-core 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "9b5cadb6b25c77aeff80ba701712494213f4a8418fcda2ee11b6560c3ad0bf4c"
959+"checksum either 1.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "bb1f6b1ce1c140482ea30ddd3335fc0024ac7ee112895426e0a629a6c20adfe3"
960+"checksum futures 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "1e05b85ec287aac0dc34db7d4a569323df697f9c55b99b15d6b4ef8cde49f613"
961+"checksum futures-channel 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "f366ad74c28cca6ba456d95e6422883cfb4b252a83bed929c83abfdbbf2967d5"
962+"checksum futures-core 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "59f5fff90fd5d971f936ad674802482ba441b6f09ba5e15fd8b39145582ca399"
963+"checksum futures-executor 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "10d6bb888be1153d3abeb9006b11b02cf5e9b209fda28693c31ae1e4e012e314"
964+"checksum futures-io 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "de27142b013a8e869c14957e6d2edeef89e97c289e69d042ee3a49acd8b51789"
965+"checksum futures-macro 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "d0b5a30a4328ab5473878237c447333c093297bded83a4983d10f4deea240d39"
529−"checksum failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "795bd83d3abeb9220f257e597aa0080a508b27533824adf336529648f6abf7e2"
966+"checksum futures-sink 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "3f2032893cb734c7a05d85ce0cc8b8c4075278e93b24b66f9de99d6eb0fa8acc"
530−"checksum failure_derive 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "ea1063915fd7ef4309e222a5a07cf9c319fb9c7836b1f89b85458672dbb127e1"
967+"checksum futures-task 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "bdb66b5f09e22019b1ab0830f7785bcea8e7a42148683f99214f73f8ec21a626"
531−"checksum futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)" = "49e7653e374fe0d0c12de4250f0bdb60680b8c80eed558c5c7538eec9c89e21b"
968+"checksum futures-util 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "8764574ff08b701a084482c3c7031349104b07ac897393010494beaa18ce32c6"
969+"checksum fxhash 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c"
970+"checksum half 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d36fab90f82edc3c747f9d438e06cf0a491055896f2a279638bb5beed6c40177"
532"checksum heck 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205"
971"checksum heck 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205"
533−"checksum humantime 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3ca7e5f2e110db35f93b837c81797f3714500b81d517bf20c431b16d3ca4f114"
972+"checksum hermit-abi 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "307c3c9f937f38e3534b1d6447ecf090cafcc9744e4a6360e8b037b2cf5af120"
973+"checksum itertools 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "284f18f85651fe11e8a991b2adb42cb078325c996ed026d994719efcfca1d54b"
974+"checksum itoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "501266b7edd0174f8530248f87f99c88fbe60ca4ef3dd486835b8d8d53136f7f"
534−"checksum js-sys 0.3.11 (registry+https://github.com/rust-lang/crates.io-index)" = "838d3c46e859486a1c7d24defa6fb2a4d4a7fc64ab79079a2b8dd6a92b4c327f"
975+"checksum js-sys 0.3.32 (registry+https://github.com/rust-lang/crates.io-index)" = "1c840fdb2167497b0bd0db43d6dfe61e91637fa72f9d061f8bd17ddc44ba6414"
535−"checksum lazy_static 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a374c89b9db55895453a74c1e38861d9deec0b01b405a82516e9d5de4820dea1"
976+"checksum lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
536−"checksum libc 0.2.48 (registry+https://github.com/rust-lang/crates.io-index)" = "e962c7641008ac010fa60a7dfdc1712449f29c44ef2d4702394aea943ee75047"
977+"checksum libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)" = "d515b1f41455adea1313a4a2ac8a8a477634fbae63cc6100e3aebb207ce61558"
537−"checksum log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c84ec4b527950aa83a329754b01dbe3f58361d1c5efacd1f6d68c494d08a17c6"
978+"checksum log 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)" = "4fabed175da42fed1fa0746b0ea71f412aa9d35e76e95e59b192c64b9dc2bf8b"
538−"checksum memchr 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2efc7bc57c883d4a4d6e3246905283d8dae951bb3bd32f49d6ef297f546e1c39"
979+"checksum longest-increasing-subsequence 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b3bd0dd2cd90571056fdb71f6275fada10131182f84899f4b2a916e565d81d86"
539−"checksum nom 4.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b30adc557058ce00c9d0d7cb3c6e0b5bc6f36e2e2eabe74b0ba726d194abd588"
980+"checksum memchr 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "88579771288728879b57485cc7d6b07d648c9f0141eb955f8ab7f9d45394468e"
540−"checksum proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)" = "4d317f9caece796be1980837fd5cb3dfec5613ebdb04ad0956deea83ce168915"
981+"checksum memoffset 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "75189eb85871ea5c2e2c15abbdd541185f63b408415e5051f5cac122d8c774b9"
541−"checksum quick-error 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9274b940887ce9addde99c4eee6b5c44cc494b182b97e73dc8ffdcb3397fd3f0"
982+"checksum nom 4.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2ad2a91a8e869eeb30b9cb3119ae87773a8f4ae617f41b1eb9c154b2905f7bd6"
983+"checksum num-traits 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)" = "d4c81ffc11c212fa327657cb19dd85eb7419e163b5b076bede2bdb5c974c07e4"
984+"checksum num_cpus 1.11.1 (registry+https://github.com/rust-lang/crates.io-index)" = "76dac5ed2a876980778b8b85f75a71b6cbf0db0b1232ee12f826bccb00d09d72"
985+"checksum once_cell 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b1c601810575c99596d4afc46f78a678c80105117c379eb3650cf99b8a21ce5b"
986+"checksum oorandom 11.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ebcec7c9c2a95cacc7cd0ecb89d8a8454eca13906f6deb55258ffff0adeb9405"
987+"checksum pin-project 0.4.16 (registry+https://github.com/rust-lang/crates.io-index)" = "81d480cb4e89522ccda96d0eed9af94180b7a5f93fb28f66e1fd7d68431663d1"
988+"checksum pin-project-internal 0.4.16 (registry+https://github.com/rust-lang/crates.io-index)" = "a82996f11efccb19b685b14b5df818de31c1edcee3daa256ab5775dd98e72feb"
989+"checksum pin-utils 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
990+"checksum plotters 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)" = "4e3bb8da247d27ae212529352020f3e5ee16e83c0c258061d27b08ab92675eeb"
991+"checksum proc-macro-hack 0.5.11 (registry+https://github.com/rust-lang/crates.io-index)" = "ecd45702f76d6d3c75a80564378ae228a85f0b59d2f3ed43c91b4a69eb2ebfc5"
992+"checksum proc-macro-nested 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "369a6ed065f249a159e06c45752c780bda2fb53c995718f9e484d08daa9eb42e"
993+"checksum proc-macro2 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)" = "beae6331a816b1f65d04c45b078fd8e6c93e8071771f41b8163255bbd8d7c8fa"
542−"checksum quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)" = "cdd8e04bd9c52e0342b406469d494fcb033be4bdbe5c606016defbb1681411e1"
994+"checksum quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "053a8c8bcc71fcce321828dc897a98ab9760bef03a4fc36693c231e5b3216cfe"
543−"checksum redox_syscall 0.1.51 (registry+https://github.com/rust-lang/crates.io-index)" = "423e376fffca3dfa06c9e9790a9ccd282fafb3cc6e6397d01dbf64f9bacc6b85"
995+"checksum rayon 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "db6ce3297f9c85e16621bb8cca38a06779ffc31bb8184e1be4bed2be4678a098"
996+"checksum rayon-core 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "08a89b46efaf957e52b18062fb2f4660f8b8a4dde1807ca002690868ef2c85a9"
544−"checksum redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76"
997+"checksum regex 1.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "b5508c1941e4e7cb19965abef075d35a9a8b5cdf0846f30b4050e9b55dc55e87"
545−"checksum regex 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "37e7cbbd370869ce2e8dff25c7018702d10b21a20ef7135316f8daecd6c25b7f"
998+"checksum regex-automata 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "92b73c2a1770c255c240eaa4ee600df1704a38dc3feaa6e949e7fcd4f8dc09f9"
546−"checksum regex-syntax 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)" = "8c2f35eedad5295fdf00a63d7d4b238135723f92b434ec06774dad15c7ab0861"
999+"checksum regex-syntax 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)" = "e734e891f5b408a29efbf8309e656876276f49ab6a6ac208600b4419bd893d90"
547−"checksum rustc-demangle 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)" = "adacaae16d02b6ec37fdc7acfcddf365978de76d1983d3ee22afc260e1ca9619"
1000+"checksum rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a"
548−"checksum scoped-tls 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "332ffa32bf586782a3efaeb58f127980944bbc8c4d6913a86107ac2a5ab24b28"
1001+"checksum ryu 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "bfa8506c1de11c9c4e4c38863ccbe02a305c8188e85a05a784c9e11e1c3910c8"
1002+"checksum same-file 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "585e8ddcedc187886a30fa705c47985c3fa88d06624095856b36ca0b82ff4421"
1003+"checksum scoped-tls 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2"
1004+"checksum scopeguard 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b42e15e59b18a828bbf5c58ea01debb36b9b096346de35d941dcb89009f24a0d"
1005+"checksum semver 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403"
1006+"checksum semver-parser 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
1007+"checksum serde 1.0.114 (registry+https://github.com/rust-lang/crates.io-index)" = "5317f7588f0a5078ee60ef675ef96735a1442132dc645eb1d12c018620ed8cd3"
1008+"checksum serde_cbor 0.11.1 (registry+https://github.com/rust-lang/crates.io-index)" = "1e18acfa2f90e8b735b2836ab8d538de304cbb6729a7360729ea5a895d15a622"
1009+"checksum serde_derive 1.0.114 (registry+https://github.com/rust-lang/crates.io-index)" = "2a0be94b04690fbaed37cddffc5c134bf537c8e3329d53e982fe04c374978f8e"
1010+"checksum serde_json 1.0.57 (registry+https://github.com/rust-lang/crates.io-index)" = "164eacbdb13512ec2745fb09d51fd5b22b0d65ed294a1dcf7285a360c80a675c"
1011+"checksum slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8"
549"checksum sourcefile 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "4bf77cb82ba8453b42b6ae1d692e4cdc92f9a47beaf89a847c8be83f4e328ad3"
1012"checksum sourcefile 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "4bf77cb82ba8453b42b6ae1d692e4cdc92f9a47beaf89a847c8be83f4e328ad3"
550−"checksum syn 0.15.26 (registry+https://github.com/rust-lang/crates.io-index)" = "f92e629aa1d9c827b2bb8297046c1ccffc57c99b947a680d3ccff1f136a3bee9"
1013+"checksum syn 1.0.33 (registry+https://github.com/rust-lang/crates.io-index)" = "e8d5d96e8cbb005d6959f119f773bfaebb5684296108fb32600c00cde305b2cd"
551−"checksum synstructure 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)" = "73687139bf99285483c96ac0add482c3776528beac1d97d444f6e91f203a2015"
1014+"checksum textwrap 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
552−"checksum termcolor 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "4096add70612622289f2fdcdbd5086dc81c1e2675e6ae58d6c4f62a16c6d7f2f"
1015+"checksum tinytemplate 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "6d3dc76004a03cec1c5932bca4cdc2e39aaa798e3f82363dd94f9adf6098c12f"
553−"checksum termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "689a3bdfaab439fd92bc87df5c4c78417d3cbe537487274e9b0b2dce76e92096"
1016+"checksum unicode-segmentation 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0"
554−"checksum thread_local 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c6b53e329000edc2b34dbe8545fd20e55a333362d0a321909685a19bd28c3f1b"
1017+"checksum unicode-width 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "7007dbd421b92cc6e28410fe7362e2e0a2503394908f417b68ec8d1c364c4e20"
555−"checksum ucd-util 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "535c204ee4d8434478593480b8f86ab45ec9aae0e83c568ca81abf0fd0e88f86"
556−"checksum unicode-segmentation 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "aa6024fc12ddfd1c6dbc14a80fa2324d4568849869b779f6bd37e5e4c03344d1"
557−"checksum unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc"
1018+"checksum unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c"
558−"checksum utf8-ranges 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "796f7e48bef87609f7ade7e06495a87d5cd06c7866e6a5cbfceffc558a243737"
559"checksum version_check 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd"
1019"checksum version_check 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd"
1020+"checksum walkdir 2.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "777182bc735b6424e1a57516d35ed72cb8019d85c8c9bf536dccb3445c1a2f7d"
560−"checksum wasm-bindgen 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)" = "0f71ae64fc0c84c202fbaf421f2433a9529adf8ab6b5145fc4bc557da39d006b"
1021+"checksum wasm-bindgen 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)" = "29ae32af33bacd663a9a28241abecf01f2be64e6a185c6139b04f18b6385c5f2"
561−"checksum wasm-bindgen-backend 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)" = "a566cfa6bc9802a13a49a89df84b28f2d3ac92e169c1d30fd4fdd800981ba956"
1022+"checksum wasm-bindgen-backend 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)" = "1845584bd3593442dc0de6e6d9f84454a59a057722f36f005e44665d6ab19d85"
562−"checksum wasm-bindgen-futures 0.3.11 (registry+https://github.com/rust-lang/crates.io-index)" = "3408930b9c2bf6658ecc2f4f902b5f36b1ee99d3c288fe593567f595b8ff4081"
1023+"checksum wasm-bindgen-futures 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)" = "1458706aa1b8fe6898d19433c9f110d93a05d1f22ae6adf55810409a94df34b4"
563−"checksum wasm-bindgen-macro 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)" = "01869df54a1821867d100eb1c351fad50a10248463c3097e990c3ed0fac7233a"
1024+"checksum wasm-bindgen-macro 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)" = "87fcc747e6b73c93d22c947a6334644d22cfec5abd8b66238484dc2b0aeb9fe4"
564−"checksum wasm-bindgen-macro-support 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)" = "1e727ad9d3811bbf0f6d0b8b3cc784169e832ca6ed8a9190587ebfde87893d70"
1025+"checksum wasm-bindgen-macro-support 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)" = "3dc4b3f2c4078c8c4a5f363b92fcf62604c5913cbd16c6ff5aaf0f74ec03f570"
565−"checksum wasm-bindgen-shared 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)" = "77b13edbe385eb9b3f7f17960f6dbf0adcdb8dac422c7f14a6279426c052336f"
1026+"checksum wasm-bindgen-shared 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)" = "ca0b78d6d3be8589b95d1d49cdc0794728ca734adf36d7c9f07e6459508bb53d"
566−"checksum wasm-bindgen-test 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)" = "5d2220139a62fccd897d00f2a3ef953a9fcd2082461bfa50d8a9f9c0c85a6c94"
1027+"checksum wasm-bindgen-test 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "ed5833682054e941db0b7e2b4877b61bbaafd680608e40d268e070d34558b305"
567−"checksum wasm-bindgen-test-macro 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)" = "12201f5a4fe123303e93efbb1f799e9577312c6e53cb00f6539ace9355b70ad5"
1028+"checksum wasm-bindgen-test-macro 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "9c5dcf70d21d0ff0ea5cdb97dba9a9b56ddad4a9bd10c8912eedc55618be022f"
568−"checksum wasm-bindgen-webidl 0.2.28 (registry+https://github.com/rust-lang/crates.io-index)" = "e8acb042c9989ffa6128467193bc7fe046b475a6151b9f31b0cfe7e5b313cc11"
1029+"checksum wasm-bindgen-webidl 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)" = "3126356474ceb717c8fb5549ae387c9fbf4872818454f4d87708bee997214bb5"
569−"checksum web-sys 0.3.11 (registry+https://github.com/rust-lang/crates.io-index)" = "254e5875171dad18fda43e02054d83bc0699d447b72297110bc7c2a647572416"
1030+"checksum web-sys 0.3.32 (registry+https://github.com/rust-lang/crates.io-index)" = "98405c0a2e722ed3db341b4c5b70eb9fe0021621f7350bab76df93b09b649bbf"
570−"checksum weedle 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "26a4c67f132386d965390b8a734d5d10adbcd30eb5cc74bd9229af8b83f10044"
1031+"checksum weedle 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3bb43f70885151e629e2a19ce9e50bd730fd436cfd4b666894c9ce4de9141164"
571−"checksum winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "92c1eb33641e276cfa214a0522acad57be5c56b10cb348b3c5117db75f3ac4b0"
1032+"checksum winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6"
572"checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
1033"checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
573"checksum winapi-util 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7168bab6e1daee33b4557efd0e95d5ca70a03706d39fa5f3fe7a236f584b03c9"
1034"checksum winapi-util 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7168bab6e1daee33b4557efd0e95d5ca70a03706d39fa5f3fe7a236f584b03c9"
574"checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
1035"checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
575−"checksum wincolor 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "561ed901ae465d6185fa7864d63fbd5720d0ef718366c9a4dc83cf6170d7e9ba"
Cargo.toml+67−14
@@ -1,22 +1,59 @@
1[package]
1[package]
2−name = "dodrio"
3−version = "0.1.0"
4authors = ["Nick Fitzgerald <fitzgen@gmail.com>"]
2authors = ["Nick Fitzgerald <fitzgen@gmail.com>"]
3+categories = ["wasm", "web-programming"]
4+description = "A fast, bump-allocated virtual DOM library."
5edition = "2018"
5edition = "2018"
6+keywords = ["virtual-dom", "vdom", "render", "bump"]
7+license = "MPL-2.0"
8+name = "dodrio"
9+readme = "./README.md"
10+repository = "https://github.com/fitzgen/dodrio"
11+version = "0.2.0"
12+
13+[badges]
14+travis-ci = { repository = "fitzgen/dodrio" }
15+
16+[lib]
17+path = "src/lib.rs"
18+bench = false
19+
20+[[test]]
21+name = "web"
22+path = "tests/web/main.rs"
23+required-features = ["xxx-unstable-internal-use-only"]
24+
25+[[bench]]
26+name = "benches"
27+path = "benches/benches.rs"
28+harness = false
29+required-features = ["xxx-unstable-internal-use-only"]
6
30
7[features]
31[features]
8−default = []
32+# Don't rely on this feature! Its behavior will change and it could go away at
33+# any time. This enables monkey-patching DOM methods so that we wrap them and
34+# trace what DOM methods are used and how many times they are invoked when
35+# applying our emitted change lists to the physical DOM. This is useful for
36+# evaluating the differences between how Dodrio applies changes from virtual
37+# nodes to the physical DOM versus how other virtual DOM libraries do the same.
38+xxx-unstable-strace = []
39+
40+# Don't use this feature! It only exists for exposing otherwise-internal APIs
41+# for testing and benchmarks.
42+xxx-unstable-internal-use-only = []
9
43
10[dependencies]
44[dependencies]
11−bumpalo = "2.0.0"
45+bumpalo = "2.6.0"
12−futures = "0.1"
46+cfg-if = "0.1.10"
13−wasm-bindgen = "0.2.34"
47+fxhash = "0.2.1"
14−wasm-bindgen-futures = "0.3.10"
15−js-sys = "0.3.10"
48+js-sys = "0.3.32"
16−log = "0.4.6"
49+log = { version = "0.4.11", optional = true }
50+longest-increasing-subsequence = "0.1.0"
51+serde = { version = "1.0.114", optional = true }
52+wasm-bindgen = "0.2.55"
53+wasm-bindgen-futures = "0.4.5"
17
54
18[dependencies.web-sys]
55[dependencies.web-sys]
19−version = "0.3.10"
56+version = "0.3.32"
20features = [
57features = [
21 "console",
58 "console",
22 "Document",
59 "Document",
@@ -27,11 +64,14 @@ features = [
27]
64]
28
65
29[dev-dependencies]
66[dev-dependencies]
67+console_log = "0.2.0"
68+dodrio-js-api = { version = "=0.2.0", path = "./crates/js-api" }
69+futures = "0.3.5"
70+log = "0.4.11"
30−wasm-bindgen-test = "0.2"
71+wasm-bindgen-test = "0.3.5"
31−console_log = "0.1.2"
32
72
33[dev-dependencies.web-sys]
73[dev-dependencies.web-sys]
34−version = "0.3.10"
74+version = "0.3.32"
35features = [
75features = [
36 "Attr",
76 "Attr",
37 "EventTarget",
77 "EventTarget",
@@ -40,13 +80,26 @@ features = [
40 "NodeList",
80 "NodeList",
41]
81]
42
82
83+[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
84+criterion = "0.3.3"
85+
86+[profile.bench]
87+debug = true
88+
43[profile.release]
89[profile.release]
44−# Tell `rustc` to optimize for small code size.
90+incremental = false
91+lto = true
45opt-level = "s"
92opt-level = "s"
46
93
47[workspace]
94[workspace]
48members = [
95members = [
96+ "./crates/js-api",
49 "./examples/counter",
97 "./examples/counter",
98+ "./examples/game-of-life",
50 "./examples/hello-world",
99 "./examples/hello-world",
51 "./examples/input-form",
100 "./examples/input-form",
101+ "./examples/js-component",
102+ "./examples/moire",
103+ "./examples/sierpinski-triangle",
104+ "./examples/todomvc",
52]
105]
README.md+60−23
@@ -1,15 +1,14 @@
1−# `dodrio`
1+# Dodrio
2
2
3−`dodrio` is an **experimental** virtual DOM library for Rust and WebAssembly. It
3+A fast, bump-allocated virtual DOM library for Rust and WebAssembly. Note that
4−is a proving ground for a bump allocation-based virtual DOM architecture, that I
4+Dodrio is still **experimental**.
5−believe is the best way to take advantage of WebAssembly's strengths in the
6−context of a virtual DOM library.
7
5
8<!-- START doctoc generated TOC please keep comment here to allow auto update -->
6<!-- START doctoc generated TOC please keep comment here to allow auto update -->
9<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
7<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
10
8
11
9
12- [Warning](#warning)
10- [Warning](#warning)
11+- [Examples](#examples)
13- [Design](#design)
12- [Design](#design)
14 - [Bump Allocation](#bump-allocation)
13 - [Bump Allocation](#bump-allocation)
15 - [Change List as Stack Machine](#change-list-as-stack-machine)
14 - [Change List as Stack Machine](#change-list-as-stack-machine)
@@ -19,11 +18,53 @@ context of a virtual DOM library.
19
18
20## Warning
19## Warning
21
20
22−I reiterate that `dodrio` is in a very **experimental** state. It has not
21+I reiterate that Dodrio is in a very **experimental** state. It probably has
23−actually been profiled or tuned for performance yet, so while I think the design
22+bugs, and no one is using it in production.
24−should yield a very fast virtual DOM library, `dodrio` is almost certainly not
23+
25−fast right now. Additionally, it is probably riddled with bugs, and is assuredly
24+## Examples
26−missing features that are critical for actually building Web applications.
25+
26+Here is the classic "Hello, World!" example:
27+
28+```rust
29+struct Hello {
30+ who: String,
31+}
32+
33+impl Render for Hello {
34+ fn render<'a>(&self, cx: &mut RenderContext<a>) -> Node<'a> {
35+ let who = bumpalo::format!(in cx.bump, "Hello, {}!", self.who);
36+ div(cx)
37+ .children([text(who.into_bump_str())])
38+ .finish()
39+ }
40+}
41+```
42+
43+More examples can be found in [the `examples`
44+directory](https://github.com/fitzgen/dodrio/tree/master/examples), including:
45+
46+* [`counter`](https://github.com/fitzgen/dodrio/tree/master/examples/counter):
47+ Incrementing and decrementing a counter.
48+* [`input-form`](https://github.com/fitzgen/dodrio/tree/master/examples/input-form):
49+ Reading an `<input>` and displaying its contents.
50+* [`todomvc`](https://github.com/fitzgen/dodrio/tree/master/examples/todomvc):
51+ An implementation of the infamous TodoMVC application.
52+* [`moire`](https://github.com/fitzgen/dodrio/tree/master/examples/moire): The
53+ WebRender Moiré patterns demo.
54+* [`game-of-life`](https://github.com/fitzgen/dodrio/tree/master/examples/game-of-life):
55+ The Rust and WebAssembly book's Game of Life tutorial rendered with Dodrio
56+ instead of to 2D canvas.
57+* [`js-component`](https://github.com/fitzgen/dodrio/tree/master/examples/js-component):
58+ Defines a rendering component in JavaScript with the `dodrio-js-api` crate.
59+
60+## Cargo Features
61+
62+* `log` &mdash; enable debugging-oriented log messages with the `log` crate's
63+ facade. You still have to initialize a logger for the messages to go anywhere,
64+ such as [`console_log`](https://github.com/iamcodemaker/console_log).
65+
66+* `serde` &mdash; enable `serde::{Serialize, Deserialize}` implementations for
67+ `Cached<R>` where `R` is serializable and deserializable.
27
68
28## Design
69## Design
29
70
@@ -33,7 +74,7 @@ Bump allocation is essentially the fastest method of allocating objects. It has
33constraints, but works particularly well when allocation lifetimes match program
74constraints, but works particularly well when allocation lifetimes match program
34phases. And virtual DOMs are very phase oriented.
75phases. And virtual DOMs are very phase oriented.
35
76
36−`dodrio` maintains three bump allocation arenas:
77+Dodrio maintains three bump allocation arenas:
37
78
381. The newest, most up-to-date virtual DOM. The virtual DOM nodes themselves and
791. The newest, most up-to-date virtual DOM. The virtual DOM nodes themselves and
39 any temporary containers needed while creating them are allocated into this
80 any temporary containers needed while creating them are allocated into this
@@ -46,12 +87,12 @@ phases. And virtual DOMs are very phase oriented.
46
87
47Rendering happens as follows:
88Rendering happens as follows:
48
89
49−* The application state is rendered into bump allocation arena (1).
90+1. The application state is rendered into bump allocation arena (1).
50−* (1) diffed with (2) and the changes are emitted into (3).
91+2. (1) is diffed with (2) and the changes are emitted into (3).
51−* JavaScript code applies the change list in (3) to the physical DOM.
92+3. JavaScript code applies the change list in (3) to the physical DOM.
52−* (1) and (2) are swapped, double-buffering style, and the new (1) has its bump
93+4. (1) and (2) are swapped, double-buffering style, and the new (1) has its bump
53− allocation pointer reset, as does (3).
94+ allocation pointer reset, as does (3).
54−* Rinse and repeat.
95+5. Rinse and repeat.
55
96
56### Change List as Stack Machine
97### Change List as Stack Machine
57
98
@@ -62,10 +103,6 @@ diffs, a task that is essentially a tree traversal.
62
103
63### Library — Not Framework
104### Library — Not Framework
64
105
65−`dodrio` is just a library. (And did I mention it is experimental?!) It is not a
106+Dodrio is just a library. (And did I mention it is experimental?!) It is not a
66full-fledged, complete, batteries-included solution for all frontend Web
107full-fledged, complete, batteries-included solution for all frontend Web
67−development. And it never intends to become that either. Its highest ambition is
108+development. And it never intends to become that either.
68−to prove that its bump allocation-based design is a good one, and maaaayyyyyybe
69−become a production-grade virtual DOM library that you could plug into a larger
70−application or toolkit eventually. But it will never be a complete,
71−batteries-included framework.
benches/benches.rsnew file+118
@@ -0,0 +1,118 @@
1+#![cfg(all(
2+ feature = "xxx-unstable-internal-use-only",
3+ not(target_arch = "wasm32")
4+))]
5+
6+use criterion::{
7+ black_box, criterion_group, criterion_main, Benchmark, Criterion, ParameterizedBenchmark,
8+ Throughput,
9+};
10+use dodrio::{
11+ builder::*,
12+ bumpalo::{self, Bump},
13+ CachedSet, Node, Render, RenderContext, Vdom,
14+};
15+use std::cell::RefCell;
16+use std::convert::TryInto;
17+
18+/// The simplest thing we can render: `<div/>`.
19+struct Empty;
20+impl<'a> Render<'a> for Empty {
21+ fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
22+ div(&cx).finish()
23+ }
24+}
25+
26+/// Render a list that is `self.0` items long, has attributes and listeners.
27+struct SimpleList(usize);
28+impl<'a> Render<'a> for SimpleList {
29+ fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
30+ let mut children = bumpalo::collections::Vec::with_capacity_in(self.0, cx.bump);
31+ children.extend((0..self.0).map(|_| {
32+ li(&cx)
33+ .attr("class", "my-list-item")
34+ .on("click", |_root, _vdom, _event| {
35+ panic!("no one should call this")
36+ })
37+ .children([text("a list item")])
38+ .finish()
39+ }));
40+ ol(&cx).attr("id", "my-list").children(children).finish()
41+ }
42+}
43+
44+fn criterion_benchmark(c: &mut Criterion) {
45+ c.bench(
46+ "render",
47+ Benchmark::new("empty", {
48+ let mut bump = Bump::new();
49+ let cached_set = RefCell::new(CachedSet::default());
50+ let mut templates = Default::default();
51+ move |b| {
52+ bump.reset();
53+ let mut cx = RenderContext::new(&bump, &cached_set, &mut templates);
54+ b.iter(|| {
55+ black_box(Empty.render(&mut cx));
56+ })
57+ }
58+ }),
59+ );
60+
61+ c.bench(
62+ "render",
63+ ParameterizedBenchmark::new(
64+ "list",
65+ {
66+ let mut bump = Bump::new();
67+ let cached_set = RefCell::new(CachedSet::default());
68+ let mut templates = Default::default();
69+ move |b, n| {
70+ bump.reset();
71+ let mut cx = RenderContext::new(&bump, &cached_set, &mut templates);
72+ b.iter(|| {
73+ black_box(SimpleList(*n).render(&mut cx));
74+ })
75+ }
76+ },
77+ vec![100, 1_000, 10_000],
78+ )
79+ .throughput(|n| Throughput::Elements((*n).try_into().unwrap())),
80+ );
81+
82+ c.bench(
83+ "render-and-diff",
84+ ParameterizedBenchmark::new(
85+ "same-list",
86+ |b, &n| {
87+ let vdom = Vdom::new(&(), SimpleList(n));
88+ b.iter(|| {
89+ vdom.immediately_render_and_diff(SimpleList(n));
90+ black_box(&vdom);
91+ })
92+ },
93+ vec![100, 1_000, 10_000],
94+ )
95+ .with_function("empty-to-full-list-to-empty", |b, &n| {
96+ b.iter(|| {
97+ let vdom = Vdom::new(&(), Empty);
98+ vdom.immediately_render_and_diff(SimpleList(n));
99+ black_box(&vdom);
100+ vdom.immediately_render_and_diff(Empty);
101+ black_box(&vdom);
102+ });
103+ })
104+ .with_function("append-one-and-remove-one", |b, &n| {
105+ let vdom = Vdom::new(&(), SimpleList(n));
106+ b.iter(|| {
107+ vdom.immediately_render_and_diff(SimpleList(n + 1));
108+ black_box(&vdom);
109+ vdom.immediately_render_and_diff(SimpleList(n));
110+ black_box(&vdom);
111+ })
112+ })
113+ .throughput(|n| Throughput::Elements((*n).try_into().unwrap())),
114+ );
115+}
116+
117+criterion_group!(benches, criterion_benchmark);
118+criterion_main!(benches);
build.sh+3−10
@@ -3,16 +3,9 @@
3cd $(dirname $0)
3cd $(dirname $0)
4set -eux
4set -eux
5
5
6−function with_dir {
7− pushd $1
8− shift
9− $@
10− popd
11−}
12−
13−cargo fmt --all
6+cargo fmt --all || true
14−cargo check --all --target wasm32-unknown-unknown
7+cargo build --all --target wasm32-unknown-unknown
15
8
16for x in ./examples/*; do
9for x in ./examples/*; do
17− wasm-pack build --target no-modules "$x"
10+ wasm-pack build --target web "$x"
18done
11done
crates/js-api/Cargo.tomlnew file+22
@@ -0,0 +1,22 @@
1+[package]
2+authors = ["Nick Fitzgerald <fitzgen@gmail.com>"]
3+categories = ["wasm", "web-programming"]
4+description = "An API for implementing Dodrio rendering components with JavaScript"
5+edition = "2018"
6+keywords = ["javascript", "js", "vdom", "virtual-dom"]
7+license = "MPL-2.0"
8+name = "dodrio-js-api"
9+repository = "https://github.com/fitzgen/dodrio"
10+version = "0.2.0"
11+
12+[badges]
13+travis-ci = { repository = "fitzgen/dodrio" }
14+
15+[dependencies]
16+dodrio = { version = "=0.2.0", path = "../.." }
17+wasm-bindgen = "0.2.55"
18+js-sys = "0.3.32"
19+wasm-bindgen-futures = "0.4.5"
20+
21+[dev-dependencies]
22+wasm-bindgen-test = "0.3.5"
crates/js-api/src/lib.rsnew file+324
@@ -0,0 +1,324 @@
1+/*!
2+
3+Implementing `dodrio` render components with JavaScript.
4+
5+This crate provides a Rust type `JsRender` that wraps a JavaScript object with a
6+`render` method. `JsRender` implements `dodrio::Render` by calling its wrapped
7+object's `render` method to get a JavaScript virtual DOM represented as a tree
8+of JavaScript values. It then converts this tree of JavaScript values into
9+`dodrio`'s normal bump-allocated virtual DOM representation.
10+
11+This is likely much slower than rendering virtual DOMs directly into the bump
12+allocator from the Rust side of things! Additionally, the shape of the
13+JavaScript virtual DOM is a bit funky and unidiomatic. Keep in mind that this
14+crate exists as a proof of concept for integrating JavaScript components into
15+`dodrio` -- which is itself *also* experimental -- and so this crate definitely
16+has some rough edges.
17+
18+# Example
19+
20+Here is a JavaScript implementation of a rendering component:
21+
22+```javascript
23+class Greeting {
24+ constructor(who) {
25+ this.who = who;
26+ }
27+
28+ render() {
29+ return {
30+ tagName: "p",
31+ attributes: [
32+ {
33+ name: "class",
34+ value: "greeting",
35+ },
36+ ],
37+ listeners: [
38+ {
39+ on: "click",
40+ callback: this.onClick.bind(this),
41+ }
42+ ],
43+ children: [
44+ "Hello, ",
45+ {
46+ tagName: "strong",
47+ children: [this.who],
48+ }
49+ ],
50+ };
51+ }
52+
53+ async onClick(vdom, event) {
54+ // Be more excited!
55+ this.who += "!";
56+
57+ // Schedule a re-render.
58+ await vdom.render();
59+
60+ console.log("re-rendering finished!");
61+ }
62+}
63+```
64+
65+And here is a Rust rendering component that internally uses the JS rendering
66+component:
67+
68+```rust,no_run
69+use dodrio::{Node, Render, RenderContext, Vdom};
70+use dodrio_js_api::JsRender;
71+use js_sys::Object;
72+use wasm_bindgen::prelude::*;
73+
74+#[wasm_bindgen]
75+extern {
76+ // Import the JS `Greeting` class.
77+ #[wasm_bindgen(extends = Object)]
78+ #[derive(Clone, Debug)]
79+ type Greeting;
80+
81+ // And the `Greeting` class's constructor.
82+ #[wasm_bindgen(constructor)]
83+ fn new(who: &str) -> Greeting;
84+}
85+
86+/// This is our Rust rendering component that wraps the JS rendering component.
87+pub struct GreetingViaJs {
88+ js: JsRender,
89+}
90+
91+impl GreetingViaJs {
92+ /// Create a new `GreetingViaJs`, which will internally create a new JS
93+ /// `Greeting`.
94+ pub fn new(who: &str) -> GreetingViaJs {
95+ let js = JsRender::new(Greeting::new(who));
96+ GreetingViaJs { js }
97+ }
98+}
99+
100+/// And finally the `Render` implementation! This adds a `<p>` element and some
101+/// text around whatever the inner JS `Greeting` component renders.
102+impl<'a> Render<'a> for GreetingViaJs {
103+ fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
104+ use dodrio::builder::*;
105+ p(&cx)
106+ .children([
107+ text("JavaScript says: "),
108+ self.js.render(cx),
109+ ])
110+ .finish()
111+ }
112+}
113+```
114+
115+ */
116+#![deny(missing_docs, missing_debug_implementations)]
117+
118+use dodrio::{builder, bumpalo, Node, Render, RenderContext};
119+use js_sys::{Object, Promise, Reflect};
120+use wasm_bindgen::prelude::*;
121+use wasm_bindgen::JsCast;
122+
123+#[wasm_bindgen]
124+extern "C" {
125+ /// A rendering component implemented in JavaScript.
126+ ///
127+ /// The rendering API is a bit duck-typed: any JS object with a `render`
128+ /// method that returns a virtual DOM as JS values with the right shape
129+ /// works.
130+ ///
131+ /// See `JsRender::new` for converting existing JS objects into `JsRender`s.
132+ #[derive(Debug, Clone)]
133+ pub type JsRender;
134+ #[wasm_bindgen(structural, method)]
135+ fn render(this: &JsRender) -> JsValue;
136+
137+ #[wasm_bindgen(extends = Object)]
138+ #[derive(Debug, Clone)]
139+ type Element;
140+ #[wasm_bindgen(structural, getter, method, js_name = tagName)]
141+ fn tag_name(this: &Element) -> String;
142+ #[wasm_bindgen(structural, getter, method)]
143+ fn listeners(this: &Element) -> js_sys::Array;
144+ #[wasm_bindgen(structural, getter, method)]
145+ fn attributes(this: &Element) -> js_sys::Array;
146+ #[wasm_bindgen(structural, getter, method)]
147+ fn children(this: &Element) -> js_sys::Array;
148+
149+ #[wasm_bindgen(extends = Object)]
150+ #[derive(Debug, Clone)]
151+ type Listener;
152+ #[wasm_bindgen(structural, getter, method)]
153+ fn on(this: &Listener) -> String;
154+ #[wasm_bindgen(structural, getter, method)]
155+ fn callback(this: &Listener) -> js_sys::Function;
156+
157+ #[wasm_bindgen(extends = Object)]
158+ #[derive(Debug, Clone)]
159+ type Attribute;
160+ #[wasm_bindgen(structural, getter, method)]
161+ fn name(this: &Attribute) -> String;
162+ #[wasm_bindgen(structural, getter, method)]
163+ fn value(this: &Attribute) -> String;
164+}
165+
166+/// A weak handle to a virtual DOM.
167+///
168+/// This is essentially the same as `dodrio::VdomWeak`, but exposed to
169+/// JavaScript.
170+#[wasm_bindgen]
171+#[derive(Clone, Debug)]
172+pub struct VdomWeak {
173+ inner: dodrio::VdomWeak,
174+}
175+
176+impl VdomWeak {
177+ fn new(inner: dodrio::VdomWeak) -> VdomWeak {
178+ VdomWeak { inner }
179+ }
180+}
181+
182+#[wasm_bindgen]
183+impl VdomWeak {
184+ /// Schedule re-rendering of the virtual DOM. A promise is returned that is
185+ /// resolved after the rendering has happened.
186+ pub fn render(&self) -> Promise {
187+ let future = self.inner.render();
188+
189+ wasm_bindgen_futures::future_to_promise(async move {
190+ if let Err(e) = future.await {
191+ let msg = e.to_string();
192+ Err(js_sys::Error::new(&msg).into())
193+ } else {
194+ Ok(JsValue::null())
195+ }
196+ })
197+ }
198+}
199+
200+impl JsRender {
201+ /// Convert a `js_sys::Object` into a `JsRender`.
202+ ///
203+ /// The given object must have a `render` method that conforms to the
204+ /// duck-typed virtual DOM interface which is described in the crate-level
205+ /// documentation.
206+ pub fn new<O>(object: O) -> JsRender
207+ where
208+ O: Into<Object>,
209+ {
210+ let object = object.into();
211+ debug_assert!(
212+ has_property(&object, "render"),
213+ "JS rendering components must have a `render` method"
214+ );
215+ object.unchecked_into::<JsRender>()
216+ }
217+}
218+
219+impl<'a> Render<'a> for JsRender {
220+ fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
221+ create(cx, self.render())
222+ }
223+}
224+
225+fn has_property(obj: &Object, property: &str) -> bool {
226+ Reflect::has(obj, &property.into()).unwrap_or_default()
227+}
228+
229+fn create<'a>(cx: &mut RenderContext<'a>, val: JsValue) -> Node<'a> {
230+ if let Some(txt) = val.as_string() {
231+ let text = bumpalo::collections::String::from_str_in(&txt, cx.bump);
232+ return builder::text(text.into_bump_str());
233+ }
234+
235+ let elem = val.unchecked_into::<Element>();
236+ debug_assert!(
237+ elem.is_instance_of::<Object>(),
238+ "JS render methods should only return strings for text nodes or objects for elements"
239+ );
240+ debug_assert!(
241+ has_property(&elem, "tagName"),
242+ "element objects returned by JS render methods must have a `tagName` property"
243+ );
244+
245+ let tag_name = elem.tag_name();
246+ let tag_name = bumpalo::collections::String::from_str_in(&tag_name, cx.bump);
247+
248+ builder::ElementBuilder::new(cx.bump, tag_name.into_bump_str())
249+ .listeners({
250+ let mut listeners =
251+ bumpalo::collections::Vec::new_in(cx.bump);
252+ if has_property(&elem, "listeners") {
253+ let js_listeners = elem.listeners();
254+ listeners.reserve(js_listeners.length() as usize);
255+ js_listeners.for_each(&mut |listener, _index, _array| {
256+ let listener = listener.unchecked_into::<Listener>();
257+ debug_assert!(
258+ listener.is_instance_of::<Object>(),
259+ "listeners returned by JS render methods must be objects"
260+ );
261+ debug_assert!(
262+ has_property(&listener, "on"),
263+ "listener objects returned by JS render methods must have an `on` property"
264+ );
265+ debug_assert!(
266+ has_property(&listener, "callback"),
267+ "listener objects returned by JS render methods must have an `callback` property"
268+ );
269+ let on = listener.on();
270+ let on = bumpalo::collections::String::from_str_in(&on, cx.bump);
271+ let callback = listener.callback();
272+ let elem = elem.clone();
273+ listeners.push(builder::on(cx.bump, on.into_bump_str(), move |_root, vdom, event| {
274+ let vdom = VdomWeak::new(vdom);
275+ let vdom: JsValue = vdom.into();
276+ if let Err(e) = callback.call2(&elem, &vdom, &event) {
277+ wasm_bindgen::throw_val(e);
278+ }
279+ }));
280+ });
281+ }
282+ listeners
283+ })
284+ .attributes({
285+ let mut attributes = bumpalo::collections::Vec::new_in(cx.bump);
286+ if has_property(&elem, "attributes") {
287+ let js_attributes = elem.attributes();
288+ attributes.reserve(js_attributes.length() as usize);
289+ js_attributes.for_each(&mut |attribute, _index, _array| {
290+ let attribute = attribute.unchecked_into::<Attribute>();
291+ debug_assert!(
292+ attribute.is_instance_of::<Object>(),
293+ "attributes returned by JS render methods must be objects"
294+ );
295+ debug_assert!(
296+ has_property(&attribute, "name"),
297+ "attribute objects returned by JS render methods must have a `name` property"
298+ );
299+ debug_assert!(
300+ has_property(&attribute, "value"),
301+ "attribute objects returned by JS render methods must have a `value` property"
302+ );
303+ let name = attribute.name();
304+ let name = bumpalo::collections::String::from_str_in(&name, cx.bump);
305+ let value = attribute.value();
306+ let value = bumpalo::collections::String::from_str_in(&value, cx.bump);
307+ attributes.push(builder::attr(name.into_bump_str(), value.into_bump_str()));
308+ });
309+ }
310+ attributes
311+ })
312+ .children({
313+ let mut children = bumpalo::collections::Vec::new_in(cx.bump);
314+ if has_property(&elem, "children") {
315+ let js_children = elem.children();
316+ children.reserve(js_children.length() as usize);
317+ js_children.for_each(&mut |child, _index, _array| {
318+ children.push(create(cx, child));
319+ });
320+ }
321+ children
322+ })
323+ .finish()
324+}
examples/counter/.gitignoredeleted−7
@@ -1,7 +0,0 @@
1−dist
2−node_modules
3−crate/bin
4−crate/pkg
5−crate/target
6−crate/wasm-pack.log
7−crate/Cargo.lock
examples/counter/.travis.ymldeleted−11
@@ -1,11 +0,0 @@
1−language: rust
2−
3−install:
4− - curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.8/install.sh | bash
5− - source ~/.nvm/nvm.sh
6− - nvm install v10.5
7− - curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh -s -- -f
8−
9−script:
10− - npm install
11− - npm run build
examples/counter/Cargo.toml+7−7
@@ -5,19 +5,19 @@ authors = ["Nick Fitzgerald <fitzgen@gmail.com>"]
5edition = "2018"
5edition = "2018"
6
6
7[lib]
7[lib]
8−crate-type = ["cdylib", "rlib"]
8+crate-type = ["cdylib"]
9
9
10[features]
10[features]
11
11
12[dependencies]
12[dependencies]
13−console_error_panic_hook = "0.1.1"
13+console_error_panic_hook = "0.1.6"
14−console_log = "0.1.2"
14+console_log = "0.2.0"
15dodrio = { path = "../.." }
15dodrio = { path = "../.." }
16−log = "0.4.6"
16+log = "0.4.11"
17−wasm-bindgen = "0.2"
17+wasm-bindgen = "0.2.55"
18
18
19[dependencies.web-sys]
19[dependencies.web-sys]
20−version = "0.3.8"
20+version = "0.3.32"
21features = [
21features = [
22 "console",
22 "console",
23 "Document",
23 "Document",
@@ -30,4 +30,4 @@ features = [
30]
30]
31
31
32[dev-dependencies]
32[dev-dependencies]
33−wasm-bindgen-test = "0.2"
33+wasm-bindgen-test = "0.3.5"
examples/counter/LICENSE-APACHEdeleted−201
@@ -1,201 +0,0 @@
1− Apache License
2− Version 2.0, January 2004
3− http://www.apache.org/licenses/
4−
5−TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6−
7−1. Definitions.
8−
9− "License" shall mean the terms and conditions for use, reproduction,
10− and distribution as defined by Sections 1 through 9 of this document.
11−
12− "Licensor" shall mean the copyright owner or entity authorized by
13− the copyright owner that is granting the License.
14−
15− "Legal Entity" shall mean the union of the acting entity and all
16− other entities that control, are controlled by, or are under common
17− control with that entity. For the purposes of this definition,
18− "control" means (i) the power, direct or indirect, to cause the
19− direction or management of such entity, whether by contract or
20− otherwise, or (ii) ownership of fifty percent (50%) or more of the
21− outstanding shares, or (iii) beneficial ownership of such entity.
22−
23− "You" (or "Your") shall mean an individual or Legal Entity
24− exercising permissions granted by this License.
25−
26− "Source" form shall mean the preferred form for making modifications,
27− including but not limited to software source code, documentation
28− source, and configuration files.
29−
30− "Object" form shall mean any form resulting from mechanical
31− transformation or translation of a Source form, including but
32− not limited to compiled object code, generated documentation,
33− and conversions to other media types.
34−
35− "Work" shall mean the work of authorship, whether in Source or
36− Object form, made available under the License, as indicated by a
37− copyright notice that is included in or attached to the work
38− (an example is provided in the Appendix below).
39−
40− "Derivative Works" shall mean any work, whether in Source or Object
41− form, that is based on (or derived from) the Work and for which the
42− editorial revisions, annotations, elaborations, or other modifications
43− represent, as a whole, an original work of authorship. For the purposes
44− of this License, Derivative Works shall not include works that remain
45− separable from, or merely link (or bind by name) to the interfaces of,
46− the Work and Derivative Works thereof.
47−
48− "Contribution" shall mean any work of authorship, including
49− the original version of the Work and any modifications or additions
50− to that Work or Derivative Works thereof, that is intentionally
51− submitted to Licensor for inclusion in the Work by the copyright owner
52− or by an individual or Legal Entity authorized to submit on behalf of
53− the copyright owner. For the purposes of this definition, "submitted"
54− means any form of electronic, verbal, or written communication sent
55− to the Licensor or its representatives, including but not limited to
56− communication on electronic mailing lists, source code control systems,
57− and issue tracking systems that are managed by, or on behalf of, the
58− Licensor for the purpose of discussing and improving the Work, but
59− excluding communication that is conspicuously marked or otherwise
60− designated in writing by the copyright owner as "Not a Contribution."
61−
62− "Contributor" shall mean Licensor and any individual or Legal Entity
63− on behalf of whom a Contribution has been received by Licensor and
64− subsequently incorporated within the Work.
65−
66−2. Grant of Copyright License. Subject to the terms and conditions of
67− this License, each Contributor hereby grants to You a perpetual,
68− worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69− copyright license to reproduce, prepare Derivative Works of,
70− publicly display, publicly perform, sublicense, and distribute the
71− Work and such Derivative Works in Source or Object form.
72−
73−3. Grant of Patent License. Subject to the terms and conditions of
74− this License, each Contributor hereby grants to You a perpetual,
75− worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76− (except as stated in this section) patent license to make, have made,
77− use, offer to sell, sell, import, and otherwise transfer the Work,
78− where such license applies only to those patent claims licensable
79− by such Contributor that are necessarily infringed by their
80− Contribution(s) alone or by combination of their Contribution(s)
81− with the Work to which such Contribution(s) was submitted. If You
82− institute patent litigation against any entity (including a
83− cross-claim or counterclaim in a lawsuit) alleging that the Work
84− or a Contribution incorporated within the Work constitutes direct
85− or contributory patent infringement, then any patent licenses
86− granted to You under this License for that Work shall terminate
87− as of the date such litigation is filed.
88−
89−4. Redistribution. You may reproduce and distribute copies of the
90− Work or Derivative Works thereof in any medium, with or without
91− modifications, and in Source or Object form, provided that You
92− meet the following conditions:
93−
94− (a) You must give any other recipients of the Work or
95− Derivative Works a copy of this License; and
96−
97− (b) You must cause any modified files to carry prominent notices
98− stating that You changed the files; and
99−
100− (c) You must retain, in the Source form of any Derivative Works
101− that You distribute, all copyright, patent, trademark, and
102− attribution notices from the Source form of the Work,
103− excluding those notices that do not pertain to any part of
104− the Derivative Works; and
105−
106− (d) If the Work includes a "NOTICE" text file as part of its
107− distribution, then any Derivative Works that You distribute must
108− include a readable copy of the attribution notices contained
109− within such NOTICE file, excluding those notices that do not
110− pertain to any part of the Derivative Works, in at least one
111− of the following places: within a NOTICE text file distributed
112− as part of the Derivative Works; within the Source form or
113− documentation, if provided along with the Derivative Works; or,
114− within a display generated by the Derivative Works, if and
115− wherever such third-party notices normally appear. The contents
116− of the NOTICE file are for informational purposes only and
117− do not modify the License. You may add Your own attribution
118− notices within Derivative Works that You distribute, alongside
119− or as an addendum to the NOTICE text from the Work, provided
120− that such additional attribution notices cannot be construed
121− as modifying the License.
122−
123− You may add Your own copyright statement to Your modifications and
124− may provide additional or different license terms and conditions
125− for use, reproduction, or distribution of Your modifications, or
126− for any such Derivative Works as a whole, provided Your use,
127− reproduction, and distribution of the Work otherwise complies with
128− the conditions stated in this License.
129−
130−5. Submission of Contributions. Unless You explicitly state otherwise,
131− any Contribution intentionally submitted for inclusion in the Work
132− by You to the Licensor shall be under the terms and conditions of
133− this License, without any additional terms or conditions.
134− Notwithstanding the above, nothing herein shall supersede or modify
135− the terms of any separate license agreement you may have executed
136− with Licensor regarding such Contributions.
137−
138−6. Trademarks. This License does not grant permission to use the trade
139− names, trademarks, service marks, or product names of the Licensor,
140− except as required for reasonable and customary use in describing the
141− origin of the Work and reproducing the content of the NOTICE file.
142−
143−7. Disclaimer of Warranty. Unless required by applicable law or
144− agreed to in writing, Licensor provides the Work (and each
145− Contributor provides its Contributions) on an "AS IS" BASIS,
146− WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147− implied, including, without limitation, any warranties or conditions
148− of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149− PARTICULAR PURPOSE. You are solely responsible for determining the
150− appropriateness of using or redistributing the Work and assume any
151− risks associated with Your exercise of permissions under this License.
152−
153−8. Limitation of Liability. In no event and under no legal theory,
154− whether in tort (including negligence), contract, or otherwise,
155− unless required by applicable law (such as deliberate and grossly
156− negligent acts) or agreed to in writing, shall any Contributor be
157− liable to You for damages, including any direct, indirect, special,
158− incidental, or consequential damages of any character arising as a
159− result of this License or out of the use or inability to use the
160− Work (including but not limited to damages for loss of goodwill,
161− work stoppage, computer failure or malfunction, or any and all
162− other commercial damages or losses), even if such Contributor
163− has been advised of the possibility of such damages.
164−
165−9. Accepting Warranty or Additional Liability. While redistributing
166− the Work or Derivative Works thereof, You may choose to offer,
167− and charge a fee for, acceptance of support, warranty, indemnity,
168− or other liability obligations and/or rights consistent with this
169− License. However, in accepting such obligations, You may act only
170− on Your own behalf and on Your sole responsibility, not on behalf
171− of any other Contributor, and only if You agree to indemnify,
172− defend, and hold each Contributor harmless for any liability
173− incurred by, or claims asserted against, such Contributor by reason
174− of your accepting any such warranty or additional liability.
175−
176−END OF TERMS AND CONDITIONS
177−
178−APPENDIX: How to apply the Apache License to your work.
179−
180− To apply the Apache License to your work, attach the following
181− boilerplate notice, with the fields enclosed by brackets "[]"
182− replaced with your own identifying information. (Don't include
183− the brackets!) The text should be enclosed in the appropriate
184− comment syntax for the file format. We also recommend that a
185− file or class name and description of purpose be included on the
186− same "printed page" as the copyright notice for easier
187− identification within third-party archives.
188−
189−Copyright [yyyy] [name of copyright owner]
190−
191−Licensed under the Apache License, Version 2.0 (the "License");
192−you may not use this file except in compliance with the License.
193−You may obtain a copy of the License at
194−
195− http://www.apache.org/licenses/LICENSE-2.0
196−
197−Unless required by applicable law or agreed to in writing, software
198−distributed under the License is distributed on an "AS IS" BASIS,
199−WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200−See the License for the specific language governing permissions and
201−limitations under the License.
examples/counter/LICENSE-MITdeleted−25
@@ -1,25 +0,0 @@
1−Copyright (c) 2015 The Rust Project Developers
2−
3−Permission is hereby granted, free of charge, to any
4−person obtaining a copy of this software and associated
5−documentation files (the "Software"), to deal in the
6−Software without restriction, including without
7−limitation the rights to use, copy, modify, merge,
8−publish, distribute, sublicense, and/or sell copies of
9−the Software, and to permit persons to whom the Software
10−is furnished to do so, subject to the following
11−conditions:
12−
13−The above copyright notice and this permission notice
14−shall be included in all copies or substantial portions
15−of the Software.
16−
17−THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
18−ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
19−TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
20−PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
21−SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
22−CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
23−OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
24−IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
25−DEALINGS IN THE SOFTWARE.
examples/counter/README.md+12−20
@@ -1,29 +1,21 @@
1−# 🦀🕸 `rust-webpack-template`
1+# Counter
2
2
3−> **Kickstart your Rust, WebAssembly, and Webpack project!**
3+A counter that can be incremented and decremented.
4
4
5−This template is designed for creating monorepo-style Web applications with
5+## Source
6−Rust-generated WebAssembly and Webpack without publishing your wasm to NPM.
7
6
8−* Want to create and publish NPM packages with Rust and WebAssembly? [Check out
7+See `src/lib.rs`.
9− `wasm-pack-template`.](https://github.com/rustwasm/wasm-pack-template)
10
8
11−## 🔋 Batteries Included
9+## Build
12
10
13−This template comes pre-configured with all the boilerplate for compiling Rust
11+```
14−to WebAssembly and hooking into a Webpack build pipeline.
12+wasm-pack build --target web
15−
13+```
16−* `npm run start` -- Serve the project locally for development at
17− `http://localhost:8080`.
18−
19−* `npm run build` -- Bundle the project (in production mode).
20−
21−## 🚴 Using This Template
22
14
23−First, [install `wasm-pack`!](https://rustwasm.github.io/wasm-pack/installer/)
15+## Serve
24
16
25−Then, use `npm init` to clone this template:
17+Use any HTTP server, for example:
26
18
27−```sh
19+```
28−npm init rust-webpack my-app
20+python -m SimpleHTTPServer
29```
21```
examples/counter/index.html+3−3
@@ -5,9 +5,9 @@
5 <title>Counter</title>
5 <title>Counter</title>
6 </head>
6 </head>
7 <body>
7 <body>
8+ <script type="module">
8− <script src="pkg/dodrio_counter.js"></script>
9+ import init from "./pkg/dodrio_counter.js";
9− <script>
10− wasm_bindgen("pkg/dodrio_counter_bg.wasm");
10+ init("./pkg/dodrio_counter_bg.wasm");
11 </script>
11 </script>
12 </body>
12 </body>
13</html>
13</html>
examples/counter/src/lib.rsmode 100644 → 100755+46−39
@@ -1,70 +1,77 @@
1−use dodrio::bumpalo::{self, Bump};
1+use dodrio::{bumpalo, Node, Render, RenderContext};
2−use dodrio::{on, Node, Render};
3use log::*;
2use log::*;
4use wasm_bindgen::prelude::*;
3use wasm_bindgen::prelude::*;
5
4
5+/// A counter that can be incremented and decremented!
6struct Counter {
6struct Counter {
7− val: isize,
7+ count: isize,
8}
8}
9
9
10impl Counter {
10impl Counter {
11+ /// Construct a new, zeroed counter.
11 fn new() -> Counter {
12 fn new() -> Counter {
12− Counter { val: 0 }
13+ Counter { count: 0 }
13 }
14 }
14
15
16+ /// Increment this counter's count.
15 fn increment(&mut self) {
17 fn increment(&mut self) {
16− self.val += 1;
18+ self.count += 1;
17 }
19 }
18
20
21+ /// Decrement this counter's count.
19 fn decrement(&mut self) {
22 fn decrement(&mut self) {
20− self.val -= 1;
23+ self.count -= 1;
21 }
24 }
22}
25}
23
26
27+// The `Render` implementation for `Counter`s displays the current count and has
28+// buttons to increment and decrement the count.
24−impl Render for Counter {
29+impl<'a> Render<'a> for Counter {
25− fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> dodrio::Node<'bump>
30+ fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
26− where
31+ use dodrio::builder::*;
27− 'a: 'bump,
28− {
29− let val = bumpalo::format!(in bump, "{}", self.val);
30
32
31− Node::element(
33+ // Stringify the count as a bump-allocated string.
32− bump,
34+ let count = bumpalo::format!(in cx.bump, "{}", self.count);
35+
33− "div",
36+ div(&cx)
34− [],
37+ .children([
35− [],
36− [
37− Node::element(
38− bump,
39− "button",
38+ button(&cx)
40− [on(bump, "click", |root, vdom, _event| {
39+ .on("click", |root, vdom, _event| {
40+ // Cast the root render component to a `Counter`, since
41+ // we know that's what it is.
41− root.unwrap_mut::<Counter>().increment();
42+ let counter = root.unwrap_mut::<Counter>();
43+
44+ // Increment the counter.
45+ counter.increment();
46+
47+ // Since the count has updated, we should re-render the
48+ // counter on the next animation frame.
42 vdom.schedule_render();
49 vdom.schedule_render();
43− })],
50+ })
44− [],
45− [Node::text("+")],
51+ .children([text("+")])
46− ),
52+ .finish(),
47− Node::text(val.into_bump_str()),
53+ text(count.into_bump_str()),
48− Node::element(
49− bump,
50− "button",
54+ button(&cx)
51− [on(bump, "click", |root, vdom, _event| {
55+ .on("click", |root, vdom, _event| {
56+ // Same as above, but decrementing instead of incrementing.
52− root.unwrap_mut::<Counter>().decrement();
57+ let counter = root.unwrap_mut::<Counter>();
58+ counter.decrement();
53 vdom.schedule_render();
59 vdom.schedule_render();
54− })],
60+ })
55− [],
56− [Node::text("-")],
61+ .children([text("-")])
57− ),
62+ .finish(),
58− ],
63+ ])
59− )
64+ .finish()
60 }
65 }
61}
66}
62
67
63#[wasm_bindgen(start)]
68#[wasm_bindgen(start)]
64pub fn run() {
69pub fn run() {
70+ // Initialize debug logging for if/when things go wrong.
65 console_error_panic_hook::set_once();
71 console_error_panic_hook::set_once();
66 console_log::init_with_level(Level::Trace).expect("should initialize logging OK");
72 console_log::init_with_level(Level::Trace).expect("should initialize logging OK");
67
73
74+ // Get the document's `<body>`.
68 let window = web_sys::window().unwrap();
75 let window = web_sys::window().unwrap();
69 let document = window.document().unwrap();
76 let document = window.document().unwrap();
70 let body = document.body().unwrap();
77 let body = document.body().unwrap();
examples/game-of-life/Cargo.tomlnew file+29
@@ -0,0 +1,29 @@
1+[package]
2+name = "dodrio-game-of-life"
3+version = "0.1.0"
4+authors = ["Nick Fitzgerald <fitzgen@gmail.com>"]
5+edition = "2018"
6+
7+[lib]
8+crate-type = ["cdylib"]
9+
10+[features]
11+
12+[dependencies]
13+dodrio = { path = "../.." }
14+wasm-bindgen = "0.2.55"
15+console_error_panic_hook = "0.1.6"
16+js-sys = "0.3.32"
17+wasm-bindgen-futures = "0.4.5"
18+
19+[dependencies.web-sys]
20+version = "0.3.32"
21+features = [
22+ "Document",
23+ "HtmlElement",
24+ "Node",
25+ "Window",
26+]
27+
28+[dev-dependencies]
29+wasm-bindgen-test = "0.3.5"
examples/game-of-life/README.mdnew file+23
@@ -0,0 +1,23 @@
1+# Conway's Game of Life
2+
3+A port of the [Rust and WebAssembly tutorial's Game of Life
4+implementation](https://rustwasm.github.io/book/game-of-life/introduction.html)
5+to rendering to HTML with Dodrio instead of to a canvas via JavaScript.
6+
7+## Source
8+
9+See `src/lib.rs`.
10+
11+## Build
12+
13+```
14+wasm-pack build --target web
15+```
16+
17+## Serve
18+
19+Use any HTTP server, for example:
20+
21+```
22+python -m SimpleHTTPServer
23+```
examples/game-of-life/index.htmlnew file+44
@@ -0,0 +1,44 @@
1+<!DOCTYPE html>
2+<html>
3+ <head>
4+ <meta http-equiv="Content-type" content="text/html; charset=utf-8"/>
5+ <title>Conway's Game of Life</title>
6+ <style>
7+ .cell {
8+ display: inline-block;
9+ width: 1vh;
10+ height: 1vh;
11+ border: 1px #cccccc solid;
12+ margin: -1px;
13+ padding: 0;
14+ }
15+
16+ .row {
17+ height: 1vh;
18+ margin: 0;
19+ padding: 0;
20+ }
21+
22+ body {
23+ width: 100vw;
24+ height: 100vh;
25+ display: flex;
26+ align-items: center;
27+ justify-content: center;
28+ padding: 0;
29+ margin: 0;
30+ }
31+
32+ html {
33+ padding: 0;
34+ margin: 0;
35+ }
36+ </style>
37+ </head>
38+ <body>
39+ <script type="module">
40+ import init from "./pkg/dodrio_game_of_life.js";
41+ init("./pkg/dodrio_game_of_life_bg.wasm");
42+ </script>
43+ </body>
44+</html>
examples/game-of-life/src/lib.rsnew file+218
@@ -0,0 +1,218 @@
1+use dodrio::{bumpalo, Node, Render, RenderContext, Vdom};
2+use std::cell::RefCell;
3+use std::rc::Rc;
4+use wasm_bindgen::{prelude::*, JsCast};
5+
6+#[repr(u8)]
7+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
8+pub enum Cell {
9+ Dead = 0,
10+ Alive = 1,
11+}
12+
13+/// The universe containing all of the cells for our Game of Life.
14+struct Universe {
15+ width: u32,
16+ height: u32,
17+ cells: Vec<Cell>,
18+}
19+
20+impl Universe {
21+ /// Construct a new universe.
22+ pub fn new() -> Universe {
23+ let width = 64;
24+ let height = 64;
25+
26+ let cells = (0..width * height)
27+ .map(|i| {
28+ if i % 2 == 0 || i % 7 == 0 {
29+ Cell::Alive
30+ } else {
31+ Cell::Dead
32+ }
33+ })
34+ .collect();
35+
36+ Universe {
37+ width,
38+ height,
39+ cells,
40+ }
41+ }
42+
43+ /// Compute one tick of the universe.
44+ pub fn tick(&mut self) {
45+ let mut next = self.cells.clone();
46+
47+ for row in 0..self.height {
48+ for col in 0..self.width {
49+ let idx = self.get_index(row, col);
50+ let cell = self.cells[idx];
51+ let live_neighbors = self.live_neighbor_count(row, col);
52+
53+ let next_cell = match (cell, live_neighbors) {
54+ // Rule 1: Any live cell with fewer than two live neighbours
55+ // dies, as if caused by underpopulation.
56+ (Cell::Alive, x) if x < 2 => Cell::Dead,
57+ // Rule 2: Any live cell with two or three live neighbours
58+ // lives on to the next generation.
59+ (Cell::Alive, 2) | (Cell::Alive, 3) => Cell::Alive,
60+ // Rule 3: Any live cell with more than three live
61+ // neighbours dies, as if by overpopulation.
62+ (Cell::Alive, x) if x > 3 => Cell::Dead,
63+ // Rule 4: Any dead cell with exactly three live neighbours
64+ // becomes a live cell, as if by reproduction.
65+ (Cell::Dead, 3) => Cell::Alive,
66+ // All other cells remain in the same state.
67+ (otherwise, _) => otherwise,
68+ };
69+
70+ next[idx] = next_cell;
71+ }
72+ }
73+
74+ self.cells = next;
75+ }
76+
77+ fn get_index(&self, row: u32, column: u32) -> usize {
78+ (row * self.width + column) as usize
79+ }
80+
81+ fn live_neighbor_count(&self, row: u32, column: u32) -> u8 {
82+ let mut count = 0;
83+
84+ let north = if row == 0 { self.height - 1 } else { row - 1 };
85+
86+ let south = if row == self.height - 1 { 0 } else { row + 1 };
87+
88+ let west = if column == 0 {
89+ self.width - 1
90+ } else {
91+ column - 1
92+ };
93+
94+ let east = if column == self.width - 1 {
95+ 0
96+ } else {
97+ column + 1
98+ };
99+
100+ let nw = self.get_index(north, west);
101+ count += self.cells[nw] as u8;
102+
103+ let n = self.get_index(north, column);
104+ count += self.cells[n] as u8;
105+
106+ let ne = self.get_index(north, east);
107+ count += self.cells[ne] as u8;
108+
109+ let w = self.get_index(row, west);
110+ count += self.cells[w] as u8;
111+
112+ let e = self.get_index(row, east);
113+ count += self.cells[e] as u8;
114+
115+ let sw = self.get_index(south, west);
116+ count += self.cells[sw] as u8;
117+
118+ let s = self.get_index(south, column);
119+ count += self.cells[s] as u8;
120+
121+ let se = self.get_index(south, east);
122+ count += self.cells[se] as u8;
123+
124+ count
125+ }
126+}
127+
128+/// The rendering implementation for our Game of Life.
129+impl<'a> Render<'a> for Universe {
130+ fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
131+ use dodrio::builder::*;
132+
133+ let mut rows = bumpalo::collections::Vec::with_capacity_in(self.height as usize, cx.bump);
134+
135+ for row in self.cells.chunks(self.width as usize) {
136+ let mut cells =
137+ bumpalo::collections::Vec::with_capacity_in(self.width as usize, cx.bump);
138+
139+ for cell in row {
140+ cells.push(
141+ span(&cx)
142+ .attr("class", "cell")
143+ .attr(
144+ "style",
145+ match cell {
146+ Cell::Alive => "background-color: black",
147+ Cell::Dead => "background-color: white",
148+ },
149+ )
150+ .finish(),
151+ );
152+ }
153+
154+ rows.push(div(&cx).attr("class", "row").children(cells).finish());
155+ }
156+
157+ div(&cx).attr("class", "universe").children(rows).finish()
158+ }
159+}
160+
161+#[wasm_bindgen(start)]
162+pub fn run() {
163+ // Set up the panic hook for debugging when things go wrong.
164+ console_error_panic_hook::set_once();
165+
166+ // Grab the document's `<body>`.
167+ let window = web_sys::window().unwrap_throw();
168+ let document = window.document().unwrap_throw();
169+ let body = document.body().unwrap_throw();
170+
171+ // Create a new Game of Life `Universe` render component.
172+ let universe = Universe::new();
173+
174+ // Create a virtual DOM and mount it and the `Universe` render component to the
175+ // `<body>`.
176+ let vdom = Vdom::new(body.as_ref(), universe);
177+
178+ // Kick off a loop that keeps computing a tick of the universe and then
179+ // re-rendering on every animation frame.
180+ let rc = <Rc<RefCell<Option<Closure<dyn FnMut()>>>>>::default();
181+ let rc2 = rc.clone();
182+ let window2 = window.clone();
183+ let weak = vdom.weak();
184+ let f = Closure::wrap(Box::new(move || {
185+ weak.schedule_render();
186+
187+ wasm_bindgen_futures::spawn_local({
188+ let weak = weak.clone();
189+ async move {
190+ let fut = weak.with_component(|root| {
191+ let universe = root.unwrap_mut::<Universe>();
192+ universe.tick();
193+ });
194+
195+ if fut.await.is_err() {
196+ wasm_bindgen::throw_str("impossible, we always keep the vdom alive");
197+ }
198+ }
199+ });
200+
201+ window
202+ .request_animation_frame(
203+ rc.borrow()
204+ .as_ref()
205+ .unwrap_throw()
206+ .as_ref()
207+ .unchecked_ref::<js_sys::Function>(),
208+ )
209+ .unwrap_throw();
210+ }) as Box<dyn FnMut()>);
211+ window2
212+ .request_animation_frame(f.as_ref().unchecked_ref::<js_sys::Function>())
213+ .unwrap_throw();
214+ *rc2.borrow_mut() = Some(f);
215+
216+ // Run the virtual DOM forever and don't unmount it.
217+ vdom.forget();
218+}
examples/hello-world/.gitignoredeleted−7
@@ -1,7 +0,0 @@
1−dist
2−node_modules
3−crate/bin
4−crate/pkg
5−crate/target
6−crate/wasm-pack.log
7−crate/Cargo.lock
examples/hello-world/.travis.ymldeleted−11
@@ -1,11 +0,0 @@
1−language: rust
2−
3−install:
4− - curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.8/install.sh | bash
5− - source ~/.nvm/nvm.sh
6− - nvm install v10.5
7− - curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh -s -- -f
8−
9−script:
10− - npm install
11− - npm run build
examples/hello-world/Cargo.toml+5−5
@@ -5,17 +5,17 @@ authors = ["Nick Fitzgerald <fitzgen@gmail.com>"]
5edition = "2018"
5edition = "2018"
6
6
7[lib]
7[lib]
8−crate-type = ["cdylib", "rlib"]
8+crate-type = ["cdylib"]
9
9
10[features]
10[features]
11
11
12[dependencies]
12[dependencies]
13dodrio = { path = "../.." }
13dodrio = { path = "../.." }
14−wasm-bindgen = "0.2"
14+wasm-bindgen = "0.2.55"
15−console_error_panic_hook = "0.1.1"
15+console_error_panic_hook = "0.1.6"
16
16
17[dependencies.web-sys]
17[dependencies.web-sys]
18−version = "0.3.8"
18+version = "0.3.32"
19features = [
19features = [
20 "Document",
20 "Document",
21 "HtmlElement",
21 "HtmlElement",
@@ -24,4 +24,4 @@ features = [
24]
24]
25
25
26[dev-dependencies]
26[dev-dependencies]
27−wasm-bindgen-test = "0.2"
27+wasm-bindgen-test = "0.3.5"
examples/hello-world/LICENSE-APACHEdeleted−201
@@ -1,201 +0,0 @@
1− Apache License
2− Version 2.0, January 2004
3− http://www.apache.org/licenses/
4−
5−TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6−
7−1. Definitions.
8−
9− "License" shall mean the terms and conditions for use, reproduction,
10− and distribution as defined by Sections 1 through 9 of this document.
11−
12− "Licensor" shall mean the copyright owner or entity authorized by
13− the copyright owner that is granting the License.
14−
15− "Legal Entity" shall mean the union of the acting entity and all
16− other entities that control, are controlled by, or are under common
17− control with that entity. For the purposes of this definition,
18− "control" means (i) the power, direct or indirect, to cause the
19− direction or management of such entity, whether by contract or
20− otherwise, or (ii) ownership of fifty percent (50%) or more of the
21− outstanding shares, or (iii) beneficial ownership of such entity.
22−
23− "You" (or "Your") shall mean an individual or Legal Entity
24− exercising permissions granted by this License.
25−
26− "Source" form shall mean the preferred form for making modifications,
27− including but not limited to software source code, documentation
28− source, and configuration files.
29−
30− "Object" form shall mean any form resulting from mechanical
31− transformation or translation of a Source form, including but
32− not limited to compiled object code, generated documentation,
33− and conversions to other media types.
34−
35− "Work" shall mean the work of authorship, whether in Source or
36− Object form, made available under the License, as indicated by a
37− copyright notice that is included in or attached to the work
38− (an example is provided in the Appendix below).
39−
40− "Derivative Works" shall mean any work, whether in Source or Object
41− form, that is based on (or derived from) the Work and for which the
42− editorial revisions, annotations, elaborations, or other modifications
43− represent, as a whole, an original work of authorship. For the purposes
44− of this License, Derivative Works shall not include works that remain
45− separable from, or merely link (or bind by name) to the interfaces of,
46− the Work and Derivative Works thereof.
47−
48− "Contribution" shall mean any work of authorship, including
49− the original version of the Work and any modifications or additions
50− to that Work or Derivative Works thereof, that is intentionally
51− submitted to Licensor for inclusion in the Work by the copyright owner
52− or by an individual or Legal Entity authorized to submit on behalf of
53− the copyright owner. For the purposes of this definition, "submitted"
54− means any form of electronic, verbal, or written communication sent
55− to the Licensor or its representatives, including but not limited to
56− communication on electronic mailing lists, source code control systems,
57− and issue tracking systems that are managed by, or on behalf of, the
58− Licensor for the purpose of discussing and improving the Work, but
59− excluding communication that is conspicuously marked or otherwise
60− designated in writing by the copyright owner as "Not a Contribution."
61−
62− "Contributor" shall mean Licensor and any individual or Legal Entity
63− on behalf of whom a Contribution has been received by Licensor and
64− subsequently incorporated within the Work.
65−
66−2. Grant of Copyright License. Subject to the terms and conditions of
67− this License, each Contributor hereby grants to You a perpetual,
68− worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69− copyright license to reproduce, prepare Derivative Works of,
70− publicly display, publicly perform, sublicense, and distribute the
71− Work and such Derivative Works in Source or Object form.
72−
73−3. Grant of Patent License. Subject to the terms and conditions of
74− this License, each Contributor hereby grants to You a perpetual,
75− worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76− (except as stated in this section) patent license to make, have made,
77− use, offer to sell, sell, import, and otherwise transfer the Work,
78− where such license applies only to those patent claims licensable
79− by such Contributor that are necessarily infringed by their
80− Contribution(s) alone or by combination of their Contribution(s)
81− with the Work to which such Contribution(s) was submitted. If You
82− institute patent litigation against any entity (including a
83− cross-claim or counterclaim in a lawsuit) alleging that the Work
84− or a Contribution incorporated within the Work constitutes direct
85− or contributory patent infringement, then any patent licenses
86− granted to You under this License for that Work shall terminate
87− as of the date such litigation is filed.
88−
89−4. Redistribution. You may reproduce and distribute copies of the
90− Work or Derivative Works thereof in any medium, with or without
91− modifications, and in Source or Object form, provided that You
92− meet the following conditions:
93−
94− (a) You must give any other recipients of the Work or
95− Derivative Works a copy of this License; and
96−
97− (b) You must cause any modified files to carry prominent notices
98− stating that You changed the files; and
99−
100− (c) You must retain, in the Source form of any Derivative Works
101− that You distribute, all copyright, patent, trademark, and
102− attribution notices from the Source form of the Work,
103− excluding those notices that do not pertain to any part of
104− the Derivative Works; and
105−
106− (d) If the Work includes a "NOTICE" text file as part of its
107− distribution, then any Derivative Works that You distribute must
108− include a readable copy of the attribution notices contained
109− within such NOTICE file, excluding those notices that do not
110− pertain to any part of the Derivative Works, in at least one
111− of the following places: within a NOTICE text file distributed
112− as part of the Derivative Works; within the Source form or
113− documentation, if provided along with the Derivative Works; or,
114− within a display generated by the Derivative Works, if and
115− wherever such third-party notices normally appear. The contents
116− of the NOTICE file are for informational purposes only and
117− do not modify the License. You may add Your own attribution
118− notices within Derivative Works that You distribute, alongside
119− or as an addendum to the NOTICE text from the Work, provided
120− that such additional attribution notices cannot be construed
121− as modifying the License.
122−
123− You may add Your own copyright statement to Your modifications and
124− may provide additional or different license terms and conditions
125− for use, reproduction, or distribution of Your modifications, or
126− for any such Derivative Works as a whole, provided Your use,
127− reproduction, and distribution of the Work otherwise complies with
128− the conditions stated in this License.
129−
130−5. Submission of Contributions. Unless You explicitly state otherwise,
131− any Contribution intentionally submitted for inclusion in the Work
132− by You to the Licensor shall be under the terms and conditions of
133− this License, without any additional terms or conditions.
134− Notwithstanding the above, nothing herein shall supersede or modify
135− the terms of any separate license agreement you may have executed
136− with Licensor regarding such Contributions.
137−
138−6. Trademarks. This License does not grant permission to use the trade
139− names, trademarks, service marks, or product names of the Licensor,
140− except as required for reasonable and customary use in describing the
141− origin of the Work and reproducing the content of the NOTICE file.
142−
143−7. Disclaimer of Warranty. Unless required by applicable law or
144− agreed to in writing, Licensor provides the Work (and each
145− Contributor provides its Contributions) on an "AS IS" BASIS,
146− WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147− implied, including, without limitation, any warranties or conditions
148− of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149− PARTICULAR PURPOSE. You are solely responsible for determining the
150− appropriateness of using or redistributing the Work and assume any
151− risks associated with Your exercise of permissions under this License.
152−
153−8. Limitation of Liability. In no event and under no legal theory,
154− whether in tort (including negligence), contract, or otherwise,
155− unless required by applicable law (such as deliberate and grossly
156− negligent acts) or agreed to in writing, shall any Contributor be
157− liable to You for damages, including any direct, indirect, special,
158− incidental, or consequential damages of any character arising as a
159− result of this License or out of the use or inability to use the
160− Work (including but not limited to damages for loss of goodwill,
161− work stoppage, computer failure or malfunction, or any and all
162− other commercial damages or losses), even if such Contributor
163− has been advised of the possibility of such damages.
164−
165−9. Accepting Warranty or Additional Liability. While redistributing
166− the Work or Derivative Works thereof, You may choose to offer,
167− and charge a fee for, acceptance of support, warranty, indemnity,
168− or other liability obligations and/or rights consistent with this
169− License. However, in accepting such obligations, You may act only
170− on Your own behalf and on Your sole responsibility, not on behalf
171− of any other Contributor, and only if You agree to indemnify,
172− defend, and hold each Contributor harmless for any liability
173− incurred by, or claims asserted against, such Contributor by reason
174− of your accepting any such warranty or additional liability.
175−
176−END OF TERMS AND CONDITIONS
177−
178−APPENDIX: How to apply the Apache License to your work.
179−
180− To apply the Apache License to your work, attach the following
181− boilerplate notice, with the fields enclosed by brackets "[]"
182− replaced with your own identifying information. (Don't include
183− the brackets!) The text should be enclosed in the appropriate
184− comment syntax for the file format. We also recommend that a
185− file or class name and description of purpose be included on the
186− same "printed page" as the copyright notice for easier
187− identification within third-party archives.
188−
189−Copyright [yyyy] [name of copyright owner]
190−
191−Licensed under the Apache License, Version 2.0 (the "License");
192−you may not use this file except in compliance with the License.
193−You may obtain a copy of the License at
194−
195− http://www.apache.org/licenses/LICENSE-2.0
196−
197−Unless required by applicable law or agreed to in writing, software
198−distributed under the License is distributed on an "AS IS" BASIS,
199−WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200−See the License for the specific language governing permissions and
201−limitations under the License.
examples/hello-world/LICENSE-MITdeleted−25
@@ -1,25 +0,0 @@
1−Copyright (c) 2015 The Rust Project Developers
2−
3−Permission is hereby granted, free of charge, to any
4−person obtaining a copy of this software and associated
5−documentation files (the "Software"), to deal in the
6−Software without restriction, including without
7−limitation the rights to use, copy, modify, merge,
8−publish, distribute, sublicense, and/or sell copies of
9−the Software, and to permit persons to whom the Software
10−is furnished to do so, subject to the following
11−conditions:
12−
13−The above copyright notice and this permission notice
14−shall be included in all copies or substantial portions
15−of the Software.
16−
17−THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
18−ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
19−TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
20−PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
21−SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
22−CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
23−OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
24−IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
25−DEALINGS IN THE SOFTWARE.
examples/hello-world/README.md+12−20
@@ -1,29 +1,21 @@
1−# 🦀🕸 `rust-webpack-template`
1+# Hello World
2
2
3−> **Kickstart your Rust, WebAssembly, and Webpack project!**
3+The most basic example!
4
4
5−This template is designed for creating monorepo-style Web applications with
5+## Source
6−Rust-generated WebAssembly and Webpack without publishing your wasm to NPM.
7
6
8−* Want to create and publish NPM packages with Rust and WebAssembly? [Check out
7+See `src/lib.rs`.
9− `wasm-pack-template`.](https://github.com/rustwasm/wasm-pack-template)
10
8
11−## 🔋 Batteries Included
9+## Build
12
10
13−This template comes pre-configured with all the boilerplate for compiling Rust
11+```
14−to WebAssembly and hooking into a Webpack build pipeline.
12+wasm-pack build --target web
15−
13+```
16−* `npm run start` -- Serve the project locally for development at
17− `http://localhost:8080`.
18−
19−* `npm run build` -- Bundle the project (in production mode).
20−
21−## 🚴 Using This Template
22
14
23−First, [install `wasm-pack`!](https://rustwasm.github.io/wasm-pack/installer/)
15+## Serve
24
16
25−Then, use `npm init` to clone this template:
17+Use any HTTP server, for example:
26
18
27−```sh
19+```
28−npm init rust-webpack my-app
20+python -m SimpleHTTPServer
29```
21```
examples/hello-world/index.html+3−3
@@ -5,9 +5,9 @@
5 <title>Hello, World!</title>
5 <title>Hello, World!</title>
6 </head>
6 </head>
7 <body>
7 <body>
8+ <script type="module">
8− <script src="pkg/dodrio_hello_world.js"></script>
9+ import init from "./pkg/dodrio_hello_world.js";
9− <script>
10− wasm_bindgen("pkg/dodrio_hello_world_bg.wasm");
10+ init("./pkg/dodrio_hello_world_bg.wasm");
11 </script>
11 </script>
12 </body>
12 </body>
13</html>
13</html>
examples/hello-world/src/lib.rs+18−23
@@ -1,42 +1,37 @@
1−use dodrio::bumpalo::Bump;
1+use dodrio::{builder::*, bumpalo};
2−use dodrio::{Node, Render, Vdom};
2+use dodrio::{Node, Render, RenderContext, Vdom};
3use wasm_bindgen::prelude::*;
3use wasm_bindgen::prelude::*;
4
4
5+/// A rendering component that displays a greeting.
5−struct Hello<'who> {
6+struct Hello {
6− who: &'who str,
7+ /// Who to greet.
8+ who: String,
7}
9}
8
10
9−impl<'who> Hello<'who> {
11+// The `Render` implementation describes how to render a `Hello` component into
10− fn new(who: &str) -> Hello {
12+// HTML.
11− Hello { who }
12− }
13−}
14−
15−impl<'who> Render for Hello<'who> {
13+impl<'a> Render<'a> for Hello {
16− fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
14+ fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
17− where
15+ let msg = bumpalo::format!(in cx.bump, "Hello, {}!", self.who);
18− 'a: 'bump,
16+ let msg = msg.into_bump_str();
19− {
17+ p(&cx).children([text(msg)]).finish()
20− Node::element(
21− bump,
22− "p",
23− [],
24− [],
25− [Node::text("Hello, "), Node::text(self.who), Node::text("!")],
26− )
27 }
18 }
28}
19}
29
20
30#[wasm_bindgen(start)]
21#[wasm_bindgen(start)]
31pub fn run() {
22pub fn run() {
23+ // Set up the panic hook for debugging when things go wrong.
32 console_error_panic_hook::set_once();
24 console_error_panic_hook::set_once();
33
25
26+ // Grab the document's `<body>`.
34 let window = web_sys::window().unwrap_throw();
27 let window = web_sys::window().unwrap_throw();
35 let document = window.document().unwrap_throw();
28 let document = window.document().unwrap_throw();
36 let body = document.body().unwrap_throw();
29 let body = document.body().unwrap_throw();
37
30
38 // Create a new `Hello` render component.
31 // Create a new `Hello` render component.
39− let component = Hello::new("World");
32+ let component = Hello {
33+ who: String::from("World"),
34+ };
40
35
41 // Create a virtual DOM and mount it and the `Hello` render component to the
36 // Create a virtual DOM and mount it and the `Hello` render component to the
42 // `<body>`.
37 // `<body>`.
examples/input-form/.gitignoredeleted−7
@@ -1,7 +0,0 @@
1−dist
2−node_modules
3−crate/bin
4−crate/pkg
5−crate/target
6−crate/wasm-pack.log
7−crate/Cargo.lock
examples/input-form/.travis.ymldeleted−11
@@ -1,11 +0,0 @@
1−language: rust
2−
3−install:
4− - curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.8/install.sh | bash
5− - source ~/.nvm/nvm.sh
6− - nvm install v10.5
7− - curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh -s -- -f
8−
9−script:
10− - npm install
11− - npm run build
examples/input-form/Cargo.toml+5−5
@@ -5,17 +5,17 @@ authors = ["Nick Fitzgerald <fitzgen@gmail.com>"]
5edition = "2018"
5edition = "2018"
6
6
7[lib]
7[lib]
8−crate-type = ["cdylib", "rlib"]
8+crate-type = ["cdylib"]
9
9
10[features]
10[features]
11
11
12[dependencies]
12[dependencies]
13dodrio = { path = "../.." }
13dodrio = { path = "../.." }
14−wasm-bindgen = "0.2"
14+wasm-bindgen = "0.2.55"
15−console_error_panic_hook = "0.1.1"
15+console_error_panic_hook = "0.1.6"
16
16
17[dependencies.web-sys]
17[dependencies.web-sys]
18−version = "0.3.8"
18+version = "0.3.32"
19features = [
19features = [
20 "console",
20 "console",
21 "Document",
21 "Document",
@@ -29,4 +29,4 @@ features = [
29]
29]
30
30
31[dev-dependencies]
31[dev-dependencies]
32−wasm-bindgen-test = "0.2"
32+wasm-bindgen-test = "0.3.5"
examples/input-form/LICENSE-APACHEdeleted−201
@@ -1,201 +0,0 @@
1− Apache License
2− Version 2.0, January 2004
3− http://www.apache.org/licenses/
4−
5−TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6−
7−1. Definitions.
8−
9− "License" shall mean the terms and conditions for use, reproduction,
10− and distribution as defined by Sections 1 through 9 of this document.
11−
12− "Licensor" shall mean the copyright owner or entity authorized by
13− the copyright owner that is granting the License.
14−
15− "Legal Entity" shall mean the union of the acting entity and all
16− other entities that control, are controlled by, or are under common
17− control with that entity. For the purposes of this definition,
18− "control" means (i) the power, direct or indirect, to cause the
19− direction or management of such entity, whether by contract or
20− otherwise, or (ii) ownership of fifty percent (50%) or more of the
21− outstanding shares, or (iii) beneficial ownership of such entity.
22−
23− "You" (or "Your") shall mean an individual or Legal Entity
24− exercising permissions granted by this License.
25−
26− "Source" form shall mean the preferred form for making modifications,
27− including but not limited to software source code, documentation
28− source, and configuration files.
29−
30− "Object" form shall mean any form resulting from mechanical
31− transformation or translation of a Source form, including but
32− not limited to compiled object code, generated documentation,
33− and conversions to other media types.
34−
35− "Work" shall mean the work of authorship, whether in Source or
36− Object form, made available under the License, as indicated by a
37− copyright notice that is included in or attached to the work
38− (an example is provided in the Appendix below).
39−
40− "Derivative Works" shall mean any work, whether in Source or Object
41− form, that is based on (or derived from) the Work and for which the
42− editorial revisions, annotations, elaborations, or other modifications
43− represent, as a whole, an original work of authorship. For the purposes
44− of this License, Derivative Works shall not include works that remain
45− separable from, or merely link (or bind by name) to the interfaces of,
46− the Work and Derivative Works thereof.
47−
48− "Contribution" shall mean any work of authorship, including
49− the original version of the Work and any modifications or additions
50− to that Work or Derivative Works thereof, that is intentionally
51− submitted to Licensor for inclusion in the Work by the copyright owner
52− or by an individual or Legal Entity authorized to submit on behalf of
53− the copyright owner. For the purposes of this definition, "submitted"
54− means any form of electronic, verbal, or written communication sent
55− to the Licensor or its representatives, including but not limited to
56− communication on electronic mailing lists, source code control systems,
57− and issue tracking systems that are managed by, or on behalf of, the
58− Licensor for the purpose of discussing and improving the Work, but
59− excluding communication that is conspicuously marked or otherwise
60− designated in writing by the copyright owner as "Not a Contribution."
61−
62− "Contributor" shall mean Licensor and any individual or Legal Entity
63− on behalf of whom a Contribution has been received by Licensor and
64− subsequently incorporated within the Work.
65−
66−2. Grant of Copyright License. Subject to the terms and conditions of
67− this License, each Contributor hereby grants to You a perpetual,
68− worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69− copyright license to reproduce, prepare Derivative Works of,
70− publicly display, publicly perform, sublicense, and distribute the
71− Work and such Derivative Works in Source or Object form.
72−
73−3. Grant of Patent License. Subject to the terms and conditions of
74− this License, each Contributor hereby grants to You a perpetual,
75− worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76− (except as stated in this section) patent license to make, have made,
77− use, offer to sell, sell, import, and otherwise transfer the Work,
78− where such license applies only to those patent claims licensable
79− by such Contributor that are necessarily infringed by their
80− Contribution(s) alone or by combination of their Contribution(s)
81− with the Work to which such Contribution(s) was submitted. If You
82− institute patent litigation against any entity (including a
83− cross-claim or counterclaim in a lawsuit) alleging that the Work
84− or a Contribution incorporated within the Work constitutes direct
85− or contributory patent infringement, then any patent licenses
86− granted to You under this License for that Work shall terminate
87− as of the date such litigation is filed.
88−
89−4. Redistribution. You may reproduce and distribute copies of the
90− Work or Derivative Works thereof in any medium, with or without
91− modifications, and in Source or Object form, provided that You
92− meet the following conditions:
93−
94− (a) You must give any other recipients of the Work or
95− Derivative Works a copy of this License; and
96−
97− (b) You must cause any modified files to carry prominent notices
98− stating that You changed the files; and
99−
100− (c) You must retain, in the Source form of any Derivative Works
101− that You distribute, all copyright, patent, trademark, and
102− attribution notices from the Source form of the Work,
103− excluding those notices that do not pertain to any part of
104− the Derivative Works; and
105−
106− (d) If the Work includes a "NOTICE" text file as part of its
107− distribution, then any Derivative Works that You distribute must
108− include a readable copy of the attribution notices contained
109− within such NOTICE file, excluding those notices that do not
110− pertain to any part of the Derivative Works, in at least one
111− of the following places: within a NOTICE text file distributed
112− as part of the Derivative Works; within the Source form or
113− documentation, if provided along with the Derivative Works; or,
114− within a display generated by the Derivative Works, if and
115− wherever such third-party notices normally appear. The contents
116− of the NOTICE file are for informational purposes only and
117− do not modify the License. You may add Your own attribution
118− notices within Derivative Works that You distribute, alongside
119− or as an addendum to the NOTICE text from the Work, provided
120− that such additional attribution notices cannot be construed
121− as modifying the License.
122−
123− You may add Your own copyright statement to Your modifications and
124− may provide additional or different license terms and conditions
125− for use, reproduction, or distribution of Your modifications, or
126− for any such Derivative Works as a whole, provided Your use,
127− reproduction, and distribution of the Work otherwise complies with
128− the conditions stated in this License.
129−
130−5. Submission of Contributions. Unless You explicitly state otherwise,
131− any Contribution intentionally submitted for inclusion in the Work
132− by You to the Licensor shall be under the terms and conditions of
133− this License, without any additional terms or conditions.
134− Notwithstanding the above, nothing herein shall supersede or modify
135− the terms of any separate license agreement you may have executed
136− with Licensor regarding such Contributions.
137−
138−6. Trademarks. This License does not grant permission to use the trade
139− names, trademarks, service marks, or product names of the Licensor,
140− except as required for reasonable and customary use in describing the
141− origin of the Work and reproducing the content of the NOTICE file.
142−
143−7. Disclaimer of Warranty. Unless required by applicable law or
144− agreed to in writing, Licensor provides the Work (and each
145− Contributor provides its Contributions) on an "AS IS" BASIS,
146− WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147− implied, including, without limitation, any warranties or conditions
148− of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149− PARTICULAR PURPOSE. You are solely responsible for determining the
150− appropriateness of using or redistributing the Work and assume any
151− risks associated with Your exercise of permissions under this License.
152−
153−8. Limitation of Liability. In no event and under no legal theory,
154− whether in tort (including negligence), contract, or otherwise,
155− unless required by applicable law (such as deliberate and grossly
156− negligent acts) or agreed to in writing, shall any Contributor be
157− liable to You for damages, including any direct, indirect, special,
158− incidental, or consequential damages of any character arising as a
159− result of this License or out of the use or inability to use the
160− Work (including but not limited to damages for loss of goodwill,
161− work stoppage, computer failure or malfunction, or any and all
162− other commercial damages or losses), even if such Contributor
163− has been advised of the possibility of such damages.
164−
165−9. Accepting Warranty or Additional Liability. While redistributing
166− the Work or Derivative Works thereof, You may choose to offer,
167− and charge a fee for, acceptance of support, warranty, indemnity,
168− or other liability obligations and/or rights consistent with this
169− License. However, in accepting such obligations, You may act only
170− on Your own behalf and on Your sole responsibility, not on behalf
171− of any other Contributor, and only if You agree to indemnify,
172− defend, and hold each Contributor harmless for any liability
173− incurred by, or claims asserted against, such Contributor by reason
174− of your accepting any such warranty or additional liability.
175−
176−END OF TERMS AND CONDITIONS
177−
178−APPENDIX: How to apply the Apache License to your work.
179−
180− To apply the Apache License to your work, attach the following
181− boilerplate notice, with the fields enclosed by brackets "[]"
182− replaced with your own identifying information. (Don't include
183− the brackets!) The text should be enclosed in the appropriate
184− comment syntax for the file format. We also recommend that a
185− file or class name and description of purpose be included on the
186− same "printed page" as the copyright notice for easier
187− identification within third-party archives.
188−
189−Copyright [yyyy] [name of copyright owner]
190−
191−Licensed under the Apache License, Version 2.0 (the "License");
192−you may not use this file except in compliance with the License.
193−You may obtain a copy of the License at
194−
195− http://www.apache.org/licenses/LICENSE-2.0
196−
197−Unless required by applicable law or agreed to in writing, software
198−distributed under the License is distributed on an "AS IS" BASIS,
199−WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200−See the License for the specific language governing permissions and
201−limitations under the License.
examples/input-form/LICENSE-MITdeleted−25
@@ -1,25 +0,0 @@
1−Copyright (c) 2015 The Rust Project Developers
2−
3−Permission is hereby granted, free of charge, to any
4−person obtaining a copy of this software and associated
5−documentation files (the "Software"), to deal in the
6−Software without restriction, including without
7−limitation the rights to use, copy, modify, merge,
8−publish, distribute, sublicense, and/or sell copies of
9−the Software, and to permit persons to whom the Software
10−is furnished to do so, subject to the following
11−conditions:
12−
13−The above copyright notice and this permission notice
14−shall be included in all copies or substantial portions
15−of the Software.
16−
17−THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
18−ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
19−TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
20−PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
21−SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
22−CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
23−OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
24−IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
25−DEALINGS IN THE SOFTWARE.
examples/input-form/README.md+13−20
@@ -1,29 +1,22 @@
1−# 🦀🕸 `rust-webpack-template`
1+# Input Forms
2
2
3−> **Kickstart your Rust, WebAssembly, and Webpack project!**
3+Renders a greeting to a user-supplied person via an `<input>` value. The
4+greeting is live-updated with new inputs.
4
5
5−This template is designed for creating monorepo-style Web applications with
6+## Source
6−Rust-generated WebAssembly and Webpack without publishing your wasm to NPM.
7
7
8−* Want to create and publish NPM packages with Rust and WebAssembly? [Check out
8+See `src/lib.rs`.
9− `wasm-pack-template`.](https://github.com/rustwasm/wasm-pack-template)
10
9
11−## 🔋 Batteries Included
10+## Build
12
11
13−This template comes pre-configured with all the boilerplate for compiling Rust
12+```
14−to WebAssembly and hooking into a Webpack build pipeline.
13+wasm-pack build --target web
15−
14+```
16−* `npm run start` -- Serve the project locally for development at
17− `http://localhost:8080`.
18−
19−* `npm run build` -- Bundle the project (in production mode).
20−
21−## 🚴 Using This Template
22
15
23−First, [install `wasm-pack`!](https://rustwasm.github.io/wasm-pack/installer/)
16+## Serve
24
17
25−Then, use `npm init` to clone this template:
18+Use any HTTP server, for example:
26
19
27−```sh
20+```
28−npm init rust-webpack my-app
21+python -m SimpleHTTPServer
29```
22```
examples/input-form/index.html+3−3
@@ -5,9 +5,9 @@
5 <title>Input Form</title>
5 <title>Input Form</title>
6 </head>
6 </head>
7 <body>
7 <body>
8+ <script type="module">
8− <script src="pkg/dodrio_input_form.js"></script>
9+ import init from "./pkg/dodrio_input_form.js";
9− <script>
10− wasm_bindgen("pkg/dodrio_input_form_bg.wasm");
10+ init("./pkg/dodrio_input_form_bg.wasm");
11 </script>
11 </script>
12 </body>
12 </body>
13</html>
13</html>
examples/input-form/src/lib.rs+41−36
@@ -1,71 +1,76 @@
1−use dodrio::bumpalo::{self, Bump};
1+use dodrio::{bumpalo, Node, Render, RenderContext};
2−use dodrio::{on, Attribute, Node, Render};
3use wasm_bindgen::prelude::*;
2use wasm_bindgen::prelude::*;
4use wasm_bindgen::JsCast;
3use wasm_bindgen::JsCast;
5
4
5+/// Say hello to someone.
6struct SayHelloTo {
6struct SayHelloTo {
7+ /// Who to say hello to.
7 who: String,
8 who: String,
8}
9}
9
10
10impl SayHelloTo {
11impl SayHelloTo {
12+ /// Construct a new `SayHelloTo` component.
11 fn new<S: Into<String>>(who: S) -> SayHelloTo {
13 fn new<S: Into<String>>(who: S) -> SayHelloTo {
12 let who = who.into();
14 let who = who.into();
13 SayHelloTo { who }
15 SayHelloTo { who }
14 }
16 }
15
17
18+ /// Update who to say hello to.
16 fn set_who(&mut self, who: String) {
19 fn set_who(&mut self, who: String) {
17 self.who = who;
20 self.who = who;
18 }
21 }
19}
22}
20
23
24+// The `Render` implementation has a text `<input>` and a `<div>` that shows a
25+// greeting to the `<input>`'s value.
21−impl Render for SayHelloTo {
26+impl<'a> Render<'a> for SayHelloTo {
22− fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> dodrio::Node<'bump>
27+ fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
23− where
28+ use dodrio::builder::*;
24− 'a: 'bump,
25− {
26− let input = Node::element(
27− bump,
28− "input",
29− [on(bump, "input", |root, vdom, event| {
30− let input = match event
31− .target()
32− .and_then(|t| t.dyn_into::<web_sys::HtmlInputElement>().ok())
33− {
34− Some(input) => input,
35− None => return,
36− };
37
29
38− root.unwrap_mut::<SayHelloTo>().set_who(input.value());
30+ div(&cx)
39− vdom.schedule_render();
31+ .children([
40− })],
32+ input(&cx)
41− [
42− Attribute {
43− name: "type",
33+ .attr("type", "text")
44− value: "text",
34+ .attr(
45− },
35+ "value",
46− Attribute {
36+ bumpalo::format!(in cx.bump, "{}", self.who).into_bump_str(),
47− name: "value",
37+ )
48− value: &self.who,
38+ .on("input", |root, vdom, event| {
49− },
39+ // If the event's target is our input...
50− ],
40+ let input = match event
51− [],
41+ .target()
42+ .and_then(|t| t.dyn_into::<web_sys::HtmlInputElement>().ok())
43+ {
44+ None => return,
45+ Some(input) => input,
52− );
46+ };
53
47
54− let hello = bumpalo::format!(in bump, "Hello, {}!", self.who);
48+ // ...then get its value and update who we are greeting.
55− let hello = Node::text(hello.into_bump_str());
49+ let value = input.value();
50+ let hello = root.unwrap_mut::<SayHelloTo>();
51+ hello.set_who(value);
56
52
57− Node::element(bump, "div", [], [], [input, hello])
53+ // Finally, re-render the component on the next animation frame.
54+ vdom.schedule_render();
55+ })
56+ .finish(),
57+ text(bumpalo::format!(in cx.bump, "Hello, {}!", self.who).into_bump_str()),
58+ ])
59+ .finish()
58 }
60 }
59}
61}
60
62
61#[wasm_bindgen(start)]
63#[wasm_bindgen(start)]
62pub fn run() {
64pub fn run() {
65+ // Initialize debugging for when/if something goes wrong.
63 console_error_panic_hook::set_once();
66 console_error_panic_hook::set_once();
64
67
68+ // Get the document's `<body>`.
65 let window = web_sys::window().unwrap();
69 let window = web_sys::window().unwrap();
66 let document = window.document().unwrap();
70 let document = window.document().unwrap();
67 let body = document.body().unwrap();
71 let body = document.body().unwrap();
68
72
73+ // Construct a new `SayHelloTo` rendering component.
69 let say_hello = SayHelloTo::new("World");
74 let say_hello = SayHelloTo::new("World");
70
75
71 // Mount the component to the `<body>`.
76 // Mount the component to the `<body>`.
examples/js-component/Cargo.tomlnew file+26
@@ -0,0 +1,26 @@
1+[package]
2+name = "dodrio-js-component"
3+version = "0.1.0"
4+authors = ["Nick Fitzgerald <fitzgen@gmail.com>"]
5+edition = "2018"
6+
7+[lib]
8+crate-type = ["cdylib"]
9+
10+[features]
11+
12+[dependencies]
13+dodrio = { path = "../.." }
14+dodrio-js-api = { path = "../../crates/js-api" }
15+wasm-bindgen = "0.2.55"
16+console_error_panic_hook = "0.1.6"
17+js-sys = "0.3.32"
18+
19+[dependencies.web-sys]
20+version = "0.3.32"
21+features = [
22+ "Document",
23+ "HtmlElement",
24+ "Node",
25+ "Window",
26+]
examples/js-component/README.mdnew file+23
@@ -0,0 +1,23 @@
1+# Defining a Rendering Component in JavaScript
2+
3+Heterogeneous Rust and JavaScript rendering component trees!
4+
5+## Source
6+
7+See `greeting.js` for the JavaScript code.
8+
9+See `src/lib.rs` for the Rust code.
10+
11+## Build
12+
13+```
14+wasm-pack build --target web
15+```
16+
17+## Serve
18+
19+Use any HTTP server, for example:
20+
21+```
22+python -m SimpleHTTPServer
23+```
examples/js-component/greeting.jsnew file+41
@@ -0,0 +1,41 @@
1+// A rendering component defined in JavaScript!
2+class Greeting {
3+ constructor(who) {
4+ this.who = who;
5+ }
6+
7+ render() {
8+ return {
9+ tagName: "p",
10+ attributes: [
11+ {
12+ name: "class",
13+ value: "greeting",
14+ },
15+ ],
16+ listeners: [
17+ {
18+ on: "click",
19+ callback: this.onClick.bind(this),
20+ }
21+ ],
22+ children: [
23+ "Hello, ",
24+ {
25+ tagName: "strong",
26+ children: [this.who],
27+ }
28+ ],
29+ };
30+ }
31+
32+ async onClick(vdom, event) {
33+ // Be more excited!
34+ this.who += "!";
35+
36+ // Schedule a re-render.
37+ await vdom.render();
38+
39+ console.log("re-rendering finished!");
40+ }
41+}
examples/js-component/index.htmlnew file+14
@@ -0,0 +1,14 @@
1+<!DOCTYPE html>
2+<html>
3+ <head>
4+ <meta http-equiv="Content-type" content="text/html; charset=utf-8"/>
5+ <title>A JavaScript component!</title>
6+ </head>
7+ <body>
8+ <script src="greeting.js"></script>
9+ <script type="module">
10+ import init from "./pkg/dodrio_js_component.js";
11+ init("./pkg/dodrio_js_component_bg.wasm");
12+ </script>
13+ </body>
14+</html>
examples/js-component/src/lib.rsnew file+63
@@ -0,0 +1,63 @@
1+use dodrio::{Node, Render, RenderContext, Vdom};
2+use dodrio_js_api::JsRender;
3+use js_sys::Object;
4+use wasm_bindgen::prelude::*;
5+
6+#[wasm_bindgen]
7+extern "C" {
8+ // Import the JS `Greeting` class.
9+ #[wasm_bindgen(extends = Object)]
10+ #[derive(Clone, Debug)]
11+ type Greeting;
12+
13+ // And the `Greeting` class's constructor.
14+ #[wasm_bindgen(constructor)]
15+ fn new(who: &str) -> Greeting;
16+}
17+
18+/// This is our Rust rendering component that wraps the JS rendering component.
19+pub struct GreetingViaJs {
20+ js: JsRender,
21+}
22+
23+impl GreetingViaJs {
24+ /// Create a new `GreetingViaJs`, which will internally create a new JS
25+ /// `Greeting`.
26+ pub fn new(who: &str) -> GreetingViaJs {
27+ let js = JsRender::new(Greeting::new(who));
28+ GreetingViaJs { js }
29+ }
30+}
31+
32+/// Here's the `Render` implementation! This adds a `<p>` element and some text
33+/// around whatever the inner JS `Greeting` component renders.
34+impl<'a> Render<'a> for GreetingViaJs {
35+ fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
36+ use dodrio::builder::*;
37+ p(&cx)
38+ .children([text("JavaScript says: "), self.js.render(cx)])
39+ .finish()
40+ }
41+}
42+
43+/// Finally our main initialization function that kicks everything off!
44+#[wasm_bindgen(start)]
45+pub fn run() {
46+ // Set up the panic hook for debugging when things go wrong.
47+ console_error_panic_hook::set_once();
48+
49+ // Grab the document's `<body>`.
50+ let window = web_sys::window().unwrap_throw();
51+ let document = window.document().unwrap_throw();
52+ let body = document.body().unwrap_throw();
53+
54+ // Create a new `GreetingViaJs` render component.
55+ let component = GreetingViaJs::new("World");
56+
57+ // Create a virtual DOM and mount it and the `GreetingViaJs` render component to the
58+ // `<body>`.
59+ let vdom = Vdom::new(body.as_ref(), component);
60+
61+ // Run the virtual DOM forever and don't unmount it.
62+ vdom.forget();
63+}
examples/moire/Cargo.tomlnew file+43
@@ -0,0 +1,43 @@
1+[package]
2+name = "dodrio-moire"
3+version = "0.1.0"
4+authors = ["Nick Fitzgerald <fitzgen@gmail.com>"]
5+edition = "2018"
6+
7+[lib]
8+crate-type = ["cdylib"]
9+
10+[dependencies]
11+cfg-if = "0.1.10"
12+dodrio = { path = "../.." }
13+log = "0.4.11"
14+wasm-bindgen = "0.2.55"
15+wasm-bindgen-futures = "0.4.5"
16+
17+# Optional dependencies for logging.
18+console_error_panic_hook = { optional = true, version = "0.1.6" }
19+console_log = { optional = true, version = "0.2.0" }
20+js-sys = "0.3.32"
21+
22+[dependencies.web-sys]
23+version = "0.3.32"
24+features = [
25+ "Document",
26+ "Element",
27+ "Event",
28+ "EventTarget",
29+ "HtmlElement",
30+ "HtmlInputElement",
31+ "Node",
32+ "Performance",
33+ "Window",
34+]
35+
36+[dev-dependencies]
37+wasm-bindgen-test = "0.3.5"
38+
39+[features]
40+logging = [
41+ "console_log",
42+ "console_error_panic_hook",
43+]
examples/moire/README.mdnew file+26
@@ -0,0 +1,26 @@
1+# Moiré Patterns!
2+
3+This was originally a demo meant to show off [WebRender][], and so the
4+performance bottleneck is painting, not DOM manipulation, but it looks cool
5+either way!
6+
7+[WebRender]: https://hacks.mozilla.org/2017/10/the-whole-web-at-maximum-fps-how-webrender-gets-rid-of-jank/
8+
9+## Source
10+
11+See `src/lib.rs` for the main bits, and `src/colors.rs` for HSV to RGB color
12+conversion and other color utilities.
13+
14+## Build
15+
16+```
17+wasm-pack build --target web
18+```
19+
20+## Serve
21+
22+Use any HTTP server, for example:
23+
24+```
25+python -m SimpleHTTPServer
26+```
examples/moire/index.htmlnew file+67
@@ -0,0 +1,67 @@
1+<!DOCTYPE html>
2+<html>
3+ <head>
4+ <meta http-equiv="Content-type" content="text/html; charset=utf-8"/>
5+ <title>Moire patterns! Originally a WebRender demo</title>
6+ <style>
7+ main {
8+ position: absolute;
9+ top: 0;
10+ left: 0;
11+ right: 0;
12+ bottom: 0;
13+ overflow: hidden;
14+ }
15+ .circle {
16+ border-radius: 50%;
17+ position: absolute;
18+ top: 50%;
19+ left: 50%;
20+ border-style: solid;
21+ border-width: 3px;
22+ }
23+ .object {
24+ position: absolute;
25+ width: 100%;
26+ height: 100%;
27+ left: 0;
28+ top: 0;
29+ }
30+ code {
31+ position: relative;
32+ font-size: 150%;
33+ z-index: 10;
34+ }
35+ #circle-count {
36+ position: absolute;
37+ margin-top: 30px;
38+ z-index: 2;
39+ }
40+ </style>
41+ </head>
42+ <body>
43+ <code>FPS: <output id="fps"></code></code>
44+ <div id="scene"></div>
45+ <script type="module">
46+ import init from "./pkg/dodrio_moire.js";
47+ init("./pkg/dodrio_moire_bg.wasm");
48+ </script>
49+ <script>
50+ let lastTime = 0
51+ let frameCount = 0
52+ const fps = document.getElementById('fps')
53+ function updateFPS(time) {
54+ frameCount++
55+ if (lastTime + 1000.0 <= time) {
56+ fps.textContent = frameCount
57+ lastTime = time
58+ frameCount = 0
59+ }
60+ }
61+ requestAnimationFrame(function loop(now) {
62+ updateFPS(now);
63+ requestAnimationFrame(loop);
64+ })
65+ </script>
66+ </body>
67+</html>
examples/moire/src/colors.rsnew file+162
@@ -0,0 +1,162 @@
1+//! Color utility functions.
2+
3+use std::f64;
4+
5+/// An RGB color.
6+#[derive(Copy, Clone)]
7+pub struct Rgb {
8+ pub r: u8,
9+ pub g: u8,
10+ pub b: u8,
11+}
12+
13+/// An HSV color.
14+#[derive(Copy, Clone)]
15+pub struct Hsv {
16+ pub h: f64,
17+ pub s: f64,
18+ pub v: f64,
19+}
20+
21+/// Convert an RGB color into HSV.
22+impl From<Rgb> for Hsv {
23+ fn from(Rgb { r, g, b }: Rgb) -> Hsv {
24+ let r = r as f64 / 255.0;
25+ let g = g as f64 / 255.0;
26+ let b = b as f64 / 255.0;
27+ let max = f64::max(f64::max(r, g), b);
28+ let min = f64::min(f64::min(r, g), b);
29+ let v = max;
30+ let diff = v - min;
31+ let mut h = 0.0;
32+ let mut s = 0.0;
33+ if diff != 0.0 {
34+ s = diff / v;
35+ let diffc = |c| (v - c) / 6.0 / diff + 1.0 / 2.0;
36+ let rr = diffc(r);
37+ let gg = diffc(g);
38+ let bb = diffc(b);
39+ if r == v {
40+ h = bb - gg;
41+ } else if g == v {
42+ h = (1.0 / 3.0) + rr - bb;
43+ } else if b == v {
44+ h = (2.0 / 3.0) + gg - rr;
45+ }
46+ }
47+ Hsv { h, s, v }
48+ }
49+}
50+
51+/// Convert an HSV color into RGB.
52+impl From<Hsv> for Rgb {
53+ fn from(Hsv { h, s, v }: Hsv) -> Rgb {
54+ let i = (h * 6.0).floor();
55+ let f = h * 6.0 - i;
56+ let p = v * (1.0 - s);
57+ let q = v * (1.0 - f * s);
58+ let t = v * (1.0 - (1.0 - f) * s);
59+ let r;
60+ let g;
61+ let b;
62+ match i as u64 % 6 {
63+ 0 => {
64+ r = v;
65+ g = t;
66+ b = p;
67+ }
68+ 1 => {
69+ r = q;
70+ g = v;
71+ b = p;
72+ }
73+ 2 => {
74+ r = p;
75+ g = v;
76+ b = t;
77+ }
78+ 3 => {
79+ r = p;
80+ g = q;
81+ b = v;
82+ }
83+ 4 => {
84+ r = t;
85+ g = p;
86+ b = v;
87+ }
88+ 5 => {
89+ r = v;
90+ g = p;
91+ b = q;
92+ }
93+ _ => wasm_bindgen::throw_str("impossible"),
94+ }
95+ let r = (r * 255.0).round() as u8;
96+ let g = (g * 255.0).round() as u8;
97+ let b = (b * 255.0).round() as u8;
98+ Rgb { r, g, b }
99+ }
100+}
101+
102+/// Linearly interpolate between two values.
103+fn lerp(a: f64, b: f64, t: f64) -> f64 {
104+ a + (b - a) * t
105+}
106+
107+/// Clamp the `x` value to within the range `lo..hi`.
108+fn clamp(lo: f64, hi: f64, x: f64) -> f64 {
109+ if x < lo {
110+ lo
111+ } else if x > hi {
112+ hi
113+ } else {
114+ x
115+ }
116+}
117+
118+/// Linearly interpolate two HSV colors and convert the result into RGB.
119+fn lerp_hsv_to_rgb(a: Hsv, b: Hsv, t: f64) -> Rgb {
120+ let h = lerp(a.h, b.h, t);
121+ let s = lerp(a.s, b.s, t);
122+ let v = lerp(a.v, b.v, t);
123+ let hsv = Hsv { h, s, v };
124+ hsv.into()
125+}
126+
127+thread_local! {
128+ static PALETTES: [(Hsv, Hsv); 3] = [
129+ (
130+ (Rgb { r: 0xfe, g: 0xf6, b: 0xdf }).into(),
131+ (Rgb { r: 0x64, g: 0xc3, b: 0xc8 }).into(),
132+ ),
133+ (
134+ (Rgb { r: 0xfc, g: 0xcf, b: 0x74 }).into(),
135+ (Rgb { r: 0x41, g: 0x65, b: 0x7f }).into(),
136+ ),
137+ (
138+ (Rgb { r: 0xd9, g: 0xe0, b: 0xe8 }).into(),
139+ (Rgb { r: 0x94, g: 0x5d, b: 0x72 }).into(),
140+ ),
141+ ];
142+}
143+
144+/// Get a color from our color palette that is linearly interpolated based on
145+/// the `t` elapsed time value.
146+///
147+/// The `which` function allows callers to select whether they want the
148+/// foreground or background color.
149+pub fn get_interpolated_color<F>(mut which: F, t: f64) -> Rgb
150+where
151+ F: FnMut((Hsv, Hsv)) -> Hsv,
152+{
153+ PALETTES.with(|palettes| {
154+ let t = t * (palettes.len() as f64);
155+ let prev_index = t.floor() as usize % palettes.len();
156+ let next_index = (prev_index + 1) % palettes.len();
157+ let t = clamp(-0.5, 0.5, (t % 1.0 - 0.5) * 5.0) + 0.5;
158+ let a = which(palettes[prev_index]);
159+ let b = which(palettes[next_index]);
160+ lerp_hsv_to_rgb(a, b, t)
161+ })
162+}
examples/moire/src/lib.rsnew file+259
@@ -0,0 +1,259 @@
1+mod colors;
2+
3+use cfg_if::cfg_if;
4+use dodrio::{bumpalo, Node, Render, RenderContext, Vdom};
5+use std::cell::RefCell;
6+use std::rc::Rc;
7+use std::str;
8+use wasm_bindgen::{prelude::*, JsCast};
9+
10+/// This is the main function that is automatically invoked when the wasm module
11+/// is loaded.
12+#[wasm_bindgen(start)]
13+pub fn run() {
14+ // Set up the panic hook for debugging when things go wrong.
15+ init_logging();
16+
17+ // Grab the document's `<body>`.
18+ let window = web_sys::window().unwrap_throw();
19+ let document = window.document().unwrap_throw();
20+ let scene = document.get_element_by_id("scene").unwrap_throw();
21+
22+ // Create a new `Moire` render component.
23+ let component = Moire::new();
24+
25+ // Create a virtual DOM and mount it and the `Moire` render component to the
26+ // scene.
27+ let vdom = Vdom::new(&scene, component);
28+
29+ // Kick off a loop that keeps re-rendering on every animation frame.
30+ let rc = <Rc<RefCell<Option<Closure<dyn FnMut()>>>>>::default();
31+ let rc2 = rc.clone();
32+ let window2 = window.clone();
33+ let weak = vdom.weak();
34+ let f = Closure::wrap(Box::new(move || {
35+ weak.schedule_render();
36+ window
37+ .request_animation_frame(
38+ rc.borrow()
39+ .as_ref()
40+ .unwrap_throw()
41+ .as_ref()
42+ .unchecked_ref::<js_sys::Function>(),
43+ )
44+ .unwrap_throw();
45+ }) as Box<dyn FnMut()>);
46+ window2
47+ .request_animation_frame(f.as_ref().unchecked_ref::<js_sys::Function>())
48+ .unwrap_throw();
49+ *rc2.borrow_mut() = Some(f);
50+
51+ // Run the virtual DOM forever and don't unmount it.
52+ vdom.forget();
53+}
54+
55+cfg_if! {
56+ if #[cfg(feature = "logging")] {
57+ fn init_logging() {
58+ console_error_panic_hook::set_once();
59+ console_log::init_with_level(log::Level::Trace)
60+ .expect_throw("should initialize logging OK");
61+ }
62+ } else {
63+ fn init_logging() {
64+ // Do nothing.
65+ }
66+ }
67+}
68+
69+/// A rendering component that renders two overlapping sets of concentric
70+/// circles that move around and form Moiré patterns.
71+///
72+/// https://en.wikipedia.org/wiki/Moir%C3%A9_pattern
73+pub struct Moire {
74+ // How many circles to render in each of our sets. This is controlled by the
75+ // `<input>` slider.
76+ count: u32,
77+}
78+
79+impl Moire {
80+ /// Construct a new `Moire` rendering component.
81+ ///
82+ /// Defaults to 200 circles.
83+ pub fn new() -> Moire {
84+ Moire { count: 200 }
85+ }
86+
87+ /// Callback for "change" events emitted on the `<input>` slider that
88+ /// controls how many circles we render on each frame.
89+ fn on_change(&mut self, event: web_sys::Event) {
90+ // Get the `<input>` element.
91+ let input = event
92+ .target()
93+ .unwrap_throw()
94+ .unchecked_into::<web_sys::HtmlInputElement>();
95+
96+ // Parse its new value.
97+ let value = input.value();
98+ let value: u32 = value
99+ .parse()
100+ .expect_throw("<input type=\"range\"> value should always be an integer");
101+
102+ // And update our circle count!
103+ self.count = value;
104+ }
105+}
106+
107+impl<'a> Render<'a> for Moire {
108+ fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
109+ use dodrio::builder::*;
110+
111+ let elapsed = web_sys::window()
112+ .unwrap_throw()
113+ .performance()
114+ .unwrap_throw()
115+ .now();
116+ let elapsed = elapsed / 1600.0;
117+
118+ main(&cx)
119+ .attr("style", self.moire_style(cx, elapsed))
120+ .children([
121+ // The `<input>` that lets users control how many circles to
122+ // render.
123+ input(&cx)
124+ .attr("id", "circle-count")
125+ .attr("type", "range")
126+ .attr("min", "30")
127+ .attr("max", "500")
128+ .attr("value", {
129+ let count = bumpalo::format!(in cx.bump, "{}", self.count);
130+ count.into_bump_str()
131+ })
132+ .on("change", |root, _vdom, event| {
133+ root.unwrap_mut::<Moire>().on_change(event);
134+
135+ // Note: there is no need to manually schedule a
136+ // re-render here, since we already started a
137+ // `requestAnimationFrame` loop that automatically
138+ // re-renders on every frame in the `run` function.
139+ })
140+ .finish(),
141+ // And the actual concentric circles that form the Moire patterns.
142+ self.orbiting_objects(cx, elapsed),
143+ self.lemniscate_objects(cx, elapsed),
144+ ])
145+ .finish()
146+ }
147+}
148+
149+/// Rendering helper methods.
150+impl Moire {
151+ /// Generate the main Moire element's inline CSS styles.
152+ fn moire_style<'a>(&self, cx: &mut RenderContext<'a>, elapsed: f64) -> &'a str {
153+ let elapsed = elapsed / 3.0;
154+ let color = colors::get_interpolated_color(|(_, bg)| bg, elapsed % 1.0);
155+ let style = bumpalo::format!(
156+ in cx.bump,
157+ "background-color: rgb({}, {}, {})",
158+ color.r,
159+ color.g,
160+ color.b
161+ );
162+ style.into_bump_str()
163+ }
164+
165+ /// Generate the orbiting circle objects.
166+ fn orbiting_objects<'a>(&self, cx: &mut RenderContext<'a>, elapsed: f64) -> Node<'a> {
167+ const D: f64 = 200.0;
168+ let x = elapsed.sin() * D;
169+ let y = elapsed.cos() * D;
170+ self.moving_object(cx, elapsed, x, y)
171+ }
172+
173+ /// Generate the lemniscate circle objects that interact with the orbiting
174+ /// circle objects to form Moire patterns.
175+ fn lemniscate_objects<'a>(&self, cx: &mut RenderContext<'a>, elapsed: f64) -> Node<'a> {
176+ const A: f64 = 150.0;
177+ let x = elapsed.sin() * A;
178+ let y = elapsed.sin() * elapsed.cos() * A;
179+ self.moving_object(cx, elapsed, x, y)
180+ }
181+
182+ fn moving_object<'a>(
183+ &self,
184+ cx: &mut RenderContext<'a>,
185+ elapsed: f64,
186+ x: f64,
187+ y: f64,
188+ ) -> Node<'a> {
189+ use dodrio::builder::*;
190+ div(&cx)
191+ .attr("class", "object")
192+ .attr("style", self.moving_object_style(cx, x, y))
193+ .children([self.circle(cx, elapsed, self.count)])
194+ .finish()
195+ }
196+
197+ /// Generate inline CSS styles for a moving object.
198+ fn moving_object_style<'a>(&self, cx: &mut RenderContext<'a>, x: f64, y: f64) -> &'a str {
199+ bumpalo::format!(in cx.bump, "left: {}px; top: {}px;", x, y).into_bump_str()
200+ }
201+
202+ /// Recursively generate `self.count` concentric circles.
203+ fn circle<'a>(&self, cx: &mut RenderContext<'a>, elapsed: f64, n: u32) -> Node<'a> {
204+ use dodrio::builder::*;
205+
206+ let r = n * 16;
207+
208+ let mut circle = div(&cx)
209+ .attr("class", "circle")
210+ .attr("data-radius", {
211+ let r = bumpalo::format!(in cx.bump, "{}", r);
212+ r.into_bump_str()
213+ })
214+ .attr("style", self.circle_style(cx, elapsed, n, r));
215+
216+ if n > 0 {
217+ circle = circle.child(self.circle(cx, elapsed, n - 1));
218+ }
219+
220+ circle.finish()
221+ }
222+
223+ /// Generate inline CSS styles for a circle.
224+ fn circle_style<'a>(
225+ &self,
226+ cx: &mut RenderContext<'a>,
227+ elapsed: f64,
228+ n: u32,
229+ r: u32,
230+ ) -> &'a str {
231+ let (border, alpha) = self.circle_color(elapsed, n);
232+ let margin = r / 2 + 3;
233+ let style = bumpalo::format!(
234+ in cx.bump,
235+ "border-color: rgba({}, {}, {}, {});\
236+ margin-left: -{}px;\
237+ margin-top: -{}px;\
238+ width: {}px;\
239+ height: {}px;",
240+ border.r,
241+ border.g,
242+ border.b,
243+ alpha,
244+ margin,
245+ margin,
246+ r,
247+ r
248+ );
249+ style.into_bump_str()
250+ }
251+
252+ /// Compute the color for a circle.
253+ fn circle_color(&self, elapsed: f64, n: u32) -> (colors::Rgb, f64) {
254+ let elapsed = elapsed / 3.0;
255+ let base = colors::get_interpolated_color(|(fg, _)| fg, elapsed % 1.0);
256+ let lightness = 1.0 - (n as f64) / (self.count as f64);
257+ (base, lightness)
258+ }
259+}
examples/sierpinski-triangle/Cargo.tomlnew file+28
@@ -0,0 +1,28 @@
1+[package]
2+name = "dodrio-sierpinski"
3+version = "0.1.0"
4+authors = ["Steven Seguin <steven.seguin@gmail.com>"]
5+edition = "2018"
6+
7+[lib]
8+crate-type = ["cdylib"]
9+
10+[dependencies]
11+dodrio = { path = "../.." }
12+wasm-bindgen = "0.2.55"
13+console_error_panic_hook = "0.1.6"
14+js-sys = "0.3.32"
15+wasm-bindgen-futures = "0.4.5"
16+
17+[dependencies.web-sys]
18+version = "0.3.32"
19+features = [
20+ "Document",
21+ "HtmlElement",
22+ "Node",
23+ "Performance",
24+ "Window",
25+]
26+
27+[dev-dependencies]
28+wasm-bindgen-test = "0.3.5"
examples/sierpinski-triangle/README.mdnew file+24
@@ -0,0 +1,24 @@
1+# Sierpinski Triangle
2+
3+A Sierpinkski triangle that constantly shrinks and grows, and whose nodes have
4+a value that increments by one every second.
5+
6+Ported from the React Fiber example [here](https://github.com/claudiopro/react-fiber-vs-stack-demo).
7+
8+## Source
9+
10+See `src/lib.rs`.
11+
12+## Build
13+
14+```
15+wasm-pack build --target web
16+```
17+
18+## Serve
19+
20+Use any HTTP server, for example:
21+
22+```
23+python -m SimpleHTTPServer
24+```
examples/sierpinski-triangle/ferris.pngnew file

Binary file not shown.

examples/sierpinski-triangle/index.htmlnew file+69
@@ -0,0 +1,69 @@
1+<!DOCTYPE html>
2+<html>
3+
4+<head>
5+ <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
6+ <title>Sierpinski Triangle</title>
7+ <style>
8+ .container {
9+ position: absolute;
10+ transform-origin: 0 0;
11+ left: 50%;
12+ top: 50%;
13+ width: 10px;
14+ height: 10px;
15+ background: #eee;
16+ }
17+
18+ .dot {
19+ position: absolute;
20+ background: #61dafb;
21+ font: normal 15px sans-serif;
22+ text-align: center;
23+ cursor: pointer;
24+ }
25+
26+ .dot:hover {
27+ background: url("ferris.png") no-repeat center center;
28+ z-index: 999;
29+ }
30+
31+ code {
32+ position: relative;
33+ font-size: 150%;
34+ }
35+ </style>
36+</head>
37+
38+<body>
39+ <code>FPS: <output id="fps">...</output></code>
40+ <div id="scene"></div>
41+
42+ <script type="module">
43+ import init from "./pkg/dodrio_sierpinski.js";
44+ init("./pkg/dodrio_sierpinski_bg.wasm");
45+ </script>
46+
47+ <script>
48+ let lastTime = 0;
49+ let frameCount = 0;
50+ const fps = document.getElementById('fps');
51+
52+ function updateFPS(time) {
53+ frameCount++;
54+ if (lastTime + 1000.0 <= time) {
55+ fps.textContent = frameCount;
56+ lastTime = time;
57+ frameCount = 0;
58+ }
59+ }
60+
61+ requestAnimationFrame(function loop(now) {
62+ updateFPS(now);
63+ requestAnimationFrame(loop);
64+ })
65+
66+ </script>
67+</body>
68+
69+</html>
examples/sierpinski-triangle/src/lib.rsnew file+157
@@ -0,0 +1,157 @@
1+use dodrio::{bumpalo, Node, Render, RenderContext, Vdom};
2+use std::cell::RefCell;
3+use std::rc::Rc;
4+use wasm_bindgen::{prelude::*, JsCast};
5+use web_sys::Window;
6+
7+// Triangle container defining target size.
8+struct Container {
9+ size: f64,
10+}
11+
12+impl Container {
13+ // Construct a new container.
14+ pub fn new() -> Container {
15+ Container { size: 25.0 }
16+ }
17+
18+ // Generate the container style to fluctuate triangle width.
19+ fn container_transform<'a>(&self, cx: &mut RenderContext<'a>, elapsed: f64) -> &'a str {
20+ let t = elapsed % 10.0;
21+ let scale = 0.45 + (if t > 5.0 { 10.0 - t } else { t }) / 40.0;
22+ let transform = bumpalo::format!(
23+ in cx.bump,
24+ "transform: scaleX({}) scaleY(0.7) translateZ(0.1px)",
25+ scale
26+ );
27+ transform.into_bump_str()
28+ }
29+
30+ // Generate the dot's position on the grid.
31+ fn dot_style<'a>(&self, cx: &mut RenderContext<'a>, x: f64, y: f64) -> &'a str {
32+ let s = self.size * 1.3;
33+ let radius = s / 2.0;
34+ let styles = bumpalo::format!(
35+ in cx.bump,
36+ r#"
37+ width: {}px;
38+ height: {}px;
39+ left: {}px;
40+ top: {}px;
41+ border-radius: {}px;
42+ line-height: {}px;
43+ "#,
44+ s, s, x, y, radius, s
45+ );
46+ styles.into_bump_str()
47+ }
48+
49+ // Create a dot node.
50+ fn dot<'a>(&self, cx: &mut RenderContext<'a>, x: f64, y: f64, content: u32) -> Node<'a> {
51+ use dodrio::builder::{div, text};
52+
53+ div(&cx)
54+ .attr("class", "dot")
55+ .attr("style", self.dot_style(cx, x, y))
56+ .child(text(
57+ bumpalo::format!(in cx.bump, "{}", content).into_bump_str(),
58+ ))
59+ .finish()
60+ }
61+
62+ // Create a flattened vector of dot nodes (arranged in recursive triangles)
63+ fn triangle<'a>(
64+ &self,
65+ cx: &mut RenderContext<'a>,
66+ x: f64,
67+ y: f64,
68+ s: f64,
69+ content: u32,
70+ children: &mut bumpalo::collections::Vec<Node<'a>>,
71+ ) {
72+ if s <= self.size {
73+ children.push(self.dot(cx, x, y, content));
74+ return;
75+ }
76+
77+ let s = s / 2.0;
78+
79+ // Top of triangle
80+ self.triangle(cx, x, y - (s / 2.0), s, content, children);
81+ // Bottom-left of triangle
82+ self.triangle(cx, x - s, y + (s / 2.0), s, content, children);
83+ // Bottom-right of triangle
84+ self.triangle(cx, x + s, y + (s / 2.0), s, content, children);
85+ }
86+}
87+
88+impl<'a> Render<'a> for Container {
89+ fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
90+ use dodrio::builder::div;
91+
92+ let elapsed = web_sys::window()
93+ .unwrap_throw()
94+ .performance()
95+ .unwrap_throw()
96+ .now()
97+ / 1000.0;
98+
99+ let modulus = elapsed as u32 % 10;
100+
101+ let mut children = bumpalo::collections::Vec::new_in(cx.bump);
102+ self.triangle(cx, 0.0, 0.0, 1000.0, modulus, &mut children);
103+
104+ div(&cx)
105+ .attr("class", "container")
106+ .attr("style", self.container_transform(cx, elapsed))
107+ .children(children)
108+ .finish()
109+ }
110+}
111+
112+// Kick off a loop that keeps re-rendering on every animation frame.
113+fn animate(window: Window, vdom: Vdom) {
114+ let rc = <Rc<RefCell<Option<Closure<dyn FnMut()>>>>>::default();
115+ let rc2 = rc.clone();
116+ let window2 = window.clone();
117+ let weak = vdom.weak();
118+ let f = Closure::wrap(Box::new(move || {
119+ weak.schedule_render();
120+ window
121+ .request_animation_frame(
122+ rc.borrow()
123+ .as_ref()
124+ .unwrap_throw()
125+ .as_ref()
126+ .unchecked_ref::<js_sys::Function>(),
127+ )
128+ .unwrap_throw();
129+ }) as Box<dyn FnMut()>);
130+ window2
131+ .request_animation_frame(f.as_ref().unchecked_ref::<js_sys::Function>())
132+ .unwrap_throw();
133+ *rc2.borrow_mut() = Some(f);
134+
135+ // Run the virtual DOM and its listeners forever.
136+ vdom.forget();
137+}
138+
139+#[wasm_bindgen(start)]
140+pub fn run() {
141+ // Set up the panic hook
142+ console_error_panic_hook::set_once();
143+
144+ // Get the scene element to render within
145+ let window = web_sys::window().unwrap();
146+ let document = window.document().unwrap();
147+ let scene = document.get_element_by_id("scene").unwrap();
148+
149+ // Construct a new Container component.
150+ let container = Container::new();
151+
152+ // Mount our container component to the scene div.
153+ let vdom = Vdom::new(&scene, container);
154+
155+ // Kick off animation loop
156+ animate(window, vdom);
157+}
examples/todomvc/Cargo.tomlnew file+47
@@ -0,0 +1,47 @@
1+[package]
2+authors = ["Nick Fitzgerald <fitzgen@gmail.com>"]
3+edition = "2018"
4+name = "dodrio-todomvc"
5+version = "0.1.0"
6+
7+[dependencies]
8+cfg-if = "0.1.10"
9+dodrio = { path = "../..", features = ["serde"] }
10+js-sys = "0.3.32"
11+serde = { features = ["derive"], version = "1.0.114" }
12+serde_json = "1.0.57"
13+wasm-bindgen = "0.2.55"
14+wasm-bindgen-futures = "0.4.5"
15+
16+# Optional dependencies for logging.
17+console_error_panic_hook = { optional = true, version = "0.1.6" }
18+console_log = { optional = true, version = "0.2.0" }
19+log = { optional = true, version = "0.4.11" }
20+
21+[dependencies.web-sys]
22+version = "0.3.32"
23+features = [
24+ "Document",
25+ "Event",
26+ "EventTarget",
27+ "HtmlElement",
28+ "HtmlInputElement",
29+ "KeyboardEvent",
30+ "Location",
31+ "Storage",
32+ "Node",
33+ "Window",
34+]
35+
36+[dev-dependencies]
37+wasm-bindgen-test = "0.3.5"
38+
39+[features]
40+logging = [
41+ "console_log",
42+ "console_error_panic_hook",
43+ "log",
44+]
45+
46+[lib]
47+crate-type = ["cdylib"]
examples/todomvc/README.mdnew file+33
@@ -0,0 +1,33 @@
1+# TodoMVC
2+
3+`dodrio` implementation of the popular [TodoMVC](http://todomvc.com/) app. It
4+correctly and completely fulfills [the
5+specification](https://github.com/tastejs/todomvc/blob/master/app-spec.md) to
6+the best of my knowledge.
7+
8+## Source
9+
10+There are a number of modules in this `dodrio` implementation of TodoMVC. The
11+most important are:
12+
13+* `src/lib.rs`: The entry point to the application.
14+* `src/todos.rs`: Definition of `Todos` model and its rendering.
15+* `src/todo.rs`: Definition of `Todo` model and its rendering.
16+* `src/controller.rs`: The controller handles UI interactions and translates
17+ them into updates on the model. Finally, it triggers re-rendering after those
18+ updates.
19+* `src/router.rs`: A simple URL hash-based router.
20+
21+## Build
22+
23+```
24+wasm-pack build --target web
25+```
26+
27+## Serve
28+
29+Use any HTTP server, for example:
30+
31+```
32+python -m SimpleHTTPServer
33+```
examples/todomvc/index.htmlnew file+21
@@ -0,0 +1,21 @@
1+<!doctype html>
2+<html lang="en">
3+ <head>
4+ <meta charset="utf-8">
5+ <meta name="viewport" content="width=device-width, initial-scale=1">
6+ <title>dodrio • TodoMVC</title>
7+ <link rel="stylesheet" href="https://unpkg.com/todomvc-common@^1.0.0/base.css">
8+ <link rel="stylesheet" href="https://unpkg.com/todomvc-app-css@^2.0.0/index.css">
9+ </head>
10+ <body>
11+ <section class="todoapp">
12+ </section>
13+ <footer class="info">
14+ <p>Double-click to edit a todo</p>
15+ </footer>
16+ <script type="module">
17+ import init from "./pkg/dodrio_todomvc.js";
18+ init("./pkg/dodrio_todomvc_bg.wasm");
19+ </script>
20+ </body>
21+</html>
examples/todomvc/src/controller.rsnew file+140
@@ -0,0 +1,140 @@
1+//! The controller handles UI events, translates them into updates on the model,
2+//! and schedules re-renders.
3+
4+use crate::todo::{Todo, TodoActions};
5+use crate::todos::{Todos, TodosActions};
6+use crate::visibility::Visibility;
7+use dodrio::{RootRender, VdomWeak};
8+use serde::{Deserialize, Serialize};
9+use std::ops::{Deref, DerefMut};
10+
11+/// The controller for the TodoMVC app.
12+///
13+/// This `Controller` struct is never actually instantiated. It is only used for
14+/// its `*Actions` trait implementations, none of which take a `self` parameter.
15+///
16+/// One could imagine alternative controller implementations with `*Actions`
17+/// trait implementations for (e.g.) testing that will assert various expected
18+/// action methods are called after rendering todo items and sending DOM events.
19+#[derive(Default, Deserialize, Serialize)]
20+pub struct Controller;
21+
22+impl TodosActions for Controller {
23+ fn toggle_all(root: &mut dyn RootRender, vdom: VdomWeak) {
24+ let mut todos = AutoCommitTodos::new(root, vdom);
25+ let all_complete = todos.todos().iter().all(|t| t.is_complete());
26+ for t in todos.todos_mut() {
27+ t.set_complete(!all_complete);
28+ }
29+ }
30+
31+ fn update_draft(root: &mut dyn RootRender, vdom: VdomWeak, draft: String) {
32+ let mut todos = AutoCommitTodos::new(root, vdom);
33+ todos.set_draft(draft);
34+ }
35+
36+ fn finish_draft(root: &mut dyn RootRender, vdom: VdomWeak) {
37+ let mut todos = AutoCommitTodos::new(root, vdom);
38+ let title = todos.take_draft();
39+ let title = title.trim();
40+ if !title.is_empty() {
41+ let id = todos.todos().len();
42+ let new = Todo::new(id, title);
43+ todos.add_todo(new);
44+ }
45+ }
46+
47+ fn change_visibility(root: &mut dyn RootRender, vdom: VdomWeak, vis: Visibility) {
48+ let mut todos = AutoCommitTodos::new(root, vdom);
49+ todos.set_visibility(vis);
50+ }
51+
52+ fn delete_completed(root: &mut dyn RootRender, vdom: VdomWeak) {
53+ let mut todos = AutoCommitTodos::new(root, vdom);
54+ todos.delete_completed();
55+ }
56+}
57+
58+impl TodoActions for Controller {
59+ fn toggle_completed(root: &mut dyn RootRender, vdom: VdomWeak, id: usize) {
60+ let mut todos = AutoCommitTodos::new(root, vdom);
61+ let t = &mut todos.todos_mut()[id];
62+ let completed = t.is_complete();
63+ t.set_complete(!completed);
64+ }
65+
66+ fn delete(root: &mut dyn RootRender, vdom: VdomWeak, id: usize) {
67+ let mut todos = AutoCommitTodos::new(root, vdom);
68+ todos.delete_todo(id);
69+ }
70+
71+ fn begin_editing(root: &mut dyn RootRender, vdom: VdomWeak, id: usize) {
72+ let mut todos = AutoCommitTodos::new(root, vdom);
73+ let t = &mut todos.todos_mut()[id];
74+ let desc = t.title().to_string();
75+ t.set_edits(Some(desc));
76+ }
77+
78+ fn update_edits(root: &mut dyn RootRender, vdom: VdomWeak, id: usize, edits: String) {
79+ let mut todos = AutoCommitTodos::new(root, vdom);
80+ let t = &mut todos.todos_mut()[id];
81+ t.set_edits(Some(edits));
82+ }
83+
84+ fn finish_edits(root: &mut dyn RootRender, vdom: VdomWeak, id: usize) {
85+ let mut todos = AutoCommitTodos::new(root, vdom);
86+ let t = &mut todos.todos_mut()[id];
87+ if let Some(edits) = t.take_edits() {
88+ let edits = edits.trim();
89+ if edits.is_empty() {
90+ todos.delete_todo(id);
91+ } else {
92+ t.set_title(edits);
93+ }
94+ }
95+ }
96+
97+ fn cancel_edits(root: &mut dyn RootRender, vdom: VdomWeak, id: usize) {
98+ let mut todos = AutoCommitTodos::new(root, vdom);
99+ let t = &mut todos.todos_mut()[id];
100+ let _ = t.take_edits();
101+ }
102+}
103+
104+/// An RAII type that dereferences to a `Todos` and once it is dropped, saves
105+/// the (presumably just modified) todos to local storage, and schedules a new
106+/// `dodrio` render.
107+pub struct AutoCommitTodos<'a> {
108+ todos: &'a mut Todos,
109+ vdom: VdomWeak,
110+}
111+
112+impl AutoCommitTodos<'_> {
113+ /// Construct a new `AutoCommitTodos` from the root rendering component and
114+ /// `vdom` handle.
115+ pub fn new(root: &mut dyn RootRender, vdom: VdomWeak) -> AutoCommitTodos {
116+ let todos = root.unwrap_mut::<Todos>();
117+ AutoCommitTodos { todos, vdom }
118+ }
119+}
120+
121+impl Drop for AutoCommitTodos<'_> {
122+ fn drop(&mut self) {
123+ self.todos.save_to_local_storage();
124+ self.vdom.schedule_render();
125+ }
126+}
127+
128+impl Deref for AutoCommitTodos<'_> {
129+ type Target = Todos;
130+
131+ fn deref(&self) -> &Todos {
132+ &self.todos
133+ }
134+}
135+
136+impl DerefMut for AutoCommitTodos<'_> {
137+ fn deref_mut(&mut self) -> &mut Todos {
138+ &mut self.todos
139+ }
140+}
examples/todomvc/src/keys.rsnew file+7
@@ -0,0 +1,7 @@
1+//! Constants for `KeyboardEvent::key_code`.`
2+
3+/// The key code for the enter key.
4+pub const ENTER: u32 = 13;
5+
6+/// The key code for the escape key.
7+pub const ESCAPE: u32 = 27;
examples/todomvc/src/lib.rsnew file+60
@@ -0,0 +1,60 @@
1+//! TodoMVC implemented in `dodrio`!
2+
3+#![deny(missing_docs)]
4+
5+pub mod controller;
6+pub mod keys;
7+pub mod router;
8+pub mod todo;
9+pub mod todos;
10+pub mod utils;
11+pub mod visibility;
12+
13+use crate::controller::Controller;
14+use crate::todos::Todos;
15+use dodrio::Vdom;
16+use wasm_bindgen::prelude::*;
17+
18+/// Run the TodoMVC app!
19+///
20+/// Since this is marked `#[wasm_bindgen(start)]` it is automatically invoked
21+/// once the wasm module instantiated on the Web page.
22+#[wasm_bindgen(start)]
23+pub fn run() -> Result<(), JsValue> {
24+ // Set up the logging for debugging if/when things go wrong.
25+ init_logging();
26+
27+ // Grab the todo app container.
28+ let document = utils::document();
29+ let container = document
30+ .query_selector(".todoapp")?
31+ .ok_or_else(|| js_sys::Error::new("could not find `.todoapp` container"))?;
32+
33+ // Create a new `Todos` render component.
34+ let todos = Todos::<Controller>::new();
35+
36+ // Create a virtual DOM and mount it and the `Todos` render component.
37+ let vdom = Vdom::new(&container, todos);
38+
39+ // Start the URL router.
40+ router::start(vdom.weak());
41+
42+ // Run the virtual DOM forever and don't unmount it.
43+ vdom.forget();
44+
45+ Ok(())
46+}
47+
48+cfg_if::cfg_if! {
49+ if #[cfg(feature = "logging")] {
50+ fn init_logging() {
51+ console_error_panic_hook::set_once();
52+ console_log::init_with_level(log::Level::Trace)
53+ .expect_throw("should initialize logging OK");
54+ }
55+ } else {
56+ fn init_logging() {
57+ // Do nothing.
58+ }
59+ }
60+}
examples/todomvc/src/router.rsnew file+68
@@ -0,0 +1,68 @@
1+//! A simple `#`-fragment router.
2+
3+use crate::todos::Todos;
4+use crate::utils;
5+use crate::visibility::Visibility;
6+use dodrio::VdomWeak;
7+use std::str::FromStr;
8+use wasm_bindgen::{prelude::*, JsCast};
9+
10+/// Start the router.
11+pub fn start(vdom: VdomWeak) {
12+ // Callback fired whenever the URL's hash fragment changes. Keeps the root
13+ // todos collection's visibility in sync with the `#` fragment.
14+ let on_hash_change = move || {
15+ let new_vis = utils::hash()
16+ .and_then(|hash| {
17+ if hash.starts_with("#/") {
18+ Visibility::from_str(&hash[2..]).ok()
19+ } else {
20+ None
21+ }
22+ })
23+ .unwrap_or_else(|| {
24+ // If we couldn't parse a visibility, make sure we canonicalize
25+ // it back to our default hash.
26+ let v = Visibility::default();
27+ utils::set_hash(&format!("#/{}", v));
28+ v
29+ });
30+
31+ wasm_bindgen_futures::spawn_local({
32+ let vdom = vdom.clone();
33+ async move {
34+ let _ = vdom
35+ .with_component({
36+ let vdom = vdom.clone();
37+ move |root| {
38+ let todos = root.unwrap_mut::<Todos>();
39+ // If the todos' visibility already matches the event's
40+ // visibility, then there is nothing to do (ha). If they
41+ // don't match, then we need to update the todos' visibility
42+ // and re-render.
43+ if todos.visibility() != new_vis {
44+ todos.set_visibility(new_vis);
45+ vdom.schedule_render();
46+ }
47+ }
48+ })
49+ .await;
50+ }
51+ });
52+ };
53+
54+ // Call it once to handle the initial `#` fragment.
55+ on_hash_change();
56+
57+ // Now listen for hash changes forever.
58+ //
59+ // Note that if we ever intended to unmount our todos app, we would want to
60+ // provide a method for removing this router's event listener and cleaning
61+ // up after ourselves.
62+ let on_hash_change = Closure::wrap(Box::new(on_hash_change) as Box<dyn FnMut()>);
63+ let window = utils::window();
64+ window
65+ .add_event_listener_with_callback("hashchange", on_hash_change.as_ref().unchecked_ref())
66+ .unwrap_throw();
67+ on_hash_change.forget();
68+}
examples/todomvc/src/todo.rsnew file+229
@@ -0,0 +1,229 @@
1+//! Type definition and `dodrio::Render` implementation for a single todo item.
2+
3+use crate::keys;
4+use dodrio::{Cached, Node, Render, RenderContext, RootRender, VdomWeak};
5+use serde::{Deserialize, Serialize};
6+use std::marker::PhantomData;
7+use wasm_bindgen::{prelude::*, JsCast};
8+
9+/// A single todo item.
10+#[derive(Default)]
11+pub struct Todo<C> {
12+ inner: Cached<TodoInner<C>>,
13+}
14+
15+#[derive(Serialize, Deserialize)]
16+struct TodoInner<C> {
17+ id: usize,
18+ title: String,
19+ completed: bool,
20+
21+ #[serde(skip)]
22+ edits: Option<String>,
23+
24+ #[serde(skip)]
25+ _controller: PhantomData<C>,
26+}
27+
28+/// Actions on a single todo item that can be triggered from the UI.
29+pub trait TodoActions {
30+ /// Toggle the completion state of the todo item with the given id.
31+ fn toggle_completed(root: &mut dyn RootRender, vdom: VdomWeak, id: usize);
32+
33+ /// Delete the todo item with the given id.
34+ fn delete(root: &mut dyn RootRender, vdom: VdomWeak, id: usize);
35+
36+ /// Begin editing the todo item with the given id.
37+ fn begin_editing(root: &mut dyn RootRender, vdom: VdomWeak, id: usize);
38+
39+ /// Update the edits for the todo with the given id.
40+ fn update_edits(root: &mut dyn RootRender, vdom: VdomWeak, id: usize, edits: String);
41+
42+ /// Finish editing the todo with the given id.
43+ fn finish_edits(root: &mut dyn RootRender, vdom: VdomWeak, id: usize);
44+
45+ /// Cancel editing the todo with the given id.
46+ fn cancel_edits(root: &mut dyn RootRender, vdom: VdomWeak, id: usize);
47+}
48+
49+impl<C> Todo<C> {
50+ /// Construct a new `Todo` with the given identifier and title.
51+ pub fn new<S: Into<String>>(id: usize, title: S) -> Self {
52+ let title = title.into();
53+ let completed = false;
54+ let edits = None;
55+ Todo {
56+ inner: Cached::new(TodoInner {
57+ id,
58+ title,
59+ completed,
60+ edits,
61+ _controller: PhantomData,
62+ }),
63+ }
64+ }
65+
66+ /// Set this todo item's id.
67+ pub fn set_id(&mut self, id: usize) {
68+ if id != self.inner.id {
69+ Cached::invalidate(&self.inner);
70+ }
71+ self.inner.id = id;
72+ }
73+
74+ /// Is this `Todo` complete?
75+ pub fn is_complete(&self) -> bool {
76+ self.inner.completed
77+ }
78+
79+ /// Mark the `Todo` as complete or not.
80+ pub fn set_complete(&mut self, to: bool) {
81+ if to != self.inner.completed {
82+ Cached::invalidate(&self.inner);
83+ }
84+ self.inner.completed = to;
85+ }
86+
87+ /// Get this todo's title.
88+ pub fn title(&self) -> &str {
89+ &self.inner.title
90+ }
91+
92+ /// Set this todo item's title.
93+ pub fn set_title<S: Into<String>>(&mut self, title: S) {
94+ let title = title.into();
95+ if title != self.inner.title {
96+ Cached::invalidate(&self.inner);
97+ }
98+ self.inner.title = title;
99+ }
100+
101+ /// Set the edits for this todo.
102+ pub fn set_edits<S: Into<String>>(&mut self, edits: Option<S>) {
103+ let edits = edits.map(Into::into);
104+ if edits != self.inner.edits {
105+ Cached::invalidate(&self.inner);
106+ }
107+ self.inner.edits = edits;
108+ }
109+
110+ /// Take this todo's edits, leaving `None` in their place.
111+ pub fn take_edits(&mut self) -> Option<String> {
112+ if self.inner.edits.is_some() {
113+ Cached::invalidate(&self.inner);
114+ }
115+ self.inner.edits.take()
116+ }
117+}
118+
119+impl<'a, C: TodoActions> Render<'a> for Todo<C> {
120+ fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
121+ use dodrio::{
122+ builder::*,
123+ bumpalo::{self, collections::String},
124+ };
125+
126+ let id = self.inner.id;
127+ let title = self.inner.edits.as_ref().unwrap_or(&self.inner.title);
128+ let title = bumpalo::format!(in cx.bump, "{}", title).into_bump_str();
129+
130+ li(&cx)
131+ .attr("class", {
132+ let mut class = String::new_in(cx.bump);
133+ if self.inner.completed {
134+ class.push_str("completed ");
135+ }
136+ if self.inner.edits.is_some() {
137+ class.push_str("editing");
138+ }
139+ class.into_bump_str()
140+ })
141+ .children([
142+ div(&cx)
143+ .attr("class", "view")
144+ .children([
145+ input(&cx)
146+ .attr("class", "toggle")
147+ .attr("type", "checkbox")
148+ .bool_attr("checked", self.inner.completed)
149+ .on("click", move |root, vdom, _event| {
150+ C::toggle_completed(root, vdom, id);
151+ })
152+ .finish(),
153+ label(&cx)
154+ .on("dblclick", move |root, vdom, _event| {
155+ C::begin_editing(root, vdom, id);
156+ })
157+ .children([text(title)])
158+ .finish(),
159+ button(&cx)
160+ .attr("class", "destroy")
161+ .on("click", move |root, vdom, _event| {
162+ C::delete(root, vdom, id);
163+ })
164+ .finish(),
165+ ])
166+ .finish(),
167+ input(&cx)
168+ .attr("class", "edit")
169+ .attr("value", title)
170+ .attr("name", "title")
171+ .attr(
172+ "id",
173+ bumpalo::format!(in cx.bump, "todo-{}", id).into_bump_str(),
174+ )
175+ .on("input", move |root, vdom, event| {
176+ let input = event
177+ .target()
178+ .unwrap_throw()
179+ .unchecked_into::<web_sys::HtmlInputElement>();
180+ C::update_edits(root, vdom, id, input.value());
181+ })
182+ .on("blur", move |root, vdom, _event| {
183+ C::finish_edits(root, vdom, id);
184+ })
185+ .on("keydown", move |root, vdom, event| {
186+ let event = event.unchecked_into::<web_sys::KeyboardEvent>();
187+ match event.key_code() {
188+ keys::ENTER => C::finish_edits(root, vdom, id),
189+ keys::ESCAPE => C::cancel_edits(root, vdom, id),
190+ _ => {}
191+ }
192+ })
193+ .finish(),
194+ ])
195+ .finish()
196+ }
197+}
198+
199+impl<C> Default for TodoInner<C> {
200+ fn default() -> Self {
201+ TodoInner {
202+ id: Default::default(),
203+ title: Default::default(),
204+ completed: Default::default(),
205+ edits: Default::default(),
206+ _controller: PhantomData,
207+ }
208+ }
209+}
210+
211+impl<C> serde::Serialize for Todo<C> {
212+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
213+ where
214+ S: serde::Serializer,
215+ {
216+ self.inner.serialize(serializer)
217+ }
218+}
219+
220+impl<'de, C> Deserialize<'de> for Todo<C> {
221+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
222+ where
223+ D: serde::Deserializer<'de>,
224+ {
225+ Ok(Todo {
226+ inner: <Cached<TodoInner<C>>>::deserialize(deserializer)?,
227+ })
228+ }
229+}
examples/todomvc/src/todos.rsnew file+300
@@ -0,0 +1,300 @@
1+//! Type definitions and `dodrio::Render` implementation for a collection of
2+//! todo items.
3+
4+use crate::controller::Controller;
5+use crate::todo::{Todo, TodoActions};
6+use crate::visibility::Visibility;
7+use crate::{keys, utils};
8+use dodrio::{bumpalo, Node, Render, RenderContext, RootRender, VdomWeak};
9+use serde::{Deserialize, Serialize};
10+use std::marker::PhantomData;
11+use std::mem;
12+use wasm_bindgen::{prelude::*, JsCast};
13+
14+/// A collection of todos.
15+#[derive(Default, Serialize, Deserialize)]
16+#[serde(rename = "todos-dodrio", bound = "")]
17+pub struct Todos<C = Controller> {
18+ todos: Vec<Todo<C>>,
19+
20+ #[serde(skip)]
21+ draft: String,
22+
23+ #[serde(skip)]
24+ visibility: Visibility,
25+
26+ #[serde(skip)]
27+ _controller: PhantomData<C>,
28+}
29+
30+/// Actions for `Todos` that can be triggered by UI interactions.
31+pub trait TodosActions: 'static + TodoActions {
32+ /// Toggle the completion state of all todo items.
33+ fn toggle_all(root: &mut dyn RootRender, vdom: VdomWeak);
34+
35+ /// Update the draft todo item's text.
36+ fn update_draft(root: &mut dyn RootRender, vdom: VdomWeak, draft: String);
37+
38+ /// Finish the current draft todo item and add it to the collection of
39+ /// todos.
40+ fn finish_draft(root: &mut dyn RootRender, vdom: VdomWeak);
41+
42+ /// Change the todo item visibility filtering to the given `Visibility`.
43+ fn change_visibility(root: &mut dyn RootRender, vdom: VdomWeak, vis: Visibility);
44+
45+ /// Delete all completed todo items.
46+ fn delete_completed(root: &mut dyn RootRender, vdom: VdomWeak);
47+}
48+
49+impl<C> Todos<C> {
50+ /// Construct a new todos set.
51+ ///
52+ /// If an existing set is available in local storage, then use that,
53+ /// otherwise create a new set.
54+ pub fn new() -> Self
55+ where
56+ C: Default,
57+ {
58+ Self::from_local_storage().unwrap_or_default()
59+ }
60+
61+ /// Deserialize a set of todos from local storage.
62+ pub fn from_local_storage() -> Option<Self> {
63+ utils::local_storage()
64+ .get("todomvc-dodrio")
65+ .ok()
66+ .and_then(|opt| opt)
67+ .and_then(|json| serde_json::from_str(&json).ok())
68+ }
69+
70+ /// Serialize this set of todos to local storage.
71+ pub fn save_to_local_storage(&self) {
72+ let serialized = serde_json::to_string(self).unwrap_throw();
73+ utils::local_storage()
74+ .set("todomvc-dodrio", &serialized)
75+ .unwrap_throw();
76+ }
77+
78+ /// Add a new todo item to this collection.
79+ pub fn add_todo(&mut self, todo: Todo<C>) {
80+ self.todos.push(todo);
81+ }
82+
83+ /// Delete the todo with the given id.
84+ pub fn delete_todo(&mut self, id: usize) {
85+ self.todos.remove(id);
86+ self.fix_ids();
87+ }
88+
89+ /// Delete all completed todo items.
90+ pub fn delete_completed(&mut self) {
91+ self.todos.retain(|t| !t.is_complete());
92+ self.fix_ids();
93+ }
94+
95+ // Fix all todo identifiers so that they match their index once again.
96+ fn fix_ids(&mut self) {
97+ for (id, todo) in self.todos.iter_mut().enumerate() {
98+ todo.set_id(id);
99+ }
100+ }
101+
102+ /// Get a shared slice of the underlying set of todo items.
103+ pub fn todos(&self) -> &[Todo<C>] {
104+ &self.todos
105+ }
106+
107+ /// Get an exclusive slice of the underlying set of todo items.
108+ pub fn todos_mut(&mut self) -> &mut [Todo<C>] {
109+ &mut self.todos
110+ }
111+
112+ /// Set the draft todo item text.
113+ pub fn set_draft<S: Into<String>>(&mut self, draft: S) {
114+ self.draft = draft.into();
115+ }
116+
117+ /// Take the current draft text and replace it with an empty string.
118+ pub fn take_draft(&mut self) -> String {
119+ mem::replace(&mut self.draft, String::new())
120+ }
121+
122+ /// Get the current visibility for these todos.
123+ pub fn visibility(&self) -> Visibility {
124+ self.visibility
125+ }
126+
127+ /// Set the visibility for these todoS.
128+ pub fn set_visibility(&mut self, vis: Visibility) {
129+ self.visibility = vis;
130+ }
131+}
132+
133+/// Rendering helpers.
134+impl<C: TodosActions> Todos<C> {
135+ fn header<'a>(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
136+ use dodrio::builder::*;
137+
138+ header(&cx)
139+ .attr("class", "header")
140+ .children([
141+ h1(&cx).children([text("todos")]).finish(),
142+ input(&cx)
143+ .on("input", |root, vdom, event| {
144+ let input = event
145+ .target()
146+ .unwrap_throw()
147+ .unchecked_into::<web_sys::HtmlInputElement>();
148+ C::update_draft(root, vdom, input.value());
149+ })
150+ .on("keydown", |root, vdom, event| {
151+ let event = event.unchecked_into::<web_sys::KeyboardEvent>();
152+ if event.key_code() == keys::ENTER {
153+ C::finish_draft(root, vdom);
154+ }
155+ })
156+ .attr("class", "new-todo")
157+ .attr("placeholder", "What needs to be done?")
158+ .attr("autofocus", "")
159+ .attr(
160+ "value",
161+ bumpalo::format!(in cx.bump, "{}", self.draft).into_bump_str(),
162+ )
163+ .finish(),
164+ ])
165+ .finish()
166+ }
167+
168+ fn todos_list<'a>(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
169+ use dodrio::{builder::*, bumpalo::collections::Vec};
170+
171+ let mut todos = Vec::with_capacity_in(self.todos.len(), cx.bump);
172+ todos.extend(
173+ self.todos
174+ .iter()
175+ .filter(|t| match self.visibility {
176+ Visibility::All => true,
177+ Visibility::Active => !t.is_complete(),
178+ Visibility::Completed => t.is_complete(),
179+ })
180+ .map(|t| t.render(cx)),
181+ );
182+
183+ section(&cx)
184+ .attr("class", "main")
185+ .attr(
186+ "visibility",
187+ if self.todos.is_empty() {
188+ "hidden"
189+ } else {
190+ "visible"
191+ },
192+ )
193+ .children([
194+ input(&cx)
195+ .attr("class", "toggle-all")
196+ .attr("id", "toggle-all")
197+ .attr("type", "checkbox")
198+ .attr("name", "toggle")
199+ .bool_attr("checked", self.todos.iter().all(|t| t.is_complete()))
200+ .on("click", |root, vdom, _event| {
201+ C::toggle_all(root, vdom);
202+ })
203+ .finish(),
204+ label(&cx)
205+ .attr("for", "toggle-all")
206+ .children([text("Mark all as complete")])
207+ .finish(),
208+ ul(&cx).attr("class", "todo-list").children(todos).finish(),
209+ ])
210+ .finish()
211+ }
212+
213+ fn footer<'a>(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
214+ use dodrio::builder::*;
215+
216+ let completed_count = self.todos.iter().filter(|t| t.is_complete()).count();
217+ let incomplete_count = self.todos.len() - completed_count;
218+ let items_left = if incomplete_count == 1 {
219+ " item left"
220+ } else {
221+ " items left"
222+ };
223+ let incomplete_count = bumpalo::format!(in cx.bump, "{}", incomplete_count);
224+
225+ let clear_completed_text = bumpalo::format!(
226+ in cx.bump,
227+ "Clear completed ({})",
228+ self.todos.iter().filter(|t| t.is_complete()).count()
229+ );
230+
231+ footer(&cx)
232+ .attr("class", "footer")
233+ .bool_attr("hidden", self.todos.is_empty())
234+ .children([
235+ span(&cx)
236+ .attr("class", "todo-count")
237+ .children([
238+ strong(&cx)
239+ .children([text(incomplete_count.into_bump_str())])
240+ .finish(),
241+ text(items_left),
242+ ])
243+ .finish(),
244+ ul(&cx)
245+ .attr("class", "filters")
246+ .children([
247+ self.visibility_swap(cx, "#/", Visibility::All),
248+ self.visibility_swap(cx, "#/active", Visibility::Active),
249+ self.visibility_swap(cx, "#/completed", Visibility::Completed),
250+ ])
251+ .finish(),
252+ button(&cx)
253+ .on("click", |root, vdom, _event| {
254+ C::delete_completed(root, vdom);
255+ })
256+ .attr("class", "clear-completed")
257+ .bool_attr("hidden", completed_count == 0)
258+ .children([text(clear_completed_text.into_bump_str())])
259+ .finish(),
260+ ])
261+ .finish()
262+ }
263+
264+ fn visibility_swap<'a>(
265+ &self,
266+ cx: &mut RenderContext<'a>,
267+ url: &'static str,
268+ target_vis: Visibility,
269+ ) -> Node<'a> {
270+ use dodrio::builder::*;
271+
272+ li(&cx)
273+ .on("click", move |root, vdom, _event| {
274+ C::change_visibility(root, vdom, target_vis);
275+ })
276+ .children([a(&cx)
277+ .attr("href", url)
278+ .attr(
279+ "class",
280+ if self.visibility == target_vis {
281+ "selected"
282+ } else {
283+ ""
284+ },
285+ )
286+ .children([text(target_vis.label())])
287+ .finish()])
288+ .finish()
289+ }
290+}
291+
292+impl<'a, C: TodosActions> Render<'a> for Todos<C> {
293+ fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
294+ use dodrio::builder::*;
295+
296+ div(&cx)
297+ .children([self.header(cx), self.todos_list(cx), self.footer(cx)])
298+ .finish()
299+ }
300+}
examples/todomvc/src/utils.rsnew file+32
@@ -0,0 +1,32 @@
1+//! Small utility functions.
2+
3+use wasm_bindgen::UnwrapThrowExt;
4+
5+/// Get the top-level window.
6+pub fn window() -> web_sys::Window {
7+ web_sys::window().unwrap_throw()
8+}
9+
10+/// Get the current location hash, if any.
11+pub fn hash() -> Option<String> {
12+ window()
13+ .location()
14+ .hash()
15+ .ok()
16+ .and_then(|h| if h.is_empty() { None } else { Some(h) })
17+}
18+
19+/// Set the current location hash.
20+pub fn set_hash(hash: &str) {
21+ window().location().set_hash(hash).unwrap_throw();
22+}
23+
24+/// Get the top-level document.
25+pub fn document() -> web_sys::Document {
26+ window().document().unwrap_throw()
27+}
28+
29+/// Get the top-level window's local storage.
30+pub fn local_storage() -> web_sys::Storage {
31+ window().local_storage().unwrap_throw().unwrap_throw()
32+}
examples/todomvc/src/visibility.rsnew file+51
@@ -0,0 +1,51 @@
1+//! Visibility filtering.
2+
3+use std::fmt;
4+use std::str::FromStr;
5+
6+/// The visibility filtering for todo items.
7+#[derive(Clone, Copy, Eq, PartialEq)]
8+pub enum Visibility {
9+ /// Show all todos.
10+ All,
11+ /// Show only active, incomplete todos.
12+ Active,
13+ /// Show only inactive, completed todos.
14+ Completed,
15+}
16+
17+impl Default for Visibility {
18+ fn default() -> Visibility {
19+ Visibility::All
20+ }
21+}
22+
23+impl FromStr for Visibility {
24+ type Err = ();
25+
26+ fn from_str(s: &str) -> Result<Self, Self::Err> {
27+ match s {
28+ "all" => Ok(Visibility::All),
29+ "active" => Ok(Visibility::Active),
30+ "completed" => Ok(Visibility::Completed),
31+ _ => Err(()),
32+ }
33+ }
34+}
35+
36+impl fmt::Display for Visibility {
37+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
38+ write!(f, "{}", self.label().to_lowercase())
39+ }
40+}
41+
42+impl Visibility {
43+ /// Get a string label for this visibility.
44+ pub fn label(self) -> &'static str {
45+ match self {
46+ Visibility::All => "All",
47+ Visibility::Active => "Active",
48+ Visibility::Completed => "Completed",
49+ }
50+ }
51+}
js/change-list-interpreter.jsnew file+377
@@ -0,0 +1,377 @@
1+const decoder = new TextDecoder();
2+
3+function top(stack) {
4+ return stack[stack.length - 1];
5+}
6+
7+function string(mem, pointer, length) {
8+ const buf = mem.subarray(pointer, pointer + length);
9+ return decoder.decode(buf);
10+}
11+
12+const OP_TABLE = [
13+ // 0
14+ function setText(interpreter, mem8, mem32, i) {
15+ const pointer = mem32[i++];
16+ const length = mem32[i++];
17+ const str = string(mem8, pointer, length);
18+ top(interpreter.stack).textContent = str;
19+ return i;
20+ },
21+
22+ // 1
23+ function removeSelfAndNextSiblings(interpreter, mem8, mem32, i) {
24+ const node = interpreter.stack.pop();
25+ let sibling = node.nextSibling;
26+ while (sibling) {
27+ const temp = sibling.nextSibling;
28+ sibling.remove();
29+ sibling = temp;
30+ }
31+ node.remove();
32+ return i;
33+ },
34+
35+ // 2
36+ function replaceWith(interpreter, mem8, mem32, i) {
37+ const newNode = interpreter.stack.pop();
38+ const oldNode = interpreter.stack.pop();
39+ oldNode.replaceWith(newNode);
40+ interpreter.stack.push(newNode);
41+ return i;
42+ },
43+
44+ // 3
45+ function setAttribute(interpreter, mem8, mem32, i) {
46+ const nameId = mem32[i++];
47+ const valueId = mem32[i++];
48+ const name = interpreter.getCachedString(nameId);
49+ const value = interpreter.getCachedString(valueId);
50+ const node = top(interpreter.stack);
51+ node.setAttribute(name, value);
52+
53+ // Some attributes are "volatile" and don't work through `setAttribute`.
54+ if (name === "value") {
55+ node.value = value;
56+ }
57+ if (name === "checked") {
58+ node.checked = true;
59+ }
60+ if (name === "selected") {
61+ node.selected = true;
62+ }
63+
64+ return i;
65+ },
66+
67+ // 4
68+ function removeAttribute(interpreter, mem8, mem32, i) {
69+ const nameId = mem32[i++];
70+ const name = interpreter.getCachedString(nameId);
71+ const node = top(interpreter.stack);
72+ node.removeAttribute(name);
73+
74+ // Some attributes are "volatile" and don't work through `removeAttribute`.
75+ if (name === "value") {
76+ node.value = null;
77+ }
78+ if (name === "checked") {
79+ node.checked = false;
80+ }
81+ if (name === "selected") {
82+ node.selected = false;
83+ }
84+
85+ return i;
86+ },
87+
88+ // 5
89+ function pushReverseChild(interpreter, mem8, mem32, i) {
90+ const n = mem32[i++];
91+ const parent = top(interpreter.stack);
92+ const children = parent.childNodes;
93+ const child = children[children.length - n - 1];
94+ interpreter.stack.push(child);
95+ return i;
96+ },
97+
98+ // 6
99+ function popPushChild(interpreter, mem8, mem32, i) {
100+ const n = mem32[i++];
101+ interpreter.stack.pop();
102+ const parent = top(interpreter.stack);
103+ const children = parent.childNodes;
104+ const child = children[n];
105+ interpreter.stack.push(child);
106+ return i;
107+ },
108+
109+ // 7
110+ function pop(interpreter, mem8, mem32, i) {
111+ interpreter.stack.pop();
112+ return i;
113+ },
114+
115+ // 8
116+ function appendChild(interpreter, mem8, mem32, i) {
117+ const child = interpreter.stack.pop();
118+ top(interpreter.stack).appendChild(child);
119+ return i;
120+ },
121+
122+ // 9
123+ function createTextNode(interpreter, mem8, mem32, i) {
124+ const pointer = mem32[i++];
125+ const length = mem32[i++];
126+ const text = string(mem8, pointer, length);
127+ interpreter.stack.push(document.createTextNode(text));
128+ return i;
129+ },
130+
131+ // 10
132+ function createElement(interpreter, mem8, mem32, i) {
133+ const tagNameId = mem32[i++];
134+ const tagName = interpreter.getCachedString(tagNameId);
135+ interpreter.stack.push(document.createElement(tagName));
136+ return i;
137+ },
138+
139+ // 11
140+ function newEventListener(interpreter, mem8, mem32, i) {
141+ const eventId = mem32[i++];
142+ const eventType = interpreter.getCachedString(eventId);
143+ const a = mem32[i++];
144+ const b = mem32[i++];
145+ const el = top(interpreter.stack);
146+ el.addEventListener(eventType, interpreter.eventHandler);
147+ el[`dodrio-a-${eventType}`] = a;
148+ el[`dodrio-b-${eventType}`] = b;
149+ return i;
150+ },
151+
152+ // 12
153+ function updateEventListener(interpreter, mem8, mem32, i) {
154+ const eventId = mem32[i++];
155+ const eventType = interpreter.getCachedString(eventId);
156+ const el = top(interpreter.stack);
157+ el[`dodrio-a-${eventType}`] = mem32[i++];
158+ el[`dodrio-b-${eventType}`] = mem32[i++];
159+ return i;
160+ },
161+
162+ // 13
163+ function removeEventListener(interpreter, mem8, mem32, i) {
164+ const eventId = mem32[i++];
165+ const eventType = interpreter.getCachedString(eventId);
166+ const el = top(interpreter.stack);
167+ el.removeEventListener(eventType, interpreter.eventHandler);
168+ return i;
169+ },
170+
171+ // 14
172+ function addCachedString(interpreter, mem8, mem32, i) {
173+ const pointer = mem32[i++];
174+ const length = mem32[i++];
175+ const id = mem32[i++];
176+ const str = string(mem8, pointer, length);
177+ interpreter.addCachedString(str, id);
178+ return i;
179+ },
180+
181+ // 15
182+ function dropCachedString(interpreter, mem8, mem32, i) {
183+ const id = mem32[i++];
184+ interpreter.dropCachedString(id);
185+ return i;
186+ },
187+
188+ // 16
189+ function createElementNS(interpreter, mem8, mem32, i) {
190+ const tagNameId = mem32[i++];
191+ const tagName = interpreter.getCachedString(tagNameId);
192+ const nsId = mem32[i++];
193+ const ns = interpreter.getCachedString(nsId);
194+ interpreter.stack.push(document.createElementNS(ns, tagName));
195+ return i;
196+ },
197+
198+ // 17
199+ function saveChildrenToTemporaries(interpreter, mem8, mem32, i) {
200+ let temp = mem32[i++];
201+ const start = mem32[i++];
202+ const end = mem32[i++];
203+ const parent = top(interpreter.stack);
204+ const children = parent.childNodes;
205+ for (let i = start; i < end; i++) {
206+ interpreter.temporaries[temp++] = children[i];
207+ }
208+ return i;
209+ },
210+
211+ // 18
212+ function pushChild(interpreter, mem8, mem32, i) {
213+ const parent = top(interpreter.stack);
214+ const n = mem32[i++];
215+ const child = parent.childNodes[n];
216+ interpreter.stack.push(child);
217+ return i;
218+ },
219+
220+ // 19
221+ function pushTemporary(interpreter, mem8, mem32, i) {
222+ const temp = mem32[i++];
223+ interpreter.stack.push(interpreter.temporaries[temp]);
224+ return i;
225+ },
226+
227+ // 20
228+ function insertBefore(interpreter, mem8, mem32, i) {
229+ const before = interpreter.stack.pop();
230+ const after = interpreter.stack.pop();
231+ after.parentNode.insertBefore(before, after);
232+ interpreter.stack.push(before);
233+ return i;
234+ },
235+
236+ // 21
237+ function popPushReverseChild(interpreter, mem8, mem32, i) {
238+ const n = mem32[i++];
239+ interpreter.stack.pop();
240+ const parent = top(interpreter.stack);
241+ const children = parent.childNodes;
242+ const child = children[children.length - n - 1];
243+ interpreter.stack.push(child);
244+ return i;
245+ },
246+
247+ // 22
248+ function removeChild(interpreter, mem8, mem32, i) {
249+ const n = mem32[i++];
250+ const parent = top(interpreter.stack);
251+ const child = parent.childNodes[n];
252+ child.remove();
253+ return i;
254+ },
255+
256+ // 23
257+ function setClass(interpreter, mem8, mem32, i) {
258+ const classId = mem32[i++];
259+ const className = interpreter.getCachedString(classId);
260+ top(interpreter.stack).className = className;
261+ return i;
262+ },
263+
264+ // 24
265+ function saveTemplate(interpreter, mem8, mem32, i) {
266+ const id = mem32[i++];
267+ const template = top(interpreter.stack);
268+ interpreter.saveTemplate(id, template.cloneNode(true));
269+ return i;
270+ },
271+
272+ // 25
273+ function pushTemplate(interpreter, mem8, mem32, i) {
274+ const id = mem32[i++];
275+ const template = interpreter.getTemplate(id);
276+ interpreter.stack.push(template.cloneNode(true));
277+ return i;
278+ }
279+];
280+
281+export class ChangeListInterpreter {
282+ constructor(container) {
283+ this.trampoline = null;
284+ this.container = container;
285+ this.ranges = [];
286+ this.stack = [];
287+ this.strings = new Map();
288+ this.temporaries = [];
289+ this.templates = new Map();
290+ }
291+
292+ unmount() {
293+ this.trampoline.mounted = false;
294+
295+ // Null out all of our properties just to ensure that if we mistakenly ever
296+ // call a method on this instance again, it will throw.
297+ this.trampoline = null;
298+ this.container = null;
299+ this.ranges = null;
300+ this.stack = null;
301+ this.strings = null;
302+ this.temporaries = null;
303+ this.templates = null;
304+ }
305+
306+ addChangeListRange(start, len) {
307+ this.ranges.push(start);
308+ this.ranges.push(len);
309+ }
310+
311+ applyChanges(memory) {
312+ if (this.ranges.length == 0) {
313+ return;
314+ }
315+
316+ this.stack.push(this.container.firstChild);
317+ const mem8 = new Uint8Array(memory.buffer);
318+ const mem32 = new Uint32Array(memory.buffer);
319+
320+ for (let i = 0; i < this.ranges.length; i += 2) {
321+ const start = this.ranges[i];
322+ const len = this.ranges[i + 1];
323+ this.applyChangeRange(mem8, mem32, start, len);
324+ }
325+
326+ this.ranges.length = 0;
327+ this.stack.length = 0;
328+ this.temporaries.length = 0;
329+ }
330+
331+ applyChangeRange(mem8, mem32, start, len) {
332+ const end = (start + len) / 4;
333+ for (let i = start / 4; i < end; ) {
334+ const op = mem32[i++];
335+ i = OP_TABLE[op](this, mem8, mem32, i);
336+ }
337+ }
338+
339+ addCachedString(str, id) {
340+ this.strings.set(id, str);
341+ }
342+
343+ dropCachedString(id) {
344+ this.strings.delete(id);
345+ }
346+
347+ getCachedString(id) {
348+ return this.strings.get(id);
349+ }
350+
351+ saveTemplate(id, template) {
352+ this.templates.set(id, template);
353+ }
354+
355+ getTemplate(id) {
356+ return this.templates.get(id);
357+ }
358+
359+ initEventsTrampoline(trampoline) {
360+ this.trampoline = trampoline;
361+ trampoline.mounted = true;
362+ this.eventHandler = function(event) {
363+ if (!trampoline.mounted) {
364+ throw new Error("invocation of listener after VDOM has been unmounted");
365+ }
366+
367+ // `this` always refers to the element the handler was added to.
368+ // Since we're adding the handler to all elements our content wants
369+ // to listen for events on, this ensures that we always get the right
370+ // values for `a` and `b`.
371+ const type = event.type;
372+ const a = this[`dodrio-a-${type}`];
373+ const b = this[`dodrio-b-${type}`];
374+ trampoline(event, a, b);
375+ }
376+ }
377+}
js/change-list.jsdeleted−231
@@ -1,231 +0,0 @@
1−const decoder = new TextDecoder();
2−
3−function top(stack) {
4− return stack[stack.length - 1];
5−}
6−
7−function string(mem, pointer, length) {
8− const buf = mem.subarray(pointer, pointer + length);
9− return decoder.decode(buf);
10−}
11−
12−const OP_TABLE = [
13− // 0
14− function setText(changeList, mem8, mem32, i) {
15− const pointer = mem32[i++];
16− const length = mem32[i++];
17− const str = string(mem8, pointer, length);
18− top(changeList.stack).textContent = str;
19− return i;
20− },
21−
22− // 1
23− function removeSelfAndNextSiblings(changeList, mem8, mem32, i) {
24− const node = changeList.stack.pop();
25− let sibling = node.nextSibling;
26− while (sibling) {
27− const temp = sibling.nextSibling;
28− sibling.remove();
29− sibling = temp;
30− }
31− node.remove();
32− return i;
33− },
34−
35− // 2
36− function replaceWith(changeList, mem8, mem32, i) {
37− const newNode = changeList.stack.pop();
38− const oldNode = changeList.stack.pop();
39− oldNode.replaceWith(newNode);
40− changeList.stack.push(newNode);
41− return i;
42− },
43−
44− // 3
45− function setAttribute(changeList, mem8, mem32, i) {
46− const pointer1 = mem32[i++];
47− const length1 = mem32[i++];
48− const name = string(mem8, pointer1, length1);
49− const pointer2 = mem32[i++];
50− const length2 = mem32[i++];
51− const value = string(mem8, pointer2, length2);
52− top(changeList.stack).setAttribute(name, value);
53− return i;
54− },
55−
56− // 4
57− function removeAttribute(changeList, mem8, mem32, i) {
58− const pointer = mem32[i++];
59− const length = mem32[i++];
60− const name = string(mem8, pointer, length);
61− top(changeList.stack).removeAttribute(name);
62− return i;
63− },
64−
65− // 5
66− function pushFirstChild(changeList, mem8, mem32, i) {
67− changeList.stack.push(top(changeList.stack).firstChild);
68− return i;
69− },
70−
71− // 6
72− function popPushNextSibling(changeList, mem8, mem32, i) {
73− const node = changeList.stack.pop();
74− changeList.stack.push(node.nextSibling);
75− return i;
76− },
77−
78− // 7
79− function pop(changeList, mem8, mem32, i) {
80− changeList.stack.pop();
81− return i;
82− },
83−
84− // 8
85− function appendChild(changeList, mem8, mem32, i) {
86− const child = changeList.stack.pop();
87− top(changeList.stack).appendChild(child);
88− return i;
89− },
90−
91− // 9
92− function createTextNode(changeList, mem8, mem32, i) {
93− const pointer = mem32[i++];
94− const length = mem32[i++];
95− const text = string(mem8, pointer, length);
96− changeList.stack.push(document.createTextNode(text));
97− return i;
98− },
99−
100− // 10
101− function createElement(changeList, mem8, mem32, i) {
102− const pointer = mem32[i++];
103− const length = mem32[i++];
104− const tagName = string(mem8, pointer, length);
105− changeList.stack.push(document.createElement(tagName));
106− return i;
107− },
108−
109− // 11
110− function newEventListener(changeList, mem8, mem32, i) {
111− const pointer = mem32[i++];
112− const length = mem32[i++];
113− const eventType = string(mem8, pointer, length);
114− const a = mem32[i++];
115− const b = mem32[i++];
116− const el = top(changeList.stack);
117− const listener = new Listener(a, b, eventType, changeList.eventsTrampoline, el);
118− changeList.listeners.add(listener);
119− el.addEventListener(eventType, listener.callback);
120− el[`dodrio-${eventType}`] = listener;
121− return i;
122− },
123−
124− // 12
125− function updateEventListener(changeList, mem8, mem32, i) {
126− const pointer = mem32[i++];
127− const length = mem32[i++];
128− const eventType = string(mem8, pointer, length);
129− const el = top(changeList.stack);
130− const listener = el[`dodrio-${eventType}`];
131− listener.a = mem32[i++];
132− listener.b = mem32[i++];
133− return i;
134− },
135−
136− // 13
137− function removeEventListener(changeList, mem8, mem32, i) {
138− const pointer = mem32[i++];
139− const length = mem32[i++];
140− const eventType = string(mem8, pointer, length);
141− const el = top(changeList.stack);
142− const listener = el[`dodrio-${eventType}`];
143− el.removeEventListener(eventType, listener.callback);
144− changeList.listeners.delete(listener);
145− return i;
146− }
147−];
148−
149−class Listener {
150− constructor(a, b, eventType, trampoline, el) {
151− this.a = a;
152− this.b = b;
153− this.eventType = eventType;
154− this.trampoline = trampoline;
155− this.el = el;
156− this.callback = this.callback.bind(this);
157− }
158−
159− callback(event) {
160− this.trampoline(event, this.a, this.b);
161− }
162−}
163−
164−// export
165−class ChangeList {
166− constructor(container) {
167− this.listeners = new Set();
168− this.trampoline = null;
169− this.container = container;
170− this.ranges = [];
171− this.stack = [];
172− }
173−
174− unmount() {
175− for (const listener of this.listeners) {
176− listener.el.removeEventListener(listener.eventType, listener.callback);
177− listener.trampoline = () => {
178− throw new Error("invocation of listener after it has been removed");
179− };
180− listener.a = 0;
181− listener.b = 0;
182− }
183−
184− // Null out all of our properties just to ensure that if we mistakenly ever
185− // call a method on this instance again, it will throw.
186− this.listeners = null;
187− this.trampoline = null;
188− this.container = null;
189− this.ranges = null;
190− this.stack = null;
191− }
192−
193− addChangeListRange(start, len) {
194− this.ranges.push(start);
195− this.ranges.push(len);
196− }
197−
198− applyChanges(memory) {
199− if (this.ranges.length == 0) {
200− return;
201− }
202−
203− this.stack.push(this.container.firstChild);
204− const mem8 = new Uint8Array(memory.buffer);
205− const mem32 = new Uint32Array(memory.buffer);
206−
207− for (let i = 0; i < this.ranges.length; i += 2) {
208− const start = this.ranges[i];
209− const len = this.ranges[i + 1];
210− this.applyChangeRange(mem8, mem32, start, len);
211− }
212−
213− this.ranges.length = 0;
214− this.stack.length = 0;
215− }
216−
217− applyChangeRange(mem8, mem32, start, len) {
218− const end = (start + len) / 4;
219− for (let i = start / 4; i < end; ) {
220− const op = mem32[i++];
221− i = OP_TABLE[op](this, mem8, mem32, i);
222− }
223− }
224−
225− initEventsTrampoline(trampoline) {
226− this.eventsTrampoline = (...args) => {
227− trampoline(...args);
228− };
229− }
230−}
231−window.ChangeList = ChangeList;
js/package-lock.jsondeleted−5
@@ -1,5 +0,0 @@
1−{
2− "name": "dodrio",
3− "version": "0.1.0",
4− "lockfileVersion": 1
5−}
js/package.jsondeleted−11
@@ -1,11 +0,0 @@
1−{
2− "name": "dodrio",
3− "version": "0.1.0",
4− "description": "Dodrio",
5− "main": "dodrio.js",
6− "scripts": {
7− "test": "echo \"Error: no test specified\" && exit 1"
8− },
9− "author": "Nick Fitzgerald <fitzgen@gmail.com>",
10− "license": "MPL-2.0"
11−}
js/strace.jsnew file+80
@@ -0,0 +1,80 @@
1+export function initStrace() {
2+ instrumentAll(Element.prototype);
3+ instrumentAll(Document.prototype);
4+ instrumentAll(Node.prototype);
5+ instrumentAll(EventTarget.prototype);
6+}
7+
8+const counts = new Map;
9+counts.total = 0;
10+
11+let timer = null;
12+
13+function log(ctorName, kind, key) {
14+ counts.total += 1;
15+
16+ const entry = `${kind} ${ctorName}#${key}`
17+ const count = counts.get(entry);
18+ if (count === undefined) {
19+ counts.set(entry, 1);
20+ } else {
21+ counts.set(entry, count + 1);
22+ }
23+
24+ if (timer !== null) {
25+ clearTimeout(timer);
26+ }
27+ timer = setTimeout(dumpAndReset, 100);
28+}
29+
30+function dumpAndReset() {
31+ const data = [...counts]
32+ .sort((a, b) => b[1] - a[1])
33+ .map(a => ({ "DOM Method": a[0], "Count": a[1] }));
34+
35+ data.push({ "DOM Method": "<total>", "Count": counts.total });
36+
37+ console.table(data, ["DOM Method", "Count"]);
38+
39+ counts.clear();
40+ counts.total = 0;
41+}
42+
43+function instrument(proto, key, desc) {
44+ const ctorName = proto.constructor.name;
45+
46+ if (typeof desc.value == "function") {
47+ const f = desc.value;
48+ desc.value = function (...args) {
49+ log(ctorName, "call", key);
50+ return f.apply(this, args);
51+ };
52+ }
53+
54+ if (typeof desc.get == "function") {
55+ const get = desc.get;
56+ desc.get = function (...args) {
57+ log(ctorName, "get", key);
58+ return get.apply(this, args);
59+ };
60+ }
61+
62+ if (typeof desc.set == "function") {
63+ const set = desc.set;
64+ desc.set = function (...args) {
65+ log(ctorName, "set", key);
66+ return set.apply(this, args);
67+ };
68+ }
69+
70+ Object.defineProperty(proto, key, desc);
71+}
72+
73+function instrumentAll(proto) {
74+ for (const key of Object.keys(proto)) {
75+ const desc = Object.getOwnPropertyDescriptor(proto, key);
76+ if (desc.configurable) {
77+ instrument(proto, key, desc);
78+ }
79+ }
80+}
publish.shnew file+11
@@ -0,0 +1,11 @@
1+#!/usr/bin/env bash
2+
3+set -ex
4+
5+cd "$(dirname "$0")"
6+
7+cd crates/js-api
8+cargo publish --no-verify $PUBLISH_FLAGS
9+cd ../..
10+
11+cargo publish $PUBLISH_FLAGS
src/builder.rsnew file+1075
@@ -0,0 +1,1075 @@
1+//! Helpers for building virtual DOM nodes.
2+
3+use crate::{node::NodeKey, Attribute, Listener, Node, RootRender, VdomWeak};
4+use bumpalo::Bump;
5+
6+/// A virtual DOM element builder.
7+///
8+/// Typically constructed with element-specific constructors, eg the `div`
9+/// function for building `<div>` elements or the `button` function for building
10+/// `<button>` elements.
11+#[derive(Clone, Debug)]
12+pub struct ElementBuilder<'a, Listeners, Attributes, Children>
13+where
14+ Listeners: 'a + AsRef<[Listener<'a>]>,
15+ Attributes: 'a + AsRef<[Attribute<'a>]>,
16+ Children: 'a + AsRef<[Node<'a>]>,
17+{
18+ bump: &'a Bump,
19+ key: NodeKey,
20+ tag_name: &'a str,
21+ listeners: Listeners,
22+ attributes: Attributes,
23+ children: Children,
24+ namespace: Option<&'a str>,
25+}
26+
27+impl<'a>
28+ ElementBuilder<
29+ 'a,
30+ bumpalo::collections::Vec<'a, Listener<'a>>,
31+ bumpalo::collections::Vec<'a, Attribute<'a>>,
32+ bumpalo::collections::Vec<'a, Node<'a>>,
33+ >
34+{
35+ /// Create a new `ElementBuilder` for an element with the given tag name.
36+ ///
37+ /// In general, only use this constructor if the tag is dynamic (i.e. you
38+ /// might build a `<div>` or you might build a `<span>` and you don't know
39+ /// until runtime). Prefer using the tag-specific constructors instead:
40+ /// `div(bump)` or `span(bump)`, etc.
41+ ///
42+ /// # Example
43+ ///
44+ /// ```
45+ /// use dodrio::{builder::*, bumpalo::Bump};
46+ ///
47+ /// let b = Bump::new();
48+ ///
49+ /// let tag_name = if flip_coin() {
50+ /// "div"
51+ /// } else {
52+ /// "span"
53+ /// };
54+ ///
55+ /// let my_element_builder = ElementBuilder::new(&b, tag_name);
56+ /// # fn flip_coin() -> bool { true }
57+ /// ```
58+ pub fn new<B>(bump: B, tag_name: &'a str) -> Self
59+ where
60+ B: Into<&'a Bump>,
61+ {
62+ let bump = bump.into();
63+ ElementBuilder {
64+ bump,
65+ key: NodeKey::NONE,
66+ tag_name,
67+ listeners: bumpalo::collections::Vec::new_in(bump),
68+ attributes: bumpalo::collections::Vec::new_in(bump),
69+ children: bumpalo::collections::Vec::new_in(bump),
70+ namespace: None,
71+ }
72+ }
73+}
74+
75+impl<'a, Listeners, Attributes, Children> ElementBuilder<'a, Listeners, Attributes, Children>
76+where
77+ Listeners: 'a + AsRef<[Listener<'a>]>,
78+ Attributes: 'a + AsRef<[Attribute<'a>]>,
79+ Children: 'a + AsRef<[Node<'a>]>,
80+{
81+ /// Set the listeners for this element.
82+ ///
83+ /// You can use this method to customize the backing storage for listeners,
84+ /// for example to use a fixed-size array instead of the default
85+ /// dynamically-sized `bumpalo::collections::Vec`.
86+ ///
87+ /// Any listeners already added to the builder will be overridden.
88+ ///
89+ /// # Example
90+ ///
91+ /// ```no_run
92+ /// use dodrio::{builder::*, bumpalo::Bump};
93+ ///
94+ /// let b = Bump::new();
95+ ///
96+ /// // Create a `<div>` with a fixed-size array of two listeners.
97+ /// let my_div = div(&b)
98+ /// .listeners([
99+ /// on(&b, "click", |root, vdom, event| {
100+ /// // ...
101+ /// }),
102+ /// on(&b, "dblclick", |root, vdom, event| {
103+ /// // ...
104+ /// }),
105+ /// ])
106+ /// .finish();
107+ /// ```
108+ #[inline]
109+ pub fn listeners<L>(self, listeners: L) -> ElementBuilder<'a, L, Attributes, Children>
110+ where
111+ L: 'a + AsRef<[Listener<'a>]>,
112+ {
113+ ElementBuilder {
114+ bump: self.bump,
115+ key: self.key,
116+ tag_name: self.tag_name,
117+ listeners,
118+ attributes: self.attributes,
119+ children: self.children,
120+ namespace: self.namespace,
121+ }
122+ }
123+
124+ /// Set the attributes for this element.
125+ ///
126+ /// You can use this method to customize the backing storage for attributes,
127+ /// for example to use a fixed-size array instead of the default
128+ /// dynamically-sized `bumpalo::collections::Vec`.
129+ ///
130+ /// Any attributes already added to the builder will be overridden.
131+ ///
132+ /// # Example
133+ ///
134+ /// ```no_run
135+ /// use dodrio::{builder::*, bumpalo::Bump, Attribute};
136+ ///
137+ /// let b = Bump::new();
138+ ///
139+ /// // Create a `<div>` with a fixed-size array of two attributes.
140+ /// let my_div = div(&b)
141+ /// .attributes([
142+ /// attr("id", "my-div"),
143+ /// attr("class", "notification"),
144+ /// ])
145+ /// .finish();
146+ /// ```
147+ #[inline]
148+ pub fn attributes<A>(self, attributes: A) -> ElementBuilder<'a, Listeners, A, Children>
149+ where
150+ A: 'a + AsRef<[Attribute<'a>]>,
151+ {
152+ ElementBuilder {
153+ bump: self.bump,
154+ key: self.key,
155+ tag_name: self.tag_name,
156+ listeners: self.listeners,
157+ attributes,
158+ children: self.children,
159+ namespace: self.namespace,
160+ }
161+ }
162+
163+ /// Set the children for this element.
164+ ///
165+ /// You can use this method to customize the backing storage for children,
166+ /// for example to use a fixed-size array instead of the default
167+ /// dynamically-sized `bumpalo::collections::Vec`.
168+ ///
169+ /// Any children already added to the builder will be overridden.
170+ ///
171+ /// # Example
172+ ///
173+ /// ```no_run
174+ /// use dodrio::{builder::*, bumpalo::Bump};
175+ ///
176+ /// let b = Bump::new();
177+ ///
178+ /// // Create a `<div>` with a fixed-size array of two `<span>` children.
179+ /// let my_div = div(&b)
180+ /// .children([
181+ /// span(&b).finish(),
182+ /// span(&b).finish(),
183+ /// ])
184+ /// .finish();
185+ /// ```
186+ #[inline]
187+ pub fn children<C>(self, children: C) -> ElementBuilder<'a, Listeners, Attributes, C>
188+ where
189+ C: 'a + AsRef<[Node<'a>]>,
190+ {
191+ ElementBuilder {
192+ bump: self.bump,
193+ key: self.key,
194+ tag_name: self.tag_name,
195+ listeners: self.listeners,
196+ attributes: self.attributes,
197+ children,
198+ namespace: self.namespace,
199+ }
200+ }
201+
202+ /// Set the namespace for this element.
203+ ///
204+ /// # Example
205+ ///
206+ /// ```no_run
207+ /// use dodrio::{builder::*, bumpalo::Bump};
208+ ///
209+ /// let b = Bump::new();
210+ ///
211+ /// // Create a `<td>` tag with an xhtml namespace
212+ /// let my_td = td(&b)
213+ /// .namespace(Some("http://www.w3.org/1999/xhtml"))
214+ /// .finish();
215+ /// ```
216+ #[inline]
217+ pub fn namespace(self, namespace: Option<&'a str>) -> Self {
218+ ElementBuilder {
219+ bump: self.bump,
220+ key: self.key,
221+ tag_name: self.tag_name,
222+ listeners: self.listeners,
223+ attributes: self.attributes,
224+ children: self.children,
225+ namespace,
226+ }
227+ }
228+
229+ /// Set this element's key.
230+ ///
231+ /// When diffing sets of siblings, if an old sibling and new sibling share a
232+ /// key, then they will always reuse the same physical DOM node. This is
233+ /// important when using CSS animations, web components, third party JS, or
234+ /// anything else that makes the diffing implementation observable.
235+ ///
236+ /// Do not use keys if such a scenario does not apply. Keyed diffing is
237+ /// generally more expensive than not, since it is putting greater
238+ /// constraints on the diffing algorithm.
239+ ///
240+ /// # Invariants You Must Uphold
241+ ///
242+ /// The key may not be `u32::MAX`, which is a reserved key value.
243+ ///
244+ /// Keys must be unique among siblings.
245+ ///
246+ /// All sibling nodes must be keyed, or they must all not be keyed. You may
247+ /// not mix keyed and unkeyed siblings.
248+ ///
249+ /// # Example
250+ ///
251+ /// ```no_run
252+ /// use dodrio::{builder::*, bumpalo::Bump};
253+ ///
254+ /// let b = Bump::new();
255+ ///
256+ /// let my_li = li(&b)
257+ /// .key(1337)
258+ /// .finish();
259+ /// ```
260+ #[inline]
261+ pub fn key(mut self, key: u32) -> Self {
262+ use std::u32;
263+ debug_assert!(key != u32::MAX);
264+ self.key = NodeKey(key);
265+ self
266+ }
267+
268+ /// Create the virtual DOM node described by this builder.
269+ ///
270+ /// # Example
271+ ///
272+ /// ```no_run
273+ /// use dodrio::{builder::*, bumpalo::Bump, Node};
274+ ///
275+ /// let b = Bump::new();
276+ ///
277+ /// // Start with a builder...
278+ /// let builder: ElementBuilder<_, _, _> = div(&b);
279+ ///
280+ /// // ...and finish it to create a virtual DOM node!
281+ /// let my_div: Node = builder.finish();
282+ /// ```
283+ #[inline]
284+ pub fn finish(self) -> Node<'a> {
285+ let children: &'a Children = self.bump.alloc(self.children);
286+ let children: &'a [Node<'a>] = children.as_ref();
287+
288+ let listeners: &'a Listeners = self.bump.alloc(self.listeners);
289+ let listeners: &'a [Listener<'a>] = listeners.as_ref();
290+
291+ let attributes: &'a Attributes = self.bump.alloc(self.attributes);
292+ let attributes: &'a [Attribute<'a>] = attributes.as_ref();
293+
294+ Node::element(
295+ self.bump,
296+ self.key,
297+ self.tag_name,
298+ listeners,
299+ attributes,
300+ children,
301+ self.namespace,
302+ )
303+ }
304+}
305+
306+impl<'a, Attributes, Children>
307+ ElementBuilder<'a, bumpalo::collections::Vec<'a, Listener<'a>>, Attributes, Children>
308+where
309+ Attributes: 'a + AsRef<[Attribute<'a>]>,
310+ Children: 'a + AsRef<[Node<'a>]>,
311+{
312+ /// Add a new event listener to this element.
313+ ///
314+ /// The `event` string specifies which event will be listened for. The
315+ /// `callback` function is the function that will be invoked if the
316+ /// specified event occurs.
317+ ///
318+ /// # Example
319+ ///
320+ /// ```no_run
321+ /// use dodrio::{builder::*, bumpalo::Bump};
322+ ///
323+ /// let b = Bump::new();
324+ ///
325+ /// // A button that does something when clicked!
326+ /// let my_button = button(&b)
327+ /// .on("click", |root, vdom, event| {
328+ /// // ...
329+ /// })
330+ /// .finish();
331+ /// ```
332+ #[inline]
333+ pub fn on<F>(mut self, event: &'a str, callback: F) -> Self
334+ where
335+ F: 'static + Fn(&mut dyn RootRender, VdomWeak, web_sys::Event),
336+ {
337+ self.listeners.push(Listener {
338+ event,
339+ callback: self.bump.alloc(callback),
340+ });
341+ self
342+ }
343+}
344+
345+impl<'a, Listeners, Children>
346+ ElementBuilder<'a, Listeners, bumpalo::collections::Vec<'a, Attribute<'a>>, Children>
347+where
348+ Listeners: 'a + AsRef<[Listener<'a>]>,
349+ Children: 'a + AsRef<[Node<'a>]>,
350+{
351+ /// Add a new attribute to this element.
352+ ///
353+ /// # Example
354+ ///
355+ /// ```no_run
356+ /// use dodrio::{builder::*, bumpalo::Bump};
357+ ///
358+ /// let b = Bump::new();
359+ ///
360+ /// // Create the `<div id="my-div"/>` element.
361+ /// let my_div = div(&b).attr("id", "my-div").finish();
362+ /// ```
363+ #[inline]
364+ pub fn attr(mut self, name: &'a str, value: &'a str) -> Self {
365+ self.attributes.push(Attribute { name, value });
366+ self
367+ }
368+
369+ /// Conditionally add a "boolean-style" attribute to this element.
370+ ///
371+ /// If the `should_add` parameter is true, then adds an attribute with the
372+ /// given `name` and an empty string value. If the `should_add` parameter is
373+ /// false, then the attribute is not added.
374+ ///
375+ /// This method is useful for attributes whose semantics are defined by
376+ /// whether or not the attribute is present or not, and whose value is
377+ /// ignored. Example attributes like this include:
378+ ///
379+ /// * `checked`
380+ /// * `hidden`
381+ /// * `selected`
382+ ///
383+ /// # Example
384+ ///
385+ /// ```no_run
386+ /// use dodrio::{builder::*, bumpalo::Bump};
387+ /// use js_sys::Math;
388+ ///
389+ /// let b = Bump::new();
390+ ///
391+ /// // Create the `<div>` that is randomly hidden 50% of the time.
392+ /// let my_div = div(&b)
393+ /// .bool_attr("hidden", Math::random() >= 0.5)
394+ /// .finish();
395+ /// ```
396+ pub fn bool_attr(mut self, name: &'a str, should_add: bool) -> Self {
397+ if should_add {
398+ self.attributes.push(Attribute { name, value: "" });
399+ }
400+ self
401+ }
402+}
403+
404+impl<'a, Listeners, Attributes>
405+ ElementBuilder<'a, Listeners, Attributes, bumpalo::collections::Vec<'a, Node<'a>>>
406+where
407+ Listeners: 'a + AsRef<[Listener<'a>]>,
408+ Attributes: 'a + AsRef<[Attribute<'a>]>,
409+{
410+ /// Add a new child to this element.
411+ ///
412+ /// # Example
413+ ///
414+ /// ```no_run
415+ /// use dodrio::{builder::*, bumpalo::Bump};
416+ /// use js_sys::Math;
417+ ///
418+ /// let b = Bump::new();
419+ ///
420+ /// // Create `<p><span></span></p>`.
421+ /// let my_div = p(&b)
422+ /// .child(span(&b).finish())
423+ /// .finish();
424+ /// ```
425+ #[inline]
426+ pub fn child(mut self, child: Node<'a>) -> Self {
427+ self.children.push(child);
428+ self
429+ }
430+}
431+
432+macro_rules! builder_constructors {
433+ ( $(
434+ $(#[$attr:meta])*
435+ $name:ident;
436+ )* ) => {
437+ $(
438+ $(#[$attr])*
439+ #[inline]
440+ pub fn $name<'a, B>(
441+ bump: B,
442+ ) -> ElementBuilder<
443+ 'a,
444+ bumpalo::collections::Vec<'a, Listener<'a>>,
445+ bumpalo::collections::Vec<'a, Attribute<'a>>,
446+ bumpalo::collections::Vec<'a, Node<'a>>,
447+ >
448+ where
449+ B: Into<&'a Bump>
450+ {
451+ ElementBuilder::new(bump, stringify!($name))
452+ }
453+ )*
454+ };
455+ ( $(
456+ $(#[$attr:meta])*
457+ $name:ident <> $namespace:tt;
458+ )* ) => {
459+ $(
460+ $(#[$attr])*
461+ #[inline]
462+ pub fn $name<'a>(
463+ bump: &'a Bump,
464+ ) -> ElementBuilder<
465+ 'a,
466+ bumpalo::collections::Vec<'a, Listener<'a>>,
467+ bumpalo::collections::Vec<'a, Attribute<'a>>,
468+ bumpalo::collections::Vec<'a, Node<'a>>,
469+ > {
470+ let builder = ElementBuilder::new(bump, stringify!($name));
471+ builder.namespace(Some($namespace))
472+ }
473+ )*
474+ }
475+}
476+
477+// Organized in the same order as
478+// https://developer.mozilla.org/en-US/docs/Web/HTML/Element
479+//
480+// Does not include obsolete elements.
481+builder_constructors! {
482+ // Document metadata
483+
484+ /// Build a
485+ /// [`<base>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base)
486+ /// element.
487+ base;
488+ /// Build a
489+ /// [`<head>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/head)
490+ /// element.
491+ head;
492+ /// Build a
493+ /// [`<link>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link)
494+ /// element.
495+ link;
496+ /// Build a
497+ /// [`<meta>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta)
498+ /// element.
499+ meta;
500+ /// Build a
501+ /// [`<style>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/style)
502+ /// element.
503+ style;
504+ /// Build a
505+ /// [`<title>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/title)
506+ /// element.
507+ title;
508+
509+ // Sectioning root
510+
511+ /// Build a
512+ /// [`<body>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/body)
513+ /// element.
514+ body;
515+
516+ // Content sectioning
517+
518+ /// Build a
519+ /// [`<address>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/address)
520+ /// element.
521+ address;
522+ /// Build a
523+ /// [`<article>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/article)
524+ /// element.
525+ article;
526+ /// Build a
527+ /// [`<aside>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/aside)
528+ /// element.
529+ aside;
530+ /// Build a
531+ /// [`<footer>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/footer)
532+ /// element.
533+ footer;
534+ /// Build a
535+ /// [`<header>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/header)
536+ /// element.
537+ header;
538+ /// Build a
539+ /// [`<h1>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/h1)
540+ /// element.
541+ h1;
542+ /// Build a
543+ /// [`<h2>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/h2)
544+ /// element.
545+ h2;
546+ /// Build a
547+ /// [`<h3>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/h3)
548+ /// element.
549+ h3;
550+ /// Build a
551+ /// [`<h4>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/h4)
552+ /// element.
553+ h4;
554+ /// Build a
555+ /// [`<h5>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/h5)
556+ /// element.
557+ h5;
558+ /// Build a
559+ /// [`<h6>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/h6)
560+ /// element.
561+ h6;
562+ /// Build a
563+ /// [`<hgroup>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/hgroup)
564+ /// element.
565+ hgroup;
566+ /// Build a
567+ /// [`<main>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/main)
568+ /// element.
569+ main;
570+ /// Build a
571+ /// [`<nav>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/nav)
572+ /// element.
573+ nav;
574+ /// Build a
575+ /// [`<section>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/section)
576+ /// element.
577+ section;
578+
579+ // Text content
580+
581+ /// Build a
582+ /// [`<blockquote>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/blockquote)
583+ /// element.
584+ blockquote;
585+ /// Build a
586+ /// [`<dd>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dd)
587+ /// element.
588+ dd;
589+ /// Build a
590+ /// [`<div>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/div)
591+ /// element.
592+ div;
593+ /// Build a
594+ /// [`<dl>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dl)
595+ /// element.
596+ dl;
597+ /// Build a
598+ /// [`<dt>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dt)
599+ /// element.
600+ dt;
601+ /// Build a
602+ /// [`<figcaption>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/figcaption)
603+ /// element.
604+ figcaption;
605+ /// Build a
606+ /// [`<figure>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/figure)
607+ /// element.
608+ figure;
609+ /// Build a
610+ /// [`<hr>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/hr)
611+ /// element.
612+ hr;
613+ /// Build a
614+ /// [`<li>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/li)
615+ /// element.
616+ li;
617+ /// Build a
618+ /// [`<ol>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/ol)
619+ /// element.
620+ ol;
621+ /// Build a
622+ /// [`<p>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/p)
623+ /// element.
624+ p;
625+ /// Build a
626+ /// [`<pre>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/pre)
627+ /// element.
628+ pre;
629+ /// Build a
630+ /// [`<ul>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/ul)
631+ /// element.
632+ ul;
633+
634+ // Inline text semantics
635+
636+ /// Build a
637+ /// [`<a>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a)
638+ /// element.
639+ a;
640+ /// Build a
641+ /// [`<abbr>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/abbr)
642+ /// element.
643+ abbr;
644+ /// Build a
645+ /// [`<b>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/b)
646+ /// element.
647+ b;
648+ /// Build a
649+ /// [`<bdi>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/bdi)
650+ /// element.
651+ bdi;
652+ /// Build a
653+ /// [`<bdo>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/bdo)
654+ /// element.
655+ bdo;
656+ /// Build a
657+ /// [`<br>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/br)
658+ /// element.
659+ br;
660+ /// Build a
661+ /// [`<cite>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/cite)
662+ /// element.
663+ cite;
664+ /// Build a
665+ /// [`<code>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/code)
666+ /// element.
667+ code;
668+ /// Build a
669+ /// [`<data>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/data)
670+ /// element.
671+ data;
672+ /// Build a
673+ /// [`<dfn>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dfn)
674+ /// element.
675+ dfn;
676+ /// Build a
677+ /// [`<em>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/em)
678+ /// element.
679+ em;
680+ /// Build a
681+ /// [`<i>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/i)
682+ /// element.
683+ i;
684+ /// Build a
685+ /// [`<kbd>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/kbd)
686+ /// element.
687+ kbd;
688+ /// Build a
689+ /// [`<mark>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/mark)
690+ /// element.
691+ mark;
692+ /// Build a
693+ /// [`<q>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/q)
694+ /// element.
695+ q;
696+ /// Build a
697+ /// [`<rb>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/rb)
698+ /// element.
699+ rb;
700+ /// Build a
701+ /// [`<rp>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/rp)
702+ /// element.
703+ rp;
704+ /// Build a
705+ /// [`<rt>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/rt)
706+ /// element.
707+ rt;
708+ /// Build a
709+ /// [`<rtc>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/rtc)
710+ /// element.
711+ rtc;
712+ /// Build a
713+ /// [`<ruby>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/ruby)
714+ /// element.
715+ ruby;
716+ /// Build a
717+ /// [`<s>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/s)
718+ /// element.
719+ s;
720+ /// Build a
721+ /// [`<samp>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/samp)
722+ /// element.
723+ samp;
724+ /// Build a
725+ /// [`<small>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/small)
726+ /// element.
727+ small;
728+ /// Build a
729+ /// [`<span>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/span)
730+ /// element.
731+ span;
732+ /// Build a
733+ /// [`<strong>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/strong)
734+ /// element.
735+ strong;
736+ /// Build a
737+ /// [`<sub>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/sub)
738+ /// element.
739+ sub;
740+ /// Build a
741+ /// [`<sup>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/sup)
742+ /// element.
743+ sup;
744+ /// Build a
745+ /// [`<time>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/time)
746+ /// element.
747+ time;
748+ /// Build a
749+ /// [`<u>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/u)
750+ /// element.
751+ u;
752+ /// Build a
753+ /// [`<var>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/var)
754+ /// element.
755+ var;
756+ /// Build a
757+ /// [`<wbr>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/wbr)
758+ /// element.
759+ wbr;
760+
761+ // Image and multimedia
762+
763+ /// Build a
764+ /// [`<area>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/area)
765+ /// element.
766+ area;
767+ /// Build a
768+ /// [`<audio>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/audio)
769+ /// element.
770+ audio;
771+ /// Build a
772+ /// [`<img>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img)
773+ /// element.
774+ img;
775+ /// Build a
776+ /// [`<map>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/map)
777+ /// element.
778+ map;
779+ /// Build a
780+ /// [`<track>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/track)
781+ /// element.
782+ track;
783+ /// Build a
784+ /// [`<video>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/video)
785+ /// element.
786+ video;
787+
788+ // Embedded content
789+
790+ /// Build a
791+ /// [`<embed>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/embed)
792+ /// element.
793+ embed;
794+ /// Build a
795+ /// [`<iframe>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe)
796+ /// element.
797+ iframe;
798+ /// Build a
799+ /// [`<object>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/object)
800+ /// element.
801+ object;
802+ /// Build a
803+ /// [`<param>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/param)
804+ /// element.
805+ param;
806+ /// Build a
807+ /// [`<picture>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/picture)
808+ /// element.
809+ picture;
810+ /// Build a
811+ /// [`<source>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/source)
812+ /// element.
813+ source;
814+
815+ // Scripting
816+
817+ /// Build a
818+ /// [`<canvas>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/canvas)
819+ /// element.
820+ canvas;
821+ /// Build a
822+ /// [`<noscript>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/noscript)
823+ /// element.
824+ noscript;
825+ /// Build a
826+ /// [`<script>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script)
827+ /// element.
828+ script;
829+
830+ // Demarcating edits
831+
832+ /// Build a
833+ /// [`<del>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/del)
834+ /// element.
835+ del;
836+ /// Build a
837+ /// [`<ins>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/ins)
838+ /// element.
839+ ins;
840+
841+ // Table content
842+
843+ /// Build a
844+ /// [`<caption>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/caption)
845+ /// element.
846+ caption;
847+ /// Build a
848+ /// [`<col>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/col)
849+ /// element.
850+ col;
851+ /// Build a
852+ /// [`<colgroup>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/colgroup)
853+ /// element.
854+ colgroup;
855+ /// Build a
856+ /// [`<table>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/table)
857+ /// element.
858+ table;
859+ /// Build a
860+ /// [`<tbody>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/tbody)
861+ /// element.
862+ tbody;
863+ /// Build a
864+ /// [`<td>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/td)
865+ /// element.
866+ td;
867+ /// Build a
868+ /// [`<tfoot>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/tfoot)
869+ /// element.
870+ tfoot;
871+ /// Build a
872+ /// [`<th>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/th)
873+ /// element.
874+ th;
875+ /// Build a
876+ /// [`<thead>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/thead)
877+ /// element.
878+ thead;
879+ /// Build a
880+ /// [`<tr>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/tr)
881+ /// element.
882+ tr;
883+
884+ // Forms
885+
886+ /// Build a
887+ /// [`<button>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button)
888+ /// element.
889+ button;
890+ /// Build a
891+ /// [`<datalist>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/datalist)
892+ /// element.
893+ datalist;
894+ /// Build a
895+ /// [`<fieldset>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/fieldset)
896+ /// element.
897+ fieldset;
898+ /// Build a
899+ /// [`<form>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form)
900+ /// element.
901+ form;
902+ /// Build a
903+ /// [`<input>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input)
904+ /// element.
905+ input;
906+ /// Build a
907+ /// [`<label>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/label)
908+ /// element.
909+ label;
910+ /// Build a
911+ /// [`<legend>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/legend)
912+ /// element.
913+ legend;
914+ /// Build a
915+ /// [`<meter>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meter)
916+ /// element.
917+ meter;
918+ /// Build a
919+ /// [`<optgroup>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/optgroup)
920+ /// element.
921+ optgroup;
922+ /// Build a
923+ /// [`<option>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/option)
924+ /// element.
925+ option;
926+ /// Build a
927+ /// [`<output>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/output)
928+ /// element.
929+ output;
930+ /// Build a
931+ /// [`<progress>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/progress)
932+ /// element.
933+ progress;
934+ /// Build a
935+ /// [`<select>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/select)
936+ /// element.
937+ select;
938+ /// Build a
939+ /// [`<textarea>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea)
940+ /// element.
941+ textarea;
942+
943+ // Interactive elements
944+
945+ /// Build a
946+ /// [`<details>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/details)
947+ /// element.
948+ details;
949+ /// Build a
950+ /// [`<dialog>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dialog)
951+ /// element.
952+ dialog;
953+ /// Build a
954+ /// [`<menu>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/menu)
955+ /// element.
956+ menu;
957+ /// Build a
958+ /// [`<menuitem>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/menuitem)
959+ /// element.
960+ menuitem;
961+ /// Build a
962+ /// [`<summary>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/summary)
963+ /// element.
964+ summary;
965+
966+ // Web components
967+
968+ /// Build a
969+ /// [`<slot>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/slot)
970+ /// element.
971+ slot;
972+ /// Build a
973+ /// [`<template>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template)
974+ /// element.
975+ template;
976+}
977+
978+builder_constructors! {
979+ // SVG components
980+
981+ /// Build a
982+ /// [`<svg>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/svg)
983+ /// element.
984+ svg <> "http://www.w3.org/2000/svg" ;
985+ /// Build a
986+ /// [`<path>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/path)
987+ /// element.
988+ path <> "http://www.w3.org/2000/svg";
989+ /// Build a
990+ /// [`<circle>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/circle)
991+ /// element.
992+ circle <> "http://www.w3.org/2000/svg";
993+ /// Build a
994+ /// [`<ellipse>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/ellipse)
995+ /// element.
996+ ellipse <> "http://www.w3.org/2000/svg";
997+ /// Build a
998+ /// [`<line>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/line)
999+ /// element.
1000+ line <> "http://www.w3.org/2000/svg";
1001+ /// Build a
1002+ /// [`<polygon>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/polygon)
1003+ /// element.
1004+ polygon <> "http://www.w3.org/2000/svg";
1005+ /// Build a
1006+ /// [`<polyline>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/polyline)
1007+ /// element.
1008+ polyline <> "http://www.w3.org/2000/svg";
1009+ /// Build a
1010+ /// [`<rect>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/rect)
1011+ /// element.
1012+ rect <> "http://www.w3.org/2000/svg";
1013+ /// Build a
1014+ /// [`<image>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/image)
1015+ /// element.
1016+ image <> "http://www.w3.org/2000/svg";
1017+}
1018+
1019+/// Construct a text node.
1020+///
1021+/// This is `dodrio`'s virtual DOM equivalent of `document.createTextNode`.
1022+///
1023+/// # Example
1024+///
1025+/// ```no_run
1026+/// use dodrio::builder::*;
1027+///
1028+/// let my_text = text("hello, dodrio!");
1029+/// ```
1030+#[inline]
1031+pub fn text<'a>(contents: &'a str) -> Node<'a> {
1032+ Node::text(contents)
1033+}
1034+
1035+/// Construct an attribute for an element.
1036+///
1037+/// # Example
1038+///
1039+/// This example creates the `id="my-id"` for some element like `<div
1040+/// id="my-id"/>`.
1041+///
1042+/// ```no_run
1043+/// use dodrio::builder::*;
1044+///
1045+/// let my_id_attr = attr("id", "my-id");
1046+/// ```
1047+pub fn attr<'a>(name: &'a str, value: &'a str) -> Attribute<'a> {
1048+ Attribute { name, value }
1049+}
1050+
1051+/// Create an event listener.
1052+///
1053+/// `event` is the type of event to listen for, e.g. `"click"`. The `callback`
1054+/// is the function that will be invoked when the event occurs.
1055+///
1056+/// # Example
1057+///
1058+/// ```no_run
1059+/// use dodrio::{builder::*, bumpalo::Bump};
1060+///
1061+/// let b = Bump::new();
1062+///
1063+/// let listener = on(&b, "click", |root, vdom, event| {
1064+/// // do something when a click happens...
1065+/// });
1066+/// ```
1067+pub fn on<'a, F>(bump: &'a Bump, event: &'a str, callback: F) -> Listener<'a>
1068+where
1069+ F: Fn(&mut dyn RootRender, VdomWeak, web_sys::Event) + 'static,
1070+{
1071+ Listener {
1072+ event,
1073+ callback: bump.alloc(callback),
1074+ }
1075+}
src/cached.rs+141−75
@@ -1,45 +1,52 @@
1−use crate::{Node, Render};
1+use crate::{
2−use bumpalo::Bump;
2+ cached_set::CachedSet,
3+ node::{CachedNode, NodeKey},
4+ Node, Render, RenderContext,
5+};
6+use std::any::TypeId;
3−use std::cell::RefCell;
7+use std::cell::Cell;
4−use std::mem;
5use std::ops::{Deref, DerefMut};
8use std::ops::{Deref, DerefMut};
6
9
7−// TODO: This implementation will only cache the rendering (generation of the
8−// virtual DOM) but not the diffing of the cached subtree. We could skip diffing
9−// for cached tree by adding `fn is_cached(&self) -> bool` to `Node` that we can
10−// check during diffing. This comes at the cost of bloating `Node`'s size (since
11−// we don't have any padding to sneak an extra `bool` field into). We should
12−// investigate whether it is worth adding this or not.
13−
14/// A renderable that supports caching for when rendering is expensive but can
10/// A renderable that supports caching for when rendering is expensive but can
15/// generate the same DOM tree.
11/// generate the same DOM tree.
12+///
13+/// # Example
14+///
15+/// The [TodoMVC
16+/// example](https://github.com/fitzgen/dodrio/blob/master/examples/todomvc/src/todo.rs)
17+/// caches individual todo items.
18+#[derive(Clone, Debug, Default)]
16−pub struct Cached<R> {
19+pub struct Cached<R>
20+where
21+ R: Default,
22+{
17 inner: R,
23 inner: R,
18− bump: bumpalo::Bump,
19− // Actually a self-reference into `self.bump`. Safe because we ensure that
20− // whenever we hand out a cached node, we use a lifetime that cannot outlive
21− // its owning `Cached<R>`.
22− cached: RefCell<Option<Node<'static>>>,
24+ cached: Cell<Option<CachedNode>>,
25+}
26+
27+pub_unstable_internal! {
28+ #[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
29+ pub(crate) struct TemplateId(TypeId);
23}
30}
24
31
25−impl<R> Cached<R> {
32+impl<R> Cached<R>
33+where
34+ R: Default,
35+{
26 /// Construct a new `Cached<R>` of an inner `R`.
36 /// Construct a new `Cached<R>` of an inner `R`.
27 ///
37 ///
28 /// # Example
38 /// # Example
29 ///
39 ///
30 /// ```
40 /// ```
31− /// use bumpalo::Bump;
32− /// use dodrio::{Cached, Node, Render};
41+ /// use dodrio::{Cached, Node, Render, RenderContext};
33 ///
42 ///
43+ /// #[derive(Default)]
34 /// pub struct Counter {
44 /// pub struct Counter {
35 /// count: u32,
45 /// count: u32,
36 /// }
46 /// }
37 ///
47 ///
38− /// impl Render for Counter {
48+ /// impl<'a> Render<'a> for Counter {
39− /// fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
49+ /// fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
40− /// where
41− /// 'a: 'bump
42− /// {
43 /// // ...
50 /// // ...
44 /// # unimplemented!()
51 /// # unimplemented!()
45 /// }
52 /// }
@@ -53,13 +60,8 @@ impl<R> Cached<R> {
53 /// ```
60 /// ```
54 #[inline]
61 #[inline]
55 pub fn new(inner: R) -> Cached<R> {
62 pub fn new(inner: R) -> Cached<R> {
56− let bump = Bump::new();
57− let cached = RefCell::new(None);
63+ let cached = Cell::new(None);
58− Cached {
64+ Cached { inner, cached }
59− inner,
60− bump,
61− cached,
62− }
63 }
65 }
64
66
65 /// Invalidate the cached rendering.
67 /// Invalidate the cached rendering.
@@ -71,28 +73,25 @@ impl<R> Cached<R> {
71 /// # Example
73 /// # Example
72 ///
74 ///
73 /// The `Cached<Hello>` component must have its cache invalidated whenever
75 /// The `Cached<Hello>` component must have its cache invalidated whenever
74− /// the `who` string is changed, or else the cached rendering whill keep
76+ /// the `who` string is changed, or else the cached rendering will keep
75 /// displaying greetings to old `who`s.
77 /// displaying greetings to old `who`s.
76 ///
78 ///
77 /// ```
79 /// ```
78− /// use bumpalo::Bump;
79− /// use dodrio::{Cached, Node, Render};
80+ /// use dodrio::{bumpalo, Cached, Node, Render, RenderContext};
80 ///
81 ///
81 /// /// A component that renders to "<p>Hello, {who}!</p>"
82 /// /// A component that renders to "<p>Hello, {who}!</p>"
83+ /// #[derive(Default)]
82 /// pub struct Hello {
84 /// pub struct Hello {
83 /// who: String
85 /// who: String
84 /// }
86 /// }
85 ///
87 ///
86− /// impl Render for Hello {
88+ /// impl<'a> Render<'a> for Hello {
87− /// fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
89+ /// fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
88− /// where
90+ /// use dodrio::builder::*;
89− /// 'a: 'bump,
91+ /// let greeting = bumpalo::format!(in cx.bump, "Hello, {}!", self.who);
90− /// {
92+ /// p(&cx)
91− /// Node::element(bump, "p", [], [], [
93+ /// .children([text(greeting.into_bump_str())])
92− /// Node::text("Hello, "),
93− /// Node::text(&self.who),
94− /// Node::text("!"),
95− /// ])
94+ /// .finish()
96 /// }
95 /// }
97 /// }
96 /// }
98 ///
97 ///
@@ -104,18 +103,30 @@ impl<R> Cached<R> {
104 /// }
103 /// }
105 /// ```
104 /// ```
106 #[inline]
105 #[inline]
107− pub fn invalidate(cached: &mut Self) {
106+ pub fn invalidate(cached: &Self) {
108− *cached.cached.borrow_mut() = None;
107+ cached.cached.set(None);
109 }
108 }
110
109
111 /// Convert a `Cached<R>` back into a plain `R`.
110 /// Convert a `Cached<R>` back into a plain `R`.
112 #[inline]
111 #[inline]
113− pub fn into_inner(cached: Cached<R>) -> R {
112+ pub fn into_inner(cached: Self) -> R {
114 cached.inner
113 cached.inner
115 }
114 }
116}
115}
117
116
117+impl<R> Cached<R>
118+where
119+ R: 'static + Default,
120+{
121+ pub(crate) fn template_id() -> TemplateId {
122+ TemplateId(TypeId::of::<R>())
123+ }
124+}
125+
118−impl<R> Deref for Cached<R> {
126+impl<R> Deref for Cached<R>
127+where
128+ R: Default,
129+{
119 type Target = R;
130 type Target = R;
120
131
121 fn deref(&self) -> &R {
132 fn deref(&self) -> &R {
@@ -123,41 +134,96 @@ impl<R> Deref for Cached<R> {
123 }
134 }
124}
135}
125
136
126−impl<R> DerefMut for Cached<R> {
137+impl<R> DerefMut for Cached<R>
138+where
139+ R: Default,
140+{
127 fn deref_mut(&mut self) -> &mut R {
141 fn deref_mut(&mut self) -> &mut R {
128 &mut self.inner
142 &mut self.inner
129 }
143 }
130}
144}
131
145
132−unsafe fn extend_node_lifetime<'a>(node: Node<'a>) -> Node<'static> {
146+impl<'a, R> Render<'a> for Cached<R>
133− mem::transmute(node)
147+where
148+ R: 'static + Default + for<'b> Render<'b>,
149+{
150+ fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
151+ let template = cx.template::<R>();
152+ let cached = match self.cached.get() {
153+ // This does-the-cache-contain-this-id check is necessary because
154+ // the same `Cached<R>` instance can be rendered into vdom A, which
155+ // will save the results into A's cached set and yield id X. Then,
156+ // it can be rendered *again* into a second vdom B, and B's cached
157+ // set does not have the saved results for id X. If we didn't have
158+ // this check, instead of a cache miss, we would have a panic.
159+ //
160+ // This scenario should basically never happen in the real world,
161+ // however. If we ever find that it is worth sharing a cached render
162+ // component between multiple vdoms, and want to avoid these
163+ // "unnecessary" cache misses, we can do this:
164+ //
165+ // * Make each `Cached<R>` have an instance id and generation
166+ // counter.
167+ //
168+ // * On invalidation, bump the generation counter.
169+ //
170+ // * Add a generation member to the `CacheEntry`.
171+ //
172+ // * Each vdom maintains a map from `Cached<R>` instance id to
173+ // `CacheEntry`s.
174+ //
175+ // * We only re-use the cached results if the `Cached<R>`'s
176+ // generation counter matches the entry in the vdom's cached set.
177+ //
178+ // This is all do-able but is a bit more book keeping than we really
179+ // want to do unless it is well motivated.
180+ Some(cached)
181+ if {
182+ let cached_set = cx.cached_set.borrow();
183+ cached_set.contains(cached.id)
184+ } =>
185+ {
186+ cached
187+ }
188+ _ => {
189+ let mut key = NodeKey::NONE;
190+ let id = CachedSet::insert(cx, false, template, |nested_cx| {
191+ let node = self.inner.render(nested_cx);
192+ key = node.key();
193+ node
194+ });
195+ let cached = CachedNode { id, key };
196+ self.cached.set(Some(cached));
197+ cached
198+ }
199+ };
200+
201+ cached.into()
202+ }
134}
203}
135
204
205+#[cfg(feature = "serde")]
136−impl<R: Render> Render for Cached<R> {
206+impl<R> serde::Serialize for Cached<R>
137− fn render<'a, 'bump>(&'a self, _: &'bump Bump) -> Node<'bump>
207+where
208+ R: Default + serde::Serialize,
209+{
210+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
138 where
211 where
139− 'a: 'bump,
212+ S: serde::Serializer,
140 {
213 {
141− let mut cached = self.cached.borrow_mut();
214+ self.inner.serialize(serializer)
142−
215+ }
143− if let Some(cached) = cached.as_ref() {
144− // The cached node is actually a self-reference, so it has the `'a`
145− // lifetime.
146− let cached: Node<'a> = cached.clone();
147−
148− // But the `'a` lifetime outlives `'bump`, so we can safely convert
149− // it to the narrower `'bump` lifetime.
150− let cached: Node<'bump> = cached;
151−
152− // Return the cached rendering!
153− return cached;
154− }
216+}
155
217
156− // We don't have a cached node. Render into our own `Bump`, cache it for
218+#[cfg(feature = "serde")]
157− // future renders, and return it. Same lifetimes as above.
219+impl<'de, R> serde::Deserialize<'de> for Cached<R>
158− let node: Node<'a> = self.inner.render(&self.bump);
220+where
159− *cached = Some(unsafe { extend_node_lifetime(node.clone()) });
221+ R: Default + serde::Deserialize<'de>,
160− let node: Node<'bump> = node;
222+{
161− node
223+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
224+ where
225+ D: serde::Deserializer<'de>,
226+ {
227+ R::deserialize(deserializer).map(Cached::new)
162 }
228 }
163}
229}
src/cached_set.rsnew file+186
@@ -0,0 +1,186 @@
1+use crate::{
2+ events::EventsRegistry,
3+ node::{Node, NodeKind},
4+ render_context::RenderContext,
5+};
6+use bumpalo::Bump;
7+use fxhash::{FxHashMap, FxHashSet};
8+use std::sync::atomic::{AtomicUsize, Ordering};
9+use std::u32;
10+use wasm_bindgen::prelude::*;
11+
12+static ID_COUNTER: AtomicUsize = AtomicUsize::new(0);
13+
14+pub_unstable_internal! {
15+ #[derive(Debug, Default)]
16+ pub(crate) struct CachedSet {
17+ items: FxHashMap<CacheId, CacheEntry>,
18+ }
19+}
20+
21+pub_unstable_internal! {
22+ #[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
23+ pub(crate) struct CacheId(u32);
24+}
25+
26+#[derive(Debug)]
27+pub(crate) struct CacheEntry {
28+ bump: Bump,
29+
30+ // Actually a reference into `bump`.
31+ node: *const Node<'static>,
32+
33+ // All the other cached entries that `node` references (including transitive
34+ // edges). By eagerly following transitive edges and recording them the
35+ // once, we don't have to repeat the tracing every time we want to garbage
36+ // collect cache entries.
37+ edges: FxHashSet<CacheId>,
38+
39+ // The template for this cached virtual subtree.
40+ //
41+ // When a cache entry has a template, that means that the `ChangeList` will
42+ // lazily build a physical DOM subtree based on the template, and when we
43+ // create new versions of this cached result, we clone the template's
44+ // physical DOM subtree and then modify it, rather than building the cached
45+ // result up from scratch. This is a nice win because we bounce between JS
46+ // and DOM methods less often, and get the C++ DOM implementation to do most
47+ // of the subtree construction for us.
48+ template: Option<CacheId>,
49+
50+ // Whether this entry should never be garbage collected. Typically only
51+ // templates are pinned.
52+ pinned: bool,
53+}
54+
55+impl From<CacheId> for u32 {
56+ #[inline]
57+ fn from(id: CacheId) -> u32 {
58+ id.0
59+ }
60+}
61+
62+impl CachedSet {
63+ pub(crate) fn new_roots_set(&self) -> FxHashSet<CacheId> {
64+ let mut roots = FxHashSet::default();
65+ roots.reserve(self.items.len());
66+ roots
67+ }
68+
69+ pub(crate) fn gc(&mut self, registry: &mut EventsRegistry, roots: FxHashSet<CacheId>) {
70+ let mut marked = FxHashSet::default();
71+ marked.reserve(self.items.len());
72+
73+ for root in roots {
74+ if marked.insert(root) {
75+ let entry = self
76+ .items
77+ .get(&root)
78+ .expect_throw("CachedSet::gc: should have root in cached set");
79+
80+ // NB: no need to recurse here because `edges` already contains
81+ // transitive edges!
82+ marked.extend(entry.edges.iter().cloned());
83+ }
84+ }
85+
86+ self.items.retain(|id, entry| {
87+ let keep = entry.pinned || marked.contains(id);
88+ if !keep {
89+ let node: &Node = unsafe { &*entry.node };
90+ registry.remove_subtree(node);
91+ }
92+ keep
93+ });
94+ }
95+
96+ // Trace all the transitive edges to other cached entries that the given
97+ // node has.
98+ fn trace(&self, node: &Node) -> FxHashSet<CacheId> {
99+ let mut edges = FxHashSet::default();
100+ self.trace_recursive(&mut edges, node);
101+ edges
102+ }
103+
104+ fn trace_recursive(&self, edges: &mut FxHashSet<CacheId>, node: &Node) {
105+ match &node.kind {
106+ NodeKind::Text(_) => return,
107+ NodeKind::Cached(c) => {
108+ debug_assert!(self.items.contains_key(&c.id));
109+ edges.insert(c.id);
110+ edges.extend(
111+ self.items
112+ .get(&c.id)
113+ .expect_throw("CachedSet::trace: should have c.id in cached set")
114+ .edges
115+ .iter()
116+ .cloned(),
117+ );
118+ }
119+ NodeKind::Element(el) => {
120+ for child in el.children {
121+ self.trace_recursive(edges, child);
122+ }
123+ }
124+ }
125+ }
126+
127+ fn next_id(&mut self) -> CacheId {
128+ let next = ID_COUNTER.fetch_add(1, Ordering::AcqRel) as u32;
129+ let next = if next == u32::MAX { None } else { Some(next) };
130+ CacheId(next.expect_throw("ID_COUNTER overflowed"))
131+ }
132+
133+ pub(crate) fn insert<F>(
134+ cx: &mut RenderContext,
135+ pinned: bool,
136+ template: Option<CacheId>,
137+ f: F,
138+ ) -> CacheId
139+ where
140+ F: for<'a> FnOnce(&mut RenderContext<'a>) -> Node<'a>,
141+ {
142+ let set = cx.cached_set;
143+ let bump = Bump::new();
144+ let (node, edges) = {
145+ let mut nested_cx = RenderContext::new(&bump, cx.cached_set, cx.templates);
146+ let node = f(&mut nested_cx);
147+ let node = bump.alloc(node);
148+ let edges = {
149+ let set = set.borrow();
150+ set.trace(node)
151+ };
152+ (
153+ node as *mut Node<'_> as usize as *const Node<'static>,
154+ edges,
155+ )
156+ };
157+
158+ let entry = CacheEntry {
159+ bump,
160+ node,
161+ edges,
162+ template,
163+ pinned,
164+ };
165+
166+ let mut set = set.borrow_mut();
167+ let id = set.next_id();
168+ set.items.insert(id, entry);
169+ id
170+ }
171+
172+ /// Does the cached set contain a cached node with the given id?
173+ pub fn contains(&self, id: CacheId) -> bool {
174+ self.items.contains_key(&id)
175+ }
176+
177+ /// Get the cached node and its template (if any) for the given cache id.
178+ pub fn get(&self, id: CacheId) -> (&Node, Option<CacheId>) {
179+ let entry = self
180+ .items
181+ .get(&id)
182+ .expect_throw("CachedSet::get: should have id in set");
183+ let node: &Node = unsafe { &*entry.node };
184+ (node, entry.template)
185+ }
186+}
src/change_list.rsdeleted−379
@@ -1,379 +0,0 @@
1−use crate::Listener;
2−use bumpalo::Bump;
3−use log::*;
4−use std::fmt;
5−use std::sync::Once;
6−use wasm_bindgen::prelude::*;
7−
8−pub mod js {
9− use wasm_bindgen::prelude::*;
10−
11− #[wasm_bindgen]
12− extern "C" {
13− #[derive(Clone, Debug)]
14− pub type ChangeList;
15−
16− #[wasm_bindgen(constructor)]
17− pub fn new(container: &web_sys::Node) -> ChangeList;
18−
19− #[wasm_bindgen(structural, method)]
20− pub fn unmount(this: &ChangeList);
21−
22− #[wasm_bindgen(structural, method, js_name = addChangeListRange)]
23− pub fn add_change_list_range(this: &ChangeList, start: usize, len: usize);
24−
25− #[wasm_bindgen(structural, method, js_name = applyChanges)]
26− pub fn apply_changes(this: &ChangeList, memory: JsValue);
27−
28− #[wasm_bindgen(structural, method, js_name = initEventsTrampoline)]
29− pub fn init_events_trampoline(
30− this: &ChangeList,
31− trampoline: &Closure<Fn(web_sys::Event, u32, u32)>,
32− );
33− }
34−}
35−
36−pub(crate) struct ChangeList {
37− bump: Bump,
38− js: js::ChangeList,
39− events_trampoline: Option<Closure<Fn(web_sys::Event, u32, u32)>>,
40−}
41−
42−impl Drop for ChangeList {
43− fn drop(&mut self) {
44− debug!("Dropping ChangeList");
45− self.js.unmount();
46− }
47−}
48−
49−impl fmt::Debug for ChangeList {
50− fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
51− f.debug_struct("ChangeList")
52− .field("bump", &self.bump)
53− .field("js", &self.js)
54− .field("events_trampoline", &"..")
55− .finish()
56− }
57−}
58−
59−impl ChangeList {
60− pub(crate) fn new(container: &web_sys::Node) -> ChangeList {
61− // XXX: Because wasm-bindgen-test doesn't support third party JS
62− // dependencies, we can't use `wasm_bindgen(module = "...")` for our
63− // `ChangeList` JS import. Instead, this *should* be a local JS snippet,
64− // but that isn't implemented yet:
65− // https://github.com/rustwasm/rfcs/pull/6
66− static EVAL: Once = Once::new();
67− EVAL.call_once(|| {
68− js_sys::eval(include_str!("../js/change-list.js"))
69− .expect("should eval change-list.js OK");
70− });
71−
72− let bump = Bump::new();
73− let js = js::ChangeList::new(container);
74− ChangeList {
75− bump,
76− js,
77− events_trampoline: None,
78− }
79− }
80−
81− pub(crate) fn apply_changes(&mut self) {
82− let js = &self.js;
83− unsafe {
84− self.bump.each_allocated_chunk(|ch| {
85− js.add_change_list_range(ch.as_ptr() as usize, ch.len());
86− });
87− }
88− js.apply_changes(wasm_bindgen::memory());
89− self.bump.reset();
90− }
91−
92− pub(crate) fn init_events_trampoline(
93− &mut self,
94− trampoline: Closure<Fn(web_sys::Event, u32, u32)>,
95− ) {
96− debug_assert!(self.events_trampoline.is_none());
97− self.js.init_events_trampoline(&trampoline);
98− self.events_trampoline = Some(trampoline);
99− }
100−}
101−
102−#[repr(u32)]
103−pub enum ChangeDiscriminant {
104− /// Immediates: `(pointer, length)`
105− ///
106− /// Stack: `[... TextNode] -> [... TextNode]`
107− ///
108− /// ```text
109− /// stack.top().textContent = String(pointer, length)
110− /// ```
111− SetText = 0,
112−
113− /// Immediates: `()`
114− ///
115− /// Stack: `[... Node] -> [...]`
116− ///
117− /// ```text
118− /// node = stack.pop()
119− /// while (node.nextSibling) {
120− /// node.nextSibling.remove();
121− /// }
122− /// node.remove()
123− /// ```
124− RemoveSelfAndNextSiblings = 1,
125−
126− /// Immediates: `()`
127− ///
128− /// Stack: `[... Node Node] -> [... Node]`
129− ///
130− /// ```text
131− /// new = stack.pop()
132− /// old = stack.pop()
133− /// old.replaceWith(new)
134− /// stack.push(new)
135− /// ```
136− ReplaceWith = 2,
137−
138− /// Immediates: `(pointer1, length1, pointer2, length2)`
139− ///
140− /// Stack: `[... Node] -> [... Node]`
141− ///
142− /// ```text
143− /// stack.top().setAttribute(String(pointer1, length1), String(pointer2, length2))
144− /// ```
145− SetAttribute = 3,
146−
147− /// Immediates: (pointer, length)
148− ///
149− /// Stack: `[... Node] -> [... Node]`
150− ///
151− /// ```text
152− /// stack.top().removeAttribute(String(pointer, length))
153− /// ```
154− RemoveAttribute = 4,
155−
156− /// Immediates: `()`
157− ///
158− /// Stack: `[... Node] -> [... Node node]`
159− ///
160− /// ```text
161− /// stack.push(stack.top().firstChild)
162− /// ```
163− PushFirstChild = 5,
164−
165− /// Immediates: `()`
166− ///
167− /// Stack: `[... Node] -> [... Node]`
168− ///
169− /// ```text
170− /// stack.push(stack.pop().nextSibling)
171− /// ```
172− PopPushNextSibling = 6,
173−
174− /// Immediates: `()`
175− ///
176− /// Stack: `[... T] -> [...]`
177− ///
178− /// ```text
179− /// stack.pop()
180− /// ```
181− Pop = 7,
182−
183− /// Immediates: `()`
184− ///
185− /// Stack: `[... Node Node] -> [... Node]`
186− ///
187− /// ```text
188− /// child = stack.pop()
189− /// stack.top().appendChild(child)
190− /// ```
191− AppendChild = 8,
192−
193− /// Immediates: `(pointer, length)`
194− ///
195− /// Stack: `[...] -> [... Node]`
196− ///
197− /// ```text
198− /// stack.push(createTextNode(String(pointer, length)))
199− /// ```
200− CreateTextNode = 9,
201−
202− /// Immediates: `(pointer, length)`
203− ///
204− /// Stack: `[...] -> [... Node]`
205− ///
206− /// ```text
207− /// stack.push(createElement(String(pointer, length))
208− /// ```
209− CreateElement = 10,
210−
211− /// Immediate: `(pointer, length, A, B)`
212− ///
213− /// Stack: `[... Node] -> [... Node]`
214− ///
215− /// ```text
216− /// event = String(pointer, length)
217− /// callback = ProxyToRustCallback(A, B);
218− /// stack.top().addEventListener(event, callback);
219− /// ```
220− NewEventListener = 11,
221−
222− /// Immediate: `(pointer, length, A, B)`
223− ///
224− /// Stack: `[... Node] -> [... Node]`
225− ///
226− /// ```text
227− /// event = String(pointer, length)
228− /// new_callback = ProxyToRustCallback(A, B);
229− /// stack.top().updateEventlistener(new_callback)
230− /// ```
231− UpdateEventListener = 12,
232−
233− /// Immediates: `(pointer, length)`
234− ///
235− /// Stack: `[... Node] -> [... Node]`
236− ///
237− /// ```text
238− /// stack.top().removeEventListener(String(pointer, length));
239− /// ```
240− RemoveEventListener = 13,
241−}
242−
243−// Allocation utilities to ensure that we only allocation sequences of `u32`s
244−// into the change list's bump arena without any padding. This helps maintain
245−// the invariants required for `Bump::each_allocated_chunk`'s safety.
246−impl ChangeList {
247− // Allocate an opcode with zero immediates.
248− fn op0(&self, discriminant: ChangeDiscriminant) {
249− self.bump.alloc(discriminant as u32);
250− }
251−
252− // Note: no 1-immediate opcodes at this time.
253−
254− // Allocate an opcode with two immediates.
255− fn op2(&self, discriminant: ChangeDiscriminant, a: u32, b: u32) {
256− self.bump.alloc([discriminant as u32, a, b]);
257− }
258−
259− // Note: no 3-immediate opcodes at this time.
260−
261− // Allocate an opcode with four immediates.
262− fn op4(&self, discriminant: ChangeDiscriminant, a: u32, b: u32, c: u32, d: u32) {
263− self.bump.alloc([discriminant as u32, a, b, c, d]);
264− }
265−}
266−
267−impl ChangeList {
268− pub(crate) fn emit_set_text(&self, text: &str) {
269− debug!("emit_set_text({:?})", text);
270− self.op2(
271− ChangeDiscriminant::SetText,
272− text.as_ptr() as u32,
273− text.len() as u32,
274− );
275− }
276−
277− pub(crate) fn emit_remove_self_and_next_siblings(&self) {
278− debug!("emit_remove_self_and_next_siblings()");
279− self.op0(ChangeDiscriminant::RemoveSelfAndNextSiblings);
280− }
281−
282− pub(crate) fn emit_replace_with(&self) {
283− debug!("emit_replace_with()");
284− self.op0(ChangeDiscriminant::ReplaceWith);
285− }
286−
287− pub(crate) fn emit_set_attribute(&self, name: &str, value: &str) {
288− debug!("emit_set_attribute({:?}, {:?})", name, value);
289− self.op4(
290− ChangeDiscriminant::SetAttribute,
291− name.as_ptr() as u32,
292− name.len() as u32,
293− value.as_ptr() as u32,
294− value.len() as u32,
295− );
296− }
297−
298− pub(crate) fn emit_remove_attribute(&self, name: &str) {
299− debug!("emit_remove_attribute({:?})", name);
300− self.op2(
301− ChangeDiscriminant::RemoveAttribute,
302− name.as_ptr() as u32,
303− name.len() as u32,
304− );
305− }
306−
307− pub(crate) fn emit_push_first_child(&self) {
308− debug!("emit_push_first_child()");
309− self.op0(ChangeDiscriminant::PushFirstChild);
310− }
311−
312− pub(crate) fn emit_pop_push_next_sibling(&self) {
313− debug!("emit_pop_push_next_sibling()");
314− self.op0(ChangeDiscriminant::PopPushNextSibling);
315− }
316−
317− pub(crate) fn emit_pop(&self) {
318− debug!("emit_pop()");
319− self.op0(ChangeDiscriminant::Pop);
320− }
321−
322− pub(crate) fn emit_append_child(&self) {
323− debug!("emit_append_child()");
324− self.op0(ChangeDiscriminant::AppendChild);
325− }
326−
327− pub(crate) fn emit_create_text_node(&self, text: &str) {
328− debug!("emit_create_text_node({:?})", text);
329− self.op2(
330− ChangeDiscriminant::CreateTextNode,
331− text.as_ptr() as u32,
332− text.len() as u32,
333− );
334− }
335−
336− pub(crate) fn emit_create_element(&self, tag_name: &str) {
337− debug!("emit_create_element({:?})", tag_name);
338− self.op2(
339− ChangeDiscriminant::CreateElement,
340− tag_name.as_ptr() as u32,
341− tag_name.len() as u32,
342− );
343− }
344−
345− pub(crate) fn emit_new_event_listener(&self, listener: &Listener) {
346− debug!("emit_new_event_listener({:?})", listener);
347− let (a, b) = listener.get_callback_parts();
348− debug_assert!(a != 0);
349− self.op4(
350− ChangeDiscriminant::NewEventListener,
351− listener.event.as_ptr() as u32,
352− listener.event.len() as u32,
353− a,
354− b,
355− );
356− }
357−
358− pub(crate) fn emit_update_event_listener(&self, listener: &Listener) {
359− debug!("emit_update_event_listener({:?})", listener);
360− let (a, b) = listener.get_callback_parts();
361− debug_assert!(a != 0);
362− self.op4(
363− ChangeDiscriminant::UpdateEventListener,
364− listener.event.as_ptr() as u32,
365− listener.event.len() as u32,
366− a,
367− b,
368− );
369− }
370−
371− pub(crate) fn emit_remove_event_listener(&self, event: &str) {
372− debug!("emit_remove_event_listener({:?})", event);
373− self.op2(
374− ChangeDiscriminant::RemoveEventListener,
375− event.as_ptr() as u32,
376− event.len() as u32,
377− );
378− }
379−}
src/change_list/emitter.rsnew file+292
@@ -0,0 +1,292 @@
1+//! The `InstructionEmitter` is responsible for encoding change list
2+//! instructions and ensuring that each instruction has the correct number of
3+//! immediates. It ensures that the resulting change list instruction stream is
4+//! *syntactically* correct (opcodes have the correct arity of immediate
5+//! arguments, etc), while `ChangeListBuilder` wraps an emitter and additionally
6+//! ensures that the resulting change list program is *semantically* correct
7+//! (doesn't reference cached strings before they've been added to the cache,
8+//! etc).
9+//!
10+//! We encode the instructions directly into a dedicated bump arena. We
11+//! eventually pass the bump arena's chunks to the interpreter in JS, so it is
12+//! critical that nothing other than change list instructions are allocated
13+//! inside this bump, and that the instructions themselves do not contain any
14+//! padding or uninitialized memory. See the documentation for the the
15+//! `Bump::each_allocated_chunk` method for details.
16+
17+use bumpalo::Bump;
18+
19+#[derive(Debug)]
20+pub(crate) struct InstructionEmitter {
21+ bump: Bump,
22+}
23+
24+impl InstructionEmitter {
25+ /// Construct a new `InstructionEmitter` with its own bump arena.
26+ pub fn new() -> InstructionEmitter {
27+ let bump = Bump::new();
28+ InstructionEmitter { bump }
29+ }
30+
31+ /// Invoke the given function with each of the allocated instruction
32+ /// sequences that this emitter has built up.
33+ #[cfg_attr(feature = "xxx-unstable-internal-use-only", allow(dead_code))]
34+ pub fn each_instruction_sequence<F>(&mut self, f: F)
35+ where
36+ F: FnMut(&[u8]),
37+ {
38+ // Note: the safety invariants required for `each_allocated_chunk` are
39+ // maintained by the fact that everything we encode as allocations in
40+ // the bump arena are in the form of `u32`s, and therefore we don't have
41+ // any uninitialized memory padding in the arena.
42+ unsafe {
43+ self.bump.each_allocated_chunk(f);
44+ }
45+ }
46+
47+ /// Reset to an empty sequence of instructions.
48+ pub fn reset(&mut self) {
49+ self.bump.reset();
50+ }
51+}
52+
53+macro_rules! define_change_list_instructions {
54+ ( $(
55+ $( #[$attr:meta] )*
56+ $name:ident (
57+ $($immediate:ident),*
58+ ) = $discriminant:expr,
59+ )* ) => {
60+ impl InstructionEmitter {
61+ $(
62+ $( #[$attr] )*
63+ #[inline]
64+ pub fn $name(&self $(, $immediate: u32)*) {
65+ self.bump.alloc_with(|| [$discriminant $(, $immediate )* ]);
66+ }
67+ )*
68+ }
69+ }
70+}
71+
72+define_change_list_instructions! {
73+ /// Stack: `[... TextNode] -> [... TextNode]`
74+ ///
75+ /// ```text
76+ /// stack.top().textContent = readString(pointer, length)
77+ /// ```
78+ set_text(pointer, length) = 0,
79+
80+ /// Stack: `[... Node] -> [...]`
81+ ///
82+ /// ```text
83+ /// node = stack.pop()
84+ /// while (node.nextSibling) {
85+ /// node.nextSibling.remove();
86+ /// }
87+ /// node.remove()
88+ /// ```
89+ remove_self_and_next_siblings() = 1,
90+
91+ /// Stack: `[... Node Node] -> [... Node]`
92+ ///
93+ /// ```text
94+ /// new = stack.pop()
95+ /// old = stack.pop()
96+ /// old.replaceWith(new)
97+ /// stack.push(new)
98+ /// ```
99+ replace_with() = 2,
100+
101+ /// Stack: `[... Node] -> [... Node]`
102+ ///
103+ /// ```text
104+ /// stack.top().setAttribute(getCachedString(attribute_key), getCachedString(value_key))
105+ /// ```
106+ set_attribute(attribute_key, value_key) = 3,
107+
108+ /// Stack: `[... Node] -> [... Node]`
109+ ///
110+ /// ```text
111+ /// stack.top().removeAttribute(getCachedString(attribute_key))
112+ /// ```
113+ remove_attribute(attribute_key) = 4,
114+
115+ /// Stack: `[... Node] -> [... Node Node]`
116+ ///
117+ /// ```text
118+ /// parent = stack.top()
119+ /// child = parent.childNodes[parent.childNodes.length - n - 1]
120+ /// stack.push(child)
121+ /// ```
122+ push_reverse_child(n) = 5,
123+
124+ /// Stack: `[... Node Node] -> [... Node Node]`
125+ ///
126+ /// ```text
127+ /// stack.pop();
128+ /// parent = stack.top();
129+ /// child = parent.childNodes[n]
130+ /// stack.push(child)
131+ /// ```
132+ pop_push_child(n) = 6,
133+
134+ /// Stack: `[... T] -> [...]`
135+ ///
136+ /// ```text
137+ /// stack.pop()
138+ /// ```
139+ pop() = 7,
140+
141+ /// Stack: `[... Node Node] -> [... Node]`
142+ ///
143+ /// ```text
144+ /// child = stack.pop()
145+ /// stack.top().appendChild(child)
146+ /// ```
147+ append_child() = 8,
148+
149+ /// Stack: `[...] -> [... Node]`
150+ ///
151+ /// ```text
152+ /// stack.push(document.createTextNode(readString(pointer, length)))
153+ /// ```
154+ create_text_node(pointer, length) = 9,
155+
156+ /// Stack: `[...] -> [... Node]`
157+ ///
158+ /// ```text
159+ /// stack.push(document.createElement(getCachedString(tag_name_key))
160+ /// ```
161+ create_element(tag_name_key) = 10,
162+
163+ /// Stack: `[... Node] -> [... Node]`
164+ ///
165+ /// ```text
166+ /// event = getCachedString(event_key)
167+ /// callback = createProxyToRustCallback(a, b)
168+ /// stack.top().addEventListener(event, callback)
169+ /// ```
170+ new_event_listener(event_key, a, b) = 11,
171+
172+ /// Stack: `[... Node] -> [... Node]`
173+ ///
174+ /// ```text
175+ /// event = getCachedString(event_key)
176+ /// new_callback = createProxyToRustCallback(a, b);
177+ /// stack.top().updateEventlistener(new_callback)
178+ /// ```
179+ update_event_listener(event_key, a, b) = 12,
180+
181+ /// Stack: `[... Node] -> [... Node]`
182+ ///
183+ /// ```text
184+ /// stack.top().removeEventListener(getCachedString(event_key));
185+ /// ```
186+ remove_event_listener(event_key) = 13,
187+
188+ /// Stack: `[...] -> [...]`
189+ ///
190+ /// ```text
191+ /// addCachedString(readString(pointer, length), key);
192+ /// ```
193+ add_cached_string(pointer, length, key) = 14,
194+
195+ /// Stack: `[...] -> [...]`
196+ ///
197+ /// ```text
198+ /// dropCachedString(key);
199+ /// ```
200+ drop_cached_string(key) = 15,
201+
202+ /// Stack: `[...] -> [... Node]`
203+ ///
204+ /// ```text
205+ /// tag_name = getCachedString(tag_name_key)
206+ /// namespace = getCachedString(tag_name_key)
207+ /// stack.push(document.createElementNS(tag_name, namespace))
208+ /// ```
209+ create_element_ns(tag_name_key, namespace_key) = 16,
210+
211+ /// Stack: `[...] -> [...]`
212+ ///
213+ /// ```text
214+ /// parent = stack.top()
215+ /// children = parent.childNodes
216+ /// temp = temp_base
217+ /// for i in start .. end:
218+ /// temporaries[temp] = children[i]
219+ /// temp += 1
220+ /// ```
221+ save_children_to_temporaries(temp_base, start, end) = 17,
222+
223+ /// Stack: `[... Node] -> [... Node Node]`
224+ ///
225+ /// ```text
226+ /// parent = stack.top()
227+ /// child = parent.childNodes[n]
228+ /// stack.push(child)
229+ /// ```
230+ push_child(n) = 18,
231+
232+ /// Stack: `[...] -> [... Node]`
233+ ///
234+ /// ```text
235+ /// stack.push(temporaries[temp])
236+ /// ```
237+ push_temporary(temp) = 19,
238+
239+ /// Stack: `[... Node Node] -> [... Node]`
240+ ///
241+ /// ```text
242+ /// before = stack.pop()
243+ /// after = stack.pop()
244+ /// after.insertBefore(before)
245+ /// stack.push(before)
246+ /// ```
247+ insert_before() = 20,
248+
249+ /// Stack: `[... Node Node] -> [... Node Node]`
250+ ///
251+ /// ```text
252+ /// stack.pop()
253+ /// parent = stack.top()
254+ /// child = parent.childNodes[parent.childNodes.length - n - 1]
255+ /// stack.push(child)
256+ /// ```
257+ pop_push_reverse_child(n) = 21,
258+
259+ /// Stack: `[... Node] -> [... Node]`
260+ ///
261+ /// ```text
262+ /// parent = stack.top()
263+ /// child = parent.childNodes[n]
264+ /// child.remove()
265+ /// ```
266+ remove_child(n) = 22,
267+
268+ /// Stack: `[... Node] -> [... Node]`
269+ ///
270+ /// ```text
271+ /// class = getCachedString(class)
272+ /// node = stack.top()
273+ /// node.className = class
274+ /// ```
275+ set_class(class) = 23,
276+
277+ /// Stack: `[... Node] -> [... Node]`
278+ ///
279+ /// ```text
280+ /// template = stack.top()
281+ /// saveTemplate(id, template)
282+ /// ```
283+ save_template(id) = 24,
284+
285+ /// Stack: `[...] -> [... Node]`
286+ ///
287+ /// ```text
288+ /// template = getTemplate(id)
289+ /// stack.push(template.cloneNode(true))
290+ /// ```
291+ push_template(id) = 25,
292+}
src/change_list/js.rsnew file+40
@@ -0,0 +1,40 @@
1+cfg_if::cfg_if! {
2+ if #[cfg(all(feature = "xxx-unstable-internal-use-only", not(target_arch = "wasm32")))] {
3+ #[derive(Clone, Debug)]
4+ pub struct ChangeListInterpreter {}
5+ impl ChangeListInterpreter {
6+ pub fn new(_container: &crate::Element) -> ChangeListInterpreter {
7+ ChangeListInterpreter {}
8+ }
9+ pub fn unmount(&self) {}
10+ pub fn add_change_list_range(&self, _start: usize, _len: usize) {}
11+ pub fn init_events_trampoline(&self, _trampoline: &crate::EventsTrampoline) {}
12+ }
13+ } else {
14+ use wasm_bindgen::prelude::*;
15+
16+ #[wasm_bindgen(module = "/js/change-list-interpreter.js")]
17+ extern "C" {
18+ #[derive(Clone, Debug)]
19+ pub type ChangeListInterpreter;
20+
21+ #[wasm_bindgen(constructor)]
22+ pub fn new(container: &web_sys::Element) -> ChangeListInterpreter;
23+
24+ #[wasm_bindgen(structural, method)]
25+ pub fn unmount(this: &ChangeListInterpreter);
26+
27+ #[wasm_bindgen(structural, method, js_name = addChangeListRange)]
28+ pub fn add_change_list_range(this: &ChangeListInterpreter, start: usize, len: usize);
29+
30+ #[wasm_bindgen(structural, method, js_name = applyChanges)]
31+ pub fn apply_changes(this: &ChangeListInterpreter, memory: JsValue);
32+
33+ #[wasm_bindgen(structural, method, js_name = initEventsTrampoline)]
34+ pub fn init_events_trampoline(
35+ this: &ChangeListInterpreter,
36+ trampoline: &crate::EventsTrampoline,
37+ );
38+ }
39+ }
40+}
src/change_list/mod.rsnew file+353
@@ -0,0 +1,353 @@
1+pub(crate) mod emitter;
2+pub(crate) mod strings;
3+pub(crate) mod traversal;
4+
5+// Note: has to be `pub` because of `wasm-bindgen` visibility restrictions.
6+pub mod js;
7+
8+use self::emitter::InstructionEmitter;
9+use self::strings::{StringKey, StringsCache};
10+use self::traversal::{MoveTo, Traversal};
11+use crate::{cached_set::CacheId, Listener};
12+use fxhash::FxHashSet;
13+
14+#[derive(Debug)]
15+pub(crate) struct ChangeListPersistentState {
16+ strings: StringsCache,
17+ emitter: InstructionEmitter,
18+ traversal: Traversal,
19+ interpreter: js::ChangeListInterpreter,
20+ templates: FxHashSet<CacheId>,
21+}
22+
23+pub(crate) struct ChangeListBuilder<'a> {
24+ state: &'a mut ChangeListPersistentState,
25+ next_temporary: u32,
26+ forcing_new_listeners: bool,
27+}
28+
29+impl Drop for ChangeListPersistentState {
30+ fn drop(&mut self) {
31+ self.interpreter.unmount();
32+ }
33+}
34+
35+impl ChangeListPersistentState {
36+ pub(crate) fn new(container: &crate::Element) -> ChangeListPersistentState {
37+ let strings = StringsCache::new();
38+ let emitter = InstructionEmitter::new();
39+ let traversal = Traversal::new();
40+ let interpreter = js::ChangeListInterpreter::new(container);
41+ let templates = Default::default();
42+ ChangeListPersistentState {
43+ strings,
44+ emitter,
45+ traversal,
46+ interpreter,
47+ templates,
48+ }
49+ }
50+
51+ pub(crate) fn init_events_trampoline(&mut self, trampoline: &crate::EventsTrampoline) {
52+ self.interpreter.init_events_trampoline(trampoline);
53+ }
54+
55+ pub(crate) fn builder<'a>(&'a mut self) -> ChangeListBuilder<'a> {
56+ ChangeListBuilder {
57+ state: self,
58+ next_temporary: 0,
59+ forcing_new_listeners: false,
60+ }
61+ }
62+}
63+
64+cfg_if::cfg_if! {
65+ if #[cfg(all(feature = "xxx-unstable-internal-use-only", not(target_arch = "wasm32")))] {
66+ impl ChangeListBuilder<'_> {
67+ pub(crate) fn finish(self) {
68+ self.state.strings.drop_unused_strings(&self.state.emitter);
69+
70+ // Nothing to actually apply the changes to.
71+
72+ self.state.emitter.reset();
73+ self.state.traversal.reset();
74+ }
75+ }
76+ } else {
77+ impl ChangeListBuilder<'_> {
78+ pub(crate) fn finish(self) {
79+ self.state.strings.drop_unused_strings(&self.state.emitter);
80+
81+ // Apply the changes.
82+ let interpreter = &self.state.interpreter;
83+ self.state.emitter.each_instruction_sequence(|seq| {
84+ interpreter.add_change_list_range(seq.as_ptr() as usize, seq.len());
85+ });
86+ interpreter.apply_changes(wasm_bindgen::memory());
87+
88+ self.state.emitter.reset();
89+ self.state.traversal.reset();
90+ }
91+ }
92+ }
93+}
94+
95+/// Traversal methods.
96+impl ChangeListBuilder<'_> {
97+ pub fn go_down(&mut self) {
98+ self.state.traversal.down();
99+ }
100+
101+ pub fn go_down_to_child(&mut self, index: usize) {
102+ self.state.traversal.down();
103+ self.state.traversal.sibling(index);
104+ }
105+
106+ pub fn go_down_to_reverse_child(&mut self, index: usize) {
107+ self.state.traversal.down();
108+ self.state.traversal.reverse_sibling(index);
109+ }
110+
111+ pub fn go_up(&mut self) {
112+ self.state.traversal.up();
113+ }
114+
115+ pub fn go_to_sibling(&mut self, index: usize) {
116+ self.state.traversal.sibling(index);
117+ }
118+
119+ pub fn go_to_temp_sibling(&mut self, temp: u32) {
120+ self.state.traversal.up();
121+ self.state.traversal.down_to_temp(temp);
122+ }
123+
124+ pub fn go_down_to_temp_child(&mut self, temp: u32) {
125+ self.state.traversal.down_to_temp(temp);
126+ }
127+
128+ pub fn commit_traversal(&mut self) {
129+ if self.state.traversal.is_committed() {
130+ return;
131+ }
132+
133+ for mv in self.state.traversal.commit() {
134+ match mv {
135+ MoveTo::Parent => {
136+ debug!("emit: pop");
137+ self.state.emitter.pop();
138+ }
139+ MoveTo::Child(n) => {
140+ debug!("emit: push_child({})", n);
141+ self.state.emitter.push_child(n);
142+ }
143+ MoveTo::ReverseChild(n) => {
144+ debug!("emit: push_reverse_child({})", n);
145+ self.state.emitter.push_reverse_child(n);
146+ }
147+ MoveTo::Sibling(n) => {
148+ debug!("emit: pop_push_child({})", n);
149+ self.state.emitter.pop_push_child(n);
150+ }
151+ MoveTo::ReverseSibling(n) => {
152+ debug!("emit: pop_push_reverse_child({})", n);
153+ self.state.emitter.pop_push_reverse_child(n);
154+ }
155+ MoveTo::TempChild(temp) => {
156+ debug!("emit: push_temporary({})", temp);
157+ self.state.emitter.push_temporary(temp);
158+ }
159+ }
160+ }
161+ }
162+
163+ pub fn traversal_is_committed(&self) -> bool {
164+ self.state.traversal.is_committed()
165+ }
166+}
167+
168+impl ChangeListBuilder<'_> {
169+ pub fn next_temporary(&self) -> u32 {
170+ self.next_temporary
171+ }
172+
173+ pub fn set_next_temporary(&mut self, next_temporary: u32) {
174+ self.next_temporary = next_temporary;
175+ }
176+
177+ pub fn save_children_to_temporaries(&mut self, start: usize, end: usize) -> u32 {
178+ debug_assert!(self.traversal_is_committed());
179+ debug_assert!(start < end);
180+ let temp_base = self.next_temporary;
181+ debug!(
182+ "emit: save_children_to_temporaries({}, {}, {})",
183+ temp_base, start, end
184+ );
185+ self.next_temporary = temp_base + (end - start) as u32;
186+ self.state
187+ .emitter
188+ .save_children_to_temporaries(temp_base, start as u32, end as u32);
189+ temp_base
190+ }
191+
192+ pub fn push_temporary(&self, temp: u32) {
193+ debug_assert!(self.traversal_is_committed());
194+ debug!("emit: push_temporary({})", temp);
195+ self.state.emitter.push_temporary(temp);
196+ }
197+
198+ pub fn remove_child(&self, child: usize) {
199+ debug_assert!(self.traversal_is_committed());
200+ debug!("emit: remove_child({})", child);
201+ self.state.emitter.remove_child(child as u32);
202+ }
203+
204+ pub fn insert_before(&self) {
205+ debug_assert!(self.traversal_is_committed());
206+ debug!("emit: insert_before()");
207+ self.state.emitter.insert_before();
208+ }
209+
210+ pub fn ensure_string(&mut self, string: &str) -> StringKey {
211+ self.state
212+ .strings
213+ .ensure_string(string, &self.state.emitter)
214+ }
215+
216+ pub fn set_text(&self, text: &str) {
217+ debug_assert!(self.traversal_is_committed());
218+ debug!("emit: set_text({:?})", text);
219+ self.state
220+ .emitter
221+ .set_text(text.as_ptr() as u32, text.len() as u32);
222+ }
223+
224+ pub fn remove_self_and_next_siblings(&self) {
225+ debug_assert!(self.traversal_is_committed());
226+ debug!("emit: remove_self_and_next_siblings()");
227+ self.state.emitter.remove_self_and_next_siblings();
228+ }
229+
230+ pub fn replace_with(&self) {
231+ debug_assert!(self.traversal_is_committed());
232+ debug!("emit: replace_with()");
233+ self.state.emitter.replace_with();
234+ }
235+
236+ pub fn set_attribute(&mut self, name: &str, value: &str, is_namespaced: bool) {
237+ debug_assert!(self.traversal_is_committed());
238+ if name == "class" && !is_namespaced {
239+ let class_id = self.ensure_string(value);
240+ debug!("emit: set_class({:?})", value);
241+ self.state.emitter.set_class(class_id.into());
242+ } else {
243+ let name_id = self.ensure_string(name);
244+ let value_id = self.ensure_string(value);
245+ debug!("emit: set_attribute({:?}, {:?})", name, value);
246+ self.state
247+ .emitter
248+ .set_attribute(name_id.into(), value_id.into());
249+ }
250+ }
251+
252+ pub fn remove_attribute(&mut self, name: &str) {
253+ debug_assert!(self.traversal_is_committed());
254+ debug!("emit: remove_attribute({:?})", name);
255+ let name_id = self.ensure_string(name);
256+ self.state.emitter.remove_attribute(name_id.into());
257+ }
258+
259+ pub fn append_child(&self) {
260+ debug_assert!(self.traversal_is_committed());
261+ debug!("emit: append_child()");
262+ self.state.emitter.append_child();
263+ }
264+
265+ pub fn create_text_node(&self, text: &str) {
266+ debug_assert!(self.traversal_is_committed());
267+ debug!("emit: create_text_node({:?})", text);
268+ self.state
269+ .emitter
270+ .create_text_node(text.as_ptr() as u32, text.len() as u32);
271+ }
272+
273+ pub fn create_element(&mut self, tag_name: &str) {
274+ debug_assert!(self.traversal_is_committed());
275+ debug!("emit: create_element({:?})", tag_name);
276+ let tag_name_id = self.ensure_string(tag_name);
277+ self.state.emitter.create_element(tag_name_id.into());
278+ }
279+
280+ pub fn create_element_ns(&mut self, tag_name: &str, ns: &str) {
281+ debug_assert!(self.traversal_is_committed());
282+ debug!("emit: create_element_ns({:?}, {:?})", tag_name, ns);
283+ let tag_name_id = self.ensure_string(tag_name);
284+ let ns_id = self.ensure_string(ns);
285+ self.state
286+ .emitter
287+ .create_element_ns(tag_name_id.into(), ns_id.into());
288+ }
289+
290+ pub fn push_force_new_listeners(&mut self) -> bool {
291+ let old = self.forcing_new_listeners;
292+ self.forcing_new_listeners = true;
293+ old
294+ }
295+
296+ pub fn pop_force_new_listeners(&mut self, previous: bool) {
297+ debug_assert!(self.forcing_new_listeners);
298+ self.forcing_new_listeners = previous;
299+ }
300+
301+ pub fn new_event_listener(&mut self, listener: &Listener) {
302+ debug_assert!(self.traversal_is_committed());
303+ debug!("emit: new_event_listener({:?})", listener);
304+ let (a, b) = listener.get_callback_parts();
305+ debug_assert!(a != 0);
306+ let event_id = self.ensure_string(listener.event);
307+ self.state.emitter.new_event_listener(event_id.into(), a, b);
308+ }
309+
310+ pub fn update_event_listener(&mut self, listener: &Listener) {
311+ debug_assert!(self.traversal_is_committed());
312+
313+ if self.forcing_new_listeners {
314+ self.new_event_listener(listener);
315+ return;
316+ }
317+
318+ debug!("emit: update_event_listener({:?})", listener);
319+ let (a, b) = listener.get_callback_parts();
320+ debug_assert!(a != 0);
321+ let event_id = self.ensure_string(listener.event);
322+ self.state
323+ .emitter
324+ .update_event_listener(event_id.into(), a, b);
325+ }
326+
327+ pub fn remove_event_listener(&mut self, event: &str) {
328+ debug_assert!(self.traversal_is_committed());
329+ debug!("emit: remove_event_listener({:?})", event);
330+ let event_id = self.ensure_string(event);
331+ self.state.emitter.remove_event_listener(event_id.into());
332+ }
333+
334+ #[inline]
335+ pub fn has_template(&mut self, id: CacheId) -> bool {
336+ self.state.templates.contains(&id)
337+ }
338+
339+ pub fn save_template(&mut self, id: CacheId) {
340+ debug_assert!(self.traversal_is_committed());
341+ debug_assert!(!self.has_template(id));
342+ debug!("emit: save_template({:?})", id);
343+ self.state.templates.insert(id);
344+ self.state.emitter.save_template(id.into());
345+ }
346+
347+ pub fn push_template(&mut self, id: CacheId) {
348+ debug_assert!(self.traversal_is_committed());
349+ debug_assert!(self.has_template(id));
350+ debug!("emit: push_template({:?})", id);
351+ self.state.emitter.push_template(id.into());
352+ }
353+}
src/change_list/strings.rsnew file+66
@@ -0,0 +1,66 @@
1+use crate::change_list::emitter::InstructionEmitter;
2+use fxhash::FxHashMap;
3+
4+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
5+pub struct StringKey(u32);
6+
7+impl From<StringKey> for u32 {
8+ #[inline]
9+ fn from(key: StringKey) -> u32 {
10+ key.0
11+ }
12+}
13+
14+#[derive(Debug)]
15+struct StringsCacheEntry {
16+ key: StringKey,
17+ used: bool,
18+}
19+
20+#[derive(Debug, Default)]
21+pub(crate) struct StringsCache {
22+ entries: FxHashMap<String, StringsCacheEntry>,
23+ next_string_key: u32,
24+}
25+
26+impl StringsCache {
27+ /// Create a new, empty strings cache.
28+ pub fn new() -> StringsCache {
29+ Default::default()
30+ }
31+
32+ /// Ensure that the given string is cached, and get its key.
33+ pub fn ensure_string(&mut self, string: &str, emitter: &InstructionEmitter) -> StringKey {
34+ if let Some(entry) = self.entries.get_mut(string) {
35+ entry.used = true;
36+ entry.key
37+ } else {
38+ let key = StringKey(self.next_string_key);
39+ self.next_string_key += 1;
40+ let entry = StringsCacheEntry { key, used: true };
41+ self.entries.insert(string.to_string(), entry);
42+ emitter.add_cached_string(string.as_ptr() as u32, string.len() as u32, key.into());
43+ key
44+ }
45+ }
46+
47+ pub fn drop_unused_strings(&mut self, emitter: &InstructionEmitter) {
48+ self.entries.retain(|string, entry| {
49+ if entry.used {
50+ // Since this entry was used during while rendering this frame,
51+ // it is likely going to be used for the next frame as well. So
52+ // we keep it in the cache so that we don't have to repopulate
53+ // and resync the cache next frame (assuming it is reused), but
54+ // we set the `used` flag to false so that if it is not used
55+ // next frame, it will be cleaned up.
56+ entry.used = false;
57+ true
58+ } else {
59+ let key = entry.key.into();
60+ debug!("emit: drop_cached_string({}) = {:?}", key, string);
61+ emitter.drop_cached_string(key);
62+ false
63+ }
64+ });
65+ }
66+}
src/change_list/traversal.rsnew file+344
@@ -0,0 +1,344 @@
1+//! Keeps track of where we are moving in a DOM tree, and shortens traversal
2+//! paths between mutations to their minimal number of operations.
3+
4+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
5+pub enum MoveTo {
6+ /// Move from the current node up to its parent.
7+ Parent,
8+
9+ /// Move to the current node's n^th child.
10+ Child(u32),
11+
12+ /// Move to the current node's n^th from last child.
13+ ReverseChild(u32),
14+
15+ /// Move to the n^th sibling. Not relative from the current
16+ /// location. Absolute indexed within all of the current siblings.
17+ Sibling(u32),
18+
19+ /// Move to the n^th from last sibling. Not relative from the current
20+ /// location. Absolute indexed within all of the current siblings.
21+ ReverseSibling(u32),
22+
23+ /// Move down to the given saved temporary child.
24+ TempChild(u32),
25+}
26+
27+#[derive(Debug)]
28+pub struct Traversal {
29+ uncommitted: Vec<MoveTo>,
30+}
31+
32+impl Traversal {
33+ /// Construct a new `Traversal` with its internal storage backed by the
34+ /// given bump arena.
35+ pub fn new() -> Traversal {
36+ Traversal {
37+ uncommitted: Vec::with_capacity(32),
38+ }
39+ }
40+
41+ /// Move the traversal up in the tree.
42+ pub fn up(&mut self) {
43+ match self.uncommitted.last() {
44+ Some(MoveTo::Sibling(_)) | Some(MoveTo::ReverseSibling(_)) => {
45+ self.uncommitted.pop();
46+ self.uncommitted.push(MoveTo::Parent);
47+ }
48+ Some(MoveTo::TempChild(_)) | Some(MoveTo::Child(_)) | Some(MoveTo::ReverseChild(_)) => {
49+ self.uncommitted.pop();
50+ // And we're back at the parent.
51+ }
52+ _ => {
53+ self.uncommitted.push(MoveTo::Parent);
54+ }
55+ }
56+ }
57+
58+ /// Move the traversal down in the tree to the first child of the current
59+ /// node.
60+ pub fn down(&mut self) {
61+ if let Some(&MoveTo::Parent) = self.uncommitted.last() {
62+ self.uncommitted.pop();
63+ self.sibling(0);
64+ } else {
65+ self.uncommitted.push(MoveTo::Child(0));
66+ }
67+ }
68+
69+ /// Move the traversal to the n^th sibling.
70+ pub fn sibling(&mut self, index: usize) {
71+ let index = index as u32;
72+ match self.uncommitted.last_mut() {
73+ Some(MoveTo::Sibling(ref mut n)) | Some(MoveTo::Child(ref mut n)) => {
74+ *n = index;
75+ }
76+ Some(MoveTo::ReverseSibling(_)) => {
77+ self.uncommitted.pop();
78+ self.uncommitted.push(MoveTo::Sibling(index));
79+ }
80+ Some(MoveTo::TempChild(_)) | Some(MoveTo::ReverseChild(_)) => {
81+ self.uncommitted.pop();
82+ self.uncommitted.push(MoveTo::Child(index))
83+ }
84+ _ => {
85+ self.uncommitted.push(MoveTo::Sibling(index));
86+ }
87+ }
88+ }
89+
90+ /// Move the the n^th from last sibling.
91+ pub fn reverse_sibling(&mut self, index: usize) {
92+ let index = index as u32;
93+ match self.uncommitted.last_mut() {
94+ Some(MoveTo::ReverseSibling(ref mut n)) | Some(MoveTo::ReverseChild(ref mut n)) => {
95+ *n = index;
96+ }
97+ Some(MoveTo::Sibling(_)) => {
98+ self.uncommitted.pop();
99+ self.uncommitted.push(MoveTo::ReverseSibling(index));
100+ }
101+ Some(MoveTo::TempChild(_)) | Some(MoveTo::Child(_)) => {
102+ self.uncommitted.pop();
103+ self.uncommitted.push(MoveTo::ReverseChild(index))
104+ }
105+ _ => {
106+ self.uncommitted.push(MoveTo::ReverseSibling(index));
107+ }
108+ }
109+ }
110+
111+ /// Go to the given saved temporary.
112+ pub fn down_to_temp(&mut self, temp: u32) {
113+ match self.uncommitted.last() {
114+ Some(MoveTo::Sibling(_)) | Some(MoveTo::ReverseSibling(_)) => {
115+ self.uncommitted.pop();
116+ }
117+ Some(MoveTo::Parent)
118+ | Some(MoveTo::TempChild(_))
119+ | Some(MoveTo::Child(_))
120+ | Some(MoveTo::ReverseChild(_))
121+ | None => {
122+ // Can't remove moves to parents since we rely on their stack
123+ // pops.
124+ }
125+ }
126+ self.uncommitted.push(MoveTo::TempChild(temp));
127+ }
128+
129+ /// Are all the traversal's moves committed? That is, are there no moves
130+ /// that have *not* been committed yet?
131+ #[inline]
132+ pub fn is_committed(&self) -> bool {
133+ self.uncommitted.is_empty()
134+ }
135+
136+ /// Commit this traversals moves and return the optimized path from the last
137+ /// commit.
138+ #[inline]
139+ pub fn commit(&mut self) -> Moves {
140+ Moves {
141+ inner: self.uncommitted.drain(..),
142+ }
143+ }
144+
145+ #[inline]
146+ pub fn reset(&mut self) {
147+ self.uncommitted.clear();
148+ }
149+}
150+
151+pub struct Moves<'a> {
152+ inner: std::vec::Drain<'a, MoveTo>,
153+}
154+
155+impl Iterator for Moves<'_> {
156+ type Item = MoveTo;
157+
158+ #[inline]
159+ fn next(&mut self) -> Option<MoveTo> {
160+ self.inner.next()
161+ }
162+}
163+
164+#[cfg(test)]
165+mod tests {
166+ use super::*;
167+
168+ #[test]
169+ fn test_traversal() {
170+ fn t<F>(f: F) -> Box<dyn FnMut(&mut Traversal)>
171+ where
172+ F: 'static + FnMut(&mut Traversal),
173+ {
174+ Box::new(f) as _
175+ }
176+
177+ for (mut traverse, expected_moves) in vec![
178+ (
179+ t(|t| {
180+ t.down();
181+ }),
182+ vec![MoveTo::Child(0)],
183+ ),
184+ (
185+ t(|t| {
186+ t.up();
187+ }),
188+ vec![MoveTo::Parent],
189+ ),
190+ (
191+ t(|t| {
192+ t.sibling(42);
193+ }),
194+ vec![MoveTo::Sibling(42)],
195+ ),
196+ (
197+ t(|t| {
198+ t.down();
199+ t.up();
200+ }),
201+ vec![],
202+ ),
203+ (
204+ t(|t| {
205+ t.down();
206+ t.sibling(2);
207+ t.up();
208+ }),
209+ vec![],
210+ ),
211+ (
212+ t(|t| {
213+ t.down();
214+ t.sibling(3);
215+ }),
216+ vec![MoveTo::Child(3)],
217+ ),
218+ (
219+ t(|t| {
220+ t.down();
221+ t.sibling(4);
222+ t.sibling(8);
223+ }),
224+ vec![MoveTo::Child(8)],
225+ ),
226+ (
227+ t(|t| {
228+ t.sibling(1);
229+ t.sibling(1);
230+ }),
231+ vec![MoveTo::Sibling(1)],
232+ ),
233+ (
234+ t(|t| {
235+ t.reverse_sibling(3);
236+ }),
237+ vec![MoveTo::ReverseSibling(3)],
238+ ),
239+ (
240+ t(|t| {
241+ t.down();
242+ t.reverse_sibling(3);
243+ }),
244+ vec![MoveTo::ReverseChild(3)],
245+ ),
246+ (
247+ t(|t| {
248+ t.down();
249+ t.reverse_sibling(3);
250+ t.up();
251+ }),
252+ vec![],
253+ ),
254+ (
255+ t(|t| {
256+ t.down();
257+ t.reverse_sibling(3);
258+ t.reverse_sibling(6);
259+ }),
260+ vec![MoveTo::ReverseChild(6)],
261+ ),
262+ (
263+ t(|t| {
264+ t.up();
265+ t.reverse_sibling(3);
266+ t.reverse_sibling(6);
267+ }),
268+ vec![MoveTo::Parent, MoveTo::ReverseSibling(6)],
269+ ),
270+ (
271+ t(|t| {
272+ t.up();
273+ t.sibling(3);
274+ t.sibling(6);
275+ }),
276+ vec![MoveTo::Parent, MoveTo::Sibling(6)],
277+ ),
278+ (
279+ t(|t| {
280+ t.sibling(3);
281+ t.sibling(6);
282+ t.up();
283+ }),
284+ vec![MoveTo::Parent],
285+ ),
286+ (
287+ t(|t| {
288+ t.reverse_sibling(3);
289+ t.reverse_sibling(6);
290+ t.up();
291+ }),
292+ vec![MoveTo::Parent],
293+ ),
294+ (
295+ t(|t| {
296+ t.down();
297+ t.down_to_temp(3);
298+ }),
299+ vec![MoveTo::Child(0), MoveTo::TempChild(3)],
300+ ),
301+ (
302+ t(|t| {
303+ t.down_to_temp(3);
304+ t.sibling(5);
305+ }),
306+ vec![MoveTo::Child(5)],
307+ ),
308+ (
309+ t(|t| {
310+ t.down_to_temp(3);
311+ t.reverse_sibling(5);
312+ }),
313+ vec![MoveTo::ReverseChild(5)],
314+ ),
315+ (
316+ t(|t| {
317+ t.down_to_temp(3);
318+ t.up();
319+ }),
320+ vec![],
321+ ),
322+ (
323+ t(|t| {
324+ t.sibling(2);
325+ t.up();
326+ t.down_to_temp(3);
327+ }),
328+ vec![MoveTo::Parent, MoveTo::TempChild(3)],
329+ ),
330+ (
331+ t(|t| {
332+ t.up();
333+ t.down_to_temp(3);
334+ }),
335+ vec![MoveTo::Parent, MoveTo::TempChild(3)],
336+ ),
337+ ] {
338+ let mut traversal = Traversal::new();
339+ traverse(&mut traversal);
340+ let actual_moves: Vec<_> = traversal.commit().collect();
341+ assert_eq!(actual_moves, expected_moves);
342+ }
343+ }
344+}
src/diff.rsnew file+1125
@@ -0,0 +1,1125 @@
1+use crate::{
2+ cached_set::{CacheId, CachedSet},
3+ change_list::ChangeListBuilder,
4+ events::EventsRegistry,
5+ node::{Attribute, ElementNode, Listener, Node, NodeKind, TextNode},
6+};
7+use fxhash::{FxHashMap, FxHashSet};
8+use std::cmp::Ordering;
9+use std::u32;
10+use wasm_bindgen::UnwrapThrowExt;
11+
12+// Diff the `old` node with the `new` node. Emits instructions to modify a
13+// physical DOM node that reflects `old` into something that reflects `new`.
14+//
15+// Upon entry to this function, the physical DOM node must be on the top of the
16+// change list stack:
17+//
18+// [... node]
19+//
20+// The change list stack is in the same state when this function exits.
21+pub(crate) fn diff(
22+ cached_set: &CachedSet,
23+ change_list: &mut ChangeListBuilder,
24+ registry: &mut EventsRegistry,
25+ old: &Node,
26+ new: &Node,
27+ cached_roots: &mut FxHashSet<CacheId>,
28+) {
29+ match (&new.kind, &old.kind) {
30+ (
31+ &NodeKind::Text(TextNode { text: new_text }),
32+ &NodeKind::Text(TextNode { text: old_text }),
33+ ) => {
34+ if new_text != old_text {
35+ change_list.commit_traversal();
36+ change_list.set_text(new_text);
37+ }
38+ }
39+
40+ (&NodeKind::Text(_), &NodeKind::Element(_)) => {
41+ change_list.commit_traversal();
42+ create(cached_set, change_list, registry, new, cached_roots);
43+ registry.remove_subtree(&old);
44+ change_list.replace_with();
45+ }
46+
47+ (&NodeKind::Element(_), &NodeKind::Text(_)) => {
48+ change_list.commit_traversal();
49+ create(cached_set, change_list, registry, new, cached_roots);
50+ // Note: text nodes cannot have event listeners, so we don't need to
51+ // remove the old node's listeners from our registry her.
52+ change_list.replace_with();
53+ }
54+
55+ (
56+ &NodeKind::Element(ElementNode {
57+ key: _,
58+ tag_name: new_tag_name,
59+ listeners: new_listeners,
60+ attributes: new_attributes,
61+ children: new_children,
62+ namespace: new_namespace,
63+ }),
64+ &NodeKind::Element(ElementNode {
65+ key: _,
66+ tag_name: old_tag_name,
67+ listeners: old_listeners,
68+ attributes: old_attributes,
69+ children: old_children,
70+ namespace: old_namespace,
71+ }),
72+ ) => {
73+ if new_tag_name != old_tag_name || new_namespace != old_namespace {
74+ change_list.commit_traversal();
75+ create(cached_set, change_list, registry, new, cached_roots);
76+ registry.remove_subtree(&old);
77+ change_list.replace_with();
78+ return;
79+ }
80+ diff_listeners(change_list, registry, old_listeners, new_listeners);
81+ diff_attributes(change_list, old_attributes, new_attributes, new_namespace.is_some());
82+ diff_children(
83+ cached_set,
84+ change_list,
85+ registry,
86+ old_children,
87+ new_children,
88+ cached_roots,
89+ );
90+ }
91+
92+ // Both the new and old nodes are cached.
93+ (&NodeKind::Cached(ref new), &NodeKind::Cached(ref old)) => {
94+ cached_roots.insert(new.id);
95+
96+ if new.id == old.id {
97+ // This is the same cached node, so nothing has changed!
98+ return;
99+ }
100+
101+ let (new, new_template) = cached_set.get(new.id);
102+ let (old, old_template) = cached_set.get(old.id);
103+ if new_template == old_template {
104+ // If they are both using the same template, then just diff the
105+ // subtrees.
106+ diff(cached_set, change_list, registry, old, new, cached_roots);
107+ } else {
108+ // Otherwise, they are probably different enough that
109+ // re-constructing the subtree from scratch should be faster.
110+ // This doubly holds true if we have a new template.
111+ change_list.commit_traversal();
112+ create_and_replace(
113+ cached_set,
114+ change_list,
115+ registry,
116+ new_template,
117+ old,
118+ new,
119+ cached_roots,
120+ );
121+ }
122+ }
123+
124+ // New cached node when the old node was not cached. In this scenario,
125+ // we assume that they are pretty different, and it isn't worth diffing
126+ // the subtrees, so we just create the new cached node afresh.
127+ (&NodeKind::Cached(ref c), _) => {
128+ change_list.commit_traversal();
129+ cached_roots.insert(c.id);
130+ let (new, new_template) = cached_set.get(c.id);
131+ create_and_replace(
132+ cached_set,
133+ change_list,
134+ registry,
135+ new_template,
136+ old,
137+ new,
138+ cached_roots,
139+ );
140+ }
141+
142+ // Old cached node and new non-cached node. Again, assume that they are
143+ // probably pretty different and create the new non-cached node afresh.
144+ (_, &NodeKind::Cached(_)) => {
145+ change_list.commit_traversal();
146+ create(cached_set, change_list, registry, new, cached_roots);
147+ registry.remove_subtree(&old);
148+ change_list.replace_with();
149+ }
150+ }
151+}
152+
153+// Diff event listeners between `old` and `new`.
154+//
155+// The listeners' node must be on top of the change list stack:
156+//
157+// [... node]
158+//
159+// The change list stack is left unchanged.
160+fn diff_listeners(
161+ change_list: &mut ChangeListBuilder,
162+ registry: &mut EventsRegistry,
163+ old: &[Listener],
164+ new: &[Listener],
165+) {
166+ if !old.is_empty() || !new.is_empty() {
167+ change_list.commit_traversal();
168+ }
169+
170+ 'outer1: for new_l in new {
171+ unsafe {
172+ // Safety relies on removing `new_l` from the registry manually at
173+ // the end of its lifetime. This happens below in the `'outer2`
174+ // loop, and elsewhere in diffing when removing old dom trees.
175+ registry.add(new_l);
176+ }
177+
178+ for old_l in old {
179+ if new_l.event == old_l.event {
180+ change_list.update_event_listener(new_l);
181+ continue 'outer1;
182+ }
183+ }
184+
185+ change_list.new_event_listener(new_l);
186+ }
187+
188+ 'outer2: for old_l in old {
189+ registry.remove(old_l);
190+
191+ for new_l in new {
192+ if new_l.event == old_l.event {
193+ continue 'outer2;
194+ }
195+ }
196+ change_list.remove_event_listener(old_l.event);
197+ }
198+}
199+
200+// Diff a node's attributes.
201+//
202+// The attributes' node must be on top of the change list stack:
203+//
204+// [... node]
205+//
206+// The change list stack is left unchanged.
207+fn diff_attributes(change_list: &mut ChangeListBuilder, old: &[Attribute], new: &[Attribute], is_namespaced: bool) {
208+ // Do O(n^2) passes to add/update and remove attributes, since
209+ // there are almost always very few attributes.
210+ 'outer: for new_attr in new {
211+ if new_attr.is_volatile() {
212+ change_list.commit_traversal();
213+ change_list.set_attribute(new_attr.name, new_attr.value, is_namespaced);
214+ } else {
215+ for old_attr in old {
216+ if old_attr.name == new_attr.name {
217+ if old_attr.value != new_attr.value {
218+ change_list.commit_traversal();
219+ change_list.set_attribute(new_attr.name, new_attr.value, is_namespaced);
220+ }
221+ continue 'outer;
222+ }
223+ }
224+
225+ change_list.commit_traversal();
226+ change_list.set_attribute(new_attr.name, new_attr.value, is_namespaced);
227+ }
228+ }
229+
230+ 'outer2: for old_attr in old {
231+ for new_attr in new {
232+ if old_attr.name == new_attr.name {
233+ continue 'outer2;
234+ }
235+ }
236+
237+ change_list.commit_traversal();
238+ change_list.remove_attribute(old_attr.name);
239+ }
240+}
241+
242+// Diff the given set of old and new children.
243+//
244+// The parent must be on top of the change list stack when this function is
245+// entered:
246+//
247+// [... parent]
248+//
249+// the change list stack is in the same state when this function returns.
250+fn diff_children(
251+ cached_set: &CachedSet,
252+ change_list: &mut ChangeListBuilder,
253+ registry: &mut EventsRegistry,
254+ old: &[Node],
255+ new: &[Node],
256+ cached_roots: &mut FxHashSet<CacheId>,
257+) {
258+ if new.is_empty() {
259+ if !old.is_empty() {
260+ change_list.commit_traversal();
261+ remove_all_children(change_list, registry, old);
262+ }
263+ return;
264+ }
265+
266+ if new.len() == 1 {
267+ match (old.first(), &new[0]) {
268+ (
269+ Some(&Node {
270+ kind: NodeKind::Text(TextNode { text: old_text }),
271+ }),
272+ &Node {
273+ kind: NodeKind::Text(TextNode { text: new_text }),
274+ },
275+ ) if old_text == new_text => {
276+ // Don't take this fast path...
277+ }
278+ (
279+ _,
280+ &Node {
281+ kind: NodeKind::Text(TextNode { text }),
282+ },
283+ ) => {
284+ change_list.commit_traversal();
285+ change_list.set_text(text);
286+ for o in old {
287+ registry.remove_subtree(o);
288+ }
289+ return;
290+ }
291+ (_, _) => {}
292+ }
293+ }
294+
295+ if old.is_empty() {
296+ if !new.is_empty() {
297+ change_list.commit_traversal();
298+ create_and_append_children(cached_set, change_list, registry, new, cached_roots);
299+ }
300+ return;
301+ }
302+
303+ let new_is_keyed = new[0].key().is_some();
304+ let old_is_keyed = old[0].key().is_some();
305+
306+ debug_assert!(
307+ new.iter().all(|n| n.key().is_some() == new_is_keyed),
308+ "all siblings must be keyed or all siblings must be non-keyed"
309+ );
310+ debug_assert!(
311+ old.iter().all(|o| o.key().is_some() == old_is_keyed),
312+ "all siblings must be keyed or all siblings must be non-keyed"
313+ );
314+
315+ if new_is_keyed && old_is_keyed {
316+ let t = change_list.next_temporary();
317+ diff_keyed_children(cached_set, change_list, registry, old, new, cached_roots);
318+ change_list.set_next_temporary(t);
319+ } else {
320+ diff_non_keyed_children(cached_set, change_list, registry, old, new, cached_roots);
321+ }
322+}
323+
324+// Diffing "keyed" children.
325+//
326+// With keyed children, we care about whether we delete, move, or create nodes
327+// versus mutate existing nodes in place. Presumably there is some sort of CSS
328+// transition animation that makes the virtual DOM diffing algorithm
329+// observable. By specifying keys for nodes, we know which virtual DOM nodes
330+// must reuse (or not reuse) the same physical DOM nodes.
331+//
332+// This is loosely based on Inferno's keyed patching implementation. However, we
333+// have to modify the algorithm since we are compiling the diff down into change
334+// list instructions that will be executed later, rather than applying the
335+// changes to the DOM directly as we compare virtual DOMs.
336+//
337+// https://github.com/infernojs/inferno/blob/36fd96/packages/inferno/src/DOM/patching.ts#L530-L739
338+//
339+// When entering this function, the parent must be on top of the change list
340+// stack:
341+//
342+// [... parent]
343+//
344+// Upon exiting, the change list stack is in the same state.
345+fn diff_keyed_children(
346+ cached_set: &CachedSet,
347+ change_list: &mut ChangeListBuilder,
348+ registry: &mut EventsRegistry,
349+ old: &[Node],
350+ new: &[Node],
351+ cached_roots: &mut FxHashSet<CacheId>,
352+) {
353+ if cfg!(debug_assertions) {
354+ let mut keys = FxHashSet::default();
355+ let mut assert_unique_keys = |children: &[Node]| {
356+ keys.clear();
357+ for child in children {
358+ let key = child.key();
359+ debug_assert!(
360+ key.is_some(),
361+ "if any sibling is keyed, all siblings must be keyed"
362+ );
363+ keys.insert(key);
364+ }
365+ debug_assert_eq!(
366+ children.len(),
367+ keys.len(),
368+ "keyed siblings must each have a unique key"
369+ );
370+ };
371+ assert_unique_keys(old);
372+ assert_unique_keys(new);
373+ }
374+
375+ // First up, we diff all the nodes with the same key at the beginning of the
376+ // children.
377+ //
378+ // `shared_prefix_count` is the count of how many nodes at the start of
379+ // `new` and `old` share the same keys.
380+ let shared_prefix_count =
381+ match diff_keyed_prefix(cached_set, change_list, registry, old, new, cached_roots) {
382+ KeyedPrefixResult::Finished => return,
383+ KeyedPrefixResult::MoreWorkToDo(count) => count,
384+ };
385+
386+ // Next, we find out how many of the nodes at the end of the children have
387+ // the same key. We do _not_ diff them yet, since we want to emit the change
388+ // list instructions such that they can be applied in a single pass over the
389+ // DOM. Instead, we just save this information for later.
390+ //
391+ // `shared_suffix_count` is the count of how many nodes at the end of `new`
392+ // and `old` share the same keys.
393+ let shared_suffix_count = old[shared_prefix_count..]
394+ .iter()
395+ .rev()
396+ .zip(new[shared_prefix_count..].iter().rev())
397+ .take_while(|&(old, new)| old.key() == new.key())
398+ .count();
399+
400+ let old_shared_suffix_start = old.len() - shared_suffix_count;
401+ let new_shared_suffix_start = new.len() - shared_suffix_count;
402+
403+ // Ok, we now hopefully have a smaller range of children in the middle
404+ // within which to re-order nodes with the same keys, remove old nodes with
405+ // now-unused keys, and create new nodes with fresh keys.
406+ diff_keyed_middle(
407+ cached_set,
408+ change_list,
409+ registry,
410+ &old[shared_prefix_count..old_shared_suffix_start],
411+ &new[shared_prefix_count..new_shared_suffix_start],
412+ cached_roots,
413+ shared_prefix_count,
414+ shared_suffix_count,
415+ old_shared_suffix_start,
416+ );
417+
418+ // Finally, diff the nodes at the end of `old` and `new` that share keys.
419+ let old_suffix = &old[old_shared_suffix_start..];
420+ let new_suffix = &new[new_shared_suffix_start..];
421+ debug_assert_eq!(old_suffix.len(), new_suffix.len());
422+ if !old_suffix.is_empty() {
423+ diff_keyed_suffix(
424+ cached_set,
425+ change_list,
426+ registry,
427+ old_suffix,
428+ new_suffix,
429+ cached_roots,
430+ new_shared_suffix_start,
431+ );
432+ }
433+}
434+
435+enum KeyedPrefixResult {
436+ // Fast path: we finished diffing all the children just by looking at the
437+ // prefix of shared keys!
438+ Finished,
439+ // There is more diffing work to do. Here is a count of how many children at
440+ // the beginning of `new` and `old` we already processed.
441+ MoreWorkToDo(usize),
442+}
443+
444+// Diff the prefix of children in `new` and `old` that share the same keys in
445+// the same order.
446+//
447+// Upon entry of this function, the change list stack must be:
448+//
449+// [... parent]
450+//
451+// Upon exit, the change list stack is the same.
452+fn diff_keyed_prefix(
453+ cached_set: &CachedSet,
454+ change_list: &mut ChangeListBuilder,
455+ registry: &mut EventsRegistry,
456+ old: &[Node],
457+ new: &[Node],
458+ cached_roots: &mut FxHashSet<CacheId>,
459+) -> KeyedPrefixResult {
460+ change_list.go_down();
461+ let mut shared_prefix_count = 0;
462+
463+ for (i, (old, new)) in old.iter().zip(new.iter()).enumerate() {
464+ if old.key() != new.key() {
465+ break;
466+ }
467+
468+ change_list.go_to_sibling(i);
469+ diff(cached_set, change_list, registry, old, new, cached_roots);
470+ shared_prefix_count += 1;
471+ }
472+
473+ // If that was all of the old children, then create and append the remaining
474+ // new children and we're finished.
475+ if shared_prefix_count == old.len() {
476+ change_list.go_up();
477+ change_list.commit_traversal();
478+ create_and_append_children(
479+ cached_set,
480+ change_list,
481+ registry,
482+ &new[shared_prefix_count..],
483+ cached_roots,
484+ );
485+ return KeyedPrefixResult::Finished;
486+ }
487+
488+ // And if that was all of the new children, then remove all of the remaining
489+ // old children and we're finished.
490+ if shared_prefix_count == new.len() {
491+ change_list.go_to_sibling(shared_prefix_count);
492+ change_list.commit_traversal();
493+ remove_self_and_next_siblings(change_list, registry, &old[shared_prefix_count..]);
494+ return KeyedPrefixResult::Finished;
495+ }
496+
497+ change_list.go_up();
498+ KeyedPrefixResult::MoreWorkToDo(shared_prefix_count)
499+}
500+
501+// The most-general, expensive code path for keyed children diffing.
502+//
503+// We find the longest subsequence within `old` of children that are relatively
504+// ordered the same way in `new` (via finding a longest-increasing-subsequence
505+// of the old child's index within `new`). The children that are elements of
506+// this subsequence will remain in place, minimizing the number of DOM moves we
507+// will have to do.
508+//
509+// Upon entry to this function, the change list stack must be:
510+//
511+// [... parent]
512+//
513+// Upon exit from this function, it will be restored to that same state.
514+fn diff_keyed_middle(
515+ cached_set: &CachedSet,
516+ change_list: &mut ChangeListBuilder,
517+ registry: &mut EventsRegistry,
518+ old: &[Node],
519+ mut new: &[Node],
520+ cached_roots: &mut FxHashSet<CacheId>,
521+ shared_prefix_count: usize,
522+ shared_suffix_count: usize,
523+ old_shared_suffix_start: usize,
524+) {
525+ // Should have already diffed the shared-key prefixes and suffixes.
526+ debug_assert_ne!(new.first().map(|n| n.key()), old.first().map(|o| o.key()));
527+ debug_assert_ne!(new.last().map(|n| n.key()), old.last().map(|o| o.key()));
528+
529+ // The algorithm below relies upon using `u32::MAX` as a sentinel
530+ // value, so if we have that many new nodes, it won't work. This
531+ // check is a bit academic (hence only enabled in debug), since
532+ // wasm32 doesn't have enough address space to hold that many nodes
533+ // in memory.
534+ debug_assert!(new.len() < u32::MAX as usize);
535+
536+ // Map from each `old` node's key to its index within `old`.
537+ let mut old_key_to_old_index = FxHashMap::default();
538+ old_key_to_old_index.reserve(old.len());
539+ old_key_to_old_index.extend(old.iter().enumerate().map(|(i, o)| (o.key(), i)));
540+
541+ // The set of shared keys between `new` and `old`.
542+ let mut shared_keys = FxHashSet::default();
543+ // Map from each index in `new` to the index of the node in `old` that
544+ // has the same key.
545+ let mut new_index_to_old_index = Vec::with_capacity(new.len());
546+ new_index_to_old_index.extend(new.iter().map(|n| {
547+ let key = n.key();
548+ if let Some(&i) = old_key_to_old_index.get(&key) {
549+ shared_keys.insert(key);
550+ i
551+ } else {
552+ u32::MAX as usize
553+ }
554+ }));
555+
556+ // If none of the old keys are reused by the new children, then we
557+ // remove all the remaining old children and create the new children
558+ // afresh.
559+ if shared_suffix_count == 0 && shared_keys.is_empty() {
560+ if shared_prefix_count == 0 {
561+ change_list.commit_traversal();
562+ remove_all_children(change_list, registry, old);
563+ } else {
564+ change_list.go_down_to_child(shared_prefix_count);
565+ change_list.commit_traversal();
566+ remove_self_and_next_siblings(change_list, registry, &old[shared_prefix_count..]);
567+ }
568+ create_and_append_children(cached_set, change_list, registry, new, cached_roots);
569+ return;
570+ }
571+
572+ // Save each of the old children whose keys are reused in the new
573+ // children.
574+ let mut old_index_to_temp = vec![u32::MAX; old.len()];
575+ let mut start = 0;
576+ loop {
577+ let end = (start..old.len())
578+ .find(|&i| {
579+ let key = old[i].key();
580+ !shared_keys.contains(&key)
581+ })
582+ .unwrap_or(old.len());
583+
584+ if end - start > 0 {
585+ change_list.commit_traversal();
586+ let mut t = change_list.save_children_to_temporaries(
587+ shared_prefix_count + start,
588+ shared_prefix_count + end,
589+ );
590+ for i in start..end {
591+ old_index_to_temp[i] = t;
592+ t += 1;
593+ }
594+ }
595+
596+ debug_assert!(end <= old.len());
597+ if end == old.len() {
598+ break;
599+ } else {
600+ start = end + 1;
601+ }
602+ }
603+
604+ // Remove any old children whose keys were not reused in the new
605+ // children. Remove from the end first so that we don't mess up indices.
606+ let mut removed_count = 0;
607+ for (i, old_child) in old.iter().enumerate().rev() {
608+ if !shared_keys.contains(&old_child.key()) {
609+ registry.remove_subtree(old_child);
610+ change_list.commit_traversal();
611+ change_list.remove_child(i + shared_prefix_count);
612+ removed_count += 1;
613+ }
614+ }
615+
616+ // If there aren't any more new children, then we are done!
617+ if new.is_empty() {
618+ return;
619+ }
620+
621+ // The longest increasing subsequence within `new_index_to_old_index`. This
622+ // is the longest sequence on DOM nodes in `old` that are relatively ordered
623+ // correctly within `new`. We will leave these nodes in place in the DOM,
624+ // and only move nodes that are not part of the LIS. This results in the
625+ // maximum number of DOM nodes left in place, AKA the minimum number of DOM
626+ // nodes moved.
627+ let mut new_index_is_in_lis = FxHashSet::default();
628+ new_index_is_in_lis.reserve(new_index_to_old_index.len());
629+ let mut predecessors = vec![0; new_index_to_old_index.len()];
630+ let mut starts = vec![0; new_index_to_old_index.len()];
631+ longest_increasing_subsequence::lis_with(
632+ &new_index_to_old_index,
633+ &mut new_index_is_in_lis,
634+ |a, b| a < b,
635+ &mut predecessors,
636+ &mut starts,
637+ );
638+
639+ // Now we will iterate from the end of the new children back to the
640+ // beginning, diffing old children we are reusing and if they aren't in the
641+ // LIS moving them to their new destination, or creating new children. Note
642+ // that iterating in reverse order lets us use `Node.prototype.insertBefore`
643+ // to move/insert children.
644+ //
645+ // But first, we ensure that we have a child on the change list stack that
646+ // we can `insertBefore`. We handle this once before looping over `new`
647+ // children, so that we don't have to keep checking on every loop iteration.
648+ if shared_suffix_count > 0 {
649+ // There is a shared suffix after these middle children. We will be
650+ // inserting before that shared suffix, so add the first child of that
651+ // shared suffix to the change list stack.
652+ //
653+ // [... parent]
654+ change_list.go_down_to_child(old_shared_suffix_start - removed_count);
655+ // [... parent first_child_of_shared_suffix]
656+ } else {
657+ // There is no shared suffix coming after these middle children.
658+ // Therefore we have to process the last child in `new` and move it to
659+ // the end of the parent's children if it isn't already there.
660+ let last_index = new.len() - 1;
661+ let last = new.last().unwrap_throw();
662+ new = &new[..new.len() - 1];
663+ if shared_keys.contains(&last.key()) {
664+ let old_index = new_index_to_old_index[last_index];
665+ let temp = old_index_to_temp[old_index];
666+ // [... parent]
667+ change_list.go_down_to_temp_child(temp);
668+ // [... parent last]
669+ diff(
670+ cached_set,
671+ change_list,
672+ registry,
673+ &old[old_index],
674+ last,
675+ cached_roots,
676+ );
677+ if new_index_is_in_lis.contains(&last_index) {
678+ // Don't move it, since it is already where it needs to be.
679+ } else {
680+ change_list.commit_traversal();
681+ // [... parent last]
682+ change_list.append_child();
683+ // [... parent]
684+ change_list.go_down_to_temp_child(temp);
685+ // [... parent last]
686+ }
687+ } else {
688+ change_list.commit_traversal();
689+ // [... parent]
690+ create(cached_set, change_list, registry, last, cached_roots);
691+ // [... parent last]
692+ change_list.append_child();
693+ // [... parent]
694+ change_list.go_down_to_reverse_child(0);
695+ // [... parent last]
696+ }
697+ }
698+
699+ for (new_index, new_child) in new.iter().enumerate().rev() {
700+ let old_index = new_index_to_old_index[new_index];
701+ if old_index == u32::MAX as usize {
702+ debug_assert!(!shared_keys.contains(&new_child.key()));
703+ change_list.commit_traversal();
704+ // [... parent successor]
705+ create(cached_set, change_list, registry, new_child, cached_roots);
706+ // [... parent successor new_child]
707+ change_list.insert_before();
708+ // [... parent new_child]
709+ } else {
710+ debug_assert!(shared_keys.contains(&new_child.key()));
711+ let temp = old_index_to_temp[old_index];
712+ debug_assert_ne!(temp, u32::MAX);
713+
714+ if new_index_is_in_lis.contains(&new_index) {
715+ // [... parent successor]
716+ change_list.go_to_temp_sibling(temp);
717+ // [... parent new_child]
718+ } else {
719+ change_list.commit_traversal();
720+ // [... parent successor]
721+ change_list.push_temporary(temp);
722+ // [... parent successor new_child]
723+ change_list.insert_before();
724+ // [... parent new_child]
725+ }
726+
727+ diff(
728+ cached_set,
729+ change_list,
730+ registry,
731+ &old[old_index],
732+ new_child,
733+ cached_roots,
734+ );
735+ }
736+ }
737+
738+ // [... parent child]
739+ change_list.go_up();
740+ // [... parent]
741+}
742+
743+// Diff the suffix of keyed children that share the same keys in the same order.
744+//
745+// The parent must be on the change list stack when we enter this function:
746+//
747+// [... parent]
748+//
749+// When this function exits, the change list stack remains the same.
750+fn diff_keyed_suffix(
751+ cached_set: &CachedSet,
752+ change_list: &mut ChangeListBuilder,
753+ registry: &mut EventsRegistry,
754+ old: &[Node],
755+ new: &[Node],
756+ cached_roots: &mut FxHashSet<CacheId>,
757+ new_shared_suffix_start: usize,
758+) {
759+ debug_assert_eq!(old.len(), new.len());
760+ debug_assert!(!old.is_empty());
761+
762+ // [... parent]
763+ change_list.go_down();
764+ // [... parent new_child]
765+
766+ for (i, (old_child, new_child)) in old.iter().zip(new.iter()).enumerate() {
767+ change_list.go_to_sibling(new_shared_suffix_start + i);
768+ diff(
769+ cached_set,
770+ change_list,
771+ registry,
772+ old_child,
773+ new_child,
774+ cached_roots,
775+ );
776+ }
777+
778+ // [... parent]
779+ change_list.go_up();
780+}
781+
782+// Diff children that are not keyed.
783+//
784+// The parent must be on the top of the change list stack when entering this
785+// function:
786+//
787+// [... parent]
788+//
789+// the change list stack is in the same state when this function returns.
790+fn diff_non_keyed_children(
791+ cached_set: &CachedSet,
792+ change_list: &mut ChangeListBuilder,
793+ registry: &mut EventsRegistry,
794+ old: &[Node],
795+ new: &[Node],
796+ cached_roots: &mut FxHashSet<CacheId>,
797+) {
798+ // Handled these cases in `diff_children` before calling this function.
799+ debug_assert!(!new.is_empty());
800+ debug_assert!(!old.is_empty());
801+
802+ // [... parent]
803+ change_list.go_down();
804+ // [... parent child]
805+
806+ for (i, (new_child, old_child)) in new.iter().zip(old.iter()).enumerate() {
807+ // [... parent prev_child]
808+ change_list.go_to_sibling(i);
809+ // [... parent this_child]
810+ diff(
811+ cached_set,
812+ change_list,
813+ registry,
814+ old_child,
815+ new_child,
816+ cached_roots,
817+ );
818+ }
819+
820+ match old.len().cmp(&new.len()) {
821+ Ordering::Greater => {
822+ // [... parent prev_child]
823+ change_list.go_to_sibling(new.len());
824+ // [... parent first_child_to_remove]
825+ change_list.commit_traversal();
826+ remove_self_and_next_siblings(change_list, registry, &old[new.len()..]);
827+ // [... parent]
828+ }
829+ Ordering::Less => {
830+ // [... parent last_child]
831+ change_list.go_up();
832+ // [... parent]
833+ change_list.commit_traversal();
834+ create_and_append_children(
835+ cached_set,
836+ change_list,
837+ registry,
838+ &new[old.len()..],
839+ cached_roots,
840+ );
841+ }
842+ Ordering::Equal => {
843+ // [... parent child]
844+ change_list.go_up();
845+ // [... parent]
846+ }
847+ }
848+}
849+
850+// Create the given children and append them to the parent node.
851+//
852+// The parent node must currently be on top of the change list stack:
853+//
854+// [... parent]
855+//
856+// When this function returns, the change list stack is in the same state.
857+fn create_and_append_children(
858+ cached_set: &CachedSet,
859+ change_list: &mut ChangeListBuilder,
860+ registry: &mut EventsRegistry,
861+ new: &[Node],
862+ cached_roots: &mut FxHashSet<CacheId>,
863+) {
864+ debug_assert!(change_list.traversal_is_committed());
865+ for child in new {
866+ create(cached_set, change_list, registry, child, cached_roots);
867+ change_list.append_child();
868+ }
869+}
870+
871+// Remove all of a node's children.
872+//
873+// The change list stack must have this shape upon entry to this function:
874+//
875+// [... parent]
876+//
877+// When this function returns, the change list stack is in the same state.
878+fn remove_all_children(
879+ change_list: &mut ChangeListBuilder,
880+ registry: &mut EventsRegistry,
881+ old: &[Node],
882+) {
883+ debug_assert!(change_list.traversal_is_committed());
884+ for child in old {
885+ registry.remove_subtree(child);
886+ }
887+ // Fast way to remove all children: set the node's textContent to an empty
888+ // string.
889+ change_list.set_text("");
890+}
891+
892+// Remove the current child and all of its following siblings.
893+//
894+// The change list stack must have this shape upon entry to this function:
895+//
896+// [... parent child]
897+//
898+// After the function returns, the child is no longer on the change list stack:
899+//
900+// [... parent]
901+fn remove_self_and_next_siblings(
902+ change_list: &mut ChangeListBuilder,
903+ registry: &mut EventsRegistry,
904+ old: &[Node],
905+) {
906+ debug_assert!(change_list.traversal_is_committed());
907+ for child in old {
908+ registry.remove_subtree(child);
909+ }
910+ change_list.remove_self_and_next_siblings();
911+}
912+
913+// Emit instructions to create the given virtual node.
914+//
915+// The change list stack may have any shape upon entering this function:
916+//
917+// [...]
918+//
919+// When this function returns, the new node is on top of the change list stack:
920+//
921+// [... node]
922+fn create(
923+ cached_set: &CachedSet,
924+ change_list: &mut ChangeListBuilder,
925+ registry: &mut EventsRegistry,
926+ node: &Node,
927+ cached_roots: &mut FxHashSet<CacheId>,
928+) {
929+ debug_assert!(change_list.traversal_is_committed());
930+ match node.kind {
931+ NodeKind::Text(TextNode { text }) => {
932+ change_list.create_text_node(text);
933+ }
934+ NodeKind::Element(&ElementNode {
935+ key: _,
936+ tag_name,
937+ listeners,
938+ attributes,
939+ children,
940+ namespace,
941+ }) => {
942+ if let Some(namespace) = namespace {
943+ change_list.create_element_ns(tag_name, namespace);
944+ } else {
945+ change_list.create_element(tag_name);
946+ }
947+
948+ for l in listeners {
949+ unsafe {
950+ registry.add(l);
951+ }
952+ change_list.new_event_listener(l);
953+ }
954+
955+ for attr in attributes {
956+ change_list.set_attribute(&attr.name, &attr.value, namespace.is_some());
957+ }
958+
959+ // Fast path: if there is a single text child, it is faster to
960+ // create-and-append the text node all at once via setting the
961+ // parent's `textContent` in a single change list instruction than
962+ // to emit three instructions to (1) create a text node, (2) set its
963+ // text content, and finally (3) append the text node to this
964+ // parent.
965+ if children.len() == 1 {
966+ if let Node {
967+ kind: NodeKind::Text(TextNode { text }),
968+ } = children[0]
969+ {
970+ change_list.set_text(text);
971+ return;
972+ }
973+ }
974+
975+ for child in children {
976+ create(cached_set, change_list, registry, child, cached_roots);
977+ change_list.append_child();
978+ }
979+ }
980+ NodeKind::Cached(ref c) => {
981+ cached_roots.insert(c.id);
982+ let (node, template) = cached_set.get(c.id);
983+ if let Some(template) = template {
984+ create_with_template(
985+ cached_set,
986+ change_list,
987+ registry,
988+ template,
989+ node,
990+ cached_roots,
991+ );
992+ } else {
993+ create(cached_set, change_list, registry, node, cached_roots);
994+ }
995+ }
996+ }
997+}
998+
999+// Get or create the template.
1000+//
1001+// Upon entering this function the change list stack may be in any shape:
1002+//
1003+// [...]
1004+//
1005+// When this function returns, it leaves a freshly cloned copy of the template
1006+// on the top of the change list stack:
1007+//
1008+// [... template]
1009+#[inline]
1010+fn get_or_create_template<'a>(
1011+ cached_set: &'a CachedSet,
1012+ change_list: &mut ChangeListBuilder,
1013+ registry: &mut EventsRegistry,
1014+ cached_roots: &mut FxHashSet<CacheId>,
1015+ template_id: CacheId,
1016+) -> (&'a Node<'a>, bool) {
1017+ let (template, template_template) = cached_set.get(template_id);
1018+ debug_assert!(
1019+ template_template.is_none(),
1020+ "templates should not be templated themselves"
1021+ );
1022+
1023+ // If we haven't already created and saved the physical DOM subtree for this
1024+ // template, do that now.
1025+ if change_list.has_template(template_id) {
1026+ // Clone the template and push it onto the stack.
1027+ //
1028+ // [...]
1029+ change_list.push_template(template_id);
1030+ // [... template]
1031+
1032+ (template, true)
1033+ } else {
1034+ // [...]
1035+ create(cached_set, change_list, registry, template, cached_roots);
1036+ // [... template]
1037+ change_list.save_template(template_id);
1038+ // [... template]
1039+
1040+ (template, false)
1041+ }
1042+}
1043+
1044+fn create_and_replace(
1045+ cached_set: &CachedSet,
1046+ change_list: &mut ChangeListBuilder,
1047+ registry: &mut EventsRegistry,
1048+ new_template: Option<CacheId>,
1049+ old: &Node,
1050+ new: &Node,
1051+ cached_roots: &mut FxHashSet<CacheId>,
1052+) {
1053+ debug_assert!(change_list.traversal_is_committed());
1054+
1055+ if let Some(template_id) = new_template {
1056+ let (template, needs_listeners) =
1057+ get_or_create_template(cached_set, change_list, registry, cached_roots, template_id);
1058+ change_list.replace_with();
1059+
1060+ let mut old_forcing = None;
1061+ if needs_listeners {
1062+ old_forcing = Some(change_list.push_force_new_listeners());
1063+ }
1064+
1065+ diff(
1066+ cached_set,
1067+ change_list,
1068+ registry,
1069+ template,
1070+ new,
1071+ cached_roots,
1072+ );
1073+
1074+ if let Some(old) = old_forcing {
1075+ change_list.pop_force_new_listeners(old);
1076+ }
1077+
1078+ change_list.commit_traversal();
1079+ } else {
1080+ create(cached_set, change_list, registry, new, cached_roots);
1081+ change_list.replace_with();
1082+ }
1083+ registry.remove_subtree(old);
1084+}
1085+
1086+fn create_with_template(
1087+ cached_set: &CachedSet,
1088+ change_list: &mut ChangeListBuilder,
1089+ registry: &mut EventsRegistry,
1090+ template_id: CacheId,
1091+ node: &Node,
1092+ cached_roots: &mut FxHashSet<CacheId>,
1093+) {
1094+ debug_assert!(change_list.traversal_is_committed());
1095+
1096+ // [...]
1097+ let (template, needs_listeners) =
1098+ get_or_create_template(cached_set, change_list, registry, cached_roots, template_id);
1099+ // [... template]
1100+
1101+ // Now diff the node with its template.
1102+ //
1103+ // We must force adding new listeners instead of updating existing ones,
1104+ // since listeners don't get cloned in `cloneNode`.
1105+ let mut old_forcing = None;
1106+ if needs_listeners {
1107+ old_forcing = Some(change_list.push_force_new_listeners());
1108+ }
1109+
1110+ diff(
1111+ cached_set,
1112+ change_list,
1113+ registry,
1114+ template,
1115+ node,
1116+ cached_roots,
1117+ );
1118+
1119+ if let Some(old) = old_forcing {
1120+ change_list.pop_force_new_listeners(old);
1121+ }
1122+
1123+ // Make sure that we come back up to the level we were at originally.
1124+ change_list.commit_traversal();
1125+}
src/events.rs+123−84
@@ -1,99 +1,138 @@
1−use crate::vdom::{VdomInner, VdomWeak};
1+use crate::{node::Node, vdom::VdomInner, Listener};
2−use crate::{Listener, ListenerCallback};
3−use log::*;
4use std::cell::RefCell;
2use std::cell::RefCell;
5−use std::collections::HashMap;
6−use std::fmt;
7−use std::mem;
8use std::rc::{Rc, Weak};
3use std::rc::{Rc, Weak};
9−use wasm_bindgen::closure::Closure;
10−use wasm_bindgen::prelude::*;
11
4
12−/// The events registry manages event listeners for a virtual DOM.
5+cfg_if::cfg_if! {
13−///
6+ if #[cfg(all(feature = "xxx-unstable-internal-use-only", not(target_arch = "wasm32")))] {
14−/// The events registry is persistent across virtual DOM rendering and double
7+ #[derive(Debug)]
15−/// buffering.
16−pub(crate) struct EventsRegistry {
8+ pub(crate) struct EventsRegistry {}
9+ impl EventsRegistry {
17− vdom: Weak<VdomInner>,
10+ pub(crate) fn new(_vdom: Weak<VdomInner>) -> (
11+ Rc<RefCell<EventsRegistry>>,
12+ crate::EventsTrampoline,
13+ ) {
14+ (Rc::new(RefCell::new(EventsRegistry {})), ())
15+ }
16+ pub(crate) fn remove(&mut self, _listener: &Listener) {}
17+ pub(crate) fn remove_subtree(&mut self, _node: &Node) {}
18+ pub(crate) unsafe fn add<'a>(&mut self, _listener: &'a Listener<'a>) {}
19+ pub(crate) fn clear_active_listeners(&mut self) {}
20+ }
21+ } else {
22+ use crate::{
18− active: HashMap<(u32, u32), ListenerCallback<'static>>,
23+ node::{ElementNode, ListenerCallback, NodeKind},
24+ vdom::VdomWeak,
19−}
25+ };
26+ use fxhash::FxHashMap;
27+ use std::fmt;
28+ use std::mem;
29+ use wasm_bindgen::closure::Closure;
30+ use wasm_bindgen::prelude::*;
20
31
21−impl fmt::Debug for EventsRegistry {
32+ /// The events registry manages event listeners for a virtual DOM.
22− fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
33+ ///
34+ /// The events registry is persistent across virtual DOM rendering and double
35+ /// buffering.
23− f.debug_struct("EventsRegistry")
36+ pub(crate) struct EventsRegistry {
24− .field("active", &self.active.keys().collect::<Vec<_>>())
37+ vdom: Weak<VdomInner>,
25− .finish()
38+ active: FxHashMap<(u32, u32), ListenerCallback<'static>>,
26− }
27−}
39+ }
28
40
29−impl EventsRegistry {
41+ impl fmt::Debug for EventsRegistry {
30− /// Construct a new events registry and JS function trampoline that weakly
42+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
31− /// holds the new registry and can be used by JS to invoke listeners on the
43+ f.debug_struct("EventsRegistry")
32− /// Rust side.
44+ .field("active", &self.active.keys().collect::<Vec<_>>())
33− pub(crate) fn new(
45+ .finish()
34− vdom: Weak<VdomInner>,
46+ }
35− ) -> (
36− Rc<RefCell<EventsRegistry>>,
37− Closure<Fn(web_sys::Event, u32, u32)>,
38− ) {
39− let registry = Rc::new(RefCell::new(EventsRegistry {
40− vdom,
41− active: HashMap::new(),
42− }));
47+ }
43
48
44− let weak_registry = Rc::downgrade(&registry);
49+ impl EventsRegistry {
45− let closure = Closure::wrap(Box::new(move |event, a, b| {
50+ /// Construct a new events registry and JS function trampoline that weakly
46− debug!("Events trampoline invoked with (0x{:x}, 0x{:x})", a, b);
51+ /// holds the new registry and can be used by JS to invoke listeners on the
47− debug_assert!(a != 0);
52+ /// Rust side.
53+ pub(crate) fn new(
54+ vdom: Weak<VdomInner>,
55+ ) -> (
56+ Rc<RefCell<EventsRegistry>>,
57+ crate::EventsTrampoline,
58+ ) {
59+ let registry = Rc::new(RefCell::new(EventsRegistry {
60+ vdom,
61+ active: FxHashMap::default(),
62+ }));
48
63
49− // if the VdomInnerExclusive is keeping this closure alive, then the
64+ let weak_registry = Rc::downgrade(&registry);
50− // VdomInnerExclusive should also be keeping the registry alive
65+ let closure = Closure::wrap(Box::new(move |event, a, b| {
51− let registry = weak_registry.upgrade().unwrap_throw();
66+ debug_assert!(a != 0);
52− let registry = registry.borrow();
53
67
68+ // if the VdomInnerExclusive is keeping this closure alive, then the
69+ // VdomInnerExclusive should also be keeping the registry alive
70+ let registry = weak_registry.upgrade().unwrap_throw();
71+ let registry = registry.borrow();
72+
54− match registry.active.get(&(a, b)) {
73+ match registry.active.get(&(a, b)) {
55− None => warn!(
74+ None => warn!(
56− "EventsRegistry closure invoked with unknown listener parts: \
75+ "EventsRegistry closure invoked with unknown listener parts: \
57− (0x{:x}, 0x{:x})",
76+ (0x{:x}, 0x{:x})",
58− a, b
77+ a, b
59− ),
78+ ),
60− Some(callback) => {
79+ Some(callback) => {
61− let vdom = registry.vdom.upgrade().expect_throw(
80+ let vdom = registry.vdom.upgrade().expect_throw(
62− "if the registry is still around, then the vdom should still be around",
81+ "if the registry is still around, then the vdom should still be around",
63− );
82+ );
64− let vdom_weak = VdomWeak::new(&vdom);
83+ let vdom_weak = VdomWeak::new(&vdom);
65− let mut vdom = vdom.exclusive.borrow_mut();
84+ let mut vdom = vdom.exclusive.borrow_mut();
66− let component = vdom.component_raw_mut();
85+ let component = vdom.component_raw_mut();
67− callback(component, vdom_weak, event);
86+ callback(component, vdom_weak, event);
68− }
87+ }
88+ }
89+ }) as Box<dyn Fn(web_sys::Event, u32, u32)>);
90+
91+ (registry, closure)
69 }
92 }
70− }) as Box<Fn(web_sys::Event, u32, u32)>);
71
93
72− (registry, closure)
94+ pub(crate) fn remove(&mut self, listener: &Listener) {
95+ let id = listener.get_callback_parts();
96+ debug_assert!(id.0 != 0);
97+ self.active.remove(&id);
98+ }
99+
100+ pub(crate) fn remove_subtree(&mut self, node: &Node) {
101+ match node.kind {
102+ NodeKind::Cached(_) | NodeKind::Text(_) => {},
103+ NodeKind::Element(&ElementNode {listeners, children, ..}) => {
104+ for l in listeners {
105+ self.remove(l);
106+ }
107+ for child in children {
108+ self.remove_subtree(child)
109+ }
110+ }
111+ }
73− }
112+ }
74
113
75− /// Add an event listener to the registry, exposing to JS.
114+ /// Add an event listener to the registry, exposing to JS.
76− ///
115+ ///
77− /// # Unsafety
116+ /// # Unsafety
78− ///
117+ ///
79− /// The listener's lifetime is extended to `'static` and it is the caller's
118+ /// The listener's lifetime is extended to `'static` and it is the
80− /// responsibility to ensure that the listener is not kept in the registry
119+ /// caller's responsibility to ensure that the listener is not kept
81− /// after it is dropped. This is typically done via clearing the whole
120+ /// in the registry after it is dropped. This is maintained during
82− /// registry on each render and repopulating it during diffing.
121+ /// diffing.
83− pub(crate) unsafe fn add<'a>(&mut self, listener: &'a Listener<'a>) {
122+ pub(crate) unsafe fn add<'a>(&mut self, listener: &'a Listener<'a>) {
84− let id = listener.get_callback_parts();
123+ let id = listener.get_callback_parts();
85− debug!("EventsRegistry::add(0x{:x}, 0x{:x})", id.0, id.1);
86− debug_assert!(id.0 != 0);
124+ debug_assert!(id.0 != 0);
87
125
88− let callback =
126+ let callback =
89− mem::transmute::<ListenerCallback<'a>, ListenerCallback<'static>>(listener.callback);
127+ mem::transmute::<ListenerCallback<'a>, ListenerCallback<'static>>(listener.callback);
90− let old = self.active.insert(id, callback);
128+ let old = self.active.insert(id, callback);
91− debug_assert!(old.is_none());
129+ debug_assert!(old.is_none());
92− }
130+ }
93
131
94− /// Clear all event listeners from the registry.
132+ /// Clear all event listeners from the registry.
95− pub(crate) fn clear_active_listeners(&mut self) {
133+ pub(crate) fn clear_active_listeners(&mut self) {
96− debug!("EventsRegistry::clear_active_listeners()");
97− self.active.clear();
134+ self.active.clear();
135+ }
136+ }
98 }
137 }
99}
138}
src/lib.rs+84−129
@@ -3,7 +3,7 @@
3//! ## Example
3//! ## Example
4//!
4//!
5//! ```no_run
5//! ```no_run
6−//! use dodrio::{on, bumpalo::Bump, Attribute, Node, Render};
6+//! use dodrio::{bumpalo, Attribute, Node, Render, RenderContext};
7//! use wasm_bindgen::UnwrapThrowExt;
7//! use wasm_bindgen::UnwrapThrowExt;
8//!
8//!
9//! /// A component that greets someone.
9//! /// A component that greets someone.
@@ -18,163 +18,118 @@
18//! }
18//! }
19//! }
19//! }
20//!
20//!
21−//! impl<'who> Render for Hello<'who> {
21+//! impl<'a, 'who> Render<'a> for Hello<'who> {
22−//! fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
22+//! fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
23−//! where
23+//! use dodrio::builder::*;
24−//! 'a: 'bump,
25−//! {
24+//!
26−//! let id = bumpalo::format!(in bump, "hello-{}", self.who);
25+//! let id = bumpalo::format!(in cx.bump, "hello-{}", self.who);
27−//! Node::element(
26+//! let who = bumpalo::collections::String::from_str_in(self.who, cx.bump).into_bump_str();
28−//! bump,
27+//!
29−//! // The element's tag name. In this case a `<p>` element.
28+//! div(&cx)
30−//! "p",
29+//! .attr("id", id.into_bump_str())
31−//! // Listeners.
32−//! [on(bump, "click", |root, _vdom, _event| {
30+//! .on("click", |root, _vdom, _event| {
33//! let hello = root.unwrap_mut::<Hello>();
31//! let hello = root.unwrap_mut::<Hello>();
32+//! web_sys::window()
34−//! let window = web_sys::window().expect_throw("should have a `Window` on the Web");
33+//! .expect_throw("should have a `Window` on the Web")
35−//! window.alert_with_message(hello.who);
34+//! .alert_with_message(hello.who);
36−//! })],
35+//! })
37−//! // Attributes.
36+//! .children([
38−//! [Attribute { name: "id", value: id.into_bump_str() }],
39−//! // Child nodes.
40−//! [
41−//! Node::text("Hello, "),
37+//! text("Hello, "),
42−//! Node::element(
43−//! bump,
44−//! "strong",
38+//! strong(&cx)
45−//! [],
46−//! [],
47−//! [
39+//! .children([
48−//! Node::text(self.who),
40+//! text(who),
49−//! Node::text("!"),
41+//! text("!"),
50−//! ],
42+//! ])
51−//! ),
43+//! .finish(),
52−//! ],
44+//! ])
53−//! )
45+//! .finish()
54//! }
46//! }
55//! }
47//! }
56//! ```
48//! ```
49+#![deny(missing_docs, missing_debug_implementations)]
57
50
58// Re-export the `bumpalo` crate.
51// Re-export the `bumpalo` crate.
59pub use bumpalo;
52pub use bumpalo;
60
53
61−use bumpalo::Bump;
54+cfg_if::cfg_if! {
62−use std::any::Any;
55+ if #[cfg(feature = "log")] {
63−use std::rc::Rc;
56+ #[macro_use]
64−use wasm_bindgen::UnwrapThrowExt;
57+ extern crate log;
58+ } else {
59+ #[macro_use]
60+ mod logging;
61+ }
62+}
63+
64+// A macro to expose items unstably, only for internal/testing usage.
65+cfg_if::cfg_if! {
66+ if #[cfg(feature = "xxx-unstable-internal-use-only")] {
67+ macro_rules! pub_unstable_internal {
68+ ( $(#[$attr:meta])* pub(crate) $( $thing:tt )* ) => {
69+ #[doc(hidden)]
70+ $( #[$attr] )*
71+ pub $( $thing )*
72+ }
73+ }
74+ } else {
75+ macro_rules! pub_unstable_internal {
76+ ( $(#[$attr:meta])* pub(crate) $( $thing:tt )* ) => {
77+ $( #[$attr] )*
78+ pub(crate) $( $thing )*
79+ }
80+ }
81+ }
82+}
65
83
66// Only `pub` so that the wasm-bindgen bindings work.
84// Only `pub` so that the wasm-bindgen bindings work.
67#[doc(hidden)]
85#[doc(hidden)]
68pub mod change_list;
86pub mod change_list;
69
87
70mod cached;
88mod cached;
89+mod cached_set;
90+mod diff;
71mod events;
91mod events;
72mod node;
92mod node;
93+mod render;
94+mod render_context;
95+mod strace;
73mod vdom;
96mod vdom;
74
97
98+pub mod builder;
99+
75// Re-export items at the top level.
100// Re-export items at the top level.
76pub use self::cached::Cached;
101pub use self::cached::Cached;
77−pub use self::node::{on, Attribute, ElementNode, Listener, ListenerCallback, Node, TextNode};
102+pub use self::node::{Attribute, Listener, Node, NodeKey};
103+pub use self::render::{Render, RootRender};
104+pub use self::render_context::RenderContext;
78pub use self::vdom::{Vdom, VdomWeak};
105pub use self::vdom::{Vdom, VdomWeak};
79
106
80−pub trait Render {
107+cfg_if::cfg_if! {
81− fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
108+ if #[cfg(all(target_arch = "wasm32", not(feature = "xxx-unstable-internal-use-only")))] {
82− where
109+ use wasm_bindgen::__rt::WasmRefCell as RefCell;
83− 'a: 'bump;
110+ } else {
84−}
111+ use std::cell::RefCell;
85−
86−impl<'r, R> Render for &'r R
87−where
88− R: Render,
89−{
90− fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
91− where
92− 'a: 'bump,
93− {
94− (**self).render(bump)
95 }
112 }
96}
113}
97
114
98−impl<R> Render for Rc<R>
115+// Polyfill some Web stuff for benchmarking...
99−where
116+cfg_if::cfg_if! {
100− R: Render,
117+ if #[cfg(all(feature = "xxx-unstable-internal-use-only", not(target_arch = "wasm32")))] {
101−{
118+ /// An element node in the physical DOM.
102− fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
119+ pub type Element = ();
103− where
104− 'a: 'bump,
105− {
106− (**self).render(bump)
107− }
108−}
109
120
110−/// A `RootRender` is a render component that can be the root rendering component
121+ pub(crate) type EventsTrampoline = ();
111−/// mounted to a virtual DOM.
122+ } else {
112−///
123+ /// An element node in the physical DOM.
113−/// In addition to rendering, it must also be `'static` so that it can be owned
124+ pub type Element = web_sys::Element;
114−/// by the virtual DOM and `Any` so that it can be downcast to its concrete type
115−/// by event listener callbacks.
116−///
117−/// You do not need to implement this trait by hand: there is a blanket
118−/// implementation for all `Render` types that fulfill the `RootRender`
119−/// requirements.
120−pub trait RootRender: Any + Render {
121− /// Get this `&RootRender` trait object as an `&Any` trait object reference.
122− fn as_any(&self) -> &Any;
123
125
124− /// Get this `&mut RootRender` trait object as an `&mut Any` trait object
126+ pub(crate) type EventsTrampoline = wasm_bindgen::closure::Closure<dyn Fn(web_sys::Event, u32, u32)>;
125− /// reference.
126− fn as_any_mut(&mut self) -> &mut Any;
127−}
128−
129−impl<T: Any + Render> RootRender for T {
130− fn as_any(&self) -> &Any {
131− self
132− }
133−
134− fn as_any_mut(&mut self) -> &mut Any {
135− self
136 }
127 }
137}
128}
138
129
139−impl dyn RootRender {
130+cfg_if::cfg_if! {
140− /// Downcast this shared `&dyn RootRender` trait object reference to its
131+ if #[cfg(feature = "xxx-unstable-internal-use-only")] {
141− /// underlying concrete type.
132+ pub use self::cached_set::{CachedSet};
142− ///
133+ pub use self::node::{ElementNode, NodeKind, TextNode};
143− /// # Panics
144− ///
145− /// Panics if this virtual DOM's root rendering component is not an `R`
146− /// instance.
147− pub fn unwrap_ref<R: RootRender>(&self) -> &R {
148− self.as_any()
149− .downcast_ref::<R>()
150− .expect_throw("bad `RootRender::unwrap_ref` call")
151− }
152−
153− /// Downcast this exclusive `&mut dyn RootRender` trait object reference to
154− /// its underlying concrete type.
155− ///
156− /// # Panics
157− ///
158− /// Panics if this virtual DOM's root rendering component is not an `R`
159− /// instance.
160− pub fn unwrap_mut<R: RootRender>(&mut self) -> &mut R {
161− self.as_any_mut()
162− .downcast_mut::<R>()
163− .expect_throw("bad `RootRender::unwrap_ref` call")
164− }
165−}
166−
167−#[cfg(test)]
168−mod tests {
169− #[test]
170− fn render_is_object_safe() {
171− #[allow(dead_code)]
172− fn takes_dyn_render(_: &dyn super::Render) {}
173− }
174−
175− #[test]
176− fn root_render_is_object_safe() {
177− #[allow(dead_code)]
178− fn takes_dyn_render(_: &dyn super::RootRender) {}
179 }
134 }
180}
135}
src/logging.rsnew file+65
@@ -0,0 +1,65 @@
1+#[allow(unused_macros)]
2+macro_rules! debug {
3+ ( $( $e:expr ),* $(,)* ) => {
4+ if false {
5+ $(
6+ let _ = $e;
7+ )*
8+ }
9+ }
10+}
11+
12+#[allow(unused_macros)]
13+macro_rules! error {
14+ ( $( $e:expr ),* $(,)* ) => {
15+ if false {
16+ $(
17+ let _ = $e;
18+ )*
19+ }
20+ }
21+}
22+
23+#[allow(unused_macros)]
24+macro_rules! info {
25+ ( $( $e:expr ),* $(,)* ) => {
26+ if false {
27+ $(
28+ let _ = $e;
29+ )*
30+ }
31+ }
32+}
33+
34+#[allow(unused_macros)]
35+macro_rules! log {
36+ ( $( $e:expr ),* $(,)* ) => {
37+ if false {
38+ $(
39+ let _ = $e;
40+ )*
41+ }
42+ }
43+}
44+
45+#[allow(unused_macros)]
46+macro_rules! trace {
47+ ( $( $e:expr ),* $(,)* ) => {
48+ if false {
49+ $(
50+ let _ = $e;
51+ )*
52+ }
53+ }
54+}
55+
56+#[allow(unused_macros)]
57+macro_rules! warn {
58+ ( $( $e:expr ),* $(,)* ) => {
59+ if false {
60+ $(
61+ let _ = $e;
62+ )*
63+ }
64+ }
65+}
src/node.rs+183−98
@@ -1,33 +1,111 @@
1−use crate::{RootRender, VdomWeak};
1+use crate::{cached_set::CacheId, RootRender, VdomWeak};
2use bumpalo::Bump;
2use bumpalo::Bump;
3use std::fmt;
3use std::fmt;
4+use std::iter;
4use std::mem;
5use std::mem;
6+use std::u32;
5
7
6−/// A node is either a text node or an element.
8+/// A virtual DOM node.
7#[derive(Debug, Clone)]
9#[derive(Debug, Clone)]
8−pub enum Node<'a> {
10+pub struct Node<'a> {
9− /// A text node.
11+ #[cfg(feature = "xxx-unstable-internal-use-only")]
10− Text(TextNode<'a>),
12+ #[doc(hidden)]
13+ pub kind: NodeKind<'a>,
11
14
12− /// An element potentially with attributes and children.
15+ #[cfg(not(feature = "xxx-unstable-internal-use-only"))]
13− Element(ElementNode<'a>),
16+ pub(crate) kind: NodeKind<'a>,
14}
17}
15
18
16−/// Text nodes are just a string of text. They cannot have attributes or
19+pub_unstable_internal! {
17−/// children.
20+ /// A node is either a text node or an element.
18−#[derive(Debug, Clone)]
21+ #[derive(Debug, Clone)]
22+ pub(crate) enum NodeKind<'a> {
23+ /// A text node.
19−pub struct TextNode<'a> {
24+ Text(TextNode<'a>),
20− pub(crate) text: &'a str,
25+
26+ /// An element potentially with attributes and children.
27+ Element(&'a ElementNode<'a>),
28+
29+ /// A node in the vdom's `CachedSet`. This allows us to avoid
30+ /// re-rendering and re-diffing subtrees.
31+ Cached(CachedNode),
32+ }
21}
33}
22
34
35+pub_unstable_internal! {
36+ /// Text nodes are just a string of text. They cannot have attributes or
37+ /// children.
38+ #[derive(Debug, Clone)]
39+ pub(crate) struct TextNode<'a> {
40+ pub text: &'a str,
41+ }
42+}
43+
44+pub_unstable_internal! {
23−/// Elements have a tag name, zero or more attributes, and zero or more
45+ /// Elements have a tag name, zero or more attributes, and zero or more
24−/// children.
46+ /// children.
25−#[derive(Debug, Clone)]
47+ #[derive(Debug, Clone)]
26−pub struct ElementNode<'a> {
48+ pub(crate) struct ElementNode<'a> {
49+ pub key: NodeKey,
27− pub(crate) tag_name: &'a str,
50+ pub tag_name: &'a str,
28− pub(crate) listeners: &'a [Listener<'a>],
51+ pub listeners: &'a [Listener<'a>],
29− pub(crate) attributes: &'a [Attribute<'a>],
52+ pub attributes: &'a [Attribute<'a>],
30− pub(crate) children: &'a [Node<'a>],
53+ pub children: &'a [Node<'a>],
54+ pub namespace: Option<&'a str>,
55+ }
56+}
57+
58+pub_unstable_internal! {
59+ /// A cached node exists in an arena that is internal to the `Vdom`. It
60+ /// allows us to avoid both re-rendering a sub-tree and re-diffing
61+ /// it. `CachedNode`s cannot contain cycles, since they can only contain
62+ /// other `CachedNode`s that already exist when they are being constructed.
63+ #[derive(Debug, Clone, Copy)]
64+ pub(crate) struct CachedNode {
65+ pub id: CacheId,
66+ // Always a copy of the inner cached node's key.
67+ pub key: NodeKey,
68+ }
69+}
70+
71+/// The key for keyed children.
72+///
73+/// Keys must be unique among siblings.
74+///
75+/// If any sibling is keyed, then they all must be keyed.
76+#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
77+pub struct NodeKey(pub(crate) u32);
78+
79+impl Default for NodeKey {
80+ fn default() -> NodeKey {
81+ NodeKey::NONE
82+ }
83+}
84+
85+impl NodeKey {
86+ /// The default, lack of a key.
87+ pub const NONE: NodeKey = NodeKey(u32::MAX);
88+
89+ /// Is this key `NodeKey::NONE`?
90+ #[inline]
91+ pub fn is_none(&self) -> bool {
92+ *self == Self::NONE
93+ }
94+
95+ /// Is this key not `NodeKey::NONE`?
96+ #[inline]
97+ pub fn is_some(&self) -> bool {
98+ !self.is_none()
99+ }
100+
101+ /// Create a new `NodeKey`.
102+ ///
103+ /// `key` must not be `u32::MAX`.
104+ #[inline]
105+ pub fn new(key: u32) -> Self {
106+ debug_assert_ne!(key, u32::MAX);
107+ NodeKey(key)
108+ }
31}
109}
32
110
33/// An event listener callback function.
111/// An event listener callback function.
@@ -36,22 +114,33 @@ pub struct ElementNode<'a> {
36///
114///
37/// 1. The virtual DOM's root rendering component.
115/// 1. The virtual DOM's root rendering component.
38/// 2. A capability to scheduler virtual DOM re-rendering.
116/// 2. A capability to scheduler virtual DOM re-rendering.
39−/// 3. The event that ocurred.
117+/// 3. The event that occurred.
40−pub type ListenerCallback<'a> =
118+pub(crate) type ListenerCallback<'a> =
41 &'a (dyn Fn(&mut dyn RootRender, VdomWeak, web_sys::Event) + 'static);
119 &'a (dyn Fn(&mut dyn RootRender, VdomWeak, web_sys::Event) + 'static);
42
120
43/// An event listener.
121/// An event listener.
44pub struct Listener<'a> {
122pub struct Listener<'a> {
45 /// The type of event to listen for.
123 /// The type of event to listen for.
46− pub event: &'a str,
124+ pub(crate) event: &'a str,
47 /// The callback to invoke when the event happens.
125 /// The callback to invoke when the event happens.
48− pub callback: ListenerCallback<'a>,
126+ pub(crate) callback: ListenerCallback<'a>,
49}
127}
50
128
129+/// An attribute on a DOM node, such as `id="my-thing"` or
130+/// `href="https://example.com"`.
51#[derive(Clone, Debug)]
131#[derive(Clone, Debug)]
52pub struct Attribute<'a> {
132pub struct Attribute<'a> {
53− pub name: &'a str,
133+ pub(crate) name: &'a str,
54− pub value: &'a str,
134+ pub(crate) value: &'a str,
135+}
136+
137+impl<'a> From<CachedNode> for Node<'a> {
138+ #[inline]
139+ fn from(c: CachedNode) -> Self {
140+ Node {
141+ kind: NodeKind::Cached(c),
142+ }
143+ }
55}
144}
56
145
57impl fmt::Debug for Listener<'_> {
146impl fmt::Debug for Listener<'_> {
@@ -66,82 +155,94 @@ impl fmt::Debug for Listener<'_> {
66 }
155 }
67}
156}
68
157
158+impl<'a> Attribute<'a> {
159+ /// Get this attribute's name, such as `"id"` in `<div id="my-thing" />`.
160+ #[inline]
161+ pub fn name(&self) -> &'a str {
162+ self.name
163+ }
164+
165+ /// The attribute value, such as `"my-thing"` in `<div id="my-thing" />`.
166+ #[inline]
167+ pub fn value(&self) -> &'a str {
168+ self.value
169+ }
170+
171+ /// Certain attributes are considered "volatile" and can change via user
172+ /// input that we can't see when diffing against the old virtual DOM. For
173+ /// these attributes, we want to always re-set the attribute on the physical
174+ /// DOM node, even if the old and new virtual DOM nodes have the same value.
175+ #[inline]
176+ pub(crate) fn is_volatile(&self) -> bool {
177+ match self.name {
178+ "value" | "checked" | "selected" => true,
179+ _ => false,
180+ }
181+ }
182+}
183+
69impl<'a> Node<'a> {
184impl<'a> Node<'a> {
70− /// Construct a new element node with the given tag name and children.
185+ /// Low-level constructor for making a new `Node` of type element with given
186+ /// parts.
187+ ///
188+ /// This is primarily intended for JSX and templating proc-macros to compile
189+ /// down into. If you are building nodes by-hand, prefer using the
190+ /// `dodrio::builder::*` APIs.
71 #[inline]
191 #[inline]
72− pub fn element<Listeners, Attributes, Children>(
192+ pub fn element(
73 bump: &'a Bump,
193 bump: &'a Bump,
194+ key: NodeKey,
74 tag_name: &'a str,
195 tag_name: &'a str,
75− listeners: Listeners,
196+ listeners: &'a [Listener<'a>],
76− attributes: Attributes,
197+ attributes: &'a [Attribute<'a>],
77− children: Children,
198+ children: &'a [Node<'a>],
199+ namespace: Option<&'a str>,
78− ) -> Node<'a>
200+ ) -> Node<'a> {
79− where
201+ let element = bump.alloc_with(|| ElementNode {
80− Listeners: 'a + AsRef<[Listener<'a>]>,
202+ key,
81− Attributes: 'a + AsRef<[Attribute<'a>]>,
82− Children: 'a + AsRef<[Node<'a>]>,
83− {
84− let children: &'a Children = bump.alloc(children);
85− let children: &'a [Node<'a>] = children.as_ref();
86−
87− let listeners: &'a Listeners = bump.alloc(listeners);
88− let listeners: &'a [Listener<'a>] = listeners.as_ref();
89−
90− let attributes: &'a Attributes = bump.alloc(attributes);
91− let attributes: &'a [Attribute<'a>] = attributes.as_ref();
92−
93− Node::Element(ElementNode {
94 tag_name,
203 tag_name,
95 listeners,
204 listeners,
96 attributes,
205 attributes,
97 children,
206 children,
207+ namespace,
98− })
208+ });
99− }
100
209
101− /// Is this node a text node?
210+ Node {
102− pub fn is_text(&self) -> bool {
211+ kind: NodeKind::Element(element),
103− match self {
104− Node::Text(_) => true,
105− _ => false,
106− }
107− }
108−
109− /// Is this node an element?
110− pub fn is_element(&self) -> bool {
111− match self {
112− Node::Element { .. } => true,
113− _ => false,
114 }
212 }
115 }
213 }
116
214
117 /// Construct a new text node with the given text.
215 /// Construct a new text node with the given text.
118 #[inline]
216 #[inline]
119− pub fn text(text: &'a str) -> Node<'a> {
217+ pub(crate) fn text(text: &'a str) -> Node<'a> {
218+ Node {
120− Node::Text(TextNode { text })
219+ kind: NodeKind::Text(TextNode { text }),
220+ }
121 }
221 }
122−}
123
222
124−impl<'a> TextNode<'a> {
223+ #[inline]
125− /// Get this text node's text content.
126− pub fn text(&self) -> &'a str {
224+ pub(crate) fn key(&self) -> NodeKey {
127− self.text
225+ match &self.kind {
226+ NodeKind::Text(_) => NodeKey::NONE,
227+ NodeKind::Element(e) => e.key,
228+ NodeKind::Cached(c) => c.key,
229+ }
128 }
230 }
129}
231}
130
232
131−impl<'a> ElementNode<'a> {
233+/// A node can become an iterator that yields the node itself once.
132− /// Get this element's tag name.
234+///
133− pub fn tag_name(&self) -> &'a str {
235+/// This implementation of `IntoIterator` mostly exists to improve the
134− self.tag_name
236+/// `typed-html` ergonomics, where the macro invokes `.into_iter()` on the child
135− }
237+/// contents of a tag. By implementing `IntoIterator` here, we avoid having to
136−
238+/// do nasty shenanigans like `<p>vec![$contents]</p>` instead of plain old
137− /// Get this element's attributes.
239+/// `<p>$contents</p>`.
138− pub fn attributes(&self) -> &'a [Attribute<'a>] {
240+impl<'a> IntoIterator for Node<'a> {
139− self.attributes
241+ type Item = Node<'a>;
140− }
242+ type IntoIter = iter::Once<Self::Item>;
141
243
142− /// Get this element's attributes.
244+ fn into_iter(self) -> Self::IntoIter {
143− pub fn children(&self) -> &'a [Node<'a>] {
245+ iter::once(self)
144− self.children
145 }
246 }
146}
247}
147
248
@@ -168,19 +269,3 @@ impl Listener<'_> {
168 }
269 }
169 }
270 }
170}
271}
171−
172−/// Utility function for creating event listeners and downcasting the component
173−/// to its `RootRender` concrete type.
174−pub fn on<'a, F>(bump: &'a Bump, event: &'a str, callback: F) -> Listener<'a>
175−where
176− F: Fn(&mut dyn RootRender, VdomWeak, web_sys::Event) + 'static,
177−{
178− Listener {
179− event,
180− callback: bump.alloc(
181− move |component: &mut dyn RootRender, vdom: VdomWeak, event: web_sys::Event| {
182− callback(component, vdom, event);
183− },
184− ),
185− }
186−}
src/render.rsnew file+168
@@ -0,0 +1,168 @@
1+use crate::{Node, RenderContext};
2+use std::any::Any;
3+use std::rc::Rc;
4+use wasm_bindgen::UnwrapThrowExt;
5+
6+/// A trait for any component that can be rendered to HTML.
7+///
8+/// Takes a shared reference to `self` and generates the virtual DOM that
9+/// represents its rendered HTML.
10+///
11+/// ## `Bump` Allocation
12+///
13+/// `Render` implementations can use the `Bump` inside the provided
14+/// `RenderContext` for very fast allocation for anything that needs to be
15+/// temporarily allocated during rendering.
16+///
17+/// ## Example
18+///
19+/// ```no_run
20+/// use dodrio::{Node, Render, RenderContext};
21+///
22+/// pub struct MyComponent;
23+///
24+/// impl<'a> Render<'a> for MyComponent {
25+/// fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
26+/// use dodrio::builder::*;
27+///
28+/// p(&cx)
29+/// .children([
30+/// text("This is "),
31+/// strong(&cx).children([text("my component")]).finish(),
32+/// text(" rendered!"),
33+/// ])
34+/// .finish()
35+/// }
36+/// }
37+/// ```
38+pub trait Render<'a> {
39+ /// Render `self` as a virtual DOM. Use the given context's `Bump` for
40+ /// temporary allocations.
41+ fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a>;
42+}
43+
44+impl<'a, 'r, R> Render<'a> for &'r R
45+where
46+ R: Render<'a>,
47+{
48+ fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
49+ (**self).render(cx)
50+ }
51+}
52+
53+impl<'a, R> Render<'a> for Rc<R>
54+where
55+ R: Render<'a>,
56+{
57+ fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
58+ (**self).render(cx)
59+ }
60+}
61+
62+/// A `RootRender` is a render component that can be the root rendering component
63+/// mounted to a virtual DOM.
64+///
65+/// In addition to rendering, it must also be `'static` so that it can be owned
66+/// by the virtual DOM and `Any` so that it can be downcast to its concrete type
67+/// by event listener callbacks.
68+///
69+/// You do not need to implement this trait by hand: there is a blanket
70+/// implementation for all `Render` types that fulfill the `RootRender`
71+/// requirements.
72+pub trait RootRender: Any + for<'a> Render<'a> {
73+ /// Get this `&RootRender` trait object as an `&Any` trait object reference.
74+ fn as_any(&self) -> &dyn Any;
75+
76+ /// Get this `&mut RootRender` trait object as an `&mut Any` trait object
77+ /// reference.
78+ fn as_any_mut(&mut self) -> &mut dyn Any;
79+}
80+
81+impl<T> RootRender for T
82+where
83+ T: Any + for<'a> Render<'a>,
84+{
85+ fn as_any(&self) -> &dyn Any {
86+ self
87+ }
88+
89+ fn as_any_mut(&mut self) -> &mut dyn Any {
90+ self
91+ }
92+}
93+
94+impl dyn RootRender {
95+ /// Downcast this shared `&dyn RootRender` trait object reference to its
96+ /// underlying concrete type.
97+ ///
98+ /// # Panics
99+ ///
100+ /// Panics if this virtual DOM's root rendering component is not an `R`
101+ /// instance.
102+ pub fn unwrap_ref<R>(&self) -> &R
103+ where
104+ R: RootRender,
105+ {
106+ self.as_any()
107+ .downcast_ref::<R>()
108+ .expect_throw("bad `RootRender::unwrap_ref` call")
109+ }
110+
111+ /// Downcast this exclusive `&mut dyn RootRender` trait object reference to
112+ /// its underlying concrete type.
113+ ///
114+ /// # Panics
115+ ///
116+ /// Panics if this virtual DOM's root rendering component is not an `R`
117+ /// instance.
118+ pub fn unwrap_mut<R>(&mut self) -> &mut R
119+ where
120+ R: RootRender,
121+ {
122+ self.as_any_mut()
123+ .downcast_mut::<R>()
124+ .expect_throw("bad `RootRender::unwrap_ref` call")
125+ }
126+}
127+
128+#[cfg(test)]
129+mod tests {
130+ #[test]
131+ fn render_is_object_safe() {
132+ #[allow(dead_code)]
133+ fn takes_dyn_render(_: &dyn super::Render) {}
134+ }
135+
136+ #[test]
137+ fn root_render_is_object_safe() {
138+ #[allow(dead_code)]
139+ fn takes_dyn_render(_: &dyn super::RootRender) {}
140+ }
141+
142+ #[test]
143+ fn render_bump_scoped_child() {
144+ use crate::{builder::*, bumpalo::collections::String, Node, Render, RenderContext};
145+
146+ struct Child<'a> {
147+ name: &'a str,
148+ }
149+
150+ impl<'a> Render<'a> for Child<'a> {
151+ fn render(&self, _cx: &mut RenderContext<'a>) -> Node<'a> {
152+ text(self.name)
153+ }
154+ }
155+
156+ struct Parent;
157+
158+ impl<'a> Render<'a> for Parent {
159+ fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
160+ let child_name = String::from_str_in("child", cx.bump).into_bump_str();
161+
162+ div(&cx)
163+ .children([Child { name: child_name }.render(cx)])
164+ .finish()
165+ }
166+ }
167+ }
168+}
src/render_context.rsnew file+104
@@ -0,0 +1,104 @@
1+use crate::{
2+ cached::{Cached, TemplateId},
3+ cached_set::{CacheId, CachedSet},
4+ Node, Render,
5+};
6+use bumpalo::Bump;
7+use fxhash::FxHashMap;
8+use std::fmt;
9+
10+/// Common context available to all `Render` implementations.
11+///
12+/// Notably, the `RenderContext` gives access to the bump arena that the virtual
13+/// DOM should be allocated within. This is available via the `bump` field.
14+pub struct RenderContext<'a> {
15+ /// The underlying bump arena that virtual DOMs are rendered into.
16+ ///
17+ /// ## Example
18+ ///
19+ /// ```
20+ /// use dodrio::RenderContext;
21+ ///
22+ /// // Given a rendering context, allocate an i32 inside its bump arena.
23+ /// fn foo<'a>(cx: &mut RenderContext<'a>) -> &'a mut i32 {
24+ /// cx.bump.alloc(42)
25+ /// }
26+ /// ```
27+ pub bump: &'a Bump,
28+
29+ pub(crate) cached_set: &'a crate::RefCell<CachedSet>,
30+
31+ pub(crate) templates: &'a mut FxHashMap<TemplateId, Option<CacheId>>,
32+
33+ // Prevent exhaustive matching on the rendering context, so we can always
34+ // add more members in a semver-compatible way.
35+ _non_exhaustive: (),
36+}
37+
38+impl fmt::Debug for RenderContext<'_> {
39+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
40+ f.debug_struct("RenderContext")
41+ .field("bump", &self.bump)
42+ .finish()
43+ }
44+}
45+
46+impl<'a> RenderContext<'a> {
47+ pub_unstable_internal! {
48+ pub(crate) fn new(
49+ bump: &'a Bump,
50+ cached_set: &'a crate::RefCell<CachedSet>,
51+ templates: &'a mut FxHashMap<TemplateId, Option<CacheId>>
52+ ) -> Self {
53+ RenderContext {
54+ bump,
55+ cached_set,
56+ templates,
57+ _non_exhaustive: (),
58+ }
59+ }
60+ }
61+
62+ pub(crate) fn cache<F>(&mut self, pinned: bool, template: Option<CacheId>, f: F) -> CacheId
63+ where
64+ F: for<'b> FnOnce(&mut RenderContext<'b>) -> Node<'b>,
65+ {
66+ CachedSet::insert(self, pinned, template, f)
67+ }
68+
69+ /// Get or create the cached template for `Cached<R>`.
70+ pub(crate) fn template<R>(&mut self) -> Option<CacheId>
71+ where
72+ R: 'static + Default + for<'b> Render<'b>,
73+ {
74+ let template_id = Cached::<R>::template_id();
75+ if let Some(cache_id) = self.templates.get(&template_id).cloned() {
76+ return cache_id;
77+ }
78+
79+ // Prevent re-entrancy from infinite looping. Any attempts to get `R`'s
80+ // template while constructing the template will simply fail to use the
81+ // templated fast path.
82+ self.templates.insert(template_id, None);
83+
84+ // Render the default `R` and save that as the template for all
85+ // `Cached<R>`s.
86+ let cache_id = self.cache(true, None, |nested_cx| R::default().render(nested_cx));
87+ self.templates.insert(template_id, Some(cache_id));
88+ Some(cache_id)
89+ }
90+}
91+
92+impl<'a, 'b> From<&'b RenderContext<'a>> for &'a Bump {
93+ #[inline]
94+ fn from(cx: &'b RenderContext<'a>) -> &'a Bump {
95+ cx.bump
96+ }
97+}
98+
99+impl<'a, 'b, 'c> From<&'c &'b mut RenderContext<'a>> for &'a Bump {
100+ #[inline]
101+ fn from(cx: &'c &'b mut RenderContext<'a>) -> &'a Bump {
102+ cx.bump
103+ }
104+}
src/strace.rsnew file+22
@@ -0,0 +1,22 @@
1+cfg_if::cfg_if! {
2+ if #[cfg(feature = "xxx-unstable-strace")] {
3+ use wasm_bindgen::prelude::*;
4+
5+ #[wasm_bindgen(module = "/js/strace.js")]
6+ extern "C" {
7+ #[wasm_bindgen(js_name = initStrace)]
8+ fn really_init_strace();
9+ }
10+
11+ pub fn init_strace() {
12+ use std::sync::Once;
13+ static STRACE: Once = Once::new();
14+ STRACE.call_once(|| {
15+ really_init_strace();
16+ });
17+ }
18+ } else {
19+ #[inline]
20+ pub fn init_strace() {}
21+ }
22+}
src/vdom.rs+186−340
@@ -1,14 +1,16 @@
1−use super::change_list::ChangeList;
1+use super::change_list::ChangeListPersistentState;
2−use super::node::{Attribute, ElementNode, Listener, Node, TextNode};
3use super::RootRender;
2use super::RootRender;
3+use crate::cached::TemplateId;
4+use crate::cached_set::{CacheId, CachedSet};
4use crate::events::EventsRegistry;
5use crate::events::EventsRegistry;
6+use crate::node::{Node, NodeKey};
7+use crate::RenderContext;
5use bumpalo::Bump;
8use bumpalo::Bump;
6−use futures::future::Future;
9+use fxhash::FxHashMap;
7−use log::*;
8use std::cell::Cell;
10use std::cell::Cell;
9use std::cell::RefCell;
11use std::cell::RefCell;
10−use std::cmp;
11use std::fmt;
12use std::fmt;
13+use std::future::Future;
12use std::mem;
14use std::mem;
13use std::mem::ManuallyDrop;
15use std::mem::ManuallyDrop;
14use std::rc::{Rc, Weak};
16use std::rc::{Rc, Weak};
@@ -22,6 +24,7 @@ use wasm_bindgen_futures::JsFuture;
22/// removed. To keep it mounted forever, use the `Vdom::forget` method.
24/// removed. To keep it mounted forever, use the `Vdom::forget` method.
23#[must_use = "A `Vdom` will only keep rendering and listening to events while it has not been \
25#[must_use = "A `Vdom` will only keep rendering and listening to events while it has not been \
24 dropped. If you want a `Vdom` to run forever, call `Vdom::forget`."]
26 dropped. If you want a `Vdom` to run forever, call `Vdom::forget`."]
27+#[derive(Debug)]
25pub struct Vdom {
28pub struct Vdom {
26 inner: Rc<VdomInner>,
29 inner: Rc<VdomInner>,
27}
30}
@@ -52,12 +55,15 @@ pub(crate) struct VdomInnerExclusive {
52 // Always `Some` except just before we drop. Just an option so that
55 // Always `Some` except just before we drop. Just an option so that
53 // `unmount` can take the component out but we can still have a Drop
56 // `unmount` can take the component out but we can still have a Drop
54 // implementation.
57 // implementation.
55− component: Option<Box<RootRender>>,
58+ component: Option<Box<dyn RootRender>>,
56
59
57− dom_buffers: [Bump; 2],
60+ dom_buffers: Option<[Bump; 2]>,
58− change_list: ManuallyDrop<ChangeList>,
61+ change_list: ManuallyDrop<ChangeListPersistentState>,
59− container: web_sys::Element,
62+ container: crate::Element,
60 events_registry: Option<Rc<RefCell<EventsRegistry>>>,
63 events_registry: Option<Rc<RefCell<EventsRegistry>>>,
64+ events_trampoline: Option<crate::EventsTrampoline>,
65+ cached_set: crate::RefCell<CachedSet>,
66+ templates: FxHashMap<TemplateId, Option<CacheId>>,
61
67
62 // Actually a reference into `self.dom_buffers[0]` or if `self.component` is
68 // Actually a reference into `self.dom_buffers[0]` or if `self.component` is
63 // caching renders, into `self.component`'s bump.
69 // caching renders, into `self.component`'s bump.
@@ -90,6 +96,7 @@ impl fmt::Debug for VdomInnerExclusive {
90 .field("change_list", &self.change_list)
96 .field("change_list", &self.change_list)
91 .field("container", &self.container)
97 .field("container", &self.container)
92 .field("events_registry", &self.events_registry)
98 .field("events_registry", &self.events_registry)
99+ .field("events_trampoline", &"..")
93 .field("current_root", &self.current_root)
100 .field("current_root", &self.current_root)
94 .finish()
101 .finish()
95 }
102 }
@@ -109,7 +116,36 @@ impl Drop for VdomInnerExclusive {
109 let mut registry = registry.borrow_mut();
116 let mut registry = registry.borrow_mut();
110 registry.clear_active_listeners();
117 registry.clear_active_listeners();
111
118
119+ empty_container(&self.container);
120+ }
121+}
122+
123+cfg_if::cfg_if! {
124+ if #[cfg(all(feature = "xxx-unstable-internal-use-only", not(target_arch = "wasm32")))] {
125+ fn empty_container(_container: &crate::Element) {}
126+ fn initialize_container(_container: &crate::Element) {}
127+ } else {
128+ fn empty_container(container: &crate::Element) {
112− self.container.set_inner_html("");
129+ container.set_inner_html("");
130+ }
131+
132+ fn initialize_container(container: &crate::Element) {
133+ empty_container(container);
134+
135+ // Create the dummy `<div/>` child in the container.
136+ let window = web_sys::window().expect_throw("should have access to the Window");
137+ let document = window
138+ .document()
139+ .expect("should have access to the Document");
140+ container
141+ .append_child(
142+ document
143+ .create_element("div")
144+ .expect("should create element OK")
145+ .as_ref(),
146+ )
147+ .expect("should append child OK");
148+ }
113 }
149 }
114}
150}
115
151
@@ -118,40 +154,29 @@ impl Vdom {
118 /// rendering component.
154 /// rendering component.
119 ///
155 ///
120 /// This will box the given component into trait object.
156 /// This will box the given component into trait object.
121− pub fn new<R>(container: &web_sys::Element, component: R) -> Vdom
157+ pub fn new<R>(container: &crate::Element, component: R) -> Vdom
122 where
158 where
123 R: RootRender,
159 R: RootRender,
124 {
160 {
125− Self::with_boxed_root_render(container, Box::new(component) as Box<RootRender>)
161+ Self::with_boxed_root_render(container, Box::new(component) as Box<dyn RootRender>)
126 }
162 }
127
163
128 /// Construct a `Vdom` with the already-boxed-as-a-trait-object root
164 /// Construct a `Vdom` with the already-boxed-as-a-trait-object root
129 /// rendering component.
165 /// rendering component.
130 pub fn with_boxed_root_render(
166 pub fn with_boxed_root_render(
131− container: &web_sys::Element,
167+ container: &crate::Element,
132− component: Box<RootRender>,
168+ component: Box<dyn RootRender>,
133 ) -> Vdom {
169 ) -> Vdom {
134− let dom_buffers = [Bump::new(), Bump::new()];
170+ crate::strace::init_strace();
135− let change_list = ManuallyDrop::new(ChangeList::new(container));
136
171
137− // Ensure that the container is empty.
172+ let dom_buffers = [Bump::new(), Bump::new()];
138− container.set_inner_html("");
173+ let change_list = ManuallyDrop::new(ChangeListPersistentState::new(container));
139
174
140 // Create a dummy `<div/>` in our container.
175 // Create a dummy `<div/>` in our container.
176+ initialize_container(container);
177+ let current_root =
141− let current_root = Node::element(&dom_buffers[0], "div", [], [], []);
178+ Node::element(&dom_buffers[0], NodeKey::NONE, "div", &[], &[], &[], None);
142 let current_root = Some(unsafe { extend_node_lifetime(current_root) });
179 let current_root = Some(unsafe { extend_node_lifetime(current_root) });
143− let window = web_sys::window().expect("should have acess to the Window");
144− let document = window
145− .document()
146− .expect("should have access to the Document");
147− container
148− .append_child(
149− document
150− .create_element("div")
151− .expect("should create element OK")
152− .as_ref(),
153− )
154− .expect("should append child OK");
155
180
156 let container = container.clone();
181 let container = container.clone();
157 let inner = Rc::new(VdomInner {
182 let inner = Rc::new(VdomInner {
@@ -160,11 +185,14 @@ impl Vdom {
160 },
185 },
161 exclusive: RefCell::new(VdomInnerExclusive {
186 exclusive: RefCell::new(VdomInnerExclusive {
162 component: Some(component),
187 component: Some(component),
163− dom_buffers,
188+ dom_buffers: Some(dom_buffers),
164 change_list,
189 change_list,
165 container,
190 container,
166 current_root,
191 current_root,
167 events_registry: None,
192 events_registry: None,
193+ events_trampoline: None,
194+ cached_set: crate::RefCell::new(Default::default()),
195+ templates: Default::default(),
168 }),
196 }),
169 });
197 });
170
198
@@ -173,7 +201,9 @@ impl Vdom {
173 {
201 {
174 let mut inner = inner.exclusive.borrow_mut();
202 let mut inner = inner.exclusive.borrow_mut();
175 inner.events_registry = Some(events_registry);
203 inner.events_registry = Some(events_registry);
176− inner.change_list.init_events_trampoline(events_trampoline);
204+ inner.change_list.init_events_trampoline(&events_trampoline);
205+ debug_assert!(inner.events_trampoline.is_none());
206+ inner.events_trampoline = Some(events_trampoline);
177
207
178 // Diff and apply the `contents` against our dummy `<div/>`.
208 // Diff and apply the `contents` against our dummy `<div/>`.
179 inner.render();
209 inner.render();
@@ -182,6 +212,19 @@ impl Vdom {
182 Vdom { inner }
212 Vdom { inner }
183 }
213 }
184
214
215+ /// Immediately re-render and diff. Only for internal testing and
216+ /// benchmarking purposes.
217+ #[cfg(feature = "xxx-unstable-internal-use-only")]
218+ pub fn immediately_render_and_diff<R>(&self, component: R)
219+ where
220+ R: RootRender,
221+ {
222+ let mut exclusive = self.inner.exclusive.borrow_mut();
223+ let component = Box::new(component) as Box<dyn RootRender>;
224+ exclusive.component = Some(component);
225+ exclusive.render();
226+ }
227+
185 /// Run this virtual DOM and its listeners forever and never unmount it.
228 /// Run this virtual DOM and its listeners forever and never unmount it.
186 #[inline]
229 #[inline]
187 pub fn forget(self) {
230 pub fn forget(self) {
@@ -197,7 +240,7 @@ impl Vdom {
197 /// Unmount this virtual DOM, unregister its event listeners, and return its
240 /// Unmount this virtual DOM, unregister its event listeners, and return its
198 /// root render component.
241 /// root render component.
199 #[inline]
242 #[inline]
200− pub fn unmount(self) -> Box<RootRender> {
243+ pub fn unmount(self) -> Box<dyn RootRender> {
201 Rc::try_unwrap(self.inner.clone())
244 Rc::try_unwrap(self.inner.clone())
202 .map_err(|_| ())
245 .map_err(|_| ())
203 .unwrap_throw()
246 .unwrap_throw()
@@ -221,256 +264,69 @@ impl VdomInnerExclusive {
221 pub(crate) fn render(&mut self) {
264 pub(crate) fn render(&mut self) {
222 unsafe {
265 unsafe {
223 let events_registry = self.events_registry.take().unwrap();
266 let events_registry = self.events_registry.take().unwrap();
224−
225 {
267 {
226− // All the old listeners are no longer active. We will build a new
227− // set of active listeners when diffing.
228− //
229− // NB: if we end up avoiding diffing cached renders (instead of just
230− // avoiding re-rendering them) then we will need to maintain cached
231− // active listeners, and can't just clear all active listeners and
232− // rebuild them here.
233 let mut registry = events_registry.borrow_mut();
268 let mut registry = events_registry.borrow_mut();
234− registry.clear_active_listeners();
235
269
236 // Reset the inactive bump arena's pointer.
270 // Reset the inactive bump arena's pointer.
237− self.dom_buffers[1].reset();
271+ let mut dom_buffers = self.dom_buffers.take().unwrap_throw();
272+ dom_buffers[1].reset();
238
273
239 // Render the new current contents into the inactive bump arena.
274 // Render the new current contents into the inactive bump arena.
275+ let mut cx =
276+ RenderContext::new(&dom_buffers[1], &self.cached_set, &mut self.templates);
240− let new_contents = self
277+ let new_contents = self.component.as_ref().unwrap_throw().render(&mut cx);
241− .component
242− .as_ref()
243− .unwrap_throw()
244− .render(&self.dom_buffers[1]);
245 let new_contents = extend_node_lifetime(new_contents);
278 let new_contents = extend_node_lifetime(new_contents);
246
279
247 // Diff the old contents with the new contents.
280 // Diff the old contents with the new contents.
248 let old_contents = self.current_root.take().unwrap();
281 let old_contents = self.current_root.take().unwrap();
282+ let mut cache_roots;
283+ {
284+ let cached_set = self.cached_set.borrow();
285+ cache_roots = cached_set.new_roots_set();
286+ let mut change_list = self.change_list.builder();
287+ crate::diff::diff(
288+ &cached_set,
289+ &mut change_list,
290+ &mut registry,
249− self.diff(&mut registry, old_contents, new_contents.clone());
291+ &old_contents,
292+ &new_contents,
293+ &mut cache_roots,
294+ );
295+
296+ // Tell JS to apply our diff-generated changes to the physical DOM!
297+ change_list.finish();
298+ }
299+
300+ {
301+ // Clean up unused cached renders.
302+ let mut cached_set = self.cached_set.borrow_mut();
303+ cached_set.gc(&mut registry, cache_roots);
304+ }
250
305
251 // Swap the buffers to make the bump arena with the new contents the
306 // Swap the buffers to make the bump arena with the new contents the
252 // active arena, and the old one into the inactive arena.
307 // active arena, and the old one into the inactive arena.
253− self.swap_buffers();
308+ self.swap_buffers(dom_buffers);
254 self.set_current_root(new_contents);
309 self.set_current_root(new_contents);
255 }
310 }
256
311
257 self.events_registry = Some(events_registry);
312 self.events_registry = Some(events_registry);
258−
259− // Tell JS to apply our diff-generated changes to the physical DOM!
260− self.change_list.apply_changes();
261 }
313 }
262 }
314 }
263
315
264− fn swap_buffers(&mut self) {
316+ fn swap_buffers(&mut self, mut dom_buffers: [Bump; 2]) {
317+ debug_assert!(self.dom_buffers.is_none());
265− let (first, second) = self.dom_buffers.as_mut().split_at_mut(1);
318+ let (first, second) = dom_buffers.as_mut().split_at_mut(1);
266 mem::swap(&mut first[0], &mut second[0]);
319 mem::swap(&mut first[0], &mut second[0]);
320+ self.dom_buffers = Some(dom_buffers);
267 }
321 }
268
322
269 unsafe fn set_current_root(&mut self, current: Node<'static>) {
323 unsafe fn set_current_root(&mut self, current: Node<'static>) {
270 debug_assert!(self.current_root.is_none());
324 debug_assert!(self.current_root.is_none());
271 self.current_root = Some(current);
325 self.current_root = Some(current);
272 }
326 }
273−
274− fn diff<'a>(&self, registry: &mut EventsRegistry, old: Node<'a>, new: Node<'a>) {
275− // debug!("---------------------------------------------------------");
276− // debug!("dodrio::Vdom::diff");
277− // debug!(" old = {:#?}", old);
278− // debug!(" new = {:#?}", new);
279− match (&new, old) {
280− (&Node::Text(TextNode { text: new_text }), Node::Text(TextNode { text: old_text })) => {
281− debug!(" both are text nodes");
282− if new_text != old_text {
283− debug!(" text needs updating");
284− self.change_list.emit_set_text(new_text);
285− }
286− }
287− (&Node::Text(TextNode { .. }), Node::Element(ElementNode { .. })) => {
288− debug!(" replacing a text node with an element");
289− self.create(registry, new);
290− self.change_list.emit_replace_with();
291− }
292− (&Node::Element(ElementNode { .. }), Node::Text(TextNode { .. })) => {
293− debug!(" replacing an element with a text node");
294− self.create(registry, new);
295− self.change_list.emit_replace_with();
296− }
297− (
298− &Node::Element(ElementNode {
299− tag_name: new_tag_name,
300− listeners: new_listeners,
301− attributes: new_attributes,
302− children: new_children,
303− }),
304− Node::Element(ElementNode {
305− tag_name: old_tag_name,
306− listeners: old_listeners,
307− attributes: old_attributes,
308− children: old_children,
309− }),
310− ) => {
311− debug!(" updating an element");
312− if new_tag_name != old_tag_name {
313− debug!(" different tag names; creating new element and replacing old element");
314− self.create(registry, new);
315− self.change_list.emit_replace_with();
316− return;
317− }
318− self.diff_listeners(registry, old_listeners, new_listeners);
319− self.diff_attributes(old_attributes, new_attributes);
320− self.diff_children(registry, old_children, new_children);
321− }
322− }
323− }
324−
325− fn diff_listeners<'a>(
326− &self,
327− registry: &mut EventsRegistry,
328− old: &'a [Listener<'a>],
329− new: &'a [Listener<'a>],
330− ) {
331− debug!(" updating event listeners");
332−
333− 'outer1: for new_l in new {
334− unsafe {
335− registry.add(new_l);
336− }
337− for old_l in old {
338− if new_l.event == old_l.event {
339− self.change_list.emit_update_event_listener(new_l);
340− continue 'outer1;
341− }
342− }
343− self.change_list.emit_new_event_listener(new_l);
344− }
345−
346− 'outer2: for old_l in old {
347− for new_l in new {
348− if new_l.event == old_l.event {
349− continue 'outer2;
350− }
351− }
352− self.change_list.emit_remove_event_listener(old_l.event);
353− }
354− }
355−
356− fn diff_attributes(&self, old: &[Attribute], new: &[Attribute]) {
357− debug!(" updating attributes");
358−
359− // Do O(n^2) passes to add/update and remove attributes, since
360− // there are almost always very few attributes.
361− 'outer: for new_attr in new {
362− for old_attr in old {
363− if old_attr.name == new_attr.name {
364− if old_attr.value != new_attr.value {
365− self.change_list
366− .emit_set_attribute(new_attr.name, new_attr.value);
367− }
368− continue 'outer;
369− }
370− }
371− self.change_list
372− .emit_set_attribute(new_attr.name, new_attr.value);
373− }
374−
375− 'outer2: for old_attr in old {
376− for new_attr in new {
377− if old_attr.name == new_attr.name {
378− continue 'outer2;
379− }
380− }
381− self.change_list.emit_remove_attribute(old_attr.name);
382− }
383− }
384−
385− fn diff_children<'a>(
386− &self,
387− registry: &mut EventsRegistry,
388− old: &'a [Node<'a>],
389− new: &'a [Node<'a>],
390− ) {
391− debug!(" updating children shared by old and new");
392−
393− let num_children_to_diff = cmp::min(new.len(), old.len());
394− let mut new_children = new.iter();
395− let mut old_children = old.iter();
396− let mut pushed = false;
397−
398− for (i, (new_child, old_child)) in new_children
399− .by_ref()
400− .zip(old_children.by_ref())
401− .take(num_children_to_diff)
402− .enumerate()
403− {
404− if i == 0 {
405− self.change_list.emit_push_first_child();
406− pushed = true;
407− } else {
408− debug_assert!(pushed);
409− self.change_list.emit_pop_push_next_sibling();
410− }
411−
412− self.diff(registry, old_child.clone(), new_child.clone());
413− }
414−
415− if old_children.next().is_some() {
416− debug!(" removing extra old children");
417− debug_assert!(new_children.next().is_none());
418− if !pushed {
419− self.change_list.emit_push_first_child();
420− } else {
421− self.change_list.emit_pop_push_next_sibling();
422− }
423− self.change_list.emit_remove_self_and_next_siblings();
424− pushed = false;
425− } else {
426− debug!(" creating new children");
427− for (i, new_child) in new_children.enumerate() {
428− if i == 0 && pushed {
429− self.change_list.emit_pop();
430− pushed = false;
431− }
432− self.create(registry, new_child.clone());
433− self.change_list.emit_append_child();
434− }
435− }
436−
437− debug!(" done updating children");
438− if pushed {
439− self.change_list.emit_pop();
440− }
441− }
442−
443− fn create<'a>(&self, registry: &mut EventsRegistry, node: Node<'a>) {
444− match node {
445− Node::Text(TextNode { text }) => {
446− self.change_list.emit_create_text_node(text);
447− }
448− Node::Element(ElementNode {
449− tag_name,
450− listeners,
451− attributes,
452− children,
453− }) => {
454− self.change_list.emit_create_element(tag_name);
455− for l in listeners {
456− unsafe {
457− registry.add(l);
458− }
459− self.change_list.emit_new_event_listener(l);
460− }
461− for attr in attributes {
462− self.change_list.emit_set_attribute(&attr.name, &attr.value);
463− }
464− for child in children {
465− self.create(registry, child.clone());
466− self.change_list.emit_append_child();
467− }
468− }
469− }
470− }
471}
327}
472
328
473−fn request_animation_frame(f: &Closure<FnMut()>) {
329+fn request_animation_frame(f: &Closure<dyn FnMut()>) {
474 web_sys::window()
330 web_sys::window()
475 .expect_throw("should have a window")
331 .expect_throw("should have a window")
476 .request_animation_frame(f.as_ref().unchecked_ref())
332 .request_animation_frame(f.as_ref().unchecked_ref())
@@ -487,24 +343,12 @@ where
487 let f = Closure::wrap(Box::new(move || {
343 let f = Closure::wrap(Box::new(move || {
488 *g.borrow_mut() = None;
344 *g.borrow_mut() = None;
489 f();
345 f();
490− }) as Box<FnMut()>);
346+ }) as Box<dyn FnMut()>);
491 request_animation_frame(&f);
347 request_animation_frame(&f);
492
348
493 *h.borrow_mut() = Some(f);
349 *h.borrow_mut() = Some(f);
494}
350}
495
351
496−trait CellOptionExt<T> {
497− fn take(&self) -> Option<T>;
498−}
499−
500−impl<T> CellOptionExt<T> for Cell<Option<T>> {
501− fn take(&self) -> Option<T> {
502− let temp = Cell::new(None);
503− self.swap(&temp);
504− temp.into_inner()
505− }
506−}
507−
508/// An operation failed because the virtual DOM was already dropped and
352/// An operation failed because the virtual DOM was already dropped and
509/// unmounted.
353/// unmounted.
510#[derive(Debug)]
354#[derive(Debug)]
@@ -527,26 +371,26 @@ impl VdomWeak {
527 }
371 }
528 }
372 }
529
373
530− /// TODO
374+ /// Replace the root rendering component with the new `root`.
375+ ///
376+ /// Returns a future that resolves to the *old* root component.
531− pub fn set_component(
377+ pub async fn set_component(
532 self,
378 self,
533 root: Box<dyn RootRender>,
379 root: Box<dyn RootRender>,
534− ) -> impl Future<Item = Box<dyn RootRender + 'static>, Error = VdomDroppedError> {
380+ ) -> Result<Box<dyn RootRender + 'static>, VdomDroppedError> {
535− futures::future::ok(self.inner.upgrade())
536− .and_then(|inner| inner.ok_or(()))
537− .map_err(|_| VdomDroppedError {})
381+ let inner = self.inner.upgrade().ok_or(VdomDroppedError {})?;
538− .and_then(|inner| {
382+
383+ // Wait for a new tick of the micro-task queue
539− let promise = js_sys::Promise::resolve(&JsValue::null());
384+ let _ = JsFuture::from(js_sys::Promise::resolve(&JsValue::null())).await;
540− JsFuture::from(promise)
385+
541− .map_err(|_| VdomDroppedError {})
542− .and_then(move |_| {
543− let old = {
386+ let old = {
544− let mut exclusive = inner.exclusive.borrow_mut();
387+ let mut exclusive = inner.exclusive.borrow_mut();
545− mem::replace(&mut *exclusive.component.as_mut().unwrap_throw(), root)
388+ mem::replace(&mut *exclusive.component.as_mut().unwrap_throw(), root)
546− };
389+ };
390+
547− VdomWeak::new(&inner).render().map(|_| old)
391+ VdomWeak::new(&inner).render().await?;
548− })
392+
549− })
393+ Ok(old)
550 }
394 }
551
395
552 /// Execute `f` with a reference to this virtual DOM's root rendering
396 /// Execute `f` with a reference to this virtual DOM's root rendering
@@ -554,24 +398,18 @@ impl VdomWeak {
554 ///
398 ///
555 /// To ensure exclusive access to the root rendering component, the
399 /// To ensure exclusive access to the root rendering component, the
556 /// invocation takes place on a new tick of the micro-task queue.
400 /// invocation takes place on a new tick of the micro-task queue.
557− pub fn with_component<F, T>(&self, f: F) -> impl Future<Item = T, Error = VdomDroppedError>
401+ pub async fn with_component<F, T>(&self, f: F) -> Result<T, VdomDroppedError>
558 where
402 where
559 F: 'static + FnOnce(&mut dyn RootRender) -> T,
403 F: 'static + FnOnce(&mut dyn RootRender) -> T,
560 {
404 {
561− futures::future::ok(self.inner.upgrade())
562− .and_then(|inner| inner.ok_or(()))
563− .map_err(|_| VdomDroppedError {})
405+ let inner = self.inner.upgrade().ok_or(VdomDroppedError {})?;
564− .and_then(|inner| {
406+
565− let mut f = Some(f);
407+ // Wait for a new tick of the micro-task queue
566− let promise = js_sys::Promise::resolve(&JsValue::null());
408+ let _ = JsFuture::from(js_sys::Promise::resolve(&JsValue::null())).await;
567− JsFuture::from(promise)
409+
568− .map_err(|_| VdomDroppedError {})
569− .map(move |_| {
570− let f = f.take().unwrap_throw();
571− let mut exclusive = inner.exclusive.borrow_mut();
410+ let mut exclusive = inner.exclusive.borrow_mut();
411+
572− f(exclusive.component_raw_mut())
412+ Ok(f(exclusive.component_raw_mut()))
573− })
574− })
575 }
413 }
576
414
577 /// Schedule a render to occur during the next animation frame.
415 /// Schedule a render to occur during the next animation frame.
@@ -580,7 +418,12 @@ impl VdomWeak {
580 /// `render` instead.
418 /// `render` instead.
581 pub fn schedule_render(&self) {
419 pub fn schedule_render(&self) {
582 debug!("VdomWeak::schedule_render");
420 debug!("VdomWeak::schedule_render");
421+
422+ let future = self.render();
423+
583− wasm_bindgen_futures::spawn_local(self.render().map_err(|_| ()));
424+ wasm_bindgen_futures::spawn_local(async move {
425+ let _ = future.await;
426+ });
584 }
427 }
585
428
586 /// Schedule a render to occur during the next animation frame and return a
429 /// Schedule a render to occur during the next animation frame and return a
@@ -588,43 +431,46 @@ impl VdomWeak {
588 ///
431 ///
589 /// If you don't want to do more things after the render completes, then use
432 /// If you don't want to do more things after the render completes, then use
590 /// `schedule_render` instead of `render`.
433 /// `schedule_render` instead of `render`.
591− pub fn render(&self) -> impl Future<Item = (), Error = VdomDroppedError> {
434+ pub fn render(&self) -> impl Future<Output = Result<(), VdomDroppedError>> {
592− debug!("VdomWeak::render: initiating render in new animation frame");
593− futures::future::ok(self.inner.upgrade())
435+ let inner = self.inner.upgrade();
594− .and_then(|inner| inner.ok_or(()))
436+
437+ async move {
595− .map_err(|_| VdomDroppedError {})
438+ let inner = inner.ok_or(VdomDroppedError {})?;
596− .and_then(|inner| {
439+
597− let promise = inner.shared.render_scheduled.take().unwrap_or_else(|| {
440+ let promise = inner.shared.render_scheduled.take().unwrap_or_else(|| {
598− js_sys::Promise::new(&mut |resolve, reject| {
441+ js_sys::Promise::new(&mut |resolve, reject| {
599− let vdom = VdomWeak {
442+ let vdom = VdomWeak {
600− inner: Rc::downgrade(&inner),
443+ inner: Rc::downgrade(&inner),
601− };
444+ };
445+
602− with_animation_frame(move || match vdom.inner.upgrade() {
446+ with_animation_frame(move || match vdom.inner.upgrade() {
603− None => {
447+ None => {
604− warn!("VdomWeak::render: vdom unmounted before we could render");
448+ warn!("VdomWeak::render: vdom unmounted before we could render");
605− let r = reject.call0(&JsValue::null());
449+ let r = reject.call0(&JsValue::null());
606− debug_assert!(r.is_ok());
450+ debug_assert!(r.is_ok());
607− }
451+ }
608− Some(inner) => {
452+ Some(inner) => {
609− let mut exclusive = inner.exclusive.borrow_mut();
453+ let mut exclusive = inner.exclusive.borrow_mut();
610− exclusive.render();
454+ exclusive.render();
611−
455+
612− // We did the render, so take the promise away
456+ // We did the render, so take the promise away
613− // and let future `render` calls request new
457+ // and let future `render` calls request new
614− // animation frames.
458+ // animation frames.
615− let _ = inner.shared.render_scheduled.take();
459+ let _ = inner.shared.render_scheduled.take();
616−
460+
617− debug!("VdomWeak::render: finished rendering");
618− let r = resolve.call0(&JsValue::null());
461+ let r = resolve.call0(&JsValue::null());
619− debug_assert!(r.is_ok());
462+ debug_assert!(r.is_ok());
620− }
463+ }
621− });
464+ });
622− })
465+ })
623− });
466+ });
467+
624− inner.shared.render_scheduled.set(Some(promise.clone()));
468+ inner.shared.render_scheduled.set(Some(promise.clone()));
469+
625− JsFuture::from(promise)
470+ JsFuture::from(promise)
471+ .await
626− .map(|_| ())
472+ .map(drop)
627− .map_err(|_| VdomDroppedError {})
473+ .map_err(|_| VdomDroppedError {})
628− })
474+ }
629 }
475 }
630}
476}
tests/web/cached.jsnew file+11
@@ -0,0 +1,11 @@
1+let cloneNodeCount = 0;
2+
3+const origCloneNode = Node.prototype.cloneNode;
4+Node.prototype.cloneNode = function (...args) {
5+ cloneNodeCount += 1;
6+ return origCloneNode.apply(this, args);
7+};
8+
9+export function getCloneNodeCount() {
10+ return cloneNodeCount;
11+}
tests/web/cached.rs+242−52
@@ -1,11 +1,11 @@
1−use super::{assert_rendered, create_element, RenderFn};
1+use super::{assert_rendered, before_after, create_element, RenderFn};
2−use dodrio::{bumpalo::Bump, Cached, Node, Render, Vdom};
2+use dodrio::{builder::*, bumpalo, Cached, Node, Render, RenderContext, RootRender, Vdom};
3−use futures::prelude::*;
4use std::cell::Cell;
3use std::cell::Cell;
5use std::rc::Rc;
4use std::rc::Rc;
6−use wasm_bindgen::*;
5+use wasm_bindgen::prelude::*;
7use wasm_bindgen_test::*;
6use wasm_bindgen_test::*;
8
7
8+#[derive(Default)]
9pub struct CountRenders {
9pub struct CountRenders {
10 render_count: Cell<usize>,
10 render_count: Cell<usize>,
11}
11}
@@ -18,21 +18,20 @@ impl CountRenders {
18 }
18 }
19}
19}
20
20
21−impl Render for CountRenders {
21+impl<'a> Render<'a> for CountRenders {
22− fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
22+ fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
23− where
24− 'a: 'bump,
25− {
26 let count = self.render_count.get() + 1;
23 let count = self.render_count.get() + 1;
27 self.render_count.set(count);
24 self.render_count.set(count);
28
25
29− let s = bumpalo::format!(in bump, "{}", count);
26+ let s = bumpalo::format!(in cx.bump, "{}", count);
30− Node::text(s.into_bump_str())
27+ text(s.into_bump_str())
31 }
28 }
32}
29}
33
30
34−#[wasm_bindgen_test(async)]
31+#[wasm_bindgen_test]
35−fn uses_cached_render() -> impl Future<Item = (), Error = JsValue> {
32+async fn uses_cached_render() {
33+ use dodrio::builder::*;
34+
36 let cached = Cached::new(CountRenders::new());
35 let cached = Cached::new(CountRenders::new());
37
36
38 let container0 = create_element("div");
37 let container0 = create_element("div");
@@ -50,50 +49,241 @@ fn uses_cached_render() -> impl Future<Item = (), Error = JsValue> {
50 let vdom6 = vdom0.clone();
49 let vdom6 = vdom0.clone();
51 let vdom7 = vdom0.clone();
50 let vdom7 = vdom0.clone();
52
51
52+ // We render, populate the cache, and get "1".
53+ vdom0.weak().render().await.unwrap();
53 vdom0
54 vdom0
54 .weak()
55 .weak()
55− // We render, populate the cache, and get "1".
56− .render()
57− .and_then(move |_| {
58− vdom0.weak().with_component(move |comp| {
56+ .with_component(move |comp| {
59− let comp = comp.unwrap_mut::<Cached<CountRenders>>();
57+ let comp = comp.unwrap_mut::<Cached<CountRenders>>();
60− assert_eq!(comp.render_count.get(), 1);
58+ assert_eq!(comp.render_count.get(), 1);
61− assert_rendered(&container1, &RenderFn(|_| Node::text("1")));
59+ assert_rendered(&container1, &RenderFn(|_| text("1")));
62− })
63 })
60 })
61+ .await
62+ .unwrap();
63+
64− // We re-render, re-use the cached node, and get "1" again.
64+ // We re-render, re-use the cached node, and get "1" again.
65− .and_then(move |_| vdom1.weak().render())
65+ vdom1.weak().render().await.unwrap();
66− .and_then(move |_| {
66+ vdom2
67+ .weak()
67− vdom2.weak().with_component(move |comp| {
68+ .with_component(move |comp| {
68− let comp = comp.unwrap_mut::<Cached<CountRenders>>();
69+ let comp = comp.unwrap_mut::<Cached<CountRenders>>();
69− assert_eq!(comp.render_count.get(), 1);
70+ assert_eq!(comp.render_count.get(), 1);
70− assert_rendered(&container2, &RenderFn(|_| Node::text("1")));
71+ assert_rendered(&container2, &RenderFn(|_| text("1")));
71− })
72 })
72 })
73+ .await
74+ .unwrap();
75+
73− // We invalidate the cache, re-render, re-populate the cache, and should
76+ // We invalidate the cache, re-render, re-populate the cache, and should now have "2".
74− // now have "2".
77+ vdom3
75− .and_then(move |_| {
78+ .weak()
76− vdom3.weak().with_component(move |comp| {
79+ .with_component(move |comp| {
77− let comp = comp.unwrap_mut::<Cached<CountRenders>>();
80+ let comp = comp.unwrap_mut::<Cached<CountRenders>>();
78− Cached::invalidate(comp);
81+ Cached::invalidate(comp);
79− })
80 })
82 })
83+ .await
84+ .unwrap();
81− .and_then(move |_| vdom4.weak().render())
85+ vdom4.weak().render().await.unwrap();
82− .and_then(move |_| {
86+ vdom5
87+ .weak()
83− vdom5.weak().with_component(move |comp| {
88+ .with_component(move |comp| {
84− let comp = comp.unwrap_mut::<Cached<CountRenders>>();
89+ let comp = comp.unwrap_mut::<Cached<CountRenders>>();
85− assert_eq!(comp.render_count.get(), 2);
90+ assert_eq!(comp.render_count.get(), 2);
86− assert_rendered(&container3, &RenderFn(|_| Node::text("2")));
91+ assert_rendered(&container3, &RenderFn(|_| text("2")));
87− })
88 })
92 })
93+ .await
94+ .unwrap();
95+
89− // We re-render, re-use the cached node, and get "2" again.
96+ // We re-render, re-use the cached node, and get "2" again.
90− .and_then(move |_| vdom6.weak().render())
97+ vdom6.weak().render().await.unwrap();
91− .and_then(move |_| {
98+ vdom7
99+ .weak()
92− vdom7.weak().with_component(move |comp| {
100+ .with_component(move |comp| {
93− let comp = comp.unwrap_mut::<Cached<CountRenders>>();
101+ let comp = comp.unwrap_mut::<Cached<CountRenders>>();
94− assert_eq!(comp.render_count.get(), 2);
102+ assert_eq!(comp.render_count.get(), 2);
95− assert_rendered(&container4, &RenderFn(|_| Node::text("2")));
103+ assert_rendered(&container4, &RenderFn(|_| text("2")));
96− })
97 })
104 })
98− .map_err(|e| JsValue::from(e.to_string()))
105+ .await
106+ .unwrap();
107+}
108+
109+#[wasm_bindgen(module = "/tests/web/cached.js")]
110+extern "C" {
111+ #[wasm_bindgen(js_name = getCloneNodeCount)]
112+ fn get_clone_node_count() -> u32;
113+}
114+
115+#[wasm_bindgen_test]
116+async fn creating_cached_nodes_uses_clone_node() {
117+ use dodrio::builder::*;
118+
119+ let container = create_element("div");
120+
121+ // The initial vdom render should populate the cache and save a template.
122+ let vdom0 = Rc::new(Vdom::new(&container, Cached::new(CountRenders::new())));
123+ let vdom1 = vdom0.clone();
124+ let _vdom2 = vdom0.clone();
125+
126+ let clone_node_count_before = get_clone_node_count();
127+
128+ // Render something entirely different...
129+ vdom0
130+ .weak()
131+ .set_component(Box::new(RenderFn(|_| text("hi"))) as Box<dyn RootRender>)
132+ .await
133+ .unwrap();
134+
135+ // Re-render our cached node. Since we are creating it from scratch,
136+ // and already have a template from the earlier render, we should
137+ // call `cloneNode` here.
138+ vdom1
139+ .weak()
140+ .set_component(Box::new(Cached::new(CountRenders::new())) as Box<dyn RootRender>)
141+ .await
142+ .unwrap();
143+
144+ let clone_node_count_after = get_clone_node_count();
145+ assert_eq!(clone_node_count_after, clone_node_count_before + 1);
146+}
147+
148+struct Id(&'static str);
149+
150+impl Default for Id {
151+ fn default() -> Id {
152+ Id("")
153+ }
154+}
155+
156+impl<'a> Render<'a> for Id {
157+ fn render(&self, _cx: &mut RenderContext<'a>) -> Node<'a> {
158+ text(self.0)
159+ }
160+}
161+
162+thread_local! {
163+ static WARM_CHEESE: Rc<Cached<Id>> = Rc::new(Cached::new(Id("cheese")));
164+ static WARM_CHEESIER: Rc<Cached<Id>> = Rc::new(Cached::new(Id("cheesier")));
165+}
166+
167+fn warm_cheese<'a>(cx: &mut RenderContext<'a>) -> Node<'a> {
168+ WARM_CHEESE.with(|c| {
169+ let _ = c.render(cx);
170+ c.render(cx)
171+ })
172+}
173+
174+fn warm_cheesier<'a>(cx: &mut RenderContext<'a>) -> Node<'a> {
175+ WARM_CHEESIER.with(|c| {
176+ let _ = c.render(cx);
177+ c.render(cx)
178+ })
179+}
180+
181+before_after! {
182+ cold_cache_and_not_cached {
183+ before(cx) {
184+ Cached::new(Id("cheese")).render(cx)
185+ }
186+ after(cx) {
187+ Id("ravioli").render(cx)
188+ }
189+ }
190+
191+ not_cached_and_cold_cache {
192+ before(cx) {
193+ Id("ravioli").render(cx)
194+ }
195+ after(cx) {
196+ Cached::new(Id("cheese")).render(cx)
197+ }
198+ }
199+
200+ cold_cache_and_cold_cache_same {
201+ before(cx) {
202+ Cached::new(Id("cheese")).render(cx)
203+ }
204+ after(cx) {
205+ Cached::new(Id("cheese")).render(cx)
206+ }
207+ }
208+
209+ cold_cache_and_cold_cache_different {
210+ before(cx) {
211+ Cached::new(Id("cheese")).render(cx)
212+ }
213+ after(cx) {
214+ Cached::new(Id("ravioli")).render(cx)
215+ }
216+ }
217+
218+ warm_cache_and_not_cached {
219+ before(cx) {
220+ warm_cheese(cx)
221+ }
222+ after(cx) {
223+ Id("ravioli").render(cx)
224+ }
225+ }
226+
227+ not_cached_and_warm_cache {
228+ before(cx) {
229+ Id("ravioli").render(cx)
230+ }
231+ after(cx) {
232+ warm_cheese(cx)
233+ }
234+ }
235+
236+ warm_cache_and_warm_cache_same {
237+ before(cx) {
238+ warm_cheese(cx)
239+ }
240+ after(cx) {
241+ warm_cheese(cx)
242+ }
243+ }
244+
245+ warm_cache_and_warm_cache_different {
246+ before(cx) {
247+ warm_cheese(cx)
248+ }
249+ after(cx) {
250+ warm_cheesier(cx)
251+ }
252+ }
253+
254+ cold_cache_and_warm_cache_same {
255+ before(cx) {
256+ Cached::new(Id("cheese")).render(cx)
257+ }
258+ after(cx) {
259+ warm_cheese(cx)
260+ }
261+ }
262+
263+ cold_cache_and_warm_cache_different {
264+ before(cx) {
265+ Cached::new(Id("cheese")).render(cx)
266+ }
267+ after(cx) {
268+ warm_cheesier(cx)
269+ }
270+ }
271+
272+ warm_cache_and_cold_cache_same {
273+ before(cx) {
274+ warm_cheese(cx)
275+ }
276+ after(cx) {
277+ Cached::new(Id("cheese")).render(cx)
278+ }
279+ }
280+
281+ warm_cache_and_cold_cache_different {
282+ before(cx) {
283+ warm_cheesier(cx)
284+ }
285+ after(cx) {
286+ Cached::new(Id("cheese")).render(cx)
287+ }
288+ }
99}
289}
tests/web/events.rs+70−191
@@ -1,7 +1,6 @@
1use super::create_element;
1use super::create_element;
2−use bumpalo::Bump;
3−use dodrio::{on, Attribute, Node, Render, Vdom};
2+use dodrio::{Node, Render, RenderContext, Vdom};
4−use futures::Future;
3+use futures::future::{select, Either};
5use std::cell::{Cell, RefCell};
4use std::cell::{Cell, RefCell};
6use std::rc::Rc;
5use std::rc::Rc;
7use wasm_bindgen::prelude::*;
6use wasm_bindgen::prelude::*;
@@ -10,7 +9,7 @@ use wasm_bindgen_test::*;
10
9
11struct EventContainer {
10struct EventContainer {
12 event: &'static str,
11 event: &'static str,
13− on_event: Box<FnMut()>,
12+ on_event: Box<dyn FnMut()>,
14}
13}
15
14
16impl EventContainer {
15impl EventContainer {
@@ -25,23 +24,15 @@ impl EventContainer {
25 }
24 }
26}
25}
27
26
28−impl Render for EventContainer {
27+impl<'a> Render<'a> for EventContainer {
29− fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
28+ fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
30− where
29+ use dodrio::builder::*;
31− 'a: 'bump,
32− {
33− Node::element(
34− bump,
35− "div",
30+ div(&cx)
31+ .attr("id", "target")
36− [on(bump, self.event, |root, _scheduler, _event| {
32+ .on(self.event, |root, _scheduler, _event| {
37 (root.unwrap_mut::<EventContainer>().on_event)();
33 (root.unwrap_mut::<EventContainer>().on_event)();
38− })],
34+ })
39− [Attribute {
35+ .finish()
40− name: "id",
41− value: "target",
42− }],
43− [],
44− )
45 }
36 }
46}
37}
47
38
@@ -53,14 +44,14 @@ fn target(container: &web_sys::Element) -> web_sys::HtmlElement {
53 .unchecked_into()
44 .unchecked_into()
54}
45}
55
46
56−#[wasm_bindgen_test(async)]
47+#[wasm_bindgen_test]
57−fn click() -> impl Future<Item = (), Error = JsValue> {
48+async fn click() {
58 let container = create_element("div");
49 let container = create_element("div");
59
50
60− let (sender, receiver) = futures::sync::oneshot::channel();
51+ let (sender, receiver) = futures::channel::oneshot::channel();
61 let mut sender = Some(sender);
52 let mut sender = Some(sender);
62
53
63− let vdom = Vdom::new(
54+ let _vdom = Vdom::new(
64 &container,
55 &container,
65 EventContainer::new("click", move || {
56 EventContainer::new("click", move || {
66 sender
57 sender
@@ -73,18 +64,14 @@ fn click() -> impl Future<Item = (), Error = JsValue> {
73
64
74 target(&container).click();
65 target(&container).click();
75
66
76− receiver
67+ receiver.await.unwrap();
77− .map(|_| {
78− drop(vdom);
79− })
80− .map_err(|e| JsValue::from(e.to_string()))
81}
68}
82
69
83−#[wasm_bindgen_test(async)]
70+#[wasm_bindgen_test]
84−fn updated_listener_is_called() -> impl Future<Item = (), Error = JsValue> {
71+async fn updated_listener_is_called() {
85 let container = create_element("div");
72 let container = create_element("div");
86
73
87− let (first_sender, first_receiver) = futures::sync::oneshot::channel();
74+ let (first_sender, first_receiver) = futures::channel::oneshot::channel();
88 let mut first_sender = Some(first_sender);
75 let mut first_sender = Some(first_sender);
89
76
90 let vdom = Vdom::new(
77 let vdom = Vdom::new(
@@ -98,7 +85,7 @@ fn updated_listener_is_called() -> impl Future<Item = (), Error = JsValue> {
98 }),
85 }),
99 );
86 );
100
87
101− let (second_sender, second_receiver) = futures::sync::oneshot::channel();
88+ let (second_sender, second_receiver) = futures::channel::oneshot::channel();
102 let mut second_sender = Some(second_sender);
89 let mut second_sender = Some(second_sender);
103
90
104 vdom.weak()
91 vdom.weak()
@@ -109,25 +96,22 @@ fn updated_listener_is_called() -> impl Future<Item = (), Error = JsValue> {
109 .send("second")
96 .send("second")
110 .expect_throw("should not have dropped the receiver");
97 .expect_throw("should not have dropped the receiver");
111 })))
98 })))
112− .map_err(|e| JsValue::from(e.to_string()))
99+ .await
113− .and_then(move |_| {
100+ .unwrap();
101+
114− target(&container).click();
102+ target(&container).click();
115−
103+
116− first_receiver
117− .select(second_receiver)
104+ match select(first_receiver, second_receiver).await {
118− .map(|(which, _other)| which)
105+ Either::Left((Ok(_), _)) => panic!(),
119− .or_else(|(_error, other)| other)
120− .map(|which| {
121− assert_eq!(which, "second");
106+ Either::Left((Err(_), second)) => assert_eq!(second.await, Ok("second")),
122− drop(vdom);
107+ Either::Right((Ok(msg), _)) => assert_eq!(msg, "second"),
123− })
108+ Either::Right((Err(_), _)) => panic!(),
124− .map_err(|e| JsValue::from(e.to_string()))
125− })
109+ }
126}
110}
127
111
128struct ListensOnlyOnFirstRender {
112struct ListensOnlyOnFirstRender {
129 count: Cell<usize>,
113 count: Cell<usize>,
130− callback: Box<FnMut()>,
114+ callback: Box<dyn FnMut()>,
131}
115}
132
116
133impl ListensOnlyOnFirstRender {
117impl ListensOnlyOnFirstRender {
@@ -142,46 +126,28 @@ impl ListensOnlyOnFirstRender {
142 }
126 }
143}
127}
144
128
145−impl Render for ListensOnlyOnFirstRender {
129+impl<'a> Render<'a> for ListensOnlyOnFirstRender {
146− fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
130+ fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
147− where
131+ use dodrio::builder::*;
148− 'a: 'bump,
132+
149− {
150 let count = self.count.get();
133 let count = self.count.get();
151 self.count.set(count + 1);
134 self.count.set(count + 1);
135+
136+ let mut elem = div(&cx).attr("id", "target");
152 if count == 0 {
137 if count == 0 {
153− Node::element(
154− bump,
155− "div",
156− [on(bump, "click", |root, _scheduler, _event| {
138+ elem = elem.on("click", |root, _scheduler, _event| {
157− (root.unwrap_mut::<ListensOnlyOnFirstRender>().callback)();
139+ (root.unwrap_mut::<ListensOnlyOnFirstRender>().callback)();
158− })],
159− [Attribute {
160− name: "id",
161− value: "target",
162− }],
163− [],
164− )
165− } else {
166− Node::element(
167− bump,
168− "div",
169− [],
170− [Attribute {
171− name: "id",
172− value: "target",
173− }],
174− [],
175− )
140+ });
176 }
141 }
142+ elem.finish()
177 }
143 }
178}
144}
179
145
180−#[wasm_bindgen_test(async)]
146+#[wasm_bindgen_test]
181−fn removed_listener_is_not_called() -> impl Future<Item = (), Error = JsValue> {
147+async fn removed_listener_is_not_called() {
182 let container = create_element("div");
148 let container = create_element("div");
183
149
184− let (outer_sender, outer_receiver) = futures::sync::oneshot::channel();
150+ let (outer_sender, outer_receiver) = futures::channel::oneshot::channel();
185 let outer_sender = Rc::new(RefCell::new(Some(outer_sender)));
151 let outer_sender = Rc::new(RefCell::new(Some(outer_sender)));
186 let outer_listener = Closure::wrap(Box::new(move |_| {
152 let outer_listener = Closure::wrap(Box::new(move |_| {
187 outer_sender
153 outer_sender
@@ -190,13 +156,13 @@ fn removed_listener_is_not_called() -> impl Future<Item = (), Error = JsValue> {
190 .expect_throw("should only invoke outer_listener once")
156 .expect_throw("should only invoke outer_listener once")
191 .send("outer")
157 .send("outer")
192 .expect_throw("should not have dropped receiver");
158 .expect_throw("should not have dropped receiver");
193− }) as Box<FnMut(web_sys::Event)>);
159+ }) as Box<dyn FnMut(web_sys::Event)>);
194
160
195 container
161 container
196 .add_event_listener_with_callback("click", outer_listener.as_ref().unchecked_ref())
162 .add_event_listener_with_callback("click", outer_listener.as_ref().unchecked_ref())
197 .unwrap();
163 .unwrap();
198
164
199− let (vdom_sender, vdom_receiver) = futures::sync::oneshot::channel();
165+ let (vdom_sender, vdom_receiver) = futures::channel::oneshot::channel();
200 let mut vdom_sender = Some(vdom_sender);
166 let mut vdom_sender = Some(vdom_sender);
201
167
202 // Render a vdom with a listener into container.
168 // Render a vdom with a listener into container.
@@ -211,33 +177,26 @@ fn removed_listener_is_not_called() -> impl Future<Item = (), Error = JsValue> {
211 }),
177 }),
212 );
178 );
213
179
214− vdom.weak()
215− // Re-render, so we aren't listening anymore.
180+ // Re-render, so we aren't listening anymore.
216− .render()
181+ vdom.weak().render().await.unwrap();
217− .map_err(|e| JsValue::from(e.to_string()))
182+
218− .and_then(move |_| {
219− target(&container).click();
183+ target(&container).click();
220−
184+
221− // We should get our container's event handler fired, and not the unmounted
185+ // We should get our container's event handler fired, and not the unmounted
222− // vdom's event handler.
186+ // vdom's event handler.
223− outer_receiver
224− .select(vdom_receiver)
187+ match select(outer_receiver, vdom_receiver).await {
225− .map(|(which, _other)| which)
188+ Either::Left((Ok(msg), _)) => assert_eq!(msg, ("outer")),
226− .or_else(|(_error, other)| other)
189+ Either::Left((Err(_), _)) => panic!(),
227− .map(|which| {
190+ Either::Right((Ok(_), _)) => panic!(),
228− assert_eq!(which, "outer");
191+ Either::Right((Err(_), outer)) => assert_eq!(outer.await, Ok("outer")),
229− drop(outer_listener);
230− drop(vdom)
231− })
232− .map_err(|e| JsValue::from(e.to_string()))
233− })
192+ }
234}
193}
235
194
236−#[wasm_bindgen_test(async)]
195+#[wasm_bindgen_test]
237−fn event_does_not_fire_after_unmounting() -> impl Future<Item = (), Error = JsValue> {
196+async fn event_does_not_fire_after_unmounting() {
238 let container = create_element("div");
197 let container = create_element("div");
239
198
240− let (outer_sender, outer_receiver) = futures::sync::oneshot::channel();
199+ let (outer_sender, outer_receiver) = futures::channel::oneshot::channel();
241 let outer_sender = Rc::new(RefCell::new(Some(outer_sender)));
200 let outer_sender = Rc::new(RefCell::new(Some(outer_sender)));
242 let outer_listener = Closure::wrap(Box::new(move |_| {
201 let outer_listener = Closure::wrap(Box::new(move |_| {
243 outer_sender
202 outer_sender
@@ -246,13 +205,13 @@ fn event_does_not_fire_after_unmounting() -> impl Future<Item = (), Error = JsVa
246 .expect_throw("should only invoke outer_listener once")
205 .expect_throw("should only invoke outer_listener once")
247 .send("outer")
206 .send("outer")
248 .expect_throw("should not have dropped receiver");
207 .expect_throw("should not have dropped receiver");
249− }) as Box<FnMut(web_sys::Event)>);
208+ }) as Box<dyn FnMut(web_sys::Event)>);
250
209
251 container
210 container
252 .add_event_listener_with_callback("click", outer_listener.as_ref().unchecked_ref())
211 .add_event_listener_with_callback("click", outer_listener.as_ref().unchecked_ref())
253 .unwrap();
212 .unwrap();
254
213
255− let (vdom_sender, vdom_receiver) = futures::sync::oneshot::channel();
214+ let (vdom_sender, vdom_receiver) = futures::channel::oneshot::channel();
256 let mut vdom_sender = Some(vdom_sender);
215 let mut vdom_sender = Some(vdom_sender);
257
216
258 // Render a vdom with a listener into container.
217 // Render a vdom with a listener into container.
@@ -282,90 +241,10 @@ fn event_does_not_fire_after_unmounting() -> impl Future<Item = (), Error = JsVa
282
241
283 // We should get our container's event handler fired, and not the unmounted
242 // We should get our container's event handler fired, and not the unmounted
284 // vdom's event handler.
243 // vdom's event handler.
285− outer_receiver
244+ match select(outer_receiver, vdom_receiver).await {
286− .select(vdom_receiver)
287− .map(|(which, _other)| which)
288− .or_else(|(_error, other)| other)
289− .map(|which| {
290− assert_eq!(which, "outer");
245+ Either::Left((Ok(msg), _)) => assert_eq!(msg, ("outer")),
291− drop(outer_listener);
246+ Either::Left((Err(_), _)) => panic!(),
292− })
247+ Either::Right((Ok(_), _)) => panic!(),
293− .map_err(|e| JsValue::from(e.to_string()))
248+ Either::Right((Err(_), outer)) => assert_eq!(outer.await, Ok("outer")),
294−}
295−
296−struct CountRendersAndListen {
297− count: Cell<usize>,
298− callback: Box<FnMut(usize)>,
299−}
300−
301−impl CountRendersAndListen {
302− fn new<F>(callback: F) -> CountRendersAndListen
303− where
304− F: 'static + FnMut(usize),
305− {
306− CountRendersAndListen {
307− count: Cell::new(0),
308− callback: Box::new(callback),
309− }
310− }
311−}
312−
313−impl Render for CountRendersAndListen {
314− fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
315− where
316− 'a: 'bump,
317− {
318− let count = self.count.get() + 1;
319− self.count.set(count);
320− Node::element(
321− bump,
322− "button",
323− [on(bump, "click", |comp, _vdom, _event| {
324− let comp: &mut _ = comp.unwrap_mut::<dodrio::Cached<CountRendersAndListen>>();
325− let comp: &mut CountRendersAndListen = comp;
326− let n = comp.count.get();
327− (comp.callback)(n);
328− })],
329− [Attribute {
330− name: "id",
331− value: "target",
332− }],
333− [],
334− )
335 }
249 }
336}
250}
337−
338−#[wasm_bindgen_test(async)]
339−fn cached_listeners_work_correctly() -> impl Future<Item = (), Error = JsValue> {
340− let container = create_element("div");
341−
342− let (sender, receiver) = futures::sync::oneshot::channel();
343− let mut sender = Some(sender);
344− let vdom = Vdom::new(
345− &container,
346− dodrio::Cached::new(CountRendersAndListen::new(move |n| {
347− sender.take().unwrap().send(n).unwrap();
348− })),
349− );
350−
351− let weak0 = vdom.weak();
352− let weak1 = vdom.weak();
353− let weak2 = vdom.weak();
354− vdom.weak()
355− // Re-render a few times, so we are using the cached version.
356− .render()
357− .and_then(move |_| weak0.render())
358− .and_then(move |_| weak1.render())
359− .and_then(move |_| weak2.render())
360− .map_err(|e| JsValue::from(e.to_string()))
361− .and_then(move |_| {
362− target(&container).click();
363−
364− receiver
365− .map(|n| {
366− assert_eq!(n, 1);
367− drop(vdom);
368− })
369− .map_err(|e| JsValue::from(e.to_string()))
370− })
371−}
tests/web/js_api.rsnew file+153
@@ -0,0 +1,153 @@
1+use super::{assert_rendered, create_element, RenderFn};
2+use dodrio::{builder::*, Node, Render, RenderContext, Vdom};
3+use dodrio_js_api::JsRender;
4+use futures::channel::oneshot;
5+use js_sys::Object;
6+use std::rc::Rc;
7+use wasm_bindgen::prelude::*;
8+use wasm_bindgen::JsCast;
9+use wasm_bindgen_test::*;
10+
11+pub struct WrapJs {
12+ inner: JsRender,
13+}
14+
15+impl WrapJs {
16+ pub fn new() -> (WrapJs, oneshot::Receiver<()>, Closure<dyn FnMut()>) {
17+ let (sender, receiver) = oneshot::channel();
18+
19+ let on_after_render = Closure::wrap(Box::new({
20+ let mut sender = Some(sender);
21+ move || {
22+ sender
23+ .take()
24+ .expect_throw("on_after_render should only be called once")
25+ .send(())
26+ .expect_throw("receiver should not have been dropped");
27+ }
28+ }) as Box<dyn FnMut()>);
29+
30+ let component = WrapJs {
31+ inner: JsRender::new(JsComponent::new(&on_after_render)),
32+ };
33+
34+ (component, receiver, on_after_render)
35+ }
36+}
37+
38+impl<'a> Render<'a> for WrapJs {
39+ fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
40+ div(&cx)
41+ .attr("class", "wrap-js")
42+ .children([self.inner.render(cx)])
43+ .finish()
44+ }
45+}
46+
47+#[wasm_bindgen]
48+extern "C" {
49+ #[wasm_bindgen(extends = Object)]
50+ #[derive(Clone, Debug)]
51+ type JsComponent;
52+
53+ #[wasm_bindgen(constructor)]
54+ fn new(on_after_render: &Closure<dyn FnMut()>) -> JsComponent;
55+}
56+
57+fn eval_js_rendering_component() {
58+ use std::sync::Once;
59+ static EVAL: Once = Once::new();
60+ EVAL.call_once(|| {
61+ super::init_logging();
62+ js_sys::eval(
63+ r#"
64+ window.JsComponent = class JsComponent {
65+ constructor(onAfterRender) {
66+ this.count = 0;
67+ this.onAfterRender = onAfterRender;
68+ }
69+
70+ render() {
71+ return {
72+ tagName: "span",
73+ attributes: [{ name: "class", value: "js-component" }],
74+ listeners: [{ on: "click", callback: this.onClick.bind(this) }],
75+ children: [
76+ "Here is some plain text",
77+ {
78+ tagName: "b",
79+ children: ["...and here is some bold text"]
80+ },
81+ String(this.count),
82+ ]
83+ };
84+ }
85+
86+ async onClick(vdom, event) {
87+ this.count++;
88+ await vdom.render();
89+ this.onAfterRender();
90+ }
91+ };
92+ "#,
93+ )
94+ .expect_throw("should eval JS component OK");
95+ });
96+}
97+
98+#[wasm_bindgen_test]
99+async fn can_use_js_rendering_components() {
100+ eval_js_rendering_component();
101+
102+ let container = create_element("div");
103+ let (component, receiver, _closure) = WrapJs::new();
104+ let _vdom = Rc::new(Vdom::new(&container, component));
105+
106+ assert_rendered(
107+ &container,
108+ &RenderFn(|cx| {
109+ div(&cx)
110+ .attr("class", "wrap-js")
111+ .children([span(&cx)
112+ .attr("class", "js-component")
113+ .children([
114+ text("Here is some plain text"),
115+ b(&cx)
116+ .children([text("...and here is some bold text")])
117+ .finish(),
118+ text("0"),
119+ ])
120+ .finish()])
121+ .finish()
122+ }),
123+ );
124+
125+ container
126+ .query_selector(".js-component")
127+ .expect_throw("should querySelector OK")
128+ .expect_throw("should find `.js-component` element OK")
129+ .dyn_into::<web_sys::HtmlElement>()
130+ .expect_throw("should be an `HTMLElement` object")
131+ .click();
132+
133+ receiver.await.unwrap();
134+
135+ assert_rendered(
136+ &container,
137+ &RenderFn(|cx| {
138+ div(&cx)
139+ .attr("class", "wrap-js")
140+ .children([span(&cx)
141+ .attr("class", "js-component")
142+ .children([
143+ text("Here is some plain text"),
144+ b(&cx)
145+ .children([text("...and here is some bold text")])
146+ .finish(),
147+ text("1"),
148+ ])
149+ .finish()])
150+ .finish()
151+ }),
152+ );
153+}
tests/web/keyed.jsnew file+25
@@ -0,0 +1,25 @@
1+export function saveKeyedElements(container) {
2+ const saved = new Map;
3+ const children = container.querySelectorAll(".keyed");
4+ for (const child of children) {
5+ saved.set(child.id, child);
6+ }
7+ return saved;
8+}
9+
10+function assertEq(a, b, msg) {
11+ if (a !== b) {
12+ throw new Error(`assertEq failed: ${a} !== ${b}; ${msg}`);
13+ }
14+}
15+
16+export function checkKeyedElements(container, saved) {
17+ const children = container.querySelectorAll(".keyed");
18+ for (const child of children) {
19+ console.log(`checking child=${child.outerHTML}`);
20+ const original = saved.get(child.id);
21+ if (original) {
22+ assertEq(original, child, `did not preserve child with key=${child.id}`);
23+ }
24+ }
25+}
tests/web/keyed.rsnew file+269
@@ -0,0 +1,269 @@
1+use crate::{assert_rendered, create_element};
2+use dodrio::{builder::*, bumpalo, Node, Render, RenderContext, Vdom};
3+use log::*;
4+use std::rc::Rc;
5+use wasm_bindgen::prelude::*;
6+use wasm_bindgen_test::*;
7+
8+struct Keyed(u16);
9+
10+impl<'a> Render<'a> for Keyed {
11+ fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
12+ let key = bumpalo::format!(in cx.bump, "{}", self.0).into_bump_str();
13+ div(&cx)
14+ .attr("class", "keyed")
15+ .attr("id", key)
16+ .key(self.0 as u32)
17+ .finish()
18+ }
19+}
20+
21+fn keyed<'a, Keys>(cx: &mut RenderContext<'a>, keys: Keys) -> Node<'a>
22+where
23+ Keys: AsRef<[u16]>,
24+{
25+ let mut parent = div(&cx).attr("class", "parent");
26+
27+ for &k in keys.as_ref() {
28+ parent = parent.child(Keyed(k).render(cx));
29+ }
30+
31+ parent.finish()
32+}
33+
34+async fn assert_keyed<Before, After>(before: Before, after: After) -> Result<(), JsValue>
35+where
36+ Before: 'static + for<'a> Render<'a>,
37+ After: 'static + for<'a> Render<'a>,
38+{
39+ #[wasm_bindgen(module = "/tests/web/keyed.js")]
40+ extern "C" {
41+ #[wasm_bindgen(js_name = saveKeyedElements)]
42+ fn save_keyed_elements(container: &web_sys::Element) -> JsValue;
43+
44+ #[wasm_bindgen(js_name = checkKeyedElements)]
45+ fn check_keyed_elements(container: &web_sys::Element, saved: JsValue);
46+ }
47+
48+ let container = create_element("div");
49+
50+ let before = Rc::new(before);
51+ let after = Rc::new(after);
52+
53+ debug!("====== Rendering the *before* DOM into the physical DOM ======");
54+ let vdom1 = Rc::new(Vdom::new(&container, before.clone()));
55+ let _vdom2 = vdom1.clone();
56+ let saved = save_keyed_elements(&container);
57+
58+ debug!("====== Checking the *before* DOM against the physical DOM ======");
59+ assert_rendered(&container, &before);
60+
61+ debug!("====== Rendering the *after* DOM into the physical DOM ======");
62+ let weak = vdom1.weak();
63+ weak.set_component(Box::new(after.clone()))
64+ .await
65+ .map_err(|e| e.to_string())?;
66+
67+ debug!("====== Checking the *after* DOM against the physical DOM ======");
68+ assert_rendered(&container, &after);
69+ check_keyed_elements(&container, saved);
70+
71+ Ok(())
72+}
73+
74+macro_rules! keyed_tests {
75+ ( $(
76+ $name:ident {
77+ before($before_cx:ident) {
78+ $( $before:tt )*
79+ }
80+ after($after_cx:ident) {
81+ $( $after:tt )*
82+ }
83+ }
84+ )* ) => {
85+ $(
86+ #[wasm_bindgen_test]
87+ async fn $name() {
88+ use crate::RenderFn;
89+ log::debug!("############### {} ###############", stringify!($name));
90+ assert_keyed(
91+ RenderFn(|$before_cx| { $( $before )* }),
92+ RenderFn(|$after_cx| { $( $after )* }),
93+ )
94+ .await
95+ .unwrap()
96+ }
97+ )*
98+ }
99+}
100+
101+keyed_tests! {
102+ same_order {
103+ before(cx) {
104+ keyed(cx, [1, 2, 3])
105+ }
106+ after(cx) {
107+ keyed(cx, [1, 2, 3])
108+ }
109+ }
110+
111+ same_order_append {
112+ before(cx) {
113+ keyed(cx, [1])
114+ }
115+ after(cx) {
116+ keyed(cx, [1, 2])
117+ }
118+ }
119+
120+ same_order_delete {
121+ before(cx) {
122+ keyed(cx, [1, 2, 3])
123+ }
124+ after(cx) {
125+ keyed(cx, [1, 2])
126+ }
127+ }
128+
129+ same_suffix {
130+ before(cx) {
131+ keyed(cx, [1, 2, 3])
132+ }
133+ after(cx) {
134+ keyed(cx, [4, 2, 3])
135+ }
136+ }
137+
138+ same_prefix_and_suffix_reorder_middle {
139+ before(cx) {
140+ keyed(cx, [1, 2, 3, 4])
141+ }
142+ after(cx) {
143+ keyed(cx, [1, 3, 2, 4])
144+ }
145+ }
146+
147+ same_prefix_and_suffix_new_middle {
148+ before(cx) {
149+ keyed(cx, [2, 3, 4, 5])
150+ }
151+ after(cx) {
152+ keyed(cx, [2, 7, 8, 5])
153+ }
154+ }
155+
156+ reverse_order {
157+ before(cx) {
158+ keyed(cx, [1, 2, 3])
159+ }
160+ after(cx) {
161+ keyed(cx, [3, 2, 1])
162+ }
163+ }
164+
165+ no_shared_keys {
166+ before(cx) {
167+ keyed(cx, [1, 2])
168+ }
169+ after(cx) {
170+ keyed(cx, [3, 4])
171+ }
172+ }
173+
174+ new_keys_in_middle {
175+ before(cx) {
176+ keyed(cx, [1, 2])
177+ }
178+ after(cx) {
179+ keyed(cx, [1, 3, 4, 2])
180+ }
181+ }
182+
183+ new_keys_at_start_and_end {
184+ before(cx) {
185+ keyed(cx, [1, 2, 3])
186+ }
187+ after(cx) {
188+ keyed(cx, [4, 1, 2, 3, 5])
189+ }
190+ }
191+
192+ delete_prefix {
193+ before(cx) {
194+ keyed(cx, [1, 2, 3])
195+ }
196+ after(cx) {
197+ keyed(cx, [2, 3])
198+ }
199+ }
200+
201+ delete_suffix {
202+ before(cx) {
203+ keyed(cx, [1, 2, 3])
204+ }
205+ after(cx) {
206+ keyed(cx, [1, 2])
207+ }
208+ }
209+
210+ delete_middle {
211+ before(cx) {
212+ keyed(cx, [1, 2, 3])
213+ }
214+ after(cx) {
215+ keyed(cx, [1, 3])
216+ }
217+ }
218+
219+ nested_keyed_children {
220+ before(cx) {
221+ ul(&cx)
222+ .children([
223+ li(&cx)
224+ .key(1)
225+ .children([
226+ keyed(cx, [2, 3, 4])
227+ ])
228+ .finish(),
229+ li(&cx)
230+ .key(5)
231+ .children([
232+ keyed(cx, [6, 7, 8])
233+ ])
234+ .finish(),
235+ li(&cx)
236+ .key(9)
237+ .children([
238+ keyed(cx, [10, 11, 12])
239+ ])
240+ .finish(),
241+ ])
242+ .finish()
243+ }
244+ after(cx) {
245+ ul(&cx)
246+ .children([
247+ li(&cx)
248+ .key(9)
249+ .children([
250+ keyed(cx, [12, 11, 10])
251+ ])
252+ .finish(),
253+ li(&cx)
254+ .key(5)
255+ .children([
256+ keyed(cx, [8, 7, 6])
257+ ])
258+ .finish(),
259+ li(&cx)
260+ .key(1)
261+ .children([
262+ keyed(cx, [4, 3, 2])
263+ ])
264+ .finish(),
265+ ])
266+ .finish()
267+ }
268+ }
269+}
tests/web/main.rs+65−41
@@ -1,11 +1,14 @@
1//! Test suite for the Web and headless browsers.
1//! Test suite for the Web and headless browsers.
2
2
3−#![cfg(target_arch = "wasm32")]
3+#![cfg(all(feature = "xxx-unstable-internal-use-only", target_arch = "wasm32"))]
4
4
5use bumpalo::Bump;
5use bumpalo::Bump;
6+use dodrio::{
6−use dodrio::{Attribute, Node, Render, Vdom};
7+ Attribute, CachedSet, ElementNode, Node, NodeKind, Render, RenderContext, TextNode, Vdom,
7−use futures::prelude::*;
8+};
9+use fxhash::FxHashMap;
8use log::*;
10use log::*;
11+use std::cell::RefCell;
9use std::rc::Rc;
12use std::rc::Rc;
10use wasm_bindgen::prelude::*;
13use wasm_bindgen::prelude::*;
11use wasm_bindgen::JsCast;
14use wasm_bindgen::JsCast;
@@ -15,6 +18,8 @@ wasm_bindgen_test_configure!(run_in_browser);
15
18
16pub mod cached;
19pub mod cached;
17pub mod events;
20pub mod events;
21+pub mod js_api;
22+pub mod keyed;
18pub mod render;
23pub mod render;
19
24
20pub fn window() -> web_sys::Window {
25pub fn window() -> web_sys::Window {
@@ -45,15 +50,20 @@ pub fn init_logging() {
45
50
46/// Assert that the `container` contains the physical DOM tree that matches
51/// Assert that the `container` contains the physical DOM tree that matches
47/// `r`'s rendered virtual DOM.
52/// `r`'s rendered virtual DOM.
48−pub fn assert_rendered<R: Render>(container: &web_sys::Element, r: &R) {
53+pub fn assert_rendered<R: for<'a> Render<'a>>(container: &web_sys::Element, r: &R) {
49 init_logging();
54 init_logging();
50
55
56+ let cached_set = &RefCell::new(CachedSet::default());
51 let bump = &Bump::new();
57 let bump = &Bump::new();
58+ let templates = &mut FxHashMap::default();
59+ let cx = &mut RenderContext::new(bump, cached_set, templates);
52− let node = r.render(bump);
60+ let node = r.render(cx);
53 let child = container
61 let child = container
54 .first_child()
62 .first_child()
55 .expect("container does not have anything rendered into it?");
63 .expect("container does not have anything rendered into it?");
64+
65+ let cached_set = cached_set.borrow();
56− check_node(&child, &node);
66+ check_node(&cached_set, &child, &node);
57
67
58 fn stringify_actual_node(n: &web_sys::Node) -> String {
68 fn stringify_actual_node(n: &web_sys::Node) -> String {
59 if let Some(el) = n.dyn_ref::<web_sys::Element>() {
69 if let Some(el) = n.dyn_ref::<web_sys::Element>() {
@@ -63,12 +73,12 @@ pub fn assert_rendered<R: Render>(container: &web_sys::Element, r: &R) {
63 }
73 }
64 }
74 }
65
75
66− fn check_node(actual: &web_sys::Node, expected: &Node) {
76+ fn check_node(cached_set: &CachedSet, actual: &web_sys::Node, expected: &Node) {
67 debug!("check_render:");
77 debug!("check_render:");
68 debug!(" actual = {}", stringify_actual_node(&actual));
78 debug!(" actual = {}", stringify_actual_node(&actual));
69 debug!(" expected = {:#?}", expected);
79 debug!(" expected = {:#?}", expected);
70− match expected {
80+ match expected.kind {
71− Node::Text(text_node) => {
81+ NodeKind::Text(TextNode { text }) => {
72 assert_eq!(
82 assert_eq!(
73 actual.node_name().to_uppercase(),
83 actual.node_name().to_uppercase(),
74 "#TEXT",
84 "#TEXT",
@@ -76,21 +86,34 @@ pub fn assert_rendered<R: Render>(container: &web_sys::Element, r: &R) {
76 );
86 );
77 assert_eq!(
87 assert_eq!(
78 actual.text_content().unwrap_or_default(),
88 actual.text_content().unwrap_or_default(),
79− text_node.text(),
89+ text,
80 "actual.text_content() == expected.text()"
90 "actual.text_content() == expected.text()"
81 );
91 );
82 }
92 }
83− Node::Element(elem) => {
93+ NodeKind::Element(&ElementNode {
94+ tag_name,
95+ attributes,
96+ children,
97+ namespace,
98+ ..
99+ }) => {
84 assert_eq!(
100 assert_eq!(
85 actual.node_name().to_uppercase(),
101 actual.node_name().to_uppercase(),
86− elem.tag_name().to_uppercase(),
102+ tag_name.to_uppercase(),
87 "actual.node_name() == expected.tag_name()"
103 "actual.node_name() == expected.tag_name()"
88 );
104 );
89 let actual = actual
105 let actual = actual
90 .dyn_ref::<web_sys::Element>()
106 .dyn_ref::<web_sys::Element>()
91 .expect("`actual` should be an `Element`");
107 .expect("`actual` should be an `Element`");
92− check_attributes(actual.attributes(), elem.attributes());
108+ check_attributes(actual.attributes(), attributes);
93− check_children(actual.child_nodes(), elem.children());
109+ check_children(cached_set, actual.child_nodes(), children);
110+ if let Some(namespace) = namespace {
111+ assert_eq!(actual.namespace_uri(), Some(namespace.into()))
112+ }
113+ }
114+ NodeKind::Cached(ref c) => {
115+ let (expected, _template) = cached_set.get(c.id);
116+ check_node(cached_set, actual, &expected);
94 }
117 }
95 }
118 }
96 }
119 }
@@ -103,18 +126,18 @@ pub fn assert_rendered<R: Render>(container: &web_sys::Element, r: &R) {
103 );
126 );
104 for attr in expected {
127 for attr in expected {
105 let actual_attr = actual
128 let actual_attr = actual
106− .get_named_item(attr.name)
129+ .get_named_item(attr.name())
107− .expect(&format!("should have attribute \"{}\"", attr.name));
130+ .expect(&format!("should have attribute \"{}\"", attr.name()));
108 assert_eq!(
131 assert_eq!(
109 actual_attr.value(),
132 actual_attr.value(),
110− attr.value,
133+ attr.value(),
111 "actual attr value == expected attr value for attr \"{}\"",
134 "actual attr value == expected attr value for attr \"{}\"",
112− attr.name
135+ attr.name()
113 );
136 );
114 }
137 }
115 }
138 }
116
139
117− fn check_children(actual: web_sys::NodeList, expected: &[Node]) {
140+ fn check_children(cached_set: &CachedSet, actual: web_sys::NodeList, expected: &[Node]) {
118 assert_eq!(
141 assert_eq!(
119 actual.length(),
142 actual.length(),
120 expected.len() as u32,
143 expected.len() as u32,
@@ -122,7 +145,7 @@ pub fn assert_rendered<R: Render>(container: &web_sys::Element, r: &R) {
122 );
145 );
123 for (i, child) in expected.iter().enumerate() {
146 for (i, child) in expected.iter().enumerate() {
124 let actual_child = actual.item(i as u32).unwrap();
147 let actual_child = actual.item(i as u32).unwrap();
125− check_node(&actual_child, child);
148+ check_node(cached_set, &actual_child, child);
126 }
149 }
127 }
150 }
128}
151}
@@ -130,27 +153,24 @@ pub fn assert_rendered<R: Render>(container: &web_sys::Element, r: &R) {
130/// Use the function `F` to render.
153/// Use the function `F` to render.
131pub struct RenderFn<F>(F)
154pub struct RenderFn<F>(F)
132where
155where
133− F: for<'bump> Fn(&'bump Bump) -> Node<'bump>;
156+ F: for<'a> Fn(&mut RenderContext<'a>) -> Node<'a>;
134
157
135−impl<F> Render for RenderFn<F>
158+impl<'a, F> Render<'a> for RenderFn<F>
136where
159where
137− F: for<'bump> Fn(&'bump Bump) -> Node<'bump>,
160+ F: for<'b> Fn(&mut RenderContext<'b>) -> Node<'b>,
138{
161{
139− fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
162+ fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
140− where
141− 'a: 'bump,
142− {
143− (self.0)(bump)
163+ (self.0)(cx)
144 }
164 }
145}
165}
146
166
147/// Assert that if we start by rendering the `before` virtual DOM tree into a
167/// Assert that if we start by rendering the `before` virtual DOM tree into a
148/// physical DOM tree, and then diff it with the `after` virtual DOM tree, then
168/// physical DOM tree, and then diff it with the `after` virtual DOM tree, then
149/// the physical DOM tree correctly matches `after`.
169/// the physical DOM tree correctly matches `after`.
150−pub fn assert_before_after<R, S>(before: R, after: S) -> impl Future<Item = (), Error = JsValue>
170+pub async fn assert_before_after<R, S>(before: R, after: S) -> Result<(), JsValue>
151where
171where
152− R: 'static + Render,
172+ R: 'static + for<'a> Render<'a>,
153− S: 'static + Render,
173+ S: 'static + for<'a> Render<'a>,
154{
174{
155 let container = create_element("div");
175 let container = create_element("div");
156
176
@@ -159,7 +179,7 @@ where
159
179
160 debug!("====== Rendering the *before* DOM into the physical DOM ======");
180 debug!("====== Rendering the *before* DOM into the physical DOM ======");
161 let vdom1 = Rc::new(Vdom::new(&container, before.clone()));
181 let vdom1 = Rc::new(Vdom::new(&container, before.clone()));
162− let vdom2 = vdom1.clone();
182+ let _vdom2 = vdom1.clone();
163
183
164 debug!("====== Checking the *before* DOM against the physical DOM ======");
184 debug!("====== Checking the *before* DOM against the physical DOM ======");
165 assert_rendered(&container, &before);
185 assert_rendered(&container, &before);
@@ -167,15 +187,16 @@ where
167 debug!("====== Rendering the *after* DOM into the physical DOM ======");
187 debug!("====== Rendering the *after* DOM into the physical DOM ======");
168 let weak = vdom1.weak();
188 let weak = vdom1.weak();
169 weak.set_component(Box::new(after.clone()))
189 weak.set_component(Box::new(after.clone()))
170− .map(move |_| {
190+ .await
191+ .map_err(|e| e.to_string())?;
192+
171− debug!("====== Checking the *after* DOM against the physical DOM ======");
193+ debug!("====== Checking the *after* DOM against the physical DOM ======");
172− assert_rendered(&container, &after);
194+ assert_rendered(&container, &after);
173− drop(vdom2);
195+
174− })
196+ Ok(())
175− .map_err(|e| JsValue::from(e.to_string()))
176}
197}
177
198
178−/// A helper macro for decalaring a bunch of `assert_before_after` tests.
199+/// A helper macro for declaring a bunch of `assert_before_after` tests.
179#[macro_export]
200#[macro_export]
180macro_rules! before_after {
201macro_rules! before_after {
181 ( $(
202 ( $(
@@ -189,13 +210,16 @@ macro_rules! before_after {
189 }
210 }
190 )* ) => {
211 )* ) => {
191 $(
212 $(
192− #[wasm_bindgen_test(async)]
213+ #[wasm_bindgen_test]
193− fn $name() -> impl Future<Item = (), Error = wasm_bindgen::JsValue> {
214+ async fn $name() {
194 use crate::{assert_before_after, RenderFn};
215 use crate::{assert_before_after, RenderFn};
216+ log::debug!("############### {} ###############", stringify!($name));
195 assert_before_after(
217 assert_before_after(
196 RenderFn(|$before_bump| { $( $before )* }),
218 RenderFn(|$before_bump| { $( $before )* }),
197 RenderFn(|$after_bump| { $( $after )* })
219 RenderFn(|$after_bump| { $( $after )* })
198 )
220 )
221+ .await
222+ .unwrap()
199 }
223 }
200 )*
224 )*
201 }
225 }
tests/web/render.rs+233−142
@@ -1,12 +1,12 @@
1use super::{assert_rendered, before_after, create_element, RenderFn};
1use super::{assert_rendered, before_after, create_element, RenderFn};
2−use dodrio::{Attribute, Node, Vdom};
2+use dodrio::{builder::*, bumpalo::collections::String, Node, Render, RenderContext, Vdom};
3−use futures::prelude::*;
4use std::rc::Rc;
3use std::rc::Rc;
4+use wasm_bindgen::{ JsCast};
5use wasm_bindgen_test::*;
5use wasm_bindgen_test::*;
6
6
7#[wasm_bindgen_test]
7#[wasm_bindgen_test]
8fn render_initial_text() {
8fn render_initial_text() {
9− let hello = Rc::new(RenderFn(|_bump| Node::text("hello")));
9+ let hello = Rc::new(RenderFn(|_cx| text("hello")));
10
10
11 let container = create_element("div");
11 let container = create_element("div");
12 let _vdom = Vdom::new(&container, hello.clone());
12 let _vdom = Vdom::new(&container, hello.clone());
@@ -15,20 +15,11 @@ fn render_initial_text() {
15
15
16#[wasm_bindgen_test]
16#[wasm_bindgen_test]
17fn render_initial_node() {
17fn render_initial_node() {
18− let hello = Rc::new(RenderFn(|bump| {
18+ let hello = Rc::new(RenderFn(|cx| {
19− Node::element(
20− bump,
21− "div",
19+ div(&cx)
22− [],
23− [Attribute {
24− name: "id",
25− value: "hello-world",
20+ .attr("id", "hello-world")
26− }],
21+ .children([text("Hello "), span(&cx).child(text("World!")).finish()])
27− [
22+ .finish()
28− Node::text("Hello "),
29− Node::element(bump, "span", [], [], [Node::text("World!")]),
30− ],
31− )
32 }));
23 }));
33
24
34 let container = create_element("div");
25 let container = create_element("div");
@@ -39,214 +30,314 @@ fn render_initial_node() {
39#[wasm_bindgen_test]
30#[wasm_bindgen_test]
40fn container_is_emptied_upon_drop() {
31fn container_is_emptied_upon_drop() {
41 let container = create_element("div");
32 let container = create_element("div");
42− let vdom = Vdom::new(&container, RenderFn(|_bump| Node::text("blah")));
33+ let vdom = Vdom::new(&container, RenderFn(|_cx| text("blah")));
43 drop(vdom);
34 drop(vdom);
44 assert!(container.first_child().is_none());
35 assert!(container.first_child().is_none());
45}
36}
46
37
38+/// Renders a child with a lifetime scoped to the RenderContext bump arena.
39+#[wasm_bindgen_test]
40+fn render_bump_scoped_node() {
41+ struct Child<'a> {
42+ name: &'a str,
43+ }
44+
45+ impl<'a> Render<'a> for Child<'a> {
46+ fn render(&self, _cx: &mut RenderContext<'a>) -> Node<'a> {
47+ text(self.name)
48+ }
49+ }
50+
51+ struct Parent;
52+
53+ impl<'a> Render<'a> for Parent {
54+ fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
55+ let child_name = String::from_str_in("child", cx.bump).into_bump_str();
56+
57+ div(&cx)
58+ .children([Child { name: child_name }.render(cx)])
59+ .finish()
60+ }
61+ }
62+
63+ let parent = Rc::new(RenderFn(|cx| {
64+ Parent.render(cx)
65+ }));
66+
67+ let container = create_element("div");
68+ let _vdom = Vdom::new(&container, parent.clone());
69+
70+ assert_rendered(&container, &parent);
71+}
72+
73+/// Originally, dodrio would use the className property for SVGs.
74+///
75+/// This is problematic because when SVG elements are created, the className is flagged as a read
76+/// only property, so setting it causes an exception to be thrown. Here's an example of how this
77+/// happens:
78+///
79+/// let elem = web_sys::window()
80+/// .unwrap()
81+/// .document()
82+/// .unwrap()
83+/// .create_element_ns(Some("http://www.w3.org/2000/svg"), "svg")
84+/// .unwrap();
85+///
86+/// elem.set_class_name("does-not-work");
87+///
88+/// -----------------------------------------------------------------------------------------------
89+///
90+/// wasm-bindgen: imported JS function that was not marked as `catch` threw an error:
91+/// setting getter-only property "className"
92+///
93+/// -----------------------------------------------------------------------------------------------
94+///
95+/// Now, dodrio passes the 'class' attribute of all namespaced elements into set_attribute. This
96+/// satisfies the restrictions on SVG and keeps the optimized path for non-namespaced elements
97+#[wasm_bindgen_test(async)]
98+async fn test_svg_set_class() {
99+ let container = create_element("div");
100+
101+
102+ let valid_svg = Rc::new(RenderFn(|cx| {
103+ ElementBuilder::new(cx.bump, "svg")
104+ .namespace(Some("http://www.w3.org/2000/svg"))
105+ .attr("class", "works")
106+ .finish()
107+ }));
108+
109+ let vdom = Vdom::new(&container, valid_svg.clone());
110+ let weak = vdom.weak();
111+
112+ weak.render().await.unwrap();
113+
114+ assert_eq!(
115+ "works",
116+ container.first_child()
117+ .expect("unable to get svg")
118+ .dyn_ref::<web_sys::Element>()
119+ .expect("svg should be an element")
120+ .get_attribute("class")
121+ .expect("unable to get 'class' of svg")
122+ );
123+}
124+
47before_after! {
125before_after! {
48 same_text {
126 same_text {
49− before(_bump) {
127+ before(_cx) {
50− Node::text("hello")
128+ text("hello")
51 }
129 }
52− after(_bump) {
130+ after(_cx) {
53− Node::text("hello")
131+ text("hello")
54 }
132 }
55 }
133 }
56
134
57 update_text {
135 update_text {
58− before(_bump) {
136+ before(_cx) {
59− Node::text("before")
137+ text("before")
60 }
138 }
61− after(_bump) {
139+ after(_cx) {
62− Node::text("after")
140+ text("after")
63 }
141 }
64 }
142 }
65
143
66 replace_text_with_elem {
144 replace_text_with_elem {
67− before(_bump) {
145+ before(_cx) {
68− Node::text("before")
146+ text("before")
69 }
147 }
70− after(bump) {
148+ after(cx) {
71− Node::element(bump, "div", [], [], [])
149+ div(&cx).finish()
72 }
150 }
73 }
151 }
74
152
75 replace_elem_with_text {
153 replace_elem_with_text {
76− before(bump) {
154+ before(cx) {
77− Node::element(bump, "div", [], [], [])
155+ div(&cx).finish()
78 }
156 }
79− after(_bump) {
157+ after(_cx) {
80− Node::text("before")
158+ text("before")
81 }
159 }
82 }
160 }
83
161
84 same_elem {
162 same_elem {
85− before(bump) {
163+ before(cx) {
86− Node::element(bump, "div", [], [], [])
164+ div(&cx).finish()
87 }
165 }
88− after(bump) {
166+ after(cx) {
89− Node::element(bump, "div", [], [], [])
167+ div(&cx).finish()
90 }
168 }
91 }
169 }
92
170
93 elems_with_different_tag_names {
171 elems_with_different_tag_names {
94− before(bump) {
172+ before(cx) {
95− Node::element(bump, "span", [], [], [])
173+ span(&cx).finish()
96 }
174 }
97− after(bump) {
175+ after(cx) {
98− Node::element(bump, "div", [], [], [])
176+ div(&cx).finish()
99 }
177 }
100 }
178 }
101
179
102 same_tag_name_update_attribute {
180 same_tag_name_update_attribute {
103− before(bump) {
181+ before(cx) {
104− Node::element(bump, "div", [], [Attribute { name: "value", value: "1" }], [])
182+ div(&cx).attr("value", "1").finish()
105 }
183 }
106− after(bump) {
184+ after(cx) {
107− Node::element(bump, "div", [], [Attribute { name: "value", value: "2" }], [])
185+ div(&cx).attr("value", "2").finish()
108 }
186 }
109 }
187 }
110
188
111 same_tag_name_remove_attribute {
189 same_tag_name_remove_attribute {
112− before(bump) {
190+ before(cx) {
113− Node::element(bump, "div", [], [Attribute { name: "value", value: "1" }], [])
191+ div(&cx).attr("value", "1").finish()
114 }
192 }
115− after(bump) {
193+ after(cx) {
116− Node::element(bump, "div", [], [], [])
194+ div(&cx).finish()
117 }
195 }
118 }
196 }
119
197
120 same_tag_name_add_attribute {
198 same_tag_name_add_attribute {
121− before(bump) {
199+ before(cx) {
122− Node::element(bump, "div", [], [], [])
200+ div(&cx).finish()
123 }
201 }
124− after(bump) {
202+ after(cx) {
125− Node::element(bump, "div", [], [Attribute { name: "value", value: "2" }], [])
203+ div(&cx).attr("value", "2").finish()
126 }
204 }
127 }
205 }
128
206
129 same_tag_name_many_attributes {
207 same_tag_name_many_attributes {
130− before(bump) {
208+ before(cx) {
131− Node::element(bump, "div", [], [
209+ div(&cx)
132− Attribute { name: "before-1", value: "1" },
210+ .attr("before-1", "1")
133− Attribute { name: "shared-1", value: "1" },
211+ .attr("shared-1", "1")
134− Attribute { name: "modified-1", value: "1" },
212+ .attr("modified-1", "1")
135− Attribute { name: "before-2", value: "2" },
213+ .attr("before-2", "2")
136− Attribute { name: "shared-2", value: "2" },
214+ .attr("shared-2", "2")
137− Attribute { name: "modified-2", value: "2" },
215+ .attr("modified-2", "2")
138− Attribute { name: "before-3", value: "3" },
216+ .attr("before-3", "3")
139− Attribute { name: "shared-3", value: "3" },
217+ .attr("shared-3", "3")
140− Attribute { name: "modified-3", value: "3" },
218+ .attr("modified-3", "3")
141− ], [])
219+ .finish()
142− }
220+ }
143− after(bump) {
221+ after(cx) {
144− Node::element(bump, "div", [], [
222+ div(&cx)
145− Attribute { name: "after-1", value: "1" },
223+ .attr("after-1", "1")
146− Attribute { name: "shared-1", value: "1" },
224+ .attr("shared-1", "1")
147− Attribute { name: "modified-1", value: "100" },
225+ .attr("modified-1", "100")
148− Attribute { name: "after-2", value: "2" },
226+ .attr("after-2", "2")
149− Attribute { name: "shared-2", value: "2" },
227+ .attr("shared-2", "2")
150− Attribute { name: "modified-2", value: "200" },
228+ .attr("modified-2", "200")
151− Attribute { name: "after-3", value: "3" },
229+ .attr("after-3", "3")
152− Attribute { name: "shared-3", value: "3" },
230+ .attr("shared-3", "3")
153− Attribute { name: "modified-3", value: "300" },
231+ .attr("modified-3", "300")
154− ], [])
232+ .finish()
155 }
233 }
156 }
234 }
157
235
158 same_tag_same_children {
236 same_tag_same_children {
159− before(bump) {
237+ before(cx) {
160− Node::element(bump, "div", [], [], [
161− Node::text("child")
238+ div(&cx).child(text("child")).finish()
162− ])
163 }
239 }
164− after(bump) {
240+ after(cx) {
165− Node::element(bump, "div", [], [], [
166− Node::text("child")
241+ div(&cx).child(text("child")).finish()
167− ])
168 }
242 }
169 }
243 }
170
244
171 same_tag_update_child {
245 same_tag_update_child {
172− before(bump) {
246+ before(cx) {
173− Node::element(bump, "div", [], [], [
174− Node::text("before")
247+ div(&cx).child(text("before")).finish()
175− ])
176 }
248 }
177− after(bump) {
249+ after(cx) {
178− Node::element(bump, "div", [], [], [
179− Node::text("after")
250+ div(&cx).child(text("after")).finish()
180− ])
181 }
251 }
182 }
252 }
183
253
184 same_tag_add_child {
254 same_tag_add_child {
185− before(bump) {
255+ before(cx) {
186− Node::element(bump, "div", [], [], [])
256+ div(&cx).finish()
187 }
257 }
188− after(bump) {
258+ after(cx) {
189− Node::element(bump, "div", [], [], [
190− Node::text("child")
259+ div(&cx).child(text("child")).finish()
191− ])
192 }
260 }
193 }
261 }
194
262
195 same_tag_remove_child {
263 same_tag_remove_child {
196− before(bump) {
264+ before(cx) {
197− Node::element(bump, "div", [], [], [
198− Node::text("child")
265+ div(&cx).child(text("child")).finish()
199− ])
200 }
266 }
201− after(bump) {
267+ after(cx) {
202− Node::element(bump, "div", [], [], [])
268+ div(&cx).finish()
203 }
269 }
204 }
270 }
205
271
206 same_tag_update_many_children {
272 same_tag_update_many_children {
207− before(bump) {
273+ before(cx) {
208− Node::element(bump, "div", [], [], [
274+ div(&cx)
209− Node::element(bump, "div", [], [], []),
275+ .children([
210− Node::element(bump, "span", [], [], []),
276+ div(&cx).finish(),
211− Node::element(bump, "p", [], [], []),
277+ span(&cx).finish(),
212− ])
278+ p(&cx).finish(),
279+ ])
280+ .finish()
213− }
281+ }
214− after(bump) {
282+ after(cx) {
215− Node::element(bump, "div", [], [], [
283+ div(&cx)
216− Node::element(bump, "span", [], [], []),
284+ .children([
217− Node::element(bump, "p", [], [], []),
285+ span(&cx).finish(),
218− Node::element(bump, "div", [], [], []),
286+ p(&cx).finish(),
219− ])
287+ div(&cx).finish(),
288+ ])
289+ .finish()
220 }
290 }
221 }
291 }
222
292
223 same_tag_remove_many_children {
293 same_tag_remove_many_children {
224− before(bump) {
294+ before(cx) {
225− Node::element(bump, "div", [], [], [
295+ div(&cx)
226− Node::element(bump, "div", [], [], []),
296+ .children([
227− Node::element(bump, "span", [], [], []),
297+ div(&cx).finish(),
228− Node::element(bump, "p", [], [], []),
298+ span(&cx).finish(),
229− ])
299+ p(&cx).finish(),
300+ ])
301+ .finish()
230− }
302+ }
231− after(bump) {
303+ after(cx) {
232− Node::element(bump, "div", [], [], [
304+ div(&cx)
233− Node::element(bump, "div", [], [], []),
305+ .children([
306+ div(&cx).finish(),
234− ])
307+ ])
308+ .finish()
235 }
309 }
236 }
310 }
237
311
238 same_tag_add_many_children {
312 same_tag_add_many_children {
239− before(bump) {
313+ before(cx) {
240− Node::element(bump, "div", [], [], [
314+ div(&cx)
241− Node::element(bump, "div", [], [], []),
315+ .children([
316+ div(&cx).finish(),
242− ])
317+ ])
318+ .finish()
243− }
319+ }
244− after(bump) {
320+ after(cx) {
245− Node::element(bump, "div", [], [], [
321+ div(&cx)
246− Node::element(bump, "div", [], [], []),
322+ .children([
247− Node::element(bump, "span", [], [], []),
323+ div(&cx).finish(),
248− Node::element(bump, "p", [], [], []),
324+ span(&cx).finish(),
249− ])
325+ p(&cx).finish(),
326+ ])
327+ .finish()
328+ }
329+ }
330+
331+ same_tag_different_namespace {
332+ before(cx) {
333+ div(&cx)
334+ .namespace(Some("http://example.com"))
335+ .finish()
336+ }
337+ after(cx) {
338+ div(&cx)
339+ .namespace(Some("http://example.net"))
340+ .finish()
250 }
341 }
251 }
342 }
252}
343}