1package cef
2
3import (
4 "sync"
5
6 purecef "github.com/bnema/purego-cef/cef"
7 cef2gtk "github.com/bnema/purego-cef2gtk"
8
9 "github.com/bnema/dumber/internal/logging"
10)
11
12type popupSurface interface {
13 SetPopupVisible(bool)
14 SetPopupRect(purecef.Rect)
15}
16
17type dumberRenderHandler struct {
18 wv *WebView
19 main purecef.RenderHandler
20 popup purecef.RenderHandler
21 popupSurface popupSurface
22 a11y purecef.AccessibilityHandler
23}
24
25var _ purecef.RenderHandler = (*dumberRenderHandler)(nil)
26
27// startupPresentationHooks consumes the one-shot facts emitted by
28// purego-cef2gtk. The bridge owns the native DMABUF and frame-clock boundaries;
29// Dumber only records their ordered application-level timeline.
30func startupPresentationHooks(trace *startupTrace) cef2gtk.Hooks {
31 return cef2gtk.Hooks{
32 OnFirstAcceleratedPaint: func() {
33 trace.Mark("first_accelerated_paint_received")
34 },
35 OnFirstDMABUFTextureSwap: func() {
36 trace.Mark("first_dmabuf_texture_swap")
37 },
38 OnFirstPresentation: func() {
39 trace.MarkGTKAfterPaint()
40 },
41 OnDMABUFUnsupported: func() {
42 trace.SetIncompleteReason("dmabuf_texture_swap_unavailable")
43 },
44 }
45}
46
47func newDumberRenderHandler(wv *WebView) purecef.RenderHandler {
48 if wv == nil || wv.viewBridge == nil {
49 return nil
50 }
51 h := &dumberRenderHandler{wv: wv}
52 // Install a non-nil AccessibilityHandler before CEF wraps the render
53 // handler. Outside capture the sink is truly nil so callbacks return before
54 // WriteJson; capture workers (Task 2) wire enqueueAccessibilityPayload.
55 var sink func(accessibilityPayload) bool
56 if wv.a11yWorker != nil {
57 sink = wv.enqueueAccessibilityPayload
58 }
59 h.a11y = newAccessibilityHandler(sink)
60
61 var unsupportedPaintOnce sync.Once
62 hooks := startupPresentationHooks(activeStartupTrace())
63 // Tear down the theme background flash guard once the first frame is
64 // presented, without dropping the startup trace mark the bridge already set.
65 firstAcceleratedPaint := hooks.OnFirstAcceleratedPaint
66 hooks.OnFirstAcceleratedPaint = func() {
67 if firstAcceleratedPaint != nil {
68 firstAcceleratedPaint()
69 }
70 wv.markFirstFramePainted()
71 }
72 hooks.OnUnsupportedPaint = func() {
73 unsupportedPaintOnce.Do(func() {
74 if wv.ctx != nil {
75 logging.FromContext(wv.ctx).Warn().Msg("cef: unsupported CPU paint from accelerated bridge")
76 }
77 })
78 }
79 hooks.OnError = func(err error) {
80 if wv.ctx != nil {
81 logging.FromContext(wv.ctx).Warn().Err(err).Msg("cef: accelerated render bridge error")
82 }
83 }
84 hooks.OnTextSelectionChanged = func(selectedText string, _ *purecef.Range) {
85 handleRenderTextSelectionChanged(wv, selectedText)
86 }
87 h.main = wv.viewBridge.RenderHandler(hooks)
88
89 if wv.popupSurface != nil {
90 var popupUnsupportedPaintOnce sync.Once
91 h.popupSurface = wv.popupSurface
92 h.popup = wv.popupSurface.RenderHandler(cef2gtk.Hooks{
93 OnUnsupportedPaint: func() {
94 popupUnsupportedPaintOnce.Do(func() {
95 if wv.ctx != nil {
96 logging.FromContext(wv.ctx).Warn().Msg("cef: unsupported CPU paint from popup surface")
97 }
98 })
99 },
100 OnError: func(err error) {
101 if wv.ctx != nil {
102 logging.FromContext(wv.ctx).Warn().Err(err).Msg("cef: popup surface render error")
103 }
104 },
105 })
106 }
107
108 return h
109}
110
111func (h *dumberRenderHandler) renderTargetForPaint(elementType purecef.PaintElementType) purecef.RenderHandler {
112 if h == nil {
113 return nil
114 }
115 if elementType == purecef.PaintElementTypePetPopup && h.popup != nil {
116 return h.popup
117 }
118 return h.main
119}
120
121func (h *dumberRenderHandler) GetAccessibilityHandler() purecef.AccessibilityHandler {
122 if h == nil {
123 return nil
124 }
125 return h.a11y
126}
127
128func (h *dumberRenderHandler) GetRootScreenRect(browser purecef.Browser, rect *purecef.Rect) int32 {
129 if h == nil || h.main == nil {
130 return 0
131 }
132 return h.main.GetRootScreenRect(browser, rect)
133}
134
135func (h *dumberRenderHandler) GetViewRect(browser purecef.Browser, rect *purecef.Rect) {
136 if h == nil || h.main == nil {
137 if rect != nil {
138 rect.X, rect.Y, rect.Width, rect.Height = 0, 0, 1, 1
139 }
140 return
141 }
142 h.main.GetViewRect(browser, rect)
143}
144
145func (h *dumberRenderHandler) GetScreenPoint(browser purecef.Browser, viewX, viewY int32, screenX, screenY *int32) int32 {
146 if h == nil || h.main == nil {
147 return 0
148 }
149 return h.main.GetScreenPoint(browser, viewX, viewY, screenX, screenY)
150}
151
152func (h *dumberRenderHandler) GetScreenInfo(browser purecef.Browser, info *purecef.ScreenInfo) int32 {
153 if h == nil || h.main == nil {
154 return 0
155 }
156 return h.main.GetScreenInfo(browser, info)
157}
158
159func (h *dumberRenderHandler) OnPopupShow(browser purecef.Browser, show int32) {
160 if h != nil && h.popupSurface != nil {
161 visible := show != 0
162 if h.wv != nil {
163 h.wv.runOnGTKSync(func() {
164 h.popupSurface.SetPopupVisible(visible)
165 })
166 } else {
167 h.popupSurface.SetPopupVisible(visible)
168 }
169 }
170 if h != nil && h.popup != nil {
171 h.popup.OnPopupShow(browser, show)
172 }
173}
174
175func (h *dumberRenderHandler) OnPopupSize(browser purecef.Browser, rect *purecef.Rect) {
176 if h != nil && h.popupSurface != nil && rect != nil {
177 popupRect := *rect
178 if h.wv != nil {
179 popupRect = popupRectForWidget(h.wv, popupRect)
180 h.wv.runOnGTKSync(func() {
181 h.popupSurface.SetPopupRect(popupRect)
182 })
183 } else {
184 h.popupSurface.SetPopupRect(popupRect)
185 }
186 }
187 if h != nil && h.popup != nil {
188 h.popup.OnPopupSize(browser, rect)
189 }
190}
191
192func popupRectForWidget(wv *WebView, rect purecef.Rect) purecef.Rect {
193 if wv == nil {
194 return rect
195 }
196 // Only convert when the OSR backing-scale compatibility path is active.
197 // A monitor scale above 1 is not enough by itself because CEF already reports
198 // logical popup coordinates when backing-scale mode is off.
199 if wv.osrBackingScaleFactor() <= 1 {
200 return rect
201 }
202 scale := wv.viewBridgeScale()
203 return purecef.Rect{
204 X: deviceToLogicalCoord(rect.X, scale),
205 Y: deviceToLogicalCoord(rect.Y, scale),
206 Width: deviceToLogicalSize(rect.Width, scale),
207 Height: deviceToLogicalSize(rect.Height, scale),
208 }
209}
210
211func (h *dumberRenderHandler) OnPaint(
212 browser purecef.Browser,
213 elementType purecef.PaintElementType,
214 dirtyRects []purecef.Rect,
215 buffer []byte,
216 width, height int32,
217) {
218 if delegate := h.renderTargetForPaint(elementType); delegate != nil {
219 delegate.OnPaint(browser, elementType, dirtyRects, buffer, width, height)
220 }
221}
222
223func (h *dumberRenderHandler) OnAcceleratedPaint(
224 browser purecef.Browser,
225 elementType purecef.PaintElementType,
226 dirtyRects []purecef.Rect,
227 info *purecef.AcceleratedPaintInfo,
228) {
229 if delegate := h.renderTargetForPaint(elementType); delegate != nil {
230 delegate.OnAcceleratedPaint(browser, elementType, dirtyRects, info)
231 }
232}
233
234func (h *dumberRenderHandler) GetTouchHandleSize(browser purecef.Browser, orientation purecef.HorizontalAlignment, size *purecef.Size) {
235 if h == nil || h.main == nil {
236 return
237 }
238 h.main.GetTouchHandleSize(browser, orientation, size)
239}
240
241func (h *dumberRenderHandler) OnTouchHandleStateChanged(browser purecef.Browser, state *purecef.TouchHandleState) {
242 if h == nil || h.main == nil {
243 return
244 }
245 h.main.OnTouchHandleStateChanged(browser, state)
246}
247
248func (h *dumberRenderHandler) StartDragging(
249 browser purecef.Browser,
250 dragData purecef.DragData,
251 allowedOps purecef.DragOperationsMask,
252 x, y int32,
253) int32 {
254 if h == nil || h.main == nil {
255 return 0
256 }
257 return h.main.StartDragging(browser, dragData, allowedOps, x, y)
258}
259
260func (h *dumberRenderHandler) UpdateDragCursor(browser purecef.Browser, operation purecef.DragOperationsMask) {
261 if h == nil || h.main == nil {
262 return
263 }
264 h.main.UpdateDragCursor(browser, operation)
265}
266
267func (h *dumberRenderHandler) OnScrollOffsetChanged(browser purecef.Browser, x, y float64) {
268 if h == nil || h.main == nil {
269 return
270 }
271 h.main.OnScrollOffsetChanged(browser, x, y)
272}
273
274func (h *dumberRenderHandler) OnImeCompositionRangeChanged(
275 browser purecef.Browser,
276 selectedRange *purecef.Range,
277 characterBounds []purecef.Rect,
278) {
279 if h == nil || h.main == nil {
280 return
281 }
282 h.main.OnImeCompositionRangeChanged(browser, selectedRange, characterBounds)
283}
284
285func (h *dumberRenderHandler) OnTextSelectionChanged(browser purecef.Browser, selectedText string, selectedRange *purecef.Range) {
286 if h == nil || h.main == nil {
287 return
288 }
289 h.main.OnTextSelectionChanged(browser, selectedText, selectedRange)
290}
291
292func (h *dumberRenderHandler) OnVirtualKeyboardRequested(browser purecef.Browser, mode purecef.TextInputMode) {
293 if h == nil || h.main == nil {
294 return
295 }
296 h.main.OnVirtualKeyboardRequested(browser, mode)
297}
298
299func handleRenderTextSelectionChanged(wv *WebView, selectedText string) {
300 if wv == nil {
301 return
302 }
303 previous, changed := wv.setSelectedText(selectedText)
304 if !changed {
305 return
306 }
307 if wv.ctx != nil && selectedText == "" {
308 if previous != "" {
309 logging.FromContext(wv.ctx).Debug().
310 Int("prev_text_len", len(previous)).
311 Msg("cef: text selection cleared")
312 }
313 } else if wv.ctx != nil {
314 logging.FromContext(wv.ctx).Debug().
315 Int("text_len", len(selectedText)).
316 Msg("cef: text selection changed")
317 }
318 if wv.engine != nil {
319 wv.scheduleSelectionUpdate(selectedText)
320 }
321}