Browse Source

Merge branch 'master' into clipboard-image

pull/12246/head
viordash 3 years ago
committed by GitHub
parent
commit
0735c6c602
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 18
      native/Avalonia.Native/src/OSX/app.mm
  2. 1
      native/Avalonia.Native/src/OSX/common.h
  3. 8
      native/Avalonia.Native/src/OSX/main.mm
  4. 7
      samples/Sandbox/MainWindow.axaml
  5. 1
      src/Avalonia.Base/Animation/Transitions/Rotate3DTransition.cs
  6. 20
      src/Avalonia.Base/AvaloniaObjectExtensions.cs
  7. 6
      src/Avalonia.Base/Media/GlyphRun.cs
  8. 321
      src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
  9. 7
      src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs
  10. 4
      src/Avalonia.Base/Threading/Dispatcher.Invoke.cs
  11. 2
      src/Avalonia.Base/Threading/Dispatcher.MainLoop.cs
  12. 20
      src/Avalonia.Base/Threading/Dispatcher.Queue.cs
  13. 2
      src/Avalonia.Base/Threading/Dispatcher.Timers.cs
  14. 3
      src/Avalonia.Base/Threading/Dispatcher.cs
  15. 8
      src/Avalonia.Base/Threading/DispatcherFrame.cs
  16. 2
      src/Avalonia.Base/Threading/DispatcherOperation.cs
  17. 19
      src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs
  18. 7
      src/Avalonia.Controls/Window.cs
  19. 10
      src/Avalonia.Native/AvaloniaNativePlatform.cs
  20. 12
      src/Browser/Avalonia.Browser/webapp/package-lock.json
  21. 2
      tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs
  22. 31
      tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests.cs
  23. 35
      tests/Avalonia.Controls.UnitTests/WindowTests.cs
  24. 116
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs

18
native/Avalonia.Native/src/OSX/app.mm

@ -2,6 +2,7 @@
#include "AvnString.h" #include "AvnString.h"
@interface AvnAppDelegate : NSObject<NSApplicationDelegate> @interface AvnAppDelegate : NSObject<NSApplicationDelegate>
-(AvnAppDelegate* _Nonnull) initWithEvents: (IAvnApplicationEvents* _Nonnull) events; -(AvnAppDelegate* _Nonnull) initWithEvents: (IAvnApplicationEvents* _Nonnull) events;
-(void) releaseEvents;
@end @end
NSApplicationActivationPolicy AvnDesiredActivationPolicy = NSApplicationActivationPolicyRegular; NSApplicationActivationPolicy AvnDesiredActivationPolicy = NSApplicationActivationPolicyRegular;
@ -15,6 +16,11 @@ ComPtr<IAvnApplicationEvents> _events;
return self; return self;
} }
- (void)releaseEvents
{
_events = nil;
}
- (void)applicationWillFinishLaunching:(NSNotification *)notification - (void)applicationWillFinishLaunching:(NSNotification *)notification
{ {
if([[NSApplication sharedApplication] activationPolicy] != AvnDesiredActivationPolicy) if([[NSApplication sharedApplication] activationPolicy] != AvnDesiredActivationPolicy)
@ -105,6 +111,18 @@ extern void InitializeAvnApp(IAvnApplicationEvents* events, bool disableAppDeleg
} }
} }
extern void ReleaseAvnAppEvents()
{
NSApplication* app = [AvnApplication sharedApplication];
id delegate = [app delegate];
if ([delegate isMemberOfClass:[AvnAppDelegate class]])
{
AvnAppDelegate* avnDelegate = delegate;
[avnDelegate releaseEvents];
[app setDelegate:nil];
}
}
HRESULT AvnApplicationCommands::HideApp() HRESULT AvnApplicationCommands::HideApp()
{ {
START_COM_CALL; START_COM_CALL;

1
native/Avalonia.Native/src/OSX/common.h

@ -38,6 +38,7 @@ extern IAvnMenu* GetAppMenu ();
extern NSMenuItem* GetAppMenuItem (); extern NSMenuItem* GetAppMenuItem ();
extern void InitializeAvnApp(IAvnApplicationEvents* events, bool disableAppDelegate); extern void InitializeAvnApp(IAvnApplicationEvents* events, bool disableAppDelegate);
extern void ReleaseAvnAppEvents();
extern NSApplicationActivationPolicy AvnDesiredActivationPolicy; extern NSApplicationActivationPolicy AvnDesiredActivationPolicy;
extern NSPoint ToNSPoint (AvnPoint p); extern NSPoint ToNSPoint (AvnPoint p);
extern NSRect ToNSRect (AvnRect r); extern NSRect ToNSRect (AvnRect r);

8
native/Avalonia.Native/src/OSX/main.mm

@ -197,6 +197,14 @@ class AvaloniaNative : public ComSingleObject<IAvaloniaNativeFactory, &IID_IAval
public: public:
FORWARD_IUNKNOWN() FORWARD_IUNKNOWN()
virtual ~AvaloniaNative() override
{
ReleaseAvnAppEvents();
_deallocator = nullptr;
_dispatcher = nullptr;
}
virtual HRESULT Initialize(IAvnGCHandleDeallocatorCallback* deallocator, virtual HRESULT Initialize(IAvnGCHandleDeallocatorCallback* deallocator,
IAvnApplicationEvents* events, IAvnApplicationEvents* events,
IAvnDispatcher* dispatcher) override IAvnDispatcher* dispatcher) override

7
samples/Sandbox/MainWindow.axaml

@ -1,11 +1,4 @@
<Window xmlns="https://github.com/avaloniaui" <Window xmlns="https://github.com/avaloniaui"
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml' xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
x:Class="Sandbox.MainWindow"> x:Class="Sandbox.MainWindow">
<ScrollViewer>
<StackPanel>
<Button Margin="0 100000000000000000 0 0">0</Button>
<Button>1</Button>
</StackPanel>
</ScrollViewer>
</Window> </Window>

1
src/Avalonia.Base/Animation/Transitions/Rotate3DTransition.cs

@ -89,6 +89,7 @@ public class Rotate3DTransition: PageSlide
{ {
Easing = SlideInEasing, Easing = SlideInEasing,
Duration = Duration, Duration = Duration,
FillMode = FillMode.Forward,
Children = Children =
{ {
CreateKeyFrame(0d, 90d * (forward ? 1 : -1), 1), CreateKeyFrame(0d, 90d * (forward ? 1 : -1), 1),

20
src/Avalonia.Base/AvaloniaObjectExtensions.cs

@ -60,6 +60,8 @@ namespace Avalonia
} }
/// <inheritdoc cref="GetObservable{T}(AvaloniaObject, AvaloniaProperty{T})"/> /// <inheritdoc cref="GetObservable{T}(AvaloniaObject, AvaloniaProperty{T})"/>
/// <typeparam name="TSource">The type of the values held by the <paramref name="property"/>.</typeparam>
/// <typeparam name="TResult">The type of the value returned by the <paramref name="converter"/>.</typeparam>
/// <param name="o"/> /// <param name="o"/>
/// <param name="property"/> /// <param name="property"/>
/// <param name="converter">A method which is executed to convert each property value to <typeparamref name="TResult"/>.</param> /// <param name="converter">A method which is executed to convert each property value to <typeparamref name="TResult"/>.</param>
@ -71,6 +73,15 @@ namespace Avalonia
converter ?? throw new ArgumentNullException(nameof(converter))); converter ?? throw new ArgumentNullException(nameof(converter)));
} }
/// <inheritdoc cref="GetObservable{TSource,TResult}"/>
public static IObservable<TResult> GetObservable<TResult>(this AvaloniaObject o, AvaloniaProperty property, Func<object?, TResult> converter)
{
return new AvaloniaPropertyObservable<object?, TResult>(
o ?? throw new ArgumentNullException(nameof(o)),
property ?? throw new ArgumentNullException(nameof(property)),
converter ?? throw new ArgumentNullException(nameof(converter)));
}
/// <summary> /// <summary>
/// Gets an observable for an <see cref="AvaloniaProperty"/>. /// Gets an observable for an <see cref="AvaloniaProperty"/>.
/// </summary> /// </summary>
@ -92,6 +103,15 @@ namespace Avalonia
property ?? throw new ArgumentNullException(nameof(property))); property ?? throw new ArgumentNullException(nameof(property)));
} }
/// <inheritdoc cref="GetObservable{TSource,TResult}"/>
public static IObservable<BindingValue<TResult>> GetBindingObservable<TResult>(this AvaloniaObject o, AvaloniaProperty property, Func<object?, TResult> converter)
{
return new AvaloniaPropertyBindingObservable<object?, TResult>(
o ?? throw new ArgumentNullException(nameof(o)),
property ?? throw new ArgumentNullException(nameof(property)),
converter?? throw new ArgumentNullException(nameof(converter)));
}
/// <summary> /// <summary>
/// Gets an observable for an <see cref="AvaloniaProperty"/>. /// Gets an observable for an <see cref="AvaloniaProperty"/>.
/// </summary> /// </summary>

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

@ -424,13 +424,13 @@ namespace Avalonia.Media
/// </returns> /// </returns>
public CharacterHit GetPreviousCaretCharacterHit(CharacterHit characterHit) public CharacterHit GetPreviousCaretCharacterHit(CharacterHit characterHit)
{ {
var previousCharacterHit = FindNearestCharacterHit(characterHit.FirstCharacterIndex - 1, out _);
if (characterHit.TrailingLength != 0) if (characterHit.TrailingLength != 0)
{ {
return new CharacterHit(characterHit.FirstCharacterIndex); return previousCharacterHit;
} }
var previousCharacterHit = FindNearestCharacterHit(characterHit.FirstCharacterIndex - 1, out _);
return new CharacterHit(previousCharacterHit.FirstCharacterIndex); return new CharacterHit(previousCharacterHit.FirstCharacterIndex);
} }

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

@ -9,7 +9,7 @@ namespace Avalonia.Media.TextFormatting
internal static Comparer<TextBounds> TextBoundsComparer { get; } = internal static Comparer<TextBounds> TextBoundsComparer { get; } =
Comparer<TextBounds>.Create((x, y) => x.Rectangle.Left.CompareTo(y.Rectangle.Left)); Comparer<TextBounds>.Create((x, y) => x.Rectangle.Left.CompareTo(y.Rectangle.Left));
private IReadOnlyList<IndexedTextRun>? _indexedTextRuns; internal IReadOnlyList<IndexedTextRun>? _indexedTextRuns;
private readonly TextRun[] _textRuns; private readonly TextRun[] _textRuns;
private readonly double _paragraphWidth; private readonly double _paragraphWidth;
private readonly TextParagraphProperties _paragraphProperties; private readonly TextParagraphProperties _paragraphProperties;
@ -512,38 +512,45 @@ namespace Avalonia.Media.TextFormatting
/// <inheritdoc/> /// <inheritdoc/>
public override CharacterHit GetNextCaretCharacterHit(CharacterHit characterHit) public override CharacterHit GetNextCaretCharacterHit(CharacterHit characterHit)
{ {
if (_textRuns.Length == 0) if (_textRuns.Length == 0 || _indexedTextRuns is null)
{ {
return new CharacterHit(); return new CharacterHit();
} }
if (TryFindNextCharacterHit(characterHit, out var nextCharacterHit)) var currentCharacterrHit = characterHit;
{ var characterIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
return nextCharacterHit;
}
var lastTextPosition = FirstTextSourceIndex + Length;
// Can't move, we're after the last character var currentRun = GetRunAtCharacterIndex(characterIndex, LogicalDirection.Forward, out var currentPosition);
var runIndex = GetRunIndexAtCharacterIndex(lastTextPosition, LogicalDirection.Forward, out var currentPosition);
var currentRun = _textRuns[runIndex]; var nextCharacterHit = characterHit;
switch (currentRun) switch (currentRun)
{ {
case ShapedTextRun shapedRun: case ShapedTextRun shapedRun:
{ {
nextCharacterHit = shapedRun.GlyphRun.GetNextCaretCharacterHit(characterHit); var offset = Math.Max(0, currentPosition - shapedRun.GlyphRun.Metrics.FirstCluster - characterHit.TrailingLength);
if (offset > 0)
{
currentCharacterrHit = new CharacterHit(Math.Max(0, characterHit.FirstCharacterIndex - offset), characterHit.TrailingLength);
}
nextCharacterHit = shapedRun.GlyphRun.GetNextCaretCharacterHit(currentCharacterrHit);
if (offset > 0)
{
nextCharacterHit = new CharacterHit(nextCharacterHit.FirstCharacterIndex + offset, nextCharacterHit.TrailingLength);
}
break; break;
} }
default: case TextRun:
{ {
nextCharacterHit = new CharacterHit(currentPosition + currentRun.Length); nextCharacterHit = new CharacterHit(currentPosition + currentRun.Length);
break; break;
} }
} }
if (characterHit.FirstCharacterIndex + characterHit.TrailingLength == nextCharacterHit.FirstCharacterIndex + nextCharacterHit.TrailingLength) if (characterIndex == nextCharacterHit.FirstCharacterIndex + nextCharacterHit.TrailingLength)
{ {
return characterHit; return characterHit;
} }
@ -554,17 +561,75 @@ namespace Avalonia.Media.TextFormatting
/// <inheritdoc/> /// <inheritdoc/>
public override CharacterHit GetPreviousCaretCharacterHit(CharacterHit characterHit) public override CharacterHit GetPreviousCaretCharacterHit(CharacterHit characterHit)
{ {
if (TryFindPreviousCharacterHit(characterHit, out var previousCharacterHit)) if (_textRuns.Length == 0 || _indexedTextRuns is null)
{
return new CharacterHit();
}
if (characterHit.TrailingLength > 0 && characterHit.FirstCharacterIndex <= FirstTextSourceIndex)
{
return new CharacterHit(FirstTextSourceIndex);
}
var characterIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
if (characterIndex <= FirstTextSourceIndex)
{ {
return previousCharacterHit; return new CharacterHit(FirstTextSourceIndex);
}
var currentCharacterrHit = characterHit;
var currentRun = GetRunAtCharacterIndex(characterIndex, LogicalDirection.Backward, out var currentPosition);
if (currentPosition == characterHit.FirstCharacterIndex)
{
currentRun = GetRunAtCharacterIndex(characterHit.FirstCharacterIndex, LogicalDirection.Backward, out currentPosition);
}
var previousCharacterHit = characterHit;
switch (currentRun)
{
case ShapedTextRun shapedRun:
{
var offset = Math.Max(0, currentPosition - shapedRun.GlyphRun.Metrics.FirstCluster);
if (offset > 0)
{
currentCharacterrHit = new CharacterHit(Math.Max(0, characterHit.FirstCharacterIndex - offset), characterHit.TrailingLength);
}
previousCharacterHit = shapedRun.GlyphRun.GetPreviousCaretCharacterHit(currentCharacterrHit);
if (offset > 0)
{
previousCharacterHit = new CharacterHit(previousCharacterHit.FirstCharacterIndex + offset, previousCharacterHit.TrailingLength);
}
break;
}
case TextRun:
{
if (characterHit.TrailingLength > 0)
{
previousCharacterHit = new CharacterHit(currentPosition, currentRun.Length);
}
else
{
previousCharacterHit = new CharacterHit(currentPosition + currentRun.Length);
}
break;
}
} }
if (characterHit.FirstCharacterIndex <= FirstTextSourceIndex) if (characterIndex == previousCharacterHit.FirstCharacterIndex + previousCharacterHit.TrailingLength)
{ {
characterHit = new CharacterHit(FirstTextSourceIndex); return characterHit;
} }
return characterHit; // Can't move, we're before the first character return previousCharacterHit;
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -1009,161 +1074,7 @@ namespace Avalonia.Media.TextFormatting
if (_textLineBreak is null && _textRuns.Length > 1 && _textRuns[_textRuns.Length - 1] is TextEndOfLine textEndOfLine) if (_textLineBreak is null && _textRuns.Length > 1 && _textRuns[_textRuns.Length - 1] is TextEndOfLine textEndOfLine)
{ {
_textLineBreak = new TextLineBreak(textEndOfLine); _textLineBreak = new TextLineBreak(textEndOfLine);
}
}
/// <summary>
/// Tries to find the next character hit.
/// </summary>
/// <param name="characterHit">The current character hit.</param>
/// <param name="nextCharacterHit">The next character hit.</param>
/// <returns></returns>
private bool TryFindNextCharacterHit(CharacterHit characterHit, out CharacterHit nextCharacterHit)
{
nextCharacterHit = characterHit;
var codepointIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
var lastCodepointIndex = FirstTextSourceIndex + Length;
if (codepointIndex >= lastCodepointIndex)
{
return false; // Cannot go forward anymore
}
if (codepointIndex < FirstTextSourceIndex)
{
codepointIndex = FirstTextSourceIndex;
}
var runIndex = GetRunIndexAtCharacterIndex(codepointIndex, LogicalDirection.Forward, out var currentPosition);
while (runIndex < _textRuns.Length)
{
var currentRun = _textRuns[runIndex];
switch (currentRun)
{
case ShapedTextRun shapedRun:
{
var foundCharacterHit = shapedRun.GlyphRun.FindNearestCharacterHit(characterHit.FirstCharacterIndex + characterHit.TrailingLength, out _);
var isAtEnd = foundCharacterHit.FirstCharacterIndex + foundCharacterHit.TrailingLength == FirstTextSourceIndex + Length;
if (isAtEnd && !shapedRun.GlyphRun.IsLeftToRight)
{
nextCharacterHit = foundCharacterHit;
return true;
}
nextCharacterHit = isAtEnd || characterHit.TrailingLength != 0 ?
foundCharacterHit :
new CharacterHit(foundCharacterHit.FirstCharacterIndex + foundCharacterHit.TrailingLength);
if (isAtEnd || nextCharacterHit.FirstCharacterIndex > characterHit.FirstCharacterIndex)
{
return true;
}
break;
}
default:
{
var textPosition = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
if (textPosition == currentPosition)
{
nextCharacterHit = new CharacterHit(currentPosition + currentRun.Length);
return true;
}
break;
}
}
currentPosition += currentRun.Length;
runIndex++;
}
return false;
}
/// <summary>
/// Tries to find the previous character hit.
/// </summary>
/// <param name="characterHit">The current character hit.</param>
/// <param name="previousCharacterHit">The previous character hit.</param>
/// <returns></returns>
private bool TryFindPreviousCharacterHit(CharacterHit characterHit, out CharacterHit previousCharacterHit)
{
var characterIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
if (characterIndex == FirstTextSourceIndex)
{
previousCharacterHit = new CharacterHit(FirstTextSourceIndex);
return true;
}
previousCharacterHit = characterHit;
if (characterIndex < FirstTextSourceIndex)
{
return false; // Cannot go backward anymore.
}
var runIndex = GetRunIndexAtCharacterIndex(characterIndex, LogicalDirection.Backward, out var currentPosition);
while (runIndex >= 0)
{
var currentRun = _textRuns[runIndex];
switch (currentRun)
{
case ShapedTextRun shapedRun:
{
var foundCharacterHit = shapedRun.GlyphRun.FindNearestCharacterHit(characterHit.FirstCharacterIndex - 1, out _);
if (foundCharacterHit.FirstCharacterIndex + foundCharacterHit.TrailingLength < characterIndex)
{
previousCharacterHit = foundCharacterHit;
return true;
}
var previousPosition = foundCharacterHit.FirstCharacterIndex + foundCharacterHit.TrailingLength;
if (foundCharacterHit.TrailingLength > 0 && previousPosition == characterIndex)
{
previousCharacterHit = new CharacterHit(foundCharacterHit.FirstCharacterIndex);
}
if (previousCharacterHit != characterHit)
{
return true;
}
break;
}
default:
{
if (characterIndex == currentPosition + currentRun.Length)
{
previousCharacterHit = new CharacterHit(currentPosition);
return true;
}
break;
}
}
currentPosition -= currentRun.Length;
runIndex--;
} }
return false;
} }
/// <summary> /// <summary>
@ -1173,15 +1084,23 @@ namespace Avalonia.Media.TextFormatting
/// <param name="direction">The logical direction.</param> /// <param name="direction">The logical direction.</param>
/// <param name="textPosition">The text position of the found run index.</param> /// <param name="textPosition">The text position of the found run index.</param>
/// <returns>The text run index.</returns> /// <returns>The text run index.</returns>
private int GetRunIndexAtCharacterIndex(int codepointIndex, LogicalDirection direction, out int textPosition) private TextRun? GetRunAtCharacterIndex(int codepointIndex, LogicalDirection direction, out int textPosition)
{ {
var runIndex = 0; var runIndex = 0;
textPosition = FirstTextSourceIndex; textPosition = FirstTextSourceIndex;
if (_indexedTextRuns is null)
{
return null;
}
TextRun? currentRun = null;
TextRun? previousRun = null; TextRun? previousRun = null;
while (runIndex < _textRuns.Length) while (runIndex < _indexedTextRuns.Count)
{ {
var currentRun = _textRuns[runIndex]; var indexedRun = _indexedTextRuns[runIndex];
currentRun = indexedRun.TextRun;
switch (currentRun) switch (currentRun)
{ {
@ -1189,64 +1108,49 @@ namespace Avalonia.Media.TextFormatting
{ {
var firstCluster = shapedRun.GlyphRun.Metrics.FirstCluster; var firstCluster = shapedRun.GlyphRun.Metrics.FirstCluster;
if (firstCluster > codepointIndex) firstCluster += Math.Max(0, indexedRun.TextSourceCharacterIndex - firstCluster);
{
break;
}
if (previousRun is ShapedTextRun previousShaped && !previousShaped.ShapedBuffer.IsLeftToRight)
{
if (shapedRun.ShapedBuffer.IsLeftToRight)
{
if (firstCluster >= codepointIndex)
{
return --runIndex;
}
}
else
{
if (codepointIndex > firstCluster + currentRun.Length)
{
return --runIndex;
}
}
}
if (direction == LogicalDirection.Forward) if (direction == LogicalDirection.Forward)
{ {
if (codepointIndex >= firstCluster && codepointIndex <= firstCluster + currentRun.Length) if (codepointIndex >= firstCluster && codepointIndex < firstCluster + currentRun.Length)
{ {
return runIndex; return currentRun;
} }
} }
else else
{ {
if (codepointIndex > firstCluster && if (previousRun is not null && previousRun is not ShapedTextRun && codepointIndex == textPosition + firstCluster)
codepointIndex <= firstCluster + currentRun.Length) {
textPosition -= previousRun.Length;
return previousRun;
}
if (codepointIndex > firstCluster && codepointIndex <= firstCluster + currentRun.Length)
{ {
return runIndex; return currentRun;
} }
} }
if (runIndex + 1 >= _textRuns.Length) if (runIndex + 1 >= _textRuns.Length)
{ {
return runIndex; return currentRun;
} }
textPosition += currentRun.Length; textPosition += currentRun.Length;
break; break;
} }
default: case TextRun:
{ {
if (codepointIndex == textPosition) if (codepointIndex == textPosition)
{ {
return runIndex; return currentRun;
} }
if (runIndex + 1 >= _textRuns.Length) if (runIndex + 1 >= _textRuns.Length)
{ {
return runIndex; return currentRun;
} }
textPosition += currentRun.Length; textPosition += currentRun.Length;
@ -1257,10 +1161,11 @@ namespace Avalonia.Media.TextFormatting
} }
runIndex++; runIndex++;
previousRun = currentRun; previousRun = currentRun;
} }
return runIndex; return currentRun;
} }
private TextLineMetrics CreateLineMetrics() private TextLineMetrics CreateLineMetrics()

7
src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs

@ -99,14 +99,15 @@ namespace Avalonia.Threading
} }
} }
public static RestoreContext Ensure(DispatcherPriority priority) public static RestoreContext Ensure(DispatcherPriority priority) => Ensure(Dispatcher.UIThread, priority);
public static RestoreContext Ensure(Dispatcher dispatcher, DispatcherPriority priority)
{ {
if (Current is AvaloniaSynchronizationContext avaloniaContext if (Current is AvaloniaSynchronizationContext avaloniaContext
&& avaloniaContext.Priority == priority) && avaloniaContext.Priority == priority)
return default; return default;
var oldContext = Current; var oldContext = Current;
Dispatcher.UIThread.VerifyAccess(); dispatcher.VerifyAccess();
SetSynchronizationContext(Dispatcher.UIThread.GetContextWithPriority(priority)); SetSynchronizationContext(dispatcher.GetContextWithPriority(priority));
return new RestoreContext(oldContext); return new RestoreContext(oldContext);
} }
} }

4
src/Avalonia.Base/Threading/Dispatcher.Invoke.cs

@ -106,7 +106,7 @@ public partial class Dispatcher
// call the callback directly. // call the callback directly.
if (!cancellationToken.IsCancellationRequested && priority == DispatcherPriority.Send && CheckAccess()) if (!cancellationToken.IsCancellationRequested && priority == DispatcherPriority.Send && CheckAccess())
{ {
using (AvaloniaSynchronizationContext.Ensure(priority)) using (AvaloniaSynchronizationContext.Ensure(this, priority))
callback(); callback();
return; return;
} }
@ -228,7 +228,7 @@ public partial class Dispatcher
// call the callback directly. // call the callback directly.
if (!cancellationToken.IsCancellationRequested && priority == DispatcherPriority.Send && CheckAccess()) if (!cancellationToken.IsCancellationRequested && priority == DispatcherPriority.Send && CheckAccess())
{ {
using (AvaloniaSynchronizationContext.Ensure(priority)) using (AvaloniaSynchronizationContext.Ensure(this, priority))
return callback(); return callback();
} }

2
src/Avalonia.Base/Threading/Dispatcher.MainLoop.cs

@ -49,7 +49,7 @@ public partial class Dispatcher
try try
{ {
_frames.Push(frame); _frames.Push(frame);
using (AvaloniaSynchronizationContext.Ensure(DispatcherPriority.Normal)) using (AvaloniaSynchronizationContext.Ensure(this, DispatcherPriority.Normal))
frame.Run(_controlledImpl); frame.Run(_controlledImpl);
} }
finally finally

20
src/Avalonia.Base/Threading/Dispatcher.Queue.cs

@ -9,7 +9,9 @@ public partial class Dispatcher
private readonly DispatcherPriorityQueue _queue = new(); private readonly DispatcherPriorityQueue _queue = new();
private bool _signaled; private bool _signaled;
private bool _explicitBackgroundProcessingRequested; private bool _explicitBackgroundProcessingRequested;
private const int MaximumTimeProcessingBackgroundJobs = 50; private const int MaximumInputStarvationTimeInFallbackMode = 50;
private const int MaximumInputStarvationTimeInExplicitProcessingExplicitMode = 50;
private int _maximumInputStarvationTime;
void RequestBackgroundProcessing() void RequestBackgroundProcessing()
{ {
@ -35,8 +37,8 @@ public partial class Dispatcher
lock (InstanceLock) lock (InstanceLock)
{ {
_explicitBackgroundProcessingRequested = false; _explicitBackgroundProcessingRequested = false;
ExecuteJobsCore();
} }
ExecuteJobsCore(true);
} }
/// <summary> /// <summary>
@ -130,10 +132,10 @@ public partial class Dispatcher
lock (InstanceLock) lock (InstanceLock)
_signaled = false; _signaled = false;
ExecuteJobsCore(); ExecuteJobsCore(false);
} }
void ExecuteJobsCore() void ExecuteJobsCore(bool fromExplicitBackgroundProcessingCallback)
{ {
long? backgroundJobExecutionStartedAt = null; long? backgroundJobExecutionStartedAt = null;
while (true) while (true)
@ -151,7 +153,6 @@ public partial class Dispatcher
if (job.Priority > DispatcherPriority.Input) if (job.Priority > DispatcherPriority.Input)
{ {
ExecuteJob(job); ExecuteJob(job);
backgroundJobExecutionStartedAt = null;
} }
// If platform supports pending input query, ask the platform if we can continue running low priority jobs // If platform supports pending input query, ask the platform if we can continue running low priority jobs
else if (_pendingInputImpl?.CanQueryPendingInput == true) else if (_pendingInputImpl?.CanQueryPendingInput == true)
@ -164,6 +165,13 @@ public partial class Dispatcher
return; return;
} }
} }
// We can't ask if the implementation has pending input, so we should let it to call us back
// Once it thinks that input is handled
else if (_backgroundProcessingImpl != null && !fromExplicitBackgroundProcessingCallback)
{
RequestBackgroundProcessing();
return;
}
// We can't check if there is pending input, but still need to enforce interactivity // We can't check if there is pending input, but still need to enforce interactivity
// so we stop processing background jobs after some timeout and start a timer to continue later // so we stop processing background jobs after some timeout and start a timer to continue later
else else
@ -171,7 +179,7 @@ public partial class Dispatcher
if (backgroundJobExecutionStartedAt == null) if (backgroundJobExecutionStartedAt == null)
backgroundJobExecutionStartedAt = Now; backgroundJobExecutionStartedAt = Now;
if (Now - backgroundJobExecutionStartedAt.Value > MaximumTimeProcessingBackgroundJobs) if (Now - backgroundJobExecutionStartedAt.Value > _maximumInputStarvationTime)
{ {
_signaled = true; _signaled = true;
RequestBackgroundProcessing(); RequestBackgroundProcessing();

2
src/Avalonia.Base/Threading/Dispatcher.Timers.cs

@ -127,7 +127,7 @@ public partial class Dispatcher
if (needToPromoteTimers) if (needToPromoteTimers)
PromoteTimers(); PromoteTimers();
if (needToProcessQueue) if (needToProcessQueue)
ExecuteJobsCore(); ExecuteJobsCore(false);
UpdateOSTimer(); UpdateOSTimer();
} }

3
src/Avalonia.Base/Threading/Dispatcher.cs

@ -34,6 +34,9 @@ public partial class Dispatcher : IDispatcher
_controlledImpl = _impl as IControlledDispatcherImpl; _controlledImpl = _impl as IControlledDispatcherImpl;
_pendingInputImpl = _impl as IDispatcherImplWithPendingInput; _pendingInputImpl = _impl as IDispatcherImplWithPendingInput;
_backgroundProcessingImpl = _impl as IDispatcherImplWithExplicitBackgroundProcessing; _backgroundProcessingImpl = _impl as IDispatcherImplWithExplicitBackgroundProcessing;
_maximumInputStarvationTime = _backgroundProcessingImpl == null ?
MaximumInputStarvationTimeInFallbackMode :
MaximumInputStarvationTimeInExplicitProcessingExplicitMode;
if (_backgroundProcessingImpl != null) if (_backgroundProcessingImpl != null)
_backgroundProcessingImpl.ReadyForBackgroundProcessing += OnReadyForExplicitBackgroundProcessing; _backgroundProcessingImpl.ReadyForBackgroundProcessing += OnReadyForExplicitBackgroundProcessing;
} }

8
src/Avalonia.Base/Threading/DispatcherFrame.cs

@ -38,10 +38,14 @@ public class DispatcherFrame
/// for their important criteria to be met. These frames /// for their important criteria to be met. These frames
/// should have a timeout associated with them. /// should have a timeout associated with them.
/// </param> /// </param>
public DispatcherFrame(bool exitWhenRequested) public DispatcherFrame(bool exitWhenRequested) : this(Dispatcher.UIThread, exitWhenRequested)
{ {
Dispatcher = Dispatcher.UIThread;
Dispatcher.VerifyAccess(); Dispatcher.VerifyAccess();
}
internal DispatcherFrame(Dispatcher dispatcher, bool exitWhenRequested)
{
Dispatcher = dispatcher;
_exitWhenRequested = exitWhenRequested; _exitWhenRequested = exitWhenRequested;
_continue = true; _continue = true;
} }

2
src/Avalonia.Base/Threading/DispatcherOperation.cs

@ -258,7 +258,7 @@ public class DispatcherOperation
try try
{ {
using (AvaloniaSynchronizationContext.Ensure(Priority)) using (AvaloniaSynchronizationContext.Ensure(Dispatcher, Priority))
InvokeCore(); InvokeCore();
} }
finally finally

19
src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs

@ -473,18 +473,31 @@ namespace Avalonia.Controls.Presenters
} }
Viewport = finalSize; Viewport = finalSize;
Extent = ComputeExtent(finalSize);
_isAnchorElementDirty = true;
return finalSize;
}
private Size ComputeExtent(Size viewportSize)
{
var childMargin = Child!.Margin; var childMargin = Child!.Margin;
if (Child.UseLayoutRounding) if (Child.UseLayoutRounding)
{ {
var scale = LayoutHelper.GetLayoutScale(Child); var scale = LayoutHelper.GetLayoutScale(Child);
childMargin = LayoutHelper.RoundLayoutThickness(childMargin, scale, scale); childMargin = LayoutHelper.RoundLayoutThickness(childMargin, scale, scale);
} }
Extent = Child!.Bounds.Size.Inflate(childMargin); var extent = Child!.Bounds.Size.Inflate(childMargin);
_isAnchorElementDirty = true;
return finalSize; if (MathUtilities.AreClose(extent.Width, viewportSize.Width, LayoutHelper.LayoutEpsilon))
extent = extent.WithWidth(viewportSize.Width);
if (MathUtilities.AreClose(extent.Height, viewportSize.Height, LayoutHelper.LayoutEpsilon))
extent = extent.WithHeight(viewportSize.Height);
return extent;
} }
private void OnScrollGesture(object? sender, ScrollGestureEventArgs e) private void OnScrollGesture(object? sender, ScrollGestureEventArgs e)

7
src/Avalonia.Controls/Window.cs

@ -169,6 +169,7 @@ namespace Avalonia.Controls
private readonly Size _maxPlatformClientSize; private readonly Size _maxPlatformClientSize;
private bool _shown; private bool _shown;
private bool _showingAsDialog; private bool _showingAsDialog;
private bool _wasShownBefore;
/// <summary> /// <summary>
/// Initializes static members of the <see cref="Window"/> class. /// Initializes static members of the <see cref="Window"/> class.
@ -718,6 +719,7 @@ namespace Avalonia.Controls
StartRendering(); StartRendering();
PlatformImpl?.Show(ShowActivated, false); PlatformImpl?.Show(ShowActivated, false);
OnOpened(EventArgs.Empty); OnOpened(EventArgs.Empty);
_wasShownBefore = true;
} }
} }
@ -871,6 +873,11 @@ namespace Avalonia.Controls
private void SetWindowStartupLocation(Window? owner = null) private void SetWindowStartupLocation(Window? owner = null)
{ {
if (_wasShownBefore == true)
{
return;
}
var startupLocation = WindowStartupLocation; var startupLocation = WindowStartupLocation;
if (startupLocation == WindowStartupLocation.CenterOwner && if (startupLocation == WindowStartupLocation.CenterOwner &&

10
src/Avalonia.Native/AvaloniaNativePlatform.cs

@ -3,9 +3,7 @@ using System.Runtime.InteropServices;
using Avalonia.Controls.Platform; using Avalonia.Controls.Platform;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Input.Platform; using Avalonia.Input.Platform;
using Avalonia.MicroCom;
using Avalonia.Native.Interop; using Avalonia.Native.Interop;
using Avalonia.OpenGL;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.Rendering; using Avalonia.Rendering;
using Avalonia.Rendering.Composition; using Avalonia.Rendering.Composition;
@ -163,6 +161,14 @@ namespace Avalonia.Native
Compositor = new Compositor(_platformGraphics, true); Compositor = new Compositor(_platformGraphics, true);
AppDomain.CurrentDomain.ProcessExit += OnProcessExit;
}
private void OnProcessExit(object? sender, EventArgs e)
{
AppDomain.CurrentDomain.ProcessExit -= OnProcessExit;
_factory.Dispose();
} }
public ITrayIconImpl CreateTrayIcon() public ITrayIconImpl CreateTrayIcon()

12
src/Browser/Avalonia.Browser/webapp/package-lock.json

@ -3296,9 +3296,9 @@
} }
}, },
"node_modules/word-wrap": { "node_modules/word-wrap": {
"version": "1.2.3", "version": "1.2.4",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.4.tgz",
"integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", "integrity": "sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA==",
"dev": true, "dev": true,
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
@ -5560,9 +5560,9 @@
} }
}, },
"word-wrap": { "word-wrap": {
"version": "1.2.3", "version": "1.2.4",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.4.tgz",
"integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", "integrity": "sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA==",
"dev": true "dev": true
}, },
"wrappy": { "wrappy": {

2
tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs

@ -111,7 +111,7 @@ namespace Avalonia.Base.UnitTests.Media
using(UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) using(UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
using (var glyphRun = CreateGlyphRun(advances, clusters, bidiLevel)) using (var glyphRun = CreateGlyphRun(advances, clusters, bidiLevel))
{ {
var characterHit = glyphRun.GetPreviousCaretCharacterHit(new CharacterHit(currentIndex, currentLength)); var characterHit = glyphRun.GetPreviousCaretCharacterHit(new CharacterHit(currentIndex + currentLength));
Assert.Equal(previousIndex, characterHit.FirstCharacterIndex); Assert.Equal(previousIndex, characterHit.FirstCharacterIndex);

31
tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests.cs

@ -275,6 +275,37 @@ namespace Avalonia.Controls.UnitTests.Presenters
Assert.Equal(new Size(203.2, 203.2), target.Extent); Assert.Equal(new Size(203.2, 203.2), target.Extent);
} }
[Fact]
public void Extent_Should_Be_Rounded_To_Viewport_When_Close()
{
var root = new TestRoot
{
LayoutScaling = 1.75,
UseLayoutRounding = true
};
var target = new ScrollContentPresenter
{
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
Content = new Border
{
Width = 164.57142857142858,
Height = 164.57142857142858,
Margin = new Thickness(6)
}
};
root.Child = target;
target.UpdateChild();
target.Measure(new Size(1000, 1000));
target.Arrange(new Rect(0, 0, 1000, 1000));
Assert.Equal(new Size(176.00000000000003, 176.00000000000003), target.Child!.DesiredSize);
Assert.Equal(new Size(176, 176), target.Viewport);
Assert.Equal(new Size(176, 176), target.Extent);
}
[Fact] [Fact]
public void Extent_Width_Should_Be_Arrange_Width_When_CanScrollHorizontally_False() public void Extent_Width_Should_Be_Arrange_Width_When_CanScrollHorizontally_False()
{ {

35
tests/Avalonia.Controls.UnitTests/WindowTests.cs

@ -513,6 +513,41 @@ namespace Avalonia.Controls.UnitTests
} }
} }
[Fact]
public void Window_Should_Not_Be_Centered_When_WindowStartupLocation_Is_CenterScreen_And_Window_Is_Hidden_And_Shown()
{
var screen1 = new Mock<Screen>(1.0, new PixelRect(new PixelSize(1920, 1080)), new PixelRect(new PixelSize(1920, 1040)), true);
var screens = new Mock<IScreenImpl>();
screens.Setup(x => x.AllScreens).Returns(new Screen[] { screen1.Object });
screens.Setup(x => x.ScreenFromPoint(It.IsAny<PixelPoint>())).Returns(screen1.Object);
var windowImpl = MockWindowingPlatform.CreateWindowMock();
windowImpl.Setup(x => x.ClientSize).Returns(new Size(800, 480));
windowImpl.Setup(x => x.DesktopScaling).Returns(1);
windowImpl.Setup(x => x.RenderScaling).Returns(1);
windowImpl.Setup(x => x.Screen).Returns(screens.Object);
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var window = new Window(windowImpl.Object)
{
WindowStartupLocation = WindowStartupLocation.CenterScreen
};
window.Show();
var expected = new PixelPoint(150, 400);
window.Position = expected;
window.IsVisible = false;
window.IsVisible = true;
Assert.Equal(expected, window.Position);
}
}
[Fact] [Fact]
public void Window_Should_Be_Centered_When_WindowStartupLocation_Is_CenterScreen() public void Window_Should_Be_Centered_When_WindowStartupLocation_Is_CenterScreen()
{ {

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

@ -194,7 +194,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
for (var i = 0; i < clusters.Count; i++) for (var i = 0; i < clusters.Count; i++)
{ {
var expectedCluster = clusters[i]; var expectedCluster = clusters[i];
var actualCluster = nextCharacterHit.FirstCharacterIndex; var actualCluster = nextCharacterHit.FirstCharacterIndex + nextCharacterHit.TrailingLength;
Assert.Equal(expectedCluster, actualCluster); Assert.Equal(expectedCluster, actualCluster);
@ -278,16 +278,6 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
Assert.Equal(clusters[i], Assert.Equal(clusters[i],
previousCharacterHit.FirstCharacterIndex + previousCharacterHit.TrailingLength); previousCharacterHit.FirstCharacterIndex + previousCharacterHit.TrailingLength);
} }
firstCharacterHit = previousCharacterHit;
firstCharacterHit = textLine.GetPreviousCaretCharacterHit(firstCharacterHit);
previousCharacterHit = textLine.GetPreviousCaretCharacterHit(firstCharacterHit);
Assert.Equal(firstCharacterHit.FirstCharacterIndex, previousCharacterHit.FirstCharacterIndex);
Assert.Equal(0, previousCharacterHit.TrailingLength);
} }
} }
@ -728,6 +718,110 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
} }
} }
[Fact]
public void Should_GetNextCaretCharacterHit_From_Mixed_TextBuffer()
{
using (Start())
{
var defaultProperties = new GenericTextRunProperties(Typeface.Default);
var textSource = new MixedTextBufferTextSource();
var formatter = new TextFormatterImpl();
var textLine =
formatter.FormatLine(textSource, 0, double.PositiveInfinity,
new GenericTextParagraphProperties(defaultProperties));
var characterHit = textLine.GetNextCaretCharacterHit(new CharacterHit(9, 1));
Assert.Equal(10, characterHit.FirstCharacterIndex);
Assert.Equal(1, characterHit.TrailingLength);
characterHit = textLine.GetNextCaretCharacterHit(characterHit);
Assert.Equal(11, characterHit.FirstCharacterIndex);
Assert.Equal(1, characterHit.TrailingLength);
characterHit = textLine.GetNextCaretCharacterHit(new CharacterHit(19, 1));
Assert.Equal(20, characterHit.FirstCharacterIndex);
Assert.Equal(1, characterHit.TrailingLength);
characterHit = textLine.GetNextCaretCharacterHit(new CharacterHit(10));
Assert.Equal(11, characterHit.FirstCharacterIndex);
Assert.Equal(0, characterHit.TrailingLength);
characterHit = textLine.GetNextCaretCharacterHit(characterHit);
Assert.Equal(12, characterHit.FirstCharacterIndex);
Assert.Equal(0, characterHit.TrailingLength);
characterHit = textLine.GetNextCaretCharacterHit(new CharacterHit(20));
Assert.Equal(21, characterHit.FirstCharacterIndex);
Assert.Equal(0, characterHit.TrailingLength);
}
}
[Fact]
public void Should_GetPreviousCaretCharacterHit_From_Mixed_TextBuffer()
{
using (Start())
{
var defaultProperties = new GenericTextRunProperties(Typeface.Default);
var textSource = new MixedTextBufferTextSource();
var formatter = new TextFormatterImpl();
var textLine =
formatter.FormatLine(textSource, 0, double.PositiveInfinity,
new GenericTextParagraphProperties(defaultProperties));
var characterHit = textLine.GetPreviousCaretCharacterHit(new CharacterHit(20, 1));
Assert.Equal(19, characterHit.FirstCharacterIndex);
Assert.Equal(1, characterHit.TrailingLength);
characterHit = textLine.GetPreviousCaretCharacterHit(new CharacterHit(10, 1));
Assert.Equal(9, characterHit.FirstCharacterIndex);
Assert.Equal(1, characterHit.TrailingLength);
characterHit = textLine.GetPreviousCaretCharacterHit(characterHit);
Assert.Equal(8, characterHit.FirstCharacterIndex);
Assert.Equal(1, characterHit.TrailingLength);
characterHit = textLine.GetPreviousCaretCharacterHit(new CharacterHit(21));
Assert.Equal(20, characterHit.FirstCharacterIndex);
Assert.Equal(0, characterHit.TrailingLength);
characterHit = textLine.GetPreviousCaretCharacterHit(new CharacterHit(11));
Assert.Equal(10, characterHit.FirstCharacterIndex);
Assert.Equal(0, characterHit.TrailingLength);
characterHit = textLine.GetPreviousCaretCharacterHit(characterHit);
Assert.Equal(9, characterHit.FirstCharacterIndex);
Assert.Equal(0, characterHit.TrailingLength);
}
}
private class MixedTextBufferTextSource : ITextSource private class MixedTextBufferTextSource : ITextSource
{ {
public TextRun? GetTextRun(int textSourceIndex) public TextRun? GetTextRun(int textSourceIndex)

Loading…
Cancel
Save