Browse Source

Merge remote-tracking branch 'upstream/master' into addWasmMobileSandbox

pull/11682/head
Benedikt Stebner 3 years ago
parent
commit
2ff2b6ada1
  1. 5
      samples/RenderDemo/Pages/CustomStringAnimator.cs
  2. 94
      src/Avalonia.Base/Animation/Animation.AnimatorRegistry.cs
  3. 59
      src/Avalonia.Base/Animation/Animation.cs
  4. 30
      src/Avalonia.Base/Animation/ICustomAnimator.cs
  5. 7
      src/Avalonia.Base/CornerRadius.cs
  6. 2
      src/Avalonia.Base/Input/TouchDevice.cs
  7. 6
      src/Avalonia.Base/Media/BoxShadow.cs
  8. 6
      src/Avalonia.Base/Media/BoxShadows.cs
  9. 5
      src/Avalonia.Base/Media/Brush.cs
  10. 7
      src/Avalonia.Base/Media/Color.cs
  11. 2
      src/Avalonia.Base/Media/Effects/EffectAnimator.cs
  12. 13
      src/Avalonia.Base/Media/GlyphRun.cs
  13. 37
      src/Avalonia.Base/Media/MediaContext.Clock.cs
  14. 3
      src/Avalonia.Base/Media/MediaContext.Compositor.cs
  15. 3
      src/Avalonia.Base/Media/MediaContext.cs
  16. 2
      src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
  17. 22
      src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
  18. 131
      src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
  19. 6
      src/Avalonia.Base/Media/Transform.cs
  20. 7
      src/Avalonia.Base/Point.cs
  21. 5
      src/Avalonia.Base/Rect.cs
  22. 7
      src/Avalonia.Base/RelativePoint.cs
  23. 4
      src/Avalonia.Base/Rendering/Composition/Compositor.cs
  24. 5
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs
  25. 10
      src/Avalonia.Base/Rendering/Composition/Server/ServerRenderResource.cs
  26. 7
      src/Avalonia.Base/Size.cs
  27. 7
      src/Avalonia.Base/Thickness.cs
  28. 7
      src/Avalonia.Base/Vector.cs
  29. 40
      src/Avalonia.Controls.ColorPicker/ColorPalettes/FlatColorPalette.cs
  30. 18
      src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs
  31. 10
      src/Avalonia.Controls.DataGrid/DataGridColumn.cs
  32. 19
      src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs
  33. 12
      src/Avalonia.Controls/Platform/ManagedDispatcherImpl.cs
  34. 116
      src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.Framebuffer.cs
  35. 365
      src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs
  36. 47
      src/Avalonia.Controls/ToggleSwitch.cs
  37. 12
      src/Avalonia.Controls/TopLevel.cs
  38. 19
      src/Avalonia.Controls/TreeView.cs
  39. 8
      src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs
  40. 2
      src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs
  41. 1
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs
  42. 20
      src/Avalonia.Themes.Fluent/Controls/ToggleSwitch.xaml
  43. 13
      src/Avalonia.Themes.Simple/Controls/ToggleSwitch.xaml
  44. 2
      src/tools/Avalonia.Designer.HostApp/Avalonia.Designer.HostApp.csproj
  45. 15
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs
  46. 33
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs

5
samples/RenderDemo/Pages/CustomStringAnimator.cs

@ -1,9 +1,10 @@
using Avalonia.Animation;
using System;
using Avalonia.Animation;
using Avalonia.Animation.Animators;
namespace RenderDemo.Pages
{
public class CustomStringAnimator : CustomAnimatorBase<string>
public class CustomStringAnimator : InterpolatingAnimator<string>
{
public override string Interpolate(double progress, string oldValue, string newValue)
{

94
src/Avalonia.Base/Animation/Animation.AnimatorRegistry.cs

@ -0,0 +1,94 @@
using System;
using System.Collections.Generic;
using Avalonia.Animation.Animators;
using Avalonia.Media;
namespace Avalonia.Animation;
partial class Animation
{
/// <summary>
/// Sets the value of the Animator attached property for a setter.
/// </summary>
/// <param name="setter">The animation setter.</param>
/// <param name="value">The property animator value.</param>
[Obsolete("CustomAnimatorBase will be removed before 11.0, use InterpolatingAnimator<T>", true)]
public static void SetAnimator(IAnimationSetter setter, CustomAnimatorBase value)
{
s_animators[setter] = (value.WrapperType, value.CreateWrapper);
}
/// <summary>
/// Sets the value of the Animator attached property for a setter.
/// </summary>
/// <param name="setter">The animation setter.</param>
/// <param name="value">The property animator value.</param>
public static void SetAnimator(IAnimationSetter setter, ICustomAnimator value)
{
s_animators[setter] = (value.WrapperType, value.CreateWrapper);
}
private readonly static List<(Func<AvaloniaProperty, bool> Condition, Type Animator, Func<IAnimator> Factory)>
Animators = new()
{
(prop =>(typeof(double).IsAssignableFrom(prop.PropertyType) && typeof(Transform).IsAssignableFrom(prop.OwnerType)),
typeof(TransformAnimator), () => new TransformAnimator()),
(prop => typeof(bool).IsAssignableFrom(prop.PropertyType), typeof(BoolAnimator), () => new BoolAnimator()),
(prop => typeof(byte).IsAssignableFrom(prop.PropertyType), typeof(ByteAnimator), () => new ByteAnimator()),
(prop => typeof(Int16).IsAssignableFrom(prop.PropertyType), typeof(Int16Animator), () => new Int16Animator()),
(prop => typeof(Int32).IsAssignableFrom(prop.PropertyType), typeof(Int32Animator), () => new Int32Animator()),
(prop => typeof(Int64).IsAssignableFrom(prop.PropertyType), typeof(Int64Animator), () => new Int64Animator()),
(prop => typeof(UInt16).IsAssignableFrom(prop.PropertyType), typeof(UInt16Animator), () => new UInt16Animator()),
(prop => typeof(UInt32).IsAssignableFrom(prop.PropertyType), typeof(UInt32Animator), () => new UInt32Animator()),
(prop => typeof(UInt64).IsAssignableFrom(prop.PropertyType), typeof(UInt64Animator), () => new UInt64Animator()),
(prop => typeof(float).IsAssignableFrom(prop.PropertyType), typeof(FloatAnimator), () => new FloatAnimator()),
(prop => typeof(double).IsAssignableFrom(prop.PropertyType), typeof(DoubleAnimator), () => new DoubleAnimator()),
(prop => typeof(decimal).IsAssignableFrom(prop.PropertyType), typeof(DecimalAnimator), () => new DecimalAnimator()),
};
static Animation()
{
RegisterAnimator<IEffect?, EffectAnimator>();
RegisterAnimator<BoxShadow, BoxShadowAnimator>();
RegisterAnimator<BoxShadows, BoxShadowsAnimator>();
RegisterAnimator<IBrush?, BaseBrushAnimator>();
RegisterAnimator<CornerRadius, CornerRadiusAnimator>();
RegisterAnimator<Color, ColorAnimator>();
RegisterAnimator<Vector, VectorAnimator>();
RegisterAnimator<Point, PointAnimator>();
RegisterAnimator<Rect, RectAnimator>();
RegisterAnimator<RelativePoint, RelativePointAnimator>();
RegisterAnimator<Size, SizeAnimator>();
RegisterAnimator<Thickness, ThicknessAnimator>();
}
/// <summary>
/// Registers a <see cref="Animator{T}"/> that can handle
/// a value type that matches the specified condition.
/// </summary>
static void RegisterAnimator<T, TAnimator>()
where TAnimator : Animator<T>, new()
{
Animators.Insert(0,
(prop => typeof(T).IsAssignableFrom(prop.PropertyType), typeof(TAnimator), () => new TAnimator()));
}
public static void RegisterCustomAnimator<T, TAnimator>() where TAnimator : InterpolatingAnimator<T>, new()
{
Animators.Insert(0, (prop => typeof(T).IsAssignableFrom(prop.PropertyType),
typeof(InterpolatingAnimator<T>.AnimatorWrapper), () => new TAnimator().CreateWrapper()));
}
private static (Type Type, Func<IAnimator> Factory)? GetAnimatorType(AvaloniaProperty property)
{
foreach (var (condition, type, factory) in Animators)
{
if (condition(property))
{
return (type, factory);
}
}
return null;
}
}

59
src/Avalonia.Base/Animation/Animation.cs

@ -1,12 +1,9 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Avalonia.Reactive;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Animation.Animators;
using Avalonia.Animation.Easings;
using Avalonia.Data;
using Avalonia.Metadata;
@ -16,7 +13,7 @@ namespace Avalonia.Animation
/// <summary>
/// Tracks the progress of an animation.
/// </summary>
public sealed class Animation : AvaloniaObject, IAnimation
public sealed partial class Animation : AvaloniaObject, IAnimation
{
/// <summary>
/// Defines the <see cref="Duration"/> property.
@ -195,60 +192,6 @@ namespace Avalonia.Animation
return null;
}
/// <summary>
/// Sets the value of the Animator attached property for a setter.
/// </summary>
/// <param name="setter">The animation setter.</param>
/// <param name="value">The property animator value.</param>
public static void SetAnimator(IAnimationSetter setter, CustomAnimatorBase value)
{
s_animators[setter] = (value.WrapperType, value.CreateWrapper);
}
private readonly static List<(Func<AvaloniaProperty, bool> Condition, Type Animator, Func<IAnimator> Factory)> Animators = new()
{
( prop => typeof(bool).IsAssignableFrom(prop.PropertyType), typeof(BoolAnimator), () => new BoolAnimator() ),
( prop => typeof(byte).IsAssignableFrom(prop.PropertyType), typeof(ByteAnimator), () => new ByteAnimator() ),
( prop => typeof(Int16).IsAssignableFrom(prop.PropertyType), typeof(Int16Animator), () => new Int16Animator() ),
( prop => typeof(Int32).IsAssignableFrom(prop.PropertyType), typeof(Int32Animator), () => new Int32Animator() ),
( prop => typeof(Int64).IsAssignableFrom(prop.PropertyType), typeof(Int64Animator), () => new Int64Animator() ),
( prop => typeof(UInt16).IsAssignableFrom(prop.PropertyType), typeof(UInt16Animator), () => new UInt16Animator() ),
( prop => typeof(UInt32).IsAssignableFrom(prop.PropertyType), typeof(UInt32Animator), () => new UInt32Animator() ),
( prop => typeof(UInt64).IsAssignableFrom(prop.PropertyType), typeof(UInt64Animator), () => new UInt64Animator() ),
( prop => typeof(float).IsAssignableFrom(prop.PropertyType), typeof(FloatAnimator), () => new FloatAnimator() ),
( prop => typeof(double).IsAssignableFrom(prop.PropertyType), typeof(DoubleAnimator), () => new DoubleAnimator() ),
( prop => typeof(decimal).IsAssignableFrom(prop.PropertyType), typeof(DecimalAnimator), () => new DecimalAnimator() ),
};
/// <summary>
/// Registers a <see cref="Animator{T}"/> that can handle
/// a value type that matches the specified condition.
/// </summary>
/// <param name="condition">
/// The condition to which the <see cref="Animator{T}"/>
/// is to be activated and used.
/// </param>
/// <typeparam name="TAnimator">
/// The type of the animator to instantiate.
/// </typeparam>
internal static void RegisterAnimator<TAnimator>(Func<AvaloniaProperty, bool> condition)
where TAnimator : IAnimator, new()
{
Animators.Insert(0, (condition, typeof(TAnimator), () => new TAnimator()));
}
private static (Type Type, Func<IAnimator> Factory)? GetAnimatorType(AvaloniaProperty property)
{
foreach (var (condition, type, factory) in Animators)
{
if (condition(property))
{
return (type, factory);
}
}
return null;
}
private (IList<IAnimator> Animators, IList<IDisposable> subscriptions) InterpretKeyframes(Animatable control)
{
var handlerList = new Dictionary<(Type type, AvaloniaProperty Property), Func<IAnimator>>();

30
src/Avalonia.Base/Animation/ICustomAnimator.cs

@ -1,14 +1,15 @@
using System;
using Avalonia.Animation.Animators;
namespace Avalonia.Animation;
[Obsolete("This class will be removed before 11.0, use InterpolatingAnimator<T>", true)]
public abstract class CustomAnimatorBase
{
internal abstract IAnimator CreateWrapper();
internal abstract Type WrapperType { get; }
}
[Obsolete("This class will be removed before 11.0, use InterpolatingAnimator<T>", true)]
public abstract class CustomAnimatorBase<T> : CustomAnimatorBase
{
public abstract T Interpolate(double progress, T oldValue, T newValue);
@ -25,6 +26,33 @@ public abstract class CustomAnimatorBase<T> : CustomAnimatorBase
_parent = parent;
}
public override T Interpolate(double progress, T oldValue, T newValue) => _parent.Interpolate(progress, oldValue, newValue);
}
}
public interface ICustomAnimator
{
internal IAnimator CreateWrapper();
internal Type WrapperType { get; }
}
public abstract class InterpolatingAnimator<T> : ICustomAnimator
{
public abstract T Interpolate(double progress, T oldValue, T newValue);
Type ICustomAnimator.WrapperType => typeof(AnimatorWrapper);
IAnimator ICustomAnimator.CreateWrapper() => new AnimatorWrapper(this);
internal IAnimator CreateWrapper() => new AnimatorWrapper(this);
internal class AnimatorWrapper : Animator<T>
{
private readonly InterpolatingAnimator<T> _parent;
public AnimatorWrapper(InterpolatingAnimator<T> parent)
{
_parent = parent;
}
public override T Interpolate(double progress, T oldValue, T newValue) => _parent.Interpolate(progress, oldValue, newValue);
}
}

7
src/Avalonia.Base/CornerRadius.cs

@ -15,13 +15,6 @@ namespace Avalonia
#endif
readonly struct CornerRadius : IEquatable<CornerRadius>
{
static CornerRadius()
{
#if !BUILDTASK
Animation.Animation.RegisterAnimator<CornerRadiusAnimator>(prop => typeof(CornerRadius).IsAssignableFrom(prop.PropertyType));
#endif
}
public CornerRadius(double uniformRadius)
{
TopLeft = TopRight = BottomLeft = BottomRight = uniformRadius;

2
src/Avalonia.Base/Input/TouchDevice.cs

@ -51,7 +51,7 @@ namespace Avalonia.Input
pointer.Capture(hit);
}
var target = pointer.Captured ?? args.Root;
var target = pointer.Captured ?? args.InputHitTestResult ?? args.Root;
var gestureTarget = pointer.CapturedGestureRecognizer?.Target;
var updateKind = args.Type.ToUpdateKind();
var keyModifier = args.InputModifiers.ToKeyModifiers();

6
src/Avalonia.Base/Media/BoxShadow.cs

@ -16,12 +16,6 @@ namespace Avalonia.Media
public Color Color { get; set; }
public bool IsInset { get; set; }
static BoxShadow()
{
Animation.Animation.RegisterAnimator<BoxShadowAnimator>(prop =>
typeof(BoxShadow).IsAssignableFrom(prop.PropertyType));
}
public bool Equals(in BoxShadow other)
{
return OffsetX.Equals(other.OffsetX) && OffsetY.Equals(other.OffsetY) && Blur.Equals(other.Blur) && Spread.Equals(other.Spread) && Color.Equals(other.Color);

6
src/Avalonia.Base/Media/BoxShadows.cs

@ -10,12 +10,6 @@ namespace Avalonia.Media
private readonly BoxShadow _first;
private readonly BoxShadow[]? _list;
public int Count { get; }
static BoxShadows()
{
Animation.Animation.RegisterAnimator<BoxShadowsAnimator>(prop =>
typeof(BoxShadows).IsAssignableFrom(prop.PropertyType));
}
public BoxShadows(BoxShadow shadow)
{

5
src/Avalonia.Base/Media/Brush.cs

@ -34,11 +34,6 @@ namespace Avalonia.Media
/// </summary>
public static readonly StyledProperty<RelativePoint> TransformOriginProperty =
AvaloniaProperty.Register<Brush, RelativePoint>(nameof(TransformOrigin));
static Brush()
{
Animation.Animation.RegisterAnimator<BaseBrushAnimator>(prop => typeof(IBrush).IsAssignableFrom(prop.PropertyType));
}
/// <summary>
/// Gets or sets the opacity of the brush.

7
src/Avalonia.Base/Media/Color.cs

@ -25,13 +25,6 @@ namespace Avalonia.Media
{
private const double byteToDouble = 1.0 / 255;
static Color()
{
#if !BUILDTASK
Animation.Animation.RegisterAnimator<ColorAnimator>(prop => typeof(Color).IsAssignableFrom(prop.PropertyType));
#endif
}
/// <summary>
/// Gets the Alpha component of the color.
/// </summary>

2
src/Avalonia.Base/Media/Effects/EffectAnimator.cs

@ -63,8 +63,6 @@ internal class EffectAnimator : Animator<IEffect?>
if(s_Registered)
return;
s_Registered = true;
Animation.RegisterAnimator<EffectAnimator>(prop =>
typeof(IEffect).IsAssignableFrom(prop.PropertyType));
}
}

13
src/Avalonia.Base/Media/GlyphRun.cs

@ -643,12 +643,13 @@ namespace Avalonia.Media
lastCluster = _glyphInfos[_glyphInfos.Count - 1].GlyphCluster;
}
var isReversed = firstCluster > lastCluster;
if (!IsLeftToRight)
{
(lastCluster, firstCluster) = (firstCluster, lastCluster);
}
var isReversed = firstCluster > lastCluster;
var height = GlyphTypeface.Metrics.LineSpacing * Scale;
var widthIncludingTrailingWhitespace = 0d;
@ -766,15 +767,13 @@ namespace Avalonia.Media
if (!charactersSpan.IsEmpty)
{
var characterIndex = 0;
var characterIndex = charactersSpan.Length - 1;
for (var i = 0; i < _glyphInfos.Count; i++)
{
var currentCluster = _glyphInfos[i].GlyphCluster;
var codepoint = Codepoint.ReadAt(charactersSpan, characterIndex, out var characterLength);
characterIndex += characterLength;
if (!codepoint.IsWhiteSpace)
{
break;
@ -784,9 +783,9 @@ namespace Avalonia.Media
var j = i;
while (j - 1 >= 0)
while (j + 1 < _glyphInfos.Count)
{
var nextCluster = _glyphInfos[--j].GlyphCluster;
var nextCluster = _glyphInfos[++j].GlyphCluster;
if (currentCluster == nextCluster)
{
@ -798,6 +797,8 @@ namespace Avalonia.Media
break;
}
characterIndex -= clusterLength;
if (codepoint.IsBreakChar)
{
newLineLength += clusterLength;

37
src/Avalonia.Base/Media/MediaContext.Clock.cs

@ -4,6 +4,7 @@ using System.Diagnostics;
using Avalonia.Animation;
using Avalonia.Reactive;
using Avalonia.Threading;
using Avalonia.Utilities;
namespace Avalonia.Media;
@ -17,8 +18,12 @@ internal partial class MediaContext
{
private readonly MediaContext _parent;
private List<IObserver<TimeSpan>> _observers = new();
public bool HasNewSubscriptions { get; set; }
public bool HasSubscriptions => _observers.Count > 0;
private List<IObserver<TimeSpan>> _newObservers = new();
private Queue<Action<TimeSpan>> _queuedAnimationFrames = new();
private Queue<Action<TimeSpan>> _queuedAnimationFramesNext = new();
private TimeSpan _currentAnimationTimestamp;
public bool HasNewSubscriptions => _newObservers.Count > 0;
public bool HasSubscriptions => _observers.Count > 0 || _queuedAnimationFrames.Count > 0;
public MediaContextClock(MediaContext parent)
{
@ -29,19 +34,41 @@ internal partial class MediaContext
{
_parent.ScheduleRender(false);
Dispatcher.UIThread.VerifyAccess();
HasNewSubscriptions = true;
_observers.Add(observer);
_newObservers.Add(observer);
return Disposable.Create(() =>
{
Dispatcher.UIThread.VerifyAccess();
_observers.Remove(observer);
});
}
public void RequestAnimationFrame(Action<TimeSpan> action)
{
_parent.ScheduleRender(false);
_queuedAnimationFrames.Enqueue(action);
}
public void Pulse(TimeSpan now)
{
_newObservers.Clear();
_currentAnimationTimestamp = now;
// We are swapping the queues before enumeration
(_queuedAnimationFrames, _queuedAnimationFramesNext) = (_queuedAnimationFramesNext, _queuedAnimationFrames);
var animationFrames = _queuedAnimationFramesNext;
while (animationFrames.TryDequeue(out var callback))
callback(now);
foreach (var observer in _observers.ToArray())
observer.OnNext(now);
observer.OnNext(_currentAnimationTimestamp);
}
public void PulseNewSubscriptions()
{
foreach (var observer in _newObservers.ToArray())
observer.OnNext(_currentAnimationTimestamp);
_newObservers.Clear();
}
public PlayState PlayState
@ -50,4 +77,6 @@ internal partial class MediaContext
set => throw new InvalidOperationException();
}
}
public void RequestAnimationFrame(Action<TimeSpan> action) => _clock.RequestAnimationFrame(action);
}

3
src/Avalonia.Base/Media/MediaContext.Compositor.cs

@ -1,4 +1,5 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Avalonia.Platform;
using Avalonia.Rendering.Composition;
@ -78,7 +79,7 @@ partial class MediaContext
// Nothing to do, and there are no pending commits
return false;
foreach (var c in _requestedCommits)
foreach (var c in _requestedCommits.ToArray())
CommitCompositor(c);
_requestedCommits.Clear();

3
src/Avalonia.Base/Media/MediaContext.cs

@ -131,12 +131,11 @@ internal partial class MediaContext : ICompositorScheduler
// We are doing several iterations when it happens
for (var c = 0; c < 10; c++)
{
_clock.HasNewSubscriptions = false;
FireInvokeOnRenderCallbacks();
if (_clock.HasNewSubscriptions)
{
_clock.Pulse(now);
_clock.PulseNewSubscriptions();
continue;
}

2
src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs

@ -684,7 +684,9 @@ namespace Avalonia.Media.TextFormatting
var textRuns = new TextRun[] { new ShapedTextRun(shapedBuffer, properties) };
var line = new TextLineImpl(textRuns, firstTextSourceIndex, 0, paragraphWidth, paragraphProperties, flowDirection);
line.FinalizeLine();
return line;
}

22
src/Avalonia.Base/Media/TextFormatting/TextLayout.cs

@ -128,7 +128,7 @@ namespace Avalonia.Media.TextFormatting
/// <summary>
/// Gets the text spacing.
/// </summary>
public double LetterSpacing => _paragraphProperties.LetterSpacing;
public double LetterSpacing => _paragraphProperties.LetterSpacing;
/// <summary>
/// Gets the text lines.
@ -271,11 +271,13 @@ namespace Avalonia.Media.TextFormatting
var currentY = 0.0;
foreach (var textLine in _textLines)
for (var i = 0; i < _textLines.Length; i++)
{
var textLine = _textLines[i];
var end = textLine.FirstTextSourceIndex + textLine.Length;
if (end <= textPosition && end < _textSourceLength)
if (end <= textPosition && i + 1 < _textLines.Length)
{
currentY += textLine.Height;
@ -511,7 +513,7 @@ namespace Avalonia.Media.TextFormatting
{
var textLine = TextFormatterImpl.CreateEmptyTextLine(0, double.PositiveInfinity, _paragraphProperties);
UpdateMetrics(textLine, ref lineStartOfLongestLine, ref origin, ref first,
UpdateMetrics(textLine, ref lineStartOfLongestLine, ref origin, ref first,
ref accBlackBoxLeft, ref accBlackBoxTop, ref accBlackBoxRight, ref accBlackBoxBottom);
return new TextLine[] { textLine };
@ -638,13 +640,13 @@ namespace Avalonia.Media.TextFormatting
}
private void UpdateMetrics(
TextLine currentLine,
ref double lineStartOfLongestLine,
ref Point origin,
ref bool first,
TextLine currentLine,
ref double lineStartOfLongestLine,
ref Point origin,
ref bool first,
ref double accBlackBoxLeft,
ref double accBlackBoxTop,
ref double accBlackBoxRight,
ref double accBlackBoxTop,
ref double accBlackBoxRight,
ref double accBlackBoxBottom)
{
var blackBoxLeft = origin.X + currentLine.Start + currentLine.OverhangLeading;

131
src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs

@ -371,14 +371,16 @@ namespace Avalonia.Media.TextFormatting
IndexedTextRun currentIndexedRun = _indexedTextRuns[i];
while(currentIndexedRun.TextSourceCharacterIndex != currentPosition)
while (currentIndexedRun.TextSourceCharacterIndex != currentPosition)
{
if(i + 1 < _indexedTextRuns.Count)
if (i + 1 == _indexedTextRuns.Count)
{
i++;
currentIndexedRun = _indexedTextRuns[i];
break;
}
i++;
currentIndexedRun = _indexedTextRuns[i];
}
return currentIndexedRun;
@ -430,7 +432,7 @@ namespace Avalonia.Media.TextFormatting
if (currentTextRun == null)
{
return 0;
return Start;
}
var directionalWidth = 0.0;
@ -584,6 +586,8 @@ namespace Avalonia.Media.TextFormatting
var currentPosition = FirstTextSourceIndex;
var remainingLength = textLength;
TextBounds? lastBounds = null;
static FlowDirection GetDirection(TextRun textRun, FlowDirection currentDirection)
{
if (textRun is ShapedTextRun shapedTextRun)
@ -604,12 +608,14 @@ namespace Avalonia.Media.TextFormatting
while (currentIndexedRun.TextSourceCharacterIndex != currentPosition)
{
if (i + 1 < _indexedTextRuns.Count)
if (i + 1 == _indexedTextRuns.Count)
{
i++;
currentIndexedRun = _indexedTextRuns[i];
break;
}
i++;
currentIndexedRun = _indexedTextRuns[i];
}
return currentIndexedRun;
@ -632,6 +638,40 @@ namespace Avalonia.Media.TextFormatting
return distance;
}
bool TryMergeWithLastBounds(TextBounds currentBounds, TextBounds lastBounds)
{
if (currentBounds.FlowDirection != lastBounds.FlowDirection)
{
return false;
}
if (currentBounds.Rectangle.Left == lastBounds.Rectangle.Right)
{
foreach (var runBounds in currentBounds.TextRunBounds)
{
lastBounds.TextRunBounds.Add(runBounds);
}
lastBounds.Rectangle = lastBounds.Rectangle.Union(currentBounds.Rectangle);
return true;
}
if (currentBounds.Rectangle.Right == lastBounds.Rectangle.Left)
{
for (int i = 0; i < currentBounds.TextRunBounds.Count; i++)
{
lastBounds.TextRunBounds.Insert(i, currentBounds.TextRunBounds[i]);
}
lastBounds.Rectangle = lastBounds.Rectangle.Union(currentBounds.Rectangle);
return true;
}
return false;
}
while (remainingLength > 0 && currentPosition < FirstTextSourceIndex + Length)
{
var currentIndexedRun = FindIndexedRun();
@ -667,67 +707,21 @@ namespace Avalonia.Media.TextFormatting
directionalWidth = currentDrawable.Size.Width;
}
if (currentTextRun is not TextEndOfLine)
{
if (currentDirection == FlowDirection.LeftToRight)
{
// Find consecutive runs of same direction
for (; lastRunIndex + 1 < _textRuns.Length; lastRunIndex++)
{
var nextRun = _textRuns[lastRunIndex + 1];
var nextDirection = GetDirection(nextRun, currentDirection);
if (currentDirection != nextDirection)
{
break;
}
if (nextRun is DrawableTextRun nextDrawable)
{
directionalWidth += nextDrawable.Size.Width;
}
}
}
else
{
// Find consecutive runs of same direction
for (; firstRunIndex - 1 > 0; firstRunIndex--)
{
var previousRun = _textRuns[firstRunIndex - 1];
var previousDirection = GetDirection(previousRun, currentDirection);
if (currentDirection != previousDirection)
{
break;
}
if (previousRun is DrawableTextRun previousDrawable)
{
directionalWidth += previousDrawable.Size.Width;
currentX -= previousDrawable.Size.Width;
}
}
}
}
int coveredLength;
TextBounds? textBounds;
TextBounds? currentBounds;
switch (currentDirection)
{
case FlowDirection.RightToLeft:
{
textBounds = GetTextRunBoundsRightToLeft(firstRunIndex, lastRunIndex, currentX + directionalWidth, firstTextSourceIndex,
currentBounds = GetTextRunBoundsRightToLeft(firstRunIndex, lastRunIndex, currentX + directionalWidth, firstTextSourceIndex,
currentPosition, remainingLength, out coveredLength, out currentPosition);
break;
}
default:
{
textBounds = GetTextBoundsLeftToRight(firstRunIndex, lastRunIndex, currentX, firstTextSourceIndex,
currentBounds = GetTextBoundsLeftToRight(firstRunIndex, lastRunIndex, currentX, firstTextSourceIndex,
currentPosition, remainingLength, out coveredLength, out currentPosition);
break;
@ -736,7 +730,18 @@ namespace Avalonia.Media.TextFormatting
if (coveredLength > 0)
{
result.Add(textBounds);
if (lastBounds != null && TryMergeWithLastBounds(currentBounds, lastBounds))
{
currentBounds = lastBounds;
result[result.Count - 1] = currentBounds;
}
else
{
result.Add(currentBounds);
}
lastBounds = currentBounds;
remainingLength -= coveredLength;
}
@ -997,14 +1002,14 @@ namespace Avalonia.Media.TextFormatting
public void FinalizeLine()
{
_indexedTextRuns = BidiReorderer.Instance.BidiReorder(_textRuns, _paragraphProperties.FlowDirection, FirstTextSourceIndex);
_textLineMetrics = CreateLineMetrics();
if (_textLineBreak is null && _textRuns.Length > 1 && _textRuns[_textRuns.Length - 1] is TextEndOfLine textEndOfLine)
{
_textLineBreak = new TextLineBreak(textEndOfLine);
}
_indexedTextRuns = BidiReorderer.Instance.BidiReorder(_textRuns, _paragraphProperties.FlowDirection, FirstTextSourceIndex);
}
}
/// <summary>

6
src/Avalonia.Base/Media/Transform.cs

@ -15,12 +15,6 @@ namespace Avalonia.Media
/// </summary>
public abstract class Transform : Animatable, IMutableTransform, ICompositionRenderResource<ITransform>, ICompositorSerializable
{
static Transform()
{
Animation.Animation.RegisterAnimator<TransformAnimator>(prop =>
typeof(ITransform).IsAssignableFrom(prop.OwnerType));
}
internal Transform()
{

7
src/Avalonia.Base/Point.cs

@ -16,13 +16,6 @@ namespace Avalonia
#endif
readonly struct Point : IEquatable<Point>
{
static Point()
{
#if !BUILDTASK
Animation.Animation.RegisterAnimator<PointAnimator>(prop => typeof(Point).IsAssignableFrom(prop.PropertyType));
#endif
}
/// <summary>
/// The X position.
/// </summary>

5
src/Avalonia.Base/Rect.cs

@ -11,11 +11,6 @@ namespace Avalonia
/// </summary>
public readonly struct Rect : IEquatable<Rect>
{
static Rect()
{
Animation.Animation.RegisterAnimator<RectAnimator>(prop => typeof(Rect).IsAssignableFrom(prop.PropertyType));
}
/// <summary>
/// The X position.
/// </summary>

7
src/Avalonia.Base/RelativePoint.cs

@ -54,13 +54,6 @@ namespace Avalonia
private readonly RelativeUnit _unit;
static RelativePoint()
{
#if !BUILDTASK
Animation.Animation.RegisterAnimator<RelativePointAnimator>(prop => typeof(RelativePoint).IsAssignableFrom(prop.PropertyType));
#endif
}
/// <summary>
/// Initializes a new instance of the <see cref="RelativePoint"/> struct.
/// </summary>

4
src/Avalonia.Base/Rendering/Composition/Compositor.cs

@ -130,7 +130,7 @@ namespace Avalonia.Rendering.Composition
Dispatcher.UIThread.VerifyAccess();
using var noPump = NonPumpingLockHelper.Use();
_nextCommit ??= new();
var commit = _nextCommit ??= new();
(_invokeBeforeCommitRead, _invokeBeforeCommitWrite) = (_invokeBeforeCommitWrite, _invokeBeforeCommitRead);
while (_invokeBeforeCommitRead.Count > 0)
@ -188,7 +188,7 @@ namespace Avalonia.Rendering.Composition
}, TaskContinuationOptions.ExecuteSynchronously);
_nextCommit = null;
return _pendingBatch;
return commit;
}
}

5
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs

@ -14,7 +14,7 @@ namespace Avalonia.Rendering.Composition.Server;
/// <summary>
/// Server-side counterpart of <see cref="CompositionDrawListVisual"/>
/// </summary>
internal class ServerCompositionDrawListVisual : ServerCompositionContainerVisual
internal class ServerCompositionDrawListVisual : ServerCompositionContainerVisual, IServerRenderResourceObserver
{
#if DEBUG
// This is needed for debugging purposes so we could see inspect the associated visual from debugger
@ -37,6 +37,7 @@ internal class ServerCompositionDrawListVisual : ServerCompositionContainerVisua
{
_renderCommands?.Dispose();
_renderCommands = reader.ReadObject<ServerCompositionRenderData?>();
_renderCommands?.AddObserver(this);
}
base.DeserializeChangesCore(reader, committedAt);
}
@ -50,6 +51,8 @@ internal class ServerCompositionDrawListVisual : ServerCompositionContainerVisua
base.RenderCore(canvas, currentTransformedClip);
}
public void DependencyQueuedInvalidate(IServerRenderResource sender) => ValuesInvalidated();
#if DEBUG
public override string ToString()
{

10
src/Avalonia.Base/Rendering/Composition/Server/ServerRenderResource.cs

@ -13,8 +13,8 @@ internal interface IServerRenderResourceObserver
internal interface IServerRenderResource : IServerRenderResourceObserver
{
void AddObserver(IServerRenderResource observer);
void RemoveObserver(IServerRenderResource observer);
void AddObserver(IServerRenderResourceObserver observer);
void RemoveObserver(IServerRenderResourceObserver observer);
void QueuedInvalidate();
}
@ -23,7 +23,7 @@ internal class SimpleServerRenderResource : SimpleServerObject, IServerRenderRes
private bool _pendingInvalidation;
private bool _disposed;
public bool IsDisposed => _disposed;
private RefCountingSmallDictionary<IServerRenderResource> _observers;
private RefCountingSmallDictionary<IServerRenderResourceObserver> _observers;
public SimpleServerRenderResource(ServerCompositor compositor) : base(compositor)
{
@ -97,7 +97,7 @@ internal class SimpleServerRenderResource : SimpleServerObject, IServerRenderRes
}
public void AddObserver(IServerRenderResource observer)
public void AddObserver(IServerRenderResourceObserver observer)
{
Debug.Assert(!_disposed);
if(_disposed)
@ -105,7 +105,7 @@ internal class SimpleServerRenderResource : SimpleServerObject, IServerRenderRes
_observers.Add(observer);
}
public void RemoveObserver(IServerRenderResource observer)
public void RemoveObserver(IServerRenderResourceObserver observer)
{
if (_disposed)
return;

7
src/Avalonia.Base/Size.cs

@ -15,13 +15,6 @@ namespace Avalonia
#endif
readonly struct Size : IEquatable<Size>
{
static Size()
{
#if !BUILDTASK
Animation.Animation.RegisterAnimator<SizeAnimator>(prop => typeof(Size).IsAssignableFrom(prop.PropertyType));
#endif
}
/// <summary>
/// A size representing infinity.
/// </summary>

7
src/Avalonia.Base/Thickness.cs

@ -15,13 +15,6 @@ namespace Avalonia
#endif
readonly struct Thickness : IEquatable<Thickness>
{
static Thickness()
{
#if !BUILDTASK
Animation.Animation.RegisterAnimator<ThicknessAnimator>(prop => typeof(Thickness).IsAssignableFrom(prop.PropertyType));
#endif
}
/// <summary>
/// The thickness on the left.
/// </summary>

7
src/Avalonia.Base/Vector.cs

@ -17,13 +17,6 @@ namespace Avalonia
#endif
readonly struct Vector : IEquatable<Vector>
{
static Vector()
{
#if !BUILDTASK
Animation.Animation.RegisterAnimator<VectorAnimator>(prop => typeof(Vector).IsAssignableFrom(prop.PropertyType));
#endif
}
/// <summary>
/// The X component.
/// </summary>

40
src/Avalonia.Controls.ColorPicker/ColorPalettes/FlatColorPalette.cs

@ -266,26 +266,26 @@ namespace Avalonia.Controls
MidnightBlue9 = 0xFF1C2833,
MidnightBlue10 = 0xFF17202A,
Pomegranate = Pomegranate3,
Alizarin = Alizarin3,
Amethyst = Amethyst3,
Wisteria = Wisteria3,
BelizeHole = BelizeHole3,
PeterRiver = PeterRiver3,
Turquoise = Turquoise3,
GreenSea = GreenSea3,
Nephritis = Nephritis3,
Emerald = Emerald3,
Sunflower = Sunflower3,
Orange = Orange3,
Carrot = Carrot3,
Pumpkin = Pumpkin3,
Clouds = Clouds3,
Silver = Silver3,
Concrete = Concrete3,
Asbestos = Asbestos3,
WetAsphalt = WetAsphalt3,
MidnightBlue = MidnightBlue3,
Pomegranate = Pomegranate6,
Alizarin = Alizarin6,
Amethyst = Amethyst6,
Wisteria = Wisteria6,
BelizeHole = BelizeHole6,
PeterRiver = PeterRiver6,
Turquoise = Turquoise6,
GreenSea = GreenSea6,
Nephritis = Nephritis6,
Emerald = Emerald6,
Sunflower = Sunflower6,
Orange = Orange6,
Carrot = Carrot6,
Pumpkin = Pumpkin6,
Clouds = Clouds6,
Silver = Silver6,
Concrete = Concrete6,
Asbestos = Asbestos6,
WetAsphalt = WetAsphalt6,
MidnightBlue = MidnightBlue6,
};
// See: https://htmlcolorcodes.com/assets/downloads/flat-design-colors/flat-design-color-chart.png

18
src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs

@ -45,7 +45,6 @@ namespace Avalonia.Controls.Primitives
private bool _updatingColor = false;
private bool _updatingHsvColor = false;
private bool _coercedInitialColor = false;
private bool _isPointerPressed = false;
private bool _shouldShowLargeSelection = false;
private List<Hsv> _hsvValues = new List<Hsv>();
@ -622,7 +621,7 @@ namespace Avalonia.Controls.Primitives
// that no color has been selected by the user. Note that #00000000 is different than
// #00FFFFFF (Transparent).
//
// In this situation, the first time the user clicks on the spectrum the third
// In this situation, whenever the user clicks on the spectrum, the third
// component and alpha component will remain zero. This is because the spectrum only
// controls two components at any given time.
//
@ -633,16 +632,19 @@ namespace Avalonia.Controls.Primitives
// though the desired value is simply full color.
//
// To work around this usability issue with an initial #00000000 color, the selected
// color is coerced (only the first time) into a color with maximum third component
// value and maximum alpha. This can only happen once and only if those two components
// are already zero.
// color is coerced into a color with maximum third component value and maximum alpha.
// This can only happen here in the spectrum if those two components are already zero.
//
// In the past this coercion was restricted to occur only one time. However, when
// ColorPicker controls are re-used or recycled #00000000 can be set multiple times.
// Each time needs this special logic for usability so now anytime the color is
// changed on the spectrum this logic will run.
//
// Also note this is NOT currently done for #00FFFFFF (Transparent) but based on
// further usability study that case may need to be handled here as well. Right now
// Transparent is treated as a normal color value with the alpha intentionally set
// to zero so the alpha slider must still be adjusted after the spectrum.
if (!_coercedInitialColor &&
IsLoaded)
if (IsLoaded)
{
bool isAlphaComponentZero = (alpha == 0.0);
bool isThirdComponentZero = false;
@ -691,8 +693,6 @@ namespace Avalonia.Controls.Primitives
newHsv.H = 360.0;
break;
}
_coercedInitialColor = true;
}
}

10
src/Avalonia.Controls.DataGrid/DataGridColumn.cs

@ -1103,6 +1103,16 @@ namespace Avalonia.Controls
get;
set;
}
/// <summary>
/// Gets or sets an object associated with this column.
/// </summary>
public object Tag
{
get;
set;
}
/// <summary>
/// Holds a Comparer to use for sorting, if not using the default.
/// </summary>

19
src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs

@ -1,14 +1,10 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Media;
using Avalonia.Metadata;
using Avalonia.Platform;
using Avalonia.Rendering;
using Avalonia.Rendering.Composition;
using Avalonia.Threading;
namespace Avalonia.Controls.Embedding.Offscreen
{
@ -17,7 +13,6 @@ namespace Avalonia.Controls.Embedding.Offscreen
{
private double _scaling = 1;
private Size _clientSize;
private ManualRenderTimer _manualRenderTimer = new();
public IInputRoot? InputRoot { get; private set; }
public bool IsDisposed { get; private set; }
@ -27,21 +22,10 @@ namespace Avalonia.Controls.Embedding.Offscreen
IsDisposed = true;
}
class ManualRenderTimer : IRenderTimer
{
static Stopwatch St = Stopwatch.StartNew();
public event Action<TimeSpan>? Tick;
public bool RunsInBackground => false;
public void TriggerTick() => Tick?.Invoke(St.Elapsed);
}
public Compositor Compositor { get; }
public OffscreenTopLevelImplBase()
{
Compositor = new Compositor(new RenderLoop(_manualRenderTimer), null, false,
MediaContext.Instance, false);
}
=> Compositor = new Compositor(null);
public abstract IEnumerable<object> Surfaces { get; }
@ -76,7 +60,6 @@ namespace Avalonia.Controls.Embedding.Offscreen
public void SetFrameThemeVariant(PlatformThemeVariant themeVariant) { }
/// <inheritdoc/>
public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } = new AcrylicPlatformCompensationLevels(1, 1, 1);
public void SetInputRoot(IInputRoot inputRoot) => InputRoot = inputRoot;

12
src/Avalonia.Controls/Platform/ManagedDispatcherImpl.cs

@ -99,9 +99,15 @@ public class ManagedDispatcherImpl : IControlledDispatcherImpl
continue;
}
if (_nextTimer != null)
TimeSpan? nextTimer;
lock (_lock)
{
nextTimer = _nextTimer;
}
if (nextTimer != null)
{
var waitFor = _clock.Elapsed - _nextTimer.Value;
var waitFor = nextTimer.Value - _clock.Elapsed;
if (waitFor.TotalMilliseconds < 1)
continue;
_wakeup.WaitOne(waitFor);
@ -112,4 +118,4 @@ public class ManagedDispatcherImpl : IControlledDispatcherImpl
registration.Dispose();
}
}
}

116
src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.Framebuffer.cs

@ -0,0 +1,116 @@
using System;
using System.Runtime.InteropServices;
using System.Threading;
using Avalonia.Platform;
using Avalonia.Remote.Protocol.Viewport;
using PlatformPixelFormat = Avalonia.Platform.PixelFormat;
using ProtocolPixelFormat = Avalonia.Remote.Protocol.Viewport.PixelFormat;
namespace Avalonia.Controls.Remote.Server
{
internal partial class RemoteServerTopLevelImpl
{
private enum FrameStatus
{
NotRendered,
Rendered,
CopiedToMessage
}
private sealed class Framebuffer
{
public static Framebuffer Empty { get; } = new(ProtocolPixelFormat.Rgba8888, default, 1.0);
private readonly double _dpi;
private readonly PixelSize _frameSize;
private readonly object _dataLock = new();
private readonly byte[] _data; // for rendering only
private readonly byte[] _dataCopy; // for messages only
private FrameStatus _status = FrameStatus.NotRendered;
public Framebuffer(ProtocolPixelFormat format, Size clientSize, double renderScaling)
{
var frameSize = PixelSize.FromSize(clientSize, renderScaling);
if (frameSize.Width <= 0 || frameSize.Height <= 0)
frameSize = PixelSize.Empty;
var bpp = format == ProtocolPixelFormat.Rgb565 ? 2 : 4;
var stride = frameSize.Width * bpp;
var dataLength = Math.Max(0, stride * frameSize.Height);
_dpi = renderScaling * 96.0;
_frameSize = frameSize;
Format = format;
ClientSize = clientSize;
RenderScaling = renderScaling;
(Stride, _data, _dataCopy) = dataLength > 0 ?
(stride, new byte[dataLength], new byte[dataLength]) :
(0, Array.Empty<byte>(), Array.Empty<byte>());
}
public ProtocolPixelFormat Format { get; }
public Size ClientSize { get; }
public double RenderScaling { get; }
public int Stride { get; }
public FrameStatus GetStatus()
{
lock (_dataLock)
return _status;
}
public ILockedFramebuffer Lock(Action onUnlocked)
{
var handle = GCHandle.Alloc(_data, GCHandleType.Pinned);
Monitor.Enter(_dataLock);
try
{
return new LockedFramebuffer(
handle.AddrOfPinnedObject(),
_frameSize,
Stride,
new Vector(_dpi, _dpi),
new PlatformPixelFormat((PixelFormatEnum)Format),
() =>
{
handle.Free();
Array.Copy(_data, _dataCopy, _data.Length);
_status = FrameStatus.Rendered;
Monitor.Exit(_dataLock);
onUnlocked();
});
}
catch
{
handle.Free();
Monitor.Exit(_dataLock);
throw;
}
}
/// <remarks>The returned message must NOT be kept around, as it contains a shared buffer.</remarks>
public FrameMessage ToMessage(long sequenceId)
{
lock (_dataLock)
_status = FrameStatus.CopiedToMessage;
return new FrameMessage
{
SequenceId = sequenceId,
Data = _dataCopy,
Format = Format,
Width = _frameSize.Width,
Height = _frameSize.Height,
Stride = Stride,
DpiX = _dpi,
DpiY = _dpi
};
}
}
}
}

365
src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs

@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using Avalonia.Controls.Embedding.Offscreen;
using Avalonia.Controls.Platform.Surfaces;
using Avalonia.Input;
@ -11,7 +10,6 @@ using Avalonia.Platform;
using Avalonia.Remote.Protocol;
using Avalonia.Remote.Protocol.Input;
using Avalonia.Remote.Protocol.Viewport;
using Avalonia.Rendering;
using Avalonia.Threading;
using Key = Avalonia.Input.Key;
using ProtocolPixelFormat = Avalonia.Remote.Protocol.Viewport.PixelFormat;
@ -20,28 +18,28 @@ using ProtocolMouseButton = Avalonia.Remote.Protocol.Input.MouseButton;
namespace Avalonia.Controls.Remote.Server
{
[Unstable]
internal class RemoteServerTopLevelImpl : OffscreenTopLevelImplBase, IFramebufferPlatformSurface, ITopLevelImpl
internal partial class RemoteServerTopLevelImpl : OffscreenTopLevelImplBase, IFramebufferPlatformSurface, ITopLevelImpl
{
private readonly IAvaloniaRemoteTransportConnection _transport;
private LockedFramebuffer? _framebuffer;
private readonly object _lock = new();
private readonly Action _sendLastFrameIfNeeded;
private readonly Action _renderAndSendFrameIfNeeded;
private Framebuffer _framebuffer = Framebuffer.Empty;
private long _lastSentFrame = -1;
private long _lastReceivedFrame = -1;
private long _nextFrameNumber = 1;
private ClientViewportAllocatedMessage? _pendingAllocation;
private bool _queuedNextRender;
private bool _inRender;
private Vector _dpi = new Vector(96, 96);
private ProtocolPixelFormat[]? _supportedFormats;
private ProtocolPixelFormat? _format;
public RemoteServerTopLevelImpl(IAvaloniaRemoteTransportConnection transport)
{
_sendLastFrameIfNeeded = SendLastFrameIfNeeded;
_renderAndSendFrameIfNeeded = RenderAndSendFrameIfNeeded;
_transport = transport;
_transport.OnMessage += OnMessage;
KeyboardDevice = AvaloniaLocator.Current.GetRequiredService<IKeyboardDevice>();
QueueNextRender();
Compositor.AfterCommit += QueueNextRender;
}
private static RawPointerEventType GetAvaloniaEventType(ProtocolMouseButton button, bool pressed)
@ -112,45 +110,41 @@ namespace Avalonia.Controls.Remote.Server
{
lock (_lock)
{
if (obj is FrameReceivedMessage lastFrame)
switch (obj)
{
lock (_lock)
{
case FrameReceivedMessage lastFrame:
_lastReceivedFrame = Math.Max(lastFrame.SequenceId, _lastReceivedFrame);
}
Dispatcher.UIThread.Post(RenderIfNeeded);
}
if(obj is ClientRenderInfoMessage renderInfo)
{
lock(_lock)
{
_dpi = new Vector(renderInfo.DpiX, renderInfo.DpiY);
_queuedNextRender = true;
}
Dispatcher.UIThread.Post(RenderIfNeeded);
}
if (obj is ClientSupportedPixelFormatsMessage supportedFormats)
{
lock (_lock)
_supportedFormats = supportedFormats.Formats;
Dispatcher.UIThread.Post(RenderIfNeeded);
}
if (obj is MeasureViewportMessage measure)
Dispatcher.UIThread.Post(() =>
{
var m = Measure(new Size(measure.Width, measure.Height));
_transport.Send(new MeasureViewportMessage
Dispatcher.UIThread.Post(_sendLastFrameIfNeeded);
break;
case ClientRenderInfoMessage renderInfo:
Dispatcher.UIThread.Post(() =>
{
Width = m.Width,
Height = m.Height
RenderScaling = renderInfo.DpiX / 96.0;
RenderAndSendFrameIfNeeded();
});
});
if (obj is ClientViewportAllocatedMessage allocated)
{
lock (_lock)
{
break;
case ClientSupportedPixelFormatsMessage supportedFormats:
_format = TryGetValidPixelFormat(supportedFormats.Formats);
Dispatcher.UIThread.Post(_renderAndSendFrameIfNeeded);
break;
case MeasureViewportMessage measure:
Dispatcher.UIThread.Post(() =>
{
var m = Measure(new Size(measure.Width, measure.Height));
_transport.Send(new MeasureViewportMessage
{
Width = m.Width,
Height = m.Height
});
});
break;
case ClientViewportAllocatedMessage allocated:
if (_pendingAllocation == null)
{
Dispatcher.UIThread.Post(() =>
{
ClientViewportAllocatedMessage allocation;
@ -159,101 +153,111 @@ namespace Avalonia.Controls.Remote.Server
allocation = _pendingAllocation!;
_pendingAllocation = null;
}
_dpi = new Vector(allocation.DpiX, allocation.DpiY);
RenderScaling = allocation.DpiX / 96.0;
ClientSize = new Size(allocation.Width, allocation.Height);
RenderIfNeeded();
RenderAndSendFrameIfNeeded();
});
}
_pendingAllocation = allocated;
}
}
if(obj is PointerMovedEventMessage pointer)
{
Dispatcher.UIThread.Post(() =>
{
Input?.Invoke(new RawPointerEventArgs(
MouseDevice,
0,
InputRoot!,
RawPointerEventType.Move,
new Point(pointer.X, pointer.Y),
GetAvaloniaRawInputModifiers(pointer.Modifiers)));
}, DispatcherPriority.Input);
}
if(obj is PointerPressedEventMessage pressed)
{
Dispatcher.UIThread.Post(() =>
{
Input?.Invoke(new RawPointerEventArgs(
MouseDevice,
0,
InputRoot!,
GetAvaloniaEventType(pressed.Button, true),
new Point(pressed.X, pressed.Y),
GetAvaloniaRawInputModifiers(pressed.Modifiers)));
}, DispatcherPriority.Input);
}
if (obj is PointerReleasedEventMessage released)
{
Dispatcher.UIThread.Post(() =>
{
Input?.Invoke(new RawPointerEventArgs(
MouseDevice,
0,
InputRoot!,
GetAvaloniaEventType(released.Button, false),
new Point(released.X, released.Y),
GetAvaloniaRawInputModifiers(released.Modifiers)));
}, DispatcherPriority.Input);
}
if(obj is ScrollEventMessage scroll)
{
Dispatcher.UIThread.Post(() =>
{
Input?.Invoke(new RawMouseWheelEventArgs(
MouseDevice,
0,
InputRoot!,
new Point(scroll.X, scroll.Y),
new Vector(scroll.DeltaX, scroll.DeltaY),
GetAvaloniaRawInputModifiers(scroll.Modifiers)));
}, DispatcherPriority.Input);
}
if(obj is KeyEventMessage key)
{
Dispatcher.UIThread.Post(() =>
{
Dispatcher.UIThread.RunJobs(DispatcherPriority.Input + 1);
Input?.Invoke(new RawKeyEventArgs(
KeyboardDevice,
0,
InputRoot!,
key.IsDown ? RawKeyEventType.KeyDown : RawKeyEventType.KeyUp,
(Key)key.Key,
GetAvaloniaRawInputModifiers(key.Modifiers)));
}, DispatcherPriority.Input);
}
if(obj is TextInputEventMessage text)
{
Dispatcher.UIThread.Post(() =>
{
Dispatcher.UIThread.RunJobs(DispatcherPriority.Input + 1);
Input?.Invoke(new RawTextInputEventArgs(
KeyboardDevice,
0,
InputRoot!,
text.Text));
}, DispatcherPriority.Input);
break;
case PointerMovedEventMessage pointer:
Dispatcher.UIThread.Post(() =>
{
Input?.Invoke(new RawPointerEventArgs(
MouseDevice,
0,
InputRoot!,
RawPointerEventType.Move,
new Point(pointer.X, pointer.Y),
GetAvaloniaRawInputModifiers(pointer.Modifiers)));
}, DispatcherPriority.Input);
break;
case PointerPressedEventMessage pressed:
Dispatcher.UIThread.Post(() =>
{
Input?.Invoke(new RawPointerEventArgs(
MouseDevice,
0,
InputRoot!,
GetAvaloniaEventType(pressed.Button, true),
new Point(pressed.X, pressed.Y),
GetAvaloniaRawInputModifiers(pressed.Modifiers)));
}, DispatcherPriority.Input);
break;
case PointerReleasedEventMessage released:
Dispatcher.UIThread.Post(() =>
{
Input?.Invoke(new RawPointerEventArgs(
MouseDevice,
0,
InputRoot!,
GetAvaloniaEventType(released.Button, false),
new Point(released.X, released.Y),
GetAvaloniaRawInputModifiers(released.Modifiers)));
}, DispatcherPriority.Input);
break;
case ScrollEventMessage scroll:
Dispatcher.UIThread.Post(() =>
{
Input?.Invoke(new RawMouseWheelEventArgs(
MouseDevice,
0,
InputRoot!,
new Point(scroll.X, scroll.Y),
new Vector(scroll.DeltaX, scroll.DeltaY),
GetAvaloniaRawInputModifiers(scroll.Modifiers)));
}, DispatcherPriority.Input);
break;
case KeyEventMessage key:
Dispatcher.UIThread.Post(() =>
{
Dispatcher.UIThread.RunJobs(DispatcherPriority.Input + 1);
Input?.Invoke(new RawKeyEventArgs(
KeyboardDevice,
0,
InputRoot!,
key.IsDown ? RawKeyEventType.KeyDown : RawKeyEventType.KeyUp,
(Key)key.Key,
GetAvaloniaRawInputModifiers(key.Modifiers)));
}, DispatcherPriority.Input);
break;
case TextInputEventMessage text:
Dispatcher.UIThread.Post(() =>
{
Dispatcher.UIThread.RunJobs(DispatcherPriority.Input + 1);
Input?.Invoke(new RawTextInputEventArgs(
KeyboardDevice,
0,
InputRoot!,
text.Text));
}, DispatcherPriority.Input);
break;
}
}
}
protected void SetDpi(Vector dpi)
private static ProtocolPixelFormat? TryGetValidPixelFormat(ProtocolPixelFormat[]? formats)
{
_dpi = dpi;
RenderIfNeeded();
if (formats is not null)
{
foreach (var format in formats)
{
if (format is >= 0 and <= ProtocolPixelFormat.MaxValue)
return format;
}
}
return null;
}
protected virtual Size Measure(Size constraint)
@ -265,88 +269,63 @@ namespace Avalonia.Controls.Remote.Server
public override IEnumerable<object> Surfaces => new[] { this };
private FrameMessage RenderFrame(int width, int height, ProtocolPixelFormat? format)
private Framebuffer GetOrCreateFramebuffer()
{
var scalingX = _dpi.X / 96.0;
var scalingY = _dpi.Y / 96.0;
width = (int)(width * scalingX);
height = (int)(height * scalingY);
var fmt = format ?? ProtocolPixelFormat.Rgba8888;
var bpp = fmt == ProtocolPixelFormat.Rgb565 ? 2 : 4;
var data = new byte[width * height * bpp];
var handle = GCHandle.Alloc(data, GCHandleType.Pinned);
try
{
if (width > 0 && height > 0)
{
_framebuffer = new LockedFramebuffer(handle.AddrOfPinnedObject(), new PixelSize(width, height), width * bpp, _dpi, new((PixelFormatEnum)fmt),
null);
Paint?.Invoke(new Rect(0, 0, width, height));
}
}
finally
lock (_lock)
{
_framebuffer = null;
handle.Free();
if (_format is not { } format)
_framebuffer = Framebuffer.Empty;
else if (_framebuffer.Format != format || _framebuffer.ClientSize != ClientSize || _framebuffer.RenderScaling != RenderScaling)
_framebuffer = new Framebuffer(format, ClientSize, RenderScaling);
return _framebuffer;
}
return new FrameMessage
{
Data = data,
Format = fmt,
Width = width,
Height = height,
Stride = width * bpp,
DpiX = _dpi.X,
DpiY = _dpi.Y
};
}
public ILockedFramebuffer Lock()
{
if (_framebuffer == null)
throw new InvalidOperationException("Paint was not requested, wait for Paint event");
return _framebuffer;
}
=> GetOrCreateFramebuffer().Lock(_sendLastFrameIfNeeded);
protected void RenderIfNeeded()
private void SendLastFrameIfNeeded()
{
lock (_lock)
{
if (_lastReceivedFrame != _lastSentFrame || !_queuedNextRender || _supportedFormats == null)
return;
if (IsDisposed)
return;
}
Framebuffer framebuffer;
long sequenceId;
var format = ProtocolPixelFormat.Rgba8888;
foreach(var fmt in _supportedFormats)
if (fmt <= ProtocolPixelFormat.MaxValue)
{
format = fmt;
break;
}
_inRender = true;
var frame = RenderFrame((int) ClientSize.Width, (int) ClientSize.Height, format);
lock (_lock)
{
// Ideally we should only send a frame if its status is Rendered: since the renderer might not be
// initialized at the start, we're sending black frames in this case. However, this was the historical
// behavior and some external programs are depending on receiving a frame asap.
if (_lastReceivedFrame != _lastSentFrame || _framebuffer.GetStatus() == FrameStatus.CopiedToMessage)
return;
framebuffer = _framebuffer;
_lastSentFrame = _nextFrameNumber++;
frame.SequenceId = _lastSentFrame;
_queuedNextRender = false;
sequenceId = _lastSentFrame;
}
_inRender = false;
_transport.Send(frame);
_transport.Send(framebuffer.ToMessage(sequenceId));
}
private void QueueNextRender()
protected void RenderAndSendFrameIfNeeded()
{
if (!_inRender && !IsDisposed)
if (IsDisposed)
return;
lock (_lock)
{
_queuedNextRender = true;
DispatcherTimer.RunOnce(RenderIfNeeded, TimeSpan.FromMilliseconds(2), DispatcherPriority.Background);
if (_lastReceivedFrame != _lastSentFrame || _format is null)
return;
}
var framebuffer = GetOrCreateFramebuffer();
if (framebuffer.Stride > 0)
Paint?.Invoke(new Rect(framebuffer.ClientSize));
SendLastFrameIfNeeded();
}
public override IMouseDevice MouseDevice { get; } = new MouseDevice();

47
src/Avalonia.Controls/ToggleSwitch.cs

@ -1,4 +1,5 @@
using Avalonia.Controls.Metadata;
using Avalonia.Animation;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
@ -42,6 +43,10 @@ namespace Avalonia.Controls
x.UpdateKnobPos(x.IsChecked.Value);
}
});
KnobTransitionsProperty.Changed.AddClassHandler<ToggleSwitch>((x, e) =>
{
x.UpdateKnobTransitions();
});
}
/// <summary>
@ -68,6 +73,12 @@ namespace Avalonia.Controls
public static readonly StyledProperty<IDataTemplate?> OnContentTemplateProperty =
AvaloniaProperty.Register<ToggleSwitch, IDataTemplate?>(nameof(OnContentTemplate));
/// <summary>
/// Defines the <see cref="KnobTransitions"/> property.
/// </summary>
public static readonly StyledProperty<Transitions> KnobTransitionsProperty =
AvaloniaProperty.Register<ToggleSwitch, Transitions>(nameof(KnobTransitions));
/// <summary>
/// Gets or Sets the Content that is displayed when in the On State.
/// </summary>
@ -116,6 +127,17 @@ namespace Avalonia.Controls
set { SetValue(OnContentTemplateProperty, value); }
}
/// <summary>
/// Gets or Sets the <see cref="Transitions"/> of switching knob.
/// </summary>
public Transitions KnobTransitions
{
get { return GetValue(KnobTransitionsProperty); }
set { SetValue(KnobTransitionsProperty, value); }
}
private void OffContentChanged(AvaloniaPropertyChangedEventArgs e)
{
if (e.OldValue is ILogical oldChild)
@ -177,7 +199,21 @@ namespace Avalonia.Controls
UpdateKnobPos(IsChecked.Value);
}
}
protected override void OnLoaded()
{
base.OnLoaded();
UpdateKnobTransitions();
}
private void UpdateKnobTransitions()
{
if (_knobsPanel != null)
{
_knobsPanel.Transitions = KnobTransitions;
}
}
private void KnobsPanel_PointerPressed(object? sender, Input.PointerPressedEventArgs e)
{
_switchStartPoint = e.GetPosition(_switchKnob);
@ -194,7 +230,7 @@ namespace Avalonia.Controls
_knobsPanel!.ClearValue(Canvas.LeftProperty);
PseudoClasses.Set(":dragging", false);
if (shouldBecomeChecked == IsChecked)
{
UpdateKnobPos(shouldBecomeChecked);
@ -203,6 +239,7 @@ namespace Avalonia.Controls
{
SetCurrentValue(IsCheckedProperty, shouldBecomeChecked);
}
UpdateKnobTransitions();
}
else
{
@ -218,6 +255,10 @@ namespace Avalonia.Controls
{
if (_knobsPanelPressed)
{
if(_knobsPanel != null)
{
_knobsPanel.Transitions = null;
}
var difference = e.GetPosition(_switchKnob) - _switchStartPoint;
if ((!_isDragging) && (System.Math.Abs(difference.X) > 3))

12
src/Avalonia.Controls/TopLevel.cs

@ -25,6 +25,7 @@ using System.Linq;
using System.Threading.Tasks;
using Avalonia.Metadata;
using Avalonia.Rendering.Composition;
using Avalonia.Threading;
namespace Avalonia.Controls
{
@ -535,7 +536,16 @@ namespace Avalonia.Controls
return Disposable.Create(() => { });
}
}
/// <summary>
/// Enqueues a callback to be called on the next animation tick
/// </summary>
public void RequestAnimationFrame(Action<TimeSpan> action)
{
Dispatcher.UIThread.VerifyAccess();
MediaContext.Instance.RequestAnimationFrame(action);
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);

19
src/Avalonia.Controls/TreeView.cs

@ -84,6 +84,11 @@ namespace Avalonia.Controls
/// <summary>
/// Gets or sets a value indicating whether to automatically scroll to newly selected items.
/// </summary>
/// <remarks>
/// This property is of limited use with <see cref="TreeView"/> as it will only scroll
/// to realized items. To scroll to a non-expanded item, you need to ensure that its
/// ancestors are expanded.
/// </remarks>
public bool AutoScrollToSelectedItem
{
get => GetValue(AutoScrollToSelectedItemProperty);
@ -353,9 +358,13 @@ namespace Avalonia.Controls
SelectedItemsAdded(e.NewItems!.Cast<object>().ToArray());
if (AutoScrollToSelectedItem)
var selectedItem = SelectedItem;
if (AutoScrollToSelectedItem &&
selectedItem is not null &&
e.NewItems![0] == selectedItem)
{
var container = ContainerFromItem(e.NewItems![0]!);
var container = TreeContainerFromItem(selectedItem);
container?.BringIntoView();
}
@ -531,6 +540,12 @@ namespace Avalonia.Controls
// The IsSelected property is not set on the container: update the container
// selection based on the current selection as understood by this control.
MarkContainerSelected(container, SelectedItems.Contains(item));
// If the newly realized container is the selected container, scroll to it after layout.
if (AutoScrollToSelectedItem && SelectedItem == item)
{
Dispatcher.UIThread.Post(container.BringIntoView, DispatcherPriority.Loaded);
}
}
/// <inheritdoc/>

8
src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs

@ -53,7 +53,11 @@ namespace Avalonia.DesignerSupport.Remote
// In previewer mode we completely ignore client-side viewport size
if (obj is ClientViewportAllocatedMessage alloc)
{
Dispatcher.UIThread.Post(() => SetDpi(new Vector(alloc.DpiX, alloc.DpiY)));
Dispatcher.UIThread.Post(() =>
{
RenderScaling = alloc.DpiX / 96.0;
RenderAndSendFrameIfNeeded();
});
return;
}
base.OnMessage(transport, obj);
@ -67,7 +71,7 @@ namespace Avalonia.DesignerSupport.Remote
Height = clientSize.Height
});
ClientSize = clientSize;
RenderIfNeeded();
RenderAndSendFrameIfNeeded();
}
public void Move(PixelPoint point)

2
src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs

@ -52,7 +52,7 @@ namespace Avalonia.DesignerSupport.Remote
.Bind<IKeyboardDevice>().ToConstant(Keyboard)
.Bind<IPlatformSettings>().ToSingleton<DefaultPlatformSettings>()
.Bind<IDispatcherImpl>().ToConstant(new ManagedDispatcherImpl(null))
.Bind<IRenderTimer>().ToConstant(new DefaultRenderTimer(60))
.Bind<IRenderTimer>().ToConstant(new UiThreadRenderTimer(60))
.Bind<IWindowingPlatform>().ToConstant(instance)
.Bind<IPlatformIconLoader>().ToSingleton<IconLoaderStub>()
.Bind<PlatformHotkeyConfiguration>().ToSingleton<PlatformHotkeyConfiguration>();

1
src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs

@ -44,6 +44,7 @@ namespace Avalonia.Diagnostics.ViewModels
SelectedTab = 0;
if (root is TopLevel topLevel)
{
_pointerOverRoot = topLevel;
_pointerOverSubscription = topLevel.GetObservable(TopLevel.PointerOverElementProperty)
.Subscribe(x => PointerOverElement = x);

20
src/Avalonia.Themes.Fluent/Controls/ToggleSwitch.xaml

@ -28,6 +28,14 @@
<Setter Property="HorizontalContentAlignment" Value="Left" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="FontSize" Value="{DynamicResource ControlContentThemeFontSize}" />
<Setter Property="KnobTransitions">
<Transitions>
<DoubleTransition
Easing="CubicEaseOut"
Property="Canvas.Left"
Duration="0:0:0.2" />
</Transitions>
</Setter>
<Setter Property="Template">
<ControlTemplate>
<Grid Background="{TemplateBinding Background}" RowDefinitions="Auto,*">
@ -134,18 +142,6 @@
<Setter Property="Margin" Value="0" />
</Style>
<!-- NormalState -->
<Style Selector="^:not(:dragging) /template/ Grid#PART_MovingKnobs">
<Setter Property="Transitions">
<Transitions>
<DoubleTransition
Easing="CubicEaseOut"
Property="Canvas.Left"
Duration="0:0:0.2" />
</Transitions>
</Setter>
</Style>
<!-- PointerOverState -->
<Style Selector="^:pointerover /template/ Border#OuterBorder">
<Setter Property="BorderBrush" Value="{DynamicResource ToggleSwitchStrokeOffPointerOver}" />

13
src/Avalonia.Themes.Simple/Controls/ToggleSwitch.xaml

@ -46,6 +46,14 @@
<Setter Property="HorizontalContentAlignment" Value="Left" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="FontSize" Value="{DynamicResource FontSizeNormal}" />
<Setter Property="KnobTransitions">
<Transitions>
<DoubleTransition
Easing="CubicEaseOut"
Property="Canvas.Left"
Duration="0:0:0.2" />
</Transitions>
</Setter>
<Setter Property="Template">
<ControlTemplate>
<Grid Background="{TemplateBinding Background}"
@ -123,11 +131,6 @@
<Grid x:Name="PART_MovingKnobs"
Width="20" Height="20">
<Grid.Transitions>
<Transitions>
<DoubleTransition Property="Canvas.Left" Duration="0:0:0.2" Easing="CubicEaseOut" />
</Transitions>
</Grid.Transitions>
<Ellipse x:Name="SwitchKnobOn"
Fill="{DynamicResource HighlightForegroundColor}"

2
src/tools/Avalonia.Designer.HostApp/Avalonia.Designer.HostApp.csproj

@ -9,8 +9,6 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Avalonia.DesignerSupport\Avalonia.DesignerSupport.csproj" />
<ProjectReference Include="..\..\Markup\Avalonia.Markup.Xaml\Avalonia.Markup.Xaml.csproj" />
<ProjectReference Include="..\..\Markup\Avalonia.Markup\Avalonia.Markup.csproj" />
<ProjectReference Include="..\..\Avalonia.Base\Avalonia.Base.csproj" />
<ProjectReference Include="..\..\Avalonia.Controls\Avalonia.Controls.csproj" />
<ProjectReference Include="..\..\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" />

15
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs

@ -1112,6 +1112,21 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
}
}
[Fact]
public void Should_HitTestTextPosition_EndOfLine_RTL()
{
var text = "גש\r\n";
using (Start())
{
var textLayout = new TextLayout(text, Typeface.Default, 12, Brushes.Black, flowDirection: FlowDirection.RightToLeft);
var rect = textLayout.HitTestTextPosition(text.Length);
Assert.Equal(14.0625, rect.Top);
}
}
private static IDisposable Start()
{

33
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs

@ -2,12 +2,10 @@
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Runtime.InteropServices;
using Avalonia.Headless;
using Avalonia.Media;
using Avalonia.Media.TextFormatting;
using Avalonia.UnitTests;
using Avalonia.Utilities;
using Xunit;
namespace Avalonia.Skia.UnitTests.Media.TextFormatting
@ -1072,7 +1070,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
}
[Fact]
public void Should_GetTextBounds_BiDi()
public void Should_GetTextBounds_Bidi()
{
var text = "אבגדה 12345 ABCDEF אבגדה";
@ -1114,12 +1112,39 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
bounds = textLine.GetTextBounds(0, 25);
Assert.Equal(5, bounds.Count);
Assert.Equal(4, bounds.Count);
Assert.Equal(textLine.WidthIncludingTrailingWhitespace, bounds.Last().Rectangle.Right);
}
}
[Fact]
public void Should_GetTextBounds_Bidi_2()
{
var text = "אבג ABC אבג 123";
using (Start())
{
var defaultProperties = new GenericTextRunProperties(Typeface.Default);
var textSource = new SingleBufferTextSource(text, defaultProperties, true);
var formatter = new TextFormatterImpl();
var textLine =
formatter.FormatLine(textSource, 0, double.PositiveInfinity,
new GenericTextParagraphProperties(FlowDirection.LeftToRight, TextAlignment.Left,
true, true, defaultProperties, TextWrapping.NoWrap, 0, 0, 0));
var bounds = textLine.GetTextBounds(0, text.Length);
Assert.Equal(4, bounds.Count);
var right = bounds.Last().Rectangle.Right;
Assert.Equal(textLine.WidthIncludingTrailingWhitespace, right);
}
}
private class FixedRunsTextSource : ITextSource
{
private readonly IReadOnlyList<TextRun> _textRuns;

Loading…
Cancel
Save