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.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/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 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 - 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 17726e6353..126c488d59 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs @@ -9,6 +9,7 @@ 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; @@ -286,6 +287,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.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/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/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/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/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/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/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.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 6f0f6702e5..b725326855 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 2b5a2f5022..2047f4d2d0 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) @@ -840,20 +861,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 { @@ -925,7 +951,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); @@ -934,7 +962,7 @@ namespace Avalonia.PropertyStore break; } - e.EndReevaluation(); + e.UnsubscribeIfNecessary(); } } finally 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!); } } } 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/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 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/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; 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/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/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/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 @@ 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 @@ + + diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs b/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs index b3e106a7bf..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) { @@ -724,7 +721,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) diff --git a/src/Avalonia.Controls/Flyouts/MenuFlyout.cs b/src/Avalonia.Controls/Flyouts/MenuFlyout.cs index ba5489ac25..8e1bc214a7 100644 --- a/src/Avalonia.Controls/Flyouts/MenuFlyout.cs +++ b/src/Avalonia.Controls/Flyouts/MenuFlyout.cs @@ -90,7 +90,7 @@ namespace Avalonia.Controls { return new MenuFlyoutPresenter { - [!ItemsControl.ItemsProperty] = this[!ItemsProperty], + [!ItemsControl.ItemsSourceProperty] = this[!ItemsProperty], [!ItemsControl.ItemTemplateProperty] = this[!ItemTemplateProperty], [!ItemsControl.ItemContainerThemeProperty] = this[!ItemContainerThemeProperty], }; 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 06f0d54e4c..54ffba462f 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); 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/Primitives/HeaderedItemsControl.cs b/src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs index 273271d2ce..d71439ece6 100644 --- a/src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs @@ -124,7 +124,7 @@ namespace Avalonia.Controls.Primitives treeTemplate.Match(item) && treeTemplate.ItemsSelector(item) is { } itemsBinding) { - _itemsBinding = BindingOperations.Apply(this, ItemsProperty, itemsBinding, null); + _itemsBinding = BindingOperations.Apply(this, ItemsSourceProperty, itemsBinding, null); } } 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/Avalonia.Controls/SelectableTextBlock.cs b/src/Avalonia.Controls/SelectableTextBlock.cs index 60b0a77f30..5d14611a62 100644 --- a/src/Avalonia.Controls/SelectableTextBlock.cs +++ b/src/Avalonia.Controls/SelectableTextBlock.cs @@ -19,10 +19,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( @@ -231,7 +231,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) @@ -317,7 +317,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); @@ -400,7 +400,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 ""; @@ -418,7 +421,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; } 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); } /// diff --git a/src/Avalonia.Controls/SystemDialog.cs b/src/Avalonia.Controls/SystemDialog.cs index ee41b94b5a..d2b893df37 100644 --- a/src/Avalonia.Controls/SystemDialog.cs +++ b/src/Avalonia.Controls/SystemDialog.cs @@ -18,7 +18,7 @@ namespace Avalonia.Controls /// Gets or sets a collection of filters which determine the types of files displayed in an /// or an . /// - public List? Filters { get; set; } = new List(); + public List Filters { get; set; } = new List(); /// /// Gets or sets initial file name that is displayed when the dialog is opened. diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 40202255ab..2cd7a6f5ef 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.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..7b58671996 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(), @@ -129,7 +130,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, diff --git a/src/Avalonia.Fonts.Inter/AppBuilderExtension.cs b/src/Avalonia.Fonts.Inter/AppBuilderExtension.cs index 842629c923..e63032aa56 100644 --- a/src/Avalonia.Fonts.Inter/AppBuilderExtension.cs +++ b/src/Avalonia.Fonts.Inter/AppBuilderExtension.cs @@ -1,4 +1,6 @@ -namespace Avalonia.Fonts.Inter +using Avalonia.Fonts.Inter; + +namespace Avalonia { public static class AppBuilderExtension { diff --git a/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj b/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj index da6ae9e856..d8162c0486 100644 --- a/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj +++ b/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj @@ -13,7 +13,7 @@ - + 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) { 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/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); } } 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; } } diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Coercion.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Coercion.cs index 2154bb63d0..fe4262331f 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,118 @@ 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); + } + + [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 = AvaloniaProperty.Register( - "Qux", + "Foo", defaultValue: 11, coerce: CoerceFoo); @@ -215,13 +325,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 +378,51 @@ namespace Avalonia.Base.UnitTests return -value; } } + + 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 = + 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(); 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() { 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/ControlBindingTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlBindingTests.cs index affa292a7d..6018e2438f 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlBindingTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlBindingTests.cs @@ -73,14 +73,14 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml var xaml = @" - + - + 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); 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;