diff --git a/native/Avalonia.Native/src/OSX/app.mm b/native/Avalonia.Native/src/OSX/app.mm index 9cc9fc9523..88cdf4d9de 100644 --- a/native/Avalonia.Native/src/OSX/app.mm +++ b/native/Avalonia.Native/src/OSX/app.mm @@ -2,6 +2,7 @@ #include "AvnString.h" @interface AvnAppDelegate : NSObject -(AvnAppDelegate* _Nonnull) initWithEvents: (IAvnApplicationEvents* _Nonnull) events; +-(void) releaseEvents; @end NSApplicationActivationPolicy AvnDesiredActivationPolicy = NSApplicationActivationPolicyRegular; @@ -15,6 +16,11 @@ ComPtr _events; return self; } +- (void)releaseEvents +{ + _events = nil; +} + - (void)applicationWillFinishLaunching:(NSNotification *)notification { 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() { START_COM_CALL; diff --git a/native/Avalonia.Native/src/OSX/common.h b/native/Avalonia.Native/src/OSX/common.h index 5cf1b94a2f..672525c64a 100644 --- a/native/Avalonia.Native/src/OSX/common.h +++ b/native/Avalonia.Native/src/OSX/common.h @@ -38,6 +38,7 @@ extern IAvnMenu* GetAppMenu (); extern NSMenuItem* GetAppMenuItem (); extern void InitializeAvnApp(IAvnApplicationEvents* events, bool disableAppDelegate); +extern void ReleaseAvnAppEvents(); extern NSApplicationActivationPolicy AvnDesiredActivationPolicy; extern NSPoint ToNSPoint (AvnPoint p); extern NSRect ToNSRect (AvnRect r); diff --git a/native/Avalonia.Native/src/OSX/main.mm b/native/Avalonia.Native/src/OSX/main.mm index 1c7e2cf25a..3fddb72529 100644 --- a/native/Avalonia.Native/src/OSX/main.mm +++ b/native/Avalonia.Native/src/OSX/main.mm @@ -197,6 +197,14 @@ class AvaloniaNative : public ComSingleObject - - - - - - - diff --git a/src/Avalonia.Base/Animation/Transitions/Rotate3DTransition.cs b/src/Avalonia.Base/Animation/Transitions/Rotate3DTransition.cs index b4a1f02f09..fb8e9488a7 100644 --- a/src/Avalonia.Base/Animation/Transitions/Rotate3DTransition.cs +++ b/src/Avalonia.Base/Animation/Transitions/Rotate3DTransition.cs @@ -89,6 +89,7 @@ public class Rotate3DTransition: PageSlide { Easing = SlideInEasing, Duration = Duration, + FillMode = FillMode.Forward, Children = { CreateKeyFrame(0d, 90d * (forward ? 1 : -1), 1), diff --git a/src/Avalonia.Base/AvaloniaObjectExtensions.cs b/src/Avalonia.Base/AvaloniaObjectExtensions.cs index b3f41eb420..833cd4b034 100644 --- a/src/Avalonia.Base/AvaloniaObjectExtensions.cs +++ b/src/Avalonia.Base/AvaloniaObjectExtensions.cs @@ -60,6 +60,8 @@ namespace Avalonia } /// + /// The type of the values held by the . + /// The type of the value returned by the . /// /// /// A method which is executed to convert each property value to . @@ -71,6 +73,15 @@ namespace Avalonia converter ?? throw new ArgumentNullException(nameof(converter))); } + /// + public static IObservable GetObservable(this AvaloniaObject o, AvaloniaProperty property, Func converter) + { + return new AvaloniaPropertyObservable( + o ?? throw new ArgumentNullException(nameof(o)), + property ?? throw new ArgumentNullException(nameof(property)), + converter ?? throw new ArgumentNullException(nameof(converter))); + } + /// /// Gets an observable for an . /// @@ -92,6 +103,15 @@ namespace Avalonia property ?? throw new ArgumentNullException(nameof(property))); } + /// + public static IObservable> GetBindingObservable(this AvaloniaObject o, AvaloniaProperty property, Func converter) + { + return new AvaloniaPropertyBindingObservable( + o ?? throw new ArgumentNullException(nameof(o)), + property ?? throw new ArgumentNullException(nameof(property)), + converter?? throw new ArgumentNullException(nameof(converter))); + } + /// /// Gets an observable for an . /// diff --git a/src/Avalonia.Base/Media/GlyphRun.cs b/src/Avalonia.Base/Media/GlyphRun.cs index 0f70386424..fcb2cec733 100644 --- a/src/Avalonia.Base/Media/GlyphRun.cs +++ b/src/Avalonia.Base/Media/GlyphRun.cs @@ -424,13 +424,13 @@ namespace Avalonia.Media /// public CharacterHit GetPreviousCaretCharacterHit(CharacterHit characterHit) { + var previousCharacterHit = FindNearestCharacterHit(characterHit.FirstCharacterIndex - 1, out _); + if (characterHit.TrailingLength != 0) { - return new CharacterHit(characterHit.FirstCharacterIndex); + return previousCharacterHit; } - var previousCharacterHit = FindNearestCharacterHit(characterHit.FirstCharacterIndex - 1, out _); - return new CharacterHit(previousCharacterHit.FirstCharacterIndex); } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index ca31d9a6d0..44f53420de 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -9,7 +9,7 @@ namespace Avalonia.Media.TextFormatting internal static Comparer TextBoundsComparer { get; } = Comparer.Create((x, y) => x.Rectangle.Left.CompareTo(y.Rectangle.Left)); - private IReadOnlyList? _indexedTextRuns; + internal IReadOnlyList? _indexedTextRuns; private readonly TextRun[] _textRuns; private readonly double _paragraphWidth; private readonly TextParagraphProperties _paragraphProperties; @@ -512,38 +512,45 @@ namespace Avalonia.Media.TextFormatting /// public override CharacterHit GetNextCaretCharacterHit(CharacterHit characterHit) { - if (_textRuns.Length == 0) + if (_textRuns.Length == 0 || _indexedTextRuns is null) { return new CharacterHit(); } - if (TryFindNextCharacterHit(characterHit, out var nextCharacterHit)) - { - return nextCharacterHit; - } - - var lastTextPosition = FirstTextSourceIndex + Length; + var currentCharacterrHit = characterHit; + var characterIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength; - // Can't move, we're after the last character - var runIndex = GetRunIndexAtCharacterIndex(lastTextPosition, LogicalDirection.Forward, out var currentPosition); + var currentRun = GetRunAtCharacterIndex(characterIndex, LogicalDirection.Forward, out var currentPosition); - var currentRun = _textRuns[runIndex]; + var nextCharacterHit = characterHit; switch (currentRun) { 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; } - default: + case TextRun: { nextCharacterHit = new CharacterHit(currentPosition + currentRun.Length); break; } } - if (characterHit.FirstCharacterIndex + characterHit.TrailingLength == nextCharacterHit.FirstCharacterIndex + nextCharacterHit.TrailingLength) + if (characterIndex == nextCharacterHit.FirstCharacterIndex + nextCharacterHit.TrailingLength) { return characterHit; } @@ -554,17 +561,75 @@ namespace Avalonia.Media.TextFormatting /// 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; } /// @@ -1009,161 +1074,7 @@ namespace Avalonia.Media.TextFormatting if (_textLineBreak is null && _textRuns.Length > 1 && _textRuns[_textRuns.Length - 1] is TextEndOfLine textEndOfLine) { _textLineBreak = new TextLineBreak(textEndOfLine); - } - } - - /// - /// Tries to find the next character hit. - /// - /// The current character hit. - /// The next character hit. - /// - 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; - } - - /// - /// Tries to find the previous character hit. - /// - /// The current character hit. - /// The previous character hit. - /// - 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; } /// @@ -1173,15 +1084,23 @@ namespace Avalonia.Media.TextFormatting /// The logical direction. /// The text position of the found run index. /// The text run index. - private int GetRunIndexAtCharacterIndex(int codepointIndex, LogicalDirection direction, out int textPosition) + private TextRun? GetRunAtCharacterIndex(int codepointIndex, LogicalDirection direction, out int textPosition) { var runIndex = 0; textPosition = FirstTextSourceIndex; + + if (_indexedTextRuns is null) + { + return null; + } + + TextRun? currentRun = 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) { @@ -1189,64 +1108,49 @@ namespace Avalonia.Media.TextFormatting { var firstCluster = shapedRun.GlyphRun.Metrics.FirstCluster; - if (firstCluster > codepointIndex) - { - 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; - } - } - } + firstCluster += Math.Max(0, indexedRun.TextSourceCharacterIndex - firstCluster); if (direction == LogicalDirection.Forward) { - if (codepointIndex >= firstCluster && codepointIndex <= firstCluster + currentRun.Length) + if (codepointIndex >= firstCluster && codepointIndex < firstCluster + currentRun.Length) { - return runIndex; + return currentRun; } } else { - if (codepointIndex > firstCluster && - codepointIndex <= firstCluster + currentRun.Length) + if (previousRun is not null && previousRun is not ShapedTextRun && codepointIndex == textPosition + firstCluster) + { + textPosition -= previousRun.Length; + + return previousRun; + } + + if (codepointIndex > firstCluster && codepointIndex <= firstCluster + currentRun.Length) { - return runIndex; + return currentRun; } } if (runIndex + 1 >= _textRuns.Length) { - return runIndex; + return currentRun; } textPosition += currentRun.Length; break; } - default: + case TextRun: { if (codepointIndex == textPosition) { - return runIndex; + return currentRun; } if (runIndex + 1 >= _textRuns.Length) { - return runIndex; + return currentRun; } textPosition += currentRun.Length; @@ -1257,10 +1161,11 @@ namespace Avalonia.Media.TextFormatting } runIndex++; + previousRun = currentRun; } - return runIndex; + return currentRun; } private TextLineMetrics CreateLineMetrics() diff --git a/src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs b/src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs index e0563876bf..1efaa01442 100644 --- a/src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs +++ b/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 && avaloniaContext.Priority == priority) return default; var oldContext = Current; - Dispatcher.UIThread.VerifyAccess(); - SetSynchronizationContext(Dispatcher.UIThread.GetContextWithPriority(priority)); + dispatcher.VerifyAccess(); + SetSynchronizationContext(dispatcher.GetContextWithPriority(priority)); return new RestoreContext(oldContext); } } diff --git a/src/Avalonia.Base/Threading/Dispatcher.Invoke.cs b/src/Avalonia.Base/Threading/Dispatcher.Invoke.cs index 5995a03758..add990bd57 100644 --- a/src/Avalonia.Base/Threading/Dispatcher.Invoke.cs +++ b/src/Avalonia.Base/Threading/Dispatcher.Invoke.cs @@ -106,7 +106,7 @@ public partial class Dispatcher // call the callback directly. if (!cancellationToken.IsCancellationRequested && priority == DispatcherPriority.Send && CheckAccess()) { - using (AvaloniaSynchronizationContext.Ensure(priority)) + using (AvaloniaSynchronizationContext.Ensure(this, priority)) callback(); return; } @@ -228,7 +228,7 @@ public partial class Dispatcher // call the callback directly. if (!cancellationToken.IsCancellationRequested && priority == DispatcherPriority.Send && CheckAccess()) { - using (AvaloniaSynchronizationContext.Ensure(priority)) + using (AvaloniaSynchronizationContext.Ensure(this, priority)) return callback(); } diff --git a/src/Avalonia.Base/Threading/Dispatcher.MainLoop.cs b/src/Avalonia.Base/Threading/Dispatcher.MainLoop.cs index e1833fef2b..4b60ee7479 100644 --- a/src/Avalonia.Base/Threading/Dispatcher.MainLoop.cs +++ b/src/Avalonia.Base/Threading/Dispatcher.MainLoop.cs @@ -49,7 +49,7 @@ public partial class Dispatcher try { _frames.Push(frame); - using (AvaloniaSynchronizationContext.Ensure(DispatcherPriority.Normal)) + using (AvaloniaSynchronizationContext.Ensure(this, DispatcherPriority.Normal)) frame.Run(_controlledImpl); } finally diff --git a/src/Avalonia.Base/Threading/Dispatcher.Queue.cs b/src/Avalonia.Base/Threading/Dispatcher.Queue.cs index 829ab4cf87..1644332aea 100644 --- a/src/Avalonia.Base/Threading/Dispatcher.Queue.cs +++ b/src/Avalonia.Base/Threading/Dispatcher.Queue.cs @@ -9,7 +9,9 @@ public partial class Dispatcher private readonly DispatcherPriorityQueue _queue = new(); private bool _signaled; private bool _explicitBackgroundProcessingRequested; - private const int MaximumTimeProcessingBackgroundJobs = 50; + private const int MaximumInputStarvationTimeInFallbackMode = 50; + private const int MaximumInputStarvationTimeInExplicitProcessingExplicitMode = 50; + private int _maximumInputStarvationTime; void RequestBackgroundProcessing() { @@ -35,8 +37,8 @@ public partial class Dispatcher lock (InstanceLock) { _explicitBackgroundProcessingRequested = false; - ExecuteJobsCore(); } + ExecuteJobsCore(true); } /// @@ -130,10 +132,10 @@ public partial class Dispatcher lock (InstanceLock) _signaled = false; - ExecuteJobsCore(); + ExecuteJobsCore(false); } - void ExecuteJobsCore() + void ExecuteJobsCore(bool fromExplicitBackgroundProcessingCallback) { long? backgroundJobExecutionStartedAt = null; while (true) @@ -151,7 +153,6 @@ public partial class Dispatcher if (job.Priority > DispatcherPriority.Input) { ExecuteJob(job); - backgroundJobExecutionStartedAt = null; } // If platform supports pending input query, ask the platform if we can continue running low priority jobs else if (_pendingInputImpl?.CanQueryPendingInput == true) @@ -164,6 +165,13 @@ public partial class Dispatcher 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 // so we stop processing background jobs after some timeout and start a timer to continue later else @@ -171,7 +179,7 @@ public partial class Dispatcher if (backgroundJobExecutionStartedAt == null) backgroundJobExecutionStartedAt = Now; - if (Now - backgroundJobExecutionStartedAt.Value > MaximumTimeProcessingBackgroundJobs) + if (Now - backgroundJobExecutionStartedAt.Value > _maximumInputStarvationTime) { _signaled = true; RequestBackgroundProcessing(); diff --git a/src/Avalonia.Base/Threading/Dispatcher.Timers.cs b/src/Avalonia.Base/Threading/Dispatcher.Timers.cs index bb252b7f55..51408daad1 100644 --- a/src/Avalonia.Base/Threading/Dispatcher.Timers.cs +++ b/src/Avalonia.Base/Threading/Dispatcher.Timers.cs @@ -127,7 +127,7 @@ public partial class Dispatcher if (needToPromoteTimers) PromoteTimers(); if (needToProcessQueue) - ExecuteJobsCore(); + ExecuteJobsCore(false); UpdateOSTimer(); } diff --git a/src/Avalonia.Base/Threading/Dispatcher.cs b/src/Avalonia.Base/Threading/Dispatcher.cs index f257072dc8..46c6699b5a 100644 --- a/src/Avalonia.Base/Threading/Dispatcher.cs +++ b/src/Avalonia.Base/Threading/Dispatcher.cs @@ -34,6 +34,9 @@ public partial class Dispatcher : IDispatcher _controlledImpl = _impl as IControlledDispatcherImpl; _pendingInputImpl = _impl as IDispatcherImplWithPendingInput; _backgroundProcessingImpl = _impl as IDispatcherImplWithExplicitBackgroundProcessing; + _maximumInputStarvationTime = _backgroundProcessingImpl == null ? + MaximumInputStarvationTimeInFallbackMode : + MaximumInputStarvationTimeInExplicitProcessingExplicitMode; if (_backgroundProcessingImpl != null) _backgroundProcessingImpl.ReadyForBackgroundProcessing += OnReadyForExplicitBackgroundProcessing; } diff --git a/src/Avalonia.Base/Threading/DispatcherFrame.cs b/src/Avalonia.Base/Threading/DispatcherFrame.cs index e826432475..1a80740420 100644 --- a/src/Avalonia.Base/Threading/DispatcherFrame.cs +++ b/src/Avalonia.Base/Threading/DispatcherFrame.cs @@ -38,10 +38,14 @@ public class DispatcherFrame /// for their important criteria to be met. These frames /// should have a timeout associated with them. /// - public DispatcherFrame(bool exitWhenRequested) + public DispatcherFrame(bool exitWhenRequested) : this(Dispatcher.UIThread, exitWhenRequested) { - Dispatcher = Dispatcher.UIThread; Dispatcher.VerifyAccess(); + } + + internal DispatcherFrame(Dispatcher dispatcher, bool exitWhenRequested) + { + Dispatcher = dispatcher; _exitWhenRequested = exitWhenRequested; _continue = true; } diff --git a/src/Avalonia.Base/Threading/DispatcherOperation.cs b/src/Avalonia.Base/Threading/DispatcherOperation.cs index 8bd6d3bc01..0008d771c6 100644 --- a/src/Avalonia.Base/Threading/DispatcherOperation.cs +++ b/src/Avalonia.Base/Threading/DispatcherOperation.cs @@ -258,7 +258,7 @@ public class DispatcherOperation try { - using (AvaloniaSynchronizationContext.Ensure(Priority)) + using (AvaloniaSynchronizationContext.Ensure(Dispatcher, Priority)) InvokeCore(); } finally diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs index bd3d2b5171..4ff8e7cfef 100644 --- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs @@ -473,18 +473,31 @@ namespace Avalonia.Controls.Presenters } Viewport = finalSize; + Extent = ComputeExtent(finalSize); + _isAnchorElementDirty = true; + return finalSize; + } + + private Size ComputeExtent(Size viewportSize) + { var childMargin = Child!.Margin; + if (Child.UseLayoutRounding) { var scale = LayoutHelper.GetLayoutScale(Child); childMargin = LayoutHelper.RoundLayoutThickness(childMargin, scale, scale); } - Extent = Child!.Bounds.Size.Inflate(childMargin); - _isAnchorElementDirty = true; + var extent = Child!.Bounds.Size.Inflate(childMargin); - 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) diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index ce4fc8dd5c..dc58947994 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -169,6 +169,7 @@ namespace Avalonia.Controls private readonly Size _maxPlatformClientSize; private bool _shown; private bool _showingAsDialog; + private bool _wasShownBefore; /// /// Initializes static members of the class. @@ -718,6 +719,7 @@ namespace Avalonia.Controls StartRendering(); PlatformImpl?.Show(ShowActivated, false); OnOpened(EventArgs.Empty); + _wasShownBefore = true; } } @@ -871,6 +873,11 @@ namespace Avalonia.Controls private void SetWindowStartupLocation(Window? owner = null) { + if (_wasShownBefore == true) + { + return; + } + var startupLocation = WindowStartupLocation; if (startupLocation == WindowStartupLocation.CenterOwner && diff --git a/src/Avalonia.Native/AvaloniaNativePlatform.cs b/src/Avalonia.Native/AvaloniaNativePlatform.cs index 37e5e41faf..b7c58cded9 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatform.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatform.cs @@ -3,9 +3,7 @@ using System.Runtime.InteropServices; using Avalonia.Controls.Platform; using Avalonia.Input; using Avalonia.Input.Platform; -using Avalonia.MicroCom; using Avalonia.Native.Interop; -using Avalonia.OpenGL; using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Rendering.Composition; @@ -163,6 +161,14 @@ namespace Avalonia.Native 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() diff --git a/src/Browser/Avalonia.Browser/webapp/package-lock.json b/src/Browser/Avalonia.Browser/webapp/package-lock.json index 12757fd7a0..fd4c6a9539 100644 --- a/src/Browser/Avalonia.Browser/webapp/package-lock.json +++ b/src/Browser/Avalonia.Browser/webapp/package-lock.json @@ -3296,9 +3296,9 @@ } }, "node_modules/word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.4.tgz", + "integrity": "sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA==", "dev": true, "engines": { "node": ">=0.10.0" @@ -5560,9 +5560,9 @@ } }, "word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.4.tgz", + "integrity": "sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA==", "dev": true }, "wrappy": { diff --git a/tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs b/tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs index c273cc6489..69d7fc4916 100644 --- a/tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs +++ b/tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs @@ -111,7 +111,7 @@ namespace Avalonia.Base.UnitTests.Media using(UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) 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); diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests.cs index c7ea5c1b69..30628b1af8 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests.cs +++ b/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); } + [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] public void Extent_Width_Should_Be_Arrange_Width_When_CanScrollHorizontally_False() { diff --git a/tests/Avalonia.Controls.UnitTests/WindowTests.cs b/tests/Avalonia.Controls.UnitTests/WindowTests.cs index cd45a3d920..186b2f6836 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowTests.cs +++ b/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(1.0, new PixelRect(new PixelSize(1920, 1080)), new PixelRect(new PixelSize(1920, 1040)), true); + + var screens = new Mock(); + screens.Setup(x => x.AllScreens).Returns(new Screen[] { screen1.Object }); + screens.Setup(x => x.ScreenFromPoint(It.IsAny())).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] public void Window_Should_Be_Centered_When_WindowStartupLocation_Is_CenterScreen() { diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs index 12427e1f9e..d576a64523 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs +++ b/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++) { var expectedCluster = clusters[i]; - var actualCluster = nextCharacterHit.FirstCharacterIndex; + var actualCluster = nextCharacterHit.FirstCharacterIndex + nextCharacterHit.TrailingLength; Assert.Equal(expectedCluster, actualCluster); @@ -278,16 +278,6 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting Assert.Equal(clusters[i], 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 { public TextRun? GetTextRun(int textSourceIndex)