diff --git a/desktop/src/nativeWidgetPickerWindow.ts b/desktop/src/nativeWidgetPickerWindow.ts index 9e864d3..1c592bd 100644 --- a/desktop/src/nativeWidgetPickerWindow.ts +++ b/desktop/src/nativeWidgetPickerWindow.ts @@ -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 { 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(resolve => { window.setTimeout(resolve, 40); }); + await new Promise(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 { 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); } diff --git a/desktop/src/widget-main.tsx b/desktop/src/widget-main.tsx index 8226a6c..3471b78 100644 --- a/desktop/src/widget-main.tsx +++ b/desktop/src/widget-main.tsx @@ -118,7 +118,8 @@ const WidgetApp: React.FC = () => { }, [theme]); const [pickSlotId, setPickSlotId] = useState(null); - const [pickerLoading, setPickerLoading] = useState(false); + const [pickerReady, setPickerReady] = useState(false); + const sizeBeforePickerRef = useRef(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(null); - - const handlePickerOpenChange = useCallback(async (open: boolean): Promise => { - 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

위젯 정보를 불러올 수 없습니다.

; @@ -220,37 +230,36 @@ const WidgetApp: React.FC = () => { paperAutoTradeEnabled={false} chartRealtimeSource="BACKEND_STOMP" > -
- {pickerLoading && ( +
+ {}} + 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 && (
위젯 목록 준비 중…
)} - {pickSlotId != null && !pickerLoading ? ( + + {pickSlotId != null && pickerReady && ( - ) : !pickerLoading ? ( - {}} - 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} + )}
); diff --git a/frontend/src/styles/widgetDashboard.css b/frontend/src/styles/widgetDashboard.css index d6b05f8..bcad5d1 100644 --- a/frontend/src/styles/widgetDashboard.css +++ b/frontend/src/styles/widgetDashboard.css @@ -1241,7 +1241,46 @@ 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 { position: fixed; inset: 0; @@ -1367,18 +1406,6 @@ 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; - 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-splitter { display: none !important; diff --git a/frontend/src/types/floatingWidget.ts b/frontend/src/types/floatingWidget.ts index 4fd7fc6..72cb557 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 창 크기 (논리 px, 웹 AppPopup과 유사) */ -export const NATIVE_WIDGET_PICKER_WINDOW = { width: 820, height: 900 } as const; +/** Tauri 네이티브 위젯 창 — 위젯 선택 시 OS 창 크기 (웹 AppPopup 720×860 과 동일) */ +export const NATIVE_WIDGET_PICKER_WINDOW = { width: 720, height: 860 } as const; export interface FloatingWidgetInstance { id: string;