diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f476806013..006318776c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -239,3 +239,62 @@ jobs: with: command: rustc args: --manifest-path=druid-shell/Cargo.toml --features=x11 -- -D warnings + + test-stable-wasm: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [macOS-latest, windows-2019, ubuntu-latest] + + name: cargo test stable (wasm32) + steps: + - uses: actions/checkout@v2 + + - name: install cairo + run: brew install cairo + if: contains(matrix.os, 'mac') + + - name: install libgtk-dev + run: | + sudo apt update + sudo apt install libgtk-3-dev + if: contains(matrix.os, 'ubuntu') + + - name: install deps + run: cargo install wasm-pack + + - name: install stable toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + target: wasm32-unknown-unknown + components: clippy + profile: minimal + override: true + + - name: cargo clippy (wasm32) + uses: actions-rs/cargo@v1 + with: + command: clippy + args: --all --target wasm32-unknown-unknown -- -D warnings + + - name: cargo test compile druid-shell (wasm32) + uses: actions-rs/cargo@v1 + with: + command: test + args: --manifest-path=druid-shell/Cargo.toml --no-run --target wasm32-unknown-unknown + + - name: cargo test compile druid (wasm32) + uses: actions-rs/cargo@v1 + with: + command: test + args: --manifest-path=druid/Cargo.toml --no-run --target wasm32-unknown-unknown + + - name: wasm-pack build examples + run: wasm-pack build --target web druid/examples/wasm + + - name: Run rustc -D warnings in druid/examples/wasm + uses: actions-rs/cargo@v1 + with: + command: rustc + args: --manifest-path=druid/examples/wasm/Cargo.toml -- -D warnings diff --git a/Cargo.lock b/Cargo.lock index d32590f0d6..b3f959cf4a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -156,6 +156,24 @@ name = "color_quant" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "console_error_panic_hook" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", + "wasm-bindgen 0.2.60 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "console_log" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "web-sys 0.3.37 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "core-foundation" version = "0.6.4" @@ -278,7 +296,7 @@ dependencies = [ [[package]] name = "deflate" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "adler32 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", @@ -325,13 +343,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" name = "druid" version = "0.6.0" dependencies = [ + "console_log 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", "druid-derive 0.4.0", "druid-shell 0.6.0", "fluent-bundle 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", "fluent-langneg 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)", "fluent-syntax 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)", "fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", - "image 0.23.2 (registry+https://github.com/rust-lang/crates.io-index)", + "image 0.23.3 (registry+https://github.com/rust-lang/crates.io-index)", + "instant 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", "simple_logger 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)", "unic-langid 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -365,17 +385,33 @@ dependencies = [ "glib-sys 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", "gtk 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", "gtk-sys 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)", + "instant 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "js-sys 0.3.37 (registry+https://github.com/rust-lang/crates.io-index)", "kurbo 0.5.11 (registry+https://github.com/rust-lang/crates.io-index)", "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", "objc 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)", "piet-common 0.0.12 (registry+https://github.com/rust-lang/crates.io-index)", "time 0.2.9 (registry+https://github.com/rust-lang/crates.io-index)", + "wasm-bindgen 0.2.60 (registry+https://github.com/rust-lang/crates.io-index)", + "web-sys 0.3.37 (registry+https://github.com/rust-lang/crates.io-index)", "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", "wio 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", "xcb 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "druid-wasm-examples" +version = "0.1.0" +dependencies = [ + "console_error_panic_hook 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "druid 0.6.0", + "instant 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "simple_logger 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "wasm-bindgen 0.2.60 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "dxgi" version = "0.1.7" @@ -418,7 +454,7 @@ dependencies = [ "intl-memoizer 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "intl_pluralrules 6.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "rental 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)", - "smallvec 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "smallvec 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "unic-langid 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -742,7 +778,7 @@ dependencies = [ [[package]] name = "image" -version = "0.23.2" +version = "0.23.3" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "bytemuck 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -752,7 +788,7 @@ dependencies = [ "num-iter 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)", "num-rational 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)", "num-traits 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", - "png 0.16.1 (registry+https://github.com/rust-lang/crates.io-index)", + "png 0.16.2 (registry+https://github.com/rust-lang/crates.io-index)", "scoped_threadpool 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)", "tiff 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -765,6 +801,14 @@ dependencies = [ "adler32 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "instant" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "web-sys 0.3.37 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "intl-memoizer" version = "0.4.0" @@ -1072,12 +1116,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "png" -version = "0.16.1" +version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "crc32fast 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", - "deflate 0.8.3 (registry+https://github.com/rust-lang/crates.io-index)", + "deflate 0.8.4 (registry+https://github.com/rust-lang/crates.io-index)", "inflate 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -1247,12 +1291,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "serde" -version = "1.0.105" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "serde_derive" -version = "1.0.105" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "proc-macro2 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1262,12 +1306,12 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.50" +version = "1.0.51" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "itoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", "ryu 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.105 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.106 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -1314,7 +1358,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "smallvec" -version = "1.2.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] @@ -1324,7 +1368,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "standback" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] @@ -1347,8 +1391,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "proc-macro2 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)", "quote 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.105 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_derive 1.0.105 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.106 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive 1.0.106 (registry+https://github.com/rust-lang/crates.io-index)", "syn 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -1360,9 +1404,9 @@ dependencies = [ "base-x 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", "proc-macro2 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)", "quote 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.105 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_derive 1.0.105 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_json 1.0.50 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.106 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive 1.0.106 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.51 (registry+https://github.com/rust-lang/crates.io-index)", "sha1 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", "syn 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -1409,7 +1453,7 @@ dependencies = [ "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.68 (registry+https://github.com/rust-lang/crates.io-index)", "rustversion 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", - "standback 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "standback 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", "stdweb 0.4.20 (registry+https://github.com/rust-lang/crates.io-index)", "time-macros 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1662,6 +1706,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum cmake 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)" = "81fb25b677f8bf1eb325017cb6bb8452f87969db0fedb4f757b297bee78a7c62" "checksum cocoa 0.20.0 (registry+https://github.com/rust-lang/crates.io-index)" = "0a4736c86d51bd878b474400d9ec888156f4037015f5d09794fab9f26eab1ad4" "checksum color_quant 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0dbbb57365263e881e805dc77d94697c9118fd94d8da011240555aa7b23445bd" +"checksum console_error_panic_hook 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "b8d976903543e0c48546a91908f21588a680a8c8f984df9a5d69feccb2b2a211" +"checksum console_log 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1e7871d2947441b0fdd8e2bd1ce2a2f75304f896582c0d572162d48290683c48" "checksum core-foundation 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)" = "25b9e03f145fd4f2bf705e07b900cd41fc636598fe5dc452fd0db1441c3f496d" "checksum core-foundation 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "57d24c7a13c43e870e37c1556b74555437870a04514f7685f5b354e090567171" "checksum core-foundation-sys 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e7ca8a5221364ef15ce201e8ed2f609fc312682a8f4e0e3d4aa5879764e0fa3b" @@ -1675,7 +1721,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum crossbeam-queue 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c695eeca1e7173472a32221542ae469b3e9aac3a4fc81f7696bcad82029493db" "checksum crossbeam-utils 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8" "checksum data-url 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d33fe99ccedd6e84bc035f1931bb2e6be79739d6242bd895e7311c886c50dc9c" -"checksum deflate 0.8.3 (registry+https://github.com/rust-lang/crates.io-index)" = "050ef6de42a33903b30a7497b76b40d3d58691d4d3eec355348c122444a388f0" +"checksum deflate 0.8.4 (registry+https://github.com/rust-lang/crates.io-index)" = "e7e5d2a2273fed52a7f947ee55b092c4057025d7a3e04e5ecdbd25d6c3fb1bd7" "checksum direct2d 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7fa6ff10857eb253d1ae16987ebfd27372f4129b0c7a3fa41466fbdf7e453e75" "checksum direct3d11 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "315aa929e68ba066cb6fb86f1b22af24f517e02fd9b5734c4d07e42cb9f4aefa" "checksum directwrite 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "8cdcd739e9351c411b8caf5cab32a27c818cfe06260595da121382ecdd22083d" @@ -1715,8 +1761,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum harfbuzz-sys 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "212d74cab8498b2d15700b694fb38f77562869d05e1f8b602dd05221a1ca2d63" "checksum harfbuzz_rs 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cab35982090055087fad29795c465b33e8cf201bda50bfa008311ffe88630f16" "checksum hermit-abi 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "725cf19794cf90aa94e65050cb4191ff5d8fa87a498383774c47b332e3af952e" -"checksum image 0.23.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9062b90712d25bc6bb165d110aa59c6b47c849246e341e7b86a98daff9d49f60" +"checksum image 0.23.3 (registry+https://github.com/rust-lang/crates.io-index)" = "bfc5483f8d5afd3653b38a196c52294dcb239c3e1a5bade1990353ea13bcf387" "checksum inflate 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)" = "1cdb29978cc5797bd8dcc8e5bf7de604891df2a8dc576973d71a281e916db2ff" +"checksum instant 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6c346c299e3fe8ef94dc10c2c0253d858a69aac1245157a3bf4125915d528caf" "checksum intl-memoizer 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "9867e2d65d82936ef34217ed0f87b639a94384e93a0676158142c861c705391f" "checksum intl_pluralrules 6.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d82c14d8eece42c03353e0ce86a4d3f97b1f1cef401e4d962dca6c6214a85002" "checksum itoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)" = "b8b7a7c0c47db5545ed3fef7468ee7bb5b74691498139e4b3f6a20685dc6dd8e" @@ -1752,7 +1799,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum piet-web 0.0.12 (registry+https://github.com/rust-lang/crates.io-index)" = "a63b986af9dc8ebfd8267cde277b0ae62af322ba877466422f450cabf1678e22" "checksum pin-utils 0.1.0-alpha.4 (registry+https://github.com/rust-lang/crates.io-index)" = "5894c618ce612a3fa23881b152b608bafb8c56cfc22f434a3ba3120b40f7b587" "checksum pkg-config 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)" = "05da548ad6865900e60eaba7f589cc0783590a92e940c26953ff81ddbab2d677" -"checksum png 0.16.1 (registry+https://github.com/rust-lang/crates.io-index)" = "46060468187c21c00ffa2a920690b29997d7fd543f5a4d400461e4a7d4fccde8" +"checksum png 0.16.2 (registry+https://github.com/rust-lang/crates.io-index)" = "910f09135b1ed14bb16be445a8c23ddf0777eca485fbfc7cee00d81fecab158a" "checksum proc-macro-hack 0.5.15 (registry+https://github.com/rust-lang/crates.io-index)" = "0d659fe7c6d27f25e9d80a1a094c223f5246f6a6596453e09d7229bf42750b63" "checksum proc-macro-nested 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "8e946095f9d3ed29ec38de908c22f95d9ac008e424c7bcae54c75a79c527c694" "checksum proc-macro2 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)" = "df246d292ff63439fea9bc8c0a270bed0e390d5ebd4db4ba15aba81111b5abe3" @@ -1775,18 +1822,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum scopeguard 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" "checksum semver 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" "checksum semver-parser 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" -"checksum serde 1.0.105 (registry+https://github.com/rust-lang/crates.io-index)" = "e707fbbf255b8fc8c3b99abb91e7257a622caeb20a9818cbadbeeede4e0932ff" -"checksum serde_derive 1.0.105 (registry+https://github.com/rust-lang/crates.io-index)" = "ac5d00fc561ba2724df6758a17de23df5914f20e41cb00f94d5b7ae42fffaff8" -"checksum serde_json 1.0.50 (registry+https://github.com/rust-lang/crates.io-index)" = "78a7a12c167809363ec3bd7329fc0a3369056996de43c4b37ef3cd54a6ce4867" +"checksum serde 1.0.106 (registry+https://github.com/rust-lang/crates.io-index)" = "36df6ac6412072f67cf767ebbde4133a5b2e88e76dc6187fa7104cd16f783399" +"checksum serde_derive 1.0.106 (registry+https://github.com/rust-lang/crates.io-index)" = "9e549e3abf4fb8621bd1609f11dfc9f5e50320802273b12f3811a67e6716ea6c" +"checksum serde_json 1.0.51 (registry+https://github.com/rust-lang/crates.io-index)" = "da07b57ee2623368351e9a0488bb0b261322a15a6e0ae53e243cbdc0f4208da9" "checksum servo-freetype-sys 4.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "2c4ccb6d0d32d277d3ef7dea86203d8210945eb7a45fba89dd445b3595dd0dfc" "checksum sha1 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2579985fda508104f7587689507983eadd6a6e84dd35d6d115361f530916fa0d" "checksum simple_logger 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fea0c4611f32f4c2bac73754f22dca1f57e6c1945e0590dae4e5f2a077b92367" "checksum simplecss 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "596554e63596d556a0dbd681416342ca61c75f1a45203201e7e77d3fa2fa9014" "checksum siphasher 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "0b8de496cf83d4ed58b6be86c3a275b8602f6ffe98d3024a869e124147a9a3ac" "checksum slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" -"checksum smallvec 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5c2fb2ec9bcd216a5b0d0ccf31ab17b5ed1d627960edff65bbe95d3ce221cefc" +"checksum smallvec 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "05720e22615919e4734f6a99ceae50d00226c3c5aca406e102ebc33298214e0a" "checksum stable_deref_trait 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "dba1a27d3efae4351c8051072d619e3ade2820635c3958d826bfea39d59b54c8" -"checksum standback 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4edf667ea8f60afc06d6aeec079d20d5800351109addec1faea678a8663da4e1" +"checksum standback 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "ee531c64ad0f80d289504bd32fb047f42a9e957cda584276ab96eb587e9abac3" "checksum stdweb 0.4.20 (registry+https://github.com/rust-lang/crates.io-index)" = "d022496b16281348b52d0e30ae99e01a73d737b2f45d38fed4edf79f9325a1d5" "checksum stdweb-derive 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "c87a60a40fccc84bef0652345bbbbbe20a605bf5d0ce81719fc476f5c03b50ef" "checksum stdweb-internal-macros 0.2.9 (registry+https://github.com/rust-lang/crates.io-index)" = "58fa5ff6ad0d98d1ffa8cb115892b6e69d67799f6763e162a1c9db421dc22e11" diff --git a/Cargo.toml b/Cargo.toml index 8f29bbd1c2..790c26d3f8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,4 +4,5 @@ members = [ "druid-shell", "druid-derive", "docs/book_examples", + "druid/examples/wasm", ] diff --git a/druid-shell/Cargo.toml b/druid-shell/Cargo.toml index ce8d3c128a..abf821cb1a 100644 --- a/druid-shell/Cargo.toml +++ b/druid-shell/Cargo.toml @@ -23,6 +23,8 @@ time = "0.2.7" cfg-if = "0.1.10" # NOTE: if changing, ensure version is compatible with the version in piet kurbo = "0.5.11" +# NOTE: This defaults to mimicking std::time on non-wasm targets +instant = { version = "0.1", features = [ "wasm-bindgen" ] } cairo-rs = { version = "0.8.1", default_features = false, optional = true } cairo-sys-rs = { version = "0.9.2", default_features = false, optional = true } @@ -60,3 +62,8 @@ gtk-sys = "0.9.0" [target.'cfg(target_os="linux")'.dependencies.gtk] version = "0.8.1" features = ["v3_20"] + +[target.'cfg(target_arch="wasm32")'.dependencies] +wasm-bindgen = "0.2.59" +js-sys = "0.3.36" +web-sys = { version = "0.3.36", features = ["Window", "MouseEvent", "CssStyleDeclaration", "WheelEvent", "KeyEvent", "KeyboardEvent"] } diff --git a/druid-shell/examples/shello.rs b/druid-shell/examples/shello.rs index d1ca52cdbe..990fba4d03 100644 --- a/druid-shell/examples/shello.rs +++ b/druid-shell/examples/shello.rs @@ -64,7 +64,7 @@ impl WinHandler for HelloState { } fn key_down(&mut self, event: KeyEvent) -> bool { - let deadline = std::time::Instant::now() + std::time::Duration::from_millis(500); + let deadline = instant::Instant::now() + std::time::Duration::from_millis(500); let id = self.handle.request_timer(deadline); println!("keydown: {:?}, timer id = {:?}", event, id); diff --git a/druid-shell/src/keyboard.rs b/druid-shell/src/keyboard.rs index 217490be70..eddd0f0d06 100644 --- a/druid-shell/src/keyboard.rs +++ b/druid-shell/src/keyboard.rs @@ -41,12 +41,12 @@ pub struct KeyEvent { impl KeyEvent { /// Create a new `KeyEvent` struct. This accepts either &str or char for the last /// two arguments. - pub(crate) fn new( + pub(crate) fn new<'a>( key_code: impl Into, is_repeat: bool, mods: KeyModifiers, - text: impl Into, - unmodified_text: impl Into, + text: impl Into>, + unmodified_text: impl Into>, ) -> Self { let text = match text.into() { StrOrChar::Char(c) => c.into(), @@ -176,25 +176,25 @@ impl From for TinyStr { /// A type we use in the constructor of `KeyEvent`, specifically to avoid exposing /// internals. -pub enum StrOrChar { +pub enum StrOrChar<'a> { Char(char), - Str(&'static str), + Str(&'a str), } -impl From<&'static str> for StrOrChar { - fn from(src: &'static str) -> Self { +impl<'a> From<&'a str> for StrOrChar<'a> { + fn from(src: &'a str) -> Self { StrOrChar::Str(src) } } -impl From for StrOrChar { - fn from(src: char) -> StrOrChar { +impl From for StrOrChar<'static> { + fn from(src: char) -> StrOrChar<'static> { StrOrChar::Char(src) } } -impl From> for StrOrChar { - fn from(src: Option) -> StrOrChar { +impl From> for StrOrChar<'static> { + fn from(src: Option) -> StrOrChar<'static> { match src { Some(c) => StrOrChar::Char(c), None => StrOrChar::Str(""), diff --git a/druid-shell/src/platform/mod.rs b/druid-shell/src/platform/mod.rs index 71048ce459..7cff27496e 100644 --- a/druid-shell/src/platform/mod.rs +++ b/druid-shell/src/platform/mod.rs @@ -27,5 +27,8 @@ cfg_if::cfg_if! { } else if #[cfg(target_os = "linux")] { mod gtk; pub use self::gtk::*; + } else if #[cfg(target_arch = "wasm32")] { + mod web; + pub use web::*; } } diff --git a/druid-shell/src/platform/web/application.rs b/druid-shell/src/platform/web/application.rs new file mode 100644 index 0000000000..fa3ddd98ba --- /dev/null +++ b/druid-shell/src/platform/web/application.rs @@ -0,0 +1,39 @@ +// Copyright 2020 The xi-editor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Web implementation of features at the application scope. + +use super::clipboard::Clipboard; +use crate::application::AppHandler; + +pub struct Application; + +impl Application { + pub fn new(_handler: Option>) -> Application { + Application + } + + pub fn run(&mut self) {} + + pub fn quit() {} + + pub fn clipboard() -> Clipboard { + Clipboard + } + + pub fn get_locale() -> String { + //TODO ahem + "en-US".into() + } +} diff --git a/druid-shell/src/platform/web/clipboard.rs b/druid-shell/src/platform/web/clipboard.rs new file mode 100644 index 0000000000..0cbb9974e0 --- /dev/null +++ b/druid-shell/src/platform/web/clipboard.rs @@ -0,0 +1,60 @@ +// Copyright 2020 The xi-editor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Interactions with the browser pasteboard. + +use crate::clipboard::{ClipboardFormat, FormatId}; + +/// The browser clipboard. +#[derive(Debug, Clone, Default)] +pub struct Clipboard; + +impl Clipboard { + /// Put a string onto the system clipboard. + pub fn put_string(&mut self, _s: impl AsRef) { + log::warn!("unimplemented"); + } + + /// Put multi-format data on the system clipboard. + pub fn put_formats(&mut self, _formats: &[ClipboardFormat]) { + log::warn!("unimplemented"); + } + + /// Get a string from the system clipboard, if one is available. + pub fn get_string(&self) -> Option { + log::warn!("unimplemented"); + None + } + + /// Given a list of supported clipboard types, returns the supported type which has + /// highest priority on the system clipboard, or `None` if no types are supported. + pub fn preferred_format(&self, _formats: &[FormatId]) -> Option { + log::warn!("unimplemented"); + None + } + + /// Return data in a given format, if available. + /// + /// It is recommended that the `fmt` argument be a format returned by + /// [`Clipboard::preferred_format`] + pub fn get_format(&self, _format: FormatId) -> Option> { + log::warn!("unimplemented"); + None + } + + pub fn available_type_names(&self) -> Vec { + log::warn!("unimplemented"); + Vec::new() + } +} diff --git a/druid-shell/src/platform/web/error.rs b/druid-shell/src/platform/web/error.rs new file mode 100644 index 0000000000..5adef2a490 --- /dev/null +++ b/druid-shell/src/platform/web/error.rs @@ -0,0 +1,50 @@ +// Copyright 2020 The xi-editor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Web platform errors. + +use wasm_bindgen::JsValue; + +#[derive(Debug, Clone)] +pub enum Error { + NoWindow, + NoDocument, + Js(JsValue), + JsCast, + NoElementById(String), + NoContext, + Unimplemented, +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + Error::NoWindow => write!(f, "No global window found"), + Error::NoDocument => write!(f, "No global document found"), + Error::Js(err) => write!(f, "JavaScript error: {:?}", err.as_string()), + Error::JsCast => write!(f, "JavaScript cast error"), + Error::NoElementById(err) => write!(f, "get_element_by_id error: {}", err), + Error::NoContext => write!(f, "Failed to get a draw context"), + Error::Unimplemented => write!(f, "Requested an unimplemented feature"), + } + } +} + +impl From for Error { + fn from(js: JsValue) -> Error { + Error::Js(js) + } +} + +impl std::error::Error for Error {} diff --git a/druid-shell/src/platform/web/keycodes.rs b/druid-shell/src/platform/web/keycodes.rs new file mode 100644 index 0000000000..13ba2bc653 --- /dev/null +++ b/druid-shell/src/platform/web/keycodes.rs @@ -0,0 +1,600 @@ +// Copyright 2020 The xi-editor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Web keycode handling. + +use web_sys::{KeyEvent, KeyboardEvent}; + +use crate::keycodes::KeyCode; + +pub type RawKeyCode = u32; + +//const LOC_STANDARD: u32 = KeyboardEvent::DOM_KEY_LOCATION_STANDARD; +const LOC_LEFT: u32 = KeyboardEvent::DOM_KEY_LOCATION_LEFT; +const LOC_RIGHT: u32 = KeyboardEvent::DOM_KEY_LOCATION_RIGHT; +const LOC_NUMPAD: u32 = KeyboardEvent::DOM_KEY_LOCATION_NUMPAD; + +macro_rules! map_keys { + ($( ($id:path, ($loc:tt)) => $code:path),*) => { + impl From for u32 { + fn from(src: KeyCode) -> u32 { + match src { + $( + $code => $id + ),*, + other => { + log::warn!("Unrecognized druid KeyCode: {:?}", other); + u32::max_value() + } + } + } + } + + // The first number is the keycode and the second is the location + impl From<(u32, u32)> for KeyCode { + fn from(src: (u32, u32)) -> KeyCode { + match src { + $( + ($id, $loc) => $code + ),*, + other => KeyCode::Unknown(other.0), + } + } + } + } +} + +map_keys! { + //(KeyEvent::DOM_VK_CANCEL, (_)) => KeyCode::Cancel, + //(KeyEvent::DOM_VK_HELP, (_)) => KeyCode::Help, + (KeyEvent::DOM_VK_BACK_SPACE, (_)) => KeyCode::Backspace, + (KeyEvent::DOM_VK_TAB, (_)) => KeyCode::Tab, + //(KeyEvent::DOM_VK_CLEAR, (_)) => KeyCode::NumLock, + (KeyEvent::DOM_VK_RETURN, (_)) => KeyCode::Return, + (KeyEvent::DOM_VK_SHIFT, (LOC_LEFT)) => KeyCode::LeftShift, + (KeyEvent::DOM_VK_SHIFT, (LOC_RIGHT)) => KeyCode::RightShift, + (KeyEvent::DOM_VK_CONTROL, (LOC_LEFT)) => KeyCode::LeftControl, + (KeyEvent::DOM_VK_CONTROL, (LOC_RIGHT)) => KeyCode::RightControl, + (KeyEvent::DOM_VK_ALT, (LOC_LEFT)) => KeyCode::LeftAlt, + (KeyEvent::DOM_VK_ALT, (LOC_RIGHT)) => KeyCode::RightAlt, + (KeyEvent::DOM_VK_PAUSE, (_)) => KeyCode::Pause, + (KeyEvent::DOM_VK_CAPS_LOCK, (_)) => KeyCode::CapsLock, + //(KeyEvent::DOM_VK_KANA, (_)) => KeyCode::KANA, + //(KeyEvent::DOM_VK_HANGUL, (_)) => KeyCode::HANGUL, + //(KeyEvent::DOM_VK_EISU, (_)) => KeyCode::EISU, + //(KeyEvent::DOM_VK_JUNJA, (_)) => KeyCode::JUNJA, + //(KeyEvent::DOM_VK_FINAL, (_)) => KeyCode::FINAL, + //(KeyEvent::DOM_VK_HANJA, (_)) => KeyCode::HANJA, + //(KeyEvent::DOM_VK_KANJI, (_)) => KeyCode::KANJI, + (KeyEvent::DOM_VK_ESCAPE, (_)) => KeyCode::Escape, + //(KeyEvent::DOM_VK_CONVERT, (_)) => KeyCode::CONVERT, + //(KeyEvent::DOM_VK_NONCONVERT, (_)) => KeyCode::NONCONVERT, + //(KeyEvent::DOM_VK_ACCEPT, (_)) => KeyCode::ACCEPT, + //(KeyEvent::DOM_VK_MODECHANGE, (_)) => KeyCode::MODECHANGE, + (KeyEvent::DOM_VK_SPACE, (_)) => KeyCode::Space, + (KeyEvent::DOM_VK_PAGE_UP, (_)) => KeyCode::PageUp, + (KeyEvent::DOM_VK_PAGE_DOWN, (_)) => KeyCode::PageDown, + (KeyEvent::DOM_VK_END, (_)) => KeyCode::End, + (KeyEvent::DOM_VK_HOME, (_)) => KeyCode::Home, + (KeyEvent::DOM_VK_LEFT, (_)) => KeyCode::ArrowLeft, + (KeyEvent::DOM_VK_UP, (_)) => KeyCode::ArrowUp, + (KeyEvent::DOM_VK_RIGHT, (_)) => KeyCode::ArrowRight, + (KeyEvent::DOM_VK_DOWN, (_)) => KeyCode::ArrowDown, + //(KeyEvent::DOM_VK_SELECT, (_)) => KeyCode::SELECT, + //(KeyEvent::DOM_VK_PRINT, (_)) => KeyCode::PrintScreen, + //(KeyEvent::DOM_VK_EXECUTE, (_)) => KeyCode::EXECUTE, + (KeyEvent::DOM_VK_PRINTSCREEN, (_)) => KeyCode::PrintScreen, + (KeyEvent::DOM_VK_INSERT, (_)) => KeyCode::Insert, + (KeyEvent::DOM_VK_DELETE, (_)) => KeyCode::Delete, + (KeyEvent::DOM_VK_0, (_)) => KeyCode::Key0, + (KeyEvent::DOM_VK_1, (_)) => KeyCode::Key1, + (KeyEvent::DOM_VK_2, (_)) => KeyCode::Key2, + (KeyEvent::DOM_VK_3, (_)) => KeyCode::Key3, + (KeyEvent::DOM_VK_4, (_)) => KeyCode::Key4, + (KeyEvent::DOM_VK_5, (_)) => KeyCode::Key5, + (KeyEvent::DOM_VK_6, (_)) => KeyCode::Key6, + (KeyEvent::DOM_VK_7, (_)) => KeyCode::Key7, + (KeyEvent::DOM_VK_8, (_)) => KeyCode::Key8, + (KeyEvent::DOM_VK_9, (_)) => KeyCode::Key9, + //(KeyEvent::DOM_VK_COLON, (_)) => KeyCode::Semicolon, + (KeyEvent::DOM_VK_SEMICOLON, (_)) => KeyCode::Semicolon, + //(KeyEvent::DOM_VK_LESS_THAN, (_)) => KeyCode::Comma, + (KeyEvent::DOM_VK_EQUALS, (_)) => KeyCode::Equals, + //(KeyEvent::DOM_VK_GREATER_THAN, (_)) => KeyCode::Period, + //(KeyEvent::DOM_VK_QUESTION_MARK, (_)) => KeyCode::Slash, + //(KeyEvent::DOM_VK_AT, (_)) => KeyCode::AT, + (KeyEvent::DOM_VK_A, (_)) => KeyCode::KeyA, + (KeyEvent::DOM_VK_B, (_)) => KeyCode::KeyB, + (KeyEvent::DOM_VK_C, (_)) => KeyCode::KeyC, + (KeyEvent::DOM_VK_D, (_)) => KeyCode::KeyD, + (KeyEvent::DOM_VK_E, (_)) => KeyCode::KeyE, + (KeyEvent::DOM_VK_F, (_)) => KeyCode::KeyF, + (KeyEvent::DOM_VK_G, (_)) => KeyCode::KeyG, + (KeyEvent::DOM_VK_H, (_)) => KeyCode::KeyH, + (KeyEvent::DOM_VK_I, (_)) => KeyCode::KeyI, + (KeyEvent::DOM_VK_J, (_)) => KeyCode::KeyJ, + (KeyEvent::DOM_VK_K, (_)) => KeyCode::KeyK, + (KeyEvent::DOM_VK_L, (_)) => KeyCode::KeyL, + (KeyEvent::DOM_VK_M, (_)) => KeyCode::KeyM, + (KeyEvent::DOM_VK_N, (_)) => KeyCode::KeyN, + (KeyEvent::DOM_VK_O, (_)) => KeyCode::KeyO, + (KeyEvent::DOM_VK_P, (_)) => KeyCode::KeyP, + (KeyEvent::DOM_VK_Q, (_)) => KeyCode::KeyQ, + (KeyEvent::DOM_VK_R, (_)) => KeyCode::KeyR, + (KeyEvent::DOM_VK_S, (_)) => KeyCode::KeyS, + (KeyEvent::DOM_VK_T, (_)) => KeyCode::KeyT, + (KeyEvent::DOM_VK_U, (_)) => KeyCode::KeyU, + (KeyEvent::DOM_VK_V, (_)) => KeyCode::KeyV, + (KeyEvent::DOM_VK_W, (_)) => KeyCode::KeyW, + (KeyEvent::DOM_VK_X, (_)) => KeyCode::KeyX, + (KeyEvent::DOM_VK_Y, (_)) => KeyCode::KeyY, + (KeyEvent::DOM_VK_Z, (_)) => KeyCode::KeyZ, + //(KeyEvent::DOM_VK_WIN, (LOC_LEFT)) => KeyCode::LeftMeta, + //(KeyEvent::DOM_VK_WIN, (LOC_RIGHT)) => KeyCode::RightMeta, + //(KeyEvent::DOM_VK_CONTEXT_MENU, (_)) => KeyCode::CONTEXT_MENU, + //(KeyEvent::DOM_VK_SLEEP, (_)) => KeyCode::SLEEP, + (KeyEvent::DOM_VK_NUMPAD0, (_)) => KeyCode::Numpad0, + (KeyEvent::DOM_VK_NUMPAD1, (_)) => KeyCode::Numpad1, + (KeyEvent::DOM_VK_NUMPAD2, (_)) => KeyCode::Numpad2, + (KeyEvent::DOM_VK_NUMPAD3, (_)) => KeyCode::Numpad3, + (KeyEvent::DOM_VK_NUMPAD4, (_)) => KeyCode::Numpad4, + (KeyEvent::DOM_VK_NUMPAD5, (_)) => KeyCode::Numpad5, + (KeyEvent::DOM_VK_NUMPAD6, (_)) => KeyCode::Numpad6, + (KeyEvent::DOM_VK_NUMPAD7, (_)) => KeyCode::Numpad7, + (KeyEvent::DOM_VK_NUMPAD8, (_)) => KeyCode::Numpad8, + (KeyEvent::DOM_VK_NUMPAD9, (_)) => KeyCode::Numpad9, + (KeyEvent::DOM_VK_MULTIPLY, (LOC_NUMPAD)) => KeyCode::NumpadMultiply, + (KeyEvent::DOM_VK_ADD, (LOC_NUMPAD)) => KeyCode::NumpadAdd, + //(KeyEvent::DOM_VK_SEPARATOR, (_)) => KeyCode::SEPARATOR, + (KeyEvent::DOM_VK_SUBTRACT, (LOC_NUMPAD)) => KeyCode::NumpadSubtract, + (KeyEvent::DOM_VK_DECIMAL, (LOC_NUMPAD)) => KeyCode::NumpadDecimal, + (KeyEvent::DOM_VK_DIVIDE, (LOC_NUMPAD)) => KeyCode::NumpadDivide, + (KeyEvent::DOM_VK_F1, (_)) => KeyCode::F1, + (KeyEvent::DOM_VK_F2, (_)) => KeyCode::F2, + (KeyEvent::DOM_VK_F3, (_)) => KeyCode::F3, + (KeyEvent::DOM_VK_F4, (_)) => KeyCode::F4, + (KeyEvent::DOM_VK_F5, (_)) => KeyCode::F5, + (KeyEvent::DOM_VK_F6, (_)) => KeyCode::F6, + (KeyEvent::DOM_VK_F7, (_)) => KeyCode::F7, + (KeyEvent::DOM_VK_F8, (_)) => KeyCode::F8, + (KeyEvent::DOM_VK_F9, (_)) => KeyCode::F9, + (KeyEvent::DOM_VK_F10, (_)) => KeyCode::F10, + (KeyEvent::DOM_VK_F11, (_)) => KeyCode::F11, + (KeyEvent::DOM_VK_F12, (_)) => KeyCode::F12, + //(KeyEvent::DOM_VK_F13, (_)) => KeyCode::F13, + //(KeyEvent::DOM_VK_F14, (_)) => KeyCode::F14, + //(KeyEvent::DOM_VK_F15, (_)) => KeyCode::F15, + //(KeyEvent::DOM_VK_F16, (_)) => KeyCode::F16, + //(KeyEvent::DOM_VK_F17, (_)) => KeyCode::F17, + //(KeyEvent::DOM_VK_F18, (_)) => KeyCode::F18, + //(KeyEvent::DOM_VK_F19, (_)) => KeyCode::F19, + //(KeyEvent::DOM_VK_F20, (_)) => KeyCode::F20, + //(KeyEvent::DOM_VK_F21, (_)) => KeyCode::F21, + //(KeyEvent::DOM_VK_F22, (_)) => KeyCode::F22, + //(KeyEvent::DOM_VK_F23, (_)) => KeyCode::F23, + //(KeyEvent::DOM_VK_F24, (_)) => KeyCode::F24, + (KeyEvent::DOM_VK_NUM_LOCK, (_)) => KeyCode::NumLock, + (KeyEvent::DOM_VK_SCROLL_LOCK, (_)) => KeyCode::ScrollLock, + //(KeyEvent::DOM_VK_WIN_OEM_FJ_JISHO, (_)) => KeyCode::WIN_OEM_FJ_JISHO, + //(KeyEvent::DOM_VK_WIN_OEM_FJ_MASSHOU, (_)) => KeyCode::WIN_OEM_FJ_MASSHOU, + //(KeyEvent::DOM_VK_WIN_OEM_FJ_TOUROKU, (_)) => KeyCode::WIN_OEM_FJ_TOUROKU, + //(KeyEvent::DOM_VK_WIN_OEM_FJ_LOYA, (_)) => KeyCode::WIN_OEM_FJ_LOYA, + //(KeyEvent::DOM_VK_WIN_OEM_FJ_ROYA, (_)) => KeyCode::WIN_OEM_FJ_ROYA, + //(KeyEvent::DOM_VK_CIRCUMFLEX, (_)) => KeyCode::CIRCUMFLEX, + //(KeyEvent::DOM_VK_EXCLAMATION, (_)) => KeyCode::Key1, + //(KeyEvent::DOM_VK_DOUBLE_QUOTE, (_)) => KeyCode::Quote, + //(KeyEvent::DOM_VK_HASH, (_)) => KeyCode::Key3, + //(KeyEvent::DOM_VK_DOLLAR, (_)) => KeyCode::Key4, + //(KeyEvent::DOM_VK_PERCENT, (_)) => KeyCode::Key5, + //(KeyEvent::DOM_VK_AMPERSAND, (_)) => KeyCode::Key7, + //(KeyEvent::DOM_VK_UNDERSCORE, (_)) => KeyCode::Minus, + //(KeyEvent::DOM_VK_OPEN_PAREN, (_)) => KeyCode::Key9, + //(KeyEvent::DOM_VK_CLOSE_PAREN, (_)) => KeyCode::Key0, + //(KeyEvent::DOM_VK_ASTERISK, (_)) => KeyCode::Key8, + //(KeyEvent::DOM_VK_PLUS, (_)) => KeyCode::Equals, + //(KeyEvent::DOM_VK_PIPE, (_)) => KeyCode::Backslash, + (KeyEvent::DOM_VK_HYPHEN_MINUS, (_)) => KeyCode::Minus, + //(KeyEvent::DOM_VK_OPEN_CURLY_BRACKET, (_)) => KeyCode::LeftBracket, + //(KeyEvent::DOM_VK_CLOSE_CURLY_BRACKET, (_)) => KeyCode::RightBracket, + (KeyEvent::DOM_VK_TILDE, (_)) => KeyCode::Backtick, + //(KeyEvent::DOM_VK_VOLUME_MUTE, (_)) => KeyCode::VOLUME_MUTE, + //(KeyEvent::DOM_VK_VOLUME_DOWN, (_)) => KeyCode::VOLUME_DOWN, + //(KeyEvent::DOM_VK_VOLUME_UP, (_)) => KeyCode::VOLUME_UP, + (KeyEvent::DOM_VK_COMMA, (_)) => KeyCode::Comma, + (KeyEvent::DOM_VK_PERIOD, (_)) => KeyCode::Period, + (KeyEvent::DOM_VK_SLASH, (_)) => KeyCode::Slash, + //(KeyEvent::DOM_VK_BACK_QUOTE, (_)) => KeyCode::Quote, + (KeyEvent::DOM_VK_OPEN_BRACKET, (_)) => KeyCode::LeftBracket, + (KeyEvent::DOM_VK_BACK_SLASH, (_)) => KeyCode::Backslash, + (KeyEvent::DOM_VK_CLOSE_BRACKET, (_)) => KeyCode::RightBracket, + (KeyEvent::DOM_VK_QUOTE, (_)) => KeyCode::Quote, + (KeyEvent::DOM_VK_META, (LOC_LEFT)) => KeyCode::LeftMeta, + (KeyEvent::DOM_VK_META, (LOC_RIGHT)) => KeyCode::RightMeta + //(KeyEvent::DOM_VK_ALTGR, (_)) => KeyCode::RightAlt, + //(KeyEvent::DOM_VK_WIN_ICO_HELP, (_)) => KeyCode::WIN_ICO_HELP, + //(KeyEvent::DOM_VK_WIN_ICO_00, (_)) => KeyCode::WIN_ICO_00, + //(KeyEvent::DOM_VK_PROCESSKEY, (_)) => KeyCode::PROCESSKEY, + //(KeyEvent::DOM_VK_WIN_ICO_CLEAR, (_)) => KeyCode::WIN_ICO_CLEAR, + //(KeyEvent::DOM_VK_WIN_OEM_RESET, (_)) => KeyCode::WIN_OEM_RESET, + //(KeyEvent::DOM_VK_WIN_OEM_JUMP, (_)) => KeyCode::WIN_OEM_JUMP, + //(KeyEvent::DOM_VK_WIN_OEM_PA1, (_)) => KeyCode::WIN_OEM_PA1, + //(KeyEvent::DOM_VK_WIN_OEM_PA2, (_)) => KeyCode::WIN_OEM_PA2, + //(KeyEvent::DOM_VK_WIN_OEM_PA3, (_)) => KeyCode::WIN_OEM_PA3, + //(KeyEvent::DOM_VK_WIN_OEM_WSCTRL, (_)) => KeyCode::WIN_OEM_WSCTRL, + //(KeyEvent::DOM_VK_WIN_OEM_CUSEL, (_)) => KeyCode::WIN_OEM_CUSEL, + //(KeyEvent::DOM_VK_WIN_OEM_ATTN, (_)) => KeyCode::WIN_OEM_ATTN, + //(KeyEvent::DOM_VK_WIN_OEM_FINISH, (_)) => KeyCode::WIN_OEM_FINISH, + //(KeyEvent::DOM_VK_WIN_OEM_COPY, (_)) => KeyCode::WIN_OEM_COPY, + //(KeyEvent::DOM_VK_WIN_OEM_AUTO, (_)) => KeyCode::WIN_OEM_AUTO, + //(KeyEvent::DOM_VK_WIN_OEM_ENLW, (_)) => KeyCode::WIN_OEM_ENLW, + //(KeyEvent::DOM_VK_WIN_OEM_BACKTAB, (_)) => KeyCode::WIN_OEM_BACKTAB, + //(KeyEvent::DOM_VK_ATTN, (_)) => KeyCode::ATTN, + //(KeyEvent::DOM_VK_CRSEL, (_)) => KeyCode::CRSEL, + //(KeyEvent::DOM_VK_EXSEL, (_)) => KeyCode::EXSEL, + //(KeyEvent::DOM_VK_EREOF, (_)) => KeyCode::EREOF, + //(KeyEvent::DOM_VK_PLAY, (_)) => KeyCode::PLAY, + //(KeyEvent::DOM_VK_ZOOM, (_)) => KeyCode::ZOOM, + //(KeyEvent::DOM_VK_PA1, (_)) => KeyCode::PA1, + //(KeyEvent::DOM_VK_WIN_OEM_CLEAR, (_)) => KeyCode::WIN_OEM_CLEAR, +} + +/// A helper to convert the key string to the text that it is supposed to represent. +pub(crate) fn key_to_text(key: &str) -> &str { + // The following list was taken from + // https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values + match key { + // Whitespace + "Enter" => "\n", + "Tab" => "\t", + + // Numpad Keys + "Decimal" => ".", + "Multiply" => "*", + "Add" => "+", + "Divide" => "/", + "Subtract" => "-", + + // Special + "Unidentified" + // Modifiers + | "Alt" + | "AltGraph" + | "CapsLock" + | "Control" + | "Fn" + | "FnLock" + | "Hyper" + | "Meta" + | "NumLock" + | "ScrollLock" + | "Shift" + | "Super" + | "Symbol" + | "SymbolLock" + // Navigation + | "ArrowDown" + | "ArrowLeft" + | "ArrowRight" + | "ArrowUp" + | "End" + | "Home" + | "PageDown" + | "PageUp" + // Editing + | "Backspace" + | "Clear" + | "Copy" + | "CrSel" + | "Cut" + | "Delete" + | "EraseEof" + | "ExSel" + | "Insert" + | "Paste" + | "Redo" + | "Undo" + // UI + | "Accept" + | "Again" + | "Attn" + | "Cancel" + | "ContextMenu" + | "Escape" + | "Execute" + | "Find" + | "Finish" + | "Help" + | "Pause" + | "Play" + | "Props" + | "Select" + | "ZoomIn" + | "ZoomOut" + // Device + | "BrightnessDown" + | "BrightnessUp" + | "Eject" + | "LogOff" + | "Power" + | "PowerOff" + | "PrintScreen" + | "Hibernate" + | "Standby" + | "WakeUp" + // IME + | "AllCandidates" + | "Alphanumeric" + | "CodeInput" + | "Compose" + | "Convert" + | "Dead" + | "FinalMode" + | "GroupFirst" + | "GroupLast" + | "GroupNext" + | "GroupPrevious" + | "ModeChange" + | "NextCandidate" + | "NonConvert" + | "PreviousCandidate" + | "Process" + | "SingleCandidate" + // Korean + | "HangulMode" + | "HanjaMode" + | "JunjaMode" + // Japanese + | "Eisu" + | "Hankaku" + | "Hiragana" + | "HiraganaKatakana" + | "KanaMode" + | "KanjiMode" + | "Katakana" + | "Romaji" + | "Zenkaku" + | "ZenkakuHanaku" + // Function + | "F1" + | "F2" + | "F3" + | "F4" + | "F5" + | "F6" + | "F7" + | "F8" + | "F9" + | "F10" + | "F11" + | "F12" + | "F13" + | "F14" + | "F15" + | "F16" + | "F17" + | "F18" + | "F19" + | "F20" + | "Soft1" + | "Soft2" + | "Soft3" + | "Soft4" + // Phone + | "AppSwitch" + | "Call" + | "Camera" + | "CameraFocus" + | "EndCall" + | "GoBack" + | "GoHome" + | "HeadsetHook" + | "LastNumberRedial" + | "Notification" + | "MannerMode" + | "VoiceDial" + // Multimedia + | "ChannelDown" + | "ChannelUp" + | "MediaFastForward" + | "MediaPause" + | "MediaPlay" + | "MediaPlayPause" + | "MediaRecord" + | "MediaRewind" + | "MediaStop" + | "MediaTrackNext" + | "MediaTrackPrevious" + // Audio + | "AudioBalanceLeft" + | "AudioBalanceRight" + | "AudioBassDown" + | "AudioBassBoostDown" + | "AudioBassBoostToggle" + | "AudioBassBoostUp" + | "AudioBassUp" + | "AudioFaderFront" + | "AudioFaderRear" + | "AudioSurroundModeNext" + | "AudioTrebleDown" + | "AudioTrebleUp" + | "AudioVolumeDown" + | "AudioVolumeMute" + | "AudioVolumeUp" + | "MicrophoneToggle" + | "MicrophoneVolumeDown" + | "MicrophoneVolumeMute" + | "MicrophoneVolumeUp" + // TV control + | "TV" + | "TV3DMode" + | "TVAntennaCable" + | "TVAudioDescription" + | "TVAudioDescriptionMixDown" + | "TVAudioDescriptionMixUp" + | "TVContentsMenu" + | "TVDataService" + | "TVInput" + | "TVInputComponent1" + | "TVInputComponent2" + | "TVInputComposite1" + | "TVInputComposite2" + | "TVInputHDMI1" + | "TVInputHDMI2" + | "TVInputHDMI3" + | "TVInputHDMI4" + | "TVInputVGA1" + | "TVMediaContext" + | "TVNetwork" + | "TVNumberEntry" + | "TVPower" + | "TVRadioService" + | "TVSatellite" + | "TVSatelliteBS" + | "TVSatelliteCS" + | "TVSatelliteToggle" + | "TVTerrestrialAnalog" + | "TVTerrestrialDigital" + | "TVTimer" + // Media Control (Possibly repeated) + | "AVRInput" + | "AVRPower" + | "ColorF0Red" + | "ColorF1Green" + | "ColorF2Yellow" + | "ColorF3Blue" + | "ColorF4Grey" + | "ColorF5Brown" + | "ClosedCaptionToggle" + | "Dimmer" + | "DisplaySwap" + | "DVR" + | "Exit" + | "FavoriteClear0" + | "FavoriteClear1" + | "FavoriteClear2" + | "FavoriteClear3" + | "FavoriteRecall0" + | "FavoriteRecall1" + | "FavoriteRecall2" + | "FavoriteRecall3" + | "FavoriteStore0" + | "FavoriteStore1" + | "FavoriteStore2" + | "FavoriteStore3" + | "Guide" + | "GuideNextDay" + | "GuidePreviousDay" + | "Info" + | "InstantReplay" + | "Link" + | "ListProgram" + | "LiveContent" + | "Lock" + | "MediaApps" + | "MediaAudioTrack" + | "MediaLast" + | "MediaSkipBackward" + | "MediaSkipForward" + | "MediaStepBackward" + | "MediaStepForward" + | "MediaTopMenu" + | "NavigateIn" + | "NavigateNext" + | "NavigateOut" + | "NavigatePrevious" + | "NextFavoriteChannel" + | "NextUserProfile" + | "OnDemand" + | "Pairing" + | "PinPDown" + | "PinPMove" + | "PinPToggle" + | "PinPUp" + | "PlaySpeedDown" + | "PlaySpeedReset" + | "PlaySpeedUp" + | "RandomToggle" + | "RcLowBattery" + | "RecordSpeedNext" + | "RfBypass" + | "ScanChannelsToggle" + | "ScreenModeNext" + | "Settings" + | "SplitScreenToggle" + | "STBInput" + | "STBPower" + | "Subtitle" + | "Teletext" + | "VideoModeNext" + | "Wink" + | "ZoomToggle" + // Speech recognition + | "SpeechCorrectionList" + | "SpeechInputToggle" + // Document + | "Close" + | "New" + | "Open" + | "Print" + | "Save" + | "SpellCheck" + | "MailForward" + | "MailReply" + | "MailSend" + // Application selector + | "LaunchCalculator" + | "LaunchCalendar" + | "LaunchContacts" + | "LaunchMail" + | "LaunchMediaPlayer" + | "LaunchMusicPlayer" + | "LaunchMyComputer" + | "LaunchPhone" + | "LaunchScreenSaver" + | "LaunchSpreadsheet" + | "LaunchWebBrowser" + | "LaunchWebCam" + | "LaunchWordProcessor" + | "LaunchApplication1" + | "LaunchApplication2" + | "LaunchApplication3" + | "LaunchApplication4" + | "LaunchApplication5" + | "LaunchApplication6" + | "LaunchApplication7" + | "LaunchApplication8" + | "LaunchApplication9" + | "LaunchApplication10" + | "LaunchApplication11" + | "LaunchApplication12" + | "LaunchApplication13" + | "LaunchApplication14" + | "LaunchApplication15" + | "LaunchApplication16" + // Browser Control + | "BrowserBack" + | "BrowserFavorites" + | "BrowserForward" + | "BrowserHome" + | "BrowserRefresh" + | "BrowserSearch" + | "BrowserStop" + // Numeric keypad + | "Key11" + | "Key12" + | "Separator" => "", + + k => k, + } +} diff --git a/druid-shell/src/platform/web/menu.rs b/druid-shell/src/platform/web/menu.rs new file mode 100644 index 0000000000..0c7928e11f --- /dev/null +++ b/druid-shell/src/platform/web/menu.rs @@ -0,0 +1,56 @@ +// Copyright 2020 The xi-editor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Safe wrapper for menus. + +use crate::hotkey::HotKey; + +/// A menu object, which can be either a top-level menubar or a +/// submenu. +pub struct Menu; + +impl Drop for Menu { + fn drop(&mut self) { + // TODO + } +} + +impl Menu { + pub fn new() -> Menu { + Menu + } + + pub fn new_for_popup() -> Menu { + Menu + } + + pub fn add_dropdown(&mut self, _menu: Menu, _text: &str, _enabled: bool) { + log::warn!("unimplemented"); + } + + pub fn add_item( + &mut self, + _id: u32, + _text: &str, + _key: Option<&HotKey>, + _enabled: bool, + _selected: bool, + ) { + log::warn!("unimplemented"); + } + + pub fn add_separator(&mut self) { + log::warn!("unimplemented"); + } +} diff --git a/druid-shell/src/platform/web/mod.rs b/druid-shell/src/platform/web/mod.rs new file mode 100644 index 0000000000..b588cec083 --- /dev/null +++ b/druid-shell/src/platform/web/mod.rs @@ -0,0 +1,22 @@ +// Copyright 2020 The xi-editor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Web-based platform support + +pub mod application; +pub mod clipboard; +pub mod error; +pub mod keycodes; +pub mod menu; +pub mod window; diff --git a/druid-shell/src/platform/web/window.rs b/druid-shell/src/platform/web/window.rs new file mode 100644 index 0000000000..685d979fb3 --- /dev/null +++ b/druid-shell/src/platform/web/window.rs @@ -0,0 +1,618 @@ +// Copyright 2020 The xi-editor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Web window creation and management. + +use std::any::Any; +use std::cell::{Cell, RefCell}; +use std::ffi::OsString; +use std::rc::{Rc, Weak}; +use std::sync::{Arc, Mutex}; + +use instant::Instant; + +use wasm_bindgen::prelude::*; +use wasm_bindgen::JsCast; + +use crate::kurbo::{Point, Size, Vec2}; + +use crate::piet::RenderContext; + +use super::error::Error; +use super::keycodes::key_to_text; +use super::menu::Menu; +use crate::common_util::IdleCallback; +use crate::dialog::{FileDialogOptions, FileDialogType, FileInfo}; + +use crate::keyboard; +use crate::keycodes::KeyCode; +use crate::mouse::{Cursor, MouseButton, MouseEvent}; +use crate::window::{IdleToken, Text, TimerToken, WinHandler}; +use crate::KeyModifiers; + +// This is a macro instead of a function since KeyboardEvent and MouseEvent has identical functions +// to query modifier key states. +macro_rules! get_modifiers { + ($event:ident) => { + KeyModifiers { + shift: $event.shift_key(), + alt: $event.alt_key(), + ctrl: $event.ctrl_key(), + meta: $event.meta_key(), + } + }; +} + +type Result = std::result::Result; + +const NOMINAL_DPI: f32 = 96.0; + +/// Builder abstraction for creating new windows. +pub struct WindowBuilder { + handler: Option>, + title: String, + cursor: Cursor, + menu: Option, +} + +#[derive(Clone, Default)] +pub struct WindowHandle(Weak); + +/// A handle that can get used to schedule an idle handler. Note that +/// this handle is thread safe. +#[derive(Clone)] +pub struct IdleHandle { + state: Weak, + queue: Arc>>, +} + +enum IdleKind { + Callback(Box), + Token(IdleToken), +} + +struct WindowState { + dpr: Cell, + idle_queue: Arc>>, + handler: RefCell>, + window: web_sys::Window, + canvas: web_sys::HtmlCanvasElement, + context: web_sys::CanvasRenderingContext2d, +} + +impl WindowState { + fn render(&self) -> bool { + self.context + .clear_rect(0.0, 0.0, self.get_width() as f64, self.get_height() as f64); + let mut piet_ctx = piet_common::Piet::new(self.context.clone(), self.window.clone()); + let want_anim_frame = self.handler.borrow_mut().paint(&mut piet_ctx); + if let Err(e) = piet_ctx.finish() { + log::error!("piet error on render: {:?}", e); + } + let res = piet_ctx.finish(); + if let Err(e) = res { + log::error!("EndDraw error: {:?}", e); + } + want_anim_frame + } + + fn process_idle_queue(&self) { + let mut queue = self.idle_queue.lock().expect("process_idle_queue"); + for item in queue.drain(..) { + match item { + IdleKind::Callback(cb) => cb.call(&self.handler), + IdleKind::Token(tok) => self.handler.borrow_mut().idle(tok), + } + } + } + + fn get_width(&self) -> u32 { + self.canvas.offset_width() as u32 + } + + fn get_height(&self) -> u32 { + self.canvas.offset_height() as u32 + } + + fn request_animation_frame(&self, f: impl FnOnce() + 'static) -> Result { + Ok(self + .window + .request_animation_frame(Closure::once_into_js(f).as_ref().unchecked_ref())?) + } + + /// Returns the window size in css units + fn get_window_size_and_dpr(&self) -> (f64, f64, f64) { + let w = &self.window; + let width = w.inner_width().unwrap().as_f64().unwrap(); + let height = w.inner_height().unwrap().as_f64().unwrap(); + let dpr = w.device_pixel_ratio(); + (width, height, dpr) + } +} + +fn setup_mouse_down_callback(ws: &Rc) { + let state = ws.clone(); + register_canvas_event_listener(ws, "mousedown", move |event: web_sys::MouseEvent| { + let button = mouse_button(event.button()).unwrap(); + let event = MouseEvent { + pos: Point::new(event.offset_x() as f64, event.offset_y() as f64), + mods: get_modifiers!(event), + button, + count: 1, + }; + state.handler.borrow_mut().mouse_down(&event); + }); +} + +fn setup_mouse_move_callback(ws: &Rc) { + let state = ws.clone(); + register_canvas_event_listener(ws, "mousemove", move |event: web_sys::MouseEvent| { + let button = mouse_button(event.button()).unwrap(); + let event = MouseEvent { + pos: Point::new(event.offset_x() as f64, event.offset_y() as f64), + mods: get_modifiers!(event), + button, + count: 1, + }; + state.handler.borrow_mut().mouse_move(&event); + }); +} + +fn setup_mouse_up_callback(ws: &Rc) { + let state = ws.clone(); + register_canvas_event_listener(ws, "mouseup", move |event: web_sys::MouseEvent| { + let button = mouse_button(event.button()).unwrap(); + let event = MouseEvent { + pos: Point::new(event.offset_x() as f64, event.offset_y() as f64), + mods: get_modifiers!(event), + button, + count: 0, + }; + state.handler.borrow_mut().mouse_up(&event); + }); +} + +fn setup_scroll_callback(ws: &Rc) { + let state = ws.clone(); + register_canvas_event_listener(ws, "wheel", move |event: web_sys::WheelEvent| { + let delta_mode = event.delta_mode(); + + let dx = event.delta_x(); + let dy = event.delta_y(); + let height = state.canvas.height() as f64; + let width = state.canvas.width() as f64; + + let modifiers = get_modifiers!(event); + let mut handler = state.handler.borrow_mut(); + + // The value 35.0 was manually picked to produce similar behavior to mac/linux. + match delta_mode { + web_sys::WheelEvent::DOM_DELTA_PIXEL => handler.wheel(Vec2::from((dx, dy)), modifiers), + web_sys::WheelEvent::DOM_DELTA_LINE => { + handler.wheel(Vec2::from((35.0 * dx, 35.0 * dy)), modifiers) + } + web_sys::WheelEvent::DOM_DELTA_PAGE => { + handler.wheel(Vec2::from((width * dx, height * dy)), modifiers) + } + _ => log::warn!("Invalid deltaMode in WheelEvent: {}", delta_mode), + } + }); +} + +fn setup_resize_callback(ws: &Rc) { + let state = ws.clone(); + register_window_event_listener(ws, "resize", move |_: web_sys::UiEvent| { + let (css_width, css_height, dpr) = state.get_window_size_and_dpr(); + let physical_width = (dpr * css_width) as u32; + let physical_height = (dpr * css_height) as u32; + state.dpr.replace(dpr); + state.canvas.set_width(physical_width); + state.canvas.set_height(physical_height); + let _ = state.context.scale(dpr, dpr); + state + .handler + .borrow_mut() + .size(physical_width, physical_height); + }); +} + +fn setup_keyup_callback(ws: &Rc) { + let state = ws.clone(); + register_window_event_listener(ws, "keyup", move |event: web_sys::KeyboardEvent| { + let code = KeyCode::from((event.key_code(), event.location())); + let mods = get_modifiers!(event); + let key = event.key(); + let text = key_to_text(key.as_str()); + let repeat = event.repeat(); + let event = keyboard::KeyEvent::new(code, repeat, mods, text, text); + state.handler.borrow_mut().key_up(event); + }); +} + +fn setup_keydown_callback(ws: &Rc) { + let state = ws.clone(); + register_window_event_listener(ws, "keydown", move |event: web_sys::KeyboardEvent| { + let code = KeyCode::from((event.key_code(), event.location())); + let mods = get_modifiers!(event); + let key = event.key(); + let text = key_to_text(key.as_str()); + let repeat = event.repeat(); + if let KeyCode::Backspace = code { + // Prevent the browser from going back a page by default. + event.prevent_default(); + } + let event = keyboard::KeyEvent::new(code, repeat, mods, text, text); + state.handler.borrow_mut().key_down(event); + }); +} + +/// A helper function to register a window event listener with `addEventListener`. +fn register_window_event_listener(window_state: &Rc, event_type: &str, f: F) +where + F: 'static + FnMut(E), + E: 'static + wasm_bindgen::convert::FromWasmAbi, +{ + let closure = Closure::wrap(Box::new(f) as Box); + window_state + .window + .add_event_listener_with_callback(event_type, closure.as_ref().unchecked_ref()) + .unwrap(); + closure.forget(); +} + +/// A helper function to register a canvas event listener with `addEventListener`. +fn register_canvas_event_listener(window_state: &Rc, event_type: &str, f: F) +where + F: 'static + FnMut(E), + E: 'static + wasm_bindgen::convert::FromWasmAbi, +{ + let closure = Closure::wrap(Box::new(f) as Box); + window_state + .canvas + .add_event_listener_with_callback(event_type, closure.as_ref().unchecked_ref()) + .unwrap(); + closure.forget(); +} + +fn setup_web_callbacks(window_state: &Rc) { + setup_mouse_down_callback(window_state); + setup_mouse_move_callback(window_state); + setup_mouse_up_callback(window_state); + setup_resize_callback(window_state); + setup_scroll_callback(window_state); + setup_keyup_callback(window_state); + setup_keydown_callback(window_state); +} + +impl WindowBuilder { + pub fn new() -> WindowBuilder { + WindowBuilder { + handler: None, + title: String::new(), + cursor: Cursor::Arrow, + menu: None, + } + } + + /// This takes ownership, and is typically used with UiMain + pub fn set_handler(&mut self, handler: Box) { + self.handler = Some(handler); + } + + pub fn set_size(&mut self, _: Size) { + // Ignored + } + + pub fn set_min_size(&mut self, _: Size) { + // Ignored + } + + pub fn resizable(&mut self, _resizable: bool) { + // Ignored + } + + pub fn show_titlebar(&mut self, _show_titlebar: bool) { + // Ignored + } + + pub fn set_title>(&mut self, title: S) { + self.title = title.into(); + } + + pub fn set_menu(&mut self, menu: Menu) { + self.menu = Some(menu); + } + + pub fn build(self) -> Result { + let window = web_sys::window().ok_or_else(|| Error::NoWindow)?; + let canvas = window + .document() + .ok_or(Error::NoDocument)? + .get_element_by_id("canvas") + .ok_or_else(|| Error::NoElementById("canvas".to_string()))? + .dyn_into::() + .map_err(|_| Error::JsCast)?; + let context = canvas + .get_context("2d")? + .ok_or(Error::NoContext)? + .dyn_into::() + .map_err(|_| Error::JsCast)?; + + let dpr = window.device_pixel_ratio(); + let old_w = canvas.offset_width(); + let old_h = canvas.offset_height(); + let new_w = (old_w as f64 * dpr) as u32; + let new_h = (old_h as f64 * dpr) as u32; + + canvas.set_width(new_w as u32); + canvas.set_height(new_h as u32); + let _ = context.scale(dpr, dpr); + + set_cursor(&canvas, &self.cursor); + + let handler = self.handler.unwrap(); + + let window = Rc::new(WindowState { + dpr: Cell::new(dpr), + idle_queue: Default::default(), + handler: RefCell::new(handler), + window, + canvas, + context, + }); + + setup_web_callbacks(&window); + + // Register the size with the window handler. + let wh = window.clone(); + window + .request_animation_frame(move || { + wh.handler.borrow_mut().size(new_w, new_h); + }) + .expect("Failed to request animation frame"); + + let handle = WindowHandle(Rc::downgrade(&window)); + + window.handler.borrow_mut().connect(&handle.clone().into()); + + Ok(handle) + } +} + +impl WindowHandle { + pub fn show(&self) { + self.render_soon(); + } + + pub fn resizable(&self, _resizable: bool) { + log::warn!("resizable unimplemented for web"); + } + + pub fn show_titlebar(&self, _show_titlebar: bool) { + log::warn!("show_titlebar unimplemented for web"); + } + + pub fn close(&self) { + // TODO + } + + pub fn bring_to_front_and_focus(&self) { + log::warn!("bring_to_frontand_focus unimplemented for web"); + } + + pub fn invalidate(&self) { + self.render_soon(); + } + + pub fn text(&self) -> Text { + let s = self + .0 + .upgrade() + .unwrap_or_else(|| panic!("Failed to produce a text context")); + + Text::new(s.context.clone(), s.window.clone()) + } + + pub fn request_timer(&self, deadline: Instant) -> TimerToken { + use std::convert::TryFrom; + let interval = deadline.duration_since(Instant::now()).as_millis(); + let interval = match i32::try_from(interval) { + Ok(iv) => iv, + Err(_) => { + log::warn!("Timer duration exceeds 32 bit integer max"); + i32::max_value() + } + }; + + let token = TimerToken::next(); + + if let Some(state) = self.0.upgrade() { + let s = state.clone(); + let f = move || { + if let Ok(mut handler_borrow) = s.handler.try_borrow_mut() { + handler_borrow.timer(token); + } + }; + state + .window + .set_timeout_with_callback_and_timeout_and_arguments_0( + Closure::once_into_js(f).as_ref().unchecked_ref(), + interval, + ) + .expect("Failed to call setTimeout with a callback"); + } + token + } + + pub fn set_cursor(&mut self, cursor: &Cursor) { + if let Some(s) = self.0.upgrade() { + set_cursor(&s.canvas, cursor); + } + } + + pub fn open_file_sync(&mut self, options: FileDialogOptions) -> Option { + log::warn!("open_file_sync is currently unimplemented for web."); + self.file_dialog(FileDialogType::Open, options) + .ok() + .map(|s| FileInfo { path: s.into() }) + } + + pub fn save_as_sync(&mut self, options: FileDialogOptions) -> Option { + log::warn!("save_as_sync is currently unimplemented for web."); + self.file_dialog(FileDialogType::Save, options) + .ok() + .map(|s| FileInfo { path: s.into() }) + } + + fn render_soon(&self) { + if let Some(s) = self.0.upgrade() { + let handle = self.clone(); + let state = s.clone(); + s.request_animation_frame(move || { + let want_anim_frame = state.render(); + if want_anim_frame { + handle.render_soon(); + } + }) + .expect("Failed to request animation frame"); + } + } + + pub fn file_dialog( + &self, + _ty: FileDialogType, + _options: FileDialogOptions, + ) -> std::result::Result { + Err(crate::Error::Platform(Error::Unimplemented)) + } + + /// Get a handle that can be used to schedule an idle task. + pub fn get_idle_handle(&self) -> Option { + self.0.upgrade().map(|w| IdleHandle { + state: Rc::downgrade(&w), + queue: w.idle_queue.clone(), + }) + } + + /// Get the dpi of the window. + pub fn get_dpi(&self) -> f32 { + self.0 + .upgrade() + .map(|w| NOMINAL_DPI * w.dpr.get() as f32) + .unwrap_or(NOMINAL_DPI) + } + + /// Convert a dimension in px units to physical pixels (rounding). + pub fn px_to_pixels(&self, x: f32) -> i32 { + (x * self.get_dpi() / NOMINAL_DPI).round() as i32 + } + + /// Convert a point in px units to physical pixels (rounding). + pub fn px_to_pixels_xy(&self, x: f32, y: f32) -> (i32, i32) { + let scale = self.get_dpi() / NOMINAL_DPI; + ((x * scale).round() as i32, (y * scale).round() as i32) + } + + /// Convert a dimension in physical pixels to px units. + pub fn pixels_to_px>(&self, x: T) -> f32 { + (x.into() as f32) * NOMINAL_DPI / self.get_dpi() + } + + /// Convert a point in physical pixels to px units. + pub fn pixels_to_px_xy>(&self, x: T, y: T) -> (f32, f32) { + let scale = NOMINAL_DPI / self.get_dpi(); + ((x.into() as f32) * scale, (y.into() as f32) * scale) + } + + pub fn set_menu(&self, _menu: Menu) { + log::warn!("set_menu unimplemented for web"); + } + + pub fn show_context_menu(&self, _menu: Menu, _pos: Point) { + log::warn!("show_context_menu unimplemented for web"); + } + + pub fn set_title(&self, title: impl Into) { + if let Some(state) = self.0.upgrade() { + state.canvas.set_title(&(title.into())) + } + } +} + +unsafe impl Send for IdleHandle {} + +impl IdleHandle { + /// Add an idle handler, which is called (once) when the main thread is idle. + pub fn add_idle_callback(&self, callback: F) + where + F: FnOnce(&dyn Any) + Send + 'static, + { + let mut queue = self.queue.lock().expect("IdleHandle::add_idle queue"); + queue.push(IdleKind::Callback(Box::new(callback))); + + if queue.len() == 1 { + if let Some(window_state) = self.state.upgrade() { + let state = window_state.clone(); + window_state + .request_animation_frame(move || { + state.process_idle_queue(); + }) + .expect("request_animation_frame failed"); + } + } + } + + pub fn add_idle_token(&self, token: IdleToken) { + let mut queue = self.queue.lock().expect("IdleHandle::add_idle queue"); + queue.push(IdleKind::Token(token)); + + if queue.len() == 1 { + if let Some(window_state) = self.state.upgrade() { + let state = window_state.clone(); + window_state + .request_animation_frame(move || { + state.process_idle_queue(); + }) + .expect("request_animation_frame failed"); + } + } + } +} + +fn mouse_button(button: i16) -> Option { + match button { + 0 => Some(MouseButton::Left), + 1 => Some(MouseButton::Middle), + 2 => Some(MouseButton::Right), + _ => None, + } +} + +fn set_cursor(canvas: &web_sys::HtmlCanvasElement, cursor: &Cursor) { + canvas + .style() + .set_property( + "cursor", + match cursor { + Cursor::Arrow => "default", + Cursor::IBeam => "text", + Cursor::Crosshair => "crosshair", + Cursor::OpenHand => "grab", + Cursor::NotAllowed => "not-allowed", + Cursor::ResizeLeftRight => "ew-resize", + Cursor::ResizeUpDown => "ns-resize", + }, + ) + .unwrap_or_else(|_| log::warn!("Failed to set cursor")); +} diff --git a/druid-shell/src/platform/x11/keycodes.rs b/druid-shell/src/platform/x11/keycodes.rs index 75b4e8ba7d..ca41b398c7 100644 --- a/druid-shell/src/platform/x11/keycodes.rs +++ b/druid-shell/src/platform/x11/keycodes.rs @@ -129,9 +129,9 @@ impl From for KeyCode { } } -impl Into for KeyCode { +impl Into> for KeyCode { #[allow(clippy::just_underscores_and_digits, non_upper_case_globals)] - fn into(self) -> StrOrChar { + fn into(self) -> StrOrChar<'static> { match self { KeyCode::Numpad1 => StrOrChar::Char('1'), KeyCode::Numpad2 => StrOrChar::Char('2'), diff --git a/druid-shell/src/window.rs b/druid-shell/src/window.rs index ddfc9c7b9e..da6521d329 100644 --- a/druid-shell/src/window.rs +++ b/druid-shell/src/window.rs @@ -157,7 +157,7 @@ impl WindowHandle { /// requiring precision. /// /// [`WinHandler::timer()`]: trait.WinHandler.html#tymethod.timer - pub fn request_timer(&self, deadline: std::time::Instant) -> TimerToken { + pub fn request_timer(&self, deadline: instant::Instant) -> TimerToken { self.0.request_timer(deadline) } diff --git a/druid/Cargo.toml b/druid/Cargo.toml index 95ea81c380..833e0609c4 100644 --- a/druid/Cargo.toml +++ b/druid/Cargo.toml @@ -31,6 +31,7 @@ usvg = {version = "0.9.0", optional = true} fnv = "1.0.3" xi-unicode = "0.2.0" image = {version = "0.23.2", optional = true} +instant = { version = "0.1", features = [ "wasm-bindgen" ] } [dependencies.simple_logger] version = "1.6.0" @@ -43,3 +44,6 @@ version = "0.6.0" [dependencies.druid-derive] path = "../druid-derive" version = "0.4.0" + +[target.'cfg(target_arch="wasm32")'.dependencies] +console_log = "0.1.2" diff --git a/druid/examples/anim.rs b/druid/examples/anim.rs index 352240b5ac..d0313b3132 100644 --- a/druid/examples/anim.rs +++ b/druid/examples/anim.rs @@ -26,12 +26,9 @@ struct AnimWidget { impl Widget for AnimWidget { fn event(&mut self, ctx: &mut EventCtx, event: &Event, _data: &mut u32, _env: &Env) { - match event { - Event::MouseDown(_) => { - self.t = 0.0; - ctx.request_anim_frame(); - } - _ => (), + if let Event::MouseDown(_) = event { + self.t = 0.0; + ctx.request_anim_frame(); } } @@ -68,7 +65,7 @@ impl Widget for AnimWidget { } } -fn main() { +pub fn main() { let window = WindowDesc::new(|| AnimWidget { t: 0.0 }).title( LocalizedString::new("anim-demo-window-title") .with_placeholder("You spin me right round..."), diff --git a/druid/examples/calc.rs b/druid/examples/calc.rs index 6950cda4a4..e15f118ee2 100644 --- a/druid/examples/calc.rs +++ b/druid/examples/calc.rs @@ -242,7 +242,7 @@ fn build_calc() -> impl Widget { ) } -fn main() { +pub fn main() { let window = WindowDesc::new(build_calc) .window_size((223., 300.)) .resizable(false) diff --git a/druid/examples/custom_widget.rs b/druid/examples/custom_widget.rs index 6a886daec4..5c5145433a 100644 --- a/druid/examples/custom_widget.rs +++ b/druid/examples/custom_widget.rs @@ -116,7 +116,7 @@ impl Widget for CustomWidget { } } -fn main() { +pub fn main() { let window = WindowDesc::new(|| CustomWidget {}).title( LocalizedString::new("custom-widget-demo-window-title").with_placeholder("Fancy Colors"), ); @@ -131,7 +131,7 @@ fn make_image_data(width: usize, height: usize) -> Vec { for y in 0..height { for x in 0..width { let ix = (y * width + x) * 4; - result[ix + 0] = x as u8; + result[ix] = x as u8; result[ix + 1] = y as u8; result[ix + 2] = !(x as u8); result[ix + 3] = 127; diff --git a/druid/examples/either.rs b/druid/examples/either.rs index 9010cce3a5..d0e81b951d 100644 --- a/druid/examples/either.rs +++ b/druid/examples/either.rs @@ -21,7 +21,7 @@ struct AppState { value: f64, } -fn main() { +pub fn main() { let main_window = WindowDesc::new(ui_builder).title( LocalizedString::new("either-demo-window-title") .with_placeholder("Switcheroo") diff --git a/druid/examples/ext_event.rs b/druid/examples/ext_event.rs index 5af50f8719..0c048cc84b 100644 --- a/druid/examples/ext_event.rs +++ b/druid/examples/ext_event.rs @@ -14,8 +14,9 @@ //! An example of sending commands from another thread. +use instant::Instant; use std::thread; -use std::time::{Duration, Instant}; +use std::time::Duration; use druid::kurbo::RoundedRect; use druid::widget::prelude::*; @@ -80,7 +81,7 @@ impl Widget for ColorWell { } } -fn main() { +pub fn main() { let window = WindowDesc::new(make_ui).title( LocalizedString::new("identity-demo-window-title").with_placeholder("External Event Demo"), ); diff --git a/druid/examples/flex.rs b/druid/examples/flex.rs index 185d3a433e..fe86325856 100644 --- a/druid/examples/flex.rs +++ b/druid/examples/flex.rs @@ -299,7 +299,7 @@ fn make_ui() -> impl Widget { .padding(10.0) } -fn main() -> Result<(), PlatformError> { +pub fn main() -> Result<(), PlatformError> { let main_window = WindowDesc::new(make_ui) .window_size((620., 600.00)) .with_min_size((620., 265.00)) diff --git a/druid/examples/game_of_life.rs b/druid/examples/game_of_life.rs index 2921f75044..09fd97b575 100644 --- a/druid/examples/game_of_life.rs +++ b/druid/examples/game_of_life.rs @@ -12,10 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. +#![allow(clippy::unreadable_literal)] + //! Game of life +use instant::Instant; use std::ops::{Index, IndexMut}; -use std::time::{Duration, Instant}; +use std::time::Duration; use druid::widget::prelude::*; use druid::widget::{Button, Flex, Label, Slider}; @@ -53,7 +56,7 @@ struct ColorScheme { } impl GridPos { - pub fn above(&self) -> Option { + pub fn above(self) -> Option { if self.row == 0 { None } else { @@ -63,7 +66,7 @@ impl GridPos { }) } } - pub fn below(&self) -> Option { + pub fn below(self) -> Option { if self.row == GRID_SIZE - 1 { None } else { @@ -73,7 +76,7 @@ impl GridPos { }) } } - pub fn left(&self) -> Option { + pub fn left(self) -> Option { if self.col == 0 { None } else { @@ -83,7 +86,7 @@ impl GridPos { }) } } - pub fn right(&self) -> Option { + pub fn right(self) -> Option { if self.col == GRID_SIZE - 1 { None } else { @@ -94,17 +97,17 @@ impl GridPos { } } #[allow(dead_code)] - pub fn above_left(&self) -> Option { + pub fn above_left(self) -> Option { self.above().and_then(|pos| pos.left()) } - pub fn above_right(&self) -> Option { + pub fn above_right(self) -> Option { self.above().and_then(|pos| pos.right()) } #[allow(dead_code)] - pub fn below_left(&self) -> Option { + pub fn below_left(self) -> Option { self.below().and_then(|pos| pos.left()) } - pub fn below_right(&self) -> Option { + pub fn below_right(self) -> Option { self.below().and_then(|pos| pos.right()) } } @@ -413,7 +416,7 @@ fn make_widget() -> impl Widget { ) } -fn main() { +pub fn main() { let window = WindowDesc::new(make_widget) .window_size(Size { width: 800.0, @@ -474,7 +477,7 @@ impl PartialEq for Grid { return false; } } - return true; + true } } diff --git a/druid/examples/hello.rs b/druid/examples/hello.rs index 6e6155c591..e86ee28bba 100644 --- a/druid/examples/hello.rs +++ b/druid/examples/hello.rs @@ -24,7 +24,7 @@ struct HelloState { name: String, } -fn main() { +pub fn main() { // describe the main window let main_window = WindowDesc::new(build_root_widget) .title(WINDOW_TITLE) diff --git a/druid/examples/identity.rs b/druid/examples/identity.rs index 112631fc93..ad0c0b47a6 100644 --- a/druid/examples/identity.rs +++ b/druid/examples/identity.rs @@ -27,7 +27,8 @@ //! in your `Data` type) but this is an example, and I couldn't think of anything //! better. ¯\_(ツ)_/¯ -use std::time::{Duration, Instant}; +use instant::Instant; +use std::time::Duration; use druid::kurbo::RoundedRect; use druid::widget::{Button, CrossAxisAlignment, Flex, WidgetId}; @@ -147,7 +148,7 @@ impl Widget for ColorWell { } } -fn main() { +pub fn main() { let window = WindowDesc::new(make_ui).title( LocalizedString::new("identity-demo-window-title").with_placeholder("Color Freezing Fun"), ); diff --git a/druid/examples/image.rs b/druid/examples/image.rs index a4169dfc76..0f9707c472 100644 --- a/druid/examples/image.rs +++ b/druid/examples/image.rs @@ -19,20 +19,20 @@ //! #[cfg(not(feature = "image"))] -fn main() { +pub fn main() { eprintln!("This examples requires the \"image\" feature to be enabled:"); eprintln!("cargo run --example image --features \"image\""); } #[cfg(feature = "image")] -fn main() { +pub fn main() { use druid::{ widget::{FillStrat, Flex, Image, ImageData, WidgetExt}, AppLauncher, Color, Widget, WindowDesc, }; fn ui_builder() -> impl Widget { - let png_data = ImageData::from_file("examples/PicWithAlpha.png").unwrap(); + let png_data = ImageData::from_data(include_bytes!("PicWithAlpha.png")).unwrap(); let mut col = Flex::column(); diff --git a/druid/examples/layout.rs b/druid/examples/layout.rs index 0776acc7ef..85621419d7 100644 --- a/druid/examples/layout.rs +++ b/druid/examples/layout.rs @@ -54,7 +54,7 @@ fn build_app() -> impl Widget { col.debug_paint_layout() } -fn main() { +pub fn main() { let window = WindowDesc::new(build_app) .title(LocalizedString::new("layout-demo-window-title").with_placeholder("Very flexible")); AppLauncher::with_window(window) diff --git a/druid/examples/lens.rs b/druid/examples/lens.rs index f14cb9650a..dc2e3257f4 100644 --- a/druid/examples/lens.rs +++ b/druid/examples/lens.rs @@ -16,7 +16,7 @@ use druid::widget::Slider; use druid::widget::{CrossAxisAlignment, Flex, Label, TextBox}; use druid::{AppLauncher, Data, Env, Lens, LocalizedString, Widget, WidgetExt, WindowDesc}; -fn main() { +pub fn main() { let main_window = WindowDesc::new(ui_builder) .title(LocalizedString::new("lens-demo-window-title").with_placeholder("Lens Demo")); let data = MyComplexState { diff --git a/druid/examples/list.rs b/druid/examples/list.rs index 1335e55344..373b5e06f3 100644 --- a/druid/examples/list.rs +++ b/druid/examples/list.rs @@ -28,7 +28,7 @@ struct AppData { right: Arc>, } -fn main() { +pub fn main() { let main_window = WindowDesc::new(ui_builder) .title(LocalizedString::new("list-demo-window-title").with_placeholder("List Demo")); // Set our initial data diff --git a/druid/examples/multiwin.rs b/druid/examples/multiwin.rs index c6b93a21b6..1fb59e2b6f 100644 --- a/druid/examples/multiwin.rs +++ b/druid/examples/multiwin.rs @@ -33,7 +33,8 @@ struct State { selected: usize, } -fn main() { +pub fn main() { + #[cfg(not(target_arch = "wasm32"))] simple_logger::init().unwrap(); let main_window = WindowDesc::new(ui_builder) .menu(make_menu(&State::default())) @@ -130,7 +131,7 @@ impl AppDelegate for Delegate { // wouldn't it be nice if a menu (like a button) could just mutate state // directly if desired? (Target::Window(id), &MENU_INCREMENT_ACTION) => { - data.menu_count = data.menu_count + 1; + data.menu_count += 1; let menu = make_menu::(data); let cmd = Command::new(druid::commands::SET_MENU, menu); ctx.submit_command(cmd, *id); diff --git a/druid/examples/panels.rs b/druid/examples/panels.rs index 02ef2f552b..38ab093002 100644 --- a/druid/examples/panels.rs +++ b/druid/examples/panels.rs @@ -91,7 +91,7 @@ fn build_app() -> impl Widget<()> { ) } -fn main() -> Result<(), PlatformError> { +pub fn main() -> Result<(), PlatformError> { let main_window = WindowDesc::new(build_app) .title(LocalizedString::new("panels-demo-window-title").with_placeholder("Fancy Boxes!")); AppLauncher::with_window(main_window) diff --git a/druid/examples/parse.rs b/druid/examples/parse.rs index eeab124709..8b5dd14653 100644 --- a/druid/examples/parse.rs +++ b/druid/examples/parse.rs @@ -15,7 +15,7 @@ use druid::widget::{Align, Flex, Label, Parse, TextBox}; use druid::{AppLauncher, LocalizedString, Widget, WindowDesc}; -fn main() { +pub fn main() { let main_window = WindowDesc::new(ui_builder).title( LocalizedString::new("parse-demo-window-title").with_placeholder("Number Parsing Demo"), ); diff --git a/druid/examples/scroll.rs b/druid/examples/scroll.rs index 35e139e309..66508c9beb 100644 --- a/druid/examples/scroll.rs +++ b/druid/examples/scroll.rs @@ -21,7 +21,7 @@ use druid::widget::prelude::*; use druid::widget::{Flex, Padding, Scroll}; use druid::{AppLauncher, Data, Insets, LocalizedString, Rect, WindowDesc}; -fn main() { +pub fn main() { let window = WindowDesc::new(build_widget) .title(LocalizedString::new("scroll-demo-window-title").with_placeholder("Scroll demo")); AppLauncher::with_window(window) diff --git a/druid/examples/scroll_colors.rs b/druid/examples/scroll_colors.rs index 91044670cf..307ba75042 100644 --- a/druid/examples/scroll_colors.rs +++ b/druid/examples/scroll_colors.rs @@ -41,7 +41,7 @@ fn build_app() -> impl Widget { Scroll::new(col) } -fn main() { +pub fn main() { let main_window = WindowDesc::new(build_app).title( LocalizedString::new("scroll-colors-demo-window-title").with_placeholder("Rainbows!"), ); diff --git a/druid/examples/split_demo.rs b/druid/examples/split_demo.rs index 3d5cffa563..be49721eb6 100644 --- a/druid/examples/split_demo.rs +++ b/druid/examples/split_demo.rs @@ -56,7 +56,7 @@ fn build_app() -> impl Widget { ) .border(Color::WHITE, 1.0), ); - let draggable_rows = Padding::new( + Padding::new( 10.0, Container::new( Split::rows( @@ -73,11 +73,10 @@ fn build_app() -> impl Widget { .draggable(true), ) .border(Color::WHITE, 1.0), - ); - draggable_rows + ) } -fn main() { +pub fn main() { let window = WindowDesc::new(build_app) .title(LocalizedString::new("split-demo-window-title").with_placeholder("Split Demo")); AppLauncher::with_window(window) diff --git a/druid/examples/styled_text.rs b/druid/examples/styled_text.rs index 3e8368c535..a75bdb2645 100644 --- a/druid/examples/styled_text.rs +++ b/druid/examples/styled_text.rs @@ -28,7 +28,7 @@ struct AppData { text: String, size: f64, } -fn main() -> Result<(), PlatformError> { +pub fn main() -> Result<(), PlatformError> { let main_window = WindowDesc::new(ui_builder).title( LocalizedString::new("styled-text-demo-window-title").with_placeholder("Type Styler"), ); diff --git a/druid/examples/svg.rs b/druid/examples/svg.rs index ec38e91072..2d04137e0d 100644 --- a/druid/examples/svg.rs +++ b/druid/examples/svg.rs @@ -18,13 +18,13 @@ //! `cargo run --example svg --features "svg"` #[cfg(not(feature = "svg"))] -fn main() { +pub fn main() { eprintln!("This examples requires the \"svg\" feature to be enabled:"); eprintln!("cargo run --example svg --features \"svg\""); } #[cfg(feature = "svg")] -fn main() { +pub fn main() { use log::error; use druid::{ diff --git a/druid/examples/switch.rs b/druid/examples/switch.rs index 903d6d490f..244649c691 100644 --- a/druid/examples/switch.rs +++ b/druid/examples/switch.rs @@ -63,7 +63,7 @@ fn build_widget() -> impl Widget { col.center() } -fn main() { +pub fn main() { let window = WindowDesc::new(build_widget) .title(LocalizedString::new("switch-demo-window-title").with_placeholder("Switch Demo")); AppLauncher::with_window(window) diff --git a/druid/examples/timer.rs b/druid/examples/timer.rs index 4281c5297c..a243ed296e 100644 --- a/druid/examples/timer.rs +++ b/druid/examples/timer.rs @@ -14,7 +14,8 @@ //! An example of a timer. -use std::time::{Duration, Instant}; +use instant::Instant; +use std::time::Duration; use druid::widget::prelude::*; use druid::widget::BackgroundBrush; @@ -119,7 +120,7 @@ impl Widget for SimpleBox { } } -fn main() { +pub fn main() { let window = WindowDesc::new(|| TimerWidget { timer_id: TimerToken::INVALID, simple_box: WidgetPod::new(SimpleBox), diff --git a/druid/examples/view_switcher.rs b/druid/examples/view_switcher.rs index 0a65978641..f8d7e8c859 100644 --- a/druid/examples/view_switcher.rs +++ b/druid/examples/view_switcher.rs @@ -23,7 +23,7 @@ struct AppState { current_text: String, } -fn main() { +pub fn main() { let main_window = WindowDesc::new(make_ui).title(LocalizedString::new("View Switcher")); let data = AppState { current_view: 0, diff --git a/druid/examples/wasm/.gitignore b/druid/examples/wasm/.gitignore new file mode 100644 index 0000000000..2cd19e7cce --- /dev/null +++ b/druid/examples/wasm/.gitignore @@ -0,0 +1,3 @@ +src/examples.in +src/examples +html diff --git a/druid/examples/wasm/Cargo.toml b/druid/examples/wasm/Cargo.toml new file mode 100644 index 0000000000..1916cf9bcd --- /dev/null +++ b/druid/examples/wasm/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "druid-wasm-examples" +version = "0.1.0" +license = "Apache-2.0" +description = "Wasm scaffolding for druid examples" +repository = "https://github.com/xi-editor/druid" +edition = "2018" +publish = false + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +druid = { path="../.."} +wasm-bindgen = "0.2.60" +console_error_panic_hook = { version = "0.1.6" } +log = "0.4.8" +instant = { version = "0.1", features = [ "wasm-bindgen" ] } + +[target.'cfg(not(target_arch="wasm32"))'.dependencies] +simple_logger = { version = "1.6.0", default-features = false } diff --git a/druid/examples/wasm/README.md b/druid/examples/wasm/README.md new file mode 100644 index 0000000000..93d02a34ea --- /dev/null +++ b/druid/examples/wasm/README.md @@ -0,0 +1,35 @@ +# druid WASM examples + +This crate generates and builds all necessary files for deploying `druid` examples to the web. + +## Building + +You will need `cargo` and `wasm-pack` for building the code and a simple +server like [`http`](https://crates.io/crates/https) for serving the web pages. + +First build with + +``` +> wasm-pack build --target web +``` + +This step has two main functions: + + 1. It generates an HTML document for each of the `druid` examples with a script that + calls the appropriate function in the JavaScript module exposing the raw WASM. + 2. It builds the WASM binary which exposes all functions annotated with `#[wasm_bindgen]`. + 3. It builds the JavaScript module that loads the WASM binary and binds all exposed functions to + JavaScript functions so they can be called directly from JavaScript. + +To preview the build in a web browser, run + +``` +> http -d html +``` + +which should start serving the specified folder. + +Finally, point your browser to the appropriate localhost url (usually http://localhost:8000) and you +should see a list of HTML documents -- one for each example. + +When you make changes to the project, re-run `wasm-pack build --target web` and you can see the changes in your browser when you refresh -- no need to restart `http`. diff --git a/druid/examples/wasm/build.rs b/druid/examples/wasm/build.rs new file mode 100644 index 0000000000..0dd3ab6995 --- /dev/null +++ b/druid/examples/wasm/build.rs @@ -0,0 +1,119 @@ +use std::io::Result; +use std::path::PathBuf; +use std::{env, fs}; + +/// Examples known to not work with WASM are skipped. Ideally this list will eventually be empty. +const EXCEPTIONS: &[&str] = &[ + "svg", // usvg doesn't currently build with WASM. + "ext_event", // WASM doesn't currently support spawning threads. +]; + +fn main() -> Result<()> { + let crate_dir = PathBuf::from(&env::var("CARGO_MANIFEST_DIR").unwrap()); + let src_dir = crate_dir.join("src"); + let examples_dir = src_dir.join("examples"); + + let parent_dir = crate_dir.parent().unwrap(); + + // Create a symlink (platform specific) to the examples directory. + #[cfg(unix)] + std::os::unix::fs::symlink(parent_dir, &examples_dir).ok(); + #[cfg(windows)] + std::os::windows::fs::symlink_dir(parent_dir, &examples_dir).ok(); + + // Generate example module and the necessary html documents. + + // Declare the newly found example modules in examples.in + let mut examples_in = r#" +// This file is automatically generated and must not be committed. + +/// This is a module collecting all valid examples in the parent examples directory. +mod examples { +"# + .to_string(); + + for entry in examples_dir.read_dir()? { + let path = entry?.path(); + if let Some(r) = path.extension() { + if r != "rs" { + continue; + } + } else { + continue; + } + + if let Some(example) = path.file_stem() { + let example_str = example.to_string_lossy(); + + // Skip examples that are known to not work with wasm. + if EXCEPTIONS.contains(&example_str.as_ref()) { + continue; + } + + // Record the valid example module we found to add to the generated examples.in + examples_in.push_str(&format!(" pub mod {};\n", example_str)); + + // The "switch" example name would conflict with JavaScript's switch statement. So we + // rename it here to switch_demo. + let js_entry_fn_name = if &example_str == "switch" { + "switch_demo".to_string() + } else { + example_str.to_string() + }; + + // Create an html document for each example. + let html = format!( + r#" + + + + + Druid WASM example - {name} + + + + + + + +"#, + name = js_entry_fn_name + ); + + // Write out the html file into a designated html directory located in crate root. + let html_dir = crate_dir.join("html"); + if !html_dir.exists() { + fs::create_dir(&html_dir).unwrap_or_else(|_| { + panic!("Failed to create output html directory: {:?}", &html_dir) + }); + } + + fs::write(html_dir.join(example).with_extension("html"), html) + .unwrap_or_else(|_| panic!("Failed to create {}.html", example_str)); + } + } + + examples_in.push_str("}"); + + // Write out the contents of the examples.in module. + fs::write(src_dir.join("examples.in"), examples_in)?; + + Ok(()) +} diff --git a/druid/examples/wasm/src/lib.rs b/druid/examples/wasm/src/lib.rs new file mode 100644 index 0000000000..d2ea7b4041 --- /dev/null +++ b/druid/examples/wasm/src/lib.rs @@ -0,0 +1,61 @@ +// Copyright 2020 The xi-editor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use wasm_bindgen::prelude::*; + +// This line includes an automatically generated (in build.rs) examples module. +// This particular mechanism is chosen to avoid any kinds of modifications to committed files at +// build time, keeping the source tree clean from build artifacts. +include!("examples.in"); + +macro_rules! impl_example { + ($wasm_fn:ident, $expr:expr) => { + #[wasm_bindgen] + pub fn $wasm_fn() { + std::panic::set_hook(Box::new(console_error_panic_hook::hook)); + $expr; + } + }; + ($fn:ident) => { + impl_example!($fn, examples::$fn::main()); + }; + ($fn:ident.unwrap()) => { + impl_example!($fn, examples::$fn::main().unwrap()); + }; +} + +impl_example!(anim); +impl_example!(calc); +impl_example!(custom_widget); +impl_example!(either); +//impl_example!(ext_event); // No thread support on wasm +impl_example!(flex.unwrap()); +impl_example!(game_of_life); +impl_example!(hello); +impl_example!(identity); +impl_example!(image); +impl_example!(layout); +impl_example!(lens); +impl_example!(list); +impl_example!(multiwin); +impl_example!(panels.unwrap()); +impl_example!(parse); +impl_example!(scroll_colors); +impl_example!(scroll); +impl_example!(split_demo); +impl_example!(styled_text.unwrap()); +//impl_example!(svg); // usvg doesn't compile on usvg at the time of this writing +impl_example!(switch_demo, examples::switch::main()); +impl_example!(timer); +impl_example!(view_switcher); diff --git a/druid/src/app.rs b/druid/src/app.rs index 656cbbe238..650daab98f 100644 --- a/druid/src/app.rs +++ b/druid/src/app.rs @@ -85,7 +85,10 @@ impl AppLauncher { /// /// Meant for use during development only. pub fn use_simple_logger(self) -> Self { + #[cfg(not(target_arch = "wasm32"))] simple_logger::init().ok(); + #[cfg(target_arch = "wasm32")] + console_log::init_with_level(log::Level::Trace).ok(); self } diff --git a/druid/src/contexts.rs b/druid/src/contexts.rs index 668843e38c..3cc3e96994 100644 --- a/druid/src/contexts.rs +++ b/druid/src/contexts.rs @@ -15,9 +15,8 @@ //! The context types that are passed into various widget methods. use std::ops::{Deref, DerefMut}; -use std::time::Instant; -use log; +use instant::Instant; use crate::core::{BaseState, CommandQueue, FocusChange}; use crate::piet::Piet; diff --git a/druid/src/event.rs b/druid/src/event.rs index 224c7bea87..6bb306f9c8 100644 --- a/druid/src/event.rs +++ b/druid/src/event.rs @@ -351,6 +351,7 @@ mod state_cell { } impl StateCheckFn { + #[cfg(not(target_arch = "wasm32"))] pub(crate) fn new(f: impl Fn(&BaseState) + 'static) -> Self { StateCheckFn(Rc::new(f)) } diff --git a/druid/src/lib.rs b/druid/src/lib.rs index a4971ebc74..f60053aa01 100644 --- a/druid/src/lib.rs +++ b/druid/src/lib.rs @@ -125,6 +125,7 @@ pub mod lens; mod localization; mod menu; mod mouse; +#[cfg(not(target_arch = "wasm32"))] #[cfg(test)] mod tests; pub mod text; @@ -161,5 +162,6 @@ pub use widget::{Widget, WidgetExt, WidgetId}; pub use win_handler::DruidHandler; pub use window::{Window, WindowId}; +#[cfg(not(target_arch = "wasm32"))] #[cfg(test)] pub(crate) use event::{StateCell, StateCheckFn}; diff --git a/druid/src/text/backspace.rs b/druid/src/text/backspace.rs index 668294ae85..d20ef95bba 100644 --- a/druid/src/text/backspace.rs +++ b/druid/src/text/backspace.rs @@ -193,6 +193,7 @@ fn backspace_offset(text: &impl EditableText, start: usize) -> usize { } /// Calculate resulting offset for a backwards delete. +#[allow(clippy::trivially_copy_pass_by_ref)] pub fn offset_for_delete_backwards(region: &Selection, text: &impl EditableText) -> usize { if !region.is_caret() { region.min() diff --git a/druid/src/widget/scroll.rs b/druid/src/widget/scroll.rs index 28b578e682..ff6079b338 100644 --- a/druid/src/widget/scroll.rs +++ b/druid/src/widget/scroll.rs @@ -15,7 +15,8 @@ //! A container that scrolls its contents. use std::f64::INFINITY; -use std::time::{Duration, Instant}; + +use instant::{Duration, Instant}; use crate::kurbo::{Affine, Point, Rect, RoundedRect, Size, Vec2}; use crate::theme; diff --git a/druid/src/widget/stepper.rs b/druid/src/widget/stepper.rs index 1ac3d93d32..a35fb839c6 100644 --- a/druid/src/widget/stepper.rs +++ b/druid/src/widget/stepper.rs @@ -14,15 +14,16 @@ //! A stepper widget. -use crate::{ - BoxConstraints, Env, Event, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx, Size, - TimerToken, UpdateCtx, Widget, -}; use std::f64::EPSILON; -use std::time::{Duration, Instant}; + +use instant::{Duration, Instant}; use crate::kurbo::{BezPath, Rect, RoundedRect}; use crate::piet::{LinearGradient, RenderContext, UnitPoint}; +use crate::{ + BoxConstraints, Env, Event, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx, Size, + TimerToken, UpdateCtx, Widget, +}; use crate::theme; use crate::Point; diff --git a/druid/src/widget/textbox.rs b/druid/src/widget/textbox.rs index 204c567d9e..df289cdd6c 100644 --- a/druid/src/widget/textbox.rs +++ b/druid/src/widget/textbox.rs @@ -14,7 +14,7 @@ //! A textbox widget. -use std::time::{Duration, Instant}; +use instant::{Duration, Instant}; use crate::{ Application, BoxConstraints, Cursor, Env, Event, EventCtx, HotKey, KeyCode, LayoutCtx, diff --git a/druid/src/window.rs b/druid/src/window.rs index d469576eb2..04e0bc8fe7 100644 --- a/druid/src/window.rs +++ b/druid/src/window.rs @@ -15,7 +15,9 @@ //! Management of multiple windows. use std::mem; -use std::time::Instant; + +// Automatically defaults to std::time::Instant on non Wasm platforms +use instant::Instant; use crate::kurbo::{Point, Rect, Size}; use crate::piet::{Piet, RenderContext}; @@ -317,6 +319,7 @@ impl Window { } /// only expose `layout` for testing; normally it is called as part of `do_paint` + #[cfg(not(target_arch = "wasm32"))] #[cfg(test)] pub(crate) fn just_layout( &mut self,