From 9282d5fa37aece405b540de16401e15d70583df4 Mon Sep 17 00:00:00 2001 From: CHOMPStation2StaffMirrorBot <94713762+CHOMPStation2StaffMirrorBot@users.noreply.github.com> Date: Mon, 15 Dec 2025 09:21:22 -0700 Subject: [PATCH] [MIRROR] dragport [NO GBP] (#12123) Co-authored-by: Kashargul <144968721+Kashargul@users.noreply.github.com> --- tgui/packages/tgui/drag.ts | 198 ++++++++++++++++++++++++------------- 1 file changed, 127 insertions(+), 71 deletions(-) diff --git a/tgui/packages/tgui/drag.ts b/tgui/packages/tgui/drag.ts index 205a21e9f6..68ed9df38d 100644 --- a/tgui/packages/tgui/drag.ts +++ b/tgui/packages/tgui/drag.ts @@ -7,64 +7,118 @@ import { storage } from 'common/storage'; import type { BooleanLike } from 'tgui-core/react'; import { vecAdd, vecMultiply, vecScale, vecSubtract } from 'tgui-core/vector'; - import { createLogger } from './logging'; +type Point = [number, number]; +type GeometryPayload = Partial<{ + pos: string; + size: string; +}>; + 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]; +let screenOffset: Point = [0, 0]; +let screenOffsetPromise: Promise; +let dragPointOffset: Point; +let resizeMatrix: Point; +let initialSize: Point; +let size: Point; + +let winsetRaf: number | undefined; +let pendingWinset: GeometryPayload = {}; +let lastSentWinset: GeometryPayload = {}; + +/** Expends all stored winset calls in a single call. */ +function flushWinset(): void { + winsetRaf = undefined; + + const payload: GeometryPayload = {}; + if (pendingWinset.pos && pendingWinset.pos !== lastSentWinset.pos) { + payload.pos = pendingWinset.pos; + } + if (pendingWinset.size && pendingWinset.size !== lastSentWinset.size) { + payload.size = pendingWinset.size; + } + + pendingWinset = {}; + + if (payload.pos || payload.size) { + Byond.winset(Byond.windowId, payload); + lastSentWinset = { ...lastSentWinset, ...payload }; + } +} + +function scheduleWinset(): void { + if (winsetRaf !== undefined) { + return; + } + winsetRaf = requestAnimationFrame(flushWinset); +} + +function flushWinsetNow(): void { + if (winsetRaf !== undefined) { + cancelAnimationFrame(winsetRaf); + winsetRaf = undefined; + } + flushWinset(); +} // Set the window key -export const setWindowKey = (key: string): void => { +export function setWindowKey(key: string): void { windowKey = key; -}; +} // Get window position -export const getWindowPosition = (): [number, number] => [ - window.screenLeft * pixelRatio, - window.screenTop * pixelRatio, -]; +export function getWindowPosition(): Point { + return [window.screenLeft * pixelRatio, window.screenTop * pixelRatio]; +} // Get window size -export const getWindowSize = (): [number, number] => [ - window.innerWidth * pixelRatio, - window.innerHeight * pixelRatio, -]; +export function getWindowSize(): Point { + return [window.innerWidth * pixelRatio, window.innerHeight * pixelRatio]; +} // Set window position -const setWindowPosition = (vec: [number, number]) => { +export function setWindowPosition(vec: Point): void { const byondPos = vecAdd(vec, screenOffset); - return Byond.winset(Byond.windowId, { + Byond.winset(Byond.windowId, { pos: `${byondPos[0]},${byondPos[1]}`, }); -}; +} + +function setWindowPositionBatched(vec: Point): void { + const byondPos = vecAdd(vec, screenOffset); + pendingWinset.pos = `${byondPos[0]},${byondPos[1]}`; + scheduleWinset(); +} // Set window size -const setWindowSize = (vec: [number, number]) => { - return Byond.winset(Byond.windowId, { +function setWindowSize(vec: Point): void { + Byond.winset(Byond.windowId, { size: `${vec[0]}x${vec[1]}`, }); -}; +} + +function setWindowSizeBatched(vec: Point): void { + pendingWinset.size = `${vec[0]}x${vec[1]}`; + scheduleWinset(); +} // Get screen position -const getScreenPosition = (): [number, number] => [ - 0 - screenOffset[0], - 0 - screenOffset[1], -]; +function getScreenPosition(): Point { + return [0 - screenOffset[0], 0 - screenOffset[1]]; +} // Get screen size -const getScreenSize = (): [number, number] => [ - window.screen.availWidth * pixelRatio, - window.screen.availHeight * pixelRatio, -]; +function getScreenSize(): Point { + return [ + window.screen.availWidth * pixelRatio, + window.screen.availHeight * pixelRatio, + ]; +} /** * Moves an item to the top of the recents array, and keeps its length @@ -74,11 +128,11 @@ const getScreenSize = (): [number, number] => [ * * Returns new recents and an item which was trimmed. */ -export const touchRecents = ( +export function touchRecents( recents: string[], touchedItem: string, limit = 50, -): [string[], string | undefined] => { +): [string[], string | undefined] { const nextRecents: string[] = [touchedItem]; let trimmedItem: string | undefined; for (let i = 0; i < recents.length; i++) { @@ -93,10 +147,10 @@ export const touchRecents = ( } } return [nextRecents, trimmedItem]; -}; +} // Store window geometry in local storage -const storeWindowGeometry = async () => { +export async function storeWindowGeometry(): Promise { logger.log('storing geometry'); const geometry = { pos: getWindowPosition(), @@ -112,19 +166,20 @@ const storeWindowGeometry = async () => { storage.remove(trimmedKey); } storage.set('geometries', geometries); -}; +} + +type RecallOptions = Partial<{ + pos: Point; + size: Point; + locked: BooleanLike; + scale: BooleanLike; +}>; // Recall window geometry from local storage and apply it -export const recallWindowGeometry = async ( - options: { - fancy?: BooleanLike; - pos?: [number, number]; - size?: [number, number]; - locked?: BooleanLike; - scale?: BooleanLike; - } = {}, -) => { - const geometry = options.fancy && (await storage.get(windowKey)); +export async function recallWindowGeometry( + options: RecallOptions = {}, +): Promise { + const geometry = await storage.get(windowKey); if (geometry) { logger.log('recalled geometry:', geometry); } @@ -175,10 +230,10 @@ export const recallWindowGeometry = async ( ); setWindowPosition(pos); } -}; +} // Setup draggable window -export const setupDrag = async () => { +export async function setupDrag(): Promise { // Calculate screen offset caused by the windows taskbar const windowPosition = getWindowPosition(); @@ -188,20 +243,18 @@ export const setupDrag = async () => { ]); 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]] => { +function constraintPosition(pos: Point, size: Point): [boolean, Point] { const screenPos = getScreenPosition(); const screenSize = getScreenSize(); - const nextPos: [number, number] = [pos[0], pos[1]]; + const nextPos: Point = [pos[0], pos[1]]; let relocated = false; + for (let i = 0; i < 2; i++) { const leftBoundary = screenPos[i]; const rightBoundary = screenPos[i] + screenSize[i]; @@ -214,57 +267,59 @@ const constraintPosition = ( } } return [relocated, nextPos]; -}; +} // Start dragging the window -export const dragStartHandler = (event) => { +export function dragStartHandler(event): void { logger.log('drag start'); dragging = true; dragPointOffset = vecSubtract( [event.screenX * pixelRatio, event.screenY * pixelRatio], getWindowPosition(), - ) as [number, number]; + ) as Point; // 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) => { +function dragEndHandler(event): void { logger.log('drag end'); dragMoveHandler(event); + flushWinsetNow(); document.removeEventListener('mousemove', dragMoveHandler); document.removeEventListener('mouseup', dragEndHandler); dragging = false; storeWindowGeometry(); -}; +} // Move the window while dragging -const dragMoveHandler = (event: MouseEvent) => { +function dragMoveHandler(event: MouseEvent): void { if (!dragging) { return; } event.preventDefault(); - setWindowPosition( + setWindowPositionBatched( vecSubtract( [event.screenX * pixelRatio, event.screenY * pixelRatio], dragPointOffset, - ) as [number, number], + ) as Point, ); -}; +} // Start resizing the window export const resizeStartHandler = - (x: number, y: number) => (event: MouseEvent) => { + (x: number, y: number) => + (event: MouseEvent): void => { resizeMatrix = [x, y]; logger.log('resize start', resizeMatrix); resizing = true; dragPointOffset = vecSubtract( [event.screenX * pixelRatio, event.screenY * pixelRatio], getWindowPosition(), - ) as [number, number]; + ) as Point; initialSize = getWindowSize(); // Focus click target (event.target as HTMLElement)?.focus(); @@ -274,17 +329,18 @@ export const resizeStartHandler = }; // End resizing the window -const resizeEndHandler = (event: MouseEvent) => { +function resizeEndHandler(event: MouseEvent): void { logger.log('resize end', size); resizeMoveHandler(event); + flushWinsetNow(); document.removeEventListener('mousemove', resizeMoveHandler); document.removeEventListener('mouseup', resizeEndHandler); resizing = false; storeWindowGeometry(); -}; +} // Move the window while resizing -const resizeMoveHandler = (event: MouseEvent) => { +function resizeMoveHandler(event: MouseEvent): void { if (!resizing) { return; } @@ -302,5 +358,5 @@ const resizeMoveHandler = (event: MouseEvent) => { // Sane window size values size[0] = Math.max(size[0], 150 * pixelRatio); size[1] = Math.max(size[1], 50 * pixelRatio); - setWindowSize(size); -}; + setWindowSizeBatched(size); +}