Skip to content

gpui-whiteboard API

The complete public API of gpui-whiteboard — every exported item, with its signature, parameters, return contract, edge cases, and cost. For the what-and-why (features, quick start, keyboard map, host integration), see the README.

Everything below is the complete public surface — if it isn’t listed here, it isn’t public. Colors throughout are packed 0xRRGGBBAA u32s; geometry is world-space f32 (see Camera).

ItemKindSignaturePurpose
Scenestruct{ camera, elements }The board document — everything persisted
Scene::from_jsonassoc fnfn from_json(s: &str) -> SelfParse stored JSON; empty board on bad input
Scene::to_jsonmethodfn to_json(&self) -> StringSerialize for persistence
Elementstruct{ id, kind, stroke, fill, label, label_color, styles }One board element
ElementKindenum13 variantsWhat an element is (pen stroke, shape, text, …)
Strokestruct{ points, width }Freehand pen geometry
BoxGeomstruct{ x, y, w, h, width, rotation }Box-shape geometry (rect/ellipse/…)
SegGeomstruct{ x1, y1, x2, y2, width }Line / arrow geometry
TextGeomstruct{ x, y, content, size, rotation, … }Free-text geometry
EmbedGeomstruct{ page_id, title, x, y, w, h }Page-card geometry
ImageGeomstruct{ src, x, y, w, h, rotation }Image geometry (host-managed src)
Camerastruct{ x, y, zoom }The viewport: pan offset + zoom
StyleSpanstruct{ start, end, style }A formatting run over a byte range
RunStylestruct{ bold, italic, underline, strike, highlight }The formatting of a run of characters
FormatenumBold | Italic | Underline | StrikeA toggleable boolean format
Toolenum15 variantsThe active tool (UI state, not persisted)
WhiteboardStylestruct9 fieldsTheme colors, read at paint time
WhiteboardStyleFntype aliasRc<dyn Fn() -> WhiteboardStyle>The host’s theme closure
ChangeFntype aliasRc<dyn Fn(String, &mut Window, &mut App)>Persist-on-change hook
PlaceEmbedFntype aliasRc<dyn Fn(f32, f32, &mut Window, &mut App)>Page-card tool clicked
OpenPageFntype aliasRc<dyn Fn(i64, &mut Window, &mut App)>Page-card double-clicked
ImageFntype aliasRc<dyn Fn(&str, f32, &mut Window, &mut App) -> Option<ImageSource>>Fetch a decoded (rotated) bitmap
PlaceImageFntype aliasRc<dyn Fn(f32, f32, &mut Window, &mut App)>Image tool clicked
DropFilesFntype aliasRc<dyn Fn(Vec<PathBuf>, f32, f32, &mut Window, &mut App)>Files dropped on the canvas
CopyFntype aliasRc<dyn Fn(String, &mut Window, &mut App)>Write the selection to the clipboard
PasteFntype aliasRc<dyn Fn(&mut Window, &mut App) -> Option<String>>Read board elements from the clipboard
SaveTemplateFntype aliasRc<dyn Fn(String, &mut Window, &mut App)>Save the selection as a template
DeleteTemplateFntype aliasRc<dyn Fn(i64, &mut Window, &mut App)>Delete a stored template
SavedColorsFntype aliasRc<dyn Fn(Vec<u32>, &mut Window, &mut App)>Persist the saved-color palette
PickFontFntype aliasRc<dyn Fn(FontPick, &mut Window, &mut App)>Font flyout choice
MoveToolbarFntype aliasRc<dyn Fn(Option<(f32, f32)>, bool, &mut Window, &mut App)>Persist the toolbar layout
FontPickenumUpload | DefaultWhich face the Font flyout offers
Templatestruct{ id, name, elements }A reusable, stampable element group
Template::from_jsonassoc fnfn from_json(id: i64, name: impl Into<String>, elements_json: &str) -> SelfBuild from the host’s stored row
WhiteboardViewstructimpl RenderThe board entity: canvas + entire editing UI
WhiteboardView::newconstructorfn new(scene, style, cx) -> SelfBuild a view over a scene
WhiteboardView::set_on_changemethodfn set_on_change(&mut self, f: ChangeFn)Install the persistence hook
WhiteboardView::set_on_place_embedmethodfn set_on_place_embed(&mut self, f: PlaceEmbedFn)Install the page-card placement hook
WhiteboardView::set_on_openmethodfn set_on_open(&mut self, f: OpenPageFn)Install the open-page hook
WhiteboardView::set_on_save_templatemethodfn set_on_save_template(&mut self, f: SaveTemplateFn)Install the save-template hook
WhiteboardView::set_on_delete_templatemethodfn set_on_delete_template(&mut self, f: DeleteTemplateFn)Install the delete-template hook
WhiteboardView::set_on_imagemethodfn set_on_image(&mut self, f: ImageFn)Install the image-fetch hook
WhiteboardView::set_on_place_imagemethodfn set_on_place_image(&mut self, f: PlaceImageFn)Install the place-image hook
WhiteboardView::set_on_drop_filesmethodfn set_on_drop_files(&mut self, f: DropFilesFn)Install the file-drop hook
WhiteboardView::set_on_copymethodfn set_on_copy(&mut self, f: CopyFn)Install the copy hook
WhiteboardView::set_on_pastemethodfn set_on_paste(&mut self, f: PasteFn)Install the paste hook (menu item hidden without it)
WhiteboardView::set_on_save_colorsmethodfn set_on_save_colors(&mut self, f: SavedColorsFn)Install the saved-colors hook
WhiteboardView::set_on_pick_fontmethodfn set_on_pick_font(&mut self, f: PickFontFn)Install the font-picker hook (button hidden without it)
WhiteboardView::set_on_move_toolbarmethodfn set_on_move_toolbar(&mut self, f: MoveToolbarFn)Install the toolbar-moved hook
WhiteboardView::set_toolbar_posmethodfn set_toolbar_pos(&mut self, pos: Option<(f32, f32)>, cx)Push the persisted toolbar position
WhiteboardView::set_toolbar_verticalmethodfn set_toolbar_vertical(&mut self, vertical: bool, cx)Push the persisted toolbar orientation
WhiteboardView::set_saved_colorsmethodfn set_saved_colors(&mut self, colors: Vec<u32>, cx)Push the persisted saved-color palette
WhiteboardView::set_templatesmethodfn set_templates(&mut self, templates: Vec<Template>, cx)Push the stored template list
WhiteboardView::set_fontmethodfn set_font(&mut self, font: Font, cx)Swap the text face
WhiteboardView::add_embedmethodfn add_embed(&mut self, page_id, title, x, y, cx)Insert a page-card (host, after PlaceEmbedFn)
WhiteboardView::add_image_atmethodfn add_image_at(&mut self, src, px_w, px_h, cx_world, cy_world, cx)Insert an image (host, after PlaceImageFn/drop)
WhiteboardView::viewport_centermethodfn viewport_center(&self) -> [f32; 2]World point at the viewport center
WhiteboardView::scenemethodfn scene(&self) -> &SceneBorrow the current board document
WhiteboardView::toolmethodfn tool(&self) -> ToolThe active tool
WhiteboardView::set_toolmethodfn set_tool(&mut self, tool: Tool, cx)Switch the active tool
WhiteboardView::reset_viewmethodfn reset_view(&mut self, cx)Back to the origin at 100%
WhiteboardView::zoom_inmethodfn zoom_in(&mut self, cx)Zoom a step about the canvas center
WhiteboardView::zoom_outmethodfn zoom_out(&mut self, cx)Zoom out a step about the canvas center
WhiteboardView::undomethodfn undo(&mut self, window, cx)Revert the last change
WhiteboardView::redomethodfn redo(&mut self, window, cx)Re-apply the last undone change
WhiteboardView::paste_elementsmethodfn paste_elements(&mut self, json: &str, window, cx)Stamp a serialized selection onto the board
impl Render for WhiteboardViewtrait implRenders the board + its whole editing UI
FontstructClone (cheap)A TTF/OTF face backing whiteboard text
Font::from_bytesassoc fnfn from_bytes(bytes: Vec<u8>, index: u32) -> Option<Self>Build from raw face bytes
impl Default for Fonttrait implfn default() -> SelfThe bundled JetBrains Mono face
Font::measuremethodfn measure(&self, content, font_size) -> (f32, f32)Text block extent, unwrapped
Font::measure_wrappedmethodfn measure_wrapped(&self, content, font_size, max_width) -> (f32, f32)Text block extent with word-wrap
Font::fit_sizemethodfn fit_size(&self, content, max_w, max_h, max_size) -> f32Largest font size that fits a box
Font::caret_posmethodfn caret_pos(&self, content, font_size, at) -> [f32; 2]Caret top-left at a byte offset
Font::caret_pos_wrappedmethodfn caret_pos_wrapped(&self, content, font_size, max_width, at) -> [f32; 2]Ditto, honoring a wrap width
Font::index_at_wrappedmethodfn index_at_wrapped(&self, content, font_size, max_width, p) -> usizeByte offset nearest a local point
Font::selection_rectsmethodfn selection_rects(&self, content, font_size, start, end) -> Vec<[f32; 4]>Selection highlight rects
Font::selection_rects_wrappedmethodfn selection_rects_wrapped(&self, content, font_size, max_width, start, end) -> Vec<[f32; 4]>Ditto, honoring a wrap width
Font::layout_wrappedmethodfn layout_wrapped(…) -> TextLayoutGlyph outlines (return type not exported — see note)
Font::layout_styledmethodfn layout_styled(…) -> StyledLayoutStyled glyph outlines (not callable externally — see note)

Scene, Element, ElementKind, the geometry structs, Camera, RunStyle, and StyleSpan all derive Clone, Debug, Serialize, and Deserialize (Scene and RunStyle also Default; Camera has a hand-written Default = origin at zoom 1). Tool, Format, and FontPick are Clone + Copy + PartialEq + Eq + Debug. Nothing here is Send: the view is a gpui entity and the callbacks are Rcs — main-thread only, like all gpui UI (Font is the one exception).


pub struct Scene {
pub camera: Camera,
pub elements: Vec<Element>,
}

The board document: everything persisted for a whiteboard. The host stores to_json opaquely (Zorite keeps it in the content column of a kind = 'whiteboard' page) and reloads with from_json.

Fields

FieldTypeMeaning
cameraCameraThe viewport (pan + zoom). Persisted so a board reopens where you left it.
elementsVec<Element>The board’s content, painted in order — later = on top.

Serialization — plain serde JSON. Every field is #[serde(default)], so older boards keep loading as the model grows (missing fields take their defaults). Scene::default() is an empty board with the default camera.

pub fn from_json(s: &str) -> Self

Parse a board from its stored JSON.

Parameters

NameTypeDescription
s&strThe stored scene JSON (what to_json / ChangeFn produced).

Returns — the parsed Scene. Never panics, never errors: empty or whitespace-only input yields Scene::default(); malformed JSON logs a log::warn! and yields Scene::default() — a corrupt row never blocks opening the board.

Guarantees & edge cases

  • The camera zoom is sanitized after parsing: a non-finite or <= 0 zoom (e.g. from hand-edited JSON) is reset to 1.0, so the world↔screen math can’t divide by zero.
  • Unknown element kinds or fields fail the whole parse (serde) → empty board; missing fields are fine (defaults).
pub fn to_json(&self) -> String

Serialize for persistence.

Parameters — none.

Returns — the scene as a JSON string. Falls back to "{}" if serialization fails (unreachable for well-formed scenes — no fallible types are serialized).

Guarantees & edge casesOption fields that are None and empty styles vecs are omitted from the output (skip_serializing_if), keeping stored boards compact and stable.


pub struct Element {
pub id: u64,
pub kind: ElementKind,
pub stroke: Option<u32>,
pub fill: Option<u32>,
pub label: Option<String>,
pub label_color: Option<u32>,
pub styles: Vec<StyleSpan>,
}

One board element: a stable id plus its geometry/kind and appearance.

Fields

FieldTypeMeaning
idu64Stable per-board identity. WhiteboardView::new seeds its id counter from max(id) + 1.
kindElementKindWhat it is + its geometry.
strokeOption<u32>Stroke / ink color, packed 0xRRGGBBAA. None follows the theme ink, so uncolored elements adapt to light/dark.
fillOption<u32>Fill for closed shapes; None = unfilled outline. Ignored by other kinds.
labelOption<String>Centered text label for closed shapes, word-wrapped and auto-shrunk to the shape’s inscribed area. None/empty = no label; ignored by non-shape kinds.
label_colorOption<u32>Color of label; None follows stroke (theme ink if that’s unset too).
stylesVec<StyleSpan>Rich-text runs over the element’s text (a Text element’s content, or a shape’s label). Kept sorted, non-overlapping, non-plain; empty = unstyled.

Serializationstroke / fill / label / label_color are omitted when None, styles when empty; all take defaults on load, so pre-feature boards keep loading.


#[serde(rename_all = "snake_case")]
pub enum ElementKind {
Draw(Stroke),
Rect(BoxGeom),
Ellipse(BoxGeom),
Diamond(BoxGeom),
Triangle(BoxGeom),
RoundRect(BoxGeom),
Star(BoxGeom),
Hexagon(BoxGeom),
Line(SegGeom),
Arrow(SegGeom),
Text(TextGeom),
Embed(EmbedGeom),
Image(ImageGeom),
}

The kinds of thing a board can hold.

Variants

VariantGeometryMeaning
DrawStrokeFreehand pen stroke
Rect / Ellipse / Diamond / Triangle / RoundRect / Star / HexagonBoxGeomClosed shapes (fillable, labelable, rotatable)
Line / ArrowSegGeomStraight connectors (arrow has a head at (x2, y2))
TextTextGeomFree text
EmbedEmbedGeomPage-card linking to a host page
ImageImageGeomImage referencing a host-managed file

Serialization — externally tagged with snake_case variant names: {"rect": {…}}, {"round_rect": {…}}, {"image": {…}}, ….


pub struct Stroke {
pub points: Vec<[f32; 2]>,
pub width: f32,
}

A freehand pen stroke.

Fields

FieldTypeMeaning
pointsVec<[f32; 2]>World-space polyline points, input-thinned at draw time.
widthf32Stroke width, world units (drawn as NIB / zoom so it scales with content).

pub struct BoxGeom {
pub x: f32,
pub y: f32,
pub w: f32,
pub h: f32,
pub width: f32,
pub rotation: f32,
}

A box shape (rect / ellipse / diamond / …), world-space. x, y, w, h describe the unrotated box; rotation spins it about its center at paint time.

Fields

FieldTypeMeaning
x, yf32Top-left of the unrotated box, world space.
w, hf32Extent, world units.
widthf32Outline stroke width, world units.
rotationf32Radians, clockwise, about the box center. #[serde(default)] — absent in older boards → 0.

pub struct SegGeom {
pub x1: f32,
pub y1: f32,
pub x2: f32,
pub y2: f32,
pub width: f32,
}

A directed segment (line / arrow), world-space. An arrow’s head sits at (x2, y2).

Fields

FieldTypeMeaning
x1, y1f32Start point, world space.
x2, y2f32End point, world space.
widthf32Stroke width, world units.

pub struct TextGeom {
pub x: f32,
pub y: f32,
pub content: String,
pub size: f32,
pub rotation: f32,
pub measured_w: f32, // #[serde(skip)] — runtime cache
pub measured_h: f32, // #[serde(skip)] — runtime cache
}

A free-text label: a top-left anchor, its content, and a world-space font size.

Fields

FieldTypeMeaning
x, yf32Top-left of the text block, world space.
contentStringThe text. Newlines break lines.
sizef32Font size, world units (created as 18 screen px ÷ zoom).
rotationf32Radians about the block’s center. #[serde(default)] → 0 in older boards.
measured_w, measured_hf32Cached world-space extent, set each render from the font layout so selection/hit-test fit the real glyphs. #[serde(skip)] — not persisted; 0 means unmeasured (a rough estimate is used until first paint).

pub struct EmbedGeom {
pub page_id: i64,
pub title: String,
pub x: f32,
pub y: f32,
pub w: f32,
pub h: f32,
}

A page-card: a titled box that links to a host page. The crate is page-agnostic — the host supplies the id + title (WhiteboardView::add_embed) and handles opening it (OpenPageFn); the crate just stores and draws the card.

Fields

FieldTypeMeaning
page_idi64Host page identity — opaque to the crate.
titleStringThe card’s displayed title.
x, y, w, hf32World-space box.

pub struct ImageGeom {
pub src: String,
pub x: f32,
pub y: f32,
pub w: f32,
pub h: f32,
pub rotation: f32,
}

An image referencing a host-managed file. The crate is storage-agnostic: the host imports the file and serves the decoded bitmap via ImageFn; the scene stores only the reference + geometry.

Fields

FieldTypeMeaning
srcStringHost file reference (e.g. images/<name>) — opaque to the crate.
x, y, w, hf32World-space box.
rotationf32Radians about the image center. #[serde(default)] → 0. The host re-rotates the bitmap to match (see ImageFn).

pub struct Camera {
pub x: f32,
pub y: f32,
pub zoom: f32,
}

The viewport: a world-space pan offset and a zoom factor. (x, y) is the world point that maps to the canvas’s top-left corner, so a canvas-relative screen point s is the world point offset + s / zoom (and world → screen is (p - offset) * zoom).

Fields

FieldTypeMeaning
x, yf32World point at the canvas top-left. #[serde(default)] → 0.
zoomf32Screen px per world unit. Serde default 1.0; interaction clamps it to [0.1, 8.0].

Guarantees & edge cases

  • Camera::default() = origin at 100% (x: 0, y: 0, zoom: 1).
  • Scene::from_json resets a non-finite or non-positive zoom to 1.0; the internal math additionally floors the divisor at 0.1, so hand-edited JSON can’t produce NaN geometry.

pub struct StyleSpan {
pub start: usize,
pub end: usize,
pub style: RunStyle,
}

A RunStyle over the byte range [start, end) of an element’s text (a Text element’s content or a shape’s label). See Element::styles.

Fields

FieldTypeMeaning
start, endusizeUTF-8 byte offsets, [start, end).
styleRunStyleThe formatting of that run.

Guarantees & edge cases — the view keeps an element’s spans sorted, non-overlapping, and non-plain (a plain run is an implicit gap), and re-aligns them across every text edit. If you construct scenes by hand, keep the same invariants.


pub struct RunStyle {
pub bold: bool,
pub italic: bool,
pub underline: bool,
pub strike: bool,
pub highlight: Option<u32>,
}

The formatting of a run of characters; RunStyle::default() is plain text.

Fields

FieldTypeMeaning
boldboolSynthetic bold (a doubled, offset outline pass — works with any face).
italicboolSynthetic oblique (~12° shear).
underlineboolUnderline decoration.
strikeboolStrikethrough decoration.
highlightOption<u32>Highlight color behind the glyphs, packed 0xRRGGBBAA; None = none.

Serialization — every flag is omitted when unset (false / None), so stored runs are minimal.


pub enum Format {
Bold,
Italic,
Underline,
Strike,
}

A toggleable boolean format — the four on/off axes of a RunStyle. (Highlight is a color, toggled on its own.) Exposed as part of the model vocabulary; the view’s keyboard/menu formatting drives it internally — hosts normally never construct one.


pub enum Tool {
Pan, Select, Pen,
Rect, Ellipse, Diamond, Triangle, RoundRect, Star, Hexagon,
Line, Arrow, Text, Embed, Image,
}

The active tool. UI state — not part of the persisted scene. Pan is the default (navigation before drawing). The view renders its own toolbar for these and handles their single-key shortcuts (H V P R O D G U S X L A T I; Embed has no key) — use set_tool/tool only to drive tools from your own chrome.


pub struct WhiteboardStyle {
pub bg: Hsla,
pub grid: Hsla,
pub text: Hsla,
pub ink: Hsla,
pub panel: Hsla,
pub panel_strong: Hsla,
pub accent: Hsla,
pub selection: Hsla,
pub swatches: Vec<Hsla>,
}

Theme colors, read at paint time (via WhiteboardStyleFn) so the board follows live theme changes with no push from the host.

Fields

FieldTypeMeaning
bgHslaCanvas background.
gridHslaBackground grid dots.
textHslaHUD / muted on-canvas text (zoom %, placeholders).
inkHslaDefault stroke/shape color; per-element colors override it.
panelHslaToolbar / flyout pill background — small pills, can be glassy.
panel_strongHslaThe larger color-picker / menu surface — keep much more opaque than panel so it reads over a busy canvas.
accentHslaActive-tool highlight (subtle fill behind the current tool button).
selectionHslaSelection outline — use a strong, clearly visible color.
swatchesVec<Hsla>Quick swatches in the color picker (typically the host’s theme palette).

pub type WhiteboardStyleFn = Rc<dyn Fn() -> WhiteboardStyle>;

The host’s theme closure, passed to WhiteboardView::new.

Fires — every paint (and when tooltips/panels are built).

The host must — return the current theme mapped to a WhiteboardStyle. Return fresh values each call; the board never caches them, which is what makes live light/dark switching free.

Cost — called at paint frequency: keep it a cheap struct build, not a theme recomputation.


pub type ChangeFn = Rc<dyn Fn(String, &mut Window, &mut App)>;

The persistence hook — install with set_on_change.

Fires — when the board changes: an element committed / moved / resized / deleted, text edited, z-order changed, undo/redo, paste, template stamp, or the camera moved. Changes made during a drag are batched (a dirty flag) and flushed on mouse-up, so you get one call per gesture, not per mouse-move.

Arguments — the full scene serialized as JSON (Scene::to_json).

The host must — store the string wherever this board lives. Nothing else; don’t parse it.

Guarantees & edge casesadd_embed / add_image_at deliberately do not fire it (they run mid-host-update; a re-entrant save would panic) — persist explicitly via scene after calling them.


pub type PlaceEmbedFn = Rc<dyn Fn(f32, f32, &mut Window, &mut App)>;

Install with set_on_place_embed.

Fires — when the page-card tool is clicked on the canvas, with the world (x, y) of the click.

The host must — show its page picker, then call WhiteboardView::add_embed(page_id, title, x, y, cx) with the chosen page — and persist via scene afterward (see ChangeFn).


pub type OpenPageFn = Rc<dyn Fn(i64, &mut Window, &mut App)>;

Install with set_on_open.

Fires — when a page-card is double-clicked, with its page_id.

The host must — open that page (e.g. in a tab). Without the hook, double-clicking a card does nothing.


pub type ImageFn = Rc<dyn Fn(&str, f32, &mut Window, &mut App) -> Option<gpui::ImageSource>>;

Install with set_on_image.

Fireseach render, once per image element, with the element’s src and its rotation in radians (0 = upright).

The host must — return the decoded bitmap for src, pre-rotated by the given angle, from its own cache. Return None until the decode is ready (the board shows a placeholder), then notify/re-render when it lands. Decode and rotate off-thread and cache by (src, angle) — this is a per-paint call, so a cache miss must not block the UI thread. A steady angle hits the cache; re-rotation only happens when the angle changes.

Guarantees & edge cases — without the hook, image elements render as placeholders; the scene still stores them.


pub type PlaceImageFn = Rc<dyn Fn(f32, f32, &mut Window, &mut App)>;

Install with set_on_place_image.

Fires — when the image tool is clicked on the canvas, with the world (x, y).

The host must — show a file picker, import the chosen image into its own store, then call WhiteboardView::add_image_at — and persist via scene afterward.


pub type DropFilesFn = Rc<dyn Fn(Vec<std::path::PathBuf>, f32, f32, &mut Window, &mut App)>;

Install with set_on_drop_files.

Fires — when files are dropped onto the canvas, with the paths and the world (x, y) of the drop point.

The host must — import any image files and place them via add_image_at (ignore non-images), then persist via scene.


pub type CopyFn = Rc<dyn Fn(String, &mut Window, &mut App)>;

Install with set_on_copy.

Fires — on ⌘C / ⌘X with a non-empty selection (⌘X then deletes), with the selection serialized as a Vec<Element> JSON string, translated so the group’s bounding box starts at the origin — the same portable format as SaveTemplateFn and paste_elements, so a copy pastes onto any board.

The host must — write the string to the system clipboard (the crate never touches the clipboard itself).


pub type PasteFn = Rc<dyn Fn(&mut Window, &mut App) -> Option<String>>;

Install with set_on_paste.

Fires — on ⌘V, and when the context-menu Paste item is clicked.

The host must — read the system clipboard and return previously copied whiteboard elements (the JSON a CopyFn wrote), or None if the clipboard holds no board elements. The view passes the returned JSON to paste_elements.

Guarantees & edge cases

  • Returning None on ⌘V lets the keystroke propagate, so the host can handle a clipboard image instead (elements are preferred over images).
  • Without the hook, the Paste menu item is hidden and ⌘V always propagates.

pub type SaveTemplateFn = Rc<dyn Fn(String, &mut Window, &mut App)>;

Install with set_on_save_template.

Fires — when the user saves the current selection as a template (right-click → save), with the selected elements serialized and origin-normalized (same format as CopyFn).

The host must — prompt for a name, store (id, name, json) in its own store, then push the updated list back via set_templates (rebuild each row with Template::from_json).


pub type DeleteTemplateFn = Rc<dyn Fn(i64, &mut Window, &mut App)>;

Install with set_on_delete_template.

Fires — when a template card is right-clicked → delete, with the host id the template was built with.

The host must — confirm if desired, remove the stored row, and push the updated list back via set_templates.


pub type SavedColorsFn = Rc<dyn Fn(Vec<u32>, &mut Window, &mut App)>;

Install with set_on_save_colors.

Fires — when the user’s saved-color palette changes (a swatch added via the picker’s +, or removed via right-click), with the full updated list (packed 0xRRGGBBAA).

The host must — persist the list and feed it back via set_saved_colors on board open. Without the hook, the palette still works but is per-session.


pub type PickFontFn = Rc<dyn Fn(FontPick, &mut Window, &mut App)>;

Install with set_on_pick_font.

Fires — when the user picks from the toolbar’s Aa Font flyout: FontPick::Upload (Upload font…) or FontPick::Default (Use default).

The host must — for Upload, show a file dialog, build the face with Font::from_bytes, and call set_font; for Default, call set_font(Font::default(), cx). Persist the per-board choice and restore it on reopen (the crate doesn’t store the font in the scene).

Guarantees & edge cases — without the hook, the Font toolbar button is hidden.


pub type MoveToolbarFn = Rc<dyn Fn(Option<(f32, f32)>, bool, &mut Window, &mut App)>;

Install with set_on_move_toolbar.

Fires — when the toolbar is dragged to a new spot, reset (double-click its grip), or flipped row ↔ column (R mid-drag). Arguments: the new board-relative top-left (None = the default top-center) and whether the bar is vertical.

The host must — persist both values and feed them back via set_toolbar_pos / set_toolbar_vertical on open. Without the hook, the layout is per-session.


pub enum FontPick {
Upload,
Default,
}

Which face the Font flyout offers — handed to the host’s PickFontFn.

VariantMeaning
UploadPick a .ttf / .otf from disk (the host shows the file dialog).
DefaultRevert to the bundled default face (JetBrains Mono).

pub struct Template {
pub id: i64,
pub name: String,
pub elements: Vec<Element>,
}

A reusable group of elements the user can stamp onto a board (shown as preview cards in the Pages & Images flyout’s gallery). Element positions are normalized so the group’s bounding box starts at the origin; applying re-bases them centered in the viewport, with fresh ids. The host owns persistence and the id; the crate renders the preview and instantiates on click.

Fields

FieldTypeMeaning
idi64Host storage identity — handed back verbatim by DeleteTemplateFn.
nameStringShown on the gallery card.
elementsVec<Element>Origin-normalized element group.
pub fn from_json(id: i64, name: impl Into<String>, elements_json: &str) -> Self

Build a template from the host’s stored row.

Parameters

NameTypeDescription
idi64Your storage id.
nameimpl Into<String>The display name.
elements_json&strA serialized Vec<Element> — the JSON a SaveTemplateFn handed you.

Returns — the Template. Never panics: malformed JSON yields an empty (still-listable) template rather than an error.


pub struct WhiteboardView { /* private */ }
impl Render for WhiteboardView { /* the board + its entire editing UI */ }

The whiteboard entity. It owns the scene, the active tool, selection, in-progress edits, undo/redo history, and renders the whole editing UI — toolbar, tool flyouts, color picker, thickness/font flyouts, templates gallery, and right-click context menu. The host holds it in an Entity<WhiteboardView> (keyed by board id) and renders it into a tab/panel like any entity. All methods run on the UI thread inside entity updates (they take &mut Context<Self>); methods taking cx call cx.notify() themselves.

With no callbacks installed it is still a working board (draw, select, style, zoom, undo/redo; text editing even uses the system clipboard directly via gpui) — the hooks add persistence, pages, images, element copy/paste through the system clipboard, templates, fonts, and toolbar-layout memory.

pub fn new(scene: Scene, style: WhiteboardStyleFn, cx: &mut Context<Self>) -> Self

Build a view over scene. Call inside cx.new(|cx| WhiteboardView::new(..)).

Parameters

NameTypeDescription
sceneSceneThe board to edit — Scene::default() or Scene::from_json of a stored board. Taken by value; the view owns it.
styleWhiteboardStyleFnTheme closure, read each paint.
cx&mut Context<Self>Entity context (used to create the focus handle).

Returns — the view. Infallible.

Guarantees & edge cases

  • The internal id counter seeds from max(element ids) + 1 (0 for an empty board), so new elements never collide with loaded ones.
  • Initial state: tool Pan, nothing selected, no hooks installed, empty saved-colors and templates, default toolbar layout (top-center, horizontal), bundled font, empty undo history.

Example

let board = cx.new(|cx| {
let mut v = WhiteboardView::new(Scene::from_json(&stored), style_fn.clone(), cx);
v.set_on_change(Rc::new(|json, _w, _cx| store(json)));
v
});

Thirteen setters, one per callback type; each simply stores the closure (no notify — the hook takes effect from the next event). All optional; install after new, typically inside the same cx.new closure.

pub fn set_on_change(&mut self, f: ChangeFn)

Install the persistence hook. See ChangeFn for when it fires and the batching contract.

pub fn set_on_place_embed(&mut self, f: PlaceEmbedFn)

Install the page-card placement hook (page-card tool click). See PlaceEmbedFn.

pub fn set_on_open(&mut self, f: OpenPageFn)

Install the open-page hook (double-click a card). See OpenPageFn.

pub fn set_on_save_template(&mut self, f: SaveTemplateFn)

Install the save-template hook (right-click selection → save). See SaveTemplateFn.

pub fn set_on_delete_template(&mut self, f: DeleteTemplateFn)

Install the delete-template hook (right-click a template card → delete). See DeleteTemplateFn.

pub fn set_on_image(&mut self, f: ImageFn)

Install the image-fetch hook (decoded bitmap for an element’s src, called per paint). See ImageFn — the caching contract matters.

pub fn set_on_place_image(&mut self, f: PlaceImageFn)

Install the place-image hook (image tool click → host file picker). See PlaceImageFn.

pub fn set_on_drop_files(&mut self, f: DropFilesFn)

Install the file-drop hook (files dropped on the canvas). See DropFilesFn.

pub fn set_on_copy(&mut self, f: CopyFn)

Install the copy hook (⌘C / ⌘X → write the selection to the system clipboard). See CopyFn.

pub fn set_on_paste(&mut self, f: PasteFn)

Install the paste hook (context-menu Paste → read board elements from the clipboard). Without it, the Paste menu item is hidden. See PasteFn.

pub fn set_on_save_colors(&mut self, f: SavedColorsFn)

Install the saved-colors hook (the palette changed → host persists it). See SavedColorsFn.

pub fn set_on_pick_font(&mut self, f: PickFontFn)

Install the font-picker hook. Without it, the Font (Aa) toolbar button is hidden. See PickFontFn.

pub fn set_on_move_toolbar(&mut self, f: MoveToolbarFn)

Install the toolbar-moved hook (the host persists position + orientation). See MoveToolbarFn.

The counterparts of the persistence hooks: the host pushes stored values on open and after each change. Each replaces the current value and calls cx.notify().

pub fn set_toolbar_pos(&mut self, pos: Option<(f32, f32)>, cx: &mut Context<Self>)

Set the toolbar’s board-relative top-left; None restores the default top-center. Feed back what a MoveToolbarFn reported.

pub fn set_toolbar_vertical(&mut self, vertical: bool, cx: &mut Context<Self>)

Set the toolbar orientation (true = a vertical column).

pub fn set_saved_colors(&mut self, colors: Vec<u32>, cx: &mut Context<Self>)

Replace the user’s saved-color palette (packed 0xRRGGBBAA), shown in the color picker. Feed back what a SavedColorsFn reported.

pub fn set_templates(&mut self, templates: Vec<Template>, cx: &mut Context<Self>)

Replace the stored templates shown in the Pages & Images flyout / gallery. Call on open and after any save/delete round-trip.

pub fn set_font(&mut self, font: Font, cx: &mut Context<Self>)

Swap the face used to render all board text (see Font). Takes effect next paint; existing elements re-render in the new face (layout is per-paint, nothing is baked).

Both insert, select the new element, and switch to the Select tool — and neither fires ChangeFn: they’re called from inside a host update (re-entrant persistence would panic), so persist explicitly with scene.to_json() afterward. Both push an undo step.

pub fn add_embed(
&mut self,
page_id: i64,
title: impl Into<String>,
x: f32,
y: f32,
cx: &mut Context<Self>,
)

Insert a page-card at world (x, y) — the coordinates a PlaceEmbedFn handed you.

Parameters

NameTypeDescription
page_idi64Your page identity (round-trips through OpenPageFn).
titleimpl Into<String>Card title.
x, yf32World-space top-left.
cx&mut Context<Self>Entity context.

Guarantees & edge cases — the card is created at 210 × 76 screen px (divided by the current zoom), so it appears the same size regardless of zoom level.

pub fn add_image_at(
&mut self,
src: impl Into<String>,
px_w: f32,
px_h: f32,
cx_world: f32,
cy_world: f32,
cx: &mut Context<Self>,
)

Insert an image element referencing src, centered at world (cx_world, cy_world).

Parameters

NameTypeDescription
srcimpl Into<String>Your file reference (what ImageFn will be asked for).
px_w, px_hf32The image’s pixel dimensions — used only for the aspect ratio / default size.
cx_world, cy_worldf32World-space center (e.g. the drop point, or viewport_center for a paste).
cx&mut Context<Self>Entity context.

Guarantees & edge cases — sized so the longest edge is 280 screen px at the current zoom, aspect preserved; degenerate pixel dimensions are floored at 1 so the math can’t divide by zero. Rotation starts at 0.

pub fn viewport_center(&self) -> [f32; 2]

The world point at the center of the current viewport — where a host-initiated paste should drop an image (the host has no other access to the camera). Returns [x, y] world coordinates. Before first paint the canvas bounds are zero, so this degenerates to the camera offset.

pub fn scene(&self) -> &Scene

Borrow the current board document — e.g. view.scene().to_json() to persist after an add_embed / add_image_at.

pub fn tool(&self) -> Tool

The active tool (for host-driven chrome).

Most boards never need these — the built-in toolbar and shortcuts drive them — but they exist for custom chrome.

pub fn set_tool(&mut self, tool: Tool, cx: &mut Context<Self>)

Switch the active drawing tool. Switching to anything other than Tool::Select clears the selection; always closes an open tool flyout.

pub fn reset_view(&mut self, cx: &mut Context<Self>)

Reset the viewport to the origin at 100% (Camera::default()). Also bound to double-click on empty canvas. The camera change is marked dirty and persisted at the next flush (mouse-up on the canvas).

WhiteboardView::zoom_in / WhiteboardView::zoom_out

Section titled “WhiteboardView::zoom_in / WhiteboardView::zoom_out”
pub fn zoom_in(&mut self, cx: &mut Context<Self>)
pub fn zoom_out(&mut self, cx: &mut Context<Self>)

Zoom by a 1.2× step (in / out), keeping the world point at the canvas center fixed. Clamped to zoom [0.1, 8.0]; a step at the clamp edge is a no-op. Persisted at the next flush, like reset_view.

WhiteboardView::undo / WhiteboardView::redo

Section titled “WhiteboardView::undo / WhiteboardView::redo”
pub fn undo(&mut self, window: &mut Window, cx: &mut Context<Self>)
pub fn redo(&mut self, window: &mut Window, cx: &mut Context<Self>)

Step the history (⌘Z / ⌘⇧Z do this already). Each history step is a full scene snapshot; the stack keeps at most 50. A step clears the selection and fires ChangeFn immediately. Empty stack → no-op. Any new mutation clears the redo stack.

pub fn paste_elements(&mut self, json: &str, window: &mut Window, cx: &mut Context<Self>)

Stamp a serialized Vec<Element> (the origin-normalized JSON a CopyFn wrote) onto the board: centered in the viewport, with fresh ids, selected, tool switched to Select. Call this from your PasteFn flow, or directly when the system clipboard holds board elements.

Parameters

NameTypeDescription
json&strA serialized Vec<Element> (from CopyFn / SaveTemplateFn).
window, cxWindow + entity context.

Guarantees & edge cases

  • Invalid JSON is silently ignored (no-op); an empty element list is a no-op.
  • Unlike add_embed/add_image_at, this does push undo and fire ChangeFn — it’s driven by user gestures, not mid-host-update.

pub struct Font { /* Arc<Vec<u8>> + face index */ }
impl Default for Font { /* the bundled JetBrains Mono Regular (OFL) */ }

A font backing whiteboard text. Board text is rendered as vector outlines (via ttf-parser), not gpui glyph sprites, so it rotates/scales with the camera; a Font is just the raw face bytes, parsed on demand. Clone is an Arc bump — cheap. Unlike everything else in this crate, Font holds no Rc/gpui state and its methods are pure — safe to use from any thread.

All coordinates and sizes in the methods below are unit-agnostic (the whiteboard passes world units): text-local space with the origin at the block’s top-left, x right, y down. content newlines break lines; byte offsets are UTF-8 offsets into content.

Cost — every method re-parses the face from bytes (ttf_parser::Face::parse) per call. That’s a header parse, not a decode — fine at UI frequency, but don’t call in a tight per-glyph loop.

pub fn from_bytes(bytes: Vec<u8>, index: u32) -> Option<Self>

Build a font from raw face bytes (e.g. a user-uploaded .ttf/.otf).

Parameters

NameTypeDescription
bytesVec<u8>The font file’s contents.
indexu32Face index within a collection (.ttc); pass 0 otherwise.

ReturnsSome(Font) if the bytes parse as a valid face at index, None otherwise (never panics). Validation happens here, so the layout methods can assume a parseable face (and still degrade gracefully if not).

pub fn measure(&self, content: &str, font_size: f32) -> (f32, f32)
pub fn measure_wrapped(&self, content: &str, font_size: f32, max_width: Option<f32>) -> (f32, f32)

The block’s (width, height) at font_size without building outlines — for selection bounds and hit-testing (no Window needed). measure is measure_wrapped(…, None); with Some(max_width) the text word-wraps and the height reflects the wrapped line count.

Returns(width, height): width of the widest line, and line_count.max(1) × line_height — so "" still measures one line high.

pub fn fit_size(&self, content: &str, max_w: f32, max_h: f32, max_size: f32) -> f32

The largest font size in (0, max_size] at which content, wrapped to max_w, fits within a max_w × max_h box — how shape labels auto-shrink.

Parameters

NameTypeDescription
content&strThe label text.
max_w, max_hf32The box to fit (world units in board use).
max_sizef32The preferred (starting) font size.

Returns — the fitting size. Returns max_size unchanged for empty content or a non-positive box; never returns below 1.0 world unit (a tiny box may then still overflow slightly).

Cost — early-out if max_size already fits; otherwise a 20-iteration bisection, each step a measure_wrapped.

pub fn caret_pos(&self, content: &str, font_size: f32, at: usize) -> [f32; 2]
pub fn caret_pos_wrapped(&self, content: &str, font_size: f32, max_width: Option<f32>, at: usize) -> [f32; 2]

Text-local top-left of the caret at content byte offset at (the _wrapped variant honors a wrap width — label editing). Out-of-range or non-boundary offsets clamp to the end of the text. Caret stops come from the same layout pass as the glyphs, so the caret always lands exactly between rendered characters.

pub fn index_at_wrapped(&self, content: &str, font_size: f32, max_width: Option<f32>, p: [f32; 2]) -> usize

The content byte offset whose caret sits nearest the text-local point p — how a click lands the caret between letters. Picks the line by p[1] (clamped to the first/last line), then the closest caret stop by p[0]. Returns 0 for empty layouts. Always a valid char boundary of content.

Font::selection_rects / Font::selection_rects_wrapped

Section titled “Font::selection_rects / Font::selection_rects_wrapped”
pub fn selection_rects(&self, content: &str, font_size: f32, start: usize, end: usize) -> Vec<[f32; 4]>
pub fn selection_rects_wrapped(&self, content: &str, font_size: f32, max_width: Option<f32>, start: usize, end: usize) -> Vec<[f32; 4]>

Text-local highlight rectangles [x, y, w, h] for the selection [start, end) (byte offsets) — one rect per line the selection touches. start >= end returns an empty vec. A line whose trailing newline is inside the selection gets a small stub (0.3 × font size) so selected newlines and empty lines still read.

Font::layout_wrapped / Font::layout_styled (crate-internal)

Section titled “Font::layout_wrapped / Font::layout_styled (crate-internal)”
pub fn layout_wrapped(&self, content: &str, font_size: f32, max_width: Option<f32>) -> TextLayout
pub fn layout_styled(&self, content: &str, font_size: f32, max_width: Option<f32>,
style_at: impl Fn(usize) -> GlyphStyle) -> StyledLayout

The outline-producing layout passes the whiteboard’s renderer feeds to gpui’s PathBuilder. They are pub, but their types (TextLayout, StyledLayout, GlyphStyle, Seg, Decoration, DecoKind) live in the crate’s private font module and are not re-exported — so outside the crate layout_wrapped’s result can’t be named (usable only via inference), and layout_styled can’t be called at all (you can’t produce a GlyphStyle). Treat both as internal; the supported external surface is the measure/caret/selection methods above. layout_styled bakes synthetic italic (shear) and bold (doubled, offset pass) into the outlines and emits underline/strike/highlight runs as decorations; wrapping and caret geometry are identical to the unstyled path.