From 5bdbd930d980999b249f3a7334b16fbd58b6e22e Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Wed, 16 Nov 2022 16:56:11 +0000 Subject: [PATCH 01/87] Add Refresh Container --- samples/ControlCatalog/MainView.xaml | 3 + .../Pages/RefreshContainerPage.axaml | 24 + .../Pages/RefreshContainerPage.axaml.cs | 36 ++ .../ViewModels/RefreshContainerViewModel.cs | 26 + src/Avalonia.Base/Input/Gestures.cs | 8 + .../Input/PullGestureEventArgs.cs | 43 ++ .../Input/PullGestureRecognizer.cs | 152 +++++ .../PullToRefresh/RefreshContainer.cs | 221 +++++++ .../PullToRefresh/RefreshInfoProvider.cs | 129 +++++ .../PullToRefresh/RefreshVisualizer.cs | 544 ++++++++++++++++++ ...ScrollViewerIRefreshInfoProviderAdapter.cs | 227 ++++++++ .../Accents/FluentControlResourcesDark.xaml | 6 +- .../Accents/FluentControlResourcesLight.xaml | 4 + .../Controls/FluentControls.xaml | 2 + .../Controls/RefreshContainer.xaml | 24 + .../Controls/RefreshVisualizer.xaml | 67 +++ .../Accents/BaseDark.xaml | 3 + .../Accents/BaseLight.xaml | 4 +- .../Controls/RefreshContainer.xaml | 24 + .../Controls/RefreshVisualizer.xaml | 72 +++ .../Controls/SimpleControls.xaml | 2 + 21 files changed, 1619 insertions(+), 2 deletions(-) create mode 100644 samples/ControlCatalog/Pages/RefreshContainerPage.axaml create mode 100644 samples/ControlCatalog/Pages/RefreshContainerPage.axaml.cs create mode 100644 samples/ControlCatalog/ViewModels/RefreshContainerViewModel.cs create mode 100644 src/Avalonia.Base/Input/PullGestureEventArgs.cs create mode 100644 src/Avalonia.Base/Input/PullGestureRecognizer.cs create mode 100644 src/Avalonia.Controls/PullToRefresh/RefreshContainer.cs create mode 100644 src/Avalonia.Controls/PullToRefresh/RefreshInfoProvider.cs create mode 100644 src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs create mode 100644 src/Avalonia.Controls/PullToRefresh/ScrollViewerIRefreshInfoProviderAdapter.cs create mode 100644 src/Avalonia.Themes.Fluent/Controls/RefreshContainer.xaml create mode 100644 src/Avalonia.Themes.Fluent/Controls/RefreshVisualizer.xaml create mode 100644 src/Avalonia.Themes.Simple/Controls/RefreshContainer.xaml create mode 100644 src/Avalonia.Themes.Simple/Controls/RefreshVisualizer.xaml diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index b5a09b5fbd..33ea8c0db0 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -132,6 +132,9 @@ + + + diff --git a/samples/ControlCatalog/Pages/RefreshContainerPage.axaml b/samples/ControlCatalog/Pages/RefreshContainerPage.axaml new file mode 100644 index 0000000000..0123251bd0 --- /dev/null +++ b/samples/ControlCatalog/Pages/RefreshContainerPage.axaml @@ -0,0 +1,24 @@ + + + + + + + + diff --git a/samples/ControlCatalog/Pages/RefreshContainerPage.axaml.cs b/samples/ControlCatalog/Pages/RefreshContainerPage.axaml.cs new file mode 100644 index 0000000000..4c52179e00 --- /dev/null +++ b/samples/ControlCatalog/Pages/RefreshContainerPage.axaml.cs @@ -0,0 +1,36 @@ +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using ControlCatalog.ViewModels; + +namespace ControlCatalog.Pages +{ + public class RefreshContainerPage : UserControl + { + private RefreshContainerViewModel _viewModel; + + public RefreshContainerPage() + { + this.InitializeComponent(); + + _viewModel = new RefreshContainerViewModel(); + + DataContext = _viewModel; + } + + private async void RefreshContainerPage_RefreshRequested(object? sender, RefreshRequestedEventArgs e) + { + var deferral = e.GetRefreshCompletionDeferral(); + + await _viewModel.AddToTop(); + + deferral.Complete(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/samples/ControlCatalog/ViewModels/RefreshContainerViewModel.cs b/samples/ControlCatalog/ViewModels/RefreshContainerViewModel.cs new file mode 100644 index 0000000000..a78bc9e44a --- /dev/null +++ b/samples/ControlCatalog/ViewModels/RefreshContainerViewModel.cs @@ -0,0 +1,26 @@ +using System.Collections.ObjectModel; +using System.Linq; +using System.Reactive; +using System.Threading.Tasks; +using Avalonia.Controls.Notifications; +using ControlCatalog.Pages; +using MiniMvvm; + +namespace ControlCatalog.ViewModels +{ + public class RefreshContainerViewModel : ViewModelBase + { + public ObservableCollection Items { get; } + + public RefreshContainerViewModel() + { + Items = new ObservableCollection(Enumerable.Range(1, 200).Select(i => $"Item {i}")); + } + + public async Task AddToTop() + { + await Task.Delay(1000); + Items.Insert(0, $"Item {200 - Items.Count}"); + } + } +} diff --git a/src/Avalonia.Base/Input/Gestures.cs b/src/Avalonia.Base/Input/Gestures.cs index 82ed96d982..acd78515cd 100644 --- a/src/Avalonia.Base/Input/Gestures.cs +++ b/src/Avalonia.Base/Input/Gestures.cs @@ -46,6 +46,14 @@ namespace Avalonia.Input private static readonly WeakReference s_lastPress = new WeakReference(null); private static Point s_lastPressPoint; + public static readonly RoutedEvent PullGestureEvent = + RoutedEvent.Register( + "PullGesture", RoutingStrategies.Bubble, typeof(Gestures)); + + public static readonly RoutedEvent PullGestureEndedEvent = + RoutedEvent.Register( + "PullGestureEnded", RoutingStrategies.Bubble, typeof(Gestures)); + static Gestures() { InputElement.PointerPressedEvent.RouteFinished.Subscribe(PointerPressed); diff --git a/src/Avalonia.Base/Input/PullGestureEventArgs.cs b/src/Avalonia.Base/Input/PullGestureEventArgs.cs new file mode 100644 index 0000000000..57ad24f7a3 --- /dev/null +++ b/src/Avalonia.Base/Input/PullGestureEventArgs.cs @@ -0,0 +1,43 @@ +using System; +using Avalonia.Interactivity; + +namespace Avalonia.Input +{ + public class PullGestureEventArgs : RoutedEventArgs + { + public int Id { get; } + public Vector Delta { get; } + public PullDirection PullDirection { get; } + + private static int _nextId = 1; + + public static int GetNextFreeId() => _nextId++; + + public PullGestureEventArgs(int id, Vector delta, PullDirection pullDirection) : base(Gestures.PullGestureEvent) + { + Id = id; + Delta = delta; + PullDirection = pullDirection; + } + } + + public class PullGestureEndedEventArgs : RoutedEventArgs + { + public int Id { get; } + public PullDirection PullDirection { get; } + + public PullGestureEndedEventArgs(int id, PullDirection pullDirection) : base(Gestures.PullGestureEndedEvent) + { + Id = id; + PullDirection = pullDirection; + } + } + + public enum PullDirection + { + TopToBottom, + BottomToTop, + LeftToRight, + RightToLeft + } +} diff --git a/src/Avalonia.Base/Input/PullGestureRecognizer.cs b/src/Avalonia.Base/Input/PullGestureRecognizer.cs new file mode 100644 index 0000000000..bbbded44fa --- /dev/null +++ b/src/Avalonia.Base/Input/PullGestureRecognizer.cs @@ -0,0 +1,152 @@ +using Avalonia.Input.GestureRecognizers; + +namespace Avalonia.Input +{ + public class PullGestureRecognizer : StyledElement, IGestureRecognizer + { + private IInputElement? _target; + private IGestureRecognizerActionsDispatcher? _actions; + private Point _initialPosition; + private int _gestureId; + private IPointer? _tracking; + private PullDirection _pullDirection; + + /// + /// Defines the property. + /// + public static readonly DirectProperty PullDirectionProperty = + AvaloniaProperty.RegisterDirect( + nameof(PullDirection), + o => o.PullDirection, + (o, v) => o.PullDirection = v); + + public PullDirection PullDirection + { + get => _pullDirection; + set => SetAndRaise(PullDirectionProperty, ref _pullDirection, value); + } + + public PullGestureRecognizer(PullDirection pullDirection) + { + PullDirection = pullDirection; + } + + public void Initialize(IInputElement target, IGestureRecognizerActionsDispatcher actions) + { + _target = target; + _actions = actions; + + _target?.AddHandler(InputElement.PointerPressedEvent, OnPointerPressed, Interactivity.RoutingStrategies.Tunnel | Interactivity.RoutingStrategies.Bubble); + _target?.AddHandler(InputElement.PointerReleasedEvent, OnPointerReleased, Interactivity.RoutingStrategies.Tunnel | Interactivity.RoutingStrategies.Bubble); + } + + private void OnPointerPressed(object? sender, PointerPressedEventArgs e) + { + PointerPressed(e); + } + + private void OnPointerReleased(object? sender, PointerReleasedEventArgs e) + { + PointerReleased(e); + } + + public void PointerCaptureLost(IPointer pointer) + { + if (_tracking == pointer) + { + EndPull(); + } + } + + public void PointerMoved(PointerEventArgs e) + { + if (_tracking == e.Pointer) + { + var currentPosition = e.GetPosition(_target); + _actions!.Capture(e.Pointer, this); + + Vector delta = default; + switch (PullDirection) + { + case PullDirection.TopToBottom: + if (currentPosition.Y > _initialPosition.Y) + { + delta = new Vector(0, currentPosition.Y - _initialPosition.Y); + } + break; + case PullDirection.BottomToTop: + if (currentPosition.Y < _initialPosition.Y) + { + delta = new Vector(0, _initialPosition.Y - currentPosition.Y); + } + break; + case PullDirection.LeftToRight: + if (currentPosition.X > _initialPosition.X) + { + delta = new Vector(currentPosition.X - _initialPosition.X, 0); + } + break; + case PullDirection.RightToLeft: + if (currentPosition.X < _initialPosition.X) + { + delta = new Vector(_initialPosition.X - currentPosition.X, 0); + } + break; + } + + _target?.RaiseEvent(new PullGestureEventArgs(_gestureId, delta, PullDirection)); + } + } + + public void PointerPressed(PointerPressedEventArgs e) + { + if (_target != null && (e.Pointer.Type == PointerType.Touch || e.Pointer.Type == PointerType.Pen)) + { + var position = e.GetPosition(_target); + + var canPull = false; + + var bounds = _target.Bounds; + + switch (PullDirection) + { + case PullDirection.TopToBottom: + canPull = position.Y < bounds.Height * 0.1; + break; + case PullDirection.BottomToTop: + canPull = position.Y > bounds.Height - (bounds.Height * 0.1); + break; + case PullDirection.LeftToRight: + canPull = position.X < bounds.Width * 0.1; + break; + case PullDirection.RightToLeft: + canPull = position.X > bounds.Width - (bounds.Width * 0.1); + break; + } + + if (canPull) + { + _gestureId = PullGestureEventArgs.GetNextFreeId(); + _tracking = e.Pointer; + _initialPosition = position; + } + } + } + + public void PointerReleased(PointerReleasedEventArgs e) + { + if (_tracking == e.Pointer) + { + EndPull(); + } + } + + private void EndPull() + { + _tracking = null; + _initialPosition = default; + + _target?.RaiseEvent(new PullGestureEndedEventArgs(_gestureId, PullDirection)); + } + } +} diff --git a/src/Avalonia.Controls/PullToRefresh/RefreshContainer.cs b/src/Avalonia.Controls/PullToRefresh/RefreshContainer.cs new file mode 100644 index 0000000000..b882cf5a0f --- /dev/null +++ b/src/Avalonia.Controls/PullToRefresh/RefreshContainer.cs @@ -0,0 +1,221 @@ +using System; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.PullToRefresh; +using Avalonia.Input; +using Avalonia.Interactivity; + +namespace Avalonia.Controls +{ + public class RefreshContainer : ContentControl + { + internal const int DefaultPullDimensionSize = 100; + + private readonly bool _hasDefaultRefreshInfoProviderAdapter; + + private ScrollViewerIRefreshInfoProviderAdapter _refreshInfoProviderAdapter; + private RefreshInfoProvider _refreshInfoProvider; + private IDisposable _visualizerSizeSubscription; + private Grid? _visualizerPresenter; + private RefreshVisualizer _refreshVisualizer; + + public static readonly RoutedEvent RefreshRequestedEvent = + RoutedEvent.Register(nameof(RefreshRequested), RoutingStrategies.Bubble); + + internal static readonly DirectProperty RefreshInfoProviderAdapterProperty = + AvaloniaProperty.RegisterDirect(nameof(RefreshInfoProviderAdapter), + (s) => s.RefreshInfoProviderAdapter, (s, o) => s.RefreshInfoProviderAdapter = o); + + public static readonly DirectProperty RefreshVisualizerProperty = + AvaloniaProperty.RegisterDirect(nameof(RefreshVisualizer), + s => s.RefreshVisualizer, (s, o) => s.RefreshVisualizer = o); + + public static readonly StyledProperty PullDirectionProperty = + AvaloniaProperty.Register(nameof(PullDirection), PullDirection.TopToBottom); + + public ScrollViewerIRefreshInfoProviderAdapter RefreshInfoProviderAdapter + { + get => _refreshInfoProviderAdapter; set + { + SetAndRaise(RefreshInfoProviderAdapterProperty, ref _refreshInfoProviderAdapter, value); + } + } + + private bool _hasDefaultRefreshVisualizer; + + public RefreshVisualizer RefreshVisualizer + { + get => _refreshVisualizer; set + { + if (_refreshVisualizer != null) + { + _visualizerSizeSubscription?.Dispose(); + _refreshVisualizer.RefreshRequested -= Visualizer_RefreshRequested; + } + + SetAndRaise(RefreshVisualizerProperty, ref _refreshVisualizer, value); + } + } + + public PullDirection PullDirection + { + get => GetValue(PullDirectionProperty); + set => SetValue(PullDirectionProperty, value); + } + + public event EventHandler? RefreshRequested + { + add => AddHandler(RefreshRequestedEvent, value); + remove => RemoveHandler(RefreshRequestedEvent, value); + } + + public RefreshContainer() + { + _hasDefaultRefreshInfoProviderAdapter = true; + RefreshInfoProviderAdapter = new ScrollViewerIRefreshInfoProviderAdapter(PullDirection); + } + + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + + _visualizerPresenter = e.NameScope.Find("PART_RefreshVisualizerPresenter"); + + if (_refreshVisualizer == null) + { + _hasDefaultRefreshVisualizer = true; + RefreshVisualizer = new RefreshVisualizer(); + } + else + { + _hasDefaultRefreshVisualizer = false; + RaisePropertyChanged(RefreshVisualizerProperty, null, _refreshVisualizer); + } + + OnPullDirectionChanged(); + } + + private void OnVisualizerSizeChanged(Rect obj) + { + if (_hasDefaultRefreshInfoProviderAdapter) + { + RefreshInfoProviderAdapter = new ScrollViewerIRefreshInfoProviderAdapter(PullDirection); + } + } + + private void Visualizer_RefreshRequested(object? sender, RefreshRequestedEventArgs e) + { + var ev = new RefreshRequestedEventArgs(e.GetRefreshCompletionDeferral(), RefreshRequestedEvent); + RaiseEvent(ev); + ev.DecrementCount(); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == RefreshInfoProviderAdapterProperty) + { + if (_refreshInfoProvider != null) + { + _refreshVisualizer.RefreshInfoProvider = _refreshInfoProvider; + } + else + { + if (RefreshInfoProviderAdapter != null && _refreshVisualizer != null) + { + _refreshInfoProvider = RefreshInfoProviderAdapter.AdaptFromTree(this, _refreshVisualizer.Bounds.Size); + + if (_refreshInfoProvider != null) + { + _refreshVisualizer.RefreshInfoProvider = _refreshInfoProvider; + } + } + } + } + else if (change.Property == RefreshVisualizerProperty) + { + if (_visualizerPresenter != null) + { + _visualizerPresenter.Children.Clear(); + if (_refreshVisualizer != null) + { + _visualizerPresenter.Children.Add(_refreshVisualizer); + } + } + + if (_refreshVisualizer != null) + { + _refreshVisualizer.RefreshRequested += Visualizer_RefreshRequested; + _visualizerSizeSubscription = _refreshVisualizer.GetObservable(Control.BoundsProperty).Subscribe(OnVisualizerSizeChanged); + } + } + else if (change.Property == PullDirectionProperty) + { + OnPullDirectionChanged(); + } + } + + private void OnPullDirectionChanged() + { + if (_visualizerPresenter != null) + { + switch (PullDirection) + { + case PullDirection.TopToBottom: + _visualizerPresenter.VerticalAlignment = Layout.VerticalAlignment.Top; + _visualizerPresenter.HorizontalAlignment = Layout.HorizontalAlignment.Stretch; + if (_hasDefaultRefreshVisualizer) + { + _refreshVisualizer.PullDirection = PullDirection.TopToBottom; + _refreshVisualizer.Height = DefaultPullDimensionSize; + _refreshVisualizer.Width = double.NaN; + } + break; + case PullDirection.BottomToTop: + _visualizerPresenter.VerticalAlignment = Layout.VerticalAlignment.Bottom; + _visualizerPresenter.HorizontalAlignment = Layout.HorizontalAlignment.Stretch; + if (_hasDefaultRefreshVisualizer) + { + _refreshVisualizer.PullDirection = PullDirection.BottomToTop; + _refreshVisualizer.Height = DefaultPullDimensionSize; + _refreshVisualizer.Width = double.NaN; + } + break; + case PullDirection.LeftToRight: + _visualizerPresenter.VerticalAlignment = Layout.VerticalAlignment.Stretch; + _visualizerPresenter.HorizontalAlignment = Layout.HorizontalAlignment.Left; + if (_hasDefaultRefreshVisualizer) + { + _refreshVisualizer.PullDirection = PullDirection.LeftToRight; + _refreshVisualizer.Width = DefaultPullDimensionSize; + _refreshVisualizer.Height = double.NaN; + } + break; + case PullDirection.RightToLeft: + _visualizerPresenter.VerticalAlignment = Layout.VerticalAlignment.Stretch; + _visualizerPresenter.HorizontalAlignment = Layout.HorizontalAlignment.Right; + if (_hasDefaultRefreshVisualizer) + { + _refreshVisualizer.PullDirection = PullDirection.RightToLeft; + _refreshVisualizer.Width = DefaultPullDimensionSize; + _refreshVisualizer.Height = double.NaN; + } + break; + } + + if (_hasDefaultRefreshInfoProviderAdapter && + _hasDefaultRefreshVisualizer && + _refreshVisualizer.Bounds.Height == DefaultPullDimensionSize && + _refreshVisualizer.Bounds.Width == DefaultPullDimensionSize) + { + RefreshInfoProviderAdapter = new ScrollViewerIRefreshInfoProviderAdapter(PullDirection); + } + } + } + + public void RequestRefresh() + { + _refreshVisualizer?.RequestRefresh(); + } + } +} diff --git a/src/Avalonia.Controls/PullToRefresh/RefreshInfoProvider.cs b/src/Avalonia.Controls/PullToRefresh/RefreshInfoProvider.cs new file mode 100644 index 0000000000..3fc32cdd08 --- /dev/null +++ b/src/Avalonia.Controls/PullToRefresh/RefreshInfoProvider.cs @@ -0,0 +1,129 @@ +using System; +using Avalonia.Input; +using Avalonia.Interactivity; + +namespace Avalonia.Controls.PullToRefresh +{ + public class RefreshInfoProvider : Interactive + { + internal const double ExecutionRatio = 0.8; + + private readonly PullDirection _refreshPullDirection; + private readonly Size _refreshVisualizerSize; + + private readonly Visual _visual; + private bool _isInteractingForRefresh; + private double _interactionRatio; + private bool _entered; + + public DirectProperty IsInteractingForRefreshProperty = + AvaloniaProperty.RegisterDirect(nameof(IsInteractingForRefresh), + s => s.IsInteractingForRefresh, (s, o) => s.IsInteractingForRefresh = o); + + public DirectProperty InteractionRatioProperty = + AvaloniaProperty.RegisterDirect(nameof(InteractionRatio), + s => s.InteractionRatio, (s, o) => s.InteractionRatio = o); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent RefreshStartedEvent = + RoutedEvent.Register(nameof(RefreshStarted), RoutingStrategies.Bubble); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent RefreshCompletedEvent = + RoutedEvent.Register(nameof(RefreshCompleted), RoutingStrategies.Bubble); + + public bool PeekingMode { get; internal set; } + + public bool IsInteractingForRefresh + { + get => _isInteractingForRefresh; internal set + { + var isInteractingForRefresh = value && !PeekingMode; + + if (isInteractingForRefresh != _isInteractingForRefresh) + { + SetAndRaise(IsInteractingForRefreshProperty, ref _isInteractingForRefresh, isInteractingForRefresh); + } + } + } + + public double InteractionRatio + { + get => _interactionRatio; + set + { + SetAndRaise(InteractionRatioProperty, ref _interactionRatio, value); + } + } + + internal Visual Visual => _visual; + + public event EventHandler RefreshStarted + { + add => AddHandler(RefreshStartedEvent, value); + remove => RemoveHandler(RefreshStartedEvent, value); + } + + public event EventHandler RefreshCompleted + { + add => AddHandler(RefreshCompletedEvent, value); + remove => RemoveHandler(RefreshCompletedEvent, value); + } + + internal void InteractingStateEntered(object sender, PullGestureEventArgs e) + { + if (!_entered) + { + IsInteractingForRefresh = true; + _entered = true; + } + + ValuesChanged(e.Delta); + } + + internal void InteractingStateExited(object sender, PullGestureEndedEventArgs e) + { + IsInteractingForRefresh = false; + _entered = false; + + ValuesChanged(default); + } + + + public RefreshInfoProvider(PullDirection refreshPullDirection, Size refreshVIsualizerSize, Visual visual) + { + _refreshPullDirection = refreshPullDirection; + _refreshVisualizerSize = refreshVIsualizerSize; + _visual = visual; + } + + public void OnRefreshStarted() + { + RaiseEvent(new RoutedEventArgs(RefreshStartedEvent)); + } + + public void OnRefreshCompleted() + { + RaiseEvent(new RoutedEventArgs(RefreshCompletedEvent)); + } + + internal void ValuesChanged(Vector value) + { + switch (_refreshPullDirection) + { + case PullDirection.TopToBottom: + case PullDirection.BottomToTop: + InteractionRatio = _refreshVisualizerSize.Height == 0 ? 1 : Math.Min(1, value.Y / _refreshVisualizerSize.Height); + break; + case PullDirection.LeftToRight: + case PullDirection.RightToLeft: + InteractionRatio = _refreshVisualizerSize.Height == 0 ? 1 : Math.Min(1, value.X / _refreshVisualizerSize.Width); + break; + } + } + } +} diff --git a/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs b/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs new file mode 100644 index 0000000000..81c613443d --- /dev/null +++ b/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs @@ -0,0 +1,544 @@ +using System; +using System.Reactive.Linq; +using System.Threading; +using Avalonia.Animation; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.PullToRefresh; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Media; + +namespace Avalonia.Controls +{ + public class RefreshVisualizer : ContentControl + { + private const int DefaultIndicatorSize = 24; + private const double MinimumIndicatorOpacity = 0.4; + private const string ArrowPathData = "M18.6195264,3.31842271 C19.0080059,3.31842271 19.3290603,3.60710385 19.3798716,3.9816481 L19.3868766,4.08577298 L19.3868766,6.97963208 C19.3868766,7.36811161 19.0981955,7.68916605 18.7236513,7.73997735 L18.6195264,7.74698235 L15.7256673,7.74698235 C15.3018714,7.74698235 14.958317,7.40342793 14.958317,6.97963208 C14.958317,6.59115255 15.2469981,6.27009811 15.6215424,6.21928681 L15.7256673,6.21228181 L16.7044011,6.21182461 C13.7917384,3.87107476 9.52212532,4.05209336 6.81933829,6.75488039 C3.92253872,9.65167996 3.92253872,14.34832 6.81933829,17.2451196 C9.71613786,20.1419192 14.4127779,20.1419192 17.3095775,17.2451196 C19.0725398,15.4821573 19.8106555,12.9925923 19.3476248,10.58925 C19.2674502,10.173107 19.5398064,9.77076216 19.9559494,9.69058758 C20.3720923,9.610413 20.7744372,9.88276918 20.8546118,10.2989121 C21.4129973,13.1971899 20.5217103,16.2033812 18.3947747,18.3303168 C14.8986373,21.8264542 9.23027854,21.8264542 5.73414113,18.3303168 C2.23800371,14.8341794 2.23800371,9.16582064 5.73414113,5.66968323 C9.05475132,2.34907304 14.3349409,2.18235834 17.8523166,5.16953912 L17.8521761,4.08577298 C17.8521761,3.66197713 18.1957305,3.31842271 18.6195264,3.31842271 Z"; + private double _executingRatio = 0.8; + + private RotateTransform _visualizerRotateTransform; + private TranslateTransform _contentTranslateTransform; + private RefreshVisualizerState _refreshVisualizerState; + private RefreshInfoProvider _refreshInfoProvider; + private IDisposable _isInteractingSubscription; + private IDisposable _interactionRatioSubscription; + private bool _isInteractingForRefresh; + private Grid? _root; + private Control _content; + private RefreshVisualizerOrientation _orientation; + private float _startingRotationAngle; + private double _interactionRatio; + + private bool IsPullDirectionVertical => PullDirection == PullDirection.TopToBottom || PullDirection == PullDirection.BottomToTop; + private bool IsPullDirectionFar => PullDirection == PullDirection.BottomToTop || PullDirection == PullDirection.RightToLeft; + + public static readonly StyledProperty PullDirectionProperty = + AvaloniaProperty.Register(nameof(PullDirection), PullDirection.TopToBottom); + public static readonly RoutedEvent RefreshRequestedEvent = + RoutedEvent.Register(nameof(RefreshRequested), RoutingStrategies.Bubble); + + public static readonly DirectProperty RefreshVisualizerStateProperty = + AvaloniaProperty.RegisterDirect(nameof(RefreshVisualizerState), + s => s.RefreshVisualizerState); + + public static readonly DirectProperty OrientationProperty = + AvaloniaProperty.RegisterDirect(nameof(Orientation), + s => s.Orientation, (s, o) => s.Orientation = o); + + public DirectProperty RefreshInfoProviderProperty = + AvaloniaProperty.RegisterDirect(nameof(RefreshInfoProvider), + s => s.RefreshInfoProvider, (s, o) => s.RefreshInfoProvider = o); + + public RefreshVisualizerState RefreshVisualizerState + { + get + { + return _refreshVisualizerState; + } + private set + { + SetAndRaise(RefreshVisualizerStateProperty, ref _refreshVisualizerState, value); + UpdateContent(); + } + } + + public RefreshVisualizerOrientation Orientation + { + get + { + return _orientation; + } + set + { + SetAndRaise(OrientationProperty, ref _orientation, value); + } + } + + internal PullDirection PullDirection + { + get => GetValue(PullDirectionProperty); + set + { + SetValue(PullDirectionProperty, value); + + OnOrientationChanged(); + + UpdateContent(); + } + } + + public RefreshInfoProvider RefreshInfoProvider + { + get => _refreshInfoProvider; internal set + { + if (_refreshInfoProvider != null) + { + _refreshInfoProvider.RenderTransform = null; + } + SetAndRaise(RefreshInfoProviderProperty, ref _refreshInfoProvider, value); + } + } + + public event EventHandler? RefreshRequested + { + add => AddHandler(RefreshRequestedEvent, value); + remove => RemoveHandler(RefreshRequestedEvent, value); + } + + public RefreshVisualizer() + { + _visualizerRotateTransform = new RotateTransform(); + _contentTranslateTransform = new TranslateTransform(); + } + + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + + _root = e.NameScope.Find("PART_Root"); + + if (_root != null) + { + if (_content == null) + { + Content = new PathIcon() + { + Data = PathGeometry.Parse(ArrowPathData), + Height = DefaultIndicatorSize, + Width = DefaultIndicatorSize + }; + } + else + { + RaisePropertyChanged(ContentProperty, null, Content); + } + } + + OnOrientationChanged(); + + UpdateContent(); + } + + private void UpdateContent() + { + if (_content != null) + { + switch (RefreshVisualizerState) + { + case RefreshVisualizerState.Idle: + _content.Classes.Remove("refreshing"); + _root.Classes.Remove("pending"); + _content.RenderTransform = _visualizerRotateTransform; + _content.Opacity = MinimumIndicatorOpacity; + _visualizerRotateTransform.Angle = _startingRotationAngle; + _contentTranslateTransform.X = 0; + _contentTranslateTransform.Y = 0; + break; + case RefreshVisualizerState.Interacting: + _content.Classes.Remove("refreshing"); + _root.Classes.Remove("pending"); + _content.RenderTransform = _visualizerRotateTransform; + _content.Opacity = MinimumIndicatorOpacity; + _visualizerRotateTransform.Angle = _startingRotationAngle + (_interactionRatio * 360); + _content.Height = DefaultIndicatorSize; + _content.Width = DefaultIndicatorSize; + if (IsPullDirectionVertical) + { + _contentTranslateTransform.X = 0; + _contentTranslateTransform.Y = _interactionRatio * (IsPullDirectionFar ? -1 : 1) * _root.Bounds.Height; + } + else + { + _contentTranslateTransform.Y = 0; + _contentTranslateTransform.X = _interactionRatio * (IsPullDirectionFar ? -1 : 1) * _root.Bounds.Width; + } + break; + case RefreshVisualizerState.Pending: + _content.Classes.Remove("refreshing"); + _content.Opacity = 1; + _content.RenderTransform = _visualizerRotateTransform; + if (IsPullDirectionVertical) + { + _contentTranslateTransform.X = 0; + _contentTranslateTransform.Y = _interactionRatio * (IsPullDirectionFar ? -1 : 1) * _root.Bounds.Height; + } + else + { + _contentTranslateTransform.Y = 0; + _contentTranslateTransform.X = _interactionRatio * (IsPullDirectionFar ? -1 : 1) * _root.Bounds.Width; + } + + _root.Classes.Add("pending"); + break; + case RefreshVisualizerState.Refreshing: + _root.Classes.Remove("pending"); + _content.Classes.Add("refreshing"); + _content.Opacity = 1; + _content.Height = DefaultIndicatorSize; + _content.Width = DefaultIndicatorSize; + break; + case RefreshVisualizerState.Peeking: + _root.Classes.Remove("pending"); + _content.Opacity = 1; + _visualizerRotateTransform.Angle += _startingRotationAngle; + break; + } + } + } + + public void RequestRefresh() + { + RefreshVisualizerState = RefreshVisualizerState.Refreshing; + RefreshInfoProvider?.OnRefreshStarted(); + + RaiseRefreshRequested(); + } + + private void RefreshCompleted() + { + RefreshVisualizerState = RefreshVisualizerState.Idle; + + RefreshInfoProvider?.OnRefreshCompleted(); + } + + private void RaiseRefreshRequested() + { + var refreshArgs = new RefreshRequestedEventArgs(RefreshCompleted, RefreshRequestedEvent); + + refreshArgs.IncrementCount(); + + RaiseEvent(refreshArgs); + + refreshArgs.DecrementCount(); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == RefreshInfoProviderProperty) + { + OnRefreshInfoProviderChanged(); + } + else if (change.Property == ContentProperty) + { + if (_root != null) + { + if (_content == null) + { + _content = new PathIcon() + { + Data = PathGeometry.Parse(ArrowPathData), + Height = DefaultIndicatorSize, + Width = DefaultIndicatorSize + }; + + var transformGroup = new TransformGroup(); + transformGroup.Children.Add(_visualizerRotateTransform); + + _content.RenderTransform = _visualizerRotateTransform; + _root.RenderTransform = _contentTranslateTransform; + + var transition = new Transitions + { + new DoubleTransition() + { + Property = OpacityProperty, + Duration = TimeSpan.FromSeconds(0.5) + }, + }; + + _content.Transitions = transition; + } + + var scalingGrid = new Grid(); + scalingGrid.VerticalAlignment = Layout.VerticalAlignment.Center; + scalingGrid.HorizontalAlignment = Layout.HorizontalAlignment.Center; + scalingGrid.RenderTransform = new ScaleTransform(); + + scalingGrid.Children.Add(_content); + + _root.Children.Insert(0, scalingGrid); + _content.VerticalAlignment = Layout.VerticalAlignment.Center; + _content.HorizontalAlignment = Layout.HorizontalAlignment.Center; + } + + UpdateContent(); + } + else if (change.Property == OrientationProperty) + { + OnOrientationChanged(); + + UpdateContent(); + } + else if (change.Property == BoundsProperty) + { + if (_content != null) + { + var parent = _content.Parent as Control; + switch (PullDirection) + { + case PullDirection.TopToBottom: + parent.Margin = new Thickness(0, -Bounds.Height - DefaultIndicatorSize - (0.5 * DefaultIndicatorSize), 0, 0); + break; + case PullDirection.BottomToTop: + parent.Margin = new Thickness(0, 0, 0, -Bounds.Height - DefaultIndicatorSize - (0.5 * DefaultIndicatorSize)); + break; + case PullDirection.LeftToRight: + parent.Margin = new Thickness(-Bounds.Width - DefaultIndicatorSize - (0.5 * DefaultIndicatorSize), 0, 0, 0); + break; + case PullDirection.RightToLeft: + parent.Margin = new Thickness(0, 0, -Bounds.Width - DefaultIndicatorSize - (0.5 * DefaultIndicatorSize), 0); + break; + } + } + } + } + + private void OnOrientationChanged() + { + switch (_orientation) + { + case RefreshVisualizerOrientation.Auto: + switch (PullDirection) + { + case PullDirection.TopToBottom: + case PullDirection.BottomToTop: + _startingRotationAngle = 0.0f; + break; + case PullDirection.LeftToRight: + _startingRotationAngle = 270; + break; + case PullDirection.RightToLeft: + _startingRotationAngle = 90; + break; + } + break; + case RefreshVisualizerOrientation.Normal: + _startingRotationAngle = 0.0f; + break; + case RefreshVisualizerOrientation.Rotate90DegreesCounterclockwise: + _startingRotationAngle = 270; + break; + case RefreshVisualizerOrientation.Rotate270DegreesCounterclockwise: + _startingRotationAngle = 90; + break; + } + } + + private void OnRefreshInfoProviderChanged() + { + _isInteractingSubscription?.Dispose(); + _isInteractingSubscription = null; + _interactionRatioSubscription?.Dispose(); + _interactionRatioSubscription = null; + + if (_refreshInfoProvider != null) + { + _isInteractingSubscription = _refreshInfoProvider.GetObservable(RefreshInfoProvider.IsInteractingForRefreshProperty) + .Subscribe(InteractingForRefreshObserver); + + _interactionRatioSubscription = _refreshInfoProvider.GetObservable(RefreshInfoProvider.InteractionRatioProperty) + .Subscribe(InteractionRatioObserver); + + var visual = _refreshInfoProvider.Visual; + visual.RenderTransform = _contentTranslateTransform; + + _executingRatio = RefreshInfoProvider.ExecutionRatio; + } + else + { + _executingRatio = 1; + } + } + + private void InteractionRatioObserver(double obj) + { + var wasAtZero = _interactionRatio == 0.0; + _interactionRatio = obj; + + if (_isInteractingForRefresh) + { + if (RefreshVisualizerState == RefreshVisualizerState.Idle) + { + if (wasAtZero) + { + if (_interactionRatio > _executingRatio) + { + RefreshVisualizerState = RefreshVisualizerState.Pending; + } + else if (_interactionRatio > 0) + { + RefreshVisualizerState = RefreshVisualizerState.Interacting; + } + } + else if (_interactionRatio > 0) + { + RefreshVisualizerState = RefreshVisualizerState.Peeking; + } + } + else if (RefreshVisualizerState == RefreshVisualizerState.Interacting) + { + if (_interactionRatio <= 0) + { + RefreshVisualizerState = RefreshVisualizerState.Idle; + } + else if (_interactionRatio > _executingRatio) + { + RefreshVisualizerState = RefreshVisualizerState.Pending; + } + else + { + UpdateContent(); + } + } + else if (RefreshVisualizerState == RefreshVisualizerState.Pending) + { + if (_interactionRatio <= _executingRatio) + { + RefreshVisualizerState = RefreshVisualizerState.Interacting; + } + else if (_interactionRatio <= 0) + { + RefreshVisualizerState = RefreshVisualizerState.Idle; + } + else + { + UpdateContent(); + } + } + } + else + { + if (RefreshVisualizerState != RefreshVisualizerState.Refreshing) + { + if (_interactionRatio > 0) + { + RefreshVisualizerState = RefreshVisualizerState.Peeking; + } + else + { + RefreshVisualizerState = RefreshVisualizerState.Idle; + } + } + } + } + + private void InteractingForRefreshObserver(bool obj) + { + _isInteractingForRefresh = obj; + + if (!_isInteractingForRefresh) + { + switch (_refreshVisualizerState) + { + case RefreshVisualizerState.Pending: + RequestRefresh(); + break; + case RefreshVisualizerState.Refreshing: + // We don't want to interrupt a currently executing refresh. + break; + default: + RefreshVisualizerState = RefreshVisualizerState.Idle; + break; + } + } + } + } + + public enum RefreshVisualizerState + { + Idle, + Peeking, + Interacting, + Pending, + Refreshing + } + + public enum RefreshVisualizerOrientation + { + Auto, + Normal, + Rotate90DegreesCounterclockwise, + Rotate270DegreesCounterclockwise + } + + public class RefreshRequestedEventArgs : RoutedEventArgs + { + private RefreshCompletionDeferral _refreshCompletionDeferral; + + public RefreshCompletionDeferral GetRefreshCompletionDeferral() + { + return _refreshCompletionDeferral.Get(); + } + + public RefreshRequestedEventArgs(Action deferredAction, RoutedEvent? routedEvent) : base(routedEvent) + { + _refreshCompletionDeferral = new RefreshCompletionDeferral(deferredAction); + } + + public RefreshRequestedEventArgs(RefreshCompletionDeferral completionDeferral, RoutedEvent? routedEvent) : base(routedEvent) + { + _refreshCompletionDeferral = completionDeferral; + } + + internal void IncrementCount() + { + _refreshCompletionDeferral?.Get(); + } + + internal void DecrementCount() + { + _refreshCompletionDeferral?.Complete(); + } + } + + public class RefreshCompletionDeferral + { + private Action _deferredAction; + private int _deferCount; + + public RefreshCompletionDeferral(Action deferredAction) + { + _deferredAction = deferredAction; + } + + public void Complete() + { + Interlocked.Decrement(ref _deferCount); + + if (_deferCount == 0) + { + _deferredAction?.Invoke(); + } + } + + public RefreshCompletionDeferral Get() + { + Interlocked.Increment(ref _deferCount); + + return this; + } + } +} diff --git a/src/Avalonia.Controls/PullToRefresh/ScrollViewerIRefreshInfoProviderAdapter.cs b/src/Avalonia.Controls/PullToRefresh/ScrollViewerIRefreshInfoProviderAdapter.cs new file mode 100644 index 0000000000..0b0c8c99b2 --- /dev/null +++ b/src/Avalonia.Controls/PullToRefresh/ScrollViewerIRefreshInfoProviderAdapter.cs @@ -0,0 +1,227 @@ +using System; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.VisualTree; + +namespace Avalonia.Controls.PullToRefresh +{ + public class ScrollViewerIRefreshInfoProviderAdapter + { + private const int MaxSearchDepth = 10; + private const int InitialOffsetThreshold = 1; + + private PullDirection _refreshPullDirection; + private ScrollViewer _scrollViewer; + private RefreshInfoProvider _refreshInfoProvider; + private PullGestureRecognizer _pullGestureRecognizer; + private InputElement? _interactionSource; + private bool _isVisualizerInteractionSourceAttached; + + public ScrollViewerIRefreshInfoProviderAdapter(PullDirection pullDirection) + { + _refreshPullDirection = pullDirection; + } + + public RefreshInfoProvider AdaptFromTree(IVisual root, Size refreshVIsualizerSize) + { + if (root is ScrollViewer scrollViewer) + { + return Adapt(scrollViewer, refreshVIsualizerSize); + } + else + { + int depth = 0; + while (depth < MaxSearchDepth) + { + var scroll = AdaptFromTreeRecursiveHelper(root, depth); + + if (scroll != null) + { + return Adapt(scroll, refreshVIsualizerSize); + } + + depth++; + } + } + + ScrollViewer AdaptFromTreeRecursiveHelper(IVisual root, int depth) + { + if (depth == 0) + { + foreach (var child in root.VisualChildren) + { + if (child is ScrollViewer viewer) + { + return viewer; + } + } + } + else + { + foreach (var child in root.VisualChildren) + { + var viewer = AdaptFromTreeRecursiveHelper(child, depth - 1); + if (viewer != null) + { + return viewer; + } + } + } + + return null; + } + + return null; + } + + public RefreshInfoProvider Adapt(ScrollViewer adaptee, Size refreshVIsualizerSize) + { + if (adaptee == null) + { + throw new ArgumentNullException(nameof(adaptee), "Adaptee cannot be null"); + } + + if (_scrollViewer != null) + { + CleanUpScrollViewer(); + } + + if (_refreshInfoProvider != null && _interactionSource != null) + { + _interactionSource.RemoveHandler(Gestures.PullGestureEvent, _refreshInfoProvider.InteractingStateEntered); + _interactionSource.RemoveHandler(Gestures.PullGestureEndedEvent, _refreshInfoProvider.InteractingStateExited); + } + + _refreshInfoProvider = null; + _scrollViewer = adaptee; + + if (_scrollViewer.Content == null) + { + throw new ArgumentException(nameof(adaptee), "Adaptee's content property cannot be null."); + } + + var content = adaptee.Content as Visual; + + if (content == null) + { + throw new ArgumentException(nameof(adaptee), "Adaptee's content property must be a Visual"); + } + + if (content.GetVisualParent() == null) + { + _scrollViewer.Loaded += ScrollViewer_Loaded; + } + else + { + ScrollViewer_Loaded(null, null); + + if (content.Parent is not InputElement) + { + throw new ArgumentException(nameof(adaptee), "Adaptee's content's parent must be a InputElement"); + } + } + + _refreshInfoProvider = new RefreshInfoProvider(_refreshPullDirection, refreshVIsualizerSize, content); + + _pullGestureRecognizer = new PullGestureRecognizer(_refreshPullDirection); + + if (_interactionSource != null) + { + _interactionSource.GestureRecognizers.Add(_pullGestureRecognizer); + _interactionSource.AddHandler(Gestures.PullGestureEvent, _refreshInfoProvider.InteractingStateEntered); + _interactionSource.AddHandler(Gestures.PullGestureEndedEvent, _refreshInfoProvider.InteractingStateExited); + _isVisualizerInteractionSourceAttached = true; + } + + _scrollViewer.PointerPressed += ScrollViewer_PointerPressed; + _scrollViewer.PointerReleased += ScrollViewer_PointerReleased; + _scrollViewer.ScrollChanged += ScrollViewer_ScrollChanged; + + return _refreshInfoProvider; + } + + private void ScrollViewer_ScrollChanged(object sender, ScrollChangedEventArgs e) + { + if (_isVisualizerInteractionSourceAttached && _refreshInfoProvider != null && _refreshInfoProvider.IsInteractingForRefresh) + { + if (!IsWithinOffsetThreashold()) + { + _refreshInfoProvider.IsInteractingForRefresh = false; + } + } + } + + private void ScrollViewer_Loaded(object sender, RoutedEventArgs e) + { + var content = _scrollViewer.Content as Visual; + if (content == null) + { + throw new ArgumentException(nameof(_scrollViewer), "Adaptee's content property must be a Visual"); + } + + if (content.Parent is not InputElement) + { + throw new ArgumentException(nameof(_scrollViewer), "Adaptee's content parent must be an InputElement"); + } + + MakeInteractionSource(content.Parent as InputElement); + + _scrollViewer.Loaded -= ScrollViewer_Loaded; + } + + private void MakeInteractionSource(InputElement element) + { + _interactionSource = element; + + if (_pullGestureRecognizer != null) + { + element.GestureRecognizers.Add(_pullGestureRecognizer); + _interactionSource.AddHandler(Gestures.PullGestureEvent, _refreshInfoProvider.InteractingStateEntered); + _interactionSource.AddHandler(Gestures.PullGestureEndedEvent, _refreshInfoProvider.InteractingStateExited); + _isVisualizerInteractionSourceAttached = true; + } + } + + private void ScrollViewer_PointerReleased(object sender, PointerReleasedEventArgs e) + { + if (_refreshInfoProvider != null) + { + _refreshInfoProvider.IsInteractingForRefresh = false; + } + } + + private void ScrollViewer_PointerPressed(object sender, PointerPressedEventArgs e) + { + _refreshInfoProvider.PeekingMode = !IsWithinOffsetThreashold(); + } + + private bool IsWithinOffsetThreashold() + { + if (_scrollViewer != null) + { + var offset = _scrollViewer.Offset; + + switch (_refreshPullDirection) + { + case PullDirection.TopToBottom: + return offset.Y < InitialOffsetThreshold; + case PullDirection.LeftToRight: + return offset.X < InitialOffsetThreshold; + case PullDirection.RightToLeft: + return offset.X > _scrollViewer.Extent.Width - _scrollViewer.Viewport.Width - InitialOffsetThreshold; + case PullDirection.BottomToTop: + return offset.Y > _scrollViewer.Extent.Height - _scrollViewer.Viewport.Height - InitialOffsetThreshold; + } + } + + return false; + } + + private void CleanUpScrollViewer() + { + _scrollViewer.PointerPressed -= ScrollViewer_PointerPressed; + _scrollViewer.PointerReleased -= ScrollViewer_PointerReleased; + _scrollViewer.ScrollChanged -= ScrollViewer_ScrollChanged; + } + } +} diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml index 7e3c8673f5..d9be545801 100644 --- a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml @@ -637,6 +637,10 @@ - + + + + + diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml index 7917315e19..e80b137b2d 100644 --- a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml @@ -633,5 +633,9 @@ + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml index 5383aa3180..5d38b055b3 100644 --- a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml @@ -69,6 +69,8 @@ + + diff --git a/src/Avalonia.Themes.Fluent/Controls/RefreshContainer.xaml b/src/Avalonia.Themes.Fluent/Controls/RefreshContainer.xaml new file mode 100644 index 0000000000..97002c25bd --- /dev/null +++ b/src/Avalonia.Themes.Fluent/Controls/RefreshContainer.xaml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/RefreshVisualizer.xaml b/src/Avalonia.Themes.Fluent/Controls/RefreshVisualizer.xaml new file mode 100644 index 0000000000..388ca814f8 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/Controls/RefreshVisualizer.xaml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Simple/Accents/BaseDark.xaml b/src/Avalonia.Themes.Simple/Accents/BaseDark.xaml index 1843abebfd..c748775a12 100644 --- a/src/Avalonia.Themes.Simple/Accents/BaseDark.xaml +++ b/src/Avalonia.Themes.Simple/Accents/BaseDark.xaml @@ -33,6 +33,9 @@ + + + diff --git a/src/Avalonia.Themes.Simple/Accents/BaseLight.xaml b/src/Avalonia.Themes.Simple/Accents/BaseLight.xaml index 6247815303..43acb16be5 100644 --- a/src/Avalonia.Themes.Simple/Accents/BaseLight.xaml +++ b/src/Avalonia.Themes.Simple/Accents/BaseLight.xaml @@ -33,6 +33,8 @@ - + + + diff --git a/src/Avalonia.Themes.Simple/Controls/RefreshContainer.xaml b/src/Avalonia.Themes.Simple/Controls/RefreshContainer.xaml new file mode 100644 index 0000000000..326b217068 --- /dev/null +++ b/src/Avalonia.Themes.Simple/Controls/RefreshContainer.xaml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Simple/Controls/RefreshVisualizer.xaml b/src/Avalonia.Themes.Simple/Controls/RefreshVisualizer.xaml new file mode 100644 index 0000000000..0b12fe133d --- /dev/null +++ b/src/Avalonia.Themes.Simple/Controls/RefreshVisualizer.xaml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml b/src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml index 4aefa0136c..4bad556338 100644 --- a/src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml +++ b/src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml @@ -65,6 +65,8 @@ + + From 68fd64f1640b657eb3f15987ff9e077eb7f366ac Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Thu, 17 Nov 2022 15:29:39 +0000 Subject: [PATCH 02/87] Use Composition Api for animation --- .../Pages/RefreshContainerPage.axaml | 2 + .../Input/PullGestureRecognizer.cs | 2 +- .../PullToRefresh/RefreshContainer.cs | 48 +-- .../PullToRefresh/RefreshInfoProvider.cs | 13 +- .../PullToRefresh/RefreshVisualizer.cs | 282 +++++++++++------- ...ScrollViewerIRefreshInfoProviderAdapter.cs | 91 ++++-- .../Controls/RefreshVisualizer.xaml | 32 +- 7 files changed, 277 insertions(+), 193 deletions(-) diff --git a/samples/ControlCatalog/Pages/RefreshContainerPage.axaml b/samples/ControlCatalog/Pages/RefreshContainerPage.axaml index 0123251bd0..d467d14427 100644 --- a/samples/ControlCatalog/Pages/RefreshContainerPage.axaml +++ b/samples/ControlCatalog/Pages/RefreshContainerPage.axaml @@ -2,9 +2,11 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:viewModels="using:ControlCatalog.ViewModels" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" + x:DataType="viewModels:RefreshContainerViewModel" x:Class="ControlCatalog.Pages.RefreshContainerPage"> RefreshRequestedEvent = RoutedEvent.Register(nameof(RefreshRequested), RoutingStrategies.Bubble); - internal static readonly DirectProperty RefreshInfoProviderAdapterProperty = - AvaloniaProperty.RegisterDirect(nameof(RefreshInfoProviderAdapter), + internal static readonly DirectProperty RefreshInfoProviderAdapterProperty = + AvaloniaProperty.RegisterDirect(nameof(RefreshInfoProviderAdapter), (s) => s.RefreshInfoProviderAdapter, (s, o) => s.RefreshInfoProviderAdapter = o); - public static readonly DirectProperty RefreshVisualizerProperty = - AvaloniaProperty.RegisterDirect(nameof(RefreshVisualizer), + public static readonly DirectProperty RefreshVisualizerProperty = + AvaloniaProperty.RegisterDirect(nameof(RefreshVisualizer), s => s.RefreshVisualizer, (s, o) => s.RefreshVisualizer = o); public static readonly StyledProperty PullDirectionProperty = AvaloniaProperty.Register(nameof(PullDirection), PullDirection.TopToBottom); - public ScrollViewerIRefreshInfoProviderAdapter RefreshInfoProviderAdapter + public ScrollViewerIRefreshInfoProviderAdapter? RefreshInfoProviderAdapter { get => _refreshInfoProviderAdapter; set { @@ -42,7 +42,7 @@ namespace Avalonia.Controls private bool _hasDefaultRefreshVisualizer; - public RefreshVisualizer RefreshVisualizer + public RefreshVisualizer? RefreshVisualizer { get => _refreshVisualizer; set { @@ -88,7 +88,7 @@ namespace Avalonia.Controls else { _hasDefaultRefreshVisualizer = false; - RaisePropertyChanged(RefreshVisualizerProperty, null, _refreshVisualizer); + RaisePropertyChanged(RefreshVisualizerProperty, default, _refreshVisualizer); } OnPullDirectionChanged(); @@ -115,19 +115,23 @@ namespace Avalonia.Controls if (change.Property == RefreshInfoProviderAdapterProperty) { - if (_refreshInfoProvider != null) - { - _refreshVisualizer.RefreshInfoProvider = _refreshInfoProvider; - } - else + if (_refreshVisualizer != null) { - if (RefreshInfoProviderAdapter != null && _refreshVisualizer != null) + if (_refreshInfoProvider != null) { - _refreshInfoProvider = RefreshInfoProviderAdapter.AdaptFromTree(this, _refreshVisualizer.Bounds.Size); - - if (_refreshInfoProvider != null) + _refreshVisualizer.RefreshInfoProvider = _refreshInfoProvider; + } + else + { + if (RefreshInfoProviderAdapter != null && _refreshVisualizer != null) { - _refreshVisualizer.RefreshInfoProvider = _refreshInfoProvider; + _refreshInfoProvider = RefreshInfoProviderAdapter?.AdaptFromTree(this, _refreshVisualizer.Bounds.Size); + + if (_refreshInfoProvider != null) + { + _refreshVisualizer.RefreshInfoProvider = _refreshInfoProvider; + RefreshInfoProviderAdapter?.SetAnimations(_refreshVisualizer); + } } } } @@ -157,7 +161,7 @@ namespace Avalonia.Controls private void OnPullDirectionChanged() { - if (_visualizerPresenter != null) + if (_visualizerPresenter != null && _refreshVisualizer != null) { switch (PullDirection) { diff --git a/src/Avalonia.Controls/PullToRefresh/RefreshInfoProvider.cs b/src/Avalonia.Controls/PullToRefresh/RefreshInfoProvider.cs index 3fc32cdd08..506b48f6e2 100644 --- a/src/Avalonia.Controls/PullToRefresh/RefreshInfoProvider.cs +++ b/src/Avalonia.Controls/PullToRefresh/RefreshInfoProvider.cs @@ -1,6 +1,7 @@ using System; using Avalonia.Input; using Avalonia.Interactivity; +using Avalonia.Rendering.Composition; namespace Avalonia.Controls.PullToRefresh { @@ -11,7 +12,7 @@ namespace Avalonia.Controls.PullToRefresh private readonly PullDirection _refreshPullDirection; private readonly Size _refreshVisualizerSize; - private readonly Visual _visual; + private readonly CompositionVisual? _visual; private bool _isInteractingForRefresh; private double _interactionRatio; private bool _entered; @@ -60,7 +61,7 @@ namespace Avalonia.Controls.PullToRefresh } } - internal Visual Visual => _visual; + internal CompositionVisual? Visual => _visual; public event EventHandler RefreshStarted { @@ -74,7 +75,7 @@ namespace Avalonia.Controls.PullToRefresh remove => RemoveHandler(RefreshCompletedEvent, value); } - internal void InteractingStateEntered(object sender, PullGestureEventArgs e) + internal void InteractingStateEntered(object? sender, PullGestureEventArgs e) { if (!_entered) { @@ -85,7 +86,7 @@ namespace Avalonia.Controls.PullToRefresh ValuesChanged(e.Delta); } - internal void InteractingStateExited(object sender, PullGestureEndedEventArgs e) + internal void InteractingStateExited(object? sender, PullGestureEndedEventArgs e) { IsInteractingForRefresh = false; _entered = false; @@ -94,10 +95,10 @@ namespace Avalonia.Controls.PullToRefresh } - public RefreshInfoProvider(PullDirection refreshPullDirection, Size refreshVIsualizerSize, Visual visual) + public RefreshInfoProvider(PullDirection refreshPullDirection, Size? refreshVIsualizerSize, CompositionVisual? visual) { _refreshPullDirection = refreshPullDirection; - _refreshVisualizerSize = refreshVIsualizerSize; + _refreshVisualizerSize = refreshVIsualizerSize ?? default; _visual = visual; } diff --git a/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs b/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs index 81c613443d..5d0eee7478 100644 --- a/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs +++ b/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs @@ -1,4 +1,5 @@ using System; +using System.Numerics; using System.Reactive.Linq; using System.Threading; using Avalonia.Animation; @@ -7,25 +8,24 @@ using Avalonia.Controls.PullToRefresh; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Media; +using Avalonia.Rendering.Composition; namespace Avalonia.Controls { public class RefreshVisualizer : ContentControl { private const int DefaultIndicatorSize = 24; - private const double MinimumIndicatorOpacity = 0.4; + private const float MinimumIndicatorOpacity = 0.4f; private const string ArrowPathData = "M18.6195264,3.31842271 C19.0080059,3.31842271 19.3290603,3.60710385 19.3798716,3.9816481 L19.3868766,4.08577298 L19.3868766,6.97963208 C19.3868766,7.36811161 19.0981955,7.68916605 18.7236513,7.73997735 L18.6195264,7.74698235 L15.7256673,7.74698235 C15.3018714,7.74698235 14.958317,7.40342793 14.958317,6.97963208 C14.958317,6.59115255 15.2469981,6.27009811 15.6215424,6.21928681 L15.7256673,6.21228181 L16.7044011,6.21182461 C13.7917384,3.87107476 9.52212532,4.05209336 6.81933829,6.75488039 C3.92253872,9.65167996 3.92253872,14.34832 6.81933829,17.2451196 C9.71613786,20.1419192 14.4127779,20.1419192 17.3095775,17.2451196 C19.0725398,15.4821573 19.8106555,12.9925923 19.3476248,10.58925 C19.2674502,10.173107 19.5398064,9.77076216 19.9559494,9.69058758 C20.3720923,9.610413 20.7744372,9.88276918 20.8546118,10.2989121 C21.4129973,13.1971899 20.5217103,16.2033812 18.3947747,18.3303168 C14.8986373,21.8264542 9.23027854,21.8264542 5.73414113,18.3303168 C2.23800371,14.8341794 2.23800371,9.16582064 5.73414113,5.66968323 C9.05475132,2.34907304 14.3349409,2.18235834 17.8523166,5.16953912 L17.8521761,4.08577298 C17.8521761,3.66197713 18.1957305,3.31842271 18.6195264,3.31842271 Z"; private double _executingRatio = 0.8; - private RotateTransform _visualizerRotateTransform; - private TranslateTransform _contentTranslateTransform; private RefreshVisualizerState _refreshVisualizerState; - private RefreshInfoProvider _refreshInfoProvider; - private IDisposable _isInteractingSubscription; - private IDisposable _interactionRatioSubscription; + private RefreshInfoProvider? _refreshInfoProvider; + private IDisposable? _isInteractingSubscription; + private IDisposable? _interactionRatioSubscription; private bool _isInteractingForRefresh; private Grid? _root; - private Control _content; + private Control? _content; private RefreshVisualizerOrientation _orientation; private float _startingRotationAngle; private double _interactionRatio; @@ -46,9 +46,11 @@ namespace Avalonia.Controls AvaloniaProperty.RegisterDirect(nameof(Orientation), s => s.Orientation, (s, o) => s.Orientation = o); - public DirectProperty RefreshInfoProviderProperty = - AvaloniaProperty.RegisterDirect(nameof(RefreshInfoProvider), + public DirectProperty RefreshInfoProviderProperty = + AvaloniaProperty.RegisterDirect(nameof(RefreshInfoProvider), s => s.RefreshInfoProvider, (s, o) => s.RefreshInfoProvider = o); + private Vector3 _defaultOffset; + private bool _played; public RefreshVisualizerState RefreshVisualizerState { @@ -88,7 +90,7 @@ namespace Avalonia.Controls } } - public RefreshInfoProvider RefreshInfoProvider + public RefreshInfoProvider? RefreshInfoProvider { get => _refreshInfoProvider; internal set { @@ -106,20 +108,17 @@ namespace Avalonia.Controls remove => RemoveHandler(RefreshRequestedEvent, value); } - public RefreshVisualizer() - { - _visualizerRotateTransform = new RotateTransform(); - _contentTranslateTransform = new TranslateTransform(); - } - protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { base.OnApplyTemplate(e); + this.ClipToBounds = false; + _root = e.NameScope.Find("PART_Root"); if (_root != null) { + _content = Content as Control; if (_content == null) { Content = new PathIcon() @@ -140,69 +139,113 @@ namespace Avalonia.Controls UpdateContent(); } + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + + UpdateContent(); + } + private void UpdateContent() { - if (_content != null) + if (_content != null && _root != null) { - switch (RefreshVisualizerState) + var root = _root; + var visual = _refreshInfoProvider?.Visual; + var contentVisual = ElementComposition.GetElementVisual(_content); + var visualizerVisual = ElementComposition.GetElementVisual(this); + if (visual != null && contentVisual != null && visualizerVisual != null) { - case RefreshVisualizerState.Idle: - _content.Classes.Remove("refreshing"); - _root.Classes.Remove("pending"); - _content.RenderTransform = _visualizerRotateTransform; - _content.Opacity = MinimumIndicatorOpacity; - _visualizerRotateTransform.Angle = _startingRotationAngle; - _contentTranslateTransform.X = 0; - _contentTranslateTransform.Y = 0; - break; - case RefreshVisualizerState.Interacting: - _content.Classes.Remove("refreshing"); - _root.Classes.Remove("pending"); - _content.RenderTransform = _visualizerRotateTransform; - _content.Opacity = MinimumIndicatorOpacity; - _visualizerRotateTransform.Angle = _startingRotationAngle + (_interactionRatio * 360); - _content.Height = DefaultIndicatorSize; - _content.Width = DefaultIndicatorSize; - if (IsPullDirectionVertical) - { - _contentTranslateTransform.X = 0; - _contentTranslateTransform.Y = _interactionRatio * (IsPullDirectionFar ? -1 : 1) * _root.Bounds.Height; - } - else - { - _contentTranslateTransform.Y = 0; - _contentTranslateTransform.X = _interactionRatio * (IsPullDirectionFar ? -1 : 1) * _root.Bounds.Width; - } - break; - case RefreshVisualizerState.Pending: - _content.Classes.Remove("refreshing"); - _content.Opacity = 1; - _content.RenderTransform = _visualizerRotateTransform; - if (IsPullDirectionVertical) - { - _contentTranslateTransform.X = 0; - _contentTranslateTransform.Y = _interactionRatio * (IsPullDirectionFar ? -1 : 1) * _root.Bounds.Height; - } - else - { - _contentTranslateTransform.Y = 0; - _contentTranslateTransform.X = _interactionRatio * (IsPullDirectionFar ? -1 : 1) * _root.Bounds.Width; - } - - _root.Classes.Add("pending"); - break; - case RefreshVisualizerState.Refreshing: - _root.Classes.Remove("pending"); - _content.Classes.Add("refreshing"); - _content.Opacity = 1; - _content.Height = DefaultIndicatorSize; - _content.Width = DefaultIndicatorSize; - break; - case RefreshVisualizerState.Peeking: - _root.Classes.Remove("pending"); - _content.Opacity = 1; - _visualizerRotateTransform.Angle += _startingRotationAngle; - break; + contentVisual.CenterPoint = new Vector3((float)(_content.Bounds.Width / 2), (float)(_content.Bounds.Height / 2), 0); + switch (RefreshVisualizerState) + { + case RefreshVisualizerState.Idle: + _played = false; + contentVisual.Opacity = MinimumIndicatorOpacity; + contentVisual.RotationAngle = (float)(_startingRotationAngle * Math.PI / 180f); + visualizerVisual.Offset = IsPullDirectionVertical ? + new Vector3(visualizerVisual.Offset.X, 0, 0) : + new Vector3(0, visualizerVisual.Offset.Y, 0); + contentVisual.Scale = new Vector3(0.9f, 0.9f, 0.9f); + visual.Offset = default; + break; + case RefreshVisualizerState.Interacting: + _played = false; + contentVisual.Opacity = MinimumIndicatorOpacity; + contentVisual.RotationAngle = (float)((_startingRotationAngle + (_interactionRatio * 360)) * Math.PI / 180f); + Vector3 offset = default; + if (IsPullDirectionVertical) + { + offset = new Vector3(0, (float)(_interactionRatio * (IsPullDirectionFar ? -1 : 1) * root.Bounds.Height), 0); + } + else + { + offset = new Vector3((float)(_interactionRatio * (IsPullDirectionFar ? -1 : 1) * root.Bounds.Width), 0, 0); + } + visual.Offset = offset; + visualizerVisual.Offset = IsPullDirectionVertical ? + new Vector3(visualizerVisual.Offset.X, offset.Y, 0) : + new Vector3(offset.X, visualizerVisual.Offset.Y, 0); + contentVisual.Scale = new Vector3(0.9f, 0.9f, 0.9f); + break; + case RefreshVisualizerState.Pending: + contentVisual.Opacity = 1; + contentVisual.RotationAngle = (float)((_startingRotationAngle + 360) * Math.PI / 180f); + if (IsPullDirectionVertical) + { + offset = new Vector3(0, (float)(_interactionRatio * (IsPullDirectionFar ? -1 : 1) * root.Bounds.Height), 0); + } + else + { + offset = new Vector3((float)(_interactionRatio * (IsPullDirectionFar ? -1 : 1) * root.Bounds.Width), 0, 0); + } + visual.Offset = offset; + visualizerVisual.Offset = IsPullDirectionVertical ? + new Vector3(visualizerVisual.Offset.X, offset.Y, 0) : + new Vector3(offset.X, visualizerVisual.Offset.Y, 0); + + if (!_played) + { + _played = true; + var scaleAnimation = contentVisual.Compositor!.CreateVector3KeyFrameAnimation(); + scaleAnimation.Target = "Scale"; + scaleAnimation.InsertKeyFrame(0.5f, new Vector3(1.5f, 1.5f, 1)); + scaleAnimation.InsertKeyFrame(1f, new Vector3(1f, 1f, 1)); + scaleAnimation.Duration = TimeSpan.FromSeconds(0.3); + + contentVisual.StartAnimation("Scale", scaleAnimation); + } + break; + case RefreshVisualizerState.Refreshing: + var rotateAnimation = contentVisual.Compositor!.CreateScalarKeyFrameAnimation(); + rotateAnimation.Target = "RotationAngle"; + rotateAnimation.InsertKeyFrame(0, (float)(0)); + rotateAnimation.InsertKeyFrame(0.5f, (float)(Math.PI)); + rotateAnimation.InsertKeyFrame(1, (float)(2 * Math.PI)); + rotateAnimation.Duration = TimeSpan.FromSeconds(1); + rotateAnimation.IterationCount = 1000; + + contentVisual.StartAnimation("RotationAngle", rotateAnimation); + contentVisual.Opacity = 1; + contentVisual.Scale = new Vector3(0.9f, 0.9f, 0.9f); + if (IsPullDirectionVertical) + { + offset = new Vector3(0, (float)(_executingRatio * (IsPullDirectionFar ? -1 : 1) * root.Bounds.Height), 0); + } + else + { + offset = new Vector3((float)(_executingRatio * (IsPullDirectionFar ? -1 : 1) * root.Bounds.Width), 0, 0); + } + visual.Offset = offset; + visualizerVisual.Offset = IsPullDirectionVertical ? + new Vector3(visualizerVisual.Offset.X, offset.Y, 0) : + new Vector3(offset.X, visualizerVisual.Offset.Y, 0); + break; + case RefreshVisualizerState.Peeking: + contentVisual.Opacity = 1; + contentVisual.RotationAngle = (float)(_startingRotationAngle * Math.PI / 180f); + break; + } } } } @@ -254,12 +297,6 @@ namespace Avalonia.Controls Width = DefaultIndicatorSize }; - var transformGroup = new TransformGroup(); - transformGroup.Children.Add(_visualizerRotateTransform); - - _content.RenderTransform = _visualizerRotateTransform; - _root.RenderTransform = _contentTranslateTransform; - var transition = new Transitions { new DoubleTransition() @@ -270,16 +307,42 @@ namespace Avalonia.Controls }; _content.Transitions = transition; - } - var scalingGrid = new Grid(); - scalingGrid.VerticalAlignment = Layout.VerticalAlignment.Center; - scalingGrid.HorizontalAlignment = Layout.HorizontalAlignment.Center; - scalingGrid.RenderTransform = new ScaleTransform(); - - scalingGrid.Children.Add(_content); + _content.Loaded += (s, e) => + { + var composition = ElementComposition.GetElementVisual(_content); + var compositor = composition!.Compositor; + composition.Opacity = 0; + + var smoothRotationAnimation + = compositor.CreateScalarKeyFrameAnimation(); + smoothRotationAnimation.Target = "RotationAngle"; + smoothRotationAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue"); + smoothRotationAnimation.Duration = TimeSpan.FromMilliseconds(100); + + var offsetAnimation = compositor.CreateVector3KeyFrameAnimation(); + offsetAnimation.Target = "Offset"; + offsetAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue"); + offsetAnimation.Duration = TimeSpan.FromMilliseconds(150); + + var scaleAnimation + = compositor.CreateVector3KeyFrameAnimation(); + scaleAnimation.Target = "Scale"; + scaleAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue"); + scaleAnimation.Duration = TimeSpan.FromMilliseconds(100); + + var animation = compositor.CreateImplicitAnimationCollection(); + animation["RotationAngle"] = smoothRotationAnimation; + animation["Offset"] = offsetAnimation; + animation["Scale"] = scaleAnimation; + + composition.ImplicitAnimations = animation; + + UpdateContent(); + }; + } - _root.Children.Insert(0, scalingGrid); + _root.Children.Add(_content); _content.VerticalAlignment = Layout.VerticalAlignment.Center; _content.HorizontalAlignment = Layout.HorizontalAlignment.Center; } @@ -294,25 +357,23 @@ namespace Avalonia.Controls } else if (change.Property == BoundsProperty) { - if (_content != null) + switch (PullDirection) { - var parent = _content.Parent as Control; - switch (PullDirection) - { - case PullDirection.TopToBottom: - parent.Margin = new Thickness(0, -Bounds.Height - DefaultIndicatorSize - (0.5 * DefaultIndicatorSize), 0, 0); - break; - case PullDirection.BottomToTop: - parent.Margin = new Thickness(0, 0, 0, -Bounds.Height - DefaultIndicatorSize - (0.5 * DefaultIndicatorSize)); - break; - case PullDirection.LeftToRight: - parent.Margin = new Thickness(-Bounds.Width - DefaultIndicatorSize - (0.5 * DefaultIndicatorSize), 0, 0, 0); - break; - case PullDirection.RightToLeft: - parent.Margin = new Thickness(0, 0, -Bounds.Width - DefaultIndicatorSize - (0.5 * DefaultIndicatorSize), 0); - break; - } + case PullDirection.TopToBottom: + RenderTransform = new TranslateTransform(0, -Bounds.Height); + break; + case PullDirection.BottomToTop: + RenderTransform = new TranslateTransform(0, Bounds.Height); + break; + case PullDirection.LeftToRight: + RenderTransform = new TranslateTransform(-Bounds.Width, 0); + break; + case PullDirection.RightToLeft: + RenderTransform = new TranslateTransform(Bounds.Width, 0); + break; } + + UpdateContent(); } } @@ -354,16 +415,15 @@ namespace Avalonia.Controls _interactionRatioSubscription?.Dispose(); _interactionRatioSubscription = null; - if (_refreshInfoProvider != null) + if (RefreshInfoProvider != null) { - _isInteractingSubscription = _refreshInfoProvider.GetObservable(RefreshInfoProvider.IsInteractingForRefreshProperty) + _isInteractingSubscription = RefreshInfoProvider.GetObservable(RefreshInfoProvider.IsInteractingForRefreshProperty) .Subscribe(InteractingForRefreshObserver); - _interactionRatioSubscription = _refreshInfoProvider.GetObservable(RefreshInfoProvider.InteractionRatioProperty) + _interactionRatioSubscription = RefreshInfoProvider.GetObservable(RefreshInfoProvider.InteractionRatioProperty) .Subscribe(InteractionRatioObserver); - var visual = _refreshInfoProvider.Visual; - visual.RenderTransform = _contentTranslateTransform; + var visual = RefreshInfoProvider.Visual; _executingRatio = RefreshInfoProvider.ExecutionRatio; } diff --git a/src/Avalonia.Controls/PullToRefresh/ScrollViewerIRefreshInfoProviderAdapter.cs b/src/Avalonia.Controls/PullToRefresh/ScrollViewerIRefreshInfoProviderAdapter.cs index 0b0c8c99b2..d8e90c01c0 100644 --- a/src/Avalonia.Controls/PullToRefresh/ScrollViewerIRefreshInfoProviderAdapter.cs +++ b/src/Avalonia.Controls/PullToRefresh/ScrollViewerIRefreshInfoProviderAdapter.cs @@ -1,6 +1,7 @@ using System; using Avalonia.Input; using Avalonia.Interactivity; +using Avalonia.Rendering.Composition; using Avalonia.VisualTree; namespace Avalonia.Controls.PullToRefresh @@ -11,9 +12,9 @@ namespace Avalonia.Controls.PullToRefresh private const int InitialOffsetThreshold = 1; private PullDirection _refreshPullDirection; - private ScrollViewer _scrollViewer; - private RefreshInfoProvider _refreshInfoProvider; - private PullGestureRecognizer _pullGestureRecognizer; + private ScrollViewer? _scrollViewer; + private RefreshInfoProvider? _refreshInfoProvider; + private PullGestureRecognizer? _pullGestureRecognizer; private InputElement? _interactionSource; private bool _isVisualizerInteractionSourceAttached; @@ -22,7 +23,7 @@ namespace Avalonia.Controls.PullToRefresh _refreshPullDirection = pullDirection; } - public RefreshInfoProvider AdaptFromTree(IVisual root, Size refreshVIsualizerSize) + public RefreshInfoProvider? AdaptFromTree(IVisual root, Size? refreshVIsualizerSize) { if (root is ScrollViewer scrollViewer) { @@ -44,7 +45,7 @@ namespace Avalonia.Controls.PullToRefresh } } - ScrollViewer AdaptFromTreeRecursiveHelper(IVisual root, int depth) + ScrollViewer? AdaptFromTreeRecursiveHelper(IVisual root, int depth) { if (depth == 0) { @@ -74,7 +75,7 @@ namespace Avalonia.Controls.PullToRefresh return null; } - public RefreshInfoProvider Adapt(ScrollViewer adaptee, Size refreshVIsualizerSize) + public RefreshInfoProvider Adapt(ScrollViewer adaptee, Size? refreshVIsualizerSize) { if (adaptee == null) { @@ -121,7 +122,7 @@ namespace Avalonia.Controls.PullToRefresh } } - _refreshInfoProvider = new RefreshInfoProvider(_refreshPullDirection, refreshVIsualizerSize, content); + _refreshInfoProvider = new RefreshInfoProvider(_refreshPullDirection, refreshVIsualizerSize, ElementComposition.GetElementVisual(content)); _pullGestureRecognizer = new PullGestureRecognizer(_refreshPullDirection); @@ -140,7 +141,7 @@ namespace Avalonia.Controls.PullToRefresh return _refreshInfoProvider; } - private void ScrollViewer_ScrollChanged(object sender, ScrollChangedEventArgs e) + private void ScrollViewer_ScrollChanged(object? sender, ScrollChangedEventArgs e) { if (_isVisualizerInteractionSourceAttached && _refreshInfoProvider != null && _refreshInfoProvider.IsInteractingForRefresh) { @@ -151,9 +152,46 @@ namespace Avalonia.Controls.PullToRefresh } } - private void ScrollViewer_Loaded(object sender, RoutedEventArgs e) + public void SetAnimations(RefreshVisualizer refreshVisualizer) { - var content = _scrollViewer.Content as Visual; + var visualizerComposition = ElementComposition.GetElementVisual(refreshVisualizer); + if (visualizerComposition != null) + { + var compositor = visualizerComposition.Compositor; + + var offsetAnimation = compositor.CreateVector3KeyFrameAnimation(); + offsetAnimation.Target = "Offset"; + offsetAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue"); + offsetAnimation.Duration = TimeSpan.FromMilliseconds(150); + + var animation = compositor.CreateImplicitAnimationCollection(); + animation["Offset"] = offsetAnimation; + visualizerComposition.ImplicitAnimations = animation; + } + + if(_scrollViewer != null && _scrollViewer.Content is Visual visual) + { + var scollContentComposition = ElementComposition.GetElementVisual(visual); + + if(scollContentComposition != null) + { + var compositor = scollContentComposition.Compositor; + + var offsetAnimation = compositor.CreateVector3KeyFrameAnimation(); + offsetAnimation.Target = "Offset"; + offsetAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue"); + offsetAnimation.Duration = TimeSpan.FromMilliseconds(150); + + var animation = compositor.CreateImplicitAnimationCollection(); + animation["Offset"] = offsetAnimation; + scollContentComposition.ImplicitAnimations = animation; + } + } + } + + private void ScrollViewer_Loaded(object? sender, RoutedEventArgs? e) + { + var content = _scrollViewer?.Content as Visual; if (content == null) { throw new ArgumentException(nameof(_scrollViewer), "Adaptee's content property must be a Visual"); @@ -166,23 +204,26 @@ namespace Avalonia.Controls.PullToRefresh MakeInteractionSource(content.Parent as InputElement); - _scrollViewer.Loaded -= ScrollViewer_Loaded; + if (_scrollViewer != null) + { + _scrollViewer.Loaded -= ScrollViewer_Loaded; + } } - private void MakeInteractionSource(InputElement element) + private void MakeInteractionSource(InputElement? element) { _interactionSource = element; - if (_pullGestureRecognizer != null) + if (_pullGestureRecognizer != null && _refreshInfoProvider != null) { - element.GestureRecognizers.Add(_pullGestureRecognizer); - _interactionSource.AddHandler(Gestures.PullGestureEvent, _refreshInfoProvider.InteractingStateEntered); - _interactionSource.AddHandler(Gestures.PullGestureEndedEvent, _refreshInfoProvider.InteractingStateExited); + element?.GestureRecognizers.Add(_pullGestureRecognizer); + _interactionSource?.AddHandler(Gestures.PullGestureEvent, _refreshInfoProvider.InteractingStateEntered); + _interactionSource?.AddHandler(Gestures.PullGestureEndedEvent, _refreshInfoProvider.InteractingStateExited); _isVisualizerInteractionSourceAttached = true; } } - private void ScrollViewer_PointerReleased(object sender, PointerReleasedEventArgs e) + private void ScrollViewer_PointerReleased(object? sender, PointerReleasedEventArgs e) { if (_refreshInfoProvider != null) { @@ -190,9 +231,12 @@ namespace Avalonia.Controls.PullToRefresh } } - private void ScrollViewer_PointerPressed(object sender, PointerPressedEventArgs e) + private void ScrollViewer_PointerPressed(object? sender, PointerPressedEventArgs e) { - _refreshInfoProvider.PeekingMode = !IsWithinOffsetThreashold(); + if (_refreshInfoProvider != null) + { + _refreshInfoProvider.PeekingMode = !IsWithinOffsetThreashold(); + } } private bool IsWithinOffsetThreashold() @@ -219,9 +263,12 @@ namespace Avalonia.Controls.PullToRefresh private void CleanUpScrollViewer() { - _scrollViewer.PointerPressed -= ScrollViewer_PointerPressed; - _scrollViewer.PointerReleased -= ScrollViewer_PointerReleased; - _scrollViewer.ScrollChanged -= ScrollViewer_ScrollChanged; + if (_scrollViewer != null) + { + _scrollViewer.PointerPressed -= ScrollViewer_PointerPressed; + _scrollViewer.PointerReleased -= ScrollViewer_PointerReleased; + _scrollViewer.ScrollChanged -= ScrollViewer_ScrollChanged; + } } } } diff --git a/src/Avalonia.Themes.Fluent/Controls/RefreshVisualizer.xaml b/src/Avalonia.Themes.Fluent/Controls/RefreshVisualizer.xaml index 388ca814f8..3fafabb667 100644 --- a/src/Avalonia.Themes.Fluent/Controls/RefreshVisualizer.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/RefreshVisualizer.xaml @@ -14,37 +14,7 @@ - - - - - + + + + + + diff --git a/src/Avalonia.Themes.Simple/Controls/RefreshVisualizer.xaml b/src/Avalonia.Themes.Simple/Controls/RefreshVisualizer.xaml index 0b12fe133d..bd7e43530a 100644 --- a/src/Avalonia.Themes.Simple/Controls/RefreshVisualizer.xaml +++ b/src/Avalonia.Themes.Simple/Controls/RefreshVisualizer.xaml @@ -18,51 +18,10 @@ MinHeight="80" Background="{TemplateBinding Background}"> - - From 6e1ab5f4578ad7a4edb968f87b203a714c64c0a1 Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Fri, 18 Nov 2022 12:37:31 +0000 Subject: [PATCH 04/87] remove debug stuff --- src/Avalonia.Base/Input/PullGestureRecognizer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Input/PullGestureRecognizer.cs b/src/Avalonia.Base/Input/PullGestureRecognizer.cs index 6939646f13..bbbded44fa 100644 --- a/src/Avalonia.Base/Input/PullGestureRecognizer.cs +++ b/src/Avalonia.Base/Input/PullGestureRecognizer.cs @@ -100,7 +100,7 @@ namespace Avalonia.Input public void PointerPressed(PointerPressedEventArgs e) { - if (_target != null)// && (e.Pointer.Type == PointerType.Touch || e.Pointer.Type == PointerType.Pen)) + if (_target != null && (e.Pointer.Type == PointerType.Touch || e.Pointer.Type == PointerType.Pen)) { var position = e.GetPosition(_target); From 7d6932bd5ef3fca26b44f47911a162212f4bdfeb Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 28 Nov 2022 17:04:14 +0000 Subject: [PATCH 05/87] add failing unit tests. --- .../WindowTests.cs | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/WindowTests.cs b/tests/Avalonia.Controls.UnitTests/WindowTests.cs index 0cdde445d5..f73c3ac215 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowTests.cs @@ -986,7 +986,46 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(SizeToContent.WidthAndHeight, target.SizeToContent); } } + + [Fact] + public void IsVisible_Should_Open_Window() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var target = new Window(); + var raised = false; + + target.Opened += (s, e) => raised = true; + target.IsVisible = true; + Assert.True(raised); + } + } + + [Fact] + public void IsVisible_Should_Close_DialogWindow() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var parent = new Window(); + parent.Show(); + + var target = new Window(); + + var raised = false; + + var task = target.ShowDialog(parent); + + target.Closed += (sender, args) => raised = true; + + target.IsVisible = false; + + Assert.True(raised); + + Assert.False(task.Result); + } + } + protected virtual void Show(Window window) { window.Show(); From a7a3df912a4984273fd20788de9afe13022d4d39 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 29 Nov 2022 09:46:27 +0000 Subject: [PATCH 06/87] implement isvisible controlling show / hide / close (for dialogs) --- src/Avalonia.Controls/Window.cs | 50 +++++++++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 559d674c02..672099c3bb 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -72,6 +72,9 @@ namespace Avalonia.Controls private bool _isExtendedIntoWindowDecorations; private Thickness _windowDecorationMargin; private Thickness _offScreenMargin; + private bool _shown; + private bool _showingAsDialog; + private bool _inShowHideMethods; /// /// Defines the property. @@ -506,8 +509,14 @@ namespace Avalonia.Controls } Owner = null; + _showingAsDialog = false; + _shown = false; + _inShowHideMethods = true; PlatformImpl?.Dispose(); + + _inShowHideMethods = false; + } private bool ShouldCancelClose(CancelEventArgs? args = null) @@ -563,7 +572,7 @@ namespace Avalonia.Controls /// public override void Hide() { - if (!IsVisible) + if (!_shown) { return; } @@ -585,7 +594,10 @@ namespace Avalonia.Controls Owner = null; PlatformImpl?.Hide(); + _shown = false; + _inShowHideMethods = true; IsVisible = false; + _inShowHideMethods = false; } /// @@ -639,7 +651,7 @@ namespace Avalonia.Controls } } - if (IsVisible) + if (_shown) { return; } @@ -648,7 +660,10 @@ namespace Avalonia.Controls EnsureInitialized(); ApplyStyling(); + _inShowHideMethods = true; + _shown = true; IsVisible = true; + _inShowHideMethods = false; var initialSize = new Size( double.IsNaN(Width) ? Math.Max(MinWidth, ClientSize.Width) : Width, @@ -728,7 +743,11 @@ namespace Avalonia.Controls EnsureInitialized(); ApplyStyling(); + _shown = true; + _showingAsDialog = true; + _inShowHideMethods = true; IsVisible = true; + _inShowHideMethods = false; var initialSize = new Size( double.IsNaN(Width) ? ClientSize.Width : Width, @@ -999,6 +1018,33 @@ namespace Avalonia.Controls PlatformImpl?.SetSystemDecorations(typedNewValue); } + + if (change.Property == IsVisibleProperty) + { + if (!_inShowHideMethods) + { + var isVisible = change.GetNewValue(); + + if (_shown != isVisible) + { + if (!_shown) + { + ShowCore(null); + } + else + { + if (_showingAsDialog) + { + Close(false); + } + else + { + Hide(); + } + } + } + } + } } protected override AutomationPeer OnCreateAutomationPeer() From fdf76c5765ceb8f2c4191aa1257c2eb6cea3bccd Mon Sep 17 00:00:00 2001 From: daniel mayost Date: Wed, 30 Nov 2022 14:50:27 +0200 Subject: [PATCH 07/87] Replace IVisual with Visual --- nukebuild/Numerge | 2 +- src/Avalonia.Base/Animation/CrossFade.cs | 2 +- src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs | 2 +- src/Avalonia.Base/Media/Immutable/ImmutableTransform.cs | 2 +- src/Avalonia.Base/Media/Immutable/ImmutableVisualBrush.cs | 2 +- src/Avalonia.Base/Media/MatrixTransform.cs | 2 +- src/Avalonia.Base/Media/RotateTransform.cs | 2 +- src/Avalonia.Base/Media/ScaleTransform.cs | 2 +- src/Avalonia.Base/Media/SkewTransform.cs | 2 +- src/Avalonia.Base/Media/Transform.cs | 2 +- src/Avalonia.Base/Media/TranslateTransform.cs | 2 +- src/Avalonia.Base/Rendering/SceneGraph/VisualNode.cs | 2 +- 12 files changed, 12 insertions(+), 12 deletions(-) diff --git a/nukebuild/Numerge b/nukebuild/Numerge index aef10ae67d..fb92f917cd 160000 --- a/nukebuild/Numerge +++ b/nukebuild/Numerge @@ -1 +1 @@ -Subproject commit aef10ae67dc55c95f49b52a505a0be33bfa297a5 +Subproject commit fb92f917cd2d3aaec0d2294635d922184ff1e0fc diff --git a/src/Avalonia.Base/Animation/CrossFade.cs b/src/Avalonia.Base/Animation/CrossFade.cs index 608a0880ec..a229bc7ce6 100644 --- a/src/Avalonia.Base/Animation/CrossFade.cs +++ b/src/Avalonia.Base/Animation/CrossFade.cs @@ -10,7 +10,7 @@ using Avalonia.VisualTree; namespace Avalonia.Animation { /// - /// Defines a cross-fade animation between two s. + /// Defines a cross-fade animation between two s. /// public class CrossFade : IPageTransition { diff --git a/src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs b/src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs index c4508c3f5c..d56711ad68 100644 --- a/src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs +++ b/src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs @@ -7,7 +7,7 @@ using Avalonia.VisualTree; namespace Avalonia.Media.Imaging { /// - /// A bitmap that holds the rendering of a . + /// A bitmap that holds the rendering of a . /// public class RenderTargetBitmap : Bitmap, IDisposable, IRenderTarget { diff --git a/src/Avalonia.Base/Media/Immutable/ImmutableTransform.cs b/src/Avalonia.Base/Media/Immutable/ImmutableTransform.cs index d5ff2b8317..4478504eca 100644 --- a/src/Avalonia.Base/Media/Immutable/ImmutableTransform.cs +++ b/src/Avalonia.Base/Media/Immutable/ImmutableTransform.cs @@ -3,7 +3,7 @@ namespace Avalonia.Media.Immutable { /// - /// Represents a transform on an . + /// Represents a transform on an . /// public class ImmutableTransform : ITransform { diff --git a/src/Avalonia.Base/Media/Immutable/ImmutableVisualBrush.cs b/src/Avalonia.Base/Media/Immutable/ImmutableVisualBrush.cs index 1e0133c9b7..9b443391c5 100644 --- a/src/Avalonia.Base/Media/Immutable/ImmutableVisualBrush.cs +++ b/src/Avalonia.Base/Media/Immutable/ImmutableVisualBrush.cs @@ -4,7 +4,7 @@ using Avalonia.VisualTree; namespace Avalonia.Media.Immutable { /// - /// Paints an area with an . + /// Paints an area with an . /// internal class ImmutableVisualBrush : ImmutableTileBrush, IVisualBrush { diff --git a/src/Avalonia.Base/Media/MatrixTransform.cs b/src/Avalonia.Base/Media/MatrixTransform.cs index 4e60e1e290..c61acb730c 100644 --- a/src/Avalonia.Base/Media/MatrixTransform.cs +++ b/src/Avalonia.Base/Media/MatrixTransform.cs @@ -4,7 +4,7 @@ using Avalonia.VisualTree; namespace Avalonia.Media { /// - /// Transforms an according to a . + /// Transforms an according to a . /// public class MatrixTransform : Transform { diff --git a/src/Avalonia.Base/Media/RotateTransform.cs b/src/Avalonia.Base/Media/RotateTransform.cs index 126bb7c274..3bd409149c 100644 --- a/src/Avalonia.Base/Media/RotateTransform.cs +++ b/src/Avalonia.Base/Media/RotateTransform.cs @@ -4,7 +4,7 @@ using Avalonia.VisualTree; namespace Avalonia.Media { /// - /// Rotates an . + /// Rotates an . /// public class RotateTransform : Transform { diff --git a/src/Avalonia.Base/Media/ScaleTransform.cs b/src/Avalonia.Base/Media/ScaleTransform.cs index 259b23cbd2..d4c1a7f993 100644 --- a/src/Avalonia.Base/Media/ScaleTransform.cs +++ b/src/Avalonia.Base/Media/ScaleTransform.cs @@ -4,7 +4,7 @@ using Avalonia.VisualTree; namespace Avalonia.Media { /// - /// Scale an . + /// Scale an . /// public class ScaleTransform : Transform { diff --git a/src/Avalonia.Base/Media/SkewTransform.cs b/src/Avalonia.Base/Media/SkewTransform.cs index a96710e331..066f5371c3 100644 --- a/src/Avalonia.Base/Media/SkewTransform.cs +++ b/src/Avalonia.Base/Media/SkewTransform.cs @@ -4,7 +4,7 @@ using Avalonia.VisualTree; namespace Avalonia.Media { /// - /// Skews an . + /// Skews an . /// public class SkewTransform : Transform { diff --git a/src/Avalonia.Base/Media/Transform.cs b/src/Avalonia.Base/Media/Transform.cs index 023a8b9cdd..85393ab189 100644 --- a/src/Avalonia.Base/Media/Transform.cs +++ b/src/Avalonia.Base/Media/Transform.cs @@ -7,7 +7,7 @@ using Avalonia.VisualTree; namespace Avalonia.Media { /// - /// Represents a transform on an . + /// Represents a transform on an . /// public abstract class Transform : Animatable, IMutableTransform { diff --git a/src/Avalonia.Base/Media/TranslateTransform.cs b/src/Avalonia.Base/Media/TranslateTransform.cs index d6d6809f3d..0f910f3600 100644 --- a/src/Avalonia.Base/Media/TranslateTransform.cs +++ b/src/Avalonia.Base/Media/TranslateTransform.cs @@ -4,7 +4,7 @@ using Avalonia.VisualTree; namespace Avalonia.Media { /// - /// Translates (moves) an . + /// Translates (moves) an . /// public class TranslateTransform : Transform { diff --git a/src/Avalonia.Base/Rendering/SceneGraph/VisualNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/VisualNode.cs index 4eec214f4f..a991f2f657 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/VisualNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/VisualNode.cs @@ -10,7 +10,7 @@ using Avalonia.VisualTree; namespace Avalonia.Rendering.SceneGraph { /// - /// A node in the low-level scene graph representing an . + /// A node in the low-level scene graph representing an . /// internal class VisualNode : IVisualNode { From 8c019e669194784a29243a4922b6beb98dbf99e2 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 30 Nov 2022 14:15:25 +0000 Subject: [PATCH 08/87] simpler fix. --- src/Avalonia.Controls/Window.cs | 47 ++------------------------------- 1 file changed, 2 insertions(+), 45 deletions(-) diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 672099c3bb..280853967a 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -72,9 +72,6 @@ namespace Avalonia.Controls private bool _isExtendedIntoWindowDecorations; private Thickness _windowDecorationMargin; private Thickness _offScreenMargin; - private bool _shown; - private bool _showingAsDialog; - private bool _inShowHideMethods; /// /// Defines the property. @@ -179,6 +176,7 @@ namespace Avalonia.Controls private object? _dialogResult; private readonly Size _maxPlatformClientSize; private WindowStartupLocation _windowStartupLocation; + private bool _shown; /// /// Initializes static members of the class. @@ -509,14 +507,8 @@ namespace Avalonia.Controls } Owner = null; - _showingAsDialog = false; - _shown = false; - _inShowHideMethods = true; PlatformImpl?.Dispose(); - - _inShowHideMethods = false; - } private bool ShouldCancelClose(CancelEventArgs? args = null) @@ -594,10 +586,8 @@ namespace Avalonia.Controls Owner = null; PlatformImpl?.Hide(); - _shown = false; - _inShowHideMethods = true; IsVisible = false; - _inShowHideMethods = false; + _shown = false; } /// @@ -660,10 +650,8 @@ namespace Avalonia.Controls EnsureInitialized(); ApplyStyling(); - _inShowHideMethods = true; _shown = true; IsVisible = true; - _inShowHideMethods = false; var initialSize = new Size( double.IsNaN(Width) ? Math.Max(MinWidth, ClientSize.Width) : Width, @@ -743,11 +731,7 @@ namespace Avalonia.Controls EnsureInitialized(); ApplyStyling(); - _shown = true; - _showingAsDialog = true; - _inShowHideMethods = true; IsVisible = true; - _inShowHideMethods = false; var initialSize = new Size( double.IsNaN(Width) ? ClientSize.Width : Width, @@ -1018,33 +1002,6 @@ namespace Avalonia.Controls PlatformImpl?.SetSystemDecorations(typedNewValue); } - - if (change.Property == IsVisibleProperty) - { - if (!_inShowHideMethods) - { - var isVisible = change.GetNewValue(); - - if (_shown != isVisible) - { - if (!_shown) - { - ShowCore(null); - } - else - { - if (_showingAsDialog) - { - Close(false); - } - else - { - Hide(); - } - } - } - } - } } protected override AutomationPeer OnCreateAutomationPeer() From fb821c7899ca6ca1e900526df25d74326dddbb23 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 30 Nov 2022 14:26:34 +0000 Subject: [PATCH 09/87] ensure that isvisible = false can close dialogs. --- src/Avalonia.Controls/Window.cs | 230 +++++++++++++++++----------- src/Avalonia.Controls/WindowBase.cs | 18 +-- 2 files changed, 149 insertions(+), 99 deletions(-) diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 280853967a..aa0ce50bad 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -177,6 +177,7 @@ namespace Avalonia.Controls private readonly Size _maxPlatformClientSize; private WindowStartupLocation _windowStartupLocation; private bool _shown; + private bool _showingAsDialog; /// /// Initializes static members of the class. @@ -509,6 +510,8 @@ namespace Avalonia.Controls Owner = null; PlatformImpl?.Dispose(); + + _showingAsDialog = false; } private bool ShouldCancelClose(CancelEventArgs? args = null) @@ -601,6 +604,33 @@ namespace Avalonia.Controls ShowCore(null); } + protected override void IsVisibleChanged(AvaloniaPropertyChangedEventArgs e) + { + if (!IgnoreVisibilityChange) + { + var isVisible = e.GetNewValue(); + + if (_shown != isVisible) + { + if(!_shown) + { + Show(); + } + else + { + if (_showingAsDialog) + { + Close(false); + } + else + { + Hide(); + } + } + } + } + } + /// /// Shows the window as a child of . /// @@ -620,63 +650,72 @@ namespace Avalonia.Controls private void ShowCore(Window? parent) { - if (PlatformImpl == null) - { - throw new InvalidOperationException("Cannot re-show a closed window."); - } - - if (parent != null) + try { - if (parent.PlatformImpl == null) + IgnoreVisibilityChange = true; + + if (PlatformImpl == null) { - throw new InvalidOperationException("Cannot show a window with a closed parent."); + throw new InvalidOperationException("Cannot re-show a closed window."); } - else if (parent == this) + + if (parent != null) { - throw new InvalidOperationException("A Window cannot be its own parent."); + if (parent.PlatformImpl == null) + { + throw new InvalidOperationException("Cannot show a window with a closed parent."); + } + else if (parent == this) + { + throw new InvalidOperationException("A Window cannot be its own parent."); + } + else if (!parent.IsVisible) + { + throw new InvalidOperationException("Cannot show window with non-visible parent."); + } } - else if (!parent.IsVisible) + + if (_shown) { - throw new InvalidOperationException("Cannot show window with non-visible parent."); + return; } - } - if (_shown) - { - return; - } - - RaiseEvent(new RoutedEventArgs(WindowOpenedEvent)); + RaiseEvent(new RoutedEventArgs(WindowOpenedEvent)); - EnsureInitialized(); - ApplyStyling(); - _shown = true; - IsVisible = true; + EnsureInitialized(); + ApplyStyling(); + _shown = true; + IsVisible = true; - var initialSize = new Size( - double.IsNaN(Width) ? Math.Max(MinWidth, ClientSize.Width) : Width, - double.IsNaN(Height) ? Math.Max(MinHeight, ClientSize.Height) : Height); + var initialSize = new Size( + double.IsNaN(Width) ? Math.Max(MinWidth, ClientSize.Width) : Width, + double.IsNaN(Height) ? Math.Max(MinHeight, ClientSize.Height) : Height); - if (initialSize != ClientSize) - { - PlatformImpl?.Resize(initialSize, PlatformResizeReason.Layout); - } + if (initialSize != ClientSize) + { + PlatformImpl?.Resize(initialSize, PlatformResizeReason.Layout); + } - LayoutManager.ExecuteInitialLayoutPass(); + LayoutManager.ExecuteInitialLayoutPass(); - if (PlatformImpl != null && parent?.PlatformImpl is not null) - { - PlatformImpl.SetParent(parent.PlatformImpl); - } + if (PlatformImpl != null && parent?.PlatformImpl is not null) + { + PlatformImpl.SetParent(parent.PlatformImpl); + } - Owner = parent; - parent?.AddChild(this, false); + Owner = parent; + parent?.AddChild(this, false); - SetWindowStartupLocation(parent?.PlatformImpl); + SetWindowStartupLocation(parent?.PlatformImpl); - PlatformImpl?.Show(ShowActivated, false); - Renderer?.Start(); - OnOpened(EventArgs.Empty); + PlatformImpl?.Show(ShowActivated, false); + Renderer?.Start(); + OnOpened(EventArgs.Empty); + } + finally + { + IgnoreVisibilityChange = false; + } } /// @@ -706,68 +745,79 @@ namespace Avalonia.Controls /// public Task ShowDialog(Window owner) { - if (owner == null) - { - throw new ArgumentNullException(nameof(owner)); - } - else if (owner.PlatformImpl == null) - { - throw new InvalidOperationException("Cannot show a window with a closed owner."); - } - else if (owner == this) - { - throw new InvalidOperationException("A Window cannot be its own owner."); - } - else if (IsVisible) - { - throw new InvalidOperationException("The window is already being shown."); - } - else if (!owner.IsVisible) + try { - throw new InvalidOperationException("Cannot show window with non-visible parent."); - } + IgnoreVisibilityChange = true; + + if (owner == null) + { + throw new ArgumentNullException(nameof(owner)); + } + else if (owner.PlatformImpl == null) + { + throw new InvalidOperationException("Cannot show a window with a closed owner."); + } + else if (owner == this) + { + throw new InvalidOperationException("A Window cannot be its own owner."); + } + else if (IsVisible) + { + throw new InvalidOperationException("The window is already being shown."); + } + else if (!owner.IsVisible) + { + throw new InvalidOperationException("Cannot show window with non-visible parent."); + } - RaiseEvent(new RoutedEventArgs(WindowOpenedEvent)); + RaiseEvent(new RoutedEventArgs(WindowOpenedEvent)); - EnsureInitialized(); - ApplyStyling(); - IsVisible = true; + EnsureInitialized(); + ApplyStyling(); + _shown = true; + _showingAsDialog = true; + IsVisible = true; - var initialSize = new Size( - double.IsNaN(Width) ? ClientSize.Width : Width, - double.IsNaN(Height) ? ClientSize.Height : Height); + var initialSize = new Size( + double.IsNaN(Width) ? ClientSize.Width : Width, + double.IsNaN(Height) ? ClientSize.Height : Height); - if (initialSize != ClientSize) - { - PlatformImpl?.Resize(initialSize, PlatformResizeReason.Layout); - } + if (initialSize != ClientSize) + { + PlatformImpl?.Resize(initialSize, PlatformResizeReason.Layout); + } - LayoutManager.ExecuteInitialLayoutPass(); + LayoutManager.ExecuteInitialLayoutPass(); - var result = new TaskCompletionSource(); + var result = new TaskCompletionSource(); - PlatformImpl?.SetParent(owner.PlatformImpl); - Owner = owner; - owner.AddChild(this, true); + PlatformImpl?.SetParent(owner.PlatformImpl); + Owner = owner; + owner.AddChild(this, true); - SetWindowStartupLocation(owner.PlatformImpl); + SetWindowStartupLocation(owner.PlatformImpl); - PlatformImpl?.Show(ShowActivated, true); + PlatformImpl?.Show(ShowActivated, true); - Renderer?.Start(); + Renderer?.Start(); - Observable.FromEventPattern( - x => Closed += x, - x => Closed -= x) - .Take(1) - .Subscribe(_ => - { - owner.Activate(); - result.SetResult((TResult)(_dialogResult ?? default(TResult)!)); - }); + Observable.FromEventPattern( + x => Closed += x, + x => Closed -= x) + .Take(1) + .Subscribe(_ => + { + owner.Activate(); + result.SetResult((TResult)(_dialogResult ?? default(TResult)!)); + }); - OnOpened(EventArgs.Empty); - return result.Task; + OnOpened(EventArgs.Empty); + return result.Task; + } + finally + { + IgnoreVisibilityChange = false; + } } private void UpdateEnabled() diff --git a/src/Avalonia.Controls/WindowBase.cs b/src/Avalonia.Controls/WindowBase.cs index 8f1b2198ad..46653c8203 100644 --- a/src/Avalonia.Controls/WindowBase.cs +++ b/src/Avalonia.Controls/WindowBase.cs @@ -42,7 +42,7 @@ namespace Avalonia.Controls private bool _hasExecutedInitialLayoutPass; private bool _isActive; - private bool _ignoreVisibilityChange; + protected bool IgnoreVisibilityChange { get; set; } private WindowBase? _owner; static WindowBase() @@ -125,7 +125,7 @@ namespace Avalonia.Controls /// public virtual void Hide() { - _ignoreVisibilityChange = true; + IgnoreVisibilityChange = true; try { @@ -135,7 +135,7 @@ namespace Avalonia.Controls } finally { - _ignoreVisibilityChange = false; + IgnoreVisibilityChange = false; } } @@ -144,7 +144,7 @@ namespace Avalonia.Controls /// public virtual void Show() { - _ignoreVisibilityChange = true; + IgnoreVisibilityChange = true; try { @@ -163,7 +163,7 @@ namespace Avalonia.Controls } finally { - _ignoreVisibilityChange = false; + IgnoreVisibilityChange = false; } } @@ -202,7 +202,7 @@ namespace Avalonia.Controls protected override void HandleClosed() { - _ignoreVisibilityChange = true; + IgnoreVisibilityChange = true; try { @@ -217,7 +217,7 @@ namespace Avalonia.Controls } finally { - _ignoreVisibilityChange = false; + IgnoreVisibilityChange = false; } } @@ -318,9 +318,9 @@ namespace Avalonia.Controls Deactivated?.Invoke(this, EventArgs.Empty); } - private void IsVisibleChanged(AvaloniaPropertyChangedEventArgs e) + protected virtual void IsVisibleChanged(AvaloniaPropertyChangedEventArgs e) { - if (!_ignoreVisibilityChange) + if (!IgnoreVisibilityChange) { if ((bool)e.NewValue!) { From 4a490694a24f7e0cd2eff83cd7832fb2f18655f9 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 30 Nov 2022 20:08:42 +0000 Subject: [PATCH 10/87] fix unit test. --- src/Avalonia.Controls/Window.cs | 43 ++++++++++++++++++++------------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index aa0ce50bad..1ff08f0169 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -567,30 +567,39 @@ namespace Avalonia.Controls /// public override void Hide() { - if (!_shown) + try { - return; - } + IgnoreVisibilityChange = true; + + if (!_shown) + { + return; + } - Renderer?.Stop(); + Renderer?.Stop(); - if (Owner is Window owner) - { - owner.RemoveChild(this); - } + if (Owner is Window owner) + { + owner.RemoveChild(this); + } - if (_children.Count > 0) - { - foreach (var child in _children.ToArray()) + if (_children.Count > 0) { - child.child.Hide(); + foreach (var child in _children.ToArray()) + { + child.child.Hide(); + } } - } - Owner = null; - PlatformImpl?.Hide(); - IsVisible = false; - _shown = false; + Owner = null; + PlatformImpl?.Hide(); + IsVisible = false; + _shown = false; + } + finally + { + IgnoreVisibilityChange = false; + } } /// From 390221b3cee6173161e620da648081b477de9206 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Wed, 30 Nov 2022 22:32:27 -0500 Subject: [PATCH 11/87] Implement compile time MergedDictionaties --- .../Themes/Fluent/ColorPicker.xaml | 4 - .../Themes/Fluent/Fluent.xaml | 10 +- .../Themes/Simple/ColorPicker.xaml | 4 - .../Themes/Simple/Simple.xaml | 10 +- src/Avalonia.Themes.Fluent/Accents/Base.xaml | 1 + .../Controls/Button.xaml | 2 - .../Controls/FluentControls.xaml | 126 +++++++++--------- .../Controls/RepeatButton.xaml | 2 - .../Controls/ToggleButton.xaml | 2 - src/Avalonia.Themes.Fluent/FluentTheme.xaml | 4 +- .../FluentTheme.xaml.cs | 8 +- .../Controls/SimpleControls.xaml | 124 ++++++++--------- src/Avalonia.Themes.Simple/SimpleTheme.xaml | 2 +- .../SimpleTheme.xaml.cs | 4 +- .../AvaloniaRuntimeXamlLoader.cs | 4 +- .../AvaloniaXamlIlCompiler.cs | 1 + .../IXamlAstGroupTransformer.cs | 2 +- .../XamlIncludeGroupTransformer.cs | 75 +++++++---- .../XamlMergeResourceGroupTransformer.cs | 113 ++++++++++++++++ .../AvaloniaXamlIlWellKnownTypes.cs | 2 + .../XamlDocumentParseException.cs | 5 + .../Avalonia.Markup.Xaml.csproj | 1 + .../Styling/MergeResourceInclude.cs | 69 ++++++++++ .../Xaml/MergeResourceIncludeTests.cs | 124 +++++++++++++++++ 24 files changed, 515 insertions(+), 184 deletions(-) create mode 100644 src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlMergeResourceGroupTransformer.cs create mode 100644 src/Markup/Avalonia.Markup.Xaml/Styling/MergeResourceInclude.cs create mode 100644 tests/Avalonia.Markup.Xaml.UnitTests/Xaml/MergeResourceIncludeTests.cs diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml index 9d2a994e5b..2ccf20d460 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml @@ -3,10 +3,6 @@ xmlns:controls="using:Avalonia.Controls" xmlns:primitives="using:Avalonia.Controls.Primitives"> - - - - diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml index 85f4e417e6..2cc8a1d38a 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml @@ -50,13 +50,13 @@ - - - + + + - - + + diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorPicker.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorPicker.xaml index 626ddd4b43..b82d36a288 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorPicker.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorPicker.xaml @@ -3,10 +3,6 @@ xmlns:controls="using:Avalonia.Controls" xmlns:primitives="using:Avalonia.Controls.Primitives"> - - - - diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Simple/Simple.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Simple/Simple.xaml index 7aefa23706..8c4dfa9c87 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Simple/Simple.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Simple/Simple.xaml @@ -50,13 +50,13 @@ - - - + + + - - + + diff --git a/src/Avalonia.Themes.Fluent/Accents/Base.xaml b/src/Avalonia.Themes.Fluent/Accents/Base.xaml index 259d107b5c..479bcd8531 100644 --- a/src/Avalonia.Themes.Fluent/Accents/Base.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/Base.xaml @@ -22,6 +22,7 @@ 10,6,6,5 20 20 + 8,5,8,6 3 diff --git a/src/Avalonia.Themes.Fluent/Controls/Button.xaml b/src/Avalonia.Themes.Fluent/Controls/Button.xaml index 7828fd52ed..126f2c22e0 100644 --- a/src/Avalonia.Themes.Fluent/Controls/Button.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/Button.xaml @@ -8,8 +8,6 @@ - - 8,5,8,6 diff --git a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml index a029be6b8d..2733365479 100644 --- a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml @@ -4,70 +4,70 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + diff --git a/src/Avalonia.Themes.Fluent/Controls/RepeatButton.xaml b/src/Avalonia.Themes.Fluent/Controls/RepeatButton.xaml index a54187104b..fd04c85fed 100644 --- a/src/Avalonia.Themes.Fluent/Controls/RepeatButton.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/RepeatButton.xaml @@ -10,8 +10,6 @@ - 8,5,8,6 - diff --git a/src/Avalonia.Themes.Fluent/Controls/ToggleButton.xaml b/src/Avalonia.Themes.Fluent/Controls/ToggleButton.xaml index 7a46f21534..da2021790a 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ToggleButton.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ToggleButton.xaml @@ -10,8 +10,6 @@ - 8,5,8,6 - diff --git a/src/Avalonia.Themes.Fluent/FluentTheme.xaml b/src/Avalonia.Themes.Fluent/FluentTheme.xaml index d8f8267fe5..44ca60e2fa 100644 --- a/src/Avalonia.Themes.Fluent/FluentTheme.xaml +++ b/src/Avalonia.Themes.Fluent/FluentTheme.xaml @@ -4,8 +4,8 @@ - - + + diff --git a/src/Avalonia.Themes.Fluent/FluentTheme.xaml.cs b/src/Avalonia.Themes.Fluent/FluentTheme.xaml.cs index 728e81b198..80460e1bde 100644 --- a/src/Avalonia.Themes.Fluent/FluentTheme.xaml.cs +++ b/src/Avalonia.Themes.Fluent/FluentTheme.xaml.cs @@ -97,15 +97,15 @@ namespace Avalonia.Themes.Fluent var themeVariantResource1 = Mode == FluentThemeMode.Dark ? _baseDark : _baseLight; var themeVariantResource2 = Mode == FluentThemeMode.Dark ? _fluentDark : _fluentLight; var dict = Resources.MergedDictionaries; - if (dict.Count == 2) + if (dict.Count == 0) { - dict.Insert(1, themeVariantResource1); + dict.Add(themeVariantResource1); dict.Add(themeVariantResource2); } else { - dict[1] = themeVariantResource1; - dict[3] = themeVariantResource2; + dict[0] = themeVariantResource1; + dict[1] = themeVariantResource2; } } diff --git a/src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml b/src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml index 4aefa0136c..2d7fdcdd50 100644 --- a/src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml +++ b/src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml @@ -3,68 +3,68 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Simple/SimpleTheme.xaml b/src/Avalonia.Themes.Simple/SimpleTheme.xaml index fe296bd288..5b0cae7fd2 100644 --- a/src/Avalonia.Themes.Simple/SimpleTheme.xaml +++ b/src/Avalonia.Themes.Simple/SimpleTheme.xaml @@ -4,7 +4,7 @@ - + diff --git a/src/Avalonia.Themes.Simple/SimpleTheme.xaml.cs b/src/Avalonia.Themes.Simple/SimpleTheme.xaml.cs index af9d305043..e452646eab 100644 --- a/src/Avalonia.Themes.Simple/SimpleTheme.xaml.cs +++ b/src/Avalonia.Themes.Simple/SimpleTheme.xaml.cs @@ -56,13 +56,13 @@ namespace Avalonia.Themes.Simple { var themeVariantResource = Mode == SimpleThemeMode.Dark ? _simpleDark : _simpleLight; var dict = Resources.MergedDictionaries; - if (dict.Count == 1) + if (dict.Count == 0) { dict.Add(themeVariantResource); } else { - dict[1] = themeVariantResource; + dict[0] = themeVariantResource; } } } diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/AvaloniaRuntimeXamlLoader.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/AvaloniaRuntimeXamlLoader.cs index b4c951fc5e..6f6420f66d 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/AvaloniaRuntimeXamlLoader.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/AvaloniaRuntimeXamlLoader.cs @@ -56,8 +56,8 @@ namespace Avalonia.Markup.Xaml /// /// Collection of documents. /// Xaml loader configuration. - /// The loaded objects per each input document. - public static IReadOnlyList LoadGroup(IReadOnlyCollection documents, RuntimeXamlLoaderConfiguration? configuration = null) + /// The loaded objects per each input document. If document was removed, the element by index is null. + public static IReadOnlyList LoadGroup(IReadOnlyCollection documents, RuntimeXamlLoaderConfiguration? configuration = null) => AvaloniaXamlIlRuntimeCompiler.LoadGroup(documents, configuration ?? new RuntimeXamlLoaderConfiguration()); /// diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs index b8350c3f11..aaaee39b0d 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs @@ -85,6 +85,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions GroupTransformers = new() { + new XamlMergeResourceGroupTransformer(), new AvaloniaXamlIncludeTransformer() }; } diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/IXamlAstGroupTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/IXamlAstGroupTransformer.cs index 32bf37431f..eeb5293325 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/IXamlAstGroupTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/IXamlAstGroupTransformer.cs @@ -18,7 +18,7 @@ internal class AstGroupTransformationContext : AstTransformationContext public IXamlDocumentResource CurrentDocument { get; set; } public IReadOnlyCollection Documents { get; } - + public new IXamlAstNode ParseError(string message, IXamlAstNode node) => Error(node, new XamlDocumentParseException(CurrentDocument?.FileSource?.FilePath, message, node)); diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlIncludeGroupTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlIncludeGroupTransformer.cs index cc29d5ccb5..813c135dc6 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlIncludeGroupTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlIncludeGroupTransformer.cs @@ -15,6 +15,7 @@ using XamlX.TypeSystem; namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.GroupTransformers; +#nullable enable internal class AvaloniaXamlIncludeTransformer : IXamlAstGroupTransformer { public IXamlAstNode Transform(AstGroupTransformationContext context, IXamlAstNode node) @@ -34,40 +35,26 @@ internal class AvaloniaXamlIncludeTransformer : IXamlAstGroupTransformer { throw new InvalidOperationException($"\"{nodeTypeName}\".Loaded property is expected to be defined"); } - + if (valueNode.Manipulation is not XamlObjectInitializationNode { Manipulation: XamlPropertyAssignmentNode { Property: { Name: "Source" } } sourceProperty }) { - return context.ParseError($"Source property must be set on the \"{nodeTypeName}\" node.", node); + throw new XamlDocumentParseException(context.CurrentDocument, + $"Source property must be set on the \"{nodeTypeName}\" node.", valueNode); } - // We expect that AvaloniaXamlIlLanguageParseIntrinsics has already parsed the Uri and created node like: `new Uri(assetPath, uriKind)`. - if (sourceProperty.Values.OfType().FirstOrDefault() is not { } sourceUriNode - || sourceUriNode.Type.GetClrType() != context.GetAvaloniaTypes().Uri - || sourceUriNode.Arguments.FirstOrDefault() is not XamlConstantNode { Constant: string originalAssetPath } - || sourceUriNode.Arguments.Skip(1).FirstOrDefault() is not XamlConstantNode { Constant: int uriKind }) + var (assetPathUri, sourceUriNode) = ResolveSourceFromXamlInclude(context, nodeTypeName, sourceProperty, false); + if (assetPathUri is null) { - // TODO: make it a compiler warning - // Source value can be set with markup extension instead of the Uri object node, we don't support it here yet. return node; } - - var uriPath = new Uri(originalAssetPath, (UriKind)uriKind); - if (!uriPath.IsAbsoluteUri) + else { - var baseUrl = context.CurrentDocument.Uri ?? throw new InvalidOperationException("CurrentDocument URI is null."); - uriPath = new Uri(new Uri(baseUrl, UriKind.Absolute), uriPath); - } - else if (!uriPath.Scheme.Equals("avares", StringComparison.CurrentCultureIgnoreCase)) - { - return context.ParseError( - $"\"{nodeTypeName}.Source\" supports only \"avares://\" absolute or relative uri.", - sourceUriNode, node); + sourceUriNode ??= valueNode; } - var assetPathUri = Uri.UnescapeDataString(uriPath.AbsoluteUri); var assetPath = assetPathUri.Replace("avares://", ""); var assemblyNameSeparator = assetPath.IndexOf('/'); var assembly = assetPath.Substring(0, assemblyNameSeparator); @@ -119,7 +106,7 @@ internal class AvaloniaXamlIncludeTransformer : IXamlAstGroupTransformer $"Unable to resolve XAML resource \"{assetPathUri}\" in the \"{assembly}\" assembly.", sourceUriNode, node); } - + private static IXamlAstNode FromType(AstTransformationContext context, IXamlType type, IXamlAstNode li, IXamlType expectedLoadedType, IXamlAstNode fallbackNode, string assetPathUri, string assembly) { @@ -151,7 +138,49 @@ internal class AvaloniaXamlIncludeTransformer : IXamlAstGroupTransformer new[] { new NewServiceProviderNode(sp, li) }); } - internal class NewServiceProviderNode : XamlAstNode, IXamlAstValueNode,IXamlAstNodeNeedsParentStack, + internal static (string?, IXamlAstNode?) ResolveSourceFromXamlInclude( + AstGroupTransformationContext context, string nodeTypeName, XamlPropertyAssignmentNode sourceProperty, + bool strictSourceValueType) + { + // We expect that AvaloniaXamlIlLanguageParseIntrinsics has already parsed the Uri and created node like: `new Uri(assetPath, uriKind)`. + if (sourceProperty.Values.OfType().FirstOrDefault() is not { } sourceUriNode + || sourceUriNode.Type.GetClrType() != context.GetAvaloniaTypes().Uri + || sourceUriNode.Arguments.FirstOrDefault() is not XamlConstantNode { Constant: string originalAssetPath } + || sourceUriNode.Arguments.Skip(1).FirstOrDefault() is not XamlConstantNode { Constant: int uriKind }) + { + // Source value can be set with markup extension instead of the Uri object node, we don't support it here yet. + var anyPropValue = sourceProperty.Values.FirstOrDefault(); + if (strictSourceValueType) + { + context.Error(anyPropValue, + new XamlDocumentParseException(context.CurrentDocument, + $"\"{nodeTypeName}.Source\" supports only \"avares://\" absolute or relative uri.", anyPropValue)); + } + else + { + // TODO: make it a compiler warning + } + return (null, anyPropValue); + } + + var uriPath = new Uri(originalAssetPath, (UriKind)uriKind); + if (!uriPath.IsAbsoluteUri) + { + var baseUrl = context.CurrentDocument.Uri ?? throw new InvalidOperationException("CurrentDocument URI is null."); + uriPath = new Uri(new Uri(baseUrl, UriKind.Absolute), uriPath); + } + else if (!uriPath.Scheme.Equals("avares", StringComparison.CurrentCultureIgnoreCase)) + { + context.Error(sourceUriNode, + new XamlDocumentParseException(context.CurrentDocument, + $"\"{nodeTypeName}.Source\" supports only \"avares://\" absolute or relative uri.", sourceUriNode)); + return (null, sourceUriNode); + } + + return (Uri.UnescapeDataString(uriPath.AbsoluteUri), sourceUriNode); + } + + private class NewServiceProviderNode : XamlAstNode, IXamlAstValueNode,IXamlAstNodeNeedsParentStack, IXamlAstEmitableNode { public NewServiceProviderNode(IXamlType type, IXamlLineInfo lineInfo) : base(lineInfo) diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlMergeResourceGroupTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlMergeResourceGroupTransformer.cs new file mode 100644 index 0000000000..8c83c74248 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlMergeResourceGroupTransformer.cs @@ -0,0 +1,113 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers; +using XamlX.Ast; +using XamlX.IL.Emitters; + +namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.GroupTransformers; +#nullable enable + +internal class XamlMergeResourceGroupTransformer : IXamlAstGroupTransformer +{ + public IXamlAstNode Transform(AstGroupTransformationContext context, IXamlAstNode node) + { + var resourceDictionaryType = context.GetAvaloniaTypes().ResourceDictionary; + if (node is not XamlObjectInitializationNode resourceDictionaryNode + || resourceDictionaryNode.Type != resourceDictionaryType + || resourceDictionaryNode.Manipulation is not XamlManipulationGroupNode resourceDictionaryManipulation) + { + return node; + } + + var mergeResourceIncludeType = context.GetAvaloniaTypes().MergeResourceInclude; + var mergeSourceNodes = new List(); + var hasAnyNonMergedResource = false; + foreach (var manipulationNode in resourceDictionaryManipulation.Children.ToArray()) + { + void ProcessXamlPropertyAssignmentNode(XamlManipulationGroupNode parent, XamlPropertyAssignmentNode assignmentNode) + { + if (assignmentNode.Property.Name == "MergedDictionaries" + && assignmentNode.Values.FirstOrDefault() is XamlValueWithManipulationNode valueNode) + { + if (valueNode.Type.GetClrType() == mergeResourceIncludeType) + { + if (valueNode.Manipulation is XamlObjectInitializationNode objectInitialization + && objectInitialization.Manipulation is XamlPropertyAssignmentNode sourceAssignmentNode) + { + parent.Children.Remove(assignmentNode); + mergeSourceNodes.Add(sourceAssignmentNode); + } + else + { + throw new XamlDocumentParseException(context.CurrentDocument, + "Invalid MergeResourceInclude node found. Make sure that Source property is set.", + valueNode); + } + } + else + { + hasAnyNonMergedResource = true; + } + + if (hasAnyNonMergedResource && mergeSourceNodes.Any()) + { + throw new XamlDocumentParseException(context.CurrentDocument, + "Mix of MergeResourceInclude and other dictionaries inside of the ResourceDictionary.MergedDictionaries is not allowed", + valueNode); + } + } + } + + if (manipulationNode is XamlPropertyAssignmentNode singleValueAssignment) + { + ProcessXamlPropertyAssignmentNode(resourceDictionaryManipulation, singleValueAssignment); + } + else if (manipulationNode is XamlManipulationGroupNode groupNodeValues) + { + foreach (var groupNodeValue in groupNodeValues.Children.OfType().ToArray()) + { + ProcessXamlPropertyAssignmentNode(groupNodeValues, groupNodeValue); + } + } + } + + var manipulationGroup = new XamlManipulationGroupNode(node, new List()); + foreach (var sourceNode in mergeSourceNodes) + { + var (originalAssetPath, propertyNode) = + AvaloniaXamlIncludeTransformer.ResolveSourceFromXamlInclude(context, "MergeResourceInclude", sourceNode, true); + if (originalAssetPath is null) + { + return node; + } + + var targetDocument = context.Documents.FirstOrDefault(d => + string.Equals(d.Uri, originalAssetPath, StringComparison.InvariantCultureIgnoreCase)) + ?.XamlDocument.Root as XamlValueWithManipulationNode; + if (targetDocument is null) + { + return context.ParseError( + $"Node MergeResourceInclude is unable to resolve \"{originalAssetPath}\" path.", propertyNode, node); + } + + var singleRootObject = ((XamlManipulationGroupNode)targetDocument.Manipulation) + .Children.OfType().Single(); + if (singleRootObject.Type != resourceDictionaryType) + { + return context.ParseError( + $"MergeResourceInclude can only include another ResourceDictionary", propertyNode, node); + } + + manipulationGroup.Children.Add(singleRootObject.Manipulation); + } + + if (manipulationGroup.Children.Any()) + { + // MergedDictionaries are read first, so we need ot inject our merged values in the beginning. + resourceDictionaryManipulation.Children.Insert(0, manipulationGroup); + } + + return node; + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs index 87a037c16a..5753a1008c 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs @@ -104,6 +104,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers public IXamlType IStyle { get; } public IXamlType StyleInclude { get; } public IXamlType ResourceInclude { get; } + public IXamlType MergeResourceInclude { get; } public IXamlType IResourceDictionary { get; } public IXamlType ResourceDictionary { get; } public IXamlMethod ResourceDictionaryDeferredAdd { get; } @@ -236,6 +237,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers IStyle = cfg.TypeSystem.GetType("Avalonia.Styling.IStyle"); StyleInclude = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.Styling.StyleInclude"); ResourceInclude = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.Styling.ResourceInclude"); + MergeResourceInclude = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.Styling.MergeResourceInclude"); IResourceDictionary = cfg.TypeSystem.GetType("Avalonia.Controls.IResourceDictionary"); ResourceDictionary = cfg.TypeSystem.GetType("Avalonia.Controls.ResourceDictionary"); ResourceDictionaryDeferredAdd = ResourceDictionary.FindMethod("AddDeferred", XamlIlTypes.Void, true, XamlIlTypes.Object, diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlDocumentParseException.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlDocumentParseException.cs index d031a6086b..0532287a67 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlDocumentParseException.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlDocumentParseException.cs @@ -18,4 +18,9 @@ internal class XamlDocumentParseException : XamlParseException { FilePath = path; } + + public XamlDocumentParseException(IXamlDocumentResource document, string message, IXamlLineInfo lineInfo) + : this(document.FileSource?.FilePath, message, lineInfo) + { + } } diff --git a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj index 0ab00007e7..4c3aaa4ec0 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj +++ b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj @@ -44,6 +44,7 @@ + diff --git a/src/Markup/Avalonia.Markup.Xaml/Styling/MergeResourceInclude.cs b/src/Markup/Avalonia.Markup.Xaml/Styling/MergeResourceInclude.cs new file mode 100644 index 0000000000..c81a3c1416 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/Styling/MergeResourceInclude.cs @@ -0,0 +1,69 @@ +using System; +using Avalonia.Controls; + +namespace Avalonia.Markup.Xaml.Styling; + +public class MergeResourceInclude : IResourceProvider +{ + private readonly IServiceProvider _serviceProvider; + private readonly Uri? _baseUri; + private IResourceDictionary? _loaded; + private bool _isLoading; + + /// + /// Initializes a new instance of the class. + /// + /// The XAML service provider. + public MergeResourceInclude(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + _baseUri = serviceProvider.GetContextBaseUri(); + } + + /// + /// Gets the loaded resource dictionary. + /// + public IResourceDictionary Loaded + { + get + { + if (_loaded == null) + { + _isLoading = true; + _loaded = (IResourceDictionary)AvaloniaXamlLoader.Load(_serviceProvider, Source, _baseUri); + _isLoading = false; + } + + return _loaded; + } + } + + public IResourceHost? Owner => Loaded.Owner; + + /// + /// Gets or sets the source URL. + /// + public Uri? Source { get; set; } + + bool IResourceNode.HasResources => Loaded.HasResources; + + public event EventHandler? OwnerChanged + { + add => Loaded.OwnerChanged += value; + remove => Loaded.OwnerChanged -= value; + } + + bool IResourceNode.TryGetResource(object key, out object? value) + { + if (!_isLoading) + { + return Loaded.TryGetResource(key, out value); + } + + value = null; + return false; + } + + void IResourceProvider.AddOwner(IResourceHost owner) => Loaded.AddOwner(owner); + void IResourceProvider.RemoveOwner(IResourceHost owner) => Loaded.RemoveOwner(owner); +} diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/MergeResourceIncludeTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/MergeResourceIncludeTests.cs new file mode 100644 index 0000000000..520abee59a --- /dev/null +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/MergeResourceIncludeTests.cs @@ -0,0 +1,124 @@ +using System; +using System.Xml; +using Avalonia.Controls; +using Avalonia.Media; +using Avalonia.Styling; +using Xunit; + +namespace Avalonia.Markup.Xaml.UnitTests.Xaml; + +public class MergeResourceIncludeTests +{ + [Fact] + public void MergeResourceInclude_Works_With_Single_Resource() + { + var documents = new[] + { + new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Resources.xaml"), @" + + Red +"), + new RuntimeXamlLoaderDocument(@" + + + + Blue + + + + + +") + }; + + var objects = AvaloniaRuntimeXamlLoader.LoadGroup(documents); + var contentControl = Assert.IsType(objects[1]); + + var resources = Assert.IsType(contentControl.Resources); + Assert.Empty(resources.MergedDictionaries); + + var initialResource = (ISolidColorBrush)resources["brush1"]!; + Assert.Equal(Colors.Blue, initialResource.Color); + + var mergedResource = (ISolidColorBrush)resources["brush2"]!; + Assert.Equal(Colors.Red, mergedResource.Color); + } + + [Fact] + public void Mixing_MergeResourceInclude_And_ResourceInclude_Is_Not_Allowed() + { + var documents = new[] + { + new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Resources1.xaml"), @" + + Red +"), + new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Resources2.xaml"), @" + + Blue +"), + new RuntimeXamlLoaderDocument(@" + + + + + +") + }; + + Assert.ThrowsAny(() => AvaloniaRuntimeXamlLoader.LoadGroup(documents)); + } + + [Fact] + public void MergeResourceInclude_Works_With_Multiple_Resources() + { + var documents = new[] + { + new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Resources1.xaml"), @" + + Red + Blue +"), + new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Resources2.xaml"), @" + + Yellow + + + +"), + new RuntimeXamlLoaderDocument(@" + + + + + + Black + White +"), + new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Resources1_2.xaml"), @" + + Green +"), + }; + + var objects = AvaloniaRuntimeXamlLoader.LoadGroup(documents); + var resources = Assert.IsType(objects[2]); + Assert.Empty(resources.MergedDictionaries); + + Assert.Equal(Colors.Red, ((ISolidColorBrush)resources["brush1"]!).Color); + Assert.Equal(Colors.Blue, ((ISolidColorBrush)resources["brush2"]!).Color); + Assert.Equal(Colors.Green, ((ISolidColorBrush)resources["brush3"]!).Color); + Assert.Equal(Colors.Yellow, ((ISolidColorBrush)resources["brush4"]!).Color); + Assert.Equal(Colors.Black, ((ISolidColorBrush)resources["brush5"]!).Color); + Assert.Equal(Colors.White, ((ISolidColorBrush)resources["brush6"]!).Color); + } +} From c03187ff7ce32d8b64b5b823bf16382d6ed6f013 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Thu, 1 Dec 2022 01:14:13 -0500 Subject: [PATCH 12/87] Add a new benchmark --- .../Themes/ThemeBenchmark.cs | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/Avalonia.Benchmarks/Themes/ThemeBenchmark.cs b/tests/Avalonia.Benchmarks/Themes/ThemeBenchmark.cs index e82576c7d9..70636d1fe6 100644 --- a/tests/Avalonia.Benchmarks/Themes/ThemeBenchmark.cs +++ b/tests/Avalonia.Benchmarks/Themes/ThemeBenchmark.cs @@ -16,6 +16,8 @@ namespace Avalonia.Benchmarks.Themes public class ThemeBenchmark : IDisposable { private IDisposable _app; + private readonly FluentTheme _reusableFluentTheme = new FluentTheme(); + private readonly SimpleTheme _reusableSimpleTheme = new SimpleTheme(); public ThemeBenchmark() { @@ -49,6 +51,26 @@ namespace Avalonia.Benchmarks.Themes }; return ((IResourceHost)UnitTestApplication.Current).TryGetResource("ThemeAccentColor", out _); } + + [Benchmark] + [Arguments(typeof(Button))] + [Arguments(typeof(TextBox))] + [Arguments(typeof(DatePicker))] + public object FindFluentControlTheme(Type type) + { + _reusableFluentTheme.TryGetResource(type, out var theme); + return theme; + } + + [Benchmark] + [Arguments(typeof(Button))] + [Arguments(typeof(TextBox))] + [Arguments(typeof(DatePicker))] + public object FindSimpleControlTheme(Type type) + { + _reusableSimpleTheme.TryGetResource(type, out var theme); + return theme; + } public void Dispose() { From 1caf79aa93158297629be9a82e3ef1140525ccd2 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Thu, 1 Dec 2022 01:26:51 -0500 Subject: [PATCH 13/87] Fix build --- .../Avalonia.Markup.Xaml/Styling/MergeResourceInclude.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Markup/Avalonia.Markup.Xaml/Styling/MergeResourceInclude.cs b/src/Markup/Avalonia.Markup.Xaml/Styling/MergeResourceInclude.cs index c81a3c1416..ed7bb98833 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Styling/MergeResourceInclude.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Styling/MergeResourceInclude.cs @@ -30,7 +30,8 @@ public class MergeResourceInclude : IResourceProvider if (_loaded == null) { _isLoading = true; - _loaded = (IResourceDictionary)AvaloniaXamlLoader.Load(_serviceProvider, Source, _baseUri); + var source = Source ?? throw new InvalidOperationException("MergeResourceInclude.Source must be set."); + _loaded = (IResourceDictionary)AvaloniaXamlLoader.Load(source, _baseUri); _isLoading = false; } From 1af7eb6f3a93b5ed3b109abd9041409b041ebd9e Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 1 Dec 2022 17:17:07 +0000 Subject: [PATCH 14/87] fix window showdialog, and show raising exceptions when incorrect behavior is used. --- src/Avalonia.Controls/Window.cs | 99 ++++++++++--------- .../WindowTests.cs | 8 +- 2 files changed, 56 insertions(+), 51 deletions(-) diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 1ff08f0169..6b936e495c 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -641,47 +641,64 @@ namespace Avalonia.Controls } /// - /// Shows the window as a child of . + /// Shows the window as a child of . /// - /// Window that will be a parent of the shown window. + /// Window that will be the owner of the shown window. /// /// The window has already been closed. /// - public void Show(Window parent) + public void Show(Window owner) { - if (parent is null) + if (owner is null) { - throw new ArgumentNullException(nameof(parent), "Showing a child window requires valid parent."); + throw new ArgumentNullException(nameof(owner), "Showing a child window requires valid parent."); } - ShowCore(parent); + ShowCore(owner); } - private void ShowCore(Window? parent) + private void EnsureStateBeforeShow() + { + if (PlatformImpl == null) + { + throw new InvalidOperationException("Cannot re-show a closed window."); + } + + if (_shown) + { + throw new InvalidOperationException("The window is already being shown."); + } + } + + private void EnsureParentStateBeforeShow(Window owner) + { + if (owner.PlatformImpl == null) + { + throw new InvalidOperationException("Cannot show a window with a closed owner."); + } + + if (owner == this) + { + throw new InvalidOperationException("A Window cannot be its own owner."); + } + + if (!owner.IsVisible) + { + throw new InvalidOperationException("Cannot show window with non-visible owner."); + } + } + + private void ShowCore(Window? owner) { try { IgnoreVisibilityChange = true; - - if (PlatformImpl == null) - { - throw new InvalidOperationException("Cannot re-show a closed window."); - } - if (parent != null) + EnsureStateBeforeShow(); + + if (owner != null) { - if (parent.PlatformImpl == null) - { - throw new InvalidOperationException("Cannot show a window with a closed parent."); - } - else if (parent == this) - { - throw new InvalidOperationException("A Window cannot be its own parent."); - } - else if (!parent.IsVisible) - { - throw new InvalidOperationException("Cannot show window with non-visible parent."); - } + EnsureParentStateBeforeShow(owner); } if (_shown) @@ -707,15 +724,15 @@ namespace Avalonia.Controls LayoutManager.ExecuteInitialLayoutPass(); - if (PlatformImpl != null && parent?.PlatformImpl is not null) + if (PlatformImpl != null && owner?.PlatformImpl is not null) { - PlatformImpl.SetParent(parent.PlatformImpl); + PlatformImpl.SetParent(owner.PlatformImpl); } - Owner = parent; - parent?.AddChild(this, false); + Owner = owner; + owner?.AddChild(this, false); - SetWindowStartupLocation(parent?.PlatformImpl); + SetWindowStartupLocation(owner?.PlatformImpl); PlatformImpl?.Show(ShowActivated, false); Renderer?.Start(); @@ -758,26 +775,14 @@ namespace Avalonia.Controls { IgnoreVisibilityChange = true; + EnsureStateBeforeShow(); + if (owner == null) { throw new ArgumentNullException(nameof(owner)); } - else if (owner.PlatformImpl == null) - { - throw new InvalidOperationException("Cannot show a window with a closed owner."); - } - else if (owner == this) - { - throw new InvalidOperationException("A Window cannot be its own owner."); - } - else if (IsVisible) - { - throw new InvalidOperationException("The window is already being shown."); - } - else if (!owner.IsVisible) - { - throw new InvalidOperationException("Cannot show window with non-visible parent."); - } + + EnsureParentStateBeforeShow(owner); RaiseEvent(new RoutedEventArgs(WindowOpenedEvent)); @@ -800,7 +805,7 @@ namespace Avalonia.Controls var result = new TaskCompletionSource(); - PlatformImpl?.SetParent(owner.PlatformImpl); + PlatformImpl?.SetParent(owner.PlatformImpl!); Owner = owner; owner.AddChild(this, true); diff --git a/tests/Avalonia.Controls.UnitTests/WindowTests.cs b/tests/Avalonia.Controls.UnitTests/WindowTests.cs index f73c3ac215..e544ca62bb 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowTests.cs @@ -403,7 +403,7 @@ namespace Avalonia.Controls.UnitTests parent.Close(); var ex = Assert.Throws(() => target.Show(parent)); - Assert.Equal("Cannot show a window with a closed parent.", ex.Message); + Assert.Equal("Cannot show a window with a closed owner.", ex.Message); } } @@ -431,7 +431,7 @@ namespace Avalonia.Controls.UnitTests var target = new Window(); var ex = Assert.Throws(() => target.Show(parent)); - Assert.Equal("Cannot show window with non-visible parent.", ex.Message); + Assert.Equal("Cannot show window with non-visible owner.", ex.Message); } } @@ -444,7 +444,7 @@ namespace Avalonia.Controls.UnitTests var target = new Window(); var ex = await Assert.ThrowsAsync(() => target.ShowDialog(parent)); - Assert.Equal("Cannot show window with non-visible parent.", ex.Message); + Assert.Equal("Cannot show window with non-visible owner.", ex.Message); } } @@ -456,7 +456,7 @@ namespace Avalonia.Controls.UnitTests var target = new Window(); var ex = Assert.Throws(() => target.Show(target)); - Assert.Equal("A Window cannot be its own parent.", ex.Message); + Assert.Equal("A Window cannot be its own owner.", ex.Message); } } From e7039842f5c38900a4dd3253207b99b95465c010 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 1 Dec 2022 17:34:07 +0000 Subject: [PATCH 15/87] Show can be called multiple times but ignored. --- src/Avalonia.Controls/Window.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 6b936e495c..729bbaa4b3 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -663,11 +663,6 @@ namespace Avalonia.Controls { throw new InvalidOperationException("Cannot re-show a closed window."); } - - if (_shown) - { - throw new InvalidOperationException("The window is already being shown."); - } } private void EnsureParentStateBeforeShow(Window owner) @@ -783,6 +778,11 @@ namespace Avalonia.Controls } EnsureParentStateBeforeShow(owner); + + if (_shown) + { + throw new InvalidOperationException("The window is already being shown."); + } RaiseEvent(new RoutedEventArgs(WindowOpenedEvent)); From 8fe2c5b7cb3345867ece264aef57610d942495e6 Mon Sep 17 00:00:00 2001 From: robloo Date: Thu, 1 Dec 2022 19:25:49 -0500 Subject: [PATCH 16/87] Standardize IsDefault member in CornerRadius --- src/Avalonia.Base/CornerRadius.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Base/CornerRadius.cs b/src/Avalonia.Base/CornerRadius.cs index 893f7c4565..67ef79f752 100644 --- a/src/Avalonia.Base/CornerRadius.cs +++ b/src/Avalonia.Base/CornerRadius.cs @@ -61,9 +61,13 @@ namespace Avalonia public double BottomLeft { get; } /// - /// Gets a value indicating whether all corner radii are set to 0. + /// Gets a value indicating whether the instance has default values (all corner radii are set to 0). /// - public bool IsEmpty => TopLeft.Equals(0) && IsUniform; + public bool IsDefault => TopLeft == 0 && TopRight == 0 && BottomLeft == 0 && BottomRight == 0; + + /// + [Obsolete("Use IsDefault instead.")] + public bool IsEmpty => IsDefault; /// /// Gets a value indicating whether all corner radii are equal. @@ -79,7 +83,6 @@ namespace Avalonia { // ReSharper disable CompareOfFloatsByEqualityOperator return TopLeft == other.TopLeft && - TopRight == other.TopRight && BottomRight == other.BottomRight && BottomLeft == other.BottomLeft; From 9634a2cccf3419771ad1bbe0a35d998285075cf6 Mon Sep 17 00:00:00 2001 From: robloo Date: Thu, 1 Dec 2022 19:26:19 -0500 Subject: [PATCH 17/87] Standardize IsDefault member in BoxShadow --- src/Avalonia.Base/Media/BoxShadow.cs | 11 +++++++++-- src/Avalonia.Base/Media/BoxShadows.cs | 4 ++-- src/Skia/Avalonia.Skia/DrawingContextImpl.cs | 4 ++-- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Base/Media/BoxShadow.cs b/src/Avalonia.Base/Media/BoxShadow.cs index cc97d89cfc..a9f073df67 100644 --- a/src/Avalonia.Base/Media/BoxShadow.cs +++ b/src/Avalonia.Base/Media/BoxShadow.cs @@ -45,7 +45,14 @@ namespace Avalonia.Media } } - public bool IsEmpty => OffsetX == 0 && OffsetY == 0 && Blur == 0 && Spread == 0; + /// + /// Gets a value indicating whether the instance has default values. + /// + public bool IsDefault => OffsetX == 0 && OffsetY == 0 && Blur == 0 && Spread == 0; + + /// + [Obsolete("Use IsDefault instead.")] + public bool IsEmpty => IsDefault; private readonly static char[] s_Separator = new char[] { ' ', '\t' }; @@ -82,7 +89,7 @@ namespace Avalonia.Media { var sb = StringBuilderCache.Acquire(); - if (IsEmpty) + if (IsDefault) { return "none"; } diff --git a/src/Avalonia.Base/Media/BoxShadows.cs b/src/Avalonia.Base/Media/BoxShadows.cs index ab2694389f..136f37a42b 100644 --- a/src/Avalonia.Base/Media/BoxShadows.cs +++ b/src/Avalonia.Base/Media/BoxShadows.cs @@ -21,7 +21,7 @@ namespace Avalonia.Media { _first = shadow; _list = null; - Count = _first.IsEmpty ? 0 : 1; + Count = _first.IsDefault ? 0 : 1; } public BoxShadows(BoxShadow first, BoxShadow[] rest) @@ -118,7 +118,7 @@ namespace Avalonia.Media get { foreach(var boxShadow in this) - if (!boxShadow.IsEmpty && boxShadow.IsInset) + if (!boxShadow.IsDefault && boxShadow.IsInset) return true; return false; } diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index 6dcafdcfd7..0a1bbe558b 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -362,7 +362,7 @@ namespace Avalonia.Skia foreach (var boxShadow in boxShadows) { - if (!boxShadow.IsEmpty && !boxShadow.IsInset) + if (!boxShadow.IsDefault && !boxShadow.IsInset) { using (var shadow = BoxShadowFilter.Create(_boxShadowPaint, boxShadow, _currentOpacity)) { @@ -418,7 +418,7 @@ namespace Avalonia.Skia foreach (var boxShadow in boxShadows) { - if (!boxShadow.IsEmpty && boxShadow.IsInset) + if (!boxShadow.IsDefault && boxShadow.IsInset) { using (var shadow = BoxShadowFilter.Create(_boxShadowPaint, boxShadow, _currentOpacity)) { From 06a6284ee48c3ed716ae8999145b9efe10aa1e44 Mon Sep 17 00:00:00 2001 From: robloo Date: Thu, 1 Dec 2022 19:39:38 -0500 Subject: [PATCH 18/87] Standardize IsDefault member in PixelRect --- src/Avalonia.Base/Media/Imaging/CroppedBitmap.cs | 2 +- src/Avalonia.Base/PixelRect.cs | 12 ++++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Base/Media/Imaging/CroppedBitmap.cs b/src/Avalonia.Base/Media/Imaging/CroppedBitmap.cs index 70f9fbf567..ac462aeb72 100644 --- a/src/Avalonia.Base/Media/Imaging/CroppedBitmap.cs +++ b/src/Avalonia.Base/Media/Imaging/CroppedBitmap.cs @@ -79,7 +79,7 @@ namespace Avalonia.Media.Imaging { if (Source is not IBitmap bmp) return Size.Empty; - if (SourceRect.IsEmpty) + if (SourceRect.IsDefault) return Source.Size; return SourceRect.Size.ToSizeWithDpi(bmp.Dpi); } diff --git a/src/Avalonia.Base/PixelRect.cs b/src/Avalonia.Base/PixelRect.cs index 855ba104ad..343bbccc9a 100644 --- a/src/Avalonia.Base/PixelRect.cs +++ b/src/Avalonia.Base/PixelRect.cs @@ -133,9 +133,13 @@ namespace Avalonia public PixelPoint Center => new PixelPoint(X + (Width / 2), Y + (Height / 2)); /// - /// Gets a value that indicates whether the rectangle is empty. + /// Gets a value indicating whether the instance has default values. /// - public bool IsEmpty => Width == 0 && Height == 0; + public bool IsDefault => Width == 0 && Height == 0; + + /// + [Obsolete("Use IsDefault instead.")] + public bool IsEmpty => IsDefault; /// /// Checks for equality between two s. @@ -290,11 +294,11 @@ namespace Avalonia /// The union. public PixelRect Union(PixelRect rect) { - if (IsEmpty) + if (IsDefault) { return rect; } - else if (rect.IsEmpty) + else if (rect.IsDefault) { return this; } From b8a1aaa329e10190afd31aae324c8cbf7b180f36 Mon Sep 17 00:00:00 2001 From: robloo Date: Thu, 1 Dec 2022 20:49:19 -0500 Subject: [PATCH 19/87] Standardize IsDefault member in Rect --- src/Avalonia.Base/Media/FormattedText.cs | 2 +- src/Avalonia.Base/Media/ImageDrawing.cs | 2 +- src/Avalonia.Base/PixelRect.cs | 2 +- src/Avalonia.Base/Rect.cs | 14 +++++++++----- .../Composition/Server/ServerCompositionTarget.cs | 6 +++--- .../Composition/Server/ServerCompositionVisual.cs | 6 +++--- src/Avalonia.Base/Rendering/DeferredRenderer.cs | 2 +- src/Avalonia.Base/Rendering/DirtyRects.cs | 2 +- .../DataGridCheckBoxColumn.cs | 4 ++-- src/Avalonia.Controls/NativeControlHost.cs | 2 +- .../PopupPositioning/ManagedPopupPositioner.cs | 2 +- src/Avalonia.Controls/Repeater/ViewportManager.cs | 4 ++-- 12 files changed, 26 insertions(+), 22 deletions(-) diff --git a/src/Avalonia.Base/Media/FormattedText.cs b/src/Avalonia.Base/Media/FormattedText.cs index 90b9755493..87dca16205 100644 --- a/src/Avalonia.Base/Media/FormattedText.cs +++ b/src/Avalonia.Base/Media/FormattedText.cs @@ -1383,7 +1383,7 @@ namespace Avalonia.Media } } - if (accumulatedBounds?.PlatformImpl == null || accumulatedBounds.PlatformImpl.Bounds.IsEmpty) + if (accumulatedBounds?.PlatformImpl == null || accumulatedBounds.PlatformImpl.Bounds.IsDefault) { return null; } diff --git a/src/Avalonia.Base/Media/ImageDrawing.cs b/src/Avalonia.Base/Media/ImageDrawing.cs index 82f97b52b4..d3e5c4841b 100644 --- a/src/Avalonia.Base/Media/ImageDrawing.cs +++ b/src/Avalonia.Base/Media/ImageDrawing.cs @@ -42,7 +42,7 @@ namespace Avalonia.Media var imageSource = ImageSource; var rect = Rect; - if (imageSource is object && !rect.IsEmpty) + if (imageSource is object && !rect.IsDefault) { context.DrawImage(imageSource, rect); } diff --git a/src/Avalonia.Base/PixelRect.cs b/src/Avalonia.Base/PixelRect.cs index 343bbccc9a..409a259182 100644 --- a/src/Avalonia.Base/PixelRect.cs +++ b/src/Avalonia.Base/PixelRect.cs @@ -133,7 +133,7 @@ namespace Avalonia public PixelPoint Center => new PixelPoint(X + (Width / 2), Y + (Height / 2)); /// - /// Gets a value indicating whether the instance has default values. + /// Gets a value indicating whether the instance has default values (the rectangle is empty). /// public bool IsDefault => Width == 0 && Height == 0; diff --git a/src/Avalonia.Base/Rect.cs b/src/Avalonia.Base/Rect.cs index a91b089a33..862e4caec1 100644 --- a/src/Avalonia.Base/Rect.cs +++ b/src/Avalonia.Base/Rect.cs @@ -169,12 +169,16 @@ namespace Avalonia public Point Center => new Point(_x + (_width / 2), _y + (_height / 2)); /// - /// Gets a value that indicates whether the rectangle is empty. + /// Gets a value indicating whether the instance has default values (the rectangle is empty). /// // ReSharper disable CompareOfFloatsByEqualityOperator - public bool IsEmpty => _width == 0 && _height == 0; + public bool IsDefault => _width == 0 && _height == 0; // ReSharper restore CompareOfFloatsByEqualityOperator + /// + [Obsolete("Use IsDefault instead.")] + public bool IsEmpty => IsDefault; + /// /// Checks for equality between two s. /// @@ -457,7 +461,7 @@ namespace Avalonia /// public Rect Normalize() { - Rect rect = this; + Rect rect = this; if(double.IsNaN(rect.Right) || double.IsNaN(rect.Bottom) || double.IsNaN(rect.X) || double.IsNaN(rect.Y) || @@ -493,11 +497,11 @@ namespace Avalonia /// The union. public Rect Union(Rect rect) { - if (IsEmpty) + if (IsDefault) { return rect; } - else if (rect.IsEmpty) + else if (rect.IsDefault) { return this; } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs index 5c1ac0312c..a8fcdc41cd 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs @@ -89,7 +89,7 @@ namespace Avalonia.Rendering.Composition.Server Compositor.UpdateServerTime(); - if(_dirtyRect.IsEmpty && !_redrawRequested) + if(_dirtyRect.IsDefault && !_redrawRequested) return; Revision++; @@ -117,7 +117,7 @@ namespace Avalonia.Rendering.Composition.Server _layerSize = layerSize; } - if (!_dirtyRect.IsEmpty) + if (!_dirtyRect.IsDefault) { var visualBrushHelper = new CompositorDrawingContextProxy.VisualBrushRenderer(); using (var context = _layer.CreateDrawingContext(visualBrushHelper)) @@ -179,7 +179,7 @@ namespace Avalonia.Rendering.Composition.Server public void AddDirtyRect(Rect rect) { - if(rect.IsEmpty) + if(rect.IsDefault) return; var snapped = SnapToDevicePixels(rect, Scaling); DebugEvents?.RectInvalidated(rect); diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs index d724e14298..5e6dcab209 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs @@ -37,7 +37,7 @@ namespace Avalonia.Rendering.Composition.Server return; currentTransformedClip = currentTransformedClip.Intersect(_combinedTransformedClipBounds); - if(currentTransformedClip.IsEmpty) + if(currentTransformedClip.IsDefault) return; Root!.RenderedVisuals++; @@ -139,7 +139,7 @@ namespace Avalonia.Rendering.Composition.Server if (ownBounds != _oldOwnContentBounds || positionChanged) { _oldOwnContentBounds = ownBounds; - if (ownBounds.IsEmpty) + if (ownBounds.IsDefault) TransformedOwnContentBounds = default; else TransformedOwnContentBounds = @@ -163,7 +163,7 @@ namespace Avalonia.Rendering.Composition.Server EffectiveOpacity = Opacity * (Parent?.EffectiveOpacity ?? 1); IsVisibleInFrame = _parent?.IsVisibleInFrame != false && Visible && EffectiveOpacity > 0.04 && !_isBackface && - !_combinedTransformedClipBounds.IsEmpty; + !_combinedTransformedClipBounds.IsDefault; if (wasVisible != IsVisibleInFrame || positionChanged) { diff --git a/src/Avalonia.Base/Rendering/DeferredRenderer.cs b/src/Avalonia.Base/Rendering/DeferredRenderer.cs index 971702269c..33820a0eac 100644 --- a/src/Avalonia.Base/Rendering/DeferredRenderer.cs +++ b/src/Avalonia.Base/Rendering/DeferredRenderer.cs @@ -420,7 +420,7 @@ namespace Avalonia.Rendering { clipBounds = node.ClipBounds.Intersect(clipBounds); - if (!clipBounds.IsEmpty && node.Opacity > 0) + if (!clipBounds.IsDefault && node.Opacity > 0) { var isLayerRoot = node.Visual == layer; diff --git a/src/Avalonia.Base/Rendering/DirtyRects.cs b/src/Avalonia.Base/Rendering/DirtyRects.cs index f4b6a6b6ce..e4238f8308 100644 --- a/src/Avalonia.Base/Rendering/DirtyRects.cs +++ b/src/Avalonia.Base/Rendering/DirtyRects.cs @@ -27,7 +27,7 @@ namespace Avalonia.Rendering /// public void Add(Rect rect) { - if (!rect.IsEmpty) + if (!rect.IsDefault) { for (var i = 0; i < _rects.Count; ++i) { diff --git a/src/Avalonia.Controls.DataGrid/DataGridCheckBoxColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridCheckBoxColumn.cs index 6b1796e50b..c308312398 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridCheckBoxColumn.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridCheckBoxColumn.cs @@ -192,14 +192,14 @@ namespace Avalonia.Controls void OnLayoutUpdated(object sender, EventArgs e) { - if(!editingCheckBox.Bounds.IsEmpty) + if(!editingCheckBox.Bounds.IsDefault) { editingCheckBox.LayoutUpdated -= OnLayoutUpdated; ProcessPointerArgs(); } } - if(editingCheckBox.Bounds.IsEmpty) + if(editingCheckBox.Bounds.IsDefault) { editingCheckBox.LayoutUpdated += OnLayoutUpdated; } diff --git a/src/Avalonia.Controls/NativeControlHost.cs b/src/Avalonia.Controls/NativeControlHost.cs index bcf0866129..18dc1b1264 100644 --- a/src/Avalonia.Controls/NativeControlHost.cs +++ b/src/Avalonia.Controls/NativeControlHost.cs @@ -145,7 +145,7 @@ namespace Avalonia.Controls if (IsEffectivelyVisible && bounds.HasValue) { - if (bounds.Value.IsEmpty) + if (bounds.Value.IsDefault) return false; _attachment?.ShowInBounds(bounds.Value); } diff --git a/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs b/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs index a80a60350e..62e5f37273 100644 --- a/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs +++ b/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs @@ -112,7 +112,7 @@ namespace Avalonia.Controls.Primitives.PopupPositioning ?? screens.FirstOrDefault(s => s.Bounds.Intersects(parentGeometry)) ?? screens.FirstOrDefault(); - if (targetScreen != null && targetScreen.WorkingArea.IsEmpty) + if (targetScreen != null && targetScreen.WorkingArea.IsDefault) { return targetScreen.Bounds; } diff --git a/src/Avalonia.Controls/Repeater/ViewportManager.cs b/src/Avalonia.Controls/Repeater/ViewportManager.cs index e24ed37f1e..10e3dd57a0 100644 --- a/src/Avalonia.Controls/Repeater/ViewportManager.cs +++ b/src/Avalonia.Controls/Repeater/ViewportManager.cs @@ -465,7 +465,7 @@ namespace Avalonia.Controls _pendingViewportShift = default; _unshiftableShift = default; - if (_visibleWindow.IsEmpty) + if (_visibleWindow.IsDefault) { // We got cleared. _layoutExtent = default; @@ -551,7 +551,7 @@ namespace Avalonia.Controls private void TryInvalidateMeasure() { // Don't invalidate measure if we have an invalid window. - if (!_visibleWindow.IsEmpty) + if (!_visibleWindow.IsDefault) { // We invalidate measure instead of just invalidating arrange because // we don't invalidate measure in UpdateViewport if the view is changing to From acfc66eeb196bf5008f56e7f08d70a6dbfde9443 Mon Sep 17 00:00:00 2001 From: robloo Date: Thu, 1 Dec 2022 20:50:46 -0500 Subject: [PATCH 20/87] Add DirtyRects.IsEmpty comment IsEmpty is allowed to stay here because it is an internal API. Empty also makes sense when working with collections. --- src/Avalonia.Base/Rendering/DirtyRects.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Avalonia.Base/Rendering/DirtyRects.cs b/src/Avalonia.Base/Rendering/DirtyRects.cs index e4238f8308..723fe400b3 100644 --- a/src/Avalonia.Base/Rendering/DirtyRects.cs +++ b/src/Avalonia.Base/Rendering/DirtyRects.cs @@ -10,6 +10,9 @@ namespace Avalonia.Rendering { private List _rects = new List(); + /// + /// Gets a value indicating whether the collection of dirty rectangles is empty. + /// public bool IsEmpty => _rects.Count == 0; /// From 210390243f9f2b346ac168b4598465860f78b0c6 Mon Sep 17 00:00:00 2001 From: robloo Date: Thu, 1 Dec 2022 20:54:21 -0500 Subject: [PATCH 21/87] Standardize IsDefault member in Thickness --- src/Avalonia.Base/Thickness.cs | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/Avalonia.Base/Thickness.cs b/src/Avalonia.Base/Thickness.cs index da3a98088f..beaf85c9d3 100644 --- a/src/Avalonia.Base/Thickness.cs +++ b/src/Avalonia.Base/Thickness.cs @@ -97,10 +97,9 @@ namespace Avalonia /// public double Bottom => _bottom; - /// - /// Gets a value indicating whether all sides are set to 0. - /// - public bool IsEmpty => Left.Equals(0) && IsUniform; + /// + [Obsolete("Use IsDefault instead.")] + public bool IsEmpty => IsDefault; /// /// Gets a value indicating whether all sides are equal. @@ -292,15 +291,13 @@ namespace Avalonia left = this._left; top = this._top; right = this._right; - bottom = this._bottom; + bottom = this._bottom; } /// - /// Gets a value indicating whether the left, top, right and bottom thickness values are zero. + /// Gets a value indicating whether the instance has default values + /// (the left, top, right and bottom values are zero). /// - public bool IsDefault - { - get { return (_left == 0) && (_top == 0) && (_right == 0) && (_bottom == 0); } - } + public bool IsDefault => (_left == 0) && (_top == 0) && (_right == 0) && (_bottom == 0); } } From 5a743ad4c85067a22ee3d0f03878364ec6c61bdc Mon Sep 17 00:00:00 2001 From: robloo Date: Sat, 3 Dec 2022 10:20:10 -0500 Subject: [PATCH 22/87] Standardize Default member in PixelRect --- samples/ControlCatalog/Pages/ImagePage.xaml.cs | 2 +- src/Avalonia.Base/PixelRect.cs | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/samples/ControlCatalog/Pages/ImagePage.xaml.cs b/samples/ControlCatalog/Pages/ImagePage.xaml.cs index 511b01c7ac..00878dc5a8 100644 --- a/samples/ControlCatalog/Pages/ImagePage.xaml.cs +++ b/samples/ControlCatalog/Pages/ImagePage.xaml.cs @@ -70,7 +70,7 @@ namespace ControlCatalog.Pages 3 => new PixelRect(new PixelPoint(bitmapWidth - cropSize.Width, 0), cropSize), 4 => new PixelRect(new PixelPoint(0, bitmapHeight - cropSize.Height), cropSize), 5 => new PixelRect(new PixelPoint(bitmapWidth - cropSize.Width, bitmapHeight - cropSize.Height), cropSize), - _ => PixelRect.Empty + _ => PixelRect.Default }; } diff --git a/src/Avalonia.Base/PixelRect.cs b/src/Avalonia.Base/PixelRect.cs index 409a259182..330edbd872 100644 --- a/src/Avalonia.Base/PixelRect.cs +++ b/src/Avalonia.Base/PixelRect.cs @@ -10,9 +10,13 @@ namespace Avalonia public readonly struct PixelRect : IEquatable { /// - /// An empty rectangle. + /// A shared default instance representing an empty rectangle. /// - public static readonly PixelRect Empty = default; + public static readonly PixelRect Default = default; + + /// + [Obsolete("Use Default instead.")] + public static readonly PixelRect Empty = Default; /// /// Initializes a new instance of the structure. @@ -261,7 +265,7 @@ namespace Avalonia } else { - return Empty; + return Default; } } From 509ebddcc4fc8408aa5799367e2a40a1c1586c0f Mon Sep 17 00:00:00 2001 From: robloo Date: Sat, 3 Dec 2022 10:25:13 -0500 Subject: [PATCH 23/87] Standardize Default member in Rect --- src/Avalonia.Base/Media/Geometry.cs | 4 ++-- src/Avalonia.Base/Media/GeometryDrawing.cs | 2 +- src/Avalonia.Base/Media/GlyphRunDrawing.cs | 2 +- .../Media/TextFormatting/TextLineImpl.cs | 4 ++-- src/Avalonia.Base/Rect.cs | 12 ++++++++---- .../Server/ServerCompositionDrawListVisual.cs | 2 +- .../Composition/Server/ServerCompositionTarget.cs | 2 +- .../Composition/Server/ServerCompositionVisual.cs | 2 +- src/Avalonia.Base/Rendering/ImmediateRenderer.cs | 2 +- .../Rendering/SceneGraph/BitmapBlendModeNode.cs | 2 +- src/Avalonia.Base/Rendering/SceneGraph/ClipNode.cs | 2 +- .../Rendering/SceneGraph/GeometryClipNode.cs | 2 +- .../Rendering/SceneGraph/OpacityMaskNode.cs | 4 ++-- .../Rendering/SceneGraph/OpacityNode.cs | 2 +- .../Rendering/SceneGraph/SceneBuilder.cs | 6 +++--- .../Primitives/DataGridCellsPresenter.cs | 2 +- src/Avalonia.Controls/Flyouts/FlyoutBase.cs | 2 +- .../HeadlessPlatformRenderInterface.cs | 2 +- .../Avalonia.Browser/WebEmbeddableControlRoot.cs | 2 +- src/Skia/Avalonia.Skia/GeometryImpl.cs | 2 +- src/Skia/Avalonia.Skia/StreamGeometryImpl.cs | 2 +- tests/Avalonia.Base.UnitTests/RectTests.cs | 2 +- .../Rendering/DeferredRendererTests_HitTesting.cs | 4 ++-- .../Rendering/SceneGraph/DrawOperationTests.cs | 4 ++-- .../ItemsPresenterTests_Virtualization_Simple.cs | 2 +- .../Shapes/RectangleTests.cs | 2 +- 26 files changed, 40 insertions(+), 36 deletions(-) diff --git a/src/Avalonia.Base/Media/Geometry.cs b/src/Avalonia.Base/Media/Geometry.cs index c31a6699c2..7d56140f22 100644 --- a/src/Avalonia.Base/Media/Geometry.cs +++ b/src/Avalonia.Base/Media/Geometry.cs @@ -30,7 +30,7 @@ namespace Avalonia.Media /// /// Gets the geometry's bounding rectangle. /// - public Rect Bounds => PlatformImpl?.Bounds ?? Rect.Empty; + public Rect Bounds => PlatformImpl?.Bounds ?? Rect.Default; /// /// Gets the platform-specific implementation of the geometry. @@ -84,7 +84,7 @@ namespace Avalonia.Media /// /// The stroke thickness. /// The bounding rectangle. - public Rect GetRenderBounds(IPen pen) => PlatformImpl?.GetRenderBounds(pen) ?? Rect.Empty; + public Rect GetRenderBounds(IPen pen) => PlatformImpl?.GetRenderBounds(pen) ?? Rect.Default; /// /// Indicates whether the geometry's fill contains the specified point. diff --git a/src/Avalonia.Base/Media/GeometryDrawing.cs b/src/Avalonia.Base/Media/GeometryDrawing.cs index 7df7d25954..e9cbcecabe 100644 --- a/src/Avalonia.Base/Media/GeometryDrawing.cs +++ b/src/Avalonia.Base/Media/GeometryDrawing.cs @@ -69,7 +69,7 @@ namespace Avalonia.Media public override Rect GetBounds() { IPen pen = Pen ?? s_boundsPen; - return Geometry?.GetRenderBounds(pen) ?? Rect.Empty; + return Geometry?.GetRenderBounds(pen) ?? Rect.Default; } } } diff --git a/src/Avalonia.Base/Media/GlyphRunDrawing.cs b/src/Avalonia.Base/Media/GlyphRunDrawing.cs index 7e0d5c3c81..eab2d99387 100644 --- a/src/Avalonia.Base/Media/GlyphRunDrawing.cs +++ b/src/Avalonia.Base/Media/GlyphRunDrawing.cs @@ -32,7 +32,7 @@ public override Rect GetBounds() { - return GlyphRun != null ? new Rect(GlyphRun.Size) : Rect.Empty; + return GlyphRun != null ? new Rect(GlyphRun.Size) : Rect.Default; } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index 96f88d1f44..8f56e1d4c9 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -528,7 +528,7 @@ namespace Avalonia.Media.TextFormatting var startX = Start; double currentWidth = 0; - var currentRect = Rect.Empty; + var currentRect = Rect.Default; TextRunBounds lastRunBounds = default; @@ -762,7 +762,7 @@ namespace Avalonia.Media.TextFormatting var startX = WidthIncludingTrailingWhitespace; double currentWidth = 0; - var currentRect = Rect.Empty; + var currentRect = Rect.Default; for (var index = TextRuns.Count - 1; index >= 0; index--) { diff --git a/src/Avalonia.Base/Rect.cs b/src/Avalonia.Base/Rect.cs index 862e4caec1..50b776d14f 100644 --- a/src/Avalonia.Base/Rect.cs +++ b/src/Avalonia.Base/Rect.cs @@ -16,9 +16,13 @@ namespace Avalonia } /// - /// An empty rectangle. + /// A shared default instance representing an empty rectangle. /// - public static readonly Rect Empty = default(Rect); + public static readonly Rect Default = default; + + /// + [Obsolete("Use Default instead.")] + public static readonly Rect Empty = Default; /// /// The X position. @@ -394,7 +398,7 @@ namespace Avalonia } else { - return Empty; + return Default; } } @@ -467,7 +471,7 @@ namespace Avalonia double.IsNaN(rect.X) || double.IsNaN(rect.Y) || double.IsNaN(Height) || double.IsNaN(Width)) { - return Rect.Empty; + return Rect.Default; } if (rect.Width < 0) diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs index 6cbd2797f9..95ed8e27c1 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs @@ -37,7 +37,7 @@ internal class ServerCompositionDrawListVisual : ServerCompositionContainerVisua { if (_contentBounds == null) { - var rect = Rect.Empty; + var rect = Rect.Default; if(_renderCommands!=null) foreach (var cmd in _renderCommands) rect = rect.Union(cmd.Item.Bounds); diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs index c9517d01f8..c378f4981a 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs @@ -160,7 +160,7 @@ namespace Avalonia.Rendering.Composition.Server } RenderedVisuals = 0; - _dirtyRect = Rect.Empty; + _dirtyRect = Rect.Default; } } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs index 5e6dcab209..e6943211e4 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs @@ -192,7 +192,7 @@ namespace Avalonia.Rendering.Composition.Server void AddDirtyRect(Rect rc) { - if(rc == Rect.Empty) + if(rc == Rect.Default) return; Root?.AddDirtyRect(rc); } diff --git a/src/Avalonia.Base/Rendering/ImmediateRenderer.cs b/src/Avalonia.Base/Rendering/ImmediateRenderer.cs index 9e1582ed43..98a224181c 100644 --- a/src/Avalonia.Base/Rendering/ImmediateRenderer.cs +++ b/src/Avalonia.Base/Rendering/ImmediateRenderer.cs @@ -127,7 +127,7 @@ namespace Avalonia.Rendering /// public void AddDirty(Visual visual) { - if (visual.Bounds != Rect.Empty) + if (visual.Bounds != Rect.Default) { var m = visual.TransformToVisual(_root); diff --git a/src/Avalonia.Base/Rendering/SceneGraph/BitmapBlendModeNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/BitmapBlendModeNode.cs index 98e89f6549..6739e5b287 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/BitmapBlendModeNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/BitmapBlendModeNode.cs @@ -27,7 +27,7 @@ namespace Avalonia.Rendering.SceneGraph } /// - public Rect Bounds => Rect.Empty; + public Rect Bounds => Rect.Default; /// /// Gets the BitmapBlend to be pushed or null if the operation represents a pop. diff --git a/src/Avalonia.Base/Rendering/SceneGraph/ClipNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/ClipNode.cs index 90430bed02..9de18c068c 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/ClipNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/ClipNode.cs @@ -40,7 +40,7 @@ namespace Avalonia.Rendering.SceneGraph } /// - public Rect Bounds => Rect.Empty; + public Rect Bounds => Rect.Default; /// /// Gets the clip to be pushed or null if the operation represents a pop. diff --git a/src/Avalonia.Base/Rendering/SceneGraph/GeometryClipNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/GeometryClipNode.cs index 667b66420b..e8690ac6f8 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/GeometryClipNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/GeometryClipNode.cs @@ -28,7 +28,7 @@ namespace Avalonia.Rendering.SceneGraph } /// - public Rect Bounds => Rect.Empty; + public Rect Bounds => Rect.Default; /// /// Gets the clip to be pushed or null if the operation represents a pop. diff --git a/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs index 5fd200ddff..521fafe816 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs @@ -19,7 +19,7 @@ namespace Avalonia.Rendering.SceneGraph /// The bounds of the mask. /// Auxiliary data required to draw the brush. public OpacityMaskNode(IBrush mask, Rect bounds, IDisposable? aux = null) - : base(Rect.Empty, Matrix.Identity, aux) + : base(Rect.Default, Matrix.Identity, aux) { Mask = mask.ToImmutable(); MaskBounds = bounds; @@ -30,7 +30,7 @@ namespace Avalonia.Rendering.SceneGraph /// opacity mask pop. /// public OpacityMaskNode() - : base(Rect.Empty, Matrix.Identity, null) + : base(Rect.Default, Matrix.Identity, null) { } diff --git a/src/Avalonia.Base/Rendering/SceneGraph/OpacityNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/OpacityNode.cs index 8fc630588f..af8b710191 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/OpacityNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/OpacityNode.cs @@ -26,7 +26,7 @@ namespace Avalonia.Rendering.SceneGraph } /// - public Rect Bounds => Rect.Empty; + public Rect Bounds => Rect.Default; /// /// Gets the opacity to be pushed or null if the operation represents a pop. diff --git a/src/Avalonia.Base/Rendering/SceneGraph/SceneBuilder.cs b/src/Avalonia.Base/Rendering/SceneGraph/SceneBuilder.cs index d54bd3fad8..863662c93f 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/SceneBuilder.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/SceneBuilder.cs @@ -331,8 +331,8 @@ namespace Avalonia.Rendering.SceneGraph scene.Size = newSize; - Rect horizontalDirtyRect = Rect.Empty; - Rect verticalDirtyRect = Rect.Empty; + Rect horizontalDirtyRect = Rect.Default; + Rect verticalDirtyRect = Rect.Default; if (newSize.Width > oldSize.Width) { @@ -429,7 +429,7 @@ namespace Avalonia.Rendering.SceneGraph else { layer.OpacityMask = null; - layer.OpacityMaskRect = Rect.Empty; + layer.OpacityMaskRect = Rect.Default; } layer.GeometryClip = node.HasAncestorGeometryClip ? diff --git a/src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs b/src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs index 38d559a031..0e552af2ba 100644 --- a/src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs +++ b/src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs @@ -161,7 +161,7 @@ namespace Avalonia.Controls.Primitives { // Clip RectangleGeometry rg = new RectangleGeometry(); - rg.Rect = Rect.Empty; + rg.Rect = Rect.Default; cell.Clip = rg; } } diff --git a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs index 3ff248f0be..2fad480bba 100644 --- a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs +++ b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs @@ -457,7 +457,7 @@ namespace Avalonia.Controls.Primitives PopupPositioning.PopupPositionerConstraintAdjustment.SlideY; } - var trgtBnds = Target?.Bounds ?? Rect.Empty; + var trgtBnds = Target?.Bounds ?? Rect.Default; switch (Placement) { diff --git a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs index 7abc0ca131..c39f37aded 100644 --- a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs +++ b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs @@ -212,7 +212,7 @@ namespace Avalonia.Headless class HeadlessStreamingGeometryStub : HeadlessGeometryStub, IStreamGeometryImpl { - public HeadlessStreamingGeometryStub() : base(Rect.Empty) + public HeadlessStreamingGeometryStub() : base(Rect.Default) { } diff --git a/src/Browser/Avalonia.Browser/WebEmbeddableControlRoot.cs b/src/Browser/Avalonia.Browser/WebEmbeddableControlRoot.cs index e389ee98ea..8659b8f0d9 100644 --- a/src/Browser/Avalonia.Browser/WebEmbeddableControlRoot.cs +++ b/src/Browser/Avalonia.Browser/WebEmbeddableControlRoot.cs @@ -18,7 +18,7 @@ namespace Avalonia.Browser _onFirstRender = onFirstRender; } - public Rect Bounds => Rect.Empty; + public Rect Bounds => Rect.Default; public bool HasRendered => _hasRendered; diff --git a/src/Skia/Avalonia.Skia/GeometryImpl.cs b/src/Skia/Avalonia.Skia/GeometryImpl.cs index 4037cc4a35..6ff6354b4f 100644 --- a/src/Skia/Avalonia.Skia/GeometryImpl.cs +++ b/src/Skia/Avalonia.Skia/GeometryImpl.cs @@ -244,7 +244,7 @@ namespace Avalonia.Skia public void Invalidate() { CachedStrokePath?.Dispose(); - CachedGeometryRenderBounds = Rect.Empty; + CachedGeometryRenderBounds = Rect.Default; _cachedStrokeWidth = default(float); } } diff --git a/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs b/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs index 86450690e6..6e0d42b478 100644 --- a/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs +++ b/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs @@ -34,7 +34,7 @@ namespace Avalonia.Skia /// /// Initializes a new instance of the class. /// - public StreamGeometryImpl() : this(CreateEmptyPath(), Rect.Empty) + public StreamGeometryImpl() : this(CreateEmptyPath(), Rect.Default) { } diff --git a/tests/Avalonia.Base.UnitTests/RectTests.cs b/tests/Avalonia.Base.UnitTests/RectTests.cs index 95a438b287..15265f1fcd 100644 --- a/tests/Avalonia.Base.UnitTests/RectTests.cs +++ b/tests/Avalonia.Base.UnitTests/RectTests.cs @@ -52,7 +52,7 @@ namespace Avalonia.Base.UnitTests double.PositiveInfinity, double.PositiveInfinity) .Normalize(); - Assert.Equal(Rect.Empty, result); + Assert.Equal(Rect.Default, result); } } } diff --git a/tests/Avalonia.Base.UnitTests/Rendering/DeferredRendererTests_HitTesting.cs b/tests/Avalonia.Base.UnitTests/Rendering/DeferredRendererTests_HitTesting.cs index 2cf42d9604..ce4fff77e9 100644 --- a/tests/Avalonia.Base.UnitTests/Rendering/DeferredRendererTests_HitTesting.cs +++ b/tests/Avalonia.Base.UnitTests/Rendering/DeferredRendererTests_HitTesting.cs @@ -403,7 +403,7 @@ namespace Avalonia.Base.UnitTests.Rendering root.Measure(Size.Infinity); root.Arrange(new Rect(container.DesiredSize)); - root.Renderer.Paint(Rect.Empty); + root.Renderer.Paint(Rect.Default); var result = root.Renderer.HitTest(new Point(50, 150), root, null).First(); Assert.Equal(item1, result); @@ -419,7 +419,7 @@ namespace Avalonia.Base.UnitTests.Rendering container.InvalidateArrange(); container.Arrange(new Rect(container.DesiredSize)); - root.Renderer.Paint(Rect.Empty); + root.Renderer.Paint(Rect.Default); result = root.Renderer.HitTest(new Point(50, 150), root, null).First(); Assert.Equal(item2, result); diff --git a/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs b/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs index 2fac968206..4a347762e1 100644 --- a/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs +++ b/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs @@ -13,9 +13,9 @@ namespace Avalonia.Base.UnitTests.Rendering.SceneGraph [Fact] public void Empty_Bounds_Remain_Empty() { - var target = new TestDrawOperation(Rect.Empty, Matrix.Identity, null); + var target = new TestDrawOperation(Rect.Default, Matrix.Identity, null); - Assert.Equal(Rect.Empty, target.Bounds); + Assert.Equal(Rect.Default, target.Bounds); } [Theory] diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs index 08fd777ac6..d40f4f8cd0 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs @@ -570,7 +570,7 @@ namespace Avalonia.Controls.UnitTests.Presenters var items = (IList)target.Items; target.ApplyTemplate(); target.Measure(Size.Empty); - target.Arrange(Rect.Empty); + target.Arrange(Rect.Default); // Check for issue #591: this should not throw. target.ScrollIntoView(0); diff --git a/tests/Avalonia.Controls.UnitTests/Shapes/RectangleTests.cs b/tests/Avalonia.Controls.UnitTests/Shapes/RectangleTests.cs index 8d8ce10d4c..6a6d3975a7 100644 --- a/tests/Avalonia.Controls.UnitTests/Shapes/RectangleTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Shapes/RectangleTests.cs @@ -18,7 +18,7 @@ namespace Avalonia.Controls.UnitTests.Shapes target.Measure(new Size(100, 100)); var geometry = Assert.IsType(target.RenderedGeometry); - Assert.Equal(Rect.Empty, geometry.Rect); + Assert.Equal(Rect.Default, geometry.Rect); } [Fact] From 4fae1b6e7abf8ab1ccd7090110e8cc2055d4edb4 Mon Sep 17 00:00:00 2001 From: daniel mayost Date: Mon, 5 Dec 2022 10:15:05 +0200 Subject: [PATCH 24/87] Move FlewDirection to Visual and fix tests --- src/Avalonia.Base/Visual.cs | 90 +++++++++++++++++++ src/Avalonia.Controls/ComboBox.cs | 5 +- src/Avalonia.Controls/Control.cs | 85 ------------------ .../FlowDirectionTests.cs | 27 +++--- 4 files changed, 108 insertions(+), 99 deletions(-) diff --git a/src/Avalonia.Base/Visual.cs b/src/Avalonia.Base/Visual.cs index 7694119589..e3dc4fbb75 100644 --- a/src/Avalonia.Base/Visual.cs +++ b/src/Avalonia.Base/Visual.cs @@ -89,6 +89,14 @@ namespace Avalonia public static readonly StyledProperty RenderTransformOriginProperty = AvaloniaProperty.Register(nameof(RenderTransformOrigin), defaultValue: RelativePoint.Center); + /// + /// Defines the property. + /// + public static readonly AttachedProperty FlowDirectionProperty = + AvaloniaProperty.RegisterAttached( + nameof(FlowDirection), + inherits: true); + /// /// Defines the property. /// @@ -263,6 +271,15 @@ namespace Avalonia set { SetValue(RenderTransformOriginProperty, value); } } + /// + /// Gets or sets the text flow direction. + /// + public FlowDirection FlowDirection + { + get => GetValue(FlowDirectionProperty); + set => SetValue(FlowDirectionProperty, value); + } + /// /// Gets or sets the Z index of the control. /// @@ -306,6 +323,36 @@ namespace Avalonia /// internal Visual? VisualParent => _visualParent; + /// + /// Gets a value indicating whether control bypass FlowDirecton policies. + /// + /// + /// Related to FlowDirection system and returns false as default, so if + /// is RTL then control will get a mirror presentation. + /// For controls that want to avoid this behavior, override this property and return true. + /// + protected virtual bool BypassFlowDirectionPolicies => false; + + /// + /// Gets the value of the attached on a control. + /// + /// The control. + /// The flow direction. + public static FlowDirection GetFlowDirection(Visual visual) + { + return visual.GetValue(FlowDirectionProperty); + } + + /// + /// Sets the value of the attached on a control. + /// + /// The control. + /// The property value to set. + public static void SetFlowDirection(Visual visual, FlowDirection value) + { + visual.SetValue(FlowDirectionProperty, value); + } + /// /// Invalidates the visual and queues a repaint. /// @@ -387,6 +434,22 @@ namespace Avalonia } } + /// + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == FlowDirectionProperty) + { + InvalidateMirrorTransform(); + + foreach (var child in VisualChildren) + { + child.InvalidateMirrorTransform(); + } + } + } + protected override void LogicalChildrenCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) { base.LogicalChildrenCollectionChanged(sender, e); @@ -682,5 +745,32 @@ namespace Avalonia visual.SetVisualParent(parent); } } + + /// + /// Computes the value according to the + /// and + /// + public virtual void InvalidateMirrorTransform() + { + var flowDirection = this.FlowDirection; + var parentFlowDirection = FlowDirection.LeftToRight; + + bool bypassFlowDirectionPolicies = BypassFlowDirectionPolicies; + bool parentBypassFlowDirectionPolicies = false; + + var parent = VisualParent; + if (parent != null) + { + parentFlowDirection = parent.FlowDirection; + parentBypassFlowDirectionPolicies = parent.BypassFlowDirectionPolicies; + } + + bool thisShouldBeMirrored = flowDirection == FlowDirection.RightToLeft && !bypassFlowDirectionPolicies; + bool parentShouldBeMirrored = parentFlowDirection == FlowDirection.RightToLeft && !parentBypassFlowDirectionPolicies; + + bool shouldApplyMirrorTransform = thisShouldBeMirrored != parentShouldBeMirrored; + + HasMirrorTransform = shouldApplyMirrorTransform; + } } } diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs index 2b407cc42a..f02df2e9c1 100644 --- a/src/Avalonia.Controls/ComboBox.cs +++ b/src/Avalonia.Controls/ComboBox.cs @@ -454,10 +454,9 @@ namespace Avalonia.Controls { if (SelectionBoxItem is Rectangle rectangle) { - if ((rectangle.Fill as VisualBrush)?.Visual is Control content) + if ((rectangle.Fill as VisualBrush)?.Visual is Visual content) { - var flowDirection = (((Visual)content!).VisualParent as Control)?.FlowDirection ?? - FlowDirection.LeftToRight; + var flowDirection = content.VisualParent?.FlowDirection ?? FlowDirection.LeftToRight; rectangle.FlowDirection = flowDirection; } } diff --git a/src/Avalonia.Controls/Control.cs b/src/Avalonia.Controls/Control.cs index 063e6ae7c8..88c9823952 100644 --- a/src/Avalonia.Controls/Control.cs +++ b/src/Avalonia.Controls/Control.cs @@ -91,13 +91,6 @@ namespace Avalonia.Controls RoutedEvent.Register( nameof(SizeChanged), RoutingStrategies.Direct); - /// - /// Defines the property. - /// - public static readonly AttachedProperty FlowDirectionProperty = - AvaloniaProperty.RegisterAttached( - nameof(FlowDirection), - inherits: true); // Note the following: // _loadedQueue : @@ -170,15 +163,6 @@ namespace Avalonia.Controls get => GetValue(TagProperty); set => SetValue(TagProperty, value); } - - /// - /// Gets or sets the text flow direction. - /// - public FlowDirection FlowDirection - { - get => GetValue(FlowDirectionProperty); - set => SetValue(FlowDirectionProperty, value); - } /// /// Occurs when the user has completed a context input gesture, such as a right-click. @@ -229,39 +213,9 @@ namespace Avalonia.Controls public new Control? Parent => (Control?)base.Parent; - /// - /// Gets the value of the attached on a control. - /// - /// The control. - /// The flow direction. - public static FlowDirection GetFlowDirection(Control control) - { - return control.GetValue(FlowDirectionProperty); - } - - /// - /// Sets the value of the attached on a control. - /// - /// The control. - /// The property value to set. - public static void SetFlowDirection(Control control, FlowDirection value) - { - control.SetValue(FlowDirectionProperty, value); - } - /// bool IDataTemplateHost.IsDataTemplatesInitialized => _dataTemplates != null; - /// - /// Gets a value indicating whether control bypass FlowDirecton policies. - /// - /// - /// Related to FlowDirection system and returns false as default, so if - /// is RTL then control will get a mirror presentation. - /// For controls that want to avoid this behavior, override this property and return true. - /// - protected virtual bool BypassFlowDirectionPolicies => false; - /// void ISetterValue.Initialize(ISetter setter) { @@ -571,45 +525,6 @@ namespace Avalonia.Controls RaiseEvent(sizeChangedEventArgs); } } - else if (change.Property == FlowDirectionProperty) - { - InvalidateMirrorTransform(); - - foreach (var visual in VisualChildren) - { - if (visual is Control child) - { - child.InvalidateMirrorTransform(); - } - } - } - } - - /// - /// Computes the value according to the - /// and - /// - public virtual void InvalidateMirrorTransform() - { - var flowDirection = this.FlowDirection; - var parentFlowDirection = FlowDirection.LeftToRight; - - bool bypassFlowDirectionPolicies = BypassFlowDirectionPolicies; - bool parentBypassFlowDirectionPolicies = false; - - var parent = this.VisualParent as Control; - if (parent != null) - { - parentFlowDirection = parent.FlowDirection; - parentBypassFlowDirectionPolicies = parent.BypassFlowDirectionPolicies; - } - - bool thisShouldBeMirrored = flowDirection == FlowDirection.RightToLeft && !bypassFlowDirectionPolicies; - bool parentShouldBeMirrored = parentFlowDirection == FlowDirection.RightToLeft && !parentBypassFlowDirectionPolicies; - - bool shouldApplyMirrorTransform = thisShouldBeMirrored != parentShouldBeMirrored; - - HasMirrorTransform = shouldApplyMirrorTransform; } } } diff --git a/tests/Avalonia.Controls.UnitTests/FlowDirectionTests.cs b/tests/Avalonia.Controls.UnitTests/FlowDirectionTests.cs index 6c43103ecb..f790ed7412 100644 --- a/tests/Avalonia.Controls.UnitTests/FlowDirectionTests.cs +++ b/tests/Avalonia.Controls.UnitTests/FlowDirectionTests.cs @@ -8,7 +8,7 @@ namespace Avalonia.Controls.UnitTests [Fact] public void HasMirrorTransform_Should_Be_True() { - var target = new Control + var target = new Visual { FlowDirection = FlowDirection.RightToLeft, }; @@ -19,31 +19,36 @@ namespace Avalonia.Controls.UnitTests [Fact] public void HasMirrorTransform_Of_LTR_Children_Should_Be_True_For_RTL_Parent() { - Control child; - var target = new Decorator + var child = new Visual() + { + FlowDirection = FlowDirection.LeftToRight, + }; + + var target = new Visual { FlowDirection = FlowDirection.RightToLeft, - Child = child = new Control() }; + target.VisualChildren.Add(child); - child.FlowDirection = FlowDirection.LeftToRight; + child.InvalidateMirrorTransform(); Assert.True(target.HasMirrorTransform); Assert.True(child.HasMirrorTransform); } [Fact] - public void HasMirrorTransform_Of_Children_Is_Updated_After_Parent_Changeed() + public void HasMirrorTransform_Of_Children_Is_Updated_After_Parent_Changed() { - Control child; + var child = new Visual() + { + FlowDirection = FlowDirection.LeftToRight, + }; + var target = new Decorator { FlowDirection = FlowDirection.LeftToRight, - Child = child = new Control() - { - FlowDirection = FlowDirection.LeftToRight, - } }; + target.VisualChildren.Add(child); Assert.False(target.HasMirrorTransform); Assert.False(child.HasMirrorTransform); From 16f73a32c5760ec21f519c0e557ecdd5c48ab9c4 Mon Sep 17 00:00:00 2001 From: daniel mayost Date: Mon, 5 Dec 2022 10:23:55 +0200 Subject: [PATCH 25/87] Rmove BypassFlowDirectionPolicies from TopLavel --- src/Avalonia.Controls/TopLevel.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 59ad696148..d2d5a8309c 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -357,12 +357,6 @@ namespace Avalonia.Controls /// protected virtual ILayoutManager CreateLayoutManager() => new LayoutManager(this); - public override void InvalidateMirrorTransform() - { - } - - protected override bool BypassFlowDirectionPolicies => true; - /// /// Handles a paint notification from . /// From 18aa1e16e51b914419bd9b344c056a641bfc7962 Mon Sep 17 00:00:00 2001 From: daniel mayost Date: Mon, 5 Dec 2022 10:38:46 +0200 Subject: [PATCH 26/87] Submodel --- nukebuild/Numerge | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nukebuild/Numerge b/nukebuild/Numerge index fb92f917cd..aef10ae67d 160000 --- a/nukebuild/Numerge +++ b/nukebuild/Numerge @@ -1 +1 @@ -Subproject commit fb92f917cd2d3aaec0d2294635d922184ff1e0fc +Subproject commit aef10ae67dc55c95f49b52a505a0be33bfa297a5 From aa31daecde49ac4ec880a61dd9eb0cd2e6197eb9 Mon Sep 17 00:00:00 2001 From: daniel mayost Date: Mon, 5 Dec 2022 13:46:11 +0200 Subject: [PATCH 27/87] Move FlowDirectionTests to Avalonia.Base.Tests --- .../FlowDirectionTests.cs | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/{Avalonia.Controls.UnitTests => Avalonia.Base.UnitTests}/FlowDirectionTests.cs (100%) diff --git a/tests/Avalonia.Controls.UnitTests/FlowDirectionTests.cs b/tests/Avalonia.Base.UnitTests/FlowDirectionTests.cs similarity index 100% rename from tests/Avalonia.Controls.UnitTests/FlowDirectionTests.cs rename to tests/Avalonia.Base.UnitTests/FlowDirectionTests.cs From a799f722420080991e9732bf2ac17378fbc7a327 Mon Sep 17 00:00:00 2001 From: robloo Date: Mon, 5 Dec 2022 07:33:08 -0500 Subject: [PATCH 28/87] Remove PixelRect.Default --- samples/ControlCatalog/Pages/ImagePage.xaml.cs | 2 +- src/Avalonia.Base/PixelRect.cs | 11 ++++------- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/samples/ControlCatalog/Pages/ImagePage.xaml.cs b/samples/ControlCatalog/Pages/ImagePage.xaml.cs index 00878dc5a8..5b3169a1b0 100644 --- a/samples/ControlCatalog/Pages/ImagePage.xaml.cs +++ b/samples/ControlCatalog/Pages/ImagePage.xaml.cs @@ -70,7 +70,7 @@ namespace ControlCatalog.Pages 3 => new PixelRect(new PixelPoint(bitmapWidth - cropSize.Width, 0), cropSize), 4 => new PixelRect(new PixelPoint(0, bitmapHeight - cropSize.Height), cropSize), 5 => new PixelRect(new PixelPoint(bitmapWidth - cropSize.Width, bitmapHeight - cropSize.Height), cropSize), - _ => PixelRect.Default + _ => default }; } diff --git a/src/Avalonia.Base/PixelRect.cs b/src/Avalonia.Base/PixelRect.cs index 330edbd872..469f33e7fd 100644 --- a/src/Avalonia.Base/PixelRect.cs +++ b/src/Avalonia.Base/PixelRect.cs @@ -10,13 +10,10 @@ namespace Avalonia public readonly struct PixelRect : IEquatable { /// - /// A shared default instance representing an empty rectangle. + /// An empty rectangle. /// - public static readonly PixelRect Default = default; - - /// - [Obsolete("Use Default instead.")] - public static readonly PixelRect Empty = Default; + [Obsolete("Use the default keyword instead.")] + public static readonly PixelRect Empty = default; /// /// Initializes a new instance of the structure. @@ -265,7 +262,7 @@ namespace Avalonia } else { - return Default; + return default; } } From 3fc6a51571d65dfcdeb21a20df1c9667b8c1164b Mon Sep 17 00:00:00 2001 From: robloo Date: Mon, 5 Dec 2022 07:43:51 -0500 Subject: [PATCH 29/87] Remove Rect.Default --- src/Avalonia.Base/Media/Geometry.cs | 4 ++-- src/Avalonia.Base/Media/GeometryDrawing.cs | 2 +- src/Avalonia.Base/Media/GlyphRunDrawing.cs | 2 +- .../Media/TextFormatting/TextLineImpl.cs | 4 ++-- src/Avalonia.Base/Rect.cs | 13 +++++-------- .../Server/ServerCompositionDrawListVisual.cs | 2 +- .../Composition/Server/ServerCompositionTarget.cs | 2 +- .../Composition/Server/ServerCompositionVisual.cs | 2 +- src/Avalonia.Base/Rendering/ImmediateRenderer.cs | 2 +- .../Rendering/SceneGraph/BitmapBlendModeNode.cs | 2 +- src/Avalonia.Base/Rendering/SceneGraph/ClipNode.cs | 2 +- .../Rendering/SceneGraph/GeometryClipNode.cs | 2 +- .../Rendering/SceneGraph/OpacityMaskNode.cs | 4 ++-- .../Rendering/SceneGraph/OpacityNode.cs | 2 +- .../Rendering/SceneGraph/SceneBuilder.cs | 6 +++--- .../Primitives/DataGridCellsPresenter.cs | 2 +- src/Avalonia.Controls/Flyouts/FlyoutBase.cs | 2 +- .../HeadlessPlatformRenderInterface.cs | 2 +- .../Avalonia.Browser/WebEmbeddableControlRoot.cs | 2 +- src/Skia/Avalonia.Skia/GeometryImpl.cs | 2 +- src/Skia/Avalonia.Skia/StreamGeometryImpl.cs | 2 +- tests/Avalonia.Base.UnitTests/RectTests.cs | 2 +- .../Rendering/DeferredRendererTests_HitTesting.cs | 4 ++-- .../Rendering/SceneGraph/DrawOperationTests.cs | 4 ++-- .../ItemsPresenterTests_Virtualization_Simple.cs | 2 +- .../Shapes/RectangleTests.cs | 2 +- 26 files changed, 37 insertions(+), 40 deletions(-) diff --git a/src/Avalonia.Base/Media/Geometry.cs b/src/Avalonia.Base/Media/Geometry.cs index 7d56140f22..2019f54c70 100644 --- a/src/Avalonia.Base/Media/Geometry.cs +++ b/src/Avalonia.Base/Media/Geometry.cs @@ -30,7 +30,7 @@ namespace Avalonia.Media /// /// Gets the geometry's bounding rectangle. /// - public Rect Bounds => PlatformImpl?.Bounds ?? Rect.Default; + public Rect Bounds => PlatformImpl?.Bounds ?? default; /// /// Gets the platform-specific implementation of the geometry. @@ -84,7 +84,7 @@ namespace Avalonia.Media /// /// The stroke thickness. /// The bounding rectangle. - public Rect GetRenderBounds(IPen pen) => PlatformImpl?.GetRenderBounds(pen) ?? Rect.Default; + public Rect GetRenderBounds(IPen pen) => PlatformImpl?.GetRenderBounds(pen) ?? default; /// /// Indicates whether the geometry's fill contains the specified point. diff --git a/src/Avalonia.Base/Media/GeometryDrawing.cs b/src/Avalonia.Base/Media/GeometryDrawing.cs index e9cbcecabe..26cc2c3cab 100644 --- a/src/Avalonia.Base/Media/GeometryDrawing.cs +++ b/src/Avalonia.Base/Media/GeometryDrawing.cs @@ -69,7 +69,7 @@ namespace Avalonia.Media public override Rect GetBounds() { IPen pen = Pen ?? s_boundsPen; - return Geometry?.GetRenderBounds(pen) ?? Rect.Default; + return Geometry?.GetRenderBounds(pen) ?? default; } } } diff --git a/src/Avalonia.Base/Media/GlyphRunDrawing.cs b/src/Avalonia.Base/Media/GlyphRunDrawing.cs index eab2d99387..242b9913fa 100644 --- a/src/Avalonia.Base/Media/GlyphRunDrawing.cs +++ b/src/Avalonia.Base/Media/GlyphRunDrawing.cs @@ -32,7 +32,7 @@ public override Rect GetBounds() { - return GlyphRun != null ? new Rect(GlyphRun.Size) : Rect.Default; + return GlyphRun != null ? new Rect(GlyphRun.Size) : default; } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index 8f56e1d4c9..5312807a00 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -528,7 +528,7 @@ namespace Avalonia.Media.TextFormatting var startX = Start; double currentWidth = 0; - var currentRect = Rect.Default; + var currentRect = default(Rect); TextRunBounds lastRunBounds = default; @@ -762,7 +762,7 @@ namespace Avalonia.Media.TextFormatting var startX = WidthIncludingTrailingWhitespace; double currentWidth = 0; - var currentRect = Rect.Default; + var currentRect = default(Rect); for (var index = TextRuns.Count - 1; index >= 0; index--) { diff --git a/src/Avalonia.Base/Rect.cs b/src/Avalonia.Base/Rect.cs index 50b776d14f..831ab28adc 100644 --- a/src/Avalonia.Base/Rect.cs +++ b/src/Avalonia.Base/Rect.cs @@ -16,13 +16,10 @@ namespace Avalonia } /// - /// A shared default instance representing an empty rectangle. + /// An empty rectangle. /// - public static readonly Rect Default = default; - - /// - [Obsolete("Use Default instead.")] - public static readonly Rect Empty = Default; + [Obsolete("Use the default keyword instead.")] + public static readonly Rect Empty = default; /// /// The X position. @@ -398,7 +395,7 @@ namespace Avalonia } else { - return Default; + return default; } } @@ -471,7 +468,7 @@ namespace Avalonia double.IsNaN(rect.X) || double.IsNaN(rect.Y) || double.IsNaN(Height) || double.IsNaN(Width)) { - return Rect.Default; + return default; } if (rect.Width < 0) diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs index 95ed8e27c1..8dc088fed1 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs @@ -37,7 +37,7 @@ internal class ServerCompositionDrawListVisual : ServerCompositionContainerVisua { if (_contentBounds == null) { - var rect = Rect.Default; + var rect = default(Rect); if(_renderCommands!=null) foreach (var cmd in _renderCommands) rect = rect.Union(cmd.Item.Bounds); diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs index c378f4981a..f68994c812 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs @@ -160,7 +160,7 @@ namespace Avalonia.Rendering.Composition.Server } RenderedVisuals = 0; - _dirtyRect = Rect.Default; + _dirtyRect = default; } } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs index e6943211e4..387998f8d6 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs @@ -192,7 +192,7 @@ namespace Avalonia.Rendering.Composition.Server void AddDirtyRect(Rect rc) { - if(rc == Rect.Default) + if(rc == default) return; Root?.AddDirtyRect(rc); } diff --git a/src/Avalonia.Base/Rendering/ImmediateRenderer.cs b/src/Avalonia.Base/Rendering/ImmediateRenderer.cs index 98a224181c..989d4eb4d7 100644 --- a/src/Avalonia.Base/Rendering/ImmediateRenderer.cs +++ b/src/Avalonia.Base/Rendering/ImmediateRenderer.cs @@ -127,7 +127,7 @@ namespace Avalonia.Rendering /// public void AddDirty(Visual visual) { - if (visual.Bounds != Rect.Default) + if (!visual.Bounds.IsDefault) { var m = visual.TransformToVisual(_root); diff --git a/src/Avalonia.Base/Rendering/SceneGraph/BitmapBlendModeNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/BitmapBlendModeNode.cs index 6739e5b287..b1190a159b 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/BitmapBlendModeNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/BitmapBlendModeNode.cs @@ -27,7 +27,7 @@ namespace Avalonia.Rendering.SceneGraph } /// - public Rect Bounds => Rect.Default; + public Rect Bounds => default; /// /// Gets the BitmapBlend to be pushed or null if the operation represents a pop. diff --git a/src/Avalonia.Base/Rendering/SceneGraph/ClipNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/ClipNode.cs index 9de18c068c..e1bfaa4aa3 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/ClipNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/ClipNode.cs @@ -40,7 +40,7 @@ namespace Avalonia.Rendering.SceneGraph } /// - public Rect Bounds => Rect.Default; + public Rect Bounds => default; /// /// Gets the clip to be pushed or null if the operation represents a pop. diff --git a/src/Avalonia.Base/Rendering/SceneGraph/GeometryClipNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/GeometryClipNode.cs index e8690ac6f8..842edf2bcb 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/GeometryClipNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/GeometryClipNode.cs @@ -28,7 +28,7 @@ namespace Avalonia.Rendering.SceneGraph } /// - public Rect Bounds => Rect.Default; + public Rect Bounds => default; /// /// Gets the clip to be pushed or null if the operation represents a pop. diff --git a/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs index 521fafe816..3ecc07fa54 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs @@ -19,7 +19,7 @@ namespace Avalonia.Rendering.SceneGraph /// The bounds of the mask. /// Auxiliary data required to draw the brush. public OpacityMaskNode(IBrush mask, Rect bounds, IDisposable? aux = null) - : base(Rect.Default, Matrix.Identity, aux) + : base(default, Matrix.Identity, aux) { Mask = mask.ToImmutable(); MaskBounds = bounds; @@ -30,7 +30,7 @@ namespace Avalonia.Rendering.SceneGraph /// opacity mask pop. /// public OpacityMaskNode() - : base(Rect.Default, Matrix.Identity, null) + : base(default, Matrix.Identity, null) { } diff --git a/src/Avalonia.Base/Rendering/SceneGraph/OpacityNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/OpacityNode.cs index af8b710191..e41e639067 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/OpacityNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/OpacityNode.cs @@ -26,7 +26,7 @@ namespace Avalonia.Rendering.SceneGraph } /// - public Rect Bounds => Rect.Default; + public Rect Bounds => default; /// /// Gets the opacity to be pushed or null if the operation represents a pop. diff --git a/src/Avalonia.Base/Rendering/SceneGraph/SceneBuilder.cs b/src/Avalonia.Base/Rendering/SceneGraph/SceneBuilder.cs index 863662c93f..55ff772772 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/SceneBuilder.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/SceneBuilder.cs @@ -331,8 +331,8 @@ namespace Avalonia.Rendering.SceneGraph scene.Size = newSize; - Rect horizontalDirtyRect = Rect.Default; - Rect verticalDirtyRect = Rect.Default; + Rect horizontalDirtyRect = default; + Rect verticalDirtyRect = default; if (newSize.Width > oldSize.Width) { @@ -429,7 +429,7 @@ namespace Avalonia.Rendering.SceneGraph else { layer.OpacityMask = null; - layer.OpacityMaskRect = Rect.Default; + layer.OpacityMaskRect = default; } layer.GeometryClip = node.HasAncestorGeometryClip ? diff --git a/src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs b/src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs index 0e552af2ba..06a77f0894 100644 --- a/src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs +++ b/src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs @@ -161,7 +161,7 @@ namespace Avalonia.Controls.Primitives { // Clip RectangleGeometry rg = new RectangleGeometry(); - rg.Rect = Rect.Default; + rg.Rect = default; cell.Clip = rg; } } diff --git a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs index 2fad480bba..7f7600e877 100644 --- a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs +++ b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs @@ -457,7 +457,7 @@ namespace Avalonia.Controls.Primitives PopupPositioning.PopupPositionerConstraintAdjustment.SlideY; } - var trgtBnds = Target?.Bounds ?? Rect.Default; + var trgtBnds = Target?.Bounds ?? default; switch (Placement) { diff --git a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs index c39f37aded..d95ce3fe85 100644 --- a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs +++ b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs @@ -212,7 +212,7 @@ namespace Avalonia.Headless class HeadlessStreamingGeometryStub : HeadlessGeometryStub, IStreamGeometryImpl { - public HeadlessStreamingGeometryStub() : base(Rect.Default) + public HeadlessStreamingGeometryStub() : base(default) { } diff --git a/src/Browser/Avalonia.Browser/WebEmbeddableControlRoot.cs b/src/Browser/Avalonia.Browser/WebEmbeddableControlRoot.cs index 8659b8f0d9..df1a24fa0f 100644 --- a/src/Browser/Avalonia.Browser/WebEmbeddableControlRoot.cs +++ b/src/Browser/Avalonia.Browser/WebEmbeddableControlRoot.cs @@ -18,7 +18,7 @@ namespace Avalonia.Browser _onFirstRender = onFirstRender; } - public Rect Bounds => Rect.Default; + public Rect Bounds => default; public bool HasRendered => _hasRendered; diff --git a/src/Skia/Avalonia.Skia/GeometryImpl.cs b/src/Skia/Avalonia.Skia/GeometryImpl.cs index 6ff6354b4f..1141763097 100644 --- a/src/Skia/Avalonia.Skia/GeometryImpl.cs +++ b/src/Skia/Avalonia.Skia/GeometryImpl.cs @@ -244,7 +244,7 @@ namespace Avalonia.Skia public void Invalidate() { CachedStrokePath?.Dispose(); - CachedGeometryRenderBounds = Rect.Default; + CachedGeometryRenderBounds = default; _cachedStrokeWidth = default(float); } } diff --git a/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs b/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs index 6e0d42b478..df847d2224 100644 --- a/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs +++ b/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs @@ -34,7 +34,7 @@ namespace Avalonia.Skia /// /// Initializes a new instance of the class. /// - public StreamGeometryImpl() : this(CreateEmptyPath(), Rect.Default) + public StreamGeometryImpl() : this(CreateEmptyPath(), default) { } diff --git a/tests/Avalonia.Base.UnitTests/RectTests.cs b/tests/Avalonia.Base.UnitTests/RectTests.cs index 15265f1fcd..c44b328ed5 100644 --- a/tests/Avalonia.Base.UnitTests/RectTests.cs +++ b/tests/Avalonia.Base.UnitTests/RectTests.cs @@ -52,7 +52,7 @@ namespace Avalonia.Base.UnitTests double.PositiveInfinity, double.PositiveInfinity) .Normalize(); - Assert.Equal(Rect.Default, result); + Assert.Equal(default, result); } } } diff --git a/tests/Avalonia.Base.UnitTests/Rendering/DeferredRendererTests_HitTesting.cs b/tests/Avalonia.Base.UnitTests/Rendering/DeferredRendererTests_HitTesting.cs index ce4fff77e9..c164012446 100644 --- a/tests/Avalonia.Base.UnitTests/Rendering/DeferredRendererTests_HitTesting.cs +++ b/tests/Avalonia.Base.UnitTests/Rendering/DeferredRendererTests_HitTesting.cs @@ -403,7 +403,7 @@ namespace Avalonia.Base.UnitTests.Rendering root.Measure(Size.Infinity); root.Arrange(new Rect(container.DesiredSize)); - root.Renderer.Paint(Rect.Default); + root.Renderer.Paint(default); var result = root.Renderer.HitTest(new Point(50, 150), root, null).First(); Assert.Equal(item1, result); @@ -419,7 +419,7 @@ namespace Avalonia.Base.UnitTests.Rendering container.InvalidateArrange(); container.Arrange(new Rect(container.DesiredSize)); - root.Renderer.Paint(Rect.Default); + root.Renderer.Paint(default); result = root.Renderer.HitTest(new Point(50, 150), root, null).First(); Assert.Equal(item2, result); diff --git a/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs b/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs index 4a347762e1..07d2d672ae 100644 --- a/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs +++ b/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs @@ -13,9 +13,9 @@ namespace Avalonia.Base.UnitTests.Rendering.SceneGraph [Fact] public void Empty_Bounds_Remain_Empty() { - var target = new TestDrawOperation(Rect.Default, Matrix.Identity, null); + var target = new TestDrawOperation(default, Matrix.Identity, null); - Assert.Equal(Rect.Default, target.Bounds); + Assert.Equal(default, target.Bounds); } [Theory] diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs index d40f4f8cd0..a2aba84c34 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs @@ -570,7 +570,7 @@ namespace Avalonia.Controls.UnitTests.Presenters var items = (IList)target.Items; target.ApplyTemplate(); target.Measure(Size.Empty); - target.Arrange(Rect.Default); + target.Arrange(default); // Check for issue #591: this should not throw. target.ScrollIntoView(0); diff --git a/tests/Avalonia.Controls.UnitTests/Shapes/RectangleTests.cs b/tests/Avalonia.Controls.UnitTests/Shapes/RectangleTests.cs index 6a6d3975a7..067d709ae1 100644 --- a/tests/Avalonia.Controls.UnitTests/Shapes/RectangleTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Shapes/RectangleTests.cs @@ -18,7 +18,7 @@ namespace Avalonia.Controls.UnitTests.Shapes target.Measure(new Size(100, 100)); var geometry = Assert.IsType(target.RenderedGeometry); - Assert.Equal(Rect.Default, geometry.Rect); + Assert.Equal(default, geometry.Rect); } [Fact] From 932d52dcf3151f8d5a248eac4b0001b93584f826 Mon Sep 17 00:00:00 2001 From: robloo Date: Mon, 5 Dec 2022 07:52:00 -0500 Subject: [PATCH 30/87] Standardize Default members in Size --- src/Avalonia.Base/Media/Imaging/CroppedBitmap.cs | 2 +- .../Composition/Server/DrawingContextProxy.cs | 2 +- src/Avalonia.Base/Rendering/DeferredRenderer.cs | 2 +- src/Avalonia.Base/Rendering/ImmediateRenderer.cs | 2 +- src/Avalonia.Base/Size.cs | 8 +++----- src/Avalonia.Controls.DataGrid/DataGrid.cs | 4 ++-- .../Primitives/DataGridColumnHeadersPresenter.cs | 2 +- .../Primitives/DataGridDetailsPresenter.cs | 2 +- src/Avalonia.Controls/Flyouts/FlyoutBase.cs | 2 +- src/Avalonia.Controls/LayoutTransformControl.cs | 10 +++++----- src/Avalonia.Controls/Presenters/ItemsPresenter.cs | 6 +++--- src/Avalonia.Controls/Shapes/Shape.cs | 2 +- .../ItemsPresenterTests_Virtualization_Simple.cs | 2 +- 13 files changed, 22 insertions(+), 24 deletions(-) diff --git a/src/Avalonia.Base/Media/Imaging/CroppedBitmap.cs b/src/Avalonia.Base/Media/Imaging/CroppedBitmap.cs index ac462aeb72..525a543b70 100644 --- a/src/Avalonia.Base/Media/Imaging/CroppedBitmap.cs +++ b/src/Avalonia.Base/Media/Imaging/CroppedBitmap.cs @@ -78,7 +78,7 @@ namespace Avalonia.Media.Imaging get { if (Source is not IBitmap bmp) - return Size.Empty; + return default; if (SourceRect.IsDefault) return Source.Size; return SourceRect.Size.ToSizeWithDpi(bmp.Dpi); diff --git a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs index 03859d241f..e6bbba6ec0 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs @@ -163,7 +163,7 @@ internal class CompositorDrawingContextProxy : IDrawingContextImpl, IDrawingCont public CompositionDrawList? VisualBrushDrawList { get; set; } public Size GetRenderTargetSize(IVisualBrush brush) { - return VisualBrushDrawList?.Size ?? Size.Empty; + return VisualBrushDrawList?.Size ?? default; } public void RenderVisualBrush(IDrawingContextImpl context, IVisualBrush brush) diff --git a/src/Avalonia.Base/Rendering/DeferredRenderer.cs b/src/Avalonia.Base/Rendering/DeferredRenderer.cs index a70a9ccbdf..4ad7c11d6d 100644 --- a/src/Avalonia.Base/Rendering/DeferredRenderer.cs +++ b/src/Avalonia.Base/Rendering/DeferredRenderer.cs @@ -277,7 +277,7 @@ namespace Avalonia.Rendering /// Size IVisualBrushRenderer.GetRenderTargetSize(IVisualBrush brush) { - return TryGetChildScene(_currentDraw)?.Size ?? Size.Empty; + return TryGetChildScene(_currentDraw)?.Size ?? default; } /// diff --git a/src/Avalonia.Base/Rendering/ImmediateRenderer.cs b/src/Avalonia.Base/Rendering/ImmediateRenderer.cs index 989d4eb4d7..4e9e7a0615 100644 --- a/src/Avalonia.Base/Rendering/ImmediateRenderer.cs +++ b/src/Avalonia.Base/Rendering/ImmediateRenderer.cs @@ -189,7 +189,7 @@ namespace Avalonia.Rendering Size IVisualBrushRenderer.GetRenderTargetSize(IVisualBrush brush) { (brush.Visual as IVisualBrushInitialize)?.EnsureInitialized(); - return brush.Visual?.Bounds.Size ?? Size.Empty; + return brush.Visual?.Bounds.Size ?? default; } /// diff --git a/src/Avalonia.Base/Size.cs b/src/Avalonia.Base/Size.cs index 5f20206200..aec237afae 100644 --- a/src/Avalonia.Base/Size.cs +++ b/src/Avalonia.Base/Size.cs @@ -28,8 +28,9 @@ namespace Avalonia public static readonly Size Infinity = new Size(double.PositiveInfinity, double.PositiveInfinity); /// - /// A size representing zero + /// A size representing zero. /// + [Obsolete("Use the default keyword instead.")] public static readonly Size Empty = new Size(0, 0); /// @@ -309,9 +310,6 @@ namespace Avalonia /// /// Gets a value indicating whether the Width and Height values are zero. /// - public bool IsDefault - { - get { return (_width == 0) && (_height == 0); } - } + public bool IsDefault => (_width == 0) && (_height == 0); } } diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs index 454678c64b..68799915dc 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs @@ -1124,7 +1124,7 @@ namespace Avalonia.Controls EnsureColumnHeadersVisibility(); if (!newValueCols) { - _columnHeadersPresenter.Measure(Size.Empty); + _columnHeadersPresenter.Measure(default(Size)); } else { @@ -1165,7 +1165,7 @@ namespace Avalonia.Controls _topLeftCornerHeader.IsVisible = newValueRows && newValueCols; if (_topLeftCornerHeader.IsVisible) { - _topLeftCornerHeader.Measure(Size.Empty); + _topLeftCornerHeader.Measure(default(Size)); } } diff --git a/src/Avalonia.Controls.DataGrid/Primitives/DataGridColumnHeadersPresenter.cs b/src/Avalonia.Controls.DataGrid/Primitives/DataGridColumnHeadersPresenter.cs index b34f52f47d..f9b84793c6 100644 --- a/src/Avalonia.Controls.DataGrid/Primitives/DataGridColumnHeadersPresenter.cs +++ b/src/Avalonia.Controls.DataGrid/Primitives/DataGridColumnHeadersPresenter.cs @@ -305,7 +305,7 @@ namespace Avalonia.Controls.Primitives } if (!OwningGrid.AreColumnHeadersVisible) { - return Size.Empty; + return default; } double height = OwningGrid.ColumnHeaderHeight; bool autoSizeHeight; diff --git a/src/Avalonia.Controls.DataGrid/Primitives/DataGridDetailsPresenter.cs b/src/Avalonia.Controls.DataGrid/Primitives/DataGridDetailsPresenter.cs index 543485b311..07e7708003 100644 --- a/src/Avalonia.Controls.DataGrid/Primitives/DataGridDetailsPresenter.cs +++ b/src/Avalonia.Controls.DataGrid/Primitives/DataGridDetailsPresenter.cs @@ -112,7 +112,7 @@ namespace Avalonia.Controls.Primitives { if (OwningGrid == null || Children.Count == 0) { - return Size.Empty; + return default; } double desiredWidth = OwningGrid.AreRowDetailsFrozen ? diff --git a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs index 7f7600e877..8455495830 100644 --- a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs +++ b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs @@ -435,7 +435,7 @@ namespace Avalonia.Controls.Primitives { Size sz; // Popup.Child can't be null here, it was set in ShowAtCore. - if (Popup.Child!.DesiredSize == Size.Empty) + if (Popup.Child!.DesiredSize.IsDefault) { // Popup may not have been shown yet. Measure content sz = LayoutHelper.MeasureChild(Popup.Child, Size.Infinity, new Thickness()); diff --git a/src/Avalonia.Controls/LayoutTransformControl.cs b/src/Avalonia.Controls/LayoutTransformControl.cs index 5668b79e81..766a712c88 100644 --- a/src/Avalonia.Controls/LayoutTransformControl.cs +++ b/src/Avalonia.Controls/LayoutTransformControl.cs @@ -91,7 +91,7 @@ namespace Avalonia.Controls arrangedsize = TransformRoot.Bounds.Size; // This is the first opportunity under Silverlight to find out the Child's true DesiredSize - if (IsSizeSmaller(finalSizeTransformed, arrangedsize) && (Size.Empty == _childActualSize)) + if (IsSizeSmaller(finalSizeTransformed, arrangedsize) && _childActualSize.IsDefault) { //// Unfortunately, all the work so far is invalid because the wrong DesiredSize was used //// Make a note of the actual DesiredSize @@ -102,7 +102,7 @@ namespace Avalonia.Controls else { // Clear the "need to measure/arrange again" flag - _childActualSize = Size.Empty; + _childActualSize = default; } // Return result to perform the transformation @@ -122,7 +122,7 @@ namespace Avalonia.Controls } Size measureSize; - if (_childActualSize == Size.Empty) + if (_childActualSize.IsDefault) { // Determine the largest size after the transformation measureSize = ComputeLargestTransformedSize(availableSize); @@ -206,7 +206,7 @@ namespace Avalonia.Controls /// /// Actual DesiredSize of Child element (the value it returned from its MeasureOverride method). /// - private Size _childActualSize = Size.Empty; + private Size _childActualSize = default; /// /// RenderTransform/MatrixTransform applied to TransformRoot. @@ -281,7 +281,7 @@ namespace Avalonia.Controls private Size ComputeLargestTransformedSize(Size arrangeBounds) { // Computed largest transformed size - Size computedSize = Size.Empty; + Size computedSize = default; // Detect infinite bounds and constrain the scenario bool infiniteWidth = double.IsInfinity(arrangeBounds.Width); diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs index 924c4567f6..96c9b7b5d9 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs @@ -77,7 +77,7 @@ namespace Avalonia.Controls.Presenters } /// - Size IScrollable.Extent => Virtualizer?.Extent ?? Size.Empty; + Size IScrollable.Extent => Virtualizer?.Extent ?? default; /// Vector IScrollable.Offset @@ -136,12 +136,12 @@ namespace Avalonia.Controls.Presenters /// protected override Size MeasureOverride(Size availableSize) { - return Virtualizer?.MeasureOverride(availableSize) ?? Size.Empty; + return Virtualizer?.MeasureOverride(availableSize) ?? default; } protected override Size ArrangeOverride(Size finalSize) { - return Virtualizer?.ArrangeOverride(finalSize) ?? Size.Empty; + return Virtualizer?.ArrangeOverride(finalSize) ?? default; } /// diff --git a/src/Avalonia.Controls/Shapes/Shape.cs b/src/Avalonia.Controls/Shapes/Shape.cs index 1a7218ff2a..e2a13512a5 100644 --- a/src/Avalonia.Controls/Shapes/Shape.cs +++ b/src/Avalonia.Controls/Shapes/Shape.cs @@ -292,7 +292,7 @@ namespace Avalonia.Controls.Shapes return finalSize; } - return Size.Empty; + return default; } internal static (Size size, Matrix transform) CalculateSizeAndTransform(Size availableSize, Rect shapeBounds, Stretch Stretch) diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs index a2aba84c34..2d1dad6be0 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs @@ -569,7 +569,7 @@ namespace Avalonia.Controls.UnitTests.Presenters var target = CreateTarget(itemCount: 10); var items = (IList)target.Items; target.ApplyTemplate(); - target.Measure(Size.Empty); + target.Measure(default(Size)); target.Arrange(default); // Check for issue #591: this should not throw. From 80e1e1eb4cfb544a89e12c07ae56a994bfb37490 Mon Sep 17 00:00:00 2001 From: Daniil Pavliuchyk Date: Mon, 5 Dec 2022 18:53:24 +0200 Subject: [PATCH 31/87] Disabled controls should lose focus and don't accept keyboard events --- src/Avalonia.Base/Input/InputElement.cs | 18 +++++++++++++++ .../TextBoxTests.cs | 22 +++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/src/Avalonia.Base/Input/InputElement.cs b/src/Avalonia.Base/Input/InputElement.cs index d0130258c3..60d8ef87a3 100644 --- a/src/Avalonia.Base/Input/InputElement.cs +++ b/src/Avalonia.Base/Input/InputElement.cs @@ -199,6 +199,7 @@ namespace Avalonia.Input private bool _isFocusVisible; private bool _isPointerOver; private GestureRecognizerCollection? _gestureRecognizers; + private bool _restoreFocus; /// /// Initializes static members of the class. @@ -442,6 +443,23 @@ namespace Avalonia.Input { SetAndRaise(IsEffectivelyEnabledProperty, ref _isEffectivelyEnabled, value); PseudoClasses.Set(":disabled", !value); + + if (!IsEffectivelyEnabled) + { + if (FocusManager.Instance?.Current == this) + { + _restoreFocus = true; + FocusManager.Instance?.Focus(null); + } + else + { + _restoreFocus = false; + } + } + else if (IsEffectivelyEnabled && _restoreFocus) + { + FocusManager.Instance?.Focus(this); + } } } diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs index 6b9105ccb5..bd6d5d55e2 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs @@ -59,6 +59,28 @@ namespace Avalonia.Controls.UnitTests } } + [Fact] + public void TextBox_Should_Lose_Focus_When_Disabled() + { + using (UnitTestApplication.Start(FocusServices)) + { + var target = new TextBox + { + Template = CreateTemplate() + }; + + target.ApplyTemplate(); + + var root = new TestRoot() { Child = target }; + + target.Focus(); + Assert.True(target.IsFocused); + target.IsEnabled = false; + Assert.False(target.IsFocused); + Assert.False(target.IsEnabled); + } + } + [Fact] public void Opening_Context_Flyout_Does_not_Lose_Selection() { From eeeb7f8110533f813cd5d9e5c555f6873c60c5ad Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Mon, 5 Dec 2022 17:54:35 +0100 Subject: [PATCH 32/87] feat: Enable Rule CA1815 --- .editorconfig | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.editorconfig b/.editorconfig index 238e9887bd..1583d3e469 100644 --- a/.editorconfig +++ b/.editorconfig @@ -144,6 +144,8 @@ dotnet_diagnostic.CS1591.severity = suggestion dotnet_diagnostic.CA1304.severity = warning # CA1802: Use literals where appropriate dotnet_diagnostic.CA1802.severity = warning +# CA1815: Override equals and operator equals on value types +dotnet_diagnostic.CA1815.severity = warning # CA1820: Test for empty strings using string length dotnet_diagnostic.CA1820.severity = warning # CA1821: Remove empty finalizers From 7eac150e20059938f21a913995c9964266bac91a Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Mon, 5 Dec 2022 18:07:34 +0100 Subject: [PATCH 33/87] feat: Address Rule CA1815 --- src/Android/Avalonia.Android/AndroidInputMethod.cs | 2 +- src/Avalonia.Base/Animation/Cue.cs | 7 ++++++- src/Avalonia.Base/Data/BindingValue.cs | 2 +- src/Avalonia.Base/Input/PointerPoint.cs | 4 ++-- src/Avalonia.Base/Input/Raw/RawPointerEventArgs.cs | 2 +- .../Input/TextInput/ITextInputMethodClient.cs | 2 +- src/Avalonia.Base/Logging/ParametrizedLogger.cs | 2 +- src/Avalonia.Base/Matrix.cs | 2 +- src/Avalonia.Base/Media/BoxShadows.cs | 8 +++++++- src/Avalonia.Base/Media/DrawingContext.cs | 2 +- src/Avalonia.Base/Media/FontMetrics.cs | 2 +- src/Avalonia.Base/Media/GlyphMetrics.cs | 2 +- src/Avalonia.Base/Media/GlyphRunMetrics.cs | 2 +- src/Avalonia.Base/Media/TextCollapsingCreateInfo.cs | 2 +- .../Media/TextFormatting/ShapedBuffer.cs | 2 +- .../Media/TextFormatting/TextLineMetrics.cs | 2 +- .../Media/TextFormatting/TextMetrics.cs | 2 +- src/Avalonia.Base/Media/TextFormatting/TextRange.cs | 2 +- .../Media/TextFormatting/TextRunBounds.cs | 2 +- .../Media/TextFormatting/TextShaperOptions.cs | 2 +- .../Media/TextFormatting/Unicode/Codepoint.cs | 2 +- .../Media/TextFormatting/Unicode/Grapheme.cs | 2 +- .../Media/TextFormatting/Unicode/LineBreak.cs | 2 +- src/Avalonia.Base/Media/TextHitTestResult.cs | 2 +- .../Media/Transformation/TransformOperation.cs | 12 ++++++------ .../Media/Transformation/TransformOperations.cs | 2 +- src/Avalonia.Base/Media/UnicodeRange.cs | 4 ++-- src/Avalonia.Base/Platform/IRuntimePlatform.cs | 2 +- .../Rendering/Composition/Transport/BatchStream.cs | 2 +- src/Avalonia.Base/Styling/SelectorMatch.cs | 2 +- src/Avalonia.Base/Utilities/ReadOnlySlice.cs | 2 +- src/Avalonia.Base/Utilities/SmallDictionary.cs | 2 +- src/Avalonia.Base/Utilities/StringTokenizer.cs | 2 +- .../Utilities/SynchronousCompletionAsyncResult.cs | 2 +- src/Avalonia.Base/Utilities/ValueSpan.cs | 2 +- .../AcrylicPlatformCompensationLevels.cs | 2 +- .../Primitives/PopupPositioning/IPopupPositioner.cs | 2 +- src/Avalonia.Controls/Selection/ISelectionModel.cs | 2 +- src/Avalonia.Controls/Selection/SelectionModel.cs | 2 +- src/Avalonia.Controls/TextBlock.cs | 2 +- src/Avalonia.FreeDesktop/IX11InputMethod.cs | 2 ++ src/Avalonia.OpenGL/GlVersion.cs | 2 +- src/Avalonia.X11/X11Structs.cs | 4 +++- src/Windows/Avalonia.Direct2D1/OptionalDispose.cs | 2 +- src/Windows/Avalonia.Win32/WinRT/WinRTColor.cs | 2 +- 45 files changed, 66 insertions(+), 51 deletions(-) diff --git a/src/Android/Avalonia.Android/AndroidInputMethod.cs b/src/Android/Avalonia.Android/AndroidInputMethod.cs index 8d56086470..c885a7768c 100644 --- a/src/Android/Avalonia.Android/AndroidInputMethod.cs +++ b/src/Android/Avalonia.Android/AndroidInputMethod.cs @@ -167,7 +167,7 @@ namespace Avalonia.Android } } - public readonly struct ComposingRegion + public readonly record struct ComposingRegion { private readonly int _start = -1; private readonly int _end = -1; diff --git a/src/Avalonia.Base/Animation/Cue.cs b/src/Avalonia.Base/Animation/Cue.cs index 6578148b07..934ae16762 100644 --- a/src/Avalonia.Base/Animation/Cue.cs +++ b/src/Avalonia.Base/Animation/Cue.cs @@ -8,7 +8,7 @@ namespace Avalonia.Animation /// Determines the time index for a . /// [TypeConverter(typeof(CueTypeConverter))] - public readonly struct Cue : IEquatable, IEquatable + public readonly record struct Cue : IEquatable, IEquatable { /// /// The normalized percent value, ranging from 0.0 to 1.0 @@ -68,6 +68,11 @@ namespace Avalonia.Animation { return CueValue == other; } + + public override int GetHashCode() + { + return CueValue.GetHashCode(); + } } public class CueTypeConverter : TypeConverter diff --git a/src/Avalonia.Base/Data/BindingValue.cs b/src/Avalonia.Base/Data/BindingValue.cs index 4e07ebf445..3bc172f596 100644 --- a/src/Avalonia.Base/Data/BindingValue.cs +++ b/src/Avalonia.Base/Data/BindingValue.cs @@ -80,7 +80,7 @@ namespace Avalonia.Data /// - For an unset value, use or simply `default` /// - For other types, call one of the static factory methods /// - public readonly struct BindingValue + public readonly record struct BindingValue { private readonly T _value; diff --git a/src/Avalonia.Base/Input/PointerPoint.cs b/src/Avalonia.Base/Input/PointerPoint.cs index c51f286053..bd36b73b26 100644 --- a/src/Avalonia.Base/Input/PointerPoint.cs +++ b/src/Avalonia.Base/Input/PointerPoint.cs @@ -5,7 +5,7 @@ namespace Avalonia.Input /// /// Provides basic properties for the input pointer associated with a single mouse, pen/stylus, or touch contact. /// - public struct PointerPoint + public record struct PointerPoint { public PointerPoint(IPointer pointer, Point position, PointerPointProperties properties) { @@ -33,7 +33,7 @@ namespace Avalonia.Input /// /// Provides extended properties for a PointerPoint object. /// - public struct PointerPointProperties + public record struct PointerPointProperties { /// /// Gets a value that indicates whether the pointer input was triggered by the primary action mode of an input device. diff --git a/src/Avalonia.Base/Input/Raw/RawPointerEventArgs.cs b/src/Avalonia.Base/Input/Raw/RawPointerEventArgs.cs index b8f6f99ae8..854dd4b83b 100644 --- a/src/Avalonia.Base/Input/Raw/RawPointerEventArgs.cs +++ b/src/Avalonia.Base/Input/Raw/RawPointerEventArgs.cs @@ -130,7 +130,7 @@ namespace Avalonia.Input.Raw internal IInputElement? InputHitTestResult { get; set; } } - public struct RawPointerPoint + public record struct RawPointerPoint { /// /// Pointer position, in client DIPs. diff --git a/src/Avalonia.Base/Input/TextInput/ITextInputMethodClient.cs b/src/Avalonia.Base/Input/TextInput/ITextInputMethodClient.cs index 4a60f8a046..531cf3c704 100644 --- a/src/Avalonia.Base/Input/TextInput/ITextInputMethodClient.cs +++ b/src/Avalonia.Base/Input/TextInput/ITextInputMethodClient.cs @@ -46,7 +46,7 @@ namespace Avalonia.Input.TextInput void SelectInSurroundingText(int start, int end); } - public struct TextInputMethodSurroundingText + public record struct TextInputMethodSurroundingText { public string Text { get; set; } public int CursorOffset { get; set; } diff --git a/src/Avalonia.Base/Logging/ParametrizedLogger.cs b/src/Avalonia.Base/Logging/ParametrizedLogger.cs index 6b7331b504..a82af41a75 100644 --- a/src/Avalonia.Base/Logging/ParametrizedLogger.cs +++ b/src/Avalonia.Base/Logging/ParametrizedLogger.cs @@ -5,7 +5,7 @@ namespace Avalonia.Logging /// /// Logger sink parametrized for given logging level. /// - public readonly struct ParametrizedLogger + public readonly record struct ParametrizedLogger { private readonly ILogSink _sink; private readonly LogEventLevel _level; diff --git a/src/Avalonia.Base/Matrix.cs b/src/Avalonia.Base/Matrix.cs index df6439b1b3..a72710ffd4 100644 --- a/src/Avalonia.Base/Matrix.cs +++ b/src/Avalonia.Base/Matrix.cs @@ -571,7 +571,7 @@ namespace Avalonia return true; } - public struct Decomposed + public record struct Decomposed { public Vector Translate; public Vector Scale; diff --git a/src/Avalonia.Base/Media/BoxShadows.cs b/src/Avalonia.Base/Media/BoxShadows.cs index ab2694389f..9c20b9541d 100644 --- a/src/Avalonia.Base/Media/BoxShadows.cs +++ b/src/Avalonia.Base/Media/BoxShadows.cs @@ -62,7 +62,7 @@ namespace Avalonia.Media } [EditorBrowsable(EditorBrowsableState.Never)] - public struct BoxShadowsEnumerator + public record struct BoxShadowsEnumerator { private int _index; private BoxShadows _shadows; @@ -149,5 +149,11 @@ namespace Avalonia.Media return hashCode; } } + + public static bool operator ==(BoxShadows left, BoxShadows right) => + left.Equals(right); + + public static bool operator !=(BoxShadows left, BoxShadows right) => + !(left == right); } } diff --git a/src/Avalonia.Base/Media/DrawingContext.cs b/src/Avalonia.Base/Media/DrawingContext.cs index d0f3b9e21a..eabd7c8274 100644 --- a/src/Avalonia.Base/Media/DrawingContext.cs +++ b/src/Avalonia.Base/Media/DrawingContext.cs @@ -261,7 +261,7 @@ namespace Avalonia.Media DrawRectangle(brush, null, rect, cornerRadius, cornerRadius); } - public readonly struct PushedState : IDisposable + public readonly record struct PushedState : IDisposable { private readonly int _level; private readonly DrawingContext _context; diff --git a/src/Avalonia.Base/Media/FontMetrics.cs b/src/Avalonia.Base/Media/FontMetrics.cs index 1cd01675db..6d952a6b93 100644 --- a/src/Avalonia.Base/Media/FontMetrics.cs +++ b/src/Avalonia.Base/Media/FontMetrics.cs @@ -3,7 +3,7 @@ /// /// The font metrics is holding information about a font's ascent, descent, etc. in design em units. /// - public readonly struct FontMetrics + public readonly record struct FontMetrics { /// /// Gets the font design units per em. diff --git a/src/Avalonia.Base/Media/GlyphMetrics.cs b/src/Avalonia.Base/Media/GlyphMetrics.cs index 2ee1f87d38..e9b5a112ac 100644 --- a/src/Avalonia.Base/Media/GlyphMetrics.cs +++ b/src/Avalonia.Base/Media/GlyphMetrics.cs @@ -1,6 +1,6 @@ namespace Avalonia.Media; -public readonly struct GlyphMetrics +public readonly record struct GlyphMetrics { /// /// Distance from the x-origin to the left extremum of the glyph. diff --git a/src/Avalonia.Base/Media/GlyphRunMetrics.cs b/src/Avalonia.Base/Media/GlyphRunMetrics.cs index a8698a7d82..4c22b28093 100644 --- a/src/Avalonia.Base/Media/GlyphRunMetrics.cs +++ b/src/Avalonia.Base/Media/GlyphRunMetrics.cs @@ -1,6 +1,6 @@ namespace Avalonia.Media { - public readonly struct GlyphRunMetrics + public readonly record struct GlyphRunMetrics { public GlyphRunMetrics(double width, double widthIncludingTrailingWhitespace, int trailingWhitespaceLength, int newlineLength, double height) diff --git a/src/Avalonia.Base/Media/TextCollapsingCreateInfo.cs b/src/Avalonia.Base/Media/TextCollapsingCreateInfo.cs index 78f15b724a..40ba613717 100644 --- a/src/Avalonia.Base/Media/TextCollapsingCreateInfo.cs +++ b/src/Avalonia.Base/Media/TextCollapsingCreateInfo.cs @@ -2,7 +2,7 @@ namespace Avalonia.Media { - public readonly struct TextCollapsingCreateInfo + public readonly record struct TextCollapsingCreateInfo { public readonly double Width; public readonly TextRunProperties TextRunProperties; diff --git a/src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs b/src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs index 85924a3d32..8d6756c585 100644 --- a/src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs +++ b/src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs @@ -255,7 +255,7 @@ namespace Avalonia.Media.TextFormatting } } - public readonly struct GlyphInfo + public readonly record struct GlyphInfo { public GlyphInfo(ushort glyphIndex, int glyphCluster, double glyphAdvance = 0, Vector glyphOffset = default) { diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineMetrics.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineMetrics.cs index 1799c9d3db..16a8a46574 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineMetrics.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineMetrics.cs @@ -4,7 +4,7 @@ /// Represents a metric for a objects, /// that holds information about ascent, descent, line gap, size and origin of the text line. /// - public readonly struct TextLineMetrics + public readonly record struct TextLineMetrics { public TextLineMetrics(bool hasOverflowed, double height, int newLineLength, double start, double textBaseline, int trailingWhitespaceLength, double width, diff --git a/src/Avalonia.Base/Media/TextFormatting/TextMetrics.cs b/src/Avalonia.Base/Media/TextFormatting/TextMetrics.cs index 0382e66b5a..a036599d4c 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextMetrics.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextMetrics.cs @@ -3,7 +3,7 @@ /// /// A metric that holds information about text specific measurements. /// - public readonly struct TextMetrics + public readonly record struct TextMetrics { public TextMetrics(Typeface typeface, double fontRenderingEmSize) { diff --git a/src/Avalonia.Base/Media/TextFormatting/TextRange.cs b/src/Avalonia.Base/Media/TextFormatting/TextRange.cs index 1177c758f4..e8bab55aff 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextRange.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextRange.cs @@ -5,7 +5,7 @@ namespace Avalonia.Media.TextFormatting /// /// References a portion of a text buffer. /// - public readonly struct TextRange + public readonly record struct TextRange { public TextRange(int start, int length) { diff --git a/src/Avalonia.Base/Media/TextFormatting/TextRunBounds.cs b/src/Avalonia.Base/Media/TextFormatting/TextRunBounds.cs index bdc7a1ca89..707c9048dc 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextRunBounds.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextRunBounds.cs @@ -3,7 +3,7 @@ /// /// The bounding rectangle of text run /// - public readonly struct TextRunBounds + public readonly record struct TextRunBounds { /// /// Constructing TextRunBounds diff --git a/src/Avalonia.Base/Media/TextFormatting/TextShaperOptions.cs b/src/Avalonia.Base/Media/TextFormatting/TextShaperOptions.cs index 80bbbcdbfe..610fc3dbc9 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextShaperOptions.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextShaperOptions.cs @@ -5,7 +5,7 @@ namespace Avalonia.Media.TextFormatting /// /// Options to customize text shaping. /// - public readonly struct TextShaperOptions + public readonly record struct TextShaperOptions { public TextShaperOptions( IGlyphTypeface typeface, diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs index de40839853..3059be2182 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs @@ -4,7 +4,7 @@ using Avalonia.Utilities; namespace Avalonia.Media.TextFormatting.Unicode { - public readonly struct Codepoint + public readonly record struct Codepoint { private readonly uint _value; diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/Grapheme.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/Grapheme.cs index f268340eb9..4aaa62154c 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/Grapheme.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/Grapheme.cs @@ -5,7 +5,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// /// Represents the smallest unit of a writing system of any given language. /// - public readonly struct Grapheme + public readonly record struct Grapheme { public Grapheme(Codepoint firstCodepoint, ReadOnlySlice text) { diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreak.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreak.cs index 59c4df0a2e..0e470341f3 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreak.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreak.cs @@ -24,7 +24,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// Information about a potential line break position /// [DebuggerDisplay("{PositionMeasure}/{PositionWrap} @ {Required}")] - public readonly struct LineBreak + public readonly record struct LineBreak { /// /// Constructor diff --git a/src/Avalonia.Base/Media/TextHitTestResult.cs b/src/Avalonia.Base/Media/TextHitTestResult.cs index c8922f06c8..d19adc2807 100644 --- a/src/Avalonia.Base/Media/TextHitTestResult.cs +++ b/src/Avalonia.Base/Media/TextHitTestResult.cs @@ -5,7 +5,7 @@ namespace Avalonia.Media /// /// Holds a hit test result from a . /// - public readonly struct TextHitTestResult + public readonly record struct TextHitTestResult { public TextHitTestResult(CharacterHit characterHit, int textPosition, bool isInside, bool isTrailing) { diff --git a/src/Avalonia.Base/Media/Transformation/TransformOperation.cs b/src/Avalonia.Base/Media/Transformation/TransformOperation.cs index a8c0fe9b12..542d71963d 100644 --- a/src/Avalonia.Base/Media/Transformation/TransformOperation.cs +++ b/src/Avalonia.Base/Media/Transformation/TransformOperation.cs @@ -5,7 +5,7 @@ namespace Avalonia.Media.Transformation /// /// Represents a single primitive transform (like translation, rotation, scale, etc.). /// - public struct TransformOperation + public record struct TransformOperation { public OperationType Type; public Matrix Matrix; @@ -196,7 +196,7 @@ namespace Avalonia.Media.Transformation } [StructLayout(LayoutKind.Explicit)] - public struct DataLayout + public record struct DataLayout { [FieldOffset(0)] public SkewLayout Skew; @@ -206,25 +206,25 @@ namespace Avalonia.Media.Transformation [FieldOffset(0)] public RotateLayout Rotate; - public struct SkewLayout + public record struct SkewLayout { public double X; public double Y; } - public struct ScaleLayout + public record struct ScaleLayout { public double X; public double Y; } - public struct TranslateLayout + public record struct TranslateLayout { public double X; public double Y; } - public struct RotateLayout + public record struct RotateLayout { public double Angle; } diff --git a/src/Avalonia.Base/Media/Transformation/TransformOperations.cs b/src/Avalonia.Base/Media/Transformation/TransformOperations.cs index 334bb93562..46ca41386a 100644 --- a/src/Avalonia.Base/Media/Transformation/TransformOperations.cs +++ b/src/Avalonia.Base/Media/Transformation/TransformOperations.cs @@ -165,7 +165,7 @@ namespace Avalonia.Media.Transformation return Math.Max(from._operations.Count, to._operations.Count); } - public readonly struct Builder + public readonly record struct Builder { private readonly List _operations; diff --git a/src/Avalonia.Base/Media/UnicodeRange.cs b/src/Avalonia.Base/Media/UnicodeRange.cs index 344b85bae9..f87f38b444 100644 --- a/src/Avalonia.Base/Media/UnicodeRange.cs +++ b/src/Avalonia.Base/Media/UnicodeRange.cs @@ -7,7 +7,7 @@ namespace Avalonia.Media /// /// The descripes a set of Unicode characters. /// - public readonly struct UnicodeRange + public readonly record struct UnicodeRange { public readonly static UnicodeRange Default = Parse("0-10FFFD"); @@ -102,7 +102,7 @@ namespace Avalonia.Media } } - public readonly struct UnicodeRangeSegment + public readonly record struct UnicodeRangeSegment { private static Regex s_regex = new Regex(@"^(?:[uU]\+)?(?:([0-9a-fA-F](?:[0-9a-fA-F?]{1,5})?))$"); diff --git a/src/Avalonia.Base/Platform/IRuntimePlatform.cs b/src/Avalonia.Base/Platform/IRuntimePlatform.cs index 3f8983479f..91d2a1e0cf 100644 --- a/src/Avalonia.Base/Platform/IRuntimePlatform.cs +++ b/src/Avalonia.Base/Platform/IRuntimePlatform.cs @@ -21,7 +21,7 @@ namespace Avalonia.Platform } [Unstable] - public struct RuntimePlatformInfo + public record struct RuntimePlatformInfo { public OperatingSystemType OperatingSystem { get; set; } diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/BatchStream.cs b/src/Avalonia.Base/Rendering/Composition/Transport/BatchStream.cs index 8b68900994..a3cad3cebd 100644 --- a/src/Avalonia.Base/Rendering/Composition/Transport/BatchStream.cs +++ b/src/Avalonia.Base/Rendering/Composition/Transport/BatchStream.cs @@ -21,7 +21,7 @@ internal class BatchStreamData public Queue> Structs { get; } = new(); } -public struct BatchStreamSegment +public record struct BatchStreamSegment { public TData Data { get; set; } public int ElementCount { get; set; } diff --git a/src/Avalonia.Base/Styling/SelectorMatch.cs b/src/Avalonia.Base/Styling/SelectorMatch.cs index 26b525347e..2eac04301a 100644 --- a/src/Avalonia.Base/Styling/SelectorMatch.cs +++ b/src/Avalonia.Base/Styling/SelectorMatch.cs @@ -43,7 +43,7 @@ namespace Avalonia.Styling /// A selector match describes whether and how a matches a control, and /// in addition whether the selector can ever match a control of the same type. /// - public readonly struct SelectorMatch + public readonly record struct SelectorMatch { /// /// A selector match with the result of . diff --git a/src/Avalonia.Base/Utilities/ReadOnlySlice.cs b/src/Avalonia.Base/Utilities/ReadOnlySlice.cs index 583a3139b9..cb0cd1f0de 100644 --- a/src/Avalonia.Base/Utilities/ReadOnlySlice.cs +++ b/src/Avalonia.Base/Utilities/ReadOnlySlice.cs @@ -11,7 +11,7 @@ namespace Avalonia.Utilities /// /// The type of elements in the slice. [DebuggerTypeProxy(typeof(ReadOnlySlice<>.ReadOnlySliceDebugView))] - public readonly struct ReadOnlySlice : IReadOnlyList where T : struct + public readonly record struct ReadOnlySlice : IReadOnlyList where T : struct { private readonly int _bufferOffset; diff --git a/src/Avalonia.Base/Utilities/SmallDictionary.cs b/src/Avalonia.Base/Utilities/SmallDictionary.cs index 7d6a21c136..8aaf2200df 100644 --- a/src/Avalonia.Base/Utilities/SmallDictionary.cs +++ b/src/Avalonia.Base/Utilities/SmallDictionary.cs @@ -5,7 +5,7 @@ using System.Diagnostics.CodeAnalysis; namespace Avalonia.Utilities; -public struct InlineDictionary : IEnumerable> where TKey : class where TValue : class +public record struct InlineDictionary : IEnumerable> where TKey : class where TValue : class { object? _data; TValue? _value; diff --git a/src/Avalonia.Base/Utilities/StringTokenizer.cs b/src/Avalonia.Base/Utilities/StringTokenizer.cs index 748eb09209..726c9735ef 100644 --- a/src/Avalonia.Base/Utilities/StringTokenizer.cs +++ b/src/Avalonia.Base/Utilities/StringTokenizer.cs @@ -8,7 +8,7 @@ namespace Avalonia.Utilities #if !BUILDTASK public #endif - struct StringTokenizer : IDisposable + record struct StringTokenizer : IDisposable { private const char DefaultSeparatorChar = ','; diff --git a/src/Avalonia.Base/Utilities/SynchronousCompletionAsyncResult.cs b/src/Avalonia.Base/Utilities/SynchronousCompletionAsyncResult.cs index fb8d3ed224..e574462e2c 100644 --- a/src/Avalonia.Base/Utilities/SynchronousCompletionAsyncResult.cs +++ b/src/Avalonia.Base/Utilities/SynchronousCompletionAsyncResult.cs @@ -8,7 +8,7 @@ namespace Avalonia.Utilities /// A task-like operation that is guaranteed to finish continuations synchronously, /// can be used for parametrized one-shot events /// - public struct SynchronousCompletionAsyncResult : INotifyCompletion + public record struct SynchronousCompletionAsyncResult : INotifyCompletion { private readonly SynchronousCompletionAsyncResultSource? _source; private readonly T? _result; diff --git a/src/Avalonia.Base/Utilities/ValueSpan.cs b/src/Avalonia.Base/Utilities/ValueSpan.cs index 7a10d865ef..4c2de2776d 100644 --- a/src/Avalonia.Base/Utilities/ValueSpan.cs +++ b/src/Avalonia.Base/Utilities/ValueSpan.cs @@ -3,7 +3,7 @@ /// /// Pairing of value and positions sharing that value. /// - public readonly struct ValueSpan + public readonly record struct ValueSpan { public ValueSpan(int start, int length, T value) { diff --git a/src/Avalonia.Controls/AcrylicPlatformCompensationLevels.cs b/src/Avalonia.Controls/AcrylicPlatformCompensationLevels.cs index e1f1c4029e..2402de56ee 100644 --- a/src/Avalonia.Controls/AcrylicPlatformCompensationLevels.cs +++ b/src/Avalonia.Controls/AcrylicPlatformCompensationLevels.cs @@ -5,7 +5,7 @@ /// It controls the base opacity level of the 'tracing paper' layer that compensates /// for low blur radius. /// - public struct AcrylicPlatformCompensationLevels + public record struct AcrylicPlatformCompensationLevels { public AcrylicPlatformCompensationLevels(double transparent, double blurred, double acrylic) { diff --git a/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs b/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs index 17dfec118f..2e70947457 100644 --- a/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs +++ b/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs @@ -64,7 +64,7 @@ namespace Avalonia.Controls.Primitives.PopupPositioning /// surface. /// [Unstable] - public struct PopupPositionerParameters + public record struct PopupPositionerParameters { private PopupGravity _gravity; private PopupAnchor _anchor; diff --git a/src/Avalonia.Controls/Selection/ISelectionModel.cs b/src/Avalonia.Controls/Selection/ISelectionModel.cs index 3f4ae48263..4c2a355bb5 100644 --- a/src/Avalonia.Controls/Selection/ISelectionModel.cs +++ b/src/Avalonia.Controls/Selection/ISelectionModel.cs @@ -39,7 +39,7 @@ namespace Avalonia.Controls.Selection return new BatchUpdateOperation(model); } - public struct BatchUpdateOperation : IDisposable + public record struct BatchUpdateOperation : IDisposable { private readonly ISelectionModel _owner; private bool _isDisposed; diff --git a/src/Avalonia.Controls/Selection/SelectionModel.cs b/src/Avalonia.Controls/Selection/SelectionModel.cs index 154f1868f3..9bbddfcbb2 100644 --- a/src/Avalonia.Controls/Selection/SelectionModel.cs +++ b/src/Avalonia.Controls/Selection/SelectionModel.cs @@ -738,7 +738,7 @@ namespace Avalonia.Controls.Selection } } - public struct BatchUpdateOperation : IDisposable + public record struct BatchUpdateOperation : IDisposable { private readonly SelectionModel _owner; private bool _isDisposed; diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index c8e05e5cb3..483d1c0ddc 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -827,7 +827,7 @@ namespace Avalonia.Controls InvalidateTextLayout(); } - protected readonly struct SimpleTextSource : ITextSource + protected readonly record struct SimpleTextSource : ITextSource { private readonly ReadOnlySlice _text; private readonly TextRunProperties _defaultProperties; diff --git a/src/Avalonia.FreeDesktop/IX11InputMethod.cs b/src/Avalonia.FreeDesktop/IX11InputMethod.cs index 5d91118978..9fa9c1809e 100644 --- a/src/Avalonia.FreeDesktop/IX11InputMethod.cs +++ b/src/Avalonia.FreeDesktop/IX11InputMethod.cs @@ -11,7 +11,9 @@ namespace Avalonia.FreeDesktop (ITextInputMethodImpl method, IX11InputMethodControl control) CreateClient(IntPtr xid); } +#pragma warning disable CA1815 // Override equals and operator equals on value types public struct X11InputMethodForwardedKey +#pragma warning restore CA1815 // Override equals and operator equals on value types { public int KeyVal { get; set; } public KeyModifiers Modifiers { get; set; } diff --git a/src/Avalonia.OpenGL/GlVersion.cs b/src/Avalonia.OpenGL/GlVersion.cs index 042ff4c2f0..16ed18e45a 100644 --- a/src/Avalonia.OpenGL/GlVersion.cs +++ b/src/Avalonia.OpenGL/GlVersion.cs @@ -6,7 +6,7 @@ namespace Avalonia.OpenGL OpenGLES } - public struct GlVersion + public record struct GlVersion { public GlProfileType Type { get; } public int Major { get; } diff --git a/src/Avalonia.X11/X11Structs.cs b/src/Avalonia.X11/X11Structs.cs index 7e46606c36..26515762b4 100644 --- a/src/Avalonia.X11/X11Structs.cs +++ b/src/Avalonia.X11/X11Structs.cs @@ -1768,7 +1768,9 @@ namespace Avalonia.X11 { } [StructLayout(LayoutKind.Sequential)] +#pragma warning disable CA1815 // Override equals and operator equals on value types public unsafe struct XImage +#pragma warning restore CA1815 // Override equals and operator equals on value types { public int width, height; /* size of image */ public int xoffset; /* number of pixels offset in X direction */ @@ -1799,7 +1801,7 @@ namespace Avalonia.X11 { internal IntPtr green_mask; internal IntPtr blue_mask; internal int colormap_size; - internal int bits_per_rgb; + internal int bits_per_rgb; } internal enum XIMFeedback diff --git a/src/Windows/Avalonia.Direct2D1/OptionalDispose.cs b/src/Windows/Avalonia.Direct2D1/OptionalDispose.cs index e302e71102..1cdf7661df 100644 --- a/src/Windows/Avalonia.Direct2D1/OptionalDispose.cs +++ b/src/Windows/Avalonia.Direct2D1/OptionalDispose.cs @@ -2,7 +2,7 @@ namespace Avalonia.Direct2D1 { - public readonly struct OptionalDispose : IDisposable where T : IDisposable + public readonly record struct OptionalDispose : IDisposable where T : IDisposable { private readonly bool _dispose; diff --git a/src/Windows/Avalonia.Win32/WinRT/WinRTColor.cs b/src/Windows/Avalonia.Win32/WinRT/WinRTColor.cs index 786d698daa..35f0737d5f 100644 --- a/src/Windows/Avalonia.Win32/WinRT/WinRTColor.cs +++ b/src/Windows/Avalonia.Win32/WinRT/WinRTColor.cs @@ -3,7 +3,7 @@ namespace Avalonia.Win32.WinRT { [StructLayout(LayoutKind.Sequential, Pack = 1)] - public struct WinRTColor + public record struct WinRTColor { public byte A; public byte R; From 6bd6b26a99d86b35b5a0e15594142688e8cb11c3 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Mon, 5 Dec 2022 18:33:32 +0100 Subject: [PATCH 34/87] fix: Missing RegEx RegexOptions.Compiled --- src/Avalonia.Base/Media/UnicodeRange.cs | 2 +- .../Internal/ManagedFileChooserFilterViewModel.cs | 2 +- src/Avalonia.Remote.Protocol/MetsysBson.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Base/Media/UnicodeRange.cs b/src/Avalonia.Base/Media/UnicodeRange.cs index 344b85bae9..bdad870878 100644 --- a/src/Avalonia.Base/Media/UnicodeRange.cs +++ b/src/Avalonia.Base/Media/UnicodeRange.cs @@ -104,7 +104,7 @@ namespace Avalonia.Media public readonly struct UnicodeRangeSegment { - private static Regex s_regex = new Regex(@"^(?:[uU]\+)?(?:([0-9a-fA-F](?:[0-9a-fA-F?]{1,5})?))$"); + private static Regex s_regex = new Regex(@"^(?:[uU]\+)?(?:([0-9a-fA-F](?:[0-9a-fA-F?]{1,5})?))$", RegexOptions.Compiled); public UnicodeRangeSegment(int start, int end) { diff --git a/src/Avalonia.Dialogs/Internal/ManagedFileChooserFilterViewModel.cs b/src/Avalonia.Dialogs/Internal/ManagedFileChooserFilterViewModel.cs index 8389a25386..6da7c68a75 100644 --- a/src/Avalonia.Dialogs/Internal/ManagedFileChooserFilterViewModel.cs +++ b/src/Avalonia.Dialogs/Internal/ManagedFileChooserFilterViewModel.cs @@ -19,7 +19,7 @@ namespace Avalonia.Dialogs.Internal } _patterns = filter.Patterns? - .Select(e => new Regex(Regex.Escape(e).Replace(@"\*", ".*").Replace(@"\?", "."), RegexOptions.Singleline | RegexOptions.IgnoreCase)) + .Select(e => new Regex(Regex.Escape(e).Replace(@"\*", ".*").Replace(@"\?", "."), RegexOptions.Singleline | RegexOptions.IgnoreCase | RegexOptions.Compiled)) .ToArray(); } diff --git a/src/Avalonia.Remote.Protocol/MetsysBson.cs b/src/Avalonia.Remote.Protocol/MetsysBson.cs index d011a963be..2ebeda8403 100644 --- a/src/Avalonia.Remote.Protocol/MetsysBson.cs +++ b/src/Avalonia.Remote.Protocol/MetsysBson.cs @@ -1379,7 +1379,7 @@ namespace Metsys.Bson if (optionsString.Contains('w')) options = options | RegexOptions.IgnorePatternWhitespace; if (optionsString.Contains('x')) options = options | RegexOptions.ExplicitCapture; - return new Regex(pattern, options); + return new Regex(pattern, options | RegexOptions.Compiled); } private Types ReadType() From 8b490152d49b1cc310faf86a2aebdc52fd5f1760 Mon Sep 17 00:00:00 2001 From: dif-sam <41672086+dif-sam@users.noreply.github.com> Date: Mon, 5 Dec 2022 23:49:13 +0400 Subject: [PATCH 35/87] Increase GenerateElement performance Use ability of the IRecyclingDataTemplate to speed up performance, when CellTemplate is an instance of DataTemplate type. --- src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs index 3ec78d6d6a..3649ec4308 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs @@ -64,9 +64,11 @@ namespace Avalonia.Controls protected override Control GenerateElement(DataGridCell cell, object dataItem) { - if(CellTemplate != null) + if (CellTemplate != null) { - return CellTemplate.Build(dataItem); + return (CellTemplate is IRecyclingDataTemplate recyclingDataTemplate) + ? recyclingDataTemplate.Build(dataItem, cell.Content as IControl) + : CellTemplate.Build(dataItem); } if (Design.IsDesignMode) { From ef535eeea6de0492c4ed342d8fa26a1090ccd8c7 Mon Sep 17 00:00:00 2001 From: dif-sam <41672086+dif-sam@users.noreply.github.com> Date: Tue, 6 Dec 2022 00:12:14 +0400 Subject: [PATCH 36/87] Switch IControl to Control --- src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs index 3649ec4308..24ae358dcc 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs @@ -67,7 +67,7 @@ namespace Avalonia.Controls if (CellTemplate != null) { return (CellTemplate is IRecyclingDataTemplate recyclingDataTemplate) - ? recyclingDataTemplate.Build(dataItem, cell.Content as IControl) + ? recyclingDataTemplate.Build(dataItem, cell.Content as Control) : CellTemplate.Build(dataItem); } if (Design.IsDesignMode) From 5a1390721f28753653887f8954e18ea822f21de5 Mon Sep 17 00:00:00 2001 From: Kevin Ivarsen Date: Mon, 5 Dec 2022 23:57:59 -0800 Subject: [PATCH 37/87] Better fix for #9633: assign keys to correct state --- src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml b/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml index fb4335f4de..dd8575c989 100644 --- a/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml +++ b/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml @@ -6,8 +6,8 @@ M1875 1011l-787 787v-1798h-128v1798l-787 -787l-90 90l941 941l941 -941z M1965 947l-941 -941l-941 941l90 90l787 -787v1798h128v-1798l787 787z - M515 93l930 931l-930 931l90 90l1022 -1021l-1022 -1021z - M109 486 19 576 1024 1581 2029 576 1939 486 1024 1401z + M515 93l930 931l-930 931l90 90l1022 -1021l-1022 -1021z + M109 486 19 576 1024 1581 2029 576 1939 486 1024 1401z @@ -379,7 +379,7 @@ HorizontalAlignment="Center" VerticalAlignment="Center"> @@ -387,7 +387,7 @@ From eaf2ce38a4780123d4d06931068926353f31824b Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Tue, 6 Dec 2022 12:15:16 +0600 Subject: [PATCH 38/87] Context management --- .../Avalonia.Android/AndroidPlatform.cs | 9 +- .../OpenGL/GlPlatformSurface.cs | 32 - .../Avalonia.Android/OpenGL/GlRenderTarget.cs | 30 - .../Platform/SkiaPlatform/TopLevelImpl.cs | 18 +- .../Media/Imaging/RenderTargetBitmap.cs | 2 + .../Platform/IOptionalFeatureProvider.cs | 18 + src/Avalonia.Base/Platform/IPlatformGpu.cs | 14 +- .../Platform/IPlatformRenderInterface.cs | 29 +- src/Avalonia.Base/Platform/IRenderTarget.cs | 10 +- .../Composition/CompositingRenderer.cs | 9 +- .../Composition/Compositor.Factories.cs | 7 +- .../Rendering/Composition/Compositor.cs | 33 +- .../Server/ServerCompositionTarget.cs | 16 +- .../Composition/Server/ServerCompositor.cs | 54 +- .../Rendering/DeferredRenderer.cs | 53 +- src/Avalonia.Base/Rendering/IRenderRoot.cs | 6 - src/Avalonia.Base/Rendering/IRenderer.cs | 6 + .../Rendering/ImmediateRenderer.cs | 26 +- .../Rendering/OwnedDisposable.cs | 24 + .../PlatformRenderInterfaceContextManager.cs | 66 + src/Avalonia.Base/Rendering/RenderLoop.cs | 27 +- .../Rendering/SceneGraph/Scene.cs | 2 + src/Avalonia.Controls/AppBuilderBase.cs | 2 +- .../Offscreen/OffscreenTopLevelImpl.cs | 4 +- src/Avalonia.Controls/TopLevel.cs | 12 +- src/Avalonia.DesignerSupport/Remote/Stubs.cs | 5 +- .../HeadlessPlatformRenderInterface.cs | 14 +- src/Avalonia.Headless/HeadlessWindowImpl.cs | 5 +- src/Avalonia.Native/AvaloniaNativePlatform.cs | 12 +- .../AvaloniaNativePlatformOpenGlInterface.cs | 60 +- src/Avalonia.Native/PopupImpl.cs | 7 +- src/Avalonia.Native/WindowImpl.cs | 7 +- src/Avalonia.Native/WindowImplBase.cs | 16 +- .../Angle/AngleWin32EglDisplay.cs | 95 -- .../Controls/OpenGlControlBase.cs | 118 +- src/Avalonia.OpenGL/Egl/EglConsts.cs | 5 + src/Avalonia.OpenGL/Egl/EglContext.cs | 69 +- src/Avalonia.OpenGL/Egl/EglDisplay.cs | 273 ++-- src/Avalonia.OpenGL/Egl/EglDisplayOptions.cs | 30 + src/Avalonia.OpenGL/Egl/EglDisplayUtils.cs | 135 ++ .../Egl/EglGlPlatformSurface.cs | 39 +- .../Egl/EglGlPlatformSurfaceBase.cs | 49 +- src/Avalonia.OpenGL/Egl/EglInterface.cs | 3 + .../Egl/EglPlatformGraphics.cs | 48 + .../Egl/EglPlatformOpenGlInterface.cs | 77 -- src/Avalonia.OpenGL/Egl/EglSurface.cs | 6 +- src/Avalonia.OpenGL/IGlContext.cs | 12 +- .../IOpenGlAwarePlatformRenderInterface.cs | 9 - ...ureSharingRenderInterfaceContextFeature.cs | 13 + .../IPlatformOpenGlInterface.cs | 15 - src/Avalonia.OpenGL/Imaging/OpenGlBitmap.cs | 16 +- src/Avalonia.OpenGL/OpenGlException.cs | 11 +- .../Surfaces/IGlPlatformSurface.cs | 2 +- src/Avalonia.X11/Glx/GlxContext.cs | 10 + src/Avalonia.X11/Glx/GlxDisplay.cs | 4 +- src/Avalonia.X11/Glx/GlxGlPlatformSurface.cs | 10 +- src/Avalonia.X11/Glx/GlxPlatformFeature.cs | 20 +- src/Avalonia.X11/X11CursorFactory.cs | 3 +- src/Avalonia.X11/X11IconLoader.cs | 3 +- src/Avalonia.X11/X11ImmediateRendererProxy.cs | 11 +- src/Avalonia.X11/X11Platform.cs | 11 +- src/Avalonia.X11/X11Window.cs | 20 +- src/Browser/Avalonia.Browser/AvaloniaView.cs | 3 +- .../BrowserSingleViewLifetime.cs | 3 +- .../Avalonia.Browser/BrowserTopLevelImpl.cs | 3 +- .../Avalonia.Browser/Skia/BrowserSkiaGpu.cs | 26 + .../Avalonia.Browser/WindowingPlatform.cs | 2 + .../FramebufferToplevelImpl.cs | 4 +- .../LinuxFramebufferPlatform.cs | 4 +- .../Output/DrmOutput.cs | 50 +- .../Output/IGlOutputBackend.cs | 3 +- .../Avalonia.Skia/FramebufferRenderTarget.cs | 2 + src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs | 4 +- .../Gpu/OpenGl/GlRenderTarget.cs | 5 +- .../Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs | 55 +- .../Gpu/OpenGl/OpenGlBitmapImpl.cs | 6 +- .../Avalonia.Skia/Gpu/SkiaGpuRenderTarget.cs | 2 +- .../Avalonia.Skia/PlatformRenderInterface.cs | 57 +- src/Skia/Avalonia.Skia/SkiaBackendContext.cs | 47 + src/Skia/Avalonia.Skia/SkiaOptions.cs | 5 - src/Skia/Avalonia.Skia/SkiaPlatform.cs | 3 +- src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs | 2 + src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs | 3 +- .../Avalonia.Direct2D1/Direct2D1Platform.cs | 22 +- .../ExternalRenderTarget.cs | 2 + .../FramebufferShimRenderTarget.cs | 2 + .../Imaging/D2DRenderTargetBitmapImpl.cs | 2 + .../Imaging/WicRenderTargetBitmapImpl.cs | 2 + .../Avalonia.Direct2D1/RenderTarget.cs | 2 + .../SwapChainRenderTarget.cs | 2 + .../Wpf/WpfTopLevelImpl.cs | 4 +- .../Avalonia.Win32}/AngleOptions.cs | 3 +- .../Avalonia.Win32/Avalonia.Win32.csproj | 4 + .../Avalonia.Win32/DirectX/DirectXEnums.cs | 51 +- .../Avalonia.Win32/DirectX/DirectXStructs.cs | 1108 +---------------- .../DirectX/DirectXUnmanagedMethods.cs | 15 +- .../Avalonia.Win32/DirectX/DxgiConnection.cs | 34 +- .../DirectX/DxgiRenderTarget.cs | 19 +- .../DirectX/DxgiSwapchainWindow.cs | 16 +- .../IDirect3D11TexturePlatformSurface.cs | 26 + .../Avalonia.Win32/DirectX/directx.idl | 186 ++- .../OpenGl/Angle/AngleD3DTextureFeature.cs | 103 ++ .../OpenGl}/Angle/AngleEglInterface.cs | 19 +- .../OpenGl/Angle/AngleWin32EglDisplay.cs | 140 +++ .../Angle/AngleWin32PlatformGraphics.cs | 134 ++ .../Avalonia.Win32/OpenGl/WglContext.cs | 16 + .../OpenGl/WglGlPlatformSurface.cs | 8 +- .../OpenGl/WglPlatformOpenGlInterface.cs | 12 +- src/Windows/Avalonia.Win32/Win32GlManager.cs | 31 +- src/Windows/Avalonia.Win32/Win32Platform.cs | 23 +- .../Composition/WinUICompositedWindow.cs | 109 -- .../Composition/WinUICompositorConnection.cs | 311 ----- .../Composition/WinUiCompositedWindow.cs | 110 ++ .../WinUiCompositedWindowSurface.cs | 225 +++- .../Composition/WinUiCompositionShared.cs | 34 + .../Composition/WinUiCompositionUtils.cs | 101 ++ .../Composition/WinUiCompositorConnection.cs | 151 +++ src/Windows/Avalonia.Win32/WindowImpl.cs | 53 +- src/iOS/Avalonia.iOS/AvaloniaView.cs | 3 +- src/iOS/Avalonia.iOS/EaglDisplay.cs | 57 +- src/iOS/Avalonia.iOS/EaglLayerSurface.cs | 4 +- src/iOS/Avalonia.iOS/Platform.cs | 8 +- .../Rendering/CompositorTestsBase.cs | 2 +- .../Rendering/DeferredRendererTests.cs | 11 + .../DeferredRendererTests_HitTesting.cs | 26 +- .../Rendering/ImmediateRendererTests.cs | 10 +- .../ImmediateRendererTests_HitTesting.cs | 18 +- .../VisualTree/MockRenderInterface.cs | 14 +- .../VisualExtensions_GetVisualsAt.cs | 4 +- tests/Avalonia.Benchmarks/NullRenderer.cs | 3 + .../NullRenderingPlatform.cs | 13 +- tests/Avalonia.LeakTests/ControlTests.cs | 4 +- .../Avalonia.RenderTests/Media/BitmapTests.cs | 5 +- tests/Avalonia.RenderTests/TestBase.cs | 24 +- tests/Avalonia.Skia.UnitTests/HitTesting.cs | 4 +- .../MockPlatformRenderInterface.cs | 11 +- 136 files changed, 2869 insertions(+), 2473 deletions(-) delete mode 100644 src/Android/Avalonia.Android/OpenGL/GlPlatformSurface.cs delete mode 100644 src/Android/Avalonia.Android/OpenGL/GlRenderTarget.cs create mode 100644 src/Avalonia.Base/Platform/IOptionalFeatureProvider.cs create mode 100644 src/Avalonia.Base/Rendering/OwnedDisposable.cs create mode 100644 src/Avalonia.Base/Rendering/PlatformRenderInterfaceContextManager.cs delete mode 100644 src/Avalonia.OpenGL/Angle/AngleWin32EglDisplay.cs create mode 100644 src/Avalonia.OpenGL/Egl/EglDisplayOptions.cs create mode 100644 src/Avalonia.OpenGL/Egl/EglDisplayUtils.cs create mode 100644 src/Avalonia.OpenGL/Egl/EglPlatformGraphics.cs delete mode 100644 src/Avalonia.OpenGL/Egl/EglPlatformOpenGlInterface.cs delete mode 100644 src/Avalonia.OpenGL/IOpenGlAwarePlatformRenderInterface.cs create mode 100644 src/Avalonia.OpenGL/IOpenGlTextureSharingRenderInterfaceContextFeature.cs delete mode 100644 src/Avalonia.OpenGL/IPlatformOpenGlInterface.cs create mode 100644 src/Skia/Avalonia.Skia/SkiaBackendContext.cs rename src/{Avalonia.OpenGL => Windows/Avalonia.Win32}/AngleOptions.cs (90%) create mode 100644 src/Windows/Avalonia.Win32/DirectX/IDirect3D11TexturePlatformSurface.cs create mode 100644 src/Windows/Avalonia.Win32/OpenGl/Angle/AngleD3DTextureFeature.cs rename src/{Avalonia.OpenGL => Windows/Avalonia.Win32/OpenGl}/Angle/AngleEglInterface.cs (56%) create mode 100644 src/Windows/Avalonia.Win32/OpenGl/Angle/AngleWin32EglDisplay.cs create mode 100644 src/Windows/Avalonia.Win32/OpenGl/Angle/AngleWin32PlatformGraphics.cs delete mode 100644 src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositedWindow.cs delete mode 100644 src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs create mode 100644 src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositedWindow.cs create mode 100644 src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositionShared.cs create mode 100644 src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositionUtils.cs create mode 100644 src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositorConnection.cs diff --git a/src/Android/Avalonia.Android/AndroidPlatform.cs b/src/Android/Avalonia.Android/AndroidPlatform.cs index 2b6d29e7c5..75856e4b52 100644 --- a/src/Android/Avalonia.Android/AndroidPlatform.cs +++ b/src/Android/Avalonia.Android/AndroidPlatform.cs @@ -32,6 +32,7 @@ namespace Avalonia.Android public static AndroidPlatformOptions Options { get; private set; } internal static Compositor Compositor { get; private set; } + internal static PlatformRenderInterfaceContextManager RenderInterface { get; private set; } public static void Initialize() { @@ -51,15 +52,19 @@ namespace Avalonia.Android if (Options.UseGpu) { - EglPlatformOpenGlInterface.TryInitialize(); + EglPlatformGraphics.TryInitialize(); } if (Options.UseCompositor) { Compositor = new Compositor( AvaloniaLocator.Current.GetRequiredService(), - AvaloniaLocator.Current.GetService()); + AvaloniaLocator.Current.GetService()); } + else + RenderInterface = + new PlatformRenderInterfaceContextManager(AvaloniaLocator.Current + .GetService()); } } diff --git a/src/Android/Avalonia.Android/OpenGL/GlPlatformSurface.cs b/src/Android/Avalonia.Android/OpenGL/GlPlatformSurface.cs deleted file mode 100644 index e85ed11028..0000000000 --- a/src/Android/Avalonia.Android/OpenGL/GlPlatformSurface.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Avalonia.OpenGL; -using Avalonia.OpenGL.Egl; -using Avalonia.OpenGL.Surfaces; - -namespace Avalonia.Android.OpenGL -{ - internal sealed class GlPlatformSurface : EglGlPlatformSurfaceBase - { - private readonly EglPlatformOpenGlInterface _egl; - private readonly IEglWindowGlPlatformSurfaceInfo _info; - - private GlPlatformSurface(EglPlatformOpenGlInterface egl, IEglWindowGlPlatformSurfaceInfo info) - { - _egl = egl; - _info = info; - } - - public override IGlPlatformSurfaceRenderTarget CreateGlRenderTarget() => - new GlRenderTarget(_egl, _info, _egl.CreateWindowSurface(_info.Handle), _info.Handle); - - public static GlPlatformSurface TryCreate(IEglWindowGlPlatformSurfaceInfo info) - { - var feature = AvaloniaLocator.Current.GetService(); - if (feature is EglPlatformOpenGlInterface egl) - { - return new GlPlatformSurface(egl, info); - } - - return null; - } - } -} diff --git a/src/Android/Avalonia.Android/OpenGL/GlRenderTarget.cs b/src/Android/Avalonia.Android/OpenGL/GlRenderTarget.cs deleted file mode 100644 index f9071d9b27..0000000000 --- a/src/Android/Avalonia.Android/OpenGL/GlRenderTarget.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System; - -using Avalonia.OpenGL.Egl; -using Avalonia.OpenGL.Surfaces; - -namespace Avalonia.Android.OpenGL -{ - internal sealed class GlRenderTarget : EglPlatformSurfaceRenderTargetBase, IGlPlatformSurfaceRenderTargetWithCorruptionInfo - { - private readonly EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo _info; - private readonly EglSurface _surface; - private readonly IntPtr _handle; - - public GlRenderTarget( - EglPlatformOpenGlInterface egl, - EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo info, - EglSurface surface, - IntPtr handle) - : base(egl) - { - _info = info; - _surface = surface; - _handle = handle; - } - - public bool IsCorrupted => _handle != _info.Handle; - - public override IGlPlatformSurfaceRenderingSession BeginDraw() => BeginDraw(_surface, _info); - } -} diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs index 4150b52946..56dbadca03 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs @@ -8,7 +8,6 @@ using Android.Runtime; using Android.Text; using Android.Views; using Android.Views.InputMethods; -using Avalonia.Android.OpenGL; using Avalonia.Android.Platform.Specific; using Avalonia.Android.Platform.Specific.Helpers; using Avalonia.Android.Platform.Storage; @@ -30,7 +29,7 @@ using AndroidRect = Android.Graphics.Rect; namespace Avalonia.Android.Platform.SkiaPlatform { - class TopLevelImpl : IAndroidView, ITopLevelImpl, EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo, + class TopLevelImpl : IAndroidView, ITopLevelImpl, EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo, ITopLevelImplWithTextInputMethod, ITopLevelImplWithNativeControlHost, ITopLevelImplWithStorageProvider { private readonly IGlPlatformSurface _gl; @@ -47,7 +46,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform _textInputMethod = new AndroidInputMethod(_view); _keyboardHelper = new AndroidKeyboardEventsHelper(this); _pointerHelper = new AndroidMotionEventsHelper(this); - _gl = GlPlatformSurface.TryCreate(this); + _gl = new EglGlPlatformSurface(this); _framebuffer = new FramebufferManager(this); RenderScaling = _view.Scaling; @@ -106,10 +105,15 @@ namespace Avalonia.Android.Platform.SkiaPlatform public IRenderer CreateRenderer(IRenderRoot root) => AndroidPlatform.Options.UseCompositor - ? new CompositingRenderer(root, AndroidPlatform.Compositor) + ? new CompositingRenderer(root, AndroidPlatform.Compositor, () => Surfaces) : AndroidPlatform.Options.UseDeferredRendering - ? new DeferredRenderer(root, AvaloniaLocator.Current.GetRequiredService()) { RenderOnlyOnRenderThread = true } - : new ImmediateRenderer((Visual)root); + ? new DeferredRenderer(root, AvaloniaLocator.Current.GetRequiredService(), + () => AndroidPlatform.RenderInterface.CreateRenderTarget(Surfaces), + AndroidPlatform.RenderInterface) + { RenderOnlyOnRenderThread = true } + : new ImmediateRenderer((Visual)root, + () => AndroidPlatform.RenderInterface.CreateRenderTarget(Surfaces), + AndroidPlatform.RenderInterface); public virtual void Hide() { @@ -283,7 +287,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform public AcrylicPlatformCompensationLevels AcrylicCompensationLevels => new AcrylicPlatformCompensationLevels(1, 1, 1); - IntPtr EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo.Handle => ((IPlatformHandle)_view).Handle; + IntPtr EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo.Handle => ((IPlatformHandle)_view).Handle; public PixelSize Size => _view.Size; diff --git a/src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs b/src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs index d56711ad68..88e5e627ee 100644 --- a/src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs +++ b/src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs @@ -60,5 +60,7 @@ namespace Avalonia.Media.Imaging /// public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer? vbr) => PlatformImpl.Item.CreateDrawingContext(vbr); + + bool IRenderTarget.IsCorrupted => false; } } diff --git a/src/Avalonia.Base/Platform/IOptionalFeatureProvider.cs b/src/Avalonia.Base/Platform/IOptionalFeatureProvider.cs new file mode 100644 index 0000000000..b6464dea58 --- /dev/null +++ b/src/Avalonia.Base/Platform/IOptionalFeatureProvider.cs @@ -0,0 +1,18 @@ +using System; + +namespace Avalonia.Platform; + +public interface IOptionalFeatureProvider +{ + /// + /// Queries for an optional feature + /// + /// Feature type + public object? TryGetFeature(Type featureType); +} + +public static class OptionalFeatureProviderExtensions +{ + public static T? TryGetFeature(this IOptionalFeatureProvider provider) where T : class => + (T?)provider.TryGetFeature(typeof(T)); +} \ No newline at end of file diff --git a/src/Avalonia.Base/Platform/IPlatformGpu.cs b/src/Avalonia.Base/Platform/IPlatformGpu.cs index 0507dea1d7..f6953619d2 100644 --- a/src/Avalonia.Base/Platform/IPlatformGpu.cs +++ b/src/Avalonia.Base/Platform/IPlatformGpu.cs @@ -4,13 +4,21 @@ using Avalonia.Metadata; namespace Avalonia.Platform; [Unstable] -public interface IPlatformGpu +public interface IPlatformGraphics { - IPlatformGpuContext PrimaryContext { get; } + bool UsesSharedContext { get; } + IPlatformGraphicsContext CreateContext(); + IPlatformGraphicsContext GetSharedContext(); } [Unstable] -public interface IPlatformGpuContext : IDisposable +public interface IPlatformGraphicsContext : IDisposable, IOptionalFeatureProvider { + bool IsLost { get; } IDisposable EnsureCurrent(); +} + +public class PlatformGraphicsContextLostException : Exception +{ + } \ No newline at end of file diff --git a/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs b/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs index 518c5f37b8..1828f24aff 100644 --- a/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs +++ b/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs @@ -65,15 +65,6 @@ namespace Avalonia.Platform /// The geometry returned contains the combined geometry of all glyphs in the glyph run. IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun); - /// - /// Creates a renderer. - /// - /// - /// The list of native platform surfaces that can be used for output. - /// - /// An . - IRenderTarget CreateRenderTarget(IEnumerable surfaces); - /// /// Creates a render target bitmap implementation. /// @@ -181,6 +172,13 @@ namespace Avalonia.Platform /// IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphIndices, IReadOnlyList? glyphAdvances, IReadOnlyList? glyphOffsets); + /// + /// Creates a backend-specific object using a low-level API graphics context + /// + /// An underlying low-level graphics context (e. g. wrapped OpenGL context, Vulkan device, D3DDevice, etc) + /// + IPlatformRenderInterfaceContext CreateBackendContext(IPlatformGraphicsContext? graphicsApiContext); + /// /// Gets a value indicating whether the platform directly supports rectangles with rounded corners. /// @@ -200,4 +198,17 @@ namespace Avalonia.Platform /// public PixelFormat DefaultPixelFormat { get; } } + + [Unstable] + public interface IPlatformRenderInterfaceContext : IOptionalFeatureProvider, IDisposable + { + /// + /// Creates a renderer. + /// + /// + /// The list of native platform surfaces that can be used for output. + /// + /// An . + IRenderTarget CreateRenderTarget(IEnumerable surfaces); + } } diff --git a/src/Avalonia.Base/Platform/IRenderTarget.cs b/src/Avalonia.Base/Platform/IRenderTarget.cs index 7023f2ca51..73e9e58da4 100644 --- a/src/Avalonia.Base/Platform/IRenderTarget.cs +++ b/src/Avalonia.Base/Platform/IRenderTarget.cs @@ -19,10 +19,10 @@ namespace Avalonia.Platform /// to be drawn. /// IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer? visualBrushRenderer); - } - - public interface IRenderTargetWithCorruptionInfo : IRenderTarget - { - bool IsCorrupted { get; } + + /// + /// Indicates if the render target is no longer usable and needs to be recreated + /// + public bool IsCorrupted { get; } } } diff --git a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs index ea8a408e6f..b2080aeb87 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs @@ -38,13 +38,12 @@ public class CompositingRenderer : IRendererWithCompositor /// public bool RenderOnlyOnRenderThread { get; set; } = true; - public CompositingRenderer(IRenderRoot root, - Compositor compositor) + public CompositingRenderer(IRenderRoot root, Compositor compositor, Func> surfaces) { _root = root; _compositor = compositor; _recordingContext = new DrawingContext(_recorder); - CompositionTarget = compositor.CreateCompositionTarget(root.CreateRenderTarget); + CompositionTarget = compositor.CreateCompositionTarget(surfaces); CompositionTarget.Root = ((Visual)root).AttachToCompositor(compositor); _update = Update; } @@ -301,7 +300,9 @@ public class CompositingRenderer : IRendererWithCompositor { CompositionTarget.IsEnabled = false; } - + + public ValueTask TryGetRenderInterfaceFeature(Type featureType) => Compositor.TryGetRenderInterfaceFeature(featureType); + public void Dispose() { Stop(); diff --git a/src/Avalonia.Base/Rendering/Composition/Compositor.Factories.cs b/src/Avalonia.Base/Rendering/Composition/Compositor.Factories.cs index 00fa7b3315..4ce87b67a5 100644 --- a/src/Avalonia.Base/Rendering/Composition/Compositor.Factories.cs +++ b/src/Avalonia.Base/Rendering/Composition/Compositor.Factories.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using Avalonia.Platform; using Avalonia.Rendering.Composition.Animations; using Avalonia.Rendering.Composition.Server; @@ -10,11 +11,11 @@ public partial class Compositor /// /// Creates a new CompositionTarget /// - /// A factory method to create IRenderTarget to be called from the render thread + /// A factory method to create IRenderTarget to be called from the render thread /// - public CompositionTarget CreateCompositionTarget(Func renderTargetFactory) + public CompositionTarget CreateCompositionTarget(Func> surfaces) { - return new CompositionTarget(this, new ServerCompositionTarget(_server, renderTargetFactory)); + return new CompositionTarget(this, new ServerCompositionTarget(_server, surfaces)); } public CompositionContainerVisual CreateContainerVisual() => new(this, new ServerCompositionContainerVisual(_server)); diff --git a/src/Avalonia.Base/Rendering/Composition/Compositor.cs b/src/Avalonia.Base/Rendering/Composition/Compositor.cs index e6b9600e45..6512f753fc 100644 --- a/src/Avalonia.Base/Rendering/Composition/Compositor.cs +++ b/src/Avalonia.Base/Rendering/Composition/Compositor.cs @@ -34,6 +34,7 @@ namespace Avalonia.Rendering.Composition internal ServerCompositor Server => _server; private Task? _pendingBatch; private readonly object _pendingBatchLock = new(); + private List _pendingServerCompositorJobs = new(); internal IEasing DefaultEasing { get; } @@ -43,7 +44,7 @@ namespace Avalonia.Rendering.Composition /// /// /// - public Compositor(IRenderLoop loop, IPlatformGpu? gpu) + public Compositor(IRenderLoop loop, IPlatformGraphics? gpu) { Loop = loop; _server = new ServerCompositor(loop, gpu, _batchObjectPool, _batchMemoryPool); @@ -101,6 +102,13 @@ namespace Avalonia.Rendering.Composition #endif } _objectsForSerialization.Clear(); + if (_pendingServerCompositorJobs.Count > 0) + { + writer.WriteObject(ServerCompositor.RenderThreadJobsStartMarker); + foreach (var job in _pendingServerCompositorJobs) + writer.WriteObject(job); + writer.WriteObject(ServerCompositor.RenderThreadJobsEndMarker); + } } batch.CommitedAt = Server.Clock.Elapsed; @@ -136,5 +144,28 @@ namespace Avalonia.Rendering.Composition _invokeBeforeCommit.Enqueue(action); RequestCommitAsync(); } + + /// + /// Attempts to query for a feature from the platform render interface + /// + public ValueTask TryGetRenderInterfaceFeature(Type featureType) + { + var tcs = new TaskCompletionSource(); + _pendingServerCompositorJobs.Add(() => + { + try + { + using (Server.RenderInterface.EnsureCurrent()) + { + tcs.TrySetResult(Server.RenderInterface.Value.TryGetFeature(featureType)); + } + } + catch (Exception e) + { + tcs.SetResult(e); + } + }); + return new ValueTask(tcs.Task); + } } } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs index f5a46506a3..8ecc43dd6e 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs @@ -20,7 +20,7 @@ namespace Avalonia.Rendering.Composition.Server internal partial class ServerCompositionTarget : IDisposable { private readonly ServerCompositor _compositor; - private readonly Func _renderTargetFactory; + private readonly Func> _surfaces; private static long s_nextId = 1; public long Id { get; } public ulong Revision { get; private set; } @@ -39,11 +39,11 @@ namespace Avalonia.Rendering.Composition.Server public ReadbackIndices Readback { get; } = new(); public int RenderedVisuals { get; set; } - public ServerCompositionTarget(ServerCompositor compositor, Func renderTargetFactory) : + public ServerCompositionTarget(ServerCompositor compositor, Func> surfaces) : base(compositor) { _compositor = compositor; - _renderTargetFactory = renderTargetFactory; + _surfaces = surfaces; Id = Interlocked.Increment(ref s_nextId); } @@ -79,13 +79,14 @@ namespace Avalonia.Rendering.Composition.Server if (Root == null) return; - if ((_renderTarget as IRenderTargetWithCorruptionInfo)?.IsCorrupted == true) + if (_renderTarget?.IsCorrupted == true) { _renderTarget!.Dispose(); _renderTarget = null; + _redrawRequested = true; } - _renderTarget ??= _renderTargetFactory(); + _renderTarget ??= _compositor.CreateRenderTarget(_surfaces()); Compositor.UpdateServerTime(); @@ -109,12 +110,13 @@ namespace Avalonia.Rendering.Composition.Server using (var targetContext = _renderTarget.CreateDrawingContext(null)) { var layerSize = Size * Scaling; - if (layerSize != _layerSize || _layer == null) + if (layerSize != _layerSize || _layer == null || _layer.IsCorrupted) { _layer?.Dispose(); _layer = null; _layer = targetContext.CreateLayer(Size); _layerSize = layerSize; + _dirtyRect = new Rect(0, 0, layerSize.Width, layerSize.Height); } if (!_dirtyRect.IsEmpty) @@ -197,7 +199,7 @@ namespace Avalonia.Rendering.Composition.Server if(_disposed) return; _disposed = true; - using (_compositor.GpuContext?.EnsureCurrent()) + using (_compositor.RenderInterface.EnsureCurrent()) { if (_layer != null) { diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs index bfc2b2d626..041a3dd6af 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using Avalonia.Logging; using Avalonia.Platform; using Avalonia.Rendering.Composition.Animations; using Avalonia.Rendering.Composition.Expressions; @@ -19,6 +20,7 @@ namespace Avalonia.Rendering.Composition.Server internal class ServerCompositor : IRenderLoopTask { private readonly IRenderLoop _renderLoop; + private readonly Queue _batches = new Queue(); public long LastBatchId { get; private set; } public Stopwatch Clock { get; } = Stopwatch.StartNew(); @@ -29,13 +31,15 @@ namespace Avalonia.Rendering.Composition.Server internal BatchStreamObjectPool BatchObjectPool; internal BatchStreamMemoryPool BatchMemoryPool; private object _lock = new object(); - public IPlatformGpuContext? GpuContext { get; } + public PlatformRenderInterfaceContextManager RenderInterface { get; } + internal static readonly object RenderThreadJobsStartMarker = new(); + internal static readonly object RenderThreadJobsEndMarker = new(); - public ServerCompositor(IRenderLoop renderLoop, IPlatformGpu? platformGpu, + public ServerCompositor(IRenderLoop renderLoop, IPlatformGraphics? platformGraphics, BatchStreamObjectPool batchObjectPool, BatchStreamMemoryPool batchMemoryPool) { - GpuContext = platformGpu?.PrimaryContext; _renderLoop = renderLoop; + RenderInterface = new PlatformRenderInterfaceContextManager(platformGraphics); BatchObjectPool = batchObjectPool; BatchMemoryPool = batchMemoryPool; _renderLoop.Add(this); @@ -66,7 +70,14 @@ namespace Avalonia.Rendering.Composition.Server { while (!stream.IsObjectEof) { - var target = (ServerObject)stream.ReadObject()!; + var readObject = stream.ReadObject(); + if (readObject == RenderThreadJobsStartMarker) + { + ReadAndExecuteJobs(stream); + continue; + } + + var target = (ServerObject)readObject!; target.DeserializeChanges(stream, batch); #if DEBUG_COMPOSITOR_SERIALIZATION if (stream.ReadObject() != BatchStreamDebugMarkers.ObjectEndMarker) @@ -84,6 +95,23 @@ namespace Avalonia.Rendering.Composition.Server } } + void ReadAndExecuteJobs(BatchStreamReader reader) + { + object? readObject; + while ((readObject = reader.ReadObject()) != RenderThreadJobsEndMarker) + { + var job = (Action)readObject!; + try + { + job(); + } + catch + { + // Ignore + } + } + } + void CompletePendingBatches() { foreach(var batch in _reusableToCompleteList) @@ -118,8 +146,16 @@ namespace Avalonia.Rendering.Composition.Server _animationsToUpdate.Clear(); - foreach (var t in _activeTargets) - t.Render(); + try + { + RenderInterface.EnsureValidBackendContext(); + foreach (var t in _activeTargets) + t.Render(); + } + catch (Exception e) + { + Logger.TryGet(LogEventLevel.Error, LogArea.Visual)?.Log(this, "Exception when rendering: {Error}", e); + } } public void AddCompositionTarget(ServerCompositionTarget target) @@ -137,5 +173,11 @@ namespace Avalonia.Rendering.Composition.Server public void RemoveFromClock(IAnimationInstance animationInstance) => _activeAnimations.Remove(animationInstance); + + public IRenderTarget CreateRenderTarget(IEnumerable surfaces) + { + using (RenderInterface.EnsureCurrent()) + return RenderInterface.CreateRenderTarget(surfaces); + } } } diff --git a/src/Avalonia.Base/Rendering/DeferredRenderer.cs b/src/Avalonia.Base/Rendering/DeferredRenderer.cs index c3bf861c47..787f08515a 100644 --- a/src/Avalonia.Base/Rendering/DeferredRenderer.cs +++ b/src/Avalonia.Base/Rendering/DeferredRenderer.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; +using System.Threading.Tasks; using Avalonia.Logging; using Avalonia.Media; using Avalonia.Media.Immutable; @@ -22,6 +23,8 @@ namespace Avalonia.Rendering { private readonly IDispatcher? _dispatcher; private readonly IRenderLoop? _renderLoop; + private readonly Func? _renderTargetFactory; + private readonly PlatformRenderInterfaceContextManager? _renderInterface; private readonly Visual _root; private readonly ISceneBuilder _sceneBuilder; @@ -39,6 +42,7 @@ namespace Avalonia.Rendering private readonly object _startStopLock = new object(); private readonly object _renderLoopIsRenderingLock = new object(); private readonly Action _updateSceneIfNeededDelegate; + private List? _pendingRenderThreadJobs; /// /// Initializes a new instance of the class. @@ -51,6 +55,8 @@ namespace Avalonia.Rendering public DeferredRenderer( IRenderRoot root, IRenderLoop renderLoop, + Func renderTargetFactory, + PlatformRenderInterfaceContextManager? renderInterface = null, ISceneBuilder? sceneBuilder = null, IDispatcher? dispatcher = null, IDeferredRendererLock? rendererLock = null) : base(true) @@ -60,6 +66,8 @@ namespace Avalonia.Rendering _sceneBuilder = sceneBuilder ?? new SceneBuilder(); Layers = new RenderLayers(); _renderLoop = renderLoop; + _renderTargetFactory = renderTargetFactory; + _renderInterface = renderInterface; _lock = rendererLock ?? new ManagedDeferredRendererLock(); _updateSceneIfNeededDelegate = UpdateSceneIfNeeded; } @@ -256,6 +264,30 @@ namespace Avalonia.Rendering } } + public ValueTask TryGetRenderInterfaceFeature(Type featureType) + { + if (_renderInterface == null) + return new((object?)null); + + var tcs = new TaskCompletionSource(); + _pendingRenderThreadJobs ??= new(); + _pendingRenderThreadJobs.Add(() => + { + try + { + using (_renderInterface.EnsureCurrent()) + { + tcs.TrySetResult(_renderInterface.Value.TryGetFeature(featureType)); + } + } + catch (Exception e) + { + tcs.SetResult(e); + } + }); + return new ValueTask(tcs.Task); + } + bool NeedsUpdate => _dirty == null || _dirty.Count > 0; bool IRenderLoopTask.NeedsUpdate => NeedsUpdate; @@ -337,7 +369,16 @@ namespace Avalonia.Rendering } finally { - scene.Item.MarkAsRendered(); + try + { + if(scene.Item.RenderThreadJobs!=null) + foreach (var job in scene.Item.RenderThreadJobs) + job(); + } + finally + { + scene.Item.MarkAsRendered(); + } } } } @@ -604,7 +645,7 @@ namespace Avalonia.Rendering return; } - if ((RenderTarget as IRenderTargetWithCorruptionInfo)?.IsCorrupted == true) + if (RenderTarget?.IsCorrupted == true) { RenderTarget!.Dispose(); RenderTarget = null; @@ -612,7 +653,7 @@ namespace Avalonia.Rendering if (RenderTarget == null) { - RenderTarget = ((IRenderRoot)_root).CreateRenderTarget(); + RenderTarget = _renderTargetFactory!(); } context = RenderTarget.CreateDrawingContext(this); @@ -637,7 +678,11 @@ namespace Avalonia.Rendering } if (_root.IsVisible) { - var sceneRef = RefCountable.Create(_scene?.Item.CloneScene() ?? new Scene(_root)); + var sceneRef = RefCountable.Create(_scene?.Item.CloneScene() ?? new Scene(_root) + { + RenderThreadJobs = _pendingRenderThreadJobs + }); + _pendingRenderThreadJobs = null; var scene = sceneRef.Item; if (_dirty == null) diff --git a/src/Avalonia.Base/Rendering/IRenderRoot.cs b/src/Avalonia.Base/Rendering/IRenderRoot.cs index aa06ce7bed..cabb1302cd 100644 --- a/src/Avalonia.Base/Rendering/IRenderRoot.cs +++ b/src/Avalonia.Base/Rendering/IRenderRoot.cs @@ -25,12 +25,6 @@ namespace Avalonia.Rendering /// double RenderScaling { get; } - /// - /// Creates a render target for the window. - /// - /// An . - IRenderTarget CreateRenderTarget(); - /// /// Adds a rectangle to the window's dirty region. /// diff --git a/src/Avalonia.Base/Rendering/IRenderer.cs b/src/Avalonia.Base/Rendering/IRenderer.cs index cb36aad7cc..f3f5b5e99b 100644 --- a/src/Avalonia.Base/Rendering/IRenderer.cs +++ b/src/Avalonia.Base/Rendering/IRenderer.cs @@ -1,6 +1,7 @@ using System; using Avalonia.VisualTree; using System.Collections.Generic; +using System.Threading.Tasks; using Avalonia.Rendering.Composition; namespace Avalonia.Rendering @@ -87,6 +88,11 @@ namespace Avalonia.Rendering /// Stops the renderer. /// void Stop(); + + /// + /// Attempts to query for a feature from the platform render interface + /// + public ValueTask TryGetRenderInterfaceFeature(Type featureType); } public interface IRendererWithCompositor : IRenderer diff --git a/src/Avalonia.Base/Rendering/ImmediateRenderer.cs b/src/Avalonia.Base/Rendering/ImmediateRenderer.cs index 9e1582ed43..1c797a5348 100644 --- a/src/Avalonia.Base/Rendering/ImmediateRenderer.cs +++ b/src/Avalonia.Base/Rendering/ImmediateRenderer.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using Avalonia.Logging; using Avalonia.Media; using Avalonia.Platform; @@ -19,6 +20,8 @@ namespace Avalonia.Rendering public class ImmediateRenderer : RendererBase, IRenderer, IVisualBrushRenderer { private readonly Visual _root; + private readonly Func _renderTargetFactory; + private readonly PlatformRenderInterfaceContextManager? _renderContext; private readonly IRenderRoot? _renderRoot; private bool _updateTransformedBounds = true; private IRenderTarget? _renderTarget; @@ -27,15 +30,19 @@ namespace Avalonia.Rendering /// Initializes a new instance of the class. /// /// The control to render. - public ImmediateRenderer(Visual root) + public ImmediateRenderer(Visual root, Func renderTargetFactory, + PlatformRenderInterfaceContextManager? renderContext = null) { _root = root ?? throw new ArgumentNullException(nameof(root)); + _renderTargetFactory = renderTargetFactory; + _renderContext = renderContext; _renderRoot = root as IRenderRoot; } - private ImmediateRenderer(Visual root, bool updateTransformedBounds) + private ImmediateRenderer(Visual root, Func renderTargetFactory, bool updateTransformedBounds) { _root = root ?? throw new ArgumentNullException(nameof(root)); + _renderTargetFactory = renderTargetFactory; _renderRoot = root as IRenderRoot; _updateTransformedBounds = updateTransformedBounds; } @@ -54,7 +61,7 @@ namespace Avalonia.Rendering { if (_renderTarget == null) { - _renderTarget = ((IRenderRoot)_root).CreateRenderTarget(); + _renderTarget = _renderTargetFactory(); } try @@ -104,7 +111,7 @@ namespace Avalonia.Rendering /// The render target. public static void Render(Visual visual, IRenderTarget target) { - using (var renderer = new ImmediateRenderer(visual, updateTransformedBounds: false)) + using (var renderer = new ImmediateRenderer(visual, () => target, updateTransformedBounds: false)) using (var context = new DrawingContext(target.CreateDrawingContext(renderer))) { renderer.Render(context, visual, visual.Bounds); @@ -118,7 +125,9 @@ namespace Avalonia.Rendering /// The drawing context. public static void Render(Visual visual, DrawingContext context) { - using (var renderer = new ImmediateRenderer(visual, updateTransformedBounds: false)) + using (var renderer = new ImmediateRenderer(visual, + () => throw new InvalidOperationException("This is not supposed to be called"), + updateTransformedBounds: false)) { renderer.Render(context, visual, visual.Bounds); } @@ -185,6 +194,9 @@ namespace Avalonia.Rendering { } + public ValueTask TryGetRenderInterfaceFeature(Type featureType) => + new(_renderContext?.Value?.TryGetFeature(featureType)); + /// Size IVisualBrushRenderer.GetRenderTargetSize(IVisualBrush brush) { @@ -201,7 +213,9 @@ namespace Avalonia.Rendering internal static void Render(Visual visual, DrawingContext context, bool updateTransformedBounds) { - using var renderer = new ImmediateRenderer(visual, updateTransformedBounds); + using var renderer = new ImmediateRenderer(visual, + () => throw new InvalidOperationException("This is not supposed to be called"), + updateTransformedBounds); renderer.Render(context, visual, visual.Bounds); } diff --git a/src/Avalonia.Base/Rendering/OwnedDisposable.cs b/src/Avalonia.Base/Rendering/OwnedDisposable.cs new file mode 100644 index 0000000000..27122751aa --- /dev/null +++ b/src/Avalonia.Base/Rendering/OwnedDisposable.cs @@ -0,0 +1,24 @@ +using System; + +namespace Avalonia.Rendering; + +struct OwnedDisposable :IDisposable where T : class, IDisposable +{ + private readonly bool _owns; + private T? _value; + + public T Value => _value ?? throw new ObjectDisposedException("OwnedDisposable"); + + public OwnedDisposable(T value, bool owns) + { + _owns = owns; + _value = value; + } + + public void Dispose() + { + if(_owns) + _value?.Dispose(); + _value = null; + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/PlatformRenderInterfaceContextManager.cs b/src/Avalonia.Base/Rendering/PlatformRenderInterfaceContextManager.cs new file mode 100644 index 0000000000..198b36564a --- /dev/null +++ b/src/Avalonia.Base/Rendering/PlatformRenderInterfaceContextManager.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; +using System.Reactive.Disposables; +using Avalonia.Metadata; +using Avalonia.Platform; + +namespace Avalonia.Rendering; + +[Unstable] +// TODO: Make it internal once legacy renderers are removed +public class PlatformRenderInterfaceContextManager +{ + private readonly IPlatformGraphics? _graphics; + private IPlatformRenderInterfaceContext? _backend; + private OwnedDisposable? _gpuContext; + + public PlatformRenderInterfaceContextManager(IPlatformGraphics? graphics) + { + _graphics = graphics; + } + + public void EnsureValidBackendContext() + { + if (_backend == null || _gpuContext?.Value.IsLost == true) + { + _backend?.Dispose(); + _backend = null; + _gpuContext?.Dispose(); + _gpuContext = null; + + if (_graphics != null) + { + if (_graphics.UsesSharedContext) + _gpuContext = new OwnedDisposable(_graphics.GetSharedContext(), false); + else + _gpuContext = new OwnedDisposable(_graphics.CreateContext(), true); + } + + _backend = AvaloniaLocator.Current.GetRequiredService() + .CreateBackendContext(_gpuContext?.Value); + } + } + + public IPlatformRenderInterfaceContext Value + { + get + { + EnsureValidBackendContext(); + return _backend!; + } + } + + public IDisposable EnsureCurrent() + { + EnsureValidBackendContext(); + if (_gpuContext.HasValue) + return _gpuContext.Value.Value.EnsureCurrent(); + return Disposable.Empty; + } + + public IRenderTarget CreateRenderTarget(IEnumerable surfaces) + { + EnsureValidBackendContext(); + return _backend!.CreateRenderTarget(surfaces); + } +} diff --git a/src/Avalonia.Base/Rendering/RenderLoop.cs b/src/Avalonia.Base/Rendering/RenderLoop.cs index c66fec92aa..6f3d7bce16 100644 --- a/src/Avalonia.Base/Rendering/RenderLoop.cs +++ b/src/Avalonia.Base/Rendering/RenderLoop.cs @@ -19,6 +19,7 @@ namespace Avalonia.Rendering private readonly IDispatcher _dispatcher; private List _items = new List(); private List _itemsCopy = new List(); + private List _updateItemsCopy = new List(); private IRenderTimer? _timer; private int _inTick; private int _inUpdate; @@ -97,7 +98,14 @@ namespace Avalonia.Rendering { bool needsUpdate = false; - foreach (IRenderLoopTask item in _items) + lock (_items) + { + _itemsCopy.Clear(); + foreach (var i in _items) + _itemsCopy.Add(i); + } + + foreach (IRenderLoopTask item in _itemsCopy) { if (item.NeedsUpdate) { @@ -112,10 +120,13 @@ namespace Avalonia.Rendering { _dispatcher.Post(() => { - for (var i = 0; i < _items.Count; ++i) + lock (_items) + { + _updateItemsCopy.Clear(); + _updateItemsCopy.AddRange(_items); + } + foreach (var item in _updateItemsCopy) { - var item = _items[i]; - if (item.NeedsUpdate) { try @@ -128,18 +139,12 @@ namespace Avalonia.Rendering } } } + _updateItemsCopy.Clear(); Interlocked.Exchange(ref _inUpdate, 0); }, DispatcherPriority.Render); } - lock (_items) - { - _itemsCopy.Clear(); - foreach (var i in _items) - _itemsCopy.Add(i); - } - for (int i = 0; i < _itemsCopy.Count; i++) { _itemsCopy[i].Render(); diff --git a/src/Avalonia.Base/Rendering/SceneGraph/Scene.cs b/src/Avalonia.Base/Rendering/SceneGraph/Scene.cs index 2da8923497..735eb3bb3f 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/Scene.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/Scene.cs @@ -346,5 +346,7 @@ namespace Avalonia.Rendering.SceneGraph } public void MarkAsRendered() => _rendered.TrySetResult(true); + + public List? RenderThreadJobs { get; set; } } } diff --git a/src/Avalonia.Controls/AppBuilderBase.cs b/src/Avalonia.Controls/AppBuilderBase.cs index 604f2369e7..a100d38d78 100644 --- a/src/Avalonia.Controls/AppBuilderBase.cs +++ b/src/Avalonia.Controls/AppBuilderBase.cs @@ -229,8 +229,8 @@ namespace Avalonia.Controls s_setupWasAlreadyCalled = true; _optionsInitializers?.Invoke(); RuntimePlatformServicesInitializer(); - WindowingSubsystemInitializer(); RenderingSubsystemInitializer(); + WindowingSubsystemInitializer(); AfterPlatformServicesSetupCallback(Self); Instance = _appFactory(); Instance.ApplicationLifetime = _lifetime; diff --git a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs index 4f80892857..4a227a0c00 100644 --- a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs +++ b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs @@ -13,6 +13,7 @@ namespace Avalonia.Controls.Embedding.Offscreen { private double _scaling = 1; private Size _clientSize; + private PlatformRenderInterfaceContextManager _renderInterface = new(null); public IInputRoot? InputRoot { get; private set; } public bool IsDisposed { get; private set; } @@ -22,7 +23,8 @@ namespace Avalonia.Controls.Embedding.Offscreen IsDisposed = true; } - public IRenderer CreateRenderer(IRenderRoot root) => new ImmediateRenderer((Visual)root); + public IRenderer CreateRenderer(IRenderRoot root) => + new ImmediateRenderer((Visual)root, () => _renderInterface.CreateRenderTarget(Surfaces), _renderInterface); public abstract void Invalidate(Rect rect); public abstract IEnumerable Surfaces { get; } diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 59ad696148..67da366727 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -323,17 +323,7 @@ namespace Avalonia.Controls ??= AvaloniaLocator.Current.GetService()?.CreateProvider(this) ?? (PlatformImpl as ITopLevelImplWithStorageProvider)?.StorageProvider ?? throw new InvalidOperationException("StorageProvider platform implementation is not available."); - - IRenderTarget IRenderRoot.CreateRenderTarget() => CreateRenderTarget(); - - /// - protected virtual IRenderTarget CreateRenderTarget() - { - if(PlatformImpl == null) - throw new InvalidOperationException("Can't create render target, PlatformImpl is null (might be already disposed)"); - return _renderInterface!.CreateRenderTarget(PlatformImpl.Surfaces); - } - + /// void IRenderRoot.Invalidate(Rect rect) { diff --git a/src/Avalonia.DesignerSupport/Remote/Stubs.cs b/src/Avalonia.DesignerSupport/Remote/Stubs.cs index 71389be1a6..80a4c7d897 100644 --- a/src/Avalonia.DesignerSupport/Remote/Stubs.cs +++ b/src/Avalonia.DesignerSupport/Remote/Stubs.cs @@ -61,7 +61,10 @@ namespace Avalonia.DesignerSupport.Remote })); } - public IRenderer CreateRenderer(IRenderRoot root) => new ImmediateRenderer((Visual)root); + public IRenderer CreateRenderer(IRenderRoot root) => new ImmediateRenderer((Visual)root, () => + new PlatformRenderInterfaceContextManager(null) + .CreateRenderTarget(Surfaces)); + public void Dispose() { } diff --git a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs index 7abc0ca131..be0c7ed7a7 100644 --- a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs +++ b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs @@ -12,7 +12,7 @@ using Avalonia.Media.Imaging; namespace Avalonia.Headless { - internal class HeadlessPlatformRenderInterface : IPlatformRenderInterface + internal class HeadlessPlatformRenderInterface : IPlatformRenderInterface, IPlatformRenderInterfaceContext { public static void Initialize() { @@ -22,6 +22,8 @@ namespace Avalonia.Headless public IEnumerable InstalledFontNames { get; } = new[] { "Tahoma" }; + public IPlatformRenderInterfaceContext CreateBackendContext(IPlatformGraphicsContext graphicsContext) => this; + public bool SupportsIndividualRoundRects => false; public AlphaFormat DefaultAlphaFormat => AlphaFormat.Premul; @@ -47,6 +49,7 @@ namespace Avalonia.Headless public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2) => throw new NotImplementedException(); public IRenderTarget CreateRenderTarget(IEnumerable surfaces) => new HeadlessRenderTarget(); + public object TryGetFeature(Type featureType) => null; public IRenderTargetBitmapImpl CreateRenderTargetBitmap(PixelSize size, Vector dpi) { @@ -313,6 +316,8 @@ namespace Avalonia.Headless return new HeadlessDrawingContextStub(); } + public bool IsCorrupted => false; + public void Blit(IDrawingContextImpl context) { @@ -474,6 +479,13 @@ namespace Avalonia.Headless { return new HeadlessDrawingContextStub(); } + + public bool IsCorrupted => false; + } + + public void Dispose() + { + } } } diff --git a/src/Avalonia.Headless/HeadlessWindowImpl.cs b/src/Avalonia.Headless/HeadlessWindowImpl.cs index 742df3324b..725fab1eaa 100644 --- a/src/Avalonia.Headless/HeadlessWindowImpl.cs +++ b/src/Avalonia.Headless/HeadlessWindowImpl.cs @@ -56,8 +56,9 @@ namespace Avalonia.Headless public IRenderer CreateRenderer(IRenderRoot root) => AvaloniaHeadlessPlatform.Compositor != null - ? new CompositingRenderer(root, AvaloniaHeadlessPlatform.Compositor) - : new DeferredRenderer(root, AvaloniaLocator.Current.GetRequiredService()); + ? new CompositingRenderer(root, AvaloniaHeadlessPlatform.Compositor, () => Surfaces) + : new DeferredRenderer(root, AvaloniaLocator.Current.GetRequiredService(), + () => new PlatformRenderInterfaceContextManager(null).CreateRenderTarget(Surfaces), null); public void Invalidate(Rect rect) { diff --git a/src/Avalonia.Native/AvaloniaNativePlatform.cs b/src/Avalonia.Native/AvaloniaNativePlatform.cs index db6b6f57cd..a5b2ea30cc 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatform.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatform.cs @@ -18,13 +18,14 @@ namespace Avalonia.Native { private readonly IAvaloniaNativeFactory _factory; private AvaloniaNativePlatformOptions _options; - private AvaloniaNativePlatformOpenGlInterface _platformGl; + private AvaloniaNativeGlPlatformGraphics _platformGl; [DllImport("libAvaloniaNative")] static extern IntPtr CreateAvaloniaNative(); internal static readonly KeyboardDevice KeyboardDevice = new KeyboardDevice(); [CanBeNull] internal static Compositor Compositor { get; private set; } + [CanBeNull] internal static PlatformRenderInterfaceContextManager RenderInterface { get; private set; } public static AvaloniaNativePlatform Initialize(IntPtr factory, AvaloniaNativePlatformOptions options) { @@ -126,10 +127,9 @@ namespace Avalonia.Native { try { - _platformGl = new AvaloniaNativePlatformOpenGlInterface(_factory.ObtainGlDisplay()); + _platformGl = new AvaloniaNativeGlPlatformGraphics(_factory.ObtainGlDisplay()); AvaloniaLocator.CurrentMutable - .Bind().ToConstant(_platformGl) - .Bind().ToConstant(_platformGl); + .Bind().ToConstant(_platformGl); } catch (Exception) @@ -137,12 +137,14 @@ namespace Avalonia.Native // ignored } } - + if (_options.UseDeferredRendering && _options.UseCompositor) { Compositor = new Compositor(renderLoop, _platformGl); } + else + RenderInterface = new PlatformRenderInterfaceContextManager(_platformGl); } public ITrayIconImpl CreateTrayIcon() diff --git a/src/Avalonia.Native/AvaloniaNativePlatformOpenGlInterface.cs b/src/Avalonia.Native/AvaloniaNativePlatformOpenGlInterface.cs index 14d27a90e9..8e40960af6 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatformOpenGlInterface.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatformOpenGlInterface.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using Avalonia.OpenGL; using Avalonia.Native.Interop; using System.Drawing; @@ -8,18 +9,18 @@ using Avalonia.Threading; namespace Avalonia.Native { - class AvaloniaNativePlatformOpenGlInterface : IPlatformOpenGlInterface + class AvaloniaNativeGlPlatformGraphics : IPlatformGraphics { private readonly IAvnGlDisplay _display; - public AvaloniaNativePlatformOpenGlInterface(IAvnGlDisplay display) + public AvaloniaNativeGlPlatformGraphics(IAvnGlDisplay display) { _display = display; - var immediate = display.CreateContext(null); + var context = display.CreateContext(null); int major, minor; GlInterface glInterface; - using (immediate.MakeCurrent()) + using (context.MakeCurrent()) { var basic = new GlBasicInfoInterface(display.GetProcAddress); basic.GetIntegerv(GlConsts.GL_MAJOR_VERSION, out major); @@ -32,24 +33,23 @@ namespace Avalonia.Native }); } - GlDisplay = new GlDisplay(display, glInterface, immediate.SampleCount, immediate.StencilSize); - MainContext = new GlContext(GlDisplay, null, immediate, _version); + GlDisplay = new GlDisplay(display, glInterface, context.SampleCount, context.StencilSize); + SharedContext =(GlContext)CreateContext(); } - internal GlContext MainContext { get; } - public IGlContext PrimaryContext => MainContext; - IPlatformGpuContext IPlatformGpu.PrimaryContext => PrimaryContext; + + public bool UsesSharedContext => true; + public IPlatformGraphicsContext CreateContext() => new GlContext(GlDisplay, + null, _display.CreateContext(null), _version); + + public IPlatformGraphicsContext GetSharedContext() => SharedContext; + public bool CanShareContexts => true; public bool CanCreateContexts => true; internal GlDisplay GlDisplay; private readonly GlVersion _version; - - public IGlContext CreateSharedContext() => new GlContext(GlDisplay, - MainContext, _display.CreateContext(MainContext.Context), _version); - - public IGlContext CreateContext() => new GlContext(GlDisplay, - null, _display.CreateContext(null), _version); + internal GlContext SharedContext { get; } } class GlDisplay @@ -71,6 +71,10 @@ namespace Avalonia.Native public int StencilSize { get; } public void ClearContext() => _display.LegacyClearCurrentContext(); + + public GlContext CreateSharedContext(GlContext share) => + new GlContext(this, share, _display.CreateContext(share.Context), share.Version); + } class GlContext : IGlContext @@ -91,7 +95,14 @@ namespace Avalonia.Native public GlInterface GlInterface => _display.GlInterface; public int SampleCount => _display.SampleCount; public int StencilSize => _display.StencilSize; - public IDisposable MakeCurrent() => Context.MakeCurrent(); + public IDisposable MakeCurrent() + { + if (IsLost) + throw new PlatformGraphicsContextLostException(); + return Context.MakeCurrent(); + } + + public bool IsLost => Context == null; public IDisposable EnsureCurrent() => MakeCurrent(); public bool IsSharedWith(IGlContext context) @@ -103,12 +114,18 @@ namespace Avalonia.Native || _sharedWith != null && _sharedWith == c._sharedWith; } + public bool CanCreateSharedContext => true; + + public IGlContext CreateSharedContext(IEnumerable preferredVersions = null) => + _display.CreateSharedContext(_sharedWith ?? this); public void Dispose() { Context.Dispose(); Context = null; } + + public object TryGetFeature(Type featureType) => null; } @@ -125,7 +142,6 @@ namespace Avalonia.Native public IGlPlatformSurfaceRenderingSession BeginDraw() { - var feature = (AvaloniaNativePlatformOpenGlInterface)AvaloniaLocator.Current.GetService(); return new GlPlatformSurfaceRenderingSession(_context, _target.BeginDrawing()); } @@ -172,16 +188,14 @@ namespace Avalonia.Native class GlPlatformSurface : IGlPlatformSurface { private readonly IAvnWindowBase _window; - private readonly IGlContext _context; - - public GlPlatformSurface(IAvnWindowBase window, IGlContext context) + public GlPlatformSurface(IAvnWindowBase window) { _window = window; - _context = context; } - public IGlPlatformSurfaceRenderTarget CreateGlRenderTarget() + public IGlPlatformSurfaceRenderTarget CreateGlRenderTarget(IGlContext context) { - return new GlPlatformSurfaceRenderTarget(_window.CreateGlRenderTarget(), _context); + var avnContext = (GlContext)context; + return new GlPlatformSurfaceRenderTarget(_window.CreateGlRenderTarget(), avnContext); } } diff --git a/src/Avalonia.Native/PopupImpl.cs b/src/Avalonia.Native/PopupImpl.cs index 76d3905b47..0953527284 100644 --- a/src/Avalonia.Native/PopupImpl.cs +++ b/src/Avalonia.Native/PopupImpl.cs @@ -8,12 +8,12 @@ namespace Avalonia.Native class PopupImpl : WindowBaseImpl, IPopupImpl { private readonly AvaloniaNativePlatformOptions _opts; - private readonly AvaloniaNativePlatformOpenGlInterface _glFeature; + private readonly AvaloniaNativeGlPlatformGraphics _glFeature; private readonly IWindowBaseImpl _parent; public PopupImpl(IAvaloniaNativeFactory factory, AvaloniaNativePlatformOptions opts, - AvaloniaNativePlatformOpenGlInterface glFeature, + AvaloniaNativeGlPlatformGraphics glFeature, IWindowBaseImpl parent) : base(factory, opts, glFeature) { _opts = opts; @@ -21,8 +21,7 @@ namespace Avalonia.Native _parent = parent; using (var e = new PopupEvents(this)) { - var context = _opts.UseGpu ? glFeature?.MainContext : null; - Init(factory.CreatePopup(e, context?.Context), factory.CreateScreens(), context); + Init(factory.CreatePopup(e, _glFeature.SharedContext.Context), factory.CreateScreens()); } PopupPositioner = new ManagedPopupPositioner(new ManagedPopupPositionerPopupImplHelper(parent, MoveResize)); } diff --git a/src/Avalonia.Native/WindowImpl.cs b/src/Avalonia.Native/WindowImpl.cs index 9de794ed53..2201503168 100644 --- a/src/Avalonia.Native/WindowImpl.cs +++ b/src/Avalonia.Native/WindowImpl.cs @@ -14,14 +14,14 @@ namespace Avalonia.Native internal class WindowImpl : WindowBaseImpl, IWindowImpl, ITopLevelImplWithNativeMenuExporter { private readonly AvaloniaNativePlatformOptions _opts; - private readonly AvaloniaNativePlatformOpenGlInterface _glFeature; + private readonly AvaloniaNativeGlPlatformGraphics _glFeature; IAvnWindow _native; private double _extendTitleBarHeight = -1; private DoubleClickHelper _doubleClickHelper; internal WindowImpl(IAvaloniaNativeFactory factory, AvaloniaNativePlatformOptions opts, - AvaloniaNativePlatformOpenGlInterface glFeature) : base(factory, opts, glFeature) + AvaloniaNativeGlPlatformGraphics glFeature) : base(factory, opts, glFeature) { _opts = opts; _glFeature = glFeature; @@ -29,8 +29,7 @@ namespace Avalonia.Native using (var e = new WindowEvents(this)) { - var context = _opts.UseGpu ? glFeature?.MainContext : null; - Init(_native = factory.CreateWindow(e, context?.Context), factory.CreateScreens(), context); + Init(_native = factory.CreateWindow(e, glFeature.SharedContext.Context), factory.CreateScreens()); } NativeMenuExporter = new AvaloniaNativeMenuExporter(_native, factory); diff --git a/src/Avalonia.Native/WindowImplBase.cs b/src/Avalonia.Native/WindowImplBase.cs index 4744a20107..5d66590a2b 100644 --- a/src/Avalonia.Native/WindowImplBase.cs +++ b/src/Avalonia.Native/WindowImplBase.cs @@ -66,7 +66,7 @@ namespace Avalonia.Native private IGlContext _glContext; internal WindowBaseImpl(IAvaloniaNativeFactory factory, AvaloniaNativePlatformOptions opts, - AvaloniaNativePlatformOpenGlInterface glFeature) + AvaloniaNativeGlPlatformGraphics glFeature) { _factory = factory; _gpu = opts.UseGpu && glFeature != null; @@ -78,14 +78,13 @@ namespace Avalonia.Native StorageProvider = new SystemDialogs(this, _factory.CreateSystemDialogs()); } - protected void Init(IAvnWindowBase window, IAvnScreens screens, IGlContext glContext) + protected void Init(IAvnWindowBase window, IAvnScreens screens) { _native = window; - _glContext = glContext; Handle = new MacOSTopLevelWindowHandle(window); if (_gpu) - _glSurface = new GlPlatformSurface(window, _glContext); + _glSurface = new GlPlatformSurface(window); Screen = new ScreenImpl(screens); _savedLogicalSize = ClientSize; @@ -375,14 +374,17 @@ namespace Avalonia.Native if (_deferredRendering) { if (AvaloniaNativePlatform.Compositor != null) - return new CompositingRenderer(root, AvaloniaNativePlatform.Compositor) + return new CompositingRenderer(root, AvaloniaNativePlatform.Compositor, () => Surfaces) { RenderOnlyOnRenderThread = false }; - return new DeferredRenderer(root, loop); + return new DeferredRenderer(root, loop, + () => AvaloniaNativePlatform.RenderInterface!.CreateRenderTarget(Surfaces), + AvaloniaNativePlatform.RenderInterface); } - return new ImmediateRenderer((Visual)root); + return new ImmediateRenderer((Visual)root, + () => AvaloniaNativePlatform.RenderInterface!.CreateRenderTarget(Surfaces), AvaloniaNativePlatform.RenderInterface); } public virtual void Dispose() diff --git a/src/Avalonia.OpenGL/Angle/AngleWin32EglDisplay.cs b/src/Avalonia.OpenGL/Angle/AngleWin32EglDisplay.cs deleted file mode 100644 index 3a8fdb8491..0000000000 --- a/src/Avalonia.OpenGL/Angle/AngleWin32EglDisplay.cs +++ /dev/null @@ -1,95 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Runtime.InteropServices; -using Avalonia.OpenGL.Egl; -using static Avalonia.OpenGL.Egl.EglConsts; - -namespace Avalonia.OpenGL.Angle -{ - public class AngleWin32EglDisplay : EglDisplay - { - struct AngleInfo - { - public IntPtr Display { get; set; } - public AngleOptions.PlatformApi PlatformApi { get; set; } - } - - static AngleInfo CreateAngleDisplay(EglInterface _egl) - { - if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - throw new PlatformNotSupportedException(); - var display = IntPtr.Zero; - AngleOptions.PlatformApi angleApi = default; - { - if (!_egl.IsGetPlatformDisplayExtAvailable) - throw new OpenGlException("eglGetPlatformDisplayEXT is not supported by libegl.dll"); - - var allowedApis = AvaloniaLocator.Current.GetService()?.AllowedPlatformApis - ?? new [] { AngleOptions.PlatformApi.DirectX11, AngleOptions.PlatformApi.DirectX9 }; - - foreach (var platformApi in allowedApis) - { - int dapi; - if (platformApi == AngleOptions.PlatformApi.DirectX9) - dapi = EGL_PLATFORM_ANGLE_TYPE_D3D9_ANGLE; - else if (platformApi == AngleOptions.PlatformApi.DirectX11) - dapi = EGL_PLATFORM_ANGLE_TYPE_D3D11_ANGLE; - else - continue; - - display = _egl.GetPlatformDisplayExt(EGL_PLATFORM_ANGLE_ANGLE, IntPtr.Zero, - new[] { EGL_PLATFORM_ANGLE_TYPE_ANGLE, dapi, EGL_NONE }); - if (display != IntPtr.Zero) - { - angleApi = platformApi; - break; - } - } - - if (display == IntPtr.Zero) - throw new OpenGlException("Unable to create ANGLE display"); - return new AngleInfo { Display = display, PlatformApi = angleApi }; - } - } - - private AngleWin32EglDisplay(EglInterface egl, AngleInfo info) : base(egl, false, info.Display) - { - PlatformApi = info.PlatformApi; - } - - public AngleWin32EglDisplay(EglInterface egl) : this(egl, CreateAngleDisplay(egl)) - { - - } - - public AngleWin32EglDisplay() : this(new AngleEglInterface()) - { - - } - - public AngleOptions.PlatformApi PlatformApi { get; } - - public IntPtr GetDirect3DDevice() - { - if (!EglInterface.QueryDisplayAttribExt(Handle, EglConsts.EGL_DEVICE_EXT, out var eglDevice)) - throw new OpenGlException("Unable to get EGL_DEVICE_EXT"); - if (!EglInterface.QueryDeviceAttribExt(eglDevice, PlatformApi == AngleOptions.PlatformApi.DirectX9 ? EGL_D3D9_DEVICE_ANGLE : EGL_D3D11_DEVICE_ANGLE, out var d3dDeviceHandle)) - throw new OpenGlException("Unable to get EGL_D3D9_DEVICE_ANGLE"); - return d3dDeviceHandle; - } - - public EglSurface WrapDirect3D11Texture(EglPlatformOpenGlInterface egl, IntPtr handle) - { - if (PlatformApi != AngleOptions.PlatformApi.DirectX11) - throw new InvalidOperationException("Current platform API is " + PlatformApi); - return egl.CreatePBufferFromClientBuffer(EGL_D3D_TEXTURE_ANGLE, handle, new[] { EGL_NONE, EGL_NONE }); - } - - public EglSurface WrapDirect3D11Texture(EglPlatformOpenGlInterface egl, IntPtr handle, int offsetX, int offsetY, int width, int height) - { - if (PlatformApi != AngleOptions.PlatformApi.DirectX11) - throw new InvalidOperationException("Current platform API is " + PlatformApi); - return egl.CreatePBufferFromClientBuffer(EGL_D3D_TEXTURE_ANGLE, handle, new[] { EGL_WIDTH, width, EGL_HEIGHT, height, EGL_FLEXIBLE_SURFACE_COMPATIBILITY_SUPPORTED_ANGLE, EGL_TRUE, EGL_TEXTURE_OFFSET_X_ANGLE, offsetX, EGL_TEXTURE_OFFSET_Y_ANGLE, offsetY, EGL_NONE }); - } - } -} diff --git a/src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs b/src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs index e13ee80864..c62a3d8d2f 100644 --- a/src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs +++ b/src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs @@ -1,8 +1,10 @@ using System; +using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Logging; using Avalonia.Media; using Avalonia.OpenGL.Imaging; +using Avalonia.VisualTree; using static Avalonia.OpenGL.GlConsts; namespace Avalonia.OpenGL.Controls @@ -14,8 +16,10 @@ namespace Avalonia.OpenGL.Controls private OpenGlBitmap _bitmap; private IOpenGlBitmapAttachment _attachment; private PixelSize _depthBufferSize; - private bool _glFailed; - private bool _initialized; + + private Task _initialization; + private IOpenGlTextureSharingRenderInterfaceContextFeature _feature; + protected GlVersion GlVersion { get; private set; } public sealed override void Render(DrawingContext context) { @@ -38,13 +42,6 @@ namespace Avalonia.OpenGL.Controls base.Render(context); } - private static void CheckError(GlInterface gl) - { - int err; - while ((err = gl.GetError()) != GL_NO_ERROR) - Console.WriteLine(err); - } - void EnsureTextureAttachment() { _context.GlInterface.BindFramebuffer(GL_FRAMEBUFFER, _fb); @@ -54,7 +51,7 @@ namespace Avalonia.OpenGL.Controls _attachment = null; _bitmap?.Dispose(); _bitmap = null; - _bitmap = new OpenGlBitmap(GetPixelSize(), new Vector(96, 96)); + _bitmap = new OpenGlBitmap(_feature, GetPixelSize(), new Vector(96, 96)); _attachment = _bitmap.CreateFramebufferAttachment(_context); } } @@ -87,9 +84,11 @@ namespace Avalonia.OpenGL.Controls gl.ActiveTexture(GL_TEXTURE0); gl.BindTexture(GL_TEXTURE_2D, 0); gl.BindFramebuffer(GL_FRAMEBUFFER, 0); - gl.DeleteFramebuffer(_fb); + if (_fb != 0) + gl.DeleteFramebuffer(_fb); _fb = 0; - gl.DeleteRenderbuffer(_depthBuffer); + if (_depthBuffer != 0) + gl.DeleteRenderbuffer(_depthBuffer); _depthBuffer = 0; _attachment?.Dispose(); _attachment = null; @@ -98,10 +97,10 @@ namespace Avalonia.OpenGL.Controls try { - if (_initialized) + if (_initialization is { Status: TaskStatus.RanToCompletion, Result: true }) { - _initialized = false; OnOpenGlDeinit(_context.GlInterface, _fb); + _initialization = null; } } finally @@ -111,6 +110,11 @@ namespace Avalonia.OpenGL.Controls } } } + + _fb = _depthBuffer = 0; + _attachment = null; + _bitmap = null; + _feature = null; } protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) @@ -119,26 +123,12 @@ namespace Avalonia.OpenGL.Controls base.OnDetachedFromVisualTree(e); } - private bool EnsureInitializedCore() + private bool EnsureInitializedCore(IOpenGlTextureSharingRenderInterfaceContextFeature feature) { - if (_context != null) - return true; - - if (_glFailed) - return false; - - var feature = AvaloniaLocator.Current.GetService(); - if (feature == null) - return false; - if (!feature.CanShareContexts) - { - Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log("OpenGlControlBase", - "Unable to initialize OpenGL: current platform does not support multithreaded context sharing"); - return false; - } try { _context = feature.CreateSharedContext(); + _feature = feature; } catch (Exception e) { @@ -157,7 +147,7 @@ namespace Avalonia.OpenGL.Controls GlVersion = _context.Version; try { - _bitmap = new OpenGlBitmap(GetPixelSize(), new Vector(96, 96)); + _bitmap = new OpenGlBitmap(_feature, GetPixelSize(), new Vector(96, 96)); if (!_bitmap.SupportsContext(_context)) { Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log("OpenGlControlBase", @@ -212,15 +202,68 @@ namespace Avalonia.OpenGL.Controls return true; } + void ContextLost() + { + _context = null; + _feature = null; + _initialization = null; + _attachment = null; + _bitmap = null; + _fb = 0; + _depthBuffer = 0; + _depthBufferSize = default; + OnOpenGlLost(); + } + private bool EnsureInitialized() { - if (_initialized) - return true; - _glFailed = !(_initialized = EnsureInitializedCore()); - if (_glFailed) + if (_initialization != null) + { + // Check if we've previously failed to initialize OpenGL on this platform + if (_initialization is { IsCompleted: true, Result: false } || + _initialization?.IsFaulted == true) + return false; + + // Check if we are still waiting for init to complete + if (_initialization is { IsCompleted: false }) + return false; + + if (_context.IsLost) + ContextLost(); + else + return true; + } + + _initialization = InitializeAsync(); + return false; + + } + + private async Task InitializeAsync() + { + var contextSharingFeature = + (IOpenGlTextureSharingRenderInterfaceContextFeature) + await this.GetVisualRoot()!.Renderer.TryGetRenderInterfaceFeature( + typeof(IOpenGlTextureSharingRenderInterfaceContextFeature)); + + if (contextSharingFeature == null || !contextSharingFeature.CanCreateSharedContext) + { + Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log("OpenGlControlBase", + "Unable to initialize OpenGL: current platform does not support multithreaded context sharing"); return false; + } + + if (!EnsureInitializedCore(contextSharingFeature)) + { + DoCleanup(); + return false; + } + using (_context.MakeCurrent()) OnOpenGlInit(_context.GlInterface, _fb); + + InvalidateVisual(); + return true; } @@ -242,6 +285,11 @@ namespace Avalonia.OpenGL.Controls } + protected virtual void OnOpenGlLost() + { + + } + protected abstract void OnOpenGlRender(GlInterface gl, int fb); } } diff --git a/src/Avalonia.OpenGL/Egl/EglConsts.cs b/src/Avalonia.OpenGL/Egl/EglConsts.cs index 1268845f0f..428d11857f 100644 --- a/src/Avalonia.OpenGL/Egl/EglConsts.cs +++ b/src/Avalonia.OpenGL/Egl/EglConsts.cs @@ -196,6 +196,11 @@ namespace Avalonia.OpenGL.Egl // public const int EGL_PLATFORM_ANGLE_DEVICE_TYPE_D3D_WARP_ANGLE = 0x320B; // public const int EGL_PLATFORM_ANGLE_DEVICE_TYPE_D3D_REFERENCE_ANGLE = 0x320C; +// + // EXT_platform_device + public const int EGL_PLATFORM_DEVICE_EXT = 0x313F; + + //EXT_device_query public const int EGL_DEVICE_EXT = 0x322C; diff --git a/src/Avalonia.OpenGL/Egl/EglContext.cs b/src/Avalonia.OpenGL/Egl/EglContext.cs index e7b5dc7c83..4d75a776c3 100644 --- a/src/Avalonia.OpenGL/Egl/EglContext.cs +++ b/src/Avalonia.OpenGL/Egl/EglContext.cs @@ -1,6 +1,8 @@ using System; +using System.Collections.Generic; using System.Reactive.Disposables; using System.Threading; +using Avalonia.Platform; using static Avalonia.OpenGL.Egl.EglConsts; namespace Avalonia.OpenGL.Egl @@ -10,24 +12,32 @@ namespace Avalonia.OpenGL.Egl private readonly EglDisplay _disp; private readonly EglInterface _egl; private readonly EglContext _sharedWith; - private readonly object _lock = new object(); + private bool _isLost; + private IntPtr _context; + private readonly Action _disposeCallback; + private readonly Dictionary _features; + private readonly object _lock; - public EglContext(EglDisplay display, EglInterface egl, EglContext sharedWith, IntPtr ctx, Func offscreenSurface, - GlVersion version, int sampleCount, int stencilSize) + internal EglContext(EglDisplay display, EglInterface egl, EglContext sharedWith, IntPtr ctx, EglSurface offscreenSurface, + GlVersion version, int sampleCount, int stencilSize, Action disposeCallback, Dictionary features) { _disp = display; _egl = egl; _sharedWith = sharedWith; - Context = ctx; - OffscreenSurface = offscreenSurface(this); + _context = ctx; + _disposeCallback = disposeCallback; + _features = features; + OffscreenSurface = offscreenSurface; Version = version; SampleCount = sampleCount; StencilSize = stencilSize; + _lock = display.ContextSharedSyncRoot ?? new object(); using (MakeCurrent()) GlInterface = GlInterface.FromNativeUtf8GetProcAddress(version, _egl.GetProcAddress); } - public IntPtr Context { get; } + public IntPtr Context => + _context == IntPtr.Zero ? throw new ObjectDisposedException(nameof(EglContext)) : _context; public EglSurface OffscreenSurface { get; } public GlVersion Version { get; } public GlInterface GlInterface { get; } @@ -40,7 +50,7 @@ namespace Avalonia.OpenGL.Egl private readonly EglInterface _egl; private readonly object _l; private readonly IntPtr _display; - private IntPtr _context, _read, _draw; + public IntPtr _context, _read, _draw; public RestoreContext(EglInterface egl, IntPtr defDisplay, object l) { @@ -66,6 +76,9 @@ namespace Avalonia.OpenGL.Egl public IDisposable MakeCurrent(EglSurface surface) { + if (IsLost) + throw new PlatformGraphicsContextLostException(); + Monitor.Enter(_lock); var success = false; try @@ -74,8 +87,18 @@ namespace Avalonia.OpenGL.Egl var surf = surface ?? OffscreenSurface; _egl.MakeCurrent(_disp.Handle, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero); if (!_egl.MakeCurrent(_disp.Handle, surf?.DangerousGetHandle() ?? IntPtr.Zero, - surf?.DangerousGetHandle() ?? IntPtr.Zero, Context)) - throw OpenGlException.GetFormattedException("eglMakeCurrent", _egl); + surf?.DangerousGetHandle() ?? IntPtr.Zero, Context)) + { + var error = _egl.GetError(); + if (error == EGL_CONTEXT_LOST) + { + NotifyContextLost(); + throw new PlatformGraphicsContextLostException(); + } + + throw OpenGlException.GetFormattedEglException("eglMakeCurrent", error); + } + success = true; return old; } @@ -85,7 +108,15 @@ namespace Avalonia.OpenGL.Egl Monitor.Exit(_lock); } } + + public void NotifyContextLost() + { + _isLost = true; + _disp.OnContextLost(this); + } + public bool IsLost => _isLost || _disp.IsLost || Context == IntPtr.Zero; + public IDisposable EnsureCurrent() { if(IsCurrent) @@ -110,12 +141,32 @@ namespace Avalonia.OpenGL.Egl || _sharedWith != null && _sharedWith == c._sharedWith; } + public bool CanCreateSharedContext => _disp.SupportsSharing; + + public IGlContext CreateSharedContext(IEnumerable preferredVersions = null) => + _disp.CreateContext(new EglContextOptions + { + ShareWith = _sharedWith ?? this + }); + public bool IsCurrent => _egl.GetCurrentDisplay() == _disp.Handle && _egl.GetCurrentContext() == Context; public void Dispose() { + if(_context == IntPtr.Zero) + return; _egl.DestroyContext(_disp.Handle, Context); OffscreenSurface?.Dispose(); + _context = IntPtr.Zero; + _disp.OnContextDisposed(this); + _disposeCallback?.Invoke(); + } + + public object TryGetFeature(Type featureType) + { + if (_features?.TryGetValue(featureType, out var feature) == true) + return feature; + return null; } } } diff --git a/src/Avalonia.OpenGL/Egl/EglDisplay.cs b/src/Avalonia.OpenGL/Egl/EglDisplay.cs index d3d85e8c83..59d8bcf350 100644 --- a/src/Avalonia.OpenGL/Egl/EglDisplay.cs +++ b/src/Avalonia.OpenGL/Egl/EglDisplay.cs @@ -1,193 +1,176 @@ using System; +using System.Collections.Generic; using System.Linq; +using System.Reactive.Disposables; +using System.Threading; using static Avalonia.OpenGL.Egl.EglConsts; namespace Avalonia.OpenGL.Egl { - public class EglDisplay + public class EglDisplay : IDisposable { private readonly EglInterface _egl; + private IntPtr _display; + private readonly EglDisplayOptions _options; + private EglConfigInfo _config; + private bool _isLost; + private object _lock = new(); + public bool SupportsSharing { get; } - private readonly IntPtr _display; - private readonly IntPtr _config; - private readonly int[] _contextAttributes; - private readonly int _surfaceType; public IntPtr Handle => _display; - public IntPtr Config => _config; - private int _sampleCount; - private int _stencilSize; - private GlVersion _version; - - public EglDisplay(EglInterface egl, bool supportsSharing) : this(egl, supportsSharing, -1, IntPtr.Zero, null) + public IntPtr Config => _config.Config; + internal bool SingleContext => !_options.SupportsMultipleContexts; + private List _contexts = new(); + + public EglDisplay() : this(new EglDisplayCreationOptions { - - } - - static IntPtr CreateDisplay(EglInterface egl, int platformType, IntPtr platformDisplay, int[] attrs) + Egl = new EglInterface() + }) { - var display = IntPtr.Zero; - if (platformType == -1 && platformDisplay == IntPtr.Zero) - { - if (display == IntPtr.Zero) - display = egl.GetDisplay(IntPtr.Zero); - } - else - { - if (!egl.IsGetPlatformDisplayExtAvailable) - throw new OpenGlException("eglGetPlatformDisplayEXT is not supported by libegl"); - display = egl.GetPlatformDisplayExt(platformType, platformDisplay, attrs); - } - - if (display == IntPtr.Zero) - throw OpenGlException.GetFormattedException("eglGetDisplay", egl); - return display; + } - public EglDisplay(EglInterface egl, bool supportsSharing, int platformType, IntPtr platformDisplay, int[] attrs) - : this(egl, supportsSharing, CreateDisplay(egl, platformType, platformDisplay, attrs)) + public EglDisplay(EglDisplayCreationOptions options) : this(EglDisplayUtils.CreateDisplay(options), options) { - + } - - public EglDisplay(EglInterface egl, bool supportsSharing, IntPtr display) + + public EglDisplay(IntPtr display, EglDisplayOptions options) { - _egl = egl; - SupportsSharing = supportsSharing; + _egl = options.Egl; + SupportsSharing = options.SupportsContextSharing; _display = display; + _options = options; if(_display == IntPtr.Zero) throw new ArgumentException(); - - - if (!_egl.Initialize(_display, out var major, out var minor)) - throw OpenGlException.GetFormattedException("eglInitialize", _egl); - var glProfiles = AvaloniaLocator.Current.GetService()?.GlProfiles - ?? new[] - { - new GlVersion(GlProfileType.OpenGLES, 3, 0), - new GlVersion(GlProfileType.OpenGLES, 2, 0) - }; - - var cfgs = glProfiles.Select(x => + _config = EglDisplayUtils.InitializeAndGetConfig(_egl, display, options.GlVersions); + } + + public EglInterface EglInterface => _egl; + public EglContext CreateContext(EglContextOptions options) + { + options ??= new EglContextOptions(); + lock (_lock) { - var typeBit = EGL_OPENGL_ES3_BIT; + var share = options.ShareWith; + if (share != null && !SupportsSharing) + throw new NotSupportedException("Context sharing is not supported by this display"); - switch (x.Major) - { - case 2: - typeBit = EGL_OPENGL_ES2_BIT; - break; + var offscreenSurface = options.OffscreenSurface; - case 1: - typeBit = EGL_OPENGL_ES_BIT; - break; - } - - return new + if (offscreenSurface == null) { - Attributes = new[] + // Check if eglMakeCurrent can work with EGL_NONE as read-write surfaces + var extensions = _egl.QueryString(Handle, EGL_EXTENSIONS); + if (extensions?.Contains("EGL_KHR_surfaceless_context") != true) { - EGL_CONTEXT_MAJOR_VERSION, x.Major, - EGL_CONTEXT_MINOR_VERSION, x.Minor, - EGL_NONE - }, - Api = EGL_OPENGL_ES_API, - RenderableTypeBit = typeBit, - Version = x - }; - }); - - foreach (var cfg in cfgs) - { - if (!_egl.BindApi(cfg.Api)) - continue; - foreach(var surfaceType in new[]{EGL_PBUFFER_BIT|EGL_WINDOW_BIT, EGL_WINDOW_BIT}) - foreach(var stencilSize in new[]{8, 1, 0}) - foreach (var depthSize in new []{8, 1, 0}) + // Attempt to create a PBuffer as a surface for offscreen rendering + if ((_config.SurfaceType | EGL_PBUFFER_BIT) == 0) + throw new InvalidOperationException( + "Platform doesn't support EGL_KHR_surfaceless_context and PBUFFER surfaces"); + + var pBufferSurface = _egl.CreatePBufferSurface(_display, Config, + new[] { EGL_WIDTH, 1, EGL_HEIGHT, 1, EGL_NONE }); + if (pBufferSurface == IntPtr.Zero) + throw OpenGlException.GetFormattedException("eglCreatePBufferSurface", _egl); + + offscreenSurface = new EglSurface(this, pBufferSurface); + } + } + + var ctx = _egl.CreateContext(_display, Config, share?.Context ?? IntPtr.Zero, _config.Attributes); + if (ctx == IntPtr.Zero) { - var attribs = new[] - { - EGL_SURFACE_TYPE, surfaceType, - EGL_RENDERABLE_TYPE, cfg.RenderableTypeBit, - EGL_RED_SIZE, 8, - EGL_GREEN_SIZE, 8, - EGL_BLUE_SIZE, 8, - EGL_ALPHA_SIZE, 8, - EGL_STENCIL_SIZE, stencilSize, - EGL_DEPTH_SIZE, depthSize, - EGL_NONE - }; - if (!_egl.ChooseConfig(_display, attribs, out _config, 1, out int numConfigs)) - continue; - if (numConfigs == 0) - continue; - _contextAttributes = cfg.Attributes; - _surfaceType = surfaceType; - _version = cfg.Version; - _egl.GetConfigAttrib(_display, _config, EGL_SAMPLES, out _sampleCount); - _egl.GetConfigAttrib(_display, _config, EGL_STENCIL_SIZE, out _stencilSize); - goto Found; + var ex = OpenGlException.GetFormattedException("eglCreateContext", _egl); + offscreenSurface?.Dispose(); + throw ex; } + var rv = new EglContext(this, _egl, share, ctx, offscreenSurface, + _config.Version, _config.SampleCount, _config.StencilSize, + options.DisposeCallback, options.ExtraFeatures); + _contexts.Add(rv); + return rv; } - Found: - if (_contextAttributes == null) - throw new OpenGlException("No suitable EGL config was found"); } - public EglDisplay() : this(false) + public EglSurface CreateWindowSurface(IntPtr window) { - + if (window == IntPtr.Zero) + throw new OpenGlException($"Window {window} is invalid."); + + using (Lock()) + { + var s = EglInterface.CreateWindowSurface(Handle, Config, window, + new[] { EGL_NONE, EGL_NONE }); + if (s == IntPtr.Zero) + throw OpenGlException.GetFormattedException("eglCreateWindowSurface", EglInterface); + return new EglSurface(this, s); + } } - public EglDisplay(bool supportsSharing) : this(new EglInterface(), supportsSharing) + public EglSurface CreatePBufferFromClientBuffer (int bufferType, IntPtr handle, int[] attribs) { - + using (Lock()) + { + var s = EglInterface.CreatePbufferFromClientBuffer(Handle, bufferType, handle, + Config, attribs); + + if (s == IntPtr.Zero) + throw OpenGlException.GetFormattedException("eglCreatePbufferFromClientBuffer", EglInterface); + return new EglSurface(this, s); + } } + + protected virtual bool DisplayLockIsSharedWithContexts => false; - public EglInterface EglInterface => _egl; - public EglContext CreateContext(IGlContext share) + internal object ContextSharedSyncRoot => DisplayLockIsSharedWithContexts ? _lock : null; + + internal void OnContextLost(EglContext context) { - if (share != null && !SupportsSharing) - throw new NotSupportedException("Context sharing is not supported by this display"); - - if((_surfaceType|EGL_PBUFFER_BIT) == 0) - throw new InvalidOperationException("Platform doesn't support PBUFFER surfaces"); - var shareCtx = (EglContext)share; - var ctx = _egl.CreateContext(_display, _config, shareCtx?.Context ?? IntPtr.Zero, _contextAttributes); - if (ctx == IntPtr.Zero) - throw OpenGlException.GetFormattedException("eglCreateContext", _egl); - - var extensions = _egl.QueryString(Handle, EGL_EXTENSIONS); - - IntPtr surf = IntPtr.Zero; - if (extensions?.Contains("EGL_KHR_surfaceless_context") != true) + if (_options.ContextLossIsDisplayLoss) + _isLost = true; + } + + internal void OnContextDisposed(EglContext context) + { + lock (_lock) + _contexts.Remove(context); + } + + public bool IsLost + { + get { - surf = _egl.CreatePBufferSurface(_display, _config, - new[] { EGL_WIDTH, 1, EGL_HEIGHT, 1, EGL_NONE }); - if (surf == IntPtr.Zero) - throw OpenGlException.GetFormattedException("eglCreatePBufferSurface", _egl); + if (_isLost || _display == IntPtr.Zero) + return true; + if (_options.DeviceLostCheckCallback?.Invoke() == true) + return _isLost = true; + return false; } - - var rv = new EglContext(this, _egl, shareCtx, ctx, - context => - surf == IntPtr.Zero ? null : new EglSurface(this, context, surf), - _version, _sampleCount, _stencilSize); - return rv; + } + + public IDisposable Lock() + { + Monitor.Enter(_lock); + return Disposable.Create(() => { Monitor.Exit(_lock); }); } - public EglContext CreateContext(EglContext share, EglSurface offscreenSurface) + public void Dispose() { - if (share != null && !SupportsSharing) - throw new NotSupportedException("Context sharing is not supported by this display"); - - var ctx = _egl.CreateContext(_display, _config, share?.Context ?? IntPtr.Zero, _contextAttributes); - if (ctx == IntPtr.Zero) - throw OpenGlException.GetFormattedException("eglCreateContext", _egl); - var rv = new EglContext(this, _egl, share, ctx, _ => offscreenSurface, _version, _sampleCount, _stencilSize); - rv.MakeCurrent(null); - return rv; + lock (_lock) + { + foreach(var ctx in _contexts) + ctx.Dispose(); + _contexts.Clear(); + if (_display != IntPtr.Zero) + _egl.Terminate(_display); + _display = IntPtr.Zero; + _config = null; + _options.DisposeCallback?.Invoke(); + } } } } diff --git a/src/Avalonia.OpenGL/Egl/EglDisplayOptions.cs b/src/Avalonia.OpenGL/Egl/EglDisplayOptions.cs new file mode 100644 index 0000000000..5648645c54 --- /dev/null +++ b/src/Avalonia.OpenGL/Egl/EglDisplayOptions.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; + +namespace Avalonia.OpenGL.Egl; + +public class EglDisplayOptions +{ + public EglInterface Egl { get; set; } + public bool SupportsContextSharing { get; set; } + public bool SupportsMultipleContexts { get; set; } + public bool ContextLossIsDisplayLoss { get; set; } + public Func DeviceLostCheckCallback { get; set; } + public Action DisposeCallback { get; set; } + public IEnumerable GlVersions { get; set; } +} + +public class EglContextOptions +{ + public EglContext ShareWith { get; set; } + public EglSurface OffscreenSurface { get; set; } + public Action DisposeCallback { get; set; } + public Dictionary ExtraFeatures { get; set; } +} + +public class EglDisplayCreationOptions : EglDisplayOptions +{ + public int? PlatformType { get; set; } + public IntPtr PlatformDisplay { get; set; } + public int[] PlatformDisplayAttrs { get; set; } +} diff --git a/src/Avalonia.OpenGL/Egl/EglDisplayUtils.cs b/src/Avalonia.OpenGL/Egl/EglDisplayUtils.cs new file mode 100644 index 0000000000..fbfaf1bd3d --- /dev/null +++ b/src/Avalonia.OpenGL/Egl/EglDisplayUtils.cs @@ -0,0 +1,135 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reactive.Disposables; +using System.Threading; +using static Avalonia.OpenGL.Egl.EglConsts; +namespace Avalonia.OpenGL.Egl; + +static class EglDisplayUtils +{ + public static IntPtr CreateDisplay(EglDisplayCreationOptions options) + { + var egl = options.Egl; + var display = IntPtr.Zero; + if (options.PlatformType == null) + { + if (display == IntPtr.Zero) + display = egl.GetDisplay(IntPtr.Zero); + } + else + { + if (!egl.IsGetPlatformDisplayExtAvailable) + throw new OpenGlException("eglGetPlatformDisplayEXT is not supported by libegl"); + + display = egl.GetPlatformDisplayExt(options.PlatformType.Value, options.PlatformDisplay, + options.PlatformDisplayAttrs); + } + + if (display == IntPtr.Zero) + throw OpenGlException.GetFormattedException("eglGetDisplay", egl); + return display; + } + + public static EglConfigInfo InitializeAndGetConfig(EglInterface egl, IntPtr display, IEnumerable versions) + { + if (!egl.Initialize(display, out var major, out var minor)) + throw OpenGlException.GetFormattedException("eglInitialize", egl); + + // TODO: AvaloniaLocator.Current.GetService()?.GlProfiles + versions ??= new[] + { + new GlVersion(GlProfileType.OpenGLES, 3, 0), + new GlVersion(GlProfileType.OpenGLES, 2, 0) + }; + + var cfgs = versions + .Where(x => x.Type == GlProfileType.OpenGLES) + .Select(x => + { + var typeBit = EGL_OPENGL_ES3_BIT; + + switch (x.Major) + { + case 2: + typeBit = EGL_OPENGL_ES2_BIT; + break; + + case 1: + typeBit = EGL_OPENGL_ES_BIT; + break; + } + + return new + { + Attributes = new[] + { + EGL_CONTEXT_MAJOR_VERSION, x.Major, + EGL_CONTEXT_MINOR_VERSION, x.Minor, + EGL_NONE + }, + Api = EGL_OPENGL_ES_API, + RenderableTypeBit = typeBit, + Version = x + }; + }); + + foreach (var cfg in cfgs) + { + if (!egl.BindApi(cfg.Api)) + continue; + foreach (var surfaceType in new[] { EGL_PBUFFER_BIT | EGL_WINDOW_BIT, EGL_WINDOW_BIT }) + foreach (var stencilSize in new[] { 8, 1, 0 }) + foreach (var depthSize in new[] { 8, 1, 0 }) + { + var attribs = new[] + { + EGL_SURFACE_TYPE, surfaceType, + EGL_RENDERABLE_TYPE, cfg.RenderableTypeBit, + EGL_RED_SIZE, 8, + EGL_GREEN_SIZE, 8, + EGL_BLUE_SIZE, 8, + EGL_ALPHA_SIZE, 8, + EGL_STENCIL_SIZE, stencilSize, + EGL_DEPTH_SIZE, depthSize, + EGL_NONE + }; + if (!egl.ChooseConfig(display, attribs, out var config, 1, out int numConfigs)) + continue; + if (numConfigs == 0) + continue; + + + egl.GetConfigAttrib(display, config, EGL_SAMPLES, out var sampleCount); + egl.GetConfigAttrib(display, config, EGL_STENCIL_SIZE, out var returnedStencilSize); + return new EglConfigInfo(config, cfg.Version, surfaceType, cfg.Attributes, sampleCount, + returnedStencilSize); + } + } + + throw new OpenGlException("No suitable EGL config was found"); + } + + +} + +class EglConfigInfo +{ + public IntPtr Config { get; } + public GlVersion Version { get; } + public int SurfaceType { get; } + public int[] Attributes { get; } + public int SampleCount { get; } + public int StencilSize { get; } + + public EglConfigInfo(IntPtr config, GlVersion version, int surfaceType, int[] attributes, int sampleCount, + int stencilSize) + { + Config = config; + Version = version; + SurfaceType = surfaceType; + Attributes = attributes; + SampleCount = sampleCount; + StencilSize = stencilSize; + } +} \ No newline at end of file diff --git a/src/Avalonia.OpenGL/Egl/EglGlPlatformSurface.cs b/src/Avalonia.OpenGL/Egl/EglGlPlatformSurface.cs index 3d58660d47..348028bd08 100644 --- a/src/Avalonia.OpenGL/Egl/EglGlPlatformSurface.cs +++ b/src/Avalonia.OpenGL/Egl/EglGlPlatformSurface.cs @@ -1,52 +1,61 @@ +using System; using Avalonia.OpenGL.Surfaces; namespace Avalonia.OpenGL.Egl { public class EglGlPlatformSurface : EglGlPlatformSurfaceBase { - private readonly EglPlatformOpenGlInterface _egl; + public interface IEglWindowGlPlatformSurfaceInfo + { + IntPtr Handle { get; } + PixelSize Size { get; } + double Scaling { get; } + } + private readonly IEglWindowGlPlatformSurfaceInfo _info; - public EglGlPlatformSurface(EglPlatformOpenGlInterface egl, IEglWindowGlPlatformSurfaceInfo info) : base() + public EglGlPlatformSurface(IEglWindowGlPlatformSurfaceInfo info) { - _egl = egl; _info = info; } - public override IGlPlatformSurfaceRenderTarget CreateGlRenderTarget() + public override IGlPlatformSurfaceRenderTarget CreateGlRenderTarget(IGlContext context) { - var glSurface = _egl.CreateWindowSurface(_info.Handle); - return new RenderTarget(_egl, glSurface, _info); + var eglContext = (EglContext)context; + + var glSurface = eglContext.Display.CreateWindowSurface(_info.Handle); + return new RenderTarget(glSurface, eglContext, _info); } class RenderTarget : EglPlatformSurfaceRenderTargetBase { - private readonly EglPlatformOpenGlInterface _egl; private EglSurface _glSurface; private readonly IEglWindowGlPlatformSurfaceInfo _info; private PixelSize _currentSize; + private readonly IntPtr _handle; - public RenderTarget(EglPlatformOpenGlInterface egl, - EglSurface glSurface, IEglWindowGlPlatformSurfaceInfo info) : base(egl) + public RenderTarget(EglSurface glSurface, EglContext context, IEglWindowGlPlatformSurfaceInfo info) : base(context) { - _egl = egl; _glSurface = glSurface; _info = info; _currentSize = info.Size; + _handle = _info.Handle; } public override void Dispose() => _glSurface.Dispose(); - - public override IGlPlatformSurfaceRenderingSession BeginDraw() + + public override IGlPlatformSurfaceRenderingSession BeginDrawCore() { - if (_info.Size != _currentSize || _glSurface == null) + if (_info.Size != _currentSize + || _handle != _info.Handle + || _glSurface == null) { _glSurface?.Dispose(); _glSurface = null; - _glSurface = _egl.CreateWindowSurface(_info.Handle); + _glSurface = Context.Display.CreateWindowSurface(_info.Handle); _currentSize = _info.Size; } - return base.BeginDraw(_glSurface, _info); + return base.BeginDraw(_glSurface, _info.Size, _info.Scaling); } } } diff --git a/src/Avalonia.OpenGL/Egl/EglGlPlatformSurfaceBase.cs b/src/Avalonia.OpenGL/Egl/EglGlPlatformSurfaceBase.cs index 4ea6766de2..f66f630556 100644 --- a/src/Avalonia.OpenGL/Egl/EglGlPlatformSurfaceBase.cs +++ b/src/Avalonia.OpenGL/Egl/EglGlPlatformSurfaceBase.cs @@ -5,23 +5,16 @@ namespace Avalonia.OpenGL.Egl { public abstract class EglGlPlatformSurfaceBase : IGlPlatformSurface { - public interface IEglWindowGlPlatformSurfaceInfo - { - IntPtr Handle { get; } - PixelSize Size { get; } - double Scaling { get; } - } - - public abstract IGlPlatformSurfaceRenderTarget CreateGlRenderTarget(); + public abstract IGlPlatformSurfaceRenderTarget CreateGlRenderTarget(IGlContext context); } - public abstract class EglPlatformSurfaceRenderTargetBase : IGlPlatformSurfaceRenderTarget + public abstract class EglPlatformSurfaceRenderTargetBase : IGlPlatformSurfaceRenderTargetWithCorruptionInfo { - private readonly EglPlatformOpenGlInterface _egl; + protected EglContext Context { get; } - protected EglPlatformSurfaceRenderTargetBase(EglPlatformOpenGlInterface egl) + protected EglPlatformSurfaceRenderTargetBase(EglContext context) { - _egl = egl; + Context = context; } public virtual void Dispose() @@ -29,25 +22,33 @@ namespace Avalonia.OpenGL.Egl } - public abstract IGlPlatformSurfaceRenderingSession BeginDraw(); + public IGlPlatformSurfaceRenderingSession BeginDraw() + { + if (Context.IsLost) + throw new RenderTargetCorruptedException(); + + return BeginDrawCore(); + } + + public abstract IGlPlatformSurfaceRenderingSession BeginDrawCore(); protected IGlPlatformSurfaceRenderingSession BeginDraw(EglSurface surface, - EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo info, Action onFinish = null, bool isYFlipped = false) + PixelSize size, double scaling, Action onFinish = null, bool isYFlipped = false) { - var restoreContext = _egl.PrimaryEglContext.MakeCurrent(surface); + var restoreContext = Context.MakeCurrent(surface); var success = false; try { - var egli = _egl.Display.EglInterface; + var egli = Context.Display.EglInterface; egli.WaitClient(); egli.WaitGL(); egli.WaitNative(EglConsts.EGL_CORE_NATIVE_ENGINE); - _egl.PrimaryContext.GlInterface.BindFramebuffer(GlConsts.GL_FRAMEBUFFER, 0); + Context.GlInterface.BindFramebuffer(GlConsts.GL_FRAMEBUFFER, 0); success = true; - return new Session(_egl.Display, _egl.PrimaryEglContext, surface, info, restoreContext, onFinish, isYFlipped); + return new Session(Context.Display, Context, surface, size, scaling, restoreContext, onFinish, isYFlipped); } finally { @@ -60,21 +61,21 @@ namespace Avalonia.OpenGL.Egl { private readonly EglContext _context; private readonly EglSurface _glSurface; - private readonly EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo _info; private readonly EglDisplay _display; private readonly IDisposable _restoreContext; private readonly Action _onFinish; public Session(EglDisplay display, EglContext context, - EglSurface glSurface, EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo info, + EglSurface glSurface, PixelSize size, double scaling, IDisposable restoreContext, Action onFinish, bool isYFlipped) { + Size = size; + Scaling = scaling; IsYFlipped = isYFlipped; _context = context; _display = display; _glSurface = glSurface; - _info = info; _restoreContext = restoreContext; _onFinish = onFinish; } @@ -92,9 +93,11 @@ namespace Avalonia.OpenGL.Egl } public IGlContext Context => _context; - public PixelSize Size => _info.Size; - public double Scaling => _info.Scaling; + public PixelSize Size { get; } + public double Scaling { get; } public bool IsYFlipped { get; } } + + public virtual bool IsCorrupted => Context.IsLost; } } diff --git a/src/Avalonia.OpenGL/Egl/EglInterface.cs b/src/Avalonia.OpenGL/Egl/EglInterface.cs index 4fec8e5356..ad4b55a686 100644 --- a/src/Avalonia.OpenGL/Egl/EglInterface.cs +++ b/src/Avalonia.OpenGL/Egl/EglInterface.cs @@ -53,6 +53,9 @@ namespace Avalonia.OpenGL.Egl [GetProcAddress("eglInitialize")] public partial bool Initialize(IntPtr display, out int major, out int minor); + + [GetProcAddress("eglTerminate")] + public partial void Terminate(IntPtr display); [GetProcAddress("eglGetProcAddress")] public partial IntPtr GetProcAddress(IntPtr proc); diff --git a/src/Avalonia.OpenGL/Egl/EglPlatformGraphics.cs b/src/Avalonia.OpenGL/Egl/EglPlatformGraphics.cs new file mode 100644 index 0000000000..faa9f279a6 --- /dev/null +++ b/src/Avalonia.OpenGL/Egl/EglPlatformGraphics.cs @@ -0,0 +1,48 @@ +using System; +using Avalonia.Logging; +using Avalonia.Platform; +using static Avalonia.OpenGL.Egl.EglConsts; + +namespace Avalonia.OpenGL.Egl +{ + public sealed class EglPlatformGraphics : IPlatformGraphics + { + private readonly EglDisplay _display; + public bool UsesSharedContext => false; + public IPlatformGraphicsContext CreateContext() => _display.CreateContext(null); + public IPlatformGraphicsContext GetSharedContext() => throw new NotSupportedException(); + + public EglPlatformGraphics(EglDisplay display) + { + _display = display; + } + + public static void TryInitialize() + { + var feature = TryCreate(); + if (feature != null) + AvaloniaLocator.CurrentMutable.Bind().ToConstant(feature); + } + + public static EglPlatformGraphics TryCreate() => TryCreate(() => new EglDisplay(new EglDisplayCreationOptions + { + Egl = new EglInterface(), + // Those are expected to be supported by most EGL implementations + SupportsMultipleContexts = true, + SupportsContextSharing = true + })); + + public static EglPlatformGraphics TryCreate(Func displayFactory) + { + try + { + return new EglPlatformGraphics(displayFactory()); + } + catch(Exception e) + { + Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log(null, "Unable to initialize EGL-based rendering: {0}", e); + return null; + } + } + } +} diff --git a/src/Avalonia.OpenGL/Egl/EglPlatformOpenGlInterface.cs b/src/Avalonia.OpenGL/Egl/EglPlatformOpenGlInterface.cs deleted file mode 100644 index a1ac2a9d37..0000000000 --- a/src/Avalonia.OpenGL/Egl/EglPlatformOpenGlInterface.cs +++ /dev/null @@ -1,77 +0,0 @@ -using System; -using Avalonia.Logging; -using Avalonia.Platform; -using static Avalonia.OpenGL.Egl.EglConsts; - -namespace Avalonia.OpenGL.Egl -{ - public class EglPlatformOpenGlInterface : IPlatformOpenGlInterface - { - public EglDisplay Display { get; private set; } - public bool CanCreateContexts => true; - public bool CanShareContexts => Display.SupportsSharing; - - public EglContext PrimaryEglContext { get; } - public IGlContext PrimaryContext => PrimaryEglContext; - IPlatformGpuContext IPlatformGpu.PrimaryContext => PrimaryContext; - - public EglPlatformOpenGlInterface(EglDisplay display) - { - Display = display; - PrimaryEglContext = display.CreateContext(null); - } - - public static void TryInitialize() - { - var feature = TryCreate(); - if (feature != null) - AvaloniaLocator.CurrentMutable.Bind().ToConstant(feature); - } - - public static EglPlatformOpenGlInterface TryCreate() => TryCreate(() => new EglDisplay()); - public static EglPlatformOpenGlInterface TryCreate(Func displayFactory) - { - try - { - return new EglPlatformOpenGlInterface(displayFactory()); - } - catch(Exception e) - { - Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log(null, "Unable to initialize EGL-based rendering: {0}", e); - return null; - } - } - - public IGlContext CreateContext() => Display.CreateContext(null); - public IGlContext CreateSharedContext() => Display.CreateContext(PrimaryEglContext); - - - public EglSurface CreateWindowSurface(IntPtr window) - { - if (window == IntPtr.Zero) - throw new OpenGlException($"Window {window} is invalid."); - - using (PrimaryContext.MakeCurrent()) - { - var s = Display.EglInterface.CreateWindowSurface(Display.Handle, Display.Config, window, - new[] { EGL_NONE, EGL_NONE }); - if (s == IntPtr.Zero) - throw OpenGlException.GetFormattedException("eglCreateWindowSurface", Display.EglInterface); - return new EglSurface(Display, PrimaryEglContext, s); - } - } - - public EglSurface CreatePBufferFromClientBuffer (int bufferType, IntPtr handle, int[] attribs) - { - using (PrimaryContext.MakeCurrent()) - { - var s = Display.EglInterface.CreatePbufferFromClientBuffer(Display.Handle, bufferType, handle, - Display.Config, attribs); - - if (s == IntPtr.Zero) - throw OpenGlException.GetFormattedException("eglCreatePbufferFromClientBuffer", Display.EglInterface); - return new EglSurface(Display, PrimaryEglContext, s); - } - } - } -} diff --git a/src/Avalonia.OpenGL/Egl/EglSurface.cs b/src/Avalonia.OpenGL/Egl/EglSurface.cs index a93751ca9e..f3ee85b3f6 100644 --- a/src/Avalonia.OpenGL/Egl/EglSurface.cs +++ b/src/Avalonia.OpenGL/Egl/EglSurface.cs @@ -6,19 +6,17 @@ namespace Avalonia.OpenGL.Egl public class EglSurface : SafeHandle { private readonly EglDisplay _display; - private readonly EglContext _context; private readonly EglInterface _egl; - public EglSurface(EglDisplay display, EglContext context, IntPtr surface) : base(surface, true) + public EglSurface(EglDisplay display, IntPtr surface) : base(surface, true) { _display = display; - _context = context; _egl = display.EglInterface; } protected override bool ReleaseHandle() { - using (_context.MakeCurrent()) + using (_display.Lock()) _egl.DestroySurface(_display.Handle, handle); return true; } diff --git a/src/Avalonia.OpenGL/IGlContext.cs b/src/Avalonia.OpenGL/IGlContext.cs index a52a6535da..d45ea931d0 100644 --- a/src/Avalonia.OpenGL/IGlContext.cs +++ b/src/Avalonia.OpenGL/IGlContext.cs @@ -1,9 +1,11 @@ using System; +using System.Collections.Generic; +using Avalonia.OpenGL.Surfaces; using Avalonia.Platform; namespace Avalonia.OpenGL { - public interface IGlContext : IPlatformGpuContext + public interface IGlContext : IPlatformGraphicsContext { GlVersion Version { get; } GlInterface GlInterface { get; } @@ -12,5 +14,13 @@ namespace Avalonia.OpenGL IDisposable MakeCurrent(); IDisposable EnsureCurrent(); bool IsSharedWith(IGlContext context); + bool CanCreateSharedContext { get; } + IGlContext CreateSharedContext(IEnumerable preferredVersions = null); + } + + public interface IGlPlatformSurfaceRenderTargetFactory + { + bool CanRenderToSurface(IGlContext context, object surface); + IGlPlatformSurfaceRenderTarget CreateRenderTarget(IGlContext context, object surface); } } diff --git a/src/Avalonia.OpenGL/IOpenGlAwarePlatformRenderInterface.cs b/src/Avalonia.OpenGL/IOpenGlAwarePlatformRenderInterface.cs deleted file mode 100644 index fdb9162164..0000000000 --- a/src/Avalonia.OpenGL/IOpenGlAwarePlatformRenderInterface.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Avalonia.OpenGL.Imaging; - -namespace Avalonia.OpenGL -{ - public interface IOpenGlAwarePlatformRenderInterface - { - IOpenGlBitmapImpl CreateOpenGlBitmap(PixelSize size, Vector dpi); - } -} diff --git a/src/Avalonia.OpenGL/IOpenGlTextureSharingRenderInterfaceContextFeature.cs b/src/Avalonia.OpenGL/IOpenGlTextureSharingRenderInterfaceContextFeature.cs new file mode 100644 index 0000000000..9c22d446ef --- /dev/null +++ b/src/Avalonia.OpenGL/IOpenGlTextureSharingRenderInterfaceContextFeature.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using Avalonia.OpenGL.Imaging; +using Avalonia.Platform; + +namespace Avalonia.OpenGL +{ + public interface IOpenGlTextureSharingRenderInterfaceContextFeature + { + bool CanCreateSharedContext { get; } + IGlContext CreateSharedContext(IEnumerable preferredVersions = null); + IOpenGlBitmapImpl CreateOpenGlBitmap(PixelSize size, Vector dpi); + } +} diff --git a/src/Avalonia.OpenGL/IPlatformOpenGlInterface.cs b/src/Avalonia.OpenGL/IPlatformOpenGlInterface.cs deleted file mode 100644 index 4ff7997b03..0000000000 --- a/src/Avalonia.OpenGL/IPlatformOpenGlInterface.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Avalonia.Platform; - -namespace Avalonia.OpenGL -{ - public interface IPlatformOpenGlInterface : IPlatformGpu - { - new IGlContext PrimaryContext { get; } - IGlContext CreateSharedContext(); - bool CanShareContexts { get; } - bool CanCreateContexts { get; } - IGlContext CreateContext(); - /*IGlContext TryCreateContext(GlVersion version); - */ - } -} diff --git a/src/Avalonia.OpenGL/Imaging/OpenGlBitmap.cs b/src/Avalonia.OpenGL/Imaging/OpenGlBitmap.cs index 7af44cd624..53013ae5a3 100644 --- a/src/Avalonia.OpenGL/Imaging/OpenGlBitmap.cs +++ b/src/Avalonia.OpenGL/Imaging/OpenGlBitmap.cs @@ -10,19 +10,15 @@ namespace Avalonia.OpenGL.Imaging { private IOpenGlBitmapImpl _impl; - public OpenGlBitmap(PixelSize size, Vector dpi) - : base(CreateOrThrow(size, dpi)) + public OpenGlBitmap(IOpenGlTextureSharingRenderInterfaceContextFeature feature, + PixelSize size, Vector dpi) + : base(CreateOrThrow(feature, size, dpi)) { _impl = (IOpenGlBitmapImpl)PlatformImpl.Item; } - - static IOpenGlBitmapImpl CreateOrThrow(PixelSize size, Vector dpi) - { - if (!(AvaloniaLocator.Current.GetService() is IOpenGlAwarePlatformRenderInterface - glAware)) - throw new PlatformNotSupportedException("Rendering platform does not support OpenGL integration"); - return glAware.CreateOpenGlBitmap(size, dpi); - } + + static IOpenGlBitmapImpl CreateOrThrow(IOpenGlTextureSharingRenderInterfaceContextFeature feature, + PixelSize size, Vector dpi) => feature.CreateOpenGlBitmap(size, dpi); public IOpenGlBitmapAttachment CreateFramebufferAttachment(IGlContext context) => _impl.CreateFramebufferAttachment(context, SetIsDirty); diff --git a/src/Avalonia.OpenGL/OpenGlException.cs b/src/Avalonia.OpenGL/OpenGlException.cs index 196f507ad8..4c73d7e8ef 100644 --- a/src/Avalonia.OpenGL/OpenGlException.cs +++ b/src/Avalonia.OpenGL/OpenGlException.cs @@ -18,19 +18,22 @@ namespace Avalonia.OpenGL public static OpenGlException GetFormattedException(string funcName, EglInterface egl) { - return GetFormattedException(typeof(EglErrors), funcName, egl.GetError()); + return GetFormattedException(funcName, egl.GetError()); } public static OpenGlException GetFormattedException(string funcName, GlInterface gl) { - return GetFormattedException(typeof(GlErrors), funcName, gl.GetError()); + return GetFormattedException(funcName, gl.GetError()); } - private static OpenGlException GetFormattedException(Type consts, string funcName, int errorCode) + public static OpenGlException GetFormattedEglException(string funcName, int errorCode) => + GetFormattedException(funcName, errorCode); + + private static OpenGlException GetFormattedException(string funcName, int errorCode) { try { - string errorName = Enum.GetName(consts, errorCode); + string errorName = Enum.GetName(typeof(T), errorCode); return new OpenGlException( $"{funcName} failed with error {errorName} (0x{errorCode.ToString("X")})", errorCode); } diff --git a/src/Avalonia.OpenGL/Surfaces/IGlPlatformSurface.cs b/src/Avalonia.OpenGL/Surfaces/IGlPlatformSurface.cs index 875c215336..d80e72e4e7 100644 --- a/src/Avalonia.OpenGL/Surfaces/IGlPlatformSurface.cs +++ b/src/Avalonia.OpenGL/Surfaces/IGlPlatformSurface.cs @@ -2,6 +2,6 @@ namespace Avalonia.OpenGL.Surfaces { public interface IGlPlatformSurface { - IGlPlatformSurfaceRenderTarget CreateGlRenderTarget(); + IGlPlatformSurfaceRenderTarget CreateGlRenderTarget(IGlContext context); } } diff --git a/src/Avalonia.X11/Glx/GlxContext.cs b/src/Avalonia.X11/Glx/GlxContext.cs index e9cb88cb8f..def5228e94 100644 --- a/src/Avalonia.X11/Glx/GlxContext.cs +++ b/src/Avalonia.X11/Glx/GlxContext.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Reactive.Disposables; using System.Threading; using Avalonia.OpenGL; @@ -70,6 +71,8 @@ namespace Avalonia.X11.Glx } public IDisposable MakeCurrent() => MakeCurrent(_defaultXid); + public bool IsLost => false; + public IDisposable EnsureCurrent() { if(IsCurrent) @@ -86,6 +89,11 @@ namespace Avalonia.X11.Glx || _sharedWith != null && _sharedWith == c._sharedWith; } + public bool CanCreateSharedContext => true; + + public IGlContext CreateSharedContext(IEnumerable preferredVersions = null) => + Display.CreateContext(_sharedWith ?? this); + public IDisposable MakeCurrent(IntPtr xid) { Monitor.Enter(_lock); @@ -114,5 +122,7 @@ namespace Avalonia.X11.Glx if (_ownsPBuffer) Glx.DestroyPbuffer(_x11.Display, _defaultXid); } + + public object TryGetFeature(Type featureType) => null; } } diff --git a/src/Avalonia.X11/Glx/GlxDisplay.cs b/src/Avalonia.X11/Glx/GlxDisplay.cs index 1e70608168..9b8a8f0b5b 100644 --- a/src/Avalonia.X11/Glx/GlxDisplay.cs +++ b/src/Avalonia.X11/Glx/GlxDisplay.cs @@ -111,8 +111,8 @@ namespace Avalonia.X11.Glx return Glx.CreatePbuffer(_x11.Display, _fbconfig, new[] { GLX_PBUFFER_WIDTH, 1, GLX_PBUFFER_HEIGHT, 1, 0 }); } - - public GlxContext CreateContext() => CreateContext(); + public GlxContext CreateContext() => CreateContext(CreatePBuffer(), null, DeferredContext.SampleCount, + DeferredContext.StencilSize, true); public GlxContext CreateContext(IGlContext share) => CreateContext(CreatePBuffer(), share, share.SampleCount, share.StencilSize, true); diff --git a/src/Avalonia.X11/Glx/GlxGlPlatformSurface.cs b/src/Avalonia.X11/Glx/GlxGlPlatformSurface.cs index cb4ab4aca0..ebb1e18723 100644 --- a/src/Avalonia.X11/Glx/GlxGlPlatformSurface.cs +++ b/src/Avalonia.X11/Glx/GlxGlPlatformSurface.cs @@ -9,20 +9,16 @@ namespace Avalonia.X11.Glx class GlxGlPlatformSurface: IGlPlatformSurface { - private readonly GlxDisplay _display; - private readonly GlxContext _context; private readonly EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo _info; - public GlxGlPlatformSurface(GlxDisplay display, GlxContext context, EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo info) + public GlxGlPlatformSurface(EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo info) { - _display = display; - _context = context; _info = info; } - public IGlPlatformSurfaceRenderTarget CreateGlRenderTarget() + public IGlPlatformSurfaceRenderTarget CreateGlRenderTarget(IGlContext context) { - return new RenderTarget(_context, _info); + return new RenderTarget((GlxContext)context, _info); } class RenderTarget : IGlPlatformSurfaceRenderTarget diff --git a/src/Avalonia.X11/Glx/GlxPlatformFeature.cs b/src/Avalonia.X11/Glx/GlxPlatformFeature.cs index 0968adc799..bc50fa61aa 100644 --- a/src/Avalonia.X11/Glx/GlxPlatformFeature.cs +++ b/src/Avalonia.X11/Glx/GlxPlatformFeature.cs @@ -6,38 +6,36 @@ using Avalonia.Platform; namespace Avalonia.X11.Glx { - class GlxPlatformOpenGlInterface : IPlatformOpenGlInterface + class GlxPlatformGraphics : IPlatformGraphics { public GlxDisplay Display { get; private set; } public bool CanCreateContexts => true; public bool CanShareContexts => true; - public IGlContext CreateContext() => Display.CreateContext(); - public IGlContext CreateSharedContext() => Display.CreateContext(PrimaryContext); - public GlxContext DeferredContext { get; private set; } - public IGlContext PrimaryContext => DeferredContext; - IPlatformGpuContext IPlatformGpu.PrimaryContext => PrimaryContext; + public bool UsesSharedContext => false; + IPlatformGraphicsContext IPlatformGraphics.CreateContext() => Display.CreateContext(); + + public IPlatformGraphicsContext GetSharedContext() => throw new NotSupportedException(); public static bool TryInitialize(X11Info x11, IList glProfiles) { var feature = TryCreate(x11, glProfiles); if (feature != null) { - AvaloniaLocator.CurrentMutable.Bind().ToConstant(feature); + AvaloniaLocator.CurrentMutable.Bind().ToConstant(feature); return true; } return false; } - public static GlxPlatformOpenGlInterface TryCreate(X11Info x11, IList glProfiles) + public static GlxPlatformGraphics TryCreate(X11Info x11, IList glProfiles) { try { var disp = new GlxDisplay(x11, glProfiles); - return new GlxPlatformOpenGlInterface + return new GlxPlatformGraphics { - Display = disp, - DeferredContext = disp.DeferredContext + Display = disp }; } catch(Exception e) diff --git a/src/Avalonia.X11/X11CursorFactory.cs b/src/Avalonia.X11/X11CursorFactory.cs index 6041b53a62..16de10e163 100644 --- a/src/Avalonia.X11/X11CursorFactory.cs +++ b/src/Avalonia.X11/X11CursorFactory.cs @@ -113,7 +113,8 @@ namespace Avalonia.X11 image->yhot = hotSpot.Y; image->pixels = (IntPtr)(image + 1); - using (var renderTarget = platformRenderInterface.CreateRenderTarget(new[] { this })) + using (var cpuContext = platformRenderInterface.CreateBackendContext(null)) + using (var renderTarget = cpuContext.CreateRenderTarget(new[] { this })) using (var ctx = renderTarget.CreateDrawingContext(null)) { var r = new Rect(_pixelSize.ToSize(1)); diff --git a/src/Avalonia.X11/X11IconLoader.cs b/src/Avalonia.X11/X11IconLoader.cs index 4ae1c1599f..bf59d72c0f 100644 --- a/src/Avalonia.X11/X11IconLoader.cs +++ b/src/Avalonia.X11/X11IconLoader.cs @@ -41,7 +41,8 @@ namespace Avalonia.X11 _width = Math.Min(bitmap.PixelSize.Width, 128); _height = Math.Min(bitmap.PixelSize.Height, 128); _bdata = new uint[_width * _height]; - using(var rt = AvaloniaLocator.Current.GetService().CreateRenderTarget(new[]{this})) + using(var cpuContext = AvaloniaLocator.Current.GetRequiredService().CreateBackendContext(null)) + using(var rt = cpuContext.CreateRenderTarget(new[]{this})) using (var ctx = rt.CreateDrawingContext(null)) ctx.DrawBitmap(bitmap.PlatformImpl, 1, new Rect(bitmap.Size), new Rect(0, 0, _width, _height)); diff --git a/src/Avalonia.X11/X11ImmediateRendererProxy.cs b/src/Avalonia.X11/X11ImmediateRendererProxy.cs index 293e5110f7..ef061cbe5c 100644 --- a/src/Avalonia.X11/X11ImmediateRendererProxy.cs +++ b/src/Avalonia.X11/X11ImmediateRendererProxy.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.Threading.Tasks; +using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Threading; using Avalonia.VisualTree; @@ -14,11 +16,11 @@ namespace Avalonia.X11 private bool _running; private object _lock = new object(); - public X11ImmediateRendererProxy(Visual root, IRenderLoop loop) + public X11ImmediateRendererProxy(Visual root, IRenderLoop loop, Func renderTargetFactory, + PlatformRenderInterfaceContextManager renderContext) { _loop = loop; - _renderer = new ImmediateRenderer(root); - + _renderer = new ImmediateRenderer(root, renderTargetFactory, renderContext); } public void Dispose() @@ -92,6 +94,9 @@ namespace Avalonia.X11 _renderer.Stop(); } + public ValueTask TryGetRenderInterfaceFeature(Type featureType) => + _renderer.TryGetRenderInterfaceFeature(featureType); + public bool NeedsUpdate => false; public void Update(TimeSpan time) { diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index cbb782edd0..ca88b5188e 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -30,6 +30,7 @@ namespace Avalonia.X11 public X11Info Info { get; private set; } public IX11Screens X11Screens { get; private set; } public Compositor Compositor { get; private set; } + public PlatformRenderInterfaceContextManager RenderInterface { get; private set; } public IScreenImpl Screens { get; private set; } public X11PlatformOptions Options { get; private set; } public IntPtr OrphanedWindow { get; private set; } @@ -96,17 +97,19 @@ namespace Avalonia.X11 if (options.UseGpu) { if (options.UseEGL) - EglPlatformOpenGlInterface.TryInitialize(); + EglPlatformGraphics.TryInitialize(); else - GlxPlatformOpenGlInterface.TryInitialize(Info, Options.GlProfiles); + GlxPlatformGraphics.TryInitialize(Info, Options.GlProfiles); } - var gl = AvaloniaLocator.Current.GetService(); + var gl = AvaloniaLocator.Current.GetService(); if (gl != null) - AvaloniaLocator.CurrentMutable.Bind().ToConstant(gl); + AvaloniaLocator.CurrentMutable.Bind().ToConstant(gl); if (options.UseCompositor) Compositor = new Compositor(AvaloniaLocator.Current.GetService()!, gl); + else + RenderInterface = new(gl); } diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 2f84e15b32..160401c5fc 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -74,7 +74,7 @@ namespace Avalonia.X11 _touch = new TouchDevice(); _keyboard = platform.KeyboardDevice; - var glfeature = AvaloniaLocator.Current.GetService(); + var glfeature = AvaloniaLocator.Current.GetService(); XSetWindowAttributes attr = new XSetWindowAttributes(); var valueMask = default(SetWindowValuemask); @@ -96,13 +96,13 @@ namespace Avalonia.X11 // OpenGL seems to be do weird things to it's current window which breaks resize sometimes _useRenderWindow = glfeature != null; - var glx = glfeature as GlxPlatformOpenGlInterface; + var glx = glfeature as GlxPlatformGraphics; if (glx != null) visualInfo = *glx.Display.VisualInfo; else if (glfeature == null) visualInfo = _x11.TransparentVisualInfo; - var egl = glfeature as EglPlatformOpenGlInterface; + var egl = glfeature as EglPlatformGraphics; var visual = IntPtr.Zero; var depth = 24; @@ -176,11 +176,9 @@ namespace Avalonia.X11 if (egl != null) surfaces.Insert(0, - new EglGlPlatformSurface(egl, - new SurfaceInfo(this, _x11.DeferredDisplay, _handle, _renderHandle))); + new EglGlPlatformSurface(new SurfaceInfo(this, _x11.DeferredDisplay, _handle, _renderHandle))); if (glx != null) - surfaces.Insert(0, new GlxGlPlatformSurface(glx.Display, glx.DeferredContext, - new SurfaceInfo(this, _x11.Display, _handle, _renderHandle))); + surfaces.Insert(0, new GlxGlPlatformSurface(new SurfaceInfo(this, _x11.Display, _handle, _renderHandle))); surfaces.Add(Handle); @@ -391,12 +389,14 @@ namespace Avalonia.X11 return _platform.Options.UseDeferredRendering ? _platform.Options.UseCompositor - ? new CompositingRenderer(root, this._platform.Compositor) - : new DeferredRenderer(root, loop) + ? new CompositingRenderer(root, this._platform.Compositor, () => Surfaces) + : new DeferredRenderer(root, loop, () => _platform.RenderInterface.CreateRenderTarget(Surfaces), _platform.RenderInterface) { RenderOnlyOnRenderThread = true } - : (IRenderer)new X11ImmediateRendererProxy((Visual)root, loop); + : new X11ImmediateRendererProxy((Visual)root, loop, + () => _platform.RenderInterface.CreateRenderTarget(Surfaces), + _platform.RenderInterface); } void OnEvent(ref XEvent ev) diff --git a/src/Browser/Avalonia.Browser/AvaloniaView.cs b/src/Browser/Avalonia.Browser/AvaloniaView.cs index a407e1e4d8..abda618b0d 100644 --- a/src/Browser/Avalonia.Browser/AvaloniaView.cs +++ b/src/Browser/Avalonia.Browser/AvaloniaView.cs @@ -11,6 +11,7 @@ using Avalonia.Controls.Platform; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Input.TextInput; +using Avalonia.Platform; using Avalonia.Rendering.Composition; using Avalonia.Threading; using SkiaSharp; @@ -106,7 +107,7 @@ namespace Avalonia.Browser _dpi = DomHelper.ObserveDpi(OnDpiChanged); - _useGL = skiaOptions?.CustomGpuFactory != null; + _useGL = AvaloniaLocator.Current.GetRequiredService() != null; if (_useGL) { diff --git a/src/Browser/Avalonia.Browser/BrowserSingleViewLifetime.cs b/src/Browser/Avalonia.Browser/BrowserSingleViewLifetime.cs index ee4f6eca9b..b6c766b2a5 100644 --- a/src/Browser/Avalonia.Browser/BrowserSingleViewLifetime.cs +++ b/src/Browser/Avalonia.Browser/BrowserSingleViewLifetime.cs @@ -48,7 +48,6 @@ public static class WebAppBuilder { return builder .UseWindowingSubsystem(BrowserWindowingPlatform.Register) - .UseSkia() - .With(new SkiaOptions { CustomGpuFactory = () => new BrowserSkiaGpu() }); + .UseSkia(); } } diff --git a/src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs b/src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs index 69e2d27181..85725e4cf3 100644 --- a/src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs +++ b/src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs @@ -166,7 +166,8 @@ namespace Avalonia.Browser public IRenderer CreateRenderer(IRenderRoot root) { var loop = AvaloniaLocator.Current.GetRequiredService(); - return new CompositingRenderer(root, new Compositor(loop, null)); + return new CompositingRenderer(root, + new Compositor(loop, AvaloniaLocator.Current.GetRequiredService()), () => Surfaces); } public void Invalidate(Rect rect) diff --git a/src/Browser/Avalonia.Browser/Skia/BrowserSkiaGpu.cs b/src/Browser/Avalonia.Browser/Skia/BrowserSkiaGpu.cs index a96ead93cb..e56efdb4a8 100644 --- a/src/Browser/Avalonia.Browser/Skia/BrowserSkiaGpu.cs +++ b/src/Browser/Avalonia.Browser/Skia/BrowserSkiaGpu.cs @@ -1,4 +1,7 @@ +using System; using System.Collections.Generic; +using System.Reactive.Disposables; +using Avalonia.Platform; using Avalonia.Skia; namespace Avalonia.Browser.Skia @@ -22,5 +25,28 @@ namespace Avalonia.Browser.Skia { return null; } + + public void Dispose() + { + + } + + public object? TryGetFeature(Type t) => null; + + public bool IsLost => false; + + public IDisposable EnsureCurrent() + { + return Disposable.Empty; + } + } + + class BrowserSkiaGraphics : IPlatformGraphics + { + private BrowserSkiaGpu _skia = new(); + public bool UsesSharedContext => true; + public IPlatformGraphicsContext CreateContext() => throw new NotSupportedException(); + + public IPlatformGraphicsContext GetSharedContext() => _skia; } } diff --git a/src/Browser/Avalonia.Browser/WindowingPlatform.cs b/src/Browser/Avalonia.Browser/WindowingPlatform.cs index 6535e9534c..6493374a50 100644 --- a/src/Browser/Avalonia.Browser/WindowingPlatform.cs +++ b/src/Browser/Avalonia.Browser/WindowingPlatform.cs @@ -1,5 +1,6 @@ using System; using System.Threading; +using Avalonia.Browser.Skia; using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.Platform; @@ -43,6 +44,7 @@ namespace Avalonia.Browser .Bind().ToConstant(new RenderLoop()) .Bind().ToConstant(ManualTriggerRenderTimer.Instance) .Bind().ToConstant(instance) + .Bind().ToConstant(new BrowserSkiaGraphics()) .Bind().ToSingleton() .Bind().ToSingleton(); } diff --git a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs index b64423ec10..ac54365f51 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs @@ -1,4 +1,4 @@ -using System; + using System; using System.Collections.Generic; using Avalonia.Controls; using Avalonia.Input; @@ -33,7 +33,7 @@ namespace Avalonia.LinuxFramebuffer { var factory = AvaloniaLocator.Current.GetService(); var renderLoop = AvaloniaLocator.Current.GetService(); - return factory?.Create(root, renderLoop) ?? new CompositingRenderer(root, LinuxFramebufferPlatform.Compositor); + return factory?.Create(root, renderLoop) ?? new CompositingRenderer(root, LinuxFramebufferPlatform.Compositor, () => Surfaces); } public void Dispose() diff --git a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs index d881a97af2..38498951f8 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs @@ -41,7 +41,7 @@ namespace Avalonia.LinuxFramebuffer { Threading = new InternalPlatformThreadingInterface(); if (_fb is IGlOutputBackend gl) - AvaloniaLocator.CurrentMutable.Bind().ToConstant(gl.PlatformOpenGlInterface); + AvaloniaLocator.CurrentMutable.Bind().ToConstant(gl.PlatformGraphics); var opts = AvaloniaLocator.Current.GetService() ?? new LinuxFramebufferPlatformOptions(); @@ -56,7 +56,7 @@ namespace Avalonia.LinuxFramebuffer Compositor = new Compositor( AvaloniaLocator.Current.GetRequiredService(), - AvaloniaLocator.Current.GetService()); + AvaloniaLocator.Current.GetService()); } diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs index a4d7d8b1eb..22dd407791 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs @@ -6,6 +6,7 @@ using System.Runtime.InteropServices; using Avalonia.OpenGL; using Avalonia.OpenGL.Egl; using Avalonia.OpenGL.Surfaces; +using Avalonia.Platform; using Avalonia.Platform.Interop; using JetBrains.Annotations; using static Avalonia.LinuxFramebuffer.NativeUnsafeMethods; @@ -25,10 +26,22 @@ namespace Avalonia.LinuxFramebuffer.Output get => _outputOptions.Scaling; set => _outputOptions.Scaling = value; } - public IGlContext PrimaryContext => _deferredContext; - private EglPlatformOpenGlInterface _platformGl; - public IPlatformOpenGlInterface PlatformOpenGlInterface => _platformGl; + class SharedContextGraphics : IPlatformGraphics + { + private readonly IPlatformGraphicsContext _context; + + public SharedContextGraphics(IPlatformGraphicsContext context) + { + _context = context; + } + public bool UsesSharedContext => true; + public IPlatformGraphicsContext CreateContext() => throw new NotSupportedException(); + + public IPlatformGraphicsContext GetSharedContext() => _context; + } + + public IPlatformGraphics PlatformGraphics { get; private set; } public DrmOutput(DrmCard card, DrmResources resources, DrmConnector connector, DrmModeInfo modeInfo, DrmOutputOptions? options = null) @@ -161,11 +174,22 @@ namespace Avalonia.LinuxFramebuffer.Output if(_gbmTargetSurface == IntPtr.Zero) throw new InvalidOperationException("Unable to create GBM surface"); - _eglDisplay = new EglDisplay(new EglInterface(eglGetProcAddress), false, 0x31D7, device, null); - _platformGl = new EglPlatformOpenGlInterface(_eglDisplay); - _eglSurface = _platformGl.CreateWindowSurface(_gbmTargetSurface); + _eglDisplay = new EglDisplay( + new EglDisplayCreationOptions + { + Egl = new EglInterface(eglGetProcAddress), + PlatformType = 0x31D7, + PlatformDisplay = device, + SupportsMultipleContexts = true, + SupportsContextSharing = true + }); + + var surface = _eglDisplay.EglInterface.CreateWindowSurface(_eglDisplay.Handle, _eglDisplay.Config, _gbmTargetSurface, new[] { EglConsts.EGL_NONE, EglConsts.EGL_NONE }); + + _eglSurface = new EglSurface(_eglDisplay, surface); - _deferredContext = _platformGl.PrimaryEglContext; + _deferredContext = _eglDisplay.CreateContext(null); + PlatformGraphics = new SharedContextGraphics(_deferredContext); var initialBufferSwappingColorR = _outputOptions.InitialBufferSwappingColor.R / 255.0f; var initialBufferSwappingColorG = _outputOptions.InitialBufferSwappingColor.G / 255.0f; @@ -206,11 +230,17 @@ namespace Avalonia.LinuxFramebuffer.Output } - public IGlPlatformSurfaceRenderTarget CreateGlRenderTarget() + public IGlPlatformSurfaceRenderTarget CreateGlRenderTarget() => new RenderTarget(this); + + + public IGlPlatformSurfaceRenderTarget CreateGlRenderTarget(IGlContext context) { - return new RenderTarget(this); + if (context != _deferredContext) + throw new InvalidOperationException( + "This platform backend can only create render targets for its primary context"); + return CreateGlRenderTarget(); } - + class RenderTarget : IGlPlatformSurfaceRenderTarget { private readonly DrmOutput _parent; diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Output/IGlOutputBackend.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/IGlOutputBackend.cs index 7bc73d590c..e415b1782e 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Output/IGlOutputBackend.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/IGlOutputBackend.cs @@ -1,9 +1,10 @@ using Avalonia.OpenGL; +using Avalonia.Platform; namespace Avalonia.LinuxFramebuffer.Output { public interface IGlOutputBackend : IOutputBackend { - public IPlatformOpenGlInterface PlatformOpenGlInterface { get; } + public IPlatformGraphics PlatformGraphics { get; } } } diff --git a/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs b/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs index 9dbfd0f8d9..1ae47c8b7a 100644 --- a/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs @@ -61,6 +61,8 @@ namespace Avalonia.Skia return new DrawingContextImpl(createInfo, _preFramebufferCopyHandler, canvas, framebuffer); } + public bool IsCorrupted => false; + /// /// Check if two images info are compatible. /// diff --git a/src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs b/src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs index 4a7d4d4ac8..b064445a0b 100644 --- a/src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs +++ b/src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; +using Avalonia.OpenGL; using Avalonia.OpenGL.Imaging; +using Avalonia.Platform; using SkiaSharp; namespace Avalonia.Skia @@ -8,7 +10,7 @@ namespace Avalonia.Skia /// /// Custom Skia gpu instance. /// - public interface ISkiaGpu + public interface ISkiaGpu : IPlatformGraphicsContext { /// /// Attempts to create custom render target from given surfaces. diff --git a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs index 476ecc4a39..5bf1272c2f 100644 --- a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs @@ -14,10 +14,11 @@ namespace Avalonia.Skia private readonly GRContext _grContext; private IGlPlatformSurfaceRenderTarget _surface; - public GlRenderTarget(GRContext grContext, IGlPlatformSurface glSurface) + public GlRenderTarget(GRContext grContext, IGlContext glContext, IGlPlatformSurface glSurface) { _grContext = grContext; - _surface = glSurface.CreateGlRenderTarget(); + using (glContext.EnsureCurrent()) + _surface = glSurface.CreateGlRenderTarget(glContext); } public void Dispose() => _surface.Dispose(); diff --git a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs index 08e0fbd808..002129a1eb 100644 --- a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs +++ b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs @@ -5,21 +5,21 @@ using Avalonia.Logging; using Avalonia.OpenGL; using Avalonia.OpenGL.Imaging; using Avalonia.OpenGL.Surfaces; +using Avalonia.Platform; using SkiaSharp; namespace Avalonia.Skia { - class GlSkiaGpu : IOpenGlAwareSkiaGpu + class GlSkiaGpu : IOpenGlAwareSkiaGpu, IOpenGlTextureSharingRenderInterfaceContextFeature { private GRContext _grContext; private IGlContext _glContext; private bool? _canCreateSurfaces; - public GlSkiaGpu(IPlatformOpenGlInterface openGl, long? maxResourceBytes) + public GlSkiaGpu(IGlContext context, long? maxResourceBytes) { - var context = openGl.PrimaryContext; _glContext = context; - using (context.MakeCurrent()) + using (_glContext.EnsureCurrent()) { using (var iface = context.Version.Type == GlProfileType.OpenGL ? GRGlInterface.CreateOpenGl(proc => context.GlInterface.GetProcAddress(proc)) : @@ -34,13 +34,34 @@ namespace Avalonia.Skia } } + class SurfaceWrapper : IGlPlatformSurface + { + private readonly object _surface; + + public SurfaceWrapper( object surface) + { + _surface = surface; + } + + public IGlPlatformSurfaceRenderTarget CreateGlRenderTarget(IGlContext context) + { + var feature = context.TryGetFeature()!; + return feature.CreateRenderTarget(context, _surface); + } + } + public ISkiaGpuRenderTarget TryCreateRenderTarget(IEnumerable surfaces) { + var customRenderTargetFactory = _glContext.TryGetFeature(); foreach (var surface in surfaces) { + if (customRenderTargetFactory?.CanRenderToSurface(_glContext, surface) == true) + { + return new GlRenderTarget(_grContext, _glContext, new SurfaceWrapper(surface)); + } if (surface is IGlPlatformSurface glSurface) { - return new GlRenderTarget(_grContext, glSurface); + return new GlRenderTarget(_grContext, _glContext, glSurface); } } @@ -75,6 +96,30 @@ namespace Avalonia.Skia } } + public bool CanCreateSharedContext => _glContext.CanCreateSharedContext; + + public IGlContext CreateSharedContext(IEnumerable preferredVersions = null) => + _glContext.CreateSharedContext(preferredVersions); + public IOpenGlBitmapImpl CreateOpenGlBitmap(PixelSize size, Vector dpi) => new GlOpenGlBitmapImpl(_glContext, size, dpi); + + public void Dispose() + { + if (_glContext.IsLost) + _grContext.AbandonContext(); + else + _grContext.AbandonContext(true); + _grContext.Dispose(); + } + + public bool IsLost => _glContext.IsLost; + public IDisposable EnsureCurrent() => _glContext.EnsureCurrent(); + + public object TryGetFeature(Type featureType) + { + if (featureType == typeof(IOpenGlTextureSharingRenderInterfaceContextFeature)) + return this; + return null; + } } } diff --git a/src/Skia/Avalonia.Skia/Gpu/OpenGl/OpenGlBitmapImpl.cs b/src/Skia/Avalonia.Skia/Gpu/OpenGl/OpenGlBitmapImpl.cs index 5e02500bf0..d8bff7cfc8 100644 --- a/src/Skia/Avalonia.Skia/Gpu/OpenGl/OpenGlBitmapImpl.cs +++ b/src/Skia/Avalonia.Skia/Gpu/OpenGl/OpenGlBitmapImpl.cs @@ -109,7 +109,9 @@ namespace Avalonia.Skia using (_context.EnsureCurrent()) { var glVersion = _context.Version; - InternalFormat = glVersion.Type == GlProfileType.OpenGLES ? GL_RGBA : GL_RGBA8; + InternalFormat = glVersion.Type == GlProfileType.OpenGLES && glVersion.Major == 2 + ? GL_RGBA + : GL_RGBA8; _context.GlInterface.GetIntegerv(GL_FRAMEBUFFER_BINDING, out _fbo); if (_fbo == 0) @@ -145,7 +147,7 @@ namespace Avalonia.Skia public void Present() { - using (_context.MakeCurrent()) + using (_context.EnsureCurrent()) { if (_disposed) throw new ObjectDisposedException(nameof(SharedOpenGlBitmapAttachment)); diff --git a/src/Skia/Avalonia.Skia/Gpu/SkiaGpuRenderTarget.cs b/src/Skia/Avalonia.Skia/Gpu/SkiaGpuRenderTarget.cs index 6626546c0c..6b4a7a3409 100644 --- a/src/Skia/Avalonia.Skia/Gpu/SkiaGpuRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/Gpu/SkiaGpuRenderTarget.cs @@ -6,7 +6,7 @@ namespace Avalonia.Skia /// /// Adapts to be used within our rendering pipeline. /// - internal class SkiaGpuRenderTarget : IRenderTargetWithCorruptionInfo + internal class SkiaGpuRenderTarget : IRenderTarget { private readonly ISkiaGpu _skiaGpu; private readonly ISkiaGpuRenderTarget _renderTarget; diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs index f34e25299c..b202b60cdf 100644 --- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs +++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs @@ -18,26 +18,28 @@ namespace Avalonia.Skia /// /// Skia platform render interface. /// - internal class PlatformRenderInterface : IPlatformRenderInterface, IOpenGlAwarePlatformRenderInterface + internal class PlatformRenderInterface : IPlatformRenderInterface { - private readonly ISkiaGpu _skiaGpu; + private readonly long? _maxResourceBytes; - public PlatformRenderInterface(ISkiaGpu skiaGpu, long? maxResourceBytes = null) + public PlatformRenderInterface(long? maxResourceBytes = null) { + _maxResourceBytes = maxResourceBytes; DefaultPixelFormat = SKImageInfo.PlatformColorType.ToPixelFormat(); + } - if (skiaGpu != null) - { - _skiaGpu = skiaGpu; - return; - } - var gl = AvaloniaLocator.Current.GetService(); - if (gl != null) - _skiaGpu = new GlSkiaGpu(gl, maxResourceBytes); + public IPlatformRenderInterfaceContext CreateBackendContext(IPlatformGraphicsContext graphicsContext) + { + if (graphicsContext == null) + return new SkiaContext(null); + if (graphicsContext is ISkiaGpu skiaGpu) + return new SkiaContext(skiaGpu); + if (graphicsContext is IGlContext gl) + return new SkiaContext(new GlSkiaGpu(gl, _maxResourceBytes)); + throw new ArgumentException("Graphics context of type is not supported"); } - public bool SupportsIndividualRoundRects => true; public AlphaFormat DefaultAlphaFormat => AlphaFormat.Premul; @@ -205,43 +207,12 @@ namespace Avalonia.Skia return new SurfaceRenderTarget(createInfo); } - /// - public IRenderTarget CreateRenderTarget(IEnumerable surfaces) - { - if (!(surfaces is IList)) - surfaces = surfaces.ToList(); - var gpuRenderTarget = _skiaGpu?.TryCreateRenderTarget(surfaces); - if (gpuRenderTarget != null) - { - return new SkiaGpuRenderTarget(_skiaGpu, gpuRenderTarget); - } - - foreach (var surface in surfaces) - { - if (surface is IFramebufferPlatformSurface framebufferSurface) - return new FramebufferRenderTarget(framebufferSurface); - } - - throw new NotSupportedException( - "Don't know how to create a Skia render target from any of provided surfaces"); - } - /// public IWriteableBitmapImpl CreateWriteableBitmap(PixelSize size, Vector dpi, PixelFormat format, AlphaFormat alphaFormat) { return new WriteableBitmapImpl(size, dpi, format, alphaFormat); } - public IOpenGlBitmapImpl CreateOpenGlBitmap(PixelSize size, Vector dpi) - { - if (_skiaGpu is IOpenGlAwareSkiaGpu glAware) - return glAware.CreateOpenGlBitmap(size, dpi); - if (_skiaGpu == null) - throw new PlatformNotSupportedException("GPU acceleration is not available"); - throw new PlatformNotSupportedException( - "Current GPU acceleration backend does not support OpenGL integration"); - } - public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphIndices, IReadOnlyList glyphAdvances, IReadOnlyList glyphOffsets) { diff --git a/src/Skia/Avalonia.Skia/SkiaBackendContext.cs b/src/Skia/Avalonia.Skia/SkiaBackendContext.cs new file mode 100644 index 0000000000..4949f4a50d --- /dev/null +++ b/src/Skia/Avalonia.Skia/SkiaBackendContext.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using Avalonia.Controls.Platform.Surfaces; +using Avalonia.Platform; + +namespace Avalonia.Skia; + +internal class SkiaContext : IPlatformRenderInterfaceContext +{ + private ISkiaGpu _gpu; + + public SkiaContext(ISkiaGpu gpu) + { + _gpu = gpu; + } + + public void Dispose() + { + _gpu?.Dispose(); + _gpu = null; + } + + /// + public IRenderTarget CreateRenderTarget(IEnumerable surfaces) + { + if (!(surfaces is IList)) + surfaces = surfaces.ToList(); + var gpuRenderTarget = _gpu?.TryCreateRenderTarget(surfaces); + if (gpuRenderTarget != null) + { + return new SkiaGpuRenderTarget(_gpu, gpuRenderTarget); + } + + foreach (var surface in surfaces) + { + if (surface is IFramebufferPlatformSurface framebufferSurface) + return new FramebufferRenderTarget(framebufferSurface); + } + + throw new NotSupportedException( + "Don't know how to create a Skia render target from any of provided surfaces"); + } + + public object TryGetFeature(Type featureType) => _gpu?.TryGetFeature(featureType); +} \ No newline at end of file diff --git a/src/Skia/Avalonia.Skia/SkiaOptions.cs b/src/Skia/Avalonia.Skia/SkiaOptions.cs index 493263677d..b3c3056a58 100644 --- a/src/Skia/Avalonia.Skia/SkiaOptions.cs +++ b/src/Skia/Avalonia.Skia/SkiaOptions.cs @@ -8,11 +8,6 @@ namespace Avalonia /// public class SkiaOptions { - /// - /// Custom gpu factory to use. Can be used to customize behavior of Skia renderer. - /// - public Func CustomGpuFactory { get; set; } - /// /// The maximum number of bytes for video memory to store textures and resources. /// diff --git a/src/Skia/Avalonia.Skia/SkiaPlatform.cs b/src/Skia/Avalonia.Skia/SkiaPlatform.cs index 9a5725e06f..27f2631db8 100644 --- a/src/Skia/Avalonia.Skia/SkiaPlatform.cs +++ b/src/Skia/Avalonia.Skia/SkiaPlatform.cs @@ -17,8 +17,7 @@ namespace Avalonia.Skia public static void Initialize(SkiaOptions options) { - var customGpu = options.CustomGpuFactory?.Invoke(); - var renderInterface = new PlatformRenderInterface(customGpu, options.MaxGpuResourceSizeBytes); + var renderInterface = new PlatformRenderInterface(options.MaxGpuResourceSizeBytes); AvaloniaLocator.CurrentMutable .Bind().ToConstant(renderInterface) diff --git a/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs b/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs index 74e3ebc51d..a7998353d9 100644 --- a/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs @@ -107,6 +107,8 @@ namespace Avalonia.Skia return new DrawingContextImpl(createInfo, Disposable.Create(() => Version++)); } + public bool IsCorrupted => _gpu?.IsLost == true; + /// public Vector Dpi { get; } diff --git a/src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs b/src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs index d437f514bb..dcb267b2a3 100644 --- a/src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs +++ b/src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs @@ -217,8 +217,7 @@ namespace Avalonia.Skia public int RowBytes => _bitmap.RowBytes; /// - public Vector Dpi { get; } = SkiaPlatform.DefaultDpi; - + public Vector Dpi => _parent.Dpi; /// public PixelFormat Format => _bitmap.ColorType.ToPixelFormat(); } diff --git a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs index 4d307c9762..a5f77230b7 100644 --- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs +++ b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs @@ -113,7 +113,7 @@ namespace Avalonia.Direct2D1 SharpDX.Configuration.EnableReleaseOnFinalizer = true; } - public IRenderTarget CreateRenderTarget(IEnumerable surfaces) + private IRenderTarget CreateRenderTarget(IEnumerable surfaces) { foreach (var s in surfaces) { @@ -223,6 +223,26 @@ namespace Avalonia.Direct2D1 return new GlyphRunImpl(run); } + class D2DApi : IPlatformRenderInterfaceContext + { + private readonly Direct2D1Platform _platform; + + public D2DApi(Direct2D1Platform platform) + { + _platform = platform; + } + public object TryGetFeature(Type featureType) => null; + + public void Dispose() + { + } + + public IRenderTarget CreateRenderTarget(IEnumerable surfaces) => _platform.CreateRenderTarget(surfaces); + } + + public IPlatformRenderInterfaceContext CreateBackendContext(IPlatformGraphicsContext graphicsContext) => + new D2DApi(this); + public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun) { if (glyphRun.GlyphTypeface is not GlyphTypefaceImpl glyphTypeface) diff --git a/src/Windows/Avalonia.Direct2D1/ExternalRenderTarget.cs b/src/Windows/Avalonia.Direct2D1/ExternalRenderTarget.cs index 2c0adcac32..02932c52da 100644 --- a/src/Windows/Avalonia.Direct2D1/ExternalRenderTarget.cs +++ b/src/Windows/Avalonia.Direct2D1/ExternalRenderTarget.cs @@ -38,6 +38,8 @@ namespace Avalonia.Direct2D1 }); } + public bool IsCorrupted => false; + public IDrawingContextLayerImpl CreateLayer(Size size) { var renderTarget = _externalRenderTargetProvider.GetOrCreateRenderTarget(); diff --git a/src/Windows/Avalonia.Direct2D1/FramebufferShimRenderTarget.cs b/src/Windows/Avalonia.Direct2D1/FramebufferShimRenderTarget.cs index f7e7ed3dc2..984a24fb30 100644 --- a/src/Windows/Avalonia.Direct2D1/FramebufferShimRenderTarget.cs +++ b/src/Windows/Avalonia.Direct2D1/FramebufferShimRenderTarget.cs @@ -35,6 +35,8 @@ namespace Avalonia.Direct2D1 .CreateDrawingContext(visualBrushRenderer); } + public bool IsCorrupted => false; + class FramebufferShim : WicRenderTargetBitmapImpl { private readonly ILockedFramebuffer _target; diff --git a/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs index c5f8e837ce..84f11acdd7 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs @@ -37,6 +37,8 @@ namespace Avalonia.Direct2D1.Media.Imaging return new DrawingContextImpl(visualBrushRenderer, this, _renderTarget, null, () => Version++); } + public bool IsCorrupted => false; + public void Blit(IDrawingContextImpl context) => throw new NotSupportedException(); public bool CanBlit => false; diff --git a/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicRenderTargetBitmapImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicRenderTargetBitmapImpl.cs index 8c9d01f37d..9f0d48dbc7 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicRenderTargetBitmapImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicRenderTargetBitmapImpl.cs @@ -39,6 +39,8 @@ namespace Avalonia.Direct2D1.Media public virtual IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) => CreateDrawingContext(visualBrushRenderer, null); + public bool IsCorrupted => false; + public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer, Action finishedCallback) { return new DrawingContextImpl(visualBrushRenderer, null, _renderTarget, finishedCallback: () => diff --git a/src/Windows/Avalonia.Direct2D1/RenderTarget.cs b/src/Windows/Avalonia.Direct2D1/RenderTarget.cs index d04c616bd9..1a749c1a7f 100644 --- a/src/Windows/Avalonia.Direct2D1/RenderTarget.cs +++ b/src/Windows/Avalonia.Direct2D1/RenderTarget.cs @@ -30,6 +30,8 @@ namespace Avalonia.Direct2D1 return new DrawingContextImpl(visualBrushRenderer, this, _renderTarget); } + public bool IsCorrupted => false; + public IDrawingContextLayerImpl CreateLayer(Size size) { return D2DRenderTargetBitmapImpl.CreateCompatible(_renderTarget, size); diff --git a/src/Windows/Avalonia.Direct2D1/SwapChainRenderTarget.cs b/src/Windows/Avalonia.Direct2D1/SwapChainRenderTarget.cs index f319cfae03..4935e3db48 100644 --- a/src/Windows/Avalonia.Direct2D1/SwapChainRenderTarget.cs +++ b/src/Windows/Avalonia.Direct2D1/SwapChainRenderTarget.cs @@ -35,6 +35,8 @@ namespace Avalonia.Direct2D1 return new DrawingContextImpl(visualBrushRenderer, this, _deviceContext, _swapChain); } + public bool IsCorrupted => false; + public IDrawingContextLayerImpl CreateLayer(Size size) { if (_deviceContext == null) diff --git a/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs b/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs index 1dcf45aa4e..16a20c855f 100644 --- a/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs +++ b/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs @@ -87,9 +87,11 @@ namespace Avalonia.Win32.Interop.Wpf _ttl.ScalingChanged?.Invoke(_ttl.RenderScaling); } + public IRenderer CreateRenderer(IRenderRoot root) { - return new ImmediateRenderer((Visual)root); + var mgr = new PlatformRenderInterfaceContextManager(null); + return new ImmediateRenderer((Visual)root, () => mgr.CreateRenderTarget(_surfaces), mgr); } public void Dispose() diff --git a/src/Avalonia.OpenGL/AngleOptions.cs b/src/Windows/Avalonia.Win32/AngleOptions.cs similarity index 90% rename from src/Avalonia.OpenGL/AngleOptions.cs rename to src/Windows/Avalonia.Win32/AngleOptions.cs index 0807eb7ab4..076ddd2a5d 100644 --- a/src/Avalonia.OpenGL/AngleOptions.cs +++ b/src/Windows/Avalonia.Win32/AngleOptions.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; +using Avalonia.OpenGL; -namespace Avalonia.OpenGL +namespace Avalonia.Win32 { public class AngleOptions { diff --git a/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj b/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj index e69e0ee219..08f12f5aea 100644 --- a/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj +++ b/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj @@ -16,8 +16,12 @@ + + + + diff --git a/src/Windows/Avalonia.Win32/DirectX/DirectXEnums.cs b/src/Windows/Avalonia.Win32/DirectX/DirectXEnums.cs index 3a67da530f..a749ed1b45 100644 --- a/src/Windows/Avalonia.Win32/DirectX/DirectXEnums.cs +++ b/src/Windows/Avalonia.Win32/DirectX/DirectXEnums.cs @@ -4,7 +4,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -namespace Avalonia.Win32.DxgiSwapchain +namespace Avalonia.Win32.DirectX { internal enum D3D_FEATURE_LEVEL { @@ -136,4 +136,53 @@ namespace Avalonia.Win32.DxgiSwapchain DXGI_ALPHA_MODE_FORCE_DWORD = (unchecked((int)0xffffffff)), } + + internal enum D3D_DRIVER_TYPE + { + D3D_DRIVER_TYPE_UNKNOWN = 0, + D3D_DRIVER_TYPE_HARDWARE, + D3D_DRIVER_TYPE_REFERENCE, + D3D_DRIVER_TYPE_NULL, + D3D_DRIVER_TYPE_SOFTWARE, + D3D_DRIVER_TYPE_WARP + } + + internal enum DXGI_ERROR : uint + { + DXGI_ERROR_ACCESS_DENIED = 0x887A002B, + DXGI_ERROR_ACCESS_LOST = 0x887A0026, + DXGI_ERROR_ALREADY_EXISTS = 0x887A0036, + DXGI_ERROR_CANNOT_PROTECT_CONTENT = 0x887A002A, + DXGI_ERROR_DEVICE_HUNG = 0x887A0006, + DXGI_ERROR_DEVICE_REMOVED = 0x887A0005, + DXGI_ERROR_DEVICE_RESET = 0x887A0007, + DXGI_ERROR_DRIVER_INTERNAL_ERROR = 0x887A0020, + DXGI_ERROR_FRAME_STATISTICS_DISJOINT = 0x887A000B, + DXGI_ERROR_GRAPHICS_VIDPN_SOURCE_IN_USE = 0x887A000C, + DXGI_ERROR_INVALID_CALL = 0x887A0001, + DXGI_ERROR_MORE_DATA = 0x887A0003, + DXGI_ERROR_NAME_ALREADY_EXISTS = 0x887A002C, + DXGI_ERROR_NONEXCLUSIVE = 0x887A0021, + DXGI_ERROR_NOT_CURRENTLY_AVAILABLE = 0x887A0022, + DXGI_ERROR_NOT_FOUND = 0x887A0002, + DXGI_ERROR_REMOTE_CLIENT_DISCONNECTED = 0x887A0023, + DXGI_ERROR_REMOTE_OUTOFMEMORY = 0x887A0024, + DXGI_ERROR_RESTRICT_TO_OUTPUT_STALE = 0x887A0029, + DXGI_ERROR_SDK_COMPONENT_MISSING = 0x887A002D, + DXGI_ERROR_SESSION_DISCONNECTED = 0x887A0028, + DXGI_ERROR_UNSUPPORTED = 0x887A0004, + DXGI_ERROR_WAIT_TIMEOUT = 0x887A0027, + DXGI_ERROR_WAS_STILL_DRAWING = 0x887A000A + } + + internal static class DxgiErrorExtensions + { + public static bool IsDeviceLostError(this DXGI_ERROR error) + { + return error is DXGI_ERROR.DXGI_ERROR_DEVICE_REMOVED + or DXGI_ERROR.DXGI_ERROR_DEVICE_HUNG + or DXGI_ERROR.DXGI_ERROR_DEVICE_RESET + or DXGI_ERROR.DXGI_ERROR_NOT_CURRENTLY_AVAILABLE; + } + } } diff --git a/src/Windows/Avalonia.Win32/DirectX/DirectXStructs.cs b/src/Windows/Avalonia.Win32/DirectX/DirectXStructs.cs index d606b00109..47451831a6 100644 --- a/src/Windows/Avalonia.Win32/DirectX/DirectXStructs.cs +++ b/src/Windows/Avalonia.Win32/DirectX/DirectXStructs.cs @@ -6,8 +6,10 @@ using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; using static Avalonia.Win32.Interop.UnmanagedMethods; +// ReSharper disable InconsistentNaming +#pragma warning disable CS0649 -namespace Avalonia.Win32.DxgiSwapchain +namespace Avalonia.Win32.DirectX { #nullable enable public unsafe struct HANDLE @@ -36,1092 +38,13 @@ namespace Avalonia.Win32.DxgiSwapchain public override string ToString() => ((IntPtr)Value).ToString(); } - internal unsafe partial struct MONITORINFOEXW + internal unsafe struct MONITORINFOEXW { internal MONITORINFO Base; internal fixed ushort szDevice[32]; } - - internal unsafe struct DXGI_GAMMA_CONTROL - { - public DXGI_RGB Scale; - - public DXGI_RGB Offset; - - public _GammaCurve_e__FixedBuffer GammaCurve; - - public partial struct _GammaCurve_e__FixedBuffer - { - public DXGI_RGB e0; - public DXGI_RGB e1; - public DXGI_RGB e2; - public DXGI_RGB e3; - public DXGI_RGB e4; - public DXGI_RGB e5; - public DXGI_RGB e6; - public DXGI_RGB e7; - public DXGI_RGB e8; - public DXGI_RGB e9; - public DXGI_RGB e10; - public DXGI_RGB e11; - public DXGI_RGB e12; - public DXGI_RGB e13; - public DXGI_RGB e14; - public DXGI_RGB e15; - public DXGI_RGB e16; - public DXGI_RGB e17; - public DXGI_RGB e18; - public DXGI_RGB e19; - public DXGI_RGB e20; - public DXGI_RGB e21; - public DXGI_RGB e22; - public DXGI_RGB e23; - public DXGI_RGB e24; - public DXGI_RGB e25; - public DXGI_RGB e26; - public DXGI_RGB e27; - public DXGI_RGB e28; - public DXGI_RGB e29; - public DXGI_RGB e30; - public DXGI_RGB e31; - public DXGI_RGB e32; - public DXGI_RGB e33; - public DXGI_RGB e34; - public DXGI_RGB e35; - public DXGI_RGB e36; - public DXGI_RGB e37; - public DXGI_RGB e38; - public DXGI_RGB e39; - public DXGI_RGB e40; - public DXGI_RGB e41; - public DXGI_RGB e42; - public DXGI_RGB e43; - public DXGI_RGB e44; - public DXGI_RGB e45; - public DXGI_RGB e46; - public DXGI_RGB e47; - public DXGI_RGB e48; - public DXGI_RGB e49; - public DXGI_RGB e50; - public DXGI_RGB e51; - public DXGI_RGB e52; - public DXGI_RGB e53; - public DXGI_RGB e54; - public DXGI_RGB e55; - public DXGI_RGB e56; - public DXGI_RGB e57; - public DXGI_RGB e58; - public DXGI_RGB e59; - public DXGI_RGB e60; - public DXGI_RGB e61; - public DXGI_RGB e62; - public DXGI_RGB e63; - public DXGI_RGB e64; - public DXGI_RGB e65; - public DXGI_RGB e66; - public DXGI_RGB e67; - public DXGI_RGB e68; - public DXGI_RGB e69; - public DXGI_RGB e70; - public DXGI_RGB e71; - public DXGI_RGB e72; - public DXGI_RGB e73; - public DXGI_RGB e74; - public DXGI_RGB e75; - public DXGI_RGB e76; - public DXGI_RGB e77; - public DXGI_RGB e78; - public DXGI_RGB e79; - public DXGI_RGB e80; - public DXGI_RGB e81; - public DXGI_RGB e82; - public DXGI_RGB e83; - public DXGI_RGB e84; - public DXGI_RGB e85; - public DXGI_RGB e86; - public DXGI_RGB e87; - public DXGI_RGB e88; - public DXGI_RGB e89; - public DXGI_RGB e90; - public DXGI_RGB e91; - public DXGI_RGB e92; - public DXGI_RGB e93; - public DXGI_RGB e94; - public DXGI_RGB e95; - public DXGI_RGB e96; - public DXGI_RGB e97; - public DXGI_RGB e98; - public DXGI_RGB e99; - public DXGI_RGB e100; - public DXGI_RGB e101; - public DXGI_RGB e102; - public DXGI_RGB e103; - public DXGI_RGB e104; - public DXGI_RGB e105; - public DXGI_RGB e106; - public DXGI_RGB e107; - public DXGI_RGB e108; - public DXGI_RGB e109; - public DXGI_RGB e110; - public DXGI_RGB e111; - public DXGI_RGB e112; - public DXGI_RGB e113; - public DXGI_RGB e114; - public DXGI_RGB e115; - public DXGI_RGB e116; - public DXGI_RGB e117; - public DXGI_RGB e118; - public DXGI_RGB e119; - public DXGI_RGB e120; - public DXGI_RGB e121; - public DXGI_RGB e122; - public DXGI_RGB e123; - public DXGI_RGB e124; - public DXGI_RGB e125; - public DXGI_RGB e126; - public DXGI_RGB e127; - public DXGI_RGB e128; - public DXGI_RGB e129; - public DXGI_RGB e130; - public DXGI_RGB e131; - public DXGI_RGB e132; - public DXGI_RGB e133; - public DXGI_RGB e134; - public DXGI_RGB e135; - public DXGI_RGB e136; - public DXGI_RGB e137; - public DXGI_RGB e138; - public DXGI_RGB e139; - public DXGI_RGB e140; - public DXGI_RGB e141; - public DXGI_RGB e142; - public DXGI_RGB e143; - public DXGI_RGB e144; - public DXGI_RGB e145; - public DXGI_RGB e146; - public DXGI_RGB e147; - public DXGI_RGB e148; - public DXGI_RGB e149; - public DXGI_RGB e150; - public DXGI_RGB e151; - public DXGI_RGB e152; - public DXGI_RGB e153; - public DXGI_RGB e154; - public DXGI_RGB e155; - public DXGI_RGB e156; - public DXGI_RGB e157; - public DXGI_RGB e158; - public DXGI_RGB e159; - public DXGI_RGB e160; - public DXGI_RGB e161; - public DXGI_RGB e162; - public DXGI_RGB e163; - public DXGI_RGB e164; - public DXGI_RGB e165; - public DXGI_RGB e166; - public DXGI_RGB e167; - public DXGI_RGB e168; - public DXGI_RGB e169; - public DXGI_RGB e170; - public DXGI_RGB e171; - public DXGI_RGB e172; - public DXGI_RGB e173; - public DXGI_RGB e174; - public DXGI_RGB e175; - public DXGI_RGB e176; - public DXGI_RGB e177; - public DXGI_RGB e178; - public DXGI_RGB e179; - public DXGI_RGB e180; - public DXGI_RGB e181; - public DXGI_RGB e182; - public DXGI_RGB e183; - public DXGI_RGB e184; - public DXGI_RGB e185; - public DXGI_RGB e186; - public DXGI_RGB e187; - public DXGI_RGB e188; - public DXGI_RGB e189; - public DXGI_RGB e190; - public DXGI_RGB e191; - public DXGI_RGB e192; - public DXGI_RGB e193; - public DXGI_RGB e194; - public DXGI_RGB e195; - public DXGI_RGB e196; - public DXGI_RGB e197; - public DXGI_RGB e198; - public DXGI_RGB e199; - public DXGI_RGB e200; - public DXGI_RGB e201; - public DXGI_RGB e202; - public DXGI_RGB e203; - public DXGI_RGB e204; - public DXGI_RGB e205; - public DXGI_RGB e206; - public DXGI_RGB e207; - public DXGI_RGB e208; - public DXGI_RGB e209; - public DXGI_RGB e210; - public DXGI_RGB e211; - public DXGI_RGB e212; - public DXGI_RGB e213; - public DXGI_RGB e214; - public DXGI_RGB e215; - public DXGI_RGB e216; - public DXGI_RGB e217; - public DXGI_RGB e218; - public DXGI_RGB e219; - public DXGI_RGB e220; - public DXGI_RGB e221; - public DXGI_RGB e222; - public DXGI_RGB e223; - public DXGI_RGB e224; - public DXGI_RGB e225; - public DXGI_RGB e226; - public DXGI_RGB e227; - public DXGI_RGB e228; - public DXGI_RGB e229; - public DXGI_RGB e230; - public DXGI_RGB e231; - public DXGI_RGB e232; - public DXGI_RGB e233; - public DXGI_RGB e234; - public DXGI_RGB e235; - public DXGI_RGB e236; - public DXGI_RGB e237; - public DXGI_RGB e238; - public DXGI_RGB e239; - public DXGI_RGB e240; - public DXGI_RGB e241; - public DXGI_RGB e242; - public DXGI_RGB e243; - public DXGI_RGB e244; - public DXGI_RGB e245; - public DXGI_RGB e246; - public DXGI_RGB e247; - public DXGI_RGB e248; - public DXGI_RGB e249; - public DXGI_RGB e250; - public DXGI_RGB e251; - public DXGI_RGB e252; - public DXGI_RGB e253; - public DXGI_RGB e254; - public DXGI_RGB e255; - public DXGI_RGB e256; - public DXGI_RGB e257; - public DXGI_RGB e258; - public DXGI_RGB e259; - public DXGI_RGB e260; - public DXGI_RGB e261; - public DXGI_RGB e262; - public DXGI_RGB e263; - public DXGI_RGB e264; - public DXGI_RGB e265; - public DXGI_RGB e266; - public DXGI_RGB e267; - public DXGI_RGB e268; - public DXGI_RGB e269; - public DXGI_RGB e270; - public DXGI_RGB e271; - public DXGI_RGB e272; - public DXGI_RGB e273; - public DXGI_RGB e274; - public DXGI_RGB e275; - public DXGI_RGB e276; - public DXGI_RGB e277; - public DXGI_RGB e278; - public DXGI_RGB e279; - public DXGI_RGB e280; - public DXGI_RGB e281; - public DXGI_RGB e282; - public DXGI_RGB e283; - public DXGI_RGB e284; - public DXGI_RGB e285; - public DXGI_RGB e286; - public DXGI_RGB e287; - public DXGI_RGB e288; - public DXGI_RGB e289; - public DXGI_RGB e290; - public DXGI_RGB e291; - public DXGI_RGB e292; - public DXGI_RGB e293; - public DXGI_RGB e294; - public DXGI_RGB e295; - public DXGI_RGB e296; - public DXGI_RGB e297; - public DXGI_RGB e298; - public DXGI_RGB e299; - public DXGI_RGB e300; - public DXGI_RGB e301; - public DXGI_RGB e302; - public DXGI_RGB e303; - public DXGI_RGB e304; - public DXGI_RGB e305; - public DXGI_RGB e306; - public DXGI_RGB e307; - public DXGI_RGB e308; - public DXGI_RGB e309; - public DXGI_RGB e310; - public DXGI_RGB e311; - public DXGI_RGB e312; - public DXGI_RGB e313; - public DXGI_RGB e314; - public DXGI_RGB e315; - public DXGI_RGB e316; - public DXGI_RGB e317; - public DXGI_RGB e318; - public DXGI_RGB e319; - public DXGI_RGB e320; - public DXGI_RGB e321; - public DXGI_RGB e322; - public DXGI_RGB e323; - public DXGI_RGB e324; - public DXGI_RGB e325; - public DXGI_RGB e326; - public DXGI_RGB e327; - public DXGI_RGB e328; - public DXGI_RGB e329; - public DXGI_RGB e330; - public DXGI_RGB e331; - public DXGI_RGB e332; - public DXGI_RGB e333; - public DXGI_RGB e334; - public DXGI_RGB e335; - public DXGI_RGB e336; - public DXGI_RGB e337; - public DXGI_RGB e338; - public DXGI_RGB e339; - public DXGI_RGB e340; - public DXGI_RGB e341; - public DXGI_RGB e342; - public DXGI_RGB e343; - public DXGI_RGB e344; - public DXGI_RGB e345; - public DXGI_RGB e346; - public DXGI_RGB e347; - public DXGI_RGB e348; - public DXGI_RGB e349; - public DXGI_RGB e350; - public DXGI_RGB e351; - public DXGI_RGB e352; - public DXGI_RGB e353; - public DXGI_RGB e354; - public DXGI_RGB e355; - public DXGI_RGB e356; - public DXGI_RGB e357; - public DXGI_RGB e358; - public DXGI_RGB e359; - public DXGI_RGB e360; - public DXGI_RGB e361; - public DXGI_RGB e362; - public DXGI_RGB e363; - public DXGI_RGB e364; - public DXGI_RGB e365; - public DXGI_RGB e366; - public DXGI_RGB e367; - public DXGI_RGB e368; - public DXGI_RGB e369; - public DXGI_RGB e370; - public DXGI_RGB e371; - public DXGI_RGB e372; - public DXGI_RGB e373; - public DXGI_RGB e374; - public DXGI_RGB e375; - public DXGI_RGB e376; - public DXGI_RGB e377; - public DXGI_RGB e378; - public DXGI_RGB e379; - public DXGI_RGB e380; - public DXGI_RGB e381; - public DXGI_RGB e382; - public DXGI_RGB e383; - public DXGI_RGB e384; - public DXGI_RGB e385; - public DXGI_RGB e386; - public DXGI_RGB e387; - public DXGI_RGB e388; - public DXGI_RGB e389; - public DXGI_RGB e390; - public DXGI_RGB e391; - public DXGI_RGB e392; - public DXGI_RGB e393; - public DXGI_RGB e394; - public DXGI_RGB e395; - public DXGI_RGB e396; - public DXGI_RGB e397; - public DXGI_RGB e398; - public DXGI_RGB e399; - public DXGI_RGB e400; - public DXGI_RGB e401; - public DXGI_RGB e402; - public DXGI_RGB e403; - public DXGI_RGB e404; - public DXGI_RGB e405; - public DXGI_RGB e406; - public DXGI_RGB e407; - public DXGI_RGB e408; - public DXGI_RGB e409; - public DXGI_RGB e410; - public DXGI_RGB e411; - public DXGI_RGB e412; - public DXGI_RGB e413; - public DXGI_RGB e414; - public DXGI_RGB e415; - public DXGI_RGB e416; - public DXGI_RGB e417; - public DXGI_RGB e418; - public DXGI_RGB e419; - public DXGI_RGB e420; - public DXGI_RGB e421; - public DXGI_RGB e422; - public DXGI_RGB e423; - public DXGI_RGB e424; - public DXGI_RGB e425; - public DXGI_RGB e426; - public DXGI_RGB e427; - public DXGI_RGB e428; - public DXGI_RGB e429; - public DXGI_RGB e430; - public DXGI_RGB e431; - public DXGI_RGB e432; - public DXGI_RGB e433; - public DXGI_RGB e434; - public DXGI_RGB e435; - public DXGI_RGB e436; - public DXGI_RGB e437; - public DXGI_RGB e438; - public DXGI_RGB e439; - public DXGI_RGB e440; - public DXGI_RGB e441; - public DXGI_RGB e442; - public DXGI_RGB e443; - public DXGI_RGB e444; - public DXGI_RGB e445; - public DXGI_RGB e446; - public DXGI_RGB e447; - public DXGI_RGB e448; - public DXGI_RGB e449; - public DXGI_RGB e450; - public DXGI_RGB e451; - public DXGI_RGB e452; - public DXGI_RGB e453; - public DXGI_RGB e454; - public DXGI_RGB e455; - public DXGI_RGB e456; - public DXGI_RGB e457; - public DXGI_RGB e458; - public DXGI_RGB e459; - public DXGI_RGB e460; - public DXGI_RGB e461; - public DXGI_RGB e462; - public DXGI_RGB e463; - public DXGI_RGB e464; - public DXGI_RGB e465; - public DXGI_RGB e466; - public DXGI_RGB e467; - public DXGI_RGB e468; - public DXGI_RGB e469; - public DXGI_RGB e470; - public DXGI_RGB e471; - public DXGI_RGB e472; - public DXGI_RGB e473; - public DXGI_RGB e474; - public DXGI_RGB e475; - public DXGI_RGB e476; - public DXGI_RGB e477; - public DXGI_RGB e478; - public DXGI_RGB e479; - public DXGI_RGB e480; - public DXGI_RGB e481; - public DXGI_RGB e482; - public DXGI_RGB e483; - public DXGI_RGB e484; - public DXGI_RGB e485; - public DXGI_RGB e486; - public DXGI_RGB e487; - public DXGI_RGB e488; - public DXGI_RGB e489; - public DXGI_RGB e490; - public DXGI_RGB e491; - public DXGI_RGB e492; - public DXGI_RGB e493; - public DXGI_RGB e494; - public DXGI_RGB e495; - public DXGI_RGB e496; - public DXGI_RGB e497; - public DXGI_RGB e498; - public DXGI_RGB e499; - public DXGI_RGB e500; - public DXGI_RGB e501; - public DXGI_RGB e502; - public DXGI_RGB e503; - public DXGI_RGB e504; - public DXGI_RGB e505; - public DXGI_RGB e506; - public DXGI_RGB e507; - public DXGI_RGB e508; - public DXGI_RGB e509; - public DXGI_RGB e510; - public DXGI_RGB e511; - public DXGI_RGB e512; - public DXGI_RGB e513; - public DXGI_RGB e514; - public DXGI_RGB e515; - public DXGI_RGB e516; - public DXGI_RGB e517; - public DXGI_RGB e518; - public DXGI_RGB e519; - public DXGI_RGB e520; - public DXGI_RGB e521; - public DXGI_RGB e522; - public DXGI_RGB e523; - public DXGI_RGB e524; - public DXGI_RGB e525; - public DXGI_RGB e526; - public DXGI_RGB e527; - public DXGI_RGB e528; - public DXGI_RGB e529; - public DXGI_RGB e530; - public DXGI_RGB e531; - public DXGI_RGB e532; - public DXGI_RGB e533; - public DXGI_RGB e534; - public DXGI_RGB e535; - public DXGI_RGB e536; - public DXGI_RGB e537; - public DXGI_RGB e538; - public DXGI_RGB e539; - public DXGI_RGB e540; - public DXGI_RGB e541; - public DXGI_RGB e542; - public DXGI_RGB e543; - public DXGI_RGB e544; - public DXGI_RGB e545; - public DXGI_RGB e546; - public DXGI_RGB e547; - public DXGI_RGB e548; - public DXGI_RGB e549; - public DXGI_RGB e550; - public DXGI_RGB e551; - public DXGI_RGB e552; - public DXGI_RGB e553; - public DXGI_RGB e554; - public DXGI_RGB e555; - public DXGI_RGB e556; - public DXGI_RGB e557; - public DXGI_RGB e558; - public DXGI_RGB e559; - public DXGI_RGB e560; - public DXGI_RGB e561; - public DXGI_RGB e562; - public DXGI_RGB e563; - public DXGI_RGB e564; - public DXGI_RGB e565; - public DXGI_RGB e566; - public DXGI_RGB e567; - public DXGI_RGB e568; - public DXGI_RGB e569; - public DXGI_RGB e570; - public DXGI_RGB e571; - public DXGI_RGB e572; - public DXGI_RGB e573; - public DXGI_RGB e574; - public DXGI_RGB e575; - public DXGI_RGB e576; - public DXGI_RGB e577; - public DXGI_RGB e578; - public DXGI_RGB e579; - public DXGI_RGB e580; - public DXGI_RGB e581; - public DXGI_RGB e582; - public DXGI_RGB e583; - public DXGI_RGB e584; - public DXGI_RGB e585; - public DXGI_RGB e586; - public DXGI_RGB e587; - public DXGI_RGB e588; - public DXGI_RGB e589; - public DXGI_RGB e590; - public DXGI_RGB e591; - public DXGI_RGB e592; - public DXGI_RGB e593; - public DXGI_RGB e594; - public DXGI_RGB e595; - public DXGI_RGB e596; - public DXGI_RGB e597; - public DXGI_RGB e598; - public DXGI_RGB e599; - public DXGI_RGB e600; - public DXGI_RGB e601; - public DXGI_RGB e602; - public DXGI_RGB e603; - public DXGI_RGB e604; - public DXGI_RGB e605; - public DXGI_RGB e606; - public DXGI_RGB e607; - public DXGI_RGB e608; - public DXGI_RGB e609; - public DXGI_RGB e610; - public DXGI_RGB e611; - public DXGI_RGB e612; - public DXGI_RGB e613; - public DXGI_RGB e614; - public DXGI_RGB e615; - public DXGI_RGB e616; - public DXGI_RGB e617; - public DXGI_RGB e618; - public DXGI_RGB e619; - public DXGI_RGB e620; - public DXGI_RGB e621; - public DXGI_RGB e622; - public DXGI_RGB e623; - public DXGI_RGB e624; - public DXGI_RGB e625; - public DXGI_RGB e626; - public DXGI_RGB e627; - public DXGI_RGB e628; - public DXGI_RGB e629; - public DXGI_RGB e630; - public DXGI_RGB e631; - public DXGI_RGB e632; - public DXGI_RGB e633; - public DXGI_RGB e634; - public DXGI_RGB e635; - public DXGI_RGB e636; - public DXGI_RGB e637; - public DXGI_RGB e638; - public DXGI_RGB e639; - public DXGI_RGB e640; - public DXGI_RGB e641; - public DXGI_RGB e642; - public DXGI_RGB e643; - public DXGI_RGB e644; - public DXGI_RGB e645; - public DXGI_RGB e646; - public DXGI_RGB e647; - public DXGI_RGB e648; - public DXGI_RGB e649; - public DXGI_RGB e650; - public DXGI_RGB e651; - public DXGI_RGB e652; - public DXGI_RGB e653; - public DXGI_RGB e654; - public DXGI_RGB e655; - public DXGI_RGB e656; - public DXGI_RGB e657; - public DXGI_RGB e658; - public DXGI_RGB e659; - public DXGI_RGB e660; - public DXGI_RGB e661; - public DXGI_RGB e662; - public DXGI_RGB e663; - public DXGI_RGB e664; - public DXGI_RGB e665; - public DXGI_RGB e666; - public DXGI_RGB e667; - public DXGI_RGB e668; - public DXGI_RGB e669; - public DXGI_RGB e670; - public DXGI_RGB e671; - public DXGI_RGB e672; - public DXGI_RGB e673; - public DXGI_RGB e674; - public DXGI_RGB e675; - public DXGI_RGB e676; - public DXGI_RGB e677; - public DXGI_RGB e678; - public DXGI_RGB e679; - public DXGI_RGB e680; - public DXGI_RGB e681; - public DXGI_RGB e682; - public DXGI_RGB e683; - public DXGI_RGB e684; - public DXGI_RGB e685; - public DXGI_RGB e686; - public DXGI_RGB e687; - public DXGI_RGB e688; - public DXGI_RGB e689; - public DXGI_RGB e690; - public DXGI_RGB e691; - public DXGI_RGB e692; - public DXGI_RGB e693; - public DXGI_RGB e694; - public DXGI_RGB e695; - public DXGI_RGB e696; - public DXGI_RGB e697; - public DXGI_RGB e698; - public DXGI_RGB e699; - public DXGI_RGB e700; - public DXGI_RGB e701; - public DXGI_RGB e702; - public DXGI_RGB e703; - public DXGI_RGB e704; - public DXGI_RGB e705; - public DXGI_RGB e706; - public DXGI_RGB e707; - public DXGI_RGB e708; - public DXGI_RGB e709; - public DXGI_RGB e710; - public DXGI_RGB e711; - public DXGI_RGB e712; - public DXGI_RGB e713; - public DXGI_RGB e714; - public DXGI_RGB e715; - public DXGI_RGB e716; - public DXGI_RGB e717; - public DXGI_RGB e718; - public DXGI_RGB e719; - public DXGI_RGB e720; - public DXGI_RGB e721; - public DXGI_RGB e722; - public DXGI_RGB e723; - public DXGI_RGB e724; - public DXGI_RGB e725; - public DXGI_RGB e726; - public DXGI_RGB e727; - public DXGI_RGB e728; - public DXGI_RGB e729; - public DXGI_RGB e730; - public DXGI_RGB e731; - public DXGI_RGB e732; - public DXGI_RGB e733; - public DXGI_RGB e734; - public DXGI_RGB e735; - public DXGI_RGB e736; - public DXGI_RGB e737; - public DXGI_RGB e738; - public DXGI_RGB e739; - public DXGI_RGB e740; - public DXGI_RGB e741; - public DXGI_RGB e742; - public DXGI_RGB e743; - public DXGI_RGB e744; - public DXGI_RGB e745; - public DXGI_RGB e746; - public DXGI_RGB e747; - public DXGI_RGB e748; - public DXGI_RGB e749; - public DXGI_RGB e750; - public DXGI_RGB e751; - public DXGI_RGB e752; - public DXGI_RGB e753; - public DXGI_RGB e754; - public DXGI_RGB e755; - public DXGI_RGB e756; - public DXGI_RGB e757; - public DXGI_RGB e758; - public DXGI_RGB e759; - public DXGI_RGB e760; - public DXGI_RGB e761; - public DXGI_RGB e762; - public DXGI_RGB e763; - public DXGI_RGB e764; - public DXGI_RGB e765; - public DXGI_RGB e766; - public DXGI_RGB e767; - public DXGI_RGB e768; - public DXGI_RGB e769; - public DXGI_RGB e770; - public DXGI_RGB e771; - public DXGI_RGB e772; - public DXGI_RGB e773; - public DXGI_RGB e774; - public DXGI_RGB e775; - public DXGI_RGB e776; - public DXGI_RGB e777; - public DXGI_RGB e778; - public DXGI_RGB e779; - public DXGI_RGB e780; - public DXGI_RGB e781; - public DXGI_RGB e782; - public DXGI_RGB e783; - public DXGI_RGB e784; - public DXGI_RGB e785; - public DXGI_RGB e786; - public DXGI_RGB e787; - public DXGI_RGB e788; - public DXGI_RGB e789; - public DXGI_RGB e790; - public DXGI_RGB e791; - public DXGI_RGB e792; - public DXGI_RGB e793; - public DXGI_RGB e794; - public DXGI_RGB e795; - public DXGI_RGB e796; - public DXGI_RGB e797; - public DXGI_RGB e798; - public DXGI_RGB e799; - public DXGI_RGB e800; - public DXGI_RGB e801; - public DXGI_RGB e802; - public DXGI_RGB e803; - public DXGI_RGB e804; - public DXGI_RGB e805; - public DXGI_RGB e806; - public DXGI_RGB e807; - public DXGI_RGB e808; - public DXGI_RGB e809; - public DXGI_RGB e810; - public DXGI_RGB e811; - public DXGI_RGB e812; - public DXGI_RGB e813; - public DXGI_RGB e814; - public DXGI_RGB e815; - public DXGI_RGB e816; - public DXGI_RGB e817; - public DXGI_RGB e818; - public DXGI_RGB e819; - public DXGI_RGB e820; - public DXGI_RGB e821; - public DXGI_RGB e822; - public DXGI_RGB e823; - public DXGI_RGB e824; - public DXGI_RGB e825; - public DXGI_RGB e826; - public DXGI_RGB e827; - public DXGI_RGB e828; - public DXGI_RGB e829; - public DXGI_RGB e830; - public DXGI_RGB e831; - public DXGI_RGB e832; - public DXGI_RGB e833; - public DXGI_RGB e834; - public DXGI_RGB e835; - public DXGI_RGB e836; - public DXGI_RGB e837; - public DXGI_RGB e838; - public DXGI_RGB e839; - public DXGI_RGB e840; - public DXGI_RGB e841; - public DXGI_RGB e842; - public DXGI_RGB e843; - public DXGI_RGB e844; - public DXGI_RGB e845; - public DXGI_RGB e846; - public DXGI_RGB e847; - public DXGI_RGB e848; - public DXGI_RGB e849; - public DXGI_RGB e850; - public DXGI_RGB e851; - public DXGI_RGB e852; - public DXGI_RGB e853; - public DXGI_RGB e854; - public DXGI_RGB e855; - public DXGI_RGB e856; - public DXGI_RGB e857; - public DXGI_RGB e858; - public DXGI_RGB e859; - public DXGI_RGB e860; - public DXGI_RGB e861; - public DXGI_RGB e862; - public DXGI_RGB e863; - public DXGI_RGB e864; - public DXGI_RGB e865; - public DXGI_RGB e866; - public DXGI_RGB e867; - public DXGI_RGB e868; - public DXGI_RGB e869; - public DXGI_RGB e870; - public DXGI_RGB e871; - public DXGI_RGB e872; - public DXGI_RGB e873; - public DXGI_RGB e874; - public DXGI_RGB e875; - public DXGI_RGB e876; - public DXGI_RGB e877; - public DXGI_RGB e878; - public DXGI_RGB e879; - public DXGI_RGB e880; - public DXGI_RGB e881; - public DXGI_RGB e882; - public DXGI_RGB e883; - public DXGI_RGB e884; - public DXGI_RGB e885; - public DXGI_RGB e886; - public DXGI_RGB e887; - public DXGI_RGB e888; - public DXGI_RGB e889; - public DXGI_RGB e890; - public DXGI_RGB e891; - public DXGI_RGB e892; - public DXGI_RGB e893; - public DXGI_RGB e894; - public DXGI_RGB e895; - public DXGI_RGB e896; - public DXGI_RGB e897; - public DXGI_RGB e898; - public DXGI_RGB e899; - public DXGI_RGB e900; - public DXGI_RGB e901; - public DXGI_RGB e902; - public DXGI_RGB e903; - public DXGI_RGB e904; - public DXGI_RGB e905; - public DXGI_RGB e906; - public DXGI_RGB e907; - public DXGI_RGB e908; - public DXGI_RGB e909; - public DXGI_RGB e910; - public DXGI_RGB e911; - public DXGI_RGB e912; - public DXGI_RGB e913; - public DXGI_RGB e914; - public DXGI_RGB e915; - public DXGI_RGB e916; - public DXGI_RGB e917; - public DXGI_RGB e918; - public DXGI_RGB e919; - public DXGI_RGB e920; - public DXGI_RGB e921; - public DXGI_RGB e922; - public DXGI_RGB e923; - public DXGI_RGB e924; - public DXGI_RGB e925; - public DXGI_RGB e926; - public DXGI_RGB e927; - public DXGI_RGB e928; - public DXGI_RGB e929; - public DXGI_RGB e930; - public DXGI_RGB e931; - public DXGI_RGB e932; - public DXGI_RGB e933; - public DXGI_RGB e934; - public DXGI_RGB e935; - public DXGI_RGB e936; - public DXGI_RGB e937; - public DXGI_RGB e938; - public DXGI_RGB e939; - public DXGI_RGB e940; - public DXGI_RGB e941; - public DXGI_RGB e942; - public DXGI_RGB e943; - public DXGI_RGB e944; - public DXGI_RGB e945; - public DXGI_RGB e946; - public DXGI_RGB e947; - public DXGI_RGB e948; - public DXGI_RGB e949; - public DXGI_RGB e950; - public DXGI_RGB e951; - public DXGI_RGB e952; - public DXGI_RGB e953; - public DXGI_RGB e954; - public DXGI_RGB e955; - public DXGI_RGB e956; - public DXGI_RGB e957; - public DXGI_RGB e958; - public DXGI_RGB e959; - public DXGI_RGB e960; - public DXGI_RGB e961; - public DXGI_RGB e962; - public DXGI_RGB e963; - public DXGI_RGB e964; - public DXGI_RGB e965; - public DXGI_RGB e966; - public DXGI_RGB e967; - public DXGI_RGB e968; - public DXGI_RGB e969; - public DXGI_RGB e970; - public DXGI_RGB e971; - public DXGI_RGB e972; - public DXGI_RGB e973; - public DXGI_RGB e974; - public DXGI_RGB e975; - public DXGI_RGB e976; - public DXGI_RGB e977; - public DXGI_RGB e978; - public DXGI_RGB e979; - public DXGI_RGB e980; - public DXGI_RGB e981; - public DXGI_RGB e982; - public DXGI_RGB e983; - public DXGI_RGB e984; - public DXGI_RGB e985; - public DXGI_RGB e986; - public DXGI_RGB e987; - public DXGI_RGB e988; - public DXGI_RGB e989; - public DXGI_RGB e990; - public DXGI_RGB e991; - public DXGI_RGB e992; - public DXGI_RGB e993; - public DXGI_RGB e994; - public DXGI_RGB e995; - public DXGI_RGB e996; - public DXGI_RGB e997; - public DXGI_RGB e998; - public DXGI_RGB e999; - public DXGI_RGB e1000; - public DXGI_RGB e1001; - public DXGI_RGB e1002; - public DXGI_RGB e1003; - public DXGI_RGB e1004; - public DXGI_RGB e1005; - public DXGI_RGB e1006; - public DXGI_RGB e1007; - public DXGI_RGB e1008; - public DXGI_RGB e1009; - public DXGI_RGB e1010; - public DXGI_RGB e1011; - public DXGI_RGB e1012; - public DXGI_RGB e1013; - public DXGI_RGB e1014; - public DXGI_RGB e1015; - public DXGI_RGB e1016; - public DXGI_RGB e1017; - public DXGI_RGB e1018; - public DXGI_RGB e1019; - public DXGI_RGB e1020; - public DXGI_RGB e1021; - public DXGI_RGB e1022; - public DXGI_RGB e1023; - public DXGI_RGB e1024; -#if NET6_0_OR_GREATER - public ref DXGI_RGB this[int index] - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get - { - return ref AsSpan()[index]; - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public Span AsSpan() => MemoryMarshal.CreateSpan(ref e0, 1025); -#else - // there is no way to do this outside of terrible unsafe code. Don't do this in .Net Standard 2.0 - - public DXGI_RGB this[int index] - { - get - { - if ((uint)index > 1025) - throw new ArgumentOutOfRangeException("index"); - - fixed (DXGI_RGB* basePtr = &e0) - { - DXGI_RGB* newPtr = basePtr + index; - return *newPtr; - } - } - set - { - if ((uint)index > 1025) - throw new ArgumentOutOfRangeException("index"); - - fixed (DXGI_RGB* basePtr = &e0) - { - DXGI_RGB* newPtr = basePtr + index; - *newPtr = value; - } - } - } -#endif - } - } - + internal unsafe struct DEVMODEW { public fixed ushort dmDeviceName[32]; @@ -1196,7 +119,7 @@ namespace Avalonia.Win32.DxgiSwapchain public uint Flags; } - internal unsafe struct DXGI_FRAME_STATISTICS + internal struct DXGI_FRAME_STATISTICS { public uint PresentCount; @@ -1225,11 +148,10 @@ namespace Avalonia.Win32.DxgiSwapchain internal unsafe struct DXGI_MAPPED_RECT { public int Pitch; - public byte* pBits; } - internal unsafe partial struct DXGI_MODE_DESC + internal struct DXGI_MODE_DESC { public ushort Width; public ushort Height; @@ -1239,7 +161,7 @@ namespace Avalonia.Win32.DxgiSwapchain public DXGI_MODE_SCALING Scaling; } - internal unsafe partial struct DXGI_OUTPUT_DESC + internal unsafe struct DXGI_OUTPUT_DESC { internal fixed ushort DeviceName[32]; @@ -1263,13 +185,13 @@ namespace Avalonia.Win32.DxgiSwapchain public POINT* pScrollOffset; } - internal unsafe partial struct DXGI_RATIONAL + internal struct DXGI_RATIONAL { public ushort Numerator; public ushort Denominator; } - internal partial struct DXGI_RGB + internal struct DXGI_RGB { public float Red; @@ -1278,7 +200,7 @@ namespace Avalonia.Win32.DxgiSwapchain public float Blue; } - internal partial struct DXGI_RGBA + internal struct DXGI_RGBA { public float r; @@ -1295,7 +217,7 @@ namespace Avalonia.Win32.DxgiSwapchain public uint Quality; } - internal unsafe struct DXGI_SURFACE_DESC + internal struct DXGI_SURFACE_DESC { public uint Width; @@ -1306,7 +228,7 @@ namespace Avalonia.Win32.DxgiSwapchain public DXGI_SAMPLE_DESC SampleDesc; } - internal unsafe partial struct DXGI_SWAP_CHAIN_DESC + internal struct DXGI_SWAP_CHAIN_DESC { public DXGI_MODE_DESC BufferDesc; public DXGI_SAMPLE_DESC SampleDesc; @@ -1333,7 +255,7 @@ namespace Avalonia.Win32.DxgiSwapchain public uint Flags; } - internal unsafe struct DXGI_SWAP_CHAIN_FULLSCREEN_DESC + internal struct DXGI_SWAP_CHAIN_FULLSCREEN_DESC { public DXGI_RATIONAL RefreshRate; @@ -1344,7 +266,7 @@ namespace Avalonia.Win32.DxgiSwapchain public int Windowed; } - internal partial struct D3D11_TEXTURE2D_DESC + internal struct D3D11_TEXTURE2D_DESC { public uint Width; diff --git a/src/Windows/Avalonia.Win32/DirectX/DirectXUnmanagedMethods.cs b/src/Windows/Avalonia.Win32/DirectX/DirectXUnmanagedMethods.cs index 1a739ce3d1..14a9ff1277 100644 --- a/src/Windows/Avalonia.Win32/DirectX/DirectXUnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/DirectX/DirectXUnmanagedMethods.cs @@ -5,7 +5,7 @@ using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; -namespace Avalonia.Win32.DxgiSwapchain +namespace Avalonia.Win32.DirectX { internal unsafe class DirectXUnmanagedMethods { @@ -23,7 +23,16 @@ namespace Avalonia.Win32.DxgiSwapchain [DllImport("user32", ExactSpelling = true)] internal static extern bool EnumDisplaySettingsW(ushort* lpszDeviceName, uint iModeNum, DEVMODEW* lpDevMode); - [DllImport("user32", ExactSpelling = true, SetLastError = true)] - internal static extern bool GetClientRect(IntPtr hWnd, Interop.UnmanagedMethods.RECT* lpRect); + [DllImport("d3d11", ExactSpelling = true, PreserveSig = false)] + public static extern void D3D11CreateDevice( + IntPtr adapter, D3D_DRIVER_TYPE DriverType, + IntPtr Software, + uint Flags, + D3D_FEATURE_LEVEL[] pFeatureLevels, + uint FeatureLevels, + uint SDKVersion, + out IntPtr ppDevice, + out D3D_FEATURE_LEVEL pFeatureLevel, + IntPtr* ppImmediateContext); } } diff --git a/src/Windows/Avalonia.Win32/DirectX/DxgiConnection.cs b/src/Windows/Avalonia.Win32/DirectX/DxgiConnection.cs index 8c13ecdcc1..e82f7633be 100644 --- a/src/Windows/Avalonia.Win32/DirectX/DxgiConnection.cs +++ b/src/Windows/Avalonia.Win32/DirectX/DxgiConnection.cs @@ -10,11 +10,12 @@ using Avalonia.Logging; using Avalonia.OpenGL.Angle; using Avalonia.OpenGL.Egl; using Avalonia.Rendering; +using Avalonia.Win32.OpenGl.Angle; using static Avalonia.Win32.Interop.UnmanagedMethods; -using static Avalonia.Win32.DxgiSwapchain.DirectXUnmanagedMethods; +using static Avalonia.Win32.DirectX.DirectXUnmanagedMethods; using MicroCom.Runtime; -namespace Avalonia.Win32.DxgiSwapchain +namespace Avalonia.Win32.DirectX { #pragma warning disable CA1416 // This should only be reachable on Windows #nullable enable @@ -25,34 +26,27 @@ namespace Avalonia.Win32.DxgiSwapchain public bool RunsInBackground => true; public event Action? Tick; - - private AngleWin32EglDisplay _angle; - private EglPlatformOpenGlInterface _gl; private object _syncLock; private IDXGIOutput? _output = null; private Stopwatch? _stopwatch = null; + private const string LogArea = "DXGI"; - public DxgiConnection(EglPlatformOpenGlInterface gl, object syncLock) + public DxgiConnection(object syncLock) { - _syncLock = syncLock; - _angle = (AngleWin32EglDisplay)gl.Display; - _gl = gl; } - - public EglPlatformOpenGlInterface Egl => _gl; - - public static void TryCreateAndRegister(EglPlatformOpenGlInterface angle) + + public static void TryCreateAndRegister() { try { - TryCreateAndRegisterCore(angle); + TryCreateAndRegisterCore(); } catch (Exception ex) { - Logger.TryGet(LogEventLevel.Error, nameof(DxgiSwapchain)) + Logger.TryGet(LogEventLevel.Error, LogArea) ?.Log(null, "Unable to establish Dxgi: {0}", ex); } } @@ -66,7 +60,7 @@ namespace Avalonia.Win32.DxgiSwapchain } catch (Exception ex) { - Logger.TryGet(LogEventLevel.Error, nameof(DxgiSwapchain)) + Logger.TryGet(LogEventLevel.Error, LogArea) ?.Log(this, $"Failed to wait for vblank, Exception: {ex.Message}, HRESULT = {ex.HResult}"); } @@ -84,7 +78,7 @@ namespace Avalonia.Win32.DxgiSwapchain } catch (Exception ex) { - Logger.TryGet(LogEventLevel.Error, nameof(DxgiSwapchain)) + Logger.TryGet(LogEventLevel.Error, LogArea) ?.Log(this, $"Failed to wait for vblank, Exception: {ex.Message}, HRESULT = {ex.HResult}"); _output.Dispose(); _output = null; @@ -103,7 +97,7 @@ namespace Avalonia.Win32.DxgiSwapchain } catch (Exception ex) { - Logger.TryGet(LogEventLevel.Error, nameof(DxgiSwapchain)) + Logger.TryGet(LogEventLevel.Error, LogArea) ?.Log(this, $"Failed to wait for vblank, Exception: {ex.Message}, HRESULT = {ex.HResult}"); } } @@ -168,7 +162,7 @@ namespace Avalonia.Win32.DxgiSwapchain } // Used the windows composition as a blueprint for this startup/creation - static private bool TryCreateAndRegisterCore(EglPlatformOpenGlInterface gl) + static private bool TryCreateAndRegisterCore() { var tcs = new TaskCompletionSource(); var pumpLock = new object(); @@ -178,7 +172,7 @@ namespace Avalonia.Win32.DxgiSwapchain { DxgiConnection connection; - connection = new DxgiConnection(gl, pumpLock); + connection = new DxgiConnection(pumpLock); AvaloniaLocator.CurrentMutable.BindToSelf(connection); AvaloniaLocator.CurrentMutable.Bind().ToConstant(connection); diff --git a/src/Windows/Avalonia.Win32/DirectX/DxgiRenderTarget.cs b/src/Windows/Avalonia.Win32/DirectX/DxgiRenderTarget.cs index 2ab9b30d43..cb7826e185 100644 --- a/src/Windows/Avalonia.Win32/DirectX/DxgiRenderTarget.cs +++ b/src/Windows/Avalonia.Win32/DirectX/DxgiRenderTarget.cs @@ -8,11 +8,12 @@ using System.Threading.Tasks; using Avalonia.OpenGL.Angle; using Avalonia.OpenGL.Egl; using Avalonia.OpenGL.Surfaces; +using Avalonia.Win32.OpenGl.Angle; using MicroCom.Runtime; using static Avalonia.OpenGL.Egl.EglGlPlatformSurfaceBase; using static Avalonia.Win32.Interop.UnmanagedMethods; -namespace Avalonia.Win32.DxgiSwapchain +namespace Avalonia.Win32.DirectX { #pragma warning disable CA1416 // Validate platform compatibility, if you enter this not on windows you have messed up badly #nullable enable @@ -22,8 +23,7 @@ namespace Avalonia.Win32.DxgiSwapchain public const uint DXGI_USAGE_RENDER_TARGET_OUTPUT = 0x00000020U; - private IEglWindowGlPlatformSurfaceInfo _window; - private EglPlatformOpenGlInterface _egl; + private EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo _window; private DxgiConnection _connection; private IDXGIDevice? _dxgiDevice = null; private IDXGIFactory2? _dxgiFactory = null; @@ -36,15 +36,14 @@ namespace Avalonia.Win32.DxgiSwapchain private Guid ID3D11Texture2DGuid = Guid.Parse("6F15AAF2-D208-4E89-9AB4-489535D34F9C"); - public DxgiRenderTarget(IEglWindowGlPlatformSurfaceInfo window, EglPlatformOpenGlInterface egl, DxgiConnection connection) : base(egl) + public DxgiRenderTarget(EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo window, EglContext context, DxgiConnection connection) : base(context) { _window = window; - _egl = egl; _connection = connection; // the D3D device is expected to at least be an ID3D11Device // but how do I wrap an IntPtr as a managed IUnknown now? Like this. - IUnknown pdevice = MicroComRuntime.CreateProxyFor(((AngleWin32EglDisplay)_egl.Display).GetDirect3DDevice(), false); + IUnknown pdevice = MicroComRuntime.CreateProxyFor(((AngleWin32EglDisplay)context.Display).GetDirect3DDevice(), false); _dxgiDevice = pdevice.QueryInterface(); @@ -86,14 +85,14 @@ namespace Avalonia.Win32.DxgiSwapchain _clientRect = pClientRect; } - public override IGlPlatformSurfaceRenderingSession BeginDraw() + public override IGlPlatformSurfaceRenderingSession BeginDrawCore() { if (_swapChain is null) { throw new InvalidOperationException("No chain to draw on"); } - var contextLock = _egl.PrimaryContext.EnsureCurrent(); + var contextLock = Context.EnsureCurrent(); EglSurface? surface = null; IDisposable? transaction = null; var success = false; @@ -132,10 +131,10 @@ namespace Avalonia.Win32.DxgiSwapchain _renderTexture = texture; // I also have to get the pointer to this texture directly - surface = ((AngleWin32EglDisplay)_egl.Display).WrapDirect3D11Texture(_egl, MicroComRuntime.GetNativeIntPtr(_renderTexture), + surface = ((AngleWin32EglDisplay)Context.Display).WrapDirect3D11Texture(MicroComRuntime.GetNativeIntPtr(_renderTexture), 0, 0, size.Width, size.Height); - var res = base.BeginDraw(surface, _window, () => + var res = base.BeginDraw(surface, _window.Size, _window.Scaling, () => { _swapChain.Present((ushort)0U, (ushort)0U); surface?.Dispose(); diff --git a/src/Windows/Avalonia.Win32/DirectX/DxgiSwapchainWindow.cs b/src/Windows/Avalonia.Win32/DirectX/DxgiSwapchainWindow.cs index a40d8abf80..88226c5c89 100644 --- a/src/Windows/Avalonia.Win32/DirectX/DxgiSwapchainWindow.cs +++ b/src/Windows/Avalonia.Win32/DirectX/DxgiSwapchainWindow.cs @@ -3,29 +3,29 @@ using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; +using Avalonia.OpenGL; using Avalonia.OpenGL.Egl; using Avalonia.OpenGL.Surfaces; -namespace Avalonia.Win32.DxgiSwapchain +namespace Avalonia.Win32.DirectX { public class DxgiSwapchainWindow : EglGlPlatformSurfaceBase { private DxgiConnection _connection; - private EglPlatformOpenGlInterface _egl; - private IEglWindowGlPlatformSurfaceInfo _window; + private EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo _window; - public DxgiSwapchainWindow(DxgiConnection connection, IEglWindowGlPlatformSurfaceInfo window) + public DxgiSwapchainWindow(DxgiConnection connection, EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo window) { _connection = connection; _window = window; - _egl = connection.Egl; } - public override IGlPlatformSurfaceRenderTarget CreateGlRenderTarget() + public override IGlPlatformSurfaceRenderTarget CreateGlRenderTarget(IGlContext context) { - using (_egl.PrimaryContext.EnsureCurrent()) + var eglContext = (EglContext)context; + using (eglContext.EnsureCurrent()) { - return new DxgiRenderTarget(_window, _egl, _connection); + return new DxgiRenderTarget(_window, eglContext, _connection); } } } diff --git a/src/Windows/Avalonia.Win32/DirectX/IDirect3D11TexturePlatformSurface.cs b/src/Windows/Avalonia.Win32/DirectX/IDirect3D11TexturePlatformSurface.cs new file mode 100644 index 0000000000..d2a2b2f513 --- /dev/null +++ b/src/Windows/Avalonia.Win32/DirectX/IDirect3D11TexturePlatformSurface.cs @@ -0,0 +1,26 @@ +using System; +using Avalonia.OpenGL; +using Avalonia.Platform; + +namespace Avalonia.Win32.DirectX; + +public interface IDirect3D11TexturePlatformSurface +{ + public IDirect3D11TextureRenderTarget CreateRenderTarget(IPlatformGraphicsContext graphicsContext, IntPtr d3dDevice); +} + + + +public interface IDirect3D11TextureRenderTarget : IDisposable +{ + bool IsCorrupted { get; } + IDirect3D11TextureRenderTargetRenderSession BeginDraw(); +} + +public interface IDirect3D11TextureRenderTargetRenderSession : IDisposable +{ + public IntPtr D3D11Texture2D { get; } + public PixelSize Size { get; } + public PixelPoint Offset { get; } + public double Scaling { get; } +} diff --git a/src/Windows/Avalonia.Win32/DirectX/directx.idl b/src/Windows/Avalonia.Win32/DirectX/directx.idl index 09d1adf574..a4552eedea 100644 --- a/src/Windows/Avalonia.Win32/DirectX/directx.idl +++ b/src/Windows/Avalonia.Win32/DirectX/directx.idl @@ -1,4 +1,4 @@ -@clr-namespace Avalonia.Win32.DxgiSwapchain +@clr-namespace Avalonia.Win32.DirectX @clr-access internal @clr-map FLOAT float @clr-map HSTRING IntPtr @@ -12,6 +12,7 @@ @clr-map HWND IntPtr @clr-map BOOL int @clr-map DWORD int +@clr-map SIZE_T IntPtr @clr-map boolean int @clr-map BYTE byte @clr-map INT16 short @@ -242,9 +243,9 @@ interface IDXGIOutput : IDXGIObject HRESULT WaitForVBlank(); HRESULT TakeOwnership([in, annotation("_In_")] IUnknown* pDevice, BOOL Exclusive); void ReleaseOwnership(); - HRESULT GetGammaControlCapabilities([out, annotation("_Out_")] DXGI_GAMMA_CONTROL_CAPABILITIES* pGammaCaps); - HRESULT SetGammaControl([in, annotation("_In_")] DXGI_GAMMA_CONTROL* pArray); - HRESULT GetGammaControl([out, annotation("_Out_")] DXGI_GAMMA_CONTROL* pArray); + HRESULT GetGammaControlCapabilities(IntPtr pGammaCaps); + HRESULT SetGammaControl([in, annotation("_In_")] void* pArray); + HRESULT GetGammaControl(IntPtr pArray); HRESULT SetDisplaySurface([in, annotation("_In_")] IDXGISurface* pScanoutSurface); HRESULT GetDisplaySurfaceData([in, annotation("_In_")] IDXGISurface* pDestination); HRESULT GetFrameStatistics([out, annotation("_Out_")] DXGI_FRAME_STATISTICS* pStats); @@ -303,3 +304,180 @@ interface IDXGISwapChain1 : IDXGISwapChain HRESULT GetRotation([out, annotation("_Out_")] DXGI_MODE_ROTATION* pRotation); } +enum D3D11_FEATURE +{ + D3D11_FEATURE_THREADING, + D3D11_FEATURE_DOUBLES, + D3D11_FEATURE_FORMAT_SUPPORT, + D3D11_FEATURE_FORMAT_SUPPORT2, + D3D11_FEATURE_D3D10_X_HARDWARE_OPTIONS, + D3D11_FEATURE_D3D11_OPTIONS, + D3D11_FEATURE_ARCHITECTURE_INFO, + D3D11_FEATURE_D3D9_OPTIONS, + D3D11_FEATURE_SHADER_MIN_PRECISION_SUPPORT, + D3D11_FEATURE_D3D9_SHADOW_SUPPORT, + D3D11_FEATURE_D3D11_OPTIONS1, + D3D11_FEATURE_D3D9_SIMPLE_INSTANCING_SUPPORT, + D3D11_FEATURE_MARKER_SUPPORT, + D3D11_FEATURE_D3D9_OPTIONS1, + D3D11_FEATURE_D3D11_OPTIONS2, + D3D11_FEATURE_D3D11_OPTIONS3, + D3D11_FEATURE_GPU_VIRTUAL_ADDRESS_SUPPORT, + D3D11_FEATURE_D3D11_OPTIONSS, + D3D11_FEATURE_SHADER_CACHE +} + +[uuid(db6f6ddb-ac77-4e88-8253-819df9bbf140)] +interface ID3D11Device : IUnknown +{ + HRESULT CreateBuffer( + IntPtr pDesc, + IntPtr pInitialData, + [out, retval] IUnknown** ppBuffer ); + HRESULT CreateTexture1D( + IntPtr pDesc, + IntPtr pInitialData, + [out, retval] IUnknown** ppTexture1D ); + HRESULT CreateTexture2D( + IntPtr pDesc, + IntPtr pInitialData, + [out, retval] IUnknown** ppTexture2D ); + HRESULT CreateTexture3D( + IntPtr pDesc, + IntPtr pInitialData, + [out, retval] IUnknown** ppTexture3D ); + HRESULT CreateShaderResourceView( + IntPtr pResource, + IntPtr pDesc, + [out, retval] IUnknown** ppSRView ); + HRESULT CreateUnorderedAccessView( + IntPtr pResource, + IntPtr pDesc, + [out, retval] IUnknown** ppUAView ); + HRESULT CreateRenderTargetView( + IntPtr pResource, + IntPtr pDesc, + [out, retval] IUnknown** ppRTView); + HRESULT CreateDepthStencilView( + IntPtr pResource, + IntPtr pDesc, + [out, retval] IUnknown** ppDepthStencilView ); + HRESULT CreateInputLayout( + IntPtr pInputElementDescs, + UINT NumElements, + void* pShaderBytecodeWithInputSignature, + IntPtr BytecodeLength, + [out, retval] IUnknown** ppInputLayout ); + HRESULT CreateVertexShader( + IntPtr pShaderBytecode, + [annotation("_In_")] SIZE_T BytecodeLength, + IntPtr pClassLinkage, + [out, retval] IUnknown** ppVertexShader ); + HRESULT CreateGeometryShader( + IntPtr pShaderBytecode, + [annotation("_In_")] SIZE_T BytecodeLength, + IntPtr pClassLinkage, + [out, retval] IUnknown** ppGeometryShader ); + HRESULT CreateGeometryShaderWithStreamOutput( + IntPtr pShaderBytecode, + [annotation("_In_")] SIZE_T BytecodeLength, + IntPtr pSODeclaration, + UINT NumEntries, + UINT* pBufferStrides, + UINT NumStrides, + UINT RasterizedStream, + IntPtr pClassLinkage, + [out, retval] IUnknown** ppGeometryShader ); + HRESULT CreatePixelShader( + IntPtr pShaderBytecode, + [annotation("_In_")] SIZE_T BytecodeLength, + IntPtr pClassLinkage, + [out, retval] IUnknown** ppPixelShader ); + HRESULT CreateHullShader( + IntPtr pShaderBytecode, + [annotation("_In_")] SIZE_T BytecodeLength, + IntPtr pClassLinkage, + [out, retval] IUnknown** ppHullShader ); + HRESULT CreateDomainShader( + IntPtr pShaderBytecode, + [annotation("_In_")] SIZE_T BytecodeLength, + IntPtr pClassLinkage, + [out, retval] IUnknown** ppDomainShader ); + HRESULT CreateComputeShader( + IntPtr pShaderBytecode, + [annotation("_In_")] SIZE_T BytecodeLength, + IntPtr pClassLinkage, + [out, retval] IUnknown** ppComputeShader ); + HRESULT CreateClassLinkage([out, retval]IUnknown** ppLinkage); + HRESULT CreateBlendState( + IntPtr pBlendStateDesc, + [out, retval] IUnknown** ppBlendState ); + HRESULT CreateDepthStencilState( + IntPtr pDepthStencilDesc, + [out, retval] IUnknown** ppDepthStencilState ); + HRESULT CreateRasterizerState( + IntPtr pRasterizerDesc, + [out, retval] IUnknown** ppRasterizerState ); + HRESULT CreateSamplerState( + IntPtr pSamplerDesc, + [out, retval] IUnknown** ppSamplerState ); + HRESULT CreateQuery( + IntPtr pQueryDesc, + [out, retval] IUnknown** ppQuery ); + HRESULT CreatePredicate( + IntPtr pPredicateDesc, + [out, retval] IUnknown** ppPredicate ); + HRESULT CreateCounter( + IntPtr pCounterDesc, + [out, retval] IUnknown** ppCounter ); + HRESULT CreateDeferredContext( + UINT ContextFlags, // Reserved parameter; must be 0 + [out, retval] IUnknown** ppDeferredContext ); + HRESULT OpenSharedResource( + IntPtr hResource, + [out] Guid* ReturnedInterface, + [out, retval] IUnknown** ppResource); + + // Check* + HRESULT CheckFormatSupport( + [annotation("_In_")] DXGI_FORMAT Format, + [annotation("_Out_")] UINT* pFormatSupport ); + HRESULT CheckMultisampleQualityLevels( + [annotation("_In_")] DXGI_FORMAT Format, + [annotation("_In_")] UINT SampleCount, + [annotation("_Out_")] UINT* pNumQualityLevels ); + void CheckCounterInfo(IntPtr pCounterInfo ); + HRESULT CheckCounter( + IntPtr pDesc, + IntPtr pType, + IntPtr pActiveCounters, + IntPtr szName, + UINT* pNameLength, + IntPtr szUnits, + UINT* pUnitsLength, + IntPtr szDescription, + UINT* pDescriptionLength ); + HRESULT CheckFeatureSupport( + D3D11_FEATURE Feature, + void* pFeatureSupportData, + UINT FeatureSupportDataSize ); + + HRESULT GetPrivateData( + Guid* guid, + UINT* pDataSize, + void* pData ); + HRESULT SetPrivateData( + Guid* guid, + UINT DataSize, + IntPtr* pData ); + HRESULT SetPrivateDataInterface( + Guid* guid, + IUnknown* pData ); + + D3D_FEATURE_LEVEL GetFeatureLevel(); + UINT GetCreationFlags(); + int GetDeviceRemovedReason(); + void GetImmediateContext(IntPtr* ppImmediateContext ); + HRESULT SetExceptionMode( UINT RaiseFlags ); + UINT GetExceptionMode(); +} diff --git a/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleD3DTextureFeature.cs b/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleD3DTextureFeature.cs new file mode 100644 index 0000000000..6c951010c8 --- /dev/null +++ b/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleD3DTextureFeature.cs @@ -0,0 +1,103 @@ +using System.Runtime.InteropServices; +using Avalonia.OpenGL; +using Avalonia.OpenGL.Egl; +using Avalonia.OpenGL.Surfaces; +using Avalonia.Win32.DirectX; + +namespace Avalonia.Win32.OpenGl.Angle; + +internal class AngleD3DTextureFeature : IGlPlatformSurfaceRenderTargetFactory +{ + public bool CanRenderToSurface(IGlContext context, object surface) => + context is EglContext + { + Display: AngleWin32EglDisplay { PlatformApi: AngleOptions.PlatformApi.DirectX11 } + } && surface is IDirect3D11TexturePlatformSurface; + + class RenderTargetWrapper : EglPlatformSurfaceRenderTargetBase + { + private readonly AngleWin32EglDisplay _angle; + private readonly IDirect3D11TextureRenderTarget _target; + + public RenderTargetWrapper(EglContext context, + AngleWin32EglDisplay angle, + IDirect3D11TextureRenderTarget target) : base(context) + { + _angle = angle; + _target = target; + } + + public override IGlPlatformSurfaceRenderingSession BeginDrawCore() + { + var success = false; + var contextLock = Context.EnsureCurrent(); + IDirect3D11TextureRenderTargetRenderSession session = null; + EglSurface surface = null; + try + { + try + { + session = _target.BeginDraw(); + } + catch (RenderTargetCorruptedException e) + { + if (e.InnerException is COMException com + && ((DXGI_ERROR)com.HResult).IsDeviceLostError()) + Context.NotifyContextLost(); + + throw; + } + + surface = _angle.WrapDirect3D11Texture(session.D3D11Texture2D, session.Offset.X, session.Offset.Y, + session.Size.Width, session.Size.Height); + var rv = BeginDraw(surface, session.Size, session.Scaling, () => + { + using(contextLock) + using (session) + using (surface) + { + } + }, true); + success = true; + return rv; + } + finally + { + if (!success) + { + using(contextLock) + using (session) + using (surface) + { + } + } + } + } + + public override void Dispose() + { + _target.Dispose(); + base.Dispose(); + } + + public override bool IsCorrupted => _target.IsCorrupted || base.IsCorrupted; + } + + public IGlPlatformSurfaceRenderTarget CreateRenderTarget(IGlContext context, object surface) + { + var ctx = (EglContext)context; + var angle = (AngleWin32EglDisplay)ctx.Display; + var textureSurface = (IDirect3D11TexturePlatformSurface)surface; + try + { + var target = textureSurface.CreateRenderTarget(context, angle.GetDirect3DDevice()); + return new RenderTargetWrapper(ctx, angle, target); + } + catch (COMException com) + { + if (((DXGI_ERROR)com.HResult).IsDeviceLostError()) + ctx.NotifyContextLost(); + throw; + } + } +} diff --git a/src/Avalonia.OpenGL/Angle/AngleEglInterface.cs b/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleEglInterface.cs similarity index 56% rename from src/Avalonia.OpenGL/Angle/AngleEglInterface.cs rename to src/Windows/Avalonia.Win32/OpenGl/Angle/AngleEglInterface.cs index 3a332f37ad..4e00bc2c72 100644 --- a/src/Avalonia.OpenGL/Angle/AngleEglInterface.cs +++ b/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleEglInterface.cs @@ -1,18 +1,31 @@ using System; using System.Runtime.InteropServices; using Avalonia.OpenGL.Egl; +using Avalonia.SourceGenerator; namespace Avalonia.OpenGL.Angle { - public class AngleEglInterface : EglInterface + internal partial class Win32AngleEglInterface : EglInterface { [DllImport("av_libGLESv2.dll", CharSet = CharSet.Ansi)] static extern IntPtr EGL_GetProcAddress(string proc); - public AngleEglInterface() : base(LoadAngle()) - { + public Win32AngleEglInterface() : this(LoadAngle()) + { + } + + private Win32AngleEglInterface(Func getProcAddress) : base(getProcAddress) + { + Initialize(getProcAddress); + } + + [GetProcAddress("eglCreateDeviceANGLE", true)] + public partial IntPtr CreateDeviceANGLE(int deviceType, IntPtr nativeDevice, int[] attribs); + + [GetProcAddress("eglReleaseDeviceANGLE", true)] + public partial void ReleaseDeviceANGLE(IntPtr device); static Func LoadAngle() { diff --git a/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleWin32EglDisplay.cs b/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleWin32EglDisplay.cs new file mode 100644 index 0000000000..b2d7b2014b --- /dev/null +++ b/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleWin32EglDisplay.cs @@ -0,0 +1,140 @@ +using System; +using System.ComponentModel; +using System.Runtime.InteropServices; +using Avalonia.OpenGL; +using Avalonia.OpenGL.Angle; +using Avalonia.OpenGL.Egl; +using Avalonia.Win32.DirectX; +using MicroCom.Runtime; +using static Avalonia.OpenGL.Egl.EglConsts; + +namespace Avalonia.Win32.OpenGl.Angle +{ + internal class AngleWin32EglDisplay : EglDisplay + { + protected override bool DisplayLockIsSharedWithContexts => true; + + public static AngleWin32EglDisplay CreateD3D9Display(EglInterface egl) + { + var display = egl.GetPlatformDisplayExt(EGL_PLATFORM_ANGLE_ANGLE, IntPtr.Zero, + new[] { EGL_PLATFORM_ANGLE_TYPE_ANGLE, EGL_PLATFORM_ANGLE_TYPE_D3D9_ANGLE, EGL_NONE }); + + return new AngleWin32EglDisplay(display, new EglDisplayOptions() + { + Egl = egl, + ContextLossIsDisplayLoss = true, + GlVersions = AvaloniaLocator.Current.GetService()?.GlProfiles + }, AngleOptions.PlatformApi.DirectX9); + } + + public static AngleWin32EglDisplay CreateSharedD3D11Display(EglInterface egl) + { + var display = egl.GetPlatformDisplayExt(EGL_PLATFORM_ANGLE_ANGLE, IntPtr.Zero, + new[] { EGL_PLATFORM_ANGLE_TYPE_ANGLE, EGL_PLATFORM_ANGLE_TYPE_D3D11_ANGLE, EGL_NONE }); + + return new AngleWin32EglDisplay(display, new EglDisplayOptions() + { + Egl = egl, + ContextLossIsDisplayLoss = true, + GlVersions = AvaloniaLocator.Current.GetService()?.GlProfiles + }, AngleOptions.PlatformApi.DirectX11); + } + + public static AngleWin32EglDisplay CreateD3D11Display(Win32AngleEglInterface egl) + { + unsafe + { + var featureLevels = new[] + { + D3D_FEATURE_LEVEL.D3D_FEATURE_LEVEL_11_1, + D3D_FEATURE_LEVEL.D3D_FEATURE_LEVEL_11_0, + D3D_FEATURE_LEVEL.D3D_FEATURE_LEVEL_10_1, + D3D_FEATURE_LEVEL.D3D_FEATURE_LEVEL_10_0, + D3D_FEATURE_LEVEL.D3D_FEATURE_LEVEL_9_3, + D3D_FEATURE_LEVEL.D3D_FEATURE_LEVEL_9_2, + D3D_FEATURE_LEVEL.D3D_FEATURE_LEVEL_9_1 + }; + + DirectXUnmanagedMethods.D3D11CreateDevice(IntPtr.Zero, D3D_DRIVER_TYPE.D3D_DRIVER_TYPE_HARDWARE, + IntPtr.Zero, 0, featureLevels, (uint)featureLevels.Length, + 7, out var pD3dDevice, out var featureLevel, null); + if (pD3dDevice == IntPtr.Zero) + throw new Win32Exception("Unable to create D3D11 Device"); + + var d3dDevice = MicroComRuntime.CreateProxyFor(pD3dDevice, true); + var angleDevice = IntPtr.Zero; + var display = IntPtr.Zero; + + void Cleanup() + { + if (angleDevice != IntPtr.Zero) + egl.ReleaseDeviceANGLE(angleDevice); + d3dDevice.Dispose(); + } + + bool success = false; + try + { + angleDevice = egl.CreateDeviceANGLE(EGL_D3D11_DEVICE_ANGLE, pD3dDevice, null); + if (angleDevice == IntPtr.Zero) + throw OpenGlException.GetFormattedException("eglCreateDeviceANGLE", egl); + + display = egl.GetPlatformDisplayExt(EGL_PLATFORM_DEVICE_EXT, angleDevice, null); + if (display == IntPtr.Zero) + throw OpenGlException.GetFormattedException("eglGetPlatformDisplayEXT", egl); + + + var rv = new AngleWin32EglDisplay(display, new EglDisplayOptions + { + DisposeCallback = Cleanup, + Egl = egl, + ContextLossIsDisplayLoss = true, + DeviceLostCheckCallback = () => d3dDevice.DeviceRemovedReason != 0, + GlVersions = AvaloniaLocator.Current.GetService()?.GlProfiles + }, AngleOptions.PlatformApi.DirectX11); + success = true; + return rv; + } + finally + { + if (!success) + { + if (display != IntPtr.Zero) + egl.Terminate(display); + Cleanup(); + } + } + } + } + + private AngleWin32EglDisplay(IntPtr display, EglDisplayOptions options, AngleOptions.PlatformApi platformApi) : base(display, options) + { + PlatformApi = platformApi; + } + + public AngleOptions.PlatformApi PlatformApi { get; } + + public IntPtr GetDirect3DDevice() + { + if (!EglInterface.QueryDisplayAttribExt(Handle, EglConsts.EGL_DEVICE_EXT, out var eglDevice)) + throw new OpenGlException("Unable to get EGL_DEVICE_EXT"); + if (!EglInterface.QueryDeviceAttribExt(eglDevice, PlatformApi == AngleOptions.PlatformApi.DirectX9 ? EGL_D3D9_DEVICE_ANGLE : EGL_D3D11_DEVICE_ANGLE, out var d3dDeviceHandle)) + throw new OpenGlException("Unable to get EGL_D3D9_DEVICE_ANGLE"); + return d3dDeviceHandle; + } + + public EglSurface WrapDirect3D11Texture( IntPtr handle) + { + if (PlatformApi != AngleOptions.PlatformApi.DirectX11) + throw new InvalidOperationException("Current platform API is " + PlatformApi); + return CreatePBufferFromClientBuffer(EGL_D3D_TEXTURE_ANGLE, handle, new[] { EGL_NONE, EGL_NONE }); + } + + public EglSurface WrapDirect3D11Texture(IntPtr handle, int offsetX, int offsetY, int width, int height) + { + if (PlatformApi != AngleOptions.PlatformApi.DirectX11) + throw new InvalidOperationException("Current platform API is " + PlatformApi); + return CreatePBufferFromClientBuffer(EGL_D3D_TEXTURE_ANGLE, handle, new[] { EGL_WIDTH, width, EGL_HEIGHT, height, EGL_FLEXIBLE_SURFACE_COMPATIBILITY_SUPPORTED_ANGLE, EGL_TRUE, EGL_TEXTURE_OFFSET_X_ANGLE, offsetX, EGL_TEXTURE_OFFSET_Y_ANGLE, offsetY, EGL_NONE }); + } + } +} diff --git a/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleWin32PlatformGraphics.cs b/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleWin32PlatformGraphics.cs new file mode 100644 index 0000000000..9b4eefd170 --- /dev/null +++ b/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleWin32PlatformGraphics.cs @@ -0,0 +1,134 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.ExceptionServices; +using Avalonia.Logging; +using Avalonia.OpenGL; +using Avalonia.OpenGL.Angle; +using Avalonia.OpenGL.Egl; +using Avalonia.Platform; + +namespace Avalonia.Win32.OpenGl.Angle; + +internal class AngleWin32PlatformGraphics : IPlatformGraphics +{ + private readonly Win32AngleEglInterface _egl; + private AngleWin32EglDisplay _sharedDisplay; + private EglContext _sharedContext; + public bool UsesSharedContext => PlatformApi == AngleOptions.PlatformApi.DirectX9; + + public AngleOptions.PlatformApi PlatformApi { get; } = AngleOptions.PlatformApi.DirectX11; + public IPlatformGraphicsContext CreateContext() + { + if (UsesSharedContext) + throw new InvalidOperationException(); + + var display = AngleWin32EglDisplay.CreateD3D11Display(_egl); + var success = false; + try + { + var rv = display.CreateContext(new EglContextOptions + { + DisposeCallback = display.Dispose, + ExtraFeatures = new Dictionary + { + [typeof(IGlPlatformSurfaceRenderTargetFactory)] = new AngleD3DTextureFeature() + } + }); + success = true; + return rv; + } + finally + { + if (!success) + display.Dispose(); + } + } + + public IPlatformGraphicsContext GetSharedContext() + { + if (!UsesSharedContext) + throw new InvalidOperationException(); + if (_sharedContext == null || _sharedContext.IsLost) + { + _sharedContext?.Dispose(); + _sharedContext = null; + _sharedContext = _sharedDisplay.CreateContext(new EglContextOptions()); + } + + return _sharedContext; + } + + public AngleWin32PlatformGraphics(Win32AngleEglInterface egl, AngleWin32EglDisplay display) + : this(egl, display.PlatformApi) + { + _sharedDisplay = display; + } + + public AngleWin32PlatformGraphics(Win32AngleEglInterface egl, AngleOptions.PlatformApi api) + { + _egl = egl; + PlatformApi = api; + } + + + public static AngleWin32PlatformGraphics TryCreate(AngleOptions options) + { + + + Win32AngleEglInterface egl; + try + { + egl = new(); + } + catch (Exception e) + { + Logger.TryGet(LogEventLevel.Error, "OpenGL") + ?.Log(null, "Unable to load ANGLE: {0}", e); + return null; + } + + return new AngleWin32PlatformGraphics(egl, AngleWin32EglDisplay.CreateSharedD3D11Display(egl)); + + foreach (var api in (options?.AllowedPlatformApis ?? new [] + { + AngleOptions.PlatformApi.DirectX11 + }).Distinct()) + if (api == AngleOptions.PlatformApi.DirectX11) + { + try + { + using var display = AngleWin32EglDisplay.CreateD3D11Display(egl); + using var ctx = display.CreateContext(new EglContextOptions()); + ctx.MakeCurrent().Dispose(); + } + catch (Exception e) + { + Logger.TryGet(LogEventLevel.Error, "OpenGL") + ?.Log(null, "Unable to initialize ANGLE-based rendering with DirectX11 : {0}", e); + continue; + } + + return new AngleWin32PlatformGraphics(egl, AngleOptions.PlatformApi.DirectX11); + } + else + { + AngleWin32EglDisplay sharedDisplay = null; + try + { + sharedDisplay = AngleWin32EglDisplay.CreateD3D9Display(egl); + using (var ctx = sharedDisplay.CreateContext(new EglContextOptions())) + ctx.MakeCurrent().Dispose(); + + return new AngleWin32PlatformGraphics(egl, sharedDisplay); + } + catch (Exception e) + { + sharedDisplay?.Dispose(); + Logger.TryGet(LogEventLevel.Error, "OpenGL") + ?.Log(null, "Unable to initialize ANGLE-based rendering with DirectX9 : {0}", e); + } + } + return null; + } +} diff --git a/src/Windows/Avalonia.Win32/OpenGl/WglContext.cs b/src/Windows/Avalonia.Win32/OpenGl/WglContext.cs index d6633ddb61..da8780d413 100644 --- a/src/Windows/Avalonia.Win32/OpenGl/WglContext.cs +++ b/src/Windows/Avalonia.Win32/OpenGl/WglContext.cs @@ -1,6 +1,9 @@ using System; +using System.Collections.Generic; +using System.Linq; using System.Reactive.Disposables; using Avalonia.OpenGL; +using Avalonia.Platform; using Avalonia.Win32.Interop; using static Avalonia.Win32.Interop.UnmanagedMethods; using static Avalonia.Win32.OpenGl.WglConsts; @@ -45,6 +48,7 @@ namespace Avalonia.Win32.OpenGl wglDeleteContext(_context); ReleaseDC(_hWnd, _dc); DestroyWindow(_hWnd); + IsLost = true; } public GlVersion Version { get; } @@ -55,11 +59,14 @@ namespace Avalonia.Win32.OpenGl private bool IsCurrent => wglGetCurrentContext() == _context && wglGetCurrentDC() == _dc; public IDisposable MakeCurrent() { + if (IsLost) + throw new PlatformGraphicsContextLostException(); if(IsCurrent) return Disposable.Empty; return new WglRestoreContext(_dc, _context, _lock); } + public bool IsLost { get; private set; } public IDisposable EnsureCurrent() => MakeCurrent(); @@ -81,5 +88,14 @@ namespace Avalonia.Win32.OpenGl || _sharedWith == context || _sharedWith != null && _sharedWith == c._sharedWith; } + + public bool CanCreateSharedContext => true; + public IGlContext CreateSharedContext(IEnumerable preferredVersions = null) + { + var versions = preferredVersions?.Append(Version).ToArray() ?? new[] { Version }; + return WglDisplay.CreateContext(versions, _sharedWith ?? this); + } + + public object TryGetFeature(Type featureType) => null; } } diff --git a/src/Windows/Avalonia.Win32/OpenGl/WglGlPlatformSurface.cs b/src/Windows/Avalonia.Win32/OpenGl/WglGlPlatformSurface.cs index 72bcffd447..a94fee4573 100644 --- a/src/Windows/Avalonia.Win32/OpenGl/WglGlPlatformSurface.cs +++ b/src/Windows/Avalonia.Win32/OpenGl/WglGlPlatformSurface.cs @@ -11,18 +11,16 @@ namespace Avalonia.Win32.OpenGl class WglGlPlatformSurface: IGlPlatformSurface { - private readonly WglContext _context; private readonly EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo _info; - public WglGlPlatformSurface(WglContext context, EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo info) + public WglGlPlatformSurface( EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo info) { - _context = context; _info = info; } - public IGlPlatformSurfaceRenderTarget CreateGlRenderTarget() + public IGlPlatformSurfaceRenderTarget CreateGlRenderTarget(IGlContext context) { - return new RenderTarget(_context, _info); + return new RenderTarget((WglContext)context, _info); } class RenderTarget : IGlPlatformSurfaceRenderTarget diff --git a/src/Windows/Avalonia.Win32/OpenGl/WglPlatformOpenGlInterface.cs b/src/Windows/Avalonia.Win32/OpenGl/WglPlatformOpenGlInterface.cs index 1d0880a468..39dd330d52 100644 --- a/src/Windows/Avalonia.Win32/OpenGl/WglPlatformOpenGlInterface.cs +++ b/src/Windows/Avalonia.Win32/OpenGl/WglPlatformOpenGlInterface.cs @@ -6,15 +6,13 @@ using Avalonia.Platform; namespace Avalonia.Win32.OpenGl { - class WglPlatformOpenGlInterface : IPlatformOpenGlInterface + class WglPlatformOpenGlInterface : IPlatformGraphics { public WglContext PrimaryContext { get; } - IPlatformGpuContext IPlatformGpu.PrimaryContext => PrimaryContext; - IGlContext IPlatformOpenGlInterface.PrimaryContext => PrimaryContext; - public IGlContext CreateSharedContext() => WglDisplay.CreateContext(new[] { PrimaryContext.Version }, PrimaryContext); - - public bool CanShareContexts => true; - public bool CanCreateContexts => true; + public bool UsesSharedContext => false; + IPlatformGraphicsContext IPlatformGraphics.CreateContext() => CreateContext(); + public IPlatformGraphicsContext GetSharedContext() => throw new NotSupportedException(); + public IGlContext CreateContext() => WglDisplay.CreateContext(new[] { PrimaryContext.Version }, null); private WglPlatformOpenGlInterface(WglContext primary) diff --git a/src/Windows/Avalonia.Win32/Win32GlManager.cs b/src/Windows/Avalonia.Win32/Win32GlManager.cs index 204acc82c9..25ea060576 100644 --- a/src/Windows/Avalonia.Win32/Win32GlManager.cs +++ b/src/Windows/Avalonia.Win32/Win32GlManager.cs @@ -2,8 +2,9 @@ using Avalonia.OpenGL; using Avalonia.OpenGL.Angle; using Avalonia.OpenGL.Egl; using Avalonia.Platform; -using Avalonia.Win32.DxgiSwapchain; +using Avalonia.Win32.DirectX; using Avalonia.Win32.OpenGl; +using Avalonia.Win32.OpenGl.Angle; using Avalonia.Win32.WinRT.Composition; namespace Avalonia.Win32 @@ -11,15 +12,15 @@ namespace Avalonia.Win32 static class Win32GlManager { - public static IPlatformOpenGlInterface Initialize() + public static IPlatformGraphics Initialize() { var gl = InitializeCore(); - AvaloniaLocator.CurrentMutable.Bind().ToConstant(gl); - AvaloniaLocator.CurrentMutable.Bind().ToConstant(gl); + AvaloniaLocator.CurrentMutable.Bind().ToConstant(gl); + AvaloniaLocator.CurrentMutable.Bind().ToConstant(gl); return gl; } - static IPlatformOpenGlInterface InitializeCore() + static IPlatformGraphics InitializeCore() { var opts = AvaloniaLocator.Current.GetService() ?? new Win32PlatformOptions(); @@ -31,28 +32,18 @@ namespace Avalonia.Win32 if (opts.AllowEglInitialization ?? Win32Platform.WindowsVersion > PlatformConstants.Windows7) { - var egl = EglPlatformOpenGlInterface.TryCreate(() => new AngleWin32EglDisplay()); + var egl = AngleWin32PlatformGraphics.TryCreate(AvaloniaLocator.Current.GetService() ?? + new()); - if (egl != null) + if (egl != null && egl.PlatformApi == AngleOptions.PlatformApi.DirectX11) { - if (opts.EglRendererBlacklist != null) - { - foreach (var item in opts.EglRendererBlacklist) - { - if (egl.PrimaryEglContext.GlInterface.Renderer.Contains(item)) - { - return null; - } - } - } - if (opts.UseWindowsUIComposition) { - WinUICompositorConnection.TryCreateAndRegister(egl, opts.CompositionBackdropCornerRadius); + WinUiCompositorConnection.TryCreateAndRegister(); } else if (opts.UseLowLatencyDxgiSwapChain) { - DxgiConnection.TryCreateAndRegister(egl); + DxgiConnection.TryCreateAndRegister(); } } diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index 63cad9679a..3f220f0f09 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -19,6 +19,7 @@ using Avalonia.Threading; using Avalonia.Utilities; using Avalonia.Win32.Input; using Avalonia.Win32.Interop; +using JetBrains.Annotations; using static Avalonia.Win32.Interop.UnmanagedMethods; namespace Avalonia @@ -59,11 +60,6 @@ namespace Avalonia /// GPU rendering will not be enabled if this is set to false. /// public bool? AllowEglInitialization { get; set; } - - public IList EglRendererBlacklist { get; set; } = new List - { - "Microsoft Basic Render" - }; /// /// Embeds popups to the window when set to true. The default value is false. @@ -106,6 +102,11 @@ namespace Avalonia /// which if active will override this setting. /// public bool UseLowLatencyDxgiSwapChain { get; set; } = false; + + /// + /// Provides a way to use a custom-implemented graphics context such as a custom ISkiaGpu + /// + [CanBeNull] public IPlatformGraphics CustomPlatformGraphics { get; set; } } } @@ -139,6 +140,7 @@ namespace Avalonia.Win32 public static Win32PlatformOptions Options { get; private set; } internal static Compositor Compositor { get; private set; } + internal static PlatformRenderInterfaceContextManager RenderInterface { get; private set; } public static void Initialize() { @@ -169,16 +171,19 @@ namespace Avalonia.Win32 .Bind().ToConstant(new NonPumpingSyncContext.HelperImpl()) .Bind().ToConstant(new WindowsMountedVolumeInfoProvider()) .Bind().ToConstant(s_instance); - - var gl = Win32GlManager.Initialize(); - + _uiThread = Thread.CurrentThread; + var platformGraphics = options?.CustomPlatformGraphics + ?? Win32GlManager.Initialize(); + if (OleContext.Current != null) AvaloniaLocator.CurrentMutable.Bind().ToSingleton(); if (Options.UseCompositor) - Compositor = new Compositor(AvaloniaLocator.Current.GetRequiredService(), gl); + Compositor = new Compositor(AvaloniaLocator.Current.GetRequiredService(), platformGraphics); + else + RenderInterface = new PlatformRenderInterfaceContextManager(platformGraphics); } public bool HasMessages() diff --git a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositedWindow.cs b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositedWindow.cs deleted file mode 100644 index ef3de9fbe1..0000000000 --- a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositedWindow.cs +++ /dev/null @@ -1,109 +0,0 @@ -using System; -using System.Numerics; -using System.Reactive.Disposables; -using System.Threading; -using Avalonia.MicroCom; -using Avalonia.OpenGL; -using Avalonia.OpenGL.Egl; -using Avalonia.Win32.Interop; -using MicroCom.Runtime; - -namespace Avalonia.Win32.WinRT.Composition -{ - public class WinUICompositedWindow : IDisposable - { - private EglContext _syncContext; - private readonly object _pumpLock; - private readonly IVisual _micaVisual; - private readonly ICompositionRoundedRectangleGeometry _roundedRectangleGeometry; - private readonly IVisual _blurVisual; - private ICompositionTarget _compositionTarget; - private IVisual _contentVisual; - private ICompositionDrawingSurfaceInterop _surfaceInterop; - private PixelSize _size; - - private static Guid IID_ID3D11Texture2D = Guid.Parse("6f15aaf2-d208-4e89-9ab4-489535d34f9c"); - private ICompositor _compositor; - - - internal WinUICompositedWindow(EglContext syncContext, - ICompositor compositor, - object pumpLock, - ICompositionTarget compositionTarget, - ICompositionDrawingSurfaceInterop surfaceInterop, - IVisual contentVisual, IVisual blurVisual, IVisual micaVisual, - ICompositionRoundedRectangleGeometry roundedRectangleGeometry) - { - _compositor = compositor.CloneReference(); - _syncContext = syncContext; - _pumpLock = pumpLock; - _micaVisual = micaVisual; - _roundedRectangleGeometry = roundedRectangleGeometry; - _blurVisual = blurVisual.CloneReference(); - _compositionTarget = compositionTarget.CloneReference(); - _contentVisual = contentVisual.CloneReference(); - _surfaceInterop = surfaceInterop.CloneReference(); - } - - - public void ResizeIfNeeded(PixelSize size) - { - using (_syncContext.EnsureLocked()) - { - if (_size != size) - { - _surfaceInterop.Resize(new UnmanagedMethods.POINT { X = size.Width, Y = size.Height }); - _contentVisual.SetSize(new Vector2(size.Width, size.Height)); - _roundedRectangleGeometry?.SetSize(new Vector2(size.Width, size.Height)); - _size = size; - } - } - } - - public unsafe IUnknown BeginDrawToTexture(out PixelPoint offset) - { - if (!_syncContext.IsCurrent) - throw new InvalidOperationException(); - - var iid = IID_ID3D11Texture2D; - void* pTexture; - var off = _surfaceInterop.BeginDraw(null, &iid, &pTexture); - offset = new PixelPoint(off.X, off.Y); - return MicroComRuntime.CreateProxyFor(pTexture, true); - } - - public void EndDraw() - { - if (!_syncContext.IsCurrent) - throw new InvalidOperationException(); - _surfaceInterop.EndDraw(); - } - - public void SetBlur(BlurEffect blurEffect) - { - using (_syncContext.EnsureLocked()) - { - _blurVisual.SetIsVisible(blurEffect == BlurEffect.Acrylic ? 1 : 0); - _micaVisual?.SetIsVisible(blurEffect == BlurEffect.Mica ? 1 : 0); - } - } - - public IDisposable BeginTransaction() - { - Monitor.Enter(_pumpLock); - return Disposable.Create(() => Monitor.Exit(_pumpLock)); - } - - public void Dispose() - { - if (_syncContext == null) - { - _compositor.Dispose(); - _blurVisual.Dispose(); - _contentVisual.Dispose(); - _surfaceInterop.Dispose(); - _compositionTarget.Dispose(); - } - } - } -} diff --git a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs deleted file mode 100644 index 6da17b8ea5..0000000000 --- a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs +++ /dev/null @@ -1,311 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Numerics; -using System.Runtime.InteropServices; -using System.Threading; -using System.Threading.Tasks; -using Avalonia.Logging; -using Avalonia.MicroCom; -using Avalonia.OpenGL; -using Avalonia.OpenGL.Angle; -using Avalonia.OpenGL.Egl; -using Avalonia.Rendering; -using Avalonia.Win32.Interop; -using MicroCom.Runtime; - -namespace Avalonia.Win32.WinRT.Composition -{ - class WinUICompositorConnection : IRenderTimer - { - public static readonly Version MinHostBackdropVersion = new Version(10, 0, 22000); - private readonly float? _backdropCornerRadius; - private readonly EglContext _syncContext; - private readonly ICompositionBrush _micaBrush; - private ICompositor _compositor; - private ICompositor5 _compositor5; - private ICompositorInterop _compositorInterop; - private AngleWin32EglDisplay _angle; - private ICompositionGraphicsDevice _device; - private EglPlatformOpenGlInterface _gl; - private ICompositorDesktopInterop _compositorDesktopInterop; - private ICompositionBrush _blurBrush; - private object _pumpLock = new object(); - - public WinUICompositorConnection(EglPlatformOpenGlInterface gl, object pumpLock, float? backdropCornerRadius) - { - _gl = gl; - _pumpLock = pumpLock; - _backdropCornerRadius = backdropCornerRadius; - _syncContext = _gl.PrimaryEglContext; - _angle = (AngleWin32EglDisplay)_gl.Display; - _compositor = NativeWinRTMethods.CreateInstance("Windows.UI.Composition.Compositor"); - _compositor5 = _compositor.QueryInterface(); - _compositorInterop = _compositor.QueryInterface(); - _compositorDesktopInterop = _compositor.QueryInterface(); - using var device = MicroComRuntime.CreateProxyFor(_angle.GetDirect3DDevice(), true); - - _device = _compositorInterop.CreateGraphicsDevice(device); - _blurBrush = CreateAcrylicBlurBackdropBrush(); - _micaBrush = CreateMicaBackdropBrush(); - } - - public EglPlatformOpenGlInterface Egl => _gl; - - static bool TryCreateAndRegisterCore(EglPlatformOpenGlInterface angle, float? backdropCornerRadius) - { - var tcs = new TaskCompletionSource(); - var pumpLock = new object(); - var th = new Thread(() => - { - WinUICompositorConnection connect; - try - { - NativeWinRTMethods.CreateDispatcherQueueController(new NativeWinRTMethods.DispatcherQueueOptions - { - apartmentType = NativeWinRTMethods.DISPATCHERQUEUE_THREAD_APARTMENTTYPE.DQTAT_COM_NONE, - dwSize = Marshal.SizeOf(), - threadType = NativeWinRTMethods.DISPATCHERQUEUE_THREAD_TYPE.DQTYPE_THREAD_CURRENT - }); - connect = new WinUICompositorConnection(angle, pumpLock, backdropCornerRadius); - AvaloniaLocator.CurrentMutable.BindToSelf(connect); - AvaloniaLocator.CurrentMutable.Bind().ToConstant(connect); - tcs.SetResult(true); - - } - catch (Exception e) - { - tcs.SetException(e); - return; - } - connect.RunLoop(); - }) - { - IsBackground = true - }; - th.SetApartmentState(ApartmentState.STA); - th.Start(); - return tcs.Task.Result; - } - - class RunLoopHandler : IAsyncActionCompletedHandler, IMicroComShadowContainer - { - private readonly WinUICompositorConnection _parent; - private Stopwatch _st = Stopwatch.StartNew(); - - public RunLoopHandler(WinUICompositorConnection parent) - { - _parent = parent; - } - public void Dispose() - { - - } - - public void Invoke(IAsyncAction asyncInfo, AsyncStatus asyncStatus) - { - _parent.Tick?.Invoke(_st.Elapsed); - using var act = _parent._compositor5.RequestCommitAsync(); - act.SetCompleted(this); - } - - public MicroComShadow Shadow { get; set; } - public void OnReferencedFromNative() - { - } - - public void OnUnreferencedFromNative() - { - } - } - - private void RunLoop() - { - var cts = new CancellationTokenSource(); - AppDomain.CurrentDomain.ProcessExit += (sender, args) => - cts.Cancel(); - - using (var act = _compositor5.RequestCommitAsync()) - act.SetCompleted(new RunLoopHandler(this)); - - while (!cts.IsCancellationRequested) - { - UnmanagedMethods.GetMessage(out var msg, IntPtr.Zero, 0, 0); - lock (_pumpLock) - UnmanagedMethods.DispatchMessage(ref msg); - } - } - - public static void TryCreateAndRegister(EglPlatformOpenGlInterface angle, - float? backdropCornerRadius) - { - const int majorRequired = 10; - const int buildRequired = 17134; - - var majorInstalled = Win32Platform.WindowsVersion.Major; - var buildInstalled = Win32Platform.WindowsVersion.Build; - - if (majorInstalled >= majorRequired && - buildInstalled >= buildRequired) - { - try - { - TryCreateAndRegisterCore(angle, backdropCornerRadius); - return; - } - catch (Exception e) - { - Logger.TryGet(LogEventLevel.Error, "WinUIComposition") - ?.Log(null, "Unable to initialize WinUI compositor: {0}", e); - - } - } - - var osVersionNotice = - $"Windows {majorRequired} Build {buildRequired} is required. Your machine has Windows {majorInstalled} Build {buildInstalled} installed."; - - Logger.TryGet(LogEventLevel.Warning, "WinUIComposition")?.Log(null, - $"Unable to initialize WinUI compositor: {osVersionNotice}"); - } - - - public WinUICompositedWindow CreateWindow(IntPtr hWnd) - { - using var sc = _syncContext.EnsureLocked(); - using var desktopTarget = _compositorDesktopInterop.CreateDesktopWindowTarget(hWnd, 0); - using var target = desktopTarget.QueryInterface(); - using var device2 = _device.QueryInterface(); - - using var drawingSurface = device2.CreateDrawingSurface2(new UnmanagedMethods.SIZE(), DirectXPixelFormat.B8G8R8A8UIntNormalized, - DirectXAlphaMode.Premultiplied); - using var surface = drawingSurface.QueryInterface(); - using var surfaceInterop = drawingSurface.QueryInterface(); - - using var surfaceBrush = _compositor.CreateSurfaceBrushWithSurface(surface); - using var brush = surfaceBrush.QueryInterface(); - - using var spriteVisual = _compositor.CreateSpriteVisual(); - spriteVisual.SetBrush(brush); - using var visual = spriteVisual.QueryInterface(); - using var visual2 = spriteVisual.QueryInterface(); - using var container = _compositor.CreateContainerVisual(); - using var containerVisual = container.QueryInterface(); - using var containerVisual2 = container.QueryInterface(); - containerVisual2.SetRelativeSizeAdjustment(new Vector2(1, 1)); - using var containerChildren = container.Children; - - target.SetRoot(containerVisual); - - using var blur = CreateBlurVisual(_blurBrush); - IVisual mica = null; - if (_micaBrush != null) - { - mica = CreateBlurVisual(_micaBrush); - containerChildren.InsertAtTop(mica); - } - - var compositionRoundedRectangleGeometry = ClipVisual(blur, mica); - - containerChildren.InsertAtTop(blur); - containerChildren.InsertAtTop(visual); - - return new WinUICompositedWindow(_syncContext, _compositor, _pumpLock, target, surfaceInterop, visual, - blur, mica, compositionRoundedRectangleGeometry); - } - - private ICompositionBrush CreateMicaBackdropBrush() - { - if (Win32Platform.WindowsVersion.Build < 22000) - return null; - - using var compositorWithBlurredWallpaperBackdropBrush = - _compositor.QueryInterface(); - using var blurredWallpaperBackdropBrush = - compositorWithBlurredWallpaperBackdropBrush?.TryCreateBlurredWallpaperBackdropBrush(); - using var micaBackdropBrush = blurredWallpaperBackdropBrush?.QueryInterface(); - return micaBackdropBrush.CloneReference(); - } - - private unsafe ICompositionBrush CreateAcrylicBlurBackdropBrush() - { - using var backDropParameterFactory = NativeWinRTMethods.CreateActivationFactory( - "Windows.UI.Composition.CompositionEffectSourceParameter"); - using var backdropString = new HStringInterop("backdrop"); - using var backDropParameter = - backDropParameterFactory.Create(backdropString.Handle); - using var backDropParameterAsSource = backDropParameter.QueryInterface(); - var blurEffect = new WinUIGaussianBlurEffect(backDropParameterAsSource); - using var blurEffectFactory = _compositor.CreateEffectFactory(blurEffect); - using var compositionEffectBrush = blurEffectFactory.CreateBrush(); - using var backdropBrush = CreateBackdropBrush(); - - var saturateEffect = new SaturationEffect(blurEffect); - using var satEffectFactory = _compositor.CreateEffectFactory(saturateEffect); - using var sat = satEffectFactory.CreateBrush(); - compositionEffectBrush.SetSourceParameter(backdropString.Handle, backdropBrush); - return compositionEffectBrush.QueryInterface(); - } - - private ICompositionRoundedRectangleGeometry ClipVisual(params IVisual[] containerVisuals) - { - if (!_backdropCornerRadius.HasValue) - return null; - using var roundedRectangleGeometry = _compositor5.CreateRoundedRectangleGeometry(); - roundedRectangleGeometry.SetCornerRadius(new Vector2(_backdropCornerRadius.Value, _backdropCornerRadius.Value)); - - using var compositor6 = _compositor.QueryInterface(); - using var compositionGeometry = roundedRectangleGeometry - .QueryInterface(); - - using var geometricClipWithGeometry = - compositor6.CreateGeometricClipWithGeometry(compositionGeometry); - foreach (var visual in containerVisuals) - { - visual?.SetClip(geometricClipWithGeometry.QueryInterface()); - } - - return roundedRectangleGeometry.CloneReference(); - } - - private unsafe IVisual CreateBlurVisual(ICompositionBrush compositionBrush) - { - using var spriteVisual = _compositor.CreateSpriteVisual(); - using var visual = spriteVisual.QueryInterface(); - using var visual2 = spriteVisual.QueryInterface(); - - - spriteVisual.SetBrush(compositionBrush); - visual.SetIsVisible(0); - visual2.SetRelativeSizeAdjustment(new Vector2(1.0f, 1.0f)); - - return visual.CloneReference(); - } - - private ICompositionBrush CreateBackdropBrush() - { - ICompositionBackdropBrush brush = null; - try - { - if (Win32Platform.WindowsVersion >= MinHostBackdropVersion) - { - using var compositor3 = _compositor.QueryInterface(); - brush = compositor3.CreateHostBackdropBrush(); - } - else - { - using var compositor2 = _compositor.QueryInterface(); - brush = compositor2.CreateBackdropBrush(); - } - - return brush.QueryInterface(); - } - finally - { - brush?.Dispose(); - } - } - - public event Action Tick; - public bool RunsInBackground => true; - } -} diff --git a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositedWindow.cs b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositedWindow.cs new file mode 100644 index 0000000000..32019f4c15 --- /dev/null +++ b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositedWindow.cs @@ -0,0 +1,110 @@ +using System; +using System.Numerics; +using System.Reactive.Disposables; +using System.Threading; +using Avalonia.OpenGL.Egl; +using Avalonia.Win32.Interop; +using MicroCom.Runtime; + +namespace Avalonia.Win32.WinRT.Composition; + +internal class WinUiCompositedWindow : IDisposable +{ + public EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo WindowInfo { get; } + private readonly WinUiCompositionShared _shared; + private readonly ICompositionRoundedRectangleGeometry _compositionRoundedRectangleGeometry; + private readonly IVisual _mica; + private readonly IVisual _blur; + private readonly IVisual _visual; + private PixelSize _size; + private readonly ICompositionSurfaceBrush _surfaceBrush; + private readonly ICompositionTarget _target; + + public void Dispose() + { + lock (_shared.SyncRoot) + { + _compositionRoundedRectangleGeometry?.Dispose(); + _blur?.Dispose(); + _mica?.Dispose(); + _visual.Dispose(); + _surfaceBrush.Dispose(); + _target.Dispose(); + } + } + + public WinUiCompositedWindow(EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo info, + WinUiCompositionShared shared, float? backdropCornerRadius) + { + WindowInfo = info; + _shared = shared; + using var desktopTarget = shared.DesktopInterop.CreateDesktopWindowTarget(WindowInfo.Handle, 0); + _target = desktopTarget.QueryInterface(); + + + using var container = shared.Compositor.CreateContainerVisual(); + using var containerVisual = container.QueryInterface(); + using var containerVisual2 = container.QueryInterface(); + containerVisual2.SetRelativeSizeAdjustment(new Vector2(1, 1)); + using var containerChildren = container.Children; + + _target.SetRoot(containerVisual); + + _blur = WinUiCompositionUtils.CreateBlurVisual(shared.Compositor, shared.BlurBrush); + if (shared.MicaBrush != null) + { + _mica = WinUiCompositionUtils.CreateBlurVisual(shared.Compositor, shared.MicaBrush); + containerChildren.InsertAtTop(_mica); + } + + _compositionRoundedRectangleGeometry = + WinUiCompositionUtils.ClipVisual(shared.Compositor, backdropCornerRadius, _blur, _mica); + + containerChildren.InsertAtTop(_blur); + using var spriteVisual = shared.Compositor.CreateSpriteVisual(); + _visual = spriteVisual.QueryInterface(); + containerChildren.InsertAtTop(_visual); + + _surfaceBrush = shared.Compositor.CreateSurfaceBrush(); + using var compositionBrush = _surfaceBrush.QueryInterface(); + spriteVisual.SetBrush(compositionBrush); + _target.SetRoot(containerVisual); + + + + } + + public void SetSurface(ICompositionSurface surface) => _surfaceBrush.SetSurface(surface); + + public void SetBlur(BlurEffect blurEffect) + { + lock (_shared.SyncRoot) + { + + _blur.SetIsVisible(blurEffect == BlurEffect.Acrylic + || blurEffect == BlurEffect.Mica && _mica == null ? + 1 : + 0); + _mica?.SetIsVisible(blurEffect == BlurEffect.Mica ? 1 : 0); + } + } + + public IDisposable BeginTransaction() + { + Monitor.Enter(_shared.SyncRoot); + return Disposable.Create(() => Monitor.Exit(_shared.SyncRoot)); + } + + public void ResizeIfNeeded(PixelSize size) + { + lock (_shared.SyncRoot) + { + if (_size != size) + { + _visual.SetSize(new Vector2(size.Width, size.Height)); + _compositionRoundedRectangleGeometry?.SetSize(new Vector2(size.Width, size.Height)); + _size = size; + } + } + } +} diff --git a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositedWindowSurface.cs b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositedWindowSurface.cs index fc04dcda26..c4918be70c 100644 --- a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositedWindowSurface.cs +++ b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositedWindowSurface.cs @@ -1,33 +1,226 @@ using System; using System.Runtime.InteropServices; using Avalonia.MicroCom; +using Avalonia.OpenGL; using Avalonia.OpenGL.Angle; using Avalonia.OpenGL.Egl; using Avalonia.OpenGL.Surfaces; +using Avalonia.Platform; using Avalonia.Utilities; +using Avalonia.Win32.DirectX; using Avalonia.Win32.Interop; +using Avalonia.Win32.OpenGl.Angle; using MicroCom.Runtime; namespace Avalonia.Win32.WinRT.Composition { + internal class WinUiCompositedWindowSurface : IDirect3D11TexturePlatformSurface, IDisposable, IBlurHost + { + private readonly WinUiCompositionShared _shared; + private readonly EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo _info; + private WinUiCompositedWindow _window; + private BlurEffect _blurEffect; + + public WinUiCompositedWindowSurface(WinUiCompositionShared shared, EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo info) + { + _shared = shared; + _info = info; + } + + public IDirect3D11TextureRenderTarget CreateRenderTarget(IPlatformGraphicsContext context, IntPtr d3dDevice) + { + var cornerRadius = AvaloniaLocator.Current.GetService() + ?.CompositionBackdropCornerRadius; + _window ??= new WinUiCompositedWindow(_info, _shared, cornerRadius); + _window.SetBlur(_blurEffect); + + return new WinUiCompositedWindowRenderTarget(context, _window, d3dDevice, _shared.Compositor); + } + + public void Dispose() + { + _window?.Dispose(); + _window = null; + } + + public void SetBlur(BlurEffect enable) + { + _blurEffect = enable; + _window?.SetBlur(enable); + } + } + + internal class WinUiCompositedWindowRenderTarget : IDirect3D11TextureRenderTarget + { + private readonly IPlatformGraphicsContext _context; + private readonly WinUiCompositedWindow _window; + private readonly IUnknown _d3dDevice; + private readonly ICompositor _compositor; + private readonly ICompositorInterop _interop; + private readonly ICompositionGraphicsDevice _compositionDevice; + private readonly ICompositionGraphicsDevice2 _compositionDevice2; + private readonly ICompositionSurface _surface; + private PixelSize _size; + private bool _lost; + private readonly ICompositionDrawingSurfaceInterop _surfaceInterop; + + public WinUiCompositedWindowRenderTarget(IPlatformGraphicsContext context, + WinUiCompositedWindow window, IntPtr device, + ICompositor compositor) + { + _context = context; + _window = window; + + try + { + _d3dDevice = MicroComRuntime.CreateProxyFor(device, false).CloneReference(); + _compositor = compositor.CloneReference(); + _interop = compositor.QueryInterface(); + _compositionDevice = _interop.CreateGraphicsDevice(_d3dDevice); + _compositionDevice2 = _compositionDevice.QueryInterface(); + _drawingSurface = _compositionDevice2.CreateDrawingSurface2(new UnmanagedMethods.SIZE(), + DirectXPixelFormat.B8G8R8A8UIntNormalized, DirectXAlphaMode.Premultiplied); + _surface = _drawingSurface.QueryInterface(); + _surfaceInterop = _drawingSurface.QueryInterface(); + } + finally + { + if (_surfaceInterop == null) + Dispose(); + } + } + + public void Dispose() + { + _surface?.Dispose(); + _surfaceInterop?.Dispose(); + _drawingSurface?.Dispose(); + _compositionDevice2?.Dispose(); + _compositionDevice?.Dispose(); + _interop?.Dispose(); + _compositor?.Dispose(); + _d3dDevice?.Dispose(); + } + + public bool IsCorrupted => _context.IsLost || _lost; + private static Guid IID_ID3D11Texture2D = Guid.Parse("6f15aaf2-d208-4e89-9ab4-489535d34f9c"); + private readonly ICompositionDrawingSurface _drawingSurface; + + public unsafe IDirect3D11TextureRenderTargetRenderSession BeginDraw() + { + if (IsCorrupted) + throw new RenderTargetCorruptedException(); + var transaction = _window.BeginTransaction(); + bool needsEndDraw = false; + try + { + var size = _window.WindowInfo.Size; + var scale = _window.WindowInfo.Scaling; + _window.ResizeIfNeeded(size); + _window.SetSurface(_surface); + + void* pTexture; + UnmanagedMethods.POINT off; + try + { + if (_size != size) + { + _surfaceInterop.Resize(new UnmanagedMethods.POINT + { + X = size.Width, + Y = size.Height + }); + _size = size; + } + var iid = IID_ID3D11Texture2D; + off = _surfaceInterop.BeginDraw(null, &iid, &pTexture); + } + catch (Exception e) + { + _lost = true; + throw new RenderTargetCorruptedException(e); + } + + needsEndDraw = true; + var offset = new PixelPoint(off.X, off.Y); + using var texture = MicroComRuntime.CreateProxyFor(pTexture, true); + + var session = new Session(_surfaceInterop, texture, transaction, _size, offset, scale); + transaction = null; + return session; + } + finally + { + if (transaction != null) + { + if (needsEndDraw) + _surfaceInterop.EndDraw(); + transaction?.Dispose(); + } + } + } + + class Session : IDirect3D11TextureRenderTargetRenderSession + { + private readonly IDisposable _transaction; + private readonly PixelSize _size; + private readonly PixelPoint _offset; + private readonly double _scaling; + private readonly ICompositionDrawingSurfaceInterop _surfaceInterop; + private readonly IUnknown _texture; + + public Session(ICompositionDrawingSurfaceInterop surfaceInterop, IUnknown texture, IDisposable transaction, + PixelSize size, PixelPoint offset, double scaling) + { + _transaction = transaction; + _size = size; + _offset = offset; + _scaling = scaling; + _surfaceInterop = surfaceInterop.CloneReference(); + _texture = texture.CloneReference(); + } + + public void Dispose() + { + try + { + _texture.Dispose(); + _surfaceInterop.EndDraw(); + _surfaceInterop.Dispose(); + } + finally + { + _transaction.Dispose(); + } + } + + public IntPtr D3D11Texture2D => _texture.GetNativeIntPtr(); + public PixelSize Size => _size; + public PixelPoint Offset => _offset; + public double Scaling => _scaling; + } + } +} +/* internal class WinUiCompositedWindowSurface : EglGlPlatformSurfaceBase, IBlurHost, IDisposable { private readonly WinUICompositorConnection _connection; - private EglPlatformOpenGlInterface _egl; - private readonly EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo _info; + private EglPlatformGraphics _egl; + private readonly EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo _info; private IRef _window; private BlurEffect _blurEffect; - public WinUiCompositedWindowSurface(WinUICompositorConnection connection, IEglWindowGlPlatformSurfaceInfo info) : base() + public WinUiCompositedWindowSurface(WinUICompositorConnection connection, EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo info) : base() { _connection = connection; _egl = connection.Egl; _info = info; } - public override IGlPlatformSurfaceRenderTarget CreateGlRenderTarget() + public override IGlPlatformSurfaceRenderTarget CreateGlRenderTarget(IGlContext context) { - using (_egl.PrimaryContext.EnsureCurrent()) + var egl = (EglContext)context; + using (egl.EnsureCurrent()) { if (_window?.Item == null) { @@ -35,30 +228,28 @@ namespace Avalonia.Win32.WinRT.Composition _window.Item.SetBlur(_blurEffect); } - return new CompositionRenderTarget(_egl, _window, _info); + return new CompositionRenderTarget(egl, _window, _info); } } class CompositionRenderTarget : EglPlatformSurfaceRenderTargetBase { - private readonly EglPlatformOpenGlInterface _egl; private readonly IRef _window; - private readonly IEglWindowGlPlatformSurfaceInfo _info; + private readonly EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo _info; - public CompositionRenderTarget(EglPlatformOpenGlInterface egl, + public CompositionRenderTarget(EglContext context, IRef window, - IEglWindowGlPlatformSurfaceInfo info) - : base(egl) + EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo info) + : base(context) { - _egl = egl; _window = window.Clone(); _info = info; _window.Item.ResizeIfNeeded(_info.Size); } - public override IGlPlatformSurfaceRenderingSession BeginDraw() + public override IGlPlatformSurfaceRenderingSession BeginDrawCore() { - var contextLock = _egl.PrimaryEglContext.EnsureCurrent(); + var contextLock = Context.EnsureCurrent(); IUnknown texture = null; EglSurface surface = null; IDisposable transaction = null; @@ -73,8 +264,7 @@ namespace Avalonia.Win32.WinRT.Composition _window.Item.ResizeIfNeeded(size); texture = _window.Item.BeginDrawToTexture(out var offset); - surface = ((AngleWin32EglDisplay) _egl.Display).WrapDirect3D11Texture(_egl, - texture.GetNativeIntPtr(), + surface = ((AngleWin32EglDisplay) Context.Display).WrapDirect3D11Texture(texture.GetNativeIntPtr(), offset.X, offset.Y, size.Width, size.Height); var res = base.BeginDraw(surface, _info, () => @@ -109,7 +299,7 @@ namespace Avalonia.Win32.WinRT.Composition public void Dispose() { - using (_egl.PrimaryEglContext.EnsureLocked()) + using (_egl.Display.Lock()) { _window?.Dispose(); _window = null; @@ -117,3 +307,4 @@ namespace Avalonia.Win32.WinRT.Composition } } } +*/ diff --git a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositionShared.cs b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositionShared.cs new file mode 100644 index 0000000000..301e605e7a --- /dev/null +++ b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositionShared.cs @@ -0,0 +1,34 @@ +using System; +using MicroCom.Runtime; + +namespace Avalonia.Win32.WinRT.Composition; + +internal class WinUiCompositionShared : IDisposable +{ + public ICompositor Compositor { get; } + public ICompositor5 Compositor5 { get; } + public ICompositorDesktopInterop DesktopInterop { get; } + public ICompositionBrush BlurBrush; + public ICompositionBrush MicaBrush; + public object SyncRoot { get; } = new(); + + public static readonly Version MinHostBackdropVersion = new Version(10, 0, 22000); + + public WinUiCompositionShared(ICompositor compositor) + { + Compositor = compositor.CloneReference(); + Compositor5 = compositor.QueryInterface(); + BlurBrush = WinUiCompositionUtils.CreateAcrylicBlurBackdropBrush(compositor); + MicaBrush = WinUiCompositionUtils.CreateMicaBackdropBrush(compositor); + DesktopInterop = compositor.QueryInterface(); + } + + public void Dispose() + { + BlurBrush.Dispose(); + MicaBrush.Dispose(); + DesktopInterop.Dispose(); + Compositor.Dispose(); + Compositor5.Dispose(); + } +} \ No newline at end of file diff --git a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositionUtils.cs b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositionUtils.cs new file mode 100644 index 0000000000..e7af4046d6 --- /dev/null +++ b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositionUtils.cs @@ -0,0 +1,101 @@ +using System; +using System.Numerics; +using MicroCom.Runtime; + +namespace Avalonia.Win32.WinRT.Composition; + +internal static class WinUiCompositionUtils +{ + public static ICompositionBrush CreateMicaBackdropBrush(ICompositor compositor) + { + if (Win32Platform.WindowsVersion.Build < 22000) + return null; + + using var compositorWithBlurredWallpaperBackdropBrush = + compositor.QueryInterface(); + using var blurredWallpaperBackdropBrush = + compositorWithBlurredWallpaperBackdropBrush?.TryCreateBlurredWallpaperBackdropBrush(); + return blurredWallpaperBackdropBrush?.QueryInterface(); + } + + public static unsafe ICompositionBrush CreateAcrylicBlurBackdropBrush(ICompositor compositor) + { + using var backDropParameterFactory = + NativeWinRTMethods.CreateActivationFactory( + "Windows.UI.Composition.CompositionEffectSourceParameter"); + using var backdropString = new HStringInterop("backdrop"); + using var backDropParameter = + backDropParameterFactory.Create(backdropString.Handle); + using var backDropParameterAsSource = backDropParameter.QueryInterface(); + var blurEffect = new WinUIGaussianBlurEffect(backDropParameterAsSource); + using var blurEffectFactory = compositor.CreateEffectFactory(blurEffect); + using var compositionEffectBrush = blurEffectFactory.CreateBrush(); + using var backdropBrush = CreateBackdropBrush(compositor); + + var saturateEffect = new SaturationEffect(blurEffect); + using var satEffectFactory = compositor.CreateEffectFactory(saturateEffect); + using var sat = satEffectFactory.CreateBrush(); + compositionEffectBrush.SetSourceParameter(backdropString.Handle, backdropBrush); + return compositionEffectBrush.QueryInterface(); + } + + public static ICompositionRoundedRectangleGeometry ClipVisual(ICompositor compositor, float? _backdropCornerRadius, params IVisual[] containerVisuals) + { + if (!_backdropCornerRadius.HasValue) + return null; + using var compositor5 = compositor.QueryInterface(); + using var roundedRectangleGeometry = compositor5.CreateRoundedRectangleGeometry(); + roundedRectangleGeometry.SetCornerRadius(new Vector2(_backdropCornerRadius.Value, _backdropCornerRadius.Value)); + + using var compositor6 = compositor.QueryInterface(); + using var compositionGeometry = roundedRectangleGeometry + .QueryInterface(); + + using var geometricClipWithGeometry = + compositor6.CreateGeometricClipWithGeometry(compositionGeometry); + foreach (var visual in containerVisuals) + { + visual?.SetClip(geometricClipWithGeometry.QueryInterface()); + } + + return roundedRectangleGeometry.CloneReference(); + } + + public static IVisual CreateBlurVisual(ICompositor compositor, ICompositionBrush compositionBrush) + { + using var spriteVisual = compositor.CreateSpriteVisual(); + using var visual = spriteVisual.QueryInterface(); + using var visual2 = spriteVisual.QueryInterface(); + + + spriteVisual.SetBrush(compositionBrush); + visual.SetIsVisible(0); + visual2.SetRelativeSizeAdjustment(new Vector2(1.0f, 1.0f)); + + return visual.CloneReference(); + } + + public static ICompositionBrush CreateBackdropBrush(ICompositor compositor) + { + ICompositionBackdropBrush brush = null; + try + { + if (Win32Platform.WindowsVersion >= WinUiCompositionShared.MinHostBackdropVersion) + { + using var compositor3 = compositor.QueryInterface(); + brush = compositor3.CreateHostBackdropBrush(); + } + else + { + using var compositor2 = compositor.QueryInterface(); + brush = compositor2.CreateBackdropBrush(); + } + + return brush.QueryInterface(); + } + finally + { + brush?.Dispose(); + } + } +} \ No newline at end of file diff --git a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositorConnection.cs b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositorConnection.cs new file mode 100644 index 0000000000..a1408baae0 --- /dev/null +++ b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositorConnection.cs @@ -0,0 +1,151 @@ +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using Avalonia.Logging; +using Avalonia.MicroCom; +using Avalonia.OpenGL.Egl; +using Avalonia.Rendering; +using Avalonia.Win32.DirectX; +using Avalonia.Win32.Interop; +using Avalonia.Win32.OpenGl.Angle; +using MicroCom.Runtime; + +namespace Avalonia.Win32.WinRT.Composition; + +internal class WinUiCompositorConnection : IRenderTimer +{ + private readonly WinUiCompositionShared _shared; + public event Action Tick; + public bool RunsInBackground => true; + + public unsafe WinUiCompositorConnection() + { + using var compositor = NativeWinRTMethods.CreateInstance("Windows.UI.Composition.Compositor"); + /* + var levels = new[] { D3D_FEATURE_LEVEL.D3D_FEATURE_LEVEL_11_1 }; + DirectXUnmanagedMethods.D3D11CreateDevice(IntPtr.Zero, D3D_DRIVER_TYPE.D3D_DRIVER_TYPE_HARDWARE, + IntPtr.Zero, 0, levels, (uint)levels.Length, 7, out var pD3dDevice, out var level, null); + + var d3dDevice = MicroComRuntime.CreateProxyFor(pD3dDevice, true); + + var compositionDevice = compositor.QueryInterface().CreateGraphicsDevice(d3dDevice); + var surf = compositionDevice.CreateDrawingSurface(new UnmanagedMethods.SIZE_F { X = 100, Y = 100 }, + DirectXPixelFormat.B8G8R8A8UIntNormalized, DirectXAlphaMode.Premultiplied); + var surfInterop = surf.QueryInterface(); + var IID_ID3D11Texture2D = Guid.Parse("6f15aaf2-d208-4e89-9ab4-489535d34f9c"); + void* texture = null; + surfInterop.BeginDraw(null, &IID_ID3D11Texture2D, &texture); + */ + + _shared = new WinUiCompositionShared(compositor); + } + + static bool TryCreateAndRegisterCore() + { + var tcs = new TaskCompletionSource(); + var pumpLock = new object(); + var th = new Thread(() => + { + WinUiCompositorConnection connect; + try + { + NativeWinRTMethods.CreateDispatcherQueueController(new NativeWinRTMethods.DispatcherQueueOptions + { + apartmentType = NativeWinRTMethods.DISPATCHERQUEUE_THREAD_APARTMENTTYPE.DQTAT_COM_NONE, + dwSize = Marshal.SizeOf(), + threadType = NativeWinRTMethods.DISPATCHERQUEUE_THREAD_TYPE.DQTYPE_THREAD_CURRENT + }); + connect = new WinUiCompositorConnection(); + AvaloniaLocator.CurrentMutable.BindToSelf(connect); + AvaloniaLocator.CurrentMutable.Bind().ToConstant(connect); + tcs.SetResult(true); + + } + catch (Exception e) + { + tcs.SetException(e); + return; + } + + connect.RunLoop(); + }) + { + IsBackground = true, + Name = "DwmRenderTimerLoop" + }; + th.SetApartmentState(ApartmentState.STA); + th.Start(); + return tcs.Task.Result; + } + + class RunLoopHandler : CallbackBase, IAsyncActionCompletedHandler + { + private readonly WinUiCompositorConnection _parent; + private Stopwatch _st = Stopwatch.StartNew(); + + public RunLoopHandler(WinUiCompositorConnection parent) + { + _parent = parent; + } + + public void Invoke(IAsyncAction asyncInfo, AsyncStatus asyncStatus) + { + _parent.Tick?.Invoke(_st.Elapsed); + using var act = _parent._shared.Compositor5.RequestCommitAsync(); + act.SetCompleted(this); + } + } + + private void RunLoop() + { + var cts = new CancellationTokenSource(); + AppDomain.CurrentDomain.ProcessExit += (sender, args) => + cts.Cancel(); + + lock (_shared.SyncRoot) + using (var act = _shared.Compositor5.RequestCommitAsync()) + act.SetCompleted(new RunLoopHandler(this)); + + while (!cts.IsCancellationRequested) + { + UnmanagedMethods.GetMessage(out var msg, IntPtr.Zero, 0, 0); + lock (_shared.SyncRoot) + UnmanagedMethods.DispatchMessage(ref msg); + } + } + + public static void TryCreateAndRegister() + { + const int majorRequired = 10; + const int buildRequired = 17134; + + var majorInstalled = Win32Platform.WindowsVersion.Major; + var buildInstalled = Win32Platform.WindowsVersion.Build; + + if (majorInstalled >= majorRequired && + buildInstalled >= buildRequired) + { + try + { + TryCreateAndRegisterCore(); + return; + } + catch (Exception e) + { + Logger.TryGet(LogEventLevel.Error, "WinUIComposition") + ?.Log(null, "Unable to initialize WinUI compositor: {0}", e); + + } + } + + var osVersionNotice = + $"Windows {majorRequired} Build {buildRequired} is required. Your machine has Windows {majorInstalled} Build {buildInstalled} installed."; + + Logger.TryGet(LogEventLevel.Warning, "WinUIComposition")?.Log(null, + $"Unable to initialize WinUI compositor: {osVersionNotice}"); + } + + public WinUiCompositedWindowSurface CreateSurface(EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo info) => new(_shared, info); +} diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 5f813eec1d..596b60a8cb 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -26,7 +26,8 @@ using static Avalonia.Win32.Interop.UnmanagedMethods; using Avalonia.Collections.Pooled; using Avalonia.Metadata; using Avalonia.Platform.Storage; -using Avalonia.Win32.DxgiSwapchain; +using Avalonia.Win32.DirectX; +using Avalonia.Win32.OpenGl.Angle; namespace Avalonia.Win32 { @@ -64,7 +65,6 @@ namespace Avalonia.Win32 private Thickness _offScreenMargin; private double _extendTitleBarHint = -1; private bool _isUsingComposition; - private bool _isUsingDxgiSwapchain; private IBlurHost _blurHost; private PlatformResizeReason _resizeReason; private MOUSEMOVEPOINT _lastWmMousePoint; @@ -79,7 +79,7 @@ namespace Avalonia.Win32 private readonly PenDevice _penDevice; private readonly ManagedDeferredRendererLock _rendererLock; private readonly FramebufferManager _framebuffer; - private readonly IGlPlatformSurface _gl; + private readonly object _gl; private readonly bool _wmPointerEnabled; private Win32NativeControlHost _nativeControlHost; @@ -136,23 +136,20 @@ namespace Avalonia.Win32 }; _rendererLock = new ManagedDeferredRendererLock(); - var glPlatform = AvaloniaLocator.Current.GetService(); + var glPlatform = AvaloniaLocator.Current.GetService(); - var compositionConnector = AvaloniaLocator.Current.GetService(); + var compositionConnector = AvaloniaLocator.Current.GetService(); - _isUsingComposition = compositionConnector is { } && - glPlatform is EglPlatformOpenGlInterface egl && - egl.Display is AngleWin32EglDisplay angleDisplay && - angleDisplay.PlatformApi == AngleOptions.PlatformApi.DirectX11; + var isUsingAngleDX11 = glPlatform is AngleWin32PlatformGraphics angle && + angle.PlatformApi == AngleOptions.PlatformApi.DirectX11; + _isUsingComposition = compositionConnector is { } && isUsingAngleDX11; DxgiConnection dxgiConnection = null; + var isUsingDxgiSwapchain = false; if (!_isUsingComposition) { dxgiConnection = AvaloniaLocator.Current.GetService(); - _isUsingDxgiSwapchain = dxgiConnection is { } && - glPlatform is EglPlatformOpenGlInterface eglDxgi && - eglDxgi.Display is AngleWin32EglDisplay angleDisplayDxgi && - angleDisplayDxgi.PlatformApi == AngleOptions.PlatformApi.DirectX11; + isUsingDxgiSwapchain = dxgiConnection is { } && isUsingAngleDX11; } _wmPointerEnabled = Win32Platform.WindowsVersion >= PlatformConstants.Windows8; @@ -164,24 +161,24 @@ namespace Avalonia.Win32 { if (_isUsingComposition) { - var cgl = new WinUiCompositedWindowSurface(compositionConnector, this); + var cgl = compositionConnector.CreateSurface(this); _blurHost = cgl; _gl = cgl; _isUsingComposition = true; } - else if (_isUsingDxgiSwapchain) + else if (isUsingDxgiSwapchain) { var dxgigl = new DxgiSwapchainWindow(dxgiConnection, this); _gl = dxgigl; } else { - if (glPlatform is EglPlatformOpenGlInterface egl2) - _gl = new EglGlPlatformSurface(egl2, this); + if (glPlatform is AngleWin32PlatformGraphics egl2) + _gl = new EglGlPlatformSurface(this); else if (glPlatform is WglPlatformOpenGlInterface wgl) - _gl = new WglGlPlatformSurface(wgl.PrimaryContext, this); + _gl = new WglGlPlatformSurface(this); } } @@ -434,7 +431,7 @@ namespace Avalonia.Win32 _ => BlurEffect.None }; - if (Win32Platform.WindowsVersion >= WinUICompositorConnection.MinHostBackdropVersion) + if (Win32Platform.WindowsVersion >= WinUiCompositionShared.MinHostBackdropVersion) { unsafe { @@ -443,7 +440,7 @@ namespace Avalonia.Win32 } } - if (Win32Platform.WindowsVersion < WinUICompositorConnection.MinHostBackdropVersion && effect == BlurEffect.Mica) + if (Win32Platform.WindowsVersion < WinUiCompositionShared.MinHostBackdropVersion && effect == BlurEffect.Mica) { effect = BlurEffect.Acrylic; } @@ -570,16 +567,22 @@ namespace Avalonia.Win32 return customRendererFactory.Create(root, loop); if (Win32Platform.Compositor != null) - return new CompositingRenderer(root, Win32Platform.Compositor); - + return new CompositingRenderer(root, Win32Platform.Compositor, () => Surfaces); + return Win32Platform.UseDeferredRendering ? _isUsingComposition - ? new DeferredRenderer(root, loop) + ? new DeferredRenderer(root, loop, + () => Win32Platform.RenderInterface.CreateRenderTarget(Surfaces), + Win32Platform.RenderInterface) { RenderOnlyOnRenderThread = true } - : (IRenderer)new DeferredRenderer(root, loop, rendererLock: _rendererLock) - : new ImmediateRenderer((Visual)root); + : (IRenderer)new DeferredRenderer(root, loop, rendererLock: _rendererLock, + renderTargetFactory: () => Win32Platform.RenderInterface.CreateRenderTarget(Surfaces), + renderInterface: Win32Platform.RenderInterface) + : new ImmediateRenderer((Visual)root, + () => Win32Platform.RenderInterface.CreateRenderTarget(Surfaces), + Win32Platform.RenderInterface); } public void Resize(Size value, PlatformResizeReason reason) diff --git a/src/iOS/Avalonia.iOS/AvaloniaView.cs b/src/iOS/Avalonia.iOS/AvaloniaView.cs index 2080352020..48358c745f 100644 --- a/src/iOS/Avalonia.iOS/AvaloniaView.cs +++ b/src/iOS/Avalonia.iOS/AvaloniaView.cs @@ -70,7 +70,8 @@ namespace Avalonia.iOS // No-op } - public IRenderer CreateRenderer(IRenderRoot root) => new CompositingRenderer(root, Platform.Compositor); + public IRenderer CreateRenderer(IRenderRoot root) => + new CompositingRenderer(root, Platform.Compositor, () => Surfaces); public void Invalidate(Rect rect) diff --git a/src/iOS/Avalonia.iOS/EaglDisplay.cs b/src/iOS/Avalonia.iOS/EaglDisplay.cs index 906bbc29e7..7a5e1a496d 100644 --- a/src/iOS/Avalonia.iOS/EaglDisplay.cs +++ b/src/iOS/Avalonia.iOS/EaglDisplay.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Reactive.Disposables; using Avalonia.OpenGL; using Avalonia.Platform; @@ -6,29 +7,37 @@ using OpenGLES; namespace Avalonia.iOS { - class EaglFeature : IPlatformOpenGlInterface + class EaglPlatformGraphics : IPlatformGraphics { - public IGlContext PrimaryContext => Context; - public IGlContext CreateSharedContext() => throw new NotSupportedException(); - IPlatformGpuContext IPlatformGpu.PrimaryContext => PrimaryContext; - public bool CanShareContexts => false; - public bool CanCreateContexts => false; - public IGlContext CreateContext() => throw new System.NotSupportedException(); - public GlContext Context { get; } = new GlContext(); + public IPlatformGraphicsContext GetSharedContext() => Context; + + public bool UsesSharedContext => true; + public IPlatformGraphicsContext CreateContext() => throw new System.NotSupportedException(); + public GlContext Context { get; } + public static GlVersion GlVersion { get; } = new(GlProfileType.OpenGLES, 3, 0); + + public EaglPlatformGraphics() + { + + const string path = "/System/Library/Frameworks/OpenGLES.framework/OpenGLES"; + var libGl = ObjCRuntime.Dlfcn.dlopen(path, 1); + if (libGl == IntPtr.Zero) + throw new OpenGlException("Unable to load " + path); + var iface = new GlInterface(GlVersion, proc => ObjCRuntime.Dlfcn.dlsym(libGl, proc)); + Context = new(iface, null); + } } class GlContext : IGlContext { public EAGLContext Context { get; private set; } - public GlContext() + public GlContext(GlInterface glInterface, EAGLSharegroup sharegroup) { - const string path = "/System/Library/Frameworks/OpenGLES.framework/OpenGLES"; - var libGl = ObjCRuntime.Dlfcn.dlopen(path, 1); - if (libGl == IntPtr.Zero) - throw new OpenGlException("Unable to load " + path); - GlInterface = new GlInterface(Version, proc => ObjCRuntime.Dlfcn.dlsym(libGl, proc)); - Context = new EAGLContext(EAGLRenderingAPI.OpenGLES3); + GlInterface = glInterface; + Context = sharegroup == null ? + new EAGLContext(EAGLRenderingAPI.OpenGLES3) : + new(EAGLRenderingAPI.OpenGLES3, sharegroup); } public void Dispose() @@ -59,22 +68,34 @@ namespace Avalonia.iOS public IDisposable MakeCurrent() { + if (Context == null) + throw new PlatformGraphicsContextLostException(); var old = EAGLContext.CurrentContext; if (!EAGLContext.SetCurrentContext(Context)) throw new OpenGlException("Unable to make context current"); return new ResetContext(old); } + public bool IsLost => Context == null; + public IDisposable EnsureCurrent() { + if (Context == null) + throw new PlatformGraphicsContextLostException(); if(EAGLContext.CurrentContext == Context) return Disposable.Empty; return MakeCurrent(); } - public bool IsSharedWith(IGlContext context) => false; + public bool IsSharedWith(IGlContext context) => context is GlContext other + && ReferenceEquals(other.Context?.ShareGroup, Context?.ShareGroup); + public bool CanCreateSharedContext => true; + public IGlContext CreateSharedContext(IEnumerable preferredVersions = null) + { + return new GlContext(GlInterface, Context.ShareGroup); + } - public GlVersion Version { get; } = new GlVersion(GlProfileType.OpenGLES, 3, 0); + public GlVersion Version => EaglPlatformGraphics.GlVersion; public GlInterface GlInterface { get; } public int SampleCount { @@ -92,5 +113,7 @@ namespace Avalonia.iOS return stencil; } } + + public object TryGetFeature(Type featureType) => null; } } diff --git a/src/iOS/Avalonia.iOS/EaglLayerSurface.cs b/src/iOS/Avalonia.iOS/EaglLayerSurface.cs index 0e8945d921..88812653d7 100644 --- a/src/iOS/Avalonia.iOS/EaglLayerSurface.cs +++ b/src/iOS/Avalonia.iOS/EaglLayerSurface.cs @@ -78,10 +78,12 @@ namespace Avalonia.iOS throw new InvalidOperationException("Invalid thread, go away"); } - public IGlPlatformSurfaceRenderTarget CreateGlRenderTarget() + public IGlPlatformSurfaceRenderTarget CreateGlRenderTarget(IGlContext context) { CheckThread(); var ctx = Platform.GlFeature.Context; + if (ctx != context) + throw new InvalidOperationException("Platform surface is only usable with tha main context"); using (ctx.MakeCurrent()) { var fbo = new SizeSynchronizedLayerFbo(ctx.Context, ctx.GlInterface, _layer); diff --git a/src/iOS/Avalonia.iOS/Platform.cs b/src/iOS/Avalonia.iOS/Platform.cs index c2cf8c6f6c..eb0a55734a 100644 --- a/src/iOS/Avalonia.iOS/Platform.cs +++ b/src/iOS/Avalonia.iOS/Platform.cs @@ -25,18 +25,18 @@ namespace Avalonia.iOS { static class Platform { - public static EaglFeature GlFeature; + public static EaglPlatformGraphics GlFeature; public static DisplayLinkTimer Timer; internal static Compositor Compositor { get; private set; } public static void Register() { - GlFeature ??= new EaglFeature(); + GlFeature ??= new EaglPlatformGraphics(); Timer ??= new DisplayLinkTimer(); var keyboard = new KeyboardDevice(); AvaloniaLocator.CurrentMutable - .Bind().ToConstant(GlFeature) + .Bind().ToConstant(GlFeature) .Bind().ToConstant(new CursorFactoryStub()) .Bind().ToConstant(new WindowingPlatformStub()) .Bind().ToConstant(new ClipboardImpl()) @@ -50,7 +50,7 @@ namespace Avalonia.iOS Compositor = new Compositor( AvaloniaLocator.Current.GetRequiredService(), - AvaloniaLocator.Current.GetService()); + AvaloniaLocator.Current.GetService()); } diff --git a/tests/Avalonia.Base.UnitTests/Rendering/CompositorTestsBase.cs b/tests/Avalonia.Base.UnitTests/Rendering/CompositorTestsBase.cs index 7b987d8e68..d407a09b06 100644 --- a/tests/Avalonia.Base.UnitTests/Rendering/CompositorTestsBase.cs +++ b/tests/Avalonia.Base.UnitTests/Rendering/CompositorTestsBase.cs @@ -76,7 +76,7 @@ public class CompositorTestsBase public IRenderer CreateRenderer(IRenderRoot root) { - return Renderer = new CompositingRenderer(root, _compositor); + return Renderer = new CompositingRenderer(root, _compositor, () => Surfaces); } public void Invalidate(Rect rect) diff --git a/tests/Avalonia.Base.UnitTests/Rendering/DeferredRendererTests.cs b/tests/Avalonia.Base.UnitTests/Rendering/DeferredRendererTests.cs index 08e5955ec5..f0c5a24cc4 100644 --- a/tests/Avalonia.Base.UnitTests/Rendering/DeferredRendererTests.cs +++ b/tests/Avalonia.Base.UnitTests/Rendering/DeferredRendererTests.cs @@ -46,6 +46,7 @@ namespace Avalonia.Base.UnitTests.Rendering var target = new DeferredRenderer( root, loop.Object, + renderTargetFactory: root.CreateRenderTarget, sceneBuilder: sceneBuilder.Object); target.Start(); @@ -83,6 +84,7 @@ namespace Avalonia.Base.UnitTests.Rendering root, loop.Object, sceneBuilder: sceneBuilder.Object, + renderTargetFactory: root.CreateRenderTarget, dispatcher: dispatcher); target.Start(); @@ -133,6 +135,7 @@ namespace Avalonia.Base.UnitTests.Rendering root, loop.Object, sceneBuilder: sceneBuilder, + renderTargetFactory: root.CreateRenderTarget, dispatcher: dispatcher); root.Renderer = target; @@ -178,6 +181,7 @@ namespace Avalonia.Base.UnitTests.Rendering root, loop.Object, sceneBuilder: sceneBuilder, + renderTargetFactory: root.CreateRenderTarget, dispatcher: dispatcher); root.Renderer = target; @@ -223,6 +227,7 @@ namespace Avalonia.Base.UnitTests.Rendering root, loop.Object, sceneBuilder: sceneBuilder, + renderTargetFactory: root.CreateRenderTarget, dispatcher: dispatcher); root.Renderer = target; @@ -270,6 +275,7 @@ namespace Avalonia.Base.UnitTests.Rendering root, loop.Object, sceneBuilder: sceneBuilder, + renderTargetFactory: root.CreateRenderTarget, dispatcher: dispatcher); root.Renderer = target; @@ -317,6 +323,7 @@ namespace Avalonia.Base.UnitTests.Rendering root, loop.Object, sceneBuilder: sceneBuilder, + renderTargetFactory: root.CreateRenderTarget, dispatcher: dispatcher); root.Renderer = target; @@ -366,6 +373,7 @@ namespace Avalonia.Base.UnitTests.Rendering root, loop.Object, sceneBuilder: sceneBuilder, + renderTargetFactory: root.CreateRenderTarget, dispatcher: dispatcher); root.Renderer = target; @@ -415,6 +423,7 @@ namespace Avalonia.Base.UnitTests.Rendering root, loop.Object, sceneBuilder: sceneBuilder, + renderTargetFactory: root.CreateRenderTarget, dispatcher: dispatcher); var otherSceneBuilder = new SceneBuilder(); @@ -423,6 +432,7 @@ namespace Avalonia.Base.UnitTests.Rendering otherRoot, loop.Object, sceneBuilder: otherSceneBuilder, + renderTargetFactory: root.CreateRenderTarget, dispatcher: dispatcher); root.Renderer = target; @@ -738,6 +748,7 @@ namespace Avalonia.Base.UnitTests.Rendering root, new RenderLoop(timer.Object, dispatcher), sceneBuilder: sceneBuilder, + renderTargetFactory: root.CreateRenderTarget, dispatcher: dispatcher); root.Renderer = target; target.Start(); diff --git a/tests/Avalonia.Base.UnitTests/Rendering/DeferredRendererTests_HitTesting.cs b/tests/Avalonia.Base.UnitTests/Rendering/DeferredRendererTests_HitTesting.cs index 2cf42d9604..52a9f58006 100644 --- a/tests/Avalonia.Base.UnitTests/Rendering/DeferredRendererTests_HitTesting.cs +++ b/tests/Avalonia.Base.UnitTests/Rendering/DeferredRendererTests_HitTesting.cs @@ -35,7 +35,7 @@ namespace Avalonia.Base.UnitTests.Rendering } }; - root.Renderer = new DeferredRenderer((IRenderRoot)root, null); + root.Renderer = new DeferredRenderer((IRenderRoot)root, null, root.CreateRenderTarget); root.Measure(Size.Infinity); root.Arrange(new Rect(root.DesiredSize)); @@ -63,7 +63,7 @@ namespace Avalonia.Base.UnitTests.Rendering } }; - root.Renderer = new DeferredRenderer((IRenderRoot)root, null); + root.Renderer = new DeferredRenderer((IRenderRoot)root, null, root.CreateRenderTarget); root.Measure(Size.Infinity); root.Arrange(new Rect(root.DesiredSize)); @@ -100,7 +100,7 @@ namespace Avalonia.Base.UnitTests.Rendering } }; - root.Renderer = new DeferredRenderer((IRenderRoot)root, null); + root.Renderer = new DeferredRenderer((IRenderRoot)root, null, root.CreateRenderTarget); root.Measure(Size.Infinity); root.Arrange(new Rect(root.DesiredSize)); @@ -129,7 +129,7 @@ namespace Avalonia.Base.UnitTests.Rendering } }; - root.Renderer = new DeferredRenderer((IRenderRoot)root, null); + root.Renderer = new DeferredRenderer((IRenderRoot)root, null, root.CreateRenderTarget); root.Measure(Size.Infinity); root.Arrange(new Rect(root.DesiredSize)); @@ -173,7 +173,7 @@ namespace Avalonia.Base.UnitTests.Rendering } }; - root.Renderer = new DeferredRenderer((IRenderRoot)root, null); + root.Renderer = new DeferredRenderer((IRenderRoot)root, null, root.CreateRenderTarget); root.Measure(Size.Infinity); root.Arrange(new Rect(container.DesiredSize)); @@ -227,7 +227,7 @@ namespace Avalonia.Base.UnitTests.Rendering } }; - root.Renderer = new DeferredRenderer((IRenderRoot)root, null); + root.Renderer = new DeferredRenderer((IRenderRoot)root, null, root.CreateRenderTarget); root.Measure(Size.Infinity); root.Arrange(new Rect(container.DesiredSize)); @@ -276,7 +276,7 @@ namespace Avalonia.Base.UnitTests.Rendering } }; - root.Renderer = new DeferredRenderer((IRenderRoot)root, null); + root.Renderer = new DeferredRenderer((IRenderRoot)root, null, root.CreateRenderTarget); container.Measure(Size.Infinity); container.Arrange(new Rect(container.DesiredSize)); @@ -324,7 +324,7 @@ namespace Avalonia.Base.UnitTests.Rendering } }; - root.Renderer = new DeferredRenderer((IRenderRoot)root, null); + root.Renderer = new DeferredRenderer((IRenderRoot)root, null, root.CreateRenderTarget); root.Measure(Size.Infinity); root.Arrange(new Rect(container.DesiredSize)); @@ -399,7 +399,7 @@ namespace Avalonia.Base.UnitTests.Rendering scroll.UpdateChild(); - root.Renderer = new DeferredRenderer((IRenderRoot)root, null); + root.Renderer = new DeferredRenderer((IRenderRoot)root, null, root.CreateRenderTarget); root.Measure(Size.Infinity); root.Arrange(new Rect(container.DesiredSize)); @@ -447,7 +447,7 @@ namespace Avalonia.Base.UnitTests.Rendering } }; - root.Renderer = new DeferredRenderer((IRenderRoot)root, null); + root.Renderer = new DeferredRenderer((IRenderRoot)root, null, root.CreateRenderTarget); root.Measure(Size.Infinity); root.Arrange(new Rect(root.DesiredSize)); @@ -486,7 +486,7 @@ namespace Avalonia.Base.UnitTests.Rendering } }; - root.Renderer = new DeferredRenderer((IRenderRoot)root, null); + root.Renderer = new DeferredRenderer((IRenderRoot)root, null, root.CreateRenderTarget); root.Measure(Size.Infinity); root.Arrange(new Rect(root.DesiredSize)); Assert.Equal(new Rect(100, 100, 200, 200), border.Bounds); @@ -522,7 +522,7 @@ namespace Avalonia.Base.UnitTests.Rendering } }; - root.Renderer = new DeferredRenderer((IRenderRoot)root, null); + root.Renderer = new DeferredRenderer((IRenderRoot)root, null, root.CreateRenderTarget); root.Measure(Size.Infinity); root.Arrange(new Rect(root.DesiredSize)); @@ -560,7 +560,7 @@ namespace Avalonia.Base.UnitTests.Rendering } }; - root.Renderer = new DeferredRenderer((IRenderRoot)root, null); + root.Renderer = new DeferredRenderer((IRenderRoot)root, null, root.CreateRenderTarget); root.Measure(Size.Infinity); root.Arrange(new Rect(root.DesiredSize)); diff --git a/tests/Avalonia.Base.UnitTests/Rendering/ImmediateRendererTests.cs b/tests/Avalonia.Base.UnitTests/Rendering/ImmediateRendererTests.cs index 26a155d203..a07191f464 100644 --- a/tests/Avalonia.Base.UnitTests/Rendering/ImmediateRendererTests.cs +++ b/tests/Avalonia.Base.UnitTests/Rendering/ImmediateRendererTests.cs @@ -36,7 +36,7 @@ namespace Avalonia.Base.UnitTests.Rendering root.LayoutManager.ExecuteInitialLayoutPass(); - var target = new ImmediateRenderer(root); + var target = new ImmediateRenderer(root, root.CreateRenderTarget); target.AddDirty(child); @@ -69,7 +69,7 @@ namespace Avalonia.Base.UnitTests.Rendering child.RenderTransform = new ScaleTransform() { ScaleX = 2, ScaleY = 2 }; - var target = new ImmediateRenderer(root); + var target = new ImmediateRenderer(root, root.CreateRenderTarget); target.AddDirty(child); @@ -97,7 +97,7 @@ namespace Avalonia.Base.UnitTests.Rendering Height = 400, }; - var target = new ImmediateRenderer(root); + var target = new ImmediateRenderer(root, root.CreateRenderTarget); root.LayoutManager.ExecuteInitialLayoutPass(); target.AddDirty(child); @@ -165,7 +165,7 @@ namespace Avalonia.Base.UnitTests.Rendering stackPanel.Children.Add(control3); var root = new TestRoot(rootGrid); - root.Renderer = new ImmediateRenderer(root); + root.Renderer = new ImmediateRenderer(root, root.CreateRenderTarget); root.LayoutManager.ExecuteInitialLayoutPass(); var rootSize = new Size(RootWidth, RootHeight); @@ -223,7 +223,7 @@ namespace Avalonia.Base.UnitTests.Rendering stackPanel.Children.Add(control3); var root = new TestRoot(rootGrid); - root.Renderer = new ImmediateRenderer(root); + root.Renderer = new ImmediateRenderer(root, root.CreateRenderTarget); root.LayoutManager.ExecuteInitialLayoutPass(); var rootSize = new Size(RootWidth, RootHeight); diff --git a/tests/Avalonia.Base.UnitTests/Rendering/ImmediateRendererTests_HitTesting.cs b/tests/Avalonia.Base.UnitTests/Rendering/ImmediateRendererTests_HitTesting.cs index 70e4acba1e..9ce8c42e33 100644 --- a/tests/Avalonia.Base.UnitTests/Rendering/ImmediateRendererTests_HitTesting.cs +++ b/tests/Avalonia.Base.UnitTests/Rendering/ImmediateRendererTests_HitTesting.cs @@ -32,7 +32,7 @@ namespace Avalonia.Base.UnitTests.Rendering } }; - root.Renderer = new ImmediateRenderer(root); + root.Renderer = new ImmediateRenderer(root, root.CreateRenderTarget); root.Measure(Size.Infinity); root.Arrange(new Rect(root.DesiredSize)); root.Renderer.Paint(new Rect(root.ClientSize)); @@ -70,7 +70,7 @@ namespace Avalonia.Base.UnitTests.Rendering } }; - root.Renderer = new ImmediateRenderer(root); + root.Renderer = new ImmediateRenderer(root, root.CreateRenderTarget); root.Measure(Size.Infinity); root.Arrange(new Rect(root.DesiredSize)); root.Renderer.Paint(new Rect(root.ClientSize)); @@ -100,7 +100,7 @@ namespace Avalonia.Base.UnitTests.Rendering } }; - root.Renderer = new ImmediateRenderer(root); + root.Renderer = new ImmediateRenderer(root, root.CreateRenderTarget); root.Measure(Size.Infinity); root.Arrange(new Rect(root.DesiredSize)); root.Renderer.Paint(new Rect(root.ClientSize)); @@ -145,7 +145,7 @@ namespace Avalonia.Base.UnitTests.Rendering } }; - root.Renderer = new ImmediateRenderer(root); + root.Renderer = new ImmediateRenderer(root, root.CreateRenderTarget); root.Measure(Size.Infinity); root.Arrange(new Rect(container.DesiredSize)); root.Renderer.Paint(new Rect(root.ClientSize)); @@ -200,7 +200,7 @@ namespace Avalonia.Base.UnitTests.Rendering } }; - root.Renderer = new ImmediateRenderer(root); + root.Renderer = new ImmediateRenderer(root, root.CreateRenderTarget); root.Measure(Size.Infinity); root.Arrange(new Rect(container.DesiredSize)); root.Renderer.Paint(new Rect(root.ClientSize)); @@ -259,7 +259,7 @@ namespace Avalonia.Base.UnitTests.Rendering } }; - root.Renderer = new ImmediateRenderer(root); + root.Renderer = new ImmediateRenderer(root, root.CreateRenderTarget); container.Measure(Size.Infinity); container.Arrange(new Rect(container.DesiredSize)); root.Renderer.Paint(new Rect(root.ClientSize)); @@ -308,7 +308,7 @@ namespace Avalonia.Base.UnitTests.Rendering } }; - root.Renderer = new ImmediateRenderer(root); + root.Renderer = new ImmediateRenderer(root, root.CreateRenderTarget); root.Measure(Size.Infinity); root.Arrange(new Rect(container.DesiredSize)); root.Renderer.Paint(new Rect(root.ClientSize)); @@ -384,7 +384,7 @@ namespace Avalonia.Base.UnitTests.Rendering scroll.UpdateChild(); - root.Renderer = new ImmediateRenderer(root); + root.Renderer = new ImmediateRenderer(root, root.CreateRenderTarget); root.Measure(Size.Infinity); root.Arrange(new Rect(container.DesiredSize)); root.Renderer.Paint(new Rect(root.ClientSize)); @@ -434,7 +434,7 @@ namespace Avalonia.Base.UnitTests.Rendering } }; - root.Renderer = new ImmediateRenderer(root); + root.Renderer = new ImmediateRenderer(root, root.CreateRenderTarget); root.Measure(Size.Infinity); root.Arrange(new Rect(root.DesiredSize)); root.Renderer.Paint(new Rect(root.ClientSize)); diff --git a/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs b/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs index 10db08f302..42e33729ac 100644 --- a/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs +++ b/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs @@ -8,13 +8,15 @@ using Avalonia.Media.Imaging; namespace Avalonia.Base.UnitTests.VisualTree { - class MockRenderInterface : IPlatformRenderInterface + class MockRenderInterface : IPlatformRenderInterface, IPlatformRenderInterfaceContext { public IRenderTarget CreateRenderTarget(IEnumerable surfaces) { throw new NotImplementedException(); } + public object TryGetFeature(Type featureType) => null; + public IRenderTargetBitmapImpl CreateRenderTargetBitmap(PixelSize size, Vector dpi) { throw new NotImplementedException(); @@ -77,6 +79,11 @@ namespace Avalonia.Base.UnitTests.VisualTree throw new NotImplementedException(); } + public IPlatformRenderInterfaceContext CreateBackendContext(IPlatformGraphicsContext graphicsContext) + { + return this; + } + public bool SupportsIndividualRoundRects { get; set; } public AlphaFormat DefaultAlphaFormat { get; } public PixelFormat DefaultPixelFormat { get; } @@ -263,6 +270,11 @@ namespace Avalonia.Base.UnitTests.VisualTree } } } + + public void Dispose() + { + + } } } diff --git a/tests/Avalonia.Base.UnitTests/VisualTree/VisualExtensions_GetVisualsAt.cs b/tests/Avalonia.Base.UnitTests/VisualTree/VisualExtensions_GetVisualsAt.cs index b40ea40985..0c516a0481 100644 --- a/tests/Avalonia.Base.UnitTests/VisualTree/VisualExtensions_GetVisualsAt.cs +++ b/tests/Avalonia.Base.UnitTests/VisualTree/VisualExtensions_GetVisualsAt.cs @@ -44,7 +44,7 @@ namespace Avalonia.Base.UnitTests.VisualTree } }; - root.Renderer = new DeferredRenderer((IRenderRoot)root, null); + root.Renderer = new DeferredRenderer((IRenderRoot)root, null, root.CreateRenderTarget); root.Measure(Size.Infinity); root.Arrange(new Rect(root.DesiredSize)); @@ -86,7 +86,7 @@ namespace Avalonia.Base.UnitTests.VisualTree } }; - root.Renderer = new DeferredRenderer((IRenderRoot)root, null); + root.Renderer = new DeferredRenderer((IRenderRoot)root, null, root.CreateRenderTarget); root.Measure(Size.Infinity); root.Arrange(new Rect(root.DesiredSize)); diff --git a/tests/Avalonia.Benchmarks/NullRenderer.cs b/tests/Avalonia.Benchmarks/NullRenderer.cs index 16f8998038..feb325f630 100644 --- a/tests/Avalonia.Benchmarks/NullRenderer.cs +++ b/tests/Avalonia.Benchmarks/NullRenderer.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Threading.Tasks; using Avalonia.Rendering; using Avalonia.VisualTree; @@ -43,5 +44,7 @@ namespace Avalonia.Benchmarks public void Stop() { } + + public ValueTask TryGetRenderInterfaceFeature(Type featureType) => new(0); } } diff --git a/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs b/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs index 4170de71e6..a272d89b8a 100644 --- a/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs +++ b/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs @@ -9,7 +9,7 @@ using Microsoft.Diagnostics.Runtime; namespace Avalonia.Benchmarks { - internal class NullRenderingPlatform : IPlatformRenderInterface + internal class NullRenderingPlatform : IPlatformRenderInterface, IPlatformRenderInterfaceContext { public IGeometryImpl CreateEllipseGeometry(Rect rect) { @@ -46,6 +46,8 @@ namespace Avalonia.Benchmarks throw new NotImplementedException(); } + public object TryGetFeature(Type featureType) => null; + public IRenderTargetBitmapImpl CreateRenderTargetBitmap(PixelSize size, Vector dpi) { throw new NotImplementedException(); @@ -123,10 +125,19 @@ namespace Avalonia.Benchmarks return new MockGlyphRun(); } + public IPlatformRenderInterfaceContext CreateBackendContext(IPlatformGraphicsContext graphicsContext) + { + return this; + } + public bool SupportsIndividualRoundRects => true; public AlphaFormat DefaultAlphaFormat => AlphaFormat.Premul; public PixelFormat DefaultPixelFormat => PixelFormat.Rgba8888; + public void Dispose() + { + + } } } diff --git a/tests/Avalonia.LeakTests/ControlTests.cs b/tests/Avalonia.LeakTests/ControlTests.cs index ae8df6168d..c713a9e61d 100644 --- a/tests/Avalonia.LeakTests/ControlTests.cs +++ b/tests/Avalonia.LeakTests/ControlTests.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Reactive.Disposables; - +using System.Threading.Tasks; using Avalonia.Collections; using Avalonia.Controls; using Avalonia.Controls.Presenters; @@ -1060,6 +1060,8 @@ namespace Avalonia.LeakTests public void Stop() { } + + public ValueTask TryGetRenderInterfaceFeature(Type featureType) => new(null); } } } diff --git a/tests/Avalonia.RenderTests/Media/BitmapTests.cs b/tests/Avalonia.RenderTests/Media/BitmapTests.cs index d52539c371..2d83f5ce0f 100644 --- a/tests/Avalonia.RenderTests/Media/BitmapTests.cs +++ b/tests/Avalonia.RenderTests/Media/BitmapTests.cs @@ -69,8 +69,9 @@ namespace Avalonia.Direct2D1.RenderTests.Media { var testName = nameof(FramebufferRenderResultsShouldBeUsableAsBitmap) + "_" + fmt; var fb = new Framebuffer(fmt, new PixelSize(80, 80)); - var r = Avalonia.AvaloniaLocator.Current.GetService(); - using (var target = r.CreateRenderTarget(new object[] { fb })) + var r = Avalonia.AvaloniaLocator.Current.GetRequiredService(); + using(var cpuContext = r.CreateBackendContext(null)) + using (var target = cpuContext.CreateRenderTarget(new object[] { fb })) using (var ctx = target.CreateDrawingContext(null)) { ctx.Clear(Colors.Transparent); diff --git a/tests/Avalonia.RenderTests/TestBase.cs b/tests/Avalonia.RenderTests/TestBase.cs index 3f918e2a73..8a127897d7 100644 --- a/tests/Avalonia.RenderTests/TestBase.cs +++ b/tests/Avalonia.RenderTests/TestBase.cs @@ -13,6 +13,7 @@ using System.Collections.Generic; using System.Linq; using System.Reactive.Disposables; using System.Threading; +using Avalonia.Controls.Platform.Surfaces; using Avalonia.Media; using Avalonia.Rendering.Composition; using Avalonia.Threading; @@ -122,10 +123,13 @@ namespace Avalonia.Direct2D1.RenderTests var timer = new ManualRenderTimer(); var compositor = new Compositor(new RenderLoop(timer, Dispatcher.UIThread), null); - using (var rtb = factory.CreateRenderTargetBitmap(pixelSize, dpiVector)) + using (var writableBitmap = factory.CreateWriteableBitmap(pixelSize, dpiVector, factory.DefaultPixelFormat, factory.DefaultAlphaFormat)) { - var root = new TestRenderRoot(dpiVector.X / 96, rtb); - using (var renderer = new CompositingRenderer(root, compositor) { RenderOnlyOnRenderThread = false}) + var root = new TestRenderRoot(dpiVector.X / 96, null!); + using (var renderer = new CompositingRenderer(root, compositor, () => new[] + { + new BitmapFramebufferSurface(writableBitmap) + }) { RenderOnlyOnRenderThread = false }) { root.Initialize(renderer, target); renderer.Start(); @@ -136,8 +140,20 @@ namespace Avalonia.Direct2D1.RenderTests // Free pools for (var c = 0; c < 11; c++) TestThreadingInterface.RunTimers(); - rtb.Save(compositedPath); + writableBitmap.Save(compositedPath); + } + } + + class BitmapFramebufferSurface : IFramebufferPlatformSurface + { + private readonly IWriteableBitmapImpl _bitmap; + + public BitmapFramebufferSurface(IWriteableBitmapImpl bitmap) + { + _bitmap = bitmap; } + + public ILockedFramebuffer Lock() => _bitmap.Lock(); } protected void CompareImages([CallerMemberName] string testName = "") diff --git a/tests/Avalonia.Skia.UnitTests/HitTesting.cs b/tests/Avalonia.Skia.UnitTests/HitTesting.cs index dceb0cdb9b..df267ee136 100644 --- a/tests/Avalonia.Skia.UnitTests/HitTesting.cs +++ b/tests/Avalonia.Skia.UnitTests/HitTesting.cs @@ -30,7 +30,7 @@ namespace Avalonia.Skia.UnitTests } }; - root.Renderer = new DeferredRenderer((IRenderRoot)root, null); + root.Renderer = new DeferredRenderer((IRenderRoot)root, null, root.CreateRenderTarget); root.Measure(Size.Infinity); root.Arrange(new Rect(root.DesiredSize)); @@ -64,7 +64,7 @@ namespace Avalonia.Skia.UnitTests } }; - root.Renderer = new DeferredRenderer((IRenderRoot)root, null); + root.Renderer = new DeferredRenderer((IRenderRoot)root, null, root.CreateRenderTarget); root.Measure(Size.Infinity); root.Arrange(new Rect(root.DesiredSize)); diff --git a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs index 0f951ed867..f9e1e45098 100644 --- a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs +++ b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs @@ -9,7 +9,7 @@ using Moq; namespace Avalonia.UnitTests { - public class MockPlatformRenderInterface : IPlatformRenderInterface + public class MockPlatformRenderInterface : IPlatformRenderInterface, IPlatformRenderInterfaceContext { public IGeometryImpl CreateEllipseGeometry(Rect rect) { @@ -48,6 +48,8 @@ namespace Avalonia.UnitTests return m.Object; } + + public bool IsCorrupted => false; } public IRenderTarget CreateRenderTarget(IEnumerable surfaces) @@ -55,6 +57,8 @@ namespace Avalonia.UnitTests return new MockRenderTarget(); } + public object TryGetFeature(Type featureType) => null; + public IRenderTargetBitmapImpl CreateRenderTargetBitmap(PixelSize size, Vector dpi) { return Mock.Of(); @@ -147,6 +151,8 @@ namespace Avalonia.UnitTests return Mock.Of(); } + public IPlatformRenderInterfaceContext CreateBackendContext(IPlatformGraphicsContext graphicsContext) => this; + public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun) { return Mock.Of(); @@ -172,5 +178,8 @@ namespace Avalonia.UnitTests public AlphaFormat DefaultAlphaFormat => AlphaFormat.Premul; public PixelFormat DefaultPixelFormat => PixelFormat.Rgba8888; + public void Dispose() + { + } } } From 5ab8af49575edaf02d32c31c4eee264a4d0348cc Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Tue, 6 Dec 2022 17:51:18 +0300 Subject: [PATCH 39/87] Direct2D WritableBitmap DPI fix --- .../Avalonia.Direct2D1/Media/Imaging/WriteableWicBitmapImpl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Windows/Avalonia.Direct2D1/Media/Imaging/WriteableWicBitmapImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WriteableWicBitmapImpl.cs index 885be132a4..2e40bdd9d1 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/Imaging/WriteableWicBitmapImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WriteableWicBitmapImpl.cs @@ -52,7 +52,7 @@ namespace Avalonia.Direct2D1.Media.Imaging public IntPtr Address => _lock.Data.DataPointer; public PixelSize Size => _lock.Size.ToAvalonia(); public int RowBytes => _lock.Stride; - public Vector Dpi { get; } = new Vector(96, 96); + public Vector Dpi => _parent.Dpi; public PixelFormat Format => _format; } From 51c2fa08589d9068235b3c21181a6bd33d3f42c2 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Tue, 6 Dec 2022 14:20:37 +0100 Subject: [PATCH 40/87] fix: address review --- src/Avalonia.Base/Animation/Cue.cs | 14 -------------- src/Avalonia.Base/Media/BoxShadow.cs | 6 ++++++ src/Avalonia.Base/Media/BoxShadows.cs | 4 +++- 3 files changed, 9 insertions(+), 15 deletions(-) diff --git a/src/Avalonia.Base/Animation/Cue.cs b/src/Avalonia.Base/Animation/Cue.cs index 934ae16762..c48f2ab6b0 100644 --- a/src/Avalonia.Base/Animation/Cue.cs +++ b/src/Avalonia.Base/Animation/Cue.cs @@ -49,15 +49,6 @@ namespace Avalonia.Animation } } - /// - /// Checks for equality between two s. - /// - /// The second cue. - public bool Equals(Cue other) - { - return CueValue == other.CueValue; - } - /// /// Checks for equality between a /// and a value. @@ -68,11 +59,6 @@ namespace Avalonia.Animation { return CueValue == other; } - - public override int GetHashCode() - { - return CueValue.GetHashCode(); - } } public class CueTypeConverter : TypeConverter diff --git a/src/Avalonia.Base/Media/BoxShadow.cs b/src/Avalonia.Base/Media/BoxShadow.cs index 2139516be6..fa94dd83eb 100644 --- a/src/Avalonia.Base/Media/BoxShadow.cs +++ b/src/Avalonia.Base/Media/BoxShadow.cs @@ -171,5 +171,11 @@ namespace Avalonia.Media public Rect TransformBounds(in Rect rect) => IsInset ? rect : rect.Translate(new Vector(OffsetX, OffsetY)).Inflate(Spread + Blur); + + public static bool operator ==(BoxShadow left, BoxShadow right) => + left.Equals(right); + + public static bool operator !=(BoxShadow left, BoxShadow right) => + !(left == right); } } diff --git a/src/Avalonia.Base/Media/BoxShadows.cs b/src/Avalonia.Base/Media/BoxShadows.cs index 9c20b9541d..4265e4efbc 100644 --- a/src/Avalonia.Base/Media/BoxShadows.cs +++ b/src/Avalonia.Base/Media/BoxShadows.cs @@ -62,7 +62,9 @@ namespace Avalonia.Media } [EditorBrowsable(EditorBrowsableState.Never)] - public record struct BoxShadowsEnumerator +#pragma warning disable CA1815 // Override equals and operator equals on value types + public struct BoxShadowsEnumerator +#pragma warning restore CA1815 // Override equals and operator equals on value types { private int _index; private BoxShadows _shadows; From 4502ed3729663199ec173f79cd1373e12855d28f Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 6 Dec 2022 16:29:43 +0100 Subject: [PATCH 41/87] Added ReactiveUIDemo sample. --- .ncrunch/ReactiveUIDemo.v3.ncrunchproject | 5 +++ Avalonia.Desktop.slnf | 1 + Avalonia.sln | 7 ++++ samples/ReactiveUIDemo/App.axaml | 8 ++++ samples/ReactiveUIDemo/App.axaml.cs | 37 +++++++++++++++++++ samples/ReactiveUIDemo/MainWindow.axaml | 19 ++++++++++ samples/ReactiveUIDemo/MainWindow.axaml.cs | 22 +++++++++++ samples/ReactiveUIDemo/ReactiveUIDemo.csproj | 28 ++++++++++++++ .../ReactiveUIDemo/ViewModels/BarViewModel.cs | 11 ++++++ .../ReactiveUIDemo/ViewModels/FooViewModel.cs | 11 ++++++ .../ViewModels/MainWindowViewModel.cs | 9 +++++ .../ViewModels/RoutedViewHostPageViewModel.cs | 21 +++++++++++ samples/ReactiveUIDemo/Views/BarView.axaml | 16 ++++++++ samples/ReactiveUIDemo/Views/BarView.axaml.cs | 28 ++++++++++++++ samples/ReactiveUIDemo/Views/FooView.axaml | 16 ++++++++ samples/ReactiveUIDemo/Views/FooView.axaml.cs | 28 ++++++++++++++ 16 files changed, 267 insertions(+) create mode 100644 .ncrunch/ReactiveUIDemo.v3.ncrunchproject create mode 100644 samples/ReactiveUIDemo/App.axaml create mode 100644 samples/ReactiveUIDemo/App.axaml.cs create mode 100644 samples/ReactiveUIDemo/MainWindow.axaml create mode 100644 samples/ReactiveUIDemo/MainWindow.axaml.cs create mode 100644 samples/ReactiveUIDemo/ReactiveUIDemo.csproj create mode 100644 samples/ReactiveUIDemo/ViewModels/BarViewModel.cs create mode 100644 samples/ReactiveUIDemo/ViewModels/FooViewModel.cs create mode 100644 samples/ReactiveUIDemo/ViewModels/MainWindowViewModel.cs create mode 100644 samples/ReactiveUIDemo/ViewModels/RoutedViewHostPageViewModel.cs create mode 100644 samples/ReactiveUIDemo/Views/BarView.axaml create mode 100644 samples/ReactiveUIDemo/Views/BarView.axaml.cs create mode 100644 samples/ReactiveUIDemo/Views/FooView.axaml create mode 100644 samples/ReactiveUIDemo/Views/FooView.axaml.cs diff --git a/.ncrunch/ReactiveUIDemo.v3.ncrunchproject b/.ncrunch/ReactiveUIDemo.v3.ncrunchproject new file mode 100644 index 0000000000..319cd523ce --- /dev/null +++ b/.ncrunch/ReactiveUIDemo.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/Avalonia.Desktop.slnf b/Avalonia.Desktop.slnf index 6ba05332be..3fa8e969c8 100644 --- a/Avalonia.Desktop.slnf +++ b/Avalonia.Desktop.slnf @@ -9,6 +9,7 @@ "samples\\MiniMvvm\\MiniMvvm.csproj", "samples\\SampleControls\\ControlSamples.csproj", "samples\\Sandbox\\Sandbox.csproj", + "samples\\ReactiveUIDemo\\ReactiveUIDemo.csproj", "src\\Avalonia.Base\\Avalonia.Base.csproj", "src\\Avalonia.Build.Tasks\\Avalonia.Build.Tasks.csproj", "src\\Avalonia.Controls.ColorPicker\\Avalonia.Controls.ColorPicker.csproj", diff --git a/Avalonia.sln b/Avalonia.sln index 34b5596119..63b331ec08 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -230,6 +230,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ControlCatalog.Browser", "s EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ControlCatalog.Browser.Blazor", "samples\ControlCatalog.Browser.Blazor\ControlCatalog.Browser.Blazor.csproj", "{90B08091-9BBD-4362-B712-E9F2CC62B218}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReactiveUIDemo", "samples\ReactiveUIDemo\ReactiveUIDemo.csproj", "{75C47156-C5D8-44BC-A5A7-E8657C2248D6}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -541,6 +543,10 @@ Global {90B08091-9BBD-4362-B712-E9F2CC62B218}.Debug|Any CPU.Build.0 = Debug|Any CPU {90B08091-9BBD-4362-B712-E9F2CC62B218}.Release|Any CPU.ActiveCfg = Release|Any CPU {90B08091-9BBD-4362-B712-E9F2CC62B218}.Release|Any CPU.Build.0 = Release|Any CPU + {75C47156-C5D8-44BC-A5A7-E8657C2248D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {75C47156-C5D8-44BC-A5A7-E8657C2248D6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {75C47156-C5D8-44BC-A5A7-E8657C2248D6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {75C47156-C5D8-44BC-A5A7-E8657C2248D6}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -605,6 +611,7 @@ Global {47F8530C-F19B-4B1A-B4D6-EB231522AE5D} = {86A3F706-DC3C-43C6-BE1B-B98F5BAAA268} {15B93A4C-1B46-43F6-B534-7B25B6E99932} = {9B9E3891-2366-4253-A952-D08BCEB71098} {90B08091-9BBD-4362-B712-E9F2CC62B218} = {9B9E3891-2366-4253-A952-D08BCEB71098} + {75C47156-C5D8-44BC-A5A7-E8657C2248D6} = {9B9E3891-2366-4253-A952-D08BCEB71098} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A} diff --git a/samples/ReactiveUIDemo/App.axaml b/samples/ReactiveUIDemo/App.axaml new file mode 100644 index 0000000000..dd3a39f6ac --- /dev/null +++ b/samples/ReactiveUIDemo/App.axaml @@ -0,0 +1,8 @@ + + + + + diff --git a/samples/ReactiveUIDemo/App.axaml.cs b/samples/ReactiveUIDemo/App.axaml.cs new file mode 100644 index 0000000000..4578566427 --- /dev/null +++ b/samples/ReactiveUIDemo/App.axaml.cs @@ -0,0 +1,37 @@ +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; +using ReactiveUI; +using ReactiveUIDemo.ViewModels; +using ReactiveUIDemo.Views; +using Splat; + +namespace ReactiveUIDemo +{ + public class App : Application + { + public override void Initialize() + { + AvaloniaXamlLoader.Load(this); + Locator.CurrentMutable.Register(() => new FooView(), typeof(IViewFor)); + Locator.CurrentMutable.Register(() => new BarView(), typeof(IViewFor)); + } + + public override void OnFrameworkInitializationCompleted() + { + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + desktop.MainWindow = new MainWindow(); + base.OnFrameworkInitializationCompleted(); + } + + public static int Main(string[] args) + => BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); + + public static AppBuilder BuildAvaloniaApp() + => AppBuilder.Configure() + .UsePlatformDetect() + .UseReactiveUI() + .LogToTrace(); + } +} diff --git a/samples/ReactiveUIDemo/MainWindow.axaml b/samples/ReactiveUIDemo/MainWindow.axaml new file mode 100644 index 0000000000..7775fc5a79 --- /dev/null +++ b/samples/ReactiveUIDemo/MainWindow.axaml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + diff --git a/samples/ReactiveUIDemo/MainWindow.axaml.cs b/samples/ReactiveUIDemo/MainWindow.axaml.cs new file mode 100644 index 0000000000..5bf2d476fd --- /dev/null +++ b/samples/ReactiveUIDemo/MainWindow.axaml.cs @@ -0,0 +1,22 @@ +using ReactiveUIDemo.ViewModels; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace ReactiveUIDemo +{ + public class MainWindow : Window + { + public MainWindow() + { + this.InitializeComponent(); + this.DataContext = new MainWindowViewModel(); + this.AttachDevTools(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/samples/ReactiveUIDemo/ReactiveUIDemo.csproj b/samples/ReactiveUIDemo/ReactiveUIDemo.csproj new file mode 100644 index 0000000000..94ca4ee809 --- /dev/null +++ b/samples/ReactiveUIDemo/ReactiveUIDemo.csproj @@ -0,0 +1,28 @@ + + + Exe + net6.0 + enable + + + + + + + + + + + BarView.axaml + + + FooView.axaml + + + + + + + + + diff --git a/samples/ReactiveUIDemo/ViewModels/BarViewModel.cs b/samples/ReactiveUIDemo/ViewModels/BarViewModel.cs new file mode 100644 index 0000000000..3448453d81 --- /dev/null +++ b/samples/ReactiveUIDemo/ViewModels/BarViewModel.cs @@ -0,0 +1,11 @@ +using ReactiveUI; + +namespace ReactiveUIDemo.ViewModels +{ + internal class BarViewModel : ReactiveObject, IRoutableViewModel + { + public BarViewModel(IScreen screen) => HostScreen = screen; + public string UrlPathSegment => "Bar"; + public IScreen HostScreen { get; } + } +} diff --git a/samples/ReactiveUIDemo/ViewModels/FooViewModel.cs b/samples/ReactiveUIDemo/ViewModels/FooViewModel.cs new file mode 100644 index 0000000000..1a363e18dc --- /dev/null +++ b/samples/ReactiveUIDemo/ViewModels/FooViewModel.cs @@ -0,0 +1,11 @@ +using ReactiveUI; + +namespace ReactiveUIDemo.ViewModels +{ + internal class FooViewModel : ReactiveObject, IRoutableViewModel + { + public FooViewModel(IScreen screen) => HostScreen = screen; + public string UrlPathSegment => "Foo"; + public IScreen HostScreen { get; } + } +} diff --git a/samples/ReactiveUIDemo/ViewModels/MainWindowViewModel.cs b/samples/ReactiveUIDemo/ViewModels/MainWindowViewModel.cs new file mode 100644 index 0000000000..2222137d38 --- /dev/null +++ b/samples/ReactiveUIDemo/ViewModels/MainWindowViewModel.cs @@ -0,0 +1,9 @@ +using ReactiveUI; + +namespace ReactiveUIDemo.ViewModels +{ + internal class MainWindowViewModel : ReactiveObject + { + public RoutedViewHostPageViewModel RoutedViewHost { get; } = new(); + } +} diff --git a/samples/ReactiveUIDemo/ViewModels/RoutedViewHostPageViewModel.cs b/samples/ReactiveUIDemo/ViewModels/RoutedViewHostPageViewModel.cs new file mode 100644 index 0000000000..701447cfe8 --- /dev/null +++ b/samples/ReactiveUIDemo/ViewModels/RoutedViewHostPageViewModel.cs @@ -0,0 +1,21 @@ +using ReactiveUI; + +namespace ReactiveUIDemo.ViewModels +{ + internal class RoutedViewHostPageViewModel : ReactiveObject, IScreen + { + public RoutedViewHostPageViewModel() + { + Foo = new(this); + Bar = new(this); + Router.Navigate.Execute(Foo); + } + + public RoutingState Router { get; } = new(); + public FooViewModel Foo { get; } + public BarViewModel Bar { get; } + + public void ShowFoo() => Router.Navigate.Execute(Foo); + public void ShowBar() => Router.Navigate.Execute(Bar); + } +} diff --git a/samples/ReactiveUIDemo/Views/BarView.axaml b/samples/ReactiveUIDemo/Views/BarView.axaml new file mode 100644 index 0000000000..2622245997 --- /dev/null +++ b/samples/ReactiveUIDemo/Views/BarView.axaml @@ -0,0 +1,16 @@ + + + + Bar! + + + diff --git a/samples/ReactiveUIDemo/Views/BarView.axaml.cs b/samples/ReactiveUIDemo/Views/BarView.axaml.cs new file mode 100644 index 0000000000..2fbea6de91 --- /dev/null +++ b/samples/ReactiveUIDemo/Views/BarView.axaml.cs @@ -0,0 +1,28 @@ +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using ReactiveUI; +using ReactiveUIDemo.ViewModels; + +namespace ReactiveUIDemo.Views +{ + internal partial class BarView : UserControl, IViewFor + { + public BarView() + { + InitializeComponent(); + } + + public BarViewModel? ViewModel { get; set; } + + object? IViewFor.ViewModel + { + get => ViewModel; + set => ViewModel = (BarViewModel?)value; + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/samples/ReactiveUIDemo/Views/FooView.axaml b/samples/ReactiveUIDemo/Views/FooView.axaml new file mode 100644 index 0000000000..8f73250d3b --- /dev/null +++ b/samples/ReactiveUIDemo/Views/FooView.axaml @@ -0,0 +1,16 @@ + + + + Foo! + + + diff --git a/samples/ReactiveUIDemo/Views/FooView.axaml.cs b/samples/ReactiveUIDemo/Views/FooView.axaml.cs new file mode 100644 index 0000000000..313a71044c --- /dev/null +++ b/samples/ReactiveUIDemo/Views/FooView.axaml.cs @@ -0,0 +1,28 @@ +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using ReactiveUI; +using ReactiveUIDemo.ViewModels; + +namespace ReactiveUIDemo.Views +{ + internal partial class FooView : UserControl, IViewFor + { + public FooView() + { + InitializeComponent(); + } + + public FooViewModel? ViewModel { get; set; } + + object? IViewFor.ViewModel + { + get => ViewModel; + set => ViewModel = (FooViewModel?)value; + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} From b344605c65fc3cb4227dab9b1cbd6230d0eb94be Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Tue, 6 Dec 2022 21:35:52 +0600 Subject: [PATCH 42/87] TrySetException --- src/Avalonia.Base/Rendering/Composition/Compositor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Rendering/Composition/Compositor.cs b/src/Avalonia.Base/Rendering/Composition/Compositor.cs index 6512f753fc..b4817bfe9a 100644 --- a/src/Avalonia.Base/Rendering/Composition/Compositor.cs +++ b/src/Avalonia.Base/Rendering/Composition/Compositor.cs @@ -162,7 +162,7 @@ namespace Avalonia.Rendering.Composition } catch (Exception e) { - tcs.SetResult(e); + tcs.TrySetException(e); } }); return new ValueTask(tcs.Task); From f4eeb82a58100bb23554ea40f37dbd46431151e3 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 6 Dec 2022 16:43:51 +0100 Subject: [PATCH 43/87] Actually dispose DisposeAnimationInstanceSubject. We were creating a `DisposeAnimationInstanceSubject` and then just discarding it, meaning it was never disposed. Fixes #6111 Fixes #9514 --- src/Avalonia.Base/Animation/Animators/Animator`1.cs | 3 ++- .../Animation/AnimationIterationTests.cs | 5 ++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Base/Animation/Animators/Animator`1.cs b/src/Avalonia.Base/Animation/Animators/Animator`1.cs index 8765cfb4c9..b5d1feb4a7 100644 --- a/src/Avalonia.Base/Animation/Animators/Animator`1.cs +++ b/src/Avalonia.Base/Animation/Animators/Animator`1.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Reactive.Disposables; using System.Reactive.Linq; using Avalonia.Animation.Utils; using Avalonia.Collections; @@ -39,7 +40,7 @@ namespace Avalonia.Animation.Animators VerifyConvertKeyFrames(); var subject = new DisposeAnimationInstanceSubject(this, animation, control, clock, onComplete); - return match.Subscribe(subject); + return new CompositeDisposable(match.Subscribe(subject), subject); } protected T InterpolationHandler(double animationTime, T neutralValue) diff --git a/tests/Avalonia.Base.UnitTests/Animation/AnimationIterationTests.cs b/tests/Avalonia.Base.UnitTests/Animation/AnimationIterationTests.cs index ca24b95e65..58e908aca9 100644 --- a/tests/Avalonia.Base.UnitTests/Animation/AnimationIterationTests.cs +++ b/tests/Avalonia.Base.UnitTests/Animation/AnimationIterationTests.cs @@ -181,7 +181,7 @@ namespace Avalonia.Base.UnitTests.Animation Assert.Equal(border.Width, 300d); } - [Fact(Skip = "See #6111")] + [Fact] public void Dispose_Subscription_Should_Stop_Animation() { var keyframe1 = new KeyFrame() @@ -310,7 +310,7 @@ namespace Avalonia.Base.UnitTests.Animation Assert.True(animationRun.IsCompleted); } - [Fact(Skip = "See #6111")] + [Fact] public void Cancellation_Should_Stop_Animation() { var keyframe1 = new KeyFrame() @@ -372,7 +372,6 @@ namespace Avalonia.Base.UnitTests.Animation clock.Step(TimeSpan.FromSeconds(1)); clock.Step(TimeSpan.FromSeconds(2)); clock.Step(TimeSpan.FromSeconds(3)); - //Assert.Equal(2, propertyChangedCount); animationRun.Wait(); From 2a5dceecbb2d1437eab437e6b3049e86646211dd Mon Sep 17 00:00:00 2001 From: Glen Nicol Date: Fri, 2 Dec 2022 10:24:23 -0800 Subject: [PATCH 44/87] Feature: Double click expand/collapse treeview and numpad keyboard shortcuts --- src/Avalonia.Controls/TreeView.cs | 20 + src/Avalonia.Controls/TreeViewItem.cs | 124 +++- .../Controls/TreeViewItem.xaml | 1 + .../Controls/TreeViewItem.xaml | 1 + .../TreeViewTests.cs | 599 +++++++++++++++++- tests/Avalonia.UnitTests/MouseTestHelper.cs | 16 +- 6 files changed, 738 insertions(+), 23 deletions(-) diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs index 2fa4a02fa2..ffdd32f95c 100644 --- a/src/Avalonia.Controls/TreeView.cs +++ b/src/Avalonia.Controls/TreeView.cs @@ -179,6 +179,26 @@ namespace Avalonia.Controls } } + /// + /// Collapse the specified all descendent s. + /// + /// The item to collapse. + public void CollapseSubTree(TreeViewItem item) + { + item.IsExpanded = false; + + if (item.Presenter?.Panel != null) + { + foreach (var child in item.Presenter.Panel.Children) + { + if (child is TreeViewItem treeViewItem) + { + CollapseSubTree(treeViewItem); + } + } + } + } + /// /// Selects all items in the . /// diff --git a/src/Avalonia.Controls/TreeViewItem.cs b/src/Avalonia.Controls/TreeViewItem.cs index 9bfcf5adfa..18245bd682 100644 --- a/src/Avalonia.Controls/TreeViewItem.cs +++ b/src/Avalonia.Controls/TreeViewItem.cs @@ -1,3 +1,5 @@ +using System; +using System.Collections.Generic; using System.Linq; using Avalonia.Controls.Generators; using Avalonia.Controls.Metadata; @@ -166,30 +168,94 @@ namespace Avalonia.Controls { if (!e.Handled) { - switch (e.Key) + Func? handler = + e.Key switch + { + Key.Left => ApplyToItemOrRecursivelyIfCtrl(FocusAwareCollapseItem, e.KeyModifiers), + Key.Right => ApplyToItemOrRecursivelyIfCtrl(ExpandItem, e.KeyModifiers), + Key.Enter or Key.Space => ApplyToItemOrRecursivelyIfCtrl(IsExpanded ? CollapseItem : ExpandItem, e.KeyModifiers), + + // do not handle CTRL with numpad keys + Key.Subtract => FocusAwareCollapseItem, + Key.Add => ExpandItem, + Key.Divide => ApplyToSubtree(CollapseItem), + Key.Multiply => ApplyToSubtree(ExpandItem), + _ => null, + }; + + if (handler is not null) + { + e.Handled = handler(this); + } + + // NOTE: these local functions do not use the TreeView.Expand/CollapseSubtree + // function because we want to know if any items were in fact expanded to set the + // event handled status. Also the handling here avoids a potential infinite recursion/stack overflow. + static Func ApplyToSubtree(Func f) + { + // Calling toList enumerates all items before applying functions. This avoids a + // potential infinite loop if there is an infinite tree (the control catalog is + // lazily infinite). But also means a lazily loaded tree will not be expanded completely. + return t => SubTree(t) + .ToList() + .Select(treeViewItem => f(treeViewItem)) + .Aggregate(false, (p, c) => p || c); + } + + static Func ApplyToItemOrRecursivelyIfCtrl(Func f, KeyModifiers keyModifiers) + { + if (keyModifiers.HasAllFlags(KeyModifiers.Control)) + { + return ApplyToSubtree(f); + } + + return f; + } + + static bool ExpandItem(TreeViewItem treeViewItem) + { + if (treeViewItem.ItemCount > 0 && !treeViewItem.IsExpanded) + { + treeViewItem.IsExpanded = true; + return true; + } + + return false; + } + + static bool CollapseItem(TreeViewItem treeViewItem) { - case Key.Right: - if (Items != null && Items.Cast().Any() && !IsExpanded) + if (treeViewItem.ItemCount > 0 && treeViewItem.IsExpanded) + { + treeViewItem.IsExpanded = false; + return true; + } + + return false; + } + + static bool FocusAwareCollapseItem(TreeViewItem treeViewItem) + { + if (treeViewItem.ItemCount > 0 && treeViewItem.IsExpanded) + { + if (treeViewItem.IsFocused) { - IsExpanded = true; - e.Handled = true; + treeViewItem.IsExpanded = false; } - break; - - case Key.Left: - if (Items is not null && Items.Cast().Any() && IsExpanded) + else { - if (IsFocused) - { - IsExpanded = false; - } - else - { - FocusManager.Instance?.Focus(this, NavigationMethod.Directional); - } - e.Handled = true; + FocusManager.Instance?.Focus(treeViewItem, NavigationMethod.Directional); } - break; + + return true; + } + + return false; + } + + static IEnumerable SubTree(TreeViewItem treeViewItem) + { + return new[] { treeViewItem }.Concat(treeViewItem.LogicalChildren.OfType().SelectMany(child => SubTree(child))); } } @@ -198,8 +264,19 @@ namespace Avalonia.Controls protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { + if (_header is InputElement previousInputMethod) + { + previousInputMethod.DoubleTapped -= HeaderDoubleTapped; + } + _header = e.NameScope.Find("PART_Header"); _templateApplied = true; + + if (_header is InputElement im) + { + im.DoubleTapped += HeaderDoubleTapped; + } + if (_deferredBringIntoViewFlag) { _deferredBringIntoViewFlag = false; @@ -220,6 +297,15 @@ namespace Avalonia.Controls return logical != null ? result : @default; } + private void HeaderDoubleTapped(object? sender, TappedEventArgs e) + { + if (ItemCount > 0) + { + IsExpanded = !IsExpanded; + e.Handled = true; + } + } + private void OnParentChanged(AvaloniaPropertyChangedEventArgs e) { if (!((ILogical)this).IsAttachedToLogicalTree && e.NewValue is null) diff --git a/src/Avalonia.Themes.Fluent/Controls/TreeViewItem.xaml b/src/Avalonia.Themes.Fluent/Controls/TreeViewItem.xaml index 9171791a0f..0bed388ca4 100644 --- a/src/Avalonia.Themes.Fluent/Controls/TreeViewItem.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/TreeViewItem.xaml @@ -75,6 +75,7 @@ MinHeight="{TemplateBinding MinHeight}" TemplatedControl.IsTemplateFocusTarget="True"> 0) + { + Assert.False(container.IsExpanded); + foreach (var c in node.Children) + { + AssertEachItemWithChildrenIsCollapsed(c); + } + } + else + { + Assert.True(container.IsExpanded); + } + } + } + } + + [Fact] + public void Enter_Key_Should_Expand_TreeViewItem() + { + using (Application()) + { + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree, + }; + + var visualRoot = new TestRoot(); + visualRoot.Child = target; + + CreateNodeDataTemplate(target); + ApplyTemplates(target); + CollapseAll(target); // NOTE this line + + var item = tree[0]; + var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item); + + Assert.NotNull(container); + Assert.False(container.IsExpanded); + var header = container.Header as Interactive; + Assert.NotNull(header); + + container.RaiseEvent(new KeyEventArgs + { + RoutedEvent = InputElement.KeyDownEvent, + Key = Key.Enter, + }); + + Assert.True(container.IsExpanded); + } + } + + [Fact] + public void Enter_plus_Ctrl_Key_Should_Expand_TreeViewItem_Recursively() + { + using (Application()) + { + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree, + }; + + var visualRoot = new TestRoot(); + visualRoot.Child = target; + + CreateNodeDataTemplate(target); + ApplyTemplates(target); + CollapseAll(target); // NOTE this line + + var item = tree[0]; + var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item); + + Assert.NotNull(container); + Assert.False(container.IsExpanded); + var header = container.Header as Interactive; + Assert.NotNull(header); + + container.RaiseEvent(new KeyEventArgs + { + RoutedEvent = InputElement.KeyDownEvent, + Key = Key.Enter, + KeyModifiers = KeyModifiers.Control, + }); + + Assert.True(container.IsExpanded); + + AssertEachItemWithChildrenIsExpanded(item); + + void AssertEachItemWithChildrenIsExpanded(Node node) + { + var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(node); + Assert.NotNull(container); + if (node.Children?.Count > 0) + { + Assert.True(container.IsExpanded); + foreach (var c in node.Children) + { + AssertEachItemWithChildrenIsExpanded(c); + } + } + else + { + Assert.False(container.IsExpanded); + } + } + } + } + + [Fact] + public void Space_Key_Should_Collapse_TreeViewItem() + { + using (Application()) + { + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree, + }; + + var visualRoot = new TestRoot(); + visualRoot.Child = target; + + CreateNodeDataTemplate(target); + ApplyTemplates(target); + ExpandAll(target); // NOTE this line + + var item = tree[0]; + var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item); + + Assert.NotNull(container); + Assert.True(container.IsExpanded); + var header = container.Header as Interactive; + Assert.NotNull(header); + + container.RaiseEvent(new KeyEventArgs + { + RoutedEvent = InputElement.KeyDownEvent, + Key = Key.Enter, + }); + + Assert.False(container.IsExpanded); + } + } + + [Fact] + public void Space_plus_Ctrl_Key_Should_Collapse_TreeViewItem_Recursively() + { + using (Application()) + { + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree, + }; + + var visualRoot = new TestRoot(); + visualRoot.Child = target; + + CreateNodeDataTemplate(target); + ApplyTemplates(target); + ExpandAll(target); // NOTE this line + + var item = tree[0]; + var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item); + + Assert.NotNull(container); + Assert.True(container.IsExpanded); + var header = container.Header as Interactive; + Assert.NotNull(header); + + container.RaiseEvent(new KeyEventArgs + { + RoutedEvent = InputElement.KeyDownEvent, + Key = Key.Enter, + KeyModifiers = KeyModifiers.Control, + }); + + Assert.False(container.IsExpanded); + + AssertEachItemWithChildrenIsCollapsed(item); + + void AssertEachItemWithChildrenIsCollapsed(Node node) + { + var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(node); + Assert.NotNull(container); + if (node.Children?.Count > 0) + { + Assert.False(container.IsExpanded); + foreach (var c in node.Children) + { + AssertEachItemWithChildrenIsCollapsed(c); + } + } + else + { + Assert.True(container.IsExpanded); + } + } + } + } + + [Fact] + public void Space_Key_Should_Expand_TreeViewItem() + { + using (Application()) + { + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree, + }; + + var visualRoot = new TestRoot(); + visualRoot.Child = target; + + CreateNodeDataTemplate(target); + ApplyTemplates(target); + CollapseAll(target); // NOTE this line + + var item = tree[0]; + var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item); + + Assert.NotNull(container); + Assert.False(container.IsExpanded); + var header = container.Header as Interactive; + Assert.NotNull(header); + + container.RaiseEvent(new KeyEventArgs + { + RoutedEvent = InputElement.KeyDownEvent, + Key = Key.Enter, + }); + + Assert.True(container.IsExpanded); + } + } + + [Fact] + public void Space_plus_Ctrl_Key_Should_Expand_TreeViewItem_Recursively() + { + using (Application()) + { + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree, + }; + + var visualRoot = new TestRoot(); + visualRoot.Child = target; + + CreateNodeDataTemplate(target); + ApplyTemplates(target); + CollapseAll(target); // NOTE this line + + var item = tree[0]; + var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item); + + Assert.NotNull(container); + Assert.False(container.IsExpanded); + var header = container.Header as Interactive; + Assert.NotNull(header); + + container.RaiseEvent(new KeyEventArgs + { + RoutedEvent = InputElement.KeyDownEvent, + Key = Key.Enter, + KeyModifiers = KeyModifiers.Control, + }); + + Assert.True(container.IsExpanded); + + AssertEachItemWithChildrenIsExpanded(item); + + void AssertEachItemWithChildrenIsExpanded(Node node) + { + var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(node); + Assert.NotNull(container); + if (node.Children?.Count > 0) + { + Assert.True(container.IsExpanded); + foreach (var c in node.Children) + { + AssertEachItemWithChildrenIsExpanded(c); + } + } + else + { + Assert.False(container.IsExpanded); + } + } + } + } + + [Fact] + public void Numpad_Star_Should_Expand_All_Children_Recursively() + { + using (Application()) + { + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree, + }; + + var visualRoot = new TestRoot(); + visualRoot.Child = target; + + CreateNodeDataTemplate(target); + ApplyTemplates(target); + CollapseAll(target); + + var item = tree[0]; + var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item); + + Assert.NotNull(container); + container.RaiseEvent(new KeyEventArgs + { + RoutedEvent = InputElement.KeyDownEvent, + Key = Key.Multiply, + }); + + AssertEachItemWithChildrenIsExpanded(item); + + void AssertEachItemWithChildrenIsExpanded(Node node) + { + var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(node); + Assert.NotNull(container); + if (node.Children?.Count > 0) + { + Assert.True(container.IsExpanded); + foreach (var c in node.Children) + { + AssertEachItemWithChildrenIsExpanded(c); + } + } + else + { + Assert.False(container.IsExpanded); + } + } + } + } + + [Fact] + public void Numpad_Slash_Should_Collapse_All_Children_Recursively() + { + using (Application()) + { + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree, + }; + + var visualRoot = new TestRoot(); + visualRoot.Child = target; + + CreateNodeDataTemplate(target); + ApplyTemplates(target); + ExpandAll(target); + + var item = tree[0]; + var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item); + + Assert.NotNull(container); + container.RaiseEvent(new KeyEventArgs + { + RoutedEvent = InputElement.KeyDownEvent, + Key = Key.Divide, + }); + + AssertEachItemWithChildrenIsCollapsed(item); + + void AssertEachItemWithChildrenIsCollapsed(Node node) + { + var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(node); + Assert.NotNull(container); + if (node.Children?.Count > 0) + { + Assert.False(container.IsExpanded); + foreach (var c in node.Children) + { + AssertEachItemWithChildrenIsCollapsed(c); + } + } + else + { + Assert.True(container.IsExpanded); + } + } + } + } + [Fact] public void Setting_SelectedItem_Should_Set_Container_Selected() { @@ -1313,10 +1894,14 @@ namespace Avalonia.Controls.UnitTests { Children = { - new ContentPresenter + new Border { - Name = "PART_HeaderPresenter", - [~ContentPresenter.ContentProperty] = parent[~TreeViewItem.HeaderProperty], + Name = "PART_Header", + Child = new ContentPresenter + { + Name = "PART_HeaderPresenter", + [~ContentPresenter.ContentProperty] = parent[~TreeViewItem.HeaderProperty], + }.RegisterInNameScope(scope) }.RegisterInNameScope(scope), new ItemsPresenter { @@ -1335,6 +1920,14 @@ namespace Avalonia.Controls.UnitTests } } + private void CollapseAll(TreeView tree) + { + foreach (var i in tree.ItemContainerGenerator.Containers) + { + tree.CollapseSubTree((TreeViewItem)i.ContainerControl); + } + } + private List ExtractItemHeader(TreeView tree, int level) { return ExtractItemContent(tree.Presenter.Panel, 0, level) diff --git a/tests/Avalonia.UnitTests/MouseTestHelper.cs b/tests/Avalonia.UnitTests/MouseTestHelper.cs index c9e4274d15..d63327239b 100644 --- a/tests/Avalonia.UnitTests/MouseTestHelper.cs +++ b/tests/Avalonia.UnitTests/MouseTestHelper.cs @@ -65,6 +65,7 @@ namespace Avalonia.UnitTests } public void Move(Interactive target, in Point position, KeyModifiers modifiers = default) => Move(target, target, position, modifiers); + public void Move(Interactive target, Interactive source, in Point position, KeyModifiers modifiers = default) { target.RaiseEvent(new PointerEventArgs(InputElement.PointerMovedEvent, source, _pointer, (Visual)target, position, @@ -98,13 +99,26 @@ namespace Avalonia.UnitTests public void Click(Interactive target, MouseButton button = MouseButton.Left, Point position = default, KeyModifiers modifiers = default) => Click(target, target, button, position, modifiers); + public void Click(Interactive target, Interactive source, MouseButton button = MouseButton.Left, Point position = default, KeyModifiers modifiers = default) { Down(target, source, button, position, modifiers); Up(target, source, button, position, modifiers); } - + + public void DoubleClick(Interactive target, MouseButton button = MouseButton.Left, Point position = default, + KeyModifiers modifiers = default) + => DoubleClick(target, target, button, position, modifiers); + + public void DoubleClick(Interactive target, Interactive source, MouseButton button = MouseButton.Left, + Point position = default, KeyModifiers modifiers = default) + { + Down(target, source, button, position, modifiers, clickCount: 1); + Up(target, source, button, position, modifiers); + Down(target, source, button, position, modifiers, clickCount: 2); + } + public void Enter(Interactive target) { target.RaiseEvent(new PointerEventArgs(InputElement.PointerEnteredEvent, target, _pointer, (Visual)target, default, From 6b8b5d78f491b35a40650d66727671614b4b64c2 Mon Sep 17 00:00:00 2001 From: Daniil Pavliuchyk Date: Tue, 6 Dec 2022 19:10:37 +0200 Subject: [PATCH 45/87] Rework focus so we don't return focus to the lastly enabled element --- src/Avalonia.Base/Input/InputElement.cs | 17 ++--------- .../Input/InputElement_Focus.cs | 29 +++++++++++++++++++ 2 files changed, 31 insertions(+), 15 deletions(-) diff --git a/src/Avalonia.Base/Input/InputElement.cs b/src/Avalonia.Base/Input/InputElement.cs index 60d8ef87a3..fa755277cc 100644 --- a/src/Avalonia.Base/Input/InputElement.cs +++ b/src/Avalonia.Base/Input/InputElement.cs @@ -199,7 +199,6 @@ namespace Avalonia.Input private bool _isFocusVisible; private bool _isPointerOver; private GestureRecognizerCollection? _gestureRecognizers; - private bool _restoreFocus; /// /// Initializes static members of the class. @@ -444,21 +443,9 @@ namespace Avalonia.Input SetAndRaise(IsEffectivelyEnabledProperty, ref _isEffectivelyEnabled, value); PseudoClasses.Set(":disabled", !value); - if (!IsEffectivelyEnabled) + if (!IsEffectivelyEnabled && FocusManager.Instance?.Current == this) { - if (FocusManager.Instance?.Current == this) - { - _restoreFocus = true; - FocusManager.Instance?.Focus(null); - } - else - { - _restoreFocus = false; - } - } - else if (IsEffectivelyEnabled && _restoreFocus) - { - FocusManager.Instance?.Focus(this); + FocusManager.Instance?.Focus(null); } } } diff --git a/tests/Avalonia.Base.UnitTests/Input/InputElement_Focus.cs b/tests/Avalonia.Base.UnitTests/Input/InputElement_Focus.cs index 2d8ee62ef2..e36ce21009 100644 --- a/tests/Avalonia.Base.UnitTests/Input/InputElement_Focus.cs +++ b/tests/Avalonia.Base.UnitTests/Input/InputElement_Focus.cs @@ -1,5 +1,7 @@ +using System; using Avalonia.Controls; using Avalonia.Input; +using Avalonia.Interactivity; using Avalonia.UnitTests; using Xunit; @@ -25,6 +27,33 @@ namespace Avalonia.Base.UnitTests.Input } } + [Fact] + public void Focus_Should_Not_Get_Restored_To_Enabled_Control() + { + using (UnitTestApplication.Start(TestServices.RealFocus)) + { + var sp = new StackPanel(); + Button target = new Button(); + Button target1 = new Button(); + target.Click += (s, e) => target.IsEnabled = false; + target1.Click += (s, e) => target.IsEnabled = true; + sp.Children.Add(target); + sp.Children.Add(target1); + var root = new TestRoot + { + Child = sp + }; + + target.Focus(); + target.RaiseEvent(new RoutedEventArgs(AccessKeyHandler.AccessKeyPressedEvent)); + Assert.False(target.IsEnabled); + Assert.False(target.IsFocused); + target1.RaiseEvent(new RoutedEventArgs(AccessKeyHandler.AccessKeyPressedEvent)); + Assert.True(target.IsEnabled); + Assert.False(target.IsFocused); + } + } + [Fact] public void Focus_Should_Be_Cleared_When_Control_Is_Removed_From_VisualTree() { From 5cea410df1d9c6d9b2626a1d0126f9d197dd532a Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 7 Dec 2022 00:32:55 +0600 Subject: [PATCH 46/87] Addressed review --- .../Rendering/DeferredRenderer.cs | 2 +- src/Avalonia.Base/Rendering/RenderLoop.cs | 3 +- ...cs => AvaloniaNativeGlPlatformGraphics.cs} | 0 src/Avalonia.OpenGL/OpenGlException.cs | 19 +-- src/Avalonia.X11/X11Platform.cs | 4 +- .../WinUiCompositedWindowSurface.cs | 109 +----------------- 6 files changed, 16 insertions(+), 121 deletions(-) rename src/Avalonia.Native/{AvaloniaNativePlatformOpenGlInterface.cs => AvaloniaNativeGlPlatformGraphics.cs} (100%) diff --git a/src/Avalonia.Base/Rendering/DeferredRenderer.cs b/src/Avalonia.Base/Rendering/DeferredRenderer.cs index 787f08515a..05aa1d1ea4 100644 --- a/src/Avalonia.Base/Rendering/DeferredRenderer.cs +++ b/src/Avalonia.Base/Rendering/DeferredRenderer.cs @@ -282,7 +282,7 @@ namespace Avalonia.Rendering } catch (Exception e) { - tcs.SetResult(e); + tcs.TrySetException(e); } }); return new ValueTask(tcs.Task); diff --git a/src/Avalonia.Base/Rendering/RenderLoop.cs b/src/Avalonia.Base/Rendering/RenderLoop.cs index 6f3d7bce16..5a08bfc6a1 100644 --- a/src/Avalonia.Base/Rendering/RenderLoop.cs +++ b/src/Avalonia.Base/Rendering/RenderLoop.cs @@ -101,8 +101,7 @@ namespace Avalonia.Rendering lock (_items) { _itemsCopy.Clear(); - foreach (var i in _items) - _itemsCopy.Add(i); + _itemsCopy.AddRange(_items); } foreach (IRenderLoopTask item in _itemsCopy) diff --git a/src/Avalonia.Native/AvaloniaNativePlatformOpenGlInterface.cs b/src/Avalonia.Native/AvaloniaNativeGlPlatformGraphics.cs similarity index 100% rename from src/Avalonia.Native/AvaloniaNativePlatformOpenGlInterface.cs rename to src/Avalonia.Native/AvaloniaNativeGlPlatformGraphics.cs diff --git a/src/Avalonia.OpenGL/OpenGlException.cs b/src/Avalonia.OpenGL/OpenGlException.cs index 4c73d7e8ef..efe305dba5 100644 --- a/src/Avalonia.OpenGL/OpenGlException.cs +++ b/src/Avalonia.OpenGL/OpenGlException.cs @@ -18,28 +18,33 @@ namespace Avalonia.OpenGL public static OpenGlException GetFormattedException(string funcName, EglInterface egl) { - return GetFormattedException(funcName, egl.GetError()); + return GetFormattedEglException(funcName, egl.GetError()); } public static OpenGlException GetFormattedException(string funcName, GlInterface gl) { - return GetFormattedException(funcName, gl.GetError()); + var err = gl.GetError(); + return GetFormattedException(funcName, (GlErrors)err, err); } public static OpenGlException GetFormattedEglException(string funcName, int errorCode) => - GetFormattedException(funcName, errorCode); + GetFormattedException(funcName, (EglErrors)errorCode,errorCode); - private static OpenGlException GetFormattedException(string funcName, int errorCode) + private static OpenGlException GetFormattedException(string funcName, T errorCode, int intErrorCode) where T : struct, Enum { try { - string errorName = Enum.GetName(typeof(T), errorCode); +#if NET6_0_OR_GREATER + var errorName = Enum.GetName(errorCode); +#else + var errorName = Enum.GetName(typeof(T), errorCode); +#endif return new OpenGlException( - $"{funcName} failed with error {errorName} (0x{errorCode.ToString("X")})", errorCode); + $"{funcName} failed with error {errorName} (0x{errorCode.ToString("X")})", intErrorCode); } catch (ArgumentException) { - return new OpenGlException($"{funcName} failed with error 0x{errorCode.ToString("X")}", errorCode); + return new OpenGlException($"{funcName} failed with error 0x{errorCode.ToString("X")}", intErrorCode); } } } diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index ca88b5188e..e44b5ded14 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -103,9 +103,7 @@ namespace Avalonia.X11 } var gl = AvaloniaLocator.Current.GetService(); - if (gl != null) - AvaloniaLocator.CurrentMutable.Bind().ToConstant(gl); - + if (options.UseCompositor) Compositor = new Compositor(AvaloniaLocator.Current.GetService()!, gl); else diff --git a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositedWindowSurface.cs b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositedWindowSurface.cs index c4918be70c..9ee024f9ad 100644 --- a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositedWindowSurface.cs +++ b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositedWindowSurface.cs @@ -200,111 +200,4 @@ namespace Avalonia.Win32.WinRT.Composition public double Scaling => _scaling; } } -} -/* - internal class WinUiCompositedWindowSurface : EglGlPlatformSurfaceBase, IBlurHost, IDisposable - { - private readonly WinUICompositorConnection _connection; - private EglPlatformGraphics _egl; - private readonly EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo _info; - private IRef _window; - private BlurEffect _blurEffect; - - public WinUiCompositedWindowSurface(WinUICompositorConnection connection, EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo info) : base() - { - _connection = connection; - _egl = connection.Egl; - _info = info; - } - - public override IGlPlatformSurfaceRenderTarget CreateGlRenderTarget(IGlContext context) - { - var egl = (EglContext)context; - using (egl.EnsureCurrent()) - { - if (_window?.Item == null) - { - _window = RefCountable.Create(_connection.CreateWindow(_info.Handle)); - _window.Item.SetBlur(_blurEffect); - } - - return new CompositionRenderTarget(egl, _window, _info); - } - } - - class CompositionRenderTarget : EglPlatformSurfaceRenderTargetBase - { - private readonly IRef _window; - private readonly EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo _info; - - public CompositionRenderTarget(EglContext context, - IRef window, - EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo info) - : base(context) - { - _window = window.Clone(); - _info = info; - _window.Item.ResizeIfNeeded(_info.Size); - } - - public override IGlPlatformSurfaceRenderingSession BeginDrawCore() - { - var contextLock = Context.EnsureCurrent(); - IUnknown texture = null; - EglSurface surface = null; - IDisposable transaction = null; - var success = false; - try - { - if (_window?.Item == null) - throw new ObjectDisposedException(GetType().FullName); - - var size = _info.Size; - transaction = _window.Item.BeginTransaction(); - _window.Item.ResizeIfNeeded(size); - texture = _window.Item.BeginDrawToTexture(out var offset); - - surface = ((AngleWin32EglDisplay) Context.Display).WrapDirect3D11Texture(texture.GetNativeIntPtr(), - offset.X, offset.Y, size.Width, size.Height); - - var res = base.BeginDraw(surface, _info, () => - { - surface?.Dispose(); - texture?.Dispose(); - _window.Item.EndDraw(); - transaction?.Dispose(); - contextLock?.Dispose(); - }, true); - success = true; - return res; - } - finally - { - if (!success) - { - surface?.Dispose(); - texture?.Dispose(); - transaction?.Dispose(); - contextLock.Dispose(); - } - } - } - } - - public void SetBlur(BlurEffect blurEffect) - { - _blurEffect = blurEffect; - _window?.Item?.SetBlur(blurEffect); - } - - public void Dispose() - { - using (_egl.Display.Lock()) - { - _window?.Dispose(); - _window = null; - } - } - } -} -*/ +} \ No newline at end of file From 58ee997bb7eb26af6e79579d38c0c078747c8425 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 7 Dec 2022 00:40:29 +0600 Subject: [PATCH 47/87] Block context creation for non-supporting displays --- src/Avalonia.OpenGL/Egl/EglDisplay.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Avalonia.OpenGL/Egl/EglDisplay.cs b/src/Avalonia.OpenGL/Egl/EglDisplay.cs index 59d8bcf350..eea2587587 100644 --- a/src/Avalonia.OpenGL/Egl/EglDisplay.cs +++ b/src/Avalonia.OpenGL/Egl/EglDisplay.cs @@ -51,6 +51,9 @@ namespace Avalonia.OpenGL.Egl public EglInterface EglInterface => _egl; public EglContext CreateContext(EglContextOptions options) { + if (SingleContext && _contexts.Any()) + throw new OpenGlException("This EGLDisplay can only have one active context"); + options ??= new EglContextOptions(); lock (_lock) { From 0f8b1d6fa387d2f6c9f64ea24506764cc73d46cb Mon Sep 17 00:00:00 2001 From: Adir Hudayfi Date: Tue, 6 Dec 2022 21:25:40 +0200 Subject: [PATCH 48/87] Fixed key frame animations not getting properly cleaned up --- .../Composition/Animations/KeyFrameAnimationInstance.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimationInstance.cs b/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimationInstance.cs index 570c6a6d07..570b852108 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimationInstance.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimationInstance.cs @@ -177,6 +177,7 @@ namespace Avalonia.Rendering.Composition.Animations public override void Deactivate() { TargetObject.Compositor.RemoveFromClock(this); + _finished = false; base.Deactivate(); } } From 79e5db3f4044f448f9932ddea854f8148b5020e6 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Wed, 7 Dec 2022 09:22:16 +0100 Subject: [PATCH 49/87] fix: Address review --- .../Internal/ManagedFileChooserFilterViewModel.cs | 2 +- src/Avalonia.Remote.Protocol/MetsysBson.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Dialogs/Internal/ManagedFileChooserFilterViewModel.cs b/src/Avalonia.Dialogs/Internal/ManagedFileChooserFilterViewModel.cs index 6da7c68a75..8389a25386 100644 --- a/src/Avalonia.Dialogs/Internal/ManagedFileChooserFilterViewModel.cs +++ b/src/Avalonia.Dialogs/Internal/ManagedFileChooserFilterViewModel.cs @@ -19,7 +19,7 @@ namespace Avalonia.Dialogs.Internal } _patterns = filter.Patterns? - .Select(e => new Regex(Regex.Escape(e).Replace(@"\*", ".*").Replace(@"\?", "."), RegexOptions.Singleline | RegexOptions.IgnoreCase | RegexOptions.Compiled)) + .Select(e => new Regex(Regex.Escape(e).Replace(@"\*", ".*").Replace(@"\?", "."), RegexOptions.Singleline | RegexOptions.IgnoreCase)) .ToArray(); } diff --git a/src/Avalonia.Remote.Protocol/MetsysBson.cs b/src/Avalonia.Remote.Protocol/MetsysBson.cs index 2ebeda8403..c0263b3518 100644 --- a/src/Avalonia.Remote.Protocol/MetsysBson.cs +++ b/src/Avalonia.Remote.Protocol/MetsysBson.cs @@ -1370,7 +1370,7 @@ namespace Metsys.Bson var pattern = ReadName(); var optionsString = ReadName(); - var options = RegexOptions.None; + var options = RegexOptions.Compiled; if (optionsString.Contains('e')) options = options | RegexOptions.ECMAScript; if (optionsString.Contains('i')) options = options | RegexOptions.IgnoreCase; if (optionsString.Contains('l')) options = options | RegexOptions.CultureInvariant; @@ -1379,7 +1379,7 @@ namespace Metsys.Bson if (optionsString.Contains('w')) options = options | RegexOptions.IgnorePatternWhitespace; if (optionsString.Contains('x')) options = options | RegexOptions.ExplicitCapture; - return new Regex(pattern, options | RegexOptions.Compiled); + return new Regex(pattern, options); } private Types ReadType() From 19ed6ec05f8d472e06efcc8bf1d99f07bff40566 Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Wed, 7 Dec 2022 10:03:16 +0000 Subject: [PATCH 50/87] addressed reveiw --- .../PullGestureRecognizer.cs | 10 +- .../RefreshCompletionDeferral.cs | 36 ++++++ .../RefreshRequestedEventArgs.cs | 42 +++++++ .../PullToRefresh/RefreshVisualizer.cs | 110 ++---------------- .../RefreshVisualizerOrientation.cs | 13 +++ .../PullToRefresh/RefreshVisualizerState.cs | 14 +++ ...ScrollViewerIRefreshInfoProviderAdapter.cs | 4 +- .../Controls/RefreshContainer.xaml | 2 +- .../Controls/RefreshContainer.xaml | 2 +- 9 files changed, 122 insertions(+), 111 deletions(-) rename src/Avalonia.Base/Input/{ => GestureRecognizers}/PullGestureRecognizer.cs (93%) create mode 100644 src/Avalonia.Controls/PullToRefresh/RefreshCompletionDeferral.cs create mode 100644 src/Avalonia.Controls/PullToRefresh/RefreshRequestedEventArgs.cs create mode 100644 src/Avalonia.Controls/PullToRefresh/RefreshVisualizerOrientation.cs create mode 100644 src/Avalonia.Controls/PullToRefresh/RefreshVisualizerState.cs diff --git a/src/Avalonia.Base/Input/PullGestureRecognizer.cs b/src/Avalonia.Base/Input/GestureRecognizers/PullGestureRecognizer.cs similarity index 93% rename from src/Avalonia.Base/Input/PullGestureRecognizer.cs rename to src/Avalonia.Base/Input/GestureRecognizers/PullGestureRecognizer.cs index bbbded44fa..fedd07ec32 100644 --- a/src/Avalonia.Base/Input/PullGestureRecognizer.cs +++ b/src/Avalonia.Base/Input/GestureRecognizers/PullGestureRecognizer.cs @@ -60,9 +60,9 @@ namespace Avalonia.Input public void PointerMoved(PointerEventArgs e) { - if (_tracking == e.Pointer) + if (_tracking == e.Pointer && _target is Visual visual) { - var currentPosition = e.GetPosition(_target); + var currentPosition = e.GetPosition(visual); _actions!.Capture(e.Pointer, this); Vector delta = default; @@ -100,13 +100,13 @@ namespace Avalonia.Input public void PointerPressed(PointerPressedEventArgs e) { - if (_target != null && (e.Pointer.Type == PointerType.Touch || e.Pointer.Type == PointerType.Pen)) + if (_target != null && _target is Visual visual && (e.Pointer.Type == PointerType.Touch || e.Pointer.Type == PointerType.Pen)) { - var position = e.GetPosition(_target); + var position = e.GetPosition(visual); var canPull = false; - var bounds = _target.Bounds; + var bounds = visual.Bounds; switch (PullDirection) { diff --git a/src/Avalonia.Controls/PullToRefresh/RefreshCompletionDeferral.cs b/src/Avalonia.Controls/PullToRefresh/RefreshCompletionDeferral.cs new file mode 100644 index 0000000000..a18b3c2934 --- /dev/null +++ b/src/Avalonia.Controls/PullToRefresh/RefreshCompletionDeferral.cs @@ -0,0 +1,36 @@ +using System; +using System.Threading; + +namespace Avalonia.Controls +{ + /// + /// Deferral class for notify that a work done in RefreshRequested event is done. + /// + public class RefreshCompletionDeferral + { + private Action _deferredAction; + private int _deferCount; + + public RefreshCompletionDeferral(Action deferredAction) + { + _deferredAction = deferredAction; + } + + public void Complete() + { + Interlocked.Decrement(ref _deferCount); + + if (_deferCount == 0) + { + _deferredAction?.Invoke(); + } + } + + public RefreshCompletionDeferral Get() + { + Interlocked.Increment(ref _deferCount); + + return this; + } + } +} diff --git a/src/Avalonia.Controls/PullToRefresh/RefreshRequestedEventArgs.cs b/src/Avalonia.Controls/PullToRefresh/RefreshRequestedEventArgs.cs new file mode 100644 index 0000000000..4bb25d3b2c --- /dev/null +++ b/src/Avalonia.Controls/PullToRefresh/RefreshRequestedEventArgs.cs @@ -0,0 +1,42 @@ +using System; +using Avalonia.Interactivity; + +namespace Avalonia.Controls +{ + /// + /// Provides event data for RefreshRequested events. + /// + public class RefreshRequestedEventArgs : RoutedEventArgs + { + private RefreshCompletionDeferral _refreshCompletionDeferral; + + /// + /// Gets a deferral object for managing the work done in the RefreshRequested event handler. + /// + /// A object + public RefreshCompletionDeferral GetDeferral() + { + return _refreshCompletionDeferral.Get(); + } + + public RefreshRequestedEventArgs(Action deferredAction, RoutedEvent? routedEvent) : base(routedEvent) + { + _refreshCompletionDeferral = new RefreshCompletionDeferral(deferredAction); + } + + public RefreshRequestedEventArgs(RefreshCompletionDeferral completionDeferral, RoutedEvent? routedEvent) : base(routedEvent) + { + _refreshCompletionDeferral = completionDeferral; + } + + internal void IncrementCount() + { + _refreshCompletionDeferral?.Get(); + } + + internal void DecrementCount() + { + _refreshCompletionDeferral?.Complete(); + } + } +} diff --git a/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs b/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs index 2647ac1ac1..f2f735aaa9 100644 --- a/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs +++ b/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs @@ -1,8 +1,6 @@ using System; using System.Numerics; using System.Reactive.Linq; -using System.Threading; -using Avalonia.Animation; using Avalonia.Animation.Easings; using Avalonia.Controls.Primitives; using Avalonia.Controls.PullToRefresh; @@ -104,14 +102,7 @@ namespace Avalonia.Controls internal PullDirection PullDirection { get => GetValue(PullDirectionProperty); - set - { - SetValue(PullDirectionProperty, value); - - OnOrientationChanged(); - - UpdateContent(); - } + set => SetValue(PullDirectionProperty, value); } internal RefreshInfoProvider? RefreshInfoProvider @@ -201,7 +192,7 @@ namespace Avalonia.Controls } else { - RaisePropertyChanged(ContentProperty, null, Content); + RaisePropertyChanged(ContentProperty, null, Content, Data.BindingPriority.Style, false); } } @@ -400,6 +391,12 @@ namespace Avalonia.Controls break; } + UpdateContent(); + } + else if(change.Property == PullDirectionProperty) + { + OnOrientationChanged(); + UpdateContent(); } } @@ -553,95 +550,4 @@ namespace Avalonia.Controls } } } - - /// - /// Defines constants that specify the state of a RefreshVisualizer - /// - public enum RefreshVisualizerState - { - Idle, - Peeking, - Interacting, - Pending, - Refreshing - } - - /// - /// Defines constants that specify the orientation of a RefreshVisualizer. - /// - public enum RefreshVisualizerOrientation - { - Auto, - Normal, - Rotate90DegreesCounterclockwise, - Rotate270DegreesCounterclockwise - } - - /// - /// Provides event data for RefreshRequested events. - /// - public class RefreshRequestedEventArgs : RoutedEventArgs - { - private RefreshCompletionDeferral _refreshCompletionDeferral; - - /// - /// Gets a deferral object for managing the work done in the RefreshRequested event handler. - /// - /// A object - public RefreshCompletionDeferral GetDeferral() - { - return _refreshCompletionDeferral.Get(); - } - - public RefreshRequestedEventArgs(Action deferredAction, RoutedEvent? routedEvent) : base(routedEvent) - { - _refreshCompletionDeferral = new RefreshCompletionDeferral(deferredAction); - } - - public RefreshRequestedEventArgs(RefreshCompletionDeferral completionDeferral, RoutedEvent? routedEvent) : base(routedEvent) - { - _refreshCompletionDeferral = completionDeferral; - } - - internal void IncrementCount() - { - _refreshCompletionDeferral?.Get(); - } - - internal void DecrementCount() - { - _refreshCompletionDeferral?.Complete(); - } - } - - /// - /// Deferral class for notify that a work done in RefreshRequested event is done. - /// - public class RefreshCompletionDeferral - { - private Action _deferredAction; - private int _deferCount; - - public RefreshCompletionDeferral(Action deferredAction) - { - _deferredAction = deferredAction; - } - - public void Complete() - { - Interlocked.Decrement(ref _deferCount); - - if (_deferCount == 0) - { - _deferredAction?.Invoke(); - } - } - - public RefreshCompletionDeferral Get() - { - Interlocked.Increment(ref _deferCount); - - return this; - } - } } diff --git a/src/Avalonia.Controls/PullToRefresh/RefreshVisualizerOrientation.cs b/src/Avalonia.Controls/PullToRefresh/RefreshVisualizerOrientation.cs new file mode 100644 index 0000000000..1ea37f67b9 --- /dev/null +++ b/src/Avalonia.Controls/PullToRefresh/RefreshVisualizerOrientation.cs @@ -0,0 +1,13 @@ +namespace Avalonia.Controls +{ + /// + /// Defines constants that specify the orientation of a RefreshVisualizer. + /// + public enum RefreshVisualizerOrientation + { + Auto, + Normal, + Rotate90DegreesCounterclockwise, + Rotate270DegreesCounterclockwise + } +} diff --git a/src/Avalonia.Controls/PullToRefresh/RefreshVisualizerState.cs b/src/Avalonia.Controls/PullToRefresh/RefreshVisualizerState.cs new file mode 100644 index 0000000000..5ab52f4de6 --- /dev/null +++ b/src/Avalonia.Controls/PullToRefresh/RefreshVisualizerState.cs @@ -0,0 +1,14 @@ +namespace Avalonia.Controls +{ + /// + /// Defines constants that specify the state of a RefreshVisualizer + /// + public enum RefreshVisualizerState + { + Idle, + Peeking, + Interacting, + Pending, + Refreshing + } +} diff --git a/src/Avalonia.Controls/PullToRefresh/ScrollViewerIRefreshInfoProviderAdapter.cs b/src/Avalonia.Controls/PullToRefresh/ScrollViewerIRefreshInfoProviderAdapter.cs index db06de69ad..c3aebc82c5 100644 --- a/src/Avalonia.Controls/PullToRefresh/ScrollViewerIRefreshInfoProviderAdapter.cs +++ b/src/Avalonia.Controls/PullToRefresh/ScrollViewerIRefreshInfoProviderAdapter.cs @@ -23,7 +23,7 @@ namespace Avalonia.Controls.PullToRefresh _refreshPullDirection = pullDirection; } - public RefreshInfoProvider? AdaptFromTree(IVisual root, Size? refreshVIsualizerSize) + public RefreshInfoProvider? AdaptFromTree(Visual root, Size? refreshVIsualizerSize) { if (root is ScrollViewer scrollViewer) { @@ -45,7 +45,7 @@ namespace Avalonia.Controls.PullToRefresh } } - ScrollViewer? AdaptFromTreeRecursiveHelper(IVisual root, int depth) + ScrollViewer? AdaptFromTreeRecursiveHelper(Visual root, int depth) { if (depth == 0) { diff --git a/src/Avalonia.Themes.Fluent/Controls/RefreshContainer.xaml b/src/Avalonia.Themes.Fluent/Controls/RefreshContainer.xaml index 97002c25bd..8e29e6208f 100644 --- a/src/Avalonia.Themes.Fluent/Controls/RefreshContainer.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/RefreshContainer.xaml @@ -4,7 +4,7 @@ TargetType="RefreshContainer"> - + - + Date: Wed, 7 Dec 2022 22:28:59 +0000 Subject: [PATCH 51/87] use disposable pattern to make logic more robust. --- src/Avalonia.Controls/Window.cs | 34 +++++------------- src/Avalonia.Controls/WindowBase.cs | 56 ++++++++++++++++------------- 2 files changed, 39 insertions(+), 51 deletions(-) diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 729bbaa4b3..a893c74324 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -567,10 +567,8 @@ namespace Avalonia.Controls /// public override void Hide() { - try + using (FreezeVisibilityChangeHandling()) { - IgnoreVisibilityChange = true; - if (!_shown) { return; @@ -596,10 +594,6 @@ namespace Avalonia.Controls IsVisible = false; _shown = false; } - finally - { - IgnoreVisibilityChange = false; - } } /// @@ -615,7 +609,7 @@ namespace Avalonia.Controls protected override void IsVisibleChanged(AvaloniaPropertyChangedEventArgs e) { - if (!IgnoreVisibilityChange) + if (!IgnoreVisibilityChanges) { var isVisible = e.GetNewValue(); @@ -685,12 +679,10 @@ namespace Avalonia.Controls private void ShowCore(Window? owner) { - try + using (FreezeVisibilityChangeHandling()) { - IgnoreVisibilityChange = true; - EnsureStateBeforeShow(); - + if (owner != null) { EnsureParentStateBeforeShow(owner); @@ -733,10 +725,6 @@ namespace Avalonia.Controls Renderer?.Start(); OnOpened(EventArgs.Empty); } - finally - { - IgnoreVisibilityChange = false; - } } /// @@ -766,19 +754,17 @@ namespace Avalonia.Controls /// public Task ShowDialog(Window owner) { - try + using (FreezeVisibilityChangeHandling()) { - IgnoreVisibilityChange = true; - EnsureStateBeforeShow(); - + if (owner == null) { throw new ArgumentNullException(nameof(owner)); } - + EnsureParentStateBeforeShow(owner); - + if (_shown) { throw new InvalidOperationException("The window is already being shown."); @@ -828,10 +814,6 @@ namespace Avalonia.Controls OnOpened(EventArgs.Empty); return result.Task; } - finally - { - IgnoreVisibilityChange = false; - } } private void UpdateEnabled() diff --git a/src/Avalonia.Controls/WindowBase.cs b/src/Avalonia.Controls/WindowBase.cs index 46653c8203..b71dc6df44 100644 --- a/src/Avalonia.Controls/WindowBase.cs +++ b/src/Avalonia.Controls/WindowBase.cs @@ -42,9 +42,11 @@ namespace Avalonia.Controls private bool _hasExecutedInitialLayoutPass; private bool _isActive; - protected bool IgnoreVisibilityChange { get; set; } + private int _ignoreVisibilityChanges; private WindowBase? _owner; + protected bool IgnoreVisibilityChanges => _ignoreVisibilityChanges > 0; + static WindowBase() { IsVisibleProperty.OverrideDefaultValue(false); @@ -66,6 +68,11 @@ namespace Avalonia.Controls impl.PositionChanged = HandlePositionChanged; } + protected IDisposable FreezeVisibilityChangeHandling() + { + return new IgnoreVisibilityChangesDisposable(this); + } + /// /// Fired when the window is activated. /// @@ -125,18 +132,12 @@ namespace Avalonia.Controls /// public virtual void Hide() { - IgnoreVisibilityChange = true; - - try + using (FreezeVisibilityChangeHandling()) { Renderer?.Stop(); PlatformImpl?.Hide(); IsVisible = false; } - finally - { - IgnoreVisibilityChange = false; - } } /// @@ -144,9 +145,7 @@ namespace Avalonia.Controls /// public virtual void Show() { - IgnoreVisibilityChange = true; - - try + using (FreezeVisibilityChangeHandling()) { EnsureInitialized(); ApplyStyling(); @@ -157,14 +156,11 @@ namespace Avalonia.Controls LayoutManager.ExecuteInitialLayoutPass(); _hasExecutedInitialLayoutPass = true; } + PlatformImpl?.Show(true, false); Renderer?.Start(); OnOpened(EventArgs.Empty); } - finally - { - IgnoreVisibilityChange = false; - } } /// @@ -202,23 +198,17 @@ namespace Avalonia.Controls protected override void HandleClosed() { - IgnoreVisibilityChange = true; - - try + using (FreezeVisibilityChangeHandling()) { IsVisible = false; - + if (this is IFocusScope scope) { FocusManager.Instance?.RemoveFocusScope(scope); } - + base.HandleClosed(); } - finally - { - IgnoreVisibilityChange = false; - } } /// @@ -320,7 +310,7 @@ namespace Avalonia.Controls protected virtual void IsVisibleChanged(AvaloniaPropertyChangedEventArgs e) { - if (!IgnoreVisibilityChange) + if (_ignoreVisibilityChanges == 0) { if ((bool)e.NewValue!) { @@ -332,5 +322,21 @@ namespace Avalonia.Controls } } } + + private readonly struct IgnoreVisibilityChangesDisposable : IDisposable + { + private readonly WindowBase _windowBase; + + public IgnoreVisibilityChangesDisposable(WindowBase windowBase) + { + _windowBase = windowBase; + _windowBase._ignoreVisibilityChanges++; + } + + public void Dispose() + { + _windowBase._ignoreVisibilityChanges--; + } + } } } From 895d85aa89a1b80ea6dc6c033c44913b744de883 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Thu, 8 Dec 2022 14:01:09 +0100 Subject: [PATCH 52/87] Implement CharacterBufferReference and related classes --- .../Pages/TextFormatterPage.axaml.cs | 2 +- src/Avalonia.Base/Media/FormattedText.cs | 8 +- src/Avalonia.Base/Media/GlyphRun.cs | 419 ++++++++++-------- src/Avalonia.Base/Media/GlyphRunMetrics.cs | 18 +- .../TextFormatting/CharacterBufferRange.cs | 308 +++++++++++++ .../CharacterBufferReference.cs | 176 ++++++++ .../TextFormatting/FormattedTextSource.cs | 13 +- .../TextFormatting/InterWordJustification.cs | 19 +- .../TextFormatting/ShapeableTextCharacters.cs | 20 +- .../Media/TextFormatting/ShapedBuffer.cs | 31 +- .../TextFormatting/ShapedTextCharacters.cs | 19 +- .../Media/TextFormatting/SplitResult.cs | 2 +- .../Media/TextFormatting/TextCharacters.cs | 144 ++++-- .../TextFormatting/TextEllipsisHelper.cs | 102 ++--- .../Media/TextFormatting/TextEndOfLine.cs | 4 +- .../Media/TextFormatting/TextFormatterImpl.cs | 100 +++-- .../Media/TextFormatting/TextLayout.cs | 2 +- .../TextLeadingPrefixCharacterEllipsis.cs | 5 +- .../Media/TextFormatting/TextLineImpl.cs | 358 ++++++++------- .../Media/TextFormatting/TextLineMetrics.cs | 6 +- .../Media/TextFormatting/TextMetrics.cs | 4 +- .../Media/TextFormatting/TextRun.cs | 11 +- .../Media/TextFormatting/TextShaper.cs | 11 +- .../TextTrailingCharacterEllipsis.cs | 3 +- .../TextTrailingWordEllipsis.cs | 2 +- .../Media/TextFormatting/Unicode/BiDiData.cs | 3 +- .../Media/TextFormatting/Unicode/Codepoint.cs | 9 +- .../Unicode/CodepointEnumerator.cs | 7 +- .../Media/TextFormatting/Unicode/Grapheme.cs | 8 +- .../Unicode/GraphemeEnumerator.cs | 12 +- .../Unicode/LineBreakEnumerator.cs | 15 +- .../Media/TextLeadingPrefixTrimming.cs | 11 +- .../Media/TextTrailingTrimming.cs | 11 +- src/Avalonia.Base/Media/TextTrimming.cs | 2 +- src/Avalonia.Base/Platform/ITextShaperImpl.cs | 5 +- .../Composition/Server/FpsCounter.cs | 3 +- src/Avalonia.Base/Utilities/ArraySlice.cs | 8 - src/Avalonia.Base/Utilities/ReadOnlySlice.cs | 239 ---------- src/Avalonia.Controls/Documents/LineBreak.cs | 4 +- src/Avalonia.Controls/Documents/Run.cs | 2 +- src/Avalonia.Controls/TextBlock.cs | 27 +- src/Avalonia.Controls/TextBox.cs | 4 +- .../TextBoxTextInputMethodClient.cs | 8 +- .../HeadlessPlatformStubs.cs | 6 +- src/Skia/Avalonia.Skia/TextShaperImpl.cs | 24 +- .../Media/TextShaperImpl.cs | 11 +- .../Media/GlyphRunTests.cs | 4 +- .../Media/TextFormatting/BiDiClassTests.cs | 3 +- .../GraphemeBreakClassTrieGeneratorTests.cs | 9 +- .../LineBreakEnumuratorTests.cs | 9 +- .../Utilities/ReadOnlySpanTests.cs | 37 -- .../Presenters/TextPresenter_Tests.cs | 2 +- .../Media/GlyphRunTests.cs | 29 +- .../TextFormatting/MultiBufferTextSource.cs | 3 +- .../TextFormatting/SingleBufferTextSource.cs | 19 +- .../TextFormatting/TextFormatterTests.cs | 21 +- .../Media/TextFormatting/TextLayoutTests.cs | 46 +- .../Media/TextFormatting/TextLineTests.cs | 90 ++-- .../Media/TextFormatting/TextShaperTests.cs | 6 +- .../HarfBuzzTextShaperImpl.cs | 8 +- .../Avalonia.UnitTests/MockTextShaperImpl.cs | 12 +- 61 files changed, 1427 insertions(+), 1077 deletions(-) create mode 100644 src/Avalonia.Base/Media/TextFormatting/CharacterBufferRange.cs create mode 100644 src/Avalonia.Base/Media/TextFormatting/CharacterBufferReference.cs delete mode 100644 src/Avalonia.Base/Utilities/ReadOnlySlice.cs delete mode 100644 tests/Avalonia.Base.UnitTests/Utilities/ReadOnlySpanTests.cs diff --git a/samples/RenderDemo/Pages/TextFormatterPage.axaml.cs b/samples/RenderDemo/Pages/TextFormatterPage.axaml.cs index 57a5c7101f..8fbfa854b1 100644 --- a/samples/RenderDemo/Pages/TextFormatterPage.axaml.cs +++ b/samples/RenderDemo/Pages/TextFormatterPage.axaml.cs @@ -90,7 +90,7 @@ namespace RenderDemo.Pages return new ControlRun(_control, _defaultProperties); } - return new TextCharacters(_text.AsMemory(), _defaultProperties); + return new TextCharacters(_text, _defaultProperties); } } diff --git a/src/Avalonia.Base/Media/FormattedText.cs b/src/Avalonia.Base/Media/FormattedText.cs index 90b9755493..138e8b79eb 100644 --- a/src/Avalonia.Base/Media/FormattedText.cs +++ b/src/Avalonia.Base/Media/FormattedText.cs @@ -1,10 +1,8 @@ using System; using System.Collections; -using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.Globalization; -using Avalonia.Controls; using Avalonia.Media.TextFormatting; using Avalonia.Utilities; @@ -25,7 +23,7 @@ namespace Avalonia.Media private const double MaxFontEmSize = RealInfiniteWidth / GreatestMultiplierOfEm; // properties and format runs - private ReadOnlySlice _text; + private string _text; private readonly SpanVector _formatRuns = new SpanVector(null); private SpanPosition _latestPosition; @@ -69,9 +67,7 @@ namespace Avalonia.Media ValidateFontSize(emSize); - _text = textToFormat != null ? - new ReadOnlySlice(textToFormat.AsMemory()) : - throw new ArgumentNullException(nameof(textToFormat)); + _text = textToFormat; var runProps = new GenericTextRunProperties( typeface, diff --git a/src/Avalonia.Base/Media/GlyphRun.cs b/src/Avalonia.Base/Media/GlyphRun.cs index d93a68e78b..af9e458a28 100644 --- a/src/Avalonia.Base/Media/GlyphRun.cs +++ b/src/Avalonia.Base/Media/GlyphRun.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Drawing; using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Platform; using Avalonia.Utilities; @@ -22,15 +21,12 @@ namespace Avalonia.Media private Point? _baselineOrigin; private GlyphRunMetrics? _glyphRunMetrics; - private ReadOnlySlice _characters; - + private IReadOnlyList _characters; private IReadOnlyList _glyphIndices; private IReadOnlyList? _glyphAdvances; private IReadOnlyList? _glyphOffsets; private IReadOnlyList? _glyphClusters; - private int _offsetToFirstCharacter; - /// /// Initializes a new instance of the class by specifying properties of the class. /// @@ -45,7 +41,7 @@ namespace Avalonia.Media public GlyphRun( IGlyphTypeface glyphTypeface, double fontRenderingEmSize, - ReadOnlySlice characters, + IReadOnlyList characters, IReadOnlyList glyphIndices, IReadOnlyList? glyphAdvances = null, IReadOnlyList? glyphOffsets = null, @@ -54,19 +50,19 @@ namespace Avalonia.Media { _glyphTypeface = glyphTypeface; - FontRenderingEmSize = fontRenderingEmSize; + _fontRenderingEmSize = fontRenderingEmSize; - Characters = characters; + _characters = characters; _glyphIndices = glyphIndices; - GlyphAdvances = glyphAdvances; + _glyphAdvances = glyphAdvances; - GlyphOffsets = glyphOffsets; + _glyphOffsets = glyphOffsets; - GlyphClusters = glyphClusters; + _glyphClusters = glyphClusters; - BiDiLevel = biDiLevel; + _biDiLevel = biDiLevel; } /// @@ -145,7 +141,7 @@ namespace Avalonia.Media /// /// Gets or sets the list of UTF16 code points that represent the Unicode content of the . /// - public ReadOnlySlice Characters + public IReadOnlyList Characters { get => _characters; set => Set(ref _characters, value); @@ -219,7 +215,7 @@ namespace Avalonia.Media /// public double GetDistanceFromCharacterHit(CharacterHit characterHit) { - var characterIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength - _offsetToFirstCharacter; + var characterIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength; var distance = 0.0; @@ -227,12 +223,12 @@ namespace Avalonia.Media { if (GlyphClusters != null) { - if (characterIndex < GlyphClusters[0]) + if (characterIndex < Metrics.FirstCluster) { return 0; } - if (characterIndex > GlyphClusters[GlyphClusters.Count - 1]) + if (characterIndex > Metrics.LastCluster) { return Metrics.WidthIncludingTrailingWhitespace; } @@ -268,12 +264,12 @@ namespace Avalonia.Media if (GlyphClusters != null && GlyphClusters.Count > 0) { - if (characterIndex > GlyphClusters[0]) + if (characterIndex > Metrics.LastCluster) { return 0; } - if (characterIndex <= GlyphClusters[GlyphClusters.Count - 1]) + if (characterIndex <= Metrics.FirstCluster) { return Size.Width; } @@ -299,19 +295,12 @@ namespace Avalonia.Media /// public CharacterHit GetCharacterHitFromDistance(double distance, out bool isInside) { - var characterIndex = 0; - // Before if (distance <= 0) { isInside = false; - if (GlyphClusters != null) - { - characterIndex = GlyphClusters[characterIndex]; - } - - var firstCharacterHit = FindNearestCharacterHit(characterIndex, out _); + var firstCharacterHit = FindNearestCharacterHit(IsLeftToRight ? Metrics.FirstCluster : Metrics.LastCluster, out _); return IsLeftToRight ? new CharacterHit(firstCharacterHit.FirstCharacterIndex) : firstCharacterHit; } @@ -321,18 +310,13 @@ namespace Avalonia.Media { isInside = false; - characterIndex = GlyphIndices.Count - 1; - - if (GlyphClusters != null) - { - characterIndex = GlyphClusters[characterIndex]; - } - - var lastCharacterHit = FindNearestCharacterHit(characterIndex, out _); + var lastCharacterHit = FindNearestCharacterHit(IsLeftToRight ? Metrics.LastCluster : Metrics.FirstCluster, out _); return IsLeftToRight ? lastCharacterHit : new CharacterHit(lastCharacterHit.FirstCharacterIndex); } + var characterIndex = 0; + //Within var currentX = 0d; @@ -378,7 +362,7 @@ namespace Avalonia.Media var characterHit = FindNearestCharacterHit(characterIndex, out var width); var delta = width / 2; - + var offset = IsLeftToRight ? Math.Round(distance - currentX, 3) : Math.Round(currentX - distance, 3); var isTrailing = offset > delta; @@ -400,24 +384,15 @@ namespace Avalonia.Media { characterHit = FindNearestCharacterHit(characterHit.FirstCharacterIndex, out _); - var textPosition = characterHit.FirstCharacterIndex + characterHit.TrailingLength; - - return textPosition > _characters.End ? - characterHit : - new CharacterHit(characterHit.FirstCharacterIndex + characterHit.TrailingLength); - } - - var nextCharacterHit = - FindNearestCharacterHit(characterHit.FirstCharacterIndex + characterHit.TrailingLength, out _); + if (characterHit.FirstCharacterIndex == Metrics.LastCluster) + { + return characterHit; + } - if (characterHit == nextCharacterHit) - { - return characterHit; + return new CharacterHit(characterHit.FirstCharacterIndex + characterHit.TrailingLength); } - return characterHit.TrailingLength > 0 ? - nextCharacterHit : - new CharacterHit(nextCharacterHit.FirstCharacterIndex); + return FindNearestCharacterHit(characterHit.FirstCharacterIndex + characterHit.TrailingLength, out _); } /// @@ -454,29 +429,24 @@ namespace Avalonia.Media return characterIndex; } - if (IsLeftToRight) + if (characterIndex > Metrics.LastCluster) { - if (characterIndex < GlyphClusters[0]) + if (IsLeftToRight) { - return 0; + return GlyphIndices.Count - 1; } - if (characterIndex > GlyphClusters[GlyphClusters.Count - 1]) - { - return GlyphClusters.Count - 1; - } + return 0; } - else - { - if (characterIndex < GlyphClusters[GlyphClusters.Count - 1]) - { - return GlyphClusters.Count - 1; - } - if (characterIndex > GlyphClusters[0]) + if (characterIndex < Metrics.FirstCluster) + { + if (IsLeftToRight) { return 0; } + + return GlyphIndices.Count - 1; } var comparer = IsLeftToRight ? s_ascendingComparer : s_descendingComparer; @@ -498,7 +468,7 @@ namespace Avalonia.Media if (start < 0) { - return -1; + goto result; } } @@ -517,6 +487,18 @@ namespace Avalonia.Media } } + result: + + if (start < 0) + { + return 0; + } + + if (start > GlyphIndices.Count - 1) + { + return GlyphIndices.Count - 1; + } + return start; } @@ -532,20 +514,20 @@ namespace Avalonia.Media { width = 0.0; - var start = FindGlyphIndex(index); + var glyphIndex = FindGlyphIndex(index); if (GlyphClusters == null) { width = GetGlyphAdvance(index, out _); - return new CharacterHit(start, 1); + return new CharacterHit(glyphIndex, 1); } - var cluster = GlyphClusters[start]; + var cluster = GlyphClusters[glyphIndex]; var nextCluster = cluster; - var currentIndex = start; + var currentIndex = glyphIndex; while (nextCluster == cluster) { @@ -571,20 +553,64 @@ namespace Avalonia.Media } nextCluster = GlyphClusters[currentIndex]; - } + } - int trailingLength; + var clusterLength = Math.Max(0, nextCluster - cluster); - if (nextCluster == cluster) - { - trailingLength = Characters.Start + Characters.Length - _offsetToFirstCharacter - cluster; - } - else + if (cluster == Metrics.LastCluster && clusterLength == 0) { - trailingLength = nextCluster - cluster; + var characterLength = 0; + + var currentCluster = Metrics.FirstCluster; + + if (IsLeftToRight) + { + for (int i = 1; i < GlyphClusters.Count; i++) + { + nextCluster = GlyphClusters[i]; + + if (currentCluster > cluster) + { + break; + } + + var length = nextCluster - currentCluster; + + characterLength += length; + + currentCluster = nextCluster; + } + } + else + { + for (int i = GlyphClusters.Count - 1; i >= 0; i--) + { + nextCluster = GlyphClusters[i]; + + if (currentCluster > cluster) + { + break; + } + + var length = nextCluster - currentCluster; + + characterLength += length; + + currentCluster = nextCluster; + } + } + + if (Characters != null) + { + clusterLength = Characters.Count - characterLength; + } + else + { + clusterLength = 1; + } } - return new CharacterHit(_offsetToFirstCharacter + cluster, trailingLength); + return new CharacterHit(cluster, clusterLength); } /// @@ -618,22 +644,25 @@ namespace Avalonia.Media private GlyphRunMetrics CreateGlyphRunMetrics() { - var firstCluster = 0; - var lastCluster = Characters.Length - 1; + int firstCluster = 0, lastCluster = 0; - if (!IsLeftToRight) + if (_glyphClusters != null && _glyphClusters.Count > 0) { - var cluster = firstCluster; - firstCluster = lastCluster; - lastCluster = cluster; + firstCluster = _glyphClusters[0]; + lastCluster = _glyphClusters[_glyphClusters.Count - 1]; } - - if (GlyphClusters != null && GlyphClusters.Count > 0) + else { - firstCluster = GlyphClusters[0]; - lastCluster = GlyphClusters[GlyphClusters.Count - 1]; + if (Characters != null && Characters.Count > 0) + { + firstCluster = 0; + lastCluster = Characters.Count - 1; + } + } - _offsetToFirstCharacter = Math.Max(0, Characters.Start - firstCluster); + if (!IsLeftToRight) + { + (lastCluster, firstCluster) = (firstCluster, lastCluster); } var isReversed = firstCluster > lastCluster; @@ -666,12 +695,19 @@ namespace Avalonia.Media } } - return new GlyphRunMetrics(width, widthIncludingTrailingWhitespace, trailingWhitespaceLength, newLineLength, - height); + return new GlyphRunMetrics( + width, + widthIncludingTrailingWhitespace, + height, + trailingWhitespaceLength, + newLineLength, + firstCluster, + lastCluster + ); } private int GetTrailingWhitespaceLength(bool isReversed, out int newLineLength, out int glyphCount) - { + { if (isReversed) { return GetTralingWhitespaceLengthRightToLeft(out newLineLength, out glyphCount); @@ -681,66 +717,82 @@ namespace Avalonia.Media newLineLength = 0; var trailingWhitespaceLength = 0; - if (GlyphClusters == null) + if (Characters != null) { - for (var i = _characters.Length - 1; i >= 0;) + if (GlyphClusters == null) { - var codepoint = Codepoint.ReadAt(_characters, i, out var count); - - if (!codepoint.IsWhiteSpace) + for (var i = _characters.Count - 1; i >= 0;) { - break; - } + var codepoint = Codepoint.ReadAt(_characters, i, out var count); - if (codepoint.IsBreakChar) - { - newLineLength++; - } + if (!codepoint.IsWhiteSpace) + { + break; + } - trailingWhitespaceLength++; + if (codepoint.IsBreakChar) + { + newLineLength++; + } + + trailingWhitespaceLength++; - i -= count; - glyphCount++; + i -= count; + glyphCount++; + } } - } - else - { - for (var i = GlyphClusters.Count - 1; i >= 0; i--) + else { - var currentCluster = GlyphClusters[i]; - var characterIndex = Math.Max(0, currentCluster - _characters.BufferOffset); - var codepoint = Codepoint.ReadAt(_characters, characterIndex, out _); - - if (!codepoint.IsWhiteSpace) + if (Characters.Count > 0) { - break; - } + var characterIndex = Characters.Count - 1; - var clusterLength = 1; + for (var i = GlyphClusters.Count - 1; i >= 0; i--) + { + var currentCluster = GlyphClusters[i]; + var codepoint = Codepoint.ReadAt(_characters, characterIndex, out var characterLength); - while(i - 1 >= 0) - { - var nextCluster = GlyphClusters[i - 1]; + characterIndex -= characterLength; - if(currentCluster == nextCluster) - { - clusterLength++; - i--; + if (!codepoint.IsWhiteSpace) + { + break; + } - continue; - } + var clusterLength = 1; - break; - } + while (i - 1 >= 0) + { + var nextCluster = GlyphClusters[i - 1]; - if (codepoint.IsBreakChar) - { - newLineLength += clusterLength; - } + if (currentCluster == nextCluster) + { + clusterLength++; + i--; + + if(characterIndex >= 0) + { + codepoint = Codepoint.ReadAt(_characters, characterIndex, out characterLength); + + characterIndex -= characterLength; + } + + continue; + } + + break; + } + + if (codepoint.IsBreakChar) + { + newLineLength += clusterLength; + } - trailingWhitespaceLength += clusterLength; - - glyphCount++; + trailingWhitespaceLength += clusterLength; + + glyphCount++; + } + } } } @@ -753,67 +805,73 @@ namespace Avalonia.Media newLineLength = 0; var trailingWhitespaceLength = 0; - if (GlyphClusters == null) + if (Characters != null) { - for (var i = 0; i < Characters.Length;) + if (GlyphClusters == null) { - var codepoint = Codepoint.ReadAt(_characters, i, out var count); - - if (!codepoint.IsWhiteSpace) + for (var i = 0; i < Characters.Count;) { - break; - } + var codepoint = Codepoint.ReadAt(_characters, i, out var count); - if (codepoint.IsBreakChar) - { - newLineLength++; - } + if (!codepoint.IsWhiteSpace) + { + break; + } - trailingWhitespaceLength++; + if (codepoint.IsBreakChar) + { + newLineLength++; + } - i += count; - glyphCount++; + trailingWhitespaceLength++; + + i += count; + glyphCount++; + } } - } - else - { - for (var i = 0; i < GlyphClusters.Count; i++) + else { - var currentCluster = GlyphClusters[i]; - var characterIndex = Math.Max(0, currentCluster - _characters.BufferOffset); - var codepoint = Codepoint.ReadAt(_characters, characterIndex, out _); + var characterIndex = 0; - if (!codepoint.IsWhiteSpace) + for (var i = 0; i < GlyphClusters.Count; i++) { - break; - } + var currentCluster = GlyphClusters[i]; + var codepoint = Codepoint.ReadAt(_characters, characterIndex, out var characterLength); - var clusterLength = 1; + characterIndex += characterLength; - var j = i; + if (!codepoint.IsWhiteSpace) + { + break; + } - while (j - 1 >= 0) - { - var nextCluster = GlyphClusters[--j]; + var clusterLength = 1; - if (currentCluster == nextCluster) + var j = i; + + while (j - 1 >= 0) { - clusterLength++; + var nextCluster = GlyphClusters[--j]; - continue; - } + if (currentCluster == nextCluster) + { + clusterLength++; - break; - } + continue; + } - if (codepoint.IsBreakChar) - { - newLineLength += clusterLength; - } + break; + } + + if (codepoint.IsBreakChar) + { + newLineLength += clusterLength; + } - trailingWhitespaceLength += clusterLength; + trailingWhitespaceLength += clusterLength; - glyphCount += clusterLength; + glyphCount += clusterLength; + } } } @@ -855,14 +913,9 @@ namespace Avalonia.Media throw new InvalidOperationException(); } - _glyphRunImpl = CreateGlyphRunImpl(); - } - - private IGlyphRunImpl CreateGlyphRunImpl() - { var platformRenderInterface = AvaloniaLocator.Current.GetRequiredService(); - return platformRenderInterface.CreateGlyphRun(GlyphTypeface, FontRenderingEmSize, GlyphIndices, GlyphAdvances, GlyphOffsets); + _glyphRunImpl = platformRenderInterface.CreateGlyphRun(GlyphTypeface, FontRenderingEmSize, GlyphIndices, GlyphAdvances, GlyphOffsets); } void IDisposable.Dispose() diff --git a/src/Avalonia.Base/Media/GlyphRunMetrics.cs b/src/Avalonia.Base/Media/GlyphRunMetrics.cs index a8698a7d82..983f029c7a 100644 --- a/src/Avalonia.Base/Media/GlyphRunMetrics.cs +++ b/src/Avalonia.Base/Media/GlyphRunMetrics.cs @@ -2,24 +2,30 @@ { public readonly struct GlyphRunMetrics { - public GlyphRunMetrics(double width, double widthIncludingTrailingWhitespace, int trailingWhitespaceLength, - int newlineLength, double height) + public GlyphRunMetrics(double width, double widthIncludingTrailingWhitespace, double height, + int trailingWhitespaceLength, int newLineLength, int firstCluster, int lastCluster) { Width = width; WidthIncludingTrailingWhitespace = widthIncludingTrailingWhitespace; - TrailingWhitespaceLength = trailingWhitespaceLength; - NewlineLength = newlineLength; Height = height; + TrailingWhitespaceLength = trailingWhitespaceLength; + NewLineLength= newLineLength; + FirstCluster = firstCluster; + LastCluster = lastCluster; } public double Width { get; } public double WidthIncludingTrailingWhitespace { get; } + public double Height { get; } + public int TrailingWhitespaceLength { get; } - public int NewlineLength { get; } + public int NewLineLength { get; } - public double Height { get; } + public int FirstCluster { get; } + + public int LastCluster { get; } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/CharacterBufferRange.cs b/src/Avalonia.Base/Media/TextFormatting/CharacterBufferRange.cs new file mode 100644 index 0000000000..045f336700 --- /dev/null +++ b/src/Avalonia.Base/Media/TextFormatting/CharacterBufferRange.cs @@ -0,0 +1,308 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using Avalonia.Utilities; + +namespace Avalonia.Media.TextFormatting +{ + public readonly struct CharacterBufferRange : IReadOnlyList + { + /// + /// Getting an empty character string + /// + public static CharacterBufferRange Empty => new CharacterBufferRange(); + + /// + /// Construct from character array + /// + /// character array + /// character buffer offset to the first character + /// character length + public CharacterBufferRange( + char[] characterArray, + int offsetToFirstChar, + int characterLength + ) + : this( + new CharacterBufferReference(characterArray, offsetToFirstChar), + characterLength + ) + { } + + /// + /// Construct from string + /// + /// character string + /// character buffer offset to the first character + /// character length + public CharacterBufferRange( + string characterString, + int offsetToFirstChar, + int characterLength + ) + : this( + new CharacterBufferReference(characterString, offsetToFirstChar), + characterLength + ) + { } + + /// + /// Construct from unsafe character string + /// + /// pointer to character string + /// character length + public unsafe CharacterBufferRange( + char* unsafeCharacterString, + int characterLength + ) + : this( + new CharacterBufferReference(unsafeCharacterString, characterLength), + characterLength + ) + { } + + /// + /// Construct a from + /// + /// character buffer reference + /// number of characters + public CharacterBufferRange( + CharacterBufferReference characterBufferReference, + int characterLength + ) + { + if (characterLength < 0) + { + throw new ArgumentOutOfRangeException("characterLength", "ParameterCannotBeNegative"); + } + + int maxLength = characterBufferReference.CharacterBuffer.Length > 0 ? + characterBufferReference.CharacterBuffer.Length - characterBufferReference.OffsetToFirstChar : + 0; + + if (characterLength > maxLength) + { + throw new ArgumentOutOfRangeException("characterLength", $"ParameterCannotBeGreaterThan {maxLength}"); + } + + CharacterBufferReference = characterBufferReference; + Length = characterLength; + } + + /// + /// Construct a from part of another + /// + internal CharacterBufferRange( + CharacterBufferRange characterBufferRange, + int offsetToFirstChar, + int characterLength + ) : + this( + characterBufferRange.CharacterBuffer, + characterBufferRange.OffsetToFirstChar + offsetToFirstChar, + characterLength + ) + { } + + + /// + /// Construct a from string + /// + internal CharacterBufferRange( + string charString + ) : + this( + charString, + 0, + charString.Length + ) + { } + + + /// + /// Construct from memory buffer + /// + internal CharacterBufferRange( + ReadOnlyMemory charBuffer, + int offsetToFirstChar, + int characterLength + ) : + this( + new CharacterBufferReference(charBuffer, offsetToFirstChar), + characterLength + ) + { } + + + /// + /// Construct a by extracting text info from a text run + /// + internal CharacterBufferRange(TextRun textRun) + { + CharacterBufferReference = textRun.CharacterBufferReference; + Length = textRun.Length; + } + + public char this[int index] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { +#if DEBUG + if (index.CompareTo(0) < 0 || index.CompareTo(Length) > 0) + { + throw new ArgumentOutOfRangeException(nameof(index)); + } +#endif + return Span[index]; + } + } + + /// + /// Gets a reference to the character buffer + /// + public CharacterBufferReference CharacterBufferReference { get; } + + /// + /// Gets the number of characters in text source character store + /// + public int Length { get; } + + /// + /// Gets a span from the character buffer range + /// + public ReadOnlySpan Span => + CharacterBufferReference.CharacterBuffer.Span.Slice(CharacterBufferReference.OffsetToFirstChar, Length); + + /// + /// Gets the character memory buffer + /// + internal ReadOnlyMemory CharacterBuffer + { + get { return CharacterBufferReference.CharacterBuffer; } + } + + /// + /// Gets the character offset relative to the beginning of buffer to + /// the first character of the run + /// + internal int OffsetToFirstChar + { + get { return CharacterBufferReference.OffsetToFirstChar; } + } + + /// + /// Indicate whether the character buffer range is empty + /// + internal bool IsEmpty + { + get { return CharacterBufferReference.CharacterBuffer.Length == 0 || Length <= 0; } + } + + internal CharacterBufferRange Take(int length) + { + if (IsEmpty) + { + return this; + } + + if (length > Length) + { + throw new ArgumentOutOfRangeException(nameof(length)); + } + + return new CharacterBufferRange(CharacterBufferReference, length); + } + + internal CharacterBufferRange Skip(int length) + { + if (IsEmpty) + { + return this; + } + + if (length > Length) + { + throw new ArgumentOutOfRangeException(nameof(length)); + } + + if (length == Length) + { + return new CharacterBufferRange(new CharacterBufferReference(), 0); + } + + var characterBufferReference = new CharacterBufferReference( + CharacterBufferReference.CharacterBuffer, + CharacterBufferReference.OffsetToFirstChar + length); + + return new CharacterBufferRange(characterBufferReference, Length - length); + } + + /// + /// Compute hash code + /// + public override int GetHashCode() + { + return CharacterBufferReference.GetHashCode() ^ Length; + } + + /// + /// Test equality with the input object + /// + /// The object to test + public override bool Equals(object? obj) + { + if (obj is CharacterBufferRange range) + { + return Equals(range); + } + + return false; + } + + /// + /// Test equality with the input CharacterBufferRange + /// + /// The CharacterBufferRange value to test + public bool Equals(CharacterBufferRange value) + { + return CharacterBufferReference.Equals(value.CharacterBufferReference) + && Length == value.Length; + } + + /// + /// Compare two CharacterBufferRange for equality + /// + /// left operand + /// right operand + /// whether or not two operands are equal + public static bool operator ==(CharacterBufferRange left, CharacterBufferRange right) + { + return left.Equals(right); + } + + /// + /// Compare two CharacterBufferRange for inequality + /// + /// left operand + /// right operand + /// whether or not two operands are equal + public static bool operator !=(CharacterBufferRange left, CharacterBufferRange right) + { + return !(left == right); + } + + int IReadOnlyCollection.Count => Length; + + public IEnumerator GetEnumerator() + { + return new ImmutableReadOnlyListStructEnumerator(this); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } +} diff --git a/src/Avalonia.Base/Media/TextFormatting/CharacterBufferReference.cs b/src/Avalonia.Base/Media/TextFormatting/CharacterBufferReference.cs new file mode 100644 index 0000000000..a15562cb52 --- /dev/null +++ b/src/Avalonia.Base/Media/TextFormatting/CharacterBufferReference.cs @@ -0,0 +1,176 @@ +using System; +using System.Buffers; +using System.Runtime.InteropServices; + +namespace Avalonia.Media.TextFormatting +{ + /// + /// Text character buffer reference + /// + public readonly struct CharacterBufferReference : IEquatable + { + /// + /// Construct character buffer reference from character array + /// + /// character array + /// character buffer offset to the first character + public CharacterBufferReference(char[] characterArray, int offsetToFirstChar = 0) + : this(characterArray.AsMemory(), offsetToFirstChar) + { } + + /// + /// Construct character buffer reference from string + /// + /// character string + /// character buffer offset to the first character + public CharacterBufferReference(string characterString, int offsetToFirstChar = 0) + : this(characterString.AsMemory(), offsetToFirstChar) + { } + + /// + /// Construct character buffer reference from unsafe character string + /// + /// pointer to character string + /// character length of unsafe string + public unsafe CharacterBufferReference(char* unsafeCharacterString, int characterLength) + : this(new UnmanagedMemoryManager(unsafeCharacterString, characterLength).Memory, 0) + { } + + /// + /// Construct character buffer reference from memory buffer + /// + internal CharacterBufferReference(ReadOnlyMemory characterBuffer, int offsetToFirstChar = 0) + { + if (offsetToFirstChar < 0) + { + throw new ArgumentOutOfRangeException("offsetToFirstChar", "ParameterCannotBeNegative"); + } + + // maximum offset is one less than CharacterBuffer.Count, except that zero is always a valid offset + // even in the case of an empty or null character buffer + var maxOffset = characterBuffer.Length == 0 ? 0 : Math.Max(0, characterBuffer.Length - 1); + if (offsetToFirstChar > maxOffset) + { + throw new ArgumentOutOfRangeException("offsetToFirstChar", $"ParameterCannotBeGreaterThan, {maxOffset}"); + } + + CharacterBuffer = characterBuffer; + OffsetToFirstChar = offsetToFirstChar; + } + + /// + /// Compute hash code + /// + public override int GetHashCode() + { + return CharacterBuffer.IsEmpty ? 0 : CharacterBuffer.GetHashCode(); + } + + /// + /// Test equality with the input object + /// + /// The object to test. + public override bool Equals(object? obj) + { + if (obj is CharacterBufferReference reference) + { + return Equals(reference); + } + + return false; + } + + /// + /// Test equality with the input CharacterBufferReference + /// + /// The characterBufferReference value to test + public bool Equals(CharacterBufferReference value) + { + return CharacterBuffer.Equals(value.CharacterBuffer); + } + + /// + /// Compare two CharacterBufferReference for equality + /// + /// left operand + /// right operand + /// whether or not two operands are equal + public static bool operator ==(CharacterBufferReference left, CharacterBufferReference right) + { + return left.Equals(right); + } + + /// + /// Compare two CharacterBufferReference for inequality + /// + /// left operand + /// right operand + /// whether or not two operands are equal + public static bool operator !=(CharacterBufferReference left, CharacterBufferReference right) + { + return !(left == right); + } + + public ReadOnlyMemory CharacterBuffer { get; } + + public int OffsetToFirstChar { get; } + + /// + /// A MemoryManager over a raw pointer + /// + /// The pointer is assumed to be fully unmanaged, or externally pinned - no attempt will be made to pin this data + public sealed unsafe class UnmanagedMemoryManager : MemoryManager + where T : unmanaged + { + private readonly T* _pointer; + private readonly int _length; + + /// + /// Create a new UnmanagedMemoryManager instance at the given pointer and size + /// + /// It is assumed that the span provided is already unmanaged or externally pinned + public UnmanagedMemoryManager(Span span) + { + fixed (T* ptr = &MemoryMarshal.GetReference(span)) + { + _pointer = ptr; + _length = span.Length; + } + } + /// + /// Create a new UnmanagedMemoryManager instance at the given pointer and size + /// + public UnmanagedMemoryManager(T* pointer, int length) + { + if (length < 0) + throw new ArgumentOutOfRangeException(nameof(length)); + _pointer = pointer; + _length = length; + } + /// + /// Obtains a span that represents the region + /// + public override Span GetSpan() => new Span(_pointer, _length); + + /// + /// Provides access to a pointer that represents the data (note: no actual pin occurs) + /// + public override MemoryHandle Pin(int elementIndex = 0) + { + if (elementIndex < 0 || elementIndex >= _length) + throw new ArgumentOutOfRangeException(nameof(elementIndex)); + return new MemoryHandle(_pointer + elementIndex); + } + /// + /// Has no effect + /// + public override void Unpin() { } + + /// + /// Releases all resources associated with this object + /// + protected override void Dispose(bool disposing) { } + } + } +} + diff --git a/src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs b/src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs index fb8e699d8e..e745a873a2 100644 --- a/src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs +++ b/src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs @@ -7,14 +7,15 @@ namespace Avalonia.Media.TextFormatting { internal readonly struct FormattedTextSource : ITextSource { - private readonly ReadOnlySlice _text; + private readonly CharacterBufferRange _text; + private readonly int length; private readonly TextRunProperties _defaultProperties; private readonly IReadOnlyList>? _textModifier; - public FormattedTextSource(ReadOnlySlice text, TextRunProperties defaultProperties, + public FormattedTextSource(string text, TextRunProperties defaultProperties, IReadOnlyList>? textModifier) { - _text = text; + _text = new CharacterBufferRange(text); _defaultProperties = defaultProperties; _textModifier = textModifier; } @@ -35,7 +36,7 @@ namespace Avalonia.Media.TextFormatting var textStyleRun = CreateTextStyleRun(runText, textSourceIndex, _defaultProperties, _textModifier); - return new TextCharacters(runText.Take(textStyleRun.Length), textStyleRun.Value); + return new TextCharacters(runText.Take(textStyleRun.Length).CharacterBufferReference, textStyleRun.Length, textStyleRun.Value); } /// @@ -48,7 +49,7 @@ namespace Avalonia.Media.TextFormatting /// /// The created text style run. /// - private static ValueSpan CreateTextStyleRun(ReadOnlySlice text, int firstTextSourceIndex, + private static ValueSpan CreateTextStyleRun(CharacterBufferRange text, int firstTextSourceIndex, TextRunProperties defaultProperties, IReadOnlyList>? textModifier) { if (textModifier == null || textModifier.Count == 0) @@ -122,7 +123,7 @@ namespace Avalonia.Media.TextFormatting return new ValueSpan(firstTextSourceIndex, length, currentProperties); } - private static int CoerceLength(ReadOnlySlice text, int length) + private static int CoerceLength(CharacterBufferRange text, int length) { var finalLength = 0; diff --git a/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs b/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs index a49e4ef13b..3c3a46c209 100644 --- a/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs +++ b/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs @@ -46,28 +46,30 @@ namespace Avalonia.Media.TextFormatting var breakOportunities = new Queue(); + var currentPosition = textLine.FirstTextSourceIndex; + foreach (var textRun in lineImpl.TextRuns) { - var text = textRun.Text; + var text = new CharacterBufferRange(textRun); if (text.IsEmpty) { continue; } - var start = text.Start; - var lineBreakEnumerator = new LineBreakEnumerator(text); while (lineBreakEnumerator.MoveNext()) { var currentBreak = lineBreakEnumerator.Current; - if (!currentBreak.Required && currentBreak.PositionWrap != text.Length) + if (!currentBreak.Required && currentBreak.PositionWrap != textRun.Length) { - breakOportunities.Enqueue(start + currentBreak.PositionMeasure); + breakOportunities.Enqueue(currentPosition + currentBreak.PositionMeasure); } } + + currentPosition += textRun.Length; } if (breakOportunities.Count == 0) @@ -78,9 +80,11 @@ namespace Avalonia.Media.TextFormatting var remainingSpace = Math.Max(0, paragraphWidth - lineImpl.WidthIncludingTrailingWhitespace); var spacing = remainingSpace / breakOportunities.Count; + currentPosition = textLine.FirstTextSourceIndex; + foreach (var textRun in lineImpl.TextRuns) { - var text = textRun.Text; + var text = textRun.CharacterBufferReference.CharacterBuffer; if (text.IsEmpty) { @@ -91,7 +95,6 @@ namespace Avalonia.Media.TextFormatting { var glyphRun = shapedText.GlyphRun; var shapedBuffer = shapedText.ShapedBuffer; - var currentPosition = text.Start; while (breakOportunities.Count > 0) { @@ -110,6 +113,8 @@ namespace Avalonia.Media.TextFormatting glyphRun.GlyphAdvances = shapedBuffer.GlyphAdvances; } + + currentPosition += textRun.Length; } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/ShapeableTextCharacters.cs b/src/Avalonia.Base/Media/TextFormatting/ShapeableTextCharacters.cs index b31a6f4d13..0e8d6e3e4a 100644 --- a/src/Avalonia.Base/Media/TextFormatting/ShapeableTextCharacters.cs +++ b/src/Avalonia.Base/Media/TextFormatting/ShapeableTextCharacters.cs @@ -7,30 +7,26 @@ namespace Avalonia.Media.TextFormatting /// public sealed class ShapeableTextCharacters : TextRun { - public ShapeableTextCharacters(ReadOnlySlice text, TextRunProperties properties, sbyte biDiLevel) + public ShapeableTextCharacters(CharacterBufferReference characterBufferReference, int length, + TextRunProperties properties, sbyte biDiLevel) { - TextSourceLength = text.Length; - Text = text; + CharacterBufferReference = characterBufferReference; + Length = length; Properties = properties; BidiLevel = biDiLevel; } - public override int TextSourceLength { get; } + public override int Length { get; } - public override ReadOnlySlice Text { get; } + public override CharacterBufferReference CharacterBufferReference { get; } public override TextRunProperties Properties { get; } - + public sbyte BidiLevel { get; } public bool CanShapeTogether(ShapeableTextCharacters shapeableTextCharacters) { - if (!Text.Buffer.Equals(shapeableTextCharacters.Text.Buffer)) - { - return false; - } - - if (Text.Start + Text.Length != shapeableTextCharacters.Text.Start) + if (!CharacterBufferReference.Equals(shapeableTextCharacters.CharacterBufferReference)) { return false; } diff --git a/src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs b/src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs index 85924a3d32..644c0ecbe1 100644 --- a/src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs +++ b/src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs @@ -7,16 +7,16 @@ namespace Avalonia.Media.TextFormatting public sealed class ShapedBuffer : IList { private static readonly IComparer s_clusterComparer = new CompareClusters(); - - public ShapedBuffer(ReadOnlySlice text, int length, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel) - : this(text, new GlyphInfo[length], glyphTypeface, fontRenderingEmSize, bidiLevel) + + public ShapedBuffer(CharacterBufferRange characterBufferRange, int bufferLength, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel) : + this(characterBufferRange, new GlyphInfo[bufferLength], glyphTypeface, fontRenderingEmSize, bidiLevel) { } - internal ShapedBuffer(ReadOnlySlice text, ArraySlice glyphInfos, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel) + internal ShapedBuffer(CharacterBufferRange characterBufferRange, ArraySlice glyphInfos, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel) { - Text = text; + CharacterBufferRange = characterBufferRange; GlyphInfos = glyphInfos; GlyphTypeface = glyphTypeface; FontRenderingEmSize = fontRenderingEmSize; @@ -24,9 +24,7 @@ namespace Avalonia.Media.TextFormatting } internal ArraySlice GlyphInfos { get; } - - public ReadOnlySlice Text { get; } - + public int Length => GlyphInfos.Length; public IGlyphTypeface GlyphTypeface { get; } @@ -45,6 +43,8 @@ namespace Avalonia.Media.TextFormatting public IReadOnlyList GlyphOffsets => new GlyphOffsetList(GlyphInfos); + public CharacterBufferRange CharacterBufferRange { get; } + /// /// Finds a glyph index for given character index. /// @@ -105,16 +105,23 @@ namespace Avalonia.Media.TextFormatting /// The split result. internal SplitResult Split(int length) { - if (Text.Length == length) + if (CharacterBufferRange.Length == length) { return new SplitResult(this, null); } - var glyphCount = FindGlyphIndex(Text.Start + length); + var firstCluster = GlyphClusters[0]; + var lastCluster = GlyphClusters[GlyphClusters.Count - 1]; + + var start = firstCluster < lastCluster ? firstCluster : lastCluster; + + var glyphCount = FindGlyphIndex(start + length); - var first = new ShapedBuffer(Text.Take(length), GlyphInfos.Take(glyphCount), GlyphTypeface, FontRenderingEmSize, BidiLevel); + var first = new ShapedBuffer(CharacterBufferRange.Take(length), + GlyphInfos.Take(glyphCount), GlyphTypeface, FontRenderingEmSize, BidiLevel); - var second = new ShapedBuffer(Text.Skip(length), GlyphInfos.Skip(glyphCount), GlyphTypeface, FontRenderingEmSize, BidiLevel); + var second = new ShapedBuffer(CharacterBufferRange.Skip(length), + GlyphInfos.Skip(glyphCount), GlyphTypeface, FontRenderingEmSize, BidiLevel); return new SplitResult(first, second); } diff --git a/src/Avalonia.Base/Media/TextFormatting/ShapedTextCharacters.cs b/src/Avalonia.Base/Media/TextFormatting/ShapedTextCharacters.cs index 21101f462c..3035eb7b18 100644 --- a/src/Avalonia.Base/Media/TextFormatting/ShapedTextCharacters.cs +++ b/src/Avalonia.Base/Media/TextFormatting/ShapedTextCharacters.cs @@ -1,6 +1,5 @@ using System; using Avalonia.Media.TextFormatting.Unicode; -using Avalonia.Utilities; namespace Avalonia.Media.TextFormatting { @@ -14,10 +13,10 @@ namespace Avalonia.Media.TextFormatting public ShapedTextCharacters(ShapedBuffer shapedBuffer, TextRunProperties properties) { ShapedBuffer = shapedBuffer; - Text = shapedBuffer.Text; + CharacterBufferReference = shapedBuffer.CharacterBufferRange.CharacterBufferReference; + Length = shapedBuffer.CharacterBufferRange.Length; Properties = properties; - TextSourceLength = Text.Length; - TextMetrics = new TextMetrics(properties.Typeface, properties.FontRenderingEmSize); + TextMetrics = new TextMetrics(properties.Typeface.GlyphTypeface, properties.FontRenderingEmSize); } public bool IsReversed { get; private set; } @@ -27,13 +26,13 @@ namespace Avalonia.Media.TextFormatting public ShapedBuffer ShapedBuffer { get; } /// - public override ReadOnlySlice Text { get; } + public override CharacterBufferReference CharacterBufferReference { get; } /// public override TextRunProperties Properties { get; } /// - public override int TextSourceLength { get; } + public override int Length { get; } public TextMetrics TextMetrics { get; } @@ -176,12 +175,12 @@ namespace Avalonia.Media.TextFormatting #if DEBUG - if (first.Text.Length != length) + if (first.Length != length) { throw new InvalidOperationException("Split length mismatch."); } - - #endif + +#endif var second = new ShapedTextCharacters(splitBuffer.Second!, Properties); @@ -193,7 +192,7 @@ namespace Avalonia.Media.TextFormatting return new GlyphRun( ShapedBuffer.GlyphTypeface, ShapedBuffer.FontRenderingEmSize, - Text, + new CharacterBufferRange(CharacterBufferReference, Length), ShapedBuffer.GlyphIndices, ShapedBuffer.GlyphAdvances, ShapedBuffer.GlyphOffsets, diff --git a/src/Avalonia.Base/Media/TextFormatting/SplitResult.cs b/src/Avalonia.Base/Media/TextFormatting/SplitResult.cs index 02c7174499..03b93cfaf0 100644 --- a/src/Avalonia.Base/Media/TextFormatting/SplitResult.cs +++ b/src/Avalonia.Base/Media/TextFormatting/SplitResult.cs @@ -1,6 +1,6 @@ namespace Avalonia.Media.TextFormatting { - internal readonly struct SplitResult + public readonly struct SplitResult { public SplitResult(T first, T? second) { diff --git a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs index bcfa35ae30..9587786c5b 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using Avalonia.Media.TextFormatting.Unicode; -using Avalonia.Utilities; namespace Avalonia.Media.TextFormatting { @@ -10,26 +9,98 @@ namespace Avalonia.Media.TextFormatting /// public class TextCharacters : TextRun { - public TextCharacters(ReadOnlySlice text, TextRunProperties properties) - { - TextSourceLength = text.Length; - Text = text; - Properties = properties; - } + /// + /// Construct a run of text content from character array + /// + public TextCharacters( + char[] characterArray, + int offsetToFirstChar, + int length, + TextRunProperties textRunProperties + ) : + this( + new CharacterBufferReference(characterArray, offsetToFirstChar), + length, + textRunProperties + ) + { } + + + /// + /// Construct a run for text content from string + /// + public TextCharacters( + string characterString, + TextRunProperties textRunProperties + ) : + this( + characterString, + 0, // offsetToFirstChar + (characterString == null) ? 0 : characterString.Length, + textRunProperties + ) + { } + + /// + /// Construct a run for text content from string + /// + public TextCharacters( + string characterString, + int offsetToFirstChar, + int length, + TextRunProperties textRunProperties + ) : + this( + new CharacterBufferReference(characterString, offsetToFirstChar), + length, + textRunProperties + ) + { } - public TextCharacters(ReadOnlySlice text, int offsetToFirstCharacter, int length, - TextRunProperties properties) + /// + /// Construct a run for text content from unsafe character string + /// + public unsafe TextCharacters( + char* unsafeCharacterString, + int length, + TextRunProperties textRunProperties + ) : + this( + new CharacterBufferReference(unsafeCharacterString, length), + length, + textRunProperties + ) + { } + + /// + /// Internal constructor of TextContent + /// + public TextCharacters( + CharacterBufferReference characterBufferReference, + int length, + TextRunProperties textRunProperties + ) { - Text = text.Skip(offsetToFirstCharacter).Take(length); - TextSourceLength = length; - Properties = properties; + if (length <= 0) + { + throw new ArgumentOutOfRangeException("length", "ParameterMustBeGreaterThanZero"); + } + + if (textRunProperties.FontRenderingEmSize <= 0) + { + throw new ArgumentOutOfRangeException("textRunProperties.FontRenderingEmSize", "ParameterMustBeGreaterThanZero"); + } + + CharacterBufferReference = characterBufferReference; + Length = length; + Properties = textRunProperties; } /// - public override int TextSourceLength { get; } + public override int Length { get; } /// - public override ReadOnlySlice Text { get; } + public override CharacterBufferReference CharacterBufferReference { get; } /// public override TextRunProperties Properties { get; } @@ -38,18 +109,17 @@ namespace Avalonia.Media.TextFormatting /// Gets a list of . /// /// The shapeable text characters. - internal IReadOnlyList GetShapeableCharacters(ReadOnlySlice runText, sbyte biDiLevel, - ref TextRunProperties? previousProperties) + internal IReadOnlyList GetShapeableCharacters(CharacterBufferRange characterBufferRange, sbyte biDiLevel, ref TextRunProperties? previousProperties) { var shapeableCharacters = new List(2); - while (!runText.IsEmpty) + while (characterBufferRange.Length > 0) { - var shapeableRun = CreateShapeableRun(runText, Properties, biDiLevel, ref previousProperties); + var shapeableRun = CreateShapeableRun(characterBufferRange, Properties, biDiLevel, ref previousProperties); shapeableCharacters.Add(shapeableRun); - runText = runText.Skip(shapeableRun.Text.Length); + characterBufferRange = characterBufferRange.Skip(shapeableRun.Length); previousProperties = shapeableRun.Properties; } @@ -60,45 +130,45 @@ namespace Avalonia.Media.TextFormatting /// /// Creates a shapeable text run with unique properties. /// - /// The text to create text runs from. + /// The character buffer range to create text runs from. /// The default text run properties. /// The bidi level of the run. /// /// A list of shapeable text runs. - private static ShapeableTextCharacters CreateShapeableRun(ReadOnlySlice text, + private static ShapeableTextCharacters CreateShapeableRun(CharacterBufferRange characterBufferRange, TextRunProperties defaultProperties, sbyte biDiLevel, ref TextRunProperties? previousProperties) { var defaultTypeface = defaultProperties.Typeface; var currentTypeface = defaultTypeface; var previousTypeface = previousProperties?.Typeface; - if (TryGetShapeableLength(text, currentTypeface, null, out var count, out var script)) + if (TryGetShapeableLength(characterBufferRange, currentTypeface, null, out var count, out var script)) { if (script == Script.Common && previousTypeface is not null) { - if (TryGetShapeableLength(text, previousTypeface.Value, null, out var fallbackCount, out _)) + if (TryGetShapeableLength(characterBufferRange, previousTypeface.Value, null, out var fallbackCount, out _)) { - return new ShapeableTextCharacters(text.Take(fallbackCount), + return new ShapeableTextCharacters(characterBufferRange.CharacterBufferReference, fallbackCount, defaultProperties.WithTypeface(previousTypeface.Value), biDiLevel); } } - return new ShapeableTextCharacters(text.Take(count), defaultProperties.WithTypeface(currentTypeface), + return new ShapeableTextCharacters(characterBufferRange.CharacterBufferReference, count, defaultProperties.WithTypeface(currentTypeface), biDiLevel); } if (previousTypeface is not null) { - if (TryGetShapeableLength(text, previousTypeface.Value, defaultTypeface, out count, out _)) + if (TryGetShapeableLength(characterBufferRange, previousTypeface.Value, defaultTypeface, out count, out _)) { - return new ShapeableTextCharacters(text.Take(count), + return new ShapeableTextCharacters(characterBufferRange.CharacterBufferReference, count, defaultProperties.WithTypeface(previousTypeface.Value), biDiLevel); } } var codepoint = Codepoint.ReplacementCodepoint; - var codepointEnumerator = new CodepointEnumerator(text.Skip(count)); + var codepointEnumerator = new CodepointEnumerator(characterBufferRange.Skip(count)); while (codepointEnumerator.MoveNext()) { @@ -118,10 +188,10 @@ namespace Avalonia.Media.TextFormatting defaultTypeface.Stretch, defaultTypeface.FontFamily, defaultProperties.CultureInfo, out currentTypeface); - if (matchFound && TryGetShapeableLength(text, currentTypeface, defaultTypeface, out count, out _)) + if (matchFound && TryGetShapeableLength(characterBufferRange, currentTypeface, defaultTypeface, out count, out _)) { //Fallback found - return new ShapeableTextCharacters(text.Take(count), defaultProperties.WithTypeface(currentTypeface), + return new ShapeableTextCharacters(characterBufferRange.CharacterBufferReference, count, defaultProperties.WithTypeface(currentTypeface), biDiLevel); } @@ -130,7 +200,7 @@ namespace Avalonia.Media.TextFormatting var glyphTypeface = currentTypeface.GlyphTypeface; - var enumerator = new GraphemeEnumerator(text); + var enumerator = new GraphemeEnumerator(characterBufferRange); while (enumerator.MoveNext()) { @@ -144,20 +214,20 @@ namespace Avalonia.Media.TextFormatting count += grapheme.Text.Length; } - return new ShapeableTextCharacters(text.Take(count), defaultProperties, biDiLevel); + return new ShapeableTextCharacters(characterBufferRange.CharacterBufferReference, count, defaultProperties, biDiLevel); } /// /// Tries to get a shapeable length that is supported by the specified typeface. /// - /// The text. + /// The character buffer range to shape. /// The typeface that is used to find matching characters. /// /// The shapeable length. /// /// - protected static bool TryGetShapeableLength( - ReadOnlySlice text, + internal static bool TryGetShapeableLength( + CharacterBufferRange characterBufferRange, Typeface typeface, Typeface? defaultTypeface, out int length, @@ -166,7 +236,7 @@ namespace Avalonia.Media.TextFormatting length = 0; script = Script.Unknown; - if (text.Length == 0) + if (characterBufferRange.Length == 0) { return false; } @@ -174,7 +244,7 @@ namespace Avalonia.Media.TextFormatting var font = typeface.GlyphTypeface; var defaultFont = defaultTypeface?.GlyphTypeface; - var enumerator = new GraphemeEnumerator(text); + var enumerator = new GraphemeEnumerator(characterBufferRange); while (enumerator.MoveNext()) { diff --git a/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs b/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs index 5a2169630b..a1b8985b43 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs @@ -32,86 +32,88 @@ namespace Avalonia.Media.TextFormatting switch (currentRun) { case ShapedTextCharacters shapedRun: - { - currentWidth += shapedRun.Size.Width; - - if (currentWidth > availableWidth) { - if (shapedRun.TryMeasureCharacters(availableWidth, out var measuredLength)) + currentWidth += shapedRun.Size.Width; + + if (currentWidth > availableWidth) { - if (isWordEllipsis && measuredLength < textLine.Length) + if (shapedRun.TryMeasureCharacters(availableWidth, out var measuredLength)) { - var currentBreakPosition = 0; + if (isWordEllipsis && measuredLength < textLine.Length) + { + var currentBreakPosition = 0; - var lineBreaker = new LineBreakEnumerator(currentRun.Text); + var text = new CharacterBufferRange(currentRun.CharacterBufferReference, currentRun.Length); - while (currentBreakPosition < measuredLength && lineBreaker.MoveNext()) - { - var nextBreakPosition = lineBreaker.Current.PositionMeasure; + var lineBreaker = new LineBreakEnumerator(text); - if (nextBreakPosition == 0) + while (currentBreakPosition < measuredLength && lineBreaker.MoveNext()) { - break; - } + var nextBreakPosition = lineBreaker.Current.PositionMeasure; - if (nextBreakPosition >= measuredLength) - { - break; + if (nextBreakPosition == 0) + { + break; + } + + if (nextBreakPosition >= measuredLength) + { + break; + } + + currentBreakPosition = nextBreakPosition; } - currentBreakPosition = nextBreakPosition; + measuredLength = currentBreakPosition; } - - measuredLength = currentBreakPosition; } - } - collapsedLength += measuredLength; + collapsedLength += measuredLength; - var collapsedRuns = new List(textRuns.Count); + var collapsedRuns = new List(textRuns.Count); - if (collapsedLength > 0) - { - var splitResult = TextFormatterImpl.SplitDrawableRuns(textRuns, collapsedLength); + if (collapsedLength > 0) + { + var splitResult = TextFormatterImpl.SplitDrawableRuns(textRuns, collapsedLength); - collapsedRuns.AddRange(splitResult.First); - } + collapsedRuns.AddRange(splitResult.First); + } - collapsedRuns.Add(shapedSymbol); + collapsedRuns.Add(shapedSymbol); - return collapsedRuns; - } + return collapsedRuns; + } - availableWidth -= currentRun.Size.Width; + availableWidth -= currentRun.Size.Width; - - break; - } + + break; + } case { } drawableRun: - { - //The whole run needs to fit into available space - if (currentWidth + drawableRun.Size.Width > availableWidth) { - var collapsedRuns = new List(textRuns.Count); - - if (collapsedLength > 0) + //The whole run needs to fit into available space + if (currentWidth + drawableRun.Size.Width > availableWidth) { - var splitResult = TextFormatterImpl.SplitDrawableRuns(textRuns, collapsedLength); + var collapsedRuns = new List(textRuns.Count); - collapsedRuns.AddRange(splitResult.First); - } + if (collapsedLength > 0) + { + var splitResult = TextFormatterImpl.SplitDrawableRuns(textRuns, collapsedLength); + + collapsedRuns.AddRange(splitResult.First); + } + + collapsedRuns.Add(shapedSymbol); - collapsedRuns.Add(shapedSymbol); + return collapsedRuns; + } - return collapsedRuns; + break; } - - break; - } } - collapsedLength += currentRun.TextSourceLength; + collapsedLength += currentRun.Length; runIndex++; } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextEndOfLine.cs b/src/Avalonia.Base/Media/TextFormatting/TextEndOfLine.cs index 21e354a119..ffb879e721 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextEndOfLine.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextEndOfLine.cs @@ -7,9 +7,9 @@ { public TextEndOfLine(int textSourceLength = DefaultTextSourceLength) { - TextSourceLength = textSourceLength; + Length = textSourceLength; } - public override int TextSourceLength { get; } + public override int Length { get; } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs index 7bad95c4a2..93eb4811b9 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs @@ -79,14 +79,14 @@ namespace Avalonia.Media.TextFormatting { var currentRun = textRuns[i]; - if (currentLength + currentRun.TextSourceLength < length) + if (currentLength + currentRun.Length < length) { - currentLength += currentRun.TextSourceLength; + currentLength += currentRun.Length; continue; } - var firstCount = currentRun.TextSourceLength >= 1 ? i + 1 : i; + var firstCount = currentRun.Length >= 1 ? i + 1 : i; var first = new List(firstCount); @@ -100,13 +100,13 @@ namespace Avalonia.Media.TextFormatting var secondCount = textRuns.Count - firstCount; - if (currentLength + currentRun.TextSourceLength == length) + if (currentLength + currentRun.Length == length) { var second = secondCount > 0 ? new List(secondCount) : null; if (second != null) { - var offset = currentRun.TextSourceLength >= 1 ? 1 : 0; + var offset = currentRun.Length >= 1 ? 1 : 0; for (var j = 0; j < secondCount; j++) { @@ -163,15 +163,17 @@ namespace Avalonia.Media.TextFormatting foreach (var textRun in textRuns) { - if (textRun.Text.IsEmpty) + if (textRun.CharacterBufferReference.CharacterBuffer.Length == 0) { - var text = new char[textRun.TextSourceLength]; + var characterBuffer = new CharacterBufferReference(new char[textRun.Length]); - biDiData.Append(text); + biDiData.Append(new CharacterBufferRange(characterBuffer, textRun.Length)); } else { - biDiData.Append(textRun.Text); + var text = new CharacterBufferRange(textRun.CharacterBufferReference, textRun.Length); + + biDiData.Append(text); } } @@ -207,10 +209,9 @@ namespace Avalonia.Media.TextFormatting case ShapeableTextCharacters shapeableRun: { var groupedRuns = new List(2) { shapeableRun }; - var text = currentRun.Text; - var start = currentRun.Text.Start; - var length = currentRun.Text.Length; - var bufferOffset = currentRun.Text.BufferOffset; + var characterBufferReference = currentRun.CharacterBufferReference; + var length = currentRun.Length; + var offsetToFirstCharacter = characterBufferReference.OffsetToFirstChar; while (index + 1 < processedRuns.Count) { @@ -223,19 +224,14 @@ namespace Avalonia.Media.TextFormatting { groupedRuns.Add(nextRun); - length += nextRun.Text.Length; - - if (start > nextRun.Text.Start) - { - start = nextRun.Text.Start; - } + length += nextRun.Length; - if (bufferOffset > nextRun.Text.BufferOffset) + if (offsetToFirstCharacter > nextRun.CharacterBufferReference.OffsetToFirstChar) { - bufferOffset = nextRun.Text.BufferOffset; + offsetToFirstCharacter = nextRun.CharacterBufferReference.OffsetToFirstChar; } - text = new ReadOnlySlice(text.Buffer, start, length, bufferOffset); + characterBufferReference = new CharacterBufferReference(characterBufferReference.CharacterBuffer, offsetToFirstCharacter); index++; @@ -252,7 +248,7 @@ namespace Avalonia.Media.TextFormatting shapeableRun.BidiLevel, currentRun.Properties.CultureInfo, paragraphProperties.DefaultIncrementalTab, paragraphProperties.LetterSpacing); - drawableTextRuns.AddRange(ShapeTogether(groupedRuns, text, shaperOptions)); + drawableTextRuns.AddRange(ShapeTogether(groupedRuns, characterBufferReference, length, shaperOptions)); break; } @@ -263,17 +259,17 @@ namespace Avalonia.Media.TextFormatting } private static IReadOnlyList ShapeTogether( - IReadOnlyList textRuns, ReadOnlySlice text, TextShaperOptions options) + IReadOnlyList textRuns, CharacterBufferReference text, int length, TextShaperOptions options) { var shapedRuns = new List(textRuns.Count); - var shapedBuffer = TextShaper.Current.ShapeText(text, options); + var shapedBuffer = TextShaper.Current.ShapeText(text, length, options); for (var i = 0; i < textRuns.Count; i++) { var currentRun = textRuns[i]; - var splitResult = shapedBuffer.Split(currentRun.Text.Length); + var splitResult = shapedBuffer.Split(currentRun.Length); shapedRuns.Add(new ShapedTextCharacters(splitResult.First, currentRun.Properties)); @@ -301,7 +297,7 @@ namespace Avalonia.Media.TextFormatting TextRunProperties? previousProperties = null; TextCharacters? currentRun = null; - var runText = ReadOnlySlice.Empty; + CharacterBufferRange runText = default; for (var i = 0; i < textCharacters.Count; i++) { @@ -314,12 +310,12 @@ namespace Avalonia.Media.TextFormatting yield return new[] { drawableRun }; - levelIndex += drawableRun.TextSourceLength; + levelIndex += drawableRun.Length; continue; } - runText = currentRun.Text; + runText = new CharacterBufferRange(currentRun.CharacterBufferReference, currentRun.Length); for (; j < runText.Length;) { @@ -401,7 +397,7 @@ namespace Avalonia.Media.TextFormatting { endOfLine = textEndOfLine; - textSourceLength += textEndOfLine.TextSourceLength; + textSourceLength += textEndOfLine.Length; textRuns.Add(textRun); @@ -414,7 +410,7 @@ namespace Avalonia.Media.TextFormatting { if (TryGetLineBreak(textCharacters, out var runLineBreak)) { - var splitResult = new TextCharacters(textCharacters.Text.Take(runLineBreak.PositionWrap), + var splitResult = new TextCharacters(textCharacters.CharacterBufferReference, runLineBreak.PositionWrap, textCharacters.Properties); textRuns.Add(splitResult); @@ -435,7 +431,7 @@ namespace Avalonia.Media.TextFormatting } } - textSourceLength += textRun.TextSourceLength; + textSourceLength += textRun.Length; } return textRuns; @@ -445,12 +441,14 @@ namespace Avalonia.Media.TextFormatting { lineBreak = default; - if (textRun.Text.IsEmpty) + if (textRun.CharacterBufferReference.CharacterBuffer.IsEmpty) { return false; } - var lineBreakEnumerator = new LineBreakEnumerator(textRun.Text); + var characterBufferRange = new CharacterBufferRange(textRun.CharacterBufferReference, textRun.Length); + + var lineBreakEnumerator = new LineBreakEnumerator(characterBufferRange); while (lineBreakEnumerator.MoveNext()) { @@ -461,7 +459,7 @@ namespace Avalonia.Media.TextFormatting lineBreak = lineBreakEnumerator.Current; - return lineBreak.PositionWrap >= textRun.Text.Length || true; + return lineBreak.PositionWrap >= textRun.Length || true; } return false; @@ -480,7 +478,7 @@ namespace Avalonia.Media.TextFormatting { if(shapedTextCharacters.ShapedBuffer.Length > 0) { - var firstCluster = shapedTextCharacters.ShapedBuffer.GlyphClusters[0]; + var firstCluster = shapedTextCharacters.ShapedBuffer.GlyphInfos[0].GlyphCluster; var lastCluster = firstCluster; for (var i = 0; i < shapedTextCharacters.ShapedBuffer.Length; i++) @@ -498,7 +496,7 @@ namespace Avalonia.Media.TextFormatting currentWidth += glyphInfo.GlyphAdvance; } - measuredLength += currentRun.TextSourceLength; + measuredLength += currentRun.Length; } break; @@ -511,7 +509,7 @@ namespace Avalonia.Media.TextFormatting goto found; } - measuredLength += currentRun.TextSourceLength; + measuredLength += currentRun.Length; currentWidth += currentRun.Size.Width; break; @@ -533,11 +531,11 @@ namespace Avalonia.Media.TextFormatting var flowDirection = paragraphProperties.FlowDirection; var properties = paragraphProperties.DefaultTextRunProperties; var glyphTypeface = properties.Typeface.GlyphTypeface; - var text = new ReadOnlySlice(s_empty, firstTextSourceIndex, 1); var glyph = glyphTypeface.GetGlyph(s_empty[0]); var glyphInfos = new[] { new GlyphInfo(glyph, firstTextSourceIndex) }; - var shapedBuffer = new ShapedBuffer(text, glyphInfos, glyphTypeface, properties.FontRenderingEmSize, + var characterBufferRange = new CharacterBufferRange(new CharacterBufferReference(s_empty), s_empty.Length); + var shapedBuffer = new ShapedBuffer(characterBufferRange, glyphInfos, glyphTypeface, properties.FontRenderingEmSize, (sbyte)flowDirection); var textRuns = new List { new ShapedTextCharacters(shapedBuffer, properties) }; @@ -579,7 +577,9 @@ namespace Avalonia.Media.TextFormatting { var currentRun = textRuns[index]; - var lineBreaker = new LineBreakEnumerator(currentRun.Text); + var runText = new CharacterBufferRange(currentRun.CharacterBufferReference, currentRun.Length); + + var lineBreaker = new LineBreakEnumerator(runText); var breakFound = false; @@ -612,7 +612,7 @@ namespace Avalonia.Media.TextFormatting //Find next possible wrap position (overflow) if (index < textRuns.Count - 1) { - if (lineBreaker.Current.PositionWrap != currentRun.Text.Length) + if (lineBreaker.Current.PositionWrap != currentRun.Length) { //We already found the next possible wrap position. breakFound = true; @@ -626,7 +626,7 @@ namespace Avalonia.Media.TextFormatting { currentPosition += lineBreaker.Current.PositionWrap; - if (lineBreaker.Current.PositionWrap != currentRun.Text.Length) + if (lineBreaker.Current.PositionWrap != currentRun.Length) { break; } @@ -640,7 +640,9 @@ namespace Avalonia.Media.TextFormatting currentRun = textRuns[index]; - lineBreaker = new LineBreakEnumerator(currentRun.Text); + runText = new CharacterBufferRange(currentRun.CharacterBufferReference, currentRun.Length); + + lineBreaker = new LineBreakEnumerator(runText); } } else @@ -669,7 +671,7 @@ namespace Avalonia.Media.TextFormatting if (!breakFound) { - currentLength += currentRun.TextSourceLength; + currentLength += currentRun.Length; continue; } @@ -723,12 +725,12 @@ namespace Avalonia.Media.TextFormatting return false; } - if (Current.TextSourceLength == 0) + if (Current.Length == 0) { return false; } - _pos += Current.TextSourceLength; + _pos += Current.Length; return true; } @@ -754,7 +756,9 @@ namespace Avalonia.Media.TextFormatting var shaperOptions = new TextShaperOptions(glyphTypeface, fontRenderingEmSize, (sbyte)flowDirection, cultureInfo); - var shapedBuffer = textShaper.ShapeText(textRun.Text, shaperOptions); + var characterBuffer = textRun.CharacterBufferReference; + + var shapedBuffer = textShaper.ShapeText(characterBuffer, textRun.Length, shaperOptions); return new ShapedTextCharacters(shapedBuffer, textRun.Properties); } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs index dc79e61333..f803001481 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs @@ -55,7 +55,7 @@ namespace Avalonia.Media.TextFormatting CreateTextParagraphProperties(typeface, fontSize, foreground, textAlignment, textWrapping, textDecorations, flowDirection, lineHeight, letterSpacing); - _textSource = new FormattedTextSource(text.AsMemory(), _paragraphProperties.DefaultTextRunProperties, textStyleOverrides); + _textSource = new FormattedTextSource(text ?? "", _paragraphProperties.DefaultTextRunProperties, textStyleOverrides); _textTrimming = textTrimming ?? TextTrimming.None; diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs b/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs index 5a14eda245..2752af8f0c 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using Avalonia.Utilities; namespace Avalonia.Media.TextFormatting { @@ -19,7 +18,7 @@ namespace Avalonia.Media.TextFormatting /// width in which collapsing is constrained to /// text run properties of ellipsis symbol public TextLeadingPrefixCharacterEllipsis( - ReadOnlySlice ellipsis, + string ellipsis, int prefixLength, double width, TextRunProperties textRunProperties) @@ -129,7 +128,7 @@ namespace Avalonia.Media.TextFormatting if (suffixCount > 0) { var splitSuffix = - endShapedRun.Split(run.TextSourceLength - suffixCount); + endShapedRun.Split(run.Length - suffixCount); collapsedRuns.Add(splitSuffix.Second!); } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index 96f88d1f44..3241dfd12b 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -56,7 +56,7 @@ namespace Avalonia.Media.TextFormatting public override double Height => _textLineMetrics.Height; /// - public override int NewLineLength => _textLineMetrics.NewLineLength; + public override int NewLineLength => _textLineMetrics.NewlineLength; /// public override double OverhangAfter => 0; @@ -180,7 +180,7 @@ namespace Avalonia.Media.TextFormatting { var lastRun = _textRuns[_textRuns.Count - 1]; - return GetRunCharacterHit(lastRun, FirstTextSourceIndex + Length - lastRun.TextSourceLength, lastRun.Size.Width); + return GetRunCharacterHit(lastRun, FirstTextSourceIndex + Length - lastRun.Length, lastRun.Size.Width); } // process hit that happens within the line @@ -195,18 +195,18 @@ namespace Avalonia.Media.TextFormatting if (currentRun is ShapedTextCharacters shapedRun && !shapedRun.ShapedBuffer.IsLeftToRight) { var rightToLeftIndex = i; - currentPosition += currentRun.TextSourceLength; + currentPosition += currentRun.Length; while (rightToLeftIndex + 1 <= _textRuns.Count - 1) { - var nextShaped = _textRuns[rightToLeftIndex + 1] as ShapedTextCharacters; + var nextShaped = _textRuns[++rightToLeftIndex] as ShapedTextCharacters; if (nextShaped == null || nextShaped.ShapedBuffer.IsLeftToRight) { break; } - currentPosition += nextShaped.TextSourceLength; + currentPosition += nextShaped.Length; rightToLeftIndex++; } @@ -223,27 +223,26 @@ namespace Avalonia.Media.TextFormatting if (currentDistance + currentRun.Size.Width <= distance) { currentDistance += currentRun.Size.Width; - currentPosition -= currentRun.TextSourceLength; + currentPosition -= currentRun.Length; continue; } - characterHit = GetRunCharacterHit(currentRun, currentPosition, distance - currentDistance); - - break; + return GetRunCharacterHit(currentRun, currentPosition, distance - currentDistance); } } - if (currentDistance + currentRun.Size.Width < distance) + characterHit = GetRunCharacterHit(currentRun, currentPosition, distance - currentDistance); + + if (i < _textRuns.Count - 1 && currentDistance + currentRun.Size.Width < distance) { currentDistance += currentRun.Size.Width; - currentPosition += currentRun.TextSourceLength; + + currentPosition += currentRun.Length; continue; } - characterHit = GetRunCharacterHit(currentRun, currentPosition, distance - currentDistance); - break; } @@ -264,10 +263,10 @@ namespace Avalonia.Media.TextFormatting if (shapedRun.GlyphRun.IsLeftToRight) { - offset = Math.Max(0, currentPosition - shapedRun.Text.Start); + offset = Math.Max(0, currentPosition - shapedRun.GlyphRun.Metrics.FirstCluster); } - characterHit = new CharacterHit(characterHit.FirstCharacterIndex + offset, characterHit.TrailingLength); + characterHit = new CharacterHit(offset + characterHit.FirstCharacterIndex, characterHit.TrailingLength); break; } @@ -279,7 +278,7 @@ namespace Avalonia.Media.TextFormatting } else { - characterHit = new CharacterHit(currentPosition, run.TextSourceLength); + characterHit = new CharacterHit(currentPosition, run.Length); } break; } @@ -334,14 +333,14 @@ namespace Avalonia.Media.TextFormatting rightToLeftWidth -= currentRun.Size.Width; - if (currentPosition + currentRun.TextSourceLength >= characterIndex) + if (currentPosition + currentRun.Length >= characterIndex) { break; } - currentPosition += currentRun.TextSourceLength; + currentPosition += currentRun.Length; - remainingLength -= currentRun.TextSourceLength; + remainingLength -= currentRun.Length; i--; } @@ -350,7 +349,7 @@ namespace Avalonia.Media.TextFormatting } } - if (currentPosition + currentRun.TextSourceLength >= characterIndex && + if (currentPosition + currentRun.Length >= characterIndex && TryGetDistanceFromCharacterHit(currentRun, characterHit, currentPosition, remainingLength, flowDirection, out var distance, out _)) { return Math.Max(0, currentDistance + distance); @@ -358,8 +357,8 @@ namespace Avalonia.Media.TextFormatting //No hit hit found so we add the full width currentDistance += currentRun.Size.Width; - currentPosition += currentRun.TextSourceLength; - remainingLength -= currentRun.TextSourceLength; + currentPosition += currentRun.Length; + remainingLength -= currentRun.Length; } } else @@ -383,8 +382,8 @@ namespace Avalonia.Media.TextFormatting //No hit hit found so we add the full width currentDistance -= currentRun.Size.Width; - currentPosition += currentRun.TextSourceLength; - remainingLength -= currentRun.TextSourceLength; + currentPosition += currentRun.Length; + remainingLength -= currentRun.Length; } } @@ -412,16 +411,16 @@ namespace Avalonia.Media.TextFormatting { currentGlyphRun = shapedTextCharacters.GlyphRun; - if (currentPosition + remainingLength <= currentPosition + currentRun.Text.Length) + if (currentPosition + remainingLength <= currentPosition + currentRun.Length) { - characterHit = new CharacterHit(currentRun.Text.Start + remainingLength); + characterHit = new CharacterHit(currentPosition + remainingLength); distance = currentGlyphRun.GetDistanceFromCharacterHit(characterHit); return true; } - if (currentPosition + remainingLength == currentPosition + currentRun.Text.Length && isTrailingHit) + if (currentPosition + remainingLength == currentPosition + currentRun.Length && isTrailingHit) { if (currentGlyphRun.IsLeftToRight || flowDirection == FlowDirection.RightToLeft) { @@ -440,7 +439,7 @@ namespace Avalonia.Media.TextFormatting return true; } - if (characterIndex == currentPosition + currentRun.TextSourceLength) + if (characterIndex == currentPosition + currentRun.Length) { distance = currentRun.Size.Width; @@ -479,17 +478,22 @@ namespace Avalonia.Media.TextFormatting { case ShapedTextCharacters shapedRun: { - characterHit = shapedRun.GlyphRun.GetNextCaretCharacterHit(characterHit); + nextCharacterHit = shapedRun.GlyphRun.GetNextCaretCharacterHit(characterHit); break; } default: { - characterHit = new CharacterHit(currentPosition + currentRun.TextSourceLength); + nextCharacterHit = new CharacterHit(currentPosition + currentRun.Length); break; } } - return characterHit; + if (characterHit.FirstCharacterIndex + characterHit.TrailingLength == nextCharacterHit.FirstCharacterIndex + nextCharacterHit.TrailingLength) + { + return characterHit; + } + + return nextCharacterHit; } /// @@ -542,200 +546,182 @@ namespace Avalonia.Media.TextFormatting var characterLength = 0; var endX = startX; - var currentShapedRun = currentRun as ShapedTextCharacters; - TextRunBounds currentRunBounds; double combinedWidth; - if (currentPosition + currentRun.TextSourceLength <= firstTextSourceIndex) - { - startX += currentRun.Size.Width; - - currentPosition += currentRun.TextSourceLength; - - continue; - } - - if (currentShapedRun != null && !currentShapedRun.ShapedBuffer.IsLeftToRight) + if (currentRun is ShapedTextCharacters currentShapedRun) { - var rightToLeftIndex = index; - var rightToLeftWidth = currentShapedRun.Size.Width; + var firstCluster = currentShapedRun.GlyphRun.Metrics.FirstCluster; - while (rightToLeftIndex + 1 <= _textRuns.Count - 1 && _textRuns[rightToLeftIndex + 1] is ShapedTextCharacters nextShapedRun) + if (currentPosition + currentRun.Length <= firstTextSourceIndex) { - if (nextShapedRun == null || nextShapedRun.ShapedBuffer.IsLeftToRight) - { - break; - } + startX += currentRun.Size.Width; - rightToLeftIndex++; + currentPosition += currentRun.Length; - rightToLeftWidth += nextShapedRun.Size.Width; - - if (currentPosition + nextShapedRun.TextSourceLength > firstTextSourceIndex + textLength) - { - break; - } - - currentShapedRun = nextShapedRun; + continue; } - startX = startX + rightToLeftWidth; + if (currentShapedRun.ShapedBuffer.IsLeftToRight) + { + var startIndex = firstCluster + Math.Max(0, firstTextSourceIndex - currentPosition); - currentRunBounds = GetRightToLeftTextRunBounds(currentShapedRun, startX, firstTextSourceIndex, characterIndex, currentPosition, remainingLength); + double startOffset; - remainingLength -= currentRunBounds.Length; - currentPosition = currentRunBounds.TextSourceCharacterIndex + currentRunBounds.Length; - endX = currentRunBounds.Rectangle.Right; - startX = currentRunBounds.Rectangle.Left; + double endOffset; - var rightToLeftRunBounds = new List { currentRunBounds }; + startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex)); - for (int i = rightToLeftIndex - 1; i >= index; i--) - { - currentShapedRun = TextRuns[i] as ShapedTextCharacters; + endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength)); - if(currentShapedRun == null) - { - continue; - } + startX += startOffset; - currentRunBounds = GetRightToLeftTextRunBounds(currentShapedRun, startX, firstTextSourceIndex, characterIndex, currentPosition, remainingLength); + endX += endOffset; - rightToLeftRunBounds.Insert(0, currentRunBounds); + var endHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _); - remainingLength -= currentRunBounds.Length; - startX = currentRunBounds.Rectangle.Left; + var startHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _); - currentPosition += currentRunBounds.Length; + characterLength = Math.Abs(endHit.FirstCharacterIndex + endHit.TrailingLength - startHit.FirstCharacterIndex - startHit.TrailingLength); + + currentDirection = FlowDirection.LeftToRight; } + else + { + var rightToLeftIndex = index; + var rightToLeftWidth = currentShapedRun.Size.Width; - combinedWidth = endX - startX; + while (rightToLeftIndex + 1 <= _textRuns.Count - 1 && _textRuns[rightToLeftIndex + 1] is ShapedTextCharacters nextShapedRun) + { + if (nextShapedRun == null || nextShapedRun.ShapedBuffer.IsLeftToRight) + { + break; + } - currentRect = new Rect(startX, 0, combinedWidth, Height); + rightToLeftIndex++; - currentDirection = FlowDirection.RightToLeft; + rightToLeftWidth += nextShapedRun.Size.Width; - if (!MathUtilities.IsZero(combinedWidth)) - { - result.Add(new TextBounds(currentRect, currentDirection, rightToLeftRunBounds)); - } + if (currentPosition + nextShapedRun.Length > firstTextSourceIndex + textLength) + { + break; + } - startX = endX; - } - else - { - if (currentShapedRun != null) - { - var offset = Math.Max(0, firstTextSourceIndex - currentPosition); + currentShapedRun = nextShapedRun; + } - currentPosition += offset; + startX += rightToLeftWidth; - var startIndex = currentRun.Text.Start + offset; + currentRunBounds = GetRightToLeftTextRunBounds(currentShapedRun, startX, firstTextSourceIndex, characterIndex, currentPosition, remainingLength); - double startOffset; - double endOffset; + remainingLength -= currentRunBounds.Length; + currentPosition = currentRunBounds.TextSourceCharacterIndex + currentRunBounds.Length; + endX = currentRunBounds.Rectangle.Right; + startX = currentRunBounds.Rectangle.Left; - if (currentShapedRun.ShapedBuffer.IsLeftToRight) - { - startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex)); + var rightToLeftRunBounds = new List { currentRunBounds }; - endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength)); - } - else + for (int i = rightToLeftIndex - 1; i >= index; i--) { - endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex)); - - if (currentPosition < startIndex) - { - startOffset = endOffset; - } - else + if (TextRuns[i] is not ShapedTextCharacters) { - startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength)); + continue; } - } - startX += startOffset; + currentShapedRun = (ShapedTextCharacters)TextRuns[i]; - endX += endOffset; + currentRunBounds = GetRightToLeftTextRunBounds(currentShapedRun, startX, firstTextSourceIndex, characterIndex, currentPosition, remainingLength); - var endHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _); - var startHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _); + rightToLeftRunBounds.Insert(0, currentRunBounds); - characterLength = Math.Abs(endHit.FirstCharacterIndex + endHit.TrailingLength - startHit.FirstCharacterIndex - startHit.TrailingLength); + remainingLength -= currentRunBounds.Length; + startX = currentRunBounds.Rectangle.Left; - currentDirection = FlowDirection.LeftToRight; - } - else - { - if (currentPosition + currentRun.TextSourceLength <= firstTextSourceIndex) - { - startX += currentRun.Size.Width; + currentPosition += currentRunBounds.Length; + } - currentPosition += currentRun.TextSourceLength; + combinedWidth = endX - startX; - continue; - } + currentRect = new Rect(startX, 0, combinedWidth, Height); + + currentDirection = FlowDirection.RightToLeft; - if (currentPosition < firstTextSourceIndex) + if (!MathUtilities.IsZero(combinedWidth)) { - startX += currentRun.Size.Width; + result.Add(new TextBounds(currentRect, currentDirection, rightToLeftRunBounds)); } - if (currentPosition + currentRun.TextSourceLength <= characterIndex) - { - endX += currentRun.Size.Width; + startX = endX; + } + } + else + { + if (currentPosition + currentRun.Length <= firstTextSourceIndex) + { + startX += currentRun.Size.Width; - characterLength = currentRun.TextSourceLength; - } + currentPosition += currentRun.Length; + + continue; } - if (endX < startX) + if (currentPosition < firstTextSourceIndex) { - (endX, startX) = (startX, endX); + startX += currentRun.Size.Width; } - //Lines that only contain a linebreak need to be covered here - if (characterLength == 0) + if (currentPosition + currentRun.Length <= characterIndex) { - characterLength = NewLineLength; + endX += currentRun.Size.Width; + + characterLength = currentRun.Length; } + } - combinedWidth = endX - startX; + if (endX < startX) + { + (endX, startX) = (startX, endX); + } - currentRunBounds = new TextRunBounds(new Rect(startX, 0, combinedWidth, Height), currentPosition, characterLength, currentRun); + //Lines that only contain a linebreak need to be covered here + if (characterLength == 0) + { + characterLength = NewLineLength; + } - currentPosition += characterLength; + combinedWidth = endX - startX; - remainingLength -= characterLength; + currentRunBounds = new TextRunBounds(new Rect(startX, 0, combinedWidth, Height), currentPosition, characterLength, currentRun); - startX = endX; + currentPosition += characterLength; - if (currentRunBounds.TextRun != null && !MathUtilities.IsZero(combinedWidth) || NewLineLength > 0) - { - if (result.Count > 0 && lastDirection == currentDirection && MathUtilities.AreClose(currentRect.Left, lastRunBounds.Rectangle.Right)) - { - currentRect = currentRect.WithWidth(currentWidth + combinedWidth); + remainingLength -= characterLength; - var textBounds = result[result.Count - 1]; + startX = endX; - textBounds.Rectangle = currentRect; + if (currentRunBounds.TextRun != null && !MathUtilities.IsZero(combinedWidth) || NewLineLength > 0) + { + if (result.Count > 0 && lastDirection == currentDirection && MathUtilities.AreClose(currentRect.Left, lastRunBounds.Rectangle.Right)) + { + currentRect = currentRect.WithWidth(currentWidth + combinedWidth); - textBounds.TextRunBounds.Add(currentRunBounds); - } - else - { - currentRect = currentRunBounds.Rectangle; + var textBounds = result[result.Count - 1]; - result.Add(new TextBounds(currentRect, currentDirection, new List { currentRunBounds })); - } + textBounds.Rectangle = currentRect; + + textBounds.TextRunBounds.Add(currentRunBounds); } + else + { + currentRect = currentRunBounds.Rectangle; - lastRunBounds = currentRunBounds; + result.Add(new TextBounds(currentRect, currentDirection, new List { currentRunBounds })); + } } + lastRunBounds = currentRunBounds; + currentWidth += combinedWidth; if (remainingLength <= 0 || currentPosition >= characterIndex) @@ -771,11 +757,11 @@ namespace Avalonia.Media.TextFormatting continue; } - if (currentPosition + currentRun.TextSourceLength < firstTextSourceIndex) + if (currentPosition + currentRun.Length < firstTextSourceIndex) { startX -= currentRun.Size.Width; - currentPosition += currentRun.TextSourceLength; + currentPosition += currentRun.Length; continue; } @@ -789,7 +775,7 @@ namespace Avalonia.Media.TextFormatting currentPosition += offset; - var startIndex = currentRun.Text.Start + offset; + var startIndex = currentPosition; double startOffset; double endOffset; @@ -827,7 +813,7 @@ namespace Avalonia.Media.TextFormatting } else { - if (currentPosition + currentRun.TextSourceLength <= characterIndex) + if (currentPosition + currentRun.Length <= characterIndex) { endX -= currentRun.Size.Width; } @@ -836,7 +822,7 @@ namespace Avalonia.Media.TextFormatting { startX -= currentRun.Size.Width; - characterLength = currentRun.TextSourceLength; + characterLength = currentRun.Length; } } @@ -905,7 +891,7 @@ namespace Avalonia.Media.TextFormatting currentPosition += offset; - var startIndex = currentRun.Text.Start + offset; + var startIndex = currentPosition; double startOffset; double endOffset; @@ -1172,12 +1158,12 @@ namespace Avalonia.Media.TextFormatting return true; } - var characterIndex = codepointIndex - shapedRun.Text.Start; + //var characterIndex = codepointIndex - shapedRun.Text.Start; - if (characterIndex < 0 && shapedRun.ShapedBuffer.IsLeftToRight) - { - foundCharacterHit = new CharacterHit(foundCharacterHit.FirstCharacterIndex); - } + //if (characterIndex < 0 && shapedRun.ShapedBuffer.IsLeftToRight) + //{ + // foundCharacterHit = new CharacterHit(foundCharacterHit.FirstCharacterIndex); + //} nextCharacterHit = isAtEnd || characterHit.TrailingLength != 0 ? foundCharacterHit : @@ -1196,7 +1182,7 @@ namespace Avalonia.Media.TextFormatting if (textPosition == currentPosition) { - nextCharacterHit = new CharacterHit(currentPosition + currentRun.TextSourceLength); + nextCharacterHit = new CharacterHit(currentPosition + currentRun.Length); return true; } @@ -1205,7 +1191,7 @@ namespace Avalonia.Media.TextFormatting } } - currentPosition += currentRun.TextSourceLength; + currentPosition += currentRun.Length; runIndex++; } @@ -1271,7 +1257,7 @@ namespace Avalonia.Media.TextFormatting } default: { - if (characterIndex == currentPosition + currentRun.TextSourceLength) + if (characterIndex == currentPosition + currentRun.Length) { previousCharacterHit = new CharacterHit(currentPosition); @@ -1282,7 +1268,7 @@ namespace Avalonia.Media.TextFormatting } } - currentPosition -= currentRun.TextSourceLength; + currentPosition -= currentRun.Length; runIndex--; } @@ -1310,18 +1296,25 @@ namespace Avalonia.Media.TextFormatting { case ShapedTextCharacters shapedRun: { + var firstCluster = shapedRun.GlyphRun.Metrics.FirstCluster; + + if (firstCluster > codepointIndex) + { + break; + } + if (previousRun is ShapedTextCharacters previousShaped && !previousShaped.ShapedBuffer.IsLeftToRight) { if (shapedRun.ShapedBuffer.IsLeftToRight) { - if (currentRun.Text.Start >= codepointIndex) + if (firstCluster >= codepointIndex) { return --runIndex; } } else { - if (codepointIndex > currentRun.Text.Start + currentRun.Text.Length) + if (codepointIndex > firstCluster + currentRun.Length) { return --runIndex; } @@ -1330,15 +1323,15 @@ namespace Avalonia.Media.TextFormatting if (direction == LogicalDirection.Forward) { - if (codepointIndex >= currentRun.Text.Start && codepointIndex <= currentRun.Text.End) + if (codepointIndex >= firstCluster && codepointIndex <= firstCluster + currentRun.Length) { return runIndex; } } else { - if (codepointIndex > currentRun.Text.Start && - codepointIndex <= currentRun.Text.Start + currentRun.Text.Length) + if (codepointIndex > firstCluster && + codepointIndex <= firstCluster + currentRun.Length) { return runIndex; } @@ -1349,6 +1342,8 @@ namespace Avalonia.Media.TextFormatting return runIndex; } + textPosition += currentRun.Length; + break; } @@ -1364,13 +1359,14 @@ namespace Avalonia.Media.TextFormatting return runIndex; } + textPosition += currentRun.Length; + break; } } runIndex++; previousRun = currentRun; - textPosition += currentRun.TextSourceLength; } return runIndex; @@ -1401,7 +1397,7 @@ namespace Avalonia.Media.TextFormatting case ShapedTextCharacters textRun: { var textMetrics = - new TextMetrics(textRun.Properties.Typeface, textRun.Properties.FontRenderingEmSize); + new TextMetrics(textRun.Properties.Typeface.GlyphTypeface, textRun.Properties.FontRenderingEmSize); if (fontRenderingEmSize < textRun.Properties.FontRenderingEmSize) { @@ -1432,7 +1428,7 @@ namespace Avalonia.Media.TextFormatting { width = widthIncludingWhitespace + textRun.GlyphRun.Metrics.Width; trailingWhitespaceLength = textRun.GlyphRun.Metrics.TrailingWhitespaceLength; - newLineLength = textRun.GlyphRun.Metrics.NewlineLength; + newLineLength = textRun.GlyphRun.Metrics.NewLineLength; } widthIncludingWhitespace += textRun.GlyphRun.Metrics.WidthIncludingTrailingWhitespace; diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineMetrics.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineMetrics.cs index 1799c9d3db..40a7f6167a 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineMetrics.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineMetrics.cs @@ -6,13 +6,13 @@ /// public readonly struct TextLineMetrics { - public TextLineMetrics(bool hasOverflowed, double height, int newLineLength, double start, double textBaseline, + public TextLineMetrics(bool hasOverflowed, double height, int newlineLength, double start, double textBaseline, int trailingWhitespaceLength, double width, double widthIncludingTrailingWhitespace) { HasOverflowed = hasOverflowed; Height = height; - NewLineLength = newLineLength; + NewlineLength = newlineLength; Start = start; TextBaseline = textBaseline; TrailingWhitespaceLength = trailingWhitespaceLength; @@ -33,7 +33,7 @@ /// /// Gets the number of newline characters at the end of a line. /// - public int NewLineLength { get; } + public int NewlineLength { get; } /// /// Gets the distance from the start of a paragraph to the starting point of a line. diff --git a/src/Avalonia.Base/Media/TextFormatting/TextMetrics.cs b/src/Avalonia.Base/Media/TextFormatting/TextMetrics.cs index 0382e66b5a..dc21c9b6f2 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextMetrics.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextMetrics.cs @@ -5,9 +5,9 @@ /// public readonly struct TextMetrics { - public TextMetrics(Typeface typeface, double fontRenderingEmSize) + public TextMetrics(IGlyphTypeface glyphTypeface, double fontRenderingEmSize) { - var fontMetrics = typeface.GlyphTypeface.Metrics; + var fontMetrics = glyphTypeface.Metrics; var scale = fontRenderingEmSize / fontMetrics.DesignEmHeight; diff --git a/src/Avalonia.Base/Media/TextFormatting/TextRun.cs b/src/Avalonia.Base/Media/TextFormatting/TextRun.cs index 26c3f8947a..0306054767 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextRun.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextRun.cs @@ -1,5 +1,4 @@ using System.Diagnostics; -using Avalonia.Utilities; namespace Avalonia.Media.TextFormatting { @@ -14,12 +13,12 @@ namespace Avalonia.Media.TextFormatting /// /// Gets the text source length. /// - public virtual int TextSourceLength => DefaultTextSourceLength; + public virtual int Length => DefaultTextSourceLength; /// /// Gets the text run's text. /// - public virtual ReadOnlySlice Text => default; + public virtual CharacterBufferReference CharacterBufferReference => default; /// /// A set of properties shared by every characters in the run @@ -41,9 +40,11 @@ namespace Avalonia.Media.TextFormatting { unsafe { - fixed (char* charsPtr = _textRun.Text.Span) + var characterBuffer = _textRun.CharacterBufferReference.CharacterBuffer; + + fixed (char* charsPtr = characterBuffer.Span) { - return new string(charsPtr, 0, _textRun.Text.Length); + return new string(charsPtr, 0, characterBuffer.Span.Length); } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextShaper.cs b/src/Avalonia.Base/Media/TextFormatting/TextShaper.cs index 615b1553b6..4aacec7c48 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextShaper.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextShaper.cs @@ -1,7 +1,5 @@ using System; -using System.Globalization; using Avalonia.Platform; -using Avalonia.Utilities; namespace Avalonia.Media.TextFormatting { @@ -45,9 +43,14 @@ namespace Avalonia.Media.TextFormatting } /// - public ShapedBuffer ShapeText(ReadOnlySlice text, TextShaperOptions options) + public ShapedBuffer ShapeText(CharacterBufferReference text, int length, TextShaperOptions options = default) { - return _platformImpl.ShapeText(text, options); + return _platformImpl.ShapeText(text, length, options); + } + + public ShapedBuffer ShapeText(string text, TextShaperOptions options = default) + { + return ShapeText(new CharacterBufferReference(text), text.Length, options); } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs b/src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs index 670d94e928..1de04ad061 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using Avalonia.Utilities; namespace Avalonia.Media.TextFormatting { @@ -15,7 +14,7 @@ namespace Avalonia.Media.TextFormatting /// Text used as collapsing symbol. /// Width in which collapsing is constrained to. /// Text run properties of ellipsis symbol. - public TextTrailingCharacterEllipsis(ReadOnlySlice ellipsis, double width, TextRunProperties textRunProperties) + public TextTrailingCharacterEllipsis(string ellipsis, double width, TextRunProperties textRunProperties) { Width = width; Symbol = new TextCharacters(ellipsis, textRunProperties); diff --git a/src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs b/src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs index dbffbdf060..7c94715aa4 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs @@ -16,7 +16,7 @@ namespace Avalonia.Media.TextFormatting /// width in which collapsing is constrained to. /// text run properties of ellipsis symbol. public TextTrailingWordEllipsis( - ReadOnlySlice ellipsis, + string ellipsis, double width, TextRunProperties textRunProperties ) diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs index 72df2815a9..0c51b0898d 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. // Ported from: https://github.com/SixLabors/Fonts/ +using System; using Avalonia.Utilities; namespace Avalonia.Media.TextFormatting.Unicode @@ -63,7 +64,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// Appends text to the bidi data. /// /// The text to process. - public void Append(ReadOnlySlice text) + public void Append(CharacterBufferRange text) { _classes.Add(text.Length); _pairedBracketTypes.Add(text.Length); diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs index de40839853..9e5186552e 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs @@ -1,6 +1,5 @@ -using System; +using System.Collections.Generic; using System.Runtime.CompilerServices; -using Avalonia.Utilities; namespace Avalonia.Media.TextFormatting.Unicode { @@ -166,11 +165,11 @@ namespace Avalonia.Media.TextFormatting.Unicode /// The index to read at. /// The count of character that were read. /// - public static Codepoint ReadAt(ReadOnlySpan text, int index, out int count) + public static Codepoint ReadAt(IReadOnlyList text, int index, out int count) { count = 1; - if (index >= text.Length) + if (index >= text.Count) { return ReplacementCodepoint; } @@ -184,7 +183,7 @@ namespace Avalonia.Media.TextFormatting.Unicode { hi = code; - if (index + 1 == text.Length) + if (index + 1 == text.Count) { return ReplacementCodepoint; } diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/CodepointEnumerator.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/CodepointEnumerator.cs index 9e1f748ebb..330ead476a 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/CodepointEnumerator.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/CodepointEnumerator.cs @@ -1,12 +1,13 @@ -using Avalonia.Utilities; +using System; namespace Avalonia.Media.TextFormatting.Unicode { public ref struct CodepointEnumerator { - private ReadOnlySlice _text; + private CharacterBufferRange _text; + private int _pos; - public CodepointEnumerator(ReadOnlySlice text) + public CodepointEnumerator(CharacterBufferRange text) { _text = text; Current = Codepoint.ReplacementCodepoint; diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/Grapheme.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/Grapheme.cs index f268340eb9..69015fb17d 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/Grapheme.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/Grapheme.cs @@ -1,13 +1,13 @@ -using Avalonia.Utilities; +using System; namespace Avalonia.Media.TextFormatting.Unicode { /// /// Represents the smallest unit of a writing system of any given language. /// - public readonly struct Grapheme + public readonly ref struct Grapheme { - public Grapheme(Codepoint firstCodepoint, ReadOnlySlice text) + public Grapheme(Codepoint firstCodepoint, ReadOnlySpan text) { FirstCodepoint = firstCodepoint; Text = text; @@ -21,6 +21,6 @@ namespace Avalonia.Media.TextFormatting.Unicode /// /// The text that is representing the . /// - public ReadOnlySlice Text { get; } + public ReadOnlySpan Text { get; } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/GraphemeEnumerator.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/GraphemeEnumerator.cs index 1e4ac8fe0f..5ca120c856 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/GraphemeEnumerator.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/GraphemeEnumerator.cs @@ -3,16 +3,16 @@ // // Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. +using System.Collections.Generic; using System.Runtime.InteropServices; -using Avalonia.Utilities; namespace Avalonia.Media.TextFormatting.Unicode { public ref struct GraphemeEnumerator { - private ReadOnlySlice _text; + private CharacterBufferRange _text; - public GraphemeEnumerator(ReadOnlySlice text) + public GraphemeEnumerator(CharacterBufferRange text) { _text = text; Current = default; @@ -187,7 +187,7 @@ namespace Avalonia.Media.TextFormatting.Unicode var text = _text.Take(processor.CurrentCodeUnitOffset); - Current = new Grapheme(firstCodepoint, text); + Current = new Grapheme(firstCodepoint, text.Span); _text = _text.Skip(processor.CurrentCodeUnitOffset); @@ -197,10 +197,10 @@ namespace Avalonia.Media.TextFormatting.Unicode [StructLayout(LayoutKind.Auto)] private ref struct Processor { - private readonly ReadOnlySlice _buffer; + private readonly CharacterBufferRange _buffer; private int _codeUnitLengthOfCurrentScalar; - internal Processor(ReadOnlySlice buffer) + internal Processor(CharacterBufferRange buffer) { _buffer = buffer; _codeUnitLengthOfCurrentScalar = 0; diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreakEnumerator.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreakEnumerator.cs index e12a7c06f1..41a476c17e 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreakEnumerator.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreakEnumerator.cs @@ -2,7 +2,8 @@ // Licensed under the Apache License, Version 2.0. // Ported from: https://github.com/SixLabors/Fonts/ -using Avalonia.Utilities; +using System; +using System.Collections.Generic; namespace Avalonia.Media.TextFormatting.Unicode { @@ -12,7 +13,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// public ref struct LineBreakEnumerator { - private readonly ReadOnlySlice _text; + private readonly IReadOnlyList _text; private int _position; private int _lastPosition; private LineBreakClass _currentClass; @@ -28,7 +29,7 @@ namespace Avalonia.Media.TextFormatting.Unicode private int _lb30a; private bool _lb31; - public LineBreakEnumerator(ReadOnlySlice text) + public LineBreakEnumerator(IReadOnlyList text) : this() { _text = text; @@ -62,7 +63,7 @@ namespace Avalonia.Media.TextFormatting.Unicode _lb30a = 0; } - while (_position < _text.Length) + while (_position < _text.Count) { _lastPosition = _position; var lastClass = _nextClass; @@ -92,11 +93,11 @@ namespace Avalonia.Media.TextFormatting.Unicode } } - if (_position >= _text.Length) + if (_position >= _text.Count) { - if (_lastPosition < _text.Length) + if (_lastPosition < _text.Count) { - _lastPosition = _text.Length; + _lastPosition = _text.Count; var required = false; diff --git a/src/Avalonia.Base/Media/TextLeadingPrefixTrimming.cs b/src/Avalonia.Base/Media/TextLeadingPrefixTrimming.cs index 19ca1a0198..7ba25eb005 100644 --- a/src/Avalonia.Base/Media/TextLeadingPrefixTrimming.cs +++ b/src/Avalonia.Base/Media/TextLeadingPrefixTrimming.cs @@ -1,21 +1,16 @@ using Avalonia.Media.TextFormatting; -using Avalonia.Utilities; namespace Avalonia.Media { public sealed class TextLeadingPrefixTrimming : TextTrimming { - private readonly ReadOnlySlice _ellipsis; + private readonly string _ellipsis; private readonly int _prefixLength; - public TextLeadingPrefixTrimming(char ellipsis, int prefixLength) : this(new[] { ellipsis }, prefixLength) - { - } - - public TextLeadingPrefixTrimming(char[] ellipsis, int prefixLength) + public TextLeadingPrefixTrimming(string ellipsis, int prefixLength) { _prefixLength = prefixLength; - _ellipsis = new ReadOnlySlice(ellipsis); + _ellipsis = ellipsis; } public override TextCollapsingProperties CreateCollapsingProperties(TextCollapsingCreateInfo createInfo) diff --git a/src/Avalonia.Base/Media/TextTrailingTrimming.cs b/src/Avalonia.Base/Media/TextTrailingTrimming.cs index 5bb35f0ba7..2edbaabbc6 100644 --- a/src/Avalonia.Base/Media/TextTrailingTrimming.cs +++ b/src/Avalonia.Base/Media/TextTrailingTrimming.cs @@ -1,21 +1,16 @@ using Avalonia.Media.TextFormatting; -using Avalonia.Utilities; namespace Avalonia.Media { public sealed class TextTrailingTrimming : TextTrimming { - private readonly ReadOnlySlice _ellipsis; + private readonly string _ellipsis; private readonly bool _isWordBased; - - public TextTrailingTrimming(char ellipsis, bool isWordBased) : this(new[] {ellipsis}, isWordBased) - { - } - public TextTrailingTrimming(char[] ellipsis, bool isWordBased) + public TextTrailingTrimming(string ellipsis, bool isWordBased) { _isWordBased = isWordBased; - _ellipsis = new ReadOnlySlice(ellipsis); + _ellipsis = ellipsis; } public override TextCollapsingProperties CreateCollapsingProperties(TextCollapsingCreateInfo createInfo) diff --git a/src/Avalonia.Base/Media/TextTrimming.cs b/src/Avalonia.Base/Media/TextTrimming.cs index e2737210be..34642c11df 100644 --- a/src/Avalonia.Base/Media/TextTrimming.cs +++ b/src/Avalonia.Base/Media/TextTrimming.cs @@ -8,7 +8,7 @@ namespace Avalonia.Media /// public abstract class TextTrimming { - internal const char DefaultEllipsisChar = '\u2026'; + internal const string DefaultEllipsisChar = "\u2026"; /// /// Text is not trimmed. diff --git a/src/Avalonia.Base/Platform/ITextShaperImpl.cs b/src/Avalonia.Base/Platform/ITextShaperImpl.cs index 10e58b7d0b..ff91097eda 100644 --- a/src/Avalonia.Base/Platform/ITextShaperImpl.cs +++ b/src/Avalonia.Base/Platform/ITextShaperImpl.cs @@ -1,6 +1,5 @@ using Avalonia.Media.TextFormatting; using Avalonia.Metadata; -using Avalonia.Utilities; namespace Avalonia.Platform { @@ -13,9 +12,9 @@ namespace Avalonia.Platform /// /// Shapes the specified region within the text and returns a shaped buffer. /// - /// The text. + /// The text buffer. /// Text shaper options to customize the shaping process. /// A shaped glyph run. - ShapedBuffer ShapeText(ReadOnlySlice text, TextShaperOptions options); + ShapedBuffer ShapeText(CharacterBufferReference text, int length, TextShaperOptions options); } } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs b/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs index 2019ad6faa..18cb7a6308 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs @@ -1,6 +1,7 @@ using System; using System.Diagnostics; using System.Globalization; +using System.Linq; using Avalonia.Media; using Avalonia.Media.TextFormatting; using Avalonia.Platform; @@ -31,7 +32,7 @@ internal class FpsCounter { var s = new string((char)c, 1); var glyph = typeface.GetGlyph((uint)(s[0])); - _runs[c - FirstChar] = new GlyphRun(typeface, 18, new ReadOnlySlice(s.AsMemory()), new ushort[] { glyph }); + _runs[c - FirstChar] = new GlyphRun(typeface, 18, s.ToArray(), new ushort[] { glyph }); } } diff --git a/src/Avalonia.Base/Utilities/ArraySlice.cs b/src/Avalonia.Base/Utilities/ArraySlice.cs index 482f807fe0..39c0cd5556 100644 --- a/src/Avalonia.Base/Utilities/ArraySlice.cs +++ b/src/Avalonia.Base/Utilities/ArraySlice.cs @@ -111,14 +111,6 @@ namespace Avalonia.Utilities } } - /// - /// Defines an implicit conversion of a to a - /// - public static implicit operator ReadOnlySlice(ArraySlice slice) - { - return new ReadOnlySlice(slice._data, 0, slice.Length, slice.Start); - } - /// /// Defines an implicit conversion of an array to a /// diff --git a/src/Avalonia.Base/Utilities/ReadOnlySlice.cs b/src/Avalonia.Base/Utilities/ReadOnlySlice.cs deleted file mode 100644 index 583a3139b9..0000000000 --- a/src/Avalonia.Base/Utilities/ReadOnlySlice.cs +++ /dev/null @@ -1,239 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Diagnostics; -using System.Runtime.CompilerServices; - -namespace Avalonia.Utilities -{ - /// - /// ReadOnlySlice enables the ability to work with a sequence within a region of memory and retains the position in within that region. - /// - /// The type of elements in the slice. - [DebuggerTypeProxy(typeof(ReadOnlySlice<>.ReadOnlySliceDebugView))] - public readonly struct ReadOnlySlice : IReadOnlyList where T : struct - { - private readonly int _bufferOffset; - - /// - /// Gets an empty - /// - public static ReadOnlySlice Empty => new ReadOnlySlice(Array.Empty()); - - private readonly ReadOnlyMemory _buffer; - - public ReadOnlySlice(ReadOnlyMemory buffer) : this(buffer, 0, buffer.Length) { } - - public ReadOnlySlice(ReadOnlyMemory buffer, int start, int length, int bufferOffset = 0) - { -#if DEBUG - if (start.CompareTo(0) < 0) - { - throw new ArgumentOutOfRangeException(nameof (start)); - } - - if (length.CompareTo(buffer.Length) > 0) - { - throw new ArgumentOutOfRangeException(nameof (length)); - } -#endif - - _buffer = buffer; - Start = start; - Length = length; - _bufferOffset = bufferOffset; - } - - /// - /// Gets the start. - /// - /// - /// The start. - /// - public int Start { get; } - - /// - /// Gets the end. - /// - /// - /// The end. - /// - public int End => Start + Length - 1; - - /// - /// Gets the length. - /// - /// - /// The length. - /// - public int Length { get; } - - /// - /// Gets a value that indicates whether this instance of is Empty. - /// - public bool IsEmpty => Length == 0; - - /// - /// Get the underlying span. - /// - public ReadOnlySpan Span => _buffer.Span.Slice(_bufferOffset, Length); - - /// - /// Get the buffer offset. - /// - public int BufferOffset => _bufferOffset; - - /// - /// Get the underlying buffer. - /// - public ReadOnlyMemory Buffer => _buffer; - - /// - /// Returns a value to specified element of the slice. - /// - /// The index of the element to return. - /// The . - /// - /// Thrown when index less than 0 or index greater than or equal to . - /// - public T this[int index] - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get - { -#if DEBUG - if (index.CompareTo(0) < 0 || index.CompareTo(Length) > 0) - { - throw new ArgumentOutOfRangeException(nameof (index)); - } -#endif - return Span[index]; - } - } - - /// - /// Returns a sub slice of elements that start at the specified index and has the specified number of elements. - /// - /// The start of the sub slice. - /// The length of the sub slice. - /// A that contains the specified number of elements from the specified start. - public ReadOnlySlice AsSlice(int start, int length) - { - if (IsEmpty) - { - return this; - } - - if (length == 0) - { - return Empty; - } - - if (start < 0 || _bufferOffset + start > _buffer.Length - 1) - { - throw new ArgumentOutOfRangeException(nameof(start)); - } - - if (_bufferOffset + start + length > _buffer.Length) - { - throw new ArgumentOutOfRangeException(nameof(length)); - } - - return new ReadOnlySlice(_buffer, start, length, _bufferOffset); - } - - /// - /// Returns a specified number of contiguous elements from the start of the slice. - /// - /// The number of elements to return. - /// A that contains the specified number of elements from the start of this slice. - public ReadOnlySlice Take(int length) - { - if (IsEmpty) - { - return this; - } - - if (length > Length) - { - throw new ArgumentOutOfRangeException(nameof(length)); - } - - return new ReadOnlySlice(_buffer, Start, length, _bufferOffset); - } - - /// - /// Bypasses a specified number of elements in the slice and then returns the remaining elements. - /// - /// The number of elements to skip before returning the remaining elements. - /// A that contains the elements that occur after the specified index in this slice. - public ReadOnlySlice Skip(int length) - { - if (IsEmpty) - { - return this; - } - - if (length > Length) - { - throw new ArgumentOutOfRangeException(nameof(length)); - } - - return new ReadOnlySlice(_buffer, Start + length, Length - length, _bufferOffset + length); - } - - /// - /// Returns an enumerator for the slice. - /// - public ImmutableReadOnlyListStructEnumerator GetEnumerator() - { - return new ImmutableReadOnlyListStructEnumerator(this); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - - int IReadOnlyCollection.Count => Length; - - T IReadOnlyList.this[int index] => this[index]; - - public static implicit operator ReadOnlySlice(T[] array) - { - return new ReadOnlySlice(array); - } - - public static implicit operator ReadOnlySlice(ReadOnlyMemory memory) - { - return new ReadOnlySlice(memory); - } - - public static implicit operator ReadOnlySpan(ReadOnlySlice slice) => slice.Span; - - internal class ReadOnlySliceDebugView - { - private readonly ReadOnlySlice _readOnlySlice; - - public ReadOnlySliceDebugView(ReadOnlySlice readOnlySlice) - { - _readOnlySlice = readOnlySlice; - } - - public int Start => _readOnlySlice.Start; - - public int End => _readOnlySlice.End; - - public int Length => _readOnlySlice.Length; - - public bool IsEmpty => _readOnlySlice.IsEmpty; - - public ReadOnlySpan Items => _readOnlySlice.Span; - } - } -} diff --git a/src/Avalonia.Controls/Documents/LineBreak.cs b/src/Avalonia.Controls/Documents/LineBreak.cs index 108a38d86b..ee31b85be9 100644 --- a/src/Avalonia.Controls/Documents/LineBreak.cs +++ b/src/Avalonia.Controls/Documents/LineBreak.cs @@ -4,7 +4,7 @@ using System.Text; using Avalonia.Media.TextFormatting; using Avalonia.Metadata; -namespace Avalonia.Controls.Documents +namespace Avalonia.Controls.Documents { /// /// LineBreak element that forces a line breaking. @@ -21,7 +21,7 @@ namespace Avalonia.Controls.Documents internal override void BuildTextRun(IList textRuns) { - var text = Environment.NewLine.AsMemory(); + var text = Environment.NewLine; var textRunProperties = CreateTextRunProperties(); diff --git a/src/Avalonia.Controls/Documents/Run.cs b/src/Avalonia.Controls/Documents/Run.cs index 2bd66b8a64..5d7b8998e6 100644 --- a/src/Avalonia.Controls/Documents/Run.cs +++ b/src/Avalonia.Controls/Documents/Run.cs @@ -52,7 +52,7 @@ namespace Avalonia.Controls.Documents internal override void BuildTextRun(IList textRuns) { - var text = (Text ?? "").AsMemory(); + var text = Text ?? ""; var textRunProperties = CreateTextRunProperties(); diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index c8e05e5cb3..08156ae00f 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -630,7 +630,7 @@ namespace Avalonia.Controls } else { - textSource = new SimpleTextSource((text ?? "").AsMemory(), defaultProperties); + textSource = new SimpleTextSource(text ?? "", defaultProperties); } return new TextLayout( @@ -829,12 +829,12 @@ namespace Avalonia.Controls protected readonly struct SimpleTextSource : ITextSource { - private readonly ReadOnlySlice _text; + private readonly CharacterBufferRange _text; private readonly TextRunProperties _defaultProperties; - public SimpleTextSource(ReadOnlySlice text, TextRunProperties defaultProperties) + public SimpleTextSource(string text, TextRunProperties defaultProperties) { - _text = text; + _text = new CharacterBufferRange(new CharacterBufferReference(text), text.Length); _defaultProperties = defaultProperties; } @@ -852,7 +852,7 @@ namespace Avalonia.Controls return new TextEndOfParagraph(); } - return new TextCharacters(runText, _defaultProperties); + return new TextCharacters(runText.CharacterBufferReference, runText.Length, _defaultProperties); } } @@ -873,21 +873,28 @@ namespace Avalonia.Controls foreach (var textRun in _textRuns) { - if (textRun.TextSourceLength == 0) + if (textRun.Length == 0) { continue; } - if (textSourceIndex >= currentPosition + textRun.TextSourceLength) + if (textSourceIndex >= currentPosition + textRun.Length) { - currentPosition += textRun.TextSourceLength; + currentPosition += textRun.Length; continue; } - if (textRun is TextCharacters) + if (textRun is TextCharacters) { - return new TextCharacters(textRun.Text.Skip(Math.Max(0, textSourceIndex - currentPosition)), textRun.Properties!); + var characterBufferReference = textRun.CharacterBufferReference; + + var skip = Math.Max(0, textSourceIndex - currentPosition); + + return new TextCharacters( + new CharacterBufferReference(characterBufferReference.CharacterBuffer, characterBufferReference.OffsetToFirstChar + skip), + textRun.Length - skip, + textRun.Properties!); } return textRun; diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 699170df5a..1bdec878d9 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -961,7 +961,9 @@ namespace Avalonia.Controls var length = 0; - var graphemeEnumerator = new GraphemeEnumerator(input.AsMemory()); + var inputRange = new CharacterBufferRange(new CharacterBufferReference(input), input.Length); + + var graphemeEnumerator = new GraphemeEnumerator(inputRange); while (graphemeEnumerator.MoveNext()) { diff --git a/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs b/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs index 6a79760011..52815b943d 100644 --- a/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs +++ b/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs @@ -77,12 +77,14 @@ namespace Avalonia.Controls foreach (var run in textLine.TextRuns) { - if(run.Text.Length > 0) + if(run.Length > 0) { + var characterBufferRange = new CharacterBufferRange(run.CharacterBufferReference, run.Length); + #if NET6_0 - builder.Append(run.Text.Span); + builder.Append(characterBufferRange.Span); #else - builder.Append(run.Text.Span.ToArray()); + builder.Append(characterBufferRange.Span.ToArray()); #endif } } diff --git a/src/Avalonia.Headless/HeadlessPlatformStubs.cs b/src/Avalonia.Headless/HeadlessPlatformStubs.cs index 688b8e0398..1cc0fa73bb 100644 --- a/src/Avalonia.Headless/HeadlessPlatformStubs.cs +++ b/src/Avalonia.Headless/HeadlessPlatformStubs.cs @@ -145,13 +145,15 @@ namespace Avalonia.Headless class HeadlessTextShaperStub : ITextShaperImpl { - public ShapedBuffer ShapeText(ReadOnlySlice text, TextShaperOptions options) + public ShapedBuffer ShapeText(CharacterBufferReference text, int length, TextShaperOptions options) { var typeface = options.Typeface; var fontRenderingEmSize = options.FontRenderingEmSize; var bidiLevel = options.BidiLevel; - return new ShapedBuffer(text, text.Length, typeface, fontRenderingEmSize, bidiLevel); + var characterBufferRange = new CharacterBufferRange(text, length); + + return new ShapedBuffer(characterBufferRange, length, typeface, fontRenderingEmSize, bidiLevel); } } diff --git a/src/Skia/Avalonia.Skia/TextShaperImpl.cs b/src/Skia/Avalonia.Skia/TextShaperImpl.cs index eaf588c27d..98eb35d5c5 100644 --- a/src/Skia/Avalonia.Skia/TextShaperImpl.cs +++ b/src/Skia/Avalonia.Skia/TextShaperImpl.cs @@ -3,7 +3,6 @@ using System.Globalization; using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Platform; -using Avalonia.Utilities; using HarfBuzzSharp; using Buffer = HarfBuzzSharp.Buffer; using GlyphInfo = HarfBuzzSharp.GlyphInfo; @@ -12,8 +11,9 @@ namespace Avalonia.Skia { internal class TextShaperImpl : ITextShaperImpl { - public ShapedBuffer ShapeText(ReadOnlySlice text, TextShaperOptions options) + public ShapedBuffer ShapeText(CharacterBufferReference characterBufferReference, int length, TextShaperOptions options) { + var text = new CharacterBufferRange(characterBufferReference, length); var typeface = options.Typeface; var fontRenderingEmSize = options.FontRenderingEmSize; var bidiLevel = options.BidiLevel; @@ -21,21 +21,21 @@ namespace Avalonia.Skia using (var buffer = new Buffer()) { - buffer.AddUtf16(text.Buffer.Span, text.BufferOffset, text.Length); + buffer.AddUtf16(characterBufferReference.CharacterBuffer.Span, characterBufferReference.OffsetToFirstChar, length); MergeBreakPair(buffer); - + buffer.GuessSegmentProperties(); buffer.Direction = (bidiLevel & 1) == 0 ? Direction.LeftToRight : Direction.RightToLeft; - buffer.Language = new Language(culture ?? CultureInfo.CurrentCulture); + buffer.Language = new Language(culture ?? CultureInfo.CurrentCulture); var font = ((GlyphTypefaceImpl)typeface).Font; font.Shape(buffer); - if(buffer.Direction == Direction.RightToLeft) + if (buffer.Direction == Direction.RightToLeft) { buffer.Reverse(); } @@ -64,12 +64,12 @@ namespace Avalonia.Skia var glyphOffset = GetGlyphOffset(glyphPositions, i, textScale); - if(text.Buffer.Span[glyphCluster] == '\t') + if (text[i] == '\t') { glyphIndex = typeface.GetGlyph(' '); - glyphAdvance = options.IncrementalTabWidth > 0 ? - options.IncrementalTabWidth : + glyphAdvance = options.IncrementalTabWidth > 0 ? + options.IncrementalTabWidth : 4 * typeface.GetGlyphAdvance(glyphIndex) * textScale; } @@ -87,7 +87,7 @@ namespace Avalonia.Skia var length = buffer.Length; var glyphInfos = buffer.GetGlyphInfoSpan(); - + var second = glyphInfos[length - 1]; if (!new Codepoint(second.Codepoint).IsBreakChar) @@ -98,7 +98,7 @@ namespace Avalonia.Skia if (length > 1 && glyphInfos[length - 2].Codepoint == '\r' && second.Codepoint == '\n') { var first = glyphInfos[length - 2]; - + first.Codepoint = '\u200C'; second.Codepoint = '\u200C'; second.Cluster = first.Cluster; @@ -109,7 +109,7 @@ namespace Avalonia.Skia { *p = first; } - + fixed (GlyphInfo* p = &glyphInfos[length - 1]) { *p = second; diff --git a/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs index 7f2cbc6182..6685dd00b9 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs @@ -3,7 +3,6 @@ using System.Globalization; using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Platform; -using Avalonia.Utilities; using HarfBuzzSharp; using Buffer = HarfBuzzSharp.Buffer; using GlyphInfo = HarfBuzzSharp.GlyphInfo; @@ -12,7 +11,7 @@ namespace Avalonia.Direct2D1.Media { internal class TextShaperImpl : ITextShaperImpl { - public ShapedBuffer ShapeText(ReadOnlySlice text, TextShaperOptions options) + public ShapedBuffer ShapeText(CharacterBufferReference characterBufferReference, int length, TextShaperOptions options) { var typeface = options.Typeface; var fontRenderingEmSize = options.FontRenderingEmSize; @@ -21,7 +20,7 @@ namespace Avalonia.Direct2D1.Media using (var buffer = new Buffer()) { - buffer.AddUtf16(text.Buffer.Span, text.BufferOffset, text.Length); + buffer.AddUtf16(characterBufferReference.CharacterBuffer.Span, characterBufferReference.OffsetToFirstChar, length); MergeBreakPair(buffer); @@ -46,7 +45,9 @@ namespace Avalonia.Direct2D1.Media var bufferLength = buffer.Length; - var shapedBuffer = new ShapedBuffer(text, bufferLength, typeface, fontRenderingEmSize, bidiLevel); + var characterBufferRange = new CharacterBufferRange(characterBufferReference, length); + + var shapedBuffer = new ShapedBuffer(characterBufferRange, bufferLength, typeface, fontRenderingEmSize, bidiLevel); var glyphInfos = buffer.GetGlyphInfoSpan(); @@ -64,7 +65,7 @@ namespace Avalonia.Direct2D1.Media var glyphOffset = GetGlyphOffset(glyphPositions, i, textScale); - if (text.Buffer.Span[glyphCluster] == '\t') + if (characterBufferRange[i] == '\t') { glyphIndex = typeface.GetGlyph(' '); diff --git a/tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs b/tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs index df16c1b34f..363fb3f5b3 100644 --- a/tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs +++ b/tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs @@ -181,9 +181,7 @@ namespace Avalonia.Base.UnitTests.Media var count = glyphAdvances.Length; var glyphIndices = new ushort[count]; - var start = bidiLevel == 0 ? glyphClusters[0] : glyphClusters[^1]; - - var characters = new ReadOnlySlice(Enumerable.Repeat('a', count).ToArray(), start, count); + var characters = Enumerable.Repeat('a', count).ToArray(); return new GlyphRun(new MockGlyphTypeface(), 10, characters, glyphIndices, glyphAdvances, glyphClusters: glyphClusters, biDiLevel: bidiLevel); diff --git a/tests/Avalonia.Base.UnitTests/Media/TextFormatting/BiDiClassTests.cs b/tests/Avalonia.Base.UnitTests/Media/TextFormatting/BiDiClassTests.cs index d2cea45ce1..b2c40f4ff1 100644 --- a/tests/Avalonia.Base.UnitTests/Media/TextFormatting/BiDiClassTests.cs +++ b/tests/Avalonia.Base.UnitTests/Media/TextFormatting/BiDiClassTests.cs @@ -2,6 +2,7 @@ using System.Linq; using System.Runtime.InteropServices; using System.Text; +using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting.Unicode; using Xunit; using Xunit.Abstractions; @@ -36,7 +37,7 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting var text = Encoding.UTF32.GetString(MemoryMarshal.Cast(t.CodePoints).ToArray()); // Append - bidiData.Append(text.AsMemory()); + bidiData.Append(new CharacterBufferRange(text)); // Act for (int i = 0; i < 10; i++) diff --git a/tests/Avalonia.Base.UnitTests/Media/TextFormatting/GraphemeBreakClassTrieGeneratorTests.cs b/tests/Avalonia.Base.UnitTests/Media/TextFormatting/GraphemeBreakClassTrieGeneratorTests.cs index c9f869cea9..4e0207a85d 100644 --- a/tests/Avalonia.Base.UnitTests/Media/TextFormatting/GraphemeBreakClassTrieGeneratorTests.cs +++ b/tests/Avalonia.Base.UnitTests/Media/TextFormatting/GraphemeBreakClassTrieGeneratorTests.cs @@ -1,6 +1,7 @@ using System; using System.Runtime.InteropServices; using System.Text; +using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Visuals.UnitTests.Media.TextFormatting; using Xunit; @@ -37,11 +38,11 @@ namespace Avalonia.Base.UnitTests.Media.TextFormatting var text = Encoding.UTF32.GetString(MemoryMarshal.Cast(t.Codepoints).ToArray()); var grapheme = Encoding.UTF32.GetString(MemoryMarshal.Cast(t.Grapheme).ToArray()).AsSpan(); - var enumerator = new GraphemeEnumerator(text.AsMemory()); + var enumerator = new GraphemeEnumerator(new CharacterBufferRange(text)); enumerator.MoveNext(); - var actual = enumerator.Current.Text.Span; + var actual = enumerator.Current.Text; var pass = true; @@ -86,9 +87,7 @@ namespace Avalonia.Base.UnitTests.Media.TextFormatting { const string text = "ABCDEFGHIJ"; - var textMemory = text.AsMemory(); - - var enumerator = new GraphemeEnumerator(textMemory); + var enumerator = new GraphemeEnumerator(new CharacterBufferRange(text)); var count = 0; diff --git a/tests/Avalonia.Base.UnitTests/Media/TextFormatting/LineBreakEnumuratorTests.cs b/tests/Avalonia.Base.UnitTests/Media/TextFormatting/LineBreakEnumuratorTests.cs index 15be1200c8..b2648bf348 100644 --- a/tests/Avalonia.Base.UnitTests/Media/TextFormatting/LineBreakEnumuratorTests.cs +++ b/tests/Avalonia.Base.UnitTests/Media/TextFormatting/LineBreakEnumuratorTests.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Net.Http; +using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting.Unicode; using Xunit; using Xunit.Abstractions; @@ -22,7 +23,7 @@ namespace Avalonia.Base.UnitTests.Media.TextFormatting [Fact] public void BasicLatinTest() { - var lineBreaker = new LineBreakEnumerator("Hello World\r\nThis is a test.".AsMemory()); + var lineBreaker = new LineBreakEnumerator(new CharacterBufferRange("Hello World\r\nThis is a test.")); Assert.True(lineBreaker.MoveNext()); Assert.Equal(6, lineBreaker.Current.PositionWrap); @@ -55,7 +56,7 @@ namespace Avalonia.Base.UnitTests.Media.TextFormatting [Fact] public void ForwardTextWithOuterWhitespace() { - var lineBreaker = new LineBreakEnumerator(" Apples Pears Bananas ".AsMemory()); + var lineBreaker = new LineBreakEnumerator(new CharacterBufferRange(" Apples Pears Bananas ")); var positionsF = GetBreaks(lineBreaker); Assert.Equal(1, positionsF[0].PositionWrap); Assert.Equal(0, positionsF[0].PositionMeasure); @@ -82,7 +83,7 @@ namespace Avalonia.Base.UnitTests.Media.TextFormatting [Fact] public void ForwardTest() { - var lineBreaker = new LineBreakEnumerator("Apples Pears Bananas".AsMemory()); + var lineBreaker = new LineBreakEnumerator(new CharacterBufferRange("Apples Pears Bananas")); var positionsF = GetBreaks(lineBreaker); Assert.Equal(7, positionsF[0].PositionWrap); @@ -99,7 +100,7 @@ namespace Avalonia.Base.UnitTests.Media.TextFormatting { var text = string.Join(null, codePoints.Select(char.ConvertFromUtf32)); - var lineBreaker = new LineBreakEnumerator(text.AsMemory()); + var lineBreaker = new LineBreakEnumerator(new CharacterBufferRange(text)); var foundBreaks = new List(); diff --git a/tests/Avalonia.Base.UnitTests/Utilities/ReadOnlySpanTests.cs b/tests/Avalonia.Base.UnitTests/Utilities/ReadOnlySpanTests.cs deleted file mode 100644 index da30ee9d02..0000000000 --- a/tests/Avalonia.Base.UnitTests/Utilities/ReadOnlySpanTests.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Linq; -using Avalonia.Utilities; -using Xunit; - -namespace Avalonia.Base.UnitTests.Utilities -{ - public class ReadOnlySpanTests - { - [Fact] - public void Should_Skip() - { - var buffer = new[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; - - var slice = new ReadOnlySlice(buffer); - - var skipped = slice.Skip(2); - - var expected = buffer.Skip(2); - - Assert.Equal(expected, skipped); - } - - [Fact] - public void Should_Take() - { - var buffer = new[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; - - var slice = new ReadOnlySlice(buffer); - - var taken = slice.Take(8); - - var expected = buffer.Take(8); - - Assert.Equal(expected, taken); - } - } -} diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/TextPresenter_Tests.cs b/tests/Avalonia.Controls.UnitTests/Presenters/TextPresenter_Tests.cs index 8cc8e4c16f..8e06fbd831 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/TextPresenter_Tests.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/TextPresenter_Tests.cs @@ -46,7 +46,7 @@ namespace Avalonia.Controls.UnitTests.Presenters Assert.NotNull(target.TextLayout); var actual = string.Join(null, - target.TextLayout.TextLines.SelectMany(x => x.TextRuns).Select(x => x.Text.Span.ToString())); + target.TextLayout.TextLines.SelectMany(x => x.TextRuns).Select(x => x.CharacterBufferReference.CharacterBuffer.Span.ToString())); Assert.Equal("****", actual); } diff --git a/tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs b/tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs index 3b9caa393e..4083a67b5e 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs @@ -19,7 +19,7 @@ namespace Avalonia.Skia.UnitTests.Media { var options = new TextShaperOptions(Typeface.Default.GlyphTypeface, 10, direction, CultureInfo.CurrentCulture); var shapedBuffer = - TextShaper.Current.ShapeText(text.AsMemory(), options); + TextShaper.Current.ShapeText(new CharacterBufferReference(text), text.Length, options); var glyphRun = CreateGlyphRun(shapedBuffer); @@ -39,8 +39,6 @@ namespace Avalonia.Skia.UnitTests.Media } else { - shapedBuffer.GlyphInfos.Span.Reverse(); - foreach (var rect in rects) { characterHit = glyphRun.GetNextCaretCharacterHit(characterHit); @@ -62,7 +60,7 @@ namespace Avalonia.Skia.UnitTests.Media { var options = new TextShaperOptions(Typeface.Default.GlyphTypeface, 10, direction, CultureInfo.CurrentCulture); var shapedBuffer = - TextShaper.Current.ShapeText(text.AsMemory(), options); + TextShaper.Current.ShapeText(new CharacterBufferReference(text), text.Length, options); var glyphRun = CreateGlyphRun(shapedBuffer); @@ -84,8 +82,6 @@ namespace Avalonia.Skia.UnitTests.Media } else { - shapedBuffer.GlyphInfos.Span.Reverse(); - foreach (var rect in rects) { characterHit = glyphRun.GetPreviousCaretCharacterHit(characterHit); @@ -107,7 +103,7 @@ namespace Avalonia.Skia.UnitTests.Media { var options = new TextShaperOptions(Typeface.Default.GlyphTypeface, 10, direction, CultureInfo.CurrentCulture); var shapedBuffer = - TextShaper.Current.ShapeText(text.AsMemory(), options); + TextShaper.Current.ShapeText(new CharacterBufferReference(text), text.Length, options); var glyphRun = CreateGlyphRun(shapedBuffer); @@ -116,16 +112,14 @@ namespace Avalonia.Skia.UnitTests.Media var characterHit = glyphRun.GetCharacterHitFromDistance(glyphRun.Metrics.WidthIncludingTrailingWhitespace, out _); - Assert.Equal(glyphRun.Characters.Length, characterHit.FirstCharacterIndex + characterHit.TrailingLength); + Assert.Equal(glyphRun.Characters.Count, characterHit.FirstCharacterIndex + characterHit.TrailingLength); } else { - shapedBuffer.GlyphInfos.Span.Reverse(); - - var characterHit = + var characterHit = glyphRun.GetCharacterHitFromDistance(0, out _); - Assert.Equal(glyphRun.Characters.Length, characterHit.FirstCharacterIndex + characterHit.TrailingLength); + Assert.Equal(glyphRun.Characters.Count, characterHit.FirstCharacterIndex + characterHit.TrailingLength); } var rects = BuildRects(glyphRun); @@ -218,15 +212,22 @@ namespace Avalonia.Skia.UnitTests.Media private static GlyphRun CreateGlyphRun(ShapedBuffer shapedBuffer) { - return new GlyphRun( + var glyphRun = new GlyphRun( shapedBuffer.GlyphTypeface, shapedBuffer.FontRenderingEmSize, - shapedBuffer.Text, + shapedBuffer.CharacterBufferRange, shapedBuffer.GlyphIndices, shapedBuffer.GlyphAdvances, shapedBuffer.GlyphOffsets, shapedBuffer.GlyphClusters, shapedBuffer.BidiLevel); + + if(shapedBuffer.BidiLevel == 1) + { + shapedBuffer.GlyphInfos.Span.Reverse(); + } + + return glyphRun; } private static IDisposable Start() diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/MultiBufferTextSource.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/MultiBufferTextSource.cs index 005bcdf70e..aa499bb135 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/MultiBufferTextSource.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/MultiBufferTextSource.cs @@ -29,8 +29,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var runText = _runTexts[index]; - return new TextCharacters( - new ReadOnlySlice(runText.AsMemory(), textSourceIndex, runText.Length), _defaultStyle); + return new TextCharacters(runText, _defaultStyle); } } } diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/SingleBufferTextSource.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/SingleBufferTextSource.cs index dee4fe7f77..f12f42bd5e 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/SingleBufferTextSource.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/SingleBufferTextSource.cs @@ -1,30 +1,33 @@ -using System; -using Avalonia.Media.TextFormatting; -using Avalonia.Utilities; +using Avalonia.Media.TextFormatting; namespace Avalonia.Skia.UnitTests.Media.TextFormatting { internal class SingleBufferTextSource : ITextSource { - private readonly ReadOnlySlice _text; + private readonly CharacterBufferRange _text; private readonly GenericTextRunProperties _defaultGenericPropertiesRunProperties; public SingleBufferTextSource(string text, GenericTextRunProperties defaultProperties) { - _text = text.AsMemory(); + _text = new CharacterBufferRange(text); _defaultGenericPropertiesRunProperties = defaultProperties; } public TextRun GetTextRun(int textSourceIndex) { - if (textSourceIndex > _text.Length) + if (textSourceIndex >= _text.Length) { return null; } - + var runText = _text.Skip(textSourceIndex); - return runText.IsEmpty ? null : new TextCharacters(runText, _defaultGenericPropertiesRunProperties); + if (runText.IsEmpty) + { + return null; + } + + return new TextCharacters(runText.CharacterBufferReference, runText.Length, _defaultGenericPropertiesRunProperties); } } } diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs index 316926b00c..33d4fba5f1 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs @@ -37,7 +37,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting Assert.Equal(defaultProperties.ForegroundBrush, textRun.Properties.ForegroundBrush); - Assert.Equal(text.Length, textRun.Text.Length); + Assert.Equal(text.Length, textRun.Length); } } @@ -82,7 +82,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting new ValueSpan(9, 1, defaultProperties) }; - var textSource = new FormattedTextSource(text.AsMemory(), defaultProperties, GenericTextRunPropertiesRuns); + var textSource = new FormattedTextSource(text, defaultProperties, GenericTextRunPropertiesRuns); var formatter = new TextFormatterImpl(); @@ -97,7 +97,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var textRun = textLine.TextRuns[i]; - Assert.Equal(GenericTextRunPropertiesRun.Length, textRun.Text.Length); + Assert.Equal(GenericTextRunPropertiesRun.Length, textRun.Length); } } } @@ -166,7 +166,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var firstRun = textLine.TextRuns[0]; - Assert.Equal(4, firstRun.Text.Length); + Assert.Equal(4, firstRun.Length); } } @@ -216,7 +216,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting { using (Start()) { - var lineBreaker = new LineBreakEnumerator(text.AsMemory()); + var lineBreaker = new LineBreakEnumerator(new CharacterBufferRange(text)); var expected = new List(); @@ -369,7 +369,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting new GenericTextRunProperties(new Typeface("Verdana", FontStyle.Italic),32)) }; - var textSource = new FormattedTextSource(text.AsMemory(), defaultProperties, styleSpans); + var textSource = new FormattedTextSource(text, defaultProperties, styleSpans); var formatter = new TextFormatterImpl(); @@ -389,7 +389,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting if (textLine.Width > 300 || currentHeight + textLine.Height > 240) { - textLine = textLine.Collapse(new TextTrailingWordEllipsis(new ReadOnlySlice(new[] { TextTrimming.DefaultEllipsisChar }), 300, defaultProperties)); + textLine = textLine.Collapse(new TextTrailingWordEllipsis(TextTrimming.DefaultEllipsisChar, 300, defaultProperties)); } currentHeight += textLine.Height; @@ -472,7 +472,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var textLine = formatter.FormatLine(textSource, textPosition, 50, paragraphProperties, lastBreak); - Assert.Equal(textLine.Length, textLine.TextRuns.Sum(x => x.TextSourceLength)); + Assert.Equal(textLine.Length, textLine.TextRuns.Sum(x => x.Length)); textPosition += textLine.Length; @@ -534,7 +534,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: foreground)) }; - var textSource = new FormattedTextSource(text.AsMemory(), defaultProperties, spans); + var textSource = new FormattedTextSource(text, defaultProperties, spans); var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity, paragraphProperties); @@ -614,8 +614,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting return new RectangleRun(new Rect(0, 0, 50, 50), Brushes.Green); } - return new TextCharacters(_text.AsMemory(), - new GenericTextRunProperties(Typeface.Default, foregroundBrush: Brushes.Black)); + return new TextCharacters(_text, 0, _text.Length, new GenericTextRunProperties(Typeface.Default, foregroundBrush: Brushes.Black)); } } diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs index d6da2c77c4..a407b38eb1 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs @@ -60,9 +60,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var textRun = textLine.TextRuns[1]; - Assert.Equal(2, textRun.Text.Length); + Assert.Equal(2, textRun.Length); - var actual = textRun.Text.Span.ToString(); + var actual = new CharacterBufferRange(textRun).Span.ToString(); Assert.Equal("1 ", actual); @@ -144,8 +144,8 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var expectedGlyphs = expected.TextLines.Select(x => string.Join('|', x.TextRuns.Cast() .SelectMany(x => x.ShapedBuffer.GlyphIndices))).ToList(); - var outer = new GraphemeEnumerator(text.AsMemory()); - var inner = new GraphemeEnumerator(text.AsMemory()); + var outer = new GraphemeEnumerator(new CharacterBufferRange(text)); + var inner = new GraphemeEnumerator(new CharacterBufferRange(text)); var i = 0; var j = 0; @@ -190,7 +190,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting break; } - inner = new GraphemeEnumerator(text.AsMemory()); + inner = new GraphemeEnumerator(new CharacterBufferRange(text)); i += outer.Current.Text.Length; } @@ -223,10 +223,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var textRun = textLine.TextRuns[0]; - Assert.Equal(2, textRun.Text.Length); + Assert.Equal(2, textRun.Length); - var actual = SingleLineText.Substring(textRun.Text.Start, - textRun.Text.Length); + var actual = SingleLineText[..textRun.Length]; Assert.Equal("01", actual); @@ -260,9 +259,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var textRun = textLine.TextRuns[1]; - Assert.Equal(2, textRun.Text.Length); + Assert.Equal(2, textRun.Length); - var actual = textRun.Text.Span.ToString(); + var actual = new CharacterBufferRange(textRun).Span.ToString(); Assert.Equal("89", actual); @@ -296,7 +295,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var textRun = textLine.TextRuns[0]; - Assert.Equal(1, textRun.Text.Length); + Assert.Equal(1, textRun.Length); Assert.Equal(foreground, textRun.Properties.ForegroundBrush); } @@ -330,9 +329,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var textRun = textLine.TextRuns[1]; - Assert.Equal(2, textRun.Text.Length); + Assert.Equal(2, textRun.Length); - var actual = textRun.Text.Span.ToString(); + var actual = new CharacterBufferRange(textRun).Span.ToString(); Assert.Equal("😄", actual); @@ -369,7 +368,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting Assert.Equal( MultiLineText.Length, layout.TextLines.Select(textLine => - textLine.TextRuns.Sum(textRun => textRun.Text.Length)) + textLine.TextRuns.Sum(textRun => textRun.Length)) .Sum()); } } @@ -402,7 +401,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting Assert.Equal( text.Length, layout.TextLines.Select(textLine => - textLine.TextRuns.Sum(textRun => textRun.Text.Length)) + textLine.TextRuns.Sum(textRun => textRun.Length)) .Sum()); } } @@ -558,7 +557,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var textRun = (ShapedTextCharacters)textLine.TextRuns[0]; - Assert.Equal(7, textRun.Text.Length); + Assert.Equal(7, textRun.Length); var replacementGlyph = Typeface.Default.GlyphTypeface.GetGlyph(Codepoint.ReplacementCodepoint); @@ -668,10 +667,10 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting Assert.Equal(5, layout.TextLines.Count); - Assert.Equal("123\r\n", layout.TextLines[0].TextRuns[0].Text); - Assert.Equal("\r\n", layout.TextLines[1].TextRuns[0].Text); - Assert.Equal("456\r\n", layout.TextLines[2].TextRuns[0].Text); - Assert.Equal("\r\n", layout.TextLines[3].TextRuns[0].Text); + Assert.Equal("123\r\n", new CharacterBufferRange(layout.TextLines[0].TextRuns[0])); + Assert.Equal("\r\n", new CharacterBufferRange(layout.TextLines[1].TextRuns[0])); + Assert.Equal("456\r\n", new CharacterBufferRange(layout.TextLines[2].TextRuns[0])); + Assert.Equal("\r\n", new CharacterBufferRange(layout.TextLines[3].TextRuns[0])); } } @@ -815,7 +814,10 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting { Assert.True(textLine.Width <= maxWidth); - var actual = new string(textLine.TextRuns.Cast().OrderBy(x => x.Text.Start).SelectMany(x => x.Text).ToArray()); + var actual = new string(textLine.TextRuns.Cast() + .OrderBy(x => x.CharacterBufferReference.OffsetToFirstChar) + .SelectMany(x => new CharacterBufferRange(x.CharacterBufferReference, x.Length)).ToArray()); + var expected = text.Substring(textLine.FirstTextSourceIndex, textLine.Length); Assert.Equal(expected, actual); @@ -966,7 +968,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var i = 0; - var graphemeEnumerator = new GraphemeEnumerator(text.AsMemory()); + var graphemeEnumerator = new GraphemeEnumerator(new CharacterBufferRange(text)); while (graphemeEnumerator.MoveNext()) { diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs index 87de9ed11f..d6257a0de8 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs @@ -90,7 +90,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var clusters = new List(); - foreach (var textRun in textLine.TextRuns.OrderBy(x => x.Text.Start)) + foreach (var textRun in textLine.TextRuns.OrderBy(x => x.CharacterBufferReference.OffsetToFirstChar)) { var shapedRun = (ShapedTextCharacters)textRun; @@ -137,7 +137,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var clusters = new List(); - foreach (var textRun in textLine.TextRuns.OrderBy(x => x.Text.Start)) + foreach (var textRun in textLine.TextRuns.OrderBy(x => x.CharacterBufferReference.OffsetToFirstChar)) { var shapedRun = (ShapedTextCharacters)textRun; @@ -187,14 +187,16 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting formatter.FormatLine(textSource, 0, double.PositiveInfinity, new GenericTextParagraphProperties(defaultProperties)); - var clusters = textLine.TextRuns.Cast().SelectMany(x => x.ShapedBuffer.GlyphClusters) - .ToArray(); + var clusters = BuildGlyphClusters(textLine); var nextCharacterHit = new CharacterHit(0); - for (var i = 0; i < clusters.Length; i++) + for (var i = 0; i < clusters.Count; i++) { - Assert.Equal(clusters[i], nextCharacterHit.FirstCharacterIndex); + var expectedCluster = clusters[i]; + var actualCluster = nextCharacterHit.FirstCharacterIndex; + + Assert.Equal(expectedCluster, actualCluster); nextCharacterHit = textLine.GetNextCaretCharacterHit(nextCharacterHit); } @@ -406,7 +408,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting Assert.True(collapsedLine.HasCollapsed); - var trimmedText = collapsedLine.TextRuns.SelectMany(x => x.Text).ToArray(); + var trimmedText = collapsedLine.TextRuns.SelectMany(x => new CharacterBufferRange(x)).ToArray(); Assert.Equal(expected.Length, trimmedText.Length); @@ -450,8 +452,8 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting currentHit = textLine.GetNextCaretCharacterHit(currentHit); - Assert.Equal(3, currentHit.FirstCharacterIndex); - Assert.Equal(1, currentHit.TrailingLength); + Assert.Equal(4, currentHit.FirstCharacterIndex); + Assert.Equal(0, currentHit.TrailingLength); } } @@ -473,18 +475,18 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var currentHit = textLine.GetPreviousCaretCharacterHit(new CharacterHit(3, 1)); - Assert.Equal(3, currentHit.FirstCharacterIndex); - Assert.Equal(0, currentHit.TrailingLength); + Assert.Equal(2, currentHit.FirstCharacterIndex); + Assert.Equal(1, currentHit.TrailingLength); currentHit = textLine.GetPreviousCaretCharacterHit(currentHit); - Assert.Equal(2, currentHit.FirstCharacterIndex); - Assert.Equal(0, currentHit.TrailingLength); + Assert.Equal(1, currentHit.FirstCharacterIndex); + Assert.Equal(1, currentHit.TrailingLength); currentHit = textLine.GetPreviousCaretCharacterHit(currentHit); - Assert.Equal(1, currentHit.FirstCharacterIndex); - Assert.Equal(0, currentHit.TrailingLength); + Assert.Equal(0, currentHit.FirstCharacterIndex); + Assert.Equal(1, currentHit.TrailingLength); currentHit = textLine.GetPreviousCaretCharacterHit(currentHit); @@ -509,13 +511,13 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var characterHit = textLine.GetCharacterHitFromDistance(50); - Assert.Equal(3, characterHit.FirstCharacterIndex); + Assert.Equal(5, characterHit.FirstCharacterIndex); Assert.Equal(1, characterHit.TrailingLength); characterHit = textLine.GetCharacterHitFromDistance(32); - Assert.Equal(2, characterHit.FirstCharacterIndex); - Assert.Equal(1, characterHit.TrailingLength); + Assert.Equal(3, characterHit.FirstCharacterIndex); + Assert.Equal(0, characterHit.TrailingLength); } } @@ -649,7 +651,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var run = textRuns[i]; var bounds = runBounds[i]; - Assert.Equal(run.Text.Start, bounds.TextSourceCharacterIndex); + Assert.Equal(run.CharacterBufferReference.OffsetToFirstChar, bounds.TextSourceCharacterIndex); Assert.Equal(run, bounds.TextRun); Assert.Equal(run.Size.Width, bounds.Rectangle.Width); } @@ -683,13 +685,13 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting switch (textSourceIndex) { case 0: - return new TextCharacters(new ReadOnlySlice("aaaaaaaaaa".AsMemory()), new GenericTextRunProperties(Typeface.Default)); + return new TextCharacters("aaaaaaaaaa", new GenericTextRunProperties(Typeface.Default)); case 10: - return new TextCharacters(new ReadOnlySlice("bbbbbbbbbb".AsMemory()), new GenericTextRunProperties(Typeface.Default)); + return new TextCharacters("bbbbbbbbbb", new GenericTextRunProperties(Typeface.Default)); case 20: - return new TextCharacters(new ReadOnlySlice("cccccccccc".AsMemory()), new GenericTextRunProperties(Typeface.Default)); + return new TextCharacters("cccccccccc", new GenericTextRunProperties(Typeface.Default)); case 30: - return new TextCharacters(new ReadOnlySlice("dddddddddd".AsMemory()), new GenericTextRunProperties(Typeface.Default)); + return new TextCharacters("dddddddddd", new GenericTextRunProperties(Typeface.Default)); default: return null; } @@ -698,7 +700,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting private class DrawableRunTextSource : ITextSource { - const string Text = "_A_A"; + private const string Text = "_A_A"; public TextRun GetTextRun(int textSourceIndex) { @@ -707,11 +709,11 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting case 0: return new CustomDrawableRun(); case 1: - return new TextCharacters(new ReadOnlySlice(Text.AsMemory(), 1, 1, 1), new GenericTextRunProperties(Typeface.Default)); - case 2: + return new TextCharacters(Text, new GenericTextRunProperties(Typeface.Default)); + case 5: return new CustomDrawableRun(); - case 3: - return new TextCharacters(new ReadOnlySlice(Text.AsMemory(), 3, 1, 3), new GenericTextRunProperties(Typeface.Default)); + case 6: + return new TextCharacters(Text, new GenericTextRunProperties(Typeface.Default)); default: return null; } @@ -815,19 +817,19 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting using (Start()) { var defaultProperties = new GenericTextRunProperties(Typeface.Default); - var text = "0123".AsMemory(); + var text = "0123"; var shaperOption = new TextShaperOptions(Typeface.Default.GlyphTypeface, 10, 0, CultureInfo.CurrentCulture); - var firstRun = new ShapedTextCharacters(TextShaper.Current.ShapeText(new ReadOnlySlice(text, 1, text.Length), shaperOption), defaultProperties); + var firstRun = new ShapedTextCharacters(TextShaper.Current.ShapeText(text, shaperOption), defaultProperties); var textRuns = new List { new CustomDrawableRun(), firstRun, new CustomDrawableRun(), - new ShapedTextCharacters(TextShaper.Current.ShapeText(new ReadOnlySlice(text, text.Length + 2, text.Length), shaperOption), defaultProperties), + new ShapedTextCharacters(TextShaper.Current.ShapeText(text, shaperOption), defaultProperties), new CustomDrawableRun(), - new ShapedTextCharacters(TextShaper.Current.ShapeText(new ReadOnlySlice(text, text.Length * 2 + 3, text.Length), shaperOption), defaultProperties) + new ShapedTextCharacters(TextShaper.Current.ShapeText(text, shaperOption), defaultProperties) }; var textSource = new FixedRunsTextSource(textRuns); @@ -838,7 +840,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting formatter.FormatLine(textSource, 0, double.PositiveInfinity, new GenericTextParagraphProperties(defaultProperties)); - var textBounds = textLine.GetTextBounds(0, text.Length * 3 + 3); + var textBounds = textLine.GetTextBounds(0, textLine.Length); Assert.Equal(6, textBounds.Count); Assert.Equal(textLine.WidthIncludingTrailingWhitespace, textBounds.Sum(x => x.Rectangle.Width)); @@ -848,17 +850,17 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting Assert.Equal(1, textBounds.Count); Assert.Equal(14, textBounds[0].Rectangle.Width); - textBounds = textLine.GetTextBounds(0, firstRun.Text.Length + 1); + textBounds = textLine.GetTextBounds(0, firstRun.Length + 1); Assert.Equal(2, textBounds.Count); Assert.Equal(firstRun.Size.Width + 14, textBounds.Sum(x => x.Rectangle.Width)); - textBounds = textLine.GetTextBounds(1, firstRun.Text.Length); + textBounds = textLine.GetTextBounds(1, firstRun.Length); Assert.Equal(1, textBounds.Count); Assert.Equal(firstRun.Size.Width, textBounds[0].Rectangle.Width); - textBounds = textLine.GetTextBounds(1, firstRun.Text.Length + 1); + textBounds = textLine.GetTextBounds(0, 1 + firstRun.Length); Assert.Equal(2, textBounds.Count); Assert.Equal(firstRun.Size.Width + 14, textBounds.Sum(x => x.Rectangle.Width)); @@ -878,7 +880,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var textLine = formatter.FormatLine(textSource, 0, 200, - new GenericTextParagraphProperties(FlowDirection.LeftToRight, TextAlignment.Left, + new GenericTextParagraphProperties(FlowDirection.LeftToRight, TextAlignment.Left, true, true, defaultProperties, TextWrapping.NoWrap, 0, 0, 0)); var textBounds = textLine.GetTextBounds(0, 3); @@ -899,11 +901,11 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting Assert.Equal(2, textBounds.Count); - Assert.Equal(firstRun.Size.Width, textBounds[0].Rectangle.Width); + Assert.Equal(firstRun.Size.Width, textBounds[0].Rectangle.Width); Assert.Equal(7.201171875, textBounds[1].Rectangle.Width); - Assert.Equal(firstRun.Size.Width, textBounds[1].Rectangle.Left); + Assert.Equal(firstRun.Size.Width, textBounds[1].Rectangle.Left); textBounds = textLine.GetTextBounds(0, text.Length); @@ -925,7 +927,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var textLine = formatter.FormatLine(textSource, 0, 200, - new GenericTextParagraphProperties(FlowDirection.RightToLeft, TextAlignment.Left, + new GenericTextParagraphProperties(FlowDirection.RightToLeft, TextAlignment.Left, true, true, defaultProperties, TextWrapping.NoWrap, 0, 0, 0)); var textBounds = textLine.GetTextBounds(0, 4); @@ -941,13 +943,13 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting Assert.Equal(1, textBounds.Count); - Assert.Equal(3, textBounds[0].TextRunBounds.Sum(x=> x.Length)); + Assert.Equal(3, textBounds[0].TextRunBounds.Sum(x => x.Length)); Assert.Equal(firstRun.Size.Width, textBounds.Sum(x => x.Rectangle.Width)); textBounds = textLine.GetTextBounds(0, 5); Assert.Equal(2, textBounds.Count); - Assert.Equal(5, textBounds.Sum(x=> x.TextRunBounds.Sum(x => x.Length))); + Assert.Equal(5, textBounds.Sum(x => x.TextRunBounds.Sum(x => x.Length))); Assert.Equal(secondRun.Size.Width, textBounds[1].Rectangle.Width); Assert.Equal(7.201171875, textBounds[0].Rectangle.Width); @@ -960,7 +962,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting Assert.Equal(7, textBounds.Sum(x => x.TextRunBounds.Sum(x => x.Length))); Assert.Equal(textLine.WidthIncludingTrailingWhitespace, textBounds.Sum(x => x.Rectangle.Width)); } - } + } private class FixedRunsTextSource : ITextSource { @@ -982,7 +984,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting return textRun; } - currentPosition += textRun.TextSourceLength; + currentPosition += textRun.Length; } return null; diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextShaperTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextShaperTests.cs index 94933e334d..63e0083b1d 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextShaperTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextShaperTests.cs @@ -14,11 +14,11 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting { using (Start()) { - var text = "\n\r\n".AsMemory(); + var text = "\n\r\n"; var options = new TextShaperOptions(Typeface.Default.GlyphTypeface, 12,0, CultureInfo.CurrentCulture); var shapedBuffer = TextShaper.Current.ShapeText(text, options); - Assert.Equal(shapedBuffer.Text.Length, text.Length); + Assert.Equal(shapedBuffer.CharacterBufferRange.Length, text.Length); Assert.Equal(shapedBuffer.GlyphClusters.Count, text.Length); Assert.Equal(0, shapedBuffer.GlyphClusters[0]); Assert.Equal(1, shapedBuffer.GlyphClusters[1]); @@ -31,7 +31,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting { using (Start()) { - var text = "\t".AsMemory(); + var text = "\t"; var options = new TextShaperOptions(Typeface.Default.GlyphTypeface, 12, 0, CultureInfo.CurrentCulture, 100); var shapedBuffer = TextShaper.Current.ShapeText(text, options); diff --git a/tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs b/tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs index 7b7488bd5a..ae7e00aca1 100644 --- a/tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs +++ b/tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs @@ -11,7 +11,7 @@ namespace Avalonia.UnitTests { public class HarfBuzzTextShaperImpl : ITextShaperImpl { - public ShapedBuffer ShapeText(ReadOnlySlice text, TextShaperOptions options) + public ShapedBuffer ShapeText(CharacterBufferReference text, int textLength, TextShaperOptions options) { var typeface = options.Typeface; var fontRenderingEmSize = options.FontRenderingEmSize; @@ -20,7 +20,7 @@ namespace Avalonia.UnitTests using (var buffer = new Buffer()) { - buffer.AddUtf16(text.Buffer.Span, text.Start, text.Length); + buffer.AddUtf16(text.CharacterBuffer.Span, text.OffsetToFirstChar, textLength); MergeBreakPair(buffer); @@ -45,7 +45,9 @@ namespace Avalonia.UnitTests var bufferLength = buffer.Length; - var shapedBuffer = new ShapedBuffer(text, bufferLength, typeface, fontRenderingEmSize, bidiLevel); + var characterBufferRange = new CharacterBufferRange(text, textLength); + + var shapedBuffer = new ShapedBuffer(characterBufferRange, bufferLength, typeface, fontRenderingEmSize, bidiLevel); var glyphInfos = buffer.GetGlyphInfoSpan(); diff --git a/tests/Avalonia.UnitTests/MockTextShaperImpl.cs b/tests/Avalonia.UnitTests/MockTextShaperImpl.cs index 7c34bd192e..00bcef295a 100644 --- a/tests/Avalonia.UnitTests/MockTextShaperImpl.cs +++ b/tests/Avalonia.UnitTests/MockTextShaperImpl.cs @@ -1,24 +1,24 @@ using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Platform; -using Avalonia.Utilities; namespace Avalonia.UnitTests { public class MockTextShaperImpl : ITextShaperImpl { - public ShapedBuffer ShapeText(ReadOnlySlice text, TextShaperOptions options) + public ShapedBuffer ShapeText(CharacterBufferReference text, int length, TextShaperOptions options) { var typeface = options.Typeface; var fontRenderingEmSize = options.FontRenderingEmSize; var bidiLevel = options.BidiLevel; - - var shapedBuffer = new ShapedBuffer(text, text.Length, typeface, fontRenderingEmSize, bidiLevel); + var characterBufferRange = new CharacterBufferRange(text, length); + var shapedBuffer = new ShapedBuffer(characterBufferRange, length, typeface, fontRenderingEmSize, bidiLevel); for (var i = 0; i < shapedBuffer.Length;) { - var glyphCluster = i + text.Start; - var codepoint = Codepoint.ReadAt(text, i, out var count); + var glyphCluster = i + text.OffsetToFirstChar; + + var codepoint = Codepoint.ReadAt(characterBufferRange, i, out var count); var glyphIndex = typeface.GetGlyph(codepoint); From 98d53bb3f5fffde2ca8778127a15a8a3707b6deb Mon Sep 17 00:00:00 2001 From: Adir Hudayfi Date: Thu, 8 Dec 2022 17:02:59 +0200 Subject: [PATCH 53/87] Moved finished check to Activate --- .../Composition/Animations/KeyFrameAnimationInstance.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimationInstance.cs b/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimationInstance.cs index 570b852108..0117fe0713 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimationInstance.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimationInstance.cs @@ -170,6 +170,10 @@ namespace Avalonia.Rendering.Composition.Animations public override void Activate() { + if (_finished) + { + return; + } TargetObject.Compositor.AddToClock(this); base.Activate(); } @@ -177,7 +181,6 @@ namespace Avalonia.Rendering.Composition.Animations public override void Deactivate() { TargetObject.Compositor.RemoveFromClock(this); - _finished = false; base.Deactivate(); } } From 7ff602e471d4c09283a340d2f8c5fe464fa30d62 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Thu, 8 Dec 2022 12:48:10 -0800 Subject: [PATCH 54/87] Fixed WGL HDC management --- .../Interop/UnmanagedMethods.cs | 2 +- .../Avalonia.Win32/OpenGl/WglContext.cs | 4 +- .../Avalonia.Win32/OpenGl/WglDCManager.cs | 88 +++++++++++++++++++ .../Avalonia.Win32/OpenGl/WglDisplay.cs | 6 +- .../OpenGl/WglGlPlatformSurface.cs | 2 +- .../OpenGl/WglRestoreContext.cs | 5 +- 6 files changed, 99 insertions(+), 8 deletions(-) create mode 100644 src/Windows/Avalonia.Win32/OpenGl/WglDCManager.cs diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index 9b86154043..a7cca5b0f3 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -1627,7 +1627,7 @@ namespace Avalonia.Win32.Interop public static extern bool wglDeleteContext(IntPtr context); - [DllImport("opengl32.dll")] + [DllImport("opengl32.dll", SetLastError = true)] public static extern bool wglMakeCurrent(IntPtr hdc, IntPtr context); [DllImport("opengl32.dll")] diff --git a/src/Windows/Avalonia.Win32/OpenGl/WglContext.cs b/src/Windows/Avalonia.Win32/OpenGl/WglContext.cs index da8780d413..a2c0d9203d 100644 --- a/src/Windows/Avalonia.Win32/OpenGl/WglContext.cs +++ b/src/Windows/Avalonia.Win32/OpenGl/WglContext.cs @@ -46,7 +46,7 @@ namespace Avalonia.Win32.OpenGl public void Dispose() { wglDeleteContext(_context); - ReleaseDC(_hWnd, _dc); + WglDCManager.ReleaseDC(_hWnd, _dc); DestroyWindow(_hWnd); IsLost = true; } @@ -72,7 +72,7 @@ namespace Avalonia.Win32.OpenGl public IntPtr CreateConfiguredDeviceContext(IntPtr hWnd) { - var dc = GetDC(hWnd); + var dc = WglDCManager.GetDC(hWnd); var fmt = _formatDescriptor; SetPixelFormat(dc, _pixelFormat, ref fmt); return dc; diff --git a/src/Windows/Avalonia.Win32/OpenGl/WglDCManager.cs b/src/Windows/Avalonia.Win32/OpenGl/WglDCManager.cs new file mode 100644 index 0000000000..2698c8eb5f --- /dev/null +++ b/src/Windows/Avalonia.Win32/OpenGl/WglDCManager.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Avalonia.Win32.Interop; + +namespace Avalonia.Win32.OpenGl; + +/// +/// 1) ReleaseDC can only happen from the same thread that has called GetDC +/// 2) When thread exits all of its HDCs are getting destroyed +/// 3) We need to create OpenGL render targets from thread pool threads +/// +/// So this class hosts a dedicated thread for managing HDCs for OpenGL +/// + +internal class WglDCManager +{ + class GetDCOp + { + public IntPtr Window; + public TaskCompletionSource Result; + } + + class ReleaseDCOp + { + public IntPtr Window; + public IntPtr DC; + public TaskCompletionSource Result; + } + + private static readonly Queue s_Queue = new(); + private static readonly AutoResetEvent s_Event = new(false); + + static void Worker() + { + while (true) + { + s_Event.WaitOne(); + lock (s_Queue) + { + if(s_Queue.Count == 0) + continue; + var job = s_Queue.Dequeue(); + if (job is GetDCOp getDc) + getDc.Result.TrySetResult(UnmanagedMethods.GetDC(getDc.Window)); + else if (job is ReleaseDCOp releaseDc) + { + UnmanagedMethods.ReleaseDC(releaseDc.Window, releaseDc.DC); + releaseDc.Result.SetResult(null); + } + } + } + } + + static WglDCManager() + { + new Thread(Worker) { IsBackground = true }.Start(); + } + + public static IntPtr GetDC(IntPtr hWnd) + { + var tcs = new TaskCompletionSource(); + lock(s_Queue) + s_Queue.Enqueue(new GetDCOp + { + Window = hWnd, + Result = tcs + }); + s_Event.Set(); + return tcs.Task.Result; + } + + public static void ReleaseDC(IntPtr hWnd, IntPtr hDC) + { + var tcs = new TaskCompletionSource(); + lock(s_Queue) + s_Queue.Enqueue(new ReleaseDCOp() + { + Window = hWnd, + DC = hDC, + Result = tcs + }); + s_Event.Set(); + tcs.Task.Wait(); + + } +} diff --git a/src/Windows/Avalonia.Win32/OpenGl/WglDisplay.cs b/src/Windows/Avalonia.Win32/OpenGl/WglDisplay.cs index bc27589689..976d226bdd 100644 --- a/src/Windows/Avalonia.Win32/OpenGl/WglDisplay.cs +++ b/src/Windows/Avalonia.Win32/OpenGl/WglDisplay.cs @@ -55,7 +55,7 @@ namespace Avalonia.Win32.OpenGl _windowClass = RegisterClassEx(ref wndClassEx); _bootstrapWindow = CreateOffscreenWindow(); - _bootstrapDc = GetDC(_bootstrapWindow); + _bootstrapDc = WglDCManager.GetDC(_bootstrapWindow); _defaultPfd = new PixelFormatDescriptor { Size = (ushort)Marshal.SizeOf(), @@ -132,7 +132,7 @@ namespace Avalonia.Win32.OpenGl using (new WglRestoreContext(_bootstrapDc, _bootstrapContext, null)) { var window = CreateOffscreenWindow(); - var dc = GetDC(window); + var dc = WglDCManager.GetDC(window); SetPixelFormat(dc, _defaultPixelFormat, ref _defaultPfd); foreach (var version in versions) { @@ -159,7 +159,7 @@ namespace Avalonia.Win32.OpenGl _defaultPixelFormat, _defaultPfd); } - ReleaseDC(window, dc); + WglDCManager.ReleaseDC(window, dc); DestroyWindow(window); return null; } diff --git a/src/Windows/Avalonia.Win32/OpenGl/WglGlPlatformSurface.cs b/src/Windows/Avalonia.Win32/OpenGl/WglGlPlatformSurface.cs index a94fee4573..2624df07ee 100644 --- a/src/Windows/Avalonia.Win32/OpenGl/WglGlPlatformSurface.cs +++ b/src/Windows/Avalonia.Win32/OpenGl/WglGlPlatformSurface.cs @@ -37,7 +37,7 @@ namespace Avalonia.Win32.OpenGl public void Dispose() { - UnmanagedMethods.ReleaseDC(_hdc, _info.Handle); + WglDCManager.ReleaseDC(_info.Handle, _hdc); } public IGlPlatformSurfaceRenderingSession BeginDraw() diff --git a/src/Windows/Avalonia.Win32/OpenGl/WglRestoreContext.cs b/src/Windows/Avalonia.Win32/OpenGl/WglRestoreContext.cs index 265f078a5c..b145ffbb37 100644 --- a/src/Windows/Avalonia.Win32/OpenGl/WglRestoreContext.cs +++ b/src/Windows/Avalonia.Win32/OpenGl/WglRestoreContext.cs @@ -1,4 +1,5 @@ using System; +using System.Runtime.InteropServices; using System.Threading; using Avalonia.OpenGL; using static Avalonia.Win32.Interop.UnmanagedMethods; @@ -22,9 +23,11 @@ namespace Avalonia.Win32.OpenGl if (!wglMakeCurrent(gc, context)) { + var lastError = Marshal.GetLastWin32Error(); + var caps = GetDeviceCaps(gc, (DEVICECAP)12); if(monitor != null && takeMonitor) Monitor.Exit(monitor); - throw new OpenGlException("Unable to make the context current"); + throw new OpenGlException($"Unable to make the context current: {lastError}, DC valid: {caps != 0}"); } } From d5bea7b6ba618fde9107125da0a73e82a246ec97 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Fri, 9 Dec 2022 12:01:47 +0100 Subject: [PATCH 55/87] Visibility cleanup --- .../TextFormatting/CharacterBufferRange.cs | 15 ---- .../CharacterBufferReference.cs | 83 +++---------------- .../Media/TextFormatting/TextCharacters.cs | 15 ---- 3 files changed, 11 insertions(+), 102 deletions(-) diff --git a/src/Avalonia.Base/Media/TextFormatting/CharacterBufferRange.cs b/src/Avalonia.Base/Media/TextFormatting/CharacterBufferRange.cs index 045f336700..d76f212f26 100644 --- a/src/Avalonia.Base/Media/TextFormatting/CharacterBufferRange.cs +++ b/src/Avalonia.Base/Media/TextFormatting/CharacterBufferRange.cs @@ -47,21 +47,6 @@ namespace Avalonia.Media.TextFormatting ) { } - /// - /// Construct from unsafe character string - /// - /// pointer to character string - /// character length - public unsafe CharacterBufferRange( - char* unsafeCharacterString, - int characterLength - ) - : this( - new CharacterBufferReference(unsafeCharacterString, characterLength), - characterLength - ) - { } - /// /// Construct a from /// diff --git a/src/Avalonia.Base/Media/TextFormatting/CharacterBufferReference.cs b/src/Avalonia.Base/Media/TextFormatting/CharacterBufferReference.cs index a15562cb52..672fcf3377 100644 --- a/src/Avalonia.Base/Media/TextFormatting/CharacterBufferReference.cs +++ b/src/Avalonia.Base/Media/TextFormatting/CharacterBufferReference.cs @@ -1,6 +1,4 @@ using System; -using System.Buffers; -using System.Runtime.InteropServices; namespace Avalonia.Media.TextFormatting { @@ -26,15 +24,6 @@ namespace Avalonia.Media.TextFormatting public CharacterBufferReference(string characterString, int offsetToFirstChar = 0) : this(characterString.AsMemory(), offsetToFirstChar) { } - - /// - /// Construct character buffer reference from unsafe character string - /// - /// pointer to character string - /// character length of unsafe string - public unsafe CharacterBufferReference(char* unsafeCharacterString, int characterLength) - : this(new UnmanagedMemoryManager(unsafeCharacterString, characterLength).Memory, 0) - { } /// /// Construct character buffer reference from memory buffer @@ -58,6 +47,17 @@ namespace Avalonia.Media.TextFormatting OffsetToFirstChar = offsetToFirstChar; } + /// + /// Gets the character memory buffer + /// + public ReadOnlyMemory CharacterBuffer { get; } + + /// + /// Gets the character offset relative to the beginning of buffer to + /// the first character of the run + /// + public int OffsetToFirstChar { get; } + /// /// Compute hash code /// @@ -110,67 +110,6 @@ namespace Avalonia.Media.TextFormatting { return !(left == right); } - - public ReadOnlyMemory CharacterBuffer { get; } - - public int OffsetToFirstChar { get; } - - /// - /// A MemoryManager over a raw pointer - /// - /// The pointer is assumed to be fully unmanaged, or externally pinned - no attempt will be made to pin this data - public sealed unsafe class UnmanagedMemoryManager : MemoryManager - where T : unmanaged - { - private readonly T* _pointer; - private readonly int _length; - - /// - /// Create a new UnmanagedMemoryManager instance at the given pointer and size - /// - /// It is assumed that the span provided is already unmanaged or externally pinned - public UnmanagedMemoryManager(Span span) - { - fixed (T* ptr = &MemoryMarshal.GetReference(span)) - { - _pointer = ptr; - _length = span.Length; - } - } - /// - /// Create a new UnmanagedMemoryManager instance at the given pointer and size - /// - public UnmanagedMemoryManager(T* pointer, int length) - { - if (length < 0) - throw new ArgumentOutOfRangeException(nameof(length)); - _pointer = pointer; - _length = length; - } - /// - /// Obtains a span that represents the region - /// - public override Span GetSpan() => new Span(_pointer, _length); - - /// - /// Provides access to a pointer that represents the data (note: no actual pin occurs) - /// - public override MemoryHandle Pin(int elementIndex = 0) - { - if (elementIndex < 0 || elementIndex >= _length) - throw new ArgumentOutOfRangeException(nameof(elementIndex)); - return new MemoryHandle(_pointer + elementIndex); - } - /// - /// Has no effect - /// - public override void Unpin() { } - - /// - /// Releases all resources associated with this object - /// - protected override void Dispose(bool disposing) { } - } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs index 9587786c5b..0be753bd04 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs @@ -57,21 +57,6 @@ namespace Avalonia.Media.TextFormatting ) { } - /// - /// Construct a run for text content from unsafe character string - /// - public unsafe TextCharacters( - char* unsafeCharacterString, - int length, - TextRunProperties textRunProperties - ) : - this( - new CharacterBufferReference(unsafeCharacterString, length), - length, - textRunProperties - ) - { } - /// /// Internal constructor of TextContent /// From f793f2b318b774feeb7811b4cb098718f0af52f7 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Fri, 9 Dec 2022 08:04:13 -0800 Subject: [PATCH 56/87] WGL: Lock shared context before creating a new one --- .../Avalonia.Win32/OpenGl/WglContext.cs | 7 ++++ .../Avalonia.Win32/OpenGl/WglDisplay.cs | 33 +++++++++++-------- 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/src/Windows/Avalonia.Win32/OpenGl/WglContext.cs b/src/Windows/Avalonia.Win32/OpenGl/WglContext.cs index a2c0d9203d..22d125fede 100644 --- a/src/Windows/Avalonia.Win32/OpenGl/WglContext.cs +++ b/src/Windows/Avalonia.Win32/OpenGl/WglContext.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Reactive.Disposables; +using System.Threading; using Avalonia.OpenGL; using Avalonia.Platform; using Avalonia.Win32.Interop; @@ -69,6 +70,12 @@ namespace Avalonia.Win32.OpenGl public bool IsLost { get; private set; } public IDisposable EnsureCurrent() => MakeCurrent(); + internal IDisposable Lock() + { + Monitor.Enter(_lock); + return Disposable.Create(_lock, Monitor.Exit); + } + public IntPtr CreateConfiguredDeviceContext(IntPtr hWnd) { diff --git a/src/Windows/Avalonia.Win32/OpenGl/WglDisplay.cs b/src/Windows/Avalonia.Win32/OpenGl/WglDisplay.cs index 976d226bdd..cac044e953 100644 --- a/src/Windows/Avalonia.Win32/OpenGl/WglDisplay.cs +++ b/src/Windows/Avalonia.Win32/OpenGl/WglDisplay.cs @@ -138,20 +138,25 @@ namespace Avalonia.Win32.OpenGl { if(version.Type != GlProfileType.OpenGL) continue; - var context = WglCreateContextAttribsArb(dc, shareContext?.Handle ?? IntPtr.Zero, - new[] - { - // major - WGL_CONTEXT_MAJOR_VERSION_ARB, version.Major, - // minor - WGL_CONTEXT_MINOR_VERSION_ARB, version.Minor, - // core profile - WGL_CONTEXT_PROFILE_MASK_ARB, 1, - // debug - // WGL_CONTEXT_FLAGS_ARB, 1, - // end - 0, 0 - }); + IntPtr context; + using (shareContext?.Lock()) + { + context = WglCreateContextAttribsArb(dc, shareContext?.Handle ?? IntPtr.Zero, + new[] + { + // major + WGL_CONTEXT_MAJOR_VERSION_ARB, version.Major, + // minor + WGL_CONTEXT_MINOR_VERSION_ARB, version.Minor, + // core profile + WGL_CONTEXT_PROFILE_MASK_ARB, 1, + // debug + // WGL_CONTEXT_FLAGS_ARB, 1, + // end + 0, 0 + }); + } + using(new WglRestoreContext(dc, context, null)) GlDebugMessageCallback(Marshal.GetFunctionPointerForDelegate(_debugCallback), IntPtr.Zero); if (context != IntPtr.Zero) From 165790dd635ae4979e1707124ba1a6a1e66388f1 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Fri, 9 Dec 2022 08:19:34 -0800 Subject: [PATCH 57/87] Use a named thread for HDC management --- src/Windows/Avalonia.Win32/OpenGl/WglDCManager.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Windows/Avalonia.Win32/OpenGl/WglDCManager.cs b/src/Windows/Avalonia.Win32/OpenGl/WglDCManager.cs index 2698c8eb5f..e35e90c447 100644 --- a/src/Windows/Avalonia.Win32/OpenGl/WglDCManager.cs +++ b/src/Windows/Avalonia.Win32/OpenGl/WglDCManager.cs @@ -55,7 +55,11 @@ internal class WglDCManager static WglDCManager() { - new Thread(Worker) { IsBackground = true }.Start(); + new Thread(Worker) + { + IsBackground = true, + Name = "Win32 OpenGL HDC manager" + }.Start(); } public static IntPtr GetDC(IntPtr hWnd) From 30db969e364e8ec2f99314c14de5fd39e95c5eaf Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Fri, 9 Dec 2022 08:47:33 -0800 Subject: [PATCH 58/87] Make WglDCManager to also manage offscreen windows --- .../Avalonia.Win32/OpenGl/WglContext.cs | 2 +- .../Avalonia.Win32/OpenGl/WglDCManager.cs | 75 +++++++++++++++++++ .../Avalonia.Win32/OpenGl/WglDisplay.cs | 45 ++--------- 3 files changed, 84 insertions(+), 38 deletions(-) diff --git a/src/Windows/Avalonia.Win32/OpenGl/WglContext.cs b/src/Windows/Avalonia.Win32/OpenGl/WglContext.cs index 22d125fede..c4034c7211 100644 --- a/src/Windows/Avalonia.Win32/OpenGl/WglContext.cs +++ b/src/Windows/Avalonia.Win32/OpenGl/WglContext.cs @@ -48,7 +48,7 @@ namespace Avalonia.Win32.OpenGl { wglDeleteContext(_context); WglDCManager.ReleaseDC(_hWnd, _dc); - DestroyWindow(_hWnd); + WglDCManager.DestroyWindow(_hWnd); IsLost = true; } diff --git a/src/Windows/Avalonia.Win32/OpenGl/WglDCManager.cs b/src/Windows/Avalonia.Win32/OpenGl/WglDCManager.cs index e35e90c447..17c7fa62c6 100644 --- a/src/Windows/Avalonia.Win32/OpenGl/WglDCManager.cs +++ b/src/Windows/Avalonia.Win32/OpenGl/WglDCManager.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; using Avalonia.Win32.Interop; @@ -28,9 +29,22 @@ internal class WglDCManager public IntPtr DC; public TaskCompletionSource Result; } + + class CreateWindowOp + { + public TaskCompletionSource Result; + } + + class DestroyWindowOp + { + public IntPtr Window; + public TaskCompletionSource Result; + } private static readonly Queue s_Queue = new(); private static readonly AutoResetEvent s_Event = new(false); + private static readonly ushort s_WindowClass; + private static readonly UnmanagedMethods.WndProc s_wndProcDelegate = WndProc; static void Worker() { @@ -49,12 +63,41 @@ internal class WglDCManager UnmanagedMethods.ReleaseDC(releaseDc.Window, releaseDc.DC); releaseDc.Result.SetResult(null); } + else if (job is CreateWindowOp createWindow) + createWindow.Result.TrySetResult(UnmanagedMethods.CreateWindowEx( + 0, + s_WindowClass, + null, + (int)UnmanagedMethods.WindowStyles.WS_OVERLAPPEDWINDOW, + 0, + 0, + 640, + 480, + IntPtr.Zero, + IntPtr.Zero, + IntPtr.Zero, + IntPtr.Zero)); + else if (job is DestroyWindowOp destroyWindow) + { + UnmanagedMethods.DestroyWindow(destroyWindow.Window); + destroyWindow.Result.TrySetResult(null); + } } } } static WglDCManager() { + var wndClassEx = new UnmanagedMethods.WNDCLASSEX + { + cbSize = Marshal.SizeOf(), + hInstance = UnmanagedMethods.GetModuleHandle(null), + lpfnWndProc = s_wndProcDelegate, + lpszClassName = "AvaloniaGlWindow-" + Guid.NewGuid(), + style = (int)UnmanagedMethods.ClassStyles.CS_OWNDC + }; + + s_WindowClass = UnmanagedMethods.RegisterClassEx(ref wndClassEx); new Thread(Worker) { IsBackground = true, @@ -62,6 +105,25 @@ internal class WglDCManager }.Start(); } + + + static IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) + { + return UnmanagedMethods.DefWindowProc(hWnd, msg, wParam, lParam); + } + + public static IntPtr CreateOffscreenWindow() + { + var tcs = new TaskCompletionSource(); + lock(s_Queue) + s_Queue.Enqueue(new CreateWindowOp() + { + Result = tcs + }); + s_Event.Set(); + return tcs.Task.Result; + } + public static IntPtr GetDC(IntPtr hWnd) { var tcs = new TaskCompletionSource(); @@ -87,6 +149,19 @@ internal class WglDCManager }); s_Event.Set(); tcs.Task.Wait(); + } + + public static void DestroyWindow(IntPtr hWnd) + { + var tcs = new TaskCompletionSource(); + lock(s_Queue) + s_Queue.Enqueue(new DestroyWindowOp() + { + Window = hWnd, + Result = tcs + }); + s_Event.Set(); + tcs.Task.Wait(); } } diff --git a/src/Windows/Avalonia.Win32/OpenGl/WglDisplay.cs b/src/Windows/Avalonia.Win32/OpenGl/WglDisplay.cs index cac044e953..300e43e656 100644 --- a/src/Windows/Avalonia.Win32/OpenGl/WglDisplay.cs +++ b/src/Windows/Avalonia.Win32/OpenGl/WglDisplay.cs @@ -1,6 +1,7 @@ using System; using System.Runtime.InteropServices; using Avalonia.OpenGL; +using Avalonia.Threading; using Avalonia.Win32.Interop; using static Avalonia.Win32.Interop.UnmanagedMethods; using static Avalonia.Win32.OpenGl.WglConsts; @@ -9,8 +10,6 @@ namespace Avalonia.Win32.OpenGl internal class WglDisplay { private static bool? _initialized; - private static ushort _windowClass; - private static readonly WndProc _wndProcDelegate = WndProc; private static readonly DebugCallbackDelegate _debugCallback = DebugCallback; private static IntPtr _bootstrapContext; @@ -44,17 +43,8 @@ namespace Avalonia.Win32.OpenGl } static bool InitializeCore() { - var wndClassEx = new WNDCLASSEX - { - cbSize = Marshal.SizeOf(), - hInstance = GetModuleHandle(null), - lpfnWndProc = _wndProcDelegate, - lpszClassName = "AvaloniaGlWindow-" + Guid.NewGuid(), - style = (int)ClassStyles.CS_OWNDC - }; - - _windowClass = RegisterClassEx(ref wndClassEx); - _bootstrapWindow = CreateOffscreenWindow(); + Dispatcher.UIThread.VerifyAccess(); + _bootstrapWindow = WglDCManager.CreateOffscreenWindow(); _bootstrapDc = WglDCManager.GetDC(_bootstrapWindow); _defaultPfd = new PixelFormatDescriptor { @@ -105,17 +95,11 @@ namespace Avalonia.Win32.OpenGl DescribePixelFormat(_bootstrapDc, formats[0], Marshal.SizeOf(), ref _defaultPfd); _defaultPixelFormat = formats[0]; } - - + wglMakeCurrent(IntPtr.Zero, IntPtr.Zero); return true; } - - static IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) - { - return DefWindowProc(hWnd, msg, wParam, lParam); - } - + private static void DebugCallback(int source, int type, int id, int severity, int len, IntPtr message, IntPtr userparam) { var err = Marshal.PtrToStringAnsi(message, len); @@ -131,7 +115,7 @@ namespace Avalonia.Win32.OpenGl using (new WglRestoreContext(_bootstrapDc, _bootstrapContext, null)) { - var window = CreateOffscreenWindow(); + var window = WglDCManager.CreateOffscreenWindow(); var dc = WglDCManager.GetDC(window); SetPixelFormat(dc, _defaultPixelFormat, ref _defaultPfd); foreach (var version in versions) @@ -165,25 +149,12 @@ namespace Avalonia.Win32.OpenGl } WglDCManager.ReleaseDC(window, dc); - DestroyWindow(window); + WglDCManager.DestroyWindow(window); return null; } } - static IntPtr CreateOffscreenWindow() => - CreateWindowEx( - 0, - _windowClass, - null, - (int)WindowStyles.WS_OVERLAPPEDWINDOW, - 0, - 0, - 640, - 480, - IntPtr.Zero, - IntPtr.Zero, - IntPtr.Zero, - IntPtr.Zero); + } } From 124e055ffc87b49d5cb5e628936315362a43cd54 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Fri, 9 Dec 2022 08:51:02 -0800 Subject: [PATCH 59/87] Naming --- src/Windows/Avalonia.Win32/OpenGl/WglContext.cs | 6 +++--- src/Windows/Avalonia.Win32/OpenGl/WglDisplay.cs | 12 ++++++------ .../{WglDCManager.cs => WglGdiResourceManager.cs} | 12 ++++++------ .../Avalonia.Win32/OpenGl/WglGlPlatformSurface.cs | 2 +- 4 files changed, 16 insertions(+), 16 deletions(-) rename src/Windows/Avalonia.Win32/OpenGl/{WglDCManager.cs => WglGdiResourceManager.cs} (91%) diff --git a/src/Windows/Avalonia.Win32/OpenGl/WglContext.cs b/src/Windows/Avalonia.Win32/OpenGl/WglContext.cs index c4034c7211..d535efc5b7 100644 --- a/src/Windows/Avalonia.Win32/OpenGl/WglContext.cs +++ b/src/Windows/Avalonia.Win32/OpenGl/WglContext.cs @@ -47,8 +47,8 @@ namespace Avalonia.Win32.OpenGl public void Dispose() { wglDeleteContext(_context); - WglDCManager.ReleaseDC(_hWnd, _dc); - WglDCManager.DestroyWindow(_hWnd); + WglGdiResourceManager.ReleaseDC(_hWnd, _dc); + WglGdiResourceManager.DestroyWindow(_hWnd); IsLost = true; } @@ -79,7 +79,7 @@ namespace Avalonia.Win32.OpenGl public IntPtr CreateConfiguredDeviceContext(IntPtr hWnd) { - var dc = WglDCManager.GetDC(hWnd); + var dc = WglGdiResourceManager.GetDC(hWnd); var fmt = _formatDescriptor; SetPixelFormat(dc, _pixelFormat, ref fmt); return dc; diff --git a/src/Windows/Avalonia.Win32/OpenGl/WglDisplay.cs b/src/Windows/Avalonia.Win32/OpenGl/WglDisplay.cs index 300e43e656..9a1963a97a 100644 --- a/src/Windows/Avalonia.Win32/OpenGl/WglDisplay.cs +++ b/src/Windows/Avalonia.Win32/OpenGl/WglDisplay.cs @@ -44,8 +44,8 @@ namespace Avalonia.Win32.OpenGl static bool InitializeCore() { Dispatcher.UIThread.VerifyAccess(); - _bootstrapWindow = WglDCManager.CreateOffscreenWindow(); - _bootstrapDc = WglDCManager.GetDC(_bootstrapWindow); + _bootstrapWindow = WglGdiResourceManager.CreateOffscreenWindow(); + _bootstrapDc = WglGdiResourceManager.GetDC(_bootstrapWindow); _defaultPfd = new PixelFormatDescriptor { Size = (ushort)Marshal.SizeOf(), @@ -115,8 +115,8 @@ namespace Avalonia.Win32.OpenGl using (new WglRestoreContext(_bootstrapDc, _bootstrapContext, null)) { - var window = WglDCManager.CreateOffscreenWindow(); - var dc = WglDCManager.GetDC(window); + var window = WglGdiResourceManager.CreateOffscreenWindow(); + var dc = WglGdiResourceManager.GetDC(window); SetPixelFormat(dc, _defaultPixelFormat, ref _defaultPfd); foreach (var version in versions) { @@ -148,8 +148,8 @@ namespace Avalonia.Win32.OpenGl _defaultPixelFormat, _defaultPfd); } - WglDCManager.ReleaseDC(window, dc); - WglDCManager.DestroyWindow(window); + WglGdiResourceManager.ReleaseDC(window, dc); + WglGdiResourceManager.DestroyWindow(window); return null; } } diff --git a/src/Windows/Avalonia.Win32/OpenGl/WglDCManager.cs b/src/Windows/Avalonia.Win32/OpenGl/WglGdiResourceManager.cs similarity index 91% rename from src/Windows/Avalonia.Win32/OpenGl/WglDCManager.cs rename to src/Windows/Avalonia.Win32/OpenGl/WglGdiResourceManager.cs index 17c7fa62c6..1a87520590 100644 --- a/src/Windows/Avalonia.Win32/OpenGl/WglDCManager.cs +++ b/src/Windows/Avalonia.Win32/OpenGl/WglGdiResourceManager.cs @@ -8,14 +8,14 @@ using Avalonia.Win32.Interop; namespace Avalonia.Win32.OpenGl; /// -/// 1) ReleaseDC can only happen from the same thread that has called GetDC -/// 2) When thread exits all of its HDCs are getting destroyed -/// 3) We need to create OpenGL render targets from thread pool threads +/// - ReleaseDC can only happen from the same thread that has called GetDC +/// - When thread exits all of its windows and HDCs are getting destroyed +/// - We need to create OpenGL context (require a window and an HDC) and render targets (require an HDC) from thread pool threads /// -/// So this class hosts a dedicated thread for managing HDCs for OpenGL +/// So this class hosts a dedicated thread for managing offscreen windows and HDCs for OpenGL /// -internal class WglDCManager +internal class WglGdiResourceManager { class GetDCOp { @@ -86,7 +86,7 @@ internal class WglDCManager } } - static WglDCManager() + static WglGdiResourceManager() { var wndClassEx = new UnmanagedMethods.WNDCLASSEX { diff --git a/src/Windows/Avalonia.Win32/OpenGl/WglGlPlatformSurface.cs b/src/Windows/Avalonia.Win32/OpenGl/WglGlPlatformSurface.cs index 2624df07ee..6d3d2d28c4 100644 --- a/src/Windows/Avalonia.Win32/OpenGl/WglGlPlatformSurface.cs +++ b/src/Windows/Avalonia.Win32/OpenGl/WglGlPlatformSurface.cs @@ -37,7 +37,7 @@ namespace Avalonia.Win32.OpenGl public void Dispose() { - WglDCManager.ReleaseDC(_info.Handle, _hdc); + WglGdiResourceManager.ReleaseDC(_info.Handle, _hdc); } public IGlPlatformSurfaceRenderingSession BeginDraw() From abafe2ad33f2afd076e208e96e8c05f65ea0643d Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Fri, 9 Dec 2022 09:09:28 -0800 Subject: [PATCH 60/87] Use STA just in case --- .../Avalonia.Win32/OpenGl/WglGdiResourceManager.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/Windows/Avalonia.Win32/OpenGl/WglGdiResourceManager.cs b/src/Windows/Avalonia.Win32/OpenGl/WglGdiResourceManager.cs index 1a87520590..ed40488c70 100644 --- a/src/Windows/Avalonia.Win32/OpenGl/WglGdiResourceManager.cs +++ b/src/Windows/Avalonia.Win32/OpenGl/WglGdiResourceManager.cs @@ -98,11 +98,10 @@ internal class WglGdiResourceManager }; s_WindowClass = UnmanagedMethods.RegisterClassEx(ref wndClassEx); - new Thread(Worker) - { - IsBackground = true, - Name = "Win32 OpenGL HDC manager" - }.Start(); + var th = new Thread(Worker) { IsBackground = true, Name = "Win32 OpenGL HDC manager" }; + // This makes CLR to automatically pump the event queue from WaitOne + th.SetApartmentState(ApartmentState.STA); + th.Start(); } From be9923139982aa19107104fae6397d4ea5a0003c Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Fri, 9 Dec 2022 09:25:50 -0800 Subject: [PATCH 61/87] Delay SKSurface disposal if context is lost --- .../Gpu/OpenGl/FboSkiaSurface.cs | 39 +++++++++++++------ .../Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs | 14 ++++++- 2 files changed, 41 insertions(+), 12 deletions(-) diff --git a/src/Skia/Avalonia.Skia/Gpu/OpenGl/FboSkiaSurface.cs b/src/Skia/Avalonia.Skia/Gpu/OpenGl/FboSkiaSurface.cs index 5cc338a469..e19379df09 100644 --- a/src/Skia/Avalonia.Skia/Gpu/OpenGl/FboSkiaSurface.cs +++ b/src/Skia/Avalonia.Skia/Gpu/OpenGl/FboSkiaSurface.cs @@ -1,11 +1,13 @@ using System; using Avalonia.OpenGL; +using Avalonia.Platform; using SkiaSharp; using static Avalonia.OpenGL.GlConsts; namespace Avalonia.Skia { - public class FboSkiaSurface : ISkiaSurface + internal class FboSkiaSurface : ISkiaSurface { + private readonly GlSkiaGpu _gpu; private readonly GRContext _grContext; private readonly IGlContext _glContext; private readonly PixelSize _pixelSize; @@ -14,8 +16,9 @@ namespace Avalonia.Skia private int _texture; private static readonly bool[] TrueFalse = new[] { true, false }; - public FboSkiaSurface(GRContext grContext, IGlContext glContext, PixelSize pixelSize, GRSurfaceOrigin surfaceOrigin) + public FboSkiaSurface(GlSkiaGpu gpu, GRContext grContext, IGlContext glContext, PixelSize pixelSize, GRSurfaceOrigin surfaceOrigin) { + _gpu = gpu; _grContext = grContext; _glContext = glContext; _pixelSize = pixelSize; @@ -93,19 +96,33 @@ namespace Avalonia.Skia public void Dispose() { - using (_glContext.EnsureCurrent()) + try { - Surface?.Dispose(); - Surface = null; - var gl = _glContext.GlInterface; - if (_fbo != 0) + using (_glContext.EnsureCurrent()) { - gl.DeleteFramebuffer(_fbo); - gl.DeleteTexture(_texture); - gl.DeleteRenderbuffer(_depthStencil); - _fbo = _texture = _depthStencil = 0; + Surface?.Dispose(); + Surface = null; + var gl = _glContext.GlInterface; + if (_fbo != 0) + { + gl.DeleteFramebuffer(_fbo); + gl.DeleteTexture(_texture); + gl.DeleteRenderbuffer(_depthStencil); + } } } + catch (PlatformGraphicsContextLostException) + { + if (Surface != null) + // We need to dispose SKSurface _after_ GRContext.Abandon was called, + // otherwise it will try to do OpenGL calls without a proper context + _gpu.AddPostDispose(Surface.Dispose); + Surface = null; + } + finally + { + _fbo = _texture = _depthStencil = 0; + } } public SKSurface Surface { get; private set; } diff --git a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs index 002129a1eb..cdba3b9ea2 100644 --- a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs +++ b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs @@ -14,6 +14,7 @@ namespace Avalonia.Skia { private GRContext _grContext; private IGlContext _glContext; + private List _postDisposeCallbacks = new(); private bool? _canCreateSurfaces; public GlSkiaGpu(IGlContext context, long? maxResourceBytes) @@ -83,7 +84,8 @@ namespace Avalonia.Skia return null; try { - var surface = new FboSkiaSurface(_grContext, _glContext, size, session?.SurfaceOrigin ?? GRSurfaceOrigin.TopLeft); + var surface = new FboSkiaSurface(this, _grContext, _glContext, size, + session?.SurfaceOrigin ?? GRSurfaceOrigin.TopLeft); _canCreateSurfaces = true; return surface; } @@ -110,6 +112,10 @@ namespace Avalonia.Skia else _grContext.AbandonContext(true); _grContext.Dispose(); + + lock(_postDisposeCallbacks) + foreach (var cb in _postDisposeCallbacks) + cb(); } public bool IsLost => _glContext.IsLost; @@ -121,5 +127,11 @@ namespace Avalonia.Skia return this; return null; } + + public void AddPostDispose(Action dispose) + { + lock (_postDisposeCallbacks) + _postDisposeCallbacks.Add(dispose); + } } } From 728fdd5e8054bebe28ff212126f326c6c53aed52 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Sat, 10 Dec 2022 11:02:30 +0100 Subject: [PATCH 62/87] fix: EmbeddableControlRoot Themes --- src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs | 2 -- src/Avalonia.Themes.Fluent/Controls/EmbeddableControlRoot.xaml | 1 + src/Avalonia.Themes.Simple/Controls/EmbeddableControlRoot.xaml | 1 + 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs b/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs index 5ff0fd1feb..b79fef55b9 100644 --- a/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs +++ b/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs @@ -4,7 +4,6 @@ using Avalonia.Controls.Platform; using Avalonia.Input; using Avalonia.Platform; using Avalonia.Styling; -using JetBrains.Annotations; namespace Avalonia.Controls.Embedding { @@ -12,7 +11,6 @@ namespace Avalonia.Controls.Embedding { public EmbeddableControlRoot(ITopLevelImpl impl) : base(impl) { - } public EmbeddableControlRoot() : base(PlatformManager.CreateEmbeddableWindow()) diff --git a/src/Avalonia.Themes.Fluent/Controls/EmbeddableControlRoot.xaml b/src/Avalonia.Themes.Fluent/Controls/EmbeddableControlRoot.xaml index f8b4854553..67e70d154f 100644 --- a/src/Avalonia.Themes.Fluent/Controls/EmbeddableControlRoot.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/EmbeddableControlRoot.xaml @@ -1,6 +1,7 @@ + diff --git a/src/Avalonia.Themes.Simple/Controls/EmbeddableControlRoot.xaml b/src/Avalonia.Themes.Simple/Controls/EmbeddableControlRoot.xaml index 79d6c6d917..3d52464858 100644 --- a/src/Avalonia.Themes.Simple/Controls/EmbeddableControlRoot.xaml +++ b/src/Avalonia.Themes.Simple/Controls/EmbeddableControlRoot.xaml @@ -2,6 +2,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> + From aaeff2832eb83d09847740436c6b622220f0952f Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Sun, 11 Dec 2022 18:21:09 +0100 Subject: [PATCH 63/87] fix: MSB4011 --- src/Windows/Avalonia.Win32/Avalonia.Win32.csproj | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj b/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj index 08f12f5aea..eb51b7fd07 100644 --- a/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj +++ b/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj @@ -23,8 +23,6 @@ - - $(NoWarn);CA1416 From 60159ffb595e70c3851aece51df94631f4d61c7c Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sun, 11 Dec 2022 14:24:29 -0500 Subject: [PATCH 64/87] Update documentation and add missing trimming attribute --- .../Styling/MergeResourceInclude.cs | 11 ++++++++++- .../Avalonia.Markup.Xaml/Styling/ResourceInclude.cs | 4 ++++ .../Avalonia.Markup.Xaml/Styling/StyleInclude.cs | 4 ++++ 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/Markup/Avalonia.Markup.Xaml/Styling/MergeResourceInclude.cs b/src/Markup/Avalonia.Markup.Xaml/Styling/MergeResourceInclude.cs index ed7bb98833..cd3b235e10 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Styling/MergeResourceInclude.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Styling/MergeResourceInclude.cs @@ -1,8 +1,17 @@ using System; +using System.Diagnostics.CodeAnalysis; using Avalonia.Controls; namespace Avalonia.Markup.Xaml.Styling; +/// +/// Loads a resource dictionary from a specified URL. +/// +/// +/// If used from the XAML code, it is merged into the parent dictionary in the compile time. +/// When used in runtime, this type behaves like . +/// +[RequiresUnreferencedCode(TrimmingMessages.StyleResourceIncludeRequiresUnreferenceCodeMessage)] public class MergeResourceInclude : IResourceProvider { private readonly IServiceProvider _serviceProvider; @@ -31,7 +40,7 @@ public class MergeResourceInclude : IResourceProvider { _isLoading = true; var source = Source ?? throw new InvalidOperationException("MergeResourceInclude.Source must be set."); - _loaded = (IResourceDictionary)AvaloniaXamlLoader.Load(source, _baseUri); + _loaded = (IResourceDictionary)AvaloniaXamlLoader.Load(_serviceProvider, source, _baseUri); _isLoading = false; } diff --git a/src/Markup/Avalonia.Markup.Xaml/Styling/ResourceInclude.cs b/src/Markup/Avalonia.Markup.Xaml/Styling/ResourceInclude.cs index 3d8a61364a..595b37f7d1 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Styling/ResourceInclude.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Styling/ResourceInclude.cs @@ -9,6 +9,10 @@ namespace Avalonia.Markup.Xaml.Styling /// /// Loads a resource dictionary from a specified URL. /// + /// + /// If used from the XAML code, it is replaced with direct resource dictionary reference. + /// When used in runtime, this type might be unsafe with trimming and AOT. + /// [RequiresUnreferencedCode(TrimmingMessages.StyleResourceIncludeRequiresUnreferenceCodeMessage)] public class ResourceInclude : IResourceProvider { diff --git a/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs b/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs index bdde6e725c..b87aa64297 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs @@ -11,6 +11,10 @@ namespace Avalonia.Markup.Xaml.Styling /// /// Includes a style from a URL. /// + /// + /// If used from the XAML code, it is replaced with direct style reference. + /// When used in runtime, this type might be unsafe with trimming and AOT. + /// [RequiresUnreferencedCode(TrimmingMessages.StyleResourceIncludeRequiresUnreferenceCodeMessage)] public class StyleInclude : IStyle, IResourceProvider { From 19b9cd2ee644b8a4dd5190bab4d21e8fe230b054 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sun, 11 Dec 2022 14:25:41 -0500 Subject: [PATCH 65/87] Inherit ResourceInclude from the MergeResourceInclude --- .../Styling/MergeResourceInclude.cs | 63 ++----------------- 1 file changed, 5 insertions(+), 58 deletions(-) diff --git a/src/Markup/Avalonia.Markup.Xaml/Styling/MergeResourceInclude.cs b/src/Markup/Avalonia.Markup.Xaml/Styling/MergeResourceInclude.cs index cd3b235e10..d231a1d221 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Styling/MergeResourceInclude.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Styling/MergeResourceInclude.cs @@ -2,6 +2,8 @@ using System.Diagnostics.CodeAnalysis; using Avalonia.Controls; +#nullable enable + namespace Avalonia.Markup.Xaml.Styling; /// @@ -12,68 +14,13 @@ namespace Avalonia.Markup.Xaml.Styling; /// When used in runtime, this type behaves like . /// [RequiresUnreferencedCode(TrimmingMessages.StyleResourceIncludeRequiresUnreferenceCodeMessage)] -public class MergeResourceInclude : IResourceProvider +public class MergeResourceInclude : ResourceInclude { - private readonly IServiceProvider _serviceProvider; - private readonly Uri? _baseUri; - private IResourceDictionary? _loaded; - private bool _isLoading; - - /// - /// Initializes a new instance of the class. - /// - /// The XAML service provider. - public MergeResourceInclude(IServiceProvider serviceProvider) - { - _serviceProvider = serviceProvider; - _baseUri = serviceProvider.GetContextBaseUri(); - } - - /// - /// Gets the loaded resource dictionary. - /// - public IResourceDictionary Loaded - { - get - { - if (_loaded == null) - { - _isLoading = true; - var source = Source ?? throw new InvalidOperationException("MergeResourceInclude.Source must be set."); - _loaded = (IResourceDictionary)AvaloniaXamlLoader.Load(_serviceProvider, source, _baseUri); - _isLoading = false; - } - - return _loaded; - } - } - - public IResourceHost? Owner => Loaded.Owner; - - /// - /// Gets or sets the source URL. - /// - public Uri? Source { get; set; } - - bool IResourceNode.HasResources => Loaded.HasResources; - - public event EventHandler? OwnerChanged + public MergeResourceInclude(Uri? baseUri) : base(baseUri) { - add => Loaded.OwnerChanged += value; - remove => Loaded.OwnerChanged -= value; } - bool IResourceNode.TryGetResource(object key, out object? value) + public MergeResourceInclude(IServiceProvider serviceProvider) : base(serviceProvider) { - if (!_isLoading) - { - return Loaded.TryGetResource(key, out value); - } - - value = null; - return false; } - - void IResourceProvider.AddOwner(IResourceHost owner) => Loaded.AddOwner(owner); - void IResourceProvider.RemoveOwner(IResourceHost owner) => Loaded.RemoveOwner(owner); } From 4bb3a0d8bebce6eeadca964f36f729f55bc46621 Mon Sep 17 00:00:00 2001 From: Julian Date: Mon, 12 Dec 2022 11:55:28 +0000 Subject: [PATCH 66/87] Add missing SystemControlBackgroundAltLowBrush in fluent theme --- src/Avalonia.Themes.Fluent/Accents/BaseDark.xaml | 1 + src/Avalonia.Themes.Fluent/Accents/BaseLight.xaml | 1 + 2 files changed, 2 insertions(+) diff --git a/src/Avalonia.Themes.Fluent/Accents/BaseDark.xaml b/src/Avalonia.Themes.Fluent/Accents/BaseDark.xaml index 89841c92c0..0192fb1b54 100644 --- a/src/Avalonia.Themes.Fluent/Accents/BaseDark.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/BaseDark.xaml @@ -36,6 +36,7 @@ + diff --git a/src/Avalonia.Themes.Fluent/Accents/BaseLight.xaml b/src/Avalonia.Themes.Fluent/Accents/BaseLight.xaml index 89b646fb52..a9e5ed949a 100644 --- a/src/Avalonia.Themes.Fluent/Accents/BaseLight.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/BaseLight.xaml @@ -36,6 +36,7 @@ + From cc7427dd721f436b481a4f71c296b13fd6f22af0 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 12 Dec 2022 14:06:22 +0100 Subject: [PATCH 67/87] Don't send KeyUp/KeyDown events for Key.None. --- .../Avalonia.Win32/WindowImpl.AppWndProc.cs | 28 +++++++++++++------ 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs index 5537a0b704..fe7d881f11 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs @@ -134,13 +134,18 @@ namespace Avalonia.Win32 case WindowsMessage.WM_KEYDOWN: case WindowsMessage.WM_SYSKEYDOWN: { - e = new RawKeyEventArgs( - WindowsKeyboardDevice.Instance, - timestamp, - _owner, - RawKeyEventType.KeyDown, - KeyInterop.KeyFromVirtualKey(ToInt32(wParam), ToInt32(lParam)), - WindowsKeyboardDevice.Instance.Modifiers); + var key = KeyInterop.KeyFromVirtualKey(ToInt32(wParam), ToInt32(lParam)); + + if (key != Key.None) + { + e = new RawKeyEventArgs( + WindowsKeyboardDevice.Instance, + timestamp, + _owner, + RawKeyEventType.KeyDown, + key, + WindowsKeyboardDevice.Instance.Modifiers); + } break; } @@ -159,13 +164,18 @@ namespace Avalonia.Win32 case WindowsMessage.WM_KEYUP: case WindowsMessage.WM_SYSKEYUP: { - e = new RawKeyEventArgs( + var key = KeyInterop.KeyFromVirtualKey(ToInt32(wParam), ToInt32(lParam)); + + if (key != Key.None) + { + e = new RawKeyEventArgs( WindowsKeyboardDevice.Instance, timestamp, _owner, RawKeyEventType.KeyUp, - KeyInterop.KeyFromVirtualKey(ToInt32(wParam), ToInt32(lParam)), + key, WindowsKeyboardDevice.Instance.Modifiers); + } break; } case WindowsMessage.WM_CHAR: From 8c16d0420db4b064ff990a33fa6e88253e81caf2 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Mon, 12 Dec 2022 15:10:40 +0100 Subject: [PATCH 68/87] fix: Drop Direct3DInteropSample --- Avalonia.sln | 11 +- .../interop/Direct3DInteropSample/App.paml | 5 - .../interop/Direct3DInteropSample/App.paml.cs | 21 -- .../Direct3DInteropSample.csproj | 32 -- .../Direct3DInteropSample/MainWindow.cs | 283 ------------------ .../Direct3DInteropSample/MainWindow.paml | 14 - .../MainWindowViewModel.cs | 45 --- .../interop/Direct3DInteropSample/MiniCube.fx | 47 --- .../interop/Direct3DInteropSample/Program.cs | 16 - 9 files changed, 2 insertions(+), 472 deletions(-) delete mode 100644 samples/interop/Direct3DInteropSample/App.paml delete mode 100644 samples/interop/Direct3DInteropSample/App.paml.cs delete mode 100644 samples/interop/Direct3DInteropSample/Direct3DInteropSample.csproj delete mode 100644 samples/interop/Direct3DInteropSample/MainWindow.cs delete mode 100644 samples/interop/Direct3DInteropSample/MainWindow.paml delete mode 100644 samples/interop/Direct3DInteropSample/MainWindowViewModel.cs delete mode 100644 samples/interop/Direct3DInteropSample/MiniCube.fx delete mode 100644 samples/interop/Direct3DInteropSample/Program.cs diff --git a/Avalonia.sln b/Avalonia.sln index 7efb294b64..e6898131b0 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -136,8 +136,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Linux", "Linux", "{86C53C40 EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.LinuxFramebuffer", "src\Linux\Avalonia.LinuxFramebuffer\Avalonia.LinuxFramebuffer.csproj", "{854568D5-13D1-4B4F-B50D-534DC7EFD3C9}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Direct3DInteropSample", "samples\interop\Direct3DInteropSample\Direct3DInteropSample.csproj", "{638580B0-7910-40EF-B674-DCB34DA308CD}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Win32.Interop", "src\Windows\Avalonia.Win32.Interop\Avalonia.Win32.Interop.csproj", "{CBC4FF2F-92D4-420B-BE21-9FE0B930B04E}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Skia.RenderTests", "tests\Avalonia.Skia.RenderTests\Avalonia.Skia.RenderTests.csproj", "{E1582370-37B3-403C-917F-8209551B1634}" @@ -228,9 +226,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Browser", "src\Bro EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Browser.Blazor", "src\Browser\Avalonia.Browser.Blazor\Avalonia.Browser.Blazor.csproj", "{47F8530C-F19B-4B1A-B4D6-EB231522AE5D}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ControlCatalog.Browser", "samples\ControlCatalog.Browser\ControlCatalog.Browser.csproj", "{15B93A4C-1B46-43F6-B534-7B25B6E99932}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlCatalog.Browser", "samples\ControlCatalog.Browser\ControlCatalog.Browser.csproj", "{15B93A4C-1B46-43F6-B534-7B25B6E99932}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ControlCatalog.Browser.Blazor", "samples\ControlCatalog.Browser.Blazor\ControlCatalog.Browser.Blazor.csproj", "{90B08091-9BBD-4362-B712-E9F2CC62B218}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlCatalog.Browser.Blazor", "samples\ControlCatalog.Browser.Blazor\ControlCatalog.Browser.Blazor.csproj", "{90B08091-9BBD-4362-B712-E9F2CC62B218}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReactiveUIDemo", "samples\ReactiveUIDemo\ReactiveUIDemo.csproj", "{75C47156-C5D8-44BC-A5A7-E8657C2248D6}" EndProject @@ -366,10 +364,6 @@ Global {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.Debug|Any CPU.Build.0 = Debug|Any CPU {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.Release|Any CPU.ActiveCfg = Release|Any CPU {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.Release|Any CPU.Build.0 = Release|Any CPU - {638580B0-7910-40EF-B674-DCB34DA308CD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {638580B0-7910-40EF-B674-DCB34DA308CD}.Debug|Any CPU.Build.0 = Debug|Any CPU - {638580B0-7910-40EF-B674-DCB34DA308CD}.Release|Any CPU.ActiveCfg = Release|Any CPU - {638580B0-7910-40EF-B674-DCB34DA308CD}.Release|Any CPU.Build.0 = Release|Any CPU {CBC4FF2F-92D4-420B-BE21-9FE0B930B04E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {CBC4FF2F-92D4-420B-BE21-9FE0B930B04E}.Debug|Any CPU.Build.0 = Debug|Any CPU {CBC4FF2F-92D4-420B-BE21-9FE0B930B04E}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -580,7 +574,6 @@ Global {7D2D3083-71DD-4CC9-8907-39A0D86FB322} = {3743B0F2-CC41-4F14-A8C8-267F579BF91E} {39D7B147-1A5B-47C2-9D01-21FB7C47C4B3} = {9B9E3891-2366-4253-A952-D08BCEB71098} {854568D5-13D1-4B4F-B50D-534DC7EFD3C9} = {86C53C40-57AA-45B8-AD42-FAE0EFDF0F2B} - {638580B0-7910-40EF-B674-DCB34DA308CD} = {A0CC0258-D18C-4AB3-854F-7101680FC3F9} {CBC4FF2F-92D4-420B-BE21-9FE0B930B04E} = {B39A8919-9F95-48FE-AD7B-76E08B509888} {E1582370-37B3-403C-917F-8209551B1634} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {E2999E4A-9086-401F-898C-AEB0AD38E676} = {9B9E3891-2366-4253-A952-D08BCEB71098} diff --git a/samples/interop/Direct3DInteropSample/App.paml b/samples/interop/Direct3DInteropSample/App.paml deleted file mode 100644 index e6d77dfaf4..0000000000 --- a/samples/interop/Direct3DInteropSample/App.paml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/samples/interop/Direct3DInteropSample/App.paml.cs b/samples/interop/Direct3DInteropSample/App.paml.cs deleted file mode 100644 index 29365decfe..0000000000 --- a/samples/interop/Direct3DInteropSample/App.paml.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Avalonia; -using Avalonia.Controls.ApplicationLifetimes; -using Avalonia.Markup.Xaml; - -namespace Direct3DInteropSample -{ - public class App : Application - { - public override void Initialize() - { - AvaloniaXamlLoader.Load(this); - } - - public override void OnFrameworkInitializationCompleted() - { - if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) - desktop.MainWindow = new MainWindow(); - base.OnFrameworkInitializationCompleted(); - } - } -} diff --git a/samples/interop/Direct3DInteropSample/Direct3DInteropSample.csproj b/samples/interop/Direct3DInteropSample/Direct3DInteropSample.csproj deleted file mode 100644 index f9ef4693d5..0000000000 --- a/samples/interop/Direct3DInteropSample/Direct3DInteropSample.csproj +++ /dev/null @@ -1,32 +0,0 @@ - - - Exe - net461 - - - - - - %(Filename) - - - Designer - - - - - - - - PreserveNewest - - - - - - - - - - - diff --git a/samples/interop/Direct3DInteropSample/MainWindow.cs b/samples/interop/Direct3DInteropSample/MainWindow.cs deleted file mode 100644 index 6cc3cb9116..0000000000 --- a/samples/interop/Direct3DInteropSample/MainWindow.cs +++ /dev/null @@ -1,283 +0,0 @@ -using System; - -using Avalonia; -using Avalonia.Controls; -using Avalonia.Direct2D1; -using Avalonia.Direct2D1.Media; -using Avalonia.Markup.Xaml; -using Avalonia.Platform; -using Avalonia.Rendering; - -using SharpDX; -using SharpDX.D3DCompiler; -using SharpDX.Direct2D1; -using SharpDX.Direct3D; -using SharpDX.Direct3D11; -using SharpDX.DXGI; - -using AlphaMode = SharpDX.Direct2D1.AlphaMode; -using Buffer = SharpDX.Direct3D11.Buffer; -using DeviceContext = SharpDX.Direct2D1.DeviceContext; -using Factory2 = SharpDX.DXGI.Factory2; -using InputElement = SharpDX.Direct3D11.InputElement; -using Matrix = SharpDX.Matrix; -using PixelFormat = SharpDX.Direct2D1.PixelFormat; -using Resource = SharpDX.Direct3D11.Resource; - -namespace Direct3DInteropSample -{ - public class MainWindow : Window - { - Texture2D _backBuffer; - RenderTargetView _renderView; - Texture2D _depthBuffer; - DepthStencilView _depthView; - private readonly SwapChain _swapChain; - private SwapChainDescription1 _desc; - private Matrix _proj = Matrix.Identity; - private readonly Matrix _view; - private Buffer _contantBuffer; - private DeviceContext _deviceContext; - private readonly MainWindowViewModel _model; - - public MainWindow() - { - DataContext = _model = new MainWindowViewModel(); - - _desc = new SwapChainDescription1() - { - BufferCount = 1, - Width = (int)ClientSize.Width, - Height = (int)ClientSize.Height, - Format = Format.R8G8B8A8_UNorm, - SampleDescription = new SampleDescription(1, 0), - SwapEffect = SwapEffect.Discard, - Usage = Usage.RenderTargetOutput - }; - - using (var factory = Direct2D1Platform.DxgiDevice.Adapter.GetParent()) - { - _swapChain = new SwapChain1(factory, Direct2D1Platform.DxgiDevice, PlatformImpl?.Handle.Handle ?? IntPtr.Zero, ref _desc); - } - - _deviceContext = new DeviceContext(Direct2D1Platform.Direct2D1Device, DeviceContextOptions.None) - { - DotsPerInch = new Size2F(96, 96) - }; - - CreateMesh(); - - _view = Matrix.LookAtLH(new Vector3(0, 0, -5), new Vector3(0, 0, 0), Vector3.UnitY); - - this.GetObservable(ClientSizeProperty).Subscribe(Resize); - - Resize(ClientSize); - - AvaloniaXamlLoader.Load(this); - - Background = Avalonia.Media.Brushes.Transparent; - } - - - protected override void HandlePaint(Rect rect) - { - var viewProj = Matrix.Multiply(_view, _proj); - var context = Direct2D1Platform.Direct3D11Device.ImmediateContext; - - // Clear views - context.ClearDepthStencilView(_depthView, DepthStencilClearFlags.Depth, 1.0f, 0); - context.ClearRenderTargetView(_renderView, Color.White); - - // Update WorldViewProj Matrix - var worldViewProj = Matrix.RotationX((float)_model.RotationX) * Matrix.RotationY((float)_model.RotationY) - * Matrix.RotationZ((float)_model.RotationZ) - * Matrix.Scaling((float)_model.Zoom) - * viewProj; - worldViewProj.Transpose(); - context.UpdateSubresource(ref worldViewProj, _contantBuffer); - - // Draw the cube - context.Draw(36, 0); - base.HandlePaint(rect); - - // Present! - _swapChain.Present(0, PresentFlags.None); - } - - private void CreateMesh() - { - var device = Direct2D1Platform.Direct3D11Device; - - // Compile Vertex and Pixel shaders - var vertexShaderByteCode = ShaderBytecode.CompileFromFile("MiniCube.fx", "VS", "vs_4_0"); - var vertexShader = new VertexShader(device, vertexShaderByteCode); - - var pixelShaderByteCode = ShaderBytecode.CompileFromFile("MiniCube.fx", "PS", "ps_4_0"); - var pixelShader = new PixelShader(device, pixelShaderByteCode); - - var signature = ShaderSignature.GetInputSignature(vertexShaderByteCode); - - var inputElements = new[] - { - new InputElement("POSITION", 0, Format.R32G32B32A32_Float, 0, 0), - new InputElement("COLOR", 0, Format.R32G32B32A32_Float, 16, 0) - }; - - // Layout from VertexShader input signature - var layout = new InputLayout( - device, - signature, - inputElements); - - // Instantiate Vertex buffer from vertex data - var vertices = Buffer.Create( - device, - BindFlags.VertexBuffer, - new[] - { - new Vector4(-1.0f, -1.0f, -1.0f, 1.0f), new Vector4(1.0f, 0.0f, 0.0f, 1.0f), // Front - new Vector4(-1.0f, 1.0f, -1.0f, 1.0f), new Vector4(1.0f, 0.0f, 0.0f, 1.0f), - new Vector4( 1.0f, 1.0f, -1.0f, 1.0f), new Vector4(1.0f, 0.0f, 0.0f, 1.0f), - new Vector4(-1.0f, -1.0f, -1.0f, 1.0f), new Vector4(1.0f, 0.0f, 0.0f, 1.0f), - new Vector4( 1.0f, 1.0f, -1.0f, 1.0f), new Vector4(1.0f, 0.0f, 0.0f, 1.0f), - new Vector4( 1.0f, -1.0f, -1.0f, 1.0f), new Vector4(1.0f, 0.0f, 0.0f, 1.0f), - - new Vector4(-1.0f, -1.0f, 1.0f, 1.0f), new Vector4(0.0f, 1.0f, 0.0f, 1.0f), // BACK - new Vector4( 1.0f, 1.0f, 1.0f, 1.0f), new Vector4(0.0f, 1.0f, 0.0f, 1.0f), - new Vector4(-1.0f, 1.0f, 1.0f, 1.0f), new Vector4(0.0f, 1.0f, 0.0f, 1.0f), - new Vector4(-1.0f, -1.0f, 1.0f, 1.0f), new Vector4(0.0f, 1.0f, 0.0f, 1.0f), - new Vector4( 1.0f, -1.0f, 1.0f, 1.0f), new Vector4(0.0f, 1.0f, 0.0f, 1.0f), - new Vector4( 1.0f, 1.0f, 1.0f, 1.0f), new Vector4(0.0f, 1.0f, 0.0f, 1.0f), - - new Vector4(-1.0f, 1.0f, -1.0f, 1.0f), new Vector4(0.0f, 0.0f, 1.0f, 1.0f), // Top - new Vector4(-1.0f, 1.0f, 1.0f, 1.0f), new Vector4(0.0f, 0.0f, 1.0f, 1.0f), - new Vector4( 1.0f, 1.0f, 1.0f, 1.0f), new Vector4(0.0f, 0.0f, 1.0f, 1.0f), - new Vector4(-1.0f, 1.0f, -1.0f, 1.0f), new Vector4(0.0f, 0.0f, 1.0f, 1.0f), - new Vector4( 1.0f, 1.0f, 1.0f, 1.0f), new Vector4(0.0f, 0.0f, 1.0f, 1.0f), - new Vector4( 1.0f, 1.0f, -1.0f, 1.0f), new Vector4(0.0f, 0.0f, 1.0f, 1.0f), - - new Vector4(-1.0f, -1.0f, -1.0f, 1.0f), new Vector4(1.0f, 1.0f, 0.0f, 1.0f), // Bottom - new Vector4( 1.0f, -1.0f, 1.0f, 1.0f), new Vector4(1.0f, 1.0f, 0.0f, 1.0f), - new Vector4(-1.0f, -1.0f, 1.0f, 1.0f), new Vector4(1.0f, 1.0f, 0.0f, 1.0f), - new Vector4(-1.0f, -1.0f, -1.0f, 1.0f), new Vector4(1.0f, 1.0f, 0.0f, 1.0f), - new Vector4( 1.0f, -1.0f, -1.0f, 1.0f), new Vector4(1.0f, 1.0f, 0.0f, 1.0f), - new Vector4( 1.0f, -1.0f, 1.0f, 1.0f), new Vector4(1.0f, 1.0f, 0.0f, 1.0f), - - new Vector4(-1.0f, -1.0f, -1.0f, 1.0f), new Vector4(1.0f, 0.0f, 1.0f, 1.0f), // Left - new Vector4(-1.0f, -1.0f, 1.0f, 1.0f), new Vector4(1.0f, 0.0f, 1.0f, 1.0f), - new Vector4(-1.0f, 1.0f, 1.0f, 1.0f), new Vector4(1.0f, 0.0f, 1.0f, 1.0f), - new Vector4(-1.0f, -1.0f, -1.0f, 1.0f), new Vector4(1.0f, 0.0f, 1.0f, 1.0f), - new Vector4(-1.0f, 1.0f, 1.0f, 1.0f), new Vector4(1.0f, 0.0f, 1.0f, 1.0f), - new Vector4(-1.0f, 1.0f, -1.0f, 1.0f), new Vector4(1.0f, 0.0f, 1.0f, 1.0f), - - new Vector4( 1.0f, -1.0f, -1.0f, 1.0f), new Vector4(0.0f, 1.0f, 1.0f, 1.0f), // Right - new Vector4( 1.0f, 1.0f, 1.0f, 1.0f), new Vector4(0.0f, 1.0f, 1.0f, 1.0f), - new Vector4( 1.0f, -1.0f, 1.0f, 1.0f), new Vector4(0.0f, 1.0f, 1.0f, 1.0f), - new Vector4( 1.0f, -1.0f, -1.0f, 1.0f), new Vector4(0.0f, 1.0f, 1.0f, 1.0f), - new Vector4( 1.0f, 1.0f, -1.0f, 1.0f), new Vector4(0.0f, 1.0f, 1.0f, 1.0f), - new Vector4( 1.0f, 1.0f, 1.0f, 1.0f), new Vector4(0.0f, 1.0f, 1.0f, 1.0f), - }); - - // Create Constant Buffer - _contantBuffer = new Buffer(device, Utilities.SizeOf(), ResourceUsage.Default, BindFlags.ConstantBuffer, CpuAccessFlags.None, ResourceOptionFlags.None, 0); - - var context = Direct2D1Platform.Direct3D11Device.ImmediateContext; - - // Prepare All the stages - context.InputAssembler.InputLayout = layout; - context.InputAssembler.PrimitiveTopology = PrimitiveTopology.TriangleList; - context.InputAssembler.SetVertexBuffers(0, new VertexBufferBinding(vertices, Utilities.SizeOf() * 2, 0)); - context.VertexShader.SetConstantBuffer(0, _contantBuffer); - context.VertexShader.Set(vertexShader); - context.PixelShader.Set(pixelShader); - } - - private void Resize(Size size) - { - Utilities.Dispose(ref _deviceContext); - Utilities.Dispose(ref _backBuffer); - Utilities.Dispose(ref _renderView); - Utilities.Dispose(ref _depthBuffer); - Utilities.Dispose(ref _depthView); - var context = Direct2D1Platform.Direct3D11Device.ImmediateContext; - - // Resize the backbuffer - _swapChain.ResizeBuffers(0, 0, 0, Format.Unknown, SwapChainFlags.None); - - // Get the backbuffer from the swapchain - _backBuffer = Resource.FromSwapChain(_swapChain, 0); - - // Renderview on the backbuffer - _renderView = new RenderTargetView(Direct2D1Platform.Direct3D11Device, _backBuffer); - - // Create the depth buffer - _depthBuffer = new Texture2D( - Direct2D1Platform.Direct3D11Device, - new Texture2DDescription() - { - Format = Format.D32_Float_S8X24_UInt, - ArraySize = 1, - MipLevels = 1, - Width = (int)size.Width, - Height = (int)size.Height, - SampleDescription = new SampleDescription(1, 0), - Usage = ResourceUsage.Default, - BindFlags = BindFlags.DepthStencil, - CpuAccessFlags = CpuAccessFlags.None, - OptionFlags = ResourceOptionFlags.None - }); - - // Create the depth buffer view - _depthView = new DepthStencilView(Direct2D1Platform.Direct3D11Device, _depthBuffer); - - // Setup targets and viewport for rendering - context.Rasterizer.SetViewport(new Viewport(0, 0, (int)size.Width, (int)size.Height, 0.0f, 1.0f)); - context.OutputMerger.SetTargets(_depthView, _renderView); - - // Setup new projection matrix with correct aspect ratio - _proj = Matrix.PerspectiveFovLH((float)Math.PI / 4.0f, (float)(size.Width / size.Height), 0.1f, 100.0f); - - using (var dxgiBackBuffer = _swapChain.GetBackBuffer(0)) - { - var renderTarget = new SharpDX.Direct2D1.RenderTarget( - Direct2D1Platform.Direct2D1Factory, - dxgiBackBuffer, - new RenderTargetProperties - { - DpiX = 96, - DpiY = 96, - Type = RenderTargetType.Default, - PixelFormat = new PixelFormat( - Format.Unknown, - AlphaMode.Premultiplied) - }); - - _deviceContext = renderTarget.QueryInterface(); - - renderTarget.Dispose(); - } - } - - private class D3DRenderTarget : IRenderTarget - { - private readonly MainWindow _window; - - public D3DRenderTarget(MainWindow window) - { - _window = window; - } - - public void Dispose() - { - } - - public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) - { - return new DrawingContextImpl(visualBrushRenderer, null, _window._deviceContext); - } - } - - - protected override IRenderTarget CreateRenderTarget() => new D3DRenderTarget(this); - } -} diff --git a/samples/interop/Direct3DInteropSample/MainWindow.paml b/samples/interop/Direct3DInteropSample/MainWindow.paml deleted file mode 100644 index 37c6265836..0000000000 --- a/samples/interop/Direct3DInteropSample/MainWindow.paml +++ /dev/null @@ -1,14 +0,0 @@ - - - - Rotation X - - Rotation Y - - Rotation Z - - Zoom - - - - \ No newline at end of file diff --git a/samples/interop/Direct3DInteropSample/MainWindowViewModel.cs b/samples/interop/Direct3DInteropSample/MainWindowViewModel.cs deleted file mode 100644 index 21679a99c5..0000000000 --- a/samples/interop/Direct3DInteropSample/MainWindowViewModel.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using MiniMvvm; - -namespace Direct3DInteropSample -{ - public class MainWindowViewModel : ViewModelBase - { - private double _rotationX; - - public double RotationX - { - get { return _rotationX; } - set { this.RaiseAndSetIfChanged(ref _rotationX, value); } - } - - private double _rotationY = 1; - - public double RotationY - { - get { return _rotationY; } - set { this.RaiseAndSetIfChanged(ref _rotationY, value); } - } - - private double _rotationZ = 2; - - public double RotationZ - { - get { return _rotationZ; } - set { this.RaiseAndSetIfChanged(ref _rotationZ, value); } - } - - - private double _zoom = 1; - - public double Zoom - { - get { return _zoom; } - set { this.RaiseAndSetIfChanged(ref _zoom, value); } - } - } -} diff --git a/samples/interop/Direct3DInteropSample/MiniCube.fx b/samples/interop/Direct3DInteropSample/MiniCube.fx deleted file mode 100644 index f246421f2d..0000000000 --- a/samples/interop/Direct3DInteropSample/MiniCube.fx +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright (c) 2010-2013 SharpDX - Alexandre Mutel -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. -struct VS_IN -{ - float4 pos : POSITION; - float4 col : COLOR; -}; - -struct PS_IN -{ - float4 pos : SV_POSITION; - float4 col : COLOR; -}; - -float4x4 worldViewProj; - -PS_IN VS( VS_IN input ) -{ - PS_IN output = (PS_IN)0; - - output.pos = mul(input.pos, worldViewProj); - output.col = input.col; - - return output; -} - -float4 PS( PS_IN input ) : SV_Target -{ - return input.col; -} \ No newline at end of file diff --git a/samples/interop/Direct3DInteropSample/Program.cs b/samples/interop/Direct3DInteropSample/Program.cs deleted file mode 100644 index bf8e76d7e4..0000000000 --- a/samples/interop/Direct3DInteropSample/Program.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Avalonia; - -namespace Direct3DInteropSample -{ - class Program - { - public static AppBuilder BuildAvaloniaApp() - => AppBuilder.Configure() - .With(new Win32PlatformOptions { UseDeferredRendering = false }) - .UseWin32() - .UseDirect2D1(); - - public static int Main(string[] args) - => BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); - } -} From 949c2b94b9755fce76527e40d04f5b72947a6dca Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Mon, 12 Dec 2022 15:17:18 +0100 Subject: [PATCH 69/87] feat: Enable Rule CA1823 --- .editorconfig | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.editorconfig b/.editorconfig index 1583d3e469..7995062f9f 100644 --- a/.editorconfig +++ b/.editorconfig @@ -152,6 +152,8 @@ dotnet_diagnostic.CA1820.severity = warning dotnet_diagnostic.CA1821.severity = warning # CA1822: Mark members as static dotnet_diagnostic.CA1822.severity = suggestion +# CA1823: Avoid unused private fields +dotnet_diagnostic.CA1823.severity = warning dotnet_code_quality.CA1822.api_surface = private, internal # CA1825: Avoid zero-length array allocations dotnet_diagnostic.CA1825.severity = warning From ade6f3f1cee1041eadd430329ff4ef39e7f2838f Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Mon, 12 Dec 2022 15:47:12 +0100 Subject: [PATCH 70/87] feat(Android): Address Rule CA1823 --- .../Platform/SkiaPlatform/AndroidFramebuffer.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/AndroidFramebuffer.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/AndroidFramebuffer.cs index aabf8160f8..94e5f4bd01 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/AndroidFramebuffer.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/AndroidFramebuffer.cs @@ -96,12 +96,14 @@ namespace Avalonia.Android.Platform.SkiaPlatform public IntPtr bits; // Do not touch. +#pragma warning disable CA1823 // Avoid unused private fields uint reserved1; uint reserved2; uint reserved3; uint reserved4; uint reserved5; uint reserved6; +#pragma warning restore CA1823 // Avoid unused private fields } } } From 0d4ddca7fdd0d9b47e38e0b649af7b908186859d Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Mon, 12 Dec 2022 15:47:30 +0100 Subject: [PATCH 71/87] feat(Base): Address Rule CA1823 --- src/Avalonia.Base/Data/Core/ExpressionNode.cs | 2 -- src/Avalonia.Base/Data/Core/ExpressionObserver.cs | 2 -- src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs | 1 - .../Media/TextFormatting/Unicode/CodepointEnumerator.cs | 1 - 4 files changed, 6 deletions(-) diff --git a/src/Avalonia.Base/Data/Core/ExpressionNode.cs b/src/Avalonia.Base/Data/Core/ExpressionNode.cs index e4b833176c..5fb2bb5c13 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionNode.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionNode.cs @@ -4,8 +4,6 @@ namespace Avalonia.Data.Core { public abstract class ExpressionNode { - private static readonly object CacheInvalid = new object(); - protected static readonly WeakReference UnsetReference = new WeakReference(AvaloniaProperty.UnsetValue); diff --git a/src/Avalonia.Base/Data/Core/ExpressionObserver.cs b/src/Avalonia.Base/Data/Core/ExpressionObserver.cs index 0a9f834aeb..2151c100e5 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionObserver.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionObserver.cs @@ -49,8 +49,6 @@ namespace Avalonia.Data.Core new TaskStreamPlugin(), new ObservableStreamPlugin(), }; - - private static readonly object UninitializedValue = new object(); private readonly ExpressionNode _node; private object? _root; private Func? _rootGetter; diff --git a/src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs b/src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs index e745a873a2..4472ba87eb 100644 --- a/src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs +++ b/src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs @@ -8,7 +8,6 @@ namespace Avalonia.Media.TextFormatting internal readonly struct FormattedTextSource : ITextSource { private readonly CharacterBufferRange _text; - private readonly int length; private readonly TextRunProperties _defaultProperties; private readonly IReadOnlyList>? _textModifier; diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/CodepointEnumerator.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/CodepointEnumerator.cs index 330ead476a..a2c36d9a13 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/CodepointEnumerator.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/CodepointEnumerator.cs @@ -5,7 +5,6 @@ namespace Avalonia.Media.TextFormatting.Unicode public ref struct CodepointEnumerator { private CharacterBufferRange _text; - private int _pos; public CodepointEnumerator(CharacterBufferRange text) { From d13290550494c9ea96d369609b1407814ff65747 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Mon, 12 Dec 2022 15:47:58 +0100 Subject: [PATCH 72/87] feat(Controls): Address Rule CA1823 --- src/Avalonia.Controls/Presenters/TextPresenter.cs | 1 - src/Avalonia.Controls/Primitives/SelectingItemsControl.cs | 2 -- src/Avalonia.Controls/Window.cs | 6 +----- 3 files changed, 1 insertion(+), 8 deletions(-) diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index cc1fa6c513..798caf0b15 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -106,7 +106,6 @@ namespace Avalonia.Controls.Presenters private Rect _caretBounds; private Point _navigationPosition; private string? _preeditText; - private CharacterHit _compositionStartHit = new CharacterHit(-1); static TextPresenter() { diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 44fa78ac21..795bc05e23 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -119,8 +119,6 @@ namespace Avalonia.Controls.Primitives /// public static readonly StyledProperty WrapSelectionProperty = AvaloniaProperty.Register(nameof(WrapSelection), defaultValue: false); - - private static readonly IList Empty = Array.Empty(); private string _textSearchTerm = string.Empty; private DispatcherTimer? _textSearchTimer; private ISelectionModel? _selection; diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index a893c74324..b0911403cc 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -169,10 +169,6 @@ namespace Avalonia.Controls /// public static readonly RoutedEvent WindowOpenedEvent = RoutedEvent.Register("WindowOpened", RoutingStrategies.Direct); - - - - private readonly NameScope _nameScope = new NameScope(); private object? _dialogResult; private readonly Size _maxPlatformClientSize; private WindowStartupLocation _windowStartupLocation; @@ -235,7 +231,7 @@ namespace Avalonia.Controls impl.GotInputWhenDisabled = OnGotInputWhenDisabled; impl.WindowStateChanged = HandleWindowStateChanged; _maxPlatformClientSize = PlatformImpl?.MaxAutoSizeHint ?? default(Size); - impl.ExtendClientAreaToDecorationsChanged = ExtendClientAreaToDecorationsChanged; + impl.ExtendClientAreaToDecorationsChanged = ExtendClientAreaToDecorationsChanged; this.GetObservable(ClientSizeProperty).Skip(1).Subscribe(x => PlatformImpl?.Resize(x, PlatformResizeReason.Application)); PlatformImpl?.ShowTaskbarIcon(ShowInTaskbar); From 2ce1e636e016b34d7cbc041923c01cbdf68f908b Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Mon, 12 Dec 2022 15:48:16 +0100 Subject: [PATCH 73/87] feat(Diagnostic): Address Rule CA1823 --- src/Avalonia.Diagnostics/Diagnostics/Controls/Application.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/Controls/Application.cs b/src/Avalonia.Diagnostics/Diagnostics/Controls/Application.cs index 42c5ea1fa9..00173dbb35 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Controls/Application.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Controls/Application.cs @@ -10,8 +10,7 @@ namespace Avalonia.Diagnostics.Controls { private readonly App _application; - private static readonly Version s_version = typeof(AvaloniaObject).Assembly?.GetName()?.Version - ?? Version.Parse("0.0.00"); + public event EventHandler? Closed; public Application(App application) From 779a31f6efc878760f2cdab0587bd4d4a5cd5399 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Mon, 12 Dec 2022 15:48:35 +0100 Subject: [PATCH 74/87] feat(Native): Address Rule CA1823 --- src/Avalonia.Native/IAvnMenu.cs | 1 - src/Avalonia.Native/WindowImplBase.cs | 1 - 2 files changed, 2 deletions(-) diff --git a/src/Avalonia.Native/IAvnMenu.cs b/src/Avalonia.Native/IAvnMenu.cs index 3e46e0c5c6..e6a5a371d0 100644 --- a/src/Avalonia.Native/IAvnMenu.cs +++ b/src/Avalonia.Native/IAvnMenu.cs @@ -46,7 +46,6 @@ namespace Avalonia.Native.Interop.Impl private AvaloniaNativeMenuExporter _exporter; private List<__MicroComIAvnMenuItemProxy> _menuItems = new List<__MicroComIAvnMenuItemProxy>(); private Dictionary _menuItemLookup = new Dictionary(); - private CompositeDisposable _propertyDisposables = new CompositeDisposable(); public void RaiseNeedsUpdate() { diff --git a/src/Avalonia.Native/WindowImplBase.cs b/src/Avalonia.Native/WindowImplBase.cs index 5d66590a2b..ca57e30b1c 100644 --- a/src/Avalonia.Native/WindowImplBase.cs +++ b/src/Avalonia.Native/WindowImplBase.cs @@ -63,7 +63,6 @@ namespace Avalonia.Native private double _savedScaling; private GlPlatformSurface _glSurface; private NativeControlHostImpl _nativeControlHost; - private IGlContext _glContext; internal WindowBaseImpl(IAvaloniaNativeFactory factory, AvaloniaNativePlatformOptions opts, AvaloniaNativeGlPlatformGraphics glFeature) From 868a22d2f326575006f188f6f58662b7828d6419 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Mon, 12 Dec 2022 15:49:03 +0100 Subject: [PATCH 75/87] feat(Linux): Address Rule CA1823 --- src/Avalonia.X11/X11Clipboard.cs | 2 -- src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevBackend.cs | 2 -- .../Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs | 1 - 3 files changed, 5 deletions(-) diff --git a/src/Avalonia.X11/X11Clipboard.cs b/src/Avalonia.X11/X11Clipboard.cs index d428d82744..2aa7797067 100644 --- a/src/Avalonia.X11/X11Clipboard.cs +++ b/src/Avalonia.X11/X11Clipboard.cs @@ -18,8 +18,6 @@ namespace Avalonia.X11 private TaskCompletionSource _requestedDataTcs; private readonly IntPtr[] _textAtoms; private readonly IntPtr _avaloniaSaveTargetsAtom; - private readonly Dictionary _formatAtoms = new Dictionary(); - private readonly Dictionary _atomFormats = new Dictionary(); public X11Clipboard(AvaloniaX11Platform platform) { diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevBackend.cs b/src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevBackend.cs index 1e3c4bed48..686050e7c2 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevBackend.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevBackend.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Threading; using Avalonia.Input; using Avalonia.Input.Raw; -using Avalonia.Threading; using static Avalonia.LinuxFramebuffer.NativeUnsafeMethods; namespace Avalonia.LinuxFramebuffer.Input.EvDev @@ -13,7 +12,6 @@ namespace Avalonia.LinuxFramebuffer.Input.EvDev private readonly EvDevDeviceDescription[] _deviceDescriptions; private readonly List _handlers = new List(); private int _epoll; - private object _lock = new object(); private Action _onInput; private IInputRoot _inputRoot; private RawEventGroupingThreadingHelper _inputQueue; diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs b/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs index 77e8202fac..bff9ddc55c 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs @@ -11,7 +11,6 @@ namespace Avalonia.LinuxFramebuffer.Input.LibInput { private IScreenInfoProvider _screen; private IInputRoot _inputRoot; - private readonly Queue _inputThreadActions = new Queue(); private TouchDevice _touch = new TouchDevice(); private const string LibInput = nameof(Avalonia.LinuxFramebuffer) + "/" + nameof(Avalonia.LinuxFramebuffer.Input) + "/" + nameof(LibInput); private readonly RawEventGroupingThreadingHelper _inputQueue; From 4fe1977c2a93b0c8c2b21b837316a09813cf6549 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Mon, 12 Dec 2022 15:49:17 +0100 Subject: [PATCH 76/87] feat(Windows): Address Rule CA1823 --- .../Avalonia.Win32/Interop/Automation/UiaCoreTypesApi.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Windows/Avalonia.Win32/Interop/Automation/UiaCoreTypesApi.cs b/src/Windows/Avalonia.Win32/Interop/Automation/UiaCoreTypesApi.cs index 966f996a91..08b3ee32fa 100644 --- a/src/Windows/Avalonia.Win32/Interop/Automation/UiaCoreTypesApi.cs +++ b/src/Windows/Avalonia.Win32/Interop/Automation/UiaCoreTypesApi.cs @@ -6,8 +6,6 @@ namespace Avalonia.Win32.Interop.Automation { internal static class UiaCoreTypesApi { - private const string StartListeningExportName = "SynchronizedInputPattern_StartListening"; - internal enum AutomationIdType { Property, From 64f84711edfda96185cde08597b63a0faa9e3cae Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Mon, 12 Dec 2022 18:49:17 +0100 Subject: [PATCH 77/87] fix: Address review --- src/Avalonia.Controls.DataGrid/DataGrid.cs | 1 - src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs | 1 - src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs | 2 -- src/Avalonia.Native/ClipboardImpl.cs | 3 +-- src/Browser/Avalonia.Browser/BrowserNativeControlHost.cs | 6 ------ src/Windows/Avalonia.Win32/WindowImpl.cs | 2 -- 6 files changed, 1 insertion(+), 14 deletions(-) diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs index 454678c64b..fe3445a30a 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs @@ -48,7 +48,6 @@ namespace Avalonia.Controls private const string DATAGRID_elementColumnHeadersPresenterName = "PART_ColumnHeadersPresenter"; private const string DATAGRID_elementFrozenColumnScrollBarSpacerName = "PART_FrozenColumnScrollBarSpacer"; private const string DATAGRID_elementHorizontalScrollbarName = "PART_HorizontalScrollbar"; - private const string DATAGRID_elementRowHeadersPresenterName = "PART_RowHeadersPresenter"; private const string DATAGRID_elementTopLeftCornerHeaderName = "PART_TopLeftCornerHeader"; private const string DATAGRID_elementTopRightCornerHeaderName = "PART_TopRightCornerHeader"; private const string DATAGRID_elementBottomRightCornerHeaderName = "PART_BottomRightCorner"; diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs b/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs index 6473c9c051..252868847a 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs @@ -34,7 +34,6 @@ namespace Avalonia.Controls } private const int DATAGRIDCOLUMNHEADER_resizeRegionWidth = 5; - private const double DATAGRIDCOLUMNHEADER_separatorThickness = 1; private const int DATAGRIDCOLUMNHEADER_columnsDragTreshold = 5; private bool _areHandlersSuspended; diff --git a/src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs b/src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs index fe3ba0abf6..c42d21d126 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs @@ -18,8 +18,6 @@ namespace Avalonia.Controls.Primitives public class DataGridRowHeader : ContentControl { private const string DATAGRIDROWHEADER_elementRootName = "PART_Root"; - private const double DATAGRIDROWHEADER_separatorThickness = 1; - private Control _rootElement; public static readonly StyledProperty SeparatorBrushProperty = diff --git a/src/Avalonia.Native/ClipboardImpl.cs b/src/Avalonia.Native/ClipboardImpl.cs index 4ee590516b..9f1c8883aa 100644 --- a/src/Avalonia.Native/ClipboardImpl.cs +++ b/src/Avalonia.Native/ClipboardImpl.cs @@ -15,8 +15,7 @@ namespace Avalonia.Native private IAvnClipboard _native; private const string NSPasteboardTypeString = "public.utf8-plain-text"; private const string NSFilenamesPboardType = "NSFilenamesPboardType"; - private const string NSPasteboardTypeFileUrl = "public.file-url"; - + public ClipboardImpl(IAvnClipboard native) { _native = native; diff --git a/src/Browser/Avalonia.Browser/BrowserNativeControlHost.cs b/src/Browser/Avalonia.Browser/BrowserNativeControlHost.cs index 7e91d29019..c2e54c7ed7 100644 --- a/src/Browser/Avalonia.Browser/BrowserNativeControlHost.cs +++ b/src/Browser/Avalonia.Browser/BrowserNativeControlHost.cs @@ -55,12 +55,6 @@ namespace Avalonia.Browser private class Attachment : INativeControlHostControlTopLevelAttachment { - private const string InitializeWithChildHandleSymbol = "InitializeWithChildHandle"; - private const string AttachToSymbol = "AttachTo"; - private const string ShowInBoundsSymbol = "ShowInBounds"; - private const string HideWithSizeSymbol = "HideWithSize"; - private const string ReleaseChildSymbol = "ReleaseChild"; - private JSObject? _native; private BrowserNativeControlHost? _attachedTo; diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 596b60a8cb..9e11959101 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -1343,8 +1343,6 @@ namespace Avalonia.Win32 } private const int MF_BYCOMMAND = 0x0; - private const int MF_BYPOSITION = 0x400; - private const int MF_REMOVE = 0x1000; private const int MF_ENABLED = 0x0; private const int MF_GRAYED = 0x1; private const int MF_DISABLED = 0x2; From c96dcc68d800325d74ca734ba33fec31842d2292 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Mon, 12 Dec 2022 18:35:25 +0100 Subject: [PATCH 78/87] fix: XML Comment --- src/Avalonia.Base/Platform/ITextShaperImpl.cs | 1 + src/Avalonia.Base/Rendering/DeferredRenderer.cs | 2 ++ src/Avalonia.Base/Rendering/ImmediateRenderer.cs | 2 ++ src/Skia/Avalonia.Skia/Helpers/ImageSavingHelper.cs | 1 + 4 files changed, 6 insertions(+) diff --git a/src/Avalonia.Base/Platform/ITextShaperImpl.cs b/src/Avalonia.Base/Platform/ITextShaperImpl.cs index ff91097eda..5ac8ba7768 100644 --- a/src/Avalonia.Base/Platform/ITextShaperImpl.cs +++ b/src/Avalonia.Base/Platform/ITextShaperImpl.cs @@ -13,6 +13,7 @@ namespace Avalonia.Platform /// Shapes the specified region within the text and returns a shaped buffer. /// /// The text buffer. + /// length of text /// Text shaper options to customize the shaping process. /// A shaped glyph run. ShapedBuffer ShapeText(CharacterBufferReference text, int length, TextShaperOptions options); diff --git a/src/Avalonia.Base/Rendering/DeferredRenderer.cs b/src/Avalonia.Base/Rendering/DeferredRenderer.cs index 05aa1d1ea4..6a3a92fd09 100644 --- a/src/Avalonia.Base/Rendering/DeferredRenderer.cs +++ b/src/Avalonia.Base/Rendering/DeferredRenderer.cs @@ -49,6 +49,8 @@ namespace Avalonia.Rendering /// /// The control to render. /// The render loop. + /// The target render factory. + /// /// The scene builder to use. Optional. /// The dispatcher to use. Optional. /// Lock object used before trying to access render target diff --git a/src/Avalonia.Base/Rendering/ImmediateRenderer.cs b/src/Avalonia.Base/Rendering/ImmediateRenderer.cs index 1c797a5348..80e4daaecc 100644 --- a/src/Avalonia.Base/Rendering/ImmediateRenderer.cs +++ b/src/Avalonia.Base/Rendering/ImmediateRenderer.cs @@ -30,6 +30,8 @@ namespace Avalonia.Rendering /// Initializes a new instance of the class. /// /// The control to render. + /// The target render factory. + /// The render contex. public ImmediateRenderer(Visual root, Func renderTargetFactory, PlatformRenderInterfaceContextManager? renderContext = null) { diff --git a/src/Skia/Avalonia.Skia/Helpers/ImageSavingHelper.cs b/src/Skia/Avalonia.Skia/Helpers/ImageSavingHelper.cs index b4dd754822..ebbb7a96dd 100644 --- a/src/Skia/Avalonia.Skia/Helpers/ImageSavingHelper.cs +++ b/src/Skia/Avalonia.Skia/Helpers/ImageSavingHelper.cs @@ -34,6 +34,7 @@ namespace Avalonia.Skia.Helpers /// Save Skia image to a stream. /// /// Image to save + /// The where write image. /// /// The optional quality for PNG compression. /// The quality value is interpreted from 0 - 100. If quality is null From 6d21b5b18efa8f26d9c5c70c8f063b3a09e3e475 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Mon, 12 Dec 2022 22:36:03 +0100 Subject: [PATCH 79/87] fix: Address review --- src/Avalonia.Base/Platform/ITextShaperImpl.cs | 2 +- src/Avalonia.Base/Rendering/DeferredRenderer.cs | 2 +- src/Skia/Avalonia.Skia/Helpers/ImageSavingHelper.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Base/Platform/ITextShaperImpl.cs b/src/Avalonia.Base/Platform/ITextShaperImpl.cs index 5ac8ba7768..c3eb89ab1a 100644 --- a/src/Avalonia.Base/Platform/ITextShaperImpl.cs +++ b/src/Avalonia.Base/Platform/ITextShaperImpl.cs @@ -13,7 +13,7 @@ namespace Avalonia.Platform /// Shapes the specified region within the text and returns a shaped buffer. /// /// The text buffer. - /// length of text + /// The length of text. /// Text shaper options to customize the shaping process. /// A shaped glyph run. ShapedBuffer ShapeText(CharacterBufferReference text, int length, TextShaperOptions options); diff --git a/src/Avalonia.Base/Rendering/DeferredRenderer.cs b/src/Avalonia.Base/Rendering/DeferredRenderer.cs index 6a3a92fd09..3110b64c81 100644 --- a/src/Avalonia.Base/Rendering/DeferredRenderer.cs +++ b/src/Avalonia.Base/Rendering/DeferredRenderer.cs @@ -50,7 +50,7 @@ namespace Avalonia.Rendering /// The control to render. /// The render loop. /// The target render factory. - /// + /// The Platform Render Context. /// The scene builder to use. Optional. /// The dispatcher to use. Optional. /// Lock object used before trying to access render target diff --git a/src/Skia/Avalonia.Skia/Helpers/ImageSavingHelper.cs b/src/Skia/Avalonia.Skia/Helpers/ImageSavingHelper.cs index ebbb7a96dd..4cb1430a3b 100644 --- a/src/Skia/Avalonia.Skia/Helpers/ImageSavingHelper.cs +++ b/src/Skia/Avalonia.Skia/Helpers/ImageSavingHelper.cs @@ -34,7 +34,7 @@ namespace Avalonia.Skia.Helpers /// Save Skia image to a stream. /// /// Image to save - /// The where write image. + /// The output stream to save the image. /// /// The optional quality for PNG compression. /// The quality value is interpreted from 0 - 100. If quality is null From 1e5c0b2911d5bace24b347897d93b60cc342f364 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Tue, 13 Dec 2022 10:36:48 +0100 Subject: [PATCH 80/87] feat: Linked AvaloniaResource Items --- .../GenerateAvaloniaResourcesTask.cs | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs b/src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs index fad6ad397b..65681a2d28 100644 --- a/src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs +++ b/src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs @@ -7,7 +7,7 @@ using System.Text; using Avalonia.Markup.Xaml.PortableXaml; using Avalonia.Utilities; using Microsoft.Build.Framework; -using SPath=System.IO.Path; +using SPath = System.IO.Path; namespace Avalonia.Build.Tasks { public class GenerateAvaloniaResourcesTask : ITask @@ -30,12 +30,17 @@ namespace Avalonia.Build.Tasks private byte[] _data; private string _sourcePath; - public Source(string relativePath, string root) + public Source(ITaskItem avaloniaResourceItem, string root) { root = SPath.GetFullPath(root); - Path = "/" + relativePath.Replace('\\', '/'); + var relativePath = avaloniaResourceItem.ItemSpec; _sourcePath = SPath.Combine(root, relativePath); Size = (int)new FileInfo(_sourcePath).Length; + var link = avaloniaResourceItem.GetMetadata("Link"); + var path = !string.IsNullOrEmpty(link) + ? link + : relativePath; + Path = "/" + path.Replace('\\', '/'); } public string SystemPath => _sourcePath ?? Path; @@ -65,8 +70,7 @@ namespace Avalonia.Build.Tasks List BuildResourceSources() => Resources.Select(r => { - - var src = new Source(r.ItemSpec, Root); + var src = new Source(r, Root); BuildEngine.LogMessage(FormattableString.Invariant($"avares -> name:{src.Path}, path: {src.SystemPath}, size:{src.Size}, ItemSpec:{r.ItemSpec}"), _reportImportance); return src; }).ToList(); @@ -93,15 +97,15 @@ namespace Avalonia.Build.Tasks ms.CopyTo(output); foreach (var s in sources) { - using(var input = s.Open()) + using (var input = s.Open()) input.CopyTo(output); } } private bool PreProcessXamlFiles(List sources) { - var typeToXamlIndex = new Dictionary(); - + var typeToXamlIndex = new Dictionary(); + foreach (var s in sources.ToArray()) { if (s.Path.ToLowerInvariant().EndsWith(".xaml") || s.Path.ToLowerInvariant().EndsWith(".paml") || s.Path.ToLowerInvariant().EndsWith(".axaml")) @@ -111,7 +115,7 @@ namespace Avalonia.Build.Tasks { info = XamlFileInfo.Parse(s.ReadAsString()); } - catch(Exception e) + catch (Exception e) { BuildEngine.LogError(BuildEngineErrorCode.InvalidXAML, s.SystemPath, "File doesn't contain valid XAML: " + e); return false; @@ -121,7 +125,7 @@ namespace Avalonia.Build.Tasks { if (typeToXamlIndex.ContainsKey(info.XClass)) { - + BuildEngine.LogError(BuildEngineErrorCode.DuplicateXClass, s.SystemPath, $"Duplicate x:Class directive, {info.XClass} is already used in {typeToXamlIndex[info.XClass]}"); return false; From 58b404a3ab25a8a7e6df0300f6356ce7f9e01dc6 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 13 Dec 2022 18:25:37 +0100 Subject: [PATCH 81/87] Added failing test for #7974. --- .../Selection/SelectionModelTests_Single.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Single.cs b/tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Single.cs index 668af3b5d7..1b629be695 100644 --- a/tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Single.cs +++ b/tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Single.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Specialized; +using System.Linq; using Avalonia.Collections; using Avalonia.Controls.Selection; using Avalonia.Controls.Utils; @@ -1144,6 +1145,24 @@ namespace Avalonia.Controls.UnitTests.Selection Assert.Equal(new[] { "foo" }, target.SelectedItems); Assert.Equal(0, target.AnchorIndex); } + + [Fact] + public void SelectedItems_Indexer_Is_Correct() + { + // Issue #7974 + var target = CreateTarget(); + var raised = 0; + + target.SelectionChanged += (s, e) => + { + Assert.Equal("bar", e.SelectedItems.First()); + Assert.Equal("bar", e.SelectedItems[0]); + ++raised; + }; + + target.Select(1); + Assert.Equal(1, raised); + } } public class BatchUpdate From 467303099cc5ebcb8e091c508d888d8c66e690c8 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 13 Dec 2022 18:32:52 +0100 Subject: [PATCH 82/87] Fix SelectedItems indexer. The index is an index into `Ranges`, not `Items`. Fixes #7974. --- src/Avalonia.Controls/Selection/SelectedItems.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/Selection/SelectedItems.cs b/src/Avalonia.Controls/Selection/SelectedItems.cs index 4fbcfde438..ef642b7bdc 100644 --- a/src/Avalonia.Controls/Selection/SelectedItems.cs +++ b/src/Avalonia.Controls/Selection/SelectedItems.cs @@ -35,9 +35,9 @@ namespace Avalonia.Controls.Selection { return _owner.SelectedItem; } - else if (Items is object) + else if (Items is not null && Ranges is not null) { - return Items[index]; + return Items[IndexRange.GetAt(Ranges, index)]; } else { From 7deed3555674f46b1ef1bb2cc4e75043b3c0c383 Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Wed, 14 Dec 2022 10:56:02 +0000 Subject: [PATCH 83/87] add refresh button to manager file chooser --- .../Controls/ManagedFileChooser.xaml | 7 +++++-- .../Controls/ManagedFileChooser.xaml | 14 ++++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/ManagedFileChooser.xaml b/src/Avalonia.Themes.Fluent/Controls/ManagedFileChooser.xaml index d1707d0af2..63b3d46a41 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ManagedFileChooser.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ManagedFileChooser.xaml @@ -153,9 +153,13 @@ - + @@ -322,7 +326,6 @@