Skip to content

Commit

Permalink
Add inlines support
Browse files Browse the repository at this point in the history
  • Loading branch information
Gillibald committed Nov 17, 2021
1 parent 932cb32 commit 4b89cb6
Show file tree
Hide file tree
Showing 14 changed files with 650 additions and 18 deletions.
17 changes: 17 additions & 0 deletions src/Avalonia.Controls/Documents/Bold.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using Avalonia.Media;

namespace Avalonia.Controls.Documents
{
/// <summary>
/// Bold element - markup helper for indicating bolded content.
/// Equivalent to a Span with FontWeight property set to FontWeights.Bold.
/// Can contain other inline elements.
/// </summary>
public sealed class Bold : Span
{
static Bold()
{
FontWeightProperty.OverrideDefaultValue<Bold>(FontWeight.Bold);
}
}
}
69 changes: 69 additions & 0 deletions src/Avalonia.Controls/Documents/Inline.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
using System.Collections.Generic;
using System.Text;
using Avalonia.Media;
using Avalonia.Media.TextFormatting;
using Avalonia.Utilities;

namespace Avalonia.Controls.Documents
{
/// <summary>
/// Inline element.
/// </summary>
public abstract class Inline : TextElement
{
/// <summary>
/// AvaloniaProperty for <see cref="TextDecorations" /> property.
/// </summary>
public static readonly StyledProperty<TextDecorationCollection> TextDecorationsProperty =
AvaloniaProperty.Register<Inline, TextDecorationCollection>(
nameof(TextDecorations));

/// <summary>
/// AvaloniaProperty for <see cref="BaselineAlignment" /> property.
/// </summary>
public static readonly StyledProperty<BaselineAlignment> BaselineAlignmentProperty =
AvaloniaProperty.Register<Inline, BaselineAlignment>(
nameof(BaselineAlignment),
BaselineAlignment.Baseline);

/// <summary>
/// The TextDecorations property specifies decorations that are added to the text of an element.
/// </summary>
public TextDecorationCollection TextDecorations
{
get { return GetValue(TextDecorationsProperty); }
set { SetValue(TextDecorationsProperty, value); }
}

/// <summary>
/// Describes how the baseline for a text-based element is positioned on the vertical axis,
/// relative to the established baseline for text.
/// </summary>
public BaselineAlignment BaselineAlignment
{
get { return GetValue(BaselineAlignmentProperty); }
set { SetValue(BaselineAlignmentProperty, value); }
}

internal abstract int BuildRun(StringBuilder stringBuilder, IList<ValueSpan<TextRunProperties>> textStyleOverrides, int firstCharacterIndex);

protected TextRunProperties CreateTextRunProperties()
{
return new GenericTextRunProperties(new Typeface(FontFamily, FontStyle, FontWeight), FontSize,
TextDecorations, Foreground, Background, BaselineAlignment);
}

protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
{
base.OnPropertyChanged(change);

switch (change.Property.Name)
{
case nameof(TextDecorations):
case nameof(BaselineAlignment):
Invalidate();
break;
}
}
}
}
93 changes: 93 additions & 0 deletions src/Avalonia.Controls/Documents/InlineCollection.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
using System;
using Avalonia.Collections;
using Avalonia.LogicalTree;

namespace Avalonia.Controls.Documents
{
/// <summary>
/// A collection of <see cref="Inline"/>s.
/// </summary>
public class InlineCollection : AvaloniaList<Inline>
{
private string _text = string.Empty;

/// <summary>
/// Initializes a new instance of the <see cref="InlineCollection"/> class.
/// </summary>
public InlineCollection(ILogical parent) : base(0)
{
ResetBehavior = ResetBehavior.Remove;

this.ForEachItem(
x =>
{
((ISetLogicalParent)x).SetParent(parent);
x.Invalidated += Invalidate;
Invalidate();
},
x =>
{
((ISetLogicalParent)x).SetParent(null);
x.Invalidated -= Invalidate;
Invalidate();
},
() => throw new NotSupportedException());
}

public bool HasComplexContent => Count > 0;

/// <summary>
/// Gets or adds the text held by the inlines collection.
/// <remarks>
/// Can be null for complex content.
/// </remarks>
/// </summary>
public string Text
{
get => _text;
set
{
if (Count > 0)
{
Add(new Run(value));
}
else
{
_text = value;
}
}
}

/// <summary>
/// Add a text segment to the collection.
/// <remarks>
/// For non complex content this appends the text to the end of currently held text.
/// For complex content this adds a <see cref="Run"/> to the collection.
/// </remarks>
/// </summary>
/// <param name="text"></param>
public void Add(string text)
{
if (Count == 0)
{
Text += text;
}
else
{
Add(new Run(text));
}
}

/// <summary>
/// Raised when an inline in the collection changes.
/// </summary>
public event EventHandler Invalidated;

/// <summary>
/// Raises the <see cref="Invalidated"/> event.
/// </summary>
protected void Invalidate() => Invalidated?.Invoke(this, EventArgs.Empty);

private void Invalidate(object sender, EventArgs e) => Invalidate();
}
}
17 changes: 17 additions & 0 deletions src/Avalonia.Controls/Documents/Italic.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using Avalonia.Media;

namespace Avalonia.Controls.Documents
{
/// <summary>
/// Italic element - markup helper for indicating italicized content.
/// Equivalent to a Span with FontStyle property set to FontStyles.Italic.
/// Can contain other inline elements.
/// </summary>
public sealed class Italic : Span
{
static Italic()
{
FontStyleProperty.OverrideDefaultValue<Italic>(FontStyle.Italic);
}
}
}
35 changes: 35 additions & 0 deletions src/Avalonia.Controls/Documents/LineBreak.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using System;
using System.Collections.Generic;
using System.Text;
using Avalonia.Media.TextFormatting;
using Avalonia.Utilities;

namespace Avalonia.Controls.Documents
{
/// <summary>
/// LineBreak element that forces a line breaking.
/// </summary>
public class LineBreak : Inline
{
/// <summary>
/// Creates a new LineBreak instance.
/// </summary>
public LineBreak()
{
}

internal override int BuildRun(StringBuilder stringBuilder,
IList<ValueSpan<TextRunProperties>> textStyleOverrides, int firstCharacterIndex)
{
var text = Environment.NewLine;

stringBuilder.Append(text);

textStyleOverrides.Add(new ValueSpan<TextRunProperties>(firstCharacterIndex, text.Length,
CreateTextRunProperties()));

return text.Length;
}
}
}

92 changes: 92 additions & 0 deletions src/Avalonia.Controls/Documents/Run.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
using System;
using System.Collections.Generic;
using System.Text;
using Avalonia.Data;
using Avalonia.Media.TextFormatting;
using Avalonia.Metadata;
using Avalonia.Utilities;

namespace Avalonia.Controls.Documents
{
/// <summary>
/// A terminal element in text flow hierarchy - contains a uniformatted run of unicode characters
/// </summary>
public class Run : Inline
{
/// <summary>
/// Initializes an instance of Run class.
/// </summary>
public Run()
{
}

/// <summary>
/// Initializes an instance of Run class specifying its text content.
/// </summary>
/// <param name="text">
/// Text content assigned to the Run.
/// </param>
public Run(string text)
{
Text = text;
}

/// <summary>
/// Dependency property backing Text.
/// </summary>
/// <remarks>
/// Note that when a TextRange that intersects with this Run gets modified (e.g. by editing
/// a selection in RichTextBox), we will get two changes to this property since we delete
/// and then insert when setting the content of a TextRange.
/// </remarks>
public static readonly StyledProperty<string> TextProperty = AvaloniaProperty.Register<Run, string> (
nameof (Text),
string.Empty, defaultBindingMode: BindingMode.TwoWay,
coerce: CoerceText);

/// <summary>
/// The content spanned by this TextElement.
/// </summary>
[Content]
public string Text {
get { return GetValue (TextProperty); }
set { SetValue (TextProperty, value); }
}

/// <summary>
/// Coercion callback for the Text property.
/// </summary>
/// <param name="d">The object that the property exists on.</param>
/// <param name="baseValue">The new value of the property, prior to any coercion attempt.</param>
/// <returns>The coerced value.</returns>
private static string CoerceText (IAvaloniaObject d, string baseValue)
{
return baseValue ?? string.Empty;
}

internal override int BuildRun(StringBuilder stringBuilder,
IList<ValueSpan<TextRunProperties>> textStyleOverrides, int firstCharacterIndex)
{
var text = Text;

stringBuilder.Append(text);

textStyleOverrides.Add(new ValueSpan<TextRunProperties>(firstCharacterIndex, text.Length,
CreateTextRunProperties()));

return text.Length;
}

protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
{
base.OnPropertyChanged(change);

switch (change.Property.Name)
{
case nameof(Text):
Invalidate();
break;
}
}
}
}
66 changes: 66 additions & 0 deletions src/Avalonia.Controls/Documents/Span.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
using System.Collections.Generic;
using System.Text;
using Avalonia.Media.TextFormatting;
using Avalonia.Metadata;
using Avalonia.Utilities;

namespace Avalonia.Controls.Documents
{
/// <summary>
/// Span element used for grouping other Inline elements.
/// </summary>
public class Span : Inline
{
/// <summary>
/// Defines the <see cref="Inlines"/> property.
/// </summary>
public static readonly DirectProperty<Span, InlineCollection> InlinesProperty =
AvaloniaProperty.RegisterDirect<Span, InlineCollection>(
nameof(Inlines),
o => o.Inlines);

/// <summary>
/// Initializes a new instance of a Span element.
/// </summary>
public Span()
{
Inlines = new InlineCollection(this);

Inlines.Invalidated += (s, e) => Invalidate();
}

/// <summary>
/// Gets or sets the inlines.
/// </summary>
[Content]
public InlineCollection Inlines { get; }

internal override int BuildRun(StringBuilder stringBuilder, IList<ValueSpan<TextRunProperties>> textStyleOverrides, int firstCharacterIndex)
{
var length = 0;

if (Inlines.HasComplexContent)
{
foreach (var inline in Inlines)
{
var inlineLength = inline.BuildRun(stringBuilder, textStyleOverrides, firstCharacterIndex);

firstCharacterIndex += inlineLength;

length += inlineLength;
}
}
else
{
stringBuilder.Append(Inlines.Text);

length = Inlines.Text.Length;

textStyleOverrides.Add(new ValueSpan<TextRunProperties>(firstCharacterIndex, length,
CreateTextRunProperties()));
}

return length;
}
}
}
Loading

0 comments on commit 4b89cb6

Please sign in to comment.