diff --git a/Perspex.sln b/Perspex.sln index f7d69d4db1..bc7e0560af 100644 --- a/Perspex.sln +++ b/Perspex.sln @@ -140,6 +140,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ControlCatalog", "samples\C EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Perspex.UnitTests", "tests\Perspex.UnitTests\Perspex.UnitTests.csproj", "{88060192-33D5-4932-B0F9-8BD2763E857D}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Perspex.Benchmarks", "tests\Perspex.Benchmarks\Perspex.Benchmarks.csproj", "{410AC439-81A1-4EB5-B5E9-6A7FC6B77F4B}" +EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution src\Shared\RenderHelpers\RenderHelpers.projitems*{fb05ac90-89ba-4f2f-a924-f37875fb547c}*SharedItemsImports = 4 @@ -1295,6 +1297,30 @@ Global {88060192-33D5-4932-B0F9-8BD2763E857D}.Release|iPhone.Build.0 = Release|Any CPU {88060192-33D5-4932-B0F9-8BD2763E857D}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {88060192-33D5-4932-B0F9-8BD2763E857D}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {410AC439-81A1-4EB5-B5E9-6A7FC6B77F4B}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU + {410AC439-81A1-4EB5-B5E9-6A7FC6B77F4B}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU + {410AC439-81A1-4EB5-B5E9-6A7FC6B77F4B}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU + {410AC439-81A1-4EB5-B5E9-6A7FC6B77F4B}.Ad-Hoc|iPhone.Build.0 = Release|Any CPU + {410AC439-81A1-4EB5-B5E9-6A7FC6B77F4B}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Release|Any CPU + {410AC439-81A1-4EB5-B5E9-6A7FC6B77F4B}.Ad-Hoc|iPhoneSimulator.Build.0 = Release|Any CPU + {410AC439-81A1-4EB5-B5E9-6A7FC6B77F4B}.AppStore|Any CPU.ActiveCfg = Release|Any CPU + {410AC439-81A1-4EB5-B5E9-6A7FC6B77F4B}.AppStore|Any CPU.Build.0 = Release|Any CPU + {410AC439-81A1-4EB5-B5E9-6A7FC6B77F4B}.AppStore|iPhone.ActiveCfg = Release|Any CPU + {410AC439-81A1-4EB5-B5E9-6A7FC6B77F4B}.AppStore|iPhone.Build.0 = Release|Any CPU + {410AC439-81A1-4EB5-B5E9-6A7FC6B77F4B}.AppStore|iPhoneSimulator.ActiveCfg = Release|Any CPU + {410AC439-81A1-4EB5-B5E9-6A7FC6B77F4B}.AppStore|iPhoneSimulator.Build.0 = Release|Any CPU + {410AC439-81A1-4EB5-B5E9-6A7FC6B77F4B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {410AC439-81A1-4EB5-B5E9-6A7FC6B77F4B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {410AC439-81A1-4EB5-B5E9-6A7FC6B77F4B}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {410AC439-81A1-4EB5-B5E9-6A7FC6B77F4B}.Debug|iPhone.Build.0 = Debug|Any CPU + {410AC439-81A1-4EB5-B5E9-6A7FC6B77F4B}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {410AC439-81A1-4EB5-B5E9-6A7FC6B77F4B}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {410AC439-81A1-4EB5-B5E9-6A7FC6B77F4B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {410AC439-81A1-4EB5-B5E9-6A7FC6B77F4B}.Release|Any CPU.Build.0 = Release|Any CPU + {410AC439-81A1-4EB5-B5E9-6A7FC6B77F4B}.Release|iPhone.ActiveCfg = Release|Any CPU + {410AC439-81A1-4EB5-B5E9-6A7FC6B77F4B}.Release|iPhone.Build.0 = Release|Any CPU + {410AC439-81A1-4EB5-B5E9-6A7FC6B77F4B}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {410AC439-81A1-4EB5-B5E9-6A7FC6B77F4B}.Release|iPhoneSimulator.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1339,5 +1365,6 @@ Global {E1AA3DBF-9056-4530-9376-18119A7A3FFE} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {61BEC86C-F307-4295-B5B8-9428610D7D55} = {9B9E3891-2366-4253-A952-D08BCEB71098} {88060192-33D5-4932-B0F9-8BD2763E857D} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} + {410AC439-81A1-4EB5-B5E9-6A7FC6B77F4B} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} EndGlobalSection EndGlobal diff --git a/src/Markup/Perspex.Markup.Xaml/Data/Binding.cs b/src/Markup/Perspex.Markup.Xaml/Data/Binding.cs index 776a78d83a..588cc12f0e 100644 --- a/src/Markup/Perspex.Markup.Xaml/Data/Binding.cs +++ b/src/Markup/Perspex.Markup.Xaml/Data/Binding.cs @@ -107,7 +107,8 @@ namespace Perspex.Markup.Xaml.Data targetProperty?.PropertyType ?? typeof(object), Converter ?? DefaultValueConverter.Instance, ConverterParameter, - FallbackValue); + FallbackValue, + Priority); return new InstancedBinding(subject, Mode, Priority); } diff --git a/src/Markup/Perspex.Markup.Xaml/Parsers/SelectorParser.cs b/src/Markup/Perspex.Markup.Xaml/Parsers/SelectorParser.cs index dd1341996e..a3d741149d 100644 --- a/src/Markup/Perspex.Markup.Xaml/Parsers/SelectorParser.cs +++ b/src/Markup/Perspex.Markup.Xaml/Parsers/SelectorParser.cs @@ -37,7 +37,7 @@ namespace Perspex.Markup.Xaml.Parsers public Selector Parse(string s) { var syntax = SelectorGrammar.Selector.Parse(s); - var result = new Selector(); + var result = default(Selector); foreach (var i in syntax) { diff --git a/src/Markup/Perspex.Markup.Xaml/PerspexXamlLoader.cs b/src/Markup/Perspex.Markup.Xaml/PerspexXamlLoader.cs index 1c27c00d1b..389e646215 100644 --- a/src/Markup/Perspex.Markup.Xaml/PerspexXamlLoader.cs +++ b/src/Markup/Perspex.Markup.Xaml/PerspexXamlLoader.cs @@ -152,11 +152,11 @@ namespace Perspex.Markup.Xaml InstanceLifeCycleListener = s_lifeCycleListener, }); - var control = result as IControl; + var topLevel = result as TopLevel; - if (control != null) + if (topLevel != null) { - DelayedBinding.ApplyBindings(control); + DelayedBinding.ApplyBindings(topLevel); } return result; diff --git a/src/Markup/Perspex.Markup/Data/ExpressionNode.cs b/src/Markup/Perspex.Markup/Data/ExpressionNode.cs index 100bb3d07f..e989a72177 100644 --- a/src/Markup/Perspex.Markup/Data/ExpressionNode.cs +++ b/src/Markup/Perspex.Markup/Data/ExpressionNode.cs @@ -3,6 +3,7 @@ using System; using System.Reactive.Subjects; +using Perspex.Data; namespace Perspex.Markup.Data { @@ -76,9 +77,9 @@ namespace Perspex.Markup.Data } } - public virtual bool SetValue(object value) + public virtual bool SetValue(object value, BindingPriority priority) { - return Next?.SetValue(value) ?? false; + return Next?.SetValue(value, priority) ?? false; } public virtual IDisposable Subscribe(IObserver observer) diff --git a/src/Markup/Perspex.Markup/Data/ExpressionObserver.cs b/src/Markup/Perspex.Markup/Data/ExpressionObserver.cs index 8bbedd52cc..e5b0d1ac78 100644 --- a/src/Markup/Perspex.Markup/Data/ExpressionObserver.cs +++ b/src/Markup/Perspex.Markup/Data/ExpressionObserver.cs @@ -7,6 +7,7 @@ using System.Reactive; using System.Reactive.Disposables; using System.Reactive.Linq; using System.Reactive.Subjects; +using Perspex.Data; using Perspex.Markup.Data.Plugins; namespace Perspex.Markup.Data @@ -105,11 +106,12 @@ namespace Perspex.Markup.Data /// Attempts to set the value of a property expression. /// /// The value to set. + /// The binding priority to use. /// /// True if the value could be set; false if the expression does not evaluate to a /// property. /// - public bool SetValue(object value) + public bool SetValue(object value, BindingPriority priority = BindingPriority.LocalValue) { IncrementCount(); @@ -120,7 +122,7 @@ namespace Perspex.Markup.Data try { - return _node?.SetValue(value) ?? false; + return _node?.SetValue(value, priority) ?? false; } finally { diff --git a/src/Markup/Perspex.Markup/Data/ExpressionSubject.cs b/src/Markup/Perspex.Markup/Data/ExpressionSubject.cs index 6b12c6dede..95b8c7ac4d 100644 --- a/src/Markup/Perspex.Markup/Data/ExpressionSubject.cs +++ b/src/Markup/Perspex.Markup/Data/ExpressionSubject.cs @@ -5,6 +5,7 @@ using System; using System.Globalization; using System.Reactive.Linq; using System.Reactive.Subjects; +using Perspex.Data; using Perspex.Utilities; namespace Perspex.Markup.Data @@ -18,6 +19,7 @@ namespace Perspex.Markup.Data private readonly ExpressionObserver _inner; private readonly Type _targetType; private readonly object _fallbackValue; + private readonly BindingPriority _priority; /// /// Initializes a new instance of the class. @@ -41,12 +43,14 @@ namespace Perspex.Markup.Data /// /// The value to use when the binding is unable to produce a value. /// + /// The binding priority. public ExpressionSubject( ExpressionObserver inner, Type targetType, IValueConverter converter, object converterParameter = null, - object fallbackValue = null) + object fallbackValue = null, + BindingPriority priority = BindingPriority.LocalValue) { Contract.Requires(inner != null); Contract.Requires(targetType != null); @@ -57,6 +61,7 @@ namespace Perspex.Markup.Data Converter = converter; ConverterParameter = converterParameter; _fallbackValue = fallbackValue; + _priority = priority; } /// @@ -100,7 +105,7 @@ namespace Perspex.Markup.Data converted = TypeUtilities.Default(type); } - _inner.SetValue(converted); + _inner.SetValue(converted, _priority); } } diff --git a/src/Markup/Perspex.Markup/Data/LogicalNotNode.cs b/src/Markup/Perspex.Markup/Data/LogicalNotNode.cs index a2941684b4..9a1eec8d83 100644 --- a/src/Markup/Perspex.Markup/Data/LogicalNotNode.cs +++ b/src/Markup/Perspex.Markup/Data/LogicalNotNode.cs @@ -4,12 +4,13 @@ using System; using System.Globalization; using System.Reactive.Linq; +using Perspex.Data; namespace Perspex.Markup.Data { internal class LogicalNotNode : ExpressionNode { - public override bool SetValue(object value) + public override bool SetValue(object value, BindingPriority priority) { return false; } diff --git a/src/Markup/Perspex.Markup/Data/Plugins/IPropertyAccessor.cs b/src/Markup/Perspex.Markup/Data/Plugins/IPropertyAccessor.cs index 772bbbafe9..776993b220 100644 --- a/src/Markup/Perspex.Markup/Data/Plugins/IPropertyAccessor.cs +++ b/src/Markup/Perspex.Markup/Data/Plugins/IPropertyAccessor.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using Perspex.Data; namespace Perspex.Markup.Data.Plugins { @@ -27,9 +28,12 @@ namespace Perspex.Markup.Data.Plugins /// /// The value to set. Guaranteed to be of a valid type for the property. /// + /// + /// The priority with which to set the value. + /// /// /// True if the property was set; false if the property could not be set. /// - bool SetValue(object value); + bool SetValue(object value, BindingPriority priority); } } diff --git a/src/Markup/Perspex.Markup/Data/Plugins/InpcPropertyAccessorPlugin.cs b/src/Markup/Perspex.Markup/Data/Plugins/InpcPropertyAccessorPlugin.cs index 4d53fdaea4..3a71859b4c 100644 --- a/src/Markup/Perspex.Markup/Data/Plugins/InpcPropertyAccessorPlugin.cs +++ b/src/Markup/Perspex.Markup/Data/Plugins/InpcPropertyAccessorPlugin.cs @@ -6,6 +6,7 @@ using System.ComponentModel; using System.Linq; using System.Reactive.Linq; using System.Reflection; +using Perspex.Data; using Perspex.Utilities; namespace Perspex.Markup.Data.Plugins @@ -106,7 +107,7 @@ namespace Perspex.Markup.Data.Plugins } } - public bool SetValue(object value) + public bool SetValue(object value, BindingPriority priority) { if (_property.CanWrite) { diff --git a/src/Markup/Perspex.Markup/Data/Plugins/PerspexPropertyAccessorPlugin.cs b/src/Markup/Perspex.Markup/Data/Plugins/PerspexPropertyAccessorPlugin.cs index 9f50359570..411fcc1555 100644 --- a/src/Markup/Perspex.Markup/Data/Plugins/PerspexPropertyAccessorPlugin.cs +++ b/src/Markup/Perspex.Markup/Data/Plugins/PerspexPropertyAccessorPlugin.cs @@ -3,6 +3,7 @@ using System; using System.Reactive.Linq; +using Perspex.Data; namespace Perspex.Markup.Data.Plugins { @@ -95,11 +96,11 @@ namespace Perspex.Markup.Data.Plugins _subscription = null; } - public bool SetValue(object value) + public bool SetValue(object value, BindingPriority priority) { if (!_property.IsReadOnly) { - Instance.SetValue(_property, value); + Instance.SetValue(_property, value, priority); return true; } diff --git a/src/Markup/Perspex.Markup/Data/PropertyAccessorNode.cs b/src/Markup/Perspex.Markup/Data/PropertyAccessorNode.cs index 9208c9d081..1aa703ff8f 100644 --- a/src/Markup/Perspex.Markup/Data/PropertyAccessorNode.cs +++ b/src/Markup/Perspex.Markup/Data/PropertyAccessorNode.cs @@ -8,6 +8,7 @@ using System.Reflection; using System.Threading; using System.Threading.Tasks; using System.Windows.Input; +using Perspex.Data; using Perspex.Markup.Data.Plugins; namespace Perspex.Markup.Data @@ -26,17 +27,17 @@ namespace Perspex.Markup.Data public Type PropertyType => _accessor?.PropertyType; - public override bool SetValue(object value) + public override bool SetValue(object value, BindingPriority priority) { if (Next != null) { - return Next.SetValue(value); + return Next.SetValue(value, priority); } else { if (_accessor != null) { - return _accessor.SetValue(value); + return _accessor.SetValue(value, priority); } return false; diff --git a/src/Perspex.Base/PerspexProperty.cs b/src/Perspex.Base/PerspexProperty.cs index 71dd7bb0fb..e88c7f4418 100644 --- a/src/Perspex.Base/PerspexProperty.cs +++ b/src/Perspex.Base/PerspexProperty.cs @@ -22,7 +22,6 @@ namespace Perspex public static readonly object UnsetValue = new Unset(); private static int s_nextId = 1; - private readonly int _id; private readonly Subject _initialized; private readonly Subject _changed; private readonly PropertyMetadata _defaultMetadata; @@ -61,7 +60,7 @@ namespace Perspex PropertyType = valueType; OwnerType = ownerType; Notifying = notifying; - _id = s_nextId++; + Id = s_nextId++; _metadata.Add(ownerType, metadata); _defaultMetadata = metadata; @@ -85,7 +84,7 @@ namespace Perspex PropertyType = source.PropertyType; OwnerType = ownerType; Notifying = source.Notifying; - _id = source._id; + Id = source.Id; _defaultMetadata = source._defaultMetadata; } @@ -164,6 +163,11 @@ namespace Perspex /// public Action Notifying { get; } + /// + /// Gets the integer ID that represents this property. + /// + internal int Id { get; } + /// /// Provides access to a property's binding via the /// indexer. @@ -392,13 +396,13 @@ namespace Perspex /// public bool Equals(PerspexProperty other) { - return other != null && _id == other._id; + return other != null && Id == other.Id; } /// public override int GetHashCode() { - return _id; + return Id; } /// diff --git a/src/Perspex.Base/PerspexPropertyRegistry.cs b/src/Perspex.Base/PerspexPropertyRegistry.cs index 871837331a..25a1013abb 100644 --- a/src/Perspex.Base/PerspexPropertyRegistry.cs +++ b/src/Perspex.Base/PerspexPropertyRegistry.cs @@ -17,14 +17,14 @@ namespace Perspex /// /// The registered properties by type. /// - private readonly Dictionary> _registered = - new Dictionary>(); + private readonly Dictionary> _registered = + new Dictionary>(); /// /// The registered attached properties by owner type. /// - private readonly Dictionary> _attached = - new Dictionary>(); + private readonly Dictionary> _attached = + new Dictionary>(); /// /// Gets the instance @@ -39,11 +39,11 @@ namespace Perspex /// A collection of definitions. public IEnumerable GetAttached(Type ownerType) { - List list; + Dictionary inner; - if (_attached.TryGetValue(ownerType, out list)) + if (_attached.TryGetValue(ownerType, out inner)) { - return list; + return inner.Values; } return Enumerable.Empty(); @@ -65,13 +65,13 @@ namespace Perspex // Ensure the type's static ctor has been run. RuntimeHelpers.RunClassConstructor(type.TypeHandle); - List list; + Dictionary inner; - if (_registered.TryGetValue(type, out list)) + if (_registered.TryGetValue(type, out inner)) { - foreach (PerspexProperty p in list) + foreach (var p in inner) { - yield return p; + yield return p.Value; } } @@ -105,15 +105,15 @@ namespace Perspex { while (type != null) { - List list; + Dictionary inner; - if (_registered.TryGetValue(type, out list)) + if (_registered.TryGetValue(type, out inner)) { - var index = list.IndexOf(property); + PerspexProperty result; - if (index != -1) + if (inner.TryGetValue(property.Id, out result)) { - return list[index]; + return result; } } @@ -250,30 +250,30 @@ namespace Perspex Contract.Requires(type != null); Contract.Requires(property != null); - List list; + Dictionary inner; - if (!_registered.TryGetValue(type, out list)) + if (!_registered.TryGetValue(type, out inner)) { - list = new List(); - _registered.Add(type, list); + inner = new Dictionary(); + _registered.Add(type, inner); } - if (!list.Contains(property)) + if (!inner.ContainsKey(property.Id)) { - list.Add(property); + inner.Add(property.Id, property); } if (property.IsAttached) { - if (!_attached.TryGetValue(property.OwnerType, out list)) + if (!_attached.TryGetValue(property.OwnerType, out inner)) { - list = new List(); - _attached.Add(property.OwnerType, list); + inner = new Dictionary(); + _attached.Add(property.OwnerType, inner); } - if (!list.Contains(property)) + if (!inner.ContainsKey(property.Id)) { - list.Add(property); + inner.Add(property.Id, property); } } } diff --git a/src/Perspex.Controls/Decorator.cs b/src/Perspex.Controls/Decorator.cs index fcf7847d6e..eea1108f67 100644 --- a/src/Perspex.Controls/Decorator.cs +++ b/src/Perspex.Controls/Decorator.cs @@ -13,8 +13,8 @@ namespace Perspex.Controls /// /// Defines the property. /// - public static readonly StyledProperty ChildProperty = - PerspexProperty.Register(nameof(Child)); + public static readonly StyledProperty ChildProperty = + PerspexProperty.Register(nameof(Child)); /// /// Defines the property. @@ -35,7 +35,7 @@ namespace Perspex.Controls /// Gets or sets the decorated control. /// [Content] - public Control Child + public IControl Child { get { return GetValue(ChildProperty); } set { SetValue(ChildProperty, value); } @@ -70,8 +70,7 @@ namespace Perspex.Controls /// protected override Size ArrangeOverride(Size finalSize) { - Control content = Child; - content?.Arrange(new Rect(finalSize).Deflate(Padding)); + Child?.Arrange(new Rect(finalSize).Deflate(Padding)); return finalSize; } diff --git a/src/Perspex.Controls/Panel.cs b/src/Perspex.Controls/Panel.cs index ec87c5ba18..f22926d454 100644 --- a/src/Perspex.Controls/Panel.cs +++ b/src/Perspex.Controls/Panel.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; -using Perspex.LogicalTree; using Perspex.Media; using Perspex.Metadata; @@ -28,8 +27,6 @@ namespace Perspex.Controls private readonly Controls _children = new Controls(); - private ILogical _childLogicalParent; - /// /// Initializes static members of the class. /// @@ -44,7 +41,6 @@ namespace Perspex.Controls public Panel() { _children.CollectionChanged += ChildrenChanged; - _childLogicalParent = this; } /// diff --git a/src/Perspex.Controls/Presenters/ContentPresenter.cs b/src/Perspex.Controls/Presenters/ContentPresenter.cs index 246db77c46..42f300f763 100644 --- a/src/Perspex.Controls/Presenters/ContentPresenter.cs +++ b/src/Perspex.Controls/Presenters/ContentPresenter.cs @@ -1,9 +1,12 @@ // Copyright (c) The Perspex Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using System; using Perspex.Controls.Primitives; using Perspex.Controls.Templates; +using Perspex.Layout; using Perspex.LogicalTree; +using Perspex.Media; namespace Perspex.Controls.Presenters { @@ -12,6 +15,24 @@ namespace Perspex.Controls.Presenters /// public class ContentPresenter : Control, IContentPresenter { + /// + /// Defines the property. + /// + public static readonly StyledProperty BackgroundProperty = + Border.BackgroundProperty.AddOwner(); + + /// + /// Defines the property. + /// + public static readonly PerspexProperty BorderBrushProperty = + Border.BorderBrushProperty.AddOwner(); + + /// + /// Defines the property. + /// + public static readonly StyledProperty BorderThicknessProperty = + Border.BorderThicknessProperty.AddOwner(); + /// /// Defines the property. /// @@ -26,6 +47,30 @@ namespace Perspex.Controls.Presenters public static readonly StyledProperty ContentProperty = ContentControl.ContentProperty.AddOwner(); + /// + /// Defines the property. + /// + public static readonly StyledProperty CornerRadiusProperty = + Border.CornerRadiusProperty.AddOwner(); + + /// + /// Defines the property. + /// + public static readonly StyledProperty HorizontalContentAlignmentProperty = + ContentControl.HorizontalContentAlignmentProperty.AddOwner(); + + /// + /// Defines the property. + /// + public static readonly StyledProperty VerticalContentAlignmentProperty = + ContentControl.VerticalContentAlignmentProperty.AddOwner(); + + /// + /// Defines the property. + /// + public static readonly StyledProperty PaddingProperty = + Border.PaddingProperty.AddOwner(); + private IControl _child; private bool _createdChild; @@ -38,6 +83,33 @@ namespace Perspex.Controls.Presenters TemplatedParentProperty.Changed.AddClassHandler(x => x.TemplatedParentChanged); } + /// + /// Gets or sets a brush with which to paint the background. + /// + public Brush Background + { + get { return GetValue(BackgroundProperty); } + set { SetValue(BackgroundProperty, value); } + } + + /// + /// Gets or sets a brush with which to paint the border. + /// + public Brush BorderBrush + { + get { return GetValue(BorderBrushProperty); } + set { SetValue(BorderBrushProperty, value); } + } + + /// + /// Gets or sets the thickness of the border. + /// + public double BorderThickness + { + get { return GetValue(BorderThicknessProperty); } + set { SetValue(BorderThicknessProperty, value); } + } + /// /// Gets the control displayed by the presenter. /// @@ -56,6 +128,42 @@ namespace Perspex.Controls.Presenters set { SetValue(ContentProperty, value); } } + /// + /// Gets or sets the radius of the border rounded corners. + /// + public float CornerRadius + { + get { return GetValue(CornerRadiusProperty); } + set { SetValue(CornerRadiusProperty, value); } + } + + /// + /// Gets or sets the horizontal alignment of the content within the control. + /// + public HorizontalAlignment HorizontalContentAlignment + { + get { return GetValue(HorizontalContentAlignmentProperty); } + set { SetValue(HorizontalContentAlignmentProperty, value); } + } + + /// + /// Gets or sets the vertical alignment of the content within the control. + /// + public VerticalAlignment VerticalContentAlignment + { + get { return GetValue(VerticalContentAlignmentProperty); } + set { SetValue(VerticalContentAlignmentProperty, value); } + } + + /// + /// Gets or sets the padding to place around the control. + /// + public Thickness Padding + { + get { return GetValue(PaddingProperty); } + set { SetValue(PaddingProperty, value); } + } + /// public override sealed void ApplyTemplate() { @@ -120,23 +228,93 @@ namespace Perspex.Controls.Presenters } /// - protected override Size MeasureCore(Size availableSize) + public override void Render(DrawingContext context) { - return base.MeasureCore(availableSize); + var background = Background; + var borderBrush = BorderBrush; + var borderThickness = BorderThickness; + var cornerRadius = CornerRadius; + var rect = new Rect(Bounds.Size).Deflate(BorderThickness); + + if (background != null) + { + context.FillRectangle(background, rect, cornerRadius); + } + + if (borderBrush != null && borderThickness > 0) + { + context.DrawRectangle(new Pen(borderBrush, borderThickness), rect, cornerRadius); + } } /// protected override Size MeasureOverride(Size availableSize) { var child = Child; + var padding = Padding + new Thickness(BorderThickness); if (child != null) { - child.Measure(availableSize); - return child.DesiredSize; + child.Measure(availableSize.Deflate(padding)); + return child.DesiredSize.Inflate(padding); + } + else + { + return new Size(padding.Left + padding.Right, padding.Bottom + padding.Top); + } + } + + /// + protected override Size ArrangeOverride(Size finalSize) + { + var child = Child; + + if (child != null) + { + var padding = Padding + new Thickness(BorderThickness); + var sizeMinusPadding = finalSize.Deflate(padding); + var size = sizeMinusPadding; + var horizontalAlignment = HorizontalContentAlignment; + var verticalAlignment = VerticalContentAlignment; + var originX = padding.Left; + var originY = padding.Top; + + if (horizontalAlignment != HorizontalAlignment.Stretch) + { + size = size.WithWidth(child.DesiredSize.Width); + } + + if (verticalAlignment != VerticalAlignment.Stretch) + { + size = size.WithHeight(child.DesiredSize.Height); + } + + switch (horizontalAlignment) + { + case HorizontalAlignment.Stretch: + case HorizontalAlignment.Center: + originX += (sizeMinusPadding.Width - size.Width) / 2; + break; + case HorizontalAlignment.Right: + originX = size.Width - child.DesiredSize.Width; + break; + } + + switch (verticalAlignment) + { + case VerticalAlignment.Stretch: + case VerticalAlignment.Center: + originY += (sizeMinusPadding.Height - size.Height) / 2; + break; + case VerticalAlignment.Bottom: + originY = size.Height - child.DesiredSize.Height; + break; + } + + child.Arrange(new Rect(originX, originY, size.Width, size.Height)); } - return new Size(); + return finalSize; } /// diff --git a/src/Perspex.Layout/Layoutable.cs b/src/Perspex.Layout/Layoutable.cs index 354f279e07..af4ee13853 100644 --- a/src/Perspex.Layout/Layoutable.cs +++ b/src/Perspex.Layout/Layoutable.cs @@ -536,20 +536,22 @@ namespace Perspex.Layout { if (IsVisible) { - double originX = finalRect.X + Margin.Left; - double originY = finalRect.Y + Margin.Top; + var originX = finalRect.X + Margin.Left; + var originY = finalRect.Y + Margin.Top; var sizeMinusMargins = new Size( Math.Max(0, finalRect.Width - Margin.Left - Margin.Right), Math.Max(0, finalRect.Height - Margin.Top - Margin.Bottom)); + var horizontalAlignment = HorizontalAlignment; + var verticalAlignment = VerticalAlignment; var size = sizeMinusMargins; var scale = GetLayoutScale(); - if (HorizontalAlignment != HorizontalAlignment.Stretch) + if (horizontalAlignment != HorizontalAlignment.Stretch) { size = size.WithWidth(Math.Min(size.Width, DesiredSize.Width)); } - if (VerticalAlignment != VerticalAlignment.Stretch) + if (verticalAlignment != VerticalAlignment.Stretch) { size = size.WithHeight(Math.Min(size.Height, DesiredSize.Height)); } @@ -568,7 +570,7 @@ namespace Perspex.Layout size = ArrangeOverride(size).Constrain(size); - switch (HorizontalAlignment) + switch (horizontalAlignment) { case HorizontalAlignment.Center: case HorizontalAlignment.Stretch: @@ -579,7 +581,7 @@ namespace Perspex.Layout break; } - switch (VerticalAlignment) + switch (verticalAlignment) { case VerticalAlignment.Center: case VerticalAlignment.Stretch: diff --git a/src/Perspex.SceneGraph/Visual.cs b/src/Perspex.SceneGraph/Visual.cs index 575f555617..63b7bf14f1 100644 --- a/src/Perspex.SceneGraph/Visual.cs +++ b/src/Perspex.SceneGraph/Visual.cs @@ -77,19 +77,8 @@ namespace Perspex public static readonly StyledProperty ZIndexProperty = PerspexProperty.Register(nameof(ZIndex)); - /// - /// The visual's bounds relative to its parent. - /// private Rect _bounds; - - /// - /// Holds the parent of the visual. - /// private IVisual _visualParent; - - /// - /// The logger for visual-level events. - /// private readonly ILogger _visualLogger; /// @@ -239,6 +228,11 @@ namespace Perspex /// IVisual IVisual.VisualParent => _visualParent; + /// + /// Gets the root of the visual tree, if the control is attached to a visual tree. + /// + IRenderRoot IVisual.VisualRoot { get; } + /// /// Invalidates the visual and queues a repaint. /// @@ -483,7 +477,6 @@ namespace Perspex _visualLogger.Verbose("Attached to visual tree"); VisualRoot = e.Root; - OnAttachedToVisualTree(e); if (VisualChildren != null) diff --git a/src/Perspex.SceneGraph/VisualTree/IVisual.cs b/src/Perspex.SceneGraph/VisualTree/IVisual.cs index 2cad9cf757..bac8b5bd2e 100644 --- a/src/Perspex.SceneGraph/VisualTree/IVisual.cs +++ b/src/Perspex.SceneGraph/VisualTree/IVisual.cs @@ -4,6 +4,7 @@ using System; using Perspex.Collections; using Perspex.Media; +using Perspex.Rendering; namespace Perspex.VisualTree { @@ -80,11 +81,21 @@ namespace Perspex.VisualTree /// IVisual VisualParent { get; } + /// + /// Gets the root of the visual tree, if the control is attached to a visual tree. + /// + IRenderRoot VisualRoot { get; } + /// /// Gets or sets the Z index of the node. /// int ZIndex { get; set; } + /// + /// Invalidates the visual and queues a repaint. + /// + void InvalidateVisual(); + /// /// Renders the scene graph node to a . /// diff --git a/src/Perspex.SceneGraph/VisualTree/VisualExtensions.cs b/src/Perspex.SceneGraph/VisualTree/VisualExtensions.cs index d08682c829..bb0da6db03 100644 --- a/src/Perspex.SceneGraph/VisualTree/VisualExtensions.cs +++ b/src/Perspex.SceneGraph/VisualTree/VisualExtensions.cs @@ -176,15 +176,7 @@ namespace Perspex.VisualTree { Contract.Requires(visual != null); - var parent = visual.VisualParent; - - while (parent != null) - { - visual = parent; - parent = visual.VisualParent; - } - - return visual; + return visual.VisualRoot as IVisual; } /// diff --git a/src/Perspex.Styling/Perspex.Styling.csproj b/src/Perspex.Styling/Perspex.Styling.csproj index 2219312704..4a6647926a 100644 --- a/src/Perspex.Styling/Perspex.Styling.csproj +++ b/src/Perspex.Styling/Perspex.Styling.csproj @@ -49,6 +49,9 @@ + + + @@ -67,6 +70,8 @@ + + diff --git a/src/Perspex.Styling/Styling/ChildSelector.cs b/src/Perspex.Styling/Styling/ChildSelector.cs new file mode 100644 index 0000000000..4cb675adc5 --- /dev/null +++ b/src/Perspex.Styling/Styling/ChildSelector.cs @@ -0,0 +1,56 @@ +// Copyright (c) The Perspex Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using Perspex.LogicalTree; + +namespace Perspex.Styling +{ + internal class ChildSelector : Selector + { + private readonly Selector _parent; + private string _selectorString; + + public ChildSelector(Selector parent) + { + if (parent == null) + { + throw new InvalidOperationException("Child selector must be preceeded by a selector."); + } + + _parent = parent; + } + + /// + public override bool InTemplate => _parent.InTemplate; + + /// + public override Type TargetType => null; + + public override string ToString() + { + if (_selectorString == null) + { + _selectorString = _parent.ToString() + " > "; + } + + return _selectorString; + } + + protected override SelectorMatch Evaluate(IStyleable control, bool subscribe) + { + var controlParent = ((ILogical)control).LogicalParent; + + if (controlParent != null) + { + return _parent.Match((IStyleable)controlParent, subscribe); + } + else + { + return SelectorMatch.False; + } + } + + protected override Selector MovePrevious() => null; + } +} diff --git a/src/Perspex.Styling/Styling/DescendentSelector.cs b/src/Perspex.Styling/Styling/DescendentSelector.cs new file mode 100644 index 0000000000..259147232f --- /dev/null +++ b/src/Perspex.Styling/Styling/DescendentSelector.cs @@ -0,0 +1,80 @@ +// Copyright (c) The Perspex 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 Perspex.LogicalTree; + +namespace Perspex.Styling +{ + internal class DescendentSelector : Selector + { + private readonly Selector _parent; + private string _selectorString; + + public DescendentSelector(Selector parent) + { + if (parent == null) + { + throw new InvalidOperationException("Descendent selector must be preceeded by a selector."); + } + + _parent = parent; + } + + /// + public override bool InTemplate => _parent.InTemplate; + + /// + public override Type TargetType => null; + + public override string ToString() + { + if (_selectorString == null) + { + _selectorString = _parent.ToString() + ' '; + } + + return _selectorString; + } + + protected override SelectorMatch Evaluate(IStyleable control, bool subscribe) + { + ILogical c = (ILogical)control; + List> descendentMatches = new List>(); + + while (c != null) + { + c = c.LogicalParent; + + if (c is IStyleable) + { + var match = _parent.Match((IStyleable)c, subscribe); + + if (match.ImmediateResult != null) + { + if (match.ImmediateResult == true) + { + return SelectorMatch.True; + } + } + else + { + descendentMatches.Add(match.ObservableResult); + } + } + } + + if (descendentMatches.Count > 0) + { + return new SelectorMatch(StyleActivator.Or(descendentMatches)); + } + else + { + return SelectorMatch.False; + } + } + + protected override Selector MovePrevious() => null; + } +} diff --git a/src/Perspex.Styling/Styling/PropertyEqualsSelector.cs b/src/Perspex.Styling/Styling/PropertyEqualsSelector.cs new file mode 100644 index 0000000000..3df29dc280 --- /dev/null +++ b/src/Perspex.Styling/Styling/PropertyEqualsSelector.cs @@ -0,0 +1,91 @@ +// Copyright (c) The Perspex 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.Reactive.Linq; +using System.Text; + +namespace Perspex.Styling +{ + /// + /// A selector that matches the common case of a type and/or name followed by a collection of + /// style classes and pseudoclasses. + /// + internal class PropertyEqualsSelector : Selector + { + private readonly Selector _previous; + private readonly PerspexProperty _property; + private readonly object _value; + private string _selectorString; + + public PropertyEqualsSelector(Selector previous, PerspexProperty property, object value) + { + Contract.Requires(property != null); + + _previous = previous; + _property = property; + _value = value; + } + + /// + public override bool InTemplate => _previous?.InTemplate ?? false; + + /// + /// Gets the name of the control to match. + /// + public string Name { get; private set; } + + /// + public override Type TargetType => _previous?.TargetType; + + /// + public override string ToString() + { + if (_selectorString == null) + { + var builder = new StringBuilder(); + + if (_previous != null) + { + builder.Append(_previous.ToString()); + } + + builder.Append('['); + + if (_property.IsAttached) + { + builder.Append(_property.OwnerType.Name); + builder.Append('.'); + } + + builder.Append(_property.Name); + builder.Append('='); + builder.Append(_value); + builder.Append(']'); + + _selectorString = builder.ToString(); + } + + return _selectorString; + } + + /// + protected override SelectorMatch Evaluate(IStyleable control, bool subscribe) + { + if (!PerspexPropertyRegistry.Instance.IsRegistered(control, _property)) + { + return SelectorMatch.False; + } + else if (subscribe) + { + return new SelectorMatch(control.GetObservable(_property).Select(v => Equals(v, _value))); + } + else + { + return new SelectorMatch(control.GetValue(_property).Equals(_value)); + } + } + + protected override Selector MovePrevious() => _previous; + } +} diff --git a/src/Perspex.Styling/Styling/Selector.cs b/src/Perspex.Styling/Styling/Selector.cs index b47a3e28fd..0192404e2f 100644 --- a/src/Perspex.Styling/Styling/Selector.cs +++ b/src/Perspex.Styling/Styling/Selector.cs @@ -9,123 +9,41 @@ namespace Perspex.Styling /// /// A selector in a . /// - /// - /// Selectors represented in markup using a CSS-like syntax, e.g. "Button > .dark" which - /// means "A child of a Button with the 'dark' class applied. The preceeding example would be - /// stored in 3 objects, linked by the property: - /// - /// - /// .dark - /// - /// A selector that selects a control with the 'dark' class applied. - /// - /// - /// - /// > - /// - /// A selector that selects a child of the previous selector. - /// - /// - /// - /// Button - /// - /// A selector that selects a Button type. - /// - /// - /// - /// - public class Selector + public abstract class Selector { - private readonly Func _evaluate; - private readonly Type _targetType; - private readonly bool _inTemplate; - private readonly bool _stopTraversal; - private string _description; - - /// - /// Initializes a new instance of the class. - /// - public Selector() - { - _evaluate = _ => SelectorMatch.True; - } - - /// - /// Initializes a new instance of the class. - /// - /// The previous selector. - /// The evaluator function. - /// The string representation of the selector. - /// The target type, if available. - /// Whether to match in a control template. - /// Whether to stop traversal at this point. - public Selector( - Selector previous, - Func evaluate, - string selectorString, - Type targetType = null, - bool inTemplate = false, - bool stopTraversal = false) - : this() - { - Contract.Requires(previous != null); - - Previous = previous; - _evaluate = evaluate; - SelectorString = selectorString; - _targetType = targetType; - _inTemplate = inTemplate || previous._inTemplate; - _stopTraversal = stopTraversal; - } - /// - /// Gets the previous selector. + /// Gets a value indicating whether either this selector or a previous selector has moved + /// into a template. /// - public Selector Previous - { - get; - } - - /// - /// Gets a string representation of the selector. - /// - public string SelectorString - { - get; - } + public abstract bool InTemplate { get; } /// /// Gets the target type of the selector, if available. /// - public Type TargetType => _targetType ?? MovePrevious()?.TargetType; - - /// - /// Returns the previous selector if traversal is not stopped. - /// - /// The previous selector. - public Selector MovePrevious() - { - return _stopTraversal ? null : Previous; - } + public abstract Type TargetType { get; } /// /// Tries to match the selector with a control. /// /// The control. + /// + /// Whether the match should subscribe to changes in order to track the match over time, + /// or simply return an immediate result. + /// /// A . - public SelectorMatch Match(IStyleable control) + public SelectorMatch Match(IStyleable control, bool subscribe = true) { List> inputs = new List>(); Selector selector = this; while (selector != null) { - if (selector._inTemplate && control.TemplatedParent == null) + if (selector.InTemplate && control.TemplatedParent == null) { return SelectorMatch.False; } - var match = selector._evaluate(control); + var match = selector.Evaluate(control, subscribe); if (match.ImmediateResult == false) { @@ -150,24 +68,19 @@ namespace Perspex.Styling } /// - /// Gets a string representation of the selector. + /// Evaluates the selector for a match. /// - /// The string representation of the selector. - public override string ToString() - { - if (_description == null) - { - string result = string.Empty; - - if (Previous != null) - { - result = Previous.ToString(); - } - - _description = result + SelectorString; - } + /// The control. + /// + /// Whether the match should subscribe to changes in order to track the match over time, + /// or simply return an immediate result. + /// + /// A . + protected abstract SelectorMatch Evaluate(IStyleable control, bool subscribe); - return _description; - } + /// + /// Moves to the previous selector. + /// + protected abstract Selector MovePrevious(); } } diff --git a/src/Perspex.Styling/Styling/SelectorMatch.cs b/src/Perspex.Styling/Styling/SelectorMatch.cs index e4ee76d0cd..cb7915b100 100644 --- a/src/Perspex.Styling/Styling/SelectorMatch.cs +++ b/src/Perspex.Styling/Styling/SelectorMatch.cs @@ -6,12 +6,12 @@ using System; namespace Perspex.Styling { /// - /// Holds the result of a match. + /// Holds the result of a match. /// /// /// There are two types of selectors - ones whose match can never change for a particular - /// control (such as ) and ones whose result can - /// change over time (such as . For the first + /// control (such as ) and ones whose result can + /// change over time (such as . For the first /// category of selectors, the value of will be set but for the /// second, will be null and will /// hold an observable which tracks the match. diff --git a/src/Perspex.Styling/Styling/Selectors.cs b/src/Perspex.Styling/Styling/Selectors.cs index 14d9d0d4be..f10b6d879b 100644 --- a/src/Perspex.Styling/Styling/Selectors.cs +++ b/src/Perspex.Styling/Styling/Selectors.cs @@ -2,13 +2,6 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.Linq; -using System.Reactive; -using System.Reactive.Linq; -using System.Reflection; -using Perspex.LogicalTree; namespace Perspex.Styling { @@ -24,9 +17,7 @@ namespace Perspex.Styling /// The selector. public static Selector Child(this Selector previous) { - Contract.Requires(previous != null); - - return new Selector(previous, x => MatchChild(x, previous), " < ", stopTraversal: true); + return new ChildSelector(previous); } /// @@ -37,10 +28,17 @@ namespace Perspex.Styling /// The selector. public static Selector Class(this Selector previous, string name) { - Contract.Requires(previous != null); - Contract.Requires(name != null); + var tac = previous as TypeNameAndClassSelector; - return new Selector(previous, x => MatchClass(x, name), name[0] == ':' ? name : '.' + name); + if (tac != null) + { + tac.Classes.Add(name); + return tac; + } + else + { + return TypeNameAndClassSelector.ForClass(previous, name); + } } /// @@ -50,9 +48,7 @@ namespace Perspex.Styling /// The selector. public static Selector Descendent(this Selector previous) { - Contract.Requires(previous != null); - - return new Selector(previous, x => MatchDescendent(x, previous), " ", stopTraversal: true); + return new DescendentSelector(previous); } /// @@ -63,9 +59,7 @@ namespace Perspex.Styling /// The selector. public static Selector Is(this Selector previous, Type type) { - Contract.Requires(previous != null); - - return new Selector(previous, x => MatchIs(x, type), $":is({type.Name})", type); + return TypeNameAndClassSelector.Is(previous, type); } /// @@ -87,9 +81,17 @@ namespace Perspex.Styling /// The selector. public static Selector Name(this Selector previous, string name) { - Contract.Requires(previous != null); + var tac = previous as TypeNameAndClassSelector; - return new Selector(previous, x => MatchName(x, name), '#' + name); + if (tac != null) + { + tac.Name = name; + return tac; + } + else + { + return TypeNameAndClassSelector.ForName(previous, name); + } } /// @@ -100,9 +102,7 @@ namespace Perspex.Styling /// The selector. public static Selector OfType(this Selector previous, Type type) { - Contract.Requires(previous != null); - - return new Selector(previous, x => MatchOfType(x, type), type.Name, type); + return TypeNameAndClassSelector.OfType(previous, type); } /// @@ -126,10 +126,7 @@ namespace Perspex.Styling /// The selector. public static Selector PropertyEquals(this Selector previous, PerspexProperty property, object value) { - Contract.Requires(previous != null); - Contract.Requires(property != null); - - return new Selector(previous, x => MatchPropertyEquals(x, property, value), $"[{property.Name}={value}]"); + return new PropertyEqualsSelector(previous, property, value); } /// @@ -141,10 +138,7 @@ namespace Perspex.Styling /// The selector. public static Selector PropertyEquals(this Selector previous, PerspexProperty property, object value) { - Contract.Requires(previous != null); - Contract.Requires(property != null); - - return new Selector(previous, x => MatchPropertyEquals(x, property, value), $"[{property.Name}={value}]"); + return new PropertyEqualsSelector(previous, property, value); } /// @@ -154,114 +148,7 @@ namespace Perspex.Styling /// The selector. public static Selector Template(this Selector previous) { - Contract.Requires(previous != null); - - return new Selector( - previous, - x => MatchTemplate(x, previous), - " /template/ ", - inTemplate: true, - stopTraversal: true); - } - - private static SelectorMatch MatchChild(IStyleable control, Selector previous) - { - var parent = ((ILogical)control).LogicalParent; - - if (parent != null) - { - return previous.Match((IStyleable)parent); - } - else - { - return SelectorMatch.False; - } - } - - private static SelectorMatch MatchClass(IStyleable control, string name) - { - var observable = Observable.FromEventPattern< - NotifyCollectionChangedEventHandler, - NotifyCollectionChangedEventArgs>( - x => control.Classes.CollectionChanged += x, - x => control.Classes.CollectionChanged -= x) - .Select(_ => Unit.Default) - .StartWith(Unit.Default) - .Select(_ => control.Classes.Contains(name)); - - return new SelectorMatch(observable); - } - - private static SelectorMatch MatchDescendent(IStyleable control, Selector previous) - { - ILogical c = (ILogical)control; - List> descendentMatches = new List>(); - - while (c != null) - { - c = c.LogicalParent; - - if (c is IStyleable) - { - var match = previous.Match((IStyleable)c); - - if (match.ImmediateResult != null) - { - if (match.ImmediateResult == true) - { - return SelectorMatch.True; - } - } - else - { - descendentMatches.Add(match.ObservableResult); - } - } - } - - return new SelectorMatch(StyleActivator.Or(descendentMatches)); - } - - private static SelectorMatch MatchIs(IStyleable control, Type type) - { - var controlType = control.StyleKey ?? control.GetType(); - return new SelectorMatch(type.GetTypeInfo().IsAssignableFrom(controlType.GetTypeInfo())); - } - - private static SelectorMatch MatchName(IStyleable control, string name) - { - return new SelectorMatch(control.Name == name); - } - - private static SelectorMatch MatchOfType(IStyleable control, Type type) - { - var controlType = control.StyleKey ?? control.GetType(); - return new SelectorMatch(controlType == type); - } - - private static SelectorMatch MatchPropertyEquals(IStyleable x, PerspexProperty property, object value) - { - if (!PerspexPropertyRegistry.Instance.IsRegistered(x, property)) - { - return SelectorMatch.False; - } - else - { - return new SelectorMatch(x.GetObservable(property).Select(v => Equals(v, value))); - } - } - - private static SelectorMatch MatchTemplate(IStyleable control, Selector previous) - { - IStyleable templatedParent = control.TemplatedParent as IStyleable; - - if (templatedParent == null) - { - throw new InvalidOperationException( - "Cannot call Template selector on control with null TemplatedParent."); - } - - return previous.Match(templatedParent); + return new TemplateSelector(previous); } } } diff --git a/src/Perspex.Styling/Styling/Style.cs b/src/Perspex.Styling/Styling/Style.cs index 58fd2fa721..7f0cb00e4b 100644 --- a/src/Perspex.Styling/Styling/Style.cs +++ b/src/Perspex.Styling/Styling/Style.cs @@ -29,7 +29,7 @@ namespace Perspex.Styling /// The style selector. public Style(Func selector) { - Selector = selector(new Selector()); + Selector = selector(null); } /// diff --git a/src/Perspex.Styling/Styling/TemplateSelector.cs b/src/Perspex.Styling/Styling/TemplateSelector.cs new file mode 100644 index 0000000000..0f6efd84c3 --- /dev/null +++ b/src/Perspex.Styling/Styling/TemplateSelector.cs @@ -0,0 +1,54 @@ +// Copyright (c) The Perspex Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; + +namespace Perspex.Styling +{ + internal class TemplateSelector : Selector + { + private readonly Selector _parent; + private string _selectorString; + + public TemplateSelector(Selector parent) + { + if (parent == null) + { + throw new InvalidOperationException("Template selector must be preceeded by a selector."); + } + + _parent = parent; + } + + /// + public override bool InTemplate => true; + + /// + public override Type TargetType => null; + + public override string ToString() + { + if (_selectorString == null) + { + _selectorString = _parent.ToString() + " /template/ "; + } + + return _selectorString; + } + + protected override SelectorMatch Evaluate(IStyleable control, bool subscribe) + { + IStyleable templatedParent = control.TemplatedParent as IStyleable; + + if (templatedParent == null) + { + throw new InvalidOperationException( + "Cannot call Template selector on control with null TemplatedParent."); + } + + return _parent.Match(templatedParent, subscribe) ?? SelectorMatch.True; + } + + protected override Selector MovePrevious() => null; + } +} diff --git a/src/Perspex.Styling/Styling/TypeNameAndClassSelector.cs b/src/Perspex.Styling/Styling/TypeNameAndClassSelector.cs new file mode 100644 index 0000000000..fa17aab012 --- /dev/null +++ b/src/Perspex.Styling/Styling/TypeNameAndClassSelector.cs @@ -0,0 +1,207 @@ +// Copyright (c) The Perspex 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 System.Collections.Specialized; +using System.Reactive; +using System.Reactive.Linq; +using System.Reflection; +using System.Text; + +namespace Perspex.Styling +{ + /// + /// A selector that matches the common case of a type and/or name followed by a collection of + /// style classes and pseudoclasses. + /// + internal class TypeNameAndClassSelector : Selector + { + private readonly Selector _previous; + private Type _targetType; + private Lazy> _classes = new Lazy>(() => new List()); + private string _selectorString; + + public static TypeNameAndClassSelector OfType(Selector previous, Type targetType) + { + var result = new TypeNameAndClassSelector(previous); + result._targetType = targetType; + result.IsConcreteType = true; + return result; + } + + public static TypeNameAndClassSelector Is(Selector previous, Type targetType) + { + var result = new TypeNameAndClassSelector(previous); + result._targetType = targetType; + result.IsConcreteType = false; + return result; + } + + public static TypeNameAndClassSelector ForName(Selector previous, string name) + { + var result = new TypeNameAndClassSelector(previous); + result.Name = name; + return result; + } + + public static TypeNameAndClassSelector ForClass(Selector previous, string className) + { + var result = new TypeNameAndClassSelector(previous); + result.Classes.Add(className); + return result; + } + + protected TypeNameAndClassSelector(Selector previous) + { + _previous = previous; + } + + /// + public override bool InTemplate => _previous?.InTemplate ?? false; + + /// + /// Gets the name of the control to match. + /// + public string Name { get; set; } + + /// + public override Type TargetType => _targetType ?? _previous?.TargetType; + + /// + /// Whether the selector matches the concrete or any object which + /// implements . + /// + public bool IsConcreteType { get; private set; } + + /// + /// The style classes which the selector matches. + /// + public IList Classes => _classes.Value; + + /// + public override string ToString() + { + if (_selectorString == null) + { + _selectorString = BuildSelectorString(); + } + + return _selectorString; + } + + /// + protected override SelectorMatch Evaluate(IStyleable control, bool subscribe) + { + if (TargetType != null) + { + var controlType = control.StyleKey ?? control.GetType(); + + if (IsConcreteType) + { + if (controlType != TargetType) + { + return SelectorMatch.False; + } + } + else + { + if (!TargetType.GetTypeInfo().IsAssignableFrom(controlType.GetTypeInfo())) + { + return SelectorMatch.False; + } + } + } + + if (Name != null && control.Name != Name) + { + return SelectorMatch.False; + } + + if (_classes.IsValueCreated && _classes.Value.Count > 0) + { + if (subscribe) + { + var observable = Observable.FromEventPattern< + NotifyCollectionChangedEventHandler, + NotifyCollectionChangedEventArgs>( + x => control.Classes.CollectionChanged += x, + x => control.Classes.CollectionChanged -= x) + .StartWith((EventPattern)null) + .Select(_ => Matches(control.Classes)); + return new SelectorMatch(observable); + } + else + { + return new SelectorMatch(Matches(control.Classes)); + } + } + else + { + return SelectorMatch.True; + } + } + + protected override Selector MovePrevious() => _previous; + + private bool Matches(IEnumerable classes) + { + int remaining = Classes.Count; + + foreach (var c in classes) + { + if (Classes.Contains(c)) + { + --remaining; + } + } + + return remaining == 0; + } + + private string BuildSelectorString() + { + var builder = new StringBuilder(); + + if (_previous != null) + { + builder.Append(_previous.ToString()); + } + + if (TargetType != null) + { + if (IsConcreteType) + { + builder.Append(TargetType.Name); + } + else + { + builder.Append(":is("); + builder.Append(TargetType.Name); + builder.Append(")"); + } + } + + if (Name != null) + { + builder.Append('#'); + builder.Append(Name); + } + + if (_classes.IsValueCreated && _classes.Value.Count > 0) + { + foreach (var c in Classes) + { + if (!c.StartsWith(":")) + { + builder.Append('.'); + } + + builder.Append(c); + } + } + + return builder.ToString(); + } + } +} diff --git a/src/Perspex.Themes.Default/Button.xaml b/src/Perspex.Themes.Default/Button.xaml index baf85381bc..114849b131 100644 --- a/src/Perspex.Themes.Default/Button.xaml +++ b/src/Perspex.Themes.Default/Button.xaml @@ -9,24 +9,22 @@ - - - + - - \ No newline at end of file diff --git a/src/Perspex.Themes.Default/DropDownItem.xaml b/src/Perspex.Themes.Default/DropDownItem.xaml index e2853da9aa..26bbe9959b 100644 --- a/src/Perspex.Themes.Default/DropDownItem.xaml +++ b/src/Perspex.Themes.Default/DropDownItem.xaml @@ -3,21 +3,19 @@ - - - + - - \ No newline at end of file diff --git a/src/Perspex.Themes.Default/ListBoxItem.xaml b/src/Perspex.Themes.Default/ListBoxItem.xaml index 7896e521aa..784731d57b 100644 --- a/src/Perspex.Themes.Default/ListBoxItem.xaml +++ b/src/Perspex.Themes.Default/ListBoxItem.xaml @@ -2,21 +2,19 @@ - - \ No newline at end of file diff --git a/src/Perspex.Themes.Default/PopupRoot.xaml b/src/Perspex.Themes.Default/PopupRoot.xaml index 4f620a25e2..cbd5da2081 100644 --- a/src/Perspex.Themes.Default/PopupRoot.xaml +++ b/src/Perspex.Themes.Default/PopupRoot.xaml @@ -2,11 +2,10 @@ - - - + \ No newline at end of file diff --git a/src/Perspex.Themes.Default/TabStripItem.xaml b/src/Perspex.Themes.Default/TabStripItem.xaml index 3af1f98f82..e32de4613b 100644 --- a/src/Perspex.Themes.Default/TabStripItem.xaml +++ b/src/Perspex.Themes.Default/TabStripItem.xaml @@ -4,13 +4,12 @@ - - - + Padding="{TemplateBinding Padding}"/> diff --git a/src/Perspex.Themes.Default/ToggleButton.xaml b/src/Perspex.Themes.Default/ToggleButton.xaml index 90b80129c6..03354c9b27 100644 --- a/src/Perspex.Themes.Default/ToggleButton.xaml +++ b/src/Perspex.Themes.Default/ToggleButton.xaml @@ -4,33 +4,31 @@ + - - - + - - - \ No newline at end of file diff --git a/src/Perspex.Themes.Default/TreeViewItem.xaml b/src/Perspex.Themes.Default/TreeViewItem.xaml index f5fc37f236..61dc149977 100644 --- a/src/Perspex.Themes.Default/TreeViewItem.xaml +++ b/src/Perspex.Themes.Default/TreeViewItem.xaml @@ -7,16 +7,14 @@ - - - + - - - diff --git a/tests/Perspex.Benchmarks/App.config b/tests/Perspex.Benchmarks/App.config new file mode 100644 index 0000000000..121e469348 --- /dev/null +++ b/tests/Perspex.Benchmarks/App.config @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/tests/Perspex.Benchmarks/Perspex.Benchmarks.csproj b/tests/Perspex.Benchmarks/Perspex.Benchmarks.csproj new file mode 100644 index 0000000000..253a26cfa1 --- /dev/null +++ b/tests/Perspex.Benchmarks/Perspex.Benchmarks.csproj @@ -0,0 +1,130 @@ + + + + + Debug + AnyCPU + {410AC439-81A1-4EB5-B5E9-6A7FC6B77F4B} + Exe + Properties + Perspex.Benchmarks + Perspex.Benchmarks + v4.5 + 512 + true + + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + false + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\..\packages\BenchmarkDotNet.0.9.2\lib\net45\BenchmarkDotNet.dll + True + + + + + + ..\..\packages\Moq.4.2.1510.2205\lib\net40\Moq.dll + True + + + ..\..\packages\AutoFixture.3.40.0\lib\net40\Ploeh.AutoFixture.dll + True + + + ..\..\packages\AutoFixture.AutoMoq.3.40.0\lib\net40\Ploeh.AutoFixture.AutoMoq.dll + True + + + + + + + + + + + + + + + + + + + + + + + {d211e587-d8bc-45b9-95a4-f297c8fa5200} + Perspex.Animation + + + {799a7bb5-3c2c-48b6-85a7-406a12c420da} + Perspex.Application + + + {b09b78d8-9b26-48b0-9149-d64a2f120f3f} + Perspex.Base + + + {d2221c82-4a25-4583-9b43-d791e3f6820c} + Perspex.Controls + + + {62024b2d-53eb-4638-b26b-85eeaa54866e} + Perspex.Input + + + {6b0ed19d-a08b-461c-a9d9-a9ee40b0c06b} + Perspex.Interactivity + + + {42472427-4774-4c81-8aff-9f27b8e31721} + Perspex.Layout + + + {eb582467-6abb-43a1-b052-e981ba910e3a} + Perspex.SceneGraph + + + {f1baa01a-f176-4c6a-b39d-5b40bb1b148f} + Perspex.Styling + + + {3e10a5fa-e8da-48b1-ad44-6a5b6cb7750f} + Perspex.Themes.Default + + + {88060192-33d5-4932-b0f9-8bd2763e857d} + Perspex.UnitTests + + + + + \ No newline at end of file diff --git a/tests/Perspex.Benchmarks/Program.cs b/tests/Perspex.Benchmarks/Program.cs new file mode 100644 index 0000000000..536e2ba7d3 --- /dev/null +++ b/tests/Perspex.Benchmarks/Program.cs @@ -0,0 +1,28 @@ +// Copyright (c) The Perspex Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System.Linq; +using System.Reflection; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Running; + +namespace Perspex.Benchmarks +{ + class Program + { + static void Main(string[] args) + { + // Use reflection for a more maintainable way of creating the benchmark switcher, + // Benchmarks are listed in namespace order first (e.g. BenchmarkDotNet.Samples.CPU, + // BenchmarkDotNet.Samples.IL, etc) then by name, so the output is easy to understand + var benchmarks = Assembly.GetExecutingAssembly().GetTypes() + .Where(t => t.GetMethods(BindingFlags.Instance | BindingFlags.Public) + .Any(m => m.GetCustomAttributes(typeof(BenchmarkAttribute), false).Any())) + .OrderBy(t => t.Namespace) + .ThenBy(t => t.Name) + .ToArray(); + var benchmarkSwitcher = new BenchmarkSwitcher(benchmarks); + benchmarkSwitcher.Run(args); + } + } +} diff --git a/tests/Perspex.Benchmarks/Properties/AssemblyInfo.cs b/tests/Perspex.Benchmarks/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..e8b6d6d82c --- /dev/null +++ b/tests/Perspex.Benchmarks/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Perspex.Benchmarks")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Perspex.Benchmarks")] +[assembly: AssemblyCopyright("Copyright © 2016")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("410ac439-81a1-4eb5-b5e9-6a7fc6b77f4b")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/tests/Perspex.Benchmarks/Styling/ApplyStyling.cs b/tests/Perspex.Benchmarks/Styling/ApplyStyling.cs new file mode 100644 index 0000000000..721ef5f451 --- /dev/null +++ b/tests/Perspex.Benchmarks/Styling/ApplyStyling.cs @@ -0,0 +1,68 @@ +// Copyright (c) The Perspex 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.Linq; +using BenchmarkDotNet.Attributes; +using Perspex.Controls; +using Perspex.Styling; +using Perspex.UnitTests; +using Perspex.VisualTree; + +namespace Perspex.Benchmarks.Styling +{ + public class ApplyStyling : IDisposable + { + private IDisposable _app; + private Window _window; + + public ApplyStyling() + { + _app = UnitTestApplication.Start(TestServices.StyledWindow); + + TextBox textBox; + + _window = new Window + { + Content = textBox = new TextBox(), + }; + + _window.ApplyTemplate(); + textBox.ApplyTemplate(); + + var border = (Border)textBox.GetVisualChildren().Single(); + + if (border.BorderThickness != 2) + { + throw new Exception("Styles not applied."); + } + + _window.Content = null; + + // Add a bunch of styles with lots of class selectors to complicate matters. + for (int i = 0; i < 100; ++i) + { + _window.Styles.Add(new Style(x => x.OfType().Class("foo").Class("bar").Class("baz")) + { + Setters = new[] + { + new Setter(TextBox.TextProperty, "foo"), + } + }); + } + } + + public void Dispose() + { + _app.Dispose(); + } + + [Benchmark] + public void Add_And_Style_TextBox() + { + var textBox = new TextBox(); + _window.Content = textBox; + textBox.ApplyTemplate(); + } + } +} diff --git a/tests/Perspex.Benchmarks/packages.config b/tests/Perspex.Benchmarks/packages.config new file mode 100644 index 0000000000..b1d27d4562 --- /dev/null +++ b/tests/Perspex.Benchmarks/packages.config @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/tests/Perspex.Markup.UnitTests/Perspex.Markup.UnitTests.csproj b/tests/Perspex.Markup.UnitTests/Perspex.Markup.UnitTests.csproj index 7d00e0b6b6..185013373c 100644 --- a/tests/Perspex.Markup.UnitTests/Perspex.Markup.UnitTests.csproj +++ b/tests/Perspex.Markup.UnitTests/Perspex.Markup.UnitTests.csproj @@ -102,6 +102,7 @@ + diff --git a/tests/Perspex.Markup.UnitTests/app.config b/tests/Perspex.Markup.UnitTests/app.config new file mode 100644 index 0000000000..fa66e8c206 --- /dev/null +++ b/tests/Perspex.Markup.UnitTests/app.config @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/tests/Perspex.Styling.UnitTests/SelectorTests_Child.cs b/tests/Perspex.Styling.UnitTests/SelectorTests_Child.cs index 0f054aea96..55eff92311 100644 --- a/tests/Perspex.Styling.UnitTests/SelectorTests_Child.cs +++ b/tests/Perspex.Styling.UnitTests/SelectorTests_Child.cs @@ -24,7 +24,7 @@ namespace Perspex.Styling.UnitTests child.LogicalParent = parent; - var selector = new Selector().OfType().Child().OfType(); + var selector = default(Selector).OfType().Child().OfType(); Assert.True(selector.Match(child).ImmediateResult); } @@ -39,7 +39,7 @@ namespace Perspex.Styling.UnitTests parent.LogicalParent = grandparent; child.LogicalParent = parent; - var selector = new Selector().OfType().Child().OfType(); + var selector = default(Selector).OfType().Child().OfType(); Assert.False(selector.Match(child).ImmediateResult); } @@ -52,7 +52,7 @@ namespace Perspex.Styling.UnitTests child.LogicalParent = parent; - var selector = new Selector().OfType().Class("foo").Child().OfType(); + var selector = default(Selector).OfType().Class("foo").Child().OfType(); var activator = selector.Match(child).ObservableResult; var result = new List(); @@ -67,11 +67,19 @@ namespace Perspex.Styling.UnitTests public void Child_Doesnt_Match_Control_When_It_Has_No_Parent() { var control = new TestLogical3(); - var selector = new Selector().OfType().Child().OfType(); + var selector = default(Selector).OfType().Child().OfType(); Assert.False(selector.Match(control).ImmediateResult); } + [Fact] + public void Child_Selector_Should_Have_Correct_String_Representation() + { + var selector = default(Selector).OfType().Child().OfType(); + + Assert.Equal("TestLogical1 > TestLogical3", selector.ToString()); + } + public abstract class TestLogical : ILogical, IStyleable { public TestLogical() diff --git a/tests/Perspex.Styling.UnitTests/SelectorTests_Class.cs b/tests/Perspex.Styling.UnitTests/SelectorTests_Class.cs index 3232279e5e..9eaf042034 100644 --- a/tests/Perspex.Styling.UnitTests/SelectorTests_Class.cs +++ b/tests/Perspex.Styling.UnitTests/SelectorTests_Class.cs @@ -16,7 +16,7 @@ namespace Perspex.Styling.UnitTests [Fact] public void Class_Selector_Should_Have_Correct_String_Representation() { - var target = new Selector().Class("foo"); + var target = default(Selector).Class("foo"); Assert.Equal(".foo", target.ToString()); } @@ -24,7 +24,7 @@ namespace Perspex.Styling.UnitTests [Fact] public void PesudoClass_Selector_Should_Have_Correct_String_Representation() { - var target = new Selector().Class(":foo"); + var target = default(Selector).Class(":foo"); Assert.Equal(":foo", target.ToString()); } @@ -37,7 +37,7 @@ namespace Perspex.Styling.UnitTests Classes = new Classes { "foo" }, }; - var target = new Selector().Class("foo"); + var target = default(Selector).Class("foo"); var activator = target.Match(control).ObservableResult; Assert.True(await activator.Take(1)); @@ -51,7 +51,7 @@ namespace Perspex.Styling.UnitTests Classes = new Classes { "bar" }, }; - var target = new Selector().Class("foo"); + var target = default(Selector).Class("foo"); var activator = target.Match(control).ObservableResult; Assert.False(await activator.Take(1)); @@ -66,7 +66,7 @@ namespace Perspex.Styling.UnitTests TemplatedParent = new Mock().Object, }; - var target = new Selector().Class("foo"); + var target = default(Selector).Class("foo"); var activator = target.Match(control).ObservableResult; Assert.True(await activator.Take(1)); @@ -77,7 +77,7 @@ namespace Perspex.Styling.UnitTests { var control = new Control1(); - var target = new Selector().Class("foo"); + var target = default(Selector).Class("foo"); var activator = target.Match(control).ObservableResult; Assert.False(await activator.Take(1)); @@ -93,7 +93,7 @@ namespace Perspex.Styling.UnitTests Classes = new Classes { "foo" }, }; - var target = new Selector().Class("foo"); + var target = default(Selector).Class("foo"); var activator = target.Match(control).ObservableResult; Assert.True(await activator.Take(1)); @@ -105,7 +105,7 @@ namespace Perspex.Styling.UnitTests public async Task Multiple_Classes() { var control = new Control1(); - var target = new Selector().Class("foo").Class("bar"); + var target = default(Selector).Class("foo").Class("bar"); var activator = target.Match(control).ObservableResult; Assert.False(await activator.Take(1)); diff --git a/tests/Perspex.Styling.UnitTests/SelectorTests_Descendent.cs b/tests/Perspex.Styling.UnitTests/SelectorTests_Descendent.cs index 1fb5a39167..07bb16ee14 100644 --- a/tests/Perspex.Styling.UnitTests/SelectorTests_Descendent.cs +++ b/tests/Perspex.Styling.UnitTests/SelectorTests_Descendent.cs @@ -24,7 +24,7 @@ namespace Perspex.Styling.UnitTests child.LogicalParent = parent; - var selector = new Selector().OfType().Descendent().OfType(); + var selector = default(Selector).OfType().Descendent().OfType(); Assert.True(selector.Match(child).ImmediateResult); } @@ -39,7 +39,7 @@ namespace Perspex.Styling.UnitTests parent.LogicalParent = grandparent; child.LogicalParent = parent; - var selector = new Selector().OfType().Descendent().OfType(); + var selector = default(Selector).OfType().Descendent().OfType(); Assert.True(selector.Match(child).ImmediateResult); } @@ -55,7 +55,7 @@ namespace Perspex.Styling.UnitTests parent.LogicalParent = grandparent; child.LogicalParent = parent; - var selector = new Selector().OfType().Class("foo").Descendent().OfType(); + var selector = default(Selector).OfType().Class("foo").Descendent().OfType(); var activator = selector.Match(child).ObservableResult; Assert.True(await activator.Take(1)); @@ -73,7 +73,7 @@ namespace Perspex.Styling.UnitTests parent.Classes.Add("foo"); child.LogicalParent = parent; - var selector = new Selector().OfType().Class("foo").Descendent().OfType(); + var selector = default(Selector).OfType().Class("foo").Descendent().OfType(); var activator = selector.Match(child).ObservableResult; Assert.False(await activator.Take(1)); @@ -89,7 +89,7 @@ namespace Perspex.Styling.UnitTests parent.LogicalParent = grandparent; child.LogicalParent = parent; - var selector = new Selector().OfType().Class("foo").Descendent().OfType(); + var selector = default(Selector).OfType().Class("foo").Descendent().OfType(); var activator = selector.Match(child).ObservableResult; Assert.False(await activator.Take(1)); @@ -103,6 +103,14 @@ namespace Perspex.Styling.UnitTests Assert.False(await activator.Take(1)); } + [Fact] + public void Descendent_Selector_Should_Have_Correct_String_Representation() + { + var selector = default(Selector).OfType().Class("foo").Descendent().OfType(); + + Assert.Equal("TestLogical1.foo TestLogical3", selector.ToString()); + } + public abstract class TestLogical : ILogical, IStyleable { public TestLogical() diff --git a/tests/Perspex.Styling.UnitTests/SelectorTests_Multiple.cs b/tests/Perspex.Styling.UnitTests/SelectorTests_Multiple.cs index 9b407a6de3..14a6b13b96 100644 --- a/tests/Perspex.Styling.UnitTests/SelectorTests_Multiple.cs +++ b/tests/Perspex.Styling.UnitTests/SelectorTests_Multiple.cs @@ -31,7 +31,7 @@ namespace Perspex.Styling.UnitTests control.ApplyTemplate(); - var selector = new Selector() + var selector = default(Selector) .OfType