From 4f549c16fc35d527da751167b19467f5bd51918a Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Fri, 31 Aug 2018 14:43:48 +0200 Subject: [PATCH 01/70] Initial --- samples/ControlCatalog/ControlCatalog.csproj | 15 ++ samples/ControlCatalog/MainView.xaml | 6 +- .../ControlCatalog/Pages/TabControlPage.xaml | 124 ++++++++++ .../Pages/TabControlPage.xaml.cs | 80 +++++++ samples/ControlCatalog/SideBar.xaml | 104 +++++---- src/Avalonia.Controls/ContentControl.cs | 4 +- .../Generators/TabItemContainerGenerator.cs | 60 +++++ .../Primitives/HeaderedContentControl.cs | 21 +- src/Avalonia.Controls/TabControl.cs | 176 ++++++++------ src/Avalonia.Controls/TabItem.cs | 73 ++++++ .../Avalonia.Themes.Default.csproj | 8 + src/Avalonia.Themes.Default/DefaultTheme.xaml | 1 + src/Avalonia.Themes.Default/TabControl.xaml | 220 ++++++++++++++---- src/Avalonia.Themes.Default/TabItem.xaml | 45 ++++ .../TabControlTests.cs | 145 ++++++------ 15 files changed, 830 insertions(+), 252 deletions(-) create mode 100644 samples/ControlCatalog/Pages/TabControlPage.xaml create mode 100644 samples/ControlCatalog/Pages/TabControlPage.xaml.cs create mode 100644 src/Avalonia.Controls/Generators/TabItemContainerGenerator.cs create mode 100644 src/Avalonia.Themes.Default/TabItem.xaml diff --git a/samples/ControlCatalog/ControlCatalog.csproj b/samples/ControlCatalog/ControlCatalog.csproj index dea9b35e24..8eab683049 100644 --- a/samples/ControlCatalog/ControlCatalog.csproj +++ b/samples/ControlCatalog/ControlCatalog.csproj @@ -11,6 +11,9 @@ + + + @@ -33,6 +36,18 @@ + + + + TabControlPage.xaml + + + + + + MSBuild:Compile + + \ No newline at end of file diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index 87cb5e9c5c..1b613aee9a 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -2,9 +2,6 @@ xmlns:pages="clr-namespace:ControlCatalog.Pages" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> - - - @@ -21,12 +18,13 @@ - + + diff --git a/samples/ControlCatalog/Pages/TabControlPage.xaml b/samples/ControlCatalog/Pages/TabControlPage.xaml new file mode 100644 index 0000000000..5b10e7d790 --- /dev/null +++ b/samples/ControlCatalog/Pages/TabControlPage.xaml @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + 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/SideBar.xaml b/samples/ControlCatalog/SideBar.xaml index 7d72d1821b..dbab4e4a27 100644 --- a/samples/ControlCatalog/SideBar.xaml +++ b/samples/ControlCatalog/SideBar.xaml @@ -1,52 +1,64 @@ - + - + - + - + diff --git a/src/Avalonia.Controls/ContentControl.cs b/src/Avalonia.Controls/ContentControl.cs index 6da6da54a5..4621524bdc 100644 --- a/src/Avalonia.Controls/ContentControl.cs +++ b/src/Avalonia.Controls/ContentControl.cs @@ -45,7 +45,7 @@ namespace Avalonia.Controls static ContentControl() { ContentControlMixin.Attach(ContentProperty, x => x.LogicalChildren); - } + } /// /// Gets or sets the content to display. @@ -65,7 +65,7 @@ namespace Avalonia.Controls { get { return GetValue(ContentTemplateProperty); } set { SetValue(ContentTemplateProperty, value); } - } + } /// /// Gets the presenter from the control's template. diff --git a/src/Avalonia.Controls/Generators/TabItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/TabItemContainerGenerator.cs new file mode 100644 index 0000000000..75788e393f --- /dev/null +++ b/src/Avalonia.Controls/Generators/TabItemContainerGenerator.cs @@ -0,0 +1,60 @@ +// 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. + +namespace Avalonia.Controls.Generators +{ + using Avalonia.Controls.Primitives; + + 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; + + if (tabItem.HeaderTemplate == null) + { + tabItem[~HeaderedContentControl.HeaderTemplateProperty] = Owner[~ItemsControl.ItemTemplateProperty]; + } + + if (tabItem.Header == null) + { + if (item is IHeadered headered) + { + if (tabItem.Header != headered.Header) + { + 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/Primitives/HeaderedContentControl.cs b/src/Avalonia.Controls/Primitives/HeaderedContentControl.cs index d67ebfd489..b0517c23f1 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 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/TabControl.cs b/src/Avalonia.Controls/TabControl.cs index 70cf8b4e05..043242543b 100644 --- a/src/Avalonia.Controls/TabControl.cs +++ b/src/Avalonia.Controls/TabControl.cs @@ -1,10 +1,14 @@ // 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 System; + 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 +18,50 @@ 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 { Orientation = Orientation.Horizontal }); + + internal ItemsPresenter ItemsPresenterPart { get; private set; } + + internal ContentPresenter ContentPart { get; private set; } /// /// Initializes static members of the class. @@ -43,35 +69,26 @@ 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; - } - - /// - /// Gets or sets the transition to use when switching tabs. - /// - public IPageTransition PageTransition - { - get { return GetValue(PageTransitionProperty); } - set { SetValue(PageTransitionProperty, value); } + get { return GetValue(VerticalContentAlignmentProperty); } + set { SetValue(VerticalContentAlignmentProperty, value); } } /// @@ -83,67 +100,82 @@ namespace Avalonia.Controls set { SetValue(TabStripPlacementProperty, value); } } - protected override IItemContainerGenerator CreateItemContainerGenerator() + /// + /// Gets or sets the data template used to display the content of the control. + /// + public IDataTemplate ContentTemplate { - // 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; + get { return GetValue(ContentTemplateProperty); } + set { SetValue(ContentTemplateProperty, value); } } - protected override void OnTemplateApplied(TemplateAppliedEventArgs e) + /// + /// Gets or sets the currently selected content. + /// + /// + /// The content of the selected. + /// + public object SelectedContent { - base.OnTemplateApplied(e); - - TabStrip = e.NameScope.Find("PART_TabStrip"); - Pages = e.NameScope.Find("PART_Content"); + get { return GetValue(SelectedContentProperty); } + set { SetValue(SelectedContentProperty, value); } } /// - /// Selects the content of a tab item. + /// Gets or sets the template for the currently selected content. /// - /// The tab item. - /// The content. - private static object SelectContent(object o) + /// + /// The selected content template. + /// + public IDataTemplate SelectedContentTemplate + { + get { return GetValue(SelectedContentTemplateProperty); } + set { SetValue(SelectedContentTemplateProperty, value); } + } + + protected override IItemContainerGenerator CreateItemContainerGenerator() + { + return new TabItemContainerGenerator(this); + } + + protected override void OnTemplateApplied(TemplateAppliedEventArgs e) { - var content = o as IContentControl; + base.OnTemplateApplied(e); + + ItemsPresenterPart = e.NameScope.Find("PART_ItemsPresenter"); - if (content != null) + if (ItemsPresenterPart == null) { - return content.Content; + throw new NotSupportedException("ItemsPresenter not found."); } - else + + ContentPart = e.NameScope.Find("PART_Content"); + + if (ContentPart == null) { - return o; - } + throw new NotSupportedException("ContentPresenter not found."); + } } - /// - /// Selects the header of a tab item. - /// - /// The tab item. - /// The content. - private static object SelectHeader(object o) + /// + protected override void OnGotFocus(GotFocusEventArgs e) { - var headered = o as IHeadered; - var control = o as IControl; + base.OnGotFocus(e); - if (headered != null) + if (e.NavigationMethod == NavigationMethod.Directional) { - return headered.Header ?? string.Empty; + e.Handled = UpdateSelectionFromEventSource(e.Source); } - 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 + } + + /// + protected override void OnPointerPressed(PointerPressedEventArgs e) + { + base.OnPointerPressed(e); + + 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..80a3846ab2 100644 --- a/src/Avalonia.Controls/TabItem.cs +++ b/src/Avalonia.Controls/TabItem.cs @@ -11,12 +11,20 @@ namespace Avalonia.Controls /// public class TabItem : HeaderedContentControl, ISelectable { + /// + /// Defines the property. + /// + public static readonly StyledProperty TabStripPlacementProperty = + TabControl.TabStripPlacementProperty.AddOwner(); + /// /// Defines the property. /// public static readonly StyledProperty IsSelectedProperty = ListBoxItem.IsSelectedProperty.AddOwner(); + private TabControl _parentTabControl; + /// /// Initializes static members of the class. /// @@ -24,6 +32,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 +55,57 @@ namespace Avalonia.Controls get { return GetValue(IsSelectedProperty); } set { SetValue(IsSelectedProperty, value); } } + + internal TabControl ParentTabControl + { + get => _parentTabControl; + set => _parentTabControl = value; + } + + 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.Themes.Default/Avalonia.Themes.Default.csproj b/src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj index eceafc2371..4e73afb755 100644 --- a/src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj +++ b/src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj @@ -2,6 +2,9 @@ netstandard2.0 + + + @@ -15,4 +18,9 @@ + + + MSBuild:Compile + + \ No newline at end of file diff --git a/src/Avalonia.Themes.Default/DefaultTheme.xaml b/src/Avalonia.Themes.Default/DefaultTheme.xaml index 2b9132ee56..7b8710866a 100644 --- a/src/Avalonia.Themes.Default/DefaultTheme.xaml +++ b/src/Avalonia.Themes.Default/DefaultTheme.xaml @@ -30,6 +30,7 @@ + diff --git a/src/Avalonia.Themes.Default/TabControl.xaml b/src/Avalonia.Themes.Default/TabControl.xaml index 76cf190151..43265764ec 100644 --- a/src/Avalonia.Themes.Default/TabControl.xaml +++ b/src/Avalonia.Themes.Default/TabControl.xaml @@ -1,50 +1,172 @@ - - - - - - \ 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..311fb2b973 --- /dev/null +++ b/src/Avalonia.Themes.Default/TabItem.xaml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + 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 From 63de9f3ddf60ce26f6eadf75f228c8f166012bcb Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Thu, 6 Sep 2018 16:22:01 +0200 Subject: [PATCH 02/70] Rework default style --- src/Avalonia.Themes.Default/TabControl.xaml | 114 ++------------------ 1 file changed, 8 insertions(+), 106 deletions(-) diff --git a/src/Avalonia.Themes.Default/TabControl.xaml b/src/Avalonia.Themes.Default/TabControl.xaml index 43265764ec..1e5b5c8841 100644 --- a/src/Avalonia.Themes.Default/TabControl.xaml +++ b/src/Avalonia.Themes.Default/TabControl.xaml @@ -44,79 +44,10 @@ - + + From ab97dbb12c87f2188eebd8bd22add64c6464ef81 Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Thu, 6 Sep 2018 20:28:20 +0200 Subject: [PATCH 03/70] Selector rework --- samples/ControlCatalog/SideBar.xaml | 5 ++--- src/Avalonia.Themes.Default/TabItem.xaml | 16 +++++++--------- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/samples/ControlCatalog/SideBar.xaml b/samples/ControlCatalog/SideBar.xaml index dbab4e4a27..3552f224fc 100644 --- a/samples/ControlCatalog/SideBar.xaml +++ b/samples/ControlCatalog/SideBar.xaml @@ -39,7 +39,7 @@ - - - - - - - - - - - - From 00cc08f979b38625393bd87dd06fe0070fff1faa Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Thu, 6 Sep 2018 23:44:04 +0200 Subject: [PATCH 04/70] New selectors --- samples/ControlCatalog/SideBar.xaml | 4 ++-- src/Avalonia.Themes.Default/TabItem.xaml | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/samples/ControlCatalog/SideBar.xaml b/samples/ControlCatalog/SideBar.xaml index 3552f224fc..62c200d8d6 100644 --- a/samples/ControlCatalog/SideBar.xaml +++ b/samples/ControlCatalog/SideBar.xaml @@ -52,11 +52,11 @@ - - diff --git a/src/Avalonia.Themes.Default/TabItem.xaml b/src/Avalonia.Themes.Default/TabItem.xaml index b51d1426ef..8b30d40d7e 100644 --- a/src/Avalonia.Themes.Default/TabItem.xaml +++ b/src/Avalonia.Themes.Default/TabItem.xaml @@ -25,19 +25,19 @@ - - - - - From 8985af21ba685036740e8d75d3a8b57b46c0e53d Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Fri, 7 Sep 2018 00:24:24 +0200 Subject: [PATCH 05/70] Selector fix --- samples/ControlCatalog/SideBar.xaml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/samples/ControlCatalog/SideBar.xaml b/samples/ControlCatalog/SideBar.xaml index 62c200d8d6..3ec8e43b07 100644 --- a/samples/ControlCatalog/SideBar.xaml +++ b/samples/ControlCatalog/SideBar.xaml @@ -52,12 +52,16 @@ + + From fc709c3a0fc9b72b6eec7038b2be3adf8917c036 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 7 Sep 2018 00:51:25 +0200 Subject: [PATCH 06/70] Partially fix tab strip orientation. --- src/Avalonia.Controls/TabControl.cs | 2 +- src/Avalonia.Themes.Default/TabControl.xaml | 30 +++++++-------------- 2 files changed, 11 insertions(+), 21 deletions(-) diff --git a/src/Avalonia.Controls/TabControl.cs b/src/Avalonia.Controls/TabControl.cs index 043242543b..f81e1560c2 100644 --- a/src/Avalonia.Controls/TabControl.cs +++ b/src/Avalonia.Controls/TabControl.cs @@ -57,7 +57,7 @@ namespace Avalonia.Controls /// The default value for the property. /// private static readonly FuncTemplate DefaultPanel = - new FuncTemplate(() => new WrapPanel { Orientation = Orientation.Horizontal }); + new FuncTemplate(() => new WrapPanel()); internal ItemsPresenter ItemsPresenterPart { get; private set; } diff --git a/src/Avalonia.Themes.Default/TabControl.xaml b/src/Avalonia.Themes.Default/TabControl.xaml index 1e5b5c8841..e551b2e841 100644 --- a/src/Avalonia.Themes.Default/TabControl.xaml +++ b/src/Avalonia.Themes.Default/TabControl.xaml @@ -24,7 +24,6 @@ + - - + From ed460795716b3df233fec43ed791a5a114ebf498 Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Fri, 7 Sep 2018 01:30:49 +0200 Subject: [PATCH 07/70] Share the same style between TabItem and TabStripItem --- src/Avalonia.Themes.Default/TabItem.xaml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/Avalonia.Themes.Default/TabItem.xaml b/src/Avalonia.Themes.Default/TabItem.xaml index 8b30d40d7e..6d7cdea1fd 100644 --- a/src/Avalonia.Themes.Default/TabItem.xaml +++ b/src/Avalonia.Themes.Default/TabItem.xaml @@ -3,12 +3,9 @@ - - - - From e5cac827b18c00e06dbe9c0088225994b7259e82 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 22 Oct 2018 09:39:52 +0200 Subject: [PATCH 08/70] Make TemplatedParent a direct property. During initial layout, `TemplatedParent` is read tens of thousands of times. This being a styled property that used `inherits: true` meant that it was showing up in profiling as taking up a significant amount of time. Make `TemplatedParent` a direct property so that reading it is a simple property access. This doesn't actually complicate the code much at all as once set the property value doesn't change, so `inherited: true` semantics are not that important. --- src/Avalonia.Controls/ItemsControl.cs | 2 -- .../Primitives/TemplatedControl.cs | 19 ++++++++++++++++++- .../Templates/TemplateExtensions.cs | 6 ++++-- src/Avalonia.Styling/StyledElement.cs | 12 ++++++++---- 4 files changed, 30 insertions(+), 9 deletions(-) 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/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/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.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); } /// From 44e12491eab44ca65673f83b594f1c558b68e1f8 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 22 Oct 2018 09:40:08 +0200 Subject: [PATCH 09/70] Ignore Avalonia.Native in ncrunch. --- .ncrunch/Avalonia.Native.v3.ncrunchproject | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .ncrunch/Avalonia.Native.v3.ncrunchproject 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 From 269fc2a5d2ab70cf33a8bb2b019921732c41b2f4 Mon Sep 17 00:00:00 2001 From: wojciech krysiak Date: Tue, 2 Oct 2018 16:13:32 +0200 Subject: [PATCH 10/70] MessCommit --- src/Avalonia.Controls/Grid.cs | 223 ++++++++++++++++++++++++++++++++++ 1 file changed, 223 insertions(+) diff --git a/src/Avalonia.Controls/Grid.cs b/src/Avalonia.Controls/Grid.cs index 1a07ccaf7e..71ae414a4f 100644 --- a/src/Avalonia.Controls/Grid.cs +++ b/src/Avalonia.Controls/Grid.cs @@ -3,10 +3,12 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Runtime.CompilerServices; using Avalonia.Collections; using Avalonia.Controls.Utils; +using Avalonia.VisualTree; using JetBrains.Annotations; namespace Avalonia.Controls @@ -44,6 +46,189 @@ namespace Avalonia.Controls public static readonly AttachedProperty RowSpanProperty = AvaloniaProperty.RegisterAttached("RowSpan", 1); + public static readonly AttachedProperty IsSharedSizeScopeProperty = + AvaloniaProperty.RegisterAttached("IsSharedSizeScope", false); + + private sealed class SharedSizeScopeHost : IDisposable + { + private class GridMeasureCache + { + public Grid Grid { get; } + public DefinitionBase Definition { get; } + public double CachedLength { get; set; } + } + + private readonly AvaloniaList _participatingGrids; + + private Dictionary _cachedSize = new Dictionary(); + + private Dictionary> _gridsInScopes = new Dictionary>(); + + private Dictionary> _scopeCache; + private int _leftToMeasure; + + public SharedSizeScopeHost(Control scope) + { + _participatingGrids = GetParticipatingGrids(scope); + + foreach (var grid in _participatingGrids) + { + grid.InvalidateMeasure(); + AddGridToScopes(grid); + } + } + + private bool _invalidating = false; + + internal void InvalidateMeasure(Grid grid) + { + if (_invalidating) + return; + _invalidating = true; + + List candidates = new List {grid}; + while (candidates.Any()) + { + var scopes = candidates.SelectMany(c => c.RowDefinitions.Select(rd => rd.SharedSizeGroup)) + .Concat(candidates.SelectMany(c => c.ColumnDefinitions.Select(rd => rd.SharedSizeGroup))).Distinct(); + + candidates = scopes.SelectMany(r => _scopeCache[r].Select(gmc => gmc.Grid)) + .Distinct().Where(c => c.IsMeasureValid).ToList(); + candidates.ForEach(c => c.InvalidateMeasure()); + } + + _invalidating = false; + } + + private void AddGridToScopes(Grid grid) + { + var scopeNames = grid.ColumnDefinitions.Select(g => g.SharedSizeGroup) + .Concat(grid.RowDefinitions.Select(g => g.SharedSizeGroup)).Distinct(); + foreach (var scopeName in scopeNames) + { + if (!_gridsInScopes.TryGetValue(scopeName, out var list)) + _gridsInScopes.Add(scopeName, list = new List() ); + list.Add(grid); + } + } + + private void RemoveGridFromScopes(Grid grid) + { + var scopeNames = grid.ColumnDefinitions.Select(g => g.SharedSizeGroup) + .Concat(grid.RowDefinitions.Select(g => g.SharedSizeGroup)).Distinct(); + foreach (var scopeName in scopeNames) + { + Debug.Assert(_gridsInScopes.TryGetValue(scopeName, out var list)); + list.Remove(grid); + if (!list.Any()) + _gridsInScopes.Remove(scopeName); + } + } + + internal void UpdateMeasureResult(GridLayout.MeasureResult result, ColumnDefinitions columnDefinitions) + { + for (var i = 0; i < columnDefinitions.Count; i++) + { + if (string.IsNullOrEmpty(columnDefinitions[i].SharedSizeGroup)) + continue; + // if any in this group is Absolute we don't care about measured values. + + } + } + + internal void UpdateMeasureResult(GridLayout.MeasureResult result, RowDefinitions rowDefinitions) + { + + } + + internal double GetExistingLimit(DefinitionBase definition) + { + List cache = _scopeCache[definition.SharedSizeGroup]; + + return cache.Where(gmc => gmc.Grid.IsMeasureValid) + .Aggregate(double.NaN, (a, gmc) => Math.Max(a, gmc.CachedLength)); + } + + internal void UpdateExistingLimit(DefinitionBase definition, double limit) + { + List cache = _scopeCache[definition.SharedSizeGroup]; + + cache.Single(gmc => ReferenceEquals(gmc.Definition, definition)).CachedLength = limit; + // if any other are lower - invalidate the grid. + } + + internal void BeginMeasurePass() + { + if (_leftToMeasure == 0) + { + _leftToMeasure = _participatingGrids.Count(g => !g.IsMeasureValid); + } + } + + private static AvaloniaList GetParticipatingGrids(Control scope) + { + var result = scope.GetVisualDescendants().OfType(); + + return new AvaloniaList(result.Where(g => g.HasSharedSizeGroups())); + } + + public void Dispose() + { + foreach (var grid in _participatingGrids) + { + grid.SharedScopeChanged(); + } + } + + internal void RegisterGrid(Grid toAdd) + { + Debug.Assert(!_participatingGrids.Contains(toAdd)); + _participatingGrids.Add(toAdd); + AddGridToScopes(toAdd); + } + + internal void UnegisterGrid(Grid toRemove) + { + Debug.Assert(_participatingGrids.Contains(toRemove)); + _participatingGrids.Remove(toRemove); + RemoveGridFromScopes(toRemove); + } + } + + protected override void OnMeasureInvalidated() + { + base.OnMeasureInvalidated(); + _sharedSizeHost?.InvalidateMeasure(this); + } + + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + var scope = this.GetVisualAncestors().OfType() + .FirstOrDefault(c => c.GetValue(IsSharedSizeScopeProperty)); + + Debug.Assert(_sharedSizeHost == null); + + if (scope != null) + { + _sharedSizeHost = scope.GetValue(s_sharedSizeScopeHostProperty); + _sharedSizeHost.RegisterGrid(this); + } + } + + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnDetachedFromVisualTree(e); + + _sharedSizeHost?.UnegisterGrid(this); + _sharedSizeHost = null; + } + + private SharedSizeScopeHost _sharedSizeHost; + + private static readonly AttachedProperty s_sharedSizeScopeHostProperty = + AvaloniaProperty.RegisterAttached("&&SharedSizeScopeHost", null); + private ColumnDefinitions _columnDefinitions; private RowDefinitions _rowDefinitions; @@ -51,6 +236,23 @@ namespace Avalonia.Controls static Grid() { AffectsParentMeasure(ColumnProperty, ColumnSpanProperty, RowProperty, RowSpanProperty); + IsSharedSizeScopeProperty.Changed.AddClassHandler(IsSharedSizeScopeChanged); + } + + private static void IsSharedSizeScopeChanged(Control source, AvaloniaPropertyChangedEventArgs arg2) + { + if ((bool)arg2.NewValue) + { + Debug.Assert(source.GetValue(s_sharedSizeScopeHostProperty) == null); + source.SetValue(IsSharedSizeScopeProperty, new SharedSizeScopeHost(source)); + } + else + { + var host = source.GetValue(s_sharedSizeScopeHostProperty) as SharedSizeScopeHost; + Debug.Assert(host != null); + host.Dispose(); + source.SetValue(IsSharedSizeScopeProperty, null); + } } /// @@ -426,5 +628,26 @@ namespace Avalonia.Controls return value; } + + internal bool HasSharedSizeGroups() + { + return ColumnDefinitions.Any(cd => !string.IsNullOrEmpty(cd.SharedSizeGroup)) || + RowDefinitions.Any(rd => !string.IsNullOrEmpty(rd.SharedSizeGroup)); + } + + internal void SharedScopeChanged() + { + _sharedSizeHost = null; + var scope = this.GetVisualAncestors().OfType() + .FirstOrDefault(c => c.GetValue(IsSharedSizeScopeProperty)); + + if (scope != null) + { + _sharedSizeHost = scope.GetValue(s_sharedSizeScopeHostProperty); + _sharedSizeHost.RegisterGrid(this); + } + + InvalidateMeasure(); + } } } From 2754649edd05acf11cf7d15544a2bc3f79b20dc3 Mon Sep 17 00:00:00 2001 From: wojciech krysiak Date: Wed, 3 Oct 2018 07:49:07 +0200 Subject: [PATCH 11/70] Moar mess --- src/Avalonia.Controls/Grid.cs | 267 +++++++++++++++------- src/Avalonia.Controls/Utils/GridLayout.cs | 6 +- 2 files changed, 190 insertions(+), 83 deletions(-) diff --git a/src/Avalonia.Controls/Grid.cs b/src/Avalonia.Controls/Grid.cs index 71ae414a4f..5dc90414ee 100644 --- a/src/Avalonia.Controls/Grid.cs +++ b/src/Avalonia.Controls/Grid.cs @@ -51,147 +51,249 @@ namespace Avalonia.Controls private sealed class SharedSizeScopeHost : IDisposable { - private class GridMeasureCache + private enum MeasurementState { - public Grid Grid { get; } - public DefinitionBase Definition { get; } - public double CachedLength { get; set; } + Invalidated, + Measuring, + Cached } - private readonly AvaloniaList _participatingGrids; + private class MeasurementCache + { + public MeasurementCache(Grid grid) + { + Grid = grid; + Results = grid.RowDefinitions.Cast() + .Concat(grid.ColumnDefinitions) + .Select(d => new MeasurementResult(d)) + .ToList(); + } - private Dictionary _cachedSize = new Dictionary(); + public void UpdateMeasureResult(GridLayout.MeasureResult rowResult, GridLayout.MeasureResult columnResult) + { + RowResult = rowResult; + ColumnResult = columnResult; + MeasurementState = MeasurementState.Cached; + for (int i = 0; i < rowResult.LengthList.Count; i++) + { + Results[i].MeasuredResult = rowResult.LengthList[i]; + } + + for (int i = 0; i < columnResult.LengthList.Count; i++) + { + Results[i + rowResult.LengthList.Count].MeasuredResult = columnResult.LengthList[i]; + } + } - private Dictionary> _gridsInScopes = new Dictionary>(); + public void InvalidateMeasure() + { + MeasurementState = MeasurementState.Invalidated; + Results.ForEach(r => r.MeasuredResult = double.NaN); + } + + public Grid Grid { get; } + public GridLayout.MeasureResult RowResult { get; private set; } + public GridLayout.MeasureResult ColumnResult { get; private set; } + public MeasurementState MeasurementState { get; private set; } - private Dictionary> _scopeCache; - private int _leftToMeasure; + public List Results { get; } + } - public SharedSizeScopeHost(Control scope) + private readonly AvaloniaList _measurementCaches; + + private class MeasurementResult { - _participatingGrids = GetParticipatingGrids(scope); - - foreach (var grid in _participatingGrids) + public MeasurementResult(DefinitionBase @base) { - grid.InvalidateMeasure(); - AddGridToScopes(grid); + Definition = @base; + MeasuredResult = double.NaN; } + + public DefinitionBase Definition { get; } + public double MeasuredResult { get; set; } } - private bool _invalidating = false; + private enum ScopeType + { + Auto, + Fixed + } - internal void InvalidateMeasure(Grid grid) + private class Group { - if (_invalidating) - return; - _invalidating = true; + public bool IsFixed { get; set; } - List candidates = new List {grid}; - while (candidates.Any()) - { - var scopes = candidates.SelectMany(c => c.RowDefinitions.Select(rd => rd.SharedSizeGroup)) - .Concat(candidates.SelectMany(c => c.ColumnDefinitions.Select(rd => rd.SharedSizeGroup))).Distinct(); - - candidates = scopes.SelectMany(r => _scopeCache[r].Select(gmc => gmc.Grid)) - .Distinct().Where(c => c.IsMeasureValid).ToList(); - candidates.ForEach(c => c.InvalidateMeasure()); - } + public List Results { get; } - _invalidating = false; + public double CalculatedLength { get; } } - private void AddGridToScopes(Grid grid) + private Dictionary _groups = new Dictionary(); + + + public SharedSizeScopeHost(Control scope) { - var scopeNames = grid.ColumnDefinitions.Select(g => g.SharedSizeGroup) - .Concat(grid.RowDefinitions.Select(g => g.SharedSizeGroup)).Distinct(); - foreach (var scopeName in scopeNames) + _measurementCaches = GetParticipatingGrids(scope); + + foreach (var cache in _measurementCaches) { - if (!_gridsInScopes.TryGetValue(scopeName, out var list)) - _gridsInScopes.Add(scopeName, list = new List() ); - list.Add(grid); + cache.Grid.InvalidateMeasure(); + AddGridToScopes(cache); } } - private void RemoveGridFromScopes(Grid grid) + internal void InvalidateMeasure(Grid grid) { - var scopeNames = grid.ColumnDefinitions.Select(g => g.SharedSizeGroup) - .Concat(grid.RowDefinitions.Select(g => g.SharedSizeGroup)).Distinct(); - foreach (var scopeName in scopeNames) - { - Debug.Assert(_gridsInScopes.TryGetValue(scopeName, out var list)); - list.Remove(grid); - if (!list.Any()) - _gridsInScopes.Remove(scopeName); - } + var cache = _measurementCaches.FirstOrDefault(mc => ReferenceEquals(mc.Grid, grid)); + Debug.Assert(cache != null); + + cache.InvalidateMeasure(); } - internal void UpdateMeasureResult(GridLayout.MeasureResult result, ColumnDefinitions columnDefinitions) + internal void UpdateMeasureStatus(Grid grid, GridLayout.MeasureResult rowResult, GridLayout.MeasureResult columnResult) { - for (var i = 0; i < columnDefinitions.Count; i++) + var cache = _measurementCaches.FirstOrDefault(mc => ReferenceEquals(mc.Grid, grid)); + Debug.Assert(cache != null); + + cache.UpdateMeasureResult(rowResult, columnResult); + } + + internal (GridLayout.MeasureResult, GridLayout.MeasureResult) HandleArrange(Grid grid, GridLayout.MeasureResult rowResult, GridLayout.MeasureResult columnResult) + { + var rowConventions = rowResult.LeanLengthList.ToList(); + var rowLengths = rowResult.LengthList.ToList(); + var rowDesiredLength = 0.0; + for (int i = 0; i < grid.RowDefinitions.Count; i++) { - if (string.IsNullOrEmpty(columnDefinitions[i].SharedSizeGroup)) + var definition = grid.RowDefinitions[i]; + if (string.IsNullOrEmpty(definition.SharedSizeGroup)) + { + rowDesiredLength += rowResult.LengthList[i]; continue; - // if any in this group is Absolute we don't care about measured values. - + } + + var group = _groups[definition.SharedSizeGroup]; + + var length = group.Results.Max(g => g.MeasuredResult); + rowConventions[i] = new GridLayout.LengthConvention( + new GridLength(length), + rowResult.LeanLengthList[i].MinLength, + rowResult.LeanLengthList[i].MaxLength + ); + rowLengths[i] = length; + rowDesiredLength += length; + } - } - internal void UpdateMeasureResult(GridLayout.MeasureResult result, RowDefinitions rowDefinitions) - { + var columnConventions = columnResult.LeanLengthList.ToList(); + var columnLengths = columnResult.LengthList.ToList(); + var columnDesiredLength = 0.0; + for (int i = 0; i < grid.ColumnDefinitions.Count; i++) + { + var definition = grid.ColumnDefinitions[i]; + if (string.IsNullOrEmpty(definition.SharedSizeGroup)) + { + columnDesiredLength += rowResult.LengthList[i]; + continue; + } + + var group = _groups[definition.SharedSizeGroup]; + + var length = group.Results.Max(g => g.MeasuredResult); + columnConventions[i] = new GridLayout.LengthConvention( + new GridLength(length), + columnResult.LeanLengthList[i].MinLength, + columnResult.LeanLengthList[i].MaxLength + ); + columnLengths[i] = length; + columnDesiredLength += length; + } + return ( + new GridLayout.MeasureResult( + rowResult.ContainerLength, + rowDesiredLength, + rowResult.GreedyDesiredLength,//?? + rowConventions, + rowLengths), + new GridLayout.MeasureResult( + columnResult.ContainerLength, + columnDesiredLength, + columnResult.GreedyDesiredLength, //?? + columnConventions, + columnLengths) + ); } - internal double GetExistingLimit(DefinitionBase definition) + + private void AddGridToScopes(MeasurementCache cache) { - List cache = _scopeCache[definition.SharedSizeGroup]; + foreach (var result in cache.Results) + { + var scopeName = result.Definition.SharedSizeGroup; + if (!_groups.TryGetValue(scopeName, out var group)) + _groups.Add(scopeName, group = new Group()); + + group.IsFixed |= IsFixed(result.Definition); - return cache.Where(gmc => gmc.Grid.IsMeasureValid) - .Aggregate(double.NaN, (a, gmc) => Math.Max(a, gmc.CachedLength)); + group.Results.Add(result); + } } - internal void UpdateExistingLimit(DefinitionBase definition, double limit) + private bool IsFixed(DefinitionBase definition) { - List cache = _scopeCache[definition.SharedSizeGroup]; - - cache.Single(gmc => ReferenceEquals(gmc.Definition, definition)).CachedLength = limit; - // if any other are lower - invalidate the grid. + return ((definition as ColumnDefinition)?.Width ?? ((RowDefinition)definition).Height).IsAbsolute; } - internal void BeginMeasurePass() + private void RemoveGridFromScopes(MeasurementCache cache) { - if (_leftToMeasure == 0) + foreach (var result in cache.Results) { - _leftToMeasure = _participatingGrids.Count(g => !g.IsMeasureValid); + var scopeName = result.Definition.SharedSizeGroup; + Debug.Assert(_groups.TryGetValue(scopeName, out var group)); + + group.Results.Remove(result); + if (!group.Results.Any()) + _groups.Remove(scopeName); + else + { + group.IsFixed = group.Results.Select(r => r.Definition).Any(IsFixed); + } } } - private static AvaloniaList GetParticipatingGrids(Control scope) + private static AvaloniaList GetParticipatingGrids(Control scope) { var result = scope.GetVisualDescendants().OfType(); - return new AvaloniaList(result.Where(g => g.HasSharedSizeGroups())); + return new AvaloniaList( + result.Where(g => g.HasSharedSizeGroups()) + .Select(g => new MeasurementCache(g))); } public void Dispose() { - foreach (var grid in _participatingGrids) + foreach (var cache in _measurementCaches) { - grid.SharedScopeChanged(); + cache.Grid.SharedScopeChanged(); } } internal void RegisterGrid(Grid toAdd) { - Debug.Assert(!_participatingGrids.Contains(toAdd)); - _participatingGrids.Add(toAdd); - AddGridToScopes(toAdd); + Debug.Assert(!_measurementCaches.Any(mc => ReferenceEquals(mc.Grid,toAdd))); + var cache = new MeasurementCache(toAdd); + _measurementCaches.Add(cache); + AddGridToScopes(cache); } internal void UnegisterGrid(Grid toRemove) { - Debug.Assert(_participatingGrids.Contains(toRemove)); - _participatingGrids.Remove(toRemove); - RemoveGridFromScopes(toRemove); + var cache = _measurementCaches.FirstOrDefault(mc => ReferenceEquals(mc.Grid, toRemove)); + + Debug.Assert(cache != null); + _measurementCaches.Remove(cache); + RemoveGridFromScopes(cache); } } @@ -473,6 +575,8 @@ namespace Avalonia.Controls _rowLayoutCache = rowLayout; _columnLayoutCache = columnLayout; + _sharedSizeHost?.UpdateMeasureStatus(this, rowResult, columnResult); + return new Size(columnResult.DesiredLength, rowResult.DesiredLength); // Measure each child only once. @@ -521,9 +625,12 @@ namespace Avalonia.Controls var (safeColumns, safeRows) = GetSafeColumnRows(); var columnLayout = _columnLayoutCache; var rowLayout = _rowLayoutCache; + + var (rowCache, columnCache) = _sharedSizeHost?.HandleArrange(this, _rowMeasureCache, _columnMeasureCache) ?? (_rowMeasureCache, _columnMeasureCache); + // Calculate for arrange result. - var columnResult = columnLayout.Arrange(finalSize.Width, _columnMeasureCache); - var rowResult = rowLayout.Arrange(finalSize.Height, _rowMeasureCache); + var columnResult = columnLayout.Arrange(finalSize.Width, rowCache); + var rowResult = rowLayout.Arrange(finalSize.Height, columnCache); // Arrange the children. foreach (var child in Children.OfType()) { diff --git a/src/Avalonia.Controls/Utils/GridLayout.cs b/src/Avalonia.Controls/Utils/GridLayout.cs index 363428b289..b1dca09be2 100644 --- a/src/Avalonia.Controls/Utils/GridLayout.cs +++ b/src/Avalonia.Controls/Utils/GridLayout.cs @@ -147,10 +147,10 @@ namespace Avalonia.Controls.Utils /// The measured result that containing the desired size and all the column/row lengths. /// [NotNull, Pure] - internal MeasureResult Measure(double containerLength) + internal MeasureResult Measure(double containerLength, IReadOnlyList conventions = null) { // Prepare all the variables that this method needs to use. - var conventions = _conventions.Select(x => x.Clone()).ToList(); + conventions = conventions ?? _conventions.Select(x => x.Clone()).ToList(); var starCount = conventions.Where(x => x.Length.IsStar).Sum(x => x.Length.Value); var aggregatedLength = 0.0; double starUnitLength; @@ -306,7 +306,7 @@ namespace Avalonia.Controls.Utils if (finalLength - measure.ContainerLength > LayoutTolerance) { // If the final length is larger, we will rerun the whole measure. - measure = Measure(finalLength); + measure = Measure(finalLength, measure.LeanLengthList); } else if (finalLength - measure.ContainerLength < -LayoutTolerance) { From 5741d0ef140f3da153a8803dd7d1aefb94e1c82c Mon Sep 17 00:00:00 2001 From: wojciech krysiak Date: Wed, 3 Oct 2018 16:49:56 +0200 Subject: [PATCH 12/70] Base case works --- src/Avalonia.Controls/Grid.cs | 136 ++++++++++++++++++++-------------- 1 file changed, 82 insertions(+), 54 deletions(-) diff --git a/src/Avalonia.Controls/Grid.cs b/src/Avalonia.Controls/Grid.cs index 5dc90414ee..d89c2947ce 100644 --- a/src/Avalonia.Controls/Grid.cs +++ b/src/Avalonia.Controls/Grid.cs @@ -60,6 +60,7 @@ namespace Avalonia.Controls private class MeasurementCache { + public MeasurementCache(Grid grid) { Grid = grid; @@ -67,19 +68,20 @@ namespace Avalonia.Controls .Concat(grid.ColumnDefinitions) .Select(d => new MeasurementResult(d)) .ToList(); + + grid.RowDefinitions. + } public void UpdateMeasureResult(GridLayout.MeasureResult rowResult, GridLayout.MeasureResult columnResult) { - RowResult = rowResult; - ColumnResult = columnResult; MeasurementState = MeasurementState.Cached; - for (int i = 0; i < rowResult.LengthList.Count; i++) + for (int i = 0; i < Grid.RowDefinitions.Count; i++) { Results[i].MeasuredResult = rowResult.LengthList[i]; } - for (int i = 0; i < columnResult.LengthList.Count; i++) + for (int i = 0; i < Grid.ColumnDefinitions.Count; i++) { Results[i + rowResult.LengthList.Count].MeasuredResult = columnResult.LengthList[i]; } @@ -92,8 +94,6 @@ namespace Avalonia.Controls } public Grid Grid { get; } - public GridLayout.MeasureResult RowResult { get; private set; } - public GridLayout.MeasureResult ColumnResult { get; private set; } public MeasurementState MeasurementState { get; private set; } public List Results { get; } @@ -103,9 +103,9 @@ namespace Avalonia.Controls private class MeasurementResult { - public MeasurementResult(DefinitionBase @base) + public MeasurementResult(DefinitionBase definition) { - Definition = @base; + Definition = definition; MeasuredResult = double.NaN; } @@ -113,22 +113,16 @@ namespace Avalonia.Controls public double MeasuredResult { get; set; } } - private enum ScopeType - { - Auto, - Fixed - } - private class Group { public bool IsFixed { get; set; } - public List Results { get; } + public List Results { get; } = new List(); public double CalculatedLength { get; } } - private Dictionary _groups = new Dictionary(); + private readonly Dictionary _groups = new Dictionary(); public SharedSizeScopeHost(Control scope) @@ -158,57 +152,79 @@ namespace Avalonia.Controls cache.UpdateMeasureResult(rowResult, columnResult); } - internal (GridLayout.MeasureResult, GridLayout.MeasureResult) HandleArrange(Grid grid, GridLayout.MeasureResult rowResult, GridLayout.MeasureResult columnResult) + private double Gather(IEnumerable measurements) { - var rowConventions = rowResult.LeanLengthList.ToList(); - var rowLengths = rowResult.LengthList.ToList(); - var rowDesiredLength = 0.0; - for (int i = 0; i < grid.RowDefinitions.Count; i++) + var result = 0.0d; + + bool onlyFixed = false; + + foreach (var measurement in measurements) { - var definition = grid.RowDefinitions[i]; - if (string.IsNullOrEmpty(definition.SharedSizeGroup)) + if (measurement.Definition is ColumnDefinition column) { - rowDesiredLength += rowResult.LengthList[i]; - continue; + if (!onlyFixed && column.Width.IsAbsolute) + { + onlyFixed = true; + result = measurement.MeasuredResult; + } + else if (onlyFixed == column.Width.IsAbsolute) + result = Math.Max(result, measurement.MeasuredResult); + + result = Math.Max(result, column.MinWidth); } + if (measurement.Definition is RowDefinition row) + { + if (!onlyFixed && row.Height.IsAbsolute) + { + onlyFixed = true; + result = measurement.MeasuredResult; + } + else if (onlyFixed == row.Height.IsAbsolute) + result = Math.Max(result, measurement.MeasuredResult); + + result = Math.Max(result, row.MinHeight); + } + } - var group = _groups[definition.SharedSizeGroup]; - - var length = group.Results.Max(g => g.MeasuredResult); - rowConventions[i] = new GridLayout.LengthConvention( - new GridLength(length), - rowResult.LeanLengthList[i].MinLength, - rowResult.LeanLengthList[i].MaxLength - ); - rowLengths[i] = length; - rowDesiredLength += length; + return result; + } - } - var columnConventions = columnResult.LeanLengthList.ToList(); - var columnLengths = columnResult.LengthList.ToList(); - var columnDesiredLength = 0.0; - for (int i = 0; i < grid.ColumnDefinitions.Count; i++) + (List, List, double) Arrange(IReadOnlyList definitions, GridLayout.MeasureResult measureResult) + { + var conventions = measureResult.LeanLengthList.ToList(); + var lengths = measureResult.LengthList.ToList(); + var desiredLength = 0.0; + for (int i = 0; i < definitions.Count; i++) { - var definition = grid.ColumnDefinitions[i]; + var definition = definitions[i]; if (string.IsNullOrEmpty(definition.SharedSizeGroup)) { - columnDesiredLength += rowResult.LengthList[i]; + desiredLength += measureResult.LengthList[i]; continue; } var group = _groups[definition.SharedSizeGroup]; - var length = group.Results.Max(g => g.MeasuredResult); - columnConventions[i] = new GridLayout.LengthConvention( + var length = Gather(group.Results); + + conventions[i] = new GridLayout.LengthConvention( new GridLength(length), - columnResult.LeanLengthList[i].MinLength, - columnResult.LeanLengthList[i].MaxLength - ); - columnLengths[i] = length; - columnDesiredLength += length; + measureResult.LeanLengthList[i].MinLength, + measureResult.LeanLengthList[i].MaxLength + ); + lengths[i] = length; + desiredLength += length; } + return (conventions, lengths, desiredLength); + } + + internal (GridLayout.MeasureResult, GridLayout.MeasureResult) HandleArrange(Grid grid, GridLayout.MeasureResult rowResult, GridLayout.MeasureResult columnResult) + { + var (rowConventions, rowLengths, rowDesiredLength) = Arrange(grid.RowDefinitions, rowResult); + var (columnConventions, columnLengths, columnDesiredLength) = Arrange(grid.ColumnDefinitions, columnResult); + return ( new GridLayout.MeasureResult( rowResult.ContainerLength, @@ -231,6 +247,8 @@ namespace Avalonia.Controls foreach (var result in cache.Results) { var scopeName = result.Definition.SharedSizeGroup; + if (string.IsNullOrEmpty(scopeName)) + continue; if (!_groups.TryGetValue(scopeName, out var group)) _groups.Add(scopeName, group = new Group()); @@ -250,6 +268,8 @@ namespace Avalonia.Controls foreach (var result in cache.Results) { var scopeName = result.Definition.SharedSizeGroup; + if (string.IsNullOrEmpty(scopeName)) + continue; Debug.Assert(_groups.TryGetValue(scopeName, out var group)); group.Results.Remove(result); @@ -346,14 +366,14 @@ namespace Avalonia.Controls if ((bool)arg2.NewValue) { Debug.Assert(source.GetValue(s_sharedSizeScopeHostProperty) == null); - source.SetValue(IsSharedSizeScopeProperty, new SharedSizeScopeHost(source)); + source.SetValue(s_sharedSizeScopeHostProperty, new SharedSizeScopeHost(source)); } else { var host = source.GetValue(s_sharedSizeScopeHostProperty) as SharedSizeScopeHost; Debug.Assert(host != null); host.Dispose(); - source.SetValue(IsSharedSizeScopeProperty, null); + source.SetValue(s_sharedSizeScopeHostProperty, null); } } @@ -626,11 +646,19 @@ namespace Avalonia.Controls var columnLayout = _columnLayoutCache; var rowLayout = _rowLayoutCache; - var (rowCache, columnCache) = _sharedSizeHost?.HandleArrange(this, _rowMeasureCache, _columnMeasureCache) ?? (_rowMeasureCache, _columnMeasureCache); + var (rowCache, columnCache) = + _sharedSizeHost?.HandleArrange(this, _rowMeasureCache, _columnMeasureCache) ?? + (_rowMeasureCache, _columnMeasureCache); + + if (_sharedSizeHost != null) + { + rowCache = rowLayout.Measure(finalSize.Width, rowCache.LeanLengthList); + columnCache = columnLayout.Measure(finalSize.Width, columnCache.LeanLengthList); + } // Calculate for arrange result. - var columnResult = columnLayout.Arrange(finalSize.Width, rowCache); - var rowResult = rowLayout.Arrange(finalSize.Height, columnCache); + var columnResult = columnLayout.Arrange(finalSize.Width, columnCache); + var rowResult = rowLayout.Arrange(finalSize.Height, rowCache); // Arrange the children. foreach (var child in Children.OfType()) { From f876f14afd99812a738067a3a2e6fa5bcde39f8c Mon Sep 17 00:00:00 2001 From: wojciech krysiak Date: Thu, 4 Oct 2018 07:00:50 +0200 Subject: [PATCH 13/70] Suporting changes to the rows/columns --- src/Avalonia.Controls/Grid.cs | 164 +++++++++++++++++++++++++++++----- 1 file changed, 144 insertions(+), 20 deletions(-) diff --git a/src/Avalonia.Controls/Grid.cs b/src/Avalonia.Controls/Grid.cs index d89c2947ce..05255920a5 100644 --- a/src/Avalonia.Controls/Grid.cs +++ b/src/Avalonia.Controls/Grid.cs @@ -3,8 +3,13 @@ using System; using System.Collections.Generic; +using System.Collections.Specialized; +using System.ComponentModel; using System.Diagnostics; using System.Linq; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using System.Reactive.Subjects; using System.Runtime.CompilerServices; using Avalonia.Collections; using Avalonia.Controls.Utils; @@ -58,8 +63,13 @@ namespace Avalonia.Controls Cached } - private class MeasurementCache + private sealed class MeasurementCache : IDisposable { + CompositeDisposable _subscriptions; + + Subject<(string, string, MeasurementResult)> _groupChanged = new Subject<(string, string, MeasurementResult)>(); + + public ISubject<(string oldName, string newName, MeasurementResult result)> GroupChanged => _groupChanged; public MeasurementCache(Grid grid) { @@ -69,8 +79,96 @@ namespace Avalonia.Controls .Select(d => new MeasurementResult(d)) .ToList(); - grid.RowDefinitions. + grid.RowDefinitions.CollectionChanged += DefinitionsCollectionChanged; + grid.ColumnDefinitions.CollectionChanged += DefinitionsCollectionChanged; + + _subscriptions = new CompositeDisposable( + Disposable.Create(() => grid.RowDefinitions.CollectionChanged -= DefinitionsCollectionChanged), + Disposable.Create(() => grid.ColumnDefinitions.CollectionChanged -= DefinitionsCollectionChanged), + grid.RowDefinitions.TrackItemPropertyChanged(DefinitionPropertyChanged), + grid.ColumnDefinitions.TrackItemPropertyChanged(DefinitionPropertyChanged)); + + } + + private void DefinitionPropertyChanged(Tuple propertyChanged) + { + if (propertyChanged.Item2.PropertyName == nameof(DefinitionBase.SharedSizeGroup)) + { + var oldName = string.Empty; + var newName = (propertyChanged.Item1 as DefinitionBase).SharedSizeGroup; + var result = Results.Single(mr => ReferenceEquals(mr.Definition, propertyChanged.Item2)); + _groupChanged.OnNext((oldName, newName, result)); + } + } + + private void DefinitionsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + int offset = 0; + if (sender is ColumnDefinitions) + offset = Grid.RowDefinitions.Count; + + var newItems = e.NewItems?.OfType().Select(db => new MeasurementResult(db)).ToList() ?? new List(); + var oldItems = Results.GetRange(e.OldStartingIndex + offset, e.OldItems?.Count ?? 0); + + void NotifyNewItems() + { + foreach (var item in newItems) + { + if (string.IsNullOrEmpty(item.Definition.SharedSizeGroup)) + continue; + + _groupChanged.OnNext((null, item.Definition.SharedSizeGroup, item)); + } + } + + void NotifyOldItems() + { + foreach (var item in oldItems) + { + if (string.IsNullOrEmpty(item.Definition.SharedSizeGroup)) + continue; + + _groupChanged.OnNext((item.Definition.SharedSizeGroup, null, item)); + } + } + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + Results.InsertRange(e.NewStartingIndex + offset, newItems); + NotifyNewItems(); + break; + + case NotifyCollectionChangedAction.Remove: + Results.RemoveRange(e.OldStartingIndex + offset, oldItems.Count); + NotifyOldItems(); + break; + + case NotifyCollectionChangedAction.Move: + Results.RemoveRange(e.OldStartingIndex + offset, oldItems.Count); + Results.InsertRange(e.NewStartingIndex + offset, oldItems); + break; + + case NotifyCollectionChangedAction.Replace: + Results.RemoveRange(e.OldStartingIndex + offset, oldItems.Count); + Results.InsertRange(e.NewStartingIndex + offset, newItems); + + NotifyOldItems(); + NotifyNewItems(); + + break; + + case NotifyCollectionChangedAction.Reset: + oldItems = Results; + newItems = Results = Grid.RowDefinitions.Cast() + .Concat(Grid.ColumnDefinitions) + .Select(d => new MeasurementResult(d)) + .ToList(); + NotifyOldItems(); + NotifyNewItems(); + + break; + } } public void UpdateMeasureResult(GridLayout.MeasureResult rowResult, GridLayout.MeasureResult columnResult) @@ -93,10 +191,16 @@ namespace Avalonia.Controls Results.ForEach(r => r.MeasuredResult = double.NaN); } + public void Dispose() + { + _subscriptions.Dispose(); + _groupChanged.OnCompleted(); + } + public Grid Grid { get; } public MeasurementState MeasurementState { get; private set; } - public List Results { get; } + public List Results { get; private set; } } private readonly AvaloniaList _measurementCaches; @@ -133,9 +237,17 @@ namespace Avalonia.Controls { cache.Grid.InvalidateMeasure(); AddGridToScopes(cache); + + cache.GroupChanged.Subscribe(SharedGroupChanged); } } + void SharedGroupChanged((string oldName, string newName, MeasurementResult result) change) + { + RemoveFromGroup(change.oldName, change.result); + AddToGroup(change.newName, change.result); + } + internal void InvalidateMeasure(Grid grid) { var cache = _measurementCaches.FirstOrDefault(mc => ReferenceEquals(mc.Grid, grid)); @@ -247,15 +359,21 @@ namespace Avalonia.Controls foreach (var result in cache.Results) { var scopeName = result.Definition.SharedSizeGroup; - if (string.IsNullOrEmpty(scopeName)) - continue; - if (!_groups.TryGetValue(scopeName, out var group)) - _groups.Add(scopeName, group = new Group()); + AddToGroup(scopeName, result); + } + } - group.IsFixed |= IsFixed(result.Definition); + private void AddToGroup(string scopeName, MeasurementResult result) + { + if (string.IsNullOrEmpty(scopeName)) + return; - group.Results.Add(result); - } + if (!_groups.TryGetValue(scopeName, out var group)) + _groups.Add(scopeName, group = new Group()); + + group.IsFixed |= IsFixed(result.Definition); + + group.Results.Add(result); } private bool IsFixed(DefinitionBase definition) @@ -268,17 +386,23 @@ namespace Avalonia.Controls foreach (var result in cache.Results) { var scopeName = result.Definition.SharedSizeGroup; - if (string.IsNullOrEmpty(scopeName)) - continue; - Debug.Assert(_groups.TryGetValue(scopeName, out var group)); + RemoveFromGroup(scopeName, result); + } + } - group.Results.Remove(result); - if (!group.Results.Any()) - _groups.Remove(scopeName); - else - { - group.IsFixed = group.Results.Select(r => r.Definition).Any(IsFixed); - } + private void RemoveFromGroup(string scopeName, MeasurementResult result) + { + if (string.IsNullOrEmpty(scopeName)) + return; + + Debug.Assert(_groups.TryGetValue(scopeName, out var group)); + + group.Results.Remove(result); + if (!group.Results.Any()) + _groups.Remove(scopeName); + else + { + group.IsFixed = group.Results.Select(r => r.Definition).Any(IsFixed); } } From fe499cea89e0a81834a40fb931fe448f0df0a418 Mon Sep 17 00:00:00 2001 From: wojciech krysiak Date: Thu, 4 Oct 2018 13:04:42 +0200 Subject: [PATCH 14/70] Pre PR commit --- src/Avalonia.Controls/Grid.cs | 391 ----------------- .../Utils/SharedSizeScopeHost.cs | 400 ++++++++++++++++++ 2 files changed, 400 insertions(+), 391 deletions(-) create mode 100644 src/Avalonia.Controls/Utils/SharedSizeScopeHost.cs diff --git a/src/Avalonia.Controls/Grid.cs b/src/Avalonia.Controls/Grid.cs index 05255920a5..264695d3d7 100644 --- a/src/Avalonia.Controls/Grid.cs +++ b/src/Avalonia.Controls/Grid.cs @@ -3,13 +3,9 @@ using System; using System.Collections.Generic; -using System.Collections.Specialized; -using System.ComponentModel; using System.Diagnostics; using System.Linq; -using System.Reactive.Disposables; using System.Reactive.Linq; -using System.Reactive.Subjects; using System.Runtime.CompilerServices; using Avalonia.Collections; using Avalonia.Controls.Utils; @@ -54,393 +50,6 @@ namespace Avalonia.Controls public static readonly AttachedProperty IsSharedSizeScopeProperty = AvaloniaProperty.RegisterAttached("IsSharedSizeScope", false); - private sealed class SharedSizeScopeHost : IDisposable - { - private enum MeasurementState - { - Invalidated, - Measuring, - Cached - } - - private sealed class MeasurementCache : IDisposable - { - CompositeDisposable _subscriptions; - - Subject<(string, string, MeasurementResult)> _groupChanged = new Subject<(string, string, MeasurementResult)>(); - - public ISubject<(string oldName, string newName, MeasurementResult result)> GroupChanged => _groupChanged; - - public MeasurementCache(Grid grid) - { - Grid = grid; - Results = grid.RowDefinitions.Cast() - .Concat(grid.ColumnDefinitions) - .Select(d => new MeasurementResult(d)) - .ToList(); - - grid.RowDefinitions.CollectionChanged += DefinitionsCollectionChanged; - grid.ColumnDefinitions.CollectionChanged += DefinitionsCollectionChanged; - - _subscriptions = new CompositeDisposable( - Disposable.Create(() => grid.RowDefinitions.CollectionChanged -= DefinitionsCollectionChanged), - Disposable.Create(() => grid.ColumnDefinitions.CollectionChanged -= DefinitionsCollectionChanged), - grid.RowDefinitions.TrackItemPropertyChanged(DefinitionPropertyChanged), - grid.ColumnDefinitions.TrackItemPropertyChanged(DefinitionPropertyChanged)); - - } - - private void DefinitionPropertyChanged(Tuple propertyChanged) - { - if (propertyChanged.Item2.PropertyName == nameof(DefinitionBase.SharedSizeGroup)) - { - var oldName = string.Empty; - var newName = (propertyChanged.Item1 as DefinitionBase).SharedSizeGroup; - var result = Results.Single(mr => ReferenceEquals(mr.Definition, propertyChanged.Item2)); - _groupChanged.OnNext((oldName, newName, result)); - } - } - - private void DefinitionsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) - { - int offset = 0; - if (sender is ColumnDefinitions) - offset = Grid.RowDefinitions.Count; - - var newItems = e.NewItems?.OfType().Select(db => new MeasurementResult(db)).ToList() ?? new List(); - var oldItems = Results.GetRange(e.OldStartingIndex + offset, e.OldItems?.Count ?? 0); - - void NotifyNewItems() - { - foreach (var item in newItems) - { - if (string.IsNullOrEmpty(item.Definition.SharedSizeGroup)) - continue; - - _groupChanged.OnNext((null, item.Definition.SharedSizeGroup, item)); - } - } - - void NotifyOldItems() - { - foreach (var item in oldItems) - { - if (string.IsNullOrEmpty(item.Definition.SharedSizeGroup)) - continue; - - _groupChanged.OnNext((item.Definition.SharedSizeGroup, null, item)); - } - } - - switch (e.Action) - { - case NotifyCollectionChangedAction.Add: - Results.InsertRange(e.NewStartingIndex + offset, newItems); - NotifyNewItems(); - break; - - case NotifyCollectionChangedAction.Remove: - Results.RemoveRange(e.OldStartingIndex + offset, oldItems.Count); - NotifyOldItems(); - break; - - case NotifyCollectionChangedAction.Move: - Results.RemoveRange(e.OldStartingIndex + offset, oldItems.Count); - Results.InsertRange(e.NewStartingIndex + offset, oldItems); - break; - - case NotifyCollectionChangedAction.Replace: - Results.RemoveRange(e.OldStartingIndex + offset, oldItems.Count); - Results.InsertRange(e.NewStartingIndex + offset, newItems); - - NotifyOldItems(); - NotifyNewItems(); - - break; - - case NotifyCollectionChangedAction.Reset: - oldItems = Results; - newItems = Results = Grid.RowDefinitions.Cast() - .Concat(Grid.ColumnDefinitions) - .Select(d => new MeasurementResult(d)) - .ToList(); - NotifyOldItems(); - NotifyNewItems(); - - break; - } - } - - public void UpdateMeasureResult(GridLayout.MeasureResult rowResult, GridLayout.MeasureResult columnResult) - { - MeasurementState = MeasurementState.Cached; - for (int i = 0; i < Grid.RowDefinitions.Count; i++) - { - Results[i].MeasuredResult = rowResult.LengthList[i]; - } - - for (int i = 0; i < Grid.ColumnDefinitions.Count; i++) - { - Results[i + rowResult.LengthList.Count].MeasuredResult = columnResult.LengthList[i]; - } - } - - public void InvalidateMeasure() - { - MeasurementState = MeasurementState.Invalidated; - Results.ForEach(r => r.MeasuredResult = double.NaN); - } - - public void Dispose() - { - _subscriptions.Dispose(); - _groupChanged.OnCompleted(); - } - - public Grid Grid { get; } - public MeasurementState MeasurementState { get; private set; } - - public List Results { get; private set; } - } - - private readonly AvaloniaList _measurementCaches; - - private class MeasurementResult - { - public MeasurementResult(DefinitionBase definition) - { - Definition = definition; - MeasuredResult = double.NaN; - } - - public DefinitionBase Definition { get; } - public double MeasuredResult { get; set; } - } - - private class Group - { - public bool IsFixed { get; set; } - - public List Results { get; } = new List(); - - public double CalculatedLength { get; } - } - - private readonly Dictionary _groups = new Dictionary(); - - - public SharedSizeScopeHost(Control scope) - { - _measurementCaches = GetParticipatingGrids(scope); - - foreach (var cache in _measurementCaches) - { - cache.Grid.InvalidateMeasure(); - AddGridToScopes(cache); - - cache.GroupChanged.Subscribe(SharedGroupChanged); - } - } - - void SharedGroupChanged((string oldName, string newName, MeasurementResult result) change) - { - RemoveFromGroup(change.oldName, change.result); - AddToGroup(change.newName, change.result); - } - - internal void InvalidateMeasure(Grid grid) - { - var cache = _measurementCaches.FirstOrDefault(mc => ReferenceEquals(mc.Grid, grid)); - Debug.Assert(cache != null); - - cache.InvalidateMeasure(); - } - - internal void UpdateMeasureStatus(Grid grid, GridLayout.MeasureResult rowResult, GridLayout.MeasureResult columnResult) - { - var cache = _measurementCaches.FirstOrDefault(mc => ReferenceEquals(mc.Grid, grid)); - Debug.Assert(cache != null); - - cache.UpdateMeasureResult(rowResult, columnResult); - } - - private double Gather(IEnumerable measurements) - { - var result = 0.0d; - - bool onlyFixed = false; - - foreach (var measurement in measurements) - { - if (measurement.Definition is ColumnDefinition column) - { - if (!onlyFixed && column.Width.IsAbsolute) - { - onlyFixed = true; - result = measurement.MeasuredResult; - } - else if (onlyFixed == column.Width.IsAbsolute) - result = Math.Max(result, measurement.MeasuredResult); - - result = Math.Max(result, column.MinWidth); - } - if (measurement.Definition is RowDefinition row) - { - if (!onlyFixed && row.Height.IsAbsolute) - { - onlyFixed = true; - result = measurement.MeasuredResult; - } - else if (onlyFixed == row.Height.IsAbsolute) - result = Math.Max(result, measurement.MeasuredResult); - - result = Math.Max(result, row.MinHeight); - } - } - - return result; - } - - - (List, List, double) Arrange(IReadOnlyList definitions, GridLayout.MeasureResult measureResult) - { - var conventions = measureResult.LeanLengthList.ToList(); - var lengths = measureResult.LengthList.ToList(); - var desiredLength = 0.0; - for (int i = 0; i < definitions.Count; i++) - { - var definition = definitions[i]; - if (string.IsNullOrEmpty(definition.SharedSizeGroup)) - { - desiredLength += measureResult.LengthList[i]; - continue; - } - - var group = _groups[definition.SharedSizeGroup]; - - var length = Gather(group.Results); - - conventions[i] = new GridLayout.LengthConvention( - new GridLength(length), - measureResult.LeanLengthList[i].MinLength, - measureResult.LeanLengthList[i].MaxLength - ); - lengths[i] = length; - desiredLength += length; - } - - return (conventions, lengths, desiredLength); - } - - internal (GridLayout.MeasureResult, GridLayout.MeasureResult) HandleArrange(Grid grid, GridLayout.MeasureResult rowResult, GridLayout.MeasureResult columnResult) - { - var (rowConventions, rowLengths, rowDesiredLength) = Arrange(grid.RowDefinitions, rowResult); - var (columnConventions, columnLengths, columnDesiredLength) = Arrange(grid.ColumnDefinitions, columnResult); - - return ( - new GridLayout.MeasureResult( - rowResult.ContainerLength, - rowDesiredLength, - rowResult.GreedyDesiredLength,//?? - rowConventions, - rowLengths), - new GridLayout.MeasureResult( - columnResult.ContainerLength, - columnDesiredLength, - columnResult.GreedyDesiredLength, //?? - columnConventions, - columnLengths) - ); - } - - - private void AddGridToScopes(MeasurementCache cache) - { - foreach (var result in cache.Results) - { - var scopeName = result.Definition.SharedSizeGroup; - AddToGroup(scopeName, result); - } - } - - private void AddToGroup(string scopeName, MeasurementResult result) - { - if (string.IsNullOrEmpty(scopeName)) - return; - - if (!_groups.TryGetValue(scopeName, out var group)) - _groups.Add(scopeName, group = new Group()); - - group.IsFixed |= IsFixed(result.Definition); - - group.Results.Add(result); - } - - private bool IsFixed(DefinitionBase definition) - { - return ((definition as ColumnDefinition)?.Width ?? ((RowDefinition)definition).Height).IsAbsolute; - } - - private void RemoveGridFromScopes(MeasurementCache cache) - { - foreach (var result in cache.Results) - { - var scopeName = result.Definition.SharedSizeGroup; - RemoveFromGroup(scopeName, result); - } - } - - private void RemoveFromGroup(string scopeName, MeasurementResult result) - { - if (string.IsNullOrEmpty(scopeName)) - return; - - Debug.Assert(_groups.TryGetValue(scopeName, out var group)); - - group.Results.Remove(result); - if (!group.Results.Any()) - _groups.Remove(scopeName); - else - { - group.IsFixed = group.Results.Select(r => r.Definition).Any(IsFixed); - } - } - - private static AvaloniaList GetParticipatingGrids(Control scope) - { - var result = scope.GetVisualDescendants().OfType(); - - return new AvaloniaList( - result.Where(g => g.HasSharedSizeGroups()) - .Select(g => new MeasurementCache(g))); - } - - public void Dispose() - { - foreach (var cache in _measurementCaches) - { - cache.Grid.SharedScopeChanged(); - } - } - - internal void RegisterGrid(Grid toAdd) - { - Debug.Assert(!_measurementCaches.Any(mc => ReferenceEquals(mc.Grid,toAdd))); - var cache = new MeasurementCache(toAdd); - _measurementCaches.Add(cache); - AddGridToScopes(cache); - } - - internal void UnegisterGrid(Grid toRemove) - { - var cache = _measurementCaches.FirstOrDefault(mc => ReferenceEquals(mc.Grid, toRemove)); - - Debug.Assert(cache != null); - _measurementCaches.Remove(cache); - RemoveGridFromScopes(cache); - } - } - protected override void OnMeasureInvalidated() { base.OnMeasureInvalidated(); diff --git a/src/Avalonia.Controls/Utils/SharedSizeScopeHost.cs b/src/Avalonia.Controls/Utils/SharedSizeScopeHost.cs new file mode 100644 index 0000000000..a0e2137934 --- /dev/null +++ b/src/Avalonia.Controls/Utils/SharedSizeScopeHost.cs @@ -0,0 +1,400 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.ComponentModel; +using System.Diagnostics; +using System.Linq; +using System.Reactive.Disposables; +using System.Reactive.Subjects; +using Avalonia.Collections; +using Avalonia.Controls.Utils; +using Avalonia.VisualTree; + +namespace Avalonia.Controls +{ + internal sealed class SharedSizeScopeHost : IDisposable + { + private enum MeasurementState + { + Invalidated, + Measuring, + Cached + } + + private sealed class MeasurementCache : IDisposable + { + readonly CompositeDisposable _subscriptions; + readonly Subject<(string, string, MeasurementResult)> _groupChanged = new Subject<(string, string, MeasurementResult)>(); + + public ISubject<(string oldName, string newName, MeasurementResult result)> GroupChanged => _groupChanged; + + public MeasurementCache(Grid grid) + { + Grid = grid; + Results = grid.RowDefinitions.Cast() + .Concat(grid.ColumnDefinitions) + .Select(d => new MeasurementResult(d)) + .ToList(); + + grid.RowDefinitions.CollectionChanged += DefinitionsCollectionChanged; + grid.ColumnDefinitions.CollectionChanged += DefinitionsCollectionChanged; + + _subscriptions = new CompositeDisposable( + Disposable.Create(() => grid.RowDefinitions.CollectionChanged -= DefinitionsCollectionChanged), + Disposable.Create(() => grid.ColumnDefinitions.CollectionChanged -= DefinitionsCollectionChanged), + grid.RowDefinitions.TrackItemPropertyChanged(DefinitionPropertyChanged), + grid.ColumnDefinitions.TrackItemPropertyChanged(DefinitionPropertyChanged)); + + } + + private void DefinitionPropertyChanged(Tuple propertyChanged) + { + if (propertyChanged.Item2.PropertyName == nameof(DefinitionBase.SharedSizeGroup)) + { + var oldName = string.Empty; // TODO: find how to determine the old name + var newName = (propertyChanged.Item1 as DefinitionBase).SharedSizeGroup; + var result = Results.Single(mr => ReferenceEquals(mr.Definition, propertyChanged.Item1)); + _groupChanged.OnNext((oldName, newName, result)); + } + } + + private void DefinitionsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + int offset = 0; + if (sender is ColumnDefinitions) + offset = Grid.RowDefinitions.Count; + + var newItems = e.NewItems?.OfType().Select(db => new MeasurementResult(db)).ToList() ?? new List(); + var oldItems = Results.GetRange(e.OldStartingIndex + offset, e.OldItems?.Count ?? 0); + + void NotifyNewItems() + { + foreach (var item in newItems) + { + if (string.IsNullOrEmpty(item.Definition.SharedSizeGroup)) + continue; + + _groupChanged.OnNext((null, item.Definition.SharedSizeGroup, item)); + } + } + + void NotifyOldItems() + { + foreach (var item in oldItems) + { + if (string.IsNullOrEmpty(item.Definition.SharedSizeGroup)) + continue; + + _groupChanged.OnNext((item.Definition.SharedSizeGroup, null, item)); + } + } + + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + Results.InsertRange(e.NewStartingIndex + offset, newItems); + NotifyNewItems(); + break; + + case NotifyCollectionChangedAction.Remove: + Results.RemoveRange(e.OldStartingIndex + offset, oldItems.Count); + NotifyOldItems(); + break; + + case NotifyCollectionChangedAction.Move: + Results.RemoveRange(e.OldStartingIndex + offset, oldItems.Count); + Results.InsertRange(e.NewStartingIndex + offset, oldItems); + break; + + case NotifyCollectionChangedAction.Replace: + Results.RemoveRange(e.OldStartingIndex + offset, oldItems.Count); + Results.InsertRange(e.NewStartingIndex + offset, newItems); + + NotifyOldItems(); + NotifyNewItems(); + + break; + + case NotifyCollectionChangedAction.Reset: + oldItems = Results; + newItems = Results = Grid.RowDefinitions.Cast() + .Concat(Grid.ColumnDefinitions) + .Select(d => new MeasurementResult(d)) + .ToList(); + NotifyOldItems(); + NotifyNewItems(); + + break; + } + } + + public void UpdateMeasureResult(GridLayout.MeasureResult rowResult, GridLayout.MeasureResult columnResult) + { + MeasurementState = MeasurementState.Cached; + for (int i = 0; i < Grid.RowDefinitions.Count; i++) + { + Results[i].MeasuredResult = rowResult.LengthList[i]; + } + + for (int i = 0; i < Grid.ColumnDefinitions.Count; i++) + { + Results[i + rowResult.LengthList.Count].MeasuredResult = columnResult.LengthList[i]; + } + } + + public void InvalidateMeasure() + { + MeasurementState = MeasurementState.Invalidated; + Results.ForEach(r => r.MeasuredResult = double.NaN); + } + + public void Dispose() + { + _subscriptions.Dispose(); + _groupChanged.OnCompleted(); + } + + public Grid Grid { get; } + public MeasurementState MeasurementState { get; private set; } + + public List Results { get; private set; } + } + + private readonly AvaloniaList _measurementCaches; + + private class MeasurementResult + { + public MeasurementResult(DefinitionBase definition) + { + Definition = definition; + MeasuredResult = double.NaN; + } + + public DefinitionBase Definition { get; } + public double MeasuredResult { get; set; } + } + + private class Group + { + public bool IsFixed { get; set; } + + public List Results { get; } = new List(); + + public double CalculatedLength { get; } + } + + private readonly Dictionary _groups = new Dictionary(); + + + public SharedSizeScopeHost(Control scope) + { + _measurementCaches = GetParticipatingGrids(scope); + + foreach (var cache in _measurementCaches) + { + cache.Grid.InvalidateMeasure(); + AddGridToScopes(cache); + + cache.GroupChanged.Subscribe(SharedGroupChanged); + } + } + + void SharedGroupChanged((string oldName, string newName, MeasurementResult result) change) + { + RemoveFromGroup(change.oldName, change.result); + AddToGroup(change.newName, change.result); + } + + internal void InvalidateMeasure(Grid grid) + { + var cache = _measurementCaches.FirstOrDefault(mc => ReferenceEquals(mc.Grid, grid)); + Debug.Assert(cache != null); + + cache.InvalidateMeasure(); + } + + internal void UpdateMeasureStatus(Grid grid, GridLayout.MeasureResult rowResult, GridLayout.MeasureResult columnResult) + { + var cache = _measurementCaches.FirstOrDefault(mc => ReferenceEquals(mc.Grid, grid)); + Debug.Assert(cache != null); + + cache.UpdateMeasureResult(rowResult, columnResult); + } + + private double Gather(IEnumerable measurements) + { + var result = 0.0d; + + bool onlyFixed = false; + + foreach (var measurement in measurements) + { + if (measurement.Definition is ColumnDefinition column) + { + if (!onlyFixed && column.Width.IsAbsolute) + { + onlyFixed = true; + result = measurement.MeasuredResult; + } + else if (onlyFixed == column.Width.IsAbsolute) + result = Math.Max(result, measurement.MeasuredResult); + + result = Math.Max(result, column.MinWidth); + } + if (measurement.Definition is RowDefinition row) + { + if (!onlyFixed && row.Height.IsAbsolute) + { + onlyFixed = true; + result = measurement.MeasuredResult; + } + else if (onlyFixed == row.Height.IsAbsolute) + result = Math.Max(result, measurement.MeasuredResult); + + result = Math.Max(result, row.MinHeight); + } + } + + return result; + } + + + (List, List, double) Arrange(IReadOnlyList definitions, GridLayout.MeasureResult measureResult) + { + var conventions = measureResult.LeanLengthList.ToList(); + var lengths = measureResult.LengthList.ToList(); + var desiredLength = 0.0; + for (int i = 0; i < definitions.Count; i++) + { + var definition = definitions[i]; + if (string.IsNullOrEmpty(definition.SharedSizeGroup)) + { + desiredLength += measureResult.LengthList[i]; + continue; + } + + var group = _groups[definition.SharedSizeGroup]; + + var length = Gather(group.Results); + + conventions[i] = new GridLayout.LengthConvention( + new GridLength(length), + measureResult.LeanLengthList[i].MinLength, + measureResult.LeanLengthList[i].MaxLength + ); + lengths[i] = length; + desiredLength += length; + } + + return (conventions, lengths, desiredLength); + } + + internal (GridLayout.MeasureResult, GridLayout.MeasureResult) HandleArrange(Grid grid, GridLayout.MeasureResult rowResult, GridLayout.MeasureResult columnResult) + { + var (rowConventions, rowLengths, rowDesiredLength) = Arrange(grid.RowDefinitions, rowResult); + var (columnConventions, columnLengths, columnDesiredLength) = Arrange(grid.ColumnDefinitions, columnResult); + + return ( + new GridLayout.MeasureResult( + rowResult.ContainerLength, + rowDesiredLength, + rowResult.GreedyDesiredLength,//?? + rowConventions, + rowLengths), + new GridLayout.MeasureResult( + columnResult.ContainerLength, + columnDesiredLength, + columnResult.GreedyDesiredLength, //?? + columnConventions, + columnLengths) + ); + } + + + private void AddGridToScopes(MeasurementCache cache) + { + foreach (var result in cache.Results) + { + var scopeName = result.Definition.SharedSizeGroup; + AddToGroup(scopeName, result); + } + } + + private void AddToGroup(string scopeName, MeasurementResult result) + { + if (string.IsNullOrEmpty(scopeName)) + return; + + if (!_groups.TryGetValue(scopeName, out var group)) + _groups.Add(scopeName, group = new Group()); + + group.IsFixed |= IsFixed(result.Definition); + + group.Results.Add(result); + } + + private bool IsFixed(DefinitionBase definition) + { + return ((definition as ColumnDefinition)?.Width ?? ((RowDefinition)definition).Height).IsAbsolute; + } + + private void RemoveGridFromScopes(MeasurementCache cache) + { + foreach (var result in cache.Results) + { + var scopeName = result.Definition.SharedSizeGroup; + RemoveFromGroup(scopeName, result); + } + } + + private void RemoveFromGroup(string scopeName, MeasurementResult result) + { + if (string.IsNullOrEmpty(scopeName)) + return; + + Debug.Assert(_groups.TryGetValue(scopeName, out var group)); + + group.Results.Remove(result); + if (!group.Results.Any()) + _groups.Remove(scopeName); + else + { + group.IsFixed = group.Results.Select(r => r.Definition).Any(IsFixed); + } + } + + private static AvaloniaList GetParticipatingGrids(Control scope) + { + var result = scope.GetVisualDescendants().OfType(); + + return new AvaloniaList( + result.Where(g => g.HasSharedSizeGroups()) + .Select(g => new MeasurementCache(g))); + } + + public void Dispose() + { + foreach (var cache in _measurementCaches) + { + cache.Grid.SharedScopeChanged(); + } + } + + internal void RegisterGrid(Grid toAdd) + { + Debug.Assert(!_measurementCaches.Any(mc => ReferenceEquals(mc.Grid, toAdd))); + var cache = new MeasurementCache(toAdd); + _measurementCaches.Add(cache); + AddGridToScopes(cache); + } + + internal void UnegisterGrid(Grid toRemove) + { + var cache = _measurementCaches.FirstOrDefault(mc => ReferenceEquals(mc.Grid, toRemove)); + + Debug.Assert(cache != null); + _measurementCaches.Remove(cache); + RemoveGridFromScopes(cache); + } + } +} From 4b02fb375f098052f833034bf416c998290e52ea Mon Sep 17 00:00:00 2001 From: wojciech krysiak Date: Sat, 6 Oct 2018 10:00:32 +0200 Subject: [PATCH 15/70] Corrected found issues --- .../Utils/SharedSizeScopeHost.cs | 194 +++++++++++++----- 1 file changed, 140 insertions(+), 54 deletions(-) diff --git a/src/Avalonia.Controls/Utils/SharedSizeScopeHost.cs b/src/Avalonia.Controls/Utils/SharedSizeScopeHost.cs index a0e2137934..25d6d7f6b8 100644 --- a/src/Avalonia.Controls/Utils/SharedSizeScopeHost.cs +++ b/src/Avalonia.Controls/Utils/SharedSizeScopeHost.cs @@ -8,6 +8,7 @@ using System.Reactive.Disposables; using System.Reactive.Subjects; using Avalonia.Collections; using Avalonia.Controls.Utils; +using Avalonia.Layout; using Avalonia.VisualTree; namespace Avalonia.Controls @@ -33,7 +34,7 @@ namespace Avalonia.Controls Grid = grid; Results = grid.RowDefinitions.Cast() .Concat(grid.ColumnDefinitions) - .Select(d => new MeasurementResult(d)) + .Select(d => new MeasurementResult(grid, d)) .ToList(); grid.RowDefinitions.CollectionChanged += DefinitionsCollectionChanged; @@ -51,9 +52,9 @@ namespace Avalonia.Controls { if (propertyChanged.Item2.PropertyName == nameof(DefinitionBase.SharedSizeGroup)) { - var oldName = string.Empty; // TODO: find how to determine the old name - var newName = (propertyChanged.Item1 as DefinitionBase).SharedSizeGroup; var result = Results.Single(mr => ReferenceEquals(mr.Definition, propertyChanged.Item1)); + var oldName = result.SizeGroup?.Name; + var newName = (propertyChanged.Item1 as DefinitionBase).SharedSizeGroup; _groupChanged.OnNext((oldName, newName, result)); } } @@ -64,7 +65,7 @@ namespace Avalonia.Controls if (sender is ColumnDefinitions) offset = Grid.RowDefinitions.Count; - var newItems = e.NewItems?.OfType().Select(db => new MeasurementResult(db)).ToList() ?? new List(); + var newItems = e.NewItems?.OfType().Select(db => new MeasurementResult(Grid, db)).ToList() ?? new List(); var oldItems = Results.GetRange(e.OldStartingIndex + offset, e.OldItems?.Count ?? 0); void NotifyNewItems() @@ -119,7 +120,7 @@ namespace Avalonia.Controls oldItems = Results; newItems = Results = Grid.RowDefinitions.Cast() .Concat(Grid.ColumnDefinitions) - .Select(d => new MeasurementResult(d)) + .Select(d => new MeasurementResult(Grid, d)) .ToList(); NotifyOldItems(); NotifyNewItems(); @@ -145,7 +146,11 @@ namespace Avalonia.Controls public void InvalidateMeasure() { MeasurementState = MeasurementState.Invalidated; - Results.ForEach(r => r.MeasuredResult = double.NaN); + Results.ForEach(r => + { + r.MeasuredResult = double.NaN; + r.SizeGroup?.Reset(); + }); } public void Dispose() @@ -160,31 +165,107 @@ namespace Avalonia.Controls public List Results { get; private set; } } - private readonly AvaloniaList _measurementCaches; - private class MeasurementResult { - public MeasurementResult(DefinitionBase definition) + public MeasurementResult(Grid owningGrid, DefinitionBase definition) { + OwningGrid = owningGrid; Definition = definition; MeasuredResult = double.NaN; } public DefinitionBase Definition { get; } public double MeasuredResult { get; set; } + public Group SizeGroup { get; set; } + public Grid OwningGrid { get; } } + private class Group { + private double? cachedResult; + private List _results = new List(); + + public string Name { get; } + + public Group(string name) + { + Name = name; + } + public bool IsFixed { get; set; } - public List Results { get; } = new List(); + public IReadOnlyList Results => _results; + + public double CalculatedLength => (cachedResult ?? (cachedResult = Gather())).Value; + + public void Reset() + { + cachedResult = null; + } + + public void Add(MeasurementResult result) + { + if (!_results.Contains(result)) + throw new AvaloniaInternalException( + $"Invalid call to Group.Add - The SharedSizeGroup {Name} already contains the passed result"); + + result.SizeGroup = this; + _results.Add(result); + } + + public void Remove(MeasurementResult result) + { + if (!_results.Contains(result)) + throw new AvaloniaInternalException( + $"Invalid call to Group.Remove - The SharedSizeGroup {Name} does not contain the passed result"); + result.SizeGroup = null; + _results.Remove(result); + } + + + private double Gather() + { + var result = 0.0d; + + bool onlyFixed = false; + + foreach (var measurement in Results) + { + if (measurement.Definition is ColumnDefinition column) + { + if (!onlyFixed && column.Width.IsAbsolute) + { + onlyFixed = true; + result = measurement.MeasuredResult; + } + else if (onlyFixed == column.Width.IsAbsolute) + result = Math.Max(result, measurement.MeasuredResult); + + result = Math.Max(result, column.MinWidth); + } + if (measurement.Definition is RowDefinition row) + { + if (!onlyFixed && row.Height.IsAbsolute) + { + onlyFixed = true; + result = measurement.MeasuredResult; + } + else if (onlyFixed == row.Height.IsAbsolute) + result = Math.Max(result, measurement.MeasuredResult); + + result = Math.Max(result, row.MinHeight); + } + } + + return result; + } - public double CalculatedLength { get; } } - private readonly Dictionary _groups = new Dictionary(); + private readonly AvaloniaList _measurementCaches; + private readonly Dictionary _groups = new Dictionary(); public SharedSizeScopeHost(Control scope) { @@ -205,59 +286,62 @@ namespace Avalonia.Controls AddToGroup(change.newName, change.result); } + private bool _invalidating; + internal void InvalidateMeasure(Grid grid) { - var cache = _measurementCaches.FirstOrDefault(mc => ReferenceEquals(mc.Grid, grid)); - Debug.Assert(cache != null); + // prevent stack overflow + if (_invalidating) + return; + _invalidating = true; - cache.InvalidateMeasure(); + InvalidateMeasureImpl(grid); + + _invalidating = false; } - internal void UpdateMeasureStatus(Grid grid, GridLayout.MeasureResult rowResult, GridLayout.MeasureResult columnResult) + private void InvalidateMeasureImpl(Grid grid) { var cache = _measurementCaches.FirstOrDefault(mc => ReferenceEquals(mc.Grid, grid)); - Debug.Assert(cache != null); - cache.UpdateMeasureResult(rowResult, columnResult); - } + if (cache == null) + throw new AvaloniaInternalException( + $"InvalidateMeasureImpl - called with a grid not present in the internal cache"); - private double Gather(IEnumerable measurements) - { - var result = 0.0d; + // already invalidated the cache, early out. + if (cache.MeasurementState == MeasurementState.Invalidated) + return; - bool onlyFixed = false; + cache.InvalidateMeasure(); - foreach (var measurement in measurements) + // maybe there is a condition to only call arrange on some of the calls? + grid.InvalidateMeasure(); + + // find all the scopes within the invalidated grid + var scopeNames = cache.Results + .Where(mr => mr.SizeGroup != null) + .Select(mr => mr.SizeGroup.Name) + .Distinct(); + // find all grids related to those scopes + var otherGrids = scopeNames.SelectMany(sn => _groups[sn].Results) + .Select(r => r.OwningGrid) + .Where(g => g.IsMeasureValid) + .Distinct(); + + // invalidate them as well + foreach (var otherGrid in otherGrids) { - if (measurement.Definition is ColumnDefinition column) - { - if (!onlyFixed && column.Width.IsAbsolute) - { - onlyFixed = true; - result = measurement.MeasuredResult; - } - else if (onlyFixed == column.Width.IsAbsolute) - result = Math.Max(result, measurement.MeasuredResult); - - result = Math.Max(result, column.MinWidth); - } - if (measurement.Definition is RowDefinition row) - { - if (!onlyFixed && row.Height.IsAbsolute) - { - onlyFixed = true; - result = measurement.MeasuredResult; - } - else if (onlyFixed == row.Height.IsAbsolute) - result = Math.Max(result, measurement.MeasuredResult); - - result = Math.Max(result, row.MinHeight); - } + InvalidateMeasureImpl(otherGrid); } - - return result; } + internal void UpdateMeasureStatus(Grid grid, GridLayout.MeasureResult rowResult, GridLayout.MeasureResult columnResult) + { + var cache = _measurementCaches.FirstOrDefault(mc => ReferenceEquals(mc.Grid, grid)); + Debug.Assert(cache != null); + + cache.UpdateMeasureResult(rowResult, columnResult); + } (List, List, double) Arrange(IReadOnlyList definitions, GridLayout.MeasureResult measureResult) { @@ -275,7 +359,7 @@ namespace Avalonia.Controls var group = _groups[definition.SharedSizeGroup]; - var length = Gather(group.Results); + var length = group.CalculatedLength; conventions[i] = new GridLayout.LengthConvention( new GridLength(length), @@ -326,11 +410,11 @@ namespace Avalonia.Controls return; if (!_groups.TryGetValue(scopeName, out var group)) - _groups.Add(scopeName, group = new Group()); + _groups.Add(scopeName, group = new Group(scopeName)); group.IsFixed |= IsFixed(result.Definition); - group.Results.Add(result); + group.Add(result); } private bool IsFixed(DefinitionBase definition) @@ -354,7 +438,7 @@ namespace Avalonia.Controls Debug.Assert(_groups.TryGetValue(scopeName, out var group)); - group.Results.Remove(result); + group.Remove(result); if (!group.Results.Any()) _groups.Remove(scopeName); else @@ -377,6 +461,7 @@ namespace Avalonia.Controls foreach (var cache in _measurementCaches) { cache.Grid.SharedScopeChanged(); + cache.Dispose(); } } @@ -395,6 +480,7 @@ namespace Avalonia.Controls Debug.Assert(cache != null); _measurementCaches.Remove(cache); RemoveGridFromScopes(cache); + cache.Dispose(); } } } From 49fda7256818deccbcd8dd726b823afef48a178f Mon Sep 17 00:00:00 2001 From: wojciech krysiak Date: Sun, 7 Oct 2018 11:14:18 +0200 Subject: [PATCH 16/70] Some more changes + GridSplitter Fix --- src/Avalonia.Controls/GridSplitter.cs | 67 ++++++++++++++----- .../Utils/SharedSizeScopeHost.cs | 10 ++- 2 files changed, 57 insertions(+), 20 deletions(-) diff --git a/src/Avalonia.Controls/GridSplitter.cs b/src/Avalonia.Controls/GridSplitter.cs index 1e4c6f2c2a..4d38d7389e 100644 --- a/src/Avalonia.Controls/GridSplitter.cs +++ b/src/Avalonia.Controls/GridSplitter.cs @@ -49,21 +49,41 @@ namespace Avalonia.Controls double min; GetDeltaConstraints(out min, out max); delta = Math.Min(Math.Max(delta, min), max); - foreach (var definition in _definitions) + + var prevIsStar = IsStar(_prevDefinition); + var nextIsStar = IsStar(_nextDefinition); + + if (prevIsStar && nextIsStar) { - if (definition == _prevDefinition) - { - SetLengthInStars(_prevDefinition, GetActualLength(_prevDefinition) + delta); - } - else if (definition == _nextDefinition) - { - SetLengthInStars(_nextDefinition, GetActualLength(_nextDefinition) - delta); - } - else if (IsStar(definition)) + foreach (var definition in _definitions) { - SetLengthInStars(definition, GetActualLength(definition)); // same size but in stars. + if (definition == _prevDefinition) + { + SetLengthInStars(_prevDefinition, GetActualLength(_prevDefinition) + delta); + } + else if (definition == _nextDefinition) + { + SetLengthInStars(_nextDefinition, GetActualLength(_nextDefinition) - delta); + } + else if (IsStar(definition)) + { + SetLengthInStars(definition, GetActualLength(definition)); // same size but in stars. + } } } + else if (prevIsStar) + { + SetLength(_nextDefinition, GetActualLength(_nextDefinition) - delta); + } + else if (nextIsStar) + { + SetLength(_prevDefinition, GetActualLength(_prevDefinition) + delta); + } + else + { + SetLength(_prevDefinition, GetActualLength(_prevDefinition) + delta); + SetLength(_nextDefinition, GetActualLength(_nextDefinition) - delta); + } } private double GetActualLength(DefinitionBase definition) @@ -71,7 +91,7 @@ namespace Avalonia.Controls if (definition == null) return 0; var columnDefinition = definition as ColumnDefinition; - return columnDefinition?.ActualWidth ?? ((RowDefinition) definition).ActualHeight; + return columnDefinition?.ActualWidth ?? ((RowDefinition)definition).ActualHeight; } private double GetMinLength(DefinitionBase definition) @@ -79,7 +99,7 @@ namespace Avalonia.Controls if (definition == null) return 0; var columnDefinition = definition as ColumnDefinition; - return columnDefinition?.MinWidth ?? ((RowDefinition) definition).MinHeight; + return columnDefinition?.MinWidth ?? ((RowDefinition)definition).MinHeight; } private double GetMaxLength(DefinitionBase definition) @@ -87,13 +107,13 @@ namespace Avalonia.Controls if (definition == null) return 0; var columnDefinition = definition as ColumnDefinition; - return columnDefinition?.MaxWidth ?? ((RowDefinition) definition).MaxHeight; + return columnDefinition?.MaxWidth ?? ((RowDefinition)definition).MaxHeight; } private bool IsStar(DefinitionBase definition) { var columnDefinition = definition as ColumnDefinition; - return columnDefinition?.Width.IsStar ?? ((RowDefinition) definition).Height.IsStar; + return columnDefinition?.Width.IsStar ?? ((RowDefinition)definition).Height.IsStar; } private void SetLengthInStars(DefinitionBase definition, double value) @@ -105,7 +125,20 @@ namespace Avalonia.Controls } else { - ((RowDefinition) definition).Height = new GridLength(value, GridUnitType.Star); + ((RowDefinition)definition).Height = new GridLength(value, GridUnitType.Star); + } + } + + private void SetLength(DefinitionBase definition, double value) + { + var columnDefinition = definition as ColumnDefinition; + if (columnDefinition != null) + { + columnDefinition.Width = new GridLength(value); + } + else + { + ((RowDefinition)definition).Height = new GridLength(value); } } @@ -160,7 +193,7 @@ namespace Avalonia.Controls } if (_grid.Children.OfType() // Decision based on other controls in the same column .Where(c => Grid.GetColumn(c) == col) - .Any(c => c.GetType() != typeof (GridSplitter))) + .Any(c => c.GetType() != typeof(GridSplitter))) { return Orientation.Horizontal; } diff --git a/src/Avalonia.Controls/Utils/SharedSizeScopeHost.cs b/src/Avalonia.Controls/Utils/SharedSizeScopeHost.cs index 25d6d7f6b8..0ff024c9a6 100644 --- a/src/Avalonia.Controls/Utils/SharedSizeScopeHost.cs +++ b/src/Avalonia.Controls/Utils/SharedSizeScopeHost.cs @@ -139,7 +139,7 @@ namespace Avalonia.Controls for (int i = 0; i < Grid.ColumnDefinitions.Count; i++) { - Results[i + rowResult.LengthList.Count].MeasuredResult = columnResult.LengthList[i]; + Results[i + Grid.RowDefinitions.Count].MeasuredResult = columnResult.LengthList[i]; } } @@ -206,7 +206,7 @@ namespace Avalonia.Controls public void Add(MeasurementResult result) { - if (!_results.Contains(result)) + if (_results.Contains(result)) throw new AvaloniaInternalException( $"Invalid call to Group.Add - The SharedSizeGroup {Name} already contains the passed result"); @@ -232,6 +232,9 @@ namespace Avalonia.Controls foreach (var measurement in Results) { + if (Double.IsInfinity(measurement.MeasuredResult)) + continue; + if (measurement.Definition is ColumnDefinition column) { if (!onlyFixed && column.Width.IsAbsolute) @@ -276,7 +279,6 @@ namespace Avalonia.Controls cache.Grid.InvalidateMeasure(); AddGridToScopes(cache); - cache.GroupChanged.Subscribe(SharedGroupChanged); } } @@ -397,6 +399,8 @@ namespace Avalonia.Controls private void AddGridToScopes(MeasurementCache cache) { + cache.GroupChanged.Subscribe(SharedGroupChanged); + foreach (var result in cache.Results) { var scopeName = result.Definition.SharedSizeGroup; From 184967a62b8c2fd45e4c9ac7111c15c386f8e58f Mon Sep 17 00:00:00 2001 From: Wojciech Krysiak Date: Sun, 21 Oct 2018 22:01:35 +0200 Subject: [PATCH 17/70] Corrected review comments + fixed some issues --- src/Avalonia.Controls/Grid.cs | 90 +++++++++------ src/Avalonia.Controls/Utils/GridLayout.cs | 17 ++- .../Utils/SharedSizeScopeHost.cs | 109 +++++++++++------- 3 files changed, 130 insertions(+), 86 deletions(-) diff --git a/src/Avalonia.Controls/Grid.cs b/src/Avalonia.Controls/Grid.cs index 264695d3d7..7bda4140a3 100644 --- a/src/Avalonia.Controls/Grid.cs +++ b/src/Avalonia.Controls/Grid.cs @@ -56,31 +56,12 @@ namespace Avalonia.Controls _sharedSizeHost?.InvalidateMeasure(this); } - protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) - { - base.OnAttachedToVisualTree(e); - var scope = this.GetVisualAncestors().OfType() - .FirstOrDefault(c => c.GetValue(IsSharedSizeScopeProperty)); - - Debug.Assert(_sharedSizeHost == null); - - if (scope != null) - { - _sharedSizeHost = scope.GetValue(s_sharedSizeScopeHostProperty); - _sharedSizeHost.RegisterGrid(this); - } - } - - protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) - { - base.OnDetachedFromVisualTree(e); - - _sharedSizeHost?.UnegisterGrid(this); - _sharedSizeHost = null; - } - private SharedSizeScopeHost _sharedSizeHost; + /// + /// Defines the SharedSizeScopeHost private property. + /// The ampersands are used to make accessing the property via xaml inconvenient. + /// private static readonly AttachedProperty s_sharedSizeScopeHostProperty = AvaloniaProperty.RegisterAttached("&&SharedSizeScopeHost", null); @@ -94,20 +75,53 @@ namespace Avalonia.Controls IsSharedSizeScopeProperty.Changed.AddClassHandler(IsSharedSizeScopeChanged); } - private static void IsSharedSizeScopeChanged(Control source, AvaloniaPropertyChangedEventArgs arg2) + public Grid() + { + this.AttachedToVisualTree += Grid_AttachedToVisualTree; + this.DetachedFromVisualTree += Grid_DetachedFromVisualTree; + } + + private void Grid_AttachedToVisualTree(object sender, VisualTreeAttachmentEventArgs e) { - if ((bool)arg2.NewValue) + var scope = + new Control[] { this }.Concat(this.GetVisualAncestors().OfType()) + .FirstOrDefault(c => c.GetValue(IsSharedSizeScopeProperty)); + + if (_sharedSizeHost != null) + throw new AvaloniaInternalException("Shared size scope already present when attaching to visual tree!"); + + if (scope != null) { - Debug.Assert(source.GetValue(s_sharedSizeScopeHostProperty) == null); - source.SetValue(s_sharedSizeScopeHostProperty, new SharedSizeScopeHost(source)); + _sharedSizeHost = scope.GetValue(s_sharedSizeScopeHostProperty); + _sharedSizeHost.RegisterGrid(this); } - else + } + + private void Grid_DetachedFromVisualTree(object sender, VisualTreeAttachmentEventArgs e) + { + _sharedSizeHost?.UnegisterGrid(this); + _sharedSizeHost = null; + } + + private static void IsSharedSizeScopeChanged(Control source, AvaloniaPropertyChangedEventArgs arg2) + { + var shouldDispose = (arg2.OldValue is bool d) && d; + if (shouldDispose) { var host = source.GetValue(s_sharedSizeScopeHostProperty) as SharedSizeScopeHost; - Debug.Assert(host != null); + if (host == null) + throw new AvaloniaInternalException("SharedScopeHost wasn't set when IsSharedSizeScope was true!"); host.Dispose(); source.SetValue(s_sharedSizeScopeHostProperty, null); } + + var shouldAssign = (arg2.NewValue is bool a) && a; + if (shouldAssign) + { + if (source.GetValue(s_sharedSizeScopeHostProperty) != null) + throw new AvaloniaInternalException("SharedScopeHost was already set when IsSharedSizeScope is only now being set to true!"); + source.SetValue(s_sharedSizeScopeHostProperty, new SharedSizeScopeHost(source)); + } } /// @@ -328,7 +342,10 @@ namespace Avalonia.Controls _rowLayoutCache = rowLayout; _columnLayoutCache = columnLayout; - _sharedSizeHost?.UpdateMeasureStatus(this, rowResult, columnResult); + if (_sharedSizeHost?.ParticipatesInScope(this) ?? false) + { + _sharedSizeHost.UpdateMeasureStatus(this, rowResult, columnResult); + } return new Size(columnResult.DesiredLength, rowResult.DesiredLength); @@ -379,13 +396,14 @@ namespace Avalonia.Controls var columnLayout = _columnLayoutCache; var rowLayout = _rowLayoutCache; - var (rowCache, columnCache) = - _sharedSizeHost?.HandleArrange(this, _rowMeasureCache, _columnMeasureCache) ?? - (_rowMeasureCache, _columnMeasureCache); + var rowCache = _rowMeasureCache; + var columnCache = _columnMeasureCache; - if (_sharedSizeHost != null) - { - rowCache = rowLayout.Measure(finalSize.Width, rowCache.LeanLengthList); + if (_sharedSizeHost?.ParticipatesInScope(this) ?? false) + { + (rowCache, columnCache) = _sharedSizeHost.HandleArrange(this, _rowMeasureCache, _columnMeasureCache); + + rowCache = rowLayout.Measure(finalSize.Height, rowCache.LeanLengthList); columnCache = columnLayout.Measure(finalSize.Width, columnCache.LeanLengthList); } diff --git a/src/Avalonia.Controls/Utils/GridLayout.cs b/src/Avalonia.Controls/Utils/GridLayout.cs index b1dca09be2..7704228a4e 100644 --- a/src/Avalonia.Controls/Utils/GridLayout.cs +++ b/src/Avalonia.Controls/Utils/GridLayout.cs @@ -143,6 +143,9 @@ namespace Avalonia.Controls.Utils /// /// The container length. Usually, it is the constraint of the method. /// + /// + /// Overriding conventions that allows the algorithm to handle external inputa + /// /// /// The measured result that containing the desired size and all the column/row lengths. /// @@ -248,7 +251,7 @@ namespace Avalonia.Controls.Utils // | min | max | | | min | | min max | max | // |#des#| fix |#des#| fix | fix | fix | fix | #des# |#des#| - var desiredStarMin = AggregateAdditionalConventionsForStars(conventions); + var (minLengths, desiredStarMin) = AggregateAdditionalConventionsForStars(conventions); aggregatedLength += desiredStarMin; // M6/7. Determine the desired length of the grid for current container length. Its value is stored in desiredLength. @@ -282,7 +285,7 @@ namespace Avalonia.Controls.Utils // Returns the measuring result. return new MeasureResult(containerLength, desiredLength, greedyDesiredLength, - conventions, dynamicConvention); + conventions, dynamicConvention, minLengths); } /// @@ -313,7 +316,7 @@ namespace Avalonia.Controls.Utils // If the final length is smaller, we measure the M6/6 procedure only. var dynamicConvention = ExpandStars(measure.LeanLengthList, finalLength); measure = new MeasureResult(finalLength, measure.DesiredLength, measure.GreedyDesiredLength, - measure.LeanLengthList, dynamicConvention); + measure.LeanLengthList, dynamicConvention, measure.MinLengths); } return new ArrangeResult(measure.LengthList); @@ -370,7 +373,7 @@ namespace Avalonia.Controls.Utils /// All the conventions that have almost been fixed except the rest *. /// The total desired length of all the * length. [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)] - private double AggregateAdditionalConventionsForStars( + private (List, double) AggregateAdditionalConventionsForStars( IReadOnlyList conventions) { // 1. Determine all one-span column's desired widths or row's desired heights. @@ -403,7 +406,7 @@ namespace Avalonia.Controls.Utils lengthList[group.Key] = Math.Max(lengthList[group.Key], length > 0 ? length : 0); } - return lengthList.Sum() - fixedLength; + return (lengthList, lengthList.Sum() - fixedLength); } /// @@ -638,13 +641,14 @@ namespace Avalonia.Controls.Utils /// Initialize a new instance of . /// internal MeasureResult(double containerLength, double desiredLength, double greedyDesiredLength, - IReadOnlyList leanConventions, IReadOnlyList expandedConventions) + IReadOnlyList leanConventions, IReadOnlyList expandedConventions, IReadOnlyList minLengths) { ContainerLength = containerLength; DesiredLength = desiredLength; GreedyDesiredLength = greedyDesiredLength; LeanLengthList = leanConventions; LengthList = expandedConventions; + MinLengths = minLengths; } /// @@ -674,6 +678,7 @@ namespace Avalonia.Controls.Utils /// Gets the length list for each column/row. /// public IReadOnlyList LengthList { get; } + public IReadOnlyList MinLengths { get; } } /// diff --git a/src/Avalonia.Controls/Utils/SharedSizeScopeHost.cs b/src/Avalonia.Controls/Utils/SharedSizeScopeHost.cs index 0ff024c9a6..5948bd7f19 100644 --- a/src/Avalonia.Controls/Utils/SharedSizeScopeHost.cs +++ b/src/Avalonia.Controls/Utils/SharedSizeScopeHost.cs @@ -135,11 +135,13 @@ namespace Avalonia.Controls for (int i = 0; i < Grid.RowDefinitions.Count; i++) { Results[i].MeasuredResult = rowResult.LengthList[i]; + Results[i].MinLength = rowResult.MinLengths[i]; } for (int i = 0; i < Grid.ColumnDefinitions.Count; i++) { Results[i + Grid.RowDefinitions.Count].MeasuredResult = columnResult.LengthList[i]; + Results[i + Grid.RowDefinitions.Count].MinLength = columnResult.MinLengths[i]; } } @@ -176,8 +178,47 @@ namespace Avalonia.Controls public DefinitionBase Definition { get; } public double MeasuredResult { get; set; } + public double MinLength { get; set; } public Group SizeGroup { get; set; } public Grid OwningGrid { get; } + + public (double length, int priority) GetPriorityLength() + { + var length = (Definition as ColumnDefinition)?.Width ?? ((RowDefinition)Definition).Height; + + if (length.IsAbsolute) + return (MeasuredResult, 1); + if (length.IsAuto) + return (MeasuredResult, 2); + if (MinLength > 0) + return (MinLength, 3); + return (MeasuredResult, 4); + } + } + + + private class LentgthGatherer + { + public double Length { get; private set; } + private int gatheredPriority = 6; + + public void Visit(MeasurementResult result) + { + var (length, priority) = result.GetPriorityLength(); + + if (gatheredPriority < priority) + return; + + gatheredPriority = priority; + if (gatheredPriority == priority) + { + Length = Math.Max(length,Length); + } + else + { + Length = length; + } + } } @@ -208,7 +249,7 @@ namespace Avalonia.Controls { if (_results.Contains(result)) throw new AvaloniaInternalException( - $"Invalid call to Group.Add - The SharedSizeGroup {Name} already contains the passed result"); + $"SharedSizeScopeHost: Invalid call to Group.Add - The SharedSizeGroup {Name} already contains the passed result"); result.SizeGroup = this; _results.Add(result); @@ -218,7 +259,7 @@ namespace Avalonia.Controls { if (!_results.Contains(result)) throw new AvaloniaInternalException( - $"Invalid call to Group.Remove - The SharedSizeGroup {Name} does not contain the passed result"); + $"SharedSizeScopeHost: Invalid call to Group.Remove - The SharedSizeGroup {Name} does not contain the passed result"); result.SizeGroup = null; _results.Remove(result); } @@ -226,44 +267,12 @@ namespace Avalonia.Controls private double Gather() { - var result = 0.0d; + var visitor = new LentgthGatherer(); - bool onlyFixed = false; + _results.ForEach(visitor.Visit); - foreach (var measurement in Results) - { - if (Double.IsInfinity(measurement.MeasuredResult)) - continue; - - if (measurement.Definition is ColumnDefinition column) - { - if (!onlyFixed && column.Width.IsAbsolute) - { - onlyFixed = true; - result = measurement.MeasuredResult; - } - else if (onlyFixed == column.Width.IsAbsolute) - result = Math.Max(result, measurement.MeasuredResult); - - result = Math.Max(result, column.MinWidth); - } - if (measurement.Definition is RowDefinition row) - { - if (!onlyFixed && row.Height.IsAbsolute) - { - onlyFixed = true; - result = measurement.MeasuredResult; - } - else if (onlyFixed == row.Height.IsAbsolute) - result = Math.Max(result, measurement.MeasuredResult); - - result = Math.Max(result, row.MinHeight); - } - } - - return result; + return visitor.Length; } - } private readonly AvaloniaList _measurementCaches; @@ -308,7 +317,7 @@ namespace Avalonia.Controls if (cache == null) throw new AvaloniaInternalException( - $"InvalidateMeasureImpl - called with a grid not present in the internal cache"); + $"SharedSizeScopeHost: InvalidateMeasureImpl - called with a grid not present in the internal cache"); // already invalidated the cache, early out. if (cache.MeasurementState == MeasurementState.Invalidated) @@ -340,7 +349,8 @@ namespace Avalonia.Controls internal void UpdateMeasureStatus(Grid grid, GridLayout.MeasureResult rowResult, GridLayout.MeasureResult columnResult) { var cache = _measurementCaches.FirstOrDefault(mc => ReferenceEquals(mc.Grid, grid)); - Debug.Assert(cache != null); + if (cache == null) + throw new AvaloniaInternalException("SharedSizeScopeHost: Attempted to update measurement status for a grid that wasn't registered!"); cache.UpdateMeasureResult(rowResult, columnResult); } @@ -386,13 +396,15 @@ namespace Avalonia.Controls rowDesiredLength, rowResult.GreedyDesiredLength,//?? rowConventions, - rowLengths), + rowLengths, + rowResult.MinLengths), new GridLayout.MeasureResult( columnResult.ContainerLength, columnDesiredLength, columnResult.GreedyDesiredLength, //?? columnConventions, - columnLengths) + columnLengths, + columnResult.MinLengths) ); } @@ -440,7 +452,8 @@ namespace Avalonia.Controls if (string.IsNullOrEmpty(scopeName)) return; - Debug.Assert(_groups.TryGetValue(scopeName, out var group)); + if (!_groups.TryGetValue(scopeName, out var group)) + throw new AvaloniaInternalException($"SharedSizeScopeHost: The scope {scopeName} wasn't found in the shared size scope"); group.Remove(result); if (!group.Results.Any()) @@ -471,7 +484,9 @@ namespace Avalonia.Controls internal void RegisterGrid(Grid toAdd) { - Debug.Assert(!_measurementCaches.Any(mc => ReferenceEquals(mc.Grid, toAdd))); + if (_measurementCaches.Any(mc => ReferenceEquals(mc.Grid, toAdd))) + throw new AvaloniaInternalException("SharedSizeScopeHost: tried to register a grid twice!"); + var cache = new MeasurementCache(toAdd); _measurementCaches.Add(cache); AddGridToScopes(cache); @@ -480,11 +495,17 @@ namespace Avalonia.Controls internal void UnegisterGrid(Grid toRemove) { var cache = _measurementCaches.FirstOrDefault(mc => ReferenceEquals(mc.Grid, toRemove)); + if (cache == null) + throw new AvaloniaInternalException("SharedSizeScopeHost: tried to unregister a grid that wasn't registered before!"); - Debug.Assert(cache != null); _measurementCaches.Remove(cache); RemoveGridFromScopes(cache); cache.Dispose(); } + + internal bool ParticipatesInScope(Grid toCheck) + { + return _measurementCaches.FirstOrDefault(mc => ReferenceEquals(mc.Grid, toCheck))?.Results.Any() ?? false; + } } } From a33f5cb4dd5fb1410d26c31133090275b4edf480 Mon Sep 17 00:00:00 2001 From: Wojciech Krysiak Date: Sat, 27 Oct 2018 12:42:42 +0200 Subject: [PATCH 18/70] Some unit tests, bugfixes and refactorings. --- src/Avalonia.Controls/ColumnDefinition.cs | 2 +- src/Avalonia.Controls/Grid.cs | 159 ++++---- src/Avalonia.Controls/GridSplitter.cs | 6 + .../Utils/SharedSizeScopeHost.cs | 340 ++++++++++++------ .../SharedSizeScopeTests.cs | 191 ++++++++++ 5 files changed, 532 insertions(+), 166 deletions(-) create mode 100644 tests/Avalonia.Controls.UnitTests/SharedSizeScopeTests.cs diff --git a/src/Avalonia.Controls/ColumnDefinition.cs b/src/Avalonia.Controls/ColumnDefinition.cs index a6b34f8a16..d316881a05 100644 --- a/src/Avalonia.Controls/ColumnDefinition.cs +++ b/src/Avalonia.Controls/ColumnDefinition.cs @@ -88,4 +88,4 @@ namespace Avalonia.Controls set { SetValue(WidthProperty, value); } } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/Grid.cs b/src/Avalonia.Controls/Grid.cs index 7bda4140a3..176c8cdb89 100644 --- a/src/Avalonia.Controls/Grid.cs +++ b/src/Avalonia.Controls/Grid.cs @@ -62,8 +62,8 @@ namespace Avalonia.Controls /// Defines the SharedSizeScopeHost private property. /// The ampersands are used to make accessing the property via xaml inconvenient. /// - private static readonly AttachedProperty s_sharedSizeScopeHostProperty = - AvaloniaProperty.RegisterAttached("&&SharedSizeScopeHost", null); + internal static readonly AttachedProperty s_sharedSizeScopeHostProperty = + AvaloniaProperty.RegisterAttached("&&SharedSizeScopeHost"); private ColumnDefinitions _columnDefinitions; @@ -81,49 +81,6 @@ namespace Avalonia.Controls this.DetachedFromVisualTree += Grid_DetachedFromVisualTree; } - private void Grid_AttachedToVisualTree(object sender, VisualTreeAttachmentEventArgs e) - { - var scope = - new Control[] { this }.Concat(this.GetVisualAncestors().OfType()) - .FirstOrDefault(c => c.GetValue(IsSharedSizeScopeProperty)); - - if (_sharedSizeHost != null) - throw new AvaloniaInternalException("Shared size scope already present when attaching to visual tree!"); - - if (scope != null) - { - _sharedSizeHost = scope.GetValue(s_sharedSizeScopeHostProperty); - _sharedSizeHost.RegisterGrid(this); - } - } - - private void Grid_DetachedFromVisualTree(object sender, VisualTreeAttachmentEventArgs e) - { - _sharedSizeHost?.UnegisterGrid(this); - _sharedSizeHost = null; - } - - private static void IsSharedSizeScopeChanged(Control source, AvaloniaPropertyChangedEventArgs arg2) - { - var shouldDispose = (arg2.OldValue is bool d) && d; - if (shouldDispose) - { - var host = source.GetValue(s_sharedSizeScopeHostProperty) as SharedSizeScopeHost; - if (host == null) - throw new AvaloniaInternalException("SharedScopeHost wasn't set when IsSharedSizeScope was true!"); - host.Dispose(); - source.SetValue(s_sharedSizeScopeHostProperty, null); - } - - var shouldAssign = (arg2.NewValue is bool a) && a; - if (shouldAssign) - { - if (source.GetValue(s_sharedSizeScopeHostProperty) != null) - throw new AvaloniaInternalException("SharedScopeHost was already set when IsSharedSizeScope is only now being set to true!"); - source.SetValue(s_sharedSizeScopeHostProperty, new SharedSizeScopeHost(source)); - } - } - /// /// Gets or sets the columns definitions for the grid. /// @@ -400,7 +357,7 @@ namespace Avalonia.Controls var columnCache = _columnMeasureCache; if (_sharedSizeHost?.ParticipatesInScope(this) ?? false) - { + { (rowCache, columnCache) = _sharedSizeHost.HandleArrange(this, _rowMeasureCache, _columnMeasureCache); rowCache = rowLayout.Measure(finalSize.Height, rowCache.LeanLengthList); @@ -438,6 +395,73 @@ namespace Avalonia.Controls return finalSize; } + /// + /// Tests whether this grid belongs to a shared size scope. + /// + /// True if the grid is registered in a shared size scope. + internal bool HasSharedSizeScope() + { + return _sharedSizeHost != null; + } + + /// + /// Called when the SharedSizeScope for a given grid has changed. + /// Unregisters the grid from it's current scope and finds a new one (if any) + /// + /// + /// This method, while not efficient, correctly handles nested scopes, with any order of scope changes. + /// + internal void SharedScopeChanged() + { + _sharedSizeHost?.UnegisterGrid(this); + + _sharedSizeHost = null; + var scope = this.GetVisualAncestors().OfType() + .FirstOrDefault(c => c.GetValue(IsSharedSizeScopeProperty)); + + if (scope != null) + { + _sharedSizeHost = scope.GetValue(s_sharedSizeScopeHostProperty); + _sharedSizeHost.RegisterGrid(this); + } + + InvalidateMeasure(); + } + + /// + /// Callback when a grid is attached to the visual tree. Finds the innermost SharedSizeScope and registers the grid + /// in it. + /// + /// The source of the event. + /// The event arguments. + private void Grid_AttachedToVisualTree(object sender, VisualTreeAttachmentEventArgs e) + { + var scope = + new Control[] { this }.Concat(this.GetVisualAncestors().OfType()) + .FirstOrDefault(c => c.GetValue(IsSharedSizeScopeProperty)); + + if (_sharedSizeHost != null) + throw new AvaloniaInternalException("Shared size scope already present when attaching to visual tree!"); + + if (scope != null) + { + _sharedSizeHost = scope.GetValue(s_sharedSizeScopeHostProperty); + _sharedSizeHost.RegisterGrid(this); + } + } + + /// + /// Callback when a grid is detached from the visual tree. Unregisters the grid from its SharedSizeScope if any. + /// + /// The source of the event. + /// The event arguments. + private void Grid_DetachedFromVisualTree(object sender, VisualTreeAttachmentEventArgs e) + { + _sharedSizeHost?.UnegisterGrid(this); + _sharedSizeHost = null; + } + + /// /// Get the safe column/columnspan and safe row/rowspan. /// This method ensures that none of the children has a column/row outside the bounds of the definitions. @@ -515,25 +539,40 @@ namespace Avalonia.Controls return value; } - internal bool HasSharedSizeGroups() - { - return ColumnDefinitions.Any(cd => !string.IsNullOrEmpty(cd.SharedSizeGroup)) || - RowDefinitions.Any(rd => !string.IsNullOrEmpty(rd.SharedSizeGroup)); - } - - internal void SharedScopeChanged() + /// + /// Called when the value of changes for a control. + /// + /// The control that triggered the change. + /// Change arguments. + private static void IsSharedSizeScopeChanged(Control source, AvaloniaPropertyChangedEventArgs arg2) { - _sharedSizeHost = null; - var scope = this.GetVisualAncestors().OfType() - .FirstOrDefault(c => c.GetValue(IsSharedSizeScopeProperty)); + var shouldDispose = (arg2.OldValue is bool d) && d; + if (shouldDispose) + { + var host = source.GetValue(s_sharedSizeScopeHostProperty) as SharedSizeScopeHost; + if (host == null) + throw new AvaloniaInternalException("SharedScopeHost wasn't set when IsSharedSizeScope was true!"); + host.Dispose(); + source.ClearValue(s_sharedSizeScopeHostProperty); + } - if (scope != null) + var shouldAssign = (arg2.NewValue is bool a) && a; + if (shouldAssign) { - _sharedSizeHost = scope.GetValue(s_sharedSizeScopeHostProperty); - _sharedSizeHost.RegisterGrid(this); + if (source.GetValue(s_sharedSizeScopeHostProperty) != null) + throw new AvaloniaInternalException("SharedScopeHost was already set when IsSharedSizeScope is only now being set to true!"); + source.SetValue(s_sharedSizeScopeHostProperty, new SharedSizeScopeHost()); } - InvalidateMeasure(); + // if the scope has changed, notify the descendant grids that they need to update. + if (source.GetVisualRoot() != null && shouldAssign || shouldDispose) + { + var participatingGrids = new[] { source }.Concat(source.GetVisualDescendants()).OfType(); + + foreach (var grid in participatingGrids) + grid.SharedScopeChanged(); + + } } } } diff --git a/src/Avalonia.Controls/GridSplitter.cs b/src/Avalonia.Controls/GridSplitter.cs index 4d38d7389e..304a760216 100644 --- a/src/Avalonia.Controls/GridSplitter.cs +++ b/src/Avalonia.Controls/GridSplitter.cs @@ -44,6 +44,12 @@ namespace Avalonia.Controls protected override void OnDragDelta(VectorEventArgs e) { + // WPF doesn't change anything when spliter is in the last row/column + // but resizes the splitter row/column when it's the first one. + // this is different, but more internally consistent. + if (_prevDefinition == null || _nextDefinition == null) + return; + var delta = _orientation == Orientation.Vertical ? e.Vector.X : e.Vector.Y; double max; double min; diff --git a/src/Avalonia.Controls/Utils/SharedSizeScopeHost.cs b/src/Avalonia.Controls/Utils/SharedSizeScopeHost.cs index 5948bd7f19..ec9c0b3eca 100644 --- a/src/Avalonia.Controls/Utils/SharedSizeScopeHost.cs +++ b/src/Avalonia.Controls/Utils/SharedSizeScopeHost.cs @@ -13,6 +13,12 @@ using Avalonia.VisualTree; namespace Avalonia.Controls { + /// + /// Shared size scope implementation. + /// Shares the size information between participating grids. + /// An instance of this class is attached to every that has its + /// IsSharedSizeScope property set to true. + /// internal sealed class SharedSizeScopeHost : IDisposable { private enum MeasurementState @@ -22,6 +28,12 @@ namespace Avalonia.Controls Cached } + /// + /// Class containing the measured rows/columns for a single grid. + /// Monitors changes to the row/column collections as well as the SharedSizeGroup changes + /// for the individual items in those collections. + /// Notifies the of SharedSizeGroup changes. + /// private sealed class MeasurementCache : IDisposable { readonly CompositeDisposable _subscriptions; @@ -129,6 +141,12 @@ namespace Avalonia.Controls } } + + /// + /// Updates the Results collection with Grid Measure results. + /// + /// Result of the GridLayout.Measure method for the RowDefinitions in the grid. + /// Result of the GridLayout.Measure method for the ColumnDefinitions in the grid. public void UpdateMeasureResult(GridLayout.MeasureResult rowResult, GridLayout.MeasureResult columnResult) { MeasurementState = MeasurementState.Cached; @@ -145,9 +163,16 @@ namespace Avalonia.Controls } } + /// + /// Clears the measurement cache, in preparation for the Measure pass. + /// public void InvalidateMeasure() { + var newItems = new List(); + var oldItems = new List(); + MeasurementState = MeasurementState.Invalidated; + Results.ForEach(r => { r.MeasuredResult = double.NaN; @@ -155,18 +180,38 @@ namespace Avalonia.Controls }); } + /// + /// Clears the subscriptions. + /// public void Dispose() { _subscriptions.Dispose(); _groupChanged.OnCompleted(); } + /// + /// Gets the for which this cache has been created. + /// public Grid Grid { get; } + + /// + /// Gets the of this cache. + /// public MeasurementState MeasurementState { get; private set; } + /// + /// Gets the list of instances. + /// + /// + /// The list is a 1-1 map of the concatenation of RowDefinitions and ColumnDefinitions + /// public List Results { get; private set; } } + + /// + /// Class containing the Measure result for a single Row/Column in a grid. + /// private class MeasurementResult { public MeasurementResult(Grid owningGrid, DefinitionBase definition) @@ -176,12 +221,35 @@ namespace Avalonia.Controls MeasuredResult = double.NaN; } + /// + /// Gets the / related to this + /// public DefinitionBase Definition { get; } + + /// + /// Gets or sets the actual result of the Measure operation for this column. + /// public double MeasuredResult { get; set; } + + /// + /// Gets or sets the Minimum constraint for a Row/Column - relevant for star Rows/Columns in unconstrained grids. + /// public double MinLength { get; set; } + + /// + /// Gets or sets the that this result belongs to. + /// public Group SizeGroup { get; set; } + + /// + /// Gets the Grid that is the parent of the Row/Column + /// public Grid OwningGrid { get; } + /// + /// Calculates the effective length that this Row/Column wishes to enforce in the SharedSizeGroup. + /// + /// A tuple of length and the priority in the shared size group. public (double length, int priority) GetPriorityLength() { var length = (Definition as ColumnDefinition)?.Width ?? ((RowDefinition)Definition).Height; @@ -196,12 +264,24 @@ namespace Avalonia.Controls } } - + /// + /// Visitor class used to gather the final length for a given SharedSizeGroup. + /// + /// + /// The values are applied according to priorities defined in . + /// private class LentgthGatherer { + /// + /// Gets the final Length to be applied to every Row/Column in a SharedSizeGroup + /// public double Length { get; private set; } private int gatheredPriority = 6; + /// + /// Visits the applying the result of to its internal cache. + /// + /// The instance to visit. public void Visit(MeasurementResult result) { var (length, priority) = result.GetPriorityLength(); @@ -221,12 +301,17 @@ namespace Avalonia.Controls } } - + /// + /// Representation of a SharedSizeGroup, containing Rows/Columns with the same SharedSizeGroup property value. + /// private class Group { private double? cachedResult; private List _results = new List(); + /// + /// Gets the name of the SharedSizeGroup. + /// public string Name { get; } public Group(string name) @@ -234,17 +319,29 @@ namespace Avalonia.Controls Name = name; } - public bool IsFixed { get; set; } - + /// + /// Gets the collection of the instances. + /// public IReadOnlyList Results => _results; + /// + /// Gets the final, calculated length for all Rows/Columns in the SharedSizeGroup. + /// public double CalculatedLength => (cachedResult ?? (cachedResult = Gather())).Value; + /// + /// Clears the previously cached result in preparation for measurement. + /// public void Reset() { cachedResult = null; } + /// + /// Ads a measurement result to this group and sets it's property + /// to this instance. + /// + /// The to include in this group. public void Add(MeasurementResult result) { if (_results.Contains(result)) @@ -255,6 +352,10 @@ namespace Avalonia.Controls _results.Add(result); } + /// + /// Removes the measurement result from this group and clears its value. + /// + /// The to clear. public void Remove(MeasurementResult result) { if (!_results.Contains(result)) @@ -275,30 +376,64 @@ namespace Avalonia.Controls } } - private readonly AvaloniaList _measurementCaches; - + private readonly AvaloniaList _measurementCaches = new AvaloniaList(); private readonly Dictionary _groups = new Dictionary(); + private bool _invalidating; - public SharedSizeScopeHost(Control scope) + /// + /// Removes the SharedSizeScope and notifies all affected grids of the change. + /// + public void Dispose() { - _measurementCaches = GetParticipatingGrids(scope); + while (_measurementCaches.Any()) + _measurementCaches[0].Grid.SharedScopeChanged(); + } - foreach (var cache in _measurementCaches) - { - cache.Grid.InvalidateMeasure(); - AddGridToScopes(cache); + /// + /// Registers the grid in this SharedSizeScope, to be called when the grid is added to the visual tree. + /// + /// The to add to this scope. + internal void RegisterGrid(Grid toAdd) + { + if (_measurementCaches.Any(mc => ReferenceEquals(mc.Grid, toAdd))) + throw new AvaloniaInternalException("SharedSizeScopeHost: tried to register a grid twice!"); - } + var cache = new MeasurementCache(toAdd); + _measurementCaches.Add(cache); + AddGridToScopes(cache); } - void SharedGroupChanged((string oldName, string newName, MeasurementResult result) change) + /// + /// Removes the registration for a grid in this SharedSizeScope. + /// + /// The to remove. + internal void UnegisterGrid(Grid toRemove) { - RemoveFromGroup(change.oldName, change.result); - AddToGroup(change.newName, change.result); + var cache = _measurementCaches.FirstOrDefault(mc => ReferenceEquals(mc.Grid, toRemove)); + if (cache == null) + throw new AvaloniaInternalException("SharedSizeScopeHost: tried to unregister a grid that wasn't registered before!"); + + _measurementCaches.Remove(cache); + RemoveGridFromScopes(cache); + cache.Dispose(); } - private bool _invalidating; + /// + /// Helper method to check if a grid needs to forward its Mesure results to, and requrest Arrange results from this scope. + /// + /// The that should be checked. + /// True if the grid should forward its calls. + internal bool ParticipatesInScope(Grid toCheck) + { + return _measurementCaches.FirstOrDefault(mc => ReferenceEquals(mc.Grid, toCheck)) + ?.Results.Any(r => r.SizeGroup != null) ?? false; + } + /// + /// Notifies the SharedSizeScope that a grid had requested its measurement to be invalidated. + /// Forwards the same call to all affected grids in this scope. + /// + /// The that had it's Measure invalidated. internal void InvalidateMeasure(Grid grid) { // prevent stack overflow @@ -311,6 +446,40 @@ namespace Avalonia.Controls _invalidating = false; } + /// + /// Updates the measurement cache with the results of the measurement pass. + /// + /// The that has been measured. + /// Measurement result for the grid's + /// Measurement result for the grid's + internal void UpdateMeasureStatus(Grid grid, GridLayout.MeasureResult rowResult, GridLayout.MeasureResult columnResult) + { + var cache = _measurementCaches.FirstOrDefault(mc => ReferenceEquals(mc.Grid, grid)); + if (cache == null) + throw new AvaloniaInternalException("SharedSizeScopeHost: Attempted to update measurement status for a grid that wasn't registered!"); + + cache.UpdateMeasureResult(rowResult, columnResult); + } + + /// + /// Calculates the measurement result including the impact of any SharedSizeGroups that might affect this grid. + /// + /// The that is being Arranged + /// The 's cached measurement result. + /// The 's cached measurement result. + /// Row and column measurement result updated with the SharedSizeScope constraints. + internal (GridLayout.MeasureResult, GridLayout.MeasureResult) HandleArrange(Grid grid, GridLayout.MeasureResult rowResult, GridLayout.MeasureResult columnResult) + { + return ( + Arrange(grid.RowDefinitions, rowResult), + Arrange(grid.ColumnDefinitions, columnResult) + ); + } + + /// + /// Invalidates the measure of all grids affected by the SharedSizeGroups contained within. + /// + /// The that is being invalidated. private void InvalidateMeasureImpl(Grid grid) { var cache = _measurementCaches.FirstOrDefault(mc => ReferenceEquals(mc.Grid, grid)); @@ -323,6 +492,10 @@ namespace Avalonia.Controls if (cache.MeasurementState == MeasurementState.Invalidated) return; + // we won't calculate, so we should not invalidate. + if (!ParticipatesInScope(grid)) + return; + cache.InvalidateMeasure(); // maybe there is a condition to only call arrange on some of the calls? @@ -346,16 +519,24 @@ namespace Avalonia.Controls } } - internal void UpdateMeasureStatus(Grid grid, GridLayout.MeasureResult rowResult, GridLayout.MeasureResult columnResult) + /// + /// callback notifying the scope that a has changed its + /// SharedSizeGroup + /// + /// Old and New name (either can be null) of the SharedSizeGroup, as well as the result. + private void SharedGroupChanged((string oldName, string newName, MeasurementResult result) change) { - var cache = _measurementCaches.FirstOrDefault(mc => ReferenceEquals(mc.Grid, grid)); - if (cache == null) - throw new AvaloniaInternalException("SharedSizeScopeHost: Attempted to update measurement status for a grid that wasn't registered!"); - - cache.UpdateMeasureResult(rowResult, columnResult); + RemoveFromGroup(change.oldName, change.result); + AddToGroup(change.newName, change.result); } - (List, List, double) Arrange(IReadOnlyList definitions, GridLayout.MeasureResult measureResult) + /// + /// Handles the impact of SharedSizeGroups on the Arrange of / + /// + /// Rows/Columns that were measured + /// The initial measurement result. + /// Modified measure result + private GridLayout.MeasureResult Arrange(IReadOnlyList definitions, GridLayout.MeasureResult measureResult) { var conventions = measureResult.LeanLengthList.ToList(); var lengths = measureResult.LengthList.ToList(); @@ -363,6 +544,8 @@ namespace Avalonia.Controls for (int i = 0; i < definitions.Count; i++) { var definition = definitions[i]; + + // for empty SharedSizeGroups pass on unmodified result. if (string.IsNullOrEmpty(definition.SharedSizeGroup)) { desiredLength += measureResult.LengthList[i]; @@ -370,7 +553,7 @@ namespace Avalonia.Controls } var group = _groups[definition.SharedSizeGroup]; - + // Length calculated over all Definitions participating in a SharedSizeGroup. var length = group.CalculatedLength; conventions[i] = new GridLayout.LengthConvention( @@ -382,33 +565,19 @@ namespace Avalonia.Controls desiredLength += length; } - return (conventions, lengths, desiredLength); - } - - internal (GridLayout.MeasureResult, GridLayout.MeasureResult) HandleArrange(Grid grid, GridLayout.MeasureResult rowResult, GridLayout.MeasureResult columnResult) - { - var (rowConventions, rowLengths, rowDesiredLength) = Arrange(grid.RowDefinitions, rowResult); - var (columnConventions, columnLengths, columnDesiredLength) = Arrange(grid.ColumnDefinitions, columnResult); - - return ( - new GridLayout.MeasureResult( - rowResult.ContainerLength, - rowDesiredLength, - rowResult.GreedyDesiredLength,//?? - rowConventions, - rowLengths, - rowResult.MinLengths), - new GridLayout.MeasureResult( - columnResult.ContainerLength, - columnDesiredLength, - columnResult.GreedyDesiredLength, //?? - columnConventions, - columnLengths, - columnResult.MinLengths) - ); + return new GridLayout.MeasureResult( + measureResult.ContainerLength, + desiredLength, + measureResult.GreedyDesiredLength,//?? + conventions, + lengths, + measureResult.MinLengths); } - + /// + /// Adds all measurement results for a grid to their repsective scopes. + /// + /// The for a grid to be added. private void AddGridToScopes(MeasurementCache cache) { cache.GroupChanged.Subscribe(SharedGroupChanged); @@ -420,6 +589,12 @@ namespace Avalonia.Controls } } + /// + /// Handles adding the to a SharedSizeGroup. + /// Does nothing for empty SharedSizeGroups. + /// + /// The name (can be null or empty) of the group to add the to. + /// The to add to a scope. private void AddToGroup(string scopeName, MeasurementResult result) { if (string.IsNullOrEmpty(scopeName)) @@ -428,16 +603,13 @@ namespace Avalonia.Controls if (!_groups.TryGetValue(scopeName, out var group)) _groups.Add(scopeName, group = new Group(scopeName)); - group.IsFixed |= IsFixed(result.Definition); - group.Add(result); } - private bool IsFixed(DefinitionBase definition) - { - return ((definition as ColumnDefinition)?.Width ?? ((RowDefinition)definition).Height).IsAbsolute; - } - + /// + /// Removes all measurement results for a grid from their respective scopes. + /// + /// The for a grid to be removed. private void RemoveGridFromScopes(MeasurementCache cache) { foreach (var result in cache.Results) @@ -447,6 +619,12 @@ namespace Avalonia.Controls } } + /// + /// Handles removing the from a SharedSizeGroup. + /// Does nothing for empty SharedSizeGroups. + /// + /// The name (can be null or empty) of the group to remove the from. + /// The to remove from a scope. private void RemoveFromGroup(string scopeName, MeasurementResult result) { if (string.IsNullOrEmpty(scopeName)) @@ -458,54 +636,6 @@ namespace Avalonia.Controls group.Remove(result); if (!group.Results.Any()) _groups.Remove(scopeName); - else - { - group.IsFixed = group.Results.Select(r => r.Definition).Any(IsFixed); - } - } - - private static AvaloniaList GetParticipatingGrids(Control scope) - { - var result = scope.GetVisualDescendants().OfType(); - - return new AvaloniaList( - result.Where(g => g.HasSharedSizeGroups()) - .Select(g => new MeasurementCache(g))); - } - - public void Dispose() - { - foreach (var cache in _measurementCaches) - { - cache.Grid.SharedScopeChanged(); - cache.Dispose(); - } - } - - internal void RegisterGrid(Grid toAdd) - { - if (_measurementCaches.Any(mc => ReferenceEquals(mc.Grid, toAdd))) - throw new AvaloniaInternalException("SharedSizeScopeHost: tried to register a grid twice!"); - - var cache = new MeasurementCache(toAdd); - _measurementCaches.Add(cache); - AddGridToScopes(cache); - } - - internal void UnegisterGrid(Grid toRemove) - { - var cache = _measurementCaches.FirstOrDefault(mc => ReferenceEquals(mc.Grid, toRemove)); - if (cache == null) - throw new AvaloniaInternalException("SharedSizeScopeHost: tried to unregister a grid that wasn't registered before!"); - - _measurementCaches.Remove(cache); - RemoveGridFromScopes(cache); - cache.Dispose(); - } - - internal bool ParticipatesInScope(Grid toCheck) - { - return _measurementCaches.FirstOrDefault(mc => ReferenceEquals(mc.Grid, toCheck))?.Results.Any() ?? false; } } } diff --git a/tests/Avalonia.Controls.UnitTests/SharedSizeScopeTests.cs b/tests/Avalonia.Controls.UnitTests/SharedSizeScopeTests.cs new file mode 100644 index 0000000000..0d0b9c2891 --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/SharedSizeScopeTests.cs @@ -0,0 +1,191 @@ +using System.Linq; +using Avalonia.Controls.Primitives; +using Avalonia.Input; +using Avalonia.Platform; +using Avalonia.UnitTests; + +using Moq; + +using Xunit; + +namespace Avalonia.Controls.UnitTests +{ + public class SharedSizeScopeTests + { + public SharedSizeScopeTests() + { + } + + [Fact] + public void All_Descendant_Grids_Are_Registered_When_Added_After_Setting_Scope() + { + var grids = new[] { new Grid(), new Grid(), new Grid() }; + var scope = new Panel(); + scope.Children.AddRange(grids); + + var root = new TestRoot(); + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + root.Child = scope; + + Assert.All(grids, g => Assert.True(g.HasSharedSizeScope())); + } + + [Fact] + public void All_Descendant_Grids_Are_Registered_When_Setting_Scope() + { + var grids = new[] { new Grid(), new Grid(), new Grid() }; + var scope = new Panel(); + scope.Children.AddRange(grids); + + var root = new TestRoot(); + root.Child = scope; + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + + Assert.All(grids, g => Assert.True(g.HasSharedSizeScope())); + } + + [Fact] + public void All_Descendant_Grids_Are_Unregistered_When_Resetting_Scope() + { + var grids = new[] { new Grid(), new Grid(), new Grid() }; + var scope = new Panel(); + scope.Children.AddRange(grids); + + var root = new TestRoot(); + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + root.Child = scope; + + Assert.All(grids, g => Assert.True(g.HasSharedSizeScope())); + root.SetValue(Grid.IsSharedSizeScopeProperty, false); + Assert.All(grids, g => Assert.False(g.HasSharedSizeScope())); + Assert.Equal(null, root.GetValue(Grid.s_sharedSizeScopeHostProperty)); + } + + [Fact] + public void Size_Is_Propagated_Between_Grids() + { + var grids = new[] { CreateGrid("A", null),CreateGrid(("A",new GridLength(30)), (null, new GridLength()))}; + var scope = new Panel(); + scope.Children.AddRange(grids); + + var root = new TestRoot(); + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + root.Child = scope; + + root.Measure(new Size(50, 50)); + root.Arrange(new Rect(new Point(), new Point(50, 50))); + Assert.Equal(30, grids[0].ColumnDefinitions[0].ActualWidth); + } + + [Fact] + public void Size_Propagation_Is_Constrained_To_Innermost_Scope() + { + var grids = new[] { CreateGrid("A", null), CreateGrid(("A", new GridLength(30)), (null, new GridLength())) }; + var innerScope = new Panel(); + innerScope.Children.AddRange(grids); + innerScope.SetValue(Grid.IsSharedSizeScopeProperty, true); + + var outerGrid = CreateGrid(("A", new GridLength(0))); + var outerScope = new Panel(); + outerScope.Children.AddRange(new[] { outerGrid, innerScope }); + + var root = new TestRoot(); + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + root.Child = outerScope; + + root.Measure(new Size(50, 50)); + root.Arrange(new Rect(new Point(), new Point(50, 50))); + Assert.Equal(0, outerGrid.ColumnDefinitions[0].ActualWidth); + } + + [Fact] + public void Size_Is_Propagated_Between_Rows_And_Columns() + { + var grid = new Grid + { + ColumnDefinitions = new ColumnDefinitions("*,30"), + RowDefinitions = new RowDefinitions("*,10") + }; + + grid.ColumnDefinitions[1].SharedSizeGroup = "A"; + grid.RowDefinitions[1].SharedSizeGroup = "A"; + + var root = new TestRoot(); + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + root.Child = grid; + + root.Measure(new Size(50, 50)); + root.Arrange(new Rect(new Point(), new Point(50, 50))); + Assert.Equal(30, grid.RowDefinitions[1].ActualHeight); + } + + [Fact] + public void Size_Group_Changes_Are_Tracked() + { + var grids = new[] { + CreateGrid((null, new GridLength(0, GridUnitType.Auto)), (null, new GridLength())), + CreateGrid(("A", new GridLength(30)), (null, new GridLength())) }; + var scope = new Panel(); + scope.Children.AddRange(grids); + + var root = new TestRoot(); + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + root.Child = scope; + + root.Measure(new Size(50, 50)); + root.Arrange(new Rect(new Point(), new Point(50, 50))); + Assert.Equal(0, grids[0].ColumnDefinitions[0].ActualWidth); + + grids[0].ColumnDefinitions[0].SharedSizeGroup = "A"; + + root.Measure(new Size(51, 51)); + root.Arrange(new Rect(new Point(), new Point(51, 51))); + Assert.Equal(30, grids[0].ColumnDefinitions[0].ActualWidth); + + grids[0].ColumnDefinitions[0].SharedSizeGroup = null; + + root.Measure(new Size(52, 52)); + root.Arrange(new Rect(new Point(), new Point(52, 52))); + Assert.Equal(0, grids[0].ColumnDefinitions[0].ActualWidth); + } + + // grid creators + private Grid CreateGrid(params string[] columnGroups) + { + return CreateGrid(columnGroups.Select(s => (s, ColumnDefinition.WidthProperty.GetDefaultValue(typeof(ColumnDefinition)))).ToArray()); + } + + private Grid CreateGrid(params (string name, GridLength width)[] columns) + { + return CreateGrid(columns.Select(c => + (c.name, c.width, ColumnDefinition.MinWidthProperty.GetDefaultValue(typeof(ColumnDefinition)))).ToArray()); + } + + private Grid CreateGrid(params (string name, GridLength width, double minWidth)[] columns) + { + return CreateGrid(columns.Select(c => + (c.name, c.width, c.minWidth, ColumnDefinition.MaxWidthProperty.GetDefaultValue(typeof(ColumnDefinition)))).ToArray()); + } + + private Grid CreateGrid(params (string name, GridLength width, double minWidth, double maxWidth)[] columns) + { + var columnDefinitions = new ColumnDefinitions(); + + columnDefinitions.AddRange( + columns.Select(c => new ColumnDefinition + { + SharedSizeGroup = c.name, + Width = c.width, + MinWidth = c.minWidth, + MaxWidth = c.maxWidth + }) + ); + var grid = new Grid + { + ColumnDefinitions = columnDefinitions + }; + + return grid; + } + } +} From 0d7e1936931c2eb3b381aa7aa4f29e85244f45c8 Mon Sep 17 00:00:00 2001 From: Wojciech Krysiak Date: Mon, 29 Oct 2018 22:08:29 +0100 Subject: [PATCH 19/70] More tests, futureproofing + 2 bugfixes --- src/Avalonia.Controls/Grid.cs | 2 + .../Utils/SharedSizeScopeHost.cs | 12 ++- .../SharedSizeScopeTests.cs | 93 +++++++++++++++++++ 3 files changed, 106 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Grid.cs b/src/Avalonia.Controls/Grid.cs index 176c8cdb89..b51583d8b3 100644 --- a/src/Avalonia.Controls/Grid.cs +++ b/src/Avalonia.Controls/Grid.cs @@ -105,6 +105,7 @@ namespace Avalonia.Controls _columnDefinitions = value; _columnDefinitions.TrackItemPropertyChanged(_ => InvalidateMeasure()); + _columnDefinitions.CollectionChanged += (_, __) => InvalidateMeasure(); } } @@ -132,6 +133,7 @@ namespace Avalonia.Controls _rowDefinitions = value; _rowDefinitions.TrackItemPropertyChanged(_ => InvalidateMeasure()); + _rowDefinitions.CollectionChanged += (_, __) => InvalidateMeasure(); } } diff --git a/src/Avalonia.Controls/Utils/SharedSizeScopeHost.cs b/src/Avalonia.Controls/Utils/SharedSizeScopeHost.cs index ec9c0b3eca..8553165e4b 100644 --- a/src/Avalonia.Controls/Utils/SharedSizeScopeHost.cs +++ b/src/Avalonia.Controls/Utils/SharedSizeScopeHost.cs @@ -52,6 +52,7 @@ namespace Avalonia.Controls grid.RowDefinitions.CollectionChanged += DefinitionsCollectionChanged; grid.ColumnDefinitions.CollectionChanged += DefinitionsCollectionChanged; + _subscriptions = new CompositeDisposable( Disposable.Create(() => grid.RowDefinitions.CollectionChanged -= DefinitionsCollectionChanged), Disposable.Create(() => grid.ColumnDefinitions.CollectionChanged -= DefinitionsCollectionChanged), @@ -60,6 +61,13 @@ namespace Avalonia.Controls } + // method to be hooked up once RowDefinitions/ColumnDefinitions collections can be replaced on a grid + private void DefinitionsChanged(object sender, AvaloniaPropertyChangedEventArgs e) + { + // route to collection changed as a Reset. + DefinitionsCollectionChanged(null, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + } + private void DefinitionPropertyChanged(Tuple propertyChanged) { if (propertyChanged.Item2.PropertyName == nameof(DefinitionBase.SharedSizeGroup)) @@ -78,7 +86,9 @@ namespace Avalonia.Controls offset = Grid.RowDefinitions.Count; var newItems = e.NewItems?.OfType().Select(db => new MeasurementResult(Grid, db)).ToList() ?? new List(); - var oldItems = Results.GetRange(e.OldStartingIndex + offset, e.OldItems?.Count ?? 0); + var oldItems = e.OldStartingIndex >= 0 + ? Results.GetRange(e.OldStartingIndex + offset, e.OldItems.Count) + : new List(); void NotifyNewItems() { diff --git a/tests/Avalonia.Controls.UnitTests/SharedSizeScopeTests.cs b/tests/Avalonia.Controls.UnitTests/SharedSizeScopeTests.cs index 0d0b9c2891..715e9da880 100644 --- a/tests/Avalonia.Controls.UnitTests/SharedSizeScopeTests.cs +++ b/tests/Avalonia.Controls.UnitTests/SharedSizeScopeTests.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Linq; using Avalonia.Controls.Primitives; using Avalonia.Input; @@ -149,6 +150,90 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(0, grids[0].ColumnDefinitions[0].ActualWidth); } + [Fact] + public void Collection_Changes_Are_Tracked() + { + var grid = CreateGrid( + ("A", new GridLength(20)), + ("A", new GridLength(30)), + ("A", new GridLength(40)), + (null, new GridLength())); + + var scope = new Panel(); + scope.Children.Add(grid); + + var root = new TestRoot(); + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + root.Child = scope; + + grid.Measure(new Size(200, 200)); + grid.Arrange(new Rect(new Point(), new Point(200, 200))); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(40, cd.ActualWidth)); + + grid.ColumnDefinitions.RemoveAt(2); + + grid.Measure(new Size(200, 200)); + grid.Arrange(new Rect(new Point(), new Point(200, 200))); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(30, cd.ActualWidth)); + + grid.ColumnDefinitions.Insert(1, new ColumnDefinition { Width = new GridLength(35), SharedSizeGroup = "A" }); + + grid.Measure(new Size(200, 200)); + grid.Arrange(new Rect(new Point(), new Point(200, 200))); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(35, cd.ActualWidth)); + + grid.ColumnDefinitions[1] = new ColumnDefinition { Width = new GridLength(10), SharedSizeGroup = "A" }; + + grid.Measure(new Size(200, 200)); + grid.Arrange(new Rect(new Point(), new Point(200, 200))); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(30, cd.ActualWidth)); + + grid.ColumnDefinitions[1] = new ColumnDefinition { Width = new GridLength(50), SharedSizeGroup = "A" }; + + grid.Measure(new Size(200, 200)); + grid.Arrange(new Rect(new Point(), new Point(200, 200))); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(50, cd.ActualWidth)); + } + + [Fact] + public void Size_Priorities_Are_Maintained() + { + var sizers = new List(); + var grid = CreateGrid( + ("A", new GridLength(20)), + ("A", new GridLength(20, GridUnitType.Auto)), + ("A", new GridLength(1, GridUnitType.Star)), + ("A", new GridLength(1, GridUnitType.Star)), + (null, new GridLength())); + for (int i = 0; i < 3; i++) + sizers.Add(AddSizer(grid, i, 6 + i * 6)); + var scope = new Panel(); + scope.Children.Add(grid); + + var root = new TestRoot(); + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + root.Child = scope; + + grid.Measure(new Size(100, 100)); + grid.Arrange(new Rect(new Point(), new Point(100, 100))); + // all in group are equal to the first fixed column + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(20, cd.ActualWidth)); + + grid.ColumnDefinitions[0].SharedSizeGroup = null; + + grid.Measure(new Size(100, 100)); + grid.Arrange(new Rect(new Point(), new Point(100, 100))); + // all in group are equal to width (MinWidth) of the sizer in the second column + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(6 + 1 * 6, cd.ActualWidth)); + + grid.ColumnDefinitions[1].SharedSizeGroup = null; + + grid.Measure(new Size(double.PositiveInfinity, 100)); + grid.Arrange(new Rect(new Point(), new Point(100, 100))); + // with no constraint star columns default to the MinWidth of the sizer in the column + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(6 + 2 * 6, cd.ActualWidth)); + } + // grid creators private Grid CreateGrid(params string[] columnGroups) { @@ -187,5 +272,13 @@ namespace Avalonia.Controls.UnitTests return grid; } + + private Control AddSizer(Grid grid, int column, double size = 30) + { + var ctrl = new Control { MinWidth = size, MinHeight = size }; + ctrl.SetValue(Grid.ColumnProperty,column); + grid.Children.Add(ctrl); + return ctrl; + } } } From 22879fe31266b3b7074df466c6753b40a7239d0d Mon Sep 17 00:00:00 2001 From: Tom Daffin Date: Sat, 3 Nov 2018 08:00:01 -0600 Subject: [PATCH 20/70] Call GtkImContextFilterKeypress after issuing the KeyDown event to get the same sequence of events as windows --- src/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs b/src/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs index 0273f6a7d8..304d17b33e 100644 --- a/src/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs +++ b/src/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs @@ -222,14 +222,14 @@ namespace Avalonia.Gtk3 { var evnt = (GdkEventKey*) pev; _lastKbdEvent = evnt->time; - if (Native.GtkImContextFilterKeypress(_imContext, pev)) - return true; var e = new RawKeyEventArgs( Gtk3Platform.Keyboard, evnt->time, evnt->type == GdkEventType.KeyPress ? RawKeyEventType.KeyDown : RawKeyEventType.KeyUp, Avalonia.Gtk.Common.KeyTransform.ConvertKey((GdkKey)evnt->keyval), GetModifierKeys((GdkModifierType)evnt->state)); OnInput(e); + if (Native.GtkImContextFilterKeypress(_imContext, pev)) + return true; return true; } From 9d5b607707406e0124eac1b6a14694d171024440 Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Wed, 7 Nov 2018 18:03:43 +0100 Subject: [PATCH 21/70] csproj fix --- src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj b/src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj index b408f2a0be..d854a136e9 100644 --- a/src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj +++ b/src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj @@ -25,4 +25,3 @@ - From e87967a4ad8e943a2214d74370786edd05ee8504 Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Wed, 7 Nov 2018 18:14:21 +0100 Subject: [PATCH 22/70] Fix csproj --- samples/ControlCatalog/ControlCatalog.csproj | 14 +++----------- .../Avalonia.Themes.Default.csproj | 13 +++++-------- 2 files changed, 8 insertions(+), 19 deletions(-) diff --git a/samples/ControlCatalog/ControlCatalog.csproj b/samples/ControlCatalog/ControlCatalog.csproj index 13b79d75b5..4af6171a89 100644 --- a/samples/ControlCatalog/ControlCatalog.csproj +++ b/samples/ControlCatalog/ControlCatalog.csproj @@ -11,9 +11,7 @@ - - - + @@ -27,15 +25,9 @@ - - TabControlPage.xaml - - - - - + MSBuild:Compile - + diff --git a/src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj b/src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj index d854a136e9..af1899bab1 100644 --- a/src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj +++ b/src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj @@ -3,9 +3,6 @@ netstandard2.0 false - - - @@ -16,12 +13,12 @@ + + + + MSBuild:Compile + - - - MSBuild:Compile - - From e1fae9692df4a9ec143b0891e7b9ad1f811c72d8 Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Wed, 7 Nov 2018 18:19:21 +0100 Subject: [PATCH 23/70] Remove extra ItemGroup --- samples/ControlCatalog/ControlCatalog.csproj | 6 ------ 1 file changed, 6 deletions(-) diff --git a/samples/ControlCatalog/ControlCatalog.csproj b/samples/ControlCatalog/ControlCatalog.csproj index 4af6171a89..7b30951d61 100644 --- a/samples/ControlCatalog/ControlCatalog.csproj +++ b/samples/ControlCatalog/ControlCatalog.csproj @@ -23,12 +23,6 @@ - - - - MSBuild:Compile - - From 26fa4f0463dc0b8adec78a6961d2fb962af40a1a Mon Sep 17 00:00:00 2001 From: ahopper Date: Thu, 8 Nov 2018 09:22:47 +0000 Subject: [PATCH 24/70] :pointerover effect added to ButtonSpinner --- src/Avalonia.Themes.Default/ButtonSpinner.xaml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Themes.Default/ButtonSpinner.xaml b/src/Avalonia.Themes.Default/ButtonSpinner.xaml index 8284b4eddf..ddeef44011 100644 --- a/src/Avalonia.Themes.Default/ButtonSpinner.xaml +++ b/src/Avalonia.Themes.Default/ButtonSpinner.xaml @@ -87,6 +87,9 @@ - + + + - \ No newline at end of file + From 5cd7c1f6f4a4cc663df6fa9abe9bbd6d1140922d Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Fri, 9 Nov 2018 21:44:45 +0300 Subject: [PATCH 25/70] Reworked dialogs for GTK/Win32 --- samples/ControlCatalog/Pages/DialogsPage.xaml | 4 -- .../ControlCatalog/Pages/DialogsPage.xaml.cs | 8 ++-- samples/ControlCatalog/Pages/MenuPage.xaml.cs | 2 +- src/Avalonia.Controls/Platform/IWindowImpl.cs | 5 +-- src/Avalonia.Controls/SystemDialog.cs | 31 ++++++++++----- src/Avalonia.Controls/Window.cs | 38 +++++++++---------- .../Remote/PreviewerWindowImpl.cs | 5 +-- src/Avalonia.DesignerSupport/Remote/Stubs.cs | 6 ++- src/Avalonia.Native/WindowImpl.cs | 4 +- src/Gtk/Avalonia.Gtk3/Interop/Native.cs | 4 ++ src/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs | 2 +- src/Gtk/Avalonia.Gtk3/WindowImpl.cs | 13 +++++-- .../Interop/UnmanagedMethods.cs | 18 +++++++-- src/Windows/Avalonia.Win32/WindowImpl.cs | 25 ++++++++---- .../WindowTests.cs | 7 +++- 15 files changed, 108 insertions(+), 64 deletions(-) diff --git a/samples/ControlCatalog/Pages/DialogsPage.xaml b/samples/ControlCatalog/Pages/DialogsPage.xaml index 710d791f3a..23b14d009d 100644 --- a/samples/ControlCatalog/Pages/DialogsPage.xaml +++ b/samples/ControlCatalog/Pages/DialogsPage.xaml @@ -3,10 +3,6 @@ - - - Modal to window - diff --git a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs index 8b3e810f0a..f215bf9e64 100644 --- a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs +++ b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs @@ -31,12 +31,12 @@ namespace ControlCatalog.Pages }.ShowAsync(GetWindow()); }; this.FindControl