mirror of
https://github.com/anomalyco/opencode.git
synced 2026-05-24 21:34:50 +00:00
feat(desktop): add pinch zoom setting (#28632)
This commit is contained in:
@@ -7,4 +7,5 @@ export const CHANNEL: Channel = raw === "dev" || raw === "beta" || raw === "prod
|
||||
export const SETTINGS_STORE = "opencode.settings"
|
||||
export const DEFAULT_SERVER_URL_KEY = "defaultServerUrl"
|
||||
export const WSL_ENABLED_KEY = "wslEnabled"
|
||||
export const PINCH_ZOOM_ENABLED_KEY = "pinchZoomEnabled"
|
||||
export const UPDATER_ENABLED = app.isPackaged && CHANNEL !== "dev"
|
||||
|
||||
@@ -14,7 +14,7 @@ import type {
|
||||
} from "../preload/types"
|
||||
import { runDesktopMenuAction } from "./desktop-menu-actions"
|
||||
import { getStore } from "./store"
|
||||
import { setTitlebar, updateTitlebar } from "./windows"
|
||||
import { getPinchZoomEnabled, setPinchZoomEnabled, setTitlebar, updateTitlebar } from "./windows"
|
||||
|
||||
const pickerFilters = (ext?: string[]) => {
|
||||
if (!ext || ext.length === 0) return undefined
|
||||
@@ -202,6 +202,10 @@ export function registerIpcHandlers(deps: Deps) {
|
||||
if (!win) return
|
||||
updateTitlebar(win)
|
||||
})
|
||||
ipcMain.handle("get-pinch-zoom-enabled", () => getPinchZoomEnabled())
|
||||
ipcMain.handle("set-pinch-zoom-enabled", (_event: IpcMainInvokeEvent, enabled: boolean) => {
|
||||
setPinchZoomEnabled(enabled)
|
||||
})
|
||||
ipcMain.handle("set-titlebar", (event: IpcMainInvokeEvent, theme: TitlebarTheme) => {
|
||||
const win = BrowserWindow.fromWebContents(event.sender)
|
||||
if (!win) return
|
||||
|
||||
@@ -3,7 +3,9 @@ import { app, BrowserWindow, dialog, net, nativeImage, nativeTheme, protocol } f
|
||||
import { dirname, isAbsolute, join, relative, resolve } from "node:path"
|
||||
import { fileURLToPath, pathToFileURL } from "node:url"
|
||||
import type { TitlebarTheme } from "../preload/types"
|
||||
import { PINCH_ZOOM_ENABLED_KEY } from "./constants"
|
||||
import { exportDebugLogs, write as writeLog } from "./logging"
|
||||
import { getStore } from "./store"
|
||||
import { createUnresponsiveSampler } from "./unresponsive"
|
||||
|
||||
const root = dirname(fileURLToPath(import.meta.url))
|
||||
@@ -33,7 +35,10 @@ let relaunchHandler = () => {
|
||||
app.exit(0)
|
||||
}
|
||||
const titlebarThemes = new WeakMap<BrowserWindow, Partial<TitlebarTheme>>()
|
||||
const pinchZoomEnabled = new WeakMap<BrowserWindow, boolean>()
|
||||
const titlebarHeight = 40
|
||||
const maxZoomLevel = 10
|
||||
const minZoomLevel = 0.2
|
||||
|
||||
export function setRelaunchHandler(handler: () => void) {
|
||||
relaunchHandler = handler
|
||||
@@ -79,6 +84,20 @@ export function updateTitlebar(win: BrowserWindow) {
|
||||
win.setTitleBarOverlay(overlay(titlebarThemes.get(win), win.webContents.getZoomFactor()))
|
||||
}
|
||||
|
||||
export function setPinchZoomEnabled(enabled: boolean) {
|
||||
getStore().set(PINCH_ZOOM_ENABLED_KEY, enabled)
|
||||
for (const win of BrowserWindow.getAllWindows()) {
|
||||
pinchZoomEnabled.set(win, enabled)
|
||||
win.webContents.send("pinch-zoom-enabled-changed", enabled)
|
||||
if (!enabled && win.webContents.getZoomFactor() !== 1) win.webContents.setZoomFactor(1)
|
||||
updateZoom(win)
|
||||
}
|
||||
}
|
||||
|
||||
export function getPinchZoomEnabled() {
|
||||
return getStore().get(PINCH_ZOOM_ENABLED_KEY) === true
|
||||
}
|
||||
|
||||
export function setDockIcon() {
|
||||
if (process.platform !== "darwin") return
|
||||
const icon = nativeImage.createFromPath(join(iconsDir(), "dock.png"))
|
||||
@@ -392,13 +411,31 @@ function isRendererUrl(value?: string, html = false) {
|
||||
}
|
||||
|
||||
function wireZoom(win: BrowserWindow) {
|
||||
pinchZoomEnabled.set(win, getPinchZoomEnabled())
|
||||
win.webContents.setZoomFactor(1)
|
||||
win.webContents.on("zoom-changed", () => {
|
||||
win.webContents.setZoomFactor(1)
|
||||
updateTitlebar(win)
|
||||
win.webContents.on("zoom-changed", (event, zoomDirection) => {
|
||||
event.preventDefault()
|
||||
if (pinchZoomEnabled.get(win)) {
|
||||
win.webContents.setZoomFactor(
|
||||
clampZoom(win.webContents.getZoomFactor() + (zoomDirection === "in" ? 0.2 : -0.2)),
|
||||
)
|
||||
updateZoom(win)
|
||||
return
|
||||
}
|
||||
if (win.webContents.getZoomFactor() !== 1) win.webContents.setZoomFactor(1)
|
||||
updateZoom(win)
|
||||
})
|
||||
}
|
||||
|
||||
function clampZoom(value: number) {
|
||||
return Math.min(Math.max(value, minZoomLevel), maxZoomLevel)
|
||||
}
|
||||
|
||||
function updateZoom(win: BrowserWindow) {
|
||||
updateTitlebar(win)
|
||||
win.webContents.send("zoom-factor-changed", win.webContents.getZoomFactor())
|
||||
}
|
||||
|
||||
function upsertKeyValue(obj: Record<string, any>, keyToChange: string, value: any) {
|
||||
const keyToChangeLower = keyToChange.toLowerCase()
|
||||
for (const key of Object.keys(obj)) {
|
||||
|
||||
@@ -60,6 +60,18 @@ const api: ElectronAPI = {
|
||||
relaunch: () => ipcRenderer.send("relaunch"),
|
||||
getZoomFactor: () => ipcRenderer.invoke("get-zoom-factor"),
|
||||
setZoomFactor: (factor) => ipcRenderer.invoke("set-zoom-factor", factor),
|
||||
getPinchZoomEnabled: () => ipcRenderer.invoke("get-pinch-zoom-enabled"),
|
||||
setPinchZoomEnabled: (enabled) => ipcRenderer.invoke("set-pinch-zoom-enabled", enabled),
|
||||
onPinchZoomEnabledChanged: (cb) => {
|
||||
const handler = (_: unknown, enabled: boolean) => cb(enabled)
|
||||
ipcRenderer.on("pinch-zoom-enabled-changed", handler)
|
||||
return () => ipcRenderer.removeListener("pinch-zoom-enabled-changed", handler)
|
||||
},
|
||||
onZoomFactorChanged: (cb) => {
|
||||
const handler = (_: unknown, factor: number) => cb(factor)
|
||||
ipcRenderer.on("zoom-factor-changed", handler)
|
||||
return () => ipcRenderer.removeListener("zoom-factor-changed", handler)
|
||||
},
|
||||
setTitlebar: (theme) => ipcRenderer.invoke("set-titlebar", theme),
|
||||
runDesktopMenuAction: (action) => ipcRenderer.invoke("run-desktop-menu-action", action),
|
||||
loadingWindowComplete: () => ipcRenderer.send("loading-window-complete"),
|
||||
|
||||
@@ -79,6 +79,10 @@ export type ElectronAPI = {
|
||||
relaunch: () => void
|
||||
getZoomFactor: () => Promise<number>
|
||||
setZoomFactor: (factor: number) => Promise<void>
|
||||
getPinchZoomEnabled: () => Promise<boolean>
|
||||
setPinchZoomEnabled: (enabled: boolean) => Promise<void>
|
||||
onPinchZoomEnabledChanged: (cb: (enabled: boolean) => void) => () => void
|
||||
onZoomFactorChanged: (cb: (factor: number) => void) => () => void
|
||||
setTitlebar: (theme: TitlebarTheme) => Promise<void>
|
||||
runDesktopMenuAction: (action: DesktopMenuAction) => Promise<void>
|
||||
loadingWindowComplete: () => void
|
||||
|
||||
@@ -21,7 +21,7 @@ import { createEffect, createResource, onCleanup, onMount, Show } from "solid-js
|
||||
import { render } from "solid-js/web"
|
||||
import pkg from "../../package.json"
|
||||
import { initI18n, t } from "./i18n"
|
||||
import { resetZoom, webviewZoom, zoomIn, zoomOut } from "./webview-zoom"
|
||||
import { resetZoom, setPinchZoomEnabled, webviewZoom, zoomIn, zoomOut } from "./webview-zoom"
|
||||
import "./styles.css"
|
||||
import { useTheme } from "@opencode-ai/ui/theme"
|
||||
|
||||
@@ -274,6 +274,10 @@ const createPlatform = (): Platform => {
|
||||
|
||||
webviewZoom,
|
||||
|
||||
getPinchZoomEnabled: () => window.api.getPinchZoomEnabled(),
|
||||
|
||||
setPinchZoomEnabled,
|
||||
|
||||
runDesktopMenuAction,
|
||||
|
||||
checkAppExists: async (appName: string) => {
|
||||
|
||||
@@ -13,9 +13,21 @@ const OS_NAME = (() => {
|
||||
|
||||
const [webviewZoom, setWebviewZoom] = createSignal(1)
|
||||
let requestedZoom = 1
|
||||
let pinchZoomEnabled = false
|
||||
let wheelPinch = undefined as
|
||||
| {
|
||||
active: boolean
|
||||
startZoom: number
|
||||
totalDelta: number
|
||||
timeout: ReturnType<typeof setTimeout> | undefined
|
||||
}
|
||||
| undefined
|
||||
|
||||
const MAX_ZOOM_LEVEL = 10
|
||||
const MIN_ZOOM_LEVEL = 0.2
|
||||
const WHEEL_PINCH_THRESHOLD = 20
|
||||
const WHEEL_PINCH_STEP = 0.2
|
||||
const WHEEL_PINCH_END_DELAY = 160
|
||||
|
||||
const clamp = (value: number) => Math.min(Math.max(value, MIN_ZOOM_LEVEL), MAX_ZOOM_LEVEL)
|
||||
|
||||
@@ -33,10 +45,77 @@ const applyZoom = (next: number) => {
|
||||
})
|
||||
}
|
||||
|
||||
window.api.onZoomFactorChanged((factor) => {
|
||||
requestedZoom = clamp(factor)
|
||||
setWebviewZoom(requestedZoom)
|
||||
})
|
||||
|
||||
void window.api.getPinchZoomEnabled().then((enabled) => {
|
||||
pinchZoomEnabled = enabled
|
||||
})
|
||||
|
||||
window.api.onPinchZoomEnabledChanged((enabled) => {
|
||||
pinchZoomEnabled = enabled
|
||||
resetWheelPinch()
|
||||
})
|
||||
|
||||
const setPinchZoomEnabled = (enabled: boolean) => {
|
||||
pinchZoomEnabled = enabled
|
||||
resetWheelPinch()
|
||||
return window.api.setPinchZoomEnabled(enabled)
|
||||
}
|
||||
|
||||
const resetZoom = () => applyZoom(1)
|
||||
const zoomIn = () => applyZoom(clamp(requestedZoom + 0.2))
|
||||
const zoomOut = () => applyZoom(clamp(requestedZoom - 0.2))
|
||||
|
||||
const resetWheelPinch = () => {
|
||||
clearTimeout(wheelPinch?.timeout)
|
||||
wheelPinch = undefined
|
||||
}
|
||||
|
||||
const normalizeWheelDelta = (event: WheelEvent) => {
|
||||
if (event.deltaMode === WheelEvent.DOM_DELTA_LINE) return event.deltaY * 16
|
||||
if (event.deltaMode === WheelEvent.DOM_DELTA_PAGE) return event.deltaY * window.innerHeight
|
||||
return event.deltaY
|
||||
}
|
||||
|
||||
const updateWheelPinch = (event: WheelEvent) => {
|
||||
wheelPinch ??= {
|
||||
active: false,
|
||||
startZoom: requestedZoom,
|
||||
totalDelta: 0,
|
||||
timeout: undefined,
|
||||
}
|
||||
|
||||
clearTimeout(wheelPinch.timeout)
|
||||
wheelPinch.timeout = setTimeout(resetWheelPinch, WHEEL_PINCH_END_DELAY)
|
||||
wheelPinch.totalDelta += normalizeWheelDelta(event)
|
||||
|
||||
if (!wheelPinch.active && Math.abs(wheelPinch.totalDelta) < WHEEL_PINCH_THRESHOLD) return
|
||||
if (!wheelPinch.active) {
|
||||
wheelPinch.active = true
|
||||
wheelPinch.startZoom = requestedZoom
|
||||
wheelPinch.totalDelta = 0
|
||||
return
|
||||
}
|
||||
|
||||
wheelPinch.active = true
|
||||
applyZoom(clamp(wheelPinch.startZoom - (wheelPinch.totalDelta / WHEEL_PINCH_THRESHOLD) * WHEEL_PINCH_STEP))
|
||||
}
|
||||
|
||||
window.addEventListener(
|
||||
"wheel",
|
||||
(event) => {
|
||||
if (!pinchZoomEnabled) return
|
||||
if (!event.ctrlKey) return
|
||||
|
||||
event.preventDefault()
|
||||
updateWheelPinch(event)
|
||||
},
|
||||
{ passive: false },
|
||||
)
|
||||
|
||||
window.addEventListener("keydown", (event) => {
|
||||
if (!(OS_NAME === "macos" ? event.metaKey : event.ctrlKey)) return
|
||||
|
||||
@@ -56,4 +135,4 @@ window.addEventListener("keydown", (event) => {
|
||||
}
|
||||
})
|
||||
|
||||
export { webviewZoom, resetZoom, zoomIn, zoomOut }
|
||||
export { webviewZoom, resetZoom, setPinchZoomEnabled, zoomIn, zoomOut }
|
||||
|
||||
Reference in New Issue
Block a user