Build for web, desktop, and mobile, and more with a single codebase. Zero-config setup, integrated hot-reloading, and signals-based state management. Add backend functionality with Server Functions and bundle with our CLI.
```rust
fn app() -> Element {
let mut count = use_signal(|| 0);
rsx! {
h1 { "High-Five counter: {count}" }
button { onclick: move |_| count += 1, "Up high!" }
button { onclick: move |_| count -= 1, "Down low!" }
}
}
```
## ⭐️ Unique features:
- Cross-platform apps in three lines of code (web, desktop, mobile, server, and more)
- [Ergonomic state management](https://dioxuslabs.com/blog/release-050) combines the best of React, Solid, and Svelte
- Built-in featureful, type-safe, fullstack web framework
- Integrated bundler for deploying to the web, macOS, Linux, and Windows
- Subsecond Rust hot-patching and asset hot-reloading
- And more! [Take a tour of Dioxus](https://dioxuslabs.com/learn/0.7/).
## Instant hot-reloading
With one command, `dx serve` and your app is running. Edit your markup, styles, and see changes in milliseconds. Use our experimental `dx serve --hotpatch` to update Rust code in real time.
## Build Beautiful Apps
Dioxus apps are styled with HTML and CSS. Use the built-in TailwindCSS support or load your favorite CSS library. Easily call into native code (objective-c, JNI, Web-Sys) for a perfect native touch.
## Truly fullstack applications
Dioxus deeply integrates with [axum](https://github.com/tokio-rs/axum) to provide powerful fullstack capabilities for both clients and servers. Pick from a wide array of built-in batteries like WebSockets, SSE, Streaming, File Upload/Download, Server-Side-Rendering, Forms, Middleware, and Hot-Reload, or go fully custom and integrate your existing axum backend.
## Experimental Native Renderer
Render using web-sys, webview, server-side-rendering, liveview, or even with our experimental WGPU-based renderer. Embed Dioxus in Bevy, WGPU, or even run on embedded Linux!
## First-party primitive components
Get started quickly with a complete set of primitives modeled after shadcn/ui and Radix-Primitives.
## First-class Android and iOS support
Dioxus is the fastest way to build native mobile apps with Rust. Simply run `dx serve --platform android` and your app is running in an emulator or on device in seconds. Call directly into JNI and Native APIs.
## Bundle for web, desktop, and mobile
Simply run `dx bundle` and your app will be built and bundled with maximization optimizations. On the web, take advantage of [`.avif` generation, `.wasm` compression, minification](https://dioxuslabs.com/learn/0.7/tutorial/assets), and more. Build WebApps weighing [less than 50kb](https://github.com/ealmloff/tiny-dioxus/) and desktop/mobile apps less than 5mb.
## Fantastic documentation
We've put a ton of effort into building clean, readable, and comprehensive documentation. All html elements and listeners are documented with MDN docs, and our Docs runs continuous integration with Dioxus itself to ensure that the docs are always up to date. Check out the [Dioxus website](https://dioxuslabs.com/learn/0.7/) for guides, references, recipes, and more. Fun fact: we use the Dioxus website as a testbed for new Dioxus features - [check it out!](https://github.com/dioxusLabs/docsite)
## Modular and Customizable
Build your own renderer. Use our modular components like RSX, VirtualDom, Blitz, Taffy, and Subsecond.
## Community
Dioxus is a community-driven project, with a very active [Discord](https://discord.gg/XgGxMSkvUM) and [GitHub](https://github.com/DioxusLabs/dioxus/issues) community. We're always looking for help, and we're happy to answer questions and help you get started. [Our SDK](https://github.com/DioxusLabs/dioxus-std) is community-run and we even have a [GitHub organization](https://github.com/dioxus-community/) for the best Dioxus crates that receive free upgrades and support.
## Full-time core team
Dioxus has grown from a side project to a small team of fulltime engineers. Thanks to the generous support of FutureWei, Satellite.im, the GitHub Accelerator program, we're able to work on Dioxus full-time. Our long term goal is for Dioxus to become self-sustaining by providing paid high-quality enterprise tools. If your company is interested in adopting Dioxus and would like to work with us, please reach out!
## Supported Platforms
Web
Render directly to the DOM using WebAssembly
Pre-render with SSR and rehydrate on the client
Simple "hello world" at about 50kb, comparable to React
Built-in dev server and hot reloading for quick iteration
Desktop
Render using Webview or - experimentally - with WGPU or Freya (Skia)
Zero-config setup. Simply `cargo run` or `dx serve` to build your app
Full support for native system access without IPC
Supports macOS, Linux, and Windows. Portable <3mb binaries
Mobile
Render using Webview or - experimentally - with WGPU or Skia
Build .ipa and .apk files for iOS and Android
Call directly into Java and Objective-C with minimal overhead
From "hello world" to running on device in seconds
Server-side Rendering
Suspense, hydration, and server-side rendering
Quickly drop in backend functionality with server functions
Extractors, middleware, and routing integrations
Static-site generation and incremental regeneration
## Running the examples
> The examples in the main branch of this repository target the git version of dioxus and the CLI. If you are looking for examples that work with the latest stable release of dioxus, check out the [0.6 branch](https://github.com/DioxusLabs/dioxus/tree/v0.6/examples).
The examples in the top level of this repository can be run with:
```sh
cargo run --example
```
However, we encourage you to download the dioxus-cli to test out features like hot-reloading. To install the most recent binary CLI, you can use cargo binstall.
```sh
cargo binstall dioxus-cli@0.7.0 --force
```
If this CLI is out-of-date, you can install it directly from git
```sh
cargo install --git https://github.com/DioxusLabs/dioxus dioxus-cli --locked
```
With the CLI, you can also run examples with the web platform. You will need to disable the default desktop feature and enable the web feature with this command:
```sh
dx serve --example --platform web -- --no-default-features
```
## Contributing
- Check out the website [section on contributing](https://dioxuslabs.com/learn/0.7/beyond/contributing).
- Report issues on our [issue tracker](https://github.com/dioxuslabs/dioxus/issues).
- [Join](https://discord.gg/XgGxMSkvUM) the discord and ask questions!
## License
This project is licensed under either the [MIT license] or the [Apache-2 License].
[apache-2 license]: https://github.com/DioxusLabs/dioxus/blob/master/LICENSE-APACHE
[mit license]: https://github.com/DioxusLabs/dioxus/blob/master/LICENSE-MIT
Unless you explicitly state otherwise, any contribution intentionally submitted
for inclusion in Dioxus by you, shall be licensed as MIT or Apache-2, without any additional
terms or conditions.
================================================
FILE: _typos.toml
================================================
[default.extend-words]
# https://ratatui.rs/
ratatui = "ratatui"
# lits is short for literals
lits = "lits"
# https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/seeked_event
seeked = "seeked"
# https://developer.apple.com/forums/thread/108953
# udid = unique device identifier
udid = "udid"
# Part of Blitz's API
unparented = "unparented"
[files]
extend-exclude = ["notes/translations/*", "CHANGELOG.md", "*.js"]
================================================
FILE: codecov.yml
================================================
comment: false
fail_ci_if_error: false
================================================
FILE: examples/01-app-demos/bluetooth-scanner/.gitignore
================================================
/target
================================================
FILE: examples/01-app-demos/bluetooth-scanner/Cargo.toml
================================================
[package]
name = "bluetooth-scanner"
version = "0.1.1"
edition = "2021"
publish = false
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
tokio = { workspace = true, features = ["full"] }
dioxus = { workspace = true }
futures-channel = { workspace = true }
futures = { workspace = true }
btleplug = "0.11.8"
[features]
default = ["desktop"]
desktop = ["dioxus/desktop"]
native = ["dioxus/native"]
================================================
FILE: examples/01-app-demos/bluetooth-scanner/README.md
================================================
# Bluetooth scanner app
This desktop app showcases the use of background threads.

================================================
FILE: examples/01-app-demos/bluetooth-scanner/assets/tailwind.css
================================================
/*! tailwindcss v4.1.5 | MIT License | https://tailwindcss.com */
@layer properties;
@layer theme, base, components, utilities;
@layer theme {
:root, :host {
--font-sans: ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
'Noto Color Emoji';
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
monospace;
--color-green-500: oklch(72.3% 0.219 149.579);
--color-indigo-500: oklch(58.5% 0.233 277.117);
--color-indigo-600: oklch(51.1% 0.262 276.966);
--color-purple-50: oklch(97.7% 0.014 308.299);
--color-purple-500: oklch(62.7% 0.265 303.9);
--color-gray-50: oklch(98.5% 0.002 247.839);
--color-gray-500: oklch(55.1% 0.027 264.364);
--color-white: #fff;
--spacing: 0.25rem;
--text-xs: 0.75rem;
--text-xs--line-height: calc(1 / 0.75);
--text-2xl: 1.5rem;
--text-2xl--line-height: calc(2 / 1.5);
--font-weight-medium: 500;
--font-weight-bold: 700;
--default-transition-duration: 150ms;
--default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
--default-font-family: var(--font-sans);
--default-mono-font-family: var(--font-mono);
}
}
@layer base {
*, ::after, ::before, ::backdrop, ::file-selector-button {
box-sizing: border-box;
margin: 0;
padding: 0;
border: 0 solid;
}
html, :host {
line-height: 1.5;
-webkit-text-size-adjust: 100%;
tab-size: 4;
font-family: var(--default-font-family, ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji');
font-feature-settings: var(--default-font-feature-settings, normal);
font-variation-settings: var(--default-font-variation-settings, normal);
-webkit-tap-highlight-color: transparent;
}
hr {
height: 0;
color: inherit;
border-top-width: 1px;
}
abbr:where([title]) {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
}
h1, h2, h3, h4, h5, h6 {
font-size: inherit;
font-weight: inherit;
}
a {
color: inherit;
-webkit-text-decoration: inherit;
text-decoration: inherit;
}
b, strong {
font-weight: bolder;
}
code, kbd, samp, pre {
font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace);
font-feature-settings: var(--default-mono-font-feature-settings, normal);
font-variation-settings: var(--default-mono-font-variation-settings, normal);
font-size: 1em;
}
small {
font-size: 80%;
}
sub, sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
table {
text-indent: 0;
border-color: inherit;
border-collapse: collapse;
}
:-moz-focusring {
outline: auto;
}
progress {
vertical-align: baseline;
}
summary {
display: list-item;
}
ol, ul, menu {
list-style: none;
}
img, svg, video, canvas, audio, iframe, embed, object {
display: block;
vertical-align: middle;
}
img, video {
max-width: 100%;
height: auto;
}
button, input, select, optgroup, textarea, ::file-selector-button {
font: inherit;
font-feature-settings: inherit;
font-variation-settings: inherit;
letter-spacing: inherit;
color: inherit;
border-radius: 0;
background-color: transparent;
opacity: 1;
}
:where(select:is([multiple], [size])) optgroup {
font-weight: bolder;
}
:where(select:is([multiple], [size])) optgroup option {
padding-inline-start: 20px;
}
::file-selector-button {
margin-inline-end: 4px;
}
::placeholder {
opacity: 1;
}
@supports (not (-webkit-appearance: -apple-pay-button)) or (contain-intrinsic-size: 1px) {
::placeholder {
color: currentcolor;
@supports (color: color-mix(in lab, red, red)) {
color: color-mix(in oklab, currentcolor 50%, transparent);
}
}
}
textarea {
resize: vertical;
}
::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-date-and-time-value {
min-height: 1lh;
text-align: inherit;
}
::-webkit-datetime-edit {
display: inline-flex;
}
::-webkit-datetime-edit-fields-wrapper {
padding: 0;
}
::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month-field, ::-webkit-datetime-edit-day-field, ::-webkit-datetime-edit-hour-field, ::-webkit-datetime-edit-minute-field, ::-webkit-datetime-edit-second-field, ::-webkit-datetime-edit-millisecond-field, ::-webkit-datetime-edit-meridiem-field {
padding-block: 0;
}
:-moz-ui-invalid {
box-shadow: none;
}
button, input:where([type='button'], [type='reset'], [type='submit']), ::file-selector-button {
appearance: button;
}
::-webkit-inner-spin-button, ::-webkit-outer-spin-button {
height: auto;
}
[hidden]:where(:not([hidden='until-found'])) {
display: none !important;
}
}
@layer utilities {
.container {
width: 100%;
@media (width >= 40rem) {
max-width: 40rem;
}
@media (width >= 48rem) {
max-width: 48rem;
}
@media (width >= 64rem) {
max-width: 64rem;
}
@media (width >= 80rem) {
max-width: 80rem;
}
@media (width >= 96rem) {
max-width: 96rem;
}
}
.mx-auto {
margin-inline: auto;
}
.mb-6 {
margin-bottom: calc(var(--spacing) * 6);
}
.flex {
display: flex;
}
.inline-block {
display: inline-block;
}
.table {
display: table;
}
.w-full {
width: 100%;
}
.table-auto {
table-layout: auto;
}
.overflow-x-auto {
overflow-x: auto;
}
.rounded {
border-radius: 0.25rem;
}
.rounded-full {
border-radius: calc(infinity * 1px);
}
.bg-gray-50 {
background-color: var(--color-gray-50);
}
.bg-green-500 {
background-color: var(--color-green-500);
}
.bg-indigo-500 {
background-color: var(--color-indigo-500);
}
.bg-purple-50 {
background-color: var(--color-purple-50);
}
.bg-white {
background-color: var(--color-white);
}
.p-4 {
padding: calc(var(--spacing) * 4);
}
.px-2 {
padding-inline: calc(var(--spacing) * 2);
}
.px-4 {
padding-inline: calc(var(--spacing) * 4);
}
.px-6 {
padding-inline: calc(var(--spacing) * 6);
}
.py-1 {
padding-block: calc(var(--spacing) * 1);
}
.py-3 {
padding-block: calc(var(--spacing) * 3);
}
.py-5 {
padding-block: calc(var(--spacing) * 5);
}
.py-8 {
padding-block: calc(var(--spacing) * 8);
}
.pb-3 {
padding-bottom: calc(var(--spacing) * 3);
}
.pl-6 {
padding-left: calc(var(--spacing) * 6);
}
.text-left {
text-align: left;
}
.text-2xl {
font-size: var(--text-2xl);
line-height: var(--tw-leading, var(--text-2xl--line-height));
}
.text-xs {
font-size: var(--text-xs);
line-height: var(--tw-leading, var(--text-xs--line-height));
}
.font-bold {
--tw-font-weight: var(--font-weight-bold);
font-weight: var(--font-weight-bold);
}
.font-medium {
--tw-font-weight: var(--font-weight-medium);
font-weight: var(--font-weight-medium);
}
.text-gray-500 {
color: var(--color-gray-500);
}
.text-purple-500 {
color: var(--color-purple-500);
}
.text-white {
color: var(--color-white);
}
.shadow {
--tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
}
.transition {
transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter, display, visibility, content-visibility, overlay, pointer-events;
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
transition-duration: var(--tw-duration, var(--default-transition-duration));
}
.duration-200 {
--tw-duration: 200ms;
transition-duration: 200ms;
}
.hover\:bg-indigo-600 {
&:hover {
@media (hover: hover) {
background-color: var(--color-indigo-600);
}
}
}
.md\:w-auto {
@media (width >= 48rem) {
width: auto;
}
}
}
@property --tw-font-weight {
syntax: "*";
inherits: false;
}
@property --tw-shadow {
syntax: "*";
inherits: false;
initial-value: 0 0 #0000;
}
@property --tw-shadow-color {
syntax: "*";
inherits: false;
}
@property --tw-shadow-alpha {
syntax: "";
inherits: false;
initial-value: 100%;
}
@property --tw-inset-shadow {
syntax: "*";
inherits: false;
initial-value: 0 0 #0000;
}
@property --tw-inset-shadow-color {
syntax: "*";
inherits: false;
}
@property --tw-inset-shadow-alpha {
syntax: "";
inherits: false;
initial-value: 100%;
}
@property --tw-ring-color {
syntax: "*";
inherits: false;
}
@property --tw-ring-shadow {
syntax: "*";
inherits: false;
initial-value: 0 0 #0000;
}
@property --tw-inset-ring-color {
syntax: "*";
inherits: false;
}
@property --tw-inset-ring-shadow {
syntax: "*";
inherits: false;
initial-value: 0 0 #0000;
}
@property --tw-ring-inset {
syntax: "*";
inherits: false;
}
@property --tw-ring-offset-width {
syntax: "";
inherits: false;
initial-value: 0px;
}
@property --tw-ring-offset-color {
syntax: "*";
inherits: false;
initial-value: #fff;
}
@property --tw-ring-offset-shadow {
syntax: "*";
inherits: false;
initial-value: 0 0 #0000;
}
@property --tw-duration {
syntax: "*";
inherits: false;
}
@layer properties {
@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) {
*, ::before, ::after, ::backdrop {
--tw-font-weight: initial;
--tw-shadow: 0 0 #0000;
--tw-shadow-color: initial;
--tw-shadow-alpha: 100%;
--tw-inset-shadow: 0 0 #0000;
--tw-inset-shadow-color: initial;
--tw-inset-shadow-alpha: 100%;
--tw-ring-color: initial;
--tw-ring-shadow: 0 0 #0000;
--tw-inset-ring-color: initial;
--tw-inset-ring-shadow: 0 0 #0000;
--tw-ring-inset: initial;
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-offset-shadow: 0 0 #0000;
--tw-duration: initial;
}
}
}
================================================
FILE: examples/01-app-demos/bluetooth-scanner/src/main.rs
================================================
use dioxus::prelude::*;
fn main() {
dioxus::launch(app)
}
fn app() -> Element {
let mut scan = use_action(|| async {
use btleplug::api::{Central, Manager as _, Peripheral, ScanFilter};
let manager = btleplug::platform::Manager::new().await?;
// get the first bluetooth adapter
let adapters = manager.adapters().await?;
let central = adapters
.into_iter()
.next()
.context("No Bluetooth adapter found")?;
// start scanning for devices
central.start_scan(ScanFilter::default()).await?;
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
// Return the list of peripherals after scanning
let mut devices = vec![];
for p in central.peripherals().await? {
if let Some(p) = p.properties().await? {
devices.push(p);
}
}
// Sort them by RSSI (signal strength)
devices.sort_by_key(|p| p.rssi.unwrap_or(-100));
dioxus::Ok(devices)
});
rsx! {
Stylesheet { href: asset!("/assets/tailwind.css") }
div {
div { class: "py-8 px-6",
div { class: "container px-4 mx-auto",
h2 { class: "text-2xl font-bold", "Scan for Bluetooth Devices" }
button {
class: "inline-block w-full md:w-auto px-6 py-3 font-medium text-white bg-indigo-500 hover:bg-indigo-600 rounded transition duration-200",
disabled: scan.pending(),
onclick: move |_| {
scan.call();
},
if scan.pending() { "Scanning" } else { "Scan" }
}
}
}
section { class: "py-8",
div { class: "container px-4 mx-auto",
div { class: "p-4 mb-6 bg-white shadow rounded overflow-x-auto",
table { class: "table-auto w-full",
thead {
tr { class: "text-xs text-gray-500 text-left",
th { class: "pl-6 pb-3 font-medium", "Strength" }
th { class: "pb-3 font-medium", "Network" }
th { class: "pb-3 font-medium", "Channel" }
th { class: "pb-3 px-2 font-medium", "Security" }
}
}
match scan.value() {
None if scan.pending() => rsx! { "Scanning..." },
None => rsx! { "Press Scan to start scanning" },
Some(Err(_err)) => rsx! { "Failed to scan" },
Some(Ok(peripherals)) => rsx! {
tbody {
for peripheral in peripherals.read().iter().rev() {
tr { class: "text-xs bg-gray-50",
td { class: "py-5 px-6 font-medium", "{peripheral.rssi.unwrap_or(-100)}" }
td { class: "flex py-3 font-medium", "{peripheral.local_name.clone().unwrap_or_default()}" }
td { span { class: "inline-block py-1 px-2 text-white bg-green-500 rounded-full", "{peripheral.address}" } }
td { span { class: "inline-block py-1 px-2 text-purple-500 bg-purple-50 rounded-full", "{peripheral.tx_power_level.unwrap_or_default()}" } }
}
}
}
}
}
}
}
}
}
}
}
}
================================================
FILE: examples/01-app-demos/bluetooth-scanner/tailwind.css
================================================
@import "tailwindcss";
@source "./src/**/*.{rs,html,css}";
================================================
FILE: examples/01-app-demos/calculator.rs
================================================
//! Calculator
//!
//! This example is a simple iOS-style calculator. Instead of wrapping the state in a single struct like the
//! `calculate_mutable` example, this example uses several closures to manage actions with the state. Most
//! components will start like this since it's the quickest way to start adding state to your app. The `Signal` type
//! in Dioxus is `Copy` - meaning you don't need to clone it to use it in a closure.
//!
//! Notice how our logic is consolidated into just a few callbacks instead of a single struct. This is a rather organic
//! way to start building state management in Dioxus, and it's a great way to start.
use dioxus::events::*;
use dioxus::html::input_data::keyboard_types::Key;
use dioxus::prelude::*;
const TITLE: &str = "Calculator";
const STYLE: Asset = asset!("/examples/assets/calculator.css");
fn main() {
dioxus::LaunchBuilder::new()
.with_cfg(desktop!({
use dioxus::desktop::{Config, LogicalSize, WindowBuilder};
Config::new().with_window(
WindowBuilder::default()
.with_title(TITLE)
.with_inner_size(LogicalSize::new(300.0, 525.0)),
)
}))
.with_cfg(native!({
use dioxus::native::{Config, LogicalSize, WindowAttributes};
Config::new().with_window_attributes(
WindowAttributes::default()
.with_title(TITLE)
.with_inner_size(LogicalSize::new(300.0, 525.0)),
)
}))
.launch(app);
}
fn app() -> Element {
let mut val = use_signal(|| String::from("0"));
let mut input_digit = move |num: String| {
if val() == "0" {
val.set(String::new());
}
val.push_str(num.as_str());
};
let mut input_operator = move |key: &str| val.push_str(key);
let handle_key_down_event = move |evt: KeyboardEvent| match evt.key() {
Key::Backspace => {
if !val().is_empty() {
val.pop();
}
}
Key::Character(character) => match character.as_str() {
"+" | "-" | "/" | "*" => input_operator(&character),
"0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" => input_digit(character),
_ => {}
},
_ => {}
};
rsx! {
Stylesheet { href: STYLE }
div { id: "wrapper",
div { class: "app",
div { class: "calculator", tabindex: "0", onkeydown: handle_key_down_event,
div { class: "calculator-display",
if val().is_empty() {
"0"
} else {
"{val}"
}
}
div { class: "calculator-keypad",
div { class: "input-keys",
div { class: "function-keys",
button {
class: "calculator-key key-clear",
onclick: move |_| {
val.set(String::new());
if !val.cloned().is_empty() {
val.set("0".into());
}
},
if val.cloned().is_empty() { "C" } else { "AC" }
}
button {
class: "calculator-key key-sign",
onclick: move |_| {
let new_val = calc_val(val.cloned().as_str());
if new_val > 0.0 {
val.set(format!("-{new_val}"));
} else {
val.set(format!("{}", new_val.abs()));
}
},
"±"
}
button {
class: "calculator-key key-percent",
onclick: move |_| val.set(format!("{}", calc_val(val.cloned().as_str()) / 100.0)),
"%"
}
}
div { class: "digit-keys",
button {
class: "calculator-key key-0",
onclick: move |_| input_digit(0.to_string()),
"0"
}
button {
class: "calculator-key key-dot",
onclick: move |_| val.push('.'),
"●"
}
for k in 1..10 {
button {
class: "calculator-key {k}",
name: "key-{k}",
onclick: move |_| input_digit(k.to_string()),
"{k}"
}
}
}
}
div { class: "operator-keys",
for (key, class) in [("/", "key-divide"), ("*", "key-multiply"), ("-", "key-subtract"), ("+", "key-add")] {
button {
class: "calculator-key {class}",
onclick: move |_| input_operator(key),
"{key}"
}
}
button {
class: "calculator-key key-equals",
onclick: move |_| val.set(format!("{}", calc_val(val.cloned().as_str()))),
"="
}
}
}
}
}
}
}
}
fn calc_val(val: &str) -> f64 {
if val.is_empty() {
return 0.0;
}
let mut temp = String::new();
let mut operation = "+".to_string();
let mut start_index = 0;
let mut temp_value;
let mut fin_index = 0;
if val.len() > 1 && &val[0..1] == "-" {
temp_value = String::from("-");
fin_index = 1;
start_index += 1;
} else {
temp_value = String::from("");
}
for c in val[fin_index..].chars() {
if c == '+' || c == '-' || c == '*' || c == '/' {
break;
}
temp_value.push(c);
start_index += 1;
}
let mut result = temp_value.parse::().unwrap();
if start_index + 1 >= val.len() {
return result;
}
for c in val[start_index..].chars() {
if c == '+' || c == '-' || c == '*' || c == '/' {
if !temp.is_empty() {
match &operation as &str {
"+" => result += temp.parse::().unwrap(),
"-" => result -= temp.parse::().unwrap(),
"*" => result *= temp.parse::().unwrap(),
"/" => result /= temp.parse::().unwrap(),
_ => unreachable!(),
};
}
operation = c.to_string();
temp = String::new();
} else {
temp.push(c);
}
}
if !temp.is_empty() {
match &operation as &str {
"+" => result += temp.parse::().unwrap(),
"-" => result -= temp.parse::().unwrap(),
"*" => result *= temp.parse::().unwrap(),
"/" => result /= temp.parse::().unwrap(),
_ => unreachable!(),
};
}
result
}
================================================
FILE: examples/01-app-demos/calculator_mutable.rs
================================================
//! This example showcases a simple calculator using an approach to state management where the state is composed of only
//! a single signal. Since Dioxus implements traditional React diffing, state can be consolidated into a typical Rust struct
//! with methods that take `&mut self`. For many use cases, this is a simple way to manage complex state without wrapping
//! everything in a signal.
//!
//! Generally, you'll want to split your state into several signals if you have a large application, but for small
//! applications, or focused components, this is a great way to manage state.
use dioxus::html::MouseEvent;
use dioxus::html::input_data::keyboard_types::Key;
use dioxus::prelude::*;
fn main() {
dioxus::LaunchBuilder::new()
.with_cfg(desktop! {{
use dioxus::desktop::{Config, LogicalSize, WindowBuilder};
Config::new().with_window(
WindowBuilder::new()
.with_title("Calculator Demo")
.with_resizable(false)
.with_inner_size(LogicalSize::new(320.0, 530.0)),
)
}})
.with_cfg(native! {{
use dioxus::native::{Config, LogicalSize, WindowAttributes};
Config::new().with_window_attributes(
WindowAttributes::default()
.with_title("Calculator Demo")
.with_inner_size(LogicalSize::new(300.0, 525.0)),
)
}})
.launch(app);
}
fn app() -> Element {
let mut state = use_signal(Calculator::new);
rsx! {
Stylesheet { href: asset!("/examples/assets/calculator.css") }
div { id: "wrapper",
div { class: "app",
div {
class: "calculator",
onkeypress: move |evt| state.write().handle_keydown(evt),
div { class: "calculator-display", {state.read().formatted_display()} }
div { class: "calculator-keypad",
div { class: "input-keys",
div { class: "function-keys",
CalculatorKey {
name: "key-clear",
onclick: move |_| state.write().clear_display(),
if state.read().display_value == "0" {
"C"
} else {
"AC"
}
}
CalculatorKey {
name: "key-sign",
onclick: move |_| state.write().toggle_sign(),
"±"
}
CalculatorKey {
name: "key-percent",
onclick: move |_| state.write().toggle_percent(),
"%"
}
}
div { class: "digit-keys",
CalculatorKey {
name: "key-0",
onclick: move |_| state.write().input_digit(0),
"0"
}
CalculatorKey {
name: "key-dot",
onclick: move |_| state.write().input_dot(),
"●"
}
for k in 1..10 {
CalculatorKey {
key: "{k}",
name: "key-{k}",
onclick: move |_| state.write().input_digit(k),
"{k}"
}
}
}
}
div { class: "operator-keys",
CalculatorKey {
name: "key-divide",
onclick: move |_| state.write().set_operator(Operator::Div),
"÷"
}
CalculatorKey {
name: "key-multiply",
onclick: move |_| state.write().set_operator(Operator::Mul),
"×"
}
CalculatorKey {
name: "key-subtract",
onclick: move |_| state.write().set_operator(Operator::Sub),
"−"
}
CalculatorKey {
name: "key-add",
onclick: move |_| state.write().set_operator(Operator::Add),
"+"
}
CalculatorKey {
name: "key-equals",
onclick: move |_| state.write().perform_operation(),
"="
}
}
}
}
}
}
}
}
#[component]
fn CalculatorKey(name: String, onclick: EventHandler, children: Element) -> Element {
rsx! {
button { class: "calculator-key {name}", onclick, {children} }
}
}
struct Calculator {
display_value: String,
operator: Option,
waiting_for_operand: bool,
cur_val: f64,
}
#[derive(Clone)]
enum Operator {
Add,
Sub,
Mul,
Div,
}
impl Calculator {
fn new() -> Self {
Calculator {
display_value: "0".to_string(),
operator: None,
waiting_for_operand: false,
cur_val: 0.0,
}
}
fn formatted_display(&self) -> String {
use separator::Separatable;
self.display_value
.parse::()
.unwrap()
.separated_string()
}
fn clear_display(&mut self) {
self.display_value = "0".to_string();
}
fn input_digit(&mut self, digit: u8) {
let content = digit.to_string();
if self.waiting_for_operand || self.display_value == "0" {
self.waiting_for_operand = false;
self.display_value = content;
} else {
self.display_value.push_str(content.as_str());
}
}
fn input_dot(&mut self) {
if !self.display_value.contains('.') {
self.display_value.push('.');
}
}
fn perform_operation(&mut self) {
if let Some(op) = &self.operator {
let rhs = self.display_value.parse::().unwrap();
let new_val = match op {
Operator::Add => self.cur_val + rhs,
Operator::Sub => self.cur_val - rhs,
Operator::Mul => self.cur_val * rhs,
Operator::Div => self.cur_val / rhs,
};
self.cur_val = new_val;
self.display_value = new_val.to_string();
self.operator = None;
}
}
fn toggle_sign(&mut self) {
if self.display_value.starts_with('-') {
self.display_value = self.display_value.trim_start_matches('-').to_string();
} else {
self.display_value = format!("-{}", self.display_value);
}
}
fn toggle_percent(&mut self) {
self.display_value = (self.display_value.parse::().unwrap() / 100.0).to_string();
}
fn backspace(&mut self) {
if !self.display_value.as_str().eq("0") {
self.display_value.pop();
}
}
fn set_operator(&mut self, operator: Operator) {
self.operator = Some(operator);
self.cur_val = self.display_value.parse::().unwrap();
self.waiting_for_operand = true;
}
fn handle_keydown(&mut self, evt: KeyboardEvent) {
match evt.key() {
Key::Backspace => self.backspace(),
Key::Character(c) => match c.as_str() {
"0" => self.input_digit(0),
"1" => self.input_digit(1),
"2" => self.input_digit(2),
"3" => self.input_digit(3),
"4" => self.input_digit(4),
"5" => self.input_digit(5),
"6" => self.input_digit(6),
"7" => self.input_digit(7),
"8" => self.input_digit(8),
"9" => self.input_digit(9),
"+" => self.operator = Some(Operator::Add),
"-" => self.operator = Some(Operator::Sub),
"/" => self.operator = Some(Operator::Div),
"*" => self.operator = Some(Operator::Mul),
_ => {}
},
_ => {}
}
}
}
================================================
FILE: examples/01-app-demos/counters.rs
================================================
//! A simple counters example that stores a list of items in a vec and then iterates over them.
use dioxus::prelude::*;
const STYLE: Asset = asset!("/examples/assets/counter.css");
fn main() {
dioxus::launch(app);
}
fn app() -> Element {
// Store the counters in a signal
let mut counters = use_signal(|| vec![0, 0, 0]);
// Whenever the counters change, sum them up
let sum = use_memo(move || counters.read().iter().copied().sum::());
rsx! {
Stylesheet { href: STYLE }
div { id: "controls",
button { onclick: move |_| counters.push(0), "Add counter" }
button { onclick: move |_| { counters.pop(); }, "Remove counter" }
}
h3 { "Total: {sum}" }
// Calling `iter` on a Signal> gives you a GenerationalRef to each entry in the vec
// We enumerate to get the idx of each counter, which we use later to modify the vec
for (i, counter) in counters.iter().enumerate() {
// We need a key to uniquely identify each counter. You really shouldn't be using the index, so we're using
// the counter value itself.
//
// If we used the index, and a counter is removed, dioxus would need to re-write the contents of all following
// counters instead of simply removing the one that was removed
//
// You should use a stable identifier for the key, like a unique id or the value of the counter itself
li { key: "{i}",
button { onclick: move |_| counters.write()[i] -= 1, "-1" }
input {
r#type: "number",
value: "{counter}",
oninput: move |e| {
if let Ok(value) = e.parsed() {
counters.write()[i] = value;
}
}
}
button { onclick: move |_| counters.write()[i] += 1, "+1" }
button { onclick: move |_| { counters.remove(i); }, "x" }
}
}
}
}
================================================
FILE: examples/01-app-demos/crm.rs
================================================
//! Tiny CRM - A simple CRM app using the Router component and global signals
//!
//! This shows how to use the `Router` component to manage different views in your app. It also shows how to use global
//! signals to manage state across the entire app.
//!
//! We could simply pass the state as a prop to each component, but this is a good example of how to use global state
//! in a way that works across pages.
//!
//! We implement a number of important details here too, like focusing inputs, handling form submits, navigating the router,
//! platform-specific configuration, and importing 3rd party CSS libraries.
use dioxus::prelude::*;
fn main() {
dioxus::LaunchBuilder::new()
.with_cfg(desktop!({
use dioxus::desktop::{LogicalSize, WindowBuilder};
dioxus::desktop::Config::default()
.with_window(WindowBuilder::new().with_inner_size(LogicalSize::new(800, 600)))
}))
.launch(|| {
rsx! {
Stylesheet {
href: "https://unpkg.com/purecss@2.0.6/build/pure-min.css",
integrity: "sha384-Uu6IeWbM+gzNVXJcM9XV3SohHtmWE+3VGi496jvgX1jyvDTXfdK+rfZc8C1Aehk5",
crossorigin: "anonymous",
}
Stylesheet { href: asset!("/examples/assets/crm.css") }
h1 { "Dioxus CRM Example" }
Router:: {}
}
});
}
/// We only have one list of clients for the whole app, so we can use a global signal.
static CLIENTS: GlobalSignal> = Signal::global(Vec::new);
struct Client {
first_name: String,
last_name: String,
description: String,
}
/// The pages of the app, each with a route
#[derive(Routable, Clone)]
enum Route {
#[route("/")]
List,
#[route("/new")]
New,
#[route("/settings")]
Settings,
}
#[component]
fn List() -> Element {
rsx! {
h2 { "List of Clients" }
Link { to: Route::New, class: "pure-button pure-button-primary", "Add Client" }
Link { to: Route::Settings, class: "pure-button", "Settings" }
for client in CLIENTS.read().iter() {
div { class: "client", style: "margin-bottom: 50px",
p { "Name: {client.first_name} {client.last_name}" }
p { "Description: {client.description}" }
}
}
}
}
#[component]
fn New() -> Element {
let mut first_name = use_signal(String::new);
let mut last_name = use_signal(String::new);
let mut description = use_signal(String::new);
let submit_client = move |_| {
// Write the client
CLIENTS.write().push(Client {
first_name: first_name(),
last_name: last_name(),
description: description(),
});
// And then navigate back to the client list
router().push(Route::List);
};
rsx! {
h2 { "Add new Client" }
form { class: "pure-form pure-form-aligned", onsubmit: submit_client,
fieldset {
div { class: "pure-control-group",
label { r#for: "first_name", "First Name" }
input {
id: "first_name",
r#type: "text",
placeholder: "First Name…",
required: true,
value: "{first_name}",
oninput: move |e| first_name.set(e.value()),
// when the form mounts, focus the first name input
onmounted: move |e| async move {
_ = e.set_focus(true).await;
},
}
}
div { class: "pure-control-group",
label { r#for: "last_name", "Last Name" }
input {
id: "last_name",
r#type: "text",
placeholder: "Last Name…",
required: true,
value: "{last_name}",
oninput: move |e| last_name.set(e.value()),
}
}
div { class: "pure-control-group",
label { r#for: "description", "Description" }
textarea {
id: "description",
placeholder: "Description…",
value: "{description}",
oninput: move |e| description.set(e.value()),
}
}
div { class: "pure-controls",
button {
r#type: "submit",
class: "pure-button pure-button-primary",
"Save"
}
Link {
to: Route::List,
class: "pure-button pure-button-primary red",
"Cancel"
}
}
}
}
}
}
#[component]
fn Settings() -> Element {
rsx! {
h2 { "Settings" }
button {
class: "pure-button pure-button-primary red",
onclick: move |_| {
CLIENTS.write().clear();
dioxus::router::router().push(Route::List);
},
"Remove all Clients"
}
Link { to: Route::List, class: "pure-button", "Go back" }
}
}
================================================
FILE: examples/01-app-demos/dog_app.rs
================================================
//! This example demonstrates a simple app that fetches a list of dog breeds and displays a random dog.
//!
//! This app combines `use_loader` and `use_action` to fetch data from the Dog API.
//! - `use_loader` automatically fetches the list of dog breeds when the component mounts.
//! - `use_action` fetches a random dog image whenever the `.dispatch` method is called.
use dioxus::prelude::*;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
fn main() {
dioxus::launch(app);
}
fn app() -> Element {
// Fetch the list of breeds from the Dog API, using the `?` syntax to suspend or throw errors
let breed_list = use_loader(move || async move {
#[derive(Deserialize, Serialize, Debug, PartialEq, Clone)]
struct ListBreeds {
message: HashMap>,
}
reqwest::get("https://dog.ceo/api/breeds/list/all")
.await?
.json::()
.await
})?;
// Whenever this action is called, it will re-run the future and return the result.
let mut breed = use_action(move |breed| async move {
#[derive(Deserialize, Serialize, Debug, PartialEq)]
struct DogApi {
message: String,
}
reqwest::get(format!("https://dog.ceo/api/breed/{breed}/images/random"))
.await
.unwrap()
.json::()
.await
});
rsx! {
h1 { "Doggo selector" }
div { width: "400px",
for cur_breed in breed_list.read().message.keys().take(20).cloned() {
button {
onclick: move |_| {
breed.call(cur_breed.clone());
},
"{cur_breed}"
}
}
}
div {
match breed.value() {
None => rsx! { div { "Click the button to fetch a dog!" } },
Some(Err(_e)) => rsx! { div { "Failed to fetch a dog, please try again." } },
Some(Ok(res)) => rsx! {
img {
max_width: "500px",
max_height: "500px",
src: "{res.read().message}"
}
},
}
}
}
}
================================================
FILE: examples/01-app-demos/ecommerce-site/.gitignore
================================================
/target
================================================
FILE: examples/01-app-demos/ecommerce-site/Cargo.toml
================================================
[package]
name = "ecommerce-site"
version = "0.1.1"
edition = "2021"
publish = false
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
dioxus = { workspace = true, features = ["fullstack", "router"] }
reqwest = { workspace = true, features = ["json"] }
serde = { workspace = true }
[target.'cfg(target_family = "wasm")'.dependencies]
chrono = { workspace = true, features = ["serde", "wasmbind"] }
[target.'cfg(not(target_family = "wasm"))'.dependencies]
chrono = { workspace = true, features = ["serde"] }
[features]
web = ["dioxus/web"]
server = ["dioxus/server"]
================================================
FILE: examples/01-app-demos/ecommerce-site/README.md
================================================
# Dioxus Example: An e-commerce site using the FakeStoreAPI
This example app is a fullstack web application leveraging the FakeStoreAPI and [Tailwind CSS](https://tailwindcss.com/).

# Development
1. Run the following commands to serve the application:
```bash
dx serve
```
Note that in Dioxus 0.7, the Tailwind watcher is initialized automatically if a `tailwind.css` file is find in your app's root.
# Status
This is a work in progress. The following features are currently implemented:
- [x] A homepage with a list of products dynamically fetched from the FakeStoreAPI (rendered using SSR)
- [x] A product detail page with details about a product (rendered using LiveView)
- [ ] A cart page
- [ ] A checkout page
- [ ] A login page
================================================
FILE: examples/01-app-demos/ecommerce-site/public/loading.css
================================================
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.spinner {
width: 10px;
height: 10px;
border: 4px solid #f3f3f3;
border-top: 4px solid #3498db;
border-radius: 50%;
animation: spin 2s linear infinite;
}
================================================
FILE: examples/01-app-demos/ecommerce-site/public/tailwind.css
================================================
/*! tailwindcss v4.1.0 | MIT License | https://tailwindcss.com */
@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) {
@layer base {
*, ::before, ::after, ::backdrop {
--tw-translate-x: 0;
--tw-translate-y: 0;
--tw-translate-z: 0;
--tw-rotate-x: rotateX(0);
--tw-rotate-y: rotateY(0);
--tw-rotate-z: rotateZ(0);
--tw-skew-x: skewX(0);
--tw-skew-y: skewY(0);
--tw-border-style: solid;
--tw-font-weight: initial;
--tw-shadow: 0 0 #0000;
--tw-shadow-color: initial;
--tw-shadow-alpha: 100%;
--tw-inset-shadow: 0 0 #0000;
--tw-inset-shadow-color: initial;
--tw-inset-shadow-alpha: 100%;
--tw-ring-color: initial;
--tw-ring-shadow: 0 0 #0000;
--tw-inset-ring-color: initial;
--tw-inset-ring-shadow: 0 0 #0000;
--tw-ring-inset: initial;
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-offset-shadow: 0 0 #0000;
--tw-duration: initial;
}
}
}
@layer theme, base, components, utilities;
@layer theme {
:root, :host {
--font-sans: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji",
"Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
"Courier New", monospace;
--color-orange-300: oklch(83.7% 0.128 66.29);
--color-orange-400: oklch(75% 0.183 55.934);
--color-blue-300: oklch(80.9% 0.105 251.813);
--color-gray-50: oklch(98.5% 0.002 247.839);
--color-gray-100: oklch(96.7% 0.003 264.542);
--color-gray-200: oklch(92.8% 0.006 264.531);
--color-gray-400: oklch(70.7% 0.022 261.325);
--color-gray-500: oklch(55.1% 0.027 264.364);
--color-gray-600: oklch(44.6% 0.03 256.802);
--color-gray-700: oklch(37.3% 0.034 259.733);
--color-gray-800: oklch(27.8% 0.033 256.848);
--color-white: #fff;
--spacing: 0.25rem;
--container-sm: 24rem;
--container-md: 28rem;
--container-xl: 36rem;
--container-2xl: 42rem;
--text-xs: 0.75rem;
--text-xs--line-height: calc(1 / 0.75);
--text-2xl: 1.5rem;
--text-2xl--line-height: calc(2 / 1.5);
--text-3xl: 1.875rem;
--text-3xl--line-height: calc(2.25 / 1.875);
--text-5xl: 3rem;
--text-5xl--line-height: 1;
--text-6xl: 3.75rem;
--text-6xl--line-height: 1;
--font-weight-semibold: 600;
--font-weight-bold: 700;
--radius-md: 0.375rem;
--radius-lg: 0.5rem;
--default-transition-duration: 150ms;
--default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
--default-font-family: var(--font-sans);
--default-mono-font-family: var(--font-mono);
}
}
@layer base {
*, ::after, ::before, ::backdrop, ::file-selector-button {
box-sizing: border-box;
margin: 0;
padding: 0;
border: 0 solid;
}
html, :host {
line-height: 1.5;
-webkit-text-size-adjust: 100%;
tab-size: 4;
font-family: var(--default-font-family, ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");
font-feature-settings: var(--default-font-feature-settings, normal);
font-variation-settings: var(--default-font-variation-settings, normal);
-webkit-tap-highlight-color: transparent;
}
hr {
height: 0;
color: inherit;
border-top-width: 1px;
}
abbr:where([title]) {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
}
h1, h2, h3, h4, h5, h6 {
font-size: inherit;
font-weight: inherit;
}
a {
color: inherit;
-webkit-text-decoration: inherit;
text-decoration: inherit;
}
b, strong {
font-weight: bolder;
}
code, kbd, samp, pre {
font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);
font-feature-settings: var(--default-mono-font-feature-settings, normal);
font-variation-settings: var(--default-mono-font-variation-settings, normal);
font-size: 1em;
}
small {
font-size: 80%;
}
sub, sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
table {
text-indent: 0;
border-color: inherit;
border-collapse: collapse;
}
:-moz-focusring {
outline: auto;
}
progress {
vertical-align: baseline;
}
summary {
display: list-item;
}
ol, ul, menu {
list-style: none;
}
img, svg, video, canvas, audio, iframe, embed, object {
display: block;
vertical-align: middle;
}
img, video {
max-width: 100%;
height: auto;
}
button, input, select, optgroup, textarea, ::file-selector-button {
font: inherit;
font-feature-settings: inherit;
font-variation-settings: inherit;
letter-spacing: inherit;
color: inherit;
border-radius: 0;
background-color: transparent;
opacity: 1;
}
:where(select:is([multiple], [size])) optgroup {
font-weight: bolder;
}
:where(select:is([multiple], [size])) optgroup option {
padding-inline-start: 20px;
}
::file-selector-button {
margin-inline-end: 4px;
}
::placeholder {
opacity: 1;
}
@supports (not (-webkit-appearance: -apple-pay-button)) or (contain-intrinsic-size: 1px) {
::placeholder {
color: color-mix(in oklab, currentColor 50%, transparent);
}
}
textarea {
resize: vertical;
}
::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-date-and-time-value {
min-height: 1lh;
text-align: inherit;
}
::-webkit-datetime-edit {
display: inline-flex;
}
::-webkit-datetime-edit-fields-wrapper {
padding: 0;
}
::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month-field, ::-webkit-datetime-edit-day-field, ::-webkit-datetime-edit-hour-field, ::-webkit-datetime-edit-minute-field, ::-webkit-datetime-edit-second-field, ::-webkit-datetime-edit-millisecond-field, ::-webkit-datetime-edit-meridiem-field {
padding-block: 0;
}
:-moz-ui-invalid {
box-shadow: none;
}
button, input:where([type="button"], [type="reset"], [type="submit"]), ::file-selector-button {
appearance: button;
}
::-webkit-inner-spin-button, ::-webkit-outer-spin-button {
height: auto;
}
[hidden]:where(:not([hidden="until-found"])) {
display: none !important;
}
}
@layer utilities {
.absolute {
position: absolute;
}
.fixed {
position: fixed;
}
.relative {
position: relative;
}
.inset-0 {
inset: calc(var(--spacing) * 0);
}
.top-0 {
top: calc(var(--spacing) * 0);
}
.top-1\/2 {
top: calc(1/2 * 100%);
}
.right-0 {
right: calc(var(--spacing) * 0);
}
.bottom-0 {
bottom: calc(var(--spacing) * 0);
}
.left-0 {
left: calc(var(--spacing) * 0);
}
.z-50 {
z-index: 50;
}
.container {
width: 100%;
@media (width >= 40rem) {
max-width: 40rem;
}
@media (width >= 48rem) {
max-width: 48rem;
}
@media (width >= 64rem) {
max-width: 64rem;
}
@media (width >= 80rem) {
max-width: 80rem;
}
@media (width >= 96rem) {
max-width: 96rem;
}
}
.m-0 {
margin: calc(var(--spacing) * 0);
}
.m-2 {
margin: calc(var(--spacing) * 2);
}
.-mx-4 {
margin-inline: calc(var(--spacing) * -4);
}
.mx-auto {
margin-inline: auto;
}
.mt-2 {
margin-top: calc(var(--spacing) * 2);
}
.mr-1 {
margin-right: calc(var(--spacing) * 1);
}
.mr-2 {
margin-right: calc(var(--spacing) * 2);
}
.mr-3 {
margin-right: calc(var(--spacing) * 3);
}
.mr-6 {
margin-right: calc(var(--spacing) * 6);
}
.mr-8 {
margin-right: calc(var(--spacing) * 8);
}
.mr-10 {
margin-right: calc(var(--spacing) * 10);
}
.mr-12 {
margin-right: calc(var(--spacing) * 12);
}
.mr-14 {
margin-right: calc(var(--spacing) * 14);
}
.mr-16 {
margin-right: calc(var(--spacing) * 16);
}
.mr-auto {
margin-right: auto;
}
.mb-4 {
margin-bottom: calc(var(--spacing) * 4);
}
.mb-6 {
margin-bottom: calc(var(--spacing) * 6);
}
.mb-8 {
margin-bottom: calc(var(--spacing) * 8);
}
.mb-10 {
margin-bottom: calc(var(--spacing) * 10);
}
.mb-12 {
margin-bottom: calc(var(--spacing) * 12);
}
.mb-14 {
margin-bottom: calc(var(--spacing) * 14);
}
.mb-16 {
margin-bottom: calc(var(--spacing) * 16);
}
.mb-24 {
margin-bottom: calc(var(--spacing) * 24);
}
.ml-8 {
margin-left: calc(var(--spacing) * 8);
}
.block {
display: block;
}
.flex {
display: flex;
}
.hidden {
display: none;
}
.inline-block {
display: inline-block;
}
.inline-flex {
display: inline-flex;
}
.h-2 {
height: calc(var(--spacing) * 2);
}
.h-6 {
height: calc(var(--spacing) * 6);
}
.h-8 {
height: calc(var(--spacing) * 8);
}
.h-9 {
height: calc(var(--spacing) * 9);
}
.h-40 {
height: calc(var(--spacing) * 40);
}
.h-full {
height: 100%;
}
.w-1\/2 {
width: calc(1/2 * 100%);
}
.w-1\/4 {
width: calc(1/4 * 100%);
}
.w-1\/6 {
width: calc(1/6 * 100%);
}
.w-2 {
width: calc(var(--spacing) * 2);
}
.w-5\/6 {
width: calc(5/6 * 100%);
}
.w-6 {
width: calc(var(--spacing) * 6);
}
.w-8 {
width: calc(var(--spacing) * 8);
}
.w-12 {
width: calc(var(--spacing) * 12);
}
.w-full {
width: 100%;
}
.max-w-2xl {
max-width: var(--container-2xl);
}
.max-w-md {
max-width: var(--container-md);
}
.max-w-sm {
max-width: var(--container-sm);
}
.max-w-xl {
max-width: var(--container-xl);
}
.shrink-0 {
flex-shrink: 0;
}
.translate-1\/2 {
--tw-translate-x: calc(1/2 * 100%);
--tw-translate-y: calc(1/2 * 100%);
translate: var(--tw-translate-x) var(--tw-translate-y);
}
.transform {
transform: var(--tw-rotate-x) var(--tw-rotate-y) var(--tw-rotate-z) var(--tw-skew-x) var(--tw-skew-y);
}
.cursor-pointer {
cursor: pointer;
}
.flex-col {
flex-direction: column;
}
.flex-row {
flex-direction: row;
}
.flex-wrap {
flex-wrap: wrap;
}
.place-items-center {
place-items: center;
}
.items-center {
align-items: center;
}
.justify-between {
justify-content: space-between;
}
.self-center {
align-self: center;
}
.overflow-y-auto {
overflow-y: auto;
}
.rounded {
border-radius: 0.25rem;
}
.rounded-full {
border-radius: calc(infinity * 1px);
}
.rounded-lg {
border-radius: var(--radius-lg);
}
.rounded-md {
border-radius: var(--radius-md);
}
.border {
border-style: var(--tw-border-style);
border-width: 1px;
}
.border-0 {
border-style: var(--tw-border-style);
border-width: 0px;
}
.border-r {
border-right-style: var(--tw-border-style);
border-right-width: 1px;
}
.border-b {
border-bottom-style: var(--tw-border-style);
border-bottom-width: 1px;
}
.border-b-2 {
border-bottom-style: var(--tw-border-style);
border-bottom-width: 2px;
}
.border-l {
border-left-style: var(--tw-border-style);
border-left-width: 1px;
}
.border-gray-200 {
border-color: var(--color-gray-200);
}
.border-transparent {
border-color: transparent;
}
.bg-gray-50 {
background-color: var(--color-gray-50);
}
.bg-gray-100 {
background-color: var(--color-gray-100);
}
.bg-gray-800 {
background-color: var(--color-gray-800);
}
.bg-orange-300 {
background-color: var(--color-orange-300);
}
.bg-white {
background-color: var(--color-white);
}
.object-cover {
object-fit: cover;
}
.object-scale-down {
object-fit: scale-down;
}
.p-2 {
padding: calc(var(--spacing) * 2);
}
.p-10 {
padding: calc(var(--spacing) * 10);
}
.px-2 {
padding-inline: calc(var(--spacing) * 2);
}
.px-4 {
padding-inline: calc(var(--spacing) * 4);
}
.px-6 {
padding-inline: calc(var(--spacing) * 6);
}
.px-8 {
padding-inline: calc(var(--spacing) * 8);
}
.px-10 {
padding-inline: calc(var(--spacing) * 10);
}
.px-12 {
padding-inline: calc(var(--spacing) * 12);
}
.py-2 {
padding-block: calc(var(--spacing) * 2);
}
.py-4 {
padding-block: calc(var(--spacing) * 4);
}
.py-5 {
padding-block: calc(var(--spacing) * 5);
}
.py-6 {
padding-block: calc(var(--spacing) * 6);
}
.py-8 {
padding-block: calc(var(--spacing) * 8);
}
.py-20 {
padding-block: calc(var(--spacing) * 20);
}
.pr-10 {
padding-right: calc(var(--spacing) * 10);
}
.pb-10 {
padding-bottom: calc(var(--spacing) * 10);
}
.pl-4 {
padding-left: calc(var(--spacing) * 4);
}
.pl-6 {
padding-left: calc(var(--spacing) * 6);
}
.text-center {
text-align: center;
}
.text-left {
text-align: left;
}
.text-2xl {
font-size: var(--text-2xl);
line-height: var(--tw-leading, var(--text-2xl--line-height));
}
.text-3xl {
font-size: var(--text-3xl);
line-height: var(--tw-leading, var(--text-3xl--line-height));
}
.text-5xl {
font-size: var(--text-5xl);
line-height: var(--tw-leading, var(--text-5xl--line-height));
}
.text-xs {
font-size: var(--text-xs);
line-height: var(--tw-leading, var(--text-xs--line-height));
}
.font-bold {
--tw-font-weight: var(--font-weight-bold);
font-weight: var(--font-weight-bold);
}
.font-semibold {
--tw-font-weight: var(--font-weight-semibold);
font-weight: var(--font-weight-semibold);
}
.text-ellipsis {
text-overflow: ellipsis;
}
.text-blue-300 {
color: var(--color-blue-300);
}
.text-gray-400 {
color: var(--color-gray-400);
}
.text-gray-500 {
color: var(--color-gray-500);
}
.text-gray-600 {
color: var(--color-gray-600);
}
.text-white {
color: var(--color-white);
}
.uppercase {
text-transform: uppercase;
}
.placeholder-gray-400 {
&::placeholder {
color: var(--color-gray-400);
}
}
.opacity-25 {
opacity: 25%;
}
.shadow-2xl {
--tw-shadow: 0 25px 50px -12px var(--tw-shadow-color, rgb(0 0 0 / 0.25));
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
}
.shadow-lg {
--tw-shadow: 0 10px 15px -3px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 4px 6px -4px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
}
.ring-1 {
--tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentColor);
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
}
.transition {
transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter;
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
transition-duration: var(--tw-duration, var(--default-transition-duration));
}
.transition-all {
transition-property: all;
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
transition-duration: var(--tw-duration, var(--default-transition-duration));
}
.duration-200 {
--tw-duration: 200ms;
transition-duration: 200ms;
}
.hover\:bg-orange-400 {
&:hover {
@media (hover: hover) {
background-color: var(--color-orange-400);
}
}
}
.hover\:text-gray-600 {
&:hover {
@media (hover: hover) {
color: var(--color-gray-600);
}
}
}
.hover\:text-gray-700 {
&:hover {
@media (hover: hover) {
color: var(--color-gray-700);
}
}
}
.hover\:shadow-2xl {
&:hover {
@media (hover: hover) {
--tw-shadow: 0 25px 50px -12px var(--tw-shadow-color, rgb(0 0 0 / 0.25));
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
}
}
}
.hover\:ring-4 {
&:hover {
@media (hover: hover) {
--tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentColor);
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
}
}
}
.focus\:border-blue-300 {
&:focus {
border-color: var(--color-blue-300);
}
}
.focus\:ring-blue-300 {
&:focus {
--tw-ring-color: var(--color-blue-300);
}
}
.focus\:ring-transparent {
&:focus {
--tw-ring-color: transparent;
}
}
.focus\:outline-hidden {
&:focus {
--tw-outline-style: none;
outline-style: none;
@media (forced-colors: active) {
outline: 2px solid transparent;
outline-offset: 2px;
}
}
}
.md\:mb-0 {
@media (width >= 48rem) {
margin-bottom: calc(var(--spacing) * 0);
}
}
.md\:w-1\/2 {
@media (width >= 48rem) {
width: calc(1/2 * 100%);
}
}
.md\:w-auto {
@media (width >= 48rem) {
width: auto;
}
}
.md\:text-right {
@media (width >= 48rem) {
text-align: right;
}
}
.md\:text-6xl {
@media (width >= 48rem) {
font-size: var(--text-6xl);
line-height: var(--tw-leading, var(--text-6xl--line-height));
}
}
.lg\:pl-20 {
@media (width >= 64rem) {
padding-left: calc(var(--spacing) * 20);
}
}
.xl\:mx-auto {
@media (width >= 80rem) {
margin-inline: auto;
}
}
.xl\:mb-0 {
@media (width >= 80rem) {
margin-bottom: calc(var(--spacing) * 0);
}
}
.xl\:block {
@media (width >= 80rem) {
display: block;
}
}
.xl\:flex {
@media (width >= 80rem) {
display: flex;
}
}
.xl\:hidden {
@media (width >= 80rem) {
display: none;
}
}
.xl\:inline-block {
@media (width >= 80rem) {
display: inline-block;
}
}
.xl\:w-2\/3 {
@media (width >= 80rem) {
width: calc(2/3 * 100%);
}
}
}
@property --tw-translate-x {
syntax: "*";
inherits: false;
initial-value: 0;
}
@property --tw-translate-y {
syntax: "*";
inherits: false;
initial-value: 0;
}
@property --tw-translate-z {
syntax: "*";
inherits: false;
initial-value: 0;
}
@property --tw-rotate-x {
syntax: "*";
inherits: false;
initial-value: rotateX(0);
}
@property --tw-rotate-y {
syntax: "*";
inherits: false;
initial-value: rotateY(0);
}
@property --tw-rotate-z {
syntax: "*";
inherits: false;
initial-value: rotateZ(0);
}
@property --tw-skew-x {
syntax: "*";
inherits: false;
initial-value: skewX(0);
}
@property --tw-skew-y {
syntax: "*";
inherits: false;
initial-value: skewY(0);
}
@property --tw-border-style {
syntax: "*";
inherits: false;
initial-value: solid;
}
@property --tw-font-weight {
syntax: "*";
inherits: false;
}
@property --tw-shadow {
syntax: "*";
inherits: false;
initial-value: 0 0 #0000;
}
@property --tw-shadow-color {
syntax: "*";
inherits: false;
}
@property --tw-shadow-alpha {
syntax: "";
inherits: false;
initial-value: 100%;
}
@property --tw-inset-shadow {
syntax: "*";
inherits: false;
initial-value: 0 0 #0000;
}
@property --tw-inset-shadow-color {
syntax: "*";
inherits: false;
}
@property --tw-inset-shadow-alpha {
syntax: "";
inherits: false;
initial-value: 100%;
}
@property --tw-ring-color {
syntax: "*";
inherits: false;
}
@property --tw-ring-shadow {
syntax: "*";
inherits: false;
initial-value: 0 0 #0000;
}
@property --tw-inset-ring-color {
syntax: "*";
inherits: false;
}
@property --tw-inset-ring-shadow {
syntax: "*";
inherits: false;
initial-value: 0 0 #0000;
}
@property --tw-ring-inset {
syntax: "*";
inherits: false;
}
@property --tw-ring-offset-width {
syntax: "";
inherits: false;
initial-value: 0px;
}
@property --tw-ring-offset-color {
syntax: "*";
inherits: false;
initial-value: #fff;
}
@property --tw-ring-offset-shadow {
syntax: "*";
inherits: false;
initial-value: 0 0 #0000;
}
@property --tw-duration {
syntax: "*";
inherits: false;
}
================================================
FILE: examples/01-app-demos/ecommerce-site/src/api.rs
================================================
use dioxus::prelude::Result;
use serde::{Deserialize, Serialize};
use std::fmt::Display;
// Cache up to 100 requests, invalidating them after 60 seconds
pub(crate) async fn fetch_product(product_id: usize) -> Result {
Ok(
reqwest::get(format!("https://fakestoreapi.com/products/{product_id}"))
.await?
.json()
.await?,
)
}
// Cache up to 100 requests, invalidating them after 60 seconds
pub(crate) async fn fetch_products(count: usize, sort: Sort) -> Result> {
Ok(reqwest::get(format!(
"https://fakestoreapi.com/products/?sort={sort}&limit={count}"
))
.await?
.json()
.await?)
}
#[derive(Serialize, Deserialize, PartialEq, Clone, Debug, Default)]
pub(crate) struct Product {
pub(crate) id: u32,
pub(crate) title: String,
pub(crate) price: f32,
pub(crate) description: String,
pub(crate) category: String,
pub(crate) image: String,
pub(crate) rating: Rating,
}
#[derive(Serialize, Deserialize, PartialEq, Clone, Debug, Default)]
pub(crate) struct Rating {
pub(crate) rate: f32,
pub(crate) count: u32,
}
impl Display for Rating {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let rounded = self.rate.round() as usize;
for _ in 0..rounded {
"★".fmt(f)?;
}
for _ in 0..(5 - rounded) {
"☆".fmt(f)?;
}
write!(f, " ({:01}) ({} ratings)", self.rate, self.count)?;
Ok(())
}
}
#[allow(unused)]
#[derive(Clone, Copy, Hash, PartialEq, Eq, PartialOrd)]
pub(crate) enum Sort {
Descending,
Ascending,
}
impl Display for Sort {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Sort::Descending => write!(f, "desc"),
Sort::Ascending => write!(f, "asc"),
}
}
}
================================================
FILE: examples/01-app-demos/ecommerce-site/src/components/error.rs
================================================
use dioxus::prelude::*;
#[component]
pub fn error_page() -> Element {
rsx! {
section { class: "py-20",
div { class: "container mx-auto px-4",
div { class: "flex flex-wrap -mx-4 mb-24 text-center",
"An internal error has occurred"
}
}
}
}
}
================================================
FILE: examples/01-app-demos/ecommerce-site/src/components/home.rs
================================================
// The homepage is statically rendered, so we don't need to a persistent websocket connection.
use crate::{
api::{fetch_products, Sort},
components::nav::Nav,
components::product_item::ProductItem,
};
use dioxus::prelude::*;
pub(crate) fn Home() -> Element {
let products = use_loader(|| fetch_products(10, Sort::Ascending))?;
rsx! {
Nav {}
section { class: "p-10",
for product in products.iter() {
ProductItem {
product: product.clone()
}
}
}
}
}
================================================
FILE: examples/01-app-demos/ecommerce-site/src/components/loading.rs
================================================
use dioxus::prelude::*;
#[component]
pub(crate) fn ChildrenOrLoading(children: Element) -> Element {
rsx! {
Stylesheet { href: asset!("/public/loading.css") }
SuspenseBoundary {
fallback: |_| rsx! { div { class: "spinner", } },
{children}
}
}
}
================================================
FILE: examples/01-app-demos/ecommerce-site/src/components/nav.rs
================================================
use dioxus::prelude::*;
#[component]
pub fn Nav() -> Element {
rsx! {
section { class: "relative",
nav { class: "flex justify-between border-b",
div { class: "px-12 py-8 flex w-full items-center",
a { class: "hidden xl:block mr-16",
href: "/",
icons::cart_icon {}
}
ul { class: "hidden xl:flex font-semibold font-heading",
li { class: "mr-12",
a { class: "hover:text-gray-600",
href: "/",
"Category"
}
}
li { class: "mr-12",
a { class: "hover:text-gray-600",
href: "/",
"Collection"
}
}
li { class: "mr-12",
a { class: "hover:text-gray-600",
href: "/",
"Story"
}
}
li {
a { class: "hover:text-gray-600",
href: "/",
"Brand"
}
}
}
a { class: "shrink-0 xl:mx-auto text-3xl font-bold font-heading",
href: "/",
img { class: "h-9",
width: "auto",
alt: "",
src: "https://shuffle.dev/yofte-assets/logos/yofte-logo.svg",
}
}
div { class: "hidden xl:inline-block mr-14",
input { class: "py-5 px-8 w-full placeholder-gray-400 text-xs uppercase font-semibold font-heading bg-gray-50 border border-gray-200 focus:ring-blue-300 focus:border-blue-300 rounded-md",
placeholder: "Search",
r#type: "text",
}
}
div { class: "hidden xl:flex items-center",
a { class: "mr-10 hover:text-gray-600",
href: "",
icons::icon_1 {}
}
a { class: "flex items-center hover:text-gray-600",
href: "/",
icons::icon_2 {}
span { class: "inline-block w-6 h-6 text-center bg-gray-50 rounded-full font-semibold font-heading",
"3"
}
}
}
}
a { class: "hidden xl:flex items-center px-12 border-l font-semibold font-heading hover:text-gray-600",
href: "/",
icons::icon_3 {}
span {
"Sign In"
}
}
a { class: "xl:hidden flex mr-6 items-center text-gray-600",
href: "/",
icons::icon_4 {}
span { class: "inline-block w-6 h-6 text-center bg-gray-50 rounded-full font-semibold font-heading",
"3"
}
}
a { class: "navbar-burger self-center mr-12 xl:hidden",
href: "/",
icons::icon_5 {}
}
}
div { class: "hidden navbar-menu fixed top-0 left-0 bottom-0 w-5/6 max-w-sm z-50",
div { class: "navbar-backdrop fixed inset-0 bg-gray-800 opacity-25",
}
nav { class: "relative flex flex-col py-6 px-6 w-full h-full bg-white border-r overflow-y-auto",
div { class: "flex items-center mb-8",
a { class: "mr-auto text-3xl font-bold font-heading",
href: "/",
img { class: "h-9",
src: "https://shuffle.dev/yofte-assets/logos/yofte-logo.svg",
width: "auto",
alt: "",
}
}
button { class: "navbar-close",
icons::icon_6 {}
}
}
div { class: "flex mb-8 justify-between",
a { class: "inline-flex items-center font-semibold font-heading",
href: "/",
icons::icon_7 {}
span {
"Sign In"
}
}
div { class: "flex items-center",
a { class: "mr-10",
href: "/",
icons::icon_8 {}
}
a { class: "flex items-center",
href: "/",
icons::icon_9 {}
span { class: "inline-block w-6 h-6 text-center bg-gray-100 rounded-full font-semibold font-heading",
"3"
}
}
}
}
input { class: "block mb-10 py-5 px-8 bg-gray-100 rounded-md border-transparent focus:ring-blue-300 focus:border-blue-300 focus:outline-hidden",
r#type: "search",
placeholder: "Search",
}
ul { class: "text-3xl font-bold font-heading",
li { class: "mb-8",
a {
href: "/",
"Category"
}
}
li { class: "mb-8",
a {
href: "/",
"Collection"
}
}
li { class: "mb-8",
a {
href: "/",
"Story"
}
}
li {
a {
href: "/",
"Brand"
}
}
}
}
}
}
}
}
mod icons {
use super::*;
pub(super) fn cart_icon() -> Element {
rsx! {
svg { class: "mr-3",
fill: "none",
xmlns: "http://www.w3.org/2000/svg",
view_box: "0 0 23 23",
width: "23",
height: "23",
path {
stroke_linejoin: "round",
d: "M18.1159 8.72461H2.50427C1.99709 8.72461 1.58594 9.12704 1.58594 9.62346V21.3085C1.58594 21.8049 1.99709 22.2074 2.50427 22.2074H18.1159C18.6231 22.2074 19.0342 21.8049 19.0342 21.3085V9.62346C19.0342 9.12704 18.6231 8.72461 18.1159 8.72461Z",
stroke: "currentColor",
stroke_linecap: "round",
stroke_width: "1.5",
}
path {
stroke: "currentColor",
stroke_linecap: "round",
d: "M6.34473 6.34469V4.95676C6.34473 3.85246 6.76252 2.79338 7.5062 2.01252C8.24988 1.23165 9.25852 0.792969 10.3102 0.792969C11.362 0.792969 12.3706 1.23165 13.1143 2.01252C13.858 2.79338 14.2758 3.85246 14.2758 4.95676V6.34469",
stroke_width: "1.5",
stroke_linejoin: "round",
}
}
}
}
pub(super) fn icon_1() -> Element {
rsx! {
svg {
xmlns: "http://www.w3.org/2000/svg",
height: "20",
view_box: "0 0 23 20",
width: "23",
fill: "none",
path {
d: "M11.4998 19.2061L2.70115 9.92527C1.92859 9.14433 1.41864 8.1374 1.24355 7.04712C1.06847 5.95684 1.23713 4.8385 1.72563 3.85053V3.85053C2.09464 3.10462 2.63366 2.45803 3.29828 1.96406C3.9629 1.47008 4.73408 1.14284 5.5483 1.00931C6.36252 0.875782 7.19647 0.939779 7.98144 1.19603C8.7664 1.45228 9.47991 1.89345 10.0632 2.48319L11.4998 3.93577L12.9364 2.48319C13.5197 1.89345 14.2332 1.45228 15.0182 1.19603C15.8031 0.939779 16.6371 0.875782 17.4513 1.00931C18.2655 1.14284 19.0367 1.47008 19.7013 1.96406C20.3659 2.45803 20.905 3.10462 21.274 3.85053V3.85053C21.7625 4.8385 21.9311 5.95684 21.756 7.04712C21.581 8.1374 21.071 9.14433 20.2984 9.92527L11.4998 19.2061Z",
stroke: "currentColor",
stroke_width: "1.5",
stroke_linejoin: "round",
stroke_linecap: "round",
}
}
}
}
pub(super) fn icon_2() -> Element {
rsx! {
svg { class: "mr-3",
fill: "none",
height: "31",
xmlns: "http://www.w3.org/2000/svg",
width: "32",
view_box: "0 0 32 31",
path {
stroke_linejoin: "round",
stroke_width: "1.5",
d: "M16.0006 16.3154C19.1303 16.3154 21.6673 13.799 21.6673 10.6948C21.6673 7.59064 19.1303 5.07422 16.0006 5.07422C12.871 5.07422 10.334 7.59064 10.334 10.6948C10.334 13.799 12.871 16.3154 16.0006 16.3154Z",
stroke_linecap: "round",
stroke: "currentColor",
}
path {
stroke_width: "1.5",
d: "M24.4225 23.8963C23.6678 22.3507 22.4756 21.0445 20.9845 20.1298C19.4934 19.2151 17.7647 18.7295 15.9998 18.7295C14.2349 18.7295 12.5063 19.2151 11.0152 20.1298C9.52406 21.0445 8.33179 22.3507 7.57715 23.8963",
stroke: "currentColor",
stroke_linecap: "round",
stroke_linejoin: "round",
}
}
}
}
pub(super) fn icon_3() -> Element {
rsx! {
svg { class: "h-2 w-2 text-gray-500 cursor-pointer",
height: "10",
width: "10",
xmlns: "http://www.w3.org/2000/svg",
fill: "none",
view_box: "0 0 10 10",
path {
stroke_width: "1.5",
stroke_linejoin: "round",
d: "M9.00002 1L1 9.00002M1.00003 1L9.00005 9.00002",
stroke: "black",
stroke_linecap: "round",
}
}
}
}
pub(super) fn icon_4() -> Element {
rsx! {
svg {
view_box: "0 0 20 12",
fill: "none",
width: "20",
xmlns: "http://www.w3.org/2000/svg",
height: "12",
path {
d: "M1 2H19C19.2652 2 19.5196 1.89464 19.7071 1.70711C19.8946 1.51957 20 1.26522 20 1C20 0.734784 19.8946 0.48043 19.7071 0.292893C19.5196 0.105357 19.2652 0 19 0H1C0.734784 0 0.48043 0.105357 0.292893 0.292893C0.105357 0.48043 0 0.734784 0 1C0 1.26522 0.105357 1.51957 0.292893 1.70711C0.48043 1.89464 0.734784 2 1 2ZM19 10H1C0.734784 10 0.48043 10.1054 0.292893 10.2929C0.105357 10.4804 0 10.7348 0 11C0 11.2652 0.105357 11.5196 0.292893 11.7071C0.48043 11.8946 0.734784 12 1 12H19C19.2652 12 19.5196 11.8946 19.7071 11.7071C19.8946 11.5196 20 11.2652 20 11C20 10.7348 19.8946 10.4804 19.7071 10.2929C19.5196 10.1054 19.2652 10 19 10ZM19 5H1C0.734784 5 0.48043 5.10536 0.292893 5.29289C0.105357 5.48043 0 5.73478 0 6C0 6.26522 0.105357 6.51957 0.292893 6.70711C0.48043 6.89464 0.734784 7 1 7H19C19.2652 7 19.5196 6.89464 19.7071 6.70711C19.8946 6.51957 20 6.26522 20 6C20 5.73478 19.8946 5.48043 19.7071 5.29289C19.5196 5.10536 19.2652 5 19 5Z",
fill: "#8594A5",
}
}
}
}
pub(super) fn icon_5() -> Element {
rsx! {
svg { class: "mr-2",
fill: "none",
xmlns: "http://www.w3.org/2000/svg",
width: "23",
height: "23",
view_box: "0 0 23 23",
path {
stroke_width: "1.5",
stroke_linecap: "round",
stroke_linejoin: "round",
d: "M18.1159 8.72461H2.50427C1.99709 8.72461 1.58594 9.12704 1.58594 9.62346V21.3085C1.58594 21.8049 1.99709 22.2074 2.50427 22.2074H18.1159C18.6231 22.2074 19.0342 21.8049 19.0342 21.3085V9.62346C19.0342 9.12704 18.6231 8.72461 18.1159 8.72461Z",
stroke: "currentColor",
}
path {
d: "M6.34473 6.34469V4.95676C6.34473 3.85246 6.76252 2.79338 7.5062 2.01252C8.24988 1.23165 9.25852 0.792969 10.3102 0.792969C11.362 0.792969 12.3706 1.23165 13.1143 2.01252C13.858 2.79338 14.2758 3.85246 14.2758 4.95676V6.34469",
stroke_linejoin: "round",
stroke_width: "1.5",
stroke_linecap: "round",
stroke: "currentColor",
}
}
}
}
pub(super) fn icon_6() -> Element {
rsx! {
svg { class: "mr-3",
height: "31",
xmlns: "http://www.w3.org/2000/svg",
view_box: "0 0 32 31",
width: "32",
fill: "none",
path {
stroke: "currentColor",
stroke_width: "1.5",
d: "M16.0006 16.3154C19.1303 16.3154 21.6673 13.799 21.6673 10.6948C21.6673 7.59064 19.1303 5.07422 16.0006 5.07422C12.871 5.07422 10.334 7.59064 10.334 10.6948C10.334 13.799 12.871 16.3154 16.0006 16.3154Z",
stroke_linecap: "round",
stroke_linejoin: "round",
}
path {
stroke_linecap: "round",
stroke_width: "1.5",
stroke: "currentColor",
stroke_linejoin: "round",
d: "M24.4225 23.8963C23.6678 22.3507 22.4756 21.0445 20.9845 20.1298C19.4934 19.2151 17.7647 18.7295 15.9998 18.7295C14.2349 18.7295 12.5063 19.2151 11.0152 20.1298C9.52406 21.0445 8.33179 22.3507 7.57715 23.8963",
}
}
}
}
pub(super) fn icon_7() -> Element {
rsx! {
svg { class: "mr-3",
view_box: "0 0 23 23",
fill: "none",
height: "23",
width: "23",
xmlns: "http://www.w3.org/2000/svg",
path {
stroke_linecap: "round",
stroke: "currentColor",
stroke_width: "1.5",
stroke_linejoin: "round",
d: "M18.1159 8.72461H2.50427C1.99709 8.72461 1.58594 9.12704 1.58594 9.62346V21.3085C1.58594 21.8049 1.99709 22.2074 2.50427 22.2074H18.1159C18.6231 22.2074 19.0342 21.8049 19.0342 21.3085V9.62346C19.0342 9.12704 18.6231 8.72461 18.1159 8.72461Z",
}
path {
d: "M6.34473 6.34469V4.95676C6.34473 3.85246 6.76252 2.79338 7.5062 2.01252C8.24988 1.23165 9.25852 0.792969 10.3102 0.792969C11.362 0.792969 12.3706 1.23165 13.1143 2.01252C13.858 2.79338 14.2758 3.85246 14.2758 4.95676V6.34469",
stroke_width: "1.5",
stroke_linecap: "round",
stroke: "currentColor",
stroke_linejoin: "round",
}
}
}
}
pub(super) fn icon_8() -> Element {
rsx! {
svg {
height: "20",
width: "23",
fill: "none",
view_box: "0 0 23 20",
xmlns: "http://www.w3.org/2000/svg",
path {
d: "M11.4998 19.2061L2.70115 9.92527C1.92859 9.14433 1.41864 8.1374 1.24355 7.04712C1.06847 5.95684 1.23713 4.8385 1.72563 3.85053V3.85053C2.09464 3.10462 2.63366 2.45803 3.29828 1.96406C3.9629 1.47008 4.73408 1.14284 5.5483 1.00931C6.36252 0.875782 7.19647 0.939779 7.98144 1.19603C8.7664 1.45228 9.47991 1.89345 10.0632 2.48319L11.4998 3.93577L12.9364 2.48319C13.5197 1.89345 14.2332 1.45228 15.0182 1.19603C15.8031 0.939779 16.6371 0.875782 17.4513 1.00931C18.2655 1.14284 19.0367 1.47008 19.7013 1.96406C20.3659 2.45803 20.905 3.10462 21.274 3.85053V3.85053C21.7625 4.8385 21.9311 5.95684 21.756 7.04712C21.581 8.1374 21.071 9.14433 20.2984 9.92527L11.4998 19.2061Z",
stroke_linejoin: "round",
stroke: "currentColor",
stroke_width: "1.5",
stroke_linecap: "round",
}
}
}
}
pub(super) fn icon_9() -> Element {
rsx! {
svg {
view_box: "0 0 18 18",
xmlns: "http://www.w3.org/2000/svg",
width: "18",
height: "18",
fill: "none",
path {
fill: "black",
d: "M18 15.4688H0V17.7207H18V15.4688Z",
}
path {
fill: "black",
d: "M11.0226 7.87402H0V10.126H11.0226V7.87402Z",
}
path {
fill: "black",
d: "M18 0.279297H0V2.53127H18V0.279297Z",
}
}
}
}
}
================================================
FILE: examples/01-app-demos/ecommerce-site/src/components/product_item.rs
================================================
use dioxus::prelude::*;
use crate::api::Product;
#[component]
pub(crate) fn ProductItem(product: Product) -> Element {
let Product {
id,
title,
price,
category,
image,
rating,
..
} = product;
rsx! {
section { class: "h-40 p-2 m-2 shadow-lg ring-1 rounded-lg flex flex-row place-items-center hover:ring-4 hover:shadow-2xl transition-all duration-200",
img {
class: "object-scale-down w-1/6 h-full",
src: "{image}",
}
div { class: "pl-4 text-left text-ellipsis",
a {
href: "/details/{id}",
class: "w-full text-center",
"{title}"
}
p {
class: "w-full",
"{rating}"
}
p {
class: "w-full",
"{category}"
}
p {
class: "w-1/4",
"${price}"
}
}
}
}
}
================================================
FILE: examples/01-app-demos/ecommerce-site/src/components/product_page.rs
================================================
use std::{fmt::Display, str::FromStr};
use crate::api::{fetch_product, Product};
use dioxus::prelude::*;
#[component]
pub fn ProductPage(product_id: ReadSignal) -> Element {
let mut quantity = use_signal(|| 1);
let mut size = use_signal(Size::default);
let product = use_loader(move || fetch_product(product_id()))?;
let Product {
title,
price,
description,
category,
image,
rating,
..
} = product();
rsx! {
section { class: "py-20",
div { class: "container mx-auto px-4",
div { class: "flex flex-wrap -mx-4 mb-24",
div { class: "w-full md:w-1/2 px-4 mb-8 md:mb-0",
div { class: "relative mb-10",
style: "height: 564px;",
a { class: "absolute top-1/2 left-0 ml-8 transform translate-1/2",
href: "#",
icons::icon_0 {}
}
img { class: "object-cover w-full h-full",
alt: "",
src: "{image}",
}
a { class: "absolute top-1/2 right-0 mr-8 transform translate-1/2",
href: "#",
icons::icon_1 {}
}
}
}
div { class: "w-full md:w-1/2 px-4",
div { class: "lg:pl-20",
div { class: "mb-10 pb-10 border-b",
h2 { class: "mt-2 mb-6 max-w-xl text-5xl md:text-6xl font-bold font-heading",
"{title}"
}
div { class: "mb-8",
"{rating}"
}
p { class: "inline-block mb-8 text-2xl font-bold font-heading text-blue-300",
span {
"${price}"
}
}
p { class: "max-w-md text-gray-500",
"{description}"
}
}
div { class: "flex mb-12",
div { class: "mr-6",
span { class: "block mb-4 font-bold font-heading text-gray-400 uppercase",
"QTY"
}
div { class: "inline-flex items-center px-4 font-semibold font-heading text-gray-500 border border-gray-200 focus:ring-blue-300 focus:border-blue-300 rounded-md",
button { class: "py-2 hover:text-gray-700",
onclick: move |_| quantity += 1,
icons::icon_2 {}
}
input { class: "w-12 m-0 px-2 py-4 text-center md:text-right border-0 focus:ring-transparent focus:outline-hidden rounded-md",
placeholder: "1",
r#type: "number",
value: "{quantity}",
oninput: move |evt| if let Ok(as_number) = evt.value().parse() { quantity.set(as_number) },
}
button { class: "py-2 hover:text-gray-700",
onclick: move |_| quantity -= 1,
icons::icon_3 {}
}
}
}
div {
span { class: "block mb-4 font-bold font-heading text-gray-400 uppercase",
"Size"
}
select { class: "pl-6 pr-10 py-4 font-semibold font-heading text-gray-500 border border-gray-200 focus:ring-blue-300 focus:border-blue-300 rounded-md",
id: "",
name: "",
onchange: move |evt| {
if let Ok(new_size) = evt.value().parse() {
size.set(new_size);
}
},
option {
value: "1",
"Medium"
}
option {
value: "2",
"Small"
}
option {
value: "3",
"Large"
}
}
}
}
div { class: "flex flex-wrap -mx-4 mb-14 items-center",
div { class: "w-full xl:w-2/3 px-4 mb-4 xl:mb-0",
a { class: "block bg-orange-300 hover:bg-orange-400 text-center text-white font-bold font-heading py-5 px-8 rounded-md uppercase transition duration-200",
href: "#",
"Add to cart"
}
}
}
div { class: "flex items-center",
span { class: "mr-8 text-gray-500 font-bold font-heading uppercase",
"SHARE IT"
}
a { class: "mr-1 w-8 h-8",
href: "#",
img {
alt: "",
src: "https://shuffle.dev/yofte-assets/buttons/facebook-circle.svg",
}
}
a { class: "mr-1 w-8 h-8",
href: "#",
img {
alt: "",
src: "https://shuffle.dev/yofte-assets/buttons/instagram-circle.svg",
}
}
a { class: "w-8 h-8",
href: "#",
img {
src: "https://shuffle.dev/yofte-assets/buttons/twitter-circle.svg",
alt: "",
}
}
}
}
}
}
div {
ul { class: "flex flex-wrap mb-16 border-b-2",
li { class: "w-1/2 md:w-auto",
a { class: "inline-block py-6 px-10 bg-white text-gray-500 font-bold font-heading shadow-2xl",
href: "#",
"Description"
}
}
li { class: "w-1/2 md:w-auto",
a { class: "inline-block py-6 px-10 text-gray-500 font-bold font-heading",
href: "#",
"Customer reviews"
}
}
li { class: "w-1/2 md:w-auto",
a { class: "inline-block py-6 px-10 text-gray-500 font-bold font-heading",
href: "#",
"Shipping & returns"
}
}
li { class: "w-1/2 md:w-auto",
a { class: "inline-block py-6 px-10 text-gray-500 font-bold font-heading",
href: "#",
"Brand"
}
}
}
h3 { class: "mb-8 text-3xl font-bold font-heading text-blue-300",
"{category}"
}
p { class: "max-w-2xl text-gray-500",
"{description}"
}
}
}
}
}
}
#[derive(Default)]
enum Size {
Small,
#[default]
Medium,
Large,
}
impl Display for Size {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Size::Small => "small".fmt(f),
Size::Medium => "medium".fmt(f),
Size::Large => "large".fmt(f),
}
}
}
impl FromStr for Size {
type Err = ();
fn from_str(s: &str) -> Result {
use Size::*;
match s.to_lowercase().as_str() {
"small" => Ok(Small),
"medium" => Ok(Medium),
"large" => Ok(Large),
_ => Err(()),
}
}
}
mod icons {
use super::*;
pub(super) fn icon_0() -> Element {
rsx! {
svg { class: "w-6 h-6",
view_box: "0 0 24 23",
xmlns: "http://www.w3.org/2000/svg",
height: "23",
fill: "none",
width: "24",
path {
stroke: "black",
fill: "black",
d: "M2.01328 18.9877C2.05682 16.7902 2.71436 12.9275 6.3326 9.87096L6.33277 9.87116L6.33979 9.86454L6.3398 9.86452C6.34682 9.85809 8.64847 7.74859 13.4997 7.74859C13.6702 7.74859 13.8443 7.75111 14.0206 7.757L14.0213 7.75702L14.453 7.76978L14.6331 7.77511V7.59486V3.49068L21.5728 10.5736L14.6331 17.6562V13.6558V13.5186L14.4998 13.4859L14.1812 13.4077C14.1807 13.4075 14.1801 13.4074 14.1792 13.4072M2.01328 18.9877L14.1792 13.4072M2.01328 18.9877C7.16281 11.8391 14.012 13.3662 14.1792 13.4072M2.01328 18.9877L14.1792 13.4072M23.125 10.6961L23.245 10.5736L23.125 10.4512L13.7449 0.877527L13.4449 0.571334V1V6.5473C8.22585 6.54663 5.70981 8.81683 5.54923 8.96832C-0.317573 13.927 0.931279 20.8573 0.946581 20.938L0.946636 20.9383L1.15618 22.0329L1.24364 22.4898L1.47901 22.0885L2.041 21.1305L2.04103 21.1305C4.18034 17.4815 6.71668 15.7763 8.8873 15.0074C10.9246 14.2858 12.6517 14.385 13.4449 14.4935V20.1473V20.576L13.7449 20.2698L23.125 10.6961Z",
stroke_width: "0.35",
}
}
}
}
pub(super) fn icon_1() -> Element {
rsx! {
svg { class: "w-6 h-6",
height: "27",
view_box: "0 0 27 27",
fill: "none",
width: "27",
xmlns: "http://www.w3.org/2000/svg",
path {
d: "M13.4993 26.2061L4.70067 16.9253C3.9281 16.1443 3.41815 15.1374 3.24307 14.0471C3.06798 12.9568 3.23664 11.8385 3.72514 10.8505V10.8505C4.09415 10.1046 4.63318 9.45803 5.29779 8.96406C5.96241 8.47008 6.73359 8.14284 7.54782 8.00931C8.36204 7.87578 9.19599 7.93978 9.98095 8.19603C10.7659 8.45228 11.4794 8.89345 12.0627 9.48319L13.4993 10.9358L14.9359 9.48319C15.5192 8.89345 16.2327 8.45228 17.0177 8.19603C17.8026 7.93978 18.6366 7.87578 19.4508 8.00931C20.265 8.14284 21.0362 8.47008 21.7008 8.96406C22.3654 9.45803 22.9045 10.1046 23.2735 10.8505V10.8505C23.762 11.8385 23.9306 12.9568 23.7556 14.0471C23.5805 15.1374 23.0705 16.1443 22.298 16.9253L13.4993 26.2061Z",
stroke: "black",
stroke_width: "1.5",
stroke_linecap: "round",
stroke_linejoin: "round",
}
}
}
}
pub(super) fn icon_2() -> Element {
rsx! {
svg {
view_box: "0 0 12 12",
height: "12",
width: "12",
fill: "none",
xmlns: "http://www.w3.org/2000/svg",
g {
opacity: "0.35",
rect {
height: "12",
x: "5",
fill: "currentColor",
width: "2",
}
rect {
fill: "currentColor",
width: "2",
height: "12",
x: "12",
y: "5",
transform: "rotate(90 12 5)",
}
}
}
}
}
pub(super) fn icon_3() -> Element {
rsx! {
svg {
width: "12",
fill: "none",
view_box: "0 0 12 2",
height: "2",
xmlns: "http://www.w3.org/2000/svg",
g {
opacity: "0.35",
rect {
transform: "rotate(90 12 0)",
height: "12",
fill: "currentColor",
x: "12",
width: "2",
}
}
}
}
}
}
================================================
FILE: examples/01-app-demos/ecommerce-site/src/main.rs
================================================
#![allow(non_snake_case)]
use components::home::Home;
use components::loading::ChildrenOrLoading;
use dioxus::prelude::*;
mod components {
pub mod error;
pub mod home;
pub mod loading;
pub mod nav;
pub mod product_item;
pub mod product_page;
}
mod api;
fn main() {
dioxus::launch(|| {
rsx! {
document::Link {
rel: "stylesheet",
href: asset!("/public/tailwind.css")
}
ChildrenOrLoading {
Router:: {}
}
}
});
}
#[derive(Clone, Routable, Debug, PartialEq)]
enum Route {
#[route("/")]
Home {},
#[route("/details/:product_id")]
Details { product_id: usize },
}
#[component]
/// Render a more sophisticated page with ssr
fn Details(product_id: usize) -> Element {
rsx! {
div {
components::nav::Nav {}
components::product_page::ProductPage {
product_id
}
}
}
}
================================================
FILE: examples/01-app-demos/ecommerce-site/tailwind.css
================================================
@import "tailwindcss";
@source "./src/**/*.{rs,html,css}";
================================================
FILE: examples/01-app-demos/file-explorer/.gitignore
================================================
# Generated by Cargo
# will have compiled files and executables
/target/
/dist/
/static/
/.dioxus/
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
Cargo.lock
# These are backup files generated by rustfmt
**/*.rs.bk
================================================
FILE: examples/01-app-demos/file-explorer/Cargo.toml
================================================
[package]
name = "file-explorer"
edition = "2021"
version = "0.1.0"
publish = false
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
dioxus = { workspace = true }
open = { workspace = true }
[features]
default = ["desktop"]
desktop = ["dioxus/desktop"]
native = ["dioxus/native"]
================================================
FILE: examples/01-app-demos/file-explorer/Dioxus.toml
================================================
[application]
# App (Project) Name
name = "file-explorer"
# Dioxus App Default Platform
# desktop, web
default_platform = "desktop"
# `build` & `serve` dist path
out_dir = "dist"
# assets file folder
asset_dir = "assets"
[web.app]
# HTML title tag content
title = "file-explorer"
[web.watcher]
# when watcher trigger, regenerate the `index.html`
reload_html = true
# which files or dirs will be watcher monitoring
watch_path = ["src", "assets"]
# include `assets` in web platform
[web.resource]
# CSS style file
style = []
# Javascript code file
script = []
[web.resource.dev]
# Javascript code file
# serve: [dev-server] only
script = []
================================================
FILE: examples/01-app-demos/file-explorer/README.md
================================================
# File-explorer with Rust and Dioxus
This example shows how a Dioxus App can directly leverage system calls and libraries to bridge native functionality with the WebView renderer.

## To run this example:
```
dx serve
```
================================================
FILE: examples/01-app-demos/file-explorer/assets/fileexplorer.css
================================================
* {
margin: 0;
padding: 0;
font-family: 'Roboto', sans-serif;
user-select: none;
transition: .2s all;
}
body {
padding-top: 77px;
}
/* header {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 10;
padding: 20px;
background-color: #2196F3;
color: white;
}
header h1 {
float: left;
font-size: 20px;
font-weight: 400;
}
header .material-icons {
float: right;
cursor: pointer;
}
header .icon-menu {
float: left;
margin-right: 20px;
} */
main {
padding: 20px 50px;
}
.folder * {
width: 100px;
}
.folder {
float: left;
width: 100px;
height: 152px;
/* //padding: 20px; */
margin-right: 50px;
margin-bottom: 70px;
border-radius: 2px;
/* //overflow: hidden; */
cursor: pointer;
}
.folder:hover h1 {
display: none;
}
.folder:hover p.cooltip {
opacity: 1;
top: 0;
}
.folder * {
text-align: center;
}
.folder i {
margin: 0;
font-size: 100px;
color: #607D8B;
}
.folder h1 {
position: relative;
display: block;
top: -37px;
font-size: 20px;
font-weight: 400;
}
.folder p.cooltip {
position: relative;
top: 5px;
left: -50%;
margin-left: 35px;
background: #212121;
font-size: 15px;
color: white;
border-radius: 4px;
padding: 10px 20px;
padding-right: 30px;
width: 100px;
opacity: 0;
}
.folder p.cooltip:before {
content: '';
position: absolute;
display: block;
top: -4px;
left: 50%;
margin-left: -5px;
height: 10px;
width: 10px;
border-radius: 2px;
background-color: #212121;
transform: rotate(45deg);
}
div.properties {
position: fixed;
top: 0;
right: 0;
bottom: 0;
z-index: 10;
width: 300px;
background-color: white;
}
div.properties:before {
content: '';
position: fixed;
top: 0;
left: 0;
right: 300px;
bottom: 0;
background-color: #212121;
opacity: .5;
overflow: hidden;
}
div.properties img {
position: relative;
top: -1px;
left: -1px;
width: 110%;
height: 200px;
filter: blur(2px);
}
div.properties h1 {
position: relative;
width: 100%;
text-align: left;
margin-left: 20px;
color: white;
}
header {
position: fixed;
top: 0;
left: 0;
right: 0;
padding: 20px;
background-color: #2196F3;
color: white;
display: flex;
align-items: center;
}
header h1 {
font-weight: 400;
}
header span {
flex: 1;
}
header i {
margin: 0 10px;
cursor: pointer;
}
header i:nth-child(1) {
margin: 0 20px;
}
================================================
FILE: examples/01-app-demos/file-explorer/src/main.rs
================================================
//! Example: File Explorer
//!
//! This is a fun little desktop application that lets you explore the file system.
//!
//! This example is interesting because it's mixing filesystem operations and GUI, which is typically hard for UI to do.
//! We store the state entirely in a single signal, making the explorer logic fairly easy to reason about.
use std::env::current_dir;
use std::path::PathBuf;
use dioxus::prelude::*;
fn main() {
dioxus::launch(app);
}
fn app() -> Element {
let mut files = use_signal(Files::new);
rsx! {
Stylesheet { href: asset!("/assets/fileexplorer.css") }
Stylesheet { href: "https://fonts.googleapis.com/icon?family=Material+Icons" }
div {
header {
i { class: "material-icons icon-menu", "menu" }
h1 { "Files: " {files.read().current()} }
span { }
i { class: "material-icons", onclick: move |_| files.write().go_up(), "logout" }
}
main {
for (dir_id, path) in files.read().path_names.iter().enumerate() {
{
let path_end = path.components().next_back().map(|p|p.as_os_str()).unwrap_or(path.as_os_str()).to_string_lossy();
let path_display = path.display();
let is_file = path.is_file();
rsx! {
div { class: "folder", key: "{path_display}",
i { class: "material-icons",
onclick: move |_| files.write().enter_dir(dir_id),
if is_file {
"description"
} else {
"folder"
}
p { class: "cooltip", "0 folders / 0 files" }
}
h1 { "{path_end}" }
}
}
}
}
if let Some(err) = files.read().err.as_ref() {
div {
code { "{err}" }
button { onclick: move |_| files.write().clear_err(), "x" }
}
}
}
}
}
}
/// A simple little struct to hold the file explorer state
///
/// We don't use any fancy signals or memoization here - Dioxus is so fast that even a file explorer can be done with a
/// single signal.
struct Files {
current_path: PathBuf,
path_names: Vec,
err: Option,
}
impl Files {
fn new() -> Self {
let mut files = Self {
current_path: std::path::absolute(current_dir().unwrap()).unwrap(),
path_names: vec![],
err: None,
};
files.reload_path_list();
files
}
fn reload_path_list(&mut self) {
let paths = match std::fs::read_dir(&self.current_path) {
Ok(e) => e,
Err(err) => {
let err = format!("An error occurred: {err:?}");
self.err = Some(err);
return;
}
};
let collected = paths.collect::>();
// clear the current state
self.clear_err();
self.path_names.clear();
for path in collected {
self.path_names.push(path.unwrap().path().to_path_buf());
}
}
fn go_up(&mut self) {
self.current_path = match self.current_path.parent() {
Some(path) => path.to_path_buf(),
None => {
self.err = Some("Cannot go up from the root directory".to_string());
return;
}
};
self.reload_path_list();
}
fn enter_dir(&mut self, dir_id: usize) {
let path = &self.path_names[dir_id];
if !path.is_dir() {
return;
}
self.current_path.clone_from(path);
self.reload_path_list();
}
fn current(&self) -> String {
self.current_path.display().to_string()
}
fn clear_err(&mut self) {
self.err = None;
}
}
================================================
FILE: examples/01-app-demos/geolocation-native-plugin/Cargo.toml
================================================
[package]
name = "geolocation-native-plugin"
version = "0.1.0"
authors = ["Sabin Regmi "]
edition = "2021"
publish = false
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
dioxus = { workspace = true, features = [] }
manganis = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
thiserror = { workspace = true }
[features]
default = ["mobile"]
web = ["dioxus/web"]
desktop = ["dioxus/desktop"]
mobile = ["dioxus/mobile"]
================================================
FILE: examples/01-app-demos/geolocation-native-plugin/Dioxus.toml
================================================
#:schema ../../../packages/cli/schema.json
[bundle]
identifier = "com.dioxuslabs.geolocation"
publisher = "Dioxus Labs"
[ios]
deployment_target = "16.2"
background_modes = ["location"]
[ios.plist]
NSSupportsLiveActivities = true
[[ios.widget_extensions]]
source = "src/ios/widget"
display_name = "Location Widget"
bundle_id_suffix = "location-widget"
deployment_target = "16.2"
module_name = "GeolocationPlugin"
[android]
min_sdk = 24
target_sdk = 34
features = ["android.hardware.location.gps"]
[permissions]
location = { precision = "fine", description = "Access your precise location to provide location-based services" }
================================================
FILE: examples/01-app-demos/geolocation-native-plugin/README.md
================================================
# Geolocation demo
A minimal Dioxus application that implements a native plugin.
The plugin demonstrated here makes it possible to access the user's geolocation. It does a few things:
- Inspect and request location permissions using the native Android/iOS dialogs.
- Configure one-shot position requests (high-accuracy toggle + maximum cached age).
- Inspect the last reported coordinates, accuracy, altitude, heading, and speed.
The example shares the same metadata pipeline as any plugin crate: the native Gradle/Swift
artifacts are embedded via linker symbols and bundled automatically by `dx`.
## Running the example
```bash
# Inside the repository root
dx serve --project examples/01-app-demos/geolocation --platform mobile
```
For Android/iOS you’ll need the respective toolchains installed (Android SDK/NDK, Xcode) so the
geolocation crate’s `build.rs` can build the native modules. The UI also works on desktop/web,
but location calls will return an error because the plugin only supports mobile targets—those
errors are shown inline in the demo.
## Things to try
1. Tap **Check permissions** to see the current OS state (granted/denied/prompt).
2. Tap **Request permissions** to trigger the native dialog from within the app.
3. Toggle *High accuracy* and set a *Max cached age* before requesting the current position.
4. Observe the coordinate grid update whenever a new reading arrives, or the error banner if the
operation fails (e.g., permissions denied or running on an unsupported platform).
================================================
FILE: examples/01-app-demos/geolocation-native-plugin/assets/main.css
================================================
body {
background-color: #05060a;
color: #f4f4f5;
font-family: 'Inter', 'Segoe UI', sans-serif;
margin: 0;
min-height: 100vh;
display: flex;
justify-content: center;
padding: calc(16px + env(safe-area-inset-top, 0px)) 0 40px;
}
.app {
width: min(960px, 100%);
padding: 0 20px;
box-sizing: border-box;
}
.hero {
display: flex;
gap: 24px;
align-items: center;
margin-bottom: 32px;
flex-wrap: wrap;
}
.hero img {
width: 200px;
max-width: 35%;
border-radius: 16px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.4);
}
.hero__copy h1 {
margin: 0 0 8px;
font-size: clamp(28px, 6vw, 36px);
}
.hero__copy p {
margin: 0;
line-height: 1.5;
color: #c8cad7;
}
.cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
gap: 24px;
margin-bottom: 16px;
}
.card {
background: linear-gradient(165deg, rgba(17, 20, 32, 0.95), rgba(6, 7, 16, 0.98));
border: 1px solid #222534;
border-radius: 16px;
padding: 24px;
box-shadow: 0 25px 45px rgba(0, 0, 0, 0.4);
}
.card h2 {
margin-top: 0;
font-size: 1.5rem;
}
.muted {
color: #a5a7b6;
font-size: 0.95rem;
}
.button-row {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-top: 16px;
}
button {
border: none;
border-radius: 999px;
padding: 10px 18px;
font-size: 0.95rem;
cursor: pointer;
transition: background 0.2s ease;
}
button.primary,
button {
background: linear-gradient(135deg, #8f63ff, #4d8dff);
color: white;
box-shadow: 0 10px 25px rgba(77, 141, 255, 0.25);
}
button.secondary {
background: transparent;
color: #b3b7cf;
border: 1px solid #2f3244;
}
button.full-width {
width: 100%;
margin-top: 16px;
}
button.toggle {
width: fit-content;
background: #1a1d29;
border: 1px solid #2c2f40;
color: #d8d9e5;
}
button.toggle--active {
background: #23304d;
border-color: #4b6cff;
color: #ffffff;
}
.settings {
display: flex;
flex-direction: column;
gap: 12px;
margin-top: 16px;
}
.field {
display: flex;
flex-direction: column;
gap: 6px;
}
.field input {
background: #0b0d13;
border: 1px solid #26293a;
border-radius: 10px;
padding: 10px 12px;
color: white;
}
.status-grid {
margin-top: 20px;
display: grid;
gap: 14px;
}
.permission-row {
display: flex;
justify-content: space-between;
align-items: center;
}
.badge {
padding: 4px 10px;
border-radius: 999px;
font-size: 0.85rem;
text-transform: uppercase;
}
.badge--granted {
background: rgba(70, 221, 154, 0.15);
color: #7efac6;
border: 1px solid rgba(70, 221, 154, 0.4);
}
.badge--denied {
background: rgba(255, 98, 98, 0.16);
color: #ff8ea0;
border: 1px solid rgba(255, 98, 98, 0.4);
}
.badge--prompt {
background: rgba(255, 205, 112, 0.16);
color: #ffd27e;
border: 1px solid rgba(255, 205, 112, 0.35);
}
.position {
margin-top: 20px;
}
.position__grid {
margin-top: 14px;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 10px;
}
.coordinate-row {
display: flex;
flex-direction: column;
gap: 2px;
padding: 10px;
background: #080a11;
border-radius: 12px;
border: 1px solid #1c1f2b;
}
.error-banner {
margin-top: 24px;
padding: 14px 18px;
background: #3c1017;
border: 1px solid #a44856;
border-radius: 12px;
color: #ffe6ea;
}
@media (max-width: 640px) {
.hero {
flex-direction: column;
text-align: center;
}
.hero img {
max-width: 60%;
}
.button-row {
flex-direction: column;
}
button {
width: 100%;
text-align: center;
}
}
================================================
FILE: examples/01-app-demos/geolocation-native-plugin/src/android/build.gradle.kts
================================================
import org.gradle.api.tasks.bundling.AbstractArchiveTask
plugins {
id("com.android.library") version "8.4.2"
kotlin("android") version "1.9.24"
}
android {
namespace = "com.dioxus.geolocation"
compileSdk = 34
defaultConfig {
minSdk = 24
targetSdk = 34
consumerProguardFiles("consumer-rules.pro")
}
buildTypes {
getByName("release") {
isMinifyEnabled = false
}
getByName("debug") {
isMinifyEnabled = false
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
}
dependencies {
implementation("androidx.core:core-ktx:1.12.0")
implementation("com.google.android.gms:play-services-location:21.3.0")
}
tasks.withType().configureEach {
archiveBaseName.set("geolocation-plugin")
}
================================================
FILE: examples/01-app-demos/geolocation-native-plugin/src/android/consumer-rules.pro
================================================
# Intentionally empty; no consumer Proguard rules required for the geolocation plugin.
================================================
FILE: examples/01-app-demos/geolocation-native-plugin/src/android/src/main/AndroidManifest.xml
================================================
================================================
FILE: examples/01-app-demos/geolocation-native-plugin/src/android/src/main/kotlin/com/dioxus/geolocation/Geolocation.kt
================================================
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
package com.dioxus.geolocation
import android.annotation.SuppressLint
import android.content.Context
import android.location.Location
import android.location.LocationManager
import android.os.SystemClock
import androidx.core.location.LocationManagerCompat
import android.util.Log
import com.google.android.gms.common.ConnectionResult
import com.google.android.gms.common.GoogleApiAvailability
import com.google.android.gms.location.LocationServices
import com.google.android.gms.location.Priority
class Geolocation(private val context: Context) {
fun isLocationServicesEnabled(): Boolean {
val lm = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
return LocationManagerCompat.isLocationEnabled(lm)
}
@SuppressWarnings("MissingPermission")
fun sendLocation(
enableHighAccuracy: Boolean,
successCallback: (location: Location) -> Unit,
errorCallback: (error: String) -> Unit,
) {
val resultCode = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context)
if (resultCode == ConnectionResult.SUCCESS) {
val lm = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
if (this.isLocationServicesEnabled()) {
var networkEnabled = false
try {
networkEnabled = lm.isProviderEnabled(LocationManager.NETWORK_PROVIDER)
} catch (_: Exception) {
Log.e("Geolocation", "isProviderEnabled failed")
}
val lowPrio =
if (networkEnabled) Priority.PRIORITY_BALANCED_POWER_ACCURACY else Priority.PRIORITY_LOW_POWER
val prio = if (enableHighAccuracy) Priority.PRIORITY_HIGH_ACCURACY else lowPrio
Log.d("Geolocation", "Using priority $prio")
LocationServices
.getFusedLocationProviderClient(context)
.getCurrentLocation(prio, null)
.addOnFailureListener { e -> e.message?.let { errorCallback(it) } }
.addOnSuccessListener { location ->
if (location == null) {
errorCallback("Location unavailable.")
} else {
successCallback(location)
}
}
} else {
errorCallback("Location disabled.")
}
} else {
errorCallback("Google Play Services unavailable.")
}
}
@SuppressLint("MissingPermission")
fun getLastLocation(maximumAge: Long): Location? {
var lastLoc: Location? = null
val lm = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
for (provider in lm.allProviders) {
val tmpLoc = lm.getLastKnownLocation(provider)
if (tmpLoc != null) {
val locationAge = SystemClock.elapsedRealtimeNanos() - tmpLoc.elapsedRealtimeNanos
val maxAgeNano = maximumAge * 1_000_000L
if (locationAge <= maxAgeNano && (lastLoc == null || lastLoc.elapsedRealtimeNanos > tmpLoc.elapsedRealtimeNanos)) {
lastLoc = tmpLoc
}
}
}
return lastLoc
}
}
================================================
FILE: examples/01-app-demos/geolocation-native-plugin/src/android/src/main/kotlin/com/dioxus/geolocation/GeolocationPlugin.kt
================================================
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
package com.dioxus.geolocation
import android.Manifest
import android.app.Activity
import android.content.pm.PackageManager
import android.location.Location
import android.os.Handler
import android.os.Looper
import android.webkit.WebView
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import org.json.JSONObject
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import java.util.Timer
import kotlin.concurrent.schedule
class GeolocationPlugin(private val activity: Activity) {
private val geolocation = Geolocation(activity)
fun checkPermissions(): Map {
val response = mutableMapOf()
val coarseStatus = ContextCompat.checkSelfPermission(activity, Manifest.permission.ACCESS_COARSE_LOCATION)
val fineStatus = ContextCompat.checkSelfPermission(activity, Manifest.permission.ACCESS_FINE_LOCATION)
response["location"] = permissionToStatus(fineStatus)
response["coarseLocation"] = permissionToStatus(coarseStatus)
return response
}
fun requestPermissions(callback: (Map) -> Unit) {
val permissionsToRequest = mutableListOf()
if (ContextCompat.checkSelfPermission(activity, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
permissionsToRequest.add(Manifest.permission.ACCESS_FINE_LOCATION)
}
if (ContextCompat.checkSelfPermission(activity, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
permissionsToRequest.add(Manifest.permission.ACCESS_COARSE_LOCATION)
}
if (permissionsToRequest.isEmpty()) {
callback(checkPermissions())
} else {
ActivityCompat.requestPermissions(activity, permissionsToRequest.toTypedArray(), 1001)
Handler(Looper.getMainLooper()).postDelayed({ callback(checkPermissions()) }, 1000)
}
}
fun getCurrentPosition(
enableHighAccuracy: Boolean,
timeout: Long,
maximumAge: Long,
successCallback: (Location) -> Unit,
errorCallback: (String) -> Unit,
) {
val lastLocation = geolocation.getLastLocation(maximumAge)
if (lastLocation != null) {
successCallback(lastLocation)
return
}
val timer = Timer()
timer.schedule(timeout) {
activity.runOnUiThread { errorCallback("Timeout waiting for location.") }
}
geolocation.sendLocation(
enableHighAccuracy,
{ location ->
timer.cancel()
successCallback(location)
},
{ error ->
timer.cancel()
errorCallback(error)
},
)
}
private fun permissionToStatus(value: Int): String =
when (value) {
PackageManager.PERMISSION_GRANTED -> "granted"
PackageManager.PERMISSION_DENIED -> "denied"
else -> "prompt"
}
// ---- Platform bridge helpers expected by Rust JNI layer ----
// Called by Rust after constructing the plugin. No-op placeholder to match signature.
fun load(webView: WebView?) { /* no-op */ }
// Serialize current permission status as JSON string
fun checkPermissionsJson(): String {
val status = checkPermissions()
val json = JSONObject()
json.put("location", status["location"]) // granted|denied|prompt
json.put("coarseLocation", status["coarseLocation"]) // granted|denied|prompt
return json.toString()
}
// Request permissions and return resulting status JSON (waits briefly for result)
fun requestPermissionsJson(permissionsJson: String?): String {
val latch = CountDownLatch(1)
var result: String = checkPermissionsJson()
requestPermissions { status ->
val json = JSONObject()
json.put("location", status["location"])
json.put("coarseLocation", status["coarseLocation"])
result = json.toString()
latch.countDown()
}
// Wait up to 5 seconds for the permission result, then return whatever we have
latch.await(5, TimeUnit.SECONDS)
return result
}
// Convert a Location to the Position JSON expected by Rust side
private fun locationToPositionJson(location: Location): String {
val coords = JSONObject()
coords.put("latitude", location.latitude)
coords.put("longitude", location.longitude)
coords.put("accuracy", location.accuracy.toDouble())
if (location.hasAltitude()) coords.put("altitude", location.altitude)
if (android.os.Build.VERSION.SDK_INT >= 26) {
val vAcc = try { location.verticalAccuracyMeters } catch (_: Exception) { null }
if (vAcc != null) coords.put("altitudeAccuracy", vAcc.toDouble())
}
if (location.hasSpeed()) coords.put("speed", location.speed.toDouble())
if (location.hasBearing()) coords.put("heading", location.bearing.toDouble())
val obj = JSONObject()
obj.put("timestamp", System.currentTimeMillis())
obj.put("coords", coords)
return obj.toString()
}
// Synchronous wrapper returning JSON for getCurrentPosition
// Accepts a JSON string with options: {"enableHighAccuracy": bool, "timeout": number, "maximumAge": number}
fun getCurrentPositionJson(optionsJson: String?): String {
val options = try {
if (optionsJson.isNullOrEmpty()) JSONObject() else JSONObject(optionsJson)
} catch (e: Exception) {
JSONObject()
}
val enableHighAccuracy = options.optBoolean("enableHighAccuracy", false)
val timeout = options.optLong("timeout", 10000L)
val maximumAge = options.optLong("maximumAge", 0L)
var output: String? = null
val latch = CountDownLatch(1)
getCurrentPosition(
enableHighAccuracy,
timeout,
maximumAge,
{ location ->
output = locationToPositionJson(location)
latch.countDown()
},
{ error ->
output = JSONObject(mapOf("error" to error)).toString()
latch.countDown()
},
)
// Wait up to the timeout + 2s buffer
latch.await(timeout + 2000, TimeUnit.MILLISECONDS)
return output ?: JSONObject(mapOf("error" to "Timeout waiting for location.")).toString()
}
}
================================================
FILE: examples/01-app-demos/geolocation-native-plugin/src/ios/.gitignore
================================================
.DS_Store
/.build
/Packages
/*.xcodeproj
xcuserdata/
DerivedData/
.swiftpm/config/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc
Package.resolved
================================================
FILE: examples/01-app-demos/geolocation-native-plugin/src/ios/plugin/Package.swift
================================================
// swift-tools-version:5.9
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
import PackageDescription
let package = Package(
name: "GeolocationPlugin",
platforms: [
.iOS(.v17), // iOS 17+ for latest ActivityKit APIs
.macOS(.v14),
],
products: [
.library(
name: "GeolocationPlugin",
type: .static,
targets: ["GeolocationPlugin"]
)
],
dependencies: [],
targets: [
.target(
name: "GeolocationPlugin",
path: "Sources",
linkerSettings: [
.linkedFramework("CoreLocation"),
.linkedFramework("Foundation"),
.linkedFramework("ActivityKit", .when(platforms: [.iOS])),
]
)
]
)
================================================
FILE: examples/01-app-demos/geolocation-native-plugin/src/ios/plugin/Sources/GeolocationPlugin.swift
================================================
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
import CoreLocation
import Foundation
import Dispatch
import ActivityKit
/**
* Simplified GeolocationPlugin for Dioxus that works without Tauri dependencies.
* This can be shared with Tauri plugins with minimal changes.
*/
@objc(GeolocationPlugin)
public class GeolocationPlugin: NSObject, CLLocationManagerDelegate {
private let locationManager = CLLocationManager()
private var positionCallbacks: [String: (String) -> Void] = [:]
override init() {
super.init()
locationManager.delegate = self
}
/**
* Get current position as JSON string (called from ObjC/Rust)
*/
@objc public func getCurrentPositionJson(_ optionsJson: String) -> String {
// Parse options from JSON
guard let optionsData = optionsJson.data(using: .utf8),
let optionsDict = try? JSONSerialization.jsonObject(with: optionsData) as? [String: Any] else {
let error = ["error": "Invalid options JSON"]
return (try? JSONSerialization.data(withJSONObject: error))?.base64EncodedString() ?? ""
}
let enableHighAccuracy = optionsDict["enableHighAccuracy"] as? Bool ?? false
let timeoutMs = optionsDict["timeout"] as? Double ?? 10000
let maximumAgeMs = optionsDict["maximumAge"] as? Double ?? 0
// If we have a recent cached location, return it immediately
if let lastLocation = self.locationManager.location {
let ageMs = Date().timeIntervalSince(lastLocation.timestamp) * 1000
if maximumAgeMs <= 0 || ageMs <= maximumAgeMs {
return self.convertLocationToJson(lastLocation)
}
}
let callbackId = UUID().uuidString
let semaphore = DispatchSemaphore(value: 0)
var responseJson: String?
self.positionCallbacks[callbackId] = { result in
responseJson = result
semaphore.signal()
}
if enableHighAccuracy {
self.locationManager.desiredAccuracy = kCLLocationAccuracyBest
} else {
self.locationManager.desiredAccuracy = kCLLocationAccuracyKilometer
}
if CLLocationManager.authorizationStatus() == .notDetermined {
self.locationManager.requestWhenInUseAuthorization()
} else {
self.locationManager.requestLocation()
}
let timeoutSeconds = max(timeoutMs / 1000.0, 0.1)
let deadline = Date().addingTimeInterval(timeoutSeconds)
while responseJson == nil && Date() < deadline {
let _ = RunLoop.current.run(mode: .default, before: Date().addingTimeInterval(0.05))
if semaphore.wait(timeout: .now()) == .success {
break
}
}
if let json = responseJson {
return json
} else {
// Timed out waiting for location
self.positionCallbacks.removeValue(forKey: callbackId)
let error = ["error": "Timeout waiting for location"]
return (try? JSONSerialization.data(withJSONObject: error)).flatMap {
String(data: $0, encoding: .utf8)
} ?? "{\"error\":\"Timeout waiting for location\"}"
}
}
/**
* Check permissions and return JSON string (called from ObjC/Rust)
*/
@objc public func checkPermissionsJson() -> String {
var status: String = ""
if CLLocationManager.locationServicesEnabled() {
switch CLLocationManager.authorizationStatus() {
case .notDetermined:
status = "prompt"
case .restricted, .denied:
status = "denied"
case .authorizedAlways, .authorizedWhenInUse:
status = "granted"
@unknown default:
status = "prompt"
}
} else {
let error = ["error": "Location services are not enabled"]
return (try? JSONSerialization.data(withJSONObject: error))?.base64EncodedString() ?? ""
}
let result: [String: String] = ["location": status, "coarseLocation": status]
if let jsonData = try? JSONSerialization.data(withJSONObject: result),
let jsonString = String(data: jsonData, encoding: .utf8) {
return jsonString
}
return ""
}
/**
* Request permissions and return JSON string (called from ObjC/Rust)
*/
@objc public func requestPermissionsJson(_ permissionsJson: String) -> String {
if CLLocationManager.locationServicesEnabled() {
if CLLocationManager.authorizationStatus() == .notDetermined {
DispatchQueue.main.async {
self.locationManager.requestWhenInUseAuthorization()
}
// Return current status - actual result comes via delegate
return self.checkPermissionsJson()
} else {
return self.checkPermissionsJson()
}
} else {
let error = ["error": "Location services are not enabled"]
if let jsonData = try? JSONSerialization.data(withJSONObject: error),
let jsonString = String(data: jsonData, encoding: .utf8) {
return jsonString
}
return ""
}
}
//
// CLLocationManagerDelegate methods
//
public func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
let errorMessage = error.localizedDescription
// Notify all position callbacks
for (_, callback) in self.positionCallbacks {
let errorJson = "{\"error\":\"\(errorMessage)\"}"
callback(errorJson)
}
self.positionCallbacks.removeAll()
}
public func locationManager(
_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]
) {
guard let location = locations.last else {
return
}
let resultJson = self.convertLocationToJson(location)
// Notify all position callbacks
for (_, callback) in self.positionCallbacks {
callback(resultJson)
}
self.positionCallbacks.removeAll()
}
public func locationManager(
_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus
) {
if !self.positionCallbacks.isEmpty {
self.locationManager.requestLocation()
}
}
//
// Internal/Helper methods
//
private func convertLocationToJson(_ location: CLLocation) -> String {
var ret: [String: Any] = [:]
var coords: [String: Any] = [:]
coords["latitude"] = location.coordinate.latitude
coords["longitude"] = location.coordinate.longitude
coords["accuracy"] = location.horizontalAccuracy
coords["altitude"] = location.altitude
coords["altitudeAccuracy"] = location.verticalAccuracy
coords["speed"] = location.speed
coords["heading"] = location.course
ret["timestamp"] = Int((location.timestamp.timeIntervalSince1970 * 1000))
ret["coords"] = coords
if let jsonData = try? JSONSerialization.data(withJSONObject: ret),
let jsonString = String(data: jsonData, encoding: .utf8) {
return jsonString
}
return "{\"error\":\"Failed to serialize location\"}"
}
//
// Live Activity methods
//
/// Start a Live Activity showing current location
/// Returns JSON with activity ID or error
@objc public func startLiveActivityJson() -> String {
if #available(iOS 16.2, *) {
// Check if Live Activities are enabled
guard ActivityAuthorizationInfo().areActivitiesEnabled else {
return "{\"error\":\"Live Activities are not enabled\"}"
}
// Get current location
guard let location = self.locationManager.location else {
return "{\"error\":\"No location available. Request location first.\"}"
}
let attributes = LocationPermissionAttributes(appName: "Geolocation Demo")
let contentState = LocationPermissionAttributes.ContentState(
latitude: location.coordinate.latitude,
longitude: location.coordinate.longitude,
accuracy: location.horizontalAccuracy,
speed: location.speed >= 0 ? location.speed : nil,
heading: location.course >= 0 ? location.course : nil,
lastUpdated: Date()
)
do {
let activity = try Activity.request(
attributes: attributes,
content: .init(state: contentState, staleDate: nil),
pushType: nil
)
let result: [String: Any] = [
"activityId": activity.id,
"latitude": location.coordinate.latitude,
"longitude": location.coordinate.longitude,
"accuracy": location.horizontalAccuracy
]
if let jsonData = try? JSONSerialization.data(withJSONObject: result),
let jsonString = String(data: jsonData, encoding: .utf8) {
return jsonString
}
return "{\"error\":\"Failed to serialize result\"}"
} catch {
return "{\"error\":\"Failed to start Live Activity: \(error.localizedDescription)\"}"
}
} else {
return "{\"error\":\"Live Activities require iOS 16.2+\"}"
}
}
/// Update the Live Activity with current location
@objc public func updateLiveActivityJson(_ statusJson: String) -> String {
if #available(iOS 16.2, *) {
// Get current location
guard let location = self.locationManager.location else {
return "{\"error\":\"No location available\"}"
}
let contentState = LocationPermissionAttributes.ContentState(
latitude: location.coordinate.latitude,
longitude: location.coordinate.longitude,
accuracy: location.horizontalAccuracy,
speed: location.speed >= 0 ? location.speed : nil,
heading: location.course >= 0 ? location.course : nil,
lastUpdated: Date()
)
// Update all running activities of this type
Task {
for activity in Activity.activities {
await activity.update(
ActivityContent(state: contentState, staleDate: nil)
)
}
}
let result: [String: Any] = [
"latitude": location.coordinate.latitude,
"longitude": location.coordinate.longitude,
"accuracy": location.horizontalAccuracy
]
if let jsonData = try? JSONSerialization.data(withJSONObject: result),
let jsonString = String(data: jsonData, encoding: .utf8) {
return jsonString
}
return "{\"error\":\"Failed to serialize result\"}"
} else {
return "{\"error\":\"Live Activities require iOS 16.2+\"}"
}
}
/// End all Live Activities
@objc public func endLiveActivityJson() -> String {
if #available(iOS 16.2, *) {
Task {
for activity in Activity.activities {
await activity.end(nil, dismissalPolicy: .immediate)
}
}
return "{\"success\":true}"
} else {
return "{\"error\":\"Live Activities require iOS 16.2+\"}"
}
}
}
================================================
FILE: examples/01-app-demos/geolocation-native-plugin/src/ios/plugin/Sources/LocationActivityAttributes.swift
================================================
// Shared ActivityAttributes for Live Activities
// This MUST be the single source of truth for both the main app and widget extension
import Foundation
import ActivityKit
/// Live Activity attributes for displaying current location.
///
/// This struct is shared between the main app (which starts/updates activities)
/// and the widget extension (which renders them on the lock screen).
public struct LocationPermissionAttributes: ActivityAttributes {
/// Dynamic content that can be updated while the activity is running
public struct ContentState: Codable, Hashable {
/// Current latitude
public var latitude: Double
/// Current longitude
public var longitude: Double
/// Horizontal accuracy in meters
public var accuracy: Double
/// Current speed in m/s (nil if not available)
public var speed: Double?
/// Current heading in degrees (nil if not available)
public var heading: Double?
/// Last update timestamp
public var lastUpdated: Date
public init(
latitude: Double,
longitude: Double,
accuracy: Double,
speed: Double? = nil,
heading: Double? = nil,
lastUpdated: Date
) {
self.latitude = latitude
self.longitude = longitude
self.accuracy = accuracy
self.speed = speed
self.heading = heading
self.lastUpdated = lastUpdated
}
}
/// Static data set when the activity is started (cannot change)
public var appName: String
public init(appName: String) {
self.appName = appName
}
}
================================================
FILE: examples/01-app-demos/geolocation-native-plugin/src/ios/plugin/Tests/PluginTests/PluginTests.swift
================================================
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
import XCTest
@testable import ExamplePlugin
final class ExamplePluginTests: XCTestCase {
func testExample() throws {
let plugin = ExamplePlugin()
}
}
================================================
FILE: examples/01-app-demos/geolocation-native-plugin/src/ios/widget/Package.swift
================================================
// swift-tools-version:5.9
// Widget Extension for displaying location permission status on lock screen
//
// IMPORTANT: The target name MUST match the main app's Swift module name.
// The plugin uses "GeolocationPlugin" as its package/target name, so the
// widget must also use "GeolocationPlugin" for ActivityKit type matching.
import PackageDescription
let package = Package(
name: "GeolocationPlugin",
platforms: [
.iOS(.v17), // iOS 17+ for latest ActivityKit APIs
],
products: [
// Executable name must be "widget" for the build system to find it
// But the TARGET name determines the Swift module name
.executable(
name: "widget",
targets: ["GeolocationPlugin"]
)
],
dependencies: [],
targets: [
// Target name = Swift module name = "GeolocationPlugin"
// This MUST match the main app's Swift plugin module name!
.executableTarget(
name: "GeolocationPlugin",
path: "Sources",
linkerSettings: [
.linkedFramework("WidgetKit"),
.linkedFramework("SwiftUI"),
.linkedFramework("ActivityKit"),
]
)
]
)
================================================
FILE: examples/01-app-demos/geolocation-native-plugin/src/ios/widget/Sources/LocationActivityAttributes.swift
================================================
// Shared ActivityAttributes for Live Activities
// This MUST be the single source of truth for both the main app and widget extension
import Foundation
import ActivityKit
/// Live Activity attributes for displaying current location.
///
/// This struct is shared between the main app (which starts/updates activities)
/// and the widget extension (which renders them on the lock screen).
public struct LocationPermissionAttributes: ActivityAttributes {
/// Dynamic content that can be updated while the activity is running
public struct ContentState: Codable, Hashable {
/// Current latitude
public var latitude: Double
/// Current longitude
public var longitude: Double
/// Horizontal accuracy in meters
public var accuracy: Double
/// Current speed in m/s (nil if not available)
public var speed: Double?
/// Current heading in degrees (nil if not available)
public var heading: Double?
/// Last update timestamp
public var lastUpdated: Date
public init(
latitude: Double,
longitude: Double,
accuracy: Double,
speed: Double? = nil,
heading: Double? = nil,
lastUpdated: Date
) {
self.latitude = latitude
self.longitude = longitude
self.accuracy = accuracy
self.speed = speed
self.heading = heading
self.lastUpdated = lastUpdated
}
}
/// Static data set when the activity is started (cannot change)
public var appName: String
public init(appName: String) {
self.appName = appName
}
}
================================================
FILE: examples/01-app-demos/geolocation-native-plugin/src/ios/widget/Sources/LocationWidget.swift
================================================
// Widget Extension for Live Activity only
// Note: Having multiple widgets in the same bundle can cause Live Activities to show black
// See: https://developer.apple.com/forums/thread/807726
import ActivityKit
import SwiftUI
import WidgetKit
@main
struct LocationWidgetBundle: WidgetBundle {
var body: some Widget {
// Only include the Live Activity - other widgets can cause rendering issues
LocationPermissionLiveActivity()
}
}
// Helper to get accuracy color
func accuracyColor(_ accuracy: Double) -> Color {
if accuracy < 10 {
return .green
} else if accuracy < 50 {
return .yellow
} else if accuracy < 100 {
return .orange
} else {
return .red
}
}
// Helper to format coordinates with degree symbol
func formatCoord(_ value: Double, isLat: Bool) -> String {
let direction = isLat ? (value >= 0 ? "N" : "S") : (value >= 0 ? "E" : "W")
return String(format: "%.5f° %@", abs(value), direction)
}
// Live Activity widget
struct LocationPermissionLiveActivity: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: LocationPermissionAttributes.self) { context in
// Lock screen view - flashy gradient design
ZStack {
// Gradient background
LinearGradient(
colors: [
Color(red: 0.1, green: 0.1, blue: 0.2),
Color(red: 0.05, green: 0.15, blue: 0.25)
],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
VStack(spacing: 12) {
// Header with pulsing indicator
HStack(spacing: 12) {
// Animated location icon with glow
ZStack {
Circle()
.fill(accuracyColor(context.state.accuracy).opacity(0.3))
.frame(width: 44, height: 44)
Circle()
.fill(accuracyColor(context.state.accuracy).opacity(0.6))
.frame(width: 32, height: 32)
Image(systemName: "location.fill")
.font(.system(size: 18, weight: .bold))
.foregroundColor(.white)
}
VStack(alignment: .leading, spacing: 2) {
Text(context.attributes.appName)
.font(.headline)
.fontWeight(.bold)
.foregroundColor(.white)
HStack(spacing: 4) {
Image(systemName: "antenna.radiowaves.left.and.right")
.font(.caption2)
Text("LIVE")
.font(.caption2)
.fontWeight(.bold)
}
.foregroundColor(accuracyColor(context.state.accuracy))
}
Spacer()
// Accuracy with animated ring
VStack(spacing: 2) {
Text("\(Int(context.state.accuracy))")
.font(.system(size: 24, weight: .bold, design: .rounded))
.foregroundColor(accuracyColor(context.state.accuracy))
.contentTransition(.numericText())
Text("meters")
.font(.caption2)
.foregroundColor(.gray)
}
.padding(.horizontal, 12)
.padding(.vertical, 8)
.background(
RoundedRectangle(cornerRadius: 12)
.fill(accuracyColor(context.state.accuracy).opacity(0.15))
.overlay(
RoundedRectangle(cornerRadius: 12)
.strokeBorder(accuracyColor(context.state.accuracy).opacity(0.5), lineWidth: 1)
)
)
}
// Coordinates in stylish cards
HStack(spacing: 8) {
// Latitude card
VStack(alignment: .leading, spacing: 4) {
HStack(spacing: 4) {
Image(systemName: "arrow.up.arrow.down")
.font(.caption2)
Text("LATITUDE")
.font(.caption2)
.fontWeight(.semibold)
}
.foregroundColor(.cyan.opacity(0.8))
Text(formatCoord(context.state.latitude, isLat: true))
.font(.system(.callout, design: .monospaced))
.fontWeight(.medium)
.foregroundColor(.white)
.contentTransition(.numericText())
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(10)
.background(
RoundedRectangle(cornerRadius: 10)
.fill(Color.cyan.opacity(0.1))
)
// Longitude card
VStack(alignment: .leading, spacing: 4) {
HStack(spacing: 4) {
Image(systemName: "arrow.left.arrow.right")
.font(.caption2)
Text("LONGITUDE")
.font(.caption2)
.fontWeight(.semibold)
}
.foregroundColor(.purple.opacity(0.8))
Text(formatCoord(context.state.longitude, isLat: false))
.font(.system(.callout, design: .monospaced))
.fontWeight(.medium)
.foregroundColor(.white)
.contentTransition(.numericText())
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(10)
.background(
RoundedRectangle(cornerRadius: 10)
.fill(Color.purple.opacity(0.1))
)
}
// Speed and heading row (if available)
if let speed = context.state.speed, speed >= 0 {
HStack(spacing: 16) {
// Speed
HStack(spacing: 6) {
Image(systemName: "speedometer")
.foregroundColor(.orange)
Text(String(format: "%.1f m/s", speed))
.font(.system(.caption, design: .monospaced))
.fontWeight(.medium)
.foregroundColor(.white)
.contentTransition(.numericText())
}
// Heading if available
if let heading = context.state.heading, heading >= 0 {
HStack(spacing: 6) {
Image(systemName: "safari")
.foregroundColor(.mint)
.rotationEffect(.degrees(heading))
Text(String(format: "%.0f°", heading))
.font(.system(.caption, design: .monospaced))
.fontWeight(.medium)
.foregroundColor(.white)
.contentTransition(.numericText())
}
}
Spacer()
// Last updated
Text(context.state.lastUpdated, style: .relative)
.font(.caption2)
.foregroundColor(.gray)
}
}
}
.padding()
}
.activitySystemActionForegroundColor(.white)
} dynamicIsland: { context in
DynamicIsland {
// Expanded regions
DynamicIslandExpandedRegion(.leading) {
VStack(alignment: .leading, spacing: 4) {
ZStack {
Circle()
.fill(accuracyColor(context.state.accuracy).opacity(0.3))
.frame(width: 36, height: 36)
Image(systemName: "location.fill")
.foregroundColor(accuracyColor(context.state.accuracy))
.font(.system(size: 16, weight: .bold))
}
Text("\(Int(context.state.accuracy))m")
.font(.caption2)
.fontWeight(.bold)
.foregroundColor(accuracyColor(context.state.accuracy))
.contentTransition(.numericText())
}
}
DynamicIslandExpandedRegion(.center) {
VStack(spacing: 6) {
// Latitude
HStack(spacing: 4) {
Text("LAT")
.font(.caption2)
.foregroundColor(.cyan.opacity(0.7))
Text(String(format: "%.5f°", context.state.latitude))
.font(.system(.caption, design: .monospaced))
.fontWeight(.semibold)
.foregroundColor(.cyan)
.contentTransition(.numericText())
}
// Longitude
HStack(spacing: 4) {
Text("LON")
.font(.caption2)
.foregroundColor(.purple.opacity(0.7))
Text(String(format: "%.5f°", context.state.longitude))
.font(.system(.caption, design: .monospaced))
.fontWeight(.semibold)
.foregroundColor(.purple)
.contentTransition(.numericText())
}
}
}
DynamicIslandExpandedRegion(.trailing) {
if let speed = context.state.speed, speed >= 0 {
VStack(alignment: .trailing, spacing: 4) {
Image(systemName: "speedometer")
.foregroundColor(.orange)
.font(.caption)
Text(String(format: "%.1f", speed))
.font(.system(.caption, design: .rounded))
.fontWeight(.bold)
.foregroundColor(.white)
.contentTransition(.numericText())
Text("m/s")
.font(.caption2)
.foregroundColor(.gray)
}
} else {
// Show heading compass if no speed
if let heading = context.state.heading, heading >= 0 {
VStack(spacing: 2) {
Image(systemName: "safari")
.font(.title3)
.foregroundColor(.mint)
.rotationEffect(.degrees(heading))
Text(String(format: "%.0f°", heading))
.font(.caption2)
.foregroundColor(.white)
}
}
}
}
DynamicIslandExpandedRegion(.bottom) {
HStack {
Text(context.attributes.appName)
.font(.caption2)
.foregroundColor(.gray)
Spacer()
Text(context.state.lastUpdated, style: .relative)
.font(.caption2)
.foregroundColor(.gray)
}
}
} compactLeading: {
ZStack {
Circle()
.fill(accuracyColor(context.state.accuracy).opacity(0.3))
.frame(width: 24, height: 24)
Image(systemName: "location.fill")
.foregroundColor(accuracyColor(context.state.accuracy))
.font(.caption)
}
} compactTrailing: {
Text(String(format: "%.4f°", context.state.latitude))
.font(.system(.caption2, design: .monospaced))
.fontWeight(.medium)
.foregroundColor(.cyan)
.contentTransition(.numericText())
} minimal: {
ZStack {
Circle()
.fill(accuracyColor(context.state.accuracy).opacity(0.3))
.frame(width: 22, height: 22)
Image(systemName: "location.fill")
.foregroundColor(accuracyColor(context.state.accuracy))
.font(.system(size: 10, weight: .bold))
}
}
}
}
}
================================================
FILE: examples/01-app-demos/geolocation-native-plugin/src/main.rs
================================================
//! A simple Dioxus app demonstrating how to build a native plugin using manganis.
//!
//! This example shows how to use the `#[manganis::ffi]` macro to automatically generate
//! FFI bindings between Rust and native platforms (Swift/Kotlin).
//!
//! It also demonstrates how to use the widget!() macro to bundle a Widget Extension
//! for Live Activities on iOS.
use dioxus::prelude::*;
// Import the local plugin module
mod plugin;
#[cfg(target_os = "ios")]
use plugin::LiveActivityResult;
use plugin::{Geolocation, PermissionState, PermissionStatus, Position, PositionOptions};
fn main() {
dioxus::launch(App);
}
#[component]
fn App() -> Element {
let mut geolocation = use_signal(Geolocation::new);
let mut permission_status = use_signal(|| None::);
let mut last_position = use_signal(|| None::);
let mut error = use_signal(|| None::);
let mut use_high_accuracy = use_signal(|| true);
let mut max_age_input = use_signal(|| String::from("0"));
let on_check_permissions = {
move |_| match geolocation.write().check_permissions() {
Ok(status) => {
permission_status.set(Some(status));
error.set(None);
}
Err(err) => error.set(Some(err.to_string())),
}
};
let on_request_permissions = move |_| {
let mut geo = geolocation.write();
match geo.request_permissions(None) {
Ok(_) => match geo.check_permissions() {
Ok(status) => {
permission_status.set(Some(status));
error.set(None);
}
Err(err) => error.set(Some(err.to_string())),
},
Err(err) => error.set(Some(err.to_string())),
}
};
let on_toggle_accuracy = move |_| use_high_accuracy.toggle();
let on_max_age_input = move |evt: FormEvent| max_age_input.set(evt.value());
let on_fetch_position = move |_| {
let maximum_age = max_age_input.read().trim().parse::().unwrap_or(0);
let options = PositionOptions {
enable_high_accuracy: use_high_accuracy(),
timeout: 10_000,
maximum_age,
};
match geolocation.write().get_current_position(Some(options)) {
Ok(position) => {
last_position.set(Some(position));
error.set(None);
}
Err(err) => error.set(Some(err.to_string())),
}
};
let accuracy_label = if use_high_accuracy() {
"High accuracy: on"
} else {
"High accuracy: off"
};
rsx! {
Stylesheet { href: asset!("/assets/main.css") }
main { class: "app",
header { class: "hero",
div { class: "hero__copy",
h1 { "Geolocation plugin demo" }
p { "One-shot location fetching through the Dioxus geolocation plugin.
Measure permissions, request access, and inspect the last fix received from the device." }
}
}
div { class: "cards",
section { class: "card",
h2 { "Permissions" }
p { class: "muted",
"First, inspect what the OS currently allows this app to do. \
On Android & iOS these calls talk to the native permission dialog APIs." }
div { class: "button-row",
button { onclick: on_check_permissions, "Check permissions" }
button { class: "secondary", onclick: on_request_permissions, "Request permissions" }
}
match permission_status() {
Some(status) => rsx! {
div { class: "status-grid",
PermissionBadge { label: "Location".to_string(), state: status.location }
PermissionBadge { label: "Coarse location".to_string(), state: status.coarse_location }
}
},
None => rsx!(p { class: "muted", "Tap “Check permissions” to see the current status." }),
}
}
section { class: "card",
h2 { "Current position" }
p { class: "muted",
"The plugin resolves the device location once per request (no background watch). \
Configure the query and then fetch the coordinates." }
div { class: "settings",
button {
class: if use_high_accuracy() { "toggle toggle--active" } else { "toggle" },
onclick: on_toggle_accuracy,
"{accuracy_label}"
}
label { class: "field",
span { "Max cached age (ms)" }
input {
r#type: "number",
inputmode: "numeric",
min: "0",
placeholder: "0",
value: "{max_age_input()}",
oninput: on_max_age_input,
}
}
}
button { class: "primary full-width", onclick: on_fetch_position, "Get current position" }
match last_position() {
Some(position) => {
let snapshot = position.clone();
let coords = snapshot.coords.clone();
rsx! {
div { class: "position",
h3 { "Latest reading" }
p { class: "muted", "Timestamp: {snapshot.timestamp} ms since Unix epoch" }
div { class: "position__grid",
CoordinateRow { label: "Latitude".to_string(), value: format!("{:.6}", coords.latitude) }
CoordinateRow { label: "Longitude".to_string(), value: format!("{:.6}", coords.longitude) }
CoordinateRow { label: "Accuracy (m)".to_string(), value: format!("{:.1}", coords.accuracy) }
CoordinateRow { label: "Altitude (m)".to_string(), value: format_optional(coords.altitude) }
CoordinateRow { label: "Altitude accuracy (m)".to_string(), value: format_optional(coords.altitude_accuracy) }
CoordinateRow { label: "Speed (m/s)".to_string(), value: format_optional(coords.speed) }
CoordinateRow { label: "Heading (°)".to_string(), value: format_optional(coords.heading) }
}
}
}
}
None => rsx!(p { class: "muted", "No location fetched yet." }),
}
}
// Live Activity card (iOS only)
LiveActivityCard { geolocation, error }
}
if let Some(message) = error() {
div { class: "error-banner", "Last error: {message}" }
}
}
}
}
#[component]
fn PermissionBadge(label: String, state: PermissionState) -> Element {
let (text, class) = match state {
PermissionState::Granted => ("Granted", "badge badge--granted"),
PermissionState::Denied => ("Denied", "badge badge--denied"),
PermissionState::Prompt | PermissionState::PromptWithRationale => {
("Needs prompt", "badge badge--prompt")
}
};
rsx! {
div { class: "permission-row",
span { class: "muted", "{label}" }
span { class: class, "{text}" }
}
}
}
#[component]
fn CoordinateRow(label: String, value: String) -> Element {
rsx! {
div { class: "coordinate-row",
span { class: "muted", "{label}" }
strong { "{value}" }
}
}
}
fn format_optional(value: Option) -> String {
value
.map(|inner| format!("{inner:.2}"))
.unwrap_or_else(|| "—".to_string())
}
#[cfg(target_os = "ios")]
#[component]
fn LiveActivityCard(
mut geolocation: Signal,
mut error: Signal