detached fdb2a2f

fix(cef): restore keyboard focus with popup overlays

commit
fdb2a2f6634e8c772ee167ecb6ccf5437c1dd97e
author
brice · · 2026-05-21 16:07 UTC
parent
8f62984
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
7 files · +96 -57
M go.mod +2 -0
@@ -84,3 +84,5 @@ require (
8484
8585 tool github.com/a-h/templ/cmd/templ
8686
87+replace github.com/bnema/purego-cef2gtk => ../purego-cef2gtk
88+
@@ -84,3 +84,5 @@ require (
84 84
85 tool github.com/a-h/templ/cmd/templ85 tool github.com/a-h/templ/cmd/templ
86 86
87+replace github.com/bnema/purego-cef2gtk => ../purego-cef2gtk
88+
M internal/infrastructure/cef/cef2gtk_adapter.go +45 -12
@@ -25,10 +25,11 @@ var ErrAdapterDestroyed = errors.New("cef2gtk_adapter: adapter is destroyed")
2525 // The adapter does NOT own GL/PBO staging, dirty-rect uploads, or GTK input
2626 // controller implementation — those belong to purego-cef2gtk.
2727 type Cef2gtkAdapter struct {
28- viewMu sync.RWMutex
29- view *cef2gtk.View
30- destroyed atomic.Bool
31- destroyCnt atomic.Uint64
28+ viewMu sync.RWMutex
29+ view *cef2gtk.View
30+ inputTargetWidget *gtk.Widget
31+ destroyed atomic.Bool
32+ destroyCnt atomic.Uint64
3233 }
3334
3435 // NewCef2gtkAdapter creates an accelerated CEF view and wraps it in a thin
@@ -137,8 +138,8 @@ func (a *Cef2gtkAdapter) AddSizeObserver(fn func(width, height int32)) func() {
137138 return a.view.AddSizeObserver(fn)
138139 }
139140
140-// HasFocus reports whether the bridge widget currently has GTK focus. Must be
141-// called on the GTK main thread.
141+// HasFocus reports whether the effective GTK input target currently has focus.
142+// Must be called on the GTK main thread.
142143 func (a *Cef2gtkAdapter) HasFocus() bool {
143144 if a == nil || a.destroyed.Load() {
144145 return false
@@ -148,6 +149,9 @@ func (a *Cef2gtkAdapter) HasFocus() bool {
148149 if a.view == nil {
149150 return false
150151 }
152+ if a.inputTargetWidget != nil {
153+ return a.inputTargetWidget.HasFocus()
154+ }
151155 return a.view.HasFocus()
152156 }
153157
@@ -243,15 +247,41 @@ func (a *Cef2gtkAdapter) RecordExternalBeginFrameSent() {
243247 //
244248 // opts.Scale configures HiDPI scale for pointer coordinate translation.
245249 func (a *Cef2gtkAdapter) AttachInput(host purecef.BrowserHost, opts cef2gtk.InputOptions) error {
250+ return a.AttachInputToWidget(host, nil, opts)
251+}
252+
253+// AttachInputToWidget attaches GTK event controllers to the specified target
254+// widget and forwards input to the given CEF browser host. When targetWidget is
255+// nil, it falls back to the bridge render widget. Must be called on the GTK
256+// main thread.
257+func (a *Cef2gtkAdapter) AttachInputToWidget(host purecef.BrowserHost, targetWidget *gtk.Widget, opts cef2gtk.InputOptions) error {
246258 if a == nil || a.destroyed.Load() {
247259 return ErrAdapterDestroyed
248260 }
249- a.viewMu.RLock()
250- defer a.viewMu.RUnlock()
261+ a.viewMu.Lock()
262+ defer a.viewMu.Unlock()
251263 if a.view == nil {
252264 return ErrAdapterDestroyed
253265 }
254- return a.view.AttachInput(host, opts)
266+ effectiveTarget := targetWidget
267+ if effectiveTarget == nil {
268+ effectiveTarget = a.view.Widget()
269+ }
270+ type inputWidgetAttacher interface {
271+ AttachInputToWidget(purecef.BrowserHost, *gtk.Widget, cef2gtk.InputOptions) error
272+ }
273+ var err error
274+ if attacher, ok := any(a.view).(inputWidgetAttacher); ok {
275+ err = attacher.AttachInputToWidget(host, effectiveTarget, opts)
276+ } else {
277+ err = a.view.AttachInput(host, opts)
278+ }
279+ if err != nil {
280+ a.inputTargetWidget = nil
281+ return err
282+ }
283+ a.inputTargetWidget = effectiveTarget
284+ return nil
255285 }
256286
257287 // SetInputHost updates the CEF browser host used by the attached input bridge.
@@ -275,12 +305,14 @@ func (a *Cef2gtkAdapter) DetachInput() error {
275305 if a == nil || a.destroyed.Load() {
276306 return ErrAdapterDestroyed
277307 }
278- a.viewMu.RLock()
279- defer a.viewMu.RUnlock()
308+ a.viewMu.Lock()
309+ defer a.viewMu.Unlock()
280310 if a.view == nil {
281311 return ErrAdapterDestroyed
282312 }
283- return a.view.DetachInput()
313+ err := a.view.DetachInput()
314+ a.inputTargetWidget = nil
315+ return err
284316 }
285317
286318 // Diagnostics returns a point-in-time snapshot of bridge rendering diagnostics
@@ -311,6 +343,7 @@ func (a *Cef2gtkAdapter) Destroy() error {
311343 }
312344 err := a.view.Destroy()
313345 a.view = nil
346+ a.inputTargetWidget = nil
314347 a.destroyCnt.Add(1)
315348 return err
316349 }
@@ -25,10 +25,11 @@ var ErrAdapterDestroyed = errors.New("cef2gtk_adapter: adapter is destroyed")
25 // The adapter does NOT own GL/PBO staging, dirty-rect uploads, or GTK input25 // The adapter does NOT own GL/PBO staging, dirty-rect uploads, or GTK input
26 // controller implementation — those belong to purego-cef2gtk.26 // controller implementation — those belong to purego-cef2gtk.
27 type Cef2gtkAdapter struct {27 type Cef2gtkAdapter struct {
28- viewMu sync.RWMutex28+ viewMu sync.RWMutex
29- view *cef2gtk.View29+ view *cef2gtk.View
30- destroyed atomic.Bool30+ inputTargetWidget *gtk.Widget
31- destroyCnt atomic.Uint6431+ destroyed atomic.Bool
32+ destroyCnt atomic.Uint64
32 }33 }
33 34
34 // NewCef2gtkAdapter creates an accelerated CEF view and wraps it in a thin35 // NewCef2gtkAdapter creates an accelerated CEF view and wraps it in a thin
@@ -137,8 +138,8 @@ func (a *Cef2gtkAdapter) AddSizeObserver(fn func(width, height int32)) func() {
137 return a.view.AddSizeObserver(fn)138 return a.view.AddSizeObserver(fn)
138 }139 }
139 140
140-// HasFocus reports whether the bridge widget currently has GTK focus. Must be141+// HasFocus reports whether the effective GTK input target currently has focus.
141-// called on the GTK main thread.142+// Must be called on the GTK main thread.
142 func (a *Cef2gtkAdapter) HasFocus() bool {143 func (a *Cef2gtkAdapter) HasFocus() bool {
143 if a == nil || a.destroyed.Load() {144 if a == nil || a.destroyed.Load() {
144 return false145 return false
@@ -148,6 +149,9 @@ func (a *Cef2gtkAdapter) HasFocus() bool {
148 if a.view == nil {149 if a.view == nil {
149 return false150 return false
150 }151 }
152+ if a.inputTargetWidget != nil {
153+ return a.inputTargetWidget.HasFocus()
154+ }
151 return a.view.HasFocus()155 return a.view.HasFocus()
152 }156 }
153 157
@@ -243,15 +247,41 @@ func (a *Cef2gtkAdapter) RecordExternalBeginFrameSent() {
243 //247 //
244 // opts.Scale configures HiDPI scale for pointer coordinate translation.248 // opts.Scale configures HiDPI scale for pointer coordinate translation.
245 func (a *Cef2gtkAdapter) AttachInput(host purecef.BrowserHost, opts cef2gtk.InputOptions) error {249 func (a *Cef2gtkAdapter) AttachInput(host purecef.BrowserHost, opts cef2gtk.InputOptions) error {
250+ return a.AttachInputToWidget(host, nil, opts)
251+}
252+
253+// AttachInputToWidget attaches GTK event controllers to the specified target
254+// widget and forwards input to the given CEF browser host. When targetWidget is
255+// nil, it falls back to the bridge render widget. Must be called on the GTK
256+// main thread.
257+func (a *Cef2gtkAdapter) AttachInputToWidget(host purecef.BrowserHost, targetWidget *gtk.Widget, opts cef2gtk.InputOptions) error {
246 if a == nil || a.destroyed.Load() {258 if a == nil || a.destroyed.Load() {
247 return ErrAdapterDestroyed259 return ErrAdapterDestroyed
248 }260 }
249- a.viewMu.RLock()261+ a.viewMu.Lock()
250- defer a.viewMu.RUnlock()262+ defer a.viewMu.Unlock()
251 if a.view == nil {263 if a.view == nil {
252 return ErrAdapterDestroyed264 return ErrAdapterDestroyed
253 }265 }
254- return a.view.AttachInput(host, opts)266+ effectiveTarget := targetWidget
267+ if effectiveTarget == nil {
268+ effectiveTarget = a.view.Widget()
269+ }
270+ type inputWidgetAttacher interface {
271+ AttachInputToWidget(purecef.BrowserHost, *gtk.Widget, cef2gtk.InputOptions) error
272+ }
273+ var err error
274+ if attacher, ok := any(a.view).(inputWidgetAttacher); ok {
275+ err = attacher.AttachInputToWidget(host, effectiveTarget, opts)
276+ } else {
277+ err = a.view.AttachInput(host, opts)
278+ }
279+ if err != nil {
280+ a.inputTargetWidget = nil
281+ return err
282+ }
283+ a.inputTargetWidget = effectiveTarget
284+ return nil
255 }285 }
256 286
257 // SetInputHost updates the CEF browser host used by the attached input bridge.287 // SetInputHost updates the CEF browser host used by the attached input bridge.
@@ -275,12 +305,14 @@ func (a *Cef2gtkAdapter) DetachInput() error {
275 if a == nil || a.destroyed.Load() {305 if a == nil || a.destroyed.Load() {
276 return ErrAdapterDestroyed306 return ErrAdapterDestroyed
277 }307 }
278- a.viewMu.RLock()308+ a.viewMu.Lock()
279- defer a.viewMu.RUnlock()309+ defer a.viewMu.Unlock()
280 if a.view == nil {310 if a.view == nil {
281 return ErrAdapterDestroyed311 return ErrAdapterDestroyed
282 }312 }
283- return a.view.DetachInput()313+ err := a.view.DetachInput()
314+ a.inputTargetWidget = nil
315+ return err
284 }316 }
285 317
286 // Diagnostics returns a point-in-time snapshot of bridge rendering diagnostics318 // Diagnostics returns a point-in-time snapshot of bridge rendering diagnostics
@@ -311,6 +343,7 @@ func (a *Cef2gtkAdapter) Destroy() error {
311 }343 }
312 err := a.view.Destroy()344 err := a.view.Destroy()
313 a.view = nil345 a.view = nil
346+ a.inputTargetWidget = nil
314 a.destroyCnt.Add(1)347 a.destroyCnt.Add(1)
315 return err348 return err
316 }349 }
M internal/infrastructure/cef/popup_surface.go +2 -1
@@ -61,8 +61,9 @@ func newPopupBridgeSurface(ctx context.Context, mainWidget *gtk.Widget, plan cef
6161 root := &overlay.Widget
6262 root.SetHexpand(true)
6363 root.SetVexpand(true)
64+ root.SetCanFocus(true)
65+ root.SetFocusable(true)
6466 root.SetFocusOnClick(true)
65- root.SetFocusChild(mainWidget)
6667 return &popupBridgeSurface{
6768 ctx: ctx,
6869 root: root,
@@ -61,8 +61,9 @@ func newPopupBridgeSurface(ctx context.Context, mainWidget *gtk.Widget, plan cef
61 root := &overlay.Widget61 root := &overlay.Widget
62 root.SetHexpand(true)62 root.SetHexpand(true)
63 root.SetVexpand(true)63 root.SetVexpand(true)
64+ root.SetCanFocus(true)
65+ root.SetFocusable(true)
64 root.SetFocusOnClick(true)66 root.SetFocusOnClick(true)
65- root.SetFocusChild(mainWidget)
66 return &popupBridgeSurface{67 return &popupBridgeSurface{
67 ctx: ctx,68 ctx: ctx,
68 root: root,69 root: root,
M internal/infrastructure/cef/webview.go +18 -0
@@ -1413,6 +1413,24 @@ func (wv *WebView) selectedTextSnapshot() string {
14131413 return wv.selectedText
14141414 }
14151415
1416+func (wv *WebView) bridgeInputOptions() cef2gtk.InputOptions {
1417+ if wv == nil {
1418+ return cef2gtk.InputOptions{}
1419+ }
1420+ return cef2gtk.InputOptions{
1421+ Scale: 0,
1422+ OnMiddleClick: func(_, _ float64) bool {
1423+ return wv.handleMiddleClickFromBridge()
1424+ },
1425+ SelectionText: wv.selectedTextSnapshot,
1426+ OnClipboardShortcut: func(action, text string) {
1427+ if wv.engine != nil {
1428+ wv.engine.handleExplicitClipboardBridgeText(wv.id, action, text)
1429+ }
1430+ },
1431+ }
1432+}
1433+
14161434 func (wv *WebView) viewBridgeScale() float64 {
14171435 if wv == nil || wv.viewBridge == nil {
14181436 return 1
@@ -1413,6 +1413,24 @@ func (wv *WebView) selectedTextSnapshot() string {
1413 return wv.selectedText1413 return wv.selectedText
1414 }1414 }
1415 1415
1416+func (wv *WebView) bridgeInputOptions() cef2gtk.InputOptions {
1417+ if wv == nil {
1418+ return cef2gtk.InputOptions{}
1419+ }
1420+ return cef2gtk.InputOptions{
1421+ Scale: 0,
1422+ OnMiddleClick: func(_, _ float64) bool {
1423+ return wv.handleMiddleClickFromBridge()
1424+ },
1425+ SelectionText: wv.selectedTextSnapshot,
1426+ OnClipboardShortcut: func(action, text string) {
1427+ if wv.engine != nil {
1428+ wv.engine.handleExplicitClipboardBridgeText(wv.id, action, text)
1429+ }
1430+ },
1431+ }
1432+}
1433+
1416 func (wv *WebView) viewBridgeScale() float64 {1434 func (wv *WebView) viewBridgeScale() float64 {
1417 if wv == nil || wv.viewBridge == nil {1435 if wv == nil || wv.viewBridge == nil {
1418 return 11436 return 1
M internal/infrastructure/cef/webview_handlers.go +1 -13
@@ -6,7 +6,6 @@ import (
66 "sync"
77
88 purecef "github.com/bnema/purego-cef/cef"
9- cef2gtk "github.com/bnema/purego-cef2gtk"
109
1110 "github.com/bnema/dumber/internal/application/dto"
1211 "github.com/bnema/dumber/internal/application/port"
@@ -640,18 +639,7 @@ func (h *handlerSet) attachAfterCreatedBrowser(
640639 wv.handleInputAttachFailure(ErrAdapterDestroyed, host)
641640 return
642641 }
643- if err := wv.viewBridge.AttachInput(host, cef2gtk.InputOptions{
644- Scale: 0,
645- OnMiddleClick: func(_, _ float64) bool {
646- return wv.handleMiddleClickFromBridge()
647- },
648- SelectionText: wv.selectedTextSnapshot,
649- OnClipboardShortcut: func(action, text string) {
650- if wv.engine != nil {
651- wv.engine.handleExplicitClipboardBridgeText(wv.id, action, text)
652- }
653- },
654- }); err != nil {
642+ if err := wv.viewBridge.AttachInputToWidget(host, wv.nativeWidget, wv.bridgeInputOptions()); err != nil {
655643 wv.handleInputAttachFailure(err, host)
656644 return
657645 }
@@ -6,7 +6,6 @@ import (
6 "sync"6 "sync"
7 7
8 purecef "github.com/bnema/purego-cef/cef"8 purecef "github.com/bnema/purego-cef/cef"
9- cef2gtk "github.com/bnema/purego-cef2gtk"
10 9
11 "github.com/bnema/dumber/internal/application/dto"10 "github.com/bnema/dumber/internal/application/dto"
12 "github.com/bnema/dumber/internal/application/port"11 "github.com/bnema/dumber/internal/application/port"
@@ -640,18 +639,7 @@ func (h *handlerSet) attachAfterCreatedBrowser(
640 wv.handleInputAttachFailure(ErrAdapterDestroyed, host)639 wv.handleInputAttachFailure(ErrAdapterDestroyed, host)
641 return640 return
642 }641 }
643- if err := wv.viewBridge.AttachInput(host, cef2gtk.InputOptions{642+ if err := wv.viewBridge.AttachInputToWidget(host, wv.nativeWidget, wv.bridgeInputOptions()); err != nil {
644- Scale: 0,
645- OnMiddleClick: func(_, _ float64) bool {
646- return wv.handleMiddleClickFromBridge()
647- },
648- SelectionText: wv.selectedTextSnapshot,
649- OnClipboardShortcut: func(action, text string) {
650- if wv.engine != nil {
651- wv.engine.handleExplicitClipboardBridgeText(wv.id, action, text)
652- }
653- },
654- }); err != nil {
655 wv.handleInputAttachFailure(err, host)643 wv.handleInputAttachFailure(err, host)
656 return644 return
657 }645 }
A internal/infrastructure/cef/webview_input_test.go +21 -0
@@ -0,0 +1,21 @@
1+package cef
2+
3+import (
4+ "testing"
5+
6+ "github.com/bnema/puregotk/v4/gtk"
7+ "github.com/stretchr/testify/require"
8+)
9+
10+func TestWebViewBridgeInputOptions_LeavesTargetSelectionToAdapter(t *testing.T) {
11+ nativeWidget := &gtk.Widget{}
12+ wv := &WebView{nativeWidget: nativeWidget}
13+
14+ opts := wv.bridgeInputOptions()
15+
16+ require.Zero(t, opts.Scale)
17+ require.NotNil(t, opts.OnMiddleClick)
18+ require.NotNil(t, opts.SelectionText)
19+ require.NotNil(t, opts.OnClipboardShortcut)
20+ require.Same(t, nativeWidget, wv.nativeWidget)
21+}
@@ -0,0 +1,21 @@
1+package cef
2+
3+import (
4+ "testing"
5+
6+ "github.com/bnema/puregotk/v4/gtk"
7+ "github.com/stretchr/testify/require"
8+)
9+
10+func TestWebViewBridgeInputOptions_LeavesTargetSelectionToAdapter(t *testing.T) {
11+ nativeWidget := &gtk.Widget{}
12+ wv := &WebView{nativeWidget: nativeWidget}
13+
14+ opts := wv.bridgeInputOptions()
15+
16+ require.Zero(t, opts.Scale)
17+ require.NotNil(t, opts.OnMiddleClick)
18+ require.NotNil(t, opts.SelectionText)
19+ require.NotNil(t, opts.OnClipboardShortcut)
20+ require.Same(t, nativeWidget, wv.nativeWidget)
21+}
M internal/ui/layout/gtk_widgets.go +7 -31
@@ -27,26 +27,13 @@ type gtkWidget struct {
2727 inner *gtk.Widget
2828 }
2929
30-func (w *gtkWidget) Show() { w.inner.Show() }
31-func (w *gtkWidget) Hide() { w.inner.Hide() }
32-func (w *gtkWidget) SetVisible(visible bool) { w.inner.SetVisible(visible) }
33-func (w *gtkWidget) IsVisible() bool { return w.inner.GetVisible() }
34-func (w *gtkWidget) SetOpacity(opacity float64) { w.inner.SetOpacity(opacity) }
35-func (w *gtkWidget) GrabFocus() bool {
36- if focusChild := gtkWidgetFocusChild(w.inner); focusChild != nil && focusChild.GrabFocus() {
37- return true
38- }
39- return w.inner.GrabFocus()
40-}
41-func (w *gtkWidget) HasFocus() bool {
42- if w.inner.HasFocus() {
43- return true
44- }
45- if focusChild := gtkWidgetFocusChild(w.inner); focusChild != nil {
46- return focusChild.HasFocus()
47- }
48- return false
49-}
30+func (w *gtkWidget) Show() { w.inner.Show() }
31+func (w *gtkWidget) Hide() { w.inner.Hide() }
32+func (w *gtkWidget) SetVisible(visible bool) { w.inner.SetVisible(visible) }
33+func (w *gtkWidget) IsVisible() bool { return w.inner.GetVisible() }
34+func (w *gtkWidget) SetOpacity(opacity float64) { w.inner.SetOpacity(opacity) }
35+func (w *gtkWidget) GrabFocus() bool { return w.inner.GrabFocus() }
36+func (w *gtkWidget) HasFocus() bool { return w.inner.HasFocus() }
5037 func (w *gtkWidget) SetCanFocus(canFocus bool) { w.inner.SetCanFocus(canFocus) }
5138 func (w *gtkWidget) SetFocusable(focusable bool) { w.inner.SetFocusable(focusable) }
5239 func (w *gtkWidget) SetFocusOnClick(focus bool) { w.inner.SetFocusOnClick(focus) }
@@ -92,17 +79,6 @@ func (w *gtkWidget) ComputePoint(target Widget) (x, y float64, ok bool) {
9279 return float64(outPoint.X), float64(outPoint.Y), true
9380 }
9481
95-func gtkWidgetFocusChild(widget *gtk.Widget) *gtk.Widget {
96- if widget == nil {
97- return nil
98- }
99- focusChild := widget.GetFocusChild()
100- if focusChild == nil || focusChild.GoPointer() == widget.GoPointer() {
101- return nil
102- }
103- return focusChild
104-}
105-
10682 // gtkPaned wraps gtk.Paned to implement PanedWidget.
10783 type gtkPaned struct {
10884 inner *gtk.Paned
@@ -27,26 +27,13 @@ type gtkWidget struct {
27 inner *gtk.Widget27 inner *gtk.Widget
28 }28 }
29 29
30-func (w *gtkWidget) Show() { w.inner.Show() }30+func (w *gtkWidget) Show() { w.inner.Show() }
31-func (w *gtkWidget) Hide() { w.inner.Hide() }31+func (w *gtkWidget) Hide() { w.inner.Hide() }
32-func (w *gtkWidget) SetVisible(visible bool) { w.inner.SetVisible(visible) }32+func (w *gtkWidget) SetVisible(visible bool) { w.inner.SetVisible(visible) }
33-func (w *gtkWidget) IsVisible() bool { return w.inner.GetVisible() }33+func (w *gtkWidget) IsVisible() bool { return w.inner.GetVisible() }
34-func (w *gtkWidget) SetOpacity(opacity float64) { w.inner.SetOpacity(opacity) }34+func (w *gtkWidget) SetOpacity(opacity float64) { w.inner.SetOpacity(opacity) }
35-func (w *gtkWidget) GrabFocus() bool {35+func (w *gtkWidget) GrabFocus() bool { return w.inner.GrabFocus() }
36- if focusChild := gtkWidgetFocusChild(w.inner); focusChild != nil && focusChild.GrabFocus() {36+func (w *gtkWidget) HasFocus() bool { return w.inner.HasFocus() }
37- return true
38- }
39- return w.inner.GrabFocus()
40-}
41-func (w *gtkWidget) HasFocus() bool {
42- if w.inner.HasFocus() {
43- return true
44- }
45- if focusChild := gtkWidgetFocusChild(w.inner); focusChild != nil {
46- return focusChild.HasFocus()
47- }
48- return false
49-}
50 func (w *gtkWidget) SetCanFocus(canFocus bool) { w.inner.SetCanFocus(canFocus) }37 func (w *gtkWidget) SetCanFocus(canFocus bool) { w.inner.SetCanFocus(canFocus) }
51 func (w *gtkWidget) SetFocusable(focusable bool) { w.inner.SetFocusable(focusable) }38 func (w *gtkWidget) SetFocusable(focusable bool) { w.inner.SetFocusable(focusable) }
52 func (w *gtkWidget) SetFocusOnClick(focus bool) { w.inner.SetFocusOnClick(focus) }39 func (w *gtkWidget) SetFocusOnClick(focus bool) { w.inner.SetFocusOnClick(focus) }
@@ -92,17 +79,6 @@ func (w *gtkWidget) ComputePoint(target Widget) (x, y float64, ok bool) {
92 return float64(outPoint.X), float64(outPoint.Y), true79 return float64(outPoint.X), float64(outPoint.Y), true
93 }80 }
94 81
95-func gtkWidgetFocusChild(widget *gtk.Widget) *gtk.Widget {
96- if widget == nil {
97- return nil
98- }
99- focusChild := widget.GetFocusChild()
100- if focusChild == nil || focusChild.GoPointer() == widget.GoPointer() {
101- return nil
102- }
103- return focusChild
104-}
105-
106 // gtkPaned wraps gtk.Paned to implement PanedWidget.82 // gtkPaned wraps gtk.Paned to implement PanedWidget.
107 type gtkPaned struct {83 type gtkPaned struct {
108 inner *gtk.Paned84 inner *gtk.Paned

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