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 { getCurrentWindow, type Window } from '@tauri-apps/api/window';
import { LogicalSize, type PhysicalSize } from '@tauri-apps/api/dpi'; 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 }; 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 }; return { width: logical.width, height: logical.height };
} }
/** 웹 AppPopup(720×860)과 비슷한 picker OS 창 크기 — 논리 픽셀 */ /** OS 창 목표 크기 — 현재 창 크기(innerWidth)가 아닌 화면 기준 */
export function computeNativePickerLogicalSize(): LogicalWindowSize { export function computeNativePickerLogicalSize(): LogicalWindowSize {
const marginW = 40; const marginW = 32;
const marginH = 48; const marginH = 40;
const availW = Math.max( const screenW = window.screen?.availWidth ?? 1280;
520, const screenH = window.screen?.availHeight ?? 900;
window.innerWidth || 0,
(window.screen?.availWidth ?? 0) - marginW,
);
const availH = Math.max(
560,
window.innerHeight || 0,
(window.screen?.availHeight ?? 0) - marginH,
);
return { return {
width: Math.min(NATIVE_WIDGET_PICKER_WINDOW.width, availW), width: Math.min(WEB_WIDGET_PICKER_WINDOW.width, Math.max(520, screenW - marginW)),
height: Math.min(NATIVE_WIDGET_PICKER_WINDOW.height, availH), height: Math.min(WEB_WIDGET_PICKER_WINDOW.height, Math.max(560, screenH - marginH)),
}; };
} }
async function waitForLogicalSize( async function waitForLogicalSize(
win: Window, win: Window,
target: LogicalWindowSize, target: LogicalWindowSize,
timeoutMs = 900, timeoutMs = 1500,
): Promise<void> { ): Promise<void> {
const start = Date.now(); const start = Date.now();
while (Date.now() - start < timeoutMs) { while (Date.now() - start < timeoutMs) {
const current = await readLogicalInnerSize(win); const current = await readLogicalInnerSize(win);
if ( if (
Math.abs(current.width - target.width) < 12 Math.abs(current.width - target.width) < 16
&& Math.abs(current.height - target.height) < 12 && Math.abs(current.height - target.height) < 16
) { ) {
return; 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 before = await readLogicalInnerSize(win);
const target = computeNativePickerLogicalSize(); 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.setSize(new LogicalSize(target.width, target.height));
await win.center(); await win.center();
await win.setFocus();
await waitForLogicalSize(win, target); 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; return before;
} }
@@ -71,5 +75,5 @@ export async function restoreNativeWidgetWindowAfterPicker(
): Promise<void> { ): Promise<void> {
await win.setMinSize(new LogicalSize(WIDGET_MIN.width, WIDGET_MIN.height)); await win.setMinSize(new LogicalSize(WIDGET_MIN.width, WIDGET_MIN.height));
await win.setSize(new LogicalSize(before.width, before.height)); await win.setSize(new LogicalSize(before.width, before.height));
await waitForLogicalSize(win, before, 600); await waitForLogicalSize(win, before, 800);
} }
+63 -54
View File
@@ -118,7 +118,8 @@ const WidgetApp: React.FC = () => {
}, [theme]); }, [theme]);
const [pickSlotId, setPickSlotId] = useState<string | null>(null); const [pickSlotId, setPickSlotId] = useState<string | null>(null);
const [pickerLoading, setPickerLoading] = useState(false); const [pickerReady, setPickerReady] = useState(false);
const sizeBeforePickerRef = useRef<LogicalWindowSize | null>(null);
useEffect(() => { useEffect(() => {
if (!instance) return; if (!instance) return;
@@ -142,49 +143,58 @@ const WidgetApp: React.FC = () => {
return () => { unlisten?.(); }; return () => { unlisten?.(); };
}, [instance?.id]); }, [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(() => { const handleClose = useCallback(() => {
void getCurrentWindow().close(); 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) => { const handleNativePickRequest = useCallback((slotId: string) => {
setPickerLoading(true);
void (async () => {
try {
await handlePickerOpenChange(true);
setPickSlotId(slotId); setPickSlotId(slotId);
} catch (err) { }, []);
console.warn('[widget-main] picker open failed', err);
setPickSlotId(slotId);
} finally {
setPickerLoading(false);
}
})();
}, [handlePickerOpenChange]);
const handleNativePickClose = useCallback(() => { const handleNativePickClose = useCallback(() => {
setPickSlotId(null); setPickSlotId(null);
void Promise.resolve(handlePickerOpenChange(false)).catch(err => { }, []);
console.warn('[widget-main] picker close resize failed', err);
});
}, [handlePickerOpenChange]);
const handleNativePickSelect = useCallback((widgetType: string) => { const handleNativePickSelect = useCallback((widgetType: string) => {
if (!pickSlotId) return; if (!pickSlotId) return;
@@ -197,8 +207,8 @@ const WidgetApp: React.FC = () => {
)), )),
}; };
}); });
handleNativePickClose(); setPickSlotId(null);
}, [pickSlotId, handleNativePickClose]); }, [pickSlotId]);
if (!instance) { if (!instance) {
return <p className="wd-widget-empty"> .</p>; return <p className="wd-widget-empty"> .</p>;
@@ -220,22 +230,7 @@ const WidgetApp: React.FC = () => {
paperAutoTradeEnabled={false} paperAutoTradeEnabled={false}
chartRealtimeSource="BACKEND_STOMP" chartRealtimeSource="BACKEND_STOMP"
> >
<div className={`fw-widget-native-root${pickSlotId ? ' fw-widget-native-root--picker' : ''}`}> <div className="fw-widget-native-root">
{pickerLoading && (
<div className="wd-picker-native-loading" aria-live="polite">
</div>
)}
{pickSlotId != null && !pickerLoading ? (
<WidgetPickerModal
open
category="all"
nativeCompact
nativeFullRoot
onClose={handleNativePickClose}
onSelect={handleNativePickSelect}
/>
) : !pickerLoading ? (
<FloatingWidgetWindow <FloatingWidgetWindow
instance={instance} instance={instance}
focused focused
@@ -250,7 +245,21 @@ const WidgetApp: React.FC = () => {
}} }}
onUpdateGridFr={(rowFr, colFr) => setInstance(prev => (prev ? { ...prev, rowFr, colFr } : prev))} onUpdateGridFr={(rowFr, colFr) => setInstance(prev => (prev ? { ...prev, rowFr, colFr } : prev))}
/> />
) : null}
{pickSlotId != null && !pickerReady && (
<div className="wd-picker-native-loading" aria-live="polite">
</div>
)}
{pickSlotId != null && pickerReady && (
<WidgetPickerModal
open
category="all"
onClose={handleNativePickClose}
onSelect={handleNativePickSelect}
/>
)}
</div> </div>
</WidgetDashboardProvider> </WidgetDashboardProvider>
); );
+40 -13
View File
@@ -1241,7 +1241,46 @@
max-height: none; max-height: none;
} }
/* Tauri 네이티브 위젯 창 — 전체 창 picker (AppPopup draggable 미사용) */ /* Tauri 네이티브 위젯 창 — 서버와 동일 AppPopup (720×860 OS 창) */
html.fw-native-widget.fw-native-picker-mode,
html.fw-native-widget.fw-native-picker-mode body {
overflow: hidden;
}
html.fw-native-widget.fw-native-picker-mode .fw-widget-native-root .fw-window {
visibility: hidden;
pointer-events: none;
}
html.fw-native-widget.fw-native-picker-mode .app-popup-overlay {
position: fixed;
inset: 0;
z-index: 13200;
}
html.fw-native-widget.fw-native-picker-mode .app-popup-shell--widget-picker {
width: min(720px, calc(100vw - 16px)) !important;
max-height: min(860px, calc(100vh - 16px));
}
html.fw-native-widget.fw-native-picker-mode .app-popup-shell--widget-picker .wd-picker-popup-body {
max-height: min(760px, calc(100vh - 120px));
overflow-y: auto;
}
html.fw-native-widget .wd-picker-native-loading {
position: fixed;
inset: 0;
z-index: 13199;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.45);
color: var(--text2);
font-size: 13px;
}
/* (legacy) Tauri 네이티브 compact picker — fallback */
html.fw-native-widget .wd-picker-native-overlay { html.fw-native-widget .wd-picker-native-overlay {
position: fixed; position: fixed;
inset: 0; inset: 0;
@@ -1367,18 +1406,6 @@ html.fw-native-widget .fw-widget-native-root--picker .wd-picker-item-desc {
font-size: 12px; font-size: 12px;
} }
html.fw-native-widget .wd-picker-native-loading {
position: fixed;
inset: 0;
z-index: 13199;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.35);
color: var(--text2);
font-size: 13px;
}
.bps-page--wd-no-left .bps-side-wrap--left, .bps-page--wd-no-left .bps-side-wrap--left,
.bps-page--wd-no-left .bps-side-wrap--left + .bps-splitter { .bps-page--wd-no-left .bps-side-wrap--left + .bps-splitter {
display: none !important; display: none !important;
+2 -2
View File
@@ -25,8 +25,8 @@ export const FLOATING_WIDGET_CELL_MIN_W = 140;
export const FLOATING_WIDGET_CELL_MIN_H = 100; export const FLOATING_WIDGET_CELL_MIN_H = 100;
export const FLOATING_WIDGET_GRID_GAP = 1; export const FLOATING_WIDGET_GRID_GAP = 1;
/** Tauri 네이티브 위젯 창 — 위젯 선택 팝업 표시용 OS 창 크기 (논리 px, 웹 AppPopup과 유사) */ /** Tauri 네이티브 위젯 창 — 위젯 선택 OS 창 크기 (웹 AppPopup 720×860 과 동일) */
export const NATIVE_WIDGET_PICKER_WINDOW = { width: 820, height: 900 } as const; export const NATIVE_WIDGET_PICKER_WINDOW = { width: 720, height: 860 } as const;
export interface FloatingWidgetInstance { export interface FloatingWidgetInstance {
id: string; id: string;