HTML elements

All 113 HTML elements are one Rust type with a constructor per tag. Here's what each kind draws like, and the methods every element shares.

  • Rust

Every HTML tag is here: div(), section(), table(), details(), dialog(), all of them. That means you can build a Rust UI out of standard HTML elements, and it lays out and draws the way the HTML and CSS specs say.

A dark Blender-style window built from HTML elements: a sidebar of links, a heading, cards, a form, a striped table, nested lists, details sections and a code block
Headings, lists, a table, details and form controls, all plain HTML elements (examples/showcase.rs).

The trick that keeps it simple: there’s one element type, Div. A tag only changes an element’s defaults: what the built-in stylesheet gives it, and whether it becomes a box, a widget or an image.

One type, every tag

There’s a constructor for every element of the WHATWG HTML index: 113 elements, plus svg() and math() as plain boxes. In rsx!, the tag name calls the constructor: <section> is section().

use blenderui::prelude::*;
fn page() -> Div {
article()
.child(h1().child("Render settings"))
.child(p().child("Samples, ").child(b().child("denoising")).child(", light paths."))
.child(ul().child(li().child("Cycles")).child(li().child("EEVEE")))
}

Because every constructor returns a Div, everything on this page and on the CSS page works on every element.

What each tag draws like

Kind Elements Drawn as
Box div, p, h1-h6, a, span, ul / ol / li, table …, details, dialog, form … Laid out and painted from CSS
Widget button, input, select, textarea, progress, meter, plus option, optgroup, datalist Form controls: browser-style, or Blender’s own widgets (the three looks)
Replaced img, video, audio, iframe, embed, object, canvas An intrinsic size; img shows PNG files or RGBA pixels
Metadata head, title, style, script, meta, link, template … Not drawn (<style> and <title> are read)
Break br, wbr Line breaks

Nine of the 113 are widgets. The full list, straight from html.rs:

  • Box: html, body, article, section, nav, aside, h1, h2, h3, h4, h5, h6, hgroup, header, footer, address, p, hr, pre, blockquote, ol, ul, menu, li, dl, dt, dd, figure, figcaption, main, search, div, a, em, strong, small, s, cite, q, dfn, abbr, ruby, rt, rp, data, time, code, var, samp, kbd, sub, sup, i, b, u, mark, bdi, bdo, span, ins, del, picture, table, caption, colgroup, col, tbody, thead, tfoot, tr, td, th, form, label, fieldset, legend, details, summary, dialog, noscript, slot, output, map, selectedcontent (and the svg, math roots)
  • Widget: button, input, select, option, optgroup, textarea, progress, meter, datalist
  • Replaced: img, iframe, embed, object, video, audio, and canvas
  • Metadata: head, title, base, link, meta, style, script, template, source, track, area
  • Break: br, wbr

Custom elements

Unknown names work too, like custom elements in a browser. custom("my-widget") or Div::new("my-widget") makes an element with no built-in styles, laid out inline like a <span> until your CSS says otherwise. In rsx!, any tag with a dash does the same: <my-widget class="block h-1" />.

The document

Your view’s root is wrapped in <html><body> unless it already is one, so body { margin: 0 } and the browser defaults behave as you’d expect. Return html() with a head() when you want a stylesheet and a title up front:

html()
.child(head().child(title().child("BUI showcase")).child(style().child(CSS)))
.child(body().child(/* your page */))

A <title> element sets the window title.

Methods every element has

Method What it does
.child(x), .children(iter) Add children: elements, components or text
.id(..) An id attribute and an identity that keeps the element’s state across renders
.key(..) Identity without the id attribute: keeps state, but #id selectors don’t see it
.class(..) Add classes (adds to any already set)
.attr(name, value) Set any HTML attribute
.styled() Give this subtree Blender’s look (the three looks)
.href, .value, .placeholder, .input_type, .name, .title, .src, .alt Typed attributes that take text
.checked, .disabled, .selected, .open Boolean attributes: true sets them, false removes them
.min, .max, .step Number attributes (f64)
.colspan Table cell spans (u32)

Text children are &'static str, String or SharedString.

FluentBuilder adds conditional building, gpui-style:

div()
.when(selected, |el| el.class("active"))
.when_some(tooltip, |el, t| el.title(t))
.when_none(&icon, |el| el.class("no-icon"))
.map(|el| if compact { el.p_1() } else { el.p_4() })

Identity and state

Every render builds a fresh tree, so how does a text field remember what you typed? Give it an .id(...). Elements with an id keep their state across renders: scroll position, a text field’s contents, a checkbox you clicked, which <details> are open. Use .key(...) instead when you want the same memory without an id attribute that CSS can match.

Lists

ul and ol get bullets and numbers from list-style-type: disc, circle, square, decimal, lower-alpha, upper-roman and more. Nested lists switch bullets as browsers do (disc, then circle, then square). list-style-position, list-style-image and <ol start> work too.

ol().attr("start", "3")
.child(li().child("Third"))
.child(li().child("Fourth"))

Details and dialogs

  • <details> / <summary>: clicking the summary toggles the details. .open(true) sets the starting state, and with an .id(...) the open state is remembered across renders.
  • <dialog> is hidden until you give it .open(true). It then shows as an absolutely positioned box above the page.
details().id("render").open(true)
.child(summary().child("Render settings"))
.child(p().child("Samples, denoising, light paths."))

Inline text

Text is broken into words that wrap in line boxes, like a browser. Inline elements (b, i, a, code, mark, small, sub, sup …) style their own words, and inline-blocks and widgets sit on the line with the text. The layout page covers white-space, text-align and friends.

Two things differ from a browser: inline boxes paint their background per word, and borders and padding on inline elements such as <kbd> and <code> aren’t drawn.

Tables

An Export Animation page in plain HTML with a browser's default look: a form with a text input, select, number input, range slider and radio buttons, and a table belowAn Export Animation page in plain HTML with a browser's default look: a form with a text input, select, number input, range slider and radio buttons, and a table below
A form and a table from plain HTML elements, with no stylesheet beyond a few margins (examples/site_plain_html.rs).

table, thead, tbody, tfoot, tr, td, th, caption, colgroup and col all work, with colspan, border-spacing, border-collapse, caption-side and empty-cells. Columns size to their content and line up across rows. Layout has the details.

table()
.child(thead().child(tr().child(th().child("Object")).child(th().child("Vertices"))))
.child(tbody().children([("Cube", "8"), ("Suzanne", "507")]
.map(|(o, v)| tr().child(td().child(o)).child(td().child(v)))))

Images

  • From a file: img().src("file.png"). PNGs are decoded once and cached.
  • From pixels: img().image_rgba(width, height, rgba), with width * height * 4 bytes, row-major, straight alpha.
  • Shared pixels: img().image(Arc<Image>) is cheap to pass every frame. Image::from_png(path) decodes a file into one.

Images are PNG only and scale with nearest-neighbour sampling. CSS object-fit, object-position and background-image: url(...) work too (CSS).

video, audio and iframe show a placeholder of their intrinsic size; there’s no media playback or embedded browsing Coming soon.

To draw your own shapes, use canvas(...): see painting your own content.

Next steps

Edit this page on GitHub

Last updated