From 269fc2a5d2ab70cf33a8bb2b019921732c41b2f4 Mon Sep 17 00:00:00 2001 From: wojciech krysiak Date: Tue, 2 Oct 2018 16:13:32 +0200 Subject: [PATCH 01/41] 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/41] 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/41] 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/41] 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/41] 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/41] 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/41] 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/41] 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/41] 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/41] 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/41] 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 da0612e66bcb2c456c9fef042d19cd7e195830ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Mon, 5 Nov 2018 22:34:40 +0100 Subject: [PATCH 12/41] Added base dark theme --- .../Accents/BaseDark.xaml | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 src/Avalonia.Themes.Default/Accents/BaseDark.xaml diff --git a/src/Avalonia.Themes.Default/Accents/BaseDark.xaml b/src/Avalonia.Themes.Default/Accents/BaseDark.xaml new file mode 100644 index 0000000000..95b351b148 --- /dev/null +++ b/src/Avalonia.Themes.Default/Accents/BaseDark.xaml @@ -0,0 +1,56 @@ + From d783d3e1fc5d09316653c88f7b7ff23b179bb0da Mon Sep 17 00:00:00 2001 From: Stano Turza Date: Tue, 6 Nov 2018 10:26:01 +0100 Subject: [PATCH 13/41] do not focus scrollbar --- src/Avalonia.Themes.Default/ScrollBar.xaml | 24 ++++++++++++------- src/Avalonia.Themes.Default/ScrollViewer.xaml | 6 +++-- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/src/Avalonia.Themes.Default/ScrollBar.xaml b/src/Avalonia.Themes.Default/ScrollBar.xaml index e128e33368..ae40929573 100644 --- a/src/Avalonia.Themes.Default/ScrollBar.xaml +++ b/src/Avalonia.Themes.Default/ScrollBar.xaml @@ -7,7 +7,8 @@ + Grid.Column="0" + Focusable="False"> @@ -21,11 +22,13 @@ Orientation="{TemplateBinding Orientation}"> + Classes="repeattrack" + Focusable="False"/> + Classes="repeattrack" + Focusable="False"/> @@ -38,7 +41,8 @@ + Grid.Column="2" + Focusable="False"> @@ -58,7 +62,8 @@ + Grid.Column="0" + Focusable="False"> @@ -72,11 +77,13 @@ Orientation="{TemplateBinding Orientation}"> + Classes="repeattrack" + Focusable="False"/> + Classes="repeattrack" + Focusable="False"/> @@ -89,7 +96,8 @@ + Grid.Column="2" + Focusable="False"> diff --git a/src/Avalonia.Themes.Default/ScrollViewer.xaml b/src/Avalonia.Themes.Default/ScrollViewer.xaml index c493fb32e3..63440921d6 100644 --- a/src/Avalonia.Themes.Default/ScrollViewer.xaml +++ b/src/Avalonia.Themes.Default/ScrollViewer.xaml @@ -19,14 +19,16 @@ Value="{TemplateBinding HorizontalScrollBarValue, Mode=TwoWay}" ViewportSize="{TemplateBinding HorizontalScrollBarViewportSize}" Visibility="{TemplateBinding HorizontalScrollBarVisibility}" - Grid.Row="1"/> + Grid.Row="1" + Focusable="False"/> + Grid.Column="1" + Focusable="False"/> From c05edaebdccb8ba2df445685aa93bb291c3899f5 Mon Sep 17 00:00:00 2001 From: Stano Turza Date: Tue, 6 Nov 2018 10:26:25 +0100 Subject: [PATCH 14/41] scrollbar PageUp/PageDown handling --- src/Avalonia.Controls/Primitives/ScrollBar.cs | 14 ++++++++++++++ src/Avalonia.Controls/ScrollViewer.cs | 15 +++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/src/Avalonia.Controls/Primitives/ScrollBar.cs b/src/Avalonia.Controls/Primitives/ScrollBar.cs index 1e290107bb..f0d8c81808 100644 --- a/src/Avalonia.Controls/Primitives/ScrollBar.cs +++ b/src/Avalonia.Controls/Primitives/ScrollBar.cs @@ -128,6 +128,20 @@ namespace Avalonia.Controls.Primitives } } + protected override void OnKeyDown(KeyEventArgs e) + { + if (e.Key == Key.PageUp) + { + LargeDecrement(); + e.Handled = true; + } + else if (e.Key == Key.PageDown) + { + LargeIncrement(); + e.Handled = true; + } + } + protected override void OnTemplateApplied(TemplateAppliedEventArgs e) { base.OnTemplateApplied(e); diff --git a/src/Avalonia.Controls/ScrollViewer.cs b/src/Avalonia.Controls/ScrollViewer.cs index 39854e0071..264b1fd2ce 100644 --- a/src/Avalonia.Controls/ScrollViewer.cs +++ b/src/Avalonia.Controls/ScrollViewer.cs @@ -4,6 +4,7 @@ using System; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; +using Avalonia.Input; namespace Avalonia.Controls { @@ -441,5 +442,19 @@ namespace Avalonia.Controls RaisePropertyChanged(VerticalScrollBarValueProperty, 0, VerticalScrollBarValue); RaisePropertyChanged(VerticalScrollBarViewportSizeProperty, 0, VerticalScrollBarViewportSize); } + + protected override void OnKeyDown(KeyEventArgs e) + { + if (e.Key == Key.PageUp) + { + VerticalScrollBarValue = Math.Max(_offset.Y - _viewport.Height, 0); + e.Handled = true; + } + else if (e.Key == Key.PageDown) + { + VerticalScrollBarValue = Math.Min(_offset.Y + _viewport.Height, VerticalScrollBarMaximum); + e.Handled = true; + } + } } } From aefec666116e3a4baf2163942b806b6f4b9dcf9a Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 6 Nov 2018 10:36:08 +0000 Subject: [PATCH 15/41] [OSX] reset lastKeyHandled flag when performKeyEquivalent is called since this indicated a key being pressed also. --- native/Avalonia.Native/src/OSX/window.mm | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index 76243493c4..362b765b3d 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -963,7 +963,11 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent - (BOOL)performKeyEquivalent:(NSEvent *)event { - return _lastKeyHandled; + bool result = _lastKeyHandled; + + _lastKeyHandled = false; + + return result; } - (void)keyDown:(NSEvent *)event From c1fda553e6ac7c841e34feb678f986f405a1fd75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Tue, 6 Nov 2018 12:01:20 +0100 Subject: [PATCH 16/41] Fix header styles --- samples/ControlCatalog/App.xaml | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/samples/ControlCatalog/App.xaml b/samples/ControlCatalog/App.xaml index 95d515ec60..19a22bb6ed 100644 --- a/samples/ControlCatalog/App.xaml +++ b/samples/ControlCatalog/App.xaml @@ -2,23 +2,16 @@ - - - - - - + + From d07217b59a1920bbb5f6f44d04249bd96e65e0d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Tue, 6 Nov 2018 12:01:36 +0100 Subject: [PATCH 17/41] Enable full theme support for main view --- samples/ControlCatalog/MainView.xaml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index ec3bf799b4..effb805728 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -1,6 +1,9 @@ + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + Background="{DynamicResource ThemeBackgroundBrush}" + TextBlock.Foreground="{DynamicResource ThemeForegroundBrush}" + TextBlock.FontSize="{DynamicResource FontSizeNormal}"> From 779cdec09356f7c825b5f70f87b5b4732a6b0036 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 7 Nov 2018 15:24:49 +0000 Subject: [PATCH 18/41] 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 b224a21433f58540fdd24aa6ebb99bba1aa3e4d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Wed, 7 Nov 2018 16:24:55 +0100 Subject: [PATCH 19/41] Set window Foreground property to enable theming --- src/Avalonia.Themes.Default/Window.xaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Themes.Default/Window.xaml b/src/Avalonia.Themes.Default/Window.xaml index 611f8ebece..2514422ce8 100644 --- a/src/Avalonia.Themes.Default/Window.xaml +++ b/src/Avalonia.Themes.Default/Window.xaml @@ -1,5 +1,6 @@ \ No newline at end of file + From c9aff424a5c3fd4c18042100b1c63513db4918b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Wed, 7 Nov 2018 16:25:27 +0100 Subject: [PATCH 20/41] Remove not required properties --- samples/ControlCatalog/MainView.xaml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index effb805728..ec3bf799b4 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -1,9 +1,6 @@ + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> From c3ec5c543f7cc3d2083120a6267ee0ebff7aef03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Wed, 7 Nov 2018 17:16:01 +0100 Subject: [PATCH 21/41] Added user control styling support --- src/Avalonia.Controls/UserControl.cs | 2 +- src/Avalonia.Themes.Default/DefaultTheme.xaml | 1 + src/Avalonia.Themes.Default/UserControl.xaml | 15 +++++++++++++++ 3 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 src/Avalonia.Themes.Default/UserControl.xaml diff --git a/src/Avalonia.Controls/UserControl.cs b/src/Avalonia.Controls/UserControl.cs index e063a65e09..3f51f613a4 100644 --- a/src/Avalonia.Controls/UserControl.cs +++ b/src/Avalonia.Controls/UserControl.cs @@ -28,7 +28,7 @@ namespace Avalonia.Controls } /// - Type IStyleable.StyleKey => typeof(ContentControl); + Type IStyleable.StyleKey => typeof(UserControl); /// void INameScope.Register(string name, object element) diff --git a/src/Avalonia.Themes.Default/DefaultTheme.xaml b/src/Avalonia.Themes.Default/DefaultTheme.xaml index 2b9132ee56..0bd91c8f1e 100644 --- a/src/Avalonia.Themes.Default/DefaultTheme.xaml +++ b/src/Avalonia.Themes.Default/DefaultTheme.xaml @@ -35,6 +35,7 @@ + diff --git a/src/Avalonia.Themes.Default/UserControl.xaml b/src/Avalonia.Themes.Default/UserControl.xaml new file mode 100644 index 0000000000..2bf5f19698 --- /dev/null +++ b/src/Avalonia.Themes.Default/UserControl.xaml @@ -0,0 +1,15 @@ + From 06a6059a1e4b50204ca5162b7eb1f77045b79a2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Wed, 7 Nov 2018 17:27:56 +0100 Subject: [PATCH 22/41] Fix failing test --- tests/Avalonia.Controls.UnitTests/UserControlTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Avalonia.Controls.UnitTests/UserControlTests.cs b/tests/Avalonia.Controls.UnitTests/UserControlTests.cs index 738c54594e..36fde09cdb 100644 --- a/tests/Avalonia.Controls.UnitTests/UserControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/UserControlTests.cs @@ -23,7 +23,7 @@ namespace Avalonia.Controls.UnitTests { Styles = { - new Style(x => x.OfType()) + new Style(x => x.OfType()) { Setters = new[] { From 55486faa43c363852bb1d04ed24d6405a10e9ccd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Wed, 7 Nov 2018 17:32:04 +0100 Subject: [PATCH 23/41] Fix type --- tests/Avalonia.Controls.UnitTests/UserControlTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Avalonia.Controls.UnitTests/UserControlTests.cs b/tests/Avalonia.Controls.UnitTests/UserControlTests.cs index 36fde09cdb..6da771217c 100644 --- a/tests/Avalonia.Controls.UnitTests/UserControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/UserControlTests.cs @@ -40,7 +40,7 @@ namespace Avalonia.Controls.UnitTests private FuncControlTemplate GetTemplate() { - return new FuncControlTemplate(parent => + return new FuncControlTemplate(parent => { return new Border { From 26fa4f0463dc0b8adec78a6961d2fb962af40a1a Mon Sep 17 00:00:00 2001 From: ahopper Date: Thu, 8 Nov 2018 09:22:47 +0000 Subject: [PATCH 24/41] :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 25/41] [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 26/41] [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 27/41] [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 28/41] 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 a7a5e4a5d18682729e3136f2dd5f490dbc5e7554 Mon Sep 17 00:00:00 2001 From: Lucas Ontivero Date: Fri, 9 Nov 2018 17:26:21 -0300 Subject: [PATCH 29/41] Fixes InvalidOperationException error Checks the element is attached to the visual tree as part of the Hit test --- src/Avalonia.Input/InputExtensions.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Input/InputExtensions.cs b/src/Avalonia.Input/InputExtensions.cs index a861bae528..f184e41998 100644 --- a/src/Avalonia.Input/InputExtensions.cs +++ b/src/Avalonia.Input/InputExtensions.cs @@ -45,7 +45,8 @@ namespace Avalonia.Input return element != null && element.IsVisible && element.IsHitTestVisible && - element.IsEnabledCore; + element.IsEnabledCore && + element.IsAttachedToVisualTree; } } } From 2fb5e7dcde47ae33758ae9e8e46e32f3ca9bfe51 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sun, 11 Nov 2018 17:33:22 +0300 Subject: [PATCH 30/41] Explicitly register CultureInfoConverter since on some machines/platforms it doesn't get recognized automatically --- src/Markup/Avalonia.Markup.Xaml/AvaloniaTypeConverters.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Markup/Avalonia.Markup.Xaml/AvaloniaTypeConverters.cs b/src/Markup/Avalonia.Markup.Xaml/AvaloniaTypeConverters.cs index 36c03be65a..13eb3f1ae3 100644 --- a/src/Markup/Avalonia.Markup.Xaml/AvaloniaTypeConverters.cs +++ b/src/Markup/Avalonia.Markup.Xaml/AvaloniaTypeConverters.cs @@ -1,6 +1,7 @@ using System; using System.ComponentModel; using System.Collections.Generic; +using System.Globalization; using Avalonia.Collections; using Avalonia.Controls; using Avalonia.Markup.Xaml.Converters; @@ -36,6 +37,7 @@ namespace Avalonia.Markup.Xaml { typeof(Selector), typeof(SelectorTypeConverter) }, { typeof(TimeSpan), typeof(TimeSpanTypeConverter) }, { typeof(WindowIcon), typeof(IconTypeConverter) }, + { typeof(CultureInfo), typeof(CultureInfoConverter)} }; /// From 06dd72e9c833f6fcf32e5f54be8287432c30ca5b Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sun, 11 Nov 2018 18:16:36 +0300 Subject: [PATCH 31/41] Enforce DeferredRenderer to produce a frame when requested by OS --- .../AvaloniaNativeDeferredRendererLock.cs | 23 ++++ src/Avalonia.Native/DeferredRendererProxy.cs | 108 ------------------ src/Avalonia.Native/WindowImplBase.cs | 4 +- .../Rendering/DeferredRenderer.cs | 26 ++++- .../Rendering/IDeferredRendererLock.cs | 9 ++ .../Rendering/ManagedDeferredRendererLock.cs | 17 +++ src/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs | 2 + src/Windows/Avalonia.Win32/WindowImpl.cs | 5 +- 8 files changed, 77 insertions(+), 117 deletions(-) create mode 100644 src/Avalonia.Native/AvaloniaNativeDeferredRendererLock.cs delete mode 100644 src/Avalonia.Native/DeferredRendererProxy.cs create mode 100644 src/Avalonia.Visuals/Rendering/IDeferredRendererLock.cs create mode 100644 src/Avalonia.Visuals/Rendering/ManagedDeferredRendererLock.cs diff --git a/src/Avalonia.Native/AvaloniaNativeDeferredRendererLock.cs b/src/Avalonia.Native/AvaloniaNativeDeferredRendererLock.cs new file mode 100644 index 0000000000..4e84c69eae --- /dev/null +++ b/src/Avalonia.Native/AvaloniaNativeDeferredRendererLock.cs @@ -0,0 +1,23 @@ +using System; +using System.Reactive.Disposables; +using Avalonia.Native.Interop; +using Avalonia.Rendering; + +namespace Avalonia.Native +{ + public class AvaloniaNativeDeferredRendererLock : IDeferredRendererLock + { + private readonly IAvnWindowBase _window; + + public AvaloniaNativeDeferredRendererLock(IAvnWindowBase window) + { + _window = window; + } + public IDisposable TryLock() + { + if (_window.TryLock()) + Disposable.Create(() => _window.Unlock()); + return null; + } + } +} diff --git a/src/Avalonia.Native/DeferredRendererProxy.cs b/src/Avalonia.Native/DeferredRendererProxy.cs deleted file mode 100644 index 126a395f73..0000000000 --- a/src/Avalonia.Native/DeferredRendererProxy.cs +++ /dev/null @@ -1,108 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using System; -using System.Collections.Generic; -using Avalonia.Native.Interop; -using Avalonia.Rendering; -using Avalonia.VisualTree; - -namespace Avalonia.Native -{ - public class DeferredRendererProxy : IRenderer, IRenderLoopTask, IRenderLoop - { - public DeferredRendererProxy(IRenderRoot root, IAvnWindowBase window) - { - if (window != null) - { - _useLock = true; - window.AddRef(); -_window = new IAvnWindowBase(window.NativePointer); - } - _renderer = new DeferredRenderer(root, this); - _rendererTask = (IRenderLoopTask)_renderer; - } - - void IRenderLoop.Add(IRenderLoopTask i) - { - AvaloniaLocator.Current.GetService().Add(this); - } - - void IRenderLoop.Remove(IRenderLoopTask i) - { - AvaloniaLocator.Current.GetService().Remove(this); - } - - private DeferredRenderer _renderer; - private IRenderLoopTask _rendererTask; - private IAvnWindowBase _window; - private bool _useLock; - - public bool DrawFps{ - get => _renderer.DrawFps; - set => _renderer.DrawFps = value; - } - public bool DrawDirtyRects - { - get => _renderer.DrawDirtyRects; - set => _renderer.DrawDirtyRects = value; - } - - public bool NeedsUpdate => _rendererTask.NeedsUpdate; - - public void AddDirty(IVisual visual) => _renderer.AddDirty(visual); - - public void Dispose() - { - _renderer.Dispose(); - _window?.Dispose(); - _window = null; - } - public IEnumerable HitTest(Point p, IVisual root, Func filter) - { - return _renderer.HitTest(p, root, filter); - } - - public void Paint(Rect rect) - { - if (NeedsUpdate) - { - Update(TimeSpan.FromMilliseconds(Environment.TickCount)); - } - - Render(); - } - - public void Resized(Size size) => _renderer.Resized(size); - - public void Start() => _renderer.Start(); - - public void Stop() => _renderer.Stop(); - - public void Update(TimeSpan time) - { - _rendererTask.Update(time); - } - - public void Render() - { - if(_useLock) - { - _rendererTask.Render(); - return; - } - if (_window == null) - return; - if (!_window.TryLock()) - return; - try - { - _rendererTask.Render(); - } - finally - { - _window.Unlock(); - } - } - } -} diff --git a/src/Avalonia.Native/WindowImplBase.cs b/src/Avalonia.Native/WindowImplBase.cs index aaaba44fff..f9901e3497 100644 --- a/src/Avalonia.Native/WindowImplBase.cs +++ b/src/Avalonia.Native/WindowImplBase.cs @@ -245,7 +245,9 @@ namespace Avalonia.Native public IRenderer CreateRenderer(IRenderRoot root) { if (_deferredRendering) - return new DeferredRendererProxy(root, _gpu ? _native : null); + return new DeferredRenderer(root, AvaloniaLocator.Current.GetService(), + rendererLock: + _gpu ? new AvaloniaNativeDeferredRendererLock(_native) : null); return new ImmediateRenderer(root); } diff --git a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs index 5a6f086f7e..40be0055d3 100644 --- a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs +++ b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs @@ -36,6 +36,7 @@ namespace Avalonia.Rendering private int _lastSceneId = -1; private DisplayDirtyRects _dirtyRectsDisplay = new DisplayDirtyRects(); private IRef _currentDraw; + private readonly IDeferredRendererLock _lock; /// /// Initializes a new instance of the class. @@ -44,11 +45,13 @@ namespace Avalonia.Rendering /// The render loop. /// The scene builder to use. Optional. /// The dispatcher to use. Optional. + /// Lock object used before trying to access render target public DeferredRenderer( IRenderRoot root, IRenderLoop renderLoop, ISceneBuilder sceneBuilder = null, - IDispatcher dispatcher = null) + IDispatcher dispatcher = null, + IDeferredRendererLock rendererLock = null) { Contract.Requires(root != null); @@ -57,6 +60,7 @@ namespace Avalonia.Rendering _sceneBuilder = sceneBuilder ?? new SceneBuilder(); Layers = new RenderLayers(); _renderLoop = renderLoop; + _lock = rendererLock ?? new ManagedDeferredRendererLock(); } /// @@ -137,6 +141,10 @@ namespace Avalonia.Rendering /// public void Paint(Rect rect) { + var t = (IRenderLoopTask)this; + if(t.NeedsUpdate) + UpdateScene(); + t.Render(); } /// @@ -170,10 +178,9 @@ namespace Avalonia.Rendering void IRenderLoopTask.Render() { - using (var scene = _scene?.Clone()) - { - Render(scene?.Item); - } + using (var l = _lock.TryLock()) + if (l != null) + Render(); } /// @@ -197,6 +204,14 @@ namespace Avalonia.Rendering internal void UnitTestRender() => Render(_scene.Item); + private void Render() + { + using (var scene = _scene?.Clone()) + { + Render(scene?.Item); + } + } + private void Render(Scene scene) { bool renderOverlay = DrawDirtyRects || DrawFps; @@ -415,7 +430,6 @@ namespace Avalonia.Rendering oldScene?.Dispose(); _dirty.Clear(); - (_root as IRenderRoot)?.Invalidate(new Rect(scene.Size)); } else { diff --git a/src/Avalonia.Visuals/Rendering/IDeferredRendererLock.cs b/src/Avalonia.Visuals/Rendering/IDeferredRendererLock.cs new file mode 100644 index 0000000000..b2ed3afe6a --- /dev/null +++ b/src/Avalonia.Visuals/Rendering/IDeferredRendererLock.cs @@ -0,0 +1,9 @@ +using System; + +namespace Avalonia.Rendering +{ + public interface IDeferredRendererLock + { + IDisposable TryLock(); + } +} diff --git a/src/Avalonia.Visuals/Rendering/ManagedDeferredRendererLock.cs b/src/Avalonia.Visuals/Rendering/ManagedDeferredRendererLock.cs new file mode 100644 index 0000000000..75d8f036d6 --- /dev/null +++ b/src/Avalonia.Visuals/Rendering/ManagedDeferredRendererLock.cs @@ -0,0 +1,17 @@ +using System; +using System.Reactive.Disposables; +using System.Threading; + +namespace Avalonia.Rendering +{ + public class ManagedDeferredRendererLock : IDeferredRendererLock + { + private readonly object _lock = new object(); + public IDisposable TryLock() + { + if (Monitor.TryEnter(_lock)) + return Disposable.Create(() => Monitor.Exit(_lock)); + return null; + } + } +} diff --git a/src/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs b/src/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs index 0273f6a7d8..88642da211 100644 --- a/src/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs +++ b/src/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs @@ -265,6 +265,8 @@ namespace Avalonia.Gtk3 Paint?.Invoke(new Rect(ClientSize)); CurrentCairoContext = IntPtr.Zero; } + else + Paint?.Invoke(new Rect(ClientSize)); return true; } diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 21986bd2ba..be97d979b3 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -633,8 +633,9 @@ namespace Avalonia.Win32 case UnmanagedMethods.WindowsMessage.WM_PAINT: UnmanagedMethods.PAINTSTRUCT ps; - - if (UnmanagedMethods.BeginPaint(_hwnd, out ps) != IntPtr.Zero) + if (Win32Platform.UseDeferredRendering) + Paint.Invoke(new Rect(ClientSize)); + else if (UnmanagedMethods.BeginPaint(_hwnd, out ps) != IntPtr.Zero) { var f = Scaling; var r = ps.rcPaint; From 148ba001159ec30df3c03c95a521457583fc4af8 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sun, 11 Nov 2018 19:20:37 +0300 Subject: [PATCH 32/41] Fixed OSX locks --- src/Avalonia.Native/AvaloniaNativeDeferredRendererLock.cs | 2 +- src/Avalonia.Native/WindowImplBase.cs | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/Avalonia.Native/AvaloniaNativeDeferredRendererLock.cs b/src/Avalonia.Native/AvaloniaNativeDeferredRendererLock.cs index 4e84c69eae..6dd5337b27 100644 --- a/src/Avalonia.Native/AvaloniaNativeDeferredRendererLock.cs +++ b/src/Avalonia.Native/AvaloniaNativeDeferredRendererLock.cs @@ -16,7 +16,7 @@ namespace Avalonia.Native public IDisposable TryLock() { if (_window.TryLock()) - Disposable.Create(() => _window.Unlock()); + return Disposable.Create(() => _window.Unlock()); return null; } } diff --git a/src/Avalonia.Native/WindowImplBase.cs b/src/Avalonia.Native/WindowImplBase.cs index f9901e3497..629c91a2e8 100644 --- a/src/Avalonia.Native/WindowImplBase.cs +++ b/src/Avalonia.Native/WindowImplBase.cs @@ -186,10 +186,6 @@ namespace Avalonia.Native void IAvnWindowBaseEvents.RunRenderPriorityJobs() { - if (_parent._deferredRendering - && _parent._lastRenderedLogicalSize != _parent.ClientSize) - // Hack to trigger Paint event on the renderer - _parent.Paint?.Invoke(new Rect()); Dispatcher.UIThread.RunJobs(DispatcherPriority.Render); } } From fb6a68e6b7319aac0c36f553ebba54f930741523 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sun, 11 Nov 2018 19:51:22 +0300 Subject: [PATCH 33/41] Disable EGL for Win32 by default since Windows 10 seems to be shipping incompatible EGL implementation --- src/Windows/Avalonia.Win32/Win32Platform.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index 103ca57cdb..cf7ebeed92 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -27,11 +27,11 @@ namespace Avalonia { public static T UseWin32( this T builder, - bool deferredRendering = true) + bool deferredRendering = true, bool allowEgl = false) where T : AppBuilderBase, new() { return builder.UseWindowingSubsystem( - () => Win32.Win32Platform.Initialize(deferredRendering), + () => Win32.Win32Platform.Initialize(deferredRendering, allowEgl), "Win32"); } } @@ -66,7 +66,7 @@ namespace Avalonia.Win32 Initialize(true); } - public static void Initialize(bool deferredRendering = true) + public static void Initialize(bool deferredRendering = true, bool allowEgl = false) { AvaloniaLocator.CurrentMutable .Bind().ToSingleton() @@ -80,7 +80,8 @@ namespace Avalonia.Win32 .Bind().ToConstant(s_instance) .Bind().ToSingleton() .Bind().ToConstant(s_instance); - Win32GlManager.Initialize(); + if (allowEgl) + Win32GlManager.Initialize(); UseDeferredRendering = deferredRendering; _uiThread = Thread.CurrentThread; From e8926b7ea8bffb64a9376d0f53fa67e222e77ceb Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sun, 11 Nov 2018 21:33:44 +0300 Subject: [PATCH 34/41] Revert WM_PAINT handling change --- src/Windows/Avalonia.Win32/WindowImpl.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index be97d979b3..f6100c6492 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -633,9 +633,7 @@ namespace Avalonia.Win32 case UnmanagedMethods.WindowsMessage.WM_PAINT: UnmanagedMethods.PAINTSTRUCT ps; - if (Win32Platform.UseDeferredRendering) - Paint.Invoke(new Rect(ClientSize)); - else if (UnmanagedMethods.BeginPaint(_hwnd, out ps) != IntPtr.Zero) + if (UnmanagedMethods.BeginPaint(_hwnd, out ps) != IntPtr.Zero) { var f = Scaling; var r = ps.rcPaint; From 860eddb3440149d22ed1a4014953855f95fa9377 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Mon, 12 Nov 2018 15:16:51 +0300 Subject: [PATCH 35/41] Fixed #2094 --- samples/ControlCatalog.Desktop/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/ControlCatalog.Desktop/Program.cs b/samples/ControlCatalog.Desktop/Program.cs index a2048005a4..329b2ab5a3 100644 --- a/samples/ControlCatalog.Desktop/Program.cs +++ b/samples/ControlCatalog.Desktop/Program.cs @@ -22,7 +22,7 @@ namespace ControlCatalog /// This method is needed for IDE previewer infrastructure /// public static AppBuilder BuildAvaloniaApp() - => AppBuilder.Configure().LogToDebug().UsePlatformDetect(); + => AppBuilder.Configure().LogToDebug().UsePlatformDetect().UseReactiveUI(); private static void ConfigureAssetAssembly(AppBuilder builder) { From c083af062d77ec6a30814aed1f27c49d6d7e3f91 Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Tue, 13 Nov 2018 02:58:53 +0200 Subject: [PATCH 36/41] add Avalonia.Layout to https://github.com/avaloniaui --- src/Avalonia.Layout/Properties/AssemblyInfo.cs | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 src/Avalonia.Layout/Properties/AssemblyInfo.cs diff --git a/src/Avalonia.Layout/Properties/AssemblyInfo.cs b/src/Avalonia.Layout/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..70fc1e9330 --- /dev/null +++ b/src/Avalonia.Layout/Properties/AssemblyInfo.cs @@ -0,0 +1,6 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using Avalonia.Metadata; + +[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Layout")] From 855bc7d1caa59710a451b51a8b3f6812d534005e Mon Sep 17 00:00:00 2001 From: wieslawsoltes Date: Wed, 14 Nov 2018 12:52:53 +0000 Subject: [PATCH 37/41] Fix ButtonSpinner styles --- .../ButtonSpinner.xaml | 28 ++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/src/Avalonia.Themes.Default/ButtonSpinner.xaml b/src/Avalonia.Themes.Default/ButtonSpinner.xaml index ddeef44011..5417d5fb0b 100644 --- a/src/Avalonia.Themes.Default/ButtonSpinner.xaml +++ b/src/Avalonia.Themes.Default/ButtonSpinner.xaml @@ -1,17 +1,17 @@ - + + From 8b0c81ac3d194ab203a9f733b4c3a9dd3abc53b3 Mon Sep 17 00:00:00 2001 From: wieslawsoltes Date: Wed, 14 Nov 2018 12:53:37 +0000 Subject: [PATCH 38/41] Fix NumericUpDown styles --- .../NumericUpDown.xaml | 27 +++++++++---------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/src/Avalonia.Themes.Default/NumericUpDown.xaml b/src/Avalonia.Themes.Default/NumericUpDown.xaml index 8c5594cee8..0433d6734b 100644 --- a/src/Avalonia.Themes.Default/NumericUpDown.xaml +++ b/src/Avalonia.Themes.Default/NumericUpDown.xaml @@ -1,10 +1,9 @@ + \ No newline at end of file From 98eb338b8dfdb424ec85933351ae0f2850dc33a0 Mon Sep 17 00:00:00 2001 From: wieslawsoltes Date: Wed, 14 Nov 2018 13:26:17 +0000 Subject: [PATCH 39/41] Updated NumericUpDown style --- src/Avalonia.Themes.Default/NumericUpDown.xaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Themes.Default/NumericUpDown.xaml b/src/Avalonia.Themes.Default/NumericUpDown.xaml index 0433d6734b..24cbb62908 100644 --- a/src/Avalonia.Themes.Default/NumericUpDown.xaml +++ b/src/Avalonia.Themes.Default/NumericUpDown.xaml @@ -1,8 +1,9 @@ From f01fa29aecf787348d294fc2ad3d991d60e11bf2 Mon Sep 17 00:00:00 2001 From: wieslawsoltes Date: Wed, 14 Nov 2018 13:28:03 +0000 Subject: [PATCH 40/41] Do not set Height --- samples/ControlCatalog/Pages/NumericUpDownPage.xaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/samples/ControlCatalog/Pages/NumericUpDownPage.xaml b/samples/ControlCatalog/Pages/NumericUpDownPage.xaml index 2bb6214b58..9b66500a2a 100644 --- a/samples/ControlCatalog/Pages/NumericUpDownPage.xaml +++ b/samples/ControlCatalog/Pages/NumericUpDownPage.xaml @@ -52,19 +52,19 @@ Minimum: + CultureInfo="{Binding #upDown.CultureInfo}" VerticalAlignment="Center" Margin="2" Width="70" HorizontalAlignment="Center"/> Maximum: + CultureInfo="{Binding #upDown.CultureInfo}" VerticalAlignment="Center" Margin="2" Width="70" HorizontalAlignment="Center"/> Increment: + Margin="2" Width="70" HorizontalAlignment="Center"/> Value: + Margin="2" Width="70" HorizontalAlignment="Center"/> @@ -72,7 +72,7 @@ Usage of NumericUpDown: From 811adc17b4f330a62e56451687aab78b70c5e0a8 Mon Sep 17 00:00:00 2001 From: wieslawsoltes Date: Wed, 14 Nov 2018 13:39:02 +0000 Subject: [PATCH 41/41] Use auto row height --- samples/ControlCatalog/Pages/NumericUpDownPage.xaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/samples/ControlCatalog/Pages/NumericUpDownPage.xaml b/samples/ControlCatalog/Pages/NumericUpDownPage.xaml index 9b66500a2a..e263f59b8d 100644 --- a/samples/ControlCatalog/Pages/NumericUpDownPage.xaml +++ b/samples/ControlCatalog/Pages/NumericUpDownPage.xaml @@ -6,7 +6,7 @@ Features: - + ShowButtonSpinner: @@ -20,7 +20,7 @@ - + FormatString: @@ -49,7 +49,7 @@ Text: - + Minimum: