From a6918116bdaf3899e28a209e19a7555306d5659c Mon Sep 17 00:00:00 2001 From: Omid Mafakher Date: Mon, 15 Jan 2024 05:57:35 +0100 Subject: [PATCH 01/14] Add tv support to form factor (#14194) * add tv support to form factor * Resolve comments and add chromium book * Add some documentation --------- Co-authored-by: Max Katz --- .../Pages/PlatformInfoPage.xaml | 3 ++ .../Avalonia.Android/AndroidPlatform.cs | 2 +- .../AndroidRuntimePlatform.cs | 52 +++++++++++++++++++ .../Platform/IRuntimePlatform.cs | 6 ++- .../BrowserRuntimePlatform.cs | 6 ++- .../Interop/AvaloniaModule.cs | 5 +- .../webapp/modules/avalonia/caniuse.ts | 4 ++ .../MarkupExtensions/OnFormFactorExtension.cs | 22 ++++++++ .../TizenApplicationExtensions.cs | 7 ++- .../Avalonia.Tizen/TizenRuntimePlatform.cs | 38 ++++++++++++++ 10 files changed, 137 insertions(+), 8 deletions(-) create mode 100644 src/Android/Avalonia.Android/AndroidRuntimePlatform.cs create mode 100644 src/Tizen/Avalonia.Tizen/TizenRuntimePlatform.cs diff --git a/samples/ControlCatalog/Pages/PlatformInfoPage.xaml b/samples/ControlCatalog/Pages/PlatformInfoPage.xaml index f02741d2da..22c47f6bef 100644 --- a/samples/ControlCatalog/Pages/PlatformInfoPage.xaml +++ b/samples/ControlCatalog/Pages/PlatformInfoPage.xaml @@ -19,6 +19,9 @@ + + + diff --git a/src/Android/Avalonia.Android/AndroidPlatform.cs b/src/Android/Avalonia.Android/AndroidPlatform.cs index d8202064e7..b991d8067f 100644 --- a/src/Android/Avalonia.Android/AndroidPlatform.cs +++ b/src/Android/Avalonia.Android/AndroidPlatform.cs @@ -20,7 +20,7 @@ namespace Avalonia public static AppBuilder UseAndroid(this AppBuilder builder) { return builder - .UseStandardRuntimePlatformSubsystem() + .UseAndroidRuntimePlatformSubsystem() .UseWindowingSubsystem(() => AndroidPlatform.Initialize(), "Android") .UseSkia(); } diff --git a/src/Android/Avalonia.Android/AndroidRuntimePlatform.cs b/src/Android/Avalonia.Android/AndroidRuntimePlatform.cs new file mode 100644 index 0000000000..f38492005a --- /dev/null +++ b/src/Android/Avalonia.Android/AndroidRuntimePlatform.cs @@ -0,0 +1,52 @@ +using System; +using Android.Content.PM; +using Android.Content; +using Avalonia.Platform; +using App = Android.App.Application; +using System.Reflection; + +namespace Avalonia +{ + internal static class AndroidRuntimePlatformServices + { + public static AppBuilder UseAndroidRuntimePlatformSubsystem(this AppBuilder builder) + { + builder.UseRuntimePlatformSubsystem(() => Register(builder.ApplicationType?.Assembly), nameof(AndroidRuntimePlatform)); + return builder; + } + + public static void Register(Assembly? assembly = null) + { + AssetLoader.RegisterResUriParsers(); + AvaloniaLocator.CurrentMutable + .Bind().ToSingleton() + .Bind().ToConstant(new StandardAssetLoader(assembly)); + } + } + + + internal class AndroidRuntimePlatform : StandardRuntimePlatform + { + private static readonly Lazy Info = new(() => + { + var isDesktop = IsRunningOnDesktop(App.Context); + var isTv = IsRunningOnTv(App.Context); + + return new RuntimePlatformInfo + { + IsDesktop = isDesktop, + IsMobile = !isTv && !isDesktop, + IsTV = isTv + }; + }); + + private static bool IsRunningOnDesktop(Context context) => + context.PackageManager.HasSystemFeature("org.chromium.arc") || + context.PackageManager.HasSystemFeature("org.chromium.arc.device_management"); + + private static bool IsRunningOnTv(Context context) => + context.PackageManager.HasSystemFeature(PackageManager.FeatureLeanback); + + public override RuntimePlatformInfo GetRuntimeInfo() => Info.Value; + } +} diff --git a/src/Avalonia.Base/Platform/IRuntimePlatform.cs b/src/Avalonia.Base/Platform/IRuntimePlatform.cs index c6efadc99a..565f44ec40 100644 --- a/src/Avalonia.Base/Platform/IRuntimePlatform.cs +++ b/src/Avalonia.Base/Platform/IRuntimePlatform.cs @@ -13,15 +13,17 @@ namespace Avalonia.Platform public record struct RuntimePlatformInfo { public FormFactorType FormFactor => IsDesktop ? FormFactorType.Desktop : - IsMobile ? FormFactorType.Mobile : FormFactorType.Unknown; + IsMobile ? FormFactorType.Mobile : IsTV ? FormFactorType.TV : FormFactorType.Unknown; public bool IsDesktop { get; set; } public bool IsMobile { get; set; } + public bool IsTV { get; set; } } public enum FormFactorType { Unknown, Desktop, - Mobile + Mobile, + TV } } diff --git a/src/Browser/Avalonia.Browser/BrowserRuntimePlatform.cs b/src/Browser/Avalonia.Browser/BrowserRuntimePlatform.cs index eee2b70c9c..8709fd79fd 100644 --- a/src/Browser/Avalonia.Browser/BrowserRuntimePlatform.cs +++ b/src/Browser/Avalonia.Browser/BrowserRuntimePlatform.cs @@ -30,10 +30,12 @@ internal class BrowserRuntimePlatform : StandardRuntimePlatform private static readonly Lazy Info = new(() => { var isMobile = AvaloniaModule.IsMobile(); + var isTv = AvaloniaModule.IsTv(); var result = new RuntimePlatformInfo { - IsMobile = isMobile, - IsDesktop = !isMobile + IsMobile = isMobile && !isTv, + IsDesktop = !isMobile && !isTv, + IsTV = isTv }; return result; diff --git a/src/Browser/Avalonia.Browser/Interop/AvaloniaModule.cs b/src/Browser/Avalonia.Browser/Interop/AvaloniaModule.cs index cb7aabbc39..b48a00919c 100644 --- a/src/Browser/Avalonia.Browser/Interop/AvaloniaModule.cs +++ b/src/Browser/Avalonia.Browser/Interop/AvaloniaModule.cs @@ -33,7 +33,10 @@ internal static partial class AvaloniaModule [JSImport("Caniuse.isMobile", AvaloniaModule.MainModuleName)] public static partial bool IsMobile(); - + + [JSImport("Caniuse.isTv", AvaloniaModule.MainModuleName)] + public static partial bool IsTv(); + [JSImport("registerServiceWorker", AvaloniaModule.MainModuleName)] public static partial void RegisterServiceWorker(string path, string? scope); } diff --git a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/caniuse.ts b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/caniuse.ts index 8fdc3a5c01..54e6900abc 100644 --- a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/caniuse.ts +++ b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/caniuse.ts @@ -14,4 +14,8 @@ export class Caniuse { const regex2 = /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw(n|u)|c55\/|capi|ccwa|cdm|cell|chtm|cldc|cmd|co(mp|nd)|craw|da(it|ll|ng)|dbte|dcs|devi|dica|dmob|do(c|p)o|ds(12|d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(|_)|g1 u|g560|gene|gf5|gmo|go(\.w|od)|gr(ad|un)|haie|hcit|hd(m|p|t)|hei|hi(pt|ta)|hp( i|ip)|hsc|ht(c(| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i(20|go|ma)|i230|iac( ||\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|[a-w])|libw|lynx|m1w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|mcr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|([1-8]|c))|phil|pire|pl(ay|uc)|pn2|po(ck|rt|se)|prox|psio|ptg|qaa|qc(07|12|21|32|60|[2-7]|i)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h|oo|p)|sdk\/|se(c(|0|1)|47|mc|nd|ri)|sgh|shar|sie(|m)|sk0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h|v|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl|tdg|tel(i|m)|tim|tmo|to(pl|sh)|ts(70|m|m3|m5)|tx9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas|your|zeto|zte/i; return regex1.test(userAgent) || regex2.test(userAgent.substr(0, 4)); } + + public static isTv(): boolean { + return navigator.userAgent.includes("SmartTV"); + } } diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/OnFormFactorExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/OnFormFactorExtension.cs index a07595a35b..1ac733a345 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/OnFormFactorExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/OnFormFactorExtension.cs @@ -4,6 +4,7 @@ using Avalonia.Platform; namespace Avalonia.Markup.Xaml.MarkupExtensions; +/// public sealed class OnFormFactorExtension : OnFormFactorExtensionBase { public OnFormFactorExtension() @@ -22,6 +23,7 @@ public sealed class OnFormFactorExtension : OnFormFactorExtensionBase public sealed class OnFormFactorExtension : OnFormFactorExtensionBase> { public OnFormFactorExtension() @@ -40,18 +42,38 @@ public sealed class OnFormFactorExtension : OnFormFactorExtensionBase +/// Provides form factor-specific value for T for the current target device. +/// This extension defines "form-factor" as a "device type" rather than "screen type". +/// public abstract class OnFormFactorExtensionBase : IAddChild where TOn : On { + /// + /// Gets or sets the value applied by default. + /// If not set, default(TReturn) is assigned to the value. + /// [MarkupExtensionDefaultOption] public TReturn? Default { get; set; } + /// + /// Gets or sets the value applied on desktop systems. + /// [MarkupExtensionOption(FormFactorType.Desktop)] public TReturn? Desktop { get; set; } + /// + /// Gets or sets the value applied on mobile systems. + /// [MarkupExtensionOption(FormFactorType.Mobile)] public TReturn? Mobile { get; set; } + /// + /// Gets or sets the value applied on TV systems. + /// + [MarkupExtensionOption(FormFactorType.TV)] + public TReturn? TV { get; set; } + // Required for the compiler, will be replaced with actual method compile time. public object ProvideValue() { return this; } void IAddChild.AddChild(TOn child) {} diff --git a/src/Tizen/Avalonia.Tizen/TizenApplicationExtensions.cs b/src/Tizen/Avalonia.Tizen/TizenApplicationExtensions.cs index 72297a4fc5..f1ee66a494 100644 --- a/src/Tizen/Avalonia.Tizen/TizenApplicationExtensions.cs +++ b/src/Tizen/Avalonia.Tizen/TizenApplicationExtensions.cs @@ -1,4 +1,7 @@ -namespace Avalonia.Tizen; +using System.Reflection; +using Avalonia.Platform; + +namespace Avalonia.Tizen; /// /// Extension to setup app builder with tizen backend @@ -13,8 +16,8 @@ public static class TizenApplicationExtensions public static AppBuilder UseTizen(this AppBuilder builder) { return builder + .UseTizenRuntimePlatformSubsystem() .UseWindowingSubsystem(TizenPlatform.Initialize, "Tizen") - .UseStandardRuntimePlatformSubsystem() .UseSkia(); } } diff --git a/src/Tizen/Avalonia.Tizen/TizenRuntimePlatform.cs b/src/Tizen/Avalonia.Tizen/TizenRuntimePlatform.cs new file mode 100644 index 0000000000..b28db8dff8 --- /dev/null +++ b/src/Tizen/Avalonia.Tizen/TizenRuntimePlatform.cs @@ -0,0 +1,38 @@ +using System.Reflection; +using Avalonia.Platform; + +namespace Avalonia.Tizen; + +internal static class TizenRuntimePlatformServices +{ + public static AppBuilder UseTizenRuntimePlatformSubsystem(this AppBuilder builder) + { + builder.UseRuntimePlatformSubsystem(() => Register(builder.ApplicationType?.Assembly), nameof(TizenRuntimePlatform)); + return builder; + } + + public static void Register(Assembly? assembly = null) + { + AssetLoader.RegisterResUriParsers(); + AvaloniaLocator.CurrentMutable + .Bind().ToSingleton() + .Bind().ToConstant(new StandardAssetLoader(assembly)); + } +} + +internal class TizenRuntimePlatform : StandardRuntimePlatform +{ + private static readonly Lazy Info = new(() => + { + global::Tizen.System.Information.TryGetValue("http://tizen.org/feature/profile", out string profile); + + return new RuntimePlatformInfo + { + IsMobile = profile.Equals("mobile", StringComparison.OrdinalIgnoreCase), + IsTV = profile.Equals("tv", StringComparison.OrdinalIgnoreCase), + IsDesktop = false + }; + }); + + public override RuntimePlatformInfo GetRuntimeInfo() => Info.Value; +} From 2a7b5568b7f61fe6df9c46a05bf1c9d474c9290e Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Tue, 16 Jan 2024 00:01:50 +0100 Subject: [PATCH 02/14] Make sure non text runs are properly ordered by the bidi reorder logic (#14183) --- .../Media/TextFormatting/BidiReorderer.cs | 29 +++++++++++++++---- .../Media/TextFormatting/TextFormatterImpl.cs | 8 +++-- .../TextFormatting/TextFormatterTests.cs | 29 +++++++++++++++++++ 3 files changed, 58 insertions(+), 8 deletions(-) diff --git a/src/Avalonia.Base/Media/TextFormatting/BidiReorderer.cs b/src/Avalonia.Base/Media/TextFormatting/BidiReorderer.cs index 39ef8cce48..85fea48edb 100644 --- a/src/Avalonia.Base/Media/TextFormatting/BidiReorderer.cs +++ b/src/Avalonia.Base/Media/TextFormatting/BidiReorderer.cs @@ -30,18 +30,25 @@ namespace Avalonia.Media.TextFormatting try { + sbyte? previousLevel = null; + _runs.Add(textRuns.Length); // Build up the collection of ordered runs. for (var i = 0; i < textRuns.Length; i++) { var textRun = textRuns[i]; - _runs[i] = new OrderedBidiRun(i, textRun, GetRunBidiLevel(textRun, flowDirection)); + + var orderedRun = new OrderedBidiRun(i, textRun, GetRunBidiLevel(textRun, flowDirection, previousLevel)); + + _runs[i] = orderedRun; if (i > 0) { _runs[i - 1].NextRunIndex = i; } + + previousLevel = orderedRun.Level; } // Reorder them into visual order. @@ -72,7 +79,8 @@ namespace Avalonia.Media.TextFormatting for (var i = 0; i < textRuns.Length; i++) { - var level = GetRunBidiLevel(textRuns[i], flowDirection); + var level = _runs[i].Level; + if (level > max) { max = level; @@ -150,15 +158,26 @@ namespace Avalonia.Media.TextFormatting } } - private static sbyte GetRunBidiLevel(TextRun run, FlowDirection flowDirection) + private static sbyte GetRunBidiLevel(TextRun run, FlowDirection flowDirection, sbyte? previousLevel) { if (run is ShapedTextRun shapedTextRun) { return shapedTextRun.BidiLevel; } - var defaultLevel = flowDirection == FlowDirection.LeftToRight ? 0 : 1; - return (sbyte)defaultLevel; + var defaultLevel = (sbyte)(flowDirection == FlowDirection.LeftToRight ? 0 : 1); + + if (run is TextEndOfLine) + { + return defaultLevel; + } + + if(previousLevel is not null) + { + return previousLevel.Value; + } + + return defaultLevel; } /// diff --git a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs index 694961cb94..1b477db1a8 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs @@ -12,7 +12,7 @@ namespace Avalonia.Media.TextFormatting internal sealed class TextFormatterImpl : TextFormatter { private static readonly char[] s_empty = { ' ' }; - private static readonly char[] s_defaultText = new char[TextRun.DefaultTextSourceLength]; + private static readonly string s_defaultText = new string('a', TextRun.DefaultTextSourceLength); [ThreadStatic] private static BidiData? t_bidiData; [ThreadStatic] private static BidiAlgorithm? t_bidiAlgorithm; @@ -206,9 +206,11 @@ namespace Avalonia.Media.TextFormatting if (!textRun.Text.IsEmpty) text = textRun.Text.Span; else if (textRun.Length == TextRun.DefaultTextSourceLength) - text = s_defaultText; + text = s_defaultText.AsSpan(); else - text = new char[textRun.Length]; + { + text = new string('a', textRun.Length).AsSpan(); + } bidiData.Append(text); } diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs index f80c89e23d..83c5fc377c 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs @@ -135,6 +135,35 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting } } + [Fact] + public void Should_Format_TextLine_With_Non_Text_TextRuns_RightToLeft() + { + using (Start()) + { + var defaultProperties = + new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: Brushes.Black); + + var textSource = new TextSourceWithDummyRuns(defaultProperties); + + var formatter = new TextFormatterImpl(); + + var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity, + new GenericTextParagraphProperties(FlowDirection.RightToLeft, TextAlignment.Left, true, true, defaultProperties, TextWrapping.NoWrap, 0, 0, 0)); + + Assert.NotNull(textLine); + + Assert.Equal(5, textLine.TextRuns.Count); + + Assert.Equal(14, textLine.Length); + + var second = textLine.TextRuns[1] as ShapedTextRun; + + Assert.NotNull(second); + + Assert.Equal("Hello".AsMemory(), second.Text); + } + } + [Fact] public void Should_Format_TextRuns_With_TextRunStyles() { From e700e12f0b040b37d6eda69f3bd39cb9ee06a4ef Mon Sep 17 00:00:00 2001 From: workgroupengineering Date: Tue, 16 Jan 2024 19:41:15 +0100 Subject: [PATCH 03/14] fix(Geometries): Relative line drawing (#14013) * test: Add invalid draw test * fix: Geometry parsing * test: Refactoring the test for address `h` and `v` command * fix: Parser of `h` command * fix: parser `v` command * test: Add case `M50,50z l -5,-5` * test: Add PathMarkupParserTests * test: Removed render tests --- src/Avalonia.Base/Media/PathMarkupParser.cs | 15 +++++++++------ .../Media/PathMarkupParserTests.cs | 19 +++++++++++++++++++ 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Base/Media/PathMarkupParser.cs b/src/Avalonia.Base/Media/PathMarkupParser.cs index 7b9fdf9330..fa790c17c0 100644 --- a/src/Avalonia.Base/Media/PathMarkupParser.cs +++ b/src/Avalonia.Base/Media/PathMarkupParser.cs @@ -259,7 +259,7 @@ namespace Avalonia.Media { ThrowIfDisposed(); - _currentPoint = relative + var next = relative ? ReadRelativePoint(ref span, _currentPoint) : ReadPoint(ref span); @@ -268,14 +268,15 @@ namespace Avalonia.Media CreateFigure(); } - _geometryContext.LineTo(_currentPoint); + _geometryContext.LineTo(next); + _currentPoint = next; } private void AddHorizontalLine(ref ReadOnlySpan span, bool relative) { ThrowIfDisposed(); - _currentPoint = relative + var next = relative ? new Point(_currentPoint.X + ReadDouble(ref span), _currentPoint.Y) : _currentPoint.WithX(ReadDouble(ref span)); @@ -284,14 +285,15 @@ namespace Avalonia.Media CreateFigure(); } - _geometryContext.LineTo(_currentPoint); + _geometryContext.LineTo(next); + _currentPoint = next; } private void AddVerticalLine(ref ReadOnlySpan span, bool relative) { ThrowIfDisposed(); - _currentPoint = relative + var next = relative ? new Point(_currentPoint.X, _currentPoint.Y + ReadDouble(ref span)) : _currentPoint.WithY(ReadDouble(ref span)); @@ -300,7 +302,8 @@ namespace Avalonia.Media CreateFigure(); } - _geometryContext.LineTo(_currentPoint); + _geometryContext.LineTo(next); + _currentPoint = next; } private void AddCubicBezierCurve(ref ReadOnlySpan span, bool relative) diff --git a/tests/Avalonia.Base.UnitTests/Media/PathMarkupParserTests.cs b/tests/Avalonia.Base.UnitTests/Media/PathMarkupParserTests.cs index c829690eb4..755ab7ff25 100644 --- a/tests/Avalonia.Base.UnitTests/Media/PathMarkupParserTests.cs +++ b/tests/Avalonia.Base.UnitTests/Media/PathMarkupParserTests.cs @@ -319,5 +319,24 @@ namespace Avalonia.Base.UnitTests.Media Assert.IsType(arcSegment); } } + + [Fact] + public void Should_Handle_StartPoint_After_Empty_Figure() + { + var pathGeometry = new PathGeometry(); + using var context = new PathGeometryContext(pathGeometry); + using var parser = new PathMarkupParser(context); + parser.Parse("M50,50z l -5,-5"); + + Assert.Equal(2, pathGeometry.Figures.Count); + + var firstFigure = pathGeometry.Figures[0]; + + Assert.Equal(new Point(50, 50), firstFigure.StartPoint); + + var secondFigure = pathGeometry.Figures[1]; + + Assert.Equal(new Point(50, 50), secondFigure.StartPoint); + } } } From eddd7df39536367721fc0bc74b33223b631866c0 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 17 Jan 2024 06:27:05 +0100 Subject: [PATCH 04/14] Fix Adorner Layer Clipping (#13921) * Don't overwrite IsClipEnabled. The user should be able to set this on an adorner themselves. * Use correct clip bounds for adorners. Only take the adorned visual's clip bounds into account if the adorner is clipped to these bounds. * Disable clipping on GridSplitter adorner. Fixes #10700 * Don't clip adorners in ControlCatalog. Default setting is `true`, but this was being overridden in `AdornerLayer.AddVisualAdorner`. That was fixed by an earlier commit in this PR, so property needs to be set explicitly to false in ControlCatalog. --- samples/ControlCatalog/Pages/AdornerLayerPage.xaml | 3 ++- .../Rendering/Composition/Server/ServerCompositionVisual.cs | 2 +- src/Avalonia.Controls/GridSplitter.cs | 1 + src/Avalonia.Controls/Primitives/AdornerLayer.cs | 1 - 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/samples/ControlCatalog/Pages/AdornerLayerPage.xaml b/samples/ControlCatalog/Pages/AdornerLayerPage.xaml index 598844d695..7501c80940 100644 --- a/samples/ControlCatalog/Pages/AdornerLayerPage.xaml +++ b/samples/ControlCatalog/Pages/AdornerLayerPage.xaml @@ -50,7 +50,8 @@ Background="Cyan" IsHitTestVisible="False" Opacity="0.3" - IsVisible="True"> + IsVisible="True" + AdornerLayer.IsClipEnabled="False"> diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs index aeb228282e..d7bdde11e9 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs @@ -205,7 +205,7 @@ namespace Avalonia.Rendering.Composition.Server } _combinedTransformedClipBounds = - AdornedVisual?._combinedTransformedClipBounds + (AdornerIsClipped ? AdornedVisual?._combinedTransformedClipBounds : null) ?? (Parent?.Effect == null ? Parent?._combinedTransformedClipBounds : null) ?? new Rect(Root!.Size); diff --git a/src/Avalonia.Controls/GridSplitter.cs b/src/Avalonia.Controls/GridSplitter.cs index 234c1ff27a..b9e77ca6db 100644 --- a/src/Avalonia.Controls/GridSplitter.cs +++ b/src/Avalonia.Controls/GridSplitter.cs @@ -341,6 +341,7 @@ namespace Avalonia.Controls _resizeData.Adorner = new PreviewAdorner(builtPreviewContent); AdornerLayer.SetAdornedElement(_resizeData.Adorner, this); + AdornerLayer.SetIsClipEnabled(_resizeData.Adorner, false); adornerLayer.Children.Add(_resizeData.Adorner); diff --git a/src/Avalonia.Controls/Primitives/AdornerLayer.cs b/src/Avalonia.Controls/Primitives/AdornerLayer.cs index f21b608667..412dd236ff 100644 --- a/src/Avalonia.Controls/Primitives/AdornerLayer.cs +++ b/src/Avalonia.Controls/Primitives/AdornerLayer.cs @@ -174,7 +174,6 @@ namespace Avalonia.Controls.Primitives } SetAdornedElement(adorner, visual); - SetIsClipEnabled(adorner, false); ((ISetLogicalParent) adorner).SetParent(visual); layer.Children.Add(adorner); From bc1900c1b2728ced957139c11e370950ccc578a7 Mon Sep 17 00:00:00 2001 From: lindexi Date: Wed, 17 Jan 2024 12:27:30 +0800 Subject: [PATCH 05/14] Remove unuse variable. (#14226) --- .../Input/LibInput/LibInputBackend.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs b/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs index fba9862a85..1a22bf06e0 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs @@ -29,9 +29,6 @@ namespace Avalonia.LinuxFramebuffer.Input.LibInput { var fd = libinput_get_fd(ctx); - var timeval = stackalloc IntPtr[2]; - - foreach (var f in Directory.GetFiles("/dev/input", "event*")) libinput_path_add_device(ctx, f); while (true) From 1c792b294f82b26396e4f91032a2b535ea9c0669 Mon Sep 17 00:00:00 2001 From: AtomCrafty Date: Wed, 17 Jan 2024 06:34:33 +0100 Subject: [PATCH 06/14] show adorner while hovering any part of the control's TreeViewItem in DevTools (#14231) --- .../Diagnostics/Views/TreePageView.xaml | 7 ++-- .../Diagnostics/Views/TreePageView.xaml.cs | 41 +++++++++++++++---- 2 files changed, 35 insertions(+), 13 deletions(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml index ecdd46dd74..0a94d428c1 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml @@ -7,13 +7,12 @@ + SelectedItem="{Binding SelectedNode, Mode=TwoWay}" + PointerMoved="UpdateAdorner"> - + diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs index b0aea64994..485216234d 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs @@ -1,14 +1,11 @@ -using System; -using System.Diagnostics; using System.Linq; using Avalonia.Controls; -using Avalonia.Controls.Generators; using Avalonia.Controls.Primitives; using Avalonia.Diagnostics.ViewModels; using Avalonia.Input; +using Avalonia.LogicalTree; using Avalonia.Markup.Xaml; using Avalonia.Media; -using Avalonia.VisualTree; namespace Avalonia.Diagnostics.Views { @@ -16,6 +13,7 @@ namespace Avalonia.Diagnostics.Views { private readonly Panel _adorner; private AdornerLayer? _currentLayer; + private TreeViewItem? _hovered; private TreeView _tree; public TreePageView() @@ -39,6 +37,11 @@ namespace Avalonia.Diagnostics.Views AdornerLayer.SetIsClipEnabled(_adorner, false); } + private static Thickness InvertThickness(Thickness input) + { + return new Thickness(-input.Left, -input.Top, -input.Right, -input.Bottom); + } + protected void AddAdorner(object? sender, PointerEventArgs e) { var node = (TreeNode?)((Control)sender!).DataContext; @@ -80,11 +83,6 @@ namespace Avalonia.Diagnostics.Views } } - private static Thickness InvertThickness(Thickness input) - { - return new Thickness(-input.Left, -input.Top, -input.Right, -input.Bottom); - } - protected void RemoveAdorner(object? sender, PointerEventArgs e) { foreach (var border in _adorner.Children.OfType()) @@ -98,6 +96,31 @@ namespace Avalonia.Diagnostics.Views _currentLayer = null; } + protected void UpdateAdorner(object? sender, PointerEventArgs e) + { + if (e.Source is not StyledElement source) + { + return; + } + + var item = source.FindLogicalAncestorOfType(); + if (item == _hovered) + { + return; + } + + RemoveAdorner(sender, e); + + if (item is null || item.TreeViewOwner != _tree) + { + _hovered = null; + return; + } + + _hovered = item; + AddAdorner(item, e); + } + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); From 26d52457cc1475c3edab6c7c2efc40c5ad2758ec Mon Sep 17 00:00:00 2001 From: BenniSim Date: Fri, 19 Jan 2024 00:51:51 +0100 Subject: [PATCH 07/14] Fix segmentation fault error when using Linux Framebuffer DRM mode with AOT compilation (#14266) Co-authored-by: Benjamin Simonsson --- src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs index c91498fe05..1cea3ac9ba 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs @@ -121,7 +121,7 @@ namespace Avalonia.LinuxFramebuffer.Output // prepare for the new ioctl call var handles = new uint[] {handle, 0, 0, 0}; var pitches = new uint[] {stride, 0, 0, 0}; - var offsets = Array.Empty(); + var offsets = new uint[4]; var ret = drmModeAddFB2(_card.Fd, w, h, format, handles, pitches, offsets, out var fbHandle, 0); From f6bda07ae6557b841d1c0bb9bd267153f2d28911 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Mutnia=C5=84ski?= <45796706+RobertMut@users.noreply.github.com> Date: Fri, 19 Jan 2024 00:53:53 +0100 Subject: [PATCH 08/14] remove coercecaretindex (#14264) --- .../AutoCompleteBox/AutoCompleteBox.Properties.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.Properties.cs b/src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.Properties.cs index bcee7f36e3..05b08af31f 100644 --- a/src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.Properties.cs +++ b/src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.Properties.cs @@ -22,8 +22,7 @@ namespace Avalonia.Controls public static readonly StyledProperty CaretIndexProperty = TextBox.CaretIndexProperty.AddOwner(new( defaultValue: 0, - defaultBindingMode:BindingMode.TwoWay, - coerce: TextBox.CoerceCaretIndex)); + defaultBindingMode:BindingMode.TwoWay)); public static readonly StyledProperty WatermarkProperty = TextBox.WatermarkProperty.AddOwner(); From c3911177956897d7a5176f9d59932b308aebc0c9 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Thu, 18 Jan 2024 23:07:57 -0800 Subject: [PATCH 09/14] Remove "netcoreapp2.0" target from the Avalonia. Make previewer use netstandard2.0 (#14268) * Remove "netcoreapp2.0" target from the Avalonia. Make previewer use netstandard2.0 * Update ApiDiff redirects --- Directory.Build.props | 2 +- nukebuild/ApiDiffHelper.cs | 4 +++- packages/Avalonia/Avalonia.csproj | 8 ++++---- packages/Avalonia/Avalonia.props | 2 +- samples/Directory.Build.props | 2 +- .../Avalonia.Markup.Xaml.Loader.csproj | 15 ++------------- .../IncludeXamlIlSre.props | 17 +++++++++++++++++ .../Avalonia.Designer.HostApp.csproj | 19 ++++--------------- .../DesignXamlLoader.cs | 2 ++ .../Avalonia.Designer.HostApp/Program.cs | 2 ++ .../DesignerSupportTests.cs | 2 +- 11 files changed, 38 insertions(+), 37 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index c19a55e8ea..ec7a7ed18b 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -2,7 +2,7 @@ $(MSBuildThisFileDirectory)build-intermediate/nuget - $(MSBuildThisFileDirectory)\src\tools\Avalonia.Designer.HostApp\bin\$(Configuration)\netcoreapp2.0\Avalonia.Designer.HostApp.dll + $(MSBuildThisFileDirectory)\src\tools\Avalonia.Designer.HostApp\bin\$(Configuration)\netstandard2.0\Avalonia.Designer.HostApp.dll false false diff --git a/nukebuild/ApiDiffHelper.cs b/nukebuild/ApiDiffHelper.cs index 946477d287..9af49533a2 100644 --- a/nukebuild/ApiDiffHelper.cs +++ b/nukebuild/ApiDiffHelper.cs @@ -105,7 +105,9 @@ public static class ApiDiffHelper { // We use StartsWith below comparing these tfm, as we ignore platform versions (like, net6.0-ios16.1) ("net6.0-android", "net7.0-android"), - ("net6.0-ios", "net7.0-ios") + ("net6.0-ios", "net7.0-ios"), + // Designer was moved from netcoreapp to netstandard + ("netcoreapp2.0", "netstandard2.0") }; public static async Task ValidatePackage( diff --git a/packages/Avalonia/Avalonia.csproj b/packages/Avalonia/Avalonia.csproj index 333b21718d..6a3b06ee6c 100644 --- a/packages/Avalonia/Avalonia.csproj +++ b/packages/Avalonia/Avalonia.csproj @@ -1,6 +1,6 @@  - net6.0;netstandard2.0;net461;netcoreapp2.0 + net6.0;netstandard2.0;net461 Avalonia @@ -27,11 +27,11 @@ - + - <_PackageFiles Include="$(DesignerHostAppPath)/Avalonia.Designer.HostApp/bin/$(Configuration)/netcoreapp2.0/Avalonia.Designer.HostApp.dll"> - tools/netcoreapp2.0/designer + <_PackageFiles Include="$(DesignerHostAppPath)/Avalonia.Designer.HostApp/bin/$(Configuration)/netstandard2.0/Avalonia.Designer.HostApp.dll"> + tools/netstandard2.0/designer false None diff --git a/packages/Avalonia/Avalonia.props b/packages/Avalonia/Avalonia.props index 19190dd43a..259349452b 100644 --- a/packages/Avalonia/Avalonia.props +++ b/packages/Avalonia/Avalonia.props @@ -1,6 +1,6 @@ - $(MSBuildThisFileDirectory)\..\tools\netcoreapp2.0\designer\Avalonia.Designer.HostApp.dll + $(MSBuildThisFileDirectory)\..\tools\netstandard2.0\designer\Avalonia.Designer.HostApp.dll $(MSBuildThisFileDirectory)\..\tools\net461\designer\Avalonia.Designer.HostApp.exe $(MSBuildThisFileDirectory)\..\tools\netstandard2.0\Avalonia.Build.Tasks.dll false diff --git a/samples/Directory.Build.props b/samples/Directory.Build.props index c8216c3031..680d6d2b89 100644 --- a/samples/Directory.Build.props +++ b/samples/Directory.Build.props @@ -2,7 +2,7 @@ false - $(MSBuildThisFileDirectory)..\src\tools\Avalonia.Designer.HostApp\bin\Debug\netcoreapp2.0\Avalonia.Designer.HostApp.dll + $(MSBuildThisFileDirectory)..\src\tools\Avalonia.Designer.HostApp\bin\Debug\netstandard2.0\Avalonia.Designer.HostApp.dll false 11 $(NoWarn);CS8002 diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/Avalonia.Markup.Xaml.Loader.csproj b/src/Markup/Avalonia.Markup.Xaml.Loader/Avalonia.Markup.Xaml.Loader.csproj index bb18dd5ee3..532f75e8eb 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/Avalonia.Markup.Xaml.Loader.csproj +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/Avalonia.Markup.Xaml.Loader.csproj @@ -5,22 +5,11 @@ true $(DefineConstants);XAMLX_INTERNAL;XAML_RUNTIME_LOADER - - - false - - - - - - - - - - + + diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/IncludeXamlIlSre.props b/src/Markup/Avalonia.Markup.Xaml.Loader/IncludeXamlIlSre.props index 251f84d304..173af923cb 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/IncludeXamlIlSre.props +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/IncludeXamlIlSre.props @@ -1,9 +1,26 @@ + + + false + + + + + + + + + + + + + + diff --git a/src/tools/Avalonia.Designer.HostApp/Avalonia.Designer.HostApp.csproj b/src/tools/Avalonia.Designer.HostApp/Avalonia.Designer.HostApp.csproj index ead954d641..d904684ffb 100644 --- a/src/tools/Avalonia.Designer.HostApp/Avalonia.Designer.HostApp.csproj +++ b/src/tools/Avalonia.Designer.HostApp/Avalonia.Designer.HostApp.csproj @@ -1,31 +1,20 @@  Exe - net461;netcoreapp2.0 + net461;netstandard2.0;net6.0 $(DefineConstants);XAMLX_INTERNAL - - - false - - - - - - - - - - - + + + diff --git a/src/tools/Avalonia.Designer.HostApp/DesignXamlLoader.cs b/src/tools/Avalonia.Designer.HostApp/DesignXamlLoader.cs index 690926a193..7752518958 100644 --- a/src/tools/Avalonia.Designer.HostApp/DesignXamlLoader.cs +++ b/src/tools/Avalonia.Designer.HostApp/DesignXamlLoader.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Reflection; @@ -9,6 +10,7 @@ using Avalonia.Markup.Xaml.XamlIl; namespace Avalonia.Designer.HostApp; +[RequiresUnreferencedCode(XamlX.TrimmingMessages.DynamicXamlReference)] class DesignXamlLoader : AvaloniaXamlLoader.IRuntimeXamlLoader { public object Load(RuntimeXamlLoaderDocument document, RuntimeXamlLoaderConfiguration configuration) diff --git a/src/tools/Avalonia.Designer.HostApp/Program.cs b/src/tools/Avalonia.Designer.HostApp/Program.cs index 43e6101446..9e99acef86 100644 --- a/src/tools/Avalonia.Designer.HostApp/Program.cs +++ b/src/tools/Avalonia.Designer.HostApp/Program.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Reflection; using Avalonia.DesignerSupport; @@ -6,6 +7,7 @@ using Avalonia.Markup.Xaml; namespace Avalonia.Designer.HostApp { + [RequiresUnreferencedCode(XamlX.TrimmingMessages.DynamicXamlReference)] class Program { #if NETFRAMEWORK diff --git a/tests/Avalonia.DesignerSupport.Tests/DesignerSupportTests.cs b/tests/Avalonia.DesignerSupport.Tests/DesignerSupportTests.cs index 6e32158fcb..9003a0a8f9 100644 --- a/tests/Avalonia.DesignerSupport.Tests/DesignerSupportTests.cs +++ b/tests/Avalonia.DesignerSupport.Tests/DesignerSupportTests.cs @@ -19,7 +19,7 @@ namespace Avalonia.DesignerSupport.Tests { public class DesignerSupportTests { - private const string DesignerAppPath = "../../../../../src/tools/Avalonia.Designer.HostApp/bin/$BUILD/netcoreapp2.0/Avalonia.Designer.HostApp.dll"; + private const string DesignerAppPath = "../../../../../src/tools/Avalonia.Designer.HostApp/bin/$BUILD/netstandard2.0/Avalonia.Designer.HostApp.dll"; private readonly Xunit.Abstractions.ITestOutputHelper outputHelper; public DesignerSupportTests(Xunit.Abstractions.ITestOutputHelper outputHelper) From 6a71056686d531476418ef91d075260340d14b20 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 19 Jan 2024 18:38:41 -0800 Subject: [PATCH 10/14] Make Classic ApplicationLifetime API a bit more reliable (#14267) * Add StartWithClassicDesktopLifetime overload with a lifetime builder * Disallow changing Application.ApplicationLifetime after setup was completed * Avoid static dependency on a singleton lifetime * Introduce SetupWithClassicDesktopLifetime method * Move more logic from Start method to Setup * Add docs * Avoid public API changes * Fix tests * Repalce locator usage with `.UseLifetimeOverride` --------- Co-authored-by: Benedikt Stebner --- src/Avalonia.Controls/AppBuilder.cs | 13 ++ src/Avalonia.Controls/Application.cs | 27 +++- .../ClassicDesktopStyleApplicationLifetime.cs | 134 ++++++++++++------ .../AvaloniaNativePlatformExtensions.cs | 25 ++-- .../DesktopStyleApplicationLifetimeTests.cs | 23 +++ 5 files changed, 158 insertions(+), 64 deletions(-) diff --git a/src/Avalonia.Controls/AppBuilder.cs b/src/Avalonia.Controls/AppBuilder.cs index 436e618a6b..3275f35989 100644 --- a/src/Avalonia.Controls/AppBuilder.cs +++ b/src/Avalonia.Controls/AppBuilder.cs @@ -6,6 +6,7 @@ using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Platform; using Avalonia.Media.Fonts; using Avalonia.Media; +using Avalonia.Metadata; namespace Avalonia { @@ -54,6 +55,11 @@ namespace Avalonia /// public Action? RenderingSubsystemInitializer { get; private set; } + /// + /// Gets a method to override a lifetime factory. + /// + public Func? LifetimeOverride { get; private set; } + /// /// Gets the name of the currently selected rendering subsystem. /// @@ -238,6 +244,13 @@ namespace Avalonia return Self; } + [PrivateApi] + public AppBuilder UseLifetimeOverride(Func func) + { + LifetimeOverride = func; + return Self; + } + /// /// Configures platform-specific options /// diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index 44de1c50eb..46b31bff3e 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -42,6 +42,8 @@ namespace Avalonia private bool _notifyingResourcesChanged; private Action>? _stylesAdded; private Action>? _stylesRemoved; + private IApplicationLifetime? _applicationLifetime; + private bool _setupCompleted; /// /// Defines the property. @@ -60,7 +62,7 @@ namespace Avalonia /// public event EventHandler? ResourcesChanged; - /// + [Obsolete("Cast ApplicationLifetime to IActivatableApplicationLifetime instead.")] public event EventHandler? UrlsOpened; /// @@ -170,15 +172,28 @@ namespace Avalonia /// bool IStyleHost.IsStylesInitialized => _styles != null; - + /// /// Application lifetime, use it for things like setting the main window and exiting the app from code /// Currently supported lifetimes are: /// - /// - /// - + /// - /// - public IApplicationLifetime? ApplicationLifetime { get; set; } + public IApplicationLifetime? ApplicationLifetime + { + get => _applicationLifetime; + set + { + if (_setupCompleted) + { + throw new InvalidOperationException($"It's not possible to change {nameof(ApplicationLifetime)} after Application was initialized."); + } + + _applicationLifetime = value; + } + } /// /// Represents a contract for accessing global platform-specific settings. @@ -207,7 +222,7 @@ namespace Avalonia /// Initializes the application by loading XAML etc. /// public virtual void Initialize() { } - + /// public bool TryGetResource(object key, ThemeVariant? theme, out object? value) { @@ -263,13 +278,15 @@ namespace Avalonia AvaloniaLocator.CurrentMutable.Bind() .ToConstant(MediaContext.Instance.Clock); + + _setupCompleted = true; } public virtual void OnFrameworkInitializationCompleted() { } - void IApplicationPlatformEvents.RaiseUrlsOpened(string[] urls) + void IApplicationPlatformEvents.RaiseUrlsOpened(string[] urls) { UrlsOpened?.Invoke(this, new UrlOpenedEventArgs (urls)); } diff --git a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs index f235d0e2cf..2c467853d4 100644 --- a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs +++ b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs @@ -8,6 +8,7 @@ using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Interactivity; using Avalonia.Platform; +using Avalonia.Reactive; using Avalonia.Threading; namespace Avalonia.Controls.ApplicationLifetimes @@ -18,38 +19,7 @@ namespace Avalonia.Controls.ApplicationLifetimes private CancellationTokenSource? _cts; private bool _isShuttingDown; private readonly AvaloniaList _windows = new(); - - private static ClassicDesktopStyleApplicationLifetime? s_activeLifetime; - - static ClassicDesktopStyleApplicationLifetime() - { - Window.WindowOpenedEvent.AddClassHandler(typeof(Window), OnWindowOpened); - Window.WindowClosedEvent.AddClassHandler(typeof(Window), OnWindowClosed); - } - - private static void OnWindowClosed(object? sender, RoutedEventArgs e) - { - var window = (Window)sender!; - s_activeLifetime?._windows.Remove(window); - s_activeLifetime?.HandleWindowClosed(window); - } - - private static void OnWindowOpened(object? sender, RoutedEventArgs e) - { - var window = (Window)sender!; - if (s_activeLifetime is not null && !s_activeLifetime._windows.Contains(window)) - { - s_activeLifetime._windows.Add(window); - } - } - - public ClassicDesktopStyleApplicationLifetime() - { - if (s_activeLifetime != null) - throw new InvalidOperationException( - "Can not have multiple active ClassicDesktopStyleApplicationLifetime instances and the previously created one was not disposed"); - s_activeLifetime = this; - } + private CompositeDisposable? _compositeDisposable; /// public event EventHandler? Startup; @@ -97,9 +67,32 @@ namespace Avalonia.Controls.ApplicationLifetimes { return DoShutdown(new ShutdownRequestedEventArgs(), true, false, exitCode); } - - public int Start(string[] args) + + internal void SetupCore(string[] args) { + if (_compositeDisposable is not null) + { + // There could be a case, when lifetime was setup without starting. + // Until developer started it manually later. To avoid API breaking changes, it will execute Setup method twice. + return; + } + + _compositeDisposable = new CompositeDisposable( + Window.WindowOpenedEvent.AddClassHandler(typeof(Window), (sender, _) => + { + var window = (Window)sender!; + if (!_windows.Contains(window)) + { + _windows.Add(window); + } + }), + Window.WindowClosedEvent.AddClassHandler(typeof(Window), (sender, _) => + { + var window = (Window)sender!; + _windows.Remove(window); + HandleWindowClosed(window); + })); + Startup?.Invoke(this, new ControlledApplicationLifetimeStartupEventArgs(args)); var options = AvaloniaLocator.Current.GetService(); @@ -116,9 +109,14 @@ namespace Avalonia.Controls.ApplicationLifetimes if (lifetimeEvents != null) lifetimeEvents.ShutdownRequested += OnShutdownRequested; + } - _cts = new CancellationTokenSource(); + public int Start(string[] args) + { + SetupCore(args); + _cts = new CancellationTokenSource(); + // Note due to a bug in the JIT we wrap this in a method, otherwise MainWindow // gets stuffed into a local var and can not be GCed until after the program stops. // this method never exits until program end. @@ -137,8 +135,8 @@ namespace Avalonia.Controls.ApplicationLifetimes public void Dispose() { - if (s_activeLifetime == this) - s_activeLifetime = null; + _compositeDisposable?.Dispose(); + _compositeDisposable = null; } private bool DoShutdown( @@ -206,21 +204,65 @@ namespace Avalonia.Controls.ApplicationLifetimes namespace Avalonia { + /// + /// IClassicDesktopStyleApplicationLifetime related AppBuilder extensions. + /// public static class ClassicDesktopStyleApplicationLifetimeExtensions { - public static int StartWithClassicDesktopLifetime( - this AppBuilder builder, string[] args, ShutdownMode shutdownMode = ShutdownMode.OnLastWindowClose) + private static ClassicDesktopStyleApplicationLifetime PrepareLifetime(AppBuilder builder, string[] args, + Action? lifetimeBuilder) { - var lifetime = AvaloniaLocator.Current.GetService(); - - if (lifetime == null) - { - lifetime = new ClassicDesktopStyleApplicationLifetime(); - } + var lifetime = builder.LifetimeOverride?.Invoke(typeof(ClassicDesktopStyleApplicationLifetime)) as ClassicDesktopStyleApplicationLifetime + ?? new ClassicDesktopStyleApplicationLifetime(); lifetime.Args = args; - lifetime.ShutdownMode = shutdownMode; + lifetimeBuilder?.Invoke(lifetime); + + return lifetime; + } + + /// + /// Setups the Application with a IClassicDesktopStyleApplicationLifetime, but doesn't show the main window and doesn't run application main loop. + /// + /// Application builder. + /// Startup arguments. + /// Lifetime builder to modify the lifetime before application started. + /// Exit code. + public static AppBuilder SetupWithClassicDesktopLifetime(this AppBuilder builder, string[] args, + Action? lifetimeBuilder = null) + { + var lifetime = PrepareLifetime(builder, args, lifetimeBuilder); + lifetime.SetupCore(args); + return builder.SetupWithLifetime(lifetime); + } + + /// + /// Starts the Application with a IClassicDesktopStyleApplicationLifetime, shows main window and runs application main loop. + /// + /// Application builder. + /// Startup arguments. + /// Lifetime builder to modify the lifetime before application started. + /// Exit code. + public static int StartWithClassicDesktopLifetime( + this AppBuilder builder, string[] args, + Action? lifetimeBuilder = null) + { + var lifetime = PrepareLifetime(builder, args, lifetimeBuilder); + builder.SetupWithLifetime(lifetime); + return lifetime.Start(args); + } + /// + /// Starts the Application with a IClassicDesktopStyleApplicationLifetime, shows main window and runs application main loop. + /// + /// Application builder. + /// Startup arguments. + /// Lifetime shutdown mode. + /// Exit code. + public static int StartWithClassicDesktopLifetime( + this AppBuilder builder, string[] args, ShutdownMode shutdownMode) + { + var lifetime = PrepareLifetime(builder, args, l => l.ShutdownMode = shutdownMode); builder.SetupWithLifetime(lifetime); return lifetime.Start(args); } diff --git a/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs b/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs index ccd358ff8e..1956e214fb 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs @@ -13,20 +13,19 @@ namespace Avalonia builder .UseStandardRuntimePlatformSubsystem() .UseWindowingSubsystem(() => - { - var platform = AvaloniaNativePlatform.Initialize( - AvaloniaLocator.Current.GetService() ?? - new AvaloniaNativePlatformOptions()); + { + var platform = AvaloniaNativePlatform.Initialize( + AvaloniaLocator.Current.GetService() ?? + new AvaloniaNativePlatformOptions()); - builder.AfterSetup (x=> - { - platform.SetupApplicationName(); - platform.SetupApplicationMenuExporter(); - }); - }); - - AvaloniaLocator.CurrentMutable.Bind() - .ToConstant(new MacOSClassicDesktopStyleApplicationLifetime()); + builder.AfterSetup (x=> + { + platform.SetupApplicationName(); + platform.SetupApplicationMenuExporter(); + }); + }) + .UseLifetimeOverride(type => type == typeof(ClassicDesktopStyleApplicationLifetime) + ? new MacOSClassicDesktopStyleApplicationLifetime() : null); return builder; } diff --git a/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs b/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs index 2ce32564ed..73571b4f5b 100644 --- a/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs +++ b/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs @@ -31,6 +31,8 @@ namespace Avalonia.Controls.UnitTests using (UnitTestApplication.Start(new TestServices(dispatcherImpl: new ManagedDispatcherImpl(null)))) using(var lifetime = new ClassicDesktopStyleApplicationLifetime()) { + lifetime.SetupCore(Array.Empty()); + Dispatcher.UIThread.Post(() => lifetime.Shutdown(1337)); var exitCode = lifetime.Start(Array.Empty()); @@ -45,6 +47,8 @@ namespace Avalonia.Controls.UnitTests using (UnitTestApplication.Start(TestServices.StyledWindow)) using(var lifetime = new ClassicDesktopStyleApplicationLifetime()) { + lifetime.SetupCore(Array.Empty()); + var windows = new List { new Window(), new Window(), new Window(), new Window() }; foreach (var window in windows) @@ -65,6 +69,7 @@ namespace Avalonia.Controls.UnitTests using(var lifetime = new ClassicDesktopStyleApplicationLifetime()) { lifetime.ShutdownMode = ShutdownMode.OnExplicitShutdown; + lifetime.SetupCore(Array.Empty()); var hasExit = false; @@ -99,6 +104,7 @@ namespace Avalonia.Controls.UnitTests using(var lifetime = new ClassicDesktopStyleApplicationLifetime()) { lifetime.ShutdownMode = ShutdownMode.OnMainWindowClose; + lifetime.SetupCore(Array.Empty()); var hasExit = false; @@ -127,6 +133,7 @@ namespace Avalonia.Controls.UnitTests using(var lifetime = new ClassicDesktopStyleApplicationLifetime()) { lifetime.ShutdownMode = ShutdownMode.OnLastWindowClose; + lifetime.SetupCore(Array.Empty()); var hasExit = false; @@ -156,6 +163,8 @@ namespace Avalonia.Controls.UnitTests using (UnitTestApplication.Start(TestServices.StyledWindow)) using(var lifetime = new ClassicDesktopStyleApplicationLifetime()) { + lifetime.SetupCore(Array.Empty()); + var window = new Window(); window.Show(); @@ -170,6 +179,8 @@ namespace Avalonia.Controls.UnitTests using (UnitTestApplication.Start(TestServices.StyledWindow)) using(var lifetime = new ClassicDesktopStyleApplicationLifetime()) { + lifetime.SetupCore(Array.Empty()); + var window = new Window(); window.Show(); @@ -188,6 +199,8 @@ namespace Avalonia.Controls.UnitTests using (UnitTestApplication.Start(TestServices.StyledWindow)) using(var lifetime = new ClassicDesktopStyleApplicationLifetime()) { + lifetime.SetupCore(Array.Empty()); + var window = new Window(); window.Show(); @@ -213,6 +226,8 @@ namespace Avalonia.Controls.UnitTests using (UnitTestApplication.Start(services)) using(var lifetime = new ClassicDesktopStyleApplicationLifetime()) { + lifetime.SetupCore(Array.Empty()); + var window = new Window(); window.Show(); @@ -261,6 +276,7 @@ namespace Avalonia.Controls.UnitTests using(var lifetime = new ClassicDesktopStyleApplicationLifetime()) { lifetime.ShutdownMode = ShutdownMode.OnMainWindowClose; + lifetime.SetupCore(Array.Empty()); var hasExit = false; @@ -298,6 +314,7 @@ namespace Avalonia.Controls.UnitTests using(var lifetime = new ClassicDesktopStyleApplicationLifetime()) { lifetime.ShutdownMode = ShutdownMode.OnLastWindowClose; + lifetime.SetupCore(Array.Empty()); var hasExit = false; @@ -336,6 +353,8 @@ namespace Avalonia.Controls.UnitTests using (UnitTestApplication.Start(TestServices.StyledWindow)) using(var lifetime = new ClassicDesktopStyleApplicationLifetime()) { + lifetime.SetupCore(Array.Empty()); + var hasExit = false; lifetime.Exit += (_, _) => hasExit = true; @@ -369,6 +388,8 @@ namespace Avalonia.Controls.UnitTests using (UnitTestApplication.Start(TestServices.StyledWindow.With(dispatcherImpl: CreateDispatcherWithInstantMainLoop()))) using(var lifetime = new ClassicDesktopStyleApplicationLifetime()) { + lifetime.SetupCore(Array.Empty()); + var hasExit = false; lifetime.Exit += (_, _) => hasExit = true; @@ -402,6 +423,8 @@ namespace Avalonia.Controls.UnitTests using (UnitTestApplication.Start(TestServices.StyledWindow)) using(var lifetime = new ClassicDesktopStyleApplicationLifetime()) { + lifetime.SetupCore(Array.Empty()); + var hasExit = false; lifetime.Exit += (_, _) => hasExit = true; From e86faa2f4520f63c540afb03a4ff76b35f65dffa Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 20 Jan 2024 01:28:19 -0800 Subject: [PATCH 11/14] Don't use WebKit on tvOS samples --- samples/ControlCatalog.iOS/EmbedSample.iOS.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/samples/ControlCatalog.iOS/EmbedSample.iOS.cs b/samples/ControlCatalog.iOS/EmbedSample.iOS.cs index ad86d2b578..8fe72d1cff 100644 --- a/samples/ControlCatalog.iOS/EmbedSample.iOS.cs +++ b/samples/ControlCatalog.iOS/EmbedSample.iOS.cs @@ -3,7 +3,6 @@ using Avalonia.Platform; using CoreGraphics; using Foundation; using UIKit; -using WebKit; using Avalonia.iOS; using ControlCatalog.Pages; @@ -13,14 +12,16 @@ public class EmbedSampleIOS : INativeDemoControl { public IPlatformHandle CreateControl(bool isSecond, IPlatformHandle parent, Func createDefault) { +#if !TVOS if (isSecond) { - var webView = new WKWebView(CGRect.Empty, new WKWebViewConfiguration()); + var webView = new WebKit.WKWebView(CGRect.Empty, new WebKit.WKWebViewConfiguration()); webView.LoadRequest(new NSUrlRequest(new NSUrl("https://www.apple.com/"))); return new UIViewControlHandle(webView); } else +#endif { var button = new UIButton(); var clickCount = 0; From 1aa1f0f736ab5013ba781ea15ea2b9fdc508a191 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 20 Jan 2024 01:40:33 -0800 Subject: [PATCH 12/14] Extend StandardRuntimePlatform with more apple platforms --- src/Avalonia.Base/Compatibility/OperatingSystem.cs | 12 ++++++++---- .../Platform/StandardRuntimePlatform.cs | 14 ++++++-------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/Avalonia.Base/Compatibility/OperatingSystem.cs b/src/Avalonia.Base/Compatibility/OperatingSystem.cs index eac199b32f..ad5fe0246a 100644 --- a/src/Avalonia.Base/Compatibility/OperatingSystem.cs +++ b/src/Avalonia.Base/Compatibility/OperatingSystem.cs @@ -8,7 +8,9 @@ namespace Avalonia.Compatibility #if NET6_0_OR_GREATER public static bool IsWindows() => OperatingSystem.IsWindows(); public static bool IsMacOS() => OperatingSystem.IsMacOS(); + public static bool IsMacCatalyst() => OperatingSystem.IsMacCatalyst(); public static bool IsLinux() => OperatingSystem.IsLinux(); + public static bool IsFreeBSD() => OperatingSystem.IsFreeBSD(); public static bool IsAndroid() => OperatingSystem.IsAndroid(); public static bool IsIOS() => OperatingSystem.IsIOS(); public static bool IsTvOS() => OperatingSystem.IsTvOS(); @@ -18,10 +20,12 @@ namespace Avalonia.Compatibility public static bool IsWindows() => RuntimeInformation.IsOSPlatform(OSPlatform.Windows); public static bool IsMacOS() => RuntimeInformation.IsOSPlatform(OSPlatform.OSX); public static bool IsLinux() => RuntimeInformation.IsOSPlatform(OSPlatform.Linux); - public static bool IsAndroid() => IsOSPlatform("ANDROID"); - public static bool IsIOS() => IsOSPlatform("IOS"); - public static bool IsTvOS() => IsOSPlatform("TVOS"); // untested - public static bool IsBrowser() => IsOSPlatform("BROWSER"); + public static bool IsFreeBSD() => false; + public static bool IsAndroid() => false; + public static bool IsIOS() => false; + public static bool IsMacCatalyst() => false; + public static bool IsTvOS() => false; + public static bool IsBrowser() => false; public static bool IsOSPlatform(string platform) => RuntimeInformation.IsOSPlatform(OSPlatform.Create(platform)); #endif } diff --git a/src/Avalonia.Base/Platform/StandardRuntimePlatform.cs b/src/Avalonia.Base/Platform/StandardRuntimePlatform.cs index 7a5c92c774..b72e10c831 100644 --- a/src/Avalonia.Base/Platform/StandardRuntimePlatform.cs +++ b/src/Avalonia.Base/Platform/StandardRuntimePlatform.cs @@ -1,20 +1,18 @@ -using System; -using System.Threading; using Avalonia.Compatibility; using Avalonia.Metadata; -using Avalonia.Platform.Internal; namespace Avalonia.Platform { [PrivateApi] public class StandardRuntimePlatform : IRuntimePlatform { - private static readonly RuntimePlatformInfo s_info = new() + public virtual RuntimePlatformInfo GetRuntimeInfo() => new() { - IsDesktop = OperatingSystemEx.IsWindows() || OperatingSystemEx.IsMacOS() || OperatingSystemEx.IsLinux(), - IsMobile = OperatingSystemEx.IsAndroid() || OperatingSystemEx.IsIOS() + IsDesktop = OperatingSystemEx.IsWindows() + || OperatingSystemEx.IsMacOS() || OperatingSystemEx.IsMacCatalyst() + || OperatingSystemEx.IsLinux() || OperatingSystemEx.IsFreeBSD(), + IsMobile = OperatingSystemEx.IsAndroid() || (OperatingSystemEx.IsIOS() && !OperatingSystemEx.IsMacCatalyst()), + IsTV = OperatingSystemEx.IsTvOS() }; - - public virtual RuntimePlatformInfo GetRuntimeInfo() => s_info; } } From 3334d0043b7e58a9b93066f9dc8b89dfb9636aba Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 20 Jan 2024 02:20:27 -0800 Subject: [PATCH 13/14] Handle mouse and stylus for iOS backend as well --- src/iOS/Avalonia.iOS/InputHandler.cs | 79 +++++++++++++++++++++------- 1 file changed, 61 insertions(+), 18 deletions(-) diff --git a/src/iOS/Avalonia.iOS/InputHandler.cs b/src/iOS/Avalonia.iOS/InputHandler.cs index 7057f5a84d..ff074cfe12 100644 --- a/src/iOS/Avalonia.iOS/InputHandler.cs +++ b/src/iOS/Avalonia.iOS/InputHandler.cs @@ -15,7 +15,9 @@ internal sealed class InputHandler private readonly AvaloniaView _view; private readonly ITopLevelImpl _tl; - public TouchDevice _device = new(); + public TouchDevice _touchDevice = new(); + public MouseDevice _mouseDevice = new(); + public PenDevice _penDevice = new(); private static long _nextTouchPointId = 1; private readonly Dictionary _knownTouches = new Dictionary(); @@ -32,23 +34,54 @@ internal sealed class InputHandler { foreach (UITouch t in touches) { - var pt = t.LocationInView(_view).ToAvalonia(); + if (t.Type == UITouchType.Indirect) + { + // Ignore Indirect input, like remote controller trackpad. + // For Avalonia we handle it independently with gestures. + continue; + } + if (!_knownTouches.TryGetValue(t, out var id)) _knownTouches[t] = id = _nextTouchPointId++; - var ev = new RawTouchEventArgs(_device, Ts(evt), Root, - t.Phase switch + var point = new RawPointerPoint + { + Position = t.LocationInView(_view).ToAvalonia(), + // in iOS "1.0 represents the force of an average touch", when Avalonia expects 0.5 for "average" + Pressure = (float)t.Force / 2 + }; + + IInputDevice device = t.Type switch + { + UITouchType.Stylus => _penDevice, + UITouchType.IndirectPointer => _mouseDevice, + _ => _touchDevice + }; + + var ev = new RawTouchEventArgs(device, Ts(evt), Root, + (device, t.Phase) switch { - UITouchPhase.Began => RawPointerEventType.TouchBegin, - UITouchPhase.Ended => RawPointerEventType.TouchEnd, - UITouchPhase.Cancelled => RawPointerEventType.TouchCancel, - _ => RawPointerEventType.TouchUpdate - }, pt, RawInputModifiers.None, id); + (TouchDevice, UITouchPhase.Began) => RawPointerEventType.TouchBegin, + (TouchDevice, UITouchPhase.Ended) => RawPointerEventType.TouchEnd, + (TouchDevice, UITouchPhase.Cancelled) => RawPointerEventType.TouchCancel, + (TouchDevice, _) => RawPointerEventType.TouchUpdate, + + (_, UITouchPhase.Began) => IsRightClick() ? RawPointerEventType.RightButtonDown : RawPointerEventType.LeftButtonDown, + (_, UITouchPhase.Ended or UITouchPhase.Cancelled) => IsRightClick() ? RawPointerEventType.RightButtonUp : RawPointerEventType.RightButtonDown, + (_, _) => RawPointerEventType.Move, + }, point, ConvertModifierKeys(evt?.ModifierFlags), id); _tl.Input?.Invoke(ev); if (t.Phase == UITouchPhase.Cancelled || t.Phase == UITouchPhase.Ended) _knownTouches.Remove(t); + + bool IsRightClick() +#if !TVOS + => evt?.ButtonMask.HasFlag(UIEventButtonMask.Secondary) ?? false; +#else + => false; +#endif } } @@ -65,15 +98,7 @@ internal sealed class InputHandler if (_supportsKey && p.Key is { } uiKey && s_keys.TryGetValue(uiKey.KeyCode, out physicalKey)) { - var uiModifier = uiKey.ModifierFlags; - if (uiModifier.HasFlag(UIKeyModifierFlags.Shift)) - modifier |= RawInputModifiers.Shift; - if (uiModifier.HasFlag(UIKeyModifierFlags.Alternate)) - modifier |= RawInputModifiers.Alt; - if (uiModifier.HasFlag(UIKeyModifierFlags.Control)) - modifier |= RawInputModifiers.Control; - if (uiModifier.HasFlag(UIKeyModifierFlags.Command)) - modifier |= RawInputModifiers.Meta; + modifier = ConvertModifierKeys(uiKey.ModifierFlags); keyDeviceType = KeyDeviceType.Keyboard; // very likely @@ -171,6 +196,24 @@ internal sealed class InputHandler } } + private static RawInputModifiers ConvertModifierKeys(UIKeyModifierFlags? uiModifier) + { + RawInputModifiers modifier = default; + if (uiModifier is { } flags) + { + if (flags.HasFlag(UIKeyModifierFlags.Shift)) + modifier |= RawInputModifiers.Shift; + if (flags.HasFlag(UIKeyModifierFlags.Alternate)) + modifier |= RawInputModifiers.Alt; + if (flags.HasFlag(UIKeyModifierFlags.Control)) + modifier |= RawInputModifiers.Control; + if (flags.HasFlag(UIKeyModifierFlags.Command)) + modifier |= RawInputModifiers.Meta; + } + + return modifier; + } + private static Dictionary s_keys = new() { //[UIKeyboardHidUsage.KeyboardErrorRollOver] = PhysicalKey.None, From 68fc39424f6312e3f70d9b607b6732340d510622 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 20 Jan 2024 02:28:21 -0800 Subject: [PATCH 14/14] And IntermediatePoints --- src/iOS/Avalonia.iOS/InputHandler.cs | 36 +++++++++++++++++++++------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/src/iOS/Avalonia.iOS/InputHandler.cs b/src/iOS/Avalonia.iOS/InputHandler.cs index ff074cfe12..c7ab898c66 100644 --- a/src/iOS/Avalonia.iOS/InputHandler.cs +++ b/src/iOS/Avalonia.iOS/InputHandler.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Avalonia.Collections.Pooled; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Platform; @@ -10,6 +11,7 @@ namespace Avalonia.iOS; internal sealed class InputHandler { + private static readonly PooledList s_intermediatePointsPooledList = new(ClearMode.Never); private readonly bool _supportsKey = OperatingSystem.IsIOSVersionAtLeast(13, 4) || OperatingSystem.IsTvOSVersionAtLeast(13, 4); @@ -44,13 +46,6 @@ internal sealed class InputHandler if (!_knownTouches.TryGetValue(t, out var id)) _knownTouches[t] = id = _nextTouchPointId++; - var point = new RawPointerPoint - { - Position = t.LocationInView(_view).ToAvalonia(), - // in iOS "1.0 represents the force of an average touch", when Avalonia expects 0.5 for "average" - Pressure = (float)t.Force / 2 - }; - IInputDevice device = t.Type switch { UITouchType.Stylus => _penDevice, @@ -69,13 +64,36 @@ internal sealed class InputHandler (_, UITouchPhase.Began) => IsRightClick() ? RawPointerEventType.RightButtonDown : RawPointerEventType.LeftButtonDown, (_, UITouchPhase.Ended or UITouchPhase.Cancelled) => IsRightClick() ? RawPointerEventType.RightButtonUp : RawPointerEventType.RightButtonDown, (_, _) => RawPointerEventType.Move, - }, point, ConvertModifierKeys(evt?.ModifierFlags), id); + }, ToPointerPoint(t), ConvertModifierKeys(evt?.ModifierFlags), id) + { + IntermediatePoints = evt is {} thisEvent ? new Lazy?>(() => + { + var coalesced = thisEvent.GetCoalescedTouches(t) ?? Array.Empty(); + s_intermediatePointsPooledList.Clear(); + s_intermediatePointsPooledList.Capacity = coalesced.Length - 1; + + // Skip the last one, as it is already processed point. + for (var i = 0; i < coalesced.Length - 1; i++) + { + s_intermediatePointsPooledList.Add(ToPointerPoint(coalesced[i])); + } + + return s_intermediatePointsPooledList; + }) : null + }; _tl.Input?.Invoke(ev); if (t.Phase == UITouchPhase.Cancelled || t.Phase == UITouchPhase.Ended) _knownTouches.Remove(t); - + + RawPointerPoint ToPointerPoint(UITouch touch) => new() + { + Position = touch.LocationInView(_view).ToAvalonia(), + // in iOS "1.0 represents the force of an average touch", when Avalonia expects 0.5 for "average" + Pressure = (float)t.Force / 2 + }; + bool IsRightClick() #if !TVOS => evt?.ButtonMask.HasFlag(UIEventButtonMask.Secondary) ?? false;