mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-31 16:33:34 +08:00
Dragging clamps against the mascot alone, so the mascot can reach a display edge through the window's transparent padding. At the top edge that means asking for a negative window y on purpose -- and the activity panel lives in exactly the padding that goes off-screen with it. Measured against the shipped layout: of a 96px panel, 78px ends up above the work area, leaving an 18px sliver under the menu bar. This is not a regression in 8f3a2f092; it is that fix's other half. The mascot reaching the menu bar and the panel following it off-screen are the same negative y. So the panel changes sides instead. The main process is the only side that knows the window position and the work area, so it decides and the renderer follows, the way the Codex overlay does it. Three things that are load-bearing: - The test is placement-independent -- panel height against the room above the mascot -- because the flip frees the very space a "does it still fit above?" test would measure next, and would then flip back once per frame. A 24px hysteresis covers the boundary. - Flipping moves the mascot inside the window, so the window moves the opposite way to hold it still on screen. Mid-drag that has to rebase the drag's window origin too, or the next tick recomputes the pre-flip position. A restore needs the same treatment: a saved y belongs to the mascot offset it was saved with, and the renderer always starts the panel above, so restoring the bare window position would drop the mascot by the panel's height on the next launch. - The renderer only sends drag start and end -- the cursor sampler in this process drives everything between -- so a flip decided mid-drag has no reply to ride back on and goes out as an event. The panel box is the union of every reported region past the mascot, which keeps the IPC payload shape unchanged. Left and right are deliberately untouched. The panel is 352px wide in a 384px window, so it can only slide +/-16px before the window itself clips it, while reaching a side edge needs about 120px. Those need the window to grow or move, which is a different change. Falsified each layer by reverting it: the placement test, the window compensation, the drag rebase, and the restore anchor each turn their own case red.
89 lines
3.8 KiB
TypeScript
89 lines
3.8 KiB
TypeScript
import { contextBridge, ipcRenderer } from 'electron'
|
|
import {
|
|
isElectronIpcChannelAllowedForPetWindow,
|
|
validateElectronIpcPayload,
|
|
} from './ipc/capabilities'
|
|
import { ELECTRON_IPC_CHANNELS, type ElectronIpcChannel } from './ipc/channels'
|
|
import { ELECTRON_EVENT_CHANNELS } from './ipc/channels'
|
|
import type { DesktopHost, DesktopPetPanelPlacement } from '../src/lib/desktopHost/types'
|
|
import type { Locale } from '../src/i18n/locale'
|
|
|
|
function invoke<T>(channel: ElectronIpcChannel, payload?: unknown): Promise<T> {
|
|
if (!isElectronIpcChannelAllowedForPetWindow(channel)) {
|
|
return Promise.reject(new Error(`Electron IPC channel ${channel} is not available to the pet window`))
|
|
}
|
|
if (!validateElectronIpcPayload(channel, payload)) {
|
|
return Promise.reject(new Error(`Invalid Electron IPC payload for ${channel}`))
|
|
}
|
|
return ipcRenderer.invoke(channel, payload) as Promise<T>
|
|
}
|
|
|
|
const petHost = {
|
|
kind: 'electron',
|
|
isDesktop: true,
|
|
capabilities: {
|
|
appMode: false,
|
|
clipboard: false,
|
|
dialogs: false,
|
|
notifications: false,
|
|
previewWebview: false,
|
|
shell: false,
|
|
terminal: false,
|
|
updates: false,
|
|
windowControls: false,
|
|
zoom: false,
|
|
},
|
|
runtime: {
|
|
getServerUrl: () => invoke<string>(ELECTRON_IPC_CHANNELS.runtimeGetServerUrl),
|
|
// Keep the shared renderer bootstrap contract while returning only the
|
|
// server-enforced companion capability, never the desktop master token.
|
|
getLocalAccessToken: () => invoke<string | null>(ELECTRON_IPC_CHANNELS.runtimeGetPetAccessToken),
|
|
},
|
|
app: {
|
|
getLocalePreference: () =>
|
|
invoke<Locale | null>(ELECTRON_IPC_CHANNELS.appGetLocalePreference),
|
|
getPreferredSystemLanguages: () =>
|
|
invoke<string[]>(ELECTRON_IPC_CHANNELS.appGetPreferredSystemLanguages),
|
|
onLocaleChanged: (handler: (locale: Locale) => void) => {
|
|
const listener = (_event: Electron.IpcRendererEvent, locale: Locale) => handler(locale)
|
|
ipcRenderer.on(ELECTRON_EVENT_CHANNELS.appLocaleChanged, listener)
|
|
return Promise.resolve(() => {
|
|
ipcRenderer.removeListener(ELECTRON_EVENT_CHANNELS.appLocaleChanged, listener)
|
|
})
|
|
},
|
|
},
|
|
appearance: {
|
|
// The pet window is transparent and shares the renderer bootstrap, which
|
|
// reports the applied theme. It has no native chrome to sync, and it must
|
|
// not repaint the main window, so this stays a no-op.
|
|
setApplied: () => Promise.resolve(),
|
|
},
|
|
pets: {
|
|
list: () => invoke(ELECTRON_IPC_CHANNELS.petsList),
|
|
hide: () => invoke<void>(ELECTRON_IPC_CHANNELS.petsHide),
|
|
showContextMenu: (closeLabel: string) =>
|
|
invoke<boolean>(ELECTRON_IPC_CHANNELS.petsShowContextMenu, { closeLabel }),
|
|
dragWindow: (payload: { phase: 'start' | 'move' | 'end', x: number, y: number }) =>
|
|
invoke<DesktopPetPanelPlacement>(ELECTRON_IPC_CHANNELS.petsDragWindow, payload),
|
|
setIgnoreMouseEvents: (ignore: boolean) =>
|
|
invoke<void>(ELECTRON_IPC_CHANNELS.petsSetIgnoreMouseEvents, ignore),
|
|
setInteractiveRegions: (regions: Array<{ x: number, y: number, width: number, height: number }>) =>
|
|
invoke<DesktopPetPanelPlacement>(ELECTRON_IPC_CHANNELS.petsSetInteractiveRegions, regions),
|
|
onPanelPlacementChanged: (handler: (placement: DesktopPetPanelPlacement) => void) => {
|
|
const listener = (
|
|
_event: Electron.IpcRendererEvent,
|
|
placement: DesktopPetPanelPlacement,
|
|
) => handler(placement)
|
|
ipcRenderer.on(ELECTRON_EVENT_CHANNELS.petPanelPlacementChanged, listener)
|
|
return Promise.resolve(() => {
|
|
ipcRenderer.removeListener(ELECTRON_EVENT_CHANNELS.petPanelPlacementChanged, listener)
|
|
})
|
|
},
|
|
focusMainWindow: () => invoke<void>(ELECTRON_IPC_CHANNELS.petsFocusMainWindow),
|
|
focusSession: (sessionId: string) =>
|
|
invoke<void>(ELECTRON_IPC_CHANNELS.petsFocusSession, sessionId),
|
|
},
|
|
} as unknown as DesktopHost
|
|
|
|
contextBridge.exposeInMainWorld('desktopHost', petHost)
|