fix(desktop): 위젯 picker를 웹 AppPopup(720×860)과 동일하게
현재 창 innerWidth로 크기를 제한하던 버그를 수정하고, nativeCompact 대신 서버와 같은 AppPopup UI를 macOS·Windows 공통 적용합니다. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user