Codebase overview

Workshop: Intro to Coding for Graphite

The Graphite editor is built as a web app powered by Svelte in the frontend and Rust in the backend which is compiled to WebAssembly (wasm) and run in the browser.

The Editor's frontend web code lives in /frontend/src and the backend Rust code lives in /editor. The web-based frontend is intended to be semi-temporary and eventually replaceable with a pure-Rust GUI frontend. Therefore, all backend code should be unaware of JavaScript or web concepts and all Editor application logic should be written in Rust not JS.

Frontend/backend communication

Frontend (JS) -> backend (Rust/wasm) communication is achieved through a thin Rust translation layer in /frontend/wasm/src/editor_api.rs which wraps the Editor backend's complex Rust data type API and provides the JS with a simpler API of callable functions. These wrapper functions are compiled by wasm-bindgen into autogenerated JS functions that serve as an entry point into the wasm.

Backend (Rust) -> frontend (JS) communication happens by sending a queue of messages to the frontend message dispatcher. After the JS calls any wrapper API function to get into backend (Rust) code execution, the Editor's business logic runs and queues up FrontendMessages (defined in /editor/src/messages/frontend/frontend_message.rs) which get mapped from Rust to JS-friendly data types in /frontend/src/wasm-communication/messages.ts. Various JS code subscribes to these messages by calling subscribeJsMessage(MessageName, (messageData) => { /* callback code */ });.

The Editor backend and Legacy Document modules

The Graphite editor backend handles all the day-to-day logic and responsibilities of a user-facing interactive application. Some duties include: user input, GUI state management, viewport tool behavior, layer management and selection, and handling of multiple document tabs.

The actual document (the artwork data and layers included in a saved .graphite file) is part of another core module located in /document-legacy. The (soon-to-be-replaced) Legacy Document codebase manages a user's document. Once it is replaced, the new Document module (that will be located in /document) will store a document's node graph and change history. While it's OK for the Editor to read data from—or make immutable function calls upon—the user's document controlled by the Legacy Document module, it should never be directly mutated. Instead, messages (called Operations) should be sent to the document to request changes occur. The Legacy Document code is designed to be used by the Editor or by third-party Rust or C/C++ code directly so a careful separation of concerns between the Editor and Legacy Document modules should be considered.

The message bus

Every part of the Graphite stack works based on the concept of message passing. Messages are pushed to the front or back of a queue and each one is processed by the module's dispatcher in the order encountered. Only the dispatcher owns a mutable reference to update its module's state.

Additional technical details

A message is an enum variant of a certain message sub-type like FrontendMessage, ToolMessage, PortfolioMessage, or DocumentMessage. Two example messages:

// Carries no data
DocumentMessage::DeleteSelectedLayers

// Carries a layer path and a string as data
DocumentMessage::RenameLayer(Vec<LayerId>, String)

Message sub-types hierarchically wrap other message sub-types; for example, DocumentMessage is wrapped by PortfolioMessage via:

// Carries the child message as data
PortfolioMessage::Document(DocumentMessage)

and EllipseMessage is wrapped by ToolMessage via:

// Carries the child message as data
ToolMessage::Ellipse(EllipseMessage)

Every message sub-type is wrapped by the top-level Message, so the previous example is actually:

Message::Tool(ToolMessage::Ellipse(EllipseMessage))

Because this is cumbersome, we have a proc macro #[child] that automatically implements the From trait on message sub-types and lets you write:

DocumentMessage::DeleteSelectedLayers.into()

instead of:

Message(PortfolioMessage::Document(DocumentMessage::DeleteSelectedLayers))