diff --git a/.github/workflows/cli_release.yml b/.github/workflows/cli_release.yml index 4cd33f47e..544c375a2 100644 --- a/.github/workflows/cli_release.yml +++ b/.github/workflows/cli_release.yml @@ -36,6 +36,8 @@ jobs: toolchain: ${{ matrix.platform.toolchain }} targets: ${{ matrix.platform.target }} + - uses: ilammy/setup-nasm@v1 + # Setup the Github Actions Cache for the CLI package - name: Setup cache uses: Swatinem/rust-cache@v2 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 1b1f1ed7b..8001af650 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -39,6 +39,7 @@ jobs: steps: - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 + - uses: ilammy/setup-nasm@v1 - run: sudo apt-get update - run: sudo apt install libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev - uses: actions/checkout@v4 @@ -51,6 +52,7 @@ jobs: steps: - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 + - uses: ilammy/setup-nasm@v1 - run: sudo apt-get update - run: sudo apt install libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev - uses: davidB/rust-cargo-make@v1 @@ -66,6 +68,7 @@ jobs: steps: - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 + - uses: ilammy/setup-nasm@v1 - run: rustup component add rustfmt - uses: actions/checkout@v4 - run: cargo fmt --all -- --check @@ -77,6 +80,7 @@ jobs: steps: - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 + - uses: ilammy/setup-nasm@v1 - run: sudo apt-get update - run: sudo apt install libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev - run: rustup component add clippy @@ -124,6 +128,8 @@ jobs: } steps: + - uses: actions/checkout@v4 + - uses: ilammy/setup-nasm@v1 - name: install stable uses: dtolnay/rust-toolchain@master with: diff --git a/.github/workflows/miri.yml b/.github/workflows/miri.yml index f9ad3855f..6c8a260b3 100644 --- a/.github/workflows/miri.yml +++ b/.github/workflows/miri.yml @@ -26,8 +26,8 @@ env: RUST_BACKTRACE: 1 # Change to specific Rust release to pin rust_stable: stable - rust_nightly: nightly-2022-11-03 - rust_clippy: 1.65.0 + rust_nightly: nightly-2023-11-16 + rust_clippy: 1.70.0 # When updating this, also update: # - README.md # - tokio/README.md @@ -70,6 +70,7 @@ jobs: run: echo "MIRIFLAGS=-Zmiri-tag-gc=1" >> $GITHUB_ENV - uses: actions/checkout@v4 + - uses: ilammy/setup-nasm@v1 - name: Install Rust ${{ env.rust_nightly }} uses: dtolnay/rust-toolchain@master with: diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 7b23c6111..6b1b5acf0 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -20,6 +20,7 @@ jobs: steps: # Do our best to cache the toolchain and node install steps - uses: actions/checkout@v4 + - uses: ilammy/setup-nasm@v1 - uses: actions/setup-node@v4 with: node-version: 16 diff --git a/Cargo.toml b/Cargo.toml index 3941298bf..9fc38df5f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -59,7 +59,7 @@ dioxus-core = { path = "packages/core", version = "0.4.2" } dioxus-core-macro = { path = "packages/core-macro", version = "0.4.0" } dioxus-router = { path = "packages/router", version = "0.4.1" } dioxus-router-macro = { path = "packages/router-macro", version = "0.4.1" } -dioxus-html = { path = "packages/html", version = "0.4.0" } +dioxus-html = { path = "packages/html", default-features = false, version = "0.4.0" } dioxus-hooks = { path = "packages/hooks", version = "0.4.0" } dioxus-web = { path = "packages/web", version = "0.4.0" } dioxus-ssr = { path = "packages/ssr", version = "0.4.0" } diff --git a/examples/compose.rs b/examples/compose.rs index 6a303a701..823e47a57 100644 --- a/examples/compose.rs +++ b/examples/compose.rs @@ -26,7 +26,7 @@ fn app(cx: Scope) -> Element { button { onclick: move |_| { let dom = VirtualDom::new_with_props(compose, ComposeProps { app_tx: tx.clone() }); - window.new_window(dom, Default::default()); + dioxus_desktop::window().new_window(dom, Default::default()); }, "Click to compose a new email" } @@ -61,11 +61,8 @@ fn compose(cx: Scope) -> Element { }, "Click to send" } - - input { - oninput: move |e| user_input.set(e.value.clone()), - value: "{user_input}" - } + + input { oninput: move |e| user_input.set(e.value()), value: "{user_input}" } } }) } diff --git a/examples/counter.rs b/examples/counter.rs index f51083a95..1de77dc38 100644 --- a/examples/counter.rs +++ b/examples/counter.rs @@ -22,7 +22,7 @@ fn app(cx: Scope) -> Element { input { value: "{counter}", oninput: move |e| { - if let Ok(value) = e.value.parse::() { + if let Ok(value) = e.value().parse::() { counters.make_mut()[i] = value; } } diff --git a/examples/crm.rs b/examples/crm.rs index 189f98e5a..8af49b401 100644 --- a/examples/crm.rs +++ b/examples/crm.rs @@ -35,14 +35,16 @@ fn App(cx: Scope) -> Element { rel: "stylesheet", href: "https://unpkg.com/purecss@2.0.6/build/pure-min.css", integrity: "sha384-Uu6IeWbM+gzNVXJcM9XV3SohHtmWE+3VGi496jvgX1jyvDTXfdK+rfZc8C1Aehk5", - crossorigin: "anonymous", + crossorigin: "anonymous" } - style { " + style { + " .red {{ background-color: rgb(202, 60, 60) !important; }} - " } + " + } h1 { "Dioxus CRM Example" } @@ -57,16 +59,8 @@ fn ClientList(cx: Scope) -> Element { cx.render(rsx! { h2 { "List of Clients" } - Link { - to: Route::ClientAdd {}, - class: "pure-button pure-button-primary", - "Add Client" - } - Link { - to: Route::Settings {}, - class: "pure-button", - "Settings" - } + Link { to: Route::ClientAdd {}, class: "pure-button pure-button-primary", "Add Client" } + Link { to: Route::Settings {}, class: "pure-button", "Settings" } clients.read().iter().map(|client| rsx! { div { @@ -94,79 +88,55 @@ fn ClientAdd(cx: Scope) -> Element { class: "pure-form pure-form-aligned", onsubmit: move |_| { let mut clients = clients.write(); - - clients.push(Client { - first_name: first_name.to_string(), - last_name: last_name.to_string(), - description: description.to_string(), - }); - + clients + .push(Client { + first_name: first_name.to_string(), + last_name: last_name.to_string(), + description: description.to_string(), + }); dioxus_router::router().push(Route::ClientList {}); }, fieldset { - div { - class: "pure-control-group", - label { - "for": "first_name", - "First Name" - } + div { class: "pure-control-group", + label { "for": "first_name", "First Name" } input { id: "first_name", "type": "text", placeholder: "First Name…", required: "", value: "{first_name}", - oninput: move |e| first_name.set(e.value.clone()) + oninput: move |e| first_name.set(e.value()) } } - div { - class: "pure-control-group", - label { - "for": "last_name", - "Last Name" - } + div { class: "pure-control-group", + label { "for": "last_name", "Last Name" } input { id: "last_name", "type": "text", placeholder: "Last Name…", required: "", value: "{last_name}", - oninput: move |e| last_name.set(e.value.clone()) + oninput: move |e| last_name.set(e.value()) } } - div { - class: "pure-control-group", - label { - "for": "description", - "Description" - } + div { class: "pure-control-group", + label { "for": "description", "Description" } textarea { id: "description", placeholder: "Description…", value: "{description}", - oninput: move |e| description.set(e.value.clone()) + oninput: move |e| description.set(e.value()) } } - div { - class: "pure-controls", - button { - "type": "submit", - class: "pure-button pure-button-primary", - "Save" - } - Link { - to: Route::ClientList {}, - class: "pure-button pure-button-primary red", - "Cancel" - } + div { class: "pure-controls", + button { "type": "submit", class: "pure-button pure-button-primary", "Save" } + Link { to: Route::ClientList {}, class: "pure-button pure-button-primary red", "Cancel" } } } - - } }) } @@ -187,10 +157,6 @@ fn Settings(cx: Scope) -> Element { "Remove all Clients" } - Link { - to: Route::ClientList {}, - class: "pure-button", - "Go back" - } + Link { to: Route::ClientList {}, class: "pure-button", "Go back" } }) } diff --git a/examples/custom_assets.rs b/examples/custom_assets.rs index 1253efba2..7eb9db375 100644 --- a/examples/custom_assets.rs +++ b/examples/custom_assets.rs @@ -10,7 +10,7 @@ fn app(cx: Scope) -> Element { p { "This should show an image:" } - img { src: "examples/assets/logo.png" } + img { src: mg!(image("examples/assets/logo.png").format(ImageType::Avif)).to_string() } } }) } diff --git a/examples/file_explorer.rs b/examples/file_explorer.rs index 88b37789d..f0534cbf8 100644 --- a/examples/file_explorer.rs +++ b/examples/file_explorer.rs @@ -18,13 +18,14 @@ fn main() { ); } +const _STYLE: &str = mg!(file("./examples/assets/fileexplorer.css")); + fn app(cx: Scope) -> Element { let files = use_ref(cx, Files::new); cx.render(rsx! { div { link { href:"https://fonts.googleapis.com/icon?family=Material+Icons", rel:"stylesheet", } - style { include_str!("./assets/fileexplorer.css") } header { i { class: "material-icons icon-menu", "menu" } h1 { "Files: ", files.read().current() } diff --git a/examples/file_upload.rs b/examples/file_upload.rs index c7358c685..882285ddf 100644 --- a/examples/file_upload.rs +++ b/examples/file_upload.rs @@ -16,7 +16,7 @@ fn App(cx: Scope) -> Element { r#type: "checkbox", checked: "{enable_directory_upload}", oninput: move |evt| { - enable_directory_upload.set(evt.value.parse().unwrap()); + enable_directory_upload.set(evt.value().parse().unwrap()); }, }, "Enable directory upload" @@ -30,7 +30,7 @@ fn App(cx: Scope) -> Element { onchange: |evt| { to_owned![files_uploaded]; async move { - if let Some(file_engine) = &evt.files { + if let Some(file_engine) = &evt.files() { let files = file_engine.files(); for file_name in files { sleep(std::time::Duration::from_secs(1)).await; diff --git a/examples/form.rs b/examples/form.rs index a7de142f3..897ffc710 100644 --- a/examples/form.rs +++ b/examples/form.rs @@ -14,8 +14,8 @@ fn app(cx: Scope) -> Element { div { h1 { "Form" } form { - onsubmit: move |ev| println!("Submitted {:?}", ev.values), - oninput: move |ev| println!("Input {:?}", ev.values), + onsubmit: move |ev| println!("Submitted {:?}", ev.values()), + oninput: move |ev| println!("Input {:?}", ev.values()), input { r#type: "text", name: "username" } input { r#type: "text", name: "full-name" } input { r#type: "password", name: "password" } diff --git a/examples/login_form.rs b/examples/login_form.rs index d14338c3d..3e8220aef 100644 --- a/examples/login_form.rs +++ b/examples/login_form.rs @@ -12,8 +12,8 @@ fn app(cx: Scope) -> Element { let resp = reqwest::Client::new() .post("http://localhost:8080/login") .form(&[ - ("username", &evt.values["username"]), - ("password", &evt.values["password"]), + ("username", &evt.values()["username"]), + ("password", &evt.values()["password"]), ]) .send() .await; @@ -31,8 +31,7 @@ fn app(cx: Scope) -> Element { cx.render(rsx! { h1 { "Login" } - form { - onsubmit: onsubmit, + form { onsubmit: onsubmit, input { r#type: "text", id: "username", name: "username" } label { "Username" } br {} diff --git a/examples/query_segments_demo/src/main.rs b/examples/query_segments_demo/src/main.rs index aafcc0f43..3303aeecd 100644 --- a/examples/query_segments_demo/src/main.rs +++ b/examples/query_segments_demo/src/main.rs @@ -14,29 +14,35 @@ use dioxus_router::prelude::*; #[derive(Routable, Clone)] #[rustfmt::skip] enum Route { - // segments that start with ?: are query segments - #[route("/blog?:query_params")] + // segments that start with ?:.. are query segments that capture the entire query + #[route("/blog?:..query_params")] BlogPost { // You must include query segments in child variants - query_params: BlogQuerySegments, + query_params: ManualBlogQuerySegments, + }, + // segments that follow the ?:field&:other_field syntax are query segments that follow the standard url query syntax + #[route("/autoblog?:name&:surname")] + AutomaticBlogPost { + name: String, + surname: String, }, } #[derive(Debug, Clone, PartialEq)] -struct BlogQuerySegments { +struct ManualBlogQuerySegments { name: String, surname: String, } /// The display impl needs to display the query in a way that can be parsed: -impl Display for BlogQuerySegments { +impl Display for ManualBlogQuerySegments { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "name={}&surname={}", self.name, self.surname) } } /// The query segment is anything that implements . You can implement that trait for a struct if you want to parse multiple query parameters. -impl FromQuery for BlogQuerySegments { +impl FromQuery for ManualBlogQuerySegments { fn from_query(query: &str) -> Self { let mut name = None; let mut surname = None; @@ -57,13 +63,21 @@ impl FromQuery for BlogQuerySegments { } #[component] -fn BlogPost(cx: Scope, query_params: BlogQuerySegments) -> Element { +fn BlogPost(cx: Scope, query_params: ManualBlogQuerySegments) -> Element { render! { div{"This is your blogpost with a query segment:"} div{format!("{:?}", query_params)} } } +#[component] +fn AutomaticBlogPost(cx: Scope, name: String, surname: String) -> Element { + render! { + div{"This is your blogpost with a query segment:"} + div{format!("name={}&surname={}", name, surname)} + } +} + #[component] fn App(cx: Scope) -> Element { render! { Router::{} } diff --git a/examples/shared_state.rs b/examples/shared_state.rs index 37271f160..487aa3fe3 100644 --- a/examples/shared_state.rs +++ b/examples/shared_state.rs @@ -64,7 +64,7 @@ fn DataEditor(cx: Scope, id: usize) -> Element { fn DataView(cx: Scope, id: usize) -> Element { let cool_data = use_shared_state::(cx).unwrap(); - let oninput = |e: FormEvent| cool_data.write().set(*id, e.value.clone()); + let oninput = |e: FormEvent| cool_data.write().set(*id, e.value()); let cool_data = cool_data.read(); let my_data = &cool_data.view(id).unwrap(); diff --git a/examples/tailwind/Dioxus.toml b/examples/tailwind/Dioxus.toml index b440d6a69..b7e78ff7f 100644 --- a/examples/tailwind/Dioxus.toml +++ b/examples/tailwind/Dioxus.toml @@ -30,7 +30,7 @@ watch_path = ["src", "public"] [web.resource] # CSS style file -style = ["/tailwind.css"] +style = [] # Javascript code file script = [] diff --git a/examples/tailwind/dist/tailwind3531548035813279582.css b/examples/tailwind/dist/tailwind3531548035813279582.css new file mode 100644 index 000000000..2196a9412 --- /dev/null +++ b/examples/tailwind/dist/tailwind3531548035813279582.css @@ -0,0 +1 @@ +*,:before,:after{box-sizing:border-box;border:0 solid #e5e7eb}:before,:after{--tw-content:""}html{-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-feature-settings:normal;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;line-height:1.5}body{line-height:inherit;margin:0}hr{color:inherit;border-top-width:1px;height:0}abbr:where([title]){text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;font-weight:inherit;line-height:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button;background-color:#0000;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{margin:0;padding:0;list-style:none}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}[hidden]{display:none}*,:before,:after,::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.container{width:100%}@media (width>=640px){.container{max-width:640px}}@media (width>=768px){.container{max-width:768px}}@media (width>=1024px){.container{max-width:1024px}}@media (width>=1280px){.container{max-width:1280px}}@media (width>=1536px){.container{max-width:1536px}}.mx-auto{margin-left:auto;margin-right:auto}.mb-16{margin-bottom:4rem}.mb-4{margin-bottom:1rem}.mb-8{margin-bottom:2rem}.ml-1{margin-left:.25rem}.ml-3{margin-left:.75rem}.ml-4{margin-left:1rem}.mr-5{margin-right:1.25rem}.mt-4{margin-top:1rem}.flex{display:flex}.inline-flex{display:inline-flex}.hidden{display:none}.h-10{height:2.5rem}.h-4{height:1rem}.w-10{width:2.5rem}.w-4{width:1rem}.w-5\/6{width:83.3333%}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.justify-center{justify-content:center}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.border-0{border-width:0}.bg-gray-800{--tw-bg-opacity:1;background-color:rgb(31 41 55/var(--tw-bg-opacity))}.bg-gray-900{--tw-bg-opacity:1;background-color:rgb(17 24 39/var(--tw-bg-opacity))}.bg-indigo-500{--tw-bg-opacity:1;background-color:rgb(99 102 241/var(--tw-bg-opacity))}.object-cover{-o-object-fit:cover;object-fit:cover}.object-center{-o-object-position:center;object-position:center}.p-2{padding:.5rem}.p-5{padding:1.25rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-24{padding-top:6rem;padding-bottom:6rem}.text-center{text-align:center}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.font-medium{font-weight:500}.leading-relaxed{line-height:1.625}.text-gray-400{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.hover\:bg-gray-700:hover{--tw-bg-opacity:1;background-color:rgb(55 65 81/var(--tw-bg-opacity))}.hover\:bg-indigo-600:hover{--tw-bg-opacity:1;background-color:rgb(79 70 229/var(--tw-bg-opacity))}.hover\:text-white:hover{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.focus\:outline-none:focus{outline-offset:2px;outline:2px solid #0000}@media (width>=640px){.sm\:text-4xl{font-size:2.25rem;line-height:2.5rem}}@media (width>=768px){.md\:mb-0{margin-bottom:0}.md\:ml-auto{margin-left:auto}.md\:mt-0{margin-top:0}.md\:w-1\/2{width:50%}.md\:flex-row{flex-direction:row}.md\:items-start{align-items:flex-start}.md\:pr-16{padding-right:4rem}.md\:text-left{text-align:left}}@media (width>=1024px){.lg\:inline-block{display:inline-block}.lg\:w-full{width:100%}.lg\:max-w-lg{max-width:32rem}.lg\:flex-grow{flex-grow:1}.lg\:pr-24{padding-right:6rem}} \ No newline at end of file diff --git a/examples/tailwind/src/main.rs b/examples/tailwind/src/main.rs index 8a49071db..10124e47c 100644 --- a/examples/tailwind/src/main.rs +++ b/examples/tailwind/src/main.rs @@ -2,13 +2,11 @@ use dioxus::prelude::*; +const _STYLE: &str = mg!(file("./public/tailwind.css")); + fn main() { #[cfg(not(target_arch = "wasm32"))] - dioxus_desktop::launch_cfg( - app, - dioxus_desktop::Config::new() - .with_custom_head(r#""#.to_string()), - ); + dioxus_desktop::launch(app); #[cfg(target_arch = "wasm32")] dioxus_web::launch(app); } diff --git a/examples/textarea.rs b/examples/textarea.rs index df684aa85..62afc45bb 100644 --- a/examples/textarea.rs +++ b/examples/textarea.rs @@ -17,7 +17,7 @@ fn app(cx: Scope) -> Element { rows: "10", cols: "80", value: "{model}", - oninput: move |e| model.set(e.value.clone()), + oninput: move |e| model.set(e.value().clone()), } }) } diff --git a/examples/todomvc.rs b/examples/todomvc.rs index 0f3fada71..cdf41aaa0 100644 --- a/examples/todomvc.rs +++ b/examples/todomvc.rs @@ -7,6 +7,8 @@ fn main() { dioxus_desktop::launch(app); } +const _STYLE: &str = mg!(file("./examples/assets/todomvc.css")); + #[derive(PartialEq, Eq, Clone, Copy)] pub enum FilterState { All, @@ -47,7 +49,6 @@ pub fn app(cx: Scope<()>) -> Element { cx.render(rsx! { section { class: "todoapp", - style { include_str!("./assets/todomvc.css") } TodoHeader { todos: todos } section { class: "main", if !todos.is_empty() { @@ -107,7 +108,7 @@ pub fn TodoHeader<'a>(cx: Scope<'a, TodoHeaderProps<'a>>) -> Element { value: "{draft}", autofocus: "true", oninput: move |evt| { - draft.set(evt.value.clone()); + draft.set(evt.value().clone()); }, onkeydown: move |evt| { if evt.key() == Key::Enter && !draft.is_empty() { @@ -154,7 +155,7 @@ pub fn TodoEntry<'a>(cx: Scope<'a, TodoEntryProps<'a>>) -> Element { id: "cbg-{todo.id}", checked: "{todo.checked}", oninput: move |evt| { - cx.props.todos.make_mut()[&cx.props.id].checked = evt.value.parse().unwrap(); + cx.props.todos.make_mut()[&cx.props.id].checked = evt.value().parse().unwrap(); } } label { @@ -175,7 +176,7 @@ pub fn TodoEntry<'a>(cx: Scope<'a, TodoEntryProps<'a>>) -> Element { input { class: "edit", value: "{todo.contents}", - oninput: move |evt| cx.props.todos.make_mut()[&cx.props.id].contents = evt.value.clone(), + oninput: move |evt| cx.props.todos.make_mut()[&cx.props.id].contents = evt.value(), autofocus: "true", onfocusout: move |_| is_editing.set(false), onkeydown: move |evt| { diff --git a/examples/window_zoom.rs b/examples/window_zoom.rs index bf0949d1f..840c12215 100644 --- a/examples/window_zoom.rs +++ b/examples/window_zoom.rs @@ -12,7 +12,7 @@ fn app(cx: Scope) -> Element { r#type: "number", value: "{level}", oninput: |e| { - if let Ok(new_zoom) = e.value.parse::() { + if let Ok(new_zoom) = e.value().parse::() { level.set(new_zoom); dioxus_desktop::window().webview.zoom(new_zoom); } diff --git a/examples/xss_safety.rs b/examples/xss_safety.rs index 5272d661b..73da2ff14 100644 --- a/examples/xss_safety.rs +++ b/examples/xss_safety.rs @@ -20,7 +20,7 @@ fn app(cx: Scope) -> Element { input { value: "{contents}", r#type: "text", - oninput: move |e| contents.set(e.value.clone()), + oninput: move |e| contents.set(e.value()), } } }) diff --git a/packages/cli/Cargo.toml b/packages/cli/Cargo.toml index 4c115184e..4335faa22 100644 --- a/packages/cli/Cargo.toml +++ b/packages/cli/Cargo.toml @@ -30,7 +30,7 @@ cargo_metadata = "0.15.0" tokio = { version = "1.16.1", features = ["fs", "sync", "rt", "macros"] } atty = "0.2.14" chrono = "0.4.19" -anyhow = "1.0.53" +anyhow = "1" hyper = "0.14.17" hyper-rustls = "0.23.2" indicatif = "0.17.5" @@ -75,6 +75,8 @@ toml_edit = "0.19.11" tauri-bundler = { version = "=1.3.0", features = ["native-tls-vendored"] } tauri-utils = "=1.4.*" +manganis-cli-support= { git = "https://github.com/DioxusLabs/collect-assets", features = ["webp", "html"] } + dioxus-autofmt = { workspace = true } dioxus-check = { workspace = true } rsx-rosetta = { workspace = true } diff --git a/packages/cli/src/builder.rs b/packages/cli/src/builder.rs index a5721d7a6..6d1ccea05 100644 --- a/packages/cli/src/builder.rs +++ b/packages/cli/src/builder.rs @@ -2,31 +2,35 @@ use crate::{ config::{CrateConfig, ExecutableType}, error::{Error, Result}, tools::Tool, - DioxusConfig, }; use cargo_metadata::{diagnostic::Diagnostic, Message}; use indicatif::{ProgressBar, ProgressStyle}; +use lazy_static::lazy_static; +use manganis_cli_support::AssetManifestExt; use serde::Serialize; use std::{ fs::{copy, create_dir_all, File}, - io::Read, + io::{Read, Write}, panic, path::PathBuf, time::Duration, }; use wasm_bindgen_cli_support::Bindgen; +lazy_static! { + static ref PROGRESS_BARS: indicatif::MultiProgress = indicatif::MultiProgress::new(); +} + #[derive(Serialize, Debug, Clone)] pub struct BuildResult { pub warnings: Vec, pub elapsed_time: u128, } -#[allow(unused)] -pub fn build(config: &CrateConfig, quiet: bool) -> Result { +pub fn build(config: &CrateConfig, _: bool, skip_assets: bool) -> Result { // [1] Build the project with cargo, generating a wasm32-unknown-unknown target (is there a more specific, better target to leverage?) // [2] Generate the appropriate build folders - // [3] Wasm-bindgen the .wasm fiile, and move it into the {builddir}/modules/xxxx/xxxx_bg.wasm + // [3] Wasm-bindgen the .wasm file, and move it into the {builddir}/modules/xxxx/xxxx_bg.wasm // [4] Wasm-opt the .wasm file with whatever optimizations need to be done // [5][OPTIONAL] Builds the Tailwind CSS file using the Tailwind standalone binary // [6] Link up the html page to the wasm module @@ -41,6 +45,8 @@ pub fn build(config: &CrateConfig, quiet: bool) -> Result { .. } = config; + let _gaurd = WebAssetConfigDropGuard::new(); + // start to build the assets let ignore_files = build_assets(config)?; @@ -60,8 +66,8 @@ pub fn build(config: &CrateConfig, quiet: bool) -> Result { .output()?; } - let cmd = subprocess::Exec::cmd("cargo"); - let cmd = cmd + let cmd = subprocess::Exec::cmd("cargo") + .env("CARGO_TARGET_DIR", target_dir) .cwd(crate_dir) .arg("build") .arg("--target") @@ -252,19 +258,28 @@ pub fn build(config: &CrateConfig, quiet: bool) -> Result { } } + if !skip_assets { + process_assets(config)?; + } + Ok(BuildResult { warnings: warning_messages, elapsed_time: t_start.elapsed().as_millis(), }) } -pub fn build_desktop(config: &CrateConfig, _is_serve: bool) -> Result { +pub fn build_desktop( + config: &CrateConfig, + _is_serve: bool, + skip_assets: bool, +) -> Result { log::info!("🚅 Running build [Desktop] command..."); let t_start = std::time::Instant::now(); let ignore_files = build_assets(config)?; let mut cmd = subprocess::Exec::cmd("cargo") + .env("CARGO_TARGET_DIR", &config.target_dir) .cwd(&config.crate_dir) .arg("build") .arg("--message-format=json"); @@ -375,6 +390,13 @@ pub fn build_desktop(config: &CrateConfig, _is_serve: bool) -> Result Result Result<()> { + let manifest = config.asset_manifest(); + let mut file = File::create(config.out_dir.join("__assets_head.html"))?; + file.write_all(manifest.head().as_bytes())?; + Ok(()) +} + fn prettier_build(cmd: subprocess::Exec) -> anyhow::Result> { let mut warning_messages: Vec = vec![]; - let pb = ProgressBar::new_spinner(); + let mut pb = ProgressBar::new_spinner(); pb.enable_steady_tick(Duration::from_millis(200)); + pb = PROGRESS_BARS.add(pb); pb.set_style( ProgressStyle::with_template("{spinner:.dim.bold} {wide_msg}") .unwrap() @@ -406,14 +436,6 @@ fn prettier_build(cmd: subprocess::Exec) -> anyhow::Result> { ); pb.set_message("💼 Waiting to start build the project..."); - struct StopSpinOnDrop(ProgressBar); - - impl Drop for StopSpinOnDrop { - fn drop(&mut self) { - self.0.finish_and_clear(); - } - } - let stdout = cmd.detached().stream_stdout()?; let reader = std::io::BufReader::new(stdout); @@ -449,13 +471,17 @@ fn prettier_build(cmd: subprocess::Exec) -> anyhow::Result> { std::process::exit(1); } } - _ => (), // Unknown message + _ => { + // Unknown message + } } } Ok(warning_messages) } -pub fn gen_page(config: &DioxusConfig, serve: bool) -> String { +pub fn gen_page(config: &CrateConfig, serve: bool, skip_assets: bool) -> String { + let _gaurd = WebAssetConfigDropGuard::new(); + let crate_root = crate::cargo::crate_root().unwrap(); let custom_html_file = crate_root.join("index.html"); let mut html = if custom_html_file.is_file() { @@ -470,7 +496,7 @@ pub fn gen_page(config: &DioxusConfig, serve: bool) -> String { String::from(include_str!("./assets/index.html")) }; - let resources = config.web.resource.clone(); + let resources = config.dioxus_config.web.resource.clone(); let mut style_list = resources.style.unwrap_or_default(); let mut script_list = resources.script.unwrap_or_default(); @@ -490,6 +516,7 @@ pub fn gen_page(config: &DioxusConfig, serve: bool) -> String { )) } if config + .dioxus_config .application .tools .clone() @@ -498,6 +525,10 @@ pub fn gen_page(config: &DioxusConfig, serve: bool) -> String { { style_str.push_str("\n"); } + if !skip_assets { + let manifest = config.asset_manifest(); + style_str.push_str(&manifest.head()); + } replace_or_insert_before("{style_include}", &style_str, " String { ); } - let base_path = match &config.web.app.base_path { + let base_path = match &config.dioxus_config.web.app.base_path { Some(path) => path, None => ".", }; - let app_name = &config.application.name; + let app_name = &config.dioxus_config.application.name; // Check if a script already exists if html.contains("{app_name}") && html.contains("{base_path}") { html = html.replace("{app_name}", app_name); @@ -547,6 +578,7 @@ pub fn gen_page(config: &DioxusConfig, serve: bool) -> String { } let title = config + .dioxus_config .web .app .title @@ -716,3 +748,42 @@ fn build_assets(config: &CrateConfig) -> Result> { Ok(result) } + +/// Process any assets collected from the binary +fn process_assets(config: &CrateConfig) -> anyhow::Result<()> { + let manifest = config.asset_manifest(); + + let static_asset_output_dir = PathBuf::from( + config + .dioxus_config + .web + .app + .base_path + .clone() + .unwrap_or_default(), + ); + let static_asset_output_dir = config.out_dir.join(static_asset_output_dir); + + manifest.copy_static_assets_to(static_asset_output_dir)?; + + Ok(()) +} + +pub(crate) struct WebAssetConfigDropGuard; + +impl WebAssetConfigDropGuard { + pub fn new() -> Self { + // Set up the collect asset config + manganis_cli_support::Config::default() + .with_assets_serve_location("/") + .save(); + Self {} + } +} + +impl Drop for WebAssetConfigDropGuard { + fn drop(&mut self) { + // Reset the config + manganis_cli_support::Config::default().save(); + } +} diff --git a/packages/cli/src/cli/build.rs b/packages/cli/src/cli/build.rs index 0deeb38ca..d8032f034 100644 --- a/packages/cli/src/cli/build.rs +++ b/packages/cli/src/cli/build.rs @@ -1,6 +1,8 @@ -use crate::cfg::Platform; #[cfg(feature = "plugin")] use crate::plugin::PluginManager; +use crate::server::fullstack::FullstackServerEnvGuard; +use crate::server::fullstack::FullstackWebEnvGuard; +use crate::{cfg::Platform, WebAssetConfigDropGuard}; use super::*; @@ -13,23 +15,26 @@ pub struct Build { } impl Build { - pub fn build(self, bin: Option) -> Result<()> { + pub fn build(self, bin: Option, target_dir: Option<&std::path::Path>) -> Result<()> { let mut crate_config = crate::CrateConfig::new(bin)?; + if let Some(target_dir) = target_dir { + crate_config.target_dir = target_dir.to_path_buf(); + } // change the release state. crate_config.with_release(self.build.release); crate_config.with_verbose(self.build.verbose); if self.build.example.is_some() { - crate_config.as_example(self.build.example.unwrap()); + crate_config.as_example(self.build.example.clone().unwrap()); } if self.build.profile.is_some() { - crate_config.set_profile(self.build.profile.unwrap()); + crate_config.set_profile(self.build.profile.clone().unwrap()); } if self.build.features.is_some() { - crate_config.set_features(self.build.features.unwrap()); + crate_config.set_features(self.build.features.clone().unwrap()); } let platform = self @@ -37,25 +42,56 @@ impl Build { .platform .unwrap_or(crate_config.dioxus_config.application.default_platform); - if let Some(target) = self.build.target { + if let Some(target) = self.build.target.clone() { crate_config.set_target(target); } - crate_config.set_cargo_args(self.build.cargo_args); + crate_config.set_cargo_args(self.build.cargo_args.clone()); // #[cfg(feature = "plugin")] // let _ = PluginManager::on_build_start(&crate_config, &platform); match platform { Platform::Web => { - crate::builder::build(&crate_config, true)?; + crate::builder::build(&crate_config, false, self.build.skip_assets)?; } Platform::Desktop => { - crate::builder::build_desktop(&crate_config, false)?; + crate::builder::build_desktop(&crate_config, false, self.build.skip_assets)?; + } + Platform::Fullstack => { + // Fullstack mode must be built with web configs on the desktop (server) binary as well as the web binary + let _config = WebAssetConfigDropGuard::new(); + { + let mut web_config = crate_config.clone(); + let _gaurd = FullstackWebEnvGuard::new(&self.build); + let web_feature = self.build.client_feature; + let features = &mut web_config.features; + match features { + Some(features) => { + features.push(web_feature); + } + None => web_config.features = Some(vec![web_feature]), + }; + crate::builder::build(&crate_config, false, self.build.skip_assets)?; + } + { + let mut desktop_config = crate_config.clone(); + let desktop_feature = self.build.server_feature; + let features = &mut desktop_config.features; + match features { + Some(features) => { + features.push(desktop_feature); + } + None => desktop_config.features = Some(vec![desktop_feature]), + }; + let _gaurd = + FullstackServerEnvGuard::new(self.build.force_debug, self.build.release); + crate::builder::build_desktop(&desktop_config, false, self.build.skip_assets)?; + } } } - let temp = gen_page(&crate_config.dioxus_config, false); + let temp = gen_page(&crate_config, false, self.build.skip_assets); let mut file = std::fs::File::create( crate_config diff --git a/packages/cli/src/cli/bundle.rs b/packages/cli/src/cli/bundle.rs index fd97930ed..aaa823373 100644 --- a/packages/cli/src/cli/bundle.rs +++ b/packages/cli/src/cli/bundle.rs @@ -83,7 +83,7 @@ impl Bundle { crate_config.set_cargo_args(self.build.cargo_args); // build the desktop app - build_desktop(&crate_config, false)?; + build_desktop(&crate_config, false, false)?; // copy the binary to the out dir let package = crate_config.manifest.package.unwrap(); @@ -134,6 +134,19 @@ impl Bundle { } } + // Add all assets from collect assets to the bundle + { + let config = manganis_cli_support::Config::current(); + let location = config.assets_serve_location().to_string(); + let location = format!("./{}", location); + println!("Adding assets from {} to bundle", location); + if let Some(resources) = &mut bundle_settings.resources { + resources.push(location); + } else { + bundle_settings.resources = Some(vec![location]); + } + } + let mut settings = SettingsBuilder::new() .project_out_directory(crate_config.out_dir) .package_settings(PackageSettings { diff --git a/packages/cli/src/cli/cfg.rs b/packages/cli/src/cli/cfg.rs index 0d2a5e212..62212e574 100644 --- a/packages/cli/src/cli/cfg.rs +++ b/packages/cli/src/cli/cfg.rs @@ -11,6 +11,11 @@ pub struct ConfigOptsBuild { #[serde(default)] pub release: bool, + /// This flag only applies to fullstack builds. By default fullstack builds will run with something in between debug and release mode. This flag will force the build to run in debug mode. [default: false] + #[clap(long)] + #[serde(default)] + pub force_debug: bool, + // Use verbose output [default: false] #[clap(long)] #[serde(default)] @@ -28,10 +33,23 @@ pub struct ConfigOptsBuild { #[clap(long, value_enum)] pub platform: Option, + /// Skip collecting assets from dependencies [default: false] + #[clap(long)] + #[serde(default)] + pub skip_assets: bool, + /// Space separated list of features to activate #[clap(long)] pub features: Option>, + /// The feature to use for the client in a fullstack app [default: "web"] + #[clap(long, default_value_t = { "web".to_string() })] + pub client_feature: String, + + /// The feature to use for the server in a fullstack app [default: "ssr"] + #[clap(long, default_value_t = { "ssr".to_string() })] + pub server_feature: String, + /// Rustc platform triple #[clap(long)] pub target: Option, @@ -41,6 +59,25 @@ pub struct ConfigOptsBuild { pub cargo_args: Vec, } +impl From for ConfigOptsBuild { + fn from(serve: ConfigOptsServe) -> Self { + Self { + target: serve.target, + release: serve.release, + verbose: serve.verbose, + example: serve.example, + profile: serve.profile, + platform: serve.platform, + features: serve.features, + client_feature: serve.client_feature, + server_feature: serve.server_feature, + skip_assets: serve.skip_assets, + force_debug: serve.force_debug, + cargo_args: serve.cargo_args, + } + } +} + #[derive(Clone, Debug, Default, Deserialize, Parser)] pub struct ConfigOptsServe { /// Port of dev server @@ -62,6 +99,11 @@ pub struct ConfigOptsServe { #[serde(default)] pub release: bool, + /// This flag only applies to fullstack builds. By default fullstack builds will run with something in between debug and release mode. This flag will force the build to run in debug mode. [default: false] + #[clap(long)] + #[serde(default)] + pub force_debug: bool, + // Use verbose output [default: false] #[clap(long)] #[serde(default)] @@ -71,7 +113,7 @@ pub struct ConfigOptsServe { #[clap(long)] pub profile: Option, - /// Build platform: support Web & Desktop [default: "default_platform"] + /// Build platform: support Web, Desktop, and Fullstack [default: "default_platform"] #[clap(long, value_enum)] pub platform: Option, @@ -90,6 +132,19 @@ pub struct ConfigOptsServe { #[clap(long)] pub features: Option>, + /// Skip collecting assets from dependencies [default: false] + #[clap(long)] + #[serde(default)] + pub skip_assets: bool, + + /// The feature to use for the client in a fullstack app [default: "web"] + #[clap(long, default_value_t = { "web".to_string() })] + pub client_feature: String, + + /// The feature to use for the server in a fullstack app [default: "ssr"] + #[clap(long, default_value_t = { "ssr".to_string() })] + pub server_feature: String, + /// Rustc platform triple #[clap(long)] pub target: Option, @@ -107,6 +162,9 @@ pub enum Platform { #[clap(name = "desktop")] #[serde(rename = "desktop")] Desktop, + #[clap(name = "fullstack")] + #[serde(rename = "fullstack")] + Fullstack, } /// Config options for the bundling system. diff --git a/packages/cli/src/cli/clean.rs b/packages/cli/src/cli/clean.rs index 5fed1e1a6..9a0f0d4fb 100644 --- a/packages/cli/src/cli/clean.rs +++ b/packages/cli/src/cli/clean.rs @@ -28,6 +28,12 @@ impl Clean { remove_dir_all(crate_config.crate_dir.join(&out_dir))?; } + let fullstack_out_dir = crate_config.crate_dir.join(".dioxus"); + + if fullstack_out_dir.is_dir() { + remove_dir_all(fullstack_out_dir)?; + } + Ok(()) } } diff --git a/packages/cli/src/cli/serve.rs b/packages/cli/src/cli/serve.rs index e08b02339..1bb24ecdb 100644 --- a/packages/cli/src/cli/serve.rs +++ b/packages/cli/src/cli/serve.rs @@ -12,6 +12,7 @@ pub struct Serve { impl Serve { pub async fn serve(self, bin: Option) -> Result<()> { let mut crate_config = crate::CrateConfig::new(bin)?; + let serve_cfg = self.serve.clone(); // change the relase state. crate_config.with_hot_reload(self.serve.hot_reload); @@ -48,21 +49,29 @@ impl Serve { match platform { cfg::Platform::Web => { // generate dev-index page - Serve::regen_dev_page(&crate_config)?; + Serve::regen_dev_page(&crate_config, self.serve.skip_assets)?; // start the develop server - server::web::startup(self.serve.port, crate_config.clone(), self.serve.open) - .await?; + server::web::startup( + self.serve.port, + crate_config.clone(), + self.serve.open, + self.serve.skip_assets, + ) + .await?; } cfg::Platform::Desktop => { - server::desktop::startup(crate_config.clone()).await?; + server::desktop::startup(crate_config.clone(), &serve_cfg).await?; + } + cfg::Platform::Fullstack => { + server::fullstack::startup(crate_config.clone(), &serve_cfg).await?; } } Ok(()) } - pub fn regen_dev_page(crate_config: &CrateConfig) -> Result<()> { - let serve_html = gen_page(&crate_config.dioxus_config, true); + pub fn regen_dev_page(crate_config: &CrateConfig, skip_assets: bool) -> Result<()> { + let serve_html = gen_page(crate_config, true, skip_assets); let dist_path = crate_config.crate_dir.join( crate_config diff --git a/packages/cli/src/config.rs b/packages/cli/src/config.rs index 727504e6a..5b4f23524 100644 --- a/packages/cli/src/config.rs +++ b/packages/cli/src/config.rs @@ -1,4 +1,6 @@ use crate::{cfg::Platform, error::Result}; +use manganis_cli_support::AssetManifest; +use manganis_cli_support::AssetManifestExt; use serde::{Deserialize, Serialize}; use std::{ collections::HashMap, @@ -303,6 +305,13 @@ impl CrateConfig { }) } + pub fn asset_manifest(&self) -> AssetManifest { + AssetManifest::load_from_path( + self.crate_dir.join("Cargo.toml"), + self.workspace_dir.join("Cargo.lock"), + ) + } + pub fn as_example(&mut self, example_name: String) -> &mut Self { self.executable = ExecutableType::Example(example_name); self diff --git a/packages/cli/src/main.rs b/packages/cli/src/main.rs index 2358551f2..0695d7ac8 100644 --- a/packages/cli/src/main.rs +++ b/packages/cli/src/main.rs @@ -63,7 +63,7 @@ async fn main() -> anyhow::Result<()> { .map_err(|e| anyhow!("🚫 Translation of HTML into RSX failed: {}", e)), Build(opts) if bin.is_ok() => opts - .build(Some(bin.unwrap().clone())) + .build(Some(bin.unwrap().clone()), None) .map_err(|e| anyhow!("🚫 Building project failed: {}", e)), Clean(opts) if bin.is_ok() => opts diff --git a/packages/cli/src/server/desktop/mod.rs b/packages/cli/src/server/desktop/mod.rs index ec21fa4c0..34f85a312 100644 --- a/packages/cli/src/server/desktop/mod.rs +++ b/packages/cli/src/server/desktop/mod.rs @@ -1,11 +1,12 @@ +use crate::server::Platform; use crate::{ + cfg::ConfigOptsServe, server::{ output::{print_console_info, PrettierOptions}, setup_file_watcher, }, BuildResult, CrateConfig, Result, }; - use dioxus_hot_reload::HotReloadMsg; use dioxus_html::HtmlCtx; use dioxus_rsx::hot_reload::*; @@ -21,7 +22,14 @@ use plugin::PluginManager; use super::HotReloadState; -pub async fn startup(config: CrateConfig) -> Result<()> { +pub async fn startup(config: CrateConfig, serve: &ConfigOptsServe) -> Result<()> { + startup_with_platform::(config, serve).await +} + +pub(crate) async fn startup_with_platform( + config: CrateConfig, + serve_cfg: &ConfigOptsServe, +) -> Result<()> { // ctrl-c shutdown checker let _crate_config = config.clone(); let _ = ctrlc::set_handler(move || { @@ -51,15 +59,18 @@ pub async fn startup(config: CrateConfig) -> Result<()> { false => None, }; - serve(config, hot_reload_state).await?; + serve::

(config, serve_cfg, hot_reload_state).await?; Ok(()) } /// Start the server without hot reload -pub async fn serve(config: CrateConfig, hot_reload_state: Option) -> Result<()> { - let (child, first_build_result) = start_desktop(&config)?; - let currently_running_child: RwLock = RwLock::new(child); +async fn serve( + config: CrateConfig, + serve: &ConfigOptsServe, + hot_reload_state: Option, +) -> Result<()> { + let platform = RwLock::new(P::start(&config, serve)?); log::info!("🚀 Starting development server..."); @@ -68,15 +79,7 @@ pub async fn serve(config: CrateConfig, hot_reload_state: Option let _watcher = setup_file_watcher( { let config = config.clone(); - - move || { - let mut current_child = currently_running_child.write().unwrap(); - log::trace!("Killing old process"); - current_child.kill()?; - let (child, result) = start_desktop(&config)?; - *current_child = child; - Ok(result) - } + move || platform.write().unwrap().rebuild(&config) }, &config, None, @@ -84,19 +87,9 @@ pub async fn serve(config: CrateConfig, hot_reload_state: Option ) .await?; - // Print serve info - print_console_info( - &config, - PrettierOptions { - changed: vec![], - warnings: first_build_result.warnings, - elapsed_time: first_build_result.elapsed_time, - }, - None, - ); - match hot_reload_state { Some(hot_reload_state) => { + // The open interprocess sockets start_desktop_hot_reload(hot_reload_state).await?; } None => { @@ -192,7 +185,7 @@ async fn start_desktop_hot_reload(hot_reload_state: HotReloadState) -> Result<() } fn clear_paths(file_socket_path: &std::path::Path) { - if cfg!(target_os = "macos") { + if cfg!(unix) { // On unix, if you force quit the application, it can leave the file socket open // This will cause the local socket listener to fail to open // We check if the file socket is already open from an old session and then delete it @@ -217,10 +210,9 @@ fn send_msg(msg: HotReloadMsg, channel: &mut impl std::io::Write) -> bool { } } -pub fn start_desktop(config: &CrateConfig) -> Result<(Child, BuildResult)> { +fn start_desktop(config: &CrateConfig, skip_assets: bool) -> Result<(RAIIChild, BuildResult)> { // Run the desktop application - log::trace!("Building application"); - let result = crate::builder::build_desktop(config, true)?; + let result = crate::builder::build_desktop(config, true, skip_assets)?; match &config.executable { crate::ExecutableType::Binary(name) @@ -230,10 +222,58 @@ pub fn start_desktop(config: &CrateConfig) -> Result<(Child, BuildResult)> { if cfg!(windows) { file.set_extension("exe"); } - log::trace!("Running application from {:?}", file); - let child = Command::new(file.to_str().unwrap()).spawn()?; + let active = "DIOXUS_ACTIVE"; + let child = RAIIChild( + Command::new(file.to_str().unwrap()) + .env(active, "true") + .spawn()?, + ); Ok((child, result)) } } } + +pub(crate) struct DesktopPlatform { + currently_running_child: RAIIChild, + skip_assets: bool, +} + +impl Platform for DesktopPlatform { + fn start(config: &CrateConfig, serve: &ConfigOptsServe) -> Result { + let (child, first_build_result) = start_desktop(config, serve.skip_assets)?; + + log::info!("🚀 Starting development server..."); + + // Print serve info + print_console_info( + config, + PrettierOptions { + changed: vec![], + warnings: first_build_result.warnings, + elapsed_time: first_build_result.elapsed_time, + }, + None, + ); + + Ok(Self { + currently_running_child: child, + skip_assets: serve.skip_assets, + }) + } + + fn rebuild(&mut self, config: &CrateConfig) -> Result { + self.currently_running_child.0.kill()?; + let (child, result) = start_desktop(config, self.skip_assets)?; + self.currently_running_child = child; + Ok(result) + } +} + +struct RAIIChild(Child); + +impl Drop for RAIIChild { + fn drop(&mut self) { + let _ = self.0.kill(); + } +} diff --git a/packages/cli/src/server/fullstack/mod.rs b/packages/cli/src/server/fullstack/mod.rs new file mode 100644 index 000000000..fa9c77459 --- /dev/null +++ b/packages/cli/src/server/fullstack/mod.rs @@ -0,0 +1,161 @@ +use crate::{ + cfg::{ConfigOptsBuild, ConfigOptsServe}, + CrateConfig, Result, WebAssetConfigDropGuard, +}; + +use super::{desktop, Platform}; + +pub async fn startup(config: CrateConfig, serve: &ConfigOptsServe) -> Result<()> { + desktop::startup_with_platform::(config, serve).await +} + +fn start_web_build_thread( + config: &CrateConfig, + serve: &ConfigOptsServe, +) -> std::thread::JoinHandle> { + let serve = serve.clone(); + let target_directory = config.crate_dir.join(".dioxus").join("web"); + std::fs::create_dir_all(&target_directory).unwrap(); + std::thread::spawn(move || build_web(serve, &target_directory)) +} + +struct FullstackPlatform { + serve: ConfigOptsServe, + desktop: desktop::DesktopPlatform, + _config: WebAssetConfigDropGuard, +} + +impl Platform for FullstackPlatform { + fn start(config: &CrateConfig, serve: &ConfigOptsServe) -> Result + where + Self: Sized, + { + let thread_handle = start_web_build_thread(config, serve); + + let mut desktop_config = config.clone(); + let desktop_feature = serve.server_feature.clone(); + let features = &mut desktop_config.features; + match features { + Some(features) => { + features.push(desktop_feature); + } + None => desktop_config.features = Some(vec![desktop_feature]), + }; + let config = WebAssetConfigDropGuard::new(); + let desktop = desktop::DesktopPlatform::start(&desktop_config, serve)?; + thread_handle + .join() + .map_err(|_| anyhow::anyhow!("Failed to join thread"))??; + + Ok(Self { + desktop, + serve: serve.clone(), + _config: config, + }) + } + + fn rebuild(&mut self, crate_config: &CrateConfig) -> Result { + let thread_handle = start_web_build_thread(crate_config, &self.serve); + let result = { + let mut desktop_config = crate_config.clone(); + let desktop_feature = self.serve.server_feature.clone(); + let features = &mut desktop_config.features; + match features { + Some(features) => { + features.push(desktop_feature); + } + None => desktop_config.features = Some(vec![desktop_feature]), + }; + let _gaurd = FullstackServerEnvGuard::new(self.serve.force_debug, self.serve.release); + self.desktop.rebuild(&desktop_config) + }; + thread_handle + .join() + .map_err(|_| anyhow::anyhow!("Failed to join thread"))??; + result + } +} + +fn build_web(serve: ConfigOptsServe, target_directory: &std::path::Path) -> Result<()> { + let mut web_config: ConfigOptsBuild = serve.into(); + let web_feature = web_config.client_feature.clone(); + let features = &mut web_config.features; + match features { + Some(features) => { + features.push(web_feature); + } + None => web_config.features = Some(vec![web_feature]), + }; + web_config.platform = Some(crate::cfg::Platform::Web); + + let _gaurd = FullstackWebEnvGuard::new(&web_config); + crate::cli::build::Build { build: web_config }.build(None, Some(target_directory)) +} + +// Debug mode web builds have a very large size by default. If debug mode is not enabled, we strip some of the debug info by default +// This reduces a hello world from ~40MB to ~2MB +pub(crate) struct FullstackWebEnvGuard { + old_rustflags: Option, +} + +impl FullstackWebEnvGuard { + pub fn new(serve: &ConfigOptsBuild) -> Self { + Self { + old_rustflags: (!serve.force_debug).then(|| { + let old_rustflags = std::env::var("RUSTFLAGS").unwrap_or_default(); + let debug_assertions = if serve.release { + "" + } else { + " -C debug-assertions" + }; + + std::env::set_var( + "RUSTFLAGS", + format!( + "{old_rustflags} -C debuginfo=none -C strip=debuginfo{debug_assertions}" + ), + ); + old_rustflags + }), + } + } +} + +impl Drop for FullstackWebEnvGuard { + fn drop(&mut self) { + if let Some(old_rustflags) = self.old_rustflags.take() { + std::env::set_var("RUSTFLAGS", old_rustflags); + } + } +} + +// Debug mode web builds have a very large size by default. If debug mode is not enabled, we strip some of the debug info by default +// This reduces a hello world from ~40MB to ~2MB +pub(crate) struct FullstackServerEnvGuard { + old_rustflags: Option, +} + +impl FullstackServerEnvGuard { + pub fn new(debug: bool, release: bool) -> Self { + Self { + old_rustflags: (!debug).then(|| { + let old_rustflags = std::env::var("RUSTFLAGS").unwrap_or_default(); + let debug_assertions = if release { "" } else { " -C debug-assertions" }; + + std::env::set_var( + "RUSTFLAGS", + format!("{old_rustflags} -C opt-level=2 {debug_assertions}"), + ); + old_rustflags + }), + } + } +} + +impl Drop for FullstackServerEnvGuard { + fn drop(&mut self) { + if let Some(old_rustflags) = self.old_rustflags.take() { + std::env::set_var("RUSTFLAGS", old_rustflags); + } + } +} diff --git a/packages/cli/src/server/mod.rs b/packages/cli/src/server/mod.rs index eb4021184..895d9e51e 100644 --- a/packages/cli/src/server/mod.rs +++ b/packages/cli/src/server/mod.rs @@ -1,4 +1,4 @@ -use crate::{BuildResult, CrateConfig, Result}; +use crate::{cfg::ConfigOptsServe, BuildResult, CrateConfig, Result}; use cargo_metadata::diagnostic::Diagnostic; use dioxus_core::Template; @@ -14,6 +14,7 @@ use tokio::sync::broadcast::{self}; mod output; use output::*; pub mod desktop; +pub mod fullstack; pub mod web; /// Sets up a file watcher @@ -141,6 +142,13 @@ async fn setup_file_watcher Result + Send + 'static>( Ok(watcher) } +pub(crate) trait Platform { + fn start(config: &CrateConfig, serve: &ConfigOptsServe) -> Result + where + Self: Sized; + fn rebuild(&mut self, config: &CrateConfig) -> Result; +} + #[derive(Clone)] pub struct HotReloadState { pub messages: broadcast::Sender>, diff --git a/packages/cli/src/server/web/mod.rs b/packages/cli/src/server/web/mod.rs index 98ff1e078..896d08643 100644 --- a/packages/cli/src/server/web/mod.rs +++ b/packages/cli/src/server/web/mod.rs @@ -48,7 +48,12 @@ struct WsReloadState { update: broadcast::Sender<()>, } -pub async fn startup(port: u16, config: CrateConfig, start_browser: bool) -> Result<()> { +pub async fn startup( + port: u16, + config: CrateConfig, + start_browser: bool, + skip_assets: bool, +) -> Result<()> { // ctrl-c shutdown checker let _crate_config = config.clone(); let _ = ctrlc::set_handler(move || { @@ -80,7 +85,15 @@ pub async fn startup(port: u16, config: CrateConfig, start_browser: bool) -> Res false => None, }; - serve(ip, port, config, start_browser, hot_reload_state).await?; + serve( + ip, + port, + config, + start_browser, + skip_assets, + hot_reload_state, + ) + .await?; Ok(()) } @@ -91,9 +104,10 @@ pub async fn serve( port: u16, config: CrateConfig, start_browser: bool, + skip_assets: bool, hot_reload_state: Option, ) -> Result<()> { - let first_build_result = crate::builder::build(&config, true)?; + let first_build_result = crate::builder::build(&config, false, skip_assets)?; log::info!("🚀 Starting development server..."); @@ -106,7 +120,7 @@ pub async fn serve( { let config = config.clone(); let reload_tx = reload_tx.clone(); - move || build(&config, &reload_tx) + move || build(&config, &reload_tx, skip_assets) }, &config, Some(WebServerInfo { @@ -420,8 +434,8 @@ async fn ws_handler( }) } -fn build(config: &CrateConfig, reload_tx: &Sender<()>) -> Result { - let result = builder::build(config, true)?; +fn build(config: &CrateConfig, reload_tx: &Sender<()>, skip_assets: bool) -> Result { + let result = builder::build(config, true, skip_assets)?; // change the websocket reload state to true; // the page will auto-reload. if config @@ -431,7 +445,7 @@ fn build(config: &CrateConfig, reload_tx: &Sender<()>) -> Result { .reload_html .unwrap_or(false) { - let _ = Serve::regen_dev_page(config); + let _ = Serve::regen_dev_page(config, skip_assets); } let _ = reload_tx.send(()); Ok(result) diff --git a/packages/core/Cargo.toml b/packages/core/Cargo.toml index ce7c6bccf..6cc7b3ad3 100644 --- a/packages/core/Cargo.toml +++ b/packages/core/Cargo.toml @@ -36,6 +36,7 @@ serde = { version = "1", features = ["derive"], optional = true } [dev-dependencies] tokio = { workspace = true, features = ["full"] } dioxus = { workspace = true } +dioxus-html = { workspace = true, features = ["serialize"] } pretty_assertions = "1.3.0" rand = "0.8.5" dioxus-ssr = { workspace = true } diff --git a/packages/core/src/events.rs b/packages/core/src/events.rs index 784a8c865..4c8e41a63 100644 --- a/packages/core/src/events.rs +++ b/packages/core/src/events.rs @@ -28,6 +28,27 @@ pub struct Event { } impl Event { + /// Map the event data to a new type + /// + /// # Example + /// + /// ```rust, ignore + /// rsx! { + /// button { + /// onclick: move |evt: Event| { + /// let data = evt.map(|data| data.value()); + /// assert_eq!(data.inner(), "hello world"); + /// } + /// } + /// } + /// ``` + pub fn map U>(&self, f: F) -> Event { + Event { + data: Rc::new(f(&self.data)), + propagates: self.propagates.clone(), + } + } + /// Prevent this event from continuing to bubble up the tree to parent elements. /// /// # Example diff --git a/packages/core/src/runtime.rs b/packages/core/src/runtime.rs index 566e9ba7c..6b5e5408a 100644 --- a/packages/core/src/runtime.rs +++ b/packages/core/src/runtime.rs @@ -98,7 +98,7 @@ impl Runtime { } } -/// A gaurd for a new runtime. This must be used to override the current runtime when importing components from a dynamic library that has it's own runtime. +/// A guard for a new runtime. This must be used to override the current runtime when importing components from a dynamic library that has it's own runtime. /// /// ```rust /// use dioxus::prelude::*; diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index 1a26d3847..ef4a5771f 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -2,7 +2,6 @@ use crate::{ any_props::AnyProps, any_props::VProps, bump_frame::BumpFrame, - innerlude::ErrorBoundary, innerlude::{DynamicNode, EventHandler, VComponent, VNodeId, VText}, lazynodes::LazyNodes, nodes::{IntoAttributeValue, IntoDynNode, RenderReturn}, diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index f469cd9e2..9327e8f29 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -16,7 +16,9 @@ use crate::{ use futures_util::{pin_mut, StreamExt}; use rustc_hash::{FxHashMap, FxHashSet}; use slab::Slab; -use std::{any::Any, cell::Cell, collections::BTreeSet, future::Future, ptr::NonNull, rc::Rc, sync::Arc}; +use std::{ + any::Any, cell::Cell, collections::BTreeSet, future::Future, ptr::NonNull, rc::Rc, sync::Arc, +}; /// A virtual node system that progresses user events and diffs UI trees. /// diff --git a/packages/core/tests/event_propagation.rs b/packages/core/tests/event_propagation.rs index d694b1ff1..d9d96ac32 100644 --- a/packages/core/tests/event_propagation.rs +++ b/packages/core/tests/event_propagation.rs @@ -6,11 +6,13 @@ static CLICKS: Mutex = Mutex::new(0); #[test] fn events_propagate() { + set_event_converter(Box::new(dioxus_html::SerializedHtmlEventConverter)); + let mut dom = VirtualDom::new(app); _ = dom.rebuild(); // Top-level click is registered - dom.handle_event("click", Rc::new(MouseData::default()), ElementId(1), true); + dom.handle_event("click", Rc::new(PlatformEventData::new(Box::::default())), ElementId(1), true); assert_eq!(*CLICKS.lock().unwrap(), 1); // break reference.... @@ -20,7 +22,7 @@ fn events_propagate() { } // Lower click is registered - dom.handle_event("click", Rc::new(MouseData::default()), ElementId(2), true); + dom.handle_event("click", Rc::new(PlatformEventData::new(Box::::default())), ElementId(2), true); assert_eq!(*CLICKS.lock().unwrap(), 3); // break reference.... @@ -30,14 +32,13 @@ fn events_propagate() { } // Stop propagation occurs - dom.handle_event("click", Rc::new(MouseData::default()), ElementId(2), true); + dom.handle_event("click", Rc::new(PlatformEventData::new(Box::::default())), ElementId(2), true); assert_eq!(*CLICKS.lock().unwrap(), 3); } fn app(cx: Scope) -> Element { render! { - div { - onclick: move |_| { + div { onclick: move |_| { println!("top clicked"); *CLICKS.lock().unwrap() += 1; }, @@ -53,17 +54,14 @@ fn app(cx: Scope) -> Element { fn problematic_child(cx: Scope) -> Element { render! { - button { - onclick: move |evt| { + button { onclick: move |evt| { println!("bottom clicked"); let mut clicks = CLICKS.lock().unwrap(); - if *clicks == 3 { evt.stop_propagation(); } else { *clicks += 1; } - } - } + } } } } diff --git a/packages/core/tests/lifecycle.rs b/packages/core/tests/lifecycle.rs index 08b342631..6db294653 100644 --- a/packages/core/tests/lifecycle.rs +++ b/packages/core/tests/lifecycle.rs @@ -4,6 +4,7 @@ //! Tests for the lifecycle of components. use dioxus::core::{ElementId, Mutation::*}; use dioxus::prelude::*; +use dioxus_html::SerializedHtmlEventConverter; use std::rc::Rc; use std::sync::{Arc, Mutex}; @@ -39,6 +40,7 @@ fn manual_diffing() { #[test] fn events_generate() { + set_event_converter(Box::new(SerializedHtmlEventConverter)); fn app(cx: Scope) -> Element { let count = cx.use_hook(|| 0); @@ -56,7 +58,12 @@ fn events_generate() { let mut dom = VirtualDom::new(app); _ = dom.rebuild(); - dom.handle_event("click", Rc::new(MouseData::default()), ElementId(1), true); + dom.handle_event( + "click", + Rc::new(PlatformEventData::new(Box::::default())), + ElementId(1), + true, + ); dom.mark_dirty(ScopeId::ROOT); let edits = dom.render_immediate(); diff --git a/packages/core/tests/miri_full_app.rs b/packages/core/tests/miri_full_app.rs index 33cbd9620..c285db7d5 100644 --- a/packages/core/tests/miri_full_app.rs +++ b/packages/core/tests/miri_full_app.rs @@ -1,15 +1,23 @@ +use crate::dioxus_elements::SerializedMouseData; use dioxus::prelude::*; use dioxus_core::ElementId; +use dioxus_elements::SerializedHtmlEventConverter; use std::rc::Rc; #[test] fn miri_rollover() { + set_event_converter(Box::new(SerializedHtmlEventConverter)); let mut dom = VirtualDom::new(App); _ = dom.rebuild(); for _ in 0..3 { - dom.handle_event("click", Rc::new(MouseData::default()), ElementId(2), true); + dom.handle_event( + "click", + Rc::new(PlatformEventData::new(Box::::default())), + ElementId(2), + true, + ); dom.process_events(); _ = dom.render_immediate(); } diff --git a/packages/core/tests/task.rs b/packages/core/tests/task.rs index 1d190ccaf..e1ff55c85 100644 --- a/packages/core/tests/task.rs +++ b/packages/core/tests/task.rs @@ -1,13 +1,33 @@ //! Verify that tasks get polled by the virtualdom properly, and that we escape wait_for_work safely -use dioxus::prelude::*; -use std::{sync::atomic::AtomicUsize, time::Duration}; - -static POLL_COUNT: AtomicUsize = AtomicUsize::new(0); - #[cfg(not(miri))] #[tokio::test] async fn it_works() { + use dioxus::prelude::*; + use std::{sync::atomic::AtomicUsize, time::Duration}; + + static POLL_COUNT: AtomicUsize = AtomicUsize::new(0); + + fn app(cx: Scope) -> Element { + cx.use_hook(|| { + cx.spawn(async { + for x in 0..10 { + tokio::time::sleep(Duration::from_micros(50)).await; + POLL_COUNT.fetch_add(x, std::sync::atomic::Ordering::Relaxed); + } + }); + + cx.spawn(async { + for x in 0..10 { + tokio::time::sleep(Duration::from_micros(25)).await; + POLL_COUNT.fetch_add(x * 2, std::sync::atomic::Ordering::Relaxed); + } + }); + }); + + cx.render(rsx!(())) + } + let mut dom = VirtualDom::new(app); let _ = dom.rebuild(); @@ -24,23 +44,3 @@ async fn it_works() { 135 ); } - -fn app(cx: Scope) -> Element { - cx.use_hook(|| { - cx.spawn(async { - for x in 0..10 { - tokio::time::sleep(Duration::from_micros(50)).await; - POLL_COUNT.fetch_add(x, std::sync::atomic::Ordering::Relaxed); - } - }); - - cx.spawn(async { - for x in 0..10 { - tokio::time::sleep(Duration::from_micros(25)).await; - POLL_COUNT.fetch_add(x * 2, std::sync::atomic::Ordering::Relaxed); - } - }); - }); - - cx.render(rsx!(())) -} diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index 4eed74b84..7f4933a7e 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -11,7 +11,12 @@ keywords = ["dom", "ui", "gui", "react"] [dependencies] dioxus-core = { workspace = true, features = ["serialize"] } -dioxus-html = { workspace = true, features = ["serialize", "native-bind"] } +dioxus-html = { workspace = true, features = [ + "serialize", + "native-bind", + "mounted", + "eval", +] } dioxus-interpreter-js = { workspace = true, features = ["binary-protocol"] } dioxus-hot-reload = { workspace = true, optional = true } @@ -45,8 +50,12 @@ crossbeam-channel = "0.5.8" tao = { version = "0.24.0", features = ["rwh_05"] } muda = "0.11.3" - [target.'cfg(any(target_os = "windows",target_os = "macos",target_os = "linux",target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd", target_os = "openbsd"))'.dependencies] +# This is only for debug mode, and it appears mobile does not support some packages this uses +manganis-cli-support = { git = "https://github.com/DioxusLabs/collect-assets", features = [ + "webp", + "html", +] } rfd = "0.12" global-hotkey = "0.4.1" diff --git a/packages/desktop/headless_tests/events.rs b/packages/desktop/headless_tests/events.rs index 4660441a2..061c040ed 100644 --- a/packages/desktop/headless_tests/events.rs +++ b/packages/desktop/headless_tests/events.rs @@ -17,7 +17,7 @@ pub(crate) fn check_app_exits(app: Component) { dioxus_desktop::launch_cfg( app, - Config::new().with_window(WindowBuilder::new().with_visible(false)), + Config::new().with_window(WindowBuilder::new().with_visible(true)), ); // Stop deadman's switch @@ -221,7 +221,7 @@ fn app(cx: Scope) -> Element { desktop_context.close(); } - cx.render(rsx! { + render! { div { button { id: "button", @@ -229,7 +229,10 @@ fn app(cx: Scope) -> Element { println!("{:?}", event.data); assert!(event.data.modifiers().is_empty()); assert!(event.data.held_buttons().is_empty()); - assert_eq!(event.data.trigger_button(), Some(dioxus_html::input_data::MouseButton::Primary)); + assert_eq!( + event.data.trigger_button(), + Some(dioxus_html::input_data::MouseButton::Primary), + ); received_events.modify(|x| *x + 1) } } @@ -238,7 +241,12 @@ fn app(cx: Scope) -> Element { onmousemove: move |event| { println!("{:?}", event.data); assert!(event.data.modifiers().is_empty()); - assert!(event.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Secondary)); + assert!( + event + .data + .held_buttons() + .contains(dioxus_html::input_data::MouseButton::Secondary), + ); received_events.modify(|x| *x + 1) } } @@ -247,8 +255,16 @@ fn app(cx: Scope) -> Element { onclick: move |event| { println!("{:?}", event.data); assert!(event.data.modifiers().is_empty()); - assert!(event.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Secondary)); - assert_eq!(event.data.trigger_button(), Some(dioxus_html::input_data::MouseButton::Secondary)); + assert!( + event + .data + .held_buttons() + .contains(dioxus_html::input_data::MouseButton::Secondary), + ); + assert_eq!( + event.data.trigger_button(), + Some(dioxus_html::input_data::MouseButton::Secondary), + ); received_events.modify(|x| *x + 1) } } @@ -257,9 +273,19 @@ fn app(cx: Scope) -> Element { ondoubleclick: move |event| { println!("{:?}", event.data); assert!(event.data.modifiers().is_empty()); - assert!(event.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Primary)); - assert!(event.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Secondary)); - assert_eq!(event.data.trigger_button(), Some(dioxus_html::input_data::MouseButton::Secondary)); + assert!( + event.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Primary), + ); + assert!( + event + .data + .held_buttons() + .contains(dioxus_html::input_data::MouseButton::Secondary), + ); + assert_eq!( + event.data.trigger_button(), + Some(dioxus_html::input_data::MouseButton::Secondary), + ); received_events.modify(|x| *x + 1) } } @@ -268,8 +294,16 @@ fn app(cx: Scope) -> Element { onmousedown: move |event| { println!("{:?}", event.data); assert!(event.data.modifiers().is_empty()); - assert!(event.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Secondary)); - assert_eq!(event.data.trigger_button(), Some(dioxus_html::input_data::MouseButton::Secondary)); + assert!( + event + .data + .held_buttons() + .contains(dioxus_html::input_data::MouseButton::Secondary), + ); + assert_eq!( + event.data.trigger_button(), + Some(dioxus_html::input_data::MouseButton::Secondary), + ); received_events.modify(|x| *x + 1) } } @@ -279,7 +313,10 @@ fn app(cx: Scope) -> Element { println!("{:?}", event.data); assert!(event.data.modifiers().is_empty()); assert!(event.data.held_buttons().is_empty()); - assert_eq!(event.data.trigger_button(), Some(dioxus_html::input_data::MouseButton::Primary)); + assert_eq!( + event.data.trigger_button(), + Some(dioxus_html::input_data::MouseButton::Primary), + ); received_events.modify(|x| *x + 1) } } @@ -303,10 +340,9 @@ fn app(cx: Scope) -> Element { assert!(event.data.modifiers().is_empty()); assert_eq!(event.data.key().to_string(), "a"); assert_eq!(event.data.code().to_string(), "KeyA"); - assert_eq!(event.data.location, 0); + assert_eq!(event.data.location(), Location::Standard); assert!(event.data.is_auto_repeating()); received_events.modify(|x| *x + 1) - } } input { @@ -316,7 +352,7 @@ fn app(cx: Scope) -> Element { assert!(event.data.modifiers().is_empty()); assert_eq!(event.data.key().to_string(), "a"); assert_eq!(event.data.code().to_string(), "KeyA"); - assert_eq!(event.data.location, 0); + assert_eq!(event.data.location(), Location::Standard); assert!(!event.data.is_auto_repeating()); received_events.modify(|x| *x + 1) } @@ -328,7 +364,7 @@ fn app(cx: Scope) -> Element { assert!(event.data.modifiers().is_empty()); assert_eq!(event.data.key().to_string(), "a"); assert_eq!(event.data.code().to_string(), "KeyA"); - assert_eq!(event.data.location, 0); + assert_eq!(event.data.location(), Location::Standard); assert!(!event.data.is_auto_repeating()); received_events.modify(|x| *x + 1) } @@ -348,5 +384,5 @@ fn app(cx: Scope) -> Element { } } } - }) + } } diff --git a/packages/desktop/headless_tests/rendering.rs b/packages/desktop/headless_tests/rendering.rs index f228b79da..50d7b8419 100644 --- a/packages/desktop/headless_tests/rendering.rs +++ b/packages/desktop/headless_tests/rendering.rs @@ -16,7 +16,7 @@ pub(crate) fn check_app_exits(app: Component) { dioxus_desktop::launch_cfg( app, - Config::new().with_window(WindowBuilder::new().with_visible(false)), + Config::new().with_window(WindowBuilder::new().with_visible(true)), ); should_panic.store(false, std::sync::atomic::Ordering::SeqCst); diff --git a/packages/desktop/src/collect_assets.rs b/packages/desktop/src/collect_assets.rs new file mode 100644 index 000000000..aa9050a75 --- /dev/null +++ b/packages/desktop/src/collect_assets.rs @@ -0,0 +1,60 @@ +pub fn copy_assets() { + #[cfg(all( + debug_assertions, + any( + target_os = "windows", + target_os = "macos", + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ) + ))] + { + // The CLI will copy assets to the current working directory + if std::env::var_os("DIOXUS_ACTIVE").is_some() { + return; + } + use manganis_cli_support::AssetManifest; + use manganis_cli_support::AssetManifestExt; + use manganis_cli_support::Config; + use std::path::PathBuf; + let config = Config::current(); + let asset_location = config.assets_serve_location(); + let asset_location = PathBuf::from(asset_location); + let _ = std::fs::remove_dir_all(&asset_location); + + println!("Finding assets... (Note: if you run a dioxus desktop application with the CLI. This process will be significantly faster.)"); + let manifest = AssetManifest::load(); + let has_assets = manifest + .packages() + .iter() + .any(|package| !package.assets().is_empty()); + + if has_assets { + println!("Copying and optimizing assets..."); + manifest.copy_static_assets_to(&asset_location).unwrap(); + println!("Copied assets to {}", asset_location.display()); + } else { + println!("No assets found"); + } + } + #[cfg(not(all( + debug_assertions, + any( + target_os = "windows", + target_os = "macos", + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ) + )))] + { + println!( + "Skipping assets in release mode. You compile assets with the dioxus-cli in release mode" + ); + } +} diff --git a/packages/desktop/src/element.rs b/packages/desktop/src/element.rs index b5f12c3f2..4c9dfbbe2 100644 --- a/packages/desktop/src/element.rs +++ b/packages/desktop/src/element.rs @@ -3,6 +3,7 @@ use dioxus_html::{geometry::euclid::Rect, MountedResult, RenderedElementBacking} use crate::{desktop_context::DesktopContext, query::QueryEngine}; +#[derive(Clone)] /// A mounted element passed to onmounted events pub struct DesktopElement { id: ElementId, @@ -17,8 +18,8 @@ impl DesktopElement { } impl RenderedElementBacking for DesktopElement { - fn get_raw_element(&self) -> dioxus_html::MountedResult<&dyn std::any::Any> { - Ok(self) + fn as_any(&self) -> &dyn std::any::Any { + self } fn get_client_rect( diff --git a/packages/desktop/src/events.rs b/packages/desktop/src/events.rs new file mode 100644 index 000000000..a4fbb4e1e --- /dev/null +++ b/packages/desktop/src/events.rs @@ -0,0 +1,166 @@ +//! Convert a serialized event to an event trigger + +use dioxus_html::*; +use serde::{Deserialize, Serialize}; + +use crate::element::DesktopElement; + +#[derive(Deserialize, Serialize, Debug, Clone)] +pub struct IpcMessage { + method: String, + params: serde_json::Value, +} + +impl IpcMessage { + pub(crate) fn method(&self) -> &str { + self.method.as_str() + } + + pub(crate) fn params(self) -> serde_json::Value { + self.params + } +} + +pub(crate) struct SerializedHtmlEventConverter; + +impl HtmlEventConverter for SerializedHtmlEventConverter { + fn convert_animation_data(&self, event: &PlatformEventData) -> AnimationData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_clipboard_data(&self, event: &PlatformEventData) -> ClipboardData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_composition_data(&self, event: &PlatformEventData) -> CompositionData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_drag_data(&self, event: &PlatformEventData) -> DragData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_focus_data(&self, event: &PlatformEventData) -> FocusData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_form_data(&self, event: &PlatformEventData) -> FormData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_image_data(&self, event: &PlatformEventData) -> ImageData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_keyboard_data(&self, event: &PlatformEventData) -> KeyboardData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_media_data(&self, event: &PlatformEventData) -> MediaData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_mounted_data(&self, event: &PlatformEventData) -> MountedData { + event.downcast::().cloned().unwrap().into() + } + + fn convert_mouse_data(&self, event: &PlatformEventData) -> MouseData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_pointer_data(&self, event: &PlatformEventData) -> PointerData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_scroll_data(&self, event: &PlatformEventData) -> ScrollData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_selection_data(&self, event: &PlatformEventData) -> SelectionData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_toggle_data(&self, event: &PlatformEventData) -> ToggleData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_touch_data(&self, event: &PlatformEventData) -> TouchData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_transition_data(&self, event: &PlatformEventData) -> TransitionData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_wheel_data(&self, event: &PlatformEventData) -> WheelData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } +} diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 3f8d9d3ce..d85b95ef4 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -6,6 +6,7 @@ mod app; mod assets; mod cfg; +mod collect_assets; mod desktop_context; mod edits; mod element; diff --git a/packages/desktop/src/protocol.rs b/packages/desktop/src/protocol.rs index be3c12231..a721a3cfc 100644 --- a/packages/desktop/src/protocol.rs +++ b/packages/desktop/src/protocol.rs @@ -144,6 +144,11 @@ fn module_loader(root_id: &str, headless: bool) -> String { /// - [ ] Linux (deb) /// - [ ] iOS /// - [ ] Android +#[allow(unreachable_code)] +pub(crate) fn get_asset_root_or_default() -> PathBuf { + get_asset_root().unwrap_or_else(|| Path::new(".").to_path_buf()) +} + #[allow(unreachable_code)] fn get_asset_root() -> Option { // If running under cargo, there's no bundle! diff --git a/packages/dioxus-tui/examples/hover.rs b/packages/dioxus-tui/examples/hover.rs index 3af2d1616..2c2cda17d 100644 --- a/packages/dioxus-tui/examples/hover.rs +++ b/packages/dioxus-tui/examples/hover.rs @@ -56,7 +56,6 @@ fn app(cx: Scope) -> Element { width: "100%", height: "100%", flex_direction: "column", - div { width: "100%", height: "50%", @@ -71,7 +70,7 @@ fn app(cx: Scope) -> Element { onmouseenter: move |m| q1_color.set([get_brightness(m.inner()), 0, 0]), onmousedown: move |m| q1_color.set([get_brightness(m.inner()), 0, 0]), onmouseup: move |m| q1_color.set([get_brightness(m.inner()), 0, 0]), - onwheel: move |w| q1_color.set([q1_color[0] + (10.0*w.delta().strip_units().y) as i32, 0, 0]), + onwheel: move |w| q1_color.set([q1_color[0] + (10.0 * w.delta().strip_units().y) as i32, 0, 0]), onmouseleave: move |_| q1_color.set([200; 3]), onmousemove: update_data, "click me" @@ -85,7 +84,7 @@ fn app(cx: Scope) -> Element { onmouseenter: move |m| q2_color.set([get_brightness(m.inner()); 3]), onmousedown: move |m| q2_color.set([get_brightness(m.inner()); 3]), onmouseup: move |m| q2_color.set([get_brightness(m.inner()); 3]), - onwheel: move |w| q2_color.set([q2_color[0] + (10.0*w.delta().strip_units().y) as i32;3]), + onwheel: move |w| q2_color.set([q2_color[0] + (10.0 * w.delta().strip_units().y) as i32; 3]), onmouseleave: move |_| q2_color.set([200; 3]), onmousemove: update_data, "click me" @@ -105,7 +104,7 @@ fn app(cx: Scope) -> Element { onmouseenter: move |m| q3_color.set([0, get_brightness(m.inner()), 0]), onmousedown: move |m| q3_color.set([0, get_brightness(m.inner()), 0]), onmouseup: move |m| q3_color.set([0, get_brightness(m.inner()), 0]), - onwheel: move |w| q3_color.set([0, q3_color[1] + (10.0*w.delta().strip_units().y) as i32, 0]), + onwheel: move |w| q3_color.set([0, q3_color[1] + (10.0 * w.delta().strip_units().y) as i32, 0]), onmouseleave: move |_| q3_color.set([200; 3]), onmousemove: update_data, "click me" @@ -119,16 +118,16 @@ fn app(cx: Scope) -> Element { onmouseenter: move |m| q4_color.set([0, 0, get_brightness(m.inner())]), onmousedown: move |m| q4_color.set([0, 0, get_brightness(m.inner())]), onmouseup: move |m| q4_color.set([0, 0, get_brightness(m.inner())]), - onwheel: move |w| q4_color.set([0, 0, q4_color[2] + (10.0*w.delta().strip_units().y) as i32]), + onwheel: move |w| q4_color.set([0, 0, q4_color[2] + (10.0 * w.delta().strip_units().y) as i32]), onmouseleave: move |_| q4_color.set([200; 3]), onmousemove: update_data, "click me" } - }, - div {"Page coordinates: {page_coordinates}"}, - div {"Element coordinates: {element_coordinates}"}, - div {"Buttons: {buttons}"}, - div {"Modifiers: {modifiers}"}, + } + div { "Page coordinates: {page_coordinates}" } + div { "Element coordinates: {element_coordinates}" } + div { "Buttons: {buttons}" } + div { "Modifiers: {modifiers}" } } }) } diff --git a/packages/dioxus-tui/examples/widgets.rs b/packages/dioxus-tui/examples/widgets.rs index b5df35388..2d0a5bf72 100644 --- a/packages/dioxus-tui/examples/widgets.rs +++ b/packages/dioxus-tui/examples/widgets.rs @@ -18,7 +18,7 @@ fn app(cx: Scope) -> Element { justify_content: "center", input { - oninput: |data| if &data.value == "good"{ + oninput: |data| if &data.value()== "good"{ bg_green.set(true); } else{ bg_green.set(false); @@ -30,7 +30,7 @@ fn app(cx: Scope) -> Element { checked: "true", } input { - oninput: |data| if &data.value == "hello world"{ + oninput: |data| if &data.value()== "hello world"{ bg_green.set(true); } else{ bg_green.set(false); @@ -41,7 +41,7 @@ fn app(cx: Scope) -> Element { } input { oninput: |data| { - if (data.value.parse::().unwrap() - 40.0).abs() < 5.0 { + if (data.value().parse::().unwrap() - 40.0).abs() < 5.0 { bg_green.set(true); } else{ bg_green.set(false); @@ -55,7 +55,7 @@ fn app(cx: Scope) -> Element { } input { oninput: |data| { - if data.value == "10"{ + if data.value()== "10"{ bg_green.set(true); } else{ bg_green.set(false); @@ -68,7 +68,7 @@ fn app(cx: Scope) -> Element { } input { oninput: |data| { - if data.value == "hello world"{ + if data.value()== "hello world"{ bg_green.set(true); } else{ bg_green.set(false); diff --git a/packages/dioxus-tui/src/element.rs b/packages/dioxus-tui/src/element.rs index eeb4d5b86..8fdb60b9f 100644 --- a/packages/dioxus-tui/src/element.rs +++ b/packages/dioxus-tui/src/element.rs @@ -1,7 +1,6 @@ use std::{ any::Any, fmt::{Display, Formatter}, - rc::Rc, }; use dioxus_core::{ElementId, Mutations, VirtualDom}; @@ -30,7 +29,7 @@ pub(crate) fn find_mount_events(mutations: &Mutations) -> Vec { // We need to queue the mounted events to give rink time to rendere and resolve the layout of elements after they are created pub(crate) fn create_mounted_events( vdom: &VirtualDom, - events: &mut Vec<(ElementId, &'static str, Rc, bool)>, + events: &mut Vec<(ElementId, &'static str, Box, bool)>, mount_events: impl Iterator, ) { let query: Query = vdom @@ -42,11 +41,12 @@ pub(crate) fn create_mounted_events( query: query.clone(), id: node_id, }; - events.push((id, "mounted", Rc::new(MountedData::new(element)), false)); + events.push((id, "mounted", Box::new(MountedData::new(element)), false)); } } -struct TuiElement { +#[derive(Clone)] +pub(crate) struct TuiElement { query: Query, id: NodeId, } @@ -82,8 +82,8 @@ impl RenderedElementBacking for TuiElement { }) } - fn get_raw_element(&self) -> dioxus_html::MountedResult<&dyn std::any::Any> { - Ok(self) + fn as_any(&self) -> &dyn std::any::Any { + self } } diff --git a/packages/dioxus-tui/src/events.rs b/packages/dioxus-tui/src/events.rs new file mode 100644 index 000000000..3e7b6fd2a --- /dev/null +++ b/packages/dioxus-tui/src/events.rs @@ -0,0 +1,108 @@ +use core::panic; + +use dioxus_html::*; + +use crate::element::TuiElement; + +fn downcast(event: &PlatformEventData) -> plasmo::EventData { + event + .downcast::() + .expect("event should be of type EventData") + .clone() +} + +pub(crate) struct SerializedHtmlEventConverter; + +impl HtmlEventConverter for SerializedHtmlEventConverter { + fn convert_animation_data(&self, _: &PlatformEventData) -> AnimationData { + panic!("animation events not supported") + } + + fn convert_clipboard_data(&self, _: &PlatformEventData) -> ClipboardData { + panic!("clipboard events not supported") + } + + fn convert_composition_data(&self, _: &PlatformEventData) -> CompositionData { + panic!("composition events not supported") + } + + fn convert_drag_data(&self, _: &PlatformEventData) -> DragData { + panic!("drag events not supported") + } + + fn convert_focus_data(&self, event: &PlatformEventData) -> FocusData { + if let plasmo::EventData::Focus(event) = downcast(event) { + FocusData::new(event) + } else { + panic!("event should be of type Focus") + } + } + + fn convert_form_data(&self, event: &PlatformEventData) -> FormData { + if let plasmo::EventData::Form(event) = downcast(event) { + FormData::new(event) + } else { + panic!("event should be of type Form") + } + } + + fn convert_image_data(&self, _: &PlatformEventData) -> ImageData { + panic!("image events not supported") + } + + fn convert_keyboard_data(&self, event: &PlatformEventData) -> KeyboardData { + if let plasmo::EventData::Keyboard(event) = downcast(event) { + KeyboardData::new(event) + } else { + panic!("event should be of type Keyboard") + } + } + + fn convert_media_data(&self, _: &PlatformEventData) -> MediaData { + panic!("media events not supported") + } + + fn convert_mounted_data(&self, event: &PlatformEventData) -> MountedData { + event.downcast::().cloned().unwrap().into() + } + + fn convert_mouse_data(&self, event: &PlatformEventData) -> MouseData { + if let plasmo::EventData::Mouse(event) = downcast(event) { + MouseData::new(event) + } else { + panic!("event should be of type Mouse") + } + } + + fn convert_pointer_data(&self, _: &PlatformEventData) -> PointerData { + panic!("pointer events not supported") + } + + fn convert_scroll_data(&self, _: &PlatformEventData) -> ScrollData { + panic!("scroll events not supported") + } + + fn convert_selection_data(&self, _: &PlatformEventData) -> SelectionData { + panic!("selection events not supported") + } + + fn convert_toggle_data(&self, _: &PlatformEventData) -> ToggleData { + panic!("toggle events not supported") + } + + fn convert_touch_data(&self, _: &PlatformEventData) -> TouchData { + panic!("touch events not supported") + } + + fn convert_transition_data(&self, _: &PlatformEventData) -> TransitionData { + panic!("transition events not supported") + } + + fn convert_wheel_data(&self, event: &PlatformEventData) -> WheelData { + if let plasmo::EventData::Wheel(event) = downcast(event) { + WheelData::new(event) + } else { + panic!("event should be of type Wheel") + } + } +} diff --git a/packages/dioxus-tui/src/lib.rs b/packages/dioxus-tui/src/lib.rs index ca0f0a07d..fff318c47 100644 --- a/packages/dioxus-tui/src/lib.rs +++ b/packages/dioxus-tui/src/lib.rs @@ -3,6 +3,7 @@ #![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")] mod element; +mod events; use std::{ any::Any, @@ -12,6 +13,7 @@ use std::{ }; use dioxus_core::{Component, ElementId, VirtualDom}; +use dioxus_html::PlatformEventData; use dioxus_native_core::dioxus::{DioxusState, NodeImmutableDioxusExt}; use dioxus_native_core::prelude::*; @@ -28,6 +30,8 @@ pub fn launch_cfg(app: Component<()>, cfg: Config) { } pub fn launch_cfg_with_props(app: Component, props: Props, cfg: Config) { + dioxus_html::set_event_converter(Box::new(events::SerializedHtmlEventConverter)); + render(cfg, |rdom, taffy, event_tx| { let dioxus_state = { let mut rdom = rdom.write().unwrap(); @@ -85,7 +89,7 @@ struct DioxusRenderer { vdom: VirtualDom, dioxus_state: Rc>, // Events that are queued up to be sent to the vdom next time the vdom is polled - queued_events: Vec<(ElementId, &'static str, Rc, bool)>, + queued_events: Vec<(ElementId, &'static str, Box, bool)>, #[cfg(all(feature = "hot-reload", debug_assertions))] hot_reload_rx: tokio::sync::mpsc::UnboundedReceiver, } @@ -126,15 +130,19 @@ impl Driver for DioxusRenderer { let id = { rdom.read().unwrap().get(id).unwrap().mounted_id() }; if let Some(id) = id { let inner_value = value.deref().clone(); + let boxed_event = Box::new(inner_value); + let platform_event = PlatformEventData::new(boxed_event); self.vdom - .handle_event(event, inner_value.into_any(), id, bubbles); + .handle_event(event, Rc::new(platform_event), id, bubbles); } } fn poll_async(&mut self) -> std::pin::Pin + '_>> { // Add any queued events for (id, event, value, bubbles) in self.queued_events.drain(..) { - self.vdom.handle_event(event, value, id, bubbles); + let platform_event = PlatformEventData::new(value); + self.vdom + .handle_event(event, Rc::new(platform_event), id, bubbles); } #[cfg(all(feature = "hot-reload", debug_assertions))] diff --git a/packages/dioxus-tui/tests/events.rs b/packages/dioxus-tui/tests/events.rs index 671cfc349..4d2639371 100644 --- a/packages/dioxus-tui/tests/events.rs +++ b/packages/dioxus-tui/tests/events.rs @@ -63,7 +63,7 @@ fn key_down() { onkeydown: move |evt| { assert_eq!(evt.data.code(), Code::KeyA); tui_ctx.quit(); - }, + } } }) } @@ -95,9 +95,11 @@ fn mouse_down() { width: "100%", height: "100%", onmousedown: move |evt| { - assert!(evt.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Primary)); + assert!( + evt.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Primary) + ); tui_ctx.quit(); - }, + } } }) } @@ -136,7 +138,7 @@ fn mouse_up() { height: "100%", onmouseup: move |_| { tui_ctx.quit(); - }, + } } }) } @@ -175,7 +177,7 @@ fn mouse_enter() { height: "50%", onmouseenter: move |_| { tui_ctx.quit(); - }, + } } }) } @@ -214,7 +216,7 @@ fn mouse_exit() { height: "50%", onmouseenter: move |_| { tui_ctx.quit(); - }, + } } }) } @@ -251,9 +253,9 @@ fn mouse_move() { div { width: "100%", height: "100%", - onmousemove: move |_|{ + onmousemove: move |_| { tui_ctx.quit(); - }, + } } }) } @@ -293,7 +295,7 @@ fn wheel() { onwheel: move |evt| { assert!(evt.data.delta().strip_units().y > 0.0); tui_ctx.quit(); - }, + } } }) } @@ -330,9 +332,9 @@ fn click() { div { width: "100%", height: "100%", - onclick: move |_|{ + onclick: move |_| { tui_ctx.quit(); - }, + } } }) } @@ -369,9 +371,9 @@ fn context_menu() { div { width: "100%", height: "100%", - oncontextmenu: move |_|{ + oncontextmenu: move |_| { tui_ctx.quit(); - }, + } } }) } diff --git a/packages/dioxus/Cargo.toml b/packages/dioxus/Cargo.toml index 2c284bf1a..170f0321e 100644 --- a/packages/dioxus/Cargo.toml +++ b/packages/dioxus/Cargo.toml @@ -16,6 +16,7 @@ dioxus-html = { workspace = true, optional = true } dioxus-core-macro = { workspace = true, optional = true } dioxus-hooks = { workspace = true, optional = true } dioxus-rsx = { workspace = true, optional = true } +manganis = { git = "https://github.com/DioxusLabs/collect-assets" } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] dioxus-hot-reload = { workspace = true, optional = true } diff --git a/packages/dioxus/src/lib.rs b/packages/dioxus/src/lib.rs index 6256e96d7..1e64715b0 100644 --- a/packages/dioxus/src/lib.rs +++ b/packages/dioxus/src/lib.rs @@ -22,6 +22,9 @@ pub use dioxus_rsx as rsx; pub use dioxus_core_macro as core_macro; pub mod prelude { + pub use manganis; + pub use manganis::mg; + #[cfg(feature = "hooks")] pub use crate::hooks::*; diff --git a/packages/fullstack/Cargo.toml b/packages/fullstack/Cargo.toml index 2f3eca420..0215fcaad 100644 --- a/packages/fullstack/Cargo.toml +++ b/packages/fullstack/Cargo.toml @@ -70,6 +70,10 @@ dioxus-hot-reload = { workspace = true } [target.'cfg(target_arch = "wasm32")'.dependencies] web-sys = { version = "0.3.61", features = ["Window", "Document", "Element", "HtmlDocument", "Storage", "console"] } +[target.'cfg(any(target_os = "windows",target_os = "macos",target_os = "linux",target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd", target_os = "openbsd"))'.dependencies] +# This is only for debug mode, and it appears mobile does not support some packages this uses +manganis-cli-support = { git = "https://github.com/DioxusLabs/collect-assets", features = ["webp", "html"] } + [features] default = ["hot-reload", "default-tls"] router = ["dioxus-router"] diff --git a/packages/fullstack/examples/axum-auth/.gitignore b/packages/fullstack/examples/axum-auth/.gitignore index b2a16319f..21fff11dd 100644 --- a/packages/fullstack/examples/axum-auth/.gitignore +++ b/packages/fullstack/examples/axum-auth/.gitignore @@ -1,3 +1,4 @@ dist target -static \ No newline at end of file +static +.dioxus \ No newline at end of file diff --git a/packages/fullstack/examples/axum-desktop/.gitignore b/packages/fullstack/examples/axum-desktop/.gitignore index 6047329c6..21fff11dd 100644 --- a/packages/fullstack/examples/axum-desktop/.gitignore +++ b/packages/fullstack/examples/axum-desktop/.gitignore @@ -1,2 +1,4 @@ dist -target \ No newline at end of file +target +static +.dioxus \ No newline at end of file diff --git a/packages/fullstack/examples/axum-hello-world/.gitignore b/packages/fullstack/examples/axum-hello-world/.gitignore index b2a16319f..21fff11dd 100644 --- a/packages/fullstack/examples/axum-hello-world/.gitignore +++ b/packages/fullstack/examples/axum-hello-world/.gitignore @@ -1,3 +1,4 @@ dist target -static \ No newline at end of file +static +.dioxus \ No newline at end of file diff --git a/packages/fullstack/examples/axum-router/.gitignore b/packages/fullstack/examples/axum-router/.gitignore index b2a16319f..21fff11dd 100644 --- a/packages/fullstack/examples/axum-router/.gitignore +++ b/packages/fullstack/examples/axum-router/.gitignore @@ -1,3 +1,4 @@ dist target -static \ No newline at end of file +static +.dioxus \ No newline at end of file diff --git a/packages/fullstack/examples/salvo-hello-world/.gitignore b/packages/fullstack/examples/salvo-hello-world/.gitignore index b2a16319f..21fff11dd 100644 --- a/packages/fullstack/examples/salvo-hello-world/.gitignore +++ b/packages/fullstack/examples/salvo-hello-world/.gitignore @@ -1,3 +1,4 @@ dist target -static \ No newline at end of file +static +.dioxus \ No newline at end of file diff --git a/packages/fullstack/examples/static-hydrated/.gitignore b/packages/fullstack/examples/static-hydrated/.gitignore index c7f237de8..b98a2a695 100644 --- a/packages/fullstack/examples/static-hydrated/.gitignore +++ b/packages/fullstack/examples/static-hydrated/.gitignore @@ -1,3 +1,4 @@ docs target -static \ No newline at end of file +static +.dioxus \ No newline at end of file diff --git a/packages/fullstack/examples/warp-hello-world/.gitignore b/packages/fullstack/examples/warp-hello-world/.gitignore index b2a16319f..21fff11dd 100644 --- a/packages/fullstack/examples/warp-hello-world/.gitignore +++ b/packages/fullstack/examples/warp-hello-world/.gitignore @@ -1,3 +1,4 @@ dist target -static \ No newline at end of file +static +.dioxus \ No newline at end of file diff --git a/packages/fullstack/src/adapters/axum_adapter.rs b/packages/fullstack/src/adapters/axum_adapter.rs index 09e6e1aa1..a14c51c0a 100644 --- a/packages/fullstack/src/adapters/axum_adapter.rs +++ b/packages/fullstack/src/adapters/axum_adapter.rs @@ -276,6 +276,9 @@ where fn serve_static_assets(mut self, assets_path: impl Into) -> Self { use tower_http::services::{ServeDir, ServeFile}; + // Copy over any assets we find + crate::collect_assets::copy_assets(); + let assets_path = assets_path.into(); // Serve all files in dist folder except index.html diff --git a/packages/fullstack/src/adapters/salvo_adapter.rs b/packages/fullstack/src/adapters/salvo_adapter.rs index 674722665..84894bbda 100644 --- a/packages/fullstack/src/adapters/salvo_adapter.rs +++ b/packages/fullstack/src/adapters/salvo_adapter.rs @@ -241,6 +241,9 @@ impl DioxusRouterExt for Router { } fn serve_static_assets(mut self, assets_path: impl Into) -> Self { + // Copy over any assets we find + crate::collect_assets::copy_assets(); + let assets_path = assets_path.into(); // Serve all files in dist folder except index.html diff --git a/packages/fullstack/src/adapters/warp_adapter.rs b/packages/fullstack/src/adapters/warp_adapter.rs index 316d70e56..682063084 100644 --- a/packages/fullstack/src/adapters/warp_adapter.rs +++ b/packages/fullstack/src/adapters/warp_adapter.rs @@ -187,6 +187,9 @@ pub fn serve_dioxus_application(cfg: Config) { let target_dir = crate_dir.join("target"); let hot_reload_socket_path = target_dir.join("dioxusin"); - #[cfg(target_os = "macos")] + #[cfg(unix)] { // On unix, if you force quit the application, it can leave the file socket open // This will cause the local socket listener to fail to open diff --git a/packages/html/Cargo.toml b/packages/html/Cargo.toml index d53a16183..68c365579 100644 --- a/packages/html/Cargo.toml +++ b/packages/html/Cargo.toml @@ -16,7 +16,7 @@ serde = { version = "1", features = ["derive"], optional = true } serde_repr = { version = "0.1", optional = true } wasm-bindgen = { workspace = true, optional = true } euclid = "0.22.7" -enumset = "1.0.11" +enumset = "1.1.2" keyboard-types = "0.7" async-trait = "0.1.58" serde-value = "0.7.0" @@ -29,28 +29,29 @@ serde_json = { version = "1", optional = true } optional = true version = "0.3.56" features = [ - "TouchEvent", - "MouseEvent", - "InputEvent", - "ClipboardEvent", - "KeyboardEvent", - "TouchEvent", - "WheelEvent", - "AnimationEvent", - "TransitionEvent", - "PointerEvent", - "FocusEvent", - "CompositionEvent", - "ClipboardEvent", + "Touch", + "TouchList", + "TouchEvent", + "MouseEvent", + "InputEvent", + "ClipboardEvent", + "KeyboardEvent", + "WheelEvent", + "AnimationEvent", + "TransitionEvent", + "PointerEvent", + "FocusEvent", + "CompositionEvent", ] [dev-dependencies] serde_json = "1" [features] -default = ["serialize", "mounted"] +default = ["serialize", "mounted", "eval"] serialize = [ "serde", + "serde/rc", "serde_repr", "serde_json", "euclid/serde", @@ -65,6 +66,10 @@ mounted = [ "web-sys/ScrollBehavior", "web-sys/HtmlElement", ] +eval = [ + "serde", + "serde_json" +] wasm-bind = ["web-sys", "wasm-bindgen"] native-bind = ["tokio"] hot-reload-context = ["dioxus-rsx"] diff --git a/packages/html/src/events.rs b/packages/html/src/events.rs index c59149493..de685b3cc 100644 --- a/packages/html/src/events.rs +++ b/packages/html/src/events.rs @@ -1,3 +1,6 @@ +use std::any::Any; +use std::sync::RwLock; + macro_rules! impl_event { ( $data:ty; @@ -12,8 +15,8 @@ macro_rules! impl_event { pub fn $name<'a, E: crate::EventReturn, T>(_cx: &'a ::dioxus_core::ScopeState, mut _f: impl FnMut(::dioxus_core::Event<$data>) -> E + 'a) -> ::dioxus_core::Attribute<'a> { ::dioxus_core::Attribute::new( stringify!($name), - _cx.listener(move |e: ::dioxus_core::Event<$data>| { - _f(e).spawn(_cx); + _cx.listener(move |e: ::dioxus_core::Event| { + _f(e.map(|e|e.into())).spawn(_cx); }), None, false, @@ -23,6 +26,193 @@ macro_rules! impl_event { }; } +static EVENT_CONVERTER: RwLock>> = RwLock::new(None); + +#[inline] +pub fn set_event_converter(converter: Box) { + *EVENT_CONVERTER.write().unwrap() = Some(converter); +} + +#[inline] +pub(crate) fn with_event_converter(f: F) -> R +where + F: FnOnce(&dyn HtmlEventConverter) -> R, +{ + let converter = EVENT_CONVERTER.read().unwrap(); + f(converter.as_ref().unwrap().as_ref()) +} + +/// A platform specific event. +pub struct PlatformEventData { + event: Box, +} + +impl PlatformEventData { + pub fn new(event: Box) -> Self { + Self { event } + } + + pub fn downcast(&self) -> Option<&T> { + self.event.downcast_ref::() + } + + pub fn downcast_mut(&mut self) -> Option<&mut T> { + self.event.downcast_mut::() + } + + pub fn into_inner(self) -> Option { + self.event.downcast::().ok().map(|e| *e) + } +} + +/// A converter between a platform specific event and a general event. All code in a renderer that has a large binary size should be placed in this trait. Each of these functions should be snipped in high levels of optimization. +pub trait HtmlEventConverter: Send + Sync { + /// Convert a general event to an animation data event + fn convert_animation_data(&self, event: &PlatformEventData) -> AnimationData; + /// Convert a general event to a clipboard data event + fn convert_clipboard_data(&self, event: &PlatformEventData) -> ClipboardData; + /// Convert a general event to a composition data event + fn convert_composition_data(&self, event: &PlatformEventData) -> CompositionData; + /// Convert a general event to a drag data event + fn convert_drag_data(&self, event: &PlatformEventData) -> DragData; + /// Convert a general event to a focus data event + fn convert_focus_data(&self, event: &PlatformEventData) -> FocusData; + /// Convert a general event to a form data event + fn convert_form_data(&self, event: &PlatformEventData) -> FormData; + /// Convert a general event to an image data event + fn convert_image_data(&self, event: &PlatformEventData) -> ImageData; + /// Convert a general event to a keyboard data event + fn convert_keyboard_data(&self, event: &PlatformEventData) -> KeyboardData; + /// Convert a general event to a media data event + fn convert_media_data(&self, event: &PlatformEventData) -> MediaData; + /// Convert a general event to a mounted data event + fn convert_mounted_data(&self, event: &PlatformEventData) -> MountedData; + /// Convert a general event to a mouse data event + fn convert_mouse_data(&self, event: &PlatformEventData) -> MouseData; + /// Convert a general event to a pointer data event + fn convert_pointer_data(&self, event: &PlatformEventData) -> PointerData; + /// Convert a general event to a scroll data event + fn convert_scroll_data(&self, event: &PlatformEventData) -> ScrollData; + /// Convert a general event to a selection data event + fn convert_selection_data(&self, event: &PlatformEventData) -> SelectionData; + /// Convert a general event to a toggle data event + fn convert_toggle_data(&self, event: &PlatformEventData) -> ToggleData; + /// Convert a general event to a touch data event + fn convert_touch_data(&self, event: &PlatformEventData) -> TouchData; + /// Convert a general event to a transition data event + fn convert_transition_data(&self, event: &PlatformEventData) -> TransitionData; + /// Convert a general event to a wheel data event + fn convert_wheel_data(&self, event: &PlatformEventData) -> WheelData; +} + +impl From<&PlatformEventData> for AnimationData { + fn from(val: &PlatformEventData) -> Self { + with_event_converter(|c| c.convert_animation_data(val)) + } +} + +impl From<&PlatformEventData> for ClipboardData { + fn from(val: &PlatformEventData) -> Self { + with_event_converter(|c| c.convert_clipboard_data(val)) + } +} + +impl From<&PlatformEventData> for CompositionData { + fn from(val: &PlatformEventData) -> Self { + with_event_converter(|c| c.convert_composition_data(val)) + } +} + +impl From<&PlatformEventData> for DragData { + fn from(val: &PlatformEventData) -> Self { + with_event_converter(|c| c.convert_drag_data(val)) + } +} + +impl From<&PlatformEventData> for FocusData { + fn from(val: &PlatformEventData) -> Self { + with_event_converter(|c| c.convert_focus_data(val)) + } +} + +impl From<&PlatformEventData> for FormData { + fn from(val: &PlatformEventData) -> Self { + with_event_converter(|c| c.convert_form_data(val)) + } +} + +impl From<&PlatformEventData> for ImageData { + fn from(val: &PlatformEventData) -> Self { + with_event_converter(|c| c.convert_image_data(val)) + } +} + +impl From<&PlatformEventData> for KeyboardData { + fn from(val: &PlatformEventData) -> Self { + with_event_converter(|c| c.convert_keyboard_data(val)) + } +} + +impl From<&PlatformEventData> for MediaData { + fn from(val: &PlatformEventData) -> Self { + with_event_converter(|c| c.convert_media_data(val)) + } +} + +impl From<&PlatformEventData> for MountedData { + fn from(val: &PlatformEventData) -> Self { + with_event_converter(|c| c.convert_mounted_data(val)) + } +} + +impl From<&PlatformEventData> for MouseData { + fn from(val: &PlatformEventData) -> Self { + with_event_converter(|c| c.convert_mouse_data(val)) + } +} + +impl From<&PlatformEventData> for PointerData { + fn from(val: &PlatformEventData) -> Self { + with_event_converter(|c| c.convert_pointer_data(val)) + } +} + +impl From<&PlatformEventData> for ScrollData { + fn from(val: &PlatformEventData) -> Self { + with_event_converter(|c| c.convert_scroll_data(val)) + } +} + +impl From<&PlatformEventData> for SelectionData { + fn from(val: &PlatformEventData) -> Self { + with_event_converter(|c| c.convert_selection_data(val)) + } +} + +impl From<&PlatformEventData> for ToggleData { + fn from(val: &PlatformEventData) -> Self { + with_event_converter(|c| c.convert_toggle_data(val)) + } +} + +impl From<&PlatformEventData> for TouchData { + fn from(val: &PlatformEventData) -> Self { + with_event_converter(|c| c.convert_touch_data(val)) + } +} + +impl From<&PlatformEventData> for TransitionData { + fn from(val: &PlatformEventData) -> Self { + with_event_converter(|c| c.convert_transition_data(val)) + } +} + +impl From<&PlatformEventData> for WheelData { + fn from(val: &PlatformEventData) -> Self { + with_event_converter(|c| c.convert_wheel_data(val)) + } +} + mod animation; mod clipboard; mod composition; @@ -151,8 +341,6 @@ pub fn event_bubbles(evt: &str) -> bool { } } -use std::future::Future; - #[doc(hidden)] pub trait EventReturn

: Sized { fn spawn(self, _cx: &dioxus_core::ScopeState) {} @@ -164,7 +352,7 @@ pub struct AsyncMarker; impl EventReturn for T where - T: Future + 'static, + T: std::future::Future + 'static, { #[inline] fn spawn(self, cx: &dioxus_core::ScopeState) { diff --git a/packages/html/src/events/animation.rs b/packages/html/src/events/animation.rs index 4495191fc..26db06f9d 100644 --- a/packages/html/src/events/animation.rs +++ b/packages/html/src/events/animation.rs @@ -2,12 +2,132 @@ use dioxus_core::Event; pub type AnimationEvent = Event; -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[derive(Debug, Clone, PartialEq)] pub struct AnimationData { - pub animation_name: String, - pub pseudo_element: String, - pub elapsed_time: f32, + inner: Box, +} + +impl AnimationData { + /// Create a new AnimationData + pub fn new(inner: impl HasAnimationData + 'static) -> Self { + Self { + inner: Box::new(inner), + } + } + + /// The name of the animation + pub fn animation_name(&self) -> String { + self.inner.animation_name() + } + + /// The name of the pseudo-element the animation runs on + pub fn pseudo_element(&self) -> String { + self.inner.pseudo_element() + } + + /// The amount of time the animation has been running + pub fn elapsed_time(&self) -> f32 { + self.inner.elapsed_time() + } + + /// Downcast this event to a concrete event type + pub fn downcast(&self) -> Option<&T> { + self.inner.as_ref().as_any().downcast_ref::() + } +} + +impl From for AnimationData { + fn from(e: E) -> Self { + Self { inner: Box::new(e) } + } +} + +impl std::fmt::Debug for AnimationData { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("AnimationData") + .field("animation_name", &self.animation_name()) + .field("pseudo_element", &self.pseudo_element()) + .field("elapsed_time", &self.elapsed_time()) + .finish() + } +} + +impl PartialEq for AnimationData { + fn eq(&self, other: &Self) -> bool { + self.animation_name() == other.animation_name() + && self.pseudo_element() == other.pseudo_element() + && self.elapsed_time() == other.elapsed_time() + } +} + +#[cfg(feature = "serialize")] +/// A serialized version of AnimationData +#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)] +pub struct SerializedAnimationData { + animation_name: String, + pseudo_element: String, + elapsed_time: f32, +} + +#[cfg(feature = "serialize")] +impl From<&AnimationData> for SerializedAnimationData { + fn from(data: &AnimationData) -> Self { + Self { + animation_name: data.animation_name(), + pseudo_element: data.pseudo_element(), + elapsed_time: data.elapsed_time(), + } + } +} + +#[cfg(feature = "serialize")] +impl HasAnimationData for SerializedAnimationData { + fn animation_name(&self) -> String { + self.animation_name.clone() + } + + fn pseudo_element(&self) -> String { + self.pseudo_element.clone() + } + + fn elapsed_time(&self) -> f32 { + self.elapsed_time + } + + fn as_any(&self) -> &dyn std::any::Any { + self + } +} + +#[cfg(feature = "serialize")] +impl serde::Serialize for AnimationData { + fn serialize(&self, serializer: S) -> Result { + SerializedAnimationData::from(self).serialize(serializer) + } +} + +#[cfg(feature = "serialize")] +impl<'de> serde::Deserialize<'de> for AnimationData { + fn deserialize>(deserializer: D) -> Result { + let data = SerializedAnimationData::deserialize(deserializer)?; + Ok(Self { + inner: Box::new(data), + }) + } +} + +/// A trait for any object that has the data for an animation event +pub trait HasAnimationData: std::any::Any { + /// The name of the animation + fn animation_name(&self) -> String; + + /// The name of the pseudo-element the animation runs on + fn pseudo_element(&self) -> String; + + /// The amount of time the animation has been running + fn elapsed_time(&self) -> f32; + + /// return self as Any + fn as_any(&self) -> &dyn std::any::Any; } impl_event! [ diff --git a/packages/html/src/events/clipboard.rs b/packages/html/src/events/clipboard.rs index 54390c6b0..c0180c728 100644 --- a/packages/html/src/events/clipboard.rs +++ b/packages/html/src/events/clipboard.rs @@ -1,10 +1,82 @@ use dioxus_core::Event; pub type ClipboardEvent = Event; -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[derive(Debug, Clone, PartialEq, Eq)] + pub struct ClipboardData { - // DOMDataTransfer clipboardData + inner: Box, +} + +impl From for ClipboardData { + fn from(e: E) -> Self { + Self { inner: Box::new(e) } + } +} + +impl std::fmt::Debug for ClipboardData { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ClipboardData").finish() + } +} + +impl PartialEq for ClipboardData { + fn eq(&self, _other: &Self) -> bool { + true + } +} + +impl ClipboardData { + /// Create a new ClipboardData + pub fn new(inner: impl HasClipboardData) -> Self { + Self { + inner: Box::new(inner), + } + } + + /// Downcast this event to a concrete event type + pub fn downcast(&self) -> Option<&T> { + self.inner.as_ref().as_any().downcast_ref::() + } +} + +#[cfg(feature = "serialize")] +/// A serialized version of ClipboardData +#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)] +pub struct SerializedClipboardData {} + +#[cfg(feature = "serialize")] +impl From<&ClipboardData> for SerializedClipboardData { + fn from(_: &ClipboardData) -> Self { + Self {} + } +} + +#[cfg(feature = "serialize")] +impl HasClipboardData for SerializedClipboardData { + fn as_any(&self) -> &dyn std::any::Any { + self + } +} + +#[cfg(feature = "serialize")] +impl serde::Serialize for ClipboardData { + fn serialize(&self, serializer: S) -> Result { + SerializedClipboardData::from(self).serialize(serializer) + } +} + +#[cfg(feature = "serialize")] +impl<'de> serde::Deserialize<'de> for ClipboardData { + fn deserialize>(deserializer: D) -> Result { + let data = SerializedClipboardData::deserialize(deserializer)?; + Ok(Self { + inner: Box::new(data), + }) + } +} + +pub trait HasClipboardData: std::any::Any { + /// return self as Any + fn as_any(&self) -> &dyn std::any::Any; } impl_event![ diff --git a/packages/html/src/events/composition.rs b/packages/html/src/events/composition.rs index 480868f24..4d161436e 100644 --- a/packages/html/src/events/composition.rs +++ b/packages/html/src/events/composition.rs @@ -1,10 +1,98 @@ use dioxus_core::Event; pub type CompositionEvent = Event; -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[derive(Debug, Clone, PartialEq, Eq)] + pub struct CompositionData { - pub data: String, + inner: Box, +} + +impl std::fmt::Debug for CompositionData { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("CompositionData") + .field("data", &self.data()) + .finish() + } +} + +impl From for CompositionData { + fn from(e: E) -> Self { + Self { inner: Box::new(e) } + } +} + +impl PartialEq for CompositionData { + fn eq(&self, other: &Self) -> bool { + self.data() == other.data() + } +} + +impl CompositionData { + /// Create a new CompositionData + pub fn new(inner: impl HasCompositionData + 'static) -> Self { + Self { + inner: Box::new(inner), + } + } + + /// The characters generated by the input method that raised the event + pub fn data(&self) -> String { + self.inner.data() + } + + pub fn downcast(&self) -> Option<&T> { + self.inner.as_any().downcast_ref() + } +} + +#[cfg(feature = "serialize")] +/// A serialized version of CompositionData +#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)] +pub struct SerializedCompositionData { + data: String, +} + +#[cfg(feature = "serialize")] +impl From<&CompositionData> for SerializedCompositionData { + fn from(data: &CompositionData) -> Self { + Self { data: data.data() } + } +} + +#[cfg(feature = "serialize")] +impl HasCompositionData for SerializedCompositionData { + fn data(&self) -> String { + self.data.clone() + } + + fn as_any(&self) -> &dyn std::any::Any { + self + } +} + +#[cfg(feature = "serialize")] +impl serde::Serialize for CompositionData { + fn serialize(&self, serializer: S) -> Result { + SerializedCompositionData::from(self).serialize(serializer) + } +} + +#[cfg(feature = "serialize")] +impl<'de> serde::Deserialize<'de> for CompositionData { + fn deserialize>(deserializer: D) -> Result { + let data = SerializedCompositionData::deserialize(deserializer)?; + Ok(Self { + inner: Box::new(data), + }) + } +} + +/// A trait for any object that has the data for a composition event +pub trait HasCompositionData: std::any::Any { + /// The characters generated by the input method that raised the event + fn data(&self) -> String; + + /// return self as Any + fn as_any(&self) -> &dyn std::any::Any; } impl_event! [ diff --git a/packages/html/src/events/drag.rs b/packages/html/src/events/drag.rs index fa3219631..e58c20e22 100644 --- a/packages/html/src/events/drag.rs +++ b/packages/html/src/events/drag.rs @@ -1,6 +1,11 @@ -use dioxus_core::Event; +use crate::geometry::{ClientPoint, Coordinates, ElementPoint, PagePoint, ScreenPoint}; +use crate::input_data::{MouseButton, MouseButtonSet}; +use crate::prelude::*; -use crate::MouseData; +use dioxus_core::Event; +use keyboard_types::Modifiers; + +use crate::HasMouseData; pub type DragEvent = Event; @@ -8,13 +13,181 @@ pub type DragEvent = Event; /// placing a pointer device (such as a mouse) on the touch surface and then dragging the pointer to a new location /// (such as another DOM element). Applications are free to interpret a drag and drop interaction in an /// application-specific way. -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[derive(Debug, Clone, PartialEq, Eq)] pub struct DragData { - /// Inherit mouse data - pub mouse: MouseData, + inner: Box, } +impl From for DragData { + fn from(e: E) -> Self { + Self { inner: Box::new(e) } + } +} + +impl std::fmt::Debug for DragData { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("DragData") + .field("coordinates", &self.coordinates()) + .field("modifiers", &self.modifiers()) + .field("held_buttons", &self.held_buttons()) + .field("trigger_button", &self.trigger_button()) + .finish() + } +} + +impl PartialEq for DragData { + fn eq(&self, other: &Self) -> bool { + self.coordinates() == other.coordinates() + && self.modifiers() == other.modifiers() + && self.held_buttons() == other.held_buttons() + && self.trigger_button() == other.trigger_button() + } +} + +impl DragData { + /// Create a new DragData + pub fn new(inner: impl HasDragData + 'static) -> Self { + Self { + inner: Box::new(inner), + } + } + + /// Downcast this event data to a specific type + pub fn downcast(&self) -> Option<&T> { + self.inner.as_any().downcast_ref::() + } +} + +impl InteractionLocation for DragData { + fn client_coordinates(&self) -> ClientPoint { + self.inner.client_coordinates() + } + + fn page_coordinates(&self) -> PagePoint { + self.inner.page_coordinates() + } + + fn screen_coordinates(&self) -> ScreenPoint { + self.inner.screen_coordinates() + } +} + +impl InteractionElementOffset for DragData { + fn element_coordinates(&self) -> ElementPoint { + self.inner.element_coordinates() + } + + fn coordinates(&self) -> Coordinates { + self.inner.coordinates() + } +} + +impl ModifiersInteraction for DragData { + fn modifiers(&self) -> Modifiers { + self.inner.modifiers() + } +} + +impl PointerInteraction for DragData { + fn held_buttons(&self) -> MouseButtonSet { + self.inner.held_buttons() + } + + // todo the following is kind of bad; should we just return None when the trigger_button is unreliable (and frankly irrelevant)? i guess we would need the event_type here + fn trigger_button(&self) -> Option { + self.inner.trigger_button() + } +} + +#[cfg(feature = "serialize")] +/// A serialized version of DragData +#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)] +pub struct SerializedDragData { + mouse: crate::point_interaction::SerializedPointInteraction, +} + +#[cfg(feature = "serialize")] +impl From<&DragData> for SerializedDragData { + fn from(data: &DragData) -> Self { + Self { + mouse: crate::point_interaction::SerializedPointInteraction::from(data), + } + } +} + +#[cfg(feature = "serialize")] +impl HasDragData for SerializedDragData {} + +#[cfg(feature = "serialize")] +impl HasMouseData for SerializedDragData { + fn as_any(&self) -> &dyn std::any::Any { + self + } +} + +#[cfg(feature = "serialize")] +impl InteractionLocation for SerializedDragData { + fn client_coordinates(&self) -> ClientPoint { + self.mouse.client_coordinates() + } + + fn page_coordinates(&self) -> PagePoint { + self.mouse.page_coordinates() + } + + fn screen_coordinates(&self) -> ScreenPoint { + self.mouse.screen_coordinates() + } +} + +#[cfg(feature = "serialize")] +impl InteractionElementOffset for SerializedDragData { + fn element_coordinates(&self) -> ElementPoint { + self.mouse.element_coordinates() + } + + fn coordinates(&self) -> Coordinates { + self.mouse.coordinates() + } +} + +#[cfg(feature = "serialize")] +impl ModifiersInteraction for SerializedDragData { + fn modifiers(&self) -> Modifiers { + self.mouse.modifiers() + } +} + +#[cfg(feature = "serialize")] +impl PointerInteraction for SerializedDragData { + fn held_buttons(&self) -> MouseButtonSet { + self.mouse.held_buttons() + } + + fn trigger_button(&self) -> Option { + self.mouse.trigger_button() + } +} + +#[cfg(feature = "serialize")] +impl serde::Serialize for DragData { + fn serialize(&self, serializer: S) -> Result { + SerializedDragData::from(self).serialize(serializer) + } +} + +#[cfg(feature = "serialize")] +impl<'de> serde::Deserialize<'de> for DragData { + fn deserialize>(deserializer: D) -> Result { + let data = SerializedDragData::deserialize(deserializer)?; + Ok(Self { + inner: Box::new(data), + }) + } +} + +/// A trait for any object that has the data for a drag event +pub trait HasDragData: HasMouseData {} + impl_event! { DragData; diff --git a/packages/html/src/events/focus.rs b/packages/html/src/events/focus.rs index 2092c972e..f121feb7b 100644 --- a/packages/html/src/events/focus.rs +++ b/packages/html/src/events/focus.rs @@ -2,9 +2,82 @@ use dioxus_core::Event; pub type FocusEvent = Event; -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct FocusData {/* DOMEventInner: Send + SyncTarget relatedTarget */} +pub struct FocusData { + inner: Box, +} + +impl From for FocusData { + fn from(e: E) -> Self { + Self { inner: Box::new(e) } + } +} + +impl std::fmt::Debug for FocusData { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("FocusData").finish() + } +} + +impl PartialEq for FocusData { + fn eq(&self, _other: &Self) -> bool { + true + } +} + +impl FocusData { + /// Create a new FocusData + pub fn new(inner: impl HasFocusData + 'static) -> Self { + Self { + inner: Box::new(inner), + } + } + + /// Downcast this event data to a specific type + pub fn downcast(&self) -> Option<&T> { + self.inner.as_any().downcast_ref::() + } +} + +#[cfg(feature = "serialize")] +/// A serialized version of FocusData +#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone, Default)] +pub struct SerializedFocusData {} + +#[cfg(feature = "serialize")] +impl From<&FocusData> for SerializedFocusData { + fn from(_: &FocusData) -> Self { + Self {} + } +} + +#[cfg(feature = "serialize")] +impl HasFocusData for SerializedFocusData { + fn as_any(&self) -> &dyn std::any::Any { + self + } +} + +#[cfg(feature = "serialize")] +impl serde::Serialize for FocusData { + fn serialize(&self, serializer: S) -> Result { + SerializedFocusData::from(self).serialize(serializer) + } +} + +#[cfg(feature = "serialize")] +impl<'de> serde::Deserialize<'de> for FocusData { + fn deserialize>(deserializer: D) -> Result { + let data = SerializedFocusData::deserialize(deserializer)?; + Ok(Self { + inner: Box::new(data), + }) + } +} + +pub trait HasFocusData: std::any::Any { + /// return self as Any + fn as_any(&self) -> &dyn std::any::Any; +} impl_event! [ FocusData; diff --git a/packages/html/src/events/form.rs b/packages/html/src/events/form.rs index fab13482b..de8bb0dbf 100644 --- a/packages/html/src/events/form.rs +++ b/packages/html/src/events/form.rs @@ -4,28 +4,176 @@ use dioxus_core::Event; pub type FormEvent = Event; -/* DOMEvent: Send + SyncTarget relatedTarget */ -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone)] pub struct FormData { - pub value: String, + inner: Box, +} - pub values: HashMap>, +impl From for FormData { + fn from(e: E) -> Self { + Self { inner: Box::new(e) } + } +} - #[cfg_attr( - feature = "serialize", - serde( - default, - skip_serializing, - deserialize_with = "deserialize_file_engine" - ) - )] - pub files: Option>, +impl PartialEq for FormData { + fn eq(&self, other: &Self) -> bool { + self.value() == other.value() && self.values() == other.values() + } +} + +impl Debug for FormData { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("FormEvent") + .field("value", &self.value()) + .field("values", &self.values()) + .finish() + } +} + +impl FormData { + /// Create a new form event + pub fn new(event: impl HasFormData + 'static) -> Self { + Self { + inner: Box::new(event), + } + } + + /// Get the value of the form event + pub fn value(&self) -> String { + self.inner.value() + } + + /// Get the values of the form event + pub fn values(&self) -> HashMap> { + self.inner.values() + } + + /// Get the files of the form event + pub fn files(&self) -> Option> { + self.inner.files() + } + + /// Downcast this event to a concrete event type + pub fn downcast(&self) -> Option<&T> { + self.inner.as_any().downcast_ref::() + } +} + +/// An object that has all the data for a form event +pub trait HasFormData: std::any::Any { + fn value(&self) -> String { + Default::default() + } + + fn values(&self) -> HashMap> { + Default::default() + } + + fn files(&self) -> Option> { + None + } + + /// return self as Any + fn as_any(&self) -> &dyn std::any::Any; } #[cfg(feature = "serialize")] -#[derive(serde::Serialize, serde::Deserialize)] -struct SerializedFileEngine { +/// A serialized form data object +#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)] +pub struct SerializedFormData { + value: String, + values: HashMap>, + files: Option>, +} + +#[cfg(feature = "serialize")] +impl SerializedFormData { + /// Create a new serialized form data object + pub fn new( + value: String, + values: HashMap>, + files: Option>, + ) -> Self { + Self { + value, + values, + files, + } + } + + /// Create a new serialized form data object from a traditional form data object + pub async fn async_from(data: &FormData) -> Self { + Self { + value: data.value(), + values: data.values(), + files: match data.files() { + Some(files) => { + let mut resolved_files = HashMap::new(); + + for file in files.files() { + let bytes = files.read_file(&file).await; + resolved_files.insert(file, bytes.unwrap_or_default()); + } + + Some(std::sync::Arc::new(SerializedFileEngine { + files: resolved_files, + })) + } + None => None, + }, + } + } + + fn from_lossy(data: &FormData) -> Self { + Self { + value: data.value(), + values: data.values(), + files: None, + } + } +} + +#[cfg(feature = "serialize")] +impl HasFormData for SerializedFormData { + fn value(&self) -> String { + self.value.clone() + } + + fn values(&self) -> HashMap> { + self.values.clone() + } + + fn files(&self) -> Option> { + self.files + .as_ref() + .map(|files| std::sync::Arc::clone(files) as std::sync::Arc) + } + + fn as_any(&self) -> &dyn std::any::Any { + self + } +} + +#[cfg(feature = "serialize")] +impl serde::Serialize for FormData { + fn serialize(&self, serializer: S) -> Result { + SerializedFormData::from_lossy(self).serialize(serializer) + } +} + +#[cfg(feature = "serialize")] +impl<'de> serde::Deserialize<'de> for FormData { + fn deserialize>(deserializer: D) -> Result { + let data = SerializedFormData::deserialize(deserializer)?; + Ok(Self { + inner: Box::new(data), + }) + } +} + +#[cfg(feature = "serialize")] +/// A file engine that serializes files to bytes +#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)] +pub struct SerializedFileEngine { files: HashMap>, } @@ -53,38 +201,6 @@ impl FileEngine for SerializedFileEngine { } } -#[cfg(feature = "serialize")] -fn deserialize_file_engine<'de, D>( - deserializer: D, -) -> Result>, D::Error> -where - D: serde::Deserializer<'de>, -{ - use serde::Deserialize; - - let Ok(file_engine) = SerializedFileEngine::deserialize(deserializer) else { - return Ok(None); - }; - - let file_engine = std::sync::Arc::new(file_engine); - Ok(Some(file_engine)) -} - -impl PartialEq for FormData { - fn eq(&self, other: &Self) -> bool { - self.value == other.value && self.values == other.values - } -} - -impl Debug for FormData { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("FormEvent") - .field("value", &self.value) - .field("values", &self.values) - .finish() - } -} - #[async_trait::async_trait(?Send)] pub trait FileEngine { // get a list of file names diff --git a/packages/html/src/events/image.rs b/packages/html/src/events/image.rs index d5858a7f1..3c75edbdd 100644 --- a/packages/html/src/events/image.rs +++ b/packages/html/src/events/image.rs @@ -1,11 +1,97 @@ use dioxus_core::Event; pub type ImageEvent = Event; -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[derive(Debug, Clone, PartialEq, Eq)] pub struct ImageData { - #[cfg_attr(feature = "serialize", serde(default))] - pub load_error: bool, + inner: Box, +} + +impl From for ImageData { + fn from(e: E) -> Self { + Self { inner: Box::new(e) } + } +} + +impl std::fmt::Debug for ImageData { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ImageData") + .field("load_error", &self.load_error()) + .finish() + } +} + +impl PartialEq for ImageData { + fn eq(&self, other: &Self) -> bool { + self.load_error() == other.load_error() + } +} + +impl ImageData { + /// Create a new ImageData + pub fn new(e: impl HasImageData) -> Self { + Self { inner: Box::new(e) } + } + + /// If the renderer encountered an error while loading the image + pub fn load_error(&self) -> bool { + self.inner.load_error() + } + + pub fn downcast(&self) -> Option<&T> { + self.inner.as_any().downcast_ref::() + } +} + +#[cfg(feature = "serialize")] +/// A serialized version of ImageData +#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)] +pub struct SerializedImageData { + load_error: bool, +} + +#[cfg(feature = "serialize")] +impl From<&ImageData> for SerializedImageData { + fn from(data: &ImageData) -> Self { + Self { + load_error: data.load_error(), + } + } +} + +#[cfg(feature = "serialize")] +impl HasImageData for SerializedImageData { + fn as_any(&self) -> &dyn std::any::Any { + self + } + + fn load_error(&self) -> bool { + self.load_error + } +} + +#[cfg(feature = "serialize")] +impl serde::Serialize for ImageData { + fn serialize(&self, serializer: S) -> Result { + SerializedImageData::from(self).serialize(serializer) + } +} + +#[cfg(feature = "serialize")] +impl<'de> serde::Deserialize<'de> for ImageData { + fn deserialize>(deserializer: D) -> Result { + let data = SerializedImageData::deserialize(deserializer)?; + Ok(Self { + inner: Box::new(data), + }) + } +} + +/// A trait for any object that has the data for an image event +pub trait HasImageData: std::any::Any { + /// If the renderer encountered an error while loading the image + fn load_error(&self) -> bool; + + /// return self as Any + fn as_any(&self) -> &dyn std::any::Any; } impl_event! [ diff --git a/packages/html/src/events/keyboard.rs b/packages/html/src/events/keyboard.rs index d00dcf8d2..feb0ab313 100644 --- a/packages/html/src/events/keyboard.rs +++ b/packages/html/src/events/keyboard.rs @@ -1,9 +1,8 @@ -use crate::input_data::{decode_key_location, encode_key_location}; use dioxus_core::Event; use keyboard_types::{Code, Key, Location, Modifiers}; -use std::convert::TryInto; -use std::fmt::{Debug, Formatter}; -use std::str::FromStr; +use std::fmt::Debug; + +use crate::prelude::ModifiersInteraction; #[cfg(feature = "serialize")] fn resilient_deserialize_code<'de, D>(deserializer: D) -> Result @@ -16,57 +15,199 @@ where } pub type KeyboardEvent = Event; -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, PartialEq, Eq)] pub struct KeyboardData { - #[deprecated( - since = "0.3.0", - note = "This may not work in all environments. Use key() instead." - )] - pub char_code: u32, + inner: Box, +} - /// Identify which "key" was entered. - #[deprecated(since = "0.3.0", note = "use key() instead")] - pub key: String, +impl From for KeyboardData { + fn from(e: E) -> Self { + Self { inner: Box::new(e) } + } +} - /// Get the key code as an enum Variant. - #[deprecated( - since = "0.3.0", - note = "This may not work in all environments. Use code() instead." - )] - pub key_code: KeyCode, +impl std::fmt::Debug for KeyboardData { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("KeyboardData") + .field("key", &self.key()) + .field("code", &self.code()) + .field("modifiers", &self.modifiers()) + .field("location", &self.location()) + .field("is_auto_repeating", &self.is_auto_repeating()) + .finish() + } +} - /// the physical key on the keyboard - #[cfg_attr( - feature = "serialize", - serde(deserialize_with = "resilient_deserialize_code") - )] +impl PartialEq for KeyboardData { + fn eq(&self, other: &Self) -> bool { + self.key() == other.key() + && self.code() == other.code() + && self.modifiers() == other.modifiers() + && self.location() == other.location() + && self.is_auto_repeating() == other.is_auto_repeating() + } +} + +impl KeyboardData { + /// Create a new KeyboardData + pub fn new(inner: impl HasKeyboardData + 'static) -> Self { + Self { + inner: Box::new(inner), + } + } + + /// The value of the key pressed by the user, taking into consideration the state of modifier keys such as Shift as well as the keyboard locale and layout. + pub fn key(&self) -> Key { + self.inner.key() + } + + /// A physical key on the keyboard (as opposed to the character generated by pressing the key). In other words, this property returns a value that isn't altered by keyboard layout or the state of the modifier keys. + pub fn code(&self) -> Code { + self.inner.code() + } + + /// The location of the key on the keyboard or other input device. + pub fn location(&self) -> Location { + self.inner.location() + } + + /// `true` iff the key is being held down such that it is automatically repeating. + pub fn is_auto_repeating(&self) -> bool { + self.inner.is_auto_repeating() + } + + /// Downcast this KeyboardData to a concrete type. + pub fn downcast(&self) -> Option<&T> { + self.inner.as_any().downcast_ref::() + } +} + +impl ModifiersInteraction for KeyboardData { + fn modifiers(&self) -> Modifiers { + self.inner.modifiers() + } +} + +#[cfg(feature = "serialize")] +/// A serialized version of KeyboardData +#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)] +pub struct SerializedKeyboardData { + char_code: u32, + key: String, + key_code: KeyCode, + #[serde(deserialize_with = "resilient_deserialize_code")] code: Code, + alt_key: bool, + ctrl_key: bool, + meta_key: bool, + shift_key: bool, + location: usize, + repeat: bool, + which: usize, +} - /// Indicate if the `alt` modifier key was pressed during this keyboard event - #[deprecated(since = "0.3.0", note = "use modifiers() instead")] - pub alt_key: bool, +#[cfg(feature = "serialize")] +impl SerializedKeyboardData { + /// Create a new SerializedKeyboardData + pub fn new( + key: Key, + code: Code, + location: Location, + is_auto_repeating: bool, + modifiers: Modifiers, + ) -> Self { + Self { + char_code: key.legacy_charcode(), + key: key.to_string(), + key_code: KeyCode::from_raw_code( + std::convert::TryInto::try_into(key.legacy_keycode()) + .expect("could not convert keycode to u8"), + ), + code, + alt_key: modifiers.contains(Modifiers::ALT), + ctrl_key: modifiers.contains(Modifiers::CONTROL), + meta_key: modifiers.contains(Modifiers::META), + shift_key: modifiers.contains(Modifiers::SHIFT), + location: crate::input_data::encode_key_location(location), + repeat: is_auto_repeating, + which: std::convert::TryInto::try_into(key.legacy_charcode()) + .expect("could not convert charcode to usize"), + } + } +} - /// Indicate if the `ctrl` modifier key was pressed during this keyboard event - #[deprecated(since = "0.3.0", note = "use modifiers() instead")] - pub ctrl_key: bool, +#[cfg(feature = "serialize")] +impl From<&KeyboardData> for SerializedKeyboardData { + fn from(data: &KeyboardData) -> Self { + Self::new( + data.key(), + data.code(), + data.location(), + data.is_auto_repeating(), + data.modifiers(), + ) + } +} - /// Indicate if the `meta` modifier key was pressed during this keyboard event - #[deprecated(since = "0.3.0", note = "use modifiers() instead")] - pub meta_key: bool, +#[cfg(feature = "serialize")] +impl HasKeyboardData for SerializedKeyboardData { + fn key(&self) -> Key { + std::str::FromStr::from_str(&self.key).unwrap_or(Key::Unidentified) + } - /// Indicate if the `shift` modifier key was pressed during this keyboard event - #[deprecated(since = "0.3.0", note = "use modifiers() instead")] - pub shift_key: bool, + fn code(&self) -> Code { + self.code + } - #[deprecated(since = "0.3.0", note = "use location() instead")] - pub location: usize, + fn location(&self) -> Location { + crate::input_data::decode_key_location(self.location) + } - #[deprecated(since = "0.3.0", note = "use is_auto_repeating() instead")] - pub repeat: bool, + fn is_auto_repeating(&self) -> bool { + self.repeat + } - #[deprecated(since = "0.3.0", note = "use code() or key() instead")] - pub which: usize, + fn as_any(&self) -> &dyn std::any::Any { + self + } +} + +#[cfg(feature = "serialize")] +impl ModifiersInteraction for SerializedKeyboardData { + fn modifiers(&self) -> Modifiers { + let mut modifiers = Modifiers::empty(); + + if self.alt_key { + modifiers.insert(Modifiers::ALT); + } + if self.ctrl_key { + modifiers.insert(Modifiers::CONTROL); + } + if self.meta_key { + modifiers.insert(Modifiers::META); + } + if self.shift_key { + modifiers.insert(Modifiers::SHIFT); + } + + modifiers + } +} + +#[cfg(feature = "serialize")] +impl serde::Serialize for KeyboardData { + fn serialize(&self, serializer: S) -> Result { + SerializedKeyboardData::from(self).serialize(serializer) + } +} + +#[cfg(feature = "serialize")] +impl<'de> serde::Deserialize<'de> for KeyboardData { + fn deserialize>(deserializer: D) -> Result { + let data = SerializedKeyboardData::deserialize(deserializer)?; + Ok(Self { + inner: Box::new(data), + }) + } } impl_event! { @@ -82,94 +223,21 @@ impl_event! { onkeyup } -impl KeyboardData { - pub fn new( - key: Key, - code: Code, - location: Location, - is_auto_repeating: bool, - modifiers: Modifiers, - ) -> Self { - #[allow(deprecated)] - KeyboardData { - char_code: key.legacy_charcode(), - key: key.to_string(), - key_code: KeyCode::from_raw_code( - key.legacy_keycode() - .try_into() - .expect("could not convert keycode to u8"), - ), - code, - alt_key: modifiers.contains(Modifiers::ALT), - ctrl_key: modifiers.contains(Modifiers::CONTROL), - meta_key: modifiers.contains(Modifiers::META), - shift_key: modifiers.contains(Modifiers::SHIFT), - location: encode_key_location(location), - repeat: is_auto_repeating, - which: key - .legacy_charcode() - .try_into() - .expect("could not convert charcode to usize"), - } - } - +pub trait HasKeyboardData: ModifiersInteraction + std::any::Any { /// The value of the key pressed by the user, taking into consideration the state of modifier keys such as Shift as well as the keyboard locale and layout. - pub fn key(&self) -> Key { - #[allow(deprecated)] - FromStr::from_str(&self.key).unwrap_or(Key::Unidentified) - } + fn key(&self) -> Key; /// A physical key on the keyboard (as opposed to the character generated by pressing the key). In other words, this property returns a value that isn't altered by keyboard layout or the state of the modifier keys. - pub fn code(&self) -> Code { - self.code - } - - /// The set of modifier keys which were pressed when the event occurred - pub fn modifiers(&self) -> Modifiers { - let mut modifiers = Modifiers::empty(); - - #[allow(deprecated)] - { - if self.alt_key { - modifiers.insert(Modifiers::ALT); - } - if self.ctrl_key { - modifiers.insert(Modifiers::CONTROL); - } - if self.meta_key { - modifiers.insert(Modifiers::META); - } - if self.shift_key { - modifiers.insert(Modifiers::SHIFT); - } - } - - modifiers - } + fn code(&self) -> Code; /// The location of the key on the keyboard or other input device. - pub fn location(&self) -> Location { - #[allow(deprecated)] - decode_key_location(self.location) - } + fn location(&self) -> Location; /// `true` iff the key is being held down such that it is automatically repeating. - pub fn is_auto_repeating(&self) -> bool { - #[allow(deprecated)] - self.repeat - } -} + fn is_auto_repeating(&self) -> bool; -impl Debug for KeyboardData { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.debug_struct("KeyboardData") - .field("key", &self.key()) - .field("code", &self.code()) - .field("modifiers", &self.modifiers()) - .field("location", &self.location()) - .field("is_auto_repeating", &self.is_auto_repeating()) - .finish() - } + /// return self as Any + fn as_any(&self) -> &dyn std::any::Any; } #[cfg(feature = "serialize")] @@ -178,6 +246,8 @@ impl<'de> serde::Deserialize<'de> for KeyCode { where D: serde::Deserializer<'de>, { + use std::convert::TryInto; + // We could be deserializing a unicode character, so we need to use u64 even if the output only takes u8 let value = u64::deserialize(deserializer)?; diff --git a/packages/html/src/events/media.rs b/packages/html/src/events/media.rs index d2ba71c08..7f3180cf7 100644 --- a/packages/html/src/events/media.rs +++ b/packages/html/src/events/media.rs @@ -1,9 +1,82 @@ use dioxus_core::Event; pub type MediaEvent = Event; -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct MediaData {} +pub struct MediaData { + inner: Box, +} + +impl From for MediaData { + fn from(e: E) -> Self { + Self { inner: Box::new(e) } + } +} + +impl std::fmt::Debug for MediaData { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("MediaData").finish() + } +} + +impl PartialEq for MediaData { + fn eq(&self, _: &Self) -> bool { + true + } +} + +impl MediaData { + /// Create a new MediaData + pub fn new(inner: impl HasMediaData + 'static) -> Self { + Self { + inner: Box::new(inner), + } + } + + /// Downcast this event to a concrete event type + pub fn downcast(&self) -> Option<&T> { + self.inner.as_any().downcast_ref::() + } +} + +#[cfg(feature = "serialize")] +/// A serialized version of MediaData +#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)] +pub struct SerializedMediaData {} + +#[cfg(feature = "serialize")] +impl From<&MediaData> for SerializedMediaData { + fn from(_: &MediaData) -> Self { + Self {} + } +} + +#[cfg(feature = "serialize")] +impl HasMediaData for SerializedMediaData { + fn as_any(&self) -> &dyn std::any::Any { + self + } +} + +#[cfg(feature = "serialize")] +impl serde::Serialize for MediaData { + fn serialize(&self, serializer: S) -> Result { + SerializedMediaData::from(self).serialize(serializer) + } +} + +#[cfg(feature = "serialize")] +impl<'de> serde::Deserialize<'de> for MediaData { + fn deserialize>(deserializer: D) -> Result { + let data = SerializedMediaData::deserialize(deserializer)?; + Ok(Self { + inner: Box::new(data), + }) + } +} + +pub trait HasMediaData: std::any::Any { + /// return self as Any + fn as_any(&self) -> &dyn std::any::Any; +} impl_event! [ MediaData; @@ -29,7 +102,7 @@ impl_event! [ ///ended onended - // todo: this conflicts with image events + // todo: this conflicts with Media events // neither have data, so it's okay // ///error // onerror diff --git a/packages/html/src/events/mounted.rs b/packages/html/src/events/mounted.rs index bc561bf1b..e3908ca8a 100644 --- a/packages/html/src/events/mounted.rs +++ b/packages/html/src/events/mounted.rs @@ -3,22 +3,18 @@ use euclid::Rect; use std::{ - any::Any, fmt::{Display, Formatter}, future::Future, pin::Pin, - rc::Rc, }; /// An Element that has been rendered and allows reading and modifying information about it. /// /// Different platforms will have different implementations and different levels of support for this trait. Renderers that do not support specific features will return `None` for those queries. // we can not use async_trait here because it does not create a trait that is object safe -pub trait RenderedElementBacking { - /// Get the renderer specific element for the given id - fn get_raw_element(&self) -> MountedResult<&dyn Any> { - Err(MountedError::NotSupported) - } +pub trait RenderedElementBacking: std::any::Any { + /// return self as Any + fn as_any(&self) -> &dyn std::any::Any; /// Get the bounding rectangle of the element relative to the viewport (this does not include the scroll position) #[allow(clippy::type_complexity)] @@ -40,7 +36,11 @@ pub trait RenderedElementBacking { } } -impl RenderedElementBacking for () {} +impl RenderedElementBacking for () { + fn as_any(&self) -> &dyn std::any::Any { + self + } +} /// The way that scrolling should be performed #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] @@ -57,22 +57,23 @@ pub enum ScrollBehavior { /// /// Different platforms will have different implementations and different levels of support for this trait. Renderers that do not support specific features will return `None` for those queries. pub struct MountedData { - inner: Rc, + inner: Box, +} + +impl From for MountedData { + fn from(e: E) -> Self { + Self { inner: Box::new(e) } + } } impl MountedData { /// Create a new MountedData pub fn new(registry: impl RenderedElementBacking + 'static) -> Self { Self { - inner: Rc::new(registry), + inner: Box::new(registry), } } - /// Get the renderer specific element for the given id - pub fn get_raw_element(&self) -> MountedResult<&dyn Any> { - self.inner.get_raw_element() - } - /// Get the bounding rectangle of the element relative to the viewport (this does not include the scroll position) pub async fn get_client_rect(&self) -> MountedResult> { self.inner.get_client_rect().await @@ -90,6 +91,11 @@ impl MountedData { pub fn set_focus(&self, focus: bool) -> Pin>>> { self.inner.set_focus(focus) } + + /// Downcast this event to a concrete event type + pub fn downcast(&self) -> Option<&T> { + self.inner.as_any().downcast_ref::() + } } use dioxus_core::Event; diff --git a/packages/html/src/events/mouse.rs b/packages/html/src/events/mouse.rs index 17128f4e4..0c659ed63 100644 --- a/packages/html/src/events/mouse.rs +++ b/packages/html/src/events/mouse.rs @@ -1,91 +1,47 @@ use crate::geometry::{ClientPoint, Coordinates, ElementPoint, PagePoint, ScreenPoint}; -use crate::input_data::{ - decode_mouse_button_set, encode_mouse_button_set, MouseButton, MouseButtonSet, -}; +use crate::input_data::{MouseButton, MouseButtonSet}; +use crate::prelude::*; use dioxus_core::Event; use keyboard_types::Modifiers; -use std::fmt::{Debug, Formatter}; pub type MouseEvent = Event; /// A synthetic event that wraps a web-style [`MouseEvent`](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent) -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, Default, PartialEq, Eq)] /// Data associated with a mouse event -/// -/// Do not use the deprecated fields; they may change or become private in the future. pub struct MouseData { - /// True if the alt key was down when the mouse event was fired. - #[deprecated(since = "0.3.0", note = "use modifiers() instead")] - pub alt_key: bool, + inner: Box, +} - /// The button number that was pressed (if applicable) when the mouse event was fired. - #[deprecated(since = "0.3.0", note = "use trigger_button() instead")] - pub button: i16, +impl From for MouseData { + fn from(e: E) -> Self { + Self { inner: Box::new(e) } + } +} - /// Indicates which buttons are pressed on the mouse (or other input device) when a mouse event is triggered. - /// - /// Each button that can be pressed is represented by a given number (see below). If more than one button is pressed, the button values are added together to produce a new number. For example, if the secondary (2) and auxiliary (4) buttons are pressed simultaneously, the value is 6 (i.e., 2 + 4). - /// - /// - 1: Primary button (usually the left button) - /// - 2: Secondary button (usually the right button) - /// - 4: Auxiliary button (usually the mouse wheel button or middle button) - /// - 8: 4th button (typically the "Browser Back" button) - /// - 16 : 5th button (typically the "Browser Forward" button) - #[deprecated(since = "0.3.0", note = "use held_buttons() instead")] - pub buttons: u16, +impl std::fmt::Debug for MouseData { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("MouseData") + .field("coordinates", &self.coordinates()) + .field("modifiers", &self.modifiers()) + .field("held_buttons", &self.held_buttons()) + .field("trigger_button", &self.trigger_button()) + .finish() + } +} - /// The horizontal coordinate within the application's viewport at which the event occurred (as opposed to the coordinate within the page). - /// - /// For example, clicking on the left edge of the viewport will always result in a mouse event with a clientX value of 0, regardless of whether the page is scrolled horizontally. - #[deprecated(since = "0.3.0", note = "use client_coordinates() instead")] - pub client_x: i32, +impl PartialEq for MouseData { + fn eq(&self, other: &E) -> bool { + self.coordinates() == other.coordinates() + && self.modifiers() == other.modifiers() + && self.held_buttons() == other.held_buttons() + && self.trigger_button() == other.trigger_button() + } +} - /// The vertical coordinate within the application's viewport at which the event occurred (as opposed to the coordinate within the page). - /// - /// For example, clicking on the top edge of the viewport will always result in a mouse event with a clientY value of 0, regardless of whether the page is scrolled vertically. - #[deprecated(since = "0.3.0", note = "use client_coordinates() instead")] - pub client_y: i32, - - /// True if the control key was down when the mouse event was fired. - #[deprecated(since = "0.3.0", note = "use modifiers() instead")] - pub ctrl_key: bool, - - /// True if the meta key was down when the mouse event was fired. - #[deprecated(since = "0.3.0", note = "use modifiers() instead")] - pub meta_key: bool, - - /// The offset in the X coordinate of the mouse pointer between that event and the padding edge of the target node. - #[deprecated(since = "0.3.0", note = "use element_coordinates() instead")] - pub offset_x: i32, - - /// The offset in the Y coordinate of the mouse pointer between that event and the padding edge of the target node. - #[deprecated(since = "0.3.0", note = "use element_coordinates() instead")] - pub offset_y: i32, - - /// The X (horizontal) coordinate (in pixels) of the mouse, relative to the left edge of the entire document. This includes any portion of the document not currently visible. - /// - /// Being based on the edge of the document as it is, this property takes into account any horizontal scrolling of the page. For example, if the page is scrolled such that 200 pixels of the left side of the document are scrolled out of view, and the mouse is clicked 100 pixels inward from the left edge of the view, the value returned by pageX will be 300. - #[deprecated(since = "0.3.0", note = "use page_coordinates() instead")] - pub page_x: i32, - - /// The Y (vertical) coordinate in pixels of the event relative to the whole document. - /// - /// See `page_x`. - #[deprecated(since = "0.3.0", note = "use page_coordinates() instead")] - pub page_y: i32, - - /// The X coordinate of the mouse pointer in global (screen) coordinates. - #[deprecated(since = "0.3.0", note = "use screen_coordinates() instead")] - pub screen_x: i32, - - /// The Y coordinate of the mouse pointer in global (screen) coordinates. - #[deprecated(since = "0.3.0", note = "use screen_coordinates() instead")] - pub screen_y: i32, - - /// True if the shift key was down when the mouse event was fired. - #[deprecated(since = "0.3.0", note = "use modifiers() instead")] - pub shift_key: bool, +/// A trait for any object that has the data for a mouse event +pub trait HasMouseData: PointerInteraction { + /// return self as Any + fn as_any(&self) -> &dyn std::any::Any; } impl_event! { @@ -163,131 +119,172 @@ pub fn ondoubleclick<'a, E: crate::EventReturn, T>( } impl MouseData { - /// Construct MouseData with the specified properties - /// - /// Note: the current implementation truncates coordinates. In the future, when we change the internal representation, it may also support a fractional part. - pub fn new( - coordinates: Coordinates, - trigger_button: Option, - held_buttons: MouseButtonSet, - modifiers: Modifiers, - ) -> Self { - let alt_key = modifiers.contains(Modifiers::ALT); - let ctrl_key = modifiers.contains(Modifiers::CONTROL); - let meta_key = modifiers.contains(Modifiers::META); - let shift_key = modifiers.contains(Modifiers::SHIFT); - - let [client_x, client_y]: [i32; 2] = coordinates.client().cast().into(); - let [offset_x, offset_y]: [i32; 2] = coordinates.element().cast().into(); - let [page_x, page_y]: [i32; 2] = coordinates.page().cast().into(); - let [screen_x, screen_y]: [i32; 2] = coordinates.screen().cast().into(); - - #[allow(deprecated)] + /// Create a new instance of MouseData + pub fn new(inner: impl HasMouseData + 'static) -> Self { Self { - alt_key, - ctrl_key, - meta_key, - shift_key, - - button: trigger_button.map_or(0, |b| b.into_web_code()), - buttons: encode_mouse_button_set(held_buttons), - - client_x, - client_y, - offset_x, - offset_y, - page_x, - page_y, - screen_x, - screen_y, + inner: Box::new(inner), } } - /// The event's coordinates relative to the application's viewport (as opposed to the coordinate within the page). - /// - /// For example, clicking in the top left corner of the viewport will always result in a mouse event with client coordinates (0., 0.), regardless of whether the page is scrolled horizontally. - pub fn client_coordinates(&self) -> ClientPoint { - #[allow(deprecated)] - ClientPoint::new(self.client_x.into(), self.client_y.into()) + /// Downcast this event to a concrete event type + pub fn downcast(&self) -> Option<&T> { + self.inner.as_any().downcast_ref::() + } +} + +impl InteractionLocation for MouseData { + fn client_coordinates(&self) -> ClientPoint { + self.inner.client_coordinates() } - /// The event's coordinates relative to the padding edge of the target element - /// - /// For example, clicking in the top left corner of an element will result in element coordinates (0., 0.) - pub fn element_coordinates(&self) -> ElementPoint { - #[allow(deprecated)] - ElementPoint::new(self.offset_x.into(), self.offset_y.into()) + fn page_coordinates(&self) -> PagePoint { + self.inner.page_coordinates() } - /// The event's coordinates relative to the entire document. This includes any portion of the document not currently visible. - /// - /// For example, if the page is scrolled 200 pixels to the right and 300 pixels down, clicking in the top left corner of the viewport would result in page coordinates (200., 300.) - pub fn page_coordinates(&self) -> PagePoint { - #[allow(deprecated)] - PagePoint::new(self.page_x.into(), self.page_y.into()) + fn screen_coordinates(&self) -> ScreenPoint { + self.inner.screen_coordinates() + } +} + +impl InteractionElementOffset for MouseData { + fn element_coordinates(&self) -> ElementPoint { + self.inner.element_coordinates() } - /// The event's coordinates relative to the entire screen. This takes into account the window's offset. - pub fn screen_coordinates(&self) -> ScreenPoint { - #[allow(deprecated)] - ScreenPoint::new(self.screen_x.into(), self.screen_y.into()) - } - - pub fn coordinates(&self) -> Coordinates { - Coordinates::new( - self.screen_coordinates(), - self.client_coordinates(), - self.element_coordinates(), - self.page_coordinates(), - ) + fn coordinates(&self) -> Coordinates { + self.inner.coordinates() } +} +impl ModifiersInteraction for MouseData { /// The set of modifier keys which were pressed when the event occurred - pub fn modifiers(&self) -> Modifiers { - let mut modifiers = Modifiers::empty(); - - #[allow(deprecated)] - { - if self.alt_key { - modifiers.insert(Modifiers::ALT); - } - if self.ctrl_key { - modifiers.insert(Modifiers::CONTROL); - } - if self.meta_key { - modifiers.insert(Modifiers::META); - } - if self.shift_key { - modifiers.insert(Modifiers::SHIFT); - } - } - - modifiers + fn modifiers(&self) -> Modifiers { + self.inner.modifiers() } +} +impl PointerInteraction for MouseData { /// The set of mouse buttons which were held when the event occurred. - pub fn held_buttons(&self) -> MouseButtonSet { - #[allow(deprecated)] - decode_mouse_button_set(self.buttons) + fn held_buttons(&self) -> MouseButtonSet { + self.inner.held_buttons() } /// The mouse button that triggered the event /// // todo the following is kind of bad; should we just return None when the trigger_button is unreliable (and frankly irrelevant)? i guess we would need the event_type here /// This is only guaranteed to indicate which button was pressed during events caused by pressing or releasing a button. As such, it is not reliable for events such as mouseenter, mouseleave, mouseover, mouseout, or mousemove. For example, a value of MouseButton::Primary may also indicate that no button was pressed. - pub fn trigger_button(&self) -> Option { - #[allow(deprecated)] - Some(MouseButton::from_web_code(self.button)) + fn trigger_button(&self) -> Option { + self.inner.trigger_button() } } -impl Debug for MouseData { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.debug_struct("MouseData") - .field("coordinates", &self.coordinates()) - .field("modifiers", &self.modifiers()) - .field("held_buttons", &self.held_buttons()) - .field("trigger_button", &self.trigger_button()) - .finish() +impl PartialEq for MouseData { + fn eq(&self, other: &Self) -> bool { + self.coordinates() == other.coordinates() + && self.modifiers() == other.modifiers() + && self.held_buttons() == other.held_buttons() + && self.trigger_button() == other.trigger_button() + } +} + +#[cfg(feature = "serialize")] +/// A serialized version of [`MouseData`] +#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone, Default)] +pub struct SerializedMouseData { + /// Common data for all pointer/mouse events + #[serde(flatten)] + point_data: crate::point_interaction::SerializedPointInteraction, +} + +#[cfg(feature = "serialize")] +impl SerializedMouseData { + /// Create a new instance of SerializedMouseData + pub fn new( + trigger_button: Option, + held_buttons: MouseButtonSet, + coordinates: Coordinates, + modifiers: Modifiers, + ) -> Self { + Self { + point_data: crate::point_interaction::SerializedPointInteraction::new( + trigger_button, + held_buttons, + coordinates, + modifiers, + ), + } + } +} + +#[cfg(feature = "serialize")] +impl From<&MouseData> for SerializedMouseData { + fn from(e: &MouseData) -> Self { + Self { + point_data: crate::point_interaction::SerializedPointInteraction::from(e), + } + } +} + +#[cfg(feature = "serialize")] +impl HasMouseData for SerializedMouseData { + fn as_any(&self) -> &dyn std::any::Any { + self + } +} + +#[cfg(feature = "serialize")] +impl InteractionLocation for SerializedMouseData { + fn client_coordinates(&self) -> ClientPoint { + self.point_data.client_coordinates() + } + + fn page_coordinates(&self) -> PagePoint { + self.point_data.page_coordinates() + } + + fn screen_coordinates(&self) -> ScreenPoint { + self.point_data.screen_coordinates() + } +} + +#[cfg(feature = "serialize")] +impl InteractionElementOffset for SerializedMouseData { + fn element_coordinates(&self) -> ElementPoint { + self.point_data.element_coordinates() + } +} + +#[cfg(feature = "serialize")] +impl ModifiersInteraction for SerializedMouseData { + fn modifiers(&self) -> Modifiers { + self.point_data.modifiers() + } +} + +#[cfg(feature = "serialize")] +impl PointerInteraction for SerializedMouseData { + fn held_buttons(&self) -> MouseButtonSet { + self.point_data.held_buttons() + } + + fn trigger_button(&self) -> Option { + self.point_data.trigger_button() + } +} + +#[cfg(feature = "serialize")] +impl serde::Serialize for MouseData { + fn serialize(&self, serializer: S) -> Result { + SerializedMouseData::from(self).serialize(serializer) + } +} + +#[cfg(feature = "serialize")] +impl<'de> serde::Deserialize<'de> for MouseData { + fn deserialize>(deserializer: D) -> Result { + let data = SerializedMouseData::deserialize(deserializer)?; + Ok(Self { + inner: Box::new(data), + }) } } diff --git a/packages/html/src/events/pointer.rs b/packages/html/src/events/pointer.rs index 4134ac852..9556502d3 100644 --- a/packages/html/src/events/pointer.rs +++ b/packages/html/src/events/pointer.rs @@ -1,33 +1,107 @@ use dioxus_core::Event; +use keyboard_types::Modifiers; +use crate::{geometry::*, input_data::*, prelude::*}; + +/// A synthetic event that wraps a web-style [`PointerEvent`](https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent) pub type PointerEvent = Event; -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[derive(Debug, Clone, PartialEq)] + pub struct PointerData { - // Mouse only - pub alt_key: bool, - pub button: i16, - pub buttons: u16, - pub client_x: i32, - pub client_y: i32, - pub ctrl_key: bool, - pub meta_key: bool, - pub page_x: i32, - pub page_y: i32, - pub screen_x: i32, - pub screen_y: i32, - pub shift_key: bool, - pub pointer_id: i32, - pub width: i32, - pub height: i32, - pub pressure: f32, - pub tangential_pressure: f32, - pub tilt_x: i32, - pub tilt_y: i32, - pub twist: i32, - pub pointer_type: String, - pub is_primary: bool, - // pub get_modifier_state: bool, + inner: Box, +} + +impl PointerData { + /// Create a new PointerData + pub fn new(data: impl HasPointerData + 'static) -> Self { + Self::from(data) + } + + /// Downcast this event to a concrete event type + pub fn downcast(&self) -> Option<&T> { + self.inner.as_any().downcast_ref::() + } +} + +impl From for PointerData { + fn from(e: E) -> Self { + Self { inner: Box::new(e) } + } +} + +impl std::fmt::Debug for PointerData { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("PointerData") + .field("pointer_id", &self.pointer_id()) + .field("width", &self.width()) + .field("height", &self.height()) + .field("pressure", &self.pressure()) + .field("tangential_pressure", &self.tangential_pressure()) + .field("tilt_x", &self.tilt_x()) + .field("tilt_y", &self.tilt_y()) + .field("twist", &self.twist()) + .field("pointer_type", &self.pointer_type()) + .field("is_primary", &self.is_primary()) + .field("coordinates", &self.coordinates()) + .field("modifiers", &self.modifiers()) + .field("held_buttons", &self.held_buttons()) + .field("trigger_button", &self.trigger_button()) + .finish() + } +} + +impl PartialEq for PointerData { + fn eq(&self, other: &Self) -> bool { + self.pointer_id() == other.pointer_id() + && self.width() == other.width() + && self.height() == other.height() + && self.pressure() == other.pressure() + && self.tangential_pressure() == other.tangential_pressure() + && self.tilt_x() == other.tilt_x() + && self.tilt_y() == other.tilt_y() + && self.twist() == other.twist() + && self.pointer_type() == other.pointer_type() + && self.is_primary() == other.is_primary() + && self.coordinates() == other.coordinates() + && self.modifiers() == other.modifiers() + && self.held_buttons() == other.held_buttons() + && self.trigger_button() == other.trigger_button() + } +} + +/// A trait for any object that has the data for a pointer event +pub trait HasPointerData: PointerInteraction { + /// Gets the unique identifier of the pointer causing the event. + fn pointer_id(&self) -> i32; + + /// Gets the width (magnitude on the X axis), in CSS pixels, of the contact geometry of the pointer. + fn width(&self) -> i32; + + /// Gets the height (magnitude on the Y axis), in CSS pixels, of the contact geometry of the pointer. + fn height(&self) -> i32; + + /// Gets the normalized pressure of the pointer input in the range of 0 to 1, + fn pressure(&self) -> f32; + + /// Gets the normalized tangential pressure of the pointer input (also known as barrel pressure or cylinder stress) in the range -1 to 1, + fn tangential_pressure(&self) -> f32; + + /// Gets the plane angle (in degrees, in the range of -90 to 90) between the Y-Z plane and the plane containing both the transducer (e.g. pen stylus) axis and the Y axis. + fn tilt_x(&self) -> i32; + + /// Gets the plane angle (in degrees, in the range of -90 to 90) between the X-Z plane and the plane containing both the transducer (e.g. pen stylus) axis and the X axis. + fn tilt_y(&self) -> i32; + + /// Gets the clockwise rotation of the pointer (e.g. pen stylus) around its major axis in degrees, with a value in the range 0 to 359.The clockwise rotation of the pointer (e.g. pen stylus) around its major axis in degrees, with a value in the range 0 to 359. + fn twist(&self) -> i32; + + /// Gets the device type that caused the event (mouse, pen, touch, etc.). + fn pointer_type(&self) -> String; + + /// Gets if the pointer represents the primary pointer of this pointer type. + fn is_primary(&self) -> bool; + + /// return self as Any + fn as_any(&self) -> &dyn std::any::Any; } impl_event![ @@ -62,3 +136,253 @@ impl_event![ /// pointerout onpointerout ]; + +impl PointerData { + /// Gets the unique identifier of the pointer causing the event. + pub fn pointer_id(&self) -> i32 { + self.inner.pointer_id() + } + + /// Gets the width (magnitude on the X axis), in CSS pixels, of the contact geometry of the pointer. + pub fn width(&self) -> i32 { + self.inner.width() + } + + /// Gets the height (magnitude on the Y axis), in CSS pixels, of the contact geometry of the pointer. + pub fn height(&self) -> i32 { + self.inner.height() + } + + /// Gets the normalized pressure of the pointer input in the range of 0 to 1, + pub fn pressure(&self) -> f32 { + self.inner.pressure() + } + + /// Gets the normalized tangential pressure of the pointer input (also known as barrel pressure or cylinder stress) in the range -1 to 1, + pub fn tangential_pressure(&self) -> f32 { + self.inner.tangential_pressure() + } + + /// Gets the plane angle (in degrees, in the range of -90 to 90) between the Y-Z plane and the plane containing both the transducer (e.g. pen stylus) axis and the Y axis. + pub fn tilt_x(&self) -> i32 { + self.inner.tilt_x() + } + + /// Gets the plane angle (in degrees, in the range of -90 to 90) between the X-Z plane and the plane containing both the transducer (e.g. pen stylus) axis and the X axis. + pub fn tilt_y(&self) -> i32 { + self.inner.tilt_y() + } + + /// Gets the clockwise rotation of the pointer (e.g. pen stylus) around its major axis in degrees, with a value in the range 0 to 359.The clockwise rotation of the pointer (e.g. pen stylus) around its major axis in degrees, with a value in the range 0 to 359. + pub fn twist(&self) -> i32 { + self.inner.twist() + } + + /// Gets the device type that caused the event (mouse, pen, touch, etc.). + pub fn pointer_type(&self) -> String { + self.inner.pointer_type() + } + + /// Gets if the pointer represents the primary pointer of this pointer type. + pub fn is_primary(&self) -> bool { + self.inner.is_primary() + } +} + +impl InteractionLocation for PointerData { + fn client_coordinates(&self) -> ClientPoint { + self.inner.client_coordinates() + } + + fn screen_coordinates(&self) -> ScreenPoint { + self.inner.screen_coordinates() + } + + fn page_coordinates(&self) -> PagePoint { + self.inner.page_coordinates() + } +} + +impl InteractionElementOffset for PointerData { + fn element_coordinates(&self) -> ElementPoint { + self.inner.element_coordinates() + } +} + +impl ModifiersInteraction for PointerData { + fn modifiers(&self) -> Modifiers { + self.inner.modifiers() + } +} + +impl PointerInteraction for PointerData { + fn held_buttons(&self) -> MouseButtonSet { + self.inner.held_buttons() + } + + fn trigger_button(&self) -> Option { + self.inner.trigger_button() + } +} + +#[cfg(feature = "serialize")] +/// A serialized version of PointerData +#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)] +pub struct SerializedPointerData { + /// Common data for all pointer/mouse events + #[serde(flatten)] + point_data: crate::point_interaction::SerializedPointInteraction, + + /// The unique identifier of the pointer causing the event. + pointer_id: i32, + + /// The width (magnitude on the X axis), in CSS pixels, of the contact geometry of the pointer. + width: i32, + + /// The height (magnitude on the Y axis), in CSS pixels, of the contact geometry of the pointer. + height: i32, + + /// The normalized pressure of the pointer input in the range of 0 to 1, + pressure: f32, + + /// The normalized tangential pressure of the pointer input (also known as barrel pressure or cylinder stress) in the range -1 to 1, + tangential_pressure: f32, + + /// The plane angle (in degrees, in the range of -90 to 90) between the Y-Z plane and the plane containing both the transducer (e.g. pen stylus) axis and the Y axis. + tilt_x: i32, + + /// The plane angle (in degrees, in the range of -90 to 90) between the X-Z plane and the plane containing both the transducer (e.g. pen stylus) axis and the X axis. + tilt_y: i32, + + /// The clockwise rotation of the pointer (e.g. pen stylus) around its major axis in degrees, with a value in the range 0 to 359.The clockwise rotation of the pointer (e.g. pen stylus) around its major axis in degrees, with a value in the range 0 to 359. + twist: i32, + + /// Indicates the device type that caused the event (mouse, pen, touch, etc.). + pointer_type: String, + + /// Indicates if the pointer represents the primary pointer of this pointer type. + is_primary: bool, +} + +#[cfg(feature = "serialize")] +impl HasPointerData for SerializedPointerData { + fn pointer_id(&self) -> i32 { + self.pointer_id + } + + fn width(&self) -> i32 { + self.width + } + + fn height(&self) -> i32 { + self.height + } + + fn pressure(&self) -> f32 { + self.pressure + } + + fn tangential_pressure(&self) -> f32 { + self.tangential_pressure + } + + fn tilt_x(&self) -> i32 { + self.tilt_x + } + + fn tilt_y(&self) -> i32 { + self.tilt_y + } + + fn twist(&self) -> i32 { + self.twist + } + + fn pointer_type(&self) -> String { + self.pointer_type.clone() + } + + fn is_primary(&self) -> bool { + self.is_primary + } + + fn as_any(&self) -> &dyn std::any::Any { + self + } +} + +#[cfg(feature = "serialize")] +impl InteractionLocation for SerializedPointerData { + fn client_coordinates(&self) -> ClientPoint { + self.point_data.client_coordinates() + } + + fn screen_coordinates(&self) -> ScreenPoint { + self.point_data.screen_coordinates() + } + + fn page_coordinates(&self) -> PagePoint { + self.point_data.page_coordinates() + } +} + +#[cfg(feature = "serialize")] +impl InteractionElementOffset for SerializedPointerData { + fn element_coordinates(&self) -> ElementPoint { + self.point_data.element_coordinates() + } +} + +#[cfg(feature = "serialize")] +impl ModifiersInteraction for SerializedPointerData { + fn modifiers(&self) -> Modifiers { + self.point_data.modifiers() + } +} + +#[cfg(feature = "serialize")] +impl PointerInteraction for SerializedPointerData { + fn held_buttons(&self) -> MouseButtonSet { + self.point_data.held_buttons() + } + + fn trigger_button(&self) -> Option { + self.point_data.trigger_button() + } +} + +#[cfg(feature = "serialize")] +impl From<&PointerData> for SerializedPointerData { + fn from(data: &PointerData) -> Self { + Self { + point_data: data.into(), + pointer_id: data.pointer_id(), + width: data.width(), + height: data.height(), + pressure: data.pressure(), + tangential_pressure: data.tangential_pressure(), + tilt_x: data.tilt_x(), + tilt_y: data.tilt_y(), + twist: data.twist(), + pointer_type: data.pointer_type().to_string(), + is_primary: data.is_primary(), + } + } +} + +#[cfg(feature = "serialize")] +impl serde::Serialize for PointerData { + fn serialize(&self, serializer: S) -> Result { + SerializedPointerData::from(self).serialize(serializer) + } +} + +#[cfg(feature = "serialize")] +impl<'de> serde::Deserialize<'de> for PointerData { + fn deserialize>(deserializer: D) -> Result { + let data = SerializedPointerData::deserialize(deserializer)?; + Ok(Self { + inner: Box::new(data), + }) + } +} diff --git a/packages/html/src/events/scroll.rs b/packages/html/src/events/scroll.rs index 798218b02..3693cc144 100644 --- a/packages/html/src/events/scroll.rs +++ b/packages/html/src/events/scroll.rs @@ -1,9 +1,83 @@ use dioxus_core::Event; pub type ScrollEvent = Event; -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ScrollData {} + +pub struct ScrollData { + inner: Box, +} + +impl From for ScrollData { + fn from(e: E) -> Self { + Self { inner: Box::new(e) } + } +} + +impl ScrollData { + /// Create a new ScrollData + pub fn new(inner: impl HasScrollData + 'static) -> Self { + Self { + inner: Box::new(inner), + } + } + + /// Downcast this event to a concrete event type + pub fn downcast(&self) -> Option<&T> { + self.inner.as_any().downcast_ref::() + } +} + +impl std::fmt::Debug for ScrollData { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ScrollData").finish() + } +} + +impl PartialEq for ScrollData { + fn eq(&self, _other: &Self) -> bool { + true + } +} + +#[cfg(feature = "serialize")] +/// A serialized version of ScrollData +#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)] +pub struct SerializedScrollData {} + +#[cfg(feature = "serialize")] +impl From<&ScrollData> for SerializedScrollData { + fn from(_: &ScrollData) -> Self { + Self {} + } +} + +#[cfg(feature = "serialize")] +impl HasScrollData for SerializedScrollData { + fn as_any(&self) -> &dyn std::any::Any { + self + } +} + +#[cfg(feature = "serialize")] +impl serde::Serialize for ScrollData { + fn serialize(&self, serializer: S) -> Result { + SerializedScrollData::from(self).serialize(serializer) + } +} + +#[cfg(feature = "serialize")] +impl<'de> serde::Deserialize<'de> for ScrollData { + fn deserialize>(deserializer: D) -> Result { + let data = SerializedScrollData::deserialize(deserializer)?; + Ok(Self { + inner: Box::new(data), + }) + } +} + +pub trait HasScrollData: std::any::Any { + /// return self as Any + fn as_any(&self) -> &dyn std::any::Any; +} impl_event! { ScrollData; diff --git a/packages/html/src/events/selection.rs b/packages/html/src/events/selection.rs index 249353e63..30c3d9bf0 100644 --- a/packages/html/src/events/selection.rs +++ b/packages/html/src/events/selection.rs @@ -1,9 +1,83 @@ use dioxus_core::Event; pub type SelectionEvent = Event; -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct SelectionData {} + +pub struct SelectionData { + inner: Box, +} + +impl SelectionData { + /// Create a new SelectionData + pub fn new(inner: impl HasSelectionData + 'static) -> Self { + Self { + inner: Box::new(inner), + } + } + + /// Downcast this event to a concrete event type + pub fn downcast(&self) -> Option<&T> { + self.inner.as_any().downcast_ref::() + } +} + +impl From for SelectionData { + fn from(e: E) -> Self { + Self { inner: Box::new(e) } + } +} + +impl std::fmt::Debug for SelectionData { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("SelectionData").finish() + } +} + +impl PartialEq for SelectionData { + fn eq(&self, _other: &Self) -> bool { + true + } +} + +#[cfg(feature = "serialize")] +/// A serialized version of SelectionData +#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)] +pub struct SerializedSelectionData {} + +#[cfg(feature = "serialize")] +impl From<&SelectionData> for SerializedSelectionData { + fn from(_: &SelectionData) -> Self { + Self {} + } +} + +#[cfg(feature = "serialize")] +impl HasSelectionData for SerializedSelectionData { + fn as_any(&self) -> &dyn std::any::Any { + self + } +} + +#[cfg(feature = "serialize")] +impl serde::Serialize for SelectionData { + fn serialize(&self, serializer: S) -> Result { + SerializedSelectionData::from(self).serialize(serializer) + } +} + +#[cfg(feature = "serialize")] +impl<'de> serde::Deserialize<'de> for SelectionData { + fn deserialize>(deserializer: D) -> Result { + let data = SerializedSelectionData::deserialize(deserializer)?; + Ok(Self { + inner: Box::new(data), + }) + } +} + +pub trait HasSelectionData: std::any::Any { + /// return self as Any + fn as_any(&self) -> &dyn std::any::Any; +} impl_event! [ SelectionData; diff --git a/packages/html/src/events/toggle.rs b/packages/html/src/events/toggle.rs index 4f225a8e6..e929a6a64 100644 --- a/packages/html/src/events/toggle.rs +++ b/packages/html/src/events/toggle.rs @@ -1,9 +1,83 @@ use dioxus_core::Event; pub type ToggleEvent = Event; -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ToggleData {} + +pub struct ToggleData { + inner: Box, +} + +impl From for ToggleData { + fn from(e: E) -> Self { + Self { inner: Box::new(e) } + } +} + +impl std::fmt::Debug for ToggleData { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ToggleData").finish() + } +} + +impl PartialEq for ToggleData { + fn eq(&self, _other: &Self) -> bool { + true + } +} + +impl ToggleData { + /// Create a new ToggleData + pub fn new(inner: impl HasToggleData + 'static) -> Self { + Self { + inner: Box::new(inner), + } + } + + /// Downcast this event to a concrete event type + pub fn downcast(&self) -> Option<&T> { + self.inner.as_any().downcast_ref::() + } +} + +#[cfg(feature = "serialize")] +/// A serialized version of ToggleData +#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)] +pub struct SerializedToggleData {} + +#[cfg(feature = "serialize")] +impl From<&ToggleData> for SerializedToggleData { + fn from(_: &ToggleData) -> Self { + Self {} + } +} + +#[cfg(feature = "serialize")] +impl HasToggleData for SerializedToggleData { + fn as_any(&self) -> &dyn std::any::Any { + self + } +} + +#[cfg(feature = "serialize")] +impl serde::Serialize for ToggleData { + fn serialize(&self, serializer: S) -> Result { + SerializedToggleData::from(self).serialize(serializer) + } +} + +#[cfg(feature = "serialize")] +impl<'de> serde::Deserialize<'de> for ToggleData { + fn deserialize>(deserializer: D) -> Result { + let data = SerializedToggleData::deserialize(deserializer)?; + Ok(Self { + inner: Box::new(data), + }) + } +} + +pub trait HasToggleData: std::any::Any { + /// return self as Any + fn as_any(&self) -> &dyn std::any::Any; +} impl_event! { ToggleData; diff --git a/packages/html/src/events/touch.rs b/packages/html/src/events/touch.rs index 1e795b6da..b30ace17c 100644 --- a/packages/html/src/events/touch.rs +++ b/packages/html/src/events/touch.rs @@ -1,17 +1,353 @@ use dioxus_core::Event; +use keyboard_types::Modifiers; + +use crate::geometry::*; +use crate::prelude::{InteractionLocation, ModifiersInteraction}; pub type TouchEvent = Event; -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[derive(Debug, Clone, PartialEq, Eq)] pub struct TouchData { - pub alt_key: bool, - pub ctrl_key: bool, - pub meta_key: bool, - pub shift_key: bool, - // get_modifier_state: bool, - // changedTouches: DOMTouchList, - // targetTouches: DOMTouchList, - // touches: DOMTouchList, + inner: Box, +} + +impl From for TouchData { + fn from(e: E) -> Self { + Self { inner: Box::new(e) } + } +} + +impl std::fmt::Debug for TouchData { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("TouchData") + .field("modifiers", &self.modifiers()) + .field("touches", &self.touches()) + .field("touches_changed", &self.touches_changed()) + .field("target_touches", &self.target_touches()) + .finish() + } +} + +impl PartialEq for TouchData { + fn eq(&self, other: &Self) -> bool { + self.modifiers() == other.modifiers() + } +} + +impl TouchData { + /// Create a new TouchData + pub fn new(inner: impl HasTouchData + 'static) -> Self { + Self { + inner: Box::new(inner), + } + } + + /// Get the pointers that are currently down + pub fn touches(&self) -> Vec { + self.inner.touches() + } + + /// Get the touches that have changed since the last event + pub fn touches_changed(&self) -> Vec { + self.inner.touches_changed() + } + + /// Get the touches that started and stayed on the element that triggered this event + pub fn target_touches(&self) -> Vec { + self.inner.target_touches() + } + + /// Downcast this event to a concrete event type + pub fn downcast(&self) -> Option<&T> { + self.inner.as_any().downcast_ref::() + } +} + +impl ModifiersInteraction for TouchData { + fn modifiers(&self) -> Modifiers { + self.inner.modifiers() + } +} + +#[cfg(feature = "serialize")] +/// A serialized version of TouchData +#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)] +pub struct SerializedTouchData { + alt_key: bool, + ctrl_key: bool, + meta_key: bool, + shift_key: bool, + touches: Vec, + changed_touches: Vec, + target_touches: Vec, +} + +#[cfg(feature = "serialize")] +impl From<&TouchData> for SerializedTouchData { + fn from(data: &TouchData) -> Self { + let modifiers = data.modifiers(); + Self { + alt_key: modifiers.contains(Modifiers::ALT), + ctrl_key: modifiers.contains(Modifiers::CONTROL), + meta_key: modifiers.contains(Modifiers::META), + shift_key: modifiers.contains(Modifiers::SHIFT), + touches: data.touches().iter().map(|t| t.into()).collect(), + changed_touches: data.touches_changed().iter().map(|t| t.into()).collect(), + target_touches: data.target_touches().iter().map(|t| t.into()).collect(), + } + } +} + +#[cfg(feature = "serialize")] +impl ModifiersInteraction for SerializedTouchData { + fn modifiers(&self) -> Modifiers { + let mut modifiers = Modifiers::default(); + if self.alt_key { + modifiers.insert(Modifiers::ALT); + } + if self.ctrl_key { + modifiers.insert(Modifiers::CONTROL); + } + if self.meta_key { + modifiers.insert(Modifiers::META); + } + if self.shift_key { + modifiers.insert(Modifiers::SHIFT); + } + modifiers + } +} + +#[cfg(feature = "serialize")] +impl HasTouchData for SerializedTouchData { + fn touches(&self) -> Vec { + self.touches.clone().into_iter().map(Into::into).collect() + } + + fn touches_changed(&self) -> Vec { + self.changed_touches + .clone() + .into_iter() + .map(Into::into) + .collect() + } + + fn target_touches(&self) -> Vec { + self.target_touches + .clone() + .into_iter() + .map(Into::into) + .collect() + } + + fn as_any(&self) -> &dyn std::any::Any { + self + } +} + +#[cfg(feature = "serialize")] +impl serde::Serialize for TouchData { + fn serialize(&self, serializer: S) -> Result { + SerializedTouchData::from(self).serialize(serializer) + } +} + +#[cfg(feature = "serialize")] +impl<'de> serde::Deserialize<'de> for TouchData { + fn deserialize>(deserializer: D) -> Result { + let data = SerializedTouchData::deserialize(deserializer)?; + Ok(Self { + inner: Box::new(data), + }) + } +} + +pub trait HasTouchData: ModifiersInteraction + std::any::Any { + /// Get the touches that are currently down + fn touches(&self) -> Vec; + + /// Get the touches that have changed since the last event + fn touches_changed(&self) -> Vec; + + /// Get the touches that started and stayed on the element that triggered this event + fn target_touches(&self) -> Vec; + + /// return self as Any + fn as_any(&self) -> &dyn std::any::Any; +} + +pub struct TouchPoint { + inner: Box, +} + +impl From for TouchPoint { + fn from(e: E) -> Self { + Self { inner: Box::new(e) } + } +} + +impl TouchPoint { + /// Create a new TouchPoint + pub fn new(inner: impl HasTouchPointData + 'static) -> Self { + Self { + inner: Box::new(inner), + } + } + + /// A unique identifier for this touch point that will be the same for the duration of the touch + fn identifier(&self) -> i32 { + self.inner.identifier() + } + + /// the pressure of the touch + fn force(&self) -> f64 { + self.inner.force() + } + + /// the radius of the touch + fn radius(&self) -> ScreenPoint { + self.inner.radius() + } + + /// the rotation of the touch in degrees between 0 and 90 + fn rotation(&self) -> f64 { + self.inner.rotation() + } + + /// Downcast this event to a concrete event type + pub fn downcast(&self) -> Option<&T> { + self.inner.as_any().downcast_ref::() + } +} + +impl std::fmt::Debug for TouchPoint { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("TouchPoint") + .field("client_coordinates", &self.client_coordinates()) + .field("page_coordinates", &self.page_coordinates()) + .field("screen_coordinates", &self.screen_coordinates()) + .field("identifier", &self.identifier()) + .field("force", &self.force()) + .field("radius", &self.radius()) + .field("rotation", &self.rotation()) + .finish() + } +} + +impl InteractionLocation for TouchPoint { + fn client_coordinates(&self) -> ClientPoint { + self.inner.client_coordinates() + } + + fn page_coordinates(&self) -> PagePoint { + self.inner.page_coordinates() + } + + fn screen_coordinates(&self) -> ScreenPoint { + self.inner.screen_coordinates() + } +} + +/// A trait for touch point data +pub trait HasTouchPointData: InteractionLocation + std::any::Any { + /// A unique identifier for this touch point that will be the same for the duration of the touch + fn identifier(&self) -> i32; + + /// the pressure of the touch + fn force(&self) -> f64; + + /// the radius of the touch + fn radius(&self) -> ScreenPoint; + + /// the rotation of the touch in degrees between 0 and 90 + fn rotation(&self) -> f64; + + /// return self as Any + fn as_any(&self) -> &dyn std::any::Any; +} + +#[cfg(feature = "serialize")] +/// A serialized version of TouchPoint +#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)] +struct SerializedTouchPoint { + identifier: i32, + client_x: f64, + client_y: f64, + page_x: f64, + page_y: f64, + screen_x: f64, + screen_y: f64, + force: f64, + radius_x: f64, + radius_y: f64, + rotation_angle: f64, +} + +#[cfg(feature = "serialize")] +impl From<&TouchPoint> for SerializedTouchPoint { + fn from(point: &TouchPoint) -> Self { + let client_coordinates = point.client_coordinates(); + + let page_coordinates = point.page_coordinates(); + let screen_coordinates = point.screen_coordinates(); + Self { + identifier: point.identifier(), + client_x: client_coordinates.x, + client_y: client_coordinates.y, + page_x: page_coordinates.x, + page_y: page_coordinates.y, + screen_x: screen_coordinates.x, + screen_y: screen_coordinates.y, + force: point.force(), + radius_x: point.radius().x, + radius_y: point.radius().y, + rotation_angle: point.rotation(), + } + } +} + +#[cfg(feature = "serialize")] +impl HasTouchPointData for SerializedTouchPoint { + /// A unique identifier for this touch point that will be the same for the duration of the touch + fn identifier(&self) -> i32 { + self.identifier + } + + /// the pressure of the touch + fn force(&self) -> f64 { + self.force + } + + /// the radius of the touch + fn radius(&self) -> ScreenPoint { + ScreenPoint::new(self.radius_x, self.radius_y) + } + + /// the rotation of the touch in degrees between 0 and 90 + fn rotation(&self) -> f64 { + self.rotation_angle + } + + /// return self as Any + fn as_any(&self) -> &dyn std::any::Any { + self + } +} + +#[cfg(feature = "serialize")] +impl InteractionLocation for SerializedTouchPoint { + /// Gets the coordinates of the event relative to the browser viewport. + fn client_coordinates(&self) -> ClientPoint { + ClientPoint::new(self.client_x, self.client_y) + } + + /// Gets the coordinates of the event relative to the screen. + fn screen_coordinates(&self) -> ScreenPoint { + ScreenPoint::new(self.screen_x, self.screen_y) + } + + /// Gets the coordinates of the event relative to the page. + fn page_coordinates(&self) -> PagePoint { + PagePoint::new(self.page_x, self.page_y) + } } impl_event! { diff --git a/packages/html/src/events/transition.rs b/packages/html/src/events/transition.rs index ecc3e1636..4e680d650 100644 --- a/packages/html/src/events/transition.rs +++ b/packages/html/src/events/transition.rs @@ -1,12 +1,111 @@ use dioxus_core::Event; pub type TransitionEvent = Event; -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[derive(Debug, Clone, PartialEq)] + pub struct TransitionData { - pub property_name: String, - pub pseudo_element: String, - pub elapsed_time: f32, + inner: Box, +} + +impl From for TransitionData { + fn from(e: E) -> Self { + Self { inner: Box::new(e) } + } +} + +impl std::fmt::Debug for TransitionData { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("TransitionData") + .field("property_name", &self.inner.property_name()) + .field("pseudo_element", &self.inner.pseudo_element()) + .field("elapsed_time", &self.inner.elapsed_time()) + .finish() + } +} + +impl PartialEq for TransitionData { + fn eq(&self, other: &Self) -> bool { + self.inner.property_name() == other.inner.property_name() + && self.inner.pseudo_element() == other.inner.pseudo_element() + && self.inner.elapsed_time() == other.inner.elapsed_time() + } +} + +impl TransitionData { + /// Create a new TransitionData + pub fn new(inner: impl HasTransitionData + 'static) -> Self { + Self { + inner: Box::new(inner), + } + } + + /// Downcast this event to a concrete event type + pub fn downcast(&self) -> Option<&T> { + self.inner.as_any().downcast_ref::() + } +} + +#[cfg(feature = "serialize")] +/// A serialized version of TransitionData +#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)] +pub struct SerializedTransitionData { + property_name: String, + pseudo_element: String, + elapsed_time: f32, +} + +#[cfg(feature = "serialize")] +impl From<&TransitionData> for SerializedTransitionData { + fn from(data: &TransitionData) -> Self { + Self { + property_name: data.inner.property_name(), + pseudo_element: data.inner.pseudo_element(), + elapsed_time: data.inner.elapsed_time(), + } + } +} + +#[cfg(feature = "serialize")] +impl HasTransitionData for SerializedTransitionData { + fn property_name(&self) -> String { + self.property_name.clone() + } + + fn pseudo_element(&self) -> String { + self.pseudo_element.clone() + } + + fn elapsed_time(&self) -> f32 { + self.elapsed_time + } + + fn as_any(&self) -> &dyn std::any::Any { + self + } +} + +#[cfg(feature = "serialize")] +impl serde::Serialize for TransitionData { + fn serialize(&self, serializer: S) -> Result { + SerializedTransitionData::from(self).serialize(serializer) + } +} + +#[cfg(feature = "serialize")] +impl<'de> serde::Deserialize<'de> for TransitionData { + fn deserialize>(deserializer: D) -> Result { + let data = SerializedTransitionData::deserialize(deserializer)?; + Ok(Self { + inner: Box::new(data), + }) + } +} + +pub trait HasTransitionData: std::any::Any { + fn property_name(&self) -> String; + fn pseudo_element(&self) -> String; + fn elapsed_time(&self) -> f32; + /// return self as Any + fn as_any(&self) -> &dyn std::any::Any; } impl_event! { diff --git a/packages/html/src/events/wheel.rs b/packages/html/src/events/wheel.rs index 758fa7936..2661eee2b 100644 --- a/packages/html/src/events/wheel.rs +++ b/packages/html/src/events/wheel.rs @@ -1,23 +1,118 @@ use dioxus_core::Event; -use euclid::UnknownUnit; -use std::fmt::{Debug, Formatter}; +use std::fmt::Formatter; -use crate::geometry::{LinesVector, PagesVector, PixelsVector, WheelDelta}; +use crate::geometry::WheelDelta; pub type WheelEvent = Event; -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, PartialEq, Default)] + pub struct WheelData { - #[deprecated(since = "0.3.0", note = "use delta() instead")] + inner: Box, +} + +impl From for WheelData { + fn from(e: E) -> Self { + Self { inner: Box::new(e) } + } +} + +impl std::fmt::Debug for WheelData { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("WheelData") + .field("delta", &self.delta()) + .finish() + } +} + +impl PartialEq for WheelData { + fn eq(&self, other: &Self) -> bool { + self.inner.delta() == other.inner.delta() + } +} + +impl WheelData { + /// Create a new WheelData + pub fn new(inner: impl HasWheelData + 'static) -> Self { + Self { + inner: Box::new(inner), + } + } + + /// The amount of wheel movement + #[allow(deprecated)] + pub fn delta(&self) -> WheelDelta { + self.inner.delta() + } + + /// Downcast this event to a concrete event type + pub fn downcast(&self) -> Option<&T> { + self.inner.as_any().downcast_ref::() + } +} + +#[cfg(feature = "serialize")] +/// A serialized version of WheelData +#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)] +pub struct SerializedWheelData { pub delta_mode: u32, - #[deprecated(since = "0.3.0", note = "use delta() instead")] pub delta_x: f64, - #[deprecated(since = "0.3.0", note = "use delta() instead")] pub delta_y: f64, - #[deprecated(since = "0.3.0", note = "use delta() instead")] pub delta_z: f64, } +#[cfg(feature = "serialize")] +impl SerializedWheelData { + /// Create a new SerializedWheelData + pub fn new(delta: WheelDelta) -> Self { + let delta_mode = match delta { + WheelDelta::Pixels(_) => 0, + WheelDelta::Lines(_) => 1, + WheelDelta::Pages(_) => 2, + }; + let delta_raw = delta.strip_units(); + Self { + delta_mode, + delta_x: delta_raw.x, + delta_y: delta_raw.y, + delta_z: delta_raw.z, + } + } +} + +#[cfg(feature = "serialize")] +impl From<&WheelData> for SerializedWheelData { + fn from(data: &WheelData) -> Self { + Self::new(data.delta()) + } +} + +#[cfg(feature = "serialize")] +impl HasWheelData for SerializedWheelData { + fn delta(&self) -> WheelDelta { + WheelDelta::from_web_attributes(self.delta_mode, self.delta_x, self.delta_y, self.delta_z) + } + + fn as_any(&self) -> &dyn std::any::Any { + self + } +} + +#[cfg(feature = "serialize")] +impl serde::Serialize for WheelData { + fn serialize(&self, serializer: S) -> Result { + SerializedWheelData::from(self).serialize(serializer) + } +} + +#[cfg(feature = "serialize")] +impl<'de> serde::Deserialize<'de> for WheelData { + fn deserialize>(deserializer: D) -> Result { + let data = SerializedWheelData::deserialize(deserializer)?; + Ok(Self { + inner: Box::new(data), + }) + } +} + impl_event![ WheelData; @@ -25,54 +120,10 @@ impl_event![ onwheel ]; -impl WheelData { - /// Construct a new WheelData with the specified wheel movement delta - pub fn new(delta: WheelDelta) -> Self { - let (delta_mode, vector) = match delta { - WheelDelta::Pixels(v) => (0, v.cast_unit::()), - WheelDelta::Lines(v) => (1, v.cast_unit::()), - WheelDelta::Pages(v) => (2, v.cast_unit::()), - }; - - #[allow(deprecated)] - WheelData { - delta_mode, - delta_x: vector.x, - delta_y: vector.y, - delta_z: vector.z, - } - } - - /// Construct from the attributes of the web wheel event - pub fn from_web_attributes(delta_mode: u32, delta_x: f64, delta_y: f64, delta_z: f64) -> Self { - #[allow(deprecated)] - Self { - delta_mode, - delta_x, - delta_y, - delta_z, - } - } - +pub trait HasWheelData: std::any::Any { /// The amount of wheel movement - #[allow(deprecated)] - pub fn delta(&self) -> WheelDelta { - let x = self.delta_x; - let y = self.delta_y; - let z = self.delta_z; - match self.delta_mode { - 0 => WheelDelta::Pixels(PixelsVector::new(x, y, z)), - 1 => WheelDelta::Lines(LinesVector::new(x, y, z)), - 2 => WheelDelta::Pages(PagesVector::new(x, y, z)), - _ => panic!("Invalid delta mode, {:?}", self.delta_mode), - } - } -} + fn delta(&self) -> WheelDelta; -impl Debug for WheelData { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.debug_struct("WheelData") - .field("delta", &self.delta()) - .finish() - } + /// return self as Any + fn as_any(&self) -> &dyn std::any::Any; } diff --git a/packages/html/src/geometry.rs b/packages/html/src/geometry.rs index 990ca5b5e..4a0b16f4a 100644 --- a/packages/html/src/geometry.rs +++ b/packages/html/src/geometry.rs @@ -47,7 +47,7 @@ pub type PagesVector = Vector3D; /// A vector representing the amount the mouse wheel was moved /// /// This may be expressed in Pixels, Lines or Pages -#[derive(Copy, Clone, Debug)] +#[derive(Copy, Clone, Debug, PartialEq)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] pub enum WheelDelta { /// Movement in Pixels @@ -59,6 +59,16 @@ pub enum WheelDelta { } impl WheelDelta { + /// Construct from the attributes of the web wheel event + pub fn from_web_attributes(delta_mode: u32, delta_x: f64, delta_y: f64, delta_z: f64) -> Self { + match delta_mode { + 0 => WheelDelta::Pixels(PixelsVector::new(delta_x, delta_y, delta_z)), + 1 => WheelDelta::Lines(LinesVector::new(delta_x, delta_y, delta_z)), + 2 => WheelDelta::Pages(PagesVector::new(delta_x, delta_y, delta_z)), + _ => panic!("Invalid delta mode, {:?}", delta_mode), + } + } + /// Convenience function for constructing a WheelDelta with pixel units pub fn pixels(x: f64, y: f64, z: f64) -> Self { WheelDelta::Pixels(PixelsVector::new(x, y, z)) @@ -96,7 +106,7 @@ impl WheelDelta { } /// Coordinates of a point in the app's interface -#[derive(Debug)] +#[derive(Debug, PartialEq)] pub struct Coordinates { screen: ScreenPoint, client: ClientPoint, diff --git a/packages/html/src/input_data.rs b/packages/html/src/input_data.rs index 28d182fdc..d354c78a9 100644 --- a/packages/html/src/input_data.rs +++ b/packages/html/src/input_data.rs @@ -8,9 +8,10 @@ use keyboard_types::Location; /// A mouse button type (such as Primary/Secondary) // note: EnumSetType also derives Copy and Clone for some reason #[allow(clippy::unused_unit)] -#[derive(EnumSetType, Debug)] +#[derive(EnumSetType, Debug, Default)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] pub enum MouseButton { + #[default] /// Primary button (typically the left button) Primary, /// Secondary button (typically the right button) diff --git a/packages/html/src/lib.rs b/packages/html/src/lib.rs index c44777801..e7cce2657 100644 --- a/packages/html/src/lib.rs +++ b/packages/html/src/lib.rs @@ -27,6 +27,7 @@ mod global_attributes; pub mod input_data; #[cfg(feature = "native-bind")] pub mod native_bind; +pub mod point_interaction; mod render_template; #[cfg(feature = "wasm-bind")] mod web_sys_bind; @@ -42,9 +43,13 @@ pub use events::*; pub use global_attributes::*; pub use render_template::*; -mod eval; +#[cfg(feature = "eval")] +pub mod eval; pub mod prelude { + #[cfg(feature = "eval")] pub use crate::eval::*; pub use crate::events::*; + pub use crate::point_interaction::*; + pub use keyboard_types::{self, Code, Key, Location, Modifiers}; } diff --git a/packages/html/src/point_interaction.rs b/packages/html/src/point_interaction.rs new file mode 100644 index 000000000..9aa4e0f31 --- /dev/null +++ b/packages/html/src/point_interaction.rs @@ -0,0 +1,214 @@ +use keyboard_types::Modifiers; + +use crate::{ + geometry::{ClientPoint, Coordinates, ElementPoint, PagePoint, ScreenPoint}, + input_data::{MouseButton, MouseButtonSet}, +}; + +/// A interaction that contains data about the location of the event. +pub trait InteractionLocation { + /// Gets the coordinates of the event relative to the browser viewport. + fn client_coordinates(&self) -> ClientPoint; + + /// Gets the coordinates of the event relative to the screen. + fn screen_coordinates(&self) -> ScreenPoint; + + /// Gets the coordinates of the event relative to the page. + fn page_coordinates(&self) -> PagePoint; +} + +/// A interaction that contains data about the location of the event. +pub trait InteractionElementOffset: InteractionLocation { + /// Gets the coordinates of the event. + fn coordinates(&self) -> Coordinates { + Coordinates::new( + self.screen_coordinates(), + self.client_coordinates(), + self.element_coordinates(), + self.page_coordinates(), + ) + } + + /// Gets the coordinates of the event relative to the target element. + fn element_coordinates(&self) -> ElementPoint; +} + +/// A interaction that contains data about the pointer button(s) that triggered the event. +pub trait PointerInteraction: InteractionElementOffset + ModifiersInteraction { + /// Gets the button that triggered the event. + fn trigger_button(&self) -> Option; + + /// Gets the buttons that are currently held down. + fn held_buttons(&self) -> MouseButtonSet; +} + +/// A interaction that contains data about the current state of the keyboard modifiers. +pub trait ModifiersInteraction { + /// Gets the modifiers of the pointer event. + fn modifiers(&self) -> Modifiers; +} + +#[cfg(feature = "serialize")] +#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone, Default)] +pub(crate) struct SerializedPointInteraction { + pub alt_key: bool, + + /// The button number that was pressed (if applicable) when the mouse event was fired. + pub button: i16, + + /// Indicates which buttons are pressed on the mouse (or other input device) when a mouse event is triggered. + /// + /// Each button that can be pressed is represented by a given number (see below). If more than one button is pressed, the button values are added together to produce a new number. For example, if the secondary (2) and auxiliary (4) buttons are pressed simultaneously, the value is 6 (i.e., 2 + 4). + /// + /// - 1: Primary button (usually the left button) + /// - 2: Secondary button (usually the right button) + /// - 4: Auxiliary button (usually the mouse wheel button or middle button) + /// - 8: 4th button (typically the "Browser Back" button) + /// - 16 : 5th button (typically the "Browser Forward" button) + pub buttons: u16, + + /// The horizontal coordinate within the application's viewport at which the event occurred (as opposed to the coordinate within the page). + /// + /// For example, clicking on the left edge of the viewport will always result in a mouse event with a clientX value of 0, regardless of whether the page is scrolled horizontally. + pub client_x: i32, + + /// The vertical coordinate within the application's viewport at which the event occurred (as opposed to the coordinate within the page). + /// + /// For example, clicking on the top edge of the viewport will always result in a mouse event with a clientY value of 0, regardless of whether the page is scrolled vertically. + pub client_y: i32, + + /// True if the control key was down when the mouse event was fired. + pub ctrl_key: bool, + + /// True if the meta key was down when the mouse event was fired. + pub meta_key: bool, + + /// The offset in the X coordinate of the mouse pointer between that event and the padding edge of the target node. + pub offset_x: i32, + + /// The offset in the Y coordinate of the mouse pointer between that event and the padding edge of the target node. + pub offset_y: i32, + + /// The X (horizontal) coordinate (in pixels) of the mouse, relative to the left edge of the entire document. This includes any portion of the document not currently visible. + /// + /// Being based on the edge of the document as it is, this property takes into account any horizontal scrolling of the page. For example, if the page is scrolled such that 200 pixels of the left side of the document are scrolled out of view, and the mouse is clicked 100 pixels inward from the left edge of the view, the value returned by pageX will be 300. + pub page_x: i32, + + /// The Y (vertical) coordinate in pixels of the event relative to the whole document. + /// + /// See `page_x`. + pub page_y: i32, + + /// The X coordinate of the mouse pointer in global (screen) coordinates. + pub screen_x: i32, + + /// The Y coordinate of the mouse pointer in global (screen) coordinates. + pub screen_y: i32, + + /// True if the shift key was down when the mouse event was fired. + pub shift_key: bool, +} + +#[cfg(feature = "serialize")] +impl SerializedPointInteraction { + pub fn new( + trigger_button: Option, + held_buttons: MouseButtonSet, + coordinates: Coordinates, + modifiers: Modifiers, + ) -> Self { + let alt_key = modifiers.contains(Modifiers::ALT); + let ctrl_key = modifiers.contains(Modifiers::CONTROL); + let meta_key = modifiers.contains(Modifiers::META); + let shift_key = modifiers.contains(Modifiers::SHIFT); + + let [client_x, client_y]: [i32; 2] = coordinates.client().cast().into(); + let [offset_x, offset_y]: [i32; 2] = coordinates.element().cast().into(); + let [page_x, page_y]: [i32; 2] = coordinates.page().cast().into(); + let [screen_x, screen_y]: [i32; 2] = coordinates.screen().cast().into(); + Self { + button: trigger_button + .map_or(MouseButton::default(), |b| b) + .into_web_code(), + buttons: crate::input_data::encode_mouse_button_set(held_buttons), + meta_key, + ctrl_key, + shift_key, + alt_key, + client_x, + client_y, + screen_x, + screen_y, + offset_x, + offset_y, + page_x, + page_y, + } + } +} + +#[cfg(feature = "serialize")] +impl From<&E> for SerializedPointInteraction { + fn from(data: &E) -> Self { + let trigger_button = data.trigger_button(); + let held_buttons = data.held_buttons(); + let coordinates = data.coordinates(); + let modifiers = data.modifiers(); + Self::new(trigger_button, held_buttons, coordinates, modifiers) + } +} + +#[cfg(feature = "serialize")] +impl PointerInteraction for SerializedPointInteraction { + fn held_buttons(&self) -> MouseButtonSet { + crate::input_data::decode_mouse_button_set(self.buttons) + } + + fn trigger_button(&self) -> Option { + Some(MouseButton::from_web_code(self.button)) + } +} + +#[cfg(feature = "serialize")] +impl ModifiersInteraction for SerializedPointInteraction { + fn modifiers(&self) -> Modifiers { + let mut modifiers = Modifiers::empty(); + + if self.alt_key { + modifiers.insert(Modifiers::ALT); + } + if self.ctrl_key { + modifiers.insert(Modifiers::CONTROL); + } + if self.meta_key { + modifiers.insert(Modifiers::META); + } + if self.shift_key { + modifiers.insert(Modifiers::SHIFT); + } + + modifiers + } +} + +#[cfg(feature = "serialize")] +impl InteractionLocation for SerializedPointInteraction { + fn client_coordinates(&self) -> ClientPoint { + ClientPoint::new(self.client_x.into(), self.client_y.into()) + } + + fn screen_coordinates(&self) -> ScreenPoint { + ScreenPoint::new(self.screen_x.into(), self.screen_y.into()) + } + + fn page_coordinates(&self) -> PagePoint { + PagePoint::new(self.page_x.into(), self.page_y.into()) + } +} + +#[cfg(feature = "serialize")] +impl InteractionElementOffset for SerializedPointInteraction { + fn element_coordinates(&self) -> ElementPoint { + ElementPoint::new(self.offset_x.into(), self.offset_y.into()) + } +} diff --git a/packages/html/src/transit.rs b/packages/html/src/transit.rs index b0fa0d16f..17a8a5f2d 100644 --- a/packages/html/src/transit.rs +++ b/packages/html/src/transit.rs @@ -4,7 +4,8 @@ use crate::events::*; use dioxus_core::ElementId; use serde::{Deserialize, Serialize}; -#[derive(Serialize, Debug, Clone, PartialEq)] +#[cfg(feature = "serialize")] +#[derive(Serialize, Debug, PartialEq)] pub struct HtmlEvent { pub element: ElementId, pub name: String, @@ -12,6 +13,7 @@ pub struct HtmlEvent { pub data: EventData, } +#[cfg(feature = "serialize")] impl<'de> Deserialize<'de> for HtmlEvent { fn deserialize(deserializer: D) -> Result where @@ -41,6 +43,7 @@ impl<'de> Deserialize<'de> for HtmlEvent { } } +#[cfg(feature = "serialize")] fn fun_name( name: &str, data: serde_value::Value, @@ -130,57 +133,90 @@ fn fun_name( Ok(data) } +#[cfg(feature = "serialize")] impl HtmlEvent { pub fn bubbles(&self) -> bool { event_bubbles(&self.name) } } -#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)] +#[derive(Deserialize, Serialize, Debug, PartialEq)] #[serde(untagged)] #[non_exhaustive] pub enum EventData { - Mouse(MouseData), - Clipboard(ClipboardData), - Composition(CompositionData), - Keyboard(KeyboardData), - Focus(FocusData), - Form(FormData), - Drag(DragData), - Pointer(PointerData), - Selection(SelectionData), - Touch(TouchData), - Scroll(ScrollData), - Wheel(WheelData), - Media(MediaData), - Animation(AnimationData), - Transition(TransitionData), - Toggle(ToggleData), - Image(ImageData), + Mouse(SerializedMouseData), + Clipboard(SerializedClipboardData), + Composition(SerializedCompositionData), + Keyboard(SerializedKeyboardData), + Focus(SerializedFocusData), + Form(SerializedFormData), + Drag(SerializedDragData), + Pointer(SerializedPointerData), + Selection(SerializedSelectionData), + Touch(SerializedTouchData), + Scroll(SerializedScrollData), + Wheel(SerializedWheelData), + Media(SerializedMediaData), + Animation(SerializedAnimationData), + Transition(SerializedTransitionData), + Toggle(SerializedToggleData), + Image(SerializedImageData), Mounted, } impl EventData { pub fn into_any(self) -> Rc { match self { - EventData::Mouse(data) => Rc::new(data) as Rc, - EventData::Clipboard(data) => Rc::new(data) as Rc, - EventData::Composition(data) => Rc::new(data) as Rc, - EventData::Keyboard(data) => Rc::new(data) as Rc, - EventData::Focus(data) => Rc::new(data) as Rc, - EventData::Form(data) => Rc::new(data) as Rc, - EventData::Drag(data) => Rc::new(data) as Rc, - EventData::Pointer(data) => Rc::new(data) as Rc, - EventData::Selection(data) => Rc::new(data) as Rc, - EventData::Touch(data) => Rc::new(data) as Rc, - EventData::Scroll(data) => Rc::new(data) as Rc, - EventData::Wheel(data) => Rc::new(data) as Rc, - EventData::Media(data) => Rc::new(data) as Rc, - EventData::Animation(data) => Rc::new(data) as Rc, - EventData::Transition(data) => Rc::new(data) as Rc, - EventData::Toggle(data) => Rc::new(data) as Rc, - EventData::Image(data) => Rc::new(data) as Rc, - EventData::Mounted => Rc::new(MountedData::new(())) as Rc, + EventData::Mouse(data) => { + Rc::new(PlatformEventData::new(Box::new(data))) as Rc + } + EventData::Clipboard(data) => { + Rc::new(PlatformEventData::new(Box::new(data))) as Rc + } + EventData::Composition(data) => { + Rc::new(PlatformEventData::new(Box::new(data))) as Rc + } + EventData::Keyboard(data) => { + Rc::new(PlatformEventData::new(Box::new(data))) as Rc + } + EventData::Focus(data) => { + Rc::new(PlatformEventData::new(Box::new(data))) as Rc + } + EventData::Form(data) => Rc::new(PlatformEventData::new(Box::new(data))) as Rc, + EventData::Drag(data) => Rc::new(PlatformEventData::new(Box::new(data))) as Rc, + EventData::Pointer(data) => { + Rc::new(PlatformEventData::new(Box::new(data))) as Rc + } + EventData::Selection(data) => { + Rc::new(PlatformEventData::new(Box::new(data))) as Rc + } + EventData::Touch(data) => { + Rc::new(PlatformEventData::new(Box::new(data))) as Rc + } + EventData::Scroll(data) => { + Rc::new(PlatformEventData::new(Box::new(data))) as Rc + } + EventData::Wheel(data) => { + Rc::new(PlatformEventData::new(Box::new(data))) as Rc + } + EventData::Media(data) => { + Rc::new(PlatformEventData::new(Box::new(data))) as Rc + } + EventData::Animation(data) => { + Rc::new(PlatformEventData::new(Box::new(data))) as Rc + } + EventData::Transition(data) => { + Rc::new(PlatformEventData::new(Box::new(data))) as Rc + } + EventData::Toggle(data) => { + Rc::new(PlatformEventData::new(Box::new(data))) as Rc + } + EventData::Image(data) => { + Rc::new(PlatformEventData::new(Box::new(data))) as Rc + } + EventData::Mounted => { + Rc::new(PlatformEventData::new(Box::new(MountedData::new(())))) as Rc + } } } } @@ -189,7 +225,7 @@ impl EventData { fn test_back_and_forth() { let data = HtmlEvent { element: ElementId(0), - data: EventData::Mouse(MouseData::default()), + data: EventData::Mouse(SerializedMouseData::default()), name: "click".to_string(), bubbles: true, }; @@ -224,3 +260,148 @@ fn test_back_and_forth() { assert_eq!(data, p); } + +/// A trait for converting from a serialized event to a concrete event type. +pub struct SerializedHtmlEventConverter; + +impl HtmlEventConverter for SerializedHtmlEventConverter { + fn convert_animation_data(&self, event: &PlatformEventData) -> AnimationData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_clipboard_data(&self, event: &PlatformEventData) -> ClipboardData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_composition_data(&self, event: &PlatformEventData) -> CompositionData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_drag_data(&self, event: &PlatformEventData) -> DragData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_focus_data(&self, event: &PlatformEventData) -> FocusData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_form_data(&self, event: &PlatformEventData) -> FormData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_image_data(&self, event: &PlatformEventData) -> ImageData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_keyboard_data(&self, event: &PlatformEventData) -> KeyboardData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_media_data(&self, event: &PlatformEventData) -> MediaData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_mounted_data(&self, _: &PlatformEventData) -> MountedData { + MountedData::from(()) + } + + fn convert_mouse_data(&self, event: &PlatformEventData) -> MouseData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_pointer_data(&self, event: &PlatformEventData) -> PointerData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_scroll_data(&self, event: &PlatformEventData) -> ScrollData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_selection_data(&self, event: &PlatformEventData) -> SelectionData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_toggle_data(&self, event: &PlatformEventData) -> ToggleData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_touch_data(&self, event: &PlatformEventData) -> TouchData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_transition_data(&self, event: &PlatformEventData) -> TransitionData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_wheel_data(&self, event: &PlatformEventData) -> WheelData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } +} diff --git a/packages/html/src/web_sys_bind/events.rs b/packages/html/src/web_sys_bind/events.rs index e5a6c0d78..b1e6c5341 100644 --- a/packages/html/src/web_sys_bind/events.rs +++ b/packages/html/src/web_sys_bind/events.rs @@ -1,17 +1,17 @@ +use crate::events::HasKeyboardData; use crate::events::{ AnimationData, CompositionData, KeyboardData, MouseData, PointerData, TouchData, TransitionData, WheelData, }; -use crate::geometry::{ClientPoint, Coordinates, ElementPoint, PagePoint, ScreenPoint}; +use crate::geometry::{ClientPoint, ElementPoint, PagePoint, ScreenPoint}; use crate::input_data::{decode_key_location, decode_mouse_button_set, MouseButton}; -use crate::{DragData, MountedData}; +use crate::prelude::*; use keyboard_types::{Code, Key, Modifiers}; -use std::convert::TryInto; use std::str::FromStr; use wasm_bindgen::{JsCast, JsValue}; use web_sys::{ - AnimationEvent, CompositionEvent, Event, KeyboardEvent, MouseEvent, PointerEvent, TouchEvent, - TransitionEvent, WheelEvent, + AnimationEvent, CompositionEvent, Event, KeyboardEvent, MouseEvent, PointerEvent, Touch, + TouchEvent, TransitionEvent, WheelEvent, }; macro_rules! uncheck_convert { @@ -20,7 +20,7 @@ macro_rules! uncheck_convert { #[inline] fn from(e: Event) -> Self { let e: $t = e.unchecked_into(); - Self::from(&e) + Self::from(e) } } @@ -28,7 +28,7 @@ macro_rules! uncheck_convert { #[inline] fn from(e: &Event) -> Self { let e: &$t = e.unchecked_ref(); - Self::from(e) + Self::from(e.clone()) } } }; @@ -38,159 +38,368 @@ macro_rules! uncheck_convert { } uncheck_convert![ - CompositionEvent => CompositionData, - KeyboardEvent => KeyboardData, - MouseEvent => MouseData, - MouseEvent => DragData, - TouchEvent => TouchData, - PointerEvent => PointerData, - WheelEvent => WheelData, - AnimationEvent => AnimationData, - TransitionEvent => TransitionData, + web_sys::CompositionEvent => CompositionData, + web_sys::KeyboardEvent => KeyboardData, + web_sys::MouseEvent => MouseData, + web_sys::TouchEvent => TouchData, + web_sys::PointerEvent => PointerData, + web_sys::WheelEvent => WheelData, + web_sys::AnimationEvent => AnimationData, + web_sys::TransitionEvent => TransitionData, + web_sys::MouseEvent => DragData, + web_sys::FocusEvent => FocusData, ]; -impl From<&CompositionEvent> for CompositionData { - fn from(e: &CompositionEvent) -> Self { - Self { - data: e.data().unwrap_or_default(), - } +impl HasCompositionData for CompositionEvent { + fn data(&self) -> std::string::String { + self.data().unwrap_or_default() + } + + fn as_any(&self) -> &dyn std::any::Any { + self } } -impl From<&KeyboardEvent> for KeyboardData { - fn from(e: &KeyboardEvent) -> Self { +impl HasKeyboardData for KeyboardEvent { + fn key(&self) -> Key { + Key::from_str(self.key().as_str()).unwrap_or(Key::Unidentified) + } + + fn code(&self) -> Code { + Code::from_str(self.code().as_str()).unwrap_or(Code::Unidentified) + } + + fn location(&self) -> keyboard_types::Location { + decode_key_location(self.location() as usize) + } + + fn is_auto_repeating(&self) -> bool { + self.repeat() + } + + fn as_any(&self) -> &dyn std::any::Any { + self + } +} + +impl ModifiersInteraction for KeyboardEvent { + fn modifiers(&self) -> Modifiers { let mut modifiers = Modifiers::empty(); - if e.alt_key() { + if self.alt_key() { modifiers.insert(Modifiers::ALT); } - if e.ctrl_key() { + if self.ctrl_key() { modifiers.insert(Modifiers::CONTROL); } - if e.meta_key() { + if self.meta_key() { modifiers.insert(Modifiers::META); } - if e.shift_key() { + if self.shift_key() { modifiers.insert(Modifiers::SHIFT); } - Self::new( - Key::from_str(&e.key()).expect("could not parse key"), - Code::from_str(&e.code()).expect("could not parse code"), - decode_key_location( - e.location() - .try_into() - .expect("could not convert location to u32"), - ), - e.repeat(), - modifiers, - ) + modifiers } } -impl From<&MouseEvent> for MouseData { - fn from(e: &MouseEvent) -> Self { +impl HasDragData for MouseEvent {} + +impl InteractionLocation for MouseEvent { + fn client_coordinates(&self) -> ClientPoint { + ClientPoint::new(self.client_x().into(), self.client_y().into()) + } + + fn page_coordinates(&self) -> PagePoint { + PagePoint::new(self.page_x().into(), self.page_y().into()) + } + + fn screen_coordinates(&self) -> ScreenPoint { + ScreenPoint::new(self.screen_x().into(), self.screen_y().into()) + } +} + +impl InteractionElementOffset for MouseEvent { + fn element_coordinates(&self) -> ElementPoint { + ElementPoint::new(self.offset_x().into(), self.offset_y().into()) + } +} + +impl ModifiersInteraction for MouseEvent { + fn modifiers(&self) -> Modifiers { let mut modifiers = Modifiers::empty(); - if e.alt_key() { + if self.alt_key() { modifiers.insert(Modifiers::ALT); } - if e.ctrl_key() { + if self.ctrl_key() { modifiers.insert(Modifiers::CONTROL); } - if e.meta_key() { + if self.meta_key() { modifiers.insert(Modifiers::META); } - if e.shift_key() { + if self.shift_key() { modifiers.insert(Modifiers::SHIFT); } - MouseData::new( - Coordinates::new( - ScreenPoint::new(e.screen_x().into(), e.screen_y().into()), - ClientPoint::new(e.client_x().into(), e.client_y().into()), - ElementPoint::new(e.offset_x().into(), e.offset_y().into()), - PagePoint::new(e.page_x().into(), e.page_y().into()), - ), - Some(MouseButton::from_web_code(e.button())), - decode_mouse_button_set(e.buttons()), - modifiers, + modifiers + } +} + +impl PointerInteraction for MouseEvent { + fn held_buttons(&self) -> crate::input_data::MouseButtonSet { + decode_mouse_button_set(self.buttons()) + } + + fn trigger_button(&self) -> Option { + Some(MouseButton::from_web_code(self.button())) + } +} + +impl HasMouseData for MouseEvent { + fn as_any(&self) -> &dyn std::any::Any { + self + } +} + +impl ModifiersInteraction for TouchEvent { + fn modifiers(&self) -> Modifiers { + let mut modifiers = Modifiers::empty(); + + if self.alt_key() { + modifiers.insert(Modifiers::ALT); + } + if self.ctrl_key() { + modifiers.insert(Modifiers::CONTROL); + } + if self.meta_key() { + modifiers.insert(Modifiers::META); + } + if self.shift_key() { + modifiers.insert(Modifiers::SHIFT); + } + + modifiers + } +} + +impl crate::events::HasTouchData for TouchEvent { + fn touches(&self) -> Vec { + let touches = TouchEvent::touches(self); + let mut result = Vec::with_capacity(touches.length() as usize); + for i in 0..touches.length() { + let touch = touches.get(i).unwrap(); + result.push(TouchPoint::new(touch)); + } + result + } + + fn touches_changed(&self) -> Vec { + let touches = self.changed_touches(); + let mut result = Vec::with_capacity(touches.length() as usize); + for i in 0..touches.length() { + let touch = touches.get(i).unwrap(); + result.push(TouchPoint::new(touch)); + } + result + } + + fn target_touches(&self) -> Vec { + let touches = self.target_touches(); + let mut result = Vec::with_capacity(touches.length() as usize); + for i in 0..touches.length() { + let touch = touches.get(i).unwrap(); + result.push(TouchPoint::new(touch)); + } + result + } + + fn as_any(&self) -> &dyn std::any::Any { + self + } +} + +impl HasTouchPointData for Touch { + fn identifier(&self) -> i32 { + self.identifier() + } + + fn radius(&self) -> ScreenPoint { + ScreenPoint::new(self.radius_x().into(), self.radius_y().into()) + } + + fn rotation(&self) -> f64 { + self.rotation_angle() as f64 + } + + fn force(&self) -> f64 { + self.force() as f64 + } + + fn as_any(&self) -> &dyn std::any::Any { + self + } +} + +impl InteractionLocation for Touch { + fn client_coordinates(&self) -> ClientPoint { + ClientPoint::new(self.client_x().into(), self.client_y().into()) + } + + fn screen_coordinates(&self) -> ScreenPoint { + ScreenPoint::new(self.screen_x().into(), self.screen_y().into()) + } + + fn page_coordinates(&self) -> PagePoint { + PagePoint::new(self.page_x().into(), self.page_y().into()) + } +} + +impl HasPointerData for PointerEvent { + fn pointer_id(&self) -> i32 { + self.pointer_id() + } + + fn width(&self) -> i32 { + self.width() + } + + fn height(&self) -> i32 { + self.height() + } + + fn pressure(&self) -> f32 { + self.pressure() + } + + fn tangential_pressure(&self) -> f32 { + self.tangential_pressure() + } + + fn tilt_x(&self) -> i32 { + self.tilt_x() + } + + fn tilt_y(&self) -> i32 { + self.tilt_y() + } + + fn twist(&self) -> i32 { + self.twist() + } + + fn pointer_type(&self) -> String { + self.pointer_type() + } + + fn is_primary(&self) -> bool { + self.is_primary() + } + + fn as_any(&self) -> &dyn std::any::Any { + self + } +} + +impl InteractionLocation for PointerEvent { + fn client_coordinates(&self) -> ClientPoint { + ClientPoint::new(self.client_x().into(), self.client_y().into()) + } + + fn screen_coordinates(&self) -> ScreenPoint { + ScreenPoint::new(self.screen_x().into(), self.screen_y().into()) + } + + fn page_coordinates(&self) -> PagePoint { + PagePoint::new(self.page_x().into(), self.page_y().into()) + } +} + +impl InteractionElementOffset for PointerEvent { + fn element_coordinates(&self) -> ElementPoint { + ElementPoint::new(self.offset_x().into(), self.offset_y().into()) + } +} + +impl ModifiersInteraction for PointerEvent { + fn modifiers(&self) -> Modifiers { + let mut modifiers = Modifiers::empty(); + + if self.alt_key() { + modifiers.insert(Modifiers::ALT); + } + if self.ctrl_key() { + modifiers.insert(Modifiers::CONTROL); + } + if self.meta_key() { + modifiers.insert(Modifiers::META); + } + if self.shift_key() { + modifiers.insert(Modifiers::SHIFT); + } + + modifiers + } +} + +impl PointerInteraction for PointerEvent { + fn held_buttons(&self) -> crate::input_data::MouseButtonSet { + decode_mouse_button_set(self.buttons()) + } + + fn trigger_button(&self) -> Option { + Some(MouseButton::from_web_code(self.button())) + } +} + +impl HasWheelData for WheelEvent { + fn delta(&self) -> crate::geometry::WheelDelta { + crate::geometry::WheelDelta::from_web_attributes( + self.delta_mode(), + self.delta_x(), + self.delta_y(), + self.delta_z(), ) } -} -impl From<&MouseEvent> for DragData { - fn from(value: &MouseEvent) -> Self { - Self { - mouse: MouseData::from(value), - } + fn as_any(&self) -> &dyn std::any::Any { + self } } -impl From<&TouchEvent> for TouchData { - fn from(e: &TouchEvent) -> Self { - Self { - alt_key: e.alt_key(), - ctrl_key: e.ctrl_key(), - meta_key: e.meta_key(), - shift_key: e.shift_key(), - } +impl HasAnimationData for AnimationEvent { + fn animation_name(&self) -> String { + self.animation_name() + } + + fn pseudo_element(&self) -> String { + self.pseudo_element() + } + + fn elapsed_time(&self) -> f32 { + self.elapsed_time() + } + + fn as_any(&self) -> &dyn std::any::Any { + self } } -impl From<&PointerEvent> for PointerData { - fn from(e: &PointerEvent) -> Self { - Self { - alt_key: e.alt_key(), - button: e.button(), - buttons: e.buttons(), - client_x: e.client_x(), - client_y: e.client_y(), - ctrl_key: e.ctrl_key(), - meta_key: e.meta_key(), - page_x: e.page_x(), - page_y: e.page_y(), - screen_x: e.screen_x(), - screen_y: e.screen_y(), - shift_key: e.shift_key(), - pointer_id: e.pointer_id(), - width: e.width(), - height: e.height(), - pressure: e.pressure(), - tangential_pressure: e.tangential_pressure(), - tilt_x: e.tilt_x(), - tilt_y: e.tilt_y(), - twist: e.twist(), - pointer_type: e.pointer_type(), - is_primary: e.is_primary(), - // get_modifier_state: evt.get_modifier_state(), - } +impl HasTransitionData for TransitionEvent { + fn elapsed_time(&self) -> f32 { + self.elapsed_time() } -} -impl From<&WheelEvent> for WheelData { - fn from(e: &WheelEvent) -> Self { - WheelData::from_web_attributes(e.delta_mode(), e.delta_x(), e.delta_y(), e.delta_z()) + fn property_name(&self) -> String { + self.property_name() } -} -impl From<&AnimationEvent> for AnimationData { - fn from(e: &AnimationEvent) -> Self { - Self { - elapsed_time: e.elapsed_time(), - animation_name: e.animation_name(), - pseudo_element: e.pseudo_element(), - } + fn pseudo_element(&self) -> String { + self.pseudo_element() } -} -impl From<&TransitionEvent> for TransitionData { - fn from(e: &TransitionEvent) -> Self { - Self { - elapsed_time: e.elapsed_time(), - property_name: e.property_name(), - pseudo_element: e.pseudo_element(), - } + fn as_any(&self) -> &dyn std::any::Any { + self } } @@ -216,8 +425,8 @@ impl crate::RenderedElementBacking for web_sys::Element { Box::pin(async { result }) } - fn get_raw_element(&self) -> crate::MountedResult<&dyn std::any::Any> { - Ok(self) + fn as_any(&self) -> &dyn std::any::Any { + self } fn scroll_to( @@ -261,3 +470,45 @@ impl std::fmt::Display for FocusError { } impl std::error::Error for FocusError {} + +impl HasScrollData for Event { + fn as_any(&self) -> &dyn std::any::Any { + self + } +} + +impl HasClipboardData for Event { + fn as_any(&self) -> &dyn std::any::Any { + self + } +} + +impl From<&Event> for ClipboardData { + fn from(e: &Event) -> Self { + ClipboardData::new(e.clone()) + } +} + +impl HasFocusData for web_sys::FocusEvent { + fn as_any(&self) -> &dyn std::any::Any { + self + } +} + +impl HasToggleData for web_sys::Event { + fn as_any(&self) -> &dyn std::any::Any { + self + } +} + +impl HasSelectionData for web_sys::Event { + fn as_any(&self) -> &dyn std::any::Any { + self + } +} + +impl HasMediaData for web_sys::Event { + fn as_any(&self) -> &dyn std::any::Any { + self + } +} diff --git a/packages/liveview/Cargo.toml b/packages/liveview/Cargo.toml index 31e5a2bda..2ed7894c7 100644 --- a/packages/liveview/Cargo.toml +++ b/packages/liveview/Cargo.toml @@ -22,8 +22,8 @@ tokio-stream = { version = "0.1.11", features = ["net"] } tokio-util = { version = "0.7.4", features = ["rt"] } serde = { version = "1.0.151", features = ["derive"] } serde_json = "1.0.91" +dioxus-html = { workspace = true, features = ["serialize", "eval", "mounted"] } rustc-hash = { workspace = true } -dioxus-html = { workspace = true, features = ["serialize"] } dioxus-core = { workspace = true, features = ["serialize"] } dioxus-interpreter-js = { workspace = true, features = ["binary-protocol"] } dioxus-hot-reload = { workspace = true, optional = true } diff --git a/packages/liveview/src/element.rs b/packages/liveview/src/element.rs index 600f7f20d..d1a66efa2 100644 --- a/packages/liveview/src/element.rs +++ b/packages/liveview/src/element.rs @@ -4,6 +4,7 @@ use dioxus_html::{geometry::euclid::Rect, MountedResult, RenderedElementBacking} use crate::query::QueryEngine; /// A mounted element passed to onmounted events +#[derive(Clone)] pub struct LiveviewElement { id: ElementId, query: QueryEngine, @@ -16,8 +17,8 @@ impl LiveviewElement { } impl RenderedElementBacking for LiveviewElement { - fn get_raw_element(&self) -> dioxus_html::MountedResult<&dyn std::any::Any> { - Ok(self) + fn as_any(&self) -> &dyn std::any::Any { + self } fn get_client_rect( diff --git a/packages/liveview/src/events.rs b/packages/liveview/src/events.rs new file mode 100644 index 000000000..b764bf7cc --- /dev/null +++ b/packages/liveview/src/events.rs @@ -0,0 +1,149 @@ +//! Convert a serialized event to an event trigger + +use dioxus_html::*; + +use crate::element::LiveviewElement; + +pub(crate) struct SerializedHtmlEventConverter; + +impl HtmlEventConverter for SerializedHtmlEventConverter { + fn convert_animation_data(&self, event: &PlatformEventData) -> AnimationData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_clipboard_data(&self, event: &PlatformEventData) -> ClipboardData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_composition_data(&self, event: &PlatformEventData) -> CompositionData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_drag_data(&self, event: &PlatformEventData) -> DragData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_focus_data(&self, event: &PlatformEventData) -> FocusData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_form_data(&self, event: &PlatformEventData) -> FormData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_image_data(&self, event: &PlatformEventData) -> ImageData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_keyboard_data(&self, event: &PlatformEventData) -> KeyboardData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_media_data(&self, event: &PlatformEventData) -> MediaData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_mounted_data(&self, event: &PlatformEventData) -> MountedData { + event.downcast::().cloned().unwrap().into() + } + + fn convert_mouse_data(&self, event: &PlatformEventData) -> MouseData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_pointer_data(&self, event: &PlatformEventData) -> PointerData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_scroll_data(&self, event: &PlatformEventData) -> ScrollData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_selection_data(&self, event: &PlatformEventData) -> SelectionData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_toggle_data(&self, event: &PlatformEventData) -> ToggleData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_touch_data(&self, event: &PlatformEventData) -> TouchData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_transition_data(&self, event: &PlatformEventData) -> TransitionData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_wheel_data(&self, event: &PlatformEventData) -> WheelData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } +} diff --git a/packages/liveview/src/lib.rs b/packages/liveview/src/lib.rs index c2ca679ce..a8bc83622 100644 --- a/packages/liveview/src/lib.rs +++ b/packages/liveview/src/lib.rs @@ -33,6 +33,7 @@ mod query; use futures_util::{SinkExt, StreamExt}; pub use pool::*; mod eval; +mod events; pub trait WebsocketTx: SinkExt {} impl WebsocketTx for T where T: SinkExt {} diff --git a/packages/liveview/src/pool.rs b/packages/liveview/src/pool.rs index eaf699248..6fac0abfe 100644 --- a/packages/liveview/src/pool.rs +++ b/packages/liveview/src/pool.rs @@ -1,11 +1,12 @@ use crate::{ element::LiveviewElement, eval::init_eval, + events::SerializedHtmlEventConverter, query::{QueryEngine, QueryResult}, LiveViewError, }; use dioxus_core::{prelude::*, BorrowedAttributeValue, Mutations}; -use dioxus_html::{event_bubbles, EventData, HtmlEvent, MountedData}; +use dioxus_html::{event_bubbles, EventData, HtmlEvent, MountedData, PlatformEventData}; use dioxus_interpreter_js::binary_protocol::Channel; use futures_util::{pin_mut, SinkExt, StreamExt}; use rustc_hash::FxHashMap; @@ -26,6 +27,9 @@ impl Default for LiveViewPool { impl LiveViewPool { pub fn new() -> Self { + // Set the event converter + dioxus_html::set_event_converter(Box::new(SerializedHtmlEventConverter)); + LiveViewPool { pool: LocalPoolHandle::new(16), } @@ -184,12 +188,11 @@ pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), Li let element = LiveviewElement::new(evt.element, query_engine.clone()); vdom.handle_event( &evt.name, - Rc::new(MountedData::new(element)), + Rc::new(PlatformEventData::new(Box::new(MountedData::new(element)))), evt.element, evt.bubbles, ); - } - else{ + } else { vdom.handle_event( &evt.name, evt.data.into_any(), diff --git a/packages/rink/Cargo.toml b/packages/rink/Cargo.toml index 92d1eae8c..28440a932 100644 --- a/packages/rink/Cargo.toml +++ b/packages/rink/Cargo.toml @@ -10,7 +10,7 @@ keywords = ["dom", "ui", "gui", "react", "terminal"] license = "MIT OR Apache-2.0" [dependencies] -dioxus-html = { workspace = true } +dioxus-html = { workspace = true, features = ["serialize", "mounted"] } dioxus-native-core = { workspace = true, features = ["layout-attributes"] } dioxus-native-core-macro = { workspace = true } diff --git a/packages/rink/examples/widgets.rs b/packages/rink/examples/widgets.rs index e1f71fe42..42dc3723d 100644 --- a/packages/rink/examples/widgets.rs +++ b/packages/rink/examples/widgets.rs @@ -1,3 +1,4 @@ +use dioxus_html::HasFormData; use dioxus_native_core::{ prelude::*, real_dom::{NodeImmutable, NodeTypeMut}, @@ -79,7 +80,7 @@ impl Driver for Counter { if event_type == "input" { // when the button is clicked, increment the counter if let EventData::Form(input_event) = &*event { - if let Ok(value) = input_event.value.parse::() { + if let Ok(value) = input_event.value().parse::() { self.count = value; } } diff --git a/packages/rink/src/hooks.rs b/packages/rink/src/hooks.rs index f8c4500d2..1ffa361f7 100644 --- a/packages/rink/src/hooks.rs +++ b/packages/rink/src/hooks.rs @@ -2,6 +2,10 @@ use crossterm::event::{ Event as TermEvent, KeyCode as TermKeyCode, KeyModifiers, ModifierKeyCode, MouseButton, MouseEventKind, }; +use dioxus_html::{ + HasFormData, HasKeyboardData, HasWheelData, SerializedFocusData, SerializedKeyboardData, + SerializedMouseData, SerializedWheelData, +}; use dioxus_native_core::prelude::*; use dioxus_native_core::real_dom::NodeImmutable; use rustc_hash::{FxHashMap, FxHashSet}; @@ -11,9 +15,10 @@ use dioxus_html::geometry::{ ClientPoint, Coordinates, ElementPoint, PagePoint, ScreenPoint, WheelDelta, }; use dioxus_html::input_data::keyboard_types::{Code, Key, Location, Modifiers}; -use dioxus_html::input_data::MouseButtonSet as DioxusMouseButtons; -use dioxus_html::input_data::{MouseButton as DioxusMouseButton, MouseButtonSet}; -use dioxus_html::{event_bubbles, FocusData, KeyboardData, MouseData, WheelData}; +use dioxus_html::input_data::{ + MouseButton as DioxusMouseButton, MouseButtonSet as DioxusMouseButtons, +}; +use dioxus_html::{event_bubbles, prelude::*}; use std::any::Any; use std::collections::HashMap; use std::{ @@ -38,10 +43,10 @@ pub struct Event { #[derive(Debug, Clone, PartialEq)] pub enum EventData { - Mouse(MouseData), - Keyboard(KeyboardData), - Focus(FocusData), - Wheel(WheelData), + Mouse(SerializedMouseData), + Keyboard(SerializedKeyboardData), + Focus(SerializedFocusData), + Wheel(SerializedWheelData), Form(FormData), } @@ -52,27 +57,31 @@ impl EventData { EventData::Keyboard(k) => Rc::new(k), EventData::Focus(f) => Rc::new(f), EventData::Wheel(w) => Rc::new(w), - EventData::Form(f) => Rc::new(f.into_html()), + EventData::Form(f) => Rc::new(f), } } } #[derive(Clone, Debug, PartialEq)] pub struct FormData { - pub value: String, + pub(crate) value: String, - pub values: HashMap>, + pub(crate) values: HashMap>, - pub files: Option, + pub(crate) files: Option, } -impl FormData { - fn into_html(self) -> dioxus_html::FormData { - dioxus_html::FormData { - value: self.value, - values: self.values, - files: None, - } +impl HasFormData for FormData { + fn value(&self) -> String { + self.value.clone() + } + + fn values(&self) -> HashMap> { + self.values.clone() + } + + fn as_any(&self) -> &dyn std::any::Any { + self } } @@ -89,9 +98,9 @@ type EventCore = (&'static str, EventData); const MAX_REPEAT_TIME: Duration = Duration::from_millis(100); pub struct InnerInputState { - mouse: Option, - wheel: Option, - last_key_pressed: Option<(KeyboardData, Instant)>, + mouse: Option, + wheel: Option, + last_key_pressed: Option<(SerializedKeyboardData, Instant)>, pub(crate) focus_state: FocusState, // subscribers: Vec>, } @@ -114,7 +123,7 @@ impl InnerInputState { EventData::Mouse(ref mut m) => { let mut held_buttons = match &self.mouse { Some(previous_data) => previous_data.held_buttons(), - None => MouseButtonSet::empty(), + None => DioxusMouseButtons::empty(), }; match evt.0 { @@ -133,10 +142,16 @@ impl InnerInputState { _ => {} } - let new_mouse_data = MouseData::new( - m.coordinates(), + let coordinates = m.coordinates(); + let new_mouse_data = SerializedMouseData::new( m.trigger_button(), held_buttons, + Coordinates::new( + m.screen_coordinates(), + m.client_coordinates(), + coordinates.element(), + m.page_coordinates(), + ), m.modifiers(), ); @@ -155,7 +170,13 @@ impl InnerInputState { .is_some(); if is_repeating { - *k = KeyboardData::new(k.key(), k.code(), k.location(), true, k.modifiers()); + *k = SerializedKeyboardData::new( + k.key(), + k.code(), + k.location(), + is_repeating, + k.modifiers(), + ); } self.last_key_pressed = Some((k.clone(), Instant::now())); @@ -199,13 +220,13 @@ impl InnerInputState { resolved_events.push(Event { name: "focus", id, - data: EventData::Focus(FocusData {}), + data: EventData::Focus(SerializedFocusData::default()), bubbles: event_bubbles("focus"), }); resolved_events.push(Event { name: "focusin", id, - data: EventData::Focus(FocusData {}), + data: EventData::Focus(SerializedFocusData::default()), bubbles: event_bubbles("focusin"), }); } @@ -213,7 +234,7 @@ impl InnerInputState { resolved_events.push(Event { name: "focusout", id, - data: EventData::Focus(FocusData {}), + data: EventData::Focus(SerializedFocusData::default()), bubbles: event_bubbles("focusout"), }); } @@ -226,7 +247,7 @@ impl InnerInputState { fn resolve_mouse_events( &mut self, - previous_mouse: Option, + previous_mouse: Option, resolved_events: &mut Vec, layout: &Taffy, dom: &mut RealDom, @@ -273,7 +294,10 @@ impl InnerInputState { } } - fn prepare_mouse_data(mouse_data: &MouseData, layout: &Layout) -> MouseData { + fn prepare_mouse_data( + mouse_data: &SerializedMouseData, + layout: &Layout, + ) -> SerializedMouseData { let Point { x, y } = layout.location; let node_origin = ClientPoint::new( layout_to_screen_space(x).into(), @@ -284,17 +308,15 @@ impl InnerInputState { .to_point() .cast_unit(); - let coordinates = Coordinates::new( - mouse_data.screen_coordinates(), - mouse_data.client_coordinates(), - new_client_coordinates, - mouse_data.page_coordinates(), - ); - - MouseData::new( - coordinates, + SerializedMouseData::new( mouse_data.trigger_button(), mouse_data.held_buttons(), + Coordinates::new( + mouse_data.screen_coordinates(), + mouse_data.client_coordinates(), + new_client_coordinates, + mouse_data.page_coordinates(), + ), mouse_data.modifiers(), ) } @@ -305,7 +327,7 @@ impl InnerInputState { // a mouse button is pressed if a button was not down and is now down let previous_buttons = previous_mouse - .map_or(MouseButtonSet::empty(), |previous_data| { + .map_or(DioxusMouseButtons::empty(), |previous_data| { previous_data.held_buttons() }); let was_pressed = !(mouse_data.held_buttons() - previous_buttons).is_empty(); @@ -689,17 +711,6 @@ fn get_event(evt: TermEvent) -> Option<(&'static str, EventData)> { // from https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent - // The `page` and `screen` coordinates are inconsistent with the MDN definition, as they are relative to the viewport (client), not the target element/page/screen, respectively. - // todo? - // But then, MDN defines them in terms of pixels, yet crossterm provides only row/column, and it might not be possible to get pixels. So we can't get 100% consistency anyway. - let coordinates = Coordinates::new( - ScreenPoint::new(x, y), - ClientPoint::new(x, y), - // offset x/y are set when the origin of the event is assigned to an element - ElementPoint::new(0., 0.), - PagePoint::new(x, y), - ); - let mut modifiers = Modifiers::empty(); if shift { modifiers.insert(Modifiers::SHIFT); @@ -715,17 +726,26 @@ fn get_event(evt: TermEvent) -> Option<(&'static str, EventData)> { } // held mouse buttons get set later by maintaining state, as crossterm does not provide them - EventData::Mouse(MouseData::new( - coordinates, + EventData::Mouse(SerializedMouseData::new( button, DioxusMouseButtons::empty(), + Coordinates::new( + // The `page` and `screen` coordinates are inconsistent with the MDN definition, as they are relative to the viewport (client), not the target element/page/screen, respectively. + // todo? + // But then, MDN defines them in terms of pixels, yet crossterm provides only row/column, and it might not be possible to get pixels. So we can't get 100% consistency anyway. + ScreenPoint::new(x, y), + ClientPoint::new(x, y), + // offset x/y are set when the origin of the event is assigned to an element + ElementPoint::new(0., 0.), + PagePoint::new(x, y), + ), modifiers, )) }; let get_wheel_data = |up| { let y = if up { -1.0 } else { 1.0 }; - EventData::Wheel(WheelData::new(WheelDelta::lines(0., y, 0.))) + EventData::Wheel(SerializedWheelData::new(WheelDelta::lines(0., y, 0.))) }; match m.kind { @@ -750,7 +770,7 @@ fn translate_key_event(event: crossterm::event::KeyEvent) -> Option { let code = guess_code_from_crossterm_key_code(event.code)?; let modifiers = modifiers_from_crossterm_modifiers(event.modifiers); - Some(EventData::Keyboard(KeyboardData::new( + Some(EventData::Keyboard(SerializedKeyboardData::new( key, code, Location::Standard, diff --git a/packages/rink/src/widgets/button.rs b/packages/rink/src/widgets/button.rs index 523b2392a..25f0e07ef 100644 --- a/packages/rink/src/widgets/button.rs +++ b/packages/rink/src/widgets/button.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; -use dioxus_html::input_data::keyboard_types::Key; +use dioxus_html::{input_data::keyboard_types::Key, HasKeyboardData}; use dioxus_native_core::{ custom_element::CustomElement, node::OwnedAttributeDiscription, @@ -11,7 +11,7 @@ use dioxus_native_core::{ }; use shipyard::UniqueView; -use crate::FormData; +use crate::hooks::FormData; use super::{RinkWidget, WidgetContext}; diff --git a/packages/rink/src/widgets/checkbox.rs b/packages/rink/src/widgets/checkbox.rs index fab809ed2..a8a8253a0 100644 --- a/packages/rink/src/widgets/checkbox.rs +++ b/packages/rink/src/widgets/checkbox.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; -use dioxus_html::input_data::keyboard_types::Key; +use dioxus_html::{input_data::keyboard_types::Key, HasKeyboardData}; use dioxus_native_core::{ custom_element::CustomElement, node::OwnedAttributeDiscription, @@ -11,7 +11,7 @@ use dioxus_native_core::{ }; use shipyard::UniqueView; -use crate::FormData; +use crate::hooks::FormData; use super::{RinkWidget, WidgetContext}; diff --git a/packages/rink/src/widgets/number.rs b/packages/rink/src/widgets/number.rs index 371c8b41d..c2a71ceac 100644 --- a/packages/rink/src/widgets/number.rs +++ b/packages/rink/src/widgets/number.rs @@ -1,4 +1,4 @@ -use dioxus_html::input_data::keyboard_types::Key; +use dioxus_html::{input_data::keyboard_types::Key, HasKeyboardData}; use dioxus_native_core::{ custom_element::CustomElement, real_dom::{NodeImmutable, RealDom}, diff --git a/packages/rink/src/widgets/slider.rs b/packages/rink/src/widgets/slider.rs index 7080b6c12..bb0639610 100644 --- a/packages/rink/src/widgets/slider.rs +++ b/packages/rink/src/widgets/slider.rs @@ -1,6 +1,9 @@ use std::collections::HashMap; -use dioxus_html::{input_data::keyboard_types::Key, KeyboardData, MouseData}; +use dioxus_html::{ + input_data::keyboard_types::Key, prelude::*, HasKeyboardData, SerializedKeyboardData, + SerializedMouseData, +}; use dioxus_native_core::{ custom_element::CustomElement, node::{OwnedAttributeDiscription, OwnedAttributeValue}, @@ -12,7 +15,8 @@ use dioxus_native_core::{ use shipyard::UniqueView; use super::{RinkWidget, WidgetContext}; -use crate::{query::get_layout, Event, EventData, FormData, Query}; +use crate::hooks::FormData; +use crate::{query::get_layout, Event, EventData, Query}; #[derive(Debug)] pub(crate) struct Slider { @@ -209,7 +213,7 @@ impl Slider { } } - fn handle_keydown(&mut self, mut root: NodeMut, data: &KeyboardData) { + fn handle_keydown(&mut self, mut root: NodeMut, data: &SerializedKeyboardData) { let key = data.key(); let step = self.step(); @@ -231,7 +235,7 @@ impl Slider { self.write_value(rdom, id); } - fn handle_mousemove(&mut self, mut root: NodeMut, data: &MouseData) { + fn handle_mousemove(&mut self, mut root: NodeMut, data: &SerializedMouseData) { if !data.held_buttons().is_empty() { let id = root.id(); let rdom = root.real_dom_mut(); diff --git a/packages/rink/src/widgets/text_like.rs b/packages/rink/src/widgets/text_like.rs index 6521073d0..770dd9d33 100644 --- a/packages/rink/src/widgets/text_like.rs +++ b/packages/rink/src/widgets/text_like.rs @@ -1,7 +1,10 @@ use std::{collections::HashMap, io::stdout}; use crossterm::{cursor::MoveTo, execute}; -use dioxus_html::{input_data::keyboard_types::Key, KeyboardData, MouseData}; +use dioxus_html::{ + input_data::keyboard_types::Key, prelude::*, HasKeyboardData, SerializedKeyboardData, + SerializedMouseData, +}; use dioxus_native_core::{ custom_element::CustomElement, node::OwnedAttributeDiscription, @@ -14,7 +17,8 @@ use dioxus_native_core::{ use shipyard::UniqueView; use taffy::geometry::Point; -use crate::{query::get_layout, Event, EventData, FormData, Query}; +use crate::hooks::FormData; +use crate::{query::get_layout, Event, EventData, Query}; use super::{RinkWidget, WidgetContext}; @@ -184,7 +188,7 @@ impl TextLike { } } - fn handle_keydown(&mut self, mut root: NodeMut, data: &KeyboardData) { + fn handle_keydown(&mut self, mut root: NodeMut, data: &SerializedKeyboardData) { let key = data.key(); let modifiers = data.modifiers(); let code = data.code(); @@ -228,7 +232,7 @@ impl TextLike { } } - fn handle_mousemove(&mut self, mut root: NodeMut, data: &MouseData) { + fn handle_mousemove(&mut self, mut root: NodeMut, data: &SerializedMouseData) { if self.dragging { let id = root.id(); let offset = data.element_coordinates(); @@ -245,7 +249,7 @@ impl TextLike { } } - fn handle_mousedown(&mut self, mut root: NodeMut, data: &MouseData) { + fn handle_mousedown(&mut self, mut root: NodeMut, data: &SerializedMouseData) { let offset = data.element_coordinates(); let mut new = Pos::new(offset.x as usize, offset.y as usize); diff --git a/packages/router-macro/src/lib.rs b/packages/router-macro/src/lib.rs index 8844c6a35..024570ae1 100644 --- a/packages/router-macro/src/lib.rs +++ b/packages/router-macro/src/lib.rs @@ -36,7 +36,7 @@ mod segment; /// 1. Static Segments: "/static" /// 2. Dynamic Segments: "/:dynamic" (where dynamic has a type that is FromStr in all child Variants) /// 3. Catch all Segments: "/:..segments" (where segments has a type that is FromSegments in all child Variants) -/// 4. Query Segments: "/?:query" (where query has a type that is FromQuery in all child Variants) +/// 4. Query Segments: "/?:..query" (where query has a type that is FromQuery in all child Variants) or "/?:query&:other_query" (where query and other_query has a type that is FromQueryArgument in all child Variants) /// /// Routes are matched: /// 1. By there specificity this order: Query Routes ("/?:query"), Static Routes ("/route"), Dynamic Routes ("/:route"), Catch All Routes ("/:..route") diff --git a/packages/router-macro/src/query.rs b/packages/router-macro/src/query.rs index c97d3be29..40af2b39a 100644 --- a/packages/router-macro/src/query.rs +++ b/packages/router-macro/src/query.rs @@ -4,12 +4,62 @@ use syn::{Ident, Type}; use proc_macro2::TokenStream as TokenStream2; #[derive(Debug)] -pub struct QuerySegment { +pub enum QuerySegment { + Single(FullQuerySegment), + Segments(Vec), +} + +impl QuerySegment { + pub fn contains_ident(&self, ident: &Ident) -> bool { + match self { + QuerySegment::Single(segment) => segment.ident == *ident, + QuerySegment::Segments(segments) => { + segments.iter().any(|segment| segment.ident == *ident) + } + } + } + + pub fn parse(&self) -> TokenStream2 { + match self { + QuerySegment::Single(segment) => segment.parse(), + QuerySegment::Segments(segments) => { + let mut tokens = TokenStream2::new(); + tokens.extend(quote! { let split_query: std::collections::HashMap<&str, &str> = query.split('&').filter_map(|s| s.split_once('=')).collect(); }); + for segment in segments { + tokens.extend(segment.parse()); + } + tokens + } + } + } + + pub fn write(&self) -> TokenStream2 { + match self { + QuerySegment::Single(segment) => segment.write(), + QuerySegment::Segments(segments) => { + let mut tokens = TokenStream2::new(); + tokens.extend(quote! { write!(f, "?")?; }); + let mut segments_iter = segments.iter(); + if let Some(first_segment) = segments_iter.next() { + tokens.extend(first_segment.write()); + } + for segment in segments_iter { + tokens.extend(quote! { write!(f, "&")?; }); + tokens.extend(segment.write()); + } + tokens + } + } + } +} + +#[derive(Debug)] +pub struct FullQuerySegment { pub ident: Ident, pub ty: Type, } -impl QuerySegment { +impl FullQuerySegment { pub fn parse(&self) -> TokenStream2 { let ident = &self.ident; let ty = &self.ty; @@ -25,3 +75,29 @@ impl QuerySegment { } } } + +#[derive(Debug)] +pub struct QueryArgument { + pub ident: Ident, + pub ty: Type, +} + +impl QueryArgument { + pub fn parse(&self) -> TokenStream2 { + let ident = &self.ident; + let ty = &self.ty; + quote! { + let #ident = match split_query.get(stringify!(#ident)) { + Some(query_argument) => <#ty as dioxus_router::routable::FromQueryArgument>::from_query_argument(query_argument).unwrap_or_default(), + None => <#ty as Default>::default(), + }; + } + } + + pub fn write(&self) -> TokenStream2 { + let ident = &self.ident; + quote! { + write!(f, "{}={}", stringify!(#ident), #ident)?; + } + } +} diff --git a/packages/router-macro/src/route.rs b/packages/router-macro/src/route.rs index 26665e575..4724a4d7b 100644 --- a/packages/router-macro/src/route.rs +++ b/packages/router-macro/src/route.rs @@ -282,7 +282,7 @@ impl Route { } } if let Some(query) = &self.query { - if &query.ident == name { + if query.contains_ident(name) { from_route = true } } diff --git a/packages/router-macro/src/segment.rs b/packages/router-macro/src/segment.rs index 7e4e40662..21ebe5b60 100644 --- a/packages/router-macro/src/segment.rs +++ b/packages/router-macro/src/segment.rs @@ -3,7 +3,7 @@ use syn::{Ident, Type}; use proc_macro2::{Span, TokenStream as TokenStream2}; -use crate::query::QuerySegment; +use crate::query::{FullQuerySegment, QueryArgument, QuerySegment}; #[derive(Debug, Clone)] pub enum RouteSegment { @@ -201,7 +201,7 @@ pub fn parse_route_segments<'a>( // check if the route has a query string let parsed_query = match query { Some(query) => { - if let Some(query) = query.strip_prefix(':') { + if let Some(query) = query.strip_prefix(":..") { let query_ident = Ident::new(query, Span::call_site()); let field = fields.find(|(name, _)| *name == &query_ident); @@ -214,12 +214,44 @@ pub fn parse_route_segments<'a>( )); }; - Some(QuerySegment { + Some(QuerySegment::Single(FullQuerySegment { ident: query_ident, ty, - }) + })) } else { - None + let mut query_arguments = Vec::new(); + for segment in query.split('&') { + if segment.is_empty() { + return Err(syn::Error::new( + route_span, + "Query segments should be non-empty", + )); + } + if let Some(query_argument) = segment.strip_prefix(':') { + let query_ident = Ident::new(query_argument, Span::call_site()); + let field = fields.find(|(name, _)| *name == &query_ident); + + let ty = if let Some((_, ty)) = field { + ty.clone() + } else { + return Err(syn::Error::new( + route_span, + format!("Could not find a field with the name '{}'", query_ident), + )); + }; + + query_arguments.push(QueryArgument { + ident: query_ident, + ty, + }); + } else { + return Err(syn::Error::new( + route_span, + "Query segments should be a : followed by the name of the query argument", + )); + } + } + Some(QuerySegment::Segments(query_arguments)) } } None => None, diff --git a/packages/router/examples/simple_routes.rs b/packages/router/examples/simple_routes.rs index a8fb367d0..6c33c275b 100644 --- a/packages/router/examples/simple_routes.rs +++ b/packages/router/examples/simple_routes.rs @@ -55,9 +55,7 @@ fn main() { #[component] fn Root(cx: Scope) -> Element { let history = LiveviewHistory::new(cx); - render! { Router:: { - config: || RouterConfig::default().history(history), - } } + render! { Router:: { config: || RouterConfig::default().history(history) } } } #[cfg(not(feature = "liveview"))] @@ -84,12 +82,19 @@ fn Route1(cx: Scope, user_id: usize, dynamic: usize, query: String, extra: Strin "Route1{{\n\tuser_id:{user_id},\n\tdynamic:{dynamic},\n\tquery:{query},\n\textra:{extra}\n}}" } Link { - to: Route::Route1 { user_id: *user_id, dynamic: *dynamic, query: String::new(), extra: extra.clone() + "." }, + to: Route::Route1 { + user_id: *user_id, + dynamic: *dynamic, + query: String::new(), + extra: extra.clone() + ".", + }, "Route1 with extra+\".\"" } p { "Footer" } Link { - to: Route::Route3 { dynamic: String::new() }, + to: Route::Route3 { + dynamic: String::new(), + }, "Home" } } @@ -98,13 +103,13 @@ fn Route1(cx: Scope, user_id: usize, dynamic: usize, query: String, extra: Strin #[component] fn Route2(cx: Scope, user_id: usize) -> Element { render! { - pre { - "Route2{{\n\tuser_id:{user_id}\n}}" - } + pre { "Route2{{\n\tuser_id:{user_id}\n}}" } (0..*user_id).map(|i| rsx!{ p { "{i}" } }), p { "Footer" } Link { - to: Route::Route3 { dynamic: String::new() }, + to: Route::Route3 { + dynamic: String::new(), + }, "Home" } } @@ -122,29 +127,34 @@ fn Route3(cx: Scope, dynamic: String) -> Element { .collect::>(); let navigator = use_navigator(cx); - + render! { input { - oninput: move |evt| *current_route_str.write() = evt.value.clone(), + oninput: move |evt| { + *current_route_str.write() = evt.value(); + }, value: "{current_route_str.read()}" } "dynamic: {dynamic}" - Link { - to: Route::Route2 { user_id: 8888 }, - "hello world link" - } + Link { to: Route::Route2 { user_id: 8888 }, "hello world link" } button { disabled: !navigator.can_go_back(), - onclick: move |_| { navigator.go_back(); }, + onclick: move |_| { + navigator.go_back(); + }, "go back" } button { disabled: !navigator.can_go_forward(), - onclick: move |_| { navigator.go_forward(); }, + onclick: move |_| { + navigator.go_forward(); + }, "go forward" } button { - onclick: move |_| { navigator.push("https://www.google.com"); }, + onclick: move |_| { + navigator.push("https://www.google.com"); + }, "google link" } p { "Site Map" } diff --git a/packages/router/src/routable.rs b/packages/router/src/routable.rs index b1bb6d915..92277fd71 100644 --- a/packages/router/src/routable.rs +++ b/packages/router/src/routable.rs @@ -24,7 +24,7 @@ impl Display for RouteParseError { } } -/// Something that can be created from a query string. +/// Something that can be created from an entire query string. /// /// This trait needs to be implemented if you want to turn a query string into a struct. /// @@ -40,6 +40,41 @@ impl From<&'a str>> FromQuery for T { } } +/// Something that can be created from a query argument. +/// +/// This trait must be implemented for every type used within a query string in the router macro. +pub trait FromQueryArgument: Default { + /// The error that can occur when parsing a query argument. + type Err; + + /// Create an instance of `Self` from a query string. + fn from_query_argument(argument: &str) -> Result; +} + +impl FromQueryArgument for T +where + ::Err: Display, +{ + type Err = ::Err; + + fn from_query_argument(argument: &str) -> Result { + let result = match urlencoding::decode(argument) { + Ok(argument) => T::from_str(&argument), + Err(err) => { + tracing::error!("Failed to decode url encoding: {}", err); + T::from_str(argument) + } + }; + match result { + Ok(result) => Ok(result), + Err(err) => { + tracing::error!("Failed to parse query argument: {}", err); + Err(err) + } + } + } +} + /// Something that can be created from a route segment. pub trait FromRouteSegment: Sized { /// The error that can occur when parsing a route segment. diff --git a/packages/ssr/Cargo.toml b/packages/ssr/Cargo.toml index 71f5b26a0..1a8d475ee 100644 --- a/packages/ssr/Cargo.toml +++ b/packages/ssr/Cargo.toml @@ -10,7 +10,7 @@ keywords = ["dom", "ui", "gui", "react", "ssr"] [dependencies] dioxus-core = { workspace = true, features = ["serialize"] } -dioxus-html = { workspace = true } +dioxus-html = { workspace = true, features = ["eval"]} askama_escape = "0.10.3" thiserror = "1.0.23" rustc-hash = "1.1.0" diff --git a/packages/web/Cargo.toml b/packages/web/Cargo.toml index 26235c925..14ece61d0 100644 --- a/packages/web/Cargo.toml +++ b/packages/web/Cargo.toml @@ -66,7 +66,9 @@ hot_reload = [ "web-sys/WebSocket", "web-sys/Location", ] -eval = [] +eval = [ + "dioxus-html/eval", +] [dev-dependencies] dioxus = { workspace = true } diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index 982368de7..da46e5012 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -1,6 +1,6 @@ //! Implementation of a renderer for Dioxus on the web. //! -//! Oustanding todos: +//! Outstanding todos: //! - Passive event listeners //! - no-op event listener patch for safari //! - tests to ensure dyn_into works for various event types. @@ -9,16 +9,15 @@ use dioxus_core::{ BorrowedAttributeValue, ElementId, Mutation, Template, TemplateAttribute, TemplateNode, }; -use dioxus_html::{event_bubbles, CompositionData, FormData, MountedData}; -use dioxus_interpreter_js::{get_node, minimal_bindings, save_template, Channel}; +use dioxus_html::{event_bubbles, MountedData, PlatformEventData}; +use dioxus_interpreter_js::get_node; +use dioxus_interpreter_js::{minimal_bindings, save_template, Channel}; use futures_channel::mpsc; -use js_sys::Array; use rustc_hash::FxHashMap; -use std::{any::Any, rc::Rc}; -use wasm_bindgen::{closure::Closure, prelude::wasm_bindgen, JsCast, JsValue}; +use wasm_bindgen::{closure::Closure, JsCast, JsValue}; use web_sys::{Document, Element, Event}; -use crate::Config; +use crate::{load_document, virtual_event_from_websys_event, Config, WebEventConverter}; pub struct WebsysDom { document: Document, @@ -27,6 +26,7 @@ pub struct WebsysDom { templates: FxHashMap, max_template_id: u16, pub(crate) interpreter: Channel, + #[cfg(feature = "mounted")] event_channel: mpsc::UnboundedSender, } @@ -34,7 +34,7 @@ pub struct UiEvent { pub name: String, pub bubbles: bool, pub element: ElementId, - pub data: Rc, + pub data: PlatformEventData, } impl WebsysDom { @@ -102,6 +102,7 @@ impl WebsysDom { root.clone().unchecked_into(), handler.as_ref().unchecked_ref(), ); + dioxus_html::set_event_converter(Box::new(WebEventConverter)); handler.forget(); Self { document, @@ -109,6 +110,7 @@ impl WebsysDom { interpreter, templates: FxHashMap::default(), max_template_id: 0, + #[cfg(feature = "mounted")] event_channel, } } @@ -179,6 +181,7 @@ impl WebsysDom { pub fn apply_edits(&mut self, mut edits: Vec) { use Mutation::*; let i = &mut self.interpreter; + #[cfg(feature = "mounted")] // we need to apply the mount events last, so we collect them here let mut to_mount = Vec::new(); for edit in &edits { @@ -234,6 +237,7 @@ impl WebsysDom { match *name { // mounted events are fired immediately after the element is mounted. "mounted" => { + #[cfg(feature = "mounted")] to_mount.push(*id); } _ => { @@ -254,6 +258,7 @@ impl WebsysDom { edits.clear(); i.flush(); + #[cfg(feature = "mounted")] for id in to_mount { self.send_mount_event(id); } @@ -263,176 +268,17 @@ impl WebsysDom { let node = get_node(id.0 as u32); if let Some(element) = node.dyn_ref::() { let data: MountedData = element.into(); - let data = Rc::new(data); + let data = Box::new(data); let _ = self.event_channel.unbounded_send(UiEvent { name: "mounted".to_string(), bubbles: false, element: id, - data, + data: PlatformEventData::new(data), }); } } } -// todo: some of these events are being casted to the wrong event type. -// We need tests that simulate clicks/etc and make sure every event type works. -pub fn virtual_event_from_websys_event(event: web_sys::Event, target: Element) -> Rc { - use dioxus_html::events::*; - - match event.type_().as_str() { - "copy" | "cut" | "paste" => Rc::new(ClipboardData {}), - "compositionend" | "compositionstart" | "compositionupdate" => { - make_composition_event(&event) - } - "keydown" | "keypress" | "keyup" => Rc::new(KeyboardData::from(event)), - "focus" | "blur" | "focusout" | "focusin" => Rc::new(FocusData {}), - - "change" | "input" | "invalid" | "reset" | "submit" => read_input_to_data(target), - - "click" | "contextmenu" | "dblclick" | "doubleclick" | "mousedown" | "mouseenter" - | "mouseleave" | "mousemove" | "mouseout" | "mouseover" | "mouseup" => { - Rc::new(MouseData::from(event)) - } - "drag" | "dragend" | "dragenter" | "dragexit" | "dragleave" | "dragover" | "dragstart" - | "drop" => { - let mouse = MouseData::from(event); - Rc::new(DragData { mouse }) - } - - "pointerdown" | "pointermove" | "pointerup" | "pointercancel" | "gotpointercapture" - | "lostpointercapture" | "pointerenter" | "pointerleave" | "pointerover" | "pointerout" => { - Rc::new(PointerData::from(event)) - } - "select" => Rc::new(SelectionData {}), - "touchcancel" | "touchend" | "touchmove" | "touchstart" => Rc::new(TouchData::from(event)), - - "scroll" => Rc::new(ScrollData {}), - "wheel" => Rc::new(WheelData::from(event)), - "animationstart" | "animationend" | "animationiteration" => { - Rc::new(AnimationData::from(event)) - } - "transitionend" => Rc::new(TransitionData::from(event)), - "abort" | "canplay" | "canplaythrough" | "durationchange" | "emptied" | "encrypted" - | "ended" | "loadeddata" | "loadedmetadata" | "loadstart" | "pause" | "play" - | "playing" | "progress" | "ratechange" | "seeked" | "seeking" | "stalled" | "suspend" - | "timeupdate" | "volumechange" | "waiting" => Rc::new(MediaData {}), - "error" => Rc::new(ImageData { load_error: true }), - "load" => Rc::new(ImageData { load_error: false }), - "toggle" => Rc::new(ToggleData {}), - - _ => Rc::new(()), - } -} - -fn make_composition_event(event: &Event) -> Rc { - let evt: &web_sys::CompositionEvent = event.dyn_ref().unwrap(); - Rc::new(CompositionData { - data: evt.data().unwrap_or_default(), - }) -} - -pub(crate) fn load_document() -> Document { - web_sys::window() - .expect("should have access to the Window") - .document() - .expect("should have access to the Document") -} - -fn read_input_to_data(target: Element) -> Rc { - // todo: these handlers might get really slow if the input box gets large and allocation pressure is heavy - // don't have a good solution with the serialized event problem - - let value: String = target - .dyn_ref() - .map(|input: &web_sys::HtmlInputElement| { - // todo: special case more input types - match input.type_().as_str() { - "checkbox" => { - match input.checked() { - true => "true".to_string(), - false => "false".to_string(), - } - }, - _ => { - input.value() - } - } - }) - .or_else(|| { - target - .dyn_ref() - .map(|input: &web_sys::HtmlTextAreaElement| input.value()) - }) - // select elements are NOT input events - because - why woudn't they be?? - .or_else(|| { - target - .dyn_ref() - .map(|input: &web_sys::HtmlSelectElement| input.value()) - }) - .or_else(|| { - target - .dyn_ref::() - .unwrap() - .text_content() - }) - .expect("only an InputElement or TextAreaElement or an element with contenteditable=true can have an oninput event listener"); - - let mut values = std::collections::HashMap::new(); - - // try to fill in form values - if let Some(form) = target.dyn_ref::() { - let form_data = get_form_data(form); - for value in form_data.entries().into_iter().flatten() { - if let Ok(array) = value.dyn_into::() { - if let Some(name) = array.get(0).as_string() { - if let Ok(item_values) = array.get(1).dyn_into::() { - let item_values = - item_values.iter().filter_map(|v| v.as_string()).collect(); - - values.insert(name, item_values); - } - } - } - } - } - - #[cfg(not(feature = "file_engine"))] - let files = None; - #[cfg(feature = "file_engine")] - let files = target - .dyn_ref() - .and_then(|input: &web_sys::HtmlInputElement| { - input.files().and_then(|files| { - #[allow(clippy::arc_with_non_send_sync)] - crate::file_engine::WebFileEngine::new(files) - .map(|f| std::sync::Arc::new(f) as std::sync::Arc) - }) - }); - - Rc::new(FormData { - value, - values, - files, - }) -} - -// web-sys does not expose the keys api for form data, so we need to manually bind to it -#[wasm_bindgen(inline_js = r#" - export function get_form_data(form) { - let values = new Map(); - const formData = new FormData(form); - - for (let name of formData.keys()) { - values.set(name, formData.getAll(name)); - } - - return values; - } -"#)] -extern "C" { - fn get_form_data(form: &web_sys::HtmlFormElement) -> js_sys::Map; -} - fn walk_event_for_id(event: &web_sys::Event) -> Option<(ElementId, web_sys::Element)> { let target = event .target() diff --git a/packages/web/src/event.rs b/packages/web/src/event.rs new file mode 100644 index 000000000..d1eed67a6 --- /dev/null +++ b/packages/web/src/event.rs @@ -0,0 +1,443 @@ +use std::{any::Any, collections::HashMap}; + +use dioxus_html::{ + FileEngine, FormData, HasFormData, HasImageData, HtmlEventConverter, ImageData, MountedData, + PlatformEventData, ScrollData, +}; +use js_sys::Array; +use wasm_bindgen::{prelude::wasm_bindgen, JsCast}; +use web_sys::{Document, Element, Event}; + +pub(crate) struct WebEventConverter; + +#[inline(always)] +fn downcast_event(event: &dioxus_html::PlatformEventData) -> &GenericWebSysEvent { + event + .downcast::() + .expect("event should be a GenericWebSysEvent") +} + +impl HtmlEventConverter for WebEventConverter { + #[inline(always)] + fn convert_animation_data( + &self, + event: &dioxus_html::PlatformEventData, + ) -> dioxus_html::AnimationData { + downcast_event(event).raw.clone().into() + } + + #[inline(always)] + fn convert_clipboard_data( + &self, + event: &dioxus_html::PlatformEventData, + ) -> dioxus_html::ClipboardData { + downcast_event(event).raw.clone().into() + } + + #[inline(always)] + fn convert_composition_data( + &self, + event: &dioxus_html::PlatformEventData, + ) -> dioxus_html::CompositionData { + downcast_event(event).raw.clone().into() + } + + #[inline(always)] + fn convert_drag_data(&self, event: &dioxus_html::PlatformEventData) -> dioxus_html::DragData { + downcast_event(event).raw.clone().into() + } + + #[inline(always)] + fn convert_focus_data(&self, event: &dioxus_html::PlatformEventData) -> dioxus_html::FocusData { + downcast_event(event).raw.clone().into() + } + + #[inline(always)] + fn convert_form_data(&self, event: &dioxus_html::PlatformEventData) -> dioxus_html::FormData { + let event = downcast_event(event); + FormData::new(WebFormData::new(event.element.clone(), event.raw.clone())) + } + + #[inline(always)] + fn convert_image_data(&self, event: &dioxus_html::PlatformEventData) -> dioxus_html::ImageData { + let event = downcast_event(event); + let error = event.raw.type_() == "error"; + ImageData::new(WebImageEvent::new(event.raw.clone(), error)) + } + + #[inline(always)] + fn convert_keyboard_data( + &self, + event: &dioxus_html::PlatformEventData, + ) -> dioxus_html::KeyboardData { + downcast_event(event).raw.clone().into() + } + + #[inline(always)] + fn convert_media_data(&self, event: &dioxus_html::PlatformEventData) -> dioxus_html::MediaData { + downcast_event(event).raw.clone().into() + } + + #[allow(unused_variables)] + #[inline(always)] + fn convert_mounted_data(&self, event: &dioxus_html::PlatformEventData) -> MountedData { + #[cfg(feature = "mounted")] + { + MountedData::from( + event + .downcast::() + .expect("event should be a web_sys::Element"), + ) + } + #[cfg(not(feature = "mounted"))] + { + panic!("mounted events are not supported without the mounted feature on the dioxus-web crate enabled") + } + } + + #[inline(always)] + fn convert_mouse_data(&self, event: &dioxus_html::PlatformEventData) -> dioxus_html::MouseData { + downcast_event(event).raw.clone().into() + } + + #[inline(always)] + fn convert_pointer_data( + &self, + event: &dioxus_html::PlatformEventData, + ) -> dioxus_html::PointerData { + downcast_event(event).raw.clone().into() + } + + #[inline(always)] + fn convert_scroll_data( + &self, + event: &dioxus_html::PlatformEventData, + ) -> dioxus_html::ScrollData { + ScrollData::from(downcast_event(event).raw.clone()) + } + + #[inline(always)] + fn convert_selection_data( + &self, + event: &dioxus_html::PlatformEventData, + ) -> dioxus_html::SelectionData { + downcast_event(event).raw.clone().into() + } + + #[inline(always)] + fn convert_toggle_data( + &self, + event: &dioxus_html::PlatformEventData, + ) -> dioxus_html::ToggleData { + downcast_event(event).raw.clone().into() + } + + #[inline(always)] + fn convert_touch_data(&self, event: &dioxus_html::PlatformEventData) -> dioxus_html::TouchData { + downcast_event(event).raw.clone().into() + } + + #[inline(always)] + fn convert_transition_data( + &self, + event: &dioxus_html::PlatformEventData, + ) -> dioxus_html::TransitionData { + downcast_event(event).raw.clone().into() + } + + #[inline(always)] + fn convert_wheel_data(&self, event: &dioxus_html::PlatformEventData) -> dioxus_html::WheelData { + downcast_event(event).raw.clone().into() + } +} + +/// A extension trait for web-sys events that provides a way to get the event as a web-sys event. +pub trait WebEventExt { + /// Get the event as a web-sys event. + fn web_event(&self) -> &E; +} + +impl WebEventExt for dioxus_html::AnimationData { + fn web_event(&self) -> &web_sys::AnimationEvent { + self.downcast::() + .expect("event should be a WebAnimationEvent") + } +} + +impl WebEventExt for dioxus_html::ClipboardData { + fn web_event(&self) -> &web_sys::Event { + self.downcast::() + .expect("event should be a web_sys::Event") + } +} + +impl WebEventExt for dioxus_html::CompositionData { + fn web_event(&self) -> &web_sys::CompositionEvent { + self.downcast::() + .expect("event should be a WebCompositionEvent") + } +} + +impl WebEventExt for dioxus_html::DragData { + fn web_event(&self) -> &web_sys::MouseEvent { + self.downcast::() + .expect("event should be a WebMouseEvent") + } +} + +impl WebEventExt for dioxus_html::FocusData { + fn web_event(&self) -> &web_sys::FocusEvent { + self.downcast::() + .expect("event should be a WebFocusEvent") + } +} + +impl WebEventExt for dioxus_html::FormData { + fn web_event(&self) -> &web_sys::Event { + self.downcast::() + .expect("event should be a WebFormData") + } +} + +impl WebEventExt for dioxus_html::ImageData { + fn web_event(&self) -> &WebImageEvent { + self.downcast::() + .expect("event should be a WebImageEvent") + } +} + +impl WebEventExt for dioxus_html::KeyboardData { + fn web_event(&self) -> &web_sys::KeyboardEvent { + self.downcast::() + .expect("event should be a WebKeyboardEvent") + } +} + +impl WebEventExt for dioxus_html::MediaData { + fn web_event(&self) -> &web_sys::Event { + self.downcast::() + .expect("event should be a WebMediaEvent") + } +} + +impl WebEventExt for MountedData { + fn web_event(&self) -> &web_sys::Element { + self.downcast::() + .expect("event should be a web_sys::Element") + } +} + +impl WebEventExt for dioxus_html::MouseData { + fn web_event(&self) -> &web_sys::MouseEvent { + self.downcast::() + .expect("event should be a WebMouseEvent") + } +} + +impl WebEventExt for dioxus_html::PointerData { + fn web_event(&self) -> &web_sys::PointerEvent { + self.downcast::() + .expect("event should be a WebPointerEvent") + } +} + +impl WebEventExt for ScrollData { + fn web_event(&self) -> &web_sys::Event { + self.downcast::() + .expect("event should be a WebScrollEvent") + } +} + +impl WebEventExt for dioxus_html::SelectionData { + fn web_event(&self) -> &web_sys::Event { + self.downcast::() + .expect("event should be a WebSelectionEvent") + } +} + +impl WebEventExt for dioxus_html::ToggleData { + fn web_event(&self) -> &web_sys::Event { + self.downcast::() + .expect("event should be a WebToggleEvent") + } +} + +impl WebEventExt for dioxus_html::TouchData { + fn web_event(&self) -> &web_sys::TouchEvent { + self.downcast::() + .expect("event should be a WebTouchEvent") + } +} + +impl WebEventExt for dioxus_html::TransitionData { + fn web_event(&self) -> &web_sys::TransitionEvent { + self.downcast::() + .expect("event should be a WebTransitionEvent") + } +} + +impl WebEventExt for dioxus_html::WheelData { + fn web_event(&self) -> &web_sys::WheelEvent { + self.downcast::() + .expect("event should be a WebWheelEvent") + } +} + +struct GenericWebSysEvent { + raw: Event, + element: Element, +} + +// todo: some of these events are being casted to the wrong event type. +// We need tests that simulate clicks/etc and make sure every event type works. +pub(crate) fn virtual_event_from_websys_event( + event: web_sys::Event, + target: Element, +) -> PlatformEventData { + PlatformEventData::new(Box::new(GenericWebSysEvent { + raw: event, + element: target, + })) +} + +pub(crate) fn load_document() -> Document { + web_sys::window() + .expect("should have access to the Window") + .document() + .expect("should have access to the Document") +} + +struct WebImageEvent { + raw: Event, + error: bool, +} + +impl WebImageEvent { + fn new(raw: Event, error: bool) -> Self { + Self { raw, error } + } +} + +impl HasImageData for WebImageEvent { + fn load_error(&self) -> bool { + self.error + } + + fn as_any(&self) -> &dyn Any { + &self.raw as &dyn Any + } +} + +struct WebFormData { + element: Element, + raw: Event, +} + +impl WebFormData { + fn new(element: Element, raw: Event) -> Self { + Self { element, raw } + } +} + +impl HasFormData for WebFormData { + fn value(&self) -> String { + let target = &self.element; + target + .dyn_ref() + .map(|input: &web_sys::HtmlInputElement| { + // todo: special case more input types + match input.type_().as_str() { + "checkbox" => { + match input.checked() { + true => "true".to_string(), + false => "false".to_string(), + } + }, + _ => { + input.value() + } + } + }) + .or_else(|| { + target + .dyn_ref() + .map(|input: &web_sys::HtmlTextAreaElement| input.value()) + }) + // select elements are NOT input events - because - why woudn't they be?? + .or_else(|| { + target + .dyn_ref() + .map(|input: &web_sys::HtmlSelectElement| input.value()) + }) + .or_else(|| { + target + .dyn_ref::() + .unwrap() + .text_content() + }) + .expect("only an InputElement or TextAreaElement or an element with contenteditable=true can have an oninput event listener") + } + + fn values(&self) -> HashMap> { + let mut values = std::collections::HashMap::new(); + + // try to fill in form values + if let Some(form) = self.element.dyn_ref::() { + let form_data = get_form_data(form); + for value in form_data.entries().into_iter().flatten() { + if let Ok(array) = value.dyn_into::() { + if let Some(name) = array.get(0).as_string() { + if let Ok(item_values) = array.get(1).dyn_into::() { + let item_values = + item_values.iter().filter_map(|v| v.as_string()).collect(); + + values.insert(name, item_values); + } + } + } + } + } + + values + } + + fn files(&self) -> Option> { + #[cfg(not(feature = "file_engine"))] + let files = None; + #[cfg(feature = "file_engine")] + let files = self + .element + .dyn_ref() + .and_then(|input: &web_sys::HtmlInputElement| { + input.files().and_then(|files| { + #[allow(clippy::arc_with_non_send_sync)] + crate::file_engine::WebFileEngine::new(files).map(|f| { + std::sync::Arc::new(f) as std::sync::Arc + }) + }) + }); + + files + } + + fn as_any(&self) -> &dyn Any { + &self.raw as &dyn Any + } +} + +// web-sys does not expose the keys api for form data, so we need to manually bind to it +#[wasm_bindgen(inline_js = r#" + export function get_form_data(form) { + let values = new Map(); + const formData = new FormData(form); + + for (let name of formData.keys()) { + values.set(name, formData.getAll(name)); + } + + return values; + } +"#)] +extern "C" { + fn get_form_data(form: &web_sys::HtmlFormElement) -> js_sys::Map; +} diff --git a/packages/web/src/lib.rs b/packages/web/src/lib.rs index 364f4aaa7..72a85fc90 100644 --- a/packages/web/src/lib.rs +++ b/packages/web/src/lib.rs @@ -55,6 +55,8 @@ // - Do the VDOM work during the idlecallback // - Do DOM work in the next requestAnimationFrame callback +use std::rc::Rc; + pub use crate::cfg::Config; #[cfg(feature = "file_engine")] pub use crate::file_engine::WebFileEngineExt; @@ -69,6 +71,8 @@ mod cfg; mod dom; #[cfg(feature = "eval")] mod eval; +mod event; +pub use event::*; #[cfg(feature = "file_engine")] mod file_engine; #[cfg(all(feature = "hot_reload", debug_assertions))] @@ -273,14 +277,12 @@ pub async fn run_with_props(root: fn(Scope) -> Element, root_prop // Dequeue all of the events from the channel in send order // todo: we should re-order these if possible while let Some(evt) = res { - web_sys::console::log_1( - &format!( - "event: {:?}, {:?}, {:?}", - evt.name, evt.bubbles, evt.element - ) - .into(), + dom.handle_event( + evt.name.as_str(), + Rc::new(evt.data), + evt.element, + evt.bubbles, ); - dom.handle_event(evt.name.as_str(), evt.data, evt.element, evt.bubbles); res = rx.try_next().transpose().unwrap().ok(); } diff --git a/playwright-tests/playwright.config.js b/playwright-tests/playwright.config.js index b4b0c4041..dcee8f0e4 100644 --- a/playwright-tests/playwright.config.js +++ b/playwright-tests/playwright.config.js @@ -76,23 +76,23 @@ module.exports = defineConfig({ command: "cargo run --package dioxus-playwright-liveview-test --bin dioxus-playwright-liveview-test", port: 3030, - timeout: 10 * 60 * 1000, + timeout: 20 * 60 * 1000, reuseExistingServer: !process.env.CI, stdout: "pipe", }, { cwd: path.join(process.cwd(), "web"), - command: "cargo run --package dioxus-cli -- serve", + command: "cargo run --package dioxus-cli --release -- serve --skip-assets", port: 8080, - timeout: 10 * 60 * 1000, + timeout: 20 * 60 * 1000, reuseExistingServer: !process.env.CI, stdout: "pipe", }, { cwd: path.join(process.cwd(), 'fullstack'), - command: 'cargo run --package dioxus-cli -- build --features web --release && cargo run --release --features ssr', + command: 'cargo run --package dioxus-cli --release -- build --features web --release --skip-assets\ncargo run --release --features ssr', port: 3333, - timeout: 10 * 60 * 1000, + timeout: 20 * 60 * 1000, reuseExistingServer: !process.env.CI, stdout: "pipe", },