feat(desktop): add UI zoom scaling
Add zoom controls to Theme settings (slider 50-200%) with Tauri native webview zoom and font-size fallback. Keyboard shortcuts (Ctrl+=/Ctrl+-/Ctrl+0) wired in desktop shell. Zoom level persists in localStorage.
This commit is contained in:
parent
4cebbb73e8
commit
50c7d1d1e4
@ -7,6 +7,7 @@
|
||||
"core:app:default",
|
||||
"core:window:default",
|
||||
"core:webview:default",
|
||||
"core:webview:allow-set-webview-zoom",
|
||||
"shell:allow-open",
|
||||
"sql:default",
|
||||
"sql:allow-load",
|
||||
|
||||
@ -116,7 +116,21 @@ export function DesktopShell({ children }: DesktopShellProps) {
|
||||
const { registerShortcuts } = await import(
|
||||
"@/lib/desktop/shortcuts"
|
||||
)
|
||||
unregister = await registerShortcuts({ triggerSync })
|
||||
const { WindowManager } = await import("@/lib/desktop/window-manager")
|
||||
unregister = await registerShortcuts({
|
||||
triggerSync,
|
||||
onZoomIn: () => {
|
||||
const current = WindowManager.getZoom()
|
||||
WindowManager.setZoom(Math.round((current + 0.1) * 10) / 10)
|
||||
},
|
||||
onZoomOut: () => {
|
||||
const current = WindowManager.getZoom()
|
||||
WindowManager.setZoom(Math.round((current - 0.1) * 10) / 10)
|
||||
},
|
||||
onZoomReset: () => {
|
||||
WindowManager.setZoom(1.0)
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Failed to register desktop shortcuts:", error)
|
||||
}
|
||||
|
||||
@ -2,9 +2,10 @@
|
||||
|
||||
import * as React from "react"
|
||||
import { useTheme } from "next-themes"
|
||||
import { Check, Moon, Sparkles, Sun, Trash2 } from "lucide-react"
|
||||
import { Check, Moon, RotateCcw, Sparkles, Sun, Trash2 } from "lucide-react"
|
||||
import { toast } from "sonner"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Slider } from "@/components/ui/slider"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { useCompassTheme } from "@/components/theme-provider"
|
||||
import { useAgentOptional } from "@/components/agent/chat-provider"
|
||||
@ -172,6 +173,59 @@ export function AppearanceTab() {
|
||||
|
||||
const isDark = resolvedTheme === "dark"
|
||||
|
||||
const [zoomLevel, setZoomLevel] = React.useState(1.0)
|
||||
|
||||
// Load persisted zoom level on mount
|
||||
React.useEffect(() => {
|
||||
try {
|
||||
const stored = localStorage.getItem("compass-zoom-level")
|
||||
if (stored) {
|
||||
const level = parseFloat(stored)
|
||||
if (!isNaN(level) && level >= 0.5 && level <= 2.0) {
|
||||
setZoomLevel(level)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// localStorage not available
|
||||
}
|
||||
}, [])
|
||||
|
||||
async function applyZoom(level: number): Promise<void> {
|
||||
const clamped = Math.min(2.0, Math.max(0.5, level))
|
||||
try {
|
||||
localStorage.setItem("compass-zoom-level", String(clamped))
|
||||
} catch {
|
||||
// localStorage not available
|
||||
}
|
||||
// Use Tauri native webview zoom (true browser-level zoom)
|
||||
try {
|
||||
const { invoke } = await import("@tauri-apps/api/core")
|
||||
await invoke("plugin:webview|set_webview_zoom", {
|
||||
label: "main",
|
||||
scaleFactor: clamped,
|
||||
})
|
||||
// Clear any CSS fallback
|
||||
document.documentElement.style.fontSize = ""
|
||||
return
|
||||
} catch {
|
||||
// Not in Tauri or permission denied — CSS fallback
|
||||
}
|
||||
// Fallback: scale root font-size (slightly thicker icons but functional)
|
||||
document.documentElement.style.fontSize = `${clamped * 16}px`
|
||||
}
|
||||
|
||||
function handleZoomChange(value: number[]): void {
|
||||
const level = value[0]
|
||||
if (level === undefined) return
|
||||
setZoomLevel(level)
|
||||
void applyZoom(level)
|
||||
}
|
||||
|
||||
function handleZoomReset(): void {
|
||||
setZoomLevel(1.0)
|
||||
void applyZoom(1.0)
|
||||
}
|
||||
|
||||
const allThemes = React.useMemo<ReadonlyArray<ThemeDefinition>>(
|
||||
() => [...THEME_PRESETS, ...customThemes],
|
||||
[customThemes],
|
||||
@ -246,6 +300,40 @@ export function AppearanceTab() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ui scale */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-sm font-medium">UI Scale</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm tabular-nums text-muted-foreground">
|
||||
{Math.round(zoomLevel * 100)}%
|
||||
</span>
|
||||
{zoomLevel !== 1.0 && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleZoomReset}
|
||||
className="flex items-center gap-1 rounded-md px-2 py-0.5 text-xs text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
<RotateCcw className="size-3" />
|
||||
Reset
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Slider
|
||||
value={[zoomLevel]}
|
||||
onValueChange={handleZoomChange}
|
||||
min={0.5}
|
||||
max={2.0}
|
||||
step={0.1}
|
||||
className="w-full"
|
||||
/>
|
||||
<div className="flex justify-between text-xs text-muted-foreground">
|
||||
<span>50%</span>
|
||||
<span>200%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* theme grid */}
|
||||
<div className="space-y-3">
|
||||
<p className="text-sm font-medium">Theme</p>
|
||||
|
||||
@ -8,6 +8,9 @@ export interface ShortcutHandlers {
|
||||
onNew?: () => void
|
||||
onSearch?: () => void
|
||||
onSettings?: () => void
|
||||
onZoomIn?: () => void
|
||||
onZoomOut?: () => void
|
||||
onZoomReset?: () => void
|
||||
}
|
||||
|
||||
interface RegisteredShortcut {
|
||||
@ -56,6 +59,18 @@ export async function registerShortcuts(
|
||||
...(handlers.onSettings
|
||||
? [{ shortcut: `${modifier}+,`, handler: handlers.onSettings }]
|
||||
: []),
|
||||
// Zoom in: Cmd/Ctrl + =
|
||||
...(handlers.onZoomIn
|
||||
? [{ shortcut: `${modifier}+=`, handler: handlers.onZoomIn }]
|
||||
: []),
|
||||
// Zoom out: Cmd/Ctrl + -
|
||||
...(handlers.onZoomOut
|
||||
? [{ shortcut: `${modifier}+-`, handler: handlers.onZoomOut }]
|
||||
: []),
|
||||
// Zoom reset: Cmd/Ctrl + 0
|
||||
...(handlers.onZoomReset
|
||||
? [{ shortcut: `${modifier}+0`, handler: handlers.onZoomReset }]
|
||||
: []),
|
||||
]
|
||||
|
||||
// Register each shortcut
|
||||
@ -128,4 +143,7 @@ export const SHORTCUTS = {
|
||||
reload: "Cmd/Ctrl + R",
|
||||
devTools: "Cmd/Ctrl + Shift + I",
|
||||
quit: "Cmd/Ctrl + Q",
|
||||
zoomIn: "Cmd/Ctrl + =",
|
||||
zoomOut: "Cmd/Ctrl + -",
|
||||
zoomReset: "Cmd/Ctrl + 0",
|
||||
} as const
|
||||
|
||||
@ -13,6 +13,7 @@ export interface WindowState {
|
||||
}
|
||||
|
||||
const WINDOW_STATE_KEY = "compass-window-state"
|
||||
const ZOOM_LEVEL_KEY = "compass-zoom-level"
|
||||
|
||||
// Internal state cache
|
||||
let cachedState: WindowState | null = null
|
||||
@ -85,6 +86,9 @@ export const WindowManager = {
|
||||
|
||||
// Also cache current state
|
||||
cachedState = await loadWindowStateFromStore()
|
||||
|
||||
// Restore zoom level
|
||||
await this.restoreZoom()
|
||||
} catch (error) {
|
||||
console.error("Failed to restore window state:", error)
|
||||
}
|
||||
@ -175,4 +179,46 @@ export const WindowManager = {
|
||||
return true
|
||||
}
|
||||
},
|
||||
|
||||
// Set webview zoom via Tauri native API with CSS font-size fallback
|
||||
async setZoom(level: number): Promise<void> {
|
||||
const clamped = Math.min(2.0, Math.max(0.5, level))
|
||||
try {
|
||||
localStorage.setItem(ZOOM_LEVEL_KEY, String(clamped))
|
||||
} catch {
|
||||
// localStorage not available
|
||||
}
|
||||
try {
|
||||
const { invoke } = await import("@tauri-apps/api/core")
|
||||
await invoke("plugin:webview|set_webview_zoom", {
|
||||
label: "main",
|
||||
scaleFactor: clamped,
|
||||
})
|
||||
document.documentElement.style.fontSize = ""
|
||||
} catch {
|
||||
document.documentElement.style.fontSize = `${clamped * 16}px`
|
||||
}
|
||||
},
|
||||
|
||||
// Get stored zoom level (defaults to 1.0)
|
||||
getZoom(): number {
|
||||
try {
|
||||
const stored = localStorage.getItem(ZOOM_LEVEL_KEY)
|
||||
if (stored) {
|
||||
const level = parseFloat(stored)
|
||||
if (!isNaN(level) && level >= 0.5 && level <= 2.0) return level
|
||||
}
|
||||
} catch {
|
||||
// localStorage not available
|
||||
}
|
||||
return 1.0
|
||||
},
|
||||
|
||||
// Restore zoom from stored level
|
||||
async restoreZoom(): Promise<void> {
|
||||
const level = this.getZoom()
|
||||
if (level !== 1.0) {
|
||||
await this.setZoom(level)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user