From bdbae8ccb01d36fa95b11f00641db98dff62bdeb Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 14 Feb 2024 12:33:07 -0800 Subject: [PATCH] Update examples, add css --- Cargo.lock | 1 + Cargo.toml | 3 +- examples/all_events.rs | 44 +++---- examples/assets/clock.css | 23 ++++ examples/assets/counter.css | 26 ++++ examples/assets/crm.css | 16 +++ examples/assets/custom_assets.css | 12 ++ examples/assets/dog_app.css | 0 examples/assets/events.css | 24 ++++ examples/assets/file_upload.css | 22 ++++ examples/assets/flat_router.css | 40 ++++++ examples/assets/links.css | 12 ++ examples/assets/roulette.css | 29 +++++ examples/backgrounded_futures.rs | 27 +++-- examples/calculator.rs | 13 +- examples/calculator_mutable.rs | 44 +++---- examples/clock.rs | 35 ++++-- examples/control_focus.rs | 31 +++-- examples/counter.rs | 53 -------- examples/counters.rs | 52 ++++++++ examples/crm.rs | 43 ++++--- examples/custom_assets.rs | 9 +- examples/custom_html.rs | 14 +-- examples/disabled.rs | 7 +- examples/dog_app.rs | 44 +++++-- examples/dynamic_asset.rs | 12 +- examples/error_handle.rs | 29 ++++- examples/eval.rs | 20 ++- examples/file_explorer.rs | 41 ++++--- examples/file_upload.rs | 34 ++++-- examples/filedragdrop.rs | 17 --- examples/flat_router.rs | 50 +++++--- examples/form.rs | 2 +- examples/future.rs | 38 ++++++ examples/generic_component.rs | 6 +- examples/global.rs | 44 ++++++- examples/hello_world.rs | 11 ++ examples/inputs.rs | 162 ------------------------- examples/link.rs | 62 ++++++---- examples/login_form.rs | 9 +- examples/memo_chain.rs | 5 + examples/multiwindow.rs | 6 + examples/optional_props.rs | 16 ++- examples/overlay.rs | 40 ++++-- examples/{compose.rs => popup.rs} | 14 +-- examples/tasks.rs | 28 ----- examples/textarea.rs | 21 ---- examples/todomvc.rs | 68 +++++++---- examples/video_stream.rs | 4 +- examples/web_component.rs | 23 +++- examples/window_event.rs | 130 +++++++++++++------- examples/window_focus.rs | 7 ++ examples/window_zoom.rs | 6 + packages/desktop/src/webview.rs | 11 +- packages/html/src/events/form.rs | 8 ++ packages/mobile/Cargo.toml | 2 +- packages/router/src/components/link.rs | 11 ++ 57 files changed, 965 insertions(+), 596 deletions(-) create mode 100644 examples/assets/clock.css create mode 100644 examples/assets/counter.css create mode 100644 examples/assets/crm.css create mode 100644 examples/assets/custom_assets.css create mode 100644 examples/assets/dog_app.css create mode 100644 examples/assets/events.css create mode 100644 examples/assets/file_upload.css create mode 100644 examples/assets/flat_router.css create mode 100644 examples/assets/links.css create mode 100644 examples/assets/roulette.css delete mode 100644 examples/counter.rs create mode 100644 examples/counters.rs delete mode 100644 examples/filedragdrop.rs create mode 100644 examples/future.rs delete mode 100644 examples/inputs.rs rename examples/{compose.rs => popup.rs} (82%) delete mode 100644 examples/tasks.rs delete mode 100644 examples/textarea.rs diff --git a/Cargo.lock b/Cargo.lock index bf9bd9902..8969db311 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2565,6 +2565,7 @@ dependencies = [ "serde", "serde_json", "tokio", + "warp", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 86d9133ca..ab16daeb3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -125,6 +125,7 @@ publish = false manganis = { workspace = true, optional = true} reqwest = { version = "0.11.9", features = ["json"], optional = true} http-range = {version = "0.1.5", optional = true } +warp = { version = "0.3.0", optional = true } [dev-dependencies] dioxus = { workspace = true, features = ["router"]} @@ -154,7 +155,7 @@ server = ["dioxus/axum"] default = ["dioxus/desktop"] web = ["dioxus/web"] collect-assets = ["manganis"] -http = ["reqwest", "http-range"] +http = ["reqwest", "http-range", "warp"] [[example]] name = "login_form" diff --git a/examples/all_events.rs b/examples/all_events.rs index 4ceca1a5a..d3c33d131 100644 --- a/examples/all_events.rs +++ b/examples/all_events.rs @@ -1,3 +1,8 @@ +//! This example shows how to listen to all events on a div and log them to the console. +//! +//! The primary demonstration here is the properties on the events themselves, hoping to give you some inspiration +//! on adding interactivity to your own application. + use dioxus::prelude::*; use std::{collections::VecDeque, fmt::Debug, rc::Rc}; @@ -5,41 +10,24 @@ fn main() { launch(app); } -const MAX_EVENTS: usize = 8; - -const CONTAINER_STYLE: &str = r#" - display: flex; - flex-direction: column; - align-items: center; -"#; - -const RECT_STYLE: &str = r#" - background: deepskyblue; - height: 50vh; - width: 50vw; - color: white; - padding: 20px; - margin: 20px; - text-aligh: center; -"#; - fn app() -> Element { - let mut events = use_signal(|| VecDeque::new() as VecDeque>); + // Using a VecDeque so its cheap to pop old events off the front + let mut events = use_signal(|| VecDeque::new()); + // All events and their data implement Debug, so we can re-cast them as Rc instead of their specific type let mut log_event = move |event: Rc| { - let mut events = events.write(); - - if events.len() >= MAX_EVENTS { - events.pop_front(); + // Only store the last 20 events + if events.read().len() >= 20 { + events.write().pop_front(); } - - events.push_back(event); + events.write().push_back(event); }; rsx! { - div { style: "{CONTAINER_STYLE}", + style { {include_str!("./assets/events.css")} } + div { id: "container", // focusing is necessary to catch keyboard events - div { style: "{RECT_STYLE}", tabindex: 0, + div { id: "receiver", tabindex: 0, onmousemove: move |event| log_event(event.data()), onclick: move |event| log_event(event.data()), ondoubleclick: move |event| log_event(event.data()), @@ -57,7 +45,7 @@ fn app() -> Element { "Hover, click, type or scroll to see the info down below" } - div { + div { id: "log", for event in events.read().iter() { div { "{event:?}" } } diff --git a/examples/assets/clock.css b/examples/assets/clock.css new file mode 100644 index 000000000..20216ac05 --- /dev/null +++ b/examples/assets/clock.css @@ -0,0 +1,23 @@ +html body { + margin: 0; + padding: 0; + height: 100vh; + font-family: 'Courier New', Courier, monospace; +} + +#app { + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + height: 100vh; + background-color: plum; + font-size: 6em; + color: aliceblue; +} + +#title { + font-size: 0.5em; + color: black; + margin-bottom: 0.5em; +} diff --git a/examples/assets/counter.css b/examples/assets/counter.css new file mode 100644 index 000000000..1603d58e0 --- /dev/null +++ b/examples/assets/counter.css @@ -0,0 +1,26 @@ +html body { + font-family: Arial, sans-serif; + margin: 20px; + padding: 0; + display: flex; + justify-content: center; + font-size: 2rem; + height: 100vh; + background-color: #f0f0f0; +} + +#controls { + display: flex; + justify-content: center; + align-items: center; + margin-top: 20px; +} + +button { + padding: 5px 10px; + margin: 0 5px; +} + +input { + width: 50px; +} diff --git a/examples/assets/crm.css b/examples/assets/crm.css new file mode 100644 index 000000000..e9d843b43 --- /dev/null +++ b/examples/assets/crm.css @@ -0,0 +1,16 @@ +body { + background-color: #f4f4f4; + font-family: 'Open Sans', sans-serif; + font-size: 14px; + line-height: 1.42857143; + color: #333; + margin: 20px; + padding: 20px; + display: flex; + flex-direction: column; + align-items: center; +} + +.red { + background-color: rgb(202, 60, 60) !important; +} diff --git a/examples/assets/custom_assets.css b/examples/assets/custom_assets.css new file mode 100644 index 000000000..bac30b618 --- /dev/null +++ b/examples/assets/custom_assets.css @@ -0,0 +1,12 @@ +body { + background-color: #f0f0f0; + font-family: Arial, sans-serif; + margin: 0; + padding: 0; + display: flex; + justify-content: center; + align-items: center; + text-align: center; + height: 100vh; + width: 100vw; +} diff --git a/examples/assets/dog_app.css b/examples/assets/dog_app.css new file mode 100644 index 000000000..e69de29bb diff --git a/examples/assets/events.css b/examples/assets/events.css new file mode 100644 index 000000000..36ed4041b --- /dev/null +++ b/examples/assets/events.css @@ -0,0 +1,24 @@ +#container { + display: flex; + flex-direction: column; + align-items: center; +} + +#receiver { + background: deepskyblue; + height: 30vh; + width: 80vw; + color: white; + padding: 20px; + margin: 20px; + text-align: center; +} + +#log { + background: lightgray; + padding: 20px; + margin: 20px; + overflow-y: scroll; + align-items: start; + text-align: left; +} diff --git a/examples/assets/file_upload.css b/examples/assets/file_upload.css new file mode 100644 index 000000000..81aba2638 --- /dev/null +++ b/examples/assets/file_upload.css @@ -0,0 +1,22 @@ +body { + font-family: Arial, sans-serif; + margin: 0; + padding: 0; + background-color: #f4f4f4; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + flex-direction: column; + gap: 20px; +} + +#drop-zone { + border: 2px dashed #ccc; + border-radius: 3px; + padding: 20px; + text-align: center; + cursor: pointer; + margin: 20px; + background-color: rgba(225, 124, 225, 0); +} diff --git a/examples/assets/flat_router.css b/examples/assets/flat_router.css new file mode 100644 index 000000000..126b16b5b --- /dev/null +++ b/examples/assets/flat_router.css @@ -0,0 +1,40 @@ +body { + font-family: Arial, sans-serif; + margin: 20px; + padding: 20px; + background-color: #f4f4f4; + height: 100vh; +} + +nav { + display: flex; + justify-content: space-around; +} + +.nav-btn { + text-decoration: none; + color: black; +} + +a { + padding: 10px; + border: none; + border-radius: 5px; + cursor: pointer; +} + +/* button hover effect */ +a:hover { + background-color: #dd6a6a; +} + +#content { + border: 2px dashed #ccc; + padding-top: 20px; + display: flex; + justify-content: center; + align-items: center; + height: 100%; + flex-direction: column; + gap: 20px; +} diff --git a/examples/assets/links.css b/examples/assets/links.css new file mode 100644 index 000000000..c256a7964 --- /dev/null +++ b/examples/assets/links.css @@ -0,0 +1,12 @@ +#external-links { + display: flex; + flex-direction: column; +} + +#nav { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1rem; + background-color: #f4f4f4; +} diff --git a/examples/assets/roulette.css b/examples/assets/roulette.css new file mode 100644 index 000000000..34077c0c2 --- /dev/null +++ b/examples/assets/roulette.css @@ -0,0 +1,29 @@ +#main { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} + +#roulette-grid { + margin: 10px; + display: grid; + grid-template-columns: repeat(9, 1fr); + grid-gap: 10px; + margin: 0 auto; + padding: 20px; +} + +#roulette-grid>input { + color: white; + font-size: 20px; + width: 50px; +} + +#roulette-grid>input:nth-child(odd) { + background-color: red; +} + +#roulette-grid>input:nth-child(even) { + background-color: black; +} diff --git a/examples/backgrounded_futures.rs b/examples/backgrounded_futures.rs index 2877daab3..160b9c122 100644 --- a/examples/backgrounded_futures.rs +++ b/examples/backgrounded_futures.rs @@ -1,3 +1,12 @@ +//! Backgrounded futures example +//! +//! This showcases how use_future, use_memo, and use_effect will stop running if the component returns early. +//! Generally you should avoid using early returns around hooks since most hooks are not properly designed to +//! handle early returns. However, use_future *does* pause the future when the component returns early, and so +//! hooks that build on top of it like use_memo and use_effect will also pause. +//! +//! This example is more of a demonstration of the behavior than a practical use case, but it's still interesting to see. + use dioxus::prelude::*; fn main() { @@ -10,17 +19,17 @@ fn app() -> Element { let child = use_memo(move || { rsx! { - Child { - count - } + Child { count } } }); rsx! { + // Some toggle/controls to show the child or increment the count button { onclick: move |_| show_child.toggle(), "Toggle child" } button { onclick: move |_| count += 1, "Increment count" } + if show_child() { - {child.cloned()} + {child()} } } } @@ -44,12 +53,12 @@ fn Child(count: Signal) -> Element { } }); - use_effect(move || { - println!("Child count: {}", count()); - }); + use_effect(move || println!("Child count: {}", count())); rsx! { - "hellO!" - {early} + div { + "Child component" + {early} + } } } diff --git a/examples/calculator.rs b/examples/calculator.rs index 88c1e8fa5..f063151ec 100644 --- a/examples/calculator.rs +++ b/examples/calculator.rs @@ -1,7 +1,12 @@ -/* -This example is a simple iOS-style calculator. This particular example can run any platform - Web, Mobile, Desktop. -This calculator version uses React-style state management. All state is held as individual use_states. -*/ +//! Calculator +//! +//! This example is a simple iOS-style calculator. Instead of wrapping the state in a single struct like the +//! `calculate_mutable` example, this example uses several closures to manage actions with the state. Most +//! components will start like this since it's the quickest way to start adding state to your app. The `Signal` type +//! in Dioxus is `Copy` - meaning you don't need to clone it to use it in a closure. +//! +//! Notice how our logic is consolidated into just a few callbacks instead of a single struct. This is a rather organic +//! way to start building state management in Dioxus, and it's a great way to start. use dioxus::events::*; use dioxus::html::input_data::keyboard_types::Key; diff --git a/examples/calculator_mutable.rs b/examples/calculator_mutable.rs index ae1a6f353..f77c53e44 100644 --- a/examples/calculator_mutable.rs +++ b/examples/calculator_mutable.rs @@ -1,38 +1,28 @@ -#![allow(non_snake_case)] - -//! Example: Calculator -//! ------------------- +//! This example showcases a simple calculator using an approach to state management where the state is composed of only +//! a single signal. Since Dioxus implements traditional React diffing, state can be consolidated into a typical Rust struct +//! with methods that take `&mut self`. For many use cases, this is a simple way to manage complex state without wrapping +//! everything in a signal. //! -//! Some components benefit through the use of "Models". Models are a single block of encapsulated state that allow mutative -//! methods to be performed on them. Dioxus exposes the ability to use the model pattern through the "use_model" hook. -//! -//! Models are commonly used in the "Model-View-Component" approach for building UI state. -//! -//! `use_model` is basically just a fancy wrapper around set_state, but saves a "working copy" of the new state behind a -//! RefCell. To modify the working copy, you need to call "get_mut" which returns the RefMut. This makes it easy to write -//! fully encapsulated apps that retain a certain feel of native Rusty-ness. A calculator app is a good example of when this -//! is useful. -//! -//! Do note that "get_mut" returns a `RefMut` (a lock over a RefCell). If two `RefMut`s are held at the same time (ie in a loop) -//! the RefCell will panic and crash. You can use `try_get_mut` or `.modify` to avoid this problem, or just not hold two -//! RefMuts at the same time. +//! Generally, you'll want to split your state into several signals if you have a large application, but for small +//! applications, or focused components, this is a great way to manage state. use dioxus::desktop::tao::dpi::LogicalSize; use dioxus::desktop::{Config, WindowBuilder}; -use dioxus::events::*; use dioxus::html::input_data::keyboard_types::Key; use dioxus::html::MouseEvent; use dioxus::prelude::*; fn main() { - let cfg = Config::new().with_window( - WindowBuilder::new() - .with_title("Calculator Demo") - .with_resizable(false) - .with_inner_size(LogicalSize::new(320.0, 530.0)), - ); - - LaunchBuilder::desktop().with_cfg(cfg).launch(app); + LaunchBuilder::desktop() + .with_cfg( + Config::new().with_window( + WindowBuilder::new() + .with_title("Calculator Demo") + .with_resizable(false) + .with_inner_size(LogicalSize::new(320.0, 530.0)), + ), + ) + .launch(app); } const STYLE: &str = include_str!("./assets/calculator.css"); @@ -109,6 +99,7 @@ struct Calculator { waiting_for_operand: bool, cur_val: f64, } + #[derive(Clone)] enum Operator { Add, @@ -116,6 +107,7 @@ enum Operator { Mul, Div, } + impl Calculator { fn new() -> Self { Calculator { diff --git a/examples/clock.rs b/examples/clock.rs index b48a4053f..9ca9fc201 100644 --- a/examples/clock.rs +++ b/examples/clock.rs @@ -1,3 +1,8 @@ +//! A simple little clock that updates the time every few milliseconds. +//! +//! Neither Rust nor Tokio have an interval function, so we just sleep until the next update. +//! Tokio timer's don't work on WASM though, so you'll need to use a slightly different approach if you're targeting the web. + use dioxus::prelude::*; fn main() { @@ -5,20 +10,36 @@ fn main() { } fn app() -> Element { - let mut count = use_signal(|| 0); + let mut millis = use_signal(|| 0); use_future(move || async move { + // Save our initial timea + let start = std::time::Instant::now(); + loop { - tokio::time::sleep(std::time::Duration::from_millis(10)).await; - count += 1; + // In lieu of an interval, we just sleep until the next update + let now = tokio::time::Instant::now(); + tokio::time::sleep_until(now + std::time::Duration::from_millis(27)).await; + + // Update the time, using a more precise approach of getting the duration since we started the timer + millis.set(start.elapsed().as_millis() as i64); } }); - use_effect(move || { - println!("High-Five counter: {}", count()); - }); + // Format the time as a string + // This is rather cheap so it's fine to leave it in the render function + let time = format!( + "{:02}:{:02}:{:03}", + millis() / 1000 / 60 % 60, + millis() / 1000 % 60, + millis() % 1000 + ); rsx! { - div { "High-Five counter: {count}" } + style { {include_str!("./assets/clock.css")} } + div { id: "app", + div { id: "title", "Carpe diem šŸŽ‰" } + div { id: "clock-display", "{time}" } + } } } diff --git a/examples/control_focus.rs b/examples/control_focus.rs index 1172327ea..95f36e7cf 100644 --- a/examples/control_focus.rs +++ b/examples/control_focus.rs @@ -1,3 +1,8 @@ +//! Managing focus +//! +//! This example shows how to manage focus in a Dioxus application. We implement a "roulette" that focuses on each input +//! in the grid every few milliseconds until the user interacts with the inputs. + use std::rc::Rc; use dioxus::prelude::*; @@ -7,6 +12,7 @@ fn main() { } fn app() -> Element { + // Element data is stored as Rc so we can clone it and pass it around let mut elements = use_signal(Vec::>::new); let mut running = use_signal(|| true); @@ -14,7 +20,7 @@ fn app() -> Element { let mut focused = 0; loop { - tokio::time::sleep(std::time::Duration::from_millis(10)).await; + tokio::time::sleep(std::time::Duration::from_millis(50)).await; if !running() { continue; @@ -31,17 +37,24 @@ fn app() -> Element { }); rsx! { - div { - h1 { "Input Roulette" } + style { {include_str!("./assets/roulette.css")} } + h1 { "Input Roulette" } + button { onclick: move |_| running.toggle(), "Toggle roulette" } + div { id: "roulette-grid", + // Restart the roulette if the user presses escape + onkeydown: move |event| { + if event.code().to_string() == "Escape" { + running.set(true); + } + }, + + // Draw the grid of inputs for i in 0..100 { input { + r#type: "number", value: "{i}", - onmounted: move |cx| { - elements.write().push(cx.data()); - }, - oninput: move |_| { - running.set(false); - } + onmounted: move |cx| elements.write().push(cx.data()), + oninput: move |_| running.set(false), } } } diff --git a/examples/counter.rs b/examples/counter.rs deleted file mode 100644 index a25115b22..000000000 --- a/examples/counter.rs +++ /dev/null @@ -1,53 +0,0 @@ -//! Comparison example with leptos' counter example -//! https://github.com/leptos-rs/leptos/blob/main/examples/counters/src/lib.rs - -use dioxus::prelude::*; - -fn main() { - launch(app); -} - -fn app() -> Element { - let mut counters = use_signal(|| vec![0, 0, 0]); - let sum = use_memo(move || counters.read().iter().copied().sum::()); - - rsx! { - div { - button { onclick: move |_| counters.write().push(0), "Add counter" } - button { - onclick: move |_| { - counters.write().pop(); - }, - "Remove counter" - } - p { "Total: {sum}" } - for i in 0..counters.len() { - Child { i, counters } - } - } - } -} - -#[component] -fn Child(counters: Signal>, i: usize) -> Element { - rsx! { - li { - button { onclick: move |_| counters.write()[i] -= 1, "-1" } - input { - value: "{counters.read()[i]}", - oninput: move |e| { - if let Ok(value) = e.value().parse::() { - counters.write()[i] = value; - } - } - } - button { onclick: move |_| counters.write()[i] += 1, "+1" } - button { - onclick: move |_| { - counters.write().remove(i); - }, - "x" - } - } - } -} diff --git a/examples/counters.rs b/examples/counters.rs new file mode 100644 index 000000000..ebedf2712 --- /dev/null +++ b/examples/counters.rs @@ -0,0 +1,52 @@ +//! A simple counters example that stores a list of items in a vec and then iterates over them. + +use dioxus::prelude::*; + +fn main() { + launch(app); +} + +fn app() -> Element { + // Store the counters in a signal + let mut counters = use_signal(|| vec![0, 0, 0]); + + // Whenver the counters change, sum them up + let sum = use_memo(move || counters.read().iter().copied().sum::()); + + rsx! { + style { {include_str!("./assets/counter.css")} } + + div { id: "controls", + button { onclick: move |_| counters.write().push(0), "Add counter" } + button { onclick: move |_| { counters.write().pop(); }, "Remove counter" } + } + + h3 { "Total: {sum}" } + + // Calling `iter` on a Signal> gives you a GenerationalRef to each entry in the vec + // We enumerate to get the idx of each counter, which we use later to modify the vec + for (i, counter) in counters.iter().enumerate() { + // We need a key to uniquely identify each counter. You really shouldn't be using the index, so we're using + // the counter value itself. + // + // If we used the index, and a counter is removed, dioxus would need to re-write the contents of all following + // counters instead of simply removing the one that was removed + // + // You should use a stable identifier for the key, like a unique id or the value of the counter itself + li { key: "{i}", + button { onclick: move |_| counters.write()[i] -= 1, "-1" } + input { + r#type: "number", + value: "{counter}", + oninput: move |e| { + if let Ok(value) = e.parsed() { + counters.write()[i] = value; + } + } + } + button { onclick: move |_| counters.write()[i] += 1, "+1" } + button { onclick: move |_| { counters.write().remove(i); }, "x" } + } + } + } +} diff --git a/examples/crm.rs b/examples/crm.rs index d9f2c9a38..5360c0480 100644 --- a/examples/crm.rs +++ b/examples/crm.rs @@ -1,4 +1,14 @@ -//! Tiny CRM: A port of the Yew CRM example to Dioxus. +//! Tiny CRM - A simple CRM app using the Router component and global signals +//! +//! This shows how to use the `Router` component to manage different views in your app. It also shows how to use global +//! signals to manage state across the entire app. +//! +//! We could simply pass the state as a prop to each component, but this is a good example of how to use global state +//! in a way that works across pages. +//! +//! We implement a number of important details here too, like focusing inputs, handling form submits, navigating the router, +//! platform-specific configuration, and importing 3rd party CSS libaries. + use dioxus::prelude::*; fn main() { @@ -16,7 +26,7 @@ fn main() { integrity: "sha384-Uu6IeWbM+gzNVXJcM9XV3SohHtmWE+3VGi496jvgX1jyvDTXfdK+rfZc8C1Aehk5", crossorigin: "anonymous" } - style { {r#" .red { background-color: rgb(202, 60, 60) !important; } "#} } + style { {include_str!("./assets/crm.css")} } h1 { "Dioxus CRM Example" } Router:: {} } @@ -32,23 +42,24 @@ struct Client { description: String, } +/// The pages of the app, each with a route #[derive(Routable, Clone)] enum Route { #[route("/")] - ClientList, + List, #[route("/new")] - ClientAdd, + New, #[route("/settings")] Settings, } #[component] -fn ClientList() -> Element { +fn List() -> Element { rsx! { h2 { "List of Clients" } - Link { to: Route::ClientAdd, class: "pure-button pure-button-primary", "Add Client" } + Link { to: Route::New, class: "pure-button pure-button-primary", "Add Client" } Link { to: Route::Settings, class: "pure-button", "Settings" } for client in CLIENTS.read().iter() { div { class: "client", style: "margin-bottom: 50px", @@ -60,12 +71,12 @@ fn ClientList() -> Element { } #[component] -fn ClientAdd() -> Element { +fn New() -> Element { let mut first_name = use_signal(String::new); let mut last_name = use_signal(String::new); let mut description = use_signal(String::new); - let submit_client = move |_: FormEvent| { + let submit_client = move |_| { // Write the client CLIENTS.write().push(Client { first_name: first_name(), @@ -74,7 +85,7 @@ fn ClientAdd() -> Element { }); // And then navigate back to the client list - dioxus::router::router().push(Route::ClientList); + router().push(Route::List); }; rsx! { @@ -87,7 +98,7 @@ fn ClientAdd() -> Element { id: "first_name", r#type: "text", placeholder: "First Nameā€¦", - required: "", + required: true, value: "{first_name}", oninput: move |e| first_name.set(e.value()), @@ -99,19 +110,19 @@ fn ClientAdd() -> Element { } div { class: "pure-control-group", - label { "for": "last_name", "Last Name" } + label { r#for: "last_name", "Last Name" } input { id: "last_name", r#type: "text", placeholder: "Last Nameā€¦", - required: "", + required: true, value: "{last_name}", oninput: move |e| last_name.set(e.value()) } } div { class: "pure-control-group", - label { "for": "description", "Description" } + label { r#for: "description", "Description" } textarea { id: "description", placeholder: "Descriptionā€¦", @@ -122,7 +133,7 @@ fn ClientAdd() -> Element { div { class: "pure-controls", button { r#type: "submit", class: "pure-button pure-button-primary", "Save" } - Link { to: Route::ClientList, class: "pure-button pure-button-primary red", "Cancel" } + Link { to: Route::List, class: "pure-button pure-button-primary red", "Cancel" } } } } @@ -137,10 +148,10 @@ fn Settings() -> Element { class: "pure-button pure-button-primary red", onclick: move |_| { CLIENTS.write().clear(); - dioxus::router::router().push(Route::ClientList); + dioxus::router::router().push(Route::List); }, "Remove all Clients" } - Link { to: Route::ClientList, class: "pure-button", "Go back" } + Link { to: Route::List, class: "pure-button", "Go back" } } } diff --git a/examples/custom_assets.rs b/examples/custom_assets.rs index 54ed831d5..de2cb8648 100644 --- a/examples/custom_assets.rs +++ b/examples/custom_assets.rs @@ -1,3 +1,10 @@ +//! A simple example on how to use assets loading from the filesystem. +//! +//! If the feature "collect-assets" is enabled, the assets will be collected via the dioxus CLI and embedded into the +//! final bundle. This lets you do various useful things like minify, compress, and optimize your assets. +//! +//! We can still use assets without the CLI middleware, but generally larger apps will benefit from it. + use dioxus::prelude::*; #[cfg(not(feature = "collect-assets"))] @@ -14,7 +21,7 @@ fn main() { fn app() -> Element { rsx! { div { - p { "This should show an image:" } + h1 { "This should show an image:" } img { src: ASSET_PATH.to_string() } } } diff --git a/examples/custom_html.rs b/examples/custom_html.rs index 37d1f3808..dbaf31d94 100644 --- a/examples/custom_html.rs +++ b/examples/custom_html.rs @@ -1,28 +1,22 @@ //! This example shows how to use a custom index.html and custom extensions //! to add things like stylesheets, scripts, and third-party JS libraries. -use dioxus::desktop::Config; use dioxus::prelude::*; fn main() { LaunchBuilder::desktop() .with_cfg( - Config::new().with_custom_head("".into()), - ) - .launch(app); - - LaunchBuilder::desktop() - .with_cfg( - Config::new().with_custom_index( + dioxus::desktop::Config::new().with_custom_index( r#" Dioxus app - + +

External HTML

@@ -35,6 +29,6 @@ fn main() { fn app() -> Element { rsx! { - div { h1 { "hello world!" } } + h1 { "Custom HTML!" } } } diff --git a/examples/disabled.rs b/examples/disabled.rs index c7288f352..6aaa70b9d 100644 --- a/examples/disabled.rs +++ b/examples/disabled.rs @@ -1,3 +1,7 @@ +//! A simple demonstration of how to set attributes on buttons to disable them. +//! +//! This example also showcases the shorthand syntax for attributes, and how signals themselves implement IntoAttribute + use dioxus::prelude::*; fn main() { @@ -8,13 +12,12 @@ fn app() -> Element { let mut disabled = use_signal(|| false); rsx! { - div { + div { style: "text-align: center; margin: 20px; display: flex; flex-direction: column; align-items: center;", button { onclick: move |_| disabled.toggle(), "click to " if disabled() { "enable" } else { "disable" } " the lower button" } - button { disabled, "lower button" } } } diff --git a/examples/dog_app.rs b/examples/dog_app.rs index 4d3005f97..5e20e30c2 100644 --- a/examples/dog_app.rs +++ b/examples/dog_app.rs @@ -1,3 +1,12 @@ +//! This example demonstrates a simple app that fetches a list of dog breeds and displays a random dog. +//! +//! The app uses the `use_signal` and `use_resource` hooks to manage state and fetch data from the Dog API. +//! `use_resource` is basically an async version of use_memo - it will track dependencies between .await points +//! and then restart the future if any of the dependencies change. +//! +//! You should generally throttle requests to an API - either client side or server side. This example doesn't do that +//! since it's unlikely the user will rapidly cause new fetches, but it's something to keep in mind. + use dioxus::prelude::*; use std::collections::HashMap; @@ -6,8 +15,18 @@ fn main() { } fn app() -> Element { + // Breed is a signal that will be updated when the user clicks a breed in the list + // `deerhound` is just a default that we know will exist. We could also use a `None` instead let mut breed = use_signal(|| "deerhound".to_string()); + + // Fetch the list of breeds from the Dog API + // Since there are no dependencies, this will never restart let breed_list = use_resource(move || async move { + #[derive(Debug, Clone, PartialEq, serde::Deserialize)] + struct ListBreeds { + message: HashMap>, + } + let list = reqwest::get("https://dog.ceo/api/breeds/list/all") .await .unwrap() @@ -19,7 +38,7 @@ fn app() -> Element { }; rsx! { - for cur_breed in breeds.message.keys().take(10).cloned() { + for cur_breed in breeds.message.keys().take(20).cloned() { li { key: "{cur_breed}", button { onclick: move |_| breed.set(cur_breed.clone()), "{cur_breed}" @@ -29,22 +48,31 @@ fn app() -> Element { } }); + // We can use early returns in dioxus! + // Traditional signal-based libraries can't do this since the scope is by default non-reactive let Some(breed_list) = breed_list() else { return rsx! { "loading breeds..." }; }; rsx! { + style { {include_str!("./assets/dog_app.css")} } h1 { "Select a dog breed!" } div { height: "500px", display: "flex", - ul { flex: "50%", {breed_list} } - div { flex: "50%", BreedPic { breed } } + ul { width: "100px", {breed_list} } + div { flex: 1, BreedPic { breed } } } } } #[component] fn BreedPic(breed: Signal) -> Element { + // This resource will restart whenever the breed changes let mut fut = use_resource(move || async move { + #[derive(serde::Deserialize, Debug)] + struct DogApi { + message: String, + } + reqwest::get(format!("https://dog.ceo/api/breed/{breed}/images/random")) .await .unwrap() @@ -61,13 +89,3 @@ fn BreedPic(breed: Signal) -> Element { None => rsx! { "loading image..." }, } } - -#[derive(Debug, Clone, PartialEq, serde::Deserialize)] -struct ListBreeds { - message: HashMap>, -} - -#[derive(serde::Deserialize, Debug)] -struct DogApi { - message: String, -} diff --git a/examples/dynamic_asset.rs b/examples/dynamic_asset.rs index 3f20c30e7..cf43dd14c 100644 --- a/examples/dynamic_asset.rs +++ b/examples/dynamic_asset.rs @@ -1,3 +1,9 @@ +//! This example shows how to load in custom assets with the use_asset_handler hook. +//! +//! This hook is currently only available on desktop and allows you to intercept any request made by the webview +//! and respond with your own data. You could use this to load in custom videos, streams, stylesheets, images, +//! or any asset that isn't known at compile time. + use dioxus::desktop::{use_asset_handler, wry::http::Response}; use dioxus::prelude::*; @@ -16,8 +22,8 @@ fn app() -> Element { }); rsx! { - div { - img { src: "/logos/logo.png" } - } + style { {include_str!("./assets/custom_assets.css")} } + h1 { "Dynamic Assets" } + img { src: "/logos/logo.png" } } } diff --git a/examples/error_handle.rs b/examples/error_handle.rs index 02410629c..ccafe0642 100644 --- a/examples/error_handle.rs +++ b/examples/error_handle.rs @@ -1,3 +1,10 @@ +//! This example showcases how to use the ErrorBoundary component to handle errors in your app. +//! +//! The ErrorBoundary component is a special component that can be used to catch panics and other errors that occur. +//! By default, Dioxus will catch panics during rendering, async, and handlers, and bubble them up to the nearest +//! error boundary. If no error boundary is present, it will be caught by the root error boundary and the app will +//! render the error message as just a string. + use dioxus::{dioxus_core::CapturedError, prelude::*}; fn main() { @@ -7,7 +14,10 @@ fn main() { fn app() -> Element { rsx! { ErrorBoundary { - handle_error: |error: CapturedError| rsx! {"Found error {error}"}, + handle_error: |error: CapturedError| rsx! { + h1 { "An error occurred" } + pre { "{error:#?}" } + }, DemoC { x: 1 } } } @@ -15,11 +25,18 @@ fn app() -> Element { #[component] fn DemoC(x: i32) -> Element { - let result = Err("Error"); - - result.throw()?; - rsx! { - h1 { "{x}" } + h1 { "Error handler demo" } + button { + onclick: move |_| { + // Create an error + let result: Result = Err("Error"); + + // And then call `throw` on it. The `throw` method is given by the `Throw` trait which is automatically + // imported via the prelude. + _ = result.throw(); + }, + "Click to throw an error" + } } } diff --git a/examples/eval.rs b/examples/eval.rs index 99d701022..9e0646147 100644 --- a/examples/eval.rs +++ b/examples/eval.rs @@ -1,3 +1,8 @@ +//! This example shows how to use the `eval` function to run JavaScript code in the webview. +//! +//! Eval will only work with renderers that support javascript - so currently only the web and desktop/mobile renderers +//! that use a webview. Native renderers will throw "unsupported" errors when calling `eval`. + use dioxus::prelude::*; fn main() { @@ -5,20 +10,33 @@ fn main() { } fn app() -> Element { + // Create a future that will resolve once the javascript has been succesffully executed. let future = use_resource(move || async move { + // Wait a little bit just to give the appearance of a loading screen + tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; + + // The `eval` is available in the prelude - and simply takes a block of JS. + // Dioxus' eval is interesting since it allows sending messages to and from the JS code using the `await dioxus.recv()` + // builtin function. This allows you to create a two-way communication channel between Rust and JS. let mut eval = eval( r#" dioxus.send("Hi from JS!"); let msg = await dioxus.recv(); console.log(msg); - return "hello world"; + return "hi from JS!"; "#, ) .unwrap(); + // Send a message to the JS code. eval.send("Hi from Rust!".into()).unwrap(); + + // Our line on the JS side will log the message and then return "hello world". let res = eval.recv().await.unwrap(); + + // This will print "Hi from JS!" and "Hi from Rust!". println!("{:?}", eval.await); + res }); diff --git a/examples/file_explorer.rs b/examples/file_explorer.rs index 958aec9ac..ae0ab65ca 100644 --- a/examples/file_explorer.rs +++ b/examples/file_explorer.rs @@ -1,12 +1,9 @@ //! Example: File Explorer -//! ------------------------- //! //! This is a fun little desktop application that lets you explore the file system. //! //! This example is interesting because it's mixing filesystem operations and GUI, which is typically hard for UI to do. -//! -//! It also uses `use_ref` to maintain a model, rather than `use_state`. That way, -//! we dont need to clutter our code with `read` commands. +//! We store the state entirely in a single signal, making the explorer logic fairly easy to reason about. use dioxus::desktop::{Config, WindowBuilder}; use dioxus::prelude::*; @@ -37,25 +34,25 @@ fn app() -> Element { } style { "{_STYLE}" } main { - {files.read().path_names.iter().enumerate().map(|(dir_id, path)| { - let path_end = path.split('/').last().unwrap_or(path.as_str()); - rsx! ( - div { - class: "folder", - key: "{path}", - i { class: "material-icons", - onclick: move |_| files.write().enter_dir(dir_id), - if path_end.contains('.') { - "description" - } else { - "folder" + for (dir_id, path) in files.read().path_names.iter().enumerate() { + { + let path_end = path.split('/').last().unwrap_or(path.as_str()); + rsx! { + div { class: "folder", key: "{path}", + i { class: "material-icons", + onclick: move |_| files.write().enter_dir(dir_id), + if path_end.contains('.') { + "description" + } else { + "folder" + } + p { class: "cooltip", "0 folders / 0 files" } } - p { class: "cooltip", "0 folders / 0 files" } + h1 { "{path_end}" } } - h1 { "{path_end}" } } - ) - })}, + } + } if let Some(err) = files.read().err.as_ref() { div { code { "{err}" } @@ -67,6 +64,10 @@ fn app() -> Element { } } +/// A simple little struct to hold the file explorer state +/// +/// We don't use any fancy signals or memoization here - Dioxus is so fast that even a file explorer can be done with a +/// single signal. struct Files { path_stack: Vec, path_names: Vec, diff --git a/examples/file_upload.rs b/examples/file_upload.rs index c98221c16..280144e44 100644 --- a/examples/file_upload.rs +++ b/examples/file_upload.rs @@ -1,13 +1,17 @@ -#![allow(non_snake_case)] +//! This example shows how to use the `file` methods on FormEvent and DragEvent to handle file uploads and drops. +//! +//! Dioxus intercepts these events and provides a Rusty interface to the file data. Since we want this interface to +//! be crossplatform, + use dioxus::html::HasFileData; use dioxus::prelude::*; use tokio::time::sleep; fn main() { - launch(App); + launch(app); } -fn App() -> Element { +fn app() -> Element { let mut enable_directory_upload = use_signal(|| false); let mut files_uploaded = use_signal(|| Vec::new() as Vec); @@ -31,12 +35,16 @@ fn App() -> Element { }; rsx! { + style { {include_str!("./assets/file_upload.css")} } + + input { + r#type: "checkbox", + id: "directory-upload", + checked: enable_directory_upload, + oninput: move |evt| enable_directory_upload.set(evt.checked()), + }, label { - input { - r#type: "checkbox", - checked: enable_directory_upload, - oninput: move |evt| enable_directory_upload.set(evt.checked()), - }, + r#for: "directory-upload", "Enable directory upload" } @@ -47,16 +55,18 @@ fn App() -> Element { directory: enable_directory_upload, onchange: upload_files, } + div { - width: "100px", - height: "100px", - border: "1px solid black", + // cheating with a little bit of JS... + "ondragover": "this.style.backgroundColor='#88FF88';", + "ondragleave": "this.style.backgroundColor='#FFFFFF';", + + id: "drop-zone", prevent_default: "ondrop dragover dragenter", ondrop: handle_file_drop, ondragover: move |event| event.stop_propagation(), "Drop files here" } - ul { for file in files_uploaded.read().iter() { li { "{file}" } diff --git a/examples/filedragdrop.rs b/examples/filedragdrop.rs deleted file mode 100644 index cf4f1742a..000000000 --- a/examples/filedragdrop.rs +++ /dev/null @@ -1,17 +0,0 @@ -use dioxus::desktop::Config; -use dioxus::prelude::*; - -fn main() { - LaunchBuilder::desktop() - .with_cfg(Config::new().with_file_drop_handler(|_w, e| { - println!("{e:?}"); - true - })) - .launch(app) -} - -fn app() -> Element { - rsx!( - div { h1 { "drag a file here and check your console" } } - ) -} diff --git a/examples/flat_router.rs b/examples/flat_router.rs index a981e4fb0..4af9ff85c 100644 --- a/examples/flat_router.rs +++ b/examples/flat_router.rs @@ -1,8 +1,18 @@ +//! This example shows how to use the `Router` component to create a simple navigation system. +//! The more complex router example uses all of the router features, while this simple exmaple showcases +//! just the `Layout` and `Route` features. +//! +//! Layouts let you wrap chunks of your app with a component. This is useful for things like a footers, heeaders, etc. +//! Routes are enum variants with that match the name of a component in scope. This way you can create a new route +//! in your app simply by adding the variant to the enum and creating a new component with the same name. You can +//! override this of course. + use dioxus::prelude::*; fn main() { launch(|| { rsx! { + style { {include_str!("./assets/flat_router.css")} } Router:: {} } }) @@ -11,7 +21,7 @@ fn main() { #[derive(Routable, Clone)] #[rustfmt::skip] enum Route { - #[layout(Footer)] + #[layout(Footer)] // wrap the entire app in a footer #[route("/")] Home {}, @@ -28,45 +38,47 @@ enum Route { #[component] fn Footer() -> Element { rsx! { - Outlet:: {} - p { "----" } nav { - style { {STYLE} } Link { to: Route::Home {}, class: "nav-btn", "Home" } Link { to: Route::Games {}, class: "nav-btn", "Games" } Link { to: Route::Play {}, class: "nav-btn", "Play" } Link { to: Route::Settings {}, class: "nav-btn", "Settings" } } + div { id: "content", + Outlet:: {} + } } } #[component] fn Home() -> Element { - rsx!("Home") + rsx!( + h1 { "Home" } + p { "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." } + ) } #[component] fn Games() -> Element { - rsx!("Games") + rsx!( + h1 { "Games" } + // Dummy text that talks about video games + p { "Lorem games are sit amet Sed do eiusmod tempor et dolore magna aliqua." } + ) } #[component] fn Play() -> Element { - rsx!("Play") + rsx!( + h1 { "Play" } + p { "Always play with your full heart adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." } + ) } #[component] fn Settings() -> Element { - rsx!("Settings") + rsx!( + h1 { "Settings" } + p { "Settings are consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." } + ) } - -const STYLE: &str = r#" - nav { - display: flex; - justify-content: space-around; - } - .nav-btn { - text-decoration: none; - color: black; - } -"#; diff --git a/examples/form.rs b/examples/form.rs index 897587daa..7da92e65c 100644 --- a/examples/form.rs +++ b/examples/form.rs @@ -1,7 +1,7 @@ //! Forms //! //! Dioxus forms deviate slightly from html, automatically returning all named inputs -//! in the "values" field +//! in the "values" field. use dioxus::prelude::*; diff --git a/examples/future.rs b/examples/future.rs new file mode 100644 index 000000000..130d526b2 --- /dev/null +++ b/examples/future.rs @@ -0,0 +1,38 @@ +//! A simple example that shows how to use the use_future hook to run a background task. +//! +//! use_future assumes your future will never complete - it won't return a value. +//! If you want to return a value, use use_resource instead. + +use dioxus::prelude::*; +use std::time::Duration; + +fn main() { + launch_desktop(app); +} + +fn app() -> Element { + let mut count = use_signal(|| 0); + + // use_future will run the future + use_future(move || async move { + loop { + tokio::time::sleep(Duration::from_millis(200)).await; + count += 1; + } + }); + + // We can also spawn futures from effects, handlers, or other futures + use_effect(move || { + spawn(async move { + tokio::time::sleep(Duration::from_secs(5)).await; + count.set(100); + }); + }); + + rsx! { + div { + h1 { "Current count: {count}" } + button { onclick: move |_| count.set(0), "Reset the count" } + } + } +} diff --git a/examples/generic_component.rs b/examples/generic_component.rs index 06513aa02..3261ea13b 100644 --- a/examples/generic_component.rs +++ b/examples/generic_component.rs @@ -1,6 +1,10 @@ -use std::fmt::Display; +//! This example demonstrates how to create a generic component in Dioxus. +//! +//! Generic components can be useful when you want to create a component that renders differently depending on the type +//! of data it receives. In this particular example, we're just using a type that implements `Display` and `PartialEq`, use dioxus::prelude::*; +use std::fmt::Display; fn main() { launch_desktop(app); diff --git a/examples/global.rs b/examples/global.rs index f3659845c..a041d211d 100644 --- a/examples/global.rs +++ b/examples/global.rs @@ -1,6 +1,9 @@ -//! Example: README.md showcase +//! Example: Global signals and memos //! -//! The example from the README.md. +//! This example demonstrates how to use global signals and memos to share state across your app. +//! Global signals are simply signals that live on the root of your app and are accessible from anywhere. To access a +//! global signal, simply use its methods like a regular signal. Calls to `read` and `write` will be forwarded to the +//! signal at the root of your app using the `static`'s address. use dioxus::prelude::*; @@ -13,8 +16,43 @@ static DOUBLED_COUNT: GlobalMemo = Signal::global_memo(|| COUNT() * 2); fn app() -> Element { rsx! { - h1 { "{COUNT} x 2 = {DOUBLED_COUNT}" } + style { {include_str!("./assets/counter.css")} } + Increment {} + Decrement {} + Reset {} + Display {} + } +} + +#[component] +fn Increment() -> Element { + rsx! { button { onclick: move |_| *COUNT.write() += 1, "Up high!" } + } +} + +#[component] +fn Decrement() -> Element { + rsx! { button { onclick: move |_| *COUNT.write() -= 1, "Down low!" } } } + +#[component] +fn Display() -> Element { + rsx! { + p { "Count: ", "{COUNT}" } + p { "Doubled: ", "{DOUBLED_COUNT}" } + } +} + +#[component] +fn Reset() -> Element { + // Not all write methods are availale on global signals since `write` requires a mutable reference. In these cases, + // We can simply pull out the actual signal using the signal() method. + let mut as_signal = use_hook(|| COUNT.signal()); + + rsx! { + button { onclick: move |_| as_signal.set(0), "Reset" } + } +} diff --git a/examples/hello_world.rs b/examples/hello_world.rs index da046a634..ee33c2230 100644 --- a/examples/hello_world.rs +++ b/examples/hello_world.rs @@ -1,3 +1,14 @@ +//! The simplest example of a Dioxus app. +//! +//! In this example we: +//! - import a number of important items from the prelude (launch, Element, rsx, div, etc.) +//! - define a main function that calls the launch function with our app function +//! - define an app function that returns a div element with the text "Hello, world!" +//! +//! The `launch` function is the entry point for all Dioxus apps. It takes a function that returns an Element. This function +//! calls "launch" on the currently-configured renderer you have. So if the `web` feature is enabled, it will launch a web +//! app, and if the `desktop` feature is enabled, it will launch a desktop app. + use dioxus::prelude::*; fn main() { diff --git a/examples/inputs.rs b/examples/inputs.rs deleted file mode 100644 index 66d4fe696..000000000 --- a/examples/inputs.rs +++ /dev/null @@ -1,162 +0,0 @@ -//! This example roughly shows how events are serialized into Rust from JavaScript. -//! -//! There is some conversion happening when input types are checkbox/radio/select/textarea etc. - -use dioxus::prelude::*; - -fn main() { - launch_desktop(app); -} - -const FIELDS: &[(&str, &str)] = &[ - ("button", "Click me!"), - ("checkbox", "CHECKBOX"), - ("color", ""), - ("date", ""), - ("datetime-local", ""), - ("email", ""), - ("file", ""), - ("image", ""), - ("number", ""), - ("password", ""), - ("radio", ""), - ("range", ""), - ("reset", ""), - ("search", ""), - ("submit", ""), - ("tel", ""), - ("text", ""), - ("time", ""), - ("url", ""), - // less supported things - ("hidden", ""), - ("month", ""), // degrades to text most of the time, but works properly as "value'" - ("week", ""), // degrades to text most of the time -]; - -fn app() -> Element { - rsx! { - div { margin_left: "30px", - {select_example()}, - div { - // handling inputs on divs will catch all input events below - // so the value of our input event will be either huey, dewey, louie, or true/false (because of the checkboxe) - // be mindful in grouping inputs together, as they will all be handled by the same event handler - oninput: move |evt| println!("{evt:?}"), - div { - input { - id: "huey", - r#type: "radio", - value: "huey", - checked: true, - name: "drone", - } - label { - r#for: "huey", - "Huey" - } - } - div { - input { - id: "dewey", - r#type: "radio", - value: "dewey", - name: "drone", - } - label { r#for: "dewey", "Dewey" } - } - div { - input { - id: "louie", - value: "louie", - r#type: "radio", - name: "drone", - } - label { - r#for: "louie", - "Louie" - } - } - div { - input { - id: "groovy", - value: "groovy", - r#type: "checkbox", - name: "drone", - } - label { - r#for: "groovy", - "groovy" - } - } - } - - // elements with driven values will preventdefault automatically. - // you can disable this override with preventdefault: false - div { - input { - id: "pdf", - value: "pdf", - name: "pdf", - r#type: "checkbox", - oninput: move |evt| { - println!("{evt:?}"); - }, - } - label { - r#for: "pdf", - "pdf" - } - } - - for (field, value) in FIELDS.iter() { - div { - input { - id: "{field}", - name: "{field}", - r#type: "{field}", - value: "{value}", - oninput: move |evt: FormEvent| { - println!("{evt:?}"); - }, - } - label { - r#for: "{field}", - "{field} element" - } - br {} - } - } - } - } -} - -fn select_example() -> Element { - rsx! { - div { - select { - id: "selection", - name: "selection", - multiple: true, - oninput: move |evt| println!("{evt:?}"), - option { - value: "Option 1", - label: "Option 1", - } - option { - value: "Option 2", - label: "Option 2", - selected: true, - }, - option { - value: "Option 3", - label: "Option 3", - } - } - label { - r#for: "selection", - "select element" - } - } - } -} diff --git a/examples/link.rs b/examples/link.rs index f11408c2e..ac853b3f6 100644 --- a/examples/link.rs +++ b/examples/link.rs @@ -1,24 +1,21 @@ +//! How to use links in Dioxus +//! +//! The `router` crate gives us a `Link` component which is a much more powerful version of the standard HTML link. +//! However, you can use the traditional `` tag if you want to build your own `Link` component. +//! +//! The `Link` component integrates with the Router and is smart enough to detect if the link is internal or external. +//! It also allows taking any `Route` as a target, making your links typesafe + use dioxus::prelude::*; fn main() { - launch_desktop(App); + launch_desktop(app); } -#[component] -fn App() -> Element { +fn app() -> Element { rsx! ( - div { - p { a { href: "http://dioxuslabs.com/", "Default link - links outside of your app" } } - p { - a { - href: "http://dioxuslabs.com/", - prevent_default: "onclick", - onclick: |_| println!("Hello Dioxus"), - "Custom event link - links inside of your app" - } - } - } - div { Router:: {} } + style { {include_str!("./assets/links.css")} } + Router:: {} ) } @@ -28,6 +25,10 @@ enum Route { #[layout(Header)] #[route("/")] Home {}, + + #[route("/default-links")] + DefaultLinks {}, + #[route("/settings")] Settings {}, } @@ -36,13 +37,10 @@ enum Route { fn Header() -> Element { rsx! { h1 { "Your app here" } - ul { - li { - Link { to: Route::Home {}, "home" } - } - li { - Link { to: Route::Settings {}, "settings" } - } + nav { id: "nav", + Link { to: Route::Home {}, "home" } + Link { to: Route::DefaultLinks {}, "default links" } + Link { to: Route::Settings {}, "settings" } } Outlet:: {} } @@ -57,3 +55,23 @@ fn Home() -> Element { fn Settings() -> Element { rsx!( h1 { "Settings" } ) } + +#[component] +fn DefaultLinks() -> Element { + rsx! { + // Just some default links + div { id: "external-links", + // This link will open in a webbrowser + a { href: "http://dioxuslabs.com/", "Default link - links outside of your app" } + + // This link will do nothing - we're preventing the default behavior + // It will just log "Hello Dioxus" to the console + a { + href: "http://dioxuslabs.com/", + prevent_default: "onclick", + onclick: |_| println!("Hello Dioxus"), + "Custom event link - links inside of your app" + } + } + } +} diff --git a/examples/login_form.rs b/examples/login_form.rs index 220472886..c813e1434 100644 --- a/examples/login_form.rs +++ b/examples/login_form.rs @@ -1,5 +1,10 @@ -//! This example demonstrates the following: -//! Futures in a callback, Router, and Forms +//! Implementing a login form +//! +//! This example demonstrates how to implement a login form using Dioxus desktop. Since forms typically navigate the +//! page on submit, we need to intercept the onsubmit event and send a request to a server. On the web, we could +//! just leave the submit action` as is, but on desktop, we need to handle the form submission ourselves. +//! +//! Todo: actually spin up a server and run the login flow. Login is way more complex than a form override :) use dioxus::prelude::*; diff --git a/examples/memo_chain.rs b/examples/memo_chain.rs index 56c3aad71..bbcca9954 100644 --- a/examples/memo_chain.rs +++ b/examples/memo_chain.rs @@ -1,3 +1,8 @@ +//! This example shows how you can chain memos together to create a tree of memoized values. +//! +//! Memos will also pause when their parent component pauses, so if you have a memo that depends on a signal, and the +//! signal pauses, the memo will pause too. + use dioxus::prelude::*; fn main() { diff --git a/examples/multiwindow.rs b/examples/multiwindow.rs index 974e024d7..978cb2bad 100644 --- a/examples/multiwindow.rs +++ b/examples/multiwindow.rs @@ -1,3 +1,9 @@ +//! Multiwindow example +//! +//! This exmaple shows how to implement a simple multiwindow application using dioxus. +//! This works by spawning a new window when the user clicks a button. We have to build a new virtualdom which has its +//! own context, root elements, etc. + use dioxus::prelude::*; fn main() { diff --git a/examples/optional_props.rs b/examples/optional_props.rs index 75ba1e3be..d4bf5755c 100644 --- a/examples/optional_props.rs +++ b/examples/optional_props.rs @@ -1,8 +1,7 @@ -#![allow(non_snake_case)] - -//! Example: README.md showcase +//! Optional props //! -//! The example from the README.md. +//! This example demonstrates how to use optional props in your components. The `Button` component has several props, +//! and we use a variety of attributes to set them. use dioxus::prelude::*; @@ -12,19 +11,27 @@ fn main() { fn app() -> Element { rsx! { + // We can set some of the props, and the rest will be filled with their default values + // By default `c` can take a `None` value, but `d` is required to wrap a `Some` value Button { a: "asd".to_string(), + // b can be omitted, and it will be filled with its default value c: "asd".to_string(), d: Some("asd".to_string()), e: Some("asd".to_string()), } + Button { a: "asd".to_string(), b: "asd".to_string(), + + // We can omit the `Some` on `c` since Dioxus automatically transforms Option into optional c: "asd".to_string(), d: Some("asd".to_string()), e: "asd".to_string(), } + + // `b` and `e` are ommitted Button { a: "asd".to_string(), c: "asd".to_string(), @@ -51,6 +58,7 @@ struct ButtonProps { type SthElse = Option; +#[allow(non_snake_casea)] fn Button(props: ButtonProps) -> Element { rsx! { button { diff --git a/examples/overlay.rs b/examples/overlay.rs index a4a927558..15795fc3e 100644 --- a/examples/overlay.rs +++ b/examples/overlay.rs @@ -1,4 +1,14 @@ -use dioxus::desktop::{tao::dpi::PhysicalPosition, LogicalSize, WindowBuilder}; +//! This example demonstrates how to create an overlay window with dioxus. +//! +//! Basically, we just create a new window with a transparent background and no decorations, size it to the screen, and +//! then we can draw whatever we want on it. In this case, we're drawing a simple overlay with a draggable header. +//! +//! We also add a global shortcut to toggle the overlay on and off, so you could build a raycast-type app with this. + +use dioxus::desktop::{ + tao::dpi::PhysicalPosition, use_global_shortcut, use_wry_event_handler, LogicalSize, + WindowBuilder, +}; use dioxus::prelude::*; fn main() { @@ -6,21 +16,27 @@ fn main() { } fn app() -> Element { - rsx! { - div { - width: "100%", - height: "100%", - background_color: "red", - border: "1px solid black", + let mut show_overlay = use_signal(|| true); + use_global_shortcut("cmd+g", move || show_overlay.toggle()); + + rsx! { + if show_overlay() { div { width: "100%", - height: "10px", - background_color: "black", - onmousedown: move |_| dioxus::desktop::window().drag(), - } + height: "100%", + background_color: "red", + border: "1px solid black", - "This is an overlay!" + div { + width: "100%", + height: "10px", + background_color: "black", + onmousedown: move |_| dioxus::desktop::window().drag(), + } + + "This is an overlay!" + } } } } diff --git a/examples/compose.rs b/examples/popup.rs similarity index 82% rename from examples/compose.rs rename to examples/popup.rs index 20ee392d0..131d536ed 100644 --- a/examples/compose.rs +++ b/examples/popup.rs @@ -1,9 +1,8 @@ //! This example shows how to create a popup window and send data back to the parent window. - -use std::rc::Rc; +//! Currently Dioxus doesn't support nested renderers, hence the need to create popups as separate windows. use dioxus::prelude::*; -use futures_util::StreamExt; +use std::rc::Rc; fn main() { launch_desktop(app); @@ -14,6 +13,7 @@ fn app() -> Element { // Wait for responses to the compose channel, and then push them to the emails_sent signal. let handle = use_coroutine(|mut rx: UnboundedReceiver| async move { + use futures_util::StreamExt; while let Some(message) = rx.next().await { emails_sent.write().push(message); } @@ -22,7 +22,7 @@ fn app() -> Element { let open_compose_window = move |_evt: MouseEvent| { let tx = handle.tx(); dioxus::desktop::window().new_window( - VirtualDom::new_with_props(compose, Rc::new(move |s| tx.unbounded_send(s).unwrap())), + VirtualDom::new_with_props(popup, Rc::new(move |s| tx.unbounded_send(s).unwrap())), Default::default(), ); }; @@ -41,21 +41,19 @@ fn app() -> Element { } } -fn compose(send: Rc) -> Element { +fn popup(send: Rc) -> Element { let mut user_input = use_signal(String::new); rsx! { div { h1 { "Compose a new email" } - button { onclick: move |_| { send(user_input.cloned()); dioxus::desktop::window().close(); }, - "Click to send" + "Send" } - input { oninput: move |e| user_input.set(e.value()), value: "{user_input}" } } } diff --git a/examples/tasks.rs b/examples/tasks.rs deleted file mode 100644 index 887668fb9..000000000 --- a/examples/tasks.rs +++ /dev/null @@ -1,28 +0,0 @@ -//! Example: README.md showcase -//! -//! The example from the README.md. - -use dioxus::prelude::*; -use std::time::Duration; - -fn main() { - launch_desktop(app); -} - -fn app() -> Element { - let mut count = use_signal(|| 0); - - use_future(move || async move { - loop { - tokio::time::sleep(Duration::from_millis(1000)).await; - count += 1; - } - }); - - rsx! { - div { - h1 { "Current count: {count}" } - button { onclick: move |_| count.set(0), "Reset the count" } - } - } -} diff --git a/examples/textarea.rs b/examples/textarea.rs deleted file mode 100644 index 3ad2143ec..000000000 --- a/examples/textarea.rs +++ /dev/null @@ -1,21 +0,0 @@ -// How to use textareas - -use dioxus::prelude::*; - -fn main() { - launch_desktop(app); -} - -fn app() -> Element { - let mut model = use_signal(|| String::from("asd")); - - rsx! { - textarea { - class: "border", - rows: "10", - cols: "80", - value: "{model}", - oninput: move |e| model.set(e.value().clone()), - } - } -} diff --git a/examples/todomvc.rs b/examples/todomvc.rs index 25a124c71..2af2daf03 100644 --- a/examples/todomvc.rs +++ b/examples/todomvc.rs @@ -1,4 +1,5 @@ -#![allow(non_snake_case)] +//! The typical TodoMVC app, implemented in Dioxus. + use dioxus::prelude::*; use dioxus_elements::input_data::keyboard_types::Key; use std::collections::HashMap; @@ -21,15 +22,21 @@ struct TodoItem { contents: String, } -const STYLE: &str = include_str!("./assets/todomvc.css"); - fn app() -> Element { + // We store the todos in a HashMap in a Signal. + // Each key is the id of the todo, and the value is the todo itself. let mut todos = use_signal(HashMap::::new); + let filter = use_signal(|| FilterState::All); + // We use a simple memoized signal to calculate the number of active todos. + // Whenever the todos change, the active_todo_count will be recalculated. let active_todo_count = use_memo(move || todos.read().values().filter(|item| !item.checked).count()); + // We use a memoized signal to filter the todos based on the current filter state. + // Whenever the todos or filter change, the filtered_todos will be recalculated. + // Note that we're only storing the IDs of the todos, not the todos themselves. let filtered_todos = use_memo(move || { let mut filtered_todos = todos .read() @@ -47,6 +54,8 @@ fn app() -> Element { filtered_todos }); + // Toggle all the todos to the opposite of the current state. + // If all todos are checked, uncheck them all. If any are unchecked, check them all. let toggle_all = move |_| { let check = active_todo_count() != 0; for (_, item) in todos.write().iter_mut() { @@ -55,8 +64,8 @@ fn app() -> Element { }; rsx! { + style { {include_str!("./assets/todomvc.css")} } section { class: "todoapp", - style { {STYLE} } TodoHeader { todos } section { class: "main", if !todos.read().is_empty() { @@ -69,17 +78,29 @@ fn app() -> Element { } label { r#for: "toggle-all" } } + + // Render the todos using the filtered_todos signal + // We pass the ID into the TodoEntry component so it can access the todo from the todos signal. + // Since we store the todos in a signal too, we also need to send down the todo list ul { class: "todo-list", for id in filtered_todos() { TodoEntry { key: "{id}", id, todos } } } + + // We only show the footer if there are todos. if !todos.read().is_empty() { ListFooter { active_todo_count, todos, filter } } } } - PageFooter {} + + // A simple info footer + footer { class: "info", + p { "Double-click to edit a todo" } + p { "Created by " a { href: "http://github.com/jkelleyrtp/", "jkelleyrtp" } } + p { "Part of " a { href: "http://todomvc.com", "TodoMVC" } } + } } } @@ -117,21 +138,34 @@ fn TodoHeader(mut todos: Signal>) -> Element { } } +/// A single todo entry +/// This takes the ID of the todo and the todos signal as props +/// We can use these together to memoize the todo contents and checked state #[component] fn TodoEntry(mut todos: Signal>, id: u32) -> Element { let mut is_editing = use_signal(|| false); + + // To avoid re-rendering this component when the todo list changes, we isolate our reads to memos + // This way, the component will only re-render when the contents of the todo change, or when the editing state changes. + // This does involve taking a local clone of the todo contents, but it allows us to prevent this component from re-rendering let checked = use_memo(move || todos.read().get(&id).unwrap().checked); let contents = use_memo(move || todos.read().get(&id).unwrap().contents.clone()); rsx! { - li { class: if checked() { "completed" }, class: if is_editing() { "editing" }, + li { + // Dioxus lets you use if statements in rsx to conditionally render attributes + // These will get merged into a single class attribute + class: if checked() { "completed" }, + class: if is_editing() { "editing" }, + + // Some basic controls for the todo div { class: "view", input { class: "toggle", r#type: "checkbox", id: "cbg-{id}", checked: "{checked}", - oninput: move |evt| todos.write().get_mut(&id).unwrap().checked = evt.value().parse().unwrap(), + oninput: move |evt| todos.write().get_mut(&id).unwrap().checked = evt.checked(), } label { r#for: "cbg-{id}", @@ -145,6 +179,8 @@ fn TodoEntry(mut todos: Signal>, id: u32) -> Element { prevent_default: "onclick" } } + + // Only render the actual input if we're editing if is_editing() { input { class: "edit", @@ -170,6 +206,8 @@ fn ListFooter( active_todo_count: ReadOnlySignal, mut filter: Signal, ) -> Element { + // We use a memoized signal to calculate whether we should show the "Clear completed" button. + // This will recompute whenever the todos change, and if the value is true, the button will be shown. let show_clear_completed = use_memo(move || todos.read().values().any(|todo| todo.checked)); rsx! { @@ -211,19 +249,3 @@ fn ListFooter( } } } - -fn PageFooter() -> Element { - rsx! { - footer { class: "info", - p { "Double-click to edit a todo" } - p { - "Created by " - a { href: "http://github.com/jkelleyrtp/", "jkelleyrtp" } - } - p { - "Part of " - a { href: "http://todomvc.com", "TodoMVC" } - } - } - } -} diff --git a/examples/video_stream.rs b/examples/video_stream.rs index b1e23e20d..7f5fd80ed 100644 --- a/examples/video_stream.rs +++ b/examples/video_stream.rs @@ -4,9 +4,7 @@ use dioxus::desktop::{use_asset_handler, AssetRequest}; use dioxus::prelude::*; use http::{header::*, response::Builder as ResponseBuilder, status::StatusCode}; use std::{io::SeekFrom, path::PathBuf}; -use tokio::io::AsyncReadExt; -use tokio::io::AsyncSeekExt; -use tokio::io::AsyncWriteExt; +use tokio::io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt}; const VIDEO_PATH: &str = "./examples/assets/test_video.mp4"; diff --git a/examples/web_component.rs b/examples/web_component.rs index 9aa712891..ef3f08aa9 100644 --- a/examples/web_component.rs +++ b/examples/web_component.rs @@ -1,3 +1,9 @@ +//! Dioxus allows webcomponents to be created with a simple syntax. +//! +//! Read more about webcomponents [here](https://developer.mozilla.org/en-US/docs/Web/Web_Components) +//! +//! We typically suggest wrapping webcomponents in a strongly typed interface using a component. + use dioxus::prelude::*; fn main() { @@ -6,8 +12,21 @@ fn main() { fn app() -> Element { rsx! { - web-component { - "my-prop": "5%", + div { + h1 { "Web Components" } + CoolWebComponet { my_prop: "Hello, world!".to_string() } + } + } +} + +/// A web-component wrapped with a strongly typed interface using a component +#[component] +fn CoolWebComponet(my_prop: String) -> Element { + rsx! { + // rsx! takes a webcomponent as long as its tag name is separated with dashes + web-component { + // Since web-components don't have built-in attributes, the attribute names must be passed as a string + "my-prop": my_prop, } } } diff --git a/examples/window_event.rs b/examples/window_event.rs index edb7c68d8..7de6dc01c 100644 --- a/examples/window_event.rs +++ b/examples/window_event.rs @@ -1,3 +1,14 @@ +//! This example demonstrates how to handle window events and change window properties. +//! +//! We're able to do things like: +//! - implement window dragging +//! - toggle fullscreen +//! - toggle always on top +//! - toggle window decorations +//! - change the window title +//! +//! The entire featuresuite of wry and tao is available to you + use dioxus::desktop::{window, Config, WindowBuilder}; use dioxus::prelude::*; @@ -14,29 +25,40 @@ fn main() { } fn app() -> Element { - let mut fullscreen = use_signal(|| false); - let mut always_on_top = use_signal(|| false); - let mut decorations = use_signal(|| false); - rsx!( - link { - href: "https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css", - rel: "stylesheet" + link { href: "https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css", rel: "stylesheet" } + Header {} + div { class: "container mx-auto", + div { class: "grid grid-cols-5", + SetOnTop {} + SetDecorations {} + SetTitle {} + } } - header { - class: "text-gray-400 bg-gray-900 body-font", - onmousedown: move |_| window().drag(), + ) +} + +#[component] +fn Header() -> Element { + let mut fullscreen = use_signal(|| false); + + rsx! { + header { class: "text-gray-400 bg-gray-900 body-font", onmousedown: move |_| window().drag(), div { class: "container mx-auto flex flex-wrap p-5 flex-col md:flex-row items-center", a { class: "flex title-font font-medium items-center text-white mb-4 md:mb-0", span { class: "ml-3 text-xl", "Dioxus" } } nav { class: "md:ml-auto flex flex-wrap items-center text-base justify-center" } + + // Set the window to minimized button { class: "inline-flex items-center bg-gray-800 border-0 py-1 px-3 focus:outline-none hover:bg-gray-700 rounded text-base mt-4 md:mt-0", onmousedown: |evt| evt.stop_propagation(), onclick: move |_| window().set_minimized(true), "Minimize" } + + // Toggle fullscreen button { class: "inline-flex items-center bg-gray-800 border-0 py-1 px-3 focus:outline-none hover:bg-gray-700 rounded text-base mt-4 md:mt-0", onmousedown: |evt| evt.stop_propagation(), @@ -47,6 +69,9 @@ fn app() -> Element { }, "Fullscreen" } + + // Close the window + // If the window is the last window open, the app will close, if you configured the close behavior to do so button { class: "inline-flex items-center bg-gray-800 border-0 py-1 px-3 focus:outline-none hover:bg-gray-700 rounded text-base mt-4 md:mt-0", onmousedown: |evt| evt.stop_propagation(), @@ -55,40 +80,57 @@ fn app() -> Element { } } } - br {} - div { class: "container mx-auto", - div { class: "grid grid-cols-5", - div { - button { - class: "inline-flex items-center text-white bg-green-500 border-0 py-1 px-3 hover:bg-green-700 rounded", - onmousedown: |evt| evt.stop_propagation(), - onclick: move |_| { - window().set_always_on_top(!always_on_top()); - always_on_top.toggle(); - }, - "Always On Top" - } - } - div { - button { - class: "inline-flex items-center text-white bg-blue-500 border-0 py-1 px-3 hover:bg-green-700 rounded", - onmousedown: |evt| evt.stop_propagation(), - onclick: move |_| { - window().set_decorations(!decorations()); - decorations.toggle(); - }, - "Set Decorations" - } - } - div { - button { - class: "inline-flex items-center text-white bg-blue-500 border-0 py-1 px-3 hover:bg-green-700 rounded", - onmousedown: |evt| evt.stop_propagation(), - onclick: move |_| window().set_title("Dioxus Application"), - "Change Title" - } - } + } +} + +#[component] +fn SetOnTop() -> Element { + let mut always_on_top = use_signal(|| false); + + rsx! { + div { + button { + class: "inline-flex items-center text-white bg-green-500 border-0 py-1 px-3 hover:bg-green-700 rounded", + onmousedown: |evt| evt.stop_propagation(), + onclick: move |_| { + window().set_always_on_top(!always_on_top()); + always_on_top.toggle(); + }, + "Always On Top" } } - ) + } +} + +#[component] +fn SetDecorations() -> Element { + let mut decorations = use_signal(|| false); + + rsx! { + div { + button { + class: "inline-flex items-center text-white bg-blue-500 border-0 py-1 px-3 hover:bg-green-700 rounded", + onmousedown: |evt| evt.stop_propagation(), + onclick: move |_| { + window().set_decorations(!decorations()); + decorations.toggle(); + }, + "Set Decorations" + } + } + } +} + +#[component] +fn SetTitle() -> Element { + rsx! { + div { + button { + class: "inline-flex items-center text-white bg-blue-500 border-0 py-1 px-3 hover:bg-green-700 rounded", + onmousedown: |evt| evt.stop_propagation(), + onclick: move |_| window().set_title("Dioxus Application"), + "Change Title" + } + } + } } diff --git a/examples/window_focus.rs b/examples/window_focus.rs index 04a4b22ae..447544077 100644 --- a/examples/window_focus.rs +++ b/examples/window_focus.rs @@ -1,3 +1,10 @@ +//! Listen for window focus events using a wry event handler +//! +//! This example shows how to use the use_wry_event_handler hook to listen for window focus events. +//! We can intercept any Wry event, but in this case we're only interested in the WindowEvent::Focused event. +//! +//! This lets you do things like backgrounding tasks, pausing animations, or changing the UI when the window is focused or not. + use dioxus::desktop::tao::event::Event as WryEvent; use dioxus::desktop::tao::event::WindowEvent; use dioxus::desktop::use_wry_event_handler; diff --git a/examples/window_zoom.rs b/examples/window_zoom.rs index 0c8e19e10..3bf47d4cc 100644 --- a/examples/window_zoom.rs +++ b/examples/window_zoom.rs @@ -1,3 +1,7 @@ +//! Adjust the zoom of a desktop app +//! +//! This example shows how to adjust the zoom of a desktop app using the webview.zoom method. + use dioxus::prelude::*; fn main() { @@ -8,6 +12,8 @@ fn app() -> Element { let mut level = use_signal(|| 1.0); rsx! { + h1 { "Zoom level: {level}" } + p { "Change the zoom level of the webview by typing a number in the input below."} input { r#type: "number", value: "{level}", diff --git a/packages/desktop/src/webview.rs b/packages/desktop/src/webview.rs index de83c6b0d..ade023c2f 100644 --- a/packages/desktop/src/webview.rs +++ b/packages/desktop/src/webview.rs @@ -37,11 +37,16 @@ impl WebviewInstance { dom: VirtualDom, shared: Rc, ) -> WebviewInstance { - let window = cfg.window.clone().build(&shared.target).unwrap(); + let mut window = cfg.window.clone(); + + // tao makes small windows for some reason, make them bigger + if cfg.window.window.inner_size.is_none() { + window = window.with_inner_size(tao::dpi::LogicalSize::new(800.0, 600.0)); + } // We assume that if the icon is None in cfg, then the user just didnt set it if cfg.window.window.window_icon.is_none() { - window.set_window_icon(Some( + window = window.with_window_icon(Some( tao::window::Icon::from_rgba( include_bytes!("./assets/default_icon.bin").to_vec(), 460, @@ -51,6 +56,8 @@ impl WebviewInstance { )); } + let window = window.build(&shared.target).unwrap(); + let mut web_context = WebContext::new(cfg.data_dir.clone()); let edit_queue = EditQueue::default(); let asset_handlers = AssetHandlerRegistry::new(dom.runtime()); diff --git a/packages/html/src/events/form.rs b/packages/html/src/events/form.rs index 442b63aa5..01d5793ec 100644 --- a/packages/html/src/events/form.rs +++ b/packages/html/src/events/form.rs @@ -90,6 +90,14 @@ impl FormData { self.inner.value() } + /// Get the value of the form event as a parsed type + pub fn parsed(&self) -> Result + where + T: std::str::FromStr, + { + self.value().parse() + } + /// Try to parse the value as a boolean /// /// Returns false if the value is not a boolean, or if it is false! diff --git a/packages/mobile/Cargo.toml b/packages/mobile/Cargo.toml index 4d3589ca6..eeb44d4d0 100644 --- a/packages/mobile/Cargo.toml +++ b/packages/mobile/Cargo.toml @@ -10,7 +10,7 @@ keywords = ["dom", "ui", "gui", "react"] license = "MIT OR Apache-2.0" [dependencies] -dioxus-desktop = { workspace = true, default-features = false, features = ["tokio_runtime"] } +dioxus-desktop = { workspace = true, features = ["tokio_runtime"] } [lib] doctest = false diff --git a/packages/router/src/components/link.rs b/packages/router/src/components/link.rs index 1645f8d0d..1d0be0979 100644 --- a/packages/router/src/components/link.rs +++ b/packages/router/src/components/link.rs @@ -99,6 +99,10 @@ pub struct LinkProps { /// The onclick event handler. pub onclick: Option>, + /// The onmounted event handler. + /// Fired when the element is mounted. + pub onmounted: Option>, + #[props(default)] /// Whether the default behavior should be executed if an `onclick` handler is provided. /// @@ -269,10 +273,17 @@ pub fn Link(props: LinkProps) -> Element { } }; + let onmounted = move |event| { + if let Some(handler) = props.onmounted.clone() { + handler.call(event); + } + }; + rsx! { a { onclick: action, href, + onmounted: onmounted, prevent_default, class, rel,