Browser look, styled and Blender components

Make any part of your Rust UI look like a web page or like Blender, and use Blender's real widgets as components.

  • Rust

Some apps should look like a web page. Some should look like Blender. Most want a bit of both: a clean document with a few Blender sliders in it. In BUI you choose the look per element, and the choice flows down to everything inside.

Three rows of the same form controls. Top, on white: browser-style button, text field, number field, checkbox, radio, slider, dropdown, progress bars, color picker and textarea. Middle, on dark grey: the same controls as Blender widgets. Bottom: Blender components for a button, text field, number slider and checkbox.
The same controls three ways, from examples/forms.rs (cargo run --example forms).

The three looks

  1. Plain HTML looks like a browser draws it: black text on white, 16 px, #0000ee underlined links, and a web browser’s form controls, all in Blender’s UI font, Inter.
  2. Inside styled, an element and everything in it get Blender’s look: the #303030 background, #e6e6e6 13 px text, blue links, dark code and pre, themed tables, fieldsets and dialogs, and Blender’s widgets for the form controls.
  3. Blender components (Button, NumberSlider, ColorSwatch …) are Blender’s widgets always, inside styled or not.

Here’s how forms.rs builds the screenshot above. One function makes the controls; the page shows it twice and then adds the components. The example is written with gpui builders; the rsx! tab is the same window in markup.

src/main.rs
use blenderui::prelude::*;
fn controls() -> impl IntoElement {
rsx! {
<div flex gap-2 items-center p-3>
<button>"Button"</button>
<input value="Text field" />
<input type="number" value="42" />
<input type="range" min={0.0} max={10.0} value="6" />
</div>
}
}
struct Forms;
impl Render for Forms {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
rsx! {
<body m-0 flex flex-col>
<h3>"Plain HTML (browser look)"</h3>
{controls()}
<div styled>
<h3>"Inside styled (Blender look)"</h3>
{controls()}
</div>
<h3>"Blender components"</h3>
<div flex gap-2 p-3>
<Button>"Button"</Button>
<TextField value="Text" />
<NumberSlider min={0.0} max={10.0} value="6" />
<Checkbox checked={true} />
</div>
</body>
}
}
}
fn main() {
Application::new().run(|cx| {
cx.open_window(WindowOptions::default(), |_, cx| cx.new(|_| Forms)).unwrap();
});
}

Blender’s look with styled

Mark an element styled and it, and everything inside it, switches to Blender’s built-in stylesheet:

rsx! { <div styled class="p-4">"Blender look"</div> }
rsx! { <div styled><div styled="false">"Browser look again"</div></div> } // opt a subtree back out
  • Your own CSS still wins over either set of defaults. styled only swaps the user-agent stylesheet underneath.
  • styled="false" turns it back off for a subtree.
  • What changes: besides the background, text color and size, styled gives pre a dark padded block, blockquote a blue left bar, kbd a border, mark Blender’s orange, thead a darker row, fieldset and dialog Blender’s greys and rounded corners, and hr a thin grey line.

Want the whole window in Blender’s look? Put styled on the root element, as the showcase example does with html().styled().

An export form and a table with a browser's default lookAn export form and a table with a browser's default look
examples/site_plain_html.rs

The same form and table in Blender's theme, with Blender's widgetsThe same form and table in Blender's theme, with Blender's widgets
examples/site_styled.rs: the same markup

Blender components

These ten components are Blender’s widgets, wherever you put them:

Component What you get Same element as
Button Blender’s push button button()
Checkbox Blender’s checkbox input type="checkbox"
Radio Blender’s radio button, grouped by name input type="radio"
NumberField A number field: drag to change, click to step, double-click to type input type="number"
NumberSlider A number slider: the field fills with the value input type="range"
TextField A single-line text field input type="text"
TextArea A multi-line text editor textarea()
ColorSwatch A color swatch that opens Blender’s color picker input type="color"
Dropdown A dropdown menu; options are option() children select()
ProgressBar A progress bar (value, max) progress()

Each one is the element it stands for, so every element method and attribute works on it: min, max, step, value, checked, placeholder, disabled, on_change, on_click, styling and so on.

rsx! {
<NumberSlider min={0.0} max={10.0} value="2.5" onChange={|e: &ChangeEvent| println!("{:?}", e.number)} />
<Button onClick={|| println!("apply")}>"Apply"</Button>
<Dropdown><option>"Cube"</option><option>"Sphere"</option></Dropdown>
}
Camera properties built from Blender components: NumberField, NumberSlider, Dropdown, Checkbox, TextField, ColorSwatch, ProgressBar and ButtonCamera properties built from Blender components: NumberField, NumberSlider, Dropdown, Checkbox, TextField, ColorSwatch, ProgressBar and Button
Blender components from Rust (examples/site_components.rs).

Form controls

Outside styled, form elements look and behave like a web browser’s: its colors and shapes, typed number fields with spin buttons, a range thumb that jumps to the click. Inside styled, and as Blender components, they’re Blender’s widgets with Blender’s interaction rules: drag a number to change it, double-click to type (number fields also take math such as 2*pi), Ctrl+Z to undo, Esc to cancel.

HTML Blender widget What ChangeEvent carries
button() with text, or input() of type submit, button or reset Button (use on_click)
input(): text, password (masked), email, search, url, tel, date … Text field value
input().input_type("number") Number field number, value
input().input_type("range") Slider number, value
input().input_type("checkbox") Checkbox checked
input().input_type("radio"), grouped by name Radio button checked
input().input_type("color") Color swatch (opens Blender’s picker) value as #rrggbb
select() with option() / optgroup() Dropdown menu number (the index), value (the text)
textarea() Multi-line text editor value
progress(), meter() Progress bar (display only)

Reading and controlling values

on_input and on_change both fire when the value changes. Each gets a ChangeEvent { value, checked, number }:

rsx! {
<input id="vol" type="range" min={0.0} max={100.0} value={format!("{}", self.volume)}
onInput={cx.listener(|this, e: &ChangeEvent, _, cx| {
this.volume = e.number;
cx.notify();
})} />
}
  • Values you pass are pushed to the widget only when they change between renders. A field the user is typing in isn’t reset under their fingers.
  • To control a field, keep its value in your state and update it from on_change, as above. Give it an .id(...) so it keeps its identity across renders.
  • A button() holding elements other than text is laid out as an ordinary box (style it with CSS) and still gets on_click.

Next steps

Edit this page on GitHub

Last updated