detached 48e3ed9

fix: improve CEF text caret visibility on click and cold start

Two issues caused the blinking cursor to not appear:

1. Clicking the GLArea didn't call GrabFocus, so GTK's focus-enter
   signal never fired and host.SetFocus(1) was never called. CEF
   needs explicit SetFocus to render the text caret.

2. On cold start, the GLArea may already have GTK focus before
   OnAfterCreated fires. The focus-enter event was missed, so
   SetFocus was never called. Now checked in OnAfterCreated.
commit
48e3ed9e619638cb85270ae4c9ea880a1aad706b
author
brice · · 2026-03-25 06:31 UTC
committer
brice · · 2026-03-28 04:39 UTC
parent
d66b022
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +20 -0
M internal/infrastructure/cef/input_bridge.go +13 -0
@@ -62,6 +62,11 @@ type inputBridge struct {
6262 // Prevent GC from collecting signal callbacks.
6363 commitCb func(gtk.IMContext, string)
6464
65+ // glArea is stored so we can GrabFocus on click. Without explicit
66+ // focus grab, GTK may not fire the focus-enter signal and CEF won't
67+ // show the text caret.
68+ glArea *gtk.GLArea
69+
6570 // onMiddleClick is called when button 2 (middle) is pressed on a link.
6671 // The callback receives the hovered URI. Set by the factory.
6772 onMiddleClick func(uri string)
@@ -81,6 +86,8 @@ func (ib *inputBridge) setHost(host purecef.BrowserHost) {
8186
8287 // attachTo creates GDK event controllers and connects them to the given GLArea.
8388 func (ib *inputBridge) attachTo(glArea *gtk.GLArea) {
89+ ib.glArea = glArea
90+
8491 // Acquire GDK clipboard for paste support. CEF OSR can't access the
8592 // Wayland clipboard, so we bridge it via GDK → ImeCommitText.
8693 if display := gdk.DisplayGetDefault(); display != nil {
@@ -115,6 +122,12 @@ func (ib *inputBridge) attachTo(glArea *gtk.GLArea) {
115122 click.SetButton(0) // listen to all buttons
116123
117124 pressedCb := func(g gtk.GestureClick, nPress int, x, y float64) {
125+ // Ensure the GLArea has GTK focus so CEF receives SetFocus(1)
126+ // and renders the text caret. Without this, clicking an input
127+ // field may not show the blinking cursor.
128+ if ib.glArea != nil {
129+ ib.glArea.GrabFocus()
130+ }
118131 btn := g.GetCurrentButton()
119132 mods := uint(g.GetCurrentEventState())
120133 ib.onMousePress(x, y, btn, mods, nPress)
@@ -62,6 +62,11 @@ type inputBridge struct {
62 // Prevent GC from collecting signal callbacks.62 // Prevent GC from collecting signal callbacks.
63 commitCb func(gtk.IMContext, string)63 commitCb func(gtk.IMContext, string)
64 64
65+ // glArea is stored so we can GrabFocus on click. Without explicit
66+ // focus grab, GTK may not fire the focus-enter signal and CEF won't
67+ // show the text caret.
68+ glArea *gtk.GLArea
69+
65 // onMiddleClick is called when button 2 (middle) is pressed on a link.70 // onMiddleClick is called when button 2 (middle) is pressed on a link.
66 // The callback receives the hovered URI. Set by the factory.71 // The callback receives the hovered URI. Set by the factory.
67 onMiddleClick func(uri string)72 onMiddleClick func(uri string)
@@ -81,6 +86,8 @@ func (ib *inputBridge) setHost(host purecef.BrowserHost) {
81 86
82 // attachTo creates GDK event controllers and connects them to the given GLArea.87 // attachTo creates GDK event controllers and connects them to the given GLArea.
83 func (ib *inputBridge) attachTo(glArea *gtk.GLArea) {88 func (ib *inputBridge) attachTo(glArea *gtk.GLArea) {
89+ ib.glArea = glArea
90+
84 // Acquire GDK clipboard for paste support. CEF OSR can't access the91 // Acquire GDK clipboard for paste support. CEF OSR can't access the
85 // Wayland clipboard, so we bridge it via GDK → ImeCommitText.92 // Wayland clipboard, so we bridge it via GDK → ImeCommitText.
86 if display := gdk.DisplayGetDefault(); display != nil {93 if display := gdk.DisplayGetDefault(); display != nil {
@@ -115,6 +122,12 @@ func (ib *inputBridge) attachTo(glArea *gtk.GLArea) {
115 click.SetButton(0) // listen to all buttons122 click.SetButton(0) // listen to all buttons
116 123
117 pressedCb := func(g gtk.GestureClick, nPress int, x, y float64) {124 pressedCb := func(g gtk.GestureClick, nPress int, x, y float64) {
125+ // Ensure the GLArea has GTK focus so CEF receives SetFocus(1)
126+ // and renders the text caret. Without this, clicking an input
127+ // field may not show the blinking cursor.
128+ if ib.glArea != nil {
129+ ib.glArea.GrabFocus()
130+ }
118 btn := g.GetCurrentButton()131 btn := g.GetCurrentButton()
119 mods := uint(g.GetCurrentEventState())132 mods := uint(g.GetCurrentEventState())
120 ib.onMousePress(x, y, btn, mods, nPress)133 ib.onMousePress(x, y, btn, mods, nPress)
M internal/infrastructure/cef/webview_handlers.go +7 -0
@@ -527,6 +527,13 @@ func (h *handlerSet) OnAfterCreated(browser purecef.Browser) {
527527 // UI elements like the text caret until explicitly told the browser is shown.
528528 host.WasHidden(0)
529529
530+ // If the GLArea already has GTK focus (cold start race), tell CEF now.
531+ // The focus-enter event fired before the browser existed, so SetFocus
532+ // was never called and the caret won't blink until the user clicks.
533+ if h.wv.input != nil && h.wv.input.glArea != nil && h.wv.input.glArea.HasFocus() {
534+ host.SetFocus(1)
535+ }
536+
530537 // Replay any navigation that was requested before the browser existed.
531538 if uri != "" {
532539 if frame := browser.GetMainFrame(); frame != nil {
@@ -527,6 +527,13 @@ func (h *handlerSet) OnAfterCreated(browser purecef.Browser) {
527 // UI elements like the text caret until explicitly told the browser is shown.527 // UI elements like the text caret until explicitly told the browser is shown.
528 host.WasHidden(0)528 host.WasHidden(0)
529 529
530+ // If the GLArea already has GTK focus (cold start race), tell CEF now.
531+ // The focus-enter event fired before the browser existed, so SetFocus
532+ // was never called and the caret won't blink until the user clicks.
533+ if h.wv.input != nil && h.wv.input.glArea != nil && h.wv.input.glArea.HasFocus() {
534+ host.SetFocus(1)
535+ }
536+
530 // Replay any navigation that was requested before the browser existed.537 // Replay any navigation that was requested before the browser existed.
531 if uri != "" {538 if uri != "" {
532 if frame := browser.GetMainFrame(); frame != nil {539 if frame := browser.GetMainFrame(); frame != nil {

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