Open History to see the last commit
1import {
2 clampPreviewMiniPlayerPosition,
3 resolvePreviewMiniPlayerFrame,
4 type PreviewMiniPlayerFrame,
5 type PreviewMiniPlayerObstacles,
6} from "../preview/previewMiniPlayerLayout";
7import type {
8 PreviewMiniPlayerPosition,
9 PreviewMiniPlayerSize,
10 PreviewMiniPlayerState,
11} from "~/previewMiniPlayerStore";
12
13export interface ChatCanvasPreview {
14 readonly key: string;
15 readonly width: number | null;
16 readonly position: PreviewMiniPlayerPosition | null;
17 readonly source: PreviewMiniPlayerSize;
18 readonly lastInteraction?: PreviewMiniPlayerState["lastInteraction"];
19}
20
21const GAP = 12;
22export const DETAILS_CARD_CLEARANCE = 32;
23
24export function resolveChatCanvasLayout({
25 container,
26 preview,
27 padding = 48,
28 maxChatWidth = 768,
29 minChatWidth = 640,
30 composerHeight = 0,
31 detailsCard = null,
32 findBar = null,
33}: {
34 container: PreviewMiniPlayerSize;
35 preview: ChatCanvasPreview | null;
36 padding?: number;
37 maxChatWidth?: number;
38 minChatWidth?: number;
39 composerHeight?: number;
40 detailsCard?: PreviewMiniPlayerObstacles["detailsCard"];
41 findBar?: PreviewMiniPlayerObstacles["detailsCard"];
42}) {
43 const centeredWidth = Math.max(0, Math.min(maxChatWidth, container.width - padding * 2));
44 const laneRight = detailsCard
45 ? detailsCard.left - DETAILS_CARD_CLEARANCE
46 : container.width - padding;
47 const normalWidth = Math.max(0, Math.min(centeredWidth, laneRight - padding));
48 const canvasCenterLeft = (container.width - normalWidth) / 2;
49 const normalLeft = Math.max(padding, Math.min(canvasCenterLeft, laneRight - normalWidth));
50 let chat = {
51 left: normalLeft,
52 width: normalWidth,
53 insetStart: 0,
54 insetEnd: Math.max(0, container.width - normalLeft * 2 - normalWidth),
55 };
56 let frame: PreviewMiniPlayerFrame | null = null;
57 let overlapsChat = false;
58 if (preview && container.width > 0 && container.height > 0) {
59 frame = resolvePreviewMiniPlayerFrame({ ...preview, container });
60 // Reserve a chat lane only if the preview still has room to drag.
61 const minimumPreviewX =
62 preview.lastInteraction === "resize" ||
63 container.width <= padding + minChatWidth + frame.width + GAP * 2
64 ? GAP
65 : padding + minChatWidth + GAP;
66 if (preview.position === null) frame = { ...frame, y: container.height - frame.height - GAP };
67 if (preview.lastInteraction === "resize" && composerHeight > 0) {
68 const laneWidth = Math.min(normalWidth, minChatWidth);
69 const transitionWidth = Math.max(GAP, normalWidth - laneWidth);
70 const lift = Math.min(
71 1,
72 Math.max(0, (padding + normalWidth + GAP - frame.x) / transitionWidth),
73 );
74 if (lift > 0) {
75 frame = resolvePreviewMiniPlayerFrame({
76 width: frame.width,
77 position: frame,
78 source: preview.source,
79 container: {
80 ...container,
81 height: Math.max(GAP * 2 + 1, container.height - composerHeight * lift),
82 },
83 });
84 }
85 }
86 const findBarObstacles = findBar ? { detailsCard: findBar, composer: null } : undefined;
87 const preferredFrame = {
88 ...frame,
89 ...clampPreviewMiniPlayerPosition(frame, container, frame, findBarObstacles, minimumPreviewX),
90 };
91 const dragObstacle =
92 detailsCard && findBar
93 ? {
94 left: Math.min(detailsCard.left, findBar.left),
95 right: Math.max(detailsCard.right, findBar.right),
96 bottom: Math.max(detailsCard.bottom, findBar.bottom),
97 }
98 : (detailsCard ?? findBar);
99 const cardObstacle = preview.lastInteraction === "resize" ? findBar : dragObstacle;
100 frame = {
101 ...frame,
102 ...clampPreviewMiniPlayerPosition(
103 frame,
104 container,
105 frame,
106 { detailsCard: cardObstacle, composer: null },
107 minimumPreviewX,
108 ),
109 };
110 const chatBeside = (player: PreviewMiniPlayerFrame) => {
111 const normalRight = normalLeft + normalWidth;
112 if (player.x >= normalRight + GAP) return chat;
113 const width = Math.min(maxChatWidth, player.x - GAP - padding);
114 if (width < minChatWidth) return null;
115 const left = Math.min(normalLeft, player.x - GAP - width);
116 return {
117 left,
118 width,
119 insetStart: 0,
120 insetEnd: Math.max(0, container.width - left * 2 - width),
121 };
122 };
123 let nextChat = chatBeside(frame);
124 if (!nextChat && detailsCard) {
125 nextChat = chatBeside(preferredFrame);
126 if (nextChat) frame = preferredFrame;
127 }
128 if (nextChat) chat = nextChat;
129 else overlapsChat = true;
130 if (overlapsChat && preview.lastInteraction === "resize") {
131 const width = Math.min(normalWidth, minChatWidth);
132 chat = {
133 left: padding,
134 width,
135 insetStart: 0,
136 insetEnd: Math.max(0, container.width - padding * 2 - width),
137 };
138 } else if (overlapsChat) {
139 const obstacles = {
140 detailsCard: cardObstacle,
141 composer: { left: chat.left, right: chat.left + chat.width, height: composerHeight },
142 };
143 frame = resolvePreviewMiniPlayerFrame({ ...preview, container, obstacles });
144 frame = {
145 ...frame,
146 ...clampPreviewMiniPlayerPosition(
147 preview.position ?? { x: frame.x, y: container.height - frame.height - GAP },
148 container,
149 frame,
150 obstacles,
151 minimumPreviewX,
152 ),
153 };
154 if (
155 frame.x < obstacles.composer.right &&
156 frame.x + frame.width > obstacles.composer.left &&
157 frame.y + frame.height > container.height - composerHeight
158 ) {
159 frame = {
160 ...frame,
161 ...clampPreviewMiniPlayerPosition(
162 frame,
163 container,
164 frame,
165 { composer: obstacles.composer, detailsCard: findBar },
166 minimumPreviewX,
167 ),
168 };
169 }
170 }
171 }
172 const overlapsDetailsCard = Boolean(
173 frame &&
174 detailsCard &&
175 frame.x < detailsCard.right &&
176 frame.x + frame.width > detailsCard.left &&
177 frame.y < detailsCard.bottom,
178 );
179 return { chat, frame, overlapsChat, overlapsDetailsCard };
180}