From d253ff3beb67b6a21c0f2ca002d3278a8a0fcf9a Mon Sep 17 00:00:00 2001 From: Markus Kohlhase Date: Mon, 9 Dec 2024 17:57:06 +0100 Subject: [PATCH 1/2] xilem_web: Add up- and download example --- Cargo.lock | 69 ++++++-- Cargo.toml | 1 + .../web_examples/up_and_download/Cargo.toml | 16 ++ .../web_examples/up_and_download/index.html | 8 + .../web_examples/up_and_download/src/main.rs | 152 ++++++++++++++++++ 5 files changed, 229 insertions(+), 17 deletions(-) create mode 100644 xilem_web/web_examples/up_and_download/Cargo.toml create mode 100644 xilem_web/web_examples/up_and_download/index.html create mode 100644 xilem_web/web_examples/up_and_download/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index eb16dc5d2..061f0b3cc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1313,6 +1313,29 @@ dependencies = [ "xml-rs", ] +[[package]] +name = "gloo-events" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27c26fb45f7c385ba980f5fa87ac677e363949e065a083722697ef1b2cc91e41" +dependencies = [ + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-file" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97563d71863fb2824b2e974e754a81d19c4a7ec47b09ced8a0e6656b6d54bd1f" +dependencies = [ + "futures-channel", + "gloo-events", + "js-sys", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "gloo-net" version = "0.6.0" @@ -1817,9 +1840,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.74" +version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a865e038f7f6ed956f788f0d7d60c541fff74c7bd74272c5d4cf15c63743e705" +checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7" dependencies = [ "once_cell", "wasm-bindgen", @@ -3785,6 +3808,19 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +[[package]] +name = "up_and_download" +version = "0.1.0" +dependencies = [ + "anyhow", + "console_error_panic_hook", + "console_log", + "gloo-file", + "log", + "web-sys", + "xilem_web", +] + [[package]] name = "url" version = "2.5.4" @@ -3893,9 +3929,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.97" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d15e63b4482863c109d70a7b8706c1e364eb6ea449b201a76c5b89cedcec2d5c" +checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396" dependencies = [ "cfg-if", "once_cell", @@ -3904,13 +3940,12 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.97" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d36ef12e3aaca16ddd3f67922bc63e48e953f126de60bd33ccc0101ef9998cd" +checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79" dependencies = [ "bumpalo", "log", - "once_cell", "proc-macro2", "quote", "syn 2.0.90", @@ -3919,9 +3954,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.47" +version = "0.4.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dfaf8f50e5f293737ee323940c7d8b08a66a95a419223d9f41610ca08b0833d" +checksum = "38176d9b44ea84e9184eff0bc34cc167ed044f816accfe5922e54d84cf48eca2" dependencies = [ "cfg-if", "js-sys", @@ -3932,9 +3967,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.97" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "705440e08b42d3e4b36de7d66c944be628d579796b8090bfa3471478a2260051" +checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3942,9 +3977,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.97" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98c9ae5a76e46f4deecd0f0255cc223cfa18dc9b261213b8aa0c7b36f61b3f1d" +checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" dependencies = [ "proc-macro2", "quote", @@ -3955,9 +3990,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.97" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ee99da9c5ba11bd675621338ef6fa52296b76b83305e9b6e5c77d4c286d6d49" +checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" [[package]] name = "wayland-backend" @@ -4070,9 +4105,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.74" +version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a98bc3c33f0fe7e59ad7cd041b89034fa82a7c2d4365ca538dda6cdaf513863c" +checksum = "04dd7223427d52553d3702c004d3b2fe07c148165faa56313cb00211e31c12bc" dependencies = [ "js-sys", "wasm-bindgen", diff --git a/Cargo.toml b/Cargo.toml index 5e56895c9..320c4512f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ members = [ "xilem_web/web_examples/spawn_tasks", "xilem_web/web_examples/svgtoy", "xilem_web/web_examples/svgdraw", + "xilem_web/web_examples/up_and_download", "tree_arena", ] diff --git a/xilem_web/web_examples/up_and_download/Cargo.toml b/xilem_web/web_examples/up_and_download/Cargo.toml new file mode 100644 index 000000000..64aeb09f4 --- /dev/null +++ b/xilem_web/web_examples/up_and_download/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "up_and_download" +version = "0.1.0" +publish = false +license.workspace = true +edition.workspace = true +rust-version.workspace = true + +[dependencies] +anyhow = "1.0.94" +console_error_panic_hook = "0.1.7" +console_log = "1.0.0" +gloo-file = { version = "0.3.0", features = ["futures"] } +log = "0.4.22" +web-sys = { version = "0.3.76", features = ["Blob", "File", "Url"] } +xilem_web = { path = "../.." } diff --git a/xilem_web/web_examples/up_and_download/index.html b/xilem_web/web_examples/up_and_download/index.html new file mode 100644 index 000000000..7c9d63243 --- /dev/null +++ b/xilem_web/web_examples/up_and_download/index.html @@ -0,0 +1,8 @@ + + + + xilem web | Up- and download example + + + + diff --git a/xilem_web/web_examples/up_and_download/src/main.rs b/xilem_web/web_examples/up_and_download/src/main.rs new file mode 100644 index 000000000..78ee076e7 --- /dev/null +++ b/xilem_web/web_examples/up_and_download/src/main.rs @@ -0,0 +1,152 @@ +// Copyright 2023 the Xilem Authors +// SPDX-License-Identifier: Apache-2.0 + +//! This example demonstrates how to download or upload a text file +//! within a client side rendered web application without a server. + +use std::{cell::RefCell, rc::Rc}; + +use gloo_file::{Blob, File, FileReadError, ObjectUrl}; +use web_sys::wasm_bindgen::JsCast; +use xilem_web::{ + concurrent::memoized_await, core::fork, document_body, elements::html, interfaces::Element, + modifiers::style, App, DomView, +}; + +struct AppState { + text: String, + upload_file: Option, + start_upload: bool, + raw_file_input_el: Rc>>, + raw_download_link: Rc>>, +} + +impl Default for AppState { + fn default() -> Self { + AppState { + text: "Hello from Xilem Web :)".to_string(), + upload_file: None, + start_upload: false, + raw_file_input_el: Rc::new(RefCell::new(None)), + raw_download_link: Rc::new(RefCell::new(None)), + } + } +} + +fn app_logic(app_state: &mut AppState) -> impl Element { + let upload_action = app_state + .start_upload + .then(|| { + app_state.upload_file.take().map(|file| { + reset_file_input(app_state); + app_state.start_upload = false; + memoized_await( + file, + |file| gloo_file::futures::read_as_text(file), + handle_upload_result, + ) + }) + }) + .flatten(); + + html::div(( + html::h1("Up- and download example"), + html::textarea(app_state.text.clone()), + html::h2("Download"), + html::button("download text").on_click(|state: &mut AppState, _| { + let el_ref = state.raw_download_link.borrow_mut(); + let blob = Blob::new(&*state.text); + let url = ObjectUrl::from(blob); + let el = el_ref.as_ref().unwrap(); + el.set_href(&url); + el.click(); + }), + hidden_download_link(app_state), + html::h2("Upload"), + html::div(( + upload_file_input(app_state), + html::button("x").on_click(|state: &mut AppState, _| { + reset_file_input(state); + }), + )), + fork( + html::button("upload").on_click(|state: &mut AppState, _| { + state.start_upload = true; + }), + upload_action, + ), + )) +} + +fn reset_file_input(state: &mut AppState) { + state.upload_file = None; + if let Some(el) = &*state.raw_file_input_el.borrow_mut() { + el.set_value(""); + } +} + +fn handle_upload_result(state: &mut AppState, result: Result) { + match result { + Ok(txt) => { + state.text = txt; + } + Err(err) => { + log::error!("Unable to upload file: {err}"); + } + } +} + +fn upload_file_input(app_state: &mut AppState) -> impl Element { + html::input(()) + .attr("type", "file") + .attr("accept", "text/plain") + .after_build({ + let el_ref = Rc::clone(&app_state.raw_file_input_el); + move |el| { + *el_ref.borrow_mut() = Some(el.clone()); + } + }) + .before_teardown({ + let el_ref = Rc::clone(&app_state.raw_file_input_el); + move |_| { + *el_ref.borrow_mut() = None; + } + }) + .on_change(|state: &mut AppState, ev| { + ev.prevent_default(); + let input = ev + .target() + .unwrap() + .unchecked_into::(); + let Some(files) = input.files() else { + state.upload_file = None; + return; + }; + state.upload_file = files.get(0).map(File::from); + }) +} + +fn hidden_download_link(state: &mut AppState) -> impl Element { + html::a("Download example text") + .style(style("display", "none")) + .attr("download", "example.txt") + .after_build({ + let el_ref = Rc::clone(&state.raw_download_link); + move |el| { + *el_ref.borrow_mut() = + Some(el.dyn_ref::().unwrap().clone()); + } + }) + .before_teardown({ + let el_ref = Rc::clone(&state.raw_download_link); + move |_| { + *el_ref.borrow_mut() = None; + } + }) +} + +pub fn main() { + _ = console_log::init_with_level(log::Level::Debug); + console_error_panic_hook::set_once(); + App::new(document_body(), AppState::default(), app_logic).run(); +} From 294e73331740f2fcc21d3100fc6988ffb669cb8f Mon Sep 17 00:00:00 2001 From: Markus Kohlhase Date: Mon, 9 Dec 2024 20:52:46 +0100 Subject: [PATCH 2/2] Rename example --- Cargo.lock | 26 +++---- Cargo.toml | 2 +- .../Cargo.toml | 2 +- .../index.html | 0 .../src/main.rs | 68 +++++++++---------- 5 files changed, 49 insertions(+), 49 deletions(-) rename xilem_web/web_examples/{up_and_download => open_and_save_file}/Cargo.toml (93%) rename xilem_web/web_examples/{up_and_download => open_and_save_file}/index.html (100%) rename xilem_web/web_examples/{up_and_download => open_and_save_file}/src/main.rs (65%) diff --git a/Cargo.lock b/Cargo.lock index 061f0b3cc..d20cda05f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2463,6 +2463,19 @@ version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +[[package]] +name = "open_and_save_file" +version = "0.1.0" +dependencies = [ + "anyhow", + "console_error_panic_hook", + "console_log", + "gloo-file", + "log", + "web-sys", + "xilem_web", +] + [[package]] name = "orbclient" version = "0.3.48" @@ -3808,19 +3821,6 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" -[[package]] -name = "up_and_download" -version = "0.1.0" -dependencies = [ - "anyhow", - "console_error_panic_hook", - "console_log", - "gloo-file", - "log", - "web-sys", - "xilem_web", -] - [[package]] name = "url" version = "2.5.4" diff --git a/Cargo.toml b/Cargo.toml index 320c4512f..5ba12d7ab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,7 @@ members = [ "xilem_web/web_examples/spawn_tasks", "xilem_web/web_examples/svgtoy", "xilem_web/web_examples/svgdraw", - "xilem_web/web_examples/up_and_download", + "xilem_web/web_examples/open_and_save_file", "tree_arena", ] diff --git a/xilem_web/web_examples/up_and_download/Cargo.toml b/xilem_web/web_examples/open_and_save_file/Cargo.toml similarity index 93% rename from xilem_web/web_examples/up_and_download/Cargo.toml rename to xilem_web/web_examples/open_and_save_file/Cargo.toml index 64aeb09f4..04c560481 100644 --- a/xilem_web/web_examples/up_and_download/Cargo.toml +++ b/xilem_web/web_examples/open_and_save_file/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "up_and_download" +name = "open_and_save_file" version = "0.1.0" publish = false license.workspace = true diff --git a/xilem_web/web_examples/up_and_download/index.html b/xilem_web/web_examples/open_and_save_file/index.html similarity index 100% rename from xilem_web/web_examples/up_and_download/index.html rename to xilem_web/web_examples/open_and_save_file/index.html diff --git a/xilem_web/web_examples/up_and_download/src/main.rs b/xilem_web/web_examples/open_and_save_file/src/main.rs similarity index 65% rename from xilem_web/web_examples/up_and_download/src/main.rs rename to xilem_web/web_examples/open_and_save_file/src/main.rs index 78ee076e7..7cad2fecd 100644 --- a/xilem_web/web_examples/up_and_download/src/main.rs +++ b/xilem_web/web_examples/open_and_save_file/src/main.rs @@ -1,7 +1,7 @@ -// Copyright 2023 the Xilem Authors +// Copyright 2024 the Xilem Authors // SPDX-License-Identifier: Apache-2.0 -//! This example demonstrates how to download or upload a text file +//! This example demonstrates how to open or save a text file //! within a client side rendered web application without a server. use std::{cell::RefCell, rc::Rc}; @@ -15,88 +15,88 @@ use xilem_web::{ struct AppState { text: String, - upload_file: Option, - start_upload: bool, + file_to_open: Option, + start_opening: bool, raw_file_input_el: Rc>>, - raw_download_link: Rc>>, + raw_save_link: Rc>>, } impl Default for AppState { fn default() -> Self { AppState { text: "Hello from Xilem Web :)".to_string(), - upload_file: None, - start_upload: false, + file_to_open: None, + start_opening: false, raw_file_input_el: Rc::new(RefCell::new(None)), - raw_download_link: Rc::new(RefCell::new(None)), + raw_save_link: Rc::new(RefCell::new(None)), } } } fn app_logic(app_state: &mut AppState) -> impl Element { - let upload_action = app_state - .start_upload + let open_action = app_state + .start_opening .then(|| { - app_state.upload_file.take().map(|file| { + app_state.file_to_open.take().map(|file| { reset_file_input(app_state); - app_state.start_upload = false; + app_state.start_opening = false; memoized_await( file, |file| gloo_file::futures::read_as_text(file), - handle_upload_result, + handle_open_result, ) }) }) .flatten(); html::div(( - html::h1("Up- and download example"), + html::h1("Open and save file example"), html::textarea(app_state.text.clone()), - html::h2("Download"), - html::button("download text").on_click(|state: &mut AppState, _| { - let el_ref = state.raw_download_link.borrow_mut(); + html::h2("Save"), + html::button("save text").on_click(|state: &mut AppState, _| { + let el_ref = state.raw_save_link.borrow_mut(); let blob = Blob::new(&*state.text); let url = ObjectUrl::from(blob); let el = el_ref.as_ref().unwrap(); el.set_href(&url); el.click(); }), - hidden_download_link(app_state), - html::h2("Upload"), + hidden_save_link(app_state), + html::h2("Open"), html::div(( - upload_file_input(app_state), + open_file_input(app_state), html::button("x").on_click(|state: &mut AppState, _| { reset_file_input(state); }), )), fork( - html::button("upload").on_click(|state: &mut AppState, _| { - state.start_upload = true; + html::button("open").on_click(|state: &mut AppState, _| { + state.start_opening = true; }), - upload_action, + open_action, ), )) } fn reset_file_input(state: &mut AppState) { - state.upload_file = None; + state.file_to_open = None; if let Some(el) = &*state.raw_file_input_el.borrow_mut() { el.set_value(""); } } -fn handle_upload_result(state: &mut AppState, result: Result) { +fn handle_open_result(state: &mut AppState, result: Result) { match result { Ok(txt) => { state.text = txt; } Err(err) => { - log::error!("Unable to upload file: {err}"); + log::error!("Unable to open file: {err}"); } } } -fn upload_file_input(app_state: &mut AppState) -> impl Element { +fn open_file_input(app_state: &mut AppState) -> impl Element { html::input(()) .attr("type", "file") .attr("accept", "text/plain") @@ -119,26 +119,26 @@ fn upload_file_input(app_state: &mut AppState) -> impl Element { .unwrap() .unchecked_into::(); let Some(files) = input.files() else { - state.upload_file = None; + state.file_to_open = None; return; }; - state.upload_file = files.get(0).map(File::from); + state.file_to_open = files.get(0).map(File::from); }) } -fn hidden_download_link(state: &mut AppState) -> impl Element { - html::a("Download example text") +fn hidden_save_link(state: &mut AppState) -> impl Element { + html::a("Save example text") .style(style("display", "none")) - .attr("download", "example.txt") + .attr("save", "example.txt") .after_build({ - let el_ref = Rc::clone(&state.raw_download_link); + let el_ref = Rc::clone(&state.raw_save_link); move |el| { *el_ref.borrow_mut() = Some(el.dyn_ref::().unwrap().clone()); } }) .before_teardown({ - let el_ref = Rc::clone(&state.raw_download_link); + let el_ref = Rc::clone(&state.raw_save_link); move |_| { *el_ref.borrow_mut() = None; }