fix(desktop): 위젯 picker를 웹 AppPopup(720×860)과 동일하게

현재 창 innerWidth로 크기를 제한하던 버그를 수정하고, nativeCompact
대신 서버와 같은 AppPopup UI를 macOS·Windows 공통 적용합니다.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Macbook
2026-06-15 17:02:59 +09:00
parent 96cb3250d6
commit a5e14ba676
4 changed files with 136 additions and 96 deletions
+26 -22
View File
@@ -1,6 +1,8 @@
import { getCurrentWindow, type Window } from '@tauri-apps/api/window';
import { LogicalSize, type PhysicalSize } from '@tauri-apps/api/dpi';
import { NATIVE_WIDGET_PICKER_WINDOW } from '@frontend/types/floatingWidget';
/** 서버 frontend AppPopup 위젯 선택 팝업과 동일 (720×860) */
export const WEB_WIDGET_PICKER_WINDOW = { width: 720, height: 860 } as const;
export type LogicalWindowSize = { width: number; height: number };
@@ -13,41 +15,33 @@ export async function readLogicalInnerSize(win: Window = getCurrentWindow()): Pr
return { width: logical.width, height: logical.height };
}
/** 웹 AppPopup(720×860)과 비슷한 picker OS 창 크기 — 논리 픽셀 */
/** OS 창 목표 크기 — 현재 창 크기(innerWidth)가 아닌 화면 기준 */
export function computeNativePickerLogicalSize(): LogicalWindowSize {
const marginW = 40;
const marginH = 48;
const availW = Math.max(
520,
window.innerWidth || 0,
(window.screen?.availWidth ?? 0) - marginW,
);
const availH = Math.max(
560,
window.innerHeight || 0,
(window.screen?.availHeight ?? 0) - marginH,
);
const marginW = 32;
const marginH = 40;
const screenW = window.screen?.availWidth ?? 1280;
const screenH = window.screen?.availHeight ?? 900;
return {
width: Math.min(NATIVE_WIDGET_PICKER_WINDOW.width, availW),
height: Math.min(NATIVE_WIDGET_PICKER_WINDOW.height, availH),
width: Math.min(WEB_WIDGET_PICKER_WINDOW.width, Math.max(520, screenW - marginW)),
height: Math.min(WEB_WIDGET_PICKER_WINDOW.height, Math.max(560, screenH - marginH)),
};
}
async function waitForLogicalSize(
win: Window,
target: LogicalWindowSize,
timeoutMs = 900,
timeoutMs = 1500,
): Promise<void> {
const start = Date.now();
while (Date.now() - start < timeoutMs) {
const current = await readLogicalInnerSize(win);
if (
Math.abs(current.width - target.width) < 12
&& Math.abs(current.height - target.height) < 12
Math.abs(current.width - target.width) < 16
&& Math.abs(current.height - target.height) < 16
) {
return;
}
await new Promise<void>(resolve => { window.setTimeout(resolve, 40); });
await new Promise<void>(resolve => { window.setTimeout(resolve, 50); });
}
}
@@ -57,11 +51,21 @@ export async function expandNativeWidgetWindowForPicker(
const before = await readLogicalInnerSize(win);
const target = computeNativePickerLogicalSize();
await win.setMinSize(new LogicalSize(480, 520));
await win.setMinSize(new LogicalSize(
Math.min(target.width, WEB_WIDGET_PICKER_WINDOW.width),
Math.min(target.height, 520),
));
await win.setSize(new LogicalSize(target.width, target.height));
await win.center();
await win.setFocus();
await waitForLogicalSize(win, target);
const after = await readLogicalInnerSize(win);
if (after.width < target.width - 24 || after.height < target.height - 24) {
await win.setSize(new LogicalSize(target.width, target.height));
await waitForLogicalSize(win, target, 800);
}
return before;
}
@@ -71,5 +75,5 @@ export async function restoreNativeWidgetWindowAfterPicker(
): Promise<void> {
await win.setMinSize(new LogicalSize(WIDGET_MIN.width, WIDGET_MIN.height));
await win.setSize(new LogicalSize(before.width, before.height));
await waitForLogicalSize(win, before, 600);
await waitForLogicalSize(win, before, 800);
}
+68 -59
View File
@@ -118,7 +118,8 @@ const WidgetApp: React.FC = () => {
}, [theme]);
const [pickSlotId, setPickSlotId] = useState<string | null>(null);
const [pickerLoading, setPickerLoading] = useState(false);
const [pickerReady, setPickerReady] = useState(false);
const sizeBeforePickerRef = useRef<LogicalWindowSize | null>(null);
useEffect(() => {
if (!instance) return;
@@ -142,49 +143,58 @@ const WidgetApp: React.FC = () => {
return () => { unlisten?.(); };
}, [instance?.id]);
useEffect(() => {
if (pickSlotId) {
document.documentElement.classList.add('fw-native-picker-mode');
} else {
document.documentElement.classList.remove('fw-native-picker-mode');
}
return () => {
document.documentElement.classList.remove('fw-native-picker-mode');
};
}, [pickSlotId]);
useEffect(() => {
if (!pickSlotId) {
setPickerReady(false);
const prev = sizeBeforePickerRef.current;
if (prev) {
sizeBeforePickerRef.current = null;
void restoreNativeWidgetWindowAfterPicker(prev).catch(err => {
console.warn('[widget-main] picker restore failed', err);
});
}
return;
}
let cancelled = false;
setPickerReady(false);
void (async () => {
try {
sizeBeforePickerRef.current = await expandNativeWidgetWindowForPicker();
if (!cancelled) setPickerReady(true);
} catch (err) {
console.warn('[widget-main] picker expand failed', err);
if (!cancelled) setPickerReady(true);
}
})();
return () => {
cancelled = true;
};
}, [pickSlotId]);
const handleClose = useCallback(() => {
void getCurrentWindow().close();
}, []);
const sizeBeforePickerRef = useRef<LogicalWindowSize | null>(null);
const handlePickerOpenChange = useCallback(async (open: boolean): Promise<void> => {
const win = getCurrentWindow();
try {
if (open) {
sizeBeforePickerRef.current = await expandNativeWidgetWindowForPicker(win);
return;
}
const prev = sizeBeforePickerRef.current;
if (!prev) return;
sizeBeforePickerRef.current = null;
await restoreNativeWidgetWindowAfterPicker(prev, win);
} catch (err) {
console.warn('[widget-main] picker window resize failed', err);
}
}, []);
const handleNativePickRequest = useCallback((slotId: string) => {
setPickerLoading(true);
void (async () => {
try {
await handlePickerOpenChange(true);
setPickSlotId(slotId);
} catch (err) {
console.warn('[widget-main] picker open failed', err);
setPickSlotId(slotId);
} finally {
setPickerLoading(false);
}
})();
}, [handlePickerOpenChange]);
setPickSlotId(slotId);
}, []);
const handleNativePickClose = useCallback(() => {
setPickSlotId(null);
void Promise.resolve(handlePickerOpenChange(false)).catch(err => {
console.warn('[widget-main] picker close resize failed', err);
});
}, [handlePickerOpenChange]);
}, []);
const handleNativePickSelect = useCallback((widgetType: string) => {
if (!pickSlotId) return;
@@ -197,8 +207,8 @@ const WidgetApp: React.FC = () => {
)),
};
});
handleNativePickClose();
}, [pickSlotId, handleNativePickClose]);
setPickSlotId(null);
}, [pickSlotId]);
if (!instance) {
return <p className="wd-widget-empty"> .</p>;
@@ -220,37 +230,36 @@ const WidgetApp: React.FC = () => {
paperAutoTradeEnabled={false}
chartRealtimeSource="BACKEND_STOMP"
>
<div className={`fw-widget-native-root${pickSlotId ? ' fw-widget-native-root--picker' : ''}`}>
{pickerLoading && (
<div className="fw-widget-native-root">
<FloatingWidgetWindow
instance={instance}
focused
nativeShell
onClose={handleClose}
onFocus={() => {}}
onNativePickRequest={handleNativePickRequest}
onUpdateSlots={slots => setInstance(prev => (prev ? { ...prev, slots } : prev))}
onUpdateSize={(width, height) => {
setInstance(prev => (prev ? { ...prev, width, height } : prev));
void getCurrentWindow().setSize(new LogicalSize(width, height));
}}
onUpdateGridFr={(rowFr, colFr) => setInstance(prev => (prev ? { ...prev, rowFr, colFr } : prev))}
/>
{pickSlotId != null && !pickerReady && (
<div className="wd-picker-native-loading" aria-live="polite">
</div>
)}
{pickSlotId != null && !pickerLoading ? (
{pickSlotId != null && pickerReady && (
<WidgetPickerModal
open
category="all"
nativeCompact
nativeFullRoot
onClose={handleNativePickClose}
onSelect={handleNativePickSelect}
/>
) : !pickerLoading ? (
<FloatingWidgetWindow
instance={instance}
focused
nativeShell
onClose={handleClose}
onFocus={() => {}}
onNativePickRequest={handleNativePickRequest}
onUpdateSlots={slots => setInstance(prev => (prev ? { ...prev, slots } : prev))}
onUpdateSize={(width, height) => {
setInstance(prev => (prev ? { ...prev, width, height } : prev));
void getCurrentWindow().setSize(new LogicalSize(width, height));
}}
onUpdateGridFr={(rowFr, colFr) => setInstance(prev => (prev ? { ...prev, rowFr, colFr } : prev))}
/>
) : null}
)}
</div>
</WidgetDashboardProvider>
);