Skip to content

fix(TextAreaControl): measure glyph width for the horizontal scroll extent#63

Open
DraftingDreamer wants to merge 1 commit into
gitextensions:masterfrom
DraftingDreamer:fix/hscrollbar-measure-glyph-width
Open

fix(TextAreaControl): measure glyph width for the horizontal scroll extent#63
DraftingDreamer wants to merge 1 commit into
gitextensions:masterfrom
DraftingDreamer:fix/hscrollbar-measure-glyph-width

Conversation

@DraftingDreamer

Copy link
Copy Markdown

Fixes gitextensions/gitextensions#13200

Proposed changes

  • TextView.GetVisualWidthColumns() (new) returns the painted width of a line in WideSpaceWidth units, measuring the glyphs through the existing per-character width cache (GetWidth(Graphics, char, Font)) and mirroring the tab-stop arithmetic of PaintLinePart.
  • TextAreaControl.UpdateLayout() uses it instead of GetVisualColumnFast(), which counted characters and so underestimated every glyph wider than x (CJK, emoji, fallback-font glyphs). The Graphics is created lazily, so a warm lineLengthCache still measures nothing.
  • TextAreaControl.OptionsChanged() clears lineLengthCache: the cached widths are now font-dependent.

Screenshots

All shots are Git Extensions built from master at 200% scaling (192 dpi), on a throwaway repository containing CJK source lines, cropped to the diff pane.

Before

The scrollbar has been dragged fully right and the right arrow clicked repeatedly afterwards - the view does not move any further, yet the string literal is cut off mid-sentence:

thumb at maximum, line still clipped

A file whose longest line has fewer characters than the viewer has columns, but whose glyphs need about twice that width: the line is clipped and there is no scrollbar at all.

no scrollbar although the line is clipped

The Commit dialog, same story:

commit dialog without a scrollbar

After

Dragging fully right now reaches the end of the line, closing quote and semicolon included:

line fully reachable

Same file and same scroll position as the second "before" shot - the scrollbar is now there:

scrollbar now shown

commit dialog with a scrollbar

Test methodology

  • Two new tests in ScrollBarTests, both failing without the change:

    • HScrollBar_should_be_shown_when_wide_glyphs_exceed_the_visible_width
    • HScrollBar_should_allow_scrolling_to_the_end_of_a_line_of_wide_glyphs

    They derive their thresholds from the control at run time (glyph width, WideSpaceWidth, DrawingPosition) rather than hard-coding pixels, and they assert the precondition that the glyph really is wider than one column, so on a machine without CJK fallback fonts they fail loudly instead of silently passing.

  • Whole suite: 54/54 pass (dotnet test --test-adapter-path:.).

  • Pure ASCII is provably unchanged: with Consolas 10, TextRenderer.MeasureText reports exactly 100 x WideSpaceWidth for 100 ASCII characters, so the computed extent is identical to before.

  • Verified end to end in Git Extensions itself - Diff tab and Commit dialog, before and after builds - see the screenshots above.

Notes for the reviewer

  • After this change GetVisualColumnFast() has no callers left, here or in gitextensions. I left it in place because it is public API - happy to remove it in this PR if you would rather.
  • Cost is one dictionary lookup per character, and only for lines that miss lineLengthCache (the visible lines right after a document change or resize).

Test environment(s)

  • GIT 2.54.0.windows.1
  • Windows NT 10.0.26200.0, .NET SDK 10.0.302
  • 200% scaling (192 dpi) - the configuration the issue was reported on

Merge strategy

I agree that the maintainer squash merge this PR (if the commit message is clear).


✒️ I contribute this code under The Developer Certificate of Origin.

…xtent

The horizontal scrollbar works in WideSpaceWidth units (the width of 'x'),
but its extent was derived from GetVisualColumnFast, which counts characters
and assumes every one of them is exactly one column wide.

Any glyph wider than 'x' - CJK, emoji, glyphs coming from a fallback font, or
any proportional font - therefore makes the scroll range shorter than the line
actually painted. Depending on how far off it is, either the scrollbar is not
shown at all, or its thumb cannot be dragged to the end of the line. Setting
HScrollBar.Value from code (caret movement, selection drag) uses the accurate
GetVisualColumn and can reach Maximum, so those paths scroll further right than
the thumb allows - which is how the mismatch becomes visible to the user.

Replace the character count with GetVisualWidthColumns, which measures the
glyphs using the existing per-character width cache and mirrors the tab
handling of PaintLinePart. The cached line widths now depend on the font, so
they are invalidated on OptionsChanged.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes horizontal scrollbar sizing in TextAreaControl by measuring the painted glyph widths (rather than counting characters) so wide glyphs (e.g., CJK) properly contribute to horizontal scroll extent, addressing gitextensions/gitextensions#13200.

Changes:

  • Add TextView.GetVisualWidthColumns() to compute a line’s visual width in WideSpaceWidth units using glyph measurements and tab-stop arithmetic.
  • Update TextAreaControl.UpdateLayout() to use the measured visual width (with lazy Graphics creation) and clear cached widths on option changes.
  • Add two scrollbar regression tests covering wide-glyph overflow and rightmost reachability.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
Test/TextEditorControl/ScrollBarTests.cs Adds regression tests to ensure wide glyphs trigger and correctly size horizontal scrolling.
Project/Src/Gui/TextView.cs Introduces a new API to compute measured visual line width in scrollbar column units.
Project/Src/Gui/TextAreaControl.cs Switches scrollbar extent calculation to measured widths and invalidates cached widths when editor options change.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +713 to +731
public int GetVisualWidthColumns(Graphics g, LineSegment line)
{
var font = TextEditorProperties.FontContainer.RegularFont;
var tabWidth = WideSpaceWidth*Document.TextEditorProperties.TabIndent;
var lineOffset = line.Offset;
var width = 0;

for (var i = 0; i < line.Length; ++i)
{
var ch = Document.GetCharAt(lineOffset + i);
if (ch == '\t')
width = (width + MinTabWidth)/tabWidth*tabWidth + tabWidth;
else
width += GetWidth(g, ch, font);
}

// round up so that a glyph which only partially reaches into the last column stays reachable
return (width + WideSpaceWidth - 1)/WideSpaceWidth;
}
{
TextArea.OptionsChanged();

// the cached line widths are measured with the current font, so they are stale after a font change
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Diff: horizontal scrollbar does not cover lines containing wide characters (CJK, emoji)

2 participants