Merge pull request #259 from bnema/fix/cef-native-select-popup
fix(cef): render native popup surfaces without focus regressions
11 files · +575 -33
M go.mod +1 -2
| @@ -7,7 +7,7 @@ require ( | |||
| 7 | 7 | github.com/andybalholm/brotli v1.2.1 | |
| 8 | 8 | github.com/bnema/purego v0.11.0-bnema.2 | |
| 9 | 9 | github.com/bnema/purego-cef v0.13.0 | |
| 10 | - | github.com/bnema/purego-cef2gtk v0.5.1 | |
| 10 | + | github.com/bnema/purego-cef2gtk v0.5.2 | |
| 11 | 11 | github.com/bnema/purego-pipewire v0.1.3 | |
| 12 | 12 | github.com/bnema/purego-sqlite v0.1.2 | |
| 13 | 13 | github.com/bnema/puregotk v0.5.2 | |
| @@ -83,4 +83,3 @@ require ( | |||
| 83 | 83 | ) | |
| 84 | 84 | ||
| 85 | 85 | tool github.com/a-h/templ/cmd/templ | |
| 86 | - | ||
| @@ -7,7 +7,7 @@ require ( | |||||
| 7 | github.com/andybalholm/brotli v1.2.1 | 7 | github.com/andybalholm/brotli v1.2.1 | ||
| 8 | github.com/bnema/purego v0.11.0-bnema.2 | 8 | github.com/bnema/purego v0.11.0-bnema.2 | ||
| 9 | github.com/bnema/purego-cef v0.13.0 | 9 | github.com/bnema/purego-cef v0.13.0 | ||
| 10 | - | github.com/bnema/purego-cef2gtk v0.5.1 | 10 | + | github.com/bnema/purego-cef2gtk v0.5.2 |
| 11 | github.com/bnema/purego-pipewire v0.1.3 | 11 | github.com/bnema/purego-pipewire v0.1.3 | ||
| 12 | github.com/bnema/purego-sqlite v0.1.2 | 12 | github.com/bnema/purego-sqlite v0.1.2 | ||
| 13 | github.com/bnema/puregotk v0.5.2 | 13 | github.com/bnema/puregotk v0.5.2 | ||
| @@ -83,4 +83,3 @@ require ( | |||||
| 83 | ) | 83 | ) | ||
| 84 | 84 | ||||
| 85 | tool github.com/a-h/templ/cmd/templ | 85 | tool github.com/a-h/templ/cmd/templ | ||
| 86 | - | ||||
M go.sum +2 -2 generated file: collapsed
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/factory.go +11 -0
| @@ -123,6 +123,17 @@ func (f *WebViewFactory) newWebView(ctx context.Context) (*WebView, error) { | |||
| 123 | 123 | windowlessFrameRateMax: f.windowlessFrameRateMax, | |
| 124 | 124 | backgroundColor: f.bgColor.Load(), | |
| 125 | 125 | } | |
| 126 | + | wv.runOnGTKSync(func() { | |
| 127 | + | nativeWidget := viewBridge.Widget() | |
| 128 | + | popupSurface := newPopupBridgeSurface(ctx, nativeWidget, f.engine.renderStackPlan) | |
| 129 | + | wv.nativeWidget = nativeWidget | |
| 130 | + | if popupSurface != nil { | |
| 131 | + | wv.popupSurface = popupSurface | |
| 132 | + | if popupRoot := popupSurface.RootWidget(); popupRoot != nil { | |
| 133 | + | wv.nativeWidget = popupRoot | |
| 134 | + | } | |
| 135 | + | } | |
| 136 | + | }) | |
| 126 | 137 | ||
| 127 | 138 | handlers := &handlerSet{wv: wv} | |
| 128 | 139 | wv.handlers = handlers | |
| @@ -123,6 +123,17 @@ func (f *WebViewFactory) newWebView(ctx context.Context) (*WebView, error) { | |||||
| 123 | windowlessFrameRateMax: f.windowlessFrameRateMax, | 123 | windowlessFrameRateMax: f.windowlessFrameRateMax, | ||
| 124 | backgroundColor: f.bgColor.Load(), | 124 | backgroundColor: f.bgColor.Load(), | ||
| 125 | } | 125 | } | ||
| 126 | + | wv.runOnGTKSync(func() { | |||
| 127 | + | nativeWidget := viewBridge.Widget() | |||
| 128 | + | popupSurface := newPopupBridgeSurface(ctx, nativeWidget, f.engine.renderStackPlan) | |||
| 129 | + | wv.nativeWidget = nativeWidget | |||
| 130 | + | if popupSurface != nil { | |||
| 131 | + | wv.popupSurface = popupSurface | |||
| 132 | + | if popupRoot := popupSurface.RootWidget(); popupRoot != nil { | |||
| 133 | + | wv.nativeWidget = popupRoot | |||
| 134 | + | } | |||
| 135 | + | } | |||
| 136 | + | }) | |||
| 126 | 137 | ||||
| 127 | handlers := &handlerSet{wv: wv} | 138 | handlers := &handlerSet{wv: wv} | ||
| 128 | wv.handlers = handlers | 139 | wv.handlers = handlers | ||
A internal/infrastructure/cef/popup_surface.go +156 -0
| @@ -0,0 +1,156 @@ | |||
| 1 | + | package cef | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "sync/atomic" | |
| 6 | + | ||
| 7 | + | purecef "github.com/bnema/purego-cef/cef" | |
| 8 | + | cef2gtk "github.com/bnema/purego-cef2gtk" | |
| 9 | + | "github.com/bnema/puregotk/v4/glib" | |
| 10 | + | "github.com/bnema/puregotk/v4/gtk" | |
| 11 | + | ||
| 12 | + | "github.com/bnema/dumber/internal/logging" | |
| 13 | + | ) | |
| 14 | + | ||
| 15 | + | type popupBridgeSurface struct { | |
| 16 | + | ctx context.Context | |
| 17 | + | root *gtk.Widget | |
| 18 | + | mainWidget *gtk.Widget | |
| 19 | + | bridge *Cef2gtkAdapter | |
| 20 | + | popupWidget *gtk.Widget | |
| 21 | + | prepared atomic.Uint32 | |
| 22 | + | } | |
| 23 | + | ||
| 24 | + | // newPopupBridgeSurface must be called on the GTK thread because it creates | |
| 25 | + | // and packs GTK widgets around the primary CEF widget. | |
| 26 | + | func newPopupBridgeSurface(ctx context.Context, mainWidget *gtk.Widget, plan cef2gtk.RenderStackPlan) *popupBridgeSurface { | |
| 27 | + | mainContext := glib.MainContextDefault() | |
| 28 | + | if mainContext == nil || !mainContext.IsOwner() { | |
| 29 | + | logging.FromContext(popupSurfaceLogContext(ctx)).Error().Msg("cef: newPopupBridgeSurface called off GTK thread") | |
| 30 | + | return nil | |
| 31 | + | } | |
| 32 | + | if mainWidget == nil { | |
| 33 | + | return nil | |
| 34 | + | } | |
| 35 | + | bridge := NewCef2gtkAdapter(plan) | |
| 36 | + | if bridge == nil { | |
| 37 | + | return nil | |
| 38 | + | } | |
| 39 | + | popupWidget := bridge.Widget() | |
| 40 | + | if popupWidget == nil { | |
| 41 | + | _ = bridge.Destroy() | |
| 42 | + | return nil | |
| 43 | + | } | |
| 44 | + | overlay := gtk.NewOverlay() | |
| 45 | + | if overlay == nil { | |
| 46 | + | _ = bridge.Destroy() | |
| 47 | + | return nil | |
| 48 | + | } | |
| 49 | + | overlay.SetChild(mainWidget) | |
| 50 | + | popupWidget.SetVisible(false) | |
| 51 | + | popupWidget.SetHalign(gtk.AlignStartValue) | |
| 52 | + | popupWidget.SetValign(gtk.AlignStartValue) | |
| 53 | + | popupWidget.SetHexpand(false) | |
| 54 | + | popupWidget.SetVexpand(false) | |
| 55 | + | popupWidget.SetCanTarget(false) | |
| 56 | + | popupWidget.SetCanFocus(false) | |
| 57 | + | popupWidget.SetFocusable(false) | |
| 58 | + | overlay.AddOverlay(popupWidget) | |
| 59 | + | overlay.SetClipOverlay(popupWidget, true) | |
| 60 | + | overlay.SetMeasureOverlay(popupWidget, false) | |
| 61 | + | root := &overlay.Widget | |
| 62 | + | root.SetHexpand(true) | |
| 63 | + | root.SetVexpand(true) | |
| 64 | + | root.SetCanFocus(true) | |
| 65 | + | root.SetFocusable(true) | |
| 66 | + | root.SetFocusOnClick(true) | |
| 67 | + | return &popupBridgeSurface{ | |
| 68 | + | ctx: ctx, | |
| 69 | + | root: root, | |
| 70 | + | mainWidget: mainWidget, | |
| 71 | + | bridge: bridge, | |
| 72 | + | popupWidget: popupWidget, | |
| 73 | + | } | |
| 74 | + | } | |
| 75 | + | ||
| 76 | + | func (s *popupBridgeSurface) RootWidget() *gtk.Widget { | |
| 77 | + | if s == nil { | |
| 78 | + | return nil | |
| 79 | + | } | |
| 80 | + | return s.root | |
| 81 | + | } | |
| 82 | + | ||
| 83 | + | func (s *popupBridgeSurface) RenderHandler(hooks cef2gtk.Hooks) purecef.RenderHandler { | |
| 84 | + | if s == nil || s.bridge == nil { | |
| 85 | + | return nil | |
| 86 | + | } | |
| 87 | + | return s.bridge.RenderHandler(hooks) | |
| 88 | + | } | |
| 89 | + | ||
| 90 | + | func (s *popupBridgeSurface) PrepareOnGTKThread() error { | |
| 91 | + | if s == nil || s.bridge == nil { | |
| 92 | + | return nil | |
| 93 | + | } | |
| 94 | + | if !s.prepared.CompareAndSwap(0, 1) { | |
| 95 | + | return nil | |
| 96 | + | } | |
| 97 | + | if err := s.bridge.PrepareOnGTKThread(); err != nil { | |
| 98 | + | s.prepared.Store(0) | |
| 99 | + | return err | |
| 100 | + | } | |
| 101 | + | return nil | |
| 102 | + | } | |
| 103 | + | ||
| 104 | + | func (s *popupBridgeSurface) SetPopupVisible(visible bool) { | |
| 105 | + | if s == nil || s.popupWidget == nil { | |
| 106 | + | return | |
| 107 | + | } | |
| 108 | + | if visible { | |
| 109 | + | if err := s.PrepareOnGTKThread(); err != nil { | |
| 110 | + | logging.FromContext(popupSurfaceLogContext(s.ctx)).Error(). | |
| 111 | + | Err(err). | |
| 112 | + | Msg("cef: popupBridgeSurface.SetPopupVisible failed to PrepareOnGTKThread") | |
| 113 | + | return | |
| 114 | + | } | |
| 115 | + | } | |
| 116 | + | s.popupWidget.SetVisible(visible) | |
| 117 | + | if s.root != nil { | |
| 118 | + | s.root.QueueAllocate() | |
| 119 | + | } | |
| 120 | + | } | |
| 121 | + | ||
| 122 | + | func (s *popupBridgeSurface) SetPopupRect(rect purecef.Rect) { | |
| 123 | + | if s == nil || s.popupWidget == nil { | |
| 124 | + | return | |
| 125 | + | } | |
| 126 | + | if rect.Width <= 0 || rect.Height <= 0 { | |
| 127 | + | s.popupWidget.SetVisible(false) | |
| 128 | + | return | |
| 129 | + | } | |
| 130 | + | s.popupWidget.SetMarginStart(int(rect.X)) | |
| 131 | + | s.popupWidget.SetMarginTop(int(rect.Y)) | |
| 132 | + | s.popupWidget.SetSizeRequest(int(rect.Width), int(rect.Height)) | |
| 133 | + | s.popupWidget.QueueResize() | |
| 134 | + | if s.root != nil { | |
| 135 | + | s.root.QueueResize() | |
| 136 | + | } | |
| 137 | + | } | |
| 138 | + | ||
| 139 | + | func popupSurfaceLogContext(ctx context.Context) context.Context { | |
| 140 | + | if ctx != nil { | |
| 141 | + | return ctx | |
| 142 | + | } | |
| 143 | + | return context.Background() | |
| 144 | + | } | |
| 145 | + | ||
| 146 | + | func (s *popupBridgeSurface) DestroyOnGTKThread() { | |
| 147 | + | if s == nil || s.bridge == nil { | |
| 148 | + | return | |
| 149 | + | } | |
| 150 | + | _ = s.bridge.Destroy() | |
| 151 | + | s.bridge = nil | |
| 152 | + | s.popupWidget = nil | |
| 153 | + | s.mainWidget = nil | |
| 154 | + | s.root = nil | |
| 155 | + | s.prepared.Store(0) | |
| 156 | + | } | |
| @@ -0,0 +1,156 @@ | |||||
| 1 | + | package cef | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "sync/atomic" | |||
| 6 | + | ||||
| 7 | + | purecef "github.com/bnema/purego-cef/cef" | |||
| 8 | + | cef2gtk "github.com/bnema/purego-cef2gtk" | |||
| 9 | + | "github.com/bnema/puregotk/v4/glib" | |||
| 10 | + | "github.com/bnema/puregotk/v4/gtk" | |||
| 11 | + | ||||
| 12 | + | "github.com/bnema/dumber/internal/logging" | |||
| 13 | + | ) | |||
| 14 | + | ||||
| 15 | + | type popupBridgeSurface struct { | |||
| 16 | + | ctx context.Context | |||
| 17 | + | root *gtk.Widget | |||
| 18 | + | mainWidget *gtk.Widget | |||
| 19 | + | bridge *Cef2gtkAdapter | |||
| 20 | + | popupWidget *gtk.Widget | |||
| 21 | + | prepared atomic.Uint32 | |||
| 22 | + | } | |||
| 23 | + | ||||
| 24 | + | // newPopupBridgeSurface must be called on the GTK thread because it creates | |||
| 25 | + | // and packs GTK widgets around the primary CEF widget. | |||
| 26 | + | func newPopupBridgeSurface(ctx context.Context, mainWidget *gtk.Widget, plan cef2gtk.RenderStackPlan) *popupBridgeSurface { | |||
| 27 | + | mainContext := glib.MainContextDefault() | |||
| 28 | + | if mainContext == nil || !mainContext.IsOwner() { | |||
| 29 | + | logging.FromContext(popupSurfaceLogContext(ctx)).Error().Msg("cef: newPopupBridgeSurface called off GTK thread") | |||
| 30 | + | return nil | |||
| 31 | + | } | |||
| 32 | + | if mainWidget == nil { | |||
| 33 | + | return nil | |||
| 34 | + | } | |||
| 35 | + | bridge := NewCef2gtkAdapter(plan) | |||
| 36 | + | if bridge == nil { | |||
| 37 | + | return nil | |||
| 38 | + | } | |||
| 39 | + | popupWidget := bridge.Widget() | |||
| 40 | + | if popupWidget == nil { | |||
| 41 | + | _ = bridge.Destroy() | |||
| 42 | + | return nil | |||
| 43 | + | } | |||
| 44 | + | overlay := gtk.NewOverlay() | |||
| 45 | + | if overlay == nil { | |||
| 46 | + | _ = bridge.Destroy() | |||
| 47 | + | return nil | |||
| 48 | + | } | |||
| 49 | + | overlay.SetChild(mainWidget) | |||
| 50 | + | popupWidget.SetVisible(false) | |||
| 51 | + | popupWidget.SetHalign(gtk.AlignStartValue) | |||
| 52 | + | popupWidget.SetValign(gtk.AlignStartValue) | |||
| 53 | + | popupWidget.SetHexpand(false) | |||
| 54 | + | popupWidget.SetVexpand(false) | |||
| 55 | + | popupWidget.SetCanTarget(false) | |||
| 56 | + | popupWidget.SetCanFocus(false) | |||
| 57 | + | popupWidget.SetFocusable(false) | |||
| 58 | + | overlay.AddOverlay(popupWidget) | |||
| 59 | + | overlay.SetClipOverlay(popupWidget, true) | |||
| 60 | + | overlay.SetMeasureOverlay(popupWidget, false) | |||
| 61 | + | root := &overlay.Widget | |||
| 62 | + | root.SetHexpand(true) | |||
| 63 | + | root.SetVexpand(true) | |||
| 64 | + | root.SetCanFocus(true) | |||
| 65 | + | root.SetFocusable(true) | |||
| 66 | + | root.SetFocusOnClick(true) | |||
| 67 | + | return &popupBridgeSurface{ | |||
| 68 | + | ctx: ctx, | |||
| 69 | + | root: root, | |||
| 70 | + | mainWidget: mainWidget, | |||
| 71 | + | bridge: bridge, | |||
| 72 | + | popupWidget: popupWidget, | |||
| 73 | + | } | |||
| 74 | + | } | |||
| 75 | + | ||||
| 76 | + | func (s *popupBridgeSurface) RootWidget() *gtk.Widget { | |||
| 77 | + | if s == nil { | |||
| 78 | + | return nil | |||
| 79 | + | } | |||
| 80 | + | return s.root | |||
| 81 | + | } | |||
| 82 | + | ||||
| 83 | + | func (s *popupBridgeSurface) RenderHandler(hooks cef2gtk.Hooks) purecef.RenderHandler { | |||
| 84 | + | if s == nil || s.bridge == nil { | |||
| 85 | + | return nil | |||
| 86 | + | } | |||
| 87 | + | return s.bridge.RenderHandler(hooks) | |||
| 88 | + | } | |||
| 89 | + | ||||
| 90 | + | func (s *popupBridgeSurface) PrepareOnGTKThread() error { | |||
| 91 | + | if s == nil || s.bridge == nil { | |||
| 92 | + | return nil | |||
| 93 | + | } | |||
| 94 | + | if !s.prepared.CompareAndSwap(0, 1) { | |||
| 95 | + | return nil | |||
| 96 | + | } | |||
| 97 | + | if err := s.bridge.PrepareOnGTKThread(); err != nil { | |||
| 98 | + | s.prepared.Store(0) | |||
| 99 | + | return err | |||
| 100 | + | } | |||
| 101 | + | return nil | |||
| 102 | + | } | |||
| 103 | + | ||||
| 104 | + | func (s *popupBridgeSurface) SetPopupVisible(visible bool) { | |||
| 105 | + | if s == nil || s.popupWidget == nil { | |||
| 106 | + | return | |||
| 107 | + | } | |||
| 108 | + | if visible { | |||
| 109 | + | if err := s.PrepareOnGTKThread(); err != nil { | |||
| 110 | + | logging.FromContext(popupSurfaceLogContext(s.ctx)).Error(). | |||
| 111 | + | Err(err). | |||
| 112 | + | Msg("cef: popupBridgeSurface.SetPopupVisible failed to PrepareOnGTKThread") | |||
| 113 | + | return | |||
| 114 | + | } | |||
| 115 | + | } | |||
| 116 | + | s.popupWidget.SetVisible(visible) | |||
| 117 | + | if s.root != nil { | |||
| 118 | + | s.root.QueueAllocate() | |||
| 119 | + | } | |||
| 120 | + | } | |||
| 121 | + | ||||
| 122 | + | func (s *popupBridgeSurface) SetPopupRect(rect purecef.Rect) { | |||
| 123 | + | if s == nil || s.popupWidget == nil { | |||
| 124 | + | return | |||
| 125 | + | } | |||
| 126 | + | if rect.Width <= 0 || rect.Height <= 0 { | |||
| 127 | + | s.popupWidget.SetVisible(false) | |||
| 128 | + | return | |||
| 129 | + | } | |||
| 130 | + | s.popupWidget.SetMarginStart(int(rect.X)) | |||
| 131 | + | s.popupWidget.SetMarginTop(int(rect.Y)) | |||
| 132 | + | s.popupWidget.SetSizeRequest(int(rect.Width), int(rect.Height)) | |||
| 133 | + | s.popupWidget.QueueResize() | |||
| 134 | + | if s.root != nil { | |||
| 135 | + | s.root.QueueResize() | |||
| 136 | + | } | |||
| 137 | + | } | |||
| 138 | + | ||||
| 139 | + | func popupSurfaceLogContext(ctx context.Context) context.Context { | |||
| 140 | + | if ctx != nil { | |||
| 141 | + | return ctx | |||
| 142 | + | } | |||
| 143 | + | return context.Background() | |||
| 144 | + | } | |||
| 145 | + | ||||
| 146 | + | func (s *popupBridgeSurface) DestroyOnGTKThread() { | |||
| 147 | + | if s == nil || s.bridge == nil { | |||
| 148 | + | return | |||
| 149 | + | } | |||
| 150 | + | _ = s.bridge.Destroy() | |||
| 151 | + | s.bridge = nil | |||
| 152 | + | s.popupWidget = nil | |||
| 153 | + | s.mainWidget = nil | |||
| 154 | + | s.root = nil | |||
| 155 | + | s.prepared.Store(0) | |||
| 156 | + | } | |||
M internal/infrastructure/cef/render_handler_adapter.go +226 -2
| @@ -9,12 +9,28 @@ import ( | |||
| 9 | 9 | "github.com/bnema/dumber/internal/logging" | |
| 10 | 10 | ) | |
| 11 | 11 | ||
| 12 | + | type popupSurface interface { | |
| 13 | + | SetPopupVisible(bool) | |
| 14 | + | SetPopupRect(purecef.Rect) | |
| 15 | + | } | |
| 16 | + | ||
| 17 | + | type dumberRenderHandler struct { | |
| 18 | + | wv *WebView | |
| 19 | + | main purecef.RenderHandler | |
| 20 | + | popup purecef.RenderHandler | |
| 21 | + | popupSurface popupSurface | |
| 22 | + | } | |
| 23 | + | ||
| 24 | + | var _ purecef.RenderHandler = (*dumberRenderHandler)(nil) | |
| 25 | + | ||
| 12 | 26 | func newDumberRenderHandler(wv *WebView) purecef.RenderHandler { | |
| 13 | 27 | if wv == nil || wv.viewBridge == nil { | |
| 14 | 28 | return nil | |
| 15 | 29 | } | |
| 30 | + | h := &dumberRenderHandler{wv: wv} | |
| 31 | + | ||
| 16 | 32 | var unsupportedPaintOnce sync.Once | |
| 17 | - | hooks := cef2gtk.Hooks{ | |
| 33 | + | h.main = wv.viewBridge.RenderHandler(cef2gtk.Hooks{ | |
| 18 | 34 | OnUnsupportedPaint: func() { | |
| 19 | 35 | unsupportedPaintOnce.Do(func() { | |
| 20 | 36 | if wv.ctx != nil { | |
| @@ -30,8 +46,216 @@ func newDumberRenderHandler(wv *WebView) purecef.RenderHandler { | |||
| 30 | 46 | OnTextSelectionChanged: func(selectedText string, _ *purecef.Range) { | |
| 31 | 47 | handleRenderTextSelectionChanged(wv, selectedText) | |
| 32 | 48 | }, | |
| 49 | + | }) | |
| 50 | + | ||
| 51 | + | if wv.popupSurface != nil { | |
| 52 | + | var popupUnsupportedPaintOnce sync.Once | |
| 53 | + | h.popupSurface = wv.popupSurface | |
| 54 | + | h.popup = wv.popupSurface.RenderHandler(cef2gtk.Hooks{ | |
| 55 | + | OnUnsupportedPaint: func() { | |
| 56 | + | popupUnsupportedPaintOnce.Do(func() { | |
| 57 | + | if wv.ctx != nil { | |
| 58 | + | logging.FromContext(wv.ctx).Warn().Msg("cef: unsupported CPU paint from popup surface") | |
| 59 | + | } | |
| 60 | + | }) | |
| 61 | + | }, | |
| 62 | + | OnError: func(err error) { | |
| 63 | + | if wv.ctx != nil { | |
| 64 | + | logging.FromContext(wv.ctx).Warn().Err(err).Msg("cef: popup surface render error") | |
| 65 | + | } | |
| 66 | + | }, | |
| 67 | + | }) | |
| 68 | + | } | |
| 69 | + | ||
| 70 | + | return h | |
| 71 | + | } | |
| 72 | + | ||
| 73 | + | func (h *dumberRenderHandler) renderTargetForPaint(elementType purecef.PaintElementType) purecef.RenderHandler { | |
| 74 | + | if h == nil { | |
| 75 | + | return nil | |
| 76 | + | } | |
| 77 | + | if elementType == purecef.PaintElementTypePetPopup && h.popup != nil { | |
| 78 | + | return h.popup | |
| 79 | + | } | |
| 80 | + | return h.main | |
| 81 | + | } | |
| 82 | + | ||
| 83 | + | func (h *dumberRenderHandler) GetAccessibilityHandler() purecef.AccessibilityHandler { | |
| 84 | + | if h == nil || h.main == nil { | |
| 85 | + | return nil | |
| 86 | + | } | |
| 87 | + | return h.main.GetAccessibilityHandler() | |
| 88 | + | } | |
| 89 | + | ||
| 90 | + | func (h *dumberRenderHandler) GetRootScreenRect(browser purecef.Browser, rect *purecef.Rect) int32 { | |
| 91 | + | if h == nil || h.main == nil { | |
| 92 | + | return 0 | |
| 93 | + | } | |
| 94 | + | return h.main.GetRootScreenRect(browser, rect) | |
| 95 | + | } | |
| 96 | + | ||
| 97 | + | func (h *dumberRenderHandler) GetViewRect(browser purecef.Browser, rect *purecef.Rect) { | |
| 98 | + | if h == nil || h.main == nil { | |
| 99 | + | if rect != nil { | |
| 100 | + | rect.X, rect.Y, rect.Width, rect.Height = 0, 0, 1, 1 | |
| 101 | + | } | |
| 102 | + | return | |
| 103 | + | } | |
| 104 | + | h.main.GetViewRect(browser, rect) | |
| 105 | + | } | |
| 106 | + | ||
| 107 | + | func (h *dumberRenderHandler) GetScreenPoint(browser purecef.Browser, viewX, viewY int32, screenX, screenY *int32) int32 { | |
| 108 | + | if h == nil || h.main == nil { | |
| 109 | + | return 0 | |
| 110 | + | } | |
| 111 | + | return h.main.GetScreenPoint(browser, viewX, viewY, screenX, screenY) | |
| 112 | + | } | |
| 113 | + | ||
| 114 | + | func (h *dumberRenderHandler) GetScreenInfo(browser purecef.Browser, info *purecef.ScreenInfo) int32 { | |
| 115 | + | if h == nil || h.main == nil { | |
| 116 | + | return 0 | |
| 117 | + | } | |
| 118 | + | return h.main.GetScreenInfo(browser, info) | |
| 119 | + | } | |
| 120 | + | ||
| 121 | + | func (h *dumberRenderHandler) OnPopupShow(browser purecef.Browser, show int32) { | |
| 122 | + | if h != nil && h.popupSurface != nil { | |
| 123 | + | visible := show != 0 | |
| 124 | + | if h.wv != nil { | |
| 125 | + | h.wv.runOnGTKSync(func() { | |
| 126 | + | h.popupSurface.SetPopupVisible(visible) | |
| 127 | + | }) | |
| 128 | + | } else { | |
| 129 | + | h.popupSurface.SetPopupVisible(visible) | |
| 130 | + | } | |
| 131 | + | } | |
| 132 | + | if h != nil && h.popup != nil { | |
| 133 | + | h.popup.OnPopupShow(browser, show) | |
| 134 | + | } | |
| 135 | + | } | |
| 136 | + | ||
| 137 | + | func (h *dumberRenderHandler) OnPopupSize(browser purecef.Browser, rect *purecef.Rect) { | |
| 138 | + | if h != nil && h.popupSurface != nil && rect != nil { | |
| 139 | + | popupRect := *rect | |
| 140 | + | if h.wv != nil { | |
| 141 | + | popupRect = popupRectForWidget(h.wv, popupRect) | |
| 142 | + | h.wv.runOnGTKSync(func() { | |
| 143 | + | h.popupSurface.SetPopupRect(popupRect) | |
| 144 | + | }) | |
| 145 | + | } else { | |
| 146 | + | h.popupSurface.SetPopupRect(popupRect) | |
| 147 | + | } | |
| 148 | + | } | |
| 149 | + | if h != nil && h.popup != nil { | |
| 150 | + | h.popup.OnPopupSize(browser, rect) | |
| 151 | + | } | |
| 152 | + | } | |
| 153 | + | ||
| 154 | + | func popupRectForWidget(wv *WebView, rect purecef.Rect) purecef.Rect { | |
| 155 | + | if wv == nil { | |
| 156 | + | return rect | |
| 157 | + | } | |
| 158 | + | // Only convert when the OSR backing-scale compatibility path is active. | |
| 159 | + | // A monitor scale above 1 is not enough by itself because CEF already reports | |
| 160 | + | // logical popup coordinates when backing-scale mode is off. | |
| 161 | + | if wv.osrBackingScaleFactor() <= 1 { | |
| 162 | + | return rect | |
| 163 | + | } | |
| 164 | + | scale := wv.viewBridgeScale() | |
| 165 | + | return purecef.Rect{ | |
| 166 | + | X: deviceToLogicalCoord(rect.X, scale), | |
| 167 | + | Y: deviceToLogicalCoord(rect.Y, scale), | |
| 168 | + | Width: deviceToLogicalSize(rect.Width, scale), | |
| 169 | + | Height: deviceToLogicalSize(rect.Height, scale), | |
| 170 | + | } | |
| 171 | + | } | |
| 172 | + | ||
| 173 | + | func (h *dumberRenderHandler) OnPaint( | |
| 174 | + | browser purecef.Browser, | |
| 175 | + | elementType purecef.PaintElementType, | |
| 176 | + | dirtyRects []purecef.Rect, | |
| 177 | + | buffer []byte, | |
| 178 | + | width, height int32, | |
| 179 | + | ) { | |
| 180 | + | if delegate := h.renderTargetForPaint(elementType); delegate != nil { | |
| 181 | + | delegate.OnPaint(browser, elementType, dirtyRects, buffer, width, height) | |
| 182 | + | } | |
| 183 | + | } | |
| 184 | + | ||
| 185 | + | func (h *dumberRenderHandler) OnAcceleratedPaint( | |
| 186 | + | browser purecef.Browser, | |
| 187 | + | elementType purecef.PaintElementType, | |
| 188 | + | dirtyRects []purecef.Rect, | |
| 189 | + | info *purecef.AcceleratedPaintInfo, | |
| 190 | + | ) { | |
| 191 | + | if delegate := h.renderTargetForPaint(elementType); delegate != nil { | |
| 192 | + | delegate.OnAcceleratedPaint(browser, elementType, dirtyRects, info) | |
| 193 | + | } | |
| 194 | + | } | |
| 195 | + | ||
| 196 | + | func (h *dumberRenderHandler) GetTouchHandleSize(browser purecef.Browser, orientation purecef.HorizontalAlignment, size *purecef.Size) { | |
| 197 | + | if h == nil || h.main == nil { | |
| 198 | + | return | |
| 199 | + | } | |
| 200 | + | h.main.GetTouchHandleSize(browser, orientation, size) | |
| 201 | + | } | |
| 202 | + | ||
| 203 | + | func (h *dumberRenderHandler) OnTouchHandleStateChanged(browser purecef.Browser, state *purecef.TouchHandleState) { | |
| 204 | + | if h == nil || h.main == nil { | |
| 205 | + | return | |
| 206 | + | } | |
| 207 | + | h.main.OnTouchHandleStateChanged(browser, state) | |
| 208 | + | } | |
| 209 | + | ||
| 210 | + | func (h *dumberRenderHandler) StartDragging( | |
| 211 | + | browser purecef.Browser, | |
| 212 | + | dragData purecef.DragData, | |
| 213 | + | allowedOps purecef.DragOperationsMask, | |
| 214 | + | x, y int32, | |
| 215 | + | ) int32 { | |
| 216 | + | if h == nil || h.main == nil { | |
| 217 | + | return 0 | |
| 218 | + | } | |
| 219 | + | return h.main.StartDragging(browser, dragData, allowedOps, x, y) | |
| 220 | + | } | |
| 221 | + | ||
| 222 | + | func (h *dumberRenderHandler) UpdateDragCursor(browser purecef.Browser, operation purecef.DragOperationsMask) { | |
| 223 | + | if h == nil || h.main == nil { | |
| 224 | + | return | |
| 225 | + | } | |
| 226 | + | h.main.UpdateDragCursor(browser, operation) | |
| 227 | + | } | |
| 228 | + | ||
| 229 | + | func (h *dumberRenderHandler) OnScrollOffsetChanged(browser purecef.Browser, x, y float64) { | |
| 230 | + | if h == nil || h.main == nil { | |
| 231 | + | return | |
| 232 | + | } | |
| 233 | + | h.main.OnScrollOffsetChanged(browser, x, y) | |
| 234 | + | } | |
| 235 | + | ||
| 236 | + | func (h *dumberRenderHandler) OnImeCompositionRangeChanged( | |
| 237 | + | browser purecef.Browser, | |
| 238 | + | selectedRange *purecef.Range, | |
| 239 | + | characterBounds []purecef.Rect, | |
| 240 | + | ) { | |
| 241 | + | if h == nil || h.main == nil { | |
| 242 | + | return | |
| 243 | + | } | |
| 244 | + | h.main.OnImeCompositionRangeChanged(browser, selectedRange, characterBounds) | |
| 245 | + | } | |
| 246 | + | ||
| 247 | + | func (h *dumberRenderHandler) OnTextSelectionChanged(browser purecef.Browser, selectedText string, selectedRange *purecef.Range) { | |
| 248 | + | if h == nil || h.main == nil { | |
| 249 | + | return | |
| 250 | + | } | |
| 251 | + | h.main.OnTextSelectionChanged(browser, selectedText, selectedRange) | |
| 252 | + | } | |
| 253 | + | ||
| 254 | + | func (h *dumberRenderHandler) OnVirtualKeyboardRequested(browser purecef.Browser, mode purecef.TextInputMode) { | |
| 255 | + | if h == nil || h.main == nil { | |
| 256 | + | return | |
| 33 | 257 | } | |
| 34 | - | return wv.viewBridge.RenderHandler(hooks) | |
| 258 | + | h.main.OnVirtualKeyboardRequested(browser, mode) | |
| 35 | 259 | } | |
| 36 | 260 | ||
| 37 | 261 | func handleRenderTextSelectionChanged(wv *WebView, selectedText string) { | |
| @@ -9,12 +9,28 @@ import ( | |||||
| 9 | "github.com/bnema/dumber/internal/logging" | 9 | "github.com/bnema/dumber/internal/logging" | ||
| 10 | ) | 10 | ) | ||
| 11 | 11 | ||||
| 12 | + | type popupSurface interface { | |||
| 13 | + | SetPopupVisible(bool) | |||
| 14 | + | SetPopupRect(purecef.Rect) | |||
| 15 | + | } | |||
| 16 | + | ||||
| 17 | + | type dumberRenderHandler struct { | |||
| 18 | + | wv *WebView | |||
| 19 | + | main purecef.RenderHandler | |||
| 20 | + | popup purecef.RenderHandler | |||
| 21 | + | popupSurface popupSurface | |||
| 22 | + | } | |||
| 23 | + | ||||
| 24 | + | var _ purecef.RenderHandler = (*dumberRenderHandler)(nil) | |||
| 25 | + | ||||
| 12 | func newDumberRenderHandler(wv *WebView) purecef.RenderHandler { | 26 | func newDumberRenderHandler(wv *WebView) purecef.RenderHandler { | ||
| 13 | if wv == nil || wv.viewBridge == nil { | 27 | if wv == nil || wv.viewBridge == nil { | ||
| 14 | return nil | 28 | return nil | ||
| 15 | } | 29 | } | ||
| 30 | + | h := &dumberRenderHandler{wv: wv} | |||
| 31 | + | ||||
| 16 | var unsupportedPaintOnce sync.Once | 32 | var unsupportedPaintOnce sync.Once | ||
| 17 | - | hooks := cef2gtk.Hooks{ | 33 | + | h.main = wv.viewBridge.RenderHandler(cef2gtk.Hooks{ |
| 18 | OnUnsupportedPaint: func() { | 34 | OnUnsupportedPaint: func() { | ||
| 19 | unsupportedPaintOnce.Do(func() { | 35 | unsupportedPaintOnce.Do(func() { | ||
| 20 | if wv.ctx != nil { | 36 | if wv.ctx != nil { | ||
| @@ -30,8 +46,216 @@ func newDumberRenderHandler(wv *WebView) purecef.RenderHandler { | |||||
| 30 | OnTextSelectionChanged: func(selectedText string, _ *purecef.Range) { | 46 | OnTextSelectionChanged: func(selectedText string, _ *purecef.Range) { | ||
| 31 | handleRenderTextSelectionChanged(wv, selectedText) | 47 | handleRenderTextSelectionChanged(wv, selectedText) | ||
| 32 | }, | 48 | }, | ||
| 49 | + | }) | |||
| 50 | + | ||||
| 51 | + | if wv.popupSurface != nil { | |||
| 52 | + | var popupUnsupportedPaintOnce sync.Once | |||
| 53 | + | h.popupSurface = wv.popupSurface | |||
| 54 | + | h.popup = wv.popupSurface.RenderHandler(cef2gtk.Hooks{ | |||
| 55 | + | OnUnsupportedPaint: func() { | |||
| 56 | + | popupUnsupportedPaintOnce.Do(func() { | |||
| 57 | + | if wv.ctx != nil { | |||
| 58 | + | logging.FromContext(wv.ctx).Warn().Msg("cef: unsupported CPU paint from popup surface") | |||
| 59 | + | } | |||
| 60 | + | }) | |||
| 61 | + | }, | |||
| 62 | + | OnError: func(err error) { | |||
| 63 | + | if wv.ctx != nil { | |||
| 64 | + | logging.FromContext(wv.ctx).Warn().Err(err).Msg("cef: popup surface render error") | |||
| 65 | + | } | |||
| 66 | + | }, | |||
| 67 | + | }) | |||
| 68 | + | } | |||
| 69 | + | ||||
| 70 | + | return h | |||
| 71 | + | } | |||
| 72 | + | ||||
| 73 | + | func (h *dumberRenderHandler) renderTargetForPaint(elementType purecef.PaintElementType) purecef.RenderHandler { | |||
| 74 | + | if h == nil { | |||
| 75 | + | return nil | |||
| 76 | + | } | |||
| 77 | + | if elementType == purecef.PaintElementTypePetPopup && h.popup != nil { | |||
| 78 | + | return h.popup | |||
| 79 | + | } | |||
| 80 | + | return h.main | |||
| 81 | + | } | |||
| 82 | + | ||||
| 83 | + | func (h *dumberRenderHandler) GetAccessibilityHandler() purecef.AccessibilityHandler { | |||
| 84 | + | if h == nil || h.main == nil { | |||
| 85 | + | return nil | |||
| 86 | + | } | |||
| 87 | + | return h.main.GetAccessibilityHandler() | |||
| 88 | + | } | |||
| 89 | + | ||||
| 90 | + | func (h *dumberRenderHandler) GetRootScreenRect(browser purecef.Browser, rect *purecef.Rect) int32 { | |||
| 91 | + | if h == nil || h.main == nil { | |||
| 92 | + | return 0 | |||
| 93 | + | } | |||
| 94 | + | return h.main.GetRootScreenRect(browser, rect) | |||
| 95 | + | } | |||
| 96 | + | ||||
| 97 | + | func (h *dumberRenderHandler) GetViewRect(browser purecef.Browser, rect *purecef.Rect) { | |||
| 98 | + | if h == nil || h.main == nil { | |||
| 99 | + | if rect != nil { | |||
| 100 | + | rect.X, rect.Y, rect.Width, rect.Height = 0, 0, 1, 1 | |||
| 101 | + | } | |||
| 102 | + | return | |||
| 103 | + | } | |||
| 104 | + | h.main.GetViewRect(browser, rect) | |||
| 105 | + | } | |||
| 106 | + | ||||
| 107 | + | func (h *dumberRenderHandler) GetScreenPoint(browser purecef.Browser, viewX, viewY int32, screenX, screenY *int32) int32 { | |||
| 108 | + | if h == nil || h.main == nil { | |||
| 109 | + | return 0 | |||
| 110 | + | } | |||
| 111 | + | return h.main.GetScreenPoint(browser, viewX, viewY, screenX, screenY) | |||
| 112 | + | } | |||
| 113 | + | ||||
| 114 | + | func (h *dumberRenderHandler) GetScreenInfo(browser purecef.Browser, info *purecef.ScreenInfo) int32 { | |||
| 115 | + | if h == nil || h.main == nil { | |||
| 116 | + | return 0 | |||
| 117 | + | } | |||
| 118 | + | return h.main.GetScreenInfo(browser, info) | |||
| 119 | + | } | |||
| 120 | + | ||||
| 121 | + | func (h *dumberRenderHandler) OnPopupShow(browser purecef.Browser, show int32) { | |||
| 122 | + | if h != nil && h.popupSurface != nil { | |||
| 123 | + | visible := show != 0 | |||
| 124 | + | if h.wv != nil { | |||
| 125 | + | h.wv.runOnGTKSync(func() { | |||
| 126 | + | h.popupSurface.SetPopupVisible(visible) | |||
| 127 | + | }) | |||
| 128 | + | } else { | |||
| 129 | + | h.popupSurface.SetPopupVisible(visible) | |||
| 130 | + | } | |||
| 131 | + | } | |||
| 132 | + | if h != nil && h.popup != nil { | |||
| 133 | + | h.popup.OnPopupShow(browser, show) | |||
| 134 | + | } | |||
| 135 | + | } | |||
| 136 | + | ||||
| 137 | + | func (h *dumberRenderHandler) OnPopupSize(browser purecef.Browser, rect *purecef.Rect) { | |||
| 138 | + | if h != nil && h.popupSurface != nil && rect != nil { | |||
| 139 | + | popupRect := *rect | |||
| 140 | + | if h.wv != nil { | |||
| 141 | + | popupRect = popupRectForWidget(h.wv, popupRect) | |||
| 142 | + | h.wv.runOnGTKSync(func() { | |||
| 143 | + | h.popupSurface.SetPopupRect(popupRect) | |||
| 144 | + | }) | |||
| 145 | + | } else { | |||
| 146 | + | h.popupSurface.SetPopupRect(popupRect) | |||
| 147 | + | } | |||
| 148 | + | } | |||
| 149 | + | if h != nil && h.popup != nil { | |||
| 150 | + | h.popup.OnPopupSize(browser, rect) | |||
| 151 | + | } | |||
| 152 | + | } | |||
| 153 | + | ||||
| 154 | + | func popupRectForWidget(wv *WebView, rect purecef.Rect) purecef.Rect { | |||
| 155 | + | if wv == nil { | |||
| 156 | + | return rect | |||
| 157 | + | } | |||
| 158 | + | // Only convert when the OSR backing-scale compatibility path is active. | |||
| 159 | + | // A monitor scale above 1 is not enough by itself because CEF already reports | |||
| 160 | + | // logical popup coordinates when backing-scale mode is off. | |||
| 161 | + | if wv.osrBackingScaleFactor() <= 1 { | |||
| 162 | + | return rect | |||
| 163 | + | } | |||
| 164 | + | scale := wv.viewBridgeScale() | |||
| 165 | + | return purecef.Rect{ | |||
| 166 | + | X: deviceToLogicalCoord(rect.X, scale), | |||
| 167 | + | Y: deviceToLogicalCoord(rect.Y, scale), | |||
| 168 | + | Width: deviceToLogicalSize(rect.Width, scale), | |||
| 169 | + | Height: deviceToLogicalSize(rect.Height, scale), | |||
| 170 | + | } | |||
| 171 | + | } | |||
| 172 | + | ||||
| 173 | + | func (h *dumberRenderHandler) OnPaint( | |||
| 174 | + | browser purecef.Browser, | |||
| 175 | + | elementType purecef.PaintElementType, | |||
| 176 | + | dirtyRects []purecef.Rect, | |||
| 177 | + | buffer []byte, | |||
| 178 | + | width, height int32, | |||
| 179 | + | ) { | |||
| 180 | + | if delegate := h.renderTargetForPaint(elementType); delegate != nil { | |||
| 181 | + | delegate.OnPaint(browser, elementType, dirtyRects, buffer, width, height) | |||
| 182 | + | } | |||
| 183 | + | } | |||
| 184 | + | ||||
| 185 | + | func (h *dumberRenderHandler) OnAcceleratedPaint( | |||
| 186 | + | browser purecef.Browser, | |||
| 187 | + | elementType purecef.PaintElementType, | |||
| 188 | + | dirtyRects []purecef.Rect, | |||
| 189 | + | info *purecef.AcceleratedPaintInfo, | |||
| 190 | + | ) { | |||
| 191 | + | if delegate := h.renderTargetForPaint(elementType); delegate != nil { | |||
| 192 | + | delegate.OnAcceleratedPaint(browser, elementType, dirtyRects, info) | |||
| 193 | + | } | |||
| 194 | + | } | |||
| 195 | + | ||||
| 196 | + | func (h *dumberRenderHandler) GetTouchHandleSize(browser purecef.Browser, orientation purecef.HorizontalAlignment, size *purecef.Size) { | |||
| 197 | + | if h == nil || h.main == nil { | |||
| 198 | + | return | |||
| 199 | + | } | |||
| 200 | + | h.main.GetTouchHandleSize(browser, orientation, size) | |||
| 201 | + | } | |||
| 202 | + | ||||
| 203 | + | func (h *dumberRenderHandler) OnTouchHandleStateChanged(browser purecef.Browser, state *purecef.TouchHandleState) { | |||
| 204 | + | if h == nil || h.main == nil { | |||
| 205 | + | return | |||
| 206 | + | } | |||
| 207 | + | h.main.OnTouchHandleStateChanged(browser, state) | |||
| 208 | + | } | |||
| 209 | + | ||||
| 210 | + | func (h *dumberRenderHandler) StartDragging( | |||
| 211 | + | browser purecef.Browser, | |||
| 212 | + | dragData purecef.DragData, | |||
| 213 | + | allowedOps purecef.DragOperationsMask, | |||
| 214 | + | x, y int32, | |||
| 215 | + | ) int32 { | |||
| 216 | + | if h == nil || h.main == nil { | |||
| 217 | + | return 0 | |||
| 218 | + | } | |||
| 219 | + | return h.main.StartDragging(browser, dragData, allowedOps, x, y) | |||
| 220 | + | } | |||
| 221 | + | ||||
| 222 | + | func (h *dumberRenderHandler) UpdateDragCursor(browser purecef.Browser, operation purecef.DragOperationsMask) { | |||
| 223 | + | if h == nil || h.main == nil { | |||
| 224 | + | return | |||
| 225 | + | } | |||
| 226 | + | h.main.UpdateDragCursor(browser, operation) | |||
| 227 | + | } | |||
| 228 | + | ||||
| 229 | + | func (h *dumberRenderHandler) OnScrollOffsetChanged(browser purecef.Browser, x, y float64) { | |||
| 230 | + | if h == nil || h.main == nil { | |||
| 231 | + | return | |||
| 232 | + | } | |||
| 233 | + | h.main.OnScrollOffsetChanged(browser, x, y) | |||
| 234 | + | } | |||
| 235 | + | ||||
| 236 | + | func (h *dumberRenderHandler) OnImeCompositionRangeChanged( | |||
| 237 | + | browser purecef.Browser, | |||
| 238 | + | selectedRange *purecef.Range, | |||
| 239 | + | characterBounds []purecef.Rect, | |||
| 240 | + | ) { | |||
| 241 | + | if h == nil || h.main == nil { | |||
| 242 | + | return | |||
| 243 | + | } | |||
| 244 | + | h.main.OnImeCompositionRangeChanged(browser, selectedRange, characterBounds) | |||
| 245 | + | } | |||
| 246 | + | ||||
| 247 | + | func (h *dumberRenderHandler) OnTextSelectionChanged(browser purecef.Browser, selectedText string, selectedRange *purecef.Range) { | |||
| 248 | + | if h == nil || h.main == nil { | |||
| 249 | + | return | |||
| 250 | + | } | |||
| 251 | + | h.main.OnTextSelectionChanged(browser, selectedText, selectedRange) | |||
| 252 | + | } | |||
| 253 | + | ||||
| 254 | + | func (h *dumberRenderHandler) OnVirtualKeyboardRequested(browser purecef.Browser, mode purecef.TextInputMode) { | |||
| 255 | + | if h == nil || h.main == nil { | |||
| 256 | + | return | |||
| 33 | } | 257 | } | ||
| 34 | - | return wv.viewBridge.RenderHandler(hooks) | 258 | + | h.main.OnVirtualKeyboardRequested(browser, mode) |
| 35 | } | 259 | } | ||
| 36 | 260 | ||||
| 37 | func handleRenderTextSelectionChanged(wv *WebView, selectedText string) { | 261 | func handleRenderTextSelectionChanged(wv *WebView, selectedText string) { | ||
A internal/infrastructure/cef/render_handler_popup_test.go +71 -0
| @@ -0,0 +1,71 @@ | |||
| 1 | + | package cef | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "testing" | |
| 5 | + | ||
| 6 | + | purecef "github.com/bnema/purego-cef/cef" | |
| 7 | + | cefmocks "github.com/bnema/purego-cef/cef/mocks" | |
| 8 | + | "github.com/stretchr/testify/assert" | |
| 9 | + | ) | |
| 10 | + | ||
| 11 | + | type popupSurfaceRecorder struct { | |
| 12 | + | visible bool | |
| 13 | + | rect purecef.Rect | |
| 14 | + | calls []string | |
| 15 | + | } | |
| 16 | + | ||
| 17 | + | func (r *popupSurfaceRecorder) SetPopupVisible(visible bool) { | |
| 18 | + | r.visible = visible | |
| 19 | + | r.calls = append(r.calls, "visible") | |
| 20 | + | } | |
| 21 | + | ||
| 22 | + | func (r *popupSurfaceRecorder) SetPopupRect(rect purecef.Rect) { | |
| 23 | + | r.rect = rect | |
| 24 | + | r.calls = append(r.calls, "rect") | |
| 25 | + | } | |
| 26 | + | ||
| 27 | + | func TestDumberRenderHandler_RoutesPopupAcceleratedPaintToPopupSurface(t *testing.T) { | |
| 28 | + | mainHandler := cefmocks.NewMockRenderHandler(t) | |
| 29 | + | popupHandler := cefmocks.NewMockRenderHandler(t) | |
| 30 | + | h := &dumberRenderHandler{ | |
| 31 | + | main: mainHandler, | |
| 32 | + | popup: popupHandler, | |
| 33 | + | } | |
| 34 | + | ||
| 35 | + | info := &purecef.AcceleratedPaintInfo{} | |
| 36 | + | popupHandler.EXPECT().OnAcceleratedPaint(nil, purecef.PaintElementTypePetPopup, []purecef.Rect(nil), info).Return().Once() | |
| 37 | + | ||
| 38 | + | h.OnAcceleratedPaint(nil, purecef.PaintElementTypePetPopup, []purecef.Rect(nil), info) | |
| 39 | + | ||
| 40 | + | mainHandler.AssertNotCalled(t, "OnAcceleratedPaint", nil, purecef.PaintElementTypePetPopup, []purecef.Rect(nil), info) | |
| 41 | + | } | |
| 42 | + | ||
| 43 | + | func TestDumberRenderHandler_TracksPopupLifecycle(t *testing.T) { | |
| 44 | + | recorder := &popupSurfaceRecorder{} | |
| 45 | + | // With a nil engine, WebView.runOnGTK/webview.go executes inline so popup | |
| 46 | + | // lifecycle updates stay synchronous in this unit test. | |
| 47 | + | wv := &WebView{} | |
| 48 | + | wv.engine = nil | |
| 49 | + | h := &dumberRenderHandler{ | |
| 50 | + | wv: wv, | |
| 51 | + | popupSurface: recorder, | |
| 52 | + | } | |
| 53 | + | ||
| 54 | + | popupRect := &purecef.Rect{X: 12, Y: 34, Width: 180, Height: 240} | |
| 55 | + | h.OnPopupShow(nil, 1) | |
| 56 | + | h.OnPopupSize(nil, popupRect) | |
| 57 | + | h.OnPopupShow(nil, 0) | |
| 58 | + | ||
| 59 | + | assert.Equal(t, []string{"visible", "rect", "visible"}, recorder.calls) | |
| 60 | + | assert.Equal(t, purecef.Rect{X: 12, Y: 34, Width: 180, Height: 240}, recorder.rect) | |
| 61 | + | assert.False(t, recorder.visible) | |
| 62 | + | } | |
| 63 | + | ||
| 64 | + | func TestDeviceToLogicalSize_RoundsUp(t *testing.T) { | |
| 65 | + | assert.Equal(t, int32(4), deviceToLogicalSize(5, 1.25)) | |
| 66 | + | assert.Equal(t, int32(1), deviceToLogicalSize(1, 1.5)) | |
| 67 | + | assert.Equal(t, int32(0), deviceToLogicalSize(0, 2.0)) | |
| 68 | + | assert.Equal(t, int32(0), deviceToLogicalSize(-1, 2.0)) | |
| 69 | + | assert.Equal(t, int32(10), deviceToLogicalSize(10, 1.0)) | |
| 70 | + | assert.Equal(t, int32(1), deviceToLogicalSize(1, 100.0)) | |
| 71 | + | } | |
| @@ -0,0 +1,71 @@ | |||||
| 1 | + | package cef | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "testing" | |||
| 5 | + | ||||
| 6 | + | purecef "github.com/bnema/purego-cef/cef" | |||
| 7 | + | cefmocks "github.com/bnema/purego-cef/cef/mocks" | |||
| 8 | + | "github.com/stretchr/testify/assert" | |||
| 9 | + | ) | |||
| 10 | + | ||||
| 11 | + | type popupSurfaceRecorder struct { | |||
| 12 | + | visible bool | |||
| 13 | + | rect purecef.Rect | |||
| 14 | + | calls []string | |||
| 15 | + | } | |||
| 16 | + | ||||
| 17 | + | func (r *popupSurfaceRecorder) SetPopupVisible(visible bool) { | |||
| 18 | + | r.visible = visible | |||
| 19 | + | r.calls = append(r.calls, "visible") | |||
| 20 | + | } | |||
| 21 | + | ||||
| 22 | + | func (r *popupSurfaceRecorder) SetPopupRect(rect purecef.Rect) { | |||
| 23 | + | r.rect = rect | |||
| 24 | + | r.calls = append(r.calls, "rect") | |||
| 25 | + | } | |||
| 26 | + | ||||
| 27 | + | func TestDumberRenderHandler_RoutesPopupAcceleratedPaintToPopupSurface(t *testing.T) { | |||
| 28 | + | mainHandler := cefmocks.NewMockRenderHandler(t) | |||
| 29 | + | popupHandler := cefmocks.NewMockRenderHandler(t) | |||
| 30 | + | h := &dumberRenderHandler{ | |||
| 31 | + | main: mainHandler, | |||
| 32 | + | popup: popupHandler, | |||
| 33 | + | } | |||
| 34 | + | ||||
| 35 | + | info := &purecef.AcceleratedPaintInfo{} | |||
| 36 | + | popupHandler.EXPECT().OnAcceleratedPaint(nil, purecef.PaintElementTypePetPopup, []purecef.Rect(nil), info).Return().Once() | |||
| 37 | + | ||||
| 38 | + | h.OnAcceleratedPaint(nil, purecef.PaintElementTypePetPopup, []purecef.Rect(nil), info) | |||
| 39 | + | ||||
| 40 | + | mainHandler.AssertNotCalled(t, "OnAcceleratedPaint", nil, purecef.PaintElementTypePetPopup, []purecef.Rect(nil), info) | |||
| 41 | + | } | |||
| 42 | + | ||||
| 43 | + | func TestDumberRenderHandler_TracksPopupLifecycle(t *testing.T) { | |||
| 44 | + | recorder := &popupSurfaceRecorder{} | |||
| 45 | + | // With a nil engine, WebView.runOnGTK/webview.go executes inline so popup | |||
| 46 | + | // lifecycle updates stay synchronous in this unit test. | |||
| 47 | + | wv := &WebView{} | |||
| 48 | + | wv.engine = nil | |||
| 49 | + | h := &dumberRenderHandler{ | |||
| 50 | + | wv: wv, | |||
| 51 | + | popupSurface: recorder, | |||
| 52 | + | } | |||
| 53 | + | ||||
| 54 | + | popupRect := &purecef.Rect{X: 12, Y: 34, Width: 180, Height: 240} | |||
| 55 | + | h.OnPopupShow(nil, 1) | |||
| 56 | + | h.OnPopupSize(nil, popupRect) | |||
| 57 | + | h.OnPopupShow(nil, 0) | |||
| 58 | + | ||||
| 59 | + | assert.Equal(t, []string{"visible", "rect", "visible"}, recorder.calls) | |||
| 60 | + | assert.Equal(t, purecef.Rect{X: 12, Y: 34, Width: 180, Height: 240}, recorder.rect) | |||
| 61 | + | assert.False(t, recorder.visible) | |||
| 62 | + | } | |||
| 63 | + | ||||
| 64 | + | func TestDeviceToLogicalSize_RoundsUp(t *testing.T) { | |||
| 65 | + | assert.Equal(t, int32(4), deviceToLogicalSize(5, 1.25)) | |||
| 66 | + | assert.Equal(t, int32(1), deviceToLogicalSize(1, 1.5)) | |||
| 67 | + | assert.Equal(t, int32(0), deviceToLogicalSize(0, 2.0)) | |||
| 68 | + | assert.Equal(t, int32(0), deviceToLogicalSize(-1, 2.0)) | |||
| 69 | + | assert.Equal(t, int32(10), deviceToLogicalSize(10, 1.0)) | |||
| 70 | + | assert.Equal(t, int32(1), deviceToLogicalSize(1, 100.0)) | |||
| 71 | + | } | |||
M internal/infrastructure/cef/scale.go +11 -0
| @@ -12,3 +12,14 @@ func normalizeScale(scale float64) float64 { | |||
| 12 | 12 | func deviceToLogicalCoord(value int32, scale float64) int32 { | |
| 13 | 13 | return int32(math.Floor(float64(value) / normalizeScale(scale))) | |
| 14 | 14 | } | |
| 15 | + | ||
| 16 | + | func deviceToLogicalSize(value int32, scale float64) int32 { | |
| 17 | + | if value <= 0 { | |
| 18 | + | return 0 | |
| 19 | + | } | |
| 20 | + | scaled := int32(math.Ceil(float64(value) / normalizeScale(scale))) | |
| 21 | + | if scaled < 1 { | |
| 22 | + | return 1 | |
| 23 | + | } | |
| 24 | + | return scaled | |
| 25 | + | } | |
| @@ -12,3 +12,14 @@ func normalizeScale(scale float64) float64 { | |||||
| 12 | func deviceToLogicalCoord(value int32, scale float64) int32 { | 12 | func deviceToLogicalCoord(value int32, scale float64) int32 { | ||
| 13 | return int32(math.Floor(float64(value) / normalizeScale(scale))) | 13 | return int32(math.Floor(float64(value) / normalizeScale(scale))) | ||
| 14 | } | 14 | } | ||
| 15 | + | ||||
| 16 | + | func deviceToLogicalSize(value int32, scale float64) int32 { | |||
| 17 | + | if value <= 0 { | |||
| 18 | + | return 0 | |||
| 19 | + | } | |||
| 20 | + | scaled := int32(math.Ceil(float64(value) / normalizeScale(scale))) | |||
| 21 | + | if scaled < 1 { | |||
| 22 | + | return 1 | |||
| 23 | + | } | |||
| 24 | + | return scaled | |||
| 25 | + | } | |||
M internal/infrastructure/cef/webview.go +30 -2
| @@ -79,6 +79,8 @@ type WebView struct { | |||
| 79 | 79 | host purecef.BrowserHost | |
| 80 | 80 | client purecef.RawClient // prevent GC from collecting the client before CEF AddRef's it | |
| 81 | 81 | viewBridge *Cef2gtkAdapter | |
| 82 | + | popupSurface *popupBridgeSurface | |
| 83 | + | nativeWidget *gtk.Widget | |
| 82 | 84 | handlers *handlerSet | |
| 83 | 85 | findCtrl *cefFindController | |
| 84 | 86 | profileCleanup func() | |
| @@ -1156,9 +1158,14 @@ func (wv *WebView) destroyViewBridgeOnGTKThread() { | |||
| 1156 | 1158 | wv.removeSizeObserver = nil | |
| 1157 | 1159 | } | |
| 1158 | 1160 | wv.disconnectViewportSyncHooksOnGTKThread() | |
| 1161 | + | if wv.popupSurface != nil { | |
| 1162 | + | wv.popupSurface.DestroyOnGTKThread() | |
| 1163 | + | wv.popupSurface = nil | |
| 1164 | + | } | |
| 1159 | 1165 | _ = wv.viewBridge.DetachInput() | |
| 1160 | 1166 | _ = wv.viewBridge.Destroy() | |
| 1161 | 1167 | wv.viewBridge = nil | |
| 1168 | + | wv.nativeWidget = nil | |
| 1162 | 1169 | } | |
| 1163 | 1170 | ||
| 1164 | 1171 | // --------------------------------------------------------------------------- | |
| @@ -1167,12 +1174,15 @@ func (wv *WebView) destroyViewBridgeOnGTKThread() { | |||
| 1167 | 1174 | ||
| 1168 | 1175 | // NativeWidget returns the uintptr for embedding the bridge GTK widget. | |
| 1169 | 1176 | func (wv *WebView) NativeWidget() uintptr { | |
| 1170 | - | if wv.viewBridge == nil { | |
| 1177 | + | if wv == nil { | |
| 1171 | 1178 | return 0 | |
| 1172 | 1179 | } | |
| 1173 | 1180 | var ptr uintptr | |
| 1174 | 1181 | wv.runOnGTKSync(func() { | |
| 1175 | - | if wv.viewBridge != nil { | |
| 1182 | + | switch { | |
| 1183 | + | case wv.nativeWidget != nil: | |
| 1184 | + | ptr = wv.nativeWidget.GoPointer() | |
| 1185 | + | case wv.viewBridge != nil: | |
| 1176 | 1186 | ptr = wv.viewBridge.NativeWidget() | |
| 1177 | 1187 | } | |
| 1178 | 1188 | }) | |
| @@ -1403,6 +1413,24 @@ func (wv *WebView) selectedTextSnapshot() string { | |||
| 1403 | 1413 | return wv.selectedText | |
| 1404 | 1414 | } | |
| 1405 | 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 | + | ||
| 1406 | 1434 | func (wv *WebView) viewBridgeScale() float64 { | |
| 1407 | 1435 | if wv == nil || wv.viewBridge == nil { | |
| 1408 | 1436 | return 1 | |
| @@ -79,6 +79,8 @@ type WebView struct { | |||||
| 79 | host purecef.BrowserHost | 79 | host purecef.BrowserHost | ||
| 80 | client purecef.RawClient // prevent GC from collecting the client before CEF AddRef's it | 80 | client purecef.RawClient // prevent GC from collecting the client before CEF AddRef's it | ||
| 81 | viewBridge *Cef2gtkAdapter | 81 | viewBridge *Cef2gtkAdapter | ||
| 82 | + | popupSurface *popupBridgeSurface | |||
| 83 | + | nativeWidget *gtk.Widget | |||
| 82 | handlers *handlerSet | 84 | handlers *handlerSet | ||
| 83 | findCtrl *cefFindController | 85 | findCtrl *cefFindController | ||
| 84 | profileCleanup func() | 86 | profileCleanup func() | ||
| @@ -1156,9 +1158,14 @@ func (wv *WebView) destroyViewBridgeOnGTKThread() { | |||||
| 1156 | wv.removeSizeObserver = nil | 1158 | wv.removeSizeObserver = nil | ||
| 1157 | } | 1159 | } | ||
| 1158 | wv.disconnectViewportSyncHooksOnGTKThread() | 1160 | wv.disconnectViewportSyncHooksOnGTKThread() | ||
| 1161 | + | if wv.popupSurface != nil { | |||
| 1162 | + | wv.popupSurface.DestroyOnGTKThread() | |||
| 1163 | + | wv.popupSurface = nil | |||
| 1164 | + | } | |||
| 1159 | _ = wv.viewBridge.DetachInput() | 1165 | _ = wv.viewBridge.DetachInput() | ||
| 1160 | _ = wv.viewBridge.Destroy() | 1166 | _ = wv.viewBridge.Destroy() | ||
| 1161 | wv.viewBridge = nil | 1167 | wv.viewBridge = nil | ||
| 1168 | + | wv.nativeWidget = nil | |||
| 1162 | } | 1169 | } | ||
| 1163 | 1170 | ||||
| 1164 | // --------------------------------------------------------------------------- | 1171 | // --------------------------------------------------------------------------- | ||
| @@ -1167,12 +1174,15 @@ func (wv *WebView) destroyViewBridgeOnGTKThread() { | |||||
| 1167 | 1174 | ||||
| 1168 | // NativeWidget returns the uintptr for embedding the bridge GTK widget. | 1175 | // NativeWidget returns the uintptr for embedding the bridge GTK widget. | ||
| 1169 | func (wv *WebView) NativeWidget() uintptr { | 1176 | func (wv *WebView) NativeWidget() uintptr { | ||
| 1170 | - | if wv.viewBridge == nil { | 1177 | + | if wv == nil { |
| 1171 | return 0 | 1178 | return 0 | ||
| 1172 | } | 1179 | } | ||
| 1173 | var ptr uintptr | 1180 | var ptr uintptr | ||
| 1174 | wv.runOnGTKSync(func() { | 1181 | wv.runOnGTKSync(func() { | ||
| 1175 | - | if wv.viewBridge != nil { | 1182 | + | switch { |
| 1183 | + | case wv.nativeWidget != nil: | |||
| 1184 | + | ptr = wv.nativeWidget.GoPointer() | |||
| 1185 | + | case wv.viewBridge != nil: | |||
| 1176 | ptr = wv.viewBridge.NativeWidget() | 1186 | ptr = wv.viewBridge.NativeWidget() | ||
| 1177 | } | 1187 | } | ||
| 1178 | }) | 1188 | }) | ||
| @@ -1403,6 +1413,24 @@ func (wv *WebView) selectedTextSnapshot() string { | |||||
| 1403 | return wv.selectedText | 1413 | return wv.selectedText | ||
| 1404 | } | 1414 | } | ||
| 1405 | 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 | + | ||||
| 1406 | func (wv *WebView) viewBridgeScale() float64 { | 1434 | func (wv *WebView) viewBridgeScale() float64 { | ||
| 1407 | if wv == nil || wv.viewBridge == nil { | 1435 | if wv == nil || wv.viewBridge == nil { | ||
| 1408 | 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 | + | } | |||