From 7c460a9bcbb47c9bc2beef4de7510b7d359d7b72 Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Sun, 26 Mar 2023 13:04:32 +0000 Subject: [PATCH 01/26] add system bar color attached property --- .../ControlCatalog.Android/MainActivity.cs | 2 +- .../Resources/values-night/colors.xml | 4 ++ .../Avalonia.Android/AvaloniaMainActivity.cs | 4 -- .../Platform/AndroidInsetsManager.cs | 43 +++++++++++++++-- .../Platform/SkiaPlatform/TopLevelImpl.cs | 13 +++--- .../Platform/IInsetsManager.cs | 8 +++- src/Avalonia.Controls/TopLevel.cs | 46 ++++++++++++++++++- .../Controls/EmbeddableControlRoot.xaml | 1 + .../Controls/Window.xaml | 1 + .../Avalonia.Browser/BrowserInsetsManager.cs | 3 ++ src/iOS/Avalonia.iOS/InsetsManager.cs | 3 ++ 11 files changed, 110 insertions(+), 18 deletions(-) create mode 100644 samples/ControlCatalog.Android/Resources/values-night/colors.xml diff --git a/samples/ControlCatalog.Android/MainActivity.cs b/samples/ControlCatalog.Android/MainActivity.cs index f6fa07dbde..486d14661e 100644 --- a/samples/ControlCatalog.Android/MainActivity.cs +++ b/samples/ControlCatalog.Android/MainActivity.cs @@ -5,7 +5,7 @@ using Avalonia.Android; namespace ControlCatalog.Android { - [Activity(Label = "ControlCatalog.Android", Theme = "@style/MyTheme.Main", Icon = "@drawable/icon", LaunchMode = LaunchMode.SingleTop, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize)] + [Activity(Label = "ControlCatalog.Android", Theme = "@style/MyTheme.Main", Icon = "@drawable/icon", LaunchMode = LaunchMode.SingleTop, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize | ConfigChanges.UiMode)] public class MainActivity : AvaloniaMainActivity { } diff --git a/samples/ControlCatalog.Android/Resources/values-night/colors.xml b/samples/ControlCatalog.Android/Resources/values-night/colors.xml new file mode 100644 index 0000000000..3d47b6fc58 --- /dev/null +++ b/samples/ControlCatalog.Android/Resources/values-night/colors.xml @@ -0,0 +1,4 @@ + + + #212121 + diff --git a/src/Android/Avalonia.Android/AvaloniaMainActivity.cs b/src/Android/Avalonia.Android/AvaloniaMainActivity.cs index eb4b6bf6a0..b2cd150933 100644 --- a/src/Android/Avalonia.Android/AvaloniaMainActivity.cs +++ b/src/Android/Avalonia.Android/AvaloniaMainActivity.cs @@ -32,10 +32,6 @@ namespace Avalonia.Android { lifetime.View = View; } - - Window?.ClearFlags(WindowManagerFlags.TranslucentStatus); - Window?.AddFlags(WindowManagerFlags.DrawsSystemBarBackgrounds); - base.OnCreate(savedInstanceState); SetContentView(View); diff --git a/src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs b/src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs index 35d1b06e6a..549815a036 100644 --- a/src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs +++ b/src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs @@ -2,11 +2,10 @@ using System.Collections.Generic; using Android.OS; using Android.Views; -using AndroidX.AppCompat.App; using AndroidX.Core.View; using Avalonia.Android.Platform.SkiaPlatform; using Avalonia.Controls.Platform; -using static Avalonia.Controls.Platform.IInsetsManager; +using Avalonia.Media; namespace Avalonia.Android.Platform { @@ -20,6 +19,7 @@ namespace Avalonia.Android.Platform private bool? _systemUiVisibility; private SystemBarTheme? _statusBarTheme; private bool? _isDefaultSystemBarLightTheme; + private Color? _systemBarColor; public event EventHandler SafeAreaChanged; @@ -36,6 +36,16 @@ namespace Avalonia.Android.Platform } WindowCompat.SetDecorFitsSystemWindows(_activity.Window, !value); + + if(value) + { + _activity.Window.AddFlags(WindowManagerFlags.TranslucentStatus); + _activity.Window.AddFlags(WindowManagerFlags.TranslucentNavigation); + } + else + { + SystemBarColor = _systemBarColor; + } } } @@ -93,6 +103,7 @@ namespace Avalonia.Android.Platform public WindowInsetsCompat OnApplyWindowInsets(View v, WindowInsetsCompat insets) { NotifySafeAreaChanged(SafeAreaPadding); + insets = ViewCompat.OnApplyWindowInsets(v, insets); return insets; } @@ -146,8 +157,6 @@ namespace Avalonia.Android.Platform compat.AppearanceLightStatusBars = value == Controls.Platform.SystemBarTheme.Light; compat.AppearanceLightNavigationBars = value == Controls.Platform.SystemBarTheme.Light; - - AppCompatDelegate.DefaultNightMode = isDefault ? AppCompatDelegate.ModeNightFollowSystem : compat.AppearanceLightStatusBars ? AppCompatDelegate.ModeNightNo : AppCompatDelegate.ModeNightYes; } } @@ -190,10 +199,36 @@ namespace Avalonia.Android.Platform } } + public Color? SystemBarColor + { + get => _systemBarColor; + set + { + _systemBarColor = value; + + if (_systemBarColor is { } color && !_displayEdgeToEdge && _activity.Window != null) + { + _activity.Window.ClearFlags(WindowManagerFlags.TranslucentStatus); + _activity.Window.ClearFlags(WindowManagerFlags.TranslucentNavigation); + _activity.Window.AddFlags(WindowManagerFlags.DrawsSystemBarBackgrounds); + + var androidColor = global::Android.Graphics.Color.Argb(color.A, color.R, color.G, color.B); + _activity.Window.SetStatusBarColor(androidColor); + + if (Build.VERSION.SdkInt >= BuildVersionCodes.O) + { + // As we can only change the navigation bar's foreground api 26 and newer, we only change the background color if running on those versions + _activity.Window.SetNavigationBarColor(androidColor); + } + } + } + } + internal void ApplyStatusBarState() { IsSystemBarVisible = _systemUiVisibility; SystemBarTheme = _statusBarTheme; + SystemBarColor = _systemBarColor; } private class InsetsAnimationCallback : WindowInsetsAnimationCompat.Callback diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs index b8d80a50ff..4e783a0873 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs @@ -3,9 +3,13 @@ using System.Collections.Generic; using Android.App; using Android.Content; using Android.Graphics; +using Android.Graphics.Drawables; +using Android.OS; using Android.Runtime; +using Android.Text; using Android.Views; using Android.Views.InputMethods; +using AndroidX.AppCompat.App; using Avalonia.Android.Platform.Specific; using Avalonia.Android.Platform.Specific.Helpers; using Avalonia.Android.Platform.Storage; @@ -22,13 +26,6 @@ using Avalonia.Platform.Storage; using Avalonia.Rendering; using Avalonia.Rendering.Composition; using Java.Lang; -using Java.Util; -using Math = System.Math; -using AndroidRect = Android.Graphics.Rect; -using Window = Android.Views.Window; -using Android.Graphics.Drawables; -using Android.OS; -using Android.Text; namespace Avalonia.Android.Platform.SkiaPlatform { @@ -286,6 +283,8 @@ namespace Avalonia.Android.Platform.SkiaPlatform _ => null, }; } + + AppCompatDelegate.DefaultNightMode = themeVariant == PlatformThemeVariant.Light ? AppCompatDelegate.ModeNightNo : AppCompatDelegate.ModeNightYes; } public AcrylicPlatformCompensationLevels AcrylicCompensationLevels => new AcrylicPlatformCompensationLevels(1, 1, 1); diff --git a/src/Avalonia.Controls/Platform/IInsetsManager.cs b/src/Avalonia.Controls/Platform/IInsetsManager.cs index 072bace154..c604b89e5c 100644 --- a/src/Avalonia.Controls/Platform/IInsetsManager.cs +++ b/src/Avalonia.Controls/Platform/IInsetsManager.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Media; using Avalonia.Metadata; #nullable enable @@ -22,7 +23,12 @@ namespace Avalonia.Controls.Platform /// Gets the current safe area padding. /// Thickness SafeAreaPadding { get; } - + + /// + /// Gets or sets the color of the platform's system bars + /// + Color? SystemBarColor { get; set; } + /// /// Occurs when safe area for the current window changes. /// diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index bf0de1d79f..19ec9b9e0e 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -87,7 +87,15 @@ namespace Avalonia.Controls /// public static readonly StyledProperty RequestedThemeVariantProperty = ThemeVariantScope.RequestedThemeVariantProperty.AddOwner(); - + + /// + /// Defines the SystemBarColor attached property. + /// + public static readonly AttachedProperty SystemBarColorProperty = + AvaloniaProperty.RegisterAttached( + "SystemBarColor", + inherits: true); + /// /// Defines the event. /// @@ -124,6 +132,22 @@ namespace Avalonia.Controls { KeyboardNavigation.TabNavigationProperty.OverrideDefaultValue(KeyboardNavigationMode.Cycle); AffectsMeasure(ClientSizeProperty); + + SystemBarColorProperty.Changed.AddClassHandler((view, e) => + { + if (e.NewValue is SolidColorBrush colorBrush) + { + if (view.Parent is TopLevel tl && tl.InsetsManager is { } insetsManager) + { + insetsManager.SystemBarColor = colorBrush.Color; + } + + if (view is TopLevel topLevel && topLevel.InsetsManager is { } insets) + { + insets.SystemBarColor = colorBrush.Color; + } + } + }); } /// @@ -379,6 +403,26 @@ namespace Avalonia.Controls set { SetValue(AccessText.ShowAccessKeyProperty, value); } } + /// + /// Helper for setting the color of the platform's system bars + /// + /// The main view attached to the toplevel, or the toplevel + /// The color to set + public static void SetSystemBarColor(Control control, SolidColorBrush? color) + { + control.SetValue(SystemBarColorProperty, color); + } + + /// + /// Helper for getting the color of the platform's system bars + /// + /// The main view attached to the toplevel, or the toplevel + /// The current color of the platform's system bars + public static SolidColorBrush? GetSystemBarColor(Control control) + { + return control.GetValue(SystemBarColorProperty); + } + /// double ILayoutRoot.LayoutScaling => PlatformImpl?.RenderScaling ?? 1; diff --git a/src/Avalonia.Themes.Fluent/Controls/EmbeddableControlRoot.xaml b/src/Avalonia.Themes.Fluent/Controls/EmbeddableControlRoot.xaml index 6a2651e3f5..f60424a2dc 100644 --- a/src/Avalonia.Themes.Fluent/Controls/EmbeddableControlRoot.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/EmbeddableControlRoot.xaml @@ -3,6 +3,7 @@ + diff --git a/src/Avalonia.Themes.Fluent/Controls/Window.xaml b/src/Avalonia.Themes.Fluent/Controls/Window.xaml index 35cc81663f..ff27cce800 100644 --- a/src/Avalonia.Themes.Fluent/Controls/Window.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/Window.xaml @@ -3,6 +3,7 @@ + diff --git a/src/Browser/Avalonia.Browser/BrowserInsetsManager.cs b/src/Browser/Avalonia.Browser/BrowserInsetsManager.cs index 30f80ba27c..0f64003699 100644 --- a/src/Browser/Avalonia.Browser/BrowserInsetsManager.cs +++ b/src/Browser/Avalonia.Browser/BrowserInsetsManager.cs @@ -5,6 +5,7 @@ using System.Text; using System.Threading.Tasks; using Avalonia.Browser.Interop; using Avalonia.Controls.Platform; +using Avalonia.Media; using static Avalonia.Controls.Platform.IInsetsManager; namespace Avalonia.Browser @@ -37,6 +38,8 @@ namespace Avalonia.Browser } } + public Color? SystemBarColor { get; set; } + public void NotifySafeAreaPaddingChanged() { SafeAreaChanged?.Invoke(this, new SafeAreaChangedArgs(SafeAreaPadding)); diff --git a/src/iOS/Avalonia.iOS/InsetsManager.cs b/src/iOS/Avalonia.iOS/InsetsManager.cs index 62e560ddf9..bd6f989dbd 100644 --- a/src/iOS/Avalonia.iOS/InsetsManager.cs +++ b/src/iOS/Avalonia.iOS/InsetsManager.cs @@ -1,5 +1,6 @@ using System; using Avalonia.Controls.Platform; +using Avalonia.Media; using UIKit; namespace Avalonia.iOS; @@ -80,4 +81,6 @@ internal class InsetsManager : IInsetsManager } public Thickness SafeAreaPadding => _controller?.SafeAreaPadding ?? default; + + public Color? SystemBarColor { get; set; } } From 12fa653ca2f8daed5244918de29c1a0af3c0d326 Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 26 Mar 2023 10:22:08 -0400 Subject: [PATCH 02/26] Use Transparent background in ColorPreviewer so DropShadow appears This addresses #10539. Drop shadow for now requires the Background to be set in order to work correctly. --- .../Themes/Fluent/ColorPreviewer.xaml | 1 + .../Themes/Simple/ColorPreviewer.xaml | 1 + 2 files changed, 2 insertions(+) diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPreviewer.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPreviewer.xaml index 3a88d25ef1..e05fa5a907 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPreviewer.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPreviewer.xaml @@ -64,6 +64,7 @@ diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorPreviewer.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorPreviewer.xaml index 0e51a0519a..a39dd91f52 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorPreviewer.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorPreviewer.xaml @@ -64,6 +64,7 @@ From df7cf037e02dd43f043addece6095be843347f2e Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 26 Mar 2023 10:22:35 -0400 Subject: [PATCH 03/26] Use SetCurrentValue() in ToggleSplitButton to fix warning --- src/Avalonia.Controls/SplitButton/ToggleSplitButton.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/SplitButton/ToggleSplitButton.cs b/src/Avalonia.Controls/SplitButton/ToggleSplitButton.cs index 70d2983b9b..509b58833f 100644 --- a/src/Avalonia.Controls/SplitButton/ToggleSplitButton.cs +++ b/src/Avalonia.Controls/SplitButton/ToggleSplitButton.cs @@ -70,7 +70,7 @@ namespace Avalonia.Controls /// protected void Toggle() { - IsChecked = !IsChecked; + SetCurrentValue(IsCheckedProperty, !IsChecked); } /// From d955a777e55fcee1a771f6c81cb9a94b00505fb5 Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 26 Mar 2023 10:51:06 -0400 Subject: [PATCH 04/26] Switch HexInputAlphaPositionProperty default to Leading to match XAML The control themes are now responsible to set the property to match slider position and CSS standards. Doing this has better compatibility with XAML by default. --- .../ColorView/ColorView.Properties.cs | 2 +- .../Themes/Fluent/ColorPicker.xaml | 2 ++ src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml | 2 ++ .../Themes/Simple/ColorPicker.xaml | 2 ++ src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorView.xaml | 2 ++ 5 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs index e334a1d323..532e87a9fc 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs @@ -48,7 +48,7 @@ namespace Avalonia.Controls public static readonly StyledProperty HexInputAlphaPositionProperty = AvaloniaProperty.Register( nameof(HexInputAlphaPosition), - AlphaComponentPosition.Trailing); // Match CSS (and default slider order) instead of XAML/WinUI + AlphaComponentPosition.Leading); // By default match XAML and the WinUI control /// /// Defines the property. diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml index a9f52b93c7..b3c7cd9f9c 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml @@ -6,6 +6,8 @@ + + diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml index f72fb11bbe..acd2c7ff15 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml @@ -295,6 +295,8 @@ + + diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorPicker.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorPicker.xaml index 7639073775..ff4e1d93a8 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorPicker.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorPicker.xaml @@ -6,6 +6,8 @@ + + diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorView.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorView.xaml index 4e219a98af..a26d3179b5 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorView.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorView.xaml @@ -257,6 +257,8 @@ + + From 0e92b5b742546edca533d785b4ee03c6f8ebb29f Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 26 Mar 2023 10:52:03 -0400 Subject: [PATCH 05/26] Always use AlphaComponentPosition.Leading in DevTools color property editors --- .../Diagnostics/Controls/BrushEditor.cs | 6 +++++- .../Diagnostics/Views/PropertyValueEditorView.cs | 5 ++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/Controls/BrushEditor.cs b/src/Avalonia.Diagnostics/Diagnostics/Controls/BrushEditor.cs index b7579ed31b..ff05614667 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Controls/BrushEditor.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Controls/BrushEditor.cs @@ -34,7 +34,11 @@ namespace Avalonia.Diagnostics.Controls { case ISolidColorBrush scb: { - var colorView = new ColorView { Color = scb.Color }; + var colorView = new ColorView + { + HexInputAlphaPosition = AlphaComponentPosition.Leading, // Always match XAML + Color = scb.Color, + }; colorView.ColorChanged += (_, e) => Brush = new ImmutableSolidColorBrush(e.NewColor); diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/PropertyValueEditorView.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/PropertyValueEditorView.cs index 6b52989f0b..40b0ce4ca4 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/PropertyValueEditorView.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/PropertyValueEditorView.cs @@ -129,7 +129,10 @@ namespace Avalonia.Diagnostics.Views IsEnabled = !Property.IsReadonly }; - var cv = new ColorView(); + var cv = new ColorView + { + HexInputAlphaPosition = AlphaComponentPosition.Leading, // Always match XAML + }; cv.Bind( ColorView.ColorProperty, From bcf1431bf9ebd9a75959a4910e2980328c29bf70 Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 26 Mar 2023 11:17:21 -0400 Subject: [PATCH 06/26] Move AlphaComponentPosition into a better directory --- .../{ColorView => }/AlphaComponentPosition.cs | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/Avalonia.Controls.ColorPicker/{ColorView => }/AlphaComponentPosition.cs (100%) diff --git a/src/Avalonia.Controls.ColorPicker/ColorView/AlphaComponentPosition.cs b/src/Avalonia.Controls.ColorPicker/AlphaComponentPosition.cs similarity index 100% rename from src/Avalonia.Controls.ColorPicker/ColorView/AlphaComponentPosition.cs rename to src/Avalonia.Controls.ColorPicker/AlphaComponentPosition.cs From a401819251cfb72d7bcf2d178e34a5da3e164c95 Mon Sep 17 00:00:00 2001 From: robloo Date: Mon, 27 Mar 2023 20:51:38 -0400 Subject: [PATCH 07/26] Remove alpha in Hex color strings when IsAlphaEnabled or IsAlphaVisible is false --- .../ColorView/ColorView.cs | 6 ++- .../Converters/ColorToHexConverter.cs | 40 +++++++++++++++---- 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs index 274e7f5851..7674b74b6a 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs @@ -61,7 +61,11 @@ namespace Avalonia.Controls { if (_hexTextBox != null) { - _hexTextBox.Text = ColorToHexConverter.ToHexString(Color, HexInputAlphaPosition); + _hexTextBox.Text = ColorToHexConverter.ToHexString( + Color, + HexInputAlphaPosition, + includeAlpha: (IsAlphaEnabled && IsAlphaVisible), + includeSymbol: false); } } diff --git a/src/Avalonia.Controls.ColorPicker/Converters/ColorToHexConverter.cs b/src/Avalonia.Controls.ColorPicker/Converters/ColorToHexConverter.cs index 8798f874f4..8257499d70 100644 --- a/src/Avalonia.Controls.ColorPicker/Converters/ColorToHexConverter.cs +++ b/src/Avalonia.Controls.ColorPicker/Converters/ColorToHexConverter.cs @@ -11,6 +11,18 @@ namespace Avalonia.Controls.Converters /// public class ColorToHexConverter : IValueConverter { + /// + /// Gets or sets a value indicating whether the alpha component is visible in the Hex formatted text. + /// + /// + /// When hidden the existing alpha component value is maintained. Also when hidden the user is still + /// able to input an 8-digit number with alpha. Alpha will be processed but then removed when displayed. + /// + /// Because this property only controls whether alpha is displayed (and it is still processed regardless) + /// it is termed 'Visible' instead of 'Enabled'. + /// + public bool IsAlphaVisible { get; set; } = true; + /// /// Gets or sets the position of a color's alpha component relative to all other components. /// @@ -48,7 +60,7 @@ namespace Avalonia.Controls.Converters return AvaloniaProperty.UnsetValue; } - return ToHexString(color, AlphaPosition, includeSymbol); + return ToHexString(color, AlphaPosition, IsAlphaVisible, includeSymbol); } /// @@ -67,26 +79,40 @@ namespace Avalonia.Controls.Converters /// /// The color to represent as a hex value string. /// The output position of the alpha component. + /// Whether the alpha component will be included in the hex string. /// Whether the hex symbol '#' will be added. /// The input color converted to its hex value string. public static string ToHexString( Color color, AlphaComponentPosition alphaPosition, + bool includeAlpha = true, bool includeSymbol = false) { uint intColor; - if (alphaPosition == AlphaComponentPosition.Trailing) + string hexColor; + + if (includeAlpha) { - intColor = ((uint)color.R << 24) | ((uint)color.G << 16) | ((uint)color.B << 8) | (uint)color.A; + if (alphaPosition == AlphaComponentPosition.Trailing) + { + intColor = ((uint)color.R << 24) | ((uint)color.G << 16) | ((uint)color.B << 8) | (uint)color.A; + } + else + { + // Default is Leading alpha (same as XAML) + intColor = ((uint)color.A << 24) | ((uint)color.R << 16) | ((uint)color.G << 8) | (uint)color.B; + } + + hexColor = intColor.ToString("x8", CultureInfo.InvariantCulture).ToUpperInvariant(); } else { - // Default is Leading alpha - intColor = ((uint)color.A << 24) | ((uint)color.R << 16) | ((uint)color.G << 8) | (uint)color.B; + // In this case the alpha position no longer matters + // Both cases are calculated the same + intColor = ((uint)color.R << 16) | ((uint)color.G << 8) | (uint)color.B; + hexColor = intColor.ToString("x6", CultureInfo.InvariantCulture).ToUpperInvariant(); } - string hexColor = intColor.ToString("x8", CultureInfo.InvariantCulture).ToUpperInvariant(); - if (includeSymbol) { hexColor = '#' + hexColor; From c6afd9bf4fed6b29f598ff842038a966a837ce2d Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Tue, 28 Mar 2023 13:13:06 +0200 Subject: [PATCH 08/26] Fix NRE for AvaloniaNativeTextInputMethod --- .../AvaloniaNativeTextInputMethod.cs | 32 +++++++++++++------ 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/src/Avalonia.Native/AvaloniaNativeTextInputMethod.cs b/src/Avalonia.Native/AvaloniaNativeTextInputMethod.cs index 796a0ced37..8438ef10b5 100644 --- a/src/Avalonia.Native/AvaloniaNativeTextInputMethod.cs +++ b/src/Avalonia.Native/AvaloniaNativeTextInputMethod.cs @@ -2,12 +2,14 @@ using Avalonia.Input.TextInput; using Avalonia.Native.Interop; +#nullable enable + namespace Avalonia.Native { internal class AvaloniaNativeTextInputMethod : ITextInputMethodImpl, IDisposable { - private ITextInputMethodClient _client; - private IAvnTextInputMethodClient _nativeClient; + private ITextInputMethodClient? _client; + private IAvnTextInputMethodClient? _nativeClient; private readonly IAvnTextInputMethod _inputMethod; public AvaloniaNativeTextInputMethod(IAvnWindowBase nativeWindow) @@ -26,7 +28,7 @@ namespace Avalonia.Native _inputMethod.Reset(); } - public void SetClient(ITextInputMethodClient client) + public void SetClient(ITextInputMethodClient? client) { if (_client is { SupportsSurroundingText: true }) { @@ -39,9 +41,9 @@ namespace Avalonia.Native _nativeClient = null; _client = client; - if (client != null) + if (_client != null) { - _nativeClient = new AvnTextInputMethodClient(client); + _nativeClient = new AvnTextInputMethodClient(_client); OnSurroundingTextChanged(this, EventArgs.Empty); OnCursorRectangleChanged(this, EventArgs.Empty); @@ -53,16 +55,28 @@ namespace Avalonia.Native _inputMethod.SetClient(_nativeClient); } - private void OnCursorRectangleChanged(object sender, EventArgs e) + private void OnCursorRectangleChanged(object? sender, EventArgs e) { if (_client == null) { return; } - var visualRoot = _client.TextViewVisual.VisualRoot; + var textViewVisual = _client.TextViewVisual; + + if(textViewVisual is null ) + { + return; + } + + var visualRoot = textViewVisual.VisualRoot; + + if(visualRoot is null) + { + return; + } - var transform = _client.TextViewVisual.TransformToVisual((Visual)visualRoot); + var transform = textViewVisual.TransformToVisual((Visual)visualRoot); if (transform == null) { @@ -74,7 +88,7 @@ namespace Avalonia.Native _inputMethod.SetCursorRect(rect.ToAvnRect()); } - private void OnSurroundingTextChanged(object sender, EventArgs e) + private void OnSurroundingTextChanged(object? sender, EventArgs e) { if (_client == null) { From 45bf804cd451cab04c9a35c6e8263f45229a125c Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Tue, 28 Mar 2023 13:45:39 +0200 Subject: [PATCH 09/26] Fix SelectableTextBlock selection --- src/Avalonia.Controls/SelectableTextBlock.cs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Controls/SelectableTextBlock.cs b/src/Avalonia.Controls/SelectableTextBlock.cs index 1007001f05..8622720236 100644 --- a/src/Avalonia.Controls/SelectableTextBlock.cs +++ b/src/Avalonia.Controls/SelectableTextBlock.cs @@ -18,10 +18,10 @@ namespace Avalonia.Controls public class SelectableTextBlock : TextBlock, IInlineHost { public static readonly StyledProperty SelectionStartProperty = - TextBox.SelectionStartProperty.AddOwner(new(coerce: TextBox.CoerceCaretIndex)); + TextBox.SelectionStartProperty.AddOwner(); public static readonly StyledProperty SelectionEndProperty = - TextBox.SelectionEndProperty.AddOwner(new(coerce: TextBox.CoerceCaretIndex)); + TextBox.SelectionEndProperty.AddOwner(); public static readonly DirectProperty SelectedTextProperty = AvaloniaProperty.RegisterDirect( @@ -228,7 +228,7 @@ namespace Avalonia.Controls { base.OnPointerPressed(e); - var text = Text; + var text = HasComplexContent ? Inlines.Text : Text; var clickInfo = e.GetCurrentPoint(this); if (text != null && clickInfo.Properties.IsLeftButtonPressed) @@ -314,7 +314,7 @@ namespace Avalonia.Controls // selection should not change during pointer move if the user right clicks if (e.Pointer.Captured == this && e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) { - var text = Text; + var text = HasComplexContent ? Inlines.Text : Text; var padding = Padding; var point = e.GetPosition(this) - new Point(padding.Left, padding.Top); @@ -397,7 +397,10 @@ namespace Avalonia.Controls private string GetSelection() { - var textLength = Text?.Length ?? 0; + var text = HasComplexContent ? Inlines.Text : Text; + + var textLength = text?.Length ?? 0; + if (textLength == 0) { return ""; @@ -415,7 +418,7 @@ namespace Avalonia.Controls var length = Math.Max(0, end - start); - var selectedText = Text!.Substring(start, length); + var selectedText = text!.Substring(start, length); return selectedText; } From 1964afb854dc13731e19fd8366399d83c89b62d0 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Tue, 28 Mar 2023 14:15:24 +0200 Subject: [PATCH 10/26] Fix possible NRE --- src/Avalonia.Controls/SelectableTextBlock.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/SelectableTextBlock.cs b/src/Avalonia.Controls/SelectableTextBlock.cs index 8622720236..66ae4a26bb 100644 --- a/src/Avalonia.Controls/SelectableTextBlock.cs +++ b/src/Avalonia.Controls/SelectableTextBlock.cs @@ -228,7 +228,7 @@ namespace Avalonia.Controls { base.OnPointerPressed(e); - var text = HasComplexContent ? Inlines.Text : Text; + var text = HasComplexContent ? Inlines?.Text : Text; var clickInfo = e.GetCurrentPoint(this); if (text != null && clickInfo.Properties.IsLeftButtonPressed) @@ -314,7 +314,7 @@ namespace Avalonia.Controls // selection should not change during pointer move if the user right clicks if (e.Pointer.Captured == this && e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) { - var text = HasComplexContent ? Inlines.Text : Text; + var text = HasComplexContent ? Inlines?.Text : Text; var padding = Padding; var point = e.GetPosition(this) - new Point(padding.Left, padding.Top); @@ -397,7 +397,7 @@ namespace Avalonia.Controls private string GetSelection() { - var text = HasComplexContent ? Inlines.Text : Text; + var text = HasComplexContent ? Inlines?.Text : Text; var textLength = text?.Length ?? 0; From e7aaf8a7f7120c971da687613aaf136b5469ae1e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 28 Mar 2023 14:45:19 +0200 Subject: [PATCH 11/26] Ensure that the default value can be coerced. When the coercion function on a property results in the default value being coerced, ensure that state is recorded in the value store. --- src/Avalonia.Base/AvaloniaProperty.cs | 6 + src/Avalonia.Base/DirectPropertyBase.cs | 5 + .../PropertyStore/EffectiveValue.cs | 49 ++++++- .../PropertyStore/EffectiveValue`1.cs | 42 ++++-- src/Avalonia.Base/PropertyStore/ValueStore.cs | 52 ++++++-- src/Avalonia.Base/StyledProperty.cs | 5 + .../AvaloniaObjectTests_Coercion.cs | 125 +++++++++++++++++- .../AvaloniaPropertyTests.cs | 5 + 8 files changed, 260 insertions(+), 29 deletions(-) diff --git a/src/Avalonia.Base/AvaloniaProperty.cs b/src/Avalonia.Base/AvaloniaProperty.cs index e98d9f0517..c57131f7b5 100644 --- a/src/Avalonia.Base/AvaloniaProperty.cs +++ b/src/Avalonia.Base/AvaloniaProperty.cs @@ -499,6 +499,12 @@ namespace Avalonia /// The object instance. internal abstract void RouteClearValue(AvaloniaObject o); + /// + /// Routes an untyped CoerceValue call on a property with its default value to a typed call. + /// + /// The object instance. + internal abstract void RouteCoerceDefaultValue(AvaloniaObject o); + /// /// Routes an untyped GetValue call to a typed call. /// diff --git a/src/Avalonia.Base/DirectPropertyBase.cs b/src/Avalonia.Base/DirectPropertyBase.cs index 94dfaaab01..7e5a962157 100644 --- a/src/Avalonia.Base/DirectPropertyBase.cs +++ b/src/Avalonia.Base/DirectPropertyBase.cs @@ -117,6 +117,11 @@ namespace Avalonia o.ClearValue(this); } + internal override void RouteCoerceDefaultValue(AvaloniaObject o) + { + // Do nothing. + } + /// internal override object? RouteGetValue(AvaloniaObject o) { diff --git a/src/Avalonia.Base/PropertyStore/EffectiveValue.cs b/src/Avalonia.Base/PropertyStore/EffectiveValue.cs index 7e9f9ae9ba..a00a51e694 100644 --- a/src/Avalonia.Base/PropertyStore/EffectiveValue.cs +++ b/src/Avalonia.Base/PropertyStore/EffectiveValue.cs @@ -36,12 +36,23 @@ namespace Avalonia.PropertyStore /// public IValueEntry? BaseValueEntry { get; private set; } + /// + /// Gets a value indicating whether the property has a coercion function. + /// + public bool HasCoercion { get; protected set; } + /// /// Gets a value indicating whether the was overridden by a call to /// . /// public bool IsOverridenCurrentValue { get; set; } + /// + /// Gets a value indicating whether the is the result of the + /// + /// + public bool IsCoercedDefaultValue { get; set; } + /// /// Begins a reevaluation pass on the effective value. /// @@ -63,10 +74,33 @@ namespace Avalonia.PropertyStore /// /// Ends a reevaluation pass on the effective value. /// + /// The associated value store. + /// The property being reevaluated. /// - /// This method unsubscribes from any unused value entries. + /// Handles coercing the default value if necessary. /// - public void EndReevaluation() + public void EndReevaluation(ValueStore owner, AvaloniaProperty property) + { + if (Priority == BindingPriority.Unset && HasCoercion) + CoerceDefaultValueAndRaise(owner, property); + } + + /// + /// Gets a value indicating whether the effective value represents the default value of the + /// property and can be removed. + /// + /// True if the effective value van be removed; otherwise false. + public bool CanRemove() + { + return Priority == BindingPriority.Unset && + !IsOverridenCurrentValue && + !IsCoercedDefaultValue; + } + + /// + /// Unsubscribes from any unused value entries. + /// + public void UnsubscribeIfNecessary() { if (Priority == BindingPriority.Unset) { @@ -130,6 +164,17 @@ namespace Avalonia.PropertyStore /// The property being cleared. public abstract void DisposeAndRaiseUnset(ValueStore owner, AvaloniaProperty property); + /// + /// Coerces the default value, raising + /// where necessary. + /// + /// The associated value store. + /// The property being coerced. + protected abstract void CoerceDefaultValueAndRaise(ValueStore owner, AvaloniaProperty property); + + /// + /// Gets the current effective value as a boxed value. + /// protected abstract object? GetBoxedValue(); protected void UpdateValueEntry(IValueEntry? entry, BindingPriority priority) diff --git a/src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs b/src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs index 330034f51d..369cd3ea63 100644 --- a/src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs +++ b/src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using Avalonia.Data; -using static Avalonia.Rendering.Composition.Animations.PropertySetSnapshot; namespace Avalonia.PropertyStore { @@ -33,19 +32,16 @@ namespace Avalonia.PropertyStore if (_metadata.CoerceValue is { } coerce) { + HasCoercion = true; _uncommon = new() { _coerce = coerce, _uncoercedValue = value, _uncoercedBaseValue = value, }; - - Value = coerce(owner, value); - } - else - { - Value = value; } + + Value = value; } /// @@ -61,7 +57,7 @@ namespace Avalonia.PropertyStore Debug.Assert(priority != BindingPriority.LocalValue); UpdateValueEntry(value, priority); - SetAndRaiseCore(owner, (StyledProperty)value.Property, GetValue(value), priority, false); + SetAndRaiseCore(owner, (StyledProperty)value.Property, GetValue(value), priority); if (priority > BindingPriority.LocalValue && value.GetDataValidationState(out var state, out var error)) @@ -75,7 +71,7 @@ namespace Avalonia.PropertyStore StyledProperty property, T value) { - SetAndRaiseCore(owner, property, value, BindingPriority.LocalValue, false); + SetAndRaiseCore(owner, property, value, BindingPriority.LocalValue); } public void SetCurrentValueAndRaise( @@ -83,8 +79,15 @@ namespace Avalonia.PropertyStore StyledProperty property, T value) { - IsOverridenCurrentValue = true; - SetAndRaiseCore(owner, property, value, Priority, true); + SetAndRaiseCore(owner, property, value, Priority, isOverriddenCurrentValue: true); + } + + public void SetCoercedDefaultValueAndRaise( + ValueStore owner, + StyledProperty property, + T value) + { + SetAndRaiseCore(owner, property, value, Priority, isCoercedDefaultValue: true); } public bool TryGetBaseValue([MaybeNullWhen(false)] out T value) @@ -117,7 +120,7 @@ namespace Avalonia.PropertyStore Debug.Assert(Priority != BindingPriority.Animation); Debug.Assert(BasePriority != BindingPriority.Unset); UpdateValueEntry(null, BindingPriority.Animation); - SetAndRaiseCore(owner, (StyledProperty)property, _baseValue!, BasePriority, false); + SetAndRaiseCore(owner, (StyledProperty)property, _baseValue!, BasePriority); } public override void CoerceValue(ValueStore owner, AvaloniaProperty property) @@ -168,6 +171,17 @@ namespace Avalonia.PropertyStore } } + protected override void CoerceDefaultValueAndRaise(ValueStore owner, AvaloniaProperty property) + { + Debug.Assert(_uncommon?._coerce is not null); + Debug.Assert(Priority == BindingPriority.Unset); + + var coercedDefaultValue = _uncommon!._coerce!(owner.Owner, _metadata.DefaultValue); + + if (!EqualityComparer.Default.Equals(_metadata.DefaultValue, coercedDefaultValue)) + SetCoercedDefaultValueAndRaise(owner, (StyledProperty)property, coercedDefaultValue); + } + protected override object? GetBoxedValue() => Value; private static T GetValue(IValueEntry entry) @@ -183,7 +197,8 @@ namespace Avalonia.PropertyStore StyledProperty property, T value, BindingPriority priority, - bool isOverriddenCurrentValue) + bool isOverriddenCurrentValue = false, + bool isCoercedDefaultValue = false) { var oldValue = Value; var valueChanged = false; @@ -191,6 +206,7 @@ namespace Avalonia.PropertyStore var v = value; IsOverridenCurrentValue = isOverriddenCurrentValue; + IsCoercedDefaultValue = isCoercedDefaultValue; if (_uncommon?._coerce is { } coerce) v = coerce(owner.Owner, value); diff --git a/src/Avalonia.Base/PropertyStore/ValueStore.cs b/src/Avalonia.Base/PropertyStore/ValueStore.cs index af31459a98..0ef14272b4 100644 --- a/src/Avalonia.Base/PropertyStore/ValueStore.cs +++ b/src/Avalonia.Base/PropertyStore/ValueStore.cs @@ -259,6 +259,27 @@ namespace Avalonia.PropertyStore { if (_effectiveValues.TryGetValue(property, out var v)) v.CoerceValue(this, property); + else + property.RouteCoerceDefaultValue(Owner); + } + + public void CoerceDefaultValue(StyledProperty property) + { + var metadata = property.GetMetadata(Owner.GetType()); + + if (metadata.CoerceValue is null) + return; + + var coercedDefaultValue = metadata.CoerceValue(Owner, metadata.DefaultValue); + + if (EqualityComparer.Default.Equals(metadata.DefaultValue, coercedDefaultValue)) + return; + + // We have a situation where the default value isn't valid according to the coerce + // function. In this case, we need to create an EffectiveValue entry. + var effectiveValue = CreateEffectiveValue(property); + AddEffectiveValue(property, effectiveValue); + effectiveValue.SetCoercedDefaultValueAndRaise(this, property, coercedDefaultValue); } public Optional GetBaseValue(StyledProperty property) @@ -838,20 +859,25 @@ namespace Avalonia.PropertyStore goto restart; } - if (current?.Priority == BindingPriority.Unset) + if (current is not null) { - if (current.BasePriority == BindingPriority.Unset) - { - RemoveEffectiveValue(property); - current.DisposeAndRaiseUnset(this, property); - } - else + current.EndReevaluation(this, property); + + if (current.CanRemove()) { - current.RemoveAnimationAndRaise(this, property); + if (current.BasePriority == BindingPriority.Unset) + { + RemoveEffectiveValue(property); + current.DisposeAndRaiseUnset(this, property); + } + else + { + current.RemoveAnimationAndRaise(this, property); + } } - } - current?.EndReevaluation(); + current.UnsubscribeIfNecessary(); + } } finally { @@ -923,7 +949,9 @@ namespace Avalonia.PropertyStore { _effectiveValues.GetKeyValue(i, out var key, out var e); - if (e.Priority == BindingPriority.Unset && !e.IsOverridenCurrentValue) + e.EndReevaluation(this, key); + + if (e.CanRemove()) { RemoveEffectiveValue(key, i); e.DisposeAndRaiseUnset(this, key); @@ -932,7 +960,7 @@ namespace Avalonia.PropertyStore break; } - e.EndReevaluation(); + e.UnsubscribeIfNecessary(); } } finally diff --git a/src/Avalonia.Base/StyledProperty.cs b/src/Avalonia.Base/StyledProperty.cs index e1b88cde49..5cb330eda9 100644 --- a/src/Avalonia.Base/StyledProperty.cs +++ b/src/Avalonia.Base/StyledProperty.cs @@ -176,6 +176,11 @@ namespace Avalonia o.ClearValue(this); } + internal override void RouteCoerceDefaultValue(AvaloniaObject o) + { + o.GetValueStore().CoerceDefaultValue(this); + } + /// internal override object? RouteGetValue(AvaloniaObject o) { diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Coercion.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Coercion.cs index 2154bb63d0..d53c055639 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Coercion.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Coercion.cs @@ -1,7 +1,10 @@ using System; using System.Collections.Generic; using System.Reactive.Subjects; +using Avalonia.Controls; using Avalonia.Data; +using Avalonia.Styling; +using Avalonia.UnitTests; using Xunit; namespace Avalonia.Base.UnitTests @@ -182,11 +185,103 @@ namespace Avalonia.Base.UnitTests Assert.Equal(-150, target.Foo); } + [Fact] + public void Default_Value_Can_Be_Coerced() + { + var target = new Class1(); + var raised = 0; + + target.MinFoo = 20; + + target.PropertyChanged += (s, e) => + { + Assert.Equal(Class1.FooProperty, e.Property); + Assert.Equal(11, e.OldValue); + Assert.Equal(20, e.NewValue); + Assert.Equal(BindingPriority.Unset, e.Priority); + ++raised; + }; + + target.CoerceValue(Class1.FooProperty); + + Assert.Equal(20, target.Foo); + Assert.Equal(1, raised); + } + + [Fact] + public void ClearValue_Respects_Coerced_Default_Value() + { + var target = new Class1(); + var raised = 0; + + target.Foo = 30; + target.MinFoo = 20; + + target.PropertyChanged += (s, e) => + { + Assert.Equal(Class1.FooProperty, e.Property); + Assert.Equal(30, e.OldValue); + Assert.Equal(20, e.NewValue); + Assert.Equal(BindingPriority.Unset, e.Priority); + ++raised; + }; + + target.ClearValue(Class1.FooProperty); + + Assert.Equal(20, target.Foo); + Assert.Equal(1, raised); + } + + [Fact] + public void Deactivating_Style_Respects_Coerced_Default_Value() + { + var target = new Control1 + { + MinFoo = 20, + }; + + var root = new TestRoot + { + Styles = + { + new Style(x => x.OfType().Class("foo")) + { + Setters = + { + new Setter(Control1.FooProperty, 50), + }, + }, + }, + Child = target, + }; + + var raised = 0; + + target.Classes.Add("foo"); + root.LayoutManager.ExecuteInitialLayoutPass(); + + Assert.Equal(50, target.Foo); + + target.PropertyChanged += (s, e) => + { + Assert.Equal(Control1.FooProperty, e.Property); + Assert.Equal(50, e.OldValue); + Assert.Equal(20, e.NewValue); + Assert.Equal(BindingPriority.Unset, e.Priority); + ++raised; + }; + + target.Classes.Remove("foo"); + + Assert.Equal(20, target.Foo); + Assert.Equal(1, raised); + } + private class Class1 : AvaloniaObject { public static readonly StyledProperty FooProperty = AvaloniaProperty.Register( - "Qux", + "Foo", defaultValue: 11, coerce: CoerceFoo); @@ -215,13 +310,15 @@ namespace Avalonia.Base.UnitTests set => SetValue(InheritedProperty, value); } + public int MinFoo { get; set; } = 0; public int MaxFoo { get; set; } = 100; public List CoreChanges { get; } = new(); public static int CoerceFoo(AvaloniaObject instance, int value) { - return Math.Min(((Class1)instance).MaxFoo, value); + var o = (Class1)instance; + return Math.Clamp(value, o.MinFoo, o.MaxFoo); } protected override void OnPropertyChangedCore(AvaloniaPropertyChangedEventArgs change) @@ -266,5 +363,29 @@ namespace Avalonia.Base.UnitTests return -value; } } + + private class Control1 : Control + { + public static readonly StyledProperty FooProperty = + AvaloniaProperty.Register( + "Foo", + defaultValue: 11, + coerce: CoerceFoo); + + public int Foo + { + get => GetValue(FooProperty); + set => SetValue(FooProperty, value); + } + + public int MinFoo { get; set; } = 0; + public int MaxFoo { get; set; } = 100; + + public static int CoerceFoo(AvaloniaObject instance, int value) + { + var o = (Control1)instance; + return Math.Clamp(value, o.MinFoo, o.MaxFoo); + } + } } } diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs b/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs index bde750efdc..e44c15d962 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs @@ -179,6 +179,11 @@ namespace Avalonia.Base.UnitTests throw new NotImplementedException(); } + internal override void RouteCoerceDefaultValue(AvaloniaObject o) + { + throw new NotImplementedException(); + } + internal override object RouteGetValue(AvaloniaObject o) { throw new NotImplementedException(); From 964f30e883b31d8f8825e64f68d49d3fecf57475 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 28 Mar 2023 14:50:10 +0200 Subject: [PATCH 12/26] Describe an edge-case. --- .../AvaloniaObjectTests_Coercion.cs | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Coercion.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Coercion.cs index d53c055639..fe4262331f 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Coercion.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Coercion.cs @@ -277,6 +277,21 @@ namespace Avalonia.Base.UnitTests Assert.Equal(1, raised); } + [Fact] + public void If_Initial_State_Has_Coerced_Default_Value_Then_CoerceValue_Must_Be_Called() + { + // This test is just explicitly describing an edge-case. If the initial state of the + // object results in a coerced property value then CoerceValue must be called before + // coercion takes effect. Confirmed as matching the behavior of WPF. + var target = new Class3(); + + Assert.Equal(11, target.Foo); + + target.CoerceValue(Class3.FooProperty); + + Assert.Equal(50, target.Foo); + } + private class Class1 : AvaloniaObject { public static readonly StyledProperty FooProperty = @@ -364,6 +379,28 @@ namespace Avalonia.Base.UnitTests } } + private class Class3: AvaloniaObject + { + public static readonly StyledProperty FooProperty = + AvaloniaProperty.Register( + "Foo", + defaultValue: 11, + coerce: CoerceFoo); + + public int Foo + { + get => GetValue(FooProperty); + set => SetValue(FooProperty, value); + } + + + public static int CoerceFoo(AvaloniaObject instance, int value) + { + var o = (Class3)instance; + return Math.Clamp(value, 50, 100); + } + } + private class Control1 : Control { public static readonly StyledProperty FooProperty = From 055f8940123e0d704e457f379eb5400a5484f95c Mon Sep 17 00:00:00 2001 From: Tom Edwards Date: Thu, 16 Mar 2023 21:38:03 +0100 Subject: [PATCH 13/26] Added conversion option to PropertyObservable --- src/Avalonia.Base/AvaloniaObjectExtensions.cs | 41 +++++++-- .../Data/Core/AvaloniaPropertyAccessorNode.cs | 2 +- .../AvaloniaPropertyBindingObservable.cs | 85 +++++++++++++++---- .../Reactive/AvaloniaPropertyObservable.cs | 73 +++++++++++++--- 4 files changed, 162 insertions(+), 39 deletions(-) diff --git a/src/Avalonia.Base/AvaloniaObjectExtensions.cs b/src/Avalonia.Base/AvaloniaObjectExtensions.cs index 9fbf680a5c..0c22213d33 100644 --- a/src/Avalonia.Base/AvaloniaObjectExtensions.cs +++ b/src/Avalonia.Base/AvaloniaObjectExtensions.cs @@ -1,6 +1,6 @@ using System; -using Avalonia.Reactive; using Avalonia.Data; +using Avalonia.Reactive; namespace Avalonia { @@ -34,8 +34,8 @@ namespace Avalonia /// public static IObservable GetObservable(this AvaloniaObject o, AvaloniaProperty property) { - return new AvaloniaPropertyObservable( - o ?? throw new ArgumentNullException(nameof(o)), + return new AvaloniaPropertyObservable( + o ?? throw new ArgumentNullException(nameof(o)), property ?? throw new ArgumentNullException(nameof(property))); } @@ -54,11 +54,23 @@ namespace Avalonia /// public static IObservable GetObservable(this AvaloniaObject o, AvaloniaProperty property) { - return new AvaloniaPropertyObservable( + return new AvaloniaPropertyObservable( o ?? throw new ArgumentNullException(nameof(o)), property ?? throw new ArgumentNullException(nameof(property))); } + /// + /// + /// + /// A method which is executed to convert each property value to . + public static IObservable GetObservable(this AvaloniaObject o, AvaloniaProperty property, Func converter) + { + return new AvaloniaPropertyObservable( + o ?? throw new ArgumentNullException(nameof(o)), + property ?? throw new ArgumentNullException(nameof(property)), + converter ?? throw new ArgumentNullException(nameof(converter))); + } + /// /// Gets an observable for an . /// @@ -75,7 +87,7 @@ namespace Avalonia this AvaloniaObject o, AvaloniaProperty property) { - return new AvaloniaPropertyBindingObservable( + return new AvaloniaPropertyBindingObservable( o ?? throw new ArgumentNullException(nameof(o)), property ?? throw new ArgumentNullException(nameof(property))); } @@ -97,12 +109,27 @@ namespace Avalonia this AvaloniaObject o, AvaloniaProperty property) { - return new AvaloniaPropertyBindingObservable( + return new AvaloniaPropertyBindingObservable( o ?? throw new ArgumentNullException(nameof(o)), property ?? throw new ArgumentNullException(nameof(property))); } + /// + /// + /// + /// A method which is executed to convert each property value to . + public static IObservable> GetBindingObservable( + this AvaloniaObject o, + AvaloniaProperty property, + Func converter) + { + return new AvaloniaPropertyBindingObservable( + o ?? throw new ArgumentNullException(nameof(o)), + property ?? throw new ArgumentNullException(nameof(property)), + converter ?? throw new ArgumentNullException(nameof(converter))); + } + /// /// Gets an observable that listens for property changed events for an /// . @@ -338,7 +365,7 @@ namespace Avalonia return InstancedBinding.OneWay(_source); } } - + private class ClassHandlerObserver : IObserver> { private readonly Action> _action; diff --git a/src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs b/src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs index 92fc843394..4bf24e901e 100644 --- a/src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs +++ b/src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs @@ -40,7 +40,7 @@ namespace Avalonia.Data.Core { if (reference.TryGetTarget(out var target) && target is AvaloniaObject obj) { - _subscription = new AvaloniaPropertyObservable(obj, _property).Subscribe(ValueChanged); + _subscription = new AvaloniaPropertyObservable(obj, _property).Subscribe(ValueChanged); } else { diff --git a/src/Avalonia.Base/Reactive/AvaloniaPropertyBindingObservable.cs b/src/Avalonia.Base/Reactive/AvaloniaPropertyBindingObservable.cs index 0789684eff..fd68381d55 100644 --- a/src/Avalonia.Base/Reactive/AvaloniaPropertyBindingObservable.cs +++ b/src/Avalonia.Base/Reactive/AvaloniaPropertyBindingObservable.cs @@ -4,18 +4,21 @@ using Avalonia.Data; namespace Avalonia.Reactive { - internal class AvaloniaPropertyBindingObservable : LightweightObservableBase>, IDescription + internal class AvaloniaPropertyBindingObservable : LightweightObservableBase>, IDescription { private readonly WeakReference _target; private readonly AvaloniaProperty _property; - private BindingValue _value = BindingValue.Unset; + private readonly Func? _converter; + private BindingValue _value = BindingValue.Unset; public AvaloniaPropertyBindingObservable( AvaloniaObject target, - AvaloniaProperty property) + AvaloniaProperty property, + Func? converter = null) { _target = new WeakReference(target); _property = property; + _converter = converter; } public string Description => $"{_target.GetType().Name}.{_property.Name}"; @@ -24,8 +27,17 @@ namespace Avalonia.Reactive { if (_target.TryGetTarget(out var target)) { - _value = (T)target.GetValue(_property)!; - target.PropertyChanged += PropertyChanged; + if (_converter is { } converter) + { + var unconvertedValue = (TSource)target.GetValue(_property)!; + _value = converter(unconvertedValue); + target.PropertyChanged += PropertyChanged_WithConversion; + } + else + { + _value = (TResult)target.GetValue(_property)!; + target.PropertyChanged += PropertyChanged; + } } } @@ -33,11 +45,18 @@ namespace Avalonia.Reactive { if (_target.TryGetTarget(out var target)) { - target.PropertyChanged -= PropertyChanged; + if (_converter is not null) + { + target.PropertyChanged -= PropertyChanged_WithConversion; + } + else + { + target.PropertyChanged -= PropertyChanged; + } } } - protected override void Subscribed(IObserver> observer, bool first) + protected override void Subscribed(IObserver> observer, bool first) { if (_value.Type != BindingValueType.UnsetValue) { @@ -49,27 +68,59 @@ namespace Avalonia.Reactive { if (e.Property == _property) { - if (e is AvaloniaPropertyChangedEventArgs typedArgs) + if (e is AvaloniaPropertyChangedEventArgs typedArgs) { - var newValue = e.Sender.GetValue(typedArgs.Property); + PublishValue(e.Sender.GetValue(typedArgs.Property)); + } + else + { + PublishUntypedValue(e.Sender.GetValue(e.Property)); + } + } + } - if (!_value.HasValue || !EqualityComparer.Default.Equals(newValue, _value.Value)) - { - _value = newValue; - PublishNext(_value); - } + private void PropertyChanged_WithConversion(object? sender, AvaloniaPropertyChangedEventArgs e) + { + if (e.Property == _property) + { + if (e is AvaloniaPropertyChangedEventArgs typedArgs) + { + var newValueRaw = e.Sender.GetValue(typedArgs.Property); + + var newValue = _converter!(newValueRaw); + + PublishValue(newValue); } else { var newValue = e.Sender.GetValue(e.Property); - if (!Equals(newValue, _value)) + if (newValue is TSource source) { - _value = (T)newValue!; - PublishNext(_value); + newValue = _converter!(source); } + + PublishUntypedValue(newValue); } } } + + private void PublishValue(TResult newValue) + { + if (!_value.HasValue || !EqualityComparer.Default.Equals(newValue, _value.Value)) + { + _value = newValue; + PublishNext(_value); + } + } + + private void PublishUntypedValue(object? newValue) + { + if (!Equals(newValue, _value)) + { + _value = (TResult)newValue!; + PublishNext(_value); + } + } } } diff --git a/src/Avalonia.Base/Reactive/AvaloniaPropertyObservable.cs b/src/Avalonia.Base/Reactive/AvaloniaPropertyObservable.cs index a4fa587a50..0d40fa96e6 100644 --- a/src/Avalonia.Base/Reactive/AvaloniaPropertyObservable.cs +++ b/src/Avalonia.Base/Reactive/AvaloniaPropertyObservable.cs @@ -4,18 +4,21 @@ using Avalonia.Data; namespace Avalonia.Reactive { - internal class AvaloniaPropertyObservable : LightweightObservableBase, IDescription + internal class AvaloniaPropertyObservable : LightweightObservableBase, IDescription { private readonly WeakReference _target; private readonly AvaloniaProperty _property; - private Optional _value; + private readonly Func? _converter; + private Optional _value; public AvaloniaPropertyObservable( AvaloniaObject target, - AvaloniaProperty property) + AvaloniaProperty property, + Func? converter = null) { _target = new WeakReference(target); _property = property; + _converter = converter; } public string Description => $"{_target.GetType().Name}.{_property.Name}"; @@ -24,8 +27,17 @@ namespace Avalonia.Reactive { if (_target.TryGetTarget(out var target)) { - _value = (T)target.GetValue(_property)!; - target.PropertyChanged += PropertyChanged; + if (_converter is { } converter) + { + var unconvertedValue = (TSource)target.GetValue(_property)!; + _value = converter(unconvertedValue); + target.PropertyChanged += PropertyChanged_WithConversion; + } + else + { + _value = (TResult)target.GetValue(_property)!; + target.PropertyChanged += PropertyChanged; + } } } @@ -33,13 +45,20 @@ namespace Avalonia.Reactive { if (_target.TryGetTarget(out var target)) { - target.PropertyChanged -= PropertyChanged; + if (_converter is not null) + { + target.PropertyChanged -= PropertyChanged_WithConversion; + } + else + { + target.PropertyChanged -= PropertyChanged; + } } _value = default; } - protected override void Subscribed(IObserver observer, bool first) + protected override void Subscribed(IObserver observer, bool first) { if (_value.HasValue) observer.OnNext(_value.Value); @@ -49,23 +68,49 @@ namespace Avalonia.Reactive { if (e.Property == _property) { - T newValue; + TResult newValue; - if (e is AvaloniaPropertyChangedEventArgs typed) + if (e is AvaloniaPropertyChangedEventArgs typed) { newValue = AvaloniaObjectExtensions.GetValue(e.Sender, typed.Property); } else { - newValue = (T)e.Sender.GetValue(e.Property)!; + newValue = (TResult)e.Sender.GetValue(e.Property)!; } - if (!_value.HasValue || - !EqualityComparer.Default.Equals(newValue, _value.Value)) + PublishNewValue(newValue); + } + } + + private void PropertyChanged_WithConversion(object? sender, AvaloniaPropertyChangedEventArgs e) + { + if (e.Property == _property) + { + TSource newValueRaw; + + if (e is AvaloniaPropertyChangedEventArgs typed) { - _value = newValue; - PublishNext(_value.Value!); + newValueRaw = AvaloniaObjectExtensions.GetValue(e.Sender, typed.Property); } + else + { + newValueRaw = (TSource)e.Sender.GetValue(e.Property)!; + } + + var newValue = _converter!(newValueRaw); + + PublishNewValue(newValue); + } + } + + private void PublishNewValue(TResult newValue) + { + if (!_value.HasValue || + !EqualityComparer.Default.Equals(newValue, _value.Value)) + { + _value = newValue; + PublishNext(_value.Value!); } } } From 18daa4cf37c77da4642de2074fb90ad6d914aa9f Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 28 Mar 2023 18:34:26 +0200 Subject: [PATCH 14/26] Make ItemsControl.Items read-only. --- src/Avalonia.Controls/ItemCollection.cs | 14 ---- src/Avalonia.Controls/ItemsControl.cs | 91 ++++++------------------- 2 files changed, 20 insertions(+), 85 deletions(-) diff --git a/src/Avalonia.Controls/ItemCollection.cs b/src/Avalonia.Controls/ItemCollection.cs index c9265558f0..03f46551c5 100644 --- a/src/Avalonia.Controls/ItemCollection.cs +++ b/src/Avalonia.Controls/ItemCollection.cs @@ -106,19 +106,6 @@ namespace Avalonia.Controls } } - internal IList? GetItemsPropertyValue() - { - if (_mode == Mode.ObsoleteItemsSetter) - return Source == s_uninitialized ? null : Source; - return this; - } - - internal void SetItems(IList? items) - { - _mode = Mode.ObsoleteItemsSetter; - SetSource(items ?? s_uninitialized); - } - internal void SetItemsSource(IEnumerable? value) { if (_mode != Mode.ItemsSource && Count > 0) @@ -159,7 +146,6 @@ namespace Avalonia.Controls { Items, ItemsSource, - ObsoleteItemsSetter, } } } diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index 1123f42afa..6a91428147 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -30,17 +30,6 @@ namespace Avalonia.Controls private static readonly FuncTemplate DefaultPanel = new(() => new StackPanel()); - /// - /// Defines the property. - /// - public static readonly DirectProperty ItemsProperty = - AvaloniaProperty.RegisterDirect( - nameof(Items), - o => o.Items, -#pragma warning disable CS0618 // Type or member is obsolete - (o, v) => o.Items = v); -#pragma warning restore CS0618 // Type or member is obsolete - /// /// Defines the property. /// @@ -94,7 +83,6 @@ namespace Avalonia.Controls /// [AssignBinding] [InheritDataTypeFromItems(nameof(ItemsSource))] - [InheritDataTypeFromItems(nameof(Items))] public IBinding? DisplayMemberBinding { get => GetValue(DisplayMemberBindingProperty); @@ -129,48 +117,20 @@ namespace Avalonia.Controls } /// - /// Gets or sets the items to display. + /// Gets the items to display. /// /// - /// Since Avalonia 11, has both an property - /// and an property. The properties have the following differences: - /// - /// - /// is initialized with an empty collection and is a direct property, - /// meaning that it cannot be styled - /// is by default null, and is a styled property. This property - /// is marked as the content property and will be used for items added via inline XAML. - /// - /// - /// In Avalonia 11 the two properties can be used almost interchangeably but this will change - /// in a later version. In order to be ready for this change, follow the following guidance: - /// - /// - /// You should use the property when you're assigning a collection of - /// item containers directly, for example adding a collection of s - /// directly to a . Add the containers to the pre-existing list, do not - /// reassign the property via the setter or with a binding. - /// You should use the property when you're assigning or - /// binding a collection of models which will be transformed by a data template. - /// + /// You use either the or the property to + /// specify the collection that should be used to generate the content of your + /// . When the property is set, the + /// collection is made read-only and fixed-size. + /// + /// When is in use, setting the + /// property to null removes the collection and restores usage to , + /// which will be an empty . /// [Content] - public IList? Items - { - get => _items.GetItemsPropertyValue(); - - [Obsolete("Use ItemsSource to set or bind items.")] - set - { - var oldItems = _items.GetItemsPropertyValue(); - - if (value != oldItems) - { - _items.SetItems(value); - RaisePropertyChanged(ItemsProperty, oldItems, value); - } - } - } + public ItemCollection Items => _items; /// /// Gets or sets the that is applied to the container element generated for each item. @@ -210,27 +170,17 @@ namespace Avalonia.Controls /// Gets or sets a collection used to generate the content of the . /// /// - /// Since Avalonia 11, has both an property - /// and an property. The properties have the following differences: - /// - /// - /// is initialized with an empty collection and is a direct property, - /// meaning that it cannot be styled - /// is by default null, and is a styled property. This property - /// is marked as the content property and will be used for items added via inline XAML. - /// - /// - /// In Avalonia 11 the two properties can be used almost interchangeably but this will change - /// in a later version. In order to be ready for this change, follow the following guidance: + /// A common scenario is to use an such as a + /// to display a data collection, or to bind an + /// to a collection object. To bind an + /// to a collection object, use the property. /// - /// - /// You should use the property when you're assigning a collection of - /// item containers directly, for example adding a collection of s - /// directly to a . Add the containers to the pre-existing list, do not - /// reassign the property via the setter or with a binding. - /// You should use the property when you're assigning or - /// binding a collection of models which will be transformed by a data template. - /// + /// When the property is set, the collection + /// is made read-only and fixed-size. + /// + /// When is in use, setting the property to null removes the + /// collection and restores usage to , which will be an empty + /// . /// public IEnumerable? ItemsSource { @@ -242,7 +192,6 @@ namespace Avalonia.Controls /// Gets or sets the data template used to display the items in the control. /// [InheritDataTypeFromItems(nameof(ItemsSource))] - [InheritDataTypeFromItems(nameof(Items))] public IDataTemplate? ItemTemplate { get => GetValue(ItemTemplateProperty); From 996578f2bf67b0001d63cce08afb6a84d1e376bb Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 28 Mar 2023 18:34:43 +0200 Subject: [PATCH 15/26] Remove all remaining uses of Items setter. --- samples/BindingDemo/MainWindow.xaml | 4 ++-- samples/ControlCatalog/Pages/TabControlPage.xaml | 2 +- samples/VirtualizationDemo/MainWindow.xaml | 8 ++++---- src/Avalonia.Controls/Flyouts/MenuFlyout.cs | 2 +- src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs | 2 +- .../Xaml/ControlBindingTests.cs | 4 ++-- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/samples/BindingDemo/MainWindow.xaml b/samples/BindingDemo/MainWindow.xaml index 08ac0426ea..d1c65ca73b 100644 --- a/samples/BindingDemo/MainWindow.xaml +++ b/samples/BindingDemo/MainWindow.xaml @@ -75,11 +75,11 @@ - + - + diff --git a/samples/ControlCatalog/Pages/TabControlPage.xaml b/samples/ControlCatalog/Pages/TabControlPage.xaml index a775056ebe..3a2464e9fd 100644 --- a/samples/ControlCatalog/Pages/TabControlPage.xaml +++ b/samples/ControlCatalog/Pages/TabControlPage.xaml @@ -51,7 +51,7 @@ Text="From DataTemplate"> diff --git a/samples/VirtualizationDemo/MainWindow.xaml b/samples/VirtualizationDemo/MainWindow.xaml index 235f3ef2cc..3aee63c246 100644 --- a/samples/VirtualizationDemo/MainWindow.xaml +++ b/samples/VirtualizationDemo/MainWindow.xaml @@ -11,7 +11,7 @@ Margin="16 0 0 0" Width="150" Spacing="4"> - Horiz. ScrollBar - Vert. ScrollBar - - + - + From 060e290fc1a77451b739135f17aa25176956738b Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 28 Mar 2023 18:36:33 +0200 Subject: [PATCH 16/26] Revert "Support multiple `InheritDataTypeFromItems`." This reverts commit 57c997bed79a73ffa4bdb6d10a04c24077aff744. --- .../InheritDataTypeFromItemsAttribute.cs | 2 +- .../Primitives/SelectingItemsControl.cs | 1 - ...valoniaXamlIlDataContextTypeTransformer.cs | 43 ++++++++----------- 3 files changed, 20 insertions(+), 26 deletions(-) diff --git a/src/Avalonia.Base/Metadata/InheritDataTypeFromItemsAttribute.cs b/src/Avalonia.Base/Metadata/InheritDataTypeFromItemsAttribute.cs index e9bd6ab89f..fac8cd8737 100644 --- a/src/Avalonia.Base/Metadata/InheritDataTypeFromItemsAttribute.cs +++ b/src/Avalonia.Base/Metadata/InheritDataTypeFromItemsAttribute.cs @@ -9,7 +9,7 @@ namespace Avalonia.Metadata; /// A typical usage example is a ListBox control, where is defined on the ItemTemplate property, /// allowing the template to inherit the data type from the Items collection binding. /// -[AttributeUsage(AttributeTargets.Property, AllowMultiple = true, Inherited = true)] +[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] public sealed class InheritDataTypeFromItemsAttribute : Attribute { /// diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index b89a75787f..9c060f2258 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -235,7 +235,6 @@ namespace Avalonia.Controls.Primitives /// [AssignBinding] [InheritDataTypeFromItems(nameof(ItemsSource))] - [InheritDataTypeFromItems(nameof(Items))] public IBinding? SelectedValueBinding { get => GetValue(SelectedValueBindingProperty); diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs index 681d2a38d4..a24d4eb6e9 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs @@ -73,32 +73,27 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers // Infer data type from collection binding on a control that displays items. var property = context.ParentNodes().OfType().FirstOrDefault(); var attributeType = context.GetAvaloniaTypes().InheritDataTypeFromItemsAttribute; - var attributes = property?.Property?.GetClrProperty().CustomAttributes - .Where(a => a.Type == attributeType).ToList(); - - if (attributes?.Count > 0) + var attribute = property?.Property?.GetClrProperty().CustomAttributes + .FirstOrDefault(a => a.Type == attributeType); + + if (attribute is not null) { - foreach (var attribute in attributes) + var propertyName = (string)attribute.Parameters.First(); + XamlAstConstructableObjectNode parentObject; + if (attribute.Properties.TryGetValue("AncestorType", out var type) + && type is IXamlType xamlType) { - var propertyName = (string)attribute.Parameters.First(); - XamlAstConstructableObjectNode parentObject; - if (attribute.Properties.TryGetValue("AncestorType", out var type) - && type is IXamlType xamlType) - { - parentObject = context.ParentNodes().OfType() - .FirstOrDefault(n => n.Type.GetClrType().FullName == xamlType.FullName); - } - else - { - parentObject = context.ParentNodes().OfType().FirstOrDefault(); - } - - if (parentObject != null) - { - inferredDataContextTypeNode = InferDataContextOfPresentedItem(context, on, parentObject, propertyName); - if (inferredDataContextTypeNode != null) - break; - } + parentObject = context.ParentNodes().OfType() + .FirstOrDefault(n => n.Type.GetClrType().FullName == xamlType.FullName); + } + else + { + parentObject = context.ParentNodes().OfType().FirstOrDefault(); + } + + if (parentObject != null) + { + inferredDataContextTypeNode = InferDataContextOfPresentedItem(context, on, parentObject, propertyName); } } From 6535e299b02e3f832ad1b6d88ec1fddcf631f3fc Mon Sep 17 00:00:00 2001 From: Lubomir Tetak Date: Wed, 29 Mar 2023 09:49:22 +0200 Subject: [PATCH 17/26] DataGrid column resize near edge fix --- src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs b/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs index b3e106a7bf..3db490d645 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs @@ -724,7 +724,7 @@ namespace Avalonia.Controls } //handle entry into reorder mode - if (_dragMode == DragMode.MouseDown && _dragColumn == null && _lastMousePositionHeaders != null && (distanceFromRight > DATAGRIDCOLUMNHEADER_resizeRegionWidth && distanceFromLeft > DATAGRIDCOLUMNHEADER_resizeRegionWidth)) + if (_dragMode == DragMode.MouseDown && _dragColumn == null && _lastMousePositionHeaders != null) { var distanceFromInitial = (Vector)(mousePositionHeaders - _lastMousePositionHeaders); if (distanceFromInitial.Length > DATAGRIDCOLUMNHEADER_columnsDragTreshold) From f829cdcdcd5e6d3a89e8fe669843a6b3f99fc2bd Mon Sep 17 00:00:00 2001 From: Lubomir Tetak Date: Wed, 29 Mar 2023 10:01:39 +0200 Subject: [PATCH 18/26] cleanup --- src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs b/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs index 3db490d645..5250f80f77 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs @@ -440,13 +440,10 @@ namespace Avalonia.Controls } Debug.Assert(OwningGrid.Parent is InputElement); - - double distanceFromLeft = mousePosition.X; - double distanceFromRight = Bounds.Width - distanceFromLeft; - + OnMouseMove_Resize(ref handled, mousePositionHeaders); - OnMouseMove_Reorder(ref handled, mousePosition, mousePositionHeaders, distanceFromLeft, distanceFromRight); + OnMouseMove_Reorder(ref handled, mousePosition, mousePositionHeaders); SetDragCursor(mousePosition); } @@ -716,7 +713,7 @@ namespace Avalonia.Controls } //TODO DragEvents - private void OnMouseMove_Reorder(ref bool handled, Point mousePosition, Point mousePositionHeaders, double distanceFromLeft, double distanceFromRight) + private void OnMouseMove_Reorder(ref bool handled, Point mousePosition, Point mousePositionHeaders) { if (handled) { From 13a586077fbf6df0bc5ba7f400922edc8529d023 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 29 Mar 2023 10:24:52 +0200 Subject: [PATCH 19/26] Bind readonly properties one-way. Fixes #10680. --- .../Diagnostics/Views/PropertyValueEditorView.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/PropertyValueEditorView.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/PropertyValueEditorView.cs index 6b52989f0b..2811d895c1 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/PropertyValueEditorView.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/PropertyValueEditorView.cs @@ -64,11 +64,12 @@ namespace Avalonia.Diagnostics.Views where TControl : Control, new() { var control = new TControl(); + var bindingMode = Property.IsReadonly ? BindingMode.OneWay : BindingMode.TwoWay; init?.Invoke(control); control.Bind(valueProperty, - new Binding(nameof(Property.Value), BindingMode.TwoWay) + new Binding(nameof(Property.Value), bindingMode) { Source = Property, Converter = converter ?? new ValueConverter(), From f432d6f0a90a87b0f7e1188f5372c7bca2b6f00d Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Wed, 29 Mar 2023 19:42:24 +0200 Subject: [PATCH 20/26] Fix GetTextBounds for combinations of EndOfParargrap and embedded RTL text --- .../Media/TextFormatting/TextLineImpl.cs | 18 +++++++++++-- .../Media/TextFormatting/TextLineTests.cs | 27 +++++++++++++++++++ 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index f5812f71ff..3264d5e88a 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -701,7 +701,14 @@ namespace Avalonia.Media.TextFormatting if (directionalWidth == 0) { //In case a run only contains a linebreak we don't want to skip it. - if (currentRun is ShapedTextRun shaped && currentRun.Length - shaped.GlyphRun.Metrics.NewLineLength > 0) + if (currentRun is ShapedTextRun shaped) + { + if(currentRun.Length - shaped.GlyphRun.Metrics.NewLineLength > 0) + { + continue; + } + } + else { continue; } @@ -840,7 +847,14 @@ namespace Avalonia.Media.TextFormatting if (directionalWidth == 0) { //In case a run only contains a linebreak we don't want to skip it. - if (currentRun is ShapedTextRun shaped && currentRun.Length - shaped.GlyphRun.Metrics.NewLineLength > 0) + if (currentRun is ShapedTextRun shaped) + { + if (currentRun.Length - shaped.GlyphRun.Metrics.NewLineLength > 0) + { + continue; + } + } + else { continue; } diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs index 0ddcb2452d..d605ecbfda 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs @@ -994,6 +994,33 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting } } + [Fact] + public void Should_GetTextBounds_With_EndOfParagraph_RightToLeft() + { + var text = "لوحة المفاتيح العربية"; + + using (Start()) + { + var defaultProperties = new GenericTextRunProperties(Typeface.Default); + var textSource = new SingleBufferTextSource(text, defaultProperties, true); + + var formatter = new TextFormatterImpl(); + + var textLine = + formatter.FormatLine(textSource, 0, double.PositiveInfinity, + new GenericTextParagraphProperties(FlowDirection.LeftToRight, TextAlignment.Left, + true, true, defaultProperties, TextWrapping.NoWrap, 0, 0, 0)); + + var textBounds = textLine.GetTextBounds(0, 1); + + Assert.Equal(1, textBounds.Count); + + var firstBounds = textBounds.First(); + + Assert.True(firstBounds.TextRunBounds.Count > 0); + } + } + private class FixedRunsTextSource : ITextSource { private readonly IReadOnlyList _textRuns; From 312f1250e27a08b01e2a1a594531e560fab5820c Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Thu, 30 Mar 2023 00:25:18 +0600 Subject: [PATCH 21/26] Don't use AvaloniaObject in IPlatformRenderInterface --- src/Avalonia.Base/CombinedGeometry.cs | 14 +++++--------- src/Avalonia.Base/Media/GeometryGroup.cs | 5 ++++- .../Platform/IPlatformRenderInterface.cs | 4 ++-- .../HeadlessPlatformRenderInterface.cs | 4 ++-- src/Skia/Avalonia.Skia/CombinedGeometryImpl.cs | 7 ++++--- src/Skia/Avalonia.Skia/GeometryGroupImpl.cs | 7 ++++--- src/Skia/Avalonia.Skia/PlatformRenderInterface.cs | 4 ++-- .../Avalonia.Direct2D1/Direct2D1Platform.cs | 4 ++-- .../Media/CombinedGeometryImpl.cs | 13 +++++++------ .../Avalonia.Direct2D1/Media/GeometryGroupImpl.cs | 7 ++++--- .../VisualTree/MockRenderInterface.cs | 4 ++-- tests/Avalonia.Benchmarks/NullRenderingPlatform.cs | 4 ++-- .../MockPlatformRenderInterface.cs | 4 ++-- 13 files changed, 42 insertions(+), 39 deletions(-) diff --git a/src/Avalonia.Base/CombinedGeometry.cs b/src/Avalonia.Base/CombinedGeometry.cs index 8f080d05c7..4b5866519b 100644 --- a/src/Avalonia.Base/CombinedGeometry.cs +++ b/src/Avalonia.Base/CombinedGeometry.cs @@ -152,19 +152,15 @@ namespace Avalonia.Media var g1 = Geometry1; var g2 = Geometry2; - if (g1 is object && g2 is object) + if (g1?.PlatformImpl != null && g2?.PlatformImpl != null) { var factory = AvaloniaLocator.Current.GetRequiredService(); - return factory.CreateCombinedGeometry(GeometryCombineMode, g1, g2); + return factory.CreateCombinedGeometry(GeometryCombineMode, g1.PlatformImpl, g2.PlatformImpl); } - else if (GeometryCombineMode == GeometryCombineMode.Intersect) - return null; - else if (g1 is object) - return g1.PlatformImpl; - else if (g2 is object) - return g2.PlatformImpl; - else + + if (GeometryCombineMode == GeometryCombineMode.Intersect) return null; + return g1?.PlatformImpl ?? g2?.PlatformImpl; } } } diff --git a/src/Avalonia.Base/Media/GeometryGroup.cs b/src/Avalonia.Base/Media/GeometryGroup.cs index 0326e606f4..3e61413919 100644 --- a/src/Avalonia.Base/Media/GeometryGroup.cs +++ b/src/Avalonia.Base/Media/GeometryGroup.cs @@ -78,7 +78,10 @@ namespace Avalonia.Media { var factory = AvaloniaLocator.Current.GetRequiredService(); - return factory.CreateGeometryGroup(FillRule, _children); + var children = new IGeometryImpl?[_children.Count]; + for (var c = 0; c < _children.Count; c++) + children[c] = _children[c].PlatformImpl; + return factory.CreateGeometryGroup(FillRule, children!); } return null; diff --git a/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs b/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs index cfc7fac3ea..81fe2c046f 100644 --- a/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs +++ b/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs @@ -48,7 +48,7 @@ namespace Avalonia.Platform /// The fill rule. /// The geometries to group. /// A combined geometry. - IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children); + IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children); /// /// Creates a geometry group implementation. @@ -57,7 +57,7 @@ namespace Avalonia.Platform /// The first geometry. /// The second geometry. /// A combined geometry. - IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2); + IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, IGeometryImpl g1, IGeometryImpl g2); /// /// Created a geometry implementation for the glyph run. diff --git a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs index f8100d3832..431989134a 100644 --- a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs +++ b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs @@ -47,8 +47,8 @@ namespace Avalonia.Headless } public IStreamGeometryImpl CreateStreamGeometry() => new HeadlessStreamingGeometryStub(); - public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children) => throw new NotImplementedException(); - public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2) => throw new NotImplementedException(); + public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children) => throw new NotImplementedException(); + public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, IGeometryImpl g1, IGeometryImpl g2) => throw new NotImplementedException(); public IRenderTarget CreateRenderTarget(IEnumerable surfaces) => new HeadlessRenderTarget(); public bool IsLost => false; diff --git a/src/Skia/Avalonia.Skia/CombinedGeometryImpl.cs b/src/Skia/Avalonia.Skia/CombinedGeometryImpl.cs index 1a4f467f40..1e9240ec70 100644 --- a/src/Skia/Avalonia.Skia/CombinedGeometryImpl.cs +++ b/src/Skia/Avalonia.Skia/CombinedGeometryImpl.cs @@ -1,4 +1,5 @@ using Avalonia.Media; +using Avalonia.Platform; using SkiaSharp; namespace Avalonia.Skia @@ -15,10 +16,10 @@ namespace Avalonia.Skia Bounds = (stroke ?? fill)?.TightBounds.ToAvaloniaRect() ?? default; } - public static CombinedGeometryImpl ForceCreate(GeometryCombineMode combineMode, Geometry g1, Geometry g2) + public static CombinedGeometryImpl ForceCreate(GeometryCombineMode combineMode, IGeometryImpl g1, IGeometryImpl g2) { - if (g1.PlatformImpl is GeometryImpl i1 - && g2.PlatformImpl is GeometryImpl i2 + if (g1 is GeometryImpl i1 + && g2 is GeometryImpl i2 && TryCreate(combineMode, i1, i2) is { } result) return result; diff --git a/src/Skia/Avalonia.Skia/GeometryGroupImpl.cs b/src/Skia/Avalonia.Skia/GeometryGroupImpl.cs index 01be42bad0..200247095f 100644 --- a/src/Skia/Avalonia.Skia/GeometryGroupImpl.cs +++ b/src/Skia/Avalonia.Skia/GeometryGroupImpl.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using Avalonia.Media; +using Avalonia.Platform; using SkiaSharp; namespace Avalonia.Skia @@ -9,7 +10,7 @@ namespace Avalonia.Skia /// internal class GeometryGroupImpl : GeometryImpl { - public GeometryGroupImpl(FillRule fillRule, IReadOnlyList children) + public GeometryGroupImpl(FillRule fillRule, IReadOnlyList children) { var fillType = fillRule == FillRule.NonZero ? SKPathFillType.Winding : SKPathFillType.EvenOdd; var count = children.Count; @@ -22,7 +23,7 @@ namespace Avalonia.Skia bool requiresFillPass = false; for (var i = 0; i < count; ++i) { - if (children[i].PlatformImpl is GeometryImpl geo) + if (children[i] is GeometryImpl geo) { if (geo.StrokePath != null) stroke.AddPath(geo.StrokePath); @@ -42,7 +43,7 @@ namespace Avalonia.Skia for (var i = 0; i < count; ++i) { - if (children[i].PlatformImpl is GeometryImpl { FillPath: { } fillPath }) + if (children[i] is GeometryImpl { FillPath: { } fillPath }) fill.AddPath(fillPath); } diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs index 9c4b326f14..a9a79ff0c5 100644 --- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs +++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs @@ -58,12 +58,12 @@ namespace Avalonia.Skia return new StreamGeometryImpl(); } - public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children) + public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children) { return new GeometryGroupImpl(fillRule, children); } - public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2) + public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, IGeometryImpl g1, IGeometryImpl g2) { return CombinedGeometryImpl.ForceCreate(combineMode, g1, g2); } diff --git a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs index 287db92b4d..826296b055 100644 --- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs +++ b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs @@ -158,8 +158,8 @@ namespace Avalonia.Direct2D1 public IGeometryImpl CreateLineGeometry(Point p1, Point p2) => new LineGeometryImpl(p1, p2); public IGeometryImpl CreateRectangleGeometry(Rect rect) => new RectangleGeometryImpl(rect); public IStreamGeometryImpl CreateStreamGeometry() => new StreamGeometryImpl(); - public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children) => new GeometryGroupImpl(fillRule, children); - public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2) => new CombinedGeometryImpl(combineMode, g1, g2); + public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children) => new GeometryGroupImpl(fillRule, children); + public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, IGeometryImpl g1, IGeometryImpl g2) => new CombinedGeometryImpl(combineMode, g1, g2); public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphInfos, Point baselineOrigin) diff --git a/src/Windows/Avalonia.Direct2D1/Media/CombinedGeometryImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/CombinedGeometryImpl.cs index 5a13c10bbc..b1ce160707 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/CombinedGeometryImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/CombinedGeometryImpl.cs @@ -1,3 +1,4 @@ +using Avalonia.Platform; using SharpDX.Direct2D1; using AM = Avalonia.Media; @@ -13,19 +14,19 @@ namespace Avalonia.Direct2D1.Media /// public CombinedGeometryImpl( AM.GeometryCombineMode combineMode, - AM.Geometry geometry1, - AM.Geometry geometry2) + IGeometryImpl geometry1, + IGeometryImpl geometry2) : base(CreateGeometry(combineMode, geometry1, geometry2)) { } private static Geometry CreateGeometry( AM.GeometryCombineMode combineMode, - AM.Geometry geometry1, - AM.Geometry geometry2) + IGeometryImpl geometry1, + IGeometryImpl geometry2) { - var g1 = ((GeometryImpl)geometry1.PlatformImpl).Geometry; - var g2 = ((GeometryImpl)geometry2.PlatformImpl).Geometry; + var g1 = ((GeometryImpl)geometry1).Geometry; + var g2 = ((GeometryImpl)geometry2).Geometry; var dest = new PathGeometry(Direct2D1Platform.Direct2D1Factory); using var sink = dest.Open(); g1.Combine(g2, (CombineMode)combineMode, sink); diff --git a/src/Windows/Avalonia.Direct2D1/Media/GeometryGroupImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/GeometryGroupImpl.cs index 352708bf03..5e49ef6d4e 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/GeometryGroupImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/GeometryGroupImpl.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using Avalonia.Platform; using SharpDX.Direct2D1; using AM = Avalonia.Media; @@ -12,19 +13,19 @@ namespace Avalonia.Direct2D1.Media /// /// Initializes a new instance of the class. /// - public GeometryGroupImpl(AM.FillRule fillRule, IReadOnlyList geometry) + public GeometryGroupImpl(AM.FillRule fillRule, IReadOnlyList geometry) : base(CreateGeometry(fillRule, geometry)) { } - private static Geometry CreateGeometry(AM.FillRule fillRule, IReadOnlyList children) + private static Geometry CreateGeometry(AM.FillRule fillRule, IReadOnlyList children) { var count = children.Count; var c = new Geometry[count]; for (var i = 0; i < count; ++i) { - c[i] = ((GeometryImpl)children[i].PlatformImpl).Geometry; + c[i] = ((GeometryImpl)children[i]).Geometry; } return new GeometryGroup(Direct2D1Platform.Direct2D1Factory, (FillMode)fillRule, c); diff --git a/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs b/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs index 481b98a0b2..d494c47a55 100644 --- a/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs +++ b/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs @@ -30,12 +30,12 @@ namespace Avalonia.Base.UnitTests.VisualTree return new MockStreamGeometry(); } - public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children) + public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children) { throw new NotImplementedException(); } - public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2) + public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, IGeometryImpl g1, IGeometryImpl g2) { throw new NotImplementedException(); } diff --git a/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs b/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs index e5cbae4ae7..d40abd9f47 100644 --- a/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs +++ b/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs @@ -32,12 +32,12 @@ namespace Avalonia.Benchmarks return new MockStreamGeometryImpl(); } - public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children) + public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children) { throw new NotImplementedException(); } - public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2) + public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, IGeometryImpl g1, IGeometryImpl g2) { throw new NotImplementedException(); } diff --git a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs index df128b8ae3..720755f2b0 100644 --- a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs +++ b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs @@ -72,12 +72,12 @@ namespace Avalonia.UnitTests return new MockStreamGeometryImpl(); } - public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children) + public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children) { return Mock.Of(); } - public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2) + public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, IGeometryImpl g1, IGeometryImpl g2) { return Mock.Of(); } From 904be53ffaa891742d3c51ca5359ea5c41bd53fb Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Wed, 29 Mar 2023 08:24:56 +0000 Subject: [PATCH 22/26] merge scroll viewer samples --- samples/ControlCatalog/MainView.xaml | 3 - .../ControlCatalog/Pages/ScrollSnapPage.xaml | 222 -------------- .../Pages/ScrollSnapPage.xaml.cs | 68 ---- .../Pages/ScrollViewerPage.xaml | 290 ++++++++++++++++-- .../Pages/ScrollViewerPage.xaml.cs | 37 +++ 5 files changed, 298 insertions(+), 322 deletions(-) delete mode 100644 samples/ControlCatalog/Pages/ScrollSnapPage.xaml delete mode 100644 samples/ControlCatalog/Pages/ScrollSnapPage.xaml.cs diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index 9f06525821..7ed2d67379 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -147,9 +147,6 @@ - - - diff --git a/samples/ControlCatalog/Pages/ScrollSnapPage.xaml b/samples/ControlCatalog/Pages/ScrollSnapPage.xaml deleted file mode 100644 index fa206f0dff..0000000000 --- a/samples/ControlCatalog/Pages/ScrollSnapPage.xaml +++ /dev/null @@ -1,222 +0,0 @@ - - - Scrollviewer can snap supported content both vertically and horizontally. Snapping occurs from scrolling with touch or pen. - - - - - - - - - - - - - - - - Vertical Snapping - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Horizontal Snapping - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/samples/ControlCatalog/Pages/ScrollSnapPage.xaml.cs b/samples/ControlCatalog/Pages/ScrollSnapPage.xaml.cs deleted file mode 100644 index 384dc67c66..0000000000 --- a/samples/ControlCatalog/Pages/ScrollSnapPage.xaml.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System.Collections.Generic; -using Avalonia.Controls; -using Avalonia.Controls.Primitives; -using Avalonia.Markup.Xaml; -using MiniMvvm; - -namespace ControlCatalog.Pages -{ - public class ScrollSnapPageViewModel : ViewModelBase - { - private SnapPointsType _snapPointsType; - private SnapPointsAlignment _snapPointsAlignment; - private bool _areSnapPointsRegular; - - public ScrollSnapPageViewModel() - { - - AvailableSnapPointsType = new List() - { - SnapPointsType.None, - SnapPointsType.Mandatory, - SnapPointsType.MandatorySingle - }; - - AvailableSnapPointsAlignment = new List() - { - SnapPointsAlignment.Near, - SnapPointsAlignment.Center, - SnapPointsAlignment.Far, - }; - } - - public bool AreSnapPointsRegular - { - get => _areSnapPointsRegular; - set => this.RaiseAndSetIfChanged(ref _areSnapPointsRegular, value); - } - - public SnapPointsType SnapPointsType - { - get => _snapPointsType; - set => this.RaiseAndSetIfChanged(ref _snapPointsType, value); - } - - public SnapPointsAlignment SnapPointsAlignment - { - get => _snapPointsAlignment; - set => this.RaiseAndSetIfChanged(ref _snapPointsAlignment, value); - } - public List AvailableSnapPointsType { get; } - public List AvailableSnapPointsAlignment { get; } - } - - public class ScrollSnapPage : UserControl - { - public ScrollSnapPage() - { - this.InitializeComponent(); - - DataContext = new ScrollSnapPageViewModel(); - } - - private void InitializeComponent() - { - AvaloniaXamlLoader.Load(this); - } - } -} diff --git a/samples/ControlCatalog/Pages/ScrollViewerPage.xaml b/samples/ControlCatalog/Pages/ScrollViewerPage.xaml index 1a3d61eb85..4af61c3399 100644 --- a/samples/ControlCatalog/Pages/ScrollViewerPage.xaml +++ b/samples/ControlCatalog/Pages/ScrollViewerPage.xaml @@ -3,35 +3,267 @@ xmlns:pages="using:ControlCatalog.Pages" x:Class="ControlCatalog.Pages.ScrollViewerPage" x:DataType="pages:ScrollViewerPageViewModel"> - - Allows for horizontal and vertical content scrolling. Supports snapping on touch and pointer wheel scrolling. - - - - - - - - - - - - - - - + + + + Allows for horizontal and vertical content scrolling. Supports snapping on touch and pointer wheel scrolling. + + + + + + + + + + + + + + + + + + + + + + + + + Scrollviewer can snap supported content both vertically and horizontally. Snapping occurs from scrolling with touch or pen. + + + + + + + - - - - - + + + + + + + + Vertical Snapping + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Horizontal Snapping + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/ScrollViewerPage.xaml.cs b/samples/ControlCatalog/Pages/ScrollViewerPage.xaml.cs index a097f1f951..7082ca1bf6 100644 --- a/samples/ControlCatalog/Pages/ScrollViewerPage.xaml.cs +++ b/samples/ControlCatalog/Pages/ScrollViewerPage.xaml.cs @@ -12,6 +12,9 @@ namespace ControlCatalog.Pages private bool _enableInertia; private ScrollBarVisibility _horizontalScrollVisibility; private ScrollBarVisibility _verticalScrollVisibility; + private SnapPointsType _snapPointsType; + private SnapPointsAlignment _snapPointsAlignment; + private bool _areSnapPointsRegular; public ScrollViewerPageViewModel() { @@ -23,6 +26,20 @@ namespace ControlCatalog.Pages ScrollBarVisibility.Disabled, }; + AvailableSnapPointsType = new List() + { + SnapPointsType.None, + SnapPointsType.Mandatory, + SnapPointsType.MandatorySingle + }; + + AvailableSnapPointsAlignment = new List() + { + SnapPointsAlignment.Near, + SnapPointsAlignment.Center, + SnapPointsAlignment.Far, + }; + HorizontalScrollVisibility = ScrollBarVisibility.Auto; VerticalScrollVisibility = ScrollBarVisibility.Auto; AllowAutoHide = true; @@ -54,6 +71,26 @@ namespace ControlCatalog.Pages } public List AvailableVisibility { get; } + + public bool AreSnapPointsRegular + { + get => _areSnapPointsRegular; + set => this.RaiseAndSetIfChanged(ref _areSnapPointsRegular, value); + } + + public SnapPointsType SnapPointsType + { + get => _snapPointsType; + set => this.RaiseAndSetIfChanged(ref _snapPointsType, value); + } + + public SnapPointsAlignment SnapPointsAlignment + { + get => _snapPointsAlignment; + set => this.RaiseAndSetIfChanged(ref _snapPointsAlignment, value); + } + public List AvailableSnapPointsType { get; } + public List AvailableSnapPointsAlignment { get; } } public class ScrollViewerPage : UserControl From 668fa942b70bd36279e980bd58c094c848a316d1 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 30 Mar 2023 22:28:55 +0200 Subject: [PATCH 23/26] Don't leak IPlatformRenderInterface. --- .../Media/GlyphRunTests.cs | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs b/tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs index 43feb75c08..84ce341e98 100644 --- a/tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs +++ b/tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs @@ -1,22 +1,13 @@ using System; -using System.Linq; using Avalonia.Media; using Avalonia.Media.TextFormatting; -using Avalonia.Platform; using Avalonia.UnitTests; -using Avalonia.Utilities; using Xunit; namespace Avalonia.Base.UnitTests.Media { public class GlyphRunTests : TestWithServicesBase { - public GlyphRunTests() - { - AvaloniaLocator.CurrentMutable - .Bind().ToSingleton(); - } - [InlineData(new double[] { 30, 0, 0 }, new int[] { 0, 0, 0 }, 0, 0, 0)] [InlineData(new double[] { 30, 0, 0 }, new int[] { 0, 0, 0 }, 0, 3, 30)] [InlineData(new double[] { 10, 10, 10 }, new int[] { 0, 1, 2 }, 1, 0, 10)] @@ -25,7 +16,7 @@ namespace Avalonia.Base.UnitTests.Media [Theory] public void Should_Get_Distance_From_CharacterHit(double[] advances, int[] clusters, int start, int trailingLength, double expectedDistance) { - using(UnitTestApplication.Start(TestServices.StyledWindow)) + using (Start()) using (var glyphRun = CreateGlyphRun(advances, clusters)) { var characterHit = new CharacterHit(start, trailingLength); @@ -44,7 +35,7 @@ namespace Avalonia.Base.UnitTests.Media public void Should_Get_CharacterHit_FromDistance(double[] advances, int[] clusters, double distance, int start, int trailingLengthExpected, bool isInsideExpected) { - using(UnitTestApplication.Start(TestServices.StyledWindow)) + using (Start()) using (var glyphRun = CreateGlyphRun(advances, clusters)) { var textBounds = glyphRun.GetCharacterHitFromDistance(distance, out var isInside); @@ -190,5 +181,11 @@ namespace Avalonia.Base.UnitTests.Media return new GlyphRun(new MockGlyphTypeface(), 10, new string('a', count).AsMemory(), glyphInfos, biDiLevel: bidiLevel); } + + private static IDisposable Start() + { + return UnitTestApplication.Start(TestServices.StyledWindow.With( + renderInterface: new MockPlatformRenderInterface())); + } } } From a28483252b9039d5e9c921eb054c3aa034963d33 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 30 Mar 2023 22:29:48 +0200 Subject: [PATCH 24/26] Make MaskedTextBox tests pass again. They need a registered `IPlatformRenderInterface` and were only passing because `GlyphRunTests` was leaking one. --- tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs index 19009416ef..a51abdb9af 100644 --- a/tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs @@ -109,7 +109,7 @@ namespace Avalonia.Controls.UnitTests [Fact] public void CaretIndex_Can_Moved_To_Position_After_The_End_Of_Text_With_Arrow_Key() { - using (Start(TestServices.StyledWindow)) + using (Start()) { var target = new MaskedTextBox { @@ -184,7 +184,7 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Control_Backspace_Should_Remove_The_Word_Before_The_Caret_If_There_Is_No_Selection() { - using (Start(TestServices.StyledWindow)) + using (Start()) { MaskedTextBox textBox = new MaskedTextBox { @@ -226,7 +226,7 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Control_Delete_Should_Remove_The_Word_After_The_Caret_If_There_Is_No_Selection() { - using (Start(TestServices.StyledWindow)) + using (Start()) { var textBox = new MaskedTextBox { @@ -812,7 +812,7 @@ namespace Avalonia.Controls.UnitTests bool fromClipboard, string expected) { - using (Start(TestServices.StyledWindow)) + using (Start()) { var target = new MaskedTextBox { @@ -898,6 +898,7 @@ namespace Avalonia.Controls.UnitTests standardCursorFactory: Mock.Of()); private static TestServices Services => TestServices.MockThreadingInterface.With( + renderInterface: new MockPlatformRenderInterface(), standardCursorFactory: Mock.Of(), textShaperImpl: new MockTextShaperImpl(), fontManagerImpl: new MockFontManagerImpl()); From 44639fbd655d84bd8627ea88c30de5ad6219c31f Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 31 Mar 2023 08:14:22 +0200 Subject: [PATCH 25/26] Added failing layout test. --- .../Layout/LayoutManagerTests.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/Avalonia.Base.UnitTests/Layout/LayoutManagerTests.cs b/tests/Avalonia.Base.UnitTests/Layout/LayoutManagerTests.cs index 21723ba182..98d2807db5 100644 --- a/tests/Avalonia.Base.UnitTests/Layout/LayoutManagerTests.cs +++ b/tests/Avalonia.Base.UnitTests/Layout/LayoutManagerTests.cs @@ -41,6 +41,24 @@ namespace Avalonia.Base.UnitTests.Layout Assert.False(control.Arranged); } + [Fact] + public void Doesnt_Measure_And_Arrange_InvalidateMeasured_Control_When_Ancestor_Is_Not_Visible() + { + var control = new LayoutTestControl(); + var parent = new Decorator { Child = control }; + var root = new LayoutTestRoot { Child = parent }; + + root.LayoutManager.ExecuteInitialLayoutPass(); + control.Measured = control.Arranged = false; + + parent.IsVisible = false; + control.InvalidateMeasure(); + root.LayoutManager.ExecuteLayoutPass(); + + Assert.False(control.Measured); + Assert.False(control.Arranged); + } + [Fact] public void Arranges_InvalidateArranged_Control() { From 6dd9106fe55ed51aa9ecf34768b3138c067d04e6 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 31 Mar 2023 08:15:28 +0200 Subject: [PATCH 26/26] Don't layout invisible controls. And fix unit tests that were relying on this behavior. --- src/Avalonia.Base/Layout/LayoutManager.cs | 24 ++++++++++++++----- .../MenuItemTests.cs | 2 ++ .../Primitives/PopupTests.cs | 1 + .../Xaml/ControlThemeTests.cs | 2 ++ 4 files changed, 23 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Base/Layout/LayoutManager.cs b/src/Avalonia.Base/Layout/LayoutManager.cs index c4742bcba4..e16be3fa85 100644 --- a/src/Avalonia.Base/Layout/LayoutManager.cs +++ b/src/Avalonia.Base/Layout/LayoutManager.cs @@ -269,21 +269,25 @@ namespace Avalonia.Layout } } - private void Measure(Layoutable control) + private bool Measure(Layoutable control) { + if (!control.IsVisible || !control.IsAttachedToVisualTree) + return false; + // Controls closest to the visual root need to be arranged first. We don't try to store // ordered invalidation lists, instead we traverse the tree upwards, measuring the // controls closest to the root first. This has been shown by benchmarks to be the // fastest and most memory-efficient algorithm. if (control.VisualParent is Layoutable parent) { - Measure(parent); + if (!Measure(parent)) + return false; } // If the control being measured has IsMeasureValid == true here then its measure was // handed by an ancestor and can be ignored. The measure may have also caused the // control to be removed. - if (!control.IsMeasureValid && control.IsAttachedToVisualTree) + if (!control.IsMeasureValid) { if (control is ILayoutRoot root) { @@ -294,16 +298,22 @@ namespace Avalonia.Layout control.Measure(control.PreviousMeasure.Value); } } + + return true; } - private void Arrange(Layoutable control) + private bool Arrange(Layoutable control) { + if (!control.IsVisible || !control.IsAttachedToVisualTree) + return false; + if (control.VisualParent is Layoutable parent) { - Arrange(parent); + if (!Arrange(parent)) + return false; } - if (!control.IsArrangeValid && control.IsAttachedToVisualTree) + if (control.IsMeasureValid && !control.IsArrangeValid) { if (control is IEmbeddedLayoutRoot embeddedRoot) control.Arrange(new Rect(embeddedRoot.AllocatedSize)); @@ -316,6 +326,8 @@ namespace Avalonia.Layout control.Arrange(control.PreviousArrange.Value); } } + + return true; } private void QueueLayoutPass() diff --git a/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs b/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs index 6fda5209ad..fc189fb3c3 100644 --- a/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs +++ b/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs @@ -334,6 +334,7 @@ namespace Avalonia.Controls.UnitTests }; var window = new Window { Content = menu }; + window.Show(); window.LayoutManager.ExecuteInitialLayoutPass(); topLevelMenu.IsSubMenuOpen = true; @@ -371,6 +372,7 @@ namespace Avalonia.Controls.UnitTests }; var window = new Window { Content = menu }; + window.Show(); window.LayoutManager.ExecuteInitialLayoutPass(); var panel = Assert.IsType(menu.Presenter.Panel); diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs index 1e22aa9129..34311949ef 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs @@ -1116,6 +1116,7 @@ namespace Avalonia.Controls.UnitTests.Primitives private static Window PreparedWindow(object content = null) { var w = new Window { Content = content }; + w.Show(); w.ApplyStyling(); w.ApplyTemplate(); return w; diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlThemeTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlThemeTests.cs index 9eb48311df..1e4c89b33a 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlThemeTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlThemeTests.cs @@ -27,6 +27,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); var button = Assert.IsType(window.Content); + window.Show(); window.LayoutManager.ExecuteInitialLayoutPass(); Assert.NotNull(button.Template); @@ -63,6 +64,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); var button = Assert.IsType(window.Content); + window.Show(); window.LayoutManager.ExecuteInitialLayoutPass(); Assert.NotNull(button.Template);