152 lines
5.5 KiB
TypeScript
152 lines
5.5 KiB
TypeScript
// IPC surface for the pop-out pet overlay (mascot window). Extracted from
|
|
// main.ts; window handles stay injected because main.ts owns their lifecycle.
|
|
import { type BrowserWindow, ipcMain } from 'electron'
|
|
|
|
export interface PetOverlayIpcDeps {
|
|
getMainWindow: () => BrowserWindow | null
|
|
getPetOverlayWindow: () => BrowserWindow | null
|
|
openPetOverlay: (bounds: unknown) => void
|
|
closePetOverlay: () => void
|
|
}
|
|
|
|
export function registerPetOverlayIpc({
|
|
getMainWindow,
|
|
getPetOverlayWindow,
|
|
openPetOverlay,
|
|
closePetOverlay
|
|
}: PetOverlayIpcDeps) {
|
|
// `request` is `{ bounds, screen }`. A fresh pop-out passes viewport-space
|
|
// bounds (screen=false): convert to screen space by adding the main window's
|
|
// content origin so the pet lands where it sat in-window. A remembered/dragged
|
|
// spot passes screen-space bounds (screen=true) and is used as-is. We return the
|
|
// resolved screen bounds so the renderer can persist exactly where it opened.
|
|
ipcMain.handle('hermes:pet-overlay:open', async (_event, request) => {
|
|
const bounds = request && request.bounds ? request.bounds : request
|
|
const isScreen = Boolean(request && request.screen)
|
|
const mainWindow = getMainWindow()
|
|
let screenBounds = bounds
|
|
|
|
try {
|
|
if (bounds && !isScreen && mainWindow && !mainWindow.isDestroyed()) {
|
|
const content = mainWindow.getContentBounds()
|
|
screenBounds = {
|
|
x: content.x + (bounds.x || 0),
|
|
y: content.y + (bounds.y || 0),
|
|
width: bounds.width,
|
|
height: bounds.height
|
|
}
|
|
}
|
|
} catch {
|
|
// Fall back to raw bounds if the window geometry is unavailable.
|
|
}
|
|
|
|
openPetOverlay(screenBounds)
|
|
|
|
return { ok: true, bounds: screenBounds }
|
|
})
|
|
ipcMain.handle('hermes:pet-overlay:close', async () => {
|
|
closePetOverlay()
|
|
|
|
return { ok: true }
|
|
})
|
|
// Drag/resize: the overlay reports new absolute screen bounds (it already knows
|
|
// the pointer's screen coords). Drag keeps the size constant; the wheel-to-scale
|
|
// gesture grows/shrinks it so the sprite is never cropped by the window edge.
|
|
// The window is created non-resizable (no stray edge-drag on the transparent
|
|
// frameless panel), which on Windows/Linux also blocks programmatic setBounds
|
|
// sizing — so briefly flip resizable on whenever the size actually changes.
|
|
ipcMain.on('hermes:pet-overlay:set-bounds', (_event, bounds) => {
|
|
const petOverlayWindow = getPetOverlayWindow()
|
|
|
|
if (!petOverlayWindow || petOverlayWindow.isDestroyed() || !bounds) {
|
|
return
|
|
}
|
|
|
|
const win = petOverlayWindow
|
|
const width = Math.max(80, Math.round(bounds.width))
|
|
const height = Math.max(80, Math.round(bounds.height))
|
|
const [curW, curH] = win.getSize()
|
|
const resizing = width !== curW || height !== curH
|
|
|
|
if (resizing && !win.isResizable()) {
|
|
win.setResizable(true)
|
|
}
|
|
|
|
win.setBounds({ x: Math.round(bounds.x), y: Math.round(bounds.y), width, height })
|
|
|
|
if (resizing) {
|
|
win.setResizable(false)
|
|
}
|
|
})
|
|
// Click-through: the overlay window is a full rectangle but only the pet pixels
|
|
// should be interactive. The renderer toggles this as the cursor enters/leaves
|
|
// the sprite so transparent margins pass clicks to whatever is behind.
|
|
ipcMain.on('hermes:pet-overlay:ignore-mouse', (_event, ignore) => {
|
|
const petOverlayWindow = getPetOverlayWindow()
|
|
|
|
if (petOverlayWindow && !petOverlayWindow.isDestroyed()) {
|
|
petOverlayWindow.setIgnoreMouseEvents(Boolean(ignore), { forward: true })
|
|
}
|
|
})
|
|
// The overlay is a non-activating panel (focusable:false) so it never steals
|
|
// the app's cmd/alt-tab anchor from the main window. But the pop-up composer
|
|
// needs the keyboard, so the renderer asks us to flip it focusable + focus it
|
|
// while the composer is open, then back to non-activating when it closes.
|
|
ipcMain.on('hermes:pet-overlay:set-focusable', (_event, focusable) => {
|
|
const petOverlayWindow = getPetOverlayWindow()
|
|
|
|
if (!petOverlayWindow || petOverlayWindow.isDestroyed()) {
|
|
return
|
|
}
|
|
|
|
petOverlayWindow.setFocusable(Boolean(focusable))
|
|
|
|
if (focusable) {
|
|
petOverlayWindow.focus()
|
|
}
|
|
})
|
|
// Main renderer → overlay: forward the latest pet state for the overlay to render.
|
|
ipcMain.on('hermes:pet-overlay:state', (_event, payload) => {
|
|
const petOverlayWindow = getPetOverlayWindow()
|
|
|
|
if (petOverlayWindow && !petOverlayWindow.isDestroyed()) {
|
|
petOverlayWindow.webContents.send('hermes:pet-overlay:state', payload)
|
|
}
|
|
})
|
|
// Overlay → main renderer: control messages (pop back in, composer submit).
|
|
ipcMain.on('hermes:pet-overlay:control', (_event, payload) => {
|
|
const mainWindow = getMainWindow()
|
|
|
|
if (!mainWindow || mainWindow.isDestroyed()) {
|
|
return
|
|
}
|
|
|
|
// Double-click toggles the app window: hide it away if it's up front, bring it
|
|
// back if it's minimized/buried. Pure window control — nothing for the
|
|
// renderer to do, so don't forward it.
|
|
if (payload && payload.type === 'toggle-app') {
|
|
if (mainWindow.isMinimized() || !mainWindow.isVisible()) {
|
|
mainWindow.show()
|
|
mainWindow.focus()
|
|
} else {
|
|
mainWindow.minimize()
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// The mail icon means "take me to the app": raise the main window (it may be
|
|
// minimized or buried) before the renderer navigates to the latest thread.
|
|
if (payload && payload.type === 'open-app') {
|
|
if (mainWindow.isMinimized()) {
|
|
mainWindow.restore()
|
|
}
|
|
|
|
mainWindow.show()
|
|
mainWindow.focus()
|
|
}
|
|
|
|
mainWindow.webContents.send('hermes:pet-overlay:control', payload)
|
|
})
|
|
}
|