Hide the editor caret while another input has focus

The caret was drawn unconditionally, so it kept showing in the main edit
window while key focus sat in the find bar's search input — reading as
if the editor still had focus. drawWrappedText now takes the field's
focus state and draws the caret only when the field is focused
(editorElem.Focused follows FocusedElementID, which is find_bar while
the find bar is open); the caret returns when focus comes back on close.

e2e: the editor TextField reports unfocused while the find bar is open
and focused again after close.
This commit is contained in:
Greg Pomerantz 2026-08-20 12:52:31 -04:00
parent cfaf4927ac
commit 1cbb367b1f
3 changed files with 20 additions and 10 deletions

View File

@ -113,6 +113,10 @@ func TestRealFile_FindBarAndNavigation(t *testing.T) {
if got := reg.(ui.Region); got.Y != ui.Dp(32)+editor.FindBarHeight {
t.Fatalf("editor region top %d, want %d (find bar must not cover text)", int(got.Y), int(ui.Dp(32)+editor.FindBarHeight))
}
// The editor field reports UNFOCUSED while the find bar is open (the
// renderer hides its caret for unfocused fields; main hands key focus to
// the search input).
waitForEditorField(t, h, func(tf ui.TextField) bool { return !tf.Focused })
// Type the query through the same channel main uses.
h.Logic().FindQueryChan() <- "NEEDLE" // case-insensitive
@ -190,6 +194,8 @@ func TestRealFile_FindBarAndNavigation(t *testing.T) {
if focus.(string) != "editor_text" {
t.Fatalf("focus %q, want editor_text after close", focus)
}
// And the editor field reports focused again (caret returns).
waitForEditorField(t, h, func(tf ui.TextField) bool { return tf.Focused })
}
func TestRealFile_FindMatchesSurviveEdits(t *testing.T) {

View File

@ -337,7 +337,7 @@ func (tf TextField) Draw(gtx layout.Context, r *Renderer) {
r.lastSelCaret = -1
r.lastIMEShowSeq = 0
}
r.drawWrappedText(gtx, tf.Value, tf.region, tf.WordWrap, tf.WrapWidth, tf.ScrollOffset, tf.CursorPosition, tf.SelectionStart, tf.SelectionEnd, tf.CaretDrag, tf.MatchRanges, tf.CurrentMatch)
r.drawWrappedText(gtx, tf.Value, tf.region, tf.WordWrap, tf.WrapWidth, tf.ScrollOffset, tf.CursorPosition, tf.SelectionStart, tf.SelectionEnd, tf.CaretDrag, tf.MatchRanges, tf.CurrentMatch, tf.Focused)
}
// runeCount returns the number of UTF-8 runes in s[:bytePos] (bytePos is a

View File

@ -777,7 +777,7 @@ func (r *Renderer) drawRangeHighlight(gtx layout.Context, layout *GlyphLayout, s
}
}
func (r *Renderer) drawWrappedText(gtx layout.Context, str string, reg Region, wordWrap bool, wrapWidth Dp, scrollOffset Dp, cursorPos, selStart, selEnd int, caretDrag bool, matchRanges [][2]int, currentMatch int) {
func (r *Renderer) drawWrappedText(gtx layout.Context, str string, reg Region, wordWrap bool, wrapWidth Dp, scrollOffset Dp, cursorPos, selStart, selEnd int, caretDrag bool, matchRanges [][2]int, currentMatch int, focused bool) {
if str == "" {
return
}
@ -920,14 +920,18 @@ func (r *Renderer) drawWrappedText(gtx layout.Context, str string, reg Region, w
return CaretPoint(layout, str, byteOff, ascent, lineH)
}
// Draw the caret only when the cursor's byte is inside the shaped window
// (window-relative: [0, len(str)]). The window covers the viewport
// exactly, so an out-of-range cursor is off-screen and its caret must not
// be drawn; the caller used to clamp it to 0, which made the caret jump
// onto the top (or, past the end, the bottom) visible line whenever the
// user scrolled past it. The boundary values are on-screen: 0 is the
// window's first byte and len(str) is the window's last insertion point.
if cursorPos >= 0 && cursorPos <= len(str) {
// Draw the caret only when (a) the field holds key focus and (b) the
// cursor's byte is inside the shaped window (window-relative:
// [0, len(str)]). (a): while another input is focused — the find bar's
// search input is the case that motivated this — a live caret in the
// editor reads as if the editor still had focus; the caret returns when
// focus comes back. (b): the window covers the viewport exactly, so an
// out-of-range cursor is off-screen and its caret must not be drawn; the
// caller used to clamp it to 0, which made the caret jump onto the top
// (or, past the end, the bottom) visible line whenever the user scrolled
// past it. The boundary values are on-screen: 0 is the window's first
// byte and len(str) is the window's last insertion point.
if focused && cursorPos >= 0 && cursorPos <= len(str) {
// Determine cursor position from `layout` and `cursorPos`
cursorX, cursorY := caretPoint(cursorPos)
cursorX = reg.X + cursorX