import React, { ReactNode, useCallback, useEffect, useState } from "react"; import * as SnapUtils from "../snapUtils"; import * as Selectors from "../selectors"; import * as Actions from "../actionCreators"; import { WindowInfo, WindowId, Box, Point } from "../types"; import { useTypedSelector, useActionCreator } from "../hooks"; const abuts = (a: Box, b: Box) => { // TODO: This is kinda a hack. They should really be touching, not just within snapping distance. // Also, overlapping should not count. const wouldMoveTo = SnapUtils.snap(a, b); return wouldMoveTo.x !== undefined || wouldMoveTo.y !== undefined; }; interface Props { windows: { [windowId: string]: ReactNode }; } type DraggingState = { moving: WindowInfo[]; stationary: WindowInfo[]; boundingBox: Box; mouseStart: Point; }; function useHandleMouseDown(propsWindows: { [windowId: string]: ReactNode; }): (key: WindowId, e: React.MouseEvent) => void { const windowsInfo = useTypedSelector(Selectors.getWindowsInfo); const getWindowHidden = useTypedSelector(Selectors.getWindowHidden); const browserWindowSize = useTypedSelector(Selectors.getBrowserWindowSize); const updateWindowPositions = useActionCreator(Actions.updateWindowPositions); const [draggingState, setDraggingState] = useState( null ); // When the mouse is down, attach a listener to track mouse move events. useEffect(() => { if (draggingState == null) { return; } const { boundingBox, moving, stationary, mouseStart } = draggingState; const handleMouseMove = (ee: MouseEvent) => { const proposedDiff = { x: ee.clientX - mouseStart.x, y: ee.clientY - mouseStart.y, }; const proposedWindows = moving.map(node => ({ ...node, ...SnapUtils.applyDiff(node, proposedDiff), })); const proposedBox = { ...boundingBox, ...SnapUtils.applyDiff(boundingBox, proposedDiff), }; const snapDiff = SnapUtils.snapDiffManyToMany( proposedWindows, stationary ); const withinDiff = SnapUtils.snapWithinDiff( proposedBox, browserWindowSize ); const finalDiff = SnapUtils.applyMultipleDiffs( proposedDiff, snapDiff, withinDiff ); const windowPositionDiff: { [windowId: string]: Point } = {}; moving.forEach(w => { windowPositionDiff[w.key] = SnapUtils.applyDiff(w, finalDiff); }); updateWindowPositions(windowPositionDiff, false); }; function handleMouseUp() { setDraggingState(null); } window.addEventListener("mouseup", handleMouseUp); window.addEventListener("mousemove", handleMouseMove); return () => { window.removeEventListener("mousemove", handleMouseMove); window.removeEventListener("mouseup", handleMouseUp); }; }, [browserWindowSize, draggingState, updateWindowPositions]); // Mouse down handler return useCallback( (key: WindowId, e: React.MouseEvent) => { if (!(e.target as HTMLElement).classList.contains("draggable")) { return; } if (getWindowHidden(key)) { // The user may be clicking on full screen Milkdrop. return; } const windows = windowsInfo.filter( w => propsWindows[w.key] != null && !getWindowHidden(w.key) ); const targetNode = windows.find(node => node.key === key); if (targetNode == null) { throw new Error(`Tried to move a node that does not exist: ${key}`); } let movingSet = new Set([targetNode]); // Only the main window brings other windows along. if (key === "main") { const findAllConnected = SnapUtils.traceConnection(abuts); movingSet = findAllConnected(windows, targetNode); } const stationary = windows.filter(w => !movingSet.has(w)); const moving = Array.from(movingSet); const mouseStart = { x: e.clientX, y: e.clientY }; const boundingBox = SnapUtils.boundingBox(moving); setDraggingState({ boundingBox, moving, stationary, mouseStart }); }, [getWindowHidden, propsWindows, windowsInfo] ); } export default function WindowManager({ windows: propsWindows }: Props) { const windowsInfo = useTypedSelector(Selectors.getWindowsInfo); const setFocusedWindow = useActionCreator(Actions.setFocusedWindow); const handleMouseDown = useHandleMouseDown(propsWindows); const windows = windowsInfo.filter(w => propsWindows[w.key]); const onBlur = useCallback( // I give up on trying to type things with `relatedTarget`. (e: any) => { const { currentTarget, relatedTarget } = e; if ( currentTarget === relatedTarget || currentTarget.contains(relatedTarget) ) { return; } setFocusedWindow(null); }, [setFocusedWindow] ); return ( <> {windows.map(w => (
) => { handleMouseDown(w.key, e); }} style={{ position: "absolute", top: 0, left: 0, transform: `translate(${w.x}px, ${w.y}px)`, }} > {propsWindows[w.key]}
))} ); }