package ui import ( "image" "testing" "time" "gioui.org/font/gofont" "gioui.org/gesture" "gioui.org/io/event" "gioui.org/io/input" "gioui.org/io/key" "gioui.org/io/pointer" "gioui.org/layout" "gioui.org/op" "gioui.org/text" "gioui.org/unit" ) // TestRevealFocusDrainTermination reproduces the IME-open scenario at the // router level: the focused editor field has registered (stale, taller) // bounds, the window's viewport shrinks, and the framework calls // Router.RevealFocus which synthesizes a pointer.Scroll for the focused // field's scroll handler. The app's shrink-frame fix (Renderer // ZeroWheelScroll) drains pointer.Scroll events for the gesture's tag before // gesture.Scroll.Update consumes them. This test verifies: // 1. RevealFocus really does queue a scroll event for the gesture tag, // 2. the drain loop terminates (bounded iteration count), // 3. after the drain, gesture.Scroll.Update returns 0 (content does not // move), and // 4. on a normal (non-shrink) frame the drain is not needed and the // gesture consumes whatever scroll it would have consumed anyway. func TestRevealFocusDrainTermination(t *testing.T) { shp := text.NewShaper(text.WithCollection(gofont.Collection())) r := New(Theme{FontSize: 14}, shp) noop := func(any) {} const ( wW = 411 wH = 914 ) editorRegion := Region{X: 10, Y: 52, W: wW - 20, H: wH - 92} // KeyDown is required: it is what makes drawElement record the // event.Op reference for the tag, without which the key queue clears // the FocusCmd target ("tag has no event.Op references"). editor := NewTextField("editor_text", "hello world\nsecond line\nthird line", editorRegion, true, editorRegion.W, 0, 5, -1, -1, []Interaction{ {Gesture: Scroll, Handler: noop}, {Gesture: KeyDown, Handler: noop}, }) editor.Focused = true elems := []Element{editor} m := unit.Metric{PxPerDp: 1, PxPerSp: 1} var rtr input.Router gtxFor := func(ops *op.Ops) layout.Context { return layout.Context{ Ops: ops, Metric: m, Source: rtr.Source(), Constraints: layout.Constraints{Min: image.Point{}, Max: image.Point{X: wW, Y: wH}}, } } // The app consumes a key.FocusFilter for the focused field every frame // (main.go); that is what marks the handler focusable so keyQueue.Frame // keeps the focus across frames. drainFocus := func(q input.Source) { for { if _, ok := q.Event(key.FocusFilter{Target: event.Tag("editor_text")}); !ok { break } } } // Frame 1: register handlers, focus the field (Draw issues the FocusCmd). { var ops op.Ops gtx := gtxFor(&ops) r.Draw(gtx, elems, 1.0) drainFocus(gtx.Source) rtr.Frame(&ops) } scroll := r.scrolls["editor_text"].scroll t.Logf("focused(editor_text) after frame1: %v", rtr.Source().Focused(event.Tag("editor_text"))) // Frame 2: the app consumes gestures every frame; gesture.Scroll.Update // registers the Scroll kind in the handler's filter, which Router.Deliver // (used by RevealFocus/ScrollFocus) requires to match. { var ops op.Ops gtx := gtxFor(&ops) r.Draw(gtx, elems, 1.0) drainFocus(gtx.Source) _ = scroll.Update(m, gtx.Source, time.Now(), gesture.Vertical, pointer.ScrollRange{}, pointer.ScrollRange{Min: -(1 << 30), Max: 1 << 30}) rtr.Frame(&ops) } // Simulate the keyboard opening: the framework shrinks the viewport and // calls RevealFocus with the new (smaller) viewport. shrunk := image.Rectangle{Min: image.Point{}, Max: image.Point{X: wW, Y: wH - 400}} rtr.RevealFocus(shrunk) // The app's shrink-frame fix: drain pointer.Scroll for the gesture tag. q := rtr.Source() drained := 0 for { evt, ok := q.Event(pointer.Filter{Target: scroll, Kinds: pointer.Scroll}) if !ok { break } drained++ if pe, ok := evt.(pointer.Event); ok { t.Logf("drained synthetic scroll: %+v", pe.Scroll) } if drained > 10 { t.Fatalf("drain loop did not terminate within 10 iterations") } } if drained == 0 { t.Fatalf("RevealFocus did not queue a scroll event for the scroll gesture tag") } // The gesture must now see no scroll (content stays put). var ops op.Ops r.Draw(gtxFor(&ops), elems, 1.0) delta := scroll.Update(m, q, time.Now(), gesture.Vertical, pointer.ScrollRange{}, pointer.ScrollRange{Min: -(1 << 30), Max: 1 << 30}) if delta != 0 { t.Fatalf("gesture consumed %d px of scroll after drain; content would shift", delta) } rtr.Frame(&ops) }