ui: dedup IME snippet/selection to fix rapid-commit desync

TextField.Draw re-pushed key.SnippetCmd and key.SelectionCmd every frame
while focused. Re-pushing an unchanged snippet resets the IME's composition
and caret, so commits arriving faster than a frame interleaved with those
resets and desynced the cursor (garbled/duplicated text).

Mirror widget.Editor's updateSnippet/selection gating: track the last-pushed
snippet and caret in the main-owned Renderer (keyed by field ID, reset on
focus (re)gain), and only emit the ops when they actually change. A fresh
push is forced whenever the field (re)gains focus so the IME always starts
from a known state. Also drop a leftover per-frame 'Focused:' debug print.

On-device: rapid back-to-back commits (0.08-0.1s cadence, faster than a
frame) now land cleanly - type HELLO, append+backspace, and a
browser->editor navigation round-trip all produce exact content.
go test -race ./... green.
This commit is contained in:
Greg Pomerantz 2026-08-16 13:00:25 -04:00
parent 4ee72cf848
commit 46383c1fc1
2 changed files with 151 additions and 113 deletions

View File

@ -7,12 +7,12 @@ import (
"strings" "strings"
"gioui.org/font" "gioui.org/font"
"gioui.org/io/key"
"gioui.org/layout" "gioui.org/layout"
"gioui.org/op" "gioui.org/op"
"gioui.org/op/clip" "gioui.org/op/clip"
"gioui.org/op/paint" "gioui.org/op/paint"
"gioui.org/unit" "gioui.org/unit"
"gioui.org/io/key"
) )
// Region defines a screen area in device-independent pixels (Dp). // Region defines a screen area in device-independent pixels (Dp).
@ -47,9 +47,9 @@ type Container struct {
Children []Element // Changed to Exported Children []Element // Changed to Exported
} }
func (c Container) Type() string { return "container" } func (c Container) Type() string { return "container" }
func (c Container) Region() Region { return c.region } func (c Container) Region() Region { return c.region }
func (c Container) Visible() bool { return c.visible } func (c Container) Visible() bool { return c.visible }
func (c Container) Draw(gtx layout.Context, r *Renderer) { func (c Container) Draw(gtx layout.Context, r *Renderer) {
// Draw background — children are drawn by drawElement, not here // Draw background — children are drawn by drawElement, not here
if c.background != (Color{}) { if c.background != (Color{}) {
@ -96,9 +96,9 @@ type Label struct {
Bold bool Bold bool
} }
func (l Label) Type() string { return "label" } func (l Label) Type() string { return "label" }
func (l Label) Region() Region { return l.region } func (l Label) Region() Region { return l.region }
func (l Label) Visible() bool { return l.visible } func (l Label) Visible() bool { return l.visible }
func (l Label) Interactions() []Interaction { return l.interactions } func (l Label) Interactions() []Interaction { return l.interactions }
func (l Label) Draw(gtx layout.Context, r *Renderer) { func (l Label) Draw(gtx layout.Context, r *Renderer) {
col := l.Color col := l.Color
@ -109,7 +109,7 @@ func (l Label) Draw(gtx layout.Context, r *Renderer) {
// which correctly applies the container offset. No separate RegisterClick needed. // which correctly applies the container offset. No separate RegisterClick needed.
r.drawText(gtx, l.Text, l.FontSize, l.region, l.Align, col, l.id) r.drawText(gtx, l.Text, l.FontSize, l.region, l.Align, col, l.id)
} }
func (l Label) ID() string { return l.id } func (l Label) ID() string { return l.id }
// String returns a string representation of the Label. // String returns a string representation of the Label.
func (l Label) String() string { func (l Label) String() string {
@ -139,9 +139,9 @@ type Icon struct {
Size Dp // 0 = default icon size Size Dp // 0 = default icon size
} }
func (i Icon) Type() string { return "icon" } func (i Icon) Type() string { return "icon" }
func (i Icon) Region() Region { return i.region } func (i Icon) Region() Region { return i.region }
func (i Icon) Visible() bool { return i.visible } func (i Icon) Visible() bool { return i.visible }
func (i Icon) Interactions() []Interaction { return i.interactions } func (i Icon) Interactions() []Interaction { return i.interactions }
func (i Icon) Draw(gtx layout.Context, r *Renderer) { func (i Icon) Draw(gtx layout.Context, r *Renderer) {
img := r.icon(i.Name) img := r.icon(i.Name)
@ -157,7 +157,7 @@ func (i Icon) Draw(gtx layout.Context, r *Renderer) {
} }
r.drawPng(gtx, img, i.region, w, h) r.drawPng(gtx, img, i.region, w, h)
} }
func (i Icon) ID() string { return i.id } func (i Icon) ID() string { return i.id }
// String returns a string representation of the Icon. // String returns a string representation of the Icon.
func (i Icon) String() string { func (i Icon) String() string {
@ -178,27 +178,27 @@ func NewIcon(name string, region Region, size Dp, interactions []Interaction) Ic
// TextField accepts text input or displays multiline text. // TextField accepts text input or displays multiline text.
type TextField struct { type TextField struct {
id string id string
region Region region Region
visible bool visible bool
interactions []Interaction interactions []Interaction
Value string Value string
Placeholder string Placeholder string
Focused bool Focused bool
Multiline bool Multiline bool
CursorPosition int CursorPosition int
ScrollOffset Dp ScrollOffset Dp
VisibleLines []Line VisibleLines []Line
WordWrap bool WordWrap bool
WrapWidth Dp WrapWidth Dp
} }
func (tf TextField) Type() string { return "textfield" } func (tf TextField) Type() string { return "textfield" }
func (tf TextField) Region() Region { return tf.region } func (tf TextField) Region() Region { return tf.region }
func (tf TextField) Visible() bool { return tf.visible } func (tf TextField) Visible() bool { return tf.visible }
func (tf TextField) ID() string { return tf.id } func (tf TextField) ID() string { return tf.id }
func (tf TextField) Interactions() []Interaction { return tf.interactions } func (tf TextField) Interactions() []Interaction { return tf.interactions }
func (tf TextField) NeedsClip() bool { return true } func (tf TextField) NeedsClip() bool { return true }
// String returns a string representation of the TextField. // String returns a string representation of the TextField.
func (tf TextField) String() string { func (tf TextField) String() string {
@ -209,7 +209,15 @@ func (tf TextField) String() string {
// and draws display lines inline — one LayoutString call, no double-shaping. // and draws display lines inline — one LayoutString call, no double-shaping.
func (tf TextField) Draw(gtx layout.Context, r *Renderer) { func (tf TextField) Draw(gtx layout.Context, r *Renderer) {
if tf.Focused { if tf.Focused {
fmt.Printf("Focused: tag = %s\n", tf.id) // (Re-)gained focus for this field (or a different field than the one the
// dedup state currently tracks): force a fresh snippet/selection push so
// the IME starts from a known state.
if r.lastIMEField != tf.id || !r.lastWasFocused {
r.lastSnippet = key.Snippet{}
r.lastSelCaret = -1
}
r.lastIMEField = tf.id
r.lastWasFocused = true
gtx.Execute(key.FocusCmd{Tag: tf.id}) gtx.Execute(key.FocusCmd{Tag: tf.id})
gtx.Execute(key.SoftKeyboardCmd{Show: true}) gtx.Execute(key.SoftKeyboardCmd{Show: true})
// IME wiring. The visible window (tf.Value) is pushed as the snippet // IME wiring. The visible window (tf.Value) is pushed as the snippet
@ -220,15 +228,34 @@ func (tf TextField) Draw(gtx layout.Context, r *Renderer) {
// Item 4: tell the IME this is a text field (enables the text keyboard, // Item 4: tell the IME this is a text field (enables the text keyboard,
// autocorrect, and suggestions). // autocorrect, and suggestions).
key.InputHintOp{Tag: tf.id, Hint: key.HintText}.Add(gtx.Ops) key.InputHintOp{Tag: tf.id, Hint: key.HintText}.Add(gtx.Ops)
// Item 2: push the snippet (the visible window) for swipe/autocorrect. // Item 2: push the snippet (the visible window) for swipe/autocorrect,
gtx.Execute(key.SnippetCmd{Tag: tf.id, Snippet: key.Snippet{ // but only when it changed. Re-pushing an unchanged snippet every frame
// resets the IME's composition/cursor, which desyncs fast commits (see
// widget.Editor's updateSnippet dedup).
snippet := key.Snippet{
Range: key.Range{Start: 0, End: runeCount(tf.Value, len(tf.Value))}, Range: key.Range{Start: 0, End: runeCount(tf.Value, len(tf.Value))},
Text: tf.Value, Text: tf.Value,
}}) }
if snippet != r.lastSnippet {
r.lastSnippet = snippet
gtx.Execute(key.SnippetCmd{Tag: tf.id, Snippet: snippet})
}
// Item 1: sync the caret so the IME's selection matches. Window-relative // Item 1: sync the caret so the IME's selection matches. Window-relative
// rune index of the caret (tf.CursorPosition is a byte offset in tf.Value). // rune index of the caret (tf.CursorPosition is a byte offset in
// tf.Value). Push only when it moves, so a static caret does not reset
// the IME every frame.
caret := runeCount(tf.Value, tf.CursorPosition) caret := runeCount(tf.Value, tf.CursorPosition)
gtx.Execute(key.SelectionCmd{Tag: tf.id, Range: key.Range{Start: caret, End: caret}, Caret: key.Caret{}}) if caret != r.lastSelCaret {
r.lastSelCaret = caret
gtx.Execute(key.SelectionCmd{Tag: tf.id, Range: key.Range{Start: caret, End: caret}, Caret: key.Caret{}})
}
} else if r.lastIMEField == tf.id {
// This (previously-focused) field lost focus: forget it so the next focus
// pushes a fresh snippet/selection.
r.lastIMEField = ""
r.lastWasFocused = false
r.lastSnippet = key.Snippet{}
r.lastSelCaret = -1
} }
r.drawWrappedText(gtx, tf.Value, tf.region, tf.WrapWidth, tf.ScrollOffset, tf.CursorPosition) r.drawWrappedText(gtx, tf.Value, tf.region, tf.WrapWidth, tf.ScrollOffset, tf.CursorPosition)
} }
@ -253,15 +280,15 @@ func runeCount(s string, bytePos int) int {
// NewTextField creates a visible multiline TextField. // NewTextField creates a visible multiline TextField.
func NewTextField(id string, value string, region Region, wrapWidth Dp, scrollOffset Dp, cursorPos int, interactions []Interaction) TextField { func NewTextField(id string, value string, region Region, wrapWidth Dp, scrollOffset Dp, cursorPos int, interactions []Interaction) TextField {
return TextField{ return TextField{
id: id, id: id,
region: region, region: region,
visible: true, visible: true,
interactions: interactions, interactions: interactions,
Value: value, Value: value,
Multiline: true, Multiline: true,
WordWrap: true, WordWrap: true,
WrapWidth: wrapWidth, WrapWidth: wrapWidth,
ScrollOffset: scrollOffset, ScrollOffset: scrollOffset,
CursorPosition: cursorPos, CursorPosition: cursorPos,
} }
} }
@ -274,21 +301,21 @@ type Line struct {
// ListView displays a scrollable list of items. // ListView displays a scrollable list of items.
type ListView struct { type ListView struct {
id string id string
region Region region Region
visible bool visible bool
interactions []Interaction interactions []Interaction
Items []ListItem Items []ListItem
ScrollOffset Dp // pixel-level scroll offset ScrollOffset Dp // pixel-level scroll offset
Selected int Selected int
RowTapHandler func(any) // handler for row taps, receives index as any RowTapHandler func(any) // handler for row taps, receives index as any
} }
func (lv ListView) Type() string { return "listview" } func (lv ListView) Type() string { return "listview" }
func (lv ListView) Region() Region { return lv.region } func (lv ListView) Region() Region { return lv.region }
func (lv ListView) Visible() bool { return lv.visible } func (lv ListView) Visible() bool { return lv.visible }
func (lv ListView) ID() string { return lv.id } func (lv ListView) ID() string { return lv.id }
func (lv ListView) NeedsClip() bool { return true } func (lv ListView) NeedsClip() bool { return true }
func (lv ListView) Interactions() []Interaction { func (lv ListView) Interactions() []Interaction {
// ListView registers its scroll in Draw, not via registerInteraction. // ListView registers its scroll in Draw, not via registerInteraction.
// Filter out Scroll so registerInteraction only handles Tap. // Filter out Scroll so registerInteraction only handles Tap.
@ -340,12 +367,12 @@ func (lv ListView) Draw(gtx layout.Context, r *Renderer) {
} }
// Draw background for selected item (removed as per request) // Draw background for selected item (removed as per request)
/* /*
if item.Selected || (lv.Selected == i) { if item.Selected || (lv.Selected == i) {
r.drawBg(gtx, Region{ r.drawBg(gtx, Region{
X: lv.region.X, Y: y, X: lv.region.X, Y: y,
W: lv.region.W, H: rowHeight, W: lv.region.W, H: rowHeight,
}, Color{R: 200, G: 220, B: 255, A: 255}) }, Color{R: 200, G: 220, B: 255, A: 255})
} }
*/ */
// Register click area for this row // Register click area for this row
rowID := fmt.Sprintf("list_row_%d", rowGlobalIndex) rowID := fmt.Sprintf("list_row_%d", rowGlobalIndex)
@ -421,10 +448,10 @@ type AlphaIndex struct {
ActiveLetter string ActiveLetter string
} }
func (ai AlphaIndex) Type() string { return "alphaindex" } func (ai AlphaIndex) Type() string { return "alphaindex" }
func (ai AlphaIndex) Region() Region { return ai.region } func (ai AlphaIndex) Region() Region { return ai.region }
func (ai AlphaIndex) Visible() bool { return ai.visible } func (ai AlphaIndex) Visible() bool { return ai.visible }
func (ai AlphaIndex) ID() string { return ai.id } func (ai AlphaIndex) ID() string { return ai.id }
func (ai AlphaIndex) Interactions() []Interaction { return ai.interactions } func (ai AlphaIndex) Interactions() []Interaction { return ai.interactions }
// String returns a string representation of the AlphaIndex. // String returns a string representation of the AlphaIndex.
@ -452,15 +479,15 @@ type Button struct {
Primary bool Primary bool
} }
func (b Button) Type() string { return "button" } func (b Button) Type() string { return "button" }
func (b Button) Region() Region { return b.region } func (b Button) Region() Region { return b.region }
func (b Button) Visible() bool { return b.visible } func (b Button) Visible() bool { return b.visible }
func (b Button) Interactions() []Interaction { return b.interactions } func (b Button) Interactions() []Interaction { return b.interactions }
func (b Button) Draw(gtx layout.Context, r *Renderer) { func (b Button) Draw(gtx layout.Context, r *Renderer) {
col := Color{R: 0, G: 0, B: 0, A: 255} col := Color{R: 0, G: 0, B: 0, A: 255}
r.drawText(gtx, b.Text, r.theme.FontSize, b.region, AlignStart, col, b.id) r.drawText(gtx, b.Text, r.theme.FontSize, b.region, AlignStart, col, b.id)
} }
func (b Button) ID() string { return b.id } func (b Button) ID() string { return b.id }
// String returns a string representation of the Button. // String returns a string representation of the Button.
func (b Button) String() string { func (b Button) String() string {
@ -490,10 +517,10 @@ type SearchBar struct {
Forward bool Forward bool
} }
func (sb SearchBar) Type() string { return "searchbar" } func (sb SearchBar) Type() string { return "searchbar" }
func (sb SearchBar) Region() Region { return sb.region } func (sb SearchBar) Region() Region { return sb.region }
func (sb SearchBar) Visible() bool { return sb.visible } func (sb SearchBar) Visible() bool { return sb.visible }
func (sb SearchBar) ID() string { return sb.id } func (sb SearchBar) ID() string { return sb.id }
func (sb SearchBar) Interactions() []Interaction { return sb.interactions } func (sb SearchBar) Interactions() []Interaction { return sb.interactions }
// String returns a string representation of the SearchBar. // String returns a string representation of the SearchBar.
@ -526,11 +553,11 @@ type Cursor struct {
Selection *Selection Selection *Selection
} }
func (c Cursor) NeedsClip() bool { return true } func (c Cursor) NeedsClip() bool { return true }
func (c Cursor) Type() string { return "cursor" } func (c Cursor) Type() string { return "cursor" }
func (c Cursor) Region() Region { return c.region } func (c Cursor) Region() Region { return c.region }
func (c Cursor) Visible() bool { return c.visible } func (c Cursor) Visible() bool { return c.visible }
func (c Cursor) ID() string { return c.id } func (c Cursor) ID() string { return c.id }
func (c Cursor) Interactions() []Interaction { return c.interactions } func (c Cursor) Interactions() []Interaction { return c.interactions }
// String returns a string representation of the Cursor. // String returns a string representation of the Cursor.
@ -596,10 +623,10 @@ type MergeHunk struct {
Resolution HunkResolution Resolution HunkResolution
} }
func (mh MergeHunk) Type() string { return "mergehunk" } func (mh MergeHunk) Type() string { return "mergehunk" }
func (mh MergeHunk) Region() Region { return mh.region } func (mh MergeHunk) Region() Region { return mh.region }
func (mh MergeHunk) Visible() bool { return mh.visible } func (mh MergeHunk) Visible() bool { return mh.visible }
func (mh MergeHunk) ID() string { return mh.id } func (mh MergeHunk) ID() string { return mh.id }
func (mh MergeHunk) Interactions() []Interaction { return mh.interactions } func (mh MergeHunk) Interactions() []Interaction { return mh.interactions }
// String returns a string representation of the MergeHunk. // String returns a string representation of the MergeHunk.
@ -627,10 +654,10 @@ type Toast struct {
Timeout int // milliseconds Timeout int // milliseconds
} }
func (t Toast) Type() string { return "toast" } func (t Toast) Type() string { return "toast" }
func (t Toast) Region() Region { return t.region } func (t Toast) Region() Region { return t.region }
func (t Toast) Visible() bool { return t.visible } func (t Toast) Visible() bool { return t.visible }
func (t Toast) ID() string { return t.id } func (t Toast) ID() string { return t.id }
func (t Toast) Interactions() []Interaction { return t.interactions } func (t Toast) Interactions() []Interaction { return t.interactions }
// String returns a string representation of the Toast. // String returns a string representation of the Toast.
@ -656,10 +683,10 @@ type Spacer struct {
interactions []Interaction interactions []Interaction
} }
func (s Spacer) Type() string { return "spacer" } func (s Spacer) Type() string { return "spacer" }
func (s Spacer) Region() Region { return s.region } func (s Spacer) Region() Region { return s.region }
func (s Spacer) Visible() bool { return s.visible } func (s Spacer) Visible() bool { return s.visible }
func (s Spacer) ID() string { return s.id } func (s Spacer) ID() string { return s.id }
func (s Spacer) Interactions() []Interaction { return s.interactions } func (s Spacer) Interactions() []Interaction { return s.interactions }
// String returns a string representation of the Spacer. // String returns a string representation of the Spacer.
@ -685,10 +712,10 @@ type GioEditor struct {
interactions []Interaction interactions []Interaction
} }
func (ge GioEditor) Type() string { return "gioeditor" } func (ge GioEditor) Type() string { return "gioeditor" }
func (ge GioEditor) Region() Region { return ge.region } func (ge GioEditor) Region() Region { return ge.region }
func (ge GioEditor) Visible() bool { return ge.visible } func (ge GioEditor) Visible() bool { return ge.visible }
func (ge GioEditor) ID() string { return ge.id } func (ge GioEditor) ID() string { return ge.id }
func (ge GioEditor) Interactions() []Interaction { return ge.interactions } func (ge GioEditor) Interactions() []Interaction { return ge.interactions }
// String returns a string representation of the GioEditor. // String returns a string representation of the GioEditor.

View File

@ -15,6 +15,7 @@ import (
"gioui.org/gesture" "gioui.org/gesture"
"gioui.org/io/event" // Import event package "gioui.org/io/event" // Import event package
"gioui.org/io/input" "gioui.org/io/input"
"gioui.org/io/key"
"gioui.org/io/pointer" "gioui.org/io/pointer"
"gioui.org/layout" "gioui.org/layout"
"gioui.org/op" "gioui.org/op"
@ -55,17 +56,27 @@ type scrollReg struct {
// that Gio mutates during draw (e.g. the search bar's widget.Editor): such // that Gio mutates during draw (e.g. the search bar's widget.Editor): such
// state must not live in the logic goroutine's State (architecture.md §1). // state must not live in the logic goroutine's State (architecture.md §1).
type Renderer struct { type Renderer struct {
theme Theme theme Theme
shp *text.Shaper shp *text.Shaper
scale float32 // px-per-Dp for the current draw pass; set in Draw scale float32 // px-per-Dp for the current draw pass; set in Draw
icons map[string]image.Image icons map[string]image.Image
clicks map[string]clickReg clicks map[string]clickReg
Keys map[string]keyReg // Exported Keys map Keys map[string]keyReg // Exported Keys map
scrolls map[string]scrollReg scrolls map[string]scrollReg
gioEditors map[string]*widget.Editor // main-owned widget editors by element ID gioEditors map[string]*widget.Editor // main-owned widget editors by element ID
displayLineCount int // number of display lines from last drawWrappedText displayLineCount int // number of display lines from last drawWrappedText
lastLineY Dp // last line baseline offset from text origin, in Dp (derived from GlyphLayout) lastLineY Dp // last line baseline offset from text origin, in Dp (derived from GlyphLayout)
glyphLayout GlyphLayout // captured per-glyph layout from last drawWrappedText glyphLayout GlyphLayout // captured per-glyph layout from last drawWrappedText
// IME dedup (main-owned, persistent across frames). Re-pushing an unchanged
// snippet or selection every frame resets the IME's composition and caret,
// which desyncs fast commits; push only on change, as widget.Editor does in
// updateSnippet and its selection gating. Keyed to the focused field's ID so
// it stays correct if a second TextField is ever added.
lastIMEField string
lastWasFocused bool
lastSnippet key.Snippet
lastSelCaret int // window-relative rune index of last-pushed caret; -1 = none
} }
// New creates a new Renderer. // New creates a new Renderer.
@ -228,7 +239,7 @@ func (r *Renderer) CheckGestures(q input.Source, m unit.Metric) []InputEvent {
// ScrollY range: Min = -scrollOffset (remaining above), Max = large (content height unknown yet). // ScrollY range: Min = -scrollOffset (remaining above), Max = large (content height unknown yet).
// With Min==Max==0, clampSplit consumes zero scroll. // With Min==Max==0, clampSplit consumes zero scroll.
delta := reg.scroll.Update(m, q, time.Now(), gesture.Vertical, delta := reg.scroll.Update(m, q, time.Now(), gesture.Vertical,
pointer.ScrollRange{}, pointer.ScrollRange{Min: -(1<<30), Max: 1<<30}) pointer.ScrollRange{}, pointer.ScrollRange{Min: -(1 << 30), Max: 1 << 30})
if delta != 0 { if delta != 0 {
events = append(events, InputEvent{ events = append(events, InputEvent{
Handler: reg.handler, Handler: reg.handler,
@ -400,7 +411,7 @@ func (r *Renderer) drawText(gtx layout.Context, str string, size unit.Sp, reg Re
case AlignStart: case AlignStart:
drawX = reg.X drawX = reg.X
case AlignCenter: case AlignCenter:
drawX = reg.X + (reg.W - textW) / 2 drawX = reg.X + (reg.W-textW)/2
case AlignEnd: case AlignEnd:
drawX = reg.X + reg.W - textW drawX = reg.X + reg.W - textW
} }
@ -486,9 +497,9 @@ func (r *Renderer) drawWrappedText(gtx layout.Context, str string, reg Region, w
PxPerEm: fixed.I(gtx.Sp(r.theme.FontSize)), PxPerEm: fixed.I(gtx.Sp(r.theme.FontSize)),
MinWidth: 0, MinWidth: 0,
MaxWidth: int(r.toPx(wrapWidth)), MaxWidth: int(r.toPx(wrapWidth)),
MaxLines: 0, // unlimited - wrap at MaxWidth MaxLines: 0, // unlimited - wrap at MaxWidth
LineHeight: fixed.I(gtx.Sp(lineHeightSp)), LineHeight: fixed.I(gtx.Sp(lineHeightSp)),
LineHeightScale: 1.0, // use LineHeight directly, don't scale LineHeightScale: 1.0, // use LineHeight directly, don't scale
WrapPolicy: text.WrapHeuristically, WrapPolicy: text.WrapHeuristically,
} }
r.shp.LayoutString(params, str) r.shp.LayoutString(params, str)
@ -515,7 +526,7 @@ func (r *Renderer) drawWrappedText(gtx layout.Context, str string, reg Region, w
var layout GlyphLayout var layout GlyphLayout
layout.LineHeight = Dp(float32(lineHeightSp)) layout.LineHeight = Dp(float32(lineHeightSp))
byteOffset := 0 byteOffset := 0
layout.VisualLineStarts = append(layout.VisualLineStarts,byteOffset) layout.VisualLineStarts = append(layout.VisualLineStarts, byteOffset)
for g, ok := r.shp.NextGlyph(); ok; g, ok = r.shp.NextGlyph() { for g, ok := r.shp.NextGlyph(); ok; g, ok = r.shp.NextGlyph() {
// Record layout data for this glyph. // Record layout data for this glyph.
// g.X is in fixed.Int26_6 — shift >> 6 for device pixels, divide by scale for Dp. // g.X is in fixed.Int26_6 — shift >> 6 for device pixels, divide by scale for Dp.
@ -537,7 +548,7 @@ func (r *Renderer) drawWrappedText(gtx layout.Context, str string, reg Region, w
line = line[:0] line = line[:0]
if g.Flags&text.FlagLineBreak != 0 { if g.Flags&text.FlagLineBreak != 0 {
lineCount++ lineCount++
layout.VisualLineStarts = append(layout.VisualLineStarts,byteOffset) layout.VisualLineStarts = append(layout.VisualLineStarts, byteOffset)
} }
} }
} }
@ -565,7 +576,7 @@ func (r *Renderer) drawWrappedText(gtx layout.Context, str string, reg Region, w
cursorY = reg.Y - scrollOffset + layout.Y[len(layout.Y)-1] - Dp(r.theme.FontSize) cursorY = reg.Y - scrollOffset + layout.Y[len(layout.Y)-1] - Dp(r.theme.FontSize)
} }
} }
// Draw the cursor (thin vertical bar) // Draw the cursor (thin vertical bar)
cursorRegion := Region{ cursorRegion := Region{
X: cursorX, X: cursorX,