mirror of
https://github.com/captbaritone/webamp.git
synced 2026-07-25 19:13:54 +00:00
Main window brings connected windows along.
This commit is contained in:
parent
2fe465ddf7
commit
994c725f60
3 changed files with 347 additions and 23 deletions
|
|
@ -1,11 +1,27 @@
|
|||
import React from "react";
|
||||
import { findDOMNode } from "react-dom";
|
||||
|
||||
import { snapToMany, snapWithin, applySnap } from "../snapUtils";
|
||||
import {
|
||||
snapDiffManyToMany,
|
||||
boundingBox,
|
||||
snapWithinDiff,
|
||||
snap,
|
||||
traceConnection
|
||||
} from "../snapUtils";
|
||||
|
||||
const WINDOW_HEIGHT = 116;
|
||||
const WINDOW_WIDTH = 275;
|
||||
|
||||
const abuts = (a, b) => {
|
||||
const wouldMoveTo = snap(a, b);
|
||||
return wouldMoveTo.x !== undefined || wouldMoveTo.y !== undefined;
|
||||
};
|
||||
|
||||
const applyDiff = (a, b) => ({
|
||||
x: a.x + b.x,
|
||||
y: a.y + b.y
|
||||
});
|
||||
|
||||
class WindowManager extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
|
@ -42,16 +58,20 @@ class WindowManager extends React.Component {
|
|||
this.windowNodes.push(node);
|
||||
}
|
||||
|
||||
otherWindows(element) {
|
||||
const domNodes = this.windowNodes.map(findDOMNode);
|
||||
return domNodes.filter(node => node !== element);
|
||||
windows() {
|
||||
return this.windowNodes.map(findDOMNode);
|
||||
}
|
||||
|
||||
nodeInfo(node) {
|
||||
otherWindows(element) {
|
||||
return this.windows.filter(node => node !== element);
|
||||
}
|
||||
|
||||
nodeInfo(node, i) {
|
||||
const child = node.childNodes[0];
|
||||
const { height, width } = child.getBoundingClientRect();
|
||||
const { offsetLeft, offsetTop } = node;
|
||||
return {
|
||||
i,
|
||||
x: offsetLeft,
|
||||
y: offsetTop,
|
||||
height,
|
||||
|
|
@ -63,44 +83,71 @@ class WindowManager extends React.Component {
|
|||
if (!e.target.classList.contains("draggable")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const mouseStart = {
|
||||
x: e.clientX,
|
||||
y: e.clientY
|
||||
};
|
||||
|
||||
const windowStart = this.nodeInfo(e.currentTarget);
|
||||
const targetNode = this.nodeInfo(e.currentTarget, i);
|
||||
|
||||
const otherWindowNodes = this.otherWindows(e.currentTarget).map(
|
||||
this.nodeInfo
|
||||
);
|
||||
const windows = this.windows(e.currentTarget).map(this.nodeInfo);
|
||||
|
||||
let movingSet = new Set([targetNode]);
|
||||
|
||||
// Only the main window brings other windows along.
|
||||
if (targetNode.i === 0) {
|
||||
const findAllConnected = traceConnection(abuts);
|
||||
movingSet = findAllConnected(windows, targetNode);
|
||||
}
|
||||
|
||||
const stationary = windows.filter(w => !movingSet.has(w));
|
||||
const moving = Array.from(movingSet);
|
||||
|
||||
const browserSize = {
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight
|
||||
};
|
||||
|
||||
const box = boundingBox(moving);
|
||||
|
||||
const handleMouseMove = ee => {
|
||||
const diff = {
|
||||
const proposedDiff = {
|
||||
x: ee.clientX - mouseStart.x,
|
||||
y: ee.clientY - mouseStart.y
|
||||
};
|
||||
|
||||
const proposedWindow = {
|
||||
...windowStart,
|
||||
x: windowStart.x + diff.x,
|
||||
y: windowStart.y + diff.y
|
||||
const proposedWindows = moving.map(node => ({
|
||||
...node,
|
||||
...applyDiff(node, proposedDiff)
|
||||
}));
|
||||
|
||||
const proposedBox = {
|
||||
...box,
|
||||
...applyDiff(box, proposedDiff)
|
||||
};
|
||||
|
||||
const snappedToOthers = snapToMany(proposedWindow, otherWindowNodes);
|
||||
const snappedToBrowser = snapWithin(proposedWindow, browserSize);
|
||||
const snapDiff = snapDiffManyToMany(proposedWindows, stationary);
|
||||
|
||||
const newPosition = applySnap(
|
||||
proposedWindow,
|
||||
snappedToBrowser,
|
||||
snappedToOthers
|
||||
);
|
||||
const withinDiff = snapWithinDiff(proposedBox, browserSize);
|
||||
|
||||
this.setState({ [i]: { top: newPosition.y, left: newPosition.x } });
|
||||
let finalDiff = proposedDiff;
|
||||
if (withinDiff.x !== 0 || withinDiff.y !== 0) {
|
||||
finalDiff = applyDiff(proposedDiff, withinDiff);
|
||||
} else {
|
||||
finalDiff = applyDiff(proposedDiff, snapDiff);
|
||||
}
|
||||
|
||||
const stateDiff = moving.reduce((diff, window) => {
|
||||
const newWindowLocation = applyDiff(window, finalDiff);
|
||||
diff[window.i] = {
|
||||
top: newWindowLocation.y,
|
||||
left: newWindowLocation.x
|
||||
};
|
||||
return diff;
|
||||
}, {});
|
||||
|
||||
this.setState(stateDiff);
|
||||
};
|
||||
|
||||
window.addEventListener("mouseup", () => {
|
||||
|
|
|
|||
|
|
@ -45,6 +45,31 @@ export const snap = (boxA, boxB) => {
|
|||
return { x, y };
|
||||
};
|
||||
|
||||
export const snapDiff = (a, b) => {
|
||||
const newPos = snap(a, b);
|
||||
return {
|
||||
x: newPos.x === undefined ? 0 : newPos.x - a.x,
|
||||
y: newPos.y === undefined ? 0 : newPos.y - a.y
|
||||
};
|
||||
};
|
||||
|
||||
// TODO: Use the first x and y combo
|
||||
export const snapDiffManyToMany = (as, bs) => {
|
||||
let x = 0;
|
||||
let y = 0;
|
||||
for (const a of as) {
|
||||
for (const b of bs) {
|
||||
const diff = snapDiff(a, b);
|
||||
x = x || diff.x;
|
||||
y = y || diff.y;
|
||||
if (x > 0 && y > 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return { x, y };
|
||||
};
|
||||
|
||||
export const snapToMany = (boxA, otherBoxes) => {
|
||||
let x, y;
|
||||
|
||||
|
|
@ -75,6 +100,14 @@ export const snapWithin = (boxA, boundingBox) => {
|
|||
return { x, y };
|
||||
};
|
||||
|
||||
export const snapWithinDiff = (a, b) => {
|
||||
const newPos = snapWithin(a, b);
|
||||
return {
|
||||
x: newPos.x === undefined ? 0 : newPos.x - a.x,
|
||||
y: newPos.y === undefined ? 0 : newPos.y - a.y
|
||||
};
|
||||
};
|
||||
|
||||
export const applySnap = (original, ...snaps) =>
|
||||
snaps.reduce(
|
||||
(previous, snapped) => ({
|
||||
|
|
@ -84,3 +117,42 @@ export const applySnap = (original, ...snaps) =>
|
|||
}),
|
||||
original
|
||||
);
|
||||
|
||||
export const boundingBox = nodes => {
|
||||
const boxes = nodes.slice();
|
||||
const firstNode = boxes.pop();
|
||||
const bounding = {
|
||||
top: top(firstNode),
|
||||
right: right(firstNode),
|
||||
bottom: bottom(firstNode),
|
||||
left: left(firstNode)
|
||||
};
|
||||
|
||||
boxes.forEach(node => {
|
||||
bounding.top = Math.min(bounding.top, top(node));
|
||||
bounding.right = Math.max(bounding.right, right(node));
|
||||
bounding.bottom = Math.max(bounding.bottom, bottom(node));
|
||||
bounding.left = Math.min(bounding.left, left(node));
|
||||
});
|
||||
|
||||
return {
|
||||
x: bounding.left,
|
||||
y: bounding.top,
|
||||
width: bounding.right - bounding.left,
|
||||
height: bounding.bottom - bounding.top
|
||||
};
|
||||
};
|
||||
|
||||
export const traceConnection = areConnected => (candidates, node) => {
|
||||
const connected = new Set();
|
||||
const checkNode = n => {
|
||||
for (const candidate of candidates) {
|
||||
if (!connected.has(candidate) && areConnected(candidate, n)) {
|
||||
connected.add(candidate);
|
||||
checkNode(candidate);
|
||||
}
|
||||
}
|
||||
};
|
||||
checkNode(node);
|
||||
return connected;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -8,7 +8,10 @@ import {
|
|||
overlapY,
|
||||
overlapX,
|
||||
snapWithin,
|
||||
applySnap
|
||||
applySnap,
|
||||
snapDiff,
|
||||
boundingBox,
|
||||
traceConnection
|
||||
} from "./snapUtils";
|
||||
|
||||
describe("side functions", () => {
|
||||
|
|
@ -248,3 +251,205 @@ describe("applySnap function", () => {
|
|||
expect(actual).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe("snap diff", () => {
|
||||
it("does not snap if A and B are obviously far apart", () => {
|
||||
const a = { x: 10, y: 10, width: 100, height: 100 };
|
||||
const b = { x: 200, y: 200, width: 100, height: 100 };
|
||||
const actual = snapDiff(a, b);
|
||||
const expected = { x: 0, y: 0 };
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
it("snaps the left of A to the right of B", () => {
|
||||
const a = { x: 120, y: 30, width: 100, height: 100 };
|
||||
const b = { x: 10, y: 10, width: 100, height: 100 };
|
||||
const actual = snapDiff(a, b);
|
||||
const expected = { x: -10, y: 0 };
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
it("snaps the right of A to the left of B", () => {
|
||||
const a = { x: 10, y: 30, width: 100, height: 100 };
|
||||
const b = { x: 120, y: 10, width: 100, height: 100 };
|
||||
const actual = snapDiff(a, b);
|
||||
const expected = { x: 10, y: 0 };
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
it("snaps the top of A to the bottom of B", () => {
|
||||
const a = { x: 30, y: 10, width: 100, height: 100 };
|
||||
const b = { x: 10, y: 120, width: 100, height: 100 };
|
||||
const actual = snapDiff(a, b);
|
||||
const expected = { x: 0, y: 10 };
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
it("snaps the bottom of A to the top of B", () => {
|
||||
const a = { x: 30, y: 120, width: 100, height: 100 };
|
||||
const b = { x: 10, y: 10, width: 100, height: 100 };
|
||||
const actual = snapDiff(a, b);
|
||||
const expected = { x: 0, y: -10 };
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
it("does not snap to the X axis if A is below B", () => {
|
||||
const a = { x: 10, y: 10, width: 100, height: 100 };
|
||||
const b = { x: 110, y: 150, width: 100, height: 100 };
|
||||
const actual = snapDiff(a, b);
|
||||
const expected = { x: 0, y: 0 };
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
it("snaps in both axis if the corners are within SNAP_DISTANCE", () => {
|
||||
const a = { x: 10, y: 10, width: 100, height: 100 };
|
||||
const b = { x: 120, y: 120, width: 100, height: 100 };
|
||||
const actual = snapDiff(a, b);
|
||||
const expected = { x: 10, y: 10 };
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
it("snaps the left of A to the left of B", () => {
|
||||
const a = { x: 10, y: 10, width: 100, height: 100 };
|
||||
const b = { x: 15, y: 110, width: 100, height: 100 };
|
||||
const actual = snapDiff(a, b);
|
||||
const expected = { x: 5, y: 0 };
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
it("snaps the top of A to the bottom of B", () => {
|
||||
const a = { x: 10, y: 10, width: 100, height: 100 };
|
||||
const b = { x: 110, y: 15, width: 100, height: 100 };
|
||||
const actual = snapDiff(a, b);
|
||||
const expected = { x: 0, y: 5 };
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
it("snaps the top left corner of A to the bottom right corner of B", () => {
|
||||
const a = { x: 110, y: 110, width: 100, height: 100 };
|
||||
const b = { x: 0, y: 0, width: 100, height: 100 };
|
||||
const actual = snapDiff(a, b);
|
||||
const expected = { x: -10, y: -10 };
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe("snapWithin function", () => {
|
||||
it("snaps the inner box to the top of the outer box", () => {
|
||||
const inner = { x: 120, y: 10, width: 100, height: 100 };
|
||||
const outer = { width: 1000, height: 1000 };
|
||||
const actual = snapWithin(inner, outer);
|
||||
const expected = { y: 0 };
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
it("snaps the inner box to the bottom of the outer box", () => {
|
||||
const inner = { x: 120, y: 910, width: 100, height: 100 };
|
||||
const outer = { width: 1000, height: 1000 };
|
||||
const actual = snapWithin(inner, outer);
|
||||
const expected = { y: 900 };
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
it("snaps the inner box to the right of the outer box", () => {
|
||||
const inner = { x: 910, y: 120, width: 100, height: 100 };
|
||||
const outer = { width: 1000, height: 1000 };
|
||||
const actual = snapWithin(inner, outer);
|
||||
const expected = { x: 900 };
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
it("snaps the inner box to the left of the outer box", () => {
|
||||
const inner = { x: 10, y: 120, width: 100, height: 100 };
|
||||
const outer = { width: 1000, height: 1000 };
|
||||
const actual = snapWithin(inner, outer);
|
||||
const expected = { x: 0 };
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
it("snaps the inner box to the top-left of the outer box", () => {
|
||||
const inner = { x: 10, y: 10, width: 100, height: 100 };
|
||||
const outer = { width: 1000, height: 1000 };
|
||||
const actual = snapWithin(inner, outer);
|
||||
const expected = { x: 0, y: 0 };
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe("boundingBox function", () => {
|
||||
it("does not mutate the given array", () => {
|
||||
const box = { x: 10, y: 10, width: 100, height: 100 };
|
||||
const boxesA = [box];
|
||||
const boxesB = [box];
|
||||
boundingBox(boxesA);
|
||||
expect(boxesA).toEqual(boxesB);
|
||||
});
|
||||
it("returns iteself when called on a single box", () => {
|
||||
const box = { x: 10, y: 10, width: 100, height: 100 };
|
||||
expect(boundingBox([box])).toEqual(box);
|
||||
});
|
||||
it("returns the larger box when one fits entire within another", () => {
|
||||
const boxes = [
|
||||
{ x: 10, y: 10, width: 100, height: 100 },
|
||||
{ x: 20, y: 20, width: 80, height: 80 }
|
||||
];
|
||||
expect(boundingBox(boxes)).toEqual(boxes[0]);
|
||||
});
|
||||
it("returns countaing box of two boxes", () => {
|
||||
const boxes = [
|
||||
{ x: 10, y: 10, width: 100, height: 100 },
|
||||
{ x: 120, y: 220, width: 100, height: 100 }
|
||||
];
|
||||
expect(boundingBox(boxes)).toEqual({
|
||||
x: 10,
|
||||
y: 10,
|
||||
width: 210,
|
||||
height: 310
|
||||
});
|
||||
});
|
||||
it("returns countaing box of three boxes", () => {
|
||||
const boxes = [
|
||||
{ x: 10, y: 10, width: 100, height: 100 },
|
||||
{ x: 120, y: 220, width: 100, height: 100 },
|
||||
{ x: 3, y: 220, width: 100, height: 100 }
|
||||
];
|
||||
expect(boundingBox(boxes)).toEqual({
|
||||
x: 3,
|
||||
y: 10,
|
||||
width: 217,
|
||||
height: 310
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("applySnap function", () => {
|
||||
it("does not apply undefined values", () => {
|
||||
const original = { x: 10, y: 10 };
|
||||
const snapped = { x: undefined, y: undefined };
|
||||
const actual = applySnap(original, snapped);
|
||||
const expected = original;
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
it("preserves other values on original", () => {
|
||||
const original = { x: 10, y: 10, foo: "bar" };
|
||||
const snapped = { x: 50, y: 50 };
|
||||
const actual = applySnap(original, snapped);
|
||||
const expected = { x: 50, y: 50, foo: "bar" };
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
it('will clobber original with falsy "0" values', () => {
|
||||
const original = { x: 10, y: 10 };
|
||||
const snapped = { x: 0, y: 0 };
|
||||
const actual = applySnap(original, snapped);
|
||||
expect(actual).toEqual(snapped);
|
||||
});
|
||||
it('will clobber original with falsy "0" values', () => {
|
||||
const original = { x: 10, y: 10 };
|
||||
const snapped = { x: 0, y: 0 };
|
||||
const actual = applySnap(original, snapped);
|
||||
expect(actual).toEqual(snapped);
|
||||
});
|
||||
it("can apply multiple snaps", () => {
|
||||
const original = { x: 10, y: 10 };
|
||||
const snapped1 = { x: 0 };
|
||||
const snapped2 = { y: 0 };
|
||||
const actual = applySnap(original, snapped1, snapped2);
|
||||
const expected = { x: 0, y: 0 };
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
it("the last snap wins", () => {
|
||||
const original = { x: 10, y: 10 };
|
||||
const snapped1 = { x: 50, y: 20 };
|
||||
const snapped2 = { x: 60, y: 60 };
|
||||
const actual = applySnap(original, snapped1, snapped2);
|
||||
const expected = { x: 60, y: 60 };
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue