Skip to content
Open
260 changes: 260 additions & 0 deletions docs/svelte-rust-connection.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
# How the Svelte UI is Connected to the Rust Code in Graphite

The connection between Svelte and Rust in Graphite is achieved through **WebAssembly (WASM)** using **wasm-bindgen** as the bridge. This document explains the architecture, implementation, and communication flow between the frontend and backend.

## Architecture Overview

```
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ Svelte UI │◄──►│ WASM Bridge │◄──►│ Rust Editor │
│ (Frontend) │ │ (wasm-bindgen) │ │ (Backend) │
└─────────────────┘ └──────────────────┘ └─────────────────┘
```

## WASM Bridge Layer

The bridge is implemented in `frontend/wasm/src/`:

- **`lib.rs`**: Main WASM entry point that initializes the editor backend
- **`editor_api.rs`**: Contains the `EditorHandle` struct with functions callable from JavaScript
- The Rust code is compiled to WASM using `wasm-pack`

## Build Process

The build process is defined in `frontend/package.json`:

```javascript
"wasm:build-dev": "wasm-pack build ./wasm --dev --target=web",
"start": "npm run wasm:build-dev && concurrently \"vite\" \"npm run wasm:watch-dev\""
```

The build flow:
1. **Rust → WASM**: `wasm-pack` compiles the Rust code in `frontend/wasm/` to WebAssembly
2. **WASM → JS bindings**: `wasm-bindgen` generates JavaScript bindings
3. **Svelte app**: Vite builds the Svelte frontend and imports the WASM module

## Connection Flow

### Initialization (`main.ts` → `App.svelte` → `editor.ts`)

```typescript
// frontend/src/editor.ts
export async function initWasm() {
// Skip if the WASM module is already initialized
if (wasmImport !== undefined) return;

// Import the WASM module JS bindings
const wasm = await init();
wasmImport = await wasmMemory();

// Set random seed for the Rust backend
const randomSeed = BigInt(Math.floor(Math.random() * Number.MAX_SAFE_INTEGER));
setRandomSeed(randomSeed);
}
```

### Editor Creation

```typescript
// frontend/src/editor.ts
export function createEditor(): Editor {
const raw: WebAssembly.Memory = wasmImport;

// Create the EditorHandle - this is the main bridge to Rust
const handle: EditorHandle = new EditorHandle((messageType, messageData) => {
// This callback handles messages FROM Rust TO JavaScript
subscriptions.handleJsMessage(messageType, messageData, raw, handle);
});

return { raw, handle, subscriptions };
}
```

## Message-Based Communication

The communication uses a **bidirectional message system**:

### JavaScript → Rust (Function Calls)

JavaScript calls functions on the `EditorHandle` (defined in `editor_api.rs`):

```rust
// frontend/wasm/src/editor_api.rs
#[wasm_bindgen(js_name = onMouseMove)]
pub fn on_mouse_move(&self, x: f64, y: f64, mouse_keys: u8, modifiers: u8) {
let editor_mouse_state = EditorMouseState::from_keys_and_editor_position(mouse_keys, (x, y).into());
let modifier_keys = ModifierKeys::from_bits(modifiers).expect("Invalid modifier keys");
let message = InputPreprocessorMessage::PointerMove { editor_mouse_state, modifier_keys };
self.dispatch(message); // Send to Rust backend
}
```

### Rust → JavaScript (Message System)

Rust sends `FrontendMessage`s back to JavaScript via the callback:

```rust
// Rust sends a message
self.send_frontend_message_to_js(FrontendMessage::UpdateDocumentArtwork { svg });
```

Messages are handled by the subscription router:

```typescript
// frontend/src/subscription-router.ts
const handleJsMessage = (messageType: JsMessageType, messageData: Record<string, unknown>) => {
const messageMaker = messageMakers[messageType];
const message = plainToInstance(messageMaker, messageData);
const callback = subscriptions[message.constructor.name];
callback(message); // Call the registered Svelte handler
};
```

## State Management

Svelte components use **state providers** that subscribe to Rust messages:

```typescript
// frontend/src/components/Editor.svelte
// State provider systems
let dialog = createDialogState(editor);
let document = createDocumentState(editor);
let fonts = createFontsState(editor);
let fullscreen = createFullscreenState(editor);
let nodeGraph = createNodeGraphState(editor);
let portfolio = createPortfolioState(editor);
let appWindow = createAppWindowState(editor);
```

Each state provider:
- Subscribes to specific `FrontendMessage` types from Rust
- Updates Svelte stores when messages are received
- Provides reactive state to Svelte components

## Message Types and Transformation

Messages are defined in `frontend/src/messages.ts` using class-transformer:

```typescript
export class UpdateDocumentArtwork extends JsMessage {
readonly svg!: string;
}

export class UpdateActiveDocument extends JsMessage {
readonly documentId!: bigint;
}

export class Color {
readonly red!: number;
readonly green!: number;
readonly blue!: number;
readonly alpha!: number;
readonly none!: boolean;

// Methods for color conversion and manipulation
}
```

## Practical Example: Layer Selection

When a user clicks on a layer in the UI:

1. **Svelte**: User clicks layer → calls `editor.handle.selectLayer(id)`

2. **WASM Bridge**: JavaScript function maps to Rust `select_layer()`:
```rust
#[wasm_bindgen(js_name = selectLayer)]
pub fn select_layer(&self, id: u64, ctrl: bool, shift: bool) {
let id = NodeId(id);
let message = DocumentMessage::SelectLayer { id, ctrl, shift };
self.dispatch(message);
}
```

3. **Rust**: Processes selection → updates document state → sends `FrontendMessage::UpdateDocumentLayerDetails`

4. **WASM Bridge**: Message serialized and sent to JavaScript callback

5. **Svelte**: State provider receives message → updates reactive store → UI re-renders

## Key Files

### Frontend (TypeScript/Svelte)
- `frontend/src/main.ts` - Entry point
- `frontend/src/App.svelte` - Root Svelte component
- `frontend/src/editor.ts` - WASM initialization and editor creation
- `frontend/src/messages.ts` - Message type definitions
- `frontend/src/subscription-router.ts` - Message routing system
- `frontend/src/components/Editor.svelte` - Main editor component
- `frontend/src/state-providers/` - Reactive state management

### WASM Bridge (Rust)
- `frontend/wasm/src/lib.rs` - WASM module entry point
- `frontend/wasm/src/editor_api.rs` - Main API bridge with JavaScript-callable functions
- `frontend/wasm/Cargo.toml` - WASM module configuration

### Backend (Rust)
- `editor/` - Main editor backend implementation
- `editor/src/messages/` - Message handling system
- `node-graph/` - Node graph processing
- `libraries/` - Shared libraries

## Configuration Files

### Build Configuration
- `frontend/vite.config.ts` - Vite build configuration
- `frontend/package.json` - NPM dependencies and scripts
- `frontend/wasm/Cargo.toml` - WASM compilation settings
- `Cargo.toml` - Root workspace configuration

### Development
- `frontend/.gitignore` - Frontend-specific ignores
- `frontend/tsconfig.json` - TypeScript configuration
- `rustfmt.toml` - Rust formatting rules

## Key Benefits

- **Performance**: Core editor logic runs in compiled Rust (fast)
- **Memory Safety**: Rust prevents crashes and memory leaks
- **Reactivity**: Svelte provides modern reactive UI
- **Type Safety**: Both ends are strongly typed with message contracts
- **Modularity**: Clear separation between UI and business logic
- **Hot Reload**: Development server supports hot reload for both Rust and Svelte changes

## Development Workflow

1. **Setup**: Run `npm run start` in `frontend/` directory
2. **WASM Build**: `wasm-pack` compiles Rust to WASM automatically
3. **Hot Reload**: Changes to Rust or Svelte code trigger automatic rebuilds
4. **Debugging**: Use browser dev tools for frontend, `log::debug!()` for Rust backend
5. **Testing**: Build with `cargo build` and test with browser

## Message Flow Diagram

```
User Interaction (Svelte)
JavaScript Function Call
EditorHandle Method (WASM Bridge)
Rust Message Dispatch
Editor Backend Processing
FrontendMessage Generation
WASM Serialization
JavaScript Callback
Subscription Router
State Provider Update
Svelte Store Update
UI Re-render
```

This architecture enables Graphite to deliver a native-like performance experience in the browser while maintaining the benefits of modern web development practices with Svelte's reactive framework.
4 changes: 2 additions & 2 deletions editor/src/messages/tool/tool_messages/pen_tool.rs
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove theses debug changes

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please check now

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You've committed more things by accident. In the future before you ping with comment please check yourself if the changes in the File Changes tab of the pr only include what you what to be part of the pr.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really sorry , the pr got quite messy I will make sure of it in the coming pull requests

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No worries. :)

Original file line number Diff line number Diff line change
Expand Up @@ -662,7 +662,6 @@ impl PenToolData {
let Some(pos) = vector.point_domain.position_from_id(id) else { continue };
let transformed_distance_between_squared = transform.transform_point2(pos).distance_squared(transform.transform_point2(self.next_point));
let snap_point_tolerance_squared = crate::consts::SNAP_POINT_TOLERANCE.powi(2);

if transformed_distance_between_squared < snap_point_tolerance_squared {
self.update_handle_type(TargetHandle::PreviewInHandle);
self.handle_end_offset = None;
Expand All @@ -684,6 +683,7 @@ impl PenToolData {
let document = snap_data.document;
let next_handle_start = self.next_handle_start;
let handle_start = self.latest_point()?.handle_start;

let mouse = snap_data.input.mouse.position;
self.handle_swapped = false;
self.handle_end_offset = None;
Expand Down Expand Up @@ -1450,6 +1450,7 @@ impl Fsm for PenToolFsmState {
tool_options: &Self::ToolOptions,
responses: &mut VecDeque<Message>,
) -> Self {

let ToolActionMessageContext {
document,
global_tool_data,
Expand Down Expand Up @@ -1478,7 +1479,6 @@ impl Fsm for PenToolFsmState {
match (self, event) {
(PenToolFsmState::PlacingAnchor | PenToolFsmState::GRSHandle, PenToolMessage::GRS { grab, rotate, scale }) => {
let Some(layer) = layer else { return PenToolFsmState::PlacingAnchor };

let Some(latest) = tool_data.latest_point() else { return PenToolFsmState::PlacingAnchor };
if latest.handle_start == latest.pos {
return PenToolFsmState::PlacingAnchor;
Expand Down
13 changes: 10 additions & 3 deletions frontend/src/components/window/workspace/Panel.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
export let panelType: PanelType | undefined = undefined;
export let clickAction: ((index: number) => void) | undefined = undefined;
export let closeAction: ((index: number) => void) | undefined = undefined;
export let dblclickEmptySpaceAction : (() => void) | undefined = undefined;

let className = "";
export { className as class };
Expand Down Expand Up @@ -100,7 +101,8 @@
</script>

<LayoutCol on:pointerdown={() => panelType && editor.handle.setActivePanel(panelType)} class={`panel ${className}`.trim()} {classes} style={styleName} {styles}>
<LayoutRow class="tab-bar" classes={{ "min-widths": tabMinWidths }}>
<LayoutRow class="tab-bar" classes={{ "min-widths": tabMinWidths }} >

<LayoutRow class="tab-group" scrollableX={true}>
{#each tabLabels as tabLabel, tabIndex}
<LayoutRow
Expand Down Expand Up @@ -148,11 +150,14 @@
{/if}
</LayoutRow>
{/each}
<LayoutRow class="tab-group-empty-space" on:dblclick={dblclickEmptySpaceAction}></LayoutRow>
</LayoutRow>

<!-- <PopoverButton style="VerticalEllipsis">
<TextLabel bold={true}>Panel Options</TextLabel>
<TextLabel multiline={true}>Coming soon</TextLabel>
</PopoverButton> -->

</LayoutRow>
<LayoutCol class="panel-body">
{#if panelType}
Expand Down Expand Up @@ -218,13 +223,15 @@
background: var(--color-1-nearblack);
border-radius: 6px;
overflow: hidden;

.tab-bar {
height: 28px;
min-height: auto;
background: var(--color-1-nearblack); // Needed for the viewport hole punch on desktop
flex-shrink: 0;

.tab-group-empty-space {
width: 100%;
}
&.min-widths .tab-group .tab {
min-width: 120px;
max-width: 360px;
Expand Down
1 change: 1 addition & 0 deletions frontend/src/components/window/workspace/Workspace.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@
tabCloseButtons={true}
tabMinWidths={true}
tabLabels={documentTabLabels}
dblclickEmptySpaceAction={()=>editor.handle.newDocumentDialog()}
clickAction={(tabIndex) => editor.handle.selectDocument($portfolio.documents[tabIndex].id)}
closeAction={(tabIndex) => editor.handle.closeDocumentWithConfirmation($portfolio.documents[tabIndex].id)}
tabActiveIndex={$portfolio.activeDocumentIndex}
Expand Down