From 5bdbd930d980999b249f3a7334b16fbd58b6e22e Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Wed, 16 Nov 2022 16:56:11 +0000 Subject: [PATCH 01/56] 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/56] 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/56] 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/56] 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/56] 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 f4804551316b4141367f3ef3fa38c50d9a702fc0 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 29 Nov 2022 12:39:24 +0100 Subject: [PATCH 07/56] Added failing test for #9565. And another (passing) test for checking that window order is correct when a child window is shown in fullscreen. --- samples/IntegrationTestApp/MainWindow.axaml | 7 +- .../IntegrationTestApp/MainWindow.axaml.cs | 3 + .../WindowTests_MacOS.cs | 74 +++++++++++++++++++ 3 files changed, 83 insertions(+), 1 deletion(-) diff --git a/samples/IntegrationTestApp/MainWindow.axaml b/samples/IntegrationTestApp/MainWindow.axaml index 038ced4e5c..801ff765c8 100644 --- a/samples/IntegrationTestApp/MainWindow.axaml +++ b/samples/IntegrationTestApp/MainWindow.axaml @@ -17,11 +17,15 @@ - + + + WindowState: + + @@ -136,6 +140,7 @@ + diff --git a/samples/IntegrationTestApp/MainWindow.axaml.cs b/samples/IntegrationTestApp/MainWindow.axaml.cs index c1acc7ca88..841947673a 100644 --- a/samples/IntegrationTestApp/MainWindow.axaml.cs +++ b/samples/IntegrationTestApp/MainWindow.axaml.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using Avalonia; @@ -178,6 +179,8 @@ namespace IntegrationTestApp ShowWindow(); if (source?.Name == "SendToBack") SendToBack(); + if (source?.Name == "EnterFullscreen") + WindowState = WindowState.FullScreen; if (source?.Name == "ExitFullscreen") WindowState = WindowState.Normal; if (source?.Name == "RestoreAll") diff --git a/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs b/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs index 05ed0616a8..8f4417a451 100644 --- a/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs +++ b/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using Avalonia.Controls; +using Avalonia.Utilities; using OpenQA.Selenium; using OpenQA.Selenium.Appium; using OpenQA.Selenium.Interactions; @@ -114,6 +115,79 @@ namespace Avalonia.IntegrationTests.Appium Assert.Equal(1, secondaryWindowIndex); } } + + [PlatformFact(TestPlatforms.MacOS)] + public void WindowOrder_Owned_Dialog_Stays_InFront_Of_FullScreen_Parent() + { + var mainWindow = _session.FindElementByAccessibilityId("MainWindow"); + + // Enter fullscreen + mainWindow.FindElementByAccessibilityId("EnterFullscreen").Click(); + + // Wait for fullscreen transition. + Thread.Sleep(1000); + + // Make sure we entered fullscreen. + var windowState = mainWindow.FindElementByAccessibilityId("WindowState"); + Assert.Equal("FullScreen", windowState.Text); + + // Open child window. + using (OpenWindow(new PixelSize(200, 100), ShowWindowMode.Owned, WindowStartupLocation.Manual)) + { + mainWindow.SendClick(); + var secondaryWindowIndex = GetWindowOrder("SecondaryWindow"); + Assert.Equal(1, secondaryWindowIndex); + } + + // Exit fullscreen by menu shortcut Command+R + mainWindow.FindElementByAccessibilityId("ExitFullscreen").Click(); + + // Wait for restore transition. + Thread.Sleep(1000); + + // Make sure we exited fullscreen. + mainWindow = _session.FindElementByAccessibilityId("MainWindow"); + windowState = mainWindow.FindElementByAccessibilityId("WindowState"); + Assert.Equal("Normal", windowState.Text); + } + + [PlatformFact(TestPlatforms.MacOS)] + public void Does_Not_Switch_Space_From_FullScreen_To_Main_Desktop_When_FullScreen_Window_Clicked() + { + // Issue #9565 + var mainWindow = _session.FindElementByAccessibilityId("MainWindow"); + AppiumWebElement windowState; + + // Open child window. + using (OpenWindow(new PixelSize(200, 100), ShowWindowMode.Owned, WindowStartupLocation.Manual)) + { + // Enter fullscreen + mainWindow.FindElementByAccessibilityId("EnterFullscreen").Click(); + + // Wait for fullscreen transition. + Thread.Sleep(1000); + + // Make sure we entered fullscreen. + mainWindow = _session.FindElementByAccessibilityId("MainWindow"); + windowState = mainWindow.FindElementByAccessibilityId("WindowState"); + Assert.Equal("FullScreen", windowState.Text); + + // Click on main window + mainWindow.Click(); + + // Failed here due to #9565: main window is no longer visible as the main space is now shown instead + // of the fullscreen space. + mainWindow.FindElementByAccessibilityId("ExitFullscreen").Click(); + + // Wait for restore transition. + Thread.Sleep(1000); + } + + // Make sure we exited fullscreen. + mainWindow = _session.FindElementByAccessibilityId("MainWindow"); + windowState = mainWindow.FindElementByAccessibilityId("WindowState"); + Assert.Equal("Normal", windowState.Text); + } [PlatformFact(TestPlatforms.MacOS)] public void WindowOrder_NonOwned_Window_Does_Not_Stay_InFront_Of_Parent() From d00f19d55671373ea0329e35e4a52afb3815364b Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 29 Nov 2022 12:40:02 +0100 Subject: [PATCH 08/56] Fix #9565 - Only bring window to front if it's on the currently active space - Ensure correct order of child windows after fullscreen transition --- native/Avalonia.Native/src/OSX/WindowImpl.mm | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.mm b/native/Avalonia.Native/src/OSX/WindowImpl.mm index 2443965957..b6dacb6ce4 100644 --- a/native/Avalonia.Native/src/OSX/WindowImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowImpl.mm @@ -137,7 +137,11 @@ void WindowImpl::BringToFront() for(auto iterator = _children.begin(); iterator != _children.end(); iterator++) { - (*iterator)->BringToFront(); + auto window = (*iterator)->Window; + + // #9565: Only bring window to front if it's on the currently active space + if ([window isOnActiveSpace]) + (*iterator)->BringToFront(); } } } @@ -161,6 +165,9 @@ void WindowImpl::StartStateTransition() { void WindowImpl::EndStateTransition() { _transitioningWindowState = false; + + // Ensure correct order of child windows after fullscreen transition. + BringToFront(); } SystemDecorations WindowImpl::Decorations() { From fdf76c5765ceb8f2c4191aa1257c2eb6cea3bccd Mon Sep 17 00:00:00 2001 From: daniel mayost Date: Wed, 30 Nov 2022 14:50:27 +0200 Subject: [PATCH 09/56] 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 10/56] 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 11/56] 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 12/56] 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 1af7eb6f3a93b5ed3b109abd9041409b041ebd9e Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 1 Dec 2022 17:17:07 +0000 Subject: [PATCH 13/56] 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 14/56] 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 915aff2eeace37ad57b16493c348159e64a2f715 Mon Sep 17 00:00:00 2001 From: zhouzj Date: Mon, 5 Dec 2022 14:34:33 +0800 Subject: [PATCH 15/56] Prevent showing trayMenu if the menu is empty. --- src/Windows/Avalonia.Win32/TrayIconImpl.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Windows/Avalonia.Win32/TrayIconImpl.cs b/src/Windows/Avalonia.Win32/TrayIconImpl.cs index 8d565d7fef..ddc42b3248 100644 --- a/src/Windows/Avalonia.Win32/TrayIconImpl.cs +++ b/src/Windows/Avalonia.Win32/TrayIconImpl.cs @@ -19,7 +19,7 @@ namespace Avalonia.Win32 public class TrayIconImpl : ITrayIconImpl { private static readonly IntPtr s_emptyIcon = new System.Drawing.Bitmap(32, 32).GetHicon(); - + private readonly int _uniqueId; private static int s_nextUniqueId; private bool _iconAdded; @@ -137,6 +137,12 @@ namespace Avalonia.Win32 private void OnRightClicked() { + var menuItems = _exporter.GetMenu(); + if (null == menuItems || menuItems.Count == 0) + { + return; + } + var _trayMenu = new TrayPopupRoot() { SystemDecorations = SystemDecorations.None, @@ -145,7 +151,7 @@ namespace Avalonia.Win32 TransparencyLevelHint = WindowTransparencyLevel.Transparent, Content = new TrayIconMenuFlyoutPresenter() { - Items = _exporter.GetMenu() + Items = menuItems } }; From 19043beda148892ee5d60d5786c9c811c0170309 Mon Sep 17 00:00:00 2001 From: zhouzj Date: Mon, 5 Dec 2022 14:40:10 +0800 Subject: [PATCH 16/56] fix typo --- src/Windows/Avalonia.Win32/TrayIconImpl.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Windows/Avalonia.Win32/TrayIconImpl.cs b/src/Windows/Avalonia.Win32/TrayIconImpl.cs index ddc42b3248..ab70d77a09 100644 --- a/src/Windows/Avalonia.Win32/TrayIconImpl.cs +++ b/src/Windows/Avalonia.Win32/TrayIconImpl.cs @@ -19,7 +19,6 @@ namespace Avalonia.Win32 public class TrayIconImpl : ITrayIconImpl { private static readonly IntPtr s_emptyIcon = new System.Drawing.Bitmap(32, 32).GetHicon(); - private readonly int _uniqueId; private static int s_nextUniqueId; private bool _iconAdded; From 4fae1b6e7abf8ab1ccd7090110e8cc2055d4edb4 Mon Sep 17 00:00:00 2001 From: daniel mayost Date: Mon, 5 Dec 2022 10:15:05 +0200 Subject: [PATCH 17/56] 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 18/56] 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 19/56] 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 20/56] 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 58b5eeb9d21a074cd588bc3b58026a9ca87b0575 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 5 Dec 2022 14:38:12 +0100 Subject: [PATCH 21/56] Added failing tests for #9561. --- .../Styling/StyledElementTests_Theming.cs | 51 +++++++++++++++---- 1 file changed, 42 insertions(+), 9 deletions(-) diff --git a/tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs b/tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs index 672945cb24..b5a9b35134 100644 --- a/tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs +++ b/tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs @@ -359,16 +359,49 @@ public class StyledElementTests_Theming } [Fact] - public void Implicit_Theme_Is_Cleared_When_Removed_From_Logical_Tree() + public void Implicit_Theme_Is_Not_Detached_When_Removed_From_Logical_Tree() { var target = CreateTarget(); var root = CreateRoot(target); - - Assert.NotNull(target.GetEffectiveTheme()); + + Assert.Equal("theme", target.Tag); root.Child = null; - Assert.Null(target.GetEffectiveTheme()); + var border = Assert.IsType(target.VisualChild); + Assert.Equal("theme", target.Tag); + Assert.Equal("theme", border.Tag); + } + + [Fact] + public void Can_Attach_Then_Reattach_To_Same_Logical_Tree() + { + var target = CreateTarget(); + var root = CreateRoot(target); + + Assert.Equal("theme", target.Tag); + + root.Child = null; + root.Child = target; + + Assert.Equal("theme", target.Tag); + } + + [Fact] + public void Implicit_Theme_Is_Reevaluated_When_Removed_And_Added_To_Different_Logical_Tree() + { + var target = CreateTarget(); + var root1 = CreateRoot(target, "theme1"); + var root2 = CreateRoot(null, "theme2"); + + Assert.Equal("theme1", target.Tag); + + root1.Child = null; + root2.Child = target; + + var border = Assert.IsType(target.VisualChild); + Assert.Equal("theme2", target.Tag); + Assert.Equal("theme2", border.Tag); } [Fact] @@ -402,10 +435,10 @@ public class StyledElementTests_Theming private static ThemedControl CreateTarget() => new ThemedControl(); - private static TestRoot CreateRoot(Control child) + private static TestRoot CreateRoot(Control? child, string themeTag = "theme") { var result = new TestRoot(); - result.Resources.Add(typeof(ThemedControl), CreateTheme()); + result.Resources.Add(typeof(ThemedControl), CreateTheme(themeTag)); result.Child = child; result.LayoutManager.ExecuteInitialLayoutPass(); return result; @@ -530,7 +563,7 @@ public class StyledElementTests_Theming } } - private static ControlTheme CreateTheme() + private static ControlTheme CreateTheme(string tag = "theme") { var template = new FuncControlTemplate((o, n) => new Border()); @@ -539,7 +572,7 @@ public class StyledElementTests_Theming TargetType = typeof(ThemedControl), Setters = { - new Setter(Control.TagProperty, "theme"), + new Setter(Control.TagProperty, tag), new Setter(TemplatedControl.TemplateProperty, template), new Setter(TemplatedControl.CornerRadiusProperty, new CornerRadius(5)), }, @@ -550,7 +583,7 @@ public class StyledElementTests_Theming Setters = { new Setter(Border.BackgroundProperty, Brushes.Red), - new Setter(Control.TagProperty, "theme"), + new Setter(Control.TagProperty, tag), } }, new Style(x => x.Nesting().Class("foo").Template().OfType()) From b382a9d6ac1a4c237d891585f4f94e1d47d5abc9 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 5 Dec 2022 14:43:39 +0100 Subject: [PATCH 22/56] Re-evaluate theme on tree detach + reattach. Fixes #9561 --- src/Avalonia.Base/StyledElement.cs | 67 ++++++++++++++++++++++++------ 1 file changed, 54 insertions(+), 13 deletions(-) diff --git a/src/Avalonia.Base/StyledElement.cs b/src/Avalonia.Base/StyledElement.cs index 6fa2027e1e..6043175eee 100644 --- a/src/Avalonia.Base/StyledElement.cs +++ b/src/Avalonia.Base/StyledElement.cs @@ -81,6 +81,7 @@ namespace Avalonia private Styles? _styles; private bool _stylesApplied; private bool _themeApplied; + private bool _templatedParentThemeApplied; private AvaloniaObject? _templatedParent; private bool _dataContextUpdating; private ControlTheme? _implicitTheme; @@ -375,6 +376,12 @@ namespace Avalonia _themeApplied = true; } + if (!_templatedParentThemeApplied) + { + ApplyTemplatedParentControlTheme(); + _templatedParentThemeApplied = true; + } + if (!_stylesApplied) { ApplyStyles(this); @@ -613,26 +620,38 @@ namespace Avalonia base.OnPropertyChanged(change); if (change.Property == ThemeProperty) - { OnControlThemeChanged(); - _themeApplied = false; - } } private protected virtual void OnControlThemeChanged() { var values = GetValueStore(); values.BeginStyling(); - try { values.RemoveFrames(FrameType.Theme); } - finally { values.EndStyling(); } + + try + { + values.RemoveFrames(FrameType.Theme); + } + finally + { + values.EndStyling(); + _themeApplied = false; + } } internal virtual void OnTemplatedParentControlThemeChanged() { var values = GetValueStore(); values.BeginStyling(); - try { values.RemoveFrames(FrameType.TemplatedParentTheme); } - finally { values.EndStyling(); } + try + { + values.RemoveFrames(FrameType.TemplatedParentTheme); + } + finally + { + values.EndStyling(); + _templatedParentThemeApplied = false; + } } internal ControlTheme? GetEffectiveTheme() @@ -743,13 +762,13 @@ namespace Avalonia private void ApplyControlTheme() { - var theme = GetEffectiveTheme(); - - if (theme is not null) + if (GetEffectiveTheme() is { } theme) ApplyControlTheme(theme, FrameType.Theme); + } - if (TemplatedParent is StyledElement styleableParent && - styleableParent.GetEffectiveTheme() is { } parentTheme) + private void ApplyTemplatedParentControlTheme() + { + if ((TemplatedParent as StyledElement)?.GetEffectiveTheme() is { } parentTheme) { ApplyControlTheme(parentTheme, FrameType.TemplatedParentTheme); } @@ -793,6 +812,28 @@ namespace Avalonia ApplyStyle(child, host, type); } + private void ReevaluateImplicitTheme() + { + // We only need to check if the theme has changed when Theme isn't set (i.e. when we + // have an implicit theme). + if (Theme is not null) + return; + + // Refetch the implicit theme. + var oldImplicitTheme = _implicitTheme == s_invalidTheme ? null : _implicitTheme; + _implicitTheme = null; + GetEffectiveTheme(); + + var newImplicitTheme = _implicitTheme == s_invalidTheme ? null : _implicitTheme; + + // If the implicit theme has changed, detach the existing theme. + if (newImplicitTheme != oldImplicitTheme) + { + OnControlThemeChanged(); + _themeApplied = false; + } + } + private void OnAttachedToLogicalTreeCore(LogicalTreeAttachmentEventArgs e) { if (this.GetLogicalParent() == null && !(this is ILogicalRoot)) @@ -811,6 +852,7 @@ namespace Avalonia { _logicalRoot = e.Root; + ReevaluateImplicitTheme(); ApplyStyling(); NotifyResourcesChanged(propagate: false); @@ -835,7 +877,6 @@ namespace Avalonia if (_logicalRoot != null) { _logicalRoot = null; - _implicitTheme = null; InvalidateStyles(recurse: false); OnDetachedFromLogicalTree(e); DetachedFromLogicalTree?.Invoke(this, e); From 387728fb2d0ab7019e22cd506851e3624c7ae469 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 5 Dec 2022 16:17:08 +0100 Subject: [PATCH 23/56] Added failing test for animations without activator. Mentioned in #9561. --- .../Styling/StyleTests.cs | 48 ++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/tests/Avalonia.Base.UnitTests/Styling/StyleTests.cs b/tests/Avalonia.Base.UnitTests/Styling/StyleTests.cs index 805b3e7aa6..4f5d1b8ec8 100644 --- a/tests/Avalonia.Base.UnitTests/Styling/StyleTests.cs +++ b/tests/Avalonia.Base.UnitTests/Styling/StyleTests.cs @@ -868,7 +868,53 @@ namespace Avalonia.Base.UnitTests.Styling } [Fact] - public void Animations_Should_Be_Activated_And_Deactivated() + public void Animations_Should_Be_Activated() + { + Style style = new Style(x => x.OfType()) + { + Animations = + { + new Avalonia.Animation.Animation + { + Duration = TimeSpan.FromSeconds(1), + Children = + { + new KeyFrame + { + Setters = + { + new Setter { Property = Class1.DoubleProperty, Value = 5.0 } + }, + }, + new KeyFrame + { + Setters = + { + new Setter { Property = Class1.DoubleProperty, Value = 10.0 } + }, + Cue = new Cue(1d) + } + }, + } + } + }; + + var clock = new TestClock(); + var target = new Class1 { Clock = clock }; + + StyleHelpers.TryAttach(style, target); + + Assert.Equal(0.0, target.Double); + + clock.Step(TimeSpan.Zero); + Assert.Equal(5.0, target.Double); + + clock.Step(TimeSpan.FromSeconds(0.5)); + Assert.Equal(7.5, target.Double); + } + + [Fact] + public void Animations_With_Trigger_Should_Be_Activated_And_Deactivated() { Style style = new Style(x => x.OfType().Class("foo")) { From a3e79d3127672748064a53b08963d98dd0665c4a Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 5 Dec 2022 16:18:10 +0100 Subject: [PATCH 24/56] Start animations in styles without activators. Fixes issue mentioned in #9561. --- src/Avalonia.Base/Styling/StyleInstance.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Avalonia.Base/Styling/StyleInstance.cs b/src/Avalonia.Base/Styling/StyleInstance.cs index 4985aa16c7..ca602167c0 100644 --- a/src/Avalonia.Base/Styling/StyleInstance.cs +++ b/src/Avalonia.Base/Styling/StyleInstance.cs @@ -70,6 +70,9 @@ namespace Avalonia.Styling _animationTrigger ??= new Subject(); foreach (var animation in _animations) animation.Apply(animatable, null, _animationTrigger); + + if (_activator is null) + _animationTrigger.OnNext(true); } } From 80e1e1eb4cfb544a89e12c07ae56a994bfb37490 Mon Sep 17 00:00:00 2001 From: Daniil Pavliuchyk Date: Mon, 5 Dec 2022 18:53:24 +0200 Subject: [PATCH 25/56] 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 a691e7c8fae35c34f9fdfed272f857a6f228e8af Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Mon, 5 Dec 2022 17:26:27 +0000 Subject: [PATCH 26/56] don't set key event to handled when selection changes with space or enter --- src/Avalonia.Controls/Primitives/SelectingItemsControl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index f8951b3fa3..44fa78ac21 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -588,7 +588,7 @@ namespace Avalonia.Controls.Primitives } else if (e.Key == Key.Space || e.Key == Key.Enter) { - e.Handled = UpdateSelectionFromEventSource( + UpdateSelectionFromEventSource( e.Source, true, e.KeyModifiers.HasFlag(KeyModifiers.Shift), 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 27/56] 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 28/56] 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 845fea9d1103eb0487335d55ff0488fe66d22c5f Mon Sep 17 00:00:00 2001 From: Kevin Ivarsen Date: Mon, 5 Dec 2022 14:36:33 -0800 Subject: [PATCH 29/56] Swap the figures used for expanded/collapsed groups in DataGrid to match the standard convention. --- src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml b/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml index a3c0ed1d0c..fb4335f4de 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 From bdceb1c3dff626173fbfdd4d24d5723b1977c37d Mon Sep 17 00:00:00 2001 From: Max Katz Date: Mon, 5 Dec 2022 23:02:18 -0500 Subject: [PATCH 30/56] Do not require matching ctor, if top level element --- .../AvaloniaXamlIlRuntimeCompiler.cs | 2 +- .../Avalonia.Markup.Xaml.Loader/xamlil.github | 2 +- .../Xaml/BasicTests.cs | 23 +++++++++++++++++++ 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/AvaloniaXamlIlRuntimeCompiler.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/AvaloniaXamlIlRuntimeCompiler.cs index 28d5a55f86..2bb91e8a32 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/AvaloniaXamlIlRuntimeCompiler.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/AvaloniaXamlIlRuntimeCompiler.cs @@ -234,7 +234,7 @@ namespace Avalonia.Markup.Xaml.XamlIl parsedDocuments.Add(new XamlDocumentResource(parsed, document.BaseUri?.ToString(), null, null, builder, compiler.DefinePopulateMethod(builder, parsed, AvaloniaXamlIlCompiler.PopulateName, true), - compiler.DefineBuildMethod(builder, parsed, AvaloniaXamlIlCompiler.BuildName, true))); + document.RootInstance is null ? compiler.DefineBuildMethod(builder, parsed, AvaloniaXamlIlCompiler.BuildName, true) : null)); originalDocuments.Add(document); } diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github b/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github index 491de981dd..b800d3e1fa 160000 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github @@ -1 +1 @@ -Subproject commit 491de981dd4433ee58bc9540e2cd4a5d168f8168 +Subproject commit b800d3e1fac0940c571bfa540e3af4dbd59f7dbb diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs index c7afaee697..18a6dd9803 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs @@ -899,6 +899,17 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml Assert.Equal("Foo", target.Text); } + [Fact] + public void Should_Parse_And_Populate_Type_Without_Public_Ctor() + { + var xaml = @""; + var target = (ObjectWithoutPublicCtor)AvaloniaRuntimeXamlLoader.Load(xaml, rootInstance: new ObjectWithoutPublicCtor("Hello")); + + Assert.NotNull(target); + Assert.Equal("World", target.Test2); + Assert.Equal("Hello", target.Test1); + } + private class SelectedItemsViewModel : INotifyPropertyChanged { public string[] Items { get; set; } @@ -928,6 +939,18 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml Child = child; } } + + public class ObjectWithoutPublicCtor + { + public ObjectWithoutPublicCtor(string param) + { + Test1 = param; + } + + public string Test1 { get; set; } + + public string Test2 { get; set; } + } public class ObjectWithAddChildOfT : IAddChild, IAddChild { From eaf2ce38a4780123d4d06931068926353f31824b Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Tue, 6 Dec 2022 12:15:16 +0600 Subject: [PATCH 31/56] 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 003923b50d635eadd18b979c98d9ec753630e5c6 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 6 Dec 2022 11:42:37 +0100 Subject: [PATCH 32/56] Fix integration tests on Windows. We need to be explicit about which element we're talking about when there are two elements with the same content, and so use accessibility IDs rather than Name. --- samples/IntegrationTestApp/MainWindow.axaml | 10 +++++----- samples/IntegrationTestApp/ShowWindowTest.axaml | 8 ++++---- .../WindowTests.cs | 16 ++++++++-------- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/samples/IntegrationTestApp/MainWindow.axaml b/samples/IntegrationTestApp/MainWindow.axaml index 801ff765c8..54c0cb0655 100644 --- a/samples/IntegrationTestApp/MainWindow.axaml +++ b/samples/IntegrationTestApp/MainWindow.axaml @@ -24,7 +24,7 @@ WindowState: - + @@ -133,10 +133,10 @@ CenterOwner - Normal - Minimized - Maximized - FullScreen + Normal + Minimized + Maximized + FullScreen diff --git a/samples/IntegrationTestApp/ShowWindowTest.axaml b/samples/IntegrationTestApp/ShowWindowTest.axaml index c3a0d8d2e2..00987429d0 100644 --- a/samples/IntegrationTestApp/ShowWindowTest.axaml +++ b/samples/IntegrationTestApp/ShowWindowTest.axaml @@ -27,10 +27,10 @@ - Normal - Minimized - Maximized - FullScreen + Normal + Minimized + Maximized + FullScreen diff --git a/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs b/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs index b6ac75b78e..4d833cdb1f 100644 --- a/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs @@ -90,7 +90,7 @@ namespace Avalonia.IntegrationTests.Appium try { _session.FindElementByAccessibilityId("WindowState").SendClick(); - _session.FindElementByName("Normal").SendClick(); + _session.FindElementByAccessibilityId("WindowStateNormal").SendClick(); // Wait for animations to run. if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) @@ -155,11 +155,11 @@ namespace Avalonia.IntegrationTests.Appium Assert.Equal("Normal", windowState.GetComboBoxValue()); windowState.Click(); - _session.FindElementByName("Maximized").SendClick(); + _session.FindElementByAccessibilityId("WindowStateMaximized").SendClick(); Assert.Equal("Maximized", windowState.GetComboBoxValue()); windowState.Click(); - _session.FindElementByName("Normal").SendClick(); + _session.FindElementByAccessibilityId("WindowStateNormal").SendClick(); var current = GetWindowInfo(); Assert.Equal(original.Position, current.Position); @@ -169,7 +169,7 @@ namespace Avalonia.IntegrationTests.Appium if (!RuntimeInformation.IsOSPlatform(OSPlatform.OSX) || mode == ShowWindowMode.NonOwned) { windowState.Click(); - _session.FindElementByName("FullScreen").SendClick(); + _session.FindElementByAccessibilityId("WindowStateFullScreen").SendClick(); Assert.Equal("FullScreen", windowState.GetComboBoxValue()); current = GetWindowInfo(); @@ -179,7 +179,7 @@ namespace Avalonia.IntegrationTests.Appium windowState.SendClick(); - _session.FindElementByName("Normal").SendClick(); + _session.FindElementByAccessibilityId("WindowStateNormal").SendClick(); current = GetWindowInfo(); Assert.Equal(original.Position, current.Position); @@ -223,13 +223,13 @@ namespace Avalonia.IntegrationTests.Appium // Not sure how to handle testing minimized windows currently. if (state == Controls.WindowState.Minimized) continue; - + // Child/Modal windows cannot be fullscreen on macOS. if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) && state == Controls.WindowState.FullScreen && mode != ShowWindowMode.NonOwned) continue; - + data.Add(size, mode, state); } } @@ -286,7 +286,7 @@ namespace Avalonia.IntegrationTests.Appium _session.FindElementByName(location.ToString()).SendClick(); stateComboBox.Click(); - _session.FindElementByName(state.ToString()).SendClick(); + _session.FindElementByAccessibilityId($"ShowWindowState{state}").SendClick(); return showButton.OpenWindowWithClick(); } From 23e45beb906fff790715a530ee1d2663601dbff3 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 6 Dec 2022 12:00:25 +0100 Subject: [PATCH 33/56] Fix integration tests on macOS. --- .../Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs b/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs index 8f4417a451..6c61a85561 100644 --- a/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs +++ b/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs @@ -128,7 +128,7 @@ namespace Avalonia.IntegrationTests.Appium Thread.Sleep(1000); // Make sure we entered fullscreen. - var windowState = mainWindow.FindElementByAccessibilityId("WindowState"); + var windowState = mainWindow.FindElementByAccessibilityId("MainWindowState"); Assert.Equal("FullScreen", windowState.Text); // Open child window. @@ -147,7 +147,7 @@ namespace Avalonia.IntegrationTests.Appium // Make sure we exited fullscreen. mainWindow = _session.FindElementByAccessibilityId("MainWindow"); - windowState = mainWindow.FindElementByAccessibilityId("WindowState"); + windowState = mainWindow.FindElementByAccessibilityId("MainWindowState"); Assert.Equal("Normal", windowState.Text); } @@ -169,7 +169,7 @@ namespace Avalonia.IntegrationTests.Appium // Make sure we entered fullscreen. mainWindow = _session.FindElementByAccessibilityId("MainWindow"); - windowState = mainWindow.FindElementByAccessibilityId("WindowState"); + windowState = mainWindow.FindElementByAccessibilityId("MainWindowState"); Assert.Equal("FullScreen", windowState.Text); // Click on main window @@ -185,7 +185,7 @@ namespace Avalonia.IntegrationTests.Appium // Make sure we exited fullscreen. mainWindow = _session.FindElementByAccessibilityId("MainWindow"); - windowState = mainWindow.FindElementByAccessibilityId("WindowState"); + windowState = mainWindow.FindElementByAccessibilityId("MainWindowState"); Assert.Equal("Normal", windowState.Text); } From 5ab8af49575edaf02d32c31c4eee264a4d0348cc Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Tue, 6 Dec 2022 17:51:18 +0300 Subject: [PATCH 34/56] 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 ba1c4dcc1b4b468c32859233a9898cbcfa525b55 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Tue, 6 Dec 2022 07:19:55 -0500 Subject: [PATCH 35/56] Switch to upstream xamlx --- src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github b/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github index b800d3e1fa..5e498f8bcc 160000 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github @@ -1 +1 @@ -Subproject commit b800d3e1fac0940c571bfa540e3af4dbd59f7dbb +Subproject commit 5e498f8bcca403a34aff5efc825cbb4e12b7fa8e From 4502ed3729663199ec173f79cd1373e12855d28f Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 6 Dec 2022 16:29:43 +0100 Subject: [PATCH 36/56] 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 37/56] 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 38/56] 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 39/56] 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 40/56] 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 41/56] 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 42/56] 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 43/56] 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 19ed6ec05f8d472e06efcc8bf1d99f07bff40566 Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Wed, 7 Dec 2022 10:03:16 +0000 Subject: [PATCH 44/56] 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 45/56] 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 46/56] 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 47/56] 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 48/56] 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 49/56] 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 50/56] 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 51/56] 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 52/56] 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 53/56] 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 54/56] 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 55/56] 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 56/56] 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"> +