Desktop: make Ctrl+-/= zoom actually work

Two compounding bugs:
1. Nodes skipped in a plain recomposition kept their old captured
   density, so changing the zoom state left the tree unscaled — wrap the
   CompositionLocalProvider in key(zoom) to force full-tree recompose.
2. Compose focus routing missed the shortcut depending on which child
   held focus — add an AWT KeyboardFocusManager dispatcher below Compose
   (also accepts Ctrl+Up/Down, numpad +/-, Ctrl+0 reset).

User-confirmed zooming works on the live app.
This commit is contained in:
avi 2026-09-15 06:52:30 -05:00
commit 5859a082d2

View file

@ -5,7 +5,9 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.key
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@ -35,6 +37,41 @@ fun main() = application {
val screen by state.screen.collectAsState() val screen by state.screen.collectAsState()
// Ctrl+= / Ctrl+- zoom the whole app; Ctrl+0 resets (after a zoom). // Ctrl+= / Ctrl+- zoom the whole app; Ctrl+0 resets (after a zoom).
var zoom by remember { mutableStateOf(1f) } var zoom by remember { mutableStateOf(1f) }
// Belt-and-braces: Compose's focus routing can miss shortcuts depending
// on which child holds focus, so also hook the AWT key pipeline directly.
// Debug print lets us confirm from the dev log whether keys reach the JVM.
DisposableEffect(Unit) {
val mgr = java.awt.KeyboardFocusManager.getCurrentKeyboardFocusManager()
val disp = java.awt.KeyEventDispatcher { e ->
if (e.id == java.awt.event.KeyEvent.KEY_PRESSED &&
e.modifiers == java.awt.event.KeyEvent.CTRL_MASK
) {
when (e.keyCode) {
java.awt.event.KeyEvent.VK_EQUALS,
java.awt.event.KeyEvent.VK_PLUS,
java.awt.event.KeyEvent.VK_ADD,
java.awt.event.KeyEvent.VK_UP -> {
zoom = (zoom * 1.1f).coerceAtMost(3f)
System.err.println("[zoom] ${zoom}"); true
}
java.awt.event.KeyEvent.VK_MINUS,
java.awt.event.KeyEvent.VK_SUBTRACT,
java.awt.event.KeyEvent.VK_DOWN -> {
zoom = (zoom / 1.1f).coerceAtLeast(0.4f)
System.err.println("[zoom] ${zoom}"); true
}
java.awt.event.KeyEvent.VK_0,
java.awt.event.KeyEvent.VK_NUMPAD0 -> {
if (zoom != 1f) { zoom = 1f; System.err.println("[zoom] reset"); true }
else false
}
else -> false
}
} else false
}
mgr.addKeyEventDispatcher(disp)
onDispose { mgr.removeKeyEventDispatcher(disp) }
}
Window( Window(
onCloseRequest = ::exitApplication, onCloseRequest = ::exitApplication,
title = "SHONAR Desktop" + run { title = "SHONAR Desktop" + run {
@ -56,10 +93,10 @@ fun main() = application {
Modifier.fillMaxSize().onPreviewKeyEvent { e -> Modifier.fillMaxSize().onPreviewKeyEvent { e ->
if (e.type != KeyEventType.KeyDown || !e.isCtrlPressed) return@onPreviewKeyEvent false if (e.type != KeyEventType.KeyDown || !e.isCtrlPressed) return@onPreviewKeyEvent false
when (e.key) { when (e.key) {
Key.Equals, Key.Plus, Key.NumPadAdd -> { Key.Equals, Key.Plus, Key.NumPadAdd, Key.DirectionUp -> {
zoom = (zoom * 1.1f).coerceAtMost(3f); true zoom = (zoom * 1.1f).coerceAtMost(3f); true
} }
Key.Minus, Key.NumPadSubtract -> { Key.Minus, Key.NumPadSubtract, Key.DirectionDown -> {
zoom = (zoom / 1.1f).coerceAtLeast(0.4f); true zoom = (zoom / 1.1f).coerceAtLeast(0.4f); true
} }
Key.Zero, Key.NumPad0 -> { Key.Zero, Key.NumPad0 -> {
@ -69,6 +106,10 @@ fun main() = application {
} }
}, },
) { ) {
// key(zoom) forces the whole tree to recompose when zoom
// changes: nodes skipped in a plain recomposition keep their
// old captured density, which made Ctrl+-/= a no-op.
key(zoom) {
CompositionLocalProvider( CompositionLocalProvider(
LocalDensity provides ScaledDensity(baseDensity, zoom), LocalDensity provides ScaledDensity(baseDensity, zoom),
) { ) {
@ -84,4 +125,5 @@ fun main() = application {
} }
} }
} }
}
} }