You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
openNG/src/client/components/window-manager.jsx

181 lines
6.5 KiB
JavaScript

'use strict';
const React = require("react");
const createReactClass = require("create-react-class");
const throttleit = require("throttleit");
const euclideanDistance = require("euclidean-distance");
const defaultValue = require("default-value");
const Window = require("./window");
/* These can be stored outside a component as effective globals, because there can only be one mouse position anyway. */
let lastMouseX = 0;
let lastMouseY = 0;
let windowRefs = new Map();
function trackRef(window_) {
return function setRef(element) {
/* FIXME: Verify that this doesn't leak memory! */
if (element != null) {
windowRefs.set(window_, element);
} else {
windowRefs.delete(window_);
}
};
}
module.exports = function WindowManager({store, children, windowMoveThreshold_}) {
let [movingType, setMovingType] = React.useState(null);
let [movingWindow, setMovingWindow] = React.useState(null);
let [movingWindowBarX, setMovingWindowBarX] = React.useState(null);
let [movingWindowBarY, setMovingWindowBarY] = React.useState(null);
let [movingWindowOriginalWidth, setMovingWindowOriginalWidth] = React.useState(null);
let [movingWindowOriginalHeight, setMovingWindowOriginalHeight] = React.useState(null);
let [movingWindowOriginX, setMovingWindowOriginX] = React.useState(null);
let [movingWindowOriginY, setMovingWindowOriginY] = React.useState(null);
let [movingWindowThresholdMet, setMovingWindowThresholdMet] = React.useState(false);
let [processMouseMove, setProcessMouseMove] = React.useState(null);
let windowMoveThreshold = defaultValue(windowMoveThreshold_, 6);
React.useEffect(() => {
processMouseMove = throttleit(() => {
/* Yes, the below check is there for a reason; just in case the `movingWindow` state changes between the call to the throttled wrapper and the wrapped function itself. */
if (movingWindow != null) {
let thresholdNewlyMet = false;
if (movingWindowThresholdMet === false) {
let origin = [movingWindowOriginX, movingWindowOriginY];
let position = [lastMouseX, lastMouseY];
if (euclideanDistance(origin, position) > windowMoveThreshold) {
thresholdNewlyMet = true;
setMovingWindowThresholdMet(true);
}
}
if (movingWindowThresholdMet || thresholdNewlyMet === true) {
/* Note: we handle this out-of-band, to avoid going through React's rendering cycle for every move event. */
let element = windowRefs.get(movingWindow);
if (movingType === "move") {
let {x, y} = getCurrentMoveValues();
element.style.transform = `translate(${x}px, ${y}px)`;
} else if (movingType === "resize") {
let {width, height} = getCurrentResizeValues();
element.style.width = `${width}px`;
element.style.height = `${height}px`;
}
}
}
}, 10);
/* NOTE: We first manually set the processMouseMove above, to ensure that the new function instance is immediately available in the render code below. */
/* HACK: React interprets functions as lazy setters, therefore we return the wrapped processMouseMove function from an arrow function. */
setProcessMouseMove(() => processMouseMove);
}, [movingWindow]);
function getCurrentMoveValues() {
return {
x: lastMouseX - movingWindowBarX,
y: lastMouseY - movingWindowBarY
};
}
function getCurrentResizeValues() {
return {
width: movingWindowOriginalWidth + (lastMouseX - movingWindowOriginX),
height: movingWindowOriginalHeight + (lastMouseY - movingWindowOriginY)
};
}
/* NOTE: Due to how synthetic events work in React, we need to separate out the 'get coordinates from event' and 'do something with the coordinates' step; if we throttle the coordinate extraction logic, we'll run into problems when synthetic events have already been cleared for reuse by the time we try to obtain the coordinates. Therefore, `processMouseMove` contains all the throttled logic, whereas the coordinate extraction happens on *every* mousemove event. */
/* FIXME: Consider sidestepping React entirely for the mousemove event handling. How much do synthetic events slow things down? */
function handleMouseMove(event) {
if (movingWindow != null) {
lastMouseX = event.pageX;
lastMouseY = event.pageY;
processMouseMove();
}
}
function handleDragStart(type, window_, event, barX, barY) {
setMovingType(type);
setMovingWindow(window_.id);
setMovingWindowThresholdMet(false);
setMovingWindowBarX(barX);
setMovingWindowBarY(barY);
setMovingWindowOriginX(event.pageX);
setMovingWindowOriginY(event.pageY);
/* NOTE: The below is to ensure that the window doesn't jump, if the user clicks the titlebar but never moves the mouse. No mousemove event is fired in that scenario, so if we don't set the last-known coordinates here, the window would jump based on whatever coordinate the mouse was last at during the *previous* window drag operation. */
lastMouseX = event.pageX;
lastMouseY = event.pageY;
console.log("started drag of type", type);
}
function handleTitleMouseDown (window_, event, barX, barY) {
return handleDragStart("move", window_, event, barX, barY);
}
function handleResizerMouseDown(window_, event, barX, barY) {
setMovingWindowOriginalWidth(window_.width);
setMovingWindowOriginalHeight(window_.height);
return handleDragStart("resize", window_, event, barX, barY);
}
function handleMouseUp () {
if (movingWindow != null) {
if (movingType === "move") {
let {x, y} = getCurrentMoveValues();
store.setPosition(movingWindow, x, y);
} else if (movingType === "resize") {
let {width, height} = getCurrentResizeValues();
store.setDimensions(movingWindow, width, height);
}
setMovingWindow(null);
}
}
/* TODO: Tiled layout */
return (
<div className="windowManager" onMouseMove={handleMouseMove} onMouseUp={handleMouseUp}>
{children}
{store.getAll().toArray().map((window_) => {
let windowStyle = {
transform: `translate(${window_.x}px, ${window_.y}px)`,
width: window_.width,
height: window_.height,
zIndex: window_.zIndex
};
let handlers = {
onTitleMouseDown: (...args) => {
return handleTitleMouseDown(window_, ...args);
},
onResizerMouseDown: (...args) => {
return handleResizerMouseDown(window_, ...args);
},
onMouseDown: () => {
store.focus(window_.id);
},
onClose: () => {
store.close(window_.id);
}
};
return (
<Window elementRef={trackRef(window_.id)} key={window_.id} style={windowStyle} title={window_.title} isActive={window_.isActive} resizable={window_.resizable} {...handlers}>
{window_.contents}
</Window>
);
})}
</div>
);
};