committed by
GitHub
82 changed files with 2646 additions and 743 deletions
@ -0,0 +1,5 @@ |
|||||
|
<ProjectConfiguration> |
||||
|
<Settings> |
||||
|
<IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely> |
||||
|
</Settings> |
||||
|
</ProjectConfiguration> |
||||
@ -0,0 +1,124 @@ |
|||||
|
<UserControl xmlns="https://github.com/avaloniaui"> |
||||
|
<DockPanel> |
||||
|
<TextBlock |
||||
|
DockPanel.Dock="Top" |
||||
|
Classes="h1" |
||||
|
Text="TabControl" |
||||
|
Margin="4"> |
||||
|
</TextBlock> |
||||
|
<TextBlock |
||||
|
DockPanel.Dock="Top" |
||||
|
Classes="h2" |
||||
|
Text="A tab control that displays a tab strip along with the content of the selected tab" |
||||
|
Margin="4"> |
||||
|
</TextBlock> |
||||
|
<Grid |
||||
|
ColumnDefinitions="*,*" |
||||
|
RowDefinitions="*,100"> |
||||
|
<DockPanel |
||||
|
Grid.Column="0" |
||||
|
Margin="4"> |
||||
|
<TextBlock |
||||
|
DockPanel.Dock="Top" |
||||
|
Classes="h1" |
||||
|
Text="From Inline TabItems"> |
||||
|
</TextBlock> |
||||
|
<TabControl |
||||
|
Margin="0 16" |
||||
|
TabStripPlacement="{Binding TabPlacement}"> |
||||
|
<TabItem> |
||||
|
<TabItem.Header> |
||||
|
<TextBlock |
||||
|
Text="Arch" |
||||
|
VerticalAlignment="Center" |
||||
|
HorizontalAlignment="Center" |
||||
|
Margin="8"> |
||||
|
</TextBlock> |
||||
|
</TabItem.Header> |
||||
|
<StackPanel Orientation="Vertical" Spacing="8"> |
||||
|
<TextBlock>This is the first page in the TabControl.</TextBlock> |
||||
|
<Image Source="resm:ControlCatalog.Assets.delicate-arch-896885_640.jpg" Width="300"/> |
||||
|
</StackPanel> |
||||
|
</TabItem> |
||||
|
<TabItem> |
||||
|
<TabItem.Header> |
||||
|
<TextBlock |
||||
|
Text="Leaf" |
||||
|
VerticalAlignment="Center" |
||||
|
HorizontalAlignment="Center" |
||||
|
Margin="8"> |
||||
|
</TextBlock> |
||||
|
</TabItem.Header> |
||||
|
<StackPanel Orientation="Vertical" Spacing="8"> |
||||
|
<TextBlock>This is the second page in the TabControl.</TextBlock> |
||||
|
<Image Source="resm:ControlCatalog.Assets.maple-leaf-888807_640.jpg" Width="300"/> |
||||
|
</StackPanel> |
||||
|
</TabItem> |
||||
|
<TabItem IsEnabled="False"> |
||||
|
<TabItem.Header> |
||||
|
<TextBlock |
||||
|
Text="Disabled" |
||||
|
VerticalAlignment="Center" |
||||
|
HorizontalAlignment="Center" |
||||
|
Margin="8"> |
||||
|
</TextBlock> |
||||
|
</TabItem.Header> |
||||
|
<TextBlock>You should not see this.</TextBlock> |
||||
|
</TabItem> |
||||
|
</TabControl> |
||||
|
</DockPanel> |
||||
|
<DockPanel |
||||
|
Grid.Column="1" |
||||
|
Margin="4"> |
||||
|
<TextBlock |
||||
|
DockPanel.Dock="Top" |
||||
|
Classes="h1" |
||||
|
Text="From DataTemplate"> |
||||
|
</TextBlock> |
||||
|
<TabControl |
||||
|
Items="{Binding Tabs}" |
||||
|
Margin="0 16" |
||||
|
TabStripPlacement="{Binding TabPlacement}"> |
||||
|
<TabControl.ItemTemplate> |
||||
|
<DataTemplate> |
||||
|
<TextBlock |
||||
|
Text="{Binding Header}" |
||||
|
VerticalAlignment="Center" |
||||
|
HorizontalAlignment="Center" |
||||
|
Margin="8"> |
||||
|
</TextBlock> |
||||
|
</DataTemplate> |
||||
|
</TabControl.ItemTemplate> |
||||
|
<TabControl.ContentTemplate> |
||||
|
<DataTemplate> |
||||
|
<StackPanel Orientation="Vertical" Spacing="8"> |
||||
|
<TextBlock Text="{Binding Text}"/> |
||||
|
<Image Source="{Binding Image}" Width="300"/> |
||||
|
</StackPanel> |
||||
|
</DataTemplate> |
||||
|
</TabControl.ContentTemplate> |
||||
|
<TabControl.Styles> |
||||
|
<Style Selector="TabItem"> |
||||
|
<Setter Property="IsEnabled" Value="{Binding IsEnabled}"/> |
||||
|
</Style> |
||||
|
</TabControl.Styles> |
||||
|
</TabControl> |
||||
|
</DockPanel> |
||||
|
<StackPanel |
||||
|
Grid.Row="1" |
||||
|
Grid.ColumnSpan="2" |
||||
|
Orientation="Horizontal" |
||||
|
Spacing="8" |
||||
|
HorizontalAlignment="Center" |
||||
|
VerticalAlignment="Center"> |
||||
|
<TextBlock VerticalAlignment="Center">Tab Placement:</TextBlock> |
||||
|
<DropDown SelectedIndex="{Binding TabPlacement, Mode=TwoWay}"> |
||||
|
<DropDownItem>Left</DropDownItem> |
||||
|
<DropDownItem>Bottom</DropDownItem> |
||||
|
<DropDownItem>Right</DropDownItem> |
||||
|
<DropDownItem>Top</DropDownItem> |
||||
|
</DropDown> |
||||
|
</StackPanel> |
||||
|
</Grid> |
||||
|
</DockPanel> |
||||
|
</UserControl> |
||||
@ -0,0 +1,80 @@ |
|||||
|
using System; |
||||
|
|
||||
|
using Avalonia; |
||||
|
using Avalonia.Controls; |
||||
|
using Avalonia.Markup.Xaml; |
||||
|
using Avalonia.Media.Imaging; |
||||
|
using Avalonia.Platform; |
||||
|
|
||||
|
using ReactiveUI; |
||||
|
|
||||
|
namespace ControlCatalog.Pages |
||||
|
{ |
||||
|
using System.Collections.Generic; |
||||
|
|
||||
|
public class TabControlPage : UserControl |
||||
|
{ |
||||
|
public TabControlPage() |
||||
|
{ |
||||
|
InitializeComponent(); |
||||
|
|
||||
|
DataContext = new PageViewModel |
||||
|
{ |
||||
|
Tabs = new[] |
||||
|
{ |
||||
|
new TabItemViewModel |
||||
|
{ |
||||
|
Header = "Arch", |
||||
|
Text = "This is the first templated tab page.", |
||||
|
Image = LoadBitmap("resm:ControlCatalog.Assets.delicate-arch-896885_640.jpg?assembly=ControlCatalog"), |
||||
|
}, |
||||
|
new TabItemViewModel |
||||
|
{ |
||||
|
Header = "Leaf", |
||||
|
Text = "This is the second templated tab page.", |
||||
|
Image = LoadBitmap("resm:ControlCatalog.Assets.maple-leaf-888807_640.jpg?assembly=ControlCatalog"), |
||||
|
}, |
||||
|
new TabItemViewModel |
||||
|
{ |
||||
|
Header = "Disabled", |
||||
|
Text = "You should not see this.", |
||||
|
IsEnabled = false, |
||||
|
}, |
||||
|
}, |
||||
|
TabPlacement = Dock.Top, |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
private void InitializeComponent() |
||||
|
{ |
||||
|
AvaloniaXamlLoader.Load(this); |
||||
|
} |
||||
|
|
||||
|
private IBitmap LoadBitmap(string uri) |
||||
|
{ |
||||
|
var assets = AvaloniaLocator.Current.GetService<IAssetLoader>(); |
||||
|
return new Bitmap(assets.Open(new Uri(uri))); |
||||
|
} |
||||
|
|
||||
|
private class PageViewModel : ReactiveObject |
||||
|
{ |
||||
|
private Dock _tabPlacement; |
||||
|
|
||||
|
public TabItemViewModel[] Tabs { get; set; } |
||||
|
|
||||
|
public Dock TabPlacement |
||||
|
{ |
||||
|
get { return _tabPlacement; } |
||||
|
set { this.RaiseAndSetIfChanged(ref _tabPlacement, value); } |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private class TabItemViewModel |
||||
|
{ |
||||
|
public string Header { get; set; } |
||||
|
public string Text { get; set; } |
||||
|
public IBitmap Image { get; set; } |
||||
|
public bool IsEnabled { get; set; } = true; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,52 +1,67 @@ |
|||||
<Styles xmlns="https://github.com/avaloniaui" |
<Styles xmlns="https://github.com/avaloniaui" |
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" > |
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" > |
||||
<Style Selector="TabControl.sidebar"> |
<Style Selector="TabControl.sidebar"> |
||||
<Setter Property="Template"> |
<Setter Property="TabStripPlacement" Value="Left"/> |
||||
<ControlTemplate> |
<Setter Property="Padding" Value="8 0 0 0"/> |
||||
<DockPanel> |
<Setter Property="Background" Value="{DynamicResource ThemeAccentBrush}"/> |
||||
<ScrollViewer MinWidth="190" Background="{DynamicResource ThemeAccentBrush}" DockPanel.Dock="Left"> |
<Setter Property="Template"> |
||||
<TabStrip Name="PART_TabStrip" |
<ControlTemplate> |
||||
MemberSelector="{x:Static TabControl.HeaderSelector}" |
<Border |
||||
Items="{TemplateBinding Items}" |
Margin="{TemplateBinding Margin}" |
||||
SelectedIndex="{TemplateBinding SelectedIndex, Mode=TwoWay}"> |
BorderBrush="{TemplateBinding BorderBrush}" |
||||
<TabStrip.ItemsPanel> |
BorderThickness="{TemplateBinding BorderThickness}"> |
||||
<ItemsPanelTemplate> |
<DockPanel> |
||||
<StackPanel Orientation="Vertical"/> |
<ScrollViewer |
||||
</ItemsPanelTemplate> |
Name="PART_ScrollViewer" |
||||
</TabStrip.ItemsPanel> |
HorizontalScrollBarVisibility="{TemplateBinding (ScrollViewer.HorizontalScrollBarVisibility)}" |
||||
</TabStrip> |
VerticalScrollBarVisibility="{TemplateBinding (ScrollViewer.VerticalScrollBarVisibility)}" |
||||
</ScrollViewer> |
Background="{TemplateBinding Background}"> |
||||
<Carousel Name="PART_Content" |
<ItemsPresenter |
||||
Margin="8 0 0 0" |
Name="PART_ItemsPresenter" |
||||
MemberSelector="{x:Static TabControl.ContentSelector}" |
MinWidth="190" |
||||
Items="{TemplateBinding Items}" |
Items="{TemplateBinding Items}" |
||||
SelectedIndex="{TemplateBinding SelectedIndex}" |
ItemsPanel="{TemplateBinding ItemsPanel}" |
||||
PageTransition="{TemplateBinding PageTransition}" |
ItemTemplate="{TemplateBinding ItemTemplate}" |
||||
Grid.Row="1"/> |
MemberSelector="{TemplateBinding MemberSelector}"> |
||||
</DockPanel> |
</ItemsPresenter> |
||||
</ControlTemplate> |
</ScrollViewer> |
||||
</Setter> |
<ContentPresenter |
||||
</Style> |
Name="PART_Content" |
||||
|
Margin="{TemplateBinding Padding}" |
||||
|
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}" |
||||
|
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}" |
||||
|
Content="{TemplateBinding SelectedContent}" |
||||
|
ContentTemplate="{TemplateBinding SelectedContentTemplate}"> |
||||
|
</ContentPresenter> |
||||
|
</DockPanel> |
||||
|
</Border> |
||||
|
</ControlTemplate> |
||||
|
</Setter> |
||||
|
</Style> |
||||
|
|
||||
<Style Selector="TabControl.sidebar TabStripItem"> |
<Style Selector="TabControl.sidebar > TabItem"> |
||||
<Setter Property="Foreground" Value="White"/> |
<Setter Property="BorderThickness" Value="0"/> |
||||
<Setter Property="FontSize" Value="14"/> |
<Setter Property="Foreground" Value="White"/> |
||||
<Setter Property="Margin" Value="0"/> |
<Setter Property="FontSize" Value="14"/> |
||||
<Setter Property="Padding" Value="16"/> |
<Setter Property="Margin" Value="0"/> |
||||
<Setter Property="Opacity" Value="0.5"/> |
<Setter Property="Padding" Value="16"/> |
||||
<Setter Property="Transitions"> |
<Setter Property="Opacity" Value="0.5"/> |
||||
<Transitions> |
<Setter Property="Transitions"> |
||||
<DoubleTransition Property="Opacity" Duration="0:0:0.2"/> |
<Transitions> |
||||
</Transitions> |
<DoubleTransition Property="Opacity" Duration="0:0:0.5"/> |
||||
</Setter> |
</Transitions> |
||||
</Style> |
</Setter> |
||||
|
</Style> |
||||
<Style Selector="TabControl.sidebar TabStripItem:pointerover"> |
<Style Selector="TabControl.sidebar > TabItem:pointerover"> |
||||
<Setter Property="Opacity" Value="1"/> |
<Setter Property="Opacity" Value="1"/> |
||||
</Style> |
</Style> |
||||
|
<Style Selector="TabControl.sidebar > TabItem:pointerover /template/ ContentPresenter#PART_ContentPresenter"> |
||||
<Style Selector="TabControl.sidebar TabStripItem:selected"> |
<Setter Property="Background" Value="Transparent"/> |
||||
<Setter Property="Background" Value="{DynamicResource ThemeAccentBrush2}"/> |
</Style> |
||||
<Setter Property="Opacity" Value="1"/> |
<Style Selector="TabControl.sidebar > TabItem:selected"> |
||||
</Style> |
<Setter Property="Opacity" Value="1"/> |
||||
|
</Style> |
||||
|
<Style Selector="TabControl.sidebar > TabItem:selected /template/ ContentPresenter#PART_ContentPresenter"> |
||||
|
<Setter Property="Background" Value="{DynamicResource ThemeAccentBrush2}"/> |
||||
|
</Style> |
||||
</Styles> |
</Styles> |
||||
|
|||||
@ -0,0 +1,57 @@ |
|||||
|
// Copyright (c) The Avalonia Project. All rights reserved.
|
||||
|
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
||||
|
|
||||
|
using Avalonia.Controls.Primitives; |
||||
|
|
||||
|
namespace Avalonia.Controls.Generators |
||||
|
{ |
||||
|
public class TabItemContainerGenerator : ItemContainerGenerator<TabItem> |
||||
|
{ |
||||
|
public TabItemContainerGenerator(TabControl owner) |
||||
|
: base(owner, ContentControl.ContentProperty, ContentControl.ContentTemplateProperty) |
||||
|
{ |
||||
|
Owner = owner; |
||||
|
} |
||||
|
|
||||
|
public new TabControl Owner { get; } |
||||
|
|
||||
|
protected override IControl CreateContainer(object item) |
||||
|
{ |
||||
|
var tabItem = (TabItem)base.CreateContainer(item); |
||||
|
|
||||
|
tabItem.ParentTabControl = Owner; |
||||
|
|
||||
|
if (tabItem.HeaderTemplate == null) |
||||
|
{ |
||||
|
tabItem[~HeaderedContentControl.HeaderTemplateProperty] = Owner[~ItemsControl.ItemTemplateProperty]; |
||||
|
} |
||||
|
|
||||
|
if (tabItem.Header == null) |
||||
|
{ |
||||
|
if (item is IHeadered headered) |
||||
|
{ |
||||
|
tabItem.Header = headered.Header; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
if (!(tabItem.DataContext is IControl)) |
||||
|
{ |
||||
|
tabItem.Header = tabItem.DataContext; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (!(tabItem.Content is IControl)) |
||||
|
{ |
||||
|
tabItem[~ContentControl.ContentTemplateProperty] = Owner[~TabControl.ContentTemplateProperty]; |
||||
|
} |
||||
|
|
||||
|
if (tabItem.Content == null) |
||||
|
{ |
||||
|
tabItem[~ContentControl.ContentProperty] = tabItem[~StyledElement.DataContextProperty]; |
||||
|
} |
||||
|
|
||||
|
return tabItem; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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 |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Shared size scope implementation.
|
||||
|
/// Shares the size information between participating grids.
|
||||
|
/// An instance of this class is attached to every <see cref="Control"/> that has its
|
||||
|
/// IsSharedSizeScope property set to true.
|
||||
|
/// </summary>
|
||||
|
internal sealed class SharedSizeScopeHost : IDisposable |
||||
|
{ |
||||
|
private enum MeasurementState |
||||
|
{ |
||||
|
Invalidated, |
||||
|
Measuring, |
||||
|
Cached |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// 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 <see cref="SharedSizeScopeHost"/> of SharedSizeGroup changes.
|
||||
|
/// </summary>
|
||||
|
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<DefinitionBase>() |
||||
|
.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<object, PropertyChangedEventArgs> 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<DefinitionBase>().Select(db => new MeasurementResult(Grid, db)).ToList() ?? new List<MeasurementResult>(); |
||||
|
var oldItems = e.OldStartingIndex >= 0 |
||||
|
? Results.GetRange(e.OldStartingIndex + offset, e.OldItems.Count) |
||||
|
: new List<MeasurementResult>(); |
||||
|
|
||||
|
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<DefinitionBase>() |
||||
|
.Concat(Grid.ColumnDefinitions) |
||||
|
.Select(d => new MeasurementResult(Grid, d)) |
||||
|
.ToList(); |
||||
|
NotifyOldItems(); |
||||
|
NotifyNewItems(); |
||||
|
|
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Updates the Results collection with Grid Measure results.
|
||||
|
/// </summary>
|
||||
|
/// <param name="rowResult">Result of the GridLayout.Measure method for the RowDefinitions in the grid.</param>
|
||||
|
/// <param name="columnResult">Result of the GridLayout.Measure method for the ColumnDefinitions in the grid.</param>
|
||||
|
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]; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Clears the measurement cache, in preparation for the Measure pass.
|
||||
|
/// </summary>
|
||||
|
public void InvalidateMeasure() |
||||
|
{ |
||||
|
var newItems = new List<MeasurementResult>(); |
||||
|
var oldItems = new List<MeasurementResult>(); |
||||
|
|
||||
|
MeasurementState = MeasurementState.Invalidated; |
||||
|
|
||||
|
Results.ForEach(r => |
||||
|
{ |
||||
|
r.MeasuredResult = double.NaN; |
||||
|
r.SizeGroup?.Reset(); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Clears the <see cref="IObservable{T}"/> subscriptions.
|
||||
|
/// </summary>
|
||||
|
public void Dispose() |
||||
|
{ |
||||
|
_subscriptions.Dispose(); |
||||
|
_groupChanged.OnCompleted(); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the <see cref="Grid"/> for which this cache has been created.
|
||||
|
/// </summary>
|
||||
|
public Grid Grid { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the <see cref="MeasurementState"/> of this cache.
|
||||
|
/// </summary>
|
||||
|
public MeasurementState MeasurementState { get; private set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the list of <see cref="MeasurementResult"/> instances.
|
||||
|
/// </summary>
|
||||
|
/// <remarks>
|
||||
|
/// The list is a 1-1 map of the concatenation of RowDefinitions and ColumnDefinitions
|
||||
|
/// </remarks>
|
||||
|
public List<MeasurementResult> Results { get; private set; } |
||||
|
} |
||||
|
|
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Class containing the Measure result for a single Row/Column in a grid.
|
||||
|
/// </summary>
|
||||
|
private class MeasurementResult |
||||
|
{ |
||||
|
public MeasurementResult(Grid owningGrid, DefinitionBase definition) |
||||
|
{ |
||||
|
OwningGrid = owningGrid; |
||||
|
Definition = definition; |
||||
|
MeasuredResult = double.NaN; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the <see cref="RowDefinition"/>/<see cref="ColumnDefinition"/> related to this <see cref="MeasurementResult"/>
|
||||
|
/// </summary>
|
||||
|
public DefinitionBase Definition { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the actual result of the Measure operation for this column.
|
||||
|
/// </summary>
|
||||
|
public double MeasuredResult { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the Minimum constraint for a Row/Column - relevant for star Rows/Columns in unconstrained grids.
|
||||
|
/// </summary>
|
||||
|
public double MinLength { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the <see cref="Group"/> that this result belongs to.
|
||||
|
/// </summary>
|
||||
|
public Group SizeGroup { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the Grid that is the parent of the Row/Column
|
||||
|
/// </summary>
|
||||
|
public Grid OwningGrid { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Calculates the effective length that this Row/Column wishes to enforce in the SharedSizeGroup.
|
||||
|
/// </summary>
|
||||
|
/// <returns>A tuple of length and the priority in the shared size group.</returns>
|
||||
|
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); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Visitor class used to gather the final length for a given SharedSizeGroup.
|
||||
|
/// </summary>
|
||||
|
/// <remarks>
|
||||
|
/// The values are applied according to priorities defined in <see cref="MeasurementResult.GetPriorityLength"/>.
|
||||
|
/// </remarks>
|
||||
|
private class LentgthGatherer |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Gets the final Length to be applied to every Row/Column in a SharedSizeGroup
|
||||
|
/// </summary>
|
||||
|
public double Length { get; private set; } |
||||
|
private int gatheredPriority = 6; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Visits the <paramref name="result"/> applying the result of <see cref="MeasurementResult.GetPriorityLength"/> to its internal cache.
|
||||
|
/// </summary>
|
||||
|
/// <param name="result">The <see cref="MeasurementResult"/> instance to visit.</param>
|
||||
|
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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Representation of a SharedSizeGroup, containing Rows/Columns with the same SharedSizeGroup property value.
|
||||
|
/// </summary>
|
||||
|
private class Group |
||||
|
{ |
||||
|
private double? cachedResult; |
||||
|
private List<MeasurementResult> _results = new List<MeasurementResult>(); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the name of the SharedSizeGroup.
|
||||
|
/// </summary>
|
||||
|
public string Name { get; } |
||||
|
|
||||
|
public Group(string name) |
||||
|
{ |
||||
|
Name = name; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the collection of the <see cref="MeasurementResult"/> instances.
|
||||
|
/// </summary>
|
||||
|
public IReadOnlyList<MeasurementResult> Results => _results; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the final, calculated length for all Rows/Columns in the SharedSizeGroup.
|
||||
|
/// </summary>
|
||||
|
public double CalculatedLength => (cachedResult ?? (cachedResult = Gather())).Value; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Clears the previously cached result in preparation for measurement.
|
||||
|
/// </summary>
|
||||
|
public void Reset() |
||||
|
{ |
||||
|
cachedResult = null; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Ads a measurement result to this group and sets it's <see cref="MeasurementResult.SizeGroup"/> property
|
||||
|
/// to this instance.
|
||||
|
/// </summary>
|
||||
|
/// <param name="result">The <see cref="MeasurementResult"/> to include in this group.</param>
|
||||
|
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); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Removes the measurement result from this group and clears its <see cref="MeasurementResult.SizeGroup"/> value.
|
||||
|
/// </summary>
|
||||
|
/// <param name="result">The <see cref="MeasurementResult"/> to clear.</param>
|
||||
|
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<MeasurementCache> _measurementCaches = new AvaloniaList<MeasurementCache>(); |
||||
|
private readonly Dictionary<string, Group> _groups = new Dictionary<string, Group>(); |
||||
|
private bool _invalidating; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Removes the SharedSizeScope and notifies all affected grids of the change.
|
||||
|
/// </summary>
|
||||
|
public void Dispose() |
||||
|
{ |
||||
|
while (_measurementCaches.Any()) |
||||
|
_measurementCaches[0].Grid.SharedScopeChanged(); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Registers the grid in this SharedSizeScope, to be called when the grid is added to the visual tree.
|
||||
|
/// </summary>
|
||||
|
/// <param name="toAdd">The <see cref="Grid"/> to add to this scope.</param>
|
||||
|
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); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Removes the registration for a grid in this SharedSizeScope.
|
||||
|
/// </summary>
|
||||
|
/// <param name="toRemove">The <see cref="Grid"/> to remove.</param>
|
||||
|
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(); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Helper method to check if a grid needs to forward its Mesure results to, and requrest Arrange results from this scope.
|
||||
|
/// </summary>
|
||||
|
/// <param name="toCheck">The <see cref="Grid"/> that should be checked.</param>
|
||||
|
/// <returns>True if the grid should forward its calls.</returns>
|
||||
|
internal bool ParticipatesInScope(Grid toCheck) |
||||
|
{ |
||||
|
return _measurementCaches.FirstOrDefault(mc => ReferenceEquals(mc.Grid, toCheck)) |
||||
|
?.Results.Any(r => r.SizeGroup != null) ?? false; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Notifies the SharedSizeScope that a grid had requested its measurement to be invalidated.
|
||||
|
/// Forwards the same call to all affected grids in this scope.
|
||||
|
/// </summary>
|
||||
|
/// <param name="grid">The <see cref="Grid"/> that had it's Measure invalidated.</param>
|
||||
|
internal void InvalidateMeasure(Grid grid) |
||||
|
{ |
||||
|
// prevent stack overflow
|
||||
|
if (_invalidating) |
||||
|
return; |
||||
|
_invalidating = true; |
||||
|
|
||||
|
InvalidateMeasureImpl(grid); |
||||
|
|
||||
|
_invalidating = false; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Updates the measurement cache with the results of the <paramref name="grid"/> measurement pass.
|
||||
|
/// </summary>
|
||||
|
/// <param name="grid">The <see cref="Grid"/> that has been measured.</param>
|
||||
|
/// <param name="rowResult">Measurement result for the grid's <see cref="RowDefinitions"/></param>
|
||||
|
/// <param name="columnResult">Measurement result for the grid's <see cref="ColumnDefinitions"/></param>
|
||||
|
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); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Calculates the measurement result including the impact of any SharedSizeGroups that might affect this grid.
|
||||
|
/// </summary>
|
||||
|
/// <param name="grid">The <see cref="Grid"/> that is being Arranged</param>
|
||||
|
/// <param name="rowResult">The <paramref name="grid"/>'s cached measurement result.</param>
|
||||
|
/// <param name="columnResult">The <paramref name="grid"/>'s cached measurement result.</param>
|
||||
|
/// <returns>Row and column measurement result updated with the SharedSizeScope constraints.</returns>
|
||||
|
internal (GridLayout.MeasureResult, GridLayout.MeasureResult) HandleArrange(Grid grid, GridLayout.MeasureResult rowResult, GridLayout.MeasureResult columnResult) |
||||
|
{ |
||||
|
return ( |
||||
|
Arrange(grid.RowDefinitions, rowResult), |
||||
|
Arrange(grid.ColumnDefinitions, columnResult) |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Invalidates the measure of all grids affected by the SharedSizeGroups contained within.
|
||||
|
/// </summary>
|
||||
|
/// <param name="grid">The <see cref="Grid"/> that is being invalidated.</param>
|
||||
|
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); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// <see cref="IObserver{T}"/> callback notifying the scope that a <see cref="MeasurementResult"/> has changed its
|
||||
|
/// SharedSizeGroup
|
||||
|
/// </summary>
|
||||
|
/// <param name="change">Old and New name (either can be null) of the SharedSizeGroup, as well as the result.</param>
|
||||
|
private void SharedGroupChanged((string oldName, string newName, MeasurementResult result) change) |
||||
|
{ |
||||
|
RemoveFromGroup(change.oldName, change.result); |
||||
|
AddToGroup(change.newName, change.result); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Handles the impact of SharedSizeGroups on the Arrange of <see cref="RowDefinitions"/>/<see cref="ColumnDefinitions"/>
|
||||
|
/// </summary>
|
||||
|
/// <param name="definitions">Rows/Columns that were measured</param>
|
||||
|
/// <param name="measureResult">The initial measurement result.</param>
|
||||
|
/// <returns>Modified measure result</returns>
|
||||
|
private GridLayout.MeasureResult Arrange(IReadOnlyList<DefinitionBase> 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); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Adds all measurement results for a grid to their repsective scopes.
|
||||
|
/// </summary>
|
||||
|
/// <param name="cache">The <see cref="MeasurementCache"/> for a grid to be added.</param>
|
||||
|
private void AddGridToScopes(MeasurementCache cache) |
||||
|
{ |
||||
|
cache.GroupChanged.Subscribe(SharedGroupChanged); |
||||
|
|
||||
|
foreach (var result in cache.Results) |
||||
|
{ |
||||
|
var scopeName = result.Definition.SharedSizeGroup; |
||||
|
AddToGroup(scopeName, result); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Handles adding the <see cref="MeasurementResult"/> to a SharedSizeGroup.
|
||||
|
/// Does nothing for empty SharedSizeGroups.
|
||||
|
/// </summary>
|
||||
|
/// <param name="scopeName">The name (can be null or empty) of the group to add the <paramref name="result"/> to.</param>
|
||||
|
/// <param name="result">The <see cref="MeasurementResult"/> to add to a scope.</param>
|
||||
|
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); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Removes all measurement results for a grid from their respective scopes.
|
||||
|
/// </summary>
|
||||
|
/// <param name="cache">The <see cref="MeasurementCache"/> for a grid to be removed.</param>
|
||||
|
private void RemoveGridFromScopes(MeasurementCache cache) |
||||
|
{ |
||||
|
foreach (var result in cache.Results) |
||||
|
{ |
||||
|
var scopeName = result.Definition.SharedSizeGroup; |
||||
|
RemoveFromGroup(scopeName, result); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Handles removing the <see cref="MeasurementResult"/> from a SharedSizeGroup.
|
||||
|
/// Does nothing for empty SharedSizeGroups.
|
||||
|
/// </summary>
|
||||
|
/// <param name="scopeName">The name (can be null or empty) of the group to remove the <paramref name="result"/> from.</param>
|
||||
|
/// <param name="result">The <see cref="MeasurementResult"/> to remove from a scope.</param>
|
||||
|
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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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")] |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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<IRenderLoop>().Add(this); |
|
||||
} |
|
||||
|
|
||||
void IRenderLoop.Remove(IRenderLoopTask i) |
|
||||
{ |
|
||||
AvaloniaLocator.Current.GetService<IRenderLoop>().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<IVisual> HitTest(Point p, IVisual root, Func<IVisual, bool> 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(); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,44 @@ |
|||||
|
using System; |
||||
|
using System.Reactive.Disposables; |
||||
|
using System.Threading; |
||||
|
|
||||
|
namespace Avalonia.OpenGL |
||||
|
{ |
||||
|
public class EglContext : IGlContext |
||||
|
{ |
||||
|
private readonly EglDisplay _disp; |
||||
|
private readonly EglInterface _egl; |
||||
|
private readonly object _lock = new object(); |
||||
|
|
||||
|
public EglContext(EglDisplay display, EglInterface egl, IntPtr ctx, IntPtr offscreenSurface) |
||||
|
{ |
||||
|
_disp = display; |
||||
|
_egl = egl; |
||||
|
Context = ctx; |
||||
|
OffscreenSurface = offscreenSurface; |
||||
|
} |
||||
|
|
||||
|
public IntPtr Context { get; } |
||||
|
public IntPtr OffscreenSurface { get; } |
||||
|
public IGlDisplay Display => _disp; |
||||
|
|
||||
|
public IDisposable Lock() |
||||
|
{ |
||||
|
Monitor.Enter(_lock); |
||||
|
return Disposable.Create(() => Monitor.Exit(_lock)); |
||||
|
} |
||||
|
|
||||
|
public void MakeCurrent() |
||||
|
{ |
||||
|
if (!_egl.MakeCurrent(_disp.Handle, IntPtr.Zero, IntPtr.Zero, Context)) |
||||
|
throw new OpenGlException("eglMakeCurrent failed"); |
||||
|
} |
||||
|
|
||||
|
public void MakeCurrent(EglSurface surface) |
||||
|
{ |
||||
|
var surf = ((EglSurface)surface)?.DangerousGetHandle() ?? OffscreenSurface; |
||||
|
if (!_egl.MakeCurrent(_disp.Handle, surf, surf, Context)) |
||||
|
throw new OpenGlException("eglMakeCurrent failed"); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,28 @@ |
|||||
|
using System; |
||||
|
using System.Runtime.InteropServices; |
||||
|
|
||||
|
namespace Avalonia.OpenGL |
||||
|
{ |
||||
|
public class EglSurface : SafeHandle |
||||
|
{ |
||||
|
private readonly EglDisplay _display; |
||||
|
private readonly EglInterface _egl; |
||||
|
|
||||
|
public EglSurface(EglDisplay display, EglInterface egl, IntPtr surface) : base(surface, true) |
||||
|
{ |
||||
|
_display = display; |
||||
|
_egl = egl; |
||||
|
} |
||||
|
|
||||
|
protected override bool ReleaseHandle() |
||||
|
{ |
||||
|
_egl.DestroySurface(_display.Handle, handle); |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
public override bool IsInvalid => handle == IntPtr.Zero; |
||||
|
|
||||
|
public IGlDisplay Display => _display; |
||||
|
public void SwapBuffers() => _egl.SwapBuffers(_display.Handle, handle); |
||||
|
} |
||||
|
} |
||||
@ -1,10 +0,0 @@ |
|||||
using System; |
|
||||
|
|
||||
namespace Avalonia.OpenGL |
|
||||
{ |
|
||||
public interface IGlSurface : IDisposable |
|
||||
{ |
|
||||
IGlDisplay Display { get; } |
|
||||
void SwapBuffers(); |
|
||||
} |
|
||||
} |
|
||||
@ -1,50 +1,56 @@ |
|||||
<Styles xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> |
<Styles xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> |
||||
<Style Selector="TabControl"> |
<Style Selector="TabControl"> |
||||
<Setter Property="Template"> |
<Setter Property="Background" Value="{DynamicResource ThemeBackgroundBrush}"/> |
||||
<ControlTemplate> |
<Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderMidBrush}"/> |
||||
<Border Background="{TemplateBinding Background}" |
<Setter Property="Padding" Value="4"/> |
||||
BorderBrush="{TemplateBinding BorderBrush}" |
<Setter Property="VerticalContentAlignment" Value="Stretch"/> |
||||
BorderThickness="{TemplateBinding BorderThickness}"> |
<Setter Property="HorizontalContentAlignment" Value="Stretch"/> |
||||
<DockPanel> |
<Setter Property="Template"> |
||||
<TabStrip Name="PART_TabStrip" |
<ControlTemplate> |
||||
MemberSelector="{x:Static TabControl.HeaderSelector}" |
<Border |
||||
Items="{TemplateBinding Items}" |
Margin="{TemplateBinding Margin}" |
||||
SelectedIndex="{TemplateBinding SelectedIndex, Mode=TwoWay}"/> |
BorderBrush="{TemplateBinding BorderBrush}" |
||||
<Carousel Name="PART_Content" |
BorderThickness="{TemplateBinding BorderThickness}" |
||||
MemberSelector="{x:Static TabControl.ContentSelector}" |
Background="{TemplateBinding Background}" |
||||
Items="{TemplateBinding Items}" |
HorizontalAlignment="{TemplateBinding HorizontalAlignment}" |
||||
SelectedIndex="{TemplateBinding SelectedIndex}" |
VerticalAlignment="{TemplateBinding VerticalAlignment}"> |
||||
PageTransition="{TemplateBinding PageTransition}" |
<DockPanel> |
||||
Grid.Row="1"/> |
<ItemsPresenter |
||||
</DockPanel> |
Name="PART_ItemsPresenter" |
||||
</Border> |
Items="{TemplateBinding Items}" |
||||
</ControlTemplate> |
ItemsPanel="{TemplateBinding ItemsPanel}" |
||||
</Setter> |
ItemTemplate="{TemplateBinding ItemTemplate}" |
||||
</Style> |
MemberSelector="{TemplateBinding MemberSelector}" > |
||||
<Style Selector="TabControl[TabStripPlacement=Top] /template/ TabStrip"> |
</ItemsPresenter> |
||||
<Setter Property="DockPanel.Dock" Value="Top"/> |
<ContentPresenter |
||||
</Style> |
Name="PART_Content" |
||||
<Style Selector="TabControl[TabStripPlacement=Bottom] /template/ TabStrip"> |
Margin="{TemplateBinding Padding}" |
||||
<Setter Property="DockPanel.Dock" Value="Bottom"/> |
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}" |
||||
</Style> |
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}" |
||||
<Style Selector="TabControl[TabStripPlacement=Left] /template/ TabStrip"> |
Content="{TemplateBinding SelectedContent}" |
||||
<Setter Property="DockPanel.Dock" Value="Left"/> |
ContentTemplate="{TemplateBinding SelectedContentTemplate}"> |
||||
<Setter Property="ItemsPanel"> |
</ContentPresenter> |
||||
<Setter.Value> |
</DockPanel> |
||||
<ItemsPanelTemplate> |
</Border> |
||||
<StackPanel Orientation="Vertical"/> |
</ControlTemplate> |
||||
</ItemsPanelTemplate> |
</Setter> |
||||
</Setter.Value> |
</Style> |
||||
</Setter> |
<Style Selector="TabControl[TabStripPlacement=Top] /template/ ItemsPresenter#PART_ItemsPresenter"> |
||||
</Style> |
<Setter Property="DockPanel.Dock" Value="Top"/> |
||||
<Style Selector="TabControl[TabStripPlacement=Right] /template/ TabStrip"> |
</Style> |
||||
<Setter Property="DockPanel.Dock" Value="Right"/> |
<Style Selector="TabControl[TabStripPlacement=Bottom] /template/ ItemsPresenter#PART_ItemsPresenter"> |
||||
<Setter Property="ItemsPanel"> |
<Setter Property="DockPanel.Dock" Value="Bottom"/> |
||||
<Setter.Value> |
</Style> |
||||
<ItemsPanelTemplate> |
<Style Selector="TabControl[TabStripPlacement=Left] /template/ ItemsPresenter#PART_ItemsPresenter"> |
||||
<StackPanel Orientation="Vertical"/> |
<Setter Property="DockPanel.Dock" Value="Left"/> |
||||
</ItemsPanelTemplate> |
</Style> |
||||
</Setter.Value> |
<Style Selector="TabControl[TabStripPlacement=Left] /template/ ItemsPresenter#PART_ItemsPresenter > WrapPanel"> |
||||
</Setter> |
<Setter Property="Orientation" Value="Vertical"/> |
||||
</Style> |
</Style> |
||||
</Styles> |
<Style Selector="TabControl[TabStripPlacement=Right] /template/ ItemsPresenter#PART_ItemsPresenter"> |
||||
|
<Setter Property="DockPanel.Dock" Value="Right"/> |
||||
|
</Style> |
||||
|
<Style Selector="TabControl[TabStripPlacement=Right] /template/ ItemsPresenter#PART_ItemsPresenter > WrapPanel"> |
||||
|
<Setter Property="Orientation" Value="Vertical"/> |
||||
|
</Style> |
||||
|
</Styles> |
||||
|
|||||
@ -0,0 +1,39 @@ |
|||||
|
<Styles xmlns="https://github.com/avaloniaui"> |
||||
|
<Style Selector="TabItem"> |
||||
|
<Setter Property="Background" Value="Transparent"/> |
||||
|
<Setter Property="FontSize" Value="{DynamicResource FontSizeLarge}"/> |
||||
|
<Setter Property="Foreground" Value="{DynamicResource ThemeForegroundLightBrush}"/> |
||||
|
<Setter Property="Template"> |
||||
|
<ControlTemplate> |
||||
|
<ContentPresenter |
||||
|
Name="PART_ContentPresenter" |
||||
|
Background="{TemplateBinding Background}" |
||||
|
BorderBrush="{TemplateBinding BorderBrush}" |
||||
|
BorderThickness="{TemplateBinding BorderThickness}" |
||||
|
ContentTemplate="{TemplateBinding HeaderTemplate}" |
||||
|
Content="{TemplateBinding Header}" |
||||
|
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}" |
||||
|
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}" |
||||
|
Padding="{TemplateBinding Padding}"/> |
||||
|
</ControlTemplate> |
||||
|
</Setter> |
||||
|
</Style> |
||||
|
<Style Selector="TabItem:disabled"> |
||||
|
<Setter Property="Opacity" Value="{DynamicResource ThemeDisabledOpacity}"/> |
||||
|
</Style> |
||||
|
<Style Selector="TabItem:pointerover /template/ ContentPresenter#PART_ContentPresenter"> |
||||
|
<Setter Property="Background" Value="{DynamicResource ThemeControlHighlightMidBrush}"/> |
||||
|
</Style> |
||||
|
<Style Selector="TabItem:selected /template/ ContentPresenter#PART_ContentPresenter"> |
||||
|
<Setter Property="Background" Value="{DynamicResource ThemeAccentBrush4}"/> |
||||
|
</Style> |
||||
|
<Style Selector="TabItem:selected:focus /template/ ContentPresenter#PART_ContentPresenter"> |
||||
|
<Setter Property="Background" Value="{DynamicResource ThemeAccentBrush3}"/> |
||||
|
</Style> |
||||
|
<Style Selector="TabItem:selected:pointerover /template/ ContentPresenter#PART_ContentPresenter"> |
||||
|
<Setter Property="Background" Value="{DynamicResource ThemeAccentBrush3}"/> |
||||
|
</Style> |
||||
|
<Style Selector="TabItem:selected:focus:pointerover /template/ ContentPresenter#PART_ContentPresenter"> |
||||
|
<Setter Property="Background" Value="{DynamicResource ThemeAccentBrush2}"/> |
||||
|
</Style> |
||||
|
</Styles> |
||||
@ -0,0 +1,9 @@ |
|||||
|
using System; |
||||
|
|
||||
|
namespace Avalonia.Rendering |
||||
|
{ |
||||
|
public interface IDeferredRendererLock |
||||
|
{ |
||||
|
IDisposable TryLock(); |
||||
|
} |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,14 @@ |
|||||
|
using System; |
||||
|
using Avalonia.Input; |
||||
|
using Avalonia.Platform; |
||||
|
|
||||
|
namespace Avalonia.Controls.UnitTests |
||||
|
{ |
||||
|
public class CursorFactoryMock : IStandardCursorFactory |
||||
|
{ |
||||
|
public IPlatformHandle GetCursor(StandardCursorType cursorType) |
||||
|
{ |
||||
|
return new PlatformHandle(IntPtr.Zero, cursorType.ToString()); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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<Control>(); |
||||
|
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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue