Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions internal/text/caret.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,10 @@ func (l *Layout) visualStops(li int) []caretStop {
if _, ok := slices.BinarySearch(l.boundaries, at); !ok {
continue
}
x := x0 + (x1-x0)*float32(k)/float32(g.Runes)
f := line.clusterFraction(g.Cluster, g.Runes, k)
x := x0 + (x1-x0)*f
if g.RTL {
x = x1 - (x1-x0)*float32(k)/float32(g.Runes)
x = x1 - (x1-x0)*f
}
affinity, arrival := Downstream, 0
if k == 0 {
Expand Down
30 changes: 30 additions & 0 deletions internal/text/caret_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,36 @@ func mixedLayout() *Layout {
return l
}

func TestTrailingZeroWidthClusterGeometry(t *testing.T) {
runes := []rune("x\u200b")
shaped := shapedLine{end: len(runes), runs: []shapedRun{{
end: len(runes), glyphs: []Glyph{{Advance: 10}},
}}}
var y float32
var buf []int
row := line(Params{}, runes, shaped, 0, false, lineMetrics{lineHeight: 20}, &y, nil, &buf, nil)
l := &Layout{Runes: runes, Lines: []Line{row}}
for _, index := range []int{1, 2} {
if x, _, _ := l.Caret(index); x != 10 {
t.Errorf("Caret(%d) = %v, want 10", index, x)
}
for _, affinity := range []Affinity{Upstream, Downstream} {
if x, _, _ := l.CaretAt(CaretPosition{index, affinity}); x != 10 {
t.Errorf("CaretAt(%d, %v) = %v, want 10", index, affinity, x)
}
}
}
if p := l.PositionAt(10, 10); p.Index != 1 {
t.Errorf("hit at visible end = %+v, want index 1", p)
}
if rs := l.Selection(0, 1); len(rs) != 1 || rs[0].X != 0 || rs[0].W != 10 {
t.Errorf("visible selection = %+v, want full glyph advance", rs)
}
if rs := l.Selection(1, 2); len(rs) != 0 {
t.Errorf("zero-width selection = %+v, want no area", rs)
}
}

func TestVisualCaretAffinityAndNavigation(t *testing.T) {
l := mixedLayout()
a, _, _ := l.CaretAt(CaretPosition{4, Upstream})
Expand Down
15 changes: 13 additions & 2 deletions internal/text/edit.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,16 @@ import (
// Rect is a rectangle in DIPs relative to the layout.
type Rect struct{ X, Y, W, H float32 }

// Trailing zero-width breaks may share a visible glyph's cluster, but
// must not take part of its advance as another ligature component.
func (line *Line) clusterFraction(start, runes, k int) float32 {
if end := line.zeroWidthStart; end >= 0 && start < end && start+runes > end {
runes = end - start
k = min(k, runes)
}
return float32(k) / float32(runes)
}

// lineCarets returns the x of every caret position of a line, from Start
// to End: inside a cluster of several runes (a ligature) the positions
// split the cluster evenly. It caches them in the line, so a layout's
Expand Down Expand Up @@ -39,7 +49,7 @@ func (line *Line) lineCarets() []float32 {
}
}
pos := func(c cluster, k int) float32 {
f := float32(k) / float32(c.runes)
f := line.clusterFraction(c.start, c.runes, k)
if c.rtl {
return c.x1 - (c.x1-c.x0)*f
}
Expand Down Expand Up @@ -182,7 +192,8 @@ func (l *Layout) AppendSelection(dst []Rect, start, end int, on bool) []Rect {
continue
}
width := x1 - x0
left, right := float32(a-first.Cluster)/float32(first.Runes), float32(z-first.Cluster)/float32(first.Runes)
left := line.clusterFraction(first.Cluster, first.Runes, a-first.Cluster)
right := line.clusterFraction(first.Cluster, first.Runes, z-first.Cluster)
if first.RTL {
left, right = 1-right, 1-left
}
Expand Down
8 changes: 6 additions & 2 deletions internal/text/layout.go
Original file line number Diff line number Diff line change
Expand Up @@ -371,7 +371,8 @@ type Line struct {
upstream []float32
// padding is the room the padding of spans' pieces takes in the line
// (Span.PaddingX).
padding float32
padding float32
zeroWidthStart int
}

// Glyph is a positioned glyph.
Expand Down Expand Up @@ -1440,7 +1441,10 @@ func glyphCount(sl shapedLine) int {
// offset of the layout, with its top at *y, and moves *y past it. Its
// glyphs go to room, which has room for those of sl; buf is a buffer.
func line(p Params, text []rune, sl shapedLine, offset int, rtl bool, m lineMetrics, y *float32, room []Glyph, buf *[]int, spans []Span) Line {
line := Line{Start: offset + sl.start, End: offset + sl.end, RTL: rtl, Ascent: m.ascent, Descent: m.descent}
line := Line{Start: offset + sl.start, End: offset + sl.end, RTL: rtl, Ascent: m.ascent, Descent: m.descent, zeroWidthStart: -1}
for end := sl.end; end > sl.start && text[end-1] == '\u200b'; end-- {
line.zeroWidthStart = offset + end - 1
}
if cap(room) > 0 {
line.Glyphs = room
}
Expand Down
Loading