clipper/frontend/src/HudCanvasState.ts

267 lines
6.8 KiB
TypeScript

import constrainNumeric from './helpers/constrainNumeric';
export const CanvasLogicalWidth = 2000;
export const CanvasLogicalHeight = 500;
export interface CanvasRange {
x1: number;
x2: number;
}
export enum HoverState {
Normal,
OverSelectionStart,
OverSelectionEnd,
OverSelection,
}
export enum EmptySelectionAction {
SelectNothing,
SelectPrevious,
}
export enum SelectionMode {
Normal,
Selecting,
Dragging,
ResizingStart,
ResizingEnd,
}
export interface State {
width: number;
emptySelectionAction: EmptySelectionAction;
hoverX: number;
selection: CanvasRange;
origSelection: CanvasRange;
mousedownX: number;
mode: SelectionMode;
prevMode: SelectionMode;
cursorClass: string;
hoverState: HoverState;
shouldPublish: boolean;
}
interface SelectionAction {
type: string;
x: number;
// TODO: selection is only used for the setselection SelectionAction. Improve
// the typing here.
selection?: CanvasRange;
}
export const stateReducer = (
{
selection: prevSelection,
origSelection,
mousedownX: prevMousedownX,
mode: prevMode,
width,
emptySelectionAction,
}: State,
{ x, type, selection: selectionToSet }: SelectionAction
): State => {
let mode: SelectionMode;
let newSelection: CanvasRange;
let hoverX: number;
let mousedownX: number;
let cursorClass: string;
let hoverState: HoverState;
let shouldPublish: boolean | null = null;
switch (type) {
case 'setselection':
newSelection = selectionToSet || { x1: 0, x2: 0 };
mousedownX = prevMousedownX;
mode = SelectionMode.Normal;
cursorClass = 'cursor-auto';
hoverX = x;
hoverState = HoverState.Normal;
shouldPublish = false;
break;
case 'mousedown':
mousedownX = x;
cursorClass = 'cursor-auto';
hoverX = x;
hoverState = HoverState.Normal;
if (isHoveringSelectionStart(x, prevSelection)) {
newSelection = prevSelection;
mode = SelectionMode.ResizingStart;
} else if (isHoveringSelectionEnd(x, prevSelection)) {
newSelection = prevSelection;
mode = SelectionMode.ResizingEnd;
} else if (isHoveringSelection(x, prevSelection)) {
newSelection = prevSelection;
mode = SelectionMode.Dragging;
cursorClass = 'cursor-move';
} else {
newSelection = { x1: x, x2: x };
mode = SelectionMode.Selecting;
cursorClass = 'cursor-col-resize';
}
origSelection = newSelection;
break;
case 'mouseup':
newSelection = prevSelection;
mousedownX = prevMousedownX;
mode = SelectionMode.Normal;
cursorClass = 'cursor-auto';
hoverX = x;
hoverState = HoverState.Normal;
if (
newSelection.x1 == newSelection.x2 &&
emptySelectionAction == EmptySelectionAction.SelectNothing
) {
newSelection = { x1: x, x2: x };
}
break;
case 'mouseleave':
newSelection = prevSelection;
mousedownX = prevMousedownX;
mode = prevMode;
cursorClass = 'cursor-auto';
hoverX = 0;
hoverState = HoverState.Normal;
break;
case 'mousemove':
mousedownX = prevMousedownX;
hoverX = x;
hoverState = HoverState.Normal;
switch (prevMode) {
case SelectionMode.Normal: {
newSelection = prevSelection;
mode = SelectionMode.Normal;
if (isHoveringSelectionStart(x, prevSelection)) {
cursorClass = 'cursor-col-resize';
hoverState = HoverState.OverSelectionStart;
} else if (isHoveringSelectionEnd(x, prevSelection)) {
cursorClass = 'cursor-col-resize';
hoverState = HoverState.OverSelectionEnd;
} else if (isHoveringSelection(x, prevSelection)) {
cursorClass = 'cursor-move';
hoverState = HoverState.OverSelection;
} else {
cursorClass = 'cursor-auto';
hoverState = HoverState.Normal;
}
break;
}
case SelectionMode.Selecting: {
cursorClass = 'cursor-col-resize';
mode = SelectionMode.Selecting;
if (x < prevMousedownX) {
newSelection = {
x1: x,
x2: prevMousedownX,
};
} else {
newSelection = {
x1: prevMousedownX,
x2: x,
};
}
break;
}
case SelectionMode.Dragging: {
mode = SelectionMode.Dragging;
cursorClass = 'cursor-move';
const diff = x - prevMousedownX;
const selectionWidth = origSelection.x2 - origSelection.x1;
let start = Math.max(0, origSelection.x1 + diff);
let end = start + selectionWidth;
if (end > width) {
end = width;
start = end - selectionWidth;
}
newSelection = { x1: start, x2: end };
break;
}
case SelectionMode.ResizingStart: {
mode = SelectionMode.ResizingStart;
cursorClass = 'cursor-col-resize';
const start = constrainNumeric(x, width);
if (start > origSelection.x2) {
newSelection = { x1: origSelection.x2, x2: start };
} else {
newSelection = { ...origSelection, x1: start };
}
break;
}
case SelectionMode.ResizingEnd: {
mode = SelectionMode.ResizingEnd;
cursorClass = 'cursor-col-resize';
const end = constrainNumeric(x, width);
if (end < origSelection.x1) {
newSelection = {
x1: end,
x2: origSelection.x1,
};
} else {
newSelection = { ...origSelection, x2: x };
}
break;
}
}
break;
default:
throw new Error();
}
// by default, only trigger the callback if the selection or mode has changed.
if (shouldPublish == null) {
shouldPublish = newSelection != prevSelection || mode != prevMode;
}
return {
width: width,
emptySelectionAction: emptySelectionAction,
hoverX: hoverX,
selection: newSelection,
origSelection: origSelection,
mousedownX: mousedownX,
mode: mode,
prevMode: prevMode,
cursorClass: cursorClass,
hoverState: hoverState,
shouldPublish: shouldPublish,
};
};
// helpers
const hoverOffset = 10;
const isHoveringSelectionStart = (
x: number,
selection: CanvasRange
): boolean => {
return x > selection.x1 - hoverOffset && x < selection.x1 + hoverOffset;
};
const isHoveringSelectionEnd = (x: number, selection: CanvasRange): boolean => {
return x > selection.x2 - hoverOffset && x < selection.x2 + hoverOffset;
};
const isHoveringSelection = (x: number, selection: CanvasRange): boolean => {
return x >= selection.x1 && x <= selection.x2;
};