detached e77e492

Add cef2gtk bridge adapter

commit
e77e49226a8fd4744d137d3e517e2a70c220f4ce
author
brice · · 2026-04-29 18:57 UTC
parent
1f211c0
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +276 -0
A internal/infrastructure/cef/cef2gtk_adapter.go +195 -0
@@ -0,0 +1,195 @@
1+package cef
2+
3+import (
4+ "errors"
5+
6+ purecef "github.com/bnema/purego-cef/cef"
7+ cef2gtk "github.com/bnema/purego-cef2gtk"
8+ "github.com/bnema/puregotk/v4/gtk"
9+)
10+
11+// ErrAdapterDestroyed is returned when an operation is attempted on a destroyed
12+// Cef2gtkAdapter.
13+var ErrAdapterDestroyed = errors.New("cef2gtk_adapter: adapter is destroyed")
14+
15+// Cef2gtkAdapter is a thin infrastructure adapter around *cef2gtk.View. It
16+// keeps purego-cef2gtk imports confined to internal/infrastructure/cef.
17+//
18+// GTK-thread affinity is a hard constraint: most methods touch GTK/CEF view
19+// lifecycle or input and MUST be called from Dumber's GTK scheduling path
20+// (e.g. runOnGTK or runOnGTKSync) unless the caller is already executing on
21+// the GTK main context. Methods are annotated below.
22+//
23+// The adapter does NOT own GL/PBO staging, dirty-rect uploads, or GTK input
24+// controller implementation — those belong to purego-cef2gtk.
25+type Cef2gtkAdapter struct {
26+ view *cef2gtk.View
27+ destroyed bool
28+}
29+
30+// NewCef2gtkAdapter creates a GtkGLArea-backed accelerated CEF view and wraps
31+// it in a thin Dumber adapter.
32+func NewCef2gtkAdapter() *Cef2gtkAdapter {
33+ v := cef2gtk.NewView()
34+ if v == nil {
35+ return nil
36+ }
37+ return &Cef2gtkAdapter{view: v}
38+}
39+
40+// Widget returns the native GTK widget for embedding into containers.
41+// The returned pointer must be used from the GTK main thread for packing.
42+func (a *Cef2gtkAdapter) Widget() *gtk.Widget {
43+ if a == nil || a.view == nil {
44+ return nil
45+ }
46+ return a.view.Widget()
47+}
48+
49+// GLArea returns the underlying GtkGLArea for signal connections (resize,
50+// tick, etc.). Signal connection/disconnection must happen on the GTK thread.
51+func (a *Cef2gtkAdapter) GLArea() *gtk.GLArea {
52+ if a == nil || a.view == nil {
53+ return nil
54+ }
55+ return a.view.GLArea()
56+}
57+
58+// NativeWidget returns the uintptr of the GLArea for embedding via
59+// port.NativeWidgetProvider.
60+func (a *Cef2gtkAdapter) NativeWidget() uintptr {
61+ area := a.GLArea()
62+ if area == nil {
63+ return 0
64+ }
65+ return area.GoPointer()
66+}
67+
68+// Size returns the bridge's last observed positive widget size, or 1x1 before
69+// the GTK widget has been allocated. It is safe to call off the GTK thread.
70+func (a *Cef2gtkAdapter) Size() (int32, int32) {
71+ if a == nil || a.view == nil {
72+ return 1, 1
73+ }
74+ return a.view.Size()
75+}
76+
77+// DeviceScaleFactor returns the bridge's last observed GTK scale factor, or 1
78+// before the widget has reported a scale. It is safe to call off the GTK thread.
79+func (a *Cef2gtkAdapter) DeviceScaleFactor() float32 {
80+ if a == nil || a.view == nil {
81+ return 1
82+ }
83+ return a.view.DeviceScaleFactor()
84+}
85+
86+// AddSizeObserver registers a positive-size change callback. Register and
87+// unregister from the GTK main thread; callbacks are invoked by the bridge from
88+// GTK size notifications.
89+func (a *Cef2gtkAdapter) AddSizeObserver(fn func(width, height int32)) func() {
90+ if a == nil || a.view == nil {
91+ return func() {}
92+ }
93+ return a.view.AddSizeObserver(fn)
94+}
95+
96+// HasFocus reports whether the bridge widget currently has GTK focus. Must be
97+// called on the GTK main thread.
98+func (a *Cef2gtkAdapter) HasFocus() bool {
99+ if a == nil || a.view == nil {
100+ return false
101+ }
102+ return a.view.HasFocus()
103+}
104+
105+// SetCursorFromName applies a named cursor to the bridge widget. Must be called
106+// on the GTK main thread.
107+func (a *Cef2gtkAdapter) SetCursorFromName(name string) {
108+ if a == nil || a.view == nil {
109+ return
110+ }
111+ a.view.SetCursorFromName(name)
112+}
113+
114+// PrepareOnGTKThread initializes GL/EGL resources owned by purego-cef2gtk.
115+// Must be called on the GTK main thread.
116+func (a *Cef2gtkAdapter) PrepareOnGTKThread() error {
117+ if a == nil || a.view == nil {
118+ return ErrAdapterDestroyed
119+ }
120+ return a.view.PrepareOnGTKThread()
121+}
122+
123+// RenderHandler returns a CEF render handler that delegates accelerated paint
124+// handling to purego-cef2gtk. The returned handler satisfies cef.RenderHandler
125+// and uses the bridge's DMABUF/EGL import path.
126+//
127+// hooks may be a zero-value cef2gtk.Hooks if no custom callbacks are needed.
128+func (a *Cef2gtkAdapter) RenderHandler(hooks cef2gtk.Hooks) purecef.RenderHandler {
129+ if a == nil || a.view == nil {
130+ return nil
131+ }
132+ return a.view.RenderHandler(hooks)
133+}
134+
135+// AttachInput attaches GTK event controllers to the view and forwards input
136+// to the given CEF browser host. Must be called on the GTK main thread.
137+//
138+// opts.Scale configures HiDPI scale for pointer coordinate translation.
139+func (a *Cef2gtkAdapter) AttachInput(host purecef.BrowserHost, opts cef2gtk.InputOptions) error {
140+ if a == nil || a.view == nil {
141+ return ErrAdapterDestroyed
142+ }
143+ return a.view.AttachInput(host, opts)
144+}
145+
146+// SetInputHost updates the CEF browser host used by the attached input bridge.
147+// Must be called on the GTK main thread. Callers must call AttachInput first or
148+// this returns an error from the underlying bridge.
149+func (a *Cef2gtkAdapter) SetInputHost(host purecef.BrowserHost) error {
150+ if a == nil || a.view == nil {
151+ return ErrAdapterDestroyed
152+ }
153+ return a.view.SetInputHost(host)
154+}
155+
156+// DetachInput removes GTK input controllers attached by AttachInput.
157+// Must be called on the GTK main thread.
158+func (a *Cef2gtkAdapter) DetachInput() error {
159+ if a == nil || a.view == nil {
160+ return ErrAdapterDestroyed
161+ }
162+ return a.view.DetachInput()
163+}
164+
165+// Diagnostics returns a point-in-time snapshot of bridge rendering diagnostics
166+// including accelerated paint counts, import/render failures, and events.
167+func (a *Cef2gtkAdapter) Diagnostics() cef2gtk.Diagnostics {
168+ if a == nil || a.view == nil {
169+ return cef2gtk.Diagnostics{}
170+ }
171+ return a.view.Diagnostics()
172+}
173+
174+// Destroy releases GL resources owned by purego-cef2gtk and disconnects GTK
175+// signal handlers. Must be called on the GTK main thread. Not safe for
176+// concurrent use. After Destroy, the adapter is marked destroyed and all
177+// subsequent operations return ErrAdapterDestroyed.
178+func (a *Cef2gtkAdapter) Destroy() error {
179+ if a == nil {
180+ return ErrAdapterDestroyed
181+ }
182+ if a.view == nil {
183+ a.destroyed = true
184+ return ErrAdapterDestroyed
185+ }
186+ err := a.view.Destroy()
187+ a.view = nil
188+ a.destroyed = true
189+ return err
190+}
191+
192+// IsDestroyed returns true if the adapter has been destroyed.
193+func (a *Cef2gtkAdapter) IsDestroyed() bool {
194+ return a == nil || a.destroyed
195+}
@@ -0,0 +1,195 @@
1+package cef
2+
3+import (
4+ "errors"
5+
6+ purecef "github.com/bnema/purego-cef/cef"
7+ cef2gtk "github.com/bnema/purego-cef2gtk"
8+ "github.com/bnema/puregotk/v4/gtk"
9+)
10+
11+// ErrAdapterDestroyed is returned when an operation is attempted on a destroyed
12+// Cef2gtkAdapter.
13+var ErrAdapterDestroyed = errors.New("cef2gtk_adapter: adapter is destroyed")
14+
15+// Cef2gtkAdapter is a thin infrastructure adapter around *cef2gtk.View. It
16+// keeps purego-cef2gtk imports confined to internal/infrastructure/cef.
17+//
18+// GTK-thread affinity is a hard constraint: most methods touch GTK/CEF view
19+// lifecycle or input and MUST be called from Dumber's GTK scheduling path
20+// (e.g. runOnGTK or runOnGTKSync) unless the caller is already executing on
21+// the GTK main context. Methods are annotated below.
22+//
23+// The adapter does NOT own GL/PBO staging, dirty-rect uploads, or GTK input
24+// controller implementation — those belong to purego-cef2gtk.
25+type Cef2gtkAdapter struct {
26+ view *cef2gtk.View
27+ destroyed bool
28+}
29+
30+// NewCef2gtkAdapter creates a GtkGLArea-backed accelerated CEF view and wraps
31+// it in a thin Dumber adapter.
32+func NewCef2gtkAdapter() *Cef2gtkAdapter {
33+ v := cef2gtk.NewView()
34+ if v == nil {
35+ return nil
36+ }
37+ return &Cef2gtkAdapter{view: v}
38+}
39+
40+// Widget returns the native GTK widget for embedding into containers.
41+// The returned pointer must be used from the GTK main thread for packing.
42+func (a *Cef2gtkAdapter) Widget() *gtk.Widget {
43+ if a == nil || a.view == nil {
44+ return nil
45+ }
46+ return a.view.Widget()
47+}
48+
49+// GLArea returns the underlying GtkGLArea for signal connections (resize,
50+// tick, etc.). Signal connection/disconnection must happen on the GTK thread.
51+func (a *Cef2gtkAdapter) GLArea() *gtk.GLArea {
52+ if a == nil || a.view == nil {
53+ return nil
54+ }
55+ return a.view.GLArea()
56+}
57+
58+// NativeWidget returns the uintptr of the GLArea for embedding via
59+// port.NativeWidgetProvider.
60+func (a *Cef2gtkAdapter) NativeWidget() uintptr {
61+ area := a.GLArea()
62+ if area == nil {
63+ return 0
64+ }
65+ return area.GoPointer()
66+}
67+
68+// Size returns the bridge's last observed positive widget size, or 1x1 before
69+// the GTK widget has been allocated. It is safe to call off the GTK thread.
70+func (a *Cef2gtkAdapter) Size() (int32, int32) {
71+ if a == nil || a.view == nil {
72+ return 1, 1
73+ }
74+ return a.view.Size()
75+}
76+
77+// DeviceScaleFactor returns the bridge's last observed GTK scale factor, or 1
78+// before the widget has reported a scale. It is safe to call off the GTK thread.
79+func (a *Cef2gtkAdapter) DeviceScaleFactor() float32 {
80+ if a == nil || a.view == nil {
81+ return 1
82+ }
83+ return a.view.DeviceScaleFactor()
84+}
85+
86+// AddSizeObserver registers a positive-size change callback. Register and
87+// unregister from the GTK main thread; callbacks are invoked by the bridge from
88+// GTK size notifications.
89+func (a *Cef2gtkAdapter) AddSizeObserver(fn func(width, height int32)) func() {
90+ if a == nil || a.view == nil {
91+ return func() {}
92+ }
93+ return a.view.AddSizeObserver(fn)
94+}
95+
96+// HasFocus reports whether the bridge widget currently has GTK focus. Must be
97+// called on the GTK main thread.
98+func (a *Cef2gtkAdapter) HasFocus() bool {
99+ if a == nil || a.view == nil {
100+ return false
101+ }
102+ return a.view.HasFocus()
103+}
104+
105+// SetCursorFromName applies a named cursor to the bridge widget. Must be called
106+// on the GTK main thread.
107+func (a *Cef2gtkAdapter) SetCursorFromName(name string) {
108+ if a == nil || a.view == nil {
109+ return
110+ }
111+ a.view.SetCursorFromName(name)
112+}
113+
114+// PrepareOnGTKThread initializes GL/EGL resources owned by purego-cef2gtk.
115+// Must be called on the GTK main thread.
116+func (a *Cef2gtkAdapter) PrepareOnGTKThread() error {
117+ if a == nil || a.view == nil {
118+ return ErrAdapterDestroyed
119+ }
120+ return a.view.PrepareOnGTKThread()
121+}
122+
123+// RenderHandler returns a CEF render handler that delegates accelerated paint
124+// handling to purego-cef2gtk. The returned handler satisfies cef.RenderHandler
125+// and uses the bridge's DMABUF/EGL import path.
126+//
127+// hooks may be a zero-value cef2gtk.Hooks if no custom callbacks are needed.
128+func (a *Cef2gtkAdapter) RenderHandler(hooks cef2gtk.Hooks) purecef.RenderHandler {
129+ if a == nil || a.view == nil {
130+ return nil
131+ }
132+ return a.view.RenderHandler(hooks)
133+}
134+
135+// AttachInput attaches GTK event controllers to the view and forwards input
136+// to the given CEF browser host. Must be called on the GTK main thread.
137+//
138+// opts.Scale configures HiDPI scale for pointer coordinate translation.
139+func (a *Cef2gtkAdapter) AttachInput(host purecef.BrowserHost, opts cef2gtk.InputOptions) error {
140+ if a == nil || a.view == nil {
141+ return ErrAdapterDestroyed
142+ }
143+ return a.view.AttachInput(host, opts)
144+}
145+
146+// SetInputHost updates the CEF browser host used by the attached input bridge.
147+// Must be called on the GTK main thread. Callers must call AttachInput first or
148+// this returns an error from the underlying bridge.
149+func (a *Cef2gtkAdapter) SetInputHost(host purecef.BrowserHost) error {
150+ if a == nil || a.view == nil {
151+ return ErrAdapterDestroyed
152+ }
153+ return a.view.SetInputHost(host)
154+}
155+
156+// DetachInput removes GTK input controllers attached by AttachInput.
157+// Must be called on the GTK main thread.
158+func (a *Cef2gtkAdapter) DetachInput() error {
159+ if a == nil || a.view == nil {
160+ return ErrAdapterDestroyed
161+ }
162+ return a.view.DetachInput()
163+}
164+
165+// Diagnostics returns a point-in-time snapshot of bridge rendering diagnostics
166+// including accelerated paint counts, import/render failures, and events.
167+func (a *Cef2gtkAdapter) Diagnostics() cef2gtk.Diagnostics {
168+ if a == nil || a.view == nil {
169+ return cef2gtk.Diagnostics{}
170+ }
171+ return a.view.Diagnostics()
172+}
173+
174+// Destroy releases GL resources owned by purego-cef2gtk and disconnects GTK
175+// signal handlers. Must be called on the GTK main thread. Not safe for
176+// concurrent use. After Destroy, the adapter is marked destroyed and all
177+// subsequent operations return ErrAdapterDestroyed.
178+func (a *Cef2gtkAdapter) Destroy() error {
179+ if a == nil {
180+ return ErrAdapterDestroyed
181+ }
182+ if a.view == nil {
183+ a.destroyed = true
184+ return ErrAdapterDestroyed
185+ }
186+ err := a.view.Destroy()
187+ a.view = nil
188+ a.destroyed = true
189+ return err
190+}
191+
192+// IsDestroyed returns true if the adapter has been destroyed.
193+func (a *Cef2gtkAdapter) IsDestroyed() bool {
194+ return a == nil || a.destroyed
195+}
A internal/infrastructure/cef/cef2gtk_migration_audit.md +81 -0
@@ -0,0 +1,81 @@
1+# purego-cef2gtk migration audit
2+
3+Date: 2026-04-29
4+Branch: `feature/use-purego-cef2gtk`
5+
6+This audit records the old Dumber CEF/GTK rendering/input stack references that must be replaced before the old files are deleted.
7+
8+## Old-stack references
9+
10+### `renderPipeline`
11+
12+- Defined in `internal/infrastructure/cef/render_pipeline.go`.
13+- Owned by `WebView.pipeline` in `webview.go`.
14+- Created in `factory.go`.
15+- Used for view rect/screen info, popup paint, CPU `OnPaint`, diagnostics, cursor updates, begin-frame `GtkGLArea`, native widget, and resize callbacks.
16+
17+Classification:
18+
19+- Rendering/GL upload: replace with `purego-cef2gtk.View.RenderHandler` and accelerated DMABUF path.
20+- Widget/GLArea access: replace with the thin Dumber `Cef2gtkAdapter`.
21+- Size/resize: use `purego-cef2gtk` size observer/size APIs plus small Dumber browser lifecycle callbacks.
22+- Software paint/PBO tests: delete or move responsibility to `purego-cef2gtk` tests.
23+
24+### `inputBridge`
25+
26+- Defined in `internal/infrastructure/cef/input_bridge.go`.
27+- Created and attached in `factory.go`.
28+- Host updated/cleared in `webview_handlers.go`.
29+- Focus queried in loading/focus paths.
30+- Middle-click link behavior wired in `factory.go` via `OnLinkMiddleClick` and cached hover URI.
31+
32+Classification:
33+
34+- Mouse/key/scroll/IME/paste/focus forwarding: replace with `purego-cef2gtk.View.AttachInput`, `SetInputHost`, and `DetachInput`.
35+- Host attach/update/detach: call through Dumber GTK scheduling path.
36+- Focus state: use bridge focus helper on GTK thread.
37+- Middle-click link open: preserve through `purego-cef2gtk.InputOptions.OnMiddleClick`.
38+- Old input translation tests: delete from Dumber or replace with lifecycle/callback wiring tests.
39+
40+### `glLoader`
41+
42+- Defined in `internal/infrastructure/cef/gl_loader.go`.
43+- Owned by `Engine.gl` and created in `engine_init.go`.
44+- Passed to factory and consumed by `renderPipeline`.
45+
46+Classification: delete from Dumber. GL/EGL loading belongs to `purego-cef2gtk` internals.
47+
48+### `resizeReconciler`
49+
50+- Defined in `internal/infrastructure/cef/resize_reconciler.go`.
51+- Owned by `WebView.resizeReconciler`.
52+- Started from resize callbacks and paint completion callbacks.
53+
54+Classification:
55+
56+- Paint-based retry/diagnostics: obsolete after deleting Dumber software paint pipeline.
57+- Browser resize notification: keep only a small Dumber lifecycle path that calls CEF host `WasResized`/`Invalidate` from bridge size events.
58+
59+## Non-obvious callouts
60+
61+- Load watchdog diagnostics in `webview.go` currently log `renderPipelineSnapshot`; replace with bridge diagnostics, accepting less Dumber-specific render detail unless a real product need appears.
62+- Cursor updates in `webview_handlers.go` currently call `wv.pipeline.glArea.SetCursorFromName`; replace with `Cef2gtkAdapter.SetCursorFromName` on GTK thread.
63+- Loading/focus checks currently use `wv.input.hasGTKFocus`; replace with bridge focus helper on GTK thread.
64+- Browser close currently clears old input host directly; replace with GTK-thread `SetInputHost(nil)` or `DetachInput`.
65+- Begin-frame loop currently uses `pipeline.glArea`; replace with adapter `GLArea` if external begin frame remains enabled.
66+- `NativeWidget` currently returns old GLArea pointer; replace with adapter native widget pointer.
67+- Popup/select handling needs runtime verification. `purego-cef2gtk` currently treats CPU `OnPaint` as diagnostic-only and popup callbacks as no-op.
68+- Text selection updates Dumber state and should be preserved through render-handler hook/composition while paint handling delegates to the bridge.
69+
70+## Upstream bridge gaps addressed during Phase 2
71+
72+`purego-cef2gtk` now exposes or implements:
73+
74+- real size access and size observers;
75+- device scale factor access;
76+- `GetScreenInfo` based on bridge size/scale;
77+- text-selection render hook;
78+- middle-click input hook;
79+- focus and cursor helpers.
80+
81+Remaining runtime risk: popup/select behavior should be smoke-tested after Dumber is fully migrated.
@@ -0,0 +1,81 @@
1+# purego-cef2gtk migration audit
2+
3+Date: 2026-04-29
4+Branch: `feature/use-purego-cef2gtk`
5+
6+This audit records the old Dumber CEF/GTK rendering/input stack references that must be replaced before the old files are deleted.
7+
8+## Old-stack references
9+
10+### `renderPipeline`
11+
12+- Defined in `internal/infrastructure/cef/render_pipeline.go`.
13+- Owned by `WebView.pipeline` in `webview.go`.
14+- Created in `factory.go`.
15+- Used for view rect/screen info, popup paint, CPU `OnPaint`, diagnostics, cursor updates, begin-frame `GtkGLArea`, native widget, and resize callbacks.
16+
17+Classification:
18+
19+- Rendering/GL upload: replace with `purego-cef2gtk.View.RenderHandler` and accelerated DMABUF path.
20+- Widget/GLArea access: replace with the thin Dumber `Cef2gtkAdapter`.
21+- Size/resize: use `purego-cef2gtk` size observer/size APIs plus small Dumber browser lifecycle callbacks.
22+- Software paint/PBO tests: delete or move responsibility to `purego-cef2gtk` tests.
23+
24+### `inputBridge`
25+
26+- Defined in `internal/infrastructure/cef/input_bridge.go`.
27+- Created and attached in `factory.go`.
28+- Host updated/cleared in `webview_handlers.go`.
29+- Focus queried in loading/focus paths.
30+- Middle-click link behavior wired in `factory.go` via `OnLinkMiddleClick` and cached hover URI.
31+
32+Classification:
33+
34+- Mouse/key/scroll/IME/paste/focus forwarding: replace with `purego-cef2gtk.View.AttachInput`, `SetInputHost`, and `DetachInput`.
35+- Host attach/update/detach: call through Dumber GTK scheduling path.
36+- Focus state: use bridge focus helper on GTK thread.
37+- Middle-click link open: preserve through `purego-cef2gtk.InputOptions.OnMiddleClick`.
38+- Old input translation tests: delete from Dumber or replace with lifecycle/callback wiring tests.
39+
40+### `glLoader`
41+
42+- Defined in `internal/infrastructure/cef/gl_loader.go`.
43+- Owned by `Engine.gl` and created in `engine_init.go`.
44+- Passed to factory and consumed by `renderPipeline`.
45+
46+Classification: delete from Dumber. GL/EGL loading belongs to `purego-cef2gtk` internals.
47+
48+### `resizeReconciler`
49+
50+- Defined in `internal/infrastructure/cef/resize_reconciler.go`.
51+- Owned by `WebView.resizeReconciler`.
52+- Started from resize callbacks and paint completion callbacks.
53+
54+Classification:
55+
56+- Paint-based retry/diagnostics: obsolete after deleting Dumber software paint pipeline.
57+- Browser resize notification: keep only a small Dumber lifecycle path that calls CEF host `WasResized`/`Invalidate` from bridge size events.
58+
59+## Non-obvious callouts
60+
61+- Load watchdog diagnostics in `webview.go` currently log `renderPipelineSnapshot`; replace with bridge diagnostics, accepting less Dumber-specific render detail unless a real product need appears.
62+- Cursor updates in `webview_handlers.go` currently call `wv.pipeline.glArea.SetCursorFromName`; replace with `Cef2gtkAdapter.SetCursorFromName` on GTK thread.
63+- Loading/focus checks currently use `wv.input.hasGTKFocus`; replace with bridge focus helper on GTK thread.
64+- Browser close currently clears old input host directly; replace with GTK-thread `SetInputHost(nil)` or `DetachInput`.
65+- Begin-frame loop currently uses `pipeline.glArea`; replace with adapter `GLArea` if external begin frame remains enabled.
66+- `NativeWidget` currently returns old GLArea pointer; replace with adapter native widget pointer.
67+- Popup/select handling needs runtime verification. `purego-cef2gtk` currently treats CPU `OnPaint` as diagnostic-only and popup callbacks as no-op.
68+- Text selection updates Dumber state and should be preserved through render-handler hook/composition while paint handling delegates to the bridge.
69+
70+## Upstream bridge gaps addressed during Phase 2
71+
72+`purego-cef2gtk` now exposes or implements:
73+
74+- real size access and size observers;
75+- device scale factor access;
76+- `GetScreenInfo` based on bridge size/scale;
77+- text-selection render hook;
78+- middle-click input hook;
79+- focus and cursor helpers.
80+
81+Remaining runtime risk: popup/select behavior should be smoke-tested after Dumber is fully migrated.

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