Skip to content
Snippets Groups Projects
Commit bfa966e7 authored by DD's avatar DD
Browse files

Add monitor layout algorithm

parent 445732b7
No related branches found
No related tags found
No related merge requests found
......@@ -11,10 +11,9 @@ const MonitorList = props => (
<Box
className={styles.monitorList}
>
{props.monitors.valueSeq().map((monitorData, index) => (
{props.monitors.valueSeq().map(monitorData => (
<Monitor
id={monitorData.id}
index={index}
key={monitorData.id}
opcode={monitorData.opcode}
params={monitorData.params}
......
......@@ -16,13 +16,12 @@ const MonitorComponent = props => (
<Draggable
bounds="parent"
defaultClassNameDragging={styles.dragging}
defaultPosition={{
x: props.x,
y: props.y
}}
onStop={props.onDragEnd}
>
<Box className={styles.monitor}>
<Box
className={styles.monitor}
componentRef={props.componentRef}
>
<Box className={styles.label}>
{props.label}
</Box>
......@@ -40,17 +39,14 @@ MonitorComponent.categories = categories;
MonitorComponent.propTypes = {
category: PropTypes.oneOf(Object.keys(categories)),
componentRef: PropTypes.func.isRequired,
label: PropTypes.string.isRequired,
onDragEnd: PropTypes.func.isRequired,
value: PropTypes.string.isRequired,
x: PropTypes.number,
y: PropTypes.number
value: PropTypes.string.isRequired
};
MonitorComponent.defaultProps = {
category: 'data',
x: 0,
y: 0
category: 'data'
};
export default MonitorComponent;
import bindAll from 'lodash.bindall';
import React from 'react';
import PropTypes from 'prop-types';
import {connect} from 'react-redux';
import {moveMonitorRect} from '../reducers/monitor-layout';
import MonitorListComponent from '../components/monitor-list/monitor-list.jsx';
......@@ -13,7 +15,7 @@ class MonitorList extends React.Component {
]);
}
handleMonitorChange (id, x, y) { // eslint-disable-line no-unused-vars
// @todo send this event to the VM
this.props.moveMonitorRect(id, x, y);
}
render () {
return (
......@@ -25,10 +27,15 @@ class MonitorList extends React.Component {
}
}
MonitorList.propTypes = {
moveMonitorRect: PropTypes.func.isRequired
};
const mapStateToProps = state => ({
monitors: state.monitors
});
const mapDispatchToProps = () => ({});
const mapDispatchToProps = dispatch => ({
moveMonitorRect: (id, x, y) => dispatch(moveMonitorRect(id, x, y))
});
export default connect(
mapStateToProps,
......
......@@ -4,25 +4,69 @@ import PropTypes from 'prop-types';
import monitorAdapter from '../lib/monitor-adapter.js';
import MonitorComponent from '../components/monitor/monitor.jsx';
import {addMonitorRect, getInitialPosition, resizeMonitorRect, removeMonitorRect} from '../reducers/monitor-layout';
import {connect} from 'react-redux';
class Monitor extends React.Component {
constructor (props) {
super(props);
bindAll(this, [
'handleDragEnd'
'handleDragEnd',
'setElement'
]);
}
componentDidMount () {
let rect;
// Load the VM provided position if not loaded already
if (this.props.x && this.props.y && !this.props.monitorLayout.savedMonitorPositions[this.props.id]) {
rect = {
upperStart: {x: this.props.x, y: this.props.y},
lowerEnd: {x: this.props.x + this.element.offsetWidth, y: this.props.y + this.element.offsetHeight}
};
this.props.addMonitorRect(this.props.id, rect, true /* savePosition */);
} else { // Newly created user monitor
rect = getInitialPosition(
this.props.monitorLayout, this.props.id, this.element.offsetWidth, this.element.offsetHeight);
this.props.addMonitorRect(this.props.id, rect);
}
this.element.style.top = `${rect.upperStart.y}px`;
this.element.style.left = `${rect.upperStart.x}px`;
}
shouldComponentUpdate (nextProps, nextState) {
if (nextState !== this.state) {
return true;
}
for (const key of Object.getOwnPropertyNames(nextProps)) {
// Don't need to rerender when other monitors are moved.
// monitorLayout is only used during initial layout.
if (key !== 'monitorLayout' && nextProps[key] !== this.props[key]) {
return true;
}
}
return false;
}
componentDidUpdate () {
this.props.resizeMonitorRect(this.props.id, this.element.offsetWidth, this.element.offsetHeight);
}
componentWillUnmount () {
this.props.removeMonitorRect(this.props.id);
}
handleDragEnd (e, {x, y}) {
this.props.onDragEnd(
this.props.id,
x,
y
parseInt(this.element.style.left, 10) + x,
parseInt(this.element.style.top, 10) + y
);
}
setElement (monitorElt) {
this.element = monitorElt;
}
render () {
const monitorProps = monitorAdapter(this.props);
return (
<MonitorComponent
componentRef={this.setElement}
{...monitorProps}
onDragEnd={this.handleDragEnd}
/>
......@@ -31,13 +75,32 @@ class Monitor extends React.Component {
}
Monitor.propTypes = {
addMonitorRect: PropTypes.func.isRequired,
id: PropTypes.string.isRequired,
index: PropTypes.number.isRequired, // eslint-disable-line react/no-unused-prop-types
monitorLayout: PropTypes.shape({
monitors: PropTypes.object,
savedMonitorPositions: PropTypes.object
}).isRequired,
onDragEnd: PropTypes.func.isRequired,
opcode: PropTypes.string.isRequired, // eslint-disable-line react/no-unused-prop-types
params: PropTypes.object, // eslint-disable-line react/no-unused-prop-types, react/forbid-prop-types
removeMonitorRect: PropTypes.func.isRequired,
resizeMonitorRect: PropTypes.func.isRequired,
spriteName: PropTypes.string, // eslint-disable-line react/no-unused-prop-types
value: PropTypes.string.isRequired // eslint-disable-line react/no-unused-prop-types
value: PropTypes.string.isRequired, // eslint-disable-line react/no-unused-prop-types
x: PropTypes.number,
y: PropTypes.number
};
export default Monitor;
const mapStateToProps = state => ({
monitorLayout: state.monitorLayout
});
const mapDispatchToProps = dispatch => ({
addMonitorRect: (id, rect, savePosition) =>
dispatch(addMonitorRect(id, rect.upperStart, rect.lowerEnd, savePosition)),
resizeMonitorRect: (id, newWidth, newHeight) => dispatch(resizeMonitorRect(id, newWidth, newHeight)),
removeMonitorRect: id => dispatch(removeMonitorRect(id))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(Monitor);
import OpcodeLabels from './opcode-labels.js';
const PADDING = 5;
const MONITOR_HEIGHT = 23;
const isUndefined = a => typeof a === 'undefined';
/**
* Convert monitors from VM format to what the GUI needs to render.
* - Convert opcode to a label and a category
* - Add missing XY position data if needed
* @param {object} block - The monitor block
* @param {string} block.id - The id of the monitor block
* @param {string} block.spriteName - Present only if the monitor applies only to the sprite
* with given target ID. The name of the target sprite when the monitor was created
* @param {number} block.index - The index of the monitor
* @param {string} block.opcode - The opcode of the monitor
* @param {object} block.params - Extra params to the monitor block
* @param {string} block.value - The monitor value
* @param {number} x - The monitor x position
* @param {number} y - The monitor y position
* @return {object} The adapted monitor with label and category
*/
export default function ({id, spriteName, index, opcode, params, value, x, y}) {
export default function ({id, spriteName, opcode, params, value}) {
let {label, category, labelFn} = OpcodeLabels(opcode);
// Use labelFn if provided for dynamic labelling (e.g. variables)
if (!isUndefined(labelFn)) label = labelFn(params);
// Append sprite name for sprite-specific monitors
if (spriteName) {
label = `${spriteName}: ${label}`;
}
// Simple layout if x or y are undefined
// @todo scratch2 has a more complex layout behavior we may want to adopt
// @todo e.g. this does not work well when monitors have already been moved
if (isUndefined(x)) x = PADDING;
if (isUndefined(y)) y = PADDING + (index * (PADDING + MONITOR_HEIGHT));
// If value is a number, round it to six decimal places
if (typeof value === 'number' || (typeof value === 'string' && String(parseFloat(value)) === value)) {
value = Number(Number(value).toFixed(6));
}
return {id, label, category, value, x, y};
return {id, label, category, value};
}
......@@ -4,6 +4,7 @@ import customProceduresReducer from './custom-procedures';
import intlReducer from './intl';
import modalReducer from './modals';
import monitorReducer from './monitors';
import monitorLayoutReducer from './monitor-layout';
import targetReducer from './targets';
import toolboxReducer from './toolbox';
import vmReducer from './vm';
......@@ -15,6 +16,7 @@ export default combineReducers({
intl: intlReducer,
modals: modalReducer,
monitors: monitorReducer,
monitorLayout: monitorLayoutReducer,
targets: targetReducer,
toolbox: toolboxReducer,
vm: vmReducer,
......
import log from '../lib/log';
const ADD_MONITOR_RECT = 'scratch-gui/monitors/ADD_MONITOR_RECT';
const MOVE_MONITOR_RECT = 'scratch-gui/monitors/MOVE_MONITOR_RECT';
const RESIZE_MONITOR_RECT = 'scratch-gui/monitors/RESIZE_MONITOR_RECT';
const REMOVE_MONITOR_RECT = 'scratch-gui/monitors/REMOVE_MONITOR_RECT';
const initialState = {
monitors: {},
savedMonitorPositions: {}
};
// Verify that the rectangle formed by the 2 points is well-formed
const _verifyRect = function (upperStart, lowerEnd) {
if (isNaN(upperStart.x) || isNaN(upperStart.y) || isNaN(lowerEnd.x) || isNaN(lowerEnd.y)) {
return false;
}
if (!(upperStart.x < lowerEnd.x)) {
return false;
}
if (!(upperStart.y < lowerEnd.y)) {
return false;
}
return true;
};
const _addMonitorRect = function (state, action) {
if (state.monitors.hasOwnProperty(action.monitorId)) {
log.error(`Can't add monitor, monitor with id ${action.monitorId} already exists.`);
return state;
}
if (!_verifyRect(action.upperStart, action.lowerEnd)) {
log.error(`Monitor rectangle not formatted correctly`);
return state;
}
return {
monitors: Object.assign({}, state.monitors, {
[action.monitorId]: {
upperStart: action.upperStart,
lowerEnd: action.lowerEnd
}
}),
savedMonitorPositions: action.savePosition ?
Object.assign({}, state.savedMonitorPositions, {
[action.monitorId]: {x: action.upperStart.x, y: action.upperStart.y}
}) :
state.savedMonitorPositions
};
};
const _moveMonitorRect = function (state, action) {
if (!state.monitors.hasOwnProperty(action.monitorId)) {
log.error(`Can't move monitor, monitor with id ${action.monitorId} does not exist.`);
return state;
}
if (isNaN(action.newX) || isNaN(action.newY)) {
log.error(`Monitor rectangle not formatted correctly`);
return state;
}
const oldMonitor = state.monitors[action.monitorId];
if (oldMonitor.upperStart.x === action.newX &&
oldMonitor.upperStart.y === action.newY) {
// Hasn't moved
return state;
}
const monitorWidth = oldMonitor.lowerEnd.x - oldMonitor.upperStart.x;
const monitorHeight = oldMonitor.lowerEnd.y - oldMonitor.upperStart.y;
return {
monitors: Object.assign({}, state.monitors, {
[action.monitorId]: {
upperStart: {x: action.newX, y: action.newY},
lowerEnd: {x: action.newX + monitorWidth, y: action.newY + monitorHeight}
}
}),
// User generated position is saved
savedMonitorPositions: Object.assign({}, state.savedMonitorPositions, {
[action.monitorId]: {x: action.newX, y: action.newY}
})
};
};
const _resizeMonitorRect = function (state, action) {
if (!state.monitors.hasOwnProperty(action.monitorId)) {
log.error(`Can't resize monitor, monitor with id ${action.monitorId} does not exist.`);
return state;
}
if (isNaN(action.newWidth) || isNaN(action.newHeight) ||
action.newWidth <= 0 || action.newHeight <= 0) {
log.error(`Monitor rectangle not formatted correctly`);
return state;
}
const oldMonitor = state.monitors[action.monitorId];
const newMonitor = {
upperStart: oldMonitor.upperStart,
lowerEnd: {
x: oldMonitor.upperStart.x + action.newWidth,
y: oldMonitor.upperStart.y + action.newHeight
}
};
if (newMonitor.lowerEnd.x === oldMonitor.lowerEnd.x &&
newMonitor.lowerEnd.y === oldMonitor.lowerEnd.y) {
// no change
return state;
}
return {
monitors: Object.assign({}, state.monitors, {[action.monitorId]: newMonitor}),
savedMonitorPositions: state.savedMonitorPositions
};
};
const _removeMonitorRect = function (state, action) {
if (!state.monitors.hasOwnProperty(action.monitorId)) {
log.error(`Can't remove monitor, monitor with id ${action.monitorId} does not exist.`);
return state;
}
const newMonitors = Object.assign({}, state.monitors);
delete newMonitors[action.monitorId];
return {
monitors: newMonitors,
savedMonitorPositions: state.savedMonitorPositions
};
};
const reducer = function (state, action) {
if (typeof state === 'undefined') state = initialState;
switch (action.type) {
case ADD_MONITOR_RECT:
return _addMonitorRect(state, action);
case MOVE_MONITOR_RECT:
return _moveMonitorRect(state, action);
case RESIZE_MONITOR_RECT:
return _resizeMonitorRect(state, action);
case REMOVE_MONITOR_RECT:
return _removeMonitorRect(state, action);
default:
return state;
}
};
// Init position --------------------------
const PADDING = 5;
// @todo fix these numbers when we fix https://github.com/LLK/scratch-gui/issues/980
const SCREEN_WIDTH = 400;
const SCREEN_HEIGHT = 300;
const SCREEN_EDGE_BUFFER = 40;
const _rectsIntersect = function (rect1, rect2) {
// If one rectangle is on left side of other
if (rect1.upperStart.x >= rect2.lowerEnd.x || rect2.upperStart.x >= rect1.lowerEnd.x) return false;
// If one rectangle is above other
if (rect1.upperStart.y >= rect2.lowerEnd.y || rect2.upperStart.y >= rect1.lowerEnd.y) return false;
return true;
};
// We need to place a monitor with the given width and height. Return a rect defining where it should be placed.
const getInitialPosition = function (state, monitorId, eltWidth, eltHeight) {
// If this monitor was purposefully moved to a certain position before, put it back in that position
if (state.savedMonitorPositions.hasOwnProperty(monitorId)) {
const saved = state.savedMonitorPositions[monitorId];
return {
upperStart: saved,
lowerEnd: {x: saved.x + eltWidth, y: saved.y + eltHeight}
};
}
// Try all starting positions for the new monitor to find one that doesn't intersect others
const endXs = [0];
const endYs = [0];
let lastX = null;
let lastY = null;
for (const monitor in state.monitors) {
let x = state.monitors[monitor].lowerEnd.x;
x = Math.ceil(x / 50) * 50; // Try to choose a sensible "tab width" so more monitors line up
endXs.push(x);
endYs.push(Math.ceil(state.monitors[monitor].lowerEnd.y));
}
endXs.sort((a, b) => a - b);
endYs.sort((a, b) => a - b);
// We'll use plan B if the monitor doesn't fit anywhere (too long or tall)
let planB = null;
for (const x of endXs) {
if (x === lastX) {
continue;
}
lastX = x;
outer:
for (const y of endYs) {
if (y === lastY) {
continue;
}
lastY = y;
const monitorRect = {
upperStart: {x: x + PADDING, y: y + PADDING},
lowerEnd: {x: x + PADDING + eltWidth, y: y + PADDING + eltHeight}
};
// Intersection testing rect that includes padding
const rect = {
upperStart: {x, y},
lowerEnd: {x: x + eltWidth + (2 * PADDING), y: y + eltHeight + (2 * PADDING)}
};
for (const monitor in state.monitors) {
if (_rectsIntersect(state.monitors[monitor], rect)) {
continue outer;
}
}
// If the rect overlaps the ends of the screen
if (rect.lowerEnd.x > SCREEN_WIDTH || rect.lowerEnd.y > SCREEN_HEIGHT) {
// If rect is not too close to completely off screen, set it as plan B
if (!planB &&
!(rect.upperStart.x + SCREEN_EDGE_BUFFER > SCREEN_WIDTH ||
rect.upperStart.y + SCREEN_EDGE_BUFFER > SCREEN_HEIGHT)) {
planB = monitorRect;
}
continue;
}
return monitorRect;
}
}
// If the monitor is too long to fit anywhere, put it in the leftmost spot available
// that intersects the right or bottom edge and isn't too close to the edge.
if (planB) {
return planB;
}
// If plan B fails and there's nowhere reasonable to put it, plan C is to place the monitor randomly
const randX = Math.ceil(Math.random() * (SCREEN_WIDTH / 2));
const randY = Math.ceil(Math.random() * (SCREEN_HEIGHT - SCREEN_EDGE_BUFFER));
return {
upperStart: {
x: randX,
y: randY
},
lowerEnd: {
x: randX + eltWidth,
y: randY + eltHeight
}
};
};
// Action creators ------------------------
/**
* @param {!string} monitorId Id to add
* @param {!object} upperStart upper point defining the rectangle
* @param {!number} upperStart.x X of top point that defines the monitor location
* @param {!number} upperStart.y Y of top point that defines the monitor location
* @param {!object} lowerEnd lower point defining the rectangle
* @param {!number} lowerEnd.x X of bottom point that defines the monitor location
* @param {!number} lowerEnd.y Y of bottom point that defines the monitor location
* @param {?boolean} savePosition True if the placement should be saved when adding the monitor
* @returns {object} action to add a new monitor at the location
*/
const addMonitorRect = function (monitorId, upperStart, lowerEnd, savePosition) {
return {
type: ADD_MONITOR_RECT,
monitorId: monitorId,
upperStart: upperStart,
lowerEnd: lowerEnd,
savePosition: savePosition
};
};
/**
* @param {!string} monitorId Id for monitor to move
* @param {!number} newX X of top point that defines the monitor location
* @param {!number} newY Y of top point that defines the monitor location
* @returns {object} action to move an existing monitor to the location
*/
const moveMonitorRect = function (monitorId, newX, newY) {
return {
type: MOVE_MONITOR_RECT,
monitorId: monitorId,
newX: newX,
newY: newY
};
};
/**
* @param {!string} monitorId Id for monitor to resize
* @param {!number} newWidth Width to set monitor to
* @param {!number} newHeight Height to set monitor to
* @returns {object} action to resize an existing monitor to the given dimensions
*/
const resizeMonitorRect = function (monitorId, newWidth, newHeight) {
return {
type: RESIZE_MONITOR_RECT,
monitorId: monitorId,
newWidth: newWidth,
newHeight: newHeight
};
};
/**
* @param {!string} monitorId Id for monitor to remove
* @returns {object} action to remove an existing monitor
*/
const removeMonitorRect = function (monitorId) {
return {
type: REMOVE_MONITOR_RECT,
monitorId: monitorId
};
};
export {
reducer as default,
addMonitorRect,
getInitialPosition,
moveMonitorRect,
resizeMonitorRect,
removeMonitorRect,
PADDING,
SCREEN_HEIGHT,
SCREEN_WIDTH
};
/* eslint-env jest */
import monitorLayoutReducer from '../../../src/reducers/monitor-layout';
import {addMonitorRect, moveMonitorRect} from '../../../src/reducers/monitor-layout';
import {resizeMonitorRect, removeMonitorRect} from '../../../src/reducers/monitor-layout';
import {getInitialPosition, PADDING, SCREEN_WIDTH, SCREEN_HEIGHT} from '../../../src/reducers/monitor-layout';
test('initialState', () => {
let defaultState;
expect(monitorLayoutReducer(defaultState /* state */, {type: 'anything'} /* action */)).toBeDefined();
expect(monitorLayoutReducer(defaultState /* state */, {type: 'anything'} /* action */).monitors).toBeDefined();
expect(monitorLayoutReducer(defaultState /* state */, {type: 'anything'} /* action */).savedMonitorPositions)
.toBeDefined();
});
test('addMonitorRect', () => {
let defaultState;
const monitorId = 1;
const monitorId2 = 2;
const upperStart = {x: 100, y: 100};
const lowerEnd = {x: 200, y: 200};
// Add a monitor rect
const reduxState = monitorLayoutReducer(defaultState, addMonitorRect(monitorId, upperStart, lowerEnd));
expect(reduxState.monitors[monitorId]).toBeDefined();
expect(reduxState.monitors[monitorId].upperStart).toEqual(upperStart);
expect(reduxState.monitors[monitorId].lowerEnd).toEqual(lowerEnd);
// Add monitor rect doesn't save position
expect(reduxState.savedMonitorPositions[monitorId]).toBeUndefined();
const reduxState2 = monitorLayoutReducer(reduxState, moveMonitorRect(monitorId, 0, 0));
// Add a second monitor rect
const reduxState3 = monitorLayoutReducer(reduxState2, addMonitorRect(monitorId2, upperStart, lowerEnd));
expect(reduxState3.monitors[monitorId]).toBeDefined();
expect(reduxState3.monitors[monitorId2]).toBeDefined();
expect(reduxState3.monitors[monitorId2].upperStart).toEqual(upperStart);
expect(reduxState3.monitors[monitorId2].lowerEnd).toEqual(lowerEnd);
// Saved positions aren't changed by adding monitor
expect(reduxState3.savedMonitorPositions).toEqual(reduxState2.savedMonitorPositions);
});
test('addMonitorRectWithSavedPosition', () => {
let defaultState;
const monitorId = 1;
const upperStart = {x: 100, y: 100};
const lowerEnd = {x: 200, y: 200};
// Add a monitor rect
const reduxState = monitorLayoutReducer(defaultState,
addMonitorRect(monitorId, upperStart, lowerEnd, true /* savePosition */));
expect(reduxState.monitors[monitorId]).toBeDefined();
expect(reduxState.monitors[monitorId].upperStart).toEqual(upperStart);
expect(reduxState.monitors[monitorId].lowerEnd).toEqual(lowerEnd);
// Save position
expect(reduxState.savedMonitorPositions[monitorId].x).toEqual(upperStart.x);
expect(reduxState.savedMonitorPositions[monitorId].y).toEqual(upperStart.y);
});
test('invalidRect', () => {
let defaultState;
const reduxState = monitorLayoutReducer(defaultState /* state */, {type: 'initialize'} /* action */);
// Problem: x end is before x start
expect(
monitorLayoutReducer(reduxState,
addMonitorRect(1, {x: 100, y: 100}, {x: 10, y: 200})))
.toEqual(reduxState);
// Problem: y end is before y start
expect(
monitorLayoutReducer(reduxState,
addMonitorRect(1, {x: 100, y: 100}, {x: 200, y: 10})))
.toEqual(reduxState);
});
test('invalidAddMonitorRect', () => {
let defaultState;
const monitorId = 1;
const upperStart = {x: 100, y: 100};
const lowerEnd = {x: 200, y: 200};
// Add a monitor rect
const reduxState = monitorLayoutReducer(defaultState, addMonitorRect(monitorId, upperStart, lowerEnd));
// Try to add the same one
expect(monitorLayoutReducer(reduxState, addMonitorRect(monitorId, upperStart, lowerEnd)))
.toEqual(reduxState);
});
test('moveMonitorRect', () => {
let defaultState;
const monitorId = 1;
const monitorId2 = 2;
const width = 102;
const height = 101;
const upperStart = {x: 100, y: 100};
const lowerEnd = {x: upperStart.x + width, y: upperStart.y + height};
const movedToPosition = {x: 0, y: 0};
const movedToPosition2 = {x: 543, y: 2};
// Add a monitor rect and move it. Expect it to be in monitors state and saved positions.
const reduxState = monitorLayoutReducer(defaultState, addMonitorRect(monitorId, upperStart, lowerEnd));
const reduxState2 = monitorLayoutReducer(reduxState,
moveMonitorRect(monitorId, movedToPosition.x, movedToPosition.y));
expect(reduxState2.monitors[monitorId]).toBeDefined();
expect(reduxState2.monitors[monitorId].upperStart).toEqual(movedToPosition);
expect(reduxState2.monitors[monitorId].lowerEnd.x).toEqual(movedToPosition.x + width);
expect(reduxState2.monitors[monitorId].lowerEnd.y).toEqual(movedToPosition.y + height);
expect(reduxState2.savedMonitorPositions[monitorId]).toBeDefined();
expect(reduxState2.savedMonitorPositions[monitorId].x).toEqual(movedToPosition.x);
expect(reduxState2.savedMonitorPositions[monitorId].y).toEqual(movedToPosition.y);
// Add a second monitor rect and move it. Expect there to now be 2 saved positions.
const reduxState3 = monitorLayoutReducer(reduxState2, addMonitorRect(monitorId2, upperStart, lowerEnd));
const reduxState4 = monitorLayoutReducer(reduxState3,
moveMonitorRect(monitorId2, movedToPosition2.x, movedToPosition2.y));
expect(reduxState4.savedMonitorPositions[monitorId]).toEqual(reduxState2.savedMonitorPositions[monitorId]);
expect(reduxState4.savedMonitorPositions[monitorId2].x).toEqual(movedToPosition2.x);
expect(reduxState4.savedMonitorPositions[monitorId2].y).toEqual(movedToPosition2.y);
});
test('invalidMoveMonitorRect', () => {
let defaultState;
let reduxState = monitorLayoutReducer(defaultState, {type: 'initialize'} /* action */);
const monitorId = 1;
// Try to move a monitor rect that doesn't exist
expect(monitorLayoutReducer(reduxState, moveMonitorRect(monitorId, 1 /* newX */, 1 /* newY */)))
.toEqual(reduxState);
// Add the monitor to move
reduxState = monitorLayoutReducer(reduxState, addMonitorRect(monitorId, {x: 100, y: 100}, {x: 200, y: 200}));
// Invalid newX
expect(monitorLayoutReducer(reduxState, moveMonitorRect(monitorId, 'Oregon' /* newX */, 1 /* newY */)))
.toEqual(reduxState);
// Invalid newY
expect(monitorLayoutReducer(reduxState, moveMonitorRect(monitorId, 1 /* newX */)))
.toEqual(reduxState);
});
test('resizeMonitorRect', () => {
let defaultState;
const monitorId = 1;
const upperStart = {x: 100, y: 100};
const newWidth = 10;
const newHeight = 20;
// Add a monitor rect and resize it
const reduxState = monitorLayoutReducer(defaultState, addMonitorRect(monitorId, upperStart, {x: 200, y: 200}));
const reduxState2 = monitorLayoutReducer(reduxState,
resizeMonitorRect(monitorId, newWidth, newHeight));
expect(reduxState2.monitors[monitorId]).toBeDefined();
expect(reduxState2.monitors[monitorId].upperStart).toEqual(upperStart);
expect(reduxState2.monitors[monitorId].lowerEnd.x).toEqual(upperStart.x + newWidth);
expect(reduxState2.monitors[monitorId].lowerEnd.y).toEqual(upperStart.y + newHeight);
// Saved positions aren't changed by resizing monitor
expect(reduxState2.savedMonitorPositions).toEqual(reduxState.savedMonitorPositions);
});
test('invalidResizeMonitorRect', () => {
let defaultState;
let reduxState = monitorLayoutReducer(defaultState, {type: 'initialize'} /* action */);
const monitorId = 1;
// Try to resize a monitor rect that doesn't exist
expect(monitorLayoutReducer(reduxState, resizeMonitorRect(monitorId, 1 /* newWidth */, 1 /* newHeight */)))
.toEqual(reduxState);
// Add the monitor to resize
reduxState = monitorLayoutReducer(reduxState, addMonitorRect(monitorId, {x: 100, y: 100}, {x: 200, y: 200}));
// Invalid newWidth
expect(monitorLayoutReducer(reduxState, resizeMonitorRect(monitorId, 'Oregon' /* newWidth */, 1 /* newHeight */)))
.toEqual(reduxState);
// Invalid newHeight
expect(monitorLayoutReducer(reduxState, moveMonitorRect(monitorId, 1 /* newWidth */)))
.toEqual(reduxState);
// newWidth < 0
expect(monitorLayoutReducer(reduxState, resizeMonitorRect(monitorId, -1 /* newWidth */, 1 /* newHeight */)))
.toEqual(reduxState);
// newHeight < 0
expect(monitorLayoutReducer(reduxState, resizeMonitorRect(monitorId, 1 /* newWidth */, -1 /* newHeight */)))
.toEqual(reduxState);
});
test('removeMonitorRect', () => {
let defaultState;
const monitorId = 1;
// Add a monitor rect, move it, and remove it
const reduxState = monitorLayoutReducer(defaultState, addMonitorRect(monitorId,
{x: 100, y: 100},
{x: 200, y: 200}
));
const reduxState2 = monitorLayoutReducer(reduxState, moveMonitorRect(monitorId, 0, 0));
const reduxState3 = monitorLayoutReducer(reduxState2, removeMonitorRect(monitorId));
expect(reduxState3.monitors[monitorId]).toBeUndefined();
// Check that saved positions aren't changed by removing monitor
expect(reduxState3.savedMonitorPositions).toEqual(reduxState2.savedMonitorPositions);
});
test('invalidRemoveMonitorRect', () => {
let defaultState;
const reduxState = monitorLayoutReducer(defaultState, {type: 'initialize'} /* action */);
// Try to remove a monitor rect that doesn't exist
expect(monitorLayoutReducer(reduxState, resizeMonitorRect(1)))
.toEqual(reduxState);
});
test('getInitialPosition_lineUpTopLeft', () => {
let defaultState;
const width = 100;
const height = 200;
// Add monitors to right and bottom, but there is a space in the top left
let reduxState = monitorLayoutReducer(defaultState, addMonitorRect(1,
{x: width + PADDING, y: 0},
{x: 100, y: height}
));
reduxState = monitorLayoutReducer(defaultState, addMonitorRect(2,
{x: 0, y: height + PADDING},
{x: width, y: 100}
));
// Check that the added monitor appears in the space
const rect = getInitialPosition(reduxState, 3, width, height);
expect(rect.upperStart).toBeDefined();
expect(rect.lowerEnd).toBeDefined();
expect(rect.lowerEnd.x - rect.upperStart.x).toEqual(width);
expect(rect.lowerEnd.y - rect.upperStart.y).toEqual(height);
expect(rect.upperStart.x).toEqual(PADDING);
expect(rect.upperStart.y).toEqual(PADDING);
});
test('getInitialPosition_savedPosition', () => {
const monitorId = 1;
const savedX = 100;
const savedY = 200;
const width = 7;
const height = 8;
const reduxState = {
monitors: {},
savedMonitorPositions: {[monitorId]: {x: savedX, y: savedY}}
};
// Check that initial position uses saved state
const rect = getInitialPosition(reduxState, monitorId, width, height);
expect(rect.upperStart).toBeDefined();
expect(rect.lowerEnd).toBeDefined();
expect(rect.lowerEnd.x - rect.upperStart.x).toEqual(width);
expect(rect.lowerEnd.y - rect.upperStart.y).toEqual(height);
expect(rect.upperStart.x).toEqual(savedX);
expect(rect.upperStart.y).toEqual(savedY);
});
test('getInitialPosition_lineUpLeft', () => {
let defaultState;
const monitor1EndY = 60;
// Add a monitor that takes up the upper left corner
const reduxState = monitorLayoutReducer(defaultState, addMonitorRect(1, {x: 0, y: 0}, {x: 100, y: monitor1EndY}));
// Check that added monitor is under it and lines up left
const rect = getInitialPosition(reduxState, 2, 20 /* width */, 20 /* height */);
expect(rect.upperStart.y >= monitor1EndY + PADDING).toBeTruthy();
});
test('getInitialPosition_lineUpTop', () => {
let defaultState;
const monitor1EndX = 100;
// Add a monitor that takes up the whole left side
const reduxState = monitorLayoutReducer(defaultState, addMonitorRect(1,
{x: 0, y: 0},
{x: monitor1EndX, y: SCREEN_HEIGHT}
));
// Check that added monitor is to the right of it and lines up top
const rect = getInitialPosition(reduxState, 2, 20 /* width */, 20 /* height */);
expect(rect.upperStart.y).toEqual(PADDING);
expect(rect.upperStart.x >= monitor1EndX + PADDING).toBeTruthy();
});
test('getInitialPosition_noRoom', () => {
let defaultState;
const width = 7;
const height = 8;
// Add a monitor that takes up the whole screen
const reduxState = monitorLayoutReducer(defaultState, addMonitorRect(1,
{x: 0, y: 0},
{x: SCREEN_WIDTH, y: SCREEN_HEIGHT}
));
// Check that added monitor exists somewhere (we don't care where)
const rect = getInitialPosition(reduxState, 2, width, height);
expect(rect.upperStart).toBeDefined();
expect(rect.lowerEnd.x - rect.upperStart.x).toEqual(width);
expect(rect.lowerEnd.y - rect.upperStart.y).toEqual(height);
});
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment