Editor panels
Give a component its own section of the paywall editor's right panel, written in Rust next to the component.
An editor panel is a component's section of the paywall editor's right panel. It says which controls to show and what they change. The editor draws them with its own fields, sliders and color pickers, so the panel looks built in.
A panel is optional. Without one, the editor generates a Props section from the component's
props: one control per prop, grouped by group, with variable binding and
translations where allowed. Write a panel to add tabs, show settings only when they apply, or reset
several props at once.
A panel replaces only Props. The sections from the component's traits stay, as do Component, with its version and preview states, and Component actions, where events are bound to actions.
Add a panel
A panel lives in the component's crate, usually in src/panel.rs, which voidhash new creates.
In an existing crate, add the runtime and panel features first; see
One crate, two modules. Declare the panel with
#[panel], naming the component's id:
use voidhash_ui::panel::prelude::*;
/// The badge's label and accent.
#[panel(for = "project/badge", title = "Badge", order = 13, multi_selectable)]
pub fn settings(_cx: &mut PanelCx) -> Panel {
panel().child(
section("Badge")
.child(field("Label").child(text_field().placeholder("New").bind(bind::prop("label"))))
.child(field("Accent").child(color_field().format("hex").bind(bind::prop("accent")))),
)
}Then list it after the component, once per crate, in src/lib.rs:
mod panel;
// `#[component]` on `fn Badge` generates the `BADGE` constant;
// `#[panel]` on `fn settings` generates `SETTINGS`.
voidhash_ui::export_panels!(BADGE => panel::SETTINGS);export_panels! declares an editor panel in the definition, so set "panel": true in
component.json. A crate lists up to eight panels, shown in that order.
Panel options
| Option | Meaning |
|---|---|
for | The component's own id; another id doesn't compile. |
title | The panel's name in editor messages, such as an error note. |
id | Unique in the crate. Defaults to the function name in camelCase. |
tab | Its right panel tab: "design" (default) or "interaction". |
order | Its place among its tab's sections. Props is 13; the default 100 is last. |
multi_selectable | Show it while several instances are selected. |
style(…) | The style keys it edits. See Edit style. |
Build the panel
A panel function returns a tree starting with panel(). Its children are usually sections of
fields, each a label with one or more controls.
| Builder | Draws |
|---|---|
section(title) | A titled section; .collapsible(true) folds. |
subsection(title), untitled_subsection() | A group inside a section. |
field(label) | A labelled row of controls. |
row(), column() | Controls side by side or stacked, with .gap("sm"). |
text(content), callout(message) | Static text, or a banner with a .tone(…). |
text_field(), number_field() | A text input, or a scrubbable number input. |
select_field(), toggle_group() | A dropdown, or a segmented control, of .options(…). |
switch_field(), checkbox_field(label) | An on/off switch or a checkbox. |
slider_field(), color_field() | A range slider, or a color input with picker. |
product_field(), image_field() | A product or image picker. |
button(label), menu() | A button, or a dropdown menu of .items(…). |
list_editor(label) | A list with add and remove. See Lists. |
popover(), popover_trigger(), popover_content() | A popover. See Popovers. |
Number fields take .icon(…), .min(…), .max(…) and .step(…). Icons name the editor's own
set, such as square, percent or betweenHorizontalStart; an unknown one draws nothing.
Popovers
A popover holds a trigger and content, keeping rarely changed settings out of the way:
popover()
.child(popover_trigger().child(button("Row style").icon("settings").variant("outline").size("sm")))
.child(
popover_content()
.align("start")
.side("left")
.title("Rows")
.child(
column()
.gap("sm")
.child(number_field().icon("square").bind(bind::prop("rowPadding")))
.child(number_field().icon("squareRoundCorner").bind(bind::prop("rowRadius"))),
),
)Bound fields
Bind a control to a prop with .bind(bind::prop("label")) and the editor does the editing. It
shows the selected instances' value, or Mixed when they disagree, and writes changes live, one undo
step per gesture. Your panel isn't called meanwhile, so bound fields stay fast. Text, number,
select, toggle group, switch, checkbox, slider and color controls can be bound. Hints shape the
value:
| Hint | Effect |
|---|---|
.range(min, max) | Clamps typed numbers. .min(…) and .max(…) set one end. |
.decimals(digits) | Rounds the shown number. |
.scale(factor) | Shows the number times factor: 100 shows 0–1 as a percentage. |
.fallback(value) | The value when an instance has none. |
.label(text) | Names the undo step. |
.field(name) | Binds one field of an object prop. |
Color props store hex, so bind color fields with .format("hex"). Enum props can share the
component's Rust type: enum_options lists its options for a toggle group or select, and read
reads the first selected instance's value as that type:
use voidhash_ui::panel::prelude::*;
use crate::Shape; // #[derive(PropEnum)] in src/lib.rs
#[panel(for = "project/badge", title = "Badge", order = 13, multi_selectable)]
pub fn settings(cx: &mut PanelCx) -> Panel {
let mut section = section("Badge").child(
field("Shape").child(
toggle_group()
.width("full")
.options(enum_options::<Shape>())
.bind(bind::prop("shape")),
),
);
// Only pills have padding to set.
if cx.prop("shape").read::<Shape>() == Shape::Pill {
section = section.child(
field("Padding").child(
number_field()
.icon("square")
.bind(bind::prop("padding").range(0.0, 32.0).decimals(0)),
),
);
}
panel().child(section)
}Read the selection
The panel function receives a PanelCx describing the selected instances, and runs again when
they change.
cx.prop(name)is a prop across the selection:.value(),.mixed()when instances disagree,.is_default(),.binding()for a variable binding, and.translated().cx.count()andcx.selection()give the selected instances and their props.cx.locale()andcx.state_override()give the locale and state being edited.
Unbound controls take handlers, and a panel can keep its own state, such as an open tab. See Panel handlers and state.
Edit style
A panel can edit the style its component's traits bring: list the keys in style(…) and bind
fields with bind::style. This one edits appearance opacity as a percentage:
#[panel(for = "project/feature-list", title = "Feature list", order = 13, style("opacity"))]
pub fn settings(_cx: &mut PanelCx) -> Panel {
panel().child(
section("Feature list").child(
field("Opacity").child(
number_field().icon("percent").bind(
bind::style("opacity").scale(100.0).range(0.0, 100.0).decimals(0).fallback(1.0),
),
),
),
)
}Style writes go to the state being edited, if any. A build refuses a panel listing keys of traits its component doesn't declare.
Test a panel
voidhash_ui::panel::testing runs a panel in cargo test: mount it with a JSON context, render
its tree, and fire handlers to see what they record:
#[cfg(test)]
mod tests {
extern crate std;
use super::*;
use voidhash_ui::panel::testing::{Harness, find, is, json};
const CONTEXT: &str = r#"{
"selection": [{ "id": "a", "type": "component", "definition": "project/feature-list" }],
"props": { "features": { "value": ["Unlimited projects"], "mixed": false, "isDefault": false } }
}"#;
#[test]
fn adds_a_feature() {
let mut harness = Harness::mount(&SETTINGS, CONTEXT);
let tree = harness.render();
let list = find(&tree, |node| is(node, "listEditor", "label", "Features")).unwrap();
assert_eq!(
harness.fire(list, "onAdd", "[]"),
json(
r#"[{ "type": "transaction", "label": "Add feature", "intents": [
{ "type": "setProp", "prop": "features", "value": ["Unlimited projects", "New feature"] }
] }]"#
)
);
}
}To see the panel in the editor as you write it, link the dev server.
Limits
A panel runs isolated in your browser, without network, storage, page or clock access. It knows only the selection and changes things only through the editor.
- Its own instances. It edits only the selected instances of its component: their props, and style keys of the declared traits.
- Size. The module is at most 512 KiB and declares one to eight panels.
- Trees. A rendered tree is at most 256 KiB, 2,000 nodes and 32 levels deep. A handler records at most 64 changes.
- Time. The editor stops a panel that takes over a second to answer, loops renders or changes, or crashes.
When the editor refuses a panel's output or stops it, instances show the default Props section with a note that the panel stopped working. It stays stopped for the rest of the editing session.
Next steps
Something wrong or missing on this page? Tell us by email or on Discord.