From 6bd0d2818c5e0c759d3a2729566ad2c0fced2689 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 29 Jun 2022 13:40:10 +0200 Subject: [PATCH 001/197] Add deferred support to ResourceDictionary. --- .../Controls/ResourceDictionary.cs | 40 +++++++++++++++---- .../Controls/Templates/ITemplateResult.cs | 8 ++++ .../Controls}/Templates/TemplateResult.cs | 3 +- 3 files changed, 43 insertions(+), 8 deletions(-) create mode 100644 src/Avalonia.Base/Controls/Templates/ITemplateResult.cs rename src/{Avalonia.Controls => Avalonia.Base/Controls}/Templates/TemplateResult.cs (80%) diff --git a/src/Avalonia.Base/Controls/ResourceDictionary.cs b/src/Avalonia.Base/Controls/ResourceDictionary.cs index 77863e5101..e3a8bbbcbc 100644 --- a/src/Avalonia.Base/Controls/ResourceDictionary.cs +++ b/src/Avalonia.Base/Controls/ResourceDictionary.cs @@ -3,6 +3,7 @@ using System.Collections; using System.Collections.Generic; using System.Linq; using Avalonia.Collections; +using Avalonia.Controls.Templates; namespace Avalonia.Controls { @@ -29,7 +30,11 @@ namespace Avalonia.Controls public object? this[object key] { - get => _inner?[key]; + get + { + TryGetValue(key, out var value); + return value; + } set { Inner[key] = value; @@ -119,6 +124,12 @@ namespace Avalonia.Controls Owner?.NotifyHostedResourcesChanged(ResourcesChangedEventArgs.Empty); } + public void AddDeferred(object key, Func factory) + { + Inner.Add(key, new DeferredItem(factory)); + Owner?.NotifyHostedResourcesChanged(ResourcesChangedEventArgs.Empty); + } + public void Clear() { if (_inner?.Count > 0) @@ -143,10 +154,8 @@ namespace Avalonia.Controls public bool TryGetResource(object key, out object? value) { - if (_inner is not null && _inner.TryGetValue(key, out value)) - { + if (TryGetValue(key, out value)) return true; - } if (_mergedDictionaries != null) { @@ -165,13 +174,24 @@ namespace Avalonia.Controls public bool TryGetValue(object key, out object? value) { - if (_inner is not null) - return _inner.TryGetValue(key, out value); + if (_inner is not null && _inner.TryGetValue(key, out value)) + { + if (value is DeferredItem deffered) + { + _inner[key] = value = deffered.Factory(null) switch + { + ITemplateResult t => t.Result, + object v => v, + _ => null, + }; + } + return true; + } + value = null; return false; } - void ICollection>.Add(KeyValuePair item) { Add(item.Key, item.Value); @@ -258,5 +278,11 @@ namespace Avalonia.Controls } } } + + private class DeferredItem + { + public DeferredItem(Func factory) => Factory = factory; + public Func Factory { get; } + } } } diff --git a/src/Avalonia.Base/Controls/Templates/ITemplateResult.cs b/src/Avalonia.Base/Controls/Templates/ITemplateResult.cs new file mode 100644 index 0000000000..6bd4d735a7 --- /dev/null +++ b/src/Avalonia.Base/Controls/Templates/ITemplateResult.cs @@ -0,0 +1,8 @@ +namespace Avalonia.Controls.Templates +{ + public interface ITemplateResult + { + public object? Result { get; } + public INameScope NameScope { get; } + } +} diff --git a/src/Avalonia.Controls/Templates/TemplateResult.cs b/src/Avalonia.Base/Controls/Templates/TemplateResult.cs similarity index 80% rename from src/Avalonia.Controls/Templates/TemplateResult.cs rename to src/Avalonia.Base/Controls/Templates/TemplateResult.cs index 770aecc329..0e38c6c0ce 100644 --- a/src/Avalonia.Controls/Templates/TemplateResult.cs +++ b/src/Avalonia.Base/Controls/Templates/TemplateResult.cs @@ -1,9 +1,10 @@ namespace Avalonia.Controls.Templates { - public class TemplateResult + public class TemplateResult : ITemplateResult { public T Result { get; } public INameScope NameScope { get; } + object? ITemplateResult.Result => Result; public TemplateResult(T result, INameScope nameScope) { From 5c6eeed4fae39ec31824c9dedb94183aee143e5d Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 29 Jun 2022 14:01:41 +0200 Subject: [PATCH 002/197] Initial support for deferred resources in XAML compiler. --- .../Controls/ResourceDictionary.cs | 18 +++- .../AvaloniaXamlIlCompiler.cs | 4 + ...aloniaXamlIlDeferredResourceTransformer.cs | 80 +++++++++++++++ .../AvaloniaXamlIlWellKnownTypes.cs | 9 ++ .../Xaml/ResourceDictionaryTests.cs | 98 +++++++++++++++++++ 5 files changed, 205 insertions(+), 4 deletions(-) create mode 100644 src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDeferredResourceTransformer.cs diff --git a/src/Avalonia.Base/Controls/ResourceDictionary.cs b/src/Avalonia.Base/Controls/ResourceDictionary.cs index e3a8bbbcbc..d6197c50c6 100644 --- a/src/Avalonia.Base/Controls/ResourceDictionary.cs +++ b/src/Avalonia.Base/Controls/ResourceDictionary.cs @@ -192,6 +192,11 @@ namespace Avalonia.Controls return false; } + public IEnumerator> GetEnumerator() + { + return _inner?.GetEnumerator() ?? Enumerable.Empty>().GetEnumerator(); + } + void ICollection>.Add(KeyValuePair item) { Add(item.Key, item.Value); @@ -218,12 +223,17 @@ namespace Avalonia.Controls return false; } - public IEnumerator> GetEnumerator() + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + internal bool ContainsDeferredKey(object key) { - return _inner?.GetEnumerator() ?? Enumerable.Empty>().GetEnumerator(); - } + if (_inner is not null && _inner.TryGetValue(key, out var result)) + { + return result is DeferredItem; + } - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + return false; + } void IResourceProvider.AddOwner(IResourceHost owner) { diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs index 04a61e5f10..fac053df14 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs @@ -59,6 +59,10 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions InsertAfter( new XDataTypeTransformer()); + InsertBefore( + new AvaloniaXamlIlDeferredResourceTransformer() + ); + // After everything else InsertBefore( new AddNameScopeRegistration(), diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDeferredResourceTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDeferredResourceTransformer.cs new file mode 100644 index 0000000000..099878df08 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDeferredResourceTransformer.cs @@ -0,0 +1,80 @@ +using System.Collections.Generic; +using System.Linq; +using XamlX.Ast; +using XamlX.Emit; +using XamlX.IL; +using XamlX.Transform; +using XamlX.TypeSystem; + +namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers +{ + internal class AvaloniaXamlIlDeferredResourceTransformer : IXamlAstTransformer + { + public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) + { + if (!(node is XamlPropertyAssignmentNode pa) || pa.Values.Count != 2) + return node; + + var types = context.GetAvaloniaTypes(); + + if (pa.Property.DeclaringType == types.ResourceDictionary && pa.Property.Name == "Content") + { + pa.Values[1] = new XamlDeferredContentNode(pa.Values[1], types.XamlIlTypes.Object, context.Configuration); + pa.PossibleSetters = new List + { + new XamlDirectCallPropertySetter(types.ResourceDictionaryDeferredAdd), + }; + } + else if (pa.Property.Name == "Resources" && pa.Property.Getter.ReturnType.Equals(types.IResourceDictionary)) + { + pa.Values[1] = new XamlDeferredContentNode(pa.Values[1], types.XamlIlTypes.Object, context.Configuration); + pa.PossibleSetters = new List + { + new AdderSetter(pa.Property.Getter, types.ResourceDictionaryDeferredAdd), + }; + } + + return node; + } + + class AdderSetter : IXamlPropertySetter, IXamlEmitablePropertySetter + { + private readonly IXamlMethod _getter; + private readonly IXamlMethod _adder; + + public AdderSetter(IXamlMethod getter, IXamlMethod adder) + { + _getter = getter; + _adder = adder; + TargetType = getter.DeclaringType; + Parameters = adder.ParametersWithThis().Skip(1).ToList(); + } + + public IXamlType TargetType { get; } + + public PropertySetterBinderParameters BinderParameters { get; } = new PropertySetterBinderParameters + { + AllowMultiple = true + }; + + public IReadOnlyList Parameters { get; } + public void Emit(IXamlILEmitter emitter) + { + var locals = new Stack(); + // Save all "setter" parameters + for (var c = Parameters.Count - 1; c >= 0; c--) + { + var loc = emitter.LocalsPool.GetLocal(Parameters[c]); + locals.Push(loc); + emitter.Stloc(loc.Local); + } + + emitter.EmitCall(_getter); + while (locals.Count > 0) + using (var loc = locals.Pop()) + emitter.Ldloc(loc.Local); + emitter.EmitCall(_adder, true); + } + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs index 28787d9b84..330d836f49 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs @@ -96,6 +96,9 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers public IXamlType TextDecorations { get; } public IXamlType TextTrimming { get; } public IXamlType ISetter { get; } + public IXamlType IResourceDictionary { get; } + public IXamlType ResourceDictionary { get; } + public IXamlMethod ResourceDictionaryDeferredAdd { get; } public AvaloniaXamlIlWellKnownTypes(TransformerConfiguration cfg) { @@ -210,6 +213,12 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers TextDecorations = cfg.TypeSystem.GetType("Avalonia.Media.TextDecorations"); TextTrimming = cfg.TypeSystem.GetType("Avalonia.Media.TextTrimming"); ISetter = cfg.TypeSystem.GetType("Avalonia.Styling.ISetter"); + IResourceDictionary = cfg.TypeSystem.GetType("Avalonia.Controls.IResourceDictionary"); + ResourceDictionary = cfg.TypeSystem.GetType("Avalonia.Controls.ResourceDictionary"); + ResourceDictionaryDeferredAdd = ResourceDictionary.FindMethod("AddDeferred", XamlIlTypes.Void, true, XamlIlTypes.Object, + cfg.TypeSystem.GetType("System.Func`2").MakeGenericType( + cfg.TypeSystem.GetType("System.IServiceProvider"), + XamlIlTypes.Object)); } } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ResourceDictionaryTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ResourceDictionaryTests.cs index 578fa888a3..939c5319e4 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ResourceDictionaryTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ResourceDictionaryTests.cs @@ -66,6 +66,104 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml } } + [Fact] + public void Item_Is_Added_To_ResourceDictionary_As_Deferred() + { + using (StyledWindow()) + { + var xaml = @" + + Red +"; + var resources = (ResourceDictionary)AvaloniaRuntimeXamlLoader.Load(xaml); + + Assert.True(resources.ContainsDeferredKey("Red")); + } + } + + [Fact] + public void Item_Is_Added_To_Window_Resources_As_Deferred() + { + using (StyledWindow()) + { + var xaml = @" + + + Red + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var resources = (ResourceDictionary)window.Resources; + + Assert.True(resources.ContainsDeferredKey("Red")); + } + } + + [Fact] + public void Item_Is_Added_To_Window_MergedDictionaries_As_Deferred() + { + using (StyledWindow()) + { + var xaml = @" + + + + + + Red + + + + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var resources = (ResourceDictionary)window.Resources.MergedDictionaries[0]; + + Assert.True(resources.ContainsDeferredKey("Red")); + } + } + + [Fact] + public void Item_Is_Added_To_Style_Resources_As_Deferred() + { + using (StyledWindow()) + { + var xaml = @" +"; + var style = (Style)AvaloniaRuntimeXamlLoader.Load(xaml); + var resources = (ResourceDictionary)style.Resources; + + Assert.True(resources.ContainsDeferredKey("Red")); + } + } + + [Fact] + public void Item_Is_Added_To_Styles_Resources_As_Deferred() + { + using (StyledWindow()) + { + var xaml = @" + + + Red + +"; + var style = (Styles)AvaloniaRuntimeXamlLoader.Load(xaml); + var resources = (ResourceDictionary)style.Resources; + + Assert.True(resources.ContainsDeferredKey("Red")); + } + } + private IDisposable StyledWindow(params (string, string)[] assets) { var services = TestServices.StyledWindow.With( From ccdc11d0accad930a11996546d4cef9af41d191b Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 30 Jun 2022 15:34:48 +0200 Subject: [PATCH 003/197] Fix missing subscribe if owner is already set. --- src/Avalonia.Base/Controls/ResourceNodeExtensions.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Avalonia.Base/Controls/ResourceNodeExtensions.cs b/src/Avalonia.Base/Controls/ResourceNodeExtensions.cs index 1758c45650..6121646107 100644 --- a/src/Avalonia.Base/Controls/ResourceNodeExtensions.cs +++ b/src/Avalonia.Base/Controls/ResourceNodeExtensions.cs @@ -132,6 +132,11 @@ namespace Avalonia.Controls { _target.OwnerChanged += OwnerChanged; _owner = _target.Owner; + + if (_owner is object) + { + _owner.ResourcesChanged += ResourcesChanged; + } } protected override void Deinitialize() From 9ab31f60b40d676fbe1a876dbf70a5e498904ebf Mon Sep 17 00:00:00 2001 From: Luis von der Eltz Date: Tue, 5 Jul 2022 17:17:51 +0200 Subject: [PATCH 004/197] Fix button flyout toggle Expose OverlayDismissEventPassThrough on FlyoutBase --- src/Avalonia.Controls/Button.cs | 56 ++++++++++++++------- src/Avalonia.Controls/Flyouts/FlyoutBase.cs | 48 ++++++++++++++---- src/Avalonia.Controls/Primitives/Popup.cs | 2 +- 3 files changed, 77 insertions(+), 29 deletions(-) diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs index 6dba33516b..8e5d4e1e06 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/src/Avalonia.Controls/Button.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using System.Linq; using System.Windows.Input; using Avalonia.Automation.Peers; @@ -281,24 +282,29 @@ namespace Avalonia.Controls /// protected override void OnKeyDown(KeyEventArgs e) { - if (e.Key == Key.Enter) + switch (e.Key) { - OnClick(); - e.Handled = true; - } - else if (e.Key == Key.Space) - { - if (ClickMode == ClickMode.Press) - { + case Key.Enter: OnClick(); + e.Handled = true; + break; + + case Key.Space: + { + if (ClickMode == ClickMode.Press) + { + OnClick(); + } + + IsPressed = true; + e.Handled = true; + break; } - IsPressed = true; - e.Handled = true; - } - else if (e.Key == Key.Escape && Flyout != null) - { - // If Flyout doesn't have focusable content, close the flyout here - Flyout.Hide(); + + case Key.Escape when Flyout != null: + // If Flyout doesn't have focusable content, close the flyout here + CloseFlyout(); + break; } base.OnKeyDown(e); @@ -327,7 +333,14 @@ namespace Avalonia.Controls { if (IsEffectivelyEnabled) { - OpenFlyout(); + if (_isFlyoutOpen) + { + CloseFlyout(); + } + else + { + OpenFlyout(); + } var e = new RoutedEventArgs(ClickEvent); RaiseEvent(e); @@ -348,6 +361,14 @@ namespace Avalonia.Controls Flyout?.ShowAt(this); } + /// + /// Closes the button's flyout. + /// + protected virtual void CloseFlyout() + { + Flyout?.Hide(); + } + /// /// Invoked when the button's flyout is opened. /// @@ -494,8 +515,7 @@ namespace Avalonia.Controls // If flyout is changed while one is already open, make sure we // close the old one first - if (oldFlyout != null && - oldFlyout.IsOpen) + if (oldFlyout != null && oldFlyout.IsOpen) { oldFlyout.Hide(); } diff --git a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs index 1504d2b25f..a0f3407b7a 100644 --- a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs +++ b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs @@ -12,11 +12,6 @@ namespace Avalonia.Controls.Primitives { public abstract class FlyoutBase : AvaloniaObject, IPopupHostProvider { - static FlyoutBase() - { - Control.ContextFlyoutProperty.Changed.Subscribe(OnContextFlyoutPropertyChanged); - } - /// /// Defines the property /// @@ -49,6 +44,12 @@ namespace Avalonia.Controls.Primitives public static readonly AttachedProperty AttachedFlyoutProperty = AvaloniaProperty.RegisterAttached("AttachedFlyout", null); + /// + /// Defines the OverlayDismissEventPassThrough property + /// + public static readonly StyledProperty OverlayDismissEventPassThroughProperty = + Popup.OverlayDismissEventPassThroughProperty.AddOwner(); + private readonly Lazy _popupLazy; private bool _isOpen; private Control? _target; @@ -58,6 +59,12 @@ namespace Avalonia.Controls.Primitives private IDisposable? _transientDisposable; private Action? _popupHostChangedHandler; + static FlyoutBase() + { + OverlayDismissEventPassThroughProperty.OverrideDefaultValue(true); + Control.ContextFlyoutProperty.Changed.Subscribe(OnContextFlyoutPropertyChanged); + } + public FlyoutBase() { _popupLazy = new Lazy(() => CreatePopup()); @@ -101,6 +108,21 @@ namespace Avalonia.Controls.Primitives private set => SetAndRaise(TargetProperty, ref _target, value); } + /// + /// Gets or sets a value indicating whether the event that closes the flyout is passed + /// through to the parent window. + /// + /// + /// Clicks outside the the flyout cause the flyout to close. When is set to + /// false, these clicks will be handled by the flyout and not be registered by the parent + /// window. When set to true, the events will be passed through to the parent window. + /// + public bool OverlayDismissEventPassThrough + { + get => GetValue(OverlayDismissEventPassThroughProperty); + set => SetValue(OverlayDismissEventPassThroughProperty, value); + } + IPopupHost? IPopupHostProvider.PopupHost => Popup?.Host; event Action? IPopupHostProvider.PopupHostChanged @@ -175,6 +197,8 @@ namespace Avalonia.Controls.Primitives IsOpen = false; Popup.IsOpen = false; + Popup.OverlayInputPassThroughElement = null; + ((ISetLogicalParent)Popup).SetParent(null); // Ensure this isn't active @@ -231,6 +255,9 @@ namespace Avalonia.Controls.Primitives Popup.Child = CreatePresenter(); } + Popup.OverlayInputPassThroughElement = placementTarget; + Popup.OverlayDismissEventPassThrough = OverlayDismissEventPassThrough; + if (CancelOpening()) { return false; @@ -356,10 +383,11 @@ namespace Avalonia.Controls.Primitives private Popup CreatePopup() { - var popup = new Popup(); - popup.WindowManagerAddShadowHint = false; - popup.IsLightDismissEnabled = true; - popup.OverlayDismissEventPassThrough = true; + var popup = new Popup + { + WindowManagerAddShadowHint = false, + IsLightDismissEnabled = true + }; popup.Opened += OnPopupOpened; popup.Closed += OnPopupClosed; @@ -372,7 +400,7 @@ namespace Avalonia.Controls.Primitives { IsOpen = true; - _popupHostChangedHandler?.Invoke(Popup!.Host); + _popupHostChangedHandler?.Invoke(Popup.Host); } private void OnPopupClosing(object? sender, CancelEventArgs e) diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index 1501d97470..3573ad9aaa 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -501,7 +501,7 @@ namespace Avalonia.Controls.Primitives if (dismissLayer != null) { dismissLayer.IsVisible = true; - dismissLayer.InputPassThroughElement = _overlayInputPassThroughElement; + dismissLayer.InputPassThroughElement = OverlayInputPassThroughElement; Disposable.Create(() => { From 97a5a9e1f64110d01b8d8bf3937a3518f6fd567b Mon Sep 17 00:00:00 2001 From: Max Katz Date: Wed, 6 Jul 2022 14:24:55 -0400 Subject: [PATCH 005/197] Add folder.GetItemsAsync API --- .../ControlCatalog/Pages/DialogsPage.xaml.cs | 16 ++++++- .../Platform/Storage/AndroidStorageItem.cs | 25 +++++++++++ .../Storage/FileIO/BclStorageFolder.cs | 12 +++++ .../Platform/Storage/IStorageFolder.cs | 11 ++++- .../Interop/Storage/StorageProviderInterop.cs | 25 +++++++++++ .../Interop/Typescript/StorageProvider.ts | 45 +++++++++++++------ .../Avalonia.iOS/Storage/IOSStorageItem.cs | 17 +++++++ 7 files changed, 134 insertions(+), 17 deletions(-) diff --git a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs index f7b6db1255..e13c2052eb 100644 --- a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs +++ b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs @@ -243,8 +243,8 @@ namespace ControlCatalog.Pages async Task SetPickerResult(IReadOnlyCollection? items) { items ??= Array.Empty(); - var mappedResults = items.Select(FullPathOrName).ToList(); bookmarkContainer.Text = items.FirstOrDefault(f => f.CanBookmark) is { } f ? await f.SaveBookmark() : "Can't bookmark"; + var mappedResults = new List(); if (items.FirstOrDefault() is IStorageItem item) { @@ -293,7 +293,19 @@ Content: lastSelectedDirectory = await item.GetParentAsync(); if (lastSelectedDirectory is not null) { - mappedResults.Insert(0, "Parent: " + FullPathOrName(lastSelectedDirectory)); + mappedResults.Add(FullPathOrName(lastSelectedDirectory)); + } + + foreach (var selectedItem in items) + { + mappedResults.Add("+> " + FullPathOrName(selectedItem)); + if (selectedItem is IStorageFolder folder) + { + foreach (var innerItems in await folder.GetItemsAsync()) + { + mappedResults.Add("++> " + FullPathOrName(innerItems)); + } + } } } diff --git a/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs b/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs index 50581d47b1..1e81642e15 100644 --- a/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs +++ b/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs @@ -1,6 +1,7 @@ #nullable enable using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; @@ -106,6 +107,30 @@ internal sealed class AndroidStorageFolder : AndroidStorageItem, IStorageBookmar { return Task.FromResult(new StorageItemProperties()); } + + public async Task> GetItemsAsync() + { + using var javaFile = new JavaFile(Uri.Path!); + + // Java file represents files AND directories. Don't be confused. + var files = await javaFile.ListFilesAsync().ConfigureAwait(false); + if (files is null) + { + return Array.Empty(); + } + + return files + .Select(f => (file: f, uri: AndroidUri.FromFile(f))) + .Where(t => t.uri is not null) + .Select(t => t.file switch + { + { IsFile: true } => (IStorageItem)new AndroidStorageFile(Context, t.uri!), + { IsDirectory: true } => new AndroidStorageFolder(Context, t.uri!), + _ => null + }) + .Where(i => i is not null) + .ToArray()!; + } } internal sealed class AndroidStorageFile : AndroidStorageItem, IStorageBookmarkFile diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs index 7267017eaf..0a22f4bd03 100644 --- a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs +++ b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs @@ -1,6 +1,8 @@ using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; +using System.Linq; using System.Security; using System.Threading.Tasks; using Avalonia.Metadata; @@ -43,6 +45,16 @@ public class BclStorageFolder : IStorageBookmarkFolder return Task.FromResult(null); } + public Task> GetItemsAsync() + { + var items = _directoryInfo.GetDirectories() + .Select(d => (IStorageItem)new BclStorageFolder(d)) + .Concat(_directoryInfo.GetFiles().Select(f => new BclStorageFile(f))) + .ToArray(); + + return Task.FromResult>(items); + } + public virtual Task SaveBookmark() { return Task.FromResult(_directoryInfo.FullName); diff --git a/src/Avalonia.Base/Platform/Storage/IStorageFolder.cs b/src/Avalonia.Base/Platform/Storage/IStorageFolder.cs index 25b9f01a92..0ffb9f41c6 100644 --- a/src/Avalonia.Base/Platform/Storage/IStorageFolder.cs +++ b/src/Avalonia.Base/Platform/Storage/IStorageFolder.cs @@ -1,4 +1,6 @@ -using Avalonia.Metadata; +using System.Collections.Generic; +using System.Threading.Tasks; +using Avalonia.Metadata; namespace Avalonia.Platform.Storage; @@ -8,4 +10,11 @@ namespace Avalonia.Platform.Storage; [NotClientImplementable] public interface IStorageFolder : IStorageItem { + /// + /// Gets the files and subfolders in the current folder. + /// + /// + /// When this method completes successfully, it returns a list of the files and folders in the current folder. Each item in the list is represented by an implementation object. + /// + Task> GetItemsAsync(); } diff --git a/src/Web/Avalonia.Web.Blazor/Interop/Storage/StorageProviderInterop.cs b/src/Web/Avalonia.Web.Blazor/Interop/Storage/StorageProviderInterop.cs index 14dc53d7b5..129463774c 100644 --- a/src/Web/Avalonia.Web.Blazor/Interop/Storage/StorageProviderInterop.cs +++ b/src/Web/Avalonia.Web.Blazor/Interop/Storage/StorageProviderInterop.cs @@ -196,5 +196,30 @@ namespace Avalonia.Web.Blazor.Interop.Storage public JSStorageFolder(IJSInProcessObjectReference fileHandle) : base(fileHandle) { } + + public async Task> GetItemsAsync() + { + var items = await FileHandle.InvokeAsync("getItems"); + if (items is null) + { + return Array.Empty(); + } + + var count = items.Invoke("count"); + + return Enumerable.Range(0, count) + .Select(index => + { + var reference = items.Invoke("at", index); + return reference.Invoke("getKind") switch + { + "directory" => (IStorageItem)new JSStorageFolder(reference), + "file" => new JSStorageFile(reference), + _ => null + }; + }) + .Where(i => i is not null) + .ToArray()!; + } } } diff --git a/src/Web/Avalonia.Web.Blazor/Interop/Typescript/StorageProvider.ts b/src/Web/Avalonia.Web.Blazor/Interop/Typescript/StorageProvider.ts index c32eef3226..aee74b9067 100644 --- a/src/Web/Avalonia.Web.Blazor/Interop/Typescript/StorageProvider.ts +++ b/src/Web/Avalonia.Web.Blazor/Interop/Typescript/StorageProvider.ts @@ -14,6 +14,8 @@ declare global { queryPermission(options?: { mode: PermissionsMode }): Promise<"granted" | "denied" | "prompt">; requestPermission(options?: { mode: PermissionsMode }): Promise<"granted" | "denied" | "prompt">; + + entries(): AsyncIterableIterator<[string, FileSystemFileHandle]>; } type WellKnownDirectory = "desktop" | "documents" | "downloads" | "music" | "pictures" | "videos"; type StartInDirectory = WellKnownDirectory | FileSystemFileHandle; @@ -53,7 +55,7 @@ class IndexedDbWrapper { } public connect(): Promise { - var conn = window.indexedDB.open(this.databaseName, 1); + const conn = window.indexedDB.open(this.databaseName, 1); conn.onupgradeneeded = event => { const db = (>event.target).result; @@ -85,7 +87,7 @@ class InnerDbConnection { const os = this.openStore(store, "readwrite"); return new Promise((resolve, reject) => { - var response = os.put(obj, key); + const response = os.put(obj, key); response.onsuccess = () => { resolve(response.result); }; @@ -99,7 +101,7 @@ class InnerDbConnection { const os = this.openStore(store, "readonly"); return new Promise((resolve, reject) => { - var response = os.get(key); + const response = os.get(key); response.onsuccess = () => { resolve(response.result); }; @@ -113,7 +115,7 @@ class InnerDbConnection { const os = this.openStore(store, "readwrite"); return new Promise((resolve, reject) => { - var response = os.delete(key); + const response = os.delete(key); response.onsuccess = () => { resolve(); }; @@ -134,17 +136,20 @@ const avaloniaDb = new IndexedDbWrapper("AvaloniaDb", [ ]) class StorageItem { - constructor(private handle: FileSystemFileHandle, private bookmarkId?: string) { } + constructor(public handle: FileSystemFileHandle, private bookmarkId?: string) { } public getName(): string { return this.handle.name } + public getKind(): string { + return this.handle.kind; + } + public async openRead(): Promise { await this.verityPermissions('read'); - var file = await this.handle.getFile(); - return file; + return await this.handle.getFile(); } public async openWrite(): Promise { @@ -154,7 +159,7 @@ class StorageItem { } public async getProperties(): Promise<{ Size: number, LastModified: number, Type: string }> { - var file = this.handle.getFile && await this.handle.getFile(); + const file = this.handle.getFile && await this.handle.getFile(); return file && { Size: file.size, @@ -163,6 +168,18 @@ class StorageItem { } } + public async getItems(): Promise { + if (this.handle.kind !== "directory"){ + return new StorageItems([]); + } + + const items: StorageItem[] = []; + for await (const [key, value] of this.handle.entries()) { + items.push(new StorageItem(value)); + } + return new StorageItems(items); + } + private async verityPermissions(mode: PermissionsMode): Promise { if (await this.handle.queryPermission({ mode }) === 'granted') { return; @@ -235,12 +252,12 @@ export class StorageProvider { } public static async selectFolderDialog( - startIn: StartInDirectory | null) + startIn: StorageItem | null) : Promise { // 'Picker' API doesn't accept "null" as a parameter, so it should be set to undefined. const options: DirectoryPickerOptions = { - startIn: (startIn || undefined) + startIn: (startIn?.handle || undefined) }; const handle = await window.showDirectoryPicker(options); @@ -248,12 +265,12 @@ export class StorageProvider { } public static async openFileDialog( - startIn: StartInDirectory | null, multiple: boolean, + startIn: StorageItem | null, multiple: boolean, types: FilePickerAcceptType[] | null, excludeAcceptAllOption: boolean) : Promise { const options: OpenFilePickerOptions = { - startIn: (startIn || undefined), + startIn: (startIn?.handle || undefined), multiple, excludeAcceptAllOption, types: (types || undefined) @@ -264,12 +281,12 @@ export class StorageProvider { } public static async saveFileDialog( - startIn: StartInDirectory | null, suggestedName: string | null, + startIn: StorageItem | null, suggestedName: string | null, types: FilePickerAcceptType[] | null, excludeAcceptAllOption: boolean) : Promise { const options: SaveFilePickerOptions = { - startIn: (startIn || undefined), + startIn: (startIn?.handle || undefined), suggestedName: (suggestedName || undefined), excludeAcceptAllOption, types: (types || undefined) diff --git a/src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs b/src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs index 6fb296d0e0..cfb2a497be 100644 --- a/src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs +++ b/src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs @@ -1,6 +1,8 @@ using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; +using System.Linq; using System.Threading.Tasks; using Avalonia.Logging; using Avalonia.Platform.Storage; @@ -118,4 +120,19 @@ internal sealed class IOSStorageFolder : IOSStorageItem, IStorageBookmarkFolder public IOSStorageFolder(NSUrl url) : base(url) { } + + public Task> GetItemsAsync() + { + var content = NSFileManager.DefaultManager.GetDirectoryContent(Url, null, NSDirectoryEnumerationOptions.None, out var error); + if (error is not null) + { + return Task.FromException>(new NSErrorException(error)); + } + + var items = content + .Select(u => u.HasDirectoryPath ? (IStorageItem)new IOSStorageFolder(u) : new IOSStorageFile(u)) + .ToArray(); + + return Task.FromResult>(items); + } } From 5a8c9f9c0904c83095291d9ae0c02abd8a9d259f Mon Sep 17 00:00:00 2001 From: Max Katz Date: Wed, 6 Jul 2022 14:31:00 -0400 Subject: [PATCH 006/197] Use Async in naming --- samples/ControlCatalog/Pages/DialogsPage.xaml.cs | 10 +++++----- .../Platform/Storage/AndroidStorageItem.cs | 8 ++++---- .../Platform/Storage/FileIO/BclStorageFile.cs | 8 ++++---- .../Platform/Storage/FileIO/BclStorageFolder.cs | 4 ++-- .../Platform/Storage/IStorageBookmarkItem.cs | 2 +- src/Avalonia.Base/Platform/Storage/IStorageFile.cs | 4 ++-- src/Avalonia.Base/Platform/Storage/IStorageItem.cs | 2 +- .../Interop/Storage/StorageProviderInterop.cs | 8 ++++---- src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs | 8 ++++---- 9 files changed, 27 insertions(+), 27 deletions(-) diff --git a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs index e13c2052eb..036dccde0e 100644 --- a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs +++ b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs @@ -195,10 +195,10 @@ namespace ControlCatalog.Pages { // Sync disposal of StreamWriter is not supported on WASM #if NET6_0_OR_GREATER - await using var stream = await file.OpenWrite(); + await using var stream = await file.OpenWriteAsync(); await using var reader = new System.IO.StreamWriter(stream); #else - using var stream = await file.OpenWrite(); + using var stream = await file.OpenWriteAsync(); using var reader = new System.IO.StreamWriter(stream); #endif await reader.WriteLineAsync(openedFileContent.Text); @@ -243,7 +243,7 @@ namespace ControlCatalog.Pages async Task SetPickerResult(IReadOnlyCollection? items) { items ??= Array.Empty(); - bookmarkContainer.Text = items.FirstOrDefault(f => f.CanBookmark) is { } f ? await f.SaveBookmark() : "Can't bookmark"; + bookmarkContainer.Text = items.FirstOrDefault(f => f.CanBookmark) is { } f ? await f.SaveBookmarkAsync() : "Can't bookmark"; var mappedResults = new List(); if (items.FirstOrDefault() is IStorageItem item) @@ -267,9 +267,9 @@ Content: if (file.CanOpenRead) { #if NET6_0_OR_GREATER - await using var stream = await file.OpenRead(); + await using var stream = await file.OpenReadAsync(); #else - using var stream = await file.OpenRead(); + using var stream = await file.OpenReadAsync(); #endif using var reader = new System.IO.StreamReader(stream); diff --git a/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs b/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs index 1e81642e15..a9b2e16d43 100644 --- a/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs +++ b/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs @@ -36,13 +36,13 @@ internal abstract class AndroidStorageItem : IStorageBookmarkItem public bool CanBookmark => true; - public Task SaveBookmark() + public Task SaveBookmarkAsync() { Context.ContentResolver?.TakePersistableUriPermission(Uri, ActivityFlags.GrantWriteUriPermission | ActivityFlags.GrantReadUriPermission); return Task.FromResult(Uri.ToString()); } - public Task ReleaseBookmark() + public Task ReleaseBookmarkAsync() { Context.ContentResolver?.ReleasePersistableUriPermission(Uri, ActivityFlags.GrantWriteUriPermission | ActivityFlags.GrantReadUriPermission); return Task.CompletedTask; @@ -143,10 +143,10 @@ internal sealed class AndroidStorageFile : AndroidStorageItem, IStorageBookmarkF public bool CanOpenWrite => true; - public Task OpenRead() => Task.FromResult(OpenContentStream(Context, Uri, false) + public Task OpenReadAsync() => Task.FromResult(OpenContentStream(Context, Uri, false) ?? throw new InvalidOperationException("Failed to open content stream")); - public Task OpenWrite() => Task.FromResult(OpenContentStream(Context, Uri, true) + public Task OpenWriteAsync() => Task.FromResult(OpenContentStream(Context, Uri, true) ?? throw new InvalidOperationException("Failed to open content stream")); private Stream? OpenContentStream(Context context, AndroidUri uri, bool isOutput) diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs index 5af02219ce..cf21e9b8b5 100644 --- a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs +++ b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs @@ -47,22 +47,22 @@ public class BclStorageFile : IStorageBookmarkFile return Task.FromResult(null); } - public Task OpenRead() + public Task OpenReadAsync() { return Task.FromResult(_fileInfo.OpenRead()); } - public Task OpenWrite() + public Task OpenWriteAsync() { return Task.FromResult(_fileInfo.OpenWrite()); } - public virtual Task SaveBookmark() + public virtual Task SaveBookmarkAsync() { return Task.FromResult(_fileInfo.FullName); } - public Task ReleaseBookmark() + public Task ReleaseBookmarkAsync() { // No-op return Task.CompletedTask; diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs index 0a22f4bd03..cd6c8be1ae 100644 --- a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs +++ b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs @@ -55,12 +55,12 @@ public class BclStorageFolder : IStorageBookmarkFolder return Task.FromResult>(items); } - public virtual Task SaveBookmark() + public virtual Task SaveBookmarkAsync() { return Task.FromResult(_directoryInfo.FullName); } - public Task ReleaseBookmark() + public Task ReleaseBookmarkAsync() { // No-op return Task.CompletedTask; diff --git a/src/Avalonia.Base/Platform/Storage/IStorageBookmarkItem.cs b/src/Avalonia.Base/Platform/Storage/IStorageBookmarkItem.cs index d21c950862..40f2720ee8 100644 --- a/src/Avalonia.Base/Platform/Storage/IStorageBookmarkItem.cs +++ b/src/Avalonia.Base/Platform/Storage/IStorageBookmarkItem.cs @@ -6,7 +6,7 @@ namespace Avalonia.Platform.Storage; [NotClientImplementable] public interface IStorageBookmarkItem : IStorageItem { - Task ReleaseBookmark(); + Task ReleaseBookmarkAsync(); } [NotClientImplementable] diff --git a/src/Avalonia.Base/Platform/Storage/IStorageFile.cs b/src/Avalonia.Base/Platform/Storage/IStorageFile.cs index 965caf8216..46aa6efa72 100644 --- a/src/Avalonia.Base/Platform/Storage/IStorageFile.cs +++ b/src/Avalonia.Base/Platform/Storage/IStorageFile.cs @@ -18,7 +18,7 @@ public interface IStorageFile : IStorageItem /// /// Opens a stream for read access. /// - Task OpenRead(); + Task OpenReadAsync(); /// /// Returns true, if file is writeable. @@ -28,5 +28,5 @@ public interface IStorageFile : IStorageItem /// /// Opens stream for writing to the file. /// - Task OpenWrite(); + Task OpenWriteAsync(); } diff --git a/src/Avalonia.Base/Platform/Storage/IStorageItem.cs b/src/Avalonia.Base/Platform/Storage/IStorageItem.cs index 8513ebc7d9..f5469d31c9 100644 --- a/src/Avalonia.Base/Platform/Storage/IStorageItem.cs +++ b/src/Avalonia.Base/Platform/Storage/IStorageItem.cs @@ -44,7 +44,7 @@ public interface IStorageItem : IDisposable /// /// Returns identifier of a bookmark. Can be null if OS denied request. /// - Task SaveBookmark(); + Task SaveBookmarkAsync(); /// /// Gets the parent folder of the current storage item. diff --git a/src/Web/Avalonia.Web.Blazor/Interop/Storage/StorageProviderInterop.cs b/src/Web/Avalonia.Web.Blazor/Interop/Storage/StorageProviderInterop.cs index 129463774c..2bc46e97b5 100644 --- a/src/Web/Avalonia.Web.Blazor/Interop/Storage/StorageProviderInterop.cs +++ b/src/Web/Avalonia.Web.Blazor/Interop/Storage/StorageProviderInterop.cs @@ -145,7 +145,7 @@ namespace Avalonia.Web.Blazor.Interop.Storage public bool CanBookmark => true; - public Task SaveBookmark() + public Task SaveBookmarkAsync() { return FileHandle.InvokeAsync("saveBookmark").AsTask(); } @@ -155,7 +155,7 @@ namespace Avalonia.Web.Blazor.Interop.Storage return Task.FromResult(null); } - public Task ReleaseBookmark() + public Task ReleaseBookmarkAsync() { return FileHandle.InvokeAsync("deleteBookmark").AsTask(); } @@ -174,7 +174,7 @@ namespace Avalonia.Web.Blazor.Interop.Storage } public bool CanOpenRead => true; - public async Task OpenRead() + public async Task OpenReadAsync() { var stream = await FileHandle.InvokeAsync("openRead"); // Remove maxAllowedSize limit, as developer can decide if they read only small part or everything. @@ -182,7 +182,7 @@ namespace Avalonia.Web.Blazor.Interop.Storage } public bool CanOpenWrite => true; - public async Task OpenWrite() + public async Task OpenWriteAsync() { var properties = await FileHandle.InvokeAsync("getProperties"); var streamWriter = await FileHandle.InvokeAsync("openWrite"); diff --git a/src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs b/src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs index cfb2a497be..a801e83562 100644 --- a/src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs +++ b/src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs @@ -51,13 +51,13 @@ internal abstract class IOSStorageItem : IStorageBookmarkItem return Task.FromResult(new IOSStorageFolder(Url.RemoveLastPathComponent())); } - public Task ReleaseBookmark() + public Task ReleaseBookmarkAsync() { // no-op return Task.CompletedTask; } - public Task SaveBookmark() + public Task SaveBookmarkAsync() { try { @@ -104,12 +104,12 @@ internal sealed class IOSStorageFile : IOSStorageItem, IStorageBookmarkFile public bool CanOpenWrite => true; - public Task OpenRead() + public Task OpenReadAsync() { return Task.FromResult(new IOSSecurityScopedStream(Url, FileAccess.Read)); } - public Task OpenWrite() + public Task OpenWriteAsync() { return Task.FromResult(new IOSSecurityScopedStream(Url, FileAccess.Write)); } From 94ecd84c8f24d210f537c495e044ded266f1147b Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Mon, 18 Jul 2022 11:25:13 +0200 Subject: [PATCH 007/197] feat(LibInputBackend): minimal linux boot with no input --- .../Input/LibInput/LibInputBackend.cs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs b/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs index 15d42789d4..6e26794b24 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs @@ -1,16 +1,15 @@ using System; using System.Collections.Generic; -using System.ComponentModel; using System.IO; using System.Threading; using Avalonia.Input; using Avalonia.Input.Raw; -using Avalonia.Threading; using static Avalonia.LinuxFramebuffer.Input.LibInput.LibInputNativeUnsafeMethods; namespace Avalonia.LinuxFramebuffer.Input.LibInput { public class LibInputBackend : IInputBackend { + private const string LibInput = nameof(Logging.LogArea.X11Platform) + "/" + nameof(LibInput); private IScreenInfoProvider _screen; private IInputRoot _inputRoot; private readonly Queue _inputThreadActions = new Queue(); @@ -29,15 +28,21 @@ namespace Avalonia.LinuxFramebuffer.Input.LibInput new Thread(()=>InputThread(ctx)).Start(); } - - private unsafe void InputThread(IntPtr ctx) { var fd = libinput_get_fd(ctx); var timeval = stackalloc IntPtr[2]; - + if (!Directory.Exists("/dev/input")) + { + if (Logging.Logger.IsEnabled(Logging.LogEventLevel.Warning,LibInput)) + { + Logging.Logger.TryGet(Logging.LogEventLevel.Warning, LibInput) + ?.Log(this, "Not connect any input device."); + } + return; + } foreach (var f in Directory.GetFiles("/dev/input", "event*")) libinput_path_add_device(ctx, f); while (true) From e5241a59e707a7cba8e7c106c36f4a6ead9d1469 Mon Sep 17 00:00:00 2001 From: Steven He Date: Fri, 22 Jul 2022 13:16:38 +0900 Subject: [PATCH 008/197] various fixes --- src/Avalonia.Base/Visual.cs | 10 ++++++++-- src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs | 7 +++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Base/Visual.cs b/src/Avalonia.Base/Visual.cs index 8feba116f0..fbc940114a 100644 --- a/src/Avalonia.Base/Visual.cs +++ b/src/Avalonia.Base/Visual.cs @@ -667,8 +667,14 @@ namespace Avalonia if (_visualParent is IRenderRoot || _visualParent?.IsAttachedToVisualTree == true) { - var root = this.FindAncestorOfType() ?? - throw new AvaloniaInternalException("Visual is atached to visual tree but root could not be found."); + var root = this.FindAncestorOfType(); + if (root is null) + { + Logger.TryGet(LogEventLevel.Error, "Visual")?.Log("Visual", + "Visual is atached to visual tree but root could not be found."); + return; + } + var e = new VisualTreeAttachmentEventArgs(_visualParent, root); OnAttachedToVisualTreeCore(e); } diff --git a/src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs b/src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs index b3469c212b..ccdc5ac19b 100644 --- a/src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs +++ b/src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs @@ -148,6 +148,13 @@ namespace Avalonia.OpenGL.Controls return false; } + if (_context == null) + { + Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log("OpenGlControlBase", + "Unable to initialize OpenGL: unable to create additional OpenGL context."); + return false; + } + GlVersion = _context.Version; try { From 0bdd0c3160d73a35982ed5bcb7fc421a3d7bd381 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 22 Jul 2022 15:24:20 +0100 Subject: [PATCH 009/197] add a failing integration test. --- .../WindowTests.cs | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs b/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs index 2b10c302bc..9e39d0ae58 100644 --- a/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs @@ -1,7 +1,9 @@ using System; +using System.Linq; using System.Runtime.InteropServices; using System.Threading; using Avalonia.Controls; +using OpenQA.Selenium; using OpenQA.Selenium.Appium; using OpenQA.Selenium.Interactions; using Xunit; @@ -55,6 +57,43 @@ namespace Avalonia.IntegrationTests.Appium } } } + + [PlatformFact(TestPlatforms.Windows)] + public void OnWindows_Docked_Windows_Retain_Size_Position_When_Restored() + { + using (OpenWindow(new Size(400, 400), ShowWindowMode.NonOwned, WindowStartupLocation.Manual)) + { + var windowState = _session.FindElementByAccessibilityId("WindowState"); + + Assert.Equal("Normal", windowState.GetComboBoxValue()); + + + var window = _session.FindElements(By.XPath("//Window")).First(); + + new Actions(_session) + .KeyDown(Keys.Meta) + .SendKeys(Keys.Left) + .KeyUp(Keys.Meta) + .Perform(); + + var original = GetWindowInfo(); + + windowState.Click(); + _session.FindElementByName("Minimized").SendClick(); + + new Actions(_session) + .KeyDown(Keys.Alt) + .SendKeys(Keys.Tab) + .KeyUp(Keys.Alt) + .Perform(); + + var current = GetWindowInfo(); + + Assert.Equal(original.Position, current.Position); + Assert.Equal(original.FrameSize, current.FrameSize); + + } + } [Theory] From cdc3100097a48f5bc1a77d483d2ffdd6ad2718ae Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 22 Jul 2022 15:24:39 +0100 Subject: [PATCH 010/197] add fix to make code path consistent with wpf. --- src/Windows/Avalonia.Win32/WindowImpl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 2f1a116af7..ce8374a68f 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -285,7 +285,7 @@ namespace Avalonia.Win32 set { - if (IsWindowVisible(_hwnd)) + if (IsWindowVisible(_hwnd) && _lastWindowState != value) { ShowWindow(value, value != WindowState.Minimized); // If the window is minimized, it shouldn't be activated } From 8c39dd54a08f9ea3bafac190b99cd29cac3351ca Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 22 Jul 2022 16:02:56 +0100 Subject: [PATCH 011/197] fix exiting fullscreen. --- src/Windows/Avalonia.Win32/WindowImpl.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index ce8374a68f..e990efafdb 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -290,6 +290,7 @@ namespace Avalonia.Win32 ShowWindow(value, value != WindowState.Minimized); // If the window is minimized, it shouldn't be activated } + _lastWindowState = value; _showWindowState = value; } } From 5fcc5ecfea3e539e37f497ff8d11602fadfd69ca Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 22 Jul 2022 17:11:34 +0100 Subject: [PATCH 012/197] disable broken tests. --- tests/Avalonia.IntegrationTests.Appium/MenuTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Avalonia.IntegrationTests.Appium/MenuTests.cs b/tests/Avalonia.IntegrationTests.Appium/MenuTests.cs index d1d231466f..b0d395464b 100644 --- a/tests/Avalonia.IntegrationTests.Appium/MenuTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/MenuTests.cs @@ -57,7 +57,7 @@ namespace Avalonia.IntegrationTests.Appium Assert.Equal("_Grandchild", clickedMenuItem.Text); } - [PlatformFact(TestPlatforms.Windows)] + /*[PlatformFact(TestPlatforms.Windows)] public void Select_Child_With_Alt_Arrow_Keys() { new Actions(_session) @@ -103,7 +103,7 @@ namespace Avalonia.IntegrationTests.Appium var clickedMenuItem = _session.FindElementByAccessibilityId("ClickedMenuItem"); Assert.Equal("_Grandchild", clickedMenuItem.Text); - } + }*/ [PlatformFact(TestPlatforms.Windows)] public void Select_Child_With_Click_Arrow_Keys() From a9cc499de3663600c9e8b9bce2646af3af659621 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 27 Jul 2022 10:04:32 +0100 Subject: [PATCH 013/197] Revert "disable broken tests." This reverts commit 5fcc5ecfea3e539e37f497ff8d11602fadfd69ca. --- tests/Avalonia.IntegrationTests.Appium/MenuTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Avalonia.IntegrationTests.Appium/MenuTests.cs b/tests/Avalonia.IntegrationTests.Appium/MenuTests.cs index b0d395464b..d1d231466f 100644 --- a/tests/Avalonia.IntegrationTests.Appium/MenuTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/MenuTests.cs @@ -57,7 +57,7 @@ namespace Avalonia.IntegrationTests.Appium Assert.Equal("_Grandchild", clickedMenuItem.Text); } - /*[PlatformFact(TestPlatforms.Windows)] + [PlatformFact(TestPlatforms.Windows)] public void Select_Child_With_Alt_Arrow_Keys() { new Actions(_session) @@ -103,7 +103,7 @@ namespace Avalonia.IntegrationTests.Appium var clickedMenuItem = _session.FindElementByAccessibilityId("ClickedMenuItem"); Assert.Equal("_Grandchild", clickedMenuItem.Text); - }*/ + } [PlatformFact(TestPlatforms.Windows)] public void Select_Child_With_Click_Arrow_Keys() From eb8ddef4122fe59e54123e125282a13bcfe822fd Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 28 Jul 2022 10:50:20 +0200 Subject: [PATCH 014/197] Add failing integration test on macOS. Hiding a child window, and then clicking on the parent window causes the child window to be erroneously re-shown. --- .../IntegrationTestApp/MainWindow.axaml.cs | 1 + .../IntegrationTestApp/ShowWindowTest.axaml | 3 +- .../WindowTests_MacOS.cs | 29 +++++++++++++++++++ 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/samples/IntegrationTestApp/MainWindow.axaml.cs b/samples/IntegrationTestApp/MainWindow.axaml.cs index 9e180b12c5..1dab74dc9e 100644 --- a/samples/IntegrationTestApp/MainWindow.axaml.cs +++ b/samples/IntegrationTestApp/MainWindow.axaml.cs @@ -99,6 +99,7 @@ namespace IntegrationTestApp foreach (var window in lifetime.Windows) { + window.Show(); if (window.WindowState == WindowState.Minimized) window.WindowState = WindowState.Normal; } diff --git a/samples/IntegrationTestApp/ShowWindowTest.axaml b/samples/IntegrationTestApp/ShowWindowTest.axaml index 40c1642e91..4001bac7e2 100644 --- a/samples/IntegrationTestApp/ShowWindowTest.axaml +++ b/samples/IntegrationTestApp/ShowWindowTest.axaml @@ -3,7 +3,7 @@ x:Class="IntegrationTestApp.ShowWindowTest" Name="SecondaryWindow" Title="Show Window Test"> - + @@ -31,5 +31,6 @@ Maximized Fullscreen + diff --git a/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs b/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs index ecbdd5bade..4e5344dd25 100644 --- a/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs +++ b/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs @@ -211,6 +211,35 @@ namespace Avalonia.IntegrationTests.Appium } } + [PlatformFact(TestPlatforms.MacOS)] + public void Hidden_Child_Window_Is_Not_Reshown_When_Parent_Clicked() + { + var mainWindow = _session.FindElementByAccessibilityId("MainWindow"); + + // We don't use dispose to close the window here, because it seems that hiding and re-showing a window + // causes Appium to think it's a different window. + OpenWindow(null, ShowWindowMode.Owned, WindowStartupLocation.Manual); + + var secondaryWindow = FindWindow(_session, "SecondaryWindow"); + var hideButton = secondaryWindow.FindElementByAccessibilityId("HideButton"); + + hideButton.Click(); + + var windows = _session.FindElementsByXPath("XCUIElementTypeWindow"); + Assert.Single(windows); + + mainWindow.Click(); + + windows = _session.FindElementsByXPath("XCUIElementTypeWindow"); + Assert.Single(windows); + + _session.FindElementByAccessibilityId("RestoreAll").Click(); + + // Close the window manually. + secondaryWindow = FindWindow(_session, "SecondaryWindow"); + secondaryWindow.GetChromeButtons().close.Click(); + } + private IDisposable OpenWindow(PixelSize? size, ShowWindowMode mode, WindowStartupLocation location) { var sizeTextBox = _session.FindElementByAccessibilityId("ShowWindowSize"); From 9d356894bca636e1e1789c456e2440f08c119a18 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 28 Jul 2022 11:04:33 +0200 Subject: [PATCH 015/197] macOS: Don't bring invisible window to front. Check that a window is visible before bringing it to front, as bringing to front also shows the window. --- native/Avalonia.Native/src/OSX/WindowImpl.mm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.mm b/native/Avalonia.Native/src/OSX/WindowImpl.mm index 95f61422cb..af8c53cb33 100644 --- a/native/Avalonia.Native/src/OSX/WindowImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowImpl.mm @@ -121,7 +121,7 @@ void WindowImpl::BringToFront() { if(Window != nullptr) { - if (![Window isMiniaturized]) + if ([Window isVisible] && ![Window isMiniaturized]) { if(IsDialog()) { From 23b08b1f34734af2c8942f75ef0cf88181943fd9 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Thu, 28 Jul 2022 17:11:39 +0200 Subject: [PATCH 016/197] Attempt to fix consistent hit testing results GetCharacterHitFromDistance and GetDistanceFromCharacterHit should match all the time --- samples/Sandbox/MainWindow.axaml | 10 + samples/Sandbox/MainWindow.axaml.cs | 75 +++- src/Avalonia.Base/Media/GlyphRun.cs | 2 +- .../TextFormatting/FormattedTextSource.cs | 24 ++ .../Media/TextFormatting/TextLineImpl.cs | 339 ++++++++++++++---- .../Presenters/TextPresenter.cs | 5 +- src/Avalonia.Controls/TextBox.cs | 10 +- .../Media/TextFormatting/TextLayoutTests.cs | 68 +++- 8 files changed, 453 insertions(+), 80 deletions(-) diff --git a/samples/Sandbox/MainWindow.axaml b/samples/Sandbox/MainWindow.axaml index 6929f192c7..0fc78795a6 100644 --- a/samples/Sandbox/MainWindow.axaml +++ b/samples/Sandbox/MainWindow.axaml @@ -1,4 +1,14 @@ + + + + + + + + + + diff --git a/samples/Sandbox/MainWindow.axaml.cs b/samples/Sandbox/MainWindow.axaml.cs index 3d54036d29..7a8b56bbb6 100644 --- a/samples/Sandbox/MainWindow.axaml.cs +++ b/samples/Sandbox/MainWindow.axaml.cs @@ -1,7 +1,11 @@ +using System.ComponentModel; +using System.Diagnostics; +using System.Runtime.CompilerServices; using Avalonia; using Avalonia.Controls; +using Avalonia.Controls.Presenters; using Avalonia.Markup.Xaml; -using Avalonia.Win32.WinRT.Composition; +using Avalonia.VisualTree; namespace Sandbox { @@ -11,6 +15,19 @@ namespace Sandbox { this.InitializeComponent(); this.AttachDevTools(); + + var textBox = this.FindControl("txtBox"); + + textBox.TemplateApplied += TextBox_TemplateApplied; + } + + private void TextBox_TemplateApplied(object sender, Avalonia.Controls.Primitives.TemplateAppliedEventArgs e) + { + var textBox = sender as TextBox; + + var textPresenter = e.NameScope.Find("PART_TextPresenter") as TextPresenter; + + DataContext = new TestViewModel(textPresenter); } private void InitializeComponent() @@ -18,4 +35,60 @@ namespace Sandbox AvaloniaXamlLoader.Load(this); } } + + public class TestViewModel : ViewModelBase + { + private readonly TextPresenter _textPresenter; + private double _distance = 45; + + public TestViewModel(TextPresenter textPresenter) + { + _textPresenter = textPresenter; + } + + public double Distance + { + get => _distance; + set + { + OnDistanceChanged(value); + RaisePropertyChanged(); + } + } + + private void OnDistanceChanged(double distance) + { + if(distance < 0) + { + distance = 0; + } + + if(distance > _textPresenter.TextLayout.Bounds.Width) + { + distance = _textPresenter.TextLayout.Bounds.Width; + } + + var height = _textPresenter.TextLayout.Bounds.Height; + + var distanceY = height / 2; + + _textPresenter.MoveCaretToPoint(new Point(distance, distanceY)); + + var caretIndex = _textPresenter.CaretIndex; + + Debug.WriteLine(caretIndex); + + _distance = distance; + } + } + + public class ViewModelBase : INotifyPropertyChanged + { + public event PropertyChangedEventHandler PropertyChanged; + + protected void RaisePropertyChanged([CallerMemberName]string propertyName = "") + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + } } diff --git a/src/Avalonia.Base/Media/GlyphRun.cs b/src/Avalonia.Base/Media/GlyphRun.cs index 2a7f3360ad..cae7a8fe75 100644 --- a/src/Avalonia.Base/Media/GlyphRun.cs +++ b/src/Avalonia.Base/Media/GlyphRun.cs @@ -265,7 +265,7 @@ namespace Avalonia.Media //RightToLeft var glyphIndex = FindGlyphIndex(characterIndex); - if (GlyphClusters != null) + if (GlyphClusters != null && GlyphClusters.Count > 0) { if (characterIndex > GlyphClusters[0]) { diff --git a/src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs b/src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs index 97df87d3d9..7ab67ea34d 100644 --- a/src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs +++ b/src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Utilities; namespace Avalonia.Media.TextFormatting @@ -116,7 +117,30 @@ namespace Avalonia.Media.TextFormatting length = text.Length; } + length = CoerceLength(text, length); + return new ValueSpan(firstTextSourceIndex, length, currentProperties); } + + private static int CoerceLength(ReadOnlySlice text, int length) + { + var finalLength = 0; + + var graphemeEnumerator = new GraphemeEnumerator(text); + + while (graphemeEnumerator.MoveNext()) + { + var grapheme = graphemeEnumerator.Current; + + finalLength += grapheme.Text.Length; + + if (finalLength >= length) + { + return finalLength; + } + } + + return length; + } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index f3c62f4994..0ee791d935 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -183,8 +183,47 @@ namespace Avalonia.Media.TextFormatting var currentPosition = FirstTextSourceIndex; var currentDistance = 0.0; - foreach (var currentRun in _textRuns) + for (var i = 0; i < _textRuns.Count; i++) { + var currentRun = _textRuns[i]; + + if(currentRun is ShapedTextCharacters shapedRun && !shapedRun.ShapedBuffer.IsLeftToRight) + { + var rightToLeftIndex = i; + var rightToLeftDistance = shapedRun.Size.Width; + + while (rightToLeftIndex + 1 <= _textRuns.Count - 1) + { + var nextShaped = _textRuns[rightToLeftIndex + 1] as ShapedTextCharacters; + + if (nextShaped == null || nextShaped.ShapedBuffer.IsLeftToRight) + { + break; + } + + rightToLeftIndex++; + + rightToLeftDistance += nextShaped.Size.Width; + } + + for (var j = rightToLeftIndex; rightToLeftIndex >= 0; j--) + { + currentRun = _textRuns[j]; + + if(distance <= currentDistance + rightToLeftDistance - currentRun.Size.Width) + { + currentPosition += currentRun.TextSourceLength; + rightToLeftDistance -= currentRun.Size.Width; + + continue; + } + + characterHit = GetRunCharacterHit(currentRun, currentPosition, distance - currentDistance); + + break; + } + } + if (currentDistance + currentRun.Size.Width < distance) { currentDistance += currentRun.Size.Width; @@ -255,6 +294,37 @@ namespace Avalonia.Media.TextFormatting { var currentRun = _textRuns[index]; + if (currentRun is ShapedTextCharacters shapedRun && !shapedRun.ShapedBuffer.IsLeftToRight) + { + var i = index; + + while (i + 1 <= _textRuns.Count - 1) + { + var nextRun = _textRuns[i + 1]; + + if (nextRun is ShapedTextCharacters nextShapedRun) + { + if (nextShapedRun.ShapedBuffer.IsLeftToRight) + { + break; + } + } + + i++; + } + + while (i > index) + { + var rightToLeftRun = _textRuns[i]; + + currentPosition += rightToLeftRun.TextSourceLength; + + remainingLength -= rightToLeftRun.TextSourceLength; + + i--; + } + } + if (TryGetDistanceFromCharacterHit(currentRun, characterHit, currentPosition, remainingLength, flowDirection, out var distance, out _)) { @@ -442,92 +512,130 @@ namespace Avalonia.Media.TextFormatting continue; } - if (currentPosition + currentRun.TextSourceLength <= firstTextSourceIndex) - { - startX += currentRun.Size.Width; - - currentPosition += currentRun.TextSourceLength; - - continue; - } - var characterLength = 0; var endX = startX; + var runWidth = 0.0; + TextRunBounds? currentRunBounds = null; - if (currentRun is ShapedTextCharacters currentShapedRun) + var currentShapedRun = currentRun as ShapedTextCharacters; + + if (currentShapedRun != null && !currentShapedRun.ShapedBuffer.IsLeftToRight) { - var offset = Math.Max(0, firstTextSourceIndex - currentPosition); + var rightToLeftIndex = index; + startX += currentShapedRun.Size.Width; - currentPosition += offset; + while (rightToLeftIndex + 1 <= _textRuns.Count - 1) + { + var nextShapedRun = _textRuns[rightToLeftIndex + 1] as ShapedTextCharacters; - var startIndex = currentRun.Text.Start + offset; + if (nextShapedRun == null || nextShapedRun.ShapedBuffer.IsLeftToRight) + { + break; + } - double startOffset; - double endOffset; + startX += nextShapedRun.Size.Width; - if (currentShapedRun.ShapedBuffer.IsLeftToRight) + rightToLeftIndex++; + } + + if (TryGetTextRunBoundsRightToLeft(startX, firstTextSourceIndex, characterIndex, rightToLeftIndex, ref currentPosition, ref remainingLength, out currentRunBounds)) { - startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex)); + startX = currentRunBounds!.Rectangle.Left; + endX = currentRunBounds.Rectangle.Right; - endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength)); + runWidth = currentRunBounds.Rectangle.Width; } - else + + currentDirection = FlowDirection.RightToLeft; + } + else + { + if (currentShapedRun != null) { - endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex)); + if (currentPosition + currentRun.TextSourceLength <= firstTextSourceIndex) + { + startX += currentRun.Size.Width; - if (currentPosition < startIndex) + currentPosition += currentRun.TextSourceLength; + + continue; + } + + var offset = Math.Max(0, firstTextSourceIndex - currentPosition); + + currentPosition += offset; + + var startIndex = currentRun.Text.Start + offset; + + double startOffset; + double endOffset; + + if (currentShapedRun.ShapedBuffer.IsLeftToRight) { - startOffset = endOffset; + startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex)); + + endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength)); } else { - startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength)); + endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex)); + + if (currentPosition < startIndex) + { + startOffset = endOffset; + } + else + { + startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength)); + } } - } - startX += startOffset; + startX += startOffset; - endX += endOffset; + endX += endOffset; - var endHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _); - var startHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _); + var endHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _); + var startHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _); - characterLength = Math.Abs(endHit.FirstCharacterIndex + endHit.TrailingLength - startHit.FirstCharacterIndex - startHit.TrailingLength); + characterLength = Math.Abs(endHit.FirstCharacterIndex + endHit.TrailingLength - startHit.FirstCharacterIndex - startHit.TrailingLength); - currentDirection = currentShapedRun.ShapedBuffer.IsLeftToRight ? - FlowDirection.LeftToRight : - FlowDirection.RightToLeft; - } - else - { - if (currentPosition < firstTextSourceIndex) + currentDirection = FlowDirection.LeftToRight; + } + else { - startX += currentRun.Size.Width; + if (currentPosition < firstTextSourceIndex) + { + startX += currentRun.Size.Width; + } + + if (currentPosition + currentRun.TextSourceLength <= characterIndex) + { + endX += currentRun.Size.Width; + + characterLength = currentRun.TextSourceLength; + } } - if (currentPosition + currentRun.TextSourceLength <= characterIndex) + if (endX < startX) { - endX += currentRun.Size.Width; + (endX, startX) = (startX, endX); + } - characterLength = currentRun.TextSourceLength; + //Lines that only contain a linebreak need to be covered here + if (characterLength == 0) + { + characterLength = NewLineLength; } - } - if (endX < startX) - { - (endX, startX) = (startX, endX); - } + runWidth = endX - startX; + currentRunBounds = new TextRunBounds(new Rect(startX, 0, runWidth, Height), currentPosition, characterLength, currentRun); - //Lines that only contain a linebreak need to be covered here - if (characterLength == 0) - { - characterLength = NewLineLength; - } + currentPosition += characterLength; - var runWidth = endX - startX; - var currentRunBounds = new TextRunBounds(new Rect(startX, 0, runWidth, Height), currentPosition, characterLength, currentRun); + remainingLength -= characterLength; + } - if (!MathUtilities.IsZero(runWidth) || NewLineLength > 0) + if (currentRunBounds != null && !MathUtilities.IsZero(runWidth) || NewLineLength > 0) { if (lastDirection == currentDirection && result.Count > 0 && MathUtilities.AreClose(currentRect.Right, startX)) { @@ -537,32 +645,26 @@ namespace Avalonia.Media.TextFormatting textBounds.Rectangle = currentRect; - textBounds.TextRunBounds.Add(currentRunBounds); + textBounds.TextRunBounds.Add(currentRunBounds!); } else { - currentRect = currentRunBounds.Rectangle; + currentRect = currentRunBounds!.Rectangle; result.Add(new TextBounds(currentRect, currentDirection, new List { currentRunBounds })); } } currentWidth += runWidth; - currentPosition += characterLength; + - if (currentPosition > characterIndex) + if (remainingLength <= 0 || currentPosition >= characterIndex) { break; } startX = endX; lastDirection = currentDirection; - remainingLength -= characterLength; - - if (remainingLength <= 0) - { - break; - } } return result; @@ -674,7 +776,7 @@ namespace Avalonia.Media.TextFormatting var currentRunBounds = new TextRunBounds(new Rect(Start + startX, 0, runWidth, Height), currentPosition, characterLength, currentRun); - if(!MathUtilities.IsZero(runWidth) || NewLineLength > 0) + if (!MathUtilities.IsZero(runWidth) || NewLineLength > 0) { if (lastDirection == currentDirection && result.Count > 0 && MathUtilities.AreClose(currentRect.Right, Start + startX)) { @@ -692,7 +794,7 @@ namespace Avalonia.Media.TextFormatting result.Add(new TextBounds(currentRect, currentDirection, new List { currentRunBounds })); } - } + } currentWidth += runWidth; currentPosition += characterLength; @@ -716,6 +818,107 @@ namespace Avalonia.Media.TextFormatting return result; } + private bool TryGetTextRunBoundsRightToLeft(double startX, int firstTextSourceIndex, int characterIndex, int runIndex, ref int currentPosition, ref int remainingLength, out TextRunBounds? textRunBounds) + { + textRunBounds = null; + + for (var index = runIndex; index >= 0; index--) + { + if (TextRuns[index] is not DrawableTextRun currentRun) + { + continue; + } + + if (currentPosition + currentRun.TextSourceLength <= firstTextSourceIndex) + { + startX -= currentRun.Size.Width; + + currentPosition += currentRun.TextSourceLength; + + continue; + } + + var characterLength = 0; + var endX = startX; + + if (currentRun is ShapedTextCharacters currentShapedRun) + { + var offset = Math.Max(0, firstTextSourceIndex - currentPosition); + + currentPosition += offset; + + var startIndex = currentRun.Text.Start + offset; + double startOffset; + double endOffset; + + if (currentShapedRun.ShapedBuffer.IsLeftToRight) + { + if (currentPosition < startIndex) + { + startOffset = endOffset = 0; + } + else + { + endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength)); + + startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex)); + } + } + else + { + endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex)); + + startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength)); + } + + startX -= currentRun.Size.Width - startOffset; + endX -= currentRun.Size.Width - endOffset; + + var endHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _); + var startHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _); + + characterLength = Math.Abs(startHit.FirstCharacterIndex + startHit.TrailingLength - endHit.FirstCharacterIndex - endHit.TrailingLength); + } + else + { + if (currentPosition + currentRun.TextSourceLength <= characterIndex) + { + endX -= currentRun.Size.Width; + } + + if (currentPosition < firstTextSourceIndex) + { + startX -= currentRun.Size.Width; + + characterLength = currentRun.TextSourceLength; + } + } + + if (endX < startX) + { + (endX, startX) = (startX, endX); + } + + //Lines that only contain a linebreak need to be covered here + if (characterLength == 0) + { + characterLength = NewLineLength; + } + + var runWidth = endX - startX; + + remainingLength -= characterLength; + + currentPosition += characterLength; + + textRunBounds = new TextRunBounds(new Rect(Start + startX, 0, runWidth, Height), currentPosition, characterLength, currentRun); + + return true; + } + + return false; + } + public override IReadOnlyList GetTextBounds(int firstTextSourceIndex, int textLength) { if (_paragraphProperties.FlowDirection == FlowDirection.LeftToRight) @@ -1319,12 +1522,12 @@ namespace Avalonia.Media.TextFormatting case TextAlignment.Center: var start = (_paragraphWidth - width) / 2; - if(paragraphFlowDirection == FlowDirection.RightToLeft) + if (paragraphFlowDirection == FlowDirection.RightToLeft) { start -= (widthIncludingTrailingWhitespace - width); } - return Math.Max(0, start); + return Math.Max(0, start); case TextAlignment.Right: return Math.Max(0, _paragraphWidth - widthIncludingTrailingWhitespace); diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index e463bc5731..4f9be86641 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -9,6 +9,7 @@ using Avalonia.VisualTree; using Avalonia.Layout; using Avalonia.Media.Immutable; using Avalonia.Controls.Documents; +using Avalonia.Media.TextFormatting.Unicode; namespace Avalonia.Controls.Presenters { @@ -496,14 +497,14 @@ namespace Avalonia.Controls.Presenters var length = Math.Max(selectionStart, selectionEnd) - start; IReadOnlyList>? textStyleOverrides = null; - + if (length > 0) { textStyleOverrides = new[] { new ValueSpan(start, length, new GenericTextRunProperties(typeface, FontSize, - foregroundBrush: SelectionForegroundBrush ?? Brushes.White)) + foregroundBrush: SelectionForegroundBrush ?? Brushes.Red)) }; } diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 1b268db2f7..8490bfd3a0 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -17,6 +17,7 @@ using Avalonia.Controls.Metadata; using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Automation.Peers; +using System.Diagnostics; namespace Avalonia.Controls { @@ -1240,9 +1241,12 @@ namespace Avalonia.Controls MathUtilities.Clamp(point.X, 0, Math.Max(_presenter.Bounds.Width - 1, 0)), MathUtilities.Clamp(point.Y, 0, Math.Max(_presenter.Bounds.Height - 1, 0))); - _presenter.MoveCaretToPoint(point); + var hit = _presenter.TextLayout.HitTestPoint(point); + + var caretIndex = hit.TextPosition; + + Debug.WriteLine($"TextPos: {caretIndex}, X: {point.X}"); - var caretIndex = _presenter.CaretIndex; var text = Text; if (text != null && _wordSelectionStart >= 0) @@ -1266,7 +1270,7 @@ namespace Avalonia.Controls } else { - SelectionEnd = _presenter.CaretIndex; + SelectionEnd = caretIndex; } } } diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs index 7d33f094fa..6d057d900e 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Globalization; using System.Linq; using Avalonia.Media; @@ -914,14 +915,14 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting public void Should_Get_CharacterHit_From_Distance_RTL() { using (Start()) - { + { var text = "أَبْجَدِيَّة عَرَبِيَّة"; var layout = new TextLayout( - text, - Typeface.Default, - 12, - Brushes.Black); + text, + Typeface.Default, + 12, + Brushes.Black); var textLine = layout.TextLines[0]; @@ -952,6 +953,63 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting rect = layout.HitTestTextPosition(23); Assert.Equal(0, rect.Left, 5); + + } + } + + [Fact] + public void Should_Get_CharacterHit_From_Distance_RTL_With_TextStyles() + { + using (Start()) + { + var text = "أَبْجَدِيَّة عَرَبِيَّة"; + + var i = 0; + + var graphemeEnumerator = new GraphemeEnumerator(text.AsMemory()); + + while (graphemeEnumerator.MoveNext()) + { + var grapheme = graphemeEnumerator.Current; + + var textStyleOverrides = new[] { new ValueSpan(i, grapheme.Text.Length, new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: Brushes.Red)) }; + + i += grapheme.Text.Length; + + var layout = new TextLayout( + text, + Typeface.Default, + 12, + Brushes.Black, + textStyleOverrides: textStyleOverrides); + + var textLine = layout.TextLines[0]; + + var shapedRuns = textLine.TextRuns.Cast().ToList(); + + var clusters = shapedRuns.SelectMany(x => x.ShapedBuffer.GlyphClusters).ToList(); + + var glyphAdvances = shapedRuns.SelectMany(x => x.ShapedBuffer.GlyphAdvances).ToList(); + + var currentX = 0.0; + + for (int j = 0; j < clusters.Count; j++) + { + var cluster = clusters[j]; + + var characterHit = textLine.GetCharacterHitFromDistance(currentX); + + Assert.Equal(cluster, characterHit.FirstCharacterIndex + characterHit.TrailingLength); + + var distance = textLine.GetDistanceFromCharacterHit(new CharacterHit(cluster)); + + Assert.Equal(currentX, distance); + + var glyphAdvance = glyphAdvances[j]; + + currentX += glyphAdvance; + } + } } } From 44b8df516c461dbbc38f825c5dab9aa01ee94dd2 Mon Sep 17 00:00:00 2001 From: Luis von der Eltz Date: Fri, 29 Jul 2022 11:26:55 +0200 Subject: [PATCH 017/197] Set OverlayDismissEventPassThrough to false, add OverlayInputPassThroughElement property --- src/Avalonia.Controls/Flyouts/FlyoutBase.cs | 50 ++++++++++----------- 1 file changed, 24 insertions(+), 26 deletions(-) diff --git a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs index a0f3407b7a..bf938abc79 100644 --- a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs +++ b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs @@ -16,8 +16,8 @@ namespace Avalonia.Controls.Primitives /// Defines the property /// public static readonly DirectProperty IsOpenProperty = - AvaloniaProperty.RegisterDirect(nameof(IsOpen), - x => x.IsOpen); + AvaloniaProperty.RegisterDirect(nameof(IsOpen), + x => x.IsOpen); /// /// Defines the property @@ -39,16 +39,18 @@ namespace Avalonia.Controls.Primitives x => x.ShowMode, (x, v) => x.ShowMode = v); /// - /// Defines the AttachedFlyout property + /// Defines the property /// - public static readonly AttachedProperty AttachedFlyoutProperty = - AvaloniaProperty.RegisterAttached("AttachedFlyout", null); + public static readonly DirectProperty OverlayInputPassThroughElementProperty = + Popup.OverlayInputPassThroughElementProperty.AddOwner( + o => o._overlayInputPassThroughElement, + (o, v) => o._overlayInputPassThroughElement = v); /// - /// Defines the OverlayDismissEventPassThrough property + /// Defines the AttachedFlyout property /// - public static readonly StyledProperty OverlayDismissEventPassThroughProperty = - Popup.OverlayDismissEventPassThroughProperty.AddOwner(); + public static readonly AttachedProperty AttachedFlyoutProperty = + AvaloniaProperty.RegisterAttached("AttachedFlyout", null); private readonly Lazy _popupLazy; private bool _isOpen; @@ -58,10 +60,10 @@ namespace Avalonia.Controls.Primitives private PixelRect? _enlargePopupRectScreenPixelRect; private IDisposable? _transientDisposable; private Action? _popupHostChangedHandler; + private IInputElement? _overlayInputPassThroughElement; static FlyoutBase() { - OverlayDismissEventPassThroughProperty.OverrideDefaultValue(true); Control.ContextFlyoutProperty.Changed.Subscribe(OnContextFlyoutPropertyChanged); } @@ -109,25 +111,20 @@ namespace Avalonia.Controls.Primitives } /// - /// Gets or sets a value indicating whether the event that closes the flyout is passed - /// through to the parent window. + /// Gets or sets an element that should receive pointer input events even when underneath + /// the flyout's overlay. /// - /// - /// Clicks outside the the flyout cause the flyout to close. When is set to - /// false, these clicks will be handled by the flyout and not be registered by the parent - /// window. When set to true, the events will be passed through to the parent window. - /// - public bool OverlayDismissEventPassThrough + public IInputElement? OverlayInputPassThroughElement { - get => GetValue(OverlayDismissEventPassThroughProperty); - set => SetValue(OverlayDismissEventPassThroughProperty, value); + get => _overlayInputPassThroughElement; + set => SetAndRaise(OverlayInputPassThroughElementProperty, ref _overlayInputPassThroughElement, value); } IPopupHost? IPopupHostProvider.PopupHost => Popup?.Host; - event Action? IPopupHostProvider.PopupHostChanged - { - add => _popupHostChangedHandler += value; + event Action? IPopupHostProvider.PopupHostChanged + { + add => _popupHostChangedHandler += value; remove => _popupHostChangedHandler -= value; } @@ -200,7 +197,7 @@ namespace Avalonia.Controls.Primitives Popup.OverlayInputPassThroughElement = null; ((ISetLogicalParent)Popup).SetParent(null); - + // Ensure this isn't active _transientDisposable?.Dispose(); _transientDisposable = null; @@ -255,8 +252,7 @@ namespace Avalonia.Controls.Primitives Popup.Child = CreatePresenter(); } - Popup.OverlayInputPassThroughElement = placementTarget; - Popup.OverlayDismissEventPassThrough = OverlayDismissEventPassThrough; + Popup.OverlayInputPassThroughElement = OverlayInputPassThroughElement; if (CancelOpening()) { @@ -386,7 +382,9 @@ namespace Avalonia.Controls.Primitives var popup = new Popup { WindowManagerAddShadowHint = false, - IsLightDismissEnabled = true + IsLightDismissEnabled = true, + //Note: This is required to prevent Button.Flyout from opening the flyout again after dismiss. + OverlayDismissEventPassThrough = false }; popup.Opened += OnPopupOpened; From 29d957282ece12d6f497027ed4fa9d28cfce33fd Mon Sep 17 00:00:00 2001 From: Luis von der Eltz Date: Fri, 29 Jul 2022 11:28:19 +0200 Subject: [PATCH 018/197] Remove unsetting --- src/Avalonia.Controls/Flyouts/FlyoutBase.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs index bf938abc79..00ebcab70e 100644 --- a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs +++ b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs @@ -194,7 +194,6 @@ namespace Avalonia.Controls.Primitives IsOpen = false; Popup.IsOpen = false; - Popup.OverlayInputPassThroughElement = null; ((ISetLogicalParent)Popup).SetParent(null); From 81b0dce302f3e588ce2e6e5e16a9d1eb0b5d69ea Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Fri, 29 Jul 2022 11:55:52 +0200 Subject: [PATCH 019/197] Fix RichTextBlock Inlines update handling --- samples/Sandbox/MainWindow.axaml | 15 ++++++ samples/Sandbox/MainWindow.axaml.cs | 46 ++++++++++++++++++- .../Media/TextFormatting/TextLayout.cs | 7 ++- .../Media/TextFormatting/TextLine.cs | 2 +- .../Media/TextFormatting/TextLineImpl.cs | 7 ++- src/Avalonia.Controls/RichTextBlock.cs | 22 ++++----- .../RichTextBlockTests.cs | 44 ++++++++++++++++++ .../Media/TextFormatting/TextLayoutTests.cs | 12 +++-- 8 files changed, 135 insertions(+), 20 deletions(-) diff --git a/samples/Sandbox/MainWindow.axaml b/samples/Sandbox/MainWindow.axaml index 0fc78795a6..cf2ce63c1f 100644 --- a/samples/Sandbox/MainWindow.axaml +++ b/samples/Sandbox/MainWindow.axaml @@ -9,6 +9,21 @@ + + + + + + diff --git a/samples/Sandbox/MainWindow.axaml.cs b/samples/Sandbox/MainWindow.axaml.cs index 7a8b56bbb6..3040385708 100644 --- a/samples/Sandbox/MainWindow.axaml.cs +++ b/samples/Sandbox/MainWindow.axaml.cs @@ -3,7 +3,9 @@ using System.Diagnostics; using System.Runtime.CompilerServices; using Avalonia; using Avalonia.Controls; +using Avalonia.Controls.Documents; using Avalonia.Controls.Presenters; +using Avalonia.Interactivity; using Avalonia.Markup.Xaml; using Avalonia.VisualTree; @@ -11,6 +13,8 @@ namespace Sandbox { public class MainWindow : Window { + private TestViewModel _dc; + public MainWindow() { this.InitializeComponent(); @@ -27,13 +31,30 @@ namespace Sandbox var textPresenter = e.NameScope.Find("PART_TextPresenter") as TextPresenter; - DataContext = new TestViewModel(textPresenter); + _dc = new TestViewModel(textPresenter); + + DataContext = _dc; } private void InitializeComponent() { AvaloniaXamlLoader.Load(this); } + + private void Button_OnClick(object? sender, RoutedEventArgs e) + { + _dc.InlineCollection = new InlineCollection + { + new Run(""), + new Run("test3") {FontWeight = Avalonia.Media.FontWeight.Bold}, + }; + // _dc.Text = "nununu"; + } + + private void TextButton_OnClick(object? sender, RoutedEventArgs e) + { + _dc.Text = "nununu"; + } } public class TestViewModel : ViewModelBase @@ -46,6 +67,29 @@ namespace Sandbox _textPresenter = textPresenter; } + private InlineCollection _inlineCollection; + private string _text; + + public string Text + { + get => _text; + set + { + _text = value; + RaisePropertyChanged(); + } + } + + public InlineCollection InlineCollection + { + get => _inlineCollection; + set + { + _inlineCollection = value; + RaisePropertyChanged(); + } + } + public double Distance { get => _distance; diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs index f3e8b5969c..0828b6518a 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs @@ -537,8 +537,13 @@ namespace Avalonia.Media.TextFormatting /// /// The collapsing width. /// The . - private TextCollapsingProperties GetCollapsingProperties(double width) + private TextCollapsingProperties? GetCollapsingProperties(double width) { + if(_textTrimming == TextTrimming.None) + { + return null; + } + return _textTrimming.CreateCollapsingProperties(new TextCollapsingCreateInfo(width, _paragraphProperties.DefaultTextRunProperties)); } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLine.cs b/src/Avalonia.Base/Media/TextFormatting/TextLine.cs index c8a23097db..61b24dc8c5 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLine.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLine.cs @@ -153,7 +153,7 @@ namespace Avalonia.Media.TextFormatting /// /// A value that represents a collapsed line that can be displayed. /// - public abstract TextLine Collapse(params TextCollapsingProperties[] collapsingPropertiesList); + public abstract TextLine Collapse(params TextCollapsingProperties?[] collapsingPropertiesList); /// /// Create a justified line based on justification text properties. diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index 0ee791d935..a0aae76fad 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -119,7 +119,7 @@ namespace Avalonia.Media.TextFormatting } /// - public override TextLine Collapse(params TextCollapsingProperties[] collapsingPropertiesList) + public override TextLine Collapse(params TextCollapsingProperties?[] collapsingPropertiesList) { if (collapsingPropertiesList.Length == 0) { @@ -128,6 +128,11 @@ namespace Avalonia.Media.TextFormatting var collapsingProperties = collapsingPropertiesList[0]; + if(collapsingProperties is null) + { + return this; + } + var collapsedRuns = collapsingProperties.Collapse(this); if (collapsedRuns is null) diff --git a/src/Avalonia.Controls/RichTextBlock.cs b/src/Avalonia.Controls/RichTextBlock.cs index 0c8b1d125d..2f8bed3be7 100644 --- a/src/Avalonia.Controls/RichTextBlock.cs +++ b/src/Avalonia.Controls/RichTextBlock.cs @@ -44,8 +44,8 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly StyledProperty InlinesProperty = - AvaloniaProperty.Register( + public static readonly StyledProperty InlinesProperty = + AvaloniaProperty.Register( nameof(Inlines)); public static readonly DirectProperty CanCopyProperty = @@ -138,7 +138,7 @@ namespace Avalonia.Controls /// Gets or sets the inlines. /// [Content] - public InlineCollection Inlines + public InlineCollection? Inlines { get => GetValue(InlinesProperty); set => SetValue(InlinesProperty, value); @@ -159,7 +159,7 @@ namespace Avalonia.Controls remove => RemoveHandler(CopyingToClipboardEvent, value); } - internal bool HasComplexContent => Inlines.Count > 0; + internal bool HasComplexContent => Inlines != null && Inlines.Count > 0; /// /// Copies the current selection to the Clipboard. @@ -260,18 +260,18 @@ namespace Avalonia.Controls { if (!string.IsNullOrEmpty(_text)) { - Inlines.Add(_text); + Inlines?.Add(_text); _text = null; } - Inlines.Add(text); + Inlines?.Add(text); } } protected override string? GetText() { - return _text ?? Inlines.Text; + return _text ?? Inlines?.Text; } protected override void SetText(string? text) @@ -301,10 +301,10 @@ namespace Avalonia.Controls ITextSource textSource; - var inlines = Inlines; - if (HasComplexContent) { + var inlines = Inlines!; + var textRuns = new List(); foreach (var inline in inlines) @@ -537,7 +537,7 @@ namespace Avalonia.Controls switch (change.Property.Name) { - case nameof(InlinesProperty): + case nameof(Inlines): { OnInlinesChanged(change.OldValue as InlineCollection, change.NewValue as InlineCollection); InvalidateTextLayout(); @@ -553,7 +553,7 @@ namespace Avalonia.Controls return ""; } - var text = Inlines.Text ?? Text; + var text = GetText(); if (string.IsNullOrEmpty(text)) { diff --git a/tests/Avalonia.Controls.UnitTests/RichTextBlockTests.cs b/tests/Avalonia.Controls.UnitTests/RichTextBlockTests.cs index eb4b88956d..c74f13b808 100644 --- a/tests/Avalonia.Controls.UnitTests/RichTextBlockTests.cs +++ b/tests/Avalonia.Controls.UnitTests/RichTextBlockTests.cs @@ -48,5 +48,49 @@ namespace Avalonia.Controls.UnitTests Assert.False(target.IsMeasureValid); } } + + [Fact] + public void Changing_Inlines_Should_Invalidate_Measure() + { + using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) + { + var target = new RichTextBlock(); + + var inlines = new InlineCollection { new Run("Hello") }; + + target.Measure(Size.Infinity); + + Assert.True(target.IsMeasureValid); + + target.Inlines = inlines; + + Assert.False(target.IsMeasureValid); + } + } + + [Fact] + public void Changing_Inlines_Should_Reset_Inlines_Parent() + { + using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) + { + var target = new RichTextBlock(); + + var run = new Run("Hello"); + + target.Inlines.Add(run); + + target.Measure(Size.Infinity); + + Assert.True(target.IsMeasureValid); + + target.Inlines = null; + + Assert.Null(run.Parent); + + target.Inlines = new InlineCollection { run }; + + Assert.Equal(target, run.Parent); + } + } } } diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs index 6d057d900e..c457a96299 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs @@ -993,9 +993,11 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var currentX = 0.0; - for (int j = 0; j < clusters.Count; j++) - { - var cluster = clusters[j]; + var cluster = text.Length; + + for (int j = 0; j < clusters.Count - 1; j++) + { + var glyphAdvance = glyphAdvances[j]; var characterHit = textLine.GetCharacterHitFromDistance(currentX); @@ -1005,9 +1007,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting Assert.Equal(currentX, distance); - var glyphAdvance = glyphAdvances[j]; - currentX += glyphAdvance; + + cluster = clusters[j]; } } } From e7be7cb88de6cbea64f8492b6899d53b4878030d Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Fri, 29 Jul 2022 16:29:36 +0200 Subject: [PATCH 020/197] Fix that thing --- .../Media/TextFormatting/TextLineImpl.cs | 89 +++++++++++++------ 1 file changed, 61 insertions(+), 28 deletions(-) diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index a0aae76fad..94095cf82b 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -176,7 +176,7 @@ namespace Avalonia.Media.TextFormatting return GetRunCharacterHit(firstRun, FirstTextSourceIndex, 0); } - if (distance > WidthIncludingTrailingWhitespace) + if (distance >= WidthIncludingTrailingWhitespace) { var lastRun = _textRuns[_textRuns.Count - 1]; @@ -195,7 +195,7 @@ namespace Avalonia.Media.TextFormatting if(currentRun is ShapedTextCharacters shapedRun && !shapedRun.ShapedBuffer.IsLeftToRight) { var rightToLeftIndex = i; - var rightToLeftDistance = shapedRun.Size.Width; + currentPosition += currentRun.TextSourceLength; while (rightToLeftIndex + 1 <= _textRuns.Count - 1) { @@ -206,19 +206,24 @@ namespace Avalonia.Media.TextFormatting break; } - rightToLeftIndex++; + currentPosition += nextShaped.TextSourceLength; - rightToLeftDistance += nextShaped.Size.Width; + rightToLeftIndex++; } - for (var j = rightToLeftIndex; rightToLeftIndex >= 0; j--) + for (var j = i; i <= rightToLeftIndex; j++) { + if(j > _textRuns.Count - 1) + { + break; + } + currentRun = _textRuns[j]; - if(distance <= currentDistance + rightToLeftDistance - currentRun.Size.Width) + if(currentDistance + currentRun.Size.Width <= distance) { - currentPosition += currentRun.TextSourceLength; - rightToLeftDistance -= currentRun.Size.Width; + currentDistance += currentRun.Size.Width; + currentPosition -= currentRun.TextSourceLength; continue; } @@ -255,14 +260,18 @@ namespace Avalonia.Media.TextFormatting { characterHit = shapedRun.GlyphRun.GetCharacterHitFromDistance(distance, out _); - var offset = Math.Max(0, currentPosition - shapedRun.Text.Start); + //var offset = 0; - if (!shapedRun.GlyphRun.IsLeftToRight) - { - offset = Math.Max(0, offset - shapedRun.Text.End); - } + //if (shapedRun.GlyphRun.IsLeftToRight) + //{ + // offset = Math.Max(0, currentPosition - shapedRun.Text.Start); + //} + //else + //{ + // offset = Math.Max(0, currentPosition - shapedRun.Text.Start + shapedRun.Text.Length); + //} - characterHit = new CharacterHit(characterHit.FirstCharacterIndex + offset, characterHit.TrailingLength); + //characterHit = new CharacterHit(characterHit.FirstCharacterIndex + offset, characterHit.TrailingLength); break; } @@ -303,35 +312,50 @@ namespace Avalonia.Media.TextFormatting { var i = index; + var rightToLeftWidth = currentRun.Size.Width; + while (i + 1 <= _textRuns.Count - 1) { var nextRun = _textRuns[i + 1]; - if (nextRun is ShapedTextCharacters nextShapedRun) + if (nextRun is ShapedTextCharacters nextShapedRun && !nextShapedRun.ShapedBuffer.IsLeftToRight) { - if (nextShapedRun.ShapedBuffer.IsLeftToRight) - { - break; - } - } + i++; + + rightToLeftWidth += nextRun.Size.Width; - i++; + continue; + } + + break; } - while (i > index) + if(i > index) { - var rightToLeftRun = _textRuns[i]; + while (i >= index) + { + currentRun = _textRuns[i]; + + rightToLeftWidth -= currentRun.Size.Width; - currentPosition += rightToLeftRun.TextSourceLength; + if (currentPosition + currentRun.TextSourceLength >= characterIndex) + { + break; + } - remainingLength -= rightToLeftRun.TextSourceLength; + currentPosition += currentRun.TextSourceLength; - i--; + remainingLength -= currentRun.TextSourceLength; + + i--; + } + + currentDistance += rightToLeftWidth; } } - if (TryGetDistanceFromCharacterHit(currentRun, characterHit, currentPosition, remainingLength, - flowDirection, out var distance, out _)) + if (currentPosition + currentRun.TextSourceLength >= characterIndex && + TryGetDistanceFromCharacterHit(currentRun, characterHit, currentPosition, remainingLength, flowDirection, out var distance, out _)) { return currentDistance + distance; } @@ -608,6 +632,15 @@ namespace Avalonia.Media.TextFormatting } else { + if (currentPosition + currentRun.TextSourceLength <= firstTextSourceIndex) + { + startX += currentRun.Size.Width; + + currentPosition += currentRun.TextSourceLength; + + continue; + } + if (currentPosition < firstTextSourceIndex) { startX += currentRun.Size.Width; From d524dc9cdbc498ff2c8a468fe7cdcdc125ce37c9 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Fri, 29 Jul 2022 16:56:43 +0200 Subject: [PATCH 021/197] Only apply the font fallback to regions of missing glyphs --- samples/Sandbox/MainWindow.axaml | 26 +--- samples/Sandbox/MainWindow.axaml.cs | 115 +----------------- .../Media/TextFormatting/TextCharacters.cs | 26 ++-- .../Presenters/TextPresenter.cs | 4 +- src/Avalonia.Controls/TextBox.cs | 2 - 5 files changed, 18 insertions(+), 155 deletions(-) diff --git a/samples/Sandbox/MainWindow.axaml b/samples/Sandbox/MainWindow.axaml index cf2ce63c1f..d429777eeb 100644 --- a/samples/Sandbox/MainWindow.axaml +++ b/samples/Sandbox/MainWindow.axaml @@ -1,29 +1,5 @@ - - - - - - - - - - - - - - - - + diff --git a/samples/Sandbox/MainWindow.axaml.cs b/samples/Sandbox/MainWindow.axaml.cs index 3040385708..2d36ed6d28 100644 --- a/samples/Sandbox/MainWindow.axaml.cs +++ b/samples/Sandbox/MainWindow.axaml.cs @@ -13,126 +13,15 @@ namespace Sandbox { public class MainWindow : Window { - private TestViewModel _dc; - public MainWindow() { this.InitializeComponent(); - this.AttachDevTools(); - - var textBox = this.FindControl("txtBox"); - - textBox.TemplateApplied += TextBox_TemplateApplied; - } - - private void TextBox_TemplateApplied(object sender, Avalonia.Controls.Primitives.TemplateAppliedEventArgs e) - { - var textBox = sender as TextBox; - - var textPresenter = e.NameScope.Find("PART_TextPresenter") as TextPresenter; - - _dc = new TestViewModel(textPresenter); - - DataContext = _dc; + this.AttachDevTools(); } private void InitializeComponent() { AvaloniaXamlLoader.Load(this); } - - private void Button_OnClick(object? sender, RoutedEventArgs e) - { - _dc.InlineCollection = new InlineCollection - { - new Run(""), - new Run("test3") {FontWeight = Avalonia.Media.FontWeight.Bold}, - }; - // _dc.Text = "nununu"; - } - - private void TextButton_OnClick(object? sender, RoutedEventArgs e) - { - _dc.Text = "nununu"; - } - } - - public class TestViewModel : ViewModelBase - { - private readonly TextPresenter _textPresenter; - private double _distance = 45; - - public TestViewModel(TextPresenter textPresenter) - { - _textPresenter = textPresenter; - } - - private InlineCollection _inlineCollection; - private string _text; - - public string Text - { - get => _text; - set - { - _text = value; - RaisePropertyChanged(); - } - } - - public InlineCollection InlineCollection - { - get => _inlineCollection; - set - { - _inlineCollection = value; - RaisePropertyChanged(); - } - } - - public double Distance - { - get => _distance; - set - { - OnDistanceChanged(value); - RaisePropertyChanged(); - } - } - - private void OnDistanceChanged(double distance) - { - if(distance < 0) - { - distance = 0; - } - - if(distance > _textPresenter.TextLayout.Bounds.Width) - { - distance = _textPresenter.TextLayout.Bounds.Width; - } - - var height = _textPresenter.TextLayout.Bounds.Height; - - var distanceY = height / 2; - - _textPresenter.MoveCaretToPoint(new Point(distance, distanceY)); - - var caretIndex = _textPresenter.CaretIndex; - - Debug.WriteLine(caretIndex); - - _distance = distance; - } - } - - public class ViewModelBase : INotifyPropertyChanged - { - public event PropertyChangedEventHandler PropertyChanged; - - protected void RaisePropertyChanged([CallerMemberName]string propertyName = "") - { - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); - } - } + } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs index ab72601c3e..6611357ee7 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs @@ -38,7 +38,7 @@ namespace Avalonia.Media.TextFormatting /// Gets a list of . /// /// The shapeable text characters. - internal IReadOnlyList GetShapeableCharacters(ReadOnlySlice runText, sbyte biDiLevel, + internal IReadOnlyList GetShapeableCharacters(ReadOnlySlice runText, sbyte biDiLevel, ref TextRunProperties? previousProperties) { var shapeableCharacters = new List(2); @@ -65,7 +65,7 @@ namespace Avalonia.Media.TextFormatting /// The bidi level of the run. /// /// A list of shapeable text runs. - private static ShapeableTextCharacters CreateShapeableRun(ReadOnlySlice text, + private static ShapeableTextCharacters CreateShapeableRun(ReadOnlySlice text, TextRunProperties defaultProperties, sbyte biDiLevel, ref TextRunProperties? previousProperties) { var defaultTypeface = defaultProperties.Typeface; @@ -76,7 +76,7 @@ namespace Avalonia.Media.TextFormatting { if (script == Script.Common && previousTypeface is not null) { - if(TryGetShapeableLength(text, previousTypeface.Value, defaultTypeface, out var fallbackCount, out _)) + if (TryGetShapeableLength(text, previousTypeface.Value, defaultTypeface, out var fallbackCount, out _)) { return new ShapeableTextCharacters(text.Take(fallbackCount), defaultProperties.WithTypeface(previousTypeface.Value), biDiLevel); @@ -86,10 +86,10 @@ namespace Avalonia.Media.TextFormatting return new ShapeableTextCharacters(text.Take(count), defaultProperties.WithTypeface(currentTypeface), biDiLevel); } - + if (previousTypeface is not null) { - if(TryGetShapeableLength(text, previousTypeface.Value, defaultTypeface, out count, out _)) + if (TryGetShapeableLength(text, previousTypeface.Value, defaultTypeface, out count, out _)) { return new ShapeableTextCharacters(text.Take(count), defaultProperties.WithTypeface(previousTypeface.Value), biDiLevel); @@ -106,12 +106,12 @@ namespace Avalonia.Media.TextFormatting { continue; } - + codepoint = codepointEnumerator.Current; - + break; } - + //ToDo: Fix FontFamily fallback var matchFound = FontManager.Current.TryMatchCharacter(codepoint, defaultTypeface.Style, defaultTypeface.Weight, @@ -157,14 +157,14 @@ namespace Avalonia.Media.TextFormatting /// /// protected static bool TryGetShapeableLength( - ReadOnlySlice text, - Typeface typeface, + ReadOnlySlice text, + Typeface typeface, Typeface? defaultTypeface, out int length, out Script script) { length = 0; - script = Script.Unknown; + script = Script.Unknown; if (text.Length == 0) { @@ -182,7 +182,7 @@ namespace Avalonia.Media.TextFormatting var currentScript = currentGrapheme.FirstCodepoint.Script; - if (currentScript != Script.Common && defaultFont != null && defaultFont.TryGetGlyph(currentGrapheme.FirstCodepoint, out _)) + if (defaultFont != null && defaultFont.TryGetGlyph(currentGrapheme.FirstCodepoint, out _)) { break; } @@ -192,7 +192,7 @@ namespace Avalonia.Media.TextFormatting { break; } - + if (currentScript != script) { if (script is Script.Unknown || currentScript != Script.Common && diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index 4f9be86641..e540f58195 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -498,13 +498,13 @@ namespace Avalonia.Controls.Presenters IReadOnlyList>? textStyleOverrides = null; - if (length > 0) + if (length > 0 && SelectionForegroundBrush != null) { textStyleOverrides = new[] { new ValueSpan(start, length, new GenericTextRunProperties(typeface, FontSize, - foregroundBrush: SelectionForegroundBrush ?? Brushes.Red)) + foregroundBrush: SelectionForegroundBrush)) }; } diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 8490bfd3a0..1773163acc 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -1245,8 +1245,6 @@ namespace Avalonia.Controls var caretIndex = hit.TextPosition; - Debug.WriteLine($"TextPos: {caretIndex}, X: {point.X}"); - var text = Text; if (text != null && _wordSelectionStart >= 0) From ca84dda76b168c0181819df735df3bfa1512b924 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Fri, 29 Jul 2022 16:59:46 +0200 Subject: [PATCH 022/197] Revert changes --- samples/Sandbox/MainWindow.axaml | 1 - samples/Sandbox/MainWindow.axaml.cs | 12 +++--------- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/samples/Sandbox/MainWindow.axaml b/samples/Sandbox/MainWindow.axaml index d429777eeb..6929f192c7 100644 --- a/samples/Sandbox/MainWindow.axaml +++ b/samples/Sandbox/MainWindow.axaml @@ -1,5 +1,4 @@ - diff --git a/samples/Sandbox/MainWindow.axaml.cs b/samples/Sandbox/MainWindow.axaml.cs index 2d36ed6d28..3d54036d29 100644 --- a/samples/Sandbox/MainWindow.axaml.cs +++ b/samples/Sandbox/MainWindow.axaml.cs @@ -1,13 +1,7 @@ -using System.ComponentModel; -using System.Diagnostics; -using System.Runtime.CompilerServices; using Avalonia; using Avalonia.Controls; -using Avalonia.Controls.Documents; -using Avalonia.Controls.Presenters; -using Avalonia.Interactivity; using Avalonia.Markup.Xaml; -using Avalonia.VisualTree; +using Avalonia.Win32.WinRT.Composition; namespace Sandbox { @@ -16,12 +10,12 @@ namespace Sandbox public MainWindow() { this.InitializeComponent(); - this.AttachDevTools(); + this.AttachDevTools(); } private void InitializeComponent() { AvaloniaXamlLoader.Load(this); } - } + } } From 6fefe5c8031a2aa24050c477535dff5297ca2f38 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Fri, 29 Jul 2022 17:27:39 +0200 Subject: [PATCH 023/197] Align justified text to the natural start --- .../TextFormatting/InterWordJustification.cs | 17 ++++++++++++----- .../Media/TextFormatting/TextLineImpl.cs | 5 +++++ 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs b/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs index df83ada34a..a49e4ef13b 100644 --- a/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs +++ b/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs @@ -15,6 +15,13 @@ namespace Avalonia.Media.TextFormatting public override void Justify(TextLine textLine) { + var lineImpl = textLine as TextLineImpl; + + if(lineImpl is null) + { + return; + } + var paragraphWidth = Width; if (double.IsInfinity(paragraphWidth)) @@ -22,12 +29,12 @@ namespace Avalonia.Media.TextFormatting return; } - if (textLine.NewLineLength > 0) + if (lineImpl.NewLineLength > 0) { return; } - var textLineBreak = textLine.TextLineBreak; + var textLineBreak = lineImpl.TextLineBreak; if (textLineBreak is not null && textLineBreak.TextEndOfLine is not null) { @@ -39,7 +46,7 @@ namespace Avalonia.Media.TextFormatting var breakOportunities = new Queue(); - foreach (var textRun in textLine.TextRuns) + foreach (var textRun in lineImpl.TextRuns) { var text = textRun.Text; @@ -68,10 +75,10 @@ namespace Avalonia.Media.TextFormatting return; } - var remainingSpace = Math.Max(0, paragraphWidth - textLine.WidthIncludingTrailingWhitespace); + var remainingSpace = Math.Max(0, paragraphWidth - lineImpl.WidthIncludingTrailingWhitespace); var spacing = remainingSpace / breakOportunities.Count; - foreach (var textRun in textLine.TextRuns) + foreach (var textRun in lineImpl.TextRuns) { var text = textRun.Text; diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index 94095cf82b..2c4e553355 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -1536,6 +1536,11 @@ namespace Avalonia.Media.TextFormatting var textAlignment = _paragraphProperties.TextAlignment; var paragraphFlowDirection = _paragraphProperties.FlowDirection; + if(textAlignment == TextAlignment.Justify) + { + textAlignment = TextAlignment.Start; + } + switch (textAlignment) { case TextAlignment.Start: From 2cf660b601c69ad28656dfb36de0be96c5a4783b Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Fri, 29 Jul 2022 19:53:44 +0200 Subject: [PATCH 024/197] Second attempt to fix the font fallback --- src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs index 6611357ee7..42a9e61c36 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs @@ -182,7 +182,7 @@ namespace Avalonia.Media.TextFormatting var currentScript = currentGrapheme.FirstCodepoint.Script; - if (defaultFont != null && defaultFont.TryGetGlyph(currentGrapheme.FirstCodepoint, out _)) + if (!currentGrapheme.FirstCodepoint.IsWhiteSpace && defaultFont != null && defaultFont.TryGetGlyph(currentGrapheme.FirstCodepoint, out _)) { break; } From d5084eaf7582b83630c7a7b1c90b88f17503ce8e Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Fri, 29 Jul 2022 20:14:48 +0200 Subject: [PATCH 025/197] Fix GetCharacterHitFromDistance --- .../Media/TextFormatting/TextLineImpl.cs | 24 +++++++++---------- src/Avalonia.Controls/RichTextBlock.cs | 2 +- src/Avalonia.Controls/TextBox.cs | 4 ++-- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index 2c4e553355..b9147c0f65 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -260,18 +260,18 @@ namespace Avalonia.Media.TextFormatting { characterHit = shapedRun.GlyphRun.GetCharacterHitFromDistance(distance, out _); - //var offset = 0; - - //if (shapedRun.GlyphRun.IsLeftToRight) - //{ - // offset = Math.Max(0, currentPosition - shapedRun.Text.Start); - //} - //else - //{ - // offset = Math.Max(0, currentPosition - shapedRun.Text.Start + shapedRun.Text.Length); - //} - - //characterHit = new CharacterHit(characterHit.FirstCharacterIndex + offset, characterHit.TrailingLength); + var offset = 0; + + if (shapedRun.GlyphRun.IsLeftToRight) + { + offset = Math.Max(0, currentPosition - shapedRun.Text.Start); + } + else + { + offset = Math.Max(0, currentPosition - shapedRun.Text.Start + shapedRun.Text.Length); + } + + characterHit = new CharacterHit(characterHit.FirstCharacterIndex + offset, characterHit.TrailingLength); break; } diff --git a/src/Avalonia.Controls/RichTextBlock.cs b/src/Avalonia.Controls/RichTextBlock.cs index 2f8bed3be7..1f8abbc30d 100644 --- a/src/Avalonia.Controls/RichTextBlock.cs +++ b/src/Avalonia.Controls/RichTextBlock.cs @@ -276,7 +276,7 @@ namespace Avalonia.Controls protected override void SetText(string? text) { - var oldValue = _text ?? Inlines?.Text; + var oldValue = GetText(); AddText(text); diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 1773163acc..4c9e9327d4 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -1241,9 +1241,9 @@ namespace Avalonia.Controls MathUtilities.Clamp(point.X, 0, Math.Max(_presenter.Bounds.Width - 1, 0)), MathUtilities.Clamp(point.Y, 0, Math.Max(_presenter.Bounds.Height - 1, 0))); - var hit = _presenter.TextLayout.HitTestPoint(point); + _presenter.MoveCaretToPoint(point); - var caretIndex = hit.TextPosition; + var caretIndex = _presenter.CaretIndex; var text = Text; From adc81678ca6049def8124965642b3ad90b34e462 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Fri, 29 Jul 2022 21:56:27 +0200 Subject: [PATCH 026/197] Only add offsets for LeftToRight runs --- src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index b9147c0f65..6c6939d2a0 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -266,10 +266,10 @@ namespace Avalonia.Media.TextFormatting { offset = Math.Max(0, currentPosition - shapedRun.Text.Start); } - else - { - offset = Math.Max(0, currentPosition - shapedRun.Text.Start + shapedRun.Text.Length); - } + //else + //{ + // offset = Math.Max(0, currentPosition - shapedRun.Text.Start + shapedRun.Text.Length); + //} characterHit = new CharacterHit(characterHit.FirstCharacterIndex + offset, characterHit.TrailingLength); From c94120c065a69838399f87814b8bbcc0340e5789 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Sat, 30 Jul 2022 09:53:07 +0200 Subject: [PATCH 027/197] Try to fix tests on mac --- .../Media/TextFormatting/TextLayoutTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs index c457a96299..43948e9229 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs @@ -1005,7 +1005,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var distance = textLine.GetDistanceFromCharacterHit(new CharacterHit(cluster)); - Assert.Equal(currentX, distance); + Assert.Equal(currentX, distance, 5); currentX += glyphAdvance; From 195ef79e0a3151007eaa3d3b64a8a79d269ee915 Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 31 Jul 2022 00:19:34 -0400 Subject: [PATCH 028/197] Convert ColorPicker default styles to ControlThemes --- .../Themes/Default/ColorPreviewer.xaml | 23 +- .../Themes/Default/ColorSlider.xaml | 346 +++++++++--------- .../Themes/Default/ColorSpectrum.xaml | 94 ++--- .../Themes/Default/Default.xaml | 76 ++-- 4 files changed, 274 insertions(+), 265 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml index c3bc7df4a4..e067fe4ab5 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml @@ -1,15 +1,14 @@ - + - - - 80 - 40 - + + 80 + 40 - + - + diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSlider.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSlider.xaml index 35cd7a9faa..9aa2dcd9f9 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSlider.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSlider.xaml @@ -1,188 +1,190 @@ - + - + - + - + - - + + + + + + - - - - + + - - + - + diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml index 0e57f6b483..0e137c89c6 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml @@ -1,9 +1,10 @@ - + - - - - + + + - - - + + + - - - - - + + + + + - + - - - + + + - + + + diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Default/Default.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Default/Default.xaml index db1fa3ee4e..b452e34394 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Default/Default.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Default/Default.xaml @@ -3,42 +3,48 @@ xmlns:converters="using:Avalonia.Controls.Converters"> - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + - - - - + - - - + + + + + + + + + + + From bdd451cdf96179dc9c28659ee21db3cec8b8ee0b Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 31 Jul 2022 00:39:45 -0400 Subject: [PATCH 029/197] Adjust ColorPicker tab background corner radius --- .../Themes/Fluent/ColorPicker.xaml | 15 ++++----------- .../Themes/Fluent/ColorView.xaml | 8 ++++++++ 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml index 907b00dfff..50c5c06479 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml @@ -3,9 +3,6 @@ xmlns:controls="using:Avalonia.Controls" x:CompileBindings="True"> - - 5,5,0,0 - @@ -25,7 +22,7 @@ Padding="0,0,10,0" UseLayoutRounding="False"> - @@ -45,8 +42,10 @@ - + - - - - diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml index 993745b1e5..153b4422ea 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml @@ -77,6 +77,8 @@ 17.7761 14 17.5 14H9.94999ZM7.5 16C6.67157 16 6 15.3284 6 14.5C6 13.6716 6.67157 13 7.5 13C8.32843 13 9 13.6716 9 14.5C9 15.3284 8.32843 16 7.5 16Z + + 5,5,0,0 @@ -644,6 +646,12 @@ + + + + From b3bd2e54ecb5fe274164f9ddd96be7482b4a1173 Mon Sep 17 00:00:00 2001 From: Takoooooo Date: Mon, 1 Aug 2022 16:19:41 +0300 Subject: [PATCH 030/197] Use correct ToggleModifier in ListBox multiselection on MacOS. --- src/Avalonia.Controls/ListBox.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/ListBox.cs b/src/Avalonia.Controls/ListBox.cs index 79285bb86b..80b5259a53 100644 --- a/src/Avalonia.Controls/ListBox.cs +++ b/src/Avalonia.Controls/ListBox.cs @@ -6,6 +6,7 @@ using Avalonia.Controls.Primitives; using Avalonia.Controls.Selection; using Avalonia.Controls.Templates; using Avalonia.Input; +using Avalonia.Input.Platform; using Avalonia.VisualTree; namespace Avalonia.Controls @@ -157,7 +158,7 @@ namespace Avalonia.Controls e.Source, true, e.KeyModifiers.HasAllFlags(KeyModifiers.Shift), - e.KeyModifiers.HasAllFlags(KeyModifiers.Control), + e.KeyModifiers.HasAllFlags(AvaloniaLocator.Current.GetRequiredService().CommandModifiers), point.Properties.IsRightButtonPressed); } } From 9f031603424cbcd7b1f4f5bcc5ce38c0c2b93152 Mon Sep 17 00:00:00 2001 From: Tako <53405089+Takoooooo@users.noreply.github.com> Date: Mon, 1 Aug 2022 18:27:15 +0300 Subject: [PATCH 031/197] Update MacOS vmImage because current one is deprecated https://github.com/actions/virtual-environments/issues/5583 --- azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index edf3c3d819..dde1da1446 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -59,7 +59,7 @@ jobs: variables: SolutionDir: '$(Build.SourcesDirectory)' pool: - vmImage: 'macOS-10.15' + vmImage: 'macos-12' steps: - task: UseDotNet@2 displayName: 'Use .NET Core SDK 3.1.418' From 8d3984b929e21e44cda15c420d5818ff925dbd4e Mon Sep 17 00:00:00 2001 From: Tako <53405089+Takoooooo@users.noreply.github.com> Date: Mon, 1 Aug 2022 18:31:34 +0300 Subject: [PATCH 032/197] macos-12 is in beta,use macos-11 instead --- azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index dde1da1446..78012b9041 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -59,7 +59,7 @@ jobs: variables: SolutionDir: '$(Build.SourcesDirectory)' pool: - vmImage: 'macos-12' + vmImage: 'macos-11' steps: - task: UseDotNet@2 displayName: 'Use .NET Core SDK 3.1.418' From 326fffc9fe14aafb571abc794331fe357423df7b Mon Sep 17 00:00:00 2001 From: Tako <53405089+Takoooooo@users.noreply.github.com> Date: Mon, 1 Aug 2022 18:45:11 +0300 Subject: [PATCH 033/197] WIP --- azure-pipelines.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 78012b9041..1d2fe88341 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -59,7 +59,7 @@ jobs: variables: SolutionDir: '$(Build.SourcesDirectory)' pool: - vmImage: 'macos-11' + vmImage: 'macos-12' steps: - task: UseDotNet@2 displayName: 'Use .NET Core SDK 3.1.418' @@ -91,7 +91,7 @@ jobs: inputs: actions: 'build' scheme: '' - sdk: 'macosx11.1' + sdk: 'macosx13.1' configuration: 'Release' xcWorkspacePath: '**/*.xcodeproj/project.xcworkspace' xcodeVersion: '12' # Options: 8, 9, default, specifyPath From a62c7936690d28cb8a42ac2304b5a30de50b1afc Mon Sep 17 00:00:00 2001 From: Tako <53405089+Takoooooo@users.noreply.github.com> Date: Mon, 1 Aug 2022 18:52:59 +0300 Subject: [PATCH 034/197] WIP --- azure-pipelines.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 1d2fe88341..2a6e1b5475 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -59,7 +59,7 @@ jobs: variables: SolutionDir: '$(Build.SourcesDirectory)' pool: - vmImage: 'macos-12' + vmImage: 'macos-11' steps: - task: UseDotNet@2 displayName: 'Use .NET Core SDK 3.1.418' @@ -91,7 +91,7 @@ jobs: inputs: actions: 'build' scheme: '' - sdk: 'macosx13.1' + sdk: 'macosx12.2' configuration: 'Release' xcWorkspacePath: '**/*.xcodeproj/project.xcworkspace' xcodeVersion: '12' # Options: 8, 9, default, specifyPath From ebb8ffed1969c5d9156b6affa381a0ebf8c55e4f Mon Sep 17 00:00:00 2001 From: Tako <53405089+Takoooooo@users.noreply.github.com> Date: Mon, 1 Aug 2022 19:10:25 +0300 Subject: [PATCH 035/197] WIP --- azure-pipelines.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 2a6e1b5475..7e4ba64fb9 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -59,7 +59,7 @@ jobs: variables: SolutionDir: '$(Build.SourcesDirectory)' pool: - vmImage: 'macos-11' + vmImage: 'macos-12' steps: - task: UseDotNet@2 displayName: 'Use .NET Core SDK 3.1.418' @@ -91,10 +91,10 @@ jobs: inputs: actions: 'build' scheme: '' - sdk: 'macosx12.2' + sdk: 'macosx12.3' configuration: 'Release' xcWorkspacePath: '**/*.xcodeproj/project.xcworkspace' - xcodeVersion: '12' # Options: 8, 9, default, specifyPath + xcodeVersion: '13.3' # Options: 8, 9, default, specifyPath args: '-derivedDataPath ./' - task: CmdLine@2 From 3d845206cd8d6c7a4395000b0892015354e0bc99 Mon Sep 17 00:00:00 2001 From: Tako <53405089+Takoooooo@users.noreply.github.com> Date: Mon, 1 Aug 2022 19:31:40 +0300 Subject: [PATCH 036/197] WIP --- azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 7e4ba64fb9..52fc8db53c 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -94,7 +94,7 @@ jobs: sdk: 'macosx12.3' configuration: 'Release' xcWorkspacePath: '**/*.xcodeproj/project.xcworkspace' - xcodeVersion: '13.3' # Options: 8, 9, default, specifyPath + xcodeVersion: '13' # Options: 8, 9, default, specifyPath args: '-derivedDataPath ./' - task: CmdLine@2 From f87052311bd6e3448a198000fd1fe1915c5ab7c6 Mon Sep 17 00:00:00 2001 From: robloo Date: Mon, 1 Aug 2022 22:01:38 -0400 Subject: [PATCH 037/197] Use resources to adjust tab background corner radius in ColorPicker --- .../Themes/Fluent/ColorPicker.xaml | 8 ++++++-- .../Themes/Fluent/ColorView.xaml | 12 +++--------- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml index 50c5c06479..797d6c90d7 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml @@ -45,7 +45,6 @@ + SelectedIndex="{Binding SelectedIndex, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"> + + + 5,5,0,0 + + diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml index 153b4422ea..59cc48975f 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml @@ -77,8 +77,8 @@ 17.7761 14 17.5 14H9.94999ZM7.5 16C6.67157 16 6 15.3284 6 14.5C6 13.6716 6.67157 13 7.5 13C8.32843 13 9 13.6716 9 14.5C9 15.3284 8.32843 16 7.5 16Z - - 5,5,0,0 + + 3 @@ -99,7 +99,7 @@ HorizontalAlignment="Stretch" VerticalAlignment="Top" Background="{DynamicResource SystemControlBackgroundBaseLowBrush}" - CornerRadius="{TemplateBinding CornerRadius}" /> + CornerRadius="{DynamicResource ColorViewTabBackgroundCornerRadius}" /> - - - - From 53e7a741c9a476e83a4a0c89956715bac8cf42cc Mon Sep 17 00:00:00 2001 From: robloo Date: Mon, 1 Aug 2022 22:01:55 -0400 Subject: [PATCH 038/197] Switch back to default flyout placement in ColorPicker --- .../Themes/Fluent/ColorPicker.xaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml index 797d6c90d7..74a1df4991 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml @@ -42,8 +42,7 @@ - + Date: Tue, 2 Aug 2022 10:35:53 +0200 Subject: [PATCH 039/197] Revert "feat(LibInputBackend): minimal linux boot with no input" This reverts commit 94ecd84c8f24d210f537c495e044ded266f1147b. --- .../Input/LibInput/LibInputBackend.cs | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs b/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs index 6e26794b24..15d42789d4 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs @@ -1,15 +1,16 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.IO; using System.Threading; using Avalonia.Input; using Avalonia.Input.Raw; +using Avalonia.Threading; using static Avalonia.LinuxFramebuffer.Input.LibInput.LibInputNativeUnsafeMethods; namespace Avalonia.LinuxFramebuffer.Input.LibInput { public class LibInputBackend : IInputBackend { - private const string LibInput = nameof(Logging.LogArea.X11Platform) + "/" + nameof(LibInput); private IScreenInfoProvider _screen; private IInputRoot _inputRoot; private readonly Queue _inputThreadActions = new Queue(); @@ -28,21 +29,15 @@ namespace Avalonia.LinuxFramebuffer.Input.LibInput new Thread(()=>InputThread(ctx)).Start(); } + + private unsafe void InputThread(IntPtr ctx) { var fd = libinput_get_fd(ctx); var timeval = stackalloc IntPtr[2]; - if (!Directory.Exists("/dev/input")) - { - if (Logging.Logger.IsEnabled(Logging.LogEventLevel.Warning,LibInput)) - { - Logging.Logger.TryGet(Logging.LogEventLevel.Warning, LibInput) - ?.Log(this, "Not connect any input device."); - } - return; - } + foreach (var f in Directory.GetFiles("/dev/input", "event*")) libinput_path_add_device(ctx, f); while (true) From 197f514ae7fbb94849ce048c28434de4f1f2aee0 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Tue, 2 Aug 2022 10:47:11 +0200 Subject: [PATCH 040/197] feat(X11): NullInputBackend --- .../Input/LibInput/LibInputBackend.cs | 6 ------ .../Input/NullInput/NullInputBackend.cs | 17 +++++++++++++++++ 2 files changed, 17 insertions(+), 6 deletions(-) create mode 100644 src/Linux/Avalonia.LinuxFramebuffer/Input/NullInput/NullInputBackend.cs diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs b/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs index 15d42789d4..702ae3f8e5 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs @@ -1,11 +1,9 @@ using System; using System.Collections.Generic; -using System.ComponentModel; using System.IO; using System.Threading; using Avalonia.Input; using Avalonia.Input.Raw; -using Avalonia.Threading; using static Avalonia.LinuxFramebuffer.Input.LibInput.LibInputNativeUnsafeMethods; namespace Avalonia.LinuxFramebuffer.Input.LibInput { @@ -29,8 +27,6 @@ namespace Avalonia.LinuxFramebuffer.Input.LibInput new Thread(()=>InputThread(ctx)).Start(); } - - private unsafe void InputThread(IntPtr ctx) { var fd = libinput_get_fd(ctx); @@ -143,8 +139,6 @@ namespace Avalonia.LinuxFramebuffer.Input.LibInput } } - - public void Initialize(IScreenInfoProvider screen, Action onInput) { diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Input/NullInput/NullInputBackend.cs b/src/Linux/Avalonia.LinuxFramebuffer/Input/NullInput/NullInputBackend.cs new file mode 100644 index 0000000000..551c0995a2 --- /dev/null +++ b/src/Linux/Avalonia.LinuxFramebuffer/Input/NullInput/NullInputBackend.cs @@ -0,0 +1,17 @@ +using System; +using Avalonia.Input; +using Avalonia.Input.Raw; + +namespace Avalonia.LinuxFramebuffer.Input.NullInput; + +internal class NullInputBackend : IInputBackend +{ + + public void Initialize(IScreenInfoProvider screen, Action onInput) + { + } + + public void SetInputRoot(IInputRoot root) + { + } +} From 46185b6a6eb78a887567dc483c5a1351fab61e30 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Tue, 2 Aug 2022 10:59:33 +0200 Subject: [PATCH 041/197] feat: add overload thath accept IInputBackend to Linux AppBuilder --- .../LinuxFramebufferPlatform.cs | 41 +++++++++---------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs index a642766809..c819407cc9 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs @@ -15,7 +15,6 @@ using Avalonia.LinuxFramebuffer.Output; using Avalonia.OpenGL; using Avalonia.Platform; using Avalonia.Rendering; -using Avalonia.Threading; using JetBrains.Annotations; namespace Avalonia.LinuxFramebuffer @@ -37,9 +36,9 @@ namespace Avalonia.LinuxFramebuffer Threading = new InternalPlatformThreadingInterface(); if (_fb is IGlOutputBackend gl) AvaloniaLocator.CurrentMutable.Bind().ToConstant(gl.PlatformOpenGlInterface); - + var opts = AvaloniaLocator.Current.GetService() ?? new LinuxFramebufferPlatformOptions(); - + AvaloniaLocator.CurrentMutable .Bind().ToConstant(Threading) .Bind().ToConstant(new DefaultRenderTimer(opts.Fps)) @@ -50,12 +49,12 @@ namespace Avalonia.LinuxFramebuffer .Bind().ToSingleton(); } - - internal static LinuxFramebufferLifetime Initialize(T builder, IOutputBackend outputBackend) where T : AppBuilderBase, new() + + internal static LinuxFramebufferLifetime Initialize(T builder, IOutputBackend outputBackend, IInputBackend inputBackend) where T : AppBuilderBase, new() { var platform = new LinuxFramebufferPlatform(outputBackend); builder.UseSkia().UseWindowingSubsystem(platform.Initialize, "fbdev"); - return new LinuxFramebufferLifetime(platform._fb); + return new LinuxFramebufferLifetime(platform._fb, inputBackend); } } @@ -71,13 +70,13 @@ namespace Avalonia.LinuxFramebuffer { _fb = fb; } - + public LinuxFramebufferLifetime(IOutputBackend fb, IInputBackend input) { _fb = fb; _inputBackend = input; } - + public Control MainView { get => (Control)_topLevel?.Content; @@ -117,7 +116,7 @@ namespace Avalonia.LinuxFramebuffer { Startup?.Invoke(this, new ControlledApplicationLifetimeStartupEventArgs(args)); } - + public void Shutdown(int exitCode) { ExitCode = exitCode; @@ -131,22 +130,22 @@ namespace Avalonia.LinuxFramebuffer public static class LinuxFramebufferPlatformExtensions { - public static int StartLinuxFbDev(this T builder, string[] args, string fbdev = null, double scaling = 1) + public static int StartLinuxFbDev(this T builder, string[] args, string fbdev = null, double scaling = 1, IInputBackend inputBackend = default) where T : AppBuilderBase, new() => - StartLinuxDirect(builder, args, new FbdevOutput(fileName: fbdev, format: null) { Scaling = scaling }); - public static int StartLinuxFbDev(this T builder, string[] args, string fbdev, PixelFormat? format, double scaling) + StartLinuxDirect(builder, args, new FbdevOutput(fileName: fbdev, format: null) { Scaling = scaling }, inputBackend); + public static int StartLinuxFbDev(this T builder, string[] args, string fbdev, PixelFormat? format, double scaling, IInputBackend inputBackend = default) where T : AppBuilderBase, new() => - StartLinuxDirect(builder, args, new FbdevOutput(fileName: fbdev, format: format) { Scaling = scaling }); - - public static int StartLinuxDrm(this T builder, string[] args, string card = null, double scaling = 1) - where T : AppBuilderBase, new() => StartLinuxDirect(builder, args, new DrmOutput(card) {Scaling = scaling}); - public static int StartLinuxDrm(this T builder, string[] args, string card = null, bool connectorsForceProbe = false, [CanBeNull] DrmOutputOptions options = null) - where T : AppBuilderBase, new() => StartLinuxDirect(builder, args, new DrmOutput(card, connectorsForceProbe, options)); - - public static int StartLinuxDirect(this T builder, string[] args, IOutputBackend backend) + StartLinuxDirect(builder, args, new FbdevOutput(fileName: fbdev, format: format) { Scaling = scaling }, inputBackend); + + public static int StartLinuxDrm(this T builder, string[] args, string card = null, double scaling = 1, IInputBackend inputBackend = default) + where T : AppBuilderBase, new() => StartLinuxDirect(builder, args, new DrmOutput(card) { Scaling = scaling }, inputBackend); + public static int StartLinuxDrm(this T builder, string[] args, string card = null, bool connectorsForceProbe = false, [CanBeNull] DrmOutputOptions options = null, IInputBackend inputBackend = default) + where T : AppBuilderBase, new() => StartLinuxDirect(builder, args, new DrmOutput(card, connectorsForceProbe, options), inputBackend); + + public static int StartLinuxDirect(this T builder, string[] args, IOutputBackend outputBackend, IInputBackend inputBackend = default) where T : AppBuilderBase, new() { - var lifetime = LinuxFramebufferPlatform.Initialize(builder, backend); + var lifetime = LinuxFramebufferPlatform.Initialize(builder, outputBackend, inputBackend); builder.SetupWithLifetime(lifetime); lifetime.Start(args); builder.Instance.Run(lifetime.Token); From b38a50009ec409a788c38a0220694918258ca0dc Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Tue, 2 Aug 2022 11:48:20 +0200 Subject: [PATCH 042/197] fix: XML Comment --- src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs | 2 +- .../Rendering/Composition/Animations/CompositionAnimation.cs | 4 ++-- .../Rendering/Composition/Animations/ExpressionAnimation.cs | 2 +- .../Rendering/Composition/Animations/KeyFrameAnimation.cs | 4 ++-- src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs | 2 +- src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs | 2 +- src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs | 2 +- src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs | 2 +- src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs | 2 +- src/Avalonia.Base/Utilities/MathUtilities.cs | 2 +- 10 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs index 56a90f31ea..ab17263806 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs @@ -224,7 +224,7 @@ namespace Avalonia.Media.TextFormatting.Unicode } /// - /// Returns if is between + /// Returns if is between /// and , inclusive. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs b/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs index 19d316eb85..23f2aeaf90 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs @@ -16,10 +16,10 @@ namespace Avalonia.Rendering.Composition.Animations /// This is the base class for ExpressionAnimation and KeyFrameAnimation. /// /// - /// Use the method to start the animation. + /// Use the method to start the animation. /// Value parameters (as opposed to reference parameters which are set using ) /// are copied and "embedded" into an expression at the time CompositionObject.StartAnimation is called. - /// Changing the value of the variable after is called will not affect + /// Changing the value of the variable after is called will not affect /// the value of the ExpressionAnimation. /// See the remarks section of ExpressionAnimation for additional information. /// diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimation.cs b/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimation.cs index 577910d975..ec2972044e 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimation.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimation.cs @@ -16,7 +16,7 @@ namespace Avalonia.Rendering.Composition.Animations /// This contrasts s, which use an interpolator to define how the animating /// property changes over time. The mathematical equation can be defined using references to properties /// of Composition objects, mathematical functions and operators and Input. - /// Use the method to start the animation. + /// Use the method to start the animation. /// public class ExpressionAnimation : CompositionAnimation { diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimation.cs b/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimation.cs index 4692fde5e3..d21a4d06e3 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimation.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimation.cs @@ -24,9 +24,9 @@ namespace Avalonia.Rendering.Composition.Animations /// The delay behavior of the key frame animation. /// public AnimationDelayBehavior DelayBehavior { get; set; } - + /// - /// Delay before the animation starts after is called. + /// Delay before the animation starts after is called. /// public System.TimeSpan DelayTime { get; set; } diff --git a/src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs index 4b43f93aee..e1d43b4268 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs @@ -19,7 +19,7 @@ namespace Avalonia.Rendering.SceneGraph /// The fill brush. /// The stroke pen. /// The geometry. - /// Child scenes for drawing visual brushes. + /// Auxiliary data required to draw the brush. public GeometryNode(Matrix transform, IBrush? brush, IPen? pen, diff --git a/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs index 1f58111ecf..b23b0684d3 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs @@ -19,7 +19,7 @@ namespace Avalonia.Rendering.SceneGraph /// The transform. /// The foreground brush. /// The glyph run to draw. - /// Child scenes for drawing visual brushes. + /// Auxiliary data required to draw the brush. public GlyphRunNode( Matrix transform, IBrush foreground, diff --git a/src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs index ee5ec0a5fc..b027e43378 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs @@ -19,7 +19,7 @@ namespace Avalonia.Rendering.SceneGraph /// The stroke pen. /// The start point of the line. /// The end point of the line. - /// Child scenes for drawing visual brushes. + /// Auxiliary data required to draw the brush. public LineNode( Matrix transform, IPen pen, diff --git a/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs index 549c1fd7de..5fd200ddff 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs @@ -17,7 +17,7 @@ namespace Avalonia.Rendering.SceneGraph /// /// The opacity mask to push. /// The bounds of the mask. - /// Child scenes for drawing visual brushes. + /// Auxiliary data required to draw the brush. public OpacityMaskNode(IBrush mask, Rect bounds, IDisposable? aux = null) : base(Rect.Empty, Matrix.Identity, aux) { diff --git a/src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs index a9d1bf96e5..f2ffd7411c 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs @@ -20,7 +20,7 @@ namespace Avalonia.Rendering.SceneGraph /// The stroke pen. /// The rectangle to draw. /// The box shadow parameters - /// Child scenes for drawing visual brushes. + /// Auxiliary data required to draw the brush. public RectangleNode( Matrix transform, IBrush? brush, diff --git a/src/Avalonia.Base/Utilities/MathUtilities.cs b/src/Avalonia.Base/Utilities/MathUtilities.cs index 3d5be806e1..d381979c1e 100644 --- a/src/Avalonia.Base/Utilities/MathUtilities.cs +++ b/src/Avalonia.Base/Utilities/MathUtilities.cs @@ -255,7 +255,7 @@ namespace Avalonia.Utilities /// /// Clamps a value between a minimum and maximum value. /// - /// The value. + /// The value. /// The minimum value. /// The maximum value. /// The clamped value. From 89c0ef56371bddf68590f8f21efe61672d1084fc Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Tue, 2 Aug 2022 12:02:04 +0200 Subject: [PATCH 043/197] refactoring: removed some unused namespace --- .../Rendering/Composition/Animations/CompositionAnimation.cs | 3 --- src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs | 2 -- src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs | 4 ---- src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs | 2 -- src/Skia/Avalonia.Skia/ImmutableBitmap.cs | 3 --- src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs | 2 -- 6 files changed, 16 deletions(-) diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs b/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs index 19d316eb85..5499e1119a 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs @@ -1,12 +1,9 @@ // ReSharper disable InconsistentNaming // ReSharper disable CheckNamespace -using System; -using System.Collections.Generic; using System.Numerics; using Avalonia.Rendering.Composition.Expressions; using Avalonia.Rendering.Composition.Server; -using Avalonia.Rendering.Composition.Transport; // Special license applies License.md diff --git a/src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs index 4b43f93aee..747e96f130 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs @@ -1,9 +1,7 @@ using System; -using System.Collections.Generic; using Avalonia.Media; using Avalonia.Media.Immutable; using Avalonia.Platform; -using Avalonia.VisualTree; namespace Avalonia.Rendering.SceneGraph { diff --git a/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs index 1f58111ecf..ed45e54f57 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs @@ -1,10 +1,6 @@ using System; -using System.Collections.Generic; - using Avalonia.Media; -using Avalonia.Media.Immutable; using Avalonia.Platform; -using Avalonia.VisualTree; namespace Avalonia.Rendering.SceneGraph { diff --git a/src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs index ee5ec0a5fc..973b28dead 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs @@ -1,9 +1,7 @@ using System; -using System.Collections.Generic; using Avalonia.Media; using Avalonia.Media.Immutable; using Avalonia.Platform; -using Avalonia.VisualTree; namespace Avalonia.Rendering.SceneGraph { diff --git a/src/Skia/Avalonia.Skia/ImmutableBitmap.cs b/src/Skia/Avalonia.Skia/ImmutableBitmap.cs index a80f406989..6400d67fde 100644 --- a/src/Skia/Avalonia.Skia/ImmutableBitmap.cs +++ b/src/Skia/Avalonia.Skia/ImmutableBitmap.cs @@ -1,11 +1,8 @@ using System; using System.IO; -using System.Runtime.CompilerServices; -using System.Security.Cryptography; using Avalonia.Media.Imaging; using Avalonia.Platform; using Avalonia.Skia.Helpers; -using Avalonia.Media.Imaging; using SkiaSharp; namespace Avalonia.Skia diff --git a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs index 7f1af46e97..81fa8c4bce 100644 --- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs +++ b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using Avalonia.Controls; @@ -9,7 +8,6 @@ using Avalonia.Direct2D1.Media.Imaging; using Avalonia.Media; using Avalonia.Media.Imaging; using Avalonia.Platform; -using Avalonia.Media.Imaging; using SharpDX.DirectWrite; using GlyphRun = Avalonia.Media.GlyphRun; using TextAlignment = Avalonia.Media.TextAlignment; From bf9a0d8656d787fc67a7ba8aea3f9c6097624f0c Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Tue, 2 Aug 2022 13:17:01 +0200 Subject: [PATCH 044/197] Use a different arabic font for tests --- .../Assets/NotoKufiArabic-Regular.ttf | Bin 122736 -> 0 bytes .../Assets/NotoSansArabic-Regular.ttf | Bin 0 -> 177004 bytes .../Media/CustomFontManagerImpl.cs | 8 +++++++- .../Media/TextFormatting/TextLayoutTests.cs | 2 +- 4 files changed, 8 insertions(+), 2 deletions(-) delete mode 100644 tests/Avalonia.RenderTests/Assets/NotoKufiArabic-Regular.ttf create mode 100644 tests/Avalonia.RenderTests/Assets/NotoSansArabic-Regular.ttf diff --git a/tests/Avalonia.RenderTests/Assets/NotoKufiArabic-Regular.ttf b/tests/Avalonia.RenderTests/Assets/NotoKufiArabic-Regular.ttf deleted file mode 100644 index 6d2ad86f947a530de5dec09b9a230948d0f512c3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 122736 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 V75=m6xC;lbHm=3_}(L8AakEU|`p@iY%Zk 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 z�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 literal 0 HcmV?d00001 diff --git a/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs b/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs index 13cc14b03e..7d0cf05b0b 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs @@ -16,7 +16,7 @@ namespace Avalonia.Skia.UnitTests.Media private readonly Typeface _defaultTypeface = new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Mono"); private readonly Typeface _arabicTypeface = - new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Kufi Arabic"); + new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Sans Arabic"); private readonly Typeface _italicTypeface = new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Sans", FontStyle.Italic); private readonly Typeface _emojiTypeface = @@ -82,6 +82,12 @@ namespace Avalonia.Skia.UnitTests.Media skTypeface = typefaceCollection.Get(typeface); break; } + case "Noto Sans Arabic": + { + var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(_arabicTypeface.FontFamily); + skTypeface = typefaceCollection.Get(typeface); + break; + } case FontFamily.DefaultFontFamilyName: case "Noto Mono": { diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs index 43948e9229..c457a96299 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs @@ -1005,7 +1005,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var distance = textLine.GetDistanceFromCharacterHit(new CharacterHit(cluster)); - Assert.Equal(currentX, distance, 5); + Assert.Equal(currentX, distance); currentX += glyphAdvance; From 4f79ac8a1c1cdecc71e89e4d94d98082ec54c8ea Mon Sep 17 00:00:00 2001 From: Benedikt Date: Tue, 2 Aug 2022 13:25:57 +0200 Subject: [PATCH 045/197] Fix precession errors --- src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs | 6 +++--- .../Media/TextFormatting/TextLayoutTests.cs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index 6c6939d2a0..fa1ab6fd29 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -357,7 +357,7 @@ namespace Avalonia.Media.TextFormatting if (currentPosition + currentRun.TextSourceLength >= characterIndex && TryGetDistanceFromCharacterHit(currentRun, characterHit, currentPosition, remainingLength, flowDirection, out var distance, out _)) { - return currentDistance + distance; + return Math.Max(0, currentDistance + distance); } //No hit hit found so we add the full width @@ -382,7 +382,7 @@ namespace Avalonia.Media.TextFormatting distance = currentGlyphRun.Size.Width - distance; } - return currentDistance - distance; + return Math.Max(0, currentDistance - distance); } //No hit hit found so we add the full width @@ -392,7 +392,7 @@ namespace Avalonia.Media.TextFormatting } } - return currentDistance; + return Math.Max(0, currentDistance); } private static bool TryGetDistanceFromCharacterHit( diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs index c457a96299..43948e9229 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs @@ -1005,7 +1005,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var distance = textLine.GetDistanceFromCharacterHit(new CharacterHit(cluster)); - Assert.Equal(currentX, distance); + Assert.Equal(currentX, distance, 5); currentX += glyphAdvance; From fe79cee6726a76e875a3184218106d7b3b6dc6de Mon Sep 17 00:00:00 2001 From: Takoooooo Date: Tue, 2 Aug 2022 16:11:08 +0300 Subject: [PATCH 046/197] Fix Tests. --- .../ListBoxTests_Single.cs | 133 +++-- .../Primitives/SelectingItemsControlTests.cs | 89 +-- .../SelectingItemsControlTests_Multiple.cs | 542 ++++++++++-------- 3 files changed, 422 insertions(+), 342 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs index 8f795104bf..bf516748cc 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs @@ -5,10 +5,12 @@ using Avalonia.Controls.Presenters; using Avalonia.Controls.Templates; using Avalonia.Data; using Avalonia.Input; +using Avalonia.Input.Platform; using Avalonia.LogicalTree; using Avalonia.Styling; using Avalonia.UnitTests; using Avalonia.VisualTree; +using Moq; using Xunit; namespace Avalonia.Controls.UnitTests @@ -60,104 +62,123 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Clicking_Item_Should_Select_It() { - var target = new ListBox + using (UnitTestApplication.Start()) { - Template = new FuncControlTemplate(CreateListBoxTemplate), - Items = new[] { "Foo", "Bar", "Baz " }, - }; - - ApplyTemplate(target); - _mouse.Click(target.Presenter.Panel.Children[0]); - - Assert.Equal(0, target.SelectedIndex); + var target = new ListBox + { + Template = new FuncControlTemplate(CreateListBoxTemplate), + Items = new[] { "Foo", "Bar", "Baz " }, + }; + AvaloniaLocator.CurrentMutable.Bind().ToConstant(new Mock().Object); + ApplyTemplate(target); + _mouse.Click(target.Presenter.Panel.Children[0]); + + Assert.Equal(0, target.SelectedIndex); + } } [Fact] public void Clicking_Selected_Item_Should_Not_Deselect_It() { - var target = new ListBox + using (UnitTestApplication.Start()) { - Template = new FuncControlTemplate(CreateListBoxTemplate), - Items = new[] { "Foo", "Bar", "Baz " }, - }; - - ApplyTemplate(target); - target.SelectedIndex = 0; + var target = new ListBox + { + Template = new FuncControlTemplate(CreateListBoxTemplate), + Items = new[] { "Foo", "Bar", "Baz " }, + }; + AvaloniaLocator.CurrentMutable.Bind().ToConstant(new Mock().Object); + ApplyTemplate(target); + target.SelectedIndex = 0; - _mouse.Click(target.Presenter.Panel.Children[0]); + _mouse.Click(target.Presenter.Panel.Children[0]); - Assert.Equal(0, target.SelectedIndex); + Assert.Equal(0, target.SelectedIndex); + } } [Fact] public void Clicking_Item_Should_Select_It_When_SelectionMode_Toggle() { - var target = new ListBox + using (UnitTestApplication.Start()) { - Template = new FuncControlTemplate(CreateListBoxTemplate), - Items = new[] { "Foo", "Bar", "Baz " }, - SelectionMode = SelectionMode.Single | SelectionMode.Toggle, - }; - - ApplyTemplate(target); + var target = new ListBox + { + Template = new FuncControlTemplate(CreateListBoxTemplate), + Items = new[] { "Foo", "Bar", "Baz " }, + SelectionMode = SelectionMode.Single | SelectionMode.Toggle, + }; + AvaloniaLocator.CurrentMutable.Bind().ToConstant(new Mock().Object); + ApplyTemplate(target); - _mouse.Click(target.Presenter.Panel.Children[0]); + _mouse.Click(target.Presenter.Panel.Children[0]); - Assert.Equal(0, target.SelectedIndex); + Assert.Equal(0, target.SelectedIndex); + } } [Fact] public void Clicking_Selected_Item_Should_Deselect_It_When_SelectionMode_Toggle() { - var target = new ListBox + using (UnitTestApplication.Start()) { - Template = new FuncControlTemplate(CreateListBoxTemplate), - Items = new[] { "Foo", "Bar", "Baz " }, - SelectionMode = SelectionMode.Toggle, - }; + var target = new ListBox + { + Template = new FuncControlTemplate(CreateListBoxTemplate), + Items = new[] { "Foo", "Bar", "Baz " }, + SelectionMode = SelectionMode.Toggle, + }; - ApplyTemplate(target); - target.SelectedIndex = 0; + AvaloniaLocator.CurrentMutable.Bind().ToConstant(new Mock().Object); + ApplyTemplate(target); + target.SelectedIndex = 0; - _mouse.Click(target.Presenter.Panel.Children[0]); + _mouse.Click(target.Presenter.Panel.Children[0]); - Assert.Equal(-1, target.SelectedIndex); + Assert.Equal(-1, target.SelectedIndex); + } } [Fact] public void Clicking_Selected_Item_Should_Not_Deselect_It_When_SelectionMode_ToggleAlwaysSelected() { - var target = new ListBox + using (UnitTestApplication.Start()) { - Template = new FuncControlTemplate(CreateListBoxTemplate), - Items = new[] { "Foo", "Bar", "Baz " }, - SelectionMode = SelectionMode.Toggle | SelectionMode.AlwaysSelected, - }; - - ApplyTemplate(target); - target.SelectedIndex = 0; + var target = new ListBox + { + Template = new FuncControlTemplate(CreateListBoxTemplate), + Items = new[] { "Foo", "Bar", "Baz " }, + SelectionMode = SelectionMode.Toggle | SelectionMode.AlwaysSelected, + }; + AvaloniaLocator.CurrentMutable.Bind().ToConstant(new Mock().Object); + ApplyTemplate(target); + target.SelectedIndex = 0; - _mouse.Click(target.Presenter.Panel.Children[0]); + _mouse.Click(target.Presenter.Panel.Children[0]); - Assert.Equal(0, target.SelectedIndex); + Assert.Equal(0, target.SelectedIndex); + } } [Fact] public void Clicking_Another_Item_Should_Select_It_When_SelectionMode_Toggle() { - var target = new ListBox + using (UnitTestApplication.Start()) { - Template = new FuncControlTemplate(CreateListBoxTemplate), - Items = new[] { "Foo", "Bar", "Baz " }, - SelectionMode = SelectionMode.Single | SelectionMode.Toggle, - }; - - ApplyTemplate(target); - target.SelectedIndex = 1; + var target = new ListBox + { + Template = new FuncControlTemplate(CreateListBoxTemplate), + Items = new[] { "Foo", "Bar", "Baz " }, + SelectionMode = SelectionMode.Single | SelectionMode.Toggle, + }; + AvaloniaLocator.CurrentMutable.Bind().ToConstant(new Mock().Object); + ApplyTemplate(target); + target.SelectedIndex = 1; - _mouse.Click(target.Presenter.Panel.Children[0]); + _mouse.Click(target.Presenter.Panel.Children[0]); - Assert.Equal(0, target.SelectedIndex); + Assert.Equal(0, target.SelectedIndex); + } } [Fact] diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs index 4b6b6a1182..330cbfd7b9 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs @@ -14,6 +14,7 @@ using Avalonia.Controls.Selection; using Avalonia.Controls.Templates; using Avalonia.Data; using Avalonia.Input; +using Avalonia.Input.Platform; using Avalonia.Interactivity; using Avalonia.Markup.Data; using Avalonia.Platform; @@ -1115,42 +1116,48 @@ namespace Avalonia.Controls.UnitTests.Primitives [Fact] public void Setting_SelectedItem_With_Pointer_Should_Set_TabOnceActiveElement() { - var target = new ListBox + using (UnitTestApplication.Start()) { - Template = Template(), - Items = new[] { "Foo", "Bar", "Baz " }, - }; - - Prepare(target); - _helper.Down((Interactive)target.Presenter.Panel.Children[1]); + var target = new ListBox + { + Template = Template(), + Items = new[] { "Foo", "Bar", "Baz " }, + }; + AvaloniaLocator.CurrentMutable.Bind().ToConstant(new Mock().Object); + Prepare(target); + _helper.Down((Interactive)target.Presenter.Panel.Children[1]); - var panel = target.Presenter.Panel; + var panel = target.Presenter.Panel; - Assert.Equal( - KeyboardNavigation.GetTabOnceActiveElement((InputElement)panel), - panel.Children[1]); + Assert.Equal( + KeyboardNavigation.GetTabOnceActiveElement((InputElement)panel), + panel.Children[1]); + } } [Fact] public void Removing_SelectedItem_Should_Clear_TabOnceActiveElement() { - var items = new ObservableCollection(new[] { "Foo", "Bar", "Baz " }); - - var target = new ListBox + using (UnitTestApplication.Start()) { - Template = Template(), - Items = items, - }; + var items = new ObservableCollection(new[] { "Foo", "Bar", "Baz " }); - Prepare(target); + var target = new ListBox + { + Template = Template(), + Items = items, + }; + AvaloniaLocator.CurrentMutable.Bind().ToConstant(new Mock().Object); + Prepare(target); - _helper.Down(target.Presenter.Panel.Children[1]); + _helper.Down(target.Presenter.Panel.Children[1]); - items.RemoveAt(1); + items.RemoveAt(1); - var panel = target.Presenter.Panel; + var panel = target.Presenter.Panel; - Assert.Null(KeyboardNavigation.GetTabOnceActiveElement((InputElement)panel)); + Assert.Null(KeyboardNavigation.GetTabOnceActiveElement((InputElement)panel)); + } } [Fact] @@ -1230,31 +1237,37 @@ namespace Avalonia.Controls.UnitTests.Primitives [Fact] public void Should_Select_Correct_Item_When_Duplicate_Items_Are_Present() { - var target = new ListBox + using (UnitTestApplication.Start()) { - Template = Template(), - Items = new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" }, - }; - - Prepare(target); - _helper.Down((Interactive)target.Presenter.Panel.Children[3]); + var target = new ListBox + { + Template = Template(), + Items = new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" }, + }; + AvaloniaLocator.CurrentMutable.Bind().ToConstant(new Mock().Object); + Prepare(target); + _helper.Down((Interactive)target.Presenter.Panel.Children[3]); - Assert.Equal(3, target.SelectedIndex); + Assert.Equal(3, target.SelectedIndex); + } } [Fact] public void Should_Apply_Selected_Pseudoclass_To_Correct_Item_When_Duplicate_Items_Are_Present() { - var target = new ListBox + using (UnitTestApplication.Start()) { - Template = Template(), - Items = new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" }, - }; - - Prepare(target); - _helper.Down((Interactive)target.Presenter.Panel.Children[3]); + var target = new ListBox + { + Template = Template(), + Items = new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" }, + }; + AvaloniaLocator.CurrentMutable.Bind().ToConstant(new Mock().Object); + Prepare(target); + _helper.Down((Interactive)target.Presenter.Panel.Children[3]); - Assert.Equal(new[] { ":pressed", ":selected" }, target.Presenter.Panel.Children[3].Classes); + Assert.Equal(new[] { ":pressed", ":selected" }, target.Presenter.Panel.Children[3].Classes); + } } [Fact] diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs index 6b26d76371..5d2f4e2a64 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs @@ -10,8 +10,10 @@ using Avalonia.Controls.Selection; using Avalonia.Controls.Templates; using Avalonia.Data; using Avalonia.Input; +using Avalonia.Input.Platform; using Avalonia.Interactivity; using Avalonia.UnitTests; +using Moq; using Xunit; namespace Avalonia.Controls.UnitTests.Primitives @@ -701,261 +703,290 @@ namespace Avalonia.Controls.UnitTests.Primitives [Fact] public void Shift_Selecting_From_No_Selection_Selects_From_Start() { - var target = new ListBox + using (UnitTestApplication.Start()) { - Template = Template(), - Items = new[] { "Foo", "Bar", "Baz" }, - SelectionMode = SelectionMode.Multiple, - }; - - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); - _helper.Click((Interactive)target.Presenter.Panel.Children[2], modifiers: KeyModifiers.Shift); - - var panel = target.Presenter.Panel; - - Assert.Equal(new[] { "Foo", "Bar", "Baz" }, target.SelectedItems); - Assert.Equal(new[] { 0, 1, 2 }, SelectedContainers(target)); + var target = new ListBox + { + Template = Template(), + Items = new[] { "Foo", "Bar", "Baz" }, + SelectionMode = SelectionMode.Multiple, + }; + AvaloniaLocator.CurrentMutable.Bind().ToConstant(new Mock().Object); + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + _helper.Click((Interactive)target.Presenter.Panel.Children[2], modifiers: KeyModifiers.Shift); + + var panel = target.Presenter.Panel; + + Assert.Equal(new[] { "Foo", "Bar", "Baz" }, target.SelectedItems); + Assert.Equal(new[] { 0, 1, 2 }, SelectedContainers(target)); + } } [Fact] public void Ctrl_Selecting_Raises_SelectionChanged_Events() { - var target = new ListBox + using (UnitTestApplication.Start()) { - Template = Template(), - Items = new[] { "Foo", "Bar", "Baz", "Qux" }, - SelectionMode = SelectionMode.Multiple, - }; - - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); + var target = new ListBox + { + Template = Template(), + Items = new[] { "Foo", "Bar", "Baz", "Qux" }, + SelectionMode = SelectionMode.Multiple, + }; + AvaloniaLocator.CurrentMutable.Bind().ToConstant(new Mock().Object); + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); - SelectionChangedEventArgs receivedArgs = null; + SelectionChangedEventArgs receivedArgs = null; - target.SelectionChanged += (_, args) => receivedArgs = args; + target.SelectionChanged += (_, args) => receivedArgs = args; - void VerifyAdded(string selection) - { - Assert.NotNull(receivedArgs); - Assert.Equal(new[] { selection }, receivedArgs.AddedItems); - Assert.Empty(receivedArgs.RemovedItems); - } + void VerifyAdded(string selection) + { + Assert.NotNull(receivedArgs); + Assert.Equal(new[] { selection }, receivedArgs.AddedItems); + Assert.Empty(receivedArgs.RemovedItems); + } - void VerifyRemoved(string selection) - { - Assert.NotNull(receivedArgs); - Assert.Equal(new[] { selection }, receivedArgs.RemovedItems); - Assert.Empty(receivedArgs.AddedItems); - } + void VerifyRemoved(string selection) + { + Assert.NotNull(receivedArgs); + Assert.Equal(new[] { selection }, receivedArgs.RemovedItems); + Assert.Empty(receivedArgs.AddedItems); + } - _helper.Click((Interactive)target.Presenter.Panel.Children[1]); + _helper.Click((Interactive)target.Presenter.Panel.Children[1]); - VerifyAdded("Bar"); + VerifyAdded("Bar"); - receivedArgs = null; - _helper.Click((Interactive)target.Presenter.Panel.Children[2], modifiers: KeyModifiers.Control); + receivedArgs = null; + _helper.Click((Interactive)target.Presenter.Panel.Children[2], modifiers: KeyModifiers.Control); - VerifyAdded("Baz"); + VerifyAdded("Baz"); - receivedArgs = null; - _helper.Click((Interactive)target.Presenter.Panel.Children[3], modifiers: KeyModifiers.Control); + receivedArgs = null; + _helper.Click((Interactive)target.Presenter.Panel.Children[3], modifiers: KeyModifiers.Control); - VerifyAdded("Qux"); + VerifyAdded("Qux"); - receivedArgs = null; - _helper.Click((Interactive)target.Presenter.Panel.Children[1], modifiers: KeyModifiers.Control); + receivedArgs = null; + _helper.Click((Interactive)target.Presenter.Panel.Children[1], modifiers: KeyModifiers.Control); - VerifyRemoved("Bar"); + VerifyRemoved("Bar"); + } } [Fact] public void Ctrl_Selecting_SelectedItem_With_Multiple_Selection_Active_Sets_SelectedItem_To_Next_Selection() { - var target = new ListBox + using (UnitTestApplication.Start()) { - Template = Template(), - Items = new[] { "Foo", "Bar", "Baz", "Qux" }, - SelectionMode = SelectionMode.Multiple, - }; - - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); - _helper.Click((Interactive)target.Presenter.Panel.Children[1]); - _helper.Click((Interactive)target.Presenter.Panel.Children[2], modifiers: KeyModifiers.Control); - _helper.Click((Interactive)target.Presenter.Panel.Children[3], modifiers: KeyModifiers.Control); - - Assert.Equal(1, target.SelectedIndex); - Assert.Equal("Bar", target.SelectedItem); - Assert.Equal(new[] { "Bar", "Baz", "Qux" }, target.SelectedItems); - - _helper.Click((Interactive)target.Presenter.Panel.Children[1], modifiers: KeyModifiers.Control); - - Assert.Equal(2, target.SelectedIndex); - Assert.Equal("Baz", target.SelectedItem); - Assert.Equal(new[] { "Baz", "Qux" }, target.SelectedItems); + var target = new ListBox + { + Template = Template(), + Items = new[] { "Foo", "Bar", "Baz", "Qux" }, + SelectionMode = SelectionMode.Multiple, + }; + AvaloniaLocator.CurrentMutable.Bind().ToConstant(new Mock().Object); + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + _helper.Click((Interactive)target.Presenter.Panel.Children[1]); + _helper.Click((Interactive)target.Presenter.Panel.Children[2], modifiers: KeyModifiers.Control); + _helper.Click((Interactive)target.Presenter.Panel.Children[3], modifiers: KeyModifiers.Control); + + Assert.Equal(1, target.SelectedIndex); + Assert.Equal("Bar", target.SelectedItem); + Assert.Equal(new[] { "Bar", "Baz", "Qux" }, target.SelectedItems); + + _helper.Click((Interactive)target.Presenter.Panel.Children[1], modifiers: KeyModifiers.Control); + + Assert.Equal(2, target.SelectedIndex); + Assert.Equal("Baz", target.SelectedItem); + Assert.Equal(new[] { "Baz", "Qux" }, target.SelectedItems); + } } [Fact] public void Ctrl_Selecting_Non_SelectedItem_With_Multiple_Selection_Active_Leaves_SelectedItem_The_Same() { - var target = new ListBox + using (UnitTestApplication.Start()) { - Template = Template(), - Items = new[] { "Foo", "Bar", "Baz" }, - SelectionMode = SelectionMode.Multiple, - }; + var target = new ListBox + { + Template = Template(), + Items = new[] { "Foo", "Bar", "Baz" }, + SelectionMode = SelectionMode.Multiple, + }; + AvaloniaLocator.CurrentMutable.Bind().ToConstant(new Mock().Object); - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); - _helper.Click((Interactive)target.Presenter.Panel.Children[1]); - _helper.Click((Interactive)target.Presenter.Panel.Children[2], modifiers: KeyModifiers.Control); + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + _helper.Click((Interactive)target.Presenter.Panel.Children[1]); + _helper.Click((Interactive)target.Presenter.Panel.Children[2], modifiers: KeyModifiers.Control); - Assert.Equal(1, target.SelectedIndex); - Assert.Equal("Bar", target.SelectedItem); + Assert.Equal(1, target.SelectedIndex); + Assert.Equal("Bar", target.SelectedItem); - _helper.Click((Interactive)target.Presenter.Panel.Children[2], modifiers: KeyModifiers.Control); + _helper.Click((Interactive)target.Presenter.Panel.Children[2], modifiers: KeyModifiers.Control); - Assert.Equal(1, target.SelectedIndex); - Assert.Equal("Bar", target.SelectedItem); + Assert.Equal(1, target.SelectedIndex); + Assert.Equal("Bar", target.SelectedItem); + } } [Fact] public void Should_Ctrl_Select_Correct_Item_When_Duplicate_Items_Are_Present() { - var target = new ListBox + using (UnitTestApplication.Start()) { - Template = Template(), - Items = new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" }, - SelectionMode = SelectionMode.Multiple, - }; - - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); - _helper.Click((Interactive)target.Presenter.Panel.Children[3]); - _helper.Click((Interactive)target.Presenter.Panel.Children[4], modifiers: KeyModifiers.Control); - - var panel = target.Presenter.Panel; - - Assert.Equal(new[] { "Foo", "Bar" }, target.SelectedItems); - Assert.Equal(new[] { 3, 4 }, SelectedContainers(target)); + var target = new ListBox + { + Template = Template(), + Items = new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" }, + SelectionMode = SelectionMode.Multiple, + }; + AvaloniaLocator.CurrentMutable.Bind().ToConstant(new Mock().Object); + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + _helper.Click((Interactive)target.Presenter.Panel.Children[3]); + _helper.Click((Interactive)target.Presenter.Panel.Children[4], modifiers: KeyModifiers.Control); + + var panel = target.Presenter.Panel; + + Assert.Equal(new[] { "Foo", "Bar" }, target.SelectedItems); + Assert.Equal(new[] { 3, 4 }, SelectedContainers(target)); + } } [Fact] public void Should_Shift_Select_Correct_Item_When_Duplicates_Are_Present() { - var target = new ListBox + using (UnitTestApplication.Start()) { - Template = Template(), - Items = new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" }, - SelectionMode = SelectionMode.Multiple, - }; - - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); - _helper.Click((Interactive)target.Presenter.Panel.Children[3]); - _helper.Click((Interactive)target.Presenter.Panel.Children[5], modifiers: KeyModifiers.Shift); - - var panel = target.Presenter.Panel; - - Assert.Equal(new[] { "Foo", "Bar", "Baz" }, target.SelectedItems); - Assert.Equal(new[] { 3, 4, 5 }, SelectedContainers(target)); + var target = new ListBox + { + Template = Template(), + Items = new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" }, + SelectionMode = SelectionMode.Multiple, + }; + AvaloniaLocator.CurrentMutable.Bind().ToConstant(new Mock().Object); + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + _helper.Click((Interactive)target.Presenter.Panel.Children[3]); + _helper.Click((Interactive)target.Presenter.Panel.Children[5], modifiers: KeyModifiers.Shift); + + var panel = target.Presenter.Panel; + + Assert.Equal(new[] { "Foo", "Bar", "Baz" }, target.SelectedItems); + Assert.Equal(new[] { 3, 4, 5 }, SelectedContainers(target)); + } } [Fact] public void Can_Shift_Select_All_Items_When_Duplicates_Are_Present() { - var target = new ListBox + using (UnitTestApplication.Start()) { - Template = Template(), - Items = new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" }, - SelectionMode = SelectionMode.Multiple, - }; - - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); - _helper.Click((Interactive)target.Presenter.Panel.Children[0]); - _helper.Click((Interactive)target.Presenter.Panel.Children[5], modifiers: KeyModifiers.Shift); - - var panel = target.Presenter.Panel; - - Assert.Equal(new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" }, target.SelectedItems); - Assert.Equal(new[] { 0, 1, 2, 3, 4, 5 }, SelectedContainers(target)); + var target = new ListBox + { + Template = Template(), + Items = new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" }, + SelectionMode = SelectionMode.Multiple, + }; + AvaloniaLocator.CurrentMutable.Bind().ToConstant(new Mock().Object); + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + _helper.Click((Interactive)target.Presenter.Panel.Children[0]); + _helper.Click((Interactive)target.Presenter.Panel.Children[5], modifiers: KeyModifiers.Shift); + + var panel = target.Presenter.Panel; + + Assert.Equal(new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" }, target.SelectedItems); + Assert.Equal(new[] { 0, 1, 2, 3, 4, 5 }, SelectedContainers(target)); + } } [Fact] public void Shift_Selecting_Raises_SelectionChanged_Events() { - var target = new ListBox + using (UnitTestApplication.Start()) { - Template = Template(), - Items = new[] { "Foo", "Bar", "Baz", "Qux" }, - SelectionMode = SelectionMode.Multiple, - }; - - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); + var target = new ListBox + { + Template = Template(), + Items = new[] { "Foo", "Bar", "Baz", "Qux" }, + SelectionMode = SelectionMode.Multiple, + }; + AvaloniaLocator.CurrentMutable.Bind().ToConstant(new Mock().Object); + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); - SelectionChangedEventArgs receivedArgs = null; + SelectionChangedEventArgs receivedArgs = null; - target.SelectionChanged += (_, args) => receivedArgs = args; + target.SelectionChanged += (_, args) => receivedArgs = args; - void VerifyAdded(params string[] selection) - { - Assert.NotNull(receivedArgs); - Assert.Equal(selection, receivedArgs.AddedItems); - Assert.Empty(receivedArgs.RemovedItems); - } + void VerifyAdded(params string[] selection) + { + Assert.NotNull(receivedArgs); + Assert.Equal(selection, receivedArgs.AddedItems); + Assert.Empty(receivedArgs.RemovedItems); + } - void VerifyRemoved(string selection) - { - Assert.NotNull(receivedArgs); - Assert.Equal(new[] { selection }, receivedArgs.RemovedItems); - Assert.Empty(receivedArgs.AddedItems); - } + void VerifyRemoved(string selection) + { + Assert.NotNull(receivedArgs); + Assert.Equal(new[] { selection }, receivedArgs.RemovedItems); + Assert.Empty(receivedArgs.AddedItems); + } - _helper.Click((Interactive)target.Presenter.Panel.Children[1]); + _helper.Click((Interactive)target.Presenter.Panel.Children[1]); - VerifyAdded("Bar"); + VerifyAdded("Bar"); - receivedArgs = null; - _helper.Click((Interactive)target.Presenter.Panel.Children[3], modifiers: KeyModifiers.Shift); + receivedArgs = null; + _helper.Click((Interactive)target.Presenter.Panel.Children[3], modifiers: KeyModifiers.Shift); - VerifyAdded("Baz" ,"Qux"); + VerifyAdded("Baz", "Qux"); - receivedArgs = null; - _helper.Click((Interactive)target.Presenter.Panel.Children[2], modifiers: KeyModifiers.Shift); + receivedArgs = null; + _helper.Click((Interactive)target.Presenter.Panel.Children[2], modifiers: KeyModifiers.Shift); - VerifyRemoved("Qux"); + VerifyRemoved("Qux"); + } } [Fact] public void Duplicate_Items_Are_Added_To_SelectedItems_In_Order() { - var target = new ListBox + using (UnitTestApplication.Start()) { - Template = Template(), - Items = new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" }, - SelectionMode = SelectionMode.Multiple, - }; + var target = new ListBox + { + Template = Template(), + Items = new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" }, + SelectionMode = SelectionMode.Multiple, + }; - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); - _helper.Click((Interactive)target.Presenter.Panel.Children[0]); + AvaloniaLocator.CurrentMutable.Bind().ToConstant(new Mock().Object); + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + _helper.Click((Interactive)target.Presenter.Panel.Children[0]); - Assert.Equal(new[] { "Foo" }, target.SelectedItems); + Assert.Equal(new[] { "Foo" }, target.SelectedItems); - _helper.Click((Interactive)target.Presenter.Panel.Children[4], modifiers: KeyModifiers.Control); + _helper.Click((Interactive)target.Presenter.Panel.Children[4], modifiers: KeyModifiers.Control); - Assert.Equal(new[] { "Foo", "Bar" }, target.SelectedItems); + Assert.Equal(new[] { "Foo", "Bar" }, target.SelectedItems); - _helper.Click((Interactive)target.Presenter.Panel.Children[3], modifiers: KeyModifiers.Control); + _helper.Click((Interactive)target.Presenter.Panel.Children[3], modifiers: KeyModifiers.Control); - Assert.Equal(new[] { "Foo", "Bar", "Foo" }, target.SelectedItems); + Assert.Equal(new[] { "Foo", "Bar", "Foo" }, target.SelectedItems); - _helper.Click((Interactive)target.Presenter.Panel.Children[1], modifiers: KeyModifiers.Control); + _helper.Click((Interactive)target.Presenter.Panel.Children[1], modifiers: KeyModifiers.Control); - Assert.Equal(new[] { "Foo", "Bar", "Foo", "Bar" }, target.SelectedItems); + Assert.Equal(new[] { "Foo", "Bar", "Foo", "Bar" }, target.SelectedItems); + } } [Fact] @@ -1158,70 +1189,79 @@ namespace Avalonia.Controls.UnitTests.Primitives [Fact] public void Left_Click_On_SelectedItem_Should_Clear_Existing_Selection() { - var target = new ListBox + using (UnitTestApplication.Start()) { - Template = Template(), - Items = new[] { "Foo", "Bar", "Baz" }, - ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Width = 20, Height = 10 }), - SelectionMode = SelectionMode.Multiple, - }; - - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); - target.SelectAll(); - - Assert.Equal(3, target.SelectedItems.Count); - - _helper.Click((Interactive)target.Presenter.Panel.Children[0]); - - Assert.Equal(1, target.SelectedItems.Count); - Assert.Equal(new[] { "Foo", }, target.SelectedItems); - Assert.Equal(new[] { 0 }, SelectedContainers(target)); + var target = new ListBox + { + Template = Template(), + Items = new[] { "Foo", "Bar", "Baz" }, + ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Width = 20, Height = 10 }), + SelectionMode = SelectionMode.Multiple, + }; + AvaloniaLocator.CurrentMutable.Bind().ToConstant(new Mock().Object); + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + target.SelectAll(); + + Assert.Equal(3, target.SelectedItems.Count); + + _helper.Click((Interactive)target.Presenter.Panel.Children[0]); + + Assert.Equal(1, target.SelectedItems.Count); + Assert.Equal(new[] { "Foo", }, target.SelectedItems); + Assert.Equal(new[] { 0 }, SelectedContainers(target)); + } } [Fact] public void Right_Click_On_SelectedItem_Should_Not_Clear_Existing_Selection() { - var target = new ListBox + using (UnitTestApplication.Start()) { - Template = Template(), - Items = new[] { "Foo", "Bar", "Baz" }, - ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Width = 20, Height = 10 }), - SelectionMode = SelectionMode.Multiple, - }; - - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); - target.SelectAll(); + var target = new ListBox + { + Template = Template(), + Items = new[] { "Foo", "Bar", "Baz" }, + ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Width = 20, Height = 10 }), + SelectionMode = SelectionMode.Multiple, + }; + AvaloniaLocator.CurrentMutable.Bind().ToConstant(new Mock().Object); + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + target.SelectAll(); - Assert.Equal(3, target.SelectedItems.Count); + Assert.Equal(3, target.SelectedItems.Count); - _helper.Click((Interactive)target.Presenter.Panel.Children[0], MouseButton.Right); + _helper.Click((Interactive)target.Presenter.Panel.Children[0], MouseButton.Right); - Assert.Equal(3, target.SelectedItems.Count); + Assert.Equal(3, target.SelectedItems.Count); + } } [Fact] public void Right_Click_On_UnselectedItem_Should_Clear_Existing_Selection() { - var target = new ListBox + using (UnitTestApplication.Start()) { - Template = Template(), - Items = new[] { "Foo", "Bar", "Baz" }, - ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Width = 20, Height = 10 }), - SelectionMode = SelectionMode.Multiple, - }; - - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); - _helper.Click((Interactive)target.Presenter.Panel.Children[0]); - _helper.Click((Interactive)target.Presenter.Panel.Children[1], modifiers: KeyModifiers.Shift); - - Assert.Equal(2, target.SelectedItems.Count); - - _helper.Click((Interactive)target.Presenter.Panel.Children[2], MouseButton.Right); - - Assert.Equal(1, target.SelectedItems.Count); + var target = new ListBox + { + Template = Template(), + Items = new[] { "Foo", "Bar", "Baz" }, + ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Width = 20, Height = 10 }), + SelectionMode = SelectionMode.Multiple, + }; + AvaloniaLocator.CurrentMutable.Bind().ToConstant(new Mock().Object); + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + _helper.Click((Interactive)target.Presenter.Panel.Children[0]); + _helper.Click((Interactive)target.Presenter.Panel.Children[1], modifiers: KeyModifiers.Shift); + + Assert.Equal(2, target.SelectedItems.Count); + + _helper.Click((Interactive)target.Presenter.Panel.Children[2], MouseButton.Right); + + Assert.Equal(1, target.SelectedItems.Count); + } } [Fact] @@ -1253,41 +1293,47 @@ namespace Avalonia.Controls.UnitTests.Primitives [Fact] public void Shift_Right_Click_Should_Not_Select_Multiple() { - var target = new ListBox + using (UnitTestApplication.Start()) { - Template = Template(), - Items = new[] { "Foo", "Bar", "Baz" }, - ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Width = 20, Height = 10 }), - SelectionMode = SelectionMode.Multiple, - }; - - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); - - _helper.Click((Interactive)target.Presenter.Panel.Children[0]); - _helper.Click((Interactive)target.Presenter.Panel.Children[2], MouseButton.Right, modifiers: KeyModifiers.Shift); - - Assert.Equal(1, target.SelectedItems.Count); + var target = new ListBox + { + Template = Template(), + Items = new[] { "Foo", "Bar", "Baz" }, + ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Width = 20, Height = 10 }), + SelectionMode = SelectionMode.Multiple, + }; + AvaloniaLocator.CurrentMutable.Bind().ToConstant(new Mock().Object); + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + + _helper.Click((Interactive)target.Presenter.Panel.Children[0]); + _helper.Click((Interactive)target.Presenter.Panel.Children[2], MouseButton.Right, modifiers: KeyModifiers.Shift); + + Assert.Equal(1, target.SelectedItems.Count); + } } [Fact] public void Ctrl_Right_Click_Should_Not_Select_Multiple() { - var target = new ListBox + using (UnitTestApplication.Start()) { - Template = Template(), - Items = new[] { "Foo", "Bar", "Baz" }, - ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Width = 20, Height = 10 }), - SelectionMode = SelectionMode.Multiple, - }; - - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); - - _helper.Click((Interactive)target.Presenter.Panel.Children[0]); - _helper.Click((Interactive)target.Presenter.Panel.Children[2], MouseButton.Right, modifiers: KeyModifiers.Control); - - Assert.Equal(1, target.SelectedItems.Count); + var target = new ListBox + { + Template = Template(), + Items = new[] { "Foo", "Bar", "Baz" }, + ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Width = 20, Height = 10 }), + SelectionMode = SelectionMode.Multiple, + }; + AvaloniaLocator.CurrentMutable.Bind().ToConstant(new Mock().Object); + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + + _helper.Click((Interactive)target.Presenter.Panel.Children[0]); + _helper.Click((Interactive)target.Presenter.Panel.Children[2], MouseButton.Right, modifiers: KeyModifiers.Control); + + Assert.Equal(1, target.SelectedItems.Count); + } } [Fact] From 1731f88822a6d0a4c2e5a58b69f866212e7ff7e4 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Tue, 2 Aug 2022 20:37:55 -0400 Subject: [PATCH 047/197] Rename Default to Simple --- Avalonia.sln | 2 +- samples/BindingDemo/App.xaml | 2 +- samples/ControlCatalog/App.xaml.cs | 2 +- samples/ControlCatalog/ControlCatalog.csproj | 2 +- samples/ControlCatalog/MainView.xaml.cs | 4 +- samples/PlatformSanityChecks/App.xaml | 3 +- .../PlatformSanityChecks.csproj | 2 +- samples/Previewer/App.xaml | 5 +- samples/Previewer/Previewer.csproj | 2 +- samples/VirtualizationDemo/App.xaml | 4 +- .../VirtualizationDemo.csproj | 2 +- .../interop/Direct3DInteropSample/App.paml | 5 +- .../Direct3DInteropSample.csproj | 2 +- src/Avalonia.Controls/ProgressBar.cs | 4 - .../Avalonia.Diagnostics.csproj | 2 +- .../Diagnostics/Views/MainWindow.xaml | 2 +- .../Diagnostics/Views/MainWindow.xaml.cs | 2 +- src/Avalonia.Themes.Default/DefaultTheme.xaml | 75 ------------------- .../DefaultTheme.xaml.cs | 11 --- .../Accents/Base.xaml | 0 .../Accents/BaseDark.xaml | 0 .../Accents/BaseLight.xaml | 0 .../ApiCompatBaseline.txt | 0 .../Avalonia.Themes.Simple.csproj} | 0 .../Controls/AutoCompleteBox.xaml | 0 .../Controls/Button.xaml | 0 .../Controls/ButtonSpinner.xaml | 6 +- .../Controls/Calendar.xaml | 0 .../Controls/CalendarButton.xaml | 0 .../Controls/CalendarDatePicker.xaml | 0 .../Controls/CalendarDayButton.xaml | 0 .../Controls/CalendarItem.xaml | 0 .../Controls/CaptionButtons.xaml | 10 +-- .../Controls/Carousel.xaml | 0 .../Controls/CheckBox.xaml | 0 .../Controls/ComboBox.xaml | 0 .../Controls/ComboBoxItem.xaml | 0 .../Controls/ContentControl.xaml | 0 .../Controls/ContextMenu.xaml | 2 +- .../Controls/DataValidationErrors.xaml | 0 .../Controls/DatePicker.xaml | 22 +++--- .../Controls/DateTimePickerShared.xaml | 22 +++--- .../Controls/DropDownButton.xaml | 0 .../Controls/EmbeddableControlRoot.xaml | 0 .../Controls/Expander.xaml | 34 ++++----- .../Controls/FlyoutPresenter.xaml | 0 .../Controls/FocusAdorner.xaml | 0 .../Controls/GridSplitter.xaml | 0 .../Controls/ItemsControl.xaml | 0 .../Controls/Label.xaml | 0 .../Controls/ListBox.xaml | 0 .../Controls/ListBoxItem.xaml | 0 .../Controls/ManagedFileChooser.xaml | 0 .../Controls/Menu.xaml | 6 +- .../Controls/MenuFlyoutPresenter.xaml | 2 +- .../Controls/MenuItem.xaml | 2 +- .../Controls/NativeMenuBar.xaml | 10 +-- .../Controls/NotificationCard.xaml | 0 .../Controls/NumericUpDown.xaml | 0 .../Controls/OverlayPopupHost.xaml | 0 .../Controls/PathIcon.xaml | 0 .../Controls/PopupRoot.xaml | 0 .../Controls/ProgressBar.xaml | 0 .../Controls/RadioButton.xaml | 0 .../Controls/RepeatButton.xaml | 0 .../Controls/RichTextBlock.xaml | 0 .../Controls/ScrollBar.xaml | 0 .../Controls/ScrollViewer.xaml | 2 +- .../Controls/Separator.xaml | 0 .../Controls/SimpleControls.xaml | 74 ++++++++++++++++++ .../Controls/Slider.xaml | 0 .../Controls/SplitButton.xaml | 8 +- .../Controls/SplitView.xaml | 0 .../Controls/TabControl.xaml | 0 .../Controls/TabItem.xaml | 0 .../Controls/TabStrip.xaml | 0 .../Controls/TabStripItem.xaml | 0 .../Controls/TextBox.xaml | 14 ++-- .../Controls/TimePicker.xaml | 20 ++--- .../Controls/TitleBar.xaml | 0 .../Controls/ToggleButton.xaml | 0 .../Controls/ToggleSwitch.xaml | 0 .../Controls/ToolTip.xaml | 0 .../Controls/TransitioningContentControl.xaml | 0 .../Controls/TreeView.xaml | 0 .../Controls/TreeViewItem.xaml | 4 +- .../Controls/UserControl.xaml | 4 +- .../Controls/Window.xaml | 0 .../Controls/WindowNotificationManager.xaml | 0 .../IBitmapToImageConverter.cs | 2 +- .../InverseBooleanValueConverter.cs | 2 +- .../Properties/AssemblyInfo.cs | 3 + .../SimpleTheme.cs | 14 ++-- .../SimpleThemeMode.cs | 2 +- .../Avalonia.Designer.HostApp.csproj | 2 +- .../Utilities/UriExtensionsTests.cs | 6 +- .../Avalonia.Benchmarks.csproj | 2 +- .../Themes/ThemeBenchmark.cs | 6 +- .../Avalonia.DesignerSupport.TestApp/App.xaml | 6 +- .../Avalonia.DesignerSupport.TestApp.csproj | 2 +- .../Avalonia.Direct2D1.RenderTests.csproj | 2 +- .../Avalonia.LeakTests.csproj | 2 +- .../Avalonia.Markup.Xaml.UnitTests.csproj | 2 +- .../Xaml/BasicTests.cs | 2 +- .../Avalonia.Skia.RenderTests.csproj | 2 +- .../Avalonia.UnitTests.csproj | 2 +- tests/Avalonia.UnitTests/TestServices.cs | 19 ++--- .../Avalonia.UnitTests/UnitTestApplication.cs | 8 +- 108 files changed, 220 insertions(+), 241 deletions(-) delete mode 100644 src/Avalonia.Themes.Default/DefaultTheme.xaml delete mode 100644 src/Avalonia.Themes.Default/DefaultTheme.xaml.cs rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Accents/Base.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Accents/BaseDark.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Accents/BaseLight.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/ApiCompatBaseline.txt (100%) rename src/{Avalonia.Themes.Default/Avalonia.Themes.Default.csproj => Avalonia.Themes.Simple/Avalonia.Themes.Simple.csproj} (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/AutoCompleteBox.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/Button.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/ButtonSpinner.xaml (94%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/Calendar.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/CalendarButton.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/CalendarDatePicker.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/CalendarDayButton.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/CalendarItem.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/CaptionButtons.xaml (93%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/Carousel.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/CheckBox.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/ComboBox.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/ComboBoxItem.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/ContentControl.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/ContextMenu.xaml (95%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/DataValidationErrors.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/DatePicker.xaml (94%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/DateTimePickerShared.xaml (87%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/DropDownButton.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/EmbeddableControlRoot.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/Expander.xaml (88%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/FlyoutPresenter.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/FocusAdorner.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/GridSplitter.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/ItemsControl.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/Label.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/ListBox.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/ListBoxItem.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/ManagedFileChooser.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/Menu.xaml (95%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/MenuFlyoutPresenter.xaml (96%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/MenuItem.xaml (98%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/NativeMenuBar.xaml (76%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/NotificationCard.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/NumericUpDown.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/OverlayPopupHost.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/PathIcon.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/PopupRoot.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/ProgressBar.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/RadioButton.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/RepeatButton.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/RichTextBlock.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/ScrollBar.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/ScrollViewer.xaml (99%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/Separator.xaml (100%) create mode 100644 src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/Slider.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/SplitButton.xaml (98%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/SplitView.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/TabControl.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/TabItem.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/TabStrip.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/TabStripItem.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/TextBox.xaml (96%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/TimePicker.xaml (94%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/TitleBar.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/ToggleButton.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/ToggleSwitch.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/ToolTip.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/TransitioningContentControl.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/TreeView.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/TreeViewItem.xaml (96%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/UserControl.xaml (90%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/Window.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/Controls/WindowNotificationManager.xaml (100%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/IBitmapToImageConverter.cs (95%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/InverseBooleanValueConverter.cs (94%) create mode 100644 src/Avalonia.Themes.Simple/Properties/AssemblyInfo.cs rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/SimpleTheme.cs (89%) rename src/{Avalonia.Themes.Default => Avalonia.Themes.Simple}/SimpleThemeMode.cs (67%) diff --git a/Avalonia.sln b/Avalonia.sln index 4999719676..35b6b2108a 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -13,7 +13,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Direct2D1", "src\W EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls", "src\Avalonia.Controls\Avalonia.Controls.csproj", "{D2221C82-4A25-4583-9B43-D791E3F6820C}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Themes.Default", "src\Avalonia.Themes.Default\Avalonia.Themes.Default.csproj", "{3E10A5FA-E8DA-48B1-AD44-6A5B6CB7750F}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Themes.Simple", "src\Avalonia.Themes.Simple\Avalonia.Themes.Simple.csproj", "{3E10A5FA-E8DA-48B1-AD44-6A5B6CB7750F}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Diagnostics", "src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj", "{7062AE20-5DCC-4442-9645-8195BDECE63E}" EndProject diff --git a/samples/BindingDemo/App.xaml b/samples/BindingDemo/App.xaml index 3e312c8685..175e838616 100644 --- a/samples/BindingDemo/App.xaml +++ b/samples/BindingDemo/App.xaml @@ -4,6 +4,6 @@ x:Class="BindingDemo.App"> - + diff --git a/samples/ControlCatalog/App.xaml.cs b/samples/ControlCatalog/App.xaml.cs index 7ebb87094a..eb8ecc3af0 100644 --- a/samples/ControlCatalog/App.xaml.cs +++ b/samples/ControlCatalog/App.xaml.cs @@ -5,7 +5,7 @@ using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml.Styling; using Avalonia.Styling; -using Avalonia.Themes.Default; +using Avalonia.Themes.Simple; using Avalonia.Themes.Fluent; using ControlCatalog.ViewModels; diff --git a/samples/ControlCatalog/ControlCatalog.csproj b/samples/ControlCatalog/ControlCatalog.csproj index 8358fb3cd4..2654574a3e 100644 --- a/samples/ControlCatalog/ControlCatalog.csproj +++ b/samples/ControlCatalog/ControlCatalog.csproj @@ -29,7 +29,7 @@ - + diff --git a/samples/ControlCatalog/MainView.xaml.cs b/samples/ControlCatalog/MainView.xaml.cs index 47d11738bc..d8d58edaf6 100644 --- a/samples/ControlCatalog/MainView.xaml.cs +++ b/samples/ControlCatalog/MainView.xaml.cs @@ -60,14 +60,14 @@ namespace ControlCatalog } else if (theme == CatalogTheme.DefaultLight) { - App.Default.Mode = Avalonia.Themes.Default.SimpleThemeMode.Light; + App.Default.Mode = Avalonia.Themes.Simple.SimpleThemeMode.Light; Application.Current.Styles[0] = App.DefaultLight; Application.Current.Styles[1] = App.ColorPickerDefault; Application.Current.Styles[2] = App.DataGridDefault; } else if (theme == CatalogTheme.DefaultDark) { - App.Default.Mode = Avalonia.Themes.Default.SimpleThemeMode.Dark; + App.Default.Mode = Avalonia.Themes.Simple.SimpleThemeMode.Dark; Application.Current.Styles[0] = App.DefaultDark; Application.Current.Styles[1] = App.ColorPickerDefault; Application.Current.Styles[2] = App.DataGridDefault; diff --git a/samples/PlatformSanityChecks/App.xaml b/samples/PlatformSanityChecks/App.xaml index 25bab6ae35..1b9d64fca6 100644 --- a/samples/PlatformSanityChecks/App.xaml +++ b/samples/PlatformSanityChecks/App.xaml @@ -1,6 +1,5 @@ - - + diff --git a/samples/PlatformSanityChecks/PlatformSanityChecks.csproj b/samples/PlatformSanityChecks/PlatformSanityChecks.csproj index 4f7f06b529..5c743aabdb 100644 --- a/samples/PlatformSanityChecks/PlatformSanityChecks.csproj +++ b/samples/PlatformSanityChecks/PlatformSanityChecks.csproj @@ -7,7 +7,7 @@ - + diff --git a/samples/Previewer/App.xaml b/samples/Previewer/App.xaml index 6bae1955af..1b9d64fca6 100644 --- a/samples/Previewer/App.xaml +++ b/samples/Previewer/App.xaml @@ -1,6 +1,5 @@ - - + - \ No newline at end of file + diff --git a/samples/Previewer/Previewer.csproj b/samples/Previewer/Previewer.csproj index 98560e9ab0..2cc84168dc 100644 --- a/samples/Previewer/Previewer.csproj +++ b/samples/Previewer/Previewer.csproj @@ -10,7 +10,7 @@ - + diff --git a/samples/VirtualizationDemo/App.xaml b/samples/VirtualizationDemo/App.xaml index 3ad1dce794..0b64d0c09b 100644 --- a/samples/VirtualizationDemo/App.xaml +++ b/samples/VirtualizationDemo/App.xaml @@ -3,7 +3,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="VirtualizationDemo.App"> - - + + \ No newline at end of file diff --git a/samples/VirtualizationDemo/VirtualizationDemo.csproj b/samples/VirtualizationDemo/VirtualizationDemo.csproj index bd6054327f..b27cfe77e8 100644 --- a/samples/VirtualizationDemo/VirtualizationDemo.csproj +++ b/samples/VirtualizationDemo/VirtualizationDemo.csproj @@ -5,7 +5,7 @@ - + diff --git a/samples/interop/Direct3DInteropSample/App.paml b/samples/interop/Direct3DInteropSample/App.paml index d9630eef58..e6d77dfaf4 100644 --- a/samples/interop/Direct3DInteropSample/App.paml +++ b/samples/interop/Direct3DInteropSample/App.paml @@ -1,6 +1,5 @@ - - + - \ No newline at end of file + diff --git a/samples/interop/Direct3DInteropSample/Direct3DInteropSample.csproj b/samples/interop/Direct3DInteropSample/Direct3DInteropSample.csproj index 81b94b4d09..f9ef4693d5 100644 --- a/samples/interop/Direct3DInteropSample/Direct3DInteropSample.csproj +++ b/samples/interop/Direct3DInteropSample/Direct3DInteropSample.csproj @@ -22,7 +22,7 @@ - + diff --git a/src/Avalonia.Controls/ProgressBar.cs b/src/Avalonia.Controls/ProgressBar.cs index 56b9caf085..91114628ee 100644 --- a/src/Avalonia.Controls/ProgressBar.cs +++ b/src/Avalonia.Controls/ProgressBar.cs @@ -119,14 +119,12 @@ namespace Avalonia.Controls nameof(Percentage), o => o.Percentage); - [Obsolete("To be removed when Avalonia.Themes.Default is discontinued.")] public static readonly DirectProperty IndeterminateStartingOffsetProperty = AvaloniaProperty.RegisterDirect( nameof(IndeterminateStartingOffset), p => p.IndeterminateStartingOffset, (p, o) => p.IndeterminateStartingOffset = o); - [Obsolete("To be removed when Avalonia.Themes.Default is discontinued.")] public static readonly DirectProperty IndeterminateEndingOffsetProperty = AvaloniaProperty.RegisterDirect( nameof(IndeterminateEndingOffset), @@ -139,14 +137,12 @@ namespace Avalonia.Controls private set { SetAndRaise(PercentageProperty, ref _percentage, value); } } - [Obsolete("To be removed when Avalonia.Themes.Default is discontinued.")] public double IndeterminateStartingOffset { get => _indeterminateStartingOffset; set => SetAndRaise(IndeterminateStartingOffsetProperty, ref _indeterminateStartingOffset, value); } - [Obsolete("To be removed when Avalonia.Themes.Default is discontinued.")] public double IndeterminateEndingOffset { get => _indeterminateEndingOffset; diff --git a/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj b/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj index adddf3f57b..d719135a7f 100644 --- a/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj +++ b/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj @@ -19,7 +19,7 @@ - + diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml index 004518598c..63073952ac 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml @@ -2,7 +2,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:views="clr-namespace:Avalonia.Diagnostics.Views" xmlns:diag="clr-namespace:Avalonia.Diagnostics" - xmlns:default="using:Avalonia.Themes.Default" + xmlns:default="using:Avalonia.Themes.Simple" Title="Avalonia DevTools" x:Class="Avalonia.Diagnostics.Views.MainWindow" Theme="{StaticResource {x:Type Window}}"> diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs index 96dc929434..c81997f2cb 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs @@ -11,7 +11,7 @@ using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Markup.Xaml; using Avalonia.Styling; -using Avalonia.Themes.Default; +using Avalonia.Themes.Simple; using Avalonia.VisualTree; namespace Avalonia.Diagnostics.Views diff --git a/src/Avalonia.Themes.Default/DefaultTheme.xaml b/src/Avalonia.Themes.Default/DefaultTheme.xaml deleted file mode 100644 index 8f5bea557c..0000000000 --- a/src/Avalonia.Themes.Default/DefaultTheme.xaml +++ /dev/null @@ -1,75 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Avalonia.Themes.Default/DefaultTheme.xaml.cs b/src/Avalonia.Themes.Default/DefaultTheme.xaml.cs deleted file mode 100644 index 598b418977..0000000000 --- a/src/Avalonia.Themes.Default/DefaultTheme.xaml.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Avalonia.Styling; - -namespace Avalonia.Themes.Default -{ - /// - /// The default Avalonia theme. - /// - public class DefaultTheme : Styles - { - } -} diff --git a/src/Avalonia.Themes.Default/Accents/Base.xaml b/src/Avalonia.Themes.Simple/Accents/Base.xaml similarity index 100% rename from src/Avalonia.Themes.Default/Accents/Base.xaml rename to src/Avalonia.Themes.Simple/Accents/Base.xaml diff --git a/src/Avalonia.Themes.Default/Accents/BaseDark.xaml b/src/Avalonia.Themes.Simple/Accents/BaseDark.xaml similarity index 100% rename from src/Avalonia.Themes.Default/Accents/BaseDark.xaml rename to src/Avalonia.Themes.Simple/Accents/BaseDark.xaml diff --git a/src/Avalonia.Themes.Default/Accents/BaseLight.xaml b/src/Avalonia.Themes.Simple/Accents/BaseLight.xaml similarity index 100% rename from src/Avalonia.Themes.Default/Accents/BaseLight.xaml rename to src/Avalonia.Themes.Simple/Accents/BaseLight.xaml diff --git a/src/Avalonia.Themes.Default/ApiCompatBaseline.txt b/src/Avalonia.Themes.Simple/ApiCompatBaseline.txt similarity index 100% rename from src/Avalonia.Themes.Default/ApiCompatBaseline.txt rename to src/Avalonia.Themes.Simple/ApiCompatBaseline.txt diff --git a/src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj b/src/Avalonia.Themes.Simple/Avalonia.Themes.Simple.csproj similarity index 100% rename from src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj rename to src/Avalonia.Themes.Simple/Avalonia.Themes.Simple.csproj diff --git a/src/Avalonia.Themes.Default/Controls/AutoCompleteBox.xaml b/src/Avalonia.Themes.Simple/Controls/AutoCompleteBox.xaml similarity index 100% rename from src/Avalonia.Themes.Default/Controls/AutoCompleteBox.xaml rename to src/Avalonia.Themes.Simple/Controls/AutoCompleteBox.xaml diff --git a/src/Avalonia.Themes.Default/Controls/Button.xaml b/src/Avalonia.Themes.Simple/Controls/Button.xaml similarity index 100% rename from src/Avalonia.Themes.Default/Controls/Button.xaml rename to src/Avalonia.Themes.Simple/Controls/Button.xaml diff --git a/src/Avalonia.Themes.Default/Controls/ButtonSpinner.xaml b/src/Avalonia.Themes.Simple/Controls/ButtonSpinner.xaml similarity index 94% rename from src/Avalonia.Themes.Default/Controls/ButtonSpinner.xaml rename to src/Avalonia.Themes.Simple/Controls/ButtonSpinner.xaml index 4585fc8e56..9798e5290b 100644 --- a/src/Avalonia.Themes.Default/Controls/ButtonSpinner.xaml +++ b/src/Avalonia.Themes.Simple/Controls/ButtonSpinner.xaml @@ -2,7 +2,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:CompileBindings="True"> - @@ -38,7 +38,7 @@ IsVisible="{TemplateBinding ShowButtonSpinner}" Rows="2"> + Theme="{StaticResource SimpleButtonSpinnerRepeatButton}"> + Theme="{StaticResource SimpleButtonSpinnerRepeatButton}"> - @@ -44,7 +44,7 @@ TextElement.FontSize="10"> +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var windowResources = (ResourceDictionary)window.Resources; + var buttonResources = (ResourceDictionary)((Button)window.Content!).Resources; + + Assert.True(windowResources.ContainsDeferredKey("Red")); + Assert.True(buttonResources.ContainsDeferredKey("Red2")); + } + } + + [Fact] + public void Item_StaticReferenced_Is_UnDeferred_On_Read() + { + using (StyledWindow()) + { + var xaml = @" + + + + + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var windowResources = (ResourceDictionary)window.Resources; + var buttonResources = (ResourceDictionary)((Button)window.Content!).Resources; + + Assert.IsType(buttonResources["Red2"]); + + Assert.False(windowResources.ContainsDeferredKey("Red")); + Assert.False(buttonResources.ContainsDeferredKey("Red2")); + } + } + private IDisposable StyledWindow(params (string, string)[] assets) { var services = TestServices.StyledWindow.With( From 64c94ca3415e3c5f9961c62eb6ca090ee7ab5c0a Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Wed, 3 Aug 2022 09:24:17 +0200 Subject: [PATCH 056/197] fix: NullInputBackend visibility --- .../Input/NullInput/NullInputBackend.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Input/NullInput/NullInputBackend.cs b/src/Linux/Avalonia.LinuxFramebuffer/Input/NullInput/NullInputBackend.cs index 551c0995a2..05dd0195b1 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Input/NullInput/NullInputBackend.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Input/NullInput/NullInputBackend.cs @@ -4,9 +4,8 @@ using Avalonia.Input.Raw; namespace Avalonia.LinuxFramebuffer.Input.NullInput; -internal class NullInputBackend : IInputBackend +public class NullInputBackend : IInputBackend { - public void Initialize(IScreenInfoProvider screen, Action onInput) { } From 3fd8763e807f195cee6c4313cf6e0ecf44e74aa5 Mon Sep 17 00:00:00 2001 From: Takoooooo Date: Wed, 3 Aug 2022 13:22:00 +0300 Subject: [PATCH 057/197] Use correct ToggleModifier in TreeView multiselection on MacOS. --- src/Avalonia.Controls/TreeView.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs index b2a188a2ea..7359f3cade 100644 --- a/src/Avalonia.Controls/TreeView.cs +++ b/src/Avalonia.Controls/TreeView.cs @@ -529,7 +529,7 @@ namespace Avalonia.Controls e.Source, true, e.KeyModifiers.HasAllFlags(KeyModifiers.Shift), - e.KeyModifiers.HasAllFlags(KeyModifiers.Control), + e.KeyModifiers.HasAllFlags(AvaloniaLocator.Current.GetRequiredService().CommandModifiers), point.Properties.IsRightButtonPressed); } } From 3717aec4f5f4e9e5c88d5b03b22d067fdd67a568 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 3 Aug 2022 14:32:38 +0300 Subject: [PATCH 058/197] Use GetFeature + API lease approach for accessing SKCanvas --- samples/RenderDemo/Pages/CustomSkiaPage.cs | 6 +- src/Avalonia.Base/Media/DrawingGroup.cs | 2 + .../Platform/IDrawingContextImpl.cs | 14 +++ .../Drawing/CompositionDrawingContext.cs | 2 + .../Composition/Server/DrawingContextProxy.cs | 3 + .../SceneGraph/DeferredDrawingContextImpl.cs | 2 + .../HeadlessPlatformRenderInterface.cs | 5 + src/Skia/Avalonia.Skia/DrawingContextImpl.cs | 97 +++++++++++++++++-- .../Helpers/DrawingContextHelper.cs | 12 ++- .../Avalonia.Skia/ISkiaDrawingContextImpl.cs | 15 --- .../ISkiaSharpApiLeaseFeature.cs | 20 ++++ .../Media/DrawingContextImpl.cs | 1 + .../NullDrawingContextImpl.cs | 5 +- 13 files changed, 152 insertions(+), 32 deletions(-) delete mode 100644 src/Skia/Avalonia.Skia/ISkiaDrawingContextImpl.cs create mode 100644 src/Skia/Avalonia.Skia/ISkiaSharpApiLeaseFeature.cs diff --git a/samples/RenderDemo/Pages/CustomSkiaPage.cs b/samples/RenderDemo/Pages/CustomSkiaPage.cs index 9c524a7932..bf27747154 100644 --- a/samples/RenderDemo/Pages/CustomSkiaPage.cs +++ b/samples/RenderDemo/Pages/CustomSkiaPage.cs @@ -40,14 +40,16 @@ namespace RenderDemo.Pages static Stopwatch St = Stopwatch.StartNew(); public void Render(IDrawingContextImpl context) { - var canvas = (context as ISkiaDrawingContextImpl)?.SkCanvas; - if (canvas == null) + var leaseFeature = context.GetFeature(); + if (leaseFeature == null) using (var c = new DrawingContext(context, false)) { c.DrawText(_noSkia, new Point()); } else { + using var lease = leaseFeature.Lease(); + var canvas = lease.SkCanvas; canvas.Save(); // create the first shader var colors = new SKColor[] { diff --git a/src/Avalonia.Base/Media/DrawingGroup.cs b/src/Avalonia.Base/Media/DrawingGroup.cs index 603bb1c1c1..e71f568207 100644 --- a/src/Avalonia.Base/Media/DrawingGroup.cs +++ b/src/Avalonia.Base/Media/DrawingGroup.cs @@ -228,6 +228,8 @@ namespace Avalonia.Media throw new NotImplementedException(); } + public object? GetFeature(Type t) => null; + public void DrawBitmap(IRef source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default) { throw new NotImplementedException(); diff --git a/src/Avalonia.Base/Platform/IDrawingContextImpl.cs b/src/Avalonia.Base/Platform/IDrawingContextImpl.cs index d84a509234..6aa5eeea3d 100644 --- a/src/Avalonia.Base/Platform/IDrawingContextImpl.cs +++ b/src/Avalonia.Base/Platform/IDrawingContextImpl.cs @@ -172,6 +172,20 @@ namespace Avalonia.Platform /// /// Custom draw operation void Custom(ICustomDrawOperation custom); + + /// + /// Attempts to get an optional feature from the drawing context implementation + /// + object? GetFeature(Type t); + } + + public static class DrawingContextImplExtensions + { + /// + /// Attempts to get an optional feature from the drawing context implementation + /// + public static T? GetFeature(this IDrawingContextImpl context) where T : class => + (T?)context.GetFeature(typeof(T)); } public interface IDrawingContextLayerImpl : IRenderTargetBitmapImpl diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs index 01216d19ed..30b57883fc 100644 --- a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs +++ b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs @@ -156,6 +156,8 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW ++_drawOperationIndex; } + public object? GetFeature(Type t) => null; + /// public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun) { diff --git a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs index 7eb35a68ed..03859d241f 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs @@ -1,3 +1,4 @@ +using System; using System.Numerics; using Avalonia.Media; using Avalonia.Media.Imaging; @@ -155,6 +156,8 @@ internal class CompositorDrawingContextProxy : IDrawingContextImpl, IDrawingCont _impl.Custom(custom); } + public object? GetFeature(Type t) => _impl.GetFeature(t); + public class VisualBrushRenderer : IVisualBrushRenderer { public CompositionDrawList? VisualBrushDrawList { get; set; } diff --git a/src/Avalonia.Base/Rendering/SceneGraph/DeferredDrawingContextImpl.cs b/src/Avalonia.Base/Rendering/SceneGraph/DeferredDrawingContextImpl.cs index 07082e4ac3..d6766fa9b8 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/DeferredDrawingContextImpl.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/DeferredDrawingContextImpl.cs @@ -203,6 +203,8 @@ namespace Avalonia.Rendering.SceneGraph ++_drawOperationindex; } + public object? GetFeature(Type t) => null; + /// public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun) { diff --git a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs index 5576368240..cb23c6c336 100644 --- a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs +++ b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs @@ -416,6 +416,11 @@ namespace Avalonia.Headless } + public object GetFeature(Type t) + { + return null; + } + public void DrawLine(IPen pen, Point p1, Point p2) { } diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index 8293769138..99ab60d1ac 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -10,6 +10,7 @@ using Avalonia.Rendering.SceneGraph; using Avalonia.Rendering.Utilities; using Avalonia.Utilities; using Avalonia.Media.Imaging; +using JetBrains.Annotations; using SkiaSharp; namespace Avalonia.Skia @@ -17,7 +18,7 @@ namespace Avalonia.Skia /// /// Skia based drawing context. /// - internal class DrawingContextImpl : IDrawingContextImpl, ISkiaDrawingContextImpl, IDrawingContextWithAcrylicLikeSupport + internal class DrawingContextImpl : IDrawingContextImpl, IDrawingContextWithAcrylicLikeSupport { private IDisposable[] _disposables; private readonly Vector _dpi; @@ -38,7 +39,8 @@ namespace Avalonia.Skia private readonly SKPaint _fillPaint = new SKPaint(); private readonly SKPaint _boxShadowPaint = new SKPaint(); private static SKShader s_acrylicNoiseShader; - private readonly ISkiaGpuRenderSession _session; + private readonly ISkiaGpuRenderSession _session; + private bool _leased = false; /// /// Context create info. @@ -83,6 +85,47 @@ namespace Avalonia.Skia public ISkiaGpuRenderSession CurrentSession; } + class SkiaLeaseFeature : ISkiaSharpApiLeaseFeature + { + private readonly DrawingContextImpl _context; + + public SkiaLeaseFeature(DrawingContextImpl context) + { + _context = context; + } + + public ISkiaSharpApiLease Lease() + { + _context.CheckLease(); + return new ApiLease(_context); + } + + class ApiLease : ISkiaSharpApiLease + { + private DrawingContextImpl _context; + private readonly SKMatrix _revertTransform; + + public ApiLease(DrawingContextImpl context) + { + _revertTransform = context.Canvas.TotalMatrix; + _context = context; + _context._leased = true; + } + + public SKCanvas SkCanvas => _context.Canvas; + public GRContext GrContext => _context.GrContext; + public SKSurface SkSurface => _context.Surface; + public double CurrentOpacity => _context._currentOpacity; + + public void Dispose() + { + _context.Canvas.SetMatrix(_revertTransform); + _context._leased = false; + _context = null; + } + } + } + /// /// Create new drawing context. /// @@ -123,20 +166,23 @@ namespace Avalonia.Skia public SKCanvas Canvas { get; } public SKSurface Surface { get; } - SKCanvas ISkiaDrawingContextImpl.SkCanvas => Canvas; - SKSurface ISkiaDrawingContextImpl.SkSurface => Surface; - GRContext ISkiaDrawingContextImpl.GrContext => _grContext; - double ISkiaDrawingContextImpl.CurrentOpacity => _currentOpacity; - + private void CheckLease() + { + if (_leased) + throw new InvalidOperationException("The underlying graphics API is currently leased"); + } + /// public void Clear(Color color) { + CheckLease(); Canvas.Clear(color.ToSKColor()); } /// public void DrawBitmap(IRef source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode) { + CheckLease(); var drawableImage = (IDrawableBitmapImpl)source.Item; var s = sourceRect.ToSKRect(); var d = destRect.ToSKRect(); @@ -157,6 +203,7 @@ namespace Avalonia.Skia /// public void DrawBitmap(IRef source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect) { + CheckLease(); PushOpacityMask(opacityMask, opacityMaskRect); DrawBitmap(source, 1, new Rect(0, 0, source.Item.PixelSize.Width, source.Item.PixelSize.Height), destRect, BitmapInterpolationMode.Default); PopOpacityMask(); @@ -165,6 +212,7 @@ namespace Avalonia.Skia /// public void DrawLine(IPen pen, Point p1, Point p2) { + CheckLease(); using (var paint = CreatePaint(_strokePaint, pen, new Size(Math.Abs(p2.X - p1.X), Math.Abs(p2.Y - p1.Y)))) { if (paint.Paint is object) @@ -177,6 +225,7 @@ namespace Avalonia.Skia /// public void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry) { + CheckLease(); var impl = (GeometryImpl) geometry; var size = geometry.Bounds.Size; @@ -260,6 +309,7 @@ namespace Avalonia.Skia { if (rect.Rect.Height <= 0 || rect.Rect.Width <= 0) return; + CheckLease(); var rc = rect.Rect.ToSKRect(); var isRounded = rect.IsRounded; @@ -296,6 +346,7 @@ namespace Avalonia.Skia { if (rect.Rect.Height <= 0 || rect.Rect.Width <= 0) return; + CheckLease(); // Arbitrary chosen values // On OSX Skia breaks OpenGL context when asked to draw, e. g. (0, 0, 623, 6666600) rect if (rect.Rect.Height > 8192 || rect.Rect.Width > 8192) @@ -421,7 +472,8 @@ namespace Avalonia.Skia { if (rect.Height <= 0 || rect.Width <= 0) return; - + CheckLease(); + var rc = rect.ToSKRect(); if (brush != null) @@ -447,6 +499,7 @@ namespace Avalonia.Skia /// public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun) { + CheckLease(); using (var paintWrapper = CreatePaint(_fillPaint, foreground, glyphRun.Size)) { var glyphRunImpl = (GlyphRunImpl)glyphRun.GlyphRunImpl; @@ -459,18 +512,21 @@ namespace Avalonia.Skia /// public IDrawingContextLayerImpl CreateLayer(Size size) { + CheckLease(); return CreateRenderTarget(size, true); } /// public void PushClip(Rect clip) { + CheckLease(); Canvas.Save(); Canvas.ClipRect(clip.ToSKRect()); } public void PushClip(RoundedRect clip) { + CheckLease(); Canvas.Save(); Canvas.ClipRoundRect(clip.ToSKRoundRect(), antialias:true); } @@ -478,12 +534,14 @@ namespace Avalonia.Skia /// public void PopClip() { + CheckLease(); Canvas.Restore(); } /// public void PushOpacity(double opacity) { + CheckLease(); _opacityStack.Push(_currentOpacity); _currentOpacity *= opacity; } @@ -491,6 +549,7 @@ namespace Avalonia.Skia /// public void PopOpacity() { + CheckLease(); _currentOpacity = _opacityStack.Pop(); } @@ -499,6 +558,7 @@ namespace Avalonia.Skia { if(_disposed) return; + CheckLease(); try { if (_grContext != null) @@ -523,6 +583,7 @@ namespace Avalonia.Skia /// public void PushGeometryClip(IGeometryImpl clip) { + CheckLease(); Canvas.Save(); Canvas.ClipPath(((GeometryImpl)clip).EffectivePath, SKClipOperation.Intersect, true); } @@ -530,12 +591,14 @@ namespace Avalonia.Skia /// public void PopGeometryClip() { + CheckLease(); Canvas.Restore(); } /// public void PushBitmapBlendMode(BitmapBlendingMode blendingMode) { + CheckLease(); _blendingModeStack.Push(_currentBlendingMode); _currentBlendingMode = blendingMode; } @@ -543,14 +606,20 @@ namespace Avalonia.Skia /// public void PopBitmapBlendMode() { + CheckLease(); _currentBlendingMode = _blendingModeStack.Pop(); } - public void Custom(ICustomDrawOperation custom) => custom.Render(this); + public void Custom(ICustomDrawOperation custom) + { + CheckLease(); + custom.Render(this); + } /// public void PushOpacityMask(IBrush mask, Rect bounds) { + CheckLease(); // TODO: This should be disposed var paint = new SKPaint(); @@ -561,6 +630,7 @@ namespace Avalonia.Skia /// public void PopOpacityMask() { + CheckLease(); using (var paint = new SKPaint { BlendMode = SKBlendMode.DstIn }) { Canvas.SaveLayer(paint); @@ -580,6 +650,7 @@ namespace Avalonia.Skia get { return _currentTransform; } set { + CheckLease(); if (_currentTransform == value) return; @@ -596,6 +667,14 @@ namespace Avalonia.Skia } } + [CanBeNull] + public object GetFeature(Type t) + { + if (t == typeof(ISkiaSharpApiLeaseFeature)) + return new SkiaLeaseFeature(this); + return null; + } + /// /// Configure paint wrapper for using gradient brush. /// diff --git a/src/Skia/Avalonia.Skia/Helpers/DrawingContextHelper.cs b/src/Skia/Avalonia.Skia/Helpers/DrawingContextHelper.cs index d0b45b7c5d..a078c364a2 100644 --- a/src/Skia/Avalonia.Skia/Helpers/DrawingContextHelper.cs +++ b/src/Skia/Avalonia.Skia/Helpers/DrawingContextHelper.cs @@ -33,7 +33,7 @@ namespace Avalonia.Skia.Helpers /// Unsupported - Wraps a GPU Backed SkiaSurface in an Avalonia DrawingContext. /// [Obsolete] - public static ISkiaDrawingContextImpl WrapSkiaSurface(this SKSurface surface, GRContext grContext, Vector dpi, params IDisposable[] disposables) + public static IDrawingContextImpl WrapSkiaSurface(this SKSurface surface, GRContext grContext, Vector dpi, params IDisposable[] disposables) { var createInfo = new DrawingContextImpl.CreateInfo { @@ -50,7 +50,7 @@ namespace Avalonia.Skia.Helpers /// Unsupported - Wraps a non-GPU Backed SkiaSurface in an Avalonia DrawingContext. /// [Obsolete] - public static ISkiaDrawingContextImpl WrapSkiaSurface(this SKSurface surface, Vector dpi, params IDisposable[] disposables) + public static IDrawingContextImpl WrapSkiaSurface(this SKSurface surface, Vector dpi, params IDisposable[] disposables) { var createInfo = new DrawingContextImpl.CreateInfo { @@ -63,7 +63,7 @@ namespace Avalonia.Skia.Helpers } [Obsolete] - public static ISkiaDrawingContextImpl CreateDrawingContext(Size size, Vector dpi, GRContext grContext = null) + public static IDrawingContextImpl CreateDrawingContext(Size size, Vector dpi, GRContext grContext = null) { if (grContext is null) { @@ -90,9 +90,11 @@ namespace Avalonia.Skia.Helpers } [Obsolete] - public static void DrawTo(this ISkiaDrawingContextImpl source, ISkiaDrawingContextImpl destination, SKPaint paint = null) + public static void DrawTo(this IDrawingContextImpl source, IDrawingContextImpl destination, SKPaint paint = null) { - destination.SkCanvas.DrawSurface(source.SkSurface, new SKPoint(0, 0), paint); + var src = (DrawingContextImpl)source; + var dst = (DrawingContextImpl)destination; + dst.Canvas.DrawSurface(src.Surface, new SKPoint(0, 0), paint); } } } diff --git a/src/Skia/Avalonia.Skia/ISkiaDrawingContextImpl.cs b/src/Skia/Avalonia.Skia/ISkiaDrawingContextImpl.cs deleted file mode 100644 index 1b60154d46..0000000000 --- a/src/Skia/Avalonia.Skia/ISkiaDrawingContextImpl.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Avalonia.Metadata; -using Avalonia.Platform; -using SkiaSharp; - -namespace Avalonia.Skia -{ - [Unstable] - public interface ISkiaDrawingContextImpl : IDrawingContextImpl - { - SKCanvas SkCanvas { get; } - GRContext GrContext { get; } - SKSurface SkSurface { get; } - double CurrentOpacity { get; } - } -} diff --git a/src/Skia/Avalonia.Skia/ISkiaSharpApiLeaseFeature.cs b/src/Skia/Avalonia.Skia/ISkiaSharpApiLeaseFeature.cs new file mode 100644 index 0000000000..b3966c0324 --- /dev/null +++ b/src/Skia/Avalonia.Skia/ISkiaSharpApiLeaseFeature.cs @@ -0,0 +1,20 @@ +using System; +using Avalonia.Metadata; +using SkiaSharp; + +namespace Avalonia.Skia; + +[Unstable] +public interface ISkiaSharpApiLeaseFeature +{ + public ISkiaSharpApiLease Lease(); +} + +[Unstable] +public interface ISkiaSharpApiLease : IDisposable +{ + SKCanvas SkCanvas { get; } + GRContext GrContext { get; } + SKSurface SkSurface { get; } + double CurrentOpacity { get; } +} \ No newline at end of file diff --git a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs index a7f1d9c3e5..180ae491b3 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs @@ -614,5 +614,6 @@ namespace Avalonia.Direct2D1.Media } public void Custom(ICustomDrawOperation custom) => custom.Render(this); + public object GetFeature(Type t) => null; } } diff --git a/tests/Avalonia.Benchmarks/NullDrawingContextImpl.cs b/tests/Avalonia.Benchmarks/NullDrawingContextImpl.cs index 6fb2a5ac24..8436881122 100644 --- a/tests/Avalonia.Benchmarks/NullDrawingContextImpl.cs +++ b/tests/Avalonia.Benchmarks/NullDrawingContextImpl.cs @@ -1,4 +1,5 @@ -using Avalonia.Media; +using System; +using Avalonia.Media; using Avalonia.Platform; using Avalonia.Rendering.SceneGraph; using Avalonia.Utilities; @@ -99,5 +100,7 @@ namespace Avalonia.Benchmarks public void Custom(ICustomDrawOperation custom) { } + + public object GetFeature(Type t) => null; } } From a30d73eb5ed9433cff2d212b21fb9eae93e7b54b Mon Sep 17 00:00:00 2001 From: Takoooooo Date: Wed, 3 Aug 2022 14:38:45 +0300 Subject: [PATCH 059/197] Fix tests. --- .../TreeViewTests.cs | 38 ++++++++++--------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs index d784caf2db..9cf21423a3 100644 --- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs @@ -18,6 +18,7 @@ using Avalonia.LogicalTree; using Avalonia.Styling; using Avalonia.UnitTests; using JetBrains.Annotations; +using Moq; using Xunit; namespace Avalonia.Controls.UnitTests @@ -885,28 +886,31 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Right_Click_On_SelectedItem_Should_Not_Clear_Existing_Selection() { - var tree = CreateTestTreeData(); - var target = new TreeView + using (UnitTestApplication.Start()) { - Template = CreateTreeViewTemplate(), - Items = tree, - SelectionMode = SelectionMode.Multiple, - }; - - var visualRoot = new TestRoot(); - visualRoot.Child = target; + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree, + SelectionMode = SelectionMode.Multiple, + }; + AvaloniaLocator.CurrentMutable.Bind().ToConstant(new Mock().Object); + var visualRoot = new TestRoot(); + visualRoot.Child = target; - CreateNodeDataTemplate(target); - ApplyTemplates(target); - target.ExpandSubTree((TreeViewItem)target.Presenter.Panel.Children[0]); - target.SelectAll(); + CreateNodeDataTemplate(target); + ApplyTemplates(target); + target.ExpandSubTree((TreeViewItem)target.Presenter.Panel.Children[0]); + target.SelectAll(); - AssertChildrenSelected(target, tree[0]); - Assert.Equal(5, target.SelectedItems.Count); + AssertChildrenSelected(target, tree[0]); + Assert.Equal(5, target.SelectedItems.Count); - _mouse.Click((Interactive)target.Presenter.Panel.Children[0], MouseButton.Right); + _mouse.Click((Interactive)target.Presenter.Panel.Children[0], MouseButton.Right); - Assert.Equal(5, target.SelectedItems.Count); + Assert.Equal(5, target.SelectedItems.Count); + } } [Fact] From fffdcfcc2a8c49f16d9f1c363c774e56e20d685b Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 3 Aug 2022 14:05:55 +0200 Subject: [PATCH 060/197] Fix fluent ButtonSpinner theme. The button was greying out its borders on pointer-over. --- .../Controls/ButtonSpinner.xaml | 30 ++++++++++++++----- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/ButtonSpinner.xaml b/src/Avalonia.Themes.Fluent/Controls/ButtonSpinner.xaml index 855dc5363e..aa55065f6d 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ButtonSpinner.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ButtonSpinner.xaml @@ -41,13 +41,31 @@ M0,9 L10,0 20,9 19,10 10,2 1,10 z M0,1 L10,10 20,1 19,0 10,8 1,0 z - - + - + + + + + + + + + @@ -83,7 +101,6 @@ Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness, Converter={StaticResource ButtonSpinnerLeftThickness}}" - CornerRadius="0" VerticalAlignment="Stretch" VerticalContentAlignment="Center" Foreground="{TemplateBinding Foreground}" @@ -99,7 +116,6 @@ Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness, Converter={StaticResource ButtonSpinnerLeftThickness}}" - CornerRadius="0" VerticalAlignment="Stretch" VerticalContentAlignment="Center" Foreground="{TemplateBinding Foreground}" From ad518ac6787ab8265adcd1a4eddc70c390590ca6 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 3 Aug 2022 15:00:42 +0300 Subject: [PATCH 061/197] Add backpressure for sending batches to the render thread --- .../Rendering/Composition/CompositingRenderer.cs | 2 +- src/Avalonia.Base/Rendering/Composition/Compositor.cs | 11 ++++++++++- .../Rendering/Composition/Server/ServerCompositor.cs | 3 +-- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs index db773bc43c..9aa3c25425 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs @@ -71,7 +71,7 @@ public class CompositingRenderer : IRendererWithCompositor if(_queuedUpdate) return; _queuedUpdate = true; - Dispatcher.UIThread.Post(_update, DispatcherPriority.Composition); + _compositor.InvokeWhenReadyForNextCommit(_update); } /// diff --git a/src/Avalonia.Base/Rendering/Composition/Compositor.cs b/src/Avalonia.Base/Rendering/Composition/Compositor.cs index 45212d0f36..f812acc016 100644 --- a/src/Avalonia.Base/Rendering/Composition/Compositor.cs +++ b/src/Avalonia.Base/Rendering/Composition/Compositor.cs @@ -33,6 +33,7 @@ namespace Avalonia.Rendering.Composition internal IEasing DefaultEasing { get; } private List? _invokeOnNextCommit; private readonly Stack> _invokeListPool = new(); + private Task? _lastBatchCompleted; /// /// Creates a new compositor on a specified render loop that would use a particular GPU @@ -86,7 +87,7 @@ namespace Avalonia.Rendering.Composition if (_invokeOnNextCommit != null) ScheduleCommitCallbacks(batch.Completed); - return batch.Completed; + return _lastBatchCompleted = batch.Completed; } async void ScheduleCommitCallbacks(Task task) @@ -139,5 +140,13 @@ namespace Avalonia.Rendering.Composition _invokeOnNextCommit ??= _invokeListPool.Count > 0 ? _invokeListPool.Pop() : new(); _invokeOnNextCommit.Add(action); } + + public void InvokeWhenReadyForNextCommit(Action action) + { + if (_lastBatchCompleted == null || _lastBatchCompleted.IsCompleted) + Dispatcher.UIThread.Post(action, DispatcherPriority.Composition); + else + _lastBatchCompleted.ContinueWith(_ => Dispatcher.UIThread.Post(action, DispatcherPriority.Composition)); + } } } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs index 621bc84f4a..bfc2b2d626 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs @@ -108,6 +108,7 @@ namespace Avalonia.Rendering.Composition.Server private void RenderCore() { ApplyPendingBatches(); + CompletePendingBatches(); foreach(var animation in _activeAnimations) _animationsToUpdate.Add(animation); @@ -119,8 +120,6 @@ namespace Avalonia.Rendering.Composition.Server foreach (var t in _activeTargets) t.Render(); - - CompletePendingBatches(); } public void AddCompositionTarget(ServerCompositionTarget target) From 0d472a100b6fc191d69675512028c4e770045081 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 3 Aug 2022 15:57:13 +0300 Subject: [PATCH 062/197] Use static callback for ContinueWith --- src/Avalonia.Base/Rendering/Composition/Compositor.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Rendering/Composition/Compositor.cs b/src/Avalonia.Base/Rendering/Composition/Compositor.cs index f812acc016..10360f7874 100644 --- a/src/Avalonia.Base/Rendering/Composition/Compositor.cs +++ b/src/Avalonia.Base/Rendering/Composition/Compositor.cs @@ -146,7 +146,9 @@ namespace Avalonia.Rendering.Composition if (_lastBatchCompleted == null || _lastBatchCompleted.IsCompleted) Dispatcher.UIThread.Post(action, DispatcherPriority.Composition); else - _lastBatchCompleted.ContinueWith(_ => Dispatcher.UIThread.Post(action, DispatcherPriority.Composition)); + _lastBatchCompleted.ContinueWith( + static (_, state) => Dispatcher.UIThread.Post((Action)state!, DispatcherPriority.Composition), + action); } } } From 3d327bc046ab6295fac0634b9da06fec92a8d493 Mon Sep 17 00:00:00 2001 From: Takoooooo Date: Wed, 3 Aug 2022 18:50:23 +0300 Subject: [PATCH 063/197] Fix and optimize StringFormatConverter. StringFormatConverter should validate values and return AvaloniaProperty.UnsetValue in case of incorrect value or exception. --- .../Converters/StringFormatConverter.cs | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/src/Avalonia.Controls/Converters/StringFormatConverter.cs b/src/Avalonia.Controls/Converters/StringFormatConverter.cs index ae920dac7e..7075d3de99 100644 --- a/src/Avalonia.Controls/Converters/StringFormatConverter.cs +++ b/src/Avalonia.Controls/Converters/StringFormatConverter.cs @@ -1,8 +1,6 @@ using System; using System.Collections.Generic; using System.Globalization; -using System.Linq; -using Avalonia.Data; using Avalonia.Data.Converters; namespace Avalonia.Controls.Converters; @@ -15,13 +13,25 @@ public class StringFormatConverter : IMultiValueConverter { public object? Convert(IList values, Type targetType, object? parameter, CultureInfo culture) { - try - { - return string.Format((string)values[0]!, values.Skip(1).ToArray()); - } - catch (Exception e) + if (values != null && + values.Count == 5 && + values[0] is string format && + values[1] is double && + values[2] is double && + values[3] is double && + values[4] is double) + { - return new BindingNotification(e, BindingErrorType.Error); + + try + { + return string.Format(format, values[1], values[2], values[3], values[4]); + } + catch + { + return AvaloniaProperty.UnsetValue; + } } + return AvaloniaProperty.UnsetValue; } } From e7797eea673ecac0da6463314c43ce5645a96064 Mon Sep 17 00:00:00 2001 From: ErrorCraft <51973682+ErrorCraft@users.noreply.github.com> Date: Wed, 3 Aug 2022 20:51:10 +0200 Subject: [PATCH 064/197] Add Sector shape --- src/Avalonia.Base/Utilities/MathUtilities.cs | 35 +++++++++++ src/Avalonia.Controls/Shapes/Sector.cs | 65 ++++++++++++++++++++ 2 files changed, 100 insertions(+) create mode 100644 src/Avalonia.Controls/Shapes/Sector.cs diff --git a/src/Avalonia.Base/Utilities/MathUtilities.cs b/src/Avalonia.Base/Utilities/MathUtilities.cs index d381979c1e..3c48c3469e 100644 --- a/src/Avalonia.Base/Utilities/MathUtilities.cs +++ b/src/Avalonia.Base/Utilities/MathUtilities.cs @@ -324,6 +324,41 @@ namespace Avalonia.Utilities return angle * 2 * Math.PI; } + /// + /// Calculates the point of an angle on an ellipse. + /// + /// The centre point of the ellipse. + /// The x radius of the ellipse. + /// The y radius of the ellipse. + /// The angle in radians. + /// A point on the ellipse. + public static Point GetEllipsePoint(Point centre, double radiusX, double radiusY, double angle) + { + return new Point(radiusX * Math.Cos(angle) + centre.X, radiusY * Math.Sin(angle) + centre.Y); + } + + /// + /// Gets the minimum and maximum from the specified numbers. + /// + /// The first number. + /// The second number. + /// A tuple containing the minimum and maximum of the two specified numbers. + public static (double min, double max) GetMinMax(double a, double b) + { + return a < b ? (a, b) : (b, a); + } + + /// + /// Gets the minimum and maximum from the specified number and the difference with that number. + /// + /// The initial value to use. + /// The difference for . + /// A tuple containing the minimum and maximum of the specified number and the difference with that number. + public static (double min, double max) GetMinMaxFromDelta(double initialValue, double delta) + { + return GetMinMax(initialValue, initialValue + delta); + } + private static void ThrowCannotBeGreaterThanException(T min, T max) { throw new ArgumentException($"{min} cannot be greater than {max}."); diff --git a/src/Avalonia.Controls/Shapes/Sector.cs b/src/Avalonia.Controls/Shapes/Sector.cs new file mode 100644 index 0000000000..5d2f6701a7 --- /dev/null +++ b/src/Avalonia.Controls/Shapes/Sector.cs @@ -0,0 +1,65 @@ +using System; +using Avalonia.Media; +using Avalonia.Utilities; + +namespace Avalonia.Controls.Shapes +{ + public class Sector : Shape + { + public static readonly StyledProperty StartAngleProperty = AvaloniaProperty.Register(nameof(StartAngle), 0.0d); + public static readonly StyledProperty AngleProperty = AvaloniaProperty.Register(nameof(Angle), 0.0d); + + public double StartAngle + { + get => GetValue(StartAngleProperty); + set => SetValue(StartAngleProperty, value); + } + + public double Angle + { + get => GetValue(AngleProperty); + set => SetValue(AngleProperty, value); + } + + static Sector() + { + StrokeThicknessProperty.OverrideDefaultValue(1.0d); + AffectsGeometry(BoundsProperty, StrokeThicknessProperty, StartAngleProperty, AngleProperty); + } + + protected override Geometry? CreateDefiningGeometry() + { + Rect rect = new Rect(Bounds.Size); + Rect deflatedRect = rect.Deflate(StrokeThickness * 0.5d); + + if (Angle >= 360.0d || Angle <= -360.0d) + { + return new EllipseGeometry(deflatedRect); + } + + if (Angle == 0.0d) + { + return new StreamGeometry(); + } + + (double startAngle, double endAngle) = MathUtilities.GetMinMaxFromDelta(MathUtilities.Deg2Rad(StartAngle), MathUtilities.Deg2Rad(Angle)); + + Point centre = new Point(rect.Width * 0.5d, rect.Height * 0.5d); + double radiusX = deflatedRect.Width * 0.5d; + double radiusY = deflatedRect.Height * 0.5d; + Point startCurvePoint = MathUtilities.GetEllipsePoint(centre, radiusX, radiusY, startAngle); + Point endCurvePoint = MathUtilities.GetEllipsePoint(centre, radiusX, radiusY, endAngle); + Size size = new Size(radiusX, radiusY); + + StreamGeometry streamGeometry = new StreamGeometry(); + using StreamGeometryContext streamGeometryContext = streamGeometry.Open(); + + streamGeometryContext.BeginFigure(startCurvePoint, false); + streamGeometryContext.ArcTo(endCurvePoint, size, 0.0d, Math.Abs(Angle) > 180.0d, SweepDirection.Clockwise); + streamGeometryContext.LineTo(centre); + streamGeometryContext.EndFigure(true); + + return streamGeometry; + } + } +} From d4840a55bc1037970cc8cc2e2125fb0446741ebb Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Tue, 2 Aug 2022 18:31:57 +0300 Subject: [PATCH 065/197] add failing test for #8669 --- .../ListBoxTests.cs | 49 +++++++++++++++++-- 1 file changed, 44 insertions(+), 5 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs index afa153a593..f6d96edb99 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs @@ -9,7 +9,6 @@ using Avalonia.Data; using Avalonia.Input; using Avalonia.LogicalTree; using Avalonia.Styling; -using Avalonia.Threading; using Avalonia.UnitTests; using Avalonia.VisualTree; using Xunit; @@ -19,7 +18,7 @@ namespace Avalonia.Controls.UnitTests public class ListBoxTests { private MouseTestHelper _mouse = new MouseTestHelper(); - + [Fact] public void Should_Use_ItemTemplate_To_Create_Item_Content() { @@ -433,6 +432,47 @@ namespace Avalonia.Controls.UnitTests } } + [Fact] + public void ListBox_Should_Be_Valid_After_Remove_Of_Item_In_NonVisibleArea() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var items = new AvaloniaList(Enumerable.Range(1, 30).Select(v => v.ToString())); + + var wnd = new Window() { Width = 100, Height = 100, IsVisible = true }; + + var target = new ListBox() + { + AutoScrollToSelectedItem = true, + Height = 100, + Width = 50, + VirtualizationMode = ItemVirtualizationMode.Simple, + ItemTemplate = new FuncDataTemplate((c, _) => new Border() { Height = 10 }), + Items = items, + }; + wnd.Content = target; + + var lm = wnd.LayoutManager; + + lm.ExecuteInitialLayoutPass(); + + //select last / scroll to last item + target.SelectedItem = items.Last(); + + lm.ExecuteLayoutPass(); + + //remove the first item (in non realized area of the listbox) + items.Remove("1"); + lm.ExecuteLayoutPass(); + + Assert.Equal("30", target.ItemContainerGenerator.ContainerFromIndex(items.Count - 1).DataContext); + Assert.Equal("29", target.ItemContainerGenerator.ContainerFromIndex(items.Count - 2).DataContext); + Assert.Equal("28", target.ItemContainerGenerator.ContainerFromIndex(items.Count - 3).DataContext); + Assert.Equal("27", target.ItemContainerGenerator.ContainerFromIndex(items.Count - 4).DataContext); + Assert.Equal("26", target.ItemContainerGenerator.ContainerFromIndex(items.Count - 5).DataContext); + } + } + [Fact] public void Clicking_Item_Should_Raise_BringIntoView_For_Correct_Control() { @@ -656,7 +696,6 @@ namespace Avalonia.Controls.UnitTests public string Value { get; } } - [Fact] public void SelectedItem_Validation() { @@ -670,11 +709,11 @@ namespace Avalonia.Controls.UnitTests }; Prepare(target); - + var exception = new System.InvalidCastException("failed validation"); var textObservable = new BehaviorSubject(new BindingNotification(exception, BindingErrorType.DataValidationError)); target.Bind(ComboBox.SelectedItemProperty, textObservable); - + Assert.True(DataValidationErrors.GetHasErrors(target)); Assert.True(DataValidationErrors.GetErrors(target).SequenceEqual(new[] { exception })); } From d68efa1cd4f89142eb7cf159425ea6939d978e02 Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Thu, 4 Aug 2022 15:07:23 +0300 Subject: [PATCH 066/197] fix for #8669 --- src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs index 39a512a773..4db2ec6d50 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs @@ -187,7 +187,7 @@ namespace Avalonia.Controls.Presenters break; case NotifyCollectionChangedAction.Remove: - if ((e.OldStartingIndex >= FirstIndex && e.OldStartingIndex < NextIndex) || + if (e.OldStartingIndex < NextIndex || panel.Children.Count > ItemCount) { RecycleContainersOnRemove(); From 6520722e5488350433e525232877e139ad57cf53 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Wed, 3 Aug 2022 03:20:37 -0400 Subject: [PATCH 067/197] Attempt to not deferr value types --- ...valoniaXamlIlDeferredResourceTransformer.cs | 10 ++++++++++ .../Xaml/ResourceDictionaryTests.cs | 18 ++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDeferredResourceTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDeferredResourceTransformer.cs index 099878df08..662263e513 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDeferredResourceTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDeferredResourceTransformer.cs @@ -15,6 +15,9 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers if (!(node is XamlPropertyAssignmentNode pa) || pa.Values.Count != 2) return node; + if (!ShouldBeDeferred(pa.Values[1])) + return node; + var types = context.GetAvaloniaTypes(); if (pa.Property.DeclaringType == types.ResourceDictionary && pa.Property.Name == "Content") @@ -37,6 +40,13 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers return node; } + private static bool ShouldBeDeferred(IXamlAstValueNode node) + { + // XAML compiler is currently strict about value types, allowing them to be created only through converters. + // At the moment it should be safe to not defer structs. + return !node.Type.GetClrType().IsValueType; + } + class AdderSetter : IXamlPropertySetter, IXamlEmitablePropertySetter { private readonly IXamlMethod _getter; diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ResourceDictionaryTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ResourceDictionaryTests.cs index 74b6a1e15d..19fb88b391 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ResourceDictionaryTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ResourceDictionaryTests.cs @@ -3,6 +3,7 @@ using Avalonia.Controls; using Avalonia.Controls.Presenters; using Avalonia.Controls.Templates; using Avalonia.Media; +using Avalonia.Media.Immutable; using Avalonia.Styling; using Avalonia.UnitTests; using Xunit; @@ -237,6 +238,23 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml Assert.False(buttonResources.ContainsDeferredKey("Red2")); } } + + [Fact] + public void Value_Type_With_Parse_Should_Not_Be_Deferred() + { + using (StyledWindow()) + { + var xaml = @" + + Red +"; + var resources = (ResourceDictionary)AvaloniaRuntimeXamlLoader.Load(xaml); + + Assert.False(resources.ContainsDeferredKey("Red")); + Assert.IsType(resources["Red"]); + } + } private IDisposable StyledWindow(params (string, string)[] assets) { From 41d31d8857dec976aa293d8070ea69296f84c2d1 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Wed, 3 Aug 2022 03:53:01 -0400 Subject: [PATCH 068/197] Add thickness test as well --- .../Xaml/ResourceDictionaryTests.cs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ResourceDictionaryTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ResourceDictionaryTests.cs index 19fb88b391..5066341bbb 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ResourceDictionaryTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ResourceDictionaryTests.cs @@ -240,7 +240,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml } [Fact] - public void Value_Type_With_Parse_Should_Not_Be_Deferred() + public void Value_Type_With_Parse_Converter_Should_Not_Be_Deferred() { using (StyledWindow()) { @@ -255,6 +255,23 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml Assert.IsType(resources["Red"]); } } + + [Fact] + public void Value_Type_With_Ctor_Converter_Should_Not_Be_Deferred() + { + using (StyledWindow()) + { + var xaml = @" + + 1 1 1 1 +"; + var resources = (ResourceDictionary)AvaloniaRuntimeXamlLoader.Load(xaml); + + Assert.False(resources.ContainsDeferredKey("Margin")); + Assert.IsType(resources["Margin"]); + } + } private IDisposable StyledWindow(params (string, string)[] assets) { From 0360af6780f476f195142fa2772fc9f008e9da47 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 4 Aug 2022 18:11:07 +0200 Subject: [PATCH 069/197] Added failing test for #8672. --- .../Data/TemplateBindingTests.cs | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/tests/Avalonia.Markup.UnitTests/Data/TemplateBindingTests.cs b/tests/Avalonia.Markup.UnitTests/Data/TemplateBindingTests.cs index d9ea3e374c..979dbec674 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/TemplateBindingTests.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/TemplateBindingTests.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Globalization; using System.Linq; using Avalonia.Controls; @@ -217,6 +218,37 @@ namespace Avalonia.Markup.UnitTests.Data } } + [Fact] + public void Should_Not_Pass_UnsetValue_To_MultiBinding_During_ApplyTemplate() + { + var converter = new MultiConverter(); + var source = new Button + { + Content = "foo", + Template = new FuncControlTemplate