fix(cef): address popup surface review findings
- commit
- 8f62984be35e709fdca7dd17e044005920388c91
- author
- brice · · 2026-05-21 13:53 UTC
- committer
- brice · · 2026-05-21 14:19 UTC
- parent
- 68ec9b2
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
7 files · +517 -11
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 +155 -0
| @@ -0,0 +1,155 @@ | |||
| 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.SetFocusOnClick(true) | |
| 65 | + | root.SetFocusChild(mainWidget) | |
| 66 | + | return &popupBridgeSurface{ | |
| 67 | + | ctx: ctx, | |
| 68 | + | root: root, | |
| 69 | + | mainWidget: mainWidget, | |
| 70 | + | bridge: bridge, | |
| 71 | + | popupWidget: popupWidget, | |
| 72 | + | } | |
| 73 | + | } | |
| 74 | + | ||
| 75 | + | func (s *popupBridgeSurface) RootWidget() *gtk.Widget { | |
| 76 | + | if s == nil { | |
| 77 | + | return nil | |
| 78 | + | } | |
| 79 | + | return s.root | |
| 80 | + | } | |
| 81 | + | ||
| 82 | + | func (s *popupBridgeSurface) RenderHandler(hooks cef2gtk.Hooks) purecef.RenderHandler { | |
| 83 | + | if s == nil || s.bridge == nil { | |
| 84 | + | return nil | |
| 85 | + | } | |
| 86 | + | return s.bridge.RenderHandler(hooks) | |
| 87 | + | } | |
| 88 | + | ||
| 89 | + | func (s *popupBridgeSurface) PrepareOnGTKThread() error { | |
| 90 | + | if s == nil || s.bridge == nil { | |
| 91 | + | return nil | |
| 92 | + | } | |
| 93 | + | if !s.prepared.CompareAndSwap(0, 1) { | |
| 94 | + | return nil | |
| 95 | + | } | |
| 96 | + | if err := s.bridge.PrepareOnGTKThread(); err != nil { | |
| 97 | + | s.prepared.Store(0) | |
| 98 | + | return err | |
| 99 | + | } | |
| 100 | + | return nil | |
| 101 | + | } | |
| 102 | + | ||
| 103 | + | func (s *popupBridgeSurface) SetPopupVisible(visible bool) { | |
| 104 | + | if s == nil || s.popupWidget == nil { | |
| 105 | + | return | |
| 106 | + | } | |
| 107 | + | if visible { | |
| 108 | + | if err := s.PrepareOnGTKThread(); err != nil { | |
| 109 | + | logging.FromContext(popupSurfaceLogContext(s.ctx)).Error(). | |
| 110 | + | Err(err). | |
| 111 | + | Msg("cef: popupBridgeSurface.SetPopupVisible failed to PrepareOnGTKThread") | |
| 112 | + | return | |
| 113 | + | } | |
| 114 | + | } | |
| 115 | + | s.popupWidget.SetVisible(visible) | |
| 116 | + | if s.root != nil { | |
| 117 | + | s.root.QueueAllocate() | |
| 118 | + | } | |
| 119 | + | } | |
| 120 | + | ||
| 121 | + | func (s *popupBridgeSurface) SetPopupRect(rect purecef.Rect) { | |
| 122 | + | if s == nil || s.popupWidget == nil { | |
| 123 | + | return | |
| 124 | + | } | |
| 125 | + | if rect.Width <= 0 || rect.Height <= 0 { | |
| 126 | + | s.popupWidget.SetVisible(false) | |
| 127 | + | return | |
| 128 | + | } | |
| 129 | + | s.popupWidget.SetMarginStart(int(rect.X)) | |
| 130 | + | s.popupWidget.SetMarginTop(int(rect.Y)) | |
| 131 | + | s.popupWidget.SetSizeRequest(int(rect.Width), int(rect.Height)) | |
| 132 | + | s.popupWidget.QueueResize() | |
| 133 | + | if s.root != nil { | |
| 134 | + | s.root.QueueResize() | |
| 135 | + | } | |
| 136 | + | } | |
| 137 | + | ||
| 138 | + | func popupSurfaceLogContext(ctx context.Context) context.Context { | |
| 139 | + | if ctx != nil { | |
| 140 | + | return ctx | |
| 141 | + | } | |
| 142 | + | return context.Background() | |
| 143 | + | } | |
| 144 | + | ||
| 145 | + | func (s *popupBridgeSurface) DestroyOnGTKThread() { | |
| 146 | + | if s == nil || s.bridge == nil { | |
| 147 | + | return | |
| 148 | + | } | |
| 149 | + | _ = s.bridge.Destroy() | |
| 150 | + | s.bridge = nil | |
| 151 | + | s.popupWidget = nil | |
| 152 | + | s.mainWidget = nil | |
| 153 | + | s.root = nil | |
| 154 | + | s.prepared.Store(0) | |
| 155 | + | } | |
| @@ -0,0 +1,155 @@ | |||||
| 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.SetFocusOnClick(true) | |||
| 65 | + | root.SetFocusChild(mainWidget) | |||
| 66 | + | return &popupBridgeSurface{ | |||
| 67 | + | ctx: ctx, | |||
| 68 | + | root: root, | |||
| 69 | + | mainWidget: mainWidget, | |||
| 70 | + | bridge: bridge, | |||
| 71 | + | popupWidget: popupWidget, | |||
| 72 | + | } | |||
| 73 | + | } | |||
| 74 | + | ||||
| 75 | + | func (s *popupBridgeSurface) RootWidget() *gtk.Widget { | |||
| 76 | + | if s == nil { | |||
| 77 | + | return nil | |||
| 78 | + | } | |||
| 79 | + | return s.root | |||
| 80 | + | } | |||
| 81 | + | ||||
| 82 | + | func (s *popupBridgeSurface) RenderHandler(hooks cef2gtk.Hooks) purecef.RenderHandler { | |||
| 83 | + | if s == nil || s.bridge == nil { | |||
| 84 | + | return nil | |||
| 85 | + | } | |||
| 86 | + | return s.bridge.RenderHandler(hooks) | |||
| 87 | + | } | |||
| 88 | + | ||||
| 89 | + | func (s *popupBridgeSurface) PrepareOnGTKThread() error { | |||
| 90 | + | if s == nil || s.bridge == nil { | |||
| 91 | + | return nil | |||
| 92 | + | } | |||
| 93 | + | if !s.prepared.CompareAndSwap(0, 1) { | |||
| 94 | + | return nil | |||
| 95 | + | } | |||
| 96 | + | if err := s.bridge.PrepareOnGTKThread(); err != nil { | |||
| 97 | + | s.prepared.Store(0) | |||
| 98 | + | return err | |||
| 99 | + | } | |||
| 100 | + | return nil | |||
| 101 | + | } | |||
| 102 | + | ||||
| 103 | + | func (s *popupBridgeSurface) SetPopupVisible(visible bool) { | |||
| 104 | + | if s == nil || s.popupWidget == nil { | |||
| 105 | + | return | |||
| 106 | + | } | |||
| 107 | + | if visible { | |||
| 108 | + | if err := s.PrepareOnGTKThread(); err != nil { | |||
| 109 | + | logging.FromContext(popupSurfaceLogContext(s.ctx)).Error(). | |||
| 110 | + | Err(err). | |||
| 111 | + | Msg("cef: popupBridgeSurface.SetPopupVisible failed to PrepareOnGTKThread") | |||
| 112 | + | return | |||
| 113 | + | } | |||
| 114 | + | } | |||
| 115 | + | s.popupWidget.SetVisible(visible) | |||
| 116 | + | if s.root != nil { | |||
| 117 | + | s.root.QueueAllocate() | |||
| 118 | + | } | |||
| 119 | + | } | |||
| 120 | + | ||||
| 121 | + | func (s *popupBridgeSurface) SetPopupRect(rect purecef.Rect) { | |||
| 122 | + | if s == nil || s.popupWidget == nil { | |||
| 123 | + | return | |||
| 124 | + | } | |||
| 125 | + | if rect.Width <= 0 || rect.Height <= 0 { | |||
| 126 | + | s.popupWidget.SetVisible(false) | |||
| 127 | + | return | |||
| 128 | + | } | |||
| 129 | + | s.popupWidget.SetMarginStart(int(rect.X)) | |||
| 130 | + | s.popupWidget.SetMarginTop(int(rect.Y)) | |||
| 131 | + | s.popupWidget.SetSizeRequest(int(rect.Width), int(rect.Height)) | |||
| 132 | + | s.popupWidget.QueueResize() | |||
| 133 | + | if s.root != nil { | |||
| 134 | + | s.root.QueueResize() | |||
| 135 | + | } | |||
| 136 | + | } | |||
| 137 | + | ||||
| 138 | + | func popupSurfaceLogContext(ctx context.Context) context.Context { | |||
| 139 | + | if ctx != nil { | |||
| 140 | + | return ctx | |||
| 141 | + | } | |||
| 142 | + | return context.Background() | |||
| 143 | + | } | |||
| 144 | + | ||||
| 145 | + | func (s *popupBridgeSurface) DestroyOnGTKThread() { | |||
| 146 | + | if s == nil || s.bridge == nil { | |||
| 147 | + | return | |||
| 148 | + | } | |||
| 149 | + | _ = s.bridge.Destroy() | |||
| 150 | + | s.bridge = nil | |||
| 151 | + | s.popupWidget = nil | |||
| 152 | + | s.mainWidget = nil | |||
| 153 | + | s.root = nil | |||
| 154 | + | s.prepared.Store(0) | |||
| 155 | + | } | |||
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 +12 -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 | }) | |
| @@ -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 | }) | ||
M internal/ui/layout/gtk_widgets.go +31 -7
| @@ -27,13 +27,26 @@ 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 { return w.inner.GrabFocus() } | |
| 36 | - | func (w *gtkWidget) HasFocus() bool { return w.inner.HasFocus() } | |
| 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 | + | } | |
| 37 | 50 | func (w *gtkWidget) SetCanFocus(canFocus bool) { w.inner.SetCanFocus(canFocus) } | |
| 38 | 51 | func (w *gtkWidget) SetFocusable(focusable bool) { w.inner.SetFocusable(focusable) } | |
| 39 | 52 | func (w *gtkWidget) SetFocusOnClick(focus bool) { w.inner.SetFocusOnClick(focus) } | |
| @@ -79,6 +92,17 @@ func (w *gtkWidget) ComputePoint(target Widget) (x, y float64, ok bool) { | |||
| 79 | 92 | return float64(outPoint.X), float64(outPoint.Y), true | |
| 80 | 93 | } | |
| 81 | 94 | ||
| 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 | + | ||
| 82 | 106 | // gtkPaned wraps gtk.Paned to implement PanedWidget. | |
| 83 | 107 | type gtkPaned struct { | |
| 84 | 108 | inner *gtk.Paned | |
| @@ -27,13 +27,26 @@ 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 { return w.inner.GrabFocus() } | 35 | + | func (w *gtkWidget) GrabFocus() bool { |
| 36 | - | func (w *gtkWidget) HasFocus() bool { return w.inner.HasFocus() } | 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 | + | } | |||
| 37 | func (w *gtkWidget) SetCanFocus(canFocus bool) { w.inner.SetCanFocus(canFocus) } | 50 | func (w *gtkWidget) SetCanFocus(canFocus bool) { w.inner.SetCanFocus(canFocus) } | ||
| 38 | func (w *gtkWidget) SetFocusable(focusable bool) { w.inner.SetFocusable(focusable) } | 51 | func (w *gtkWidget) SetFocusable(focusable bool) { w.inner.SetFocusable(focusable) } | ||
| 39 | func (w *gtkWidget) SetFocusOnClick(focus bool) { w.inner.SetFocusOnClick(focus) } | 52 | func (w *gtkWidget) SetFocusOnClick(focus bool) { w.inner.SetFocusOnClick(focus) } | ||
| @@ -79,6 +92,17 @@ func (w *gtkWidget) ComputePoint(target Widget) (x, y float64, ok bool) { | |||||
| 79 | return float64(outPoint.X), float64(outPoint.Y), true | 92 | return float64(outPoint.X), float64(outPoint.Y), true | ||
| 80 | } | 93 | } | ||
| 81 | 94 | ||||
| 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 | + | ||||
| 82 | // gtkPaned wraps gtk.Paned to implement PanedWidget. | 106 | // gtkPaned wraps gtk.Paned to implement PanedWidget. | ||
| 83 | type gtkPaned struct { | 107 | type gtkPaned struct { | ||
| 84 | inner *gtk.Paned | 108 | inner *gtk.Paned | ||