diff --git a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj index f11b027173..a07532412d 100644 --- a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj +++ b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj @@ -59,6 +59,7 @@ BC11A5BE2608D58F0017BAD0 /* automation.h in Headers */ = {isa = PBXBuildFile; fileRef = BC11A5BC2608D58F0017BAD0 /* automation.h */; }; BC11A5BF2608D58F0017BAD0 /* automation.mm in Sources */ = {isa = PBXBuildFile; fileRef = BC11A5BD2608D58F0017BAD0 /* automation.mm */; }; ED3791C42862E1F40080BD62 /* UniformTypeIdentifiers.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = ED3791C32862E1F40080BD62 /* UniformTypeIdentifiers.framework */; }; + ED754D262A97306B0078B4DF /* PlatformRenderTimer.mm in Sources */ = {isa = PBXBuildFile; fileRef = ED754D252A97306B0078B4DF /* PlatformRenderTimer.mm */; }; EDF8CDCD2964CB01001EE34F /* PlatformSettings.mm in Sources */ = {isa = PBXBuildFile; fileRef = EDF8CDCC2964CB01001EE34F /* PlatformSettings.mm */; }; /* End PBXBuildFile section */ @@ -122,6 +123,7 @@ BC11A5BC2608D58F0017BAD0 /* automation.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = automation.h; sourceTree = ""; }; BC11A5BD2608D58F0017BAD0 /* automation.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = automation.mm; sourceTree = ""; }; ED3791C32862E1F40080BD62 /* UniformTypeIdentifiers.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UniformTypeIdentifiers.framework; path = System/Library/Frameworks/UniformTypeIdentifiers.framework; sourceTree = SDKROOT; }; + ED754D252A97306B0078B4DF /* PlatformRenderTimer.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = PlatformRenderTimer.mm; sourceTree = ""; }; EDF8CDCC2964CB01001EE34F /* PlatformSettings.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = PlatformSettings.mm; sourceTree = ""; }; /* End PBXFileReference section */ @@ -162,6 +164,7 @@ AB7A61E62147C814003C5833 = { isa = PBXGroup; children = ( + ED754D252A97306B0078B4DF /* PlatformRenderTimer.mm */, 855EDC9E28C6546F00807998 /* PlatformBehaviorInhibition.mm */, 8D2F3511292F6AAE007FCF54 /* AvnTextInputMethodDelegate.h */, 8D300D68292E1E5D00320C49 /* AvnTextInputMethod.mm */, @@ -333,6 +336,7 @@ 18391068E48EF96E3DB5FDAB /* ResizeScope.mm in Sources */, 18391D4EB311BC7EF8B8C0A6 /* AvnView.mm in Sources */, 18391AA7E0BBA74D184C5734 /* AutoFitContentView.mm in Sources */, + ED754D262A97306B0078B4DF /* PlatformRenderTimer.mm in Sources */, 1839151F32D1BB1AB51A7BB6 /* AvnPanelWindow.mm in Sources */, 18391AC16726CBC45856233B /* AvnWindow.mm in Sources */, 18391D8CD1756DC858DC1A09 /* PopupImpl.mm in Sources */, diff --git a/native/Avalonia.Native/src/OSX/PlatformRenderTimer.mm b/native/Avalonia.Native/src/OSX/PlatformRenderTimer.mm new file mode 100644 index 0000000000..f372cc9047 --- /dev/null +++ b/native/Avalonia.Native/src/OSX/PlatformRenderTimer.mm @@ -0,0 +1,84 @@ +#include "common.h" + +class PlatformRenderTimer : public ComSingleObject +{ +private: + ComPtr _callback; + CVDisplayLinkRef _displayLink; + +public: + FORWARD_IUNKNOWN() + virtual HRESULT RegisterTick ( + IAvnActionCallback* callback) override + { + START_COM_CALL; + + @autoreleasepool + { + if (_displayLink != nil) + { + return E_UNEXPECTED; + } + + _callback = callback; + auto result = CVDisplayLinkCreateWithActiveCGDisplays(&_displayLink); + if (result != 0) + { + return E_FAIL; + } + + result = CVDisplayLinkSetOutputCallback(_displayLink, OnTick, this); + if (result != 0) + { + return E_FAIL; + } + } + return S_OK; + } + + virtual void Start () override + { + START_COM_CALL; + + @autoreleasepool + { + if (CVDisplayLinkIsRunning(_displayLink) == false) { + CVDisplayLinkStart(_displayLink); + } + } + } + + virtual void Stop () override + { + START_COM_CALL; + + @autoreleasepool + { + if (CVDisplayLinkIsRunning(_displayLink) == true) { + CVDisplayLinkStop(_displayLink); + } + } + } + + virtual bool RunsInBackground () override + { + START_COM_CALL; + + @autoreleasepool + { + return true; + } + } + + static CVReturn OnTick(CVDisplayLinkRef displayLink, const CVTimeStamp *inNow, const CVTimeStamp *inOutputTime, CVOptionFlags flagsIn, CVOptionFlags *flagsOut, void *displayLinkContext) + { + PlatformRenderTimer *object = (PlatformRenderTimer *)displayLinkContext; + object->_callback->Run(); + return kCVReturnSuccess; + } +}; + +extern IAvnPlatformRenderTimer* CreatePlatformRenderTimer() +{ + return new PlatformRenderTimer(); +} diff --git a/native/Avalonia.Native/src/OSX/common.h b/native/Avalonia.Native/src/OSX/common.h index 672525c64a..44441ee15d 100644 --- a/native/Avalonia.Native/src/OSX/common.h +++ b/native/Avalonia.Native/src/OSX/common.h @@ -32,6 +32,7 @@ extern IAvnApplicationCommands* CreateApplicationCommands(); extern IAvnPlatformBehaviorInhibition* CreatePlatformBehaviorInhibition(); extern IAvnNativeControlHost* CreateNativeControlHost(NSView* parent); extern IAvnPlatformSettings* CreatePlatformSettings(); +extern IAvnPlatformRenderTimer* CreatePlatformRenderTimer(); extern void SetAppMenu(IAvnMenu *menu); extern void SetServicesMenu (IAvnMenu* menu); extern IAvnMenu* GetAppMenu (); diff --git a/native/Avalonia.Native/src/OSX/main.mm b/native/Avalonia.Native/src/OSX/main.mm index 3fddb72529..41d6fd37ab 100644 --- a/native/Avalonia.Native/src/OSX/main.mm +++ b/native/Avalonia.Native/src/OSX/main.mm @@ -446,6 +446,17 @@ public: return S_OK; } } + + virtual HRESULT CreatePlatformRenderTimer(IAvnPlatformRenderTimer** ppv) override + { + START_COM_CALL; + + @autoreleasepool + { + *ppv = ::CreatePlatformRenderTimer(); + return S_OK; + } + } }; extern "C" IAvaloniaNativeFactory* CreateAvaloniaNative() diff --git a/readme.md b/readme.md index fc5e218a92..783e0aaddc 100644 --- a/readme.md +++ b/readme.md @@ -2,7 +2,7 @@ ![Header](https://user-images.githubusercontent.com/552074/235865745-2a8e7274-4f66-4f77-8f05-feeb76e7d478.png) [![Telegram](https://raw.githubusercontent.com/Patrolavia/telegram-badge/master/chat.svg)](https://t.me/Avalonia) -[![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/AvaloniaUI/Avalonia?utm_campaign=pr-badge&utm_content=badge&utm_medium=badge&utm_source=badge) [![Discord](https://img.shields.io/badge/discord-join%20chat-46BC99)]( https://aka.ms/dotnet-discord) [![Build Status](https://dev.azure.com/AvaloniaUI/AvaloniaUI/_apis/build/status/AvaloniaUI.Avalonia)](https://dev.azure.com/AvaloniaUI/AvaloniaUI/_build/latest?definitionId=4) [![Backers on Open Collective](https://opencollective.com/Avalonia/backers/badge.svg)](#backers) [![Sponsors on Open Collective](https://opencollective.com/Avalonia/sponsors/badge.svg)](#sponsors) ![License](https://img.shields.io/github/license/avaloniaui/avalonia.svg) +[![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/AvaloniaUI/Avalonia?utm_campaign=pr-badge&utm_content=badge&utm_medium=badge&utm_source=badge) [![Discord](https://img.shields.io/badge/discord-join%20chat-46BC99)]( https://aka.ms/dotnet-discord) [![Build Status](https://dev.azure.com/AvaloniaUI/AvaloniaUI/_apis/build/status/AvaloniaUI.Avalonia)](https://dev.azure.com/AvaloniaUI/AvaloniaUI/_build/latest?definitionId=4) [![Backers on Open Collective](https://img.shields.io/opencollective/backers/Avalonia?logo=opencollective)](#backers) [![Sponsors on Open Collective](https://img.shields.io/opencollective/sponsors/Avalonia?logo=opencollective)](#sponsors) [![GitHub Sponsors](https://img.shields.io/github/sponsors/AvaloniaUI?logo=github)](https://github.com/sponsors/AvaloniaUI) ![License](https://img.shields.io/github/license/avaloniaui/avalonia.svg)
[![NuGet](https://img.shields.io/nuget/v/Avalonia.svg)](https://www.nuget.org/packages/Avalonia) [![downloads](https://img.shields.io/nuget/dt/avalonia)](https://www.nuget.org/packages/Avalonia) ![Size](https://img.shields.io/github/repo-size/avaloniaui/avalonia.svg) diff --git a/samples/ControlCatalog/Pages/PointerCanvas.cs b/samples/ControlCatalog/Pages/PointerCanvas.cs index 32e46af9dd..1311a1a144 100644 --- a/samples/ControlCatalog/Pages/PointerCanvas.cs +++ b/samples/ControlCatalog/Pages/PointerCanvas.cs @@ -74,6 +74,7 @@ public class PointerCanvas : Control public void HandleEvent(PointerEventArgs e, Visual v) { e.Handled = true; + e.PreventGestureRecognition(); var currentPoint = e.GetCurrentPoint(v); if (e.RoutedEvent == PointerPressedEvent) AddPoint(currentPoint.Position, Brushes.Green, 10); diff --git a/samples/ControlCatalog/Pages/PointerContactsTab.cs b/samples/ControlCatalog/Pages/PointerContactsTab.cs index 1751b046c0..323ac685e5 100644 --- a/samples/ControlCatalog/Pages/PointerContactsTab.cs +++ b/samples/ControlCatalog/Pages/PointerContactsTab.cs @@ -72,6 +72,7 @@ public class PointerContactsTab : Control UpdatePointer(e); e.Pointer.Capture(this); e.Handled = true; + e.PreventGestureRecognition(); base.OnPointerPressed(e); } diff --git a/samples/ControlCatalog/Pages/PointersPage.xaml.cs b/samples/ControlCatalog/Pages/PointersPage.xaml.cs index 16d7ecceb5..16ee95469d 100644 --- a/samples/ControlCatalog/Pages/PointersPage.xaml.cs +++ b/samples/ControlCatalog/Pages/PointersPage.xaml.cs @@ -72,6 +72,7 @@ Position: ??? ???"; { e.Pointer.Capture(sender as Border); e.Handled = true; + e.PreventGestureRecognition(); } private void InitializeComponent() diff --git a/src/Avalonia.Base/Animation/Transition.cs b/src/Avalonia.Base/Animation/Transition.cs index 519ed52578..2ad92c9183 100644 --- a/src/Avalonia.Base/Animation/Transition.cs +++ b/src/Avalonia.Base/Animation/Transition.cs @@ -1,53 +1,22 @@ using System; -using System.Diagnostics.CodeAnalysis; -using Avalonia.Animation.Easings; namespace Avalonia.Animation { /// /// Defines how a property should be animated using a transition. /// - public abstract class Transition : AvaloniaObject, ITransition + public abstract class Transition : TransitionBase { - private AvaloniaProperty? _prop; - - /// - /// Gets or sets the duration of the transition. - /// - public TimeSpan Duration { get; set; } - - /// - /// Gets or sets delay before starting the transition. - /// - public TimeSpan Delay { get; set; } = TimeSpan.Zero; - - /// - /// Gets the easing class to be used. - /// - public Easing Easing { get; set; } = new LinearEasing(); - - /// - [DisallowNull] - public AvaloniaProperty? Property + static Transition() { - get - { - return _prop; - } - set - { - if (!(value.PropertyType.IsAssignableFrom(typeof(T)))) - throw new InvalidCastException - ($"Invalid property type \"{typeof(T).Name}\" for this transition: {GetType().Name}."); - - _prop = value; - } + PropertyProperty.Changed.AddClassHandler>((x, e) => x.OnPropertyPropertyChanged(e)); } - AvaloniaProperty ITransition.Property + private void OnPropertyPropertyChanged(AvaloniaPropertyChangedEventArgs e) { - get => Property ?? throw new InvalidOperationException("Transition has no property specified."); - set => Property = value; + if ((e.NewValue is AvaloniaProperty newValue) && !newValue.PropertyType.IsAssignableFrom(typeof(T))) + throw new InvalidCastException + ($"Invalid property type \"{typeof(T).Name}\" for this transition: {GetType().Name}."); } /// @@ -55,11 +24,7 @@ namespace Avalonia.Animation /// internal abstract IObservable DoTransition(IObservable progress, T oldValue, T newValue); - /// - IDisposable ITransition.Apply(Animatable control, IClock clock, object? oldValue, object? newValue) - => Apply(control, clock, oldValue, newValue); - - internal virtual IDisposable Apply(Animatable control, IClock clock, object? oldValue, object? newValue) + internal override IDisposable Apply(Animatable control, IClock clock, object? oldValue, object? newValue) { if (Property is null) throw new InvalidOperationException("Transition has no property specified."); diff --git a/src/Avalonia.Base/Animation/TransitionBase.cs b/src/Avalonia.Base/Animation/TransitionBase.cs new file mode 100644 index 0000000000..8cc06ce7ed --- /dev/null +++ b/src/Avalonia.Base/Animation/TransitionBase.cs @@ -0,0 +1,100 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using Avalonia.Animation.Easings; + +namespace Avalonia.Animation +{ + /// + /// Defines how a property should be animated using a transition. + /// + public abstract class TransitionBase : AvaloniaObject, ITransition + { + /// + /// Defines the property. + /// + public static readonly DirectProperty DurationProperty = + AvaloniaProperty.RegisterDirect( + nameof(Duration), + o => o._duration, + (o, v) => o._duration = v); + + /// + /// Defines the property. + /// + public static readonly DirectProperty DelayProperty = + AvaloniaProperty.RegisterDirect( + nameof(Delay), + o => o._delay, + (o, v) => o._delay = v); + + /// + /// Defines the property. + /// + public static readonly DirectProperty EasingProperty = + AvaloniaProperty.RegisterDirect( + nameof(Easing), + o => o._easing, + (o, v) => o._easing = v); + + /// + /// Defines the property. + /// + public static readonly DirectProperty PropertyProperty = + AvaloniaProperty.RegisterDirect( + nameof(Property), + o => o._prop, + (o, v) => o._prop = v); + + private TimeSpan _duration; + private TimeSpan _delay = TimeSpan.Zero; + private Easing _easing = new LinearEasing(); + private AvaloniaProperty? _prop; + + /// + /// Gets or sets the duration of the transition. + /// + public TimeSpan Duration + { + get { return _duration; } + set { SetAndRaise(DurationProperty, ref _duration, value); } + } + + /// + /// Gets or sets delay before starting the transition. + /// + public TimeSpan Delay + { + get { return _delay; } + set { SetAndRaise(DelayProperty, ref _delay, value); } + } + + /// + /// Gets the easing class to be used. + /// + public Easing Easing + { + get { return _easing; } + set { SetAndRaise(EasingProperty, ref _easing, value); } + } + + /// + [DisallowNull] + public AvaloniaProperty? Property + { + get { return _prop; } + set { SetAndRaise(PropertyProperty, ref _prop, value); } + } + + AvaloniaProperty ITransition.Property + { + get => Property ?? throw new InvalidOperationException("Transition has no property specified."); + set => Property = value; + } + + /// + IDisposable ITransition.Apply(Animatable control, IClock clock, object? oldValue, object? newValue) + => Apply(control, clock, oldValue, newValue); + + internal abstract IDisposable Apply(Animatable control, IClock clock, object? oldValue, object? newValue); + } +} diff --git a/src/Avalonia.Base/Animation/Transitions/Rotate3DTransition.cs b/src/Avalonia.Base/Animation/Transitions/Rotate3DTransition.cs index fb8e9488a7..239f3aea08 100644 --- a/src/Avalonia.Base/Animation/Transitions/Rotate3DTransition.cs +++ b/src/Avalonia.Base/Animation/Transitions/Rotate3DTransition.cs @@ -71,6 +71,7 @@ public class Rotate3DTransition: PageSlide { Easing = SlideOutEasing, Duration = Duration, + FillMode = FillMode.Forward, Children = { CreateKeyFrame(0d, 0d, 2), diff --git a/src/Avalonia.Base/Input/GestureRecognizers/GestureRecognizerCollection.cs b/src/Avalonia.Base/Input/GestureRecognizers/GestureRecognizerCollection.cs index 05dce8214b..74e8061292 100644 --- a/src/Avalonia.Base/Input/GestureRecognizers/GestureRecognizerCollection.cs +++ b/src/Avalonia.Base/Input/GestureRecognizers/GestureRecognizerCollection.cs @@ -1,3 +1,4 @@ +using System; using System.Collections; using System.Collections.Generic; using Avalonia.Controls; @@ -59,6 +60,20 @@ namespace Avalonia.Input.GestureRecognizers return e.Handled; } + internal void HandleCaptureLost(IPointer pointer) + { + if (_recognizers == null || pointer is not Pointer p) + return; + + foreach (var r in _recognizers) + { + if (p.CapturedGestureRecognizer == r) + continue; + + r.PointerCaptureLostInternal(pointer); + } + } + internal bool HandlePointerReleased(PointerReleasedEventArgs e) { if (_recognizers == null) diff --git a/src/Avalonia.Base/Input/GestureRecognizers/PinchGestureRecognizer.cs b/src/Avalonia.Base/Input/GestureRecognizers/PinchGestureRecognizer.cs index b02c82a066..5a22ba7a14 100644 --- a/src/Avalonia.Base/Input/GestureRecognizers/PinchGestureRecognizer.cs +++ b/src/Avalonia.Base/Input/GestureRecognizers/PinchGestureRecognizer.cs @@ -1,4 +1,5 @@ -using Avalonia.Input.GestureRecognizers; +using System.Diagnostics; +using Avalonia.Input.GestureRecognizers; namespace Avalonia.Input { @@ -110,6 +111,7 @@ namespace Avalonia.Input _secondContact = null; } + Target?.RaiseEvent(new PinchEndedEventArgs()); } } diff --git a/src/Avalonia.Base/Input/InputElement.cs b/src/Avalonia.Base/Input/InputElement.cs index 46f543d25b..91dae88dbb 100644 --- a/src/Avalonia.Base/Input/InputElement.cs +++ b/src/Avalonia.Base/Input/InputElement.cs @@ -230,6 +230,7 @@ namespace Avalonia.Input PointerMovedEvent.AddClassHandler((x, e) => x.OnGesturePointerMoved(e), handledEventsToo: true); PointerPressedEvent.AddClassHandler((x, e) => x.OnGesturePointerPressed(e), handledEventsToo: true); PointerReleasedEvent.AddClassHandler((x, e) => x.OnGesturePointerReleased(e), handledEventsToo: true); + PointerCaptureLostEvent.AddClassHandler((x, e) => x.OnGesturePointerCaptureLost(e), handledEventsToo: true); } public InputElement() @@ -615,6 +616,11 @@ namespace Avalonia.Input } } + private void OnGesturePointerCaptureLost(PointerCaptureLostEventArgs e) + { + _gestureRecognizers?.HandleCaptureLost(e.Pointer); + } + private void OnGesturePointerPressed(PointerPressedEventArgs e) { if (!e.IsGestureRecognitionSkipped) diff --git a/src/Avalonia.Base/Input/Pointer.cs b/src/Avalonia.Base/Input/Pointer.cs index 5744ddf963..e82432a00d 100644 --- a/src/Avalonia.Base/Input/Pointer.cs +++ b/src/Avalonia.Base/Input/Pointer.cs @@ -91,12 +91,16 @@ namespace Avalonia.Input internal void CaptureGestureRecognizer(GestureRecognizer? gestureRecognizer) { if (CapturedGestureRecognizer != gestureRecognizer) + { CapturedGestureRecognizer?.PointerCaptureLostInternal(this); + } + + CapturedGestureRecognizer = gestureRecognizer; if (gestureRecognizer != null) + { Capture(null); - - CapturedGestureRecognizer = gestureRecognizer; + } } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index a2936f1857..085e60da5f 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -1278,14 +1278,11 @@ namespace Avalonia.Media.TextFormatting var start = GetParagraphOffsetX(width, widthIncludingWhitespace); var overhangLeading = Math.Max(0, bounds.Left - start); var overhangTrailing = Math.Max(0, bounds.Width - widthIncludingWhitespace); - var hasOverflowed = overhangLeading + widthIncludingWhitespace + overhangTrailing > _paragraphWidth; + var hasOverflowed = width > _paragraphWidth; if (!double.IsNaN(lineHeight) && !MathUtilities.IsZero(lineHeight)) { - if (lineHeight > height) - { - height = lineHeight; - } + height = lineHeight; } return new TextLineMetrics diff --git a/src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.cs index ebf66164b7..a6cf363492 100644 --- a/src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.cs +++ b/src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.cs @@ -1,4 +1,4 @@ -// (c) Copyright Microsoft Corporation. +// (c) Copyright Microsoft Corporation. // This source is subject to the Microsoft Public License (Ms-PL). // Please see https://go.microsoft.com/fwlink/?LinkID=131993 for details. // All other rights reserved. @@ -165,6 +165,16 @@ namespace Avalonia.Controls /// private bool _allowWrite; + /// + /// A boolean indicating if a cancellation was requested + /// + private bool _cancelRequested; + + /// + /// A boolean indicating if filtering is in action + /// + private bool _filterInAction; + /// /// The TextBox template part. /// @@ -1379,6 +1389,15 @@ namespace Avalonia.Controls /// private void RefreshView() { + // If we have a running filter, trigger a request first + if (_filterInAction) + { + _cancelRequested = true; + } + + // Indicate that filtering is ongoing + _filterInAction = true; + if (_items == null) { ClearView(); @@ -1391,79 +1410,65 @@ namespace Avalonia.Controls // Determine if any filtering mode is on bool stringFiltering = TextFilter != null; bool objectFiltering = FilterMode == AutoCompleteFilterMode.Custom && TextFilter == null; - - int view_index = 0; - int view_count = _view!.Count; + List items = _items; + + // cache properties + var textFilter = TextFilter; + var itemFilter = ItemFilter; + var _newViewItems = new Collection(); + + // if the mode is objectFiltering and itemFilter is null, we throw an exception + if (objectFiltering && itemFilter is null) + { + // indicate that filtering is not ongoing anymore + _filterInAction = false; + _cancelRequested = false; + + throw new Exception( + "ItemFilter property can not be null when FilterMode has value AutoCompleteFilterMode.Custom"); + } + foreach (object item in items) { + // Exit the fitter when requested if cancellation is requested + if (_cancelRequested) + { + return; + } + bool inResults = !(stringFiltering || objectFiltering); + if (!inResults) { if (stringFiltering) { - inResults = TextFilter!(text, FormatValue(item)); + inResults = textFilter!(text, FormatValue(item)); } - else + else if (objectFiltering) { - if (ItemFilter is null) - { - throw new Exception("ItemFilter property can not be null when FilterMode has value AutoCompleteFilterMode.Custom"); - } - else - { - inResults = ItemFilter(text, item); - } + inResults = itemFilter!(text, item); } } - if (view_count > view_index && inResults && _view[view_index] == item) + if (inResults) { - // Item is still in the view - view_index++; - } - else if (inResults) - { - // Insert the item - if (view_count > view_index && _view[view_index] != item) - { - // Replace item - // Unfortunately replacing via index throws a fatal - // exception: View[view_index] = item; - // Cost: O(n) vs O(1) - _view.RemoveAt(view_index); - _view.Insert(view_index, item); - view_index++; - } - else - { - // Add the item - if (view_index == view_count) - { - // Constant time is preferred (Add). - _view.Add(item); - } - else - { - _view.Insert(view_index, item); - } - view_index++; - view_count++; - } - } - else if (view_count > view_index && _view[view_index] == item) - { - // Remove the item - _view.RemoveAt(view_index); - view_count--; + _newViewItems.Add(item); } } + _view?.Clear(); + _view?.AddRange(_newViewItems); + // Clear the evaluator to discard a reference to the last item if (_valueBindingEvaluator != null) { _valueBindingEvaluator.ClearDataContext(); } + + // indicate that filtering is not ongoing anymore + _filterInAction = false; + _cancelRequested = false; } /// diff --git a/src/Avalonia.Controls/Documents/Inline.cs b/src/Avalonia.Controls/Documents/Inline.cs index 23b806583e..7de3ce9d98 100644 --- a/src/Avalonia.Controls/Documents/Inline.cs +++ b/src/Avalonia.Controls/Documents/Inline.cs @@ -10,12 +10,14 @@ namespace Avalonia.Controls.Documents /// public abstract class Inline : TextElement { + // TODO12: change the field type to an AttachedProperty for consistency (breaking change) /// /// AvaloniaProperty for property. /// public static readonly StyledProperty TextDecorationsProperty = - AvaloniaProperty.Register( - nameof(TextDecorations)); + AvaloniaProperty.RegisterAttached( + nameof(TextDecorations), + inherits: true); /// /// AvaloniaProperty for property. @@ -43,7 +45,27 @@ namespace Avalonia.Controls.Documents get { return GetValue(BaselineAlignmentProperty); } set { SetValue(BaselineAlignmentProperty, value); } } + + /// + /// Gets the value of the attached on a control. + /// + /// The control. + /// The font style. + public static TextDecorationCollection? GetTextDecorations(Control control) + { + return control.GetValue(TextDecorationsProperty); + } + /// + /// Sets the value of the attached on a control. + /// + /// The control. + /// The property value to set. + public static void SetTextDecorations(Control control, TextDecorationCollection? value) + { + control.SetValue(TextDecorationsProperty, value); + } + internal abstract void BuildTextRun(IList textRuns); internal abstract void AppendText(StringBuilder stringBuilder); diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index 83aa88a7b6..41e9e7c111 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -67,6 +67,9 @@ namespace Avalonia.Controls public static readonly StyledProperty DisplayMemberBindingProperty = AvaloniaProperty.Register(nameof(DisplayMemberBinding)); + private static readonly AttachedProperty AppliedItemContainerTheme = + AvaloniaProperty.RegisterAttached("AppliedItemContainerTheme"); + /// /// Gets or sets the to use for binding to the display member of each item. /// @@ -663,13 +666,26 @@ namespace Avalonia.Controls internal void PrepareItemContainer(Control container, object? item, int index) { - var itemContainerTheme = ItemContainerTheme; - - if (itemContainerTheme is not null && - !container.IsSet(ThemeProperty) && - StyledElement.GetStyleKey(container) == itemContainerTheme.TargetType) + // If the container has no theme set, or we've already applied our ItemContainerTheme + // (and it hasn't changed since) then we're in control of the container's Theme and may + // need to update it. + if (!container.IsSet(ThemeProperty) || container.GetValue(AppliedItemContainerTheme) == container.Theme) { - container.Theme = itemContainerTheme; + var itemContainerTheme = ItemContainerTheme; + + if (itemContainerTheme?.TargetType?.IsAssignableFrom(GetStyleKey(container)) == true) + { + // We have an ItemContainerTheme and it matches the container. Set the Theme + // property, and mark the container as having had ItemContainerTheme applied. + container.SetCurrentValue(ThemeProperty, itemContainerTheme); + container.SetValue(AppliedItemContainerTheme, itemContainerTheme); + } + else + { + // Otherwise clear the theme and the AppliedItemContainerTheme property. + container.ClearValue(ThemeProperty); + container.ClearValue(AppliedItemContainerTheme); + } } if (item is not Control) diff --git a/src/Avalonia.Controls/Notifications/NotificationCard.cs b/src/Avalonia.Controls/Notifications/NotificationCard.cs index 705d40380e..7d5b6cc0ca 100644 --- a/src/Avalonia.Controls/Notifications/NotificationCard.cs +++ b/src/Avalonia.Controls/Notifications/NotificationCard.cs @@ -39,7 +39,7 @@ namespace Avalonia.Controls.Notifications this.GetObservable(ContentProperty) .Subscribe(x => { - if (x is Notification notification) + if (x is INotification notification) { switch (notification.Type) { diff --git a/src/Avalonia.Controls/Primitives/AccessText.cs b/src/Avalonia.Controls/Primitives/AccessText.cs index 2e570e25c7..7f939f2d3a 100644 --- a/src/Avalonia.Controls/Primitives/AccessText.cs +++ b/src/Avalonia.Controls/Primitives/AccessText.cs @@ -3,6 +3,7 @@ using Avalonia.Input; using Avalonia.Reactive; using Avalonia.Media; using Avalonia.Media.TextFormatting; +using System; namespace Avalonia.Controls.Primitives { @@ -68,11 +69,15 @@ namespace Avalonia.Controls.Primitives if (underscore != -1 && ShowAccessKey) { var rect = TextLayout!.HitTestTextPosition(underscore); - var offset = new Vector(0, -1.5); + + var x1 = Math.Round(rect.Left, MidpointRounding.AwayFromZero); + var x2 = Math.Round(rect.Right, MidpointRounding.AwayFromZero); + var y = Math.Round(rect.Bottom, MidpointRounding.AwayFromZero) - 1.5; + context.DrawLine( new Pen(Foreground, 1), - rect.BottomLeft + offset, - rect.BottomRight + offset); + new Point(x1, y), + new Point(x2, y)); } } diff --git a/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs b/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs index 5dd0f23e8e..d63759cc42 100644 --- a/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs +++ b/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs @@ -110,6 +110,13 @@ namespace Avalonia.Controls.Primitives get { var rc = new Rect(default, _overlayLayer.AvailableSize); + var topLevel = TopLevel.GetTopLevel(this); + if(topLevel != null) + { + var padding = topLevel.InsetsManager?.SafeAreaPadding ?? default; + rc = rc.Deflate(padding); + } + return new[] {new ManagedPopupPositionerScreenInfo(rc, rc)}; } } diff --git a/src/Avalonia.Controls/Shapes/Polygon.cs b/src/Avalonia.Controls/Shapes/Polygon.cs index 78def84448..e51e117e33 100644 --- a/src/Avalonia.Controls/Shapes/Polygon.cs +++ b/src/Avalonia.Controls/Shapes/Polygon.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using Avalonia.Media; +using Avalonia.Data; namespace Avalonia.Controls.Shapes { @@ -15,9 +16,9 @@ namespace Avalonia.Controls.Shapes public Polygon() { - Points = new Points(); + SetValue(PointsProperty, new Points(), BindingPriority.Template); } - + public IList Points { get => GetValue(PointsProperty); diff --git a/src/Avalonia.Controls/Shapes/Polyline.cs b/src/Avalonia.Controls/Shapes/Polyline.cs index 2533794f89..84fccb66f1 100644 --- a/src/Avalonia.Controls/Shapes/Polyline.cs +++ b/src/Avalonia.Controls/Shapes/Polyline.cs @@ -1,9 +1,10 @@ using System.Collections.Generic; using Avalonia.Media; +using Avalonia.Data; namespace Avalonia.Controls.Shapes { - public class Polyline: Shape + public class Polyline : Shape { public static readonly StyledProperty> PointsProperty = AvaloniaProperty.Register>("Points"); @@ -16,7 +17,7 @@ namespace Avalonia.Controls.Shapes public Polyline() { - Points = new Points(); + SetValue(PointsProperty, new Points(), BindingPriority.Template); } public IList Points diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index ea420c7c45..cc92bdd752 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -135,7 +135,7 @@ namespace Avalonia.Controls /// Defines the property. /// public static readonly StyledProperty TextDecorationsProperty = - AvaloniaProperty.Register(nameof(TextDecorations)); + Inline.TextDecorationsProperty.AddOwner(); /// /// Defines the property. diff --git a/src/Avalonia.Controls/ToggleSwitch.cs b/src/Avalonia.Controls/ToggleSwitch.cs index dc6b946381..65413cea5a 100644 --- a/src/Avalonia.Controls/ToggleSwitch.cs +++ b/src/Avalonia.Controls/ToggleSwitch.cs @@ -243,10 +243,6 @@ namespace Avalonia.Controls } UpdateKnobTransitions(); } - else - { - base.Toggle(); - } _isDragging = false; @@ -276,14 +272,6 @@ namespace Avalonia.Controls } } - protected override void Toggle() - { - if ((_switchKnob != null) && (!_switchKnob.IsPointerOver)) - { - base.Toggle(); - } - } - private void UpdateKnobPos(bool value) { if ((_switchKnob != null) && (_knobsPanel != null)) diff --git a/src/Avalonia.DesignerSupport/DesignWindowLoader.cs b/src/Avalonia.DesignerSupport/DesignWindowLoader.cs index c248116614..0a901ae798 100644 --- a/src/Avalonia.DesignerSupport/DesignWindowLoader.cs +++ b/src/Avalonia.DesignerSupport/DesignWindowLoader.cs @@ -29,7 +29,7 @@ namespace Avalonia.DesignerSupport if (assemblyPath != null) { if (xamlFileProjectPath == null) - xamlFileProjectPath = "/Designer/Fake.xaml"; + xamlFileProjectPath = "/Fake.xaml"; //Fabricate fake Uri baseUri = new Uri($"avares://{Path.GetFileNameWithoutExtension(assemblyPath)}{xamlFileProjectPath}"); @@ -43,7 +43,7 @@ namespace Avalonia.DesignerSupport { LocalAssembly = localAsm, DesignMode = true, - UseCompiledBindingsByDefault = bool.TryParse(useCompiledBindings, out var parsedValue ) && parsedValue + UseCompiledBindingsByDefault = bool.TryParse(useCompiledBindings, out var parsedValue) && parsedValue }); var style = loaded as IStyle; var resources = loaded as ResourceDictionary; @@ -90,14 +90,14 @@ namespace Avalonia.DesignerSupport }; } else if (loaded is Application) - control = new TextBlock {Text = "Application can't be previewed in design view"}; + control = new TextBlock { Text = "Application can't be previewed in design view" }; else - control = (Control) loaded; + control = (Control)loaded; window = control as Window; if (window == null) { - window = new Window() {Content = (Control)control}; + window = new Window() { Content = (Control)control }; } if (window.PlatformImpl is OffscreenTopLevelImplBase offscreenImpl) diff --git a/src/Avalonia.Fonts.Inter/Assets/Inter-Bold.ttf b/src/Avalonia.Fonts.Inter/Assets/Inter-Bold.ttf index a372c5fcca..8e82c70d10 100644 Binary files a/src/Avalonia.Fonts.Inter/Assets/Inter-Bold.ttf and b/src/Avalonia.Fonts.Inter/Assets/Inter-Bold.ttf differ diff --git a/src/Avalonia.Fonts.Inter/Assets/Inter-ExtraLight.ttf b/src/Avalonia.Fonts.Inter/Assets/Inter-ExtraLight.ttf index 13ef261fff..64aee30a4e 100644 Binary files a/src/Avalonia.Fonts.Inter/Assets/Inter-ExtraLight.ttf and b/src/Avalonia.Fonts.Inter/Assets/Inter-ExtraLight.ttf differ diff --git a/src/Avalonia.Fonts.Inter/Assets/Inter-Light.ttf b/src/Avalonia.Fonts.Inter/Assets/Inter-Light.ttf index 9f75ff2780..9e265d8905 100644 Binary files a/src/Avalonia.Fonts.Inter/Assets/Inter-Light.ttf and b/src/Avalonia.Fonts.Inter/Assets/Inter-Light.ttf differ diff --git a/src/Avalonia.Fonts.Inter/Assets/Inter-Medium.ttf b/src/Avalonia.Fonts.Inter/Assets/Inter-Medium.ttf index 2c5e25453c..b53fb1c4ac 100644 Binary files a/src/Avalonia.Fonts.Inter/Assets/Inter-Medium.ttf and b/src/Avalonia.Fonts.Inter/Assets/Inter-Medium.ttf differ diff --git a/src/Avalonia.Fonts.Inter/Assets/Inter-Regular.ttf b/src/Avalonia.Fonts.Inter/Assets/Inter-Regular.ttf index c28fe4d744..8d4eebf206 100644 Binary files a/src/Avalonia.Fonts.Inter/Assets/Inter-Regular.ttf and b/src/Avalonia.Fonts.Inter/Assets/Inter-Regular.ttf differ diff --git a/src/Avalonia.Fonts.Inter/Assets/Inter-SemiBold.ttf b/src/Avalonia.Fonts.Inter/Assets/Inter-SemiBold.ttf index d085eb4994..c6aeeb16a6 100644 Binary files a/src/Avalonia.Fonts.Inter/Assets/Inter-SemiBold.ttf and b/src/Avalonia.Fonts.Inter/Assets/Inter-SemiBold.ttf differ diff --git a/src/Avalonia.Fonts.Inter/Assets/Inter-Thin.ttf b/src/Avalonia.Fonts.Inter/Assets/Inter-Thin.ttf index 016a13482b..7aed55d560 100644 Binary files a/src/Avalonia.Fonts.Inter/Assets/Inter-Thin.ttf and b/src/Avalonia.Fonts.Inter/Assets/Inter-Thin.ttf differ diff --git a/src/Avalonia.Native/AvaloniaNativePlatform.cs b/src/Avalonia.Native/AvaloniaNativePlatform.cs index b7c58cded9..f24c6fa96f 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatform.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatform.cs @@ -108,7 +108,7 @@ namespace Avalonia.Native .Bind().ToConstant(new NativePlatformSettings(_factory.CreatePlatformSettings())) .Bind().ToConstant(this) .Bind().ToConstant(new ClipboardImpl(_factory.CreateClipboard())) - .Bind().ToConstant(new DefaultRenderTimer(60)) + .Bind().ToConstant(new AvaloniaNativeRenderTimer(_factory.CreatePlatformRenderTimer())) .Bind().ToConstant(new MacOSMountedVolumeInfoProvider()) .Bind().ToConstant(new AvaloniaNativeDragSource(_factory)) .Bind().ToConstant(applicationPlatform) diff --git a/src/Avalonia.Native/AvaloniaNativeRenderTimer.cs b/src/Avalonia.Native/AvaloniaNativeRenderTimer.cs new file mode 100644 index 0000000000..9021fdf345 --- /dev/null +++ b/src/Avalonia.Native/AvaloniaNativeRenderTimer.cs @@ -0,0 +1,58 @@ +using System; +using System.Diagnostics; +using Avalonia.Native.Interop; +using Avalonia.Rendering; +#nullable enable + +namespace Avalonia.Native; + +internal sealed class AvaloniaNativeRenderTimer : NativeCallbackBase, IRenderTimer, IAvnActionCallback +{ + private readonly IAvnPlatformRenderTimer _platformRenderTimer; + private readonly Stopwatch _stopwatch; + private Action? _tick; + private int _subscriberCount; + private bool registered; + + public AvaloniaNativeRenderTimer(IAvnPlatformRenderTimer platformRenderTimer) + { + _platformRenderTimer = platformRenderTimer; + _stopwatch = Stopwatch.StartNew(); + } + + public event Action Tick + { + add + { + _tick += value; + + if (!registered) + { + registered = true; + _platformRenderTimer.RegisterTick(this); + } + + if (_subscriberCount++ == 0) + { + _platformRenderTimer.Start(); + } + } + + remove + { + if (--_subscriberCount == 0) + { + _platformRenderTimer.Stop(); + } + + _tick -= value; + } + } + + public bool RunsInBackground => _platformRenderTimer.RunsInBackground().FromComBool(); + + public void Run() + { + _tick?.Invoke(_stopwatch.Elapsed); + } +} diff --git a/src/Avalonia.Native/avn.idl b/src/Avalonia.Native/avn.idl index 0911e5ffff..7eac1d33a8 100644 --- a/src/Avalonia.Native/avn.idl +++ b/src/Avalonia.Native/avn.idl @@ -507,6 +507,7 @@ interface IAvaloniaNativeFactory : IUnknown HRESULT CreateApplicationCommands(IAvnApplicationCommands** ppv); HRESULT CreatePlatformSettings(IAvnPlatformSettings** ppv); HRESULT CreatePlatformBehaviorInhibition(IAvnPlatformBehaviorInhibition** ppv); + HRESULT CreatePlatformRenderTimer(IAvnPlatformRenderTimer** ppv); } [uuid(233e094f-9b9f-44a3-9a6e-6948bbdd9fb1)] @@ -999,3 +1000,12 @@ interface IAvnPlatformBehaviorInhibition : IUnknown { void SetInhibitAppSleep(bool inhibitAppSleep, char* reason); } + +[uuid(22edf20d-5803-2d3f-9247-b4842e5e9322)] +interface IAvnPlatformRenderTimer : IUnknown +{ + HRESULT RegisterTick(IAvnActionCallback* callback); + void Start(); + void Stop(); + bool RunsInBackground(); +} diff --git a/src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs b/src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs index cefc2a4c06..c427ecf38b 100644 --- a/src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs +++ b/src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs @@ -108,8 +108,6 @@ namespace Avalonia.OpenGL.Controls _visual.Size = new Vector(Bounds.Width, Bounds.Height); _visual.Surface = _resources.Surface; ElementComposition.SetElementChildVisual(this, _visual); - using (_resources.Context.MakeCurrent()) - OnOpenGlInit(_resources.Context.GlInterface); return true; } diff --git a/src/Avalonia.Themes.Fluent/Controls/DropDownButton.xaml b/src/Avalonia.Themes.Fluent/Controls/DropDownButton.xaml index 438ad6cf18..2b26a7ef89 100644 --- a/src/Avalonia.Themes.Fluent/Controls/DropDownButton.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/DropDownButton.xaml @@ -71,6 +71,9 @@ + +