diff --git a/samples/ControlCatalog/Pages/TextBlockPage.xaml b/samples/ControlCatalog/Pages/TextBlockPage.xaml
index 9998a20d42..fe9455bd29 100644
--- a/samples/ControlCatalog/Pages/TextBlockPage.xaml
+++ b/samples/ControlCatalog/Pages/TextBlockPage.xaml
@@ -117,6 +117,17 @@
+
+
+ This is a
+ TextBlock
+ with several
+ Span elements,
+
+ using a variety of styles
+ .
+
+
diff --git a/src/Avalonia.Base/Metadata/TrimSurroundingWhitespaceAttribute.cs b/src/Avalonia.Base/Metadata/TrimSurroundingWhitespaceAttribute.cs
new file mode 100644
index 0000000000..c46891b3ad
--- /dev/null
+++ b/src/Avalonia.Base/Metadata/TrimSurroundingWhitespaceAttribute.cs
@@ -0,0 +1,10 @@
+using System;
+
+namespace Avalonia.Metadata
+{
+ [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
+ public class TrimSurroundingWhitespaceAttribute : Attribute
+ {
+
+ }
+}
diff --git a/src/Avalonia.Base/Metadata/WhitespaceSignificantCollectionAttribute.cs b/src/Avalonia.Base/Metadata/WhitespaceSignificantCollectionAttribute.cs
new file mode 100644
index 0000000000..aeaa38dad9
--- /dev/null
+++ b/src/Avalonia.Base/Metadata/WhitespaceSignificantCollectionAttribute.cs
@@ -0,0 +1,12 @@
+using System;
+
+namespace Avalonia.Metadata
+{
+ ///
+ /// Indicates that a collection type should be processed as being whitespace significant by a XAML processor.
+ ///
+ [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
+ public class WhitespaceSignificantCollectionAttribute : Attribute
+ {
+ }
+}
diff --git a/src/Avalonia.Controls/Documents/Bold.cs b/src/Avalonia.Controls/Documents/Bold.cs
new file mode 100644
index 0000000000..7d0a9130ae
--- /dev/null
+++ b/src/Avalonia.Controls/Documents/Bold.cs
@@ -0,0 +1,17 @@
+using Avalonia.Media;
+
+namespace Avalonia.Controls.Documents
+{
+ ///
+ /// Bold element - markup helper for indicating bolded content.
+ /// Equivalent to a Span with FontWeight property set to FontWeights.Bold.
+ /// Can contain other inline elements.
+ ///
+ public sealed class Bold : Span
+ {
+ static Bold()
+ {
+ FontWeightProperty.OverrideDefaultValue(FontWeight.Bold);
+ }
+ }
+}
diff --git a/src/Avalonia.Controls/Documents/Inline.cs b/src/Avalonia.Controls/Documents/Inline.cs
new file mode 100644
index 0000000000..5b63f95432
--- /dev/null
+++ b/src/Avalonia.Controls/Documents/Inline.cs
@@ -0,0 +1,71 @@
+using System.Collections.Generic;
+using System.Text;
+using Avalonia.Media;
+using Avalonia.Media.TextFormatting;
+using Avalonia.Utilities;
+
+namespace Avalonia.Controls.Documents
+{
+ ///
+ /// Inline element.
+ ///
+ public abstract class Inline : TextElement
+ {
+ ///
+ /// AvaloniaProperty for property.
+ ///
+ public static readonly StyledProperty TextDecorationsProperty =
+ AvaloniaProperty.Register(
+ nameof(TextDecorations));
+
+ ///
+ /// AvaloniaProperty for property.
+ ///
+ public static readonly StyledProperty BaselineAlignmentProperty =
+ AvaloniaProperty.Register(
+ nameof(BaselineAlignment),
+ BaselineAlignment.Baseline);
+
+ ///
+ /// The TextDecorations property specifies decorations that are added to the text of an element.
+ ///
+ public TextDecorationCollection TextDecorations
+ {
+ get { return GetValue(TextDecorationsProperty); }
+ set { SetValue(TextDecorationsProperty, value); }
+ }
+
+ ///
+ /// Describes how the baseline for a text-based element is positioned on the vertical axis,
+ /// relative to the established baseline for text.
+ ///
+ public BaselineAlignment BaselineAlignment
+ {
+ get { return GetValue(BaselineAlignmentProperty); }
+ set { SetValue(BaselineAlignmentProperty, value); }
+ }
+
+ internal abstract int BuildRun(StringBuilder stringBuilder, IList> textStyleOverrides, int firstCharacterIndex);
+
+ internal abstract int AppendText(StringBuilder stringBuilder);
+
+ protected TextRunProperties CreateTextRunProperties()
+ {
+ return new GenericTextRunProperties(new Typeface(FontFamily, FontStyle, FontWeight), FontSize,
+ TextDecorations, Foreground, Background, BaselineAlignment);
+ }
+
+ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
+ {
+ base.OnPropertyChanged(change);
+
+ switch (change.Property.Name)
+ {
+ case nameof(TextDecorations):
+ case nameof(BaselineAlignment):
+ Invalidate();
+ break;
+ }
+ }
+ }
+}
diff --git a/src/Avalonia.Controls/Documents/InlineCollection.cs b/src/Avalonia.Controls/Documents/InlineCollection.cs
new file mode 100644
index 0000000000..45c715c13a
--- /dev/null
+++ b/src/Avalonia.Controls/Documents/InlineCollection.cs
@@ -0,0 +1,123 @@
+using System;
+using System.Text;
+using Avalonia.Collections;
+using Avalonia.LogicalTree;
+using Avalonia.Metadata;
+
+namespace Avalonia.Controls.Documents
+{
+ ///
+ /// A collection of s.
+ ///
+ [WhitespaceSignificantCollection]
+ public class InlineCollection : AvaloniaList
+ {
+ private string? _text = string.Empty;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ 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;
+
+ ///
+ /// Gets or adds the text held by the inlines collection.
+ ///
+ /// Can be null for complex content.
+ ///
+ ///
+ public string? Text
+ {
+ get
+ {
+ if (!HasComplexContent)
+ {
+ return _text;
+ }
+
+ var builder = new StringBuilder();
+
+ foreach(var inline in this)
+ {
+ inline.AppendText(builder);
+ }
+
+ return builder.ToString();
+ }
+ set
+ {
+ if (HasComplexContent)
+ {
+ Add(new Run(value));
+ }
+ else
+ {
+ _text = value;
+ }
+ }
+ }
+
+ ///
+ /// Add a text segment to the collection.
+ ///
+ /// For non complex content this appends the text to the end of currently held text.
+ /// For complex content this adds a to the collection.
+ ///
+ ///
+ ///
+ public void Add(string text)
+ {
+ if (HasComplexContent)
+ {
+ Add(new Run(text));
+ }
+ else
+ {
+ _text += text;
+ }
+ }
+
+ public override void Add(Inline item)
+ {
+ if (!HasComplexContent)
+ {
+ base.Add(new Run(_text));
+
+ _text = string.Empty;
+ }
+
+ base.Add(item);
+ }
+
+ ///
+ /// Raised when an inline in the collection changes.
+ ///
+ public event EventHandler? Invalidated;
+
+ ///
+ /// Raises the event.
+ ///
+ protected void Invalidate() => Invalidated?.Invoke(this, EventArgs.Empty);
+
+ private void Invalidate(object? sender, EventArgs e) => Invalidate();
+ }
+}
diff --git a/src/Avalonia.Controls/Documents/Italic.cs b/src/Avalonia.Controls/Documents/Italic.cs
new file mode 100644
index 0000000000..e9f4698fc4
--- /dev/null
+++ b/src/Avalonia.Controls/Documents/Italic.cs
@@ -0,0 +1,17 @@
+using Avalonia.Media;
+
+namespace Avalonia.Controls.Documents
+{
+ ///
+ /// Italic element - markup helper for indicating italicized content.
+ /// Equivalent to a Span with FontStyle property set to FontStyles.Italic.
+ /// Can contain other inline elements.
+ ///
+ public sealed class Italic : Span
+ {
+ static Italic()
+ {
+ FontStyleProperty.OverrideDefaultValue(FontStyle.Italic);
+ }
+ }
+}
diff --git a/src/Avalonia.Controls/Documents/LineBreak.cs b/src/Avalonia.Controls/Documents/LineBreak.cs
new file mode 100644
index 0000000000..5e0cd1d387
--- /dev/null
+++ b/src/Avalonia.Controls/Documents/LineBreak.cs
@@ -0,0 +1,44 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Avalonia.Media.TextFormatting;
+using Avalonia.Metadata;
+using Avalonia.Utilities;
+
+namespace Avalonia.Controls.Documents
+{
+ ///
+ /// LineBreak element that forces a line breaking.
+ ///
+ [TrimSurroundingWhitespace]
+ public class LineBreak : Inline
+ {
+ ///
+ /// Creates a new LineBreak instance.
+ ///
+ public LineBreak()
+ {
+ }
+
+ internal override int BuildRun(StringBuilder stringBuilder,
+ IList> textStyleOverrides, int firstCharacterIndex)
+ {
+ var length = AppendText(stringBuilder);
+
+ textStyleOverrides.Add(new ValueSpan(firstCharacterIndex, length,
+ CreateTextRunProperties()));
+
+ return length;
+ }
+
+ internal override int AppendText(StringBuilder stringBuilder)
+ {
+ var text = Environment.NewLine;
+
+ stringBuilder.Append(text);
+
+ return text.Length;
+ }
+ }
+}
+
diff --git a/src/Avalonia.Controls/Documents/Run.cs b/src/Avalonia.Controls/Documents/Run.cs
new file mode 100644
index 0000000000..a7dd5fd94f
--- /dev/null
+++ b/src/Avalonia.Controls/Documents/Run.cs
@@ -0,0 +1,86 @@
+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
+{
+ ///
+ /// A terminal element in text flow hierarchy - contains a uniformatted run of unicode characters
+ ///
+ public class Run : Inline
+ {
+ ///
+ /// Initializes an instance of Run class.
+ ///
+ public Run()
+ {
+ }
+
+ ///
+ /// Initializes an instance of Run class specifying its text content.
+ ///
+ ///
+ /// Text content assigned to the Run.
+ ///
+ public Run(string? text)
+ {
+ Text = text;
+ }
+
+ ///
+ /// Dependency property backing Text.
+ ///
+ ///
+ /// 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.
+ ///
+ public static readonly StyledProperty TextProperty = AvaloniaProperty.Register (
+ nameof (Text), defaultBindingMode: BindingMode.TwoWay);
+
+ ///
+ /// The content spanned by this TextElement.
+ ///
+ [Content]
+ public string? Text {
+ get { return GetValue (TextProperty); }
+ set { SetValue (TextProperty, value); }
+ }
+
+ internal override int BuildRun(StringBuilder stringBuilder,
+ IList> textStyleOverrides, int firstCharacterIndex)
+ {
+ var length = AppendText(stringBuilder);
+
+ textStyleOverrides.Add(new ValueSpan(firstCharacterIndex, length,
+ CreateTextRunProperties()));
+
+ return length;
+ }
+
+ internal override int AppendText(StringBuilder stringBuilder)
+ {
+ var text = Text ?? "";
+
+ stringBuilder.Append(text);
+
+ return text.Length;
+ }
+
+ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
+ {
+ base.OnPropertyChanged(change);
+
+ switch (change.Property.Name)
+ {
+ case nameof(Text):
+ Invalidate();
+ break;
+ }
+ }
+ }
+}
diff --git a/src/Avalonia.Controls/Documents/Span.cs b/src/Avalonia.Controls/Documents/Span.cs
new file mode 100644
index 0000000000..c086997b07
--- /dev/null
+++ b/src/Avalonia.Controls/Documents/Span.cs
@@ -0,0 +1,95 @@
+using System.Collections.Generic;
+using System.Text;
+using Avalonia.Media.TextFormatting;
+using Avalonia.Metadata;
+using Avalonia.Utilities;
+
+namespace Avalonia.Controls.Documents
+{
+ ///
+ /// Span element used for grouping other Inline elements.
+ ///
+ public class Span : Inline
+ {
+ ///
+ /// Defines the property.
+ ///
+ public static readonly DirectProperty InlinesProperty =
+ AvaloniaProperty.RegisterDirect(
+ nameof(Inlines),
+ o => o.Inlines);
+
+ ///
+ /// Initializes a new instance of a Span element.
+ ///
+ public Span()
+ {
+ Inlines = new InlineCollection(this);
+
+ Inlines.Invalidated += (s, e) => Invalidate();
+ }
+
+ ///
+ /// Gets or sets the inlines.
+ ///
+ [Content]
+ public InlineCollection Inlines { get; }
+
+ internal override int BuildRun(StringBuilder stringBuilder, IList> 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
+ {
+ if (Inlines.Text == null)
+ {
+ return length;
+ }
+
+ stringBuilder.Append(Inlines.Text);
+
+ length = Inlines.Text.Length;
+
+ textStyleOverrides.Add(new ValueSpan(firstCharacterIndex, length,
+ CreateTextRunProperties()));
+ }
+
+ return length;
+ }
+
+ internal override int AppendText(StringBuilder stringBuilder)
+ {
+ if (Inlines.HasComplexContent)
+ {
+ var length = 0;
+
+ foreach (var inline in Inlines)
+ {
+ length += inline.AppendText(stringBuilder);
+ }
+
+ return length;
+ }
+
+ if (Inlines.Text == null)
+ {
+ return 0;
+ }
+
+ stringBuilder.Append(Inlines.Text);
+
+ return Inlines.Text.Length;
+ }
+ }
+}
diff --git a/src/Avalonia.Controls/Documents/TextElement.cs b/src/Avalonia.Controls/Documents/TextElement.cs
new file mode 100644
index 0000000000..4083524881
--- /dev/null
+++ b/src/Avalonia.Controls/Documents/TextElement.cs
@@ -0,0 +1,129 @@
+using System;
+using Avalonia.Media;
+
+namespace Avalonia.Controls.Documents
+{
+ ///
+ /// TextElement is an base class for content in text based controls.
+ /// TextElements span other content, applying property values or providing structural information.
+ ///
+ public abstract class TextElement : StyledElement
+ {
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty BackgroundProperty =
+ AvaloniaProperty.Register(nameof(Background), inherits: true);
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly AttachedProperty FontFamilyProperty =
+ TextBlock.FontFamilyProperty.AddOwner();
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly AttachedProperty FontSizeProperty =
+ TextBlock.FontSizeProperty.AddOwner();
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly AttachedProperty FontStyleProperty =
+ TextBlock.FontStyleProperty.AddOwner();
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly AttachedProperty FontWeightProperty =
+ TextBlock.FontWeightProperty.AddOwner();
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly AttachedProperty ForegroundProperty =
+ TextBlock.ForegroundProperty.AddOwner();
+
+ ///
+ /// Gets or sets a brush used to paint the control's background.
+ ///
+ public IBrush? Background
+ {
+ get { return GetValue(BackgroundProperty); }
+ set { SetValue(BackgroundProperty, value); }
+ }
+
+ ///
+ /// Gets or sets the font family.
+ ///
+ public FontFamily FontFamily
+ {
+ get { return GetValue(FontFamilyProperty); }
+ set { SetValue(FontFamilyProperty, value); }
+ }
+
+ ///
+ /// Gets or sets the font size.
+ ///
+ public double FontSize
+ {
+ get { return GetValue(FontSizeProperty); }
+ set { SetValue(FontSizeProperty, value); }
+ }
+
+ ///
+ /// Gets or sets the font style.
+ ///
+ public FontStyle FontStyle
+ {
+ get { return GetValue(FontStyleProperty); }
+ set { SetValue(FontStyleProperty, value); }
+ }
+
+ ///
+ /// Gets or sets the font weight.
+ ///
+ public FontWeight FontWeight
+ {
+ get { return GetValue(FontWeightProperty); }
+ set { SetValue(FontWeightProperty, value); }
+ }
+
+ ///
+ /// Gets or sets a brush used to paint the text.
+ ///
+ public IBrush? Foreground
+ {
+ get { return GetValue(ForegroundProperty); }
+ set { SetValue(ForegroundProperty, value); }
+ }
+
+ ///
+ /// Raised when the visual representation of the text element changes.
+ ///
+ public event EventHandler? Invalidated;
+
+ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
+ {
+ base.OnPropertyChanged(change);
+
+ switch (change.Property.Name)
+ {
+ case nameof(Background):
+ case nameof(FontFamily):
+ case nameof(FontSize):
+ case nameof(FontStyle):
+ case nameof(FontWeight):
+ case nameof(Foreground):
+ Invalidate();
+ break;
+ }
+ }
+
+ ///
+ /// Raises the event.
+ ///
+ protected void Invalidate() => Invalidated?.Invoke(this, EventArgs.Empty);
+ }
+}
diff --git a/src/Avalonia.Controls/Documents/Underline.cs b/src/Avalonia.Controls/Documents/Underline.cs
new file mode 100644
index 0000000000..fcd46c8439
--- /dev/null
+++ b/src/Avalonia.Controls/Documents/Underline.cs
@@ -0,0 +1,15 @@
+namespace Avalonia.Controls.Documents
+{
+ ///
+ /// Underline element - markup helper for indicating superscript content.
+ /// Equivalent to a Span with TextDecorations property set to TextDecorations.Underlined.
+ /// Can contain other inline elements.
+ ///
+ public sealed class Underline : Span
+ {
+ static Underline()
+ {
+ TextDecorationsProperty.OverrideDefaultValue(Media.TextDecorations.Underline);
+ }
+ }
+}
diff --git a/src/Avalonia.Controls/Primitives/PopupRoot.cs b/src/Avalonia.Controls/Primitives/PopupRoot.cs
index be447ea512..a9a362a762 100644
--- a/src/Avalonia.Controls/Primitives/PopupRoot.cs
+++ b/src/Avalonia.Controls/Primitives/PopupRoot.cs
@@ -44,7 +44,7 @@ namespace Avalonia.Controls.Primitives
/// The dependency resolver to use. If null the default dependency resolver will be used.
///
public PopupRoot(TopLevel parent, IPopupImpl impl, IAvaloniaDependencyResolver? dependencyResolver)
- : base(ValidatingPopupImpl.Wrap(impl), dependencyResolver)
+ : base(impl, dependencyResolver)
{
_parent = parent;
}
diff --git a/src/Avalonia.Controls/Properties/AssemblyInfo.cs b/src/Avalonia.Controls/Properties/AssemblyInfo.cs
index 05561a38ef..302a2bbc13 100644
--- a/src/Avalonia.Controls/Properties/AssemblyInfo.cs
+++ b/src/Avalonia.Controls/Properties/AssemblyInfo.cs
@@ -14,3 +14,4 @@ using Avalonia.Metadata;
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Templates")]
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Notifications")]
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Chrome")]
+[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Documents")]
diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs
index 2dbb10f8e2..9e50a760fb 100644
--- a/src/Avalonia.Controls/TextBlock.cs
+++ b/src/Avalonia.Controls/TextBlock.cs
@@ -1,5 +1,8 @@
using System;
+using System.Collections.Generic;
using System.Reactive.Linq;
+using System.Text;
+using Avalonia.Controls.Documents;
using Avalonia.LogicalTree;
using Avalonia.Media;
using Avalonia.Media.TextFormatting;
@@ -108,6 +111,14 @@ namespace Avalonia.Controls
o => o.Text,
(o, v) => o.Text = v);
+ ///
+ /// Defines the property.
+ ///
+ public static readonly DirectProperty InlinesProperty =
+ AvaloniaProperty.RegisterDirect(
+ nameof(Inlines),
+ o => o.Inlines);
+
///
/// Defines the property.
///
@@ -132,7 +143,6 @@ namespace Avalonia.Controls
public static readonly StyledProperty TextDecorationsProperty =
AvaloniaProperty.Register(nameof(TextDecorations));
- private string? _text;
private TextLayout? _textLayout;
private Size _constraint;
@@ -151,7 +161,9 @@ namespace Avalonia.Controls
///
public TextBlock()
{
- _text = string.Empty;
+ Inlines = new InlineCollection(this);
+
+ Inlines.Invalidated += InlinesChanged;
}
///
@@ -186,13 +198,30 @@ namespace Avalonia.Controls
///
/// Gets or sets the text.
///
- [Content]
public string? Text
{
- get { return _text; }
- set { SetAndRaise(TextProperty, ref _text, value); }
+ get => Inlines.Text;
+ set
+ {
+ var old = Text;
+
+ if (value == old)
+ {
+ return;
+ }
+
+ Inlines.Text = value;
+
+ RaisePropertyChanged(TextProperty, old, value);
+ }
}
+ ///
+ /// Gets or sets the inlines.
+ ///
+ [Content]
+ public InlineCollection Inlines { get; }
+
///
/// Gets or sets the font family.
///
@@ -463,6 +492,23 @@ namespace Avalonia.Controls
/// A object.
protected virtual TextLayout CreateTextLayout(Size constraint, string? text)
{
+ List>? textStyleOverrides = null;
+
+ if (Inlines.HasComplexContent)
+ {
+ textStyleOverrides = new List>(Inlines.Count);
+
+ var textPosition = 0;
+ var stringBuilder = new StringBuilder();
+
+ foreach (var inline in Inlines)
+ {
+ textPosition += inline.BuildRun(stringBuilder, textStyleOverrides, textPosition);
+ }
+
+ text = stringBuilder.ToString();
+ }
+
return new TextLayout(
text ?? string.Empty,
new Typeface(FontFamily, FontStyle, FontWeight, FontStretch),
@@ -476,7 +522,8 @@ namespace Avalonia.Controls
constraint.Width,
constraint.Height,
maxLines: MaxLines,
- lineHeight: LineHeight);
+ lineHeight: LineHeight,
+ textStyleOverrides: textStyleOverrides);
}
///
@@ -491,6 +538,11 @@ namespace Avalonia.Controls
protected override Size MeasureOverride(Size availableSize)
{
+ if (!Inlines.HasComplexContent && string.IsNullOrEmpty(Text))
+ {
+ return new Size();
+ }
+
var padding = Padding;
_constraint = availableSize.Deflate(padding);
@@ -552,6 +604,11 @@ namespace Avalonia.Controls
break;
}
}
+ }
+
+ private void InlinesChanged(object? sender, EventArgs e)
+ {
+ InvalidateTextLayout();
}
}
}
diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs
index 6bba889748..a4fe154515 100644
--- a/src/Avalonia.Controls/TopLevel.cs
+++ b/src/Avalonia.Controls/TopLevel.cs
@@ -134,8 +134,6 @@ namespace Avalonia.Controls
"Could not create window implementation: maybe no windowing subsystem was initialized?");
}
- impl = ValidatingToplevelImpl.Wrap(impl);
-
PlatformImpl = impl;
_actualTransparencyLevel = PlatformImpl.TransparencyLevel;
diff --git a/src/Avalonia.Controls/ValidatingToplevel.cs b/src/Avalonia.Controls/ValidatingToplevel.cs
deleted file mode 100644
index 7e15bf4879..0000000000
--- a/src/Avalonia.Controls/ValidatingToplevel.cs
+++ /dev/null
@@ -1,344 +0,0 @@
-using System;
-using System.Collections.Generic;
-using Avalonia.Controls.Platform;
-using Avalonia.Controls.Primitives.PopupPositioning;
-using Avalonia.Input;
-using Avalonia.Input.Raw;
-using Avalonia.Input.TextInput;
-using Avalonia.Platform;
-using Avalonia.Rendering;
-
-namespace Avalonia.Controls;
-
-internal class ValidatingToplevelImpl : ITopLevelImpl, ITopLevelImplWithNativeControlHost,
- ITopLevelImplWithNativeMenuExporter, ITopLevelImplWithTextInputMethod
-{
- private readonly ITopLevelImpl _impl;
- private bool _disposed;
-
- public ValidatingToplevelImpl(ITopLevelImpl impl)
- {
- _impl = impl ?? throw new InvalidOperationException(
- "Could not create TopLevel implementation: maybe no windowing subsystem was initialized?");
- }
-
- public void Dispose()
- {
- _disposed = true;
- _impl.Dispose();
- }
-
- protected void CheckDisposed()
- {
- if (_disposed)
- throw new ObjectDisposedException(_impl.GetType().FullName);
- }
-
- protected ITopLevelImpl Inner
- {
- get
- {
- CheckDisposed();
- return _impl;
- }
- }
-
- public static ITopLevelImpl Wrap(ITopLevelImpl impl)
- {
-#if DEBUG
- if (impl is ValidatingToplevelImpl)
- return impl;
- return new ValidatingToplevelImpl(impl);
-#else
- return impl;
-#endif
- }
-
- public Size ClientSize => Inner.ClientSize;
- public Size? FrameSize => Inner.FrameSize;
- public double RenderScaling => Inner.RenderScaling;
- public IEnumerable
/// The window implementation.
public Window(IWindowImpl impl)
- : base(ValidatingWindowImpl.Wrap(impl))
+ : base(impl)
{
- var wrapped = (IWindowImpl)base.PlatformImpl!;
- wrapped.Closing = HandleClosing;
- wrapped.GotInputWhenDisabled = OnGotInputWhenDisabled;
- wrapped.WindowStateChanged = HandleWindowStateChanged;
+ impl.Closing = HandleClosing;
+ impl.GotInputWhenDisabled = OnGotInputWhenDisabled;
+ impl.WindowStateChanged = HandleWindowStateChanged;
_maxPlatformClientSize = PlatformImpl?.MaxAutoSizeHint ?? default(Size);
- wrapped.ExtendClientAreaToDecorationsChanged = ExtendClientAreaToDecorationsChanged;
+ impl.ExtendClientAreaToDecorationsChanged = ExtendClientAreaToDecorationsChanged;
this.GetObservable(ClientSizeProperty).Skip(1).Subscribe(x => PlatformImpl?.Resize(x, PlatformResizeReason.Application));
PlatformImpl?.ShowTaskbarIcon(ShowInTaskbar);
diff --git a/src/Avalonia.Controls/WindowBase.cs b/src/Avalonia.Controls/WindowBase.cs
index 4464491020..cebdd8d897 100644
--- a/src/Avalonia.Controls/WindowBase.cs
+++ b/src/Avalonia.Controls/WindowBase.cs
@@ -57,13 +57,12 @@ namespace Avalonia.Controls
{
}
- public WindowBase(IWindowBaseImpl impl, IAvaloniaDependencyResolver? dependencyResolver) : base(ValidatingWindowBaseImpl.Wrap(impl), dependencyResolver)
+ public WindowBase(IWindowBaseImpl impl, IAvaloniaDependencyResolver? dependencyResolver) : base(impl, dependencyResolver)
{
Screens = new Screens(PlatformImpl?.Screen);
- var wrapped = PlatformImpl!;
- wrapped.Activated = HandleActivated;
- wrapped.Deactivated = HandleDeactivated;
- wrapped.PositionChanged = HandlePositionChanged;
+ impl.Activated = HandleActivated;
+ impl.Deactivated = HandleDeactivated;
+ impl.PositionChanged = HandlePositionChanged;
}
///
diff --git a/src/Avalonia.Visuals/Avalonia.Visuals.csproj b/src/Avalonia.Visuals/Avalonia.Visuals.csproj
index 6c978e970e..a0b1f99fa1 100644
--- a/src/Avalonia.Visuals/Avalonia.Visuals.csproj
+++ b/src/Avalonia.Visuals/Avalonia.Visuals.csproj
@@ -12,6 +12,9 @@
+
+
+
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/FormattedTextSource.cs b/src/Avalonia.Visuals/Media/TextFormatting/FormattedTextSource.cs
index 98344141f1..1b0feaa718 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/FormattedTextSource.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/FormattedTextSource.cs
@@ -69,7 +69,7 @@ namespace Avalonia.Media.TextFormatting
var textRange = new TextRange(propertiesOverride.Start, propertiesOverride.Length);
- if (textRange.Start + textRange.Length < text.Start)
+ if (textRange.Start + textRange.Length <= text.Start)
{
continue;
}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs
index 8d3b7a67c7..750ec64798 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs
@@ -79,14 +79,12 @@ namespace Avalonia.Media.TextFormatting
if(TryGetShapeableLength(text, previousTypeface.Value, out var fallbackCount, out _))
{
return new ShapeableTextCharacters(text.Take(fallbackCount),
- new GenericTextRunProperties(previousTypeface.Value, defaultProperties.FontRenderingEmSize,
- defaultProperties.TextDecorations, defaultProperties.ForegroundBrush), biDiLevel);
+ defaultProperties.WithTypeface(previousTypeface.Value), biDiLevel);
}
}
- return new ShapeableTextCharacters(text.Take(count),
- new GenericTextRunProperties(currentTypeface, defaultProperties.FontRenderingEmSize,
- defaultProperties.TextDecorations, defaultProperties.ForegroundBrush), biDiLevel);
+ return new ShapeableTextCharacters(text.Take(count), defaultProperties.WithTypeface(currentTypeface),
+ biDiLevel);
}
if (previousTypeface is not null)
@@ -94,8 +92,7 @@ namespace Avalonia.Media.TextFormatting
if(TryGetShapeableLength(text, previousTypeface.Value, out count, out _))
{
return new ShapeableTextCharacters(text.Take(count),
- new GenericTextRunProperties(previousTypeface.Value, defaultProperties.FontRenderingEmSize,
- defaultProperties.TextDecorations, defaultProperties.ForegroundBrush), biDiLevel);
+ defaultProperties.WithTypeface(previousTypeface.Value), biDiLevel);
}
}
@@ -124,9 +121,8 @@ namespace Avalonia.Media.TextFormatting
if (matchFound && TryGetShapeableLength(text, currentTypeface, out count, out _))
{
//Fallback found
- return new ShapeableTextCharacters(text.Take(count),
- new GenericTextRunProperties(currentTypeface, defaultProperties.FontRenderingEmSize,
- defaultProperties.TextDecorations, defaultProperties.ForegroundBrush), biDiLevel);
+ return new ShapeableTextCharacters(text.Take(count), defaultProperties.WithTypeface(currentTypeface),
+ biDiLevel);
}
// no fallback found
@@ -148,9 +144,7 @@ namespace Avalonia.Media.TextFormatting
count += grapheme.Text.Length;
}
- return new ShapeableTextCharacters(text.Take(count),
- new GenericTextRunProperties(currentTypeface, defaultProperties.FontRenderingEmSize,
- defaultProperties.TextDecorations, defaultProperties.ForegroundBrush), biDiLevel);
+ return new ShapeableTextCharacters(text.Take(count), defaultProperties, biDiLevel);
}
///
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextRunProperties.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextRunProperties.cs
index 513778b596..99fcbd805f 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextRunProperties.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextRunProperties.cs
@@ -90,5 +90,11 @@ namespace Avalonia.Media.TextFormatting
{
return !Equals(left, right);
}
+
+ internal TextRunProperties WithTypeface(Typeface typeface)
+ {
+ return new GenericTextRunProperties(typeface, FontRenderingEmSize,
+ TextDecorations, ForegroundBrush, BackgroundBrush, BaselineAlignment);
+ }
}
}
diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguage.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguage.cs
index 1db0208310..8ed94f6b20 100644
--- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguage.cs
+++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguage.cs
@@ -39,6 +39,14 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
{
typeSystem.GetType("Avalonia.Metadata.ContentAttribute")
},
+ WhitespaceSignificantCollectionAttributes =
+ {
+ typeSystem.GetType("Avalonia.Metadata.WhitespaceSignificantCollectionAttribute")
+ },
+ TrimSurroundingWhitespaceAttributes =
+ {
+ typeSystem.GetType("Avalonia.Metadata.TrimSurroundingWhitespaceAttribute")
+ },
ProvideValueTarget = typeSystem.GetType("Avalonia.Markup.Xaml.IProvideValueTarget"),
RootObjectProvider = typeSystem.GetType("Avalonia.Markup.Xaml.IRootObjectProvider"),
RootObjectProviderIntermediateRootPropertyName = "IntermediateRootObject",
diff --git a/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs b/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs
index b180a536a5..0ed1f8d2d0 100644
--- a/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs
@@ -1,3 +1,5 @@
+using System;
+using Avalonia.Controls.Documents;
using Avalonia.Data;
using Avalonia.Media;
using Avalonia.Rendering;
@@ -60,5 +62,47 @@ namespace Avalonia.Controls.UnitTests
renderer.Verify(x => x.AddDirty(target), Times.Once);
}
+
+ [Fact]
+ public void Changing_InlinesCollection_Should_Invalidate_Measure()
+ {
+ using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
+ {
+ var target = new TextBlock();
+
+ target.Measure(Size.Infinity);
+
+ Assert.True(target.IsMeasureValid);
+
+ target.Inlines.Add(new Run("Hello"));
+
+ Assert.False(target.IsMeasureValid);
+
+ target.Measure(Size.Infinity);
+
+ Assert.True(target.IsMeasureValid);
+ }
+ }
+
+ [Fact]
+ public void Changing_Inlines_Properties_Should_Invalidate_Measure()
+ {
+ using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
+ {
+ var target = new TextBlock();
+
+ var inline = new Run("Hello");
+
+ target.Inlines.Add(inline);
+
+ target.Measure(Size.Infinity);
+
+ Assert.True(target.IsMeasureValid);
+
+ inline.Text = "1337";
+
+ Assert.False(target.IsMeasureValid);
+ }
+ }
}
}
diff --git a/tests/Avalonia.Controls.UnitTests/WindowTests.cs b/tests/Avalonia.Controls.UnitTests/WindowTests.cs
index 4166242455..eb128ef038 100644
--- a/tests/Avalonia.Controls.UnitTests/WindowTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/WindowTests.cs
@@ -821,7 +821,7 @@ namespace Avalonia.Controls.UnitTests
target.Width = 410;
target.LayoutManager.ExecuteLayoutPass();
- var windowImpl = Mock.Get(ValidatingWindowImpl.Unwrap(target.PlatformImpl));
+ var windowImpl = Mock.Get(target.PlatformImpl);
windowImpl.Verify(x => x.Resize(new Size(410, 800), PlatformResizeReason.Application));
Assert.Equal(410, target.Width);
}
diff --git a/tests/Avalonia.LeakTests/ControlTests.cs b/tests/Avalonia.LeakTests/ControlTests.cs
index eed767e771..087d42370e 100644
--- a/tests/Avalonia.LeakTests/ControlTests.cs
+++ b/tests/Avalonia.LeakTests/ControlTests.cs
@@ -496,7 +496,7 @@ namespace Avalonia.LeakTests
AttachShowAndDetachContextMenu(window);
- Mock.Get(ValidatingWindowImpl.Unwrap(window.PlatformImpl)).Invocations.Clear();
+ Mock.Get(window.PlatformImpl).Invocations.Clear();
dotMemory.Check(memory =>
Assert.Equal(initialMenuCount, memory.GetObjects(where => where.Type.Is()).ObjectsCount));
dotMemory.Check(memory =>
@@ -541,7 +541,7 @@ namespace Avalonia.LeakTests
BuildAndShowContextMenu(window);
BuildAndShowContextMenu(window);
- Mock.Get(ValidatingWindowImpl.Unwrap(window.PlatformImpl)).Invocations.Clear();
+ Mock.Get(window.PlatformImpl).Invocations.Clear();
dotMemory.Check(memory =>
Assert.Equal(initialMenuCount, memory.GetObjects(where => where.Type.Is()).ObjectsCount));
dotMemory.Check(memory =>
diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs
index f20faa2287..e51fff5416 100644
--- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs
+++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs
@@ -935,6 +935,28 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
}
}
+ [Fact]
+ public void Should_Parse_Tip_With_Comment()
+ {
+ var xaml = @"
+
+
+
+
+ Foo
+
+
+ ";
+
+ var textBlock = AvaloniaRuntimeXamlLoader.Parse(xaml);
+
+ var toolTip = ToolTip.GetTip(textBlock) as ToolTip;
+
+ Assert.NotNull(toolTip);
+
+ Assert.Equal("Foo", toolTip.Content);
+ }
+
private class SelectedItemsViewModel : INotifyPropertyChanged
{
public string[] Items { get; set; }
diff --git a/tests/Avalonia.UnitTests/MockTextShaperImpl.cs b/tests/Avalonia.UnitTests/MockTextShaperImpl.cs
index 3018c07819..fc22791102 100644
--- a/tests/Avalonia.UnitTests/MockTextShaperImpl.cs
+++ b/tests/Avalonia.UnitTests/MockTextShaperImpl.cs
@@ -21,7 +21,7 @@ namespace Avalonia.UnitTests
var glyphIndex = typeface.GetGlyph(codepoint);
- shapedBuffer[i] = new GlyphInfo(glyphIndex, glyphCluster);
+ shapedBuffer[i] = new GlyphInfo(glyphIndex, glyphCluster, 10);
i += count;
}