Files
CHOMPStation2/tgui/packages/tgui_ch/drag.ts
2023-06-19 19:41:48 +02:00

287 lines
7.8 KiB
TypeScript

/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
import { vecAdd, vecMultiply, vecScale, vecSubtract } from 'common/vector';
import { createLogger } from './logging';
import { storage } from 'common/storage';
const logger = createLogger('drag');
const pixelRatio = window.devicePixelRatio ?? 1;
let windowKey = Byond.windowId;
let dragging = false;
let resizing = false;
let screenOffset: [number, number] = [0, 0];
let screenOffsetPromise: Promise<[number, number]>;
let dragPointOffset: [number, number];
let resizeMatrix: [number, number];
let initialSize: [number, number];
let size: [number, number];
// Set the window key
export const setWindowKey = (key: string): void => {
windowKey = key;
};
// Get window position
export const getWindowPosition = (): [number, number] => [
window.screenLeft * pixelRatio,
window.screenTop * pixelRatio,
];
// Get window size
export const getWindowSize = (): [number, number] => [
window.innerWidth * pixelRatio,
window.innerHeight * pixelRatio,
];
// Set window position
const setWindowPosition = (vec: [number, number]) => {
const byondPos = vecAdd(vec, screenOffset);
return Byond.winset(Byond.windowId, {
pos: byondPos[0] + ',' + byondPos[1],
});
};
// Set window size
const setWindowSize = (vec: [number, number]) => {
return Byond.winset(Byond.windowId, {
size: vec[0] + 'x' + vec[1],
});
};
// Get screen position
const getScreenPosition = (): [number, number] => [
0 - screenOffset[0],
0 - screenOffset[1],
];
// Get screen size
const getScreenSize = (): [number, number] => [
window.screen.availWidth * pixelRatio,
window.screen.availHeight * pixelRatio,
];
/**
* Moves an item to the top of the recents array, and keeps its length
* limited to the number in `limit` argument.
*
* Uses a strict equality check for comparisons.
*
* Returns new recents and an item which was trimmed.
*/
export const touchRecents = (
recents: string[],
touchedItem: string,
limit = 50
): [string[], string | undefined] => {
const nextRecents: string[] = [touchedItem];
let trimmedItem: string | undefined;
for (let i = 0; i < recents.length; i++) {
const item = recents[i];
if (item === touchedItem) {
continue;
}
if (nextRecents.length < limit) {
nextRecents.push(item);
} else {
trimmedItem = item;
}
}
return [nextRecents, trimmedItem];
};
// Store window geometry in local storage
const storeWindowGeometry = async () => {
logger.log('storing geometry');
const geometry = {
pos: getWindowPosition(),
size: getWindowSize(),
};
storage.set(windowKey, geometry);
// Update the list of stored geometries
const [geometries, trimmedKey] = touchRecents(
(await storage.get('geometries')) || [],
windowKey
);
if (trimmedKey) {
storage.remove(trimmedKey);
}
storage.set('geometries', geometries);
};
// Recall window geometry from local storage and apply it
export const recallWindowGeometry = async (
options: {
fancy?: boolean;
pos?: [number, number];
size?: [number, number];
locked?: boolean;
} = {}
) => {
const geometry = options.fancy && (await storage.get(windowKey));
if (geometry) {
logger.log('recalled geometry:', geometry);
}
// options.pos is assumed to already be in display-pixels
let pos = geometry?.pos || options.pos;
let size = options.size;
// Convert size from css-pixels to display-pixels
if (size) {
size = [size[0] * pixelRatio, size[1] * pixelRatio];
}
// Wait until screen offset gets resolved
await screenOffsetPromise;
const areaAvailable = getScreenSize();
// Set window size
if (size) {
// Constraint size to not exceed available screen area
size = [
Math.min(areaAvailable[0], size[0]),
Math.min(areaAvailable[1], size[1]),
];
setWindowSize(size);
}
// Set window position
if (pos) {
// Constraint window position if monitor lock was set in preferences.
if (size && options.locked) {
pos = constraintPosition(pos, size)[1];
}
setWindowPosition(pos);
// Set window position at the center of the screen.
} else if (size) {
pos = vecAdd(
vecScale(areaAvailable, 0.5),
vecScale(size, -0.5),
vecScale(screenOffset, -1.0)
);
setWindowPosition(pos);
}
};
// Setup draggable window
export const setupDrag = async () => {
// Calculate screen offset caused by the windows taskbar
let windowPosition = getWindowPosition();
screenOffsetPromise = Byond.winget(Byond.windowId, 'pos').then((pos) => [
pos.x - windowPosition[0],
pos.y - windowPosition[1],
]);
screenOffset = await screenOffsetPromise;
logger.debug('screen offset', screenOffset);
};
/**
* Constraints window position to safe screen area, accounting for safe
* margins which could be a system taskbar.
*/
const constraintPosition = (
pos: [number, number],
size: [number, number]
): [boolean, [number, number]] => {
const screenPos = getScreenPosition();
const screenSize = getScreenSize();
const nextPos: [number, number] = [pos[0], pos[1]];
let relocated = false;
for (let i = 0; i < 2; i++) {
const leftBoundary = screenPos[i];
const rightBoundary = screenPos[i] + screenSize[i];
if (pos[i] < leftBoundary) {
nextPos[i] = leftBoundary;
relocated = true;
} else if (pos[i] + size[i] > rightBoundary) {
nextPos[i] = rightBoundary - size[i];
relocated = true;
}
}
return [relocated, nextPos];
};
// Start dragging the window
export const dragStartHandler = (event: MouseEvent) => {
logger.log('drag start');
dragging = true;
dragPointOffset = vecSubtract(
[event.screenX, event.screenY],
getWindowPosition()
);
// Focus click target
(event.target as HTMLElement)?.focus();
document.addEventListener('mousemove', dragMoveHandler);
document.addEventListener('mouseup', dragEndHandler);
dragMoveHandler(event);
};
// End dragging the window
const dragEndHandler = (event: MouseEvent) => {
logger.log('drag end');
dragMoveHandler(event);
document.removeEventListener('mousemove', dragMoveHandler);
document.removeEventListener('mouseup', dragEndHandler);
dragging = false;
storeWindowGeometry();
};
// Move the window while dragging
const dragMoveHandler = (event: MouseEvent) => {
if (!dragging) {
return;
}
event.preventDefault();
setWindowPosition(
vecSubtract([event.screenX, event.screenY], dragPointOffset)
);
};
// Start resizing the window
export const resizeStartHandler =
(x: number, y: number) => (event: MouseEvent) => {
resizeMatrix = [x, y];
logger.log('resize start', resizeMatrix);
resizing = true;
dragPointOffset = vecSubtract(
[event.screenX, event.screenY],
getWindowPosition()
);
initialSize = getWindowSize();
// Focus click target
(event.target as HTMLElement)?.focus();
document.addEventListener('mousemove', resizeMoveHandler);
document.addEventListener('mouseup', resizeEndHandler);
resizeMoveHandler(event);
};
// End resizing the window
const resizeEndHandler = (event: MouseEvent) => {
logger.log('resize end', size);
resizeMoveHandler(event);
document.removeEventListener('mousemove', resizeMoveHandler);
document.removeEventListener('mouseup', resizeEndHandler);
resizing = false;
storeWindowGeometry();
};
// Move the window while resizing
const resizeMoveHandler = (event: MouseEvent) => {
if (!resizing) {
return;
}
event.preventDefault();
const currentOffset = vecSubtract(
[event.screenX, event.screenY],
getWindowPosition()
);
const delta = vecSubtract(currentOffset, dragPointOffset);
// Extra 1x1 area is added to ensure the browser can see the cursor
size = vecAdd(initialSize, vecMultiply(resizeMatrix, delta), [1, 1]);
// Sane window size values
size[0] = Math.max(size[0], 150 * pixelRatio);
size[1] = Math.max(size[1], 50 * pixelRatio);
setWindowSize(size);
};