Getting started
From an empty crate to a laid-out page
This page adds the crate, writes a page with HTML and CSS, lays it out and clicks it. Today the result is a list of boxes rather than a window, because the HTML path does not draw on screen yet (#43).
Add the crate
Add the crate from crates.io. 0.0.1 has both paths and depends on compose-rust 0.0.1; the 0.0.x releases are early and the API will change:
cargo add dioxus-compose@0.0.1
cargo add dioxus-hooks@0.7 dioxus-signals@0.7
dioxus-hooks and dioxus-signals give you use_signal and
the signal traits, the way the examples use them. The first build also fetches the renderer,
through compose-rust's build script.
The widget path draws with it; HTML screens do not use it yet.
Write a page
A page is a component and a stylesheet. The examples keep both in one file and export them
as app and STYLE, so a window can show the page and a test can lay
it out without one.
use dioxus_compose::html::prelude::*;
use dioxus_hooks::use_signal;
pub const STYLE: &str = r#"
body { margin: 0; font-family: system-ui, sans-serif; font-size: 16px; line-height: 1.5; }
.page { padding: 40px 48px; }
h1 { margin: 0 0 16px; font-size: 32px; line-height: 40px; }
p { margin: 12px 0; }
"#;
pub fn app() -> Element {
let mut hellos = use_signal(|| 0u32);
let status = match hellos() {
0 => "Nobody has said hello yet".to_string(),
1 => "You said hello once".to_string(),
n => format!("You said hello {n} times"),
};
rsx! {
main { class: "page",
h1 { id: "title", "Hello, Dioxus" }
p { id: "action",
a {
id: "greet",
href: "#",
onclick: move |event| {
event.prevent_default();
hellos += 1;
},
"Say hello"
}
}
p { id: "status", "{status}" }
}
}
}
The elements are Dioxus's own HTML elements, the same ones a web or desktop Dioxus app uses.
The attribute is class, events are onclick and
oninput, and event.prevent_default() works as it does on the web.
Lay it out
HtmlDom runs the app and lays it out at a viewport size in CSS pixels and a
scale in device pixels per CSS pixel. The display list it returns has one entry per box, in
paint order.
fn main() {
let mut dom = HtmlDom::with_config(app, HtmlConfig {
stylesheets: vec![STYLE.to_string()],
..HtmlConfig::default()
});
let list = dom.layout(800.0, 600.0, 1.0);
for entry in &list.entries {
println!("{:<6} {:?}", entry.tag, entry.rect);
}
}
cargo run prints the boxes and exits. No window opens: drawing them is the
bridge that is not built yet. HtmlConfig also takes a text measurer, a colour
scheme, a base URL and an image resolver, which the following pages cover.
Click it
A click at a point goes to the handler of the element under it, the way a pointer event from the renderer will. This is the hello example's own test, trimmed. The numbers come from its stylesheet and a test measurer that makes every character half the font size wide.
let mut dom = HtmlDom::with_config(hello::app, config(hello::STYLE, Measurer::new()));
dom.layout(800.0, 600.0, 1.0);
let run = &entry(&dom, "action").texts[0];
assert_eq!(run.text, "Say hello");
assert_rect(run.rect, 48.0, 132.0, 72.0, 24.0, "the link's text");
let (x, y) = centre(run.rect);
assert_eq!(dom.click(x, y), Some(node(&dom, "greet")));
dom.render();
dom.layout(800.0, 600.0, 1.0);
assert_eq!(text_of(entry(&dom, "status")), "You said hello once");
Handlers run during click. render applies what they changed to the
document, and the next layout shows it.
Run the examples
Eleven examples in samples/ are written this way, one crate each, and the rest of this guide quotes them. Their tests check every layout against numbers worked out by hand, and click, type and submit through the same calls a renderer makes. In a checkout of the repository:
cargo test -p sample-html-hello
cargo test -p sample-html-hello -p sample-html-chat -p sample-html-todo
| Example | What it shows | Page |
|---|---|---|
hello | A heading, inline bold and italic, a link that counts its clicks | Text |
article | Headings, wrapping paragraphs, lists, a quotation, a code block | Text |
dashboard | A top bar, a sidebar and wrapping metric cards, all flexbox | Layout |
gallery | A CSS grid of photos under a gradient header | Images |
settings | A form: text field, text area, select, checkbox, radio group | Forms |
table | An HTML table that scrolls sideways; the amount header sorts | Layout |
chat | A scrolling conversation above a composer pinned to the bottom | Scrolling |
todo | Add, tick, delete and reorder keyed rows | Forms |
overlay | A dropdown menu, a dialog over a fixed backdrop, a clipped preview | Overlays |
theme | Colours as CSS custom properties, a dark set, an override button | Theming |
workbench | A small code editor: activity bar, file tree, tabs, status bar | Layout |
The widget path, which draws today
An app written with Compose widget names runs in a window now, with the same dependency.
dioxus_compose::launch(app) opens the window, and cargo run builds
and starts it. The renderer comes from compose-rust, whose build script downloads the one
for your target. See Widgets in rsx! for how those apps are
written, and compose-rust/samples/ for examples.
Working on this repository
Everything here is Rust, so the tests are one command:
cargo test --workspace
./scripts/check.sh is the full gate: formatting, Clippy with warnings denied, the
tests and the benchmarks. The renderer, its build and the design systems live in
compose-rust, and its README covers
working on them.