feat(cef): add GDK-to-CEF input bridge
- commit
- 3a799c66bce49733a25b77ad59247d57cdf4f75c
- author
- brice · · 2026-03-19 15:26 UTC
- committer
- brice · · 2026-03-28 04:35 UTC
- parent
- cca2b3e
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
3 files · +342 -0
M go.mod +2 -0
| @@ -3,6 +3,7 @@ module github.com/bnema/dumber | |||
| 3 | 3 | go 1.26.1 | |
| 4 | 4 | ||
| 5 | 5 | require ( | |
| 6 | + | github.com/bnema/purego-cef v0.0.0-00010101000000-000000000000 | |
| 6 | 7 | github.com/bnema/purego-sqlite v0.1.1 | |
| 7 | 8 | github.com/bnema/puregotk v0.2.2 | |
| 8 | 9 | github.com/bnema/puregotk-webkit v0.0.16 | |
| @@ -39,6 +40,7 @@ require ( | |||
| 39 | 40 | github.com/clipperhouse/uax29/v2 v2.5.0 // indirect | |
| 40 | 41 | github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect | |
| 41 | 42 | github.com/davecgh/go-spew v1.1.1 // indirect | |
| 43 | + | github.com/ebitengine/purego v0.10.0 // indirect | |
| 42 | 44 | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect | |
| 43 | 45 | github.com/go-viper/mapstructure/v2 v2.5.0 // indirect | |
| 44 | 46 | github.com/inconshreveable/mousetrap v1.1.0 // indirect | |
| @@ -3,6 +3,7 @@ module github.com/bnema/dumber | |||||
| 3 | go 1.26.1 | 3 | go 1.26.1 | ||
| 4 | 4 | ||||
| 5 | require ( | 5 | require ( | ||
| 6 | + | github.com/bnema/purego-cef v0.0.0-00010101000000-000000000000 | |||
| 6 | github.com/bnema/purego-sqlite v0.1.1 | 7 | github.com/bnema/purego-sqlite v0.1.1 | ||
| 7 | github.com/bnema/puregotk v0.2.2 | 8 | github.com/bnema/puregotk v0.2.2 | ||
| 8 | github.com/bnema/puregotk-webkit v0.0.16 | 9 | github.com/bnema/puregotk-webkit v0.0.16 | ||
| @@ -39,6 +40,7 @@ require ( | |||||
| 39 | github.com/clipperhouse/uax29/v2 v2.5.0 // indirect | 40 | github.com/clipperhouse/uax29/v2 v2.5.0 // indirect | ||
| 40 | github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect | 41 | github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect | ||
| 41 | github.com/davecgh/go-spew v1.1.1 // indirect | 42 | github.com/davecgh/go-spew v1.1.1 // indirect | ||
| 43 | + | github.com/ebitengine/purego v0.10.0 // indirect | |||
| 42 | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect | 44 | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect | ||
| 43 | github.com/go-viper/mapstructure/v2 v2.5.0 // indirect | 45 | github.com/go-viper/mapstructure/v2 v2.5.0 // indirect | ||
| 44 | github.com/inconshreveable/mousetrap v1.1.0 // indirect | 46 | github.com/inconshreveable/mousetrap v1.1.0 // indirect | ||
M go.sum +2 -0
| @@ -42,6 +42,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c | |||
| 42 | 42 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | |
| 43 | 43 | github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= | |
| 44 | 44 | github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= | |
| 45 | + | github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU= | |
| 46 | + | github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= | |
| 45 | 47 | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= | |
| 46 | 48 | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= | |
| 47 | 49 | github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= | |
| @@ -42,6 +42,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c | |||||
| 42 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | 42 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||
| 43 | github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= | 43 | github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= | ||
| 44 | github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= | 44 | github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= | ||
| 45 | + | github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU= | |||
| 46 | + | github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= | |||
| 45 | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= | 47 | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= | ||
| 46 | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= | 48 | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= | ||
| 47 | github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= | 49 | github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= | ||
A internal/infrastructure/cef/input_bridge.go +338 -0
| @@ -0,0 +1,338 @@ | |||
| 1 | + | package cef | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "sync" | |
| 5 | + | "unsafe" | |
| 6 | + | ||
| 7 | + | purecef "github.com/bnema/purego-cef/cef" | |
| 8 | + | "github.com/jwijenbergh/puregotk/v4/gdk" | |
| 9 | + | "github.com/jwijenbergh/puregotk/v4/gtk" | |
| 10 | + | ) | |
| 11 | + | ||
| 12 | + | // inputBridge translates GDK events from GTK event controllers into CEF input | |
| 13 | + | // events and forwards them to the CEF BrowserHost. The host field is nil until | |
| 14 | + | // the browser is created asynchronously (OnAfterCreated), so all dispatch | |
| 15 | + | // methods guard against a nil host. | |
| 16 | + | type inputBridge struct { | |
| 17 | + | host purecef.BrowserHost | |
| 18 | + | scale int32 | |
| 19 | + | mu sync.Mutex | |
| 20 | + | } | |
| 21 | + | ||
| 22 | + | // newInputBridge creates an input bridge with the given HiDPI scale factor. | |
| 23 | + | func newInputBridge(scale int32) *inputBridge { | |
| 24 | + | return &inputBridge{scale: scale} | |
| 25 | + | } | |
| 26 | + | ||
| 27 | + | // setHost is called once the browser is created to enable event dispatch. | |
| 28 | + | func (ib *inputBridge) setHost(host purecef.BrowserHost) { | |
| 29 | + | ib.mu.Lock() | |
| 30 | + | defer ib.mu.Unlock() | |
| 31 | + | ib.host = host | |
| 32 | + | } | |
| 33 | + | ||
| 34 | + | // attachTo creates GDK event controllers and connects them to the given GLArea. | |
| 35 | + | func (ib *inputBridge) attachTo(glArea *gtk.GLArea) { | |
| 36 | + | // Motion (mouse move + enter/leave) | |
| 37 | + | motion := gtk.NewEventControllerMotion() | |
| 38 | + | motionCb := func(_ gtk.EventControllerMotion, x, y float64) { | |
| 39 | + | ib.onMouseMove(x, y, 0, false) | |
| 40 | + | } | |
| 41 | + | motion.ConnectMotion(&motionCb) | |
| 42 | + | ||
| 43 | + | leaveCb := func(_ gtk.EventControllerMotion) { | |
| 44 | + | ib.onMouseMove(0, 0, 0, true) | |
| 45 | + | } | |
| 46 | + | motion.ConnectLeave(&leaveCb) | |
| 47 | + | ||
| 48 | + | glArea.AddController(&motion.EventController) | |
| 49 | + | ||
| 50 | + | // Click (press / release) | |
| 51 | + | click := gtk.NewGestureClick() | |
| 52 | + | click.SetButton(0) // listen to all buttons | |
| 53 | + | ||
| 54 | + | pressedCb := func(g gtk.GestureClick, nPress int, x, y float64) { | |
| 55 | + | btn := g.GetCurrentButton() | |
| 56 | + | ib.onMousePress(x, y, btn, uint(0), nPress) | |
| 57 | + | } | |
| 58 | + | click.ConnectPressed(&pressedCb) | |
| 59 | + | ||
| 60 | + | releasedCb := func(g gtk.GestureClick, nPress int, x, y float64) { | |
| 61 | + | btn := g.GetCurrentButton() | |
| 62 | + | ib.onMouseRelease(x, y, btn, uint(0), nPress) | |
| 63 | + | } | |
| 64 | + | click.ConnectReleased(&releasedCb) | |
| 65 | + | ||
| 66 | + | glArea.AddController(&click.EventController) | |
| 67 | + | ||
| 68 | + | // Scroll | |
| 69 | + | scroll := gtk.NewEventControllerScroll(gtk.EventControllerScrollBothAxesValue) | |
| 70 | + | scrollCb := func(_ gtk.EventControllerScroll, dx, dy float64) bool { | |
| 71 | + | ib.onScroll(dx, dy) | |
| 72 | + | return true | |
| 73 | + | } | |
| 74 | + | scroll.ConnectScroll(&scrollCb) | |
| 75 | + | ||
| 76 | + | glArea.AddController(&scroll.EventController) | |
| 77 | + | ||
| 78 | + | // Keyboard | |
| 79 | + | key := gtk.NewEventControllerKey() | |
| 80 | + | keyPressCb := func(_ gtk.EventControllerKey, keyval, keycode uint, state gdk.ModifierType) bool { | |
| 81 | + | ib.onKeyPress(keyval, keycode, uint(state)) | |
| 82 | + | return true | |
| 83 | + | } | |
| 84 | + | key.ConnectKeyPressed(&keyPressCb) | |
| 85 | + | ||
| 86 | + | keyReleaseCb := func(_ gtk.EventControllerKey, keyval, keycode uint, state gdk.ModifierType) { | |
| 87 | + | ib.onKeyRelease(keyval, keycode, uint(state)) | |
| 88 | + | } | |
| 89 | + | key.ConnectKeyReleased(&keyReleaseCb) | |
| 90 | + | ||
| 91 | + | glArea.AddController(&key.EventController) | |
| 92 | + | ||
| 93 | + | // Make the GLArea focusable so it can receive key events. | |
| 94 | + | glArea.SetFocusable(true) | |
| 95 | + | glArea.SetCanFocus(true) | |
| 96 | + | } | |
| 97 | + | ||
| 98 | + | // --------------------------------------------------------------------------- | |
| 99 | + | // Event handlers | |
| 100 | + | // --------------------------------------------------------------------------- | |
| 101 | + | ||
| 102 | + | func (ib *inputBridge) onMouseMove(x, y float64, mods uint, leave bool) { | |
| 103 | + | ib.mu.Lock() | |
| 104 | + | host := ib.host | |
| 105 | + | ib.mu.Unlock() | |
| 106 | + | if host == nil { | |
| 107 | + | return | |
| 108 | + | } | |
| 109 | + | ||
| 110 | + | evt := buildMouseEvent(x, y, mods, ib.scale) | |
| 111 | + | mouseLeave := int32(0) | |
| 112 | + | if leave { | |
| 113 | + | mouseLeave = 1 | |
| 114 | + | } | |
| 115 | + | host.SendMouseMoveEvent(&evt, mouseLeave) | |
| 116 | + | } | |
| 117 | + | ||
| 118 | + | func (ib *inputBridge) onMousePress(x, y float64, button, mods uint, clickCount int) { | |
| 119 | + | ib.mu.Lock() | |
| 120 | + | host := ib.host | |
| 121 | + | ib.mu.Unlock() | |
| 122 | + | if host == nil { | |
| 123 | + | return | |
| 124 | + | } | |
| 125 | + | ||
| 126 | + | evt := buildMouseEvent(x, y, mods, ib.scale) | |
| 127 | + | btn := translateMouseButton(button) | |
| 128 | + | host.SendMouseClickEvent(&evt, btn, 0, int32(clickCount)) | |
| 129 | + | } | |
| 130 | + | ||
| 131 | + | func (ib *inputBridge) onMouseRelease(x, y float64, button, mods uint, clickCount int) { | |
| 132 | + | ib.mu.Lock() | |
| 133 | + | host := ib.host | |
| 134 | + | ib.mu.Unlock() | |
| 135 | + | if host == nil { | |
| 136 | + | return | |
| 137 | + | } | |
| 138 | + | ||
| 139 | + | evt := buildMouseEvent(x, y, mods, ib.scale) | |
| 140 | + | btn := translateMouseButton(button) | |
| 141 | + | host.SendMouseClickEvent(&evt, btn, 1, int32(clickCount)) | |
| 142 | + | } | |
| 143 | + | ||
| 144 | + | func (ib *inputBridge) onScroll(dx, dy float64) { | |
| 145 | + | ib.mu.Lock() | |
| 146 | + | host := ib.host | |
| 147 | + | ib.mu.Unlock() | |
| 148 | + | if host == nil { | |
| 149 | + | return | |
| 150 | + | } | |
| 151 | + | ||
| 152 | + | evt := buildMouseEvent(0, 0, 0, ib.scale) | |
| 153 | + | deltaX, deltaY := translateScrollDeltas(dx, dy) | |
| 154 | + | host.SendMouseWheelEvent(&evt, deltaX, deltaY) | |
| 155 | + | } | |
| 156 | + | ||
| 157 | + | func (ib *inputBridge) onKeyPress(keyval, keycode uint, mods uint) { | |
| 158 | + | ib.mu.Lock() | |
| 159 | + | host := ib.host | |
| 160 | + | ib.mu.Unlock() | |
| 161 | + | if host == nil { | |
| 162 | + | return | |
| 163 | + | } | |
| 164 | + | ||
| 165 | + | // Send RAWKEYDOWN first, then CHAR if printable. | |
| 166 | + | evt := buildKeyEvent(keyval, keycode, mods, purecef.KeyEventTypeKeyeventRawkeydown) | |
| 167 | + | host.SendKeyEvent(&evt) | |
| 168 | + | ||
| 169 | + | // Follow up with a CHAR event for printable characters. | |
| 170 | + | if ch := keyvalToChar(keyval); ch != 0 { | |
| 171 | + | charEvt := buildKeyEvent(keyval, keycode, mods, purecef.KeyEventTypeKeyeventChar) | |
| 172 | + | charEvt.Character = ch | |
| 173 | + | charEvt.UnmodifiedCharacter = ch | |
| 174 | + | host.SendKeyEvent(&charEvt) | |
| 175 | + | } | |
| 176 | + | } | |
| 177 | + | ||
| 178 | + | func (ib *inputBridge) onKeyRelease(keyval, keycode uint, mods uint) { | |
| 179 | + | ib.mu.Lock() | |
| 180 | + | host := ib.host | |
| 181 | + | ib.mu.Unlock() | |
| 182 | + | if host == nil { | |
| 183 | + | return | |
| 184 | + | } | |
| 185 | + | ||
| 186 | + | evt := buildKeyEvent(keyval, keycode, mods, purecef.KeyEventTypeKeyeventKeyup) | |
| 187 | + | host.SendKeyEvent(&evt) | |
| 188 | + | } | |
| 189 | + | ||
| 190 | + | // --------------------------------------------------------------------------- | |
| 191 | + | // Translation helpers | |
| 192 | + | // --------------------------------------------------------------------------- | |
| 193 | + | ||
| 194 | + | func buildMouseEvent(x, y float64, gdkMods uint, scale int32) purecef.MouseEvent { | |
| 195 | + | return purecef.MouseEvent{ | |
| 196 | + | X: int32(x * float64(scale)), | |
| 197 | + | Y: int32(y * float64(scale)), | |
| 198 | + | Modifiers: translateModifiers(gdkMods), | |
| 199 | + | } | |
| 200 | + | } | |
| 201 | + | ||
| 202 | + | func buildKeyEvent(keyval, keycode uint, gdkMods uint, eventType purecef.KeyEventType) purecef.KeyEvent { | |
| 203 | + | var evt purecef.KeyEvent | |
| 204 | + | evt.Size = unsafe.Sizeof(evt) | |
| 205 | + | // Use unsafe to set Type field — the underlying types are both int32 but | |
| 206 | + | // the struct field uses an internal (unexportable) named type. | |
| 207 | + | *(*int32)(unsafe.Pointer(&evt.Type)) = int32(eventType) | |
| 208 | + | evt.Modifiers = translateModifiers(gdkMods) | |
| 209 | + | evt.WindowsKeyCode = gdkKeyvalToWindowsVK(keyval) | |
| 210 | + | evt.NativeKeyCode = int32(keycode) | |
| 211 | + | return evt | |
| 212 | + | } | |
| 213 | + | ||
| 214 | + | // translateModifiers maps GDK modifier masks to CEF EventFlags values. | |
| 215 | + | func translateModifiers(gdkState uint) uint32 { | |
| 216 | + | var flags uint32 | |
| 217 | + | ||
| 218 | + | if gdkState&uint(gdk.ShiftMaskValue) != 0 { | |
| 219 | + | flags |= uint32(purecef.EventFlagsEventflagShiftDown) | |
| 220 | + | } | |
| 221 | + | if gdkState&uint(gdk.ControlMaskValue) != 0 { | |
| 222 | + | flags |= uint32(purecef.EventFlagsEventflagControlDown) | |
| 223 | + | } | |
| 224 | + | if gdkState&uint(gdk.AltMaskValue) != 0 { | |
| 225 | + | flags |= uint32(purecef.EventFlagsEventflagAltDown) | |
| 226 | + | } | |
| 227 | + | if gdkState&uint(gdk.Button1MaskValue) != 0 { | |
| 228 | + | flags |= uint32(purecef.EventFlagsEventflagLeftMouseButton) | |
| 229 | + | } | |
| 230 | + | if gdkState&uint(gdk.Button2MaskValue) != 0 { | |
| 231 | + | flags |= uint32(purecef.EventFlagsEventflagMiddleMouseButton) | |
| 232 | + | } | |
| 233 | + | if gdkState&uint(gdk.Button3MaskValue) != 0 { | |
| 234 | + | flags |= uint32(purecef.EventFlagsEventflagRightMouseButton) | |
| 235 | + | } | |
| 236 | + | ||
| 237 | + | return flags | |
| 238 | + | } | |
| 239 | + | ||
| 240 | + | // translateMouseButton maps a GDK button number to a CEF MouseButtonType. | |
| 241 | + | func translateMouseButton(gdkButton uint) purecef.MouseButtonType { | |
| 242 | + | switch gdkButton { | |
| 243 | + | case 1: | |
| 244 | + | return purecef.MouseButtonTypeMbtLeft | |
| 245 | + | case 2: | |
| 246 | + | return purecef.MouseButtonTypeMbtMiddle | |
| 247 | + | case 3: | |
| 248 | + | return purecef.MouseButtonTypeMbtRight | |
| 249 | + | default: | |
| 250 | + | return purecef.MouseButtonTypeMbtLeft | |
| 251 | + | } | |
| 252 | + | } | |
| 253 | + | ||
| 254 | + | // translateScrollDeltas converts GDK scroll deltas to CEF units (120 per notch). | |
| 255 | + | func translateScrollDeltas(dx, dy float64) (int32, int32) { | |
| 256 | + | return int32(dx * 120), int32(-dy * 120) // CEF: positive Y = scroll up | |
| 257 | + | } | |
| 258 | + | ||
| 259 | + | // keyvalToChar returns the UTF-16 character for a printable GDK keyval, or 0. | |
| 260 | + | func keyvalToChar(keyval uint) uint16 { | |
| 261 | + | // Printable ASCII range. | |
| 262 | + | if keyval >= 0x020 && keyval <= 0x07e { | |
| 263 | + | return uint16(keyval) | |
| 264 | + | } | |
| 265 | + | // GDK_KEY_Return → carriage return | |
| 266 | + | if keyval == 0xff0d { | |
| 267 | + | return '\r' | |
| 268 | + | } | |
| 269 | + | // GDK_KEY_Tab | |
| 270 | + | if keyval == 0xff09 { | |
| 271 | + | return '\t' | |
| 272 | + | } | |
| 273 | + | // GDK_KEY_BackSpace | |
| 274 | + | if keyval == 0xff08 { | |
| 275 | + | return '\b' | |
| 276 | + | } | |
| 277 | + | return 0 | |
| 278 | + | } | |
| 279 | + | ||
| 280 | + | // gdkKeyvalToWindowsVK translates a GDK keyval to a Windows virtual-key code. | |
| 281 | + | func gdkKeyvalToWindowsVK(keyval uint) int32 { | |
| 282 | + | // Lowercase letters → uppercase VK codes (A-Z = 0x41-0x5A) | |
| 283 | + | if keyval >= 0x061 && keyval <= 0x07a { | |
| 284 | + | return int32(keyval-0x061) + 0x41 | |
| 285 | + | } | |
| 286 | + | // Uppercase letters | |
| 287 | + | if keyval >= 0x041 && keyval <= 0x05a { | |
| 288 | + | return int32(keyval-0x041) + 0x41 | |
| 289 | + | } | |
| 290 | + | // Digits 0-9 = 0x30-0x39 | |
| 291 | + | if keyval >= 0x030 && keyval <= 0x039 { | |
| 292 | + | return int32(keyval) | |
| 293 | + | } | |
| 294 | + | // F1-F12: GDK 0xffbe-0xffc9 → VK 0x70-0x7B | |
| 295 | + | if keyval >= 0xffbe && keyval <= 0xffc9 { | |
| 296 | + | return int32(keyval-0xffbe) + 0x70 | |
| 297 | + | } | |
| 298 | + | // Arrow keys: GDK 0xff51-0xff54 → VK Left=0x25, Up=0x26, Right=0x27, Down=0x28 | |
| 299 | + | if keyval >= 0xff51 && keyval <= 0xff54 { | |
| 300 | + | return int32(keyval-0xff51) + 0x25 | |
| 301 | + | } | |
| 302 | + | ||
| 303 | + | // Individual keys | |
| 304 | + | switch keyval { | |
| 305 | + | case 0xff0d: // Return | |
| 306 | + | return 0x0D | |
| 307 | + | case 0xff1b: // Escape | |
| 308 | + | return 0x1B | |
| 309 | + | case 0xff09: // Tab | |
| 310 | + | return 0x09 | |
| 311 | + | case 0xff08: // BackSpace | |
| 312 | + | return 0x08 | |
| 313 | + | case 0xffff: // Delete | |
| 314 | + | return 0x2E | |
| 315 | + | case 0x020: // space | |
| 316 | + | return 0x20 | |
| 317 | + | case 0xff50: // Home | |
| 318 | + | return 0x24 | |
| 319 | + | case 0xff57: // End | |
| 320 | + | return 0x23 | |
| 321 | + | case 0xff55: // Page_Up | |
| 322 | + | return 0x21 | |
| 323 | + | case 0xff56: // Page_Down | |
| 324 | + | return 0x22 | |
| 325 | + | case 0xffe1, 0xffe2: // Shift_L, Shift_R | |
| 326 | + | return 0xA0 | |
| 327 | + | case 0xffe3, 0xffe4: // Control_L, Control_R | |
| 328 | + | return 0xA2 | |
| 329 | + | case 0xffe9, 0xffea: // Alt_L, Alt_R | |
| 330 | + | return 0xA4 | |
| 331 | + | default: | |
| 332 | + | // For unmapped keys, return the keyval if it fits in a reasonable range. | |
| 333 | + | if keyval < 0x100 { | |
| 334 | + | return int32(keyval) | |
| 335 | + | } | |
| 336 | + | return 0 | |
| 337 | + | } | |
| 338 | + | } | |
| @@ -0,0 +1,338 @@ | |||||
| 1 | + | package cef | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "sync" | |||
| 5 | + | "unsafe" | |||
| 6 | + | ||||
| 7 | + | purecef "github.com/bnema/purego-cef/cef" | |||
| 8 | + | "github.com/jwijenbergh/puregotk/v4/gdk" | |||
| 9 | + | "github.com/jwijenbergh/puregotk/v4/gtk" | |||
| 10 | + | ) | |||
| 11 | + | ||||
| 12 | + | // inputBridge translates GDK events from GTK event controllers into CEF input | |||
| 13 | + | // events and forwards them to the CEF BrowserHost. The host field is nil until | |||
| 14 | + | // the browser is created asynchronously (OnAfterCreated), so all dispatch | |||
| 15 | + | // methods guard against a nil host. | |||
| 16 | + | type inputBridge struct { | |||
| 17 | + | host purecef.BrowserHost | |||
| 18 | + | scale int32 | |||
| 19 | + | mu sync.Mutex | |||
| 20 | + | } | |||
| 21 | + | ||||
| 22 | + | // newInputBridge creates an input bridge with the given HiDPI scale factor. | |||
| 23 | + | func newInputBridge(scale int32) *inputBridge { | |||
| 24 | + | return &inputBridge{scale: scale} | |||
| 25 | + | } | |||
| 26 | + | ||||
| 27 | + | // setHost is called once the browser is created to enable event dispatch. | |||
| 28 | + | func (ib *inputBridge) setHost(host purecef.BrowserHost) { | |||
| 29 | + | ib.mu.Lock() | |||
| 30 | + | defer ib.mu.Unlock() | |||
| 31 | + | ib.host = host | |||
| 32 | + | } | |||
| 33 | + | ||||
| 34 | + | // attachTo creates GDK event controllers and connects them to the given GLArea. | |||
| 35 | + | func (ib *inputBridge) attachTo(glArea *gtk.GLArea) { | |||
| 36 | + | // Motion (mouse move + enter/leave) | |||
| 37 | + | motion := gtk.NewEventControllerMotion() | |||
| 38 | + | motionCb := func(_ gtk.EventControllerMotion, x, y float64) { | |||
| 39 | + | ib.onMouseMove(x, y, 0, false) | |||
| 40 | + | } | |||
| 41 | + | motion.ConnectMotion(&motionCb) | |||
| 42 | + | ||||
| 43 | + | leaveCb := func(_ gtk.EventControllerMotion) { | |||
| 44 | + | ib.onMouseMove(0, 0, 0, true) | |||
| 45 | + | } | |||
| 46 | + | motion.ConnectLeave(&leaveCb) | |||
| 47 | + | ||||
| 48 | + | glArea.AddController(&motion.EventController) | |||
| 49 | + | ||||
| 50 | + | // Click (press / release) | |||
| 51 | + | click := gtk.NewGestureClick() | |||
| 52 | + | click.SetButton(0) // listen to all buttons | |||
| 53 | + | ||||
| 54 | + | pressedCb := func(g gtk.GestureClick, nPress int, x, y float64) { | |||
| 55 | + | btn := g.GetCurrentButton() | |||
| 56 | + | ib.onMousePress(x, y, btn, uint(0), nPress) | |||
| 57 | + | } | |||
| 58 | + | click.ConnectPressed(&pressedCb) | |||
| 59 | + | ||||
| 60 | + | releasedCb := func(g gtk.GestureClick, nPress int, x, y float64) { | |||
| 61 | + | btn := g.GetCurrentButton() | |||
| 62 | + | ib.onMouseRelease(x, y, btn, uint(0), nPress) | |||
| 63 | + | } | |||
| 64 | + | click.ConnectReleased(&releasedCb) | |||
| 65 | + | ||||
| 66 | + | glArea.AddController(&click.EventController) | |||
| 67 | + | ||||
| 68 | + | // Scroll | |||
| 69 | + | scroll := gtk.NewEventControllerScroll(gtk.EventControllerScrollBothAxesValue) | |||
| 70 | + | scrollCb := func(_ gtk.EventControllerScroll, dx, dy float64) bool { | |||
| 71 | + | ib.onScroll(dx, dy) | |||
| 72 | + | return true | |||
| 73 | + | } | |||
| 74 | + | scroll.ConnectScroll(&scrollCb) | |||
| 75 | + | ||||
| 76 | + | glArea.AddController(&scroll.EventController) | |||
| 77 | + | ||||
| 78 | + | // Keyboard | |||
| 79 | + | key := gtk.NewEventControllerKey() | |||
| 80 | + | keyPressCb := func(_ gtk.EventControllerKey, keyval, keycode uint, state gdk.ModifierType) bool { | |||
| 81 | + | ib.onKeyPress(keyval, keycode, uint(state)) | |||
| 82 | + | return true | |||
| 83 | + | } | |||
| 84 | + | key.ConnectKeyPressed(&keyPressCb) | |||
| 85 | + | ||||
| 86 | + | keyReleaseCb := func(_ gtk.EventControllerKey, keyval, keycode uint, state gdk.ModifierType) { | |||
| 87 | + | ib.onKeyRelease(keyval, keycode, uint(state)) | |||
| 88 | + | } | |||
| 89 | + | key.ConnectKeyReleased(&keyReleaseCb) | |||
| 90 | + | ||||
| 91 | + | glArea.AddController(&key.EventController) | |||
| 92 | + | ||||
| 93 | + | // Make the GLArea focusable so it can receive key events. | |||
| 94 | + | glArea.SetFocusable(true) | |||
| 95 | + | glArea.SetCanFocus(true) | |||
| 96 | + | } | |||
| 97 | + | ||||
| 98 | + | // --------------------------------------------------------------------------- | |||
| 99 | + | // Event handlers | |||
| 100 | + | // --------------------------------------------------------------------------- | |||
| 101 | + | ||||
| 102 | + | func (ib *inputBridge) onMouseMove(x, y float64, mods uint, leave bool) { | |||
| 103 | + | ib.mu.Lock() | |||
| 104 | + | host := ib.host | |||
| 105 | + | ib.mu.Unlock() | |||
| 106 | + | if host == nil { | |||
| 107 | + | return | |||
| 108 | + | } | |||
| 109 | + | ||||
| 110 | + | evt := buildMouseEvent(x, y, mods, ib.scale) | |||
| 111 | + | mouseLeave := int32(0) | |||
| 112 | + | if leave { | |||
| 113 | + | mouseLeave = 1 | |||
| 114 | + | } | |||
| 115 | + | host.SendMouseMoveEvent(&evt, mouseLeave) | |||
| 116 | + | } | |||
| 117 | + | ||||
| 118 | + | func (ib *inputBridge) onMousePress(x, y float64, button, mods uint, clickCount int) { | |||
| 119 | + | ib.mu.Lock() | |||
| 120 | + | host := ib.host | |||
| 121 | + | ib.mu.Unlock() | |||
| 122 | + | if host == nil { | |||
| 123 | + | return | |||
| 124 | + | } | |||
| 125 | + | ||||
| 126 | + | evt := buildMouseEvent(x, y, mods, ib.scale) | |||
| 127 | + | btn := translateMouseButton(button) | |||
| 128 | + | host.SendMouseClickEvent(&evt, btn, 0, int32(clickCount)) | |||
| 129 | + | } | |||
| 130 | + | ||||
| 131 | + | func (ib *inputBridge) onMouseRelease(x, y float64, button, mods uint, clickCount int) { | |||
| 132 | + | ib.mu.Lock() | |||
| 133 | + | host := ib.host | |||
| 134 | + | ib.mu.Unlock() | |||
| 135 | + | if host == nil { | |||
| 136 | + | return | |||
| 137 | + | } | |||
| 138 | + | ||||
| 139 | + | evt := buildMouseEvent(x, y, mods, ib.scale) | |||
| 140 | + | btn := translateMouseButton(button) | |||
| 141 | + | host.SendMouseClickEvent(&evt, btn, 1, int32(clickCount)) | |||
| 142 | + | } | |||
| 143 | + | ||||
| 144 | + | func (ib *inputBridge) onScroll(dx, dy float64) { | |||
| 145 | + | ib.mu.Lock() | |||
| 146 | + | host := ib.host | |||
| 147 | + | ib.mu.Unlock() | |||
| 148 | + | if host == nil { | |||
| 149 | + | return | |||
| 150 | + | } | |||
| 151 | + | ||||
| 152 | + | evt := buildMouseEvent(0, 0, 0, ib.scale) | |||
| 153 | + | deltaX, deltaY := translateScrollDeltas(dx, dy) | |||
| 154 | + | host.SendMouseWheelEvent(&evt, deltaX, deltaY) | |||
| 155 | + | } | |||
| 156 | + | ||||
| 157 | + | func (ib *inputBridge) onKeyPress(keyval, keycode uint, mods uint) { | |||
| 158 | + | ib.mu.Lock() | |||
| 159 | + | host := ib.host | |||
| 160 | + | ib.mu.Unlock() | |||
| 161 | + | if host == nil { | |||
| 162 | + | return | |||
| 163 | + | } | |||
| 164 | + | ||||
| 165 | + | // Send RAWKEYDOWN first, then CHAR if printable. | |||
| 166 | + | evt := buildKeyEvent(keyval, keycode, mods, purecef.KeyEventTypeKeyeventRawkeydown) | |||
| 167 | + | host.SendKeyEvent(&evt) | |||
| 168 | + | ||||
| 169 | + | // Follow up with a CHAR event for printable characters. | |||
| 170 | + | if ch := keyvalToChar(keyval); ch != 0 { | |||
| 171 | + | charEvt := buildKeyEvent(keyval, keycode, mods, purecef.KeyEventTypeKeyeventChar) | |||
| 172 | + | charEvt.Character = ch | |||
| 173 | + | charEvt.UnmodifiedCharacter = ch | |||
| 174 | + | host.SendKeyEvent(&charEvt) | |||
| 175 | + | } | |||
| 176 | + | } | |||
| 177 | + | ||||
| 178 | + | func (ib *inputBridge) onKeyRelease(keyval, keycode uint, mods uint) { | |||
| 179 | + | ib.mu.Lock() | |||
| 180 | + | host := ib.host | |||
| 181 | + | ib.mu.Unlock() | |||
| 182 | + | if host == nil { | |||
| 183 | + | return | |||
| 184 | + | } | |||
| 185 | + | ||||
| 186 | + | evt := buildKeyEvent(keyval, keycode, mods, purecef.KeyEventTypeKeyeventKeyup) | |||
| 187 | + | host.SendKeyEvent(&evt) | |||
| 188 | + | } | |||
| 189 | + | ||||
| 190 | + | // --------------------------------------------------------------------------- | |||
| 191 | + | // Translation helpers | |||
| 192 | + | // --------------------------------------------------------------------------- | |||
| 193 | + | ||||
| 194 | + | func buildMouseEvent(x, y float64, gdkMods uint, scale int32) purecef.MouseEvent { | |||
| 195 | + | return purecef.MouseEvent{ | |||
| 196 | + | X: int32(x * float64(scale)), | |||
| 197 | + | Y: int32(y * float64(scale)), | |||
| 198 | + | Modifiers: translateModifiers(gdkMods), | |||
| 199 | + | } | |||
| 200 | + | } | |||
| 201 | + | ||||
| 202 | + | func buildKeyEvent(keyval, keycode uint, gdkMods uint, eventType purecef.KeyEventType) purecef.KeyEvent { | |||
| 203 | + | var evt purecef.KeyEvent | |||
| 204 | + | evt.Size = unsafe.Sizeof(evt) | |||
| 205 | + | // Use unsafe to set Type field — the underlying types are both int32 but | |||
| 206 | + | // the struct field uses an internal (unexportable) named type. | |||
| 207 | + | *(*int32)(unsafe.Pointer(&evt.Type)) = int32(eventType) | |||
| 208 | + | evt.Modifiers = translateModifiers(gdkMods) | |||
| 209 | + | evt.WindowsKeyCode = gdkKeyvalToWindowsVK(keyval) | |||
| 210 | + | evt.NativeKeyCode = int32(keycode) | |||
| 211 | + | return evt | |||
| 212 | + | } | |||
| 213 | + | ||||
| 214 | + | // translateModifiers maps GDK modifier masks to CEF EventFlags values. | |||
| 215 | + | func translateModifiers(gdkState uint) uint32 { | |||
| 216 | + | var flags uint32 | |||
| 217 | + | ||||
| 218 | + | if gdkState&uint(gdk.ShiftMaskValue) != 0 { | |||
| 219 | + | flags |= uint32(purecef.EventFlagsEventflagShiftDown) | |||
| 220 | + | } | |||
| 221 | + | if gdkState&uint(gdk.ControlMaskValue) != 0 { | |||
| 222 | + | flags |= uint32(purecef.EventFlagsEventflagControlDown) | |||
| 223 | + | } | |||
| 224 | + | if gdkState&uint(gdk.AltMaskValue) != 0 { | |||
| 225 | + | flags |= uint32(purecef.EventFlagsEventflagAltDown) | |||
| 226 | + | } | |||
| 227 | + | if gdkState&uint(gdk.Button1MaskValue) != 0 { | |||
| 228 | + | flags |= uint32(purecef.EventFlagsEventflagLeftMouseButton) | |||
| 229 | + | } | |||
| 230 | + | if gdkState&uint(gdk.Button2MaskValue) != 0 { | |||
| 231 | + | flags |= uint32(purecef.EventFlagsEventflagMiddleMouseButton) | |||
| 232 | + | } | |||
| 233 | + | if gdkState&uint(gdk.Button3MaskValue) != 0 { | |||
| 234 | + | flags |= uint32(purecef.EventFlagsEventflagRightMouseButton) | |||
| 235 | + | } | |||
| 236 | + | ||||
| 237 | + | return flags | |||
| 238 | + | } | |||
| 239 | + | ||||
| 240 | + | // translateMouseButton maps a GDK button number to a CEF MouseButtonType. | |||
| 241 | + | func translateMouseButton(gdkButton uint) purecef.MouseButtonType { | |||
| 242 | + | switch gdkButton { | |||
| 243 | + | case 1: | |||
| 244 | + | return purecef.MouseButtonTypeMbtLeft | |||
| 245 | + | case 2: | |||
| 246 | + | return purecef.MouseButtonTypeMbtMiddle | |||
| 247 | + | case 3: | |||
| 248 | + | return purecef.MouseButtonTypeMbtRight | |||
| 249 | + | default: | |||
| 250 | + | return purecef.MouseButtonTypeMbtLeft | |||
| 251 | + | } | |||
| 252 | + | } | |||
| 253 | + | ||||
| 254 | + | // translateScrollDeltas converts GDK scroll deltas to CEF units (120 per notch). | |||
| 255 | + | func translateScrollDeltas(dx, dy float64) (int32, int32) { | |||
| 256 | + | return int32(dx * 120), int32(-dy * 120) // CEF: positive Y = scroll up | |||
| 257 | + | } | |||
| 258 | + | ||||
| 259 | + | // keyvalToChar returns the UTF-16 character for a printable GDK keyval, or 0. | |||
| 260 | + | func keyvalToChar(keyval uint) uint16 { | |||
| 261 | + | // Printable ASCII range. | |||
| 262 | + | if keyval >= 0x020 && keyval <= 0x07e { | |||
| 263 | + | return uint16(keyval) | |||
| 264 | + | } | |||
| 265 | + | // GDK_KEY_Return → carriage return | |||
| 266 | + | if keyval == 0xff0d { | |||
| 267 | + | return '\r' | |||
| 268 | + | } | |||
| 269 | + | // GDK_KEY_Tab | |||
| 270 | + | if keyval == 0xff09 { | |||
| 271 | + | return '\t' | |||
| 272 | + | } | |||
| 273 | + | // GDK_KEY_BackSpace | |||
| 274 | + | if keyval == 0xff08 { | |||
| 275 | + | return '\b' | |||
| 276 | + | } | |||
| 277 | + | return 0 | |||
| 278 | + | } | |||
| 279 | + | ||||
| 280 | + | // gdkKeyvalToWindowsVK translates a GDK keyval to a Windows virtual-key code. | |||
| 281 | + | func gdkKeyvalToWindowsVK(keyval uint) int32 { | |||
| 282 | + | // Lowercase letters → uppercase VK codes (A-Z = 0x41-0x5A) | |||
| 283 | + | if keyval >= 0x061 && keyval <= 0x07a { | |||
| 284 | + | return int32(keyval-0x061) + 0x41 | |||
| 285 | + | } | |||
| 286 | + | // Uppercase letters | |||
| 287 | + | if keyval >= 0x041 && keyval <= 0x05a { | |||
| 288 | + | return int32(keyval-0x041) + 0x41 | |||
| 289 | + | } | |||
| 290 | + | // Digits 0-9 = 0x30-0x39 | |||
| 291 | + | if keyval >= 0x030 && keyval <= 0x039 { | |||
| 292 | + | return int32(keyval) | |||
| 293 | + | } | |||
| 294 | + | // F1-F12: GDK 0xffbe-0xffc9 → VK 0x70-0x7B | |||
| 295 | + | if keyval >= 0xffbe && keyval <= 0xffc9 { | |||
| 296 | + | return int32(keyval-0xffbe) + 0x70 | |||
| 297 | + | } | |||
| 298 | + | // Arrow keys: GDK 0xff51-0xff54 → VK Left=0x25, Up=0x26, Right=0x27, Down=0x28 | |||
| 299 | + | if keyval >= 0xff51 && keyval <= 0xff54 { | |||
| 300 | + | return int32(keyval-0xff51) + 0x25 | |||
| 301 | + | } | |||
| 302 | + | ||||
| 303 | + | // Individual keys | |||
| 304 | + | switch keyval { | |||
| 305 | + | case 0xff0d: // Return | |||
| 306 | + | return 0x0D | |||
| 307 | + | case 0xff1b: // Escape | |||
| 308 | + | return 0x1B | |||
| 309 | + | case 0xff09: // Tab | |||
| 310 | + | return 0x09 | |||
| 311 | + | case 0xff08: // BackSpace | |||
| 312 | + | return 0x08 | |||
| 313 | + | case 0xffff: // Delete | |||
| 314 | + | return 0x2E | |||
| 315 | + | case 0x020: // space | |||
| 316 | + | return 0x20 | |||
| 317 | + | case 0xff50: // Home | |||
| 318 | + | return 0x24 | |||
| 319 | + | case 0xff57: // End | |||
| 320 | + | return 0x23 | |||
| 321 | + | case 0xff55: // Page_Up | |||
| 322 | + | return 0x21 | |||
| 323 | + | case 0xff56: // Page_Down | |||
| 324 | + | return 0x22 | |||
| 325 | + | case 0xffe1, 0xffe2: // Shift_L, Shift_R | |||
| 326 | + | return 0xA0 | |||
| 327 | + | case 0xffe3, 0xffe4: // Control_L, Control_R | |||
| 328 | + | return 0xA2 | |||
| 329 | + | case 0xffe9, 0xffea: // Alt_L, Alt_R | |||
| 330 | + | return 0xA4 | |||
| 331 | + | default: | |||
| 332 | + | // For unmapped keys, return the keyval if it fits in a reasonable range. | |||
| 333 | + | if keyval < 0x100 { | |||
| 334 | + | return int32(keyval) | |||
| 335 | + | } | |||
| 336 | + | return 0 | |||
| 337 | + | } | |||
| 338 | + | } | |||