Stop the find input clearing itself while typing
The main-loop mirror of the X-button clear was level-triggered: 'FindQuery=="" && widget has text'. But the widget updates on every keystroke while the logic only stores the query a round-trip later, so any frame drawn in that window still carried FindQuery=="" and wiped the input - the periodic self-clearing. Make it edge-triggered: FindState.ClearSeq bumps once per clear; the frame carries it as FindClearSeq; main wipes the widget input once per NEW value (lastFindClearSeq handshake). findClear bumps it; findReset preserves it (main tracks it monotonically, so a reset to 0 would swallow a later clear). Tests: unit asserts the bump, monotonicity, and close/reopen preservation.
This commit is contained in:
parent
d726ea7244
commit
c789de4b44
|
|
@ -132,6 +132,10 @@ func run(w *app.Window) error {
|
||||||
// main-owned input, so main hands key focus to it exactly once per open
|
// main-owned input, so main hands key focus to it exactly once per open
|
||||||
// (see the focus handoff below).
|
// (see the focus handoff below).
|
||||||
var findBarFocused bool
|
var findBarFocused bool
|
||||||
|
// lastFindClearSeq is the FindClearSeq value whose wipe has already been
|
||||||
|
// applied to the main-owned find input (edge-triggered, see
|
||||||
|
// Frame.FindClearSeq).
|
||||||
|
var lastFindClearSeq int
|
||||||
|
|
||||||
for {
|
for {
|
||||||
switch e := w.Event().(type) {
|
switch e := w.Event().(type) {
|
||||||
|
|
@ -218,12 +222,15 @@ func run(w *app.Window) error {
|
||||||
// The logic goroutine handles filtering and triggers a new frame.
|
// The logic goroutine handles filtering and triggers a new frame.
|
||||||
newQuery := searchEditor.Text()
|
newQuery := searchEditor.Text()
|
||||||
sendQuery := newQuery != frame.Query
|
sendQuery := newQuery != frame.Query
|
||||||
// The find bar's X button (editor.FindClear) empties the LOGIC-side
|
// Mirror each NEW find-clear (the bar's X button) into the main-
|
||||||
// query; the main-owned widget still shows the old text, so mirror
|
// owned widget input exactly once. Edge-triggered on ClearSeq, NOT
|
||||||
// the clear into it before the change comparison below (which then
|
// "FindQuery==\"\" && widget has text": that level check also fired on
|
||||||
// sees "" == "" and forwards nothing).
|
// stale frames between the user's typing and the logic storing it,
|
||||||
if frame.FindQuery == "" && findEditor.Text() != "" {
|
// wiping the input while typing.
|
||||||
|
if frame.FindClearSeq > lastFindClearSeq {
|
||||||
|
lastFindClearSeq = frame.FindClearSeq
|
||||||
findEditor.SetText("")
|
findEditor.SetText("")
|
||||||
|
w.Invalidate()
|
||||||
}
|
}
|
||||||
newFind := findEditor.Text()
|
newFind := findEditor.Text()
|
||||||
sendFind := newFind != frame.FindQuery
|
sendFind := newFind != frame.FindQuery
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,9 @@ type Frame struct {
|
||||||
// "find_bar" widget's text and forwards changes via FindQueryChan — the
|
// "find_bar" widget's text and forwards changes via FindQueryChan — the
|
||||||
// same contract as Query/searchQueryChan.
|
// same contract as Query/searchQueryChan.
|
||||||
FindQuery string
|
FindQuery string
|
||||||
|
// FindClearSeq mirrors EditorState.Find.ClearSeq: main wipes the widget
|
||||||
|
// input once per NEW value (the X button cleared the logic-side query).
|
||||||
|
FindClearSeq int
|
||||||
// WindowStartByte / WindowStartLine / EditSeq: the editor window this
|
// WindowStartByte / WindowStartLine / EditSeq: the editor window this
|
||||||
// frame's elements describe. The main goroutine forwards them with the
|
// frame's elements describe. The main goroutine forwards them with the
|
||||||
// shaped glyph layout (LayoutFeedback) so the logic goroutine can apply
|
// shaped glyph layout (LayoutFeedback) so the logic goroutine can apply
|
||||||
|
|
@ -54,6 +57,7 @@ func (l *Logic) frameOf(elems []ui.Element) Frame {
|
||||||
FocusedElementID: l.state.FocusedElementID,
|
FocusedElementID: l.state.FocusedElementID,
|
||||||
Query: l.state.Browser.Query,
|
Query: l.state.Browser.Query,
|
||||||
FindQuery: l.state.Editor.Find.Query,
|
FindQuery: l.state.Editor.Find.Query,
|
||||||
|
FindClearSeq: l.state.Editor.Find.ClearSeq,
|
||||||
WindowStartByte: l.state.Editor.IMEWindowStartByte,
|
WindowStartByte: l.state.Editor.IMEWindowStartByte,
|
||||||
WindowStartLine: l.state.WindowStartLine,
|
WindowStartLine: l.state.WindowStartLine,
|
||||||
WindowText: l.state.Editor.IMEWindowText,
|
WindowText: l.state.Editor.IMEWindowText,
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,15 @@ type FindState struct {
|
||||||
SettleByte int
|
SettleByte int
|
||||||
SettleScroll ui.Dp
|
SettleScroll ui.Dp
|
||||||
SettlePasses int
|
SettlePasses int
|
||||||
|
|
||||||
|
// ClearSeq bumps once per clear (the bar's X button, findClear). Main
|
||||||
|
// mirrors each NEW value into the main-owned widget input exactly once —
|
||||||
|
// an edge-triggered handshake, deliberately NOT a level check: a
|
||||||
|
// "FindQuery==\"\" && the widget has text" level check would also fire on
|
||||||
|
// any stale frame between the user's typing (widget updates immediately)
|
||||||
|
// and the logic storing it (a round-trip away), wiping the input while
|
||||||
|
// typing.
|
||||||
|
ClearSeq int
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToggleFind opens the find bar (or closes it when open). It is the tap
|
// ToggleFind opens the find bar (or closes it when open). It is the tap
|
||||||
|
|
@ -95,6 +104,7 @@ func (e *EditorState) findClear() {
|
||||||
f.Cur = -1
|
f.Cur = -1
|
||||||
f.Scanning = false
|
f.Scanning = false
|
||||||
f.Gen++
|
f.Gen++
|
||||||
|
f.ClearSeq++
|
||||||
f.SettleByte = -1
|
f.SettleByte = -1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -120,7 +130,10 @@ func (e *EditorState) findReset() {
|
||||||
// Monotonic generation bump (do not zero the counter): a stale
|
// Monotonic generation bump (do not zero the counter): a stale
|
||||||
// in-flight scan carrying a HIGH generation must never equal a fresh
|
// in-flight scan carrying a HIGH generation must never equal a fresh
|
||||||
// one, or its result would pass the gen gate in applySearchResult.
|
// one, or its result would pass the gen gate in applySearchResult.
|
||||||
e.Find = FindState{Query: e.Find.Query, Gen: e.Find.Gen + 1, SettleByte: -1}
|
// ClearSeq is preserved: main tracks it monotonically, so resetting it
|
||||||
|
// to 0 would make a later clear (seq 1) indistinguishable from an old
|
||||||
|
// one already mirrored.
|
||||||
|
e.Find = FindState{Query: e.Find.Query, Gen: e.Find.Gen + 1, ClearSeq: e.Find.ClearSeq, SettleByte: -1}
|
||||||
}
|
}
|
||||||
|
|
||||||
// findSetQuery processes a query forwarded from the main-owned "find_bar"
|
// findSetQuery processes a query forwarded from the main-owned "find_bar"
|
||||||
|
|
|
||||||
|
|
@ -212,6 +212,20 @@ func TestFindClear_EmptiesQueryKeepsBarOpen(t *testing.T) {
|
||||||
if e.Find.Gen != oldGen+1 {
|
if e.Find.Gen != oldGen+1 {
|
||||||
t.Fatalf("gen %d, want %d (in-flight scan must be superseded)", e.Find.Gen, oldGen+1)
|
t.Fatalf("gen %d, want %d (in-flight scan must be superseded)", e.Find.Gen, oldGen+1)
|
||||||
}
|
}
|
||||||
|
if e.Find.ClearSeq != 1 {
|
||||||
|
t.Fatalf("ClearSeq %d, want 1 (main mirrors wipes on the edge)", e.Find.ClearSeq)
|
||||||
|
}
|
||||||
|
e.findClear()
|
||||||
|
if e.Find.ClearSeq != 2 {
|
||||||
|
t.Fatalf("ClearSeq %d after second clear, want 2 (monotonic)", e.Find.ClearSeq)
|
||||||
|
}
|
||||||
|
// findReset (bar close + reopen) preserves the sequence.
|
||||||
|
TheState.FocusedElementID = "editor_text"
|
||||||
|
e.findClose()
|
||||||
|
e.findShow()
|
||||||
|
if e.Find.ClearSeq != 2 {
|
||||||
|
t.Fatalf("ClearSeq %d after close/reopen, want 2 (preserved)", e.Find.ClearSeq)
|
||||||
|
}
|
||||||
// The in-flight scan of the old query must now be dropped.
|
// The in-flight scan of the old query must now be dropped.
|
||||||
e.applySearchResult(pool.Result{TaskType: pool.TypeSearch, Success: true, Data: pool.SearchData{Gen: oldGen, Matches: [][2]int{{0, 1}}}})
|
e.applySearchResult(pool.Result{TaskType: pool.TypeSearch, Success: true, Data: pool.SearchData{Gen: oldGen, Matches: [][2]int{{0, 1}}}})
|
||||||
if len(e.Find.Matches) != 0 {
|
if len(e.Find.Matches) != 0 {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user