diff --git a/public/css/components.css b/public/css/components.css index 8460782..9bacb0d 100644 --- a/public/css/components.css +++ b/public/css/components.css @@ -11,27 +11,36 @@ grid-template-rows: auto 1fr; } .windowManager .windowSpace { position: relative; } - .windowManager .sidebar { + .windowManager .fixedAreas { position: absolute; top: 0px; bottom: 0px; - min-width: 80px; - height: 100%; - background-color: rgba(0, 0, 0, 0.3); - border-color: #121b26; - border-width: 1px; } - .windowManager .sidebar.side-left { - left: 0px; - border-right-style: solid; } - .windowManager .sidebar.side-right { - right: 0px; - border-left-style: solid; } - .windowManager .sidebar.highlight { - background-color: rgba(9, 14, 54, 0.3); - border-color: #0d4c99; } + left: 0px; + right: 0px; + display: flex; } + .windowManager .fixedAreas .sidebar { + min-width: 80px; + overflow-y: auto; + background-color: rgba(0, 0, 0, 0.5); + border-color: #121b26; + border-width: 1px; } + .windowManager .fixedAreas .sidebar.side-left { + left: 0px; + border-right-style: solid; } + .windowManager .fixedAreas .sidebar.side-right { + right: 0px; + border-left-style: solid; + margin-left: auto; } + .windowManager .fixedAreas .sidebar.highlight { + background-color: rgba(9, 14, 54, 0.5); + border-color: #0d4c99; } + .windowManager .fixedAreas .sidebar .window { + position: static; + /* FIXME: Below 19px should become 9px, once there's a way to account for the scrollbar that may be visible. */ + margin: 0px 19px 12px 9px; } .window { - position: absolute; + position: fixed; left: 0px; top: 0px; display: grid; diff --git a/src/client/components/window-manager/floating-window-area.jsx b/src/client/components/window-manager/floating-window-area.jsx new file mode 100644 index 0000000..9e3bc7e --- /dev/null +++ b/src/client/components/window-manager/floating-window-area.jsx @@ -0,0 +1,37 @@ +"use strict"; + +const React = require("react"); + +const Window = require("../window"); + +module.exports = function FloatingWindowArea({windows, onStartMove, onStartCornerResize, onClose, onFocus, onWindowRef}) { + return ( + windows.map((window_) => { + let windowStyle = { + transform: `translate(${window_.x}px, ${window_.y}px)`, + width: window_.width, + height: window_.height, + zIndex: window_.zIndex + }; + + let handlers = { + onTitleMouseDown: (event) => { + onStartMove(window_, event); + }, + onResizerMouseDown: (event) => { + onStartCornerResize(window_, event); + }, + onMouseDown: () => { + onFocus(window_); + }, + onClose: () => { + onClose(window_); + } + }; + + return ( + + ); + }) + ); +}; diff --git a/src/client/components/window-manager.jsx b/src/client/components/window-manager/index.jsx similarity index 55% rename from src/client/components/window-manager.jsx rename to src/client/components/window-manager/index.jsx index 84ec498..12d4635 100644 --- a/src/client/components/window-manager.jsx +++ b/src/client/components/window-manager/index.jsx @@ -1,42 +1,33 @@ 'use strict'; const React = require("react"); -const classnames = require("classnames"); - const defaultValue = require("default-value"); -const Window = require("./window"); -const { useDraggable, mouseMove } = require("../hooks/draggable"); - -let windowRefs = new Map(); +const Sidebar = require("./sidebar"); +const FloatingWindowArea = require("./floating-window-area"); +const { useDraggable, mouseMove } = require("../../hooks/draggable"); -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_); - } - }; -} +/* 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. */ -function SideBar({elementRef, side, onMouseOver, onMouseOut, highlight}) { - function onMouseOverHandler(event) { - onMouseOver(event, side); - } +let windowRefs = new Map(); - function onMouseOutHandler(event) { - onMouseOut(event, side); +function trackRef(window_, element) { + /* FIXME: Verify that this doesn't leak memory! */ + if (element != null) { + windowRefs.set(window_, element); + } else { + windowRefs.delete(window_); } - - return ( -
- -
- ); } +/* 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 +*/ + function WindowSpace({children}) { return (
@@ -65,6 +56,7 @@ module.exports = function WindowManager({store, children, windowMoveThreshold}) 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, @@ -111,12 +103,12 @@ module.exports = function WindowManager({store, children, windowMoveThreshold}) setRightBarBounds(null); } - }, [leftBarRef, rightBarRef]); + }, [leftBarRef, rightBarRef, barUpdateTrigger]); React.useEffect(() => { if (draggedWindow != null) { if (lastDragType === "move") { - store.setPosition(draggedWindow, newX, newY); + store.setUserPosition(draggedWindow, newX, newY); } else if (lastDragType === "resize") { store.setDimensions(draggedWindow, newX, newY); } @@ -125,6 +117,13 @@ module.exports = function WindowManager({store, children, windowMoveThreshold}) } }, [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); } @@ -150,8 +149,10 @@ module.exports = function WindowManager({store, children, windowMoveThreshold}) }); if (type === "move") { + store.markMoving(window_.id, true); setIsMoving(true); } else if (type === "resize") { + store.markResizing(window_.id, true); setIsResizing(true); } @@ -161,18 +162,30 @@ module.exports = function WindowManager({store, children, windowMoveThreshold}) mouseMove(event.pageX, event.pageY); } - function handleTitleMouseDown (window_, event) { - return handleDragStart("move", window_, event, window_.x, window_.y); - } - - function handleResizerMouseDown(window_, event) { - return handleDragStart("resize", window_, event, window_.width, window_.height); - } - 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); } } @@ -190,6 +203,34 @@ module.exports = function WindowManager({store, children, windowMoveThreshold}) 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 (
@@ -197,37 +238,11 @@ module.exports = function WindowManager({store, children, windowMoveThreshold}) {children} - - {store.getFloating().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_.contents} - - ); - })} - +
+ + +
+
); diff --git a/src/client/components/window-manager/sidebar.jsx b/src/client/components/window-manager/sidebar.jsx new file mode 100644 index 0000000..e058dee --- /dev/null +++ b/src/client/components/window-manager/sidebar.jsx @@ -0,0 +1,74 @@ +"use strict"; + +const React = require("react"); +const throttleit = require("throttleit"); +const classnames = require("classnames"); + +const Window = require("../window"); + +module.exports = function Sidebar({elementRef, side, windows, onMouseOver, onMouseOut, highlight, onStartMove, onStartBottomResize, onClose, onFocus, onRecalculateWindowPosition, onWindowRef}) { + let [onScroll, setOnScroll] = React.useState(null); + + React.useEffect(() => { + onScroll = throttleit(() => { + windows.forEach((window_) => { + onRecalculateWindowPosition(window_.id); + }); + }, 100); + + /* NOTE: Lazy initializer */ + setOnScroll(() => { + return onScroll; + }); + }, [windows]); + + function onMouseOverHandler(event) { + onMouseOver(event, side); + } + + function onMouseOutHandler(event) { + onMouseOut(event, side); + } + + function trackRef(windowId) { + return function (ref) { + onWindowRef(windowId, ref); + + if (ref != null) { + onRecalculateWindowPosition(windowId); + } + }; + } + + return ( +
+ {windows.map((window_) => { + let windowStyle = { + width: window_.width, + height: window_.height, + /* The below is a bit of a hack to ensure that a being-dragged window always displays with coordinates absolute to the page, not relative to the sidebar. */ + position: (window_.isMoving) ? "fixed" : "static" + }; + + let handlers = { + onTitleMouseDown: (event) => { + onStartMove(window_, event); + }, + onResizerMouseDown: (event) => { + onStartBottomResize(window_, event); + }, + onMouseDown: () => { + onFocus(window_); + }, + onClose: () => { + onClose(window_); + } + }; + + return ( + + ); + })} +
+ ); +}; diff --git a/src/client/components/window.jsx b/src/client/components/window.jsx index 38bf44c..cb15eb1 100644 --- a/src/client/components/window.jsx +++ b/src/client/components/window.jsx @@ -77,17 +77,17 @@ function Resizer({onMouseDown}) { ); } -module.exports = function Window(props) { +module.exports = function Window({elementRef, style, window: window_, onMouseDown, onTitleMouseDown, onClose, onResizerMouseDown}) { return ( -
- +
+
- {props.children} + {window_.contents}
- {renderIf(props.resizable, )} + {renderIf(window_.resizable, )}
); diff --git a/src/client/hooks/draggable.js b/src/client/hooks/draggable.js index b05efc5..22cec54 100644 --- a/src/client/hooks/draggable.js +++ b/src/client/hooks/draggable.js @@ -56,12 +56,13 @@ module.exports = { } }, useDraggable: function (options) { - let dragEnded; + let dragEnded = false; + let dragStarted = false; if (isDragging === true && options.isDragging === false) { dragEnded = true; - } else { - dragEnded = false; + } else if (isDragging === false && options.isDragging === true) { + dragStarted = true; } operationData = options.operationData; @@ -69,6 +70,10 @@ module.exports = { onChangeCallback = options.onChange; isDragging = options.isDragging; + if (dragStarted) { + processMouseMove(); + } + if (dragEnded) { return [newX, newY]; } else { diff --git a/src/client/scss/components.scss b/src/client/scss/components.scss index 0ef8d64..e95b448 100644 --- a/src/client/scss/components.scss +++ b/src/client/scss/components.scss @@ -23,36 +23,54 @@ position: relative; } - .sidebar { + .fixedAreas { position: absolute; top: 0px; bottom: 0px; - min-width: 80px; - height: 100%; + left: 0px; + right: 0px; + display: flex; + + .sidebar { + // position: absolute; + // top: 0px; + // bottom: 0px; + min-width: 80px; + // height: 100%; + // overflow-x: hidden; + overflow-y: auto; - background-color: rgba(0, 0, 0, 0.3); - border-color: rgb(18, 27, 38); - border-width: 1px; + background-color: rgba(0, 0, 0, 0.5); + border-color: rgb(18, 27, 38); + border-width: 1px; - &.side-left { - left: 0px; - border-right-style: solid; - } + &.side-left { + left: 0px; + border-right-style: solid; + } - &.side-right { - right: 0px; - border-left-style: solid; - } + &.side-right { + right: 0px; + border-left-style: solid; + margin-left: auto; + } - &.highlight { - background-color: rgba(9, 14, 54, 0.3); - border-color: rgb(13, 76, 153); + &.highlight { + background-color: rgba(9, 14, 54, 0.5); + border-color: rgb(13, 76, 153); + } + + .window { + position: static; + /* FIXME: Below 19px should become 9px, once there's a way to account for the scrollbar that may be visible. */ + margin: 0px 19px 12px 9px; + } } } } .window { - position: absolute; + position: fixed; left: 0px; top: 0px; diff --git a/src/client/stores/windows.js b/src/client/stores/windows.js index ce02ea8..9098208 100644 --- a/src/client/stores/windows.js +++ b/src/client/stores/windows.js @@ -6,27 +6,42 @@ const immutable = require("immutable"); module.exports = function createWindowStore({onUpdated}) { let windows = immutable.OrderedMap([]); - let floatingWindows = immutable.OrderedMap([]); - let leftSidebarWindows = immutable.OrderedMap([]); - let rightSidebarWindows = immutable.OrderedMap([]); + let floatingWindows = []; + let leftSidebarWindows = []; + let rightSidebarWindows = []; let activeWindow; let lastZIndex = 0; function updateWindowIndexes() { - floatingWindows = windows.filter((window_) => window_.sidebar == null); - leftSidebarWindows = windows.filter((window_) => window_.sidebar === "left"); - rightSidebarWindows = windows.filter((window_) => window_.sidebar === "right"); + floatingWindows = windows.filter((window_) => window_.sidebar == null).toArray(); + leftSidebarWindows = windows.filter((window_) => window_.sidebar === "left").toArray(); + rightSidebarWindows = windows.filter((window_) => window_.sidebar === "right").toArray(); } function setSide(id, side) { + setValues(id, { + sidebar: side + }, () => { + updateWindowIndexes(); + }); + } + + function setValues(id, values, postProcessing) { let targetWindow = windows.get(id); - targetWindow.sidebar = side; + Object.assign(targetWindow, values); + + if (postProcessing != null) { + postProcessing(targetWindow); + } - updateWindowIndexes(); onUpdated(); } + function setValuesWithoutUpdate(id, values) { + Object.assign( windows.get(id), values); + } + return { add: function (window_) { let id = nanoid(); @@ -35,6 +50,8 @@ module.exports = function createWindowStore({onUpdated}) { id: id, x: window_.initialX, y: window_.initialY, + userX: 0, + userY: 0, width: window_.initialWidth, height: window_.initialHeight })); @@ -59,21 +76,35 @@ module.exports = function createWindowStore({onUpdated}) { onUpdated(); }, - setPosition: function (id, x, y) { - let targetWindow = windows.get(id); - - targetWindow.x = x; - targetWindow.y = y; - - onUpdated(); + setUserPosition: function (id, x, y) { + setValues(id, { + userX: x, + userY: y, + x: x, + y: y + }); + }, + setCurrentPosition: function (id, x, y) { + setValuesWithoutUpdate(id, { + x: x, + y: y + }); }, setDimensions: function (id, width, height) { - let targetWindow = windows.get(id); - - targetWindow.width = width; - targetWindow.height = height; - - onUpdated(); + setValues(id, { + width: width, + height: height + }); + }, + markMoving: function (id, isMoving) { + setValues(id, { + isMoving: isMoving + }); + }, + markResizing: function (id, isResizing) { + setValues(id, { + isResizing: isResizing + }); }, moveToSidebar: function (id, side) { setSide(id, side); @@ -86,6 +117,9 @@ module.exports = function createWindowStore({onUpdated}) { updateWindowIndexes(); onUpdated(); }, + get: function (id) { + return windows.get(id); + }, getAll: function () { return windows; },