fix(desktop): 위젯 picker OS 창 크기 확대 및 논리 px 보정

picker 표시 전 OS 창을 820×900(논리 px)으로 확장하고 PhysicalSize/
LogicalSize 혼용 버그를 수정해 macOS·Windows에서 목록이 넓게 보이게 합니다.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Macbook
2026-06-15 16:53:24 +09:00
parent 5086d9e6bd
commit 046ef0a458
4 changed files with 141 additions and 39 deletions
+75
View File
@@ -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<LogicalWindowSize> {
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<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
) {
return;
}
await new Promise<void>(resolve => { window.setTimeout(resolve, 40); });
}
}
export async function expandNativeWidgetWindowForPicker(
win: Window = getCurrentWindow(),
): Promise<LogicalWindowSize> {
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<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);
}
+36 -36
View File
@@ -9,9 +9,14 @@ import FloatingWidgetWindow from '@frontend/components/floatingWidgets/FloatingW
import WidgetPickerModal from '@frontend/components/widgetDashboard/WidgetPickerModal'; import WidgetPickerModal from '@frontend/components/widgetDashboard/WidgetPickerModal';
import { import {
defaultGridFr, defaultGridFr,
NATIVE_WIDGET_PICKER_WINDOW,
type FloatingWidgetInstance, type FloatingWidgetInstance,
} from '@frontend/types/floatingWidget'; } from '@frontend/types/floatingWidget';
import {
expandNativeWidgetWindowForPicker,
readLogicalInnerSize,
restoreNativeWidgetWindowAfterPicker,
type LogicalWindowSize,
} from './nativeWidgetPickerWindow';
import type { Theme } from '@frontend/types'; import type { Theme } from '@frontend/types';
import type { WidgetSlot } from '@frontend/types/widgetDashboard'; import type { WidgetSlot } from '@frontend/types/widgetDashboard';
import { useMarketTicker } from '@frontend/hooks/useMarketTicker'; import { useMarketTicker } from '@frontend/hooks/useMarketTicker';
@@ -112,18 +117,23 @@ const WidgetApp: React.FC = () => {
syncDocumentTheme(theme); syncDocumentTheme(theme);
}, [theme]); }, [theme]);
const [pickSlotId, setPickSlotId] = useState<string | null>(null);
const [pickerLoading, setPickerLoading] = useState(false);
useEffect(() => { useEffect(() => {
if (!instance) return; if (!instance) return;
const title = `GoldenChart — 위젯 ${instance.rows}×${instance.cols}`; const title = pickSlotId
? 'GoldenChart — 위젯 선택'
: `GoldenChart — 위젯 ${instance.rows}×${instance.cols}`;
void getCurrentWindow().setTitle(title); void getCurrentWindow().setTitle(title);
}, [instance?.rows, instance?.cols, instance]); }, [instance?.rows, instance?.cols, instance, pickSlotId]);
useEffect(() => { useEffect(() => {
if (!instance) return; if (!instance) return;
let unlisten: (() => void) | undefined; let unlisten: (() => void) | undefined;
void (async () => { void (async () => {
unlisten = await getCurrentWindow().onResized(async () => { unlisten = await getCurrentWindow().onResized(async () => {
const size = await getCurrentWindow().innerSize(); const size = await readLogicalInnerSize();
setInstance(prev => ( setInstance(prev => (
prev ? { ...prev, width: size.width, height: size.height } : prev prev ? { ...prev, width: size.width, height: size.height } : prev
)); ));
@@ -132,56 +142,41 @@ const WidgetApp: React.FC = () => {
return () => { unlisten?.(); }; return () => { unlisten?.(); };
}, [instance?.id]); }, [instance?.id]);
const [pickSlotId, setPickSlotId] = useState<string | null>(null);
const handleClose = useCallback(() => { const handleClose = useCallback(() => {
void getCurrentWindow().close(); void getCurrentWindow().close();
}, []); }, []);
const sizeBeforePickerRef = useRef<{ width: number; height: number } | null>(null); const sizeBeforePickerRef = useRef<LogicalWindowSize | null>(null);
const handlePickerOpenChange = useCallback(async (open: boolean): Promise<void> => { const handlePickerOpenChange = useCallback(async (open: boolean): Promise<void> => {
const win = getCurrentWindow(); const win = getCurrentWindow();
try { try {
if (open) { if (open) {
const size = await win.innerSize(); sizeBeforePickerRef.current = await expandNativeWidgetWindowForPicker(win);
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<void>(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<void>(resolve => { window.setTimeout(resolve, 400); }),
]);
return; return;
} }
const prev = sizeBeforePickerRef.current; const prev = sizeBeforePickerRef.current;
if (!prev) return; if (!prev) return;
sizeBeforePickerRef.current = null; sizeBeforePickerRef.current = null;
await win.setSize(new LogicalSize(prev.width, prev.height)); await restoreNativeWidgetWindowAfterPicker(prev, win);
} catch (err) { } catch (err) {
console.warn('[widget-main] picker window resize failed', 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);
void Promise.resolve(handlePickerOpenChange(true)).catch(err => { } catch (err) {
console.warn('[widget-main] picker open resize failed', err); console.warn('[widget-main] picker open failed', err);
}); setPickSlotId(slotId);
} finally {
setPickerLoading(false);
}
})();
}, [handlePickerOpenChange]); }, [handlePickerOpenChange]);
const handleNativePickClose = useCallback(() => { const handleNativePickClose = useCallback(() => {
@@ -226,7 +221,12 @@ const WidgetApp: React.FC = () => {
chartRealtimeSource="BACKEND_STOMP" chartRealtimeSource="BACKEND_STOMP"
> >
<div className={`fw-widget-native-root${pickSlotId ? ' fw-widget-native-root--picker' : ''}`}> <div className={`fw-widget-native-root${pickSlotId ? ' fw-widget-native-root--picker' : ''}`}>
{pickSlotId != null ? ( {pickerLoading && (
<div className="wd-picker-native-loading" aria-live="polite">
</div>
)}
{pickSlotId != null && !pickerLoading ? (
<WidgetPickerModal <WidgetPickerModal
open open
category="all" category="all"
@@ -235,7 +235,7 @@ const WidgetApp: React.FC = () => {
onClose={handleNativePickClose} onClose={handleNativePickClose}
onSelect={handleNativePickSelect} onSelect={handleNativePickSelect}
/> />
) : ( ) : !pickerLoading ? (
<FloatingWidgetWindow <FloatingWidgetWindow
instance={instance} instance={instance}
focused focused
@@ -250,7 +250,7 @@ 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}
</div> </div>
</WidgetDashboardProvider> </WidgetDashboardProvider>
); );
+27
View File
@@ -1340,6 +1340,33 @@ html.fw-native-widget .wd-picker-native-root .wd-picker-native-shell {
box-shadow: none; 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 { html.fw-native-widget .wd-picker-native-loading {
position: fixed; position: fixed;
inset: 0; inset: 0;
+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 창 크기 */ /** Tauri 네이티브 위젯 창 — 위젯 선택 팝업 표시용 OS 창 크기 (논리 px, 웹 AppPopup과 유사) */
export const NATIVE_WIDGET_PICKER_WINDOW = { width: 740, height: 860 } as const; export const NATIVE_WIDGET_PICKER_WINDOW = { width: 820, height: 900 } as const;
export interface FloatingWidgetInstance { export interface FloatingWidgetInstance {
id: string; id: string;