Skip to content

Commit 8cebde7

Browse files
authored
Add support for RMB/Escape canceling layer drag reordering in the Layers panel (#3426)
* Add support for RMB/Escape canceling layer drag reordering in the Layers panel * Disable hover effects on layers during drag; fix insertion line getting cut off at top of stack
1 parent ab5c87f commit 8cebde7

File tree

3 files changed

+152
-94
lines changed

3 files changed

+152
-94
lines changed

frontend/src/components/Editor.svelte

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
44
import { type Editor } from "@graphite/editor";
55
import { createClipboardManager } from "@graphite/io-managers/clipboard";
6-
import { createDragManager } from "@graphite/io-managers/drag";
76
import { createHyperlinkManager } from "@graphite/io-managers/hyperlinks";
87
import { createInputManager } from "@graphite/io-managers/input";
98
import { createLocalizationManager } from "@graphite/io-managers/localization";
@@ -46,7 +45,6 @@
4645
createLocalizationManager(editor);
4746
createPanicManager(editor, dialog);
4847
createPersistenceManager(editor, portfolio);
49-
let dragManagerDestructor = createDragManager();
5048
let inputManagerDestructor = createInputManager(editor, dialog, portfolio, document, fullscreen);
5149
5250
onMount(() => {
@@ -56,7 +54,6 @@
5654
5755
onDestroy(() => {
5856
// Call the destructor for each manager
59-
dragManagerDestructor();
6057
inputManagerDestructor();
6158
});
6259
</script>

frontend/src/components/panels/Layers.svelte

Lines changed: 152 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
import { getContext, onMount, onDestroy, tick } from "svelte";
33
44
import type { Editor } from "@graphite/editor";
5-
import { beginDraggingElement } from "@graphite/io-managers/drag";
65
import {
76
defaultWidgetLayout,
87
patchWidgetLayout,
@@ -40,6 +39,14 @@
4039
markerHeight: number;
4140
};
4241
42+
type InternalDragState = {
43+
active: boolean;
44+
layerId: bigint;
45+
listing: LayerListingInfo;
46+
startX: number;
47+
startY: number;
48+
};
49+
4350
const editor = getContext<Editor>("editor");
4451
const nodeGraph = getContext<NodeGraphState>("nodeGraph");
4552
@@ -52,7 +59,9 @@
5259
// Interactive dragging
5360
let draggable = true;
5461
let draggingData: undefined | DraggingData = undefined;
62+
let internalDragState: InternalDragState | undefined = undefined;
5563
let fakeHighlightOfNotYetSelectedLayerBeingDragged: undefined | bigint = undefined;
64+
let justFinishedDrag = false; // Used to prevent click events after a drag
5665
let dragInPanel = false;
5766
5867
// Interactive clipping
@@ -92,6 +101,11 @@
92101
updateLayerInTree(targetId, targetLayer);
93102
});
94103
104+
addEventListener("pointerup", draggingPointerUp);
105+
addEventListener("pointermove", draggingPointerMove);
106+
addEventListener("mousedown", draggingMouseDown);
107+
addEventListener("keydown", draggingKeyDown);
108+
95109
addEventListener("pointermove", clippingHover);
96110
addEventListener("keydown", clippingKeyPress);
97111
addEventListener("keyup", clippingKeyPress);
@@ -104,6 +118,11 @@
104118
editor.subscriptions.unsubscribeJsMessage(UpdateDocumentLayerStructureJs);
105119
editor.subscriptions.unsubscribeJsMessage(UpdateDocumentLayerDetails);
106120
121+
removeEventListener("pointerup", draggingPointerUp);
122+
removeEventListener("pointermove", draggingPointerMove);
123+
removeEventListener("mousedown", draggingMouseDown);
124+
removeEventListener("keydown", draggingKeyDown);
125+
107126
removeEventListener("pointermove", clippingHover);
108127
removeEventListener("keydown", clippingKeyPress);
109128
removeEventListener("keyup", clippingKeyPress);
@@ -223,6 +242,13 @@
223242
}
224243
225244
function selectLayerWithModifiers(e: MouseEvent, listing: LayerListingInfo) {
245+
if (justFinishedDrag) {
246+
justFinishedDrag = false;
247+
// Prevent bubbling to deselectAllLayers
248+
e.stopPropagation();
249+
return;
250+
}
251+
226252
// Get the pressed state of the modifier keys
227253
const [ctrl, meta, shift, alt] = [e.ctrlKey, e.metaKey, e.shiftKey, e.altKey];
228254
// Get the state of the platform's accel key and its opposite platform's accel key
@@ -255,7 +281,7 @@
255281
return;
256282
}
257283
258-
// Check if the cursor is near the border btween two layers
284+
// Check if the cursor is near the border between two layers
259285
const DISTANCE = 6;
260286
const distanceFromTop = e.clientY - target.getBoundingClientRect().top;
261287
const distanceFromBottom = target.getBoundingClientRect().bottom - e.clientY;
@@ -288,6 +314,11 @@
288314
}
289315
290316
async function deselectAllLayers() {
317+
if (justFinishedDrag) {
318+
justFinishedDrag = false;
319+
return;
320+
}
321+
291322
editor.handle.deselectAllLayers();
292323
}
293324
@@ -371,83 +402,135 @@
371402
};
372403
}
373404
374-
async function dragStart(event: DragEvent, listing: LayerListingInfo) {
375-
const layer = listing.entry;
376-
dragInPanel = true;
377-
if (!$nodeGraph.selected.includes(layer.id)) {
378-
fakeHighlightOfNotYetSelectedLayerBeingDragged = layer.id;
379-
}
380-
const select = () => {
381-
if (!$nodeGraph.selected.includes(layer.id)) selectLayer(listing, false, false);
405+
function layerPointerDown(e: PointerEvent, listing: LayerListingInfo) {
406+
// Only left click drags
407+
if (e.button !== 0 || !draggable) return;
408+
409+
internalDragState = {
410+
active: false,
411+
layerId: listing.entry.id,
412+
listing: listing,
413+
startX: e.clientX,
414+
startY: e.clientY,
382415
};
416+
}
417+
418+
function draggingPointerMove(e: PointerEvent) {
419+
if (!internalDragState || !list) return;
420+
421+
// Calculate distance moved
422+
if (!internalDragState.active) {
423+
const distance = Math.hypot(e.clientX - internalDragState.startX, e.clientY - internalDragState.startY);
424+
const DRAG_THRESHOLD = 5;
425+
426+
if (distance > DRAG_THRESHOLD) {
427+
internalDragState.active = true;
428+
dragInPanel = true;
429+
430+
const layer = internalDragState.listing.entry;
431+
if (!$nodeGraph.selected.includes(layer.id)) {
432+
fakeHighlightOfNotYetSelectedLayerBeingDragged = layer.id;
433+
}
434+
}
435+
}
436+
437+
// Perform drag calculations if a drag is occurring
438+
if (internalDragState.active) {
439+
const select = () => {
440+
if (internalDragState && !$nodeGraph.selected.includes(internalDragState.layerId)) {
441+
selectLayer(internalDragState.listing, false, false);
442+
}
443+
};
444+
445+
draggingData = calculateDragIndex(list, e.clientY, select);
446+
}
447+
}
448+
449+
function draggingPointerUp() {
450+
if (internalDragState?.active && draggingData) {
451+
const { select, insertParentId, insertIndex } = draggingData;
452+
453+
// Commit the move
454+
select?.();
455+
editor.handle.moveLayerInTree(insertParentId, insertIndex);
456+
457+
// Prevent the subsequent click event from processing
458+
justFinishedDrag = true;
459+
} else if (justFinishedDrag) {
460+
// Avoid right-click abort getting stuck with `justFinishedDrag` set and blocking the first subsequent click to select a layer
461+
setTimeout(() => {
462+
justFinishedDrag = false;
463+
}, 0);
464+
}
383465
384-
const target = (event.target instanceof HTMLElement && event.target) || undefined;
385-
const closest = target?.closest("[data-layer]") || undefined;
386-
const draggingELement = (closest instanceof HTMLElement && closest) || undefined;
387-
if (draggingELement) beginDraggingElement(draggingELement);
466+
// Reset state
467+
abortDrag();
468+
}
388469
389-
// Set style of cursor for drag
390-
if (event.dataTransfer) {
391-
event.dataTransfer.dropEffect = "move";
392-
event.dataTransfer.effectAllowed = "move";
470+
function abortDrag() {
471+
internalDragState = undefined;
472+
draggingData = undefined;
473+
fakeHighlightOfNotYetSelectedLayerBeingDragged = undefined;
474+
dragInPanel = false;
475+
}
476+
477+
function draggingMouseDown(e: MouseEvent) {
478+
// Abort if a drag is active and the user presses the right mouse button (button 2)
479+
if (e.button === 2 && internalDragState?.active) {
480+
justFinishedDrag = true;
481+
abortDrag();
393482
}
483+
}
394484
395-
if (list) draggingData = calculateDragIndex(list, event.clientY, select);
485+
function draggingKeyDown(e: KeyboardEvent) {
486+
if (e.key === "Escape" && internalDragState?.active) {
487+
justFinishedDrag = true;
488+
abortDrag();
489+
}
396490
}
397491
398-
function updateInsertLine(event: DragEvent) {
399-
if (!draggable) return;
492+
function fileDragOver(e: DragEvent) {
493+
if (!draggable || !e.dataTransfer || !e.dataTransfer.types.includes("Files")) return;
400494
401495
// Stop the drag from being shown as cancelled
402-
event.preventDefault();
496+
e.preventDefault();
403497
dragInPanel = true;
404498
405-
if (list) draggingData = calculateDragIndex(list, event.clientY, draggingData?.select);
499+
if (list) draggingData = calculateDragIndex(list, e.clientY);
406500
}
407501
408-
function drop(e: DragEvent) {
409-
if (!draggingData) return;
410-
const { select, insertParentId, insertIndex } = draggingData;
502+
function fileDrop(e: DragEvent) {
503+
if (!draggingData || !e.dataTransfer || !e.dataTransfer.types.includes("Files")) return;
504+
505+
const { insertParentId, insertIndex } = draggingData;
411506
412507
e.preventDefault();
413508
414-
if (e.dataTransfer) {
415-
// Moving layers
416-
if (e.dataTransfer.items.length === 0) {
417-
if (draggable && dragInPanel) {
418-
select?.();
419-
editor.handle.moveLayerInTree(insertParentId, insertIndex);
420-
}
509+
Array.from(e.dataTransfer.items).forEach(async (item) => {
510+
const file = item.getAsFile();
511+
if (!file) return;
512+
513+
if (file.type.includes("svg")) {
514+
const svgData = await file.text();
515+
editor.handle.pasteSvg(file.name, svgData, undefined, undefined, insertParentId, insertIndex);
516+
return;
421517
}
422-
// Importing files
423-
else {
424-
Array.from(e.dataTransfer.items).forEach(async (item) => {
425-
const file = item.getAsFile();
426-
if (!file) return;
427-
428-
if (file.type.includes("svg")) {
429-
const svgData = await file.text();
430-
editor.handle.pasteSvg(file.name, svgData, undefined, undefined, insertParentId, insertIndex);
431-
return;
432-
}
433518
434-
if (file.type.startsWith("image")) {
435-
const imageData = await extractPixelData(file);
436-
editor.handle.pasteImage(file.name, new Uint8Array(imageData.data), imageData.width, imageData.height, undefined, undefined, insertParentId, insertIndex);
437-
return;
438-
}
519+
if (file.type.startsWith("image")) {
520+
const imageData = await extractPixelData(file);
521+
editor.handle.pasteImage(file.name, new Uint8Array(imageData.data), imageData.width, imageData.height, undefined, undefined, insertParentId, insertIndex);
522+
return;
523+
}
439524
440-
// When we eventually have sub-documents, this should be changed to import the document instead of opening it in a separate tab
441-
const graphiteFileSuffix = "." + editor.handle.fileExtension();
442-
if (file.name.endsWith(graphiteFileSuffix)) {
443-
const content = await file.text();
444-
const documentName = file.name.slice(0, -graphiteFileSuffix.length);
445-
editor.handle.openDocumentFile(documentName, content);
446-
return;
447-
}
448-
});
525+
// When we eventually have sub-documents, this should be changed to import the document instead of opening it in a separate tab
526+
const graphiteFileSuffix = "." + editor.handle.fileExtension();
527+
if (file.name.endsWith(graphiteFileSuffix)) {
528+
const content = await file.text();
529+
const documentName = file.name.slice(0, -graphiteFileSuffix.length);
530+
editor.handle.openDocumentFile(documentName, content);
531+
return;
449532
}
450-
}
533+
});
451534
452535
draggingData = undefined;
453536
fakeHighlightOfNotYetSelectedLayerBeingDragged = undefined;
@@ -502,16 +585,15 @@
502585
{/if}
503586
<WidgetLayout layout={layersPanelControlBarRightLayout} />
504587
</LayoutRow>
505-
<LayoutRow class="list-area" scrollableY={true}>
588+
<LayoutRow class="list-area" classes={{ "drag-ongoing": Boolean(internalDragState?.active && draggingData) }} scrollableY={true}>
506589
<LayoutCol
507590
class="list"
508591
styles={{ cursor: layerToClipUponClick && layerToClipAltKeyPressed && layerToClipUponClick.entry.clippable ? "alias" : "auto" }}
509592
data-layer-panel
510593
bind:this={list}
511594
on:click={() => deselectAllLayers()}
512-
on:dragover={updateInsertLine}
513-
on:dragend={drop}
514-
on:drop={drop}
595+
on:dragover={fileDragOver}
596+
on:drop={fileDrop}
515597
>
516598
{#each layers as listing, index}
517599
{@const selected = fakeHighlightOfNotYetSelectedLayerBeingDragged !== undefined ? fakeHighlightOfNotYetSelectedLayerBeingDragged === listing.entry.id : listing.entry.selected}
@@ -528,8 +610,7 @@
528610
data-layer
529611
data-index={index}
530612
tooltip={listing.entry.tooltip}
531-
{draggable}
532-
on:dragstart={(e) => draggable && dragStart(e, listing)}
613+
on:pointerdown={(e) => layerPointerDown(e, listing)}
533614
on:click={(e) => selectLayerWithModifiers(e, listing)}
534615
>
535616
{#if listing.entry.childrenAllowed}
@@ -642,10 +723,14 @@
642723
// Layer hierarchy
643724
.list-area {
644725
position: relative;
645-
margin-top: 4px;
726+
padding-top: 4px;
646727
// Combine with the bottom bar to avoid a double border
647728
margin-bottom: -1px;
648729
730+
&.drag-ongoing .layer {
731+
pointer-events: none;
732+
}
733+
649734
.layer {
650735
flex: 0 0 auto;
651736
align-items: center;
@@ -813,7 +898,7 @@
813898
left: 4px;
814899
right: 4px;
815900
background: var(--color-e-nearwhite);
816-
margin-top: -3px;
901+
margin-top: 1px;
817902
height: 5px;
818903
z-index: 1;
819904
pointer-events: none;

frontend/src/io-managers/drag.ts

Lines changed: 0 additions & 24 deletions
This file was deleted.

0 commit comments

Comments
 (0)