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 Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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 { | |||
| 62 | 62 | // Prevent GC from collecting signal callbacks. | |
| 63 | 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 | 70 | // onMiddleClick is called when button 2 (middle) is pressed on a link. | |
| 66 | 71 | // The callback receives the hovered URI. Set by the factory. | |
| 67 | 72 | onMiddleClick func(uri string) | |
| @@ -81,6 +86,8 @@ func (ib *inputBridge) setHost(host purecef.BrowserHost) { | |||
| 81 | 86 | ||
| 82 | 87 | // attachTo creates GDK event controllers and connects them to the given GLArea. | |
| 83 | 88 | func (ib *inputBridge) attachTo(glArea *gtk.GLArea) { | |
| 89 | + | ib.glArea = glArea | |
| 90 | + | ||
| 84 | 91 | // Acquire GDK clipboard for paste support. CEF OSR can't access the | |
| 85 | 92 | // Wayland clipboard, so we bridge it via GDK → ImeCommitText. | |
| 86 | 93 | if display := gdk.DisplayGetDefault(); display != nil { | |
| @@ -115,6 +122,12 @@ func (ib *inputBridge) attachTo(glArea *gtk.GLArea) { | |||
| 115 | 122 | click.SetButton(0) // listen to all buttons | |
| 116 | 123 | ||
| 117 | 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 | 131 | btn := g.GetCurrentButton() | |
| 119 | 132 | mods := uint(g.GetCurrentEventState()) | |
| 120 | 133 | 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 the | 91 | // 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 buttons | 122 | 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) { | |||
| 527 | 527 | // UI elements like the text caret until explicitly told the browser is shown. | |
| 528 | 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 | 537 | // Replay any navigation that was requested before the browser existed. | |
| 531 | 538 | if uri != "" { | |
| 532 | 539 | 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 { | ||