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:
1 parent
8421c3aef0
commit
f2b1c6d97f
424 files changed
+17284
-6624
No files matched your search
@@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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!" }
|
||||
}
|
||||
}
|
||||
@@ -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}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.." } ),
|
||||
}
|
||||
}
|
||||
@@ -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}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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!"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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!" }
|
||||
}
|
||||
}
|
||||
@@ -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():?}"
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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" }
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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}")
|
||||
}
|
||||
@@ -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" }
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -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!" }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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!"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user