From 269fc2a5d2ab70cf33a8bb2b019921732c41b2f4 Mon Sep 17 00:00:00 2001 From: wojciech krysiak Date: Tue, 2 Oct 2018 16:13:32 +0200 Subject: [PATCH 01/42] 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 02/42] 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 03/42] 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 04/42] 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 05/42] 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 06/42] 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 07/42] 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 08/42] 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 09/42] 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 10/42] 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 11/42] 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 779cdec09356f7c825b5f70f87b5b4732a6b0036 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 7 Nov 2018 15:24:49 +0000 Subject: [PATCH 12/42] initial work to fixing clipboard text encoding issues. --- native/Avalonia.Native/inc/avalonia-native.h | 8 ++- .../project.pbxproj | 6 ++ native/Avalonia.Native/src/OSX/AvnString.h | 14 +++++ native/Avalonia.Native/src/OSX/AvnString.mm | 55 +++++++++++++++++++ native/Avalonia.Native/src/OSX/clipboard.mm | 15 +++-- src/Avalonia.Native/ClipboardImpl.cs | 9 +-- 6 files changed, 97 insertions(+), 10 deletions(-) create mode 100644 native/Avalonia.Native/src/OSX/AvnString.h create mode 100644 native/Avalonia.Native/src/OSX/AvnString.mm diff --git a/native/Avalonia.Native/inc/avalonia-native.h b/native/Avalonia.Native/inc/avalonia-native.h index 0c965b7498..1d5a112929 100644 --- a/native/Avalonia.Native/inc/avalonia-native.h +++ b/native/Avalonia.Native/inc/avalonia-native.h @@ -173,6 +173,12 @@ public: virtual HRESULT ObtainGlFeature(IAvnGlFeature** ppv) = 0; }; +AVNCOM(IAvnString, 17) : IUnknown +{ + virtual HRESULT GetPointer(void**retOut) = 0; + virtual HRESULT GetLength(int*ret) = 0; +}; + AVNCOM(IAvnWindowBase, 02) : IUnknown { virtual HRESULT Show() = 0; @@ -315,7 +321,7 @@ AVNCOM(IAvnScreens, 0e) : IUnknown AVNCOM(IAvnClipboard, 0f) : IUnknown { - virtual HRESULT GetText (void** retOut) = 0; + virtual HRESULT GetText (IAvnString** ppv ) = 0; virtual HRESULT SetText (char* text) = 0; virtual HRESULT Clear() = 0; }; diff --git a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj index bd8ac481a8..cc74d5669f 100644 --- a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj +++ b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj @@ -9,6 +9,7 @@ /* Begin PBXBuildFile section */ 37A517B32159597E00FBA241 /* Screens.mm in Sources */ = {isa = PBXBuildFile; fileRef = 37A517B22159597E00FBA241 /* Screens.mm */; }; 37C09D8821580FE4006A6758 /* SystemDialogs.mm in Sources */ = {isa = PBXBuildFile; fileRef = 37C09D8721580FE4006A6758 /* SystemDialogs.mm */; }; + 37DDA9B0219330F8002E132B /* AvnString.mm in Sources */ = {isa = PBXBuildFile; fileRef = 37DDA9AF219330F8002E132B /* AvnString.mm */; }; 37E2330F21583241000CB7E2 /* KeyTransform.mm in Sources */ = {isa = PBXBuildFile; fileRef = 37E2330E21583241000CB7E2 /* KeyTransform.mm */; }; 5B21A982216530F500CEE36E /* cursor.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5B21A981216530F500CEE36E /* cursor.mm */; }; 5B8BD94F215BFEA6005ED2A7 /* clipboard.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5B8BD94E215BFEA6005ED2A7 /* clipboard.mm */; }; @@ -26,6 +27,8 @@ 37A517B22159597E00FBA241 /* Screens.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = Screens.mm; sourceTree = ""; }; 37C09D8721580FE4006A6758 /* SystemDialogs.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = SystemDialogs.mm; sourceTree = ""; }; 37C09D8A21581EF2006A6758 /* window.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = window.h; sourceTree = ""; }; + 37DDA9AF219330F8002E132B /* AvnString.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = AvnString.mm; sourceTree = ""; }; + 37DDA9B121933371002E132B /* AvnString.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AvnString.h; sourceTree = ""; }; 37E2330E21583241000CB7E2 /* KeyTransform.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = KeyTransform.mm; sourceTree = ""; }; 5B21A981216530F500CEE36E /* cursor.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = cursor.mm; sourceTree = ""; }; 5B8BD94E215BFEA6005ED2A7 /* clipboard.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = clipboard.mm; sourceTree = ""; }; @@ -65,6 +68,8 @@ AB7A61E62147C814003C5833 = { isa = PBXGroup; children = ( + 37DDA9B121933371002E132B /* AvnString.h */, + 37DDA9AF219330F8002E132B /* AvnString.mm */, 37A4E71A2178846A00EACBCD /* headers */, AB573DC3217605E400D389A2 /* gl.mm */, 5BF943652167AD1D009CAE35 /* cursor.h */, @@ -161,6 +166,7 @@ files = ( 5B8BD94F215BFEA6005ED2A7 /* clipboard.mm in Sources */, 5B21A982216530F500CEE36E /* cursor.mm in Sources */, + 37DDA9B0219330F8002E132B /* AvnString.mm in Sources */, AB8F7D6B21482D7F0057DBA5 /* platformthreading.mm in Sources */, 37E2330F21583241000CB7E2 /* KeyTransform.mm in Sources */, 37A517B32159597E00FBA241 /* Screens.mm in Sources */, diff --git a/native/Avalonia.Native/src/OSX/AvnString.h b/native/Avalonia.Native/src/OSX/AvnString.h new file mode 100644 index 0000000000..9a8f5a1318 --- /dev/null +++ b/native/Avalonia.Native/src/OSX/AvnString.h @@ -0,0 +1,14 @@ +// +// AvnString.h +// Avalonia.Native.OSX +// +// Created by Dan Walmsley on 07/11/2018. +// Copyright © 2018 Avalonia. All rights reserved. +// + +#ifndef AvnString_h +#define AvnString_h + +extern IAvnString* CreateAvnString(NSString* string); + +#endif /* AvnString_h */ diff --git a/native/Avalonia.Native/src/OSX/AvnString.mm b/native/Avalonia.Native/src/OSX/AvnString.mm new file mode 100644 index 0000000000..a16c286634 --- /dev/null +++ b/native/Avalonia.Native/src/OSX/AvnString.mm @@ -0,0 +1,55 @@ +// +// AvnString.m +// Avalonia.Native.OSX +// +// Created by Dan Walmsley on 07/11/2018. +// Copyright © 2018 Avalonia. All rights reserved. +// + +#include "common.h" + +class AvnStringImpl : public virtual ComSingleObject +{ +private: + NSString* _string; + +public: + FORWARD_IUNKNOWN() + + AvnStringImpl(NSString* string) + { + _string = string; + } + + virtual HRESULT GetPointer(void**retOut) override + { + @autoreleasepool + { + if(retOut == nullptr) + { + return E_POINTER; + } + + *retOut = (void*)_string.UTF8String; + + return S_OK; + } + } + + virtual HRESULT GetLength(int*retOut) override + { + if(retOut == nullptr) + { + return E_POINTER; + } + + *retOut = (int)_string.length; + + return S_OK; + } +}; + +IAvnString* CreateAvnString(NSString* string) +{ + return new AvnStringImpl(string); +} diff --git a/native/Avalonia.Native/src/OSX/clipboard.mm b/native/Avalonia.Native/src/OSX/clipboard.mm index 19e5c25801..f941e8ca6c 100644 --- a/native/Avalonia.Native/src/OSX/clipboard.mm +++ b/native/Avalonia.Native/src/OSX/clipboard.mm @@ -2,20 +2,25 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. #include "common.h" +#include "AvnString.h" class Clipboard : public ComSingleObject { public: FORWARD_IUNKNOWN() - virtual HRESULT GetText (void** retOut) override + virtual HRESULT GetText (IAvnString** retOut) override { @autoreleasepool { - NSString *str = [[NSPasteboard generalPasteboard] stringForType:NSPasteboardTypeString]; - *retOut = (void *)str.UTF8String; - } + if(retOut == nullptr) + { + return E_POINTER; + } + + *retOut = CreateAvnString([[NSPasteboard generalPasteboard] stringForType:NSPasteboardTypeString]); - return S_OK; + return S_OK; + } } virtual HRESULT SetText (char* text) override diff --git a/src/Avalonia.Native/ClipboardImpl.cs b/src/Avalonia.Native/ClipboardImpl.cs index d54bc95fbb..a2a1416645 100644 --- a/src/Avalonia.Native/ClipboardImpl.cs +++ b/src/Avalonia.Native/ClipboardImpl.cs @@ -24,12 +24,13 @@ namespace Avalonia.Native return Task.CompletedTask; } - public Task GetTextAsync() + public unsafe Task GetTextAsync() { - var outPtr = _native.GetText(); - var text = Marshal.PtrToStringAnsi(outPtr); + var text = _native.GetText(); - return Task.FromResult(text); + var result = System.Text.Encoding.UTF8.GetString((byte*)text.GetPointer(), text.GetLength()); + + return Task.FromResult(result); } public Task SetTextAsync(string text) From 26fa4f0463dc0b8adec78a6961d2fb962af40a1a Mon Sep 17 00:00:00 2001 From: ahopper Date: Thu, 8 Nov 2018 09:22:47 +0000 Subject: [PATCH 13/42] :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 f6d080feecd1a68ce5ba8028dbdab37b5e8e8264 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 8 Nov 2018 17:20:51 +0000 Subject: [PATCH 14/42] [OSX] fix pasting from clipboard --- native/Avalonia.Native/inc/avalonia-native.h | 6 +++--- native/Avalonia.Native/src/OSX/AvnString.mm | 4 ++-- native/Avalonia.Native/src/OSX/clipboard.mm | 11 ++--------- src/Avalonia.Native/ClipboardImpl.cs | 9 +++++---- 4 files changed, 12 insertions(+), 18 deletions(-) diff --git a/native/Avalonia.Native/inc/avalonia-native.h b/native/Avalonia.Native/inc/avalonia-native.h index 1d5a112929..b9e16e6643 100644 --- a/native/Avalonia.Native/inc/avalonia-native.h +++ b/native/Avalonia.Native/inc/avalonia-native.h @@ -175,8 +175,8 @@ public: AVNCOM(IAvnString, 17) : IUnknown { - virtual HRESULT GetPointer(void**retOut) = 0; - virtual HRESULT GetLength(int*ret) = 0; + virtual HRESULT Pointer(void**retOut) = 0; + virtual HRESULT Length(int*ret) = 0; }; AVNCOM(IAvnWindowBase, 02) : IUnknown @@ -321,7 +321,7 @@ AVNCOM(IAvnScreens, 0e) : IUnknown AVNCOM(IAvnClipboard, 0f) : IUnknown { - virtual HRESULT GetText (IAvnString** ppv ) = 0; + virtual IAvnString* GetText () = 0; virtual HRESULT SetText (char* text) = 0; virtual HRESULT Clear() = 0; }; diff --git a/native/Avalonia.Native/src/OSX/AvnString.mm b/native/Avalonia.Native/src/OSX/AvnString.mm index a16c286634..b491cf2a92 100644 --- a/native/Avalonia.Native/src/OSX/AvnString.mm +++ b/native/Avalonia.Native/src/OSX/AvnString.mm @@ -21,7 +21,7 @@ public: _string = string; } - virtual HRESULT GetPointer(void**retOut) override + virtual HRESULT Pointer(void**retOut) override { @autoreleasepool { @@ -36,7 +36,7 @@ public: } } - virtual HRESULT GetLength(int*retOut) override + virtual HRESULT Length(int*retOut) override { if(retOut == nullptr) { diff --git a/native/Avalonia.Native/src/OSX/clipboard.mm b/native/Avalonia.Native/src/OSX/clipboard.mm index f941e8ca6c..8f95433f64 100644 --- a/native/Avalonia.Native/src/OSX/clipboard.mm +++ b/native/Avalonia.Native/src/OSX/clipboard.mm @@ -8,18 +8,11 @@ class Clipboard : public ComSingleObject { public: FORWARD_IUNKNOWN() - virtual HRESULT GetText (IAvnString** retOut) override + virtual IAvnString* GetText () override { @autoreleasepool { - if(retOut == nullptr) - { - return E_POINTER; - } - - *retOut = CreateAvnString([[NSPasteboard generalPasteboard] stringForType:NSPasteboardTypeString]); - - return S_OK; + return CreateAvnString([[NSPasteboard generalPasteboard] stringForType:NSPasteboardTypeString]); } } diff --git a/src/Avalonia.Native/ClipboardImpl.cs b/src/Avalonia.Native/ClipboardImpl.cs index a2a1416645..9a49976683 100644 --- a/src/Avalonia.Native/ClipboardImpl.cs +++ b/src/Avalonia.Native/ClipboardImpl.cs @@ -26,11 +26,12 @@ namespace Avalonia.Native public unsafe Task GetTextAsync() { - var text = _native.GetText(); - - var result = System.Text.Encoding.UTF8.GetString((byte*)text.GetPointer(), text.GetLength()); + using (var text = _native.GetText()) + { + var result = System.Text.Encoding.UTF8.GetString((byte*)text.Pointer(), text.Length()); - return Task.FromResult(result); + return Task.FromResult(result); + } } public Task SetTextAsync(string text) From d06ed4a7dba0729b303b84b02014052b0b0e0718 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 8 Nov 2018 17:38:28 +0000 Subject: [PATCH 15/42] [OSX] correctly use UTF8 encoded text for passing strings. SetTitle on Window, SetText on Clipboard. --- native/Avalonia.Native/inc/avalonia-native.h | 4 ++-- .../xcshareddata/IDEWorkspaceChecks.plist | 8 ++++++++ native/Avalonia.Native/src/OSX/clipboard.mm | 4 ++-- native/Avalonia.Native/src/OSX/window.mm | 4 ++-- src/Avalonia.Native/ClipboardImpl.cs | 6 +++++- src/Avalonia.Native/WindowImpl.cs | 6 +++++- 6 files changed, 24 insertions(+), 8 deletions(-) create mode 100644 native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/native/Avalonia.Native/inc/avalonia-native.h b/native/Avalonia.Native/inc/avalonia-native.h index b9e16e6643..f353509346 100644 --- a/native/Avalonia.Native/inc/avalonia-native.h +++ b/native/Avalonia.Native/inc/avalonia-native.h @@ -216,7 +216,7 @@ AVNCOM(IAvnWindow, 04) : virtual IAvnWindowBase virtual HRESULT ShowDialog (IUnknown**ppv) = 0; virtual HRESULT SetCanResize(bool value) = 0; virtual HRESULT SetHasDecorations(bool value) = 0; - virtual HRESULT SetTitle (const char* title) = 0; + virtual HRESULT SetTitle (void* utf8Title) = 0; virtual HRESULT SetTitleBarColor (AvnColor color) = 0; virtual HRESULT SetWindowState(AvnWindowState state) = 0; virtual HRESULT GetWindowState(AvnWindowState*ret) = 0; @@ -322,7 +322,7 @@ AVNCOM(IAvnScreens, 0e) : IUnknown AVNCOM(IAvnClipboard, 0f) : IUnknown { virtual IAvnString* GetText () = 0; - virtual HRESULT SetText (char* text) = 0; + virtual HRESULT SetText (void* utf8Text) = 0; virtual HRESULT Clear() = 0; }; diff --git a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000000..18d981003d --- /dev/null +++ b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/native/Avalonia.Native/src/OSX/clipboard.mm b/native/Avalonia.Native/src/OSX/clipboard.mm index 8f95433f64..be77ff52d8 100644 --- a/native/Avalonia.Native/src/OSX/clipboard.mm +++ b/native/Avalonia.Native/src/OSX/clipboard.mm @@ -16,13 +16,13 @@ public: } } - virtual HRESULT SetText (char* text) override + virtual HRESULT SetText (void* utf8String) override { @autoreleasepool { NSPasteboard *pasteBoard = [NSPasteboard generalPasteboard]; [pasteBoard clearContents]; - [pasteBoard setString:@(text) forType:NSPasteboardTypeString]; + [pasteBoard setString:[NSString stringWithUTF8String:(const char*)utf8String] forType:NSPasteboardTypeString]; } return S_OK; diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index 362b765b3d..16b21efcd5 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -530,11 +530,11 @@ private: } } - virtual HRESULT SetTitle (const char* title) override + virtual HRESULT SetTitle (void* utf8title) override { @autoreleasepool { - _lastTitle = [NSString stringWithUTF8String:title]; + _lastTitle = [NSString stringWithUTF8String:(const char*)utf8title]; [Window setTitle:_lastTitle]; [Window setTitleVisibility:NSWindowTitleVisible]; diff --git a/src/Avalonia.Native/ClipboardImpl.cs b/src/Avalonia.Native/ClipboardImpl.cs index 9a49976683..c756a6d9c2 100644 --- a/src/Avalonia.Native/ClipboardImpl.cs +++ b/src/Avalonia.Native/ClipboardImpl.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using System.Runtime.InteropServices; using Avalonia.Input.Platform; using Avalonia.Native.Interop; +using Avalonia.Platform.Interop; namespace Avalonia.Native { @@ -40,7 +41,10 @@ namespace Avalonia.Native if (text != null) { - _native.SetText(text); + using (var buffer = new Utf8Buffer(text)) + { + _native.SetText(buffer.DangerousGetHandle()); + } } return Task.CompletedTask; diff --git a/src/Avalonia.Native/WindowImpl.cs b/src/Avalonia.Native/WindowImpl.cs index 5d30408e52..3b1b4ff3f9 100644 --- a/src/Avalonia.Native/WindowImpl.cs +++ b/src/Avalonia.Native/WindowImpl.cs @@ -5,6 +5,7 @@ using System; using Avalonia.Controls; using Avalonia.Native.Interop; using Avalonia.Platform; +using Avalonia.Platform.Interop; namespace Avalonia.Native { @@ -68,7 +69,10 @@ namespace Avalonia.Native public void SetTitle(string title) { - _native.SetTitle(title); + using (var buffer = new Utf8Buffer(title)) + { + _native.SetTitle(buffer.DangerousGetHandle()); + } } public WindowState WindowState From 5af1c1784a8dd58491b9e834bf97c489a235cc95 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 8 Nov 2018 17:43:36 +0000 Subject: [PATCH 16/42] [Avalonia.Native OSX] fix api for clipboard --- native/Avalonia.Native/inc/avalonia-native.h | 2 +- native/Avalonia.Native/src/OSX/clipboard.mm | 11 +++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/native/Avalonia.Native/inc/avalonia-native.h b/native/Avalonia.Native/inc/avalonia-native.h index f353509346..0e3edaa2dc 100644 --- a/native/Avalonia.Native/inc/avalonia-native.h +++ b/native/Avalonia.Native/inc/avalonia-native.h @@ -321,7 +321,7 @@ AVNCOM(IAvnScreens, 0e) : IUnknown AVNCOM(IAvnClipboard, 0f) : IUnknown { - virtual IAvnString* GetText () = 0; + virtual HRESULT GetText (IAvnString**ppv) = 0; virtual HRESULT SetText (void* utf8Text) = 0; virtual HRESULT Clear() = 0; }; diff --git a/native/Avalonia.Native/src/OSX/clipboard.mm b/native/Avalonia.Native/src/OSX/clipboard.mm index be77ff52d8..53c1fe3c2c 100644 --- a/native/Avalonia.Native/src/OSX/clipboard.mm +++ b/native/Avalonia.Native/src/OSX/clipboard.mm @@ -8,11 +8,18 @@ class Clipboard : public ComSingleObject { public: FORWARD_IUNKNOWN() - virtual IAvnString* GetText () override + virtual HRESULT GetText (IAvnString**ppv) override { @autoreleasepool { - return CreateAvnString([[NSPasteboard generalPasteboard] stringForType:NSPasteboardTypeString]); + if(ppv == nullptr) + { + return E_POINTER; + } + + *ppv = CreateAvnString([[NSPasteboard generalPasteboard] stringForType:NSPasteboardTypeString]); + + return S_OK; } } From 7762fa6079b06850087b1d1888b7b3a463b2541a Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 8 Nov 2018 17:46:52 +0000 Subject: [PATCH 17/42] remove plist file --- .../xcshareddata/IDEWorkspaceChecks.plist | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d981003d..0000000000 --- a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - From 5cd7c1f6f4a4cc663df6fa9abe9bbd6d1140922d Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Fri, 9 Nov 2018 21:44:45 +0300 Subject: [PATCH 18/42] 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