Skip to content

Commit

Permalink
Support re-ordering MIDI editor/CV output instances via DnD
Browse files Browse the repository at this point in the history
  • Loading branch information
Ameobea committed Feb 9, 2025
1 parent b6229b2 commit 26ec39c
Show file tree
Hide file tree
Showing 10 changed files with 342 additions and 113 deletions.
1 change: 1 addition & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ export default [
},
],
'react/react-in-jsx-scope': 0,
curly: ['warn', 'all'],
},
},
];
4 changes: 3 additions & 1 deletion src/midiEditor/CVOutput/CVOutputControls.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
export let view: Writable<MIDIEditorBaseView>;
export let getCursorPosBeats: () => number;
export let setCursorPosBeats: (newCursorPosBeats: number) => void;
export let activateDrag: () => void;
const expand = () => {
$state.isExpanded = true;
Expand All @@ -26,7 +27,7 @@
</script>

{#if !$state.isExpanded}
<CollapsedCvOutputControls {name} {expand} {deleteOutput} {setName} />
<CollapsedCvOutputControls {name} {expand} {deleteOutput} {setName} {activateDrag} />
{:else}
<CVOutputControlsInner
{name}
Expand All @@ -39,5 +40,6 @@
view={$view}
{getCursorPosBeats}
{setCursorPosBeats}
{activateDrag}
/>
{/if}
8 changes: 7 additions & 1 deletion src/midiEditor/CVOutput/CVOutputControlsInner.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import { mkCVOutputSettingsPopup } from './CVOutputSettingsPopup';
import type { MIDIEditorBaseView } from 'src/midiEditor';
import Cursor from 'src/midiEditor/CVOutput/Cursor.svelte';
import SvelteDragHandle from 'src/midiEditor/SvelteDragHandle.svelte';
export let name: string;
export let setName: (name: string) => void;
Expand All @@ -26,6 +27,7 @@
export let view: MIDIEditorBaseView;
export let getCursorPosBeats: () => number;
export let setCursorPosBeats: (newCursorPosBeats: number) => void;
export let activateDrag: () => void;
let width: number | undefined;
let widthObserver: ResizeObserver | undefined;
Expand Down Expand Up @@ -54,7 +56,11 @@
role="button"
>
<EditableInstanceName {name} {setName} left={PIANO_KEYBOARD_WIDTH} />
<SvelteDragHandle
{activateDrag}
style={{ zIndex: 2, top: 0, left: 28, position: 'absolute', height: 16 }}
/>
<EditableInstanceName {name} {setName} left={60} />
<button class="delete-cv-output-button" on:click={deleteOutput}>✕</button>
</header>

Expand Down
8 changes: 7 additions & 1 deletion src/midiEditor/CVOutput/CollapsedCVOutputControls.svelte
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
<script lang="ts">
import EditableInstanceName from 'src/midiEditor/EditableInstanceName.svelte';
import SvelteDragHandle from 'src/midiEditor/SvelteDragHandle.svelte';
export let name: string;
export let expand: () => void;
export let deleteOutput: () => void;
export let setName: (name: string) => void;
export let activateDrag: () => void;
</script>

<div
Expand All @@ -15,6 +17,10 @@
aria-label="Expand"
role="button"
>
› <EditableInstanceName left={24} {name} {setName} />
› <SvelteDragHandle
{activateDrag}
style={{ zIndex: 2, top: 0, left: 28, position: 'absolute', height: 16 }}
/>
<EditableInstanceName left={60} {name} {setName} />
<button class="delete-cv-output-button" on:click={deleteOutput}>✕</button>
</div>
8 changes: 7 additions & 1 deletion src/midiEditor/CollapsedMIDIEditor.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@
import { PIANO_KEYBOARD_WIDTH } from 'src/midiEditor/conf';
import type { ManagedMIDIEditorUIInstance } from 'src/midiEditor/MIDIEditorUIManager';
import EditableInstanceName from './EditableInstanceName.svelte';
import SvelteDragHandle from 'src/midiEditor/SvelteDragHandle.svelte';
export let parentInstance: MIDIEditorInstance;
export let inst: ManagedMIDIEditorUIInstance;
export let pxPerBeat: Readable<number>;
export let scrollHorizontalBeats: Readable<number>;
export let expand: () => void;
export let instIx: number;
export let activateDrag: () => void;
let minimapContainer: HTMLDivElement | null = null;
let svg: SVGSVGElement | null = null;
Expand Down Expand Up @@ -54,6 +56,10 @@
>
</button>
<SvelteDragHandle
style={{ zIndex: 2, top: -1, left: 28, position: 'absolute', height: 16 }}
{activateDrag}
/>
<button
class="delete-cv-output-button"
on:click={() => parentInstance.uiManager.deleteMIDIEditorInstance(inst.id)}
Expand All @@ -62,7 +68,7 @@
</button>
<EditableInstanceName
left={24}
left={60}
name={inst.name}
setName={newName => parentInstance.uiManager.renameInstance(inst.name, newName)}
transparent
Expand Down
100 changes: 100 additions & 0 deletions src/midiEditor/DnD.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { createContext, useCallback, useRef } from 'react';
import { useDrag, useDrop } from 'react-dnd';

const ItemTypes = {
INSTANCE: 'instance',
};

export const DragActivationContext = createContext<() => void>(() => {});

interface DragHandleProps {
activateDrag: () => void;
style?: React.CSSProperties;
}

export const DragHandle: React.FC<DragHandleProps> = ({ style, activateDrag }) => (
<div
className='drag-handle'
style={style}
onMouseDown={e => {
e.stopPropagation();
activateDrag();
}}
/>
);

interface DraggableInstanceProps {
index: number;
instanceKey: string;
moveInstance: (dragIndex: number, hoverIndex: number) => void;
children: React.ReactNode;
}

export const DraggableInstance: React.FC<DraggableInstanceProps> = ({
index,
instanceKey,
moveInstance,
children,
}) => {
const ref = useRef<HTMLDivElement>(null);
const dragAllowedRef = useRef(false);

const [{ isDragging }, drag] = useDrag({
type: ItemTypes.INSTANCE,
item: { index },
canDrag: () => dragAllowedRef.current,
collect: monitor => ({ isDragging: monitor.isDragging() }),
end: () => {
dragAllowedRef.current = false;
},
});

drag(ref);

const [, drop] = useDrop({
accept: ItemTypes.INSTANCE,
hover(item: { index: number }, monitor) {
if (!ref.current) {
return;
}

const dragIndex = item.index;
const hoverIndex = index;
if (dragIndex === hoverIndex) {
return;
}

const hoverBoundingRect = ref.current.getBoundingClientRect();
const boundingHeight = hoverBoundingRect.bottom - hoverBoundingRect.top;
const hoverCutoffY = boundingHeight < 100 ? boundingHeight / 2 : 50;
const clientOffset = monitor.getClientOffset();
if (!clientOffset) {
return;
}
const hoverClientY = clientOffset.y - hoverBoundingRect.top;

if (
(dragIndex < hoverIndex && hoverClientY < hoverCutoffY) ||
(dragIndex > hoverIndex && hoverClientY > hoverCutoffY)
) {
return;
}
moveInstance(dragIndex, hoverIndex);
item.index = hoverIndex;
},
});

drop(ref);

const activateDrag = useCallback(() => {
dragAllowedRef.current = true;
}, []);

return (
<div ref={ref} key={instanceKey} style={{ opacity: isDragging ? 0.5 : 1 }}>
<DragActivationContext.Provider value={activateDrag}>
{children}
</DragActivationContext.Provider>
</div>
);
};
20 changes: 20 additions & 0 deletions src/midiEditor/MIDIEditor.css
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,26 @@
}
}
}

.drag-handle {
width: 30px;
height: 18px;
border: 1px solid #999;
cursor: grab;

/* vertical ridges to indicate that it's a handle */
background-image: repeating-linear-gradient(
90deg,
#888,
#888 2px,
#444 1px,
#444 4px
);
}

.drag-handle:active {
cursor: grabbing;
}
}

.expanded-midi-editor-instance {
Expand Down
Loading

0 comments on commit 26ec39c

Please sign in to comment.