Skip to content
Open
7 changes: 5 additions & 2 deletions src/AvaloniaEdit/Folding/FoldingElementGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ public override VisualLineElement ConstructElement(int offset)
var properties = new VisualLineElementTextRunProperties(CurrentContext.GlobalTextRunProperties);
properties.SetForegroundBrush(TextBrush);
var text = TextFormatter.Current.FormatLine(new SimpleTextSource(title, properties), 0, double.MaxValue, new GenericTextParagraphProperties(properties));
return new FoldingLineElement(foldingSection, text, foldedUntil - offset, TextBrush);
return new FoldingLineElement(foldingSection, text, foldedUntil - offset, TextBrush, title.Length);
} else {
return null;
}
Expand All @@ -167,12 +167,15 @@ private sealed class FoldingLineElement : FormattedTextElement
private readonly FoldingSection _fs;
private readonly IBrush _textBrush;

public FoldingLineElement(FoldingSection fs, TextLine text, int documentLength, IBrush textBrush) : base(text, documentLength)
public FoldingLineElement(FoldingSection fs, TextLine text, int documentLength, IBrush textBrush, int displayColumnLength) : base(text, documentLength)
{
_fs = fs;
_textBrush = textBrush;
DisplayColumnLength = displayColumnLength;
}

public override int DisplayColumnLength { get; }

public override TextRun CreateTextRun(int startVisualColumn, ITextRunConstructionContext context)
{
return new FoldingLineTextRun(this, this.TextRunProperties, _textBrush);
Expand Down
104 changes: 18 additions & 86 deletions src/AvaloniaEdit/Rendering/SingleCharacterElementGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.

using System;
using System.Diagnostics.CodeAnalysis;
using Avalonia;
using Avalonia.Media;
Expand All @@ -30,7 +29,7 @@ namespace AvaloniaEdit.Rendering
// This class is internal because it does not need to be accessed by the user - it can be configured using TextEditorOptions.

/// <summary>
/// Element generator that displays · for spaces and » for tabs and a box for control characters.
/// Element generator that displays · for spaces and a box for control characters.
/// </summary>
/// <remarks>
/// This element generator is present in every TextView by default; the enabled features can be configured using the
Expand All @@ -44,11 +43,6 @@ internal sealed class SingleCharacterElementGenerator : VisualLineElementGenerat
/// </summary>
public bool ShowSpaces { get; set; }

/// <summary>
/// Gets/Sets whether to show » for tabs.
/// </summary>
public bool ShowTabs { get; set; }

/// <summary>
/// Gets/Sets whether to show a box with the hex code for control characters.
/// </summary>
Expand All @@ -60,17 +54,23 @@ internal sealed class SingleCharacterElementGenerator : VisualLineElementGenerat
public SingleCharacterElementGenerator()
{
ShowSpaces = true;
ShowTabs = true;
ShowBoxForControlCharacters = true;
}

void IBuiltinElementGenerator.FetchOptions(TextEditorOptions options)
{
ShowSpaces = options.ShowSpaces;
ShowTabs = options.ShowTabs;
ShowBoxForControlCharacters = options.ShowBoxForControlCharacters;
}

/// <summary>
/// Tabs are control characters, but they are laid out by <see cref="TabElementGenerator"/>.
/// </summary>
private bool ShowsBoxFor(char c)
{
return ShowBoxForControlCharacters && char.IsControl(c) && c != '\t';
}

public override int GetFirstInterestedOffset(int startOffset)
{
var endLine = CurrentContext.VisualLine.LastDocumentLine;
Expand All @@ -83,12 +83,8 @@ public override int GetFirstInterestedOffset(int startOffset)
if (ShowSpaces)
return startOffset + i;
break;
case '\t':
if (ShowTabs)
return startOffset + i;
break;
default:
if (ShowBoxForControlCharacters && char.IsControl(c)) {
if (ShowsBoxFor(c)) {
return startOffset + i;
}
break;
Expand All @@ -110,22 +106,14 @@ public override VisualLineElement ConstructElement(int offset)
return new SpaceTextElement(textLine);
}

if (ShowTabs && (c == '\t'))
{
var properties = new VisualLineElementTextRunProperties(CurrentContext.GlobalTextRunProperties);
properties.SetForegroundBrush(CurrentContext.TextView.NonPrintableCharacterBrush);
var textSource = new SimpleTextSource(CurrentContext.TextView.Options.ShowTabsGlyph, properties);
var textLine = TextFormatter.Current.FormatLine(textSource, 0, double.MaxValue, new GenericTextParagraphProperties(properties));
return new TabTextElement(textLine);
}

if (ShowBoxForControlCharacters && char.IsControl(c))
if (ShowsBoxFor(c))
{
var properties = new VisualLineElementTextRunProperties(CurrentContext.GlobalTextRunProperties);
properties.SetForegroundBrush(Brushes.White);
var textSource = new SimpleTextSource(TextUtilities.GetControlCharacterName(c), properties);
var name = TextUtilities.GetControlCharacterName(c);
var textSource = new SimpleTextSource(name, properties);
var textLine = TextFormatter.Current.FormatLine(textSource, 0, double.MaxValue, new GenericTextParagraphProperties(properties));
return new SpecialCharacterBoxElement(textLine);
return new SpecialCharacterBoxElement(textLine, name.Length);
}

return null;
Expand All @@ -151,71 +139,15 @@ public override bool IsWhitespace(int visualColumn)
}
}

private sealed class TabTextElement : VisualLineElement
{
internal readonly TextLine Text;

public TabTextElement(TextLine text) : base(2, 1)
{
Text = text;
}

public override TextRun CreateTextRun(int startVisualColumn, ITextRunConstructionContext context)
{
// the TabTextElement consists of two TextRuns:
// first a TabGlyphRun, then TextCharacters '\t' to let WPF handle the tab indentation
if (startVisualColumn == VisualColumn)
return new TabGlyphRun(this, TextRunProperties);
else if (startVisualColumn == VisualColumn + 1)
return new TextCharacters("\t".AsMemory(), TextRunProperties);
else
throw new ArgumentOutOfRangeException(nameof(startVisualColumn));
}

public override int GetNextCaretPosition(int visualColumn, LogicalDirection direction, CaretPositioningMode mode)
{
if (mode == CaretPositioningMode.Normal || mode == CaretPositioningMode.EveryCodepoint)
return base.GetNextCaretPosition(visualColumn, direction, mode);
else
return -1;
}

public override bool IsWhitespace(int visualColumn)
{
return true;
}
}

private sealed class TabGlyphRun : DrawableTextRun
{
private readonly TabTextElement _element;

public TabGlyphRun(TabTextElement element, TextRunProperties properties)
{
if (properties == null)
throw new ArgumentNullException(nameof(properties));
Properties = properties;
_element = element;
}

public override TextRunProperties Properties { get; }

public override double Baseline => _element.Text.Baseline;

public override Size Size => default;

public override void Draw(DrawingContext drawingContext, Point origin)
{
_element.Text.Draw(drawingContext, origin);
}
}

private sealed class SpecialCharacterBoxElement : FormattedTextElement
{
public SpecialCharacterBoxElement(TextLine text) : base(text, 1)
public SpecialCharacterBoxElement(TextLine text, int displayColumnLength) : base(text, 1)
{
DisplayColumnLength = displayColumnLength;
}

public override int DisplayColumnLength { get; }

public override TextRun CreateTextRun(int startVisualColumn, ITextRunConstructionContext context)
{
return new SpecialCharacterTextRun(this, TextRunProperties);
Expand Down
177 changes: 177 additions & 0 deletions src/AvaloniaEdit/Rendering/TabElementGenerator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
// software and associated documentation files (the "Software"), to deal in the Software
// without restriction, including without limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
// to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.

using System;
using Avalonia;
using Avalonia.Media;
using Avalonia.Media.TextFormatting;
using AvaloniaEdit.Document;
using AvaloniaEdit.Utils;
using LogicalDirection = AvaloniaEdit.Document.LogicalDirection;

namespace AvaloniaEdit.Rendering
{
// This class is internal because it does not need to be accessed by the user - it can be configured using TextEditorOptions.

/// <summary>
/// Element generator for tab characters. A tab spans the columns up to the next multiple of
/// <see cref="TextEditorOptions.IndentationSize"/>, so that text after it lines up.
/// </summary>
/// <remarks>
/// A column is <see cref="TextView.WideSpaceWidth"/> wide, so the alignment holds for the
/// monospaced typefaces a code editor is used with, but not for proportional ones. Columns are
/// counted from the start of the visual line, so on a wrapped row they continue the line they
/// belong to rather than restarting, and <see cref="TextEditorOptions.WordWrapIndentation"/>
/// shifts that whole row.
/// </remarks>
internal sealed class TabElementGenerator : VisualLineElementGenerator
{
/// <summary>
/// Gets/Sets whether to show » for tabs.
/// </summary>
public bool ShowTabs { get; set; }

private ReadOnlyMemory<char> _padding;

private TextLine _glyph;
private string _glyphText;
private TextRunProperties _glyphProperties;

public void FetchOptions(TextEditorOptions options)
{
ShowTabs = options.ShowTabs;
}

public override bool RunsOnLongLines => true;

public override int GetFirstInterestedOffset(int startOffset)
{
var endLine = CurrentContext.VisualLine.LastDocumentLine;
var relevantText = CurrentContext.GetText(startOffset, endLine.EndOffset - startOffset);
var tab = relevantText.Text.AsSpan(relevantText.Offset, relevantText.Count).IndexOf('\t');

return tab < 0 ? -1 : startOffset + tab;
}

public override VisualLineElement ConstructElement(int offset)
{
if (CurrentContext.Document.GetCharAt(offset) != '\t')
return null;

var columnsUntilNextTabStop = TabStop.ColumnsUntilNext(
CurrentContext.VisualLine.CurrentDisplayColumn,
CurrentContext.TextView.Options.IndentationSize);

return new TabTextElement(ShowTabs ? GetGlyph() : null, GetPadding(columnsUntilNextTabStop));
}

/// <summary>
/// Formats the glyph, and keeps it for as long as its inputs stay the same. The global text
/// run properties are a new instance for every redraw, which is what expires it.
/// </summary>
private TextLine GetGlyph()
{
var glyphText = CurrentContext.TextView.Options.ShowTabsGlyph;
var globalProperties = CurrentContext.GlobalTextRunProperties;

if (_glyph == null || _glyphText != glyphText || _glyphProperties != globalProperties)
{
var properties = new VisualLineElementTextRunProperties(globalProperties);
properties.SetForegroundBrush(CurrentContext.TextView.NonPrintableCharacterBrush);
var textSource = new SimpleTextSource(glyphText, properties);

_glyph = TextFormatter.Current.FormatLine(textSource, 0, double.MaxValue, new GenericTextParagraphProperties(properties));
_glyphText = glyphText;
_glyphProperties = globalProperties;
}

return _glyph;
}

private ReadOnlyMemory<char> GetPadding(int columnCount)
{
if (_padding.Length < columnCount)
_padding = new string(' ', columnCount).AsMemory();
return _padding.Slice(0, columnCount);
}

private sealed class TabTextElement : VisualLineElement
{
private readonly TextLine _glyph;
private readonly ReadOnlyMemory<char> _padding;

public TabTextElement(TextLine glyph, ReadOnlyMemory<char> padding) : base(padding.Length, 1)
{
_glyph = glyph;
_padding = padding;
}

public override TextRun CreateTextRun(int startVisualColumn, ITextRunConstructionContext context)
{
var column = startVisualColumn - VisualColumn;
if (column < 0 || column >= VisualLength)
throw new ArgumentOutOfRangeException(nameof(startVisualColumn));

// one column wide rather than as wide as the glyph, so that a wide ShowTabsGlyph
// cannot stretch a tab (#206, #207)
if (column == 0 && _glyph != null)
return new TabGlyphRun(_glyph, TextRunProperties, context.TextView.WideSpaceWidth);

return new TextCharacters(_padding.Slice(column), TextRunProperties);
}

public override int GetNextCaretPosition(int visualColumn, LogicalDirection direction, CaretPositioningMode mode)
{
if (mode == CaretPositioningMode.Normal || mode == CaretPositioningMode.EveryCodepoint)
return base.GetNextCaretPosition(visualColumn, direction, mode);
else
return -1;
}

public override bool IsWhitespace(int visualColumn)
{
return true;
}
}

private sealed class TabGlyphRun : DrawableTextRun
{
private readonly TextLine _glyph;

public TabGlyphRun(TextLine glyph, TextRunProperties properties, double width)
{
if (properties == null)
throw new ArgumentNullException(nameof(properties));
Properties = properties;
_glyph = glyph;
Size = new Size(width, 0);
}

public override TextRunProperties Properties { get; }

public override double Baseline => _glyph.Baseline;

public override Size Size { get; }

public override void Draw(DrawingContext drawingContext, Point origin)
{
_glyph.Draw(drawingContext, origin);
}
}
}
}
Loading