diff --git a/docs/svelte-rust-connection.md b/docs/svelte-rust-connection.md new file mode 100644 index 0000000000..3c63ca996a --- /dev/null +++ b/docs/svelte-rust-connection.md @@ -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) => { + 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. \ No newline at end of file diff --git a/editor/src/messages/tool/tool_messages/pen_tool.rs b/editor/src/messages/tool/tool_messages/pen_tool.rs index 0abab93f48..72982c9a04 100644 --- a/editor/src/messages/tool/tool_messages/pen_tool.rs +++ b/editor/src/messages/tool/tool_messages/pen_tool.rs @@ -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; @@ -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; @@ -1450,6 +1450,7 @@ impl Fsm for PenToolFsmState { tool_options: &Self::ToolOptions, responses: &mut VecDeque, ) -> Self { + let ToolActionMessageContext { document, global_tool_data, @@ -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; diff --git a/frontend/src/components/window/workspace/Panel.svelte b/frontend/src/components/window/workspace/Panel.svelte index 66247cbc65..7eeb046df5 100644 --- a/frontend/src/components/window/workspace/Panel.svelte +++ b/frontend/src/components/window/workspace/Panel.svelte @@ -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 }; @@ -100,7 +101,8 @@ panelType && editor.handle.setActivePanel(panelType)} class={`panel ${className}`.trim()} {classes} style={styleName} {styles}> - + + {#each tabLabels as tabLabel, tabIndex} {/each} + + + {#if panelType} @@ -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; diff --git a/frontend/src/components/window/workspace/Workspace.svelte b/frontend/src/components/window/workspace/Workspace.svelte index 266b0bc412..af61b0bf3c 100644 --- a/frontend/src/components/window/workspace/Workspace.svelte +++ b/frontend/src/components/window/workspace/Workspace.svelte @@ -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}