mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 22:28:10 +08:00
Refactor panel layouts to generalize recursive panel group subdivision splits (#4014)
* Generalize recursive panel group splits * Code review
This commit is contained in:
@@ -3,24 +3,29 @@
|
||||
import Dialog from "/src/components/floating-menus/Dialog.svelte";
|
||||
import Tooltip from "/src/components/floating-menus/Tooltip.svelte";
|
||||
import LayoutCol from "/src/components/layout/LayoutCol.svelte";
|
||||
import LayoutRow from "/src/components/layout/LayoutRow.svelte";
|
||||
import TextLabel from "/src/components/widgets/labels/TextLabel.svelte";
|
||||
import PanelSubdivision from "/src/components/window/PanelSubdivision.svelte";
|
||||
import StatusBar from "/src/components/window/StatusBar.svelte";
|
||||
import TitleBar from "/src/components/window/TitleBar.svelte";
|
||||
import Workspace from "/src/components/window/Workspace.svelte";
|
||||
import type { AppWindowStore } from "/src/stores/app-window";
|
||||
import type { DialogStore } from "/src/stores/dialog";
|
||||
import type { PortfolioStore } from "/src/stores/portfolio";
|
||||
import type { TooltipStore } from "/src/stores/tooltip";
|
||||
|
||||
const dialog = getContext<DialogStore>("dialog");
|
||||
const tooltip = getContext<TooltipStore>("tooltip");
|
||||
const appWindow = getContext<AppWindowStore>("appWindow");
|
||||
const portfolio = getContext<PortfolioStore>("portfolio");
|
||||
</script>
|
||||
|
||||
<LayoutCol class="main-window" classes={{ "viewport-hole-punch": $appWindow.viewportHolePunch }}>
|
||||
{#if !($appWindow.platform == "Mac" && $appWindow.fullscreen)}
|
||||
<TitleBar />
|
||||
{/if}
|
||||
<Workspace />
|
||||
<LayoutRow class="workspace" data-workspace>
|
||||
<PanelSubdivision subdivision={$portfolio.panelLayout.root} depth={0} />
|
||||
</LayoutRow>
|
||||
<StatusBar />
|
||||
{#if $dialog.visible}
|
||||
<Dialog />
|
||||
@@ -46,25 +51,63 @@
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.release-candidate-expiry {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background-color: var(--color-e-nearwhite);
|
||||
color: var(--color-2-mildblack);
|
||||
opacity: 0.9;
|
||||
pointer-events: none;
|
||||
padding: 12px 40px;
|
||||
border-radius: 4px;
|
||||
text-align-last: justify;
|
||||
font-size: 18px;
|
||||
z-index: 1000;
|
||||
.workspace {
|
||||
position: relative;
|
||||
flex: 1 1 100%;
|
||||
|
||||
.text-label {
|
||||
line-height: 1.5;
|
||||
.workspace-grid-subdivision {
|
||||
position: relative;
|
||||
flex: 1 1 0;
|
||||
min-height: 28px;
|
||||
|
||||
&.folded {
|
||||
flex-grow: 0;
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.workspace-grid-resize-gutter {
|
||||
flex: 0 0 4px;
|
||||
|
||||
&.layout-row {
|
||||
cursor: ns-resize;
|
||||
}
|
||||
|
||||
&.layout-col {
|
||||
cursor: ew-resize;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Needed for the viewport hole punch on desktop
|
||||
.viewport-hole-punch .workspace .workspace-grid-subdivision:has(.panel.document-panel)::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 6px;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 0 0 calc(100vw + 100vh) var(--color-2-mildblack);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.release-candidate-expiry {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background-color: var(--color-e-nearwhite);
|
||||
color: var(--color-2-mildblack);
|
||||
opacity: 0.9;
|
||||
pointer-events: none;
|
||||
padding: 12px 40px;
|
||||
border-radius: 4px;
|
||||
text-align-last: justify;
|
||||
font-size: 18px;
|
||||
z-index: 1000;
|
||||
|
||||
.text-label {
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
import IconButton from "/src/components/widgets/buttons/IconButton.svelte";
|
||||
import TextLabel from "/src/components/widgets/labels/TextLabel.svelte";
|
||||
import { panelDrag, startCrossPanelDrag, endCrossPanelDrag, updateCrossPanelHover } from "/src/stores/panel-drag";
|
||||
import type { EditorWrapper, PanelType, PanelGroupId } from "/wrapper/pkg/graphite_wasm_wrapper";
|
||||
import type { EditorWrapper, PanelType } from "/wrapper/pkg/graphite_wasm_wrapper";
|
||||
|
||||
const PANEL_COMPONENTS = {
|
||||
Welcome,
|
||||
@@ -31,7 +31,7 @@
|
||||
export let tabLabels: { name: string; unsaved?: boolean; tooltipLabel?: string; tooltipDescription?: string; tooltipShortcut?: string }[];
|
||||
export let tabActiveIndex: number;
|
||||
export let panelTypes: PanelType[];
|
||||
export let panelId: PanelGroupId;
|
||||
export let panelId: string;
|
||||
export let clickAction: ((index: number) => void) | undefined = undefined;
|
||||
export let closeAction: ((index: number) => void) | undefined = undefined;
|
||||
export let reorderAction: ((oldIndex: number, newIndex: number) => void) | undefined = undefined;
|
||||
|
||||
200
frontend/src/components/window/PanelSubdivision.svelte
Normal file
200
frontend/src/components/window/PanelSubdivision.svelte
Normal file
@@ -0,0 +1,200 @@
|
||||
<script lang="ts">
|
||||
import { getContext, onDestroy } from "svelte";
|
||||
import LayoutCol from "/src/components/layout/LayoutCol.svelte";
|
||||
import LayoutRow from "/src/components/layout/LayoutRow.svelte";
|
||||
import Panel from "/src/components/window/Panel.svelte";
|
||||
import type { PortfolioStore } from "/src/stores/portfolio";
|
||||
import type { EditorWrapper, OpenDocument, PanelGroupState, PanelLayoutSubdivision } from "/wrapper/pkg/graphite_wasm_wrapper";
|
||||
|
||||
const MIN_PANEL_SIZE = 100;
|
||||
const DOUBLE_CLICK_MILLISECONDS = 500;
|
||||
|
||||
const editor = getContext<EditorWrapper>("editor");
|
||||
const portfolio = getContext<PortfolioStore>("portfolio");
|
||||
|
||||
export let subdivision: PanelLayoutSubdivision;
|
||||
export let depth: number;
|
||||
|
||||
// Local size overrides for gutter resizing (keyed by child index)
|
||||
let sizeOverrides: Record<number, number> = {};
|
||||
// Gutter resize state
|
||||
let gutterResizeRestore: [number, number] | undefined = undefined;
|
||||
let pointerCaptureId: number | undefined = undefined;
|
||||
let activeResizeCleanup: (() => void) | undefined = undefined;
|
||||
let lastGutterClickTarget: EventTarget | undefined = undefined;
|
||||
let lastGutterClickTime = 0;
|
||||
|
||||
// At even depths (0, 2, 4...) children are in a row, at odd depths (1, 3, 5...) in a column
|
||||
$: horizontal = depth % 2 === 0;
|
||||
// Reset overrides when the subdivision changes (e.g., backend sends a new layout)
|
||||
$: if (subdivision) sizeOverrides = {};
|
||||
// Reactive array of resolved sizes (merging backend defaults with local overrides)
|
||||
$: resolvedSizes = "Split" in subdivision ? subdivision.Split.children.map((child, index) => sizeOverrides[index] ?? child.size) : [];
|
||||
$: documentTabLabels = $portfolio.documents.map((doc: OpenDocument) => {
|
||||
const name = doc.details.name;
|
||||
const unsaved = !doc.details.isSaved;
|
||||
if (!editor.inDevelopmentMode()) return { name, unsaved };
|
||||
|
||||
const tooltipDescription = `Document ID: ${doc.id}`;
|
||||
return { name, unsaved, tooltipLabel: name, tooltipDescription };
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
activeResizeCleanup?.();
|
||||
});
|
||||
|
||||
function resizePanel(e: PointerEvent, prevIndex: number, nextIndex: number) {
|
||||
if (!("Split" in subdivision)) return;
|
||||
|
||||
const gutter = e.target;
|
||||
if (!(gutter instanceof HTMLDivElement)) return;
|
||||
|
||||
const nextSibling = gutter.nextElementSibling;
|
||||
const prevSibling = gutter.previousElementSibling;
|
||||
const parentElement = gutter.parentElement;
|
||||
if (!(nextSibling instanceof HTMLDivElement) || !(prevSibling instanceof HTMLDivElement) || !(parentElement instanceof HTMLDivElement)) return;
|
||||
|
||||
// Double-click resets both adjacent panels to their default sizes
|
||||
const children = subdivision.Split.children;
|
||||
const now = Date.now();
|
||||
const isDoubleClick = now - lastGutterClickTime < DOUBLE_CLICK_MILLISECONDS && lastGutterClickTarget === gutter;
|
||||
lastGutterClickTime = now;
|
||||
lastGutterClickTarget = gutter;
|
||||
if (isDoubleClick) {
|
||||
sizeOverrides = { ...sizeOverrides, [prevIndex]: children[prevIndex].size, [nextIndex]: children[nextIndex].size };
|
||||
return;
|
||||
}
|
||||
|
||||
const isHorizontal = horizontal;
|
||||
|
||||
const gutterSize = isHorizontal ? gutter.getBoundingClientRect().width : gutter.getBoundingClientRect().height;
|
||||
const nextSiblingSize = isHorizontal ? nextSibling.getBoundingClientRect().width : nextSibling.getBoundingClientRect().height;
|
||||
const prevSiblingSize = isHorizontal ? prevSibling.getBoundingClientRect().width : prevSibling.getBoundingClientRect().height;
|
||||
const parentElementSize = isHorizontal ? parentElement.getBoundingClientRect().width : parentElement.getBoundingClientRect().height;
|
||||
|
||||
const totalResizingSpaceOccupied = gutterSize + nextSiblingSize + prevSiblingSize;
|
||||
const proportionBeingResized = totalResizingSpaceOccupied / parentElementSize;
|
||||
|
||||
pointerCaptureId = e.pointerId;
|
||||
gutter.setPointerCapture(pointerCaptureId);
|
||||
|
||||
const mouseStart = isHorizontal ? e.clientX : e.clientY;
|
||||
|
||||
const abortResize = () => {
|
||||
if (pointerCaptureId) gutter.releasePointerCapture(pointerCaptureId);
|
||||
pointerCaptureId = undefined;
|
||||
removeListeners();
|
||||
activeResizeCleanup = undefined;
|
||||
|
||||
if (gutterResizeRestore !== undefined) {
|
||||
sizeOverrides = { ...sizeOverrides, [nextIndex]: gutterResizeRestore[0], [prevIndex]: gutterResizeRestore[1] };
|
||||
gutterResizeRestore = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const onPointerMove = (e: PointerEvent) => {
|
||||
const mouseCurrent = isHorizontal ? e.clientX : e.clientY;
|
||||
let mouseDelta = mouseStart - mouseCurrent;
|
||||
|
||||
mouseDelta = Math.max(nextSiblingSize + mouseDelta, MIN_PANEL_SIZE) - nextSiblingSize;
|
||||
mouseDelta = prevSiblingSize - Math.max(prevSiblingSize - mouseDelta, MIN_PANEL_SIZE);
|
||||
|
||||
if (gutterResizeRestore === undefined) gutterResizeRestore = [resolvedSizes[nextIndex], resolvedSizes[prevIndex]];
|
||||
|
||||
sizeOverrides = {
|
||||
...sizeOverrides,
|
||||
[nextIndex]: ((nextSiblingSize + mouseDelta) / totalResizingSpaceOccupied) * proportionBeingResized * 100,
|
||||
[prevIndex]: ((prevSiblingSize - mouseDelta) / totalResizingSpaceOccupied) * proportionBeingResized * 100,
|
||||
};
|
||||
};
|
||||
|
||||
const onPointerUp = () => {
|
||||
gutterResizeRestore = undefined;
|
||||
if (pointerCaptureId) gutter.releasePointerCapture(pointerCaptureId);
|
||||
removeListeners();
|
||||
activeResizeCleanup = undefined;
|
||||
};
|
||||
|
||||
const onMouseDown = (e: MouseEvent) => {
|
||||
const BUTTONS_RIGHT = 0b0000_0010;
|
||||
if (e.buttons & BUTTONS_RIGHT) abortResize();
|
||||
};
|
||||
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") abortResize();
|
||||
};
|
||||
|
||||
const addListeners = () => {
|
||||
document.addEventListener("pointermove", onPointerMove);
|
||||
document.addEventListener("pointerup", onPointerUp);
|
||||
document.addEventListener("mousedown", onMouseDown);
|
||||
document.addEventListener("keydown", onKeyDown);
|
||||
};
|
||||
|
||||
const removeListeners = () => {
|
||||
document.removeEventListener("pointermove", onPointerMove);
|
||||
document.removeEventListener("pointerup", onPointerUp);
|
||||
document.removeEventListener("mousedown", onMouseDown);
|
||||
document.removeEventListener("keydown", onKeyDown);
|
||||
};
|
||||
|
||||
addListeners();
|
||||
activeResizeCleanup = removeListeners;
|
||||
}
|
||||
|
||||
function crossPanelDrop(sourcePanelId: string, targetPanelId: string, insertIndex: number) {
|
||||
editor.movePanelTab(BigInt(sourcePanelId), BigInt(targetPanelId), insertIndex);
|
||||
}
|
||||
|
||||
function isDocumentGroup(state: PanelGroupState): boolean {
|
||||
return state.tabs.some((t) => t === "Document" || t === "Welcome");
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if "PanelGroup" in subdivision}
|
||||
{@const group = subdivision.PanelGroup}
|
||||
{#if isDocumentGroup(group.state)}
|
||||
<Panel
|
||||
class="document-panel"
|
||||
panelId={String(group.id)}
|
||||
panelTypes={$portfolio.documents.length > 0 ? $portfolio.documents.map(() => "Document") : ["Welcome"]}
|
||||
tabCloseButtons={true}
|
||||
tabMinWidths={true}
|
||||
tabLabels={documentTabLabels}
|
||||
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}
|
||||
/>
|
||||
{:else}
|
||||
<Panel
|
||||
panelId={String(group.id)}
|
||||
panelTypes={group.state.tabs}
|
||||
tabLabels={group.state.tabs.map((name) => ({ name }))}
|
||||
tabActiveIndex={Number(group.state.activeTabIndex)}
|
||||
clickAction={(tabIndex) => editor.setPanelGroupActiveTab(group.id, tabIndex)}
|
||||
reorderAction={(oldIndex, newIndex) => editor.reorderPanelGroupTab(group.id, oldIndex, newIndex)}
|
||||
crossPanelDropAction={crossPanelDrop}
|
||||
/>
|
||||
{/if}
|
||||
{:else if "Split" in subdivision}
|
||||
{#each subdivision.Split.children as child, index}
|
||||
{#if index > 0}
|
||||
{#if horizontal}
|
||||
<LayoutCol class="workspace-grid-resize-gutter" data-gutter-horizontal on:pointerdown={(e) => resizePanel(e, index - 1, index)} />
|
||||
{:else}
|
||||
<LayoutRow class="workspace-grid-resize-gutter" data-gutter-vertical on:pointerdown={(e) => resizePanel(e, index - 1, index)} />
|
||||
{/if}
|
||||
{/if}
|
||||
{#if horizontal}
|
||||
<LayoutCol class="workspace-grid-subdivision" styles={{ "flex-grow": resolvedSizes[index] }}>
|
||||
<svelte:self subdivision={child.subdivision} depth={depth + 1} />
|
||||
</LayoutCol>
|
||||
{:else}
|
||||
<LayoutRow class="workspace-grid-subdivision" styles={{ "flex-grow": resolvedSizes[index] }}>
|
||||
<svelte:self subdivision={child.subdivision} depth={depth + 1} />
|
||||
</LayoutRow>
|
||||
{/if}
|
||||
{/each}
|
||||
{/if}
|
||||
@@ -1,279 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { getContext, onDestroy } from "svelte";
|
||||
import LayoutCol from "/src/components/layout/LayoutCol.svelte";
|
||||
import LayoutRow from "/src/components/layout/LayoutRow.svelte";
|
||||
import Panel from "/src/components/window/Panel.svelte";
|
||||
import type { PortfolioStore } from "/src/stores/portfolio";
|
||||
import type { EditorWrapper, OpenDocument } from "/wrapper/pkg/graphite_wasm_wrapper";
|
||||
|
||||
const MIN_PANEL_SIZE = 100;
|
||||
const PANEL_SIZES = {
|
||||
/**/ root: 100,
|
||||
/* ├─ */ content: 80,
|
||||
/* │ ├─ */ document: 70,
|
||||
/* │ └─ */ data: 30,
|
||||
/* └─ */ details: 20,
|
||||
/* ├─ */ properties: 45,
|
||||
/* └─ */ layers: 55,
|
||||
} as const;
|
||||
|
||||
let panelSizes: Record<string, number> = { ...PANEL_SIZES };
|
||||
let documentPanel: Panel | undefined;
|
||||
let gutterResizeRestore: [number, number] | undefined = undefined;
|
||||
let pointerCaptureId: number | undefined = undefined;
|
||||
let activeResizeCleanup: (() => void) | undefined = undefined;
|
||||
|
||||
// Reactive panel layout derived from backend state
|
||||
$: panelLayout = $portfolio.panelLayout;
|
||||
$: propertiesGroup = panelLayout.propertiesGroup;
|
||||
$: layersGroup = panelLayout.layersGroup;
|
||||
$: dataGroup = panelLayout.dataGroup;
|
||||
|
||||
$: documentPanel?.scrollTabIntoView($portfolio.activeDocumentIndex);
|
||||
|
||||
$: documentTabLabels = $portfolio.documents.map((doc: OpenDocument) => {
|
||||
const name = doc.details.name;
|
||||
const unsaved = !doc.details.isSaved;
|
||||
if (!editor.inDevelopmentMode()) return { name, unsaved };
|
||||
|
||||
const tooltipDescription = `Document ID: ${doc.id}`;
|
||||
return { name, unsaved, tooltipLabel: name, tooltipDescription };
|
||||
});
|
||||
|
||||
const editor = getContext<EditorWrapper>("editor");
|
||||
const portfolio = getContext<PortfolioStore>("portfolio");
|
||||
|
||||
function crossPanelDrop(sourcePanelId: string, targetPanelId: string, insertIndex: number) {
|
||||
editor.movePanelTab(sourcePanelId, targetPanelId, insertIndex);
|
||||
}
|
||||
|
||||
function isPanelName(name: string): name is keyof typeof PANEL_SIZES {
|
||||
return name in PANEL_SIZES;
|
||||
}
|
||||
|
||||
function resetPanelSizes(e: MouseEvent) {
|
||||
const gutter = e.currentTarget;
|
||||
if (!(gutter instanceof HTMLDivElement)) return;
|
||||
|
||||
const nextSibling = gutter.nextElementSibling;
|
||||
const prevSibling = gutter.previousElementSibling;
|
||||
if (!(nextSibling instanceof HTMLDivElement) || !(prevSibling instanceof HTMLDivElement)) return;
|
||||
|
||||
const nextSiblingName = nextSibling.getAttribute("data-subdivision-name") || undefined;
|
||||
const prevSiblingName = prevSibling.getAttribute("data-subdivision-name") || undefined;
|
||||
if (!nextSiblingName || !prevSiblingName || !isPanelName(nextSiblingName) || !isPanelName(prevSiblingName)) return;
|
||||
|
||||
panelSizes = { ...panelSizes, [nextSiblingName]: PANEL_SIZES[nextSiblingName], [prevSiblingName]: PANEL_SIZES[prevSiblingName] };
|
||||
}
|
||||
|
||||
function resizePanel(e: PointerEvent) {
|
||||
const gutter = e.target;
|
||||
if (!(gutter instanceof HTMLDivElement)) return;
|
||||
|
||||
const nextSibling = gutter.nextElementSibling;
|
||||
const prevSibling = gutter.previousElementSibling;
|
||||
|
||||
const parentElement = gutter.parentElement;
|
||||
if (!(nextSibling instanceof HTMLDivElement) || !(prevSibling instanceof HTMLDivElement) || !(parentElement instanceof HTMLDivElement)) return;
|
||||
|
||||
const nextSiblingName = nextSibling.getAttribute("data-subdivision-name") || undefined;
|
||||
const prevSiblingName = prevSibling.getAttribute("data-subdivision-name") || undefined;
|
||||
|
||||
if (!nextSiblingName || !prevSiblingName || !(nextSiblingName in PANEL_SIZES) || !(prevSiblingName in PANEL_SIZES)) return;
|
||||
|
||||
// Are we resizing horizontally?
|
||||
const isHorizontal = gutter.getAttribute("data-gutter-horizontal") !== null;
|
||||
|
||||
// Get the current size in px of the panels being resized and the gutter
|
||||
const gutterSize = isHorizontal ? gutter.getBoundingClientRect().width : gutter.getBoundingClientRect().height;
|
||||
const nextSiblingSize = isHorizontal ? nextSibling.getBoundingClientRect().width : nextSibling.getBoundingClientRect().height;
|
||||
const prevSiblingSize = isHorizontal ? prevSibling.getBoundingClientRect().width : prevSibling.getBoundingClientRect().height;
|
||||
const parentElementSize = isHorizontal ? parentElement.getBoundingClientRect().width : parentElement.getBoundingClientRect().height;
|
||||
|
||||
// Measure the resizing panels as a percentage of all sibling panels
|
||||
const totalResizingSpaceOccupied = gutterSize + nextSiblingSize + prevSiblingSize;
|
||||
const proportionBeingResized = totalResizingSpaceOccupied / parentElementSize;
|
||||
|
||||
// Prevent cursor flicker as mouse temporarily leaves the gutter
|
||||
pointerCaptureId = e.pointerId;
|
||||
gutter.setPointerCapture(pointerCaptureId);
|
||||
|
||||
const mouseStart = isHorizontal ? e.clientX : e.clientY;
|
||||
|
||||
const abortResize = () => {
|
||||
if (pointerCaptureId) gutter.releasePointerCapture(pointerCaptureId);
|
||||
removeListeners();
|
||||
activeResizeCleanup = undefined;
|
||||
|
||||
pointerCaptureId = e.pointerId;
|
||||
gutter.setPointerCapture(pointerCaptureId);
|
||||
|
||||
if (gutterResizeRestore !== undefined) {
|
||||
panelSizes[nextSiblingName] = gutterResizeRestore[0];
|
||||
panelSizes[prevSiblingName] = gutterResizeRestore[1];
|
||||
gutterResizeRestore = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const onPointerMove = (e: PointerEvent) => {
|
||||
const mouseCurrent = isHorizontal ? e.clientX : e.clientY;
|
||||
let mouseDelta = mouseStart - mouseCurrent;
|
||||
|
||||
mouseDelta = Math.max(nextSiblingSize + mouseDelta, MIN_PANEL_SIZE) - nextSiblingSize;
|
||||
mouseDelta = prevSiblingSize - Math.max(prevSiblingSize - mouseDelta, MIN_PANEL_SIZE);
|
||||
|
||||
if (gutterResizeRestore === undefined) gutterResizeRestore = [panelSizes[nextSiblingName], panelSizes[prevSiblingName]];
|
||||
|
||||
panelSizes[nextSiblingName] = ((nextSiblingSize + mouseDelta) / totalResizingSpaceOccupied) * proportionBeingResized * 100;
|
||||
panelSizes[prevSiblingName] = ((prevSiblingSize - mouseDelta) / totalResizingSpaceOccupied) * proportionBeingResized * 100;
|
||||
};
|
||||
|
||||
const onPointerUp = () => {
|
||||
gutterResizeRestore = undefined;
|
||||
if (pointerCaptureId) gutter.releasePointerCapture(pointerCaptureId);
|
||||
removeListeners();
|
||||
activeResizeCleanup = undefined;
|
||||
};
|
||||
|
||||
const onMouseDown = (e: MouseEvent) => {
|
||||
const BUTTONS_RIGHT = 0b0000_0010;
|
||||
if (e.buttons & BUTTONS_RIGHT) abortResize();
|
||||
};
|
||||
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") abortResize();
|
||||
};
|
||||
|
||||
const addListeners = () => {
|
||||
document.addEventListener("pointermove", onPointerMove);
|
||||
document.addEventListener("pointerup", onPointerUp);
|
||||
document.addEventListener("mousedown", onMouseDown);
|
||||
document.addEventListener("keydown", onKeyDown);
|
||||
};
|
||||
|
||||
const removeListeners = () => {
|
||||
document.removeEventListener("pointermove", onPointerMove);
|
||||
document.removeEventListener("pointerup", onPointerUp);
|
||||
document.removeEventListener("mousedown", onMouseDown);
|
||||
document.removeEventListener("keydown", onKeyDown);
|
||||
};
|
||||
|
||||
addListeners();
|
||||
activeResizeCleanup = removeListeners;
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
activeResizeCleanup?.();
|
||||
});
|
||||
</script>
|
||||
|
||||
<LayoutRow class="workspace" data-workspace>
|
||||
<LayoutRow class="workspace-grid-subdivision" styles={{ "flex-grow": panelSizes["root"] }} data-subdivision-name="root">
|
||||
<LayoutCol class="workspace-grid-subdivision" styles={{ "flex-grow": panelSizes["content"] }} data-subdivision-name="content">
|
||||
<LayoutRow class="workspace-grid-subdivision" styles={{ "flex-grow": panelSizes["document"] }} data-subdivision-name="document">
|
||||
<Panel
|
||||
class="document-panel"
|
||||
panelId="DocumentGroup"
|
||||
panelTypes={$portfolio.documents.length > 0 ? $portfolio.documents.map(() => "Document") : ["Welcome"]}
|
||||
tabCloseButtons={true}
|
||||
tabMinWidths={true}
|
||||
tabLabels={documentTabLabels}
|
||||
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}
|
||||
/>
|
||||
</LayoutRow>
|
||||
{#if dataGroup.tabs.length > 0}
|
||||
<LayoutRow class="workspace-grid-resize-gutter" data-gutter-vertical on:pointerdown={(e) => resizePanel(e)} on:dblclick={(e) => resetPanelSizes(e)} />
|
||||
<LayoutRow class="workspace-grid-subdivision" styles={{ "flex-grow": panelSizes["data"] }} data-subdivision-name="data">
|
||||
<Panel
|
||||
panelId="DataGroup"
|
||||
panelTypes={dataGroup.tabs}
|
||||
tabLabels={dataGroup.tabs.map((name) => ({ name }))}
|
||||
tabActiveIndex={dataGroup.activeTabIndex}
|
||||
clickAction={(tabIndex) => editor.setPanelGroupActiveTab("DataGroup", tabIndex)}
|
||||
reorderAction={(oldIndex, newIndex) => editor.reorderPanelGroupTab("DataGroup", oldIndex, newIndex)}
|
||||
crossPanelDropAction={crossPanelDrop}
|
||||
/>
|
||||
</LayoutRow>
|
||||
{/if}
|
||||
</LayoutCol>
|
||||
{#if propertiesGroup.tabs.length > 0 || layersGroup.tabs.length > 0}
|
||||
<LayoutCol class="workspace-grid-resize-gutter" data-gutter-horizontal on:pointerdown={(e) => resizePanel(e)} on:dblclick={(e) => resetPanelSizes(e)} />
|
||||
<LayoutCol class="workspace-grid-subdivision" styles={{ "flex-grow": panelSizes["details"] }} data-subdivision-name="details">
|
||||
{#if propertiesGroup.tabs.length > 0}
|
||||
<LayoutRow class="workspace-grid-subdivision" styles={{ "flex-grow": panelSizes["properties"] }} data-subdivision-name="properties">
|
||||
<Panel
|
||||
panelId="PropertiesGroup"
|
||||
panelTypes={propertiesGroup.tabs}
|
||||
tabLabels={propertiesGroup.tabs.map((name) => ({ name }))}
|
||||
tabActiveIndex={propertiesGroup.activeTabIndex}
|
||||
clickAction={(tabIndex) => editor.setPanelGroupActiveTab("PropertiesGroup", tabIndex)}
|
||||
reorderAction={(oldIndex, newIndex) => editor.reorderPanelGroupTab("PropertiesGroup", oldIndex, newIndex)}
|
||||
crossPanelDropAction={crossPanelDrop}
|
||||
/>
|
||||
</LayoutRow>
|
||||
{/if}
|
||||
{#if propertiesGroup.tabs.length > 0 && layersGroup.tabs.length > 0}
|
||||
<LayoutRow class="workspace-grid-resize-gutter" data-gutter-vertical on:pointerdown={(e) => resizePanel(e)} on:dblclick={(e) => resetPanelSizes(e)} />
|
||||
{/if}
|
||||
{#if layersGroup.tabs.length > 0}
|
||||
<LayoutRow class="workspace-grid-subdivision" styles={{ "flex-grow": panelSizes["layers"] }} data-subdivision-name="layers">
|
||||
<Panel
|
||||
panelId="LayersGroup"
|
||||
panelTypes={layersGroup.tabs}
|
||||
tabLabels={layersGroup.tabs.map((name) => ({ name }))}
|
||||
tabActiveIndex={layersGroup.activeTabIndex}
|
||||
clickAction={(tabIndex) => editor.setPanelGroupActiveTab("LayersGroup", tabIndex)}
|
||||
reorderAction={(oldIndex, newIndex) => editor.reorderPanelGroupTab("LayersGroup", oldIndex, newIndex)}
|
||||
crossPanelDropAction={crossPanelDrop}
|
||||
/>
|
||||
</LayoutRow>
|
||||
{/if}
|
||||
</LayoutCol>
|
||||
{/if}
|
||||
</LayoutRow>
|
||||
</LayoutRow>
|
||||
|
||||
<style lang="scss">
|
||||
.workspace {
|
||||
position: relative;
|
||||
flex: 1 1 100%;
|
||||
|
||||
.workspace-grid-subdivision {
|
||||
position: relative;
|
||||
flex: 1 1 0;
|
||||
min-height: 28px;
|
||||
|
||||
&.folded {
|
||||
flex-grow: 0;
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.workspace-grid-resize-gutter {
|
||||
flex: 0 0 4px;
|
||||
|
||||
&.layout-row {
|
||||
cursor: ns-resize;
|
||||
}
|
||||
|
||||
&.layout-col {
|
||||
cursor: ew-resize;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Needed for the viewport hole punch on desktop
|
||||
.viewport-hole-punch .workspace .workspace-grid-subdivision:has(.panel.document-panel)::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 6px;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 0 0 calc(100vw + 100vh) var(--color-2-mildblack);
|
||||
z-index: -1;
|
||||
}
|
||||
</style>
|
||||
@@ -4,21 +4,10 @@ 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, PanelType } from "/wrapper/pkg/graphite_wasm_wrapper";
|
||||
import type { EditorWrapper, OpenDocument, WorkspacePanelLayout } 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[];
|
||||
@@ -29,11 +18,7 @@ const initialState: PortfolioStoreState = {
|
||||
unsaved: false,
|
||||
documents: [],
|
||||
activeDocumentIndex: 0,
|
||||
panelLayout: {
|
||||
propertiesGroup: { tabs: ["Properties"], activeTabIndex: 0 },
|
||||
layersGroup: { tabs: ["Layers"], activeTabIndex: 0 },
|
||||
dataGroup: { tabs: [], activeTabIndex: 0 },
|
||||
},
|
||||
panelLayout: { root: { Split: { children: [] } }, nextGroupId: 0n },
|
||||
};
|
||||
|
||||
let subscriptionsRouter: SubscriptionsRouter | undefined = undefined;
|
||||
@@ -115,14 +100,8 @@ export function createPortfolioStore(subscriptions: SubscriptionsRouter, editor:
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
update((state) => {
|
||||
state.panelLayout = layout;
|
||||
state.panelLayout = data.panelLayout;
|
||||
return state;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,7 +18,7 @@ use editor::messages::input_mapper::utility_types::input_keyboard::ModifierKeys;
|
||||
use editor::messages::input_mapper::utility_types::input_mouse::{EditorMouseState, ScrollDelta};
|
||||
use editor::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
use editor::messages::portfolio::document::utility_types::network_interface::ImportOrExport;
|
||||
use editor::messages::portfolio::utility_types::{FontCatalog, FontCatalogFamily};
|
||||
use editor::messages::portfolio::utility_types::{FontCatalog, FontCatalogFamily, PanelGroupId};
|
||||
use editor::messages::prelude::*;
|
||||
use editor::messages::tool::tool_messages::tool_prelude::WidgetId;
|
||||
use graph_craft::document::NodeId;
|
||||
@@ -435,25 +435,31 @@ impl EditorWrapper {
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = reorderPanelGroupTab)]
|
||||
pub fn reorder_panel_group_tab(&self, group: String, old_index: usize, new_index: usize) {
|
||||
let group = group.into();
|
||||
let message = PortfolioMessage::ReorderPanelGroupTab { group, old_index, new_index };
|
||||
pub fn reorder_panel_group_tab(&self, group: u64, old_index: usize, new_index: usize) {
|
||||
let message = PortfolioMessage::ReorderPanelGroupTab {
|
||||
group: PanelGroupId(group),
|
||||
old_index,
|
||||
new_index,
|
||||
};
|
||||
self.dispatch(message);
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = movePanelTab)]
|
||||
pub fn move_panel_tab(&self, source_group: String, target_group: String, insert_index: usize) {
|
||||
pub fn move_panel_tab(&self, source_group: u64, target_group: u64, insert_index: usize) {
|
||||
let message = PortfolioMessage::MovePanelTab {
|
||||
source_group: source_group.into(),
|
||||
target_group: target_group.into(),
|
||||
source_group: PanelGroupId(source_group),
|
||||
target_group: PanelGroupId(target_group),
|
||||
insert_index,
|
||||
};
|
||||
self.dispatch(message);
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = setPanelGroupActiveTab)]
|
||||
pub fn set_panel_group_active_tab(&self, group: String, tab_index: usize) {
|
||||
let message = PortfolioMessage::SetPanelGroupActiveTab { group: group.into(), tab_index };
|
||||
pub fn set_panel_group_active_tab(&self, group: u64, tab_index: usize) {
|
||||
let message = PortfolioMessage::SetPanelGroupActiveTab {
|
||||
group: PanelGroupId(group),
|
||||
tab_index,
|
||||
};
|
||||
self.dispatch(message);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user