v0.33.0 62a09f5

adaptive_frame_rate.go RawHistoryFolder

1package cef
2
3import (
4	"strings"
5
6	cef2gtk "github.com/bnema/purego-cef2gtk"
7	"github.com/bnema/puregotk/v4/glib"
8	"github.com/bnema/puregotk/v4/gtk"
9
10	"github.com/bnema/dumber/internal/logging"
11)
12
13const (
14	adaptiveFrameRatePollIntervalMS = 1000
15	adaptiveFrameRateMinFPS         = 30
16)
17
18func adaptiveFrameRateForRefresh(refreshRateMilliHz int, fallbackFPS, maxFPS int32) int32 {
19	return cef2gtk.WindowlessFrameRateForMonitorRefresh(refreshRateMilliHz, cef2gtk.RefreshRateOptions{
20		DefaultFPS: fallbackFPS,
21		MinFPS:     adaptiveFrameRateMinFPS,
22		MaxFPS:     maxFPS,
23	})
24}
25
26// resolveWindowlessFrameRate applies a pinned rate on top of the configured OSR
27// frame rate. A pinned rate wins over both the configured value and the adaptive
28// monitor polling, which is suspended so the pinned cadence is what CEF keeps.
29func resolveWindowlessFrameRate(configRate int32, adaptive bool) (int32, bool) {
30	if pinned, ok := windowlessFrameRateOverride(); ok {
31		return pinned, false
32	}
33	return normalizedWindowlessFrameRate(configRate), adaptive
34}
35
36func (wv *WebView) scheduleStartAdaptiveFrameRatePolling() {
37	if wv == nil || !wv.adaptiveWindowlessFrameRate {
38		return
39	}
40	wv.runOnGTK(func() { wv.startAdaptiveFrameRatePolling() })
41}
42
43func (wv *WebView) scheduleStopAdaptiveFrameRatePolling() {
44	if wv == nil {
45		return
46	}
47	wv.runOnGTK(func() { wv.stopAdaptiveFrameRatePolling() })
48}
49
50func (wv *WebView) startAdaptiveFrameRatePolling() {
51	if wv == nil || wv.destroyed.Load() || !wv.adaptiveWindowlessFrameRate || wv.viewBridge == nil || wv.viewBridge.Widget() == nil {
52		return
53	}
54
55	wv.mu.Lock()
56	if wv.adaptiveFrameRatePollID != 0 || wv.host == nil {
57		wv.mu.Unlock()
58		return
59	}
60	cb := new(glib.SourceFunc)
61	*cb = func(uintptr) bool {
62		if wv.destroyed.Load() {
63			return false
64		}
65		wv.applyAdaptiveWindowlessFrameRate()
66		return true
67	}
68	wv.adaptiveFrameRatePoll = cb
69	wv.adaptiveFrameRatePollID = glib.TimeoutAdd(adaptiveFrameRatePollIntervalMS, cb, 0)
70	wv.mu.Unlock()
71
72	wv.applyAdaptiveWindowlessFrameRate()
73}
74
75func (wv *WebView) stopAdaptiveFrameRatePolling() {
76	if wv == nil {
77		return
78	}
79	wv.mu.Lock()
80	pollID := wv.adaptiveFrameRatePollID
81	wv.adaptiveFrameRatePollID = 0
82	wv.adaptiveFrameRatePoll = nil
83	wv.mu.Unlock()
84	if pollID != 0 {
85		glib.SourceRemove(pollID)
86	}
87}
88
89func (wv *WebView) applyAdaptiveWindowlessFrameRate() {
90	if wv == nil || !wv.adaptiveWindowlessFrameRate || wv.viewBridge == nil {
91		return
92	}
93	widget := wv.viewBridge.Widget()
94	refreshRateMilliHz, displayName, ok := widgetMonitorRefreshRateMilliHz(widget)
95	if !ok || !isWaylandDisplayName(displayName) {
96		return
97	}
98	fps := adaptiveFrameRateForRefresh(refreshRateMilliHz, wv.windowlessFrameRate, wv.windowlessFrameRateMax)
99
100	wv.mu.Lock()
101	host := wv.host
102	if host == nil || fps == wv.lastAdaptiveFrameRate {
103		wv.mu.Unlock()
104		return
105	}
106	old := wv.lastAdaptiveFrameRate
107	wv.lastAdaptiveFrameRate = fps
108	wv.mu.Unlock()
109
110	host.SetWindowlessFrameRate(fps)
111	if wv.ctx != nil {
112		logging.FromContext(wv.ctx).Info().
113			Int32("old_windowless_frame_rate", old).
114			Int32("windowless_frame_rate", fps).
115			Int("monitor_refresh_millihz", refreshRateMilliHz).
116			Str("display", displayName).
117			Msg("cef: adaptive windowless frame rate updated")
118	}
119}
120
121func widgetMonitorRefreshRateMilliHz(widget *gtk.Widget) (int, string, bool) {
122	if widget == nil {
123		return 0, "", false
124	}
125	native := widget.GetNative()
126	if native == nil {
127		return 0, "", false
128	}
129	surface := native.GetSurface()
130	if surface == nil {
131		return 0, "", false
132	}
133	display := surface.GetDisplay()
134	if display == nil {
135		return 0, "", false
136	}
137	monitor := display.GetMonitorAtSurface(surface)
138	if monitor == nil {
139		return 0, display.GetName(), false
140	}
141	return monitor.GetRefreshRate(), display.GetName(), true
142}
143
144func isWaylandDisplayName(name string) bool {
145	return strings.Contains(strings.ToLower(name), "wayland")
146}

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