Implement sidebar docking

feature/core
Sven Slootweg 6 years ago
parent 4efe0c0ad0
commit 79fc14a78e

@ -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;

@ -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 (
<Window elementRef={onWindowRef.bind(null, window_.id)} key={window_.id} style={windowStyle} window={window_} resizeDirection="both" {...handlers} />
);
})
);
};

@ -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 (
<div ref={elementRef} className={classnames("sidebar", `side-${side}`, {highlight: highlight})} onMouseOver={onMouseOverHandler} onMouseOut={onMouseOutHandler}>
</div>
);
}
/* 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 (
<div className="windowSpace">
@ -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 (
<div className="windowManager" onMouseMove={handleMouseMove} onMouseUp={handleMouseUp}>
@ -197,37 +238,11 @@ module.exports = function WindowManager({store, children, windowMoveThreshold})
{children}
</ContentSpace>
<WindowSpace>
<SideBar elementRef={setLeftBarRef} side="left" highlight={highlightBar === "left"} onMouseOver={onMouseOverBar} onMouseOut={onMouseOutBar} />
{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 elementRef={trackRef(window_.id)} key={window_.id} style={windowStyle} title={window_.title} isActive={window_.isActive} resizable={window_.resizable} {...handlers}>
{window_.contents}
</Window>
);
})}
<SideBar elementRef={setRightBarRef} side="right" highlight={highlightBar === "right"} onMouseOver={onMouseOverBar} onMouseOut={onMouseOutBar} />
<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>
);

@ -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 (
<div ref={elementRef} className={classnames("sidebar", `side-${side}`, {highlight: highlight})} onMouseOver={onMouseOverHandler} onMouseOut={onMouseOutHandler} onScroll={onScroll}>
{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 (
<Window elementRef={trackRef(window_.id)} key={window_.id} style={windowStyle} window={window_} resizeDirection="vertical" {...handlers} />
);
})}
</div>
);
};

@ -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 (
<div ref={props.elementRef} className={classnames("window", {active: props.isActive})} style={props.style} onMouseDown={props.onMouseDown}>
<TitleBar title="Window Title Goes Here" onMouseDown={props.onTitleMouseDown} onClose={props.onClose} />
<div ref={elementRef} className={classnames("window", {active: window_.isActive})} style={style} onMouseDown={onMouseDown}>
<TitleBar title="Window Title Goes Here" onMouseDown={onTitleMouseDown} onClose={onClose} />
<div className="body">
<div className="contents">
{props.children}
{window_.contents}
</div>
{renderIf(props.resizable, <Resizer onMouseDown={props.onResizerMouseDown} />)}
{renderIf(window_.resizable, <Resizer onMouseDown={onResizerMouseDown} />)}
</div>
</div>
);

@ -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 {

@ -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;

@ -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;
},

Loading…
Cancel
Save