├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_requst.md ├── dependabot.yml └── workflows │ └── main.yml ├── .gitignore ├── Cargo.toml ├── README.md ├── docs └── todomvc │ ├── index-a7e35c24844438bc.js │ ├── index-a7e35c24844438bc_bg.wasm │ └── index.html ├── dog-app ├── .gitignore ├── Cargo.toml ├── README.md ├── dog-app.png └── src │ ├── main.rs │ └── models.rs ├── ecommerce-site ├── .gitignore ├── Cargo.toml ├── README.md ├── demo.png ├── input.css ├── public │ └── tailwind.css ├── src │ ├── api.rs │ ├── components │ │ ├── cart.rs │ │ ├── error.rs │ │ ├── home.rs │ │ ├── nav.rs │ │ ├── product_item.rs │ │ └── product_page.rs │ └── main.rs └── tailwind.config.js ├── file-explorer ├── .gitignore ├── Cargo.toml ├── Dioxus.toml ├── README.md ├── assets │ ├── image.png │ └── main.css └── src │ └── main.rs ├── image_generator_open_ai ├── .gitignore ├── Cargo.toml └── src │ └── main.rs ├── ios_demo ├── .gitignore ├── Cargo.toml ├── README.md ├── assets │ ├── screenshot.jpeg │ └── screenshot_smaller.jpeg ├── mobile.toml └── src │ ├── lib.rs │ └── style.css ├── jsframework-benchmark ├── .gitignore ├── Cargo.toml ├── index.html └── src │ └── main.rs ├── src └── main.rs ├── todomvc ├── .gitignore ├── Cargo.toml ├── README.md ├── docs │ ├── index.html │ └── wasm │ │ ├── module.js │ │ └── module_bg.wasm ├── example.png └── src │ ├── lib.rs │ ├── main.rs │ └── style.css ├── weatherapp ├── .gitignore ├── Cargo.toml ├── README.md ├── src │ └── main.rs └── weatherapp.png └── wifi-scanner ├── .gitignore ├── Cargo.toml ├── README.md ├── demo_small.png └── src └── main.rs /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: DioxusLabs # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | open_collective: dioxus-labs # Replace with a single Open Collective username 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve Dioxus 4 | --- 5 | 6 | **Problem** 7 | 8 | 9 | 10 | **Steps To Reproduce** 11 | 12 | Steps to reproduce the behavior: 13 | 14 | - 15 | - 16 | - 17 | 18 | **Expected behavior** 19 | 20 | A clear and concise description of what you expected to happen. 21 | 22 | **Screenshots** 23 | 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Environment:** 27 | - Dioxus version: [e.g. v0.17, `master`] 28 | - Rust version: [e.g. 1.43.0, `nightly`] 29 | - OS info: [e.g. MacOS] 30 | - App platform: [e.g. `web`, `desktop`] 31 | 32 | **Questionnaire** 33 | 34 | - [ ] I'm interested in fixing this myself but don't know where to start 35 | - [ ] I would like to fix and I have a solution 36 | - [ ] I don't have time to fix this right now, but maybe later -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_requst.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: If you have any interesting advice, you can tell us. 4 | --- 5 | 6 | ## Specific Demand 7 | 8 | 11 | 12 | ## Implement Suggestion 13 | 14 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # Maintain dependencies for GitHub Actions 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "weekly" 8 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Rust CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | paths: 8 | - /* 9 | - .github/** 10 | - lib.rs 11 | - Cargo.toml 12 | 13 | pull_request: 14 | types: [opened, synchronize, reopened, ready_for_review] 15 | branches: 16 | - master 17 | paths: 18 | - packages/** 19 | - examples/** 20 | - src/** 21 | - .github/** 22 | - lib.rs 23 | - Cargo.toml 24 | 25 | jobs: 26 | check: 27 | if: github.event.pull_request.draft == false 28 | name: Check 29 | runs-on: ubuntu-latest 30 | steps: 31 | - uses: dtolnay/rust-toolchain@stable 32 | - uses: Swatinem/rust-cache@v2 33 | - run: sudo apt-get update 34 | - run: sudo apt-get install libsoup2.4 javascriptcoregtk-4.0 35 | - run: sudo apt install libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev 36 | - uses: actions/checkout@v4 37 | - run: cargo check --all --examples --tests --exclude dioxus-ios-demo 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | dist -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "dog-app", 4 | "ecommerce-site", 5 | "image_generator_open_ai", 6 | "ios_demo", 7 | "jsframework-benchmark", 8 | "todomvc", 9 | "weatherapp", 10 | "wifi-scanner", 11 | ] 12 | 13 | # This is a "virtual package" 14 | # It is not meant to be published, but is used so "cargo run --example XYZ" works properly 15 | [package] 16 | name = "dioxus-example-projects" 17 | version = "0.0.1" 18 | edition = "2021" 19 | description = "Top level crate for the Dioxus examples repository" 20 | authors = ["Jonathan Kelley"] 21 | license = "MIT OR Apache-2.0" 22 | repository = "https://github.com/DioxusLabs/dioxus/" 23 | homepage = "https://dioxuslabs.com" 24 | documentation = "https://dioxuslabs.com" 25 | keywords = ["dom", "ui", "gui", "react", "wasm"] 26 | rust-version = "1.60.0" 27 | publish = false 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # **These examples have moved to the main dioxus repo. You can view updated versions [here](https://github.com/DioxusLabs/dioxus/tree/main/examples)** 2 | 3 | # Example projects with Dioxus 4 | 5 | This repository holds the code for a variety of example projects built with Dioxus. 6 | 7 | Each project has information on how to build/deploy. 8 | 9 | If you want to add your own, feel free to make a PR! 10 | 11 | 12 | Current Projects: 13 | 14 | 15 | | Example | Platform | Creator | 16 | | -------------------------------- | ------------ | ----------- | 17 | | [TodoMVC](./todomvc) | Web, Desktop | @jkelleyrtp | 18 | | [TodoMVC](./ios_demo) | iOS | @jkelleyrtp | 19 | | [File Explorer](./file-explorer) | Desktop | @jkelleyrtp | 20 | | [E-Commerce](./ecommerce-site) | LiveView | @jkelleyrtp | 21 | | [WiFi Scanner](./wifi-scanner) | Desktop | @jkelleyrtp | 22 | | [Weather App](./weatherapp) | Web | @jkelleyrtp | 23 | | [Dog App](./dog-app) | Desktop | @jkelleyrtp | 24 | 25 | ## TODOMVC (Desktop, Web) 26 | 27 | [![TodoMVC](./todomvc/example.png)](./todomvc) 28 | 29 | ## iOS TODOMVC 30 | [![TodoMVC Mobile](./ios_demo/assets/screenshot_smaller.jpeg)](./ios_demo) 31 | 32 | ## File Explorer (Desktop) 33 | [![File Explorer](./file-explorer/image.png)](./file-explorer) 34 | 35 | 36 | ## E-Commerce (Liveview) 37 | [![E Commerce](./ecommerce-site/demo.png)](./ecommerce-site) 38 | 39 | 40 | ## WiFi Scanner (Desktop) 41 | [![WiFi Scanner](./wifi-scanner/demo_small.png)](./wifi-scanner) 42 | 43 | ## Weather App (Web) 44 | [Instructions](./weatherapp/README.md/#Instructions) 45 | 46 | [![Weather App](./weatherapp/weatherapp.png)](./weatherapp) 47 | 48 | ## Dog App (Desktop) 49 | [Instructions](./dog-app/README.md/#Instructions) 50 | 51 | [![Dog App](./dog-app/dog-app.png)](./dog-app) 52 | -------------------------------------------------------------------------------- /docs/todomvc/index-a7e35c24844438bc_bg.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DioxusLabs/example-projects/db87199d2fcdbdd70cf5b6bed5b156ac35fb1e91/docs/todomvc/index-a7e35c24844438bc_bg.wasm -------------------------------------------------------------------------------- /docs/todomvc/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /dog-app/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /dog-app/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dog-app" 3 | version = "0.1.1" 4 | edition = "2021" 5 | description = "Search the Dog CEO API for your favorite doggo" 6 | license = "MIT/Apache-2.0" 7 | repository = "https://github.com/DioxusLabs/dioxus/" 8 | homepage = "https://dioxuslabs.com" 9 | documentation = "https://dioxuslabs.com" 10 | keywords = ["dom", "ui", "gui", "react", "wasm"] 11 | 12 | [dependencies] 13 | dioxus = "0.4.0" 14 | dioxus-desktop = "0.4.0" 15 | reqwest = { version = "0.11.8", features = ["json"] } 16 | serde = { version = "1.0.132", features = ["derive"] } 17 | 18 | [package.metadata.bundle] 19 | name = "Dog Search Engine" 20 | identifier = "com.jon.dogsearch" 21 | version = "1.0.1" 22 | copyright = "Copyright (c) Jane Doe 2016. All rights reserved." 23 | category = "Developer Tool" 24 | short_description = "An example application." 25 | long_description = """ 26 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do 27 | eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut 28 | enim ad minim veniam, quis nostrud exercitation ullamco laboris 29 | nisi ut aliquip ex ea commodo consequat. 30 | """ 31 | # deb_depends = ["libgl1-mesa-glx", "libsdl2-2.0-0 (>= 2.0.5)"] 32 | # osx_frameworks = ["SDL2"] 33 | -------------------------------------------------------------------------------- /dog-app/README.md: -------------------------------------------------------------------------------- 1 | # 🐶 Dog App 2 | 3 | ![Dog App](./dog-app.png) 4 | 5 | ### Instructions 6 | 7 | To start it locally, run: 8 | 9 | ```bash 10 | cargo run 11 | ``` -------------------------------------------------------------------------------- /dog-app/dog-app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DioxusLabs/example-projects/db87199d2fcdbdd70cf5b6bed5b156ac35fb1e91/dog-app/dog-app.png -------------------------------------------------------------------------------- /dog-app/src/main.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_snake_case)] 2 | 3 | use dioxus::{html::input_data::keyboard_types::Key, prelude::*}; 4 | use dioxus_desktop::{Config, WindowBuilder}; 5 | 6 | use crate::models::{list_all_breeds, random_image_by_breed}; 7 | mod models; 8 | 9 | fn main() { 10 | dioxus_desktop::launch_cfg( 11 | app, 12 | Config::default().with_window( 13 | WindowBuilder::new() 14 | .with_maximized(true) 15 | .with_title("Dog App"), 16 | ), 17 | ) 18 | } 19 | 20 | fn app(cx: Scope) -> Element { 21 | let selected_breed = use_state::>(cx, || None); 22 | let search_input = use_state(cx, String::new); 23 | 24 | // Fetch the dog list when the component mounts 25 | let fut = use_future!(cx, |()| async move { list_all_breeds().await }); 26 | 27 | render!( 28 | link { 29 | rel: "stylesheet", 30 | href: "https://unpkg.com/tailwindcss@^2.0/dist/tailwind.min.css" 31 | } 32 | div { 33 | nav { class: "bg-white shadow p-8 border-b border-gray-400 sticky top-0", 34 | div { class: "flex justify-center align-vertical", 35 | input { 36 | class: "p-4 w-full", 37 | "type": "text", 38 | value: "{search_input}", 39 | placeholder: "Search for doggo", 40 | oninput: move |evt| search_input.set(evt.value.clone()), 41 | onkeydown: move |evt| { if evt.key() == Key::Enter {} } 42 | } 43 | } 44 | } 45 | div { class: "px-2 flex gap-2", 46 | div { class: "grow w-full", 47 | if let Some(Ok(breeds)) = fut.value() { 48 | let current_search = search_input.get(); 49 | rsx!{ 50 | breeds.message.iter().filter_map(|(breed, subbreeds)| { 51 | if current_search.is_empty() || breed.contains(current_search) { 52 | let onclick = move |_| { 53 | to_owned![selected_breed, breed]; 54 | cx.spawn(async move { 55 | if let Ok(image) = random_image_by_breed(&breed).await { 56 | selected_breed.set(Some(image.message)); 57 | } 58 | }) 59 | }; 60 | 61 | Some(rsx! { 62 | Card { 63 | key: "{breed}", 64 | title: breed.to_string(), 65 | list: subbreeds.clone(), 66 | onclick: onclick, 67 | } 68 | }) 69 | } else { 70 | None 71 | } 72 | }) 73 | } 74 | } else { 75 | rsx!("No dogs 🐶😔") 76 | } 77 | } 78 | if let Some(selected_breed) = selected_breed.get() { 79 | rsx!( 80 | div { 81 | class: "fixed bottom-0 right-0 m-10 shadow-lg", 82 | img { 83 | class: "w-80 h-80 rounded-lg", 84 | src: "{selected_breed}" 85 | } 86 | } 87 | ) 88 | } 89 | } 90 | } 91 | ) 92 | } 93 | 94 | #[allow(non_snake_case)] 95 | #[inline_props] 96 | fn Card<'a>(cx: Scope, title: String, list: Vec, onclick: EventHandler<'a>) -> Element { 97 | render!( 98 | div { 99 | onclick: |_| onclick.call(()), 100 | class: "my-2 bg-gray-100 w-full rounded-lg p-4 hover:bg-gray-200 ease-in-out duration-75", 101 | h3 { class: "text-1xl", "{title}" } 102 | ul { class: "list-disc ml-8", 103 | {list.iter().map(|item| rsx!( li { 104 | key: "{item}", 105 | "{item}" 106 | }))} 107 | } 108 | } 109 | ) 110 | } 111 | -------------------------------------------------------------------------------- /dog-app/src/models.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_snake_case)] 2 | use serde::{Deserialize, Serialize}; 3 | use std::collections::HashMap; 4 | 5 | #[derive(Serialize, Deserialize)] 6 | pub struct AllBreeds { 7 | pub message: HashMap>, 8 | pub status: String, 9 | } 10 | 11 | pub async fn list_all_breeds() -> reqwest::Result { 12 | reqwest::get("https://dog.ceo/api/breeds/list/all") 13 | .await? 14 | .json::() 15 | .await 16 | } 17 | 18 | #[derive(Serialize, Deserialize)] 19 | pub struct RandomImageByBreed { 20 | pub message: String, 21 | pub status: String, 22 | } 23 | 24 | pub async fn random_image_by_breed(breed: &str) -> reqwest::Result { 25 | reqwest::get(format!("https://dog.ceo/api/breed/{breed}/images/random")) 26 | .await? 27 | .json::() 28 | .await 29 | } 30 | -------------------------------------------------------------------------------- /ecommerce-site/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /ecommerce-site/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ecommerce-site" 3 | version = "0.1.1" 4 | edition = "2018" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | axum = { version = "0.6.16", features = ["ws"] } 10 | cached = "0.44.0" 11 | dioxus = { git = "https://github.com/DioxusLabs/dioxus" } 12 | dioxus-ssr = { git = "https://github.com/DioxusLabs/dioxus" } 13 | dioxus-liveview = { git = "https://github.com/DioxusLabs/dioxus", features = ["axum"] } 14 | reqwest = { version = "0.11.16", features = ["json"] } 15 | serde = "1.0.160" 16 | tokio = { version = "1.15.0", features = ["full"] } 17 | tower-http = { version = "0.4.0", features = ["fs"] } 18 | chrono = { version = "0.4.24", features = ["serde"] } 19 | -------------------------------------------------------------------------------- /ecommerce-site/README.md: -------------------------------------------------------------------------------- 1 | # Dioxus Example: An e-commerce site using the FakeStoreAPI 2 | 3 | This example app is a LiveView site leveraging the [FakeStoreAPI](https://fakestoreapi.com) and [Tailwind CSS](https://tailwindcss.com/). 4 | 5 | ![Demo Image](demo.png) 6 | 7 | # Development 8 | 9 | 1. Run the following commands to serve the application (see the tailwind example in the main Dioxus repo for more detailed information about setting up tailwind): 10 | 11 | ```bash 12 | npx tailwindcss -i ./input.css -o ./public/tailwind.css --watch 13 | cargo watch -x 'run --release' 14 | ``` 15 | 16 | # Status 17 | 18 | This is a work in progress. The following features are currently implemented: 19 | 20 | - [x] A homepage with a list of products dynamically fetched from the FakeStoreAPI (rendered using SSR) 21 | - [x] A product detail page with details about a product (rendered using LiveView) 22 | - [ ] A cart page 23 | - [ ] A checkout page 24 | - [ ] A login page 25 | -------------------------------------------------------------------------------- /ecommerce-site/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DioxusLabs/example-projects/db87199d2fcdbdd70cf5b6bed5b156ac35fb1e91/ecommerce-site/demo.png -------------------------------------------------------------------------------- /ecommerce-site/input.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; -------------------------------------------------------------------------------- /ecommerce-site/public/tailwind.css: -------------------------------------------------------------------------------- 1 | /* 2 | ! tailwindcss v3.3.1 | MIT License | https://tailwindcss.com 3 | */ 4 | 5 | /* 6 | 1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) 7 | 2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116) 8 | */ 9 | 10 | *, 11 | ::before, 12 | ::after { 13 | box-sizing: border-box; 14 | /* 1 */ 15 | border-width: 0; 16 | /* 2 */ 17 | border-style: solid; 18 | /* 2 */ 19 | border-color: #e5e7eb; 20 | /* 2 */ 21 | } 22 | 23 | ::before, 24 | ::after { 25 | --tw-content: ''; 26 | } 27 | 28 | /* 29 | 1. Use a consistent sensible line-height in all browsers. 30 | 2. Prevent adjustments of font size after orientation changes in iOS. 31 | 3. Use a more readable tab size. 32 | 4. Use the user's configured `sans` font-family by default. 33 | 5. Use the user's configured `sans` font-feature-settings by default. 34 | 6. Use the user's configured `sans` font-variation-settings by default. 35 | */ 36 | 37 | html { 38 | line-height: 1.5; 39 | /* 1 */ 40 | -webkit-text-size-adjust: 100%; 41 | /* 2 */ 42 | -moz-tab-size: 4; 43 | /* 3 */ 44 | -o-tab-size: 4; 45 | tab-size: 4; 46 | /* 3 */ 47 | font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; 48 | /* 4 */ 49 | font-feature-settings: normal; 50 | /* 5 */ 51 | font-variation-settings: normal; 52 | /* 6 */ 53 | } 54 | 55 | /* 56 | 1. Remove the margin in all browsers. 57 | 2. Inherit line-height from `html` so users can set them as a class directly on the `html` element. 58 | */ 59 | 60 | body { 61 | margin: 0; 62 | /* 1 */ 63 | line-height: inherit; 64 | /* 2 */ 65 | } 66 | 67 | /* 68 | 1. Add the correct height in Firefox. 69 | 2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) 70 | 3. Ensure horizontal rules are visible by default. 71 | */ 72 | 73 | hr { 74 | height: 0; 75 | /* 1 */ 76 | color: inherit; 77 | /* 2 */ 78 | border-top-width: 1px; 79 | /* 3 */ 80 | } 81 | 82 | /* 83 | Add the correct text decoration in Chrome, Edge, and Safari. 84 | */ 85 | 86 | abbr:where([title]) { 87 | -webkit-text-decoration: underline dotted; 88 | text-decoration: underline dotted; 89 | } 90 | 91 | /* 92 | Remove the default font size and weight for headings. 93 | */ 94 | 95 | h1, 96 | h2, 97 | h3, 98 | h4, 99 | h5, 100 | h6 { 101 | font-size: inherit; 102 | font-weight: inherit; 103 | } 104 | 105 | /* 106 | Reset links to optimize for opt-in styling instead of opt-out. 107 | */ 108 | 109 | a { 110 | color: inherit; 111 | text-decoration: inherit; 112 | } 113 | 114 | /* 115 | Add the correct font weight in Edge and Safari. 116 | */ 117 | 118 | b, 119 | strong { 120 | font-weight: bolder; 121 | } 122 | 123 | /* 124 | 1. Use the user's configured `mono` font family by default. 125 | 2. Correct the odd `em` font sizing in all browsers. 126 | */ 127 | 128 | code, 129 | kbd, 130 | samp, 131 | pre { 132 | font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; 133 | /* 1 */ 134 | font-size: 1em; 135 | /* 2 */ 136 | } 137 | 138 | /* 139 | Add the correct font size in all browsers. 140 | */ 141 | 142 | small { 143 | font-size: 80%; 144 | } 145 | 146 | /* 147 | Prevent `sub` and `sup` elements from affecting the line height in all browsers. 148 | */ 149 | 150 | sub, 151 | sup { 152 | font-size: 75%; 153 | line-height: 0; 154 | position: relative; 155 | vertical-align: baseline; 156 | } 157 | 158 | sub { 159 | bottom: -0.25em; 160 | } 161 | 162 | sup { 163 | top: -0.5em; 164 | } 165 | 166 | /* 167 | 1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) 168 | 2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) 169 | 3. Remove gaps between table borders by default. 170 | */ 171 | 172 | table { 173 | text-indent: 0; 174 | /* 1 */ 175 | border-color: inherit; 176 | /* 2 */ 177 | border-collapse: collapse; 178 | /* 3 */ 179 | } 180 | 181 | /* 182 | 1. Change the font styles in all browsers. 183 | 2. Remove the margin in Firefox and Safari. 184 | 3. Remove default padding in all browsers. 185 | */ 186 | 187 | button, 188 | input, 189 | optgroup, 190 | select, 191 | textarea { 192 | font-family: inherit; 193 | /* 1 */ 194 | font-size: 100%; 195 | /* 1 */ 196 | font-weight: inherit; 197 | /* 1 */ 198 | line-height: inherit; 199 | /* 1 */ 200 | color: inherit; 201 | /* 1 */ 202 | margin: 0; 203 | /* 2 */ 204 | padding: 0; 205 | /* 3 */ 206 | } 207 | 208 | /* 209 | Remove the inheritance of text transform in Edge and Firefox. 210 | */ 211 | 212 | button, 213 | select { 214 | text-transform: none; 215 | } 216 | 217 | /* 218 | 1. Correct the inability to style clickable types in iOS and Safari. 219 | 2. Remove default button styles. 220 | */ 221 | 222 | button, 223 | [type='button'], 224 | [type='reset'], 225 | [type='submit'] { 226 | -webkit-appearance: button; 227 | /* 1 */ 228 | background-color: transparent; 229 | /* 2 */ 230 | background-image: none; 231 | /* 2 */ 232 | } 233 | 234 | /* 235 | Use the modern Firefox focus style for all focusable elements. 236 | */ 237 | 238 | :-moz-focusring { 239 | outline: auto; 240 | } 241 | 242 | /* 243 | Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) 244 | */ 245 | 246 | :-moz-ui-invalid { 247 | box-shadow: none; 248 | } 249 | 250 | /* 251 | Add the correct vertical alignment in Chrome and Firefox. 252 | */ 253 | 254 | progress { 255 | vertical-align: baseline; 256 | } 257 | 258 | /* 259 | Correct the cursor style of increment and decrement buttons in Safari. 260 | */ 261 | 262 | ::-webkit-inner-spin-button, 263 | ::-webkit-outer-spin-button { 264 | height: auto; 265 | } 266 | 267 | /* 268 | 1. Correct the odd appearance in Chrome and Safari. 269 | 2. Correct the outline style in Safari. 270 | */ 271 | 272 | [type='search'] { 273 | -webkit-appearance: textfield; 274 | /* 1 */ 275 | outline-offset: -2px; 276 | /* 2 */ 277 | } 278 | 279 | /* 280 | Remove the inner padding in Chrome and Safari on macOS. 281 | */ 282 | 283 | ::-webkit-search-decoration { 284 | -webkit-appearance: none; 285 | } 286 | 287 | /* 288 | 1. Correct the inability to style clickable types in iOS and Safari. 289 | 2. Change font properties to `inherit` in Safari. 290 | */ 291 | 292 | ::-webkit-file-upload-button { 293 | -webkit-appearance: button; 294 | /* 1 */ 295 | font: inherit; 296 | /* 2 */ 297 | } 298 | 299 | /* 300 | Add the correct display in Chrome and Safari. 301 | */ 302 | 303 | summary { 304 | display: list-item; 305 | } 306 | 307 | /* 308 | Removes the default spacing and border for appropriate elements. 309 | */ 310 | 311 | blockquote, 312 | dl, 313 | dd, 314 | h1, 315 | h2, 316 | h3, 317 | h4, 318 | h5, 319 | h6, 320 | hr, 321 | figure, 322 | p, 323 | pre { 324 | margin: 0; 325 | } 326 | 327 | fieldset { 328 | margin: 0; 329 | padding: 0; 330 | } 331 | 332 | legend { 333 | padding: 0; 334 | } 335 | 336 | ol, 337 | ul, 338 | menu { 339 | list-style: none; 340 | margin: 0; 341 | padding: 0; 342 | } 343 | 344 | /* 345 | Prevent resizing textareas horizontally by default. 346 | */ 347 | 348 | textarea { 349 | resize: vertical; 350 | } 351 | 352 | /* 353 | 1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) 354 | 2. Set the default placeholder color to the user's configured gray 400 color. 355 | */ 356 | 357 | input::-moz-placeholder, textarea::-moz-placeholder { 358 | opacity: 1; 359 | /* 1 */ 360 | color: #9ca3af; 361 | /* 2 */ 362 | } 363 | 364 | input::placeholder, 365 | textarea::placeholder { 366 | opacity: 1; 367 | /* 1 */ 368 | color: #9ca3af; 369 | /* 2 */ 370 | } 371 | 372 | /* 373 | Set the default cursor for buttons. 374 | */ 375 | 376 | button, 377 | [role="button"] { 378 | cursor: pointer; 379 | } 380 | 381 | /* 382 | Make sure disabled buttons don't get the pointer cursor. 383 | */ 384 | 385 | :disabled { 386 | cursor: default; 387 | } 388 | 389 | /* 390 | 1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14) 391 | 2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) 392 | This can trigger a poorly considered lint error in some tools but is included by design. 393 | */ 394 | 395 | img, 396 | svg, 397 | video, 398 | canvas, 399 | audio, 400 | iframe, 401 | embed, 402 | object { 403 | display: block; 404 | /* 1 */ 405 | vertical-align: middle; 406 | /* 2 */ 407 | } 408 | 409 | /* 410 | Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) 411 | */ 412 | 413 | img, 414 | video { 415 | max-width: 100%; 416 | height: auto; 417 | } 418 | 419 | /* Make elements with the HTML hidden attribute stay hidden by default */ 420 | 421 | [hidden] { 422 | display: none; 423 | } 424 | 425 | *, ::before, ::after { 426 | --tw-border-spacing-x: 0; 427 | --tw-border-spacing-y: 0; 428 | --tw-translate-x: 0; 429 | --tw-translate-y: 0; 430 | --tw-rotate: 0; 431 | --tw-skew-x: 0; 432 | --tw-skew-y: 0; 433 | --tw-scale-x: 1; 434 | --tw-scale-y: 1; 435 | --tw-pan-x: ; 436 | --tw-pan-y: ; 437 | --tw-pinch-zoom: ; 438 | --tw-scroll-snap-strictness: proximity; 439 | --tw-ordinal: ; 440 | --tw-slashed-zero: ; 441 | --tw-numeric-figure: ; 442 | --tw-numeric-spacing: ; 443 | --tw-numeric-fraction: ; 444 | --tw-ring-inset: ; 445 | --tw-ring-offset-width: 0px; 446 | --tw-ring-offset-color: #fff; 447 | --tw-ring-color: rgb(59 130 246 / 0.5); 448 | --tw-ring-offset-shadow: 0 0 #0000; 449 | --tw-ring-shadow: 0 0 #0000; 450 | --tw-shadow: 0 0 #0000; 451 | --tw-shadow-colored: 0 0 #0000; 452 | --tw-blur: ; 453 | --tw-brightness: ; 454 | --tw-contrast: ; 455 | --tw-grayscale: ; 456 | --tw-hue-rotate: ; 457 | --tw-invert: ; 458 | --tw-saturate: ; 459 | --tw-sepia: ; 460 | --tw-drop-shadow: ; 461 | --tw-backdrop-blur: ; 462 | --tw-backdrop-brightness: ; 463 | --tw-backdrop-contrast: ; 464 | --tw-backdrop-grayscale: ; 465 | --tw-backdrop-hue-rotate: ; 466 | --tw-backdrop-invert: ; 467 | --tw-backdrop-opacity: ; 468 | --tw-backdrop-saturate: ; 469 | --tw-backdrop-sepia: ; 470 | } 471 | 472 | ::backdrop { 473 | --tw-border-spacing-x: 0; 474 | --tw-border-spacing-y: 0; 475 | --tw-translate-x: 0; 476 | --tw-translate-y: 0; 477 | --tw-rotate: 0; 478 | --tw-skew-x: 0; 479 | --tw-skew-y: 0; 480 | --tw-scale-x: 1; 481 | --tw-scale-y: 1; 482 | --tw-pan-x: ; 483 | --tw-pan-y: ; 484 | --tw-pinch-zoom: ; 485 | --tw-scroll-snap-strictness: proximity; 486 | --tw-ordinal: ; 487 | --tw-slashed-zero: ; 488 | --tw-numeric-figure: ; 489 | --tw-numeric-spacing: ; 490 | --tw-numeric-fraction: ; 491 | --tw-ring-inset: ; 492 | --tw-ring-offset-width: 0px; 493 | --tw-ring-offset-color: #fff; 494 | --tw-ring-color: rgb(59 130 246 / 0.5); 495 | --tw-ring-offset-shadow: 0 0 #0000; 496 | --tw-ring-shadow: 0 0 #0000; 497 | --tw-shadow: 0 0 #0000; 498 | --tw-shadow-colored: 0 0 #0000; 499 | --tw-blur: ; 500 | --tw-brightness: ; 501 | --tw-contrast: ; 502 | --tw-grayscale: ; 503 | --tw-hue-rotate: ; 504 | --tw-invert: ; 505 | --tw-saturate: ; 506 | --tw-sepia: ; 507 | --tw-drop-shadow: ; 508 | --tw-backdrop-blur: ; 509 | --tw-backdrop-brightness: ; 510 | --tw-backdrop-contrast: ; 511 | --tw-backdrop-grayscale: ; 512 | --tw-backdrop-hue-rotate: ; 513 | --tw-backdrop-invert: ; 514 | --tw-backdrop-opacity: ; 515 | --tw-backdrop-saturate: ; 516 | --tw-backdrop-sepia: ; 517 | } 518 | 519 | .container { 520 | width: 100%; 521 | } 522 | 523 | @media (min-width: 640px) { 524 | .container { 525 | max-width: 640px; 526 | } 527 | } 528 | 529 | @media (min-width: 768px) { 530 | .container { 531 | max-width: 768px; 532 | } 533 | } 534 | 535 | @media (min-width: 1024px) { 536 | .container { 537 | max-width: 1024px; 538 | } 539 | } 540 | 541 | @media (min-width: 1280px) { 542 | .container { 543 | max-width: 1280px; 544 | } 545 | } 546 | 547 | @media (min-width: 1536px) { 548 | .container { 549 | max-width: 1536px; 550 | } 551 | } 552 | 553 | .static { 554 | position: static; 555 | } 556 | 557 | .fixed { 558 | position: fixed; 559 | } 560 | 561 | .absolute { 562 | position: absolute; 563 | } 564 | 565 | .relative { 566 | position: relative; 567 | } 568 | 569 | .inset-0 { 570 | inset: 0px; 571 | } 572 | 573 | .bottom-0 { 574 | bottom: 0px; 575 | } 576 | 577 | .left-0 { 578 | left: 0px; 579 | } 580 | 581 | .right-0 { 582 | right: 0px; 583 | } 584 | 585 | .top-0 { 586 | top: 0px; 587 | } 588 | 589 | .top-1\/2 { 590 | top: 50%; 591 | } 592 | 593 | .z-50 { 594 | z-index: 50; 595 | } 596 | 597 | .m-0 { 598 | margin: 0px; 599 | } 600 | 601 | .m-2 { 602 | margin: 0.5rem; 603 | } 604 | 605 | .-mx-4 { 606 | margin-left: -1rem; 607 | margin-right: -1rem; 608 | } 609 | 610 | .mx-auto { 611 | margin-left: auto; 612 | margin-right: auto; 613 | } 614 | 615 | .mb-10 { 616 | margin-bottom: 2.5rem; 617 | } 618 | 619 | .mb-12 { 620 | margin-bottom: 3rem; 621 | } 622 | 623 | .mb-14 { 624 | margin-bottom: 3.5rem; 625 | } 626 | 627 | .mb-16 { 628 | margin-bottom: 4rem; 629 | } 630 | 631 | .mb-24 { 632 | margin-bottom: 6rem; 633 | } 634 | 635 | .mb-4 { 636 | margin-bottom: 1rem; 637 | } 638 | 639 | .mb-6 { 640 | margin-bottom: 1.5rem; 641 | } 642 | 643 | .mb-8 { 644 | margin-bottom: 2rem; 645 | } 646 | 647 | .ml-8 { 648 | margin-left: 2rem; 649 | } 650 | 651 | .mr-1 { 652 | margin-right: 0.25rem; 653 | } 654 | 655 | .mr-10 { 656 | margin-right: 2.5rem; 657 | } 658 | 659 | .mr-12 { 660 | margin-right: 3rem; 661 | } 662 | 663 | .mr-14 { 664 | margin-right: 3.5rem; 665 | } 666 | 667 | .mr-16 { 668 | margin-right: 4rem; 669 | } 670 | 671 | .mr-2 { 672 | margin-right: 0.5rem; 673 | } 674 | 675 | .mr-3 { 676 | margin-right: 0.75rem; 677 | } 678 | 679 | .mr-6 { 680 | margin-right: 1.5rem; 681 | } 682 | 683 | .mr-8 { 684 | margin-right: 2rem; 685 | } 686 | 687 | .mr-auto { 688 | margin-right: auto; 689 | } 690 | 691 | .mt-2 { 692 | margin-top: 0.5rem; 693 | } 694 | 695 | .block { 696 | display: block; 697 | } 698 | 699 | .inline-block { 700 | display: inline-block; 701 | } 702 | 703 | .flex { 704 | display: flex; 705 | } 706 | 707 | .inline-flex { 708 | display: inline-flex; 709 | } 710 | 711 | .hidden { 712 | display: none; 713 | } 714 | 715 | .h-2 { 716 | height: 0.5rem; 717 | } 718 | 719 | .h-40 { 720 | height: 10rem; 721 | } 722 | 723 | .h-6 { 724 | height: 1.5rem; 725 | } 726 | 727 | .h-8 { 728 | height: 2rem; 729 | } 730 | 731 | .h-9 { 732 | height: 2.25rem; 733 | } 734 | 735 | .h-full { 736 | height: 100%; 737 | } 738 | 739 | .w-1\/2 { 740 | width: 50%; 741 | } 742 | 743 | .w-1\/4 { 744 | width: 25%; 745 | } 746 | 747 | .w-1\/6 { 748 | width: 16.666667%; 749 | } 750 | 751 | .w-12 { 752 | width: 3rem; 753 | } 754 | 755 | .w-2 { 756 | width: 0.5rem; 757 | } 758 | 759 | .w-5\/6 { 760 | width: 83.333333%; 761 | } 762 | 763 | .w-6 { 764 | width: 1.5rem; 765 | } 766 | 767 | .w-8 { 768 | width: 2rem; 769 | } 770 | 771 | .w-full { 772 | width: 100%; 773 | } 774 | 775 | .max-w-2xl { 776 | max-width: 42rem; 777 | } 778 | 779 | .max-w-md { 780 | max-width: 28rem; 781 | } 782 | 783 | .max-w-sm { 784 | max-width: 24rem; 785 | } 786 | 787 | .max-w-xl { 788 | max-width: 36rem; 789 | } 790 | 791 | .flex-shrink-0 { 792 | flex-shrink: 0; 793 | } 794 | 795 | .transform { 796 | transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); 797 | } 798 | 799 | .cursor-pointer { 800 | cursor: pointer; 801 | } 802 | 803 | .flex-row { 804 | flex-direction: row; 805 | } 806 | 807 | .flex-col { 808 | flex-direction: column; 809 | } 810 | 811 | .flex-wrap { 812 | flex-wrap: wrap; 813 | } 814 | 815 | .place-items-center { 816 | place-items: center; 817 | } 818 | 819 | .items-center { 820 | align-items: center; 821 | } 822 | 823 | .justify-between { 824 | justify-content: space-between; 825 | } 826 | 827 | .self-center { 828 | align-self: center; 829 | } 830 | 831 | .overflow-y-auto { 832 | overflow-y: auto; 833 | } 834 | 835 | .text-ellipsis { 836 | text-overflow: ellipsis; 837 | } 838 | 839 | .rounded { 840 | border-radius: 0.25rem; 841 | } 842 | 843 | .rounded-full { 844 | border-radius: 9999px; 845 | } 846 | 847 | .rounded-lg { 848 | border-radius: 0.5rem; 849 | } 850 | 851 | .rounded-md { 852 | border-radius: 0.375rem; 853 | } 854 | 855 | .border { 856 | border-width: 1px; 857 | } 858 | 859 | .border-0 { 860 | border-width: 0px; 861 | } 862 | 863 | .border-b { 864 | border-bottom-width: 1px; 865 | } 866 | 867 | .border-b-2 { 868 | border-bottom-width: 2px; 869 | } 870 | 871 | .border-l { 872 | border-left-width: 1px; 873 | } 874 | 875 | .border-r { 876 | border-right-width: 1px; 877 | } 878 | 879 | .border-gray-200 { 880 | --tw-border-opacity: 1; 881 | border-color: rgb(229 231 235 / var(--tw-border-opacity)); 882 | } 883 | 884 | .border-transparent { 885 | border-color: transparent; 886 | } 887 | 888 | .bg-gray-100 { 889 | --tw-bg-opacity: 1; 890 | background-color: rgb(243 244 246 / var(--tw-bg-opacity)); 891 | } 892 | 893 | .bg-gray-50 { 894 | --tw-bg-opacity: 1; 895 | background-color: rgb(249 250 251 / var(--tw-bg-opacity)); 896 | } 897 | 898 | .bg-gray-800 { 899 | --tw-bg-opacity: 1; 900 | background-color: rgb(31 41 55 / var(--tw-bg-opacity)); 901 | } 902 | 903 | .bg-orange-300 { 904 | --tw-bg-opacity: 1; 905 | background-color: rgb(253 186 116 / var(--tw-bg-opacity)); 906 | } 907 | 908 | .bg-white { 909 | --tw-bg-opacity: 1; 910 | background-color: rgb(255 255 255 / var(--tw-bg-opacity)); 911 | } 912 | 913 | .object-cover { 914 | -o-object-fit: cover; 915 | object-fit: cover; 916 | } 917 | 918 | .object-scale-down { 919 | -o-object-fit: scale-down; 920 | object-fit: scale-down; 921 | } 922 | 923 | .p-10 { 924 | padding: 2.5rem; 925 | } 926 | 927 | .p-2 { 928 | padding: 0.5rem; 929 | } 930 | 931 | .px-10 { 932 | padding-left: 2.5rem; 933 | padding-right: 2.5rem; 934 | } 935 | 936 | .px-12 { 937 | padding-left: 3rem; 938 | padding-right: 3rem; 939 | } 940 | 941 | .px-2 { 942 | padding-left: 0.5rem; 943 | padding-right: 0.5rem; 944 | } 945 | 946 | .px-4 { 947 | padding-left: 1rem; 948 | padding-right: 1rem; 949 | } 950 | 951 | .px-6 { 952 | padding-left: 1.5rem; 953 | padding-right: 1.5rem; 954 | } 955 | 956 | .px-8 { 957 | padding-left: 2rem; 958 | padding-right: 2rem; 959 | } 960 | 961 | .py-2 { 962 | padding-top: 0.5rem; 963 | padding-bottom: 0.5rem; 964 | } 965 | 966 | .py-20 { 967 | padding-top: 5rem; 968 | padding-bottom: 5rem; 969 | } 970 | 971 | .py-4 { 972 | padding-top: 1rem; 973 | padding-bottom: 1rem; 974 | } 975 | 976 | .py-5 { 977 | padding-top: 1.25rem; 978 | padding-bottom: 1.25rem; 979 | } 980 | 981 | .py-6 { 982 | padding-top: 1.5rem; 983 | padding-bottom: 1.5rem; 984 | } 985 | 986 | .py-8 { 987 | padding-top: 2rem; 988 | padding-bottom: 2rem; 989 | } 990 | 991 | .pb-10 { 992 | padding-bottom: 2.5rem; 993 | } 994 | 995 | .pl-4 { 996 | padding-left: 1rem; 997 | } 998 | 999 | .pl-6 { 1000 | padding-left: 1.5rem; 1001 | } 1002 | 1003 | .pr-10 { 1004 | padding-right: 2.5rem; 1005 | } 1006 | 1007 | .text-left { 1008 | text-align: left; 1009 | } 1010 | 1011 | .text-center { 1012 | text-align: center; 1013 | } 1014 | 1015 | .text-2xl { 1016 | font-size: 1.5rem; 1017 | line-height: 2rem; 1018 | } 1019 | 1020 | .text-3xl { 1021 | font-size: 1.875rem; 1022 | line-height: 2.25rem; 1023 | } 1024 | 1025 | .text-5xl { 1026 | font-size: 3rem; 1027 | line-height: 1; 1028 | } 1029 | 1030 | .text-xs { 1031 | font-size: 0.75rem; 1032 | line-height: 1rem; 1033 | } 1034 | 1035 | .font-bold { 1036 | font-weight: 700; 1037 | } 1038 | 1039 | .font-semibold { 1040 | font-weight: 600; 1041 | } 1042 | 1043 | .uppercase { 1044 | text-transform: uppercase; 1045 | } 1046 | 1047 | .text-blue-300 { 1048 | --tw-text-opacity: 1; 1049 | color: rgb(147 197 253 / var(--tw-text-opacity)); 1050 | } 1051 | 1052 | .text-gray-400 { 1053 | --tw-text-opacity: 1; 1054 | color: rgb(156 163 175 / var(--tw-text-opacity)); 1055 | } 1056 | 1057 | .text-gray-500 { 1058 | --tw-text-opacity: 1; 1059 | color: rgb(107 114 128 / var(--tw-text-opacity)); 1060 | } 1061 | 1062 | .text-gray-600 { 1063 | --tw-text-opacity: 1; 1064 | color: rgb(75 85 99 / var(--tw-text-opacity)); 1065 | } 1066 | 1067 | .text-white { 1068 | --tw-text-opacity: 1; 1069 | color: rgb(255 255 255 / var(--tw-text-opacity)); 1070 | } 1071 | 1072 | .placeholder-gray-400::-moz-placeholder { 1073 | --tw-placeholder-opacity: 1; 1074 | color: rgb(156 163 175 / var(--tw-placeholder-opacity)); 1075 | } 1076 | 1077 | .placeholder-gray-400::placeholder { 1078 | --tw-placeholder-opacity: 1; 1079 | color: rgb(156 163 175 / var(--tw-placeholder-opacity)); 1080 | } 1081 | 1082 | .opacity-25 { 1083 | opacity: 0.25; 1084 | } 1085 | 1086 | .shadow-2xl { 1087 | --tw-shadow: 0 25px 50px -12px rgb(0 0 0 / 0.25); 1088 | --tw-shadow-colored: 0 25px 50px -12px var(--tw-shadow-color); 1089 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); 1090 | } 1091 | 1092 | .shadow-lg { 1093 | --tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); 1094 | --tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color); 1095 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); 1096 | } 1097 | 1098 | .ring-1 { 1099 | --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); 1100 | --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color); 1101 | box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); 1102 | } 1103 | 1104 | .transition { 1105 | transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter; 1106 | transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter; 1107 | transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-backdrop-filter; 1108 | transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); 1109 | transition-duration: 150ms; 1110 | } 1111 | 1112 | .transition-all { 1113 | transition-property: all; 1114 | transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); 1115 | transition-duration: 150ms; 1116 | } 1117 | 1118 | .duration-200 { 1119 | transition-duration: 200ms; 1120 | } 1121 | 1122 | .hover\:bg-orange-400:hover { 1123 | --tw-bg-opacity: 1; 1124 | background-color: rgb(251 146 60 / var(--tw-bg-opacity)); 1125 | } 1126 | 1127 | .hover\:text-gray-600:hover { 1128 | --tw-text-opacity: 1; 1129 | color: rgb(75 85 99 / var(--tw-text-opacity)); 1130 | } 1131 | 1132 | .hover\:text-gray-700:hover { 1133 | --tw-text-opacity: 1; 1134 | color: rgb(55 65 81 / var(--tw-text-opacity)); 1135 | } 1136 | 1137 | .hover\:shadow-2xl:hover { 1138 | --tw-shadow: 0 25px 50px -12px rgb(0 0 0 / 0.25); 1139 | --tw-shadow-colored: 0 25px 50px -12px var(--tw-shadow-color); 1140 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); 1141 | } 1142 | 1143 | .hover\:ring-4:hover { 1144 | --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); 1145 | --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color); 1146 | box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); 1147 | } 1148 | 1149 | .focus\:border-blue-300:focus { 1150 | --tw-border-opacity: 1; 1151 | border-color: rgb(147 197 253 / var(--tw-border-opacity)); 1152 | } 1153 | 1154 | .focus\:outline-none:focus { 1155 | outline: 2px solid transparent; 1156 | outline-offset: 2px; 1157 | } 1158 | 1159 | .focus\:ring-blue-300:focus { 1160 | --tw-ring-opacity: 1; 1161 | --tw-ring-color: rgb(147 197 253 / var(--tw-ring-opacity)); 1162 | } 1163 | 1164 | .focus\:ring-transparent:focus { 1165 | --tw-ring-color: transparent; 1166 | } 1167 | 1168 | @media (min-width: 768px) { 1169 | .md\:mb-0 { 1170 | margin-bottom: 0px; 1171 | } 1172 | 1173 | .md\:w-1\/2 { 1174 | width: 50%; 1175 | } 1176 | 1177 | .md\:w-auto { 1178 | width: auto; 1179 | } 1180 | 1181 | .md\:text-right { 1182 | text-align: right; 1183 | } 1184 | 1185 | .md\:text-6xl { 1186 | font-size: 3.75rem; 1187 | line-height: 1; 1188 | } 1189 | } 1190 | 1191 | @media (min-width: 1024px) { 1192 | .lg\:pl-20 { 1193 | padding-left: 5rem; 1194 | } 1195 | } 1196 | 1197 | @media (min-width: 1280px) { 1198 | .xl\:mx-auto { 1199 | margin-left: auto; 1200 | margin-right: auto; 1201 | } 1202 | 1203 | .xl\:mb-0 { 1204 | margin-bottom: 0px; 1205 | } 1206 | 1207 | .xl\:block { 1208 | display: block; 1209 | } 1210 | 1211 | .xl\:inline-block { 1212 | display: inline-block; 1213 | } 1214 | 1215 | .xl\:flex { 1216 | display: flex; 1217 | } 1218 | 1219 | .xl\:hidden { 1220 | display: none; 1221 | } 1222 | 1223 | .xl\:w-2\/3 { 1224 | width: 66.666667%; 1225 | } 1226 | } -------------------------------------------------------------------------------- /ecommerce-site/src/api.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused)] 2 | 3 | use std::fmt::Display; 4 | 5 | use chrono::{DateTime, Utc}; 6 | use serde::{Deserialize, Serialize}; 7 | 8 | #[derive(Deserialize, PartialEq, Clone, Debug, Default)] 9 | pub(crate) struct Product { 10 | pub(crate) id: u32, 11 | pub(crate) title: String, 12 | pub(crate) price: f32, 13 | pub(crate) description: String, 14 | pub(crate) category: String, 15 | pub(crate) image: String, 16 | pub(crate) rating: Rating, 17 | } 18 | 19 | #[derive(Deserialize, PartialEq, Clone, Debug, Default)] 20 | pub(crate) struct Rating { 21 | pub(crate) rate: f32, 22 | pub(crate) count: u32, 23 | } 24 | 25 | impl Display for Rating { 26 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 27 | let rounded = self.rate.round() as usize; 28 | for _ in 0..rounded { 29 | "★".fmt(f)?; 30 | } 31 | for _ in 0..(5 - rounded) { 32 | "☆".fmt(f)?; 33 | } 34 | 35 | write!(f, " ({:01}) ({} ratings)", self.rate, self.count)?; 36 | 37 | Ok(()) 38 | } 39 | } 40 | 41 | #[allow(unused)] 42 | #[derive(Clone, Copy, Hash, PartialEq, Eq, PartialOrd)] 43 | pub(crate) enum Sort { 44 | Descending, 45 | Ascending, 46 | } 47 | 48 | impl Display for Sort { 49 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 50 | match self { 51 | Sort::Descending => write!(f, "desc"), 52 | Sort::Ascending => write!(f, "asc"), 53 | } 54 | } 55 | } 56 | 57 | // Cache up to 100 requests, invalidating them after 60 seconds 58 | #[cached::proc_macro::cached(size = 100, time = 60, result = true)] 59 | pub(crate) async fn fetch_user_carts(user_id: usize) -> Result, reqwest::Error> { 60 | reqwest::get(format!( 61 | "https://fakestoreapi.com/carts/user/{user_id}?startdate=2019-12-10&enddate=2023-01-01" 62 | )) 63 | .await? 64 | .json() 65 | .await 66 | } 67 | 68 | // Cache up to 100 requests, invalidating them after 60 seconds 69 | #[cached::proc_macro::cached(size = 100, time = 60, result = true)] 70 | pub(crate) async fn fetch_user(user_id: usize) -> Result { 71 | reqwest::get(format!("https://fakestoreapi.com/users/{user_id}")) 72 | .await? 73 | .json() 74 | .await 75 | } 76 | 77 | // Cache up to 100 requests, invalidating them after 60 seconds 78 | #[cached::proc_macro::cached(size = 100, time = 60, result = true)] 79 | pub(crate) async fn fetch_product(product_id: usize) -> Result { 80 | reqwest::get(format!("https://fakestoreapi.com/products/{product_id}")) 81 | .await? 82 | .json() 83 | .await 84 | } 85 | 86 | // Cache up to 100 requests, invalidating them after 60 seconds 87 | #[cached::proc_macro::cached(size = 100, time = 60, result = true)] 88 | pub(crate) async fn fetch_products( 89 | count: usize, 90 | sort: Sort, 91 | ) -> Result, reqwest::Error> { 92 | reqwest::get(format!( 93 | "https://fakestoreapi.com/products/?sort={sort}&limit={count}" 94 | )) 95 | .await? 96 | .json() 97 | .await 98 | } 99 | 100 | #[derive(Serialize, Deserialize)] 101 | pub(crate) struct User { 102 | id: usize, 103 | email: String, 104 | username: String, 105 | password: String, 106 | name: FullName, 107 | phone: String, 108 | } 109 | 110 | impl User { 111 | async fn fetch_most_recent_cart(&self) -> Result, reqwest::Error> { 112 | let all_carts = fetch_user_carts(self.id).await?; 113 | 114 | Ok(all_carts.into_iter().max_by_key(|cart| cart.date)) 115 | } 116 | } 117 | 118 | #[derive(Serialize, Deserialize)] 119 | struct FullName { 120 | firstname: String, 121 | lastname: String, 122 | } 123 | 124 | #[derive(Serialize, Deserialize, Clone)] 125 | pub(crate) struct Cart { 126 | id: usize, 127 | #[serde(rename = "userId")] 128 | user_id: usize, 129 | data: String, 130 | products: Vec, 131 | date: DateTime, 132 | } 133 | 134 | impl Cart { 135 | async fn update_database(&mut self) -> Result<(), reqwest::Error> { 136 | let id = self.id; 137 | let client = reqwest::Client::new(); 138 | *self = client 139 | .put(format!("https://fakestoreapi.com/carts/{id}")) 140 | .send() 141 | .await? 142 | .json() 143 | .await?; 144 | Ok(()) 145 | } 146 | } 147 | 148 | #[derive(Serialize, Deserialize, Clone)] 149 | pub(crate) struct ProductInCart { 150 | #[serde(rename = "productId")] 151 | product_id: usize, 152 | quantity: usize, 153 | } 154 | 155 | impl ProductInCart { 156 | pub async fn fetch_product(&self) -> Result { 157 | fetch_product(self.product_id).await 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /ecommerce-site/src/components/cart.rs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DioxusLabs/example-projects/db87199d2fcdbdd70cf5b6bed5b156ac35fb1e91/ecommerce-site/src/components/cart.rs -------------------------------------------------------------------------------- /ecommerce-site/src/components/error.rs: -------------------------------------------------------------------------------- 1 | use dioxus::prelude::*; 2 | 3 | #[inline_props] 4 | pub fn error_page(cx: Scope) -> Element { 5 | cx.render(rsx!( 6 | section { class: "py-20", 7 | div { class: "container mx-auto px-4", 8 | div { class: "flex flex-wrap -mx-4 mb-24 text-center", 9 | "An internal error has occurred" 10 | } 11 | } 12 | } 13 | )) 14 | } 15 | -------------------------------------------------------------------------------- /ecommerce-site/src/components/home.rs: -------------------------------------------------------------------------------- 1 | // The homepage is statically rendered, so we don't need to a persistent websocket connection. 2 | 3 | use crate::{ 4 | api::{fetch_products, Sort}, 5 | block_on, 6 | components::nav, 7 | components::product_item::product_item, 8 | }; 9 | use dioxus::prelude::*; 10 | 11 | pub(crate) fn Home(cx: Scope) -> Element { 12 | let products = cx.use_hook(|| block_on(fetch_products(10, Sort::Ascending))); 13 | 14 | cx.render(rsx!( 15 | head { 16 | link { 17 | rel: "stylesheet", 18 | href: "/public/tailwind.css" 19 | } 20 | } 21 | body { 22 | nav::nav {} 23 | section { class: "p-10", 24 | products.iter().flatten().map(|product| rsx!{ 25 | product_item { 26 | product: product.clone() 27 | } 28 | }) 29 | } 30 | } 31 | )) 32 | } 33 | -------------------------------------------------------------------------------- /ecommerce-site/src/components/nav.rs: -------------------------------------------------------------------------------- 1 | use dioxus::prelude::*; 2 | 3 | pub fn nav(cx: Scope) -> Element { 4 | cx.render(rsx!( 5 | section { class: "relative", 6 | nav { class: "flex justify-between border-b", 7 | div { class: "px-12 py-8 flex w-full items-center", 8 | a { class: "hidden xl:block mr-16", 9 | href: "/", 10 | icons::cart_icon {} 11 | } 12 | ul { class: "hidden xl:flex font-semibold font-heading", 13 | li { class: "mr-12", 14 | a { class: "hover:text-gray-600", 15 | href: "/", 16 | "Category" 17 | } 18 | } 19 | li { class: "mr-12", 20 | a { class: "hover:text-gray-600", 21 | href: "/", 22 | "Collection" 23 | } 24 | } 25 | li { class: "mr-12", 26 | a { class: "hover:text-gray-600", 27 | href: "/", 28 | "Story" 29 | } 30 | } 31 | li { 32 | a { class: "hover:text-gray-600", 33 | href: "/", 34 | "Brand" 35 | } 36 | } 37 | } 38 | a { class: "flex-shrink-0 xl:mx-auto text-3xl font-bold font-heading", 39 | href: "/", 40 | img { class: "h-9", 41 | width: "auto", 42 | alt: "", 43 | src: "https://shuffle.dev/yofte-assets/logos/yofte-logo.svg", 44 | } 45 | } 46 | div { class: "hidden xl:inline-block mr-14", 47 | input { class: "py-5 px-8 w-full placeholder-gray-400 text-xs uppercase font-semibold font-heading bg-gray-50 border border-gray-200 focus:ring-blue-300 focus:border-blue-300 rounded-md", 48 | placeholder: "Search", 49 | r#type: "text", 50 | } 51 | } 52 | div { class: "hidden xl:flex items-center", 53 | a { class: "mr-10 hover:text-gray-600", 54 | href: "", 55 | icons::icon_1 {} 56 | } 57 | a { class: "flex items-center hover:text-gray-600", 58 | href: "/", 59 | icons::icon_2 {} 60 | span { class: "inline-block w-6 h-6 text-center bg-gray-50 rounded-full font-semibold font-heading", 61 | "3" 62 | } 63 | } 64 | } 65 | } 66 | a { class: "hidden xl:flex items-center px-12 border-l font-semibold font-heading hover:text-gray-600", 67 | href: "/", 68 | icons::icon_3 {} 69 | span { 70 | "Sign In" 71 | } 72 | } 73 | a { class: "xl:hidden flex mr-6 items-center text-gray-600", 74 | href: "/", 75 | icons::icon_4 {} 76 | span { class: "inline-block w-6 h-6 text-center bg-gray-50 rounded-full font-semibold font-heading", 77 | "3" 78 | } 79 | } 80 | a { class: "navbar-burger self-center mr-12 xl:hidden", 81 | href: "/", 82 | icons::icon_5 {} 83 | } 84 | } 85 | div { class: "hidden navbar-menu fixed top-0 left-0 bottom-0 w-5/6 max-w-sm z-50", 86 | div { class: "navbar-backdrop fixed inset-0 bg-gray-800 opacity-25", 87 | } 88 | nav { class: "relative flex flex-col py-6 px-6 w-full h-full bg-white border-r overflow-y-auto", 89 | div { class: "flex items-center mb-8", 90 | a { class: "mr-auto text-3xl font-bold font-heading", 91 | href: "/", 92 | img { class: "h-9", 93 | src: "https://shuffle.dev/yofte-assets/logos/yofte-logo.svg", 94 | width: "auto", 95 | alt: "", 96 | } 97 | } 98 | button { class: "navbar-close", 99 | icons::icon_6 {} 100 | } 101 | } 102 | div { class: "flex mb-8 justify-between", 103 | a { class: "inline-flex items-center font-semibold font-heading", 104 | href: "/", 105 | icons::icon_7 {} 106 | span { 107 | "Sign In" 108 | } 109 | } 110 | div { class: "flex items-center", 111 | a { class: "mr-10", 112 | href: "/", 113 | icons::icon_8 {} 114 | } 115 | a { class: "flex items-center", 116 | href: "/", 117 | icons::icon_9 {} 118 | span { class: "inline-block w-6 h-6 text-center bg-gray-100 rounded-full font-semibold font-heading", 119 | "3" 120 | } 121 | } 122 | } 123 | } 124 | input { class: "block mb-10 py-5 px-8 bg-gray-100 rounded-md border-transparent focus:ring-blue-300 focus:border-blue-300 focus:outline-none", 125 | r#type: "search", 126 | placeholder: "Search", 127 | } 128 | ul { class: "text-3xl font-bold font-heading", 129 | li { class: "mb-8", 130 | a { 131 | href: "/", 132 | "Category" 133 | } 134 | } 135 | li { class: "mb-8", 136 | a { 137 | href: "/", 138 | "Collection" 139 | } 140 | } 141 | li { class: "mb-8", 142 | a { 143 | href: "/", 144 | "Story" 145 | } 146 | } 147 | li { 148 | a { 149 | href: "/", 150 | "Brand" 151 | } 152 | } 153 | } 154 | } 155 | } 156 | } 157 | )) 158 | } 159 | 160 | mod icons { 161 | use super::*; 162 | 163 | pub(super) fn cart_icon(cx: Scope) -> Element { 164 | cx.render(rsx!( 165 | svg { class: "mr-3", 166 | fill: "none", 167 | xmlns: "http://www.w3.org/2000/svg", 168 | view_box: "0 0 23 23", 169 | width: "23", 170 | height: "23", 171 | path { 172 | stroke_linejoin: "round", 173 | d: "M18.1159 8.72461H2.50427C1.99709 8.72461 1.58594 9.12704 1.58594 9.62346V21.3085C1.58594 21.8049 1.99709 22.2074 2.50427 22.2074H18.1159C18.6231 22.2074 19.0342 21.8049 19.0342 21.3085V9.62346C19.0342 9.12704 18.6231 8.72461 18.1159 8.72461Z", 174 | stroke: "currentColor", 175 | stroke_linecap: "round", 176 | stroke_width: "1.5", 177 | } 178 | path { 179 | stroke: "currentColor", 180 | stroke_linecap: "round", 181 | d: "M6.34473 6.34469V4.95676C6.34473 3.85246 6.76252 2.79338 7.5062 2.01252C8.24988 1.23165 9.25852 0.792969 10.3102 0.792969C11.362 0.792969 12.3706 1.23165 13.1143 2.01252C13.858 2.79338 14.2758 3.85246 14.2758 4.95676V6.34469", 182 | stroke_width: "1.5", 183 | stroke_linejoin: "round", 184 | } 185 | } 186 | )) 187 | } 188 | 189 | pub(super) fn icon_1(cx: Scope) -> Element { 190 | cx.render(rsx!( 191 | svg { 192 | xmlns: "http://www.w3.org/2000/svg", 193 | height: "20", 194 | view_box: "0 0 23 20", 195 | width: "23", 196 | fill: "none", 197 | path { 198 | d: "M11.4998 19.2061L2.70115 9.92527C1.92859 9.14433 1.41864 8.1374 1.24355 7.04712C1.06847 5.95684 1.23713 4.8385 1.72563 3.85053V3.85053C2.09464 3.10462 2.63366 2.45803 3.29828 1.96406C3.9629 1.47008 4.73408 1.14284 5.5483 1.00931C6.36252 0.875782 7.19647 0.939779 7.98144 1.19603C8.7664 1.45228 9.47991 1.89345 10.0632 2.48319L11.4998 3.93577L12.9364 2.48319C13.5197 1.89345 14.2332 1.45228 15.0182 1.19603C15.8031 0.939779 16.6371 0.875782 17.4513 1.00931C18.2655 1.14284 19.0367 1.47008 19.7013 1.96406C20.3659 2.45803 20.905 3.10462 21.274 3.85053V3.85053C21.7625 4.8385 21.9311 5.95684 21.756 7.04712C21.581 8.1374 21.071 9.14433 20.2984 9.92527L11.4998 19.2061Z", 199 | stroke: "currentColor", 200 | stroke_width: "1.5", 201 | stroke_linejoin: "round", 202 | stroke_linecap: "round", 203 | } 204 | } 205 | )) 206 | } 207 | 208 | pub(super) fn icon_2(cx: Scope) -> Element { 209 | cx.render(rsx!( 210 | svg { class: "mr-3", 211 | fill: "none", 212 | height: "31", 213 | xmlns: "http://www.w3.org/2000/svg", 214 | width: "32", 215 | view_box: "0 0 32 31", 216 | path { 217 | stroke_linejoin: "round", 218 | stroke_width: "1.5", 219 | d: "M16.0006 16.3154C19.1303 16.3154 21.6673 13.799 21.6673 10.6948C21.6673 7.59064 19.1303 5.07422 16.0006 5.07422C12.871 5.07422 10.334 7.59064 10.334 10.6948C10.334 13.799 12.871 16.3154 16.0006 16.3154Z", 220 | stroke_linecap: "round", 221 | stroke: "currentColor", 222 | } 223 | path { 224 | stroke_width: "1.5", 225 | d: "M24.4225 23.8963C23.6678 22.3507 22.4756 21.0445 20.9845 20.1298C19.4934 19.2151 17.7647 18.7295 15.9998 18.7295C14.2349 18.7295 12.5063 19.2151 11.0152 20.1298C9.52406 21.0445 8.33179 22.3507 7.57715 23.8963", 226 | stroke: "currentColor", 227 | stroke_linecap: "round", 228 | stroke_linejoin: "round", 229 | } 230 | } 231 | )) 232 | } 233 | 234 | pub(super) fn icon_3(cx: Scope) -> Element { 235 | cx.render(rsx!( 236 | svg { class: "h-2 w-2 text-gray-500 cursor-pointer", 237 | height: "10", 238 | width: "10", 239 | xmlns: "http://www.w3.org/2000/svg", 240 | fill: "none", 241 | view_box: "0 0 10 10", 242 | path { 243 | stroke_width: "1.5", 244 | stroke_linejoin: "round", 245 | d: "M9.00002 1L1 9.00002M1.00003 1L9.00005 9.00002", 246 | stroke: "black", 247 | stroke_linecap: "round", 248 | } 249 | } 250 | )) 251 | } 252 | 253 | pub(super) fn icon_4(cx: Scope) -> Element { 254 | cx.render(rsx!( 255 | svg { 256 | view_box: "0 0 20 12", 257 | fill: "none", 258 | width: "20", 259 | xmlns: "http://www.w3.org/2000/svg", 260 | height: "12", 261 | path { 262 | d: "M1 2H19C19.2652 2 19.5196 1.89464 19.7071 1.70711C19.8946 1.51957 20 1.26522 20 1C20 0.734784 19.8946 0.48043 19.7071 0.292893C19.5196 0.105357 19.2652 0 19 0H1C0.734784 0 0.48043 0.105357 0.292893 0.292893C0.105357 0.48043 0 0.734784 0 1C0 1.26522 0.105357 1.51957 0.292893 1.70711C0.48043 1.89464 0.734784 2 1 2ZM19 10H1C0.734784 10 0.48043 10.1054 0.292893 10.2929C0.105357 10.4804 0 10.7348 0 11C0 11.2652 0.105357 11.5196 0.292893 11.7071C0.48043 11.8946 0.734784 12 1 12H19C19.2652 12 19.5196 11.8946 19.7071 11.7071C19.8946 11.5196 20 11.2652 20 11C20 10.7348 19.8946 10.4804 19.7071 10.2929C19.5196 10.1054 19.2652 10 19 10ZM19 5H1C0.734784 5 0.48043 5.10536 0.292893 5.29289C0.105357 5.48043 0 5.73478 0 6C0 6.26522 0.105357 6.51957 0.292893 6.70711C0.48043 6.89464 0.734784 7 1 7H19C19.2652 7 19.5196 6.89464 19.7071 6.70711C19.8946 6.51957 20 6.26522 20 6C20 5.73478 19.8946 5.48043 19.7071 5.29289C19.5196 5.10536 19.2652 5 19 5Z", 263 | fill: "#8594A5", 264 | } 265 | } 266 | )) 267 | } 268 | 269 | pub(super) fn icon_5(cx: Scope) -> Element { 270 | cx.render(rsx!( 271 | svg { class: "mr-2", 272 | fill: "none", 273 | xmlns: "http://www.w3.org/2000/svg", 274 | width: "23", 275 | height: "23", 276 | view_box: "0 0 23 23", 277 | path { 278 | stroke_width: "1.5", 279 | stroke_linecap: "round", 280 | stroke_linejoin: "round", 281 | d: "M18.1159 8.72461H2.50427C1.99709 8.72461 1.58594 9.12704 1.58594 9.62346V21.3085C1.58594 21.8049 1.99709 22.2074 2.50427 22.2074H18.1159C18.6231 22.2074 19.0342 21.8049 19.0342 21.3085V9.62346C19.0342 9.12704 18.6231 8.72461 18.1159 8.72461Z", 282 | stroke: "currentColor", 283 | } 284 | path { 285 | d: "M6.34473 6.34469V4.95676C6.34473 3.85246 6.76252 2.79338 7.5062 2.01252C8.24988 1.23165 9.25852 0.792969 10.3102 0.792969C11.362 0.792969 12.3706 1.23165 13.1143 2.01252C13.858 2.79338 14.2758 3.85246 14.2758 4.95676V6.34469", 286 | stroke_linejoin: "round", 287 | stroke_width: "1.5", 288 | stroke_linecap: "round", 289 | stroke: "currentColor", 290 | } 291 | } 292 | )) 293 | } 294 | 295 | pub(super) fn icon_6(cx: Scope) -> Element { 296 | cx.render(rsx!( 297 | svg { class: "mr-3", 298 | height: "31", 299 | xmlns: "http://www.w3.org/2000/svg", 300 | view_box: "0 0 32 31", 301 | width: "32", 302 | fill: "none", 303 | path { 304 | stroke: "currentColor", 305 | stroke_width: "1.5", 306 | d: "M16.0006 16.3154C19.1303 16.3154 21.6673 13.799 21.6673 10.6948C21.6673 7.59064 19.1303 5.07422 16.0006 5.07422C12.871 5.07422 10.334 7.59064 10.334 10.6948C10.334 13.799 12.871 16.3154 16.0006 16.3154Z", 307 | stroke_linecap: "round", 308 | stroke_linejoin: "round", 309 | } 310 | path { 311 | stroke_linecap: "round", 312 | stroke_width: "1.5", 313 | stroke: "currentColor", 314 | stroke_linejoin: "round", 315 | d: "M24.4225 23.8963C23.6678 22.3507 22.4756 21.0445 20.9845 20.1298C19.4934 19.2151 17.7647 18.7295 15.9998 18.7295C14.2349 18.7295 12.5063 19.2151 11.0152 20.1298C9.52406 21.0445 8.33179 22.3507 7.57715 23.8963", 316 | } 317 | } 318 | )) 319 | } 320 | 321 | pub(super) fn icon_7(cx: Scope) -> Element { 322 | cx.render(rsx!( 323 | svg { class: "mr-3", 324 | view_box: "0 0 23 23", 325 | fill: "none", 326 | height: "23", 327 | width: "23", 328 | xmlns: "http://www.w3.org/2000/svg", 329 | path { 330 | stroke_linecap: "round", 331 | stroke: "currentColor", 332 | stroke_width: "1.5", 333 | stroke_linejoin: "round", 334 | d: "M18.1159 8.72461H2.50427C1.99709 8.72461 1.58594 9.12704 1.58594 9.62346V21.3085C1.58594 21.8049 1.99709 22.2074 2.50427 22.2074H18.1159C18.6231 22.2074 19.0342 21.8049 19.0342 21.3085V9.62346C19.0342 9.12704 18.6231 8.72461 18.1159 8.72461Z", 335 | } 336 | path { 337 | d: "M6.34473 6.34469V4.95676C6.34473 3.85246 6.76252 2.79338 7.5062 2.01252C8.24988 1.23165 9.25852 0.792969 10.3102 0.792969C11.362 0.792969 12.3706 1.23165 13.1143 2.01252C13.858 2.79338 14.2758 3.85246 14.2758 4.95676V6.34469", 338 | stroke_width: "1.5", 339 | stroke_linecap: "round", 340 | stroke: "currentColor", 341 | stroke_linejoin: "round", 342 | } 343 | } 344 | )) 345 | } 346 | 347 | pub(super) fn icon_8(cx: Scope) -> Element { 348 | cx.render(rsx!( 349 | svg { 350 | height: "20", 351 | width: "23", 352 | fill: "none", 353 | view_box: "0 0 23 20", 354 | xmlns: "http://www.w3.org/2000/svg", 355 | path { 356 | d: "M11.4998 19.2061L2.70115 9.92527C1.92859 9.14433 1.41864 8.1374 1.24355 7.04712C1.06847 5.95684 1.23713 4.8385 1.72563 3.85053V3.85053C2.09464 3.10462 2.63366 2.45803 3.29828 1.96406C3.9629 1.47008 4.73408 1.14284 5.5483 1.00931C6.36252 0.875782 7.19647 0.939779 7.98144 1.19603C8.7664 1.45228 9.47991 1.89345 10.0632 2.48319L11.4998 3.93577L12.9364 2.48319C13.5197 1.89345 14.2332 1.45228 15.0182 1.19603C15.8031 0.939779 16.6371 0.875782 17.4513 1.00931C18.2655 1.14284 19.0367 1.47008 19.7013 1.96406C20.3659 2.45803 20.905 3.10462 21.274 3.85053V3.85053C21.7625 4.8385 21.9311 5.95684 21.756 7.04712C21.581 8.1374 21.071 9.14433 20.2984 9.92527L11.4998 19.2061Z", 357 | stroke_linejoin: "round", 358 | stroke: "currentColor", 359 | stroke_width: "1.5", 360 | stroke_linecap: "round", 361 | } 362 | } 363 | )) 364 | } 365 | 366 | pub(super) fn icon_9(cx: Scope) -> Element { 367 | cx.render(rsx!( 368 | svg { 369 | view_box: "0 0 18 18", 370 | xmlns: "http://www.w3.org/2000/svg", 371 | width: "18", 372 | height: "18", 373 | fill: "none", 374 | path { 375 | fill: "black", 376 | d: "M18 15.4688H0V17.7207H18V15.4688Z", 377 | } 378 | path { 379 | fill: "black", 380 | d: "M11.0226 7.87402H0V10.126H11.0226V7.87402Z", 381 | } 382 | path { 383 | fill: "black", 384 | d: "M18 0.279297H0V2.53127H18V0.279297Z", 385 | } 386 | } 387 | )) 388 | } 389 | 390 | } 391 | 392 | 393 | -------------------------------------------------------------------------------- /ecommerce-site/src/components/product_item.rs: -------------------------------------------------------------------------------- 1 | use dioxus::prelude::*; 2 | 3 | use crate::api::Product; 4 | 5 | #[inline_props] 6 | pub(crate) fn product_item(cx: Scope, product: Product) -> Element { 7 | let Product { 8 | id, 9 | title, 10 | price, 11 | category, 12 | image, 13 | rating, 14 | .. 15 | } = product; 16 | 17 | render! { 18 | section { class: "h-40 p-2 m-2 shadow-lg ring-1 rounded-lg flex flex-row place-items-center hover:ring-4 hover:shadow-2xl transition-all duration-200", 19 | img { 20 | class: "object-scale-down w-1/6 h-full", 21 | src: "{image}", 22 | } 23 | div { class: "pl-4 text-left text-ellipsis", 24 | a { 25 | href: "/details/{id}", 26 | class: "w-full text-center", 27 | "{title}" 28 | } 29 | p { 30 | class: "w-full", 31 | "{rating}" 32 | } 33 | p { 34 | class: "w-full", 35 | "{category}" 36 | } 37 | p { 38 | class: "w-1/4", 39 | "${price}" 40 | } 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /ecommerce-site/src/components/product_page.rs: -------------------------------------------------------------------------------- 1 | use std::{str::FromStr, fmt::Display}; 2 | 3 | use dioxus::prelude::*; 4 | use crate::{api::{fetch_product, Product}, }; 5 | 6 | #[derive(Default)] 7 | enum Size { 8 | Small, 9 | #[default] 10 | Medium, 11 | Large 12 | } 13 | 14 | impl Display for Size{ 15 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 16 | match self{ 17 | Size::Small => "small".fmt(f), 18 | Size::Medium => "medium".fmt(f), 19 | Size::Large => "large".fmt(f), 20 | } 21 | } 22 | } 23 | 24 | impl FromStr for Size { 25 | type Err = (); 26 | 27 | fn from_str(s: &str) -> Result { 28 | use Size::*; 29 | match s.to_lowercase().as_str(){ 30 | "small"=> Ok(Small), 31 | "medium" => Ok(Medium), 32 | "large" => Ok(Large), 33 | _ => Err(()) 34 | } 35 | } 36 | } 37 | 38 | #[derive(PartialEq, Props)] 39 | pub struct ProductPageProps{ 40 | product_id:usize 41 | } 42 | 43 | pub fn product_page(cx: Scope) -> Element { 44 | let quantity = use_state(cx, || 1); 45 | let size = use_state(cx, Size::default); 46 | let id = cx.props.product_id; 47 | 48 | let product = use_future!(cx, |()| async move{ 49 | fetch_product(id).await 50 | }); 51 | 52 | let Product { title, price, description, category, image, rating,.. }=product.value()?.as_ref().ok()?; 53 | 54 | render!{ 55 | section { class: "py-20", 56 | div { class: "container mx-auto px-4", 57 | div { class: "flex flex-wrap -mx-4 mb-24", 58 | div { class: "w-full md:w-1/2 px-4 mb-8 md:mb-0", 59 | div { class: "relative mb-10", 60 | style: "height: 564px;", 61 | a { class: "absolute top-1/2 left-0 ml-8 transform translate-1/2", 62 | href: "#", 63 | icons::icon_0 {} 64 | } 65 | img { class: "object-cover w-full h-full", 66 | alt: "", 67 | src: "{image}", 68 | } 69 | a { class: "absolute top-1/2 right-0 mr-8 transform translate-1/2", 70 | href: "#", 71 | icons::icon_1 {} 72 | } 73 | } 74 | } 75 | div { class: "w-full md:w-1/2 px-4", 76 | div { class: "lg:pl-20", 77 | div { class: "mb-10 pb-10 border-b", 78 | h2 { class: "mt-2 mb-6 max-w-xl text-5xl md:text-6xl font-bold font-heading", 79 | "{title}" 80 | } 81 | div { class: "mb-8", 82 | "{rating}" 83 | } 84 | p { class: "inline-block mb-8 text-2xl font-bold font-heading text-blue-300", 85 | span { 86 | "${price}" 87 | } 88 | } 89 | p { class: "max-w-md text-gray-500", 90 | "{description}" 91 | } 92 | } 93 | div { class: "flex mb-12", 94 | div { class: "mr-6", 95 | span { class: "block mb-4 font-bold font-heading text-gray-400 uppercase", 96 | "QTY" 97 | } 98 | div { class: "inline-flex items-center px-4 font-semibold font-heading text-gray-500 border border-gray-200 focus:ring-blue-300 focus:border-blue-300 rounded-md", 99 | button { class: "py-2 hover:text-gray-700", 100 | onclick: move |_| quantity.modify(|q| *q + 1), 101 | icons::icon_2 {} 102 | } 103 | input { class: "w-12 m-0 px-2 py-4 text-center md:text-right border-0 focus:ring-transparent focus:outline-none rounded-md", 104 | placeholder: "1", 105 | r#type: "number", 106 | value: "{quantity}", 107 | oninput: move |evt| if let Ok(as_number) = evt.value.parse() { quantity.set(as_number) }, 108 | } 109 | button { class: "py-2 hover:text-gray-700", 110 | onclick: move |_| quantity.modify(|q| q - 1), 111 | icons::icon_3 {} 112 | } 113 | } 114 | } 115 | div { 116 | span { class: "block mb-4 font-bold font-heading text-gray-400 uppercase", 117 | "Size" 118 | } 119 | select { class: "pl-6 pr-10 py-4 font-semibold font-heading text-gray-500 border border-gray-200 focus:ring-blue-300 focus:border-blue-300 rounded-md", 120 | id: "", 121 | name: "", 122 | onchange: move |evt| { 123 | if let Ok(new_size) = evt.value.parse() { 124 | size.set(new_size); 125 | } 126 | }, 127 | option { 128 | value: "1", 129 | "Medium" 130 | } 131 | option { 132 | value: "2", 133 | "Small" 134 | } 135 | option { 136 | value: "3", 137 | "Large" 138 | } 139 | } 140 | } 141 | } 142 | div { class: "flex flex-wrap -mx-4 mb-14 items-center", 143 | div { class: "w-full xl:w-2/3 px-4 mb-4 xl:mb-0", 144 | a { class: "block bg-orange-300 hover:bg-orange-400 text-center text-white font-bold font-heading py-5 px-8 rounded-md uppercase transition duration-200", 145 | href: "#", 146 | "Add to cart" 147 | } 148 | } 149 | } 150 | div { class: "flex items-center", 151 | span { class: "mr-8 text-gray-500 font-bold font-heading uppercase", 152 | "SHARE IT" 153 | } 154 | a { class: "mr-1 w-8 h-8", 155 | href: "#", 156 | img { 157 | alt: "", 158 | src: "https://shuffle.dev/yofte-assets/buttons/facebook-circle.svg", 159 | } 160 | } 161 | a { class: "mr-1 w-8 h-8", 162 | href: "#", 163 | img { 164 | alt: "", 165 | src: "https://shuffle.dev/yofte-assets/buttons/instagram-circle.svg", 166 | } 167 | } 168 | a { class: "w-8 h-8", 169 | href: "#", 170 | img { 171 | src: "https://shuffle.dev/yofte-assets/buttons/twitter-circle.svg", 172 | alt: "", 173 | } 174 | } 175 | } 176 | } 177 | } 178 | } 179 | div { 180 | ul { class: "flex flex-wrap mb-16 border-b-2", 181 | li { class: "w-1/2 md:w-auto", 182 | a { class: "inline-block py-6 px-10 bg-white text-gray-500 font-bold font-heading shadow-2xl", 183 | href: "#", 184 | "Description" 185 | } 186 | } 187 | li { class: "w-1/2 md:w-auto", 188 | a { class: "inline-block py-6 px-10 text-gray-500 font-bold font-heading", 189 | href: "#", 190 | "Customer reviews" 191 | } 192 | } 193 | li { class: "w-1/2 md:w-auto", 194 | a { class: "inline-block py-6 px-10 text-gray-500 font-bold font-heading", 195 | href: "#", 196 | "Shipping & returns" 197 | } 198 | } 199 | li { class: "w-1/2 md:w-auto", 200 | a { class: "inline-block py-6 px-10 text-gray-500 font-bold font-heading", 201 | href: "#", 202 | "Brand" 203 | } 204 | } 205 | } 206 | h3 { class: "mb-8 text-3xl font-bold font-heading text-blue-300", 207 | "{category}" 208 | } 209 | p { class: "max-w-2xl text-gray-500", 210 | "{description}" 211 | } 212 | } 213 | } 214 | } 215 | } 216 | } 217 | 218 | mod icons { 219 | use super::*; 220 | 221 | pub(super) fn icon_0(cx: Scope) -> Element { 222 | cx.render(rsx!( 223 | svg { class: "w-6 h-6", 224 | view_box: "0 0 24 23", 225 | xmlns: "http://www.w3.org/2000/svg", 226 | height: "23", 227 | fill: "none", 228 | width: "24", 229 | path { 230 | stroke: "black", 231 | fill: "black", 232 | d: "M2.01328 18.9877C2.05682 16.7902 2.71436 12.9275 6.3326 9.87096L6.33277 9.87116L6.33979 9.86454L6.3398 9.86452C6.34682 9.85809 8.64847 7.74859 13.4997 7.74859C13.6702 7.74859 13.8443 7.75111 14.0206 7.757L14.0213 7.75702L14.453 7.76978L14.6331 7.77511V7.59486V3.49068L21.5728 10.5736L14.6331 17.6562V13.6558V13.5186L14.4998 13.4859L14.1812 13.4077C14.1807 13.4075 14.1801 13.4074 14.1792 13.4072M2.01328 18.9877L14.1792 13.4072M2.01328 18.9877C7.16281 11.8391 14.012 13.3662 14.1792 13.4072M2.01328 18.9877L14.1792 13.4072M23.125 10.6961L23.245 10.5736L23.125 10.4512L13.7449 0.877527L13.4449 0.571334V1V6.5473C8.22585 6.54663 5.70981 8.81683 5.54923 8.96832C-0.317573 13.927 0.931279 20.8573 0.946581 20.938L0.946636 20.9383L1.15618 22.0329L1.24364 22.4898L1.47901 22.0885L2.041 21.1305L2.04103 21.1305C4.18034 17.4815 6.71668 15.7763 8.8873 15.0074C10.9246 14.2858 12.6517 14.385 13.4449 14.4935V20.1473V20.576L13.7449 20.2698L23.125 10.6961Z", 233 | stroke_width: "0.35", 234 | } 235 | } 236 | )) 237 | } 238 | 239 | pub(super) fn icon_1(cx: Scope) -> Element { 240 | cx.render(rsx!( 241 | svg { class: "w-6 h-6", 242 | height: "27", 243 | view_box: "0 0 27 27", 244 | fill: "none", 245 | width: "27", 246 | xmlns: "http://www.w3.org/2000/svg", 247 | path { 248 | d: "M13.4993 26.2061L4.70067 16.9253C3.9281 16.1443 3.41815 15.1374 3.24307 14.0471C3.06798 12.9568 3.23664 11.8385 3.72514 10.8505V10.8505C4.09415 10.1046 4.63318 9.45803 5.29779 8.96406C5.96241 8.47008 6.73359 8.14284 7.54782 8.00931C8.36204 7.87578 9.19599 7.93978 9.98095 8.19603C10.7659 8.45228 11.4794 8.89345 12.0627 9.48319L13.4993 10.9358L14.9359 9.48319C15.5192 8.89345 16.2327 8.45228 17.0177 8.19603C17.8026 7.93978 18.6366 7.87578 19.4508 8.00931C20.265 8.14284 21.0362 8.47008 21.7008 8.96406C22.3654 9.45803 22.9045 10.1046 23.2735 10.8505V10.8505C23.762 11.8385 23.9306 12.9568 23.7556 14.0471C23.5805 15.1374 23.0705 16.1443 22.298 16.9253L13.4993 26.2061Z", 249 | stroke: "black", 250 | stroke_width: "1.5", 251 | stroke_linecap: "round", 252 | stroke_linejoin: "round", 253 | } 254 | } 255 | )) 256 | } 257 | 258 | pub(super) fn icon_2(cx: Scope) -> Element { 259 | cx.render(rsx!( 260 | svg { 261 | view_box: "0 0 12 12", 262 | height: "12", 263 | width: "12", 264 | fill: "none", 265 | xmlns: "http://www.w3.org/2000/svg", 266 | g { 267 | opacity: "0.35", 268 | rect { 269 | height: "12", 270 | x: "5", 271 | fill: "currentColor", 272 | width: "2", 273 | } 274 | rect { 275 | fill: "currentColor", 276 | width: "2", 277 | height: "12", 278 | x: "12", 279 | y: "5", 280 | transform: "rotate(90 12 5)", 281 | } 282 | } 283 | } 284 | )) 285 | } 286 | 287 | pub(super) fn icon_3(cx: Scope) -> Element { 288 | cx.render(rsx!( 289 | svg { 290 | width: "12", 291 | fill: "none", 292 | view_box: "0 0 12 2", 293 | height: "2", 294 | xmlns: "http://www.w3.org/2000/svg", 295 | g { 296 | opacity: "0.35", 297 | rect { 298 | transform: "rotate(90 12 0)", 299 | height: "12", 300 | fill: "currentColor", 301 | x: "12", 302 | width: "2", 303 | } 304 | } 305 | } 306 | )) 307 | } 308 | } 309 | 310 | 311 | -------------------------------------------------------------------------------- /ecommerce-site/src/main.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_snake_case)] 2 | 3 | use axum::{ 4 | extract::{Path, WebSocketUpgrade}, 5 | response::Html, 6 | routing::get, 7 | Router, 8 | }; 9 | use components::home::Home; 10 | use dioxus::prelude::*; 11 | use std::{future::Future, net::SocketAddr}; 12 | use tokio::runtime::Handle; 13 | use tower_http::services::ServeDir; 14 | 15 | mod components { 16 | pub mod error; 17 | pub mod home; 18 | pub mod nav; 19 | pub mod product_item; 20 | pub mod product_page; 21 | } 22 | mod api; 23 | 24 | #[tokio::main] 25 | async fn main() { 26 | // Create a liveview pool 27 | let view = dioxus_liveview::LiveViewPool::new(); 28 | 29 | let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); 30 | 31 | // build our application router 32 | let app = Router::new() 33 | // serve the public directory 34 | .nest_service("/public", ServeDir::new("public")) 35 | // serve the SSR rendered homepage 36 | .route("/", get(root)) 37 | // serve the liveview rendered details page 38 | .route( 39 | "/details/:id", 40 | get(move |Path(id): Path| async move { 41 | Html(format!( 42 | r#" 43 | 44 | 45 | 46 | Dioxus Ecomerse 47 | 48 | 49 |
50 | {} 51 | 52 | "#, 53 | dioxus_liveview::interpreter_glue(&format!("ws://{addr}/details/{id}/ws")) 54 | )) 55 | }), 56 | ) 57 | .route( 58 | "/details/:id/ws", 59 | get( 60 | move |Path(id): Path, ws: WebSocketUpgrade| async move { 61 | ws.on_upgrade(move |socket| async move { 62 | _ = view 63 | .launch_with_props(dioxus_liveview::axum_socket(socket), details, id) 64 | .await; 65 | }) 66 | }, 67 | ), 68 | ); 69 | 70 | // run it 71 | println!("listening on http://{}", addr); 72 | println!("- Route available on http://{}", addr); 73 | println!("- Route available on http://{}/details/1", addr); 74 | axum::Server::bind(&addr) 75 | .serve(app.into_make_service()) 76 | .await 77 | .unwrap(); 78 | } 79 | 80 | // Just render a simple page directly from the request 81 | async fn root() -> Html { 82 | // The root page blocks on futures so we need to render it in a spawn_blocking task 83 | tokio::task::spawn_blocking(move || async move { 84 | let mut app = VirtualDom::new(Home); 85 | let _ = app.rebuild(); 86 | Html(dioxus_ssr::render(&app)) 87 | }) 88 | .await 89 | .unwrap() 90 | .await 91 | } 92 | 93 | /// Render a more sophisticated page with ssr 94 | fn details(cx: Scope) -> Element { 95 | cx.render(rsx!( 96 | div { 97 | components::nav::nav {} 98 | components::product_page::product_page { 99 | product_id: *cx.props 100 | } 101 | } 102 | )) 103 | } 104 | 105 | pub(crate) fn block_on( 106 | f: impl Future + Send + Sync + 'static, 107 | ) -> T { 108 | let handle = Handle::current(); 109 | std::thread::spawn(move || { 110 | // Using Handle::block_on to run async code in the new thread. 111 | handle.block_on(f) 112 | }) 113 | .join() 114 | .unwrap() 115 | } 116 | -------------------------------------------------------------------------------- /ecommerce-site/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | mode: "all", 3 | content: [ 4 | // include all rust, html and css files in the src directory 5 | "./src/**/*.{rs,html,css}", 6 | // include all html files in the output (dist) directory 7 | "./dist/**/*.html", 8 | ], 9 | theme: { 10 | extend: {}, 11 | }, 12 | plugins: [], 13 | }; 14 | -------------------------------------------------------------------------------- /file-explorer/.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | /dist/ 5 | /static/ 6 | /.dioxus/ 7 | 8 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 9 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 10 | Cargo.lock 11 | 12 | # These are backup files generated by rustfmt 13 | **/*.rs.bk 14 | -------------------------------------------------------------------------------- /file-explorer/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "file-explorer" 3 | edition = "2021" 4 | version = "0.1.0" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | 10 | dioxus = { version = "0.5", features = ["desktop"] } 11 | 12 | # Debug 13 | log = "0.4.19" 14 | dioxus-logger = "0.4.1" 15 | open = "5.1.2" 16 | 17 | [package.metadata.bundle] 18 | name = "Dioxus File Explorerer" 19 | identifier = "com.doe.exampleapplication" 20 | icon = ["32x32.png", "128x128.png", "128x128@2x.png"] 21 | version = "1.1.0" 22 | authors = ["tkr-sh ", "Jonathan Kelley "] 23 | resources = ["assets", "images/**/*.png", "secrets/public_key.txt"] 24 | copyright = "Copyright (c) Jane Doe 2016. All rights reserved." 25 | category = "Developer Tool" 26 | short_description = "An example application." 27 | 28 | # This package is not part of the workspace to avoid dependency resolution issues between dioxus 0.4 and 0.5 29 | [workspace] 30 | -------------------------------------------------------------------------------- /file-explorer/Dioxus.toml: -------------------------------------------------------------------------------- 1 | [application] 2 | 3 | # App (Project) Name 4 | name = "file-explorer" 5 | 6 | # Dioxus App Default Platform 7 | # desktop, web 8 | default_platform = "desktop" 9 | 10 | # `build` & `serve` dist path 11 | out_dir = "dist" 12 | 13 | # assets file folder 14 | asset_dir = "assets" 15 | 16 | [web.app] 17 | 18 | # HTML title tag content 19 | title = "file-explorer" 20 | 21 | [web.watcher] 22 | 23 | # when watcher trigger, regenerate the `index.html` 24 | reload_html = true 25 | 26 | # which files or dirs will be watcher monitoring 27 | watch_path = ["src", "assets"] 28 | 29 | # include `assets` in web platform 30 | [web.resource] 31 | 32 | # CSS style file 33 | 34 | style = [] 35 | 36 | # Javascript code file 37 | script = [] 38 | 39 | [web.resource.dev] 40 | 41 | # Javascript code file 42 | # serve: [dev-server] only 43 | script = [] 44 | -------------------------------------------------------------------------------- /file-explorer/README.md: -------------------------------------------------------------------------------- 1 | # File-explorer with Rust and Dioxus 2 | 3 | This example shows how a Dioxus App can directly leverage system calls and libraries to bridge native functionality with the WebView renderer. 4 | 5 | ![example](./assets/image.png) 6 | 7 | 8 | ## To run this example: 9 | 10 | ``` 11 | dx serve 12 | ``` 13 | 14 | -------------------------------------------------------------------------------- /file-explorer/assets/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DioxusLabs/example-projects/db87199d2fcdbdd70cf5b6bed5b156ac35fb1e91/file-explorer/assets/image.png -------------------------------------------------------------------------------- /file-explorer/assets/main.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | font-family: 'Roboto', sans-serif; 5 | user-select: none; 6 | transition: .2s all; 7 | } 8 | 9 | body { 10 | padding-top: 77px; 11 | } 12 | 13 | 14 | main { 15 | padding: 20px 50px; 16 | } 17 | 18 | .folder * { 19 | width: 100px; 20 | } 21 | 22 | .folder { 23 | float: left; 24 | width: 100px; 25 | height: 152px; 26 | /* //padding: 20px; */ 27 | margin-right: 50px; 28 | margin-bottom: 70px; 29 | border-radius: 2px; 30 | /* //overflow: hidden; */ 31 | cursor: pointer; 32 | } 33 | 34 | .folder:hover h1 { 35 | display: none; 36 | } 37 | 38 | .folder:hover p.cooltip { 39 | opacity: 1; 40 | top: 0; 41 | } 42 | 43 | .folder * { 44 | text-align: center; 45 | } 46 | 47 | .folder i { 48 | margin: 0; 49 | font-size: 100px; 50 | color: #607D8B; 51 | } 52 | 53 | .folder h1 { 54 | position: relative; 55 | display: block; 56 | top: -37px; 57 | font-size: 20px; 58 | font-weight: 400; 59 | } 60 | 61 | .folder p.cooltip { 62 | position: relative; 63 | top: 5px; 64 | left: -50%; 65 | margin-left: 35px; 66 | background: #212121; 67 | font-size: 15px; 68 | color: white; 69 | border-radius: 4px; 70 | padding: 10px 20px; 71 | padding-right: 30px; 72 | width: 100px; 73 | opacity: 0; 74 | } 75 | 76 | .folder p.cooltip:before { 77 | content: ''; 78 | position: absolute; 79 | display: block; 80 | top: -4px; 81 | left: 50%; 82 | margin-left: -5px; 83 | height: 10px; 84 | width: 10px; 85 | border-radius: 2px; 86 | background-color: #212121; 87 | transform: rotate(45deg); 88 | } 89 | 90 | div.properties { 91 | position: fixed; 92 | top: 0; 93 | right: 0; 94 | bottom: 0; 95 | z-index: 10; 96 | width: 300px; 97 | background-color: white; 98 | } 99 | 100 | div.properties:before { 101 | content: ''; 102 | position: fixed; 103 | top: 0; 104 | left: 0; 105 | right: 300px; 106 | bottom: 0; 107 | background-color: #212121; 108 | opacity: .5; 109 | overflow: hidden; 110 | } 111 | 112 | div.properties img { 113 | position: relative; 114 | top: -1px; 115 | left: -1px; 116 | width: 110%; 117 | height: 200px; 118 | filter: blur(2px); 119 | } 120 | 121 | div.properties h1 { 122 | position: relative; 123 | width: 100%; 124 | text-align: left; 125 | margin-left: 20px; 126 | color: white; 127 | } 128 | 129 | header { 130 | position: fixed; 131 | top: 0; 132 | left: 0; 133 | right: 0; 134 | padding: 20px; 135 | background-color: #2196F3; 136 | color: white; 137 | display: flex; 138 | align-items: center; 139 | } 140 | 141 | header h1 { 142 | font-weight: 400; 143 | } 144 | 145 | header span { 146 | flex: 1; 147 | } 148 | 149 | header i { 150 | margin: 0 10px; 151 | cursor: pointer; 152 | } 153 | 154 | header i:nth-child(1) { 155 | margin: 0 20px; 156 | } 157 | -------------------------------------------------------------------------------- /file-explorer/src/main.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_snake_case)] 2 | //! Example: File Explorer 3 | //! ------------------------- 4 | //! 5 | //! This is a fun little desktop application that lets you explore the file system. 6 | //! 7 | //! This example is interesting because it's mixing filesystem operations and GUI, which is typically hard for UI to do. 8 | 9 | use dioxus::prelude::*; 10 | use log::LevelFilter; 11 | 12 | fn main() { 13 | // Init debug 14 | dioxus_logger::init(LevelFilter::Info).expect("failed to init logger"); 15 | 16 | dioxus::launch(App); 17 | } 18 | 19 | #[component] 20 | fn App() -> Element { 21 | // Build cool things ✌️ 22 | let mut files = use_signal(|| Files::new()); 23 | 24 | rsx! { 25 | div { 26 | link { href: "https://fonts.googleapis.com/icon?family=Material+Icons", rel: "stylesheet" } 27 | link { rel: "stylesheet", href: "main.css" } 28 | header { 29 | i { class: "material-icons icon-menu", "menu" } 30 | h1 { "Files: {files.read().current()}" } 31 | span { } 32 | i { class: "material-icons", onclick: move |_| files.write().go_up(), "logout" } 33 | } 34 | main { 35 | for (dir_id, path) in files.read().path_names.iter().enumerate() { 36 | div { 37 | class: "folder", key: "{path.name}", 38 | i { class: "material-icons", 39 | onclick: move |_| files.write().enter_dir(dir_id), 40 | // Change the icon 41 | if path.is_directory { "folder" } else { "description" } 42 | // The tooltip 43 | p { class: "cooltip", "0 folders / 0 files" } 44 | } 45 | h1 { "{path.name}" } 46 | } 47 | } 48 | if let Some(err) = files.read().err.as_ref() { 49 | div { 50 | code { "{err}" } 51 | button { onclick: move |_| files.write().clear_err(), "x" } 52 | } 53 | } 54 | } 55 | } 56 | } 57 | } 58 | 59 | #[derive(Debug)] 60 | struct File { 61 | is_directory: bool, 62 | name: String, 63 | } 64 | 65 | struct Files { 66 | path_stack: Vec, 67 | path_names: Vec, 68 | err: Option, 69 | } 70 | 71 | impl Files { 72 | fn new() -> Self { 73 | let mut files = Self { 74 | path_stack: vec![".".to_string()], 75 | path_names: vec![], 76 | err: None, 77 | }; 78 | 79 | files.reload_path_list(); 80 | 81 | files 82 | } 83 | 84 | fn reload_path_list(&mut self) { 85 | let cur_path = self.path_stack.join("/"); 86 | log::info!("Reloading path list for {:?}", cur_path); 87 | let paths = match std::fs::read_dir(&cur_path) { 88 | Ok(e) => e, 89 | Err(err) => { 90 | // Likely we're trying to open a file, so let's open it! 91 | if let Ok(_) = open::that(cur_path) { 92 | log::info!("Opened file"); 93 | return; 94 | } else { 95 | let err = format!("An error occurred: {:?}", err); 96 | self.err = Some(err); 97 | self.path_stack.pop(); 98 | return; 99 | } 100 | } 101 | }; 102 | 103 | let collected = paths.collect::>(); 104 | log::info!("Path list reloaded {:#?}", collected); 105 | 106 | // clear the current state 107 | self.clear_err(); 108 | self.path_names.clear(); 109 | 110 | for path in collected { 111 | let file = path.unwrap(); 112 | self.path_names.push(File { 113 | name: file.file_name().to_str().unwrap().to_string(), 114 | is_directory: file.file_type().unwrap().is_dir(), 115 | }); 116 | } 117 | log::info!("path names are {:#?}", self.path_names); 118 | } 119 | 120 | fn go_up(&mut self) { 121 | if self.path_stack.len() > 1 { 122 | self.path_stack.pop(); 123 | } 124 | self.reload_path_list(); 125 | } 126 | 127 | fn enter_dir(&mut self, dir_id: usize) { 128 | let path = &self.path_names[dir_id]; 129 | self.path_stack.push(path.name.to_string()); 130 | self.reload_path_list(); 131 | } 132 | 133 | fn current(&self) -> String { 134 | self.path_stack.join("/") 135 | } 136 | 137 | fn clear_err(&mut self) { 138 | self.err = None; 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /image_generator_open_ai/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /image_generator_open_ai/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "image_generator_open_ai" 3 | version = "0.1.1" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | 10 | dioxus = { version = "0.4.0" } 11 | dioxus-desktop = { version = "0.4.0" } 12 | reqwest = "0.11.13" 13 | tokio = { version = "1", features = ["full"] } 14 | serde_json = "1.0.91" 15 | serde = "1.0.152" -------------------------------------------------------------------------------- /image_generator_open_ai/src/main.rs: -------------------------------------------------------------------------------- 1 | /* A simple project to inspire e show how to use Framework Dioxus and query OpenAI to generate images.*/ 2 | use dioxus::prelude::*; 3 | use serde::{Deserialize, Serialize}; 4 | use serde_json::json; 5 | 6 | #[tokio::main] 7 | async fn main() { 8 | dioxus_desktop::launch(app) 9 | } 10 | 11 | fn app(cx: Scope) -> Element { 12 | let api = use_state(cx, || "".to_string()); 13 | let prompt = use_state(cx, || "".to_string()); 14 | let n_image = use_state(cx, || 1.to_string()); 15 | let image = use_state(cx, || ImageResponse { 16 | created: 0, 17 | data: Vec::new(), 18 | }); 19 | let loading = use_state(cx, || "".to_string()); 20 | 21 | cx.render(rsx! { 22 | head { 23 | link { 24 | rel: "stylesheet", 25 | href: "https://unpkg.com/bulma@0.9.0/css/bulma.min.css", 26 | } 27 | } 28 | div { class: "container", 29 | div { class: "columns", 30 | div { class: "column", 31 | input { class: "input is-primary mt-4", 32 | value:"{api}", 33 | r#type: "text", 34 | placeholder: "API", 35 | oninput: move |evt| { 36 | api.set(evt.value.clone()); 37 | }, 38 | } 39 | 40 | input { class: "input is-primary mt-4", 41 | placeholder: "MAX 1000 Dgts", 42 | r#type: "text", 43 | value:"{prompt}", 44 | oninput: move |evt| { 45 | prompt.set(evt.value.clone()); 46 | }, 47 | } 48 | 49 | input { class: "input is-primary mt-4", 50 | r#type: "number", 51 | min:"1", 52 | max:"10", 53 | value:"{n_image}", 54 | oninput: move |evt| { 55 | n_image.set(evt.value.clone()); 56 | }, 57 | } 58 | } 59 | } 60 | 61 | button { class: "button is-primary {loading}", 62 | 63 | onclick: move |_| cx.spawn({ 64 | 65 | let loading = loading.clone(); 66 | loading.set("is-loading".to_string()); 67 | let image_request = image.clone(); 68 | let api_request = api.clone(); 69 | let prompt_request = prompt.clone(); 70 | let n_image_request = n_image.clone(); 71 | async move { 72 | image_request.set(request(api_request.current().to_string(), prompt_request.current().to_string(), n_image_request.current().to_string()).await); 73 | loading.set("".to_string()); 74 | } 75 | }), 76 | "Generate image" 77 | } 78 | br { 79 | } 80 | } 81 | image.data.iter().map(|image| { 82 | rsx!( 83 | section { class: "is-flex", 84 | div { class: "container is-fluid", 85 | div { class: "container has-text-centered", 86 | div { class: "is-justify-content-center", 87 | div { class: "level", 88 | div { class: "level-item", 89 | figure { class: "image", 90 | img { 91 | alt: "", 92 | src: "{image.url}", 93 | } 94 | } 95 | } 96 | } 97 | } 98 | } 99 | } 100 | } 101 | ) 102 | }) 103 | }) 104 | } 105 | async fn request(api: String, prompt: String, n_image: String) -> ImageResponse { 106 | let client = reqwest::Client::new(); 107 | let body = json!({ 108 | "prompt": prompt, 109 | "n":n_image.parse::().unwrap_or(1), 110 | "size":"1024x1024", 111 | }); 112 | 113 | let mut authorization = "Bearer ".to_string(); 114 | authorization.push_str(&api); 115 | 116 | let res = client 117 | .post("https://api.openai.com/v1/images/generations") 118 | .body(body.to_string()) 119 | .header("Content-Type", "application/json") 120 | .header("Authorization", authorization) 121 | .send() 122 | .await 123 | .unwrap() 124 | .text() 125 | .await 126 | .unwrap(); 127 | let deserialized: ImageResponse = serde_json::from_str(&res).unwrap(); 128 | deserialized 129 | } 130 | 131 | #[derive(Serialize, Deserialize, Debug, PartialEq, Props, Clone)] 132 | struct UrlImage { 133 | url: String, 134 | } 135 | 136 | #[derive(Serialize, Deserialize, Debug, PartialEq, Props, Clone)] 137 | struct ImageResponse { 138 | created: i32, 139 | data: Vec, 140 | } 141 | -------------------------------------------------------------------------------- /ios_demo/.gitignore: -------------------------------------------------------------------------------- 1 | # Rust 2 | target/ 3 | **/*.rs.bk 4 | 5 | # cargo-mobile 6 | .cargo/ 7 | /gen 8 | 9 | # macOS 10 | .DS_Store 11 | -------------------------------------------------------------------------------- /ios_demo/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dioxus-ios-demo" 3 | version = "0.1.1" 4 | authors = ["Jonathan Kelley "] 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type = ["staticlib", "cdylib", "rlib"] 9 | 10 | [[bin]] 11 | name = "dioxus-ios-demo-desktop" 12 | path = "gen/bin/desktop.rs" 13 | 14 | [dependencies] 15 | mobile-entry-point = "0.1.0" 16 | dioxus = { version = "0.4.0" } 17 | dioxus-mobile = { version = "0.4.0" } 18 | simple_logger = "4.2.0" 19 | im-rc = "15.0.0" 20 | 21 | 22 | 23 | # [target.'cfg(target_os = "android")'.dependencies] 24 | # android_logger = "0.9.0" 25 | # log = "0.4.11" 26 | # ndk-glue = "0.2.1" 27 | 28 | # [target.'cfg(not(target_os = "android"))'.dependencies] 29 | # simple_logger = "1.11.0" 30 | -------------------------------------------------------------------------------- /ios_demo/README.md: -------------------------------------------------------------------------------- 1 | # Dioxus: iOS 2 | 3 | This example showcases Dioxus support for mobile. Currently, only iOS is supported. To get started, follow the cargo-mobile getting started guide and then use the `cargo apple` commands to build and test for iOS. 4 | 5 | To run, `cargo apple run` works. To open XCode and select a device, try `cargo apple open`. 6 | 7 | ![image](assets/screenshot.jpeg) 8 | -------------------------------------------------------------------------------- /ios_demo/assets/screenshot.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DioxusLabs/example-projects/db87199d2fcdbdd70cf5b6bed5b156ac35fb1e91/ios_demo/assets/screenshot.jpeg -------------------------------------------------------------------------------- /ios_demo/assets/screenshot_smaller.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DioxusLabs/example-projects/db87199d2fcdbdd70cf5b6bed5b156ac35fb1e91/ios_demo/assets/screenshot_smaller.jpeg -------------------------------------------------------------------------------- /ios_demo/mobile.toml: -------------------------------------------------------------------------------- 1 | [app] 2 | name = "dioxus-ios-demo" 3 | stylized-name = "Dioxus Ios Demo" 4 | domain = "jonathankelley.com" 5 | template-pack = "winit" 6 | 7 | [apple] 8 | development-team = "34U4FG9TJ8" 9 | -------------------------------------------------------------------------------- /ios_demo/src/lib.rs: -------------------------------------------------------------------------------- 1 | use dioxus::{html::input_data::keyboard_types::Key, prelude::*}; 2 | use mobile_entry_point::mobile_entry_point; 3 | 4 | #[mobile_entry_point] 5 | fn main() { 6 | simple_logger::SimpleLogger::new().init().unwrap(); 7 | dioxus_mobile::launch(app); 8 | } 9 | 10 | #[derive(PartialEq)] 11 | pub enum FilterState { 12 | All, 13 | Active, 14 | Completed, 15 | } 16 | 17 | pub type Todos = im_rc::HashMap; 18 | 19 | #[derive(Debug, PartialEq, Clone)] 20 | pub struct TodoItem { 21 | pub id: u32, 22 | pub checked: bool, 23 | pub contents: String, 24 | } 25 | 26 | pub fn app(cx: Scope<()>) -> Element { 27 | let todos = use_ref(cx, im_rc::HashMap::::default); 28 | let filter = use_state(cx, || FilterState::All); 29 | let draft = use_ref(cx, String::new); 30 | let todo_id = use_state(cx, || 0); 31 | 32 | // Filter the todos based on the filter state 33 | let mut filtered_todos = todos 34 | .read() 35 | .iter() 36 | .filter(|(_, item)| match filter.get() { 37 | FilterState::All => true, 38 | FilterState::Active => !item.checked, 39 | FilterState::Completed => item.checked, 40 | }) 41 | .map(|f| *f.0) 42 | .collect::>(); 43 | filtered_todos.sort_unstable(); 44 | 45 | let show_clear_completed = todos.read().values().any(|todo| todo.checked); 46 | let items_left = filtered_todos.len(); 47 | let item_text = match items_left { 48 | 1 => "item", 49 | _ => "items", 50 | }; 51 | 52 | cx.render(rsx!{ 53 | section { class: "todoapp", 54 | style { include_str!("../src/style.css") } 55 | div { 56 | header { class: "header", 57 | h1 {"todos"} 58 | input { 59 | class: "new-todo", 60 | placeholder: "What needs to be done?", 61 | value: "{draft.read()}", 62 | autofocus: "true", 63 | oninput: move |evt| draft.set(evt.value.clone()), 64 | onkeydown: move |evt| { 65 | if evt.key() == Key::Enter && !draft.read().is_empty() { 66 | todos.write().insert( 67 | *todo_id.get(), 68 | TodoItem { 69 | id: *todo_id.get(), 70 | checked: false, 71 | contents: draft.read().clone(), 72 | }, 73 | ); 74 | todo_id.set(todo_id + 1); 75 | draft.set("".to_string()); 76 | } 77 | } 78 | } 79 | } 80 | ul { class: "todo-list", 81 | filtered_todos.iter().map(|id| rsx!(todo_entry{ key: "{id}", id: *id, set_todos: todos })) 82 | } 83 | (!todos.read().is_empty()).then(|| rsx!( 84 | footer { class: "footer", 85 | span { class: "todo-count", 86 | strong {"{items_left} "} 87 | span {"{item_text} left"} 88 | } 89 | ul { class: "filters", 90 | li { class: "All", a { onclick: move |_| filter.set(FilterState::All), "All" }} 91 | li { class: "Active", a { onclick: move |_| filter.set(FilterState::Active), "Active" }} 92 | li { class: "Completed", a { onclick: move |_| filter.set(FilterState::Completed), "Completed" }} 93 | } 94 | (show_clear_completed).then(|| rsx!( 95 | button { 96 | class: "clear-completed", 97 | onclick: move |_| todos.write().retain(|_, todo| !todo.checked), 98 | "Clear completed" 99 | } 100 | )) 101 | } 102 | )) 103 | } 104 | } 105 | footer { class: "info", 106 | p {"Double-click to edit a todo"} 107 | p { "Created by ", a { href: "http://github.com/jkelleyrtp/", "jkelleyrtp" }} 108 | p { "Part of ", a { href: "http://todomvc.com", "TodoMVC" }} 109 | } 110 | }) 111 | } 112 | 113 | #[derive(Props)] 114 | pub struct TodoEntryProps<'a> { 115 | set_todos: &'a UseRef, 116 | id: u32, 117 | } 118 | 119 | pub fn todo_entry<'a>(cx: Scope<'a, TodoEntryProps<'a>>) -> Element { 120 | let editing = use_state(cx, || false); 121 | 122 | let todos = cx.props.set_todos.read(); 123 | let todo = &todos[&cx.props.id]; 124 | let completed = if todo.checked { "completed" } else { "" }; 125 | let is_editing = (**editing).then_some("editing").unwrap_or_default(); 126 | 127 | render!(li { 128 | class: "{completed} {is_editing}", 129 | onclick: move |_| editing.set(true), 130 | onfocusout: move |_| editing.set(false), 131 | div { class: "view", 132 | input { class: "toggle", r#type: "checkbox", id: "cbg-{todo.id}", checked: "{todo.checked}", 133 | onchange: move |evt| { 134 | cx.props.set_todos.write()[&cx.props.id].checked = evt.value.parse().unwrap(); 135 | } 136 | } 137 | label { r#for: "cbg-{todo.id}", pointer_events: "none", "{todo.contents}" } 138 | } 139 | if **editing { 140 | rsx!{ 141 | input { 142 | class: "edit", 143 | value: "{todo.contents}", 144 | oninput: move |evt| cx.props.set_todos.write()[&cx.props.id].contents = evt.value.clone(), 145 | autofocus: "true", 146 | onkeydown: move |evt| { 147 | match evt.key().to_string().as_str() { 148 | "Enter" | "Escape" | "Tab" => editing.set(false), 149 | _ => {} 150 | } 151 | }, 152 | } 153 | } 154 | } 155 | }) 156 | } 157 | -------------------------------------------------------------------------------- /ios_demo/src/style.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | margin: 0; 4 | padding: 0; 5 | } 6 | 7 | button { 8 | margin: 0; 9 | padding: 0; 10 | border: 0; 11 | background: none; 12 | font-size: 100%; 13 | vertical-align: baseline; 14 | font-family: inherit; 15 | font-weight: inherit; 16 | color: inherit; 17 | -webkit-appearance: none; 18 | appearance: none; 19 | -webkit-font-smoothing: antialiased; 20 | -moz-osx-font-smoothing: grayscale; 21 | } 22 | 23 | body { 24 | font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; 25 | line-height: 1.4em; 26 | background: #f5f5f5; 27 | color: #4d4d4d; 28 | min-width: 230px; 29 | max-width: 550px; 30 | margin: 0 auto; 31 | -webkit-font-smoothing: antialiased; 32 | -moz-osx-font-smoothing: grayscale; 33 | font-weight: 300; 34 | } 35 | 36 | :focus { 37 | outline: 0; 38 | } 39 | 40 | .hidden { 41 | display: none; 42 | } 43 | 44 | .todoapp { 45 | background: #fff; 46 | margin: 130px 0 40px 0; 47 | position: relative; 48 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 49 | 0 25px 50px 0 rgba(0, 0, 0, 0.1); 50 | } 51 | 52 | .todoapp input::-webkit-input-placeholder { 53 | font-style: italic; 54 | font-weight: 300; 55 | color: #e6e6e6; 56 | } 57 | 58 | .todoapp input::-moz-placeholder { 59 | font-style: italic; 60 | font-weight: 300; 61 | color: #e6e6e6; 62 | } 63 | 64 | .todoapp input::input-placeholder { 65 | font-style: italic; 66 | font-weight: 300; 67 | color: #e6e6e6; 68 | } 69 | 70 | .todoapp h1 { 71 | position: absolute; 72 | top: -155px; 73 | width: 100%; 74 | font-size: 100px; 75 | font-weight: 100; 76 | text-align: center; 77 | color: rgba(175, 47, 47, 0.15); 78 | -webkit-text-rendering: optimizeLegibility; 79 | -moz-text-rendering: optimizeLegibility; 80 | text-rendering: optimizeLegibility; 81 | } 82 | 83 | .new-todo, 84 | .edit { 85 | position: relative; 86 | margin: 0; 87 | width: 100%; 88 | font-size: 24px; 89 | font-family: inherit; 90 | font-weight: inherit; 91 | line-height: 1.4em; 92 | border: 0; 93 | color: inherit; 94 | padding: 6px; 95 | border: 1px solid #999; 96 | box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); 97 | box-sizing: border-box; 98 | -webkit-font-smoothing: antialiased; 99 | -moz-osx-font-smoothing: grayscale; 100 | } 101 | 102 | .new-todo { 103 | padding: 16px 16px 16px 60px; 104 | border: none; 105 | background: rgba(0, 0, 0, 0.003); 106 | box-shadow: inset 0 -2px 1px rgba(0, 0, 0, 0.03); 107 | } 108 | 109 | .main { 110 | position: relative; 111 | z-index: 2; 112 | border-top: 1px solid #e6e6e6; 113 | } 114 | 115 | .toggle-all { 116 | text-align: center; 117 | border: none; 118 | /* Mobile Safari */ 119 | opacity: 0; 120 | position: absolute; 121 | } 122 | 123 | .toggle-all+label { 124 | width: 60px; 125 | height: 34px; 126 | font-size: 0; 127 | position: absolute; 128 | top: -52px; 129 | left: -13px; 130 | -webkit-transform: rotate(90deg); 131 | transform: rotate(90deg); 132 | } 133 | 134 | .toggle-all+label:before { 135 | content: '❯'; 136 | font-size: 22px; 137 | color: #e6e6e6; 138 | padding: 10px 27px 10px 27px; 139 | } 140 | 141 | .toggle-all:checked+label:before { 142 | color: #737373; 143 | } 144 | 145 | .todo-list { 146 | margin: 0; 147 | padding: 0; 148 | list-style: none; 149 | } 150 | 151 | .todo-list li { 152 | position: relative; 153 | font-size: 24px; 154 | border-bottom: 1px solid #ededed; 155 | } 156 | 157 | .todo-list li:last-child { 158 | border-bottom: none; 159 | } 160 | 161 | .todo-list li.editing { 162 | border-bottom: none; 163 | padding: 0; 164 | } 165 | 166 | .todo-list li.editing .edit { 167 | display: block; 168 | width: 506px; 169 | padding: 12px 16px; 170 | margin: 0 0 0 43px; 171 | } 172 | 173 | .todo-list li.editing .view { 174 | display: none; 175 | } 176 | 177 | .todo-list li .toggle { 178 | text-align: center; 179 | width: 40px; 180 | /* auto, since non-WebKit browsers doesn't support input styling */ 181 | height: auto; 182 | position: absolute; 183 | top: 0; 184 | bottom: 0; 185 | margin: auto 0; 186 | border: none; 187 | /* Mobile Safari */ 188 | -webkit-appearance: none; 189 | appearance: none; 190 | } 191 | 192 | .todo-list li .toggle { 193 | opacity: 0; 194 | } 195 | 196 | .todo-list li .toggle+label { 197 | /* 198 | Firefox requires `#` to be escaped - https://bugzilla.mozilla.org/show_bug.cgi?id=922433 199 | IE and Edge requires *everything* to be escaped to render, so we do that instead of just the `#` - https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7157459/ 200 | */ 201 | background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E'); 202 | background-repeat: no-repeat; 203 | background-position: center left; 204 | } 205 | 206 | .todo-list li .toggle:checked+label { 207 | background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E'); 208 | } 209 | 210 | .todo-list li label { 211 | word-break: break-all; 212 | padding: 15px 15px 15px 60px; 213 | display: block; 214 | line-height: 1.2; 215 | transition: color 0.4s; 216 | } 217 | 218 | .todo-list li.completed label { 219 | color: #d9d9d9; 220 | text-decoration: line-through; 221 | } 222 | 223 | .todo-list li .destroy { 224 | display: none; 225 | position: absolute; 226 | top: 0; 227 | right: 10px; 228 | bottom: 0; 229 | width: 40px; 230 | height: 40px; 231 | margin: auto 0; 232 | font-size: 30px; 233 | color: #cc9a9a; 234 | margin-bottom: 11px; 235 | transition: color 0.2s ease-out; 236 | } 237 | 238 | .todo-list li .destroy:hover { 239 | color: #af5b5e; 240 | } 241 | 242 | .todo-list li .destroy:after { 243 | content: '×'; 244 | } 245 | 246 | .todo-list li:hover .destroy { 247 | display: block; 248 | } 249 | 250 | .todo-list li .edit { 251 | display: none; 252 | } 253 | 254 | .todo-list li.editing:last-child { 255 | margin-bottom: -1px; 256 | } 257 | 258 | .footer { 259 | color: #777; 260 | padding: 10px 15px; 261 | height: 20px; 262 | text-align: center; 263 | border-top: 1px solid #e6e6e6; 264 | } 265 | 266 | .footer:before { 267 | content: ''; 268 | position: absolute; 269 | right: 0; 270 | bottom: 0; 271 | left: 0; 272 | height: 50px; 273 | overflow: hidden; 274 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 275 | 0 8px 0 -3px #f6f6f6, 276 | 0 9px 1px -3px rgba(0, 0, 0, 0.2), 277 | 0 16px 0 -6px #f6f6f6, 278 | 0 17px 2px -6px rgba(0, 0, 0, 0.2); 279 | } 280 | 281 | .todo-count { 282 | float: left; 283 | text-align: left; 284 | } 285 | 286 | .todo-count strong { 287 | font-weight: 300; 288 | } 289 | 290 | .filters { 291 | margin: 0; 292 | padding: 0; 293 | list-style: none; 294 | position: absolute; 295 | right: 0; 296 | left: 0; 297 | } 298 | 299 | .filters li { 300 | display: inline; 301 | } 302 | 303 | .filters li a { 304 | color: inherit; 305 | margin: 3px; 306 | padding: 3px 7px; 307 | text-decoration: none; 308 | border: 1px solid transparent; 309 | border-radius: 3px; 310 | } 311 | 312 | .filters li a:hover { 313 | border-color: rgba(175, 47, 47, 0.1); 314 | } 315 | 316 | .filters li a.selected { 317 | border-color: rgba(175, 47, 47, 0.2); 318 | } 319 | 320 | .clear-completed, 321 | html .clear-completed:active { 322 | float: right; 323 | position: relative; 324 | line-height: 20px; 325 | text-decoration: none; 326 | cursor: pointer; 327 | } 328 | 329 | .clear-completed:hover { 330 | text-decoration: underline; 331 | } 332 | 333 | .info { 334 | margin: 65px auto 0; 335 | color: #bfbfbf; 336 | font-size: 10px; 337 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); 338 | text-align: center; 339 | } 340 | 341 | .info p { 342 | line-height: 1; 343 | } 344 | 345 | .info a { 346 | color: inherit; 347 | text-decoration: none; 348 | font-weight: 400; 349 | } 350 | 351 | .info a:hover { 352 | text-decoration: underline; 353 | } 354 | 355 | /* 356 | Hack to remove background from Mobile Safari. 357 | Can't use it globally since it destroys checkboxes in Firefox 358 | */ 359 | @media screen and (-webkit-min-device-pixel-ratio:0) { 360 | 361 | .toggle-all, 362 | .todo-list li .toggle { 363 | background: none; 364 | } 365 | 366 | .todo-list li .toggle { 367 | height: 40px; 368 | } 369 | } 370 | 371 | @media (max-width: 430px) { 372 | .footer { 373 | height: 50px; 374 | } 375 | 376 | .filters { 377 | bottom: 10px; 378 | } 379 | } 380 | -------------------------------------------------------------------------------- /jsframework-benchmark/.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | dist 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /jsframework-benchmark/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "jsframework-benchmark" 3 | version = "0.1.1" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | rand = { version = "0.8.4", features = ["small_rng"] } 10 | getrandom = { version = "*", features = ["js"] } 11 | wasm-bindgen = { version = "0.2.79", features = ["enable-interning"] } 12 | 13 | # for local iteration 14 | # dioxus = { path = "../../../dioxus", features = ["web"] } 15 | dioxus = { version = "0.4.0" } 16 | dioxus-web = { version = "0.4.0" } 17 | -------------------------------------------------------------------------------- /jsframework-benchmark/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /jsframework-benchmark/src/main.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_snake_case)] 2 | 3 | use dioxus::prelude::*; 4 | use rand::prelude::*; 5 | 6 | fn main() { 7 | dioxus_web::launch(app); 8 | } 9 | 10 | #[derive(Clone, PartialEq)] 11 | struct Label { 12 | key: usize, 13 | labels: [&'static str; 3], 14 | } 15 | 16 | impl Label { 17 | fn new_list(num: usize) -> Vec { 18 | let mut rng = SmallRng::from_entropy(); 19 | let mut labels = Vec::with_capacity(num); 20 | for x in 0..num { 21 | labels.push(Label { 22 | key: x, 23 | labels: [ 24 | ADJECTIVES.choose(&mut rng).unwrap(), 25 | COLOURS.choose(&mut rng).unwrap(), 26 | NOUNS.choose(&mut rng).unwrap(), 27 | ], 28 | }); 29 | } 30 | labels 31 | } 32 | } 33 | 34 | fn app(cx: Scope) -> Element { 35 | let items = use_ref(cx, Vec::new); 36 | let selected = use_state(cx, || None); 37 | 38 | cx.render(rsx! { 39 | div { class: "container", 40 | div { class: "jumbotron", 41 | div { class: "row", 42 | div { class: "col-md-6", h1 { "Dioxus" } } 43 | div { class: "col-md-6", 44 | div { class: "row", 45 | ActionButton { name: "Create 1,000 rows", id: "run", 46 | onclick: move |_| items.set(Label::new_list(1_000)), 47 | } 48 | ActionButton { name: "Create 10,000 rows", id: "runlots", 49 | onclick: move |_| items.set(Label::new_list(10_000)), 50 | } 51 | ActionButton { name: "Append 1,000 rows", id: "add", 52 | onclick: move |_| items.write().extend(Label::new_list(1_000)), 53 | } 54 | ActionButton { name: "Update every 10th row", id: "update", 55 | onclick: move |_| items.write().iter_mut().step_by(10).for_each(|item| item.labels[2] = "!!!"), 56 | } 57 | ActionButton { name: "Clear", id: "clear", 58 | onclick: move |_| items.write().clear(), 59 | } 60 | ActionButton { name: "Swap rows", id: "swaprows", 61 | onclick: move |_| items.write().swap(0, 998), 62 | } 63 | } 64 | } 65 | } 66 | } 67 | table { 68 | tbody { 69 | items.read().iter().enumerate().map(|(id, item)| { 70 | let is_in_danger = if (*selected).map(|s| s == id).unwrap_or(false) {"danger"} else {""}; 71 | rsx!(tr { class: "{is_in_danger}", 72 | td { class:"col-md-1" } 73 | td { class:"col-md-1", "{item.key}" } 74 | td { class:"col-md-1", onclick: move |_| selected.set(Some(id)), 75 | a { class: "lbl", "{item.labels[0]}{item.labels[1]}{item.labels[2]}" } 76 | } 77 | td { class: "col-md-1", 78 | a { class: "remove", onclick: move |_| { items.write().remove(id); }, 79 | span { class: "glyphicon glyphicon-remove remove", aria_hidden: "true" } 80 | } 81 | } 82 | td { class: "col-md-6" } 83 | }) 84 | }) 85 | } 86 | } 87 | span { class: "preloadicon glyphicon glyphicon-remove", aria_hidden: "true" } 88 | } 89 | }) 90 | } 91 | 92 | #[derive(Props)] 93 | struct ActionButtonProps<'a> { 94 | name: &'a str, 95 | id: &'a str, 96 | onclick: EventHandler<'a>, 97 | } 98 | 99 | fn ActionButton<'a>(cx: Scope<'a, ActionButtonProps<'a>>) -> Element { 100 | cx.render(rsx! { 101 | div { 102 | class: "col-sm-6 smallpad", 103 | button { 104 | class:"btn btn-primary btn-block", 105 | r#type: "button", 106 | id: "{cx.props.id}", 107 | onclick: move |_| cx.props.onclick.call(()), 108 | 109 | "{cx.props.name}" 110 | } 111 | } 112 | }) 113 | } 114 | 115 | static ADJECTIVES: &[&str] = &[ 116 | "pretty", 117 | "large", 118 | "big", 119 | "small", 120 | "tall", 121 | "short", 122 | "long", 123 | "handsome", 124 | "plain", 125 | "quaint", 126 | "clean", 127 | "elegant", 128 | "easy", 129 | "angry", 130 | "crazy", 131 | "helpful", 132 | "mushy", 133 | "odd", 134 | "unsightly", 135 | "adorable", 136 | "important", 137 | "inexpensive", 138 | "cheap", 139 | "expensive", 140 | "fancy", 141 | ]; 142 | 143 | static COLOURS: &[&str] = &[ 144 | "red", "yellow", "blue", "green", "pink", "brown", "purple", "brown", "white", "black", 145 | "orange", 146 | ]; 147 | 148 | static NOUNS: &[&str] = &[ 149 | "table", "chair", "house", "bbq", "desk", "car", "pony", "cookie", "sandwich", "burger", 150 | "pizza", "mouse", "keyboard", 151 | ]; 152 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() {} 2 | -------------------------------------------------------------------------------- /todomvc/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /todomvc/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "todomvc" 3 | version = "0.1.1" 4 | edition = "2018" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | dioxus = { version = "0.4.0" } 10 | dioxus-web = { version = "0.4.0" } 11 | wasm-logger = "0.2.0" 12 | wasm-bindgen = "0.2.78" 13 | log = "0.4.14" 14 | console_error_panic_hook = "*" 15 | im-rc = "15.0.0" 16 | -------------------------------------------------------------------------------- /todomvc/README.md: -------------------------------------------------------------------------------- 1 | # TodoApp with Rust + Dioxus 2 | 3 | 4 | This repository holds example code for a variety of TodoMVCs built with different patterns. 5 | - Hooks only 6 | - Model 7 | - Hooks + Shared State 8 | - Model + Shared State 9 | - Borrowed 10 | 11 | There isn't a real "best pattern" - but the goal with showcasing these patterns is to highlight the tradeoffs of each. 12 | 13 | In general: 14 | - use_ref is more performant than use_state, but _can_ panic at runtime. 15 | - Shared State is an automatic memoization barrier, but falls apart for collections (Recoil supports collections natively) 16 | - Borrowing tends to be quite convenient but never benefits from memoization 17 | - Models tend to be easier to understand but less composable than hooks 18 | 19 | With Dioxus, you can really write whatever you want. 20 | 21 | It's important to remember that our "budget" is 16ms - and that's a massive amount of time compared to your Rust code's performance. For perspective, Dioxus takes less than 100us to diff and update the TodoMVC app, regardless of approach. These strategies are only relevant when your app gets huge or on platforms with constrained resources. 22 | 23 | 24 | ![Live Example](./example.png) 25 | 26 | -------------------------------------------------------------------------------- /todomvc/docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 |
10 | 11 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /todomvc/docs/wasm/module.js: -------------------------------------------------------------------------------- 1 | 2 | let wasm; 3 | 4 | const heap = new Array(32).fill(undefined); 5 | 6 | heap.push(undefined, null, true, false); 7 | 8 | function getObject(idx) { return heap[idx]; } 9 | 10 | let heap_next = heap.length; 11 | 12 | function dropObject(idx) { 13 | if (idx < 36) return; 14 | heap[idx] = heap_next; 15 | heap_next = idx; 16 | } 17 | 18 | function takeObject(idx) { 19 | const ret = getObject(idx); 20 | dropObject(idx); 21 | return ret; 22 | } 23 | 24 | let cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }); 25 | 26 | cachedTextDecoder.decode(); 27 | 28 | let cachegetUint8Memory0 = null; 29 | function getUint8Memory0() { 30 | if (cachegetUint8Memory0 === null || cachegetUint8Memory0.buffer !== wasm.memory.buffer) { 31 | cachegetUint8Memory0 = new Uint8Array(wasm.memory.buffer); 32 | } 33 | return cachegetUint8Memory0; 34 | } 35 | 36 | function getStringFromWasm0(ptr, len) { 37 | return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len)); 38 | } 39 | 40 | function addHeapObject(obj) { 41 | if (heap_next === heap.length) heap.push(heap.length + 1); 42 | const idx = heap_next; 43 | heap_next = heap[idx]; 44 | 45 | heap[idx] = obj; 46 | return idx; 47 | } 48 | 49 | function debugString(val) { 50 | // primitive types 51 | const type = typeof val; 52 | if (type == 'number' || type == 'boolean' || val == null) { 53 | return `${val}`; 54 | } 55 | if (type == 'string') { 56 | return `"${val}"`; 57 | } 58 | if (type == 'symbol') { 59 | const description = val.description; 60 | if (description == null) { 61 | return 'Symbol'; 62 | } else { 63 | return `Symbol(${description})`; 64 | } 65 | } 66 | if (type == 'function') { 67 | const name = val.name; 68 | if (typeof name == 'string' && name.length > 0) { 69 | return `Function(${name})`; 70 | } else { 71 | return 'Function'; 72 | } 73 | } 74 | // objects 75 | if (Array.isArray(val)) { 76 | const length = val.length; 77 | let debug = '['; 78 | if (length > 0) { 79 | debug += debugString(val[0]); 80 | } 81 | for(let i = 1; i < length; i++) { 82 | debug += ', ' + debugString(val[i]); 83 | } 84 | debug += ']'; 85 | return debug; 86 | } 87 | // Test for built-in 88 | const builtInMatches = /\[object ([^\]]+)\]/.exec(toString.call(val)); 89 | let className; 90 | if (builtInMatches.length > 1) { 91 | className = builtInMatches[1]; 92 | } else { 93 | // Failed to match the standard '[object ClassName]' 94 | return toString.call(val); 95 | } 96 | if (className == 'Object') { 97 | // we're a user defined class or Object 98 | // JSON.stringify avoids problems with cycles, and is generally much 99 | // easier than looping through ownProperties of `val`. 100 | try { 101 | return 'Object(' + JSON.stringify(val) + ')'; 102 | } catch (_) { 103 | return 'Object'; 104 | } 105 | } 106 | // errors 107 | if (val instanceof Error) { 108 | return `${val.name}: ${val.message}\n${val.stack}`; 109 | } 110 | // TODO we could test for more things here, like `Set`s and `Map`s. 111 | return className; 112 | } 113 | 114 | let WASM_VECTOR_LEN = 0; 115 | 116 | let cachedTextEncoder = new TextEncoder('utf-8'); 117 | 118 | const encodeString = (typeof cachedTextEncoder.encodeInto === 'function' 119 | ? function (arg, view) { 120 | return cachedTextEncoder.encodeInto(arg, view); 121 | } 122 | : function (arg, view) { 123 | const buf = cachedTextEncoder.encode(arg); 124 | view.set(buf); 125 | return { 126 | read: arg.length, 127 | written: buf.length 128 | }; 129 | }); 130 | 131 | function passStringToWasm0(arg, malloc, realloc) { 132 | 133 | if (realloc === undefined) { 134 | const buf = cachedTextEncoder.encode(arg); 135 | const ptr = malloc(buf.length); 136 | getUint8Memory0().subarray(ptr, ptr + buf.length).set(buf); 137 | WASM_VECTOR_LEN = buf.length; 138 | return ptr; 139 | } 140 | 141 | let len = arg.length; 142 | let ptr = malloc(len); 143 | 144 | const mem = getUint8Memory0(); 145 | 146 | let offset = 0; 147 | 148 | for (; offset < len; offset++) { 149 | const code = arg.charCodeAt(offset); 150 | if (code > 0x7F) break; 151 | mem[ptr + offset] = code; 152 | } 153 | 154 | if (offset !== len) { 155 | if (offset !== 0) { 156 | arg = arg.slice(offset); 157 | } 158 | ptr = realloc(ptr, len, len = offset + arg.length * 3); 159 | const view = getUint8Memory0().subarray(ptr + offset, ptr + len); 160 | const ret = encodeString(arg, view); 161 | 162 | offset += ret.written; 163 | } 164 | 165 | WASM_VECTOR_LEN = offset; 166 | return ptr; 167 | } 168 | 169 | let cachegetInt32Memory0 = null; 170 | function getInt32Memory0() { 171 | if (cachegetInt32Memory0 === null || cachegetInt32Memory0.buffer !== wasm.memory.buffer) { 172 | cachegetInt32Memory0 = new Int32Array(wasm.memory.buffer); 173 | } 174 | return cachegetInt32Memory0; 175 | } 176 | 177 | function makeClosure(arg0, arg1, dtor, f) { 178 | const state = { a: arg0, b: arg1, cnt: 1, dtor }; 179 | const real = (...args) => { 180 | // First up with a closure we increment the internal reference 181 | // count. This ensures that the Rust closure environment won't 182 | // be deallocated while we're invoking it. 183 | state.cnt++; 184 | try { 185 | return f(state.a, state.b, ...args); 186 | } finally { 187 | if (--state.cnt === 0) { 188 | wasm.__wbindgen_export_2.get(state.dtor)(state.a, state.b); 189 | state.a = 0; 190 | 191 | } 192 | } 193 | }; 194 | real.original = state; 195 | 196 | return real; 197 | } 198 | function __wbg_adapter_18(arg0, arg1, arg2) { 199 | wasm._dyn_core__ops__function__Fn__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hee66633b5afceae2(arg0, arg1, addHeapObject(arg2)); 200 | } 201 | 202 | function makeMutClosure(arg0, arg1, dtor, f) { 203 | const state = { a: arg0, b: arg1, cnt: 1, dtor }; 204 | const real = (...args) => { 205 | // First up with a closure we increment the internal reference 206 | // count. This ensures that the Rust closure environment won't 207 | // be deallocated while we're invoking it. 208 | state.cnt++; 209 | const a = state.a; 210 | state.a = 0; 211 | try { 212 | return f(a, state.b, ...args); 213 | } finally { 214 | if (--state.cnt === 0) { 215 | wasm.__wbindgen_export_2.get(state.dtor)(a, state.b); 216 | 217 | } else { 218 | state.a = a; 219 | } 220 | } 221 | }; 222 | real.original = state; 223 | 224 | return real; 225 | } 226 | 227 | let stack_pointer = 32; 228 | 229 | function addBorrowedObject(obj) { 230 | if (stack_pointer == 1) throw new Error('out of js stack'); 231 | heap[--stack_pointer] = obj; 232 | return stack_pointer; 233 | } 234 | function __wbg_adapter_21(arg0, arg1, arg2) { 235 | try { 236 | wasm._dyn_core__ops__function__FnMut___A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h6c1480f21af15b9d(arg0, arg1, addBorrowedObject(arg2)); 237 | } finally { 238 | heap[stack_pointer++] = undefined; 239 | } 240 | } 241 | 242 | function __wbg_adapter_24(arg0, arg1, arg2) { 243 | wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h8541ea54ad7ea27a(arg0, arg1, addHeapObject(arg2)); 244 | } 245 | 246 | function __wbg_adapter_27(arg0, arg1) { 247 | wasm._dyn_core__ops__function__FnMut_____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h467f168fe7c0222c(arg0, arg1); 248 | } 249 | 250 | function getCachedStringFromWasm0(ptr, len) { 251 | if (ptr === 0) { 252 | return getObject(len); 253 | } else { 254 | return getStringFromWasm0(ptr, len); 255 | } 256 | } 257 | 258 | function isLikeNone(x) { 259 | return x === undefined || x === null; 260 | } 261 | 262 | function handleError(f, args) { 263 | try { 264 | return f.apply(this, args); 265 | } catch (e) { 266 | wasm.__wbindgen_exn_store(addHeapObject(e)); 267 | } 268 | } 269 | 270 | async function load(module, imports) { 271 | if (typeof Response === 'function' && module instanceof Response) { 272 | if (typeof WebAssembly.instantiateStreaming === 'function') { 273 | try { 274 | return await WebAssembly.instantiateStreaming(module, imports); 275 | 276 | } catch (e) { 277 | if (module.headers.get('Content-Type') != 'application/wasm') { 278 | console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e); 279 | 280 | } else { 281 | throw e; 282 | } 283 | } 284 | } 285 | 286 | const bytes = await module.arrayBuffer(); 287 | return await WebAssembly.instantiate(bytes, imports); 288 | 289 | } else { 290 | const instance = await WebAssembly.instantiate(module, imports); 291 | 292 | if (instance instanceof WebAssembly.Instance) { 293 | return { instance, module }; 294 | 295 | } else { 296 | return instance; 297 | } 298 | } 299 | } 300 | 301 | async function init(input) { 302 | if (typeof input === 'undefined') { 303 | input = new URL('hooks_shared_state_bg.wasm', import.meta.url); 304 | } 305 | const imports = {}; 306 | imports.wbg = {}; 307 | imports.wbg.__wbindgen_cb_drop = function(arg0) { 308 | const obj = takeObject(arg0).original; 309 | if (obj.cnt-- == 1) { 310 | obj.a = 0; 311 | return true; 312 | } 313 | var ret = false; 314 | return ret; 315 | }; 316 | imports.wbg.__wbindgen_object_drop_ref = function(arg0) { 317 | takeObject(arg0); 318 | }; 319 | imports.wbg.__wbindgen_is_function = function(arg0) { 320 | var ret = typeof(getObject(arg0)) === 'function'; 321 | return ret; 322 | }; 323 | imports.wbg.__wbg_new_59cb74e423758ede = function() { 324 | var ret = new Error(); 325 | return addHeapObject(ret); 326 | }; 327 | imports.wbg.__wbg_stack_558ba5917b466edd = function(arg0, arg1) { 328 | var ret = getObject(arg1).stack; 329 | var ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); 330 | var len0 = WASM_VECTOR_LEN; 331 | getInt32Memory0()[arg0 / 4 + 1] = len0; 332 | getInt32Memory0()[arg0 / 4 + 0] = ptr0; 333 | }; 334 | imports.wbg.__wbg_error_4bb6c2a97407129a = function(arg0, arg1) { 335 | var v0 = getCachedStringFromWasm0(arg0, arg1); 336 | if (arg0 !== 0) { wasm.__wbindgen_free(arg0, arg1); } 337 | console.error(v0); 338 | }; 339 | imports.wbg.__wbindgen_string_new = function(arg0, arg1) { 340 | var ret = getStringFromWasm0(arg0, arg1); 341 | return addHeapObject(ret); 342 | }; 343 | imports.wbg.__wbindgen_object_clone_ref = function(arg0) { 344 | var ret = getObject(arg0); 345 | return addHeapObject(ret); 346 | }; 347 | imports.wbg.__wbg_Window_f826a1dec163bacb = function(arg0) { 348 | var ret = getObject(arg0).Window; 349 | return addHeapObject(ret); 350 | }; 351 | imports.wbg.__wbindgen_is_undefined = function(arg0) { 352 | var ret = getObject(arg0) === undefined; 353 | return ret; 354 | }; 355 | imports.wbg.__wbg_WorkerGlobalScope_967d186155183d38 = function(arg0) { 356 | var ret = getObject(arg0).WorkerGlobalScope; 357 | return addHeapObject(ret); 358 | }; 359 | imports.wbg.__wbg_instanceof_Window_c4b70662a0d2c5ec = function(arg0) { 360 | var ret = getObject(arg0) instanceof Window; 361 | return ret; 362 | }; 363 | imports.wbg.__wbg_document_1c64944725c0d81d = function(arg0) { 364 | var ret = getObject(arg0).document; 365 | return isLikeNone(ret) ? 0 : addHeapObject(ret); 366 | }; 367 | imports.wbg.__wbg_requestAnimationFrame_71638ca922068239 = function() { return handleError(function (arg0, arg1) { 368 | var ret = getObject(arg0).requestAnimationFrame(getObject(arg1)); 369 | return ret; 370 | }, arguments) }; 371 | imports.wbg.__wbg_requestIdleCallback_183ce6abc9f992af = function() { return handleError(function (arg0, arg1) { 372 | var ret = getObject(arg0).requestIdleCallback(getObject(arg1)); 373 | return ret; 374 | }, arguments) }; 375 | imports.wbg.__wbg_clearTimeout_2c1ba0016d8bca41 = function(arg0, arg1) { 376 | getObject(arg0).clearTimeout(arg1); 377 | }; 378 | imports.wbg.__wbg_setTimeout_df66d951b1726b78 = function() { return handleError(function (arg0, arg1, arg2) { 379 | var ret = getObject(arg0).setTimeout(getObject(arg1), arg2); 380 | return ret; 381 | }, arguments) }; 382 | imports.wbg.__wbg_createComment_710531dc62f02c0c = function(arg0, arg1, arg2) { 383 | var v0 = getCachedStringFromWasm0(arg1, arg2); 384 | var ret = getObject(arg0).createComment(v0); 385 | return addHeapObject(ret); 386 | }; 387 | imports.wbg.__wbg_createElement_86c152812a141a62 = function() { return handleError(function (arg0, arg1, arg2) { 388 | var v0 = getCachedStringFromWasm0(arg1, arg2); 389 | var ret = getObject(arg0).createElement(v0); 390 | return addHeapObject(ret); 391 | }, arguments) }; 392 | imports.wbg.__wbg_createElementNS_ae12b8681c3957a3 = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) { 393 | var v0 = getCachedStringFromWasm0(arg1, arg2); 394 | var v1 = getCachedStringFromWasm0(arg3, arg4); 395 | var ret = getObject(arg0).createElementNS(v0, v1); 396 | return addHeapObject(ret); 397 | }, arguments) }; 398 | imports.wbg.__wbg_createTextNode_365db3bc3d0523ab = function(arg0, arg1, arg2) { 399 | var v0 = getCachedStringFromWasm0(arg1, arg2); 400 | var ret = getObject(arg0).createTextNode(v0); 401 | return addHeapObject(ret); 402 | }; 403 | imports.wbg.__wbg_getElementById_f3e94458ce77f0d0 = function(arg0, arg1, arg2) { 404 | var v0 = getCachedStringFromWasm0(arg1, arg2); 405 | var ret = getObject(arg0).getElementById(v0); 406 | return isLikeNone(ret) ? 0 : addHeapObject(ret); 407 | }; 408 | imports.wbg.__wbg_querySelectorAll_7f26183d7dfc576e = function() { return handleError(function (arg0, arg1, arg2) { 409 | var v0 = getCachedStringFromWasm0(arg1, arg2); 410 | var ret = getObject(arg0).querySelectorAll(v0); 411 | return addHeapObject(ret); 412 | }, arguments) }; 413 | imports.wbg.__wbg_instanceof_HtmlInputElement_8cafe5f30dfdb6bc = function(arg0) { 414 | var ret = getObject(arg0) instanceof HTMLInputElement; 415 | return ret; 416 | }; 417 | imports.wbg.__wbg_checked_39d5ce76226024a7 = function(arg0) { 418 | var ret = getObject(arg0).checked; 419 | return ret; 420 | }; 421 | imports.wbg.__wbg_setchecked_206243371da58f6a = function(arg0, arg1) { 422 | getObject(arg0).checked = arg1 !== 0; 423 | }; 424 | imports.wbg.__wbg_type_ed14b550c9bab1c2 = function(arg0, arg1) { 425 | var ret = getObject(arg1).type; 426 | var ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); 427 | var len0 = WASM_VECTOR_LEN; 428 | getInt32Memory0()[arg0 / 4 + 1] = len0; 429 | getInt32Memory0()[arg0 / 4 + 0] = ptr0; 430 | }; 431 | imports.wbg.__wbg_value_0627d4b1c27534e6 = function(arg0, arg1) { 432 | var ret = getObject(arg1).value; 433 | var ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); 434 | var len0 = WASM_VECTOR_LEN; 435 | getInt32Memory0()[arg0 / 4 + 1] = len0; 436 | getInt32Memory0()[arg0 / 4 + 0] = ptr0; 437 | }; 438 | imports.wbg.__wbg_setvalue_2459f62386b6967f = function(arg0, arg1, arg2) { 439 | var v0 = getCachedStringFromWasm0(arg1, arg2); 440 | getObject(arg0).value = v0; 441 | }; 442 | imports.wbg.__wbg_instanceof_HtmlOptionElement_5d5f65dbc2cf3509 = function(arg0) { 443 | var ret = getObject(arg0) instanceof HTMLOptionElement; 444 | return ret; 445 | }; 446 | imports.wbg.__wbg_setselected_ed15d65374c26375 = function(arg0, arg1) { 447 | getObject(arg0).selected = arg1 !== 0; 448 | }; 449 | imports.wbg.__wbg_instanceof_KeyboardEvent_96bb8fcff2603455 = function(arg0) { 450 | var ret = getObject(arg0) instanceof KeyboardEvent; 451 | return ret; 452 | }; 453 | imports.wbg.__wbg_charCode_94907480ee48e8d5 = function(arg0) { 454 | var ret = getObject(arg0).charCode; 455 | return ret; 456 | }; 457 | imports.wbg.__wbg_keyCode_490ed69472addfdc = function(arg0) { 458 | var ret = getObject(arg0).keyCode; 459 | return ret; 460 | }; 461 | imports.wbg.__wbg_altKey_3dcb50d5afbc5036 = function(arg0) { 462 | var ret = getObject(arg0).altKey; 463 | return ret; 464 | }; 465 | imports.wbg.__wbg_ctrlKey_fb62ba10b63b34a4 = function(arg0) { 466 | var ret = getObject(arg0).ctrlKey; 467 | return ret; 468 | }; 469 | imports.wbg.__wbg_shiftKey_bd2875540e5db840 = function(arg0) { 470 | var ret = getObject(arg0).shiftKey; 471 | return ret; 472 | }; 473 | imports.wbg.__wbg_metaKey_94ca09e07f21f240 = function(arg0) { 474 | var ret = getObject(arg0).metaKey; 475 | return ret; 476 | }; 477 | imports.wbg.__wbg_location_5bab830bd31662a2 = function(arg0) { 478 | var ret = getObject(arg0).location; 479 | return ret; 480 | }; 481 | imports.wbg.__wbg_repeat_8004e40e8ff1a1f1 = function(arg0) { 482 | var ret = getObject(arg0).repeat; 483 | return ret; 484 | }; 485 | imports.wbg.__wbg_key_10dcaa4bb6d5449f = function(arg0, arg1) { 486 | var ret = getObject(arg1).key; 487 | var ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); 488 | var len0 = WASM_VECTOR_LEN; 489 | getInt32Memory0()[arg0 / 4 + 1] = len0; 490 | getInt32Memory0()[arg0 / 4 + 0] = ptr0; 491 | }; 492 | imports.wbg.__wbg_instanceof_Event_480a3ec3d45b75a6 = function(arg0) { 493 | var ret = getObject(arg0) instanceof Event; 494 | return ret; 495 | }; 496 | imports.wbg.__wbg_type_7a49279491e15d0a = function(arg0, arg1) { 497 | var ret = getObject(arg1).type; 498 | var ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); 499 | var len0 = WASM_VECTOR_LEN; 500 | getInt32Memory0()[arg0 / 4 + 1] = len0; 501 | getInt32Memory0()[arg0 / 4 + 0] = ptr0; 502 | }; 503 | imports.wbg.__wbg_target_cc69dde6c2d9ec90 = function(arg0) { 504 | var ret = getObject(arg0).target; 505 | return isLikeNone(ret) ? 0 : addHeapObject(ret); 506 | }; 507 | imports.wbg.__wbg_instanceof_HtmlSelectElement_27fb687660e6b5ba = function(arg0) { 508 | var ret = getObject(arg0) instanceof HTMLSelectElement; 509 | return ret; 510 | }; 511 | imports.wbg.__wbg_value_bc4bb925ad58795b = function(arg0, arg1) { 512 | var ret = getObject(arg1).value; 513 | var ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); 514 | var len0 = WASM_VECTOR_LEN; 515 | getInt32Memory0()[arg0 / 4 + 1] = len0; 516 | getInt32Memory0()[arg0 / 4 + 0] = ptr0; 517 | }; 518 | imports.wbg.__wbg_length_62e6735d81b8b0f1 = function(arg0) { 519 | var ret = getObject(arg0).length; 520 | return ret; 521 | }; 522 | imports.wbg.__wbg_get_3bae69d3ceb47cc3 = function(arg0, arg1) { 523 | var ret = getObject(arg0)[arg1 >>> 0]; 524 | return isLikeNone(ret) ? 0 : addHeapObject(ret); 525 | }; 526 | imports.wbg.__wbg_instanceof_Node_ea9524a9184ff6d0 = function(arg0) { 527 | var ret = getObject(arg0) instanceof Node; 528 | return ret; 529 | }; 530 | imports.wbg.__wbg_parentNode_cd9f47bcc933ed0e = function(arg0) { 531 | var ret = getObject(arg0).parentNode; 532 | return isLikeNone(ret) ? 0 : addHeapObject(ret); 533 | }; 534 | imports.wbg.__wbg_textContent_eef491bffc43d8d6 = function(arg0, arg1) { 535 | var ret = getObject(arg1).textContent; 536 | var ptr0 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); 537 | var len0 = WASM_VECTOR_LEN; 538 | getInt32Memory0()[arg0 / 4 + 1] = len0; 539 | getInt32Memory0()[arg0 / 4 + 0] = ptr0; 540 | }; 541 | imports.wbg.__wbg_settextContent_799ebbf96e16265d = function(arg0, arg1, arg2) { 542 | var v0 = getCachedStringFromWasm0(arg1, arg2); 543 | getObject(arg0).textContent = v0; 544 | }; 545 | imports.wbg.__wbg_appendChild_d318db34c4559916 = function() { return handleError(function (arg0, arg1) { 546 | var ret = getObject(arg0).appendChild(getObject(arg1)); 547 | return addHeapObject(ret); 548 | }, arguments) }; 549 | imports.wbg.__wbg_insertBefore_5b314357408fbec1 = function() { return handleError(function (arg0, arg1, arg2) { 550 | var ret = getObject(arg0).insertBefore(getObject(arg1), getObject(arg2)); 551 | return addHeapObject(ret); 552 | }, arguments) }; 553 | imports.wbg.__wbg_removeChild_d3ca7b53e537867e = function() { return handleError(function (arg0, arg1) { 554 | var ret = getObject(arg0).removeChild(getObject(arg1)); 555 | return addHeapObject(ret); 556 | }, arguments) }; 557 | imports.wbg.__wbg_instanceof_Text_8d7df795b9c7f8f9 = function(arg0) { 558 | var ret = getObject(arg0) instanceof Text; 559 | return ret; 560 | }; 561 | imports.wbg.__wbg_pageX_9b9d392e5134f3db = function(arg0) { 562 | var ret = getObject(arg0).pageX; 563 | return ret; 564 | }; 565 | imports.wbg.__wbg_pageY_ee824e6b949cdd34 = function(arg0) { 566 | var ret = getObject(arg0).pageY; 567 | return ret; 568 | }; 569 | imports.wbg.__wbg_which_222325717b841326 = function(arg0) { 570 | var ret = getObject(arg0).which; 571 | return ret; 572 | }; 573 | imports.wbg.__wbg_instanceof_MouseEvent_7cb2c66c3440bfb0 = function(arg0) { 574 | var ret = getObject(arg0) instanceof MouseEvent; 575 | return ret; 576 | }; 577 | imports.wbg.__wbg_screenX_6d3e727e1e71c8ba = function(arg0) { 578 | var ret = getObject(arg0).screenX; 579 | return ret; 580 | }; 581 | imports.wbg.__wbg_screenY_f7ccb175523cee84 = function(arg0) { 582 | var ret = getObject(arg0).screenY; 583 | return ret; 584 | }; 585 | imports.wbg.__wbg_clientX_97ff0f5c7b19e687 = function(arg0) { 586 | var ret = getObject(arg0).clientX; 587 | return ret; 588 | }; 589 | imports.wbg.__wbg_clientY_cacd4a7e44b9719b = function(arg0) { 590 | var ret = getObject(arg0).clientY; 591 | return ret; 592 | }; 593 | imports.wbg.__wbg_ctrlKey_9761d22fa42f09c0 = function(arg0) { 594 | var ret = getObject(arg0).ctrlKey; 595 | return ret; 596 | }; 597 | imports.wbg.__wbg_shiftKey_78ee0fc1aa572c2e = function(arg0) { 598 | var ret = getObject(arg0).shiftKey; 599 | return ret; 600 | }; 601 | imports.wbg.__wbg_altKey_8936038d973c56db = function(arg0) { 602 | var ret = getObject(arg0).altKey; 603 | return ret; 604 | }; 605 | imports.wbg.__wbg_metaKey_e6b9e0aa35aa2974 = function(arg0) { 606 | var ret = getObject(arg0).metaKey; 607 | return ret; 608 | }; 609 | imports.wbg.__wbg_button_a02c0467d38e8338 = function(arg0) { 610 | var ret = getObject(arg0).button; 611 | return ret; 612 | }; 613 | imports.wbg.__wbg_buttons_9d7b6c334f0b37de = function(arg0) { 614 | var ret = getObject(arg0).buttons; 615 | return ret; 616 | }; 617 | imports.wbg.__wbg_instanceof_CompositionEvent_e1cd64fd98d4425d = function(arg0) { 618 | var ret = getObject(arg0) instanceof CompositionEvent; 619 | return ret; 620 | }; 621 | imports.wbg.__wbg_data_dbff09eb89176161 = function(arg0, arg1) { 622 | var ret = getObject(arg1).data; 623 | var ptr0 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); 624 | var len0 = WASM_VECTOR_LEN; 625 | getInt32Memory0()[arg0 / 4 + 1] = len0; 626 | getInt32Memory0()[arg0 / 4 + 0] = ptr0; 627 | }; 628 | imports.wbg.__wbg_setProperty_1460c660bc329763 = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) { 629 | var v0 = getCachedStringFromWasm0(arg1, arg2); 630 | var v1 = getCachedStringFromWasm0(arg3, arg4); 631 | getObject(arg0).setProperty(v0, v1); 632 | }, arguments) }; 633 | imports.wbg.__wbg_addEventListener_52721772cc0a7f30 = function() { return handleError(function (arg0, arg1, arg2, arg3) { 634 | var v0 = getCachedStringFromWasm0(arg1, arg2); 635 | getObject(arg0).addEventListener(v0, getObject(arg3)); 636 | }, arguments) }; 637 | imports.wbg.__wbg_instanceof_IdleDeadline_e91ef932d81715c3 = function(arg0) { 638 | var ret = getObject(arg0) instanceof IdleDeadline; 639 | return ret; 640 | }; 641 | imports.wbg.__wbg_timeRemaining_6fa9d12d6d15e73e = function(arg0) { 642 | var ret = getObject(arg0).timeRemaining(); 643 | return ret; 644 | }; 645 | imports.wbg.__wbg_instanceof_TouchEvent_0ab696cda73333ac = function(arg0) { 646 | var ret = getObject(arg0) instanceof TouchEvent; 647 | return ret; 648 | }; 649 | imports.wbg.__wbg_altKey_7cfb90a08ce4db73 = function(arg0) { 650 | var ret = getObject(arg0).altKey; 651 | return ret; 652 | }; 653 | imports.wbg.__wbg_metaKey_49d641fb7c6fd755 = function(arg0) { 654 | var ret = getObject(arg0).metaKey; 655 | return ret; 656 | }; 657 | imports.wbg.__wbg_ctrlKey_2008616a1339c848 = function(arg0) { 658 | var ret = getObject(arg0).ctrlKey; 659 | return ret; 660 | }; 661 | imports.wbg.__wbg_shiftKey_aa89958b58ad5242 = function(arg0) { 662 | var ret = getObject(arg0).shiftKey; 663 | return ret; 664 | }; 665 | imports.wbg.__wbg_instanceof_WheelEvent_41916fe296da5b3c = function(arg0) { 666 | var ret = getObject(arg0) instanceof WheelEvent; 667 | return ret; 668 | }; 669 | imports.wbg.__wbg_deltaX_8cfc6cd15e97d97c = function(arg0) { 670 | var ret = getObject(arg0).deltaX; 671 | return ret; 672 | }; 673 | imports.wbg.__wbg_deltaY_080604c20160c0e8 = function(arg0) { 674 | var ret = getObject(arg0).deltaY; 675 | return ret; 676 | }; 677 | imports.wbg.__wbg_deltaZ_2a27c02ace57a19e = function(arg0) { 678 | var ret = getObject(arg0).deltaZ; 679 | return ret; 680 | }; 681 | imports.wbg.__wbg_deltaMode_c5ec1ee518ea0a08 = function(arg0) { 682 | var ret = getObject(arg0).deltaMode; 683 | return ret; 684 | }; 685 | imports.wbg.__wbg_instanceof_Element_97d85e53f1805b82 = function(arg0) { 686 | var ret = getObject(arg0) instanceof Element; 687 | return ret; 688 | }; 689 | imports.wbg.__wbg_getAttribute_bb1d602e925e860a = function(arg0, arg1, arg2, arg3) { 690 | var v0 = getCachedStringFromWasm0(arg2, arg3); 691 | var ret = getObject(arg1).getAttribute(v0); 692 | var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); 693 | var len1 = WASM_VECTOR_LEN; 694 | getInt32Memory0()[arg0 / 4 + 1] = len1; 695 | getInt32Memory0()[arg0 / 4 + 0] = ptr1; 696 | }; 697 | imports.wbg.__wbg_removeAttribute_eea03ed128669b8f = function() { return handleError(function (arg0, arg1, arg2) { 698 | var v0 = getCachedStringFromWasm0(arg1, arg2); 699 | getObject(arg0).removeAttribute(v0); 700 | }, arguments) }; 701 | imports.wbg.__wbg_setAttribute_1b533bf07966de55 = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) { 702 | var v0 = getCachedStringFromWasm0(arg1, arg2); 703 | var v1 = getCachedStringFromWasm0(arg3, arg4); 704 | getObject(arg0).setAttribute(v0, v1); 705 | }, arguments) }; 706 | imports.wbg.__wbg_after_41c4547474e5c7b2 = function() { return handleError(function (arg0, arg1) { 707 | getObject(arg0).after(...getObject(arg1)); 708 | }, arguments) }; 709 | imports.wbg.__wbg_before_9614b2163991f8df = function() { return handleError(function (arg0, arg1) { 710 | getObject(arg0).before(...getObject(arg1)); 711 | }, arguments) }; 712 | imports.wbg.__wbg_remove_c63cabc94a77cacb = function(arg0) { 713 | getObject(arg0).remove(); 714 | }; 715 | imports.wbg.__wbg_replaceWith_1272417ed0a0204e = function() { return handleError(function (arg0, arg1) { 716 | getObject(arg0).replaceWith(...getObject(arg1)); 717 | }, arguments) }; 718 | imports.wbg.__wbg_debug_f6147a62af5fb117 = function(arg0, arg1, arg2, arg3) { 719 | console.debug(getObject(arg0), getObject(arg1), getObject(arg2), getObject(arg3)); 720 | }; 721 | imports.wbg.__wbg_error_cc38ce2b4b661e1d = function(arg0) { 722 | console.error(getObject(arg0)); 723 | }; 724 | imports.wbg.__wbg_error_8b4a1487636c965d = function(arg0, arg1, arg2, arg3) { 725 | console.error(getObject(arg0), getObject(arg1), getObject(arg2), getObject(arg3)); 726 | }; 727 | imports.wbg.__wbg_info_74a03c22e1fa6688 = function(arg0, arg1, arg2, arg3) { 728 | console.info(getObject(arg0), getObject(arg1), getObject(arg2), getObject(arg3)); 729 | }; 730 | imports.wbg.__wbg_log_ad41dbc3d891c2dc = function(arg0, arg1, arg2, arg3) { 731 | console.log(getObject(arg0), getObject(arg1), getObject(arg2), getObject(arg3)); 732 | }; 733 | imports.wbg.__wbg_warn_c1cc594c33944c11 = function(arg0, arg1, arg2, arg3) { 734 | console.warn(getObject(arg0), getObject(arg1), getObject(arg2), getObject(arg3)); 735 | }; 736 | imports.wbg.__wbg_instanceof_HtmlElement_df66c8b4a687aa43 = function(arg0) { 737 | var ret = getObject(arg0) instanceof HTMLElement; 738 | return ret; 739 | }; 740 | imports.wbg.__wbg_style_c88e323890d3a091 = function(arg0) { 741 | var ret = getObject(arg0).style; 742 | return addHeapObject(ret); 743 | }; 744 | imports.wbg.__wbg_clearTimeout_c1f246d6874c0679 = function(arg0, arg1) { 745 | getObject(arg0).clearTimeout(arg1); 746 | }; 747 | imports.wbg.__wbg_setTimeout_5314850184d61a44 = function() { return handleError(function (arg0, arg1, arg2) { 748 | var ret = getObject(arg0).setTimeout(getObject(arg1), arg2); 749 | return ret; 750 | }, arguments) }; 751 | imports.wbg.__wbg_instanceof_CharacterData_923547385daf396d = function(arg0) { 752 | var ret = getObject(arg0) instanceof CharacterData; 753 | return ret; 754 | }; 755 | imports.wbg.__wbg_after_c472195dda6b326c = function() { return handleError(function (arg0, arg1) { 756 | getObject(arg0).after(...getObject(arg1)); 757 | }, arguments) }; 758 | imports.wbg.__wbg_before_fb2c3a2663318a78 = function() { return handleError(function (arg0, arg1) { 759 | getObject(arg0).before(...getObject(arg1)); 760 | }, arguments) }; 761 | imports.wbg.__wbg_replaceWith_2c48dcaa450a6671 = function() { return handleError(function (arg0, arg1) { 762 | getObject(arg0).replaceWith(...getObject(arg1)); 763 | }, arguments) }; 764 | imports.wbg.__wbg_instanceof_DocumentType_0e86823a57dde305 = function(arg0) { 765 | var ret = getObject(arg0) instanceof DocumentType; 766 | return ret; 767 | }; 768 | imports.wbg.__wbg_after_c0743062e8170d00 = function() { return handleError(function (arg0, arg1) { 769 | getObject(arg0).after(...getObject(arg1)); 770 | }, arguments) }; 771 | imports.wbg.__wbg_before_52cbfbd938a19b42 = function() { return handleError(function (arg0, arg1) { 772 | getObject(arg0).before(...getObject(arg1)); 773 | }, arguments) }; 774 | imports.wbg.__wbg_replaceWith_8c24775c6a372049 = function() { return handleError(function (arg0, arg1) { 775 | getObject(arg0).replaceWith(...getObject(arg1)); 776 | }, arguments) }; 777 | imports.wbg.__wbg_instanceof_AnimationEvent_741ec19d568ec9b8 = function(arg0) { 778 | var ret = getObject(arg0) instanceof AnimationEvent; 779 | return ret; 780 | }; 781 | imports.wbg.__wbg_animationName_e6b926c751875085 = function(arg0, arg1) { 782 | var ret = getObject(arg1).animationName; 783 | var ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); 784 | var len0 = WASM_VECTOR_LEN; 785 | getInt32Memory0()[arg0 / 4 + 1] = len0; 786 | getInt32Memory0()[arg0 / 4 + 0] = ptr0; 787 | }; 788 | imports.wbg.__wbg_elapsedTime_4b480f2e6b2791cd = function(arg0) { 789 | var ret = getObject(arg0).elapsedTime; 790 | return ret; 791 | }; 792 | imports.wbg.__wbg_pseudoElement_7c2bfde68d10a4b6 = function(arg0, arg1) { 793 | var ret = getObject(arg1).pseudoElement; 794 | var ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); 795 | var len0 = WASM_VECTOR_LEN; 796 | getInt32Memory0()[arg0 / 4 + 1] = len0; 797 | getInt32Memory0()[arg0 / 4 + 0] = ptr0; 798 | }; 799 | imports.wbg.__wbg_instanceof_HtmlTextAreaElement_c2f3b4bd6871d5ad = function(arg0) { 800 | var ret = getObject(arg0) instanceof HTMLTextAreaElement; 801 | return ret; 802 | }; 803 | imports.wbg.__wbg_value_686b2a68422cb88d = function(arg0, arg1) { 804 | var ret = getObject(arg1).value; 805 | var ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); 806 | var len0 = WASM_VECTOR_LEN; 807 | getInt32Memory0()[arg0 / 4 + 1] = len0; 808 | getInt32Memory0()[arg0 / 4 + 0] = ptr0; 809 | }; 810 | imports.wbg.__wbg_setvalue_0a07023245efa3cc = function(arg0, arg1, arg2) { 811 | var v0 = getCachedStringFromWasm0(arg1, arg2); 812 | getObject(arg0).value = v0; 813 | }; 814 | imports.wbg.__wbg_instanceof_PointerEvent_a131c52e2d8689a4 = function(arg0) { 815 | var ret = getObject(arg0) instanceof PointerEvent; 816 | return ret; 817 | }; 818 | imports.wbg.__wbg_pointerId_9302f0e125f0b48e = function(arg0) { 819 | var ret = getObject(arg0).pointerId; 820 | return ret; 821 | }; 822 | imports.wbg.__wbg_width_e3c349a44d741557 = function(arg0) { 823 | var ret = getObject(arg0).width; 824 | return ret; 825 | }; 826 | imports.wbg.__wbg_height_b04227a17ca32a40 = function(arg0) { 827 | var ret = getObject(arg0).height; 828 | return ret; 829 | }; 830 | imports.wbg.__wbg_pressure_ccb6528a41a7988e = function(arg0) { 831 | var ret = getObject(arg0).pressure; 832 | return ret; 833 | }; 834 | imports.wbg.__wbg_tangentialPressure_7e9160a69763fa5a = function(arg0) { 835 | var ret = getObject(arg0).tangentialPressure; 836 | return ret; 837 | }; 838 | imports.wbg.__wbg_tiltX_e72f0f8d2db90a01 = function(arg0) { 839 | var ret = getObject(arg0).tiltX; 840 | return ret; 841 | }; 842 | imports.wbg.__wbg_tiltY_c632d7fa14c6264a = function(arg0) { 843 | var ret = getObject(arg0).tiltY; 844 | return ret; 845 | }; 846 | imports.wbg.__wbg_twist_10483210c2b4bf9f = function(arg0) { 847 | var ret = getObject(arg0).twist; 848 | return ret; 849 | }; 850 | imports.wbg.__wbg_pointerType_2b40a4fc77c24d55 = function(arg0, arg1) { 851 | var ret = getObject(arg1).pointerType; 852 | var ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); 853 | var len0 = WASM_VECTOR_LEN; 854 | getInt32Memory0()[arg0 / 4 + 1] = len0; 855 | getInt32Memory0()[arg0 / 4 + 0] = ptr0; 856 | }; 857 | imports.wbg.__wbg_isPrimary_1b61e5694cebdb4a = function(arg0) { 858 | var ret = getObject(arg0).isPrimary; 859 | return ret; 860 | }; 861 | imports.wbg.__wbg_instanceof_TransitionEvent_a0154619d908db2f = function(arg0) { 862 | var ret = getObject(arg0) instanceof TransitionEvent; 863 | return ret; 864 | }; 865 | imports.wbg.__wbg_propertyName_05bec2f4569cb7fe = function(arg0, arg1) { 866 | var ret = getObject(arg1).propertyName; 867 | var ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); 868 | var len0 = WASM_VECTOR_LEN; 869 | getInt32Memory0()[arg0 / 4 + 1] = len0; 870 | getInt32Memory0()[arg0 / 4 + 0] = ptr0; 871 | }; 872 | imports.wbg.__wbg_elapsedTime_6f18fd8e77d055cd = function(arg0) { 873 | var ret = getObject(arg0).elapsedTime; 874 | return ret; 875 | }; 876 | imports.wbg.__wbg_pseudoElement_08a057e1e2493048 = function(arg0, arg1) { 877 | var ret = getObject(arg1).pseudoElement; 878 | var ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); 879 | var len0 = WASM_VECTOR_LEN; 880 | getInt32Memory0()[arg0 / 4 + 1] = len0; 881 | getInt32Memory0()[arg0 / 4 + 0] = ptr0; 882 | }; 883 | imports.wbg.__wbg_new_949bbc1147195c4e = function() { 884 | var ret = new Array(); 885 | return addHeapObject(ret); 886 | }; 887 | imports.wbg.__wbg_newnoargs_be86524d73f67598 = function(arg0, arg1) { 888 | var v0 = getCachedStringFromWasm0(arg0, arg1); 889 | var ret = new Function(v0); 890 | return addHeapObject(ret); 891 | }; 892 | imports.wbg.__wbg_call_888d259a5fefc347 = function() { return handleError(function (arg0, arg1) { 893 | var ret = getObject(arg0).call(getObject(arg1)); 894 | return addHeapObject(ret); 895 | }, arguments) }; 896 | imports.wbg.__wbg_push_284486ca27c6aa8b = function(arg0, arg1) { 897 | var ret = getObject(arg0).push(getObject(arg1)); 898 | return ret; 899 | }; 900 | imports.wbg.__wbg_instanceof_Object_66786225e0dbc8ba = function(arg0) { 901 | var ret = getObject(arg0) instanceof Object; 902 | return ret; 903 | }; 904 | imports.wbg.__wbg_hasOwnProperty_9060fa1885e12d11 = function(arg0, arg1) { 905 | var ret = getObject(arg0).hasOwnProperty(getObject(arg1)); 906 | return ret; 907 | }; 908 | imports.wbg.__wbg_resolve_d23068002f584f22 = function(arg0) { 909 | var ret = Promise.resolve(getObject(arg0)); 910 | return addHeapObject(ret); 911 | }; 912 | imports.wbg.__wbg_then_2fcac196782070cc = function(arg0, arg1) { 913 | var ret = getObject(arg0).then(getObject(arg1)); 914 | return addHeapObject(ret); 915 | }; 916 | imports.wbg.__wbg_self_c6fbdfc2918d5e58 = function() { return handleError(function () { 917 | var ret = self.self; 918 | return addHeapObject(ret); 919 | }, arguments) }; 920 | imports.wbg.__wbg_window_baec038b5ab35c54 = function() { return handleError(function () { 921 | var ret = window.window; 922 | return addHeapObject(ret); 923 | }, arguments) }; 924 | imports.wbg.__wbg_globalThis_3f735a5746d41fbd = function() { return handleError(function () { 925 | var ret = globalThis.globalThis; 926 | return addHeapObject(ret); 927 | }, arguments) }; 928 | imports.wbg.__wbg_global_1bc0b39582740e95 = function() { return handleError(function () { 929 | var ret = global.global; 930 | return addHeapObject(ret); 931 | }, arguments) }; 932 | imports.wbg.__wbindgen_debug_string = function(arg0, arg1) { 933 | var ret = debugString(getObject(arg1)); 934 | var ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); 935 | var len0 = WASM_VECTOR_LEN; 936 | getInt32Memory0()[arg0 / 4 + 1] = len0; 937 | getInt32Memory0()[arg0 / 4 + 0] = ptr0; 938 | }; 939 | imports.wbg.__wbindgen_throw = function(arg0, arg1) { 940 | throw new Error(getStringFromWasm0(arg0, arg1)); 941 | }; 942 | imports.wbg.__wbindgen_closure_wrapper444 = function(arg0, arg1, arg2) { 943 | var ret = makeClosure(arg0, arg1, 160, __wbg_adapter_18); 944 | return addHeapObject(ret); 945 | }; 946 | imports.wbg.__wbindgen_closure_wrapper446 = function(arg0, arg1, arg2) { 947 | var ret = makeMutClosure(arg0, arg1, 160, __wbg_adapter_21); 948 | return addHeapObject(ret); 949 | }; 950 | imports.wbg.__wbindgen_closure_wrapper528 = function(arg0, arg1, arg2) { 951 | var ret = makeMutClosure(arg0, arg1, 210, __wbg_adapter_24); 952 | return addHeapObject(ret); 953 | }; 954 | imports.wbg.__wbindgen_closure_wrapper566 = function(arg0, arg1, arg2) { 955 | var ret = makeMutClosure(arg0, arg1, 228, __wbg_adapter_27); 956 | return addHeapObject(ret); 957 | }; 958 | 959 | if (typeof input === 'string' || (typeof Request === 'function' && input instanceof Request) || (typeof URL === 'function' && input instanceof URL)) { 960 | input = fetch(input); 961 | } 962 | 963 | 964 | 965 | const { instance, module } = await load(await input, imports); 966 | 967 | wasm = instance.exports; 968 | init.__wbindgen_wasm_module = module; 969 | wasm.__wbindgen_start(); 970 | return wasm; 971 | } 972 | 973 | export default init; 974 | 975 | -------------------------------------------------------------------------------- /todomvc/docs/wasm/module_bg.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DioxusLabs/example-projects/db87199d2fcdbdd70cf5b6bed5b156ac35fb1e91/todomvc/docs/wasm/module_bg.wasm -------------------------------------------------------------------------------- /todomvc/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DioxusLabs/example-projects/db87199d2fcdbdd70cf5b6bed5b156ac35fb1e91/todomvc/example.png -------------------------------------------------------------------------------- /todomvc/src/lib.rs: -------------------------------------------------------------------------------- 1 | use dioxus::{html::input_data::keyboard_types::Key, prelude::*}; 2 | 3 | #[derive(PartialEq)] 4 | pub enum FilterState { 5 | All, 6 | Active, 7 | Completed, 8 | } 9 | 10 | pub type Todos = im_rc::HashMap; 11 | 12 | #[derive(Debug, PartialEq, Clone)] 13 | pub struct TodoItem { 14 | pub id: u32, 15 | pub checked: bool, 16 | pub contents: String, 17 | } 18 | 19 | pub fn app(cx: Scope<()>) -> Element { 20 | let todos = use_ref(cx, im_rc::HashMap::::default); 21 | let filter = use_state(cx, || FilterState::All); 22 | let draft = use_ref(cx, String::new); 23 | let todo_id = use_state(cx, || 0); 24 | 25 | // Filter the todos based on the filter state 26 | let mut filtered_todos = todos 27 | .read() 28 | .iter() 29 | .filter(|(_, item)| match filter.get() { 30 | FilterState::All => true, 31 | FilterState::Active => !item.checked, 32 | FilterState::Completed => item.checked, 33 | }) 34 | .map(|f| *f.0) 35 | .collect::>(); 36 | filtered_todos.sort_unstable(); 37 | 38 | let show_clear_completed = todos.read().values().any(|todo| todo.checked); 39 | let items_left = filtered_todos.len(); 40 | let item_text = match items_left { 41 | 1 => "item", 42 | _ => "items", 43 | }; 44 | 45 | cx.render(rsx!{ 46 | section { class: "todoapp", 47 | style { include_str!("../src/style.css") } 48 | div { 49 | header { class: "header", 50 | h1 {"todos"} 51 | input { 52 | class: "new-todo", 53 | placeholder: "What needs to be done?", 54 | value: "{draft.read()}", 55 | autofocus: "true", 56 | oninput: move |evt| draft.set(evt.value.clone()), 57 | onkeydown: move |evt| { 58 | if evt.key() == Key::Enter && !draft.read().is_empty() { 59 | todos.write().insert( 60 | *todo_id.get(), 61 | TodoItem { 62 | id: *todo_id.get(), 63 | checked: false, 64 | contents: draft.read().clone(), 65 | }, 66 | ); 67 | todo_id.set(todo_id + 1); 68 | draft.set("".to_string()); 69 | } 70 | } 71 | } 72 | } 73 | ul { class: "todo-list", 74 | filtered_todos.iter().map(|id| rsx!(todo_entry{ key: "{id}", id: *id, set_todos: todos })) 75 | } 76 | if !todos.read().is_empty() { 77 | rsx! { 78 | footer { class: "footer", 79 | span { class: "todo-count", 80 | strong {"{items_left} "} 81 | span {"{item_text} left"} 82 | } 83 | ul { class: "filters", 84 | li { class: "All", a { onclick: move |_| filter.set(FilterState::All), "All" }} 85 | li { class: "Active", a { onclick: move |_| filter.set(FilterState::Active), "Active" }} 86 | li { class: "Completed", a { onclick: move |_| filter.set(FilterState::Completed), "Completed" }} 87 | } 88 | if show_clear_completed { 89 | rsx!( 90 | button { 91 | class: "clear-completed", 92 | onclick: move |_| todos.write().retain(|_, todo| !todo.checked), 93 | "Clear completed" 94 | } 95 | ) 96 | } 97 | } 98 | } 99 | } 100 | } 101 | } 102 | footer { class: "info", 103 | p {"Double-click to edit a todo"} 104 | p { "Created by ", a { href: "http://github.com/jkelleyrtp/", "jkelleyrtp" }} 105 | p { "Part of ", a { href: "http://todomvc.com", "TodoMVC" }} 106 | } 107 | }) 108 | } 109 | 110 | #[derive(Props)] 111 | pub struct TodoEntryProps<'a> { 112 | set_todos: &'a UseRef, 113 | id: u32, 114 | } 115 | 116 | pub fn todo_entry<'a>(cx: Scope<'a, TodoEntryProps<'a>>) -> Element { 117 | let editing = use_state(cx, || false); 118 | 119 | let todos = cx.props.set_todos.read(); 120 | let todo = &todos[&cx.props.id]; 121 | let is_checked = todo.checked; 122 | let completed = if is_checked { "completed" } else { "" }; 123 | let is_editing = (**editing).then_some("editing").unwrap_or_default(); 124 | 125 | render!(li { 126 | class: "{completed} {is_editing}", 127 | onclick: move |_| { 128 | if !is_checked { 129 | editing.set(true) 130 | } 131 | }, 132 | onfocusout: move |_| editing.set(false), 133 | div { class: "view", 134 | input { class: "toggle", r#type: "checkbox", id: "cbg-{todo.id}", checked: "{is_checked}", 135 | onchange: move |evt| { 136 | cx.props.set_todos.write()[&cx.props.id].checked = evt.value.parse().unwrap(); 137 | } 138 | } 139 | label { r#for: "cbg-{todo.id}", pointer_events: "none", "{todo.contents}" } 140 | } 141 | if **editing { 142 | rsx!{ 143 | input { 144 | class: "edit", 145 | value: "{todo.contents}", 146 | oninput: move |evt| cx.props.set_todos.write()[&cx.props.id].contents = evt.value.clone(), 147 | autofocus: "true", 148 | onkeydown: move |evt| { 149 | match evt.key().to_string().as_str() { 150 | "Enter" | "Escape" | "Tab" => editing.set(false), 151 | _ => {} 152 | } 153 | }, 154 | } 155 | } 156 | } 157 | }) 158 | } 159 | -------------------------------------------------------------------------------- /todomvc/src/main.rs: -------------------------------------------------------------------------------- 1 | //! trunk only lets main.rs, not any binary 2 | //! 3 | //! 4 | use todomvc::*; 5 | 6 | pub fn main() { 7 | dioxus_web::launch(app); 8 | } 9 | -------------------------------------------------------------------------------- /todomvc/src/style.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | margin: 0; 4 | padding: 0; 5 | } 6 | 7 | button { 8 | margin: 0; 9 | padding: 0; 10 | border: 0; 11 | background: none; 12 | font-size: 100%; 13 | vertical-align: baseline; 14 | font-family: inherit; 15 | font-weight: inherit; 16 | color: inherit; 17 | -webkit-appearance: none; 18 | appearance: none; 19 | -webkit-font-smoothing: antialiased; 20 | -moz-osx-font-smoothing: grayscale; 21 | } 22 | 23 | body { 24 | font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; 25 | line-height: 1.4em; 26 | background: #f5f5f5; 27 | color: #4d4d4d; 28 | min-width: 230px; 29 | max-width: 550px; 30 | margin: 0 auto; 31 | -webkit-font-smoothing: antialiased; 32 | -moz-osx-font-smoothing: grayscale; 33 | font-weight: 300; 34 | } 35 | 36 | :focus { 37 | outline: 0; 38 | } 39 | 40 | .hidden { 41 | display: none; 42 | } 43 | 44 | .todoapp { 45 | background: #fff; 46 | margin: 130px 0 40px 0; 47 | position: relative; 48 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 49 | 0 25px 50px 0 rgba(0, 0, 0, 0.1); 50 | } 51 | 52 | .todoapp input::-webkit-input-placeholder { 53 | font-style: italic; 54 | font-weight: 300; 55 | color: #e6e6e6; 56 | } 57 | 58 | .todoapp input::-moz-placeholder { 59 | font-style: italic; 60 | font-weight: 300; 61 | color: #e6e6e6; 62 | } 63 | 64 | .todoapp input::input-placeholder { 65 | font-style: italic; 66 | font-weight: 300; 67 | color: #e6e6e6; 68 | } 69 | 70 | .todoapp h1 { 71 | position: absolute; 72 | top: -155px; 73 | width: 100%; 74 | font-size: 100px; 75 | font-weight: 100; 76 | text-align: center; 77 | color: rgba(175, 47, 47, 0.15); 78 | -webkit-text-rendering: optimizeLegibility; 79 | -moz-text-rendering: optimizeLegibility; 80 | text-rendering: optimizeLegibility; 81 | } 82 | 83 | .new-todo, 84 | .edit { 85 | position: relative; 86 | margin: 0; 87 | width: 100%; 88 | font-size: 24px; 89 | font-family: inherit; 90 | font-weight: inherit; 91 | line-height: 1.4em; 92 | border: 0; 93 | color: inherit; 94 | padding: 6px; 95 | border: 1px solid #999; 96 | box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); 97 | box-sizing: border-box; 98 | -webkit-font-smoothing: antialiased; 99 | -moz-osx-font-smoothing: grayscale; 100 | } 101 | 102 | .new-todo { 103 | padding: 16px 16px 16px 60px; 104 | border: none; 105 | background: rgba(0, 0, 0, 0.003); 106 | box-shadow: inset 0 -2px 1px rgba(0, 0, 0, 0.03); 107 | } 108 | 109 | .main { 110 | position: relative; 111 | z-index: 2; 112 | border-top: 1px solid #e6e6e6; 113 | } 114 | 115 | .toggle-all { 116 | text-align: center; 117 | border: none; 118 | /* Mobile Safari */ 119 | opacity: 0; 120 | position: absolute; 121 | } 122 | 123 | .toggle-all+label { 124 | width: 60px; 125 | height: 34px; 126 | font-size: 0; 127 | position: absolute; 128 | top: -52px; 129 | left: -13px; 130 | -webkit-transform: rotate(90deg); 131 | transform: rotate(90deg); 132 | } 133 | 134 | .toggle-all+label:before { 135 | content: '❯'; 136 | font-size: 22px; 137 | color: #e6e6e6; 138 | padding: 10px 27px 10px 27px; 139 | } 140 | 141 | .toggle-all:checked+label:before { 142 | color: #737373; 143 | } 144 | 145 | .todo-list { 146 | margin: 0; 147 | padding: 0; 148 | list-style: none; 149 | } 150 | 151 | .todo-list li { 152 | position: relative; 153 | font-size: 24px; 154 | border-bottom: 1px solid #ededed; 155 | } 156 | 157 | .todo-list li:last-child { 158 | border-bottom: none; 159 | } 160 | 161 | .todo-list li.editing { 162 | border-bottom: none; 163 | padding: 0; 164 | } 165 | 166 | .todo-list li.editing .edit { 167 | display: block; 168 | width: 506px; 169 | padding: 12px 16px; 170 | margin: 0 0 0 43px; 171 | } 172 | 173 | .todo-list li.editing .view { 174 | display: none; 175 | } 176 | 177 | .todo-list li .toggle { 178 | text-align: center; 179 | width: 40px; 180 | /* auto, since non-WebKit browsers doesn't support input styling */ 181 | height: auto; 182 | position: absolute; 183 | top: 0; 184 | bottom: 0; 185 | margin: auto 0; 186 | border: none; 187 | /* Mobile Safari */ 188 | -webkit-appearance: none; 189 | appearance: none; 190 | } 191 | 192 | .todo-list li .toggle { 193 | opacity: 0; 194 | } 195 | 196 | .todo-list li .toggle+label { 197 | /* 198 | Firefox requires `#` to be escaped - https://bugzilla.mozilla.org/show_bug.cgi?id=922433 199 | IE and Edge requires *everything* to be escaped to render, so we do that instead of just the `#` - https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7157459/ 200 | */ 201 | background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E'); 202 | background-repeat: no-repeat; 203 | background-position: center left; 204 | } 205 | 206 | .todo-list li .toggle:checked+label { 207 | background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E'); 208 | } 209 | 210 | .todo-list li label { 211 | word-break: break-all; 212 | padding: 15px 15px 15px 60px; 213 | display: block; 214 | line-height: 1.2; 215 | transition: color 0.4s; 216 | } 217 | 218 | .todo-list li.completed label { 219 | color: #d9d9d9; 220 | text-decoration: line-through; 221 | } 222 | 223 | .todo-list li .destroy { 224 | display: none; 225 | position: absolute; 226 | top: 0; 227 | right: 10px; 228 | bottom: 0; 229 | width: 40px; 230 | height: 40px; 231 | margin: auto 0; 232 | font-size: 30px; 233 | color: #cc9a9a; 234 | margin-bottom: 11px; 235 | transition: color 0.2s ease-out; 236 | } 237 | 238 | .todo-list li .destroy:hover { 239 | color: #af5b5e; 240 | } 241 | 242 | .todo-list li .destroy:after { 243 | content: '×'; 244 | } 245 | 246 | .todo-list li:hover .destroy { 247 | display: block; 248 | } 249 | 250 | .todo-list li .edit { 251 | display: none; 252 | } 253 | 254 | .todo-list li.editing:last-child { 255 | margin-bottom: -1px; 256 | } 257 | 258 | .footer { 259 | color: #777; 260 | padding: 10px 15px; 261 | height: 20px; 262 | text-align: center; 263 | border-top: 1px solid #e6e6e6; 264 | } 265 | 266 | .footer:before { 267 | content: ''; 268 | position: absolute; 269 | right: 0; 270 | bottom: 0; 271 | left: 0; 272 | height: 50px; 273 | overflow: hidden; 274 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 275 | 0 8px 0 -3px #f6f6f6, 276 | 0 9px 1px -3px rgba(0, 0, 0, 0.2), 277 | 0 16px 0 -6px #f6f6f6, 278 | 0 17px 2px -6px rgba(0, 0, 0, 0.2); 279 | } 280 | 281 | .todo-count { 282 | float: left; 283 | text-align: left; 284 | } 285 | 286 | .todo-count strong { 287 | font-weight: 300; 288 | } 289 | 290 | .filters { 291 | margin: 0; 292 | padding: 0; 293 | list-style: none; 294 | position: absolute; 295 | right: 0; 296 | left: 0; 297 | } 298 | 299 | .filters li { 300 | display: inline; 301 | } 302 | 303 | .filters li a { 304 | color: inherit; 305 | margin: 3px; 306 | padding: 3px 7px; 307 | text-decoration: none; 308 | border: 1px solid transparent; 309 | border-radius: 3px; 310 | } 311 | 312 | .filters li a:hover { 313 | border-color: rgba(175, 47, 47, 0.1); 314 | } 315 | 316 | .filters li a.selected { 317 | border-color: rgba(175, 47, 47, 0.2); 318 | } 319 | 320 | .clear-completed, 321 | html .clear-completed:active { 322 | float: right; 323 | position: relative; 324 | line-height: 20px; 325 | text-decoration: none; 326 | cursor: pointer; 327 | } 328 | 329 | .clear-completed:hover { 330 | text-decoration: underline; 331 | } 332 | 333 | .info { 334 | margin: 65px auto 0; 335 | color: #bfbfbf; 336 | font-size: 10px; 337 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); 338 | text-align: center; 339 | } 340 | 341 | .info p { 342 | line-height: 1; 343 | } 344 | 345 | .info a { 346 | color: inherit; 347 | text-decoration: none; 348 | font-weight: 400; 349 | } 350 | 351 | .info a:hover { 352 | text-decoration: underline; 353 | } 354 | 355 | /* 356 | Hack to remove background from Mobile Safari. 357 | Can't use it globally since it destroys checkboxes in Firefox 358 | */ 359 | @media screen and (-webkit-min-device-pixel-ratio:0) { 360 | 361 | .toggle-all, 362 | .todo-list li .toggle { 363 | background: none; 364 | } 365 | 366 | .todo-list li .toggle { 367 | height: 40px; 368 | } 369 | } 370 | 371 | @media (max-width: 430px) { 372 | .footer { 373 | height: 50px; 374 | } 375 | 376 | .filters { 377 | bottom: 10px; 378 | } 379 | } 380 | -------------------------------------------------------------------------------- /weatherapp/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /weatherapp/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "weatherapp" 3 | version = "0.1.1" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | chrono = { version = "0.4.26", features = ["serde"] } 8 | dioxus = "0.4.0" 9 | dioxus-web = "0.4.0" 10 | reqwest = { version = "0.11.18", features = ["json"] } 11 | serde = "1.0.171" 12 | serde_json = "1.0.102" 13 | -------------------------------------------------------------------------------- /weatherapp/README.md: -------------------------------------------------------------------------------- 1 | # Weather App with Rust + Dioxus 2 | 3 | This example shows a reactive Dioxus App with the following featurea: 4 | - tailwind integration for styling 5 | - use_state hook to leverage reactive state 6 | - use_future hook for async API calls 7 | 8 | ![example](./weatherapp.png) 9 | 10 | -------------------------------------------------------------------------------- /weatherapp/src/main.rs: -------------------------------------------------------------------------------- 1 | use chrono::Utc; 2 | use dioxus::prelude::*; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | fn main() { 6 | dioxus_web::launch(app); 7 | } 8 | 9 | fn app(cx: Scope) -> Element { 10 | let country = use_state(cx, || WeatherLocation { 11 | name: "Berlin".to_string(), 12 | country: "Germany".to_string(), 13 | latitude: 52.5244, 14 | longitude: 13.4105, 15 | id: 2950159, 16 | }); 17 | 18 | let weather = use_future( 19 | cx, 20 | country, 21 | |country| async move { get_weather(&country).await }, 22 | ); 23 | 24 | cx.render(rsx!( 25 | link { 26 | rel: "stylesheet", 27 | href: "https://unpkg.com/tailwindcss@^2.0/dist/tailwind.min.css" 28 | } 29 | div { class: "mx-auto p-4 bg-gray-100 h-screen flex justify-center", 30 | div { class: "flex items-center justify-center flex-row", 31 | div { class: "flex items-start justify-center flex-row", 32 | search_box { country: country.clone() } 33 | div { class: "flex flex-wrap w-full px-2", 34 | div { class: "bg-gray-900 text-white relative min-w-0 break-words rounded-lg overflow-hidden shadow-sm mb-4 w-full bg-white dark:bg-gray-600", 35 | div { class: "px-6 py-6 relative", 36 | if let Some(Ok(weather)) = weather.value() { 37 | rsx!( 38 | CountryData { 39 | country: country.clone(), 40 | weather: weather.clone() 41 | } 42 | Forecast { 43 | weather: weather.clone(), 44 | } 45 | ) 46 | } else { 47 | rsx!( 48 | p { 49 | "Loading.." 50 | } 51 | ) 52 | } 53 | } 54 | } 55 | } 56 | } 57 | } 58 | } 59 | )) 60 | } 61 | 62 | #[allow(non_snake_case)] 63 | #[inline_props] 64 | fn CountryData(cx: Scope, weather: WeatherResponse, country: UseState) -> Element { 65 | let today = Utc::now().format("%y/%m/%d"); 66 | let max_temp = weather.daily.temperature_2m_max.first().unwrap(); 67 | let min_temp = weather.daily.temperature_2m_min.first().unwrap(); 68 | 69 | cx.render(rsx!( 70 | div { class: "flex mb-4 justify-between items-center", 71 | div { 72 | h5 { class: "mb-0 font-medium text-xl", "{country.name} 🏞️" } 73 | h6 { class: "mb-0", "{today}" } 74 | } 75 | div { 76 | div { class: "flex items-center", 77 | span { "Temp min" } 78 | span { class: "px-2 inline-block", "👉 {min_temp}°" } 79 | } 80 | div { class: "flex items-center", 81 | span { "Temp max" } 82 | span { class: "px-2 inline-block ", "👉 {max_temp}º" } 83 | } 84 | } 85 | } 86 | )) 87 | } 88 | 89 | #[allow(non_snake_case)] 90 | #[inline_props] 91 | fn Forecast(cx: Scope, weather: WeatherResponse) -> Element { 92 | let today = (weather.daily.temperature_2m_max.first().unwrap() 93 | + weather.daily.temperature_2m_max.first().unwrap()) 94 | / 2.0; 95 | let tomorrow = (weather.daily.temperature_2m_max.get(1).unwrap() 96 | + weather.daily.temperature_2m_max.get(1).unwrap()) 97 | / 2.0; 98 | let past_tomorrow = (weather.daily.temperature_2m_max.get(2).unwrap() 99 | + weather.daily.temperature_2m_max.get(2).unwrap()) 100 | / 2.0; 101 | render!( 102 | div { class: "px-6 pt-4 relative", 103 | div { class: "w-full h-px bg-gray-100 mb-4" } 104 | div { p { class: "text-center w-full mb-4", "👇 Forecast 📆" } } 105 | div { class: "text-center justify-between items-center flex", 106 | div { class: "text-center mb-0 flex items-center justify-center flex-col mx-4 w-16", 107 | span { class: "block my-1", "Today" } 108 | span { class: "block my-1", "{today}°" } 109 | } 110 | div { class: "text-center mb-0 flex items-center justify-center flex-col mx-8 w-16", 111 | span { class: "block my-1", "Tomorrow" } 112 | span { class: "block my-1", "{tomorrow}°" } 113 | } 114 | div { class: "text-center mb-0 flex items-center justify-center flex-col mx-2 w-30", 115 | span { class: "block my-1", "Past Tomorrow" } 116 | span { class: "block my-1", "{past_tomorrow}°" } 117 | } 118 | } 119 | } 120 | ) 121 | } 122 | 123 | #[inline_props] 124 | fn search_box(cx: Scope, country: UseState) -> Element { 125 | let input = use_state(cx, String::new); 126 | 127 | let locations = use_future(cx, input, |input| async move { 128 | get_locations(&input).await.unwrap_or_default() 129 | }); 130 | 131 | let oninput = |e: FormEvent| { 132 | input.set(e.data.value.clone()); 133 | }; 134 | 135 | cx.render(rsx!( 136 | div { 137 | div { class: "inline-flex flex-col justify-center relative text-gray-500", 138 | div { class: "relative", 139 | input { 140 | class: "p-2 pl-8 rounded-lg border border-gray-200 bg-gray-200 focus:bg-white focus:outline-none focus:ring-2 focus:ring-yellow-600 focus:border-transparent", 141 | placeholder: "Country name", 142 | "type": "text", 143 | autofocus: true, 144 | oninput: oninput 145 | } 146 | svg { 147 | class: "w-4 h-4 absolute left-2.5 top-3.5", 148 | "viewBox": "0 0 24 24", 149 | fill: "none", 150 | stroke: "currentColor", 151 | xmlns: "http://www.w3.org/2000/svg", 152 | path { 153 | d: "M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z", 154 | "stroke-linejoin": "round", 155 | "stroke-linecap": "round", 156 | "stroke-width": "2" 157 | } 158 | } 159 | } 160 | ul { class: "bg-white border border-gray-100 w-full mt-2 max-h-72 overflow-auto", 161 | if let Some(locations) = locations.value() { 162 | rsx!( 163 | locations.iter().map(|location| { 164 | let onclick = |_| { 165 | country.set(location.clone()); 166 | }; 167 | rsx!{ 168 | li { class: "pl-8 pr-2 py-1 border-b-2 border-gray-100 relative cursor-pointer hover:bg-yellow-50 hover:text-gray-900", 169 | onclick: onclick, 170 | MapIcon {} 171 | b { 172 | "{location.name}" 173 | } 174 | " · {location.country}" 175 | } 176 | } 177 | }) 178 | ) 179 | } 180 | } 181 | } 182 | } 183 | )) 184 | } 185 | 186 | #[allow(non_snake_case)] 187 | fn MapIcon(cx: Scope) -> Element { 188 | render!( 189 | svg { 190 | class: "stroke-current absolute w-4 h-4 left-2 top-2", 191 | stroke: "currentColor", 192 | xmlns: "http://www.w3.org/2000/svg", 193 | "viewBox": "0 0 24 24", 194 | fill: "none", 195 | path { 196 | "stroke-linejoin": "round", 197 | "stroke-width": "2", 198 | "stroke-linecap": "round", 199 | d: "M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" 200 | } 201 | path { 202 | "stroke-linecap": "round", 203 | "stroke-linejoin": "round", 204 | d: "M15 11a3 3 0 11-6 0 3 3 0 016 0z", 205 | "stroke-width": "2" 206 | } 207 | } 208 | ) 209 | } 210 | 211 | #[derive(Debug, Default, Serialize, Deserialize, Clone)] 212 | struct WeatherLocation { 213 | id: usize, 214 | name: String, 215 | latitude: f32, 216 | longitude: f32, 217 | country: String, 218 | } 219 | 220 | type WeatherLocations = Vec; 221 | 222 | #[derive(Debug, Default, Serialize, Deserialize)] 223 | struct SearchResponse { 224 | results: WeatherLocations, 225 | } 226 | 227 | async fn get_locations(input: &str) -> reqwest::Result { 228 | let res = reqwest::get(&format!( 229 | "https://geocoding-api.open-meteo.com/v1/search?name={input}" 230 | )) 231 | .await? 232 | .json::() 233 | .await?; 234 | 235 | Ok(res.results) 236 | } 237 | 238 | #[derive(Debug, Default, Serialize, Deserialize, PartialEq, Clone)] 239 | struct WeatherResponse { 240 | daily: DailyWeather, 241 | hourly: HourlyWeather, 242 | } 243 | 244 | #[derive(Debug, Default, Serialize, Deserialize, PartialEq, Clone)] 245 | struct HourlyWeather { 246 | time: Vec, 247 | temperature_2m: Vec, 248 | } 249 | 250 | #[derive(Debug, Default, Serialize, Deserialize, PartialEq, Clone)] 251 | struct DailyWeather { 252 | temperature_2m_min: Vec, 253 | temperature_2m_max: Vec, 254 | } 255 | 256 | async fn get_weather(location: &WeatherLocation) -> reqwest::Result { 257 | let res = reqwest::get(&format!("https://api.open-meteo.com/v1/forecast?latitude={}&longitude={}&hourly=temperature_2m&daily=temperature_2m_max,temperature_2m_min,apparent_temperature_max,apparent_temperature_min&timezone=GMT", location.latitude, location.longitude)) 258 | .await 259 | ? 260 | .json::() 261 | .await 262 | ?; 263 | 264 | Ok(res) 265 | } 266 | -------------------------------------------------------------------------------- /weatherapp/weatherapp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DioxusLabs/example-projects/db87199d2fcdbdd70cf5b6bed5b156ac35fb1e91/weatherapp/weatherapp.png -------------------------------------------------------------------------------- /wifi-scanner/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /wifi-scanner/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wifi-scanner" 3 | version = "0.1.1" 4 | edition = "2018" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | anyhow = "1.0.52" 10 | pretty_env_logger = "0.5.0" 11 | tokio = { version = "1.12.0", features = ["full"] } 12 | dioxus = { version = "0.4.0" } 13 | dioxus-desktop = { version = "0.4.0" } 14 | wifiscanner = "0.5.1" 15 | futures-channel = "0.3.19" 16 | futures = "0.3.19" 17 | -------------------------------------------------------------------------------- /wifi-scanner/README.md: -------------------------------------------------------------------------------- 1 | # WiFi scanner app 2 | 3 | This desktop app showcases the use of background threads. 4 | 5 | ![Demo of app](./demo_small.png) 6 | -------------------------------------------------------------------------------- /wifi-scanner/demo_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DioxusLabs/example-projects/db87199d2fcdbdd70cf5b6bed5b156ac35fb1e91/wifi-scanner/demo_small.png -------------------------------------------------------------------------------- /wifi-scanner/src/main.rs: -------------------------------------------------------------------------------- 1 | use dioxus::prelude::*; 2 | use dioxus_desktop::Config; 3 | use futures::StreamExt; 4 | use futures_channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender}; 5 | use std::cell::Cell; 6 | use wifiscanner::Wifi; 7 | 8 | fn main() { 9 | let (sender, receiver) = unbounded(); 10 | 11 | let other = sender.clone(); 12 | 13 | // launch our initial background scan in a thread 14 | std::thread::spawn(move || { 15 | let _ = other.unbounded_send(Status::Scanning); 16 | let _ = other.unbounded_send(perform_scan()); 17 | }); 18 | 19 | // launch our app on the current thread - important because we spawn a window 20 | dioxus_desktop::launch_with_props( 21 | app, 22 | AppProps { 23 | sender: Cell::new(Some(sender)), 24 | receiver: Cell::new(Some(receiver)), 25 | }, 26 | Config::default(), 27 | ) 28 | } 29 | 30 | fn perform_scan() -> Status { 31 | if let Ok(devices) = wifiscanner::scan() { 32 | if devices.is_empty() { 33 | Status::NoneFound 34 | } else { 35 | Status::Found(devices) 36 | } 37 | } else { 38 | Status::NoneFound 39 | } 40 | } 41 | 42 | enum Status { 43 | NoneFound, 44 | Scanning, 45 | Found(Vec), 46 | } 47 | 48 | struct AppProps { 49 | sender: Cell>>, 50 | receiver: Cell>>, 51 | } 52 | 53 | fn app(cx: Scope) -> Element { 54 | let status = use_state(cx, || Status::NoneFound); 55 | 56 | let _ = use_coroutine(cx, |_: UnboundedReceiver<()>| { 57 | let receiver = cx.props.receiver.take(); 58 | let status = status.to_owned(); 59 | async move { 60 | if let Some(mut receiver) = receiver { 61 | while let Some(msg) = receiver.next().await { 62 | status.set(msg); 63 | } 64 | } 65 | } 66 | }); 67 | 68 | cx.render(rsx!( 69 | link { rel: "stylesheet", href: "https://unpkg.com/tailwindcss@^2.0/dist/tailwind.min.css" }, 70 | div { 71 | div { class: "py-8 px-6", 72 | div { class: "container px-4 mx-auto", 73 | h2 { class: "text-2xl font-bold", "Scan for WiFi Networks" } 74 | button { 75 | class: "inline-block w-full md:w-auto px-6 py-3 font-medium text-white bg-indigo-500 hover:bg-indigo-600 rounded transition duration-200", 76 | onclick: move |_| { 77 | let sender = cx.props.sender.take(); 78 | std::thread::spawn( || { 79 | if let Some(sender) = sender { 80 | let _ = sender.unbounded_send(Status::Scanning); 81 | let _ = sender.unbounded_send(perform_scan()); 82 | } 83 | }); 84 | }, 85 | match status.get() { 86 | Status::Scanning => rsx!("Scanning"), 87 | _ => rsx!("Scan"), 88 | } 89 | } 90 | } 91 | } 92 | 93 | section { class: "py-8", 94 | div { class: "container px-4 mx-auto", 95 | div { class: "p-4 mb-6 bg-white shadow rounded overflow-x-auto", 96 | table { class: "table-auto w-full", 97 | thead { 98 | tr { class: "text-xs text-gray-500 text-left", 99 | th { class: "pl-6 pb-3 font-medium", "Strength" } 100 | th { class: "pb-3 font-medium", "Network" } 101 | th { class: "pb-3 font-medium", "Channel" } 102 | th { class: "pb-3 px-2 font-medium", "Security" } 103 | } 104 | } 105 | 106 | match status.get() { 107 | Status::Scanning => rsx!(""), 108 | Status::NoneFound => rsx!("No networks found. Try scanning again"), 109 | Status::Found(wifis) => { 110 | // Create vector of tuples of (signal_level, wifi) for sorting by signal_level 111 | let mut sorted_wifis = wifis 112 | .iter() 113 | .map(|wif: &Wifi| (wif, wif.signal_level.parse::().unwrap())) 114 | .collect::>(); 115 | sorted_wifis.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap()); 116 | 117 | rsx! { 118 | tbody { 119 | sorted_wifis.into_iter().rev().map(|(wi, _)|{ 120 | let Wifi { mac: _, ssid, channel, signal_level, security } = wi; 121 | rsx!( 122 | tr { class: "text-xs bg-gray-50", 123 | td { class: "py-5 px-6 font-medium", "{signal_level}" } 124 | td { class: "flex py-3 font-medium", "{ssid}" } 125 | td { span { class: "inline-block py-1 px-2 text-white bg-green-500 rounded-full", "{channel}" } } 126 | td { span { class: "inline-block py-1 px-2 text-purple-500 bg-purple-50 rounded-full", "{security}" } } 127 | } 128 | ) 129 | }) 130 | } 131 | } 132 | } 133 | } 134 | } 135 | } 136 | } 137 | } 138 | } 139 | )) 140 | } 141 | --------------------------------------------------------------------------------