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.

251 lines
7.9 KiB
JavaScript

'use strict';
const React = require("react");
const defaultValue = require("default-value");
const Sidebar = require("./sidebar");
const FloatingWindowArea = require("./floating-window-area");
const { useDraggable, mouseMove } = require("../../hooks/draggable");
/* FIXME: While the WindowManager area is used as a 'valid to drag' area, such that an embedded window manager in a specific section of the application is possible, all the actual coordination calculation code currently assumes that the WindowManager area starts at (0,0). That needs to eventually be fixed. */
let windowRefs = new Map();
function trackRef(window_, element) {
/* FIXME: Verify that this doesn't leak memory! */
if (element != null) {
windowRefs.set(window_, element);
} else {
windowRefs.delete(window_);
}
}
/* TODO:
- Default width for sidebar
- Flip sidebar'd windows from corner resize to bottom resize
- Width-scale sidebar'd windows with sidebar
- Reorderable sidebar windows
- Insertion indicator while sidebarring floating windows or reordering sidebar'd windows
- Reserve space for used sidebar
*/
function WindowSpace({children}) {
return (
<div className="windowSpace">
{children}
</div>
);
}
function ContentSpace({children}) {
return (
<div className="contentSpace">
{children}
</div>
);
}
module.exports = function WindowManager({store, children, windowMoveThreshold}) {
let [isResizing, setIsResizing] = React.useState(false);
let [isMoving, setIsMoving] = React.useState(false);
let [lastDragType, setLastDragType] = React.useState(null);
let [dragOperationData, setDragOperationData] = React.useState(null);
let [draggedWindow, setDraggedWindow] = React.useState(null);
let [highlightBar, setHighlightBar] = React.useState(null);
let [leftBarRef, setLeftBarRef] = React.useState(null);
let [rightBarRef, setRightBarRef] = React.useState(null);
let [leftBarBounds, setLeftBarBounds] = React.useState(null);
let [rightBarBounds, setRightBarBounds] = React.useState(null);
let [barUpdateTrigger, setBarUpdateTrigger] = React.useState(0);
let [newX, newY] = useDraggable({
isDragging: isResizing || isMoving,
operationData: dragOperationData,
threshold: defaultValue(windowMoveThreshold, 6),
onChange: (xValue, yValue, mouseX, mouseY) => {
/* NOTE: we handle this out-of-band, to avoid going through React's rendering cycle for every move event. */
let element = windowRefs.get(draggedWindow);
if (isMoving) {
/* NOTE: We *only* change the highlightBar state if necessary, because otherwise we would still go through a rendering cycle for every move event after all. */
if (isInLeftBar(mouseX, mouseY)) {
if (highlightBar !== "left") {
setHighlightBar("left");
}
} else if (isInRightBar(mouseX, mouseY)) {
if (highlightBar !== "right") {
setHighlightBar("right");
}
} else {
if (highlightBar != null) {
setHighlightBar(null);
}
}
element.style.transform = `translate(${xValue}px, ${yValue}px)`;
} else if (isResizing) {
element.style.width = `${xValue}px`;
element.style.height = `${yValue}px`;
}
}
});
React.useEffect(() => {
if (leftBarRef != null) {
setLeftBarBounds(leftBarRef.getBoundingClientRect());
} else {
setLeftBarBounds(null);
}
if (rightBarRef != null) {
setRightBarBounds(rightBarRef.getBoundingClientRect());
} else {
setRightBarBounds(null);
}
}, [leftBarRef, rightBarRef, barUpdateTrigger]);
React.useEffect(() => {
if (draggedWindow != null) {
if (lastDragType === "move") {
store.setUserPosition(draggedWindow, newX, newY);
} else if (lastDragType === "resize") {
store.setDimensions(draggedWindow, newX, newY);
}
setDraggedWindow(null);
}
}, [newX, newY]);
function recalculateWindow(windowId) {
let element = windowRefs.get(windowId);
let currentPosition = element.getBoundingClientRect();
store.setCurrentPosition(windowId, currentPosition.left, currentPosition.top);
}
function isInArea(x, y, bounds) {
return (x >= bounds.left && x < bounds.right && y >= bounds.top && y < bounds.bottom);
}
function isInLeftBar(x, y) {
return isInArea(x, y, leftBarBounds);
}
function isInRightBar(x, y) {
return isInArea(x, y, rightBarBounds);
}
/* FIXME: Consider sidestepping React entirely for the mousemove event handling. How much do synthetic events slow things down? */
function handleDragStart(type, window_, event, currentX, currentY) {
setDraggedWindow(window_.id);
setDragOperationData({
initialMouseX: event.pageX,
initialMouseY: event.pageY,
initialValueX: currentX, // width or X pos
initialValueY: currentY // height or Y pos
});
if (type === "move") {
store.markMoving(window_.id, true);
setIsMoving(true);
} else if (type === "resize") {
store.markResizing(window_.id, true);
setIsResizing(true);
}
setLastDragType(type);
/* 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. */
mouseMove(event.pageX, event.pageY);
}
function handleMouseUp () {
if (draggedWindow != null) {
let window_ = store.get(draggedWindow);
store.markMoving(window_.id, false);
store.markResizing(window_.id, false);
setIsMoving(false);
setIsResizing(false);
if (highlightBar != null) {
if (window_.sidebar !== highlightBar) {
store.moveToSidebar(window_.id, highlightBar);
setBarUpdateTrigger(Math.random());
} else {
windowRefs.get(window_.id).style.transform = null;
}
} else {
if (window_.sidebar != null) {
store.makeFloating(window_.id);
setBarUpdateTrigger(Math.random());
}
}
setHighlightBar(null);
}
}
function handleMouseMove(event) {
mouseMove(event.pageX, event.pageY);
}
function onMouseOverBar(_event, side) {
if (isMoving) {
setHighlightBar(side);
}
}
function onMouseOutBar(_event, _side) {
setHighlightBar(null);
}
let windowHandlers = {
onClose: function (window_) {
store.close(window_.id);
},
onFocus: function (window_) {
store.focus(window_.id);
},
onStartMove: function (window_, event) {
// /* NOTE: We use the current actual on-screen position of the window's DOM element here instead of the position in the store, because when a window is docked in the sidebar, the store-provided position is incorrect. */
// let currentPosition = event.target.getBoundingClientRect();
// return handleDragStart("move", window_, event, currentPosition.left, currentPosition.top);
/* FIXME: Immediatelly call onChange from the draggable hook, to prevent a jump when originating from a sidebar dock? */
return handleDragStart("move", window_, event, window_.x, window_.y);
},
onStartCornerResize: function (window_, event) {
return handleDragStart("resize", window_, event, window_.width, window_.height);
},
onStartBottomResize: function (window_, _event) {
// TODO
},
onRecalculateWindowPosition: function (windowId) {
recalculateWindow(windowId);
},
onWindowRef: function (windowId, ref) {
trackRef(windowId, ref);
}
};
/* TODO: Tiled layout */
return (
<div className="windowManager" onMouseMove={handleMouseMove} onMouseUp={handleMouseUp}>
<ContentSpace>
{children}
</ContentSpace>
<WindowSpace>
<div className="fixedAreas">
<Sidebar elementRef={setLeftBarRef} side="left" highlight={highlightBar === "left"} windows={store.getLeftSidebar()} {...windowHandlers} onMouseOver={onMouseOverBar} onMouseOut={onMouseOutBar} />
<Sidebar elementRef={setRightBarRef} side="right" highlight={highlightBar === "right"} windows={store.getRightSidebar()} {...windowHandlers} onMouseOver={onMouseOverBar} onMouseOut={onMouseOutBar} />
</div>
<FloatingWindowArea windows={store.getFloating()} {...windowHandlers} />
</WindowSpace>
</div>
);
};