diff --git a/.ncrunch/Avalonia.Native.v3.ncrunchproject b/.ncrunch/Avalonia.Native.v3.ncrunchproject new file mode 100644 index 0000000000..319cd523ce --- /dev/null +++ b/.ncrunch/Avalonia.Native.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index ec3bf799b4..7c2ae441d0 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -1,33 +1,41 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + Background="{DynamicResource ThemeBackgroundBrush}" + Foreground="{DynamicResource ThemeForegroundBrush}" + FontSize="{DynamicResource FontSizeNormal}"> + + + Light + Dark + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/MainView.xaml.cs b/samples/ControlCatalog/MainView.xaml.cs index 0be5d25a09..a498b17bdd 100644 --- a/samples/ControlCatalog/MainView.xaml.cs +++ b/samples/ControlCatalog/MainView.xaml.cs @@ -2,6 +2,7 @@ using System.Collections; using Avalonia; using Avalonia.Controls; using Avalonia.Markup.Xaml; +using Avalonia.Markup.Xaml.Styling; using Avalonia.Platform; using ControlCatalog.Pages; @@ -27,6 +28,22 @@ namespace ControlCatalog }); } + var light = AvaloniaXamlLoader.Parse(@""); + var dark = AvaloniaXamlLoader.Parse(@""); + var themes = this.Find("Themes"); + themes.SelectionChanged += (sender, e) => + { + switch (themes.SelectedIndex) + { + case 0: + Styles[0] = light; + break; + case 1: + Styles[0] = dark; + break; + } + }; + Styles.Add(light); } private void InitializeComponent() diff --git a/samples/ControlCatalog/Pages/TabControlPage.xaml b/samples/ControlCatalog/Pages/TabControlPage.xaml new file mode 100644 index 0000000000..430ac28347 --- /dev/null +++ b/samples/ControlCatalog/Pages/TabControlPage.xaml @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + This is the first page in the TabControl. + + + + + + This is the second page in the TabControl. + + + + + You should not see this. + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tab Placement: + + Left + Bottom + Right + Top + + + + + diff --git a/samples/ControlCatalog/Pages/TabControlPage.xaml.cs b/samples/ControlCatalog/Pages/TabControlPage.xaml.cs new file mode 100644 index 0000000000..808d90a49c --- /dev/null +++ b/samples/ControlCatalog/Pages/TabControlPage.xaml.cs @@ -0,0 +1,80 @@ +using System; + +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Avalonia.Media.Imaging; +using Avalonia.Platform; + +using ReactiveUI; + +namespace ControlCatalog.Pages +{ + using System.Collections.Generic; + + public class TabControlPage : UserControl + { + public TabControlPage() + { + InitializeComponent(); + + DataContext = new PageViewModel + { + Tabs = new[] + { + new TabItemViewModel + { + Header = "Arch", + Text = "This is the first templated tab page.", + Image = LoadBitmap("resm:ControlCatalog.Assets.delicate-arch-896885_640.jpg?assembly=ControlCatalog"), + }, + new TabItemViewModel + { + Header = "Leaf", + Text = "This is the second templated tab page.", + Image = LoadBitmap("resm:ControlCatalog.Assets.maple-leaf-888807_640.jpg?assembly=ControlCatalog"), + }, + new TabItemViewModel + { + Header = "Disabled", + Text = "You should not see this.", + IsEnabled = false, + }, + }, + TabPlacement = Dock.Top, + }; + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + private IBitmap LoadBitmap(string uri) + { + var assets = AvaloniaLocator.Current.GetService(); + return new Bitmap(assets.Open(new Uri(uri))); + } + + private class PageViewModel : ReactiveObject + { + private Dock _tabPlacement; + + public TabItemViewModel[] Tabs { get; set; } + + public Dock TabPlacement + { + get { return _tabPlacement; } + set { this.RaiseAndSetIfChanged(ref _tabPlacement, value); } + } + } + + private class TabItemViewModel + { + public string Header { get; set; } + public string Text { get; set; } + public IBitmap Image { get; set; } + public bool IsEnabled { get; set; } = true; + } + } +} diff --git a/samples/ControlCatalog/Pages/ViewboxPage.xaml b/samples/ControlCatalog/Pages/ViewboxPage.xaml new file mode 100644 index 0000000000..89a82c4791 --- /dev/null +++ b/samples/ControlCatalog/Pages/ViewboxPage.xaml @@ -0,0 +1,65 @@ + + + + F1 M 16.6309,18.6563C 17.1309, + 8.15625 29.8809,14.1563 29.8809, + 14.1563C 30.8809,11.1563 34.1308, + 11.4063 34.1308,11.4063C 33.5,12 + 34.6309,13.1563 34.6309,13.1563C + 32.1309,13.1562 31.1309,14.9062 + 31.1309,14.9062C 41.1309,23.9062 + 32.6309,27.9063 32.6309,27.9062C + 24.6309,24.9063 21.1309,22.1562 + 16.6309,18.6563 Z M 16.6309,19.9063C + 21.6309,24.1563 25.1309,26.1562 + 31.6309,28.6562C 31.6309,28.6562 + 26.3809,39.1562 18.3809,36.1563C + 18.3809,36.1563 18,38 16.3809,36.9063C + 15,36 16.3809,34.9063 16.3809,34.9063C + 16.3809,34.9063 10.1309,30.9062 16.6309,19.9063 Z + + + + + + Viewbox + A control used to scale single child. + + + None + Fill + Uniform + UniformToFill + + + Hello World! + + + Hello World! + + + Hello World! + + + Hello World! + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/ViewboxPage.xaml.cs b/samples/ControlCatalog/Pages/ViewboxPage.xaml.cs new file mode 100644 index 0000000000..1b5f4bc7f4 --- /dev/null +++ b/samples/ControlCatalog/Pages/ViewboxPage.xaml.cs @@ -0,0 +1,18 @@ +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace ControlCatalog.Pages +{ + public class ViewboxPage : UserControl + { + public ViewboxPage() + { + this.InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/samples/ControlCatalog/SideBar.xaml b/samples/ControlCatalog/SideBar.xaml index cc3c31d13a..ae9ab7f6a6 100644 --- a/samples/ControlCatalog/SideBar.xaml +++ b/samples/ControlCatalog/SideBar.xaml @@ -1,52 +1,66 @@ - + - - - - - + + + + + diff --git a/samples/RenderDemo/MainWindow.xaml b/samples/RenderDemo/MainWindow.xaml index fc5b1ce94d..41164c7780 100644 --- a/samples/RenderDemo/MainWindow.xaml +++ b/samples/RenderDemo/MainWindow.xaml @@ -24,9 +24,6 @@ - - - @@ -38,4 +35,4 @@ - \ No newline at end of file + diff --git a/samples/RenderDemo/SideBar.xaml b/samples/RenderDemo/SideBar.xaml index 26da2cc556..624c1a7b28 100644 --- a/samples/RenderDemo/SideBar.xaml +++ b/samples/RenderDemo/SideBar.xaml @@ -1,53 +1,66 @@ - + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" > + - - - - - + + + + + diff --git a/src/Avalonia.Base/Data/Core/ExpressionNode.cs b/src/Avalonia.Base/Data/Core/ExpressionNode.cs index 9ee4787e47..f2f3ed9bfc 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionNode.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionNode.cs @@ -88,18 +88,21 @@ namespace Avalonia.Data.Core _subscriber(value); } - protected void ValueChanged(object value) + protected void ValueChanged(object value) => ValueChanged(value, true); + + private void ValueChanged(object value, bool notify) { var notification = value as BindingNotification; if (notification == null) { LastValue = new WeakReference(value); + if (Next != null) { - Next.Target = new WeakReference(value); + Next.Target = LastValue; } - else + else if (notify) { _subscriber(value); } @@ -110,7 +113,7 @@ namespace Avalonia.Data.Core if (Next != null) { - Next.Target = new WeakReference(notification.Value); + Next.Target = LastValue; } if (Next == null || notification.Error != null) @@ -136,6 +139,7 @@ namespace Avalonia.Data.Core } else { + ValueChanged(AvaloniaProperty.UnsetValue, notify:false); _listening = false; } } diff --git a/src/Avalonia.Controls/DropDown.cs b/src/Avalonia.Controls/DropDown.cs index 8596d06d2c..93b33e0589 100644 --- a/src/Avalonia.Controls/DropDown.cs +++ b/src/Avalonia.Controls/DropDown.cs @@ -2,9 +2,12 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Linq; using Avalonia.Controls.Generators; +using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Shapes; +using Avalonia.Controls.Templates; using Avalonia.Input; using Avalonia.LogicalTree; using Avalonia.Media; @@ -12,12 +15,17 @@ using Avalonia.VisualTree; namespace Avalonia.Controls { - /// /// A drop-down list control. /// public class DropDown : SelectingItemsControl { + /// + /// The default value for the property. + /// + private static readonly FuncTemplate DefaultPanel = + new FuncTemplate(() => new VirtualizingStackPanel()); + /// /// Defines the property. /// @@ -39,6 +47,12 @@ namespace Avalonia.Controls public static readonly DirectProperty SelectionBoxItemProperty = AvaloniaProperty.RegisterDirect(nameof(SelectionBoxItem), o => o.SelectionBoxItem); + /// + /// Defines the property. + /// + public static readonly StyledProperty VirtualizationModeProperty = + ItemsPresenter.VirtualizationModeProperty.AddOwner(); + private bool _isDropDownOpen; private Popup _popup; private object _selectionBoxItem; @@ -48,6 +62,7 @@ namespace Avalonia.Controls /// static DropDown() { + ItemsPanelProperty.OverrideDefaultValue(DefaultPanel); FocusableProperty.OverrideDefaultValue(true); SelectedItemProperty.Changed.AddClassHandler(x => x.SelectedItemChanged); KeyDownEvent.AddClassHandler(x => x.OnKeyDown, Interactivity.RoutingStrategies.Tunnel); @@ -80,6 +95,15 @@ namespace Avalonia.Controls set { SetAndRaise(SelectionBoxItemProperty, ref _selectionBoxItem, value); } } + /// + /// Gets or sets the virtualization mode for the items. + /// + public ItemVirtualizationMode VirtualizationMode + { + get { return GetValue(VirtualizationModeProperty); } + set { SetValue(VirtualizationModeProperty, value); } + } + /// protected override IItemContainerGenerator CreateItemContainerGenerator() { @@ -138,6 +162,16 @@ namespace Avalonia.Controls e.Handled = true; } } + else if (IsDropDownOpen && SelectedIndex < 0 && ItemCount > 0 && + (e.Key == Key.Up || e.Key == Key.Down)) + { + var firstChild = Presenter?.Panel?.Children.FirstOrDefault(c => CanFocus(c)); + if (firstChild != null) + { + FocusManager.Instance?.Focus(firstChild, NavigationMethod.Directional); + e.Handled = true; + } + } } /// @@ -159,6 +193,7 @@ namespace Avalonia.Controls e.Handled = true; } } + base.OnPointerPressed(e); } @@ -168,28 +203,65 @@ namespace Avalonia.Controls if (_popup != null) { _popup.Opened -= PopupOpened; + _popup.Closed -= PopupClosed; } _popup = e.NameScope.Get("PART_Popup"); _popup.Opened += PopupOpened; + _popup.Closed += PopupClosed; + + base.OnTemplateApplied(e); } - private void PopupOpened(object sender, EventArgs e) + internal void ItemFocused(DropDownItem dropDownItem) { - var selectedIndex = SelectedIndex; + if (IsDropDownOpen && dropDownItem.IsFocused && dropDownItem.IsArrangeValid) + { + dropDownItem.BringIntoView(); + } + } - if (selectedIndex != -1) + private void PopupClosed(object sender, EventArgs e) + { + if (CanFocus(this)) { - var container = ItemContainerGenerator.ContainerFromIndex(selectedIndex); - container?.Focus(); + Focus(); } } + private void PopupOpened(object sender, EventArgs e) + { + TryFocusSelectedItem(); + } + private void SelectedItemChanged(AvaloniaPropertyChangedEventArgs e) { UpdateSelectionBoxItem(e.NewValue); + TryFocusSelectedItem(); + } + + private void TryFocusSelectedItem() + { + var selectedIndex = SelectedIndex; + if (IsDropDownOpen && selectedIndex != -1) + { + var container = ItemContainerGenerator.ContainerFromIndex(selectedIndex); + + if(container == null && SelectedItems.Count > 0) + { + ScrollIntoView(SelectedItems[0]); + container = ItemContainerGenerator.ContainerFromIndex(selectedIndex); + } + + if (container != null && CanFocus(container)) + { + container.Focus(); + } + } } + private bool CanFocus(IControl control) => control.Focusable && control.IsEnabledCore && control.IsVisible; + private void UpdateSelectionBoxItem(object item) { var contentControl = item as IContentControl; @@ -219,7 +291,8 @@ namespace Avalonia.Controls } else { - SelectionBoxItem = item; + var selector = MemberSelector; + SelectionBoxItem = selector != null ? selector.Select(item) : item; } } diff --git a/src/Avalonia.Controls/DropDownItem.cs b/src/Avalonia.Controls/DropDownItem.cs index fb465e93ec..1e22ededf6 100644 --- a/src/Avalonia.Controls/DropDownItem.cs +++ b/src/Avalonia.Controls/DropDownItem.cs @@ -2,43 +2,19 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Reactive.Linq; namespace Avalonia.Controls { /// /// A selectable item in a . /// - public class DropDownItem : ContentControl, ISelectable + public class DropDownItem : ListBoxItem { - /// - /// Defines the property. - /// - public static readonly StyledProperty IsSelectedProperty = - AvaloniaProperty.Register(nameof(IsSelected)); - - /// - /// Initializes static members of the class. - /// - static DropDownItem() - { - FocusableProperty.OverrideDefaultValue(true); - } - public DropDownItem() { - this.GetObservable(DropDownItem.IsFocusedProperty).Subscribe(focused => - { - PseudoClasses.Set(":selected", focused); - }); - } - - /// - /// Gets or sets the selection state of the item. - /// - public bool IsSelected - { - get { return GetValue(IsSelectedProperty); } - set { SetValue(IsSelectedProperty, value); } + this.GetObservable(DropDownItem.IsFocusedProperty).Where(focused => focused) + .Subscribe(_ => (Parent as DropDown)?.ItemFocused(this)); } } } diff --git a/src/Avalonia.Controls/Generators/TabItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/TabItemContainerGenerator.cs new file mode 100644 index 0000000000..a6a64e570b --- /dev/null +++ b/src/Avalonia.Controls/Generators/TabItemContainerGenerator.cs @@ -0,0 +1,59 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using Avalonia.Controls.Primitives; + +namespace Avalonia.Controls.Generators +{ + public class TabItemContainerGenerator : ItemContainerGenerator + { + public TabItemContainerGenerator(TabControl owner) + : base(owner, ContentControl.ContentProperty, ContentControl.ContentTemplateProperty) + { + Owner = owner; + } + + public new TabControl Owner { get; } + + protected override IControl CreateContainer(object item) + { + var tabItem = (TabItem)base.CreateContainer(item); + + tabItem.ParentTabControl = Owner; + + tabItem[~TabControl.TabStripPlacementProperty] = Owner[~TabControl.TabStripPlacementProperty]; + + if (tabItem.HeaderTemplate == null) + { + tabItem[~HeaderedContentControl.HeaderTemplateProperty] = Owner[~ItemsControl.ItemTemplateProperty]; + } + + if (tabItem.Header == null) + { + if (item is IHeadered headered) + { + tabItem.Header = headered.Header; + } + else + { + if (!(tabItem.DataContext is IControl)) + { + tabItem.Header = tabItem.DataContext; + } + } + } + + if (!(tabItem.Content is IControl)) + { + tabItem[~ContentControl.ContentTemplateProperty] = Owner[~TabControl.ContentTemplateProperty]; + } + + if (tabItem.Content == null) + { + tabItem[~ContentControl.ContentProperty] = tabItem[~StyledElement.DataContextProperty]; + } + + return tabItem; + } + } +} diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index 9d4cbb9260..d74078c712 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -249,8 +249,6 @@ namespace Avalonia.Controls if (containerControl != null) { ((ISetLogicalParent)containerControl).SetParent(this); - containerControl.SetValue(TemplatedParentProperty, null); - containerControl.UpdateChild(); if (containerControl.Child != null) diff --git a/src/Avalonia.Controls/LayoutTransformControl.cs b/src/Avalonia.Controls/LayoutTransformControl.cs index 87e3853643..950d4f34da 100644 --- a/src/Avalonia.Controls/LayoutTransformControl.cs +++ b/src/Avalonia.Controls/LayoutTransformControl.cs @@ -5,28 +5,30 @@ // http://silverlight.codeplex.com/SourceControl/changeset/view/74775#Release/Silverlight4/Source/Controls.Layout.Toolkit/LayoutTransformer/LayoutTransformer.cs // -using Avalonia.Controls.Primitives; -using Avalonia.Media; -using Avalonia.VisualTree; using System; using System.Diagnostics.CodeAnalysis; -using System.Linq; using System.Reactive.Linq; +using Avalonia.Media; namespace Avalonia.Controls { /// /// Control that implements support for transformations as if applied by LayoutTransform. /// - public class LayoutTransformControl : ContentControl + public class LayoutTransformControl : Decorator { public static readonly AvaloniaProperty LayoutTransformProperty = AvaloniaProperty.Register(nameof(LayoutTransform)); static LayoutTransformControl() { + ClipToBoundsProperty.OverrideDefaultValue(true); + LayoutTransformProperty.Changed .AddClassHandler(x => x.OnLayoutTransformChanged); + + ChildProperty.Changed + .AddClassHandler(x => x.OnChildChanged); } /// @@ -38,8 +40,7 @@ namespace Avalonia.Controls set { SetValue(LayoutTransformProperty, value); } } - public Control TransformRoot => _transformRoot ?? - (_transformRoot = this.GetVisualChildren().OfType().FirstOrDefault()); + public IControl TransformRoot => Child; /// /// Provides the behavior for the "Arrange" pass of layout. @@ -132,16 +133,8 @@ namespace Avalonia.Controls return transformedDesiredSize; } - /// - /// Builds the visual tree for the LayoutTransformerControl when a new - /// template is applied. - /// - protected override void OnTemplateApplied(TemplateAppliedEventArgs e) + private void OnChildChanged(AvaloniaPropertyChangedEventArgs e) { - base.OnTemplateApplied(e); - - _matrixTransform = new MatrixTransform(); - if (null != TransformRoot) { TransformRoot.RenderTransform = _matrixTransform; @@ -169,14 +162,14 @@ namespace Avalonia.Controls /// /// RenderTransform/MatrixTransform applied to TransformRoot. /// - private MatrixTransform _matrixTransform; + private MatrixTransform _matrixTransform = new MatrixTransform(); /// /// Transformation matrix corresponding to _matrixTransform. /// private Matrix _transformation; private IDisposable _transformChangedEvent = null; - private Control _transformRoot; + /// /// Returns true if Size a is smaller than Size b in either dimension. /// @@ -215,7 +208,8 @@ namespace Avalonia.Controls /// private void ApplyLayoutTransform() { - if (LayoutTransform == null) return; + if (LayoutTransform == null) + return; // Get the transform matrix and apply it _transformation = RoundMatrix(LayoutTransform.Value, DecimalsAfterRound); @@ -376,11 +370,8 @@ namespace Avalonia.Controls { var newTransform = e.NewValue as Transform; - if (_transformChangedEvent != null) - { - _transformChangedEvent.Dispose(); - _transformChangedEvent = null; - } + _transformChangedEvent?.Dispose(); + _transformChangedEvent = null; if (newTransform != null) { @@ -392,4 +383,4 @@ namespace Avalonia.Controls ApplyLayoutTransform(); } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/Primitives/HeaderedContentControl.cs b/src/Avalonia.Controls/Primitives/HeaderedContentControl.cs index d67ebfd489..7a46e0f776 100644 --- a/src/Avalonia.Controls/Primitives/HeaderedContentControl.cs +++ b/src/Avalonia.Controls/Primitives/HeaderedContentControl.cs @@ -1,6 +1,8 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using Avalonia.Controls.Templates; + namespace Avalonia.Controls.Primitives { /// @@ -12,7 +14,13 @@ namespace Avalonia.Controls.Primitives /// Defines the property. /// public static readonly StyledProperty HeaderProperty = - AvaloniaProperty.Register(nameof(Header)); + AvaloniaProperty.Register(nameof(Header)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty HeaderTemplateProperty = + AvaloniaProperty.Register(nameof(HeaderTemplate)); /// /// Gets or sets the header content. @@ -21,6 +29,15 @@ namespace Avalonia.Controls.Primitives { get { return GetValue(HeaderProperty); } set { SetValue(HeaderProperty, value); } + } + + /// + /// Gets or sets the data template used to display the header content of the control. + /// + public IDataTemplate HeaderTemplate + { + get { return GetValue(HeaderTemplateProperty); } + set { SetValue(HeaderTemplateProperty, value); } } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index ccbdc71b1d..c40ddc37ad 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -431,9 +431,12 @@ namespace Avalonia.Controls.Primitives { if (i.ContainerControl != null && i.Item != null) { - MarkContainerSelected( - i.ContainerControl, - SelectedItems.Contains(i.Item)); + var ms = MemberSelector; + bool selected = ms == null ? + SelectedItems.Contains(i.Item) : + SelectedItems.OfType().Any(v => Equals(ms.Select(v), i.Item)); + + MarkContainerSelected(i.ContainerControl, selected); } } } diff --git a/src/Avalonia.Controls/Primitives/TemplatedControl.cs b/src/Avalonia.Controls/Primitives/TemplatedControl.cs index 296134ca48..ba4c5027d0 100644 --- a/src/Avalonia.Controls/Primitives/TemplatedControl.cs +++ b/src/Avalonia.Controls/Primitives/TemplatedControl.cs @@ -260,7 +260,7 @@ namespace Avalonia.Controls.Primitives var child = template.Build(this); var nameScope = new NameScope(); NameScope.SetNameScope((Control)child, nameScope); - child.SetValue(TemplatedParentProperty, this); + ApplyTemplatedParent(child); RegisterNames(child, nameScope); ((ISetLogicalParent)child).SetParent(this); VisualChildren.Add(child); @@ -326,6 +326,23 @@ namespace Avalonia.Controls.Primitives InvalidateMeasure(); } + /// + /// Sets the TemplatedParent property for the created template children. + /// + /// The control. + private void ApplyTemplatedParent(IControl control) + { + control.SetValue(TemplatedParentProperty, this); + + foreach (var child in control.LogicalChildren) + { + if (child is IControl c) + { + ApplyTemplatedParent(c); + } + } + } + /// /// Registers each control with its name scope. /// diff --git a/src/Avalonia.Controls/TabControl.cs b/src/Avalonia.Controls/TabControl.cs index 3aae256858..8eaf166f57 100644 --- a/src/Avalonia.Controls/TabControl.cs +++ b/src/Avalonia.Controls/TabControl.cs @@ -1,10 +1,12 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. -using Avalonia.Animation; using Avalonia.Controls.Generators; +using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; +using Avalonia.Input; +using Avalonia.Layout; namespace Avalonia.Controls { @@ -14,28 +16,46 @@ namespace Avalonia.Controls public class TabControl : SelectingItemsControl { /// - /// Defines the property. + /// Defines the property. /// - public static readonly StyledProperty PageTransitionProperty = - Avalonia.Controls.Carousel.PageTransitionProperty.AddOwner(); + public static readonly StyledProperty TabStripPlacementProperty = + AvaloniaProperty.Register(nameof(TabStripPlacement), defaultValue: Dock.Top); /// - /// Defines an that selects the content of a . + /// Defines the property. /// - public static readonly IMemberSelector ContentSelector = - new FuncMemberSelector(SelectContent); + public static readonly StyledProperty HorizontalContentAlignmentProperty = + ContentControl.HorizontalContentAlignmentProperty.AddOwner(); /// - /// Defines an that selects the header of a . + /// Defines the property. /// - public static readonly IMemberSelector HeaderSelector = - new FuncMemberSelector(SelectHeader); + public static readonly StyledProperty VerticalContentAlignmentProperty = + ContentControl.VerticalContentAlignmentProperty.AddOwner(); /// - /// Defines the property. + /// Defines the property. /// - public static readonly StyledProperty TabStripPlacementProperty = - AvaloniaProperty.Register(nameof(TabStripPlacement), defaultValue: Dock.Top); + public static readonly StyledProperty ContentTemplateProperty = + ContentControl.ContentTemplateProperty.AddOwner(); + + /// + /// The selected content property + /// + public static readonly StyledProperty SelectedContentProperty = + AvaloniaProperty.Register(nameof(SelectedContent)); + + /// + /// The selected content template property + /// + public static readonly StyledProperty SelectedContentTemplateProperty = + AvaloniaProperty.Register(nameof(SelectedContentTemplate)); + + /// + /// The default value for the property. + /// + private static readonly FuncTemplate DefaultPanel = + new FuncTemplate(() => new WrapPanel()); /// /// Initializes static members of the class. @@ -43,107 +63,107 @@ namespace Avalonia.Controls static TabControl() { SelectionModeProperty.OverrideDefaultValue(SelectionMode.AlwaysSelected); - FocusableProperty.OverrideDefaultValue(false); + ItemsPanelProperty.OverrideDefaultValue(DefaultPanel); AffectsMeasure(TabStripPlacementProperty); } /// - /// Gets the pages portion of the 's template. + /// Gets or sets the horizontal alignment of the content within the control. /// - public IControl Pages + public HorizontalAlignment HorizontalContentAlignment { - get; - private set; + get { return GetValue(HorizontalContentAlignmentProperty); } + set { SetValue(HorizontalContentAlignmentProperty, value); } } /// - /// Gets the tab strip portion of the 's template. + /// Gets or sets the vertical alignment of the content within the control. /// - public IControl TabStrip + public VerticalAlignment VerticalContentAlignment { - get; - private set; + get { return GetValue(VerticalContentAlignmentProperty); } + set { SetValue(VerticalContentAlignmentProperty, value); } } /// - /// Gets or sets the transition to use when switching tabs. + /// Gets or sets the tabstrip placement of the TabControl. /// - public IPageTransition PageTransition + public Dock TabStripPlacement { - get { return GetValue(PageTransitionProperty); } - set { SetValue(PageTransitionProperty, value); } + get { return GetValue(TabStripPlacementProperty); } + set { SetValue(TabStripPlacementProperty, value); } } /// - /// Gets or sets the tabstrip placement of the tabcontrol. + /// Gets or sets the default data template used to display the content of the selected tab. /// - public Dock TabStripPlacement + public IDataTemplate ContentTemplate { - get { return GetValue(TabStripPlacementProperty); } - set { SetValue(TabStripPlacementProperty, value); } + get { return GetValue(ContentTemplateProperty); } + set { SetValue(ContentTemplateProperty, value); } } + /// + /// Gets or sets the content of the selected tab. + /// + /// + /// The content of the selected tab. + /// + public object SelectedContent + { + get { return GetValue(SelectedContentProperty); } + internal set { SetValue(SelectedContentProperty, value); } + } + + /// + /// Gets or sets the content template for the selected tab. + /// + /// + /// The content template of the selected tab. + /// + public IDataTemplate SelectedContentTemplate + { + get { return GetValue(SelectedContentTemplateProperty); } + internal set { SetValue(SelectedContentTemplateProperty, value); } + } + + internal ItemsPresenter ItemsPresenterPart { get; private set; } + + internal ContentPresenter ContentPart { get; private set; } + protected override IItemContainerGenerator CreateItemContainerGenerator() { - // TabControl doesn't actually create items - instead its TabStrip and Carousel - // children create the items. However we want it to be a SelectingItemsControl - // so that it has the Items/SelectedItem etc properties. In this case, we can - // return a null ItemContainerGenerator to disable the creation of item containers. - return null; + return new TabItemContainerGenerator(this); } protected override void OnTemplateApplied(TemplateAppliedEventArgs e) { base.OnTemplateApplied(e); - TabStrip = e.NameScope.Find("PART_TabStrip"); - Pages = e.NameScope.Find("PART_Content"); + ItemsPresenterPart = e.NameScope.Get("PART_ItemsPresenter"); + + ContentPart = e.NameScope.Get("PART_Content"); } - /// - /// Selects the content of a tab item. - /// - /// The tab item. - /// The content. - private static object SelectContent(object o) + /// + protected override void OnGotFocus(GotFocusEventArgs e) { - var content = o as IContentControl; + base.OnGotFocus(e); - if (content != null) + if (e.NavigationMethod == NavigationMethod.Directional) { - return content.Content; + e.Handled = UpdateSelectionFromEventSource(e.Source); } - else - { - return o; - } } - /// - /// Selects the header of a tab item. - /// - /// The tab item. - /// The content. - private static object SelectHeader(object o) + /// + protected override void OnPointerPressed(PointerPressedEventArgs e) { - var headered = o as IHeadered; - var control = o as IControl; + base.OnPointerPressed(e); - if (headered != null) - { - return headered.Header ?? string.Empty; - } - else if (control != null) - { - // Non-headered control items should result in TabStripItems with empty content. - // If a TabStrip is created with non IHeadered controls as its items, don't try to - // display the control in the TabStripItem: the content portion will also try to - // display this control, resulting in dual-parentage breakage. - return string.Empty; - } - else + if (e.MouseButton == MouseButton.Left) { - return o; + e.Handled = UpdateSelectionFromEventSource(e.Source); } } } diff --git a/src/Avalonia.Controls/TabItem.cs b/src/Avalonia.Controls/TabItem.cs index 4fb68c8b6f..47a2348d59 100644 --- a/src/Avalonia.Controls/TabItem.cs +++ b/src/Avalonia.Controls/TabItem.cs @@ -11,6 +11,12 @@ namespace Avalonia.Controls /// public class TabItem : HeaderedContentControl, ISelectable { + /// + /// Defines the property. + /// + public static readonly StyledProperty TabStripPlacementProperty = + TabControl.TabStripPlacementProperty.AddOwner(); + /// /// Defines the property. /// @@ -24,6 +30,19 @@ namespace Avalonia.Controls { SelectableMixin.Attach(IsSelectedProperty); FocusableProperty.OverrideDefaultValue(typeof(TabItem), true); + IsSelectedProperty.Changed.AddClassHandler(x => x.UpdateSelectedContent); + DataContextProperty.Changed.AddClassHandler(x => x.UpdateHeader); + } + + /// + /// Gets the tab strip placement. + /// + /// + /// The tab strip placement. + /// + public Dock TabStripPlacement + { + get { return GetValue(TabStripPlacementProperty); } } /// @@ -34,5 +53,53 @@ namespace Avalonia.Controls get { return GetValue(IsSelectedProperty); } set { SetValue(IsSelectedProperty, value); } } + + internal TabControl ParentTabControl { get; set; } + + private void UpdateHeader(AvaloniaPropertyChangedEventArgs obj) + { + if (Header == null) + { + if (obj.NewValue is IHeadered headered) + { + if (Header != headered.Header) + { + Header = headered.Header; + } + } + else + { + if (!(obj.NewValue is IControl)) + { + Header = obj.NewValue; + } + } + } + else + { + if (Header == obj.OldValue) + { + Header = obj.NewValue; + } + } + } + + private void UpdateSelectedContent(AvaloniaPropertyChangedEventArgs e) + { + if (!IsSelected) + { + return; + } + + if (ParentTabControl.SelectedContentTemplate != ContentTemplate) + { + ParentTabControl.SelectedContentTemplate = ContentTemplate; + } + + if (ParentTabControl.SelectedContent != Content) + { + ParentTabControl.SelectedContent = Content; + } + } } } diff --git a/src/Avalonia.Controls/Templates/TemplateExtensions.cs b/src/Avalonia.Controls/Templates/TemplateExtensions.cs index 09da737836..18c8bfdeda 100644 --- a/src/Avalonia.Controls/Templates/TemplateExtensions.cs +++ b/src/Avalonia.Controls/Templates/TemplateExtensions.cs @@ -24,12 +24,14 @@ namespace Avalonia.Controls.Templates { foreach (IControl child in control.GetVisualChildren()) { - if (child.TemplatedParent == templatedParent) + var childTemplatedParent = child.TemplatedParent; + + if (childTemplatedParent == templatedParent) { yield return child; } - if (child.TemplatedParent != null) + if (childTemplatedParent != null) { foreach (var descendant in GetTemplateChildren(child, templatedParent)) { diff --git a/src/Avalonia.Controls/Viewbox.cs b/src/Avalonia.Controls/Viewbox.cs new file mode 100644 index 0000000000..db753f4ab4 --- /dev/null +++ b/src/Avalonia.Controls/Viewbox.cs @@ -0,0 +1,124 @@ +using System; +using Avalonia.Media; + +namespace Avalonia.Controls +{ + /// + /// Viewbox is used to scale single child. + /// + /// + public class Viewbox : Decorator + { + /// + /// The stretch property + /// + public static AvaloniaProperty StretchProperty = + AvaloniaProperty.RegisterDirect(nameof(Stretch), + v => v.Stretch, (c, v) => c.Stretch = v, Stretch.Uniform); + + private Stretch _stretch = Stretch.Uniform; + + /// + /// Gets or sets the stretch mode, + /// which determines how child fits into the available space. + /// + /// + /// The stretch. + /// + public Stretch Stretch + { + get => _stretch; + set => SetAndRaise(StretchProperty, ref _stretch, value); + } + + static Viewbox() + { + ClipToBoundsProperty.OverrideDefaultValue(true); + AffectsMeasure(StretchProperty); + } + + protected override Size MeasureOverride(Size availableSize) + { + var child = Child; + + if (child != null) + { + child.Measure(Size.Infinity); + + var childSize = child.DesiredSize; + + var scale = GetScale(availableSize, childSize, Stretch); + + return childSize * scale; + } + + return new Size(); + } + + protected override Size ArrangeOverride(Size finalSize) + { + var child = Child; + + if (child != null) + { + var childSize = child.DesiredSize; + var scale = GetScale(finalSize, childSize, Stretch); + var scaleTransform = child.RenderTransform as ScaleTransform; + + if (scaleTransform == null) + { + child.RenderTransform = scaleTransform = new ScaleTransform(scale.X, scale.Y); + child.RenderTransformOrigin = RelativePoint.TopLeft; + } + + scaleTransform.ScaleX = scale.X; + scaleTransform.ScaleY = scale.Y; + + child.Arrange(new Rect(childSize)); + + return childSize * scale; + } + + return new Size(); + } + + private static Vector GetScale(Size availableSize, Size childSize, Stretch stretch) + { + double scaleX = 1.0; + double scaleY = 1.0; + + bool validWidth = !double.IsPositiveInfinity(availableSize.Width); + bool validHeight = !double.IsPositiveInfinity(availableSize.Height); + + if (stretch != Stretch.None && (validWidth || validHeight)) + { + scaleX = childSize.Width <= 0.0 ? 0.0 : availableSize.Width / childSize.Width; + scaleY = childSize.Height <= 0.0 ? 0.0 : availableSize.Height / childSize.Height; + + if (!validWidth) + { + scaleX = scaleY; + } + else if (!validHeight) + { + scaleY = scaleX; + } + else + { + switch (stretch) + { + case Stretch.Uniform: + scaleX = scaleY = Math.Min(scaleX, scaleY); + break; + + case Stretch.UniformToFill: + scaleX = scaleY = Math.Max(scaleX, scaleY); + break; + } + } + } + + return new Vector(scaleX, scaleY); + } + } +} diff --git a/src/Avalonia.Styling/StyledElement.cs b/src/Avalonia.Styling/StyledElement.cs index 3d0c840040..e52a1961ba 100644 --- a/src/Avalonia.Styling/StyledElement.cs +++ b/src/Avalonia.Styling/StyledElement.cs @@ -49,8 +49,11 @@ namespace Avalonia /// /// Defines the property. /// - public static readonly StyledProperty TemplatedParentProperty = - AvaloniaProperty.Register(nameof(TemplatedParent), inherits: true); + public static readonly DirectProperty TemplatedParentProperty = + AvaloniaProperty.RegisterDirect( + nameof(TemplatedParent), + o => o.TemplatedParent, + (o ,v) => o.TemplatedParent = v); private int _initCount; private string _name; @@ -62,6 +65,7 @@ namespace Avalonia private Styles _styles; private bool _styled; private Subject _styleDetach = new Subject(); + private ITemplatedControl _templatedParent; private bool _dataContextUpdating; /// @@ -269,8 +273,8 @@ namespace Avalonia /// public ITemplatedControl TemplatedParent { - get { return GetValue(TemplatedParentProperty); } - internal set { SetValue(TemplatedParentProperty, value); } + get => _templatedParent; + internal set => SetAndRaise(TemplatedParentProperty, ref _templatedParent, value); } /// diff --git a/src/Avalonia.Themes.Default/Accents/BaseDark.xaml b/src/Avalonia.Themes.Default/Accents/BaseDark.xaml index 95b351b148..e9d710b3f0 100644 --- a/src/Avalonia.Themes.Default/Accents/BaseDark.xaml +++ b/src/Avalonia.Themes.Default/Accents/BaseDark.xaml @@ -52,5 +52,7 @@ 10 12 16 + + 10 diff --git a/src/Avalonia.Themes.Default/Accents/BaseLight.xaml b/src/Avalonia.Themes.Default/Accents/BaseLight.xaml index 4d1c4b1ab0..c40305151e 100644 --- a/src/Avalonia.Themes.Default/Accents/BaseLight.xaml +++ b/src/Avalonia.Themes.Default/Accents/BaseLight.xaml @@ -52,5 +52,7 @@ 10 12 16 + + 10 diff --git a/src/Avalonia.Themes.Default/DefaultTheme.xaml b/src/Avalonia.Themes.Default/DefaultTheme.xaml index 0bd91c8f1e..48e5f231d0 100644 --- a/src/Avalonia.Themes.Default/DefaultTheme.xaml +++ b/src/Avalonia.Themes.Default/DefaultTheme.xaml @@ -12,7 +12,6 @@ - @@ -30,6 +29,7 @@ + diff --git a/src/Avalonia.Themes.Default/DropDown.xaml b/src/Avalonia.Themes.Default/DropDown.xaml index 451f1c2f23..ad2be275d6 100644 --- a/src/Avalonia.Themes.Default/DropDown.xaml +++ b/src/Avalonia.Themes.Default/DropDown.xaml @@ -35,15 +35,21 @@ MinWidth="{Binding Bounds.Width, RelativeSource={RelativeSource TemplatedParent}}" MaxHeight="{TemplateBinding MaxDropDownHeight}" PlacementTarget="{TemplateBinding}" + ObeyScreenEdges="True" StaysOpen="False"> - - - + + + + + @@ -54,4 +60,4 @@ - \ No newline at end of file + diff --git a/src/Avalonia.Themes.Default/DropDownItem.xaml b/src/Avalonia.Themes.Default/DropDownItem.xaml index f52608c0a8..f542a34d71 100644 --- a/src/Avalonia.Themes.Default/DropDownItem.xaml +++ b/src/Avalonia.Themes.Default/DropDownItem.xaml @@ -18,7 +18,7 @@ - + diff --git a/src/Avalonia.Themes.Default/FocusAdorner.xaml b/src/Avalonia.Themes.Default/FocusAdorner.xaml index 573c43dc8d..2d5e369573 100644 --- a/src/Avalonia.Themes.Default/FocusAdorner.xaml +++ b/src/Avalonia.Themes.Default/FocusAdorner.xaml @@ -3,7 +3,8 @@ + StrokeDashArray="1,2" + Margin="1"/> - \ No newline at end of file + diff --git a/src/Avalonia.Themes.Default/LayoutTransformControl.xaml b/src/Avalonia.Themes.Default/LayoutTransformControl.xaml deleted file mode 100644 index b26f053622..0000000000 --- a/src/Avalonia.Themes.Default/LayoutTransformControl.xaml +++ /dev/null @@ -1,13 +0,0 @@ - \ No newline at end of file diff --git a/src/Avalonia.Themes.Default/ScrollBar.xaml b/src/Avalonia.Themes.Default/ScrollBar.xaml index ae40929573..2cb8ce2d24 100644 --- a/src/Avalonia.Themes.Default/ScrollBar.xaml +++ b/src/Avalonia.Themes.Default/ScrollBar.xaml @@ -3,7 +3,7 @@ - + - - - - - \ No newline at end of file + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Default/TabItem.xaml b/src/Avalonia.Themes.Default/TabItem.xaml new file mode 100644 index 0000000000..fcdb76524e --- /dev/null +++ b/src/Avalonia.Themes.Default/TabItem.xaml @@ -0,0 +1,45 @@ + + + + + + + + + + diff --git a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs index a268eff78a..b48efaa34e 100644 --- a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs +++ b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs @@ -37,6 +37,7 @@ namespace Avalonia.Rendering private DisplayDirtyRects _dirtyRectsDisplay = new DisplayDirtyRects(); private IRef _currentDraw; private readonly IDeferredRendererLock _lock; + private readonly object _sceneLock = new object(); /// /// Initializes a new instance of the class. @@ -84,6 +85,7 @@ namespace Avalonia.Rendering RenderTarget = renderTarget; _sceneBuilder = sceneBuilder ?? new SceneBuilder(); Layers = new RenderLayers(); + _lock = new ManagedDeferredRendererLock(); } /// @@ -118,8 +120,13 @@ namespace Avalonia.Rendering /// public void Dispose() { - var scene = Interlocked.Exchange(ref _scene, null); - scene?.Dispose(); + lock (_sceneLock) + { + var scene = _scene; + _scene = null; + scene?.Dispose(); + } + Stop(); Layers.Clear(); @@ -134,7 +141,8 @@ namespace Avalonia.Rendering // When unit testing the renderLoop may be null, so update the scene manually. UpdateScene(); } - + //It's safe to access _scene here without a lock since + //it's only changed from UI thread which we are currently on return _scene?.Item.HitTest(p, root, filter) ?? Enumerable.Empty(); } @@ -172,7 +180,8 @@ namespace Avalonia.Rendering } } - bool IRenderLoopTask.NeedsUpdate => _dirty == null || _dirty.Count > 0; + bool NeedsUpdate => _dirty == null || _dirty.Count > 0; + bool IRenderLoopTask.NeedsUpdate => NeedsUpdate; void IRenderLoopTask.Update(TimeSpan time) => UpdateScene(); @@ -197,79 +206,105 @@ namespace Avalonia.Rendering internal void UnitTestUpdateScene() => UpdateScene(); - internal void UnitTestRender() => Render(_scene.Item, false); + internal void UnitTestRender() => Render(false); private void Render(bool forceComposite) { using (var l = _lock.TryLock()) - if (l != null) - using (var scene = _scene?.Clone()) - { - Render(scene?.Item, forceComposite); - } - } - - private void Render(Scene scene, bool forceComposite) - { - bool renderOverlay = DrawDirtyRects || DrawFps; - bool composite = false; - - if (RenderTarget == null) { - RenderTarget = ((IRenderRoot)_root).CreateRenderTarget(); - } + if (l == null) + return; - if (renderOverlay) - { - _dirtyRectsDisplay.Tick(); - } - - try - { - if (scene != null && scene.Size != Size.Empty) + IDrawingContextImpl context = null; + try { - IDrawingContextImpl context = null; - - if (scene.Generation != _lastSceneId) + try { - context = RenderTarget.CreateDrawingContext(this); - Layers.Update(scene, context); + IDrawingContextImpl GetContext() + { + if (context != null) + return context; + if (RenderTarget == null) + RenderTarget = ((IRenderRoot)_root).CreateRenderTarget(); + return context = RenderTarget.CreateDrawingContext(this); - RenderToLayers(scene); + } - if (DebugFramesPath != null) + var (scene, updated) = UpdateRenderLayersAndConsumeSceneIfNeeded(GetContext); + using (scene) { - SaveDebugFrames(scene.Generation); + var overlay = DrawDirtyRects || DrawFps; + if (DrawDirtyRects) + _dirtyRectsDisplay.Tick(); + if (overlay) + RenderOverlay(scene.Item, GetContext()); + if (updated || forceComposite || overlay) + RenderComposite(scene.Item, GetContext()); } + } + finally + { + context?.Dispose(); + } + } + catch (RenderTargetCorruptedException ex) + { + Logging.Logger.Information("Renderer", this, "Render target was corrupted. Exception: {0}", ex); + RenderTarget?.Dispose(); + RenderTarget = null; + } + } + } - _lastSceneId = scene.Generation; + private (IRef scene, bool updated) UpdateRenderLayersAndConsumeSceneIfNeeded(Func contextFactory, + bool recursiveCall = false) + { + IRef sceneRef; + lock (_sceneLock) + sceneRef = _scene?.Clone(); + if (sceneRef == null) + return (null, false); + using (sceneRef) + { + var scene = sceneRef.Item; + if (scene.Generation != _lastSceneId) + { + var context = contextFactory(); + Layers.Update(scene, context); - composite = true; - } + RenderToLayers(scene); - if (renderOverlay) + if (DebugFramesPath != null) { - context = context ?? RenderTarget.CreateDrawingContext(this); - RenderOverlay(scene, context); - RenderComposite(scene, context); + SaveDebugFrames(scene.Generation); } - else if (composite || forceComposite) + + lock (_sceneLock) + _lastSceneId = scene.Generation; + + + // We have consumed the previously available scene, but there might be some dirty + // rects since the last update. *If* we are on UI thread, we can force immediate scene + // rebuild before rendering anything on-screen + // We are calling the same method recursively here + if (!recursiveCall && Dispatcher.UIThread.CheckAccess() && NeedsUpdate) { - context = context ?? RenderTarget.CreateDrawingContext(this); - RenderComposite(scene, context); + UpdateScene(); + var (rs, _) = UpdateRenderLayersAndConsumeSceneIfNeeded(contextFactory, true); + return (rs, true); } - - context?.Dispose(); + + // Indicate that we have updated the layers + return (sceneRef.Clone(), true); } + + // Just return scene, layers weren't updated + return (sceneRef.Clone(), false); } - catch (RenderTargetCorruptedException ex) - { - Logging.Logger.Information("Renderer", this, "Render target was corrupted. Exception: {0}", ex); - RenderTarget?.Dispose(); - RenderTarget = null; - } + } + private void Render(IDrawingContextImpl context, VisualNode node, IVisual layer, Rect clipBounds) { if (layer == null || node.LayerRoot == layer) @@ -405,6 +440,11 @@ namespace Avalonia.Rendering private void UpdateScene() { Dispatcher.UIThread.VerifyAccess(); + lock (_sceneLock) + { + if (_scene?.Item.Generation > _lastSceneId) + return; + } if (_root.IsVisible) { var sceneRef = RefCountable.Create(_scene?.Item.CloneScene() ?? new Scene(_root)); @@ -423,15 +463,23 @@ namespace Avalonia.Rendering } } - var oldScene = Interlocked.Exchange(ref _scene, sceneRef); - oldScene?.Dispose(); + lock (_sceneLock) + { + var oldScene = _scene; + _scene = sceneRef; + oldScene?.Dispose(); + } _dirty.Clear(); } else { - var oldScene = Interlocked.Exchange(ref _scene, null); - oldScene?.Dispose(); + lock (_sceneLock) + { + var oldScene = _scene; + _scene = null; + oldScene?.Dispose(); + } } } diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/RelativeSourceExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/RelativeSourceExtension.cs index ab82b7a28b..2f7256fa22 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/RelativeSourceExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/RelativeSourceExtension.cs @@ -1,13 +1,12 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using System; using Avalonia.Data; +using Portable.Xaml.Markup; namespace Avalonia.Markup.Xaml.MarkupExtensions { - using Portable.Xaml.Markup; - using System; - public class RelativeSourceExtension : MarkupExtension { public RelativeSourceExtension() @@ -24,10 +23,19 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions return new RelativeSource { Mode = Mode, + AncestorType = AncestorType, + AncestorLevel = AncestorLevel, + Tree = Tree, }; } [ConstructorArgument("mode")] - public RelativeSourceMode Mode { get; set; } + public RelativeSourceMode Mode { get; set; } = RelativeSourceMode.FindAncestor; + + public Type AncestorType { get; set; } + + public TreeType Tree { get; set; } + + public int AncestorLevel { get; set; } = 1; } } \ No newline at end of file diff --git a/src/Shared/PlatformSupport/StandardRuntimePlatform.cs b/src/Shared/PlatformSupport/StandardRuntimePlatform.cs index 186c55d9eb..8a5a725594 100644 --- a/src/Shared/PlatformSupport/StandardRuntimePlatform.cs +++ b/src/Shared/PlatformSupport/StandardRuntimePlatform.cs @@ -89,9 +89,11 @@ namespace Avalonia.Shared.PlatformSupport #if DEBUG if (Thread.CurrentThread.ManagedThreadId == GCThread?.ManagedThreadId) { - Console.Error.WriteLine("Native blob disposal from finalizer thread\nBacktrace: " - + Environment.StackTrace - + "\n\nBlob created by " + _backtrace); + lock(_lock) + if (!IsDisposed) + Console.Error.WriteLine("Native blob disposal from finalizer thread\nBacktrace: " + + Environment.StackTrace + + "\n\nBlob created by " + _backtrace); } #endif DoDispose(); diff --git a/src/Windows/Avalonia.Win32/FramebufferManager.cs b/src/Windows/Avalonia.Win32/FramebufferManager.cs index c910703181..87c5a1bb02 100644 --- a/src/Windows/Avalonia.Win32/FramebufferManager.cs +++ b/src/Windows/Avalonia.Win32/FramebufferManager.cs @@ -5,7 +5,7 @@ using Avalonia.Win32.Interop; namespace Avalonia.Win32 { - class FramebufferManager : IFramebufferPlatformSurface, IDisposable + class FramebufferManager : IFramebufferPlatformSurface { private readonly IntPtr _hwnd; private WindowFramebuffer _fb; @@ -29,10 +29,5 @@ namespace Avalonia.Win32 } return _fb; } - - public void Dispose() - { - _fb?.Deallocate(); - } } } diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 4c08e985cd..18f0696cd8 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -13,6 +13,7 @@ using Avalonia.Input.Raw; using Avalonia.OpenGL; using Avalonia.Platform; using Avalonia.Rendering; +using Avalonia.Threading; using Avalonia.Win32.Input; using Avalonia.Win32.Interop; using static Avalonia.Win32.Interop.UnmanagedMethods; @@ -234,8 +235,6 @@ namespace Avalonia.Win32 public void Dispose() { - _framebuffer?.Dispose(); - _framebuffer = null; if (_hwnd != IntPtr.Zero) { UnmanagedMethods.DestroyWindow(_hwnd); diff --git a/tests/Avalonia.Controls.UnitTests/LayoutTransformControlTests.cs b/tests/Avalonia.Controls.UnitTests/LayoutTransformControlTests.cs index d5f9818f89..13c946b549 100644 --- a/tests/Avalonia.Controls.UnitTests/LayoutTransformControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/LayoutTransformControlTests.cs @@ -1,6 +1,4 @@ -using Avalonia.Controls.Presenters; using Avalonia.Controls.Shapes; -using Avalonia.Controls.Templates; using Avalonia.Media; using Xunit; @@ -311,20 +309,10 @@ namespace Avalonia.Controls.UnitTests { var lt = new LayoutTransformControl() { - LayoutTransform = transform, - Template = new FuncControlTemplate( - p => new ContentPresenter() { Content = p.Content }) + LayoutTransform = transform }; - lt.Content = new Rectangle() { Width = width, Height = height }; - - lt.ApplyTemplate(); - - //we need to force create visual child - //so the measure after is correct - (lt.Presenter as ContentPresenter).UpdateChild(); - - Assert.NotNull(lt.Presenter?.Child); + lt.Child = new Rectangle() { Width = width, Height = height }; lt.Measure(Size.Infinity); lt.Arrange(new Rect(lt.DesiredSize)); @@ -332,4 +320,4 @@ namespace Avalonia.Controls.UnitTests return lt; } } -} \ No newline at end of file +} diff --git a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs index 322c14c6bd..a5c3881d37 100644 --- a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs @@ -22,7 +22,7 @@ namespace Avalonia.Controls.UnitTests TabItem selected; var target = new TabControl { - Template = new FuncControlTemplate(CreateTabControlTemplate), + Template = TabControlTemplate(), Items = new[] { (selected = new TabItem @@ -61,7 +61,7 @@ namespace Avalonia.Controls.UnitTests var target = new TabControl { - Template = new FuncControlTemplate(CreateTabControlTemplate), + Template = TabControlTemplate(), Items = items, }; @@ -94,7 +94,7 @@ namespace Avalonia.Controls.UnitTests var target = new TabControl { - Template = new FuncControlTemplate(CreateTabControlTemplate), + Template = TabControlTemplate(), Items = collection, }; @@ -147,7 +147,7 @@ namespace Avalonia.Controls.UnitTests }, Child = new TabControl { - Template = new FuncControlTemplate(CreateTabControlTemplate), + Template = TabControlTemplate(), Items = collection, } }; @@ -172,7 +172,7 @@ namespace Avalonia.Controls.UnitTests var target = new TabControl { - Template = new FuncControlTemplate(CreateTabControlTemplate), + Template = TabControlTemplate(), DataContext = "Base", DataTemplates = { @@ -182,41 +182,39 @@ namespace Avalonia.Controls.UnitTests }; ApplyTemplate(target); - var carousel = (Carousel)target.Pages; - var container = (ContentPresenter)carousel.Presenter.Panel.Children.Single(); - container.UpdateChild(); - var dataContext = ((TextBlock)container.Child).DataContext; + target.ContentPart.UpdateChild(); + var dataContext = ((TextBlock)target.ContentPart.Child).DataContext; Assert.Equal(items[0], dataContext); target.SelectedIndex = 1; - container = (ContentPresenter)carousel.Presenter.Panel.Children.Single(); - container.UpdateChild(); - dataContext = ((Button)container.Child).DataContext; + target.ContentPart.UpdateChild(); + dataContext = ((Button)target.ContentPart.Child).DataContext; Assert.Equal(items[1], dataContext); target.SelectedIndex = 2; - dataContext = ((TextBlock)carousel.Presenter.Panel.Children.Single()).DataContext; + target.ContentPart.UpdateChild(); + dataContext = ((TextBlock)target.ContentPart.Child).DataContext; Assert.Equal("Base", dataContext); target.SelectedIndex = 3; - container = (ContentPresenter)carousel.Presenter.Panel.Children[0]; - container.UpdateChild(); - dataContext = ((TextBlock)container.Child).DataContext; + target.ContentPart.UpdateChild(); + dataContext = ((TextBlock)target.ContentPart.Child).DataContext; Assert.Equal("Qux", dataContext); target.SelectedIndex = 4; - dataContext = ((TextBlock)carousel.Presenter.Panel.Children.Single()).DataContext; + target.ContentPart.UpdateChild(); + dataContext = target.ContentPart.DataContext; Assert.Equal("Base", dataContext); } /// - /// Non-headered control items should result in TabStripItems with empty content. + /// Non-headered control items should result in TabItems with empty header. /// /// - /// If a TabStrip is created with non IHeadered controls as its items, don't try to - /// display the control in the TabStripItem: if the TabStrip is part of a TabControl - /// then *that* will also try to display the control, resulting in dual-parentage + /// If a TabControl is created with non IHeadered controls as its items, don't try to + /// display the control in the header: if the control is part of the header then + /// *that* control would also end up in the content region, resulting in dual-parentage /// breakage. /// [Fact] @@ -230,18 +228,20 @@ namespace Avalonia.Controls.UnitTests var target = new TabControl { - Template = new FuncControlTemplate(CreateTabControlTemplate), + Template = TabControlTemplate(), Items = items, }; ApplyTemplate(target); - var result = target.TabStrip.GetLogicalChildren() - .OfType() - .Select(x => x.Content) + var logicalChildren = target.ItemsPresenterPart.Panel.GetLogicalChildren(); + + var result = logicalChildren + .OfType() + .Select(x => x.Header) .ToList(); - Assert.Equal(new object[] { string.Empty, string.Empty }, result); + Assert.Equal(new object[] { null, null }, result); } [Fact] @@ -249,7 +249,7 @@ namespace Avalonia.Controls.UnitTests { TabControl target = new TabControl { - Template = new FuncControlTemplate(CreateTabControlTemplate), + Template = TabControlTemplate(), Items = new[] { new TabItem { Header = "Foo" }, @@ -262,70 +262,61 @@ namespace Avalonia.Controls.UnitTests target.SelectedIndex = 2; - var carousel = (Carousel)target.Pages; - var page = (TabItem)carousel.SelectedItem; + var page = (TabItem)target.SelectedItem; Assert.Null(page.Content); } - private Control CreateTabControlTemplate(TabControl parent) + private IControlTemplate TabControlTemplate() { - return new StackPanel - { - Children = - { - new TabStrip - { - Name = "PART_TabStrip", - Template = new FuncControlTemplate(CreateTabStripTemplate), - MemberSelector = TabControl.HeaderSelector, - [!TabStrip.ItemsProperty] = parent[!TabControl.ItemsProperty], - [!!TabStrip.SelectedIndexProperty] = parent[!!TabControl.SelectedIndexProperty] - }, - new Carousel - { - Name = "PART_Content", - Template = new FuncControlTemplate(CreateCarouselTemplate), - MemberSelector = TabControl.ContentSelector, - [!Carousel.ItemsProperty] = parent[!TabControl.ItemsProperty], - [!Carousel.SelectedItemProperty] = parent[!TabControl.SelectedItemProperty], - } - } - }; - } + return new FuncControlTemplate(parent => - private Control CreateTabStripTemplate(TabStrip parent) - { - return new ItemsPresenter - { - Name = "PART_ItemsPresenter", - [~ItemsPresenter.ItemsProperty] = parent[~ItemsControl.ItemsProperty], - [!CarouselPresenter.MemberSelectorProperty] = parent[!ItemsControl.MemberSelectorProperty], - }; + new StackPanel + { + Children = { + new ItemsPresenter + { + Name = "PART_ItemsPresenter", + [!TabStrip.ItemsProperty] = parent[!TabControl.ItemsProperty], + [!TabStrip.ItemTemplateProperty] = parent[!TabControl.ItemTemplateProperty], + }, + new ContentPresenter + { + Name = "PART_Content", + [!ContentPresenter.ContentProperty] = parent[!TabControl.SelectedContentProperty], + [!ContentPresenter.ContentTemplateProperty] = parent[!TabControl.SelectedContentTemplateProperty], + } + } + }); } - private Control CreateCarouselTemplate(Carousel control) + private IControlTemplate TabItemTemplate() { - return new CarouselPresenter - { - Name = "PART_ItemsPresenter", - [!CarouselPresenter.ItemsProperty] = control[!ItemsControl.ItemsProperty], - [!CarouselPresenter.ItemsPanelProperty] = control[!ItemsControl.ItemsPanelProperty], - [!CarouselPresenter.MemberSelectorProperty] = control[!ItemsControl.MemberSelectorProperty], - [!CarouselPresenter.SelectedIndexProperty] = control[!SelectingItemsControl.SelectedIndexProperty], - [~CarouselPresenter.PageTransitionProperty] = control[~Carousel.PageTransitionProperty], - }; + return new FuncControlTemplate(parent => + new ContentPresenter + { + Name = "PART_ContentPresenter", + [!ContentPresenter.ContentProperty] = parent[!TabItem.HeaderProperty], + [!ContentPresenter.ContentTemplateProperty] = parent[!TabItem.HeaderTemplateProperty] + }); } private void ApplyTemplate(TabControl target) { target.ApplyTemplate(); - var carousel = (Carousel)target.Pages; - carousel.ApplyTemplate(); - carousel.Presenter.ApplyTemplate(); - var tabStrip = (TabStrip)target.TabStrip; - tabStrip.ApplyTemplate(); - tabStrip.Presenter.ApplyTemplate(); + + target.Presenter.ApplyTemplate(); + + foreach (var tabItem in target.GetLogicalChildren().OfType()) + { + tabItem.Template = TabItemTemplate(); + + tabItem.ApplyTemplate(); + + ((ContentPresenter)tabItem.Presenter).UpdateChild(); + } + + target.ContentPart.ApplyTemplate(); } private class Item diff --git a/tests/Avalonia.Controls.UnitTests/ViewboxTests.cs b/tests/Avalonia.Controls.UnitTests/ViewboxTests.cs new file mode 100644 index 0000000000..ad0f318d2f --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/ViewboxTests.cs @@ -0,0 +1,105 @@ +using Avalonia.Controls.Shapes; +using Avalonia.Media; +using Xunit; + +namespace Avalonia.Controls.UnitTests +{ + public class ViewboxTests + { + [Fact] + public void Viewbox_Stretch_Uniform_Child() + { + var target = new Viewbox() { Child = new Rectangle() { Width = 100, Height = 50 } }; + + target.Measure(new Size(200, 200)); + target.Arrange(new Rect(new Point(0, 0), target.DesiredSize)); + + Assert.Equal(new Size(200, 100), target.DesiredSize); + var scaleTransform = target.Child.RenderTransform as ScaleTransform; + + Assert.NotNull(scaleTransform); + Assert.Equal(2.0, scaleTransform.ScaleX); + Assert.Equal(2.0, scaleTransform.ScaleY); + } + + [Fact] + public void Viewbox_Stretch_None_Child() + { + var target = new Viewbox() { Stretch = Stretch.None, Child = new Rectangle() { Width = 100, Height = 50 } }; + + target.Measure(new Size(200, 200)); + target.Arrange(new Rect(new Point(0, 0), target.DesiredSize)); + + Assert.Equal(new Size(100, 50), target.DesiredSize); + var scaleTransform = target.Child.RenderTransform as ScaleTransform; + + Assert.NotNull(scaleTransform); + Assert.Equal(1.0, scaleTransform.ScaleX); + Assert.Equal(1.0, scaleTransform.ScaleY); + } + + [Fact] + public void Viewbox_Stretch_Fill_Child() + { + var target = new Viewbox() { Stretch = Stretch.Fill, Child = new Rectangle() { Width = 100, Height = 50 } }; + + target.Measure(new Size(200, 200)); + target.Arrange(new Rect(new Point(0, 0), target.DesiredSize)); + + Assert.Equal(new Size(200, 200), target.DesiredSize); + var scaleTransform = target.Child.RenderTransform as ScaleTransform; + + Assert.NotNull(scaleTransform); + Assert.Equal(2.0, scaleTransform.ScaleX); + Assert.Equal(4.0, scaleTransform.ScaleY); + } + + [Fact] + public void Viewbox_Stretch_UniformToFill_Child() + { + var target = new Viewbox() { Stretch = Stretch.UniformToFill, Child = new Rectangle() { Width = 100, Height = 50 } }; + + target.Measure(new Size(200, 200)); + target.Arrange(new Rect(new Point(0, 0), target.DesiredSize)); + + Assert.Equal(new Size(200, 200), target.DesiredSize); + var scaleTransform = target.Child.RenderTransform as ScaleTransform; + + Assert.NotNull(scaleTransform); + Assert.Equal(4.0, scaleTransform.ScaleX); + Assert.Equal(4.0, scaleTransform.ScaleY); + } + + [Fact] + public void Viewbox_Stretch_Uniform_Child_With_Unrestricted_Width() + { + var target = new Viewbox() { Child = new Rectangle() { Width = 100, Height = 50 } }; + + target.Measure(new Size(double.PositiveInfinity, 200)); + target.Arrange(new Rect(new Point(0, 0), target.DesiredSize)); + + Assert.Equal(new Size(400, 200), target.DesiredSize); + var scaleTransform = target.Child.RenderTransform as ScaleTransform; + + Assert.NotNull(scaleTransform); + Assert.Equal(4.0, scaleTransform.ScaleX); + Assert.Equal(4.0, scaleTransform.ScaleY); + } + + [Fact] + public void Viewbox_Stretch_Uniform_Child_With_Unrestricted_Height() + { + var target = new Viewbox() { Child = new Rectangle() { Width = 100, Height = 50 } }; + + target.Measure(new Size(200, double.PositiveInfinity)); + target.Arrange(new Rect(new Point(0, 0), target.DesiredSize)); + + Assert.Equal(new Size(200, 100), target.DesiredSize); + var scaleTransform = target.Child.RenderTransform as ScaleTransform; + + Assert.NotNull(scaleTransform); + Assert.Equal(2.0, scaleTransform.ScaleX); + Assert.Equal(2.0, scaleTransform.ScaleY); + } + } +} diff --git a/tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs b/tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs index 78d0efcbf9..ffdb146eec 100644 --- a/tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs +++ b/tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs @@ -99,6 +99,8 @@ namespace Avalonia.Layout.UnitTests } }; + window.Resources["ScrollBarThickness"] = 10.0; + window.Show(); window.LayoutManager.ExecuteInitialLayoutPass(window); diff --git a/tests/Avalonia.Markup.UnitTests/Data/BindingTests_RelativeSource.cs b/tests/Avalonia.Markup.UnitTests/Data/BindingTests_RelativeSource.cs index f2ddec6f3c..4c572acab1 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/BindingTests_RelativeSource.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/BindingTests_RelativeSource.cs @@ -1,9 +1,13 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using System; +using System.Collections.Generic; +using System.Reactive.Subjects; using Avalonia.Controls; using Avalonia.Data; -using Avalonia.Markup.Data; +using Avalonia.Data.Core; +using Avalonia.Markup.Parsers; using Avalonia.UnitTests; using Xunit; @@ -162,5 +166,99 @@ namespace Avalonia.Markup.UnitTests.Data decorator2.Child = target; Assert.Equal("decorator2", target.Text); } + + [Fact] + public void Should_Update_When_Detached_And_Attached_To_Visual_Tree_With_BindingPath() + { + TextBlock target; + Decorator decorator1; + Decorator decorator2; + + var viewModel = new { Value = "Foo" }; + + var root1 = new TestRoot + { + Child = decorator1 = new Decorator + { + Name = "decorator1", + Child = target = new TextBlock(), + }, + DataContext = viewModel + }; + + var root2 = new TestRoot + { + Child = decorator2 = new Decorator + { + Name = "decorator2", + }, + DataContext = viewModel + }; + + var binding = new Binding + { + Path = "DataContext.Value", + RelativeSource = new RelativeSource + { + AncestorType = typeof(Decorator), + } + }; + + target.Bind(TextBox.TextProperty, binding); + Assert.Equal("Foo", target.Text); + + decorator1.Child = null; + Assert.Null(target.Text); + + decorator2.Child = target; + Assert.Equal("Foo", target.Text); + } + + [Fact] + public void Should_Update_When_Detached_And_Attached_To_Visual_Tree_With_ComplexBindingPath() + { + TextBlock target; + Decorator decorator1; + Decorator decorator2; + + var vm = new { Foo = new { Value = "Foo" } }; + + var root1 = new TestRoot + { + Child = decorator1 = new Decorator + { + Name = "decorator1", + Child = target = new TextBlock(), + }, + DataContext = vm + }; + + var root2 = new TestRoot + { + Child = decorator2 = new Decorator + { + Name = "decorator2", + }, + DataContext = vm + }; + + var binding = new Binding + { + Path = "DataContext.Foo.Value", + RelativeSource = new RelativeSource + { + AncestorType = typeof(Decorator), + } + }; + + target.Bind(TextBox.TextProperty, binding); + Assert.Equal("Foo", target.Text); + + decorator1.Child = null; + Assert.Null(target.Text); + + decorator2.Child = target; + Assert.Equal("Foo", target.Text); + } } } diff --git a/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Property.cs b/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Property.cs index a97c998264..4f7264f2f2 100644 --- a/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Property.cs +++ b/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Property.cs @@ -1,10 +1,10 @@ -using Avalonia.Data; -using Avalonia.Markup.Parsers; -using System; +using System; using System.Collections.Generic; using System.Reactive.Linq; -using System.Text; +using System.Reactive.Subjects; using System.Threading.Tasks; +using Avalonia.Data; +using Avalonia.Markup.Parsers; using Xunit; namespace Avalonia.Markup.UnitTests.Parsers @@ -38,5 +38,55 @@ namespace Avalonia.Markup.UnitTests.Parsers GC.KeepAlive(data); } + + [Fact] + public void Should_Update_Value_After_Root_Changes() + { + var root = new { DataContext = new { Value = "Foo" } }; + var subject = new Subject(); + var obs = ExpressionObserverBuilder.Build(subject, "DataContext.Value"); + + var values = new List(); + obs.Subscribe(v => values.Add(v)); + + subject.OnNext(root); + subject.OnNext(null); + subject.OnNext(root); + + Assert.Equal("Foo", values[0]); + + Assert.IsType(values[1]); + var bn = values[1] as BindingNotification; + Assert.Equal(AvaloniaProperty.UnsetValue, bn.Value); + Assert.Equal(BindingErrorType.Error, bn.ErrorType); + + Assert.Equal(3, values.Count); + Assert.Equal("Foo", values[2]); + } + + [Fact] + public void Should_Update_Value_After_Root_Changes_With_ComplexPath() + { + var root = new { DataContext = new { Foo = new { Value = "Foo" } } }; + var subject = new Subject(); + var obs = ExpressionObserverBuilder.Build(subject, "DataContext.Foo.Value"); + + var values = new List(); + obs.Subscribe(v => values.Add(v)); + + subject.OnNext(root); + subject.OnNext(null); + subject.OnNext(root); + + Assert.Equal("Foo", values[0]); + + Assert.IsType(values[1]); + var bn = values[1] as BindingNotification; + Assert.Equal(AvaloniaProperty.UnsetValue, bn.Value); + Assert.Equal(BindingErrorType.Error, bn.ErrorType); + + Assert.Equal(3, values.Count); + Assert.Equal("Foo", values[2]); + } } } diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests_HitTesting.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests_HitTesting.cs index a53809a029..ab5f5f37f7 100644 --- a/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests_HitTesting.cs +++ b/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests_HitTesting.cs @@ -405,7 +405,8 @@ namespace Avalonia.Visuals.UnitTests.Rendering root.Renderer = new DeferredRenderer(root, null); root.Measure(Size.Infinity); root.Arrange(new Rect(container.DesiredSize)); - + + root.Renderer.Paint(Rect.Empty); var result = root.Renderer.HitTest(new Point(50, 150), root, null).First(); Assert.Equal(item1, result); @@ -421,6 +422,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering container.InvalidateArrange(); container.Arrange(new Rect(container.DesiredSize)); + root.Renderer.Paint(Rect.Empty); result = root.Renderer.HitTest(new Point(50, 150), root, null).First(); Assert.Equal(item2, result);