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"
|
"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.
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user