diff --git a/native/Avalonia.Native/inc/avalonia-native.h b/native/Avalonia.Native/inc/avalonia-native.h index e2b594bc75..4b814a9cfb 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 Pointer(void**retOut) = 0; + virtual HRESULT Length(int*ret) = 0; +}; + AVNCOM(IAvnWindowBase, 02) : IUnknown { virtual HRESULT Show() = 0; @@ -210,7 +216,7 @@ AVNCOM(IAvnWindow, 04) : virtual IAvnWindowBase virtual HRESULT ShowDialog (IAvnWindow* parent) = 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; @@ -315,8 +321,8 @@ AVNCOM(IAvnScreens, 0e) : IUnknown AVNCOM(IAvnClipboard, 0f) : IUnknown { - virtual HRESULT GetText (void** retOut) = 0; - virtual HRESULT SetText (char* text) = 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/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..b491cf2a92 --- /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 Pointer(void**retOut) override + { + @autoreleasepool + { + if(retOut == nullptr) + { + return E_POINTER; + } + + *retOut = (void*)_string.UTF8String; + + return S_OK; + } + } + + virtual HRESULT Length(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..53c1fe3c2c 100644 --- a/native/Avalonia.Native/src/OSX/clipboard.mm +++ b/native/Avalonia.Native/src/OSX/clipboard.mm @@ -2,29 +2,34 @@ // 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**ppv) override { @autoreleasepool { - NSString *str = [[NSPasteboard generalPasteboard] stringForType:NSPasteboardTypeString]; - *retOut = (void *)str.UTF8String; + if(ppv == nullptr) + { + return E_POINTER; + } + + *ppv = CreateAvnString([[NSPasteboard generalPasteboard] stringForType:NSPasteboardTypeString]); + + return S_OK; } - - return S_OK; } - 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 7d0533e6f3..6a8043f39a 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -502,11 +502,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]; @@ -948,7 +948,11 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent - (BOOL)performKeyEquivalent:(NSEvent *)event { - return _lastKeyHandled; + bool result = _lastKeyHandled; + + _lastKeyHandled = false; + + return result; } - (void)keyDown:(NSEvent *)event 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) { 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 @@ - - - - - - + + diff --git a/samples/ControlCatalog/Pages/NumericUpDownPage.xaml b/samples/ControlCatalog/Pages/NumericUpDownPage.xaml index 2bb6214b58..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,22 +49,22 @@ Text: - + 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: 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 1a07ccaf7e..b51583d8b3 100644 --- a/src/Avalonia.Controls/Grid.cs +++ b/src/Avalonia.Controls/Grid.cs @@ -3,10 +3,13 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; +using System.Reactive.Linq; using System.Runtime.CompilerServices; using Avalonia.Collections; using Avalonia.Controls.Utils; +using Avalonia.VisualTree; using JetBrains.Annotations; namespace Avalonia.Controls @@ -44,6 +47,24 @@ namespace Avalonia.Controls public static readonly AttachedProperty RowSpanProperty = AvaloniaProperty.RegisterAttached("RowSpan", 1); + public static readonly AttachedProperty IsSharedSizeScopeProperty = + AvaloniaProperty.RegisterAttached("IsSharedSizeScope", false); + + protected override void OnMeasureInvalidated() + { + base.OnMeasureInvalidated(); + _sharedSizeHost?.InvalidateMeasure(this); + } + + private SharedSizeScopeHost _sharedSizeHost; + + /// + /// Defines the SharedSizeScopeHost private property. + /// The ampersands are used to make accessing the property via xaml inconvenient. + /// + internal static readonly AttachedProperty s_sharedSizeScopeHostProperty = + AvaloniaProperty.RegisterAttached("&&SharedSizeScopeHost"); + private ColumnDefinitions _columnDefinitions; private RowDefinitions _rowDefinitions; @@ -51,6 +72,13 @@ namespace Avalonia.Controls static Grid() { AffectsParentMeasure(ColumnProperty, ColumnSpanProperty, RowProperty, RowSpanProperty); + IsSharedSizeScopeProperty.Changed.AddClassHandler(IsSharedSizeScopeChanged); + } + + public Grid() + { + this.AttachedToVisualTree += Grid_AttachedToVisualTree; + this.DetachedFromVisualTree += Grid_DetachedFromVisualTree; } /// @@ -77,6 +105,7 @@ namespace Avalonia.Controls _columnDefinitions = value; _columnDefinitions.TrackItemPropertyChanged(_ => InvalidateMeasure()); + _columnDefinitions.CollectionChanged += (_, __) => InvalidateMeasure(); } } @@ -104,6 +133,7 @@ namespace Avalonia.Controls _rowDefinitions = value; _rowDefinitions.TrackItemPropertyChanged(_ => InvalidateMeasure()); + _rowDefinitions.CollectionChanged += (_, __) => InvalidateMeasure(); } } @@ -271,6 +301,11 @@ namespace Avalonia.Controls _rowLayoutCache = rowLayout; _columnLayoutCache = columnLayout; + if (_sharedSizeHost?.ParticipatesInScope(this) ?? false) + { + _sharedSizeHost.UpdateMeasureStatus(this, rowResult, columnResult); + } + return new Size(columnResult.DesiredLength, rowResult.DesiredLength); // Measure each child only once. @@ -319,9 +354,21 @@ namespace Avalonia.Controls var (safeColumns, safeRows) = GetSafeColumnRows(); var columnLayout = _columnLayoutCache; var rowLayout = _rowLayoutCache; + + var rowCache = _rowMeasureCache; + var columnCache = _columnMeasureCache; + + 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); + } + // Calculate for arrange result. - var columnResult = columnLayout.Arrange(finalSize.Width, _columnMeasureCache); - var rowResult = rowLayout.Arrange(finalSize.Height, _rowMeasureCache); + var columnResult = columnLayout.Arrange(finalSize.Width, columnCache); + var rowResult = rowLayout.Arrange(finalSize.Height, rowCache); // Arrange the children. foreach (var child in Children.OfType()) { @@ -350,6 +397,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. @@ -426,5 +540,41 @@ namespace Avalonia.Controls return value; } + + /// + /// 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) + { + 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); + } + + 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()); + } + + // 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 1e4c6f2c2a..304a760216 100644 --- a/src/Avalonia.Controls/GridSplitter.cs +++ b/src/Avalonia.Controls/GridSplitter.cs @@ -44,26 +44,52 @@ 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; 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 +97,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 +105,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 +113,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 +131,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 +199,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/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; + } + } } } 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.Controls/Utils/GridLayout.cs b/src/Avalonia.Controls/Utils/GridLayout.cs index 363428b289..7704228a4e 100644 --- a/src/Avalonia.Controls/Utils/GridLayout.cs +++ b/src/Avalonia.Controls/Utils/GridLayout.cs @@ -143,14 +143,17 @@ 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. /// [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; @@ -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); } /// @@ -306,14 +309,14 @@ 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) { // 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 new file mode 100644 index 0000000000..8553165e4b --- /dev/null +++ b/src/Avalonia.Controls/Utils/SharedSizeScopeHost.cs @@ -0,0 +1,651 @@ +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.Layout; +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 + { + Invalidated, + Measuring, + 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; + 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(grid, 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)); + + } + + // 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)) + { + 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)); + } + } + + 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(Grid, db)).ToList() ?? new List(); + var oldItems = e.OldStartingIndex >= 0 + ? Results.GetRange(e.OldStartingIndex + offset, e.OldItems.Count) + : new List(); + + 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(Grid, d)) + .ToList(); + NotifyOldItems(); + NotifyNewItems(); + + break; + } + } + + + /// + /// 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; + 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]; + } + } + + /// + /// 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; + r.SizeGroup?.Reset(); + }); + } + + /// + /// 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) + { + OwningGrid = owningGrid; + Definition = definition; + 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; + + if (length.IsAbsolute) + return (MeasuredResult, 1); + if (length.IsAuto) + return (MeasuredResult, 2); + if (MinLength > 0) + return (MinLength, 3); + return (MeasuredResult, 4); + } + } + + /// + /// 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(); + + if (gatheredPriority < priority) + return; + + gatheredPriority = priority; + if (gatheredPriority == priority) + { + Length = Math.Max(length,Length); + } + else + { + Length = length; + } + } + } + + /// + /// 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) + { + Name = name; + } + + /// + /// 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)) + throw new AvaloniaInternalException( + $"SharedSizeScopeHost: Invalid call to Group.Add - The SharedSizeGroup {Name} already contains the passed result"); + + result.SizeGroup = this; + _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)) + throw new AvaloniaInternalException( + $"SharedSizeScopeHost: Invalid call to Group.Remove - The SharedSizeGroup {Name} does not contain the passed result"); + result.SizeGroup = null; + _results.Remove(result); + } + + + private double Gather() + { + var visitor = new LentgthGatherer(); + + _results.ForEach(visitor.Visit); + + return visitor.Length; + } + } + + private readonly AvaloniaList _measurementCaches = new AvaloniaList(); + private readonly Dictionary _groups = new Dictionary(); + private bool _invalidating; + + /// + /// Removes the SharedSizeScope and notifies all affected grids of the change. + /// + public void Dispose() + { + while (_measurementCaches.Any()) + _measurementCaches[0].Grid.SharedScopeChanged(); + } + + /// + /// 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); + } + + /// + /// Removes the registration for a grid in this SharedSizeScope. + /// + /// The to remove. + 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(); + } + + /// + /// 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 + if (_invalidating) + return; + _invalidating = true; + + InvalidateMeasureImpl(grid); + + _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)); + + if (cache == null) + throw new AvaloniaInternalException( + $"SharedSizeScopeHost: InvalidateMeasureImpl - called with a grid not present in the internal cache"); + + // already invalidated the cache, early out. + 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? + 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) + { + InvalidateMeasureImpl(otherGrid); + } + } + + /// + /// 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) + { + RemoveFromGroup(change.oldName, change.result); + AddToGroup(change.newName, change.result); + } + + /// + /// 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(); + var desiredLength = 0.0; + 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]; + continue; + } + + var group = _groups[definition.SharedSizeGroup]; + // Length calculated over all Definitions participating in a SharedSizeGroup. + var length = group.CalculatedLength; + + conventions[i] = new GridLayout.LengthConvention( + new GridLength(length), + measureResult.LeanLengthList[i].MinLength, + measureResult.LeanLengthList[i].MaxLength + ); + lengths[i] = length; + desiredLength += length; + } + + 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); + + foreach (var result in cache.Results) + { + var scopeName = result.Definition.SharedSizeGroup; + AddToGroup(scopeName, result); + } + } + + /// + /// 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)) + return; + + if (!_groups.TryGetValue(scopeName, out var group)) + _groups.Add(scopeName, group = new Group(scopeName)); + + group.Add(result); + } + + /// + /// 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) + { + var scopeName = result.Definition.SharedSizeGroup; + RemoveFromGroup(scopeName, result); + } + } + + /// + /// 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)) + return; + + 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()) + _groups.Remove(scopeName); + } + } +} 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; } } } 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")] diff --git a/src/Avalonia.Native/AvaloniaNativeDeferredRendererLock.cs b/src/Avalonia.Native/AvaloniaNativeDeferredRendererLock.cs new file mode 100644 index 0000000000..6dd5337b27 --- /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()) + return Disposable.Create(() => _window.Unlock()); + return null; + } + } +} diff --git a/src/Avalonia.Native/ClipboardImpl.cs b/src/Avalonia.Native/ClipboardImpl.cs index d54bc95fbb..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 { @@ -24,12 +25,14 @@ namespace Avalonia.Native return Task.CompletedTask; } - public Task GetTextAsync() + public unsafe Task GetTextAsync() { - var outPtr = _native.GetText(); - var text = Marshal.PtrToStringAnsi(outPtr); + using (var text = _native.GetText()) + { + var result = System.Text.Encoding.UTF8.GetString((byte*)text.Pointer(), text.Length()); - return Task.FromResult(text); + return Task.FromResult(result); + } } public Task SetTextAsync(string text) @@ -38,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/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/WindowImpl.cs b/src/Avalonia.Native/WindowImpl.cs index 349b4a39c7..4a3a23b4c8 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 diff --git a/src/Avalonia.Native/WindowImplBase.cs b/src/Avalonia.Native/WindowImplBase.cs index aaaba44fff..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); } } @@ -245,7 +241,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.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 @@ + diff --git a/src/Avalonia.Themes.Default/ButtonSpinner.xaml b/src/Avalonia.Themes.Default/ButtonSpinner.xaml index 8284b4eddf..5417d5fb0b 100644 --- a/src/Avalonia.Themes.Default/ButtonSpinner.xaml +++ b/src/Avalonia.Themes.Default/ButtonSpinner.xaml @@ -1,17 +1,17 @@ - \ No newline at end of file + + + + 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/NumericUpDown.xaml b/src/Avalonia.Themes.Default/NumericUpDown.xaml index 8c5594cee8..24cbb62908 100644 --- a/src/Avalonia.Themes.Default/NumericUpDown.xaml +++ b/src/Avalonia.Themes.Default/NumericUpDown.xaml @@ -1,10 +1,10 @@ + \ No newline at end of file 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"/> 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 @@ + 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 + 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 8db8a74e07..6d4544bfbd 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; } @@ -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/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)} }; /// 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; diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 20e3164413..4c08e985cd 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -649,7 +649,6 @@ namespace Avalonia.Win32 case UnmanagedMethods.WindowsMessage.WM_PAINT: UnmanagedMethods.PAINTSTRUCT ps; - if (UnmanagedMethods.BeginPaint(_hwnd, out ps) != IntPtr.Zero) { var f = Scaling; diff --git a/tests/Avalonia.Controls.UnitTests/SharedSizeScopeTests.cs b/tests/Avalonia.Controls.UnitTests/SharedSizeScopeTests.cs new file mode 100644 index 0000000000..715e9da880 --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/SharedSizeScopeTests.cs @@ -0,0 +1,284 @@ +using System.Collections.Generic; +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); + } + + [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) + { + 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; + } + + 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; + } + } +} diff --git a/tests/Avalonia.Controls.UnitTests/UserControlTests.cs b/tests/Avalonia.Controls.UnitTests/UserControlTests.cs index 738c54594e..6da771217c 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[] { @@ -40,7 +40,7 @@ namespace Avalonia.Controls.UnitTests private FuncControlTemplate GetTemplate() { - return new FuncControlTemplate(parent => + return new FuncControlTemplate(parent => { return new Border {