Browse Source

Merge branch 'master' into effectiveViewportChangedListeners

pull/8455/head
Lubomir Tetak 4 years ago
committed by GitHub
parent
commit
f5d5cf56b2
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      samples/ControlCatalog/Pages/TextBlockPage.xaml
  2. 72
      src/Avalonia.Controls/Documents/InlineCollection.cs
  3. 70
      src/Avalonia.Controls/Documents/Span.cs
  4. 18
      src/Avalonia.Controls/Documents/TextElement.cs
  5. 8
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  6. 4
      src/Avalonia.Controls/Primitives/AccessText.cs
  7. 574
      src/Avalonia.Controls/RichTextBlock.cs
  8. 32
      src/Avalonia.Controls/SplitView.cs
  9. 157
      src/Avalonia.Controls/TextBlock.cs
  10. 2
      tests/Avalonia.Base.UnitTests/Styling/SetterTests.cs
  11. 52
      tests/Avalonia.Controls.UnitTests/RichTextBlockTests.cs
  12. 48
      tests/Avalonia.Controls.UnitTests/TextBlockTests.cs

4
samples/ControlCatalog/Pages/TextBlockPage.xaml

@ -118,7 +118,7 @@
</StackPanel>
</Border>
<Border>
<TextBlock Margin="10" TextWrapping="Wrap">
<RichTextBlock Margin="10" TextWrapping="Wrap">
This <Span FontWeight="Bold">is</Span> a
<Span Background="Silver" Foreground="Maroon">TextBlock</Span>
with <Span TextDecorations="Underline">several</Span>
@ -126,7 +126,7 @@
<Span Foreground="Blue">
using a <Bold>variety</Bold> of <Italic>styles</Italic>
</Span>.
</TextBlock>
</RichTextBlock>
</Border>
</WrapPanel>
</StackPanel>

72
src/Avalonia.Controls/Documents/InlineCollection.cs

@ -12,39 +12,55 @@ namespace Avalonia.Controls.Documents
[WhitespaceSignificantCollection]
public class InlineCollection : AvaloniaList<Inline>
{
private readonly IInlineHost? _host;
private ILogical? _parent;
private IInlineHost? _inlineHost;
private string? _text = string.Empty;
/// <summary>
/// Initializes a new instance of the <see cref="InlineCollection"/> class.
/// </summary>
public InlineCollection(ILogical parent) : this(parent, null) { }
/// <summary>
/// Initializes a new instance of the <see cref="InlineCollection"/> class.
/// </summary>
internal InlineCollection(ILogical parent, IInlineHost? host = null) : base(0)
public InlineCollection()
{
_host = host;
ResetBehavior = ResetBehavior.Remove;
this.ForEachItem(
x =>
{
((ISetLogicalParent)x).SetParent(parent);
x.InlineHost = host;
host?.Invalidate();
((ISetLogicalParent)x).SetParent(Parent);
x.InlineHost = InlineHost;
Invalidate();
},
x =>
{
((ISetLogicalParent)x).SetParent(null);
x.InlineHost = host;
host?.Invalidate();
x.InlineHost = InlineHost;
Invalidate();
},
() => throw new NotSupportedException());
}
internal ILogical? Parent
{
get => _parent;
set
{
_parent = value;
OnParentChanged(value);
}
}
internal IInlineHost? InlineHost
{
get => _inlineHost;
set
{
_inlineHost = value;
OnInlineHostChanged(value);
}
}
public bool HasComplexContent => Count > 0;
/// <summary>
@ -61,10 +77,10 @@ namespace Avalonia.Controls.Documents
{
return _text;
}
var builder = new StringBuilder();
foreach(var inline in this)
foreach (var inline in this)
{
inline.AppendText(builder);
}
@ -100,7 +116,7 @@ namespace Avalonia.Controls.Documents
}
else
{
_text += text;
_text = text;
}
}
@ -120,7 +136,7 @@ namespace Avalonia.Controls.Documents
base.Add(new Run(_text));
}
_text = string.Empty;
_text = null;
}
base.Add(item);
@ -136,14 +152,28 @@ namespace Avalonia.Controls.Documents
/// </summary>
protected void Invalidate()
{
if(_host != null)
if(InlineHost != null)
{
_host.Invalidate();
InlineHost.Invalidate();
}
Invalidated?.Invoke(this, EventArgs.Empty);
}
private void Invalidate(object? sender, EventArgs e) => Invalidate();
private void OnParentChanged(ILogical? parent)
{
foreach(var child in this)
{
((ISetLogicalParent)child).SetParent(parent);
}
}
private void OnInlineHostChanged(IInlineHost? inlineHost)
{
foreach (var child in this)
{
child.InlineHost = inlineHost;
}
}
}
}

70
src/Avalonia.Controls/Documents/Span.cs

@ -14,25 +14,27 @@ namespace Avalonia.Controls.Documents
/// <summary>
/// Defines the <see cref="Inlines"/> property.
/// </summary>
public static readonly DirectProperty<Span, InlineCollection> InlinesProperty =
AvaloniaProperty.RegisterDirect<Span, InlineCollection>(
nameof(Inlines),
o => o.Inlines);
public static readonly StyledProperty<InlineCollection> InlinesProperty =
AvaloniaProperty.Register<Span, InlineCollection>(
nameof(Inlines));
/// <summary>
/// Initializes a new instance of a Span element.
/// </summary>
public Span()
{
Inlines = new InlineCollection(this);
Inlines.Invalidated += (s, e) => InlineHost?.Invalidate();
Inlines = new InlineCollection
{
Parent = this
};
}
/// <summary>
/// Gets or sets the inlines.
/// </summary>
[Content]
public InlineCollection Inlines { get; }
public InlineCollection Inlines
{
get => GetValue(InlinesProperty);
set => SetValue(InlinesProperty, value);
}
internal override void BuildTextRun(IList<TextRun> textRuns)
{
@ -52,7 +54,7 @@ namespace Avalonia.Controls.Documents
var textCharacters = new TextCharacters(text.AsMemory(), textRunProperties);
textRuns.Add(textCharacters);
}
}
}
}
@ -65,10 +67,52 @@ namespace Avalonia.Controls.Documents
inline.AppendText(stringBuilder);
}
}
else
{
if (Inlines.Text is string text)
{
stringBuilder.Append(text);
}
}
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
switch (change.Property.Name)
{
case nameof(InlinesProperty):
OnInlinesChanged(change.OldValue as InlineCollection, change.NewValue as InlineCollection);
InlineHost?.Invalidate();
break;
}
}
internal override void OnInlineHostChanged(IInlineHost? oldValue, IInlineHost? newValue)
{
base.OnInlineHostChanged(oldValue, newValue);
if(Inlines is not null)
{
Inlines.InlineHost = newValue;
}
}
private void OnInlinesChanged(InlineCollection? oldValue, InlineCollection? newValue)
{
if (oldValue is not null)
{
oldValue.Parent = null;
oldValue.InlineHost = null;
oldValue.Invalidated -= (s, e) => InlineHost?.Invalidate();
}
if (Inlines.Text is string text)
if (newValue is not null)
{
stringBuilder.Append(text);
newValue.Parent = this;
newValue.InlineHost = InlineHost;
newValue.Invalidated += (s, e) => InlineHost?.Invalidate();
}
}
}

18
src/Avalonia.Controls/Documents/TextElement.cs

@ -67,6 +67,8 @@ namespace Avalonia.Controls.Documents
Brushes.Black,
inherits: true);
private IInlineHost? _inlineHost;
/// <summary>
/// Gets or sets a brush used to paint the control's background.
/// </summary>
@ -250,7 +252,21 @@ namespace Avalonia.Controls.Documents
control.SetValue(ForegroundProperty, value);
}
internal IInlineHost? InlineHost { get; set; }
internal IInlineHost? InlineHost
{
get => _inlineHost;
set
{
var oldValue = _inlineHost;
_inlineHost = value;
OnInlineHostChanged(oldValue, value);
}
}
internal virtual void OnInlineHostChanged(IInlineHost? oldValue, IInlineHost? newValue)
{
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{

8
src/Avalonia.Controls/Presenters/TextPresenter.cs

@ -543,9 +543,11 @@ namespace Avalonia.Controls.Presenters
protected override Size ArrangeOverride(Size finalSize)
{
if (finalSize.Width < TextLayout.Bounds.Width)
var textWidth = Math.Ceiling(TextLayout.Bounds.Width);
if (finalSize.Width < textWidth)
{
finalSize = finalSize.WithWidth(TextLayout.Bounds.Width);
finalSize = finalSize.WithWidth(textWidth);
}
if (MathUtilities.AreClose(_constraint.Width, finalSize.Width))
@ -553,7 +555,7 @@ namespace Avalonia.Controls.Presenters
return finalSize;
}
_constraint = new Size(finalSize.Width, double.PositiveInfinity);
_constraint = new Size(Math.Ceiling(finalSize.Width), double.PositiveInfinity);
_textLayout = null;

4
src/Avalonia.Controls/Primitives/AccessText.cs

@ -79,9 +79,9 @@ namespace Avalonia.Controls.Primitives
}
/// <inheritdoc/>
protected override TextLayout CreateTextLayout(Size constraint, string? text)
protected override TextLayout CreateTextLayout(string? text)
{
return base.CreateTextLayout(constraint, RemoveAccessKeyMarker(text));
return base.CreateTextLayout(RemoveAccessKeyMarker(text));
}
/// <inheritdoc/>

574
src/Avalonia.Controls/RichTextBlock.cs

@ -0,0 +1,574 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Controls.Documents;
using Avalonia.Controls.Utils;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Interactivity;
using Avalonia.Media;
using Avalonia.Media.TextFormatting;
using Avalonia.Metadata;
using Avalonia.Utilities;
namespace Avalonia.Controls
{
/// <summary>
/// A control that displays a block of formatted text.
/// </summary>
public class RichTextBlock : TextBlock, IInlineHost
{
public static readonly StyledProperty<bool> IsTextSelectionEnabledProperty =
AvaloniaProperty.Register<RichTextBlock, bool>(nameof(IsTextSelectionEnabled), false);
public static readonly DirectProperty<RichTextBlock, int> SelectionStartProperty =
AvaloniaProperty.RegisterDirect<RichTextBlock, int>(
nameof(SelectionStart),
o => o.SelectionStart,
(o, v) => o.SelectionStart = v);
public static readonly DirectProperty<RichTextBlock, int> SelectionEndProperty =
AvaloniaProperty.RegisterDirect<RichTextBlock, int>(
nameof(SelectionEnd),
o => o.SelectionEnd,
(o, v) => o.SelectionEnd = v);
public static readonly DirectProperty<RichTextBlock, string> SelectedTextProperty =
AvaloniaProperty.RegisterDirect<RichTextBlock, string>(
nameof(SelectedText),
o => o.SelectedText);
public static readonly StyledProperty<IBrush?> SelectionBrushProperty =
AvaloniaProperty.Register<RichTextBlock, IBrush?>(nameof(SelectionBrush), Brushes.Blue);
public static readonly StyledProperty<IBrush?> SelectionForegroundBrushProperty =
AvaloniaProperty.Register<RichTextBlock, IBrush?>(nameof(SelectionForegroundBrush));
/// <summary>
/// Defines the <see cref="Inlines"/> property.
/// </summary>
public static readonly StyledProperty<InlineCollection> InlinesProperty =
AvaloniaProperty.Register<RichTextBlock, InlineCollection>(
nameof(Inlines));
public static readonly DirectProperty<TextBox, bool> CanCopyProperty =
AvaloniaProperty.RegisterDirect<TextBox, bool>(
nameof(CanCopy),
o => o.CanCopy);
public static readonly RoutedEvent<RoutedEventArgs> CopyingToClipboardEvent =
RoutedEvent.Register<RichTextBlock, RoutedEventArgs>(
nameof(CopyingToClipboard), RoutingStrategies.Bubble);
private bool _canCopy;
private int _selectionStart;
private int _selectionEnd;
static RichTextBlock()
{
FocusableProperty.OverrideDefaultValue(typeof(RichTextBlock), true);
AffectsRender<RichTextBlock>(SelectionStartProperty, SelectionEndProperty, SelectionForegroundBrushProperty, SelectionBrushProperty);
}
public RichTextBlock()
{
Inlines = new InlineCollection
{
Parent = this,
InlineHost = this
};
}
/// <summary>
/// Gets or sets the brush that highlights selected text.
/// </summary>
public IBrush? SelectionBrush
{
get => GetValue(SelectionBrushProperty);
set => SetValue(SelectionBrushProperty, value);
}
/// <summary>
/// Gets or sets a value that defines the brush used for selected text.
/// </summary>
public IBrush? SelectionForegroundBrush
{
get => GetValue(SelectionForegroundBrushProperty);
set => SetValue(SelectionForegroundBrushProperty, value);
}
/// <summary>
/// Gets or sets a character index for the beginning of the current selection.
/// </summary>
public int SelectionStart
{
get => _selectionStart;
set
{
if (SetAndRaise(SelectionStartProperty, ref _selectionStart, value))
{
RaisePropertyChanged(SelectedTextProperty, "", "");
}
}
}
/// <summary>
/// Gets or sets a character index for the end of the current selection.
/// </summary>
public int SelectionEnd
{
get => _selectionEnd;
set
{
if (SetAndRaise(SelectionEndProperty, ref _selectionEnd, value))
{
RaisePropertyChanged(SelectedTextProperty, "", "");
}
}
}
/// <summary>
/// Gets the content of the current selection.
/// </summary>
public string SelectedText
{
get => GetSelection();
}
/// <summary>
/// Gets or sets a value that indicates whether text selection is enabled, either through user action or calling selection-related API.
/// </summary>
public bool IsTextSelectionEnabled
{
get => GetValue(IsTextSelectionEnabledProperty);
set => SetValue(IsTextSelectionEnabledProperty, value);
}
/// <summary>
/// Gets or sets the inlines.
/// </summary>
[Content]
public InlineCollection Inlines
{
get => GetValue(InlinesProperty);
set => SetValue(InlinesProperty, value);
}
/// <summary>
/// Property for determining if the Copy command can be executed.
/// </summary>
public bool CanCopy
{
get => _canCopy;
private set => SetAndRaise(CanCopyProperty, ref _canCopy, value);
}
public event EventHandler<RoutedEventArgs>? CopyingToClipboard
{
add => AddHandler(CopyingToClipboardEvent, value);
remove => RemoveHandler(CopyingToClipboardEvent, value);
}
/// <summary>
/// Copies the current selection to the Clipboard.
/// </summary>
public async void Copy()
{
if (_canCopy || !IsTextSelectionEnabled)
{
return;
}
var text = GetSelection();
if (string.IsNullOrEmpty(text))
{
return;
}
var eventArgs = new RoutedEventArgs(CopyingToClipboardEvent);
RaiseEvent(eventArgs);
if (!eventArgs.Handled)
{
await ((IClipboard)AvaloniaLocator.Current.GetRequiredService(typeof(IClipboard)))
.SetTextAsync(text);
}
}
public override void Render(DrawingContext context)
{
var selectionStart = SelectionStart;
var selectionEnd = SelectionEnd;
var selectionBrush = SelectionBrush;
var selectionEnabled = IsTextSelectionEnabled;
if (selectionEnabled && selectionStart != selectionEnd && selectionBrush != null)
{
var start = Math.Min(selectionStart, selectionEnd);
var length = Math.Max(selectionStart, selectionEnd) - start;
var rects = TextLayout.HitTestTextRange(start, length);
foreach (var rect in rects)
{
context.FillRectangle(selectionBrush, PixelRect.FromRect(rect, 1).ToRect(1));
}
}
base.Render(context);
}
/// <summary>
/// Select all text in the TextBox
/// </summary>
public void SelectAll()
{
if (!IsTextSelectionEnabled)
{
return;
}
var text = Text;
SelectionStart = 0;
SelectionEnd = text?.Length ?? 0;
}
/// <summary>
/// Clears the current selection/>
/// </summary>
public void ClearSelection()
{
if (!IsTextSelectionEnabled)
{
return;
}
SelectionEnd = SelectionStart;
}
protected override string? GetText()
{
return _text ?? Inlines.Text;
}
protected override void SetText(string? text)
{
var oldValue = _text ?? Inlines?.Text;
if (Inlines is not null && Inlines.HasComplexContent)
{
Inlines.Text = text;
_text = null;
}
else
{
_text = text;
}
RaisePropertyChanged(TextProperty, oldValue, text);
}
/// <summary>
/// Creates the <see cref="TextLayout"/> used to render the text.
/// </summary>
/// <returns>A <see cref="TextLayout"/> object.</returns>
protected override TextLayout CreateTextLayout(string? text)
{
var defaultProperties = new GenericTextRunProperties(
new Typeface(FontFamily, FontStyle, FontWeight, FontStretch),
FontSize,
TextDecorations,
Foreground);
var paragraphProperties = new GenericTextParagraphProperties(FlowDirection, TextAlignment, true, false,
defaultProperties, TextWrapping, LineHeight, 0);
ITextSource textSource;
var inlines = Inlines;
if (inlines is not null && inlines.HasComplexContent)
{
var textRuns = new List<TextRun>();
foreach (var inline in inlines)
{
inline.BuildTextRun(textRuns);
}
textSource = new InlinesTextSource(textRuns);
}
else
{
textSource = new SimpleTextSource((text ?? "").AsMemory(), defaultProperties);
}
return new TextLayout(
textSource,
paragraphProperties,
TextTrimming,
_constraint.Width,
_constraint.Height,
maxLines: MaxLines,
lineHeight: LineHeight);
}
protected override void OnLostFocus(RoutedEventArgs e)
{
base.OnLostFocus(e);
ClearSelection();
}
protected override void OnKeyDown(KeyEventArgs e)
{
var handled = false;
var modifiers = e.KeyModifiers;
var keymap = AvaloniaLocator.Current.GetRequiredService<PlatformHotkeyConfiguration>();
bool Match(List<KeyGesture> gestures) => gestures.Any(g => g.Matches(e));
if (Match(keymap.Copy))
{
Copy();
handled = true;
}
e.Handled = handled;
}
protected override void OnPointerPressed(PointerPressedEventArgs e)
{
if (!IsTextSelectionEnabled)
{
return;
}
var text = Text;
var clickInfo = e.GetCurrentPoint(this);
if (text != null && clickInfo.Properties.IsLeftButtonPressed)
{
var point = e.GetPosition(this);
var clickToSelect = e.KeyModifiers.HasFlag(KeyModifiers.Shift);
var oldIndex = SelectionStart;
var hit = TextLayout.HitTestPoint(point);
var index = hit.TextPosition;
SelectionStart = SelectionEnd = index;
#pragma warning disable CS0618 // Type or member is obsolete
switch (e.ClickCount)
#pragma warning restore CS0618 // Type or member is obsolete
{
case 1:
if (clickToSelect)
{
SelectionStart = Math.Min(oldIndex, index);
SelectionEnd = Math.Max(oldIndex, index);
}
else
{
SelectionStart = SelectionEnd = index;
}
break;
case 2:
if (!StringUtils.IsStartOfWord(text, index))
{
SelectionStart = StringUtils.PreviousWord(text, index);
}
SelectionEnd = StringUtils.NextWord(text, index);
break;
case 3:
SelectAll();
break;
}
}
e.Pointer.Capture(this);
e.Handled = true;
}
protected override void OnPointerMoved(PointerEventArgs e)
{
if (!IsTextSelectionEnabled)
{
return;
}
// selection should not change during pointer move if the user right clicks
if (e.Pointer.Captured == this && e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
{
var point = e.GetPosition(this);
point = new Point(
MathUtilities.Clamp(point.X, 0, Math.Max(Bounds.Width - 1, 0)),
MathUtilities.Clamp(point.Y, 0, Math.Max(Bounds.Height - 1, 0)));
var hit = TextLayout.HitTestPoint(point);
SelectionEnd = hit.TextPosition;
}
}
protected override void OnPointerReleased(PointerReleasedEventArgs e)
{
if (!IsTextSelectionEnabled)
{
return;
}
if (e.Pointer.Captured != this)
{
return;
}
if (e.InitialPressMouseButton == MouseButton.Right)
{
var point = e.GetPosition(this);
var hit = TextLayout.HitTestPoint(point);
var caretIndex = hit.TextPosition;
// see if mouse clicked inside current selection
// if it did not, we change the selection to where the user clicked
var firstSelection = Math.Min(SelectionStart, SelectionEnd);
var lastSelection = Math.Max(SelectionStart, SelectionEnd);
var didClickInSelection = SelectionStart != SelectionEnd &&
caretIndex >= firstSelection && caretIndex <= lastSelection;
if (!didClickInSelection)
{
SelectionStart = SelectionEnd = caretIndex;
}
}
e.Pointer.Capture(null);
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
switch (change.Property.Name)
{
case nameof(InlinesProperty):
{
OnInlinesChanged(change.OldValue as InlineCollection, change.NewValue as InlineCollection);
InvalidateTextLayout();
break;
}
case nameof(TextProperty):
{
InvalidateTextLayout();
break;
}
}
}
private string GetSelection()
{
if (!IsTextSelectionEnabled)
{
return "";
}
var text = Inlines.Text ?? Text;
if (string.IsNullOrEmpty(text))
{
return "";
}
var selectionStart = SelectionStart;
var selectionEnd = SelectionEnd;
var start = Math.Min(selectionStart, selectionEnd);
var end = Math.Max(selectionStart, selectionEnd);
if (start == end || text.Length < end)
{
return "";
}
var length = Math.Max(0, end - start);
var selectedText = text.Substring(start, length);
return selectedText;
}
private void OnInlinesChanged(InlineCollection? oldValue, InlineCollection? newValue)
{
if (oldValue is not null)
{
oldValue.Parent = null;
oldValue.InlineHost = null;
oldValue.Invalidated -= (s, e) => InvalidateTextLayout();
}
if (newValue is not null)
{
newValue.Parent = this;
newValue.InlineHost = this;
newValue.Invalidated += (s, e) => InvalidateTextLayout();
}
}
void IInlineHost.AddVisualChild(IControl child)
{
if (child.VisualParent == null)
{
VisualChildren.Add(child);
}
}
void IInlineHost.Invalidate()
{
InvalidateTextLayout();
}
private readonly struct InlinesTextSource : ITextSource
{
private readonly IReadOnlyList<TextRun> _textRuns;
public InlinesTextSource(IReadOnlyList<TextRun> textRuns)
{
_textRuns = textRuns;
}
public TextRun? GetTextRun(int textSourceIndex)
{
var currentPosition = 0;
foreach (var textRun in _textRuns)
{
if (textRun.TextSourceLength == 0)
{
continue;
}
if (currentPosition >= textSourceIndex)
{
return textRun;
}
currentPosition += textRun.TextSourceLength;
}
return null;
}
}
}
}

32
src/Avalonia.Controls/SplitView.cs

@ -431,23 +431,45 @@ namespace Avalonia.Controls
}
}
private string GetPseudoClass(SplitViewDisplayMode mode)
{
return mode switch
{
SplitViewDisplayMode.Inline => "inline",
SplitViewDisplayMode.CompactInline => "compactinline",
SplitViewDisplayMode.Overlay => "overlay",
SplitViewDisplayMode.CompactOverlay => "compactoverlay",
_ => throw new ArgumentOutOfRangeException(nameof(mode), mode, null)
};
}
private string GetPseudoClass(SplitViewPanePlacement placement)
{
return placement switch
{
SplitViewPanePlacement.Left => "left",
SplitViewPanePlacement.Right => "right",
_ => throw new ArgumentOutOfRangeException(nameof(placement), placement, null)
};
}
private void OnPanePlacementChanged(AvaloniaPropertyChangedEventArgs e)
{
var oldState = e.OldValue!.ToString()!.ToLower();
var newState = e.NewValue!.ToString()!.ToLower();
var oldState = GetPseudoClass(e.GetOldValue<SplitViewPanePlacement>());
var newState = GetPseudoClass(e.GetNewValue<SplitViewPanePlacement>());
PseudoClasses.Remove($":{oldState}");
PseudoClasses.Add($":{newState}");
}
private void OnDisplayModeChanged(AvaloniaPropertyChangedEventArgs e)
{
var oldState = e.OldValue!.ToString()!.ToLower();
var newState = e.NewValue!.ToString()!.ToLower();
var oldState = GetPseudoClass(e.GetOldValue<SplitViewDisplayMode>());
var newState = GetPseudoClass(e.GetNewValue<SplitViewDisplayMode>());
PseudoClasses.Remove($":{oldState}");
PseudoClasses.Add($":{newState}");
var (closedPaneWidth, paneColumnGridLength) = (SplitViewDisplayMode)e.NewValue switch
var (closedPaneWidth, paneColumnGridLength) = e.GetNewValue<SplitViewDisplayMode>() switch
{
SplitViewDisplayMode.Overlay => (0, new GridLength(0, GridUnitType.Pixel)),
SplitViewDisplayMode.CompactOverlay => (CompactPaneLength, new GridLength(CompactPaneLength, GridUnitType.Pixel)),

157
src/Avalonia.Controls/TextBlock.cs

@ -1,12 +1,9 @@
using System;
using System.Collections.Generic;
using System.Text;
using Avalonia.Automation.Peers;
using Avalonia.Controls.Documents;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Media.TextFormatting;
using Avalonia.Metadata;
using Avalonia.Utilities;
namespace Avalonia.Controls
@ -14,7 +11,7 @@ namespace Avalonia.Controls
/// <summary>
/// A control that displays a block of text.
/// </summary>
public class TextBlock : Control, IInlineHost
public class TextBlock : Control
{
/// <summary>
/// Defines the <see cref="Background"/> property.
@ -101,14 +98,6 @@ namespace Avalonia.Controls
o => o.Text,
(o, v) => o.Text = v);
/// <summary>
/// Defines the <see cref="Inlines"/> property.
/// </summary>
public static readonly DirectProperty<TextBlock, InlineCollection> InlinesProperty =
AvaloniaProperty.RegisterDirect<TextBlock, InlineCollection>(
nameof(Inlines),
o => o.Inlines);
/// <summary>
/// Defines the <see cref="TextAlignment"/> property.
/// </summary>
@ -139,8 +128,9 @@ namespace Avalonia.Controls
public static readonly StyledProperty<TextDecorationCollection?> TextDecorationsProperty =
AvaloniaProperty.Register<TextBlock, TextDecorationCollection?>(nameof(TextDecorations));
private TextLayout? _textLayout;
private Size _constraint;
protected string? _text;
protected TextLayout? _textLayout;
protected Size _constraint;
/// <summary>
/// Initializes static members of the <see cref="TextBlock"/> class.
@ -152,14 +142,6 @@ namespace Avalonia.Controls
AffectsRender<TextBlock>(BackgroundProperty, ForegroundProperty);
}
/// <summary>
/// Initializes a new instance of the <see cref="TextBlock"/> class.
/// </summary>
public TextBlock()
{
Inlines = new InlineCollection(this, this);
}
/// <summary>
/// Gets the <see cref="TextLayout"/> used to render the text.
/// </summary>
@ -167,7 +149,7 @@ namespace Avalonia.Controls
{
get
{
return _textLayout ?? (_textLayout = CreateTextLayout(_constraint, Text));
return _textLayout ??= CreateTextLayout(_text);
}
}
@ -194,28 +176,10 @@ namespace Avalonia.Controls
/// </summary>
public string? Text
{
get => Inlines.Text;
set
{
var old = Text;
if (value == old)
{
return;
}
Inlines.Text = value;
RaisePropertyChanged(TextProperty, old, value);
}
get => GetText();
set => SetText(value);
}
/// <summary>
/// Gets the inlines.
/// </summary>
[Content]
public InlineCollection Inlines { get; }
/// <summary>
/// Gets or sets the font family used to draw the control's text.
/// </summary>
@ -509,6 +473,10 @@ namespace Avalonia.Controls
control.SetValue(MaxLinesProperty, maxLines);
}
public void Add(string text)
{
_text = text;
}
/// <summary>
/// Renders the <see cref="TextBlock"/> to a drawing context.
@ -544,13 +512,21 @@ namespace Avalonia.Controls
TextLayout.Draw(context, new Point(padding.Left, top));
}
protected virtual string? GetText()
{
return _text;
}
protected virtual void SetText(string? text)
{
SetAndRaise(TextProperty, ref _text, text);
}
/// <summary>
/// Creates the <see cref="TextLayout"/> used to render the text.
/// </summary>
/// <param name="constraint">The constraint of the text.</param>
/// <param name="text">The text to format.</param>
/// <returns>A <see cref="TextLayout"/> object.</returns>
protected virtual TextLayout CreateTextLayout(Size constraint, string? text)
protected virtual TextLayout CreateTextLayout(string? text)
{
var defaultProperties = new GenericTextRunProperties(
new Typeface(FontFamily, FontStyle, FontWeight, FontStretch),
@ -561,30 +537,12 @@ namespace Avalonia.Controls
var paragraphProperties = new GenericTextParagraphProperties(FlowDirection, TextAlignment, true, false,
defaultProperties, TextWrapping, LineHeight, 0);
ITextSource textSource;
if (Inlines.HasComplexContent)
{
var textRuns = new List<TextRun>();
foreach (var inline in Inlines)
{
inline.BuildTextRun(textRuns);
}
textSource = new InlinesTextSource(textRuns);
}
else
{
textSource = new SimpleTextSource((text ?? "").AsMemory(), defaultProperties);
}
return new TextLayout(
textSource,
new SimpleTextSource((text ?? "").AsMemory(), defaultProperties),
paragraphProperties,
TextTrimming,
constraint.Width,
constraint.Height,
_constraint.Width,
_constraint.Height,
maxLines: MaxLines,
lineHeight: LineHeight);
}
@ -601,11 +559,6 @@ namespace Avalonia.Controls
protected override Size MeasureOverride(Size availableSize)
{
if (!Inlines.HasComplexContent && string.IsNullOrEmpty(Text))
{
return new Size();
}
var scale = LayoutHelper.GetLayoutScale(this);
var padding = LayoutHelper.RoundLayoutThickness(Padding, scale, scale);
@ -623,9 +576,11 @@ namespace Avalonia.Controls
protected override Size ArrangeOverride(Size finalSize)
{
if(finalSize.Width < TextLayout.Bounds.Width)
var textWidth = Math.Ceiling(TextLayout.Bounds.Width);
if(finalSize.Width < textWidth)
{
finalSize = finalSize.WithWidth(TextLayout.Bounds.Width);
finalSize = finalSize.WithWidth(textWidth);
}
if (MathUtilities.AreClose(_constraint.Width, finalSize.Width))
@ -637,7 +592,7 @@ namespace Avalonia.Controls
var padding = LayoutHelper.RoundLayoutThickness(Padding, scale, scale);
_constraint = new Size(finalSize.Deflate(padding).Width, double.PositiveInfinity);
_constraint = new Size(Math.Ceiling(finalSize.Deflate(padding).Width), double.PositiveInfinity);
_textLayout = null;
@ -685,57 +640,7 @@ namespace Avalonia.Controls
}
}
private void InlinesChanged(object? sender, EventArgs e)
{
InvalidateTextLayout();
}
void IInlineHost.AddVisualChild(IControl child)
{
if (child.VisualParent == null)
{
VisualChildren.Add(child);
}
}
void IInlineHost.Invalidate()
{
InvalidateTextLayout();
}
private readonly struct InlinesTextSource : ITextSource
{
private readonly IReadOnlyList<TextRun> _textRuns;
public InlinesTextSource(IReadOnlyList<TextRun> textRuns)
{
_textRuns = textRuns;
}
public TextRun? GetTextRun(int textSourceIndex)
{
var currentPosition = 0;
foreach (var textRun in _textRuns)
{
if(textRun.TextSourceLength == 0)
{
continue;
}
if(currentPosition >= textSourceIndex)
{
return textRun;
}
currentPosition += textRun.TextSourceLength;
}
return null;
}
}
private readonly struct SimpleTextSource : ITextSource
protected readonly struct SimpleTextSource : ITextSource
{
private readonly ReadOnlySlice<char> _text;
private readonly TextRunProperties _defaultProperties;

2
tests/Avalonia.Base.UnitTests/Styling/SetterTests.cs

@ -49,7 +49,7 @@ namespace Avalonia.Base.UnitTests.Styling
setter.Instance(control).Start(false);
Assert.Equal("", control.Text);
Assert.Equal(null, control.Text);
}
[Fact]

52
tests/Avalonia.Controls.UnitTests/RichTextBlockTests.cs

@ -0,0 +1,52 @@
using Avalonia.Controls.Documents;
using Avalonia.Media;
using Avalonia.UnitTests;
using Xunit;
namespace Avalonia.Controls.UnitTests
{
public class RichTextBlockTests
{
[Fact]
public void Changing_InlinesCollection_Should_Invalidate_Measure()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
var target = new RichTextBlock();
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 RichTextBlock();
var inline = new Run("Hello");
target.Inlines.Add(inline);
target.Measure(Size.Infinity);
Assert.True(target.IsMeasureValid);
inline.Foreground = Brushes.Green;
Assert.False(target.IsMeasureValid);
}
}
}
}

48
tests/Avalonia.Controls.UnitTests/TextBlockTests.cs

@ -20,13 +20,11 @@ namespace Avalonia.Controls.UnitTests
}
[Fact]
public void Default_Text_Value_Should_Be_EmptyString()
public void Default_Text_Value_Should_Be_Null()
{
var textBlock = new TextBlock();
Assert.Equal(
"",
textBlock.Text);
Assert.Equal(null, textBlock.Text);
}
[Fact]
@ -62,47 +60,5 @@ 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);
}
}
}
}

Loading…
Cancel
Save