Add editor structure outline to the developer guide on the website

This commit is contained in:
Keavon Chambers
2025-07-12 23:09:29 -07:00
parent 765092fbe9
commit d6d1bbb1e6
18 changed files with 426 additions and 558 deletions

View File

@@ -32,190 +32,28 @@ Graphite is built from several main software components. New developers may choo
### Frontend
*Location: `/frontend/src`*
*Location: [`/frontend/src`](https://github.com/GraphiteEditor/Graphite/tree/master/frontend/src)*
The frontend is the interface for Graphite which users see and interact with. It is built using web technologies with TypeScript and Svelte (HTML and SCSS). The frontend's philosophy is to be as lightweight and minimal as possible. It acts as the entry point for user input and then quickly hands off its work to the WebAssembly editor backend via its Wasm wrapper API. That API is written in Rust but has TypeScript bindings generated by the [wasm-bindgen](https://github.com/rustwasm/wasm-bindgen) tooling that is part of the Vite-based build chain. The frontend is built of many components that recursively form the window, panels, and widgets that make up the user interface.
The frontend is the GUI for Graphite which users see and interact with. It is built using web technologies with TypeScript and Svelte (HTML and SCSS). The frontend's philosophy is to be as lightweight and minimal as possible. It acts as the entry point for user input and then quickly hands off its work to the WebAssembly editor backend via its [Wasm wrapper API](https://github.com/GraphiteEditor/Graphite/tree/master/frontend/wasm). That API is written in Rust but has TypeScript bindings generated by the [wasm-bindgen](https://github.com/rustwasm/wasm-bindgen) tooling that is part of the Vite-based build toolchain. The frontend is built of many [components](https://github.com/GraphiteEditor/Graphite/tree/master/frontend/src/components) that recursively form the window, panels, and widgets that make up the user interface.
### Editor
*Location: `/editor`*
*Location: [`/editor`](https://github.com/GraphiteEditor/Graphite/tree/master/editor)*
The editor is the core of the Graphite application, and it's where all the business logic occurs for the tooling and user interaction. It is written in Rust and compiled to WebAssembly. At its heart is the message system described below. It is responsible for communicating with Graphene as well as handling the actual logic, state, tooling, and responsibilities of the interactive application.
[The editor](./editor-structure) is the core of the Graphite application, and it's where all the business logic occurs for the tooling and user interaction. It is written in Rust and compiled to WebAssembly. At its heart is the message system. It is responsible for communicating with Graphene as well as handling the actual logic, state, tooling, and responsibilities of the interactive application.
### Graphene
*Location: `/node-graph`*
*Location: [`/node-graph`](https://github.com/GraphiteEditor/Graphite/tree/master/node-graph)*
[Graphene](../graphene/) is the node graph engine which manages and renders the documents. It is itself a programming language, where Graphene programs are compiled while being edited live by the user, and where executing the program renders the document.
## Frontend/backend communication
Frontend-to-backend communication is achieved through a thin Rust translation layer in `/frontend/wasm/src/editor_api.rs` which wraps the editor backend's Rust-based message system API and provides the TypeScript-compatible API of callable functions. These wrapper functions are compiled by [wasm-bindgen](https://github.com/rustwasm/wasm-bindgen) into autogenerated TS functions that serve as an entry point from TS into the Wasm binary.
Frontend-to-backend communication is achieved through a thin Rust translation layer in [`/frontend/wasm/src/editor_api.rs`](https://github.com/GraphiteEditor/Graphite/tree/master/frontend/wasm/src/editor_api.rs) which wraps the editor backend's Rust-based message system API and provides the TypeScript-compatible API of callable functions. These wrapper functions are compiled by [wasm-bindgen](https://github.com/rustwasm/wasm-bindgen) into autogenerated TS functions that serve as an entry point from TS into the Wasm binary.
Backend-to-frontend communication happens by sending a queue of messages to the frontend message dispatcher. After the TS has called any wrapper API function to get into backend code execution, the editor's business logic runs and queues up `FrontendMessage`s (defined in `/editor/src/messages/frontend/frontend_message.rs`) which get mapped from Rust to TS-friendly data types in `/frontend/src/messages.ts`. Various TS code subscribes to these messages by calling `subscribeJsMessage(MessageName, (messageData) => { /* callback code */ });`.
## The message system
The Graphite editor backend is organized into a hierarchy of subsystems, called *message handlers*, which talk to one another through message passing. Messages are pushed to the front or back of a queue and each one is processed sequentially by the backend's dispatcher.
The dispatcher lives at the root of the application hierarchy and it owns its message handlers. Thus, Rust's restrictions on mutable borrowing are satisfied because only the dispatcher mutably borrows its message handlers, one at a time, while each message is processed.
### Messages
Messages are enum variants that are dispatched to perform some intended activity within their respective message handlers. Here are two `DocumentMessage` definitions:
```rs
pub enum DocumentMessage {
...
// A message that carries one named data field
DeleteLayer {
id: NodeId,
}
// A message that carries no data
DeleteSelectedLayers,
...
}
```
As shown above, additional data fields can be included with each message. But as a special case denoted by the `#[child]` attribute, that data can also be a sub-message enum, which enables hierarchical nesting of message handler subsystems.
<details>
<summary>To view the hierarchical subsystem file structure: click here</summary>
<!--
Generated with:
cd editor/src/messages
tree -P '*_message.rs|*_message_handler.rs|*_tool.rs' --prune
Then the first line's "." was replaced with "messages"
-->
```
messages
├── broadcast
│   ├── broadcast_message.rs
│   └── broadcast_message_handler.rs
├── debug
│   ├── debug_message.rs
│   └── debug_message_handler.rs
├── dialog
│   ├── dialog_message.rs
│   ├── dialog_message_handler.rs
│   ├── export_dialog
│   │   ├── export_dialog_message.rs
│   │   └── export_dialog_message_handler.rs
│   ├── new_document_dialog
│   │   ├── new_document_dialog_message.rs
│   │   └── new_document_dialog_message_handler.rs
│   └── preferences_dialog
│   ├── preferences_dialog_message.rs
│   └── preferences_dialog_message_handler.rs
├── frontend
│   └── frontend_message.rs
├── globals
│   ├── globals_message.rs
│   └── globals_message_handler.rs
├── input_mapper
│   ├── input_mapper_message.rs
│   ├── input_mapper_message_handler.rs
│   └── key_mapping
│   ├── key_mapping_message.rs
│   └── key_mapping_message_handler.rs
├── input_preprocessor
│   ├── input_preprocessor_message.rs
│   └── input_preprocessor_message_handler.rs
├── layout
│   ├── layout_message.rs
│   └── layout_message_handler.rs
├── portfolio
│   ├── document
│   │   ├── document_message.rs
│   │   ├── document_message_handler.rs
│   │   ├── graph_operation
│   │   │   ├── graph_operation_message.rs
│   │   │   └── graph_operation_message_handler.rs
│   │   ├── navigation
│   │   │   ├── navigation_message.rs
│   │   │   └── navigation_message_handler.rs
│   │   ├── node_graph
│   │   │   ├── node_graph_message.rs
│   │   │   └── node_graph_message_handler.rs
│   │   ├── overlays
│   │   │   ├── overlays_message.rs
│   │   │   └── overlays_message_handler.rs
│   │   └── properties_panel
│   │   ├── properties_panel_message.rs
│   │   └── properties_panel_message_handler.rs
│   ├── menu_bar
│   │   ├── menu_bar_message.rs
│   │   └── menu_bar_message_handler.rs
│   ├── portfolio_message.rs
│   └── portfolio_message_handler.rs
├── preferences
│   ├── preferences_message.rs
│   └── preferences_message_handler.rs
├── tool
│   ├── tool_message.rs
│   ├── tool_message_handler.rs
│   ├── tool_messages
│   │   ├── artboard_tool.rs
│   │   ├── brush_tool.rs
│   │   ├── ellipse_tool.rs
│   │   ├── eyedropper_tool.rs
│   │   ├── fill_tool.rs
│   │   ├── freehand_tool.rs
│   │   ├── gradient_tool.rs
│   │   ├── line_tool.rs
│   │   ├── navigate_tool.rs
│   │   ├── path_tool.rs
│   │   ├── pen_tool.rs
│   │   ├── polygon_tool.rs
│   │   ├── rectangle_tool.rs
│   │   ├── select_tool.rs
│   │   ├── spline_tool.rs
│   │   └── text_tool.rs
│   └── transform_layer
│   ├── transform_layer_message.rs
│   └── transform_layer_message_handler.rs
└── workspace
├── workspace_message.rs
└── workspace_message_handler.rs
```
</details>
By convention, regular data must be written as struct-style named fields (shown above), while a sub-message enum must be written as a tuple/newtype-style field (shown below). The `DocumentMessage` enum of the previous example is defined as a child of `PortfolioMessage` which wraps it like this:
Backend-to-frontend communication happens by sending a queue of messages to the frontend message dispatcher. After the TS has called any wrapper API function to get into backend code execution, the editor's business logic runs and queues up each [`FrontendMessage`](https://github.com/GraphiteEditor/Graphite/tree/master/editor/src/messages/frontend/frontend_message.rs) which get mapped from Rust to JavaScript data structures in [`/frontend/src/messages.ts`](https://github.com/GraphiteEditor/Graphite/tree/master/frontend/src/messages.ts). Various TS code subscribes to these messages by calling:
```rs
pub enum PortfolioMessage {
...
// A message that carries the `DocumentMessage` child enum as data
#[child]
Document(DocumentMessage),
...
}
subscribeJsMessage(MessageName, (messageData) => { /* callback code */ });
```
Likewise, the `PortfolioMessage` enum is wrapped by the top-level `Message` enum. The dispatcher operates on the queue of these base-level `Message` types.
So for example, the `DeleteSelectedLayers` message mentioned previously will look like this as a `Message` data type:
```rs
Message::Portfolio(
PortfolioMessage::Document(
DocumentMessage::DeleteSelectedLayers
)
)
```
Writing out these nested message enum variants would be cumbersome, so that `#[child]` attribute shown earlier invokes a proc macro that automatically implements the `From` trait, letting you write this instead to get a `Message` data type:
```rs
DocumentMessage::DeleteSelectedLayers.into()
```
Most often, this is simplified even further because the `.into()` is called for you when pushing a message to the queue with `.add()` or `.add_front()`. So this becomes as simple as:
```rs
responses.add(DocumentMessage::DeleteSelectedLayers);
```
The `responses` message queue is composed of `Message` data types, and thanks to this system, child messages like `DocumentMessage::DeleteSelectedLayers` are automatically wrapped in their ancestor enum variants to become a `Message`, saving you from writing the verbose nested form.

View File

@@ -2,7 +2,7 @@
title = "Debugging tips"
[extra]
order = 4 # Page number after chapter intro
order = 2 # Page number after chapter intro
+++
The Wasm-based editor has some unique limitations about how you are able to debug it. This page offers tips and best practices to get the most out of your problem-solving efforts.

View File

@@ -0,0 +1,98 @@
+++
title = "Editor structure"
[extra]
order = 1 # Page number after chapter intro
css = ["/page/developer-guide-editor-structure.css"]
js = ["/js/developer-guide-editor-structure.js"]
+++
The Graphite editor is the application users interact with to create documents. Its code is one Rust crate sandwiched between the frontend and Graphene, the node-based graphics engine. The main business logic of all visual editing is handled by the editor backend. When running in the browser, it is compiled to WebAssembly and passes messages to the frontend.
## Message system
The Graphite editor backend is organized into a hierarchy of subsystems which talk to one another through message passing. Messages are pushed to the front or back of a queue and each one is processed sequentially by the editor's dispatcher.
The dispatcher lives at the root of the editor hierarchy and acts as the owner of all its top-level message handlers. This satisfies Rust's restrictions on mutable borrows because only the dispatcher may mutate its message handlers, one at a time, while each message is processed.
## Editor outline
Click to explore the outline of the editor subsystem hierarchy which forms the structure of the editor's subsystems, state, and interactions. Bookmark this page to reference it later.
<div class="structure-outline">
<!-- replacements::hierarchical_message_system_tree() -->
</div>
### Parts of the hierarchy
<span class="subsystem">Subsystem components</span>
- A <span class="subsystem">*Message</span> enum is the component of an editor subsystem that defines its message interfaces as enum variants. Messages are used for passing a request from anywhere in the application, optionally with some included data, to have a particular block of code be run by its respective message handler.
- A <span class="subsystem">*MessageHandler</span> struct is the component of an editor subsystem that has ownership over its persistent editor state and its child message handlers for the lifetime of the application. It also defines the logic for handling each of its messages that it receives from the dispatcher. Those blocks of logic may further enqueue additional messages to be processed by itself or other message handlers during the same dispatch cycle.
- A <span class="subsystem">*MessageContext</span> struct is the component of an editor subsystem that defines what data is made available from other subsystems when running the logic to handle a dispatched message. It is a struct that is passed to the message handler when processing a message, and it gets filled in with data (owned, borrowed, or mutably borrowed) from its parent message handler. Intermediate subsystem layers may forward data from their parent to their child contexts to make state available from further up the hierarchy.
<span class="submessage">Sub-messages</span>
- A <span class="submessage">#[child] *</span> attribute-decorated message enum variant is a special kind of message that encapsulates a nested subsystem. As with all messages, its handler has a manually written code block. But that code must call its corresponding child message handler's `process_message` method. The child message handler is a field of this parent message handler's state struct.
`Messages`
- A `*` message enum variant is used throughout the editor to request that a certain subsystem performs some action, potentially given some data. In that sense, it resembles a function call, but a key difference is that messages are queued up and processed sequentially in a flat order, always invoked by the dispatcher.
## How messages work
Messages are enum variants that are dispatched to perform some intended activity within their respective message handlers. Here are two <span class="subsystem">DocumentMessage</span> definitions:
```rs
pub enum DocumentMessage {
...
// A message that carries one named data field
DeleteLayer {
id: NodeId,
}
// A message that carries no data
DeleteSelectedLayers,
...
}
```
As shown above, additional data fields can be included with each message. But as a special case denoted by the <span class="submessage">#[child]</span> attribute, that data can also be a sub-message enum, which enables hierarchical nesting of message handler subsystems.
By convention, regular data must be written as struct-style named fields (shown above), while a sub-message enum must be written as a tuple/newtype-style field (shown below). The <span class="subsystem">DocumentMessage</span> enum of the previous example is defined as a child of <span class="subsystem">PortfolioMessage</span> which wraps it like this:
```rs
pub enum PortfolioMessage {
...
// A message that carries the `DocumentMessage` child enum as data
#[child]
Document(DocumentMessage),
...
}
```
Likewise, the <span class="subsystem">PortfolioMessage</span> enum is wrapped by the top-level <span class="subsystem">Message</span> enum. The dispatcher operates on the queue of these base-level <span class="subsystem">Message</span> types.
So for example, the `DeleteSelectedLayers` message mentioned previously will look like this as a <span class="subsystem">Message</span> data type:
```rs
Message::Portfolio(
PortfolioMessage::Document(
DocumentMessage::DeleteSelectedLayers
)
)
```
Writing out these nested message enum variants would be cumbersome, so that <span class="submessage">#[child]</span> attribute shown earlier invokes a proc macro that automatically implements the `From` trait, letting you write this instead to get a <span class="subsystem">Message</span> data type:
```rs
DocumentMessage::DeleteSelectedLayers.into()
```
Most often, this is simplified even further because the `.into()` is called for you when pushing a message to the queue with `.add()` or `.add_front()`. So this becomes as simple as:
```rs
responses.add(DocumentMessage::DeleteSelectedLayers);
```
The `responses` message queue is composed of <span class="subsystem">Message</span> data types, and thanks to this system, child messages like `DocumentMessage::DeleteSelectedLayers` are automatically wrapped in their ancestor enum variants to become a <span class="subsystem">Message</span>, saving you from writing the verbose nested form.