Pad/internal/editor/frame.go
Greg Pomerantz 58725a5e6c Make state single-owner and stabilize race detector
Frame now carries view state (scale/focus/query) to the main goroutine,
which reads only the frame-receiver-stored snapshot. Renderer owns Gio
widget editors (registered by ID) and the draw scale. Autosave timer
sends a token to the logic goroutine instead of touching state;
Shutdown waits for the owner to exit before FlushAll.

Tests access state only through owner-side Inspect/WithState helpers
(harness + in-package). Fixed double-Harness.Run race in two e2e tests,
made TestWorkerPool_PriorityPreemption deterministic, and raised the
lazy-loading test timeout that was too short under -race.

go test -race ./... is now green.
2026-08-16 01:32:27 -04:00

68 lines
2.2 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
FocusedElementID string
Query string
}
// 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,
FocusedElementID: l.state.FocusedElementID,
Query: l.state.Browser.Query,
}
}
// 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
}
}