From 71c24e4e4f16104464d29efdb1f8d2173043358b Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Fri, 3 Apr 2026 05:41:06 -0700 Subject: [PATCH 1/3] Add support for reordering document tabs --- .../messages/portfolio/portfolio_message.rs | 4 + .../portfolio/portfolio_message_handler.rs | 18 ++ frontend/src/components/window/Panel.svelte | 162 +++++++++++++++++- .../src/components/window/Workspace.svelte | 1 + frontend/src/stores/portfolio.ts | 2 + frontend/src/utility-functions/persistence.ts | 8 +- frontend/wrapper/src/editor_wrapper.rs | 7 + 7 files changed, 194 insertions(+), 8 deletions(-) diff --git a/editor/src/messages/portfolio/portfolio_message.rs b/editor/src/messages/portfolio/portfolio_message.rs index 97c39c624b..48379b8992 100644 --- a/editor/src/messages/portfolio/portfolio_message.rs +++ b/editor/src/messages/portfolio/portfolio_message.rs @@ -126,6 +126,10 @@ pub enum PortfolioMessage { layers: Vec, }, PrevDocument, + ReorderDocument { + document_id: DocumentId, + new_index: usize, + }, RequestWelcomeScreenButtonsLayout, RequestStatusBarInfoLayout, SetActivePanel { diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index 8254300b0f..ba72bfdfbd 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -1053,6 +1053,24 @@ impl MessageHandler> for Portfolio responses.add(PortfolioMessage::SelectDocument { document_id: prev_id }); } } + PortfolioMessage::ReorderDocument { document_id, new_index } => { + let new_index = new_index.min(self.document_ids.len().saturating_sub(1)); + let Some(current_index) = self.document_ids.iter().position(|&id| id == document_id) else { + return; + }; + + if new_index != current_index { + self.document_ids.remove(current_index); + self.document_ids.insert(new_index, document_id); + + responses.add(PortfolioMessage::UpdateOpenDocumentsList); + + // Re-send the active document so the frontend recalculates the active tab index after reordering + if let Some(active_document_id) = self.active_document_id { + responses.add(FrontendMessage::UpdateActiveDocument { document_id: active_document_id }); + } + } + } PortfolioMessage::RequestWelcomeScreenButtonsLayout => { let donate = "https://graphite.art/donate/"; diff --git a/frontend/src/components/window/Panel.svelte b/frontend/src/components/window/Panel.svelte index d36278c5e7..41f27176cc 100644 --- a/frontend/src/components/window/Panel.svelte +++ b/frontend/src/components/window/Panel.svelte @@ -1,5 +1,5 @@ panelType && editor.setActivePanel(panelType)} class={`panel ${className}`.trim()} {classes} style={styleName} {styles}> - + {#each tabLabels as tabLabel, tabIndex} { - e.stopPropagation(); - clickAction?.(tabIndex); - }} + on:pointerdown={(e) => tabPointerDown(e, tabIndex)} + on:click={(e) => e.stopPropagation()} on:auxclick={(e) => { // Middle mouse button click if (e.button === BUTTON_MIDDLE) { @@ -95,6 +226,9 @@ {/each} + {#if dragging && insertionMarkerLeft !== undefined} +
+ {/if}
{#if panelType} @@ -110,6 +244,7 @@ overflow: hidden; .tab-bar { + position: relative; height: 28px; min-height: auto; background: var(--color-1-nearblack); // Needed for the viewport hole punch on desktop @@ -217,6 +352,21 @@ } } } + + &:has(.tab-insertion-mark) .tab .icon-button { + pointer-events: none; + } + + .tab-insertion-mark { + position: absolute; + top: 4px; + bottom: 4px; + width: 3px; + margin-left: -2px; + z-index: 1; + background: var(--color-e-nearwhite); + pointer-events: none; + } } .panel-body { diff --git a/frontend/src/components/window/Workspace.svelte b/frontend/src/components/window/Workspace.svelte index b852382e8a..472574ad81 100644 --- a/frontend/src/components/window/Workspace.svelte +++ b/frontend/src/components/window/Workspace.svelte @@ -151,6 +151,7 @@ emptySpaceAction={() => editor.newDocumentDialog()} clickAction={(tabIndex) => editor.selectDocument($portfolio.documents[tabIndex].id)} closeAction={(tabIndex) => editor.closeDocumentWithConfirmation($portfolio.documents[tabIndex].id)} + reorderAction={(oldIndex, newIndex) => editor.reorderDocument($portfolio.documents[oldIndex].id, newIndex)} tabActiveIndex={$portfolio.activeDocumentIndex} bind:this={documentPanel} /> diff --git a/frontend/src/stores/portfolio.ts b/frontend/src/stores/portfolio.ts index 0f482736ae..5651b3309e 100644 --- a/frontend/src/stores/portfolio.ts +++ b/frontend/src/stores/portfolio.ts @@ -2,6 +2,7 @@ import { writable } from "svelte/store"; import type { Writable } from "svelte/store"; import type { SubscriptionsRouter } from "/src/subscriptions-router"; import { downloadFile, downloadFileBlob, upload } from "/src/utility-functions/files"; +import { storeDocumentTabOrder } from "/src/utility-functions/persistence"; import { rasterizeSVG } from "/src/utility-functions/rasterization"; import type { EditorWrapper, OpenDocument } from "/wrapper/pkg/graphite_wasm_wrapper"; @@ -41,6 +42,7 @@ export function createPortfolioStore(subscriptions: SubscriptionsRouter, editor: state.documents = data.openDocuments; return state; }); + storeDocumentTabOrder({ subscribe }); }); subscriptions.subscribeFrontendMessage("UpdateActiveDocument", (data) => { diff --git a/frontend/src/utility-functions/persistence.ts b/frontend/src/utility-functions/persistence.ts index 15156afbb3..3ef31dadc3 100644 --- a/frontend/src/utility-functions/persistence.ts +++ b/frontend/src/utility-functions/persistence.ts @@ -6,6 +6,11 @@ import type { EditorWrapper } from "/wrapper/pkg/graphite_wasm_wrapper"; const PERSISTENCE_DB = "graphite"; const PERSISTENCE_STORE = "store"; +export async function storeDocumentTabOrder(portfolio: PortfolioStore) { + const documentOrder = get(portfolio).documents.map((doc) => String(doc.id)); + await databaseSet("documents_tab_order", documentOrder); +} + export async function storeCurrentDocumentId(documentId: string) { await databaseSet("current_document_id", String(documentId)); } @@ -17,8 +22,7 @@ export async function storeDocument(autoSaveDocument: MessageBody<"TriggerPersis return documents; }); - const documentOrder = get(portfolio).documents.map((doc) => String(doc.id)); - await databaseSet("documents_tab_order", documentOrder); + await storeDocumentTabOrder(portfolio); await storeCurrentDocumentId(String(autoSaveDocument.documentId)); } diff --git a/frontend/wrapper/src/editor_wrapper.rs b/frontend/wrapper/src/editor_wrapper.rs index 13fdca90de..54a048ce22 100644 --- a/frontend/wrapper/src/editor_wrapper.rs +++ b/frontend/wrapper/src/editor_wrapper.rs @@ -427,6 +427,13 @@ impl EditorWrapper { self.dispatch(message); } + #[wasm_bindgen(js_name = reorderDocument)] + pub fn reorder_document(&self, document_id: u64, new_index: usize) { + let document_id = DocumentId(document_id); + let message = PortfolioMessage::ReorderDocument { document_id, new_index }; + self.dispatch(message); + } + #[wasm_bindgen(js_name = closeDocumentWithConfirmation)] pub fn close_document_with_confirmation(&self, document_id: u64) { let document_id = DocumentId(document_id); From 6fd2856ab2e062991d63ce2613a4037f0f10cbda Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Fri, 3 Apr 2026 05:49:53 -0700 Subject: [PATCH 2/3] Fix tab bar scrolling --- frontend/src/components/window/Panel.svelte | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/window/Panel.svelte b/frontend/src/components/window/Panel.svelte index 41f27176cc..bbe2a4e65c 100644 --- a/frontend/src/components/window/Panel.svelte +++ b/frontend/src/components/window/Panel.svelte @@ -51,6 +51,7 @@ let dragging = false; let insertionIndex: number | undefined = undefined; let insertionMarkerLeft: number | undefined = undefined; + let lastPointerX = 0; let tabGroupElement: LayoutRow | undefined = undefined; onDestroy(() => { @@ -96,9 +97,11 @@ dragging = true; } + lastPointerX = e.clientX; + // Only show insertion line while the cursor is within the tab bar if (pointerIsInsideTabBar(e)) { - calculateInsertionIndex(e.clientX); + calculateInsertionIndex(lastPointerX); } else { insertionIndex = undefined; insertionMarkerLeft = undefined; @@ -126,6 +129,12 @@ if (e instanceof KeyboardEvent && e.key === "Escape") endDrag(); } + function dragScroll() { + if (dragging && insertionIndex !== undefined) { + calculateInsertionIndex(lastPointerX); + } + } + function endDrag() { dragStartState = undefined; dragging = false; @@ -160,10 +169,10 @@ if (pointerX > tabMidpoint) { bestIndex = i + 1; - bestMarkerLeft = tabRect.right - groupRect.left + groupDiv.scrollLeft; + bestMarkerLeft = tabRect.right - groupRect.left; } else { bestIndex = i; - bestMarkerLeft = tabRect.left - groupRect.left + groupDiv.scrollLeft; + bestMarkerLeft = tabRect.left - groupRect.left; break; } } @@ -177,6 +186,7 @@ document.addEventListener("pointerup", dragPointerUp); document.addEventListener("mousedown", dragAbort); document.addEventListener("keydown", dragAbort); + tabGroupElement?.div?.()?.addEventListener("scroll", dragScroll); } function removeDragListeners() { @@ -184,6 +194,7 @@ document.removeEventListener("pointerup", dragPointerUp); document.removeEventListener("mousedown", dragAbort); document.removeEventListener("keydown", dragAbort); + tabGroupElement?.div?.()?.removeEventListener("scroll", dragScroll); } From 9a4c4e0829b2e849e4210f01bf02ed7c5c7a7831 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Fri, 3 Apr 2026 06:17:55 -0700 Subject: [PATCH 3/3] Close tab without activating it on pointerdown --- frontend/src/components/window/Panel.svelte | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frontend/src/components/window/Panel.svelte b/frontend/src/components/window/Panel.svelte index bbe2a4e65c..df17cbbbb2 100644 --- a/frontend/src/components/window/Panel.svelte +++ b/frontend/src/components/window/Panel.svelte @@ -72,6 +72,7 @@ function tabPointerDown(e: PointerEvent, tabIndex: number) { if (e.button !== BUTTON_LEFT) return; + if (e.target instanceof Element && e.target.closest("[data-close-button]")) return; // Activate the tab upon pointer down clickAction?.(tabIndex); @@ -232,6 +233,7 @@ }} icon="CloseX" size={16} + data-close-button /> {/if}