Skip to content

Component

Component is the compilation trait. Any type that can produce rendered output from metadata and property values implements it. Both leaf component instances (TextBox, Card) and modality structs (Whiteboard, Worksheet) are Components.

crates/core/src/template/component.rs [60:76]
pub trait Component: Send + Sync + 'static {
type Metadata: Hash;
type Output: Clone;
fn properties(&self) -> Vec<PropertySchema>;
fn compile(
&self,
metadata: &Self::Metadata,
values: &HashMap<String, PropertyValue>,
) -> Result<Self::Output, CompileError>;
}

Unlike Reducer::reduce(), compile() takes &self — component instances carry their own configuration.

TypeBoundPurpose
MetadataHashSpatial/structural context passed to compilation. Hash enables incremental caching.
OutputCloneThe compiled result. Clone required for caching and Slot::Cold.

Metadata: Hash is required for Session’s incremental compilation cache. The default cache key for each placement is hash(placement_id, child_metadata, resolved_values) via default_compile_child_hash(). Modalities override compile_child_hash() when compile output depends on additional state (e.g. live LoroText content). If the hash matches the previous compilation, the cached output is reused.

What Metadata represents varies by modality:

ModalityMetadata TypeRepresents
WhiteboardWhiteboardMetadataCanvas dimensions, viewport
WorksheetWorksheetMetadataPage layout, grid configuration
LessonPlanLessonPlanMetadataPresentation settings

For child components, metadata represents the space allocated by the parent:

Child ContextMetadataRepresents
Whiteboard childWhiteboardChildMetaPlacement ID + bounding Rect
Worksheet child()No spatial context needed

properties() returns the schema for configurable values. See Properties Overview for the full PropertySchema definition (10 fields). The key fields for compilation are:

crates/core/src/models/property/mod.rs [387:415]
pub struct PropertySchema {
pub key: String, // Unique key (snake_case)
pub name: String, // Display name
pub property_type: PropertyType, // Type and constraints
pub required: bool, // Must have a value
pub default_value: Option<PropertyValue>, // Default when unset
// ... + is_generator, description, icon, is_system, is_title
}

ComponentSchema (used at the FRB/API layer) is built from properties() plus display metadata like id() and name. It is not part of the trait itself.

A placed component instance, stored in the modality’s synced state. Links a component type to a position and property overrides.

crates/core/src/template/placement.rs [10:17]
pub struct ComponentPlacement<P> {
pub id: String,
pub component_key: String,
pub component_id: Option<String>,
pub position: P,
pub bindings: HashMap<String, PropertyBinding>,
}
FieldPurpose
idUnique placement identifier
component_keyComponent type (e.g. "text_box", "card"). Maps to the static registry.
component_idIf Some, a forked/customized instance loaded from storage. If None, use the registry blueprint.
positionModality-specific spatial info (generic P)
bindingsProperty bindingsReference to parent or Static value

TypedComponent replaces manual Component impls for leaf components. It pairs a ComponentProperties struct with static identity metadata:

crates/core/src/template/typed_component.rs [188:211]
pub trait TypedComponent: Send + Sync + 'static {
type Properties: ComponentProperties;
type Metadata: Hash;
type Output: Clone;
fn id() -> &'static str;
fn name() -> &'static str;
fn description() -> &'static str;
fn icon() -> &'static str;
fn compile(
&self,
props: &Self::Properties,
metadata: &Self::Metadata,
) -> Result<Self::Output, CompileError>;
}

A blanket impl<T: TypedComponent> Component for T bridges to the untyped pipeline — the blanket impl calls Properties::from_values() and delegates to the typed compile(). No changes needed to Modality, Session, caching, or Slot.

See Adding a Component for the full walkthrough.

Subtraits pin the associated types for trait object usage. This allows parent modalities to hold dyn WhiteboardComponent collections:

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;
}
trait WorksheetComponent: Component<Metadata = (), Output = WorksheetElement> {
fn id(&self) -> &'static str;
fn name(&self) -> &'static str;
fn description(&self) -> &'static str;
fn icon(&self) -> &'static str;
}

Both subtraits have blanket impls for any TypedComponent with matching associated types, so implementing TypedComponent is sufficient — you never implement WhiteboardComponent or WorksheetComponent directly.

Components register themselves via the inventory crate:

inventory::submit!(WhiteboardComponentEntry(&CardComponent));

Resolution uses inventory::iter — no manual match arm needed:

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)
}

The Hash bound on Metadata enables Session to cache compilation results per placement. The flow:

  1. Session calls layout() with a caching closure
  2. For each placement, the closure calls modality.compile_child_hash(placement, meta, values) to compute the cache key
  3. Cache hit: return stored output. Cache miss: call compile_child(), store result.

This means editing one placement in a 20-placement whiteboard only recompiles one component — the other 19 are hash hits. Selection changes (ephemeral-only) produce zero recompiles because all hashes match.

Elements are compiled output, never directly mutated. To change what renders on screen, you modify placements, property values, or metadata. Recompilation flows downward automatically through the dispatch lifecycle.

  • Modality — supertrait that adds layout orchestration on top of Component
  • Reducer — state changes trigger recompilation in the dispatch lifecycle
  • Slot — Component impl that switches between cached (Cold) and live (Hot) compilation