From c784a886389d84ccab1a5394c0af3ddd9eb8dd2b Mon Sep 17 00:00:00 2001 From: skyecodes Date: Fri, 25 Sep 2026 13:36:18 +0200 Subject: [PATCH] feat(tao): forward back/forward mouse buttons to Compose --- .../api/decorated-window-tao.api | 2 + .../window/tao/TaoEventConstants.kt | 4 +- .../tao/popup/TaoPopupSceneLayerLinux.kt | 2 + .../tao/scene/AbstractTaoComposeSceneHost.kt | 2 + .../native/linux/nucleus_tao_linux_widget.c | 12 +++--- .../src/main/native/src/events.rs | 22 +++++++++- .../tao/src/platform_impl/macos/view.rs | 14 ++++++- .../window/tao/TaoMouseButtonWireDriftTest.kt | 40 +++++++++++++++++++ 8 files changed, 89 insertions(+), 9 deletions(-) create mode 100644 decorated-window-tao/src/test/kotlin/dev/nucleusframework/window/tao/TaoMouseButtonWireDriftTest.kt diff --git a/decorated-window-tao/api/decorated-window-tao.api b/decorated-window-tao/api/decorated-window-tao.api index 874494cee..a70909b1e 100644 --- a/decorated-window-tao/api/decorated-window-tao.api +++ b/decorated-window-tao/api/decorated-window-tao.api @@ -1261,6 +1261,8 @@ public final class dev/nucleusframework/window/tao/TaoMonitors { public final class dev/nucleusframework/window/tao/TaoMouseButton { public static final field $stable I + public static final field BACK I + public static final field FORWARD I public static final field INSTANCE Ldev/nucleusframework/window/tao/TaoMouseButton; public static final field LEFT I public static final field MIDDLE I diff --git a/decorated-window-tao/src/main/kotlin/dev/nucleusframework/window/tao/TaoEventConstants.kt b/decorated-window-tao/src/main/kotlin/dev/nucleusframework/window/tao/TaoEventConstants.kt index 879cfde08..22b15d95c 100644 --- a/decorated-window-tao/src/main/kotlin/dev/nucleusframework/window/tao/TaoEventConstants.kt +++ b/decorated-window-tao/src/main/kotlin/dev/nucleusframework/window/tao/TaoEventConstants.kt @@ -169,5 +169,7 @@ public object TaoMouseButton { public const val LEFT: Int = 0 public const val RIGHT: Int = 1 public const val MIDDLE: Int = 2 - public const val OTHER: Int = 3 + public const val BACK: Int = 3 + public const val FORWARD: Int = 4 + public const val OTHER: Int = 5 } diff --git a/decorated-window-tao/src/main/kotlin/dev/nucleusframework/window/tao/popup/TaoPopupSceneLayerLinux.kt b/decorated-window-tao/src/main/kotlin/dev/nucleusframework/window/tao/popup/TaoPopupSceneLayerLinux.kt index f4af382c0..f0e18f314 100644 --- a/decorated-window-tao/src/main/kotlin/dev/nucleusframework/window/tao/popup/TaoPopupSceneLayerLinux.kt +++ b/decorated-window-tao/src/main/kotlin/dev/nucleusframework/window/tao/popup/TaoPopupSceneLayerLinux.kt @@ -821,6 +821,8 @@ internal class TaoPopupSceneLayerLinux( when (code) { TaoMouseButton.RIGHT -> PointerButton.Secondary TaoMouseButton.MIDDLE -> PointerButton.Tertiary + TaoMouseButton.BACK -> PointerButton.Back + TaoMouseButton.FORWARD -> PointerButton.Forward else -> PointerButton.Primary } diff --git a/decorated-window-tao/src/main/kotlin/dev/nucleusframework/window/tao/scene/AbstractTaoComposeSceneHost.kt b/decorated-window-tao/src/main/kotlin/dev/nucleusframework/window/tao/scene/AbstractTaoComposeSceneHost.kt index 4e52f841d..858da89fa 100644 --- a/decorated-window-tao/src/main/kotlin/dev/nucleusframework/window/tao/scene/AbstractTaoComposeSceneHost.kt +++ b/decorated-window-tao/src/main/kotlin/dev/nucleusframework/window/tao/scene/AbstractTaoComposeSceneHost.kt @@ -137,6 +137,8 @@ internal abstract class AbstractTaoComposeSceneHost { TaoMouseButton.LEFT -> PointerButton.Primary TaoMouseButton.RIGHT -> PointerButton.Secondary TaoMouseButton.MIDDLE -> PointerButton.Tertiary + TaoMouseButton.BACK -> PointerButton.Back + TaoMouseButton.FORWARD -> PointerButton.Forward else -> PointerButton.Primary } diff --git a/decorated-window-tao/src/main/native/linux/nucleus_tao_linux_widget.c b/decorated-window-tao/src/main/native/linux/nucleus_tao_linux_widget.c index 9365c6281..d61de6c15 100644 --- a/decorated-window-tao/src/main/native/linux/nucleus_tao_linux_widget.c +++ b/decorated-window-tao/src/main/native/linux/nucleus_tao_linux_widget.c @@ -988,16 +988,18 @@ typedef struct { double delta_y; } gdk_event_scroll_t; -/* Map GTK's native button code (1 = LEFT, 2 = MIDDLE, 3 = RIGHT) to - * Tao's AWT-style encoding (`TaoMouseButton.LEFT = 0`, `RIGHT = 1`, - * `MIDDLE = 2`). Anything else stays a passthrough — Compose's - * `mapButton` falls back to `Primary` for unknown codes. */ +/* Map GTK's native button code (1 = LEFT, 2 = MIDDLE, 3 = RIGHT, + * 8 = BACK, 9 = FORWARD) to Tao's AWT-style encoding + * (`TaoMouseButton.LEFT = 0` … `FORWARD = 4`, `OTHER = 5`) — the same + * codes `events.rs` `mouse_button_code` sends for the main surface. */ static int gtk_button_to_tao(unsigned int gtk_button) { switch (gtk_button) { case 1: return 0; /* LEFT */ case 2: return 2; /* MIDDLE */ case 3: return 1; /* RIGHT */ - default: return (int) gtk_button; + case 8: return 3; /* BACK */ + case 9: return 4; /* FORWARD */ + default: return 5; /* OTHER */ } } diff --git a/decorated-window-tao/src/main/native/src/events.rs b/decorated-window-tao/src/main/native/src/events.rs index 327d20655..ad49c28bb 100644 --- a/decorated-window-tao/src/main/native/src/events.rs +++ b/decorated-window-tao/src/main/native/src/events.rs @@ -245,7 +245,25 @@ pub(crate) const TOUCH_FORCE_UNKNOWN: jint = -1; pub(crate) const MOUSE_BUTTON_LEFT: jint = 0; pub(crate) const MOUSE_BUTTON_RIGHT: jint = 1; pub(crate) const MOUSE_BUTTON_MIDDLE: jint = 2; -pub(crate) const MOUSE_BUTTON_OTHER: jint = 3; +pub(crate) const MOUSE_BUTTON_BACK: jint = 3; +pub(crate) const MOUSE_BUTTON_FORWARD: jint = 4; +pub(crate) const MOUSE_BUTTON_OTHER: jint = 5; + +// Raw `MouseButton::Other(n)` numbers tao reports for the back / forward side +// buttons: `XBUTTON1` / `XBUTTON2` on Windows, X11/GDK buttons 8 / 9 on Linux, +// `NSEvent.buttonNumber` 3 / 4 on macOS. +#[cfg(target_os = "windows")] +const OTHER_BACK: u16 = 1; +#[cfg(target_os = "windows")] +const OTHER_FORWARD: u16 = 2; +#[cfg(target_os = "macos")] +const OTHER_BACK: u16 = 3; +#[cfg(target_os = "macos")] +const OTHER_FORWARD: u16 = 4; +#[cfg(not(any(target_os = "windows", target_os = "macos")))] +const OTHER_BACK: u16 = 8; +#[cfg(not(any(target_os = "windows", target_os = "macos")))] +const OTHER_FORWARD: u16 = 9; // ── User events posted from JNI calls into the event loop ───────────────── @@ -682,6 +700,8 @@ pub(crate) fn mouse_button_code(b: MouseButton) -> jint { MouseButton::Left => MOUSE_BUTTON_LEFT, MouseButton::Right => MOUSE_BUTTON_RIGHT, MouseButton::Middle => MOUSE_BUTTON_MIDDLE, + MouseButton::Other(OTHER_BACK) => MOUSE_BUTTON_BACK, + MouseButton::Other(OTHER_FORWARD) => MOUSE_BUTTON_FORWARD, _ => MOUSE_BUTTON_OTHER, } } diff --git a/decorated-window-tao/src/main/native/vendor/tao/src/platform_impl/macos/view.rs b/decorated-window-tao/src/main/native/vendor/tao/src/platform_impl/macos/view.rs index 6ead6d4f4..30a38434c 100644 --- a/decorated-window-tao/src/main/native/vendor/tao/src/platform_impl/macos/view.rs +++ b/decorated-window-tao/src/main/native/vendor/tao/src/platform_impl/macos/view.rs @@ -1152,14 +1152,24 @@ extern "C" fn right_mouse_up(this: &NSView, _sel: Sel, event: &NSEvent) { mouse_click(this, event, MouseButton::Right, ElementState::Released); } +// Nucleus: `otherMouseDown:` fires for every button past the right one, so +// read `buttonNumber` instead of assuming Middle — 3/4 are the back/forward +// side buttons, surfaced as `Other(3)` / `Other(4)`. +fn other_mouse_button(event: &NSEvent) -> MouseButton { + match event.buttonNumber() { + 2 => MouseButton::Middle, + n => MouseButton::Other(n as u16), + } +} + extern "C" fn other_mouse_down(this: &NSView, _sel: Sel, event: &NSEvent) { mouse_motion(this, event); - mouse_click(this, event, MouseButton::Middle, ElementState::Pressed); + mouse_click(this, event, other_mouse_button(event), ElementState::Pressed); } extern "C" fn other_mouse_up(this: &NSView, _sel: Sel, event: &NSEvent) { mouse_motion(this, event); - mouse_click(this, event, MouseButton::Middle, ElementState::Released); + mouse_click(this, event, other_mouse_button(event), ElementState::Released); } fn mouse_motion(this: &NSView, event: &NSEvent) { diff --git a/decorated-window-tao/src/test/kotlin/dev/nucleusframework/window/tao/TaoMouseButtonWireDriftTest.kt b/decorated-window-tao/src/test/kotlin/dev/nucleusframework/window/tao/TaoMouseButtonWireDriftTest.kt new file mode 100644 index 000000000..389b83e8b --- /dev/null +++ b/decorated-window-tao/src/test/kotlin/dev/nucleusframework/window/tao/TaoMouseButtonWireDriftTest.kt @@ -0,0 +1,40 @@ +package dev.nucleusframework.window.tao + +import java.io.File +import java.lang.reflect.Modifier +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.fail + +/** + * Mouse-button codes are written by hand on both sides of the JNI boundary: + * `events.rs` `MOUSE_BUTTON_*` and [TaoMouseButton]. A drift is silent — the + * Rust "other" code once shared its number with [TaoMouseButton.BACK], so + * every extra button reached Compose as Back — so compare them here. + */ +class TaoMouseButtonWireDriftTest { + @Test + fun `Rust MOUSE_BUTTON codes match TaoMouseButton`() { + val rust = + RUST_CODE + .findAll(eventsRs().readText()) + .associate { it.groupValues[1] to it.groupValues[2].toInt() } + val kotlin = + TaoMouseButton::class.java.declaredFields + .filter { Modifier.isStatic(it.modifiers) && it.type == Integer.TYPE && it.name != "\$stable" } + .associate { it.name to it.getInt(null) } + assertEquals(kotlin, rust, "events.rs MOUSE_BUTTON_* vs TaoMouseButton") + } + + private fun eventsRs(): File { + val relative = "src/main/native/src/events.rs" + // Module directory first (Gradle), then the repository root (IDE). + val candidates = listOf(File(relative), File("decorated-window-tao", relative)) + return candidates.firstOrNull { it.isFile } + ?: fail("cannot find $relative from ${File("").absolutePath} (tried ${candidates.map { it.path }})") + } + + private companion object { + val RUST_CODE = Regex("""pub\(crate\) const MOUSE_BUTTON_(\w+): jint = (\d+);""") + } +}