Fix double-width characters in the last column drawing over the view's frame (#6106)

A double-width character that starts in the last column of a view is
drawn over the view's frame. One that is scrolled halfway out to the
left is skipped, and the column it reaches into isn't drawn at all. The
draw loop clips cells against the edges of the view as if every cell
were one column wide.

A terminal can't show half of a double-width character. So draw a blank
in the column that is on screen, in the character's colors, and let the
selection highlight cover that column like any other.
This commit is contained in:
Stefan Haller authored and GitHub committed 2026-10-10 13:40:20 +02:00
commit 8771f1fc5d
2 files changed
+58 -7

No files matched your search

+17 -7
View File
@@ -1878,13 +1878,7 @@ func (v *View) draw(isWindowFocused bool) {
var c cell
for x < maxX {
if x < 0 {
if cellIdx < len(vline.line) {
x += uniseg.StringWidth(vline.line[cellIdx].chr)
cellIdx++
continue
}
if x < 0 && cellIdx >= len(vline.line) {
// no more characters to write so we're only going to be printing empty cells
// past this point
x = gutterWidth
@@ -1897,6 +1891,22 @@ func (v *View) draw(isWindowFocused bool) {
c = vline.line[cellIdx]
}
// Skip the cells scrolled out to the left.
if x < 0 && x+c.width <= 0 {
x += c.width
cellIdx++
continue
}
// A terminal can't show half of a double-width character, so one that is cut
// off by the left or right edge shows as a blank in the column of it that is
// on screen.
if x < 0 || x+c.width > maxX {
c.chr = " "
c.width = 1
x = max(x, 0)
}
fgColor := c.fgColor
if fgColor == ColorDefault {
fgColor = v.FgColor
+41
View File
@@ -1166,6 +1166,47 @@ func TestResizingAWrappingViewKeepsItsPlaceInTheContent(t *testing.T) {
assert.Equal(t, []string{"another wrapping line"}, v.SelectedLines())
}
// A double-width character that starts in the last column doesn't fit, and shows as a
// blank there rather than reaching into the frame.
func TestDoubleWidthCharacterAtTheRightEdge(t *testing.T) {
WithSimulationScreen(t, 14, 6)
v := NewView("name", 0, 0, 11, 5, OutputNormal) // InnerWidth 10
v.writeString("abcdefghiプ\n")
v.draw(true)
// The frame puts view column 9 at screen column 10.
chr, _, width := Screen.Get(10, 1)
assert.Equal(t, " ", chr)
assert.Equal(t, 1, width)
}
// A double-width character that is scrolled halfway out to the left shows as a blank
// in the column it still reaches into, so that the column is drawn like every other.
func TestDoubleWidthCharacterCutOffAtTheLeftEdge(t *testing.T) {
WithSimulationScreen(t, 14, 6)
v := NewView("name", 0, 0, 11, 5, OutputNormal) // InnerWidth 10
v.Highlight = true
v.SelBgColor = ColorBlue
v.writeString("にプ\n")
v.SetOriginX(1)
v.draw(true)
// The frame puts view column 0 at screen column 1.
chr, style, _ := Screen.Get(1, 1)
assert.Equal(t, " ", chr)
assert.Equal(t,
true,
style.GetBackground() != tcell.ColorDefault,
"the selection covers the column")
chr, _, _ = Screen.Get(2, 1)
assert.Equal(t, "プ", chr)
}
// The inclusion gutter reserves columns at the left of every line, draws its marker
// on the marked lines only, and moves the content out of the way.
func TestInclusionGutter(t *testing.T) {