Skip to content

Whiteboard Components

Whiteboard components implement the WhiteboardComponent subtrait, which pins the associated types for trait object usage.

trait WhiteboardComponent: Component<Metadata = WhiteboardChildMeta, Output = Vec<WhiteboardElement>> {
fn id(&self) -> &'static str;
fn name(&self) -> &'static str;
fn description(&self) -> &'static str;
fn icon(&self) -> &'static str;
}

Child metadata is WhiteboardChildMeta — the placement ID + bounding Rect derived from the placement’s WhiteboardPosition. Each component receives its metadata and resolved property values, then compiles to Vec<WhiteboardElement>.

Element Components (the Typed Element Components RFC)

Section titled “Element Components (the Typed Element Components RFC)”

Each whiteboard element type has its own Component with individual PropertyBinding entries — no JSON blob. Templates can bind any property independently via PropertyBinding::Reference.

ComponentKeyProperties
ShapeComponent"shape"shape_type (Select), fill (Select), color (Color), stroke_color (Color), stroke_width (Number), start_arrow (Select), end_arrow (Select), rotation (Number)
PathComponent"path"points (Json), color (Color), thickness (Number), is_eraser (Boolean)
TextComponent"text"prose_bytes (Json), font_family (Select), text_align (Select)
FileComponent"file"file_type (Select), url (Text), thumbnail_url (Text), file_name (Text), rotation (Number)
UrlComponent"url"url_type (Select), url (Text), title (Text), description (Text), thumbnail_url (Text)
GeogebraComponent"geogebra"geogebra_type (Select), material_id (Text), commands (Text)
MaskComponent"mask"image_url (Text), mask_path (Text)

Higher-level components that compose multiple elements:

ComponentKeyDescription
TextBoxComponent"text_box"Rich text with optional background/border
CardComponent"card"Bordered container with header and body
ListComponent"list"Ordered or unordered list items

Components are registered via inventory::submit! and resolved by their id() string:

pub struct WhiteboardComponentEntry(pub &'static dyn WhiteboardComponent);
inventory::collect!(WhiteboardComponentEntry);
pub fn resolve_component(id: &str) -> Option<&'static dyn WhiteboardComponent> {
inventory::iter::<WhiteboardComponentEntry>
.into_iter()
.find(|e| e.0.id() == id)
.map(|e| e.0)
}

Custom components can be loaded by component_id — the placement stores a UUID pointing to a component instance in storage.

Typed Properties (the Typed Component Properties RFC)

Section titled “Typed Properties (the Typed Component Properties RFC)”

All whiteboard components use #[derive(ComponentProperties)] for typed property structs. Enum fields use #[derive(PropertyField)] to generate PropertyType::Select options. The PropertyField trait bridges Rust types to PropertyValue/PropertyType:

Rust typePropertyTypePropertyValue storage
StringTextString(s)
f64NumberNumber(f64)
boolBooleanBool(b)
ColorColorString("#AARRGGBB")
Option<T>same as TNull when absent
#[derive(PropertyField)] enumSelect { options }String("lowerCamelCase")
Complex data (Vec<Point>)TextString(serde_json::to_string(...)) — JSON-encoded

Use Props::to_bindings() for full construction and PropsUpdate::apply() for partial updates:

// Create a new placement:
let bindings = ShapeProps { shape_type: ShapeType::Rect, fill: ShapeFill::Solid, .. }.to_bindings();
// Update specific fields:
ShapePropsUpdate { color: Some(Color(0xFFFF0000)), ..Default::default() }.apply(&mut p.bindings);

Each component implements TypedComponent and receives a typed properties struct:

impl TypedComponent for ShapeComponent {
type Properties = ShapeProps;
type Metadata = WhiteboardChildMeta;
type Output = Vec<WhiteboardElement>;
fn id() -> &'static str { "shape" }
fn name() -> &'static str { "Shape" }
fn description() -> &'static str { "Geometric shape with fill and stroke" }
fn icon() -> &'static str { "category" }
fn compile(&self, props: &ShapeProps, meta: &WhiteboardChildMeta)
-> Result<Vec<WhiteboardElement>, CompileError>
{
let bounds = &meta.bounds;
Ok(vec![WhiteboardElement::Shape(ShapeElement {
id: next_id(),
position: Offset { dx: bounds.x, dy: bounds.y },
size: Size { width: bounds.width, height: bounds.height },
shape_type: props.shape_type,
fill: props.fill,
color: props.color,
stroke_color: props.stroke_color,
stroke_width: props.stroke_width,
start_arrow: props.start_arrow,
end_arrow: props.end_arrow,
rotation: props.rotation,
..Default::default()
})])
}
}

The blanket Component impl handles HashMap extraction automatically — TypedComponent::compile() always receives typed props.