for WinForms
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1124 lines
48 KiB

// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <owner name="Daniel Grunwald" email="daniel@danielgrunwald.de"/>
// <version>$Revision$</version>
// </file>
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Windows.Forms;
using ICSharpCode.TextEditor.Document;
namespace ICSharpCode.TextEditor
{
/// <summary>
/// This class paints the textarea.
/// </summary>
public class TextView : AbstractMargin, IDisposable
{
private Font lastFont;
private int physicalColumn; // used for calculating physical column during paint
public TextView(TextArea textArea) : base(textArea)
{
Cursor = Cursors.IBeam;
OptionsChanged();
}
public Highlight Highlight { get; set; }
public int FirstPhysicalLine => textArea.VirtualTop.Y/FontHeight;
public int LineHeightRemainder => textArea.VirtualTop.Y%FontHeight;
/// <summary>Gets the first visible <b>logical</b> line.</summary>
public int FirstVisibleLine
{
get => textArea.Document.GetFirstLogicalLine(textArea.VirtualTop.Y/FontHeight);
set
{
if (FirstVisibleLine != value)
textArea.VirtualTop = new Point(textArea.VirtualTop.X, textArea.Document.GetVisibleLine(value)*FontHeight);
}
}
public int VisibleLineDrawingRemainder => textArea.VirtualTop.Y%FontHeight;
public int FontHeight { get; private set; }
public int VisibleLineCount => 1 + DrawingPosition.Height/FontHeight;
public int VisibleColumnCount => DrawingPosition.Width/WideSpaceWidth - 1;
/// <summary>
/// Gets the width of a space character.
/// This value can be quite small in some fonts - consider using WideSpaceWidth instead.
/// </summary>
public int SpaceWidth { get; private set; }
/// <summary>
/// Gets the width of a 'wide space' (=one quarter of a tab, if tab is set to 4 spaces).
/// On monospaced fonts, this is the same value as spaceWidth.
/// </summary>
public int WideSpaceWidth { get; private set; }
public void Dispose()
{
measureCache.Clear();
}
private static int GetFontHeight(Font font)
{
var max = Math.Max(
TextRenderer.MeasureText("_", font).Height,
(int)Math.Ceiling(font.GetHeight()));
return max + 1;
}
public void OptionsChanged()
{
lastFont = TextEditorProperties.FontContainer.RegularFont;
FontHeight = GetFontHeight(lastFont);
// use minimum width - in some fonts, space has no width but kerning is used instead
// -> DivideByZeroException
SpaceWidth = Math.Max(GetWidth(ch: ' ', lastFont), val2: 1);
// tab should have the width of 4*'x'
WideSpaceWidth = Math.Max(SpaceWidth, GetWidth(ch: 'x', lastFont));
}
#region Paint functions
public override void Paint(Graphics g, Rectangle rect)
{
if (rect.Width <= 0 || rect.Height <= 0)
return;
// Just to ensure that fontHeight and char widths are always correct...
if (lastFont != TextEditorProperties.FontContainer.RegularFont)
{
OptionsChanged();
textArea.Invalidate();
}
var horizontalDelta = textArea.VirtualTop.X;
if (horizontalDelta > 0)
g.SetClip(DrawingPosition);
for (var y = 0; y < (DrawingPosition.Height + VisibleLineDrawingRemainder)/FontHeight + 1; ++y)
{
var lineRectangle = new Rectangle(
DrawingPosition.X - horizontalDelta,
DrawingPosition.Top + y*FontHeight - VisibleLineDrawingRemainder,
DrawingPosition.Width + horizontalDelta,
FontHeight);
if (rect.IntersectsWith(lineRectangle))
{
// var fvl = textArea.Document.GetVisibleLine(FirstVisibleLine);
var currentLine = textArea.Document.GetFirstLogicalLine(textArea.Document.GetVisibleLine(FirstVisibleLine) + y);
PaintDocumentLine(g, currentLine, lineRectangle);
}
}
DrawMarkerDraw(g);
if (horizontalDelta > 0)
g.ResetClip();
textArea.Caret.PaintCaret(g);
}
private void PaintDocumentLine(Graphics g, int lineNumber, Rectangle lineRectangle)
{
Debug.Assert(lineNumber >= 0);
var bgColorBrush = GetBgColorBrush(lineNumber);
var backgroundBrush = textArea.Enabled ? bgColorBrush : SystemBrushes.InactiveBorder;
if (lineNumber >= textArea.Document.TotalNumberOfLines)
{
g.FillRectangle(backgroundBrush, lineRectangle);
if (TextEditorProperties.ShowInvalidLines)
DrawInvalidLineMarker(g, lineRectangle.Left, lineRectangle.Top);
if (TextEditorProperties.ShowVerticalRuler)
DrawVerticalRuler(g, lineRectangle);
return;
}
var physicalXPos = lineRectangle.X;
// there can't be a folding which starts in an above line and ends here, because the line is a new one,
// there must be a return before this line.
var column = 0;
physicalColumn = 0;
if (TextEditorProperties.EnableFolding)
while (true)
{
var starts = textArea.Document.FoldingManager.GetFoldedFoldingsWithStartAfterColumn(lineNumber, column - 1);
if (starts == null || starts.Count <= 0)
{
if (lineNumber < textArea.Document.TotalNumberOfLines)
physicalXPos = PaintLinePart(g, lineNumber, column, textArea.Document.GetLineSegment(lineNumber).Length, lineRectangle, physicalXPos);
break;
}
// search the first starting folding
var firstFolding = starts[index: 0];
foreach (var fm in starts)
if (fm.StartColumn < firstFolding.StartColumn)
firstFolding = fm;
starts.Clear();
physicalXPos = PaintLinePart(g, lineNumber, column, firstFolding.StartColumn, lineRectangle, physicalXPos);
column = firstFolding.EndColumn;
lineNumber = firstFolding.EndLine;
if (lineNumber >= textArea.Document.TotalNumberOfLines)
{
Debug.Assert(condition: false, "Folding ends after document end");
break;
}
var selectionRange2 = textArea.SelectionManager.GetSelectionAtLine(lineNumber);
var drawSelected = ColumnRange.WholeColumn.Equals(selectionRange2) || firstFolding.StartColumn >= selectionRange2.StartColumn && firstFolding.EndColumn <= selectionRange2.EndColumn;
physicalXPos = PaintFoldingText(g, lineNumber, physicalXPos, lineRectangle, firstFolding.FoldText, drawSelected);
}
else
physicalXPos = PaintLinePart(g, lineNumber, startColumn: 0, textArea.Document.GetLineSegment(lineNumber).Length, lineRectangle, physicalXPos);
if (lineNumber < textArea.Document.TotalNumberOfLines)
{
// Paint things after end of line
var selectionRange = textArea.SelectionManager.GetSelectionAtLine(lineNumber);
var currentLine = textArea.Document.GetLineSegment(lineNumber);
var selectionColor = textArea.Document.HighlightingStrategy.GetColorFor("Selection");
var selectionBeyondEOL = selectionRange.EndColumn > currentLine.Length || ColumnRange.WholeColumn.Equals(selectionRange);
if (TextEditorProperties.EolMarkerStyle != EolMarkerStyle.None)
{
physicalXPos += DrawEOLMarker(g, selectionBeyondEOL ? bgColorBrush : backgroundBrush, physicalXPos, lineRectangle.Y, currentLine.EolMarker);
}
else
{
if (selectionBeyondEOL)
{
g.FillRectangle(BrushRegistry.GetBrush(selectionColor.BackgroundColor), new RectangleF(physicalXPos, lineRectangle.Y, WideSpaceWidth, lineRectangle.Height));
physicalXPos += WideSpaceWidth;
}
}
var fillBrush = selectionBeyondEOL && TextEditorProperties.AllowCaretBeyondEOL ? bgColorBrush : backgroundBrush;
g.FillRectangle(
fillBrush,
new RectangleF(physicalXPos, lineRectangle.Y, lineRectangle.Width - physicalXPos + lineRectangle.X, lineRectangle.Height));
}
if (TextEditorProperties.ShowVerticalRuler)
DrawVerticalRuler(g, lineRectangle);
// bgColorBrush.Dispose();
}
private bool DrawLineMarkerAtLine(int lineNumber)
{
return lineNumber == textArea.Caret.Line && textArea.MotherTextAreaControl.TextEditorProperties.LineViewerStyle == LineViewerStyle.FullRow;
}
private Brush GetBgColorBrush(int lineNumber)
{
if (DrawLineMarkerAtLine(lineNumber))
{
var caretLine = textArea.Document.HighlightingStrategy.GetColorFor("CaretMarker");
return BrushRegistry.GetBrush(caretLine.Color);
}
var background = textArea.Document.HighlightingStrategy.GetColorFor("Default");
var bgColor = background.BackgroundColor;
return BrushRegistry.GetBrush(bgColor);
}
private const int additionalFoldTextSize = 1;
private int PaintFoldingText(Graphics g, int lineNumber, int physicalXPos, Rectangle lineRectangle, string text, bool drawSelected)
{
// TODO: get font and color from the highlighting file
var selectionColor = textArea.Document.HighlightingStrategy.GetColorFor("Selection");
var bgColorBrush = drawSelected ? BrushRegistry.GetBrush(selectionColor.BackgroundColor) : GetBgColorBrush(lineNumber);
var backgroundBrush = textArea.Enabled ? bgColorBrush : SystemBrushes.InactiveBorder;
var font = textArea.TextEditorProperties.FontContainer.RegularFont;
var wordWidth = MeasureStringWidth(g, text, font) + additionalFoldTextSize;
var rect = new Rectangle(physicalXPos, lineRectangle.Y, wordWidth, lineRectangle.Height - 1);
g.FillRectangle(backgroundBrush, rect);
physicalColumn += text.Length;
DrawString(
g,
text,
font,
drawSelected ? selectionColor.Color : Color.Gray,
rect.X + 1, rect.Y);
g.DrawRectangle(BrushRegistry.GetPen(drawSelected ? Color.DarkGray : Color.Gray), rect.X, rect.Y, rect.Width, rect.Height);
return physicalXPos + wordWidth + 1;
}
private struct MarkerToDraw
{
internal readonly TextMarker marker;
internal readonly RectangleF drawingRect;
public MarkerToDraw(TextMarker marker, RectangleF drawingRect)
{
this.marker = marker;
this.drawingRect = drawingRect;
}
}
private readonly List<MarkerToDraw> markersToDraw = new List<MarkerToDraw>();
private void DrawMarker(TextMarker marker, RectangleF drawingRect)
{
// draw markers later so they can overdraw the following text
markersToDraw.Add(new MarkerToDraw(marker, drawingRect));
}
private void DrawMarkerDraw(Graphics g)
{
foreach (var m in markersToDraw)
{
var marker = m.marker;
var drawingRect = m.drawingRect;
var drawYPos = drawingRect.Bottom - 1;
switch (marker.TextMarkerType)
{
case TextMarkerType.Underlined:
g.DrawLine(BrushRegistry.GetPen(marker.Color), drawingRect.X, drawYPos, drawingRect.Right, drawYPos);
break;
case TextMarkerType.WaveLine:
var reminder = (int)drawingRect.X%6;
for (float i = (int)drawingRect.X - reminder; i < drawingRect.Right; i += 6)
{
g.DrawLine(BrushRegistry.GetPen(marker.Color), i, drawYPos + 3 - 4, i + 3, drawYPos + 1 - 4);
if (i + 3 < drawingRect.Right)
g.DrawLine(BrushRegistry.GetPen(marker.Color), i + 3, drawYPos + 1 - 4, i + 6, drawYPos + 3 - 4);
}
break;
case TextMarkerType.SolidBlock:
g.FillRectangle(BrushRegistry.GetBrush(marker.Color), drawingRect);
break;
}
}
markersToDraw.Clear();
}
/// <summary>
/// Get the marker brush (for solid block markers) at a given position.
/// </summary>
/// <param name="offset">The offset.</param>
/// <param name="length">The length.</param>
/// <param name="markers">All markers that have been found.</param>
/// <returns>The Brush or null when no marker was found.</returns>
private static Brush GetMarkerBrush(IList<TextMarker> markers, ref Color foreColor)
{
foreach (var marker in markers)
if (marker.TextMarkerType == TextMarkerType.SolidBlock)
{
if (marker.OverrideForeColor)
foreColor = marker.ForeColor;
return BrushRegistry.GetBrush(marker.Color);
}
return null;
}
private int PaintLinePart(Graphics g, int lineNumber, int startColumn, int endColumn, Rectangle lineRectangle, int physicalXPos)
{
var drawLineMarker = DrawLineMarkerAtLine(lineNumber);
var backgroundBrush = textArea.Enabled ? GetBgColorBrush(lineNumber) : SystemBrushes.InactiveBorder;
var selectionColor = textArea.Document.HighlightingStrategy.GetColorFor("Selection");
var selectionRange = textArea.SelectionManager.GetSelectionAtLine(lineNumber);
var tabMarkerColor = textArea.Document.HighlightingStrategy.GetColorFor("TabMarkers");
var spaceMarkerColor = textArea.Document.HighlightingStrategy.GetColorFor("SpaceMarkers");
var currentLine = textArea.Document.GetLineSegment(lineNumber);
var selectionBackgroundBrush = BrushRegistry.GetBrush(selectionColor.BackgroundColor);
if (currentLine.Words == null)
return physicalXPos;
var currentWordOffset = 0; // we cannot use currentWord.Offset because it is not set on space words
TextWord nextCurrentWord = null;
var fontContainer = TextEditorProperties.FontContainer;
for (var wordIdx = 0; wordIdx < currentLine.Words.Count; wordIdx++)
{
var currentWord = currentLine.Words[wordIdx];
if (currentWordOffset < startColumn)
{
// TODO: maybe we need to split at startColumn when we support fold markers
// inside words
currentWordOffset += currentWord.Length;
continue;
}
repeatDrawCurrentWord:
//physicalXPos += 10; // leave room between drawn words - useful for debugging the drawing code
if (currentWordOffset >= endColumn || physicalXPos >= lineRectangle.Right)
break;
var currentWordEndOffset = currentWordOffset + currentWord.Length - 1;
var currentWordType = currentWord.Type;
Color wordForeColor;
if (currentWordType == TextWordType.Space)
wordForeColor = spaceMarkerColor.Color;
else if (currentWordType == TextWordType.Tab)
wordForeColor = tabMarkerColor.Color;
else
wordForeColor = currentWord.Color;
IList<TextMarker> markers = Document.MarkerStrategy.GetMarkers(currentLine.Offset + currentWordOffset, currentWord.Length);
var wordBackBrush = GetMarkerBrush(markers, ref wordForeColor);
// It is possible that we have to split the current word because a marker/the selection begins/ends inside it
if (currentWord.Length > 1)
{
var splitPos = int.MaxValue;
if (Highlight != null)
{
// split both before and after highlight
if (Highlight.OpenBrace.Y == lineNumber)
if (Highlight.OpenBrace.X >= currentWordOffset && Highlight.OpenBrace.X <= currentWordEndOffset)
splitPos = Math.Min(splitPos, Highlight.OpenBrace.X - currentWordOffset);
if (Highlight.CloseBrace.Y == lineNumber)
if (Highlight.CloseBrace.X >= currentWordOffset && Highlight.CloseBrace.X <= currentWordEndOffset)
splitPos = Math.Min(splitPos, Highlight.CloseBrace.X - currentWordOffset);
if (splitPos == 0)
splitPos = 1; // split after highlight
}
if (endColumn < currentWordEndOffset)
splitPos = Math.Min(splitPos, endColumn - currentWordOffset);
if (selectionRange.StartColumn > currentWordOffset && selectionRange.StartColumn <= currentWordEndOffset)
splitPos = Math.Min(splitPos, selectionRange.StartColumn - currentWordOffset);
else if (selectionRange.EndColumn > currentWordOffset && selectionRange.EndColumn <= currentWordEndOffset)
splitPos = Math.Min(splitPos, selectionRange.EndColumn - currentWordOffset);
foreach (var marker in markers)
{
var markerColumn = marker.Offset - currentLine.Offset;
var markerEndColumn = marker.EndOffset - currentLine.Offset + 1; // make end offset exclusive
if (markerColumn > currentWordOffset && markerColumn <= currentWordEndOffset)
splitPos = Math.Min(splitPos, markerColumn - currentWordOffset);
else if (markerEndColumn > currentWordOffset && markerEndColumn <= currentWordEndOffset)
splitPos = Math.Min(splitPos, markerEndColumn - currentWordOffset);
}
if (splitPos != int.MaxValue)
{
if (nextCurrentWord != null)
throw new ApplicationException("split part invalid: first part cannot be splitted further");
nextCurrentWord = TextWord.Split(ref currentWord, splitPos);
goto repeatDrawCurrentWord; // get markers for first word part
}
}
// get colors from selection status:
if (ColumnRange.WholeColumn.Equals(selectionRange) || selectionRange.StartColumn <= currentWordOffset
&& selectionRange.EndColumn > currentWordEndOffset)
{
// word is completely selected
wordBackBrush = selectionBackgroundBrush;
if (selectionColor.HasForeground)
wordForeColor = selectionColor.Color;
}
else if (drawLineMarker)
{
wordBackBrush = backgroundBrush;
}
if (wordBackBrush == null)
{
// use default background if no other background is set
if (currentWord.SyntaxColor != null && currentWord.SyntaxColor.HasBackground)
wordBackBrush = BrushRegistry.GetBrush(currentWord.SyntaxColor.BackgroundColor);
else
wordBackBrush = backgroundBrush;
}
RectangleF wordRectangle;
if (currentWord.Type == TextWordType.Space)
{
++physicalColumn;
wordRectangle = new RectangleF(physicalXPos, lineRectangle.Y, SpaceWidth, lineRectangle.Height);
g.FillRectangle(wordBackBrush, wordRectangle);
if (TextEditorProperties.ShowSpaces)
DrawSpaceMarker(g, wordForeColor, physicalXPos, lineRectangle.Y);
physicalXPos += SpaceWidth;
}
else if (currentWord.Type == TextWordType.Tab)
{
physicalColumn += TextEditorProperties.TabIndent;
physicalColumn = physicalColumn/TextEditorProperties.TabIndent*TextEditorProperties.TabIndent;
// go to next tabstop
var physicalTabEnd = (physicalXPos + MinTabWidth - lineRectangle.X)
/WideSpaceWidth/TextEditorProperties.TabIndent
*WideSpaceWidth*TextEditorProperties.TabIndent + lineRectangle.X;
physicalTabEnd += WideSpaceWidth*TextEditorProperties.TabIndent;
wordRectangle = new RectangleF(physicalXPos, lineRectangle.Y, physicalTabEnd - physicalXPos, lineRectangle.Height);
g.FillRectangle(wordBackBrush, wordRectangle);
if (TextEditorProperties.ShowTabs)
DrawTabMarker(g, wordForeColor, physicalXPos, lineRectangle.Y);
physicalXPos = physicalTabEnd;
}
else
{
var wordWidth = DrawDocumentWord(
g,
currentWord.Word,
new Point(physicalXPos, lineRectangle.Y),
currentWord.GetFont(fontContainer),
wordForeColor,
wordBackBrush);
wordRectangle = new RectangleF(physicalXPos, lineRectangle.Y, wordWidth, lineRectangle.Height);
physicalXPos += wordWidth;
}
foreach (var marker in markers)
if (marker.TextMarkerType != TextMarkerType.SolidBlock)
DrawMarker(marker, wordRectangle);
// draw bracket highlight
if (Highlight != null)
if (Highlight.OpenBrace.Y == lineNumber && Highlight.OpenBrace.X == currentWordOffset ||
Highlight.CloseBrace.Y == lineNumber && Highlight.CloseBrace.X == currentWordOffset)
DrawBracketHighlight(g, new Rectangle((int)wordRectangle.X, lineRectangle.Y, (int)wordRectangle.Width - 1, lineRectangle.Height - 1));
currentWordOffset += currentWord.Length;
if (nextCurrentWord != null)
{
currentWord = nextCurrentWord;
nextCurrentWord = null;
goto repeatDrawCurrentWord;
}
}
if (physicalXPos < lineRectangle.Right && endColumn >= currentLine.Length)
{
// draw markers at line end
IList<TextMarker> markers = Document.MarkerStrategy.GetMarkers(currentLine.Offset + currentLine.Length);
foreach (var marker in markers)
if (marker.TextMarkerType != TextMarkerType.SolidBlock)
DrawMarker(marker, new RectangleF(physicalXPos, lineRectangle.Y, WideSpaceWidth, lineRectangle.Height));
}
return physicalXPos;
}
private int DrawDocumentWord(Graphics g, string word, Point position, Font font, Color foreColor, Brush backBrush)
{
if (string.IsNullOrEmpty(word))
return 0;
if (word.Length > MaximumWordLength)
{
var width = 0;
for (var i = 0; i < word.Length; i += MaximumWordLength)
{
var pos = position;
pos.X += width;
if (i + MaximumWordLength < word.Length)
width += DrawDocumentWord(g, word.Substring(i, MaximumWordLength), pos, font, foreColor, backBrush);
else
width += DrawDocumentWord(g, word.Substring(i, word.Length - i), pos, font, foreColor, backBrush);
}
return width;
}
var wordWidth = MeasureStringWidth(g, word, font);
//num = ++num % 3;
g.FillRectangle(
backBrush, //num == 0 ? Brushes.LightBlue : num == 1 ? Brushes.LightGreen : Brushes.Yellow,
new RectangleF(position.X, position.Y, wordWidth + 1, FontHeight));
DrawString(
g,
word,
font,
foreColor,
position.X,
position.Y);
return wordWidth;
}
private struct WordFontPair
{
private readonly string word;
private readonly Font font;
public WordFontPair(string word, Font font)
{
this.word = word;
this.font = font;
}
public override bool Equals(object obj)
{
var myWordFontPair = (WordFontPair)obj;
if (!word.Equals(myWordFontPair.word)) return false;
return font.Equals(myWordFontPair.font);
}
public override int GetHashCode()
{
return word.GetHashCode() ^ font.GetHashCode();
}
}
private readonly Dictionary<WordFontPair, int> measureCache = new Dictionary<WordFontPair, int>();
// split words after 1000 characters. Fixes GDI+ crash on very longs words, for example
// a 100 KB Base64-file without any line breaks.
private const int MaximumWordLength = 1000;
private const int MaximumCacheSize = 2000;
private int MeasureStringWidth(Graphics g, string word, Font font)
{
if (string.IsNullOrEmpty(word))
return 0;
int width;
if (word.Length > MaximumWordLength)
{
width = 0;
for (var i = 0; i < word.Length; i += MaximumWordLength)
if (i + MaximumWordLength < word.Length)
width += MeasureStringWidth(g, word.Substring(i, MaximumWordLength), font);
else
width += MeasureStringWidth(g, word.Substring(i, word.Length - i), font);
return width;
}
if (measureCache.TryGetValue(new WordFontPair(word, font), out width))
return width;
if (measureCache.Count > MaximumCacheSize)
measureCache.Clear();
// This code here provides better results than MeasureString!
// Example line that is measured wrong:
// txt.GetPositionFromCharIndex(txt.SelectionStart)
// (Verdana 10, highlighting makes GetP... bold) -> note the space between 'x' and '('
// this also fixes "jumping" characters when selecting in non-monospace fonts
// [...]
// Replaced GDI+ measurement with GDI measurement: faster and even more exact
width = TextRenderer.MeasureText(g, word, font, new Size(short.MaxValue, short.MaxValue), textFormatFlags).Width;
measureCache.Add(new WordFontPair(word, font), width);
return width;
}
// Important: Some flags combinations work on WinXP, but not on Win2000.
// Make sure to test changes here on all operating systems.
private const TextFormatFlags textFormatFlags =
TextFormatFlags.NoPadding | TextFormatFlags.NoPrefix | TextFormatFlags.PreserveGraphicsClipping | TextFormatFlags.ExpandTabs;
#endregion
#region Conversion Functions
private readonly Dictionary<Font, Dictionary<char, int>> fontBoundCharWidth = new Dictionary<Font, Dictionary<char, int>>();
public int GetWidth(char ch, Font font)
{
if (!fontBoundCharWidth.ContainsKey(font))
fontBoundCharWidth.Add(font, new Dictionary<char, int>());
if (!fontBoundCharWidth[font].ContainsKey(ch))
using (var g = textArea.CreateGraphics())
{
return GetWidth(g, ch, font);
}
return fontBoundCharWidth[font][ch];
}
public int GetWidth(Graphics g, char ch, Font font)
{
if (!fontBoundCharWidth.ContainsKey(font))
fontBoundCharWidth.Add(font, new Dictionary<char, int>());
if (!fontBoundCharWidth[font].ContainsKey(ch))
fontBoundCharWidth[font].Add(ch, MeasureStringWidth(g, ch.ToString(), font));
return fontBoundCharWidth[font][ch];
}
public int GetWidth(Graphics g, string text, Font font)
{
int width = 0;
foreach (char ch in text)
{
width += GetWidth(g, ch, font);
}
return width;
}
public int GetVisualColumn(int logicalLine, int logicalColumn)
{
var column = 0;
using (var g = textArea.CreateGraphics())
{
CountColumns(ref column, start: 0, logicalColumn, logicalLine, g);
}
return column;
}
public int GetVisualColumnFast(LineSegment line, int logicalColumn)
{
var lineOffset = line.Offset;
var tabIndent = Document.TextEditorProperties.TabIndent;
var guessedColumn = 0;
for (var i = 0; i < logicalColumn; ++i)
{
char ch;
if (i >= line.Length)
ch = ' ';
else
ch = Document.GetCharAt(lineOffset + i);
switch (ch)
{
case '\t':
guessedColumn += tabIndent;
guessedColumn = guessedColumn/tabIndent*tabIndent;
break;
default:
++guessedColumn;
break;
}
}
return guessedColumn;
}
/// <summary>
/// returns line/column for a visual point position
/// </summary>
public TextLocation GetLogicalPosition(Point mousePosition)
{
return GetLogicalColumn(GetLogicalLine(mousePosition.Y), mousePosition.X, out var dummy);
}
/// <summary>
/// returns line/column for a visual point position
/// </summary>
public TextLocation GetLogicalPosition(int visualPosX, int visualPosY)
{
return GetLogicalColumn(GetLogicalLine(visualPosY), visualPosX, out var dummy);
}
/// <summary>
/// returns line/column for a visual point position
/// </summary>
public FoldMarker GetFoldMarkerFromPosition(int visualPosX, int visualPosY)
{
GetLogicalColumn(GetLogicalLine(visualPosY), visualPosX, out var foldMarker);
return foldMarker;
}
/// <summary>
/// returns logical line number for a visual point
/// </summary>
public int GetLogicalLine(int visualPosY)
{
var clickedVisualLine = Math.Max(val1: 0, (visualPosY + textArea.VirtualTop.Y)/FontHeight);
return Document.GetFirstLogicalLine(clickedVisualLine);
}
internal TextLocation GetLogicalColumn(int lineNumber, int visualPosX, out FoldMarker inFoldMarker)
{
visualPosX += textArea.VirtualTop.X;
inFoldMarker = null;
if (lineNumber >= Document.TotalNumberOfLines)
return new TextLocation(visualPosX/WideSpaceWidth, lineNumber);
if (visualPosX <= 0)
return new TextLocation(column: 0, lineNumber);
var start = 0; // column
var posX = 0; // visual position
int result;
using (var g = textArea.CreateGraphics())
{
// call GetLogicalColumnInternal to skip over text,
// then skip over fold markers
// and repeat as necessary.
// The loop terminates once the correct logical column is reached in
// GetLogicalColumnInternal or inside a fold marker.
while (true)
{
var line = Document.GetLineSegment(lineNumber);
var nextFolding = FindNextFoldedFoldingOnLineAfterColumn(lineNumber, start - 1);
var end = nextFolding?.StartColumn ?? int.MaxValue;
result = GetLogicalColumnInternal(g, line, start, end, ref posX, visualPosX);
// break when GetLogicalColumnInternal found the result column
if (result < end)
break;
// reached fold marker
lineNumber = nextFolding.EndLine;
start = nextFolding.EndColumn;
var newPosX = posX + 1 + MeasureStringWidth(g, nextFolding.FoldText, TextEditorProperties.FontContainer.RegularFont);
if (newPosX >= visualPosX)
{
inFoldMarker = nextFolding;
if (IsNearerToAThanB(visualPosX, posX, newPosX))
return new TextLocation(nextFolding.StartColumn, nextFolding.StartLine);
return new TextLocation(nextFolding.EndColumn, nextFolding.EndLine);
}
posX = newPosX;
}
}
return new TextLocation(result, lineNumber);
}
private int GetLogicalColumnInternal(Graphics g, LineSegment line, int start, int end, ref int drawingPos, int targetVisualPosX)
{
if (start == end)
return end;
Debug.Assert(start < end);
Debug.Assert(drawingPos < targetVisualPosX);
var tabIndent = Document.TextEditorProperties.TabIndent;
/*float spaceWidth = SpaceWidth;
float drawingPos = 0;
LineSegment currentLine = Document.GetLineSegment(logicalLine);
List<TextWord> words = currentLine.Words;
if (words == null) return 0;
int wordCount = words.Count;
int wordOffset = 0;
FontContainer fontContainer = TextEditorProperties.FontContainer;
*/
var fontContainer = TextEditorProperties.FontContainer;
var words = line.Words;
if (words == null) return 0;
var wordOffset = 0;
for (var i = 0; i < words.Count; i++)
{
var word = words[i];
if (wordOffset >= end)
return wordOffset;
if (wordOffset + word.Length >= start)
{
int newDrawingPos;
switch (word.Type)
{
case TextWordType.Space:
newDrawingPos = drawingPos + SpaceWidth;
if (newDrawingPos >= targetVisualPosX)
return IsNearerToAThanB(targetVisualPosX, drawingPos, newDrawingPos) ? wordOffset : wordOffset + 1;
break;
case TextWordType.Tab:
// go to next tab position
drawingPos = (drawingPos + MinTabWidth)/tabIndent/WideSpaceWidth*tabIndent*WideSpaceWidth;
newDrawingPos = drawingPos + tabIndent*WideSpaceWidth;
if (newDrawingPos >= targetVisualPosX)
return IsNearerToAThanB(targetVisualPosX, drawingPos, newDrawingPos) ? wordOffset : wordOffset + 1;
break;
case TextWordType.Word:
var wordStart = Math.Max(wordOffset, start);
var wordLength = Math.Min(wordOffset + word.Length, end) - wordStart;
var text = Document.GetText(line.Offset + wordStart, wordLength);
var font = word.GetFont(fontContainer) ?? fontContainer.RegularFont;
newDrawingPos = drawingPos + MeasureStringWidth(g, text, font);
if (newDrawingPos >= targetVisualPosX)
{
for (var j = 0; j < text.Length; j++)
{
newDrawingPos = drawingPos + MeasureStringWidth(g, text[j].ToString(), font);
if (newDrawingPos >= targetVisualPosX)
{
if (IsNearerToAThanB(targetVisualPosX, drawingPos, newDrawingPos))
return wordStart + j;
return wordStart + j + 1;
}
drawingPos = newDrawingPos;
}
return wordStart + text.Length;
}
break;
default:
throw new NotSupportedException();
}
drawingPos = newDrawingPos;
}
wordOffset += word.Length;
}
return wordOffset;
}
private static bool IsNearerToAThanB(int num, int a, int b)
{
return Math.Abs(a - num) < Math.Abs(b - num);
}
private FoldMarker FindNextFoldedFoldingOnLineAfterColumn(int lineNumber, int column)
{
var list = Document.FoldingManager.GetFoldedFoldingsWithStartAfterColumn(lineNumber, column);
if (list.Count != 0)
return list[index: 0];
return null;
}
private const int MinTabWidth = 4;
private float CountColumns(ref int column, int start, int end, int logicalLine, Graphics g)
{
if (start > end) throw new ArgumentException("start > end");
if (start == end) return 0;
float spaceWidth = SpaceWidth;
float drawingPos = 0;
var tabIndent = Document.TextEditorProperties.TabIndent;
var currentLine = Document.GetLineSegment(logicalLine);
var words = currentLine.Words;
if (words == null) return 0;
var wordCount = words.Count;
var wordOffset = 0;
var fontContainer = TextEditorProperties.FontContainer;
for (var i = 0; i < wordCount; i++)
{
var word = words[i];
if (wordOffset >= end)
break;
if (wordOffset + word.Length >= start)
switch (word.Type)
{
case TextWordType.Space:
drawingPos += spaceWidth;
break;
case TextWordType.Tab:
// go to next tab position
drawingPos = (int)((drawingPos + MinTabWidth)/tabIndent/WideSpaceWidth)*tabIndent*WideSpaceWidth;
drawingPos += tabIndent*WideSpaceWidth;
break;
case TextWordType.Word:
var wordStart = Math.Max(wordOffset, start);
var wordLength = Math.Min(wordOffset + word.Length, end) - wordStart;
var text = Document.GetText(currentLine.Offset + wordStart, wordLength);
drawingPos += MeasureStringWidth(g, text, word.GetFont(fontContainer) ?? fontContainer.RegularFont);
break;
}
wordOffset += word.Length;
}
for (var j = currentLine.Length; j < end; j++)
drawingPos += WideSpaceWidth;
// add one pixel in column calculation to account for floating point calculation errors
column += (int)((drawingPos + 1)/WideSpaceWidth);
/* OLD Code (does not work for fonts like Verdana)
for (int j = start; j < end; ++j) {
char ch;
if (j >= line.Length) {
ch = ' ';
} else {
ch = Document.GetCharAt(line.Offset + j);
}
switch (ch) {
case '\t':
int oldColumn = column;
column += tabIndent;
column = (column / tabIndent) * tabIndent;
drawingPos += (column - oldColumn) * spaceWidth;
break;
default:
++column;
TextWord word = line.GetWord(j);
if (word == null || word.Font == null) {
drawingPos += GetWidth(ch, TextEditorProperties.Font);
} else {
drawingPos += GetWidth(ch, word.Font);
}
break;
}
}
//*/
return drawingPos;
}
public int GetDrawingXPos(int logicalLine, int logicalColumn)
{
var foldings = Document.FoldingManager.GetTopLevelFoldedFoldings();
int i;
FoldMarker f = null;
// search the last folding that's interesting
for (i = foldings.Count - 1; i >= 0; --i)
{
f = foldings[i];
if (f.StartLine < logicalLine || f.StartLine == logicalLine && f.StartColumn < logicalColumn)
break;
var f2 = foldings[i/2];
if (f2.StartLine > logicalLine || f2.StartLine == logicalLine && f2.StartColumn >= logicalColumn)
i /= 2;
}
var column = 0;
// var tabIndent = Document.TextEditorProperties.TabIndent;
float drawingPos;
var g = textArea.CreateGraphics();
// if no folding is interesting
if (f == null || !(f.StartLine < logicalLine || f.StartLine == logicalLine && f.StartColumn < logicalColumn))
{
drawingPos = CountColumns(ref column, start: 0, logicalColumn, logicalLine, g);
return (int)(drawingPos - textArea.VirtualTop.X);
}
// if logicalLine/logicalColumn is in folding
if (f.EndLine > logicalLine || f.EndLine == logicalLine && f.EndColumn > logicalColumn)
{
logicalColumn = f.StartColumn;
logicalLine = f.StartLine;
--i;
}
var lastFolding = i;
// search backwards until a new visible line is reached
for (; i >= 0; --i)
{
f = foldings[i];
if (f.EndLine < logicalLine)
break;
}
var firstFolding = i + 1;
if (lastFolding < firstFolding)
{
drawingPos = CountColumns(ref column, start: 0, logicalColumn, logicalLine, g);
return (int)(drawingPos - textArea.VirtualTop.X);
}
var foldEnd = 0;
drawingPos = 0;
for (i = firstFolding; i <= lastFolding; ++i)
{
f = foldings[i];
drawingPos += CountColumns(ref column, foldEnd, f.StartColumn, f.StartLine, g);
foldEnd = f.EndColumn;
column += f.FoldText.Length;
drawingPos += additionalFoldTextSize;
drawingPos += MeasureStringWidth(g, f.FoldText, TextEditorProperties.FontContainer.RegularFont);
}
drawingPos += CountColumns(ref column, foldEnd, logicalColumn, logicalLine, g);
g.Dispose();
return (int)(drawingPos - textArea.VirtualTop.X);
}
#endregion
#region DrawHelper functions
private static void DrawBracketHighlight(Graphics g, Rectangle rect)
{
g.FillRectangle(BrushRegistry.GetBrush(Color.FromArgb(alpha: 50, red: 0, green: 0, blue: 255)), rect);
g.DrawRectangle(Pens.Blue, rect);
}
private static void DrawString(Graphics g, string text, Font font, Color color, int x, int y)
{
TextRenderer.DrawText(g, text, font, new Point(x, y), color, textFormatFlags);
}
private void DrawInvalidLineMarker(Graphics g, int x, int y)
{
var invalidLinesColor = textArea.Document.HighlightingStrategy.GetColorFor("InvalidLines");
DrawString(g, "~", invalidLinesColor.GetFont(TextEditorProperties.FontContainer), invalidLinesColor.Color, x, y);
}
private void DrawSpaceMarker(Graphics g, Color color, int x, int y)
{
var spaceMarkerColor = textArea.Document.HighlightingStrategy.GetColorFor("SpaceMarkers");
DrawString(g, "\u00B7", spaceMarkerColor.GetFont(TextEditorProperties.FontContainer), color, x, y);
}
private void DrawTabMarker(Graphics g, Color color, int x, int y)
{
var tabMarkerColor = textArea.Document.HighlightingStrategy.GetColorFor("TabMarkers");
DrawString(g, "\u00BB", tabMarkerColor.GetFont(TextEditorProperties.FontContainer), color, x, y);
}
private int DrawEOLMarker(Graphics g, Brush backBrush, int x, int y, EolMarker eolMarker)
{
string? representation;
switch (eolMarker)
{
case EolMarker.Cr:
representation = TextEditorProperties.EolMarkerStyle == EolMarkerStyle.Glyph ? "«" : @"\r";
break;
case EolMarker.CrLf:
representation = TextEditorProperties.EolMarkerStyle == EolMarkerStyle.Glyph ? "¤" : @"\r\n";
break;
case EolMarker.Lf:
representation = TextEditorProperties.EolMarkerStyle == EolMarkerStyle.Glyph ? "¶" : @"\n";
break;
case EolMarker.None:
default:
return 0;
}
HighlightColor eolMarkerColor = textArea.Document.HighlightingStrategy.GetColorFor("EOLMarkers");
Font font = eolMarkerColor.GetFont(TextEditorProperties.FontContainer);
int eolMarkerWidth = GetWidth(g, representation, font);
g.FillRectangle(
backBrush,
new RectangleF(x, y, eolMarkerWidth, FontHeight));
DrawString(g, representation, font, eolMarkerColor.Color, x, y);
return eolMarkerWidth;
}
private void DrawVerticalRuler(Graphics g, Rectangle lineRectangle)
{
var xpos = WideSpaceWidth*TextEditorProperties.VerticalRulerRow - textArea.VirtualTop.X;
if (xpos <= 0)
return;
var vRulerColor = textArea.Document.HighlightingStrategy.GetColorFor("VRuler");
g.DrawLine(
BrushRegistry.GetPen(vRulerColor.Color),
drawingPosition.Left + xpos,
lineRectangle.Top,
drawingPosition.Left + xpos,
lineRectangle.Bottom);
}
#endregion
}
}