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.
250 lines
7.9 KiB
React
250 lines
7.9 KiB
React
6 years ago
|
'use strict';
|
||
|
|
||
|
const React = require("react");
|
||
|
const defaultValue = require("default-value");
|
||
|
|
||
6 years ago
|
const Sidebar = require("./sidebar");
|
||
|
const FloatingWindowArea = require("./floating-window-area");
|
||
|
const { useDraggable, mouseMove } = require("../../hooks/draggable");
|
||
6 years ago
|
|
||
6 years ago
|
/* 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. */
|
||
6 years ago
|
|
||
6 years ago
|
let windowRefs = new Map();
|
||
6 years ago
|
|
||
6 years ago
|
function trackRef(window_, element) {
|
||
|
/* FIXME: Verify that this doesn't leak memory! */
|
||
|
if (element != null) {
|
||
|
windowRefs.set(window_, element);
|
||
|
} else {
|
||
|
windowRefs.delete(window_);
|
||
6 years ago
|
}
|
||
|
}
|
||
|
|
||
6 years ago
|
/* 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
|
||
|
*/
|
||
|
|
||
6 years ago
|
function WindowSpace({children}) {
|
||
|
return (
|
||
|
<div className="windowSpace">
|
||
|
{children}
|
||
|
</div>
|
||
|
);
|
||
|
}
|
||
|
|
||
|
function ContentSpace({children}) {
|
||
|
return (
|
||
|
<div className="contentSpace">
|
||
|
{children}
|
||
|
</div>
|
||
|
);
|
||
|
}
|
||
6 years ago
|
|
||
6 years ago
|
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);
|
||
6 years ago
|
let [barUpdateTrigger, setBarUpdateTrigger] = React.useState(0);
|
||
6 years ago
|
|
||
|
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);
|
||
6 years ago
|
}
|
||
|
}
|
||
6 years ago
|
|
||
|
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);
|
||
|
}
|
||
|
|
||
6 years ago
|
}, [leftBarRef, rightBarRef, barUpdateTrigger]);
|
||
6 years ago
|
|
||
|
React.useEffect(() => {
|
||
|
if (draggedWindow != null) {
|
||
|
if (lastDragType === "move") {
|
||
6 years ago
|
store.setUserPosition(draggedWindow, newX, newY);
|
||
6 years ago
|
} else if (lastDragType === "resize") {
|
||
|
store.setDimensions(draggedWindow, newX, newY);
|
||
6 years ago
|
}
|
||
6 years ago
|
|
||
|
setDraggedWindow(null);
|
||
|
}
|
||
|
}, [newX, newY]);
|
||
|
|
||
6 years ago
|
function recalculateWindow(windowId) {
|
||
|
let element = windowRefs.get(windowId);
|
||
|
|
||
|
let currentPosition = element.getBoundingClientRect();
|
||
|
store.setCurrentPosition(windowId, currentPosition.left, currentPosition.top);
|
||
|
}
|
||
|
|
||
6 years ago
|
function isInArea(x, y, bounds) {
|
||
|
return (x >= bounds.left && x < bounds.right && y >= bounds.top && y < bounds.bottom);
|
||
6 years ago
|
}
|
||
|
|
||
6 years ago
|
function isInLeftBar(x, y) {
|
||
|
return isInArea(x, y, leftBarBounds);
|
||
|
}
|
||
|
|
||
|
function isInRightBar(x, y) {
|
||
|
return isInArea(x, y, rightBarBounds);
|
||
6 years ago
|
}
|
||
|
|
||
|
/* FIXME: Consider sidestepping React entirely for the mousemove event handling. How much do synthetic events slow things down? */
|
||
6 years ago
|
|
||
|
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") {
|
||
6 years ago
|
store.markMoving(window_.id, true);
|
||
6 years ago
|
setIsMoving(true);
|
||
|
} else if (type === "resize") {
|
||
6 years ago
|
store.markResizing(window_.id, true);
|
||
6 years ago
|
setIsResizing(true);
|
||
6 years ago
|
}
|
||
|
|
||
6 years ago
|
setLastDragType(type);
|
||
6 years ago
|
|
||
|
/* 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. */
|
||
6 years ago
|
mouseMove(event.pageX, event.pageY);
|
||
6 years ago
|
}
|
||
|
|
||
|
function handleMouseUp () {
|
||
6 years ago
|
if (draggedWindow != null) {
|
||
6 years ago
|
let window_ = store.get(draggedWindow);
|
||
|
|
||
|
store.markMoving(window_.id, false);
|
||
|
store.markResizing(window_.id, false);
|
||
6 years ago
|
setIsMoving(false);
|
||
|
setIsResizing(false);
|
||
6 years ago
|
|
||
|
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);
|
||
6 years ago
|
}
|
||
|
}
|
||
6 years ago
|
|
||
6 years ago
|
function handleMouseMove(event) {
|
||
|
mouseMove(event.pageX, event.pageY);
|
||
|
}
|
||
|
|
||
|
function onMouseOverBar(_event, side) {
|
||
|
if (isMoving) {
|
||
|
setHighlightBar(side);
|
||
6 years ago
|
}
|
||
|
}
|
||
|
|
||
6 years ago
|
function onMouseOutBar(_event, _side) {
|
||
|
setHighlightBar(null);
|
||
|
}
|
||
|
|
||
6 years ago
|
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);
|
||
|
}
|
||
|
};
|
||
|
|
||
6 years ago
|
/* TODO: Tiled layout */
|
||
|
return (
|
||
|
<div className="windowManager" onMouseMove={handleMouseMove} onMouseUp={handleMouseUp}>
|
||
6 years ago
|
<ContentSpace>
|
||
|
{children}
|
||
|
</ContentSpace>
|
||
|
<WindowSpace>
|
||
6 years ago
|
<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} />
|
||
6 years ago
|
</WindowSpace>
|
||
6 years ago
|
</div>
|
||
|
);
|
||
|
};
|