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:
parent
4ee72cf848
commit
46383c1fc1
|
|
@ -7,12 +7,12 @@ import (
|
|||
"strings"
|
||||
|
||||
"gioui.org/font"
|
||||
"gioui.org/io/key"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op"
|
||||
"gioui.org/op/clip"
|
||||
"gioui.org/op/paint"
|
||||
"gioui.org/unit"
|
||||
"gioui.org/io/key"
|
||||
)
|
||||
|
||||
// Region defines a screen area in device-independent pixels (Dp).
|
||||
|
|
@ -47,9 +47,9 @@ type Container struct {
|
|||
Children []Element // Changed to Exported
|
||||
}
|
||||
|
||||
func (c Container) Type() string { return "container" }
|
||||
func (c Container) Region() Region { return c.region }
|
||||
func (c Container) Visible() bool { return c.visible }
|
||||
func (c Container) Type() string { return "container" }
|
||||
func (c Container) Region() Region { return c.region }
|
||||
func (c Container) Visible() bool { return c.visible }
|
||||
func (c Container) Draw(gtx layout.Context, r *Renderer) {
|
||||
// Draw background — children are drawn by drawElement, not here
|
||||
if c.background != (Color{}) {
|
||||
|
|
@ -96,9 +96,9 @@ type Label struct {
|
|||
Bold bool
|
||||
}
|
||||
|
||||
func (l Label) Type() string { return "label" }
|
||||
func (l Label) Region() Region { return l.region }
|
||||
func (l Label) Visible() bool { return l.visible }
|
||||
func (l Label) Type() string { return "label" }
|
||||
func (l Label) Region() Region { return l.region }
|
||||
func (l Label) Visible() bool { return l.visible }
|
||||
func (l Label) Interactions() []Interaction { return l.interactions }
|
||||
func (l Label) Draw(gtx layout.Context, r *Renderer) {
|
||||
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.
|
||||
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.
|
||||
func (l Label) String() string {
|
||||
|
|
@ -139,9 +139,9 @@ type Icon struct {
|
|||
Size Dp // 0 = default icon size
|
||||
}
|
||||
|
||||
func (i Icon) Type() string { return "icon" }
|
||||
func (i Icon) Region() Region { return i.region }
|
||||
func (i Icon) Visible() bool { return i.visible }
|
||||
func (i Icon) Type() string { return "icon" }
|
||||
func (i Icon) Region() Region { return i.region }
|
||||
func (i Icon) Visible() bool { return i.visible }
|
||||
func (i Icon) Interactions() []Interaction { return i.interactions }
|
||||
func (i Icon) Draw(gtx layout.Context, r *Renderer) {
|
||||
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)
|
||||
}
|
||||
func (i Icon) ID() string { return i.id }
|
||||
func (i Icon) ID() string { return i.id }
|
||||
|
||||
// String returns a string representation of the Icon.
|
||||
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.
|
||||
type TextField struct {
|
||||
id string
|
||||
region Region
|
||||
visible bool
|
||||
interactions []Interaction
|
||||
Value string
|
||||
Placeholder string
|
||||
Focused bool
|
||||
Multiline bool
|
||||
id string
|
||||
region Region
|
||||
visible bool
|
||||
interactions []Interaction
|
||||
Value string
|
||||
Placeholder string
|
||||
Focused bool
|
||||
Multiline bool
|
||||
CursorPosition int
|
||||
ScrollOffset Dp
|
||||
VisibleLines []Line
|
||||
WordWrap bool
|
||||
WrapWidth Dp
|
||||
ScrollOffset Dp
|
||||
VisibleLines []Line
|
||||
WordWrap bool
|
||||
WrapWidth Dp
|
||||
}
|
||||
|
||||
func (tf TextField) Type() string { return "textfield" }
|
||||
func (tf TextField) Region() Region { return tf.region }
|
||||
func (tf TextField) Visible() bool { return tf.visible }
|
||||
func (tf TextField) ID() string { return tf.id }
|
||||
func (tf TextField) Type() string { return "textfield" }
|
||||
func (tf TextField) Region() Region { return tf.region }
|
||||
func (tf TextField) Visible() bool { return tf.visible }
|
||||
func (tf TextField) ID() string { return tf.id }
|
||||
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.
|
||||
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.
|
||||
func (tf TextField) Draw(gtx layout.Context, r *Renderer) {
|
||||
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.SoftKeyboardCmd{Show: true})
|
||||
// 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,
|
||||
// autocorrect, and suggestions).
|
||||
key.InputHintOp{Tag: tf.id, Hint: key.HintText}.Add(gtx.Ops)
|
||||
// Item 2: push the snippet (the visible window) for swipe/autocorrect.
|
||||
gtx.Execute(key.SnippetCmd{Tag: tf.id, Snippet: key.Snippet{
|
||||
// Item 2: push the snippet (the visible window) for swipe/autocorrect,
|
||||
// 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))},
|
||||
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
|
||||
// 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)
|
||||
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)
|
||||
}
|
||||
|
|
@ -253,15 +280,15 @@ func runeCount(s string, bytePos int) int {
|
|||
// NewTextField creates a visible multiline TextField.
|
||||
func NewTextField(id string, value string, region Region, wrapWidth Dp, scrollOffset Dp, cursorPos int, interactions []Interaction) TextField {
|
||||
return TextField{
|
||||
id: id,
|
||||
region: region,
|
||||
visible: true,
|
||||
interactions: interactions,
|
||||
Value: value,
|
||||
Multiline: true,
|
||||
WordWrap: true,
|
||||
WrapWidth: wrapWidth,
|
||||
ScrollOffset: scrollOffset,
|
||||
id: id,
|
||||
region: region,
|
||||
visible: true,
|
||||
interactions: interactions,
|
||||
Value: value,
|
||||
Multiline: true,
|
||||
WordWrap: true,
|
||||
WrapWidth: wrapWidth,
|
||||
ScrollOffset: scrollOffset,
|
||||
CursorPosition: cursorPos,
|
||||
}
|
||||
}
|
||||
|
|
@ -274,21 +301,21 @@ type Line struct {
|
|||
|
||||
// ListView displays a scrollable list of items.
|
||||
type ListView struct {
|
||||
id string
|
||||
region Region
|
||||
visible bool
|
||||
interactions []Interaction
|
||||
Items []ListItem
|
||||
ScrollOffset Dp // pixel-level scroll offset
|
||||
Selected int
|
||||
id string
|
||||
region Region
|
||||
visible bool
|
||||
interactions []Interaction
|
||||
Items []ListItem
|
||||
ScrollOffset Dp // pixel-level scroll offset
|
||||
Selected int
|
||||
RowTapHandler func(any) // handler for row taps, receives index as any
|
||||
}
|
||||
|
||||
func (lv ListView) Type() string { return "listview" }
|
||||
func (lv ListView) Region() Region { return lv.region }
|
||||
func (lv ListView) Visible() bool { return lv.visible }
|
||||
func (lv ListView) ID() string { return lv.id }
|
||||
func (lv ListView) NeedsClip() bool { return true }
|
||||
func (lv ListView) Type() string { return "listview" }
|
||||
func (lv ListView) Region() Region { return lv.region }
|
||||
func (lv ListView) Visible() bool { return lv.visible }
|
||||
func (lv ListView) ID() string { return lv.id }
|
||||
func (lv ListView) NeedsClip() bool { return true }
|
||||
func (lv ListView) Interactions() []Interaction {
|
||||
// ListView registers its scroll in Draw, not via registerInteraction.
|
||||
// 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)
|
||||
/*
|
||||
if item.Selected || (lv.Selected == i) {
|
||||
r.drawBg(gtx, Region{
|
||||
X: lv.region.X, Y: y,
|
||||
W: lv.region.W, H: rowHeight,
|
||||
}, Color{R: 200, G: 220, B: 255, A: 255})
|
||||
}
|
||||
if item.Selected || (lv.Selected == i) {
|
||||
r.drawBg(gtx, Region{
|
||||
X: lv.region.X, Y: y,
|
||||
W: lv.region.W, H: rowHeight,
|
||||
}, Color{R: 200, G: 220, B: 255, A: 255})
|
||||
}
|
||||
*/
|
||||
// Register click area for this row
|
||||
rowID := fmt.Sprintf("list_row_%d", rowGlobalIndex)
|
||||
|
|
@ -421,10 +448,10 @@ type AlphaIndex struct {
|
|||
ActiveLetter string
|
||||
}
|
||||
|
||||
func (ai AlphaIndex) Type() string { return "alphaindex" }
|
||||
func (ai AlphaIndex) Region() Region { return ai.region }
|
||||
func (ai AlphaIndex) Visible() bool { return ai.visible }
|
||||
func (ai AlphaIndex) ID() string { return ai.id }
|
||||
func (ai AlphaIndex) Type() string { return "alphaindex" }
|
||||
func (ai AlphaIndex) Region() Region { return ai.region }
|
||||
func (ai AlphaIndex) Visible() bool { return ai.visible }
|
||||
func (ai AlphaIndex) ID() string { return ai.id }
|
||||
func (ai AlphaIndex) Interactions() []Interaction { return ai.interactions }
|
||||
|
||||
// String returns a string representation of the AlphaIndex.
|
||||
|
|
@ -452,15 +479,15 @@ type Button struct {
|
|||
Primary bool
|
||||
}
|
||||
|
||||
func (b Button) Type() string { return "button" }
|
||||
func (b Button) Region() Region { return b.region }
|
||||
func (b Button) Visible() bool { return b.visible }
|
||||
func (b Button) Type() string { return "button" }
|
||||
func (b Button) Region() Region { return b.region }
|
||||
func (b Button) Visible() bool { return b.visible }
|
||||
func (b Button) Interactions() []Interaction { return b.interactions }
|
||||
func (b Button) Draw(gtx layout.Context, r *Renderer) {
|
||||
col := Color{R: 0, G: 0, B: 0, A: 255}
|
||||
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.
|
||||
func (b Button) String() string {
|
||||
|
|
@ -490,10 +517,10 @@ type SearchBar struct {
|
|||
Forward bool
|
||||
}
|
||||
|
||||
func (sb SearchBar) Type() string { return "searchbar" }
|
||||
func (sb SearchBar) Region() Region { return sb.region }
|
||||
func (sb SearchBar) Visible() bool { return sb.visible }
|
||||
func (sb SearchBar) ID() string { return sb.id }
|
||||
func (sb SearchBar) Type() string { return "searchbar" }
|
||||
func (sb SearchBar) Region() Region { return sb.region }
|
||||
func (sb SearchBar) Visible() bool { return sb.visible }
|
||||
func (sb SearchBar) ID() string { return sb.id }
|
||||
func (sb SearchBar) Interactions() []Interaction { return sb.interactions }
|
||||
|
||||
// String returns a string representation of the SearchBar.
|
||||
|
|
@ -526,11 +553,11 @@ type Cursor struct {
|
|||
Selection *Selection
|
||||
}
|
||||
|
||||
func (c Cursor) NeedsClip() bool { return true }
|
||||
func (c Cursor) Type() string { return "cursor" }
|
||||
func (c Cursor) Region() Region { return c.region }
|
||||
func (c Cursor) Visible() bool { return c.visible }
|
||||
func (c Cursor) ID() string { return c.id }
|
||||
func (c Cursor) NeedsClip() bool { return true }
|
||||
func (c Cursor) Type() string { return "cursor" }
|
||||
func (c Cursor) Region() Region { return c.region }
|
||||
func (c Cursor) Visible() bool { return c.visible }
|
||||
func (c Cursor) ID() string { return c.id }
|
||||
func (c Cursor) Interactions() []Interaction { return c.interactions }
|
||||
|
||||
// String returns a string representation of the Cursor.
|
||||
|
|
@ -596,10 +623,10 @@ type MergeHunk struct {
|
|||
Resolution HunkResolution
|
||||
}
|
||||
|
||||
func (mh MergeHunk) Type() string { return "mergehunk" }
|
||||
func (mh MergeHunk) Region() Region { return mh.region }
|
||||
func (mh MergeHunk) Visible() bool { return mh.visible }
|
||||
func (mh MergeHunk) ID() string { return mh.id }
|
||||
func (mh MergeHunk) Type() string { return "mergehunk" }
|
||||
func (mh MergeHunk) Region() Region { return mh.region }
|
||||
func (mh MergeHunk) Visible() bool { return mh.visible }
|
||||
func (mh MergeHunk) ID() string { return mh.id }
|
||||
func (mh MergeHunk) Interactions() []Interaction { return mh.interactions }
|
||||
|
||||
// String returns a string representation of the MergeHunk.
|
||||
|
|
@ -627,10 +654,10 @@ type Toast struct {
|
|||
Timeout int // milliseconds
|
||||
}
|
||||
|
||||
func (t Toast) Type() string { return "toast" }
|
||||
func (t Toast) Region() Region { return t.region }
|
||||
func (t Toast) Visible() bool { return t.visible }
|
||||
func (t Toast) ID() string { return t.id }
|
||||
func (t Toast) Type() string { return "toast" }
|
||||
func (t Toast) Region() Region { return t.region }
|
||||
func (t Toast) Visible() bool { return t.visible }
|
||||
func (t Toast) ID() string { return t.id }
|
||||
func (t Toast) Interactions() []Interaction { return t.interactions }
|
||||
|
||||
// String returns a string representation of the Toast.
|
||||
|
|
@ -656,10 +683,10 @@ type Spacer struct {
|
|||
interactions []Interaction
|
||||
}
|
||||
|
||||
func (s Spacer) Type() string { return "spacer" }
|
||||
func (s Spacer) Region() Region { return s.region }
|
||||
func (s Spacer) Visible() bool { return s.visible }
|
||||
func (s Spacer) ID() string { return s.id }
|
||||
func (s Spacer) Type() string { return "spacer" }
|
||||
func (s Spacer) Region() Region { return s.region }
|
||||
func (s Spacer) Visible() bool { return s.visible }
|
||||
func (s Spacer) ID() string { return s.id }
|
||||
func (s Spacer) Interactions() []Interaction { return s.interactions }
|
||||
|
||||
// String returns a string representation of the Spacer.
|
||||
|
|
@ -685,10 +712,10 @@ type GioEditor struct {
|
|||
interactions []Interaction
|
||||
}
|
||||
|
||||
func (ge GioEditor) Type() string { return "gioeditor" }
|
||||
func (ge GioEditor) Region() Region { return ge.region }
|
||||
func (ge GioEditor) Visible() bool { return ge.visible }
|
||||
func (ge GioEditor) ID() string { return ge.id }
|
||||
func (ge GioEditor) Type() string { return "gioeditor" }
|
||||
func (ge GioEditor) Region() Region { return ge.region }
|
||||
func (ge GioEditor) Visible() bool { return ge.visible }
|
||||
func (ge GioEditor) ID() string { return ge.id }
|
||||
func (ge GioEditor) Interactions() []Interaction { return ge.interactions }
|
||||
|
||||
// String returns a string representation of the GioEditor.
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import (
|
|||
"gioui.org/gesture"
|
||||
"gioui.org/io/event" // Import event package
|
||||
"gioui.org/io/input"
|
||||
"gioui.org/io/key"
|
||||
"gioui.org/io/pointer"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op"
|
||||
|
|
@ -55,17 +56,27 @@ type scrollReg struct {
|
|||
// 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).
|
||||
type Renderer struct {
|
||||
theme Theme
|
||||
shp *text.Shaper
|
||||
scale float32 // px-per-Dp for the current draw pass; set in Draw
|
||||
icons map[string]image.Image
|
||||
clicks map[string]clickReg
|
||||
Keys map[string]keyReg // Exported Keys map
|
||||
scrolls map[string]scrollReg
|
||||
gioEditors map[string]*widget.Editor // main-owned widget editors by element ID
|
||||
displayLineCount int // number of display lines from last drawWrappedText
|
||||
lastLineY Dp // last line baseline offset from text origin, in Dp (derived from GlyphLayout)
|
||||
glyphLayout GlyphLayout // captured per-glyph layout from last drawWrappedText
|
||||
theme Theme
|
||||
shp *text.Shaper
|
||||
scale float32 // px-per-Dp for the current draw pass; set in Draw
|
||||
icons map[string]image.Image
|
||||
clicks map[string]clickReg
|
||||
Keys map[string]keyReg // Exported Keys map
|
||||
scrolls map[string]scrollReg
|
||||
gioEditors map[string]*widget.Editor // main-owned widget editors by element ID
|
||||
displayLineCount int // number of display lines from last drawWrappedText
|
||||
lastLineY Dp // last line baseline offset from text origin, in Dp (derived from GlyphLayout)
|
||||
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.
|
||||
|
|
@ -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).
|
||||
// With Min==Max==0, clampSplit consumes zero scroll.
|
||||
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 {
|
||||
events = append(events, InputEvent{
|
||||
Handler: reg.handler,
|
||||
|
|
@ -400,7 +411,7 @@ func (r *Renderer) drawText(gtx layout.Context, str string, size unit.Sp, reg Re
|
|||
case AlignStart:
|
||||
drawX = reg.X
|
||||
case AlignCenter:
|
||||
drawX = reg.X + (reg.W - textW) / 2
|
||||
drawX = reg.X + (reg.W-textW)/2
|
||||
case AlignEnd:
|
||||
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)),
|
||||
MinWidth: 0,
|
||||
MaxWidth: int(r.toPx(wrapWidth)),
|
||||
MaxLines: 0, // unlimited - wrap at MaxWidth
|
||||
MaxLines: 0, // unlimited - wrap at MaxWidth
|
||||
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,
|
||||
}
|
||||
r.shp.LayoutString(params, str)
|
||||
|
|
@ -515,7 +526,7 @@ func (r *Renderer) drawWrappedText(gtx layout.Context, str string, reg Region, w
|
|||
var layout GlyphLayout
|
||||
layout.LineHeight = Dp(float32(lineHeightSp))
|
||||
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() {
|
||||
// Record layout data for this glyph.
|
||||
// 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]
|
||||
if g.Flags&text.FlagLineBreak != 0 {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Draw the cursor (thin vertical bar)
|
||||
cursorRegion := Region{
|
||||
X: cursorX,
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user