Patterns / Media
Canvas
Markdown apps/gallery/src/patterns/canvas.rs
editor::init(cx);
canvas::init(cx); // after editor::init
canvas::set_kinds(cx, Kinds::new().with("session", SESSION)); // optional
let view = cx.new(|cx| CanvasView::new(Canvas::parse(json)?, cx));
cx.subscribe(&view, |_, view, event, cx| {
if let CanvasEvent::Changed(_) = event {
save(view.read(cx).canvas().to_json());
}
}).detach();The document is JSON Canvas 1.0. Fields the spec does not name are kept and written back, so a file another app wrote survives a save.
Your own kinds
const SESSION: Kind = Kind {
render: |node, zoom, window, cx| sessions(cx).view(&node.id).into_any_element(),
sizing: Sizing::Fixed, // or Grows: as tall as the content
chrome: Chrome::Card, // Outline, or Bare: the content is the node
edit: Some(Field { read: title, write: set_title }), // what f2 edits
child: kind::blank, // what tab makes under it
};A kind is keyed by the node's type. The spec's four — kind::TEXT, FILE, LINK, GROUP — are replaceable the same way, or a starting point: Kind { chrome: Chrome::Bare, ..kind::TEXT }.
State stays with the app: look the view up by the node's id. gpui cannot transform an element, so zoom is a scale the content applies itself — canvas::text_style(div(), TextStyle::Callout, zoom) sets size, leading and weight together, and Typography::scaled does a whole document. A node of fixed size clips what it paints.
Changes
CanvasView::new(doc, cx).with_changes(|canvas, change, cx| match &change {
Change::Remove { id } if is_running(id, cx) => None, // refuse
_ => Some(change), // or rewrite it
})Every edit — a key, a drop, typing in a node — is a Change: Add, Move, Reparent, Detach, Remove, Unpin or Update. The filter answers what lands, and CanvasEvent::Changed carries what did. It runs inside the view's update, so reach the view through cx.defer; view.apply(change, cx) lands one of the app's own past the filter.
Dragging a node
CanvasView::new(doc, cx).with_drag(canvas::drag::reparent);
fn my_drag(canvas: &Canvas, drag: &Drag) -> Vec<Change> {
// drag.id, drag.to(), drag.over, drag.phase (Move…, then one Drop)
}On a move, the handler's Moves are applied as they come and the rest are drawn as what the drop would do: a ring on the new parent, the connector it would make, faded connectors it would cut. The drop's changes go through the filter, with the node put back first, so a refused drop leaves it where it was. Nodes a layout moves glide there. drag::pin (the default) leaves the node where it lands and marks it "pinned": true; drag::reparent hangs it under the node it is dropped on; drag::detach cuts its edges in.
Keys
tab adds a child (under the first root when nothing is selected), enter a sibling, backspace removes a branch, f2 or a double-click edits, arrows walk the tree, escape leaves a node. cmd-=, cmd-- and cmd-0 zoom; a pinch or a cmd-wheel zooms at the pointer, and a drag or a wheel pans.
API
impl CanvasView {
pub fn new(canvas: Canvas, cx: &mut Context<Self>) -> Self;
/// `Arrange::Free` keeps positions as the document has them.
pub fn with_arrange(self, arrange: Arrange) -> Self;
pub fn with_drag(self, handler: DragHandler) -> Self;
pub fn with_changes(self, filter: impl Fn(&Canvas, Change, &mut App) -> Option<Change> + 'static) -> Self;
pub fn canvas(&self) -> &Canvas;
pub fn set_canvas(&mut self, canvas: Canvas, cx: &mut Context<Self>);
/// Through the filter, as if the reader made it. A toolbar's way in.
pub fn submit(&mut self, change: Change, cx: &mut Context<Self>) -> bool;
/// Past the filter.
pub fn apply(&mut self, change: Change, cx: &mut Context<Self>);
pub fn selected(&self) -> Option<&str>;
pub fn select(&mut self, id: Option<String>, cx: &mut Context<Self>);
/// What `backspace` does.
pub fn remove_selected(&mut self, cx: &mut Context<Self>);
pub fn zoom(&self) -> f32;
pub fn set_zoom(&mut self, zoom: f32, cx: &mut Context<Self>);
/// What `cmd-=` and `cmd--` do.
pub fn zoom_in(&mut self, cx: &mut Context<Self>);
pub fn zoom_out(&mut self, cx: &mut Context<Self>);
/// Centre the document again, as it opened.
pub fn fit(&mut self, cx: &mut Context<Self>);
/// The canvas point under the middle of the view.
pub fn center(&self) -> (i64, i64);
/// Turning `Arrange::Mindmap` on drops every pin and lays the trees out again.
pub fn set_arrange(&mut self, arrange: Arrange, cx: &mut Context<Self>);
pub fn set_drag(&mut self, handler: DragHandler);
}
// canvas::mindmap — pure; the builders answer a Change to submit
pub fn layout(canvas: &mut Canvas);
pub fn child(canvas: &Canvas, parent: &str, node: Node) -> Option<Change>;
pub fn sibling(canvas: &Canvas, of: &str, node: Node) -> Option<Change>;
pub fn root(canvas: &Canvas, node: Node, at: (i64, i64)) -> Change;
// canvas::change — pure
pub fn apply(canvas: &mut Canvas, change: &Change);The source is at apps/gallery/src/patterns/canvas.rs. Copy the file.