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.
93 lines
3.5 KiB
Go
93 lines
3.5 KiB
Go
package editor
|
|
|
|
import (
|
|
"time"
|
|
|
|
"pad/internal/ui"
|
|
)
|
|
|
|
// Frame is the unit of handoff from the logic goroutine to the frame
|
|
// receiver. Per architecture.md §9, the frame is the ONLY cross-goroutine
|
|
// state carrier: the main goroutine must never read *State directly, it
|
|
// reads the snapshot stored here by the frame receiver (under the frame
|
|
// mutex).
|
|
//
|
|
// Elems is the computed element tree for the next draw.
|
|
// The remaining fields are the view-state snapshot that the main goroutine
|
|
// needs to route events and forward input:
|
|
//
|
|
// - Scale: current px-per-Dp, so the main goroutine can detect scale
|
|
// changes and pass the scale to the renderer's Draw call.
|
|
// - FocusedElementID: which registered element receives key/edit events.
|
|
// - Query: the search query the logic goroutine is currently filtering
|
|
// with; the main goroutine compares it against the (main-owned) search
|
|
// widget's text and forwards changes via SearchQueryChan.
|
|
type Frame struct {
|
|
Elems []ui.Element
|
|
Scale float32
|
|
FontScale float32 // user font-size setting the logic bookkeeping used
|
|
FocusedElementID string
|
|
Query string
|
|
// FindQuery: the in-file search query the logic goroutine has processed
|
|
// (mirrors EditorState.Find.Query). Main compares it against the
|
|
// "find_bar" widget's text and forwards changes via FindQueryChan — the
|
|
// same contract as Query/searchQueryChan.
|
|
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
|
|
// frame's elements describe. The main goroutine forwards them with the
|
|
// shaped glyph layout (LayoutFeedback) so the logic goroutine can apply
|
|
// the layout's wrap counts to exactly the lines it was shaped for, and
|
|
// drop them if an edit landed in the meantime.
|
|
WindowStartByte int
|
|
WindowStartLine int // -1 when the frame has no editor window
|
|
WindowText string // the editor window this frame's text element holds
|
|
EditSeq uint64
|
|
}
|
|
|
|
// frameOf wraps a computed element tree with the current view-state
|
|
// snapshot. Must be called on the logic goroutine.
|
|
func (l *Logic) frameOf(elems []ui.Element) Frame {
|
|
return Frame{
|
|
Elems: elems,
|
|
Scale: l.state.scale,
|
|
FontScale: l.state.fontScale,
|
|
FocusedElementID: l.state.FocusedElementID,
|
|
Query: l.state.Browser.Query,
|
|
FindQuery: l.state.Editor.Find.Query,
|
|
FindClearSeq: l.state.Editor.Find.ClearSeq,
|
|
WindowStartByte: l.state.Editor.IMEWindowStartByte,
|
|
WindowStartLine: l.state.WindowStartLine,
|
|
WindowText: l.state.Editor.IMEWindowText,
|
|
EditSeq: l.state.Editor.EditSeq,
|
|
}
|
|
}
|
|
|
|
// inspectReq is a test-only request to run fn on the logic goroutine.
|
|
// It preserves the single-owner invariant (architecture.md §1): the fn
|
|
// executes on the owner, not on the caller. fn must not block on sends to
|
|
// logic channels.
|
|
type inspectReq struct {
|
|
fn func(st *State) any
|
|
resp chan any
|
|
}
|
|
|
|
// Inspect runs fn on the logic goroutine and returns its result. It is
|
|
// intended for tests; production code must use the regular channels.
|
|
func (l *Logic) Inspect(fn func(st *State) any) (any, bool) {
|
|
req := &inspectReq{fn: fn, resp: make(chan any, 1)}
|
|
select {
|
|
case l.inspectChan <- req:
|
|
case <-time.After(5 * time.Second):
|
|
return nil, false
|
|
}
|
|
select {
|
|
case v := <-req.resp:
|
|
return v, true
|
|
case <-time.After(5 * time.Second):
|
|
return nil, false
|
|
}
|
|
}
|