clipper/frontend/src/HudCanvasState.ts

264 lines
6.8 KiB
TypeScript

import constrainNumeric from './helpers/constrainNumeric';
export const CanvasLogicalWidth = 2000;
export const CanvasLogicalHeight = 500;
export enum HoverState {
Normal,
OverSelectionStart,
OverSelectionEnd,
OverSelection,
}
export enum EmptySelectionAction {
SelectNothing,
SelectPrevious,
}
export interface Selection {
start: number;
end: number;
}
export enum SelectionMode {
Normal,
Selecting,
Dragging,
ResizingStart,
ResizingEnd,
}
export interface State {
width: number;
emptySelectionAction: EmptySelectionAction;
hoverX: number;
selection: Selection;
origSelection: Selection;
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?: Selection;
}
export const stateReducer = (
{
selection: prevSelection,
origSelection,
mousedownX: prevMousedownX,
mode: prevMode,
width,
emptySelectionAction,
}: State,
{ x, type, selection: selectionToSet }: SelectionAction
): State => {
let mode: SelectionMode;
let newSelection: Selection;
let hoverX: number;
let mousedownX: number;
let cursorClass: string;
let hoverState: HoverState;
let shouldPublish: boolean | null = null;
switch (type) {
case 'setselection':
newSelection = selectionToSet || { start: 0, end: 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 = { start: x, end: 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.start == newSelection.end &&
emptySelectionAction == EmptySelectionAction.SelectNothing
) {
newSelection = { start: x, end: 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 = {
start: x,
end: prevMousedownX,
};
} else {
newSelection = {
start: prevMousedownX,
end: x,
};
}
break;
}
case SelectionMode.Dragging: {
mode = SelectionMode.Dragging;
cursorClass = 'cursor-move';
const diff = x - prevMousedownX;
const selectionWidth = origSelection.end - origSelection.start;
let start = Math.max(0, origSelection.start + diff);
let end = start + selectionWidth;
if (end > width) {
end = width;
start = end - selectionWidth;
}
newSelection = { start: start, end: end };
break;
}
case SelectionMode.ResizingStart: {
mode = SelectionMode.ResizingStart;
cursorClass = 'cursor-col-resize';
const start = constrainNumeric(x, width);
if (start > origSelection.end) {
newSelection = { start: origSelection.end, end: start };
} else {
newSelection = { ...origSelection, start: start };
}
break;
}
case SelectionMode.ResizingEnd: {
mode = SelectionMode.ResizingEnd;
cursorClass = 'cursor-col-resize';
const end = constrainNumeric(x, width);
if (end < origSelection.start) {
newSelection = {
start: end,
end: origSelection.start,
};
} else {
newSelection = { ...origSelection, end: 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: Selection): boolean => {
return x > selection.start - hoverOffset && x < selection.start + hoverOffset;
};
const isHoveringSelectionEnd = (x: number, selection: Selection): boolean => {
return x > selection.end - hoverOffset && x < selection.end + hoverOffset;
};
const isHoveringSelection = (x: number, selection: Selection): boolean => {
return x >= selection.start && x <= selection.end;
};