diff --git a/native/Avalonia.Native/src/OSX/AvnWindow.mm b/native/Avalonia.Native/src/OSX/AvnWindow.mm index 23abf1d53f..b1fb915e04 100644 --- a/native/Avalonia.Native/src/OSX/AvnWindow.mm +++ b/native/Avalonia.Native/src/OSX/AvnWindow.mm @@ -238,7 +238,7 @@ -(BOOL)canBecomeKeyWindow { - if(_canBecomeKeyWindow) + if(_canBecomeKeyWindow && !_closed) { // If the window has a child window being shown as a dialog then don't allow it to become the key window. auto parent = dynamic_cast(_parent.getRaw()); @@ -292,12 +292,14 @@ { if (_parent == nullptr) return; - + _parent->BringToFront(); dispatch_async(dispatch_get_main_queue(), ^{ @try { - [self invalidateShadow]; + [self invalidateShadow]; + if (self->_parent != nullptr) + self->_parent->BringToFront(); } @finally{ } diff --git a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs index e24860e3e1..e5f29abb68 100644 --- a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs +++ b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs @@ -306,25 +306,8 @@ namespace ControlCatalog.Pages resultText += @$" Content: "; -#if NET6_0_OR_GREATER - await using var stream = await file.OpenReadAsync(); -#else - using var stream = await file.OpenReadAsync(); -#endif - using var reader = new System.IO.StreamReader(stream); - // 4GB file test, shouldn't load more than 10000 chars into a memory. - const int length = 10000; - var buffer = ArrayPool.Shared.Rent(length); - try - { - var charsRead = await reader.ReadAsync(buffer, 0, length); - resultText += new string(buffer, 0, charsRead); - } - finally - { - ArrayPool.Shared.Return(buffer); - } + resultText += await ReadTextFromFile(file, 10000); } openedFileContent.Text = resultText; @@ -354,6 +337,28 @@ namespace ControlCatalog.Pages } } + public static async Task ReadTextFromFile(IStorageFile file, int length) + { +#if NET6_0_OR_GREATER + await using var stream = await file.OpenReadAsync(); +#else + using var stream = await file.OpenReadAsync(); +#endif + using var reader = new System.IO.StreamReader(stream); + + // 4GB file test, shouldn't load more than 10000 chars into a memory. + var buffer = ArrayPool.Shared.Rent(length); + try + { + var charsRead = await reader.ReadAsync(buffer, 0, length); + return new string(buffer, 0, charsRead); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { base.OnAttachedToVisualTree(e); diff --git a/samples/ControlCatalog/Pages/DragAndDropPage.xaml b/samples/ControlCatalog/Pages/DragAndDropPage.xaml index 3f8a023060..390fa32b9c 100644 --- a/samples/ControlCatalog/Pages/DragAndDropPage.xaml +++ b/samples/ControlCatalog/Pages/DragAndDropPage.xaml @@ -25,7 +25,6 @@ BorderThickness="2"> Drag Me (custom) - + + diff --git a/samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs b/samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs index e384db88b3..26430b4b61 100644 --- a/samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs +++ b/samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs @@ -1,27 +1,29 @@ using System; +using System.IO; using System.Linq; using System.Reflection; using Avalonia.Controls; using Avalonia.Input; using Avalonia.Markup.Xaml; +using Avalonia.Platform.Storage; namespace ControlCatalog.Pages { public class DragAndDropPage : UserControl { - TextBlock _DropState; + private readonly TextBlock _dropState; private const string CustomFormat = "application/xxx-avalonia-controlcatalog-custom"; public DragAndDropPage() { this.InitializeComponent(); - _DropState = this.Get("DropState"); + _dropState = this.Get("DropState"); int textCount = 0; SetupDnd("Text", d => d.Set(DataFormats.Text, $"Text was dragged {++textCount} times"), DragDropEffects.Copy | DragDropEffects.Move | DragDropEffects.Link); SetupDnd("Custom", d => d.Set(CustomFormat, "Test123"), DragDropEffects.Move); - SetupDnd("Files", d => d.Set(DataFormats.FileNames, new[] { Assembly.GetEntryAssembly()?.GetModules().FirstOrDefault()?.FullyQualifiedName }), DragDropEffects.Copy); + SetupDnd("Files", d => d.Set(DataFormats.Files, new[] { Assembly.GetEntryAssembly()?.GetModules().FirstOrDefault()?.FullyQualifiedName }), DragDropEffects.Copy); } void SetupDnd(string suffix, Action factory, DragDropEffects effects) @@ -68,12 +70,12 @@ namespace ControlCatalog.Pages // Only allow if the dragged data contains text or filenames. if (!e.Data.Contains(DataFormats.Text) - && !e.Data.Contains(DataFormats.FileNames) + && !e.Data.Contains(DataFormats.Files) && !e.Data.Contains(CustomFormat)) e.DragEffects = DragDropEffects.None; } - void Drop(object? sender, DragEventArgs e) + async void Drop(object? sender, DragEventArgs e) { if (e.Source is Control c && c.Name == "MoveTarget") { @@ -85,11 +87,41 @@ namespace ControlCatalog.Pages } if (e.Data.Contains(DataFormats.Text)) - _DropState.Text = e.Data.GetText(); + { + _dropState.Text = e.Data.GetText(); + } + else if (e.Data.Contains(DataFormats.Files)) + { + var files = e.Data.GetFiles() ?? Array.Empty(); + var contentStr = ""; + + foreach (var item in files) + { + if (item is IStorageFile file) + { + var content = await DialogsPage.ReadTextFromFile(file, 1000); + contentStr += $"File {item.Name}:{Environment.NewLine}{content}{Environment.NewLine}{Environment.NewLine}"; + } + else if (item is IStorageFolder folder) + { + var items = await folder.GetItemsAsync(); + contentStr += $"Folder {item.Name}: items {items.Count}{Environment.NewLine}{Environment.NewLine}"; + } + } + + _dropState.Text = contentStr; + } +#pragma warning disable CS0618 // Type or member is obsolete else if (e.Data.Contains(DataFormats.FileNames)) - _DropState.Text = string.Join(Environment.NewLine, e.Data.GetFileNames() ?? Array.Empty()); + { + var files = e.Data.GetFileNames(); + _dropState.Text = string.Join(Environment.NewLine, files ?? Array.Empty()); + } +#pragma warning restore CS0618 // Type or member is obsolete else if (e.Data.Contains(CustomFormat)) - _DropState.Text = "Custom: " + e.Data.Get(CustomFormat); + { + _dropState.Text = "Custom: " + e.Data.Get(CustomFormat); + } } dragMe.PointerPressed += DoDrag; diff --git a/samples/ControlCatalog/Pages/ListBoxPage.xaml b/samples/ControlCatalog/Pages/ListBoxPage.xaml index dc0eaf0a51..b1b1b99c9c 100644 --- a/samples/ControlCatalog/Pages/ListBoxPage.xaml +++ b/samples/ControlCatalog/Pages/ListBoxPage.xaml @@ -10,7 +10,7 @@ diff --git a/samples/RenderDemo/Pages/CustomSkiaPage.cs b/samples/RenderDemo/Pages/CustomSkiaPage.cs index bf27747154..4a3e20ff5b 100644 --- a/samples/RenderDemo/Pages/CustomSkiaPage.cs +++ b/samples/RenderDemo/Pages/CustomSkiaPage.cs @@ -1,6 +1,7 @@ using System; using System.Diagnostics; using System.Globalization; +using System.Linq; using Avalonia; using Avalonia.Controls; using Avalonia.Media; @@ -8,22 +9,27 @@ using Avalonia.Platform; using Avalonia.Rendering.SceneGraph; using Avalonia.Skia; using Avalonia.Threading; +using Avalonia.Utilities; using SkiaSharp; namespace RenderDemo.Pages { public class CustomSkiaPage : Control { + private readonly GlyphRun _noSkia; public CustomSkiaPage() { ClipToBounds = true; + var text = "Current rendering API is not Skia"; + var glyphs = text.Select(ch => Typeface.Default.GlyphTypeface.GetGlyph(ch)).ToArray(); + _noSkia = new GlyphRun(Typeface.Default.GlyphTypeface, 12, text.AsMemory(), glyphs); } class CustomDrawOp : ICustomDrawOperation { - private readonly FormattedText _noSkia; + private readonly GlyphRun _noSkia; - public CustomDrawOp(Rect bounds, FormattedText noSkia) + public CustomDrawOp(Rect bounds, GlyphRun noSkia) { _noSkia = noSkia; Bounds = bounds; @@ -42,10 +48,7 @@ namespace RenderDemo.Pages { var leaseFeature = context.GetFeature(); if (leaseFeature == null) - using (var c = new DrawingContext(context, false)) - { - c.DrawText(_noSkia, new Point()); - } + context.DrawGlyphRun(Brushes.Black, _noSkia.PlatformImpl); else { using var lease = leaseFeature.Lease(); @@ -114,10 +117,7 @@ namespace RenderDemo.Pages public override void Render(DrawingContext context) { - var noSkia = new FormattedText("Current rendering API is not Skia", CultureInfo.CurrentCulture, - FlowDirection.LeftToRight, Typeface.Default, 12, Brushes.Black); - - context.Custom(new CustomDrawOp(new Rect(0, 0, Bounds.Width, Bounds.Height), noSkia)); + context.Custom(new CustomDrawOp(new Rect(0, 0, Bounds.Width, Bounds.Height), _noSkia)); Dispatcher.UIThread.InvokeAsync(InvalidateVisual, DispatcherPriority.Background); } } diff --git a/samples/RenderDemo/Pages/PathMeasurementPage.cs b/samples/RenderDemo/Pages/PathMeasurementPage.cs index cc5125609c..2fe57165b3 100644 --- a/samples/RenderDemo/Pages/PathMeasurementPage.cs +++ b/samples/RenderDemo/Pages/PathMeasurementPage.cs @@ -37,11 +37,8 @@ namespace RenderDemo.Pages public override void Render(DrawingContext context) { - using (var ctxi = _bitmap.CreateDrawingContext(null)) - using (var bitmapCtx = new DrawingContext(ctxi, false)) + using (var bitmapCtx = _bitmap.CreateDrawingContext()) { - ctxi.Clear(default); - var basePath = new PathGeometry(); using (var basePathCtx = basePath.Open()) diff --git a/samples/RenderDemo/Pages/RenderTargetBitmapPage.cs b/samples/RenderDemo/Pages/RenderTargetBitmapPage.cs index f365b59c20..b88dded39b 100644 --- a/samples/RenderDemo/Pages/RenderTargetBitmapPage.cs +++ b/samples/RenderDemo/Pages/RenderTargetBitmapPage.cs @@ -28,13 +28,11 @@ namespace RenderDemo.Pages readonly Stopwatch _st = Stopwatch.StartNew(); public override void Render(DrawingContext context) { - using (var ctxi = _bitmap.CreateDrawingContext(null)) - using(var ctx = new DrawingContext(ctxi, false)) + using (var ctx = _bitmap.CreateDrawingContext()) using (ctx.PushPostTransform(Matrix.CreateTranslation(-100, -100) * Matrix.CreateRotation(_st.Elapsed.TotalSeconds) * Matrix.CreateTranslation(100, 100))) { - ctxi.Clear(default); ctx.FillRectangle(Brushes.Fuchsia, new Rect(50, 50, 100, 100)); } diff --git a/src/Avalonia.Base/Controls/ResourceNodeExtensions.cs b/src/Avalonia.Base/Controls/ResourceNodeExtensions.cs index 4b0bab0c92..8aed1545a5 100644 --- a/src/Avalonia.Base/Controls/ResourceNodeExtensions.cs +++ b/src/Avalonia.Base/Controls/ResourceNodeExtensions.cs @@ -138,18 +138,18 @@ namespace Avalonia.Controls protected override void Initialize() { _target.ResourcesChanged += ResourcesChanged; - if (_target is StyledElement themeStyleable) + if (_target is IThemeVariantHost themeVariantHost) { - themeStyleable.PropertyChanged += PropertyChanged; + themeVariantHost.ActualThemeVariantChanged += ActualThemeVariantChanged; } } protected override void Deinitialize() { _target.ResourcesChanged -= ResourcesChanged; - if (_target is StyledElement themeStyleable) + if (_target is IThemeVariantHost themeVariantHost) { - themeStyleable.PropertyChanged -= PropertyChanged; + themeVariantHost.ActualThemeVariantChanged -= ActualThemeVariantChanged; } } @@ -163,18 +163,15 @@ namespace Avalonia.Controls PublishNext(GetValue()); } - private void PropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) + private void ActualThemeVariantChanged(object? sender, EventArgs e) { - if (e.Property == StyledElement.ActualThemeVariantProperty) - { - PublishNext(GetValue()); - } + PublishNext(GetValue()); } private object? GetValue() { - if (_target is not StyledElement themeStyleable - || !_target.TryFindResource(_key, themeStyleable.ActualThemeVariant, out var value)) + if (_target is not IThemeVariantHost themeVariantHost + || !_target.TryFindResource(_key, themeVariantHost.ActualThemeVariant, out var value)) { value = _target.FindResource(_key) ?? AvaloniaProperty.UnsetValue; } @@ -236,9 +233,9 @@ namespace Avalonia.Controls { _owner.ResourcesChanged -= ResourcesChanged; } - if (_owner is StyledElement styleable) + if (_owner is IThemeVariantHost themeVariantHost) { - styleable.PropertyChanged += PropertyChanged; + themeVariantHost.ActualThemeVariantChanged += ActualThemeVariantChanged; } _owner = _target.Owner; @@ -247,20 +244,18 @@ namespace Avalonia.Controls { _owner.ResourcesChanged += ResourcesChanged; } - if (_owner is StyledElement styleable2) + if (_owner is IThemeVariantHost themeVariantHost2) { - styleable2.PropertyChanged += PropertyChanged; + themeVariantHost2.ActualThemeVariantChanged -= ActualThemeVariantChanged; } + PublishNext(); } - private void PropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) + private void ActualThemeVariantChanged(object? sender, EventArgs e) { - if (e.Property == StyledElement.ActualThemeVariantProperty) - { - PublishNext(); - } + PublishNext(); } private void ResourcesChanged(object? sender, ResourcesChangedEventArgs e) @@ -270,8 +265,8 @@ namespace Avalonia.Controls private object? GetValue() { - if (!(_target.Owner is StyledElement themeStyleable) - || !_target.Owner.TryFindResource(_key, themeStyleable.ActualThemeVariant, out var value)) + if (!(_target.Owner is IThemeVariantHost themeVariantHost) + || !_target.Owner.TryFindResource(_key, themeVariantHost.ActualThemeVariant, out var value)) { value = _target.Owner?.FindResource(_key) ?? AvaloniaProperty.UnsetValue; } diff --git a/src/Avalonia.Base/Input/DataFormats.cs b/src/Avalonia.Base/Input/DataFormats.cs index cf5a6592e1..35d50e669a 100644 --- a/src/Avalonia.Base/Input/DataFormats.cs +++ b/src/Avalonia.Base/Input/DataFormats.cs @@ -1,4 +1,6 @@ -namespace Avalonia.Input +using System; + +namespace Avalonia.Input { public static class DataFormats { @@ -7,9 +9,15 @@ /// public static readonly string Text = nameof(Text); + /// + /// Dataformat for one or more files. + /// + public static readonly string Files = nameof(Files); + /// /// Dataformat for one or more filenames /// + [Obsolete("Use DataFormats.Files, this format is supported only on desktop platforms.")] public static readonly string FileNames = nameof(FileNames); } } diff --git a/src/Avalonia.Base/Input/DataObject.cs b/src/Avalonia.Base/Input/DataObject.cs index 688f5f9cc8..93a6baa03c 100644 --- a/src/Avalonia.Base/Input/DataObject.cs +++ b/src/Avalonia.Base/Input/DataObject.cs @@ -2,37 +2,34 @@ namespace Avalonia.Input { + /// + /// Specific and mutable implementation of the IDataObject interface. + /// public class DataObject : IDataObject { - private readonly Dictionary _items = new Dictionary(); + private readonly Dictionary _items = new(); + /// public bool Contains(string dataFormat) { return _items.ContainsKey(dataFormat); } + /// public object? Get(string dataFormat) { - if (_items.ContainsKey(dataFormat)) - return _items[dataFormat]; - return null; + return _items.TryGetValue(dataFormat, out var item) ? item : null; } + /// public IEnumerable GetDataFormats() { return _items.Keys; } - public IEnumerable? GetFileNames() - { - return Get(DataFormats.FileNames) as IEnumerable; - } - - public string? GetText() - { - return Get(DataFormats.Text) as string; - } - + /// + /// Sets a value to the internal store of the data object with as a key. + /// public void Set(string dataFormat, object value) { _items[dataFormat] = value; diff --git a/src/Avalonia.Base/Input/DataObjectExtensions.cs b/src/Avalonia.Base/Input/DataObjectExtensions.cs new file mode 100644 index 0000000000..6af531b0d8 --- /dev/null +++ b/src/Avalonia.Base/Input/DataObjectExtensions.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; +using System.Linq; +using Avalonia.Platform.Storage; + +namespace Avalonia.Input +{ + public static class DataObjectExtensions + { + /// + /// Returns a list of files if the DataObject contains files or filenames. + /// . + /// + /// + /// Collection of storage items - files or folders. If format isn't available, returns null. + /// + public static IEnumerable? GetFiles(this IDataObject dataObject) + { + return dataObject.Get(DataFormats.Files) as IEnumerable; + } + + /// + /// Returns a list of filenames if the DataObject contains filenames. + /// + /// + /// + /// Collection of file names. If format isn't available, returns null. + /// + [System.Obsolete("Use GetFiles, this method is supported only on desktop platforms.")] + public static IEnumerable? GetFileNames(this IDataObject dataObject) + { + return (dataObject.Get(DataFormats.FileNames) as IEnumerable) + ?? dataObject.GetFiles()? + .Select(f => f.TryGetLocalPath()) + .Where(p => !string.IsNullOrEmpty(p)) + .OfType(); + } + + /// + /// Returns the dragged text if the DataObject contains any text. + /// + /// + /// + /// A text string. If format isn't available, returns null. + /// + public static string? GetText(this IDataObject dataObject) + { + return dataObject.Get(DataFormats.Text) as string; + } + } +} diff --git a/src/Avalonia.Base/Input/IDataObject.cs b/src/Avalonia.Base/Input/IDataObject.cs index 1db008aa3a..6ccd0a8499 100644 --- a/src/Avalonia.Base/Input/IDataObject.cs +++ b/src/Avalonia.Base/Input/IDataObject.cs @@ -1,4 +1,6 @@ using System.Collections.Generic; +using System.Linq; +using Avalonia.Platform.Storage; namespace Avalonia.Input { @@ -19,21 +21,12 @@ namespace Avalonia.Input /// bool Contains(string dataFormat); - /// - /// Returns the dragged text if the DataObject contains any text. - /// - /// - string? GetText(); - - /// - /// Returns a list of filenames if the DataObject contains filenames. - /// - /// - IEnumerable? GetFileNames(); - /// /// Tries to get the data of the given DataFormat. /// + /// + /// Object data. If format isn't available, returns null. + /// object? Get(string dataFormat); } } diff --git a/src/Avalonia.Base/Layout/Layoutable.cs b/src/Avalonia.Base/Layout/Layoutable.cs index 4a273b0291..ea84dc84bd 100644 --- a/src/Avalonia.Base/Layout/Layoutable.cs +++ b/src/Avalonia.Base/Layout/Layoutable.cs @@ -798,6 +798,12 @@ namespace Avalonia.Layout InvalidateMeasure(); } + internal override void OnTemplatedParentControlThemeChanged() + { + base.OnTemplatedParentControlThemeChanged(); + InvalidateMeasure(); + } + /// /// Called when the layout manager raises a LayoutUpdated event. /// diff --git a/src/Avalonia.Base/LogicalTree/ChildIndexChangedEventArgs.cs b/src/Avalonia.Base/LogicalTree/ChildIndexChangedEventArgs.cs index 497596fcc1..6b41c1c66c 100644 --- a/src/Avalonia.Base/LogicalTree/ChildIndexChangedEventArgs.cs +++ b/src/Avalonia.Base/LogicalTree/ChildIndexChangedEventArgs.cs @@ -1,28 +1,79 @@ -#nullable enable -using System; +using System; + +#nullable enable namespace Avalonia.LogicalTree { + /// + /// Describes the action that caused a event. + /// + public enum ChildIndexChangedAction + { + /// + /// The index of a single child changed. + /// + ChildIndexChanged, + + /// + /// The index of multiple children changed and all children should be re-evaluated. + /// + ChildIndexesReset, + + /// + /// The total number of children changed. + /// + TotalCountChanged, + } + /// /// Event args for event. /// public class ChildIndexChangedEventArgs : EventArgs { - public static new ChildIndexChangedEventArgs Empty { get; } = new ChildIndexChangedEventArgs(); - - private ChildIndexChangedEventArgs() + /// + /// Initializes a new instance of the class with + /// an action of . + /// + /// The child whose index was changed. + /// The new index of the child. + public ChildIndexChangedEventArgs(ILogical child, int index) { + Action = ChildIndexChangedAction.ChildIndexChanged; + Child = child; + Index = index; } - public ChildIndexChangedEventArgs(ILogical child) + private ChildIndexChangedEventArgs(ChildIndexChangedAction action) { - Child = child; + Action = action; + Index = -1; } /// - /// Logical child which index was changed. - /// If null, all children should be reset. + /// Gets the type of change action that ocurred on the list control. + /// + public ChildIndexChangedAction Action { get; } + + /// + /// Gets the logical child whose index was changed or null if all children should be re-evaluated. /// public ILogical? Child { get; } + + /// + /// Gets the new index of or -1 if all children should be re-evaluated. + /// + public int Index { get; } + + /// + /// Gets an instance of the with an action of + /// . + /// + public static ChildIndexChangedEventArgs ChildIndexesReset { get; } = new(ChildIndexChangedAction.ChildIndexesReset); + + /// + /// Gets an instance of the with an action of + /// . + /// + public static ChildIndexChangedEventArgs TotalCountChanged { get; } = new(ChildIndexChangedAction.TotalCountChanged); } } diff --git a/src/Avalonia.Base/LogicalTree/IChildIndexProvider.cs b/src/Avalonia.Base/LogicalTree/IChildIndexProvider.cs index 7fcd73273c..186c9527f2 100644 --- a/src/Avalonia.Base/LogicalTree/IChildIndexProvider.cs +++ b/src/Avalonia.Base/LogicalTree/IChildIndexProvider.cs @@ -25,7 +25,7 @@ namespace Avalonia.LogicalTree bool TryGetTotalCount(out int count); /// - /// Notifies subscriber when child's index or total count was changed. + /// Notifies subscriber when a child's index was changed. /// event EventHandler? ChildIndexChanged; } diff --git a/src/Avalonia.Base/Media/DrawingBrush.cs b/src/Avalonia.Base/Media/DrawingBrush.cs new file mode 100644 index 0000000000..2825628948 --- /dev/null +++ b/src/Avalonia.Base/Media/DrawingBrush.cs @@ -0,0 +1,66 @@ +using Avalonia.Media.Immutable; +using Avalonia.Rendering; +using Avalonia.Rendering.Composition; +using Avalonia.Rendering.Composition.Drawing; + +namespace Avalonia.Media +{ + /// + /// Paints an area with an . + /// + public class DrawingBrush : TileBrush, ISceneBrush, IAffectsRender + { + /// + /// Defines the property. + /// + public static readonly StyledProperty DrawingProperty = + AvaloniaProperty.Register(nameof(Drawing)); + + static DrawingBrush() + { + AffectsRender(DrawingProperty); + } + + /// + /// Initializes a new instance of the class. + /// + public DrawingBrush() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The visual to draw. + public DrawingBrush(Drawing visual) + { + Drawing = visual; + } + + /// + /// Gets or sets the visual to draw. + /// + public Drawing? Drawing + { + get { return GetValue(DrawingProperty); } + set { SetValue(DrawingProperty, value); } + } + + ISceneBrushContent? ISceneBrush.CreateContent() + { + if (Drawing == null) + return null; + + + var recorder = new CompositionDrawingContext(); + recorder.BeginUpdate(null); + Drawing?.Draw(recorder); + var drawList = recorder.EndUpdate(); + if (drawList == null) + return null; + + return new CompositionDrawListSceneBrushContent(new ImmutableSceneBrush(this), drawList, + drawList.CalculateBounds(), true); + } + } +} diff --git a/src/Avalonia.Base/Media/DrawingContext.cs b/src/Avalonia.Base/Media/DrawingContext.cs index a37fa6fd32..31a16dc69c 100644 --- a/src/Avalonia.Base/Media/DrawingContext.cs +++ b/src/Avalonia.Base/Media/DrawingContext.cs @@ -8,83 +8,45 @@ using Avalonia.Media.Imaging; namespace Avalonia.Media { - public sealed class DrawingContext : IDisposable + public abstract class DrawingContext : IDisposable { - private readonly bool _ownsImpl; - private int _currentLevel; + private static ThreadSafeObjectPool> StateStackPool { get; } = + ThreadSafeObjectPool>.Default; + private Stack? _states; - private static ThreadSafeObjectPool> StateStackPool { get; } = - ThreadSafeObjectPool>.Default; - - private static ThreadSafeObjectPool> TransformStackPool { get; } = - ThreadSafeObjectPool>.Default; - - private Stack? _states = StateStackPool.Get(); - - private Stack? _transformContainers = TransformStackPool.Get(); - - readonly struct TransformContainer - { - public readonly Matrix LocalTransform; - public readonly Matrix ContainerTransform; - - public TransformContainer(Matrix localTransform, Matrix containerTransform) - { - LocalTransform = localTransform; - ContainerTransform = containerTransform; - } - } - - public DrawingContext(IDrawingContextImpl impl) + internal DrawingContext() { - PlatformImpl = impl; - _ownsImpl = true; + } - - public DrawingContext(IDrawingContextImpl impl, bool ownsImpl) - { - _ownsImpl = ownsImpl; - PlatformImpl = impl; - } - - public IDrawingContextImpl PlatformImpl { get; } - - private Matrix _currentTransform = Matrix.Identity; - private Matrix _currentContainerTransform = Matrix.Identity; - - /// - /// Gets the current transform of the drawing context. - /// - public Matrix CurrentTransform + public void Dispose() { - get { return _currentTransform; } - private set + if (_states != null) { - _currentTransform = value; - var transform = _currentTransform * _currentContainerTransform; - PlatformImpl.Transform = transform; - } - } + while (_states.Count > 0) + _states.Pop().Dispose(); - //HACK: This is a temporary hack that is used in the render loop - //to update TransformedBounds property - [Obsolete("HACK for render loop, don't use")] - public Matrix CurrentContainerTransform => _currentContainerTransform; + StateStackPool.ReturnAndSetNull(ref _states); + } + DisposeCore(); + } + + protected abstract void DisposeCore(); + /// /// Draws an image. /// /// The image. /// The rect in the output to draw to. - public void DrawImage(IImage source, Rect rect) + public virtual void DrawImage(IImage source, Rect rect) { _ = source ?? throw new ArgumentNullException(nameof(source)); - DrawImage(source, new Rect(source.Size), rect); } + /// /// Draws an image. /// @@ -92,12 +54,22 @@ namespace Avalonia.Media /// The rect in the image to draw. /// The rect in the output to draw to. /// The bitmap interpolation mode. - public void DrawImage(IImage source, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode = default) + public virtual void DrawImage(IImage source, Rect sourceRect, Rect destRect, + BitmapInterpolationMode bitmapInterpolationMode = default) { _ = source ?? throw new ArgumentNullException(nameof(source)); - source.Draw(this, sourceRect, destRect, bitmapInterpolationMode); } + + /// + /// Draws a platform-specific bitmap impl. + /// + /// The bitmap image. + /// The opacity to draw with. + /// The rect in the image to draw. + /// The rect in the output to draw to. + /// The bitmap interpolation mode. + internal abstract void DrawBitmap(IRef source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default); /// /// Draws a line. @@ -108,11 +80,11 @@ namespace Avalonia.Media public void DrawLine(IPen pen, Point p1, Point p2) { if (PenIsVisible(pen)) - { - PlatformImpl.DrawLine(pen, p1, p2); - } + DrawLineCore(pen, p1, p2); } + protected abstract void DrawLineCore(IPen pen, Point p1, Point p2); + /// /// Draws a geometry. /// @@ -121,10 +93,10 @@ namespace Avalonia.Media /// The geometry. public void DrawGeometry(IBrush? brush, IPen? pen, Geometry geometry) { - if (geometry.PlatformImpl is not null) - DrawGeometry(brush, pen, geometry.PlatformImpl); + if ((brush != null || PenIsVisible(pen)) && geometry.PlatformImpl != null) + DrawGeometryCore(brush, pen, geometry.PlatformImpl); } - + /// /// Draws a geometry. /// @@ -133,14 +105,12 @@ namespace Avalonia.Media /// The geometry. public void DrawGeometry(IBrush? brush, IPen? pen, IGeometryImpl geometry) { - _ = geometry ?? throw new ArgumentNullException(nameof(geometry)); - - if (brush != null || PenIsVisible(pen)) - { - PlatformImpl.DrawGeometry(brush, pen, geometry); - } + if ((brush != null || PenIsVisible(pen))) + DrawGeometryCore(brush, pen, geometry); } + protected abstract void DrawGeometryCore(IBrush? brush, IPen? pen, IGeometryImpl geometry); + /// /// Draws a rectangle with the specified Brush and Pen. /// @@ -158,14 +128,12 @@ namespace Avalonia.Media /// The brush and the pen can both be null. If the brush is null, then no fill is performed. /// If the pen is null, then no stoke is performed. If both the pen and the brush are null, then the drawing is not visible. /// - public void DrawRectangle(IBrush? brush, IPen? pen, Rect rect, double radiusX = 0, double radiusY = 0, + public void DrawRectangle(IBrush? brush, IPen? pen, Rect rect, + double radiusX = 0, double radiusY = 0, BoxShadows boxShadows = default) { if (brush == null && !PenIsVisible(pen)) - { return; - } - if (!MathUtilities.IsZero(radiusX)) { radiusX = Math.Min(radiusX, rect.Width / 2); @@ -175,20 +143,48 @@ namespace Avalonia.Media { radiusY = Math.Min(radiusY, rect.Height / 2); } - - PlatformImpl.DrawRectangle(brush, pen, new RoundedRect(rect, radiusX, radiusY), boxShadows); + + DrawRectangleCore(brush, pen, new RoundedRect(rect, radiusX, radiusY), boxShadows); + } + + /// + /// Draws a rectangle with the specified Brush and Pen. + /// + /// The brush used to fill the rectangle, or null for no fill. + /// The pen used to stroke the rectangle, or null for no stroke. + /// The rectangle bounds. + /// Box shadow effect parameters + /// + /// The brush and the pen can both be null. If the brush is null, then no fill is performed. + /// If the pen is null, then no stoke is performed. If both the pen and the brush are null, then the drawing is not visible. + /// + public void DrawRectangle(IBrush? brush, IPen? pen, RoundedRect rrect, BoxShadows boxShadows = default) + { + if (brush == null && !PenIsVisible(pen)) + return; + DrawRectangleCore(brush, pen, rrect, boxShadows); } + protected abstract void DrawRectangleCore(IBrush? brush, IPen? pen, RoundedRect rrect, + BoxShadows boxShadows = default); + /// /// Draws the outline of a rectangle. /// /// The pen. /// The rectangle bounds. /// The corner radius. - public void DrawRectangle(IPen pen, Rect rect, float cornerRadius = 0.0f) - { + public void DrawRectangle(IPen pen, Rect rect, float cornerRadius = 0.0f) => DrawRectangle(null, pen, rect, cornerRadius, cornerRadius); - } + + /// + /// Draws a filled rectangle. + /// + /// The brush. + /// The rectangle bounds. + /// The corner radius. + public void FillRectangle(IBrush brush, Rect rect, float cornerRadius = 0.0f) => + DrawRectangle(brush, null, rect, cornerRadius, cornerRadius); /// /// Draws an ellipse with the specified Brush and Pen. @@ -204,35 +200,50 @@ namespace Avalonia.Media /// public void DrawEllipse(IBrush? brush, IPen? pen, Point center, double radiusX, double radiusY) { - if (brush == null && !PenIsVisible(pen)) + if (brush != null || PenIsVisible(pen)) { - return; + var originX = center.X - radiusX; + var originY = center.Y - radiusY; + var width = radiusX * 2; + var height = radiusY * 2; + DrawEllipseCore(brush, pen, new Rect(originX, originY, width, height)); } - - var originX = center.X - radiusX; - var originY = center.Y - radiusY; - var width = radiusX * 2; - var height = radiusY * 2; - - PlatformImpl.DrawEllipse(brush, pen, new Rect(originX, originY, width, height)); + } + + /// + /// Draws an ellipse with the specified Brush and Pen. + /// + /// The brush used to fill the ellipse, or null for no fill. + /// The pen used to stroke the ellipse, or null for no stroke. + /// The bounding rect. + /// + /// The brush and the pen can both be null. If the brush is null, then no fill is performed. + /// If the pen is null, then no stoke is performed. If both the pen and the brush are null, then the drawing is not visible. + /// + public void DrawEllipse(IBrush? brush, IPen? pen, Rect rect) + { + if (brush != null || PenIsVisible(pen)) + DrawEllipseCore(brush, pen, rect); } + protected abstract void DrawEllipseCore(IBrush? brush, IPen? pen, Rect rect); + /// /// Draws a custom drawing operation /// /// custom operation - public void Custom(ICustomDrawOperation custom) => PlatformImpl.Custom(custom); + public abstract void Custom(ICustomDrawOperation custom); /// /// Draws text. /// /// The upper-left corner of the text. /// The text. - public void DrawText(FormattedText text, Point origin) + public virtual void DrawText(FormattedText text, Point origin) { _ = text ?? throw new ArgumentNullException(nameof(text)); - text.Draw(this, origin); + text.Draw(this, origin); } /// @@ -240,30 +251,31 @@ namespace Avalonia.Media /// /// The foreground brush. /// The glyph run. - public void DrawGlyphRun(IBrush? foreground, GlyphRun glyphRun) + public abstract void DrawGlyphRun(IBrush? foreground, GlyphRun glyphRun); + + public record struct PushedState : IDisposable { - _ = glyphRun ?? throw new ArgumentNullException(nameof(glyphRun)); + private readonly DrawingContext _context; + private readonly int _level; - if (foreground != null) + public PushedState(DrawingContext context) { - PlatformImpl.DrawGlyphRun(foreground, glyphRun.PlatformImpl); + _context = context; + _level = _context._states!.Count; } - } - /// - /// Draws a filled rectangle. - /// - /// The brush. - /// The rectangle bounds. - /// The corner radius. - public void FillRectangle(IBrush brush, Rect rect, float cornerRadius = 0.0f) - { - DrawRectangle(brush, null, rect, cornerRadius, cornerRadius); + public void Dispose() + { + if(_context?._states == null) + return; + if(_context._states.Count != _level) + throw new InvalidOperationException("Wrong Push/Pop state order"); + _context._states.Pop().Dispose(); + } } - - public readonly record struct PushedState : IDisposable + + private readonly record struct RestoreState : IDisposable { - private readonly int _level; private readonly DrawingContext _context; private readonly Matrix _matrix; private readonly PushedStateType _type; @@ -271,62 +283,56 @@ namespace Avalonia.Media public enum PushedStateType { None, - Matrix, + Transform, Opacity, Clip, - MatrixContainer, GeometryClip, OpacityMask, + BitmapBlendMode } - public PushedState(DrawingContext context, PushedStateType type, Matrix matrix = default) + public RestoreState(DrawingContext context, PushedStateType type) { - if (context._states is null) - throw new ObjectDisposedException(nameof(DrawingContext)); - _context = context; _type = type; - _matrix = matrix; - _level = context._currentLevel += 1; - context._states.Push(this); } public void Dispose() { if (_type == PushedStateType.None) return; - if (_context._states is null || _context._transformContainers is null) + if (_context._states is null) throw new ObjectDisposedException(nameof(DrawingContext)); - if (_context._currentLevel != _level) - throw new InvalidOperationException("Wrong Push/Pop state order"); - _context._currentLevel--; - _context._states.Pop(); - if (_type == PushedStateType.Matrix) - _context.CurrentTransform = _matrix; + if (_type == PushedStateType.Transform) + _context.PopTransformCore(); else if (_type == PushedStateType.Clip) - _context.PlatformImpl.PopClip(); + _context.PopClipCore(); else if (_type == PushedStateType.Opacity) - _context.PlatformImpl.PopOpacity(); + _context.PopOpacityCore(); else if (_type == PushedStateType.GeometryClip) - _context.PlatformImpl.PopGeometryClip(); + _context.PopGeometryClipCore(); else if (_type == PushedStateType.OpacityMask) - _context.PlatformImpl.PopOpacityMask(); - else if (_type == PushedStateType.MatrixContainer) - { - var cont = _context._transformContainers.Pop(); - _context._currentContainerTransform = cont.ContainerTransform; - _context.CurrentTransform = cont.LocalTransform; - } + _context.PopOpacityMaskCore(); + else if (_type == PushedStateType.BitmapBlendMode) + _context.PopBitmapBlendModeCore(); } } - + /// + /// Pushes a clip rectangle. + /// + /// The clip rectangle. + /// A disposable used to undo the clip rectangle. public PushedState PushClip(RoundedRect clip) { - PlatformImpl.PushClip(clip); - return new PushedState(this, PushedState.PushedStateType.Clip); + PushClipCore(clip); + _states ??= StateStackPool.Get(); + _states.Push(new RestoreState(this, RestoreState.PushedStateType.Clip)); + return new PushedState(this); } + protected abstract void PushClipCore(RoundedRect rect); + /// /// Pushes a clip rectangle. /// @@ -334,9 +340,13 @@ namespace Avalonia.Media /// A disposable used to undo the clip rectangle. public PushedState PushClip(Rect clip) { - PlatformImpl.PushClip(clip); - return new PushedState(this, PushedState.PushedStateType.Clip); + PushClipCore(clip); + _states ??= StateStackPool.Get(); + _states.Push(new RestoreState(this, RestoreState.PushedStateType.Clip)); + return new PushedState(this); } + + protected abstract void PushClipCore(Rect rect); /// /// Pushes a clip geometry. @@ -345,17 +355,13 @@ namespace Avalonia.Media /// A disposable used to undo the clip geometry. public PushedState PushGeometryClip(Geometry clip) { - _ = clip ?? throw new ArgumentNullException(nameof(clip)); - - // HACK: This check was added when nullable annotations pointed out that we're potentially - // pushing a null value for the clip here. Ideally we'd return an empty PushedState here but - // I don't want to make that change as part of adding nullable annotations. - if (clip.PlatformImpl is null) - throw new InvalidOperationException("Cannot push empty geometry clip."); - - PlatformImpl.PushGeometryClip(clip.PlatformImpl); - return new PushedState(this, PushedState.PushedStateType.GeometryClip); + PushGeometryClipCore(clip); + _states ??= StateStackPool.Get(); + _states.Push(new RestoreState(this, RestoreState.PushedStateType.GeometryClip)); + return new PushedState(this); } + + protected abstract void PushGeometryClipCore(Geometry clip); /// /// Pushes an opacity value. @@ -364,11 +370,13 @@ namespace Avalonia.Media /// The bounds. /// A disposable used to undo the opacity. public PushedState PushOpacity(double opacity, Rect bounds) - //TODO: Eliminate platform-specific push opacity call { - PlatformImpl.PushOpacity(opacity, bounds); - return new PushedState(this, PushedState.PushedStateType.Opacity); + PushOpacityCore(opacity, bounds); + _states ??= StateStackPool.Get(); + _states.Push(new RestoreState(this, RestoreState.PushedStateType.Opacity)); + return new PushedState(this); } + protected abstract void PushOpacityCore(double opacity, Rect bounds); /// /// Pushes an opacity mask. @@ -380,70 +388,53 @@ namespace Avalonia.Media /// A disposable to undo the opacity mask. public PushedState PushOpacityMask(IBrush mask, Rect bounds) { - PlatformImpl.PushOpacityMask(mask, bounds); - return new PushedState(this, PushedState.PushedStateType.OpacityMask); + PushOpacityMaskCore(mask, bounds); + _states ??= StateStackPool.Get(); + _states.Push(new RestoreState(this, RestoreState.PushedStateType.OpacityMask)); + return new PushedState(this); } + protected abstract void PushOpacityMaskCore(IBrush mask, Rect bounds); - /// - /// Pushes a matrix post-transformation. - /// - /// The matrix - /// A disposable used to undo the transformation. - public PushedState PushPostTransform(Matrix matrix) => PushSetTransform(CurrentTransform * matrix); - - /// - /// Pushes a matrix pre-transformation. - /// - /// The matrix - /// A disposable used to undo the transformation. - public PushedState PushPreTransform(Matrix matrix) => PushSetTransform(matrix * CurrentTransform); - - /// - /// Sets the current matrix transformation. - /// - /// The matrix - /// A disposable used to undo the transformation. - public PushedState PushSetTransform(Matrix matrix) + public PushedState PushBitmapBlendMode(BitmapBlendingMode blendingMode) { - var oldMatrix = CurrentTransform; - CurrentTransform = matrix; - - return new PushedState(this, PushedState.PushedStateType.Matrix, oldMatrix); + PushBitmapBlendMode(blendingMode); + _states ??= StateStackPool.Get(); + _states.Push(new RestoreState(this, RestoreState.PushedStateType.BitmapBlendMode)); + return new PushedState(this); } - /// - /// Pushes a new transform context. - /// - /// A disposable used to undo the transformation. - public PushedState PushTransformContainer() - { - if (_transformContainers is null) - throw new ObjectDisposedException(nameof(DrawingContext)); - _transformContainers.Push(new TransformContainer(CurrentTransform, _currentContainerTransform)); - _currentContainerTransform = CurrentTransform * _currentContainerTransform; - _currentTransform = Matrix.Identity; - return new PushedState(this, PushedState.PushedStateType.MatrixContainer); - } + protected abstract void PushBitmapBlendModeCore(BitmapBlendingMode blendingMode); /// - /// Disposes of any resources held by the . + /// Pushes a matrix transformation. /// - public void Dispose() + /// The matrix + /// A disposable used to undo the transformation. + public PushedState PushTransform(Matrix matrix) { - if (_states is null || _transformContainers is null) - throw new ObjectDisposedException(nameof(DrawingContext)); - while (_states.Count != 0) - _states.Peek().Dispose(); - StateStackPool.Return(_states); - _states = null; - if (_transformContainers.Count != 0) - throw new InvalidOperationException("Transform container stack is non-empty"); - TransformStackPool.Return(_transformContainers); - _transformContainers = null; - if (_ownsImpl) - PlatformImpl.Dispose(); + PushTransformCore(matrix); + _states ??= StateStackPool.Get(); + _states.Push(new RestoreState(this, RestoreState.PushedStateType.Transform)); + return new PushedState(this); } + [Obsolete("Use PushTransform")] + public PushedState PushPreTransform(Matrix matrix) => PushTransform(matrix); + [Obsolete("Use PushTransform")] + public PushedState PushPostTransform(Matrix matrix) => PushTransform(matrix); + [Obsolete("Use PushTransform")] + public PushedState PushTransformContainer() => PushTransform(Matrix.Identity); + + + protected abstract void PushTransformCore(Matrix matrix); + + protected abstract void PopClipCore(); + protected abstract void PopGeometryClipCore(); + protected abstract void PopOpacityCore(); + protected abstract void PopOpacityMaskCore(); + protected abstract void PopBitmapBlendModeCore(); + protected abstract void PopTransformCore(); + private static bool PenIsVisible(IPen? pen) { return pen?.Brush != null && pen.Thickness > 0; diff --git a/src/Avalonia.Base/Media/DrawingGroup.cs b/src/Avalonia.Base/Media/DrawingGroup.cs index 7b02649b6c..812d315912 100644 --- a/src/Avalonia.Base/Media/DrawingGroup.cs +++ b/src/Avalonia.Base/Media/DrawingGroup.cs @@ -67,10 +67,7 @@ namespace Avalonia.Media } } - public DrawingContext Open() - { - return new DrawingContext(new DrawingGroupDrawingContext(this)); - } + public DrawingContext Open() => new DrawingGroupDrawingContext(this); public override void Draw(DrawingContext context) { @@ -105,7 +102,7 @@ namespace Avalonia.Media return rect; } - private class DrawingGroupDrawingContext : IDrawingContextImpl + private sealed class DrawingGroupDrawingContext : DrawingContext { private readonly DrawingGroup _drawingGroup; private readonly IPlatformRenderInterface _platformRenderInterface = AvaloniaLocator.Current.GetRequiredService(); @@ -135,17 +132,7 @@ namespace Avalonia.Media _drawingGroup = drawingGroup; } - public Matrix Transform - { - get => _transform; - set - { - _transform = value; - PushTransform(new MatrixTransform(value)); - } - } - - public void DrawEllipse(IBrush? brush, IPen? pen, Rect rect) + protected override void DrawEllipseCore(IBrush? brush, IPen? pen, Rect rect) { if ((brush == null) && (pen == null)) { @@ -159,7 +146,7 @@ namespace Avalonia.Media AddNewGeometryDrawing(brush, pen, new PlatformGeometry(geometry)); } - public void DrawGeometry(IBrush? brush, IPen? pen, IGeometryImpl geometry) + protected override void DrawGeometryCore(IBrush? brush, IPen? pen, IGeometryImpl geometry) { if ((brush == null) && (pen == null)) { @@ -169,7 +156,7 @@ namespace Avalonia.Media AddNewGeometryDrawing(brush, pen, new PlatformGeometry(geometry)); } - public void DrawGlyphRun(IBrush? foreground, IRef glyphRun) + public override void DrawGlyphRun(IBrush? foreground, GlyphRun glyphRun) { if (foreground == null) { @@ -179,124 +166,70 @@ namespace Avalonia.Media GlyphRunDrawing glyphRunDrawing = new GlyphRunDrawing { Foreground = foreground, - GlyphRun = new GlyphRun(glyphRun) + GlyphRun = glyphRun }; // Add Drawing to the Drawing graph AddDrawing(glyphRunDrawing); } - public void DrawLine(IPen? pen, Point p1, Point p2) - { - if (pen == null) - { - return; - } - - // Instantiate the geometry - var geometry = _platformRenderInterface.CreateLineGeometry(p1, p2); - - // Add Drawing to the Drawing graph - AddNewGeometryDrawing(null, pen, new PlatformGeometry(geometry)); - } - - public void DrawRectangle(IBrush? brush, IPen? pen, RoundedRect rect, BoxShadows boxShadows = default) - { - if ((brush == null) && (pen == null)) - { - return; - } - - // Instantiate the geometry - var geometry = _platformRenderInterface.CreateRectangleGeometry(rect.Rect); - - // Add Drawing to the Drawing graph - AddNewGeometryDrawing(brush, pen, new PlatformGeometry(geometry)); - } - - public void Clear(Color color) + protected override void PushClipCore(RoundedRect rect) { throw new NotImplementedException(); } - public IDrawingContextLayerImpl CreateLayer(Size size) + protected override void PushClipCore(Rect rect) { throw new NotImplementedException(); } - public void Custom(ICustomDrawOperation custom) + protected override void PushGeometryClipCore(Geometry clip) { throw new NotImplementedException(); } - public object? GetFeature(Type t) => null; - - public void DrawBitmap(IRef source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default) + protected override void PushOpacityCore(double opacity, Rect bounds) { throw new NotImplementedException(); } - public void DrawBitmap(IRef source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect) + protected override void PushOpacityMaskCore(IBrush mask, Rect bounds) { throw new NotImplementedException(); } - public void PopBitmapBlendMode() + protected override void PushBitmapBlendModeCore(BitmapBlendingMode blendingMode) { throw new NotImplementedException(); } - public void PopClip() + internal override void DrawBitmap(IRef source, double opacity, Rect sourceRect, Rect destRect, + BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default) { throw new NotImplementedException(); } - public void PopGeometryClip() + protected override void DrawLineCore(IPen pen, Point p1, Point p2) { - throw new NotImplementedException(); - } - - public void PopOpacity() - { - throw new NotImplementedException(); - } - - public void PopOpacityMask() - { - throw new NotImplementedException(); - } - - public void PushBitmapBlendMode(BitmapBlendingMode blendingMode) - { - throw new NotImplementedException(); - } - - public void PushClip(Rect clip) - { - throw new NotImplementedException(); - } + // Instantiate the geometry + var geometry = _platformRenderInterface.CreateLineGeometry(p1, p2); - public void PushClip(RoundedRect clip) - { - throw new NotImplementedException(); + // Add Drawing to the Drawing graph + AddNewGeometryDrawing(null, pen, new PlatformGeometry(geometry)); } - public void PushGeometryClip(IGeometryImpl clip) + protected override void DrawRectangleCore(IBrush? brush, IPen? pen, RoundedRect rrect, BoxShadows boxShadows = default) { - throw new NotImplementedException(); - } + // Instantiate the geometry + var geometry = _platformRenderInterface.CreateRectangleGeometry(rrect.Rect); - public void PushOpacity(double opacity, Rect bounds) - { - throw new NotImplementedException(); + // Add Drawing to the Drawing graph + AddNewGeometryDrawing(brush, pen, new PlatformGeometry(geometry)); } - public void PushOpacityMask(IBrush mask, Rect bounds) - { - throw new NotImplementedException(); - } + public override void Custom(ICustomDrawOperation custom) => throw new NotSupportedException(); - public void Dispose() + protected override void DisposeCore() { // Dispose may be called multiple times without throwing // an exception. @@ -366,22 +299,34 @@ namespace Avalonia.Media // Restore the previous value of the current drawing group _currentDrawingGroup = _previousDrawingGroupStack.Pop(); } - + /// /// PushTransform - /// Push a Transform which will apply to all drawing operations until the corresponding /// Pop. /// - /// The Transform to push. - private void PushTransform(Transform transform) + /// The transform to push. + protected override void PushTransformCore(Matrix matrix) { // Instantiate a new drawing group and set it as the _currentDrawingGroup var drawingGroup = PushNewDrawingGroup(); // Set the transform on the new DrawingGroup - drawingGroup.Transform = transform; + drawingGroup.Transform = new MatrixTransform(matrix); } + protected override void PopClipCore() => Pop(); + + protected override void PopGeometryClipCore() => Pop(); + + protected override void PopOpacityCore() => Pop(); + + protected override void PopOpacityMaskCore() => Pop(); + + protected override void PopBitmapBlendModeCore() => Pop(); + + protected override void PopTransformCore() => Pop(); + /// /// Creates a new DrawingGroup for a Push* call by setting the /// _currentDrawingGroup to a newly instantiated DrawingGroup, diff --git a/src/Avalonia.Base/Media/DrawingImage.cs b/src/Avalonia.Base/Media/DrawingImage.cs index 1b22a1ee69..52fbd87db7 100644 --- a/src/Avalonia.Base/Media/DrawingImage.cs +++ b/src/Avalonia.Base/Media/DrawingImage.cs @@ -62,7 +62,7 @@ namespace Avalonia.Media -sourceRect.Y + destRect.Y - bounds.Y); using (context.PushClip(destRect)) - using (context.PushPreTransform(translate * scale)) + using (context.PushTransform(translate * scale)) { Drawing?.Draw(context); } diff --git a/src/Avalonia.Base/Media/FormattedText.cs b/src/Avalonia.Base/Media/FormattedText.cs index 3b63a98720..d4640390d7 100644 --- a/src/Avalonia.Base/Media/FormattedText.cs +++ b/src/Avalonia.Base/Media/FormattedText.cs @@ -877,7 +877,7 @@ namespace Avalonia.Media var lastRunProps = (GenericTextRunProperties)thatFormatRider.CurrentElement!; - TextCollapsingProperties collapsingProperties = _that._trimming.CreateCollapsingProperties(new TextCollapsingCreateInfo(maxLineLength, lastRunProps)); + TextCollapsingProperties collapsingProperties = _that._trimming.CreateCollapsingProperties(new TextCollapsingCreateInfo(maxLineLength, lastRunProps, paraProps.FlowDirection)); var collapsedLine = line.Collapse(collapsingProperties); diff --git a/src/Avalonia.Base/Media/ISceneBrush.cs b/src/Avalonia.Base/Media/ISceneBrush.cs new file mode 100644 index 0000000000..df72dd1ace --- /dev/null +++ b/src/Avalonia.Base/Media/ISceneBrush.cs @@ -0,0 +1,31 @@ +using System; +using Avalonia.Media.Imaging; +using Avalonia.Media.Immutable; +using Avalonia.Metadata; +using Avalonia.Platform; +using Avalonia.Rendering.Composition.Drawing; + +namespace Avalonia.Media +{ + [NotClientImplementable] + public interface ISceneBrush : ITileBrush + { + ISceneBrushContent? CreateContent(); + } + + [NotClientImplementable] + public interface ISceneBrushContent : IImmutableBrush, IDisposable + { + ITileBrush Brush { get; } + Rect Rect { get; } + void Render(IDrawingContextImpl context, Matrix? transform); + internal bool UseScalableRasterization { get; } + } + + internal class ImmutableSceneBrush : ImmutableTileBrush + { + public ImmutableSceneBrush(ITileBrush source) : base(source) + { + } + } +} diff --git a/src/Avalonia.Base/Media/IVisualBrush.cs b/src/Avalonia.Base/Media/IVisualBrush.cs deleted file mode 100644 index a7d3e4da10..0000000000 --- a/src/Avalonia.Base/Media/IVisualBrush.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Avalonia.Metadata; - -namespace Avalonia.Media -{ - /// - /// Paints an area with an . - /// - [NotClientImplementable] - public interface IVisualBrush : ITileBrush - { - /// - /// Gets the visual to draw. - /// - Visual? Visual { get; } - } -} diff --git a/src/Avalonia.Base/Media/Imaging/Bitmap.cs b/src/Avalonia.Base/Media/Imaging/Bitmap.cs index 6577532891..c4720d772e 100644 --- a/src/Avalonia.Base/Media/Imaging/Bitmap.cs +++ b/src/Avalonia.Base/Media/Imaging/Bitmap.cs @@ -227,7 +227,7 @@ namespace Avalonia.Media.Imaging Rect destRect, BitmapInterpolationMode bitmapInterpolationMode) { - context.PlatformImpl.DrawBitmap( + context.DrawBitmap( PlatformImpl, 1, sourceRect, diff --git a/src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs b/src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs index 88e5e627ee..e77dd9d1ab 100644 --- a/src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs +++ b/src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs @@ -9,7 +9,7 @@ namespace Avalonia.Media.Imaging /// /// A bitmap that holds the rendering of a . /// - public class RenderTargetBitmap : Bitmap, IDisposable, IRenderTarget + public class RenderTargetBitmap : Bitmap, IDisposable { /// /// Initializes a new instance of the class. @@ -44,7 +44,11 @@ namespace Avalonia.Media.Imaging /// Renders a visual to the . /// /// The visual to render. - public void Render(Visual visual) => ImmediateRenderer.Render(visual, this); + public void Render(Visual visual) + { + using (var ctx = CreateDrawingContext()) + ImmediateRenderer.Render(visual, ctx); + } /// /// Creates a platform-specific implementation for a . @@ -58,9 +62,11 @@ namespace Avalonia.Media.Imaging return factory.CreateRenderTargetBitmap(size, dpi); } - /// - public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer? vbr) => PlatformImpl.Item.CreateDrawingContext(vbr); - - bool IRenderTarget.IsCorrupted => false; + public DrawingContext CreateDrawingContext() + { + var platform = PlatformImpl.Item.CreateDrawingContext(); + platform.Clear(Colors.Transparent); + return new PlatformDrawingContext(platform); + } } } diff --git a/src/Avalonia.Base/Media/ImmediateDrawingContext.cs b/src/Avalonia.Base/Media/ImmediateDrawingContext.cs index 2564d89bac..58b153482d 100644 --- a/src/Avalonia.Base/Media/ImmediateDrawingContext.cs +++ b/src/Avalonia.Base/Media/ImmediateDrawingContext.cs @@ -354,12 +354,10 @@ namespace Avalonia.Media throw new ObjectDisposedException(nameof(DrawingContext)); while (_states.Count != 0) _states.Peek().Dispose(); - StateStackPool.Return(_states); - _states = null; + StateStackPool.ReturnAndSetNull(ref _states); if (_transformContainers.Count != 0) throw new InvalidOperationException("Transform container stack is non-empty"); - TransformStackPool.Return(_transformContainers); - _transformContainers = null; + TransformStackPool.ReturnAndSetNull(ref _transformContainers); if (_ownsImpl) PlatformImpl.Dispose(); } diff --git a/src/Avalonia.Base/Media/Immutable/ImmutableVisualBrush.cs b/src/Avalonia.Base/Media/Immutable/ImmutableVisualBrush.cs deleted file mode 100644 index e9086eee37..0000000000 --- a/src/Avalonia.Base/Media/Immutable/ImmutableVisualBrush.cs +++ /dev/null @@ -1,66 +0,0 @@ -using Avalonia.Media.Imaging; - -namespace Avalonia.Media.Immutable -{ - /// - /// Paints an area with an . - /// - internal class ImmutableVisualBrush : ImmutableTileBrush, IVisualBrush - { - /// - /// Initializes a new instance of the class. - /// - /// The visual to draw. - /// The horizontal alignment of a tile in the destination. - /// The vertical alignment of a tile in the destination. - /// The rectangle on the destination in which to paint a tile. - /// The opacity of the brush. - /// The transform of the brush. - /// The transform origin of the brush - /// The rectangle of the source image that will be displayed. - /// - /// How the source rectangle will be stretched to fill the destination rect. - /// - /// The tile mode. - /// Controls the quality of interpolation. - public ImmutableVisualBrush( - Visual? visual, - AlignmentX alignmentX = AlignmentX.Center, - AlignmentY alignmentY = AlignmentY.Center, - RelativeRect? destinationRect = null, - double opacity = 1, - ImmutableTransform? transform = null, - RelativePoint transformOrigin = default, - RelativeRect? sourceRect = null, - Stretch stretch = Stretch.Uniform, - TileMode tileMode = TileMode.None, - BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default) - : base( - alignmentX, - alignmentY, - destinationRect ?? RelativeRect.Fill, - opacity, - transform, - transformOrigin, - sourceRect ?? RelativeRect.Fill, - stretch, - tileMode, - bitmapInterpolationMode) - { - Visual = visual; - } - - /// - /// Initializes a new instance of the class. - /// - /// The brush from which this brush's properties should be copied. - public ImmutableVisualBrush(IVisualBrush source) - : base(source) - { - Visual = source.Visual; - } - - /// - public Visual? Visual { get; } - } -} diff --git a/src/Avalonia.Base/Media/PlatformDrawingContext.cs b/src/Avalonia.Base/Media/PlatformDrawingContext.cs new file mode 100644 index 0000000000..eb8a93722c --- /dev/null +++ b/src/Avalonia.Base/Media/PlatformDrawingContext.cs @@ -0,0 +1,112 @@ +using System; +using System.Collections.Generic; +using Avalonia.Media.Imaging; +using Avalonia.Media.Immutable; +using Avalonia.Platform; +using Avalonia.Rendering.SceneGraph; +using Avalonia.Threading; +using Avalonia.Utilities; + +namespace Avalonia.Media; + +internal sealed class PlatformDrawingContext : DrawingContext, IDrawingContextWithAcrylicLikeSupport +{ + private readonly IDrawingContextImpl _impl; + private readonly bool _ownsImpl; + private static ThreadSafeObjectPool> TransformStackPool { get; } = + ThreadSafeObjectPool>.Default; + + private Stack? _transforms; + + + public PlatformDrawingContext(IDrawingContextImpl impl, bool ownsImpl = true) + { + _impl = impl; + _ownsImpl = ownsImpl; + } + + protected override void DrawLineCore(IPen pen, Point p1, Point p2) => + _impl.DrawLine(pen, p1, p2); + + protected override void DrawGeometryCore(IBrush? brush, IPen? pen, IGeometryImpl geometry) => + _impl.DrawGeometry(brush, pen, geometry); + + protected override void DrawRectangleCore(IBrush? brush, IPen? pen, RoundedRect rrect, + BoxShadows boxShadows = default) => + _impl.DrawRectangle(brush, pen, rrect, boxShadows); + + protected override void DrawEllipseCore(IBrush? brush, IPen? pen, Rect rect) => _impl.DrawEllipse(brush, pen, rect); + + internal override void DrawBitmap(IRef source, double opacity, Rect sourceRect, Rect destRect, + BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default) => + _impl.DrawBitmap(source, opacity, sourceRect, destRect, bitmapInterpolationMode); + + public override void Custom(ICustomDrawOperation custom) => + custom.Render(_impl); + + public override void DrawGlyphRun(IBrush? foreground, GlyphRun glyphRun) + { + _ = glyphRun ?? throw new ArgumentNullException(nameof(glyphRun)); + + if (foreground != null) + _impl.DrawGlyphRun(foreground, glyphRun.PlatformImpl); + } + + protected override void PushClipCore(RoundedRect rect) => _impl.PushClip(rect); + + protected override void PushClipCore(Rect rect) => _impl.PushClip(rect); + + protected override void PushGeometryClipCore(Geometry clip) => + _impl.PushGeometryClip(clip.PlatformImpl ?? throw new ArgumentException()); + + protected override void PushOpacityCore(double opacity, Rect bounds) => + _impl.PushOpacity(opacity, bounds); + + protected override void PushOpacityMaskCore(IBrush mask, Rect bounds) => + _impl.PushOpacityMask(mask, bounds); + + protected override void PushBitmapBlendModeCore(BitmapBlendingMode blendingMode) => + _impl.PushBitmapBlendMode(blendingMode); + + protected override void PushTransformCore(Matrix matrix) + { + _transforms ??= TransformStackPool.Get(); + var current = _impl.Transform; + _transforms.Push(current); + _impl.Transform = matrix * current; + } + + protected override void PopClipCore() => _impl.PopClip(); + + protected override void PopGeometryClipCore() => _impl.PopGeometryClip(); + + protected override void PopOpacityCore() => _impl.PopOpacity(); + + protected override void PopOpacityMaskCore() => _impl.PopOpacityMask(); + + protected override void PopBitmapBlendModeCore() => _impl.PopBitmapBlendMode(); + + protected override void PopTransformCore() => + _impl.Transform = + (_transforms ?? throw new ObjectDisposedException(nameof(PlatformDrawingContext))).Pop(); + + protected override void DisposeCore() + { + if (_ownsImpl) + _impl.Dispose(); + if (_transforms != null) + { + if (_transforms.Count != 0) + throw new InvalidOperationException("Not all states are disposed"); + TransformStackPool.ReturnAndSetNull(ref _transforms); + } + } + + public void DrawRectangle(IExperimentalAcrylicMaterial material, RoundedRect rect) + { + if (_impl is IDrawingContextWithAcrylicLikeSupport idc) + idc.DrawRectangle(material, rect); + else + DrawRectangle(new ImmutableSolidColorBrush(material.FallbackColor), null, rect); + } +} diff --git a/src/Avalonia.Base/Media/TextCollapsingCreateInfo.cs b/src/Avalonia.Base/Media/TextCollapsingCreateInfo.cs index 40ba613717..9b7bf3f74c 100644 --- a/src/Avalonia.Base/Media/TextCollapsingCreateInfo.cs +++ b/src/Avalonia.Base/Media/TextCollapsingCreateInfo.cs @@ -6,11 +6,13 @@ namespace Avalonia.Media { public readonly double Width; public readonly TextRunProperties TextRunProperties; + public readonly FlowDirection FlowDirection; - public TextCollapsingCreateInfo(double width, TextRunProperties textRunProperties) + public TextCollapsingCreateInfo(double width, TextRunProperties textRunProperties, FlowDirection flowDirection) { Width = width; TextRunProperties = textRunProperties; + FlowDirection = flowDirection; } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs b/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs index 0d85f3e7c5..c1b9b77401 100644 --- a/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs +++ b/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs @@ -27,16 +27,6 @@ namespace Avalonia.Media.TextFormatting return; } - if (lineImpl.NewLineLength > 0) - { - return; - } - - if (lineImpl.TextLineBreak is { TextEndOfLine: not null, IsSplit: false }) - { - return; - } - var breakOportunities = new Queue(); var currentPosition = textLine.FirstTextSourceIndex; @@ -97,7 +87,8 @@ namespace Avalonia.Media.TextFormatting continue; } - var glyphIndex = glyphRun.FindGlyphIndex(characterIndex); + var offset = Math.Max(0, currentPosition - glyphRun.Metrics.FirstCluster); + var glyphIndex = glyphRun.FindGlyphIndex(characterIndex - offset); var glyphInfo = shapedBuffer.GlyphInfos[glyphIndex]; shapedBuffer.GlyphInfos[glyphIndex] = new GlyphInfo(glyphInfo.GlyphIndex, diff --git a/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs b/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs index 7f23ac98b4..568148e15c 100644 --- a/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs +++ b/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs @@ -148,33 +148,38 @@ namespace Avalonia.Media.TextFormatting internal SplitResult Split(int length) { - if (IsReversed) + var isReversed = IsReversed; + + if (isReversed) { Reverse(); - } + length = Length - length; + } #if DEBUG - if(length == 0) + if (length == 0) { throw new ArgumentOutOfRangeException(nameof(length), "length must be greater than zero."); } -#endif - +#endif var splitBuffer = ShapedBuffer.Split(length); var first = new ShapedTextRun(splitBuffer.First, Properties); - #if DEBUG +#if DEBUG if (first.Length != length) { throw new InvalidOperationException("Split length mismatch."); } - #endif - var second = new ShapedTextRun(splitBuffer.Second!, Properties); + if (isReversed) + { + return new SplitResult(second, first); + } + return new SplitResult(first, second); } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextCollapsingProperties.cs b/src/Avalonia.Base/Media/TextFormatting/TextCollapsingProperties.cs index 72882df0b5..7cdf81ecc9 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextCollapsingProperties.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextCollapsingProperties.cs @@ -1,4 +1,6 @@ -namespace Avalonia.Media.TextFormatting +using System.Collections.Generic; + +namespace Avalonia.Media.TextFormatting { /// /// Properties of text collapsing. @@ -15,6 +17,11 @@ /// public abstract TextRun Symbol { get; } + /// + /// Gets the flow direction that is used for collapsing. + /// + public abstract FlowDirection FlowDirection { get; } + /// /// Collapses given text line. /// diff --git a/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs b/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs index 4c93a1d851..6422f23dcd 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using Avalonia.Media.TextFormatting.Unicode; namespace Avalonia.Media.TextFormatting @@ -28,97 +27,191 @@ namespace Avalonia.Media.TextFormatting var availableWidth = properties.Width - shapedSymbol.Size.Width; - while (runIndex < textRuns.Count) + if(properties.FlowDirection== FlowDirection.LeftToRight) { - var currentRun = textRuns[runIndex]; - - switch (currentRun) + while (runIndex < textRuns.Count) { - case ShapedTextRun shapedRun: - { - currentWidth += shapedRun.Size.Width; + var currentRun = textRuns[runIndex]; - if (currentWidth > availableWidth) + switch (currentRun) + { + case ShapedTextRun shapedRun: { - 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; - - var lineBreaker = new LineBreakEnumerator(currentRun.Text.Span); - - while (currentBreakPosition < measuredLength && lineBreaker.MoveNext(out var lineBreak)) + if (isWordEllipsis && measuredLength < textLine.Length) { - var nextBreakPosition = lineBreak.PositionMeasure; + var currentBreakPosition = 0; - if (nextBreakPosition == 0) - { - break; - } + var lineBreaker = new LineBreakEnumerator(currentRun.Text.Span); - if (nextBreakPosition >= measuredLength) + while (currentBreakPosition < measuredLength && lineBreaker.MoveNext(out var lineBreak)) { - break; + var nextBreakPosition = lineBreak.PositionMeasure; + + if (nextBreakPosition == 0) + { + break; + } + + if (nextBreakPosition >= measuredLength) + { + break; + } + + currentBreakPosition = nextBreakPosition; } - currentBreakPosition = nextBreakPosition; + measuredLength = currentBreakPosition; } - - measuredLength = currentBreakPosition; } + + collapsedLength += measuredLength; + + return CreateCollapsedRuns(textLine, collapsedLength, FlowDirection.LeftToRight, shapedSymbol); } - collapsedLength += measuredLength; + availableWidth -= shapedRun.Size.Width; - return CreateCollapsedRuns(textRuns, collapsedLength, shapedSymbol); + break; } - availableWidth -= shapedRun.Size.Width; + case DrawableTextRun drawableRun: + { + //The whole run needs to fit into available space + if (currentWidth + drawableRun.Size.Width > availableWidth) + { + return CreateCollapsedRuns(textLine, collapsedLength, FlowDirection.LeftToRight, shapedSymbol); + } - break; - } + availableWidth -= drawableRun.Size.Width; - case DrawableTextRun drawableRun: - { - //The whole run needs to fit into available space - if (currentWidth + drawableRun.Size.Width > availableWidth) - { - return CreateCollapsedRuns(textRuns, collapsedLength, shapedSymbol); + break; } + } - availableWidth -= drawableRun.Size.Width; + collapsedLength += currentRun.Length; - break; - } + runIndex++; } + } + else + { + runIndex = textRuns.Count - 1; + + while (runIndex >= 0) + { + var currentRun = textRuns[runIndex]; - collapsedLength += currentRun.Length; + switch (currentRun) + { + case ShapedTextRun shapedRun: + { + currentWidth += shapedRun.Size.Width; - runIndex++; - } + if (currentWidth > availableWidth) + { + if (shapedRun.TryMeasureCharacters(availableWidth, out var measuredLength)) + { + if (isWordEllipsis && measuredLength < textLine.Length) + { + var currentBreakPosition = 0; + + var lineBreaker = new LineBreakEnumerator(currentRun.Text.Span); + + while (currentBreakPosition < measuredLength && lineBreaker.MoveNext(out var lineBreak)) + { + var nextBreakPosition = lineBreak.PositionMeasure; + + if (nextBreakPosition == 0) + { + break; + } + + if (nextBreakPosition >= measuredLength) + { + break; + } + currentBreakPosition = nextBreakPosition; + } + + measuredLength = currentBreakPosition; + } + } + + collapsedLength += measuredLength; + + return CreateCollapsedRuns(textLine, collapsedLength, FlowDirection.RightToLeft, shapedSymbol); + } + + availableWidth -= shapedRun.Size.Width; + + break; + } + + case DrawableTextRun drawableRun: + { + //The whole run needs to fit into available space + if (currentWidth + drawableRun.Size.Width > availableWidth) + { + return CreateCollapsedRuns(textLine, collapsedLength, FlowDirection.RightToLeft, shapedSymbol); + } + + availableWidth -= drawableRun.Size.Width; + + break; + } + } + + collapsedLength += currentRun.Length; + + runIndex--; + } + } + return null; } - private static TextRun[] CreateCollapsedRuns(IReadOnlyList textRuns, int collapsedLength, - TextRun shapedSymbol) + private static TextRun[] CreateCollapsedRuns(TextLine textLine, int collapsedLength, + FlowDirection flowDirection, TextRun shapedSymbol) { + var textRuns = textLine.TextRuns; + if (collapsedLength <= 0) { return new[] { shapedSymbol }; } + if(flowDirection == FlowDirection.RightToLeft) + { + collapsedLength = textLine.Length - collapsedLength; + } + var objectPool = FormattingObjectPool.Instance; var (preSplitRuns, postSplitRuns) = TextFormatterImpl.SplitTextRuns(textRuns, collapsedLength, objectPool); try { - var collapsedRuns = new TextRun[preSplitRuns.Count + 1]; - preSplitRuns.CopyTo(collapsedRuns); - collapsedRuns[collapsedRuns.Length - 1] = shapedSymbol; - return collapsedRuns; + if (flowDirection == FlowDirection.RightToLeft) + { + var collapsedRuns = new TextRun[postSplitRuns!.Count + 1]; + postSplitRuns.CopyTo(collapsedRuns, 1); + collapsedRuns[0] = shapedSymbol; + return collapsedRuns; + } + else + { + var collapsedRuns = new TextRun[preSplitRuns!.Count + 1]; + preSplitRuns.CopyTo(collapsedRuns); + collapsedRuns[collapsedRuns.Length - 1] = shapedSymbol; + return collapsedRuns; + } } finally { diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs index 4dbc472133..a382416b8a 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs @@ -352,7 +352,7 @@ namespace Avalonia.Media.TextFormatting var lastTrailingIndex = 0; - if(_paragraphProperties.FlowDirection== FlowDirection.LeftToRight) + if (_paragraphProperties.FlowDirection == FlowDirection.LeftToRight) { lastTrailingIndex = textLine.FirstTextSourceIndex + textLine.Length; @@ -377,7 +377,7 @@ namespace Avalonia.Media.TextFormatting { lastTrailingIndex += textEndOfLine.Length; } - } + } var textPosition = characterHit.FirstCharacterIndex + characterHit.TrailingLength; @@ -553,26 +553,18 @@ namespace Avalonia.Media.TextFormatting if (_paragraphProperties.TextAlignment == TextAlignment.Justify) { - var whitespaceWidth = 0d; + var justificationWidth = MaxWidth; - for (var i = 0; i < textLines.Count; i++) + if (_paragraphProperties.TextWrapping != TextWrapping.NoWrap) { - var line = textLines[i]; - var lineWhitespaceWidth = line.Width - line.WidthIncludingTrailingWhitespace; - - if (lineWhitespaceWidth > whitespaceWidth) - { - whitespaceWidth = lineWhitespaceWidth; - } + justificationWidth = width; } - var justificationWidth = width - whitespaceWidth; - if (justificationWidth > 0) { var justificationProperties = new InterWordJustification(justificationWidth); - for (var i = 0; i < textLines.Count - 1; i++) + for (var i = 0; i < textLines.Count; i++) { var line = textLines[i]; @@ -597,12 +589,13 @@ namespace Avalonia.Media.TextFormatting /// The . private TextCollapsingProperties? GetCollapsingProperties(double width) { - if(_textTrimming == TextTrimming.None) + if (_textTrimming == TextTrimming.None) { return null; } - return _textTrimming.CreateCollapsingProperties(new TextCollapsingCreateInfo(width, _paragraphProperties.DefaultTextRunProperties)); + return _textTrimming.CreateCollapsingProperties( + new TextCollapsingCreateInfo(width, _paragraphProperties.DefaultTextRunProperties, _paragraphProperties.FlowDirection)); } public void Dispose() diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs b/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs index 2e85b1e187..a21a5d45e9 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs @@ -19,11 +19,13 @@ namespace Avalonia.Media.TextFormatting /// Length of leading prefix. /// width in which collapsing is constrained to /// text run properties of ellipsis symbol + /// the flow direction of the collapes line. public TextLeadingPrefixCharacterEllipsis( string ellipsis, int prefixLength, double width, - TextRunProperties textRunProperties) + TextRunProperties textRunProperties, + FlowDirection flowDirection) { if (_prefixLength < 0) { @@ -33,6 +35,7 @@ namespace Avalonia.Media.TextFormatting _prefixLength = prefixLength; Width = width; Symbol = new TextCharacters(ellipsis, textRunProperties); + FlowDirection = flowDirection; } /// @@ -41,6 +44,8 @@ namespace Avalonia.Media.TextFormatting /// public override TextRun Symbol { get; } + public override FlowDirection FlowDirection { get; } + /// public override TextRun[]? Collapse(TextLine textLine) { diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index 187b3154ad..b3321d4d9f 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -658,7 +658,7 @@ namespace Avalonia.Media.TextFormatting currentX += drawableTextRun.Size.Width; } - if(lastRunIndex - 1 < 0) + if (lastRunIndex - 1 < 0) { break; } @@ -685,7 +685,7 @@ namespace Avalonia.Media.TextFormatting directionalWidth -= drawableTextRun.Size.Width; } - if(firstRunIndex + 1 == _textRuns.Length) + if (firstRunIndex + 1 == _textRuns.Length) { break; } @@ -1097,7 +1097,7 @@ namespace Avalonia.Media.TextFormatting var runWidth = endX - startX; - return new TextRunBounds(new Rect(Start + startX, 0, runWidth, Height), currentPosition, characterLength, currentRun); + return new TextRunBounds(new Rect(startX, 0, runWidth, Height), currentPosition, characterLength, currentRun); } public override void Dispose() @@ -1439,13 +1439,6 @@ namespace Avalonia.Media.TextFormatting } } - if (index == lastRunIndex) - { - width = widthIncludingWhitespace + textRun.GlyphRun.Metrics.Width; - trailingWhitespaceLength = textRun.GlyphRun.Metrics.TrailingWhitespaceLength; - newLineLength += textRun.GlyphRun.Metrics.NewLineLength; - } - widthIncludingWhitespace += textRun.Size.Width; break; @@ -1455,12 +1448,6 @@ namespace Avalonia.Media.TextFormatting { widthIncludingWhitespace += drawableTextRun.Size.Width; - if (index == lastRunIndex) - { - width = widthIncludingWhitespace; - trailingWhitespaceLength = 0; - } - if (drawableTextRun.Size.Height > height) { height = drawableTextRun.Size.Height; @@ -1476,6 +1463,32 @@ namespace Avalonia.Media.TextFormatting } } + width = widthIncludingWhitespace; + + for (var i = _textRuns.Length - 1; i >= 0; i--) + { + var currentRun = _textRuns[i]; + + if(currentRun is ShapedTextRun shapedText) + { + var glyphRun = shapedText.GlyphRun; + var glyphRunMetrics = glyphRun.Metrics; + + newLineLength += glyphRunMetrics.NewLineLength; + + if (glyphRunMetrics.TrailingWhitespaceLength == 0) + { + break; + } + + trailingWhitespaceLength += glyphRunMetrics.TrailingWhitespaceLength; + + var whitespaceWidth = glyphRun.Size.Width - glyphRunMetrics.Width; + + width -= whitespaceWidth; + } + } + var start = GetParagraphOffsetX(width, widthIncludingWhitespace); if (!double.IsNaN(lineHeight) && !MathUtilities.IsZero(lineHeight)) @@ -1543,7 +1556,7 @@ namespace Avalonia.Media.TextFormatting return Math.Max(0, start); case TextAlignment.Right: - return Math.Max(0, _paragraphWidth - widthIncludingTrailingWhitespace); + return Math.Max(0, _paragraphWidth - width); default: return 0; diff --git a/src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs b/src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs index ccae99cc75..8a6607bce2 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs @@ -12,10 +12,13 @@ /// Text used as collapsing symbol. /// Width in which collapsing is constrained to. /// Text run properties of ellipsis symbol. - public TextTrailingCharacterEllipsis(string ellipsis, double width, TextRunProperties textRunProperties) + /// The flow direction of the collapsed line. + public TextTrailingCharacterEllipsis(string ellipsis, double width, + TextRunProperties textRunProperties, FlowDirection flowDirection) { Width = width; Symbol = new TextCharacters(ellipsis, textRunProperties); + FlowDirection = flowDirection; } /// @@ -24,6 +27,8 @@ /// public override TextRun Symbol { get; } + public override FlowDirection FlowDirection { get; } + /// public override TextRun[]? Collapse(TextLine textLine) { diff --git a/src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs b/src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs index c622c76a60..5252766382 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs @@ -12,14 +12,17 @@ /// Text used as collapsing symbol. /// width in which collapsing is constrained to. /// text run properties of ellipsis symbol. + /// flow direction of the collapsed line. public TextTrailingWordEllipsis( string ellipsis, double width, - TextRunProperties textRunProperties + TextRunProperties textRunProperties, + FlowDirection flowDirection ) { Width = width; Symbol = new TextCharacters(ellipsis, textRunProperties); + FlowDirection = flowDirection; } /// @@ -28,6 +31,8 @@ /// public override TextRun Symbol { get; } + public override FlowDirection FlowDirection { get; } + /// public override TextRun[]? Collapse(TextLine textLine) { diff --git a/src/Avalonia.Base/Media/TextLeadingPrefixTrimming.cs b/src/Avalonia.Base/Media/TextLeadingPrefixTrimming.cs index 7ba25eb005..19e6a70357 100644 --- a/src/Avalonia.Base/Media/TextLeadingPrefixTrimming.cs +++ b/src/Avalonia.Base/Media/TextLeadingPrefixTrimming.cs @@ -15,7 +15,7 @@ namespace Avalonia.Media public override TextCollapsingProperties CreateCollapsingProperties(TextCollapsingCreateInfo createInfo) { - return new TextLeadingPrefixCharacterEllipsis(_ellipsis, _prefixLength, createInfo.Width, createInfo.TextRunProperties); + return new TextLeadingPrefixCharacterEllipsis(_ellipsis, _prefixLength, createInfo.Width, createInfo.TextRunProperties, createInfo.FlowDirection); } public override string ToString() diff --git a/src/Avalonia.Base/Media/TextTrailingTrimming.cs b/src/Avalonia.Base/Media/TextTrailingTrimming.cs index 2edbaabbc6..8a3c5aa397 100644 --- a/src/Avalonia.Base/Media/TextTrailingTrimming.cs +++ b/src/Avalonia.Base/Media/TextTrailingTrimming.cs @@ -17,10 +17,10 @@ namespace Avalonia.Media { if (_isWordBased) { - return new TextTrailingWordEllipsis(_ellipsis, createInfo.Width, createInfo.TextRunProperties); + return new TextTrailingWordEllipsis(_ellipsis, createInfo.Width, createInfo.TextRunProperties, createInfo.FlowDirection); } - return new TextTrailingCharacterEllipsis(_ellipsis, createInfo.Width, createInfo.TextRunProperties); + return new TextTrailingCharacterEllipsis(_ellipsis, createInfo.Width, createInfo.TextRunProperties, createInfo.FlowDirection); } public override string ToString() diff --git a/src/Avalonia.Base/Media/VisualBrush.cs b/src/Avalonia.Base/Media/VisualBrush.cs index 2be3e9a94e..6bfe20271f 100644 --- a/src/Avalonia.Base/Media/VisualBrush.cs +++ b/src/Avalonia.Base/Media/VisualBrush.cs @@ -1,11 +1,14 @@ using Avalonia.Media.Immutable; +using Avalonia.Rendering; +using Avalonia.Rendering.Composition; +using Avalonia.Rendering.Composition.Drawing; namespace Avalonia.Media { /// /// Paints an area with an . /// - public class VisualBrush : TileBrush, IVisualBrush, IMutableBrush + public class VisualBrush : TileBrush, ISceneBrush, IAffectsRender { /// /// Defines the property. @@ -43,10 +46,23 @@ namespace Avalonia.Media set { SetValue(VisualProperty, value); } } - /// - IImmutableBrush IMutableBrush.ToImmutable() + ISceneBrushContent? ISceneBrush.CreateContent() { - return new ImmutableVisualBrush(this); + if (Visual == null) + return null; + + if (Visual is IVisualBrushInitialize initialize) + initialize.EnsureInitialized(); + + var recorder = new CompositionDrawingContext(); + recorder.BeginUpdate(null); + ImmediateRenderer.Render(recorder, Visual, Visual.Bounds); + var drawList = recorder.EndUpdate(); + if (drawList == null) + return null; + + return new CompositionDrawListSceneBrushContent(new ImmutableSceneBrush(this), drawList, + new(Visual.Bounds.Size), false); } } } diff --git a/src/Avalonia.Base/Platform/IRenderTarget.cs b/src/Avalonia.Base/Platform/IRenderTarget.cs index 73e9e58da4..31ad84341d 100644 --- a/src/Avalonia.Base/Platform/IRenderTarget.cs +++ b/src/Avalonia.Base/Platform/IRenderTarget.cs @@ -14,11 +14,7 @@ namespace Avalonia.Platform /// /// Creates an for a rendering session. /// - /// - /// A render to be used to render visual brushes. May be null if no visual brushes are - /// to be drawn. - /// - IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer? visualBrushRenderer); + IDrawingContextImpl CreateDrawingContext(); /// /// Indicates if the render target is no longer usable and needs to be recreated diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs index 5bf9ff9d9a..543fb0ab74 100644 --- a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs +++ b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs @@ -7,11 +7,6 @@ namespace Avalonia.Platform.Storage.FileIO; internal class BclStorageFile : IStorageBookmarkFile { - public BclStorageFile(string fileName) - { - FileInfo = new FileInfo(fileName); - } - public BclStorageFile(FileInfo fileInfo) { FileInfo = fileInfo ?? throw new ArgumentNullException(nameof(fileInfo)); diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs index 1e21c197bb..d8e3d91f75 100644 --- a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs +++ b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs @@ -9,15 +9,6 @@ namespace Avalonia.Platform.Storage.FileIO; internal class BclStorageFolder : IStorageBookmarkFolder { - public BclStorageFolder(string path) - { - DirectoryInfo = new DirectoryInfo(path); - if (!DirectoryInfo.Exists) - { - throw new ArgumentException("Directory must exist"); - } - } - public BclStorageFolder(DirectoryInfo directoryInfo) { DirectoryInfo = directoryInfo ?? throw new ArgumentNullException(nameof(directoryInfo)); diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/StorageProviderHelpers.cs b/src/Avalonia.Base/Platform/Storage/FileIO/StorageProviderHelpers.cs index 55e84ee937..a8cbffb417 100644 --- a/src/Avalonia.Base/Platform/Storage/FileIO/StorageProviderHelpers.cs +++ b/src/Avalonia.Base/Platform/Storage/FileIO/StorageProviderHelpers.cs @@ -7,6 +7,23 @@ namespace Avalonia.Platform.Storage.FileIO; internal static class StorageProviderHelpers { + public static IStorageItem? TryCreateBclStorageItem(string path) + { + var directory = new DirectoryInfo(path); + if (directory.Exists) + { + return new BclStorageFolder(directory); + } + + var file = new FileInfo(path); + if (file.Exists) + { + return new BclStorageFile(file); + } + + return null; + } + public static Uri FilePathToUri(string path) { var uriPath = new StringBuilder(path) diff --git a/src/Avalonia.Base/Platform/Storage/PickerOptions.cs b/src/Avalonia.Base/Platform/Storage/PickerOptions.cs index 6f97916a26..ed061aa2d5 100644 --- a/src/Avalonia.Base/Platform/Storage/PickerOptions.cs +++ b/src/Avalonia.Base/Platform/Storage/PickerOptions.cs @@ -12,6 +12,8 @@ public class PickerOptions /// /// Gets or sets the initial location where the file open picker looks for files to present to the user. + /// Can be obtained from previously picked folder or using + /// or . /// public IStorageFolder? SuggestedStartLocation { get; set; } } diff --git a/src/Avalonia.Base/Platform/Storage/StorageProviderExtensions.cs b/src/Avalonia.Base/Platform/Storage/StorageProviderExtensions.cs index 6f8b945cd6..1febb4506a 100644 --- a/src/Avalonia.Base/Platform/Storage/StorageProviderExtensions.cs +++ b/src/Avalonia.Base/Platform/Storage/StorageProviderExtensions.cs @@ -11,12 +11,24 @@ public static class StorageProviderExtensions /// public static Task TryGetFileFromPathAsync(this IStorageProvider provider, string filePath) { + // We can avoid double escaping of the path by checking for BclStorageProvider. + if (provider is BclStorageProvider) + { + return Task.FromResult(StorageProviderHelpers.TryCreateBclStorageItem(filePath) as IStorageFile); + } + return provider.TryGetFileFromPathAsync(StorageProviderHelpers.FilePathToUri(filePath)); } /// public static Task TryGetFolderFromPathAsync(this IStorageProvider provider, string folderPath) { + // We can avoid double escaping of the path by checking for BclStorageProvider. + if (provider is BclStorageProvider) + { + return Task.FromResult(StorageProviderHelpers.TryCreateBclStorageItem(folderPath) as IStorageFolder); + } + return provider.TryGetFolderFromPathAsync(StorageProviderHelpers.FilePathToUri(folderPath)); } diff --git a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs index 7fa2d4955f..01299e4ffa 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs @@ -55,7 +55,7 @@ public class CompositingRenderer : IRendererWithCompositor { _root = root; _compositor = compositor; - _recordingContext = new DrawingContext(_recorder); + _recordingContext = _recorder; CompositionTarget = compositor.CreateCompositionTarget(surfaces); CompositionTarget.Root = ((Visual)root).AttachToCompositor(compositor); _update = Update; diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawList.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawList.cs index 10a7c3e360..5d45a725c1 100644 --- a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawList.cs +++ b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawList.cs @@ -1,5 +1,6 @@ using System; using Avalonia.Collections.Pooled; +using Avalonia.Platform; using Avalonia.Rendering.Composition.Server; using Avalonia.Rendering.SceneGraph; using Avalonia.Utilities; @@ -13,8 +14,6 @@ namespace Avalonia.Rendering.Composition.Drawing; /// internal class CompositionDrawList : PooledList> { - public Size? Size { get; set; } - public CompositionDrawList() { @@ -34,21 +33,47 @@ internal class CompositionDrawList : PooledList> public CompositionDrawList Clone() { - var clone = new CompositionDrawList(Count) { Size = Size }; + var clone = new CompositionDrawList(Count); foreach (var r in this) clone.Add(r.Clone()); return clone; } - public void Render(CompositorDrawingContextProxy canvas) + public void Render(IDrawingContextImpl canvas) + { + foreach (var cmd in this) + { + if (cmd.Item is IDrawOperationWithTransform hasTransform) + canvas.Transform = hasTransform.Transform; + cmd.Item.Render(canvas); + } + } + + public void Render(IDrawingContextImpl canvas, Matrix transform) { foreach (var cmd in this) { - canvas.VisualBrushDrawList = (cmd.Item as BrushDrawOperation)?.Aux as CompositionDrawList; + if (cmd.Item is IDrawOperationWithTransform hasTransform) + canvas.Transform = hasTransform.Transform * transform; cmd.Item.Render(canvas); } + } + - canvas.VisualBrushDrawList = null; + public Rect CalculateBounds() + { + var rect = default(Rect); + foreach (var cmd in this) + rect = rect.Union(cmd.Item.Bounds); + return rect; + } + + public bool HitTest(Point pt) + { + foreach (var op in this) + if (op.Item.HitTest(pt)) + return true; + return false; } } diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawListSceneBrushContent.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawListSceneBrushContent.cs new file mode 100644 index 0000000000..85bb156475 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawListSceneBrushContent.cs @@ -0,0 +1,37 @@ +using Avalonia.Media; +using Avalonia.Media.Immutable; +using Avalonia.Platform; + +namespace Avalonia.Rendering.Composition.Drawing; + +internal class CompositionDrawListSceneBrushContent : ISceneBrushContent +{ + private readonly CompositionDrawList _drawList; + + public CompositionDrawListSceneBrushContent(ImmutableTileBrush brush, CompositionDrawList drawList, Rect rect, bool useScalableRasterization) + { + Brush = brush; + Rect = rect; + UseScalableRasterization = useScalableRasterization; + _drawList = drawList; + } + + public ITileBrush Brush { get; } + public Rect Rect { get; } + + public double Opacity => Brush.Opacity; + public ITransform? Transform => Brush.Transform; + public RelativePoint TransformOrigin => Brush.TransformOrigin; + + public void Dispose() => _drawList.Dispose(); + + public void Render(IDrawingContextImpl context, Matrix? transform) + { + if (transform.HasValue) + _drawList.Render(context, transform.Value); + else + _drawList.Render(context); + } + + public bool UseScalableRasterization { get; } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs index 6b380608fe..f81cc5a1a0 100644 --- a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs +++ b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Numerics; using Avalonia.Media; using Avalonia.Media.Imaging; @@ -7,7 +8,7 @@ using Avalonia.Platform; using Avalonia.Rendering.Composition.Drawing; using Avalonia.Rendering.SceneGraph; using Avalonia.Utilities; -using Avalonia.VisualTree; +using Avalonia.Threading; // Special license applies License.md @@ -16,46 +17,60 @@ namespace Avalonia.Rendering.Composition; /// /// An IDrawingContextImpl implementation that builds /// -internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextWithAcrylicLikeSupport +internal sealed class CompositionDrawingContext : DrawingContext, IDrawingContextWithAcrylicLikeSupport { private CompositionDrawListBuilder _builder = new(); private int _drawOperationIndex; + + private static ThreadSafeObjectPool> TransformStackPool { get; } = + ThreadSafeObjectPool>.Default; - /// - public Matrix Transform { get; set; } = Matrix.Identity; + private Stack? _transforms; - /// - public void Clear(Color color) - { - // Cannot clear a deferred scene. - } + private static ThreadSafeObjectPool> OpacityMaskPopStackPool { get; } = + ThreadSafeObjectPool>.Default; - /// - public void Dispose() - { - // Nothing to do here since we allocate no unmanaged resources. - } + private Stack? _needsToPopOpacityMask; + public Matrix Transform { get; set; } = Matrix.Identity; + public void BeginUpdate(CompositionDrawList? list) { _builder.Reset(list); _drawOperationIndex = 0; } - public CompositionDrawList EndUpdate() + public CompositionDrawList? EndUpdate() { + // Make sure that any pending pop operations are completed + Dispose(); + _builder.TrimTo(_drawOperationIndex); - return _builder.DrawOperations!; + return _builder.DrawOperations; } + + protected override void DisposeCore() + { + if (_transforms != null) + { + _transforms.Clear(); + TransformStackPool.ReturnAndSetNull(ref _transforms); + } - /// - public void DrawGeometry(IBrush? brush, IPen? pen, IGeometryImpl geometry) + if (_needsToPopOpacityMask != null) + { + _needsToPopOpacityMask.Clear(); + _needsToPopOpacityMask = null; + } + } + + protected override void DrawGeometryCore(IBrush? brush, IPen? pen, IGeometryImpl geometry) { var next = NextDrawAs(); if (next == null || !next.Item.Equals(Transform, brush, pen, geometry)) { - Add(new GeometryNode(Transform, brush, pen, geometry, CreateChildScene(brush))); + Add(new GeometryNode(Transform, ConvertBrush(brush), pen, geometry)); } else { @@ -63,9 +78,8 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW } } - /// - public void DrawBitmap(IRef source, double opacity, Rect sourceRect, Rect destRect, - BitmapInterpolationMode bitmapInterpolationMode) + internal override void DrawBitmap(IRef source, double opacity, Rect sourceRect, Rect destRect, + BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default) { var next = NextDrawAs(); @@ -81,14 +95,7 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW } /// - public void DrawBitmap(IRef source, IBrush opacityMask, Rect opacityMaskRect, Rect sourceRect) - { - // This method is currently only used to composite layers so shouldn't be called here. - throw new NotSupportedException(); - } - - /// - public void DrawLine(IPen? pen, Point p1, Point p2) + protected override void DrawLineCore(IPen? pen, Point p1, Point p2) { if (pen is null) { @@ -99,7 +106,7 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW if (next == null || !next.Item.Equals(Transform, pen, p1, p2)) { - Add(new LineNode(Transform, pen, p1, p2, CreateChildScene(pen.Brush))); + Add(new LineNode(Transform, pen, p1, p2)); } else { @@ -108,14 +115,14 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW } /// - public void DrawRectangle(IBrush? brush, IPen? pen, RoundedRect rect, + protected override void DrawRectangleCore(IBrush? brush, IPen? pen, RoundedRect rect, BoxShadows boxShadows = default) { var next = NextDrawAs(); if (next == null || !next.Item.Equals(Transform, brush, pen, rect, boxShadows)) { - Add(new RectangleNode(Transform, brush, pen, rect, boxShadows, CreateChildScene(brush))); + Add(new RectangleNode(Transform, ConvertBrush(brush), pen, rect, boxShadows)); } else { @@ -138,21 +145,21 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW } } - public void DrawEllipse(IBrush? brush, IPen? pen, Rect rect) + protected override void DrawEllipseCore(IBrush? brush, IPen? pen, Rect rect) { var next = NextDrawAs(); if (next == null || !next.Item.Equals(Transform, brush, pen, rect)) { - Add(new EllipseNode(Transform, brush, pen, rect, CreateChildScene(brush))); + Add(new EllipseNode(Transform, ConvertBrush(brush), pen, rect)); } else { ++_drawOperationIndex; } } - - public void Custom(ICustomDrawOperation custom) + + public override void Custom(ICustomDrawOperation custom) { var next = NextDrawAs(); if (next == null || !next.Item.Equals(Transform, custom)) @@ -161,10 +168,7 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW ++_drawOperationIndex; } - public object? GetFeature(Type t) => null; - - /// - public void DrawGlyphRun(IBrush? foreground, IRef glyphRun) + public override void DrawGlyphRun(IBrush? foreground, GlyphRun glyphRun) { if (foreground is null) { @@ -173,9 +177,9 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW var next = NextDrawAs(); - if (next == null || !next.Item.Equals(Transform, foreground, glyphRun)) + if (next == null || !next.Item.Equals(Transform, foreground, glyphRun.PlatformImpl)) { - Add(new GlyphRunNode(Transform, foreground, glyphRun, CreateChildScene(foreground))); + Add(new GlyphRunNode(Transform, ConvertBrush(foreground)!, glyphRun.PlatformImpl)); } else @@ -184,13 +188,17 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW } } - public IDrawingContextLayerImpl CreateLayer(Size size) + protected override void PushTransformCore(Matrix matrix) { - throw new NotSupportedException("Creating layers on a deferred drawing context not supported"); + _transforms ??= TransformStackPool.Get(); + _transforms.Push(Transform); + Transform = matrix * Transform; } + + protected override void PopTransformCore() => + Transform = (_transforms ?? throw new InvalidOperationException()).Pop(); - /// - public void PopClip() + protected override void PopClipCore() { var next = NextDrawAs(); @@ -205,7 +213,7 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW } /// - public void PopGeometryClip() + protected override void PopGeometryClipCore() { var next = NextDrawAs(); @@ -219,8 +227,7 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW } } - /// - public void PopBitmapBlendMode() + protected override void PopBitmapBlendModeCore() { var next = NextDrawAs(); @@ -234,8 +241,7 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW } } - /// - public void PopOpacity() + protected override void PopOpacityCore() { var next = NextDrawAs(); @@ -249,14 +255,16 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW } } - /// - public void PopOpacityMask() + protected override void PopOpacityMaskCore() { + if (!_needsToPopOpacityMask!.Pop()) + return; + var next = NextDrawAs(); if (next == null || !next.Item.Equals(null, null)) { - Add(new OpacityMaskNode()); + Add(new OpacityMaskPopNode()); } else { @@ -264,8 +272,8 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW } } - /// - public void PushClip(Rect clip) + + protected override void PushClipCore(Rect clip) { var next = NextDrawAs(); @@ -279,8 +287,7 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW } } - /// - public void PushClip(RoundedRect clip) + protected override void PushClipCore(RoundedRect clip) { var next = NextDrawAs(); @@ -294,26 +301,24 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW } } - /// - public void PushGeometryClip(IGeometryImpl? clip) + protected override void PushGeometryClipCore(Geometry clip) { - if (clip is null) + if (clip.PlatformImpl is null) return; var next = NextDrawAs(); - if (next == null || !next.Item.Equals(Transform, clip)) + if (next == null || !next.Item.Equals(Transform, clip.PlatformImpl)) { - Add(new GeometryClipNode(Transform, clip)); + Add(new GeometryClipNode(Transform, clip.PlatformImpl)); } else { ++_drawOperationIndex; } } - - /// - public void PushOpacity(double opacity, Rect bounds) + + protected override void PushOpacityCore(double opacity, Rect bounds) { var next = NextDrawAs(); @@ -327,23 +332,30 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW } } - /// - public void PushOpacityMask(IBrush mask, Rect bounds) + protected override void PushOpacityMaskCore(IBrush mask, Rect bounds) { var next = NextDrawAs(); + bool needsToPop = true; if (next == null || !next.Item.Equals(mask, bounds)) { - Add(new OpacityMaskNode(mask, bounds, CreateChildScene(mask))); + var immutableMask = ConvertBrush(mask); + if (immutableMask != null) + Add(new OpacityMaskNode(immutableMask, bounds)); + else + needsToPop = false; } else { ++_drawOperationIndex; } + + _needsToPopOpacityMask ??= OpacityMaskPopStackPool.Get(); + _needsToPopOpacityMask.Push(needsToPop); } /// - public void PushBitmapBlendMode(BitmapBlendingMode blendingMode) + protected override void PushBitmapBlendModeCore(BitmapBlendingMode blendingMode) { var next = NextDrawAs(); @@ -378,29 +390,12 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW : null; } - private static IDisposable? CreateChildScene(IBrush? brush) + private IImmutableBrush? ConvertBrush(IBrush? brush) { - if (brush is VisualBrush visualBrush) - { - var visual = visualBrush.Visual; - - if (visual != null) - { - // TODO: This is a temporary solution to make visual brush to work like it does with DeferredRenderer - // We should directly reference the corresponding CompositionVisual (which should - // be attached to the same composition target) like UWP does. - // Render-able visuals shouldn't be dangling unattached - (visual as IVisualBrushInitialize)?.EnsureInitialized(); - - var recorder = new CompositionDrawingContext(); - recorder.BeginUpdate(null); - ImmediateRenderer.Render(visual, new DrawingContext(recorder)); - var drawList = recorder.EndUpdate(); - drawList.Size = visual.Bounds.Size; - - return drawList; - } - } - return null; + if (brush is IMutableBrush mutable) + return mutable.ToImmutable(); + if (brush is ISceneBrush sceneBrush) + return sceneBrush.CreateContent(); + return (IImmutableBrush?)brush; } } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs index 08e506536f..eaa9a70ca0 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs @@ -21,19 +21,10 @@ namespace Avalonia.Rendering.Composition.Server; internal class CompositorDrawingContextProxy : IDrawingContextImpl, IDrawingContextWithAcrylicLikeSupport { private IDrawingContextImpl _impl; - private readonly VisualBrushRenderer _visualBrushRenderer; - public CompositorDrawingContextProxy(IDrawingContextImpl impl, VisualBrushRenderer visualBrushRenderer) + public CompositorDrawingContextProxy(IDrawingContextImpl impl) { _impl = impl; - _visualBrushRenderer = visualBrushRenderer; - } - - // This is a hack to make it work with the current way of handling visual brushes - public CompositionDrawList? VisualBrushDrawList - { - get => _visualBrushRenderer.VisualBrushDrawList; - set => _visualBrushRenderer.VisualBrushDrawList = value; } public Matrix PostTransform { get; set; } = Matrix.Identity; @@ -157,24 +148,7 @@ internal class CompositorDrawingContextProxy : IDrawingContextImpl, IDrawingCont } public object? GetFeature(Type t) => _impl.GetFeature(t); - - public class VisualBrushRenderer : IVisualBrushRenderer - { - public CompositionDrawList? VisualBrushDrawList { get; set; } - public Size GetRenderTargetSize(IVisualBrush brush) - { - return VisualBrushDrawList?.Size ?? default; - } - - public void RenderVisualBrush(IDrawingContextImpl context, IVisualBrush brush) - { - if (VisualBrushDrawList != null) - { - foreach (var cmd in VisualBrushDrawList) - cmd.Item.Render(context); - } - } - } + public void DrawRectangle(IExperimentalAcrylicMaterial material, RoundedRect rect) { diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs index 63ec8d756b..977acd8470 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs @@ -151,7 +151,7 @@ namespace Avalonia.Rendering.Composition.Server Readback.CompleteWrite(Revision); _redrawRequested = false; - using (var targetContext = _renderTarget.CreateDrawingContext(null)) + using (var targetContext = _renderTarget.CreateDrawingContext()) { var layerSize = Size * Scaling; if (layerSize != _layerSize || _layer == null || _layer.IsCorrupted) @@ -165,12 +165,11 @@ namespace Avalonia.Rendering.Composition.Server if (!_dirtyRect.IsDefault) { - var visualBrushHelper = new CompositorDrawingContextProxy.VisualBrushRenderer(); - using (var context = _layer.CreateDrawingContext(visualBrushHelper)) + using (var context = _layer.CreateDrawingContext()) { context.PushClip(_dirtyRect); context.Clear(Colors.Transparent); - Root.Render(new CompositorDrawingContextProxy(context, visualBrushHelper), _dirtyRect); + Root.Render(new CompositorDrawingContextProxy(context), _dirtyRect); context.PopClip(); } } diff --git a/src/Avalonia.Base/Rendering/IVisualBrushRenderer.cs b/src/Avalonia.Base/Rendering/IVisualBrushRenderer.cs deleted file mode 100644 index f5312ad39b..0000000000 --- a/src/Avalonia.Base/Rendering/IVisualBrushRenderer.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Avalonia.Media; -using Avalonia.Metadata; -using Avalonia.Platform; - -namespace Avalonia.Rendering -{ - /// - /// Defines a renderer used to render a visual brush to a bitmap. - /// - [Unstable] - public interface IVisualBrushRenderer - { - /// - /// Gets the size of the intermediate render target to which the visual brush should be - /// drawn. - /// - /// The visual brush. - /// The size of the intermediate render target to create. - Size GetRenderTargetSize(IVisualBrush brush); - - /// - /// Renders a visual brush to a bitmap. - /// - /// The drawing context to render to. - /// The visual brush. - /// A bitmap containing the rendered brush. - void RenderVisualBrush(IDrawingContextImpl context, IVisualBrush brush); - } -} diff --git a/src/Avalonia.Base/Rendering/ImmediateRenderer.cs b/src/Avalonia.Base/Rendering/ImmediateRenderer.cs index 09d2d55ce3..4a12e78817 100644 --- a/src/Avalonia.Base/Rendering/ImmediateRenderer.cs +++ b/src/Avalonia.Base/Rendering/ImmediateRenderer.cs @@ -14,19 +14,8 @@ namespace Avalonia.Rendering /// a simple tree traversal. /// It's currently used mostly for RenderTargetBitmap.Render and VisualBrush /// - internal class ImmediateRenderer : IVisualBrushRenderer//, IRenderer + internal class ImmediateRenderer { - /// - /// Renders a visual to a render target. - /// - /// The visual. - /// The render target. - public static void Render(Visual visual, IRenderTarget target) - { - using var context = new DrawingContext(target.CreateDrawingContext(new ImmediateRenderer())); - Render(context, visual, visual.Bounds); - } - /// /// Renders a visual to a drawing context. /// @@ -36,28 +25,6 @@ namespace Avalonia.Rendering { Render(context, visual, visual.Bounds); } - - - /// - Size IVisualBrushRenderer.GetRenderTargetSize(IVisualBrush brush) - { - (brush.Visual as IVisualBrushInitialize)?.EnsureInitialized(); - return brush.Visual?.Bounds.Size ?? default; - } - - /// - void IVisualBrushRenderer.RenderVisualBrush(IDrawingContextImpl context, IVisualBrush brush) - { - if (brush.Visual is { } visual) - { - Render(new DrawingContext(context), visual, visual.Bounds); - } - } - - internal static void Render(Visual visual, DrawingContext context, bool updateTransformedBounds) - { - Render(context, visual, visual.Bounds); - } private static Rect GetTransformedBounds(Visual visual) { @@ -75,7 +42,7 @@ namespace Avalonia.Rendering } - private static void Render(DrawingContext context, Visual visual, Rect clipRect) + public static void Render(DrawingContext context, Visual visual, Rect clipRect) { var opacity = visual.Opacity; var clipToBounds = visual.ClipToBounds; diff --git a/src/Avalonia.Base/Rendering/SceneGraph/BrushDrawOperation.cs b/src/Avalonia.Base/Rendering/SceneGraph/BrushDrawOperation.cs index e81966ce81..62fc73db44 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/BrushDrawOperation.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/BrushDrawOperation.cs @@ -8,22 +8,19 @@ namespace Avalonia.Rendering.SceneGraph /// /// Base class for draw operations that can use a brush. /// - internal abstract class BrushDrawOperation : DrawOperation + internal abstract class BrushDrawOperation : DrawOperationWithTransform { - public BrushDrawOperation(Rect bounds, Matrix transform, IDisposable? aux) + public IImmutableBrush? Brush { get; } + + public BrushDrawOperation(Rect bounds, Matrix transform, IImmutableBrush? brush) : base(bounds, transform) { - Aux = aux; + Brush = brush; } - /// - /// Auxiliary data required to draw the brush - /// - public IDisposable? Aux { get; } - public override void Dispose() { - Aux?.Dispose(); + (Brush as ISceneBrushContent)?.Dispose(); base.Dispose(); } } diff --git a/src/Avalonia.Base/Rendering/SceneGraph/ClipNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/ClipNode.cs index e1bfaa4aa3..782e287989 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/ClipNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/ClipNode.cs @@ -5,7 +5,7 @@ namespace Avalonia.Rendering.SceneGraph /// /// A node in the scene graph which represents a clip push or pop. /// - internal class ClipNode : IDrawOperation + internal class ClipNode : IDrawOperationWithTransform { /// /// Initializes a new instance of the class that represents a @@ -70,8 +70,6 @@ namespace Avalonia.Rendering.SceneGraph /// public void Render(IDrawingContextImpl context) { - context.Transform = Transform; - if (Clip.HasValue) { context.PushClip(Clip.Value); diff --git a/src/Avalonia.Base/Rendering/SceneGraph/CustomDrawOperation.cs b/src/Avalonia.Base/Rendering/SceneGraph/CustomDrawOperation.cs index b7311936d3..ff2616bfe4 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/CustomDrawOperation.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/CustomDrawOperation.cs @@ -4,30 +4,19 @@ using Avalonia.Platform; namespace Avalonia.Rendering.SceneGraph { - internal sealed class CustomDrawOperation : DrawOperation + internal sealed class CustomDrawOperation : DrawOperationWithTransform { - public Matrix Transform { get; } public ICustomDrawOperation Custom { get; } public CustomDrawOperation(ICustomDrawOperation custom, Matrix transform) : base(custom.Bounds, transform) { - Transform = transform; Custom = custom; } - public override bool HitTest(Point p) - { - if (Transform.HasInverse) - { - return Custom.HitTest(p * Transform.Invert()); - } - - return false; - } + public override bool HitTest(Point p) => Custom.HitTest(p); public override void Render(IDrawingContextImpl context) { - context.Transform = Transform; Custom.Render(context); } @@ -37,8 +26,28 @@ namespace Avalonia.Rendering.SceneGraph Transform == transform && Custom?.Equals(custom) == true; } - public interface ICustomDrawOperation : IDrawOperation, IEquatable + public interface ICustomDrawOperation : IEquatable, IDisposable { - + /// + /// Gets the bounds of the visible content in the node in global coordinates. + /// + Rect Bounds { get; } + + /// + /// Hit test the geometry in this node. + /// + /// The point in global coordinates. + /// True if the point hits the node's geometry; otherwise false. + /// + /// This method does not recurse to childs, if you want + /// to hit test children they must be hit tested manually. + /// + bool HitTest(Point p); + + /// + /// Renders the node to a drawing context. + /// + /// The drawing context. + void Render(IDrawingContextImpl context); } } diff --git a/src/Avalonia.Base/Rendering/SceneGraph/DrawOperation.cs b/src/Avalonia.Base/Rendering/SceneGraph/DrawOperation.cs index c49e7705e0..5b93cd8cfc 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/DrawOperation.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/DrawOperation.cs @@ -28,4 +28,14 @@ namespace Avalonia.Rendering.SceneGraph { } } + + internal abstract class DrawOperationWithTransform : DrawOperation, IDrawOperationWithTransform + { + protected DrawOperationWithTransform(Rect bounds, Matrix transform) : base(bounds, transform) + { + Transform = transform; + } + + public Matrix Transform { get; } + } } diff --git a/src/Avalonia.Base/Rendering/SceneGraph/EllipseNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/EllipseNode.cs index 4600653b9d..d5f0270cb2 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/EllipseNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/EllipseNode.cs @@ -14,33 +14,20 @@ namespace Avalonia.Rendering.SceneGraph { public EllipseNode( Matrix transform, - IBrush? brush, + IImmutableBrush? brush, IPen? pen, - Rect rect, - IDisposable? aux = null) - : base(rect.Inflate(pen?.Thickness ?? 0), transform, aux) + Rect rect) + : base(rect.Inflate(pen?.Thickness ?? 0), transform, brush) { - Transform = transform; - Brush = brush?.ToImmutable(); Pen = pen?.ToImmutable(); Rect = rect; } - /// - /// Gets the fill brush. - /// - public IBrush? Brush { get; } - /// /// Gets the stroke pen. /// public ImmutablePen? Pen { get; } - /// - /// Gets the transform with which the node will be drawn. - /// - public Matrix Transform { get; } - /// /// Gets the rect of the ellipse to draw. /// @@ -54,21 +41,10 @@ namespace Avalonia.Rendering.SceneGraph rect.Equals(Rect); } - public override void Render(IDrawingContextImpl context) - { - context.Transform = Transform; - context.DrawEllipse(Brush, Pen, Rect); - } + public override void Render(IDrawingContextImpl context) => context.DrawEllipse(Brush, Pen, Rect); public override bool HitTest(Point p) { - if (!Transform.TryInvert(out Matrix inverted)) - { - return false; - } - - p *= inverted; - var center = Rect.Center; var strokeThickness = Pen?.Thickness ?? 0; @@ -112,5 +88,10 @@ namespace Avalonia.Rendering.SceneGraph return false; } + + public override void Dispose() + { + (Brush as ISceneBrushContent)?.Dispose(); + } } } diff --git a/src/Avalonia.Base/Rendering/SceneGraph/ExperimentalAcrylicNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/ExperimentalAcrylicNode.cs index 82f8fc2d56..e1f79e0e10 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/ExperimentalAcrylicNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/ExperimentalAcrylicNode.cs @@ -8,7 +8,7 @@ namespace Avalonia.Rendering.SceneGraph /// /// A node in the scene graph which represents a rectangle draw. /// - internal class ExperimentalAcrylicNode : DrawOperation + internal class ExperimentalAcrylicNode : DrawOperationWithTransform { /// /// Initializes a new instance of the class. @@ -22,16 +22,10 @@ namespace Avalonia.Rendering.SceneGraph RoundedRect rect) : base(rect.Rect, transform) { - Transform = transform; Material = material.ToImmutable(); Rect = rect; } - /// - /// Gets the transform with which the node will be drawn. - /// - public Matrix Transform { get; } - public IExperimentalAcrylicMaterial Material { get; } /// @@ -60,8 +54,6 @@ namespace Avalonia.Rendering.SceneGraph /// public override void Render(IDrawingContextImpl context) { - context.Transform = Transform; - if(context is IDrawingContextWithAcrylicLikeSupport idc) { idc.DrawRectangle(Material, Rect); @@ -73,18 +65,6 @@ namespace Avalonia.Rendering.SceneGraph } /// - public override bool HitTest(Point p) - { - // TODO: This doesn't respect CornerRadius yet. - if (Transform.HasInverse) - { - p *= Transform.Invert(); - - var rect = Rect.Rect; - return rect.ContainsExclusive(p); - } - - return false; - } + public override bool HitTest(Point p) => Rect.Rect.ContainsExclusive(p); } } diff --git a/src/Avalonia.Base/Rendering/SceneGraph/GeometryClipNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/GeometryClipNode.cs index 842edf2bcb..8575e61de4 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/GeometryClipNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/GeometryClipNode.cs @@ -5,7 +5,7 @@ namespace Avalonia.Rendering.SceneGraph /// /// A node in the scene graph which represents a geometry clip push or pop. /// - internal class GeometryClipNode : IDrawOperation + internal class GeometryClipNode : IDrawOperationWithTransform { /// /// Initializes a new instance of the class that represents a @@ -58,8 +58,6 @@ namespace Avalonia.Rendering.SceneGraph /// public void Render(IDrawingContextImpl context) { - context.Transform = Transform; - if (Clip != null) { context.PushGeometryClip(Clip); diff --git a/src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs index cf53b86fa7..3ab535897a 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs @@ -19,28 +19,15 @@ namespace Avalonia.Rendering.SceneGraph /// The geometry. /// Auxiliary data required to draw the brush. public GeometryNode(Matrix transform, - IBrush? brush, + IImmutableBrush? brush, IPen? pen, - IGeometryImpl geometry, - IDisposable? aux) - : base(geometry.GetRenderBounds(pen).CalculateBoundsWithLineCaps(pen), transform, aux) + IGeometryImpl geometry) + : base(geometry.GetRenderBounds(pen).CalculateBoundsWithLineCaps(pen), transform, brush) { - Transform = transform; - Brush = brush?.ToImmutable(); Pen = pen?.ToImmutable(); Geometry = geometry; } - /// - /// Gets the transform with which the node will be drawn. - /// - public Matrix Transform { get; } - - /// - /// Gets the fill brush. - /// - public IBrush? Brush { get; } - /// /// Gets the stroke pen. /// @@ -74,21 +61,14 @@ namespace Avalonia.Rendering.SceneGraph /// public override void Render(IDrawingContextImpl context) { - context.Transform = Transform; context.DrawGeometry(Brush, Pen, Geometry); } /// public override bool HitTest(Point p) { - if (Transform.HasInverse) - { - p *= Transform.Invert(); - return (Brush != null && Geometry.FillContains(p)) || - (Pen != null && Geometry.StrokeContains(Pen, p)); - } - - return false; + return (Brush != null && Geometry.FillContains(p)) || + (Pen != null && Geometry.StrokeContains(Pen, p)); } } } diff --git a/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs index a2d914bdd7..4d8759f545 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs @@ -19,37 +19,21 @@ namespace Avalonia.Rendering.SceneGraph /// Auxiliary data required to draw the brush. public GlyphRunNode( Matrix transform, - IBrush foreground, - IRef glyphRun, - IDisposable? aux = null) - : base(new Rect(glyphRun.Item.Size), transform, aux) + IImmutableBrush foreground, + IRef glyphRun) + : base(new Rect(glyphRun.Item.Size), transform, foreground) { - Transform = transform; - Foreground = foreground.ToImmutable(); GlyphRun = glyphRun.Clone(); } - - /// - /// Gets the transform with which the node will be drawn. - /// - public Matrix Transform { get; } - - /// - /// Gets the foreground brush. - /// - public IBrush Foreground { get; } - + + /// /// Gets the glyph run to draw. /// public IRef GlyphRun { get; } /// - public override void Render(IDrawingContextImpl context) - { - context.Transform = Transform; - context.DrawGlyphRun(Foreground, GlyphRun); - } + public override void Render(IDrawingContextImpl context) => context.DrawGlyphRun(Brush, GlyphRun); /// /// Determines if this draw operation equals another. @@ -65,16 +49,17 @@ namespace Avalonia.Rendering.SceneGraph internal bool Equals(Matrix transform, IBrush foreground, IRef glyphRun) { return transform == Transform && - Equals(foreground, Foreground) && + Equals(foreground, Brush) && Equals(glyphRun.Item, GlyphRun.Item); } /// - public override bool HitTest(Point p) => Bounds.ContainsExclusive(p); + public override bool HitTest(Point p) => new Rect(GlyphRun.Item.Size).ContainsExclusive(p); public override void Dispose() { GlyphRun?.Dispose(); + base.Dispose(); } } } diff --git a/src/Avalonia.Base/Rendering/SceneGraph/IDrawOperation.cs b/src/Avalonia.Base/Rendering/SceneGraph/IDrawOperation.cs index 2bfd2080c3..6a1aefe6b2 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/IDrawOperation.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/IDrawOperation.cs @@ -6,7 +6,7 @@ namespace Avalonia.Rendering.SceneGraph /// /// Represents a node in the low-level scene graph that represents geometry. /// - public interface IDrawOperation : IDisposable + internal interface IDrawOperation : IDisposable { /// /// Gets the bounds of the visible content in the node in global coordinates. @@ -30,4 +30,12 @@ namespace Avalonia.Rendering.SceneGraph /// The drawing context. void Render(IDrawingContextImpl context); } + + internal interface IDrawOperationWithTransform : IDrawOperation + { + /// + /// Gets the transform with which the node will be drawn. + /// + Matrix Transform { get; } + } } diff --git a/src/Avalonia.Base/Rendering/SceneGraph/ImageNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/ImageNode.cs index 339881e675..dd9787e8d1 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/ImageNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/ImageNode.cs @@ -7,7 +7,7 @@ namespace Avalonia.Rendering.SceneGraph /// /// A node in the scene graph which represents an image draw. /// - internal class ImageNode : DrawOperation + internal class ImageNode : DrawOperationWithTransform { /// /// Initializes a new instance of the class. @@ -21,19 +21,13 @@ namespace Avalonia.Rendering.SceneGraph public ImageNode(Matrix transform, IRef source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode) : base(destRect, transform) { - Transform = transform; Source = source.Clone(); Opacity = opacity; SourceRect = sourceRect; DestRect = destRect; BitmapInterpolationMode = bitmapInterpolationMode; SourceVersion = Source.Item.Version; - } - - /// - /// Gets the transform with which the node will be drawn. - /// - public Matrix Transform { get; } + } /// /// Gets the image to draw. @@ -68,14 +62,6 @@ namespace Avalonia.Rendering.SceneGraph /// public BitmapInterpolationMode BitmapInterpolationMode { get; } - /// - /// The bitmap blending mode. - /// - /// - /// The blending mode. - /// - public BitmapBlendingMode BitmapBlendingMode { get; } - /// /// Determines if this draw operation equals another. /// @@ -104,12 +90,11 @@ namespace Avalonia.Rendering.SceneGraph /// public override void Render(IDrawingContextImpl context) { - context.Transform = Transform; context.DrawBitmap(Source, Opacity, SourceRect, DestRect, BitmapInterpolationMode); } /// - public override bool HitTest(Point p) => Bounds.ContainsExclusive(p); + public override bool HitTest(Point p) => DestRect.ContainsExclusive(p); public override void Dispose() { diff --git a/src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs index 0af8ba2752..f21791d038 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs @@ -8,7 +8,7 @@ namespace Avalonia.Rendering.SceneGraph /// /// A node in the scene graph which represents a line draw. /// - internal class LineNode : BrushDrawOperation + internal class LineNode : DrawOperationWithTransform { /// /// Initializes a new instance of the class. @@ -22,21 +22,14 @@ namespace Avalonia.Rendering.SceneGraph Matrix transform, IPen pen, Point p1, - Point p2, - IDisposable? aux = null) - : base(LineBoundsHelper.CalculateBounds(p1, p2, pen), transform, aux) + Point p2) + : base(LineBoundsHelper.CalculateBounds(p1, p2, pen), transform) { - Transform = transform; Pen = pen.ToImmutable(); P1 = p1; P2 = p2; } - /// - /// Gets the transform with which the node will be drawn. - /// - public Matrix Transform { get; } - /// /// Gets the stroke pen. /// @@ -71,17 +64,11 @@ namespace Avalonia.Rendering.SceneGraph public override void Render(IDrawingContextImpl context) { - context.Transform = Transform; context.DrawLine(Pen, P1, P2); } public override bool HitTest(Point p) { - if (!Transform.HasInverse) - return false; - - p *= Transform.Invert(); - var halfThickness = Pen.Thickness / 2; var minX = Math.Min(P1.X, P2.X) - halfThickness; var maxX = Math.Max(P1.X, P2.X) + halfThickness; diff --git a/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs index 3ecc07fa54..e10d712c2d 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs @@ -18,27 +18,12 @@ namespace Avalonia.Rendering.SceneGraph /// The opacity mask to push. /// The bounds of the mask. /// Auxiliary data required to draw the brush. - public OpacityMaskNode(IBrush mask, Rect bounds, IDisposable? aux = null) - : base(default, Matrix.Identity, aux) + public OpacityMaskNode(IImmutableBrush mask, Rect bounds) + : base(default, Matrix.Identity, mask) { - Mask = mask.ToImmutable(); MaskBounds = bounds; } - /// - /// Initializes a new instance of the class that represents an - /// opacity mask pop. - /// - public OpacityMaskNode() - : base(default, Matrix.Identity, null) - { - } - - /// - /// Gets the mask to be pushed or null if the operation represents a pop. - /// - public IBrush? Mask { get; } - /// /// Gets the bounds of the opacity mask or null if the operation represents a pop. /// @@ -58,19 +43,23 @@ namespace Avalonia.Rendering.SceneGraph /// The properties of the other draw operation are passed in as arguments to prevent /// allocation of a not-yet-constructed draw operation object. /// - public bool Equals(IBrush? mask, Rect? bounds) => Mask == mask && MaskBounds == bounds; + public bool Equals(IBrush? mask, Rect? bounds) => Equals(Brush, mask) && MaskBounds == bounds; /// public override void Render(IDrawingContextImpl context) { - if (Mask != null) - { - context.PushOpacityMask(Mask, MaskBounds!.Value); - } - else - { - context.PopOpacityMask(); - } + context.PushOpacityMask(Brush!, MaskBounds!.Value); } } + + internal class OpacityMaskPopNode : DrawOperation + { + public OpacityMaskPopNode() : base(default, Matrix.Identity) + { + } + + public override bool HitTest(Point p) => false; + + public override void Render(IDrawingContextImpl context) => context.PopOpacityMask(); + } } diff --git a/src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs index f2ffd7411c..cee9ce9df7 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs @@ -23,30 +23,17 @@ namespace Avalonia.Rendering.SceneGraph /// Auxiliary data required to draw the brush. public RectangleNode( Matrix transform, - IBrush? brush, + IImmutableBrush? brush, IPen? pen, RoundedRect rect, - BoxShadows boxShadows, - IDisposable? aux = null) - : base(boxShadows.TransformBounds(rect.Rect).Inflate((pen?.Thickness ?? 0) / 2), transform, aux) + BoxShadows boxShadows) + : base(boxShadows.TransformBounds(rect.Rect).Inflate((pen?.Thickness ?? 0) / 2), transform, brush) { - Transform = transform; - Brush = brush?.ToImmutable(); Pen = pen?.ToImmutable(); Rect = rect; BoxShadows = boxShadows; } - /// - /// Gets the transform with which the node will be drawn. - /// - public Matrix Transform { get; } - - /// - /// Gets the fill brush. - /// - public IBrush? Brush { get; } - /// /// Gets the stroke pen. /// @@ -85,35 +72,22 @@ namespace Avalonia.Rendering.SceneGraph } /// - public override void Render(IDrawingContextImpl context) - { - context.Transform = Transform; - - context.DrawRectangle(Brush, Pen, Rect, BoxShadows); - } + public override void Render(IDrawingContextImpl context) => context.DrawRectangle(Brush, Pen, Rect, BoxShadows); /// public override bool HitTest(Point p) { - // TODO: This doesn't respect CornerRadius yet. - if (Transform.HasInverse) + if (Brush != null) { - p *= Transform.Invert(); - - if (Brush != null) - { - var rect = Rect.Rect.Inflate((Pen?.Thickness / 2) ?? 0); - return rect.ContainsExclusive(p); - } - else - { - var borderRect = Rect.Rect.Inflate((Pen?.Thickness / 2) ?? 0); - var emptyRect = Rect.Rect.Deflate((Pen?.Thickness / 2) ?? 0); - return borderRect.ContainsExclusive(p) && !emptyRect.ContainsExclusive(p); - } + var rect = Rect.Rect.Inflate((Pen?.Thickness / 2) ?? 0); + return rect.ContainsExclusive(p); + } + else + { + var borderRect = Rect.Rect.Inflate((Pen?.Thickness / 2) ?? 0); + var emptyRect = Rect.Rect.Deflate((Pen?.Thickness / 2) ?? 0); + return borderRect.ContainsExclusive(p) && !emptyRect.ContainsExclusive(p); } - - return false; } } } diff --git a/src/Avalonia.Base/StyledElement.cs b/src/Avalonia.Base/StyledElement.cs index 5b8dac2f53..39aaf55014 100644 --- a/src/Avalonia.Base/StyledElement.cs +++ b/src/Avalonia.Base/StyledElement.cs @@ -28,7 +28,7 @@ namespace Avalonia public class StyledElement : Animatable, IDataContextProvider, ILogical, - IResourceHost, + IThemeVariantHost, IStyleHost, IStyleable, ISetLogicalParent, @@ -75,23 +75,6 @@ namespace Avalonia public static readonly StyledProperty ThemeProperty = AvaloniaProperty.Register(nameof(Theme)); - /// - /// Defines the property. - /// - public static readonly StyledProperty ActualThemeVariantProperty = - AvaloniaProperty.Register( - nameof(ThemeVariant), - inherits: true, - defaultValue: ThemeVariant.Light); - - /// - /// Defines the RequestedThemeVariant property. - /// - public static readonly StyledProperty RequestedThemeVariantProperty = - AvaloniaProperty.Register( - nameof(ThemeVariant), - defaultValue: ThemeVariant.Default); - private static readonly ControlTheme s_invalidTheme = new ControlTheme(); private int _initCount; private string? _name; @@ -160,6 +143,9 @@ namespace Avalonia /// public event EventHandler? ResourcesChanged; + /// + public event EventHandler? ActualThemeVariantChanged; + /// /// Gets or sets the name of the styled element. /// @@ -278,15 +264,6 @@ namespace Avalonia set => SetValue(ThemeProperty, value); } - /// - /// Gets the UI theme that is currently used by the element, which might be different than the . - /// - /// - /// If current control is contained in the ThemeVariantScope, TopLevel or Application with non-default RequestedThemeVariant, that value will be returned. - /// Otherwise, current OS theme variant is returned. - /// - public ThemeVariant ActualThemeVariant => GetValue(ActualThemeVariantProperty); - /// /// Gets the styled element's logical children. /// @@ -325,6 +302,9 @@ namespace Avalonia /// public StyledElement? Parent { get; private set; } + /// + public ThemeVariant ActualThemeVariant => GetValue(ThemeVariant.ActualThemeVariantProperty); + /// /// Gets the styled element's logical parent. /// @@ -394,7 +374,7 @@ namespace Avalonia /// public bool ApplyStyling() { - if (_initCount == 0 && (!_stylesApplied || !_themeApplied)) + if (_initCount == 0 && (!_stylesApplied || !_themeApplied || !_templatedParentThemeApplied)) { GetValueStore().BeginStyling(); @@ -644,13 +624,19 @@ namespace Avalonia base.OnPropertyChanged(change); if (change.Property == ThemeProperty) + { OnControlThemeChanged(); - else if (change.Property == RequestedThemeVariantProperty) + } + else if (change.Property == ThemeVariant.RequestedThemeVariantProperty) { if (change.GetNewValue() is {} themeVariant && themeVariant != ThemeVariant.Default) - SetValue(ActualThemeVariantProperty, themeVariant); + SetValue(ThemeVariant.ActualThemeVariantProperty, themeVariant); else - ClearValue(ActualThemeVariantProperty); + ClearValue(ThemeVariant.ActualThemeVariantProperty); + } + else if (change.Property == ThemeVariant.ActualThemeVariantProperty) + { + ActualThemeVariantChanged?.Invoke(this, EventArgs.Empty); } } diff --git a/src/Avalonia.Base/Styling/Activators/NthChildActivator.cs b/src/Avalonia.Base/Styling/Activators/NthChildActivator.cs index 33d4cd0824..8bdcec2e53 100644 --- a/src/Avalonia.Base/Styling/Activators/NthChildActivator.cs +++ b/src/Avalonia.Base/Styling/Activators/NthChildActivator.cs @@ -1,4 +1,5 @@ #nullable enable +using System; using Avalonia.LogicalTree; namespace Avalonia.Styling.Activators @@ -13,6 +14,7 @@ namespace Avalonia.Styling.Activators private readonly int _step; private readonly int _offset; private readonly bool _reversed; + private int _index = -1; public NthChildActivator( ILogical control, @@ -28,24 +30,51 @@ namespace Avalonia.Styling.Activators protected override bool EvaluateIsActive() { - return NthChildSelector.Evaluate(_control, _provider, _step, _offset, _reversed).IsMatch; + var index = _index >= 0 ? _index : _provider.GetChildIndex(_control); + return NthChildSelector.Evaluate(index, _provider, _step, _offset, _reversed).IsMatch; } - protected override void Initialize() => _provider.ChildIndexChanged += ChildIndexChanged; - protected override void Deinitialize() => _provider.ChildIndexChanged -= ChildIndexChanged; + protected override void Initialize() + { + _provider.ChildIndexChanged += ChildIndexChanged; + } + + protected override void Deinitialize() + { + _provider.ChildIndexChanged -= ChildIndexChanged; + } private void ChildIndexChanged(object? sender, ChildIndexChangedEventArgs e) { // Run matching again if: - // 1. Selector is reversed, so other item insertion/deletion might affect total count without changing subscribed item index. - // 2. e.Child is null, when all children indices were changed. - // 3. Subscribed child index was changed. - if (_reversed - || e.Child is null - || e.Child == _control) + // 1. Subscribed child index was changed + // 2. Child indexes were reset + // 3. We're a reversed (nth-last-child) selector and total count has changed + if ((e.Child == _control || e.Action == ChildIndexChangedAction.ChildIndexesReset) || + (_reversed && e.Action == ChildIndexChangedAction.TotalCountChanged)) { + // We're using the _index field to pass the index of the child to EvaluateIsActive + // *only* when the active state is re-evaluated via this event handler. The docs + // for EvaluateIsActive say: + // + // > This method should read directly from its inputs and not rely on any + // > subscriptions to fire in order to be up-to-date. + // + // Which is good advice in general, however in this case we need to break the rule + // and use the value from the event subscription instead of calling + // IChildIndexProvider.GetChildIndex. This is because this event can be fired during + // the process of realizing an element of a virtualized list; in this case calling + // GetChildIndex may not return the correct index as the element isn't yet realized. + _index = e.Index; ReevaluateIsActive(); + _index = -1; } } + + private void TotalCountChanged(object? sender, EventArgs e) + { + if (_reversed) + ReevaluateIsActive(); + } } } diff --git a/src/Avalonia.Base/Styling/IGlobalThemeVariantProvider.cs b/src/Avalonia.Base/Styling/IGlobalThemeVariantProvider.cs deleted file mode 100644 index 2467d99b3b..0000000000 --- a/src/Avalonia.Base/Styling/IGlobalThemeVariantProvider.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; -using Avalonia.Controls; -using Avalonia.Metadata; - -namespace Avalonia.Styling; - -/// -/// Interface for an application host element with a root theme variant. -/// -[Unstable] -public interface IGlobalThemeVariantProvider : IResourceHost -{ - /// - /// Gets the UI theme variant that is used by the control (and its child elements) for resource determination. - /// - ThemeVariant ActualThemeVariant { get; } - - /// - /// Raised when the theme variant is changed on the element or an ancestor of the element. - /// - event EventHandler? ActualThemeVariantChanged; -} diff --git a/src/Avalonia.Base/Styling/IThemeVariantHost.cs b/src/Avalonia.Base/Styling/IThemeVariantHost.cs new file mode 100644 index 0000000000..01583148a8 --- /dev/null +++ b/src/Avalonia.Base/Styling/IThemeVariantHost.cs @@ -0,0 +1,26 @@ +using System; +using Avalonia.Controls; +using Avalonia.Metadata; + +namespace Avalonia.Styling; + +/// +/// Interface for the host element with a theme variant. +/// +[Unstable] +public interface IThemeVariantHost : IResourceHost +{ + /// + /// Gets the UI theme that is currently used by the element, which might be different than the RequestedThemeVariantProperty. + /// + /// + /// If current control is contained in the ThemeVariantScope, TopLevel or Application with non-default RequestedThemeVariant, that value will be returned. + /// Otherwise, current OS theme variant is returned. + /// + ThemeVariant ActualThemeVariant { get; } + + /// + /// Raised when the theme variant is changed on the element or an ancestor of the element. + /// + event EventHandler? ActualThemeVariantChanged; +} diff --git a/src/Avalonia.Base/Styling/NthChildSelector.cs b/src/Avalonia.Base/Styling/NthChildSelector.cs index ccfc2c781d..532179bb2c 100644 --- a/src/Avalonia.Base/Styling/NthChildSelector.cs +++ b/src/Avalonia.Base/Styling/NthChildSelector.cs @@ -61,7 +61,7 @@ namespace Avalonia.Styling { return subscribe ? new SelectorMatch(new NthChildActivator(logical, childIndexProvider, Step, Offset, _reversed)) - : Evaluate(logical, childIndexProvider, Step, Offset, _reversed); + : Evaluate(childIndexProvider.GetChildIndex(logical), childIndexProvider, Step, Offset, _reversed); } else { @@ -70,10 +70,9 @@ namespace Avalonia.Styling } internal static SelectorMatch Evaluate( - ILogical logical, IChildIndexProvider childIndexProvider, + int index, IChildIndexProvider childIndexProvider, int step, int offset, bool reversed) { - var index = childIndexProvider.GetChildIndex(logical); if (index < 0) { return SelectorMatch.NeverThisInstance; diff --git a/src/Avalonia.Base/Styling/ThemeVariant.cs b/src/Avalonia.Base/Styling/ThemeVariant.cs index 8218533f4f..389136b0f5 100644 --- a/src/Avalonia.Base/Styling/ThemeVariant.cs +++ b/src/Avalonia.Base/Styling/ThemeVariant.cs @@ -6,11 +6,26 @@ using Avalonia.Platform; namespace Avalonia.Styling; /// -/// Specifies a UI theme variant that should be used for the +/// Specifies a UI theme variant that should be used for the Control and Application types. /// [TypeConverter(typeof(ThemeVariantTypeConverter))] public sealed record ThemeVariant { + /// + /// Defines the ActualThemeVariant property. + /// + internal static readonly StyledProperty ActualThemeVariantProperty = + AvaloniaProperty.Register( + "ActualThemeVariant", + inherits: true); + + /// + /// Defines the RequestedThemeVariant property. + /// + internal static readonly StyledProperty RequestedThemeVariantProperty = + AvaloniaProperty.Register( + "RequestedThemeVariant", defaultValue: Default); + /// /// Creates a new instance of the /// diff --git a/src/Avalonia.Base/Threading/ThreadSafeObjectPool.cs b/src/Avalonia.Base/Threading/ThreadSafeObjectPool.cs index 827a02334a..30b7738409 100644 --- a/src/Avalonia.Base/Threading/ThreadSafeObjectPool.cs +++ b/src/Avalonia.Base/Threading/ThreadSafeObjectPool.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; namespace Avalonia.Threading { - public class ThreadSafeObjectPool where T : class, new() + internal class ThreadSafeObjectPool where T : class, new() { private Stack _stack = new Stack(); public static ThreadSafeObjectPool Default { get; } = new ThreadSafeObjectPool(); @@ -17,11 +17,14 @@ namespace Avalonia.Threading } } - public void Return(T obj) + public void ReturnAndSetNull(ref T? obj) { + if (obj == null) + return; lock (_stack) { _stack.Push(obj); + obj = null; } } } diff --git a/src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs b/src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs index 06a77f0894..f5db7c0855 100644 --- a/src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs +++ b/src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs @@ -336,7 +336,7 @@ namespace Avalonia.Controls.Primitives internal void InvalidateChildIndex() { - _childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.Empty); + _childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.ChildIndexesReset); } private bool ShouldDisplayCell(DataGridColumn column, double frozenLeftEdge, double scrollingLeftEdge) diff --git a/src/Avalonia.Controls.DataGrid/Primitives/DataGridColumnHeadersPresenter.cs b/src/Avalonia.Controls.DataGrid/Primitives/DataGridColumnHeadersPresenter.cs index f9b84793c6..fcf72385b2 100644 --- a/src/Avalonia.Controls.DataGrid/Primitives/DataGridColumnHeadersPresenter.cs +++ b/src/Avalonia.Controls.DataGrid/Primitives/DataGridColumnHeadersPresenter.cs @@ -423,7 +423,7 @@ namespace Avalonia.Controls.Primitives internal void InvalidateChildIndex() { - _childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.Empty); + _childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.ChildIndexesReset); } } } diff --git a/src/Avalonia.Controls.DataGrid/Primitives/DataGridRowsPresenter.cs b/src/Avalonia.Controls.DataGrid/Primitives/DataGridRowsPresenter.cs index d906cd359c..5a4ddd36f4 100644 --- a/src/Avalonia.Controls.DataGrid/Primitives/DataGridRowsPresenter.cs +++ b/src/Avalonia.Controls.DataGrid/Primitives/DataGridRowsPresenter.cs @@ -66,7 +66,7 @@ namespace Avalonia.Controls.Primitives internal void InvalidateChildIndex(DataGridRow row) { - _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(row)); + _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(row, row.Index)); } /// diff --git a/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml b/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml index dd8575c989..ca516c8918 100644 --- a/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml +++ b/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml @@ -1,6 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0.6 0.8 @@ -9,19 +51,6 @@ M515 93l930 931l-930 931l90 90l1022 -1021l-1022 -1021z M109 486 19 576 1024 1581 2029 576 1939 486 1024 1401z - - - - - - - - - - - @@ -29,23 +58,10 @@ - + - - - - - - - - - @@ -565,5 +581,6 @@ + diff --git a/src/Avalonia.Controls.ItemsRepeater/Controls/ItemsRepeater.cs b/src/Avalonia.Controls.ItemsRepeater/Controls/ItemsRepeater.cs index 951e60c25b..3d3d01e06e 100644 --- a/src/Avalonia.Controls.ItemsRepeater/Controls/ItemsRepeater.cs +++ b/src/Avalonia.Controls.ItemsRepeater/Controls/ItemsRepeater.cs @@ -536,11 +536,12 @@ namespace Avalonia.Controls internal void OnElementPrepared(Control element, VirtualizationInfo virtInfo) { + var index = virtInfo.Index; + _viewportManager.OnElementPrepared(element, virtInfo); if (ElementPrepared != null) { - var index = virtInfo.Index; if (_elementPreparedArgs == null) { @@ -554,7 +555,7 @@ namespace Avalonia.Controls ElementPrepared(this, _elementPreparedArgs); } - _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(element)); + _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(element, index)); } internal void OnElementClearing(Control element) @@ -573,7 +574,7 @@ namespace Avalonia.Controls ElementClearing(this, _elementClearingArgs); } - _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(element)); + _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(element, -1)); } internal void OnElementIndexChanged(Control element, int oldIndex, int newIndex) @@ -592,7 +593,7 @@ namespace Avalonia.Controls ElementIndexChanged(this, _elementIndexChangedArgs); } - _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(element)); + _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(element, newIndex)); } private void OnDataSourcePropertyChanged(ItemsSourceView? oldValue, ItemsSourceView? newValue) diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index 6d3ba3cf8a..6d9a6bd493 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -29,7 +29,7 @@ namespace Avalonia /// method. /// - Tracks the lifetime of the application. /// - public class Application : AvaloniaObject, IDataContextProvider, IGlobalDataTemplates, IGlobalStyles, IGlobalThemeVariantProvider, IApplicationPlatformEvents + public class Application : AvaloniaObject, IDataContextProvider, IGlobalDataTemplates, IGlobalStyles, IThemeVariantHost, IApplicationPlatformEvents { /// /// The application-global data templates. @@ -50,13 +50,13 @@ namespace Avalonia public static readonly StyledProperty DataContextProperty = StyledElement.DataContextProperty.AddOwner(); - /// + /// public static readonly StyledProperty ActualThemeVariantProperty = - StyledElement.ActualThemeVariantProperty.AddOwner(); + ThemeVariantScope.ActualThemeVariantProperty.AddOwner(); - /// + /// public static readonly StyledProperty RequestedThemeVariantProperty = - StyledElement.RequestedThemeVariantProperty.AddOwner(); + ThemeVariantScope.RequestedThemeVariantProperty.AddOwner(); /// public event EventHandler? ResourcesChanged; @@ -95,11 +95,8 @@ namespace Avalonia set => SetValue(RequestedThemeVariantProperty, value); } - /// - public ThemeVariant ActualThemeVariant - { - get => GetValue(ActualThemeVariantProperty); - } + /// + public ThemeVariant ActualThemeVariant => GetValue(ActualThemeVariantProperty); /// /// Gets the current instance of the class. @@ -256,7 +253,7 @@ namespace Avalonia .Bind().ToTransient() .Bind().ToConstant(this) .Bind().ToConstant(this) - .Bind().ToConstant(this) + .Bind().ToConstant(this) .Bind().ToConstant(FocusManager) .Bind().ToConstant(InputManager) .Bind().ToTransient() diff --git a/src/Avalonia.Controls/Control.cs b/src/Avalonia.Controls/Control.cs index ab7c9948c4..dbece12575 100644 --- a/src/Avalonia.Controls/Control.cs +++ b/src/Avalonia.Controls/Control.cs @@ -161,7 +161,7 @@ namespace Avalonia.Controls get => GetValue(TagProperty); set => SetValue(TagProperty, value); } - + /// /// Occurs when the user has completed a context input gesture, such as a right-click. /// diff --git a/src/Avalonia.Controls/Expander.cs b/src/Avalonia.Controls/Expander.cs index 2ad6a58d38..668de5bca9 100644 --- a/src/Avalonia.Controls/Expander.cs +++ b/src/Avalonia.Controls/Expander.cs @@ -191,7 +191,7 @@ namespace Avalonia.Controls /// /// Invoked just before the event. /// - protected virtual void OnCollapsing(RoutedEventArgs eventArgs) + protected virtual void OnCollapsing(CancelRoutedEventArgs eventArgs) { RaiseEvent(eventArgs); } @@ -207,7 +207,7 @@ namespace Avalonia.Controls /// /// Invoked just before the event. /// - protected virtual void OnExpanding(RoutedEventArgs eventArgs) + protected virtual void OnExpanding(CancelRoutedEventArgs eventArgs) { RaiseEvent(eventArgs); } diff --git a/src/Avalonia.Controls/ExperimentalAcrylicBorder.cs b/src/Avalonia.Controls/ExperimentalAcrylicBorder.cs index e1f840672d..dbffb803a3 100644 --- a/src/Avalonia.Controls/ExperimentalAcrylicBorder.cs +++ b/src/Avalonia.Controls/ExperimentalAcrylicBorder.cs @@ -82,7 +82,7 @@ namespace Avalonia.Controls public sealed override void Render(DrawingContext context) { - if (context.PlatformImpl is IDrawingContextWithAcrylicLikeSupport idc) + if (context is IDrawingContextWithAcrylicLikeSupport idc) { var cornerRadius = CornerRadius; diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index ce12d5f2bf..9483f98881 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -102,7 +102,6 @@ namespace Avalonia.Controls private ItemContainerGenerator? _itemContainerGenerator; private EventHandler? _childIndexChanged; private IDataTemplate? _displayMemberItemTemplate; - private Tuple? _containerBeingPrepared; private ScrollViewer? _scrollViewer; private ItemsPresenter? _itemsPresenter; @@ -218,7 +217,6 @@ namespace Avalonia.Controls remove => _childIndexChanged -= value; } - /// public event EventHandler HorizontalSnapPointsChanged { @@ -495,6 +493,7 @@ namespace Avalonia.Controls else if (change.Property == ItemCountProperty) { UpdatePseudoClasses(change.GetNewValue()); + _childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.TotalCountChanged); } else if (change.Property == ItemContainerThemeProperty && _itemContainerGenerator is not null) { @@ -579,7 +578,7 @@ namespace Avalonia.Controls internal void RegisterItemsPresenter(ItemsPresenter presenter) { Presenter = presenter; - _childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.Empty); + _childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.ChildIndexesReset); } internal void PrepareItemContainer(Control container, object? item, int index) @@ -601,17 +600,14 @@ namespace Avalonia.Controls internal void ItemContainerPrepared(Control container, object? item, int index) { - _containerBeingPrepared = new(index, container); - _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(container)); - _containerBeingPrepared = null; - + _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(container, index)); _scrollViewer?.RegisterAnchorCandidate(container); } internal void ItemContainerIndexChanged(Control container, int oldIndex, int newIndex) { ContainerIndexChangedOverride(container, oldIndex, newIndex); - _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(container)); + _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(container, newIndex)); } internal void ClearItemContainer(Control container) @@ -742,9 +738,6 @@ namespace Avalonia.Controls int IChildIndexProvider.GetChildIndex(ILogical child) { - if (_containerBeingPrepared?.Item2 == child) - return _containerBeingPrepared.Item1; - return child is Control container ? IndexFromContainer(container) : -1; } diff --git a/src/Avalonia.Controls/Panel.cs b/src/Avalonia.Controls/Panel.cs index a7dc035459..fa18ee468c 100644 --- a/src/Avalonia.Controls/Panel.cs +++ b/src/Avalonia.Controls/Panel.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Collections.Specialized; +using System.ComponentModel; using System.Linq; using Avalonia.LogicalTree; using Avalonia.Media; @@ -60,8 +61,19 @@ namespace Avalonia.Controls event EventHandler? IChildIndexProvider.ChildIndexChanged { - add => _childIndexChanged += value; - remove => _childIndexChanged -= value; + add + { + if (_childIndexChanged is null) + Children.PropertyChanged += ChildrenPropertyChanged; + _childIndexChanged += value; + } + + remove + { + _childIndexChanged -= value; + if (_childIndexChanged is null) + Children.PropertyChanged -= ChildrenPropertyChanged; + } } /// @@ -152,7 +164,7 @@ namespace Avalonia.Controls throw new NotSupportedException(); } - _childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.Empty); + _childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.ChildIndexesReset); InvalidateMeasureOnChildrenChanged(); } @@ -161,6 +173,12 @@ namespace Avalonia.Controls InvalidateMeasure(); } + private void ChildrenPropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(Children.Count) || e.PropertyName is null) + _childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.TotalCountChanged); + } + private static void AffectsParentArrangeInvalidate(AvaloniaPropertyChangedEventArgs e) where TPanel : Panel { diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index d6cd71aedc..9d443d9289 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -387,6 +387,15 @@ namespace Avalonia.Controls.Primitives popupHost.Transform = null; } + if (popupHost is PopupRoot topLevelPopup) + { + topLevelPopup + .Bind( + ThemeVariantScope.ActualThemeVariantProperty, + this.GetBindingObservable(ThemeVariantScope.ActualThemeVariantProperty)) + .DisposeWith(handlerCleanup); + } + UpdateHostPosition(popupHost, placementTarget); SubscribeToEventHandler>(popupHost, RootTemplateApplied, diff --git a/src/Avalonia.Controls/SplitView.cs b/src/Avalonia.Controls/SplitView/SplitView.cs similarity index 56% rename from src/Avalonia.Controls/SplitView.cs rename to src/Avalonia.Controls/SplitView/SplitView.cs index 35b135e152..1099a40f08 100644 --- a/src/Avalonia.Controls/SplitView.cs +++ b/src/Avalonia.Controls/SplitView/SplitView.cs @@ -1,77 +1,16 @@ -using Avalonia.Controls.Metadata; +using System; +using Avalonia.Controls.Metadata; +using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; +using Avalonia.Controls.Templates; using Avalonia.Input; using Avalonia.Interactivity; +using Avalonia.LogicalTree; using Avalonia.Media; using Avalonia.Metadata; -using Avalonia.VisualTree; -using System; -using Avalonia.Reactive; -using Avalonia.Controls.Presenters; -using Avalonia.Controls.Templates; -using Avalonia.LogicalTree; namespace Avalonia.Controls { - /// - /// Defines constants for how the SplitView Pane should display - /// - public enum SplitViewDisplayMode - { - /// - /// Pane is displayed next to content, and does not auto collapse - /// when tapped outside - /// - Inline, - /// - /// Pane is displayed next to content. When collapsed, pane is still - /// visible according to CompactPaneLength. Pane does not auto collapse - /// when tapped outside - /// - CompactInline, - /// - /// Pane is displayed above content. Pane collapses when tapped outside - /// - Overlay, - /// - /// Pane is displayed above content. When collapsed, pane is still - /// visible according to CompactPaneLength. Pane collapses when tapped outside - /// - CompactOverlay - } - - /// - /// Defines constants for where the Pane should appear - /// - public enum SplitViewPanePlacement - { - Left, - Right - } - - public class SplitViewTemplateSettings : AvaloniaObject - { - internal SplitViewTemplateSettings() { } - - public static readonly StyledProperty ClosedPaneWidthProperty = - AvaloniaProperty.Register(nameof(ClosedPaneWidth), 0d); - - public static readonly StyledProperty PaneColumnGridLengthProperty = - AvaloniaProperty.Register(nameof(PaneColumnGridLength)); - - public double ClosedPaneWidth - { - get => GetValue(ClosedPaneWidthProperty); - internal set => SetValue(ClosedPaneWidthProperty, value); - } - - public GridLength PaneColumnGridLength - { - get => GetValue(PaneColumnGridLengthProperty); - internal set => SetValue(PaneColumnGridLengthProperty, value); - } - } - /// /// A control with two views: A collapsible pane and an area for content /// @@ -93,26 +32,34 @@ namespace Avalonia.Controls /// Defines the property /// public static readonly StyledProperty CompactPaneLengthProperty = - AvaloniaProperty.Register(nameof(CompactPaneLength), defaultValue: 48); + AvaloniaProperty.Register( + nameof(CompactPaneLength), + defaultValue: 48); /// /// Defines the property /// public static readonly StyledProperty DisplayModeProperty = - AvaloniaProperty.Register(nameof(DisplayMode), defaultValue: SplitViewDisplayMode.Overlay); + AvaloniaProperty.Register( + nameof(DisplayMode), + defaultValue: SplitViewDisplayMode.Overlay); /// /// Defines the property /// - public static readonly DirectProperty IsPaneOpenProperty = - AvaloniaProperty.RegisterDirect(nameof(IsPaneOpen), - x => x.IsPaneOpen, (x, v) => x.IsPaneOpen = v); + public static readonly StyledProperty IsPaneOpenProperty = + AvaloniaProperty.Register( + nameof(IsPaneOpen), + defaultValue: false, + coerce: CoerceIsPaneOpen); /// /// Defines the property /// public static readonly StyledProperty OpenPaneLengthProperty = - AvaloniaProperty.Register(nameof(OpenPaneLength), defaultValue: 320); + AvaloniaProperty.Register( + nameof(OpenPaneLength), + defaultValue: 320); /// /// Defines the property @@ -150,7 +97,38 @@ namespace Avalonia.Controls public static readonly StyledProperty TemplateSettingsProperty = AvaloniaProperty.Register(nameof(TemplateSettings)); - private bool _isPaneOpen; + /// + /// Defines the event. + /// + public static readonly RoutedEvent PaneClosedEvent = + RoutedEvent.Register( + nameof(PaneClosed), + RoutingStrategies.Bubble); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent PaneClosingEvent = + RoutedEvent.Register( + nameof(PaneClosing), + RoutingStrategies.Bubble); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent PaneOpenedEvent = + RoutedEvent.Register( + nameof(PaneOpened), + RoutingStrategies.Bubble); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent PaneOpeningEvent = + RoutedEvent.Register( + nameof(PaneOpening), + RoutingStrategies.Bubble); + private Panel? _pane; private IDisposable? _pointerDisposable; @@ -164,12 +142,6 @@ namespace Avalonia.Controls static SplitView() { - UseLightDismissOverlayModeProperty.Changed.AddClassHandler((x, v) => x.OnUseLightDismissChanged(v)); - CompactPaneLengthProperty.Changed.AddClassHandler((x, v) => x.OnCompactPaneLengthChanged(v)); - PanePlacementProperty.Changed.AddClassHandler((x, v) => x.OnPanePlacementChanged(v)); - DisplayModeProperty.Changed.AddClassHandler((x, v) => x.OnDisplayModeChanged(v)); - - PaneProperty.Changed.AddClassHandler((x, e) => x.PaneChanged(e)); } /// @@ -196,37 +168,8 @@ namespace Avalonia.Controls /// public bool IsPaneOpen { - get => _isPaneOpen; - set - { - if (value == _isPaneOpen) - { - return; - } - - if (value) - { - OnPaneOpening(this, EventArgs.Empty); - SetAndRaise(IsPaneOpenProperty, ref _isPaneOpen, value); - - PseudoClasses.Add(":open"); - PseudoClasses.Remove(":closed"); - OnPaneOpened(this, EventArgs.Empty); - } - else - { - SplitViewPaneClosingEventArgs args = new SplitViewPaneClosingEventArgs(false); - OnPaneClosing(this, args); - if (!args.Cancel) - { - SetAndRaise(IsPaneOpenProperty, ref _isPaneOpen, value); - - PseudoClasses.Add(":closed"); - PseudoClasses.Remove(":open"); - OnPaneClosed(this, EventArgs.Empty); - } - } - } + get => GetValue(IsPaneOpenProperty); + set => SetValue(IsPaneOpenProperty, value); } /// @@ -297,24 +240,48 @@ namespace Avalonia.Controls } /// - /// Fired when the pane is closed + /// Fired when the pane is closed. /// - public event EventHandler? PaneClosed; + public event EventHandler? PaneClosed + { + add => AddHandler(PaneClosedEvent, value); + remove => RemoveHandler(PaneClosedEvent, value); + } /// - /// Fired when the pane is closing + /// Fired when the pane is closing. /// - public event EventHandler? PaneClosing; + /// + /// The event args property may be set to true to cancel the event + /// and keep the pane open. + /// + public event EventHandler? PaneClosing + { + add => AddHandler(PaneClosingEvent, value); + remove => RemoveHandler(PaneClosingEvent, value); + } /// - /// Fired when the pane is opened + /// Fired when the pane is opened. /// - public event EventHandler? PaneOpened; + public event EventHandler? PaneOpened + { + add => AddHandler(PaneOpenedEvent, value); + remove => RemoveHandler(PaneOpenedEvent, value); + } /// - /// Fired when the pane is opening + /// Fired when the pane is opening. /// - public event EventHandler? PaneOpening; + /// + /// The event args property may be set to true to cancel the event + /// and keep the pane closed. + /// + public event EventHandler? PaneOpening + { + add => AddHandler(PaneOpeningEvent, value); + remove => RemoveHandler(PaneOpeningEvent, value); + } protected override bool RegisterContentPresenter(IContentPresenter presenter) { @@ -351,6 +318,89 @@ namespace Avalonia.Controls _pointerDisposable?.Dispose(); } + /// + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == CompactPaneLengthProperty) + { + var newLen = change.GetNewValue(); + var displayMode = DisplayMode; + if (displayMode == SplitViewDisplayMode.CompactInline) + { + TemplateSettings.ClosedPaneWidth = newLen; + } + else if (displayMode == SplitViewDisplayMode.CompactOverlay) + { + TemplateSettings.ClosedPaneWidth = newLen; + TemplateSettings.PaneColumnGridLength = new GridLength(newLen, GridUnitType.Pixel); + } + } + else if (change.Property == DisplayModeProperty) + { + var oldState = GetPseudoClass(change.GetOldValue()); + var newState = GetPseudoClass(change.GetNewValue()); + + PseudoClasses.Remove($":{oldState}"); + PseudoClasses.Add($":{newState}"); + + var (closedPaneWidth, paneColumnGridLength) = change.GetNewValue() switch + { + SplitViewDisplayMode.Overlay => (0, new GridLength(0, GridUnitType.Pixel)), + SplitViewDisplayMode.CompactOverlay => (CompactPaneLength, new GridLength(CompactPaneLength, GridUnitType.Pixel)), + SplitViewDisplayMode.Inline => (0, new GridLength(0, GridUnitType.Auto)), + SplitViewDisplayMode.CompactInline => (CompactPaneLength, new GridLength(0, GridUnitType.Auto)), + _ => throw new NotImplementedException(), + }; + TemplateSettings.ClosedPaneWidth = closedPaneWidth; + TemplateSettings.PaneColumnGridLength = paneColumnGridLength; + } + else if (change.Property == IsPaneOpenProperty) + { + bool isPaneOpen = change.GetNewValue(); + + if (isPaneOpen) + { + PseudoClasses.Add(":open"); + PseudoClasses.Remove(":closed"); + + OnPaneOpened(new RoutedEventArgs(PaneOpenedEvent, this)); + } + else + { + PseudoClasses.Add(":closed"); + PseudoClasses.Remove(":open"); + + OnPaneClosed(new RoutedEventArgs(PaneClosedEvent, this)); + } + } + else if (change.Property == PaneProperty) + { + if (change.OldValue is ILogical oldChild) + { + LogicalChildren.Remove(oldChild); + } + + if (change.NewValue is ILogical newChild) + { + LogicalChildren.Add(newChild); + } + } + else if (change.Property == PanePlacementProperty) + { + var oldState = GetPseudoClass(change.GetOldValue()); + var newState = GetPseudoClass(change.GetNewValue()); + PseudoClasses.Remove($":{oldState}"); + PseudoClasses.Add($":{newState}"); + } + else if (change.Property == UseLightDismissOverlayModeProperty) + { + var mode = change.GetNewValue(); + PseudoClasses.Set(":lightdismiss", mode); + } + } + private void PointerPressedOutside(object? sender, PointerPressedEventArgs e) { if (!IsPaneOpen) @@ -384,7 +434,7 @@ namespace Avalonia.Controls } if (closePane) { - IsPaneOpen = false; + SetCurrentValue(IsPaneOpenProperty, false); e.Handled = true; } } @@ -394,41 +444,29 @@ namespace Avalonia.Controls return (DisplayMode == SplitViewDisplayMode.CompactOverlay || DisplayMode == SplitViewDisplayMode.Overlay); } - protected virtual void OnPaneOpening(SplitView sender, EventArgs args) + protected virtual void OnPaneOpening(CancelRoutedEventArgs args) { - PaneOpening?.Invoke(sender, args); + RaiseEvent(args); } - protected virtual void OnPaneOpened(SplitView sender, EventArgs args) + protected virtual void OnPaneOpened(RoutedEventArgs args) { - PaneOpened?.Invoke(sender, args); + RaiseEvent(args); } - protected virtual void OnPaneClosing(SplitView sender, SplitViewPaneClosingEventArgs args) + protected virtual void OnPaneClosing(CancelRoutedEventArgs args) { - PaneClosing?.Invoke(sender, args); + RaiseEvent(args); } - protected virtual void OnPaneClosed(SplitView sender, EventArgs args) + protected virtual void OnPaneClosed(RoutedEventArgs args) { - PaneClosed?.Invoke(sender, args); - } - - private void OnCompactPaneLengthChanged(AvaloniaPropertyChangedEventArgs e) - { - var newLen = (double)e.NewValue!; - var displayMode = DisplayMode; - if (displayMode == SplitViewDisplayMode.CompactInline) - { - TemplateSettings.ClosedPaneWidth = newLen; - } - else if (displayMode == SplitViewDisplayMode.CompactOverlay) - { - TemplateSettings.ClosedPaneWidth = newLen; - TemplateSettings.PaneColumnGridLength = new GridLength(newLen, GridUnitType.Pixel); - } + RaiseEvent(args); } + /// + /// Gets the appropriate PseudoClass for the given . + /// private static string GetPseudoClass(SplitViewDisplayMode mode) { return mode switch @@ -441,6 +479,9 @@ namespace Avalonia.Controls }; } + /// + /// Gets the appropriate PseudoClass for the given . + /// private static string GetPseudoClass(SplitViewPanePlacement placement) { return placement switch @@ -451,51 +492,47 @@ namespace Avalonia.Controls }; } - private void OnPanePlacementChanged(AvaloniaPropertyChangedEventArgs e) - { - var oldState = GetPseudoClass(e.GetOldValue()); - var newState = GetPseudoClass(e.GetNewValue()); - PseudoClasses.Remove($":{oldState}"); - PseudoClasses.Add($":{newState}"); - } - - private void OnDisplayModeChanged(AvaloniaPropertyChangedEventArgs e) + /// + /// Called when the property has to be coerced. + /// + /// The value to coerce. + protected virtual bool OnCoerceIsPaneOpen(bool value) { - var oldState = GetPseudoClass(e.GetOldValue()); - var newState = GetPseudoClass(e.GetNewValue()); + CancelRoutedEventArgs eventArgs; - PseudoClasses.Remove($":{oldState}"); - PseudoClasses.Add($":{newState}"); + if (value) + { + eventArgs = new CancelRoutedEventArgs(PaneOpeningEvent, this); + OnPaneOpening(eventArgs); + } + else + { + eventArgs = new CancelRoutedEventArgs(PaneClosingEvent, this); + OnPaneClosing(eventArgs); + } - var (closedPaneWidth, paneColumnGridLength) = e.GetNewValue() switch + if (eventArgs.Cancel) { - SplitViewDisplayMode.Overlay => (0, new GridLength(0, GridUnitType.Pixel)), - SplitViewDisplayMode.CompactOverlay => (CompactPaneLength, new GridLength(CompactPaneLength, GridUnitType.Pixel)), - SplitViewDisplayMode.Inline => (0, new GridLength(0, GridUnitType.Auto)), - SplitViewDisplayMode.CompactInline => (CompactPaneLength, new GridLength(0, GridUnitType.Auto)), - _ => throw new NotImplementedException(), - }; - TemplateSettings.ClosedPaneWidth = closedPaneWidth; - TemplateSettings.PaneColumnGridLength = paneColumnGridLength; - } + return !value; + } - private void OnUseLightDismissChanged(AvaloniaPropertyChangedEventArgs e) - { - var mode = (bool)e.NewValue!; - PseudoClasses.Set(":lightdismiss", mode); + return value; } - private void PaneChanged(AvaloniaPropertyChangedEventArgs e) + /// + /// Coerces/validates the property value. + /// + /// The instance. + /// The value to coerce. + /// The coerced/validated value. + private static bool CoerceIsPaneOpen(AvaloniaObject instance, bool value) { - if (e.OldValue is ILogical oldChild) + if (instance is SplitView splitView) { - LogicalChildren.Remove(oldChild); + return splitView.OnCoerceIsPaneOpen(value); } - if (e.NewValue is ILogical newChild) - { - LogicalChildren.Add(newChild); - } + return value; } } } diff --git a/src/Avalonia.Controls/SplitView/SplitViewDisplayMode.cs b/src/Avalonia.Controls/SplitView/SplitViewDisplayMode.cs new file mode 100644 index 0000000000..6333f96f86 --- /dev/null +++ b/src/Avalonia.Controls/SplitView/SplitViewDisplayMode.cs @@ -0,0 +1,29 @@ +namespace Avalonia.Controls +{ + /// + /// Defines constants for how the SplitView Pane should display + /// + public enum SplitViewDisplayMode + { + /// + /// Pane is displayed next to content, and does not auto collapse + /// when tapped outside + /// + Inline, + /// + /// Pane is displayed next to content. When collapsed, pane is still + /// visible according to CompactPaneLength. Pane does not auto collapse + /// when tapped outside + /// + CompactInline, + /// + /// Pane is displayed above content. Pane collapses when tapped outside + /// + Overlay, + /// + /// Pane is displayed above content. When collapsed, pane is still + /// visible according to CompactPaneLength. Pane collapses when tapped outside + /// + CompactOverlay + } +} diff --git a/src/Avalonia.Controls/SplitView/SplitViewPanePlacement.cs b/src/Avalonia.Controls/SplitView/SplitViewPanePlacement.cs new file mode 100644 index 0000000000..62c5387192 --- /dev/null +++ b/src/Avalonia.Controls/SplitView/SplitViewPanePlacement.cs @@ -0,0 +1,18 @@ +namespace Avalonia.Controls +{ + /// + /// Defines constants for where the Pane should appear + /// + public enum SplitViewPanePlacement + { + /// + /// The pane is shown to the left of content. + /// + Left, + + /// + /// The pane is shown to the right of content. + /// + Right + } +} diff --git a/src/Avalonia.Controls/SplitView/SplitViewTemplateSettings.cs b/src/Avalonia.Controls/SplitView/SplitViewTemplateSettings.cs new file mode 100644 index 0000000000..f2cbf55986 --- /dev/null +++ b/src/Avalonia.Controls/SplitView/SplitViewTemplateSettings.cs @@ -0,0 +1,32 @@ +namespace Avalonia.Controls.Primitives +{ + /// + /// Provides calculated values for use with the 's control theme or template. + /// This class is NOT intended for general use. + /// + public class SplitViewTemplateSettings : AvaloniaObject + { + internal SplitViewTemplateSettings() { } + + public static readonly StyledProperty ClosedPaneWidthProperty = + AvaloniaProperty.Register(nameof(ClosedPaneWidth), + 0d); + + public static readonly StyledProperty PaneColumnGridLengthProperty = + AvaloniaProperty.Register( + nameof(PaneColumnGridLength)); + + public double ClosedPaneWidth + { + get => GetValue(ClosedPaneWidthProperty); + internal set => SetValue(ClosedPaneWidthProperty, value); + } + + public GridLength PaneColumnGridLength + { + get => GetValue(PaneColumnGridLengthProperty); + internal set => SetValue(PaneColumnGridLengthProperty, value); + } + } +} diff --git a/src/Avalonia.Controls/SplitViewPaneClosingEventArgs.cs b/src/Avalonia.Controls/SplitViewPaneClosingEventArgs.cs deleted file mode 100644 index 46fb2d161b..0000000000 --- a/src/Avalonia.Controls/SplitViewPaneClosingEventArgs.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; - -namespace Avalonia.Controls -{ - public class SplitViewPaneClosingEventArgs : EventArgs - { - public bool Cancel { get; set; } - - public SplitViewPaneClosingEventArgs(bool cancel) - { - Cancel = cancel; - } - } -} diff --git a/src/Avalonia.Controls/ThemeVariantScope.cs b/src/Avalonia.Controls/ThemeVariantScope.cs index b9724251c7..f5ad4b2f94 100644 --- a/src/Avalonia.Controls/ThemeVariantScope.cs +++ b/src/Avalonia.Controls/ThemeVariantScope.cs @@ -7,6 +7,14 @@ namespace Avalonia.Controls /// public class ThemeVariantScope : Decorator { + /// + public static readonly StyledProperty ActualThemeVariantProperty = + ThemeVariant.ActualThemeVariantProperty.AddOwner(); + + /// + public static readonly StyledProperty RequestedThemeVariantProperty = + ThemeVariant.RequestedThemeVariantProperty.AddOwner(); + /// /// Gets or sets the UI theme variant that is used by the control (and its child elements) for resource determination. /// The UI theme you specify with ThemeVariant can override the app-level ThemeVariant. diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index eea175eb3c..fdcb8cc537 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -80,6 +80,14 @@ namespace Avalonia.Controls public static readonly StyledProperty TransparencyBackgroundFallbackProperty = AvaloniaProperty.Register(nameof(TransparencyBackgroundFallback), Brushes.White); + /// + public static readonly StyledProperty ActualThemeVariantProperty = + ThemeVariantScope.ActualThemeVariantProperty.AddOwner(); + + /// + public static readonly StyledProperty RequestedThemeVariantProperty = + ThemeVariantScope.RequestedThemeVariantProperty.AddOwner(); + /// /// Defines the event. /// @@ -96,7 +104,7 @@ namespace Avalonia.Controls private readonly IAccessKeyHandler? _accessKeyHandler; private readonly IKeyboardNavigationHandler? _keyboardNavigationHandler; private readonly IGlobalStyles? _globalStyles; - private readonly IGlobalThemeVariantProvider? _applicationThemeHost; + private readonly IThemeVariantHost? _applicationThemeHost; private readonly PointerOverPreProcessor? _pointerOverPreProcessor; private readonly IDisposable? _pointerOverPreProcessorSubscription; private readonly IDisposable? _backGestureSubscription; @@ -147,7 +155,7 @@ namespace Avalonia.Controls _inputManager = TryGetService(dependencyResolver); _keyboardNavigationHandler = TryGetService(dependencyResolver); _globalStyles = TryGetService(dependencyResolver); - _applicationThemeHost = TryGetService(dependencyResolver); + _applicationThemeHost = TryGetService(dependencyResolver); Renderer = impl.CreateRenderer(this); Renderer.SceneInvalidated += SceneInvalidated; @@ -642,7 +650,7 @@ namespace Avalonia.Controls private void GlobalActualThemeVariantChanged(object? sender, EventArgs e) { - SetValue(ActualThemeVariantProperty, ((IGlobalThemeVariantProvider)sender!).ActualThemeVariant, BindingPriority.Template); + SetValue(ActualThemeVariantProperty, ((IThemeVariantHost)sender!).ActualThemeVariant, BindingPriority.Template); } private void SceneInvalidated(object? sender, SceneInvalidatedEventArgs e) diff --git a/src/Avalonia.Controls/Utils/BorderRenderHelper.cs b/src/Avalonia.Controls/Utils/BorderRenderHelper.cs index 6239a5120d..799cc47d0c 100644 --- a/src/Avalonia.Controls/Utils/BorderRenderHelper.cs +++ b/src/Avalonia.Controls/Utils/BorderRenderHelper.cs @@ -148,7 +148,7 @@ namespace Avalonia.Controls.Utils var rrect = new RoundedRect(rect, _cornerRadius.TopLeft, _cornerRadius.TopRight, _cornerRadius.BottomRight, _cornerRadius.BottomLeft); - context.PlatformImpl.DrawRectangle(background, pen, rrect, boxShadows); + context.DrawRectangle(background, pen, rrect, boxShadows); } } diff --git a/src/Avalonia.Controls/VirtualizingStackPanel.cs b/src/Avalonia.Controls/VirtualizingStackPanel.cs index 8e7690aa6c..4970a333a5 100644 --- a/src/Avalonia.Controls/VirtualizingStackPanel.cs +++ b/src/Avalonia.Controls/VirtualizingStackPanel.cs @@ -1167,7 +1167,7 @@ namespace Avalonia.Controls // Update the indexes of the elements after the removed range. end = _elements.Count; - var newIndex = first; + var newIndex = first + start; for (var i = start; i < end; ++i) { if (_elements[i] is Control element) diff --git a/src/Avalonia.Diagnostics/Diagnostics/Controls/Application.cs b/src/Avalonia.Diagnostics/Diagnostics/Controls/Application.cs index a0ff3a714f..a8f609c507 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Controls/Application.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Controls/Application.cs @@ -14,7 +14,7 @@ namespace Avalonia.Diagnostics.Controls public event EventHandler? Closed; public static readonly StyledProperty RequestedThemeVariantProperty = - StyledElement.RequestedThemeVariantProperty.AddOwner(); + ThemeVariantScope.RequestedThemeVariantProperty.AddOwner(); public Application(Avalonia.Application application) { diff --git a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs index 5b84ceef7f..31aaebcdc7 100644 --- a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs +++ b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs @@ -325,7 +325,7 @@ namespace Avalonia.Headless } - public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) + public IDrawingContextImpl CreateDrawingContext() { return new HeadlessDrawingContextStub(); } @@ -491,7 +491,7 @@ namespace Avalonia.Headless } - public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) + public IDrawingContextImpl CreateDrawingContext() { return new HeadlessDrawingContextStub(); } diff --git a/src/Avalonia.Native/ClipboardImpl.cs b/src/Avalonia.Native/ClipboardImpl.cs index 9f1c8883aa..5a6b0df801 100644 --- a/src/Avalonia.Native/ClipboardImpl.cs +++ b/src/Avalonia.Native/ClipboardImpl.cs @@ -2,11 +2,11 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using System.Runtime.InteropServices; using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.Native.Interop; -using Avalonia.Platform.Interop; +using Avalonia.Platform.Storage; +using Avalonia.Platform.Storage.FileIO; namespace Avalonia.Native { @@ -56,8 +56,13 @@ namespace Avalonia.Native { if(fmt.String == NSPasteboardTypeString) rv.Add(DataFormats.Text); - if(fmt.String == NSFilenamesPboardType) - rv.Add(DataFormats.FileNames); + if (fmt.String == NSFilenamesPboardType) + { +#pragma warning disable CS0618 // Type or member is obsolete + rv.Add(DataFormats.FileNames); +#pragma warning restore CS0618 // Type or member is obsolete + rv.Add(DataFormats.Files); + } } } } @@ -74,7 +79,13 @@ namespace Avalonia.Native public IEnumerable GetFileNames() { using (var strings = _native.GetStrings(NSFilenamesPboardType)) - return strings.ToStringArray(); + return strings?.ToStringArray(); + } + + public IEnumerable GetFiles() + { + return GetFileNames()?.Select(f => StorageProviderHelpers.TryCreateBclStorageItem(f)!) + .Where(f => f is not null); } public unsafe Task SetDataObjectAsync(IDataObject data) @@ -102,8 +113,12 @@ namespace Avalonia.Native { if (format == DataFormats.Text) return await GetTextAsync(); +#pragma warning disable CS0618 // Type or member is obsolete if (format == DataFormats.FileNames) return GetFileNames(); +#pragma warning restore CS0618 // Type or member is obsolete + if (format == DataFormats.Files) + return GetFiles(); using (var n = _native.GetBytes(format)) return n.Bytes; } @@ -131,20 +146,16 @@ namespace Avalonia.Native public bool Contains(string dataFormat) => Formats.Contains(dataFormat); - public string GetText() - { - // bad idea in general, but API is synchronous anyway - return _clipboard.GetTextAsync().Result; - } - - public IEnumerable GetFileNames() => _clipboard.GetFileNames(); - public object Get(string dataFormat) { if (dataFormat == DataFormats.Text) - return GetText(); + return _clipboard.GetTextAsync().Result; + if (dataFormat == DataFormats.Files) + return _clipboard.GetFiles(); +#pragma warning disable CS0618 if (dataFormat == DataFormats.FileNames) - return GetFileNames(); +#pragma warning restore CS0618 + return _clipboard.GetFileNames(); return null; } } diff --git a/src/Avalonia.Themes.Simple/Controls/SplitButton.xaml b/src/Avalonia.Themes.Simple/Controls/SplitButton.xaml index 3c621a981d..2a7dc081f0 100644 --- a/src/Avalonia.Themes.Simple/Controls/SplitButton.xaml +++ b/src/Avalonia.Themes.Simple/Controls/SplitButton.xaml @@ -2,8 +2,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:converters="using:Avalonia.Controls.Converters"> - + @@ -26,37 +25,7 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - + @@ -75,66 +44,75 @@ - - - + + + diff --git a/src/Avalonia.Themes.Simple/Controls/SplitView.xaml b/src/Avalonia.Themes.Simple/Controls/SplitView.xaml index d6f293a730..f839e9a598 100644 --- a/src/Avalonia.Themes.Simple/Controls/SplitView.xaml +++ b/src/Avalonia.Themes.Simple/Controls/SplitView.xaml @@ -17,8 +17,6 @@ 320 48 - 00:00:00.2 00:00:00.1 0.1,0.9,0.2,1.0 @@ -240,7 +238,9 @@ diff --git a/src/Avalonia.X11/X11CursorFactory.cs b/src/Avalonia.X11/X11CursorFactory.cs index 56fd2f14ef..13068832fb 100644 --- a/src/Avalonia.X11/X11CursorFactory.cs +++ b/src/Avalonia.X11/X11CursorFactory.cs @@ -115,7 +115,7 @@ namespace Avalonia.X11 using (var cpuContext = platformRenderInterface.CreateBackendContext(null)) using (var renderTarget = cpuContext.CreateRenderTarget(new[] { this })) - using (var ctx = renderTarget.CreateDrawingContext(null)) + using (var ctx = renderTarget.CreateDrawingContext()) { var r = new Rect(_pixelSize.ToSize(1)); ctx.DrawBitmap(RefCountable.CreateUnownedNotClonable(bitmap), 1, r, r); diff --git a/src/Avalonia.X11/X11IconLoader.cs b/src/Avalonia.X11/X11IconLoader.cs index 51db815b31..84a1d35712 100644 --- a/src/Avalonia.X11/X11IconLoader.cs +++ b/src/Avalonia.X11/X11IconLoader.cs @@ -43,7 +43,7 @@ namespace Avalonia.X11 _bdata = new uint[_width * _height]; using(var cpuContext = AvaloniaLocator.Current.GetRequiredService().CreateBackendContext(null)) using(var rt = cpuContext.CreateRenderTarget(new[]{this})) - using (var ctx = rt.CreateDrawingContext(null)) + using (var ctx = rt.CreateDrawingContext()) ctx.DrawBitmap(bitmap.PlatformImpl, 1, new Rect(bitmap.Size), new Rect(0, 0, _width, _height)); Data = new UIntPtr[_width * _height + 2]; diff --git a/src/Browser/Avalonia.Browser/AvaloniaView.cs b/src/Browser/Avalonia.Browser/AvaloniaView.cs index 3bb7260e55..76947c949c 100644 --- a/src/Browser/Avalonia.Browser/AvaloniaView.cs +++ b/src/Browser/Avalonia.Browser/AvaloniaView.cs @@ -106,6 +106,8 @@ namespace Avalonia.Browser InputHelper.SubscribePointerEvents(_containerElement, OnPointerMove, OnPointerDown, OnPointerUp, OnPointerCancel, OnWheel); + InputHelper.SubscribeDropEvents(_containerElement, OnDragEvent); + var skiaOptions = AvaloniaLocator.Current.GetService(); _dpi = DomHelper.ObserveDpi(OnDpiChanged); @@ -293,6 +295,59 @@ namespace Avalonia.Browser return modifiers; } + public bool OnDragEvent(JSObject args) + { + var eventType = args?.GetPropertyAsString("type") switch + { + "dragenter" => RawDragEventType.DragEnter, + "dragover" => RawDragEventType.DragOver, + "dragleave" => RawDragEventType.DragLeave, + "drop" => RawDragEventType.Drop, + _ => (RawDragEventType)(int)-1 + }; + var dataObject = args?.GetPropertyAsJSObject("dataTransfer"); + if (args is null || eventType < 0 || dataObject is null) + { + return false; + } + + // If file is dropped, we need storage js to be referenced. + // TODO: restructure JS files, so it's not needed. + _ = AvaloniaModule.ImportStorage(); + + var position = new Point(args.GetPropertyAsDouble("offsetX"), args.GetPropertyAsDouble("offsetY")); + var modifiers = GetModifiers(args); + + var effectAllowedStr = dataObject.GetPropertyAsString("effectAllowed") ?? "none"; + var effectAllowed = DragDropEffects.None; + if (effectAllowedStr.Contains("copy", StringComparison.OrdinalIgnoreCase)) + { + effectAllowed |= DragDropEffects.Copy; + } + if (effectAllowedStr.Contains("link", StringComparison.OrdinalIgnoreCase)) + { + effectAllowed |= DragDropEffects.Link; + } + if (effectAllowedStr.Contains("move", StringComparison.OrdinalIgnoreCase)) + { + effectAllowed |= DragDropEffects.Move; + } + if (effectAllowedStr.Equals("all", StringComparison.OrdinalIgnoreCase)) + { + effectAllowed |= DragDropEffects.Move | DragDropEffects.Copy | DragDropEffects.Link; + } + if (effectAllowed == DragDropEffects.None) + { + return false; + } + + var dropEffect = _topLevelImpl.RawDragEvent(eventType, position, modifiers, new BrowserDataObject(dataObject), effectAllowed); + dataObject.SetProperty("dropEffect", dropEffect.ToString().ToLowerInvariant()); + + return eventType is RawDragEventType.Drop or RawDragEventType.DragOver + && dropEffect != DragDropEffects.None; + } + private bool OnKeyDown (string code, string key, int modifier) { var handled = _topLevelImpl.RawKeyboardEvent(RawKeyEventType.KeyDown, code, key, (RawInputModifiers)modifier); diff --git a/src/Browser/Avalonia.Browser/BrowserDataObject.cs b/src/Browser/Avalonia.Browser/BrowserDataObject.cs new file mode 100644 index 0000000000..f1e30ee3fe --- /dev/null +++ b/src/Browser/Avalonia.Browser/BrowserDataObject.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices.JavaScript; +using Avalonia.Browser.Interop; +using Avalonia.Browser.Storage; +using Avalonia.Input; +using Avalonia.Platform.Storage; + +namespace Avalonia.Browser; + +internal class BrowserDataObject : IDataObject +{ + private readonly JSObject _dataObject; + + public BrowserDataObject(JSObject dataObject) + { + _dataObject = dataObject; + } + + public IEnumerable GetDataFormats() + { + var types = new HashSet(_dataObject.GetPropertyAsStringArray("types")); + var dataFormats = new HashSet(types.Count); + + foreach (var type in types) + { + if (type.StartsWith("text/", StringComparison.Ordinal)) + { + dataFormats.Add(DataFormats.Text); + } + else if (type.Equals("Files", StringComparison.Ordinal)) + { + dataFormats.Add(DataFormats.Files); + } + dataFormats.Add(type); + } + + // If drag'n'drop an image from the another web page, if won't add "Files" to the supported types, but only a "text/uri-list". + // With "text/uri-list" browser can add actual file as well. + var filesCount = _dataObject.GetPropertyAsJSObject("files")?.GetPropertyAsInt32("count"); + if (filesCount > 0) + { + dataFormats.Add(DataFormats.Files); + } + + return dataFormats; + } + + public bool Contains(string dataFormat) + { + return GetDataFormats().Contains(dataFormat); + } + + public object? Get(string dataFormat) + { + if (dataFormat == DataFormats.Files) + { + var files = _dataObject.GetPropertyAsJSObject("files"); + if (files is not null) + { + return StorageHelper.FilesToItemsArray(files) + .Select(reference => reference.GetPropertyAsString("kind") switch + { + "directory" => (IStorageItem)new JSStorageFolder(reference), + "file" => new JSStorageFile(reference), + _ => null + }) + .Where(i => i is not null) + .ToArray()!; + } + + return null; + } + + if (dataFormat == DataFormats.Text) + { + if (_dataObject.CallMethodString("getData", "text/plain") is { Length :> 0 } textData) + { + return textData; + } + } + + if (_dataObject.CallMethodString("getData", dataFormat) is { Length: > 0 } data) + { + return data; + } + + return null; + } +} diff --git a/src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs b/src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs index f1cd441f45..1bf4636f61 100644 --- a/src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs +++ b/src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs @@ -164,6 +164,15 @@ namespace Avalonia.Browser return false; } + + public DragDropEffects RawDragEvent(RawDragEventType eventType, Point position, RawInputModifiers modifiers, BrowserDataObject dataObject, DragDropEffects dropEffect) + { + var device = AvaloniaLocator.Current.GetRequiredService(); + var eventArgs = new RawDragEvent(device, eventType, _inputRoot!, position, dataObject, dropEffect, modifiers); + Console.WriteLine($"{eventArgs.Location} {eventArgs.Effects} {eventArgs.Type} {eventArgs.KeyModifiers}"); + Input?.Invoke(eventArgs); + return eventArgs.Effects; + } public void Dispose() { diff --git a/src/Browser/Avalonia.Browser/ClipboardImpl.cs b/src/Browser/Avalonia.Browser/ClipboardImpl.cs index b94fe2df9e..c4f5e90777 100644 --- a/src/Browser/Avalonia.Browser/ClipboardImpl.cs +++ b/src/Browser/Avalonia.Browser/ClipboardImpl.cs @@ -24,6 +24,6 @@ namespace Avalonia.Browser public Task GetFormatsAsync() => Task.FromResult(Array.Empty()); - public Task GetDataAsync(string format) => Task.FromResult(new()); + public Task GetDataAsync(string format) => Task.FromResult(null); } } diff --git a/src/Browser/Avalonia.Browser/Interop/AvaloniaModule.cs b/src/Browser/Avalonia.Browser/Interop/AvaloniaModule.cs index f1936a8d97..394f191dab 100644 --- a/src/Browser/Avalonia.Browser/Interop/AvaloniaModule.cs +++ b/src/Browser/Avalonia.Browser/Interop/AvaloniaModule.cs @@ -1,24 +1,29 @@ -using System.Runtime.InteropServices.JavaScript; +using System; +using System.Runtime.InteropServices.JavaScript; using System.Threading.Tasks; namespace Avalonia.Browser.Interop; internal static partial class AvaloniaModule { - public const string MainModuleName = "avalonia"; - public const string StorageModuleName = "storage"; - - public static Task ImportMain() + private static readonly Lazy s_importMain = new(() => { var options = AvaloniaLocator.Current.GetService() ?? new BrowserPlatformOptions(); return JSHost.ImportAsync(MainModuleName, options.FrameworkAssetPathResolver!("avalonia.js")); - } + }); - public static Task ImportStorage() + private static readonly Lazy s_importStorage = new(() => { var options = AvaloniaLocator.Current.GetService() ?? new BrowserPlatformOptions(); return JSHost.ImportAsync(StorageModuleName, options.FrameworkAssetPathResolver!("storage.js")); - } + }); + + public const string MainModuleName = "avalonia"; + public const string StorageModuleName = "storage"; + + public static Task ImportMain() => s_importMain.Value; + + public static Task ImportStorage() => s_importStorage.Value; [JSImport("Caniuse.isMobile", AvaloniaModule.MainModuleName)] public static partial bool IsMobile(); diff --git a/src/Browser/Avalonia.Browser/Interop/GeneralHelpers.cs b/src/Browser/Avalonia.Browser/Interop/GeneralHelpers.cs new file mode 100644 index 0000000000..6e3b41c05b --- /dev/null +++ b/src/Browser/Avalonia.Browser/Interop/GeneralHelpers.cs @@ -0,0 +1,22 @@ +using System.Runtime.InteropServices.JavaScript; + +namespace Avalonia.Browser.Interop; + +internal static partial class GeneralHelpers +{ + [JSImport("GeneralHelpers.itemsArrayAt", AvaloniaModule.MainModuleName)] + public static partial JSObject[] ItemsArrayAt(JSObject jsObject, string key); + public static JSObject[] GetPropertyAsJSObjectArray(this JSObject jsObject, string key) => ItemsArrayAt(jsObject, key); + + [JSImport("GeneralHelpers.itemsArrayAt", AvaloniaModule.MainModuleName)] + public static partial string[] ItemsArrayAtAsStrings(JSObject jsObject, string key); + public static string[] GetPropertyAsStringArray(this JSObject jsObject, string key) => ItemsArrayAtAsStrings(jsObject, key); + + [JSImport("GeneralHelpers.callMethod", AvaloniaModule.MainModuleName)] + public static partial string IntCallMethodString(JSObject jsObject, string name); + [JSImport("GeneralHelpers.callMethod", AvaloniaModule.MainModuleName)] + public static partial string IntCallMethodStringString(JSObject jsObject, string name, string arg1); + + public static string CallMethodString(this JSObject jsObject, string name) => IntCallMethodString(jsObject, name); + public static string CallMethodString(this JSObject jsObject, string name, string arg1) => IntCallMethodStringString(jsObject, name, arg1); +} diff --git a/src/Browser/Avalonia.Browser/Interop/InputHelper.cs b/src/Browser/Avalonia.Browser/Interop/InputHelper.cs index a816e39da8..a978c18f9b 100644 --- a/src/Browser/Avalonia.Browser/Interop/InputHelper.cs +++ b/src/Browser/Avalonia.Browser/Interop/InputHelper.cs @@ -43,13 +43,16 @@ internal static partial class InputHelper [JSMarshalAs>] Func wheel); - [JSImport("InputHelper.subscribeInputEvents", AvaloniaModule.MainModuleName)] public static partial void SubscribeInputEvents( JSObject htmlElement, [JSMarshalAs>] Func input); + [JSImport("InputHelper.subscribeDropEvents", AvaloniaModule.MainModuleName)] + public static partial void SubscribeDropEvents(JSObject containerElement, + [JSMarshalAs>] Func dragEvent); + [JSImport("InputHelper.getCoalescedEvents", AvaloniaModule.MainModuleName)] [return: JSMarshalAs>] public static partial JSObject[] GetCoalescedEvents(JSObject pointerEvent); diff --git a/src/Browser/Avalonia.Browser/Interop/StorageHelper.cs b/src/Browser/Avalonia.Browser/Interop/StorageHelper.cs index 11beba6f2c..2d96ee8d1f 100644 --- a/src/Browser/Avalonia.Browser/Interop/StorageHelper.cs +++ b/src/Browser/Avalonia.Browser/Interop/StorageHelper.cs @@ -46,6 +46,9 @@ internal static partial class StorageHelper [JSImport("StorageItems.itemsArray", AvaloniaModule.StorageModuleName)] public static partial JSObject[] ItemsArray(JSObject item); + + [JSImport("StorageItems.filesToItemsArray", AvaloniaModule.StorageModuleName)] + public static partial JSObject[] FilesToItemsArray(JSObject item); [JSImport("StorageProvider.createAcceptType", AvaloniaModule.StorageModuleName)] public static partial JSObject CreateAcceptType(string description, string[] mimeTypes, string[]? extensions); diff --git a/src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs b/src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs index 5b76d53a9d..fc32b3b4f7 100644 --- a/src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs +++ b/src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs @@ -1,10 +1,8 @@ using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Runtime.InteropServices.JavaScript; -using System.Runtime.Versioning; using System.Threading.Tasks; using Avalonia.Browser.Interop; using Avalonia.Platform.Storage; @@ -18,15 +16,13 @@ internal class BrowserStorageProvider : IStorageProvider internal const string PickerCancelMessage = "The user aborted a request"; internal const string NoPermissionsMessage = "Permissions denied"; - private readonly Lazy _lazyModule = new(() => AvaloniaModule.ImportStorage()); - public bool CanOpen => true; public bool CanSave => StorageHelper.HasNativeFilePicker(); public bool CanPickFolder => true; public async Task> OpenFilePickerAsync(FilePickerOpenOptions options) { - await _lazyModule.Value; + await AvaloniaModule.ImportStorage(); var startIn = (options.SuggestedStartLocation as JSStorageItem)?.FileHandle; var (types, excludeAll) = ConvertFileTypes(options.FileTypeFilter); @@ -60,7 +56,7 @@ internal class BrowserStorageProvider : IStorageProvider public async Task SaveFilePickerAsync(FilePickerSaveOptions options) { - await _lazyModule.Value; + await AvaloniaModule.ImportStorage(); var startIn = (options.SuggestedStartLocation as JSStorageItem)?.FileHandle; var (types, excludeAll) = ConvertFileTypes(options.FileTypeChoices); @@ -88,7 +84,7 @@ internal class BrowserStorageProvider : IStorageProvider public async Task> OpenFolderPickerAsync(FolderPickerOpenOptions options) { - await _lazyModule.Value; + await AvaloniaModule.ImportStorage(); var startIn = (options.SuggestedStartLocation as JSStorageItem)?.FileHandle; try @@ -104,14 +100,14 @@ internal class BrowserStorageProvider : IStorageProvider public async Task OpenFileBookmarkAsync(string bookmark) { - await _lazyModule.Value; + await AvaloniaModule.ImportStorage(); var item = await StorageHelper.OpenBookmark(bookmark); return item is not null ? new JSStorageFile(item) : null; } public async Task OpenFolderBookmarkAsync(string bookmark) { - await _lazyModule.Value; + await AvaloniaModule.ImportStorage(); var item = await StorageHelper.OpenBookmark(bookmark); return item is not null ? new JSStorageFolder(item) : null; } @@ -128,7 +124,7 @@ internal class BrowserStorageProvider : IStorageProvider public async Task TryGetWellKnownFolderAsync(WellKnownFolder wellKnownFolder) { - await _lazyModule.Value; + await AvaloniaModule.ImportStorage(); var directory = StorageHelper.CreateWellKnownDirectory(wellKnownFolder switch { WellKnownFolder.Desktop => "desktop", diff --git a/src/Browser/Avalonia.Browser/webapp/modules/avalonia.ts b/src/Browser/Avalonia.Browser/webapp/modules/avalonia.ts index 3fb4124c96..80faca7a50 100644 --- a/src/Browser/Avalonia.Browser/webapp/modules/avalonia.ts +++ b/src/Browser/Avalonia.Browser/webapp/modules/avalonia.ts @@ -5,6 +5,7 @@ import { Caniuse } from "./avalonia/caniuse"; import { StreamHelper } from "./avalonia/stream"; import { NativeControlHost } from "./avalonia/nativeControlHost"; import { NavigationHelper } from "./avalonia/navigationHelper"; +import { GeneralHelpers } from "./avalonia/generalHelpers"; export { Caniuse, @@ -15,5 +16,6 @@ export { AvaloniaDOM, StreamHelper, NativeControlHost, - NavigationHelper + NavigationHelper, + GeneralHelpers }; diff --git a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/generalHelpers.ts b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/generalHelpers.ts new file mode 100644 index 0000000000..fa001006ab --- /dev/null +++ b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/generalHelpers.ts @@ -0,0 +1,19 @@ +export class GeneralHelpers { + public static itemsArrayAt(instance: any, key: string): any[] { + const items = instance[key]; + if (!items) { + return []; + } + + const retItems = []; + for (let i = 0; i < items.length; i++) { + retItems[i] = items[i]; + } + return retItems; + } + + public static callMethod(instance: any, name: string /*, args */): any { + const args = Array.prototype.slice.call(arguments, 2); + return instance[name].apply(instance, args); + } +} diff --git a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/input.ts b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/input.ts index 0f0e5eb512..fb94352192 100644 --- a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/input.ts +++ b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/input.ts @@ -174,6 +174,28 @@ export class InputHelper { }; } + public static subscribeDropEvents( + element: HTMLInputElement, + dragEvent: (args: any) => boolean + ) { + const dragHandler = (args: Event) => { + if (dragEvent(args as any)) { + args.preventDefault(); + } + }; + element.addEventListener("dragover", dragHandler); + element.addEventListener("dragenter", dragHandler); + element.addEventListener("dragleave", dragHandler); + element.addEventListener("drop", dragHandler); + + return () => { + element.removeEventListener("dragover", dragHandler); + element.removeEventListener("dragenter", dragHandler); + element.removeEventListener("dragleave", dragHandler); + element.removeEventListener("drop", dragHandler); + }; + } + public static getCoalescedEvents(pointerEvent: PointerEvent): PointerEvent[] { return pointerEvent.getCoalescedEvents(); } diff --git a/src/Browser/Avalonia.Browser/webapp/modules/storage/storageItem.ts b/src/Browser/Avalonia.Browser/webapp/modules/storage/storageItem.ts index 8f47e61100..f444717094 100644 --- a/src/Browser/Avalonia.Browser/webapp/modules/storage/storageItem.ts +++ b/src/Browser/Avalonia.Browser/webapp/modules/storage/storageItem.ts @@ -3,8 +3,9 @@ import { FileSystemFileHandle, FileSystemDirectoryHandle, FileSystemWritableFile import { Caniuse } from "../avalonia"; export class StorageItem { - constructor( + private constructor( public handle?: FileSystemFileHandle | FileSystemDirectoryHandle, + private readonly file?: File, private readonly bookmarkId?: string, public wellKnownType?: WellKnownDirectory ) { @@ -14,6 +15,9 @@ export class StorageItem { if (this.handle) { return this.handle.name; } + if (this.file) { + return this.file.name; + } return this.wellKnownType ?? ""; } @@ -21,14 +25,29 @@ export class StorageItem { if (this.handle) { return this.handle.kind; } + if (this.file) { + return "file"; + } return "directory"; } + public static createFromHandle(handle: FileSystemFileHandle | FileSystemDirectoryHandle, bookmarkId?: string) { + return new StorageItem(handle, undefined, bookmarkId, undefined); + } + + public static createFromFile(file: File) { + return new StorageItem(undefined, file, undefined, undefined); + } + public static createWellKnownDirectory(type: WellKnownDirectory) { - return new StorageItem(undefined, undefined, type); + return new StorageItem(undefined, undefined, undefined, type); } public static async openRead(item: StorageItem): Promise { + if (item.file) { + return item.file; + } + if (!item.handle || item.kind !== "file") { throw new Error("StorageItem is not a file"); } @@ -41,7 +60,7 @@ export class StorageItem { public static async openWrite(item: StorageItem): Promise { if (!item.handle || item.kind !== "file") { - throw new Error("StorageItem is not a file"); + throw new Error("StorageItem is not a writeable file"); } await item.verityPermissions("readwrite"); @@ -52,8 +71,9 @@ export class StorageItem { public static async getProperties(item: StorageItem): Promise<{ Size: number; LastModified: number; Type: string } | null> { // getFile can fail with an exception depending if we use polyfill with a save file dialog or not. try { - const file = item.handle instanceof FileSystemFileHandle && - await item.handle.getFile(); + const file = item.handle && "getFile" in item.handle + ? await item.handle.getFile() + : item.file; if (!file) { return null; @@ -144,4 +164,16 @@ export class StorageItems { public static itemsArray(instance: StorageItems): StorageItem[] { return instance.items; } + + public static filesToItemsArray(files: File[]): StorageItem[] { + if (!files) { + return []; + } + + const retItems = []; + for (let i = 0; i < files.length; i++) { + retItems[i] = StorageItem.createFromFile(files[i]); + } + return retItems; + } } diff --git a/src/Browser/Avalonia.Browser/webapp/modules/storage/storageProvider.ts b/src/Browser/Avalonia.Browser/webapp/modules/storage/storageProvider.ts index 750c38b8ea..7a29992674 100644 --- a/src/Browser/Avalonia.Browser/webapp/modules/storage/storageProvider.ts +++ b/src/Browser/Avalonia.Browser/webapp/modules/storage/storageProvider.ts @@ -19,7 +19,7 @@ export class StorageProvider { }; const handle = await showDirectoryPicker(options as any); - return new StorageItem(handle); + return StorageItem.createFromHandle(handle); } public static async openFileDialog( @@ -33,7 +33,7 @@ export class StorageProvider { }; const handles = await showOpenFilePicker(options); - return new StorageItems(handles.map((handle: FileSystemFileHandle) => new StorageItem(handle))); + return new StorageItems(handles.map((handle: FileSystemFileHandle) => StorageItem.createFromHandle(handle))); } public static async saveFileDialog( @@ -48,14 +48,14 @@ export class StorageProvider { // Always prefer native save file picker, as polyfill solutions are not reliable. const handle = await (globalThis as any).showSaveFilePicker(options); - return new StorageItem(handle); + return StorageItem.createFromHandle(handle); } public static async openBookmark(key: string): Promise { const connection = await avaloniaDb.connect(); try { const handle = await connection.get(fileBookmarksStore, key); - return handle && new StorageItem(handle, key); + return handle && StorageItem.createFromHandle(handle, key); } finally { connection.close(); } diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs index cdd344becc..bef32766dc 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs @@ -34,7 +34,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions var stack = serviceProvider.GetService(); var provideTarget = serviceProvider.GetService(); - var themeVariant = (provideTarget.TargetObject as StyledElement)?.ActualThemeVariant; + var themeVariant = (provideTarget.TargetObject as IThemeVariantHost)?.ActualThemeVariant; var targetType = provideTarget.TargetProperty switch { diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index 969f0b5e2a..db7b068543 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -10,7 +10,9 @@ using Avalonia.Rendering.SceneGraph; using Avalonia.Rendering.Utilities; using Avalonia.Utilities; using Avalonia.Media.Imaging; +using Avalonia.Skia.Helpers; using SkiaSharp; +using ISceneBrush = Avalonia.Media.ISceneBrush; namespace Avalonia.Skia { @@ -25,7 +27,6 @@ namespace Avalonia.Skia private readonly Stack _opacityStack = new(); private readonly Stack _blendingModeStack = new(); private readonly Matrix? _postTransform; - private readonly IVisualBrushRenderer? _visualBrushRenderer; private double _currentOpacity = 1.0f; private BitmapBlendingMode _currentBlendingMode = BitmapBlendingMode.SourceOver; private readonly bool _canTextUseLcdRendering; @@ -61,12 +62,7 @@ namespace Avalonia.Skia /// Dpi of drawings. /// public Vector Dpi; - - /// - /// Visual brush renderer. - /// - public IVisualBrushRenderer? VisualBrushRenderer; - + /// /// Render text without Lcd rendering. /// @@ -141,7 +137,6 @@ namespace Avalonia.Skia ?? throw new ArgumentException("Invalid create info - no Canvas provided", nameof(createInfo)); _dpi = createInfo.Dpi; - _visualBrushRenderer = createInfo.VisualBrushRenderer; _disposables = disposables; _canTextUseLcdRendering = !createInfo.DisableTextLcdRendering; _grContext = createInfo.GrContext; @@ -908,7 +903,7 @@ namespace Avalonia.Skia paintWrapper.AddDisposable(intermediate); - using (var context = intermediate.CreateDrawingContext(null)) + using (var context = intermediate.CreateDrawingContext()) { var sourceRect = new Rect(tileBrushImage.PixelSize.ToSizeWithDpi(96)); var targetRect = new Rect(tileBrushImage.PixelSize.ToSizeWithDpi(_dpi)); @@ -970,36 +965,98 @@ namespace Avalonia.Skia } } - /// - /// Configure paint wrapper to use visual brush. - /// - /// Paint wrapper. - /// Visual brush. - /// Visual brush renderer. - /// Tile brush image. - private void ConfigureVisualBrush(ref PaintWrapper paintWrapper, IVisualBrush visualBrush, - IVisualBrushRenderer? visualBrushRenderer, ref IDrawableBitmapImpl? tileBrushImage) + private void ConfigureSceneBrushContent(ref PaintWrapper paintWrapper, ISceneBrushContent content, + Size targetSize) { - if (visualBrushRenderer == null) - { - throw new NotSupportedException("No IVisualBrushRenderer was supplied to DrawingContextImpl."); - } - - var intermediateSize = visualBrushRenderer.GetRenderTargetSize(visualBrush); + if(content.UseScalableRasterization) + ConfigureSceneBrushContentWithPicture(ref paintWrapper, content, targetSize); + else + ConfigureSceneBrushContentWithSurface(ref paintWrapper, content, targetSize); + } + + private void ConfigureSceneBrushContentWithSurface(ref PaintWrapper paintWrapper, ISceneBrushContent content, + Size targetSize) + { + var rect = content.Rect; + var intermediateSize = rect.Size; if (intermediateSize.Width >= 1 && intermediateSize.Height >= 1) { - var intermediate = CreateRenderTarget(intermediateSize, false); + using var intermediate = CreateRenderTarget(intermediateSize, false); - using (var ctx = intermediate.CreateDrawingContext(visualBrushRenderer)) + using (var ctx = intermediate.CreateDrawingContext()) { ctx.Clear(Colors.Transparent); - - visualBrushRenderer.RenderVisualBrush(ctx, visualBrush); + content.Render(ctx, rect.TopLeft == default ? null : Matrix.CreateTranslation(-rect.X, -rect.Y)); } - tileBrushImage = intermediate; - paintWrapper.AddDisposable(tileBrushImage); + ConfigureTileBrush(ref paintWrapper, targetSize, content.Brush, intermediate); + } + } + + private void ConfigureSceneBrushContentWithPicture(ref PaintWrapper paintWrapper, ISceneBrushContent content, + Size targetSize) + { + var rect = content.Rect; + var contentSize = rect.Size; + if (contentSize.Width <= 0 || contentSize.Height <= 0) + { + paintWrapper.Paint.Color = SKColor.Empty; + return; + } + + var tileBrush = content.Brush; + var transform = rect.TopLeft == default ? Matrix.Identity : Matrix.CreateTranslation(-rect.X, -rect.Y); + + var calc = new TileBrushCalculator(tileBrush, contentSize, targetSize); + transform *= calc.IntermediateTransform; + + using var pictureTarget = new PictureRenderTarget(_gpu, _grContext, _dpi); + using (var ctx = pictureTarget.CreateDrawingContext(calc.IntermediateSize)) + { + ctx.PushClip(calc.IntermediateClip); + content.Render(ctx, transform); + ctx.PopClip(); + } + + using var picture = pictureTarget.GetPicture(); + + var paintTransform = + tileBrush.TileMode != TileMode.None + ? SKMatrix.CreateTranslation(-(float)calc.DestinationRect.X, -(float)calc.DestinationRect.Y) + : SKMatrix.CreateIdentity(); + + SKShaderTileMode tileX = + tileBrush.TileMode == TileMode.None + ? SKShaderTileMode.Clamp + : tileBrush.TileMode == TileMode.FlipX || tileBrush.TileMode == TileMode.FlipXY + ? SKShaderTileMode.Mirror + : SKShaderTileMode.Repeat; + + SKShaderTileMode tileY = + tileBrush.TileMode == TileMode.None + ? SKShaderTileMode.Clamp + : tileBrush.TileMode == TileMode.FlipY || tileBrush.TileMode == TileMode.FlipXY + ? SKShaderTileMode.Mirror + : SKShaderTileMode.Repeat; + + paintTransform = SKMatrix.Concat(paintTransform, + SKMatrix.CreateScale((float)(96.0 / _dpi.X), (float)(96.0 / _dpi.Y))); + + if (tileBrush.Transform is { }) + { + var origin = tileBrush.TransformOrigin.ToPixels(targetSize); + var offset = Matrix.CreateTranslation(origin); + var brushTransform = (-offset) * tileBrush.Transform.Value * (offset); + + paintTransform = paintTransform.PreConcat(brushTransform.ToSKMatrix()); + } + + using (var shader = picture.ToShader(tileX, tileY, paintTransform, + new SKRect(0, 0, picture.CullRect.Width, picture.CullRect.Height))) + { + paintWrapper.Paint.FilterQuality = SKFilterQuality.None; + paintWrapper.Paint.Shader = shader; } } @@ -1113,12 +1170,25 @@ namespace Avalonia.Skia } var tileBrush = brush as ITileBrush; - var visualBrush = brush as IVisualBrush; var tileBrushImage = default(IDrawableBitmapImpl); - if (visualBrush != null) + if (brush is ISceneBrush sceneBrush) { - ConfigureVisualBrush(ref paintWrapper, visualBrush, _visualBrushRenderer, ref tileBrushImage); + using (var content = sceneBrush.CreateContent()) + { + if (content != null) + { + ConfigureSceneBrushContent(ref paintWrapper, content, targetSize); + return paintWrapper; + } + else + paint.Color = default; + } + } + else if (brush is ISceneBrushContent sceneBrushContent) + { + ConfigureSceneBrushContent(ref paintWrapper, sceneBrushContent, targetSize); + return paintWrapper; } else { diff --git a/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs b/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs index f1216100bc..a22b67e09e 100644 --- a/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs @@ -36,7 +36,7 @@ namespace Avalonia.Skia } /// - public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer? visualBrushRenderer) + public IDrawingContextImpl CreateDrawingContext() { var framebuffer = _platformSurface.Lock(); var framebufferImageInfo = new SKImageInfo(framebuffer.Size.Width, framebuffer.Size.Height, @@ -55,7 +55,6 @@ namespace Avalonia.Skia { Surface = _framebufferSurface, Dpi = framebuffer.Dpi, - VisualBrushRenderer = visualBrushRenderer, DisableTextLcdRendering = true }; diff --git a/src/Skia/Avalonia.Skia/Gpu/SkiaGpuRenderTarget.cs b/src/Skia/Avalonia.Skia/Gpu/SkiaGpuRenderTarget.cs index 7f9108481d..797c565ca1 100644 --- a/src/Skia/Avalonia.Skia/Gpu/SkiaGpuRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/Gpu/SkiaGpuRenderTarget.cs @@ -22,7 +22,7 @@ namespace Avalonia.Skia _renderTarget.Dispose(); } - public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer? visualBrushRenderer) + public IDrawingContextImpl CreateDrawingContext() { var session = _renderTarget.BeginRenderingSession(); @@ -31,7 +31,6 @@ namespace Avalonia.Skia GrContext = session.GrContext, Surface = session.SkSurface, Dpi = SkiaPlatform.DefaultDpi * session.ScaleFactor, - VisualBrushRenderer = visualBrushRenderer, DisableTextLcdRendering = true, Gpu = _skiaGpu, CurrentSession = session diff --git a/src/Skia/Avalonia.Skia/Helpers/DrawingContextHelper.cs b/src/Skia/Avalonia.Skia/Helpers/DrawingContextHelper.cs index 4d8afe9830..ec24f8f624 100644 --- a/src/Skia/Avalonia.Skia/Helpers/DrawingContextHelper.cs +++ b/src/Skia/Avalonia.Skia/Helpers/DrawingContextHelper.cs @@ -15,13 +15,12 @@ namespace Avalonia.Skia.Helpers /// /// /// DrawingContext - public static IDrawingContextImpl WrapSkiaCanvas(SKCanvas canvas, Vector dpi, IVisualBrushRenderer? visualBrushRenderer = null) + public static IDrawingContextImpl WrapSkiaCanvas(SKCanvas canvas, Vector dpi) { var createInfo = new DrawingContextImpl.CreateInfo { Canvas = canvas, Dpi = dpi, - VisualBrushRenderer = visualBrushRenderer, DisableTextLcdRendering = true, }; diff --git a/src/Skia/Avalonia.Skia/Helpers/ImageSavingHelper.cs b/src/Skia/Avalonia.Skia/Helpers/ImageSavingHelper.cs index 4cb1430a3b..6adfc01951 100644 --- a/src/Skia/Avalonia.Skia/Helpers/ImageSavingHelper.cs +++ b/src/Skia/Avalonia.Skia/Helpers/ImageSavingHelper.cs @@ -60,5 +60,15 @@ namespace Avalonia.Skia.Helpers } } } + + // This method is here mostly for debugging purposes + internal static void SavePicture(SKPicture picture, float scale, string path) + { + var snapshotSize = new SKSizeI((int)Math.Ceiling(picture.CullRect.Width * scale), + (int)Math.Ceiling(picture.CullRect.Height * scale)); + using var snap = + SKImage.FromPicture(picture, snapshotSize, SKMatrix.CreateScale(scale, scale)); + SaveImage(snap, path); + } } } diff --git a/src/Skia/Avalonia.Skia/PictureRenderTarget.cs b/src/Skia/Avalonia.Skia/PictureRenderTarget.cs new file mode 100644 index 0000000000..280b7c27cd --- /dev/null +++ b/src/Skia/Avalonia.Skia/PictureRenderTarget.cs @@ -0,0 +1,55 @@ +using System; +using Avalonia.Platform; +using Avalonia.Reactive; +using SkiaSharp; + +namespace Avalonia.Skia; + +internal class PictureRenderTarget : IDisposable +{ + private readonly ISkiaGpu? _gpu; + private readonly GRContext? _grContext; + private readonly Vector _dpi; + private SKPicture? _picture; + + public PictureRenderTarget(ISkiaGpu? gpu, GRContext? grContext, Vector dpi) + { + _gpu = gpu; + _grContext = grContext; + _dpi = dpi; + } + + public SKPicture GetPicture() + { + var rv = _picture ?? throw new InvalidOperationException(); + _picture = null; + return rv; + } + + public IDrawingContextImpl CreateDrawingContext(Size size) + { + var recorder = new SKPictureRecorder(); + var canvas = recorder.BeginRecording(new SKRect(0, 0, (float)(size.Width * _dpi.X / 96), + (float)(size.Height * _dpi.Y / 96))); + + canvas.RestoreToCount(-1); + canvas.ResetMatrix(); + + var createInfo = new DrawingContextImpl.CreateInfo + { + Canvas = canvas, + Dpi = _dpi, + DisableTextLcdRendering = true, + GrContext = _grContext, + Gpu = _gpu, + }; + return new DrawingContextImpl(createInfo, Disposable.Create(() => + { + _picture = recorder.EndRecording(); + canvas.Dispose(); + recorder.Dispose(); + })); + } + + public void Dispose() => _picture?.Dispose(); +} \ No newline at end of file diff --git a/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs b/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs index f88e74d738..92210c30e2 100644 --- a/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs @@ -97,7 +97,7 @@ namespace Avalonia.Skia } /// - public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer? visualBrushRenderer) + public IDrawingContextImpl CreateDrawingContext() { _canvas.RestoreToCount(-1); _canvas.ResetMatrix(); @@ -106,7 +106,6 @@ namespace Avalonia.Skia { Surface = _surface.Surface, Dpi = Dpi, - VisualBrushRenderer = visualBrushRenderer, DisableTextLcdRendering = _disableLcdRendering, GrContext = _grContext, Gpu = _gpu, diff --git a/src/Windows/Avalonia.Direct2D1/ExternalRenderTarget.cs b/src/Windows/Avalonia.Direct2D1/ExternalRenderTarget.cs index 02932c52da..10f9239b1a 100644 --- a/src/Windows/Avalonia.Direct2D1/ExternalRenderTarget.cs +++ b/src/Windows/Avalonia.Direct2D1/ExternalRenderTarget.cs @@ -21,11 +21,11 @@ namespace Avalonia.Direct2D1 _externalRenderTargetProvider.DestroyRenderTarget(); } - public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) + public IDrawingContextImpl CreateDrawingContext() { var target = _externalRenderTargetProvider.GetOrCreateRenderTarget(); _externalRenderTargetProvider.BeforeDrawing(); - return new DrawingContextImpl(visualBrushRenderer, null, target, null, () => + return new DrawingContextImpl( null, target, null, () => { try { diff --git a/src/Windows/Avalonia.Direct2D1/FramebufferShimRenderTarget.cs b/src/Windows/Avalonia.Direct2D1/FramebufferShimRenderTarget.cs index 984a24fb30..0af326d6a8 100644 --- a/src/Windows/Avalonia.Direct2D1/FramebufferShimRenderTarget.cs +++ b/src/Windows/Avalonia.Direct2D1/FramebufferShimRenderTarget.cs @@ -22,7 +22,7 @@ namespace Avalonia.Direct2D1 { } - public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) + public IDrawingContextImpl CreateDrawingContext() { var locked = _surface.Lock(); if (locked.Format == PixelFormat.Rgb565) @@ -32,7 +32,7 @@ namespace Avalonia.Direct2D1 } return new FramebufferShim(locked) - .CreateDrawingContext(visualBrushRenderer); + .CreateDrawingContext(); } public bool IsCorrupted => false; @@ -47,9 +47,9 @@ namespace Avalonia.Direct2D1 _target = target; } - public override IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) + public override IDrawingContextImpl CreateDrawingContext() { - return base.CreateDrawingContext(visualBrushRenderer, () => + return base.CreateDrawingContext(() => { using (var l = WicImpl.Lock(BitmapLockFlags.Read)) { diff --git a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs index 0dd9c155bb..f9b5953e3f 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs @@ -19,7 +19,6 @@ namespace Avalonia.Direct2D1.Media /// internal class DrawingContextImpl : IDrawingContextImpl { - private readonly IVisualBrushRenderer _visualBrushRenderer; private readonly ILayerFactory _layerFactory; private readonly SharpDX.Direct2D1.RenderTarget _renderTarget; private readonly DeviceContext _deviceContext; @@ -39,13 +38,11 @@ namespace Avalonia.Direct2D1.Media /// An optional swap chain associated with this drawing context. /// An optional delegate to be called when context is disposed. public DrawingContextImpl( - IVisualBrushRenderer visualBrushRenderer, ILayerFactory layerFactory, SharpDX.Direct2D1.RenderTarget renderTarget, SharpDX.DXGI.SwapChain1 swapChain = null, Action finishedCallback = null) { - _visualBrushRenderer = visualBrushRenderer; _layerFactory = layerFactory; _renderTarget = renderTarget; _swapChain = swapChain; @@ -491,7 +488,8 @@ namespace Avalonia.Direct2D1.Media var radialGradientBrush = brush as IRadialGradientBrush; var conicGradientBrush = brush as IConicGradientBrush; var imageBrush = brush as IImageBrush; - var visualBrush = brush as IVisualBrush; + var sceneBrush = brush as ISceneBrush; + var sceneBrushContent = brush as ISceneBrushContent; if (solidColorBrush != null) { @@ -518,11 +516,13 @@ namespace Avalonia.Direct2D1.Media (BitmapImpl)imageBrush.Source.PlatformImpl.Item, destinationSize); } - else if (visualBrush != null) + else if (sceneBrush != null || sceneBrushContent != null) { - if (_visualBrushRenderer != null) + sceneBrushContent ??= sceneBrush.CreateContent(); + if (sceneBrushContent != null) { - var intermediateSize = _visualBrushRenderer.GetRenderTargetSize(visualBrush); + var rect = sceneBrushContent.Rect; + var intermediateSize = rect.Size; if (intermediateSize.Width >= 1 && intermediateSize.Height >= 1) { @@ -533,28 +533,26 @@ namespace Avalonia.Direct2D1.Media var pixelSize = PixelSize.FromSizeWithDpi(intermediateSize, dpi); using (var intermediate = new BitmapRenderTarget( - _deviceContext, - CompatibleRenderTargetOptions.None, - pixelSize.ToSizeWithDpi(dpi).ToSharpDX())) + _deviceContext, + CompatibleRenderTargetOptions.None, + pixelSize.ToSizeWithDpi(dpi).ToSharpDX())) { - using (var ctx = new RenderTarget(intermediate).CreateDrawingContext(_visualBrushRenderer)) + using (var ctx = new RenderTarget(intermediate).CreateDrawingContext()) { intermediate.Clear(null); - _visualBrushRenderer.RenderVisualBrush(ctx, visualBrush); + sceneBrushContent.Render(ctx, + rect.TopLeft == default ? null : Matrix.CreateTranslation(-rect.X, -rect.Y)); } return new ImageBrushImpl( - visualBrush, + sceneBrushContent.Brush, _deviceContext, new D2DBitmapImpl(intermediate.Bitmap), destinationSize); } + } } - else - { - throw new NotSupportedException("No IVisualBrushRenderer was supplied to DrawingContextImpl."); - } } return new SolidColorBrushImpl(null, _deviceContext); diff --git a/src/Windows/Avalonia.Direct2D1/Media/ImageBrushImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/ImageBrushImpl.cs index 829b887d9d..a08c96c40c 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/ImageBrushImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/ImageBrushImpl.cs @@ -95,7 +95,7 @@ namespace Avalonia.Direct2D1.Media CompatibleRenderTargetOptions.None, calc.IntermediateSize.ToSharpDX()); - using (var context = new RenderTarget(result).CreateDrawingContext(null)) + using (var context = new RenderTarget(result).CreateDrawingContext()) { var dpi = new Vector(target.DotsPerInch.Width, target.DotsPerInch.Height); var rect = new Rect(bitmap.PixelSize.ToSizeWithDpi(dpi)); diff --git a/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs index 2dbc1d67d1..6b1ca911fb 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs @@ -30,9 +30,9 @@ namespace Avalonia.Direct2D1.Media.Imaging return new D2DRenderTargetBitmapImpl(bitmapRenderTarget); } - public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) + public IDrawingContextImpl CreateDrawingContext() { - return new DrawingContextImpl(visualBrushRenderer, this, _renderTarget, null, () => Version++); + return new DrawingContextImpl( this, _renderTarget, null, () => Version++); } public bool IsCorrupted => false; diff --git a/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicRenderTargetBitmapImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicRenderTargetBitmapImpl.cs index d6b1e618e5..fa40e75fa7 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicRenderTargetBitmapImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicRenderTargetBitmapImpl.cs @@ -34,14 +34,14 @@ namespace Avalonia.Direct2D1.Media base.Dispose(); } - public virtual IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) - => CreateDrawingContext(visualBrushRenderer, null); + public virtual IDrawingContextImpl CreateDrawingContext() + => CreateDrawingContext(null); public bool IsCorrupted => false; - public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer, Action finishedCallback) + public IDrawingContextImpl CreateDrawingContext(Action finishedCallback) { - return new DrawingContextImpl(visualBrushRenderer, null, _renderTarget, finishedCallback: () => + return new DrawingContextImpl(null, _renderTarget, finishedCallback: () => { Version++; finishedCallback?.Invoke(); diff --git a/src/Windows/Avalonia.Direct2D1/RenderTarget.cs b/src/Windows/Avalonia.Direct2D1/RenderTarget.cs index 8d5062336c..4392e35058 100644 --- a/src/Windows/Avalonia.Direct2D1/RenderTarget.cs +++ b/src/Windows/Avalonia.Direct2D1/RenderTarget.cs @@ -25,9 +25,9 @@ namespace Avalonia.Direct2D1 /// Creates a drawing context for a rendering session. /// /// An . - public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) + public IDrawingContextImpl CreateDrawingContext() { - return new DrawingContextImpl(visualBrushRenderer, this, _renderTarget); + return new DrawingContextImpl(this, _renderTarget); } public bool IsCorrupted => false; diff --git a/src/Windows/Avalonia.Direct2D1/SwapChainRenderTarget.cs b/src/Windows/Avalonia.Direct2D1/SwapChainRenderTarget.cs index 531c4119af..385120505c 100644 --- a/src/Windows/Avalonia.Direct2D1/SwapChainRenderTarget.cs +++ b/src/Windows/Avalonia.Direct2D1/SwapChainRenderTarget.cs @@ -19,7 +19,7 @@ namespace Avalonia.Direct2D1 /// Creates a drawing context for a rendering session. /// /// An . - public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) + public IDrawingContextImpl CreateDrawingContext() { var size = GetWindowSize(); var dpi = GetWindowDpi(); @@ -32,7 +32,7 @@ namespace Avalonia.Direct2D1 Resize(); } - return new DrawingContextImpl(visualBrushRenderer, this, _deviceContext, _swapChain); + return new DrawingContextImpl(this, _deviceContext, _swapChain); } public bool IsCorrupted => false; diff --git a/src/Windows/Avalonia.Win32/ClipboardFormats.cs b/src/Windows/Avalonia.Win32/ClipboardFormats.cs index 5fc4f21b2e..00fdeb2a1d 100644 --- a/src/Windows/Avalonia.Win32/ClipboardFormats.cs +++ b/src/Windows/Avalonia.Win32/ClipboardFormats.cs @@ -29,7 +29,10 @@ namespace Avalonia.Win32 private static readonly List s_formatList = new() { new ClipboardFormat(DataFormats.Text, (ushort)UnmanagedMethods.ClipboardFormat.CF_UNICODETEXT, (ushort)UnmanagedMethods.ClipboardFormat.CF_TEXT), + new ClipboardFormat(DataFormats.Files, (ushort)UnmanagedMethods.ClipboardFormat.CF_HDROP), +#pragma warning disable CS0618 // Type or member is obsolete new ClipboardFormat(DataFormats.FileNames, (ushort)UnmanagedMethods.ClipboardFormat.CF_HDROP), +#pragma warning restore CS0618 // Type or member is obsolete }; diff --git a/src/Windows/Avalonia.Win32/DataObject.cs b/src/Windows/Avalonia.Win32/DataObject.cs index 272300cbf3..a215a0a322 100644 --- a/src/Windows/Avalonia.Win32/DataObject.cs +++ b/src/Windows/Avalonia.Win32/DataObject.cs @@ -10,6 +10,7 @@ using System.Runtime.InteropServices.ComTypes; using System.Runtime.Serialization.Formatters.Binary; using Avalonia.Input; using Avalonia.MicroCom; +using Avalonia.Platform.Storage; using Avalonia.Win32.Interop; using FORMATETC = Avalonia.Win32.Interop.FORMATETC; @@ -124,16 +125,6 @@ namespace Avalonia.Win32 return _wrapped.GetDataFormats(); } - IEnumerable? IDataObject.GetFileNames() - { - return _wrapped.GetFileNames(); - } - - string? IDataObject.GetText() - { - return _wrapped.GetText(); - } - object? IDataObject.Get(string dataFormat) { return _wrapped.Get(dataFormat); @@ -260,8 +251,12 @@ namespace Avalonia.Win32 object data = _wrapped.Get(dataFormat)!; if (dataFormat == DataFormats.Text || data is string) return WriteStringToHGlobal(ref hGlobal, Convert.ToString(data) ?? string.Empty); +#pragma warning disable CS0618 // Type or member is obsolete if (dataFormat == DataFormats.FileNames && data is IEnumerable files) return WriteFileListToHGlobal(ref hGlobal, files); +#pragma warning restore CS0618 // Type or member is obsolete + if (dataFormat == DataFormats.Files && data is IEnumerable items) + return WriteFileListToHGlobal(ref hGlobal, items.Select(f => f.TryGetLocalPath()).Where(f => f is not null)!); if (data is Stream stream) { var length = (int)(stream.Length - stream.Position); diff --git a/src/Windows/Avalonia.Win32/OleDataObject.cs b/src/Windows/Avalonia.Win32/OleDataObject.cs index 247d0340c3..824303b7fa 100644 --- a/src/Windows/Avalonia.Win32/OleDataObject.cs +++ b/src/Windows/Avalonia.Win32/OleDataObject.cs @@ -8,6 +8,7 @@ using System.Runtime.InteropServices; using System.Runtime.InteropServices.ComTypes; using System.Runtime.Serialization.Formatters.Binary; using Avalonia.Input; +using Avalonia.Platform.Storage.FileIO; using Avalonia.Utilities; using Avalonia.Win32.Interop; using MicroCom.Runtime; @@ -34,16 +35,6 @@ namespace Avalonia.Win32 return GetDataFormatsCore().Distinct(); } - public string? GetText() - { - return (string?)GetDataFromOleHGLOBAL(DataFormats.Text, DVASPECT.DVASPECT_CONTENT); - } - - public IEnumerable? GetFileNames() - { - return (IEnumerable?)GetDataFromOleHGLOBAL(DataFormats.FileNames, DVASPECT.DVASPECT_CONTENT); - } - public object? Get(string dataFormat) { return GetDataFromOleHGLOBAL(dataFormat, DVASPECT.DVASPECT_CONTENT); @@ -67,8 +58,15 @@ namespace Avalonia.Win32 { if (format == DataFormats.Text) return ReadStringFromHGlobal(medium.unionmember); +#pragma warning disable CS0618 if (format == DataFormats.FileNames) +#pragma warning restore CS0618 return ReadFileNamesFromHGlobal(medium.unionmember); + if (format == DataFormats.Files) + return ReadFileNamesFromHGlobal(medium.unionmember) + .Select(f => StorageProviderHelpers.TryCreateBclStorageItem(f)!) + // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract + .Where(f => f is not null); byte[] data = ReadBytesFromHGlobal(medium.unionmember); diff --git a/tests/Avalonia.Base.UnitTests/RenderTests_Culling.cs b/tests/Avalonia.Base.UnitTests/RenderTests_Culling.cs index d75bf9fe8c..f91b4b613c 100644 --- a/tests/Avalonia.Base.UnitTests/RenderTests_Culling.cs +++ b/tests/Avalonia.Base.UnitTests/RenderTests_Culling.cs @@ -181,7 +181,7 @@ namespace Avalonia.Base.UnitTests private DrawingContext CreateDrawingContext() { - return new DrawingContext(Mock.Of()); + return new PlatformDrawingContext(Mock.Of()); } private class TestControl : Control diff --git a/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs b/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs index c1468a28e4..9d810fa110 100644 --- a/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs +++ b/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs @@ -69,7 +69,7 @@ namespace Avalonia.Base.UnitTests.Rendering.SceneGraph new Matrix(), Brushes.Black, null, - geometry, default); + geometry); geometryNode.HitTest(new Point()); } @@ -77,7 +77,7 @@ namespace Avalonia.Base.UnitTests.Rendering.SceneGraph private class TestRectangleDrawOperation : RectangleNode { public TestRectangleDrawOperation(Rect bounds, Matrix transform, Pen pen) - : base(transform, pen.Brush, pen, bounds, new BoxShadows()) + : base(transform, pen.Brush?.ToImmutable(), pen, bounds, new BoxShadows()) { } diff --git a/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/EllipseNodeTests.cs b/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/EllipseNodeTests.cs index 565b217180..a2e438e3e0 100644 --- a/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/EllipseNodeTests.cs +++ b/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/EllipseNodeTests.cs @@ -18,7 +18,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph [InlineData(0, 101, false)] public void FillOnly_HitTest(double x, double y, bool inside) { - var ellipseNode = new EllipseNode(Matrix.Identity, Brushes.Black, null, new Rect(0,0, 100, 100), null); + var ellipseNode = new EllipseNode(Matrix.Identity, Brushes.Black, null, new Rect(0,0, 100, 100)); var point = new Point(x, y); @@ -37,7 +37,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph [InlineData(0, 101, false)] public void StrokeOnly_HitTest(double x, double y, bool inside) { - var ellipseNode = new EllipseNode(Matrix.Identity, null, new ImmutablePen(Brushes.Black, 2), new Rect(0, 0, 100, 100), null); + var ellipseNode = new EllipseNode(Matrix.Identity, null, new ImmutablePen(Brushes.Black, 2), new Rect(0, 0, 100, 100)); var point = new Point(x, y); diff --git a/tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs b/tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs index b5a9b35134..60603937d9 100644 --- a/tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs +++ b/tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs @@ -104,7 +104,7 @@ public class StyledElementTests_Theming target.Theme = null; Assert.Equal("style", target.Tag); } - + [Fact] public void TemplatedParent_Theme_Is_Detached_From_Template_Controls_When_Theme_Property_Cleared() { @@ -539,12 +539,42 @@ public class StyledElementTests_Theming Assert.Same(target.Theme, theme3); } + [Fact] + public void TemplatedParent_Theme_Change_Applies_To_Children() + { + var theme = CreateDerivedTheme(); + var target = CreateTarget(); + + Assert.Null(target.Theme); + Assert.Null(target.Template); + + var root = CreateRoot(target, theme.BasedOn); + + Assert.NotNull(target.Theme); + Assert.NotNull(target.Template); + + root.Styles.Add(new Style(x => x.OfType().Class("foo")) + { + Setters = { new Setter(StyledElement.ThemeProperty, theme) } + }); + + root.LayoutManager.ExecuteLayoutPass(); + + var border = Assert.IsType(target.VisualChild); + Assert.Equal(Brushes.Red, border.Background); + + target.Classes.Add("foo"); + root.LayoutManager.ExecuteLayoutPass(); + + Assert.Equal(Brushes.Green, border.Background); + } + private static ThemedControl CreateTarget() { return new ThemedControl(); } - private static TestRoot CreateRoot(Control child) + private static TestRoot CreateRoot(Control child, ControlTheme? theme = null) { var result = new TestRoot() { @@ -552,7 +582,7 @@ public class StyledElementTests_Theming { new Style(x => x.OfType()) { - Setters = { new Setter(StyledElement.ThemeProperty, CreateTheme()) } + Setters = { new Setter(StyledElement.ThemeProperty, theme ?? CreateTheme()) } } } }; @@ -580,8 +610,8 @@ public class StyledElementTests_Theming { new Style(x => x.Nesting().Template().OfType()) { - Setters = - { + Setters = + { new Setter(Border.BackgroundProperty, Brushes.Red), new Setter(Control.TagProperty, tag), } diff --git a/tests/Avalonia.Benchmarks/Rendering/ShapeRendering.cs b/tests/Avalonia.Benchmarks/Rendering/ShapeRendering.cs index b0db806afa..2905b1e464 100644 --- a/tests/Avalonia.Benchmarks/Rendering/ShapeRendering.cs +++ b/tests/Avalonia.Benchmarks/Rendering/ShapeRendering.cs @@ -21,7 +21,7 @@ namespace Avalonia.Benchmarks.Rendering _lineFill = new Line { Fill = new SolidColorBrush() }; _lineFillAndStroke = new Line { Stroke = new SolidColorBrush(), Fill = new SolidColorBrush() }; - _drawingContext = new DrawingContext(new NullDrawingContextImpl(), true); + _drawingContext = new PlatformDrawingContext(new NullDrawingContextImpl(), true); AvaloniaLocator.CurrentMutable.Bind().ToConstant(new NullRenderingPlatform()); } diff --git a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs index 55c43f6f96..ba8e7242a1 100644 --- a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs @@ -10,6 +10,8 @@ using Avalonia.Controls.Templates; using Avalonia.Data; using Avalonia.Layout; using Avalonia.LogicalTree; +using Avalonia.Media; +using Avalonia.Styling; using Avalonia.UnitTests; using Avalonia.VisualTree; using Xunit; @@ -355,6 +357,58 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(new Vector(0, 0), scroll.Offset); } + [Fact] + public void NthChild_Selector_Works() + { + using var app = App(); + + var style = new Style(x => x.OfType().NthChild(5, 0)) + { + Setters = { new Setter(ListBoxItem.BackgroundProperty, Brushes.Red) }, + }; + + var (target, _, _) = CreateTarget(styles: new[] { style }); + var realized = target.GetRealizedContainers()!.Cast().ToList(); + + Assert.Equal(10, realized.Count); + + for (var i = 0; i < 10; ++i) + { + var container = realized[i]; + var index = target.IndexFromContainer(container); + var expectedBackground = (i == 4 || i == 9) ? Brushes.Red : null; + + Assert.Equal(i, index); + Assert.Equal(expectedBackground, container.Background); + } + } + + [Fact] + public void NthLastChild_Selector_Works() + { + using var app = App(); + + var style = new Style(x => x.OfType().NthLastChild(5, 0)) + { + Setters = { new Setter(ListBoxItem.BackgroundProperty, Brushes.Red) }, + }; + + var (target, _, _) = CreateTarget(styles: new[] { style }); + var realized = target.GetRealizedContainers()!.Cast().ToList(); + + Assert.Equal(10, realized.Count); + + for (var i = 0; i < 10; ++i) + { + var container = realized[i]; + var index = target.IndexFromContainer(container); + var expectedBackground = (i == 0 || i == 5) ? Brushes.Red : null; + + Assert.Equal(i, index); + Assert.Equal(expectedBackground, container.Background); + } + } + private static IReadOnlyList GetRealizedIndexes(VirtualizingStackPanel target, ItemsControl itemsControl) { return target.GetRealizedElements() @@ -399,7 +453,8 @@ namespace Avalonia.Controls.UnitTests private static (VirtualizingStackPanel, ScrollViewer, ItemsControl) CreateTarget( IEnumerable? items = null, - bool useItemTemplate = true) + bool useItemTemplate = true, + IEnumerable