Skip to content

Commit

Permalink
Merge pull request #845 from fralx/binarySearch
Browse files Browse the repository at this point in the history
Binary search wrapping
  • Loading branch information
andydotxyz committed Apr 16, 2020
2 parents dd84082 + 3d18b8e commit c324a03
Show file tree
Hide file tree
Showing 2 changed files with 132 additions and 22 deletions.
73 changes: 51 additions & 22 deletions widget/text.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ func (t *textProvider) updateRowBounds() {
textWrap := t.presenter.textWrap()
textStyle := t.presenter.textStyle()
textSize := theme.TextSize()
maxWidth := t.Size().Width
maxWidth := t.Size().Width - 2*theme.Padding()

t.rowBounds = lineBounds(t.buffer, textWrap, maxWidth, func(text []rune) int {
return fyne.MeasureText(string(text), textSize, textStyle).Width
Expand Down Expand Up @@ -341,13 +341,57 @@ func splitLines(text []rune) [][2]int {
return append(lines, [2]int{low, length})
}

// binarySearch accepts a function that checks if the text width less the maximum width and the start and end rune index
// binarySearch returns the index of rune located as close to the maximum line width as possible
func binarySearch(lessMaxWidth func(int, int) bool, low int, maxHigh int) int {
if low >= maxHigh {
return low
}
if lessMaxWidth(low, maxHigh) {
return maxHigh
}
high := low
delta := maxHigh - low
for delta > 0 {
delta /= 2
if lessMaxWidth(low, high+delta) {
high += delta
}
}
for (high < maxHigh) && lessMaxWidth(low, high+1) {
high++
}
return high
}

// findSpaceIndex accepts a slice of runes and a fallback index
// findSpaceIndex returns the index of the last space in the text, or fallback if there are no spaces
func findSpaceIndex(text []rune, fallback int) int {
curIndex := fallback
for ; curIndex >= 0; curIndex-- {
if unicode.IsSpace(text[curIndex]) {
break
}
}
if curIndex < 0 {
return fallback
}
return curIndex
}

// lineBounds accepts a slice of runes, a wrapping mode, a maximum line width and a function to measure line width.
// lineBounds returns a slice containing the start and end indicies of each line with the given wrapping applied.
func lineBounds(text []rune, wrap fyne.TextWrap, maxWidth int, measurer func([]rune) int) [][2]int {

lines := splitLines(text)
if maxWidth == 0 || wrap == fyne.TextWrapOff {
if maxWidth <= 0 || wrap == fyne.TextWrapOff {
return lines
}

checker := func(low int, high int) bool {
return measurer(text[low:high]) <= maxWidth
}

var bounds [][2]int
for _, l := range lines {
low := l[0]
Expand All @@ -358,22 +402,16 @@ func lineBounds(text []rune, wrap fyne.TextWrap, maxWidth int, measurer func([]r
}
switch wrap {
case fyne.TextTruncate:
for {
if measurer(text[low:high]) <= maxWidth {
bounds = append(bounds, [2]int{low, high})
break
} else {
high--
}
}
high = binarySearch(checker, low, high)
bounds = append(bounds, [2]int{low, high})
case fyne.TextWrapBreak:
for low < high {
if measurer(text[low:high]) <= maxWidth {
bounds = append(bounds, [2]int{low, high})
low = high
high = l[1]
} else {
high--
high = binarySearch(checker, low, high)
}
}
case fyne.TextWrapWord:
Expand All @@ -387,17 +425,8 @@ func lineBounds(text []rune, wrap fyne.TextWrap, maxWidth int, measurer func([]r
low++
}
} else {
last := len(sub) - 1
for ; last >= 0; last-- {
if unicode.IsSpace(sub[last]) {
break
}
}
if last < 0 {
high--
} else {
high = low + last
}
last := low + len(sub) - 1
high = low + findSpaceIndex(sub, binarySearch(checker, low, last)-low)
}
}
}
Expand Down
81 changes: 81 additions & 0 deletions widget/text_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -706,3 +706,84 @@ func TestText_lineBounds_variable_char_width(t *testing.T) {
})
}
}

func TestText_binarySearch(t *testing.T) {
maxWidth := 50
textSize := 10
textStyle := fyne.TextStyle{}
measurer := func(text []rune) int {
return fyne.MeasureText(string(text), textSize, textStyle).Width
}
for name, tt := range map[string]struct {
text string
want int
}{
"IM": {
text: "iiiiiiiiiimmmmmmmmmm",
want: 12,
},
"Single_Line": {
text: "foobar foobar",
want: 9,
},
"WH": {
text: "wwwww hhhhhh",
want: 6,
},
"DS": {
text: "dddddd sssssss",
want: 8,
},
"DI": {
text: "dididi dididd",
want: 10,
},
"XW": {
text: "xwxwxwxw xwxw",
want: 7,
},
"W": {
text: "WWWWW",
want: 4,
},
"Empty": {
text: "",
want: 0,
},
} {
checker := func(low int, high int) bool {
return measurer([]rune(tt.text[low:high])) <= maxWidth
}
t.Run(name, func(t *testing.T) {
assert.Equal(t, tt.want, binarySearch(checker, 0, len(tt.text)))
})
}
}

func TestText_findSpaceIndex(t *testing.T) {
for name, tt := range map[string]struct {
text string
want int
}{
"no_space_fallback": {
text: "iiiiiiiiiimmmmmmmmmm",
want: 19,
},
"single_space": {
text: "foobar foobar",
want: 6,
},
"double_space": {
text: "ww wwww www",
want: 7,
},
"many_spaces": {
text: "ww wwww www wwwww",
want: 11,
},
} {
t.Run(name, func(t *testing.T) {
assert.Equal(t, tt.want, findSpaceIndex([]rune(tt.text), len(tt.text)-1))
})
}
}

0 comments on commit c324a03

Please sign in to comment.