dioxus-compose guide

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:

shell
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.

src/main.rs: the page is samples/hello, trimmed
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.

src/main.rs, continued
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.

samples/hello/tests/hello.rs, trimmed
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:

shell
cargo test -p sample-html-hello
cargo test -p sample-html-hello -p sample-html-chat -p sample-html-todo
ExampleWhat it showsPage
helloA heading, inline bold and italic, a link that counts its clicksText
articleHeadings, wrapping paragraphs, lists, a quotation, a code blockText
dashboardA top bar, a sidebar and wrapping metric cards, all flexboxLayout
galleryA CSS grid of photos under a gradient headerImages
settingsA form: text field, text area, select, checkbox, radio groupForms
tableAn HTML table that scrolls sideways; the amount header sortsLayout
chatA scrolling conversation above a composer pinned to the bottomScrolling
todoAdd, tick, delete and reorder keyed rowsForms
overlayA dropdown menu, a dialog over a fixed backdrop, a clipped previewOverlays
themeColours as CSS custom properties, a dark set, an override buttonTheming
workbenchA small code editor: activity bar, file tree, tabs, status barLayout

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:

shell
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.