Add support for dragging panel tabs docked into other panel tab bars (#4006)

* Add support for dragging panel tabs docked into other panel tab bars

* Fix terminology

* Add Group suffix to PanelGroupId enums variants

* Code review
This commit is contained in:
Keavon Chambers
2026-04-04 06:28:53 -07:00
committed by GitHub
parent 19aaeb374b
commit 848ff5fd52
14 changed files with 627 additions and 182 deletions
+61
View File
@@ -0,0 +1,61 @@
import { writable } from "svelte/store";
import type { Writable } from "svelte/store";
export type PanelDragState = {
active: boolean;
sourcePanelId: string | undefined;
draggedTabLabel: string | undefined;
sourceTabIndex: number;
// Which panel's tab bar the pointer is currently hovering over (undefined if none)
hoverTargetPanelId: string | undefined;
hoverInsertionIndex: number | undefined;
hoverInsertionMarkerLeft: number | undefined;
};
const initialState: PanelDragState = {
active: false,
sourcePanelId: undefined,
draggedTabLabel: undefined,
sourceTabIndex: 0,
hoverTargetPanelId: undefined,
hoverInsertionIndex: undefined,
hoverInsertionMarkerLeft: undefined,
};
// Store state persisted across HMR to maintain reactive subscriptions in the component tree
const store: Writable<PanelDragState> = import.meta.hot?.data?.store || writable<PanelDragState>(initialState);
if (import.meta.hot) import.meta.hot.data.store = store;
export const panelDrag = store;
export function startCrossPanelDrag(sourcePanelId: string, draggedTabLabel: string, sourceTabIndex: number) {
store.update((state) => {
state.active = true;
state.sourcePanelId = sourcePanelId;
state.draggedTabLabel = draggedTabLabel;
state.sourceTabIndex = sourceTabIndex;
return state;
});
}
export function endCrossPanelDrag() {
store.update((state) => {
state.active = false;
state.sourcePanelId = undefined;
state.draggedTabLabel = undefined;
state.sourceTabIndex = 0;
state.hoverTargetPanelId = undefined;
state.hoverInsertionIndex = undefined;
state.hoverInsertionMarkerLeft = undefined;
return state;
});
}
export function updateCrossPanelHover(hoverTargetPanelId: string | undefined, hoverInsertionIndex: number | undefined, hoverInsertionMarkerLeft: number | undefined) {
store.update((state) => {
state.hoverTargetPanelId = hoverTargetPanelId;
state.hoverInsertionIndex = hoverInsertionIndex;
state.hoverInsertionMarkerLeft = hoverInsertionMarkerLeft;
return state;
});
}
+26 -25
View File
@@ -4,25 +4,36 @@ 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";
import type { EditorWrapper, OpenDocument, PanelType } from "/wrapper/pkg/graphite_wasm_wrapper";
export type PortfolioStore = ReturnType<typeof createPortfolioStore>;
export type PanelGroupState = {
tabs: PanelType[];
activeTabIndex: number;
};
export type WorkspacePanelLayout = {
propertiesGroup: PanelGroupState;
layersGroup: PanelGroupState;
dataGroup: PanelGroupState;
};
type PortfolioStoreState = {
unsaved: boolean;
documents: OpenDocument[];
activeDocumentIndex: number;
dataPanelOpen: boolean;
propertiesPanelOpen: boolean;
layersPanelOpen: boolean;
panelLayout: WorkspacePanelLayout;
};
const initialState: PortfolioStoreState = {
unsaved: false,
documents: [],
activeDocumentIndex: 0,
dataPanelOpen: false,
propertiesPanelOpen: true,
layersPanelOpen: true,
panelLayout: {
propertiesGroup: { tabs: ["Properties"], activeTabIndex: 0 },
layersGroup: { tabs: ["Layers"], activeTabIndex: 0 },
dataGroup: { tabs: [], activeTabIndex: 0 },
},
};
let subscriptionsRouter: SubscriptionsRouter | undefined = undefined;
@@ -103,23 +114,15 @@ export function createPortfolioStore(subscriptions: SubscriptionsRouter, editor:
}
});
subscriptions.subscribeFrontendMessage("UpdateDataPanelState", async (data) => {
update((state) => {
state.dataPanelOpen = data.open;
return state;
});
});
subscriptions.subscribeFrontendMessage("UpdateWorkspacePanelLayout", (data) => {
// Coerce activeTabIndex from BigInt (produced by serde_wasm_bindgen for usize) to number
const layout = data.panelLayout;
layout.propertiesGroup.activeTabIndex = Number(layout.propertiesGroup.activeTabIndex);
layout.layersGroup.activeTabIndex = Number(layout.layersGroup.activeTabIndex);
layout.dataGroup.activeTabIndex = Number(layout.dataGroup.activeTabIndex);
subscriptions.subscribeFrontendMessage("UpdatePropertiesPanelState", async (data) => {
update((state) => {
state.propertiesPanelOpen = data.open;
return state;
});
});
subscriptions.subscribeFrontendMessage("UpdateLayersPanelState", async (data) => {
update((state) => {
state.layersPanelOpen = data.open;
state.panelLayout = layout;
return state;
});
});
@@ -139,7 +142,5 @@ export function destroyPortfolioStore() {
subscriptions.unsubscribeFrontendMessage("TriggerSaveDocument");
subscriptions.unsubscribeFrontendMessage("TriggerSaveFile");
subscriptions.unsubscribeFrontendMessage("TriggerExportImage");
subscriptions.unsubscribeFrontendMessage("UpdateDataPanelState");
subscriptions.unsubscribeFrontendMessage("UpdatePropertiesPanelState");
subscriptions.unsubscribeFrontendMessage("UpdateLayersPanelState");
subscriptions.unsubscribeFrontendMessage("UpdateWorkspacePanelLayout");
}