diff --git a/Project/Src/Gui/TextAreaControl.cs b/Project/Src/Gui/TextAreaControl.cs
index 827aa14..d7241b6 100644
--- a/Project/Src/Gui/TextAreaControl.cs
+++ b/Project/Src/Gui/TextAreaControl.cs
@@ -34,6 +34,7 @@ public class TextAreaControl : Panel
private HRuler hRuler;
private int[] lineLengthCache;
+
private TextEditorControl motherTextEditorControl;
private Point scrollToPosOnNextUpdate;
@@ -280,23 +281,37 @@ static ScrollVisibilities GetScrollVisibilities(bool h, bool v)
lineLengthCache = new int[lastLineIndex + LineLengthCacheAdditionalSize];
var maxLength = 0;
- for (var lineIndex = firstLineIndex; lineIndex <= lastLineIndex; lineIndex++)
+
+ // The device context must not outlive this layout pass: a Graphics from
+ // Control.CreateGraphics() is bound to the control's current window handle and to a DC
+ // borrowed from the system cache, so keeping it across calls leaves a stale HDC which
+ // makes TextRenderer.MeasureText throw "Parameter is not valid".
+ Graphics measureGraphics = null;
+ try
{
- var lineSegment = Document.GetLineSegment(lineIndex);
- if (Document.FoldingManager.IsLineVisible(lineIndex))
+ for (var lineIndex = firstLineIndex; lineIndex <= lastLineIndex; lineIndex++)
{
- if (lineLengthCache[lineIndex] > 0)
- {
- maxLength = Math.Max(maxLength, lineLengthCache[lineIndex]);
- }
- else
+ var lineSegment = Document.GetLineSegment(lineIndex);
+ if (Document.FoldingManager.IsLineVisible(lineIndex))
{
- var visualLength = view.GetVisualColumnFast(lineSegment, lineSegment.Length);
- lineLengthCache[lineIndex] = Math.Max(1, visualLength);
- maxLength = Math.Max(maxLength, visualLength);
+ if (lineLengthCache[lineIndex] > 0)
+ {
+ maxLength = Math.Max(maxLength, lineLengthCache[lineIndex]);
+ }
+ else
+ {
+ measureGraphics ??= TextArea.CreateGraphics();
+ var visualLength = view.GetVisualWidthColumns(measureGraphics, lineSegment);
+ lineLengthCache[lineIndex] = Math.Max(1, visualLength);
+ maxLength = Math.Max(maxLength, visualLength);
+ }
}
}
}
+ finally
+ {
+ measureGraphics?.Dispose();
+ }
var hScrollBarVisible = HScrollBar.Value != 0 || maxLength > visibleColumnCount || (hScrollBarVisibleNow && VScrollBar.IsMouseDown);
vScrollBarVisible = vScrollBarVisible || (hScrollBarVisible && hScrollBarWouldCauseVScrollBar);
@@ -337,6 +352,10 @@ public void OptionsChanged()
{
TextArea.OptionsChanged();
+ // the cached line widths depend on the rendering options - font, tab size, highlighting -
+ // so any of them changing makes the cache stale
+ AdjustScrollBarsClearCache();
+
if (TextArea.TextEditorProperties.ShowHorizontalRuler)
{
if (hRuler == null)
diff --git a/Project/Src/Gui/TextView.cs b/Project/Src/Gui/TextView.cs
index 9e109c3..e796811 100644
--- a/Project/Src/Gui/TextView.cs
+++ b/Project/Src/Gui/TextView.cs
@@ -699,6 +699,64 @@ public int GetVisualColumnFast(LineSegment line, int logicalColumn)
return guessedColumn;
}
+ ///
+ /// Returns the width of expressed in units,
+ /// which is the unit the horizontal scrollbar operates in.
+ ///
+ ///
+ /// assumes that every character is exactly one column wide.
+ /// That assumption does not hold for glyphs which are wider than 'x' - CJK characters, emoji,
+ /// glyphs taken from a fallback font, or any proportional font - so a line containing them ends up
+ /// being reported as narrower than it is painted. This method measures the glyphs instead,
+ /// word by word with the fonts the highlighting assigns, mirroring .
+ ///
+ public int GetVisualWidthColumns(Graphics g, LineSegment line)
+ {
+ var fontContainer = TextEditorProperties.FontContainer;
+ var tabWidth = WideSpaceWidth*Document.TextEditorProperties.TabIndent;
+ var lineOffset = line.Offset;
+ var width = 0;
+ var measured = 0;
+
+ var words = line.Words;
+ if (words is not null)
+ {
+ for (var i = 0; i < words.Count; i++)
+ {
+ var word = words[i];
+ switch (word.Type)
+ {
+ case TextWordType.Space:
+ width += SpaceWidth;
+ break;
+ case TextWordType.Tab:
+ width = (width + MinTabWidth)/tabWidth*tabWidth + tabWidth;
+ break;
+ default:
+ width += MeasureStringWidth(g, word.Word, word.GetFont(fontContainer) ?? fontContainer.RegularFont);
+ break;
+ }
+
+ measured += word.Length;
+ }
+ }
+
+ // Whatever the highlighting has not covered - the whole line while it has not run yet, or a
+ // trailing remainder - still has to be accounted for, otherwise the line comes out too narrow
+ // again. There is no per-word font for it, so use the one WideSpaceWidth itself is derived from.
+ for (var i = measured; i < line.Length; ++i)
+ {
+ var ch = Document.GetCharAt(lineOffset + i);
+ if (ch == '\t')
+ width = (width + MinTabWidth)/tabWidth*tabWidth + tabWidth;
+ else
+ width += GetWidth(g, ch, fontContainer.RegularFont);
+ }
+
+ // round up so that a glyph which only partially reaches into the last column stays reachable
+ return (width + WideSpaceWidth - 1)/WideSpaceWidth;
+ }
+
///
/// returns line/column for a visual point position
///
diff --git a/Test/TextEditorControl/ScrollBarTests.cs b/Test/TextEditorControl/ScrollBarTests.cs
index e90d177..0c3c191 100644
--- a/Test/TextEditorControl/ScrollBarTests.cs
+++ b/Test/TextEditorControl/ScrollBarTests.cs
@@ -134,6 +134,65 @@ a VScrollBar
_textEditorControl.ActiveTextAreaControl.HScrollBar.Visible.Should().BeFalse();
}
+ [Test]
+ public void HScrollBar_should_be_shown_when_wide_glyphs_exceed_the_visible_width()
+ {
+ SetupForm(width: 300, height: 200);
+
+ var textAreaControl = _textEditorControl.ActiveTextAreaControl;
+ var view = textAreaControl.TextArea.TextView;
+ var font = view.TextEditorProperties.FontContainer.RegularFont;
+
+ int glyphWidth;
+ using (var g = textAreaControl.CreateGraphics())
+ {
+ glyphWidth = view.GetWidth(g, WideGlyph, font);
+ }
+
+ // the test is only meaningful if the glyph really is wider than one column
+ glyphWidth.Should().BeGreaterThan(view.WideSpaceWidth);
+
+ // exactly as many characters as there are visible columns: counting characters sees no
+ // overflow at all, while the glyphs need roughly twice that width to be painted
+ _textEditorControl.Text = new string(WideGlyph, view.DrawingPosition.Width / view.WideSpaceWidth);
+
+ Application.DoEvents();
+
+ textAreaControl.HScrollBar.Visible.Should().BeTrue();
+ }
+
+ [Test]
+ public void HScrollBar_should_allow_scrolling_to_the_end_of_a_line_of_wide_glyphs()
+ {
+ SetupForm(width: 300, height: 200);
+
+ _textEditorControl.Text = new string(WideGlyph, 100);
+
+ Application.DoEvents();
+
+ var textAreaControl = _textEditorControl.ActiveTextAreaControl;
+ var view = textAreaControl.TextArea.TextView;
+ var font = view.TextEditorProperties.FontContainer.RegularFont;
+
+ int lineWidth;
+ using (var g = textAreaControl.CreateGraphics())
+ {
+ view.GetWidth(g, WideGlyph, font).Should().BeGreaterThan(view.WideSpaceWidth);
+ lineWidth = view.GetWidth(g, _textEditorControl.Text, font);
+ }
+
+ // dragging the thumb all the way to the right stops at Maximum - LargeChange + 1;
+ // only setting HScrollBar.Value programmatically can go as far as Maximum
+ var hScrollBar = textAreaControl.HScrollBar;
+ var rightmostThumbValue = hScrollBar.Maximum - hScrollBar.LargeChange + 1;
+ var rightmostVisiblePixel = (rightmostThumbValue * view.WideSpaceWidth) + view.DrawingPosition.Width;
+
+ rightmostVisiblePixel.Should().BeGreaterThanOrEqualTo(lineWidth);
+ }
+
+ /// U+4E2D, a CJK ideograph: a single character which is painted about two columns wide.
+ private const char WideGlyph = (char)0x4E2D;
+
private void SetupForm(int width, int height)
{
_form = new Form