v0.33.2 57f21d6

render_handler_adapter.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close