fix(cef): restore keyboard focus with popup overlays
- commit
- fdb2a2f6634e8c772ee167ecb6ccf5437c1dd97e
- author
- brice · · 2026-05-21 16:07 UTC
- parent
- 8f62984
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
7 files · +96 -57
M go.mod +2 -0
| @@ -84,3 +84,5 @@ require ( | |||
| 84 | 84 | ||
| 85 | 85 | tool github.com/a-h/templ/cmd/templ | |
| 86 | 86 | ||
| 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/templ | 85 | 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") | |||
| 25 | 25 | // The adapter does NOT own GL/PBO staging, dirty-rect uploads, or GTK input | |
| 26 | 26 | // controller implementation — those belong to purego-cef2gtk. | |
| 27 | 27 | 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 | |
| 32 | 33 | } | |
| 33 | 34 | ||
| 34 | 35 | // 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 | 138 | return a.view.AddSizeObserver(fn) | |
| 138 | 139 | } | |
| 139 | 140 | ||
| 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. | |
| 142 | 143 | func (a *Cef2gtkAdapter) HasFocus() bool { | |
| 143 | 144 | if a == nil || a.destroyed.Load() { | |
| 144 | 145 | return false | |
| @@ -148,6 +149,9 @@ func (a *Cef2gtkAdapter) HasFocus() bool { | |||
| 148 | 149 | if a.view == nil { | |
| 149 | 150 | return false | |
| 150 | 151 | } | |
| 152 | + | if a.inputTargetWidget != nil { | |
| 153 | + | return a.inputTargetWidget.HasFocus() | |
| 154 | + | } | |
| 151 | 155 | return a.view.HasFocus() | |
| 152 | 156 | } | |
| 153 | 157 | ||
| @@ -243,15 +247,41 @@ func (a *Cef2gtkAdapter) RecordExternalBeginFrameSent() { | |||
| 243 | 247 | // | |
| 244 | 248 | // opts.Scale configures HiDPI scale for pointer coordinate translation. | |
| 245 | 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 | 258 | if a == nil || a.destroyed.Load() { | |
| 247 | 259 | return ErrAdapterDestroyed | |
| 248 | 260 | } | |
| 249 | - | a.viewMu.RLock() | |
| 250 | - | defer a.viewMu.RUnlock() | |
| 261 | + | a.viewMu.Lock() | |
| 262 | + | defer a.viewMu.Unlock() | |
| 251 | 263 | if a.view == nil { | |
| 252 | 264 | 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 | 287 | // SetInputHost updates the CEF browser host used by the attached input bridge. | |
| @@ -275,12 +305,14 @@ func (a *Cef2gtkAdapter) DetachInput() error { | |||
| 275 | 305 | if a == nil || a.destroyed.Load() { | |
| 276 | 306 | return ErrAdapterDestroyed | |
| 277 | 307 | } | |
| 278 | - | a.viewMu.RLock() | |
| 279 | - | defer a.viewMu.RUnlock() | |
| 308 | + | a.viewMu.Lock() | |
| 309 | + | defer a.viewMu.Unlock() | |
| 280 | 310 | if a.view == nil { | |
| 281 | 311 | 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 | 318 | // Diagnostics returns a point-in-time snapshot of bridge rendering diagnostics | |
| @@ -311,6 +343,7 @@ func (a *Cef2gtkAdapter) Destroy() error { | |||
| 311 | 343 | } | |
| 312 | 344 | err := a.view.Destroy() | |
| 313 | 345 | a.view = nil | |
| 346 | + | a.inputTargetWidget = nil | |
| 314 | 347 | a.destroyCnt.Add(1) | |
| 315 | 348 | return err | |
| 316 | 349 | } | |
| @@ -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 input | 25 | // 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.RWMutex | 28 | + | viewMu sync.RWMutex |
| 29 | - | view *cef2gtk.View | 29 | + | view *cef2gtk.View |
| 30 | - | destroyed atomic.Bool | 30 | + | inputTargetWidget *gtk.Widget |
| 31 | - | destroyCnt atomic.Uint64 | 31 | + | destroyed atomic.Bool |
| 32 | + | destroyCnt atomic.Uint64 | |||
| 32 | } | 33 | } | ||
| 33 | 34 | ||||
| 34 | // NewCef2gtkAdapter creates an accelerated CEF view and wraps it in a thin | 35 | // 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 be | 141 | + | // 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 false | 145 | return false | ||
| @@ -148,6 +149,9 @@ func (a *Cef2gtkAdapter) HasFocus() bool { | |||||
| 148 | if a.view == nil { | 149 | if a.view == nil { | ||
| 149 | return false | 150 | 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 ErrAdapterDestroyed | 259 | 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 ErrAdapterDestroyed | 264 | 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 ErrAdapterDestroyed | 306 | 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 ErrAdapterDestroyed | 311 | 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 diagnostics | 318 | // 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 = nil | 345 | a.view = nil | ||
| 346 | + | a.inputTargetWidget = nil | |||
| 314 | a.destroyCnt.Add(1) | 347 | a.destroyCnt.Add(1) | ||
| 315 | return err | 348 | 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 | |||
| 61 | 61 | root := &overlay.Widget | |
| 62 | 62 | root.SetHexpand(true) | |
| 63 | 63 | root.SetVexpand(true) | |
| 64 | + | root.SetCanFocus(true) | |
| 65 | + | root.SetFocusable(true) | |
| 64 | 66 | root.SetFocusOnClick(true) | |
| 65 | - | root.SetFocusChild(mainWidget) | |
| 66 | 67 | return &popupBridgeSurface{ | |
| 67 | 68 | ctx: ctx, | |
| 68 | 69 | root: root, | |
| @@ -61,8 +61,9 @@ func newPopupBridgeSurface(ctx context.Context, mainWidget *gtk.Widget, plan cef | |||||
| 61 | root := &overlay.Widget | 61 | 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 { | |||
| 1413 | 1413 | 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 | 1434 | func (wv *WebView) viewBridgeScale() float64 { | |
| 1417 | 1435 | if wv == nil || wv.viewBridge == nil { | |
| 1418 | 1436 | return 1 | |
| @@ -1413,6 +1413,24 @@ func (wv *WebView) selectedTextSnapshot() string { | |||||
| 1413 | return wv.selectedText | 1413 | 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 1 | 1436 | return 1 | ||
M internal/infrastructure/cef/webview_handlers.go +1 -13
| @@ -6,7 +6,6 @@ import ( | |||
| 6 | 6 | "sync" | |
| 7 | 7 | ||
| 8 | 8 | purecef "github.com/bnema/purego-cef/cef" | |
| 9 | - | cef2gtk "github.com/bnema/purego-cef2gtk" | |
| 10 | 9 | ||
| 11 | 10 | "github.com/bnema/dumber/internal/application/dto" | |
| 12 | 11 | "github.com/bnema/dumber/internal/application/port" | |
| @@ -640,18 +639,7 @@ func (h *handlerSet) attachAfterCreatedBrowser( | |||
| 640 | 639 | wv.handleInputAttachFailure(ErrAdapterDestroyed, host) | |
| 641 | 640 | return | |
| 642 | 641 | } | |
| 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 { | |
| 655 | 643 | wv.handleInputAttachFailure(err, host) | |
| 656 | 644 | return | |
| 657 | 645 | } | |
| @@ -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 | return | 640 | 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 | return | 644 | 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 := >k.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 := >k.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 { | |||
| 27 | 27 | inner *gtk.Widget | |
| 28 | 28 | } | |
| 29 | 29 | ||
| 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() } | |
| 50 | 37 | func (w *gtkWidget) SetCanFocus(canFocus bool) { w.inner.SetCanFocus(canFocus) } | |
| 51 | 38 | func (w *gtkWidget) SetFocusable(focusable bool) { w.inner.SetFocusable(focusable) } | |
| 52 | 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 | 79 | 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 | 82 | // gtkPaned wraps gtk.Paned to implement PanedWidget. | |
| 107 | 83 | type gtkPaned struct { | |
| 108 | 84 | inner *gtk.Paned | |
| @@ -27,26 +27,13 @@ type gtkWidget struct { | |||||
| 27 | inner *gtk.Widget | 27 | 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), true | 79 | 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.Paned | 84 | inner *gtk.Paned | ||