fix(desktop): guard manual window drag coordinates
This commit is contained in:
@@ -0,0 +1,28 @@
|
|||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
|
||||||
|
import { readFiniteScreenPoint } from './desktop-window-drag-coordinates'
|
||||||
|
|
||||||
|
describe('readFiniteScreenPoint', () => {
|
||||||
|
it('returns finite screen coordinates', () => {
|
||||||
|
expect(readFiniteScreenPoint({ screenX: 1280, screenY: 720 })).toEqual({
|
||||||
|
screenX: 1280,
|
||||||
|
screenY: 720,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rejects NaN screen coordinates', () => {
|
||||||
|
expect(readFiniteScreenPoint({ screenX: Number.NaN, screenY: 720 })).toBeNull()
|
||||||
|
expect(readFiniteScreenPoint({ screenX: 1280, screenY: Number.NaN })).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rejects infinite screen coordinates', () => {
|
||||||
|
expect(readFiniteScreenPoint({ screenX: Number.POSITIVE_INFINITY, screenY: 720 })).toBeNull()
|
||||||
|
expect(readFiniteScreenPoint({ screenX: 1280, screenY: Number.NEGATIVE_INFINITY })).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rejects missing screen coordinates', () => {
|
||||||
|
expect(readFiniteScreenPoint(undefined)).toBeNull()
|
||||||
|
expect(readFiniteScreenPoint({ screenX: 1280 })).toBeNull()
|
||||||
|
expect(readFiniteScreenPoint({ screenY: 720 })).toBeNull()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
export type DesktopWindowScreenPoint = {
|
||||||
|
screenX: number
|
||||||
|
screenY: number
|
||||||
|
}
|
||||||
|
|
||||||
|
type ScreenPointInput =
|
||||||
|
| {
|
||||||
|
screenX?: unknown
|
||||||
|
screenY?: unknown
|
||||||
|
}
|
||||||
|
| null
|
||||||
|
| undefined
|
||||||
|
|
||||||
|
function isFiniteCoordinate(value: unknown): value is number {
|
||||||
|
return typeof value === 'number' && Number.isFinite(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readFiniteScreenPoint(input: ScreenPointInput): DesktopWindowScreenPoint | null {
|
||||||
|
if (!isFiniteCoordinate(input?.screenX) || !isFiniteCoordinate(input?.screenY)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
screenX: input.screenX,
|
||||||
|
screenY: input.screenY,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,20 +1,21 @@
|
|||||||
import { useEffect, useMemo, useRef, useState } from "react";
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { Platform, type PointerEvent as RNPointerEvent, type ViewProps } from "react-native";
|
import { Platform, type PointerEvent as RNPointerEvent, type ViewProps } from 'react-native'
|
||||||
import {
|
import {
|
||||||
getIsDesktopMac,
|
getIsDesktopMac,
|
||||||
DESKTOP_TRAFFIC_LIGHT_WIDTH,
|
DESKTOP_TRAFFIC_LIGHT_WIDTH,
|
||||||
DESKTOP_TRAFFIC_LIGHT_HEIGHT,
|
DESKTOP_TRAFFIC_LIGHT_HEIGHT,
|
||||||
} from "@/constants/layout";
|
} from '@/constants/layout'
|
||||||
import { getDesktopWindow } from "@/desktop/electron/window";
|
import { getDesktopWindow } from '@/desktop/electron/window'
|
||||||
import { isDesktop } from "@/desktop/host";
|
import { isDesktop } from '@/desktop/host'
|
||||||
|
import { readFiniteScreenPoint } from './desktop-window-drag-coordinates'
|
||||||
|
|
||||||
export async function toggleMaximize() {
|
export async function toggleMaximize() {
|
||||||
const win = getDesktopWindow();
|
const win = getDesktopWindow()
|
||||||
if (win && typeof win.toggleMaximize === "function") {
|
if (win && typeof win.toggleMaximize === 'function') {
|
||||||
try {
|
try {
|
||||||
await win.toggleMaximize();
|
await win.toggleMaximize()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn("[DesktopWindow] toggleMaximize failed", error);
|
console.warn('[DesktopWindow] toggleMaximize failed', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -27,147 +28,152 @@ export async function toggleMaximize() {
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
const INTERACTIVE_SELECTOR =
|
const INTERACTIVE_SELECTOR =
|
||||||
"button, a, input, textarea, select, " +
|
'button, a, input, textarea, select, ' +
|
||||||
"[role='button'], [role='link'], [role='textbox'], [role='combobox'], " +
|
"[role='button'], [role='link'], [role='textbox'], [role='combobox'], " +
|
||||||
"[role='tab'], [role='switch'], [role='checkbox'], [role='slider'], " +
|
"[role='tab'], [role='switch'], [role='checkbox'], [role='slider'], " +
|
||||||
"[role='menuitem'], [contenteditable='true']";
|
"[role='menuitem'], [contenteditable='true']"
|
||||||
|
|
||||||
const DOUBLE_CLICK_MS = 300;
|
const DOUBLE_CLICK_MS = 300
|
||||||
|
|
||||||
type DesktopDragViewProps = Pick<
|
type DesktopDragViewProps = Pick<
|
||||||
ViewProps,
|
ViewProps,
|
||||||
"onPointerDown" | "onPointerMove" | "onPointerUp" | "onPointerCancel"
|
'onPointerDown' | 'onPointerMove' | 'onPointerUp' | 'onPointerCancel'
|
||||||
>;
|
>
|
||||||
|
|
||||||
export function useDesktopDragHandlers(): DesktopDragViewProps {
|
export function useDesktopDragHandlers(): DesktopDragViewProps {
|
||||||
const isDragging = useRef(false);
|
const isDragging = useRef(false)
|
||||||
const lastPointerDownAt = useRef(0);
|
const lastPointerDownAt = useRef(0)
|
||||||
const isActive = Platform.OS === "web" && isDesktop();
|
const isActive = Platform.OS === 'web' && isDesktop()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isActive) return;
|
if (!isActive) return
|
||||||
function handleBlur() {
|
function handleBlur() {
|
||||||
if (!isDragging.current) return;
|
if (!isDragging.current) return
|
||||||
isDragging.current = false;
|
isDragging.current = false
|
||||||
getDesktopWindow()?.endMove?.();
|
getDesktopWindow()?.endMove?.()
|
||||||
}
|
}
|
||||||
window.addEventListener("blur", handleBlur);
|
window.addEventListener('blur', handleBlur)
|
||||||
return () => window.removeEventListener("blur", handleBlur);
|
return () => window.removeEventListener('blur', handleBlur)
|
||||||
}, [isActive]);
|
}, [isActive])
|
||||||
|
|
||||||
return useMemo((): DesktopDragViewProps => {
|
return useMemo((): DesktopDragViewProps => {
|
||||||
if (!isActive) return {};
|
if (!isActive) return {}
|
||||||
|
|
||||||
function stopDrag(e: RNPointerEvent) {
|
function stopDrag(e: RNPointerEvent) {
|
||||||
if (!isDragging.current) return;
|
if (!isDragging.current) return
|
||||||
isDragging.current = false;
|
isDragging.current = false
|
||||||
// On web, currentTarget is a DOM Element (typed as HostInstance in RN)
|
// On web, currentTarget is a DOM Element (typed as HostInstance in RN)
|
||||||
const el = e.currentTarget as unknown as Element | null;
|
const el = e.currentTarget as unknown as Element | null
|
||||||
if (el && "releasePointerCapture" in el) {
|
if (el && 'releasePointerCapture' in el) {
|
||||||
el.releasePointerCapture(e.nativeEvent.pointerId);
|
el.releasePointerCapture(e.nativeEvent.pointerId)
|
||||||
}
|
}
|
||||||
getDesktopWindow()?.endMove?.();
|
getDesktopWindow()?.endMove?.()
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
onPointerDown: (e: RNPointerEvent) => {
|
onPointerDown: (e: RNPointerEvent) => {
|
||||||
if (e.nativeEvent.button !== 0) return;
|
if (e.nativeEvent.button !== 0) return
|
||||||
|
|
||||||
// On web, e.target is a DOM Element (typed as HostInstance in RN)
|
// On web, e.target is a DOM Element (typed as HostInstance in RN)
|
||||||
const target = e.target as unknown as Element;
|
const target = e.target as unknown as Element
|
||||||
if (target.closest?.(INTERACTIVE_SELECTOR)) return;
|
if (target.closest?.(INTERACTIVE_SELECTOR)) return
|
||||||
|
|
||||||
e.preventDefault();
|
e.preventDefault()
|
||||||
|
|
||||||
const now = Date.now();
|
const now = Date.now()
|
||||||
if (now - lastPointerDownAt.current < DOUBLE_CLICK_MS) {
|
if (now - lastPointerDownAt.current < DOUBLE_CLICK_MS) {
|
||||||
lastPointerDownAt.current = 0;
|
lastPointerDownAt.current = 0
|
||||||
void toggleMaximize();
|
void toggleMaximize()
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
lastPointerDownAt.current = now;
|
lastPointerDownAt.current = now
|
||||||
|
|
||||||
const win = getDesktopWindow();
|
const win = getDesktopWindow()
|
||||||
if (!win?.startMove) return;
|
if (!win?.startMove) return
|
||||||
|
const screenPoint = readFiniteScreenPoint(e.nativeEvent)
|
||||||
|
if (!screenPoint) return
|
||||||
|
|
||||||
isDragging.current = true;
|
isDragging.current = true
|
||||||
const el = e.currentTarget as unknown as Element;
|
const el = e.currentTarget as unknown as Element
|
||||||
el.setPointerCapture(e.nativeEvent.pointerId);
|
el.setPointerCapture(e.nativeEvent.pointerId)
|
||||||
win.startMove(e.nativeEvent.screenX, e.nativeEvent.screenY);
|
win.startMove(screenPoint.screenX, screenPoint.screenY)
|
||||||
},
|
},
|
||||||
onPointerMove: (e: RNPointerEvent) => {
|
onPointerMove: (e: RNPointerEvent) => {
|
||||||
if (!isDragging.current) return;
|
if (!isDragging.current) return
|
||||||
getDesktopWindow()?.moving?.(e.nativeEvent.screenX, e.nativeEvent.screenY);
|
const screenPoint = readFiniteScreenPoint(e.nativeEvent)
|
||||||
|
if (!screenPoint) {
|
||||||
|
stopDrag(e)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
getDesktopWindow()?.moving?.(screenPoint.screenX, screenPoint.screenY)
|
||||||
},
|
},
|
||||||
onPointerUp: stopDrag,
|
onPointerUp: stopDrag,
|
||||||
onPointerCancel: stopDrag,
|
onPointerCancel: stopDrag,
|
||||||
};
|
}
|
||||||
}, [isActive]);
|
}, [isActive])
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useTrafficLightPadding(): { left: number; top: number } {
|
export function useTrafficLightPadding(): { left: number; top: number } {
|
||||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
const [isFullscreen, setIsFullscreen] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (Platform.OS !== "web" || !getIsDesktopMac()) return;
|
if (Platform.OS !== 'web' || !getIsDesktopMac()) return
|
||||||
|
|
||||||
let disposed = false;
|
let disposed = false
|
||||||
let cleanup: (() => void) | undefined;
|
let cleanup: (() => void) | undefined
|
||||||
let didCleanup = false;
|
let didCleanup = false
|
||||||
|
|
||||||
function runCleanup() {
|
function runCleanup() {
|
||||||
if (!cleanup || didCleanup) return;
|
if (!cleanup || didCleanup) return
|
||||||
didCleanup = true;
|
didCleanup = true
|
||||||
try {
|
try {
|
||||||
void Promise.resolve(cleanup()).catch((error) => {
|
void Promise.resolve(cleanup()).catch((error) => {
|
||||||
console.warn("[DesktopWindow] Failed to remove resize listener", error);
|
console.warn('[DesktopWindow] Failed to remove resize listener', error)
|
||||||
});
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn("[DesktopWindow] Failed to remove resize listener", error);
|
console.warn('[DesktopWindow] Failed to remove resize listener', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setup() {
|
async function setup() {
|
||||||
const win = getDesktopWindow();
|
const win = getDesktopWindow()
|
||||||
if (!win) return;
|
if (!win) return
|
||||||
|
|
||||||
const fullscreen =
|
const fullscreen = typeof win.isFullscreen === 'function' ? await win.isFullscreen() : false
|
||||||
typeof win.isFullscreen === "function" ? await win.isFullscreen() : false;
|
if (disposed) return
|
||||||
if (disposed) return;
|
setIsFullscreen(fullscreen)
|
||||||
setIsFullscreen(fullscreen);
|
|
||||||
|
|
||||||
if (typeof win.onResized !== "function") {
|
if (typeof win.onResized !== 'function') {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const unlisten = await win.onResized(async () => {
|
const unlisten = await win.onResized(async () => {
|
||||||
if (disposed) return;
|
if (disposed) return
|
||||||
const fs =
|
const fs = typeof win.isFullscreen === 'function' ? await win.isFullscreen() : false
|
||||||
typeof win.isFullscreen === "function" ? await win.isFullscreen() : false;
|
if (disposed) return
|
||||||
if (disposed) return;
|
setIsFullscreen(fs)
|
||||||
setIsFullscreen(fs);
|
})
|
||||||
});
|
|
||||||
|
|
||||||
cleanup = unlisten;
|
cleanup = unlisten
|
||||||
if (disposed) {
|
if (disposed) {
|
||||||
runCleanup();
|
runCleanup()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void setup();
|
void setup()
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
disposed = true;
|
disposed = true
|
||||||
runCleanup();
|
runCleanup()
|
||||||
};
|
}
|
||||||
}, []);
|
}, [])
|
||||||
|
|
||||||
if (!getIsDesktopMac() || isFullscreen) {
|
if (!getIsDesktopMac() || isFullscreen) {
|
||||||
return { left: 0, top: 0 };
|
return { left: 0, top: 0 }
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
left: DESKTOP_TRAFFIC_LIGHT_WIDTH,
|
left: DESKTOP_TRAFFIC_LIGHT_WIDTH,
|
||||||
top: DESKTOP_TRAFFIC_LIGHT_HEIGHT,
|
top: DESKTOP_TRAFFIC_LIGHT_HEIGHT,
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
|
||||||
|
import {
|
||||||
|
createWindowMoveState,
|
||||||
|
readWindowMovePayload,
|
||||||
|
resolveWindowMovePosition,
|
||||||
|
} from './window-drag-coordinates'
|
||||||
|
|
||||||
|
describe('window-drag-coordinates', () => {
|
||||||
|
describe('readWindowMovePayload', () => {
|
||||||
|
it('returns finite screen coordinates', () => {
|
||||||
|
expect(readWindowMovePayload({ screenX: 1500, screenY: 900 })).toEqual({
|
||||||
|
screenX: 1500,
|
||||||
|
screenY: 900,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rejects non-finite screen coordinates', () => {
|
||||||
|
expect(readWindowMovePayload({ screenX: Number.NaN, screenY: 900 })).toBeNull()
|
||||||
|
expect(readWindowMovePayload({ screenX: 1500, screenY: Number.POSITIVE_INFINITY })).toBeNull()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('createWindowMoveState', () => {
|
||||||
|
it('derives finite offsets from the window position', () => {
|
||||||
|
expect(
|
||||||
|
createWindowMoveState({
|
||||||
|
payload: { screenX: 1500, screenY: 900 },
|
||||||
|
windowX: 1200,
|
||||||
|
windowY: 700,
|
||||||
|
})
|
||||||
|
).toEqual({
|
||||||
|
offsetX: 300,
|
||||||
|
offsetY: 200,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rejects non-finite offsets', () => {
|
||||||
|
expect(
|
||||||
|
createWindowMoveState({
|
||||||
|
payload: { screenX: 1500, screenY: 900 },
|
||||||
|
windowX: Number.NaN,
|
||||||
|
windowY: 700,
|
||||||
|
})
|
||||||
|
).toBeNull()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('resolveWindowMovePosition', () => {
|
||||||
|
it('rounds the next window position', () => {
|
||||||
|
expect(
|
||||||
|
resolveWindowMovePosition({
|
||||||
|
payload: { screenX: 1501.4, screenY: 902.6 },
|
||||||
|
state: { offsetX: 300, offsetY: 200 },
|
||||||
|
})
|
||||||
|
).toEqual({
|
||||||
|
x: 1201,
|
||||||
|
y: 703,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rejects non-finite next positions', () => {
|
||||||
|
expect(
|
||||||
|
resolveWindowMovePosition({
|
||||||
|
payload: { screenX: 1500, screenY: 900 },
|
||||||
|
state: { offsetX: Number.NaN, offsetY: 200 },
|
||||||
|
})
|
||||||
|
).toBeNull()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
export type WindowMovePayload = {
|
||||||
|
screenX: number
|
||||||
|
screenY: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type WindowMoveState = {
|
||||||
|
offsetX: number
|
||||||
|
offsetY: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type WindowPosition = {
|
||||||
|
x: number
|
||||||
|
y: number
|
||||||
|
}
|
||||||
|
|
||||||
|
type WindowMovePayloadInput =
|
||||||
|
| {
|
||||||
|
screenX?: unknown
|
||||||
|
screenY?: unknown
|
||||||
|
}
|
||||||
|
| null
|
||||||
|
| undefined
|
||||||
|
|
||||||
|
function isFiniteCoordinate(value: unknown): value is number {
|
||||||
|
return typeof value === 'number' && Number.isFinite(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readWindowMovePayload(input: WindowMovePayloadInput): WindowMovePayload | null {
|
||||||
|
if (!isFiniteCoordinate(input?.screenX) || !isFiniteCoordinate(input?.screenY)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
screenX: input.screenX,
|
||||||
|
screenY: input.screenY,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createWindowMoveState(input: {
|
||||||
|
payload: WindowMovePayload
|
||||||
|
windowX: number
|
||||||
|
windowY: number
|
||||||
|
}): WindowMoveState | null {
|
||||||
|
const offsetX = input.payload.screenX - input.windowX
|
||||||
|
const offsetY = input.payload.screenY - input.windowY
|
||||||
|
if (!Number.isFinite(offsetX) || !Number.isFinite(offsetY)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return { offsetX, offsetY }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveWindowMovePosition(input: {
|
||||||
|
payload: WindowMovePayload
|
||||||
|
state: WindowMoveState
|
||||||
|
}): WindowPosition | null {
|
||||||
|
const x = Math.round(input.payload.screenX - input.state.offsetX)
|
||||||
|
const y = Math.round(input.payload.screenY - input.state.offsetY)
|
||||||
|
if (!Number.isFinite(x) || !Number.isFinite(y)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return { x, y }
|
||||||
|
}
|
||||||
@@ -1,81 +1,103 @@
|
|||||||
import { app, BrowserWindow, ipcMain } from "electron";
|
import { app, BrowserWindow, ipcMain } from 'electron'
|
||||||
|
import {
|
||||||
|
createWindowMoveState,
|
||||||
|
readWindowMovePayload,
|
||||||
|
resolveWindowMovePosition,
|
||||||
|
type WindowMoveState,
|
||||||
|
} from './window-drag-coordinates.js'
|
||||||
|
|
||||||
export function registerWindowManager(): void {
|
export function registerWindowManager(): void {
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Manual window dragging (replaces flaky CSS -webkit-app-region: drag).
|
// Manual window dragging (replaces flaky CSS -webkit-app-region: drag).
|
||||||
// State is keyed per window id to support multiple windows.
|
// State is keyed per window id to support multiple windows.
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
const moveStates = new Map<number, { offsetX: number; offsetY: number }>();
|
const moveStates = new Map<number, WindowMoveState>()
|
||||||
|
|
||||||
ipcMain.on(
|
ipcMain.on('paseo:window:startMove', (event, payload: { screenX: number; screenY: number }) => {
|
||||||
"paseo:window:startMove",
|
const win = BrowserWindow.fromWebContents(event.sender)
|
||||||
(event, payload: { screenX: number; screenY: number }) => {
|
if (!win) return
|
||||||
if (typeof payload?.screenX !== "number" || typeof payload?.screenY !== "number") return;
|
const movePayload = readWindowMovePayload(payload)
|
||||||
const win = BrowserWindow.fromWebContents(event.sender);
|
if (!movePayload) {
|
||||||
if (!win) return;
|
moveStates.delete(win.id)
|
||||||
const [winX, winY] = win.getPosition();
|
return
|
||||||
moveStates.set(win.id, { offsetX: payload.screenX - winX, offsetY: payload.screenY - winY });
|
|
||||||
}
|
}
|
||||||
);
|
const [winX, winY] = win.getPosition()
|
||||||
|
const moveState = createWindowMoveState({
|
||||||
ipcMain.on(
|
payload: movePayload,
|
||||||
"paseo:window:moving",
|
windowX: winX,
|
||||||
(event, payload: { screenX: number; screenY: number }) => {
|
windowY: winY,
|
||||||
if (typeof payload?.screenX !== "number" || typeof payload?.screenY !== "number") return;
|
})
|
||||||
const win = BrowserWindow.fromWebContents(event.sender);
|
if (!moveState) {
|
||||||
if (!win) return;
|
moveStates.delete(win.id)
|
||||||
const state = moveStates.get(win.id);
|
return
|
||||||
if (!state) return;
|
|
||||||
win.setPosition(
|
|
||||||
Math.round(payload.screenX - state.offsetX),
|
|
||||||
Math.round(payload.screenY - state.offsetY)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
);
|
moveStates.set(win.id, moveState)
|
||||||
|
})
|
||||||
|
|
||||||
ipcMain.on("paseo:window:endMove", (event) => {
|
ipcMain.on('paseo:window:moving', (event, payload: { screenX: number; screenY: number }) => {
|
||||||
const win = BrowserWindow.fromWebContents(event.sender);
|
const win = BrowserWindow.fromWebContents(event.sender)
|
||||||
if (win) moveStates.delete(win.id);
|
if (!win) return
|
||||||
});
|
const state = moveStates.get(win.id)
|
||||||
|
if (!state) return
|
||||||
|
const movePayload = readWindowMovePayload(payload)
|
||||||
|
if (!movePayload) {
|
||||||
|
moveStates.delete(win.id)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const nextPosition = resolveWindowMovePosition({
|
||||||
|
payload: movePayload,
|
||||||
|
state,
|
||||||
|
})
|
||||||
|
if (!nextPosition) {
|
||||||
|
moveStates.delete(win.id)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
win.setPosition(nextPosition.x, nextPosition.y)
|
||||||
|
})
|
||||||
|
|
||||||
app.on("browser-window-created", (_event, win) => {
|
ipcMain.on('paseo:window:endMove', (event) => {
|
||||||
win.on("closed", () => moveStates.delete(win.id));
|
const win = BrowserWindow.fromWebContents(event.sender)
|
||||||
});
|
if (win) moveStates.delete(win.id)
|
||||||
|
})
|
||||||
|
|
||||||
ipcMain.handle("paseo:window:toggleMaximize", (event) => {
|
app.on('browser-window-created', (_event, win) => {
|
||||||
const win = BrowserWindow.fromWebContents(event.sender);
|
win.on('closed', () => moveStates.delete(win.id))
|
||||||
if (!win) return;
|
})
|
||||||
|
|
||||||
|
ipcMain.handle('paseo:window:toggleMaximize', (event) => {
|
||||||
|
const win = BrowserWindow.fromWebContents(event.sender)
|
||||||
|
if (!win) return
|
||||||
if (win.isMaximized()) {
|
if (win.isMaximized()) {
|
||||||
win.unmaximize();
|
win.unmaximize()
|
||||||
} else {
|
} else {
|
||||||
win.maximize();
|
win.maximize()
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
ipcMain.handle("paseo:window:isFullscreen", (event) => {
|
ipcMain.handle('paseo:window:isFullscreen', (event) => {
|
||||||
const win = BrowserWindow.fromWebContents(event.sender);
|
const win = BrowserWindow.fromWebContents(event.sender)
|
||||||
return win?.isFullScreen() ?? false;
|
return win?.isFullScreen() ?? false
|
||||||
});
|
})
|
||||||
|
|
||||||
ipcMain.handle("paseo:window:setBadgeCount", (_event, count?: number) => {
|
ipcMain.handle('paseo:window:setBadgeCount', (_event, count?: number) => {
|
||||||
if (process.platform === "darwin" || process.platform === "linux") {
|
if (process.platform === 'darwin' || process.platform === 'linux') {
|
||||||
app.setBadgeCount(count ?? 0);
|
app.setBadgeCount(count ?? 0)
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setupWindowResizeEvents(win: BrowserWindow): void {
|
export function setupWindowResizeEvents(win: BrowserWindow): void {
|
||||||
win.on("resize", () => {
|
win.on('resize', () => {
|
||||||
win.webContents.send("paseo:window:resized", {});
|
win.webContents.send('paseo:window:resized', {})
|
||||||
});
|
})
|
||||||
|
|
||||||
win.on("enter-full-screen", () => {
|
win.on('enter-full-screen', () => {
|
||||||
win.webContents.send("paseo:window:resized", {});
|
win.webContents.send('paseo:window:resized', {})
|
||||||
});
|
})
|
||||||
|
|
||||||
win.on("leave-full-screen", () => {
|
win.on('leave-full-screen', () => {
|
||||||
win.webContents.send("paseo:window:resized", {});
|
win.webContents.send('paseo:window:resized', {})
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -83,11 +105,11 @@ export function setupWindowResizeEvents(win: BrowserWindow): void {
|
|||||||
* The renderer handles drag-drop via standard HTML5 APIs instead.
|
* The renderer handles drag-drop via standard HTML5 APIs instead.
|
||||||
*/
|
*/
|
||||||
export function setupDragDropPrevention(win: BrowserWindow): void {
|
export function setupDragDropPrevention(win: BrowserWindow): void {
|
||||||
win.webContents.on("will-navigate", (event, url) => {
|
win.webContents.on('will-navigate', (event, url) => {
|
||||||
// Allow normal navigation (e.g. dev server hot-reload) but block file:// URLs
|
// Allow normal navigation (e.g. dev server hot-reload) but block file:// URLs
|
||||||
// that result from dropping files onto the window.
|
// that result from dropping files onto the window.
|
||||||
if (url.startsWith("file://")) {
|
if (url.startsWith('file://')) {
|
||||||
event.preventDefault();
|
event.preventDefault()
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user