├── .github └── workflows │ └── main.yml ├── .gitignore ├── BACKERS.md ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── Makefile.toml ├── README.md ├── RELEASE_CHECKLIST.md ├── examples ├── README.md ├── animation │ ├── Cargo.toml │ ├── Makefile.toml │ ├── README.md │ ├── index.html │ └── src │ │ └── lib.rs ├── auth │ ├── Cargo.toml │ ├── Makefile.toml │ ├── README.md │ ├── index.html │ └── src │ │ └── lib.rs ├── bunnies │ ├── Cargo.toml │ ├── Makefile.toml │ ├── README.md │ ├── index.html │ ├── public │ │ ├── media │ │ │ ├── bunny.png │ │ │ ├── fragment.glsl │ │ │ └── vertex.glsl │ │ └── style.css │ └── src │ │ ├── components.rs │ │ ├── config.rs │ │ ├── empty_lib.rs │ │ ├── fps_counter.rs │ │ ├── geometry.rs │ │ ├── hud.rs │ │ ├── init_world.rs │ │ ├── lib.rs │ │ ├── scene_renderer.rs │ │ └── systems.rs ├── canvas │ ├── Cargo.toml │ ├── Makefile.toml │ ├── README.md │ ├── index.html │ └── src │ │ └── lib.rs ├── charts │ ├── Cargo.toml │ ├── Makefile.toml │ ├── README.md │ ├── index.html │ └── src │ │ ├── lib.rs │ │ └── line.rs ├── component_builder │ ├── Cargo.toml │ ├── Makefile.toml │ ├── README.md │ ├── index.html │ └── src │ │ ├── button.rs │ │ └── lib.rs ├── counter │ ├── Cargo.toml │ ├── Makefile.toml │ ├── README.md │ ├── index.html │ └── src │ │ └── lib.rs ├── counters │ ├── Cargo.toml │ ├── Makefile.toml │ ├── README.md │ ├── index.html │ └── src │ │ ├── counter.rs │ │ └── lib.rs ├── custom_elements │ ├── Cargo.toml │ ├── Makefile.toml │ ├── README.md │ ├── index.html │ ├── public │ │ ├── checkbox-tristate.js │ │ ├── code-block.js │ │ ├── feather-icon.js │ │ ├── highlight │ │ │ ├── github.css │ │ │ └── highlight.pack.js │ │ └── math-tex.js │ └── src │ │ ├── checkbox_tristate.rs │ │ ├── code_block.rs │ │ ├── feather_icon.rs │ │ ├── lib.rs │ │ ├── math_tex.rs │ │ └── sl_input.rs ├── drag_and_drop │ ├── Cargo.toml │ ├── Makefile.toml │ ├── README.md │ ├── index.html │ └── src │ │ └── lib.rs ├── drop_zone │ ├── Cargo.toml │ ├── Makefile.toml │ ├── README.md │ ├── index.html │ └── src │ │ └── lib.rs ├── e2e_encryption │ ├── Cargo.lock │ ├── Cargo.toml │ ├── Makefile.toml │ ├── README.md │ ├── client │ │ ├── Cargo.toml │ │ ├── index.html │ │ └── src │ │ │ └── lib.rs │ ├── server │ │ ├── Cargo.toml │ │ └── src │ │ │ └── main.rs │ └── shared │ │ ├── Cargo.toml │ │ └── src │ │ └── lib.rs ├── el_key │ ├── Cargo.toml │ ├── Makefile.toml │ ├── README.md │ ├── index.html │ ├── public │ │ ├── card-table.png │ │ ├── control-buttons.png │ │ ├── el_key.css │ │ ├── enable-disable-with-empty.gif │ │ ├── enable-disable-with-keys.gif │ │ ├── enable-disable-without-keys-and-empty.gif │ │ ├── favicon.png │ │ ├── options.png │ │ ├── reordering-colors.gif │ │ ├── reordering-with-keys.gif │ │ ├── reordering-without-keys.gif │ │ └── screenshot.png │ └── src │ │ └── lib.rs ├── fetch │ ├── Cargo.toml │ ├── Makefile.toml │ ├── README.md │ ├── index.html │ ├── src │ │ ├── lib.rs │ │ ├── post.rs │ │ └── simple.rs │ └── user.json ├── graphql │ ├── Cargo.toml │ ├── Makefile.toml │ ├── README.md │ ├── graphql │ │ ├── queries.graphql │ │ └── schema.graphql │ ├── index.html │ └── src │ │ └── lib.rs ├── i18n │ ├── Cargo.toml │ ├── Makefile.toml │ ├── README.md │ ├── ftl_messages │ │ ├── de-DE.ftl │ │ └── en-US.ftl │ ├── index.html │ └── src │ │ ├── i18n.rs │ │ └── lib.rs ├── intersection_observer │ ├── Cargo.toml │ ├── Makefile.toml │ ├── README.md │ ├── index.html │ ├── public │ │ └── index.css │ └── src │ │ └── lib.rs ├── markdown │ ├── Cargo.toml │ ├── Makefile.toml │ ├── README.md │ ├── build.rs │ ├── index.html │ ├── md │ │ ├── examples.md │ │ ├── footer.md │ │ └── generated_html │ │ │ └── footer.html │ └── src │ │ └── lib.rs ├── no_change │ ├── Cargo.toml │ ├── Makefile.toml │ ├── README.md │ ├── index.html │ └── src │ │ └── lib.rs ├── on_insert │ ├── Cargo.toml │ ├── Makefile.toml │ ├── README.md │ ├── index.html │ └── src │ │ └── lib.rs ├── page_trait │ ├── Cargo.toml │ ├── Makefile.toml │ ├── README.md │ ├── index.html │ └── src │ │ ├── lib.rs │ │ ├── page.rs │ │ └── page │ │ ├── create_page.rs │ │ ├── my_page.rs │ │ ├── my_page_2.rs │ │ └── page_trait.rs ├── pages │ ├── Cargo.toml │ ├── Makefile.toml │ ├── README.md │ ├── index.html │ └── src │ │ ├── lib.rs │ │ ├── page.rs │ │ └── page │ │ ├── admin.rs │ │ └── admin │ │ ├── page.rs │ │ └── page │ │ └── report.rs ├── pages_hash_routing │ ├── Cargo.toml │ ├── Makefile.toml │ ├── README.md │ ├── index.html │ ├── public │ │ └── text-polyfill.min.js │ └── src │ │ ├── lib.rs │ │ ├── page.rs │ │ └── page │ │ ├── admin.rs │ │ └── admin │ │ ├── page.rs │ │ └── page │ │ └── report.rs ├── pages_keep_state │ ├── Cargo.toml │ ├── Makefile.toml │ ├── README.md │ ├── index.html │ ├── public │ │ └── text-polyfill.min.js │ └── src │ │ ├── lib.rs │ │ ├── page.rs │ │ └── page │ │ ├── admin.rs │ │ └── admin │ │ ├── page.rs │ │ └── page │ │ └── report.rs ├── record_screen │ ├── Cargo.toml │ ├── Makefile.toml │ ├── README.md │ ├── index.html │ └── src │ │ └── lib.rs ├── resize_observer │ ├── Cargo.toml │ ├── Makefile.toml │ ├── README.md │ ├── index.html │ ├── public │ │ └── observe_element_size.js │ └── src │ │ └── lib.rs ├── rust_from_js │ ├── Cargo.toml │ ├── Makefile.toml │ ├── README.md │ ├── index.html │ └── src │ │ └── lib.rs ├── server_integration │ ├── Cargo.lock │ ├── Cargo.toml │ ├── Makefile.toml │ ├── README.md │ ├── client │ │ ├── Cargo.toml │ │ ├── index.html │ │ └── src │ │ │ ├── example_a.rs │ │ │ ├── example_b.rs │ │ │ ├── example_c.rs │ │ │ ├── example_d.rs │ │ │ ├── example_e.rs │ │ │ └── lib.rs │ ├── server │ │ ├── Cargo.toml │ │ └── src │ │ │ ├── count_actor.rs │ │ │ └── main.rs │ └── shared │ │ ├── Cargo.toml │ │ └── src │ │ └── lib.rs ├── service_worker │ ├── Cargo.toml │ ├── Makefile.toml │ ├── README.md │ ├── index.html │ ├── private_key.pem │ ├── public │ │ ├── images │ │ │ └── important-notes.png │ │ └── subscribe.js │ ├── service-worker.js │ └── src │ │ ├── lib.rs │ │ └── main.rs ├── subscribe │ ├── Cargo.toml │ ├── Makefile.toml │ ├── README.md │ ├── index.html │ └── src │ │ ├── counter.rs │ │ └── lib.rs ├── tea_component │ ├── Cargo.toml │ ├── Makefile.toml │ ├── README.md │ ├── index.html │ └── src │ │ ├── counter.rs │ │ └── lib.rs ├── tests │ ├── Cargo.toml │ ├── Makefile.toml │ ├── README.md │ ├── index.html │ ├── public │ │ └── leopard.jpg │ └── src │ │ └── lib.rs ├── todomvc │ ├── Cargo.toml │ ├── Makefile.toml │ ├── README.md │ ├── index.html │ ├── public │ │ └── index.css │ └── src │ │ └── lib.rs ├── unsaved_changes │ ├── Cargo.toml │ ├── Makefile.toml │ ├── README.md │ ├── index.html │ └── src │ │ └── lib.rs ├── update_from_js │ ├── Cargo.toml │ ├── Makefile.toml │ ├── README.md │ ├── index.html │ ├── public │ │ └── index.js │ └── src │ │ └── lib.rs ├── url │ ├── Cargo.toml │ ├── Makefile.toml │ ├── README.md │ ├── index.html │ └── src │ │ └── lib.rs ├── user_media │ ├── Cargo.toml │ ├── Makefile.toml │ ├── README.md │ ├── index.html │ └── src │ │ └── lib.rs ├── websocket │ ├── Cargo.lock │ ├── Cargo.toml │ ├── Makefile.toml │ ├── README.md │ ├── index.html │ └── src │ │ ├── client.rs │ │ ├── server.rs │ │ └── shared.rs └── window_events │ ├── Cargo.toml │ ├── Makefile.toml │ ├── README.md │ ├── index.html │ ├── pkg │ └── .keep │ └── src │ └── lib.rs └── src ├── app ├── cfg.rs ├── cmd_manager.rs ├── cmds.rs ├── data.rs ├── effect.rs ├── get_element.rs ├── message_mapper.rs ├── mod.rs ├── orders │ ├── container.rs │ ├── mod.rs │ └── proxy.rs ├── render_info.rs ├── stream_manager.rs ├── streams │ ├── backoff_stream.rs │ ├── event_stream.rs │ └── mod.rs ├── sub_manager.rs └── subs │ ├── mod.rs │ └── url_requested.rs ├── browser ├── dom │ ├── cast.rs │ ├── css_units.rs │ ├── event_handler.rs │ ├── mod.rs │ ├── namespace.rs │ └── virtual_dom_bridge.rs ├── json │ └── mod.rs ├── mod.rs ├── service │ ├── mod.rs │ └── routing.rs ├── url.rs └── util.rs ├── dom_entity_names ├── attributes │ ├── attribute_names.rs │ └── mod.rs ├── events │ ├── event_names.rs │ └── mod.rs ├── mod.rs ├── styles │ ├── mod.rs │ └── style_names.rs └── tags │ ├── mod.rs │ └── tag_names.rs ├── helpers.rs ├── lib.rs ├── shortcuts.rs └── virtual_dom ├── attrs.rs ├── el_ref.rs ├── event_handler_manager ├── event_handler.rs ├── listener.rs └── mod.rs ├── mailbox.rs ├── mod.rs ├── node ├── el.rs ├── into_nodes.rs ├── mod.rs └── text.rs ├── patch ├── mod.rs └── patch_gen.rs ├── style.rs ├── to_classes.rs ├── update_el.rs ├── values.rs └── view.rs /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Main 2 | 3 | on: [push, pull_request] 4 | 5 | env: 6 | RUST_BACKTRACE: 1 7 | 8 | jobs: 9 | verify: 10 | runs-on: ${{ matrix.os }} 11 | if: "!contains(github.event.head_commit.message, '[ci skip]')" 12 | strategy: 13 | matrix: 14 | os: [ubuntu-latest, macos-latest] # windows-latest is too slow at the moment 15 | steps: 16 | - name: Checkout sources 17 | uses: actions/checkout@v3 18 | 19 | # @TODO: Doesn't work + `cargo.lock` is not usable anymore. 20 | # - name: Cache Rust dependencies 21 | # uses: actions/cache@v1 22 | # with: 23 | # path: target 24 | # key: ${{ runner.OS }}-build-${{ hashFiles('**\Cargo.lock') }} 25 | # restore-keys: | 26 | # ${{ runner.OS }}-build- 27 | 28 | - name: Install stable Rust 29 | uses: actions-rs/toolchain@v1 30 | with: 31 | profile: minimal 32 | toolchain: stable 33 | 34 | - name: Cache Dependencies 35 | uses: Swatinem/rust-cache@v1 36 | 37 | - uses: actions/cache@v3 38 | id: cache-cargo-bins 39 | with: 40 | path: | 41 | ~/.cargo/bin/wasm-pack* 42 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 43 | 44 | - name: Install wasm-pack 45 | if: steps.cache-cargo-bins.outputs.cache-hit != 'true' 46 | run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh 47 | 48 | - name: Install cargo-make 49 | uses: davidB/rust-cargo-make@v1 50 | 51 | - name: Run tests - Windows or Ubuntu 52 | if: matrix.os == 'ubuntu-latest' || matrix.os == 'windows-latest' 53 | run: | 54 | cargo make test_h chrome 55 | cargo make test_h firefox 56 | cargo make test_examples_firefox 57 | 58 | # uncomment once fixes from https://webkit.org/blog/9609/release-notes-for-safari-technology-preview-94/ 59 | # are included in installed Safari 60 | # - name: Run tests - macOS 61 | # if: matrix.os == 'macos-latest' 62 | # run: | 63 | # sudo safaridriver --enable 64 | # cargo make test_h safari 65 | 66 | - name: Verify 67 | run: cargo make verify_for_ci 68 | 69 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # --- Files --- 2 | **/*.ts 3 | **.wasm 4 | **/*.rs.bk 5 | **/*.log 6 | 7 | # --- Dependencies and Build artifacts --- 8 | **/node_modules 9 | **/target 10 | **/pkg/* 11 | 12 | # --- IDE metadata --- 13 | **/.idea 14 | **/.vscode 15 | -------------------------------------------------------------------------------- /BACKERS.md: -------------------------------------------------------------------------------- 1 | # Awesome Backers 2 | 3 | --- 4 | 5 | - Tomáš Lauer 6 | - [@sparky8251](https://github.com/sparky8251) 7 | - [@arn-the-long-beard](https://github.com/arn-the-long-beard) 8 | 9 | - Lodewijk Antonides 10 | - António Cascalheira 11 | - Shawn MacIntyre 12 | - @km-tr 13 | - @sabine 14 | - Craig Mayhew 15 | - Ke Ding 16 | - Yann Delaby 17 | - Dusty Pomerleau 18 | - Devin Alvaro 19 | - Jesus Guzman Jr. 20 | 21 | --- 22 | 23 | **Thank YOU!** 24 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Thanks for your interest in contributing to Seed. Bug reports, API improvements, 2 | performance improvements and new features are all welcome - as issues, or PRs. 3 | 4 | Required tools to build and test: 5 | - Rust 6 | - Clippy 7 | - Rustfmt 8 | - Cargo-make 9 | 10 | Before submitting a PR, please run `cargo make verify`: This will run `fmt`, `clippy`, build all examples, and run all tests. 11 | 12 | Recommended starting points: 13 | - Open issues 14 | - Adding and improving tests 15 | - Friction points in your Seed apps 16 | - `//todo` comments in code 17 | 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 David O'Connor 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /RELEASE_CHECKLIST.md: -------------------------------------------------------------------------------- 1 | # Release Checklist 2 | 3 | This is a list of steps to complete when making a new release. 4 | 5 | # Before the release 6 | 7 | - [ ] 1. Create a new issue in the Seed repo with the name `Seed x.x.x` and copy-paste this checklist into it (also add blockers and additional tasks, if exist). 8 | - [ ] 2. Update all official examples. 9 | - [ ] 3. Review the commit and PR history since last release. Ensure that all relevant 10 | changes are included in `CHANGELOG.md`, and that breaking changes 11 | are specifically annotated. 12 | - [ ] 4. Ensure the `README.md` reflects API changes. 13 | - [ ] 5. Update the `CHANGELOG.md` with the new release version. 14 | - [ ] 6. Ensure the version listed in `Cargo.toml` is updated. 15 | - [ ] 7. Update Rust tools: `rustup update`. 16 | - [ ] 8. Run `cargo make populate_all` to synchronize `St`, `At` and other enums with official values. 17 | - [ ] 9. Run `cargo make verify` to ensure tests pass, and `clippy` / `fmt` are run. 18 | - [ ] 10. Commit and push the repo. 19 | - [ ] 11. Check that CI pipeline passed. 20 | - [ ] 12. Run `cargo package`. 21 | - [ ] 13. Run `cargo publish`. 22 | - [ ] 14. Add a release on [Github](https://github.com/seed-rs/seed/releases), following the format of previous releases. 23 | - [ ] 15. Verify the [docs page](https://docs.rs/seed/) updated correctly. 24 | 25 | # After the release 26 | 27 | - [ ] 16. Update all quickstarts. 28 | - [ ] 17. Write documentation for the current release on the website. 29 | - [ ] 18. Make sure the website's version selector shows the released version by default. 30 | - [ ] 19. Notify authors of community tutorials, quickstarts and examples about a new Seed version. 31 | - [ ] 20. Write announcements (chat, forum, etc.). 32 | 33 | -------------------------------------------------------------------------------- /examples/animation/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "animation" 3 | version = "0.1.0" 4 | authors = ["David O'Connor "] 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type = ["cdylib"] 9 | 10 | [dependencies] 11 | seed = {path = "../../"} 12 | rand = "0.8.5" 13 | # https://docs.rs/getrandom/0.2.0/getrandom/#webassembly-support 14 | getrandom = { version = "0.2.8", features = ["js"] } 15 | -------------------------------------------------------------------------------- /examples/animation/Makefile.toml: -------------------------------------------------------------------------------- 1 | extend = "../../Makefile.toml" 2 | 3 | # ---- BUILD ---- 4 | 5 | [tasks.build] 6 | alias = "default_build" 7 | 8 | [tasks.build_release] 9 | alias = "default_build_release" 10 | 11 | # ---- START ---- 12 | 13 | [tasks.start] 14 | alias = "default_start" 15 | 16 | [tasks.start_release] 17 | alias = "default_start_release" 18 | 19 | # ---- TEST ---- 20 | 21 | [tasks.test_firefox] 22 | alias = "default_test_firefox" 23 | 24 | # ---- LINT ---- 25 | 26 | [tasks.clippy] 27 | alias = "default_clippy" 28 | -------------------------------------------------------------------------------- /examples/animation/README.md: -------------------------------------------------------------------------------- 1 | ## Animation example 2 | 3 | How to make a basic animation with random generators. 4 | 5 | --- 6 | 7 | ```bash 8 | cargo make start 9 | ``` 10 | 11 | Open [127.0.0.1:8000](http://127.0.0.1:8000) in your browser. -------------------------------------------------------------------------------- /examples/animation/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Animation example 8 | 9 | 10 | 11 |
12 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /examples/auth/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "auth" 3 | version = "0.1.0" 4 | authors = ["Martin Kavík "] 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type = ["cdylib"] 9 | 10 | [dependencies] 11 | gloo-console = "0.2.3" 12 | gloo-net = "0.2.6" 13 | gloo-storage = "0.2.2" 14 | seed = { path = "../../", features = ["routing"] } 15 | serde = "1.0.152" 16 | -------------------------------------------------------------------------------- /examples/auth/Makefile.toml: -------------------------------------------------------------------------------- 1 | extend = "../../Makefile.toml" 2 | 3 | # ---- BUILD ---- 4 | 5 | [tasks.build] 6 | alias = "default_build" 7 | 8 | [tasks.build_release] 9 | alias = "default_build_release" 10 | 11 | # ---- START ---- 12 | 13 | [tasks.start] 14 | alias = "default_start" 15 | 16 | [tasks.start_release] 17 | alias = "default_start_release" 18 | 19 | # ---- TEST ---- 20 | 21 | [tasks.test_firefox] 22 | alias = "default_test_firefox" 23 | 24 | # ---- LINT ---- 25 | 26 | [tasks.clippy] 27 | alias = "default_clippy" 28 | -------------------------------------------------------------------------------- /examples/auth/README.md: -------------------------------------------------------------------------------- 1 | ## Auth example 2 | 3 | How to implement login / logout. 4 | 5 | --- 6 | 7 | ```bash 8 | cargo make start 9 | ``` 10 | 11 | Open [127.0.0.1:8000](http://127.0.0.1:8000) in your browser. 12 | -------------------------------------------------------------------------------- /examples/auth/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Auth example 8 | 9 | 10 | 11 |
12 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /examples/bunnies/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bunnies" 3 | version = "0.1.0" 4 | authors = ["Martin Kavík "] 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type = ["cdylib"] 9 | # https://github.com/leudz/shipyard/issues/129 10 | path = "src/empty_lib.rs" 11 | 12 | [dependencies] 13 | seed = { path = "../../" } 14 | rand = { version = "0.8.5", features = ["small_rng"] } 15 | nalgebra = "0.32.1" 16 | awsm_web = { version = "0.38.0", features = ["tick", "webgl", "loaders", "audio", "serde_iso"], default-features = false } 17 | shipyard = { version = "0.6.2", features = ["thread_local"], default-features = false } 18 | -------------------------------------------------------------------------------- /examples/bunnies/Makefile.toml: -------------------------------------------------------------------------------- 1 | extend = "../../Makefile.toml" 2 | 3 | # ---- BUILD ---- 4 | 5 | [tasks.build] 6 | alias = "default_build" 7 | 8 | [tasks.build_release] 9 | alias = "default_build_release" 10 | 11 | # ---- START ---- 12 | 13 | [tasks.start] 14 | alias = "default_start" 15 | 16 | [tasks.start_release] 17 | alias = "default_start_release" 18 | 19 | # ---- TEST ---- 20 | 21 | [tasks.test_firefox] 22 | alias = "default_test_firefox" 23 | 24 | # ---- LINT ---- 25 | 26 | [tasks.clippy] 27 | alias = "default_clippy" 28 | -------------------------------------------------------------------------------- /examples/bunnies/README.md: -------------------------------------------------------------------------------- 1 | ## Bunnies example 2 | 3 | Intended as a demo of [Shipyard](https://github.com/leudz/shipyard) (Entity Component System) integration. 4 | 5 | _Note:_: Ported from [Shipyard demo](https://github.com/leudz/shipyard/tree/23f2998296f690aee78972f9cfe06dfd73b7971c/demo). 6 | 7 | --- 8 | 9 | ```bash 10 | cargo make start 11 | ``` 12 | 13 | Open [127.0.0.1:8000](http://127.0.0.1:8000) in your browser. 14 | -------------------------------------------------------------------------------- /examples/bunnies/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | Bunnies example 11 | 12 | 13 |
14 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /examples/bunnies/public/media/bunny.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seed-rs/seed/d39e618861f4da46a598b76cd2048d01b511fda2/examples/bunnies/public/media/bunny.png -------------------------------------------------------------------------------- /examples/bunnies/public/media/fragment.glsl: -------------------------------------------------------------------------------- 1 | precision mediump float; 2 | 3 | uniform sampler2D u_sampler; 4 | varying vec2 v_uv; 5 | 6 | void main() { 7 | gl_FragColor = texture2D(u_sampler, v_uv); 8 | } 9 | -------------------------------------------------------------------------------- /examples/bunnies/public/media/vertex.glsl: -------------------------------------------------------------------------------- 1 | precision mediump float; 2 | 3 | attribute vec2 a_vertex; 4 | attribute vec2 a_position; 5 | 6 | varying vec2 v_uv; 7 | 8 | uniform mat4 u_size; 9 | uniform mat4 u_camera; 10 | 11 | void main() { 12 | mat4 transform = mat4(1.0); 13 | 14 | //https://www.geeks3d.com/20141114/glsl-4x4-matrix-mat4-fields/ 15 | transform[3] = vec4(a_position, 0.0, 1.0); 16 | 17 | mat4 modelViewProjection = u_camera * transform; 18 | 19 | gl_Position = modelViewProjection * (u_size * vec4(a_vertex,0,1)); 20 | v_uv = a_vertex; 21 | } 22 | -------------------------------------------------------------------------------- /examples/bunnies/public/style.css: -------------------------------------------------------------------------------- 1 | /* Global */ 2 | html { 3 | box-sizing: border-box; 4 | } 5 | *, *:before, *:after { 6 | box-sizing: inherit; 7 | } 8 | 9 | html,body { 10 | padding: 0; 11 | margin: 0; 12 | font-family: Arial, Helvetica, sans-serif; 13 | } 14 | 15 | canvas { 16 | position: absolute; 17 | top: 0; 18 | left: 0; 19 | padding: 0; 20 | margin: 0; 21 | } 22 | 23 | /* loading */ 24 | .loading { 25 | font-size: xx-large; 26 | width: 100%; 27 | text-align: center; 28 | position: absolute; 29 | top: 30vh; 30 | left: 0; 31 | } 32 | /* info */ 33 | .info { 34 | position: absolute; 35 | top: 0; 36 | left: 0; 37 | display: inline-block; 38 | 39 | background-color: rgb(42, 22, 191); 40 | color: white; 41 | } 42 | 43 | .info > * { 44 | margin: 10px; 45 | } -------------------------------------------------------------------------------- /examples/bunnies/src/components.rs: -------------------------------------------------------------------------------- 1 | use crate::geometry::*; 2 | //re-exported so its easier to just use components::* 3 | pub use crate::fps_counter::FpsCounter; 4 | pub use crate::hud::Hud; 5 | pub use crate::scene_renderer::SceneRenderer; 6 | 7 | pub struct ImageArea(pub Area); 8 | pub struct StageArea(pub Area); 9 | pub struct InstancePositions(pub Vec); 10 | pub struct Fps(pub u32); 11 | pub struct Timestamp(pub f64); 12 | #[derive(PartialEq)] 13 | pub enum Controller { 14 | Adding, 15 | Waiting, 16 | } 17 | 18 | //the bunnies 19 | pub struct Position(pub Point); 20 | pub struct Speed(pub Point); 21 | pub struct Gravity(pub f64); 22 | -------------------------------------------------------------------------------- /examples/bunnies/src/config.rs: -------------------------------------------------------------------------------- 1 | pub const N_BUNNIES_PER_TICK: usize = 100; 2 | pub const START_GRAVITY: f64 = 0.75; 3 | 4 | pub fn get_media_href(path: &str) -> String { 5 | format!("/public/media/{}", path) 6 | } 7 | -------------------------------------------------------------------------------- /examples/bunnies/src/empty_lib.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /examples/bunnies/src/fps_counter.rs: -------------------------------------------------------------------------------- 1 | use seed::window; 2 | 3 | pub struct FpsCounter { 4 | begin_time: f64, 5 | prev_time: f64, 6 | frames: usize, 7 | pub current: f64, 8 | } 9 | 10 | impl FpsCounter { 11 | pub fn new() -> Self { 12 | let begin_time = Self::now(); 13 | Self { 14 | begin_time, 15 | prev_time: begin_time, 16 | frames: 0, 17 | current: 0.0, 18 | } 19 | } 20 | 21 | pub fn now() -> f64 { 22 | window().performance().unwrap().now() 23 | } 24 | 25 | pub fn begin(&mut self) { 26 | self.begin_time = Self::now(); 27 | } 28 | 29 | pub fn end(&mut self) { 30 | self.frames += 1; 31 | let time = Self::now(); 32 | 33 | if time >= (self.prev_time + 1000.0) { 34 | self.current = ((self.frames * 1000) as f64) / (time - self.prev_time); 35 | self.prev_time = time; 36 | self.frames = 0; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /examples/bunnies/src/geometry.rs: -------------------------------------------------------------------------------- 1 | use rand::{rngs::SmallRng, Rng, SeedableRng}; 2 | 3 | #[derive(Debug, Clone)] 4 | pub struct Point { 5 | pub x: f64, 6 | pub y: f64, 7 | } 8 | 9 | impl Point { 10 | pub fn new_random() -> Self { 11 | let mut rng = SmallRng::from_entropy(); 12 | let x: f64 = rng.gen(); // random number in range [0, 1) 13 | let y: f64 = rng.gen(); // random number in range [0, 1) 14 | 15 | Self { x, y } 16 | } 17 | } 18 | 19 | #[derive(Debug, Clone)] 20 | pub struct Area { 21 | pub width: u32, 22 | pub height: u32, 23 | } 24 | 25 | pub const QUAD_GEOM_UNIT: [f32; 8] = [ 26 | 0.0, 1.0, // top-left 27 | 0.0, 0.0, //bottom-left 28 | 1.0, 1.0, // top-right 29 | 1.0, 0.0, // bottom-right 30 | ]; 31 | -------------------------------------------------------------------------------- /examples/bunnies/src/hud.rs: -------------------------------------------------------------------------------- 1 | #[derive(Default, Debug)] 2 | pub struct Hud { 3 | num_bunnies: usize, 4 | fps: u32, 5 | } 6 | 7 | impl Hud { 8 | pub fn update(&mut self, len: usize, fps: u32) { 9 | self.num_bunnies = len; 10 | self.fps = fps; 11 | } 12 | 13 | pub fn num_bunnies(&self) -> usize { 14 | self.num_bunnies 15 | } 16 | 17 | pub fn fps(&self) -> u32 { 18 | self.fps 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/bunnies/src/init_world.rs: -------------------------------------------------------------------------------- 1 | use crate::components::*; 2 | use crate::geometry::*; 3 | use crate::hud::Hud; 4 | use crate::scene_renderer::SceneRenderer; 5 | use shipyard::*; 6 | 7 | pub fn init_world(img_area: Area, stage_area: Area, hud: Hud, renderer: SceneRenderer) -> World { 8 | let world = World::default(); 9 | 10 | world.add_unique(ImageArea(img_area)); 11 | world.add_unique(StageArea(stage_area)); 12 | world.add_unique(InstancePositions(Vec::new())); 13 | world.add_unique(Fps(0)); 14 | world.add_unique(Controller::Waiting); 15 | world.add_unique(FpsCounter::new()); 16 | world.add_unique(Timestamp(0.0)); 17 | world.add_unique_non_send_sync(renderer); 18 | world.add_unique_non_send_sync(hud); 19 | 20 | world 21 | .borrow::<(ViewMut, ViewMut, ViewMut)>() 22 | .tight_pack(); 23 | 24 | world 25 | } 26 | -------------------------------------------------------------------------------- /examples/canvas/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "canvas" 3 | version = "0.1.0" 4 | authors = ["David O'Connor "] 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type = ["cdylib"] 9 | 10 | [dependencies] 11 | seed = {path = "../../"} 12 | -------------------------------------------------------------------------------- /examples/canvas/Makefile.toml: -------------------------------------------------------------------------------- 1 | extend = "../../Makefile.toml" 2 | 3 | # ---- BUILD ---- 4 | 5 | [tasks.build] 6 | alias = "default_build" 7 | 8 | [tasks.build_release] 9 | alias = "default_build_release" 10 | 11 | # ---- START ---- 12 | 13 | [tasks.start] 14 | alias = "default_start" 15 | 16 | [tasks.start_release] 17 | alias = "default_start_release" 18 | 19 | # ---- TEST ---- 20 | 21 | [tasks.test_firefox] 22 | alias = "default_test_firefox" 23 | 24 | # ---- LINT ---- 25 | 26 | [tasks.clippy] 27 | alias = "default_clippy" 28 | -------------------------------------------------------------------------------- /examples/canvas/README.md: -------------------------------------------------------------------------------- 1 | ## Canvas example 2 | 3 | How to make a canvas element. 4 | [Web-sys docs](https://rustwasm.github.io/wasm-bindgen/api/web_sys/struct.CanvasRenderingContext2d.html) 5 | [Web-sys example](https://rustwasm.github.io/wasm-bindgen/examples/2d-canvas.html) 6 | [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/drawWindow) 7 | 8 | --- 9 | 10 | ```bash 11 | cargo make start 12 | ``` 13 | 14 | Open [127.0.0.1:8000](http://127.0.0.1:8000) in your browser. -------------------------------------------------------------------------------- /examples/canvas/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Canvas example 8 | 9 | 10 | 11 |
12 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /examples/charts/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "charts" 3 | version = "0.1.0" 4 | authors = ["TatriX "] 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type = ["cdylib"] 9 | 10 | [dependencies] 11 | seed = {path = "../../"} 12 | itertools = "0.10.5" 13 | -------------------------------------------------------------------------------- /examples/charts/Makefile.toml: -------------------------------------------------------------------------------- 1 | extend = "../../Makefile.toml" 2 | 3 | # ---- BUILD ---- 4 | 5 | [tasks.build] 6 | alias = "default_build" 7 | 8 | [tasks.build_release] 9 | alias = "default_build_release" 10 | 11 | # ---- START ---- 12 | 13 | [tasks.start] 14 | alias = "default_start" 15 | 16 | [tasks.start_release] 17 | alias = "default_start_release" 18 | 19 | # ---- TEST ---- 20 | 21 | [tasks.test_firefox] 22 | alias = "default_test_firefox" 23 | 24 | # ---- LINT ---- 25 | 26 | [tasks.clippy] 27 | alias = "default_clippy" 28 | -------------------------------------------------------------------------------- /examples/charts/README.md: -------------------------------------------------------------------------------- 1 | ## Charts example 2 | 3 | Drawing charts with svg. 4 | 5 | --- 6 | 7 | ```bash 8 | cargo make start 9 | ``` 10 | 11 | Open [127.0.0.1:8000](http://127.0.0.1:8000) in your browser. 12 | -------------------------------------------------------------------------------- /examples/charts/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Charts example 8 | 9 | 10 | 11 |
12 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /examples/component_builder/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "component_builder" 3 | version = "0.1.0" 4 | authors = ["Martin Kavík "] 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type = ["cdylib"] 9 | 10 | [dependencies] 11 | seed = {path = "../../"} 12 | -------------------------------------------------------------------------------- /examples/component_builder/Makefile.toml: -------------------------------------------------------------------------------- 1 | extend = "../../Makefile.toml" 2 | 3 | # ---- BUILD ---- 4 | 5 | [tasks.build] 6 | alias = "default_build" 7 | 8 | [tasks.build_release] 9 | alias = "default_build_release" 10 | 11 | # ---- START ---- 12 | 13 | [tasks.start] 14 | alias = "default_start" 15 | 16 | [tasks.start_release] 17 | alias = "default_start_release" 18 | 19 | # ---- TEST ---- 20 | 21 | [tasks.test_firefox] 22 | alias = "default_test_firefox" 23 | 24 | # ---- LINT ---- 25 | 26 | [tasks.clippy] 27 | alias = "default_clippy" 28 | -------------------------------------------------------------------------------- /examples/component_builder/README.md: -------------------------------------------------------------------------------- 1 | ## Component builder example 2 | 3 | How to write reusable views / components with builder pattern. 4 | 5 | --- 6 | 7 | ```bash 8 | cargo make start 9 | ``` 10 | 11 | Open [127.0.0.1:8000](http://127.0.0.1:8000) in your browser. 12 | -------------------------------------------------------------------------------- /examples/component_builder/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Component builder example 8 | 9 | 10 | 11 |
12 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /examples/component_builder/src/lib.rs: -------------------------------------------------------------------------------- 1 | use seed::{prelude::*, *}; 2 | 3 | mod button; 4 | use button::Button; 5 | 6 | // ------ ------ 7 | // Init 8 | // ------ ------ 9 | 10 | fn init(_: Url, _: &mut impl Orders) -> Model { 11 | Model::default() 12 | } 13 | 14 | // ------ ------ 15 | // Model 16 | // ------ ------ 17 | 18 | type Model = i32; 19 | 20 | // ------ ------ 21 | // Update 22 | // ------ ------ 23 | 24 | enum Msg { 25 | Increment, 26 | Decrement, 27 | } 28 | 29 | #[allow(clippy::needless_pass_by_value)] 30 | fn update(msg: Msg, model: &mut Model, _: &mut impl Orders) { 31 | match msg { 32 | Msg::Increment => *model += 1, 33 | Msg::Decrement => *model -= 1, 34 | } 35 | } 36 | 37 | // ------ ------ 38 | // View 39 | // ------ ------ 40 | 41 | #[allow(clippy::trivially_copy_pass_by_ref)] 42 | fn view(model: &Model) -> Node { 43 | div![ 44 | style! { St::Display => "flex" }, 45 | Button::new("-") 46 | .disabled(true) 47 | .add_on_click(|| Msg::Decrement), 48 | Button::new("-") 49 | .secondary() 50 | .large() 51 | .outline() 52 | .add_on_click(|| Msg::Decrement), 53 | Button::new("-").add_on_click(|| Msg::Decrement), 54 | div![model], 55 | Button::new("+").add_on_click(|| Msg::Increment), 56 | Button::new("+") 57 | .secondary() 58 | .large() 59 | .outline() 60 | .add_on_click(|| Msg::Increment), 61 | Button::new("+") 62 | .disabled(true) 63 | .add_on_click(|| Msg::Increment), 64 | Button::new("seed-rs.org").a("https://seed-rs.org"), 65 | ] 66 | } 67 | 68 | // ------ ------ 69 | // Start 70 | // ------ ------ 71 | 72 | #[wasm_bindgen(start)] 73 | pub fn start() { 74 | App::start("app", init, update, view); 75 | } 76 | -------------------------------------------------------------------------------- /examples/counter/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "counter" 3 | version = "0.1.0" 4 | authors = ["David O'Connor "] 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type = ["cdylib"] 9 | 10 | [dependencies] 11 | seed = {path = "../../"} 12 | -------------------------------------------------------------------------------- /examples/counter/Makefile.toml: -------------------------------------------------------------------------------- 1 | extend = "../../Makefile.toml" 2 | 3 | # ---- BUILD ---- 4 | 5 | [tasks.build] 6 | alias = "default_build" 7 | 8 | [tasks.build_release] 9 | alias = "default_build_release" 10 | 11 | # ---- START ---- 12 | 13 | [tasks.start] 14 | alias = "default_start" 15 | 16 | [tasks.start_release] 17 | alias = "default_start_release" 18 | 19 | # ---- TEST ---- 20 | 21 | [tasks.test_firefox] 22 | alias = "default_test_firefox" 23 | 24 | # ---- LINT ---- 25 | 26 | [tasks.clippy] 27 | alias = "default_clippy" 28 | -------------------------------------------------------------------------------- /examples/counter/README.md: -------------------------------------------------------------------------------- 1 | ## Counter example 2 | 3 | Intended as a demo of basic functionality. 4 | 5 | --- 6 | 7 | ```bash 8 | cargo make start 9 | ``` 10 | 11 | Open [127.0.0.1:8000](http://127.0.0.1:8000) in your browser. -------------------------------------------------------------------------------- /examples/counter/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Counter example 8 | 9 | 10 | 11 |
12 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /examples/counter/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! A simple, cliché example demonstrating structure and syntax. 2 | //! Inspired by [Elm example](https://guide.elm-lang.org/architecture/buttons.html). 3 | 4 | // Some Clippy linter rules are ignored for the sake of simplicity. 5 | #![allow(clippy::needless_pass_by_value, clippy::trivially_copy_pass_by_ref)] 6 | 7 | use seed::{prelude::*, *}; 8 | 9 | // ------ ------ 10 | // Init 11 | // ------ ------ 12 | 13 | fn init(_: Url, _: &mut impl Orders) -> Model { 14 | Model::default() 15 | } 16 | 17 | // ------ ------ 18 | // Model 19 | // ------ ------ 20 | 21 | type Model = i32; 22 | 23 | // ------ ------ 24 | // Update 25 | // ------ ------ 26 | 27 | enum Msg { 28 | Increment, 29 | Decrement, 30 | } 31 | 32 | fn update(msg: Msg, model: &mut Model, _: &mut impl Orders) { 33 | match msg { 34 | Msg::Increment => *model += 1, 35 | Msg::Decrement => *model -= 1, 36 | } 37 | } 38 | 39 | // ------ ------ 40 | // View 41 | // ------ ------ 42 | 43 | fn view(model: &Model) -> Node { 44 | div![ 45 | button![ev(Ev::Click, |_| Msg::Decrement), "-"], 46 | div![model], 47 | button![ev(Ev::Click, |_| Msg::Increment), "+"], 48 | ] 49 | } 50 | 51 | // ------ ------ 52 | // Start 53 | // ------ ------ 54 | 55 | #[wasm_bindgen(start)] 56 | pub fn start() { 57 | App::start("app", init, update, view); 58 | } 59 | -------------------------------------------------------------------------------- /examples/counters/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "counters" 3 | version = "0.1.0" 4 | authors = ["Martin Kavík "] 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type = ["cdylib"] 9 | 10 | [dependencies] 11 | seed = {path = "../../"} 12 | -------------------------------------------------------------------------------- /examples/counters/Makefile.toml: -------------------------------------------------------------------------------- 1 | extend = "../../Makefile.toml" 2 | 3 | # ---- BUILD ---- 4 | 5 | [tasks.build] 6 | alias = "default_build" 7 | 8 | [tasks.build_release] 9 | alias = "default_build_release" 10 | 11 | # ---- START ---- 12 | 13 | [tasks.start] 14 | alias = "default_start" 15 | 16 | [tasks.start_release] 17 | alias = "default_start_release" 18 | 19 | # ---- TEST ---- 20 | 21 | [tasks.test_firefox] 22 | alias = "default_test_firefox" 23 | 24 | # ---- LINT ---- 25 | 26 | [tasks.clippy] 27 | alias = "default_clippy" 28 | -------------------------------------------------------------------------------- /examples/counters/README.md: -------------------------------------------------------------------------------- 1 | ## Counters example 2 | 3 | How to use multiple "components". 4 | 5 | --- 6 | 7 | ```bash 8 | cargo make start 9 | ``` 10 | 11 | Open [127.0.0.1:8000](http://127.0.0.1:8000) in your browser. 12 | -------------------------------------------------------------------------------- /examples/counters/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Counters example 8 | 9 | 10 | 11 |
12 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /examples/counters/src/counter.rs: -------------------------------------------------------------------------------- 1 | use seed::{prelude::*, *}; 2 | 3 | // ------ ------ 4 | // Init 5 | // ------ ------ 6 | 7 | pub const fn init() -> Model { 8 | 0 9 | } 10 | 11 | // ------ ------ 12 | // Model 13 | // ------ ------ 14 | 15 | pub type Model = i32; 16 | 17 | // ------ ------ 18 | // Update 19 | // ------ ------ 20 | 21 | #[derive(Clone, Copy)] 22 | pub enum Msg { 23 | Increment, 24 | Decrement, 25 | } 26 | 27 | pub fn update(msg: Msg, model: &mut Model) { 28 | match msg { 29 | Msg::Increment => *model += 1, 30 | Msg::Decrement => *model -= 1, 31 | } 32 | } 33 | 34 | // ------ ------ 35 | // View 36 | // ------ ------ 37 | 38 | pub fn view(model: Model) -> Node { 39 | div![ 40 | button![ev(Ev::Click, |_| Msg::Decrement), "-"], 41 | div![model], 42 | button![ev(Ev::Click, |_| Msg::Increment), "+"], 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /examples/counters/src/lib.rs: -------------------------------------------------------------------------------- 1 | use seed::{prelude::*, *}; 2 | 3 | mod counter; 4 | 5 | type CounterId = usize; 6 | 7 | // ------ ------ 8 | // Init 9 | // ------ ------ 10 | 11 | fn init(_: Url, _: &mut impl Orders) -> Model { 12 | Model { 13 | counters: (0..3).map(|_| counter::init()).collect(), 14 | } 15 | } 16 | 17 | // ------ ------ 18 | // Model 19 | // ------ ------ 20 | 21 | struct Model { 22 | counters: Vec, 23 | } 24 | 25 | // ------ ------ 26 | // Update 27 | // ------ ------ 28 | 29 | enum Msg { 30 | Counter(counter::Msg, CounterId), 31 | } 32 | 33 | #[allow(clippy::needless_pass_by_value)] 34 | fn update(msg: Msg, model: &mut Model, _: &mut impl Orders) { 35 | match msg { 36 | Msg::Counter(msg, id) => counter::update(msg, &mut model.counters[id]), 37 | } 38 | } 39 | 40 | // ------ ------ 41 | // View 42 | // ------ ------ 43 | 44 | fn view(model: &Model) -> Node { 45 | div![ 46 | style! { St::Display => "flex" }, 47 | model.counters.iter().enumerate().map(|(id, model)| { 48 | counter::view(*model).map_msg(move |counter_msg| Msg::Counter(counter_msg, id)) 49 | }) 50 | ] 51 | } 52 | 53 | // ------ ------ 54 | // Start 55 | // ------ ------ 56 | 57 | #[wasm_bindgen(start)] 58 | pub fn start() { 59 | App::start("app", init, update, view); 60 | } 61 | -------------------------------------------------------------------------------- /examples/custom_elements/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "custom_elements" 3 | version = "0.1.0" 4 | authors = ["Martin Kavík "] 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type = ["cdylib"] 9 | 10 | [dependencies] 11 | seed = {path = "../../"} 12 | -------------------------------------------------------------------------------- /examples/custom_elements/Makefile.toml: -------------------------------------------------------------------------------- 1 | extend = "../../Makefile.toml" 2 | 3 | # ---- BUILD ---- 4 | 5 | [tasks.build] 6 | alias = "default_build" 7 | 8 | [tasks.build_release] 9 | alias = "default_build_release" 10 | 11 | # ---- START ---- 12 | 13 | [tasks.start] 14 | alias = "default_start" 15 | 16 | [tasks.start_release] 17 | alias = "default_start_release" 18 | 19 | # ---- TEST ---- 20 | 21 | [tasks.test_firefox] 22 | alias = "default_test_firefox" 23 | 24 | # ---- LINT ---- 25 | 26 | [tasks.clippy] 27 | alias = "default_clippy" 28 | -------------------------------------------------------------------------------- /examples/custom_elements/README.md: -------------------------------------------------------------------------------- 1 | ## Custom elements example 2 | 3 | How to create and use custom elements. 4 | 5 | --- 6 | 7 | ```bash 8 | cargo make start 9 | ``` 10 | 11 | Open [127.0.0.1:8000](http://127.0.0.1:8000) in your browser. 12 | -------------------------------------------------------------------------------- /examples/custom_elements/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Custom elements example 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /examples/custom_elements/public/checkbox-tristate.js: -------------------------------------------------------------------------------- 1 | import { LitElement, html, css } from 'https://unpkg.com/lit-element/lit-element.js?module'; 2 | 3 | class CheckboxTristateElement extends LitElement { 4 | static get properties() { 5 | return { 6 | name: { type: String }, 7 | label: { type: String }, 8 | state: { type: String }, 9 | }; 10 | } 11 | 12 | constructor() { 13 | super(); 14 | this.name = null; 15 | this.label = ""; 16 | this.state = "unchecked"; 17 | } 18 | 19 | render() { 20 | return html` 21 |
22 | 23 | 24 |
`; 25 | } 26 | 27 | _onClick() { 28 | this.dispatchEvent(new CustomEvent("cluck", { 29 | detail: this.name 30 | })); 31 | } 32 | 33 | updated(changedProperties) { 34 | if (changedProperties.has("state")) { 35 | const checkbox = this.getElementsByTagName("input")[0]; 36 | switch (this.state) { 37 | case "unchecked": 38 | checkbox.checked = false; 39 | checkbox.indeterminate = false; 40 | break 41 | 42 | case "indeterminate": 43 | checkbox.checked = false; 44 | checkbox.indeterminate = true; 45 | break 46 | 47 | case "checked": 48 | checkbox.checked = true; 49 | checkbox.indeterminate = false; 50 | break 51 | } 52 | } 53 | } 54 | 55 | createRenderRoot() { 56 | return this; 57 | } 58 | } 59 | 60 | customElements.define('checkbox-tristate', CheckboxTristateElement); 61 | -------------------------------------------------------------------------------- /examples/custom_elements/public/code-block.js: -------------------------------------------------------------------------------- 1 | import { LitElement, html, css } from 'https://unpkg.com/lit-element/lit-element.js?module'; 2 | import { unsafeHTML } from 'https://unpkg.com/lit-html/directives/unsafe-html.js?module'; 3 | 4 | class CodeBlockElement extends LitElement { 5 | static get properties() { 6 | return { 7 | lang: "", 8 | code: "", 9 | }; 10 | } 11 | 12 | render() { 13 | const highlightedCode = highlightCode(this.code, this.lang); 14 | return html`
${unsafeHTML(highlightedCode)}
`; 15 | } 16 | 17 | createRenderRoot() { 18 | return this; 19 | } 20 | } 21 | customElements.define('code-block', CodeBlockElement); 22 | 23 | function highlightCode(code, lang) { 24 | // https://highlightjs.readthedocs.io/en/latest/api.html#highlightauto-value-languagesubset 25 | const highlightedCode = 26 | window 27 | .hljs 28 | .highlightAuto(code, lang ? [lang] : undefined) 29 | .value; 30 | return highlightedCode 31 | } 32 | -------------------------------------------------------------------------------- /examples/custom_elements/public/feather-icon.js: -------------------------------------------------------------------------------- 1 | import { LitElement, html, css } from 'https://unpkg.com/lit-element/lit-element.js?module'; 2 | import { unsafeHTML } from 'https://unpkg.com/lit-html/directives/unsafe-html.js?module'; 3 | 4 | class FeatherIconElement extends LitElement { 5 | static get properties() { 6 | return { 7 | icon: { type: String }, 8 | width: { type: Number }, 9 | height: { type: Number }, 10 | }; 11 | } 12 | 13 | constructor() { 14 | super(); 15 | this.width = 24; 16 | this.height = 24; 17 | } 18 | 19 | render() { 20 | const svg = feather.icons[this.icon].toSvg({ 21 | width: this.width, 22 | height: this.height, 23 | }); 24 | return html`${unsafeHTML(svg)}`; 25 | } 26 | 27 | createRenderRoot() { 28 | return this; 29 | } 30 | } 31 | 32 | customElements.define('feather-icon', FeatherIconElement); 33 | -------------------------------------------------------------------------------- /examples/custom_elements/public/highlight/github.css: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | github.com style (c) Vasily Polovnyov 4 | 5 | */ 6 | 7 | .hljs { 8 | display: block; 9 | overflow-x: auto; 10 | padding: 0.5em; 11 | color: #333; 12 | background: #f8f8f8; 13 | } 14 | 15 | .hljs-comment, 16 | .hljs-quote { 17 | color: #998; 18 | font-style: italic; 19 | } 20 | 21 | .hljs-keyword, 22 | .hljs-selector-tag, 23 | .hljs-subst { 24 | color: #333; 25 | font-weight: bold; 26 | } 27 | 28 | .hljs-number, 29 | .hljs-literal, 30 | .hljs-variable, 31 | .hljs-template-variable, 32 | .hljs-tag .hljs-attr { 33 | color: #008080; 34 | } 35 | 36 | .hljs-string, 37 | .hljs-doctag { 38 | color: #d14; 39 | } 40 | 41 | .hljs-title, 42 | .hljs-section, 43 | .hljs-selector-id { 44 | color: #900; 45 | font-weight: bold; 46 | } 47 | 48 | .hljs-subst { 49 | font-weight: normal; 50 | } 51 | 52 | .hljs-type, 53 | .hljs-class .hljs-title { 54 | color: #458; 55 | font-weight: bold; 56 | } 57 | 58 | .hljs-tag, 59 | .hljs-name, 60 | .hljs-attribute { 61 | color: #000080; 62 | font-weight: normal; 63 | } 64 | 65 | .hljs-regexp, 66 | .hljs-link { 67 | color: #009926; 68 | } 69 | 70 | .hljs-symbol, 71 | .hljs-bullet { 72 | color: #990073; 73 | } 74 | 75 | .hljs-built_in, 76 | .hljs-builtin-name { 77 | color: #0086b3; 78 | } 79 | 80 | .hljs-meta { 81 | color: #999; 82 | font-weight: bold; 83 | } 84 | 85 | .hljs-deletion { 86 | background: #fdd; 87 | } 88 | 89 | .hljs-addition { 90 | background: #dfd; 91 | } 92 | 93 | .hljs-emphasis { 94 | font-style: italic; 95 | } 96 | 97 | .hljs-strong { 98 | font-weight: bold; 99 | } 100 | -------------------------------------------------------------------------------- /examples/custom_elements/public/math-tex.js: -------------------------------------------------------------------------------- 1 | import { LitElement, html, css } from 'https://unpkg.com/lit-element/lit-element.js?module'; 2 | import { unsafeHTML } from 'https://unpkg.com/lit-html/directives/unsafe-html.js?module'; 3 | 4 | class MathTexElement extends LitElement { 5 | 6 | connectedCallback() { 7 | super.connectedCallback(); 8 | this.style.display = "block"; 9 | } 10 | 11 | render() { 12 | const tex = this.innerHTML; 13 | const html_tex = window.MathJax.tex2mml(tex); 14 | 15 | return html`${unsafeHTML(html_tex)}`; 16 | } 17 | } 18 | 19 | customElements.define('math-tex', MathTexElement); 20 | -------------------------------------------------------------------------------- /examples/custom_elements/src/checkbox_tristate.rs: -------------------------------------------------------------------------------- 1 | use seed::{prelude::*, *}; 2 | use std::fmt; 3 | 4 | // ------ ------ 5 | // View 6 | // ------ ------ 7 | 8 | pub fn view(name: &str, label: &str, state: State, on_click: F) -> Node 9 | where 10 | F: FnOnce(String) -> Ms + Clone + 'static, 11 | { 12 | let handler = move |event: web_sys::Event| { 13 | let event = event.dyn_ref::().unwrap().clone(); 14 | on_click(event.detail().as_string().unwrap()) 15 | }; 16 | custom![ 17 | Tag::from("checkbox-tristate"), 18 | ev(Ev::from("cluck"), handler), 19 | attrs! { 20 | At::from("name") => name 21 | At::from("label") => label 22 | At::from("state") => state 23 | }, 24 | ] 25 | } 26 | 27 | // ------ State ------ 28 | 29 | #[derive(Debug, Copy, Clone, Eq, PartialEq)] 30 | pub enum State { 31 | Unchecked, 32 | Indeterminate, 33 | Checked, 34 | } 35 | 36 | impl State { 37 | pub const fn next(self) -> Self { 38 | match self { 39 | Self::Unchecked => Self::Indeterminate, 40 | Self::Indeterminate => Self::Checked, 41 | Self::Checked => Self::Unchecked, 42 | } 43 | } 44 | } 45 | 46 | impl Default for State { 47 | fn default() -> Self { 48 | Self::Unchecked 49 | } 50 | } 51 | 52 | impl fmt::Display for State { 53 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 54 | let state = match self { 55 | Self::Unchecked => "unchecked", 56 | Self::Indeterminate => "indeterminate", 57 | Self::Checked => "checked", 58 | }; 59 | write!(f, "{state}") 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /examples/custom_elements/src/code_block.rs: -------------------------------------------------------------------------------- 1 | use seed::{prelude::*, *}; 2 | 3 | // ------ ------ 4 | // View 5 | // ------ ------ 6 | 7 | pub fn view(lang: &str, code: &str) -> Node { 8 | custom![ 9 | Tag::from("code-block"), 10 | attrs! { 11 | At::from("lang") => lang, 12 | At::from("code") => code, 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /examples/custom_elements/src/feather_icon.rs: -------------------------------------------------------------------------------- 1 | use seed::{prelude::*, *}; 2 | 3 | // ------ ------ 4 | // View 5 | // ------ ------ 6 | 7 | pub fn view(icon: &str, width: Option, height: Option) -> Node { 8 | custom![ 9 | Tag::from("feather-icon"), 10 | attrs! { 11 | At::from("icon") => icon, 12 | At::Width => width.map_or(AtValue::Ignored, |width| AtValue::Some(width.to_string())), 13 | At::Height => height.map_or(AtValue::Ignored, |height| AtValue::Some(height.to_string())), 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /examples/custom_elements/src/lib.rs: -------------------------------------------------------------------------------- 1 | use seed::{prelude::*, *}; 2 | 3 | mod checkbox_tristate; 4 | mod code_block; 5 | mod feather_icon; 6 | mod math_tex; 7 | mod sl_input; 8 | 9 | // ------ ------ 10 | // Init 11 | // ------ ------ 12 | 13 | fn init(_: Url, _: &mut impl Orders) -> Model { 14 | Model::default() 15 | } 16 | 17 | // ------ ------ 18 | // Model 19 | // ------ ------ 20 | 21 | #[derive(Default)] 22 | struct Model { 23 | pub checkbox_state: checkbox_tristate::State, 24 | input_value: String, 25 | } 26 | 27 | // ------ ------ 28 | // Update 29 | // ------ ------ 30 | 31 | enum Msg { 32 | RotateCheckboxState(String), 33 | InputChanged(String), 34 | } 35 | 36 | #[allow(clippy::needless_pass_by_value)] 37 | fn update(msg: Msg, model: &mut Model, _: &mut impl Orders) { 38 | match msg { 39 | Msg::RotateCheckboxState(name) => { 40 | if name == "checkbox-tristate" { 41 | model.checkbox_state = model.checkbox_state.next(); 42 | } 43 | } 44 | Msg::InputChanged(value) => { 45 | model.input_value = value; 46 | } 47 | } 48 | } 49 | 50 | // ------ ------ 51 | // View 52 | // ------ ------ 53 | 54 | fn view(model: &Model) -> impl IntoNodes { 55 | vec![ 56 | div![ 57 | "checkbox-tristate", 58 | checkbox_tristate::view( 59 | "checkbox-tristate", 60 | "Label", 61 | model.checkbox_state, 62 | Msg::RotateCheckboxState 63 | ), 64 | ], 65 | hr![], 66 | div![ 67 | "code-block", 68 | code_block::view("rust", "let number: Option = Some(10_200);"), 69 | ], 70 | hr![], 71 | div![ 72 | "feather-icon", 73 | feather_icon::view("shopping-cart", None, None), 74 | ], 75 | hr![], 76 | div![ 77 | "math-tex", 78 | math_tex::view(r"\mathbb{1} = \sum_i \lvert i \rangle \langle i \rvert"), 79 | ], 80 | hr![], 81 | div![ 82 | "sl-input", 83 | sl_input![sl_input::on_input(Msg::InputChanged)], 84 | &model.input_value, 85 | ], 86 | ] 87 | } 88 | 89 | // ------ ------ 90 | // Start 91 | // ------ ------ 92 | 93 | #[wasm_bindgen(start)] 94 | pub fn start() { 95 | App::start("app", init, update, view); 96 | } 97 | -------------------------------------------------------------------------------- /examples/custom_elements/src/math_tex.rs: -------------------------------------------------------------------------------- 1 | use seed::{prelude::*, *}; 2 | 3 | // ------ ------ 4 | // View 5 | // ------ ------ 6 | 7 | pub fn view(expression: &str) -> Node { 8 | custom![Tag::from("math-tex"), expression,] 9 | } 10 | -------------------------------------------------------------------------------- /examples/custom_elements/src/sl_input.rs: -------------------------------------------------------------------------------- 1 | use js_sys::Reflect; 2 | use seed::prelude::*; 3 | 4 | #[macro_export] 5 | macro_rules! sl_input { 6 | ( $($part:expr),* $(,)? ) => { 7 | { 8 | custom![ 9 | Tag::from("sl-input"), 10 | $( $part )* 11 | ] 12 | } 13 | }; 14 | } 15 | 16 | pub fn on_input( 17 | handler: impl FnOnce(String) -> Ms + Clone + 'static, 18 | ) -> EventHandler { 19 | ev(Ev::from("sl-input"), |event| { 20 | let event_target = event.target().unwrap(); 21 | let property_name = JsValue::from("value"); 22 | let value = Reflect::get(&event_target, &property_name).unwrap(); 23 | handler(value.as_string().unwrap()) 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /examples/drag_and_drop/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "drag_and_drop" 3 | version = "0.1.0" 4 | authors = ["arn-the-long-beard "] 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type = ["cdylib"] 9 | 10 | [dependencies] 11 | gloo-console = "0.2.3" 12 | seed = {path = "../../"} 13 | -------------------------------------------------------------------------------- /examples/drag_and_drop/Makefile.toml: -------------------------------------------------------------------------------- 1 | extend = "../../Makefile.toml" 2 | 3 | # ---- BUILD ---- 4 | 5 | [tasks.build] 6 | alias = "default_build" 7 | 8 | [tasks.build_release] 9 | alias = "default_build_release" 10 | 11 | # ---- START ---- 12 | 13 | [tasks.start] 14 | alias = "default_start" 15 | 16 | [tasks.start_release] 17 | alias = "default_start_release" 18 | 19 | # ---- TEST ---- 20 | 21 | [tasks.test_firefox] 22 | alias = "default_test_firefox" 23 | 24 | # ---- LINT ---- 25 | 26 | [tasks.clippy] 27 | alias = "default_clippy" 28 | -------------------------------------------------------------------------------- /examples/drag_and_drop/README.md: -------------------------------------------------------------------------------- 1 | ## Drag and Drop example 2 | 3 | How to create simple drag and drop. 4 | 5 | --- 6 | 7 | ```bash 8 | cargo make start 9 | ``` 10 | 11 | Open [127.0.0.1:8000](http://127.0.0.1:8000) in your browser. 12 | -------------------------------------------------------------------------------- /examples/drag_and_drop/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Drag and Drop example 8 | 9 | 10 | 11 |
12 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /examples/drop_zone/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "drop_zone" 3 | version = "0.1.0" 4 | authors = ["David O'Connor "] 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type = ["cdylib"] 9 | 10 | [dependencies] 11 | seed = {path = "../../"} 12 | -------------------------------------------------------------------------------- /examples/drop_zone/Makefile.toml: -------------------------------------------------------------------------------- 1 | extend = "../../Makefile.toml" 2 | 3 | # ---- BUILD ---- 4 | 5 | [tasks.build] 6 | alias = "default_build" 7 | 8 | [tasks.build_release] 9 | alias = "default_build_release" 10 | 11 | # ---- START ---- 12 | 13 | [tasks.start] 14 | alias = "default_start" 15 | 16 | [tasks.start_release] 17 | alias = "default_start_release" 18 | 19 | # ---- TEST ---- 20 | 21 | [tasks.test_firefox] 22 | alias = "default_test_firefox" 23 | 24 | # ---- LINT ---- 25 | 26 | [tasks.clippy] 27 | alias = "default_clippy" 28 | -------------------------------------------------------------------------------- /examples/drop_zone/README.md: -------------------------------------------------------------------------------- 1 | ## Drop-zone example 2 | 3 | How to create a drop-zone. 4 | 5 | --- 6 | 7 | ```bash 8 | cargo make start 9 | ``` 10 | 11 | Open [127.0.0.1:8000](http://127.0.0.1:8000) in your browser. 12 | -------------------------------------------------------------------------------- /examples/drop_zone/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Drop-zone example 8 | 9 | 10 | 11 |
12 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /examples/e2e_encryption/Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | 3 | members = [ 4 | "client", 5 | "server", 6 | ] 7 | -------------------------------------------------------------------------------- /examples/e2e_encryption/README.md: -------------------------------------------------------------------------------- 1 | ## E2E encryption example 2 | 3 | Demonstrates how to register, log in and communicate with the server over unsecured network. 4 | 5 | - The Seed app is served by [warp](https://crates.io/crates/warp). 6 | - Warp also provides REST API endpoints. 7 | - [opaque-ke](https://crates.io/crates/opaque-ke) is used for registration and log in. I strongly recommend to read its docs and the article ["OPAQUE: The Best Passwords Never Leave your Device"](https://blog.cloudflare.com/opaque-oblivious-passwords/) before you start to learn this example. 8 | - [cocoon](https://crates.io/crates/cocoon) is used for encrypted communication. Messages are encrypted with the shared key generated by `opaque-ke` in the final log in step. 9 | 10 | _Warning_: The code is heavily inspired by `opaque-ke` examples and simplified to demonstrate basic principles. So there is too much `expect` calls, some strangely named variables and other things that you probably don't want in a production app. Also I'm not a security expert, so I don't guarantee there are no security bugs - use the example as a proof-of-concept and inspiration for your projects. 11 | 12 | --- 13 | 14 | ```bash 15 | cargo make start 16 | ``` 17 | 18 | Open [127.0.0.1:8000](http://127.0.0.1:8000) in your browser. 19 | 20 | Click `Register`, then `Login` and then `Send` button. 21 | Watch the changes between clicks on the website, browser console log and server log. 22 | -------------------------------------------------------------------------------- /examples/e2e_encryption/client/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "client" 3 | version = "0.1.0" 4 | authors = ["Your Name "] 5 | edition = "2021" 6 | 7 | [lib] 8 | crate-type = ["cdylib"] 9 | path = "src/lib.rs" 10 | 11 | [dev-dependencies] 12 | wasm-bindgen-test = "0.3.20" 13 | 14 | [dependencies] 15 | seed = { path = "../../../" } 16 | 17 | # sync with the `rand_core`'s one in `shared` 18 | # change "wasm-bindgen" to "js" for >=0.2 19 | getrandom = { version = "0.1.16", features = ["wasm-bindgen"] } 20 | gloo-net = "0.2.6" 21 | gloo-console = "0.2.3" 22 | 23 | shared = { path = "../shared" } 24 | -------------------------------------------------------------------------------- /examples/e2e_encryption/client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | E2E encryption example 8 | 9 | 10 | 11 |
12 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /examples/e2e_encryption/server/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "server" 3 | version = "0.1.0" 4 | authors = ["Your Name "] 5 | edition = "2021" 6 | 7 | [dependencies] 8 | tokio = { version = "0.2", features = ["full"] } # sync with the one in `warp` 9 | warp = "0.2" 10 | 11 | shared = { path = "../shared" } 12 | -------------------------------------------------------------------------------- /examples/e2e_encryption/shared/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "shared" 3 | version = "0.1.0" 4 | authors = ["Your Name "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | cocoon = "0.3.0" 9 | curve25519-dalek = "3.0.2" 10 | opaque-ke = "0.2.1" 11 | sha2 = "0.9.2" 12 | scrypt = "0.5.0" 13 | generic-array = "0.14.4" 14 | digest = "0.9.0" 15 | rand_core = { version = "0.5.1" } # sync with the one in `opaque-ke` 16 | -------------------------------------------------------------------------------- /examples/e2e_encryption/shared/src/lib.rs: -------------------------------------------------------------------------------- 1 | use cocoon::MiniCocoon; 2 | use digest::Digest; 3 | use generic_array::GenericArray; 4 | use opaque_ke::{ 5 | ciphersuite::CipherSuite, errors::InternalPakeError, hash::Hash, slow_hash::SlowHash, 6 | }; 7 | use rand_core::{OsRng, RngCore}; 8 | 9 | pub use opaque_ke; 10 | pub use rand_core; 11 | 12 | pub struct DefaultCipherSuite; 13 | 14 | impl CipherSuite for DefaultCipherSuite { 15 | type Group = curve25519_dalek::ristretto::RistrettoPoint; 16 | type KeyFormat = opaque_ke::keypair::X25519KeyPair; 17 | type KeyExchange = opaque_ke::key_exchange::tripledh::TripleDH; 18 | type Hash = sha2::Sha256; 19 | type SlowHash = ScryptHash; 20 | } 21 | 22 | pub struct ScryptHash; 23 | 24 | impl SlowHash for ScryptHash { 25 | fn hash( 26 | input: GenericArray::OutputSize>, 27 | ) -> Result, InternalPakeError> { 28 | // 256-bit derived key 29 | let mut derived_key = [0_u8; 32]; 30 | scrypt::scrypt( 31 | input.as_slice(), 32 | b"salt", 33 | // `ScryptParams::recommended` is `15, 8, 1` 34 | &scrypt::ScryptParams::new(5, 8, 1).expect("new Scrypt params"), 35 | &mut derived_key, 36 | ) 37 | .expect("32 bytes always satisfy output length requirements"); 38 | Ok(derived_key.to_vec()) 39 | } 40 | } 41 | 42 | #[must_use] 43 | pub fn encrypt(data: &[u8], key: &[u8]) -> Vec { 44 | let mut seed = [0_u8; 32]; 45 | OsRng.fill_bytes(&mut seed); 46 | 47 | let cocoon = MiniCocoon::from_key(key, &seed); 48 | cocoon.wrap(data).expect("encrypted data") 49 | } 50 | 51 | #[must_use] 52 | pub fn decrypt(data: &[u8], key: &[u8]) -> Vec { 53 | let mut seed = [0_u8; 32]; 54 | OsRng.fill_bytes(&mut seed); 55 | 56 | let cocoon = MiniCocoon::from_key(key, &seed); 57 | cocoon.unwrap(data).expect("decrypted data") 58 | } 59 | -------------------------------------------------------------------------------- /examples/el_key/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "el_key" 3 | version = "0.1.0" 4 | authors = ["Ildar Akhmetgaleev "] 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type = ["cdylib"] 9 | 10 | [dependencies] 11 | seed = { path = "../../" } 12 | rand = { version = "0.8.5", features = ["small_rng"] } 13 | regex = "1.7.1" 14 | scarlet = "1.1.0" 15 | static_assertions = "1.1.0" 16 | gloo-console = "0.2.3" 17 | pulldown-cmark = "0.9.2" 18 | -------------------------------------------------------------------------------- /examples/el_key/Makefile.toml: -------------------------------------------------------------------------------- 1 | extend = "../../Makefile.toml" 2 | 3 | # ---- BUILD ---- 4 | 5 | [tasks.build] 6 | alias = "default_build" 7 | 8 | [tasks.build_release] 9 | alias = "default_build_release" 10 | 11 | # ---- START ---- 12 | 13 | [tasks.start] 14 | alias = "default_start" 15 | 16 | [tasks.start_release] 17 | alias = "default_start_release" 18 | 19 | # ---- TEST ---- 20 | 21 | [tasks.test_firefox] 22 | alias = "default_test_firefox" 23 | 24 | # ---- LINT ---- 25 | 26 | [tasks.clippy] 27 | alias = "default_clippy" 28 | -------------------------------------------------------------------------------- /examples/el_key/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Element key demo 9 | 10 | 11 | 12 |
13 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /examples/el_key/public/card-table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seed-rs/seed/d39e618861f4da46a598b76cd2048d01b511fda2/examples/el_key/public/card-table.png -------------------------------------------------------------------------------- /examples/el_key/public/control-buttons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seed-rs/seed/d39e618861f4da46a598b76cd2048d01b511fda2/examples/el_key/public/control-buttons.png -------------------------------------------------------------------------------- /examples/el_key/public/enable-disable-with-empty.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seed-rs/seed/d39e618861f4da46a598b76cd2048d01b511fda2/examples/el_key/public/enable-disable-with-empty.gif -------------------------------------------------------------------------------- /examples/el_key/public/enable-disable-with-keys.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seed-rs/seed/d39e618861f4da46a598b76cd2048d01b511fda2/examples/el_key/public/enable-disable-with-keys.gif -------------------------------------------------------------------------------- /examples/el_key/public/enable-disable-without-keys-and-empty.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seed-rs/seed/d39e618861f4da46a598b76cd2048d01b511fda2/examples/el_key/public/enable-disable-without-keys-and-empty.gif -------------------------------------------------------------------------------- /examples/el_key/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seed-rs/seed/d39e618861f4da46a598b76cd2048d01b511fda2/examples/el_key/public/favicon.png -------------------------------------------------------------------------------- /examples/el_key/public/options.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seed-rs/seed/d39e618861f4da46a598b76cd2048d01b511fda2/examples/el_key/public/options.png -------------------------------------------------------------------------------- /examples/el_key/public/reordering-colors.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seed-rs/seed/d39e618861f4da46a598b76cd2048d01b511fda2/examples/el_key/public/reordering-colors.gif -------------------------------------------------------------------------------- /examples/el_key/public/reordering-with-keys.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seed-rs/seed/d39e618861f4da46a598b76cd2048d01b511fda2/examples/el_key/public/reordering-with-keys.gif -------------------------------------------------------------------------------- /examples/el_key/public/reordering-without-keys.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seed-rs/seed/d39e618861f4da46a598b76cd2048d01b511fda2/examples/el_key/public/reordering-without-keys.gif -------------------------------------------------------------------------------- /examples/el_key/public/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seed-rs/seed/d39e618861f4da46a598b76cd2048d01b511fda2/examples/el_key/public/screenshot.png -------------------------------------------------------------------------------- /examples/fetch/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fetch" 3 | version = "0.1.0" 4 | authors = ["TatriX "] 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type = ["cdylib"] 9 | 10 | [dependencies] 11 | gloo-net = "0.2.6" 12 | futures = "0.3.26" 13 | seed = { path = "../../" } 14 | serde = { version = "1.0.152", features = ["derive"] } 15 | -------------------------------------------------------------------------------- /examples/fetch/Makefile.toml: -------------------------------------------------------------------------------- 1 | extend = "../../Makefile.toml" 2 | 3 | # ---- BUILD ---- 4 | 5 | [tasks.build] 6 | alias = "default_build" 7 | 8 | [tasks.build_release] 9 | alias = "default_build_release" 10 | 11 | # ---- START ---- 12 | 13 | [tasks.start] 14 | alias = "default_start" 15 | 16 | [tasks.start_release] 17 | alias = "default_start_release" 18 | 19 | # ---- TEST ---- 20 | 21 | [tasks.test_firefox] 22 | alias = "default_test_firefox" 23 | 24 | # ---- LINT ---- 25 | 26 | [tasks.clippy] 27 | alias = "default_clippy" 28 | -------------------------------------------------------------------------------- /examples/fetch/README.md: -------------------------------------------------------------------------------- 1 | ## Fetch example 2 | 3 | Demo of how one can fetch resources. 4 | 5 | --- 6 | 7 | ```bash 8 | cargo make start 9 | ``` 10 | 11 | Open [127.0.0.1:8000](http://127.0.0.1:8000) in your browser. 12 | -------------------------------------------------------------------------------- /examples/fetch/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Fetch example 8 | 9 | 10 | 11 |
12 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /examples/fetch/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Example of the Fetch API. 2 | //! 3 | //! See `simple.rs` for the most basic usage. 4 | 5 | use seed::{prelude::*, *}; 6 | 7 | mod post; 8 | mod simple; 9 | 10 | // ------ ------ 11 | // Init 12 | // ------ ------ 13 | 14 | fn init(_: Url, _: &mut impl Orders) -> Model { 15 | Model::default() 16 | } 17 | 18 | // ------ ------ 19 | // Model 20 | // ------ ------ 21 | 22 | #[derive(Default)] 23 | struct Model { 24 | simple: simple::Model, 25 | post: post::Model, 26 | } 27 | 28 | // ------ ------ 29 | // Update 30 | // ------ ------ 31 | 32 | enum Msg { 33 | Simple(simple::Msg), 34 | Post(post::Msg), 35 | } 36 | 37 | fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders) { 38 | match msg { 39 | Msg::Simple(msg) => simple::update(msg, &mut model.simple, &mut orders.proxy(Msg::Simple)), 40 | Msg::Post(msg) => post::update(msg, &mut model.post, &mut orders.proxy(Msg::Post)), 41 | } 42 | } 43 | 44 | // ------ ------ 45 | // View 46 | // ------ ------ 47 | 48 | fn view(model: &Model) -> Vec> { 49 | nodes![ 50 | div![simple::view(&model.simple).map_msg(Msg::Simple)], 51 | hr![], 52 | div![post::view(&model.post).map_msg(Msg::Post)], 53 | ] 54 | } 55 | 56 | // ------ ------ 57 | // Start 58 | // ------ ------ 59 | 60 | #[wasm_bindgen(start)] 61 | pub fn start() { 62 | App::start("app", init, update, view); 63 | } 64 | -------------------------------------------------------------------------------- /examples/fetch/src/simple.rs: -------------------------------------------------------------------------------- 1 | //! The simplest fetch example. 2 | 3 | use gloo_net::http::Request; 4 | use seed::{prelude::*, *}; 5 | 6 | // ------ ------ 7 | // Model 8 | // ------ ------ 9 | 10 | #[derive(Default)] 11 | pub struct Model { 12 | user: Option, 13 | } 14 | 15 | #[derive(serde::Deserialize)] 16 | pub struct User { 17 | name: String, 18 | } 19 | 20 | // ------ ------ 21 | // Update 22 | // ------ ------ 23 | 24 | pub enum Msg { 25 | Fetch, 26 | Received(User), 27 | } 28 | 29 | pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders) { 30 | match msg { 31 | Msg::Fetch => { 32 | orders.skip(); // No need to rerender 33 | orders.perform_cmd(async { 34 | let response = Request::get("user.json") 35 | .send() 36 | .await 37 | .expect("HTTP request failed"); 38 | 39 | if !response.ok() { 40 | // TODO: handle error 41 | None 42 | } else { 43 | let user = response 44 | .json::() 45 | .await 46 | .expect("deserialization failed"); 47 | Some(Msg::Received(user)) 48 | } 49 | }); 50 | } 51 | Msg::Received(user) => { 52 | model.user = Some(user); 53 | } 54 | } 55 | } 56 | 57 | // ------ ------ 58 | // View 59 | // ------ ------ 60 | 61 | pub fn view(model: &Model) -> Node { 62 | div![ 63 | button![ev(Ev::Click, |_| Msg::Fetch), "Fetch user"], 64 | model 65 | .user 66 | .as_ref() 67 | .map(|user| div![format!("User: {}", user.name)]) 68 | ] 69 | } 70 | -------------------------------------------------------------------------------- /examples/fetch/user.json: -------------------------------------------------------------------------------- 1 | {"name": "Martin"} 2 | -------------------------------------------------------------------------------- /examples/graphql/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "graphql" 3 | version = "0.1.0" 4 | authors = ["Martin Kavík "] 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type = ["cdylib"] 9 | 10 | [dependencies] 11 | seed = {path = "../../"} 12 | graphql_client = "0.10.0" 13 | serde = "1.0.152" 14 | itertools = "0.10.5" 15 | gloo-net = "0.2.6" 16 | gloo-console = "0.2.3" 17 | -------------------------------------------------------------------------------- /examples/graphql/Makefile.toml: -------------------------------------------------------------------------------- 1 | extend = "../../Makefile.toml" 2 | 3 | # ---- BUILD ---- 4 | 5 | [tasks.build] 6 | alias = "default_build" 7 | 8 | [tasks.build_release] 9 | alias = "default_build_release" 10 | 11 | # ---- START ---- 12 | 13 | [tasks.start] 14 | alias = "default_start" 15 | 16 | [tasks.start_release] 17 | alias = "default_start_release" 18 | 19 | # ---- TEST ---- 20 | 21 | [tasks.test_firefox] 22 | alias = "default_test_firefox" 23 | 24 | # ---- LINT ---- 25 | 26 | [tasks.clippy] 27 | alias = "default_clippy" 28 | -------------------------------------------------------------------------------- /examples/graphql/README.md: -------------------------------------------------------------------------------- 1 | ## GraphQl example 2 | 3 | How to communicate with a GraphQL backend. 4 | - GraphQL queries and schemas are typed and validated. 5 | - CSS framework Bulma is used for UI. 6 | 7 | --- 8 | 9 | ```bash 10 | cargo make start 11 | ``` 12 | 13 | Open [127.0.0.1:8000](http://127.0.0.1:8000) in your browser. 14 | -------------------------------------------------------------------------------- /examples/graphql/graphql/queries.graphql: -------------------------------------------------------------------------------- 1 | query QContinents { 2 | continents { 3 | code 4 | name 5 | } 6 | } 7 | 8 | query QContinent($code: ID!) { 9 | continent(code: $code) { 10 | countries { 11 | code 12 | name 13 | } 14 | } 15 | } 16 | 17 | query QCountry($code: ID!) { 18 | country(code: $code) { 19 | code 20 | name 21 | native 22 | capital 23 | currency 24 | phone 25 | languages { 26 | name 27 | native 28 | rtl 29 | } 30 | states { 31 | name 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /examples/graphql/graphql/schema.graphql: -------------------------------------------------------------------------------- 1 | directive @cacheControl( 2 | maxAge: Int 3 | scope: CacheControlScope 4 | ) on FIELD_DEFINITION | OBJECT | INTERFACE 5 | enum CacheControlScope { 6 | PUBLIC 7 | PRIVATE 8 | } 9 | 10 | type Continent { 11 | code: ID! 12 | name: String! 13 | countries: [Country!]! 14 | } 15 | 16 | type Country { 17 | code: ID! 18 | name: String! 19 | native: String! 20 | phone: String! 21 | continent: Continent! 22 | capital: String 23 | currency: String 24 | languages: [Language!]! 25 | emoji: String! 26 | emojiU: String! 27 | states: [State!]! 28 | } 29 | 30 | type Language { 31 | code: ID! 32 | name: String 33 | native: String 34 | rtl: Boolean! 35 | } 36 | 37 | type Query { 38 | continents: [Continent!]! 39 | continent(code: ID!): Continent 40 | countries: [Country!]! 41 | country(code: ID!): Country 42 | languages: [Language!]! 43 | language(code: ID!): Language 44 | } 45 | 46 | type State { 47 | code: String 48 | name: String! 49 | country: Country! 50 | } 51 | 52 | scalar Upload 53 | 54 | -------------------------------------------------------------------------------- /examples/graphql/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | GraphQL example 9 | 10 | 11 | 12 |
13 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /examples/i18n/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "i18n" 3 | version = "0.1.0" 4 | authors = ["Tobias Mucke "] 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type = ["cdylib"] 9 | 10 | [dependencies] 11 | seed = { path = "../../" } 12 | strum = "0.24.1" 13 | strum_macros = "0.24.3" 14 | fluent = "0.16.0" 15 | unic-langid = "0.9.1" 16 | -------------------------------------------------------------------------------- /examples/i18n/Makefile.toml: -------------------------------------------------------------------------------- 1 | extend = "../../Makefile.toml" 2 | 3 | # ---- BUILD ---- 4 | 5 | [tasks.build] 6 | alias = "default_build" 7 | 8 | [tasks.build_release] 9 | alias = "default_build_release" 10 | 11 | # ---- START ---- 12 | 13 | [tasks.start] 14 | alias = "default_start" 15 | 16 | [tasks.start_release] 17 | alias = "default_start_release" 18 | 19 | # ---- TEST ---- 20 | 21 | [tasks.test_firefox] 22 | alias = "default_test_firefox" 23 | 24 | # ---- LINT ---- 25 | 26 | [tasks.clippy] 27 | alias = "default_clippy" 28 | -------------------------------------------------------------------------------- /examples/i18n/README.md: -------------------------------------------------------------------------------- 1 | ## I18N example 2 | 3 | How to support multiple languages in your web app based on [Fluent][url_project_fluent]. 4 | Includes a language selector, some sample text and [FTL strings][url_ftl_syntax_guide] 5 | demonstrating the simplicity and power of [Seed][url_project_seed] 6 | powered by [Fluent's Rust crate][url_crate_fluent]. 7 | 8 | [url_project_fluent]: https://projectfluent.org/ 9 | [url_crate_fluent]: https://docs.rs/fluent/ 10 | [url_ftl_syntax_guide]: https://projectfluent.org/fluent/guide/ 11 | [url_project_seed]: https://seed-rs.org/ 12 | 13 | --- 14 | 15 | ```bash 16 | cargo make start 17 | ``` 18 | 19 | Open [127.0.0.1:8000](http://127.0.0.1:8000) in your browser. -------------------------------------------------------------------------------- /examples/i18n/ftl_messages/de-DE.ftl: -------------------------------------------------------------------------------- 1 | # Einfache Dinge sind einfach. 2 | hello-world = Hallo Welt. 3 | hello-user = {$formal -> 4 | [true] Guten Tag {$userName}! 5 | *[other] Hallo {$userName}! 6 | } 7 | 8 | # Complex things are possible. 9 | shared-photos = 10 | {$userName} {$photoCount -> 11 | [one] hat ein neues Foto zu {$userGender -> 12 | [male] seinem Stream hinzugefügt 13 | [female] ihrem Stream hinzugefügt 14 | *[other] seinem Stream hinzugefügt 15 | } 16 | *[other] fügte {$photoCount} neue Fotos zu {$userGender -> 17 | [male] seinem Stream hinzu 18 | [female] ihrem Stream hinzu 19 | *[other] seinem Stream hinzu 20 | } 21 | }. 22 | 23 | ## Closing tabs 24 | tabs-close-button = Schließen 25 | tabs-close-tooltip = {$tabCount -> 26 | [one] Schließe {$tabCount} Tab 27 | *[other] Schließe {$tabCount} Tabs 28 | } 29 | tabs-close-warning = {$formal -> 30 | [true] Sie sind dabei {$tabCount} Tabs zu schließen. 31 | Sind Sie sicher, dass Sie das wollen? 32 | *[other] Du bist dabei {$tabCount} Tabs zu schließen. 33 | Bist Du sicher, dass Du das willst? 34 | } 35 | 36 | ## Syncing 37 | -sync-brand-name = Firefox Konto 38 | 39 | sync-dialog-title = {-sync-brand-name} 40 | sync-headline-title = 41 | {-sync-brand-name}: Die beste Art Deine Daten immer 42 | bei Dir zu haben 43 | sync-signedout-title = Verbinde Dich mit Deinem {-sync-brand-name} 44 | -------------------------------------------------------------------------------- /examples/i18n/ftl_messages/en-US.ftl: -------------------------------------------------------------------------------- 1 | # NOTE: There are IDE plugins that support 2 | # syntax highlighting for *.ftl files - e.g. vscode-fluent 3 | 4 | # Simple things are simple. 5 | hello-world = Hello World. 6 | hello-user = Hello, {$userName}! 7 | 8 | # Complex things are possible. 9 | shared-photos = 10 | {$userName} {$photoCount -> 11 | [one] added a new photo 12 | *[other] added {$photoCount} new photos 13 | } to {$userGender -> 14 | [male] his stream 15 | [female] her stream 16 | *[other] their stream 17 | }. 18 | 19 | ## Closing tabs 20 | tabs-close-button = Close 21 | tabs-close-tooltip = {$tabCount -> 22 | [one] Close {$tabCount} tab 23 | *[other] Close {$tabCount} tabs 24 | } 25 | tabs-close-warning = 26 | You are about to close {$tabCount} tabs. 27 | Are you sure you want to continue? 28 | 29 | ## Syncing 30 | -sync-brand-name = Firefox Account 31 | 32 | sync-dialog-title = {-sync-brand-name} 33 | sync-headline-title = 34 | {-sync-brand-name}: The best way to bring 35 | your data always with you 36 | sync-signedout-title = Connect with your {-sync-brand-name} 37 | -------------------------------------------------------------------------------- /examples/i18n/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | I18N example 8 | 9 | 10 | 11 |
12 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /examples/intersection_observer/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "intersection_observer" 3 | version = "0.1.0" 4 | authors = ["allan2"] 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type = ["cdylib"] 9 | 10 | [dev-dependencies] 11 | wasm-bindgen-test = "0.3.34" 12 | 13 | [dependencies] 14 | seed = { path = "../../" } 15 | js-sys = "0.3.61" 16 | 17 | [dependencies.web-sys] 18 | version = "0.3.61" 19 | features = [ 20 | "IntersectionObserver", 21 | "IntersectionObserverInit", 22 | "IntersectionObserverEntry", 23 | ] 24 | -------------------------------------------------------------------------------- /examples/intersection_observer/Makefile.toml: -------------------------------------------------------------------------------- 1 | extend = "../../Makefile.toml" 2 | 3 | # ---- BUILD ---- 4 | 5 | [tasks.build] 6 | alias = "default_build" 7 | 8 | [tasks.build_release] 9 | alias = "default_build_release" 10 | 11 | # ---- START ---- 12 | 13 | [tasks.start] 14 | alias = "default_start" 15 | 16 | [tasks.start_release] 17 | alias = "default_start_release" 18 | 19 | # ---- TEST ---- 20 | 21 | [tasks.test_firefox] 22 | alias = "default_test_firefox" 23 | 24 | # ---- LINT ---- 25 | 26 | [tasks.clippy] 27 | alias = "default_clippy" 28 | -------------------------------------------------------------------------------- /examples/intersection_observer/README.md: -------------------------------------------------------------------------------- 1 | # Intersection Observer Example 2 | 3 | This example uses the [Intersection Observer API](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API) through web-sys. 4 | 5 | The intersection observer is created. We tell it to watch the red box by giving the element reference to the observer. 6 | When the red box is entirely in view, the label "Is completely visible" will display a value of `true`. 7 | 8 | To run, use the following commands from the `intersection_observer` directory. 9 | 10 | Using `trunk`: 11 | ``` 12 | trunk serve 13 | ``` 14 | 15 | Using `cargo-make`: 16 | ``` 17 | cargo make start 18 | ``` -------------------------------------------------------------------------------- /examples/intersection_observer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Test 10 | 11 | 12 | 13 | 14 |
15 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /examples/intersection_observer/public/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | } 4 | -------------------------------------------------------------------------------- /examples/markdown/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "markdown" 3 | version = "0.1.0" 4 | authors = ["Florian Warzecha "] 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type = ["cdylib"] 9 | 10 | [build-dependencies] 11 | pulldown-cmark = "0.9.2" 12 | 13 | [dependencies] 14 | pulldown-cmark = "0.9.2" 15 | seed = { path = "../../" } 16 | -------------------------------------------------------------------------------- /examples/markdown/Makefile.toml: -------------------------------------------------------------------------------- 1 | extend = "../../Makefile.toml" 2 | 3 | # ---- BUILD ---- 4 | 5 | [tasks.build] 6 | alias = "default_build" 7 | 8 | [tasks.build_release] 9 | alias = "default_build_release" 10 | 11 | # ---- START ---- 12 | 13 | [tasks.start] 14 | alias = "default_start" 15 | 16 | [tasks.start_release] 17 | alias = "default_start_release" 18 | 19 | # ---- TEST ---- 20 | 21 | [tasks.test_firefox] 22 | alias = "default_test_firefox" 23 | 24 | # ---- LINT ---- 25 | 26 | [tasks.clippy] 27 | alias = "default_clippy" 28 | -------------------------------------------------------------------------------- /examples/markdown/README.md: -------------------------------------------------------------------------------- 1 | ## Markdown example 2 | 3 | Intended as a demo of using `md!` for markdown conversion. 4 | 5 | And how to convert MD to HTML during compilation and include the result in the app code. 6 | 7 | --- 8 | 9 | ```bash 10 | cargo make start 11 | ``` 12 | 13 | Open [127.0.0.1:8000](http://127.0.0.1:8000) in your browser. 14 | -------------------------------------------------------------------------------- /examples/markdown/build.rs: -------------------------------------------------------------------------------- 1 | use pulldown_cmark::{html::push_html, Options, Parser}; 2 | use std::fs; 3 | 4 | fn main() { 5 | let md_footer = fs::read_to_string("md/footer.md").expect("read footer.md"); 6 | let parser = Parser::new_ext(&md_footer, Options::all()); 7 | let mut html = String::new(); 8 | push_html(&mut html, parser); 9 | fs::write("md/generated_html/footer.html", html).expect("write footer.html"); 10 | } 11 | -------------------------------------------------------------------------------- /examples/markdown/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 10 | Markdown example 11 | 12 | 13 | 14 |
15 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /examples/markdown/md/examples.md: -------------------------------------------------------------------------------- 1 | # Additional MD Extensions 2 | 3 | ## Tables 4 | 5 | | Tables | Are | Cool | 6 | | ------------- |:-------------:| -----:| 7 | | col 3 is | right-aligned | $1600 | 8 | | col 2 is | centered | $12 | 9 | | zebra stripes | are neat | $1 | 10 | 11 | Markdown | Less | Pretty 12 | --- | --- | --- 13 | *Still* | `renders` | **nicely** 14 | 1 | 2 | 3 15 | 16 | ## Footnotes 17 | 18 | Here is a footnote reference,[^1] and another.[^longnote] 19 | 20 | [^1]: Here is the footnote. 21 | 22 | [^longnote]: Here's one with multiple blocks. 23 | 24 | ## Strikethrough 25 | 26 | ~~Strikethrough~~ 27 | 28 | ## Tasklists 29 | 30 | - [ ] Task 1 31 | - [x] Task 2 32 | 33 | 34 | -------------------------------------------------------------------------------- /examples/markdown/md/footer.md: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | I'm a compile-time generated footer. 4 | -------------------------------------------------------------------------------- /examples/markdown/md/generated_html/footer.html: -------------------------------------------------------------------------------- 1 |
2 |

I’m a compile-time generated footer.

3 | -------------------------------------------------------------------------------- /examples/markdown/src/lib.rs: -------------------------------------------------------------------------------- 1 | use seed::{prelude::*, *}; 2 | 3 | // ----- ------ 4 | // Init 5 | // ----- ----- 6 | 7 | fn init(_: Url, _: &mut impl Orders) -> Model { 8 | Model 9 | } 10 | 11 | // ----- ------ 12 | // Model 13 | // ----- ----- 14 | 15 | struct Model; 16 | 17 | // ------ ------ 18 | // Update 19 | // ----- ------ 20 | 21 | #[allow(clippy::empty_enum)] 22 | enum Msg {} 23 | 24 | fn update(_: Msg, _: &mut Model, _: &mut impl Orders) {} 25 | 26 | // ------ ------ 27 | // View 28 | // ------ ------ 29 | 30 | fn view(_model: &Model) -> Node { 31 | div![ 32 | // The class required by GitHub styles. See `index.html`. 33 | C!["markdown-body"], 34 | from_md(md_header()), 35 | from_md(include_str!("../md/examples.md")), 36 | // `footer.html` is generated by `build.rs` during compilation. 37 | raw!(include_str!("../md/generated_html/footer.html")), 38 | ] 39 | } 40 | 41 | const fn md_header() -> &'static str { 42 | "# Markdown Example 43 | 44 | Intended as a demo of using `md!` for markdown conversion. 45 | 46 | And how to convert MD to HTML during compilation and include the result in the app code. 47 | 48 | ```bash 49 | cargo make start 50 | ``` 51 | 52 | --- 53 | 54 | Open [127.0.0.1](//127.0.0.1:8000) in your browser." 55 | } 56 | 57 | fn from_md(md: &str) -> Vec> { 58 | let options = pulldown_cmark::Options::all(); 59 | let parser = pulldown_cmark::Parser::new_ext(md, options); 60 | let mut html_text = String::new(); 61 | pulldown_cmark::html::push_html(&mut html_text, parser); 62 | El::from_html(None, &html_text) 63 | } 64 | 65 | // ------ ------ 66 | // Start 67 | // ------ ------ 68 | 69 | #[wasm_bindgen(start)] 70 | pub fn start() { 71 | App::start("app", init, update, view); 72 | } 73 | -------------------------------------------------------------------------------- /examples/no_change/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "no_change" 3 | version = "0.1.0" 4 | authors = ["Martin Kavík "] 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type = ["cdylib"] 9 | 10 | [dependencies] 11 | seed = {path = "../../"} 12 | -------------------------------------------------------------------------------- /examples/no_change/Makefile.toml: -------------------------------------------------------------------------------- 1 | extend = "../../Makefile.toml" 2 | 3 | # ---- BUILD ---- 4 | 5 | [tasks.build] 6 | alias = "default_build" 7 | 8 | [tasks.build_release] 9 | alias = "default_build_release" 10 | 11 | # ---- START ---- 12 | 13 | [tasks.start] 14 | alias = "default_start" 15 | 16 | [tasks.start_release] 17 | alias = "default_start_release" 18 | 19 | # ---- TEST ---- 20 | 21 | [tasks.test_firefox] 22 | alias = "default_test_firefox" 23 | 24 | # ---- LINT ---- 25 | 26 | [tasks.clippy] 27 | alias = "default_clippy" 28 | -------------------------------------------------------------------------------- /examples/no_change/README.md: -------------------------------------------------------------------------------- 1 | ## NoChange example 2 | 3 | How to increase render speed by `Node::NoChange`. 4 | 5 | --- 6 | 7 | ```bash 8 | cargo make start 9 | ``` 10 | 11 | Open [127.0.0.1:8000](http://127.0.0.1:8000) in your browser. 12 | -------------------------------------------------------------------------------- /examples/no_change/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | NoChange example 8 | 9 | 10 | 11 |
12 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /examples/no_change/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::needless_pass_by_value)] 2 | 3 | use seed::{prelude::*, *}; 4 | 5 | // ------ ------ 6 | // Init 7 | // ------ ------ 8 | 9 | fn init(_: Url, _: &mut impl Orders) -> Model { 10 | Model { 11 | counter: 0, 12 | redraw_text_field: true, 13 | } 14 | } 15 | 16 | // ------ ------ 17 | // Model 18 | // ------ ------ 19 | 20 | struct Model { 21 | counter: i32, 22 | redraw_text_field: bool, 23 | } 24 | 25 | // ------ ------ 26 | // Update 27 | // ------ ------ 28 | 29 | enum Msg { 30 | Increment, 31 | Decrement, 32 | ToggleRedrawTextField, 33 | } 34 | 35 | fn update(msg: Msg, model: &mut Model, _: &mut impl Orders) { 36 | match msg { 37 | Msg::Increment => model.counter += 1, 38 | Msg::Decrement => model.counter -= 1, 39 | Msg::ToggleRedrawTextField => model.redraw_text_field = not(model.redraw_text_field), 40 | } 41 | } 42 | 43 | // ------ ------ 44 | // View 45 | // ------ ------ 46 | 47 | fn view(model: &Model) -> Node { 48 | div![ 49 | button![ev(Ev::Click, |_| Msg::Decrement), "-"], 50 | div![model.counter], 51 | button![ev(Ev::Click, |_| Msg::Increment), "+"], 52 | fieldset![ 53 | style! {St::MarginTop => px(10), St::Width => rem(20)}, 54 | if model.redraw_text_field { 55 | input![attrs! { 56 | At::Value => model.counter, 57 | At::Disabled => true.as_at_value(), 58 | }] 59 | } else { 60 | Node::NoChange 61 | }, 62 | div![ 63 | ev(Ev::Click, |_| Msg::ToggleRedrawTextField), 64 | input![attrs! { 65 | At::Type => "checkbox" 66 | At::Checked => model.redraw_text_field.as_at_value(), 67 | },], 68 | label!["Redraw the text field on each render"], 69 | ] 70 | ] 71 | ] 72 | } 73 | 74 | // ------ ------ 75 | // Start 76 | // ------ ------ 77 | 78 | #[wasm_bindgen(start)] 79 | pub fn start() { 80 | App::start("app", init, update, view); 81 | } 82 | -------------------------------------------------------------------------------- /examples/on_insert/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "on_insert" 3 | version = "0.1.0" 4 | authors = ["Glenn Slotte "] 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type = ["cdylib"] 9 | 10 | [dependencies] 11 | seed = {path = "../../"} 12 | -------------------------------------------------------------------------------- /examples/on_insert/Makefile.toml: -------------------------------------------------------------------------------- 1 | extend = "../../Makefile.toml" 2 | 3 | # ---- BUILD ---- 4 | 5 | [tasks.build] 6 | alias = "default_build" 7 | 8 | [tasks.build_release] 9 | alias = "default_build_release" 10 | 11 | # ---- START ---- 12 | 13 | [tasks.start] 14 | alias = "default_start" 15 | 16 | [tasks.start_release] 17 | alias = "default_start_release" 18 | 19 | # ---- TEST ---- 20 | 21 | [tasks.test_firefox] 22 | alias = "default_test_firefox" 23 | 24 | # ---- LINT ---- 25 | 26 | [tasks.clippy] 27 | alias = "default_clippy" 28 | -------------------------------------------------------------------------------- /examples/on_insert/README.md: -------------------------------------------------------------------------------- 1 | ## on_insert example 2 | 3 | Demonstrates the use of `on_insert` to autofocus input elements on DOM insertion. 4 | 5 | ```bash 6 | cargo make start 7 | ``` 8 | 9 | Open [127.0.0.1:8000](http://127.0.0.1:8000) in your browser. 10 | -------------------------------------------------------------------------------- /examples/on_insert/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | on_insert example 8 | 9 | 10 | 11 |
12 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /examples/on_insert/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::needless_pass_by_value)] 2 | 3 | use seed::{prelude::*, *}; 4 | 5 | // ------ ------ 6 | // Init 7 | // ------ ------ 8 | 9 | fn init(_: Url, _orders: &mut impl Orders) -> Model { 10 | Model { inputs: vec![1] } 11 | } 12 | 13 | // ------ ------ 14 | // Model 15 | // ------ ------ 16 | 17 | struct Model { 18 | inputs: Vec, 19 | } 20 | 21 | // ------ ------ 22 | // Update 23 | // ------ ------ 24 | 25 | enum Msg { 26 | AddBox, 27 | RemoveBox(u32), 28 | } 29 | 30 | fn update(msg: Msg, model: &mut Model, _orders: &mut impl Orders) { 31 | match msg { 32 | Msg::AddBox => model 33 | .inputs 34 | .push(model.inputs.iter().max().unwrap_or(&0) + 1), 35 | Msg::RemoveBox(id_to_remove) => model.inputs.retain(|id| id != &id_to_remove), 36 | } 37 | } 38 | 39 | // ------ ------ 40 | // View 41 | // ------ ------ 42 | 43 | fn view(model: &Model) -> Node { 44 | div![ 45 | style! { 46 | St::Width => vw(100), 47 | St::Height => vh(100), 48 | St::Display => "flex", 49 | St::FlexDirection => "column", 50 | St::JustifyContent => "center", 51 | St::AlignItems => "center", 52 | }, 53 | button![ev(Ev::Click, |_| Msg::AddBox), "Add Input"], 54 | model.inputs.iter().map(|id| { 55 | let id = *id; 56 | div![ 57 | input![on_insert(|el| el 58 | .dyn_into::() 59 | .unwrap() 60 | .focus() 61 | .unwrap())], 62 | button![ev(Ev::Click, move |_| Msg::RemoveBox(id)), "Remove"] 63 | ] 64 | }) 65 | ] 66 | } 67 | 68 | // ------ ------ 69 | // Start 70 | // ------ ------ 71 | 72 | #[wasm_bindgen(start)] 73 | pub fn start() { 74 | App::start("app", init, update, view); 75 | } 76 | -------------------------------------------------------------------------------- /examples/page_trait/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "page_trait" 3 | version = "0.1.0" 4 | authors = ["Martin Kavík"] 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type = ["cdylib"] 9 | 10 | [dependencies] 11 | gloo-console = "0.2.3" 12 | seed = {path = "../../"} 13 | -------------------------------------------------------------------------------- /examples/page_trait/Makefile.toml: -------------------------------------------------------------------------------- 1 | extend = "../../Makefile.toml" 2 | 3 | # ---- BUILD ---- 4 | 5 | [tasks.build] 6 | alias = "default_build" 7 | 8 | [tasks.build_release] 9 | alias = "default_build_release" 10 | 11 | # ---- START ---- 12 | 13 | [tasks.start] 14 | alias = "default_start" 15 | 16 | [tasks.start_release] 17 | alias = "default_start_release" 18 | 19 | # ---- TEST ---- 20 | 21 | [tasks.test_firefox] 22 | alias = "default_test_firefox" 23 | 24 | # ---- LINT ---- 25 | 26 | [tasks.clippy] 27 | alias = "default_clippy" 28 | -------------------------------------------------------------------------------- /examples/page_trait/README.md: -------------------------------------------------------------------------------- 1 | ## Page trait example 2 | 3 | How to reduce boilerplate when you have MANY similar pages. 4 | The demo is experimental and intended for experienced Rust and Seed developers. 5 | 6 | --- 7 | 8 | ```bash 9 | cargo make start 10 | ``` 11 | 12 | Open [127.0.0.1:8000](http://127.0.0.1:8000) in your browser. 13 | -------------------------------------------------------------------------------- /examples/page_trait/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Page trait example 8 | 9 | 10 | 11 |
12 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /examples/page_trait/src/lib.rs: -------------------------------------------------------------------------------- 1 | use seed::{prelude::*, *}; 2 | use std::any::Any; 3 | 4 | mod page; 5 | use page::*; 6 | 7 | // ------ ------ 8 | // Init 9 | // ------ ------ 10 | 11 | fn init(_: Url, orders: &mut impl Orders) -> Model { 12 | Model { 13 | page: Page::from(MyPage::new(&mut orders.proxy(Msg::Page))), 14 | } 15 | } 16 | 17 | // ------ ------ 18 | // Model 19 | // ------ ------ 20 | 21 | struct Model { 22 | page: Page, 23 | } 24 | 25 | // ------ ------ 26 | // Update 27 | // ------ ------ 28 | 29 | enum Msg { 30 | Page(Box), 31 | SendText, 32 | ShowMyPage, 33 | ShowMyPage2, 34 | } 35 | 36 | fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders) { 37 | match msg { 38 | Msg::Page(msg) => model.page.update(msg, &mut orders.proxy(Msg::Page)), 39 | Msg::SendText => { 40 | orders.notify("I'm coming from the root."); 41 | } 42 | Msg::ShowMyPage => model.page = Page::from(MyPage::new(&mut orders.proxy(Msg::Page))), 43 | Msg::ShowMyPage2 => model.page = Page::from(MyPage2::new(&mut orders.proxy(Msg::Page))), 44 | } 45 | } 46 | 47 | // ------ ------ 48 | // View 49 | // ------ ------ 50 | 51 | fn view(model: &Model) -> Vec> { 52 | nodes![ 53 | div![ 54 | button!["Show MyPage", ev(Ev::Click, |_| Msg::ShowMyPage)], 55 | button!["Show MyPage2", ev(Ev::Click, |_| Msg::ShowMyPage2)], 56 | " ", 57 | button!["Send text", ev(Ev::Click, |_| Msg::SendText)], 58 | ], 59 | hr![], 60 | model.page.view().map_msg(Msg::Page) 61 | ] 62 | } 63 | 64 | // ------ ------ 65 | // Start 66 | // ------ ------ 67 | 68 | #[wasm_bindgen(start)] 69 | pub fn start() { 70 | App::start("app", init, update, view); 71 | } 72 | -------------------------------------------------------------------------------- /examples/page_trait/src/page.rs: -------------------------------------------------------------------------------- 1 | mod create_page; 2 | 3 | crate::create_page![my_page::MyPage, my_page_2::MyPage2]; 4 | -------------------------------------------------------------------------------- /examples/page_trait/src/page/create_page.rs: -------------------------------------------------------------------------------- 1 | #[macro_export] 2 | macro_rules! create_page { 3 | ( $($module:ident :: $page:ident $(,)?)* ) => { 4 | 5 | use seed::prelude::*; 6 | use std::any::Any; 7 | 8 | mod page_trait; 9 | pub use page_trait::PageTrait; 10 | 11 | $( 12 | mod $module; 13 | pub use $module::$page; 14 | )* 15 | 16 | pub enum Page { 17 | $( 18 | $page($page), 19 | )* 20 | } 21 | 22 | type Message = Box; 23 | 24 | impl Page { 25 | 26 | pub fn update(&mut self, msg: Message, orders: &mut impl Orders) { 27 | match self { 28 | $( 29 | Self::$page(page) => page.invoke_update(msg, orders), 30 | )* 31 | } 32 | } 33 | 34 | pub fn view(&self) -> Vec> { 35 | match self { 36 | $( 37 | Self::$page(page) => page.invoke_view(), 38 | )* 39 | } 40 | } 41 | } 42 | 43 | $( 44 | impl From<$page> for Page { 45 | fn from(page: $page) -> Self { 46 | Self::$page(page) 47 | } 48 | } 49 | )* 50 | }; 51 | } 52 | -------------------------------------------------------------------------------- /examples/page_trait/src/page/my_page.rs: -------------------------------------------------------------------------------- 1 | use seed::{prelude::*, *}; 2 | use super::PageTrait; 3 | 4 | #[allow(dead_code)] 5 | pub struct MyPage { 6 | text: &'static str, 7 | received_text: Option<&'static str>, 8 | receiver_handle: SubHandle, 9 | } 10 | 11 | pub enum Msg { 12 | Clicked, 13 | TextReceived(&'static str), 14 | } 15 | 16 | impl PageTrait for MyPage { 17 | type Message = Msg; 18 | 19 | fn init(orders: &mut impl Orders) -> Self { 20 | Self { 21 | text: "", 22 | received_text: None, 23 | receiver_handle: orders.subscribe_with_handle(Msg::TextReceived), 24 | } 25 | } 26 | 27 | fn update(&mut self, msg: Self::Message, _orders: &mut impl Orders) { 28 | match msg { 29 | Msg::Clicked => self.text = "MyPage button clicked!", 30 | Msg::TextReceived(text) => self.received_text = Some(text), 31 | } 32 | } 33 | 34 | fn view(&self) -> Vec> { 35 | vec![ 36 | button![ 37 | "MyPage button", 38 | ev(Ev::Click, |_| Msg::Clicked) 39 | ], 40 | plain![self.text], 41 | div![ 42 | &self.received_text, 43 | ] 44 | ] 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /examples/page_trait/src/page/my_page_2.rs: -------------------------------------------------------------------------------- 1 | use seed::{prelude::*, *}; 2 | use super::PageTrait; 3 | 4 | #[allow(dead_code)] 5 | pub struct MyPage2 { 6 | text: &'static str, 7 | received_text: Option<&'static str>, 8 | receiver_handle: SubHandle, 9 | } 10 | 11 | pub enum Msg { 12 | Clicked, 13 | TextReceived(&'static str), 14 | } 15 | 16 | impl PageTrait for MyPage2 { 17 | type Message = Msg; 18 | 19 | fn init(orders: &mut impl Orders) -> Self { 20 | Self { 21 | text: "", 22 | received_text: None, 23 | receiver_handle: orders.subscribe_with_handle(Msg::TextReceived), 24 | } 25 | } 26 | 27 | fn update(&mut self, msg: Self::Message, _orders: &mut impl Orders) { 28 | match msg { 29 | Msg::Clicked => self.text = "MyPage2 button clicked!", 30 | Msg::TextReceived(text) => self.received_text = Some(text), 31 | } 32 | } 33 | 34 | fn view(&self) -> Vec> { 35 | vec![ 36 | button![ 37 | "MyPage2 button", 38 | ev(Ev::Click, |_| Msg::Clicked) 39 | ], 40 | plain![self.text], 41 | div![ 42 | &self.received_text, 43 | ] 44 | ] 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /examples/page_trait/src/page/page_trait.rs: -------------------------------------------------------------------------------- 1 | use seed::prelude::*; 2 | use std::any::Any; 3 | 4 | pub trait PageTrait: Sized { 5 | type Message: 'static; 6 | 7 | fn init(orders: &mut impl Orders) -> Self; 8 | 9 | fn new(orders: &mut impl Orders>) -> Self { 10 | Self::init(&mut orders.proxy(|msg| Box::new(Some(msg)) as Box)) 11 | } 12 | 13 | fn update(&mut self, msg: Self::Message, orders: &mut impl Orders); 14 | 15 | fn invoke_update(&mut self, mut msg: Box, orders: &mut impl Orders>) { 16 | 17 | msg.downcast_mut::>().and_then(Option::take) 18 | .map_or_else(||{ 19 | gloo_console::error!("Msg not handled!"); 20 | }, |msg| 21 | self.update(msg, &mut orders.proxy(|msg| Box::new(Some(msg)) as Box))); 22 | } 23 | 24 | fn view(&self) -> Vec>; 25 | 26 | fn invoke_view(&self) -> Vec>> { 27 | self.view() 28 | .map_msg(|msg| Box::new(Some(msg)) as Box) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /examples/pages/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pages" 3 | version = "0.1.0" 4 | authors = ["Martin Kavík "] 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type = ["cdylib"] 9 | 10 | [dependencies] 11 | seed = { path = "../../", features = ["routing"] } 12 | -------------------------------------------------------------------------------- /examples/pages/Makefile.toml: -------------------------------------------------------------------------------- 1 | extend = "../../Makefile.toml" 2 | 3 | # ---- BUILD ---- 4 | 5 | [tasks.build] 6 | alias = "default_build" 7 | 8 | [tasks.build_release] 9 | alias = "default_build_release" 10 | 11 | # ---- START ---- 12 | 13 | [tasks.start] 14 | alias = "default_start" 15 | 16 | [tasks.start_release] 17 | alias = "default_start_release" 18 | 19 | # ---- TEST ---- 20 | 21 | [tasks.test_firefox] 22 | alias = "default_test_firefox" 23 | 24 | # ---- LINT ---- 25 | 26 | [tasks.clippy] 27 | alias = "default_clippy" 28 | -------------------------------------------------------------------------------- /examples/pages/README.md: -------------------------------------------------------------------------------- 1 | ## Pages example 2 | 3 | How to create and browse multiple pages in your app. 4 | 5 | A simple and predictable page navigation example pattern. 6 | 7 | In this example the current page is a value from the `Page` enum and is stored in [Model.page](https://github.com/seed-rs/seed/blob/master/examples/pages/src/lib.rs#L31) 8 | 9 | The `pages_keep_state` example is probably better suited to more complex websites or apps where you want to keep Model state across page loads. 10 | 11 | --- 12 | 13 | ```bash 14 | cargo make start 15 | ``` 16 | 17 | Open [127.0.0.1:8000](http://127.0.0.1:8000) in your browser. 18 | -------------------------------------------------------------------------------- /examples/pages/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Pages example 9 | 10 | 11 | 12 |
13 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /examples/pages/src/page.rs: -------------------------------------------------------------------------------- 1 | pub mod admin; 2 | -------------------------------------------------------------------------------- /examples/pages/src/page/admin.rs: -------------------------------------------------------------------------------- 1 | use crate::Context; 2 | use seed::{prelude::*, *}; 3 | 4 | const REPORT: &str = "report"; 5 | 6 | mod page; 7 | 8 | // ------ ------ 9 | // Init 10 | // ------ ------ 11 | 12 | pub fn init(mut url: Url) -> Option { 13 | Some(Model { 14 | report_page: match url.next_path_part() { 15 | Some(REPORT) => page::report::init(url)?, 16 | _ => None?, 17 | }, 18 | }) 19 | } 20 | 21 | // ------ ------ 22 | // Model 23 | // ------ ------ 24 | 25 | pub struct Model { 26 | report_page: page::report::Model, 27 | } 28 | 29 | // ------ ------ 30 | // Urls 31 | // ------ ------ 32 | 33 | struct_urls!(); 34 | impl<'a> Urls<'a> { 35 | pub fn report_urls(self) -> page::report::Urls<'a> { 36 | page::report::Urls::new(self.base_url().add_path_part(REPORT)) 37 | } 38 | } 39 | 40 | // ------ ------ 41 | // View 42 | // ------ ------ 43 | 44 | pub fn view(model: &Model, ctx: &Context) -> Node { 45 | page::report::view(&model.report_page, ctx) 46 | } 47 | -------------------------------------------------------------------------------- /examples/pages/src/page/admin/page.rs: -------------------------------------------------------------------------------- 1 | pub mod report; 2 | -------------------------------------------------------------------------------- /examples/pages/src/page/admin/page/report.rs: -------------------------------------------------------------------------------- 1 | use crate::Context; 2 | use seed::{prelude::*, *}; 3 | 4 | const DAILY: &str = "daily"; 5 | const WEEKLY: &str = "weekly"; 6 | 7 | // ------ ------ 8 | // Init 9 | // ------ ------ 10 | 11 | pub fn init(mut url: Url) -> Option { 12 | let base_url = url.to_base_url(); 13 | 14 | let frequency = match url.remaining_path_parts().as_slice() { 15 | [] => { 16 | Urls::new(&base_url).default().go_and_replace(); 17 | Frequency::default() 18 | } 19 | [DAILY] => Frequency::Daily, 20 | [WEEKLY] => Frequency::Weekly, 21 | _ => None?, 22 | }; 23 | 24 | Some(Model { 25 | base_url, 26 | frequency, 27 | }) 28 | } 29 | 30 | // ------ ------ 31 | // Model 32 | // ------ ------ 33 | 34 | pub struct Model { 35 | base_url: Url, 36 | frequency: Frequency, 37 | } 38 | 39 | // ------ Frequency ------ 40 | 41 | enum Frequency { 42 | Daily, 43 | Weekly, 44 | } 45 | 46 | impl Default for Frequency { 47 | fn default() -> Self { 48 | Self::Daily 49 | } 50 | } 51 | 52 | // ------ ------ 53 | // Urls 54 | // ------ ------ 55 | 56 | struct_urls!(); 57 | impl<'a> Urls<'a> { 58 | pub fn default(self) -> Url { 59 | self.daily() 60 | } 61 | pub fn daily(self) -> Url { 62 | self.base_url().add_path_part(DAILY) 63 | } 64 | pub fn weekly(self) -> Url { 65 | self.base_url().add_path_part(WEEKLY) 66 | } 67 | } 68 | 69 | // ------ ------ 70 | // View 71 | // ------ ------ 72 | 73 | pub fn view(model: &Model, ctx: &Context) -> Node { 74 | let (frequency, link) = match &model.frequency { 75 | Frequency::Daily => ( 76 | "daily", 77 | a![ 78 | "Switch to weekly", 79 | attrs! { 80 | At::Href => Urls::new(&model.base_url).weekly() 81 | } 82 | ], 83 | ), 84 | Frequency::Weekly => ( 85 | "weekly", 86 | a![ 87 | "Switch to daily", 88 | attrs! { 89 | At::Href => Urls::new(&model.base_url).daily() 90 | } 91 | ], 92 | ), 93 | }; 94 | div![ 95 | format!( 96 | "Hello {}! This is your {} report.", 97 | ctx.logged_user, frequency 98 | ), 99 | link, 100 | ] 101 | } 102 | -------------------------------------------------------------------------------- /examples/pages_hash_routing/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pages_hash_routing" 3 | version = "0.1.0" 4 | authors = ["Martin Kavík "] 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type = ["cdylib"] 9 | 10 | [dependencies] 11 | seed = { path = "../../", features = ["routing"] } 12 | -------------------------------------------------------------------------------- /examples/pages_hash_routing/Makefile.toml: -------------------------------------------------------------------------------- 1 | extend = "../../Makefile.toml" 2 | 3 | # ---- BUILD ---- 4 | 5 | [tasks.build] 6 | alias = "default_build" 7 | 8 | [tasks.build_release] 9 | alias = "default_build_release" 10 | 11 | # ---- START ---- 12 | 13 | [tasks.start] 14 | alias = "default_start" 15 | 16 | [tasks.start_release] 17 | alias = "default_start_release" 18 | 19 | # ---- TEST ---- 20 | 21 | [tasks.test_firefox] 22 | alias = "default_test_firefox" 23 | 24 | # ---- LINT ---- 25 | 26 | [tasks.clippy] 27 | alias = "default_clippy" 28 | -------------------------------------------------------------------------------- /examples/pages_hash_routing/README.md: -------------------------------------------------------------------------------- 1 | ## Pages with hash routing example 2 | 3 | How to create and browse multiple pages in your app. 4 | This example uses hash routing. 5 | 6 | --- 7 | 8 | ```bash 9 | cargo make start 10 | ``` 11 | 12 | Open [127.0.0.1:8000](http://127.0.0.1:8000) in your browser. 13 | -------------------------------------------------------------------------------- /examples/pages_hash_routing/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Pages with hash routing example 8 | 9 | 10 | 11 |
12 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /examples/pages_hash_routing/public/text-polyfill.min.js: -------------------------------------------------------------------------------- 1 | (function(l){function m(b){b=void 0===b?"utf-8":b;if("utf-8"!==b)throw new RangeError("Failed to construct 'TextEncoder': The encoding label provided ('"+b+"') is invalid.");}function k(b,a){b=void 0===b?"utf-8":b;a=void 0===a?{fatal:!1}:a;if("utf-8"!==b)throw new RangeError("Failed to construct 'TextDecoder': The encoding label provided ('"+b+"') is invalid.");if(a.fatal)throw Error("Failed to construct 'TextDecoder': the 'fatal' option is unsupported.");}if(l.TextEncoder&&l.TextDecoder)return!1; 2 | Object.defineProperty(m.prototype,"encoding",{value:"utf-8"});m.prototype.encode=function(b,a){a=void 0===a?{stream:!1}:a;if(a.stream)throw Error("Failed to encode: the 'stream' option is unsupported.");a=0;for(var h=b.length,f=0,c=Math.max(32,h+(h>>1)+7),e=new Uint8Array(c>>3<<3);a=d){if(a=d)continue}f+4>e.length&&(c+=8,c*=1+a/b.length*2,c=c>>3<<3, 3 | g=new Uint8Array(c),g.set(e),e=g);if(0===(d&4294967168))e[f++]=d;else{if(0===(d&4294965248))e[f++]=d>>6&31|192;else if(0===(d&4294901760))e[f++]=d>>12&15|224,e[f++]=d>>6&63|128;else if(0===(d&4292870144))e[f++]=d>>18&7|240,e[f++]=d>>12&63|128,e[f++]=d>>6&63|128;else continue;e[f++]=d&63|128}}return e.slice(0,f)};Object.defineProperty(k.prototype,"encoding",{value:"utf-8"});Object.defineProperty(k.prototype,"fatal",{value:!1});Object.defineProperty(k.prototype,"ignoreBOM",{value:!1});k.prototype.decode= 4 | function(b,a){a=void 0===a?{stream:!1}:a;if(a.stream)throw Error("Failed to decode: the 'stream' option is unsupported.");b=new Uint8Array(b);a=0;for(var h=b.length,f=[];a>>10&1023|55296),c=56320| 5 | c&1023);f.push(c)}}return String.fromCharCode.apply(null,f)};l.TextEncoder=m;l.TextDecoder=k})("undefined"!==typeof window?window:"undefined"!==typeof global?global:this); 6 | -------------------------------------------------------------------------------- /examples/pages_hash_routing/src/page.rs: -------------------------------------------------------------------------------- 1 | pub mod admin; 2 | -------------------------------------------------------------------------------- /examples/pages_hash_routing/src/page/admin.rs: -------------------------------------------------------------------------------- 1 | use crate::Context; 2 | use seed::{prelude::*, *}; 3 | 4 | const REPORT: &str = "report"; 5 | 6 | mod page; 7 | 8 | // ------ ------ 9 | // Init 10 | // ------ ------ 11 | 12 | pub fn init(mut url: Url) -> Option { 13 | Some(Model { 14 | report_page: match url.next_hash_path_part() { 15 | Some(REPORT) => page::report::init(url)?, 16 | _ => None?, 17 | }, 18 | }) 19 | } 20 | 21 | // ------ ------ 22 | // Model 23 | // ------ ------ 24 | 25 | pub struct Model { 26 | report_page: page::report::Model, 27 | } 28 | 29 | // ------ ------ 30 | // Urls 31 | // ------ ------ 32 | 33 | struct_urls!(); 34 | impl<'a> Urls<'a> { 35 | pub fn report_urls(self) -> page::report::Urls<'a> { 36 | page::report::Urls::new(self.base_url().add_hash_path_part(REPORT)) 37 | } 38 | } 39 | 40 | // ------ ------ 41 | // View 42 | // ------ ------ 43 | 44 | pub fn view(model: &Model, ctx: &Context) -> Node { 45 | page::report::view(&model.report_page, ctx) 46 | } 47 | -------------------------------------------------------------------------------- /examples/pages_hash_routing/src/page/admin/page.rs: -------------------------------------------------------------------------------- 1 | pub mod report; 2 | -------------------------------------------------------------------------------- /examples/pages_keep_state/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pages_keep_state" 3 | version = "0.1.0" 4 | authors = ["Martin Kavík "] 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type = ["cdylib"] 9 | 10 | [dependencies] 11 | seed = { path = "../../", features = ["routing"] } 12 | -------------------------------------------------------------------------------- /examples/pages_keep_state/Makefile.toml: -------------------------------------------------------------------------------- 1 | extend = "../../Makefile.toml" 2 | 3 | # ---- BUILD ---- 4 | 5 | [tasks.build] 6 | alias = "default_build" 7 | 8 | [tasks.build_release] 9 | alias = "default_build_release" 10 | 11 | # ---- START ---- 12 | 13 | [tasks.start] 14 | alias = "default_start" 15 | 16 | [tasks.start_release] 17 | alias = "default_start_release" 18 | 19 | # ---- TEST ---- 20 | 21 | [tasks.test_firefox] 22 | alias = "default_test_firefox" 23 | 24 | # ---- LINT ---- 25 | 26 | [tasks.clippy] 27 | alias = "default_clippy" 28 | -------------------------------------------------------------------------------- /examples/pages_keep_state/README.md: -------------------------------------------------------------------------------- 1 | ## Pages-keep-state example 2 | 3 | How to create and browse multiple pages in your app, demonstrating lazy loading model values and persisting selected page state across page loads. 4 | 5 | The pattern used in this example is probably better suited to more complex websites or apps where you want to keep model state across page loads, e.g. where there are many filters configurable by the user or there are time-consuming init operations such as multiple fetches. 6 | 7 | In this example the current page is stored in [Model.page_id](https://github.com/seed-rs/seed/blob/master/examples/pages_keep_state/src/lib.rs#L36-L37) and `Model.admin_model` represents the admin report subpage selected by the user. 8 | 9 | Lazy model initialization is used and the report subpage selection persists until changed by the user. 10 | 11 | To see this in action, follow these steps: 12 | 13 | 1. Open the example and select `report`. Notice the page text "This is your _daily_ report". 14 | 2. Select `Switch to weekly`. Notice the page text "This is your _weekly_ report". 15 | 3. Select `Home` then `Report`. The report page persists at "This is your _weekly_ report". 16 | 17 | The `pages` example pattern is simpler and maybe more predictable, without lazy model loading or page state persistence. 18 | 19 | --- 20 | 21 | ```bash 22 | cargo make start 23 | ``` 24 | 25 | Open [127.0.0.1:8000](http://127.0.0.1:8000) in your browser. 26 | -------------------------------------------------------------------------------- /examples/pages_keep_state/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Pages-keep-state example 8 | 9 | 10 | 11 |
12 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /examples/pages_keep_state/public/text-polyfill.min.js: -------------------------------------------------------------------------------- 1 | (function(l){function m(b){b=void 0===b?"utf-8":b;if("utf-8"!==b)throw new RangeError("Failed to construct 'TextEncoder': The encoding label provided ('"+b+"') is invalid.");}function k(b,a){b=void 0===b?"utf-8":b;a=void 0===a?{fatal:!1}:a;if("utf-8"!==b)throw new RangeError("Failed to construct 'TextDecoder': The encoding label provided ('"+b+"') is invalid.");if(a.fatal)throw Error("Failed to construct 'TextDecoder': the 'fatal' option is unsupported.");}if(l.TextEncoder&&l.TextDecoder)return!1; 2 | Object.defineProperty(m.prototype,"encoding",{value:"utf-8"});m.prototype.encode=function(b,a){a=void 0===a?{stream:!1}:a;if(a.stream)throw Error("Failed to encode: the 'stream' option is unsupported.");a=0;for(var h=b.length,f=0,c=Math.max(32,h+(h>>1)+7),e=new Uint8Array(c>>3<<3);a=d){if(a=d)continue}f+4>e.length&&(c+=8,c*=1+a/b.length*2,c=c>>3<<3, 3 | g=new Uint8Array(c),g.set(e),e=g);if(0===(d&4294967168))e[f++]=d;else{if(0===(d&4294965248))e[f++]=d>>6&31|192;else if(0===(d&4294901760))e[f++]=d>>12&15|224,e[f++]=d>>6&63|128;else if(0===(d&4292870144))e[f++]=d>>18&7|240,e[f++]=d>>12&63|128,e[f++]=d>>6&63|128;else continue;e[f++]=d&63|128}}return e.slice(0,f)};Object.defineProperty(k.prototype,"encoding",{value:"utf-8"});Object.defineProperty(k.prototype,"fatal",{value:!1});Object.defineProperty(k.prototype,"ignoreBOM",{value:!1});k.prototype.decode= 4 | function(b,a){a=void 0===a?{stream:!1}:a;if(a.stream)throw Error("Failed to decode: the 'stream' option is unsupported.");b=new Uint8Array(b);a=0;for(var h=b.length,f=[];a>>10&1023|55296),c=56320| 5 | c&1023);f.push(c)}}return String.fromCharCode.apply(null,f)};l.TextEncoder=m;l.TextDecoder=k})("undefined"!==typeof window?window:"undefined"!==typeof global?global:this); 6 | -------------------------------------------------------------------------------- /examples/pages_keep_state/src/page.rs: -------------------------------------------------------------------------------- 1 | pub mod admin; 2 | -------------------------------------------------------------------------------- /examples/pages_keep_state/src/page/admin.rs: -------------------------------------------------------------------------------- 1 | use crate::Context; 2 | use seed::{prelude::*, *}; 3 | 4 | const REPORT: &str = "report"; 5 | 6 | mod page; 7 | 8 | // ------ ------ 9 | // Init 10 | // ------ ------ 11 | 12 | pub fn init(mut url: Url, model: &mut Option) -> Option<()> { 13 | let model = model.get_or_insert_with(Model::default); 14 | model.page_id.replace(match url.next_path_part() { 15 | Some(REPORT) => page::report::init(url, &mut model.report_model).map(|_| PageId::Report)?, 16 | _ => None?, 17 | }); 18 | Some(()) 19 | } 20 | 21 | // ------ ------ 22 | // Model 23 | // ------ ------ 24 | 25 | #[derive(Default)] 26 | pub struct Model { 27 | page_id: Option, 28 | report_model: Option, 29 | } 30 | 31 | // ------ PageId ------ 32 | 33 | #[derive(Copy, Clone, Eq, PartialEq)] 34 | enum PageId { 35 | Report, 36 | } 37 | 38 | // ------ ------ 39 | // Urls 40 | // ------ ------ 41 | 42 | struct_urls!(); 43 | impl<'a> Urls<'a> { 44 | pub fn report_urls(self) -> page::report::Urls<'a> { 45 | page::report::Urls::new(self.base_url().add_path_part(REPORT)) 46 | } 47 | } 48 | 49 | // ------ ------ 50 | // View 51 | // ------ ------ 52 | 53 | #[allow(clippy::single_match_else)] 54 | pub fn view(model: &Model, ctx: &Context) -> Node { 55 | match model.page_id { 56 | Some(PageId::Report) => { 57 | page::report::view(model.report_model.as_ref().expect("report model"), ctx) 58 | } 59 | None => div!["404"], 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /examples/pages_keep_state/src/page/admin/page.rs: -------------------------------------------------------------------------------- 1 | pub mod report; 2 | -------------------------------------------------------------------------------- /examples/record_screen/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "record_screen" 3 | version = "0.1.0" 4 | authors = ["Martin Kavík "] 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type = ["cdylib"] 9 | 10 | [dependencies] 11 | gloo-console = "0.2.3" 12 | seed = {path = "../../"} 13 | 14 | [dependencies.web-sys] 15 | version = "0.3.61" 16 | features = [ 17 | "DisplayMediaStreamConstraints", 18 | "MediaDevices", 19 | "MediaStream", 20 | "HtmlMediaElement", 21 | ] 22 | -------------------------------------------------------------------------------- /examples/record_screen/Makefile.toml: -------------------------------------------------------------------------------- 1 | extend = "../../Makefile.toml" 2 | 3 | # ---- BUILD ---- 4 | 5 | [tasks.build] 6 | alias = "default_build" 7 | 8 | [tasks.build_release] 9 | alias = "default_build_release" 10 | 11 | # ---- START ---- 12 | 13 | [tasks.start] 14 | alias = "default_start" 15 | 16 | [tasks.start_release] 17 | alias = "default_start_release" 18 | 19 | # ---- TEST ---- 20 | 21 | [tasks.test_firefox] 22 | alias = "default_test_firefox" 23 | 24 | # ---- LINT ---- 25 | 26 | [tasks.clippy] 27 | alias = "default_clippy" 28 | -------------------------------------------------------------------------------- /examples/record_screen/README.md: -------------------------------------------------------------------------------- 1 | ## Record Screen example 2 | 3 | How to record the screen using the [Screen Capture API](https://developer.mozilla.org/en-US/docs/Web/API/Screen_Capture_API/Using_Screen_Capture). 4 | 5 | Related article: [How To Take Screenshots In The Browser Using JavaScript](https://hackernoon.com/how-to-take-screenshots-in-the-browser-using-javascript-l92k3xq7) 6 | 7 | --- 8 | 9 | ```bash 10 | cargo make start 11 | ``` 12 | 13 | Open [127.0.0.1:8000](http://127.0.0.1:8000) in your browser. 14 | -------------------------------------------------------------------------------- /examples/record_screen/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Record Screen example 8 | 9 | 10 | 11 |
12 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /examples/record_screen/src/lib.rs: -------------------------------------------------------------------------------- 1 | use gloo_console::log; 2 | use seed::{prelude::*, *}; 3 | use wasm_bindgen_futures::JsFuture; 4 | use web_sys::{DisplayMediaStreamConstraints, HtmlMediaElement, MediaStream}; 5 | 6 | // ------ ------ 7 | // Init 8 | // ------ ------ 9 | 10 | fn init(_: Url, _: &mut impl Orders) -> Model { 11 | Model::default() 12 | } 13 | 14 | // ------ ------ 15 | // Model 16 | // ------ ------ 17 | 18 | #[derive(Default)] 19 | struct Model { 20 | video: ElRef, 21 | } 22 | 23 | // ------ ------ 24 | // Update 25 | // ------ ------ 26 | 27 | enum Msg { 28 | RecordScreen, 29 | DisplayMedia(Result), 30 | } 31 | 32 | fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders) { 33 | match msg { 34 | Msg::RecordScreen => { 35 | orders.perform_cmd(display_media()); 36 | } 37 | Msg::DisplayMedia(Ok(media_stream)) => { 38 | model 39 | .video 40 | .get() 41 | .expect("get video element") 42 | .set_src_object(Some(&media_stream)); 43 | } 44 | Msg::DisplayMedia(Err(error)) => { 45 | log!(format!("{error:?}")); 46 | } 47 | } 48 | } 49 | 50 | async fn display_media() -> Msg { 51 | let mut constraints = DisplayMediaStreamConstraints::new(); 52 | constraints.video(&JsValue::from(true)); 53 | 54 | let media_stream_promise = window() 55 | .navigator() 56 | .media_devices() 57 | .unwrap() 58 | .get_display_media_with_constraints(&constraints) 59 | .unwrap(); 60 | 61 | Msg::DisplayMedia( 62 | JsFuture::from(media_stream_promise) 63 | .await 64 | .map(MediaStream::from), 65 | ) 66 | } 67 | 68 | // ------ ------ 69 | // View 70 | // ------ ------ 71 | 72 | fn view(model: &Model) -> Node { 73 | div![ 74 | button![ 75 | style! { 76 | St::Display => "block", 77 | }, 78 | "Record Screen", 79 | ev(Ev::Click, |_| Msg::RecordScreen) 80 | ], 81 | video![ 82 | el_ref(&model.video), 83 | attrs! { 84 | At::Width => 1024, 85 | At::AutoPlay => AtValue::None, 86 | } 87 | ] 88 | ] 89 | } 90 | 91 | // ------ ------ 92 | // Start 93 | // ------ ------ 94 | 95 | #[wasm_bindgen(start)] 96 | pub fn start() { 97 | App::start("app", init, update, view); 98 | } 99 | -------------------------------------------------------------------------------- /examples/resize_observer/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "resize_observer" 3 | version = "0.1.0" 4 | authors = ["Martin Kavík "] 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type = ["cdylib"] 9 | 10 | [dependencies] 11 | gloo-console = "0.2.3" 12 | seed = {path = "../../"} 13 | -------------------------------------------------------------------------------- /examples/resize_observer/Makefile.toml: -------------------------------------------------------------------------------- 1 | extend = "../../Makefile.toml" 2 | 3 | # ---- BUILD ---- 4 | 5 | [tasks.build] 6 | alias = "default_build" 7 | 8 | [tasks.build_release] 9 | alias = "default_build_release" 10 | 11 | # ---- START ---- 12 | 13 | [tasks.start] 14 | alias = "default_start" 15 | 16 | [tasks.start_release] 17 | alias = "default_start_release" 18 | 19 | # ---- TEST ---- 20 | 21 | [tasks.test_firefox] 22 | alias = "default_test_firefox" 23 | 24 | # ---- LINT ---- 25 | 26 | [tasks.clippy] 27 | alias = "default_clippy" 28 | -------------------------------------------------------------------------------- /examples/resize_observer/README.md: -------------------------------------------------------------------------------- 1 | ## ResizeObserver example 2 | 3 | How to use [ResizeObserver](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver). 4 | _Note:_ It uses Javascript, because `ResizeObserver` isn't integrated to [web-sys](https://rustwasm.github.io/wasm-bindgen/api/web_sys/?search=ResizeObserver) yet. 5 | 6 | --- 7 | 8 | ```bash 9 | cargo make start 10 | ``` 11 | 12 | Open [127.0.0.1:8000](http://127.0.0.1:8000) in your browser. 13 | -------------------------------------------------------------------------------- /examples/resize_observer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | ResizeObserver example 8 | 9 | 10 | 11 |
12 | 13 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /examples/resize_observer/public/observe_element_size.js: -------------------------------------------------------------------------------- 1 | function observeElementSize(element, send_msg_resized) { 2 | 3 | const resizeObserver = new ResizeObserver(entries => { 4 | const entry = entries[0]; 5 | 6 | let size = 0; 7 | // Browsers use different structures to store the size. Don't ask me why.. 8 | if (entry.borderBoxSize instanceof ResizeObserverSize) { 9 | size = entry.borderBoxSize; 10 | } else if (entry.borderBoxSize[0] instanceof ResizeObserverSize) { 11 | size = entry.borderBoxSize[0]; 12 | } else { 13 | console.error("Cannot get borderBoxSize from ResizeObserver entry!"); 14 | } 15 | 16 | const height = size.blockSize; 17 | const width = size.inlineSize; 18 | send_msg_resized(width, height); 19 | }); 20 | 21 | resizeObserver.observe(element); 22 | } 23 | -------------------------------------------------------------------------------- /examples/rust_from_js/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rust_from_js" 3 | version = "0.1.0" 4 | authors = ["Martin Kavík "] 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type = ["cdylib"] 9 | 10 | [dependencies] 11 | gloo-console = "0.2.3" 12 | seed = {path = "../../"} 13 | -------------------------------------------------------------------------------- /examples/rust_from_js/Makefile.toml: -------------------------------------------------------------------------------- 1 | extend = "../../Makefile.toml" 2 | 3 | # ---- BUILD ---- 4 | 5 | [tasks.build] 6 | alias = "default_build" 7 | 8 | [tasks.build_release] 9 | alias = "default_build_release" 10 | 11 | # ---- START ---- 12 | 13 | [tasks.start] 14 | alias = "default_start" 15 | 16 | [tasks.start_release] 17 | alias = "default_start_release" 18 | 19 | # ---- TEST ---- 20 | 21 | [tasks.test_firefox] 22 | alias = "default_test_firefox" 23 | 24 | # ---- LINT ---- 25 | 26 | [tasks.clippy] 27 | alias = "default_clippy" 28 | -------------------------------------------------------------------------------- /examples/rust_from_js/README.md: -------------------------------------------------------------------------------- 1 | ## Rust from JS example 2 | 3 | How to call Rust functions from Javascript. 4 | 5 | _Note:_ See example `update_from_js` for more advanced example without the mutable global variable as a state. 6 | 7 | --- 8 | 9 | ```bash 10 | cargo make start 11 | ``` 12 | 13 | Open [127.0.0.1:8000](http://127.0.0.1:8000) in your browser. 14 | 15 | See the code in `index.html`. 16 | 17 | Run in the browser dev console `rust.set_title("New title")` and click the `Rerender` button. 18 | -------------------------------------------------------------------------------- /examples/rust_from_js/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Rust from JS example 8 | 9 | 10 | 11 |
12 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /examples/rust_from_js/src/lib.rs: -------------------------------------------------------------------------------- 1 | use gloo_console::log; 2 | use seed::{prelude::*, *}; 3 | use std::cell::RefCell; 4 | 5 | thread_local!(static TITLE: RefCell = RefCell::new("I'm TITLE!".to_owned())); 6 | 7 | #[wasm_bindgen] 8 | pub fn set_title(title: String) { 9 | TITLE.with(|title_cell| title_cell.replace(title)); 10 | } 11 | 12 | fn title() -> String { 13 | TITLE.with(|title_cell| title_cell.borrow().clone()) 14 | } 15 | 16 | // ------ ------ 17 | // Init 18 | // ------ ------ 19 | 20 | fn init(_: Url, _: &mut impl Orders) -> Model { 21 | Model 22 | } 23 | 24 | // ------ ------ 25 | // Model 26 | // ------ ------ 27 | 28 | struct Model; 29 | 30 | // ------ ------ 31 | // Update 32 | // ------ ------ 33 | 34 | enum Msg { 35 | Rerender, 36 | } 37 | 38 | #[allow(clippy::needless_pass_by_value)] 39 | fn update(msg: Msg, _: &mut Model, _: &mut impl Orders) { 40 | match msg { 41 | Msg::Rerender => log!("Rerendered"), 42 | } 43 | } 44 | 45 | // ------ ------ 46 | // View 47 | // ------ ------ 48 | 49 | fn view(_: &Model) -> Vec> { 50 | vec![ 51 | h1![title()], 52 | button!["Rerender", ev(Ev::Click, |_| Msg::Rerender)], 53 | ] 54 | } 55 | 56 | // ------ ------ 57 | // Start 58 | // ------ ------ 59 | 60 | #[wasm_bindgen(start)] 61 | pub fn start() { 62 | App::start("app", init, update, view); 63 | } 64 | -------------------------------------------------------------------------------- /examples/server_integration/Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | 3 | members = [ 4 | "client", 5 | "server", 6 | ] 7 | -------------------------------------------------------------------------------- /examples/server_integration/README.md: -------------------------------------------------------------------------------- 1 | ## Server integration example 2 | 3 | Example of a workspace with [Actix](https://actix.rs/) server. 4 | 5 | Client: 6 | - Is served by Actix. 7 | - Consists of Fetch API examples. Examples are submodules. 8 | 9 | --- 10 | 11 | ```bash 12 | cargo make start 13 | ``` 14 | 15 | Open [127.0.0.1:8000](http://127.0.0.1:8000) in your browser. -------------------------------------------------------------------------------- /examples/server_integration/client/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "client" 3 | version = "0.1.0" 4 | authors = ["Your Name "] 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type = ["cdylib"] 9 | path = "src/lib.rs" 10 | 11 | [dev-dependencies] 12 | wasm-bindgen-test = "0.3.20" 13 | 14 | [dependencies] 15 | gloo-console = "0.2.3" 16 | gloo-net = "0.2.6" 17 | gloo-timers = "0.2.6" 18 | seed = { path = "../../../" } 19 | serde = "1.0.117" 20 | 21 | shared = { path = "../shared"} 22 | -------------------------------------------------------------------------------- /examples/server_integration/client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Server integration example 8 | 9 | 10 | 11 |
12 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /examples/server_integration/client/src/example_b.rs: -------------------------------------------------------------------------------- 1 | use gloo_net::http::Request; 2 | use seed::{prelude::*, *}; 3 | use serde::Deserialize; 4 | 5 | pub const TITLE: &str = "Example B"; 6 | pub const DESCRIPTION: &str = 7 | "Click button 'Try to Fetch JSON' to send request to non-existent endpoint. 8 | Server will return status 404 with empty body. `Response::check_status` then return error."; 9 | 10 | type FetchResult = Result; 11 | 12 | const fn get_request_url() -> &'static str { 13 | "/api/non-existent-endpoint" 14 | } 15 | 16 | // ------ ------ 17 | // Model 18 | // ------ ------ 19 | 20 | #[derive(Default)] 21 | pub struct Model { 22 | pub fetch_result: Option>, 23 | } 24 | 25 | #[derive(Debug, Deserialize)] 26 | pub struct ExpectedResponseData { 27 | #[allow(dead_code)] 28 | something: String, 29 | } 30 | 31 | // ------ ------ 32 | // Update 33 | // ------ ------ 34 | 35 | pub enum Msg { 36 | SendRequest, 37 | Fetched(FetchResult), 38 | } 39 | 40 | pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders) { 41 | match msg { 42 | Msg::SendRequest => { 43 | orders.skip().perform_cmd(async { 44 | Msg::Fetched( 45 | async { Request::get(get_request_url()).send().await?.json().await }.await, 46 | ) 47 | }); 48 | } 49 | 50 | Msg::Fetched(fetch_result) => { 51 | model.fetch_result = Some(fetch_result); 52 | } 53 | } 54 | } 55 | 56 | // ------ ------ 57 | // View 58 | // ------ ------ 59 | 60 | pub fn view(model: &Model, intro: impl FnOnce(&str, &str) -> Vec>) -> Vec> { 61 | nodes![ 62 | intro(TITLE, DESCRIPTION), 63 | model 64 | .fetch_result 65 | .as_ref() 66 | .map(|result| div![format!("{result:#?}")]), 67 | button![ev(Ev::Click, |_| Msg::SendRequest), "Try to Fetch JSON"], 68 | ] 69 | } 70 | -------------------------------------------------------------------------------- /examples/server_integration/server/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "server" 3 | version = "0.1.0" 4 | authors = ["Your Name "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | actix = "0.10.0" 9 | actix-web = "3.1.0" 10 | actix-files = "0.4.0" 11 | actix-multipart = "0.3.0" 12 | actix-rt = "1.1.1" 13 | futures-timer = "3.0.2" 14 | futures = "0.3.6" 15 | 16 | shared = { path = "../shared" } 17 | -------------------------------------------------------------------------------- /examples/server_integration/server/src/count_actor.rs: -------------------------------------------------------------------------------- 1 | use actix::prelude::*; 2 | 3 | // ---- Actor ---- 4 | 5 | pub struct CountActor(pub u32); 6 | 7 | impl Actor for CountActor { 8 | type Context = Context; 9 | } 10 | 11 | // ---- Messages ---- 12 | 13 | pub struct MsgIncrement; 14 | 15 | impl Message for MsgIncrement { 16 | type Result = u32; 17 | } 18 | 19 | // ---- Handlers ---- 20 | 21 | impl Handler for CountActor { 22 | type Result = u32; 23 | 24 | fn handle(&mut self, _: MsgIncrement, _: &mut Context) -> Self::Result { 25 | self.0 += 1; 26 | self.0 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /examples/server_integration/shared/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "shared" 3 | version = "0.1.0" 4 | authors = ["Your Name "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | serde = "1.0.117" 9 | -------------------------------------------------------------------------------- /examples/server_integration/shared/src/lib.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Debug, Clone, Default, Serialize, Deserialize)] 4 | pub struct SendMessageRequestBody { 5 | pub text: String, 6 | } 7 | 8 | #[derive(Debug, Clone, Default, Serialize, Deserialize)] 9 | pub struct SendMessageResponseBody { 10 | pub ordinal_number: u32, 11 | pub text: String, 12 | } 13 | -------------------------------------------------------------------------------- /examples/service_worker/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "service_worker" 3 | description = "Seed service worker example" 4 | repository = "https://github.com/seed-rs/seed" 5 | version = "0.1.0" 6 | authors = ["Will Johnson "] 7 | edition = "2018" 8 | license = "MIT" 9 | 10 | [lib] 11 | crate-type = ["cdylib"] 12 | path = "src/lib.rs" 13 | 14 | [[bin]] 15 | name = "server" 16 | path = "src/main.rs" 17 | 18 | [dependencies] 19 | #common 20 | serde = "1.0.152" 21 | 22 | # server 23 | anyhow = { version = "1.0.69", optional = true } 24 | serde_json = { version = "1.0.94", optional = true } 25 | tokio = { version = "1.26.0", features = ["macros", "sync", "fs", "rt-multi-thread"], optional = true } 26 | tracing = { version = "0.1.37", optional = true } 27 | tracing-subscriber = { version = "0.3.16", optional = true, features = ["env-filter"] } 28 | warp = { version = "0.3.3", optional = true } 29 | web-push = { version = "0.9.3", optional = true } 30 | 31 | # client 32 | apply = { version = "0.3.0", optional = true } 33 | gloo-net = { version = "0.2.6", optional = true } 34 | gloo-console = { version = "0.2.3", optional = true } 35 | seed = { path = "../../", optional = true } 36 | serde-wasm-bindgen = { version = "0.5.0", optional = true } 37 | 38 | [dependencies.web-sys] 39 | version = "0.3.61" 40 | features = [ 41 | "CacheStorage", 42 | "Notification", 43 | "NotificationPermission", 44 | "ServiceWorker", 45 | "ServiceWorkerContainer", 46 | "ServiceWorkerRegistration", 47 | "ServiceWorkerState", 48 | "PushManager", 49 | "PushSubscription", 50 | "PushSubscriptionJson", 51 | "PushSubscriptionKeys", 52 | "PushSubscriptionOptionsInit", 53 | ] 54 | optional = true 55 | 56 | [features] 57 | default = [] 58 | client = ["dep:apply", "dep:seed", "dep:web-sys", "dep:serde_json", "dep:serde-wasm-bindgen", "dep:gloo-console", "dep:gloo-net"] 59 | server = ["anyhow", "tokio", "warp", "tracing", "tracing-subscriber", "web-push"] 60 | -------------------------------------------------------------------------------- /examples/service_worker/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Service Worker Example 8 | 9 | 10 | 11 |
12 | 13 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /examples/service_worker/private_key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN EC PARAMETERS----- 2 | BggqhkjOPQMBBw== 3 | -----END EC PARAMETERS----- 4 | -----BEGIN EC PRIVATE KEY----- 5 | MHcCAQEEIMij1KbDMDP+OZR8oT8zhJSqEGeRDsL5emKZQIugvflPoAoGCCqGSM49 6 | AwEHoUQDQgAEpunruwa2ksfEo8Gsxa6v9i8bbaOCH/tp2lBLahsJwn+cxhFYeeXo 7 | O6eVaqdV2MTNwjam0shaTIgoD18UVVmzrQ== 8 | -----END EC PRIVATE KEY----- 9 | -------------------------------------------------------------------------------- /examples/service_worker/public/images/important-notes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seed-rs/seed/d39e618861f4da46a598b76cd2048d01b511fda2/examples/service_worker/public/images/important-notes.png -------------------------------------------------------------------------------- /examples/service_worker/public/subscribe.js: -------------------------------------------------------------------------------- 1 | // ------ ------ 2 | // Subscribe 3 | // @manager: web_sys::PushManager - The PushManager interface of the Push API provides a way to 4 | // receive notifications from third-party servers as well as request URLs for push notifications. 5 | // - https://developer.mozilla.org/en-US/docs/Web/API/PushManager 6 | // - https://rustwasm.github.io/wasm-bindgen/api/web_sys/struct.PushManager.html 7 | // @api_key: Uint8Array - A Base64-encoded DOMString or ArrayBuffer containing an ECDSA P-256 public key that 8 | // the push server will use to authenticate your application server. 9 | // - https://developer.mozilla.org/en-US/docs/Web/API/PushManager/subscribe 10 | // @returns Promise 11 | // Until https://github.com/rustwasm/wasm-bindgen/pull/2288 is included in the new release of wasm-bindgen, 12 | // this function should be called from the seed app to subscribe to the PushManager. The applicationServerKey 13 | // is the vapid key that is used for identification on the back-end server. The `userVisibleOnly` property 14 | // indicates that the returned push subscription will only be used for messages whose effect is made visible 15 | // to the user. It must be set to `true` or the browser will reject the subscription request. This holds true 16 | // for both chrome and firefox. 17 | // ------ ------ 18 | window.subscribe = async (manager, api_key) => { 19 | let subscription = await manager.subscribe({ 20 | applicationServerKey: api_key, 21 | userVisibleOnly: true 22 | }); 23 | 24 | console.log("JS subscription", subscription); 25 | return subscription; 26 | } 27 | -------------------------------------------------------------------------------- /examples/service_worker/service-worker.js: -------------------------------------------------------------------------------- 1 | importScripts('https://storage.googleapis.com/workbox-cdn/releases/5.1.2/workbox-sw.js'); 2 | 3 | // workbox.core - Provides core workbox functionality. Ths will be used for service worker updating. 4 | const core = workbox.core; 5 | 6 | // workbox.precaching - Helps to simplify the caching process 7 | const precaching = workbox.precaching; 8 | 9 | // We want to publish a new service worker and immediately update and control the page. 10 | // - https://developers.google.com/web/tools/workbox/modules/workbox-core#skip_waiting_and_clients_claim 11 | core.skipWaiting(); 12 | core.clientsClaim(); 13 | 14 | // Cache all of the assets for offline viewing. This can be done manually or by using a tool, such as 15 | // `workbox-cli`- https://developers.google.com/web/tools/workbox/modules/workbox-cli. 16 | // By updating the revision hash after an asset has been updated, the cached resource will be 17 | // updated in the browser's cache. 18 | precaching.precacheAndRoute( 19 | [ 20 | { "revision": "12345", "url": "index.html" }, 21 | { "revision": "12345", "url": "public/subscribe.js" }, 22 | { "revision": "12345", "url": "public/images/important-notes.png" }, 23 | { "revision": "12345", "url": "/" }, 24 | { "revision": "12345", "url": "pkg/package_bg.wasm" }, 25 | { "revision": "12345", "url": "pkg/package.js" }, 26 | ] 27 | ); 28 | 29 | // Listen for and display a push notification if the push event is triggered from the server. 30 | self.addEventListener('push', (event) => { 31 | const title = 'Seed service worker!'; 32 | const options = { 33 | body: event.data.text() 34 | }; 35 | event.waitUntil(self.registration.showNotification(title, options)); 36 | }); 37 | -------------------------------------------------------------------------------- /examples/subscribe/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "subscribe" 3 | version = "0.1.0" 4 | authors = ["Martin Kavík "] 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type = ["cdylib"] 9 | 10 | [dependencies] 11 | seed = { path = "../../", features = ["routing"] } 12 | itertools = "0.10.5" 13 | futures = "0.3.26" 14 | gloo-console = "0.2.3" 15 | -------------------------------------------------------------------------------- /examples/subscribe/Makefile.toml: -------------------------------------------------------------------------------- 1 | extend = "../../Makefile.toml" 2 | 3 | # ---- BUILD ---- 4 | 5 | [tasks.build] 6 | alias = "default_build" 7 | 8 | [tasks.build_release] 9 | alias = "default_build_release" 10 | 11 | # ---- START ---- 12 | 13 | [tasks.start] 14 | alias = "default_start" 15 | 16 | [tasks.start_release] 17 | alias = "default_start_release" 18 | 19 | # ---- TEST ---- 20 | 21 | [tasks.test_firefox] 22 | alias = "default_test_firefox" 23 | 24 | # ---- LINT ---- 25 | 26 | [tasks.clippy] 27 | alias = "default_clippy" 28 | -------------------------------------------------------------------------------- /examples/subscribe/README.md: -------------------------------------------------------------------------------- 1 | ## Subscribe example 2 | 3 | How to create and use subscriptions, streams, notifications and commands. 4 | 5 | --- 6 | 7 | ```bash 8 | cargo make start 9 | ``` 10 | 11 | Open [127.0.0.1:8000](http://127.0.0.1:8000) in your browser. 12 | -------------------------------------------------------------------------------- /examples/subscribe/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Subscribe example 8 | 9 | 10 | 11 |
12 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /examples/subscribe/src/counter.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::needless_pass_by_value, clippy::trivially_copy_pass_by_ref)] 2 | 3 | use seed::{prelude::*, *}; 4 | 5 | // ------ ------ 6 | // Init 7 | // ------ ------ 8 | 9 | #[derive(Copy, Clone)] 10 | pub struct DoReset; 11 | 12 | pub fn init(orders: &mut impl Orders) -> Model { 13 | Model { 14 | value: 0, 15 | _sub_handle: orders.subscribe_with_handle(|_: DoReset| Msg::Reset), 16 | } 17 | } 18 | 19 | // ------ ------ 20 | // Model 21 | // ------ ------ 22 | 23 | pub struct Model { 24 | value: i32, 25 | _sub_handle: SubHandle, 26 | } 27 | 28 | // ------ ------ 29 | // Update 30 | // ------ ------ 31 | 32 | pub enum Msg { 33 | Increment, 34 | Decrement, 35 | Reset, 36 | } 37 | 38 | pub fn update(msg: Msg, model: &mut Model) { 39 | match msg { 40 | Msg::Increment => model.value += 1, 41 | Msg::Decrement => model.value -= 1, 42 | Msg::Reset => model.value = 0, 43 | } 44 | } 45 | 46 | // ------ ------ 47 | // View 48 | // ------ ------ 49 | 50 | pub fn view(model: &Model) -> Node { 51 | div![ 52 | style! {St::TextAlign => "center"}, 53 | button![ev(Ev::Click, |_| Msg::Decrement), "-"], 54 | div![model.value], 55 | button![ev(Ev::Click, |_| Msg::Increment), "+"], 56 | ] 57 | } 58 | -------------------------------------------------------------------------------- /examples/tea_component/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tea_component" 3 | version = "0.1.0" 4 | authors = ["Martin Kavík "] 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type = ["cdylib"] 9 | 10 | [dependencies] 11 | gloo-console = "0.2.3" 12 | seed = {path = "../../"} 13 | -------------------------------------------------------------------------------- /examples/tea_component/Makefile.toml: -------------------------------------------------------------------------------- 1 | extend = "../../Makefile.toml" 2 | 3 | # ---- BUILD ---- 4 | 5 | [tasks.build] 6 | alias = "default_build" 7 | 8 | [tasks.build_release] 9 | alias = "default_build_release" 10 | 11 | # ---- START ---- 12 | 13 | [tasks.start] 14 | alias = "default_start" 15 | 16 | [tasks.start_release] 17 | alias = "default_start_release" 18 | 19 | # ---- TEST ---- 20 | 21 | [tasks.test_firefox] 22 | alias = "default_test_firefox" 23 | 24 | # ---- LINT ---- 25 | 26 | [tasks.clippy] 27 | alias = "default_clippy" 28 | -------------------------------------------------------------------------------- /examples/tea_component/README.md: -------------------------------------------------------------------------------- 1 | ## TEA component example 2 | 3 | How to write a component in The Elm architecture. 4 | You'll also learn how to pass messages to the parent component. 5 | 6 | --- 7 | 8 | ```bash 9 | cargo make start 10 | ``` 11 | 12 | Open [127.0.0.1:8000](http://127.0.0.1:8000) in your browser. 13 | -------------------------------------------------------------------------------- /examples/tea_component/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | TEA Component example 8 | 9 | 10 | 11 |
12 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /examples/tea_component/src/counter.rs: -------------------------------------------------------------------------------- 1 | // NOTE: Don't try to create as many components as possible. 2 | // Try to reuse already existing `Msg` and other entities to prevent unnecessary nesting and complexity. 3 | 4 | use seed::{prelude::*, *}; 5 | 6 | // ------ ------ 7 | // Model 8 | // ------ ------ 9 | 10 | #[derive(Default)] 11 | pub struct Model { 12 | value: i32, 13 | } 14 | 15 | // ------ ------ 16 | // Update 17 | // ------ ------ 18 | 19 | pub enum Msg { 20 | Increment, 21 | Decrement, 22 | } 23 | 24 | #[allow(clippy::needless_pass_by_value)] 25 | pub fn update( 26 | msg: Msg, 27 | model: &mut Model, 28 | on_change: impl FnOnce(i32) -> Ms, 29 | orders: &mut impl Orders, 30 | ) { 31 | match msg { 32 | Msg::Increment => model.value += 1, 33 | Msg::Decrement => model.value -= 1, 34 | } 35 | orders.send_msg(on_change(model.value)); 36 | } 37 | 38 | // ------ ALTERNATIVE update ------ 39 | 40 | // pub enum OutMsg { 41 | // Changed 42 | // } 43 | // 44 | // pub fn update(msg: Msg, model: &mut Model) -> OutMsg { 45 | // match msg { 46 | // Msg::Increment => model.value += 1, 47 | // Msg::Decrement => model.value -= 1, 48 | // } 49 | // OutMsg::Changed 50 | // } 51 | 52 | // ------ ------ 53 | // View 54 | // ------ ------ 55 | 56 | pub fn view( 57 | model: &Model, 58 | on_click: impl FnOnce() -> Ms + Clone + 'static, 59 | to_msg: impl FnOnce(Msg) -> Ms + Clone + 'static, 60 | ) -> Node { 61 | div![ 62 | ev(Ev::Click, |_| on_click()), 63 | button![ 64 | ev(Ev::Click, { 65 | let to_msg = to_msg.clone(); 66 | move |_| to_msg(Msg::Decrement) 67 | }), 68 | "-" 69 | ], 70 | div![model.value.to_string()], 71 | button![ev(Ev::Click, move |_| to_msg(Msg::Increment)), "+"], 72 | ] 73 | } 74 | -------------------------------------------------------------------------------- /examples/tea_component/src/lib.rs: -------------------------------------------------------------------------------- 1 | // NOTE: Don't try to create as many components as possible. 2 | // Try to reuse already existing `Msg` and other entities to prevent unnecessary nesting and complexity. 3 | 4 | use gloo_console::log; 5 | use seed::{prelude::*, *}; 6 | 7 | mod counter; 8 | 9 | // ------ ------ 10 | // Init 11 | // ------ ------ 12 | 13 | fn init(_: Url, _: &mut impl Orders) -> Model { 14 | Model::default() 15 | } 16 | 17 | // ------ ------ 18 | // Model 19 | // ------ ------ 20 | 21 | #[derive(Default)] 22 | struct Model { 23 | counter_a: counter::Model, 24 | counter_b: counter::Model, 25 | } 26 | 27 | // ------ ------ 28 | // Update 29 | // ------ ------ 30 | 31 | #[allow(clippy::enum_variant_names)] 32 | enum Msg { 33 | CounterA(counter::Msg), 34 | CounterB(counter::Msg), 35 | CounterClicked(char), 36 | CounterChanged(i32), 37 | } 38 | 39 | fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders) { 40 | match msg { 41 | Msg::CounterA(msg) => { 42 | counter::update(msg, &mut model.counter_a, Msg::CounterChanged, orders); 43 | } 44 | Msg::CounterB(msg) => { 45 | counter::update(msg, &mut model.counter_b, Msg::CounterChanged, orders); 46 | } 47 | Msg::CounterClicked(counter_id) => log!(format!("CounterClicked: {counter_id}")), 48 | Msg::CounterChanged(value) => log!("CounterChanged", value), 49 | } 50 | } 51 | 52 | // ------ ------ 53 | // View 54 | // ------ ------ 55 | 56 | fn view(model: &Model) -> Node { 57 | div![ 58 | style! { St::Display => "flex"}, 59 | counter::view(&model.counter_a, || Msg::CounterClicked('A'), Msg::CounterA), 60 | counter::view(&model.counter_b, || Msg::CounterClicked('B'), Msg::CounterB), 61 | "See Console log", 62 | ] 63 | } 64 | 65 | // ------ ------ 66 | // Start 67 | // ------ ------ 68 | 69 | #[wasm_bindgen(start)] 70 | pub fn start() { 71 | App::start("app", init, update, view); 72 | } 73 | -------------------------------------------------------------------------------- /examples/tests/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tests" 3 | version = "0.1.0" 4 | authors = ["Martin Kavík "] 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type = ["cdylib"] 9 | 10 | [dev-dependencies] 11 | wasm-bindgen-test = "0.3.34" 12 | regex = "1.7.1" 13 | 14 | [dependencies] 15 | seed = {path = "../../"} 16 | -------------------------------------------------------------------------------- /examples/tests/Makefile.toml: -------------------------------------------------------------------------------- 1 | extend = "../../Makefile.toml" 2 | 3 | # ---- BUILD ---- 4 | 5 | [tasks.build] 6 | alias = "default_build" 7 | 8 | [tasks.build_release] 9 | alias = "default_build_release" 10 | 11 | # ---- START ---- 12 | 13 | [tasks.start] 14 | alias = "default_start" 15 | 16 | [tasks.start_release] 17 | alias = "default_start_release" 18 | 19 | # ---- TEST ---- 20 | 21 | [tasks.test_firefox] 22 | alias = "default_test_firefox" 23 | 24 | # ---- LINT ---- 25 | 26 | [tasks.clippy] 27 | alias = "default_clippy" 28 | -------------------------------------------------------------------------------- /examples/tests/README.md: -------------------------------------------------------------------------------- 1 | ## Tests example 2 | 3 | How to test your app. 4 | 5 | --- 6 | 7 | ```bash 8 | cargo make start 9 | ``` 10 | 11 | Open [127.0.0.1:8000](http://127.0.0.1:8000) in your browser. 12 | -------------------------------------------------------------------------------- /examples/tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Counter example 8 | 9 | 10 | 11 |
12 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /examples/tests/public/leopard.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seed-rs/seed/d39e618861f4da46a598b76cd2048d01b511fda2/examples/tests/public/leopard.jpg -------------------------------------------------------------------------------- /examples/todomvc/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "todomvc" 3 | version = "0.1.0" 4 | authors = ["Your Name "] 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type = ["cdylib"] 9 | 10 | [dependencies] 11 | seed = { path = "../../", features = ["routing"] } 12 | serde = { version = "1.0.152", features = ["derive"] } 13 | uuid = { version = "1.3.0", features = ["serde", "v4"] } 14 | indexmap = { version = "1.9.2", features = ["serde-1"] } 15 | enclose = "1.1.8" 16 | gloo-storage = "0.2.2" 17 | serde_json = "1.0.94" 18 | -------------------------------------------------------------------------------- /examples/todomvc/Makefile.toml: -------------------------------------------------------------------------------- 1 | extend = "../../Makefile.toml" 2 | 3 | # ---- BUILD ---- 4 | 5 | [tasks.build] 6 | alias = "default_build" 7 | 8 | [tasks.build_release] 9 | alias = "default_build_release" 10 | 11 | # ---- START ---- 12 | 13 | [tasks.start] 14 | alias = "default_start" 15 | 16 | [tasks.start_release] 17 | alias = "default_start_release" 18 | 19 | # ---- TEST ---- 20 | 21 | [tasks.test_firefox] 22 | alias = "default_test_firefox" 23 | 24 | # ---- LINT ---- 25 | 26 | [tasks.clippy] 27 | alias = "default_clippy" 28 | -------------------------------------------------------------------------------- /examples/todomvc/README.md: -------------------------------------------------------------------------------- 1 | # Rust & Seed TodoMVC Example 2 | 3 | 4 | > Rust is a systems programming language with a focus on safety, 5 | especially safe concurrency. 6 | 7 | > _[Rust](https://www.rust-lang.org)_ 8 | 9 | > wasm-bindgen, and its web-sys package allow Rust to be used in web browsers via WASM. 10 | 11 | > [Wasm-bindgen](https://rustwasm.github.io/wasm-bindgen/) 12 | 13 | > Seed is a high-level framework for building websites using these tools. 14 | 15 | > _[Seed](https://github.com/seed-rs/seed)_ 16 | 17 | ## Learning Rust 18 | 19 | The [Rust book](https://doc.rust-lang.org/book/index.html) is a great resource for getting started. 20 | 21 | Here are some links you may find helpful: 22 | 23 | * [Code Playground](https://play.rust-lang.org/) 24 | * [Rust Documentation](https://doc.rust-lang.org/) 25 | * [Rust Source Code](https://github.com/rust-lang/rust) 26 | * [wasm-bindgen Source Code](https://github.com/rustwasm/wasm-bindgen) 27 | * [Seed guide](https://github.com/seed-rs/seed) 28 | * [Seed quickstart repo](https://github.com/seed-rs/seed-quickstart) 29 | 30 | Get help from Rust users: 31 | 32 | * [Rust on StackOverflow](http://stackoverflow.com/questions/tagged/rust) 33 | * [Reddit](https://www.reddit.com/r/rust/) 34 | * [Gitter chat](https://gitter.im/rust-lang/rust) 35 | 36 | _If you have other helpful links to share, or find any of the links above no longer work, please [let us know](https://github.com/tastejs/todomvc/issues)._ 37 | 38 | 39 | ## Running 40 | 41 | #### Prerequisites 42 | 43 | - This framework requires you to first install [Rust](https://www.rust-lang.org/tools/install). 44 | - You'll need a recent version of Rust: `rustup update` 45 | - The wasm32 target: `rustup target add wasm32-unknown-unknown` 46 | - And cargo-make: `cargo install --force cargo-make` 47 | 48 | 49 | #### Build & Run 50 | ```bash 51 | cargo make start 52 | ``` 53 | 54 | Open [127.0.0.1:8000](http://127.0.0.1:8000) in your browser. 55 | 56 | --- 57 | 58 | ### [How to make this example standalone] 59 | - **`Makefile.toml`** 60 | - Replace tasks with aliases with their parents. 61 | - Remove root Makefile import. 62 | - **`Cargo.toml`** 63 | - replace Seed path with version number 64 | - This file 65 | - Remove this chapter 66 | -------------------------------------------------------------------------------- /examples/todomvc/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Seed • TodoMVC 7 | 8 | 9 | 10 | 11 |
12 | 13 | 20 | 21 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /examples/unsaved_changes/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "unsaved_changes" 3 | version = "0.1.0" 4 | authors = ["Martin Kavík "] 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type = ["cdylib"] 9 | 10 | [dependencies] 11 | gloo-console = "0.2.3" 12 | gloo-storage = "0.2.2" 13 | seed = { path = "../../", features = ["routing"] } 14 | -------------------------------------------------------------------------------- /examples/unsaved_changes/Makefile.toml: -------------------------------------------------------------------------------- 1 | extend = "../../Makefile.toml" 2 | 3 | # ---- BUILD ---- 4 | 5 | [tasks.build] 6 | alias = "default_build" 7 | 8 | [tasks.build_release] 9 | alias = "default_build_release" 10 | 11 | # ---- START ---- 12 | 13 | [tasks.start] 14 | alias = "default_start" 15 | 16 | [tasks.start_release] 17 | alias = "default_start_release" 18 | 19 | # ---- TEST ---- 20 | 21 | [tasks.test_firefox] 22 | alias = "default_test_firefox" 23 | 24 | # ---- LINT ---- 25 | 26 | [tasks.clippy] 27 | alias = "default_clippy" 28 | -------------------------------------------------------------------------------- /examples/unsaved_changes/README.md: -------------------------------------------------------------------------------- 1 | ## Unsaved changes example 2 | 3 | How to prevent navigating away when there are unsaved changes on the website. 4 | 5 | --- 6 | 7 | ```bash 8 | cargo make start 9 | ``` 10 | 11 | Open [127.0.0.1:8000](http://127.0.0.1:8000) in your browser. 12 | -------------------------------------------------------------------------------- /examples/unsaved_changes/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Unsaved changes example 8 | 9 | 10 | 11 |
12 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /examples/update_from_js/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "trigger_update_from_js" 3 | version = "0.1.0" 4 | authors = ["David O'Connor "] 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type = ["cdylib"] 9 | 10 | [dependencies] 11 | seed = {path = "../../"} 12 | enclose = "1.1.8" 13 | gloo-console = "0.2.3" 14 | -------------------------------------------------------------------------------- /examples/update_from_js/Makefile.toml: -------------------------------------------------------------------------------- 1 | extend = "../../Makefile.toml" 2 | 3 | # ---- BUILD ---- 4 | 5 | [tasks.build] 6 | alias = "default_build" 7 | 8 | [tasks.build_release] 9 | alias = "default_build_release" 10 | 11 | # ---- START ---- 12 | 13 | [tasks.start] 14 | alias = "default_start" 15 | 16 | [tasks.start_release] 17 | alias = "default_start_release" 18 | 19 | # ---- TEST ---- 20 | 21 | [tasks.test_firefox] 22 | alias = "default_test_firefox" 23 | 24 | # ---- LINT ---- 25 | 26 | [tasks.clippy] 27 | alias = "default_clippy" 28 | -------------------------------------------------------------------------------- /examples/update_from_js/README.md: -------------------------------------------------------------------------------- 1 | ## Update from JS example 2 | 3 | How to trigger `update` function from Javascript world. 4 | You'll also see how to call JS functions from Rust. 5 | 6 | --- 7 | 8 | ```bash 9 | cargo make start 10 | ``` 11 | 12 | Open [127.0.0.1:8000](http://127.0.0.1:8000) in your browser. 13 | -------------------------------------------------------------------------------- /examples/update_from_js/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Trigger update from JS example 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /examples/update_from_js/public/index.js: -------------------------------------------------------------------------------- 1 | import init from '/pkg/package.js'; 2 | import { start } from '/pkg/package.js'; 3 | 4 | window.enableClock = () => { 5 | const sendTick = () => { 6 | tick(new Date().toLocaleTimeString()); 7 | }; 8 | sendTick(); 9 | 10 | setInterval(() => { 11 | sendTick(); 12 | }, 1000); 13 | }; 14 | 15 | init('/pkg/package_bg.wasm').then(() => { 16 | const [js_ready, tick] = start(); 17 | window.tick = tick; 18 | js_ready(true); 19 | }); 20 | 21 | 22 | -------------------------------------------------------------------------------- /examples/url/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "url" 3 | version = "0.1.0" 4 | authors = ["Martin Kavík "] 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type = ["cdylib"] 9 | 10 | [dependencies] 11 | gloo-console = "0.2.3" 12 | seed = { path = "../../", features = ["routing"] } 13 | -------------------------------------------------------------------------------- /examples/url/Makefile.toml: -------------------------------------------------------------------------------- 1 | extend = "../../Makefile.toml" 2 | 3 | # ---- BUILD ---- 4 | 5 | [tasks.build] 6 | alias = "default_build" 7 | 8 | [tasks.build_release] 9 | alias = "default_build_release" 10 | 11 | # ---- START ---- 12 | 13 | [tasks.start] 14 | alias = "default_start" 15 | 16 | [tasks.start_release] 17 | alias = "default_start_release" 18 | 19 | # ---- TEST ---- 20 | 21 | [tasks.test_firefox] 22 | alias = "default_test_firefox" 23 | 24 | # ---- LINT ---- 25 | 26 | [tasks.clippy] 27 | alias = "default_clippy" 28 | -------------------------------------------------------------------------------- /examples/url/README.md: -------------------------------------------------------------------------------- 1 | ## Url example 2 | 3 | Intended as a demo of Url functions and browser navigation. 4 | 5 | --- 6 | 7 | ```bash 8 | cargo make start 9 | ``` 10 | 11 | Open [127.0.0.1:8000](http://127.0.0.1:8000) in your browser. 12 | -------------------------------------------------------------------------------- /examples/url/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Url example 10 | 11 | 12 | 13 |
14 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /examples/user_media/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "user_media" 3 | version = "0.1.0" 4 | authors = ["David O'Connor "] 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type = ["cdylib"] 9 | 10 | [dependencies] 11 | gloo-console = "0.2.3" 12 | seed = {path = "../../"} 13 | wasm-bindgen-futures = "0.4.34" 14 | 15 | [dependencies.web-sys] 16 | version = "0.3.61" 17 | features = [ 18 | "MediaDevices", 19 | "MediaStreamConstraints", 20 | "MediaStream", 21 | "HtmlMediaElement", 22 | ] 23 | -------------------------------------------------------------------------------- /examples/user_media/Makefile.toml: -------------------------------------------------------------------------------- 1 | extend = "../../Makefile.toml" 2 | 3 | # ---- BUILD ---- 4 | 5 | [tasks.build] 6 | alias = "default_build" 7 | 8 | [tasks.build_release] 9 | alias = "default_build_release" 10 | 11 | # ---- START ---- 12 | 13 | [tasks.start] 14 | alias = "default_start" 15 | 16 | [tasks.start_release] 17 | alias = "default_start_release" 18 | 19 | # ---- TEST ---- 20 | 21 | [tasks.test_firefox] 22 | alias = "default_test_firefox" 23 | 24 | # ---- LINT ---- 25 | 26 | [tasks.clippy] 27 | alias = "default_clippy" 28 | -------------------------------------------------------------------------------- /examples/user_media/README.md: -------------------------------------------------------------------------------- 1 | ## UserMedia example 2 | 3 | How to show your webcam output in `video` element. 4 | 5 | --- 6 | 7 | ```bash 8 | cargo make start 9 | ``` 10 | 11 | Open [127.0.0.1:8000](http://127.0.0.1:8000) in your browser. -------------------------------------------------------------------------------- /examples/user_media/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | UserMedia example 10 | 11 | 12 |
13 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /examples/user_media/src/lib.rs: -------------------------------------------------------------------------------- 1 | use gloo_console::log; 2 | use seed::{prelude::*, *}; 3 | use wasm_bindgen_futures::JsFuture; 4 | use web_sys::{HtmlMediaElement, MediaStream, MediaStreamConstraints}; 5 | 6 | // ------ ------ 7 | // Init 8 | // ------ ------ 9 | 10 | fn init(_: Url, orders: &mut impl Orders) -> Model { 11 | orders.perform_cmd(user_media()); 12 | Model::default() 13 | } 14 | 15 | async fn user_media() -> Msg { 16 | let mut constraints = MediaStreamConstraints::new(); 17 | constraints.video(&JsValue::from(true)); 18 | 19 | let media_stream_promise = window() 20 | .navigator() 21 | .media_devices() 22 | .unwrap() 23 | .get_user_media_with_constraints(&constraints) 24 | .unwrap(); 25 | 26 | Msg::UserMedia( 27 | JsFuture::from(media_stream_promise) 28 | .await 29 | .map(MediaStream::from), 30 | ) 31 | } 32 | 33 | // ------ ------ 34 | // Model 35 | // ------ ------ 36 | 37 | #[derive(Default)] 38 | struct Model { 39 | video: ElRef, 40 | } 41 | 42 | // ------ ------ 43 | // Update 44 | // ------ ------ 45 | 46 | enum Msg { 47 | UserMedia(Result), 48 | } 49 | 50 | fn update(msg: Msg, model: &mut Model, _: &mut impl Orders) { 51 | match msg { 52 | Msg::UserMedia(Ok(media_stream)) => { 53 | model 54 | .video 55 | .get() 56 | .expect("get video element") 57 | .set_src_object(Some(&media_stream)); 58 | } 59 | Msg::UserMedia(Err(error)) => { 60 | log!(error); 61 | } 62 | } 63 | } 64 | 65 | // ------ ------ 66 | // View 67 | // ------ ------ 68 | 69 | fn view(model: &Model) -> impl IntoNodes { 70 | video![ 71 | el_ref(&model.video), 72 | attrs! { 73 | At::Width => 320, 74 | At::Height => 240, 75 | At::AutoPlay => AtValue::None, 76 | } 77 | ] 78 | } 79 | 80 | // ------ ------ 81 | // Start 82 | // ------ ------ 83 | 84 | #[wasm_bindgen(start)] 85 | pub fn start() { 86 | App::start("app", init, update, view); 87 | } 88 | -------------------------------------------------------------------------------- /examples/websocket/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "websocket" 3 | version = "0.1.0" 4 | authors = ["Markus Kohlhase "] 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type = ["cdylib"] 9 | path = "src/client.rs" 10 | 11 | [[bin]] 12 | name = "server" 13 | path = "src/server.rs" 14 | 15 | [dependencies] 16 | # common 17 | serde = { version = "1.0.152", features = ["derive"] } 18 | serde_json = "1.0.94" 19 | rmp-serde = "1.1.1" 20 | 21 | # server 22 | ws = { version = "0.9.2", optional = true } 23 | 24 | #client 25 | seed = { path = "../../", optional = true } 26 | wasm-sockets = { version = "1", optional = true } 27 | gloo-console = { version = "0.2.3", optional = true } 28 | 29 | [features] 30 | default = [] 31 | client = ["seed", "wasm-sockets", "dep:gloo-console"] 32 | server = ["ws"] 33 | -------------------------------------------------------------------------------- /examples/websocket/README.md: -------------------------------------------------------------------------------- 1 | ## Websocket example 2 | 3 | Example of communicating with a server using Websockets - simple chat. 4 | 5 | - Using web-sys's Websocket in client. 6 | - Serde for [de]serializiation. 7 | - [WS-RS (ws)](https://github.com/housleyjk/ws-rs) as a websocket server. 8 | - Demonstrates sending messages and receiving messages with sender id (see console or server logs). 9 | - There is not workspace - client and server dependencies are resolved by `features`, see `Cargo.toml` and `Makefile.toml`. 10 | 11 | --- 12 | 13 | ```bash 14 | cargo make start 15 | ``` 16 | Open a new terminal window. 17 | ```bash 18 | cargo make start_server 19 | ``` 20 | 21 | Open [127.0.0.1:8000](http://127.0.0.1:8000) in your browser. -------------------------------------------------------------------------------- /examples/websocket/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Websocket example 8 | 9 | 10 | 11 |
12 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /examples/websocket/src/shared.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | /// Message from the server to the client. 4 | #[derive(Serialize, Deserialize)] 5 | pub struct ServerMessage { 6 | pub id: usize, 7 | pub text: String, 8 | } 9 | 10 | /// Message from the client to the server. 11 | #[derive(Serialize, Deserialize)] 12 | pub struct ClientMessage { 13 | pub text: String, 14 | } 15 | -------------------------------------------------------------------------------- /examples/window_events/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "window-events" 3 | version = "0.1.0" 4 | authors = ["David O'Connor "] 5 | edition = "2018" 6 | 7 | 8 | [lib] 9 | crate-type = ["cdylib"] 10 | 11 | [dependencies] 12 | seed = {path = "../../"} 13 | -------------------------------------------------------------------------------- /examples/window_events/Makefile.toml: -------------------------------------------------------------------------------- 1 | extend = "../../Makefile.toml" 2 | 3 | # ---- BUILD ---- 4 | 5 | [tasks.build] 6 | alias = "default_build" 7 | 8 | [tasks.build_release] 9 | alias = "default_build_release" 10 | 11 | # ---- START ---- 12 | 13 | [tasks.start] 14 | alias = "default_start" 15 | 16 | [tasks.start_release] 17 | alias = "default_start_release" 18 | 19 | # ---- TEST ---- 20 | 21 | [tasks.test_firefox] 22 | alias = "default_test_firefox" 23 | 24 | # ---- LINT ---- 25 | 26 | [tasks.clippy] 27 | alias = "default_clippy" 28 | -------------------------------------------------------------------------------- /examples/window_events/README.md: -------------------------------------------------------------------------------- 1 | ## Window events example 2 | 3 | A demonstration of event-handlers attached to the `window`. 4 | 5 | --- 6 | 7 | ```bash 8 | cargo make start 9 | ``` 10 | 11 | Open [127.0.0.1:8000](http://127.0.0.1:8000) in your browser. 12 | -------------------------------------------------------------------------------- /examples/window_events/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Window-events example 8 | 9 | 10 | 11 |
12 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /examples/window_events/pkg/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seed-rs/seed/d39e618861f4da46a598b76cd2048d01b511fda2/examples/window_events/pkg/.keep -------------------------------------------------------------------------------- /examples/window_events/src/lib.rs: -------------------------------------------------------------------------------- 1 | use seed::{prelude::*, *}; 2 | 3 | // ------ ------ 4 | // Init 5 | // ------ ------ 6 | 7 | fn init(_: Url, _: &mut impl Orders) -> Model { 8 | Model::default() 9 | } 10 | 11 | // ------ ------ 12 | // Model 13 | // ------ ------ 14 | 15 | #[derive(Default)] 16 | struct Model { 17 | event_streams: Vec, 18 | point: Point, 19 | key_code: u32, 20 | } 21 | 22 | #[derive(Default)] 23 | struct Point { 24 | x: i32, 25 | y: i32, 26 | } 27 | 28 | // ------ ------ 29 | // Update 30 | // ------ ------ 31 | 32 | enum Msg { 33 | ToggleWatching, 34 | MouseMoved(web_sys::MouseEvent), 35 | KeyPressed(web_sys::KeyboardEvent), 36 | } 37 | 38 | fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders) { 39 | match msg { 40 | Msg::ToggleWatching => { 41 | if model.event_streams.is_empty() { 42 | model.event_streams = vec![ 43 | orders.stream_with_handle(streams::window_event(Ev::MouseMove, |event| { 44 | Msg::MouseMoved(event.unchecked_into()) 45 | })), 46 | orders.stream_with_handle(streams::window_event(Ev::KeyDown, |event| { 47 | Msg::KeyPressed(event.unchecked_into()) 48 | })), 49 | ]; 50 | } else { 51 | model.event_streams.clear(); 52 | } 53 | } 54 | Msg::MouseMoved(ev) => { 55 | model.point = Point { 56 | x: ev.client_x(), 57 | y: ev.client_y(), 58 | } 59 | } 60 | Msg::KeyPressed(ev) => model.key_code = ev.key_code(), 61 | } 62 | } 63 | 64 | // ------ ------ 65 | // View 66 | // ------ ------ 67 | 68 | fn view(model: &Model) -> Vec> { 69 | vec![ 70 | h2![format!("X: {}, Y: {}", model.point.x, model.point.y)], 71 | h2![format!("Last key pressed: {}", model.key_code)], 72 | button![ 73 | ev(Ev::Click, |_| Msg::ToggleWatching), 74 | if model.event_streams.is_empty() { 75 | "Start watching" 76 | } else { 77 | "Stop watching" 78 | } 79 | ], 80 | ] 81 | } 82 | 83 | // ------ ------ 84 | // Start 85 | // ------ ------ 86 | 87 | #[wasm_bindgen(start)] 88 | pub fn start() { 89 | App::start("app", init, update, view); 90 | } 91 | -------------------------------------------------------------------------------- /src/app/cfg.rs: -------------------------------------------------------------------------------- 1 | use super::OrdersContainer; 2 | use crate::virtual_dom::IntoNodes; 3 | use std::rc::Rc; 4 | 5 | #[allow(clippy::module_name_repetitions, clippy::type_complexity)] 6 | pub struct AppCfg 7 | where 8 | Ms: 'static, 9 | Mdl: 'static, 10 | INodes: IntoNodes, 11 | { 12 | pub(crate) document: web_sys::Document, 13 | pub(crate) mount_point: web_sys::Element, 14 | pub(crate) update: Box)>, 15 | pub(crate) view: Box INodes>, 16 | pub(crate) base_path: Rc<[String]>, 17 | } 18 | -------------------------------------------------------------------------------- /src/app/cmd_manager.rs: -------------------------------------------------------------------------------- 1 | use futures::future::{abortable, AbortHandle, Future, FutureExt}; 2 | use wasm_bindgen_futures::spawn_local; 3 | 4 | // ------ CmdManager ------ 5 | 6 | pub(crate) struct CmdManager; 7 | 8 | impl CmdManager { 9 | pub fn perform_cmd(cmd: impl Future + 'static) { 10 | // The future is "leaked" into the JS world as a promise. 11 | // It's always executed on the next JS tick to prevent stack overflow. 12 | spawn_local(cmd); 13 | } 14 | 15 | pub fn perform_cmd_with_handle(cmd: impl Future + 'static) -> CmdHandle { 16 | let (cmd, handle) = abortable(cmd); 17 | // Ignore the error when the future is aborted. I.e. just stop the future execution. 18 | spawn_local(cmd.map(move |_| ())); 19 | CmdHandle(handle) 20 | } 21 | } 22 | 23 | // ------ CmdHandle ------ 24 | 25 | #[derive(Debug)] 26 | pub struct CmdHandle(AbortHandle); 27 | 28 | impl Drop for CmdHandle { 29 | fn drop(&mut self) { 30 | self.0.abort(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/app/cmds.rs: -------------------------------------------------------------------------------- 1 | use futures::future::{Future, FutureExt}; 2 | use gloo_timers::future::TimeoutFuture; 3 | 4 | // @TODO add fetch cmd? 5 | 6 | // ------ Timeout cmd ------ 7 | 8 | /// Set timeout in milliseconds. 9 | /// 10 | /// Handler has to return `Msg`, `Option` or `()`. 11 | /// 12 | /// # Example 13 | /// 14 | /// ```rust,ignore 15 | ///orders.perform_cmd_with_handle(cmds::timeout(2000, || Msg::OnTimeout)); 16 | ///orders.perform_cmd(cmds::timeout(1000, || log!("Tick!"))); 17 | /// ``` 18 | /// 19 | /// # Panics 20 | /// 21 | /// Panics when the command doesn't return `Msg`, `Option` or `()`. 22 | /// (It will be changed to a compile-time error). 23 | pub fn timeout( 24 | ms: u32, 25 | handler: impl FnOnce() -> MsU + Clone + 'static, 26 | ) -> impl Future { 27 | TimeoutFuture::new(ms).map(move |_| handler()) 28 | } 29 | -------------------------------------------------------------------------------- /src/app/data.rs: -------------------------------------------------------------------------------- 1 | use super::{RenderInfo, SubManager}; 2 | use crate::browser::util; 3 | use crate::virtual_dom::{El, EventHandlerManager}; 4 | use std::cell::{Cell, RefCell}; 5 | use wasm_bindgen::closure::Closure; 6 | 7 | type StoredPopstate = RefCell>>; 8 | 9 | #[allow(clippy::type_complexity, dead_code)] 10 | pub(crate) struct AppData { 11 | pub model: RefCell>, 12 | pub(crate) root_el: RefCell>>, 13 | pub popstate_closure: StoredPopstate, 14 | pub hashchange_closure: StoredPopstate, 15 | pub window_event_handler_manager: RefCell>, 16 | pub sub_manager: RefCell>, 17 | pub msg_listeners: RefCell>>, 18 | pub scheduled_render_handle: RefCell>, 19 | pub after_next_render_callbacks: RefCell Option>>>, 20 | pub render_info: Cell>, 21 | } 22 | -------------------------------------------------------------------------------- /src/app/effect.rs: -------------------------------------------------------------------------------- 1 | use super::{MessageMapper, Notification}; 2 | 3 | pub enum Effect { 4 | Msg(Option), 5 | Notification(Notification), 6 | TriggeredHandler(Box Option>), 7 | } 8 | 9 | impl MessageMapper for Effect { 10 | type SelfWithOtherMs = Effect; 11 | fn map_msg(self, f: impl FnOnce(Ms) -> OtherMs + 'static + Clone) -> Effect { 12 | match self { 13 | Effect::Msg(msg) => Effect::Msg(msg.map(f)), 14 | Effect::Notification(notification) => Effect::Notification(notification), 15 | Effect::TriggeredHandler(handler) => { 16 | Effect::TriggeredHandler(Box::new(move || handler().map(f))) 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/app/get_element.rs: -------------------------------------------------------------------------------- 1 | use crate::browser::util::document; 2 | use web_sys::{Element, HtmlElement}; 3 | 4 | pub trait GetElement { 5 | /// Returns wrapped `web_sys::Element` or tries to get one from the DOM. 6 | /// 7 | /// # Errors 8 | /// 9 | /// Returns error if the element cannot be found. 10 | fn get_element(self) -> Result; 11 | } 12 | 13 | impl GetElement for &str { 14 | fn get_element(self) -> Result { 15 | document() 16 | .get_element_by_id(self) 17 | .ok_or_else(|| format!("cannot find element with given id: {self}")) 18 | } 19 | } 20 | 21 | impl GetElement for Element { 22 | fn get_element(self) -> Result { 23 | Ok(self) 24 | } 25 | } 26 | 27 | impl GetElement for HtmlElement { 28 | fn get_element(self) -> Result { 29 | Ok(self.into()) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/app/message_mapper.rs: -------------------------------------------------------------------------------- 1 | pub trait MessageMapper { 2 | type SelfWithOtherMs; 3 | fn map_msg(self, f: impl FnOnce(Ms) -> OtherMs + 'static + Clone) -> Self::SelfWithOtherMs; 4 | } 5 | -------------------------------------------------------------------------------- /src/app/render_info.rs: -------------------------------------------------------------------------------- 1 | #[derive(Copy, Clone, Debug)] 2 | pub struct RenderInfo { 3 | pub timestamp: f64, 4 | pub timestamp_delta: Option, 5 | } 6 | -------------------------------------------------------------------------------- /src/app/stream_manager.rs: -------------------------------------------------------------------------------- 1 | use futures::future::{abortable, ready, AbortHandle, FutureExt}; 2 | use futures::stream::{Stream, StreamExt}; 3 | use wasm_bindgen_futures::spawn_local; 4 | 5 | // ------ StreamManager ------ 6 | 7 | pub(crate) struct StreamManager; 8 | 9 | impl StreamManager { 10 | pub fn stream(stream: impl Stream + 'static) { 11 | // Convert `Stream` to `Future` and execute it. The stream is "leaked" into the JS world. 12 | spawn_local(stream.for_each(|_| ready(()))); 13 | } 14 | 15 | pub fn stream_with_handle(stream: impl Stream + 'static) -> StreamHandle { 16 | // Convert `Stream` to `Future`. 17 | let stream = stream.for_each(|_| ready(())); 18 | // Create `AbortHandle`. 19 | let (stream, handle) = abortable(stream); 20 | // Ignore the error when the future is aborted. I.e. just stop the stream. 21 | spawn_local(stream.map(move |_| ())); 22 | StreamHandle(handle) 23 | } 24 | } 25 | 26 | // ------ StreamHandle ------ 27 | 28 | #[derive(Debug)] 29 | pub struct StreamHandle(AbortHandle); 30 | 31 | impl Drop for StreamHandle { 32 | fn drop(&mut self) { 33 | self.0.abort(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/app/streams/backoff_stream.rs: -------------------------------------------------------------------------------- 1 | use futures::channel::mpsc; 2 | use futures::stream::Stream; 3 | use gloo_timers::callback::Timeout; 4 | use rand::{rngs::SmallRng, Rng, SeedableRng}; 5 | use std::pin::Pin; 6 | use std::rc::Rc; 7 | use std::task::{Context, Poll}; 8 | 9 | // ------ BackoffStream ------ 10 | 11 | /// [Truncated exponential backoff](https://cloud.google.com/storage/docs/exponential-backoff) 12 | #[derive(Debug)] 13 | pub struct BackoffStream { 14 | max_seconds: u32, 15 | retries: usize, 16 | timeout: Timeout, 17 | tick_sender: Rc>, 18 | tick_receiver: mpsc::UnboundedReceiver<()>, 19 | } 20 | 21 | impl BackoffStream { 22 | pub fn new(max_seconds: u32) -> Self { 23 | let (tick_sender, tick_receiver) = mpsc::unbounded(); 24 | let tick_sender = Rc::new(tick_sender); 25 | 26 | let retries = 0; 27 | Self { 28 | max_seconds, 29 | retries, 30 | timeout: start_timeout(wait_time(retries, max_seconds), &tick_sender), 31 | tick_sender, 32 | tick_receiver, 33 | } 34 | } 35 | } 36 | 37 | impl Stream for BackoffStream { 38 | type Item = usize; 39 | 40 | fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll> { 41 | match Stream::poll_next(Pin::new(&mut self.tick_receiver), cx) { 42 | Poll::Ready(Some(_)) => { 43 | self.retries += 1; 44 | self.timeout = 45 | start_timeout(wait_time(self.retries, self.max_seconds), &self.tick_sender); 46 | Poll::Ready(Some(self.retries)) 47 | } 48 | Poll::Ready(None) => Poll::Ready(None), 49 | Poll::Pending => Poll::Pending, 50 | } 51 | } 52 | } 53 | 54 | fn wait_time(retries: usize, max_seconds: u32) -> u32 { 55 | let retries = u32::try_from(retries).unwrap_or(u32::max_value()); 56 | let random_ms = SmallRng::from_entropy().gen_range(0..=1000); 57 | 58 | let duration = 2_u32 59 | .saturating_pow(retries) 60 | .saturating_mul(1000) 61 | .saturating_add(random_ms); 62 | let max_duration = max_seconds.saturating_mul(1000); 63 | 64 | u32::min(duration, max_duration) 65 | } 66 | 67 | fn start_timeout(ms: u32, tick_sender: &Rc>) -> Timeout { 68 | let tick_sender = Rc::clone(tick_sender); 69 | Timeout::new(ms, move || { 70 | tick_sender.unbounded_send(()).expect("send backoff tick"); 71 | }) 72 | } 73 | -------------------------------------------------------------------------------- /src/app/streams/event_stream.rs: -------------------------------------------------------------------------------- 1 | use crate::virtual_dom::Ev; 2 | use futures::channel::mpsc::{unbounded, UnboundedReceiver}; 3 | use futures::stream::Stream; 4 | use std::pin::Pin; 5 | use std::task::{Context, Poll}; 6 | use wasm_bindgen::closure::Closure; 7 | use wasm_bindgen::{JsCast, JsValue}; 8 | use web_sys::EventTarget; 9 | 10 | // ------ EventStream ------ 11 | 12 | // @TODO Replace `mpsc` with `crossbeam`, `futures-signals` or `flume`? 13 | // (And integrate it into the other Seed parts (e.g. `Listener`, `SubManager`, `BackoffStream`)). 14 | 15 | // @TODO Update it to support different `web_sys` events 16 | // during implementation of https://github.com/seed-rs/seed/issues/331 17 | 18 | #[derive(Debug)] 19 | pub struct EventStream { 20 | node: EventTarget, 21 | trigger: Ev, 22 | callback: Closure, 23 | receiver: UnboundedReceiver, 24 | } 25 | 26 | impl EventStream 27 | where 28 | E: JsCast + 'static, 29 | { 30 | pub fn new(node: &EventTarget, trigger: impl Into) -> Self { 31 | let trigger = trigger.into(); 32 | 33 | let (sender, receiver) = unbounded(); 34 | 35 | // @TODO replace with `Closure::new` once stable (or use the Seed's temporary one). 36 | let callback = Closure::wrap(Box::new(move |event: JsValue| { 37 | sender.unbounded_send(event.dyn_into().unwrap()).unwrap(); 38 | }) as Box); 39 | 40 | node.add_event_listener_with_callback(trigger.as_str(), callback.as_ref().unchecked_ref()) 41 | .unwrap(); 42 | 43 | Self { 44 | node: node.clone(), 45 | trigger, 46 | callback, 47 | receiver, 48 | } 49 | } 50 | } 51 | 52 | impl Stream for EventStream { 53 | type Item = E; 54 | 55 | fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll> { 56 | Stream::poll_next(Pin::new(&mut self.receiver), cx) 57 | } 58 | } 59 | 60 | impl Drop for EventStream { 61 | fn drop(&mut self) { 62 | self.node 63 | .remove_event_listener_with_callback( 64 | self.trigger.as_str(), 65 | self.callback.as_ref().unchecked_ref(), 66 | ) 67 | .unwrap(); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/app/subs/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::browser::Url; 2 | 3 | // ------ UrlRequested sub ------ 4 | 5 | pub mod url_requested; 6 | pub use url_requested::UrlRequested; 7 | 8 | // ------ UrlChanged sub ------ 9 | 10 | /// Subscribe to url changes. 11 | /// 12 | /// # Example 13 | /// 14 | /// ```rust,ignore 15 | ///orders.subscribe(Msg::UrlChanged).notify(subs::UrlChanged(url)); 16 | ///... 17 | ///update(... Msg::UrlChanged(subs::UrlChanged(url)) => 18 | /// ``` 19 | #[derive(Debug, Clone)] 20 | pub struct UrlChanged(pub Url); 21 | -------------------------------------------------------------------------------- /src/browser/dom/namespace.rs: -------------------------------------------------------------------------------- 1 | /// Common Namespaces 2 | #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] 3 | pub enum Namespace { 4 | Html, 5 | Svg, 6 | MathMl, 7 | Xul, 8 | Xbl, 9 | Custom(String), 10 | } 11 | 12 | // https://developer.mozilla.org/en-US/docs/Web/API/Document/createElementNS 13 | impl Namespace { 14 | pub fn as_str(&self) -> &str { 15 | match self { 16 | Namespace::Html => "http://www.w3.org/1999/xhtml", 17 | Namespace::Svg => "http://www.w3.org/2000/svg", 18 | Namespace::MathMl => "http://www.w3.org/1998/mathml", 19 | Namespace::Xul => "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", 20 | Namespace::Xbl => "http://www.mozilla.org/xbl", 21 | Namespace::Custom(namespace) => namespace, 22 | } 23 | } 24 | } 25 | 26 | impl From for Namespace { 27 | fn from(namespace: String) -> Self { 28 | match namespace.as_ref() { 29 | "http://www.w3.org/1999/xhtml" => Namespace::Html, 30 | "http://www.w3.org/2000/svg" => Namespace::Svg, 31 | "http://www.w3.org/1998/mathml" => Namespace::MathMl, 32 | "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" => Namespace::Xul, 33 | "http://www.mozilla.org/xbl" => Namespace::Xbl, 34 | _ => Namespace::Custom(namespace), 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/browser/json/mod.rs: -------------------------------------------------------------------------------- 1 | use serde::{de::DeserializeOwned, Serialize}; 2 | use serde_wasm_bindgen as swb; 3 | use wasm_bindgen::JsValue; 4 | 5 | pub fn from_js_value(v: &JsValue) -> Result 6 | where 7 | T: DeserializeOwned, 8 | { 9 | Ok(swb::from_value(v.into())?) 10 | } 11 | 12 | pub fn to_js_value(v: &T) -> Result 13 | where 14 | T: Serialize + ?Sized, 15 | { 16 | Ok(v.serialize(&swb::Serializer::json_compatible())?) 17 | } 18 | -------------------------------------------------------------------------------- /src/browser/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod dom; 2 | pub mod service; 3 | pub mod url; 4 | pub mod util; 5 | 6 | #[cfg(feature = "routing")] 7 | mod json; 8 | 9 | pub use url::{Url, UrlSearch, DUMMY_BASE_URL}; 10 | -------------------------------------------------------------------------------- /src/browser/service/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "routing")] 2 | pub(crate) mod routing; 3 | -------------------------------------------------------------------------------- /src/dom_entity_names/attributes/mod.rs: -------------------------------------------------------------------------------- 1 | /// Similar to tag population. 2 | macro_rules! make_attrs { 3 | // Create shortcut macros for any element; populate these functions in this module. 4 | { $($attr_camel:ident => $attr:expr),+ } => { 5 | 6 | /// The At enum restricts element-creation to only valid attribute names, as defined here: 7 | /// [https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes) 8 | #[derive(Clone, Debug, PartialEq, Eq, Hash)] 9 | pub enum At { 10 | $( 11 | $attr_camel, 12 | )+ 13 | Custom(std::borrow::Cow<'static, str>) 14 | } 15 | 16 | impl At { 17 | pub fn as_str(&self) -> &str { 18 | match self { 19 | $ ( 20 | At::$attr_camel => $attr, 21 | ) + 22 | At::Custom(attr) => &attr 23 | } 24 | } 25 | } 26 | 27 | impl std::fmt::Display for At { 28 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 29 | write!(f, "{}", self.as_str()) 30 | } 31 | } 32 | 33 | impl>> From for At { 34 | fn from(attr: T) -> Self { 35 | let attr = attr.into(); 36 | match attr.as_ref() { 37 | $( 38 | $attr => At::$attr_camel, 39 | ) + 40 | _ => { 41 | At::Custom(attr) 42 | } 43 | } 44 | } 45 | } 46 | } 47 | } 48 | 49 | mod attribute_names; 50 | pub use attribute_names::At; 51 | -------------------------------------------------------------------------------- /src/dom_entity_names/events/mod.rs: -------------------------------------------------------------------------------- 1 | /// Similar to tag population. 2 | macro_rules! make_events { 3 | // Create shortcut macros for any element; populate these functions in this module. 4 | { $($event_camel:ident => $event:expr),+ } => { 5 | 6 | /// The Ev enum restricts element-creation to only valid event names, as defined here: 7 | /// [MDN reference Web/Events](https://developer.mozilla.org/en-US/docs/Web/Events) 8 | #[derive(Clone, Debug, PartialEq, PartialOrd, Ord, Eq)] 9 | pub enum Ev { 10 | $( 11 | $event_camel, 12 | )+ 13 | Custom(std::borrow::Cow<'static, str>) 14 | } 15 | 16 | impl Ev { 17 | pub fn as_str(&self) -> &str { 18 | match self { 19 | $( 20 | Ev::$event_camel => $event, 21 | ) + 22 | Ev::Custom(event) => &event 23 | } 24 | } 25 | } 26 | 27 | impl std::fmt::Display for Ev { 28 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 29 | write!(f, "{}", self.as_str()) 30 | } 31 | } 32 | 33 | impl>> From for Ev { 34 | fn from(event: T) -> Self { 35 | let event = event.into(); 36 | match event.as_ref() { 37 | $( 38 | $event => Ev::$event_camel, 39 | ) + 40 | _ => { 41 | Ev::Custom(event) 42 | } 43 | } 44 | } 45 | } 46 | } 47 | } 48 | 49 | mod event_names; 50 | pub use event_names::Ev; 51 | -------------------------------------------------------------------------------- /src/dom_entity_names/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod attributes; 2 | pub mod events; 3 | pub mod styles; 4 | pub mod tags; 5 | 6 | pub use attributes::At; 7 | pub use events::Ev; 8 | pub use styles::St; 9 | pub use tags::Tag; 10 | -------------------------------------------------------------------------------- /src/dom_entity_names/styles/mod.rs: -------------------------------------------------------------------------------- 1 | /// Similar to tag population. 2 | macro_rules! make_styles { 3 | // Create shortcut macros for any style; populate these functions in the submodule. 4 | { $($st_pascal_case:ident => $st:expr),+ } => { 5 | 6 | /// The St enum restricts element-creation to only valid styles. 7 | #[derive(Clone, Debug, PartialEq, Eq, Hash)] 8 | pub enum St { 9 | $( 10 | $st_pascal_case, 11 | )+ 12 | Custom(std::borrow::Cow<'static, str>) 13 | } 14 | 15 | impl St { 16 | pub fn as_str(&self) -> &str { 17 | match self { 18 | $ ( 19 | St::$st_pascal_case => $st, 20 | ) + 21 | St::Custom(style) => &style 22 | } 23 | } 24 | } 25 | 26 | impl std::fmt::Display for St { 27 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 28 | write!(f, "{}", self.as_str()) 29 | } 30 | } 31 | 32 | impl>> From for St { 33 | fn from(style: T) -> Self { 34 | let style = style.into(); 35 | match style.as_ref() { 36 | $( 37 | $st => St::$st_pascal_case, 38 | ) + 39 | _ => { 40 | St::Custom(style) 41 | } 42 | } 43 | } 44 | } 45 | } 46 | } 47 | 48 | mod style_names; 49 | pub use style_names::St; 50 | -------------------------------------------------------------------------------- /src/dom_entity_names/tags/mod.rs: -------------------------------------------------------------------------------- 1 | // Populate tags using a macro, to reduce code repetition. 2 | // The tag enum primarily exists to ensure only valid elements are allowed. 3 | // We leave out non-body tags like html, meta, title, and body. 4 | macro_rules! make_tags { 5 | // Create shortcut macros for any element; populate these functions in this module. 6 | { $($tag_camel:ident => $tag:expr),+ } => { 7 | 8 | /// The Tag enum restricts element-creation to only valid tags, as defined here: 9 | /// [https://developer.mozilla.org/en-US/docs/Web/HTML/Element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element) 10 | #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] 11 | pub enum Tag { 12 | $( 13 | $tag_camel, 14 | )+ 15 | Custom(std::borrow::Cow<'static, str>) 16 | } 17 | 18 | impl Tag { 19 | pub fn as_str(&self) -> &str { 20 | match self { 21 | $( 22 | Tag::$tag_camel => $tag, 23 | ) + 24 | Tag::Custom(tag) => &tag 25 | } 26 | } 27 | } 28 | 29 | impl std::fmt::Display for Tag { 30 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 31 | write!(f, "{}", self.as_str()) 32 | } 33 | } 34 | 35 | impl>> From for Tag { 36 | fn from(tag: T) -> Self { 37 | let tag = tag.into(); 38 | match tag.as_ref() { 39 | $( 40 | $tag => Tag::$tag_camel, 41 | ) + 42 | _ => { 43 | Tag::Custom(tag) 44 | } 45 | } 46 | } 47 | } 48 | } 49 | } 50 | 51 | mod tag_names; 52 | pub use tag_names::Tag; 53 | -------------------------------------------------------------------------------- /src/helpers.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Not; 2 | 3 | /// Alternative to `!`. 4 | /// 5 | /// # Example 6 | /// 7 | /// ```rust,ignore 8 | ///div![ 9 | /// "Button", 10 | /// IF!(not(disabled) => ev(Ev::Click, Msg::Clicked)), 11 | ///] 12 | /// ``` 13 | pub fn not(predicate: T) -> T::Output { 14 | predicate.not() 15 | } 16 | 17 | // @TODO move helpers from lib.rs or shortcuts.rs here 18 | 19 | // ------ ------ Tests ------ ------ 20 | 21 | #[cfg(test)] 22 | mod tests { 23 | use super::*; 24 | use wasm_bindgen_test::*; 25 | 26 | #[wasm_bindgen_test] 27 | fn helpers_not() { 28 | assert!(not(false)); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/virtual_dom/event_handler_manager/event_handler.rs: -------------------------------------------------------------------------------- 1 | use crate::app::MessageMapper; 2 | use crate::virtual_dom::Ev; 3 | use std::{fmt, rc::Rc}; 4 | 5 | /// `EventHandler`s are called by DOM event listeners with the same trigger (an event to listen to). 6 | pub struct EventHandler { 7 | pub trigger: Ev, 8 | pub callback: Rc Option>, 9 | } 10 | 11 | // @TODO remove custom impl once https://github.com/rust-lang/rust/issues/26925 is fixed 12 | impl Clone for EventHandler { 13 | fn clone(&self) -> Self { 14 | Self { 15 | trigger: self.trigger.clone(), 16 | callback: Rc::clone(&self.callback), 17 | } 18 | } 19 | } 20 | 21 | impl EventHandler { 22 | pub fn new( 23 | trigger: impl Into, 24 | callback: impl Fn(web_sys::Event) -> Option + 'static, 25 | ) -> Self { 26 | Self { 27 | trigger: trigger.into(), 28 | callback: Rc::new(callback), 29 | } 30 | } 31 | } 32 | 33 | impl MessageMapper for EventHandler { 34 | type SelfWithOtherMs = EventHandler; 35 | fn map_msg( 36 | self, 37 | msg_mapper: impl FnOnce(Ms) -> OtherMs + 'static + Clone, 38 | ) -> EventHandler { 39 | let old_callback = self.callback; 40 | let new_callback = move |event| { 41 | let msg_mapper = msg_mapper.clone(); 42 | old_callback(event).map(msg_mapper) 43 | }; 44 | EventHandler { 45 | trigger: self.trigger, 46 | callback: Rc::new(new_callback), 47 | } 48 | } 49 | } 50 | 51 | impl fmt::Debug for EventHandler { 52 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 53 | write!(f, "EventHandler('{}')", self.trigger.as_str()) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/virtual_dom/mailbox.rs: -------------------------------------------------------------------------------- 1 | use std::rc::Rc; 2 | 3 | pub struct Mailbox { 4 | func: Rc)>, 5 | } 6 | 7 | impl Mailbox { 8 | pub fn new(func: impl Fn(Option) + 'static) -> Self { 9 | Mailbox { 10 | func: Rc::new(func), 11 | } 12 | } 13 | 14 | pub fn send(&self, message: Option) { 15 | (self.func)(message); 16 | } 17 | } 18 | 19 | impl Clone for Mailbox { 20 | fn clone(&self) -> Self { 21 | Mailbox { 22 | func: self.func.clone(), 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/virtual_dom/node/into_nodes.rs: -------------------------------------------------------------------------------- 1 | use super::Node; 2 | 3 | /// Items that implement `IntoNodes`: 4 | /// - Can be used in `nodes!`. 5 | /// - Can be returned from `view`. 6 | pub trait IntoNodes { 7 | /// Converts item or items to `Vec`. 8 | fn into_nodes(self) -> Vec>; 9 | } 10 | 11 | impl IntoNodes for Node { 12 | fn into_nodes(self) -> Vec> { 13 | vec![self] 14 | } 15 | } 16 | 17 | impl> IntoNodes for Option { 18 | fn into_nodes(self) -> Vec> { 19 | self.map(IntoNodes::into_nodes).unwrap_or_default() 20 | } 21 | } 22 | 23 | impl IntoNodes for Vec> { 24 | fn into_nodes(self) -> Vec> { 25 | self 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/virtual_dom/node/text.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | use std::fmt; 3 | 4 | /// For representing text nodes. 5 | /// [MDN reference](https://developer.mozilla.org/en-US/docs/Web/API/Text) 6 | /// [`web_sys` reference](https://rustwasm.github.io/wasm-bindgen/api/web_sys/struct.Text.html) 7 | #[derive(Clone, Debug)] 8 | pub struct Text { 9 | pub text: Cow<'static, str>, 10 | pub node_ws: Option, 11 | } 12 | 13 | impl PartialEq for Text { 14 | fn eq(&self, other: &Self) -> bool { 15 | self.text == other.text 16 | } 17 | } 18 | 19 | impl fmt::Display for Text { 20 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 21 | write!(f, "{}", self.text) 22 | } 23 | } 24 | 25 | impl Text { 26 | pub fn new(text: impl Into>) -> Self { 27 | Self { 28 | text: text.into(), 29 | node_ws: None, 30 | } 31 | } 32 | 33 | pub fn strip_ws_node(&mut self) { 34 | self.node_ws.take(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/virtual_dom/style.rs: -------------------------------------------------------------------------------- 1 | use super::{CSSValue, St}; 2 | use indexmap::IndexMap; 3 | use std::fmt; 4 | 5 | /// Handle Style separately from Attrs, since it commonly involves multiple parts, 6 | /// and has a different semantic meaning. 7 | #[derive(Clone, Debug, PartialEq, Eq)] 8 | pub struct Style { 9 | pub vals: IndexMap, 10 | } 11 | 12 | impl Style { 13 | pub const fn new(vals: IndexMap) -> Self { 14 | Self { vals } 15 | } 16 | 17 | pub fn empty() -> Self { 18 | Self { 19 | vals: IndexMap::new(), 20 | } 21 | } 22 | 23 | pub fn add(&mut self, key: impl Into, val: impl Into) { 24 | self.vals.insert(key.into(), val.into()); 25 | } 26 | 27 | /// Combine with another Style; if there's a conflict, use the other one. 28 | pub fn merge(&mut self, other: Self) { 29 | self.vals.extend(other.vals.into_iter()); 30 | } 31 | } 32 | 33 | /// Output style as a string, as would be set in the DOM as the attribute value 34 | /// for 'style'. Eg: "display: flex; font-size: 1.5em" 35 | impl fmt::Display for Style { 36 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 37 | let string = if self.vals.keys().len() > 0 { 38 | self.vals 39 | .iter() 40 | .filter_map(|(k, v)| match v { 41 | CSSValue::Ignored => None, 42 | CSSValue::Some(value) => Some(format!("{}:{}", k.as_str(), value)), 43 | }) 44 | .collect::>() 45 | .join(";") 46 | } else { 47 | String::new() 48 | }; 49 | write!(f, "{string}") 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/virtual_dom/view.rs: -------------------------------------------------------------------------------- 1 | use super::{IntoNodes, Node}; 2 | 3 | #[deprecated( 4 | since = "0.7.0", 5 | note = "Use [`IntoNodes`](../node/into_nodes/trait.IntoNodes.html) instead." 6 | )] 7 | pub trait View: IntoNodes { 8 | fn els(self) -> Vec>; 9 | } 10 | 11 | impl View for Node { 12 | fn els(self) -> Vec> { 13 | vec![self] 14 | } 15 | } 16 | 17 | impl View for Vec> { 18 | fn els(self) -> Vec> { 19 | self 20 | } 21 | } 22 | --------------------------------------------------------------------------------