Schema-Driven Toolbar
Summary
Section titled “Summary”Replace the hardcoded ObjectToolbar (which switches on WhiteboardElement variants and builds bespoke widgets per type) with a generic, schema-driven toolbar. A Dart PropertyEditor widget takes a (PropertySchema, PropertyValue?) pair and returns a compact inline editor. The toolbar iterates the selected component’s schema in declaration order, rendering one PropertyEditor per property. This toolbar is modality-agnostic — it works for whiteboard, worksheet, document, or any modality whose components expose PropertySchema.
Motivation
Section titled “Motivation”1. Every new element type requires new toolbar code
Section titled “1. Every new element type requires new toolbar code”Today, ObjectToolbar has a switch on WhiteboardElement with dedicated _PathToolbar, _ShapeToolbar, and _TextToolbar widgets. Adding a new element type (e.g. FileComponent, UrlComponent) means writing another bespoke toolbar widget, even though the property types (Color, Select, Number) are the same across components. This doesn’t scale.
2. Components already declare their properties
Section titled “2. Components already declare their properties”the Typed Element Components RFC established that each element type is a Component with properties() -> Vec<PropertySchema>. The schema declares every property’s type, name, constraints (min/max, select options), and order. The toolbar should read this schema rather than duplicate the knowledge.
3. Toolbar should work across modalities
Section titled “3. Toolbar should work across modalities”Whiteboard, worksheet, and document modalities all use the Component trait. A worksheet question component has properties (difficulty, point value, tags) that need inline editing too. A single schema-driven toolbar widget can be reused everywhere — the modality only needs to provide (schema, values, on_change).
4. Property values already exist as PropertyValue
Section titled “4. Property values already exist as PropertyValue”The compiled snapshot already contains resolved property values per element. The toolbar needs (Vec<PropertySchema>, HashMap<String, PropertyValue>) — both are already available from Rust via FRB.
Design
Section titled “Design”Core widget: PropertyEditor
Section titled “Core widget: PropertyEditor”A stateless Dart widget that maps PropertyType to a compact inline editor:
class PropertyEditor extends StatelessWidget { final PropertySchema schema; final PropertyValue? value; final ValueChanged<PropertyValue> onChanged;
Widget build(BuildContext context) { return switch (schema.propertyType) { PropertyType_Color() => _ColorSwatch(value: value, onChanged: onChanged), PropertyType_Number() => _NumberSlider(schema: schema, value: value, onChanged: onChanged), PropertyType_Boolean() => _BoolToggle(value: value, onChanged: onChanged), PropertyType_Select() => _SelectDropdown(schema: schema, value: value, onChanged: onChanged), PropertyType_Text() => _TextChip(schema: schema, value: value, onChanged: onChanged), _ => const SizedBox.shrink(), // unsupported types hidden }; }}Each editor variant is a small, self-contained widget — _ColorSwatch opens a color picker popover, _NumberSlider renders a slider with the schema’s min/max, _SelectDropdown shows the schema’s options, etc. These replace the current bespoke widgets (_MiniColorPicker, ThicknessSlider, _FillCircle, _ArrowSelector).
Toolbar composition: SchemaToolbar
Section titled “Toolbar composition: SchemaToolbar”class SchemaToolbar extends StatelessWidget { final List<PropertySchema> schemas; final Map<String, PropertyValue> values; final ValueChanged<(String key, PropertyValue value)> onChanged;
Widget build(BuildContext context) { final visible = schemas.where((s) => !s.isSystem && s.toolbar != ToolbarHint.hidden);
return Row( mainAxisSize: MainAxisSize.min, children: [ for (final (i, schema) in visible.indexed) ...[ if (i > 0) _divider(), PropertyEditor( schema: schema, value: values[schema.key], onChanged: (v) => onChanged((schema.key, v)), ), ], ], ); }}The toolbar:
- Reads schemas in declaration order (from
Component::properties()) - Filters out system properties and those marked hidden
- Renders a
PropertyEditorfor each, separated by dividers - Calls back with
(key, value)— the parent converts this to the appropriate intent
Schema-level opt-out: toolbar hint
Section titled “Schema-level opt-out: toolbar hint”Add an optional toolbar field to PropertySchema:
pub struct PropertySchema { // ... existing fields ...
/// How this property should appear in the toolbar. /// Defaults to `Auto` (type-based widget). #[serde(default)] pub toolbar: ToolbarHint,}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]pub enum ToolbarHint { /// Default: render the standard widget for this property type. #[default] Auto, /// Hide from the toolbar entirely. Useful for internal/computed properties. Hidden,}Components that have properties unsuitable for the toolbar (e.g. PathComponent’s points property, which is a JSON blob of path data) set toolbar: ToolbarHint::Hidden in their schema declaration.
Dart-side override: custom editors and side-peeks
Section titled “Dart-side override: custom editors and side-peeks”While SchemaToolbar provides the default rendering, the Dart layer can override behavior per component or per property type. The PropertyEditor accepts an optional builder override:
class SchemaToolbar extends StatelessWidget { /// Optional per-key widget overrides. When provided for a key, /// the custom widget is rendered instead of the default PropertyEditor. final Map<String, WidgetBuilder>? overrides; // ...}This enables:
- Custom complex editors — e.g. the shape
fillproperty could use the existing_FillCirclevisual instead of a plain dropdown - Side-peek panels — Dart can intercept a property and open a panel/sheet instead of rendering inline, while still updating the same
PropertyValueunderneath - Grouped controls — e.g.
start_arrowandend_arrowcould be overridden as a single combined arrow editor widget
The key principle: Dart is in control of presentation. The schema provides the data contract; Dart chooses how to render it. A component that wants a non-standard UI provides a WidgetBuilder override — it still reads from and writes to PropertyValue.
Connecting to dispatch
Section titled “Connecting to dispatch”The onChanged callback in SchemaToolbar produces (String key, PropertyValue value). The parent widget (e.g. ObjectToolbar in the whiteboard) translates this into the appropriate intent:
SchemaToolbar( schemas: selectedComponent.schemas, values: selectedComponent.values, onChanged: (key, value) => dispatch( WhiteboardIntent.updateProperty( id: selectedId, key: key, value: value, ), ),)This requires a generic updateProperty intent on the Rust side that takes (id, key, value) and updates the corresponding binding on the placement. The existing typed intents (updateShapeProperties, updatePathProperties, etc.) can be kept as convenience wrappers or deprecated in favor of the generic one.
Exposing schema + values from snapshots
Section titled “Exposing schema + values from snapshots”The whiteboard snapshot needs to include component schemas and current property values for the selected element. Two approaches:
Option A: Rust exposes schemas via a query API. A new FRB-exposed function returns the schema for a given component key:
pub fn component_schemas(component_key: &str) -> Vec<PropertySchema>Values are already derivable from the element’s resolved bindings. Dart calls this when selection changes.
Option B: Snapshot includes schemas + values for selected element.
The WhiteboardSnapshot carries an optional SelectedComponentInfo { schemas, values } populated when there’s a single selection. No extra API call needed.
Option A is simpler and avoids bloating snapshots.
Where this lives
Section titled “Where this lives”SchemaToolbar and PropertyEditor live in a shared package (e.g. packages/modality/ or a new packages/property_editor/) — not in packages/whiteboard/. This makes them reusable across whiteboard, worksheet, document, and template editor contexts.
Implementation Plan
Section titled “Implementation Plan”- Add
ToolbarHinttoPropertySchema— newtoolbarfield withAuto/Hidden. DefaultAuto. Regenerate FRB. Verify:cargo check && flutter analyze - Add
updatePropertyintent — genericWhiteboardIntent::UpdateProperty { id, key, value }that updates a single binding on a placement. Verify:cargo test - Add
component_schemas()FRB API — looks up component by key, returnsVec<PropertySchema>. Verify: FRB codegen +flutter analyze - Build
PropertyEditorwidget — Dart widget mappingPropertyTypeto inline editors (_ColorSwatch,_NumberSlider,_BoolToggle,_SelectDropdown). Start with Color, Number, Boolean, Select. - Build
SchemaToolbarwidget — composesPropertyEditors in a row from schema list. Supportsoverridesmap. - Replace
ObjectToolbarinternals — useSchemaToolbarinsideObjectToolbar. Initially keep custom overrides for shape fill circles and arrow selectors for visual fidelity. Verify: visual comparison. - Mark non-toolbar properties — set
toolbar: Hiddenon properties likepoints(path),rotation, and other internal-only properties. Verify: toolbar only shows relevant controls. - Extend to other modalities — wire
SchemaToolbarinto worksheet and document component editing surfaces.
Alternatives Considered
Section titled “Alternatives Considered”Per-component toolbar widgets registered alongside components
Section titled “Per-component toolbar widgets registered alongside components”Each component registers not just its properties() but also a Dart toolbarBuilder. Rejected because it couples Rust component definitions to Dart UI code — the component registry is Rust-side and shouldn’t know about Flutter widgets. The schema-driven approach keeps the boundary clean: Rust declares data, Dart decides presentation.
Toolbar configuration enum per property type
Section titled “Toolbar configuration enum per property type”Instead of a simple Auto/Hidden hint, have the schema specify exact widget types (ColorPicker, Slider, Dropdown, etc.). Rejected because the schema already carries PropertyType which determines the appropriate widget. Adding explicit widget hints creates a parallel type system. The overrides map on the Dart side handles any cases where the default mapping isn’t sufficient.
Build a property panel instead of an inline toolbar
Section titled “Build a property panel instead of an inline toolbar”Replace the floating toolbar with a side panel showing all properties in a form layout (like Figma’s right panel). Rejected as the sole approach — a compact inline toolbar is better for quick edits on a canvas. However, this RFC’s SchemaToolbar can also render in a panel layout with a different parent widget. The PropertyEditor building blocks are the same either way.
Unresolved Questions
Section titled “Unresolved Questions”Conditional visibility
Section titled “Conditional visibility”Some properties only make sense given other values — e.g. stroke_width is irrelevant when fill is solid, radius only applies to rect shapes. Should the schema support conditional visibility rules (e.g. visible_when: "fill != solid")? Or should Dart handle this with its override mechanism? Leaning toward Dart-side logic initially, with schema-level conditions as a future enhancement if patterns emerge.
Multi-select toolbars
Section titled “Multi-select toolbars”When multiple elements are selected, should the toolbar show the intersection of their schemas? Show shared properties with “mixed” state? This RFC focuses on single-element selection. Multi-select toolbar behavior can be a follow-up.