codify "platform" into resolution logic, server-fn/fullstack overhaul (#4596)

overhaul dioxus fullstack using a new implementation of server_fn

This PR is a complete rewrite of dioxus fullstack designed to improve compatibility with axum and to fix a number of compilation errors based on feature flag unification plumbing through macros.
This commit is contained in:
Jonathan Kelley authored and GitHub committed 2025-10-03 18:26:19 -07:00
1 parent 8421c3aef0
commit f2b1c6d97f
424 files changed
+17284 -6624

No files matched your search

+65
View File
@@ -0,0 +1,65 @@
//! 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 async_std::task::sleep;
use dioxus::prelude::*;
const STYLE: Asset = asset!("/examples/assets/roulette.css");
fn main() {
dioxus::launch(app);
}
fn app() -> Element {
// Element data is stored as Rc<MountedData> so we can clone it and pass it around
let mut elements = use_signal(Vec::<Rc<MountedData>>::new);
let mut running = use_signal(|| true);
use_future(move || async move {
let mut focused = 0;
loop {
sleep(std::time::Duration::from_millis(50)).await;
if !running() {
continue;
}
if let Some(element) = elements.with(|f| f.get(focused).cloned()) {
_ = element.set_focus(true).await;
} else {
focused = 0;
}
focused += 1;
}
});
rsx! {
Stylesheet { href: STYLE }
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),
}
}
}
}
}
+34
View File
@@ -0,0 +1,34 @@
//! This example shows how to use a custom index.html and custom <HEAD> extensions
//! to add things like stylesheets, scripts, and third-party JS libraries.
use dioxus::prelude::*;
fn main() {
dioxus::LaunchBuilder::new()
.with_cfg(
dioxus::desktop::Config::new().with_custom_index(
r#"
<!DOCTYPE html>
<html>
<head>
<title>Dioxus app</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<style>body { background-color: olive; }</style>
</head>
<body>
<h1>External HTML</h1>
<div id="main"></div>
</body>
</html>
"#
.into(),
),
)
.launch(app);
}
fn app() -> Element {
rsx! {
h1 { "Custom HTML!" }
}
}
+49
View File
@@ -0,0 +1,49 @@
//! This example shows how to use a custom menu bar with Dioxus desktop.
//! This example is not supported on the mobile or web renderers.
use dioxus::desktop::{muda::*, use_muda_event_handler};
use dioxus::prelude::*;
fn main() {
// Create a menu bar that only contains the edit menu
let menu = Menu::new();
let edit_menu = Submenu::new("Edit", true);
edit_menu
.append_items(&[
&PredefinedMenuItem::undo(None),
&PredefinedMenuItem::redo(None),
&PredefinedMenuItem::separator(),
&PredefinedMenuItem::cut(None),
&PredefinedMenuItem::copy(None),
&PredefinedMenuItem::paste(None),
&PredefinedMenuItem::select_all(None),
&MenuItem::with_id("switch-text", "Switch text", true, None),
])
.unwrap();
menu.append(&edit_menu).unwrap();
// Create a desktop config that overrides the default menu with the custom menu
let config = dioxus::desktop::Config::new().with_menu(menu);
// Launch the app with the custom menu
dioxus::LaunchBuilder::new().with_cfg(config).launch(app)
}
fn app() -> Element {
let mut text = use_signal(String::new);
// You can use the `use_muda_event_handler` hook to run code when a menu event is triggered.
use_muda_event_handler(move |muda_event| {
if muda_event.id() == "switch-text" {
text.set("Switched to text".to_string());
}
});
rsx! {
div {
h1 { "Custom Menu" }
p { "Text: {text}" }
}
}
}
+47
View File
@@ -0,0 +1,47 @@
//! 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 async_std::task::sleep;
use dioxus::prelude::*;
fn main() {
dioxus::launch(app);
}
fn app() -> Element {
// Create a future that will resolve once the javascript has been successfully executed.
let future = use_resource(move || async move {
// Wait a little bit just to give the appearance of a loading screen
sleep(std::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 = document::eval(
r#"
dioxus.send("Hi from JS!");
let msg = await dioxus.recv();
console.log(msg);
return "hi from JS!";
"#,
);
// Send a message to the JS code.
eval.send("Hi from Rust!").unwrap();
// Our line on the JS side will log the message and then return "hello world".
let res: String = eval.recv().await.unwrap();
// This will print "Hi from JS!" and "Hi from Rust!".
println!("{:?}", eval.await);
res
});
match future.value().as_ref() {
Some(v) => rsx!( p { "{v}" } ),
_ => rsx!( p { "waiting.." } ),
}
}
+99
View File
@@ -0,0 +1,99 @@
//! 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 dioxus_html::FileData;
const STYLE: Asset = asset!("/examples/assets/file_upload.css");
fn main() {
dioxus::launch(app);
}
struct UploadedFile {
name: String,
contents: String,
}
fn app() -> Element {
let mut enable_directory_upload = use_signal(|| false);
let mut files_uploaded = use_signal(|| Vec::new() as Vec<UploadedFile>);
let mut hovered = use_signal(|| false);
let upload_files = move |files: Vec<FileData>| async move {
for file in files {
let filename = file.name();
if let Ok(contents) = file.read_string().await {
files_uploaded.write().push(UploadedFile {
name: filename,
contents,
});
} else {
files_uploaded.write().push(UploadedFile {
name: filename,
contents: "Failed to read file".into(),
});
}
}
};
rsx! {
Stylesheet { href: STYLE }
h1 { "File Upload Example" }
p { "Drop a .txt, .rs, or .js file here to read it" }
button { onclick: move |_| files_uploaded.write().clear(), "Clear files" }
div {
label { r#for: "directory-upload", "Enable directory upload" }
input {
r#type: "checkbox",
id: "directory-upload",
checked: enable_directory_upload,
oninput: move |evt| enable_directory_upload.set(evt.checked()),
}
}
div {
label { r#for: "textreader", "Upload text/rust files and read them" }
input {
r#type: "file",
accept: ".txt,.rs,.js",
multiple: true,
name: "textreader",
directory: enable_directory_upload,
onchange: move |evt| async move {
upload_files(evt.files()).await
},
}
}
div {
id: "drop-zone",
background_color: if hovered() { "lightblue" } else { "lightgray" },
ondragover: move |evt| {
evt.prevent_default();
hovered.set(true)
},
ondragleave: move |_| hovered.set(false),
ondrop: move |evt| async move {
evt.prevent_default();
hovered.set(false);
upload_files(evt.files()).await;
},
"Drop files here"
}
ul {
for file in files_uploaded.read().iter().rev() {
li {
span { "{file.name}" }
pre { "{file.contents}" }
}
}
}
}
}
+142
View File
@@ -0,0 +1,142 @@
//! Forms
//!
//! Dioxus forms deviate slightly from html, automatically returning all named inputs
//! in the "values" field.
use dioxus::prelude::*;
use std::collections::HashMap;
fn main() {
dioxus::launch(app);
}
fn app() -> Element {
let mut values = use_signal(HashMap::new);
let mut submitted_values = use_signal(HashMap::new);
rsx! {
div { style: "display: flex",
div { style: "width: 50%",
h1 { "Form" }
if !submitted_values.read().is_empty() {
h2 { "Submitted! ✅" }
}
// The form element is used to create an HTML form for user input
// You can attach regular attributes to it
form {
id: "cool-form",
style: "display: flex; flex-direction: column;",
// You can attach a handler to the entire form
oninput: move |ev| {
println!("Input event: {:#?}", ev);
values.set(ev.values());
},
// On desktop/liveview, the form will not navigate the page - the expectation is that you handle
// The form event.
// However, if your form doesn't have a submit handler, it might navigate the page depending on the webview.
// We suggest always attaching a submit handler to the form.
onsubmit: move |ev| {
println!("Submit event: {:#?}", ev);
submitted_values.set(ev.values());
},
// Regular text inputs with handlers
label { r#for: "username", "Username" }
input {
r#type: "text",
name: "username",
oninput: move |ev| {
println!("setting username");
values.set(ev.values());
}
}
// And then the various inputs that might exist
// Note for a value to be returned in .values(), it must be named!
label { r#for: "full-name", "Full Name" }
input { r#type: "text", name: "full-name" }
input { r#type: "text", name: "full-name" }
label { r#for: "email", "Email (matching <name>@example.com)" }
input { r#type: "email", pattern: ".+@example\\.com", size: "30", required: "true", id: "email", name: "email" }
label { r#for: "password", "Password" }
input { r#type: "password", name: "password" }
label { r#for: "color", "Color" }
input { r#type: "radio", checked: true, name: "color", value: "red" }
input { r#type: "radio", name: "color", value: "blue" }
input { r#type: "radio", name: "color", value: "green" }
// Select multiple comes in as a comma separated list of selected values
// You should split them on the comma to get the values manually
label { r#for: "country", "Country" }
select {
name: "country",
multiple: true,
oninput: move |ev| {
println!("Input event: {:#?}", ev);
println!("Values: {:#?}", ev.value().split(',').collect::<Vec<_>>());
},
option { value: "usa", "USA" }
option { value: "canada", "Canada" }
option { value: "mexico", "Mexico" }
}
// Safari can be quirky with color inputs on mac.
// We recommend always providing a text input for color as a fallback.
label { r#for: "color", "Color" }
input { r#type: "color", value: "#000002", name: "head", id: "head" }
// Dates!
input {
min: "2018-01-01",
value: "2018-07-22",
r#type: "date",
name: "trip-start",
max: "2025-12-31",
id: "start"
}
// CHekcboxes
label { r#for: "cbox", "Color" }
div {
label { r#for: "cbox-red", "red" }
input { r#type: "checkbox", checked: true, name: "cbox", value: "red", id: "cbox-red" }
}
div {
label { r#for: "cbox-blue", "blue" }
input { r#type: "checkbox", name: "cbox", value: "blue", id: "cbox-blue" }
}
div {
label { r#for: "cbox-green", "green" }
input { r#type: "checkbox", name: "cbox", value: "green", id: "cbox-green" }
}
div {
label { r#for: "cbox-yellow", "yellow" }
input { r#type: "checkbox", name: "cbox", value: "yellow", id: "cbox-yellow" }
}
// Buttons will submit your form by default.
button { r#type: "submit", value: "Submit", "Submit the form" }
}
}
div { style: "width: 50%",
h1 { "Oninput Values" }
pre { "{values:#?}" }
}
}
button {
onclick: move |_| {
println!("Values: {:#?}", values.read());
},
"Log values"
}
}
}
+53
View File
@@ -0,0 +1,53 @@
//! Dioxus ships out-of-the-box with tracing hooks that integrate with the Dioxus-CLI.
//!
//! The built-in tracing-subscriber automatically sets up a wasm panic hook and wires up output
//! to be consumed in a machine-readable format when running under `dx`.
//!
//! You can disable the built-in tracing-subscriber or customize the log level yourself.
//!
//! By default:
//! - in `dev` mode, the default log output is `debug`
//! - in `release` mode, the default log output is `info`
//!
//! To use the dioxus logger in your app, simply call any of the tracing functions (info!(), warn!(), error!())
use dioxus::logger::tracing::{Level, debug, error, info, warn};
use dioxus::prelude::*;
fn main() {
// `dioxus::logger::init` is optional and called automatically by `dioxus::launch`.
// In development mode, the `Debug` tracing level is set, and in release only the `Info` level is set.
// You can call it yourself manually in the cases you:
// - want to customize behavior
// - aren't using `dioxus::launch` (i.e. custom fullstack setups) but want the integration.
// The Tracing crate is the logging interface that the dioxus-logger uses.
dioxus::logger::init(Level::INFO).expect("Failed to initialize logger");
dioxus::launch(app);
}
fn app() -> Element {
rsx! {
div {
h1 { "Logger demo" }
button {
onclick: move |_| warn!("Here's a warning!"),
"Warn!"
}
button {
onclick: move |_| error!("Here's an error!"),
"Error!"
}
button {
onclick: move |_| {
debug!("Here's a debug");
warn!("The log level is set to info so there should not be a debug message")
},
"Debug!"
}
button {
onclick: move |_| info!("Here's an info!"),
"Info!"
}
}
}
}
+32
View File
@@ -0,0 +1,32 @@
//! Multiwindow example
//!
//! This example 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() {
dioxus::launch(app);
}
fn app() -> Element {
let onclick = move |_| {
dioxus::desktop::window().new_window(VirtualDom::new(popup), Default::default());
};
rsx! {
button { onclick, "New Window" }
}
}
fn popup() -> Element {
let mut count = use_signal(|| 0);
rsx! {
div {
h1 { "Popup Window" }
p { "Count: {count}" }
button { onclick: move |_| count += 1, "Increment" }
}
}
}
@@ -0,0 +1,45 @@
//! Multiwindow with tray icon example
//!
//! This example shows how to implement a simple multiwindow application and tray icon 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.
//!
//! This is useful for apps that incorporate settings panels or persistent windows like Raycast.
use dioxus::desktop::{
WindowCloseBehaviour,
trayicon::{default_tray_icon, init_tray_icon},
window,
};
use dioxus::prelude::*;
fn main() {
dioxus::launch(app);
}
fn app() -> Element {
use_hook(|| {
// Set the close behavior for the main window
// This will hide the window instead of closing it when the user clicks the close button
window().set_close_behavior(WindowCloseBehaviour::WindowHides);
// Initialize the tray icon with a default icon and no menu
// This will provide the tray into context for the application
init_tray_icon(default_tray_icon(), None)
});
rsx! {
button {
onclick: move |_| {
window().new_window(VirtualDom::new(popup), Default::default());
},
"New Window"
}
}
}
fn popup() -> Element {
rsx! {
div { "This is a popup window!" }
}
}
+27
View File
@@ -0,0 +1,27 @@
//! Run a callback
//!
//! Whenever an Element is finally mounted to the Dom, its data is available 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 dioxus::prelude::*;
use dioxus_elements::geometry::euclid::Size2D;
fn main() {
dioxus::launch(app);
}
fn app() -> Element {
let mut dimensions = use_signal(Size2D::zero);
rsx!(
Stylesheet { href: asset!("/examples/assets/read_size.css") }
div {
width: "50%",
height: "50%",
background_color: "red",
onresize: move |evt| dimensions.set(evt.data().get_content_box_size().unwrap()),
"This element is {dimensions():?}"
}
)
}
+85
View File
@@ -0,0 +1,85 @@
//! Port of the https://codepen.io/ryanfinni/pen/VwZeGxN example
use dioxus::prelude::*;
fn main() {
dioxus::launch(app);
}
fn app() -> Element {
let mut animated_classes = use_signal(|| ["animated-text", ""]);
rsx! {
Stylesheet { href: asset!("/examples/assets/visible.css") }
div {
class: "container",
p {
"Scroll to the bottom of the page. The text will transition in when it becomes visible in the viewport."
}
p {
"First, let's create a new project for our hacker news app. We can use the CLI to create a new
project. You can select a platform of your choice or view the getting started guide for more information
on each option. If you aren't sure what platform to try out, we recommend getting started with web or
desktop:"
}
p {
"The template contains some boilerplate to help you get started. For this guide, we will be rebuilding some of the code
from scratch for learning purposes. You can clear the src/main.rs file. We will be adding new code in the next
sections."
}
p {
"Next, let's setup our dependencies. We need to set up a few dependencies to work with the hacker news API: "
}
p {
"First, let's create a new project for our hacker news app. We can use the CLI to create a new
project. You can select a platform of your choice or view the getting started guide for more information
on each option. If you aren't sure what platform to try out, we recommend getting started with web or
desktop:"
}
p {
"The template contains some boilerplate to help you get started. For this guide, we will be rebuilding some of the code
from scratch for learning purposes. You can clear the src/main.rs file. We will be adding new code in the next
sections."
}
p {
"Next, let's setup our dependencies. We need to set up a few dependencies to work with the hacker news API: "
}
p {
"First, let's create a new project for our hacker news app. We can use the CLI to create a new
project. You can select a platform of your choice or view the getting started guide for more information
on each option. If you aren't sure what platform to try out, we recommend getting started with web or
desktop:"
}
p {
"The template contains some boilerplate to help you get started. For this guide, we will be rebuilding some of the code
from scratch for learning purposes. You can clear the src/main.rs file. We will be adding new code in the next
sections."
}
p {
"Next, let's setup our dependencies. We need to set up a few dependencies to work with the hacker news API: "
}
h2 {
class: animated_classes().join(" "),
onvisible: move |evt| {
let data = evt.data();
if let Ok(is_intersecting) = data.is_intersecting() {
animated_classes.write()[1] = if is_intersecting { "visible" } else { "" };
}
},
"Animated Text"
}
}
}
}
+62
View File
@@ -0,0 +1,62 @@
//! 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::{
HotKeyState, LogicalSize, WindowBuilder, tao::dpi::PhysicalPosition, use_global_shortcut,
};
use dioxus::prelude::*;
fn main() {
dioxus::LaunchBuilder::desktop()
.with_cfg(make_config())
.launch(app);
}
fn app() -> Element {
let mut show_overlay = use_signal(|| true);
_ = use_global_shortcut("cmd+g", move |state| {
if state == HotKeyState::Pressed {
show_overlay.toggle();
}
});
rsx! {
Stylesheet { href: asset!("/examples/assets/overlay.css") }
if show_overlay() {
div {
width: "100%",
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!"
}
}
}
}
fn make_config() -> dioxus::desktop::Config {
dioxus::desktop::Config::default().with_window(make_window())
}
fn make_window() -> WindowBuilder {
WindowBuilder::new()
.with_transparent(true)
.with_decorations(false)
.with_resizable(false)
.with_always_on_top(true)
.with_position(PhysicalPosition::new(0, 0))
.with_max_inner_size(LogicalSize::new(100000, 50))
}
+42
View File
@@ -0,0 +1,42 @@
//! Read the size of elements using the MountedData struct.
//!
//! Whenever an Element is finally mounted to the Dom, its data is available 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::*};
fn main() {
dioxus::launch(app);
}
fn app() -> Element {
let mut div_element = use_signal(|| None as Option<Rc<MountedData>>);
let mut dimensions = use_signal(Rect::zero);
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);
}
}
};
rsx! {
Stylesheet { href: asset!("/examples/assets/read_size.css") }
div {
width: "50%",
height: "50%",
background_color: "red",
onmounted: move |cx| div_element.set(Some(cx.data())),
"This element is {dimensions():?}"
}
button { onclick: read_dims, "Read dimensions" }
}
}
+59
View File
@@ -0,0 +1,59 @@
//! 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 a given y offset of the scrollable parent using the `scroll` method on the `MountedData`
use dioxus::html::geometry::PixelsVector2D;
use dioxus::prelude::*;
fn main() {
dioxus::launch(app);
}
fn app() -> Element {
rsx! {
ScrollToCoordinates {}
ScrollToCoordinates {}
}
}
#[component]
fn ScrollToCoordinates() -> Element {
let mut element = use_signal(|| None);
rsx! {
div { border: "1px solid black", position: "relative",
div {
height: "300px",
overflow_y: "auto",
onmounted: move |event| element.set(Some(event.data())),
for i in 0..100 {
div { height: "20px", "Item {i}" }
}
}
div { position: "absolute", top: 0, right: 0,
input {
r#type: "number",
min: "0",
max: "99",
oninput: move |event| async move {
if let Some(ul) = element.cloned() {
let data = event.data();
if let Ok(value) = data.parsed::<f64>() {
ul.scroll(PixelsVector2D::new(0.0, 20.0 * value), ScrollBehavior::Smooth)
.await
.unwrap();
}
}
},
}
}
}
}
}
+38
View File
@@ -0,0 +1,38 @@
//! 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() {
dioxus::launch(app);
}
fn app() -> Element {
let mut header_element = use_signal(|| None);
rsx! {
div {
h1 {
onmounted: move |cx| header_element.set(Some(cx.data())),
"Scroll to top example"
}
for i in 0..100 {
div { "Item {i}" }
}
button {
onclick: move |_| async move {
if let Some(header) = header_element.cloned() {
header.scroll_to(ScrollBehavior::Smooth).await.unwrap();
}
},
"Scroll to top"
}
}
}
}
+25
View File
@@ -0,0 +1,25 @@
//! 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::{HotKeyState, use_global_shortcut};
use dioxus::prelude::*;
fn main() {
dioxus::LaunchBuilder::desktop().launch(app);
}
fn app() -> Element {
let mut toggled = use_signal(|| false);
_ = use_global_shortcut("ctrl+s", move |state| {
if state == HotKeyState::Pressed {
toggled.toggle();
}
});
rsx!("toggle: {toggled}")
}
+42
View File
@@ -0,0 +1,42 @@
//! 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::*;
fn main() {
// We can render VirtualDoms
let vdom = VirtualDom::prebuilt(app);
println!("{}", dioxus_ssr::render(&vdom));
// Or we can render rsx! calls themselves
println!(
"{}",
dioxus_ssr::render_element(rsx! {
div {
h1 { "Hello, world!" }
}
})
);
// We can configure the SSR rendering to add ids for rehydration
println!("{}", dioxus_ssr::pre_render(&vdom));
// We can render to a buf directly too
let mut file = String::new();
let mut renderer = dioxus_ssr::Renderer::default();
renderer.render_to(&mut file, &vdom).unwrap();
println!("{file}");
}
fn app() -> Element {
rsx!(
div {
h1 { "Title" }
p { "Body" }
}
)
}
+22
View File
@@ -0,0 +1,22 @@
//! This example shows how to set the title of the page or window with the Title component
use dioxus::prelude::*;
fn main() {
dioxus::launch(app);
}
fn app() -> Element {
let mut count = use_signal(|| 0);
rsx! {
div {
// You can set the title of the page with the Title component
// In web applications, this sets the title in the head.
// On desktop, it sets the window title
Title { "My Application (Count {count})" }
button { onclick: move |_| count += 1, "Up high!" }
button { onclick: move |_| count -= 1, "Down low!" }
}
}
}
+197
View File
@@ -0,0 +1,197 @@
//! 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::{AssetRequest, use_asset_handler};
use dioxus::prelude::*;
use http::{header::*, response::Builder as ResponseBuilder, status::StatusCode};
use std::{io::SeekFrom, path::PathBuf};
use tokio::io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt};
const VIDEO_PATH: &str = "./examples/assets/test_video.mp4";
fn main() {
// For the sake of this example, we will download the video file if it doesn't exist
ensure_video_is_loaded();
dioxus::launch(app);
}
fn app() -> Element {
// Any request to /videos will be handled by this handler
use_asset_handler("videos", move |request, responder| {
// Using spawn works, but is slower than a dedicated thread
tokio::task::spawn(async move {
let video_file = PathBuf::from(VIDEO_PATH);
let mut file = tokio::fs::File::open(&video_file).await.unwrap();
match get_stream_response(&mut file, &request).await {
Ok(response) => responder.respond(response),
Err(err) => eprintln!("Error: {}", err),
}
});
});
rsx! {
div {
video {
src: "/videos/test_video.mp4",
autoplay: true,
controls: true,
width: 640,
height: 480
}
}
}
}
/// This was taken from wry's example
async fn get_stream_response(
asset: &mut (impl tokio::io::AsyncSeek + tokio::io::AsyncRead + Unpin + Send + Sync),
request: &AssetRequest,
) -> Result<Response<Vec<u8>>, Box<dyn std::error::Error>> {
// get stream length
let len = {
let old_pos = asset.stream_position().await?;
let len = asset.seek(SeekFrom::End(0)).await?;
asset.seek(SeekFrom::Start(old_pos)).await?;
len
};
let mut resp = ResponseBuilder::new().header(CONTENT_TYPE, "video/mp4");
// if the webview sent a range header, we need to send a 206 in return
// Actually only macOS and Windows are supported. Linux will ALWAYS return empty headers.
let http_response = if let Some(range_header) = request.headers().get("range") {
let not_satisfiable = || {
ResponseBuilder::new()
.status(StatusCode::RANGE_NOT_SATISFIABLE)
.header(CONTENT_RANGE, format!("bytes */{len}"))
.body(vec![])
};
// parse range header
let ranges = if let Ok(ranges) = http_range::HttpRange::parse(range_header.to_str()?, len) {
ranges
.iter()
// map the output back to spec range <start-end>, example: 0-499
.map(|r| (r.start, r.start + r.length - 1))
.collect::<Vec<_>>()
} else {
return Ok(not_satisfiable()?);
};
/// The Maximum bytes we send in one range
const MAX_LEN: u64 = 1000 * 1024;
if ranges.len() == 1 {
let &(start, mut end) = ranges.first().unwrap();
// check if a range is not satisfiable
//
// this should be already taken care of by HttpRange::parse
// but checking here again for extra assurance
if start >= len || end >= len || end < start {
return Ok(not_satisfiable()?);
}
// adjust end byte for MAX_LEN
end = start + (end - start).min(len - start).min(MAX_LEN - 1);
// calculate number of bytes needed to be read
let bytes_to_read = end + 1 - start;
// allocate a buf with a suitable capacity
let mut buf = Vec::with_capacity(bytes_to_read as usize);
// seek the file to the starting byte
asset.seek(SeekFrom::Start(start)).await?;
// read the needed bytes
asset.take(bytes_to_read).read_to_end(&mut buf).await?;
resp = resp.header(CONTENT_RANGE, format!("bytes {start}-{end}/{len}"));
resp = resp.header(CONTENT_LENGTH, end + 1 - start);
resp = resp.status(StatusCode::PARTIAL_CONTENT);
resp.body(buf)
} else {
let mut buf = Vec::new();
let ranges = ranges
.iter()
.filter_map(|&(start, mut end)| {
// filter out unsatisfiable ranges
//
// this should be already taken care of by HttpRange::parse
// but checking here again for extra assurance
if start >= len || end >= len || end < start {
None
} else {
// adjust end byte for MAX_LEN
end = start + (end - start).min(len - start).min(MAX_LEN - 1);
Some((start, end))
}
})
.collect::<Vec<_>>();
let boundary = format!("{:x}", rand::random::<u64>());
let boundary_sep = format!("\r\n--{boundary}\r\n");
let boundary_closer = format!("\r\n--{boundary}\r\n");
resp = resp.header(
CONTENT_TYPE,
format!("multipart/byteranges; boundary={boundary}"),
);
for (end, start) in ranges {
// a new range is being written, write the range boundary
buf.write_all(boundary_sep.as_bytes()).await?;
// write the needed headers `Content-Type` and `Content-Range`
buf.write_all(format!("{CONTENT_TYPE}: video/mp4\r\n").as_bytes())
.await?;
buf.write_all(format!("{CONTENT_RANGE}: bytes {start}-{end}/{len}\r\n").as_bytes())
.await?;
// write the separator to indicate the start of the range body
buf.write_all("\r\n".as_bytes()).await?;
// calculate number of bytes needed to be read
let bytes_to_read = end + 1 - start;
let mut local_buf = vec![0_u8; bytes_to_read as usize];
asset.seek(SeekFrom::Start(start)).await?;
asset.read_exact(&mut local_buf).await?;
buf.extend_from_slice(&local_buf);
}
// all ranges have been written, write the closing boundary
buf.write_all(boundary_closer.as_bytes()).await?;
resp.body(buf)
}
} else {
resp = resp.header(CONTENT_LENGTH, len);
let mut buf = Vec::with_capacity(len as usize);
asset.read_to_end(&mut buf).await?;
resp.body(buf)
};
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();
}
});
}
}
+250
View File
@@ -0,0 +1,250 @@
//! Demonstrate how to use dioxus as a child window for use in alternative renderers like wgpu.
//!
//! The code here is borrowed from wry's example:
//! https://github.com/tauri-apps/wry/blob/dev/examples/wgpu.rs
//!
//! To use this feature set `with_as_child_window()` on your desktop config which will then let you
use dioxus::desktop::{
Config, DesktopContext,
tao::{event::Event as WryEvent, window::WindowBuilder},
use_wry_event_handler, window,
};
use dioxus::prelude::*;
fn main() {
let config = Config::new()
.with_window(WindowBuilder::new().with_transparent(true))
.with_as_child_window();
dioxus::LaunchBuilder::desktop()
.with_cfg(config)
.launch(app);
}
fn app() -> Element {
let graphics_resources = use_resource(move || async {
GraphicsContextAsyncBuilder {
desktop: window(),
resources_builder: |ctx| Box::pin(GraphicsResources::new(ctx)),
}
.build()
.await
});
// on first render request a redraw
use_effect(|| {
window().window.request_redraw();
});
use_wry_event_handler(move |event, _| {
use dioxus::desktop::tao::event::WindowEvent;
if let WryEvent::RedrawRequested(_id) = event {
let resources = graphics_resources.read();
if let Some(resources) = resources.as_ref() {
resources.with_resources(|resources| resources.render());
}
}
if let WryEvent::WindowEvent {
event: WindowEvent::Resized(new_size),
..
} = event
{
let ctx = graphics_resources.value();
if let Some(ctx_ref) = ctx.as_ref() {
ctx_ref.with_resources(|srcs| {
let mut cfg = srcs.config.clone();
cfg.width = new_size.width;
cfg.height = new_size.height;
srcs.surface.configure(&srcs.device, &cfg);
});
}
window().window.request_redraw();
}
});
rsx! {
div {
color: "blue",
width: "100vw",
height: "100vh",
display: "flex",
justify_content: "center",
align_items: "center",
font_size: "20px",
div { "text overlaid on a wgpu surface!" }
}
}
}
/// This borrows from the `window` which is contained within an `Arc` so we need to wrap it in a self-borrowing struct
/// to be able to borrow the window for the wgpu::Surface
#[ouroboros::self_referencing]
struct GraphicsContext {
desktop: DesktopContext,
#[borrows(desktop)]
#[not_covariant]
resources: GraphicsResources<'this>,
}
struct GraphicsResources<'a> {
surface: wgpu::Surface<'a>,
device: wgpu::Device,
pipeline: wgpu::RenderPipeline,
queue: wgpu::Queue,
config: wgpu::SurfaceConfiguration,
}
impl<'a> GraphicsResources<'a> {
async fn new(context: &'a DesktopContext) -> Self {
let window = &context.window;
let size = window.inner_size();
let instance = wgpu::Instance::default();
let surface: wgpu::Surface<'a> = instance.create_surface(window).unwrap();
let adapter = instance
.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::default(),
force_fallback_adapter: false,
// Request an adapter which can render to our surface
compatible_surface: Some(&surface),
})
.await
.expect("Failed to find an appropriate adapter");
// Create the logical device and command queue
let (device, queue) = adapter
.request_device(
&wgpu::DeviceDescriptor {
label: None,
required_features: wgpu::Features::empty(),
// Make sure we use the texture resolution limits from the adapter, so we can support images the size of the swapchain.
required_limits: wgpu::Limits::downlevel_webgl2_defaults()
.using_resolution(adapter.limits()),
memory_hints: wgpu::MemoryHints::default(),
},
None,
)
.await
.expect("Failed to create device");
// Load the shaders from disk
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: None,
source: wgpu::ShaderSource::Wgsl(
r#"
@vertex
fn vs_main(@builtin(vertex_index) in_vertex_index: u32) -> @builtin(position) vec4<f32> {
let x = f32(i32(in_vertex_index) - 1);
let y = f32(i32(in_vertex_index & 1u) * 2 - 1);
return vec4<f32>(x, y, 0.0, 1.0);
}
@fragment
fn fs_main() -> @location(0) vec4<f32> {
return vec4<f32>(1.0, 0.0, 0.0, 1.0);
}
"#
.into(),
),
});
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: None,
bind_group_layouts: &[],
push_constant_ranges: &[],
});
let swapchain_capabilities = surface.get_capabilities(&adapter);
let swapchain_format = swapchain_capabilities.formats[0];
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: None,
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs_main"),
buffers: &[],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("fs_main"),
targets: &[Some(swapchain_format.into())],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
primitive: wgpu::PrimitiveState::default(),
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
multiview: None,
cache: None,
});
let config = wgpu::SurfaceConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
format: swapchain_format,
width: size.width,
height: size.height,
present_mode: wgpu::PresentMode::Fifo,
desired_maximum_frame_latency: 2,
alpha_mode: wgpu::CompositeAlphaMode::PostMultiplied,
view_formats: vec![],
};
surface.configure(&device, &config);
GraphicsResources {
surface,
device,
pipeline,
queue,
config,
}
}
fn render(&self) {
let GraphicsResources {
surface,
device,
pipeline,
queue,
..
} = self;
let frame = surface
.get_current_texture()
.expect("Failed to acquire next swap chain texture");
let view = frame
.texture
.create_view(&wgpu::TextureViewDescriptor::default());
let mut encoder =
device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
{
let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: None,
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
});
rpass.set_pipeline(pipeline);
rpass.draw(0..3, 0..1);
}
queue.submit(Some(encoder.finish()));
frame.present();
}
}
+136
View File
@@ -0,0 +1,136 @@
//! 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::{Config, WindowBuilder, window};
use dioxus::prelude::*;
fn main() {
dioxus::LaunchBuilder::desktop()
.with_cfg(
Config::new().with_window(
WindowBuilder::new()
.with_title("Borderless Window")
.with_decorations(false),
),
)
.launch(app)
}
fn app() -> Element {
rsx!(
document::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 {}
}
}
)
}
#[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(),
onclick: move |_| {
window().set_fullscreen(!fullscreen());
window().set_resizable(fullscreen());
fullscreen.toggle();
},
"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(),
onclick: move |_| window().close(),
"Close"
}
}
}
}
}
#[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"
}
}
}
}
+39
View File
@@ -0,0 +1,39 @@
//! 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;
use dioxus::prelude::*;
fn main() {
dioxus::launch(app);
}
fn app() -> Element {
let mut focused = use_signal(|| true);
use_wry_event_handler(move |event, _| {
if let WryEvent::WindowEvent {
event: WindowEvent::Focused(new_focused),
..
} = event
{
focused.set(*new_focused)
}
});
rsx! {
div { width: "100%", height: "100%", display: "flex", flex_direction: "column", align_items: "center",
if focused() {
"This window is focused!"
} else {
"This window is not focused!"
}
}
}
}
+70
View File
@@ -0,0 +1,70 @@
//! This example shows how to create a popup window and send data back to the parent window.
//! Currently Dioxus doesn't support nested renderers, hence the need to create popups as separate windows.
use dioxus::prelude::*;
use std::rc::Rc;
fn main() {
dioxus::LaunchBuilder::desktop().launch(app);
}
fn app() -> Element {
let mut emails_sent = use_signal(|| Vec::new() as Vec<String>);
// Wait for responses to the compose channel, and then push them to the emails_sent signal.
let handle = use_coroutine(move |mut rx: UnboundedReceiver<String>| async move {
use futures_util::StreamExt;
while let Some(message) = rx.next().await {
emails_sent.write().push(message);
}
});
let open_compose_window = move |_evt: MouseEvent| {
let tx = handle.tx();
dioxus::desktop::window().new_window(
VirtualDom::new_with_props(popup, Rc::new(move |s| tx.unbounded_send(s).unwrap())),
Default::default(),
);
};
rsx! {
h1 { "This is your email" }
button { onclick: open_compose_window, "Click to compose a new email" }
ul {
for message in emails_sent.read().iter() {
li {
h3 { "email" }
span { "{message}" }
}
}
}
}
}
fn popup(send: Rc<dyn Fn(String)>) -> Element {
let mut user_input = use_signal(String::new);
let window = dioxus::desktop::use_window();
let close_window = move |_| {
println!("Attempting to close Window B");
window.close();
};
rsx! {
div {
h1 { "Compose a new email" }
button {
onclick: close_window,
"Close Window B (button)"
}
button {
onclick: move |_| {
send(user_input.cloned());
dioxus::desktop::window().close();
},
"Send"
}
input { oninput: move |e| user_input.set(e.value()), value: "{user_input}" }
}
}
}
+28
View File
@@ -0,0 +1,28 @@
//! 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() {
dioxus::LaunchBuilder::desktop().launch(app);
}
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}",
oninput: move |e| {
if let Ok(new_zoom) = e.value().parse::<f64>() {
level.set(new_zoom);
_ = dioxus::desktop::window().webview.zoom(new_zoom);
}
}
}
}
}