diff --git a/desktop/src/nativeWidgetPickerWindow.ts b/desktop/src/nativeWidgetPickerWindow.ts new file mode 100644 index 0000000..9e864d3 --- /dev/null +++ b/desktop/src/nativeWidgetPickerWindow.ts @@ -0,0 +1,75 @@ +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'; + +export type LogicalWindowSize = { width: number; height: number }; + +const WIDGET_MIN = { width: 320, height: 240 } as const; + +export async function readLogicalInnerSize(win: Window = getCurrentWindow()): Promise { + const physical: PhysicalSize = await win.innerSize(); + const factor = await win.scaleFactor(); + const logical = physical.toLogical(factor); + return { width: logical.width, height: logical.height }; +} + +/** 웹 AppPopup(720×860)과 비슷한 picker OS 창 크기 — 논리 픽셀 */ +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, + ); + return { + width: Math.min(NATIVE_WIDGET_PICKER_WINDOW.width, availW), + height: Math.min(NATIVE_WIDGET_PICKER_WINDOW.height, availH), + }; +} + +async function waitForLogicalSize( + win: Window, + target: LogicalWindowSize, + timeoutMs = 900, +): Promise { + 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 + ) { + return; + } + await new Promise(resolve => { window.setTimeout(resolve, 40); }); + } +} + +export async function expandNativeWidgetWindowForPicker( + win: Window = getCurrentWindow(), +): Promise { + const before = await readLogicalInnerSize(win); + const target = computeNativePickerLogicalSize(); + + await win.setMinSize(new LogicalSize(480, 520)); + await win.setSize(new LogicalSize(target.width, target.height)); + await win.center(); + await waitForLogicalSize(win, target); + + return before; +} + +export async function restoreNativeWidgetWindowAfterPicker( + before: LogicalWindowSize, + win: Window = getCurrentWindow(), +): Promise { + 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); +} diff --git a/desktop/src/widget-main.tsx b/desktop/src/widget-main.tsx index f9bdac4..8226a6c 100644 --- a/desktop/src/widget-main.tsx +++ b/desktop/src/widget-main.tsx @@ -9,9 +9,14 @@ import FloatingWidgetWindow from '@frontend/components/floatingWidgets/FloatingW import WidgetPickerModal from '@frontend/components/widgetDashboard/WidgetPickerModal'; import { defaultGridFr, - NATIVE_WIDGET_PICKER_WINDOW, type FloatingWidgetInstance, } from '@frontend/types/floatingWidget'; +import { + expandNativeWidgetWindowForPicker, + readLogicalInnerSize, + restoreNativeWidgetWindowAfterPicker, + type LogicalWindowSize, +} from './nativeWidgetPickerWindow'; import type { Theme } from '@frontend/types'; import type { WidgetSlot } from '@frontend/types/widgetDashboard'; import { useMarketTicker } from '@frontend/hooks/useMarketTicker'; @@ -112,18 +117,23 @@ const WidgetApp: React.FC = () => { syncDocumentTheme(theme); }, [theme]); + const [pickSlotId, setPickSlotId] = useState(null); + const [pickerLoading, setPickerLoading] = useState(false); + useEffect(() => { if (!instance) return; - const title = `GoldenChart — 위젯 ${instance.rows}×${instance.cols}`; + const title = pickSlotId + ? 'GoldenChart — 위젯 선택' + : `GoldenChart — 위젯 ${instance.rows}×${instance.cols}`; void getCurrentWindow().setTitle(title); - }, [instance?.rows, instance?.cols, instance]); + }, [instance?.rows, instance?.cols, instance, pickSlotId]); useEffect(() => { if (!instance) return; let unlisten: (() => void) | undefined; void (async () => { unlisten = await getCurrentWindow().onResized(async () => { - const size = await getCurrentWindow().innerSize(); + const size = await readLogicalInnerSize(); setInstance(prev => ( prev ? { ...prev, width: size.width, height: size.height } : prev )); @@ -132,56 +142,41 @@ const WidgetApp: React.FC = () => { return () => { unlisten?.(); }; }, [instance?.id]); - const [pickSlotId, setPickSlotId] = useState(null); - const handleClose = useCallback(() => { void getCurrentWindow().close(); }, []); - const sizeBeforePickerRef = useRef<{ width: number; height: number } | null>(null); + const sizeBeforePickerRef = useRef(null); const handlePickerOpenChange = useCallback(async (open: boolean): Promise => { const win = getCurrentWindow(); try { if (open) { - const size = await win.innerSize(); - sizeBeforePickerRef.current = { width: size.width, height: size.height }; - const maxW = Math.max(480, window.screen.availWidth - 48); - const maxH = Math.max(520, window.screen.availHeight - 48); - const w = Math.min(NATIVE_WIDGET_PICKER_WINDOW.width, maxW); - const h = Math.min(NATIVE_WIDGET_PICKER_WINDOW.height, maxH); - await win.setSize(new LogicalSize(w, h)); - await win.center(); - await Promise.race([ - new Promise(resolve => { - let settled = false; - const finish = () => { - if (settled) return; - settled = true; - unlisten?.(); - resolve(); - }; - let unlisten: (() => void) | undefined; - void win.onResized(finish).then(fn => { unlisten = fn; }); - }), - new Promise(resolve => { window.setTimeout(resolve, 400); }), - ]); + sizeBeforePickerRef.current = await expandNativeWidgetWindowForPicker(win); return; } const prev = sizeBeforePickerRef.current; if (!prev) return; sizeBeforePickerRef.current = null; - await win.setSize(new LogicalSize(prev.width, prev.height)); + await restoreNativeWidgetWindowAfterPicker(prev, win); } catch (err) { console.warn('[widget-main] picker window resize failed', err); } }, []); const handleNativePickRequest = useCallback((slotId: string) => { - setPickSlotId(slotId); - void Promise.resolve(handlePickerOpenChange(true)).catch(err => { - console.warn('[widget-main] picker open resize failed', err); - }); + 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]); const handleNativePickClose = useCallback(() => { @@ -226,7 +221,12 @@ const WidgetApp: React.FC = () => { chartRealtimeSource="BACKEND_STOMP" >
- {pickSlotId != null ? ( + {pickerLoading && ( +
+ 위젯 목록 준비 중… +
+ )} + {pickSlotId != null && !pickerLoading ? ( { onClose={handleNativePickClose} onSelect={handleNativePickSelect} /> - ) : ( + ) : !pickerLoading ? ( { }} onUpdateGridFr={(rowFr, colFr) => setInstance(prev => (prev ? { ...prev, rowFr, colFr } : prev))} /> - )} + ) : null}
); diff --git a/frontend/src/styles/widgetDashboard.css b/frontend/src/styles/widgetDashboard.css index 3f6cbff..d6b05f8 100644 --- a/frontend/src/styles/widgetDashboard.css +++ b/frontend/src/styles/widgetDashboard.css @@ -1340,6 +1340,33 @@ html.fw-native-widget .wd-picker-native-root .wd-picker-native-shell { box-shadow: none; } +html.fw-native-widget .fw-widget-native-root--picker .wd-picker-popup-body { + padding: 14px 18px 20px; +} + +html.fw-native-widget .fw-widget-native-root--picker .wd-layout-intro { + margin: 0 0 14px; + font-size: 13px; + line-height: 1.5; +} + +html.fw-native-widget .fw-widget-native-root--picker .wd-picker-grid { + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 12px; +} + +html.fw-native-widget .fw-widget-native-root--picker .wd-picker-item { + padding: 14px 16px; +} + +html.fw-native-widget .fw-widget-native-root--picker .wd-picker-item-label { + font-size: 14px; +} + +html.fw-native-widget .fw-widget-native-root--picker .wd-picker-item-desc { + font-size: 12px; +} + html.fw-native-widget .wd-picker-native-loading { position: fixed; inset: 0; diff --git a/frontend/src/types/floatingWidget.ts b/frontend/src/types/floatingWidget.ts index 34d877a..4fd7fc6 100644 --- a/frontend/src/types/floatingWidget.ts +++ b/frontend/src/types/floatingWidget.ts @@ -25,8 +25,8 @@ export const FLOATING_WIDGET_CELL_MIN_W = 140; export const FLOATING_WIDGET_CELL_MIN_H = 100; export const FLOATING_WIDGET_GRID_GAP = 1; -/** Tauri 네이티브 위젯 창 — 위젯 선택 팝업 표시용 OS 창 크기 */ -export const NATIVE_WIDGET_PICKER_WINDOW = { width: 740, height: 860 } as const; +/** Tauri 네이티브 위젯 창 — 위젯 선택 팝업 표시용 OS 창 크기 (논리 px, 웹 AppPopup과 유사) */ +export const NATIVE_WIDGET_PICKER_WINDOW = { width: 820, height: 900 } as const; export interface FloatingWidgetInstance { id: string;