detached 0e75579

fix(input): reliable accelerators + mouse back/forward + alt nav + ctrl-scroll\n\n- Capture-phase key controller for Ctrl±/0 consistency\n- Only consume mouse side buttons 8/9; preserve left/right clicks\n- Add Alt+ArrowLeft/Right navigation and two-finger swipe gesture enablement\n- Add Ctrl+scroll zoom; fix accelerator matching for punctuation (cmdorctrl- and cmdorctrl+=)\n- Remove layout-specific hacks; add raw minus diagnostics

commit
0e75579e2bea5a8e87984700b40a2c518eea1866
author
bamen · · 2025-09-09 18:55 UTC
committer
bamen · · 2025-09-09 17:56 UTC
parent
606e951
signature
unsigned
3 files · +111 -5
M pkg/webkit/keyboard.go +2 -0
@@ -32,3 +32,5 @@ func getShortcutRegistry(w *WebView) shortcutRegistry {
3232 }
3333 return reg
3434 }
35+
36+// No layout-specific remaps in non-CGO build either.
@@ -32,3 +32,5 @@ func getShortcutRegistry(w *WebView) shortcutRegistry {
32 }32 }
33 return reg33 return reg
34 }34 }
35+
36+// No layout-specific remaps in non-CGO build either.
M pkg/webkit/keyboard_cgo.go +86 -5
@@ -10,6 +10,7 @@ package webkit
1010 // Declarations for Go callbacks
1111 extern void goOnKeyPress(unsigned long id, unsigned int keyval, GdkModifierType state);
1212 extern void goOnButtonPress(unsigned long id, unsigned int button, GdkModifierType state);
13+extern void goOnScroll(unsigned long id, double dx, double dy, GdkModifierType state);
1314
1415 // Key controller callback -> forward to Go
1516 static gboolean on_key_pressed(GtkEventControllerKey* controller, guint keyval, guint keycode, GdkModifierType state, gpointer user_data) {
@@ -30,6 +31,8 @@ static void on_click_pressed(GtkGestureClick* gesture, gint n_press, gdouble x,
3031 static void attach_key_controller(GtkWidget* widget, unsigned long id) {
3132 if (!widget) return;
3233 GtkEventController* keyc = gtk_event_controller_key_new();
34+ // Capture phase so we see keys even if child consumes them (e.g., WebKit)
35+ gtk_event_controller_set_propagation_phase(keyc, GTK_PHASE_CAPTURE);
3336 g_signal_connect_data(keyc, "key-pressed", G_CALLBACK(on_key_pressed), (gpointer)id, NULL, 0);
3437 gtk_widget_add_controller(widget, keyc);
3538 }
@@ -40,6 +43,37 @@ static void attach_mouse_gesture(GtkWidget* widget, unsigned long id) {
4043 g_signal_connect_data(click, "pressed", G_CALLBACK(on_click_pressed), (gpointer)id, NULL, 0);
4144 gtk_widget_add_controller(widget, GTK_EVENT_CONTROLLER(click));
4245 }
46+
47+// Legacy controller to capture side buttons (8/9) reliably in GTK4
48+static gboolean on_legacy_event(GtkEventController* controller, GdkEvent* event, gpointer user_data) {
49+ (void)controller;
50+ if (!event) return FALSE;
51+ GdkEventType type = gdk_event_get_event_type(event);
52+ if (type == GDK_BUTTON_PRESS) {
53+ guint button = gdk_button_event_get_button(event);
54+ GdkModifierType state = gdk_event_get_modifier_state(event);
55+ if (button == 8 || button == 9) {
56+ goOnButtonPress((unsigned long)user_data, button, state);
57+ return TRUE; // consume side buttons
58+ }
59+ return FALSE; // let WebKit handle normal clicks
60+ } else if (type == GDK_SCROLL) {
61+ double dx = 0.0, dy = 0.0;
62+ gdk_scroll_event_get_deltas(event, &dx, &dy);
63+ GdkModifierType state = gdk_event_get_modifier_state(event);
64+ goOnScroll((unsigned long)user_data, dx, dy, state);
65+ return FALSE; // don't steal regular scrolling
66+ }
67+ return FALSE;
68+}
69+
70+static void attach_mouse_legacy(GtkWidget* widget, unsigned long id) {
71+ if (!widget) return;
72+ GtkEventController* legacy = gtk_event_controller_legacy_new();
73+ gtk_event_controller_set_propagation_phase(legacy, GTK_PHASE_CAPTURE);
74+ g_signal_connect_data(legacy, "event", G_CALLBACK(on_legacy_event), (gpointer)id, NULL, 0);
75+ gtk_widget_add_controller(widget, legacy);
76+}
4377 */
4478 import "C"
4579
@@ -62,6 +96,8 @@ var (
6296 lastKeyTime = make(map[uintptr]map[string]time.Time)
6397 )
6498
99+// Locale hint function removed; no layout-specific remaps.
100+
65101 // RegisterKeyboardShortcut registers a callback under an accelerator string.
66102 func (w *WebView) RegisterKeyboardShortcut(accel string, callback func()) error {
67103 if w == nil || w.destroyed {
@@ -90,28 +126,42 @@ func goOnKeyPress(id C.ulong, keyval C.guint, state C.GdkModifierType) {
90126
91127 func dispatchAccelerator(uid uintptr, keyval uint, state uint) {
92128 ctrl := (state & uint(C.GDK_CONTROL_MASK)) != 0
129+ alt := (state & uint(C.GDK_ALT_MASK)) != 0
93130 // Map keyval to string names
94131 var keyName string
95132 switch keyval {
96- case uint(C.GDK_KEY_plus), uint(C.GDK_KEY_KP_Add):
97- keyName = "plus"
98- case uint(C.GDK_KEY_equal):
99- keyName = "="
100133 case uint(C.GDK_KEY_minus), uint(C.GDK_KEY_KP_Subtract), uint(C.GDK_KEY_underscore):
101- // Treat underscore as minus for layouts where '-' requires Shift (e.g., AZERTY)
134+ // Normalize minus; log raw detection for diagnostics
135+ log.Printf("[accelerator-raw] minus key detected ctrl=%v alt=%v keyval=0x%x", ctrl, alt, keyval)
102136 keyName = "-"
137+ case uint(C.GDK_KEY_equal), uint(C.GDK_KEY_KP_Add):
138+ keyName = "="
103139 case uint(C.GDK_KEY_0), uint(C.GDK_KEY_KP_0):
104140 keyName = "0"
141+ case uint(C.GDK_KEY_Left):
142+ keyName = "ArrowLeft"
143+ case uint(C.GDK_KEY_Right):
144+ keyName = "ArrowRight"
105145 case uint(C.GDK_KEY_F12):
106146 keyName = "F12"
107147 default:
148+ // If Control held, log unknown keyval for diagnostics
149+ if ctrl {
150+ log.Printf("[accelerator-miss] ctrl keyval=0x%x", keyval)
151+ }
108152 return
109153 }
110154
111155 // Candidate accelerator strings in order
112156 candidates := []string{keyName}
113157 if ctrl {
158+ // Common style: modifier + '+' + keyName (e.g., cmdorctrl+=)
114159 candidates = append([]string{"cmdorctrl+" + keyName, "ctrl+" + keyName}, candidates...)
160+ // Also accept style without the extra '+' for punctuation keys (e.g., cmdorctrl-)
161+ candidates = append([]string{"cmdorctrl" + keyName, "ctrl" + keyName}, candidates...)
162+ }
163+ if alt {
164+ candidates = append([]string{"alt+" + keyName}, candidates...)
115165 }
116166
117167 regMu.RLock()
@@ -168,9 +218,40 @@ func AttachKeyboardControllers(w *WebView) {
168218 C.attach_key_controller(w.native.win, C.ulong(w.id))
169219 }
170220 C.attach_mouse_gesture(w.native.view, C.ulong(w.id))
221+ // Legacy controller captures raw button presses (8/9) for back/forward reliably
222+ C.attach_mouse_legacy(w.native.view, C.ulong(w.id))
171223 log.Printf("[input] GTK4 key/mouse controllers attached")
172224 }
173225
226+//export goOnScroll
227+func goOnScroll(id C.ulong, dx C.double, dy C.double, state C.GdkModifierType) {
228+ uid := uintptr(id)
229+ regMu.RLock()
230+ vw := viewByID[uid]
231+ regMu.RUnlock()
232+ if vw == nil { return }
233+ // Ctrl+scroll zooms
234+ if (uint(state) & uint(C.GDK_CONTROL_MASK)) != 0 {
235+ if float64(dy) < 0 {
236+ // scroll up -> zoom in
237+ nz := vw.zoom
238+ if nz <= 0 { nz = 1.0 }
239+ nz *= 1.1
240+ if nz < 0.25 { nz = 0.25 }
241+ if nz > 5.0 { nz = 5.0 }
242+ _ = vw.SetZoom(nz)
243+ } else if float64(dy) > 0 {
244+ // scroll down -> zoom out
245+ nz := vw.zoom
246+ if nz <= 0 { nz = 1.0 }
247+ nz /= 1.1
248+ if nz < 0.25 { nz = 0.25 }
249+ if nz > 5.0 { nz = 5.0 }
250+ _ = vw.SetZoom(nz)
251+ }
252+ }
253+}
254+
174255 //export goOnThemeChanged
175256 func goOnThemeChanged(id C.ulong, preferDark C.int) {
176257 uid := uintptr(id)
@@ -10,6 +10,7 @@ package webkit
10 // Declarations for Go callbacks10 // Declarations for Go callbacks
11 extern void goOnKeyPress(unsigned long id, unsigned int keyval, GdkModifierType state);11 extern void goOnKeyPress(unsigned long id, unsigned int keyval, GdkModifierType state);
12 extern void goOnButtonPress(unsigned long id, unsigned int button, GdkModifierType state);12 extern void goOnButtonPress(unsigned long id, unsigned int button, GdkModifierType state);
13+extern void goOnScroll(unsigned long id, double dx, double dy, GdkModifierType state);
13 14
14 // Key controller callback -> forward to Go15 // Key controller callback -> forward to Go
15 static gboolean on_key_pressed(GtkEventControllerKey* controller, guint keyval, guint keycode, GdkModifierType state, gpointer user_data) {16 static gboolean on_key_pressed(GtkEventControllerKey* controller, guint keyval, guint keycode, GdkModifierType state, gpointer user_data) {
@@ -30,6 +31,8 @@ static void on_click_pressed(GtkGestureClick* gesture, gint n_press, gdouble x,
30 static void attach_key_controller(GtkWidget* widget, unsigned long id) {31 static void attach_key_controller(GtkWidget* widget, unsigned long id) {
31 if (!widget) return;32 if (!widget) return;
32 GtkEventController* keyc = gtk_event_controller_key_new();33 GtkEventController* keyc = gtk_event_controller_key_new();
34+ // Capture phase so we see keys even if child consumes them (e.g., WebKit)
35+ gtk_event_controller_set_propagation_phase(keyc, GTK_PHASE_CAPTURE);
33 g_signal_connect_data(keyc, "key-pressed", G_CALLBACK(on_key_pressed), (gpointer)id, NULL, 0);36 g_signal_connect_data(keyc, "key-pressed", G_CALLBACK(on_key_pressed), (gpointer)id, NULL, 0);
34 gtk_widget_add_controller(widget, keyc);37 gtk_widget_add_controller(widget, keyc);
35 }38 }
@@ -40,6 +43,37 @@ static void attach_mouse_gesture(GtkWidget* widget, unsigned long id) {
40 g_signal_connect_data(click, "pressed", G_CALLBACK(on_click_pressed), (gpointer)id, NULL, 0);43 g_signal_connect_data(click, "pressed", G_CALLBACK(on_click_pressed), (gpointer)id, NULL, 0);
41 gtk_widget_add_controller(widget, GTK_EVENT_CONTROLLER(click));44 gtk_widget_add_controller(widget, GTK_EVENT_CONTROLLER(click));
42 }45 }
46+
47+// Legacy controller to capture side buttons (8/9) reliably in GTK4
48+static gboolean on_legacy_event(GtkEventController* controller, GdkEvent* event, gpointer user_data) {
49+ (void)controller;
50+ if (!event) return FALSE;
51+ GdkEventType type = gdk_event_get_event_type(event);
52+ if (type == GDK_BUTTON_PRESS) {
53+ guint button = gdk_button_event_get_button(event);
54+ GdkModifierType state = gdk_event_get_modifier_state(event);
55+ if (button == 8 || button == 9) {
56+ goOnButtonPress((unsigned long)user_data, button, state);
57+ return TRUE; // consume side buttons
58+ }
59+ return FALSE; // let WebKit handle normal clicks
60+ } else if (type == GDK_SCROLL) {
61+ double dx = 0.0, dy = 0.0;
62+ gdk_scroll_event_get_deltas(event, &dx, &dy);
63+ GdkModifierType state = gdk_event_get_modifier_state(event);
64+ goOnScroll((unsigned long)user_data, dx, dy, state);
65+ return FALSE; // don't steal regular scrolling
66+ }
67+ return FALSE;
68+}
69+
70+static void attach_mouse_legacy(GtkWidget* widget, unsigned long id) {
71+ if (!widget) return;
72+ GtkEventController* legacy = gtk_event_controller_legacy_new();
73+ gtk_event_controller_set_propagation_phase(legacy, GTK_PHASE_CAPTURE);
74+ g_signal_connect_data(legacy, "event", G_CALLBACK(on_legacy_event), (gpointer)id, NULL, 0);
75+ gtk_widget_add_controller(widget, legacy);
76+}
43 */77 */
44 import "C"78 import "C"
45 79
@@ -62,6 +96,8 @@ var (
62 lastKeyTime = make(map[uintptr]map[string]time.Time)96 lastKeyTime = make(map[uintptr]map[string]time.Time)
63 )97 )
64 98
99+// Locale hint function removed; no layout-specific remaps.
100+
65 // RegisterKeyboardShortcut registers a callback under an accelerator string.101 // RegisterKeyboardShortcut registers a callback under an accelerator string.
66 func (w *WebView) RegisterKeyboardShortcut(accel string, callback func()) error {102 func (w *WebView) RegisterKeyboardShortcut(accel string, callback func()) error {
67 if w == nil || w.destroyed {103 if w == nil || w.destroyed {
@@ -90,28 +126,42 @@ func goOnKeyPress(id C.ulong, keyval C.guint, state C.GdkModifierType) {
90 126
91 func dispatchAccelerator(uid uintptr, keyval uint, state uint) {127 func dispatchAccelerator(uid uintptr, keyval uint, state uint) {
92 ctrl := (state & uint(C.GDK_CONTROL_MASK)) != 0128 ctrl := (state & uint(C.GDK_CONTROL_MASK)) != 0
129+ alt := (state & uint(C.GDK_ALT_MASK)) != 0
93 // Map keyval to string names130 // Map keyval to string names
94 var keyName string131 var keyName string
95 switch keyval {132 switch keyval {
96- case uint(C.GDK_KEY_plus), uint(C.GDK_KEY_KP_Add):
97- keyName = "plus"
98- case uint(C.GDK_KEY_equal):
99- keyName = "="
100 case uint(C.GDK_KEY_minus), uint(C.GDK_KEY_KP_Subtract), uint(C.GDK_KEY_underscore):133 case uint(C.GDK_KEY_minus), uint(C.GDK_KEY_KP_Subtract), uint(C.GDK_KEY_underscore):
101- // Treat underscore as minus for layouts where '-' requires Shift (e.g., AZERTY)134+ // Normalize minus; log raw detection for diagnostics
135+ log.Printf("[accelerator-raw] minus key detected ctrl=%v alt=%v keyval=0x%x", ctrl, alt, keyval)
102 keyName = "-"136 keyName = "-"
137+ case uint(C.GDK_KEY_equal), uint(C.GDK_KEY_KP_Add):
138+ keyName = "="
103 case uint(C.GDK_KEY_0), uint(C.GDK_KEY_KP_0):139 case uint(C.GDK_KEY_0), uint(C.GDK_KEY_KP_0):
104 keyName = "0"140 keyName = "0"
141+ case uint(C.GDK_KEY_Left):
142+ keyName = "ArrowLeft"
143+ case uint(C.GDK_KEY_Right):
144+ keyName = "ArrowRight"
105 case uint(C.GDK_KEY_F12):145 case uint(C.GDK_KEY_F12):
106 keyName = "F12"146 keyName = "F12"
107 default:147 default:
148+ // If Control held, log unknown keyval for diagnostics
149+ if ctrl {
150+ log.Printf("[accelerator-miss] ctrl keyval=0x%x", keyval)
151+ }
108 return152 return
109 }153 }
110 154
111 // Candidate accelerator strings in order155 // Candidate accelerator strings in order
112 candidates := []string{keyName}156 candidates := []string{keyName}
113 if ctrl {157 if ctrl {
158+ // Common style: modifier + '+' + keyName (e.g., cmdorctrl+=)
114 candidates = append([]string{"cmdorctrl+" + keyName, "ctrl+" + keyName}, candidates...)159 candidates = append([]string{"cmdorctrl+" + keyName, "ctrl+" + keyName}, candidates...)
160+ // Also accept style without the extra '+' for punctuation keys (e.g., cmdorctrl-)
161+ candidates = append([]string{"cmdorctrl" + keyName, "ctrl" + keyName}, candidates...)
162+ }
163+ if alt {
164+ candidates = append([]string{"alt+" + keyName}, candidates...)
115 }165 }
116 166
117 regMu.RLock()167 regMu.RLock()
@@ -168,9 +218,40 @@ func AttachKeyboardControllers(w *WebView) {
168 C.attach_key_controller(w.native.win, C.ulong(w.id))218 C.attach_key_controller(w.native.win, C.ulong(w.id))
169 }219 }
170 C.attach_mouse_gesture(w.native.view, C.ulong(w.id))220 C.attach_mouse_gesture(w.native.view, C.ulong(w.id))
221+ // Legacy controller captures raw button presses (8/9) for back/forward reliably
222+ C.attach_mouse_legacy(w.native.view, C.ulong(w.id))
171 log.Printf("[input] GTK4 key/mouse controllers attached")223 log.Printf("[input] GTK4 key/mouse controllers attached")
172 }224 }
173 225
226+//export goOnScroll
227+func goOnScroll(id C.ulong, dx C.double, dy C.double, state C.GdkModifierType) {
228+ uid := uintptr(id)
229+ regMu.RLock()
230+ vw := viewByID[uid]
231+ regMu.RUnlock()
232+ if vw == nil { return }
233+ // Ctrl+scroll zooms
234+ if (uint(state) & uint(C.GDK_CONTROL_MASK)) != 0 {
235+ if float64(dy) < 0 {
236+ // scroll up -> zoom in
237+ nz := vw.zoom
238+ if nz <= 0 { nz = 1.0 }
239+ nz *= 1.1
240+ if nz < 0.25 { nz = 0.25 }
241+ if nz > 5.0 { nz = 5.0 }
242+ _ = vw.SetZoom(nz)
243+ } else if float64(dy) > 0 {
244+ // scroll down -> zoom out
245+ nz := vw.zoom
246+ if nz <= 0 { nz = 1.0 }
247+ nz /= 1.1
248+ if nz < 0.25 { nz = 0.25 }
249+ if nz > 5.0 { nz = 5.0 }
250+ _ = vw.SetZoom(nz)
251+ }
252+ }
253+}
254+
174 //export goOnThemeChanged255 //export goOnThemeChanged
175 func goOnThemeChanged(id C.ulong, preferDark C.int) {256 func goOnThemeChanged(id C.ulong, preferDark C.int) {
176 uid := uintptr(id)257 uid := uintptr(id)
M pkg/webkit/webview_cgo.go +23 -0
@@ -204,6 +204,16 @@ static void maybe_set_media_user_gesture(WebKitSettings* settings, int require)
204204 #endif
205205 }
206206
207+// Enable two-finger swipe back/forward navigation gestures when supported
208+static void maybe_set_back_forward_gestures(WebKitSettings* settings, int enable) {
209+#if WEBKIT_CHECK_VERSION(2,38,0)
210+ if (!settings) return;
211+ webkit_settings_set_enable_back_forward_navigation_gestures(settings, enable ? 1 : 0);
212+#else
213+ (void)settings; (void)enable;
214+#endif
215+}
216+
207217 // Set cookie accept policy with compile-time compatibility across WebKit versions
208218 static void maybe_set_cookie_policy(WebKitCookieManager* cm, int policy) {
209219 if (!cm) return;
@@ -282,6 +292,7 @@ type WebView struct {
282292 msgHandler func(payload string)
283293 titleHandler func(title string)
284294 uriHandler func(uri string)
295+ zoomHandler func(level float64)
285296 }
286297
287298 // NewWebView constructs a new WebView instance with native WebKit2GTK widgets.
@@ -387,6 +398,9 @@ func NewWebView(cfg *Config) (*WebView, error) {
387398 _ = v.SetZoom(nz)
388399 })
389400 _ = v.RegisterKeyboardShortcut("cmdorctrl+0", func() { _ = v.SetZoom(1.0) })
401+ // Navigation with Alt+Arrow keys
402+ _ = v.RegisterKeyboardShortcut("alt+ArrowLeft", func() { _ = v.GoBack() })
403+ _ = v.RegisterKeyboardShortcut("alt+ArrowRight", func() { _ = v.GoForward() })
390404 // Update window title when page title changes
391405 cNotifyTitle1 := C.CString("notify::title")
392406 C.g_signal_connect_data(C.gpointer(unsafe.Pointer(viewWidget)), cNotifyTitle1, C.GCallback(C.on_title_notify), C.gpointer(unsafe.Pointer(win)), nil, 0)
@@ -420,6 +434,8 @@ func NewWebView(cfg *Config) (*WebView, error) {
420434 if cfg.Rendering.DebugGPU { C.maybe_set_draw_indicators(settings, 1) }
421435 // Reduce media pipeline churn by requiring a user gesture for playback
422436 C.maybe_set_media_user_gesture(settings, 1)
437+ // Enable trackpad back/forward gestures when available
438+ C.maybe_set_back_forward_gestures(settings, 1)
423439 }
424440
425441 if cfg.ZoomDefault > 0 {
@@ -559,6 +575,13 @@ func (w *WebView) dispatchURIChanged(uri string) {
559575 if w != nil && w.uriHandler != nil { w.uriHandler(uri) }
560576 }
561577
578+// RegisterZoomChangedHandler registers a callback invoked when zoom level changes.
579+func (w *WebView) RegisterZoomChangedHandler(cb func(level float64)) { w.zoomHandler = cb }
580+
581+func (w *WebView) dispatchZoomChanged(level float64) {
582+ if w != nil && w.zoomHandler != nil { w.zoomHandler(level) }
583+}
584+
562585 // enableUserContentManager registers the 'dumber' message handler and injects the omnibox script.
563586 func (w *WebView) enableUserContentManager() {
564587 if w == nil || w.native == nil || w.native.ucm == nil { return }
@@ -204,6 +204,16 @@ static void maybe_set_media_user_gesture(WebKitSettings* settings, int require)
204 #endif204 #endif
205 }205 }
206 206
207+// Enable two-finger swipe back/forward navigation gestures when supported
208+static void maybe_set_back_forward_gestures(WebKitSettings* settings, int enable) {
209+#if WEBKIT_CHECK_VERSION(2,38,0)
210+ if (!settings) return;
211+ webkit_settings_set_enable_back_forward_navigation_gestures(settings, enable ? 1 : 0);
212+#else
213+ (void)settings; (void)enable;
214+#endif
215+}
216+
207 // Set cookie accept policy with compile-time compatibility across WebKit versions217 // Set cookie accept policy with compile-time compatibility across WebKit versions
208 static void maybe_set_cookie_policy(WebKitCookieManager* cm, int policy) {218 static void maybe_set_cookie_policy(WebKitCookieManager* cm, int policy) {
209 if (!cm) return;219 if (!cm) return;
@@ -282,6 +292,7 @@ type WebView struct {
282 msgHandler func(payload string)292 msgHandler func(payload string)
283 titleHandler func(title string)293 titleHandler func(title string)
284 uriHandler func(uri string)294 uriHandler func(uri string)
295+ zoomHandler func(level float64)
285 }296 }
286 297
287 // NewWebView constructs a new WebView instance with native WebKit2GTK widgets.298 // NewWebView constructs a new WebView instance with native WebKit2GTK widgets.
@@ -387,6 +398,9 @@ func NewWebView(cfg *Config) (*WebView, error) {
387 _ = v.SetZoom(nz)398 _ = v.SetZoom(nz)
388 })399 })
389 _ = v.RegisterKeyboardShortcut("cmdorctrl+0", func() { _ = v.SetZoom(1.0) })400 _ = v.RegisterKeyboardShortcut("cmdorctrl+0", func() { _ = v.SetZoom(1.0) })
401+ // Navigation with Alt+Arrow keys
402+ _ = v.RegisterKeyboardShortcut("alt+ArrowLeft", func() { _ = v.GoBack() })
403+ _ = v.RegisterKeyboardShortcut("alt+ArrowRight", func() { _ = v.GoForward() })
390 // Update window title when page title changes404 // Update window title when page title changes
391 cNotifyTitle1 := C.CString("notify::title")405 cNotifyTitle1 := C.CString("notify::title")
392 C.g_signal_connect_data(C.gpointer(unsafe.Pointer(viewWidget)), cNotifyTitle1, C.GCallback(C.on_title_notify), C.gpointer(unsafe.Pointer(win)), nil, 0)406 C.g_signal_connect_data(C.gpointer(unsafe.Pointer(viewWidget)), cNotifyTitle1, C.GCallback(C.on_title_notify), C.gpointer(unsafe.Pointer(win)), nil, 0)
@@ -420,6 +434,8 @@ func NewWebView(cfg *Config) (*WebView, error) {
420 if cfg.Rendering.DebugGPU { C.maybe_set_draw_indicators(settings, 1) }434 if cfg.Rendering.DebugGPU { C.maybe_set_draw_indicators(settings, 1) }
421 // Reduce media pipeline churn by requiring a user gesture for playback435 // Reduce media pipeline churn by requiring a user gesture for playback
422 C.maybe_set_media_user_gesture(settings, 1)436 C.maybe_set_media_user_gesture(settings, 1)
437+ // Enable trackpad back/forward gestures when available
438+ C.maybe_set_back_forward_gestures(settings, 1)
423 }439 }
424 440
425 if cfg.ZoomDefault > 0 {441 if cfg.ZoomDefault > 0 {
@@ -559,6 +575,13 @@ func (w *WebView) dispatchURIChanged(uri string) {
559 if w != nil && w.uriHandler != nil { w.uriHandler(uri) }575 if w != nil && w.uriHandler != nil { w.uriHandler(uri) }
560 }576 }
561 577
578+// RegisterZoomChangedHandler registers a callback invoked when zoom level changes.
579+func (w *WebView) RegisterZoomChangedHandler(cb func(level float64)) { w.zoomHandler = cb }
580+
581+func (w *WebView) dispatchZoomChanged(level float64) {
582+ if w != nil && w.zoomHandler != nil { w.zoomHandler(level) }
583+}
584+
562 // enableUserContentManager registers the 'dumber' message handler and injects the omnibox script.585 // enableUserContentManager registers the 'dumber' message handler and injects the omnibox script.
563 func (w *WebView) enableUserContentManager() {586 func (w *WebView) enableUserContentManager() {
564 if w == nil || w.native == nil || w.native.ucm == nil { return }587 if w == nil || w.native == nil || w.native.ucm == nil { return }

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