import type { PreviewViewportSetting } from "@t3tools/contracts"; import type { BrowserSurfaceContentPresentation } from "~/browser/browserSurfaceStore"; import { resolveFittedBrowserViewport, type BrowserViewportResizeDirection, } from "~/browser/browserViewportLayout"; import type { PreviewMiniPlayerPosition, PreviewMiniPlayerSize } from "~/previewMiniPlayerStore"; export const PREVIEW_MINI_PLAYER_EDGE_GAP = 12; // The mini-player shell straddles this webview at 47 and 49; dialogs begin at 50. export const PREVIEW_MINI_PLAYER_WEBVIEW_Z_INDEX = 48; // A fresh player is the largest box at the source aspect ratio that fits here. const PREVIEW_MINI_PLAYER_DEFAULT_BOX = { width: 320, height: 320 } as const; const PREVIEW_MINI_PLAYER_MIN_SIZE = { width: 240, height: 150 } as const; export interface PreviewMiniPlayerFrame extends PreviewMiniPlayerPosition, PreviewMiniPlayerSize {} /** * The rendered size of what the floating player mirrors: the device viewport * when one is set, otherwise the size the webview had when it was floated * (`fittedSourceContent`), which the hosted webview keeps as its CSS viewport. */ export function resolvePreviewMiniPlayerSourceSize( viewport: PreviewViewportSetting, fittedSourceContent: BrowserSurfaceContentPresentation | null, zoomFactor: number, ): PreviewMiniPlayerSize { const normalizedZoomFactor = Number.isFinite(zoomFactor) && zoomFactor > 0 ? zoomFactor : 1; const fitted = resolveFittedBrowserViewport(viewport, fittedSourceContent, normalizedZoomFactor); return { width: fitted.width * normalizedZoomFactor, height: fitted.height * normalizedZoomFactor, }; } const availableArea = ( container: PreviewMiniPlayerSize, bottomInset: number, ): PreviewMiniPlayerSize => ({ width: container.width - PREVIEW_MINI_PLAYER_EDGE_GAP * 2, height: container.height - Math.max(0, bottomInset) - PREVIEW_MINI_PLAYER_EDGE_GAP * 2, }); /** * Width is the player's only free dimension; height always follows the source * aspect ratio so the webview fills the box without letterboxing. The player * never grows past the source's own size (the guest keeps its CSS viewport, so * going bigger would only upscale), and a tight container wins over the minimum. */ function fitPreviewMiniPlayerWidth( desiredWidth: number, source: PreviewMiniPlayerSize, max: PreviewMiniPlayerSize, ): PreviewMiniPlayerSize { const aspectRatio = source.width / source.height; const width = Math.min( Math.max( desiredWidth, PREVIEW_MINI_PLAYER_MIN_SIZE.width, PREVIEW_MINI_PLAYER_MIN_SIZE.height * aspectRatio, ), source.width, Math.max(1, max.width), Math.max(1, max.height * aspectRatio), ); return { width: Math.round(width), height: Math.round(width / aspectRatio) }; } function defaultPreviewMiniPlayerWidth(source: PreviewMiniPlayerSize): number { return Math.min( PREVIEW_MINI_PLAYER_DEFAULT_BOX.width, (PREVIEW_MINI_PLAYER_DEFAULT_BOX.height * source.width) / source.height, ); } export function clampPreviewMiniPlayerPosition( position: PreviewMiniPlayerPosition, container: PreviewMiniPlayerSize, player: PreviewMiniPlayerSize, bottomInset = 0, ): PreviewMiniPlayerPosition { const reservedBottomSpace = Math.max(0, bottomInset); const maxX = Math.max( PREVIEW_MINI_PLAYER_EDGE_GAP, container.width - player.width - PREVIEW_MINI_PLAYER_EDGE_GAP, ); const maxY = Math.max( PREVIEW_MINI_PLAYER_EDGE_GAP, container.height - reservedBottomSpace - player.height - PREVIEW_MINI_PLAYER_EDGE_GAP, ); return { x: Math.min(Math.max(position.x, PREVIEW_MINI_PLAYER_EDGE_GAP), maxX), y: Math.min(Math.max(position.y, PREVIEW_MINI_PLAYER_EDGE_GAP), maxY), }; } /** * Resolves the on-screen frame from the stored width and position. Clamping * happens here on every layout pass instead of being written back to the * store, so a temporarily narrow container never destroys the user's chosen * width. A player without a position sits in the top-right corner. */ export function resolvePreviewMiniPlayerFrame(input: { readonly width: number | null; readonly position: PreviewMiniPlayerPosition | null; readonly source: PreviewMiniPlayerSize; readonly container: PreviewMiniPlayerSize; readonly bottomInset?: number; }): PreviewMiniPlayerFrame { const { width, position, source, container, bottomInset = 0 } = input; const size = fitPreviewMiniPlayerWidth( width ?? defaultPreviewMiniPlayerWidth(source), source, availableArea(container, bottomInset), ); const anchored = position ?? { x: container.width - PREVIEW_MINI_PLAYER_EDGE_GAP - size.width, y: PREVIEW_MINI_PLAYER_EDGE_GAP, }; return { ...clampPreviewMiniPlayerPosition(anchored, container, size, bottomInset), ...size }; } /** * Resizes from any edge or corner while holding the aspect ratio. The edge * opposite the dragged one stays anchored, so growth stops at the container * on that axis and the pointer keeps tracking the grabbed edge. On a plain edge * drag the perpendicular axis may use the whole container, and the player * shifts as needed to stay inside. */ export function resizePreviewMiniPlayer(input: { readonly start: PreviewMiniPlayerFrame; readonly direction: BrowserViewportResizeDirection; readonly delta: PreviewMiniPlayerPosition; readonly source: PreviewMiniPlayerSize; readonly container: PreviewMiniPlayerSize; readonly bottomInset?: number; }): PreviewMiniPlayerFrame { const { start, direction, delta, source, container, bottomInset = 0 } = input; const east = direction.includes("east"); const west = direction.includes("west"); const north = direction.includes("north"); const south = direction.includes("south"); const available = availableArea(container, bottomInset); const right = start.x + start.width; const bottom = start.y + start.height; const max = { width: west ? right - PREVIEW_MINI_PLAYER_EDGE_GAP : east ? container.width - PREVIEW_MINI_PLAYER_EDGE_GAP - start.x : available.width, height: north ? bottom - PREVIEW_MINI_PLAYER_EDGE_GAP : south ? container.height - Math.max(0, bottomInset) - PREVIEW_MINI_PLAYER_EDGE_GAP - start.y : available.height, }; const desiredWidth = start.width + (east ? delta.x : west ? -delta.x : 0); const desiredHeight = start.height + (south ? delta.y : north ? -delta.y : 0); const horizontal = east || west; const vertical = north || south; const widthLeads = horizontal && !vertical ? true : vertical && !horizontal ? false : Math.abs(desiredWidth - start.width) / start.width >= Math.abs(desiredHeight - start.height) / start.height; const size = fitPreviewMiniPlayerWidth( widthLeads ? desiredWidth : (desiredHeight * source.width) / source.height, source, max, ); const position = clampPreviewMiniPlayerPosition( { x: west ? right - size.width : start.x, y: north ? bottom - size.height : start.y }, container, size, bottomInset, ); return { ...position, ...size }; }