diff --git a/Cargo.lock b/Cargo.lock index 427495b025..4f920a0fd1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2558,6 +2558,7 @@ dependencies = [ "serde", "serde_json", "tokio", + "warp", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 5fcbca6e83..748eb89a8b 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 4ceca1a5a5..c60a6a726e 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 0000000000..20216ac05a --- /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 0000000000..1603d58e0b --- /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 0000000000..e9d843b434 --- /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 0000000000..bac30b618a --- /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 0000000000..e69de29bb2 diff --git a/examples/assets/events.css b/examples/assets/events.css new file mode 100644 index 0000000000..36ed4041ba --- /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 0000000000..81aba2638c --- /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 0000000000..126b16b5b5 --- /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 0000000000..c256a7964b --- /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/radio.css b/examples/assets/radio.css new file mode 100644 index 0000000000..214731c357 --- /dev/null +++ b/examples/assets/radio.css @@ -0,0 +1,43 @@ +body { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; +} + +button { + margin: 10px; + padding: 10px; + border: none; + border-radius: 5px; + background-color: #f0f0f0; + cursor: pointer; +} + +#pause { + background-color: #ff0000; +} + +#play { + background-color: #00ff00; +} + + +.bounce { + animation: boomBox 0.5s infinite; +} + +@keyframes boomBox { + 0% { + transform: scale(1.0); + } + + 50% { + transform: scale(2); + } + + 100% { + transform: scale(1.0); + } +} diff --git a/examples/assets/roulette.css b/examples/assets/roulette.css new file mode 100644 index 0000000000..34077c0c2d --- /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/assets/router.css b/examples/assets/router.css new file mode 100644 index 0000000000..eabe84d842 --- /dev/null +++ b/examples/assets/router.css @@ -0,0 +1,13 @@ +#navbar { + display: flex; + justify-content: flex-start; + gap: 1rem; + align-items: center; +} + +#blog-list { + display: flex; + flex-direction: column; + align-items: start; + gap: 1rem; +} diff --git a/examples/backgrounded_futures.rs b/examples/backgrounded_futures.rs index 2877daab31..160b9c122e 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 88c1e8fa56..f063151ec9 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 ae1a6f3531..f77c53e446 100644 --- a/examples/calculator_mutable.rs +++ b/examples/calculator_mutable.rs @@ -1,38 +1,28 @@ -#![allow(non_snake_case)] - -//! Example: Calculator -//! ------------------- -//! -//! 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. +//! 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. //! -//! 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 b48a4053f2..9ca9fc201e 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 1172327ea2..95f36e7cf2 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 a25115b227..0000000000 --- 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 0000000000..ebedf27122 --- /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 d9f2c9a382..5360c0480b 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 54ed831d56..de2cb8648c 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 37d1f38085..dbaf31d946 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 c7288f352a..6aaa70b9d4 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 4d3005f973..5e20e30c25 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 3f20c30e7f..cf43dd14c3 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 02410629c4..ccafe0642f 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 99d7010226..9e0646147d 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 958aec9acd..ae0ab65ca1 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 c98221c169..280144e445 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 cf4f1742a3..0000000000 --- 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 a981e4fb08..4af9ff85c3 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 897587daa3..7da92e65cd 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 0000000000..130d526b29 --- /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 06513aa023..3261ea13bc 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 f3659845c9..a041d211d4 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 da046a634c..ee33c22309 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 66d4fe696c..0000000000 --- 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 f11408c2ea..ac853b3f6c 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 2204728866..c813e1434f 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 56c3aad718..bbcca99548 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 974e024d76..978cb2bad2 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 75ba1e3be6..cf3c0c08f6 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_case)] fn Button(props: ButtonProps) -> Element { rsx! { button { diff --git a/examples/overlay.rs b/examples/overlay.rs index a4a927558f..570c878e56 100644 --- a/examples/overlay.rs +++ b/examples/overlay.rs @@ -1,4 +1,13 @@ -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, LogicalSize, WindowBuilder, +}; use dioxus::prelude::*; fn main() { @@ -6,21 +15,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", + + div { + width: "100%", + height: "10px", + background_color: "black", + onmousedown: move |_| dioxus::desktop::window().drag(), + } - "This is an overlay!" + "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 20ee392d0c..131d536edb 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/read_size.rs b/examples/read_size.rs index 0a9f802b89..932c831c79 100644 --- a/examples/read_size.rs +++ b/examples/read_size.rs @@ -1,4 +1,9 @@ -#![allow(clippy::await_holding_refcell_ref)] +//! Read the size of elements using the MountedData struct. +//! +//! Whenever an Element is finally mounted to the Dom, its data is avaiable to be read. +//! These fields can typically only be read asynchronously, since various renderers need to release the main thread to +//! perform layout and painting. + use std::rc::Rc; use dioxus::{html::geometry::euclid::Rect, prelude::*}; @@ -33,6 +38,7 @@ fn app() -> Element { let read_dims = move |_| async move { let read = div_element.read(); let client_rect = read.as_ref().map(|el| el.get_client_rect()); + if let Some(client_rect) = client_rect { if let Ok(rect) = client_rect.await { dimensions.set(rect); diff --git a/examples/readme.rs b/examples/readme.rs index d29a24bdc1..7c52400869 100644 --- a/examples/readme.rs +++ b/examples/readme.rs @@ -1,3 +1,10 @@ +//! The example from the readme! +//! +//! This example demonstrates how to create a simple counter app with dioxus. The `Signal` type wraps inner values, +//! making them `Copy`, allowing them to be freely used in closures and and async functions. `Signal` also provides +//! helper methods like AddAssign, SubAssign, toggle, etc, to make it easy to update the value without running +//! into lock issues. + use dioxus::prelude::*; fn main() { diff --git a/examples/reducer.rs b/examples/reducer.rs index cc685a8e1e..082714ff52 100644 --- a/examples/reducer.rs +++ b/examples/reducer.rs @@ -15,12 +15,16 @@ fn app() -> Element { let mut state = use_signal(|| PlayerState { is_playing: false }); rsx!( - div { - h1 {"Select an option"} - h3 { "The radio is... ", {state.read().is_playing()}, "!" } - button { onclick: move |_| state.write().reduce(PlayerAction::Pause), "Pause" } - button { onclick: move |_| state.write().reduce(PlayerAction::Play), "Play" } + style { {include_str!("./assets/radio.css")} } + h1 {"Select an option"} + + // Add some cute animations if the radio is playing! + div { class: if state.read().is_playing { "bounce" }, + "The radio is... ", {state.read().is_playing()}, "!" } + + button { id: "play", onclick: move |_| state.write().reduce(PlayerAction::Pause), "Pause" } + button { id: "pause", onclick: move |_| state.write().reduce(PlayerAction::Play), "Play" } ) } diff --git a/examples/router.rs b/examples/router.rs index 5230d92a65..7b41e94e1b 100644 --- a/examples/router.rs +++ b/examples/router.rs @@ -1,32 +1,59 @@ +//! An advanced usage of the router with nested routes and redirects. +//! +//! Dioxus implements an enum-based router, which allows you to define your routes in a type-safe way. +//! However, since we need to bake quite a bit of logic into the enum, we have to add some extra syntax. +//! +//! Note that you don't need to use advanced features like nest, redirect, etc, since these can all be implemented +//! manually, but they are provided as a convenience. + use dioxus::prelude::*; fn main() { launch_desktop(|| { rsx! { + style { {include_str!("./assets/router.css")} } Router:: {} } }); } +// Turn off rustfmt since we're doing layouts and routes in the same enum #[derive(Routable, Clone, Debug, PartialEq)] #[rustfmt::skip] enum Route { + // Wrap Home in a Navbar Layout #[layout(NavBar)] + // The default route is always "/" unless otherwise specified #[route("/")] Home {}, + + // Wrap the next routes in a layout and a nest #[nest("/blog")] - #[layout(Blog)] - #[route("/")] - BlogList {}, - #[route("/:name")] - BlogPost { name: String }, - #[end_layout] + #[layout(Blog)] + // At "/blog", we want to show a list of blog posts + #[route("/")] + BlogList {}, + + // At "/blog/:name", we want to show a specific blog post, using the name slug + #[route("/:name")] + BlogPost { name: String }, + + // We need to end the blog layout and nest + // Note we don't need either - we could've just done `/blog/` and `/blog/:name` without nesting, + // but it's a bit cleaner this way + #[end_layout] #[end_nest] + + // And the regular page layout #[end_layout] + + // Add some redirects for the `/myblog` route #[nest("/myblog")] #[redirect("/", || Route::BlogList {})] #[redirect("/:name", |name: String| Route::BlogPost { name })] #[end_nest] + + // Finally, we need to handle the 404 page #[route("/:..route")] PageNotFound { route: Vec, @@ -36,15 +63,9 @@ enum Route { #[component] fn NavBar() -> Element { rsx! { - nav { - ul { - li { - Link { to: Route::Home {}, "Home" } - } - li { - Link { to: Route::BlogList {}, "Blog" } - } - } + nav { id: "navbar", + Link { to: Route::Home {}, "Home" } + Link { to: Route::BlogList {}, "Blog" } } Outlet:: {} } @@ -67,30 +88,31 @@ fn Blog() -> Element { fn BlogList() -> Element { rsx! { h2 { "Choose a post" } - ul { - li { - Link { - to: Route::BlogPost { - name: "Blog post 1".into(), - }, - "Read the first blog post" - } + div { id: "blog-list", + Link { to: Route::BlogPost { name: "Blog post 1".into() }, + "Read the first blog post" } - li { - Link { - to: Route::BlogPost { - name: "Blog post 2".into(), - }, - "Read the second blog post" - } + Link { to: Route::BlogPost { name: "Blog post 2".into() }, + "Read the second blog post" } } } } +// We can use the `name` slug to show a specific blog post +// In theory we could read from the filesystem or a database here #[component] fn BlogPost(name: String) -> Element { - rsx! { h2 { "Blog Post: {name}" } } + let contents = match name.as_str() { + "Blog post 1" => "This is the first blog post. It's not very interesting.", + "Blog post 2" => "This is the second blog post. It's not very interesting either.", + _ => "This blog post doesn't exist.", + }; + + rsx! { + h2 { "{name}" } + p { "{contents}" } + } } #[component] diff --git a/examples/rsx_usage.rs b/examples/rsx_usage.rs index e9cfa57aa5..361a3839b1 100644 --- a/examples/rsx_usage.rs +++ b/examples/rsx_usage.rs @@ -39,263 +39,266 @@ //! - Allow top-level fragments fn main() { - todo!() - //launch_desktop(App); + launch(app) } -// use core::{fmt, str::FromStr}; -// use std::fmt::Display; - -// use baller::Baller; -// use dioxus::prelude::*; - -// #[component] -// fn App() -> Element { -// let formatting = "formatting!"; -// let formatting_tuple = ("a", "b"); -// let lazy_fmt = format_args!("lazily formatted text"); -// let asd = 123; -// rsx! { -// div { -// // Elements -// div {} -// h1 {"Some text"} -// h1 {"Some text with {formatting}"} -// h1 {"Formatting basic expressions {formatting_tuple.0} and {formatting_tuple.1}"} -// h1 {"Formatting without interpolation " {formatting_tuple.0} "and" {formatting_tuple.1} } -// h2 { -// "Multiple" -// "Text" -// "Blocks" -// "Use comments as separators in html" -// } -// div { -// h1 {"multiple"} -// h2 {"nested"} -// h3 {"elements"} -// } -// div { -// class: "my special div", -// h1 {"Headers and attributes!"} -// } -// div { -// // pass simple rust expressions in -// class: lazy_fmt, -// id: format_args!("attributes can be passed lazily with std::fmt::Arguments"), -// class: "asd", -// class: "{asd}", -// // if statements can be used to conditionally render attributes -// class: if formatting.contains("form") { "{asd}" }, -// div { -// class: { -// const WORD: &str = "expressions"; -// format_args!("Arguments can be passed in through curly braces for complex {WORD}") -// } -// } -// } - -// // Expressions can be used in element position too: -// {rsx!(p { "More templating!" })}, - -// // Iterators -// {(0..10).map(|i| rsx!(li { "{i}" }))}, - -// // Iterators within expressions -// { -// let data = std::collections::HashMap::<&'static str, &'static str>::new(); -// // Iterators *should* have keys when you can provide them. -// // Keys make your app run faster. Make sure your keys are stable, unique, and predictable. -// // Using an "ID" associated with your data is a good idea. -// data.into_iter().map(|(k, v)| rsx!(li { key: "{k}", "{v}" })) -// } - -// // Matching -// match true { -// true => rsx!( h1 {"Top text"}), -// false => rsx!( h1 {"Bottom text"}) -// } - -// // Conditional rendering -// // Dioxus conditional rendering is based around None/Some. We have no special syntax for conditionals. -// // You can convert a bool condition to rsx! with .then and .or -// {true.then(|| rsx!(div {}))}, - -// // Alternatively, you can use the "if" syntax - but both branches must be resolve to Element -// if false { -// h1 {"Top text"} -// } else { -// h1 {"Bottom text"} -// } - -// // Using optionals for diverging branches -// // Note that since this is wrapped in curlies, it's interpreted as an expression -// {if true { -// Some(rsx!(h1 {"Top text"})) -// } else { -// None -// }} - -// // returning "None" without a diverging branch is a bit noisy... but rare in practice -// {None as Option<()>}, - -// // can also just use empty fragments -// Fragment {} - -// // Fragments let you insert groups of nodes without a parent. -// // This lets you make components that insert elements as siblings without a container. -// div {"A"} -// Fragment { -// div {"B"} -// div {"C"} -// Fragment { -// "D" -// Fragment { -// "E" -// "F" -// } -// } -// } - -// // Components -// // Can accept any paths -// // Notice how you still get syntax highlighting and IDE support :) -// Baller {} -// baller::Baller {} -// crate::baller::Baller {} - -// // Can take properties -// Taller { a: "asd" } - -// // Can take optional properties -// Taller { a: "asd" } - -// // Can pass in props directly as an expression -// { -// let props = TallerProps {a: "hello", children: None }; -// rsx!(Taller { ..props }) -// } - -// // Spreading can also be overridden manually -// Taller { -// ..TallerProps { a: "ballin!", children: None }, -// a: "not ballin!" -// } - -// // Can take children too! -// Taller { a: "asd", div {"hello world!"} } - -// // This component's props are defined *inline* with the `inline_props` macro -// WithInline { text: "using functionc all syntax" } - -// // Components can be generic too -// // This component takes i32 type to give you typed input -// TypedInput:: {} - -// // Type inference can be used too -// TypedInput { initial: 10.0 } - -// // geneircs with the `inline_props` macro -// Label { text: "hello geneirc world!" } -// Label { text: 99.9 } - -// // Lowercase components work too, as long as they are access using a path -// baller::lowercase_component {} - -// // For in-scope lowercase components, use the `self` keyword -// self::lowercase_helper {} - -// // helper functions -// // Anything that implements IntoVnode can be dropped directly into Rsx -// {helper("hello world!")} - -// // Strings can be supplied directly -// {String::from("Hello world!")} - -// // So can format_args -// {format_args!("Hello {}!", "world")} - -// // Or we can shell out to a helper function -// {format_dollars(10, 50)} -// } -// } -// } - -// fn format_dollars(dollars: u32, cents: u32) -> String { -// format!("${dollars}.{cents:02}") -// } - -// fn helper<'a>(cx: &'a ScopeState, text: &'a str) -> Element { -// rsx! { -// p { "{text}" } -// } -// } - -// // no_case_check disables PascalCase checking if you *really* want a snake_case component. -// // This will likely be deprecated/removed in a future update that will introduce a more polished linting system, -// // something like Clippy. -// #[component(no_case_check)] -// fn lowercase_helper() -> Element { -// rsx! { -// "asd" -// } -// } - -// mod baller { -// use super::*; - -// #[component] -// /// This component totally balls -// pub fn Baller() -> Element { -// todo!() -// } - -// // no_case_check disables PascalCase checking if you *really* want a snake_case component. -// // This will likely be deprecated/removed in a future update that will introduce a more polished linting system, -// // something like Clippy. -// #[component(no_case_check)] -// pub fn lowercase_component() -> Element { -// rsx! { "look ma, no uppercase" } -// } -// } - -// /// Documention for this component is visible within the rsx macro -// #[component] -// pub fn Taller( -// /// Fields are documented and accessible in rsx! -// a: &'static str, -// children: Element, -// ) -> Element { -// rsx! { {&children} } -// } - -// #[derive(Props, PartialEq, Eq)] -// pub struct TypedInputProps { -// #[props(optional, default)] -// initial: Option, -// } - -// #[allow(non_snake_case)] -// pub fn TypedInput(_: Scope>) -> Element -// where -// T: FromStr + fmt::Display, -// ::Err: std::fmt::Display, -// { -// todo!() -// } - -// #[component] -// fn WithInline(cx: Scope<'a>, text: &'a str) -> Element { -// rsx! { -// p { "{text}" } -// } -// } - -// #[component] -// fn Label(text: T) -> Element -// where -// T: Display, -// { -// rsx! { -// p { "{text}" } -// } -// } +use core::{fmt, str::FromStr}; +use std::fmt::Display; + +use baller::Baller; +use dioxus::prelude::*; + +fn app() -> Element { + let formatting = "formatting!"; + let formatting_tuple = ("a", "b"); + let lazy_fmt = format_args!("lazily formatted text"); + let asd = 123; + + rsx! { + div { + // Elements + div {} + h1 {"Some text"} + h1 {"Some text with {formatting}"} + h1 {"Formatting basic expressions {formatting_tuple.0} and {formatting_tuple.1}"} + h1 {"Formatting without interpolation " {formatting_tuple.0} "and" {formatting_tuple.1} } + h2 { + "Multiple" + "Text" + "Blocks" + "Use comments as separators in html" + } + div { + h1 {"multiple"} + h2 {"nested"} + h3 {"elements"} + } + div { + class: "my special div", + h1 {"Headers and attributes!"} + } + div { + // pass simple rust expressions in + class: lazy_fmt, + id: format_args!("attributes can be passed lazily with std::fmt::Arguments"), + class: "asd", + class: "{asd}", + // if statements can be used to conditionally render attributes + class: if formatting.contains("form") { "{asd}" }, + div { + class: { + const WORD: &str = "expressions"; + format_args!("Arguments can be passed in through curly braces for complex {WORD}") + } + } + } + + // Expressions can be used in element position too: + {rsx!(p { "More templating!" })}, + + // Iterators + {(0..10).map(|i| rsx!(li { "{i}" }))}, + + // Iterators within expressions + { + let data = std::collections::HashMap::<&'static str, &'static str>::new(); + // Iterators *should* have keys when you can provide them. + // Keys make your app run faster. Make sure your keys are stable, unique, and predictable. + // Using an "ID" associated with your data is a good idea. + data.into_iter().map(|(k, v)| rsx!(li { key: "{k}", "{v}" })) + } + + // Matching + match true { + true => rsx!( h1 {"Top text"}), + false => rsx!( h1 {"Bottom text"}) + } + + // Conditional rendering + // Dioxus conditional rendering is based around None/Some. We have no special syntax for conditionals. + // You can convert a bool condition to rsx! with .then and .or + {true.then(|| rsx!(div {}))}, + + // Alternatively, you can use the "if" syntax - but both branches must be resolve to Element + if false { + h1 {"Top text"} + } else { + h1 {"Bottom text"} + } + + // Using optionals for diverging branches + // Note that since this is wrapped in curlies, it's interpreted as an expression + {if true { + Some(rsx!(h1 {"Top text"})) + } else { + None + }} + + // returning "None" without a diverging branch is a bit noisy... but rare in practice + {None as Option<()>}, + + // can also just use empty fragments + Fragment {} + + // Fragments let you insert groups of nodes without a parent. + // This lets you make components that insert elements as siblings without a container. + div {"A"} + Fragment { + div {"B"} + div {"C"} + Fragment { + "D" + Fragment { + "E" + "F" + } + } + } + + // Components + // Can accept any paths + // Notice how you still get syntax highlighting and IDE support :) + Baller {} + baller::Baller {} + crate::baller::Baller {} + + // Can take properties + Taller { a: "asd" } + + // Can take optional properties + Taller { a: "asd" } + + // Can pass in props directly as an expression + { + let props = TallerProps {a: "hello", children: None }; + rsx!(Taller { ..props }) + } + + // Spreading can also be overridden manually + Taller { + ..TallerProps { a: "ballin!", children: None }, + a: "not ballin!" + } + + // Can take children too! + Taller { a: "asd", div {"hello world!"} } + + // This component's props are defined *inline* with the `inline_props` macro + WithInline { text: "using functionc all syntax" } + + // Components can be generic too + // This component takes i32 type to give you typed input + TypedInput:: {} + + // Type inference can be used too + TypedInput { initial: 10.0 } + + // geneircs with the `inline_props` macro + Label { text: "hello geneirc world!" } + Label { text: 99.9 } + + // Lowercase components work too, as long as they are access using a path + baller::lowercase_component {} + + // For in-scope lowercase components, use the `self` keyword + self::lowercase_helper {} + + // helper functions + // Anything that implements IntoVnode can be dropped directly into Rsx + {helper("hello world!")} + + // Strings can be supplied directly + {String::from("Hello world!")} + + // So can format_args + {format_args!("Hello {}!", "world")} + + // Or we can shell out to a helper function + {format_dollars(10, 50)} + } + } +} + +fn format_dollars(dollars: u32, cents: u32) -> String { + format!("${dollars}.{cents:02}") +} + +fn helper(text: &str) -> Element { + rsx! { + p { "{text}" } + } +} + +// no_case_check disables PascalCase checking if you *really* want a snake_case component. +// This will likely be deprecated/removed in a future update that will introduce a more polished linting system, +// something like Clippy. +#[component(no_case_check)] +fn lowercase_helper() -> Element { + rsx! { + "asd" + } +} + +mod baller { + use super::*; + + #[component] + /// This component totally balls + pub fn Baller() -> Element { + rsx! { "ballin'" } + } + + // no_case_check disables PascalCase checking if you *really* want a snake_case component. + // This will likely be deprecated/removed in a future update that will introduce a more polished linting system, + // something like Clippy. + #[component(no_case_check)] + pub fn lowercase_component() -> Element { + rsx! { "look ma, no uppercase" } + } +} + +/// Documention for this component is visible within the rsx macro +#[component] +pub fn Taller( + /// Fields are documented and accessible in rsx! + a: &'static str, + children: Element, +) -> Element { + rsx! { {&children} } +} + +#[derive(Props, Clone, PartialEq, Eq)] +pub struct TypedInputProps { + #[props(optional, default)] + initial: Option, +} + +#[allow(non_snake_case)] +pub fn TypedInput(props: TypedInputProps) -> Element +where + T: FromStr + fmt::Display + PartialEq + Clone + 'static, + ::Err: std::fmt::Display, +{ + if let Some(props) = props.initial { + return rsx! { "{props}" }; + } + + None +} + +#[component] +fn WithInline(text: String) -> Element { + rsx! { + p { "{text}" } + } +} + +#[component] +fn Label(text: T) -> Element +where + T: Display, +{ + rsx! { + p { "{text}" } + } +} diff --git a/examples/scroll_to_top.rs b/examples/scroll_to_top.rs index 1c042b912e..b69a86c510 100644 --- a/examples/scroll_to_top.rs +++ b/examples/scroll_to_top.rs @@ -1,3 +1,10 @@ +//! Scroll elements using their MountedData +//! +//! Dioxus exposes a few helpful APIs around elements (mimicking the DOM APIs) to allow you to interact with elements +//! across the renderers. This includes scrolling, reading dimensions, and more. +//! +//! In this example we demonstrate how to scroll to the top of the page using the `scroll_to` method on the `MountedData` + use dioxus::prelude::*; fn main() { diff --git a/examples/shortcut.rs b/examples/shortcut.rs index d7e849870c..d0192b9782 100644 --- a/examples/shortcut.rs +++ b/examples/shortcut.rs @@ -1,3 +1,10 @@ +//! Add global shortcuts to your app while a component is active +//! +//! This demo shows how to add a global shortcut to your app that toggles a signal. You could use this to implement +//! a raycast-type app, or to add a global shortcut to your app that toggles a component on and off. +//! +//! These are *global* shortcuts, so they will work even if your app is not in focus. + use dioxus::desktop::use_global_shortcut; use dioxus::prelude::*; diff --git a/examples/shorthand.rs b/examples/shorthand.rs index c53e0146b1..5b016ab930 100644 --- a/examples/shorthand.rs +++ b/examples/shorthand.rs @@ -1,3 +1,5 @@ +//! Dioxus supports shorthand syntax for creating elements and components. + use dioxus::prelude::*; fn main() { diff --git a/examples/signals.rs b/examples/signals.rs index 5b217c5081..0accddc564 100644 --- a/examples/signals.rs +++ b/examples/signals.rs @@ -1,3 +1,11 @@ +//! A simple example demonstrating how to use signals to modify state from several different places. +//! +//! This simlpe example implements a counter that can be incremented, decremented, and paused. It also demonstrates +//! that background tasks in use_futures can modify the value as well. +//! +//! Most signals implement Into>, making ReadOnlySignal a good default type when building new +//! library components that don't need to modify their values. + use dioxus::prelude::*; use std::time::Duration; diff --git a/examples/simple_list.rs b/examples/simple_list.rs index 67e9e64ff0..1d33d4f0cf 100644 --- a/examples/simple_list.rs +++ b/examples/simple_list.rs @@ -1,3 +1,7 @@ +//! A few ways of mapping elements into rsx! syntax +//! +//! Rsx allows anything that's an iterator where the output type implements Into, so you can use any of the following: + use dioxus::prelude::*; fn main() { diff --git a/examples/simple_router.rs b/examples/simple_router.rs index 7ad44fbb3c..44a06040e7 100644 --- a/examples/simple_router.rs +++ b/examples/simple_router.rs @@ -1,20 +1,32 @@ -#![allow(non_snake_case)] +//! A simple example of a router with a few routes and a nav bar. use dioxus::prelude::*; +fn main() { + // Launch the router, using our `Route` component as the generic type + // This will automatically boot the app to "/" unless otherwise specified + launch(|| rsx! { Router:: {} }); +} + +/// By default, the Routable derive will use the name of the variant as the route +/// You can also specify a specific component by adding the Component name to the `#[route]` attribute +#[rustfmt::skip] #[derive(Routable, Clone, PartialEq)] enum Route { + // Wrap the app in a Nav layout #[layout(Nav)] - #[route("/")] - Homepage {}, + #[route("/")] + Homepage {}, - #[route("/blog/:id")] - Blog { id: String }, + #[route("/blog/:id")] + Blog { id: String }, } #[component] fn Homepage() -> Element { - rsx! { h1 { "Welcome home" } } + rsx! { + h1 { "Welcome home" } + } } #[component] @@ -25,6 +37,9 @@ fn Blog(id: String) -> Element { } } +/// A simple nav bar that links to the homepage and blog pages +/// +/// The `Route` enum gives up typesafe routes, allowing us to rename routes and serialize them automatically #[component] fn Nav() -> Element { rsx! { @@ -52,7 +67,3 @@ fn Nav() -> Element { div { Outlet:: {} } } } - -fn main() { - launch_desktop(|| rsx! { Router:: {} }); -} diff --git a/examples/spread.rs b/examples/spread.rs index 763a48e12b..8bc0f8b59e 100644 --- a/examples/spread.rs +++ b/examples/spread.rs @@ -1,3 +1,8 @@ +//! This example demonstrates how to use the spread operator to pass attributes to child components. +//! +//! This lets components like the `Link` allow the user to extend the attributes of the underlying `a` tag. +//! These attributes are bundled into a `Vec` which can be spread into the child component using the `..` operator. + use dioxus::prelude::*; fn main() { diff --git a/examples/ssr.rs b/examples/ssr.rs index e2a6acd90b..c9e759e1cf 100644 --- a/examples/ssr.rs +++ b/examples/ssr.rs @@ -1,6 +1,9 @@ //! Example: SSR //! //! This example shows how we can render the Dioxus Virtualdom using SSR. +//! Dioxus' SSR is quite comprehensive and can generate a number of utility markers for things like hydration. +//! +//! You can also render without any markers to get a clean HTML output. use dioxus::prelude::*; diff --git a/examples/stale_memo.rs b/examples/stale_memo.rs deleted file mode 100644 index f44c7297a0..0000000000 --- a/examples/stale_memo.rs +++ /dev/null @@ -1,36 +0,0 @@ -use dioxus::prelude::*; - -fn main() { - launch_desktop(app); -} - -fn app() -> Element { - let mut state = use_signal(|| 0); - let mut depth = use_signal(|| 1_usize); - - if depth() == 5 { - return rsx! { - div { "Max depth reached" } - button { onclick: move |_| depth -= 1, "Remove depth" } - }; - } - - let items = use_memo(move || (0..depth()).map(|f| f as _).collect::>()); - - rsx! { - button { onclick: move |_| state += 1, "Increment" } - button { onclick: move |_| depth += 1, "Add depth" } - button { - onclick: move |_| async move { - depth += 1; - tokio::time::sleep(std::time::Duration::from_millis(100)).await; - dbg!(items.read()); - // if depth() is 5, this will be the old since the memo hasn't been re-computed - // use_memos are only re-computed when the signals they capture change - // *and* they are used in the current render - // If the use_memo isn't used, it can't be re-computed! - }, - "Add depth with sleep" - } - } -} diff --git a/examples/streams.rs b/examples/streams.rs index 8f692596a9..5e62c35aa6 100644 --- a/examples/streams.rs +++ b/examples/streams.rs @@ -1,3 +1,5 @@ +//! Handle async streams using use_future and awaiting the next value. + use dioxus::prelude::*; use futures_util::{future, stream, Stream, StreamExt}; use std::time::Duration; @@ -10,8 +12,11 @@ fn app() -> Element { let mut count = use_signal(|| 10); use_future(move || async move { + // Create the stream. + // This could be a network request, a file read, or any other async operation. let mut stream = some_stream(); + // Await the next value from the stream. while let Some(second) = stream.next().await { count.set(second); } diff --git a/examples/suspense.rs b/examples/suspense.rs index 2fa9757fe4..05c651deaf 100644 --- a/examples/suspense.rs +++ b/examples/suspense.rs @@ -1,5 +1,3 @@ -#![allow(non_snake_case)] - //! Suspense in Dioxus //! //! Currently, `rsx!` does not accept futures as values. To achieve the functionality @@ -49,6 +47,7 @@ fn app() -> Element { /// This component will re-render when the future has finished /// Suspense is achieved my moving the future into only the component that /// actually renders the data. +#[component] fn Doggo() -> Element { let mut fut = use_resource(move || async move { #[derive(serde::Deserialize)] diff --git a/examples/svg.rs b/examples/svg.rs index 022a2aa819..ae71f2bb92 100644 --- a/examples/svg.rs +++ b/examples/svg.rs @@ -1,4 +1,10 @@ -// Thanks to @japsu and their project https://github.com/japsu/jatsi for the example! +//! Thanks to @japsu and their project https://github.com/japsu/jatsi for the example! +//! +//! This example shows how to create a simple dice rolling app using SVG and Dioxus. +//! The `svg` element and its children have a custom namespace, and are attached using different methods than regular +//! HTML elements. Any element can specify a custom namespace by using the `namespace` meta attribute. +//! +//! If you `go-to-definition` on the `svg` element, you'll see its custom namespace. use dioxus::prelude::*; use rand::{thread_rng, Rng}; diff --git a/examples/tasks.rs b/examples/tasks.rs deleted file mode 100644 index 887668fb98..0000000000 --- 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 3ad2143ec9..0000000000 --- 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 25a124c71a..2af2daf035 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 b1e23e20d7..9c3c4c9194 100644 --- a/examples/video_stream.rs +++ b/examples/video_stream.rs @@ -1,35 +1,26 @@ +//! Using `wry`'s http module, we can stream a video file from the local file system. +//! +//! You could load in any file type, but this example uses a video file. + use dioxus::desktop::wry::http; use dioxus::desktop::wry::http::Response; 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"; fn main() { - let video_file = PathBuf::from(VIDEO_PATH); - if !video_file.exists() { - tokio::runtime::Runtime::new() - .unwrap() - .block_on(async move { - println!("Downloading video file..."); - let video_url = - "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"; - let mut response = reqwest::get(video_url).await.unwrap(); - let mut file = tokio::fs::File::create(&video_file).await.unwrap(); - while let Some(chunk) = response.chunk().await.unwrap() { - file.write_all(&chunk).await.unwrap(); - } - }); - } + // For the sake of this example, we will download the video file if it doesn't exist + ensure_video_is_loaded(); + launch_desktop(app); } fn app() -> Element { + // Any request to /videos will be handled by this handler use_asset_handler("videos", move |request, responder| { // Using dioxus::spawn works, but is slower than a dedicated thread tokio::task::spawn(async move { @@ -186,3 +177,21 @@ async fn get_stream_response( http_response.map_err(Into::into) } + +fn ensure_video_is_loaded() { + let video_file = PathBuf::from(VIDEO_PATH); + if !video_file.exists() { + tokio::runtime::Runtime::new() + .unwrap() + .block_on(async move { + println!("Downloading video file..."); + let video_url = + "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"; + let mut response = reqwest::get(video_url).await.unwrap(); + let mut file = tokio::fs::File::create(&video_file).await.unwrap(); + while let Some(chunk) = response.chunk().await.unwrap() { + file.write_all(&chunk).await.unwrap(); + } + }); + } +} diff --git a/examples/web_component.rs b/examples/web_component.rs index 9aa712891f..ef3f08aa9e 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! { + 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 { - "my-prop": "5%", + // 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 edb7c68d8b..7de6dc01c9 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 04a4b22aed..447544077b 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 0c8e19e103..3bf47d4cc8 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 de83c6b0dd..ade023c2f3 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 442b63aa5c..01d5793ec1 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 4d3589ca66..eeb44d4d09 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 1645f8d0d9..1d0be09795 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,