detached 3a799c6

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 VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · 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
33 go 1.26.1
44
55 require (
6+ github.com/bnema/purego-cef v0.0.0-00010101000000-000000000000
67 github.com/bnema/purego-sqlite v0.1.1
78 github.com/bnema/puregotk v0.2.2
89 github.com/bnema/puregotk-webkit v0.0.16
@@ -39,6 +40,7 @@ require (
3940 github.com/clipperhouse/uax29/v2 v2.5.0 // indirect
4041 github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
4142 github.com/davecgh/go-spew v1.1.1 // indirect
43+ github.com/ebitengine/purego v0.10.0 // indirect
4244 github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
4345 github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
4446 github.com/inconshreveable/mousetrap v1.1.0 // indirect
@@ -3,6 +3,7 @@ module github.com/bnema/dumber
3 go 1.26.13 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.17 github.com/bnema/purego-sqlite v0.1.1
7 github.com/bnema/puregotk v0.2.28 github.com/bnema/puregotk v0.2.2
8 github.com/bnema/puregotk-webkit v0.0.169 github.com/bnema/puregotk-webkit v0.0.16
@@ -39,6 +40,7 @@ require (
39 github.com/clipperhouse/uax29/v2 v2.5.0 // indirect40 github.com/clipperhouse/uax29/v2 v2.5.0 // indirect
40 github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect41 github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
41 github.com/davecgh/go-spew v1.1.1 // indirect42 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 // indirect44 github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
43 github.com/go-viper/mapstructure/v2 v2.5.0 // indirect45 github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
44 github.com/inconshreveable/mousetrap v1.1.0 // indirect46 github.com/inconshreveable/mousetrap v1.1.0 // indirect
M go.sum +2 -0 generated file: collapsed
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+}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close